From 1cd52b1fa90c7c39819b8288f95798f8d6911183 Mon Sep 17 00:00:00 2001 From: Alejandro Cabeza Romero Date: Wed, 23 Apr 2025 12:37:38 +0200 Subject: [PATCH 01/16] Add uniswap deployment scripts. --- sz-poc-offsite-2025/uniswap/.gitignore | 2 + .../uniswap/contracts/README.md | 2 + .../uniswap/contracts/deploy-contracts.js | 272 ++++++++++++++++++ .../uniswap/contracts/deploy-contracts.sh | 24 ++ .../uniswap/contracts/package.json | 16 ++ 5 files changed, 316 insertions(+) create mode 100644 sz-poc-offsite-2025/uniswap/.gitignore create mode 100644 sz-poc-offsite-2025/uniswap/contracts/README.md create mode 100644 sz-poc-offsite-2025/uniswap/contracts/deploy-contracts.js create mode 100755 sz-poc-offsite-2025/uniswap/contracts/deploy-contracts.sh create mode 100644 sz-poc-offsite-2025/uniswap/contracts/package.json diff --git a/sz-poc-offsite-2025/uniswap/.gitignore b/sz-poc-offsite-2025/uniswap/.gitignore new file mode 100644 index 0000000..fed401d --- /dev/null +++ b/sz-poc-offsite-2025/uniswap/.gitignore @@ -0,0 +1,2 @@ +package-lock.json +node_modules/ \ No newline at end of file diff --git a/sz-poc-offsite-2025/uniswap/contracts/README.md b/sz-poc-offsite-2025/uniswap/contracts/README.md new file mode 100644 index 0000000..304e16c --- /dev/null +++ b/sz-poc-offsite-2025/uniswap/contracts/README.md @@ -0,0 +1,2 @@ +Source: https://github.com/Ben-Haslam/uniswap-deploy-scripts +Commit: 759c56fd30b701b352f57f350c2c3f8727a368b7 diff --git a/sz-poc-offsite-2025/uniswap/contracts/deploy-contracts.js b/sz-poc-offsite-2025/uniswap/contracts/deploy-contracts.js new file mode 100644 index 0000000..191e508 --- /dev/null +++ b/sz-poc-offsite-2025/uniswap/contracts/deploy-contracts.js @@ -0,0 +1,272 @@ +const Web3 = require("web3"); +const Factory = require("./node_modules/@uniswap/v2-core/build/UniswapV2Factory.json"); +const Router = require("./node_modules/@uniswap/v2-periphery/build/UniswapV2Router02.json"); +const ERC20 = require("./node_modules/@openzeppelin/contracts/build/contracts/ERC20PresetFixedSupply.json"); +const Pair = require("./node_modules/@uniswap/v2-core/build/UniswapV2Pair.json"); +const WETH = require("./node_modules/canonical-weth/build/contracts/WETH9.json"); + +const RPC = "http://localhost:8545"; +const prvKey = "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"; + +const GasPrice = 0.000005; +const GasLimit = 6000000; + +// deploy Weth +async function deployWeth(web3, sender) { + try { + let weth = new web3.eth.Contract(WETH.abi); + weth = await weth + .deploy({ data: WETH.bytecode }) + .send({ from: sender, gas: GasLimit, gasprice: GasPrice }) + + console.log("Weth address:", weth.options.address); + + return weth.options.address; + } catch (error) { + console.log('Weth deployment went wrong! Lets see what happened...') + console.log(error) + } +} + +// deploy two ERC20 contracts +async function deployTokens(web3, sender) { + try { + let tokenMem = new web3.eth.Contract(ERC20.abi); + let tokenNet = new web3.eth.Contract(ERC20.abi); + + tokenMem = await tokenMem + .deploy({ + data: ERC20.bytecode, + arguments: [ + "Mehmet", + "MEM", + // 18, + web3.utils.toWei("9999999999999999999", "ether"), + sender, + ], + }) + .send({ from: sender, gas: GasLimit, gasprice: GasPrice }); + + console.log("MEM Token address:", tokenMem.options.address); + + tokenNet = await tokenNet + .deploy({ + data: ERC20.bytecode, + arguments: [ + "New Ether", + "NET", + // 18, + web3.utils.toWei("9999999999999999999", "ether"), + sender, + ], + }) + .send({ from: sender, gas: GasLimit, gasprice: GasPrice }); + + console.log("NET Token address:", tokenNet.options.address); + + return [tokenMem.options.address, tokenNet.options.address]; + } catch (error) { + console.log('ERC20 deployment went wrong! Lets see what happened...') + console.log(error) + } + +} + +// deploy a uniswapV2Router +async function deployRouter(web3, factoryAddress, wethAddress, sender) { + try { + let router = new web3.eth.Contract(Router.abi); + router = await router + .deploy({ data: Router.bytecode, arguments: [factoryAddress, wethAddress] }) + .send({ from: sender, gas: GasLimit, gasprice: GasPrice }); + + console.log("Router address:", router.options.address); + + return router.options.address; + } catch (error) { + console.log('Router deployment went wrong! Lets see what happened...') + console.log(error) + } + +} + +// deploy a uniswapV2Factory +async function deployFactory(web3, feeToSetter, sender) { + try { + let factory = new web3.eth.Contract(Factory.abi); + factory = await factory + .deploy({ data: Factory.bytecode, arguments: [feeToSetter] }) + .send({ from: sender, gas: GasLimit, gasprice: GasPrice }); + + console.log("Factory address:", factory.options.address); + + return factory.options.address; + } catch (error) { + console.log('Factory deployment went wrong! Lets see what happened...') + console.log(error) + } + +} + +async function approve(tokenContract, spender, amount, sender) { + try { + await tokenContract.methods + .approve(spender, amount) + .send({ from: sender, gas: GasLimit, gasprice: GasPrice }) + .on("transactionHash", function (hash) { + console.log("transaction hash", hash); + }) + .on("receipt", function (receipt) { + console.log("receipt", receipt); + }); + } catch (err) { + console.log("the approve transaction reverted! Lets see why..."); + + await tokenContract.methods + .approve(spender, amount) + .call({ from: sender, gas: GasLimit, gasprice: GasPrice }); + } +} + +// check some stuff on a deployed uniswapV2Pair +async function checkPair( + web3, + factoryContract, + tokenMemAddress, + tokenNetAddress, + sender, + routerAddress +) { + try { + console.log("tokenMemAddress: ", tokenMemAddress); + console.log("tokenNetAddress: ", tokenNetAddress); + + const pairAddress = await factoryContract.methods + .getPair(tokenMemAddress, tokenNetAddress) + .call(); + + console.log("tokenMem Address", tokenMemAddress); + console.log("tokenNet Address", tokenNetAddress); + console.log("pairAddress", pairAddress); + console.log("router address", routerAddress); + + const pair = new web3.eth.Contract(Pair.abi, pairAddress); + + const reserves = await pair.methods.getReserves().call(); + + console.log("reserves for tokenMem", web3.utils.fromWei(reserves._reserve0)); + console.log("reserves for tokenNet", web3.utils.fromWei(reserves._reserve1)); + } catch (err) { + console.log("the check pair reverted! Lets see why..."); + console.log(err); + } +} + +async function deployUniswap() { + const web3 = new Web3(RPC); + const account = web3.eth.accounts.wallet.add(prvKey); + const myAddress = web3.utils.toChecksumAddress(account.address); + + const wethAddress = await deployWeth(web3, myAddress); + const weth = new web3.eth.Contract(WETH.abi, wethAddress); + + const factoryAddress = await deployFactory(web3, myAddress, myAddress); + const factory = new web3.eth.Contract(Factory.abi, factoryAddress); + + const routerAddress = await deployRouter( + web3, + factoryAddress, + wethAddress, + myAddress + ); + const router = new web3.eth.Contract(Router.abi, routerAddress); + + // const multicallAddress = await deployMulticall(web3, myAddress); + // const multicall = new web3.eth.Contract(Multicall.abi, multicallAddress); + const [tokenMemAddress, tokenNetAddress] = await deployTokens(web3, myAddress); + + const tokenMem = new web3.eth.Contract(ERC20.abi, tokenMemAddress); + const tokenNet = new web3.eth.Contract(ERC20.abi, tokenNetAddress); + + return (tokenMem, tokenMemAddress, tokenNet, tokenNetAddress, myAddress, web3, router, routerAddress, factory, weth, wethAddress) +} + +async function addLiquidity(tokenA, tokenAAddress, tokenB, tokenBAddress, myAddress, web3, router, routerAddress, factory, weth, wethAddress) { + // liquidity + const amountADesired = web3.utils.toWei("10000000", "ether"); + const amountBDesired = web3.utils.toWei("10000000", "ether"); + const amountAMin = web3.utils.toWei("0", "ether"); + const amountBMin = web3.utils.toWei("0", "ether"); + + // deadline + var BN = web3.utils.BN; + const time = Math.floor(Date.now() / 1000) + 200000; + const deadline = new BN(time); + + // before calling addLiquidity we need to approve the router + // we need to approve atleast amountADesired and amountBDesired + const spender = router.options.address; + const amountA = amountADesired; + const amountB = amountBDesired; + + await approve(tokenA, spender, amountA, myAddress); + await approve(tokenB, spender, amountB, myAddress); + await approve(weth, wethAddress, amountA, myAddress); + await approve(weth, spender, amountA, myAddress); + + // try to add liquidity to a non-existen pair contract + try { + await router.methods + .addLiquidity( + tokenAAddress, + tokenBAddress, + amountADesired, + amountBDesired, + amountAMin, + amountBMin, + myAddress, + deadline + ) + .send({ + from: myAddress, + gas: GasLimit, + gasprice: GasPrice, + }) + .on("transactionHash", function (hash) { + console.log("transaction hash", hash); + }) + .on("receipt", function (receipt) { + console.log("receipt", receipt); + }); + } catch (err) { + console.log("the addLiquidity transaction reverted! Lets see why..."); + + await router.methods + .addLiquidity( + tokenAAddress, + tokenBAddress, + amountADesired, + amountBDesired, + amountAMin, + amountBMin, + myAddress, + deadline + ) + .call({ + from: myAddress, + gas: GasLimit, + gasprice: GasPrice, + }); + } + + await checkPair( + web3, + factory, + tokenAAddress, + tokenBAddress, + myAddress, + routerAddress + ); +} + +deployUniswap(); diff --git a/sz-poc-offsite-2025/uniswap/contracts/deploy-contracts.sh b/sz-poc-offsite-2025/uniswap/contracts/deploy-contracts.sh new file mode 100755 index 0000000..e00741a --- /dev/null +++ b/sz-poc-offsite-2025/uniswap/contracts/deploy-contracts.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +# Define colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +SCRIPT_DIR=$(dirname $(realpath "${BASH_SOURCE[0]}")) +cd $SCRIPT_DIR + +echo -e "${BLUE}[Step]${NC} ${YELLOW}Installing dependencies${NC}" +npm install + +echo "" +echo -e "${BLUE}[Step]${NC} ${YELLOW}Deploying contracts to http://localhost:8545${NC}" +node deploy-contracts.js + +if [ $? -eq 0 ]; then + echo -e "\n${GREEN}✓ Uniswap Deployment Successful${NC}" +else + echo -e "\n${RED}✗ Uniswap Deployment Failed${NC}" +fi \ No newline at end of file diff --git a/sz-poc-offsite-2025/uniswap/contracts/package.json b/sz-poc-offsite-2025/uniswap/contracts/package.json new file mode 100644 index 0000000..c2aa1c1 --- /dev/null +++ b/sz-poc-offsite-2025/uniswap/contracts/package.json @@ -0,0 +1,16 @@ +{ + "name": "uniswap_test", + "version": "1.0.0", + "description": "For testing uniswap on autonity", + "dependencies": { + "@openzeppelin/contracts": "^3.4.0", + "@uniswap/v2-core": "^1.0.1", + "@uniswap/v2-periphery": "^1.1.0-beta.0", + "@uniswap/v2-sdk": "^3.0.0", + "canonical-weth": "^1.4.0", + "truffle-contract": "^4.0.31", + "web3": "^1.3.4" + }, + "author": "Ben Haslam", + "license": "ISC" +} From e47c9f78a203ecf4741d6cce6a4ef2b39a6b1517 Mon Sep 17 00:00:00 2001 From: Alejandro Cabeza Romero Date: Wed, 23 Apr 2025 12:44:10 +0200 Subject: [PATCH 02/16] Add alternative uniswap interface. --- .../uniswap/nomiswap/.gitignore | 1 + sz-poc-offsite-2025/uniswap/nomiswap/LICENSE | 674 + .../uniswap/nomiswap/README.md | 47 + .../nomiswap/build/asset-manifest.json | 25 + .../uniswap/nomiswap/build/favicon.ico | Bin 0 -> 3870 bytes .../uniswap/nomiswap/build/index.html | 1 + .../uniswap/nomiswap/build/logo192.png | Bin 0 -> 5347 bytes .../uniswap/nomiswap/build/logo512.png | Bin 0 -> 9664 bytes .../uniswap/nomiswap/build/manifest.json | 25 + .../uniswap/nomiswap/build/robots.txt | 3 + .../build/static/css/2.3ab4f688.chunk.css | 8 + .../build/static/css/2.3ab4f688.chunk.css.map | 1 + .../build/static/css/main.797d0e06.chunk.css | 2 + .../static/css/main.797d0e06.chunk.css.map | 1 + .../build/static/js/2.de1e7dab.chunk.js | 3 + .../static/js/2.de1e7dab.chunk.js.LICENSE.txt | 76 + .../build/static/js/2.de1e7dab.chunk.js.map | 1 + .../build/static/js/3.c38e175b.chunk.js | 2 + .../build/static/js/3.c38e175b.chunk.js.map | 1 + .../build/static/js/main.db23c065.chunk.js | 2 + .../static/js/main.db23c065.chunk.js.map | 1 + .../build/static/js/runtime-main.99be9be0.js | 2 + .../static/js/runtime-main.99be9be0.js.map | 1 + .../uniswap/nomiswap/package.json | 55 + .../uniswap/nomiswap/public/favicon.ico | Bin 0 -> 3870 bytes .../uniswap/nomiswap/public/index.html | 43 + .../uniswap/nomiswap/public/logo192.png | Bin 0 -> 5347 bytes .../uniswap/nomiswap/public/logo512.png | Bin 0 -> 9664 bytes .../uniswap/nomiswap/public/manifest.json | 25 + .../uniswap/nomiswap/public/robots.txt | 3 + .../uniswap/nomiswap/src/App.css | 71 + .../uniswap/nomiswap/src/App.js | 50 + .../nomiswap/src/CoinSwapper/CoinButton.js | 40 + .../nomiswap/src/CoinSwapper/CoinDialog.js | 179 + .../nomiswap/src/CoinSwapper/CoinField.js | 189 + .../nomiswap/src/CoinSwapper/CoinSwapper.js | 428 + .../nomiswap/src/Components/LoadingButton.js | 40 + .../src/Components/connectWalletPage.js | 85 + .../nomiswap/src/Components/wrongNetwork.js | 35 + .../nomiswap/src/Liquidity/Liquidity.js | 74 + .../src/Liquidity/LiquidityDeployer.js | 503 + .../src/Liquidity/LiquidityFunctions.js | 364 + .../nomiswap/src/Liquidity/RemoveLiquidity.js | 480 + .../nomiswap/src/Liquidity/SwitchButton.js | 52 + .../uniswap/nomiswap/src/NavBar/MenuItems.js | 12 + .../uniswap/nomiswap/src/NavBar/NavBar.css | 62 + .../uniswap/nomiswap/src/NavBar/NavBar.js | 36 + .../nomiswap/src/build/Babylonian.json | 19 + .../nomiswap/src/build/DeflatingERC20.json | 341 + .../uniswap/nomiswap/src/build/ERC20.json | 341 + .../nomiswap/src/build/ExampleFlashSwap.json | 128 + .../src/build/ExampleOracleSimple.json | 203 + .../src/build/ExampleSlidingWindowOracle.json | 258 + .../src/build/ExampleSwapToPrice.json | 174 + .../nomiswap/src/build/FixedPoint.json | 19 + .../uniswap/nomiswap/src/build/IERC20.json | 242 + .../src/build/IUniswapV1Exchange.json | 160 + .../nomiswap/src/build/IUniswapV1Factory.json | 39 + .../nomiswap/src/build/IUniswapV2Callee.json | 48 + .../nomiswap/src/build/IUniswapV2Factory.json | 183 + .../src/build/IUniswapV2Migrator.json | 53 + .../nomiswap/src/build/IUniswapV2Pair.json | 671 + .../src/build/IUniswapV2Router01.json | 769 + .../src/build/IUniswapV2Router02.json | 971 + .../uniswap/nomiswap/src/build/IWETH.json | 64 + .../src/build/RouterEventEmitter.json | 255 + .../uniswap/nomiswap/src/build/SafeMath.json | 19 + .../nomiswap/src/build/TransferHelper.json | 19 + .../nomiswap/src/build/UniswapV2Library.json | 19 + .../nomiswap/src/build/UniswapV2Migrator.json | 94 + .../src/build/UniswapV2OracleLibrary.json | 19 + .../nomiswap/src/build/UniswapV2Router01.json | 970 + .../nomiswap/src/build/UniswapV2Router02.json | 1239 ++ .../uniswap/nomiswap/src/build/WETH9.json | 300 + .../uniswap/nomiswap/src/constants/chains.js | 25 + .../uniswap/nomiswap/src/constants/coins.js | 254 + .../uniswap/nomiswap/src/ethereumFunctions.js | 281 + .../uniswap/nomiswap/src/index.css | 0 .../uniswap/nomiswap/src/index.js | 22 + .../uniswap/nomiswap/src/network.js | 143 + .../uniswap/nomiswap/src/reportWebVitals.js | 13 + .../uniswap/nomiswap/yarn.lock | 15742 ++++++++++++++++ 82 files changed, 27800 insertions(+) create mode 100644 sz-poc-offsite-2025/uniswap/nomiswap/.gitignore create mode 100644 sz-poc-offsite-2025/uniswap/nomiswap/LICENSE create mode 100644 sz-poc-offsite-2025/uniswap/nomiswap/README.md create mode 100644 sz-poc-offsite-2025/uniswap/nomiswap/build/asset-manifest.json create mode 100644 sz-poc-offsite-2025/uniswap/nomiswap/build/favicon.ico create mode 100644 sz-poc-offsite-2025/uniswap/nomiswap/build/index.html create mode 100644 sz-poc-offsite-2025/uniswap/nomiswap/build/logo192.png create mode 100644 sz-poc-offsite-2025/uniswap/nomiswap/build/logo512.png create mode 100644 sz-poc-offsite-2025/uniswap/nomiswap/build/manifest.json create mode 100644 sz-poc-offsite-2025/uniswap/nomiswap/build/robots.txt create mode 100644 sz-poc-offsite-2025/uniswap/nomiswap/build/static/css/2.3ab4f688.chunk.css create mode 100644 sz-poc-offsite-2025/uniswap/nomiswap/build/static/css/2.3ab4f688.chunk.css.map create mode 100644 sz-poc-offsite-2025/uniswap/nomiswap/build/static/css/main.797d0e06.chunk.css create mode 100644 sz-poc-offsite-2025/uniswap/nomiswap/build/static/css/main.797d0e06.chunk.css.map create mode 100644 sz-poc-offsite-2025/uniswap/nomiswap/build/static/js/2.de1e7dab.chunk.js create mode 100644 sz-poc-offsite-2025/uniswap/nomiswap/build/static/js/2.de1e7dab.chunk.js.LICENSE.txt create mode 100644 sz-poc-offsite-2025/uniswap/nomiswap/build/static/js/2.de1e7dab.chunk.js.map create mode 100644 sz-poc-offsite-2025/uniswap/nomiswap/build/static/js/3.c38e175b.chunk.js create mode 100644 sz-poc-offsite-2025/uniswap/nomiswap/build/static/js/3.c38e175b.chunk.js.map create mode 100644 sz-poc-offsite-2025/uniswap/nomiswap/build/static/js/main.db23c065.chunk.js create mode 100644 sz-poc-offsite-2025/uniswap/nomiswap/build/static/js/main.db23c065.chunk.js.map create mode 100644 sz-poc-offsite-2025/uniswap/nomiswap/build/static/js/runtime-main.99be9be0.js create mode 100644 sz-poc-offsite-2025/uniswap/nomiswap/build/static/js/runtime-main.99be9be0.js.map create mode 100644 sz-poc-offsite-2025/uniswap/nomiswap/package.json create mode 100644 sz-poc-offsite-2025/uniswap/nomiswap/public/favicon.ico create mode 100644 sz-poc-offsite-2025/uniswap/nomiswap/public/index.html create mode 100644 sz-poc-offsite-2025/uniswap/nomiswap/public/logo192.png create mode 100644 sz-poc-offsite-2025/uniswap/nomiswap/public/logo512.png create mode 100644 sz-poc-offsite-2025/uniswap/nomiswap/public/manifest.json create mode 100644 sz-poc-offsite-2025/uniswap/nomiswap/public/robots.txt create mode 100644 sz-poc-offsite-2025/uniswap/nomiswap/src/App.css create mode 100644 sz-poc-offsite-2025/uniswap/nomiswap/src/App.js create mode 100644 sz-poc-offsite-2025/uniswap/nomiswap/src/CoinSwapper/CoinButton.js create mode 100644 sz-poc-offsite-2025/uniswap/nomiswap/src/CoinSwapper/CoinDialog.js create mode 100644 sz-poc-offsite-2025/uniswap/nomiswap/src/CoinSwapper/CoinField.js create mode 100644 sz-poc-offsite-2025/uniswap/nomiswap/src/CoinSwapper/CoinSwapper.js create mode 100644 sz-poc-offsite-2025/uniswap/nomiswap/src/Components/LoadingButton.js create mode 100644 sz-poc-offsite-2025/uniswap/nomiswap/src/Components/connectWalletPage.js create mode 100644 sz-poc-offsite-2025/uniswap/nomiswap/src/Components/wrongNetwork.js create mode 100644 sz-poc-offsite-2025/uniswap/nomiswap/src/Liquidity/Liquidity.js create mode 100644 sz-poc-offsite-2025/uniswap/nomiswap/src/Liquidity/LiquidityDeployer.js create mode 100644 sz-poc-offsite-2025/uniswap/nomiswap/src/Liquidity/LiquidityFunctions.js create mode 100644 sz-poc-offsite-2025/uniswap/nomiswap/src/Liquidity/RemoveLiquidity.js create mode 100644 sz-poc-offsite-2025/uniswap/nomiswap/src/Liquidity/SwitchButton.js create mode 100644 sz-poc-offsite-2025/uniswap/nomiswap/src/NavBar/MenuItems.js create mode 100644 sz-poc-offsite-2025/uniswap/nomiswap/src/NavBar/NavBar.css create mode 100644 sz-poc-offsite-2025/uniswap/nomiswap/src/NavBar/NavBar.js create mode 100644 sz-poc-offsite-2025/uniswap/nomiswap/src/build/Babylonian.json create mode 100644 sz-poc-offsite-2025/uniswap/nomiswap/src/build/DeflatingERC20.json create mode 100644 sz-poc-offsite-2025/uniswap/nomiswap/src/build/ERC20.json create mode 100644 sz-poc-offsite-2025/uniswap/nomiswap/src/build/ExampleFlashSwap.json create mode 100644 sz-poc-offsite-2025/uniswap/nomiswap/src/build/ExampleOracleSimple.json create mode 100644 sz-poc-offsite-2025/uniswap/nomiswap/src/build/ExampleSlidingWindowOracle.json create mode 100644 sz-poc-offsite-2025/uniswap/nomiswap/src/build/ExampleSwapToPrice.json create mode 100644 sz-poc-offsite-2025/uniswap/nomiswap/src/build/FixedPoint.json create mode 100644 sz-poc-offsite-2025/uniswap/nomiswap/src/build/IERC20.json create mode 100644 sz-poc-offsite-2025/uniswap/nomiswap/src/build/IUniswapV1Exchange.json create mode 100644 sz-poc-offsite-2025/uniswap/nomiswap/src/build/IUniswapV1Factory.json create mode 100644 sz-poc-offsite-2025/uniswap/nomiswap/src/build/IUniswapV2Callee.json create mode 100644 sz-poc-offsite-2025/uniswap/nomiswap/src/build/IUniswapV2Factory.json create mode 100644 sz-poc-offsite-2025/uniswap/nomiswap/src/build/IUniswapV2Migrator.json create mode 100644 sz-poc-offsite-2025/uniswap/nomiswap/src/build/IUniswapV2Pair.json create mode 100644 sz-poc-offsite-2025/uniswap/nomiswap/src/build/IUniswapV2Router01.json create mode 100644 sz-poc-offsite-2025/uniswap/nomiswap/src/build/IUniswapV2Router02.json create mode 100644 sz-poc-offsite-2025/uniswap/nomiswap/src/build/IWETH.json create mode 100644 sz-poc-offsite-2025/uniswap/nomiswap/src/build/RouterEventEmitter.json create mode 100644 sz-poc-offsite-2025/uniswap/nomiswap/src/build/SafeMath.json create mode 100644 sz-poc-offsite-2025/uniswap/nomiswap/src/build/TransferHelper.json create mode 100644 sz-poc-offsite-2025/uniswap/nomiswap/src/build/UniswapV2Library.json create mode 100644 sz-poc-offsite-2025/uniswap/nomiswap/src/build/UniswapV2Migrator.json create mode 100644 sz-poc-offsite-2025/uniswap/nomiswap/src/build/UniswapV2OracleLibrary.json create mode 100644 sz-poc-offsite-2025/uniswap/nomiswap/src/build/UniswapV2Router01.json create mode 100644 sz-poc-offsite-2025/uniswap/nomiswap/src/build/UniswapV2Router02.json create mode 100644 sz-poc-offsite-2025/uniswap/nomiswap/src/build/WETH9.json create mode 100644 sz-poc-offsite-2025/uniswap/nomiswap/src/constants/chains.js create mode 100644 sz-poc-offsite-2025/uniswap/nomiswap/src/constants/coins.js create mode 100644 sz-poc-offsite-2025/uniswap/nomiswap/src/ethereumFunctions.js create mode 100644 sz-poc-offsite-2025/uniswap/nomiswap/src/index.css create mode 100644 sz-poc-offsite-2025/uniswap/nomiswap/src/index.js create mode 100644 sz-poc-offsite-2025/uniswap/nomiswap/src/network.js create mode 100644 sz-poc-offsite-2025/uniswap/nomiswap/src/reportWebVitals.js create mode 100644 sz-poc-offsite-2025/uniswap/nomiswap/yarn.lock diff --git a/sz-poc-offsite-2025/uniswap/nomiswap/.gitignore b/sz-poc-offsite-2025/uniswap/nomiswap/.gitignore new file mode 100644 index 0000000..c2658d7 --- /dev/null +++ b/sz-poc-offsite-2025/uniswap/nomiswap/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/sz-poc-offsite-2025/uniswap/nomiswap/LICENSE b/sz-poc-offsite-2025/uniswap/nomiswap/LICENSE new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/sz-poc-offsite-2025/uniswap/nomiswap/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/sz-poc-offsite-2025/uniswap/nomiswap/README.md b/sz-poc-offsite-2025/uniswap/nomiswap/README.md new file mode 100644 index 0000000..3891d05 --- /dev/null +++ b/sz-poc-offsite-2025/uniswap/nomiswap/README.md @@ -0,0 +1,47 @@ +Source: https://github.com/Ben-Haslam/Alternative-Uniswap-Interface\ +Commit: 830e8ed3b3dec7cbb1dbe19b9bb1eae2f591b742 +--- + +# NOTE: This is a proof-of-concept. Use it at your own risk, and it's not intended for any sort of production use. + +This is an alternative interface to Uniswap V2 contracts deployed on an EVM blockchain. We used ReactJS for the project, with the EthersJS module to connect to the blockchain via metamask in the browser, and Material-UI for the frontend. As it was a static site, we used github pages to host the application. + + +Publishes to [https://ben-haslam.github.io/Alternative-Uniswap-Interface/]() + +Check out the blog [here](https://medium.com/clearmatics/how-i-made-a-uniswap-interface-from-scratch-b51e1027ca87) + +# Getting Started with Create React App + +This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). + +## Available Scripts + +In the project directory, you can run: + +### `yarn start` + +Runs the app in the development mode.\ +Open [http://localhost:3000](http://localhost:3000) to view it in the browser. + +The page will reload if you make edits.\ +You will also see any lint errors in the console. + +### `yarn test` + +Launches the test runner in the interactive watch mode.\ +See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. + +### `yarn build` + +Builds the app for production to the `build` folder.\ +It correctly bundles React in production mode and optimizes the build for the best performance. + +The build is minified and the filenames include the hashes.\ +Your app is ready to be deployed! + +See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. + +### `yarn eject` + +**Note: this is a one-way operation. Once you `eject`, you can’t go back!** diff --git a/sz-poc-offsite-2025/uniswap/nomiswap/build/asset-manifest.json b/sz-poc-offsite-2025/uniswap/nomiswap/build/asset-manifest.json new file mode 100644 index 0000000..9827f6f --- /dev/null +++ b/sz-poc-offsite-2025/uniswap/nomiswap/build/asset-manifest.json @@ -0,0 +1,25 @@ +{ + "files": { + "main.css": "/Alternative-Uniswap-Interface/static/css/main.797d0e06.chunk.css", + "main.js": "/Alternative-Uniswap-Interface/static/js/main.db23c065.chunk.js", + "main.js.map": "/Alternative-Uniswap-Interface/static/js/main.db23c065.chunk.js.map", + "runtime-main.js": "/Alternative-Uniswap-Interface/static/js/runtime-main.99be9be0.js", + "runtime-main.js.map": "/Alternative-Uniswap-Interface/static/js/runtime-main.99be9be0.js.map", + "static/css/2.3ab4f688.chunk.css": "/Alternative-Uniswap-Interface/static/css/2.3ab4f688.chunk.css", + "static/js/2.de1e7dab.chunk.js": "/Alternative-Uniswap-Interface/static/js/2.de1e7dab.chunk.js", + "static/js/2.de1e7dab.chunk.js.map": "/Alternative-Uniswap-Interface/static/js/2.de1e7dab.chunk.js.map", + "static/js/3.c38e175b.chunk.js": "/Alternative-Uniswap-Interface/static/js/3.c38e175b.chunk.js", + "static/js/3.c38e175b.chunk.js.map": "/Alternative-Uniswap-Interface/static/js/3.c38e175b.chunk.js.map", + "index.html": "/Alternative-Uniswap-Interface/index.html", + "static/css/2.3ab4f688.chunk.css.map": "/Alternative-Uniswap-Interface/static/css/2.3ab4f688.chunk.css.map", + "static/css/main.797d0e06.chunk.css.map": "/Alternative-Uniswap-Interface/static/css/main.797d0e06.chunk.css.map", + "static/js/2.de1e7dab.chunk.js.LICENSE.txt": "/Alternative-Uniswap-Interface/static/js/2.de1e7dab.chunk.js.LICENSE.txt" + }, + "entrypoints": [ + "static/js/runtime-main.99be9be0.js", + "static/css/2.3ab4f688.chunk.css", + "static/js/2.de1e7dab.chunk.js", + "static/css/main.797d0e06.chunk.css", + "static/js/main.db23c065.chunk.js" + ] +} \ No newline at end of file diff --git a/sz-poc-offsite-2025/uniswap/nomiswap/build/favicon.ico b/sz-poc-offsite-2025/uniswap/nomiswap/build/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..a11777cc471a4344702741ab1c8a588998b1311a GIT binary patch literal 3870 zcma);c{J4h9>;%nil|2-o+rCuEF-(I%-F}ijC~o(k~HKAkr0)!FCj~d>`RtpD?8b; zXOC1OD!V*IsqUwzbMF1)-gEDD=A573Z-&G7^LoAC9|WO7Xc0Cx1g^Zu0u_SjAPB3vGa^W|sj)80f#V0@M_CAZTIO(t--xg= z!sii`1giyH7EKL_+Wi0ab<)&E_0KD!3Rp2^HNB*K2@PHCs4PWSA32*-^7d{9nH2_E zmC{C*N*)(vEF1_aMamw2A{ZH5aIDqiabnFdJ|y0%aS|64E$`s2ccV~3lR!u<){eS` z#^Mx6o(iP1Ix%4dv`t@!&Za-K@mTm#vadc{0aWDV*_%EiGK7qMC_(`exc>-$Gb9~W!w_^{*pYRm~G zBN{nA;cm^w$VWg1O^^<6vY`1XCD|s_zv*g*5&V#wv&s#h$xlUilPe4U@I&UXZbL z0)%9Uj&@yd03n;!7do+bfixH^FeZ-Ema}s;DQX2gY+7g0s(9;`8GyvPY1*vxiF&|w z>!vA~GA<~JUqH}d;DfBSi^IT*#lrzXl$fNpq0_T1tA+`A$1?(gLb?e#0>UELvljtQ zK+*74m0jn&)5yk8mLBv;=@}c{t0ztT<v;Avck$S6D`Z)^c0(jiwKhQsn|LDRY&w(Fmi91I7H6S;b0XM{e zXp0~(T@k_r-!jkLwd1_Vre^v$G4|kh4}=Gi?$AaJ)3I+^m|Zyj#*?Kp@w(lQdJZf4 z#|IJW5z+S^e9@(6hW6N~{pj8|NO*>1)E=%?nNUAkmv~OY&ZV;m-%?pQ_11)hAr0oAwILrlsGawpxx4D43J&K=n+p3WLnlDsQ$b(9+4 z?mO^hmV^F8MV{4Lx>(Q=aHhQ1){0d*(e&s%G=i5rq3;t{JC zmgbn5Nkl)t@fPH$v;af26lyhH!k+#}_&aBK4baYPbZy$5aFx4}ka&qxl z$=Rh$W;U)>-=S-0=?7FH9dUAd2(q#4TCAHky!$^~;Dz^j|8_wuKc*YzfdAht@Q&ror?91Dm!N03=4=O!a)I*0q~p0g$Fm$pmr$ zb;wD;STDIi$@M%y1>p&_>%?UP($15gou_ue1u0!4(%81;qcIW8NyxFEvXpiJ|H4wz z*mFT(qVx1FKufG11hByuX%lPk4t#WZ{>8ka2efjY`~;AL6vWyQKpJun2nRiZYDij$ zP>4jQXPaP$UC$yIVgGa)jDV;F0l^n(V=HMRB5)20V7&r$jmk{UUIe zVjKroK}JAbD>B`2cwNQ&GDLx8{pg`7hbA~grk|W6LgiZ`8y`{Iq0i>t!3p2}MS6S+ zO_ruKyAElt)rdS>CtF7j{&6rP-#c=7evGMt7B6`7HG|-(WL`bDUAjyn+k$mx$CH;q2Dz4x;cPP$hW=`pFfLO)!jaCL@V2+F)So3}vg|%O*^T1j>C2lx zsURO-zIJC$^$g2byVbRIo^w>UxK}74^TqUiRR#7s_X$e)$6iYG1(PcW7un-va-S&u zHk9-6Zn&>T==A)lM^D~bk{&rFzCi35>UR!ZjQkdSiNX*-;l4z9j*7|q`TBl~Au`5& z+c)*8?#-tgUR$Zd%Q3bs96w6k7q@#tUn`5rj+r@_sAVVLqco|6O{ILX&U-&-cbVa3 zY?ngHR@%l{;`ri%H*0EhBWrGjv!LE4db?HEWb5mu*t@{kv|XwK8?npOshmzf=vZA@ zVSN9sL~!sn?r(AK)Q7Jk2(|M67Uy3I{eRy z_l&Y@A>;vjkWN5I2xvFFTLX0i+`{qz7C_@bo`ZUzDugfq4+>a3?1v%)O+YTd6@Ul7 zAfLfm=nhZ`)P~&v90$&UcF+yXm9sq!qCx3^9gzIcO|Y(js^Fj)Rvq>nQAHI92ap=P z10A4@prk+AGWCb`2)dQYFuR$|H6iDE8p}9a?#nV2}LBCoCf(Xi2@szia7#gY>b|l!-U`c}@ zLdhvQjc!BdLJvYvzzzngnw51yRYCqh4}$oRCy-z|v3Hc*d|?^Wj=l~18*E~*cR_kU z{XsxM1i{V*4GujHQ3DBpl2w4FgFR48Nma@HPgnyKoIEY-MqmMeY=I<%oG~l!f<+FN z1ZY^;10j4M4#HYXP zw5eJpA_y(>uLQ~OucgxDLuf}fVs272FaMxhn4xnDGIyLXnw>Xsd^J8XhcWIwIoQ9} z%FoSJTAGW(SRGwJwb=@pY7r$uQRK3Zd~XbxU)ts!4XsJrCycrWSI?e!IqwqIR8+Jh zlRjZ`UO1I!BtJR_2~7AbkbSm%XQqxEPkz6BTGWx8e}nQ=w7bZ|eVP4?*Tb!$(R)iC z9)&%bS*u(lXqzitAN)Oo=&Ytn>%Hzjc<5liuPi>zC_nw;Z0AE3Y$Jao_Q90R-gl~5 z_xAb2J%eArrC1CN4G$}-zVvCqF1;H;abAu6G*+PDHSYFx@Tdbfox*uEd3}BUyYY-l zTfEsOqsi#f9^FoLO;ChK<554qkri&Av~SIM*{fEYRE?vH7pTAOmu2pz3X?Wn*!ROX ztd54huAk&mFBemMooL33RV-*1f0Q3_(7hl$<#*|WF9P!;r;4_+X~k~uKEqdzZ$5Al zV63XN@)j$FN#cCD;ek1R#l zv%pGrhB~KWgoCj%GT?%{@@o(AJGt*PG#l3i>lhmb_twKH^EYvacVY-6bsCl5*^~L0 zonm@lk2UvvTKr2RS%}T>^~EYqdL1q4nD%0n&Xqr^cK^`J5W;lRRB^R-O8b&HENO||mo0xaD+S=I8RTlIfVgqN@SXDr2&-)we--K7w= zJVU8?Z+7k9dy;s;^gDkQa`0nz6N{T?(A&Iz)2!DEecLyRa&FI!id#5Z7B*O2=PsR0 zEvc|8{NS^)!d)MDX(97Xw}m&kEO@5jqRaDZ!+%`wYOI<23q|&js`&o4xvjP7D_xv@ z5hEwpsp{HezI9!~6O{~)lLR@oF7?J7i>1|5a~UuoN=q&6N}EJPV_GD`&M*v8Y`^2j zKII*d_@Fi$+i*YEW+Hbzn{iQk~yP z>7N{S4)r*!NwQ`(qcN#8SRQsNK6>{)X12nbF`*7#ecO7I)Q$uZsV+xS4E7aUn+U(K baj7?x%VD!5Cxk2YbYLNVeiXvvpMCWYo=by@ literal 0 HcmV?d00001 diff --git a/sz-poc-offsite-2025/uniswap/nomiswap/build/index.html b/sz-poc-offsite-2025/uniswap/nomiswap/build/index.html new file mode 100644 index 0000000..b94f10e --- /dev/null +++ b/sz-poc-offsite-2025/uniswap/nomiswap/build/index.html @@ -0,0 +1 @@ +Alternative Uniswap Interface
\ No newline at end of file diff --git a/sz-poc-offsite-2025/uniswap/nomiswap/build/logo192.png b/sz-poc-offsite-2025/uniswap/nomiswap/build/logo192.png new file mode 100644 index 0000000000000000000000000000000000000000..fc44b0a3796c0e0a64c3d858ca038bd4570465d9 GIT binary patch literal 5347 zcmZWtbyO6NvR-oO24RV%BvuJ&=?+<7=`LvyB&A_#M7mSDYw1v6DJkiYl9XjT!%$dLEBTQ8R9|wd3008in6lFF3GV-6mLi?MoP_y~}QUnaDCHI#t z7w^m$@6DI)|C8_jrT?q=f8D?0AM?L)Z}xAo^e^W>t$*Y0KlT5=@bBjT9kxb%-KNdk zeOS1tKO#ChhG7%{ApNBzE2ZVNcxbrin#E1TiAw#BlUhXllzhN$qWez5l;h+t^q#Eav8PhR2|T}y5kkflaK`ba-eoE+Z2q@o6P$)=&` z+(8}+-McnNO>e#$Rr{32ngsZIAX>GH??tqgwUuUz6kjns|LjsB37zUEWd|(&O!)DY zQLrq%Y>)Y8G`yYbYCx&aVHi@-vZ3|ebG!f$sTQqMgi0hWRJ^Wc+Ibv!udh_r%2|U) zPi|E^PK?UE!>_4`f`1k4hqqj_$+d!EB_#IYt;f9)fBOumGNyglU(ofY`yHq4Y?B%- zp&G!MRY<~ajTgIHErMe(Z8JG*;D-PJhd@RX@QatggM7+G(Lz8eZ;73)72Hfx5KDOE zkT(m}i2;@X2AT5fW?qVp?@WgN$aT+f_6eo?IsLh;jscNRp|8H}Z9p_UBO^SJXpZew zEK8fz|0Th%(Wr|KZBGTM4yxkA5CFdAj8=QSrT$fKW#tweUFqr0TZ9D~a5lF{)%-tTGMK^2tz(y2v$i%V8XAxIywrZCp=)83p(zIk6@S5AWl|Oa2hF`~~^W zI;KeOSkw1O#TiQ8;U7OPXjZM|KrnN}9arP)m0v$c|L)lF`j_rpG(zW1Qjv$=^|p*f z>)Na{D&>n`jOWMwB^TM}slgTEcjxTlUby89j1)|6ydRfWERn3|7Zd2&e7?!K&5G$x z`5U3uFtn4~SZq|LjFVrz$3iln-+ucY4q$BC{CSm7Xe5c1J<=%Oagztj{ifpaZk_bQ z9Sb-LaQMKp-qJA*bP6DzgE3`}*i1o3GKmo2pn@dj0;He}F=BgINo};6gQF8!n0ULZ zL>kC0nPSFzlcB7p41doao2F7%6IUTi_+!L`MM4o*#Y#0v~WiO8uSeAUNp=vA2KaR&=jNR2iVwG>7t%sG2x_~yXzY)7K& zk3p+O0AFZ1eu^T3s};B%6TpJ6h-Y%B^*zT&SN7C=N;g|#dGIVMSOru3iv^SvO>h4M=t-N1GSLLDqVTcgurco6)3&XpU!FP6Hlrmj}f$ zp95;b)>M~`kxuZF3r~a!rMf4|&1=uMG$;h^g=Kl;H&Np-(pFT9FF@++MMEx3RBsK?AU0fPk-#mdR)Wdkj)`>ZMl#^<80kM87VvsI3r_c@_vX=fdQ`_9-d(xiI z4K;1y1TiPj_RPh*SpDI7U~^QQ?%0&!$Sh#?x_@;ag)P}ZkAik{_WPB4rHyW#%>|Gs zdbhyt=qQPA7`?h2_8T;-E6HI#im9K>au*(j4;kzwMSLgo6u*}-K`$_Gzgu&XE)udQ zmQ72^eZd|vzI)~!20JV-v-T|<4@7ruqrj|o4=JJPlybwMg;M$Ud7>h6g()CT@wXm` zbq=A(t;RJ^{Xxi*Ff~!|3!-l_PS{AyNAU~t{h;(N(PXMEf^R(B+ZVX3 z8y0;0A8hJYp@g+c*`>eTA|3Tgv9U8#BDTO9@a@gVMDxr(fVaEqL1tl?md{v^j8aUv zm&%PX4^|rX|?E4^CkplWWNv*OKM>DxPa z!RJ)U^0-WJMi)Ksc!^ixOtw^egoAZZ2Cg;X7(5xZG7yL_;UJ#yp*ZD-;I^Z9qkP`} zwCTs0*%rIVF1sgLervtnUo&brwz?6?PXRuOCS*JI-WL6GKy7-~yi0giTEMmDs_-UX zo=+nFrW_EfTg>oY72_4Z0*uG>MnXP=c0VpT&*|rvv1iStW;*^={rP1y?Hv+6R6bxFMkxpWkJ>m7Ba{>zc_q zEefC3jsXdyS5??Mz7IET$Kft|EMNJIv7Ny8ZOcKnzf`K5Cd)&`-fTY#W&jnV0l2vt z?Gqhic}l}mCv1yUEy$%DP}4AN;36$=7aNI^*AzV(eYGeJ(Px-j<^gSDp5dBAv2#?; zcMXv#aj>%;MiG^q^$0MSg-(uTl!xm49dH!{X0){Ew7ThWV~Gtj7h%ZD zVN-R-^7Cf0VH!8O)uUHPL2mO2tmE*cecwQv_5CzWeh)ykX8r5Hi`ehYo)d{Jnh&3p z9ndXT$OW51#H5cFKa76c<%nNkP~FU93b5h-|Cb}ScHs@4Q#|}byWg;KDMJ#|l zE=MKD*F@HDBcX@~QJH%56eh~jfPO-uKm}~t7VkHxHT;)4sd+?Wc4* z>CyR*{w@4(gnYRdFq=^(#-ytb^5ESD?x<0Skhb%Pt?npNW1m+Nv`tr9+qN<3H1f<% zZvNEqyK5FgPsQ`QIu9P0x_}wJR~^CotL|n zk?dn;tLRw9jJTur4uWoX6iMm914f0AJfB@C74a;_qRrAP4E7l890P&{v<}>_&GLrW z)klculcg`?zJO~4;BBAa=POU%aN|pmZJn2{hA!d!*lwO%YSIzv8bTJ}=nhC^n}g(ld^rn#kq9Z3)z`k9lvV>y#!F4e{5c$tnr9M{V)0m(Z< z#88vX6-AW7T2UUwW`g<;8I$Jb!R%z@rCcGT)-2k7&x9kZZT66}Ztid~6t0jKb&9mm zpa}LCb`bz`{MzpZR#E*QuBiZXI#<`5qxx=&LMr-UUf~@dRk}YI2hbMsAMWOmDzYtm zjof16D=mc`^B$+_bCG$$@R0t;e?~UkF?7<(vkb70*EQB1rfUWXh$j)R2)+dNAH5%R zEBs^?N;UMdy}V};59Gu#0$q53$}|+q7CIGg_w_WlvE}AdqoS<7DY1LWS9?TrfmcvT zaypmplwn=P4;a8-%l^e?f`OpGb}%(_mFsL&GywhyN(-VROj`4~V~9bGv%UhcA|YW% zs{;nh@aDX11y^HOFXB$a7#Sr3cEtNd4eLm@Y#fc&j)TGvbbMwze zXtekX_wJqxe4NhuW$r}cNy|L{V=t#$%SuWEW)YZTH|!iT79k#?632OFse{+BT_gau zJwQcbH{b}dzKO?^dV&3nTILYlGw{27UJ72ZN){BILd_HV_s$WfI2DC<9LIHFmtyw? zQ;?MuK7g%Ym+4e^W#5}WDLpko%jPOC=aN)3!=8)s#Rnercak&b3ESRX3z{xfKBF8L z5%CGkFmGO@x?_mPGlpEej!3!AMddChabyf~nJNZxx!D&{@xEb!TDyvqSj%Y5@A{}9 zRzoBn0?x}=krh{ok3Nn%e)#~uh;6jpezhA)ySb^b#E>73e*frBFu6IZ^D7Ii&rsiU z%jzygxT-n*joJpY4o&8UXr2s%j^Q{?e-voloX`4DQyEK+DmrZh8A$)iWL#NO9+Y@!sO2f@rI!@jN@>HOA< z?q2l{^%mY*PNx2FoX+A7X3N}(RV$B`g&N=e0uvAvEN1W^{*W?zT1i#fxuw10%~))J zjx#gxoVlXREWZf4hRkgdHx5V_S*;p-y%JtGgQ4}lnA~MBz-AFdxUxU1RIT$`sal|X zPB6sEVRjGbXIP0U+?rT|y5+ev&OMX*5C$n2SBPZr`jqzrmpVrNciR0e*Wm?fK6DY& zl(XQZ60yWXV-|Ps!A{EF;=_z(YAF=T(-MkJXUoX zI{UMQDAV2}Ya?EisdEW;@pE6dt;j0fg5oT2dxCi{wqWJ<)|SR6fxX~5CzblPGr8cb zUBVJ2CQd~3L?7yfTpLNbt)He1D>*KXI^GK%<`bq^cUq$Q@uJifG>p3LU(!H=C)aEL zenk7pVg}0{dKU}&l)Y2Y2eFMdS(JS0}oZUuVaf2+K*YFNGHB`^YGcIpnBlMhO7d4@vV zv(@N}(k#REdul8~fP+^F@ky*wt@~&|(&&meNO>rKDEnB{ykAZ}k>e@lad7to>Ao$B zz<1(L=#J*u4_LB=8w+*{KFK^u00NAmeNN7pr+Pf+N*Zl^dO{LM-hMHyP6N!~`24jd zXYP|Ze;dRXKdF2iJG$U{k=S86l@pytLx}$JFFs8e)*Vi?aVBtGJ3JZUj!~c{(rw5>vuRF$`^p!P8w1B=O!skwkO5yd4_XuG^QVF z`-r5K7(IPSiKQ2|U9+`@Js!g6sfJwAHVd|s?|mnC*q zp|B|z)(8+mxXyxQ{8Pg3F4|tdpgZZSoU4P&9I8)nHo1@)9_9u&NcT^FI)6|hsAZFk zZ+arl&@*>RXBf-OZxhZerOr&dN5LW9@gV=oGFbK*J+m#R-|e6(Loz(;g@T^*oO)0R zN`N=X46b{7yk5FZGr#5&n1!-@j@g02g|X>MOpF3#IjZ_4wg{dX+G9eqS+Es9@6nC7 zD9$NuVJI}6ZlwtUm5cCAiYv0(Yi{%eH+}t)!E^>^KxB5^L~a`4%1~5q6h>d;paC9c zTj0wTCKrhWf+F#5>EgX`sl%POl?oyCq0(w0xoL?L%)|Q7d|Hl92rUYAU#lc**I&^6p=4lNQPa0 znQ|A~i0ip@`B=FW-Q;zh?-wF;Wl5!+q3GXDu-x&}$gUO)NoO7^$BeEIrd~1Dh{Tr` z8s<(Bn@gZ(mkIGnmYh_ehXnq78QL$pNDi)|QcT*|GtS%nz1uKE+E{7jdEBp%h0}%r zD2|KmYGiPa4;md-t_m5YDz#c*oV_FqXd85d@eub?9N61QuYcb3CnVWpM(D-^|CmkL z(F}L&N7qhL2PCq)fRh}XO@U`Yn<?TNGR4L(mF7#4u29{i~@k;pLsgl({YW5`Mo+p=zZn3L*4{JU;++dG9 X@eDJUQo;Ye2mwlRs?y0|+_a0zY+Zo%Dkae}+MySoIppb75o?vUW_?)>@g{U2`ERQIXV zeY$JrWnMZ$QC<=ii4X|@0H8`si75jB(ElJb00HAB%>SlLR{!zO|C9P3zxw_U8?1d8uRZ=({Ga4shyN}3 zAK}WA(ds|``G4jA)9}Bt2Hy0+f3rV1E6b|@?hpGA=PI&r8)ah|)I2s(P5Ic*Ndhn^ z*T&j@gbCTv7+8rpYbR^Ty}1AY)YH;p!m948r#%7x^Z@_-w{pDl|1S4`EM3n_PaXvK z1JF)E3qy$qTj5Xs{jU9k=y%SQ0>8E$;x?p9ayU0bZZeo{5Z@&FKX>}s!0+^>C^D#z z>xsCPvxD3Z=dP}TTOSJhNTPyVt14VCQ9MQFN`rn!c&_p?&4<5_PGm4a;WS&1(!qKE z_H$;dDdiPQ!F_gsN`2>`X}$I=B;={R8%L~`>RyKcS$72ai$!2>d(YkciA^J0@X%G4 z4cu!%Ps~2JuJ8ex`&;Fa0NQOq_nDZ&X;^A=oc1&f#3P1(!5il>6?uK4QpEG8z0Rhu zvBJ+A9RV?z%v?!$=(vcH?*;vRs*+PPbOQ3cdPr5=tOcLqmfx@#hOqX0iN)wTTO21jH<>jpmwRIAGw7`a|sl?9y9zRBh>(_%| zF?h|P7}~RKj?HR+q|4U`CjRmV-$mLW>MScKnNXiv{vD3&2@*u)-6P@h0A`eeZ7}71 zK(w%@R<4lLt`O7fs1E)$5iGb~fPfJ?WxhY7c3Q>T-w#wT&zW522pH-B%r5v#5y^CF zcC30Se|`D2mY$hAlIULL%-PNXgbbpRHgn<&X3N9W!@BUk@9g*P5mz-YnZBb*-$zMM z7Qq}ic0mR8n{^L|=+diODdV}Q!gwr?y+2m=3HWwMq4z)DqYVg0J~^}-%7rMR@S1;9 z7GFj6K}i32X;3*$SmzB&HW{PJ55kT+EI#SsZf}bD7nW^Haf}_gXciYKX{QBxIPSx2Ma? zHQqgzZq!_{&zg{yxqv3xq8YV+`S}F6A>Gtl39_m;K4dA{pP$BW0oIXJ>jEQ!2V3A2 zdpoTxG&V=(?^q?ZTj2ZUpDUdMb)T?E$}CI>r@}PFPWD9@*%V6;4Ag>D#h>!s)=$0R zRXvdkZ%|c}ubej`jl?cS$onl9Tw52rBKT)kgyw~Xy%z62Lr%V6Y=f?2)J|bZJ5(Wx zmji`O;_B+*X@qe-#~`HFP<{8$w@z4@&`q^Q-Zk8JG3>WalhnW1cvnoVw>*R@c&|o8 zZ%w!{Z+MHeZ*OE4v*otkZqz11*s!#s^Gq>+o`8Z5 z^i-qzJLJh9!W-;SmFkR8HEZJWiXk$40i6)7 zZpr=k2lp}SasbM*Nbn3j$sn0;rUI;%EDbi7T1ZI4qL6PNNM2Y%6{LMIKW+FY_yF3) zSKQ2QSujzNMSL2r&bYs`|i2Dnn z=>}c0>a}>|uT!IiMOA~pVT~R@bGlm}Edf}Kq0?*Af6#mW9f9!}RjW7om0c9Qlp;yK z)=XQs(|6GCadQbWIhYF=rf{Y)sj%^Id-ARO0=O^Ad;Ph+ z0?$eE1xhH?{T$QI>0JP75`r)U_$#%K1^BQ8z#uciKf(C701&RyLQWBUp*Q7eyn76} z6JHpC9}R$J#(R0cDCkXoFSp;j6{x{b&0yE@P7{;pCEpKjS(+1RQy38`=&Yxo%F=3y zCPeefABp34U-s?WmU#JJw23dcC{sPPFc2#J$ZgEN%zod}J~8dLm*fx9f6SpO zn^Ww3bt9-r0XaT2a@Wpw;C23XM}7_14#%QpubrIw5aZtP+CqIFmsG4`Cm6rfxl9n5 z7=r2C-+lM2AB9X0T_`?EW&Byv&K?HS4QLoylJ|OAF z`8atBNTzJ&AQ!>sOo$?^0xj~D(;kS$`9zbEGd>f6r`NC3X`tX)sWgWUUOQ7w=$TO&*j;=u%25ay-%>3@81tGe^_z*C7pb9y*Ed^H3t$BIKH2o+olp#$q;)_ zfpjCb_^VFg5fU~K)nf*d*r@BCC>UZ!0&b?AGk_jTPXaSnCuW110wjHPPe^9R^;jo3 zwvzTl)C`Zl5}O2}3lec=hZ*$JnkW#7enKKc)(pM${_$9Hc=Sr_A9Biwe*Y=T?~1CK z6eZ9uPICjy-sMGbZl$yQmpB&`ouS8v{58__t0$JP%i3R&%QR3ianbZqDs<2#5FdN@n5bCn^ZtH992~5k(eA|8|@G9u`wdn7bnpg|@{m z^d6Y`*$Zf2Xr&|g%sai#5}Syvv(>Jnx&EM7-|Jr7!M~zdAyjt*xl;OLhvW-a%H1m0 z*x5*nb=R5u><7lyVpNAR?q@1U59 zO+)QWwL8t zyip?u_nI+K$uh{y)~}qj?(w0&=SE^8`_WMM zTybjG=999h38Yes7}-4*LJ7H)UE8{mE(6;8voE+TYY%33A>S6`G_95^5QHNTo_;Ao ztIQIZ_}49%{8|=O;isBZ?=7kfdF8_@azfoTd+hEJKWE!)$)N%HIe2cplaK`ry#=pV z0q{9w-`i0h@!R8K3GC{ivt{70IWG`EP|(1g7i_Q<>aEAT{5(yD z=!O?kq61VegV+st@XCw475j6vS)_z@efuqQgHQR1T4;|-#OLZNQJPV4k$AX1Uk8Lm z{N*b*ia=I+MB}kWpupJ~>!C@xEN#Wa7V+7{m4j8c?)ChV=D?o~sjT?0C_AQ7B-vxqX30s0I_`2$in86#`mAsT-w?j{&AL@B3$;P z31G4(lV|b}uSDCIrjk+M1R!X7s4Aabn<)zpgT}#gE|mIvV38^ODy@<&yflpCwS#fRf9ZX3lPV_?8@C5)A;T zqmouFLFk;qIs4rA=hh=GL~sCFsXHsqO6_y~*AFt939UYVBSx1s(=Kb&5;j7cSowdE;7()CC2|-i9Zz+_BIw8#ll~-tyH?F3{%`QCsYa*b#s*9iCc`1P1oC26?`g<9))EJ3%xz+O!B3 zZ7$j~To)C@PquR>a1+Dh>-a%IvH_Y7^ys|4o?E%3`I&ADXfC8++hAdZfzIT#%C+Jz z1lU~K_vAm0m8Qk}K$F>|>RPK%<1SI0(G+8q~H zAsjezyP+u!Se4q3GW)`h`NPSRlMoBjCzNPesWJwVTY!o@G8=(6I%4XHGaSiS3MEBK zhgGFv6Jc>L$4jVE!I?TQuwvz_%CyO!bLh94nqK11C2W$*aa2ueGopG8DnBICVUORP zgytv#)49fVXDaR$SukloYC3u7#5H)}1K21=?DKj^U)8G;MS)&Op)g^zR2($<>C*zW z;X7`hLxiIO#J`ANdyAOJle4V%ppa*(+0i3w;8i*BA_;u8gOO6)MY`ueq7stBMJTB; z-a0R>hT*}>z|Gg}@^zDL1MrH+2hsR8 zHc}*9IvuQC^Ju)^#Y{fOr(96rQNPNhxc;mH@W*m206>Lo<*SaaH?~8zg&f&%YiOEG zGiz?*CP>Bci}!WiS=zj#K5I}>DtpregpP_tfZtPa(N<%vo^#WCQ5BTv0vr%Z{)0q+ z)RbfHktUm|lg&U3YM%lMUM(fu}i#kjX9h>GYctkx9Mt_8{@s%!K_EI zScgwy6%_fR?CGJQtmgNAj^h9B#zmaMDWgH55pGuY1Gv7D z;8Psm(vEPiwn#MgJYu4Ty9D|h!?Rj0ddE|&L3S{IP%H4^N!m`60ZwZw^;eg4sk6K{ ziA^`Sbl_4~f&Oo%n;8Ye(tiAdlZKI!Z=|j$5hS|D$bDJ}p{gh$KN&JZYLUjv4h{NY zBJ>X9z!xfDGY z+oh_Z&_e#Q(-}>ssZfm=j$D&4W4FNy&-kAO1~#3Im;F)Nwe{(*75(p=P^VI?X0GFakfh+X-px4a%Uw@fSbmp9hM1_~R>?Z8+ ziy|e9>8V*`OP}4x5JjdWp}7eX;lVxp5qS}0YZek;SNmm7tEeSF*-dI)6U-A%m6YvCgM(}_=k#a6o^%-K4{`B1+}O4x zztDT%hVb;v#?j`lTvlFQ3aV#zkX=7;YFLS$uIzb0E3lozs5`Xy zi~vF+%{z9uLjKvKPhP%x5f~7-Gj+%5N`%^=yk*Qn{`> z;xj&ROY6g`iy2a@{O)V(jk&8#hHACVDXey5a+KDod_Z&}kHM}xt7}Md@pil{2x7E~ zL$k^d2@Ec2XskjrN+IILw;#7((abu;OJii&v3?60x>d_Ma(onIPtcVnX@ELF0aL?T zSmWiL3(dOFkt!x=1O!_0n(cAzZW+3nHJ{2S>tgSK?~cFha^y(l@-Mr2W$%MN{#af8J;V*>hdq!gx=d0h$T7l}>91Wh07)9CTX zh2_ZdQCyFOQ)l(}gft0UZG`Sh2`x-w`5vC2UD}lZs*5 zG76$akzn}Xi))L3oGJ75#pcN=cX3!=57$Ha=hQ2^lwdyU#a}4JJOz6ddR%zae%#4& za)bFj)z=YQela(F#Y|Q#dp}PJghITwXouVaMq$BM?K%cXn9^Y@g43$=O)F&ZlOUom zJiad#dea;-eywBA@e&D6Pdso1?2^(pXiN91?jvcaUyYoKUmvl5G9e$W!okWe*@a<^ z8cQQ6cNSf+UPDx%?_G4aIiybZHHagF{;IcD(dPO!#=u zWfqLcPc^+7Uu#l(Bpxft{*4lv#*u7X9AOzDO z1D9?^jIo}?%iz(_dwLa{ex#T}76ZfN_Z-hwpus9y+4xaUu9cX}&P{XrZVWE{1^0yw zO;YhLEW!pJcbCt3L8~a7>jsaN{V3>tz6_7`&pi%GxZ=V3?3K^U+*ryLSb)8^IblJ0 zSRLNDvIxt)S}g30?s_3NX>F?NKIGrG_zB9@Z>uSW3k2es_H2kU;Rnn%j5qP)!XHKE zPB2mHP~tLCg4K_vH$xv`HbRsJwbZMUV(t=ez;Ec(vyHH)FbfLg`c61I$W_uBB>i^r z&{_P;369-&>23R%qNIULe=1~T$(DA`ev*EWZ6j(B$(te}x1WvmIll21zvygkS%vwG zzkR6Z#RKA2!z!C%M!O>!=Gr0(J0FP=-MN=5t-Ir)of50y10W}j`GtRCsXBakrKtG& zazmITDJMA0C51&BnLY)SY9r)NVTMs);1<=oosS9g31l{4ztjD3#+2H7u_|66b|_*O z;Qk6nalpqdHOjx|K&vUS_6ITgGll;TdaN*ta=M_YtyC)I9Tmr~VaPrH2qb6sd~=AcIxV+%z{E&0@y=DPArw zdV7z(G1hBx7hd{>(cr43^WF%4Y@PXZ?wPpj{OQ#tvc$pABJbvPGvdR`cAtHn)cSEV zrpu}1tJwQ3y!mSmH*uz*x0o|CS<^w%&KJzsj~DU0cLQUxk5B!hWE>aBkjJle8z~;s z-!A=($+}Jq_BTK5^B!`R>!MulZN)F=iXXeUd0w5lUsE5VP*H*oCy(;?S$p*TVvTxwAeWFB$jHyb0593)$zqalVlDX=GcCN1gU0 zlgU)I$LcXZ8Oyc2TZYTPu@-;7<4YYB-``Qa;IDcvydIA$%kHhJKV^m*-zxcvU4viy&Kr5GVM{IT>WRywKQ9;>SEiQD*NqplK-KK4YR`p0@JW)n_{TU3bt0 zim%;(m1=#v2}zTps=?fU5w^(*y)xT%1vtQH&}50ZF!9YxW=&7*W($2kgKyz1mUgfs zfV<*XVVIFnohW=|j+@Kfo!#liQR^x>2yQdrG;2o8WZR+XzU_nG=Ed2rK?ntA;K5B{ z>M8+*A4!Jm^Bg}aW?R?6;@QG@uQ8&oJ{hFixcfEnJ4QH?A4>P=q29oDGW;L;= z9-a0;g%c`C+Ai!UmK$NC*4#;Jp<1=TioL=t^YM)<<%u#hnnfSS`nq63QKGO1L8RzX z@MFDqs1z ztYmxDl@LU)5acvHk)~Z`RW7=aJ_nGD!mOSYD>5Odjn@TK#LY{jf?+piB5AM-CAoT_ z?S-*q7}wyLJzK>N%eMPuFgN)Q_otKP;aqy=D5f!7<=n(lNkYRXVpkB{TAYLYg{|(jtRqYmg$xH zjmq?B(RE4 zQx^~Pt}gxC2~l=K$$-sYy_r$CO(d=+b3H1MB*y_5g6WLaWTXn+TKQ|hNY^>Mp6k*$ zwkovomhu776vQATqT4blf~g;TY(MWCrf^^yfWJvSAB$p5l;jm@o#=!lqw+Lqfq>X= z$6~kxfm7`3q4zUEB;u4qa#BdJxO!;xGm)wwuisj{0y2x{R(IGMrsIzDY9LW>m!Y`= z04sx3IjnYvL<4JqxQ8f7qYd0s2Ig%`ytYPEMKI)s(LD}D@EY>x`VFtqvnADNBdeao zC96X+MxnwKmjpg{U&gP3HE}1=s!lv&D{6(g_lzyF3A`7Jn*&d_kL<;dAFx!UZ>hB8 z5A*%LsAn;VLp>3${0>M?PSQ)9s3}|h2e?TG4_F{}{Cs>#3Q*t$(CUc}M)I}8cPF6% z=+h(Kh^8)}gj(0}#e7O^FQ6`~fd1#8#!}LMuo3A0bN`o}PYsm!Y}sdOz$+Tegc=qT z8x`PH$7lvnhJp{kHWb22l;@7B7|4yL4UOOVM0MP_>P%S1Lnid)+k9{+3D+JFa#Pyf zhVc#&df87APl4W9X)F3pGS>@etfl=_E5tBcVoOfrD4hmVeTY-cj((pkn%n@EgN{0f zwb_^Rk0I#iZuHK!l*lN`ceJn(sI{$Fq6nN& zE<-=0_2WN}m+*ivmIOxB@#~Q-cZ>l136w{#TIJe478`KE7@=a{>SzPHsKLzYAyBQO zAtuuF$-JSDy_S@6GW0MOE~R)b;+0f%_NMrW(+V#c_d&U8Z9+ec4=HmOHw?gdjF(Lu zzra83M_BoO-1b3;9`%&DHfuUY)6YDV21P$C!Rc?mv&{lx#f8oc6?0?x zK08{WP65?#>(vPfA-c=MCY|%*1_<3D4NX zeVTi-JGl2uP_2@0F{G({pxQOXt_d{g_CV6b?jNpfUG9;8yle-^4KHRvZs-_2siata zt+d_T@U$&t*xaD22(fH(W1r$Mo?3dc%Tncm=C6{V9y{v&VT#^1L04vDrLM9qBoZ4@ z6DBN#m57hX7$C(=#$Y5$bJmwA$T8jKD8+6A!-IJwA{WOfs%s}yxUw^?MRZjF$n_KN z6`_bGXcmE#5e4Ym)aQJ)xg3Pg0@k`iGuHe?f(5LtuzSq=nS^5z>vqU0EuZ&75V%Z{ zYyhRLN^)$c6Ds{f7*FBpE;n5iglx5PkHfWrj3`x^j^t z7ntuV`g!9Xg#^3!x)l*}IW=(Tz3>Y5l4uGaB&lz{GDjm2D5S$CExLT`I1#n^lBH7Y zDgpMag@`iETKAI=p<5E#LTkwzVR@=yY|uBVI1HG|8h+d;G-qfuj}-ZR6fN>EfCCW z9~wRQoAPEa#aO?3h?x{YvV*d+NtPkf&4V0k4|L=uj!U{L+oLa(z#&iuhJr3-PjO3R z5s?=nn_5^*^Rawr>>Nr@K(jwkB#JK-=+HqwfdO<+P5byeim)wvqGlP-P|~Nse8=XF zz`?RYB|D6SwS}C+YQv+;}k6$-%D(@+t14BL@vM z2q%q?f6D-A5s$_WY3{^G0F131bbh|g!}#BKw=HQ7mx;Dzg4Z*bTLQSfo{ed{4}NZW zfrRm^Ca$rlE{Ue~uYv>R9{3smwATcdM_6+yWIO z*ZRH~uXE@#p$XTbCt5j7j2=86e{9>HIB6xDzV+vAo&B?KUiMP|ttOElepnl%|DPqL b{|{}U^kRn2wo}j7|0ATu<;8xA7zX}7|B6mN literal 0 HcmV?d00001 diff --git a/sz-poc-offsite-2025/uniswap/nomiswap/build/manifest.json b/sz-poc-offsite-2025/uniswap/nomiswap/build/manifest.json new file mode 100644 index 0000000..080d6c7 --- /dev/null +++ b/sz-poc-offsite-2025/uniswap/nomiswap/build/manifest.json @@ -0,0 +1,25 @@ +{ + "short_name": "React App", + "name": "Create React App Sample", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/sz-poc-offsite-2025/uniswap/nomiswap/build/robots.txt b/sz-poc-offsite-2025/uniswap/nomiswap/build/robots.txt new file mode 100644 index 0000000..e9e57dc --- /dev/null +++ b/sz-poc-offsite-2025/uniswap/nomiswap/build/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/sz-poc-offsite-2025/uniswap/nomiswap/build/static/css/2.3ab4f688.chunk.css b/sz-poc-offsite-2025/uniswap/nomiswap/build/static/css/2.3ab4f688.chunk.css new file mode 100644 index 0000000..b676c64 --- /dev/null +++ b/sz-poc-offsite-2025/uniswap/nomiswap/build/static/css/2.3ab4f688.chunk.css @@ -0,0 +1,8 @@ +@charset "UTF-8"; +/*! + * Bootstrap v5.0.2 (https://getbootstrap.com/) + * Copyright 2011-2021 The Bootstrap Authors + * Copyright 2011-2021 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */:root{--bs-blue:#0d6efd;--bs-indigo:#6610f2;--bs-purple:#6f42c1;--bs-pink:#d63384;--bs-red:#dc3545;--bs-orange:#fd7e14;--bs-yellow:#ffc107;--bs-green:#198754;--bs-teal:#20c997;--bs-cyan:#0dcaf0;--bs-white:#fff;--bs-gray:#6c757d;--bs-gray-dark:#343a40;--bs-primary:#0d6efd;--bs-secondary:#6c757d;--bs-success:#198754;--bs-info:#0dcaf0;--bs-warning:#ffc107;--bs-danger:#dc3545;--bs-light:#f8f9fa;--bs-dark:#212529;--bs-font-sans-serif:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--bs-font-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--bs-gradient:linear-gradient(180deg,hsla(0,0%,100%,0.15),hsla(0,0%,100%,0))}*,:after,:before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-family:var(--bs-font-sans-serif);font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:rgba(0,0,0,0)}hr{margin:1rem 0;color:inherit;background-color:currentColor;border:0;opacity:.25}hr:not([size]){height:1px}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2}.h1,h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){.h1,h1{font-size:2.5rem}}.h2,h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){.h2,h2{font-size:2rem}}.h3,h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){.h3,h3{font-size:1.75rem}}.h4,h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){.h4,h4{font-size:1.5rem}}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[data-bs-original-title],abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}.small,small{font-size:.875em}.mark,mark{padding:.2em;background-color:#fcf8e3}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#0d6efd;text-decoration:underline}a:hover{color:#0a58ca}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-family:var(--bs-font-monospace);font-size:1em;direction:ltr;unicode-bidi:bidi-override}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:#d63384;word-wrap:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:.875em;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:1em;font-weight:700}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:#6c757d;text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border:0 solid;border-color:inherit}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]::-webkit-calendar-picker-indicator{display:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:left;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:left}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:textfield}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::file-selector-button{font:inherit}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:calc(1.625rem + 4.5vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-1{font-size:5rem}}.display-2{font-size:calc(1.575rem + 3.9vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-2{font-size:4.5rem}}.display-3{font-size:calc(1.525rem + 3.3vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-3{font-size:4rem}}.display-4{font-size:calc(1.475rem + 2.7vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-4{font-size:3.5rem}}.display-5{font-size:calc(1.425rem + 2.1vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-5{font-size:3rem}}.display-6{font-size:calc(1.375rem + 1.5vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-6{font-size:2.5rem}}.list-inline,.list-unstyled{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:.875em;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote>:last-child{margin-bottom:0}.blockquote-footer{margin-top:-1rem;margin-bottom:1rem;font-size:.875em;color:#6c757d}.blockquote-footer:before{content:"— "}.img-fluid,.img-thumbnail{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:#fff;border:1px solid #dee2e6;border-radius:.25rem}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:.875em;color:#6c757d}.container,.container-fluid,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{width:100%;padding-right:.75rem;padding-right:var(--bs-gutter-x,.75rem);padding-left:.75rem;padding-left:var(--bs-gutter-x,.75rem);margin-right:auto;margin-left:auto}@media (min-width:576px){.container,.container-sm{max-width:540px}}@media (min-width:768px){.container,.container-md,.container-sm{max-width:720px}}@media (min-width:992px){.container,.container-lg,.container-md,.container-sm{max-width:960px}}@media (min-width:1200px){.container,.container-lg,.container-md,.container-sm,.container-xl{max-width:1140px}}@media (min-width:1400px){.container,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{max-width:1320px}}.row{--bs-gutter-x:1.5rem;--bs-gutter-y:0;display:flex;flex-wrap:wrap;margin-top:calc(var(--bs-gutter-y)*-1);margin-right:calc(var(--bs-gutter-x)*-0.5);margin-left:calc(var(--bs-gutter-x)*-0.5)}.row>*{flex-shrink:0;width:100%;max-width:100%;padding-right:calc(var(--bs-gutter-x)*0.5);padding-left:calc(var(--bs-gutter-x)*0.5);margin-top:var(--bs-gutter-y)}.col{flex:1 0}.row-cols-auto>*{flex:0 0 auto;width:auto}.row-cols-1>*{flex:0 0 auto;width:100%}.row-cols-2>*{flex:0 0 auto;width:50%}.row-cols-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-4>*{flex:0 0 auto;width:25%}.row-cols-5>*{flex:0 0 auto;width:20%}.row-cols-6>*{flex:0 0 auto;width:16.6666666667%}@media (min-width:576px){.col-sm{flex:1 0}.row-cols-sm-auto>*{flex:0 0 auto;width:auto}.row-cols-sm-1>*{flex:0 0 auto;width:100%}.row-cols-sm-2>*{flex:0 0 auto;width:50%}.row-cols-sm-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-sm-4>*{flex:0 0 auto;width:25%}.row-cols-sm-5>*{flex:0 0 auto;width:20%}.row-cols-sm-6>*{flex:0 0 auto;width:16.6666666667%}}@media (min-width:768px){.col-md{flex:1 0}.row-cols-md-auto>*{flex:0 0 auto;width:auto}.row-cols-md-1>*{flex:0 0 auto;width:100%}.row-cols-md-2>*{flex:0 0 auto;width:50%}.row-cols-md-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-md-4>*{flex:0 0 auto;width:25%}.row-cols-md-5>*{flex:0 0 auto;width:20%}.row-cols-md-6>*{flex:0 0 auto;width:16.6666666667%}}@media (min-width:992px){.col-lg{flex:1 0}.row-cols-lg-auto>*{flex:0 0 auto;width:auto}.row-cols-lg-1>*{flex:0 0 auto;width:100%}.row-cols-lg-2>*{flex:0 0 auto;width:50%}.row-cols-lg-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-lg-4>*{flex:0 0 auto;width:25%}.row-cols-lg-5>*{flex:0 0 auto;width:20%}.row-cols-lg-6>*{flex:0 0 auto;width:16.6666666667%}}@media (min-width:1200px){.col-xl{flex:1 0}.row-cols-xl-auto>*{flex:0 0 auto;width:auto}.row-cols-xl-1>*{flex:0 0 auto;width:100%}.row-cols-xl-2>*{flex:0 0 auto;width:50%}.row-cols-xl-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-xl-4>*{flex:0 0 auto;width:25%}.row-cols-xl-5>*{flex:0 0 auto;width:20%}.row-cols-xl-6>*{flex:0 0 auto;width:16.6666666667%}}@media (min-width:1400px){.col-xxl{flex:1 0}.row-cols-xxl-auto>*{flex:0 0 auto;width:auto}.row-cols-xxl-1>*{flex:0 0 auto;width:100%}.row-cols-xxl-2>*{flex:0 0 auto;width:50%}.row-cols-xxl-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-xxl-4>*{flex:0 0 auto;width:25%}.row-cols-xxl-5>*{flex:0 0 auto;width:20%}.row-cols-xxl-6>*{flex:0 0 auto;width:16.6666666667%}}.col-auto{flex:0 0 auto;width:auto}.col-1{flex:0 0 auto;width:8.33333333%}.col-2{flex:0 0 auto;width:16.66666667%}.col-3{flex:0 0 auto;width:25%}.col-4{flex:0 0 auto;width:33.33333333%}.col-5{flex:0 0 auto;width:41.66666667%}.col-6{flex:0 0 auto;width:50%}.col-7{flex:0 0 auto;width:58.33333333%}.col-8{flex:0 0 auto;width:66.66666667%}.col-9{flex:0 0 auto;width:75%}.col-10{flex:0 0 auto;width:83.33333333%}.col-11{flex:0 0 auto;width:91.66666667%}.col-12{flex:0 0 auto;width:100%}.offset-1{margin-left:8.33333333%}.offset-2{margin-left:16.66666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.33333333%}.offset-5{margin-left:41.66666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.33333333%}.offset-8{margin-left:66.66666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.33333333%}.offset-11{margin-left:91.66666667%}.g-0,.gx-0{--bs-gutter-x:0}.g-0,.gy-0{--bs-gutter-y:0}.g-1,.gx-1{--bs-gutter-x:0.25rem}.g-1,.gy-1{--bs-gutter-y:0.25rem}.g-2,.gx-2{--bs-gutter-x:0.5rem}.g-2,.gy-2{--bs-gutter-y:0.5rem}.g-3,.gx-3{--bs-gutter-x:1rem}.g-3,.gy-3{--bs-gutter-y:1rem}.g-4,.gx-4{--bs-gutter-x:1.5rem}.g-4,.gy-4{--bs-gutter-y:1.5rem}.g-5,.gx-5{--bs-gutter-x:3rem}.g-5,.gy-5{--bs-gutter-y:3rem}@media (min-width:576px){.col-sm-auto{flex:0 0 auto;width:auto}.col-sm-1{flex:0 0 auto;width:8.33333333%}.col-sm-2{flex:0 0 auto;width:16.66666667%}.col-sm-3{flex:0 0 auto;width:25%}.col-sm-4{flex:0 0 auto;width:33.33333333%}.col-sm-5{flex:0 0 auto;width:41.66666667%}.col-sm-6{flex:0 0 auto;width:50%}.col-sm-7{flex:0 0 auto;width:58.33333333%}.col-sm-8{flex:0 0 auto;width:66.66666667%}.col-sm-9{flex:0 0 auto;width:75%}.col-sm-10{flex:0 0 auto;width:83.33333333%}.col-sm-11{flex:0 0 auto;width:91.66666667%}.col-sm-12{flex:0 0 auto;width:100%}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.33333333%}.offset-sm-2{margin-left:16.66666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.33333333%}.offset-sm-5{margin-left:41.66666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.33333333%}.offset-sm-8{margin-left:66.66666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.33333333%}.offset-sm-11{margin-left:91.66666667%}.g-sm-0,.gx-sm-0{--bs-gutter-x:0}.g-sm-0,.gy-sm-0{--bs-gutter-y:0}.g-sm-1,.gx-sm-1{--bs-gutter-x:0.25rem}.g-sm-1,.gy-sm-1{--bs-gutter-y:0.25rem}.g-sm-2,.gx-sm-2{--bs-gutter-x:0.5rem}.g-sm-2,.gy-sm-2{--bs-gutter-y:0.5rem}.g-sm-3,.gx-sm-3{--bs-gutter-x:1rem}.g-sm-3,.gy-sm-3{--bs-gutter-y:1rem}.g-sm-4,.gx-sm-4{--bs-gutter-x:1.5rem}.g-sm-4,.gy-sm-4{--bs-gutter-y:1.5rem}.g-sm-5,.gx-sm-5{--bs-gutter-x:3rem}.g-sm-5,.gy-sm-5{--bs-gutter-y:3rem}}@media (min-width:768px){.col-md-auto{flex:0 0 auto;width:auto}.col-md-1{flex:0 0 auto;width:8.33333333%}.col-md-2{flex:0 0 auto;width:16.66666667%}.col-md-3{flex:0 0 auto;width:25%}.col-md-4{flex:0 0 auto;width:33.33333333%}.col-md-5{flex:0 0 auto;width:41.66666667%}.col-md-6{flex:0 0 auto;width:50%}.col-md-7{flex:0 0 auto;width:58.33333333%}.col-md-8{flex:0 0 auto;width:66.66666667%}.col-md-9{flex:0 0 auto;width:75%}.col-md-10{flex:0 0 auto;width:83.33333333%}.col-md-11{flex:0 0 auto;width:91.66666667%}.col-md-12{flex:0 0 auto;width:100%}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.33333333%}.offset-md-2{margin-left:16.66666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.33333333%}.offset-md-5{margin-left:41.66666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.33333333%}.offset-md-8{margin-left:66.66666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.33333333%}.offset-md-11{margin-left:91.66666667%}.g-md-0,.gx-md-0{--bs-gutter-x:0}.g-md-0,.gy-md-0{--bs-gutter-y:0}.g-md-1,.gx-md-1{--bs-gutter-x:0.25rem}.g-md-1,.gy-md-1{--bs-gutter-y:0.25rem}.g-md-2,.gx-md-2{--bs-gutter-x:0.5rem}.g-md-2,.gy-md-2{--bs-gutter-y:0.5rem}.g-md-3,.gx-md-3{--bs-gutter-x:1rem}.g-md-3,.gy-md-3{--bs-gutter-y:1rem}.g-md-4,.gx-md-4{--bs-gutter-x:1.5rem}.g-md-4,.gy-md-4{--bs-gutter-y:1.5rem}.g-md-5,.gx-md-5{--bs-gutter-x:3rem}.g-md-5,.gy-md-5{--bs-gutter-y:3rem}}@media (min-width:992px){.col-lg-auto{flex:0 0 auto;width:auto}.col-lg-1{flex:0 0 auto;width:8.33333333%}.col-lg-2{flex:0 0 auto;width:16.66666667%}.col-lg-3{flex:0 0 auto;width:25%}.col-lg-4{flex:0 0 auto;width:33.33333333%}.col-lg-5{flex:0 0 auto;width:41.66666667%}.col-lg-6{flex:0 0 auto;width:50%}.col-lg-7{flex:0 0 auto;width:58.33333333%}.col-lg-8{flex:0 0 auto;width:66.66666667%}.col-lg-9{flex:0 0 auto;width:75%}.col-lg-10{flex:0 0 auto;width:83.33333333%}.col-lg-11{flex:0 0 auto;width:91.66666667%}.col-lg-12{flex:0 0 auto;width:100%}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.33333333%}.offset-lg-2{margin-left:16.66666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.33333333%}.offset-lg-5{margin-left:41.66666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.33333333%}.offset-lg-8{margin-left:66.66666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.33333333%}.offset-lg-11{margin-left:91.66666667%}.g-lg-0,.gx-lg-0{--bs-gutter-x:0}.g-lg-0,.gy-lg-0{--bs-gutter-y:0}.g-lg-1,.gx-lg-1{--bs-gutter-x:0.25rem}.g-lg-1,.gy-lg-1{--bs-gutter-y:0.25rem}.g-lg-2,.gx-lg-2{--bs-gutter-x:0.5rem}.g-lg-2,.gy-lg-2{--bs-gutter-y:0.5rem}.g-lg-3,.gx-lg-3{--bs-gutter-x:1rem}.g-lg-3,.gy-lg-3{--bs-gutter-y:1rem}.g-lg-4,.gx-lg-4{--bs-gutter-x:1.5rem}.g-lg-4,.gy-lg-4{--bs-gutter-y:1.5rem}.g-lg-5,.gx-lg-5{--bs-gutter-x:3rem}.g-lg-5,.gy-lg-5{--bs-gutter-y:3rem}}@media (min-width:1200px){.col-xl-auto{flex:0 0 auto;width:auto}.col-xl-1{flex:0 0 auto;width:8.33333333%}.col-xl-2{flex:0 0 auto;width:16.66666667%}.col-xl-3{flex:0 0 auto;width:25%}.col-xl-4{flex:0 0 auto;width:33.33333333%}.col-xl-5{flex:0 0 auto;width:41.66666667%}.col-xl-6{flex:0 0 auto;width:50%}.col-xl-7{flex:0 0 auto;width:58.33333333%}.col-xl-8{flex:0 0 auto;width:66.66666667%}.col-xl-9{flex:0 0 auto;width:75%}.col-xl-10{flex:0 0 auto;width:83.33333333%}.col-xl-11{flex:0 0 auto;width:91.66666667%}.col-xl-12{flex:0 0 auto;width:100%}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.33333333%}.offset-xl-2{margin-left:16.66666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.33333333%}.offset-xl-5{margin-left:41.66666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.33333333%}.offset-xl-8{margin-left:66.66666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.33333333%}.offset-xl-11{margin-left:91.66666667%}.g-xl-0,.gx-xl-0{--bs-gutter-x:0}.g-xl-0,.gy-xl-0{--bs-gutter-y:0}.g-xl-1,.gx-xl-1{--bs-gutter-x:0.25rem}.g-xl-1,.gy-xl-1{--bs-gutter-y:0.25rem}.g-xl-2,.gx-xl-2{--bs-gutter-x:0.5rem}.g-xl-2,.gy-xl-2{--bs-gutter-y:0.5rem}.g-xl-3,.gx-xl-3{--bs-gutter-x:1rem}.g-xl-3,.gy-xl-3{--bs-gutter-y:1rem}.g-xl-4,.gx-xl-4{--bs-gutter-x:1.5rem}.g-xl-4,.gy-xl-4{--bs-gutter-y:1.5rem}.g-xl-5,.gx-xl-5{--bs-gutter-x:3rem}.g-xl-5,.gy-xl-5{--bs-gutter-y:3rem}}@media (min-width:1400px){.col-xxl-auto{flex:0 0 auto;width:auto}.col-xxl-1{flex:0 0 auto;width:8.33333333%}.col-xxl-2{flex:0 0 auto;width:16.66666667%}.col-xxl-3{flex:0 0 auto;width:25%}.col-xxl-4{flex:0 0 auto;width:33.33333333%}.col-xxl-5{flex:0 0 auto;width:41.66666667%}.col-xxl-6{flex:0 0 auto;width:50%}.col-xxl-7{flex:0 0 auto;width:58.33333333%}.col-xxl-8{flex:0 0 auto;width:66.66666667%}.col-xxl-9{flex:0 0 auto;width:75%}.col-xxl-10{flex:0 0 auto;width:83.33333333%}.col-xxl-11{flex:0 0 auto;width:91.66666667%}.col-xxl-12{flex:0 0 auto;width:100%}.offset-xxl-0{margin-left:0}.offset-xxl-1{margin-left:8.33333333%}.offset-xxl-2{margin-left:16.66666667%}.offset-xxl-3{margin-left:25%}.offset-xxl-4{margin-left:33.33333333%}.offset-xxl-5{margin-left:41.66666667%}.offset-xxl-6{margin-left:50%}.offset-xxl-7{margin-left:58.33333333%}.offset-xxl-8{margin-left:66.66666667%}.offset-xxl-9{margin-left:75%}.offset-xxl-10{margin-left:83.33333333%}.offset-xxl-11{margin-left:91.66666667%}.g-xxl-0,.gx-xxl-0{--bs-gutter-x:0}.g-xxl-0,.gy-xxl-0{--bs-gutter-y:0}.g-xxl-1,.gx-xxl-1{--bs-gutter-x:0.25rem}.g-xxl-1,.gy-xxl-1{--bs-gutter-y:0.25rem}.g-xxl-2,.gx-xxl-2{--bs-gutter-x:0.5rem}.g-xxl-2,.gy-xxl-2{--bs-gutter-y:0.5rem}.g-xxl-3,.gx-xxl-3{--bs-gutter-x:1rem}.g-xxl-3,.gy-xxl-3{--bs-gutter-y:1rem}.g-xxl-4,.gx-xxl-4{--bs-gutter-x:1.5rem}.g-xxl-4,.gy-xxl-4{--bs-gutter-y:1.5rem}.g-xxl-5,.gx-xxl-5{--bs-gutter-x:3rem}.g-xxl-5,.gy-xxl-5{--bs-gutter-y:3rem}}.table{--bs-table-bg:transparent;--bs-table-accent-bg:transparent;--bs-table-striped-color:#212529;--bs-table-striped-bg:rgba(0,0,0,0.05);--bs-table-active-color:#212529;--bs-table-active-bg:rgba(0,0,0,0.1);--bs-table-hover-color:#212529;--bs-table-hover-bg:rgba(0,0,0,0.075);width:100%;margin-bottom:1rem;color:#212529;vertical-align:top;border-color:#dee2e6}.table>:not(caption)>*>*{padding:.5rem;background-color:var(--bs-table-bg);border-bottom-width:1px;box-shadow:inset 0 0 0 9999px var(--bs-table-accent-bg)}.table>tbody{vertical-align:inherit}.table>thead{vertical-align:bottom}.table>:not(:last-child)>:last-child>*{border-bottom-color:currentColor}.caption-top{caption-side:top}.table-sm>:not(caption)>*>*{padding:.25rem}.table-bordered>:not(caption)>*{border-width:1px 0}.table-bordered>:not(caption)>*>*{border-width:0 1px}.table-borderless>:not(caption)>*>*{border-bottom-width:0}.table-striped>tbody>tr:nth-of-type(odd){--bs-table-accent-bg:var(--bs-table-striped-bg);color:var(--bs-table-striped-color)}.table-active{--bs-table-accent-bg:var(--bs-table-active-bg);color:var(--bs-table-active-color)}.table-hover>tbody>tr:hover{--bs-table-accent-bg:var(--bs-table-hover-bg);color:var(--bs-table-hover-color)}.table-primary{--bs-table-bg:#cfe2ff;--bs-table-striped-bg:#c5d7f2;--bs-table-striped-color:#000;--bs-table-active-bg:#bacbe6;--bs-table-active-color:#000;--bs-table-hover-bg:#bfd1ec;--bs-table-hover-color:#000;color:#000;border-color:#bacbe6}.table-secondary{--bs-table-bg:#e2e3e5;--bs-table-striped-bg:#d7d8da;--bs-table-striped-color:#000;--bs-table-active-bg:#cbccce;--bs-table-active-color:#000;--bs-table-hover-bg:#d1d2d4;--bs-table-hover-color:#000;color:#000;border-color:#cbccce}.table-success{--bs-table-bg:#d1e7dd;--bs-table-striped-bg:#c7dbd2;--bs-table-striped-color:#000;--bs-table-active-bg:#bcd0c7;--bs-table-active-color:#000;--bs-table-hover-bg:#c1d6cc;--bs-table-hover-color:#000;color:#000;border-color:#bcd0c7}.table-info{--bs-table-bg:#cff4fc;--bs-table-striped-bg:#c5e8ef;--bs-table-striped-color:#000;--bs-table-active-bg:#badce3;--bs-table-active-color:#000;--bs-table-hover-bg:#bfe2e9;--bs-table-hover-color:#000;color:#000;border-color:#badce3}.table-warning{--bs-table-bg:#fff3cd;--bs-table-striped-bg:#f2e7c3;--bs-table-striped-color:#000;--bs-table-active-bg:#e6dbb9;--bs-table-active-color:#000;--bs-table-hover-bg:#ece1be;--bs-table-hover-color:#000;color:#000;border-color:#e6dbb9}.table-danger{--bs-table-bg:#f8d7da;--bs-table-striped-bg:#eccccf;--bs-table-striped-color:#000;--bs-table-active-bg:#dfc2c4;--bs-table-active-color:#000;--bs-table-hover-bg:#e5c7ca;--bs-table-hover-color:#000;color:#000;border-color:#dfc2c4}.table-light{--bs-table-bg:#f8f9fa;--bs-table-striped-bg:#ecedee;--bs-table-striped-color:#000;--bs-table-active-bg:#dfe0e1;--bs-table-active-color:#000;--bs-table-hover-bg:#e5e6e7;--bs-table-hover-color:#000;color:#000;border-color:#dfe0e1}.table-dark{--bs-table-bg:#212529;--bs-table-striped-bg:#2c3034;--bs-table-striped-color:#fff;--bs-table-active-bg:#373b3e;--bs-table-active-color:#fff;--bs-table-hover-bg:#323539;--bs-table-hover-color:#fff;color:#fff;border-color:#373b3e}.table-responsive{overflow-x:auto;-webkit-overflow-scrolling:touch}@media (max-width:575.98px){.table-responsive-sm{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:767.98px){.table-responsive-md{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:991.98px){.table-responsive-lg{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1199.98px){.table-responsive-xl{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1399.98px){.table-responsive-xxl{overflow-x:auto;-webkit-overflow-scrolling:touch}}.form-label{margin-bottom:.5rem}.col-form-label{padding-top:calc(.375rem + 1px);padding-bottom:calc(.375rem + 1px);margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(.5rem + 1px);padding-bottom:calc(.5rem + 1px);font-size:1.25rem}.col-form-label-sm{padding-top:calc(.25rem + 1px);padding-bottom:calc(.25rem + 1px);font-size:.875rem}.form-text{margin-top:.25rem;font-size:.875em;color:#6c757d}.form-control{display:block;width:100%;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;background-clip:padding-box;border:1px solid #ced4da;-webkit-appearance:none;-moz-appearance:none;appearance:none;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control{transition:none}}.form-control[type=file]{overflow:hidden}.form-control[type=file]:not(:disabled):not([readonly]){cursor:pointer}.form-control:focus{color:#212529;background-color:#fff;border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-control::-webkit-date-and-time-value{height:1.5em}.form-control::-webkit-input-placeholder{color:#6c757d;opacity:1}.form-control:-ms-input-placeholder{color:#6c757d;opacity:1}.form-control::placeholder{color:#6c757d;opacity:1}.form-control:disabled,.form-control[readonly]{background-color:#e9ecef;opacity:1}.form-control::file-selector-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:#212529;background-color:#e9ecef;pointer-events:none;border:0 solid;border-color:inherit;border-inline-end-width:1px;border-radius:0;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control::file-selector-button{transition:none}}.form-control:hover:not(:disabled):not([readonly])::file-selector-button{background-color:#dde0e3}.form-control::-webkit-file-upload-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:#212529;background-color:#e9ecef;pointer-events:none;border:0 solid;border-color:inherit;border-inline-end-width:1px;border-radius:0;-webkit-transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control::-webkit-file-upload-button{-webkit-transition:none;transition:none}}.form-control:hover:not(:disabled):not([readonly])::-webkit-file-upload-button{background-color:#dde0e3}.form-control-plaintext{display:block;width:100%;padding:.375rem 0;margin-bottom:0;line-height:1.5;color:#212529;background-color:transparent;border:solid transparent;border-width:1px 0}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm{padding-right:0;padding-left:0}.form-control-sm{min-height:calc(1.5em + .5rem + 2px);padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.form-control-sm::file-selector-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-sm::-webkit-file-upload-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-lg{min-height:calc(1.5em + 1rem + 2px);padding:.5rem 1rem;font-size:1.25rem;border-radius:.3rem}.form-control-lg::file-selector-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}.form-control-lg::-webkit-file-upload-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}textarea.form-control{min-height:calc(1.5em + .75rem + 2px)}textarea.form-control-sm{min-height:calc(1.5em + .5rem + 2px)}textarea.form-control-lg{min-height:calc(1.5em + 1rem + 2px)}.form-control-color{max-width:3rem;height:auto;padding:.375rem}.form-control-color:not(:disabled):not([readonly]){cursor:pointer}.form-control-color::-moz-color-swatch{height:1.5em;border-radius:.25rem}.form-control-color::-webkit-color-swatch{height:1.5em;border-radius:.25rem}.form-select{display:block;width:100%;padding:.375rem 2.25rem .375rem .75rem;-moz-padding-start:calc(.75rem - 3px);font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right .75rem center;background-size:16px 12px;border:1px solid #ced4da;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:none;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-select{transition:none}}.form-select:focus{border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-select[multiple],.form-select[size]:not([size="1"]){padding-right:.75rem;background-image:none}.form-select:disabled{background-color:#e9ecef}.form-select:-moz-focusring{color:transparent;text-shadow:0 0 0 #212529}.form-select-sm{padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:.875rem}.form-select-lg{padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:1.25rem}.form-check{display:block;min-height:1.5rem;padding-left:1.5em;margin-bottom:.125rem}.form-check .form-check-input{float:left;margin-left:-1.5em}.form-check-input{width:1em;height:1em;margin-top:.25em;vertical-align:top;background-color:#fff;background-repeat:no-repeat;background-position:50%;background-size:contain;border:1px solid rgba(0,0,0,.25);-webkit-appearance:none;-moz-appearance:none;appearance:none;-webkit-print-color-adjust:exact;color-adjust:exact}.form-check-input[type=checkbox]{border-radius:.25em}.form-check-input[type=radio]{border-radius:50%}.form-check-input:active{-webkit-filter:brightness(90%);filter:brightness(90%)}.form-check-input:focus{border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-check-input:checked{background-color:#0d6efd;border-color:#0d6efd}.form-check-input:checked[type=checkbox]{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10l3 3 6-6'/%3E%3C/svg%3E")}.form-check-input:checked[type=radio]{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='2' fill='%23fff'/%3E%3C/svg%3E")}.form-check-input[type=checkbox]:indeterminate{background-color:#0d6efd;border-color:#0d6efd;background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3E%3C/svg%3E")}.form-check-input:disabled{pointer-events:none;-webkit-filter:none;filter:none;opacity:.5}.form-check-input:disabled~.form-check-label,.form-check-input[disabled]~.form-check-label{opacity:.5}.form-switch{padding-left:2.5em}.form-switch .form-check-input{width:2em;margin-left:-2.5em;background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='rgba(0, 0, 0, 0.25)'/%3E%3C/svg%3E");background-position:0;border-radius:2em;transition:background-position .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-switch .form-check-input{transition:none}}.form-switch .form-check-input:focus{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='%2386b7fe'/%3E%3C/svg%3E")}.form-switch .form-check-input:checked{background-position:100%;background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='%23fff'/%3E%3C/svg%3E")}.form-check-inline{display:inline-block;margin-right:1rem}.btn-check{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.btn-check:disabled+.btn,.btn-check[disabled]+.btn{pointer-events:none;-webkit-filter:none;filter:none;opacity:.65}.form-range{width:100%;height:1.5rem;padding:0;background-color:transparent;-webkit-appearance:none;-moz-appearance:none;appearance:none}.form-range:focus{outline:0}.form-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(13,110,253,.25)}.form-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(13,110,253,.25)}.form-range::-moz-focus-outer{border:0}.form-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.25rem;background-color:#0d6efd;border:0;border-radius:1rem;-webkit-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-range::-webkit-slider-thumb{-webkit-transition:none;transition:none}}.form-range::-webkit-slider-thumb:active{background-color:#b6d4fe}.form-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.form-range::-moz-range-thumb{width:1rem;height:1rem;background-color:#0d6efd;border:0;border-radius:1rem;-moz-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-range::-moz-range-thumb{-moz-transition:none;transition:none}}.form-range::-moz-range-thumb:active{background-color:#b6d4fe}.form-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.form-range:disabled{pointer-events:none}.form-range:disabled::-webkit-slider-thumb{background-color:#adb5bd}.form-range:disabled::-moz-range-thumb{background-color:#adb5bd}.form-floating{position:relative}.form-floating>.form-control,.form-floating>.form-select{height:calc(3.5rem + 2px);line-height:1.25}.form-floating>label{position:absolute;top:0;left:0;height:100%;padding:1rem .75rem;pointer-events:none;border:1px solid transparent;transform-origin:0 0;transition:opacity .1s ease-in-out,transform .1s ease-in-out}@media (prefers-reduced-motion:reduce){.form-floating>label{transition:none}}.form-floating>.form-control{padding:1rem .75rem}.form-floating>.form-control::-webkit-input-placeholder{color:transparent}.form-floating>.form-control:-ms-input-placeholder{color:transparent}.form-floating>.form-control::placeholder{color:transparent}.form-floating>.form-control:not(:-ms-input-placeholder){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:focus,.form-floating>.form-control:not(:placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:-webkit-autofill{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-select{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:not(:-ms-input-placeholder)~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control:focus~label,.form-floating>.form-control:not(:placeholder-shown)~label,.form-floating>.form-select~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control:-webkit-autofill~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.input-group{position:relative;display:flex;flex-wrap:wrap;align-items:stretch;width:100%}.input-group>.form-control,.input-group>.form-select{position:relative;flex:1 1 auto;width:1%;min-width:0}.input-group>.form-control:focus,.input-group>.form-select:focus{z-index:3}.input-group .btn{position:relative;z-index:2}.input-group .btn:focus{z-index:3}.input-group-text{display:flex;align-items:center;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:center;white-space:nowrap;background-color:#e9ecef;border:1px solid #ced4da;border-radius:.25rem}.input-group-lg>.btn,.input-group-lg>.form-control,.input-group-lg>.form-select,.input-group-lg>.input-group-text{padding:.5rem 1rem;font-size:1.25rem;border-radius:.3rem}.input-group-sm>.btn,.input-group-sm>.form-control,.input-group-sm>.form-select,.input-group-sm>.input-group-text{padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.input-group-lg>.form-select,.input-group-sm>.form-select{padding-right:3rem}.input-group.has-validation>.dropdown-toggle:nth-last-child(n+4),.input-group.has-validation>:nth-last-child(n+3):not(.dropdown-toggle):not(.dropdown-menu),.input-group:not(.has-validation)>.dropdown-toggle:nth-last-child(n+3),.input-group:not(.has-validation)>:not(:last-child):not(.dropdown-toggle):not(.dropdown-menu){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>:not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback){margin-left:-1px;border-top-left-radius:0;border-bottom-left-radius:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:#198754}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:rgba(25,135,84,.9);border-radius:.25rem}.is-valid~.valid-feedback,.is-valid~.valid-tooltip,.was-validated :valid~.valid-feedback,.was-validated :valid~.valid-tooltip{display:block}.form-control.is-valid,.was-validated .form-control:valid{border-color:#198754;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23198754' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-valid:focus,.was-validated .form-control:valid:focus{border-color:#198754;box-shadow:0 0 0 .25rem rgba(25,135,84,.25)}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.form-select.is-valid,.was-validated .form-select:valid{border-color:#198754}.form-select.is-valid:not([multiple]):not([size]),.form-select.is-valid:not([multiple])[size="1"],.was-validated .form-select:valid:not([multiple]):not([size]),.was-validated .form-select:valid:not([multiple])[size="1"]{padding-right:4.125rem;background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3E%3C/svg%3E"),url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23198754' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3E%3C/svg%3E");background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.form-select.is-valid:focus,.was-validated .form-select:valid:focus{border-color:#198754;box-shadow:0 0 0 .25rem rgba(25,135,84,.25)}.form-check-input.is-valid,.was-validated .form-check-input:valid{border-color:#198754}.form-check-input.is-valid:checked,.was-validated .form-check-input:valid:checked{background-color:#198754}.form-check-input.is-valid:focus,.was-validated .form-check-input:valid:focus{box-shadow:0 0 0 .25rem rgba(25,135,84,.25)}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:#198754}.form-check-inline .form-check-input~.valid-feedback{margin-left:.5em}.input-group .form-control.is-valid,.input-group .form-select.is-valid,.was-validated .input-group .form-control:valid,.was-validated .input-group .form-select:valid{z-index:1}.input-group .form-control.is-valid:focus,.input-group .form-select.is-valid:focus,.was-validated .input-group .form-control:valid:focus,.was-validated .input-group .form-select:valid:focus{z-index:3}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:#dc3545}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:rgba(220,53,69,.9);border-radius:.25rem}.is-invalid~.invalid-feedback,.is-invalid~.invalid-tooltip,.was-validated :invalid~.invalid-feedback,.was-validated :invalid~.invalid-tooltip{display:block}.form-control.is-invalid,.was-validated .form-control:invalid{border-color:#dc3545;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23dc3545'%3E%3Ccircle cx='6' cy='6' r='4.5'/%3E%3Cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3E%3Ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-invalid:focus,.was-validated .form-control:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.form-select.is-invalid,.was-validated .form-select:invalid{border-color:#dc3545}.form-select.is-invalid:not([multiple]):not([size]),.form-select.is-invalid:not([multiple])[size="1"],.was-validated .form-select:invalid:not([multiple]):not([size]),.was-validated .form-select:invalid:not([multiple])[size="1"]{padding-right:4.125rem;background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3E%3C/svg%3E"),url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23dc3545'%3E%3Ccircle cx='6' cy='6' r='4.5'/%3E%3Cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3E%3Ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3E%3C/svg%3E");background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.form-select.is-invalid:focus,.was-validated .form-select:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.form-check-input.is-invalid,.was-validated .form-check-input:invalid{border-color:#dc3545}.form-check-input.is-invalid:checked,.was-validated .form-check-input:invalid:checked{background-color:#dc3545}.form-check-input.is-invalid:focus,.was-validated .form-check-input:invalid:focus{box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:#dc3545}.form-check-inline .form-check-input~.invalid-feedback{margin-left:.5em}.input-group .form-control.is-invalid,.input-group .form-select.is-invalid,.was-validated .input-group .form-control:invalid,.was-validated .input-group .form-select:invalid{z-index:2}.input-group .form-control.is-invalid:focus,.input-group .form-select.is-invalid:focus,.was-validated .input-group .form-control:invalid:focus,.was-validated .input-group .form-select:invalid:focus{z-index:3}.btn{display:inline-block;font-weight:400;line-height:1.5;color:#212529;text-align:center;text-decoration:none;vertical-align:middle;cursor:pointer;-webkit-user-select:none;-ms-user-select:none;user-select:none;background-color:transparent;border:1px solid transparent;padding:.375rem .75rem;font-size:1rem;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.btn{transition:none}}.btn:hover{color:#212529}.btn-check:focus+.btn,.btn:focus{outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.btn.disabled,.btn:disabled,fieldset:disabled .btn{pointer-events:none;opacity:.65}.btn-primary{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-check:focus+.btn-primary,.btn-primary:focus,.btn-primary:hover{color:#fff;background-color:#0b5ed7;border-color:#0a58ca}.btn-check:focus+.btn-primary,.btn-primary:focus{box-shadow:0 0 0 .25rem rgba(49,132,253,.5)}.btn-check:active+.btn-primary,.btn-check:checked+.btn-primary,.btn-primary.active,.btn-primary:active,.show>.btn-primary.dropdown-toggle{color:#fff;background-color:#0a58ca;border-color:#0a53be}.btn-check:active+.btn-primary:focus,.btn-check:checked+.btn-primary:focus,.btn-primary.active:focus,.btn-primary:active:focus,.show>.btn-primary.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(49,132,253,.5)}.btn-primary.disabled,.btn-primary:disabled{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-secondary{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-check:focus+.btn-secondary,.btn-secondary:focus,.btn-secondary:hover{color:#fff;background-color:#5c636a;border-color:#565e64}.btn-check:focus+.btn-secondary,.btn-secondary:focus{box-shadow:0 0 0 .25rem rgba(130,138,145,.5)}.btn-check:active+.btn-secondary,.btn-check:checked+.btn-secondary,.btn-secondary.active,.btn-secondary:active,.show>.btn-secondary.dropdown-toggle{color:#fff;background-color:#565e64;border-color:#51585e}.btn-check:active+.btn-secondary:focus,.btn-check:checked+.btn-secondary:focus,.btn-secondary.active:focus,.btn-secondary:active:focus,.show>.btn-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(130,138,145,.5)}.btn-secondary.disabled,.btn-secondary:disabled{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-success{color:#fff;background-color:#198754;border-color:#198754}.btn-check:focus+.btn-success,.btn-success:focus,.btn-success:hover{color:#fff;background-color:#157347;border-color:#146c43}.btn-check:focus+.btn-success,.btn-success:focus{box-shadow:0 0 0 .25rem rgba(60,153,110,.5)}.btn-check:active+.btn-success,.btn-check:checked+.btn-success,.btn-success.active,.btn-success:active,.show>.btn-success.dropdown-toggle{color:#fff;background-color:#146c43;border-color:#13653f}.btn-check:active+.btn-success:focus,.btn-check:checked+.btn-success:focus,.btn-success.active:focus,.btn-success:active:focus,.show>.btn-success.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(60,153,110,.5)}.btn-success.disabled,.btn-success:disabled{color:#fff;background-color:#198754;border-color:#198754}.btn-info{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-check:focus+.btn-info,.btn-info:focus,.btn-info:hover{color:#000;background-color:#31d2f2;border-color:#25cff2}.btn-check:focus+.btn-info,.btn-info:focus{box-shadow:0 0 0 .25rem rgba(11,172,204,.5)}.btn-check:active+.btn-info,.btn-check:checked+.btn-info,.btn-info.active,.btn-info:active,.show>.btn-info.dropdown-toggle{color:#000;background-color:#3dd5f3;border-color:#25cff2}.btn-check:active+.btn-info:focus,.btn-check:checked+.btn-info:focus,.btn-info.active:focus,.btn-info:active:focus,.show>.btn-info.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(11,172,204,.5)}.btn-info.disabled,.btn-info:disabled{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-warning{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-check:focus+.btn-warning,.btn-warning:focus,.btn-warning:hover{color:#000;background-color:#ffca2c;border-color:#ffc720}.btn-check:focus+.btn-warning,.btn-warning:focus{box-shadow:0 0 0 .25rem rgba(217,164,6,.5)}.btn-check:active+.btn-warning,.btn-check:checked+.btn-warning,.btn-warning.active,.btn-warning:active,.show>.btn-warning.dropdown-toggle{color:#000;background-color:#ffcd39;border-color:#ffc720}.btn-check:active+.btn-warning:focus,.btn-check:checked+.btn-warning:focus,.btn-warning.active:focus,.btn-warning:active:focus,.show>.btn-warning.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(217,164,6,.5)}.btn-warning.disabled,.btn-warning:disabled{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-danger{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-check:focus+.btn-danger,.btn-danger:focus,.btn-danger:hover{color:#fff;background-color:#bb2d3b;border-color:#b02a37}.btn-check:focus+.btn-danger,.btn-danger:focus{box-shadow:0 0 0 .25rem rgba(225,83,97,.5)}.btn-check:active+.btn-danger,.btn-check:checked+.btn-danger,.btn-danger.active,.btn-danger:active,.show>.btn-danger.dropdown-toggle{color:#fff;background-color:#b02a37;border-color:#a52834}.btn-check:active+.btn-danger:focus,.btn-check:checked+.btn-danger:focus,.btn-danger.active:focus,.btn-danger:active:focus,.show>.btn-danger.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(225,83,97,.5)}.btn-danger.disabled,.btn-danger:disabled{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-light{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-check:focus+.btn-light,.btn-light:focus,.btn-light:hover{color:#000;background-color:#f9fafb;border-color:#f9fafb}.btn-check:focus+.btn-light,.btn-light:focus{box-shadow:0 0 0 .25rem rgba(211,212,213,.5)}.btn-check:active+.btn-light,.btn-check:checked+.btn-light,.btn-light.active,.btn-light:active,.show>.btn-light.dropdown-toggle{color:#000;background-color:#f9fafb;border-color:#f9fafb}.btn-check:active+.btn-light:focus,.btn-check:checked+.btn-light:focus,.btn-light.active:focus,.btn-light:active:focus,.show>.btn-light.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(211,212,213,.5)}.btn-light.disabled,.btn-light:disabled{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-dark{color:#fff;background-color:#212529;border-color:#212529}.btn-check:focus+.btn-dark,.btn-dark:focus,.btn-dark:hover{color:#fff;background-color:#1c1f23;border-color:#1a1e21}.btn-check:focus+.btn-dark,.btn-dark:focus{box-shadow:0 0 0 .25rem rgba(66,70,73,.5)}.btn-check:active+.btn-dark,.btn-check:checked+.btn-dark,.btn-dark.active,.btn-dark:active,.show>.btn-dark.dropdown-toggle{color:#fff;background-color:#1a1e21;border-color:#191c1f}.btn-check:active+.btn-dark:focus,.btn-check:checked+.btn-dark:focus,.btn-dark.active:focus,.btn-dark:active:focus,.show>.btn-dark.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(66,70,73,.5)}.btn-dark.disabled,.btn-dark:disabled{color:#fff;background-color:#212529;border-color:#212529}.btn-outline-primary{color:#0d6efd;border-color:#0d6efd}.btn-outline-primary:hover{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-check:focus+.btn-outline-primary,.btn-outline-primary:focus{box-shadow:0 0 0 .25rem rgba(13,110,253,.5)}.btn-check:active+.btn-outline-primary,.btn-check:checked+.btn-outline-primary,.btn-outline-primary.active,.btn-outline-primary.dropdown-toggle.show,.btn-outline-primary:active{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-check:active+.btn-outline-primary:focus,.btn-check:checked+.btn-outline-primary:focus,.btn-outline-primary.active:focus,.btn-outline-primary.dropdown-toggle.show:focus,.btn-outline-primary:active:focus{box-shadow:0 0 0 .25rem rgba(13,110,253,.5)}.btn-outline-primary.disabled,.btn-outline-primary:disabled{color:#0d6efd;background-color:transparent}.btn-outline-secondary{color:#6c757d;border-color:#6c757d}.btn-outline-secondary:hover{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-check:focus+.btn-outline-secondary,.btn-outline-secondary:focus{box-shadow:0 0 0 .25rem rgba(108,117,125,.5)}.btn-check:active+.btn-outline-secondary,.btn-check:checked+.btn-outline-secondary,.btn-outline-secondary.active,.btn-outline-secondary.dropdown-toggle.show,.btn-outline-secondary:active{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-check:active+.btn-outline-secondary:focus,.btn-check:checked+.btn-outline-secondary:focus,.btn-outline-secondary.active:focus,.btn-outline-secondary.dropdown-toggle.show:focus,.btn-outline-secondary:active:focus{box-shadow:0 0 0 .25rem rgba(108,117,125,.5)}.btn-outline-secondary.disabled,.btn-outline-secondary:disabled{color:#6c757d;background-color:transparent}.btn-outline-success{color:#198754;border-color:#198754}.btn-outline-success:hover{color:#fff;background-color:#198754;border-color:#198754}.btn-check:focus+.btn-outline-success,.btn-outline-success:focus{box-shadow:0 0 0 .25rem rgba(25,135,84,.5)}.btn-check:active+.btn-outline-success,.btn-check:checked+.btn-outline-success,.btn-outline-success.active,.btn-outline-success.dropdown-toggle.show,.btn-outline-success:active{color:#fff;background-color:#198754;border-color:#198754}.btn-check:active+.btn-outline-success:focus,.btn-check:checked+.btn-outline-success:focus,.btn-outline-success.active:focus,.btn-outline-success.dropdown-toggle.show:focus,.btn-outline-success:active:focus{box-shadow:0 0 0 .25rem rgba(25,135,84,.5)}.btn-outline-success.disabled,.btn-outline-success:disabled{color:#198754;background-color:transparent}.btn-outline-info{color:#0dcaf0;border-color:#0dcaf0}.btn-outline-info:hover{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-check:focus+.btn-outline-info,.btn-outline-info:focus{box-shadow:0 0 0 .25rem rgba(13,202,240,.5)}.btn-check:active+.btn-outline-info,.btn-check:checked+.btn-outline-info,.btn-outline-info.active,.btn-outline-info.dropdown-toggle.show,.btn-outline-info:active{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-check:active+.btn-outline-info:focus,.btn-check:checked+.btn-outline-info:focus,.btn-outline-info.active:focus,.btn-outline-info.dropdown-toggle.show:focus,.btn-outline-info:active:focus{box-shadow:0 0 0 .25rem rgba(13,202,240,.5)}.btn-outline-info.disabled,.btn-outline-info:disabled{color:#0dcaf0;background-color:transparent}.btn-outline-warning{color:#ffc107;border-color:#ffc107}.btn-outline-warning:hover{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-check:focus+.btn-outline-warning,.btn-outline-warning:focus{box-shadow:0 0 0 .25rem rgba(255,193,7,.5)}.btn-check:active+.btn-outline-warning,.btn-check:checked+.btn-outline-warning,.btn-outline-warning.active,.btn-outline-warning.dropdown-toggle.show,.btn-outline-warning:active{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-check:active+.btn-outline-warning:focus,.btn-check:checked+.btn-outline-warning:focus,.btn-outline-warning.active:focus,.btn-outline-warning.dropdown-toggle.show:focus,.btn-outline-warning:active:focus{box-shadow:0 0 0 .25rem rgba(255,193,7,.5)}.btn-outline-warning.disabled,.btn-outline-warning:disabled{color:#ffc107;background-color:transparent}.btn-outline-danger{color:#dc3545;border-color:#dc3545}.btn-outline-danger:hover{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-check:focus+.btn-outline-danger,.btn-outline-danger:focus{box-shadow:0 0 0 .25rem rgba(220,53,69,.5)}.btn-check:active+.btn-outline-danger,.btn-check:checked+.btn-outline-danger,.btn-outline-danger.active,.btn-outline-danger.dropdown-toggle.show,.btn-outline-danger:active{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-check:active+.btn-outline-danger:focus,.btn-check:checked+.btn-outline-danger:focus,.btn-outline-danger.active:focus,.btn-outline-danger.dropdown-toggle.show:focus,.btn-outline-danger:active:focus{box-shadow:0 0 0 .25rem rgba(220,53,69,.5)}.btn-outline-danger.disabled,.btn-outline-danger:disabled{color:#dc3545;background-color:transparent}.btn-outline-light{color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light:hover{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-check:focus+.btn-outline-light,.btn-outline-light:focus{box-shadow:0 0 0 .25rem rgba(248,249,250,.5)}.btn-check:active+.btn-outline-light,.btn-check:checked+.btn-outline-light,.btn-outline-light.active,.btn-outline-light.dropdown-toggle.show,.btn-outline-light:active{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-check:active+.btn-outline-light:focus,.btn-check:checked+.btn-outline-light:focus,.btn-outline-light.active:focus,.btn-outline-light.dropdown-toggle.show:focus,.btn-outline-light:active:focus{box-shadow:0 0 0 .25rem rgba(248,249,250,.5)}.btn-outline-light.disabled,.btn-outline-light:disabled{color:#f8f9fa;background-color:transparent}.btn-outline-dark{color:#212529;border-color:#212529}.btn-outline-dark:hover{color:#fff;background-color:#212529;border-color:#212529}.btn-check:focus+.btn-outline-dark,.btn-outline-dark:focus{box-shadow:0 0 0 .25rem rgba(33,37,41,.5)}.btn-check:active+.btn-outline-dark,.btn-check:checked+.btn-outline-dark,.btn-outline-dark.active,.btn-outline-dark.dropdown-toggle.show,.btn-outline-dark:active{color:#fff;background-color:#212529;border-color:#212529}.btn-check:active+.btn-outline-dark:focus,.btn-check:checked+.btn-outline-dark:focus,.btn-outline-dark.active:focus,.btn-outline-dark.dropdown-toggle.show:focus,.btn-outline-dark:active:focus{box-shadow:0 0 0 .25rem rgba(33,37,41,.5)}.btn-outline-dark.disabled,.btn-outline-dark:disabled{color:#212529;background-color:transparent}.btn-link{font-weight:400;color:#0d6efd;text-decoration:underline}.btn-link:hover{color:#0a58ca}.btn-link.disabled,.btn-link:disabled{color:#6c757d}.btn-group-lg>.btn,.btn-lg{padding:.5rem 1rem;font-size:1.25rem;border-radius:.3rem}.btn-group-sm>.btn,.btn-sm{padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.fade{transition:opacity .15s linear}@media (prefers-reduced-motion:reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{height:0;overflow:hidden;transition:height .35s ease}@media (prefers-reduced-motion:reduce){.collapsing{transition:none}}.dropdown,.dropend,.dropstart,.dropup{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle:after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-toggle:empty:after{margin-left:0}.dropdown-menu{position:absolute;z-index:1000;display:none;min-width:10rem;padding:.5rem 0;margin:0;font-size:1rem;color:#212529;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.15);border-radius:.25rem}.dropdown-menu[data-bs-popper]{top:100%;left:0;margin-top:.125rem}.dropdown-menu-start{--bs-position:start}.dropdown-menu-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-end{--bs-position:end}.dropdown-menu-end[data-bs-popper]{right:0;left:auto}@media (min-width:576px){.dropdown-menu-sm-start{--bs-position:start}.dropdown-menu-sm-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-sm-end{--bs-position:end}.dropdown-menu-sm-end[data-bs-popper]{right:0;left:auto}}@media (min-width:768px){.dropdown-menu-md-start{--bs-position:start}.dropdown-menu-md-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-md-end{--bs-position:end}.dropdown-menu-md-end[data-bs-popper]{right:0;left:auto}}@media (min-width:992px){.dropdown-menu-lg-start{--bs-position:start}.dropdown-menu-lg-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-lg-end{--bs-position:end}.dropdown-menu-lg-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1200px){.dropdown-menu-xl-start{--bs-position:start}.dropdown-menu-xl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xl-end{--bs-position:end}.dropdown-menu-xl-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1400px){.dropdown-menu-xxl-start{--bs-position:start}.dropdown-menu-xxl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xxl-end{--bs-position:end}.dropdown-menu-xxl-end[data-bs-popper]{right:0;left:auto}}.dropup .dropdown-menu[data-bs-popper]{top:auto;bottom:100%;margin-top:0;margin-bottom:.125rem}.dropup .dropdown-toggle:after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid transparent;border-bottom:.3em solid;border-left:.3em solid transparent}.dropup .dropdown-toggle:empty:after{margin-left:0}.dropend .dropdown-menu[data-bs-popper]{top:0;right:auto;left:100%;margin-top:0;margin-left:.125rem}.dropend .dropdown-toggle:after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid}.dropend .dropdown-toggle:empty:after{margin-left:0}.dropend .dropdown-toggle:after{vertical-align:0}.dropstart .dropdown-menu[data-bs-popper]{top:0;right:100%;left:auto;margin-top:0;margin-right:.125rem}.dropstart .dropdown-toggle:after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";display:none}.dropstart .dropdown-toggle:before{display:inline-block;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropstart .dropdown-toggle:empty:after{margin-left:0}.dropstart .dropdown-toggle:before{vertical-align:0}.dropdown-divider{height:0;margin:.5rem 0;overflow:hidden;border-top:1px solid rgba(0,0,0,.15)}.dropdown-item{display:block;width:100%;padding:.25rem 1rem;clear:both;font-weight:400;color:#212529;text-align:inherit;text-decoration:none;white-space:nowrap;background-color:transparent;border:0}.dropdown-item:focus,.dropdown-item:hover{color:#1e2125;background-color:#e9ecef}.dropdown-item.active,.dropdown-item:active{color:#fff;text-decoration:none;background-color:#0d6efd}.dropdown-item.disabled,.dropdown-item:disabled{color:#adb5bd;pointer-events:none;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:.5rem 1rem;margin-bottom:0;font-size:.875rem;color:#6c757d;white-space:nowrap}.dropdown-item-text{display:block;padding:.25rem 1rem;color:#212529}.dropdown-menu-dark{color:#dee2e6;background-color:#343a40;border-color:rgba(0,0,0,.15)}.dropdown-menu-dark .dropdown-item{color:#dee2e6}.dropdown-menu-dark .dropdown-item:focus,.dropdown-menu-dark .dropdown-item:hover{color:#fff;background-color:hsla(0,0%,100%,.15)}.dropdown-menu-dark .dropdown-item.active,.dropdown-menu-dark .dropdown-item:active{color:#fff;background-color:#0d6efd}.dropdown-menu-dark .dropdown-item.disabled,.dropdown-menu-dark .dropdown-item:disabled{color:#adb5bd}.dropdown-menu-dark .dropdown-divider{border-color:rgba(0,0,0,.15)}.dropdown-menu-dark .dropdown-item-text{color:#dee2e6}.dropdown-menu-dark .dropdown-header{color:#adb5bd}.btn-group,.btn-group-vertical{position:relative;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;flex:1 1 auto}.btn-group-vertical>.btn-check:checked+.btn,.btn-group-vertical>.btn-check:focus+.btn,.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn-check:checked+.btn,.btn-group>.btn-check:focus+.btn,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:1}.btn-toolbar{display:flex;flex-wrap:wrap;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group>.btn-group:not(:first-child),.btn-group>.btn:not(:first-child){margin-left:-1px}.btn-group>.btn-group:not(:last-child)>.btn,.btn-group>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:not(:first-child)>.btn,.btn-group>.btn:nth-child(n+3),.btn-group>:not(.btn-check)+.btn{border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split:after,.dropend .dropdown-toggle-split:after,.dropup .dropdown-toggle-split:after{margin-left:0}.dropstart .dropdown-toggle-split:before{margin-right:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{flex-direction:column;align-items:flex-start;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn-group:not(:first-child),.btn-group-vertical>.btn:not(:first-child){margin-top:-1px}.btn-group-vertical>.btn-group:not(:last-child)>.btn,.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child)>.btn,.btn-group-vertical>.btn~.btn{border-top-left-radius:0;border-top-right-radius:0}.nav{display:flex;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:.5rem 1rem;color:#0d6efd;text-decoration:none;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out}@media (prefers-reduced-motion:reduce){.nav-link{transition:none}}.nav-link:focus,.nav-link:hover{color:#0a58ca}.nav-link.disabled{color:#6c757d;pointer-events:none;cursor:default}.nav-tabs{border-bottom:1px solid #dee2e6}.nav-tabs .nav-link{margin-bottom:-1px;background:none;border:1px solid transparent;border-top-left-radius:.25rem;border-top-right-radius:.25rem}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{border-color:#e9ecef #e9ecef #dee2e6;isolation:isolate}.nav-tabs .nav-link.disabled{color:#6c757d;background-color:transparent;border-color:transparent}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:#495057;background-color:#fff;border-color:#dee2e6 #dee2e6 #fff}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.nav-pills .nav-link{background:none;border:0;border-radius:.25rem}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:#fff;background-color:#0d6efd}.nav-fill .nav-item,.nav-fill>.nav-link{flex:1 1 auto;text-align:center}.nav-justified .nav-item,.nav-justified>.nav-link{flex-basis:0;flex-grow:1;text-align:center}.nav-fill .nav-item .nav-link,.nav-justified .nav-item .nav-link{width:100%}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{position:relative;display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;padding-top:.5rem;padding-bottom:.5rem}.navbar>.container,.navbar>.container-fluid,.navbar>.container-lg,.navbar>.container-md,.navbar>.container-sm,.navbar>.container-xl,.navbar>.container-xxl{display:flex;flex-wrap:inherit;align-items:center;justify-content:space-between}.navbar-brand{padding-top:.3125rem;padding-bottom:.3125rem;margin-right:1rem;font-size:1.25rem;text-decoration:none;white-space:nowrap}.navbar-nav{display:flex;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link{padding-right:0;padding-left:0}.navbar-nav .dropdown-menu{position:static}.navbar-text{padding-top:.5rem;padding-bottom:.5rem}.navbar-collapse{flex-basis:100%;flex-grow:1;align-items:center}.navbar-toggler{padding:.25rem .75rem;font-size:1.25rem;line-height:1;background-color:transparent;border:1px solid transparent;border-radius:.25rem;transition:box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.navbar-toggler{transition:none}}.navbar-toggler:hover{text-decoration:none}.navbar-toggler:focus{text-decoration:none;outline:0;box-shadow:0 0 0 .25rem}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;background-repeat:no-repeat;background-position:50%;background-size:100%}.navbar-nav-scroll{max-height:75vh;max-height:var(--bs-scroll-height,75vh);overflow-y:auto}@media (min-width:576px){.navbar-expand-sm{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-sm .navbar-nav{flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-sm .navbar-nav-scroll{overflow:visible}.navbar-expand-sm .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}}@media (min-width:768px){.navbar-expand-md{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-md .navbar-nav{flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-md .navbar-nav-scroll{overflow:visible}.navbar-expand-md .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}}@media (min-width:992px){.navbar-expand-lg{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-lg .navbar-nav{flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-lg .navbar-nav-scroll{overflow:visible}.navbar-expand-lg .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}}@media (min-width:1200px){.navbar-expand-xl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xl .navbar-nav{flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xl .navbar-nav-scroll{overflow:visible}.navbar-expand-xl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}}@media (min-width:1400px){.navbar-expand-xxl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xxl .navbar-nav{flex-direction:row}.navbar-expand-xxl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xxl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xxl .navbar-nav-scroll{overflow:visible}.navbar-expand-xxl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xxl .navbar-toggler{display:none}}.navbar-expand{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand .navbar-nav{flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand .navbar-nav-scroll{overflow:visible}.navbar-expand .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-light .navbar-brand,.navbar-light .navbar-brand:focus,.navbar-light .navbar-brand:hover{color:rgba(0,0,0,.9)}.navbar-light .navbar-nav .nav-link{color:rgba(0,0,0,.55)}.navbar-light .navbar-nav .nav-link:focus,.navbar-light .navbar-nav .nav-link:hover{color:rgba(0,0,0,.7)}.navbar-light .navbar-nav .nav-link.disabled{color:rgba(0,0,0,.3)}.navbar-light .navbar-nav .nav-link.active,.navbar-light .navbar-nav .show>.nav-link{color:rgba(0,0,0,.9)}.navbar-light .navbar-toggler{color:rgba(0,0,0,.55);border-color:rgba(0,0,0,.1)}.navbar-light .navbar-toggler-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3E%3Cpath stroke='rgba(0, 0, 0, 0.55)' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3E%3C/svg%3E")}.navbar-light .navbar-text{color:rgba(0,0,0,.55)}.navbar-light .navbar-text a,.navbar-light .navbar-text a:focus,.navbar-light .navbar-text a:hover{color:rgba(0,0,0,.9)}.navbar-dark .navbar-brand,.navbar-dark .navbar-brand:focus,.navbar-dark .navbar-brand:hover{color:#fff}.navbar-dark .navbar-nav .nav-link{color:hsla(0,0%,100%,.55)}.navbar-dark .navbar-nav .nav-link:focus,.navbar-dark .navbar-nav .nav-link:hover{color:hsla(0,0%,100%,.75)}.navbar-dark .navbar-nav .nav-link.disabled{color:hsla(0,0%,100%,.25)}.navbar-dark .navbar-nav .nav-link.active,.navbar-dark .navbar-nav .show>.nav-link{color:#fff}.navbar-dark .navbar-toggler{color:hsla(0,0%,100%,.55);border-color:hsla(0,0%,100%,.1)}.navbar-dark .navbar-toggler-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3E%3Cpath stroke='rgba(255, 255, 255, 0.55)' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3E%3C/svg%3E")}.navbar-dark .navbar-text{color:hsla(0,0%,100%,.55)}.navbar-dark .navbar-text a,.navbar-dark .navbar-text a:focus,.navbar-dark .navbar-text a:hover{color:#fff}.card{position:relative;display:flex;flex-direction:column;min-width:0;word-wrap:break-word;background-color:#fff;background-clip:border-box;border:1px solid rgba(0,0,0,.125);border-radius:.25rem}.card>hr{margin-right:0;margin-left:0}.card>.list-group{border-top:inherit;border-bottom:inherit}.card>.list-group:first-child{border-top-width:0;border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card>.list-group:last-child{border-bottom-width:0;border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card>.card-header+.list-group,.card>.list-group+.card-footer{border-top:0}.card-body{flex:1 1 auto;padding:1rem}.card-title{margin-bottom:.5rem}.card-subtitle{margin-top:-.25rem}.card-subtitle,.card-text:last-child{margin-bottom:0}.card-link:hover{text-decoration:none}.card-link+.card-link{margin-left:1rem}.card-header{padding:.5rem 1rem;margin-bottom:0;background-color:rgba(0,0,0,.03);border-bottom:1px solid rgba(0,0,0,.125)}.card-header:first-child{border-radius:calc(.25rem - 1px) calc(.25rem - 1px) 0 0}.card-footer{padding:.5rem 1rem;background-color:rgba(0,0,0,.03);border-top:1px solid rgba(0,0,0,.125)}.card-footer:last-child{border-radius:0 0 calc(.25rem - 1px) calc(.25rem - 1px)}.card-header-tabs{margin-bottom:-.5rem;border-bottom:0}.card-header-pills,.card-header-tabs{margin-right:-.5rem;margin-left:-.5rem}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1rem;border-radius:calc(.25rem - 1px)}.card-img,.card-img-bottom,.card-img-top{width:100%}.card-img,.card-img-top{border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card-img,.card-img-bottom{border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card-group>.card{margin-bottom:.75rem}@media (min-width:576px){.card-group{display:flex;flex-flow:row wrap}.card-group>.card{flex:1 0;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:not(:last-child) .card-header,.card-group>.card:not(:last-child) .card-img-top{border-top-right-radius:0}.card-group>.card:not(:last-child) .card-footer,.card-group>.card:not(:last-child) .card-img-bottom{border-bottom-right-radius:0}.card-group>.card:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:not(:first-child) .card-header,.card-group>.card:not(:first-child) .card-img-top{border-top-left-radius:0}.card-group>.card:not(:first-child) .card-footer,.card-group>.card:not(:first-child) .card-img-bottom{border-bottom-left-radius:0}}.accordion-button{position:relative;display:flex;align-items:center;width:100%;padding:1rem 1.25rem;font-size:1rem;color:#212529;text-align:left;background-color:#fff;border:0;border-radius:0;overflow-anchor:none;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out,border-radius .15s ease}@media (prefers-reduced-motion:reduce){.accordion-button{transition:none}}.accordion-button:not(.collapsed){color:#0c63e4;background-color:#e7f1ff;box-shadow:inset 0 -1px 0 rgba(0,0,0,.125)}.accordion-button:not(.collapsed):after{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%230c63e4'%3E%3Cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 01.708 0L8 10.293l5.646-5.647a.5.5 0 01.708.708l-6 6a.5.5 0 01-.708 0l-6-6a.5.5 0 010-.708z'/%3E%3C/svg%3E");transform:rotate(-180deg)}.accordion-button:after{flex-shrink:0;width:1.25rem;height:1.25rem;margin-left:auto;content:"";background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23212529'%3E%3Cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 01.708 0L8 10.293l5.646-5.647a.5.5 0 01.708.708l-6 6a.5.5 0 01-.708 0l-6-6a.5.5 0 010-.708z'/%3E%3C/svg%3E");background-repeat:no-repeat;background-size:1.25rem;transition:transform .2s ease-in-out}@media (prefers-reduced-motion:reduce){.accordion-button:after{transition:none}}.accordion-button:hover{z-index:2}.accordion-button:focus{z-index:3;border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.accordion-header{margin-bottom:0}.accordion-item{background-color:#fff;border:1px solid rgba(0,0,0,.125)}.accordion-item:first-of-type{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.accordion-item:first-of-type .accordion-button{border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.accordion-item:not(:first-of-type){border-top:0}.accordion-item:last-of-type{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.accordion-item:last-of-type .accordion-button.collapsed{border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.accordion-item:last-of-type .accordion-collapse{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.accordion-body{padding:1rem 1.25rem}.accordion-flush .accordion-collapse{border-width:0}.accordion-flush .accordion-item{border-right:0;border-left:0;border-radius:0}.accordion-flush .accordion-item:first-child{border-top:0}.accordion-flush .accordion-item:last-child{border-bottom:0}.accordion-flush .accordion-item .accordion-button{border-radius:0}.breadcrumb{display:flex;flex-wrap:wrap;padding:0;margin-bottom:1rem;list-style:none}.breadcrumb-item+.breadcrumb-item{padding-left:.5rem}.breadcrumb-item+.breadcrumb-item:before{float:left;padding-right:.5rem;color:#6c757d;content:"/";content:var(--bs-breadcrumb-divider,"/")}.breadcrumb-item.active{color:#6c757d}.pagination{display:flex;padding-left:0;list-style:none}.page-link{position:relative;display:block;color:#0d6efd;text-decoration:none;background-color:#fff;border:1px solid #dee2e6;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.page-link{transition:none}}.page-link:hover{z-index:2;border-color:#dee2e6}.page-link:focus,.page-link:hover{color:#0a58ca;background-color:#e9ecef}.page-link:focus{z-index:3;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.page-item:not(:first-child) .page-link{margin-left:-1px}.page-item.active .page-link{z-index:3;color:#fff;background-color:#0d6efd;border-color:#0d6efd}.page-item.disabled .page-link{color:#6c757d;pointer-events:none;background-color:#fff;border-color:#dee2e6}.page-link{padding:.375rem .75rem}.page-item:first-child .page-link{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.page-item:last-child .page-link{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.pagination-lg .page-link{padding:.75rem 1.5rem;font-size:1.25rem}.pagination-lg .page-item:first-child .page-link{border-top-left-radius:.3rem;border-bottom-left-radius:.3rem}.pagination-lg .page-item:last-child .page-link{border-top-right-radius:.3rem;border-bottom-right-radius:.3rem}.pagination-sm .page-link{padding:.25rem .5rem;font-size:.875rem}.pagination-sm .page-item:first-child .page-link{border-top-left-radius:.2rem;border-bottom-left-radius:.2rem}.pagination-sm .page-item:last-child .page-link{border-top-right-radius:.2rem;border-bottom-right-radius:.2rem}.badge{display:inline-block;padding:.35em .65em;font-size:.75em;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.alert{position:relative;padding:1rem;margin-bottom:1rem;border:1px solid transparent;border-radius:.25rem}.alert-heading{color:inherit}.alert-link{font-weight:700}.alert-dismissible{padding-right:3rem}.alert-dismissible .btn-close{position:absolute;top:0;right:0;z-index:2;padding:1.25rem 1rem}.alert-primary{color:#084298;background-color:#cfe2ff;border-color:#b6d4fe}.alert-primary .alert-link{color:#06357a}.alert-secondary{color:#41464b;background-color:#e2e3e5;border-color:#d3d6d8}.alert-secondary .alert-link{color:#34383c}.alert-success{color:#0f5132;background-color:#d1e7dd;border-color:#badbcc}.alert-success .alert-link{color:#0c4128}.alert-info{color:#055160;background-color:#cff4fc;border-color:#b6effb}.alert-info .alert-link{color:#04414d}.alert-warning{color:#664d03;background-color:#fff3cd;border-color:#ffecb5}.alert-warning .alert-link{color:#523e02}.alert-danger{color:#842029;background-color:#f8d7da;border-color:#f5c2c7}.alert-danger .alert-link{color:#6a1a21}.alert-light{color:#636464;background-color:#fefefe;border-color:#fdfdfe}.alert-light .alert-link{color:#4f5050}.alert-dark{color:#141619;background-color:#d3d3d4;border-color:#bcbebf}.alert-dark .alert-link{color:#101214}@keyframes progress-bar-stripes{0%{background-position-x:1rem}}.progress{height:1rem;font-size:.75rem;background-color:#e9ecef;border-radius:.25rem}.progress,.progress-bar{display:flex;overflow:hidden}.progress-bar{flex-direction:column;justify-content:center;color:#fff;text-align:center;white-space:nowrap;background-color:#0d6efd;transition:width .6s ease}@media (prefers-reduced-motion:reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg,hsla(0,0%,100%,.15) 25%,transparent 0,transparent 50%,hsla(0,0%,100%,.15) 0,hsla(0,0%,100%,.15) 75%,transparent 0,transparent);background-size:1rem 1rem}.progress-bar-animated{animation:progress-bar-stripes 1s linear infinite}@media (prefers-reduced-motion:reduce){.progress-bar-animated{animation:none}}.list-group{display:flex;flex-direction:column;padding-left:0;margin-bottom:0;border-radius:.25rem}.list-group-numbered{list-style-type:none;counter-reset:section}.list-group-numbered>li:before{content:counters(section,".") ". ";counter-increment:section}.list-group-item-action{width:100%;color:#495057;text-align:inherit}.list-group-item-action:focus,.list-group-item-action:hover{z-index:1;color:#495057;text-decoration:none;background-color:#f8f9fa}.list-group-item-action:active{color:#212529;background-color:#e9ecef}.list-group-item{position:relative;display:block;padding:.5rem 1rem;color:#212529;text-decoration:none;background-color:#fff;border:1px solid rgba(0,0,0,.125)}.list-group-item:first-child{border-top-left-radius:inherit;border-top-right-radius:inherit}.list-group-item:last-child{border-bottom-right-radius:inherit;border-bottom-left-radius:inherit}.list-group-item.disabled,.list-group-item:disabled{color:#6c757d;pointer-events:none;background-color:#fff}.list-group-item.active{z-index:2;color:#fff;background-color:#0d6efd;border-color:#0d6efd}.list-group-item+.list-group-item{border-top-width:0}.list-group-item+.list-group-item.active{margin-top:-1px;border-top-width:1px}.list-group-horizontal{flex-direction:row}.list-group-horizontal>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal>.list-group-item.active{margin-top:0}.list-group-horizontal>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}@media (min-width:576px){.list-group-horizontal-sm{flex-direction:row}.list-group-horizontal-sm>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-sm>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-sm>.list-group-item.active{margin-top:0}.list-group-horizontal-sm>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-sm>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:768px){.list-group-horizontal-md{flex-direction:row}.list-group-horizontal-md>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-md>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-md>.list-group-item.active{margin-top:0}.list-group-horizontal-md>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-md>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:992px){.list-group-horizontal-lg{flex-direction:row}.list-group-horizontal-lg>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-lg>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-lg>.list-group-item.active{margin-top:0}.list-group-horizontal-lg>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-lg>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:1200px){.list-group-horizontal-xl{flex-direction:row}.list-group-horizontal-xl>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-xl>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-xl>.list-group-item.active{margin-top:0}.list-group-horizontal-xl>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-xl>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:1400px){.list-group-horizontal-xxl{flex-direction:row}.list-group-horizontal-xxl>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-xxl>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-xxl>.list-group-item.active{margin-top:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}.list-group-flush{border-radius:0}.list-group-flush>.list-group-item{border-width:0 0 1px}.list-group-flush>.list-group-item:last-child{border-bottom-width:0}.list-group-item-primary{color:#084298;background-color:#cfe2ff}.list-group-item-primary.list-group-item-action:focus,.list-group-item-primary.list-group-item-action:hover{color:#084298;background-color:#bacbe6}.list-group-item-primary.list-group-item-action.active{color:#fff;background-color:#084298;border-color:#084298}.list-group-item-secondary{color:#41464b;background-color:#e2e3e5}.list-group-item-secondary.list-group-item-action:focus,.list-group-item-secondary.list-group-item-action:hover{color:#41464b;background-color:#cbccce}.list-group-item-secondary.list-group-item-action.active{color:#fff;background-color:#41464b;border-color:#41464b}.list-group-item-success{color:#0f5132;background-color:#d1e7dd}.list-group-item-success.list-group-item-action:focus,.list-group-item-success.list-group-item-action:hover{color:#0f5132;background-color:#bcd0c7}.list-group-item-success.list-group-item-action.active{color:#fff;background-color:#0f5132;border-color:#0f5132}.list-group-item-info{color:#055160;background-color:#cff4fc}.list-group-item-info.list-group-item-action:focus,.list-group-item-info.list-group-item-action:hover{color:#055160;background-color:#badce3}.list-group-item-info.list-group-item-action.active{color:#fff;background-color:#055160;border-color:#055160}.list-group-item-warning{color:#664d03;background-color:#fff3cd}.list-group-item-warning.list-group-item-action:focus,.list-group-item-warning.list-group-item-action:hover{color:#664d03;background-color:#e6dbb9}.list-group-item-warning.list-group-item-action.active{color:#fff;background-color:#664d03;border-color:#664d03}.list-group-item-danger{color:#842029;background-color:#f8d7da}.list-group-item-danger.list-group-item-action:focus,.list-group-item-danger.list-group-item-action:hover{color:#842029;background-color:#dfc2c4}.list-group-item-danger.list-group-item-action.active{color:#fff;background-color:#842029;border-color:#842029}.list-group-item-light{color:#636464;background-color:#fefefe}.list-group-item-light.list-group-item-action:focus,.list-group-item-light.list-group-item-action:hover{color:#636464;background-color:#e5e5e5}.list-group-item-light.list-group-item-action.active{color:#fff;background-color:#636464;border-color:#636464}.list-group-item-dark{color:#141619;background-color:#d3d3d4}.list-group-item-dark.list-group-item-action:focus,.list-group-item-dark.list-group-item-action:hover{color:#141619;background-color:#bebebf}.list-group-item-dark.list-group-item-action.active{color:#fff;background-color:#141619;border-color:#141619}.btn-close{box-sizing:content-box;width:1em;height:1em;padding:.25em;color:#000;background:transparent url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3E%3C/svg%3E") 50%/1em auto no-repeat;border:0;border-radius:.25rem;opacity:.5}.btn-close:hover{color:#000;text-decoration:none;opacity:.75}.btn-close:focus{outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25);opacity:1}.btn-close.disabled,.btn-close:disabled{pointer-events:none;-webkit-user-select:none;-ms-user-select:none;user-select:none;opacity:.25}.btn-close-white{-webkit-filter:invert(1) grayscale(100%) brightness(200%);filter:invert(1) grayscale(100%) brightness(200%)}.toast{width:350px;max-width:100%;font-size:.875rem;pointer-events:auto;background-color:hsla(0,0%,100%,.85);background-clip:padding-box;border:1px solid rgba(0,0,0,.1);box-shadow:0 .5rem 1rem rgba(0,0,0,.15);border-radius:.25rem}.toast:not(.showing):not(.show){opacity:0}.toast.hide{display:none}.toast-container{width:-webkit-max-content;width:-moz-max-content;width:max-content;max-width:100%;pointer-events:none}.toast-container>:not(:last-child){margin-bottom:.75rem}.toast-header{display:flex;align-items:center;padding:.5rem .75rem;color:#6c757d;background-color:hsla(0,0%,100%,.85);background-clip:padding-box;border-bottom:1px solid rgba(0,0,0,.05);border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.toast-header .btn-close{margin-right:-.375rem;margin-left:.75rem}.toast-body{padding:.75rem;word-wrap:break-word}.modal{position:fixed;top:0;left:0;z-index:1060;display:none;width:100%;height:100%;overflow-x:hidden;overflow-y:auto;outline:0}.modal-dialog{position:relative;width:auto;margin:.5rem;pointer-events:none}.modal.fade .modal-dialog{transition:transform .3s ease-out;transform:translateY(-50px)}@media (prefers-reduced-motion:reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{transform:none}.modal.modal-static .modal-dialog{transform:scale(1.02)}.modal-dialog-scrollable{height:calc(100% - 1rem)}.modal-dialog-scrollable .modal-content{max-height:100%;overflow:hidden}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:flex;align-items:center;min-height:calc(100% - 1rem)}.modal-content{position:relative;display:flex;flex-direction:column;width:100%;pointer-events:auto;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem;outline:0}.modal-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:.5}.modal-header{display:flex;flex-shrink:0;align-items:center;justify-content:space-between;padding:1rem;border-bottom:1px solid #dee2e6;border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.modal-header .btn-close{padding:.5rem;margin:-.5rem -.5rem -.5rem auto}.modal-title{margin-bottom:0;line-height:1.5}.modal-body{position:relative;flex:1 1 auto;padding:1rem}.modal-footer{display:flex;flex-wrap:wrap;flex-shrink:0;align-items:center;justify-content:flex-end;padding:.75rem;border-top:1px solid #dee2e6;border-bottom-right-radius:calc(.3rem - 1px);border-bottom-left-radius:calc(.3rem - 1px)}.modal-footer>*{margin:.25rem}@media (min-width:576px){.modal-dialog{max-width:500px;margin:1.75rem auto}.modal-dialog-scrollable{height:calc(100% - 3.5rem)}.modal-dialog-centered{min-height:calc(100% - 3.5rem)}.modal-sm{max-width:300px}}@media (min-width:992px){.modal-lg,.modal-xl{max-width:800px}}@media (min-width:1200px){.modal-xl{max-width:1140px}}.modal-fullscreen{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen .modal-header{border-radius:0}.modal-fullscreen .modal-body{overflow-y:auto}.modal-fullscreen .modal-footer{border-radius:0}@media (max-width:575.98px){.modal-fullscreen-sm-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-sm-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-sm-down .modal-header{border-radius:0}.modal-fullscreen-sm-down .modal-body{overflow-y:auto}.modal-fullscreen-sm-down .modal-footer{border-radius:0}}@media (max-width:767.98px){.modal-fullscreen-md-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-md-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-md-down .modal-header{border-radius:0}.modal-fullscreen-md-down .modal-body{overflow-y:auto}.modal-fullscreen-md-down .modal-footer{border-radius:0}}@media (max-width:991.98px){.modal-fullscreen-lg-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-lg-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-lg-down .modal-header{border-radius:0}.modal-fullscreen-lg-down .modal-body{overflow-y:auto}.modal-fullscreen-lg-down .modal-footer{border-radius:0}}@media (max-width:1199.98px){.modal-fullscreen-xl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xl-down .modal-header{border-radius:0}.modal-fullscreen-xl-down .modal-body{overflow-y:auto}.modal-fullscreen-xl-down .modal-footer{border-radius:0}}@media (max-width:1399.98px){.modal-fullscreen-xxl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xxl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xxl-down .modal-header{border-radius:0}.modal-fullscreen-xxl-down .modal-body{overflow-y:auto}.modal-fullscreen-xxl-down .modal-footer{border-radius:0}}.tooltip{position:absolute;z-index:1080;display:block;margin:0;font-family:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;opacity:0}.tooltip.show{opacity:.9}.tooltip .tooltip-arrow{position:absolute;display:block;width:.8rem;height:.4rem}.tooltip .tooltip-arrow:before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-auto[data-popper-placement^=top],.bs-tooltip-top{padding:.4rem 0}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow,.bs-tooltip-top .tooltip-arrow{bottom:0}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow:before,.bs-tooltip-top .tooltip-arrow:before{top:-1px;border-width:.4rem .4rem 0;border-top-color:#000}.bs-tooltip-auto[data-popper-placement^=right],.bs-tooltip-end{padding:0 .4rem}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow,.bs-tooltip-end .tooltip-arrow{left:0;width:.4rem;height:.8rem}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow:before,.bs-tooltip-end .tooltip-arrow:before{right:-1px;border-width:.4rem .4rem .4rem 0;border-right-color:#000}.bs-tooltip-auto[data-popper-placement^=bottom],.bs-tooltip-bottom{padding:.4rem 0}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow,.bs-tooltip-bottom .tooltip-arrow{top:0}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow:before,.bs-tooltip-bottom .tooltip-arrow:before{bottom:-1px;border-width:0 .4rem .4rem;border-bottom-color:#000}.bs-tooltip-auto[data-popper-placement^=left],.bs-tooltip-start{padding:0 .4rem}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow,.bs-tooltip-start .tooltip-arrow{right:0;width:.4rem;height:.8rem}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow:before,.bs-tooltip-start .tooltip-arrow:before{left:-1px;border-width:.4rem 0 .4rem .4rem;border-left-color:#000}.tooltip-inner{max-width:200px;padding:.25rem .5rem;color:#fff;text-align:center;background-color:#000;border-radius:.25rem}.popover{position:absolute;top:0;left:0;z-index:1070;display:block;max-width:276px;font-family:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem}.popover .popover-arrow{position:absolute;display:block;width:1rem;height:.5rem}.popover .popover-arrow:after,.popover .popover-arrow:before{position:absolute;display:block;content:"";border-color:transparent;border-style:solid}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow,.bs-popover-top>.popover-arrow{bottom:calc(-.5rem - 1px)}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow:before,.bs-popover-top>.popover-arrow:before{bottom:0;border-width:.5rem .5rem 0;border-top-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow:after,.bs-popover-top>.popover-arrow:after{bottom:1px;border-width:.5rem .5rem 0;border-top-color:#fff}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow,.bs-popover-end>.popover-arrow{left:calc(-.5rem - 1px);width:.5rem;height:1rem}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow:before,.bs-popover-end>.popover-arrow:before{left:0;border-width:.5rem .5rem .5rem 0;border-right-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow:after,.bs-popover-end>.popover-arrow:after{left:1px;border-width:.5rem .5rem .5rem 0;border-right-color:#fff}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow,.bs-popover-bottom>.popover-arrow{top:calc(-.5rem - 1px)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow:before,.bs-popover-bottom>.popover-arrow:before{top:0;border-width:0 .5rem .5rem;border-bottom-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow:after,.bs-popover-bottom>.popover-arrow:after{top:1px;border-width:0 .5rem .5rem;border-bottom-color:#fff}.bs-popover-auto[data-popper-placement^=bottom] .popover-header:before,.bs-popover-bottom .popover-header:before{position:absolute;top:0;left:50%;display:block;width:1rem;margin-left:-.5rem;content:"";border-bottom:1px solid #f0f0f0}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow,.bs-popover-start>.popover-arrow{right:calc(-.5rem - 1px);width:.5rem;height:1rem}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow:before,.bs-popover-start>.popover-arrow:before{right:0;border-width:.5rem 0 .5rem .5rem;border-left-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow:after,.bs-popover-start>.popover-arrow:after{right:1px;border-width:.5rem 0 .5rem .5rem;border-left-color:#fff}.popover-header{padding:.5rem 1rem;margin-bottom:0;font-size:1rem;background-color:#f0f0f0;border-bottom:1px solid rgba(0,0,0,.2);border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.popover-header:empty{display:none}.popover-body{padding:1rem;color:#212529}.carousel{position:relative}.carousel.pointer-event{touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner:after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;-webkit-backface-visibility:hidden;backface-visibility:hidden;transition:transform .6s ease-in-out}@media (prefers-reduced-motion:reduce){.carousel-item{transition:none}}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block}.active.carousel-item-end,.carousel-item-next:not(.carousel-item-start){transform:translateX(100%)}.active.carousel-item-start,.carousel-item-prev:not(.carousel-item-end){transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;transform:none}.carousel-fade .carousel-item-next.carousel-item-start,.carousel-fade .carousel-item-prev.carousel-item-end,.carousel-fade .carousel-item.active{z-index:1;opacity:1}.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{z-index:0;opacity:0;transition:opacity 0s .6s}@media (prefers-reduced-motion:reduce){.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{transition:none}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;z-index:1;display:flex;align-items:center;justify-content:center;width:15%;padding:0;color:#fff;text-align:center;background:none;border:0;opacity:.5;transition:opacity .15s ease}@media (prefers-reduced-motion:reduce){.carousel-control-next,.carousel-control-prev{transition:none}}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:2rem;height:2rem;background-repeat:no-repeat;background-position:50%;background-size:100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3E%3Cpath d='M11.354 1.646a.5.5 0 010 .708L5.707 8l5.647 5.646a.5.5 0 01-.708.708l-6-6a.5.5 0 010-.708l6-6a.5.5 0 01.708 0z'/%3E%3C/svg%3E")}.carousel-control-next-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3E%3Cpath d='M4.646 1.646a.5.5 0 01.708 0l6 6a.5.5 0 010 .708l-6 6a.5.5 0 01-.708-.708L10.293 8 4.646 2.354a.5.5 0 010-.708z'/%3E%3C/svg%3E")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:2;display:flex;justify-content:center;padding:0;margin-right:15%;margin-bottom:1rem;margin-left:15%;list-style:none}.carousel-indicators [data-bs-target]{box-sizing:content-box;flex:0 1 auto;width:30px;height:3px;padding:0;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:#fff;background-clip:padding-box;border:0;border-top:10px solid transparent;border-bottom:10px solid transparent;opacity:.5;transition:opacity .6s ease}@media (prefers-reduced-motion:reduce){.carousel-indicators [data-bs-target]{transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:1.25rem;left:15%;padding-top:1.25rem;padding-bottom:1.25rem;color:#fff;text-align:center}.carousel-dark .carousel-control-next-icon,.carousel-dark .carousel-control-prev-icon{-webkit-filter:invert(1) grayscale(100);filter:invert(1) grayscale(100)}.carousel-dark .carousel-indicators [data-bs-target]{background-color:#000}.carousel-dark .carousel-caption{color:#000}@keyframes spinner-border{to{transform:rotate(1turn)}}.spinner-border{display:inline-block;width:2rem;height:2rem;vertical-align:-.125em;border:.25em solid;border-right:.25em solid transparent;border-radius:50%;animation:spinner-border .75s linear infinite}.spinner-border-sm{width:1rem;height:1rem;border-width:.2em}@keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}.spinner-grow{display:inline-block;width:2rem;height:2rem;vertical-align:-.125em;background-color:currentColor;border-radius:50%;opacity:0;animation:spinner-grow .75s linear infinite}.spinner-grow-sm{width:1rem;height:1rem}@media (prefers-reduced-motion:reduce){.spinner-border,.spinner-grow{animation-duration:1.5s}}.offcanvas{position:fixed;bottom:0;z-index:1050;display:flex;flex-direction:column;max-width:100%;visibility:hidden;background-color:#fff;background-clip:padding-box;outline:0;transition:transform .3s ease-in-out}@media (prefers-reduced-motion:reduce){.offcanvas{transition:none}}.offcanvas-header{display:flex;align-items:center;justify-content:space-between;padding:1rem}.offcanvas-header .btn-close{padding:.5rem;margin-top:-.5rem;margin-right:-.5rem;margin-bottom:-.5rem}.offcanvas-title{margin-bottom:0;line-height:1.5}.offcanvas-body{flex-grow:1;padding:1rem;overflow-y:auto}.offcanvas-start{top:0;left:0;width:400px;border-right:1px solid rgba(0,0,0,.2);transform:translateX(-100%)}.offcanvas-end{top:0;right:0;width:400px;border-left:1px solid rgba(0,0,0,.2);transform:translateX(100%)}.offcanvas-top{top:0;border-bottom:1px solid rgba(0,0,0,.2);transform:translateY(-100%)}.offcanvas-bottom,.offcanvas-top{right:0;left:0;height:30vh;max-height:100%}.offcanvas-bottom{border-top:1px solid rgba(0,0,0,.2);transform:translateY(100%)}.offcanvas.show{transform:none}.clearfix:after{display:block;clear:both;content:""}.link-primary{color:#0d6efd}.link-primary:focus,.link-primary:hover{color:#0a58ca}.link-secondary{color:#6c757d}.link-secondary:focus,.link-secondary:hover{color:#565e64}.link-success{color:#198754}.link-success:focus,.link-success:hover{color:#146c43}.link-info{color:#0dcaf0}.link-info:focus,.link-info:hover{color:#3dd5f3}.link-warning{color:#ffc107}.link-warning:focus,.link-warning:hover{color:#ffcd39}.link-danger{color:#dc3545}.link-danger:focus,.link-danger:hover{color:#b02a37}.link-light{color:#f8f9fa}.link-light:focus,.link-light:hover{color:#f9fafb}.link-dark{color:#212529}.link-dark:focus,.link-dark:hover{color:#1a1e21}.ratio{position:relative;width:100%}.ratio:before{display:block;padding-top:var(--bs-aspect-ratio);content:""}.ratio>*{position:absolute;top:0;left:0;width:100%;height:100%}.ratio-1x1{--bs-aspect-ratio:100%}.ratio-4x3{--bs-aspect-ratio:75%}.ratio-16x9{--bs-aspect-ratio:56.25%}.ratio-21x9{--bs-aspect-ratio:42.85714%}.fixed-top{top:0}.fixed-bottom,.fixed-top{position:fixed;right:0;left:0;z-index:1030}.fixed-bottom{bottom:0}.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}@media (min-width:576px){.sticky-sm-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:768px){.sticky-md-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:992px){.sticky-lg-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:1200px){.sticky-xl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:1400px){.sticky-xxl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}.visually-hidden,.visually-hidden-focusable:not(:focus):not(:focus-within){position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}.stretched-link:after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;content:""}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.float-start{float:left!important}.float-end{float:right!important}.float-none{float:none!important}.overflow-auto{overflow:auto!important}.overflow-hidden{overflow:hidden!important}.overflow-visible{overflow:visible!important}.overflow-scroll{overflow:scroll!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-grid{display:grid!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:flex!important}.d-inline-flex{display:inline-flex!important}.d-none{display:none!important}.shadow{box-shadow:0 .5rem 1rem rgba(0,0,0,.15)!important}.shadow-sm{box-shadow:0 .125rem .25rem rgba(0,0,0,.075)!important}.shadow-lg{box-shadow:0 1rem 3rem rgba(0,0,0,.175)!important}.shadow-none{box-shadow:none!important}.position-static{position:static!important}.position-relative{position:relative!important}.position-absolute{position:absolute!important}.position-fixed{position:fixed!important}.position-sticky{position:-webkit-sticky!important;position:sticky!important}.top-0{top:0!important}.top-50{top:50%!important}.top-100{top:100%!important}.bottom-0{bottom:0!important}.bottom-50{bottom:50%!important}.bottom-100{bottom:100%!important}.start-0{left:0!important}.start-50{left:50%!important}.start-100{left:100%!important}.end-0{right:0!important}.end-50{right:50%!important}.end-100{right:100%!important}.translate-middle{transform:translate(-50%,-50%)!important}.translate-middle-x{transform:translateX(-50%)!important}.translate-middle-y{transform:translateY(-50%)!important}.border{border:1px solid #dee2e6!important}.border-0{border:0!important}.border-top{border-top:1px solid #dee2e6!important}.border-top-0{border-top:0!important}.border-end{border-right:1px solid #dee2e6!important}.border-end-0{border-right:0!important}.border-bottom{border-bottom:1px solid #dee2e6!important}.border-bottom-0{border-bottom:0!important}.border-start{border-left:1px solid #dee2e6!important}.border-start-0{border-left:0!important}.border-primary{border-color:#0d6efd!important}.border-secondary{border-color:#6c757d!important}.border-success{border-color:#198754!important}.border-info{border-color:#0dcaf0!important}.border-warning{border-color:#ffc107!important}.border-danger{border-color:#dc3545!important}.border-light{border-color:#f8f9fa!important}.border-dark{border-color:#212529!important}.border-white{border-color:#fff!important}.border-1{border-width:1px!important}.border-2{border-width:2px!important}.border-3{border-width:3px!important}.border-4{border-width:4px!important}.border-5{border-width:5px!important}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.w-auto{width:auto!important}.mw-100{max-width:100%!important}.vw-100{width:100vw!important}.min-vw-100{min-width:100vw!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.h-auto{height:auto!important}.mh-100{max-height:100%!important}.vh-100{height:100vh!important}.min-vh-100{min-height:100vh!important}.flex-fill{flex:1 1 auto!important}.flex-row{flex-direction:row!important}.flex-column{flex-direction:column!important}.flex-row-reverse{flex-direction:row-reverse!important}.flex-column-reverse{flex-direction:column-reverse!important}.flex-grow-0{flex-grow:0!important}.flex-grow-1{flex-grow:1!important}.flex-shrink-0{flex-shrink:0!important}.flex-shrink-1{flex-shrink:1!important}.flex-wrap{flex-wrap:wrap!important}.flex-nowrap{flex-wrap:nowrap!important}.flex-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-0{grid-gap:0!important;gap:0!important}.gap-1{grid-gap:.25rem!important;gap:.25rem!important}.gap-2{grid-gap:.5rem!important;gap:.5rem!important}.gap-3{grid-gap:1rem!important;gap:1rem!important}.gap-4{grid-gap:1.5rem!important;gap:1.5rem!important}.gap-5{grid-gap:3rem!important;gap:3rem!important}.justify-content-start{justify-content:flex-start!important}.justify-content-end{justify-content:flex-end!important}.justify-content-center{justify-content:center!important}.justify-content-between{justify-content:space-between!important}.justify-content-around{justify-content:space-around!important}.justify-content-evenly{justify-content:space-evenly!important}.align-items-start{align-items:flex-start!important}.align-items-end{align-items:flex-end!important}.align-items-center{align-items:center!important}.align-items-baseline{align-items:baseline!important}.align-items-stretch{align-items:stretch!important}.align-content-start{align-content:flex-start!important}.align-content-end{align-content:flex-end!important}.align-content-center{align-content:center!important}.align-content-between{align-content:space-between!important}.align-content-around{align-content:space-around!important}.align-content-stretch{align-content:stretch!important}.align-self-auto{align-self:auto!important}.align-self-start{align-self:flex-start!important}.align-self-end{align-self:flex-end!important}.align-self-center{align-self:center!important}.align-self-baseline{align-self:baseline!important}.align-self-stretch{align-self:stretch!important}.order-first{order:-1!important}.order-0{order:0!important}.order-1{order:1!important}.order-2{order:2!important}.order-3{order:3!important}.order-4{order:4!important}.order-5{order:5!important}.order-last{order:6!important}.m-0{margin:0!important}.m-1{margin:.25rem!important}.m-2{margin:.5rem!important}.m-3{margin:1rem!important}.m-4{margin:1.5rem!important}.m-5{margin:3rem!important}.m-auto{margin:auto!important}.mx-0{margin-right:0!important;margin-left:0!important}.mx-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-3{margin-right:1rem!important;margin-left:1rem!important}.mx-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-5{margin-right:3rem!important;margin-left:3rem!important}.mx-auto{margin-right:auto!important;margin-left:auto!important}.my-0{margin-top:0!important;margin-bottom:0!important}.my-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-0{margin-top:0!important}.mt-1{margin-top:.25rem!important}.mt-2{margin-top:.5rem!important}.mt-3{margin-top:1rem!important}.mt-4{margin-top:1.5rem!important}.mt-5{margin-top:3rem!important}.mt-auto{margin-top:auto!important}.me-0{margin-right:0!important}.me-1{margin-right:.25rem!important}.me-2{margin-right:.5rem!important}.me-3{margin-right:1rem!important}.me-4{margin-right:1.5rem!important}.me-5{margin-right:3rem!important}.me-auto{margin-right:auto!important}.mb-0{margin-bottom:0!important}.mb-1{margin-bottom:.25rem!important}.mb-2{margin-bottom:.5rem!important}.mb-3{margin-bottom:1rem!important}.mb-4{margin-bottom:1.5rem!important}.mb-5{margin-bottom:3rem!important}.mb-auto{margin-bottom:auto!important}.ms-0{margin-left:0!important}.ms-1{margin-left:.25rem!important}.ms-2{margin-left:.5rem!important}.ms-3{margin-left:1rem!important}.ms-4{margin-left:1.5rem!important}.ms-5{margin-left:3rem!important}.ms-auto{margin-left:auto!important}.p-0{padding:0!important}.p-1{padding:.25rem!important}.p-2{padding:.5rem!important}.p-3{padding:1rem!important}.p-4{padding:1.5rem!important}.p-5{padding:3rem!important}.px-0{padding-right:0!important;padding-left:0!important}.px-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-3{padding-right:1rem!important;padding-left:1rem!important}.px-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-5{padding-right:3rem!important;padding-left:3rem!important}.py-0{padding-top:0!important;padding-bottom:0!important}.py-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-0{padding-top:0!important}.pt-1{padding-top:.25rem!important}.pt-2{padding-top:.5rem!important}.pt-3{padding-top:1rem!important}.pt-4{padding-top:1.5rem!important}.pt-5{padding-top:3rem!important}.pe-0{padding-right:0!important}.pe-1{padding-right:.25rem!important}.pe-2{padding-right:.5rem!important}.pe-3{padding-right:1rem!important}.pe-4{padding-right:1.5rem!important}.pe-5{padding-right:3rem!important}.pb-0{padding-bottom:0!important}.pb-1{padding-bottom:.25rem!important}.pb-2{padding-bottom:.5rem!important}.pb-3{padding-bottom:1rem!important}.pb-4{padding-bottom:1.5rem!important}.pb-5{padding-bottom:3rem!important}.ps-0{padding-left:0!important}.ps-1{padding-left:.25rem!important}.ps-2{padding-left:.5rem!important}.ps-3{padding-left:1rem!important}.ps-4{padding-left:1.5rem!important}.ps-5{padding-left:3rem!important}.font-monospace{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace!important;font-family:var(--bs-font-monospace)!important}.fs-1{font-size:calc(1.375rem + 1.5vw)!important}.fs-2{font-size:calc(1.325rem + .9vw)!important}.fs-3{font-size:calc(1.3rem + .6vw)!important}.fs-4{font-size:calc(1.275rem + .3vw)!important}.fs-5{font-size:1.25rem!important}.fs-6{font-size:1rem!important}.fst-italic{font-style:italic!important}.fst-normal{font-style:normal!important}.fw-light{font-weight:300!important}.fw-lighter{font-weight:lighter!important}.fw-normal{font-weight:400!important}.fw-bold{font-weight:700!important}.fw-bolder{font-weight:bolder!important}.lh-1{line-height:1!important}.lh-sm{line-height:1.25!important}.lh-base{line-height:1.5!important}.lh-lg{line-height:2!important}.text-start{text-align:left!important}.text-end{text-align:right!important}.text-center{text-align:center!important}.text-decoration-none{text-decoration:none!important}.text-decoration-underline{text-decoration:underline!important}.text-decoration-line-through{text-decoration:line-through!important}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.text-wrap{white-space:normal!important}.text-nowrap{white-space:nowrap!important}.text-break{word-wrap:break-word!important;word-break:break-word!important}.text-primary{color:#0d6efd!important}.text-secondary{color:#6c757d!important}.text-success{color:#198754!important}.text-info{color:#0dcaf0!important}.text-warning{color:#ffc107!important}.text-danger{color:#dc3545!important}.text-light{color:#f8f9fa!important}.text-dark{color:#212529!important}.text-white{color:#fff!important}.text-body{color:#212529!important}.text-muted{color:#6c757d!important}.text-black-50{color:rgba(0,0,0,.5)!important}.text-white-50{color:hsla(0,0%,100%,.5)!important}.text-reset{color:inherit!important}.bg-primary{background-color:#0d6efd!important}.bg-secondary{background-color:#6c757d!important}.bg-success{background-color:#198754!important}.bg-info{background-color:#0dcaf0!important}.bg-warning{background-color:#ffc107!important}.bg-danger{background-color:#dc3545!important}.bg-light{background-color:#f8f9fa!important}.bg-dark{background-color:#212529!important}.bg-body,.bg-white{background-color:#fff!important}.bg-transparent{background-color:transparent!important}.bg-gradient{background-image:linear-gradient(180deg,hsla(0,0%,100%,.15),hsla(0,0%,100%,0))!important;background-image:var(--bs-gradient)!important}.user-select-all{-webkit-user-select:all!important;-ms-user-select:all!important;user-select:all!important}.user-select-auto{-webkit-user-select:auto!important;-ms-user-select:auto!important;user-select:auto!important}.user-select-none{-webkit-user-select:none!important;-ms-user-select:none!important;user-select:none!important}.pe-none{pointer-events:none!important}.pe-auto{pointer-events:auto!important}.rounded{border-radius:.25rem!important}.rounded-0{border-radius:0!important}.rounded-1{border-radius:.2rem!important}.rounded-2{border-radius:.25rem!important}.rounded-3{border-radius:.3rem!important}.rounded-circle{border-radius:50%!important}.rounded-pill{border-radius:50rem!important}.rounded-top{border-top-left-radius:.25rem!important}.rounded-end,.rounded-top{border-top-right-radius:.25rem!important}.rounded-bottom,.rounded-end{border-bottom-right-radius:.25rem!important}.rounded-bottom,.rounded-start{border-bottom-left-radius:.25rem!important}.rounded-start{border-top-left-radius:.25rem!important}.visible{visibility:visible!important}.invisible{visibility:hidden!important}@media (min-width:576px){.float-sm-start{float:left!important}.float-sm-end{float:right!important}.float-sm-none{float:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-grid{display:grid!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:flex!important}.d-sm-inline-flex{display:inline-flex!important}.d-sm-none{display:none!important}.flex-sm-fill{flex:1 1 auto!important}.flex-sm-row{flex-direction:row!important}.flex-sm-column{flex-direction:column!important}.flex-sm-row-reverse{flex-direction:row-reverse!important}.flex-sm-column-reverse{flex-direction:column-reverse!important}.flex-sm-grow-0{flex-grow:0!important}.flex-sm-grow-1{flex-grow:1!important}.flex-sm-shrink-0{flex-shrink:0!important}.flex-sm-shrink-1{flex-shrink:1!important}.flex-sm-wrap{flex-wrap:wrap!important}.flex-sm-nowrap{flex-wrap:nowrap!important}.flex-sm-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-sm-0{grid-gap:0!important;gap:0!important}.gap-sm-1{grid-gap:.25rem!important;gap:.25rem!important}.gap-sm-2{grid-gap:.5rem!important;gap:.5rem!important}.gap-sm-3{grid-gap:1rem!important;gap:1rem!important}.gap-sm-4{grid-gap:1.5rem!important;gap:1.5rem!important}.gap-sm-5{grid-gap:3rem!important;gap:3rem!important}.justify-content-sm-start{justify-content:flex-start!important}.justify-content-sm-end{justify-content:flex-end!important}.justify-content-sm-center{justify-content:center!important}.justify-content-sm-between{justify-content:space-between!important}.justify-content-sm-around{justify-content:space-around!important}.justify-content-sm-evenly{justify-content:space-evenly!important}.align-items-sm-start{align-items:flex-start!important}.align-items-sm-end{align-items:flex-end!important}.align-items-sm-center{align-items:center!important}.align-items-sm-baseline{align-items:baseline!important}.align-items-sm-stretch{align-items:stretch!important}.align-content-sm-start{align-content:flex-start!important}.align-content-sm-end{align-content:flex-end!important}.align-content-sm-center{align-content:center!important}.align-content-sm-between{align-content:space-between!important}.align-content-sm-around{align-content:space-around!important}.align-content-sm-stretch{align-content:stretch!important}.align-self-sm-auto{align-self:auto!important}.align-self-sm-start{align-self:flex-start!important}.align-self-sm-end{align-self:flex-end!important}.align-self-sm-center{align-self:center!important}.align-self-sm-baseline{align-self:baseline!important}.align-self-sm-stretch{align-self:stretch!important}.order-sm-first{order:-1!important}.order-sm-0{order:0!important}.order-sm-1{order:1!important}.order-sm-2{order:2!important}.order-sm-3{order:3!important}.order-sm-4{order:4!important}.order-sm-5{order:5!important}.order-sm-last{order:6!important}.m-sm-0{margin:0!important}.m-sm-1{margin:.25rem!important}.m-sm-2{margin:.5rem!important}.m-sm-3{margin:1rem!important}.m-sm-4{margin:1.5rem!important}.m-sm-5{margin:3rem!important}.m-sm-auto{margin:auto!important}.mx-sm-0{margin-right:0!important;margin-left:0!important}.mx-sm-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-sm-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-sm-3{margin-right:1rem!important;margin-left:1rem!important}.mx-sm-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-sm-5{margin-right:3rem!important;margin-left:3rem!important}.mx-sm-auto{margin-right:auto!important;margin-left:auto!important}.my-sm-0{margin-top:0!important;margin-bottom:0!important}.my-sm-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-sm-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-sm-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-sm-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-sm-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-sm-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-sm-0{margin-top:0!important}.mt-sm-1{margin-top:.25rem!important}.mt-sm-2{margin-top:.5rem!important}.mt-sm-3{margin-top:1rem!important}.mt-sm-4{margin-top:1.5rem!important}.mt-sm-5{margin-top:3rem!important}.mt-sm-auto{margin-top:auto!important}.me-sm-0{margin-right:0!important}.me-sm-1{margin-right:.25rem!important}.me-sm-2{margin-right:.5rem!important}.me-sm-3{margin-right:1rem!important}.me-sm-4{margin-right:1.5rem!important}.me-sm-5{margin-right:3rem!important}.me-sm-auto{margin-right:auto!important}.mb-sm-0{margin-bottom:0!important}.mb-sm-1{margin-bottom:.25rem!important}.mb-sm-2{margin-bottom:.5rem!important}.mb-sm-3{margin-bottom:1rem!important}.mb-sm-4{margin-bottom:1.5rem!important}.mb-sm-5{margin-bottom:3rem!important}.mb-sm-auto{margin-bottom:auto!important}.ms-sm-0{margin-left:0!important}.ms-sm-1{margin-left:.25rem!important}.ms-sm-2{margin-left:.5rem!important}.ms-sm-3{margin-left:1rem!important}.ms-sm-4{margin-left:1.5rem!important}.ms-sm-5{margin-left:3rem!important}.ms-sm-auto{margin-left:auto!important}.p-sm-0{padding:0!important}.p-sm-1{padding:.25rem!important}.p-sm-2{padding:.5rem!important}.p-sm-3{padding:1rem!important}.p-sm-4{padding:1.5rem!important}.p-sm-5{padding:3rem!important}.px-sm-0{padding-right:0!important;padding-left:0!important}.px-sm-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-sm-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-sm-3{padding-right:1rem!important;padding-left:1rem!important}.px-sm-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-sm-5{padding-right:3rem!important;padding-left:3rem!important}.py-sm-0{padding-top:0!important;padding-bottom:0!important}.py-sm-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-sm-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-sm-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-sm-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-sm-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-sm-0{padding-top:0!important}.pt-sm-1{padding-top:.25rem!important}.pt-sm-2{padding-top:.5rem!important}.pt-sm-3{padding-top:1rem!important}.pt-sm-4{padding-top:1.5rem!important}.pt-sm-5{padding-top:3rem!important}.pe-sm-0{padding-right:0!important}.pe-sm-1{padding-right:.25rem!important}.pe-sm-2{padding-right:.5rem!important}.pe-sm-3{padding-right:1rem!important}.pe-sm-4{padding-right:1.5rem!important}.pe-sm-5{padding-right:3rem!important}.pb-sm-0{padding-bottom:0!important}.pb-sm-1{padding-bottom:.25rem!important}.pb-sm-2{padding-bottom:.5rem!important}.pb-sm-3{padding-bottom:1rem!important}.pb-sm-4{padding-bottom:1.5rem!important}.pb-sm-5{padding-bottom:3rem!important}.ps-sm-0{padding-left:0!important}.ps-sm-1{padding-left:.25rem!important}.ps-sm-2{padding-left:.5rem!important}.ps-sm-3{padding-left:1rem!important}.ps-sm-4{padding-left:1.5rem!important}.ps-sm-5{padding-left:3rem!important}.text-sm-start{text-align:left!important}.text-sm-end{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:768px){.float-md-start{float:left!important}.float-md-end{float:right!important}.float-md-none{float:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-grid{display:grid!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:flex!important}.d-md-inline-flex{display:inline-flex!important}.d-md-none{display:none!important}.flex-md-fill{flex:1 1 auto!important}.flex-md-row{flex-direction:row!important}.flex-md-column{flex-direction:column!important}.flex-md-row-reverse{flex-direction:row-reverse!important}.flex-md-column-reverse{flex-direction:column-reverse!important}.flex-md-grow-0{flex-grow:0!important}.flex-md-grow-1{flex-grow:1!important}.flex-md-shrink-0{flex-shrink:0!important}.flex-md-shrink-1{flex-shrink:1!important}.flex-md-wrap{flex-wrap:wrap!important}.flex-md-nowrap{flex-wrap:nowrap!important}.flex-md-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-md-0{grid-gap:0!important;gap:0!important}.gap-md-1{grid-gap:.25rem!important;gap:.25rem!important}.gap-md-2{grid-gap:.5rem!important;gap:.5rem!important}.gap-md-3{grid-gap:1rem!important;gap:1rem!important}.gap-md-4{grid-gap:1.5rem!important;gap:1.5rem!important}.gap-md-5{grid-gap:3rem!important;gap:3rem!important}.justify-content-md-start{justify-content:flex-start!important}.justify-content-md-end{justify-content:flex-end!important}.justify-content-md-center{justify-content:center!important}.justify-content-md-between{justify-content:space-between!important}.justify-content-md-around{justify-content:space-around!important}.justify-content-md-evenly{justify-content:space-evenly!important}.align-items-md-start{align-items:flex-start!important}.align-items-md-end{align-items:flex-end!important}.align-items-md-center{align-items:center!important}.align-items-md-baseline{align-items:baseline!important}.align-items-md-stretch{align-items:stretch!important}.align-content-md-start{align-content:flex-start!important}.align-content-md-end{align-content:flex-end!important}.align-content-md-center{align-content:center!important}.align-content-md-between{align-content:space-between!important}.align-content-md-around{align-content:space-around!important}.align-content-md-stretch{align-content:stretch!important}.align-self-md-auto{align-self:auto!important}.align-self-md-start{align-self:flex-start!important}.align-self-md-end{align-self:flex-end!important}.align-self-md-center{align-self:center!important}.align-self-md-baseline{align-self:baseline!important}.align-self-md-stretch{align-self:stretch!important}.order-md-first{order:-1!important}.order-md-0{order:0!important}.order-md-1{order:1!important}.order-md-2{order:2!important}.order-md-3{order:3!important}.order-md-4{order:4!important}.order-md-5{order:5!important}.order-md-last{order:6!important}.m-md-0{margin:0!important}.m-md-1{margin:.25rem!important}.m-md-2{margin:.5rem!important}.m-md-3{margin:1rem!important}.m-md-4{margin:1.5rem!important}.m-md-5{margin:3rem!important}.m-md-auto{margin:auto!important}.mx-md-0{margin-right:0!important;margin-left:0!important}.mx-md-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-md-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-md-3{margin-right:1rem!important;margin-left:1rem!important}.mx-md-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-md-5{margin-right:3rem!important;margin-left:3rem!important}.mx-md-auto{margin-right:auto!important;margin-left:auto!important}.my-md-0{margin-top:0!important;margin-bottom:0!important}.my-md-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-md-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-md-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-md-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-md-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-md-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-md-0{margin-top:0!important}.mt-md-1{margin-top:.25rem!important}.mt-md-2{margin-top:.5rem!important}.mt-md-3{margin-top:1rem!important}.mt-md-4{margin-top:1.5rem!important}.mt-md-5{margin-top:3rem!important}.mt-md-auto{margin-top:auto!important}.me-md-0{margin-right:0!important}.me-md-1{margin-right:.25rem!important}.me-md-2{margin-right:.5rem!important}.me-md-3{margin-right:1rem!important}.me-md-4{margin-right:1.5rem!important}.me-md-5{margin-right:3rem!important}.me-md-auto{margin-right:auto!important}.mb-md-0{margin-bottom:0!important}.mb-md-1{margin-bottom:.25rem!important}.mb-md-2{margin-bottom:.5rem!important}.mb-md-3{margin-bottom:1rem!important}.mb-md-4{margin-bottom:1.5rem!important}.mb-md-5{margin-bottom:3rem!important}.mb-md-auto{margin-bottom:auto!important}.ms-md-0{margin-left:0!important}.ms-md-1{margin-left:.25rem!important}.ms-md-2{margin-left:.5rem!important}.ms-md-3{margin-left:1rem!important}.ms-md-4{margin-left:1.5rem!important}.ms-md-5{margin-left:3rem!important}.ms-md-auto{margin-left:auto!important}.p-md-0{padding:0!important}.p-md-1{padding:.25rem!important}.p-md-2{padding:.5rem!important}.p-md-3{padding:1rem!important}.p-md-4{padding:1.5rem!important}.p-md-5{padding:3rem!important}.px-md-0{padding-right:0!important;padding-left:0!important}.px-md-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-md-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-md-3{padding-right:1rem!important;padding-left:1rem!important}.px-md-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-md-5{padding-right:3rem!important;padding-left:3rem!important}.py-md-0{padding-top:0!important;padding-bottom:0!important}.py-md-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-md-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-md-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-md-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-md-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-md-0{padding-top:0!important}.pt-md-1{padding-top:.25rem!important}.pt-md-2{padding-top:.5rem!important}.pt-md-3{padding-top:1rem!important}.pt-md-4{padding-top:1.5rem!important}.pt-md-5{padding-top:3rem!important}.pe-md-0{padding-right:0!important}.pe-md-1{padding-right:.25rem!important}.pe-md-2{padding-right:.5rem!important}.pe-md-3{padding-right:1rem!important}.pe-md-4{padding-right:1.5rem!important}.pe-md-5{padding-right:3rem!important}.pb-md-0{padding-bottom:0!important}.pb-md-1{padding-bottom:.25rem!important}.pb-md-2{padding-bottom:.5rem!important}.pb-md-3{padding-bottom:1rem!important}.pb-md-4{padding-bottom:1.5rem!important}.pb-md-5{padding-bottom:3rem!important}.ps-md-0{padding-left:0!important}.ps-md-1{padding-left:.25rem!important}.ps-md-2{padding-left:.5rem!important}.ps-md-3{padding-left:1rem!important}.ps-md-4{padding-left:1.5rem!important}.ps-md-5{padding-left:3rem!important}.text-md-start{text-align:left!important}.text-md-end{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:992px){.float-lg-start{float:left!important}.float-lg-end{float:right!important}.float-lg-none{float:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-grid{display:grid!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:flex!important}.d-lg-inline-flex{display:inline-flex!important}.d-lg-none{display:none!important}.flex-lg-fill{flex:1 1 auto!important}.flex-lg-row{flex-direction:row!important}.flex-lg-column{flex-direction:column!important}.flex-lg-row-reverse{flex-direction:row-reverse!important}.flex-lg-column-reverse{flex-direction:column-reverse!important}.flex-lg-grow-0{flex-grow:0!important}.flex-lg-grow-1{flex-grow:1!important}.flex-lg-shrink-0{flex-shrink:0!important}.flex-lg-shrink-1{flex-shrink:1!important}.flex-lg-wrap{flex-wrap:wrap!important}.flex-lg-nowrap{flex-wrap:nowrap!important}.flex-lg-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-lg-0{grid-gap:0!important;gap:0!important}.gap-lg-1{grid-gap:.25rem!important;gap:.25rem!important}.gap-lg-2{grid-gap:.5rem!important;gap:.5rem!important}.gap-lg-3{grid-gap:1rem!important;gap:1rem!important}.gap-lg-4{grid-gap:1.5rem!important;gap:1.5rem!important}.gap-lg-5{grid-gap:3rem!important;gap:3rem!important}.justify-content-lg-start{justify-content:flex-start!important}.justify-content-lg-end{justify-content:flex-end!important}.justify-content-lg-center{justify-content:center!important}.justify-content-lg-between{justify-content:space-between!important}.justify-content-lg-around{justify-content:space-around!important}.justify-content-lg-evenly{justify-content:space-evenly!important}.align-items-lg-start{align-items:flex-start!important}.align-items-lg-end{align-items:flex-end!important}.align-items-lg-center{align-items:center!important}.align-items-lg-baseline{align-items:baseline!important}.align-items-lg-stretch{align-items:stretch!important}.align-content-lg-start{align-content:flex-start!important}.align-content-lg-end{align-content:flex-end!important}.align-content-lg-center{align-content:center!important}.align-content-lg-between{align-content:space-between!important}.align-content-lg-around{align-content:space-around!important}.align-content-lg-stretch{align-content:stretch!important}.align-self-lg-auto{align-self:auto!important}.align-self-lg-start{align-self:flex-start!important}.align-self-lg-end{align-self:flex-end!important}.align-self-lg-center{align-self:center!important}.align-self-lg-baseline{align-self:baseline!important}.align-self-lg-stretch{align-self:stretch!important}.order-lg-first{order:-1!important}.order-lg-0{order:0!important}.order-lg-1{order:1!important}.order-lg-2{order:2!important}.order-lg-3{order:3!important}.order-lg-4{order:4!important}.order-lg-5{order:5!important}.order-lg-last{order:6!important}.m-lg-0{margin:0!important}.m-lg-1{margin:.25rem!important}.m-lg-2{margin:.5rem!important}.m-lg-3{margin:1rem!important}.m-lg-4{margin:1.5rem!important}.m-lg-5{margin:3rem!important}.m-lg-auto{margin:auto!important}.mx-lg-0{margin-right:0!important;margin-left:0!important}.mx-lg-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-lg-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-lg-3{margin-right:1rem!important;margin-left:1rem!important}.mx-lg-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-lg-5{margin-right:3rem!important;margin-left:3rem!important}.mx-lg-auto{margin-right:auto!important;margin-left:auto!important}.my-lg-0{margin-top:0!important;margin-bottom:0!important}.my-lg-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-lg-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-lg-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-lg-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-lg-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-lg-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-lg-0{margin-top:0!important}.mt-lg-1{margin-top:.25rem!important}.mt-lg-2{margin-top:.5rem!important}.mt-lg-3{margin-top:1rem!important}.mt-lg-4{margin-top:1.5rem!important}.mt-lg-5{margin-top:3rem!important}.mt-lg-auto{margin-top:auto!important}.me-lg-0{margin-right:0!important}.me-lg-1{margin-right:.25rem!important}.me-lg-2{margin-right:.5rem!important}.me-lg-3{margin-right:1rem!important}.me-lg-4{margin-right:1.5rem!important}.me-lg-5{margin-right:3rem!important}.me-lg-auto{margin-right:auto!important}.mb-lg-0{margin-bottom:0!important}.mb-lg-1{margin-bottom:.25rem!important}.mb-lg-2{margin-bottom:.5rem!important}.mb-lg-3{margin-bottom:1rem!important}.mb-lg-4{margin-bottom:1.5rem!important}.mb-lg-5{margin-bottom:3rem!important}.mb-lg-auto{margin-bottom:auto!important}.ms-lg-0{margin-left:0!important}.ms-lg-1{margin-left:.25rem!important}.ms-lg-2{margin-left:.5rem!important}.ms-lg-3{margin-left:1rem!important}.ms-lg-4{margin-left:1.5rem!important}.ms-lg-5{margin-left:3rem!important}.ms-lg-auto{margin-left:auto!important}.p-lg-0{padding:0!important}.p-lg-1{padding:.25rem!important}.p-lg-2{padding:.5rem!important}.p-lg-3{padding:1rem!important}.p-lg-4{padding:1.5rem!important}.p-lg-5{padding:3rem!important}.px-lg-0{padding-right:0!important;padding-left:0!important}.px-lg-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-lg-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-lg-3{padding-right:1rem!important;padding-left:1rem!important}.px-lg-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-lg-5{padding-right:3rem!important;padding-left:3rem!important}.py-lg-0{padding-top:0!important;padding-bottom:0!important}.py-lg-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-lg-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-lg-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-lg-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-lg-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-lg-0{padding-top:0!important}.pt-lg-1{padding-top:.25rem!important}.pt-lg-2{padding-top:.5rem!important}.pt-lg-3{padding-top:1rem!important}.pt-lg-4{padding-top:1.5rem!important}.pt-lg-5{padding-top:3rem!important}.pe-lg-0{padding-right:0!important}.pe-lg-1{padding-right:.25rem!important}.pe-lg-2{padding-right:.5rem!important}.pe-lg-3{padding-right:1rem!important}.pe-lg-4{padding-right:1.5rem!important}.pe-lg-5{padding-right:3rem!important}.pb-lg-0{padding-bottom:0!important}.pb-lg-1{padding-bottom:.25rem!important}.pb-lg-2{padding-bottom:.5rem!important}.pb-lg-3{padding-bottom:1rem!important}.pb-lg-4{padding-bottom:1.5rem!important}.pb-lg-5{padding-bottom:3rem!important}.ps-lg-0{padding-left:0!important}.ps-lg-1{padding-left:.25rem!important}.ps-lg-2{padding-left:.5rem!important}.ps-lg-3{padding-left:1rem!important}.ps-lg-4{padding-left:1.5rem!important}.ps-lg-5{padding-left:3rem!important}.text-lg-start{text-align:left!important}.text-lg-end{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:1200px){.float-xl-start{float:left!important}.float-xl-end{float:right!important}.float-xl-none{float:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-grid{display:grid!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:flex!important}.d-xl-inline-flex{display:inline-flex!important}.d-xl-none{display:none!important}.flex-xl-fill{flex:1 1 auto!important}.flex-xl-row{flex-direction:row!important}.flex-xl-column{flex-direction:column!important}.flex-xl-row-reverse{flex-direction:row-reverse!important}.flex-xl-column-reverse{flex-direction:column-reverse!important}.flex-xl-grow-0{flex-grow:0!important}.flex-xl-grow-1{flex-grow:1!important}.flex-xl-shrink-0{flex-shrink:0!important}.flex-xl-shrink-1{flex-shrink:1!important}.flex-xl-wrap{flex-wrap:wrap!important}.flex-xl-nowrap{flex-wrap:nowrap!important}.flex-xl-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-xl-0{grid-gap:0!important;gap:0!important}.gap-xl-1{grid-gap:.25rem!important;gap:.25rem!important}.gap-xl-2{grid-gap:.5rem!important;gap:.5rem!important}.gap-xl-3{grid-gap:1rem!important;gap:1rem!important}.gap-xl-4{grid-gap:1.5rem!important;gap:1.5rem!important}.gap-xl-5{grid-gap:3rem!important;gap:3rem!important}.justify-content-xl-start{justify-content:flex-start!important}.justify-content-xl-end{justify-content:flex-end!important}.justify-content-xl-center{justify-content:center!important}.justify-content-xl-between{justify-content:space-between!important}.justify-content-xl-around{justify-content:space-around!important}.justify-content-xl-evenly{justify-content:space-evenly!important}.align-items-xl-start{align-items:flex-start!important}.align-items-xl-end{align-items:flex-end!important}.align-items-xl-center{align-items:center!important}.align-items-xl-baseline{align-items:baseline!important}.align-items-xl-stretch{align-items:stretch!important}.align-content-xl-start{align-content:flex-start!important}.align-content-xl-end{align-content:flex-end!important}.align-content-xl-center{align-content:center!important}.align-content-xl-between{align-content:space-between!important}.align-content-xl-around{align-content:space-around!important}.align-content-xl-stretch{align-content:stretch!important}.align-self-xl-auto{align-self:auto!important}.align-self-xl-start{align-self:flex-start!important}.align-self-xl-end{align-self:flex-end!important}.align-self-xl-center{align-self:center!important}.align-self-xl-baseline{align-self:baseline!important}.align-self-xl-stretch{align-self:stretch!important}.order-xl-first{order:-1!important}.order-xl-0{order:0!important}.order-xl-1{order:1!important}.order-xl-2{order:2!important}.order-xl-3{order:3!important}.order-xl-4{order:4!important}.order-xl-5{order:5!important}.order-xl-last{order:6!important}.m-xl-0{margin:0!important}.m-xl-1{margin:.25rem!important}.m-xl-2{margin:.5rem!important}.m-xl-3{margin:1rem!important}.m-xl-4{margin:1.5rem!important}.m-xl-5{margin:3rem!important}.m-xl-auto{margin:auto!important}.mx-xl-0{margin-right:0!important;margin-left:0!important}.mx-xl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xl-auto{margin-right:auto!important;margin-left:auto!important}.my-xl-0{margin-top:0!important;margin-bottom:0!important}.my-xl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xl-0{margin-top:0!important}.mt-xl-1{margin-top:.25rem!important}.mt-xl-2{margin-top:.5rem!important}.mt-xl-3{margin-top:1rem!important}.mt-xl-4{margin-top:1.5rem!important}.mt-xl-5{margin-top:3rem!important}.mt-xl-auto{margin-top:auto!important}.me-xl-0{margin-right:0!important}.me-xl-1{margin-right:.25rem!important}.me-xl-2{margin-right:.5rem!important}.me-xl-3{margin-right:1rem!important}.me-xl-4{margin-right:1.5rem!important}.me-xl-5{margin-right:3rem!important}.me-xl-auto{margin-right:auto!important}.mb-xl-0{margin-bottom:0!important}.mb-xl-1{margin-bottom:.25rem!important}.mb-xl-2{margin-bottom:.5rem!important}.mb-xl-3{margin-bottom:1rem!important}.mb-xl-4{margin-bottom:1.5rem!important}.mb-xl-5{margin-bottom:3rem!important}.mb-xl-auto{margin-bottom:auto!important}.ms-xl-0{margin-left:0!important}.ms-xl-1{margin-left:.25rem!important}.ms-xl-2{margin-left:.5rem!important}.ms-xl-3{margin-left:1rem!important}.ms-xl-4{margin-left:1.5rem!important}.ms-xl-5{margin-left:3rem!important}.ms-xl-auto{margin-left:auto!important}.p-xl-0{padding:0!important}.p-xl-1{padding:.25rem!important}.p-xl-2{padding:.5rem!important}.p-xl-3{padding:1rem!important}.p-xl-4{padding:1.5rem!important}.p-xl-5{padding:3rem!important}.px-xl-0{padding-right:0!important;padding-left:0!important}.px-xl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xl-0{padding-top:0!important;padding-bottom:0!important}.py-xl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xl-0{padding-top:0!important}.pt-xl-1{padding-top:.25rem!important}.pt-xl-2{padding-top:.5rem!important}.pt-xl-3{padding-top:1rem!important}.pt-xl-4{padding-top:1.5rem!important}.pt-xl-5{padding-top:3rem!important}.pe-xl-0{padding-right:0!important}.pe-xl-1{padding-right:.25rem!important}.pe-xl-2{padding-right:.5rem!important}.pe-xl-3{padding-right:1rem!important}.pe-xl-4{padding-right:1.5rem!important}.pe-xl-5{padding-right:3rem!important}.pb-xl-0{padding-bottom:0!important}.pb-xl-1{padding-bottom:.25rem!important}.pb-xl-2{padding-bottom:.5rem!important}.pb-xl-3{padding-bottom:1rem!important}.pb-xl-4{padding-bottom:1.5rem!important}.pb-xl-5{padding-bottom:3rem!important}.ps-xl-0{padding-left:0!important}.ps-xl-1{padding-left:.25rem!important}.ps-xl-2{padding-left:.5rem!important}.ps-xl-3{padding-left:1rem!important}.ps-xl-4{padding-left:1.5rem!important}.ps-xl-5{padding-left:3rem!important}.text-xl-start{text-align:left!important}.text-xl-end{text-align:right!important}.text-xl-center{text-align:center!important}}@media (min-width:1400px){.float-xxl-start{float:left!important}.float-xxl-end{float:right!important}.float-xxl-none{float:none!important}.d-xxl-inline{display:inline!important}.d-xxl-inline-block{display:inline-block!important}.d-xxl-block{display:block!important}.d-xxl-grid{display:grid!important}.d-xxl-table{display:table!important}.d-xxl-table-row{display:table-row!important}.d-xxl-table-cell{display:table-cell!important}.d-xxl-flex{display:flex!important}.d-xxl-inline-flex{display:inline-flex!important}.d-xxl-none{display:none!important}.flex-xxl-fill{flex:1 1 auto!important}.flex-xxl-row{flex-direction:row!important}.flex-xxl-column{flex-direction:column!important}.flex-xxl-row-reverse{flex-direction:row-reverse!important}.flex-xxl-column-reverse{flex-direction:column-reverse!important}.flex-xxl-grow-0{flex-grow:0!important}.flex-xxl-grow-1{flex-grow:1!important}.flex-xxl-shrink-0{flex-shrink:0!important}.flex-xxl-shrink-1{flex-shrink:1!important}.flex-xxl-wrap{flex-wrap:wrap!important}.flex-xxl-nowrap{flex-wrap:nowrap!important}.flex-xxl-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-xxl-0{grid-gap:0!important;gap:0!important}.gap-xxl-1{grid-gap:.25rem!important;gap:.25rem!important}.gap-xxl-2{grid-gap:.5rem!important;gap:.5rem!important}.gap-xxl-3{grid-gap:1rem!important;gap:1rem!important}.gap-xxl-4{grid-gap:1.5rem!important;gap:1.5rem!important}.gap-xxl-5{grid-gap:3rem!important;gap:3rem!important}.justify-content-xxl-start{justify-content:flex-start!important}.justify-content-xxl-end{justify-content:flex-end!important}.justify-content-xxl-center{justify-content:center!important}.justify-content-xxl-between{justify-content:space-between!important}.justify-content-xxl-around{justify-content:space-around!important}.justify-content-xxl-evenly{justify-content:space-evenly!important}.align-items-xxl-start{align-items:flex-start!important}.align-items-xxl-end{align-items:flex-end!important}.align-items-xxl-center{align-items:center!important}.align-items-xxl-baseline{align-items:baseline!important}.align-items-xxl-stretch{align-items:stretch!important}.align-content-xxl-start{align-content:flex-start!important}.align-content-xxl-end{align-content:flex-end!important}.align-content-xxl-center{align-content:center!important}.align-content-xxl-between{align-content:space-between!important}.align-content-xxl-around{align-content:space-around!important}.align-content-xxl-stretch{align-content:stretch!important}.align-self-xxl-auto{align-self:auto!important}.align-self-xxl-start{align-self:flex-start!important}.align-self-xxl-end{align-self:flex-end!important}.align-self-xxl-center{align-self:center!important}.align-self-xxl-baseline{align-self:baseline!important}.align-self-xxl-stretch{align-self:stretch!important}.order-xxl-first{order:-1!important}.order-xxl-0{order:0!important}.order-xxl-1{order:1!important}.order-xxl-2{order:2!important}.order-xxl-3{order:3!important}.order-xxl-4{order:4!important}.order-xxl-5{order:5!important}.order-xxl-last{order:6!important}.m-xxl-0{margin:0!important}.m-xxl-1{margin:.25rem!important}.m-xxl-2{margin:.5rem!important}.m-xxl-3{margin:1rem!important}.m-xxl-4{margin:1.5rem!important}.m-xxl-5{margin:3rem!important}.m-xxl-auto{margin:auto!important}.mx-xxl-0{margin-right:0!important;margin-left:0!important}.mx-xxl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xxl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xxl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xxl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xxl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xxl-auto{margin-right:auto!important;margin-left:auto!important}.my-xxl-0{margin-top:0!important;margin-bottom:0!important}.my-xxl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xxl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xxl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xxl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xxl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xxl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xxl-0{margin-top:0!important}.mt-xxl-1{margin-top:.25rem!important}.mt-xxl-2{margin-top:.5rem!important}.mt-xxl-3{margin-top:1rem!important}.mt-xxl-4{margin-top:1.5rem!important}.mt-xxl-5{margin-top:3rem!important}.mt-xxl-auto{margin-top:auto!important}.me-xxl-0{margin-right:0!important}.me-xxl-1{margin-right:.25rem!important}.me-xxl-2{margin-right:.5rem!important}.me-xxl-3{margin-right:1rem!important}.me-xxl-4{margin-right:1.5rem!important}.me-xxl-5{margin-right:3rem!important}.me-xxl-auto{margin-right:auto!important}.mb-xxl-0{margin-bottom:0!important}.mb-xxl-1{margin-bottom:.25rem!important}.mb-xxl-2{margin-bottom:.5rem!important}.mb-xxl-3{margin-bottom:1rem!important}.mb-xxl-4{margin-bottom:1.5rem!important}.mb-xxl-5{margin-bottom:3rem!important}.mb-xxl-auto{margin-bottom:auto!important}.ms-xxl-0{margin-left:0!important}.ms-xxl-1{margin-left:.25rem!important}.ms-xxl-2{margin-left:.5rem!important}.ms-xxl-3{margin-left:1rem!important}.ms-xxl-4{margin-left:1.5rem!important}.ms-xxl-5{margin-left:3rem!important}.ms-xxl-auto{margin-left:auto!important}.p-xxl-0{padding:0!important}.p-xxl-1{padding:.25rem!important}.p-xxl-2{padding:.5rem!important}.p-xxl-3{padding:1rem!important}.p-xxl-4{padding:1.5rem!important}.p-xxl-5{padding:3rem!important}.px-xxl-0{padding-right:0!important;padding-left:0!important}.px-xxl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xxl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xxl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xxl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xxl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xxl-0{padding-top:0!important;padding-bottom:0!important}.py-xxl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xxl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xxl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xxl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xxl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xxl-0{padding-top:0!important}.pt-xxl-1{padding-top:.25rem!important}.pt-xxl-2{padding-top:.5rem!important}.pt-xxl-3{padding-top:1rem!important}.pt-xxl-4{padding-top:1.5rem!important}.pt-xxl-5{padding-top:3rem!important}.pe-xxl-0{padding-right:0!important}.pe-xxl-1{padding-right:.25rem!important}.pe-xxl-2{padding-right:.5rem!important}.pe-xxl-3{padding-right:1rem!important}.pe-xxl-4{padding-right:1.5rem!important}.pe-xxl-5{padding-right:3rem!important}.pb-xxl-0{padding-bottom:0!important}.pb-xxl-1{padding-bottom:.25rem!important}.pb-xxl-2{padding-bottom:.5rem!important}.pb-xxl-3{padding-bottom:1rem!important}.pb-xxl-4{padding-bottom:1.5rem!important}.pb-xxl-5{padding-bottom:3rem!important}.ps-xxl-0{padding-left:0!important}.ps-xxl-1{padding-left:.25rem!important}.ps-xxl-2{padding-left:.5rem!important}.ps-xxl-3{padding-left:1rem!important}.ps-xxl-4{padding-left:1.5rem!important}.ps-xxl-5{padding-left:3rem!important}.text-xxl-start{text-align:left!important}.text-xxl-end{text-align:right!important}.text-xxl-center{text-align:center!important}}@media (min-width:1200px){.fs-1{font-size:2.5rem!important}.fs-2{font-size:2rem!important}.fs-3{font-size:1.75rem!important}.fs-4{font-size:1.5rem!important}}@media print{.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-grid{display:grid!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:flex!important}.d-print-inline-flex{display:inline-flex!important}.d-print-none{display:none!important}} +/*# sourceMappingURL=2.3ab4f688.chunk.css.map */ \ No newline at end of file diff --git a/sz-poc-offsite-2025/uniswap/nomiswap/build/static/css/2.3ab4f688.chunk.css.map b/sz-poc-offsite-2025/uniswap/nomiswap/build/static/css/2.3ab4f688.chunk.css.map new file mode 100644 index 0000000..b25bdc6 --- /dev/null +++ b/sz-poc-offsite-2025/uniswap/nomiswap/build/static/css/2.3ab4f688.chunk.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["webpack://node_modules/bootstrap/scss/_type.scss","webpack://node_modules/bootstrap/dist/css/bootstrap.css","webpack://node_modules/bootstrap/scss/bootstrap.scss","webpack://node_modules/bootstrap/scss/_root.scss","webpack://node_modules/bootstrap/scss/_reboot.scss","webpack://node_modules/bootstrap/scss/_variables.scss","webpack://node_modules/bootstrap/scss/vendor/_rfs.scss","webpack://node_modules/bootstrap/scss/mixins/_border-radius.scss","webpack://node_modules/bootstrap/scss/mixins/_lists.scss","webpack://node_modules/bootstrap/scss/_images.scss","webpack://node_modules/bootstrap/scss/mixins/_image.scss","webpack://node_modules/bootstrap/scss/_containers.scss","webpack://node_modules/bootstrap/scss/mixins/_container.scss","webpack://node_modules/bootstrap/scss/mixins/_breakpoints.scss","webpack://node_modules/bootstrap/scss/_grid.scss","webpack://node_modules/bootstrap/scss/mixins/_grid.scss","webpack://node_modules/bootstrap/scss/_tables.scss","webpack://node_modules/bootstrap/scss/mixins/_table-variants.scss","webpack://node_modules/bootstrap/scss/forms/_labels.scss","webpack://node_modules/bootstrap/scss/forms/_form-text.scss","webpack://node_modules/bootstrap/scss/forms/_form-control.scss","webpack://node_modules/bootstrap/scss/mixins/_transition.scss","webpack://node_modules/bootstrap/scss/mixins/_gradients.scss","webpack://node_modules/bootstrap/scss/forms/_form-select.scss","webpack://node_modules/bootstrap/scss/forms/_form-check.scss","webpack://node_modules/bootstrap/scss/forms/_form-range.scss","webpack://node_modules/bootstrap/scss/forms/_floating-labels.scss","webpack://node_modules/bootstrap/scss/forms/_input-group.scss","webpack://node_modules/bootstrap/scss/mixins/_forms.scss","webpack://node_modules/bootstrap/scss/_buttons.scss","webpack://node_modules/bootstrap/scss/mixins/_buttons.scss","webpack://node_modules/bootstrap/scss/_transitions.scss","webpack://node_modules/bootstrap/scss/_dropdown.scss","webpack://node_modules/bootstrap/scss/mixins/_caret.scss","webpack://node_modules/bootstrap/scss/_button-group.scss","webpack://node_modules/bootstrap/scss/_nav.scss","webpack://node_modules/bootstrap/scss/_navbar.scss","webpack://node_modules/bootstrap/scss/_card.scss","webpack://node_modules/bootstrap/scss/_accordion.scss","webpack://node_modules/bootstrap/scss/_breadcrumb.scss","webpack://node_modules/bootstrap/scss/_pagination.scss","webpack://node_modules/bootstrap/scss/mixins/_pagination.scss","webpack://node_modules/bootstrap/scss/_badge.scss","webpack://node_modules/bootstrap/scss/_alert.scss","webpack://node_modules/bootstrap/scss/mixins/_alert.scss","webpack://node_modules/bootstrap/scss/_progress.scss","webpack://node_modules/bootstrap/scss/_list-group.scss","webpack://node_modules/bootstrap/scss/mixins/_list-group.scss","webpack://node_modules/bootstrap/scss/_close.scss","webpack://node_modules/bootstrap/scss/_toasts.scss","webpack://node_modules/bootstrap/scss/_modal.scss","webpack://node_modules/bootstrap/scss/_tooltip.scss","webpack://node_modules/bootstrap/scss/mixins/_reset-text.scss","webpack://node_modules/bootstrap/scss/_popover.scss","webpack://node_modules/bootstrap/scss/_carousel.scss","webpack://node_modules/bootstrap/scss/mixins/_clearfix.scss","webpack://node_modules/bootstrap/scss/_spinners.scss","webpack://node_modules/bootstrap/scss/_offcanvas.scss","webpack://node_modules/bootstrap/scss/helpers/_colored-links.scss","webpack://node_modules/bootstrap/scss/helpers/_ratio.scss","webpack://node_modules/bootstrap/scss/helpers/_position.scss","webpack://node_modules/bootstrap/scss/helpers/_visually-hidden.scss","webpack://node_modules/bootstrap/scss/mixins/_visually-hidden.scss","webpack://node_modules/bootstrap/scss/helpers/_stretched-link.scss","webpack://node_modules/bootstrap/scss/helpers/_text-truncation.scss","webpack://node_modules/bootstrap/scss/mixins/_text-truncate.scss","webpack://node_modules/bootstrap/scss/mixins/_utilities.scss","webpack://node_modules/bootstrap/scss/utilities/_api.scss"],"names":[],"mappings":"AAoGE,gBC2cF;AC/iBA;;;;;EAAA,CCAA,MAGI,iBAAA,CAAA,mBAAA,CAAA,mBAAA,CAAA,iBAAA,CAAA,gBAAA,CAAA,mBAAA,CAAA,mBAAA,CAAA,kBAAA,CAAA,iBAAA,CAAA,iBAAA,CAAA,eAAA,CAAA,iBAAA,CAAA,sBAAA,CAIA,oBAAA,CAAA,sBAAA,CAAA,oBAAA,CAAA,iBAAA,CAAA,oBAAA,CAAA,mBAAA,CAAA,kBAAA,CAAA,iBAAA,CAKF,wMAAA,CACA,kGAAA,CACA,4EFkBF,CGjBA,iBAGE,qBHoBF,CGPI,8CAJJ,MAKM,sBHWJ,CACF,CGCA,KACE,QAAA,CACA,+LCsX4B,CDtX5B,qCCsX4B,CChIxB,cALI,CF/OR,eCgY4B,CD/X5B,eCqY4B,CDpY5B,aClCS,CDoCT,qBC7CS,CD8CT,6BAAA,CACA,yCHCF,CGQA,GACE,aAAA,CACA,aCqb4B,CDpb5B,6BAAA,CACA,QAAA,CACA,WHLF,CGQA,eACE,UHLF,CGeA,0CACE,YAAA,CACA,mBC0X4B,CDvX5B,eC0X4B,CDzX5B,eHdF,CGkBA,OE4MQ,gCL1NR,CKwDI,0BF1CJ,OEmNQ,gBL7NN,CACF,CGcA,OEuMQ,+BLjNR,CK+CI,0BFrCJ,OE8MQ,cLpNN,CACF,CGUA,OEkMQ,6BLxMR,CKsCI,0BFhCJ,OEyMQ,iBL3MN,CACF,CGMA,OE6LQ,+BL/LR,CK6BI,0BF3BJ,OEoMQ,gBLlMN,CACF,CGEA,OEoLM,iBLlLN,CGGA,OE+KM,cL9KN,CGUA,EACE,YAAA,CACA,kBHPF,CGkBA,yCAEE,wCAAA,CAAA,gCAAA,CACA,WAAA,CACA,qCAAA,CAAA,6BHfF,CGqBA,QACE,kBAAA,CACA,iBAAA,CACA,mBHlBF,CGwBA,MAEE,iBHrBF,CGwBA,SAGE,YAAA,CACA,kBHrBF,CGwBA,wBAIE,eHrBF,CGwBA,GACE,eHrBF,CG0BA,GACE,mBAAA,CACA,aHvBF,CG6BA,WACE,eH1BF,CGkCA,SAEE,kBH/BF,CGuCA,aEgFM,gBLnHN,CG0CA,WACE,YCkS4B,CDjS5B,wBHvCF,CGgDA,QAEE,iBAAA,CE4DI,eALI,CFrDR,aAAA,CACA,uBH7CF,CGgDA,IAAM,aH5CN,CG6CA,IAAM,SHzCN,CG8CA,EACE,aChNQ,CDiNR,yBH3CF,CG6CE,QACE,aH3CJ,CGsDE,4DAEE,aAAA,CACA,oBHpDJ,CG2DA,kBAIE,0FCmJ4B,CDnJ5B,oCCmJ4B,CCjIxB,aALI,CFXR,aAAA,CACA,0BHxDF,CG+DA,IACE,aAAA,CACA,YAAA,CACA,kBAAA,CACA,aAAA,CEII,gBL/DN,CGgEE,SEDI,iBALI,CFQN,aAAA,CACA,iBH9DJ,CGkEA,KERM,gBALI,CFeR,aCtQQ,CDuQR,oBH/DF,CGkEE,OACE,aHhEJ,CGoEA,IACE,mBAAA,CEpBI,gBALI,CF2BR,UCnTS,CDoTT,wBC3SS,CEEP,mBNyOJ,CGmEE,QACE,SAAA,CE3BE,aALI,CFkCN,eHjEJ,CG0EA,OACE,eHvEF,CG6EA,QAEE,qBH1EF,CGkFA,MACE,mBAAA,CACA,wBH/EF,CGkFA,QACE,iBC8K4B,CD7K5B,oBC6K4B,CD5K5B,aCtVS,CDuVT,eH/EF,CGsFA,GAEE,kBAAA,CACA,+BHpFF,CGuFA,2BAQE,cAAA,CAFA,oBHlFF,CG4FA,MACE,oBHzFF,CG+FA,OAEE,eH7FF,CGqGA,iCACE,SHlGF,CGuGA,sCAKE,QAAA,CACA,mBAAA,CE1HI,iBALI,CFiIR,mBHpGF,CGwGA,cAEE,mBHrGF,CG0GA,cACE,cHvGF,CG0GA,OAGE,gBHzGF,CG4GE,gBACE,SH1GJ,CGiHA,0CACE,YH9GF,CGsHA,gDAIE,yBHnHF,CGsHI,4GACE,cHjHN,CGwHA,mBACE,SAAA,CACA,iBHrHF,CG0HA,SACE,eHvHF,CGiIA,SACE,WAAA,CACA,SAAA,CACA,QAAA,CACA,QH9HF,CGsIA,OACE,UAAA,CACA,UAAA,CACA,SAAA,CACA,mBCG4B,CClNtB,+BAAA,CFkNN,mBHpIF,CKhPI,0BF6WJ,OEpMQ,gBL2EN,CACF,CGiIE,SACE,UH/HJ,CGsIA,+OAOE,SHnIF,CGsIA,4BACE,WHnIF,CG4IA,cACE,mBAAA,CACA,4BHzIF,CG4JA,4BACE,uBHjJF,CGsJA,+BACE,SHnJF,CGyJA,uBACE,YHtJF,CG4JA,6BACE,YAAA,CACA,yBHzJF,CG8JA,OACE,oBH3JF,CGgKA,OACE,QH7JF,CGoKA,QACE,iBAAA,CACA,cHjKF,CGyKA,SACE,uBHtKF,CG8KA,SACE,sBH3KF,CDpaA,MMyQM,iBALI,CNlQR,eCuaF,CDlaE,WMsQM,gCAAA,CNpQJ,eK4bkB,CL3blB,eCqaJ,CKpUI,0BNpGF,WM6QM,cL+JN,CACF,CD7aE,WMsQM,gCAAA,CNpQJ,eK4bkB,CL3blB,eCgbJ,CK/UI,0BNpGF,WM6QM,gBL0KN,CACF,CDxbE,WMsQM,gCAAA,CNpQJ,eK4bkB,CL3blB,eC2bJ,CK1VI,0BNpGF,WM6QM,cLqLN,CACF,CDncE,WMsQM,gCAAA,CNpQJ,eK4bkB,CL3blB,eCscJ,CKrWI,0BNpGF,WM6QM,gBLgMN,CACF,CD9cE,WMsQM,gCAAA,CNpQJ,eK4bkB,CL3blB,eCidJ,CKhXI,0BNpGF,WM6QM,cL2MN,CACF,CDzdE,WMsQM,gCAAA,CNpQJ,eK4bkB,CL3blB,eC4dJ,CK3XI,0BNpGF,WM6QM,gBLsNN,CACF,CDzcA,4BQ1DE,cAAA,CACA,eP4gBF,CDhdA,kBACE,oBCmdF,CDjdE,mCACE,kBCmdJ,CDzcA,YMsNM,gBALI,CN/MR,wBC4cF,CDxcA,YACE,kBKmKO,CC4CH,iBL6PN,CDzcE,wBACE,eC2cJ,CDvcA,mBACE,gBAAA,CACA,kBKyJO,CC4CH,gBALI,CN9LR,aC0cF,CDxcE,0BACE,YC0cJ,CQliBA,0BCFE,cAAA,CAGA,WT+iBF,CQhjBA,eACE,cJ2yCkC,CI1yClC,qBJPS,CIQT,wBAAA,CFGE,oBN0iBJ,CQjiBA,QAEE,oBRmiBF,CQhiBA,YACE,mBAAA,CACA,aRmiBF,CQhiBA,gBH+PM,gBALI,CGxPR,aRmiBF,CUrkBE,mGCHA,UAAA,CACA,oBAAA,CAAA,uCAAA,CACA,mBAAA,CAAA,sCAAA,CACA,iBAAA,CACA,gBXklBF,CY1hBI,yBF5CE,yBACE,eV0kBN,CACF,CYhiBI,yBF5CE,uCACE,eV+kBN,CACF,CYriBI,yBF5CE,qDACE,eVolBN,CACF,CY1iBI,0BF5CE,mEACE,gBVylBN,CACF,CY/iBI,0BF5CE,kFACE,gBV8lBN,CACF,Ca9mBE,KCAA,oBAAA,CACA,eAAA,CACA,YAAA,CACA,cAAA,CACA,sCAAA,CACA,0CAAA,CACA,yCdinBF,CapnBI,OCYF,aAAA,CACA,UAAA,CACA,cAAA,CACA,0CAAA,CACA,yCAAA,CACA,6Bd2mBF,Cc5jBM,KACE,Qd+jBR,Cc5jBM,iBApCJ,aAAA,CACA,UdomBF,CctlBE,cACE,aAAA,CACA,UdylBJ,Cc3lBE,cACE,aAAA,CACA,Sd8lBJ,CchmBE,cACE,aAAA,CACA,oBdmmBJ,CcrmBE,cACE,aAAA,CACA,SdwmBJ,Cc1mBE,cACE,aAAA,CACA,Sd6mBJ,Cc/mBE,cACE,aAAA,CACA,oBdknBJ,CY5mBI,yBESE,QACE,QdumBN,CcpmBI,oBApCJ,aAAA,CACA,Ud4oBA,Cc9nBA,iBACE,aAAA,CACA,UdioBF,CcnoBA,iBACE,aAAA,CACA,SdsoBF,CcxoBA,iBACE,aAAA,CACA,oBd2oBF,Cc7oBA,iBACE,aAAA,CACA,SdgpBF,CclpBA,iBACE,aAAA,CACA,SdqpBF,CcvpBA,iBACE,aAAA,CACA,oBd0pBF,CACF,CYrpBI,yBESE,QACE,Qd+oBN,Cc5oBI,oBApCJ,aAAA,CACA,UdorBA,CctqBA,iBACE,aAAA,CACA,UdyqBF,Cc3qBA,iBACE,aAAA,CACA,Sd8qBF,CchrBA,iBACE,aAAA,CACA,oBdmrBF,CcrrBA,iBACE,aAAA,CACA,SdwrBF,Cc1rBA,iBACE,aAAA,CACA,Sd6rBF,Cc/rBA,iBACE,aAAA,CACA,oBdksBF,CACF,CY7rBI,yBESE,QACE,QdurBN,CcprBI,oBApCJ,aAAA,CACA,Ud4tBA,Cc9sBA,iBACE,aAAA,CACA,UditBF,CcntBA,iBACE,aAAA,CACA,SdstBF,CcxtBA,iBACE,aAAA,CACA,oBd2tBF,Cc7tBA,iBACE,aAAA,CACA,SdguBF,CcluBA,iBACE,aAAA,CACA,SdquBF,CcvuBA,iBACE,aAAA,CACA,oBd0uBF,CACF,CYruBI,0BESE,QACE,Qd+tBN,Cc5tBI,oBApCJ,aAAA,CACA,UdowBA,CctvBA,iBACE,aAAA,CACA,UdyvBF,Cc3vBA,iBACE,aAAA,CACA,Sd8vBF,CchwBA,iBACE,aAAA,CACA,oBdmwBF,CcrwBA,iBACE,aAAA,CACA,SdwwBF,Cc1wBA,iBACE,aAAA,CACA,Sd6wBF,Cc/wBA,iBACE,aAAA,CACA,oBdkxBF,CACF,CY7wBI,0BESE,SACE,QduwBN,CcpwBI,qBApCJ,aAAA,CACA,Ud4yBA,Cc9xBA,kBACE,aAAA,CACA,UdiyBF,CcnyBA,kBACE,aAAA,CACA,SdsyBF,CcxyBA,kBACE,aAAA,CACA,oBd2yBF,Cc7yBA,kBACE,aAAA,CACA,SdgzBF,CclzBA,kBACE,aAAA,CACA,SdqzBF,CcvzBA,kBACE,aAAA,CACA,oBd0zBF,CACF,CctxBM,UAtDJ,aAAA,CACA,Ud+0BF,CcpxBU,OAtEN,aAAA,CACA,iBd81BJ,CczxBU,OAtEN,aAAA,CACA,kBdm2BJ,Cc9xBU,OAtEN,aAAA,CACA,Sdw2BJ,CcnyBU,OAtEN,aAAA,CACA,kBd62BJ,CcxyBU,OAtEN,aAAA,CACA,kBdk3BJ,Cc7yBU,OAtEN,aAAA,CACA,Sdu3BJ,CclzBU,OAtEN,aAAA,CACA,kBd43BJ,CcvzBU,OAtEN,aAAA,CACA,kBdi4BJ,Cc5zBU,OAtEN,aAAA,CACA,Sds4BJ,Ccj0BU,QAtEN,aAAA,CACA,kBd24BJ,Cct0BU,QAtEN,aAAA,CACA,kBdg5BJ,Cc30BU,QAtEN,aAAA,CACA,Udq5BJ,Ccx0BY,UA9DV,uBd04BF,Cc50BY,UA9DV,wBd84BF,Cch1BY,UA9DV,edk5BF,Ccp1BY,UA9DV,wBds5BF,Ccx1BY,UA9DV,wBd05BF,Cc51BY,UA9DV,ed85BF,Cch2BY,UA9DV,wBdk6BF,Ccp2BY,UA9DV,wBds6BF,Ccx2BY,UA9DV,ed06BF,Cc52BY,WA9DV,wBd86BF,Cch3BY,WA9DV,wBdk7BF,Ccz2BQ,WAEE,ed42BV,Ccz2BQ,WAEE,ed42BV,Ccn3BQ,WAEE,qBds3BV,Ccn3BQ,WAEE,qBds3BV,Cc73BQ,WAEE,oBdg4BV,Cc73BQ,WAEE,oBdg4BV,Ccv4BQ,WAEE,kBd04BV,Ccv4BQ,WAEE,kBd04BV,Ccj5BQ,WAEE,oBdo5BV,Ccj5BQ,WAEE,oBdo5BV,Cc35BQ,WAEE,kBd85BV,Cc35BQ,WAEE,kBd85BV,CY79BI,yBE+BE,aAtDJ,aAAA,CACA,Udy/BA,Cc97BQ,UAtEN,aAAA,CACA,iBdwgCF,Ccn8BQ,UAtEN,aAAA,CACA,kBd6gCF,Ccx8BQ,UAtEN,aAAA,CACA,SdkhCF,Cc78BQ,UAtEN,aAAA,CACA,kBduhCF,Ccl9BQ,UAtEN,aAAA,CACA,kBd4hCF,Ccv9BQ,UAtEN,aAAA,CACA,SdiiCF,Cc59BQ,UAtEN,aAAA,CACA,kBdsiCF,Ccj+BQ,UAtEN,aAAA,CACA,kBd2iCF,Cct+BQ,UAtEN,aAAA,CACA,SdgjCF,Cc3+BQ,WAtEN,aAAA,CACA,kBdqjCF,Cch/BQ,WAtEN,aAAA,CACA,kBd0jCF,Ccr/BQ,WAtEN,aAAA,CACA,Ud+jCF,Ccl/BU,aA9DV,adojCA,Cct/BU,aA9DV,uBdwjCA,Cc1/BU,aA9DV,wBd4jCA,Cc9/BU,aA9DV,edgkCA,CclgCU,aA9DV,wBdokCA,CctgCU,aA9DV,wBdwkCA,Cc1gCU,aA9DV,ed4kCA,Cc9gCU,aA9DV,wBdglCA,CclhCU,aA9DV,wBdolCA,CcthCU,aA9DV,edwlCA,Cc1hCU,cA9DV,wBd4lCA,Cc9hCU,cA9DV,wBdgmCA,CcvhCM,iBAEE,ed0hCR,CcvhCM,iBAEE,ed0hCR,CcjiCM,iBAEE,qBdoiCR,CcjiCM,iBAEE,qBdoiCR,Cc3iCM,iBAEE,oBd8iCR,Cc3iCM,iBAEE,oBd8iCR,CcrjCM,iBAEE,kBdwjCR,CcrjCM,iBAEE,kBdwjCR,Cc/jCM,iBAEE,oBdkkCR,Cc/jCM,iBAEE,oBdkkCR,CczkCM,iBAEE,kBd4kCR,CczkCM,iBAEE,kBd4kCR,CACF,CY5oCI,yBE+BE,aAtDJ,aAAA,CACA,UduqCA,Cc5mCQ,UAtEN,aAAA,CACA,iBdsrCF,CcjnCQ,UAtEN,aAAA,CACA,kBd2rCF,CctnCQ,UAtEN,aAAA,CACA,SdgsCF,Cc3nCQ,UAtEN,aAAA,CACA,kBdqsCF,CchoCQ,UAtEN,aAAA,CACA,kBd0sCF,CcroCQ,UAtEN,aAAA,CACA,Sd+sCF,Cc1oCQ,UAtEN,aAAA,CACA,kBdotCF,Cc/oCQ,UAtEN,aAAA,CACA,kBdytCF,CcppCQ,UAtEN,aAAA,CACA,Sd8tCF,CczpCQ,WAtEN,aAAA,CACA,kBdmuCF,Cc9pCQ,WAtEN,aAAA,CACA,kBdwuCF,CcnqCQ,WAtEN,aAAA,CACA,Ud6uCF,CchqCU,aA9DV,adkuCA,CcpqCU,aA9DV,uBdsuCA,CcxqCU,aA9DV,wBd0uCA,Cc5qCU,aA9DV,ed8uCA,CchrCU,aA9DV,wBdkvCA,CcprCU,aA9DV,wBdsvCA,CcxrCU,aA9DV,ed0vCA,Cc5rCU,aA9DV,wBd8vCA,CchsCU,aA9DV,wBdkwCA,CcpsCU,aA9DV,edswCA,CcxsCU,cA9DV,wBd0wCA,Cc5sCU,cA9DV,wBd8wCA,CcrsCM,iBAEE,edwsCR,CcrsCM,iBAEE,edwsCR,Cc/sCM,iBAEE,qBdktCR,Cc/sCM,iBAEE,qBdktCR,CcztCM,iBAEE,oBd4tCR,CcztCM,iBAEE,oBd4tCR,CcnuCM,iBAEE,kBdsuCR,CcnuCM,iBAEE,kBdsuCR,Cc7uCM,iBAEE,oBdgvCR,Cc7uCM,iBAEE,oBdgvCR,CcvvCM,iBAEE,kBd0vCR,CcvvCM,iBAEE,kBd0vCR,CACF,CY1zCI,yBE+BE,aAtDJ,aAAA,CACA,Udq1CA,Cc1xCQ,UAtEN,aAAA,CACA,iBdo2CF,Cc/xCQ,UAtEN,aAAA,CACA,kBdy2CF,CcpyCQ,UAtEN,aAAA,CACA,Sd82CF,CczyCQ,UAtEN,aAAA,CACA,kBdm3CF,Cc9yCQ,UAtEN,aAAA,CACA,kBdw3CF,CcnzCQ,UAtEN,aAAA,CACA,Sd63CF,CcxzCQ,UAtEN,aAAA,CACA,kBdk4CF,Cc7zCQ,UAtEN,aAAA,CACA,kBdu4CF,Ccl0CQ,UAtEN,aAAA,CACA,Sd44CF,Ccv0CQ,WAtEN,aAAA,CACA,kBdi5CF,Cc50CQ,WAtEN,aAAA,CACA,kBds5CF,Ccj1CQ,WAtEN,aAAA,CACA,Ud25CF,Cc90CU,aA9DV,adg5CA,Ccl1CU,aA9DV,uBdo5CA,Cct1CU,aA9DV,wBdw5CA,Cc11CU,aA9DV,ed45CA,Cc91CU,aA9DV,wBdg6CA,Ccl2CU,aA9DV,wBdo6CA,Cct2CU,aA9DV,edw6CA,Cc12CU,aA9DV,wBd46CA,Cc92CU,aA9DV,wBdg7CA,Ccl3CU,aA9DV,edo7CA,Cct3CU,cA9DV,wBdw7CA,Cc13CU,cA9DV,wBd47CA,Ccn3CM,iBAEE,eds3CR,Ccn3CM,iBAEE,eds3CR,Cc73CM,iBAEE,qBdg4CR,Cc73CM,iBAEE,qBdg4CR,Ccv4CM,iBAEE,oBd04CR,Ccv4CM,iBAEE,oBd04CR,Ccj5CM,iBAEE,kBdo5CR,Ccj5CM,iBAEE,kBdo5CR,Cc35CM,iBAEE,oBd85CR,Cc35CM,iBAEE,oBd85CR,Ccr6CM,iBAEE,kBdw6CR,Ccr6CM,iBAEE,kBdw6CR,CACF,CYx+CI,0BE+BE,aAtDJ,aAAA,CACA,UdmgDA,Ccx8CQ,UAtEN,aAAA,CACA,iBdkhDF,Cc78CQ,UAtEN,aAAA,CACA,kBduhDF,Ccl9CQ,UAtEN,aAAA,CACA,Sd4hDF,Ccv9CQ,UAtEN,aAAA,CACA,kBdiiDF,Cc59CQ,UAtEN,aAAA,CACA,kBdsiDF,Ccj+CQ,UAtEN,aAAA,CACA,Sd2iDF,Cct+CQ,UAtEN,aAAA,CACA,kBdgjDF,Cc3+CQ,UAtEN,aAAA,CACA,kBdqjDF,Cch/CQ,UAtEN,aAAA,CACA,Sd0jDF,Ccr/CQ,WAtEN,aAAA,CACA,kBd+jDF,Cc1/CQ,WAtEN,aAAA,CACA,kBdokDF,Cc//CQ,WAtEN,aAAA,CACA,UdykDF,Cc5/CU,aA9DV,ad8jDA,CchgDU,aA9DV,uBdkkDA,CcpgDU,aA9DV,wBdskDA,CcxgDU,aA9DV,ed0kDA,Cc5gDU,aA9DV,wBd8kDA,CchhDU,aA9DV,wBdklDA,CcphDU,aA9DV,edslDA,CcxhDU,aA9DV,wBd0lDA,Cc5hDU,aA9DV,wBd8lDA,CchiDU,aA9DV,edkmDA,CcpiDU,cA9DV,wBdsmDA,CcxiDU,cA9DV,wBd0mDA,CcjiDM,iBAEE,edoiDR,CcjiDM,iBAEE,edoiDR,Cc3iDM,iBAEE,qBd8iDR,Cc3iDM,iBAEE,qBd8iDR,CcrjDM,iBAEE,oBdwjDR,CcrjDM,iBAEE,oBdwjDR,Cc/jDM,iBAEE,kBdkkDR,Cc/jDM,iBAEE,kBdkkDR,CczkDM,iBAEE,oBd4kDR,CczkDM,iBAEE,oBd4kDR,CcnlDM,iBAEE,kBdslDR,CcnlDM,iBAEE,kBdslDR,CACF,CYtpDI,0BE+BE,cAtDJ,aAAA,CACA,UdirDA,CctnDQ,WAtEN,aAAA,CACA,iBdgsDF,Cc3nDQ,WAtEN,aAAA,CACA,kBdqsDF,CchoDQ,WAtEN,aAAA,CACA,Sd0sDF,CcroDQ,WAtEN,aAAA,CACA,kBd+sDF,Cc1oDQ,WAtEN,aAAA,CACA,kBdotDF,Cc/oDQ,WAtEN,aAAA,CACA,SdytDF,CcppDQ,WAtEN,aAAA,CACA,kBd8tDF,CczpDQ,WAtEN,aAAA,CACA,kBdmuDF,Cc9pDQ,WAtEN,aAAA,CACA,SdwuDF,CcnqDQ,YAtEN,aAAA,CACA,kBd6uDF,CcxqDQ,YAtEN,aAAA,CACA,kBdkvDF,Cc7qDQ,YAtEN,aAAA,CACA,UduvDF,Cc1qDU,cA9DV,ad4uDA,Cc9qDU,cA9DV,uBdgvDA,CclrDU,cA9DV,wBdovDA,CctrDU,cA9DV,edwvDA,Cc1rDU,cA9DV,wBd4vDA,Cc9rDU,cA9DV,wBdgwDA,CclsDU,cA9DV,edowDA,CctsDU,cA9DV,wBdwwDA,Cc1sDU,cA9DV,wBd4wDA,Cc9sDU,cA9DV,edgxDA,CcltDU,eA9DV,wBdoxDA,CcttDU,eA9DV,wBdwxDA,Cc/sDM,mBAEE,edktDR,Cc/sDM,mBAEE,edktDR,CcztDM,mBAEE,qBd4tDR,CcztDM,mBAEE,qBd4tDR,CcnuDM,mBAEE,oBdsuDR,CcnuDM,mBAEE,oBdsuDR,Cc7uDM,mBAEE,kBdgvDR,Cc7uDM,mBAEE,kBdgvDR,CcvvDM,mBAEE,oBd0vDR,CcvvDM,mBAEE,oBd0vDR,CcjwDM,mBAEE,kBdowDR,CcjwDM,mBAEE,kBdowDR,CACF,Ce/3DA,OACE,yBAAA,CACA,gCAAA,CACA,gCAAA,CACA,sCAAA,CACA,+BAAA,CACA,oCAAA,CACA,8BAAA,CACA,qCAAA,CAEA,UAAA,CACA,kBX0OO,CWzOP,aXCS,CWAT,kBXogB4B,CWngB5B,oBfg4DF,Cez3DE,yBACE,aAAA,CACA,mCAAA,CACA,uBX4U0B,CW3U1B,uDf23DJ,Cex3DE,aACE,sBf03DJ,Cev3DE,aACE,qBfy3DJ,Cer3DE,uCACE,gCfu3DJ,Ce92DA,aACE,gBfi3DF,Cev2DE,4BACE,cf02DJ,Ce31DE,gCACE,kBf81DJ,Ce31DI,kCACE,kBf61DN,Cet1DE,oCACE,qBfy1DJ,Ceh1DE,yCACE,+CAAA,CACA,mCfm1DJ,Ce30DA,cACE,8CAAA,CACA,kCf80DF,Cet0DE,4BACE,6CAAA,CACA,iCfy0DJ,CgBj8DE,eAME,qBAAA,CACA,6BAAA,CACA,6BAAA,CACA,4BAAA,CACA,4BAAA,CACA,2BAAA,CACA,2BAAA,CAEA,UAbQ,CAcR,oBhB87DJ,CgB78DE,iBAME,qBAAA,CACA,6BAAA,CACA,6BAAA,CACA,4BAAA,CACA,4BAAA,CACA,2BAAA,CACA,2BAAA,CAEA,UAbQ,CAcR,oBhB08DJ,CgBz9DE,eAME,qBAAA,CACA,6BAAA,CACA,6BAAA,CACA,4BAAA,CACA,4BAAA,CACA,2BAAA,CACA,2BAAA,CAEA,UAbQ,CAcR,oBhBs9DJ,CgBr+DE,YAME,qBAAA,CACA,6BAAA,CACA,6BAAA,CACA,4BAAA,CACA,4BAAA,CACA,2BAAA,CACA,2BAAA,CAEA,UAbQ,CAcR,oBhBk+DJ,CgBj/DE,eAME,qBAAA,CACA,6BAAA,CACA,6BAAA,CACA,4BAAA,CACA,4BAAA,CACA,2BAAA,CACA,2BAAA,CAEA,UAbQ,CAcR,oBhB8+DJ,CgB7/DE,cAME,qBAAA,CACA,6BAAA,CACA,6BAAA,CACA,4BAAA,CACA,4BAAA,CACA,2BAAA,CACA,2BAAA,CAEA,UAbQ,CAcR,oBhB0/DJ,CgBzgEE,aAME,qBAAA,CACA,6BAAA,CACA,6BAAA,CACA,4BAAA,CACA,4BAAA,CACA,2BAAA,CACA,2BAAA,CAEA,UAbQ,CAcR,oBhBsgEJ,CgBrhEE,YAME,qBAAA,CACA,6BAAA,CACA,6BAAA,CACA,4BAAA,CACA,4BAAA,CACA,2BAAA,CACA,2BAAA,CAEA,UAbQ,CAcR,oBhBkhEJ,Cel5DI,kBACE,eAAA,CACA,gCfq5DN,CY59DI,4BGqEA,qBACE,eAAA,CACA,gCf25DJ,CACF,CYn+DI,4BGqEA,qBACE,eAAA,CACA,gCfi6DJ,CACF,CYz+DI,4BGqEA,qBACE,eAAA,CACA,gCfu6DJ,CACF,CY/+DI,6BGqEA,qBACE,eAAA,CACA,gCf66DJ,CACF,CYr/DI,6BGqEA,sBACE,eAAA,CACA,gCfm7DJ,CACF,CiBnkEA,YACE,mBjBqkEF,CiB5jEA,gBACE,+BAAA,CACA,kCAAA,CACA,eAAA,CZoRI,iBALI,CY3QR,ejB6jEF,CiBzjEA,mBACE,6BAAA,CACA,gCAAA,CZ0QI,iBLmzDN,CiBzjEA,mBACE,8BAAA,CACA,iCAAA,CZoQI,iBLyzDN,CkB1lEA,WACE,iBdkpBsC,CClXlC,gBALI,CavRR,alB2lEF,CmBhmEA,cACE,aAAA,CACA,UAAA,CACA,sBAAA,Cd8RI,cALI,CctRR,efua4B,Ceta5B,ef4a4B,Ce3a5B,afKS,CeJT,qBfLS,CeMT,2BAAA,CACA,wBAAA,CACA,uBAAA,CAAA,oBAAA,CAAA,eAAA,CbGE,oBAAA,CcHE,oEpBomEN,CoBhmEM,uCDhBN,cCiBQ,epBmmEN,CACF,CmBjmEE,yBACE,enBmmEJ,CmBjmEI,wDACE,cnBmmEN,CmB9lEE,oBACE,afjBO,CekBP,qBf3BO,Ce4BP,oBfgqBoC,Ce/pBpC,SAAA,CAKE,4CnB4lEN,CmBrlEE,2CAEE,YnBslEJ,CmBllEE,yCACE,af1CO,Ce4CP,SnBmlEJ,CmBtlEE,oCACE,af1CO,Ce4CP,SnBmlEJ,CmBtlEE,2BACE,af1CO,Ce4CP,SnBmlEJ,CmB3kEE,+CAEE,wBf1DO,Ce6DP,SnB0kEJ,CmBtkEE,oCACE,sBAAA,CACA,uBAAA,CACA,yBf4f0B,Ce5f1B,wBf4f0B,Ce3f1B,af9DO,CiBbT,wBjBMS,CeuEP,mBAAA,CAGA,cAAA,CAFA,oBAAA,CAGA,2BfmR0B,CelR1B,eAAA,CCtEE,6HpB+oEN,CoB3oEM,uCDuDJ,oCCtDM,epB8oEN,CACF,CmB1kEE,yEACE,wBnB4kEJ,CmBzkEE,0CACE,sBAAA,CACA,uBAAA,CACA,yBfye0B,Ceze1B,wBfye0B,Cexe1B,afjFO,CiBbT,wBjBMS,Ce0FP,mBAAA,CAGA,cAAA,CAFA,oBAAA,CAGA,2BfgQ0B,Ce/P1B,eAAA,CCzFE,qID0FF,CC1FE,6HpBqqEN,CoBjqEM,uCD0EJ,0CCzEM,uBAAA,CAAA,epBoqEN,CACF,CmB7kEE,+EACE,wBnB+kEJ,CmBtkEA,wBACE,aAAA,CACA,UAAA,CACA,iBAAA,CACA,eAAA,CACA,ef2T4B,Ce1T5B,af5GS,Ce6GT,4BAAA,CAEA,wBAAA,CAAA,kBnBykEF,CmBvkEE,gFAEE,eAAA,CACA,cnBwkEJ,CmB7jEA,iBACE,oCfkkBsC,CejkBtC,oBAAA,CdmJI,iBALI,CC7QN,mBNisEJ,CmB9jEE,uCACE,oBAAA,CACA,qBAAA,CACA,wBf6b0B,Ce7b1B,uBnBgkEJ,CmB7jEE,6CACE,oBAAA,CACA,qBAAA,CACA,wBfub0B,Cevb1B,uBnB+jEJ,CmB3jEA,iBACE,mCfgjBsC,Ce/iBtC,kBAAA,CdgII,iBALI,CC7QN,mBNktEJ,CmB5jEE,uCACE,kBAAA,CACA,mBAAA,CACA,uBf8a0B,Ce9a1B,sBnB8jEJ,CmB3jEE,6CACE,kBAAA,CACA,mBAAA,CACA,uBfwa0B,Cexa1B,sBnB6jEJ,CmBrjEE,sBACE,qCnBwjEJ,CmBrjEE,yBACE,oCnBujEJ,CmBpjEE,yBACE,mCnBsjEJ,CmBjjEA,oBACE,cAAA,CACA,WAAA,CACA,enBojEF,CmBljEE,mDACE,cnBojEJ,CmBjjEE,uCACE,YAAA,Cb/LA,oBNmvEJ,CmBhjEE,0CACE,YAAA,CbpMA,oBNuvEJ,CsBrwEA,aACE,aAAA,CACA,UAAA,CACA,sCAAA,CAEA,qCAAA,CjB2RI,cALI,CiBnRR,elBoa4B,CkBna5B,elBya4B,CkBxa5B,alBES,CkBDT,qBlBRS,CkBST,8PAAA,CACA,2BAAA,CACA,uClBgxBkC,CkB/wBlC,yBlBgxBkC,CkB/wBlC,wBAAA,ChBFE,oBAAA,CcHE,oEEQJ,CACA,uBAAA,CAAA,oBAAA,CAAA,etBqwEF,CoB1wEM,uCEfN,aFgBQ,epB6wEN,CACF,CsBxwEE,mBACE,oBlBwqBoC,CkBvqBpC,SAAA,CAKE,4CtBswEN,CsBlwEE,0DAEE,oBlBkiB0B,CkBjiB1B,qBtBmwEJ,CsBhwEE,sBAEE,wBtBiwEJ,CsB5vEE,4BACE,iBAAA,CACA,yBtB8vEJ,CsB1vEA,gBACE,kBlB2hB4B,CkB1hB5B,qBlB0hB4B,CkBzhB5B,kBlB0hB4B,CCjTxB,iBLqhEN,CsB1vEA,gBACE,iBlBwhB4B,CkBvhB5B,oBlBuhB4B,CkBthB5B,iBlBuhB4B,CCrTxB,iBL4hEN,CuB7zEA,YACE,aAAA,CACA,iBnBqtBwC,CmBptBxC,kBnBqtBwC,CmBptBxC,qBvBg0EF,CuB9zEE,8BACE,UAAA,CACA,kBvBg0EJ,CuB5zEA,kBACE,SnBysBwC,CmBxsBxC,UnBwsBwC,CmBvsBxC,gBAAA,CACA,kBAAA,CACA,qBnBbS,CmBcT,2BAAA,CACA,uBAAA,CACA,uBAAA,CACA,gCnB4sBwC,CmB3sBxC,uBAAA,CAAA,oBAAA,CAAA,eAAA,CACA,gCAAA,CAAA,kBvB+zEF,CuB5zEE,iCjBXE,mBN00EJ,CuB3zEE,8BAEE,iBvB4zEJ,CuBzzEE,yBACE,8BnB0rBsC,CmB1rBtC,sBvB2zEJ,CuBxzEE,wBACE,oBnBwpBoC,CmBvpBpC,SAAA,CACA,4CvB0zEJ,CuBvzEE,0BACE,wBnBZM,CmBaN,oBvByzEJ,CuBvzEI,yCAII,4PvBszER,CuBlzEI,sCAII,oKvBizER,CuB5yEE,+CACE,wBnBjCM,CmBkCN,oBnBlCM,CmBuCJ,sPvB0yEN,CuBtyEE,2BACE,mBAAA,CACA,mBAAA,CAAA,WAAA,CACA,UvBwyEJ,CuBjyEI,2FACE,UvBmyEN,CuBrxEA,aACE,kBvBwxEF,CuBtxEE,+BACE,SnBipB8B,CmBhpB9B,kBAAA,CACA,iLAAA,CACA,qBAAA,CjB9FA,iBAAA,CcHE,+CpB23EN,CoBv3EM,uCGyFJ,+BHxFM,epB03EN,CACF,CuB3xEI,qCACE,uKvB6xEN,CuB1xEI,uCACE,wBnBgpB4B,CmB3oB1B,oKvBwxER,CuBlxEA,mBACE,oBAAA,CACA,iBvBqxEF,CuBlxEA,WACE,iBAAA,CACA,kBAAA,CACA,mBvBqxEF,CuBjxEI,mDACE,mBAAA,CACA,mBAAA,CAAA,WAAA,CACA,WvBmxEN,CwBj6EA,YACE,UAAA,CACA,aAAA,CACA,SAAA,CACA,4BAAA,CACA,uBAAA,CAAA,oBAAA,CAAA,exBo6EF,CwBl6EE,kBACE,SxBo6EJ,CwBh6EI,wCAA0B,2DxBm6E9B,CwBl6EI,oCAA0B,2DxBq6E9B,CwBl6EE,8BACE,QxBo6EJ,CwBj6EE,kCACE,UpB6yBuC,CoB5yBvC,WpB4yBuC,CoB3yBvC,kBAAA,CHzBF,wBjBkCQ,CoBPN,QpB4yBuC,CExzBvC,kBAAA,CcHE,8GIkBF,CJlBE,sGIkBF,CACA,uBAAA,CAAA,exBk6EJ,CoBj7EM,uCIMJ,kCJLM,uBAAA,CAAA,epBo7EN,CACF,CwBr6EI,yCHjCF,wBrBy8EF,CwBn6EE,2CACE,UpBsxB8B,CoBrxB9B,YpBsxB8B,CoBrxB9B,iBAAA,CACA,cpBqxB8B,CoBpxB9B,wBpBpCO,CoBqCP,wBAAA,ClB7BA,kBNm8EJ,CwBj6EE,8BACE,UpBkxBuC,CoBjxBvC,WpBixBuC,CiBp0BzC,wBjBkCQ,CoBmBN,QpBkxBuC,CExzBvC,kBAAA,CcHE,2GI4CF,CJ5CE,sGI4CF,CACA,oBAAA,CAAA,exBk6EJ,CoB38EM,uCIiCJ,8BJhCM,oBAAA,CAAA,epB88EN,CACF,CwBr6EI,qCH3DF,wBrBm+EF,CwBn6EE,8BACE,UpB4vB8B,CoB3vB9B,YpB4vB8B,CoB3vB9B,iBAAA,CACA,cpB2vB8B,CoB1vB9B,wBpB9DO,CoB+DP,wBAAA,ClBvDA,kBN69EJ,CwBj6EE,qBACE,mBxBm6EJ,CwBj6EI,2CACE,wBxBm6EN,CwBh6EI,uCACE,wBxBk6EN,CyBz/EA,eACE,iBzB4/EF,CyB1/EE,yDAEE,yBrBu1B8B,CqBt1B9B,gBzB4/EJ,CyBz/EE,qBACE,iBAAA,CACA,KAAA,CACA,MAAA,CACA,WAAA,CACA,mBAAA,CACA,mBAAA,CACA,4BAAA,CACA,oBAAA,CLDE,4DpB6/EN,CoBz/EM,uCKXJ,qBLYM,epB4/EN,CACF,CyB5/EE,6BACE,mBzB8/EJ,CyB5/EI,wDACE,iBzB8/EN,CyB//EI,mDACE,iBzB8/EN,CyB//EI,0CACE,iBzB8/EN,CyB3/EI,yDAEE,oBrBi0B4B,CqBh0B5B,sBzB4/EN,CyB//EI,wFAEE,oBrBi0B4B,CqBh0B5B,sBzB4/EN,CyBz/EI,8CACE,oBrB4zB4B,CqB3zB5B,sBzB2/EN,CyBv/EE,4BACE,oBrBszB8B,CqBrzB9B,sBzBy/EJ,CyBn/EI,+DACE,WrBgzB4B,CqB/yB5B,0DzBu/EN,CyBz/EI,sIACE,WrBgzB4B,CqB/yB5B,0DzBu/EN,CyBl/EI,oDACE,WrByyB4B,CqBxyB5B,0DzBo/EN,C0B1iFA,aACE,iBAAA,CACA,YAAA,CACA,cAAA,CACA,mBAAA,CACA,U1B6iFF,C0B3iFE,qDAEE,iBAAA,CACA,aAAA,CACA,QAAA,CACA,W1B6iFJ,C0BziFE,iEAEE,S1B2iFJ,C0BriFE,kBACE,iBAAA,CACA,S1BuiFJ,C0BriFI,wBACE,S1BuiFN,C0B5hFA,kBACE,YAAA,CACA,kBAAA,CACA,sBAAA,CrBsPI,cALI,CqB/OR,etBgY4B,CsB/X5B,etBqY4B,CsBpY5B,atBlCS,CsBmCT,iBAAA,CACA,kBAAA,CACA,wBtB5CS,CsB6CT,wBAAA,CpBpCE,oBNokFJ,C0BthFA,kHAIE,kBAAA,CrBgOI,iBALI,CC7QN,mBN6kFJ,C0BthFA,kHAIE,oBAAA,CrBuNI,iBALI,CC7QN,mBNslFJ,C0BthFA,0DAEE,kB1ByhFF,C0BrgFI,iUpBtEA,yBAAA,CACA,4BNqlFJ,C0BrgFE,0IACE,gBAAA,CpBpEA,wBAAA,CACA,2BN4kFJ,C2BrmFE,gBACE,YAAA,CACA,UAAA,CACA,iBvB2nBoC,CClXlC,gBALI,CsBjQN,a3BumFJ,C2BpmFE,eACE,iBAAA,CACA,QAAA,CACA,SAAA,CACA,YAAA,CACA,cAAA,CACA,oBAAA,CACA,gBAAA,CtB4PE,iBALI,CsBpPN,UAvBc,CAwBd,mCAvBiB,CrBHjB,oBNioFJ,C2BlmFI,8HAEE,a3BumFN,C2BrpFI,0DAoDE,oBvB6zBmB,CuB1zBjB,kCvBipBgC,CuBhpBhC,yQAAA,CACA,2BAAA,CACA,wDAAA,CACA,2D3BmmFR,C2BhmFM,sEACE,oBvBkzBiB,CuBjzBjB,2C3BkmFR,C2BlqFI,0EAyEI,kCvB+nBgC,CuB9nBhC,6E3B6lFR,C2BvqFI,wDAiFE,oB3B0lFN,C2BvlFQ,4NAEE,sBvB4sB8B,CuB3sB9B,ufAAA,CACA,4DAAA,CACA,qE3BwlFV,C2BplFM,oEACE,oBvBmxBiB,CuBlxBjB,2C3BslFR,C2BrrFI,kEAsGE,oB3BmlFN,C2BjlFM,kFACE,wB3BmlFR,C2BhlFM,8EACE,2C3BklFR,C2B/kFM,sGACE,a3BilFR,C2B5kFI,qDACE,gB3B+kFN,C2BtsFI,sKA+HI,S3B6kFR,C2BzkFM,8LACE,S3B6kFR,C2B9rFE,kBACE,YAAA,CACA,UAAA,CACA,iBvB2nBoC,CClXlC,gBALI,CsBjQN,a3BgsFJ,C2B7rFE,iBACE,iBAAA,CACA,QAAA,CACA,SAAA,CACA,YAAA,CACA,cAAA,CACA,oBAAA,CACA,gBAAA,CtB4PE,iBALI,CsBpPN,UAvBc,CAwBd,mCAvBiB,CrBHjB,oBN0tFJ,C2B3rFI,8IAEE,a3BgsFN,C2B9uFI,8DAoDE,oBvB6zBmB,CuB1zBjB,kCvBipBgC,CuBhpBhC,qUAAA,CACA,2BAAA,CACA,wDAAA,CACA,2D3B4rFR,C2BzrFM,0EACE,oBvBkzBiB,CuBjzBjB,2C3B2rFR,C2B3vFI,8EAyEI,kCvB+nBgC,CuB9nBhC,6E3BsrFR,C2BhwFI,4DAiFE,oB3BmrFN,C2BhrFQ,oOAEE,sBvB4sB8B,CuB3sB9B,mjBAAA,CACA,4DAAA,CACA,qE3BirFV,C2B7qFM,wEACE,oBvBmxBiB,CuBlxBjB,2C3B+qFR,C2B9wFI,sEAsGE,oB3B4qFN,C2B1qFM,sFACE,wB3B4qFR,C2BzqFM,kFACE,2C3B2qFR,C2BxqFM,0GACE,a3B0qFR,C2BrqFI,uDACE,gB3BwqFN,C2B/xFI,8KAiII,S3BoqFR,C2BlqFM,sMACE,S3BsqFR,C4B5yFA,KACE,oBAAA,CAEA,exB0a4B,CwBza5B,exB+a4B,CwB9a5B,axBQS,CwBPT,iBAAA,CACA,oBAAA,CAEA,qBAAA,CACA,cAAA,CACA,wBAAA,CAAA,oBAAA,CAAA,gBAAA,CACA,4BAAA,CACA,4BAAA,CC8GA,sBAAA,CxBsKI,cALI,CC7QN,oBAAA,CcHE,6HpBkzFN,CoB9yFM,uCQhBN,KRiBQ,epBizFN,CACF,C4BlzFE,WACE,a5BozFJ,C4BhzFE,iCAEE,SAAA,CACA,4C5BizFJ,C4BnyFE,mDAGE,mBAAA,CACA,W5BmyFJ,C4BvxFE,aCvCA,UAXQ,CRLR,wBjB4Ea,CyB1Db,oB7Bk0FF,C6BzzFE,oEALE,UAdY,CRRd,wBQMmB,CAkBjB,oB7Bu0FJ,C6Bp0FE,iDASI,2C7B2zFN,C6BvzFE,0IAKE,UAlCa,CAmCb,wBArCkB,CAwClB,oB7BmzFJ,C6BjzFI,wKAKI,2C7B+yFR,C6B1yFE,4CAEE,UAjDe,CAkDf,wBzBYW,CyBTX,oB7ByyFJ,C4BrzFE,eCvCA,UAXQ,CRLR,wBjB4Ea,CyB1Db,oB7Bg2FF,C6Bv1FE,0EALE,UAdY,CRRd,wBQMmB,CAkBjB,oB7Bq2FJ,C6Bl2FE,qDASI,4C7By1FN,C6Br1FE,oJAKE,UAlCa,CAmCb,wBArCkB,CAwClB,oB7Bi1FJ,C6B/0FI,kLAKI,4C7B60FR,C6Bx0FE,gDAEE,UAjDe,CAkDf,wBzBYW,CyBTX,oB7Bu0FJ,C4Bn1FE,aCvCA,UAXQ,CRLR,wBjB4Ea,CyB1Db,oB7B83FF,C6Br3FE,oEALE,UAdY,CRRd,wBQMmB,CAkBjB,oB7Bm4FJ,C6Bh4FE,iDASI,2C7Bu3FN,C6Bn3FE,0IAKE,UAlCa,CAmCb,wBArCkB,CAwClB,oB7B+2FJ,C6B72FI,wKAKI,2C7B22FR,C6Bt2FE,4CAEE,UAjDe,CAkDf,wBzBYW,CyBTX,oB7Bq2FJ,C4Bj3FE,UCvCA,UAXQ,CRLR,wBjB4Ea,CyB1Db,oB7B45FF,C6Bn5FE,2DALE,UAdY,CRRd,wBQMmB,CAkBjB,oB7Bi6FJ,C6B95FE,2CASI,2C7Bq5FN,C6Bj5FE,2HAKE,UAlCa,CAmCb,wBArCkB,CAwClB,oB7B64FJ,C6B34FI,yJAKI,2C7By4FR,C6Bp4FE,sCAEE,UAjDe,CAkDf,wBzBYW,CyBTX,oB7Bm4FJ,C4B/4FE,aCvCA,UAXQ,CRLR,wBjB4Ea,CyB1Db,oB7B07FF,C6Bj7FE,oEALE,UAdY,CRRd,wBQMmB,CAkBjB,oB7B+7FJ,C6B57FE,iDASI,0C7Bm7FN,C6B/6FE,0IAKE,UAlCa,CAmCb,wBArCkB,CAwClB,oB7B26FJ,C6Bz6FI,wKAKI,0C7Bu6FR,C6Bl6FE,4CAEE,UAjDe,CAkDf,wBzBYW,CyBTX,oB7Bi6FJ,C4B76FE,YCvCA,UAXQ,CRLR,wBjB4Ea,CyB1Db,oB7Bw9FF,C6B/8FE,iEALE,UAdY,CRRd,wBQMmB,CAkBjB,oB7B69FJ,C6B19FE,+CASI,0C7Bi9FN,C6B78FE,qIAKE,UAlCa,CAmCb,wBArCkB,CAwClB,oB7By8FJ,C6Bv8FI,mKAKI,0C7Bq8FR,C6Bh8FE,0CAEE,UAjDe,CAkDf,wBzBYW,CyBTX,oB7B+7FJ,C4B38FE,WCvCA,UAXQ,CRLR,wBjB4Ea,CyB1Db,oB7Bs/FF,C6B7+FE,8DALE,UAdY,CRRd,wBQMmB,CAkBjB,oB7B2/FJ,C6Bx/FE,6CASI,4C7B++FN,C6B3+FE,gIAKE,UAlCa,CAmCb,wBArCkB,CAwClB,oB7Bu+FJ,C6Br+FI,8JAKI,4C7Bm+FR,C6B99FE,wCAEE,UAjDe,CAkDf,wBzBYW,CyBTX,oB7B69FJ,C4Bz+FE,UCvCA,UAXQ,CRLR,wBjB4Ea,CyB1Db,oB7BohGF,C6B3gGE,2DALE,UAdY,CRRd,wBQMmB,CAkBjB,oB7ByhGJ,C6BthGE,2CASI,yC7B6gGN,C6BzgGE,2HAKE,UAlCa,CAmCb,wBArCkB,CAwClB,oB7BqgGJ,C6BngGI,yJAKI,yC7BigGR,C6B5/FE,sCAEE,UAjDe,CAkDf,wBzBYW,CyBTX,oB7B2/FJ,C4BjgGE,qBCmBA,azBJa,CyBKb,oB7Bk/FF,C6Bh/FE,2BACE,UATY,CAUZ,wBzBTW,CyBUX,oB7Bk/FJ,C6B/+FE,iEAEE,2C7Bg/FJ,C6B7+FE,iLAKE,UArBa,CAsBb,wBzBxBW,CyByBX,oB7B2+FJ,C6Bz+FI,+MAKI,2C7Bu+FR,C6Bl+FE,4DAEE,azBvCW,CyBwCX,4B7Bm+FJ,C4B1hGE,uBCmBA,azBJa,CyBKb,oB7B2gGF,C6BzgGE,6BACE,UATY,CAUZ,wBzBTW,CyBUX,oB7B2gGJ,C6BxgGE,qEAEE,4C7BygGJ,C6BtgGE,2LAKE,UArBa,CAsBb,wBzBxBW,CyByBX,oB7BogGJ,C6BlgGI,yNAKI,4C7BggGR,C6B3/FE,gEAEE,azBvCW,CyBwCX,4B7B4/FJ,C4BnjGE,qBCmBA,azBJa,CyBKb,oB7BoiGF,C6BliGE,2BACE,UATY,CAUZ,wBzBTW,CyBUX,oB7BoiGJ,C6BjiGE,iEAEE,0C7BkiGJ,C6B/hGE,iLAKE,UArBa,CAsBb,wBzBxBW,CyByBX,oB7B6hGJ,C6B3hGI,+MAKI,0C7ByhGR,C6BphGE,4DAEE,azBvCW,CyBwCX,4B7BqhGJ,C4B5kGE,kBCmBA,azBJa,CyBKb,oB7B6jGF,C6B3jGE,wBACE,UATY,CAUZ,wBzBTW,CyBUX,oB7B6jGJ,C6B1jGE,2DAEE,2C7B2jGJ,C6BxjGE,kKAKE,UArBa,CAsBb,wBzBxBW,CyByBX,oB7BsjGJ,C6BpjGI,gMAKI,2C7BkjGR,C6B7iGE,sDAEE,azBvCW,CyBwCX,4B7B8iGJ,C4BrmGE,qBCmBA,azBJa,CyBKb,oB7BslGF,C6BplGE,2BACE,UATY,CAUZ,wBzBTW,CyBUX,oB7BslGJ,C6BnlGE,iEAEE,0C7BolGJ,C6BjlGE,iLAKE,UArBa,CAsBb,wBzBxBW,CyByBX,oB7B+kGJ,C6B7kGI,+MAKI,0C7B2kGR,C6BtkGE,4DAEE,azBvCW,CyBwCX,4B7BukGJ,C4B9nGE,oBCmBA,azBJa,CyBKb,oB7B+mGF,C6B7mGE,0BACE,UATY,CAUZ,wBzBTW,CyBUX,oB7B+mGJ,C6B5mGE,+DAEE,0C7B6mGJ,C6B1mGE,4KAKE,UArBa,CAsBb,wBzBxBW,CyByBX,oB7BwmGJ,C6BtmGI,0MAKI,0C7BomGR,C6B/lGE,0DAEE,azBvCW,CyBwCX,4B7BgmGJ,C4BvpGE,mBCmBA,azBJa,CyBKb,oB7BwoGF,C6BtoGE,yBACE,UATY,CAUZ,wBzBTW,CyBUX,oB7BwoGJ,C6BroGE,6DAEE,4C7BsoGJ,C6BnoGE,uKAKE,UArBa,CAsBb,wBzBxBW,CyByBX,oB7BioGJ,C6B/nGI,qMAKI,4C7B6nGR,C6BxnGE,wDAEE,azBvCW,CyBwCX,4B7BynGJ,C4BhrGE,kBCmBA,azBJa,CyBKb,oB7BiqGF,C6B/pGE,wBACE,UATY,CAUZ,wBzBTW,CyBUX,oB7BiqGJ,C6B9pGE,2DAEE,yC7B+pGJ,C6B5pGE,kKAKE,UArBa,CAsBb,wBzBxBW,CyByBX,oB7B0pGJ,C6BxpGI,gMAKI,yC7BspGR,C6BjpGE,sDAEE,azBvCW,CyBwCX,4B7BkpGJ,C4B7rGA,UACE,exBmW4B,CwBlW5B,axBzCQ,CwB0CR,yB5BgsGF,C4B9rGE,gBACE,a5BgsGJ,C4BxrGE,sCAEE,a5ByrGJ,C4B9qGA,2BCuBE,kBAAA,CxBsKI,iBALI,CC7QN,mBNywGJ,C4BhrGA,2BCmBE,oBAAA,CxBsKI,iBALI,CC7QN,mBN+wGJ,C8BlyGA,MVgBM,8BpBsxGN,CoBlxGM,uCUpBN,MVqBQ,epBqxGN,CACF,C8BxyGE,iBACE,S9B0yGJ,C8BpyGE,qBACE,Y9BuyGJ,C8BnyGA,YACE,QAAA,CACA,eAAA,CVDI,2BpBwyGN,CoBpyGM,uCULN,YVMQ,epBuyGN,CACF,C+B5zGA,sCAIE,iB/B+zGF,C+B5zGA,iBACE,kB/B+zGF,CgC1yGI,uBACE,oBAAA,CACA,kB5BwWwB,C4BvWxB,qB5BsWwB,C4BrWxB,UAAA,CAhCJ,qBAAA,CACA,mCAAA,CACA,eAAA,CACA,kChC60GF,CgCxxGI,6BACE,ahC0xGN,C+Br0GA,eACE,iBAAA,CACA,Y3Bu3BkC,C2Bt3BlC,YAAA,CACA,e3B48BkC,C2B38BlC,eAAA,CACA,QAAA,C1B+QI,cALI,C0BxQR,a3BPS,C2BQT,eAAA,CACA,eAAA,CACA,qB3BnBS,C2BoBT,2BAAA,CACA,gCAAA,CzBVE,oBNm1GJ,C+Br0GE,+BACE,QAAA,CACA,MAAA,CACA,kB/Bu0GJ,C+B3zGI,qBACE,mB/B8zGN,C+B5zGM,qCACE,UAAA,CACA,M/B8zGR,C+B1zGI,mBACE,iB/B6zGN,C+B3zGM,mCACE,OAAA,CACA,S/B6zGR,CY5zGI,yBmBfA,wBACE,mB/B+0GJ,C+B70GI,wCACE,UAAA,CACA,M/B+0GN,C+B30GE,sBACE,iB/B80GJ,C+B50GI,sCACE,OAAA,CACA,S/B80GN,CACF,CY90GI,yBmBfA,wBACE,mB/Bg2GJ,C+B91GI,wCACE,UAAA,CACA,M/Bg2GN,C+B51GE,sBACE,iB/B+1GJ,C+B71GI,sCACE,OAAA,CACA,S/B+1GN,CACF,CY/1GI,yBmBfA,wBACE,mB/Bi3GJ,C+B/2GI,wCACE,UAAA,CACA,M/Bi3GN,C+B72GE,sBACE,iB/Bg3GJ,C+B92GI,sCACE,OAAA,CACA,S/Bg3GN,CACF,CYh3GI,0BmBfA,wBACE,mB/Bk4GJ,C+Bh4GI,wCACE,UAAA,CACA,M/Bk4GN,C+B93GE,sBACE,iB/Bi4GJ,C+B/3GI,sCACE,OAAA,CACA,S/Bi4GN,CACF,CYj4GI,0BmBfA,yBACE,mB/Bm5GJ,C+Bj5GI,yCACE,UAAA,CACA,M/Bm5GN,C+B/4GE,uBACE,iB/Bk5GJ,C+Bh5GI,uCACE,OAAA,CACA,S/Bk5GN,CACF,C+Bz4GE,uCACE,QAAA,CACA,WAAA,CACA,YAAA,CACA,qB/B24GJ,CgCz7GI,+BACE,oBAAA,CACA,kB5BwWwB,C4BvWxB,qB5BsWwB,C4BrWxB,UAAA,CAzBJ,YAAA,CACA,mCAAA,CACA,wBAAA,CACA,kChCq9GF,CgCv6GI,qCACE,ahCy6GN,C+B/4GE,wCACE,KAAA,CACA,UAAA,CACA,SAAA,CACA,YAAA,CACA,mB/Bk5GJ,CgC98GI,gCACE,oBAAA,CACA,kB5BwWwB,C4BvWxB,qB5BsWwB,C4BrWxB,UAAA,CAlBJ,iCAAA,CACA,cAAA,CACA,oCAAA,CACA,sBhCm+GF,CgC57GI,sCACE,ahC87GN,C+B15GI,gCACE,gB/B45GN,C+Bt5GE,0CACE,KAAA,CACA,UAAA,CACA,SAAA,CACA,YAAA,CACA,oB/By5GJ,CgCt+GI,kCACE,oBAAA,CACA,kB5BwWwB,C4BvWxB,qB5BsWwB,C4BrWxB,UAAA,CAYE,YhC49GR,CgCz9GM,mCACE,oBAAA,CACA,mB5BqVsB,C4BpVtB,qB5BmVsB,C4BlVtB,UAAA,CA9BN,iCAAA,CACA,uBAAA,CACA,oChC6/GF,CgC59GI,wCACE,ahC89GN,C+Bz6GI,mCACE,gB/B26GN,C+Bp6GA,kBACE,QAAA,CACA,cAAA,CACA,eAAA,CACA,oC/Bu6GF,C+Bj6GA,eACE,aAAA,CACA,UAAA,CACA,mBAAA,CACA,UAAA,CACA,e3B0S4B,C2BzS5B,a3BvHS,C2BwHT,kBAAA,CACA,oBAAA,CACA,kBAAA,CACA,4BAAA,CACA,Q/Bo6GF,C+Bt5GE,0CAEE,a3Bm1BgC,CiB5+BlC,wBrBijHF,C+Bn5GE,4CAEE,U3B5JO,C2B6JP,oBAAA,CVjKF,wBrBsjHF,C+Bj5GE,gDAEE,a3B9JO,C2B+JP,mBAAA,CACA,4B/Bk5GJ,C+B54GA,oBACE,a/B+4GF,C+B34GA,iBACE,aAAA,CACA,kB3Bk0BkC,C2Bj0BlC,eAAA,C1B0GI,iBALI,C0BnGR,a3B/KS,C2BgLT,kB/B84GF,C+B14GA,oBACE,aAAA,CACA,mBAAA,CACA,a/B64GF,C+Bz4GA,oBACE,a3B/LS,C2BgMT,wB3B3LS,C2B4LT,4B/B44GF,C+Bz4GE,mCACE,a/B24GJ,C+Bz4GI,kFAEE,U3B5MK,CiBJT,oCrB2lHF,C+Bv4GI,oFAEE,U3BlNK,CiBJT,wBrB+lHF,C+Br4GI,wFAEE,a/Bs4GN,C+Bl4GE,sCACE,4B/Bo4GJ,C+Bj4GE,wCACE,a/Bm4GJ,C+Bh4GE,qCACE,a/Bk4GJ,CiC9mHA,+BAEE,iBAAA,CACA,mBAAA,CACA,qBjCinHF,CiC/mHE,yCACE,iBAAA,CACA,ajCknHJ,CiC7mHE,kXAME,SjCqnHJ,CiChnHA,aACE,YAAA,CACA,cAAA,CACA,0BjCmnHF,CiCjnHE,0BACE,UjCmnHJ,CiC7mHE,0EAEE,gBjCgnHJ,CiC5mHE,mG3BRE,yBAAA,CACA,4BNwnHJ,CiCxmHE,6G3BHE,wBAAA,CACA,2BNgnHJ,CiC3lHA,uBACE,sBAAA,CACA,qBjC8lHF,CiC5lHE,wGAGE,ajC4lHJ,CiCzlHE,yCACE,cjC2lHJ,CiCvlHA,yEACE,qBAAA,CACA,oBjC0lHF,CiCvlHA,yEACE,oBAAA,CACA,mBjC0lHF,CiCtkHA,oBACE,qBAAA,CACA,sBAAA,CACA,sBjCykHF,CiCvkHE,wDAEE,UjCykHJ,CiCtkHE,4FAEE,ejCwkHJ,CiCpkHE,qH3BvFE,4BAAA,CACA,2BN+pHJ,CiCpkHE,oF3B1GE,wBAAA,CACA,yBNkrHJ,CkC1sHA,KACE,YAAA,CACA,cAAA,CACA,cAAA,CACA,eAAA,CACA,elC6sHF,CkC1sHA,UACE,aAAA,CACA,kBAAA,CAGA,a9BoBQ,C8BnBR,oBAAA,CdHI,iGpB+sHN,CoB3sHM,uCcPN,UdQQ,epB8sHN,CACF,CkC9sHE,gCAEE,alC+sHJ,CkC1sHE,mBACE,a9BhBO,C8BiBP,mBAAA,CACA,clC4sHJ,CkCpsHA,UACE,+BlCusHF,CkCrsHE,oBACE,kBAAA,CACA,eAAA,CACA,4BAAA,C5BlBA,6BAAA,CACA,8BN0tHJ,CkCtsHI,oDAEE,oC9Bg3B8B,C8B92B9B,iBlCssHN,CkCnsHI,6BACE,a9B3CK,C8B4CL,4BAAA,CACA,wBlCqsHN,CkCjsHE,8DAEE,a9BlDO,C8BmDP,qB9B1DO,C8B2DP,iClCmsHJ,CkChsHE,yBAEE,eAAA,C5B5CA,wBAAA,CACA,yBN8uHJ,CkCvrHE,qBACE,eAAA,CACA,QAAA,C5BnEA,oBN8vHJ,CkCvrHE,uDAEE,U9BpFO,CiBJT,wBrBkxHF,CkC/qHE,wCAEE,aAAA,CACA,iBlCkrHJ,CkC7qHE,kDAEE,YAAA,CACA,WAAA,CACA,iBlCgrHJ,CkC1qHE,iEACE,UlC8qHJ,CkCpqHE,uBACE,YlCuqHJ,CkCrqHE,qBACE,alCuqHJ,CmC/xHA,QACE,iBAAA,CACA,YAAA,CACA,cAAA,CACA,kBAAA,CACA,6BAAA,CACA,iB/B25BkC,C+Bz5BlC,oBnCiyHF,CmC1xHE,2JACE,YAAA,CACA,iBAAA,CACA,kBAAA,CACA,6BnCkyHJ,CmC9wHA,cACE,oB/Bk4BkC,C+Bj4BlC,uB/Bi4BkC,C+Bh4BlC,iB/Bi4BkC,CCtpB9B,iBALI,C8BpOR,oBAAA,CACA,kBnCgxHF,CmCnwHA,YACE,YAAA,CACA,qBAAA,CACA,cAAA,CACA,eAAA,CACA,enCqwHF,CmCnwHE,sBACE,eAAA,CACA,cnCqwHJ,CmClwHE,2BACE,enCowHJ,CmC3vHA,aACE,iB/BszBkC,C+BrzBlC,oBnC8vHF,CmClvHA,iBACE,eAAA,CACA,WAAA,CAGA,kBnCmvHF,CmC/uHA,gBACE,qBAAA,C9B6KI,iBALI,C8BtKR,aAAA,CACA,4BAAA,CACA,4BAAA,C7BzGE,oBAAA,CcHE,sCpBg2HN,CoB51HM,uCemGN,gBflGQ,epB+1HN,CACF,CmCrvHE,sBACE,oBnCuvHJ,CmCpvHE,sBACE,oBAAA,CACA,SAAA,CACA,uBnCsvHJ,CmChvHA,qBACE,oBAAA,CACA,WAAA,CACA,YAAA,CACA,qBAAA,CACA,2BAAA,CACA,uBAAA,CACA,oBnCmvHF,CmChvHA,mBACE,eAAA,CAAA,uCAAA,CACA,enCmvHF,CY70HI,yBuBsGA,kBAEI,gBAAA,CACA,0BnC0uHN,CmCxuHM,8BACE,kBnC0uHR,CmCxuHQ,6CACE,iBnC0uHV,CmCvuHQ,wCACE,mB/BkwBwB,C+BjwBxB,kBnCyuHV,CmCruHM,qCACE,gBnCuuHR,CmCpuHM,mCACE,sBAAA,CACA,enCsuHR,CmCnuHM,kCACE,YnCquHR,CACF,CYx2HI,yBuBsGA,kBAEI,gBAAA,CACA,0BnCowHN,CmClwHM,8BACE,kBnCowHR,CmClwHQ,6CACE,iBnCowHV,CmCjwHQ,wCACE,mB/BkwBwB,C+BjwBxB,kBnCmwHV,CmC/vHM,qCACE,gBnCiwHR,CmC9vHM,mCACE,sBAAA,CACA,enCgwHR,CmC7vHM,kCACE,YnC+vHR,CACF,CYl4HI,yBuBsGA,kBAEI,gBAAA,CACA,0BnC8xHN,CmC5xHM,8BACE,kBnC8xHR,CmC5xHQ,6CACE,iBnC8xHV,CmC3xHQ,wCACE,mB/BkwBwB,C+BjwBxB,kBnC6xHV,CmCzxHM,qCACE,gBnC2xHR,CmCxxHM,mCACE,sBAAA,CACA,enC0xHR,CmCvxHM,kCACE,YnCyxHR,CACF,CY55HI,0BuBsGA,kBAEI,gBAAA,CACA,0BnCwzHN,CmCtzHM,8BACE,kBnCwzHR,CmCtzHQ,6CACE,iBnCwzHV,CmCrzHQ,wCACE,mB/BkwBwB,C+BjwBxB,kBnCuzHV,CmCnzHM,qCACE,gBnCqzHR,CmClzHM,mCACE,sBAAA,CACA,enCozHR,CmCjzHM,kCACE,YnCmzHR,CACF,CYt7HI,0BuBsGA,mBAEI,gBAAA,CACA,0BnCk1HN,CmCh1HM,+BACE,kBnCk1HR,CmCh1HQ,8CACE,iBnCk1HV,CmC/0HQ,yCACE,mB/BkwBwB,C+BjwBxB,kBnCi1HV,CmC70HM,sCACE,gBnC+0HR,CmC50HM,oCACE,sBAAA,CACA,enC80HR,CmC30HM,mCACE,YnC60HR,CACF,CmC12HI,eAEI,gBAAA,CACA,0BnC22HR,CmCz2HQ,2BACE,kBnC22HV,CmCz2HU,0CACE,iBnC22HZ,CmCx2HU,qCACE,mB/BkwBwB,C+BjwBxB,kBnC02HZ,CmCt2HQ,kCACE,gBnCw2HV,CmCr2HQ,gCACE,sBAAA,CACA,enCu2HV,CmCp2HQ,+BACE,YnCs2HV,CmCp1HI,gGAEE,oBnCy1HN,CmCp1HI,oCACE,qBnCs1HN,CmCp1HM,oFAEE,oBnCq1HR,CmCl1HM,6CACE,oBnCo1HR,CmCh1HI,qFAEE,oBnCk1HN,CmC90HE,8BACE,qB/B+tBgC,C+B9tBhC,2BnCg1HJ,CmC70HE,mCACE,sQnC+0HJ,CmC50HE,2BACE,qBnC80HJ,CmC50HI,mGAGE,oBnC80HN,CmCp0HI,6FAEE,UnCy0HN,CmCp0HI,mCACE,yBnCs0HN,CmCp0HM,kFAEE,yBnCq0HR,CmCl0HM,4CACE,yBnCo0HR,CmCh0HI,mFAEE,UnCk0HN,CmC9zHE,6BACE,yB/BqqBgC,C+BpqBhC,+BnCg0HJ,CmC7zHE,kCACE,4QnC+zHJ,CmC5zHE,0BACE,yBnC8zHJ,CmC7zHI,gGAGE,UnC+zHN,CoCzmIA,MACE,iBAAA,CACA,YAAA,CACA,qBAAA,CACA,WAAA,CAEA,oBAAA,CACA,qBhCHS,CgCIT,0BAAA,CACA,iCAAA,C9BME,oBNsmIJ,CoCzmIE,SACE,cAAA,CACA,apC2mIJ,CoCxmIE,kBACE,kBAAA,CACA,qBpC0mIJ,CoCxmII,8BACE,kBAAA,C9BEF,yCAAA,CACA,0CNymIJ,CoCxmII,6BACE,qBAAA,C9BWF,6CAAA,CACA,4CNgmIJ,CoCrmIE,8DAEE,YpCumIJ,CoCnmIA,WAGE,aAAA,CACA,YpComIF,CoChmIA,YACE,mBpCmmIF,CoChmIA,eACE,kBpComIF,CoChmIA,qCAHE,epCumIF,CoC/lIE,iBACE,oBpCkmIJ,CoC/lIE,sBACE,gBpCimIJ,CoCzlIA,aACE,kBAAA,CACA,eAAA,CAEA,gChCi/BkC,CgCh/BlC,wCpC2lIF,CoCzlIE,yB9BnEE,uDN+pIJ,CoCvlIA,aACE,kBAAA,CAEA,gChCs+BkC,CgCr+BlC,qCpCylIF,CoCvlIE,wB9B9EE,uDNwqIJ,CoChlIA,kBAEE,oBAAA,CAEA,epCmlIF,CoCzkIA,qCAbE,mBAAA,CAEA,kBpCylIF,CoCxkIA,kBACE,iBAAA,CACA,KAAA,CACA,OAAA,CACA,QAAA,CACA,MAAA,CACA,YhCoHO,CEtOL,gCN8rIJ,CoCxkIA,yCAGE,UpC2kIF,CoCxkIA,wB9BnHI,yCAAA,CACA,0CNgsIJ,CoCzkIA,2B9B1GI,6CAAA,CACA,4CNwrIJ,CoClkIE,kBACE,oBpCqkIJ,CYxqII,yBwB+FJ,YAQI,YAAA,CACA,kBpCqkIF,CoClkIE,kBAEE,QAAA,CACA,epCmkIJ,CoCjkII,wBACE,aAAA,CACA,apCmkIN,CoC9jIM,mC9BnJJ,yBAAA,CACA,4BNotIF,CoC/jIQ,iGAGE,yBpCgkIV,CoC9jIQ,oGAGE,4BpC+jIV,CoC3jIM,oC9BpJJ,wBAAA,CACA,2BNktIF,CoC5jIQ,mGAGE,wBpC6jIV,CoC3jIQ,sGAGE,2BpC4jIV,CACF,CqCzwIA,kBACE,iBAAA,CACA,YAAA,CACA,kBAAA,CACA,UAAA,CACA,oBAAA,ChC4RI,cALI,CgCrRR,ajCMS,CiCLT,eAAA,CACA,qBjCLS,CiCMT,QAAA,C/BKE,eAAA,C+BHF,oBAAA,CjBAI,qJpB6wIN,CoBzwIM,uCiBhBN,kBjBiBQ,epB4wIN,CACF,CqC/wIE,kCACE,ajC8kCsC,CiC7kCtC,wBjC4kCsC,CiC3kCtC,0CrCixIJ,CqC/wII,wCACE,uSAAA,CACA,yBrCixIN,CqC5wIE,wBACE,aAAA,CACA,ajCskCsC,CiCrkCtC,cjCqkCsC,CiCpkCtC,gBAAA,CACA,UAAA,CACA,uSAAA,CACA,2BAAA,CACA,uBjCgkCsC,CgBvlCpC,oCpBsyIN,CoBlyIM,uCiBWJ,wBjBVM,epBqyIN,CACF,CqChxIE,wBACE,SrCkxIJ,CqC/wIE,wBACE,SAAA,CACA,oBjCmpBoC,CiClpBpC,SAAA,CACA,4CrCixIJ,CqC7wIA,kBACE,erCgxIF,CqC7wIA,gBACE,qBjCpDS,CiCqDT,iCrCgxIF,CqC9wIE,8B/BnCE,6BAAA,CACA,8BNozIJ,CqC/wII,gD/BtCA,yCAAA,CACA,0CNwzIJ,CqC9wIE,oCACE,YrCgxIJ,CqC5wIE,6B/BlCE,iCAAA,CACA,gCNizIJ,CqC5wIM,yD/BtCF,6CAAA,CACA,4CNqzIJ,CqC3wII,iD/B3CA,iCAAA,CACA,gCNyzIJ,CqCzwIA,gBACE,oBrC4wIF,CqCnwIE,qCACE,crCswIJ,CqCnwIE,iCACE,cAAA,CACA,aAAA,C/BxFA,eN81IJ,CqCnwII,6CAAgB,YrCswIpB,CqCrwII,4CAAe,erCwwInB,CqCtwII,mD/B9FA,eNu2IJ,CsC13IA,YACE,YAAA,CACA,cAAA,CACA,SAAA,CACA,kBlC60CkC,CkC30ClC,etC43IF,CsCr3IE,kCACE,kBtCw3IJ,CsCt3II,yCACE,UAAA,CACA,mBlC8zC8B,CkC7zC9B,alCLK,CkCML,WAAA,CAAA,wCtCw3IN,CsCp3IE,wBACE,atCs3IJ,CuC/4IA,YACE,YAAA,ChCGA,cAAA,CACA,ePg5IF,CuCh5IA,WACE,iBAAA,CACA,aAAA,CACA,anC8BQ,CmC7BR,oBAAA,CACA,qBnCFS,CmCGT,wBAAA,CnBKI,6HpB+4IN,CoB34IM,uCmBfN,WnBgBQ,epB84IN,CACF,CuCt5IE,iBACE,SAAA,CAIA,oBvCu5IJ,CuCp5IE,kCANE,anCkRsC,CmChRtC,wBvC+5IJ,CuC35IE,iBACE,SAAA,CAGA,SnCygCgC,CmCxgChC,4CvCs5IJ,CuCj5IE,wCACE,gBvCo5IJ,CuCj5IE,6BACE,SAAA,CACA,UnC9BO,CiBJT,wBjBkCQ,CmCEN,oBvCm5IJ,CuCh5IE,+BACE,anC9BO,CmC+BP,mBAAA,CACA,qBnCtCO,CmCuCP,oBvCk5IJ,CwC77IE,WACE,sBxCg8IJ,CwCz7IQ,kClCqCJ,6BAAA,CACA,gCNw5IJ,CwCx7IQ,iClCiBJ,8BAAA,CACA,iCN06IJ,CwC18IE,0BACE,qBAAA,CnCgSE,iBL8qIN,CwCv8IQ,iDlCqCJ,4BAAA,CACA,+BNq6IJ,CwCr8IQ,gDlCiBJ,6BAAA,CACA,gCNu7IJ,CwCv9IE,0BACE,oBAAA,CnCgSE,iBL2rIN,CwCp9IQ,iDlCqCJ,4BAAA,CACA,+BNk7IJ,CwCl9IQ,gDlCiBJ,6BAAA,CACA,gCNo8IJ,CyCn+IA,OACE,oBAAA,CACA,mBAAA,CpC8RI,eALI,CoCvRR,erCya4B,CqCxa5B,aAAA,CACA,UrCHS,CqCIT,iBAAA,CACA,kBAAA,CACA,uBAAA,CnCKE,oBNk+IJ,CyCl+IE,aACE,YzCo+IJ,CyC/9IA,YACE,iBAAA,CACA,QzCk+IF,C0Cz/IA,OACE,iBAAA,CACA,YAAA,CACA,kBtCuvC8B,CsCtvC9B,4BAAA,CpCWE,oBNk/IJ,C0Cx/IA,eAEE,a1C0/IF,C0Ct/IA,YACE,e1Cy/IF,C0Cj/IA,mBACE,kB1Co/IF,C0Cj/IE,8BACE,iBAAA,CACA,KAAA,CACA,OAAA,CACA,SAAA,CACA,oB1Cm/IJ,C0Cp+IE,eClDA,aD8Cc,CrB5Cd,wBqB0CmB,CC1CnB,oB3C0hJF,C2CxhJE,2BACE,a3C0hJJ,C0C7+IE,iBClDA,aD8Cc,CrB5Cd,wBqB0CmB,CC1CnB,oB3CmiJF,C2CjiJE,6BACE,a3CmiJJ,C0Ct/IE,eClDA,aD8Cc,CrB5Cd,wBqB0CmB,CC1CnB,oB3C4iJF,C2C1iJE,2BACE,a3C4iJJ,C0C//IE,YClDA,aDgDgB,CrB9ChB,wBqB0CmB,CC1CnB,oB3CqjJF,C2CnjJE,wBACE,a3CqjJJ,C0CxgJE,eClDA,aDgDgB,CrB9ChB,wBqB0CmB,CC1CnB,oB3C8jJF,C2C5jJE,2BACE,a3C8jJJ,C0CjhJE,cClDA,aD8Cc,CrB5Cd,wBqB0CmB,CC1CnB,oB3CukJF,C2CrkJE,0BACE,a3CukJJ,C0C1hJE,aClDA,aDgDgB,CrB9ChB,wBqB0CmB,CC1CnB,oB3CglJF,C2C9kJE,yBACE,a3CglJJ,C0CniJE,YClDA,aD8Cc,CrB5Cd,wBqB0CmB,CC1CnB,oB3CylJF,C2CvlJE,wBACE,a3CylJJ,C4C5lJE,gCACE,GAAK,0B5CgmJP,CACF,C4C5lJA,UAEE,WxCgwCkC,CCv+B9B,gBALI,CuCjRR,wBxCLS,CESP,oBN2lJJ,C4C1lJA,wBATE,YAAA,CAEA,e5C6mJF,C4CtmJA,cAEE,qBAAA,CACA,sBAAA,CAEA,UxCjBS,CwCkBT,iBAAA,CACA,kBAAA,CACA,wBxCUQ,CgBtBJ,yBpB0mJN,CoBtmJM,uCwBAN,cxBCQ,epBymJN,CACF,C4C/lJA,sBvBYE,qKAAA,CuBVA,yB5CkmJF,C4C9lJE,uBACE,iD5CimJJ,C4C9lJM,uCAJJ,uBAKM,c5CimJN,CACF,C6CzoJA,YACE,YAAA,CACA,qBAAA,CAGA,cAAA,CACA,eAAA,CvCSE,oBNkoJJ,C6CvoJA,qBACE,oBAAA,CACA,qB7C0oJF,C6CxoJE,+BAEE,kCAAA,CACA,yB7CyoJJ,C6C/nJA,wBACE,UAAA,CACA,azClBS,CyCmBT,kB7CkoJF,C6C/nJE,4DAEE,SAAA,CACA,azCzBO,CyC0BP,oBAAA,CACA,wB7CgoJJ,C6C7nJE,+BACE,azC7BO,CyC8BP,wB7C+nJJ,C6CtnJA,iBACE,iBAAA,CACA,aAAA,CACA,kBAAA,CACA,azC3CS,CyC4CT,oBAAA,CACA,qBzCtDS,CyCuDT,iC7CynJF,C6CvnJE,6BvCrCE,8BAAA,CACA,+BN+pJJ,C6CvnJE,4BvC3BE,kCAAA,CACA,iCNqpJJ,C6CvnJE,oDAEE,azC7DO,CyC8DP,mBAAA,CACA,qB7CwnJJ,C6CpnJE,wBACE,SAAA,CACA,UzC3EO,CyC4EP,wBzC9CM,CyC+CN,oB7CsnJJ,C6CnnJE,kCACE,kB7CqnJJ,C6CnnJI,yCACE,eAAA,CACA,oB7CqnJN,C6CvmJI,uBACE,kB7C0mJN,C6CvmJQ,oDvCrCJ,gCAAA,CAZA,yBN4pJJ,C6CtmJQ,mDvCtDJ,8BAAA,CAYA,2BNopJJ,C6CrmJQ,+CACE,Y7CumJV,C6CpmJQ,yDACE,oBzC0OoB,CyCzOpB,mB7CsmJV,C6CpmJU,gEACE,gBAAA,CACA,qB7CsmJZ,CY1qJI,yBiC4CA,0BACE,kB7CkoJJ,C6C/nJM,uDvCrCJ,gCAAA,CAZA,yBNorJF,C6C9nJM,sDvCtDJ,8BAAA,CAYA,2BN4qJF,C6C7nJM,kDACE,Y7C+nJR,C6C5nJM,4DACE,oBzC0OoB,CyCzOpB,mB7C8nJR,C6C5nJQ,mEACE,gBAAA,CACA,qB7C8nJV,CACF,CYnsJI,yBiC4CA,0BACE,kB7C0pJJ,C6CvpJM,uDvCrCJ,gCAAA,CAZA,yBN4sJF,C6CtpJM,sDvCtDJ,8BAAA,CAYA,2BNosJF,C6CrpJM,kDACE,Y7CupJR,C6CppJM,4DACE,oBzC0OoB,CyCzOpB,mB7CspJR,C6CppJQ,mEACE,gBAAA,CACA,qB7CspJV,CACF,CY3tJI,yBiC4CA,0BACE,kB7CkrJJ,C6C/qJM,uDvCrCJ,gCAAA,CAZA,yBNouJF,C6C9qJM,sDvCtDJ,8BAAA,CAYA,2BN4tJF,C6C7qJM,kDACE,Y7C+qJR,C6C5qJM,4DACE,oBzC0OoB,CyCzOpB,mB7C8qJR,C6C5qJQ,mEACE,gBAAA,CACA,qB7C8qJV,CACF,CYnvJI,0BiC4CA,0BACE,kB7C0sJJ,C6CvsJM,uDvCrCJ,gCAAA,CAZA,yBN4vJF,C6CtsJM,sDvCtDJ,8BAAA,CAYA,2BNovJF,C6CrsJM,kDACE,Y7CusJR,C6CpsJM,4DACE,oBzC0OoB,CyCzOpB,mB7CssJR,C6CpsJQ,mEACE,gBAAA,CACA,qB7CssJV,CACF,CY3wJI,0BiC4CA,2BACE,kB7CkuJJ,C6C/tJM,wDvCrCJ,gCAAA,CAZA,yBNoxJF,C6C9tJM,uDvCtDJ,8BAAA,CAYA,2BN4wJF,C6C7tJM,mDACE,Y7C+tJR,C6C5tJM,6DACE,oBzC0OoB,CyCzOpB,mB7C8tJR,C6C5tJQ,oEACE,gBAAA,CACA,qB7C8tJV,CACF,C6CjtJA,kBvC9HI,eNk1JJ,C6CjtJE,mCACE,oB7CmtJJ,C6CjtJI,8CACE,qB7CmtJN,C8Cv2JE,yBACE,aDiKyB,CChKzB,wB9C02JJ,C8Cv2JM,4GAEE,aD2JqB,CC1JrB,wB9Cw2JR,C8Cr2JM,uDACE,U1CRG,C0CSH,wBDqJqB,CCpJrB,oB9Cu2JR,C8Cr3JE,2BACE,aDiKyB,CChKzB,wB9Cw3JJ,C8Cr3JM,gHAEE,aD2JqB,CC1JrB,wB9Cs3JR,C8Cn3JM,yDACE,U1CRG,C0CSH,wBDqJqB,CCpJrB,oB9Cq3JR,C8Cn4JE,yBACE,aDiKyB,CChKzB,wB9Cs4JJ,C8Cn4JM,4GAEE,aD2JqB,CC1JrB,wB9Co4JR,C8Cj4JM,uDACE,U1CRG,C0CSH,wBDqJqB,CCpJrB,oB9Cm4JR,C8Cj5JE,sBACE,aDmK2B,CClK3B,wB9Co5JJ,C8Cj5JM,sGAEE,aD6JuB,CC5JvB,wB9Ck5JR,C8C/4JM,oDACE,U1CRG,C0CSH,wBDuJuB,CCtJvB,oB9Ci5JR,C8C/5JE,yBACE,aDmK2B,CClK3B,wB9Ck6JJ,C8C/5JM,4GAEE,aD6JuB,CC5JvB,wB9Cg6JR,C8C75JM,uDACE,U1CRG,C0CSH,wBDuJuB,CCtJvB,oB9C+5JR,C8C76JE,wBACE,aDiKyB,CChKzB,wB9Cg7JJ,C8C76JM,0GAEE,aD2JqB,CC1JrB,wB9C86JR,C8C36JM,sDACE,U1CRG,C0CSH,wBDqJqB,CCpJrB,oB9C66JR,C8C37JE,uBACE,aDmK2B,CClK3B,wB9C87JJ,C8C37JM,wGAEE,aD6JuB,CC5JvB,wB9C47JR,C8Cz7JM,qDACE,U1CRG,C0CSH,wBDuJuB,CCtJvB,oB9C27JR,C8Cz8JE,sBACE,aDiKyB,CChKzB,wB9C48JJ,C8Cz8JM,sGAEE,aD2JqB,CC1JrB,wB9C08JR,C8Cv8JM,oDACE,U1CRG,C0CSH,wBDqJqB,CCpJrB,oB9Cy8JR,C+Ct9JA,WACE,sBAAA,CACA,S3C04C2B,C2Cz4C3B,U3Cy4C2B,C2Cx4C3B,aAAA,CACA,U3CQS,C2CPT,uWAAA,CACA,QAAA,CzCOE,oBAAA,CyCLF,U/Cy9JF,C+Ct9JE,iBACE,UAAA,CACA,oBAAA,CACA,W/Cw9JJ,C+Cr9JE,iBACE,SAAA,CACA,4C3C0jB4B,C2CzjB5B,S/Cu9JJ,C+Cp9JE,wCAEE,mBAAA,CACA,wBAAA,CAAA,oBAAA,CAAA,gBAAA,CACA,W/Cq9JJ,C+Cj9JA,iBACE,yD3Cs3C2B,C2Ct3C3B,iD/Co9JF,CgD1/JA,OACE,W5C6qCkC,C4C5qClC,cAAA,C3CmSI,iBALI,C2C3RR,mBAAA,CACA,oC5C6qCkC,C4C5qClC,2BAAA,CACA,+BAAA,CACA,uC5CmX4B,CEzW1B,oBNm/JJ,CgD1/JE,gCACE,ShD4/JJ,CgDz/JE,YACE,YhD2/JJ,CgDv/JA,iBACE,yBAAA,CAAA,sBAAA,CAAA,iBAAA,CACA,cAAA,CACA,mBhD0/JF,CgDx/JE,mCACE,oBhD0/JJ,CgDt/JA,cACE,YAAA,CACA,kBAAA,CACA,oBAAA,CACA,a5CrBS,C4CsBT,oC5CupCkC,C4CtpClC,2BAAA,CACA,uCAAA,C1CVE,yCAAA,CACA,0CNogKJ,CgDx/JE,yBACE,qBAAA,CACA,kBhD0/JJ,CgDt/JA,YACE,c5C+nCkC,C4C9nClC,oBhDy/JF,CiDniKA,OACE,cAAA,CACA,KAAA,CACA,MAAA,CACA,Y7Cm4BkC,C6Cl4BlC,YAAA,CACA,UAAA,CACA,WAAA,CACA,iBAAA,CACA,eAAA,CAGA,SjDoiKF,CiD7hKA,cACE,iBAAA,CACA,UAAA,CACA,Y7CsrCkC,C6CprClC,mBjD+hKF,CiD5hKE,0B7BlBI,iC6BmBF,CACA,2BjD8hKJ,CoB9iKM,uC6BcJ,0B7BbM,epBijKN,CACF,CiDjiKE,0BACE,cjDmiKJ,CiD/hKE,kCACE,qBjDiiKJ,CiD7hKA,yBACE,wBjDgiKF,CiD9hKE,wCACE,eAAA,CACA,ejDgiKJ,CiD7hKE,qCACE,ejD+hKJ,CiD3hKA,uBACE,YAAA,CACA,kBAAA,CACA,4BjD8hKF,CiD1hKA,eACE,iBAAA,CACA,YAAA,CACA,qBAAA,CACA,UAAA,CAGA,mBAAA,CACA,qB7CpES,C6CqET,2BAAA,CACA,+BAAA,C3C3DE,mBAAA,C2C+DF,SjDyhKF,CiDrhKA,gBACE,cAAA,CACA,KAAA,CACA,MAAA,CACA,Y7CkzBkC,C6CjzBlC,WAAA,CACA,YAAA,CACA,qBjDwhKF,CiDrhKE,qBAAS,SjDwhKX,CiDvhKE,qBAAS,UjD0hKX,CiDrhKA,cACE,YAAA,CACA,aAAA,CACA,kBAAA,CACA,6BAAA,CACA,Y7C8nCkC,C6C7nClC,+BAAA,C3ChFE,wCAAA,CACA,yCNymKJ,CiDvhKE,yBACE,aAAA,CACA,gCjDyhKJ,CiDphKA,aACE,eAAA,CACA,ejDuhKF,CiDlhKA,YACE,iBAAA,CAGA,aAAA,CACA,YjDmhKF,CiD/gKA,cACE,YAAA,CACA,cAAA,CACA,aAAA,CACA,kBAAA,CACA,wBAAA,CACA,cAAA,CACA,4BAAA,C3CnGE,4CAAA,CACA,2CNsnKJ,CiD9gKE,gBACE,ajDghKJ,CYrmKI,yBqC4FF,cACE,e7CglCgC,C6C/kChC,mBjD6gKF,CiD1gKA,yBACE,0BjD6gKF,CiD1gKA,uBACE,8BjD6gKF,CiDtgKA,UAAY,ejD0gKZ,CACF,CYxnKI,yBqCiHF,oBAEE,ejD0gKF,CACF,CY9nKI,0BqCwHF,UAAY,gBjD0gKZ,CACF,CiDlgKI,kBACE,WAAA,CACA,cAAA,CACA,WAAA,CACA,QjDogKN,CiDlgKM,iCACE,WAAA,CACA,QAAA,C3CrLJ,eN0rKJ,CiDjgKM,gC3CzLF,eN6rKJ,CiDhgKM,8BACE,ejDkgKR,CiD//JM,gC3CjMF,eNmsKJ,CY1oKI,4BqCoHA,0BACE,WAAA,CACA,cAAA,CACA,WAAA,CACA,QjD0hKJ,CiDxhKI,yCACE,WAAA,CACA,QAAA,C3CrLJ,eNgtKF,CiDvhKI,wC3CzLF,eNmtKF,CiDthKI,sCACE,ejDwhKN,CiDrhKI,wC3CjMF,eNytKF,CACF,CYjqKI,4BqCoHA,0BACE,WAAA,CACA,cAAA,CACA,WAAA,CACA,QjDgjKJ,CiD9iKI,yCACE,WAAA,CACA,QAAA,C3CrLJ,eNsuKF,CiD7iKI,wC3CzLF,eNyuKF,CiD5iKI,sCACE,ejD8iKN,CiD3iKI,wC3CjMF,eN+uKF,CACF,CYvrKI,4BqCoHA,0BACE,WAAA,CACA,cAAA,CACA,WAAA,CACA,QjDskKJ,CiDpkKI,yCACE,WAAA,CACA,QAAA,C3CrLJ,eN4vKF,CiDnkKI,wC3CzLF,eN+vKF,CiDlkKI,sCACE,ejDokKN,CiDjkKI,wC3CjMF,eNqwKF,CACF,CY7sKI,6BqCoHA,0BACE,WAAA,CACA,cAAA,CACA,WAAA,CACA,QjD4lKJ,CiD1lKI,yCACE,WAAA,CACA,QAAA,C3CrLJ,eNkxKF,CiDzlKI,wC3CzLF,eNqxKF,CiDxlKI,sCACE,ejD0lKN,CiDvlKI,wC3CjMF,eN2xKF,CACF,CYnuKI,6BqCoHA,2BACE,WAAA,CACA,cAAA,CACA,WAAA,CACA,QjDknKJ,CiDhnKI,0CACE,WAAA,CACA,QAAA,C3CrLJ,eNwyKF,CiD/mKI,yC3CzLF,eN2yKF,CiD9mKI,uCACE,ejDgnKN,CiD7mKI,yC3CjMF,eNizKF,CACF,CkDp0KA,SACE,iBAAA,CACA,Y9C64BkC,C8C54BlC,aAAA,CACA,Q9CunCkC,C+C3nClC,+L/Coa4B,C+Cpa5B,qC/Coa4B,C+Cla5B,iBAAA,CACA,e/C6a4B,C+C5a5B,e/Ckb4B,C+Cjb5B,eAAA,CACA,gBAAA,CACA,oBAAA,CACA,gBAAA,CACA,mBAAA,CACA,qBAAA,CACA,iBAAA,CACA,mBAAA,CACA,kBAAA,CACA,eAAA,C9CsRI,iBALI,C6CrRR,oBAAA,CACA,SlDg1KF,CkD90KE,cAAS,UlDi1KX,CkD/0KE,wBACE,iBAAA,CACA,aAAA,CACA,W9C2mCgC,C8C1mChC,YlDi1KJ,CkD/0KI,+BACE,iBAAA,CACA,UAAA,CACA,wBAAA,CACA,kBlDi1KN,CkD50KA,6DACE,elD+0KF,CkD70KE,2FACE,QlD+0KJ,CkD70KI,yGACE,QAAA,CACA,0BAAA,CACA,qBlD+0KN,CkD10KA,+DACE,elD60KF,CkD30KE,6FACE,MAAA,CACA,W9C6kCgC,C8C5kChC,YlD60KJ,CkD30KI,2GACE,UAAA,CACA,gCAAA,CACA,uBlD60KN,CkDx0KA,mEACE,elD20KF,CkDz0KE,iGACE,KlD20KJ,CkDz0KI,+GACE,WAAA,CACA,0BAAA,CACA,wBlD20KN,CkDt0KA,gEACE,elDy0KF,CkDv0KE,8FACE,OAAA,CACA,W9C+iCgC,C8C9iChC,YlDy0KJ,CkDv0KI,4GACE,SAAA,CACA,gCAAA,CACA,sBlDy0KN,CkDpzKA,eACE,e9CygCkC,C8CxgClC,oBAAA,CACA,U9CtGS,C8CuGT,iBAAA,CACA,qB9C9FS,CECP,oBNq5KJ,CoDx6KA,SACE,iBAAA,CACA,KAAA,CACA,MAAA,CACA,YhD24BkC,CgD14BlC,aAAA,CACA,ehD6oCkC,C+ClpClC,+L/Coa4B,C+Cpa5B,qC/Coa4B,C+Cla5B,iBAAA,CACA,e/C6a4B,C+C5a5B,e/Ckb4B,C+Cjb5B,eAAA,CACA,gBAAA,CACA,oBAAA,CACA,gBAAA,CACA,mBAAA,CACA,qBAAA,CACA,iBAAA,CACA,mBAAA,CACA,kBAAA,CACA,eAAA,C9CsRI,iBALI,C+CpRR,oBAAA,CACA,qBhDLS,CgDMT,2BAAA,CACA,+BAAA,C9CIE,mBNk7KJ,CoDl7KE,wBACE,iBAAA,CACA,aAAA,CACA,UhD6oCgC,CgD5oChC,YpDo7KJ,CoDl7KI,6DAEE,iBAAA,CACA,aAAA,CACA,UAAA,CACA,wBAAA,CACA,kBpDm7KN,CoD76KE,2FACE,yBpDg7KJ,CoD96KI,yGACE,QAAA,CACA,0BAAA,CACA,gCpDg7KN,CoD76KI,uGACE,UhDyTwB,CgDxTxB,0BAAA,CACA,qBpD+6KN,CoDz6KE,6FACE,uBAAA,CACA,WhD2mCgC,CgD1mChC,WpD46KJ,CoD16KI,2GACE,MAAA,CACA,gCAAA,CACA,kCpD46KN,CoDz6KI,yGACE,QhDqSwB,CgDpSxB,gCAAA,CACA,uBpD26KN,CoDr6KE,iGACE,sBpDw6KJ,CoDt6KI,+GACE,KAAA,CACA,0BAAA,CACA,mCpDw6KN,CoDr6KI,6GACE,OhDmRwB,CgDlRxB,0BAAA,CACA,wBpDu6KN,CoDl6KE,iHACE,iBAAA,CACA,KAAA,CACA,QAAA,CACA,aAAA,CACA,UhDkkCgC,CgDjkChC,kBAAA,CACA,UAAA,CACA,+BpDo6KJ,CoD/5KE,8FACE,wBAAA,CACA,WhDyjCgC,CgDxjChC,WpDk6KJ,CoDh6KI,4GACE,OAAA,CACA,gCAAA,CACA,iCpDk6KN,CoD/5KI,0GACE,ShDmPwB,CgDlPxB,gCAAA,CACA,sBpDi6KN,CoD54KA,gBACE,kBAAA,CACA,eAAA,C/CuJI,cALI,C+C/IR,wBhDygCkC,CgDxgClC,sCAAA,C9CtHE,wCAAA,CACA,yCNqgLJ,CoD74KE,sBACE,YpD+4KJ,CoD34KA,cACE,YAAA,CACA,apD84KF,CqD7hLA,UACE,iBrDgiLF,CqD7hLA,wBACE,kBrDgiLF,CqD7hLA,gBACE,iBAAA,CACA,UAAA,CACA,erDgiLF,CsDtjLE,sBACE,aAAA,CACA,UAAA,CACA,UtDwjLJ,CqDjiLA,eACE,iBAAA,CACA,YAAA,CACA,UAAA,CACA,UAAA,CACA,kBAAA,CACA,kCAAA,CAAA,0BAAA,CjClBI,oCpBujLN,CoBnjLM,uCiCQN,ejCPQ,epBsjLN,CACF,CqDtiLA,8DAGE,arDyiLF,CqDriLA,wEAEE,0BrDyiLF,CqDtiLA,wEAEE,2BrDyiLF,CqD9hLE,8BACE,SAAA,CACA,2BAAA,CACA,crDkiLJ,CqD/hLE,iJAGE,SAAA,CACA,SrDiiLJ,CqD9hLE,oFAEE,SAAA,CACA,SAAA,CjC/DE,yBpBgmLN,CoB5lLM,uCiCwDJ,oFjCvDM,epBgmLN,CACF,CqD7hLA,8CAEE,iBAAA,CACA,KAAA,CACA,QAAA,CACA,SAAA,CAEA,YAAA,CACA,kBAAA,CACA,sBAAA,CACA,SjD2vCmC,CiD1vCnC,SAAA,CACA,UjD7FS,CiD8FT,iBAAA,CACA,eAAA,CACA,QAAA,CACA,UjDsvCmC,CgB/0C/B,4BpBynLN,CoBrnLM,uCiCqEN,8CjCpEQ,epBynLN,CACF,CqDliLE,oHAEE,UjDvGO,CiDwGP,oBAAA,CACA,SAAA,CACA,UrDqiLJ,CqDliLA,uBACE,MrDqiLF,CqDliLA,uBACE,OrDqiLF,CqDhiLA,wDAEE,oBAAA,CACA,UjD+uCmC,CiD9uCnC,WjD8uCmC,CiD7uCnC,2BAAA,CACA,uBAAA,CACA,yBrDmiLF,CqDxhLA,4BACE,+QrDmiLF,CqDjiLA,4BACE,gRrDoiLF,CqD5hLA,qBACE,iBAAA,CACA,OAAA,CACA,QAAA,CACA,MAAA,CACA,SAAA,CACA,YAAA,CACA,sBAAA,CACA,SAAA,CAEA,gBjDurCmC,CiDtrCnC,kBAAA,CACA,ejDqrCmC,CiDprCnC,erD8hLF,CqD5hLE,sCACE,sBAAA,CACA,aAAA,CACA,UjDorCiC,CiDnrCjC,UjDorCiC,CiDnrCjC,SAAA,CACA,gBjDorCiC,CiDnrCjC,ejDmrCiC,CiDlrCjC,kBAAA,CACA,cAAA,CACA,qBjD9KO,CiD+KP,2BAAA,CACA,QAAA,CAEA,iCAAA,CACA,oCAAA,CACA,UjD2qCiC,CgBv1C/B,2BpB0sLN,CoBtsLM,uCiCwJJ,sCjCvJM,epBysLN,CACF,CqD/hLE,6BACE,SrDiiLJ,CqDxhLA,kBACE,iBAAA,CACA,SAAA,CACA,cjDkqCmC,CiDjqCnC,QAAA,CACA,mBjD+pCmC,CiD9pCnC,sBjD8pCmC,CiD7pCnC,UjDzMS,CiD0MT,iBrD2hLF,CqDrhLE,sFAEE,uCjDiqCiC,CiDjqCjC,+BrDwhLJ,CqDrhLE,qDACE,qBrDuhLJ,CqDphLE,iCACE,UrDshLJ,CuDnvLA,0BACE,GAAK,uBvDuvLL,CACF,CuDpvLA,gBACE,oBAAA,CACA,UnDs3CwB,CmDr3CxB,WnDq3CwB,CmDp3CxB,sBnDs3CwB,CmDp3CxB,kBAAA,CAAA,oCAAA,CAEA,iBAAA,CACA,6CvDqvLF,CuDlvLA,mBACE,UnDi3CwB,CmDh3CxB,WnDg3CwB,CmD/2CxB,iBvDqvLF,CuD7uLA,wBACE,GACE,kBvDgvLF,CuD9uLA,IACE,SAAA,CACA,cvDgvLF,CACF,CuD5uLA,cACE,oBAAA,CACA,UnDo1CwB,CmDn1CxB,WnDm1CwB,CmDl1CxB,sBnDo1CwB,CmDn1CxB,6BAAA,CAEA,iBAAA,CACA,SAAA,CACA,2CvD6uLF,CuD1uLA,iBACE,UnD+0CwB,CmD90CxB,WvD6uLF,CuDzuLE,uCACE,8BAEE,uBvD4uLJ,CACF,CwD9yLA,WACE,cAAA,CACA,QAAA,CACA,YpD04BkC,CoDz4BlC,YAAA,CACA,qBAAA,CACA,cAAA,CAEA,iBAAA,CACA,qBpDDS,CoDET,2BAAA,CACA,SAAA,CpCKI,oCpB2yLN,CoBvyLM,uCoCpBN,WpCqBQ,epB0yLN,CACF,CwDhzLA,kBACE,YAAA,CACA,kBAAA,CACA,6BAAA,CACA,YxDmzLF,CwDjzLE,6BACE,aAAA,CACA,iBAAA,CACA,mBAAA,CACA,oBxDmzLJ,CwD/yLA,iBACE,eAAA,CACA,exDkzLF,CwD/yLA,gBACE,WAAA,CACA,YAAA,CACA,exDkzLF,CwD/yLA,iBACE,KAAA,CACA,MAAA,CACA,WpDy3CkC,CoDx3ClC,qCAAA,CACA,2BxDkzLF,CwD/yLA,eACE,KAAA,CACA,OAAA,CACA,WpDi3CkC,CoDh3ClC,oCAAA,CACA,0BxDkzLF,CwD/yLA,eACE,KAAA,CAKA,sCAAA,CACA,2BxDkzLF,CwD/yLA,iCARE,OAAA,CACA,MAAA,CACA,WpDy2CkC,CoDx2ClC,exD6zLF,CwDxzLA,kBAKE,mCAAA,CACA,0BxDkzLF,CwD/yLA,gBACE,cxDkzLF,CsD73LE,gBACE,aAAA,CACA,UAAA,CACA,UtDg4LJ,CyDp4LE,cACE,azDu4LJ,CyDp4LM,wCAEE,azDq4LR,CyD34LE,gBACE,azD84LJ,CyD34LM,4CAEE,azD44LR,CyDl5LE,cACE,azDq5LJ,CyDl5LM,wCAEE,azDm5LR,CyDz5LE,WACE,azD45LJ,CyDz5LM,kCAEE,azD05LR,CyDh6LE,cACE,azDm6LJ,CyDh6LM,wCAEE,azDi6LR,CyDv6LE,aACE,azD06LJ,CyDv6LM,sCAEE,azDw6LR,CyD96LE,YACE,azDi7LJ,CyD96LM,oCAEE,azD+6LR,CyDr7LE,WACE,azDw7LJ,CyDr7LM,kCAEE,azDs7LR,C0D37LA,OACE,iBAAA,CACA,U1D87LF,C0D57LE,cACE,aAAA,CACA,kCAAA,CACA,U1D87LJ,C0D37LE,SACE,iBAAA,CACA,KAAA,CACA,MAAA,CACA,UAAA,CACA,W1D67LJ,C0Dx7LE,WACE,sB1D27LJ,C0D57LE,WACE,qB1D+7LJ,C0Dh8LE,YACE,wB1Dm8LJ,C0Dp8LE,YACE,2B1Du8LJ,C2D59LA,WAEE,K3Dk+LF,C2D59LA,yBAPE,cAAA,CAEA,OAAA,CACA,MAAA,CACA,Y3Du+LF,C2Dp+LA,cAGE,Q3Di+LF,C2Dv9LI,YACE,uBAAA,CAAA,eAAA,CACA,KAAA,CACA,Y3D09LN,CYr7LI,yB+CxCA,eACE,uBAAA,CAAA,eAAA,CACA,KAAA,CACA,Y3Di+LJ,CACF,CY77LI,yB+CxCA,eACE,uBAAA,CAAA,eAAA,CACA,KAAA,CACA,Y3Dw+LJ,CACF,CYp8LI,yB+CxCA,eACE,uBAAA,CAAA,eAAA,CACA,KAAA,CACA,Y3D++LJ,CACF,CY38LI,0B+CxCA,eACE,uBAAA,CAAA,eAAA,CACA,KAAA,CACA,Y3Ds/LJ,CACF,CYl9LI,0B+CxCA,gBACE,uBAAA,CAAA,eAAA,CACA,KAAA,CACA,Y3D6/LJ,CACF,C4DphMA,2ECIE,2BAAA,CACA,mBAAA,CACA,oBAAA,CACA,mBAAA,CACA,qBAAA,CACA,yBAAA,CACA,4BAAA,CACA,4BAAA,CACA,kB7DohMF,C8D/hME,sBACE,iBAAA,CACA,KAAA,CACA,OAAA,CACA,QAAA,CACA,MAAA,CACA,S1D2RsC,C0D1RtC,U9DkiMJ,C+D1iMA,eCAE,eAAA,CACA,sBAAA,CACA,kBhE8iMF,CiEngMM,gBAEI,iCjEqgMV,CiEvgMM,WAEI,4BjEygMV,CiE3gMM,cAEI,+BjE6gMV,CiE/gMM,cAEI,+BjEihMV,CiEnhMM,mBAEI,oCjEqhMV,CiEvhMM,gBAEI,iCjEyhMV,CiE3hMM,aAEI,oBjE6hMV,CiE/hMM,WAEI,qBjEiiMV,CiEniMM,YAEI,oBjEqiMV,CiEviMM,eAEI,uBjEyiMV,CiE3iMM,iBAEI,yBjE6iMV,CiE/iMM,kBAEI,0BjEijMV,CiEnjMM,iBAEI,yBjEqjMV,CiEvjMM,UAEI,wBjEyjMV,CiE3jMM,gBAEI,8BjE6jMV,CiE/jMM,SAEI,uBjEikMV,CiEnkMM,QAEI,sBjEqkMV,CiEvkMM,SAEI,uBjEykMV,CiE3kMM,aAEI,2BjE6kMV,CiE/kMM,cAEI,4BjEilMV,CiEnlMM,QAEI,sBjEqlMV,CiEvlMM,eAEI,6BjEylMV,CiE3lMM,QAEI,sBjE6lMV,CiE/lMM,QAEI,iDjEimMV,CiEnmMM,WAEI,sDjEqmMV,CiEvmMM,WAEI,iDjEymMV,CiE3mMM,aAEI,yBjE6mMV,CiE/mMM,iBAEI,yBjEinMV,CiEnnMM,mBAEI,2BjEqnMV,CiEvnMM,mBAEI,2BjEynMV,CiE3nMM,gBAEI,wBjE6nMV,CiE/nMM,iBAEI,iCAAA,CAAA,yBjEioMV,CiEnoMM,OAEI,ejEqoMV,CiEvoMM,QAEI,iBjEyoMV,CiE3oMM,SAEI,kBjE6oMV,CiE/oMM,UAEI,kBjEipMV,CiEnpMM,WAEI,oBjEqpMV,CiEvpMM,YAEI,qBjEypMV,CiE3pMM,SAEI,gBjE6pMV,CiE/pMM,UAEI,kBjEiqMV,CiEnqMM,WAEI,mBjEqqMV,CiEvqMM,OAEI,iBjEyqMV,CiE3qMM,QAEI,mBjE6qMV,CiE/qMM,SAEI,oBjEirMV,CiEnrMM,kBAEI,wCjEqrMV,CiEvrMM,oBAEI,oCjEyrMV,CiE3rMM,oBAEI,oCjE6rMV,CiE/rMM,QAEI,kCjEisMV,CiEnsMM,UAEI,kBjEqsMV,CiEvsMM,YAEI,sCjEysMV,CiE3sMM,cAEI,sBjE6sMV,CiE/sMM,YAEI,wCjEitMV,CiEntMM,cAEI,wBjEqtMV,CiEvtMM,eAEI,yCjEytMV,CiE3tMM,iBAEI,yBjE6tMV,CiE/tMM,cAEI,uCjEiuMV,CiEnuMM,gBAEI,uBjEquMV,CiEvuMM,gBAEI,8BjEyuMV,CiE3uMM,kBAEI,8BjE6uMV,CiE/uMM,gBAEI,8BjEivMV,CiEnvMM,aAEI,8BjEqvMV,CiEvvMM,gBAEI,8BjEyvMV,CiE3vMM,eAEI,8BjE6vMV,CiE/vMM,cAEI,8BjEiwMV,CiEnwMM,aAEI,8BjEqwMV,CiEvwMM,cAEI,2BjEywMV,CiE3wMM,UAEI,0BjE6wMV,CiE/wMM,UAEI,0BjEixMV,CiEnxMM,UAEI,0BjEqxMV,CiEvxMM,UAEI,0BjEyxMV,CiE3xMM,UAEI,0BjE6xMV,CiE/xMM,MAEI,mBjEiyMV,CiEnyMM,MAEI,mBjEqyMV,CiEvyMM,MAEI,mBjEyyMV,CiE3yMM,OAEI,oBjE6yMV,CiE/yMM,QAEI,oBjEizMV,CiEnzMM,QAEI,wBjEqzMV,CiEvzMM,QAEI,qBjEyzMV,CiE3zMM,YAEI,yBjE6zMV,CiE/zMM,MAEI,oBjEi0MV,CiEn0MM,MAEI,oBjEq0MV,CiEv0MM,MAEI,oBjEy0MV,CiE30MM,OAEI,qBjE60MV,CiE/0MM,QAEI,qBjEi1MV,CiEn1MM,QAEI,yBjEq1MV,CiEv1MM,QAEI,sBjEy1MV,CiE31MM,YAEI,0BjE61MV,CiE/1MM,WAEI,uBjEi2MV,CiEn2MM,UAEI,4BjEq2MV,CiEv2MM,aAEI,+BjEy2MV,CiE32MM,kBAEI,oCjE62MV,CiE/2MM,qBAEI,uCjEi3MV,CiEn3MM,aAEI,qBjEq3MV,CiEv3MM,aAEI,qBjEy3MV,CiE33MM,eAEI,uBjE63MV,CiE/3MM,eAEI,uBjEi4MV,CiEn4MM,WAEI,wBjEq4MV,CiEv4MM,aAEI,0BjEy4MV,CiE34MM,mBAEI,gCjE64MV,CiE/4MM,OAEI,oBAAA,CAAA,ejEi5MV,CiEn5MM,OAEI,yBAAA,CAAA,oBjEq5MV,CiEv5MM,OAEI,wBAAA,CAAA,mBjEy5MV,CiE35MM,OAEI,uBAAA,CAAA,kBjE65MV,CiE/5MM,OAEI,yBAAA,CAAA,oBjEi6MV,CiEn6MM,OAEI,uBAAA,CAAA,kBjEq6MV,CiEv6MM,uBAEI,oCjEy6MV,CiE36MM,qBAEI,kCjE66MV,CiE/6MM,wBAEI,gCjEi7MV,CiEn7MM,yBAEI,uCjEq7MV,CiEv7MM,wBAEI,sCjEy7MV,CiE37MM,wBAEI,sCjE67MV,CiE/7MM,mBAEI,gCjEi8MV,CiEn8MM,iBAEI,8BjEq8MV,CiEv8MM,oBAEI,4BjEy8MV,CiE38MM,sBAEI,8BjE68MV,CiE/8MM,qBAEI,6BjEi9MV,CiEn9MM,qBAEI,kCjEq9MV,CiEv9MM,mBAEI,gCjEy9MV,CiE39MM,sBAEI,8BjE69MV,CiE/9MM,uBAEI,qCjEi+MV,CiEn+MM,sBAEI,oCjEq+MV,CiEv+MM,uBAEI,+BjEy+MV,CiE3+MM,iBAEI,yBjE6+MV,CiE/+MM,kBAEI,+BjEi/MV,CiEn/MM,gBAEI,6BjEq/MV,CiEv/MM,mBAEI,2BjEy/MV,CiE3/MM,qBAEI,6BjE6/MV,CiE//MM,oBAEI,4BjEigNV,CiEngNM,aAEI,kBjEqgNV,CiEvgNM,SAEI,iBjEygNV,CiE3gNM,SAEI,iBjE6gNV,CiE/gNM,SAEI,iBjEihNV,CiEnhNM,SAEI,iBjEqhNV,CiEvhNM,SAEI,iBjEyhNV,CiE3hNM,SAEI,iBjE6hNV,CiE/hNM,YAEI,iBjEiiNV,CiEniNM,KAEI,kBjEqiNV,CiEviNM,KAEI,uBjEyiNV,CiE3iNM,KAEI,sBjE6iNV,CiE/iNM,KAEI,qBjEijNV,CiEnjNM,KAEI,uBjEqjNV,CiEvjNM,KAEI,qBjEyjNV,CiE3jNM,QAEI,qBjE6jNV,CiE/jNM,MAEI,wBAAA,CAAA,uBjEkkNV,CiEpkNM,MAEI,6BAAA,CAAA,4BjEukNV,CiEzkNM,MAEI,4BAAA,CAAA,2BjE4kNV,CiE9kNM,MAEI,2BAAA,CAAA,0BjEilNV,CiEnlNM,MAEI,6BAAA,CAAA,4BjEslNV,CiExlNM,MAEI,2BAAA,CAAA,0BjE2lNV,CiE7lNM,SAEI,2BAAA,CAAA,0BjEgmNV,CiElmNM,MAEI,sBAAA,CAAA,yBjEqmNV,CiEvmNM,MAEI,2BAAA,CAAA,8BjE0mNV,CiE5mNM,MAEI,0BAAA,CAAA,6BjE+mNV,CiEjnNM,MAEI,yBAAA,CAAA,4BjEonNV,CiEtnNM,MAEI,2BAAA,CAAA,8BjEynNV,CiE3nNM,MAEI,yBAAA,CAAA,4BjE8nNV,CiEhoNM,SAEI,yBAAA,CAAA,4BjEmoNV,CiEroNM,MAEI,sBjEuoNV,CiEzoNM,MAEI,2BjE2oNV,CiE7oNM,MAEI,0BjE+oNV,CiEjpNM,MAEI,yBjEmpNV,CiErpNM,MAEI,2BjEupNV,CiEzpNM,MAEI,yBjE2pNV,CiE7pNM,SAEI,yBjE+pNV,CiEjqNM,MAEI,wBjEmqNV,CiErqNM,MAEI,6BjEuqNV,CiEzqNM,MAEI,4BjE2qNV,CiE7qNM,MAEI,2BjE+qNV,CiEjrNM,MAEI,6BjEmrNV,CiErrNM,MAEI,2BjEurNV,CiEzrNM,SAEI,2BjE2rNV,CiE7rNM,MAEI,yBjE+rNV,CiEjsNM,MAEI,8BjEmsNV,CiErsNM,MAEI,6BjEusNV,CiEzsNM,MAEI,4BjE2sNV,CiE7sNM,MAEI,8BjE+sNV,CiEjtNM,MAEI,4BjEmtNV,CiErtNM,SAEI,4BjEutNV,CiEztNM,MAEI,uBjE2tNV,CiE7tNM,MAEI,4BjE+tNV,CiEjuNM,MAEI,2BjEmuNV,CiEruNM,MAEI,0BjEuuNV,CiEzuNM,MAEI,4BjE2uNV,CiE7uNM,MAEI,0BjE+uNV,CiEjvNM,SAEI,0BjEmvNV,CiErvNM,KAEI,mBjEuvNV,CiEzvNM,KAEI,wBjE2vNV,CiE7vNM,KAEI,uBjE+vNV,CiEjwNM,KAEI,sBjEmwNV,CiErwNM,KAEI,wBjEuwNV,CiEzwNM,KAEI,sBjE2wNV,CiE7wNM,MAEI,yBAAA,CAAA,wBjEgxNV,CiElxNM,MAEI,8BAAA,CAAA,6BjEqxNV,CiEvxNM,MAEI,6BAAA,CAAA,4BjE0xNV,CiE5xNM,MAEI,4BAAA,CAAA,2BjE+xNV,CiEjyNM,MAEI,8BAAA,CAAA,6BjEoyNV,CiEtyNM,MAEI,4BAAA,CAAA,2BjEyyNV,CiE3yNM,MAEI,uBAAA,CAAA,0BjE8yNV,CiEhzNM,MAEI,4BAAA,CAAA,+BjEmzNV,CiErzNM,MAEI,2BAAA,CAAA,8BjEwzNV,CiE1zNM,MAEI,0BAAA,CAAA,6BjE6zNV,CiE/zNM,MAEI,4BAAA,CAAA,+BjEk0NV,CiEp0NM,MAEI,0BAAA,CAAA,6BjEu0NV,CiEz0NM,MAEI,uBjE20NV,CiE70NM,MAEI,4BjE+0NV,CiEj1NM,MAEI,2BjEm1NV,CiEr1NM,MAEI,0BjEu1NV,CiEz1NM,MAEI,4BjE21NV,CiE71NM,MAEI,0BjE+1NV,CiEj2NM,MAEI,yBjEm2NV,CiEr2NM,MAEI,8BjEu2NV,CiEz2NM,MAEI,6BjE22NV,CiE72NM,MAEI,4BjE+2NV,CiEj3NM,MAEI,8BjEm3NV,CiEr3NM,MAEI,4BjEu3NV,CiEz3NM,MAEI,0BjE23NV,CiE73NM,MAEI,+BjE+3NV,CiEj4NM,MAEI,8BjEm4NV,CiEr4NM,MAEI,6BjEu4NV,CiEz4NM,MAEI,+BjE24NV,CiE74NM,MAEI,6BjE+4NV,CiEj5NM,MAEI,wBjEm5NV,CiEr5NM,MAEI,6BjEu5NV,CiEz5NM,MAEI,4BjE25NV,CiE75NM,MAEI,2BjE+5NV,CiEj6NM,MAEI,6BjEm6NV,CiEr6NM,MAEI,2BjEu6NV,CiEz6NM,gBAEI,oGAAA,CAAA,8CjE26NV,CiE76NM,MAEI,0CjE+6NV,CiEj7NM,MAEI,yCjEm7NV,CiEr7NM,MAEI,uCjEu7NV,CiEz7NM,MAEI,yCjE27NV,CiE77NM,MAEI,2BjE+7NV,CiEj8NM,MAEI,wBjEm8NV,CiEr8NM,YAEI,2BjEu8NV,CiEz8NM,YAEI,2BjE28NV,CiE78NM,UAEI,yBjE+8NV,CiEj9NM,YAEI,6BjEm9NV,CiEr9NM,WAEI,yBjEu9NV,CiEz9NM,SAEI,yBjE29NV,CiE79NM,WAEI,4BjE+9NV,CiEj+NM,MAEI,uBjEm+NV,CiEr+NM,OAEI,0BjEu+NV,CiEz+NM,SAEI,yBjE2+NV,CiE7+NM,OAEI,uBjE++NV,CiEj/NM,YAEI,yBjEm/NV,CiEr/NM,UAEI,0BjEu/NV,CiEz/NM,aAEI,2BjE2/NV,CiE7/NM,sBAEI,8BjE+/NV,CiEjgOM,2BAEI,mCjEmgOV,CiErgOM,8BAEI,sCjEugOV,CiEzgOM,gBAEI,kCjE2gOV,CiE7gOM,gBAEI,kCjE+gOV,CiEjhOM,iBAEI,mCjEmhOV,CiErhOM,WAEI,4BjEuhOV,CiEzhOM,aAEI,4BjE2hOV,CiE7hOM,YAEI,8BAAA,CAAA,+BjEiiOV,CiEniOM,cAEI,uBjEsiOV,CiExiOM,gBAEI,uBjE0iOV,CiE5iOM,cAEI,uBjE8iOV,CiEhjOM,WAEI,uBjEkjOV,CiEpjOM,cAEI,uBjEsjOV,CiExjOM,aAEI,uBjE0jOV,CiE5jOM,YAEI,uBjE8jOV,CiEhkOM,WAEI,uBjEkkOV,CiEpkOM,YAEI,oBjEskOV,CiExkOM,WAEI,uBjE0kOV,CiE5kOM,YAEI,uBjE8kOV,CiEhlOM,eAEI,8BjEklOV,CiEplOM,eAEI,kCjEslOV,CiExlOM,YAEI,uBjE0lOV,CiE5lOM,YAEI,kCjE8lOV,CiEhmOM,cAEI,kCjEkmOV,CiEpmOM,YAEI,kCjEsmOV,CiExmOM,SAEI,kCjE0mOV,CiE5mOM,YAEI,kCjE8mOV,CiEhnOM,WAEI,kCjEknOV,CiEpnOM,UAEI,kCjEsnOV,CiExnOM,SAEI,kCjE0nOV,CiE5nOM,mBAEI,+BjEkoOV,CiEpoOM,gBAEI,sCjEsoOV,CiExoOM,aAEI,wFAAA,CAAA,6CjE0oOV,CiE5oOM,iBAEI,iCAAA,CAAA,6BAAA,CAAA,yBjE8oOV,CiEhpOM,kBAEI,kCAAA,CAAA,8BAAA,CAAA,0BjEkpOV,CiEppOM,kBAEI,kCAAA,CAAA,8BAAA,CAAA,0BjEspOV,CiExpOM,SAEI,6BjE0pOV,CiE5pOM,SAEI,6BjE8pOV,CiEhqOM,SAEI,8BjEkqOV,CiEpqOM,WAEI,yBjEsqOV,CiExqOM,WAEI,6BjE0qOV,CiE5qOM,WAEI,8BjE8qOV,CiEhrOM,WAEI,6BjEkrOV,CiEprOM,gBAEI,2BjEsrOV,CiExrOM,cAEI,6BjE0rOV,CiE5rOM,aAEI,uCjE+rOV,CiEjsOM,0BAEI,wCjEosOV,CiEtsOM,6BAEI,2CjEysOV,CiE3sOM,+BAEI,0CjE8sOV,CiEhtOM,eAEI,uCjE8sOV,CiEhtOM,SAEI,4BjEktOV,CiEptOM,WAEI,2BjEstOV,CY1sOI,yBqDdE,gBAEI,oBjE2tOR,CiE7tOI,cAEI,qBjE+tOR,CiEjuOI,eAEI,oBjEmuOR,CiEruOI,aAEI,wBjEuuOR,CiEzuOI,mBAEI,8BjE2uOR,CiE7uOI,YAEI,uBjE+uOR,CiEjvOI,WAEI,sBjEmvOR,CiErvOI,YAEI,uBjEuvOR,CiEzvOI,gBAEI,2BjE2vOR,CiE7vOI,iBAEI,4BjE+vOR,CiEjwOI,WAEI,sBjEmwOR,CiErwOI,kBAEI,6BjEuwOR,CiEzwOI,WAEI,sBjE2wOR,CiE7wOI,cAEI,uBjE+wOR,CiEjxOI,aAEI,4BjEmxOR,CiErxOI,gBAEI,+BjEuxOR,CiEzxOI,qBAEI,oCjE2xOR,CiE7xOI,wBAEI,uCjE+xOR,CiEjyOI,gBAEI,qBjEmyOR,CiEryOI,gBAEI,qBjEuyOR,CiEzyOI,kBAEI,uBjE2yOR,CiE7yOI,kBAEI,uBjE+yOR,CiEjzOI,cAEI,wBjEmzOR,CiErzOI,gBAEI,0BjEuzOR,CiEzzOI,sBAEI,gCjE2zOR,CiE7zOI,UAEI,oBAAA,CAAA,ejE+zOR,CiEj0OI,UAEI,yBAAA,CAAA,oBjEm0OR,CiEr0OI,UAEI,wBAAA,CAAA,mBjEu0OR,CiEz0OI,UAEI,uBAAA,CAAA,kBjE20OR,CiE70OI,UAEI,yBAAA,CAAA,oBjE+0OR,CiEj1OI,UAEI,uBAAA,CAAA,kBjEm1OR,CiEr1OI,0BAEI,oCjEu1OR,CiEz1OI,wBAEI,kCjE21OR,CiE71OI,2BAEI,gCjE+1OR,CiEj2OI,4BAEI,uCjEm2OR,CiEr2OI,2BAEI,sCjEu2OR,CiEz2OI,2BAEI,sCjE22OR,CiE72OI,sBAEI,gCjE+2OR,CiEj3OI,oBAEI,8BjEm3OR,CiEr3OI,uBAEI,4BjEu3OR,CiEz3OI,yBAEI,8BjE23OR,CiE73OI,wBAEI,6BjE+3OR,CiEj4OI,wBAEI,kCjEm4OR,CiEr4OI,sBAEI,gCjEu4OR,CiEz4OI,yBAEI,8BjE24OR,CiE74OI,0BAEI,qCjE+4OR,CiEj5OI,yBAEI,oCjEm5OR,CiEr5OI,0BAEI,+BjEu5OR,CiEz5OI,oBAEI,yBjE25OR,CiE75OI,qBAEI,+BjE+5OR,CiEj6OI,mBAEI,6BjEm6OR,CiEr6OI,sBAEI,2BjEu6OR,CiEz6OI,wBAEI,6BjE26OR,CiE76OI,uBAEI,4BjE+6OR,CiEj7OI,gBAEI,kBjEm7OR,CiEr7OI,YAEI,iBjEu7OR,CiEz7OI,YAEI,iBjE27OR,CiE77OI,YAEI,iBjE+7OR,CiEj8OI,YAEI,iBjEm8OR,CiEr8OI,YAEI,iBjEu8OR,CiEz8OI,YAEI,iBjE28OR,CiE78OI,eAEI,iBjE+8OR,CiEj9OI,QAEI,kBjEm9OR,CiEr9OI,QAEI,uBjEu9OR,CiEz9OI,QAEI,sBjE29OR,CiE79OI,QAEI,qBjE+9OR,CiEj+OI,QAEI,uBjEm+OR,CiEr+OI,QAEI,qBjEu+OR,CiEz+OI,WAEI,qBjE2+OR,CiE7+OI,SAEI,wBAAA,CAAA,uBjEg/OR,CiEl/OI,SAEI,6BAAA,CAAA,4BjEq/OR,CiEv/OI,SAEI,4BAAA,CAAA,2BjE0/OR,CiE5/OI,SAEI,2BAAA,CAAA,0BjE+/OR,CiEjgPI,SAEI,6BAAA,CAAA,4BjEogPR,CiEtgPI,SAEI,2BAAA,CAAA,0BjEygPR,CiE3gPI,YAEI,2BAAA,CAAA,0BjE8gPR,CiEhhPI,SAEI,sBAAA,CAAA,yBjEmhPR,CiErhPI,SAEI,2BAAA,CAAA,8BjEwhPR,CiE1hPI,SAEI,0BAAA,CAAA,6BjE6hPR,CiE/hPI,SAEI,yBAAA,CAAA,4BjEkiPR,CiEpiPI,SAEI,2BAAA,CAAA,8BjEuiPR,CiEziPI,SAEI,yBAAA,CAAA,4BjE4iPR,CiE9iPI,YAEI,yBAAA,CAAA,4BjEijPR,CiEnjPI,SAEI,sBjEqjPR,CiEvjPI,SAEI,2BjEyjPR,CiE3jPI,SAEI,0BjE6jPR,CiE/jPI,SAEI,yBjEikPR,CiEnkPI,SAEI,2BjEqkPR,CiEvkPI,SAEI,yBjEykPR,CiE3kPI,YAEI,yBjE6kPR,CiE/kPI,SAEI,wBjEilPR,CiEnlPI,SAEI,6BjEqlPR,CiEvlPI,SAEI,4BjEylPR,CiE3lPI,SAEI,2BjE6lPR,CiE/lPI,SAEI,6BjEimPR,CiEnmPI,SAEI,2BjEqmPR,CiEvmPI,YAEI,2BjEymPR,CiE3mPI,SAEI,yBjE6mPR,CiE/mPI,SAEI,8BjEinPR,CiEnnPI,SAEI,6BjEqnPR,CiEvnPI,SAEI,4BjEynPR,CiE3nPI,SAEI,8BjE6nPR,CiE/nPI,SAEI,4BjEioPR,CiEnoPI,YAEI,4BjEqoPR,CiEvoPI,SAEI,uBjEyoPR,CiE3oPI,SAEI,4BjE6oPR,CiE/oPI,SAEI,2BjEipPR,CiEnpPI,SAEI,0BjEqpPR,CiEvpPI,SAEI,4BjEypPR,CiE3pPI,SAEI,0BjE6pPR,CiE/pPI,YAEI,0BjEiqPR,CiEnqPI,QAEI,mBjEqqPR,CiEvqPI,QAEI,wBjEyqPR,CiE3qPI,QAEI,uBjE6qPR,CiE/qPI,QAEI,sBjEirPR,CiEnrPI,QAEI,wBjEqrPR,CiEvrPI,QAEI,sBjEyrPR,CiE3rPI,SAEI,yBAAA,CAAA,wBjE8rPR,CiEhsPI,SAEI,8BAAA,CAAA,6BjEmsPR,CiErsPI,SAEI,6BAAA,CAAA,4BjEwsPR,CiE1sPI,SAEI,4BAAA,CAAA,2BjE6sPR,CiE/sPI,SAEI,8BAAA,CAAA,6BjEktPR,CiEptPI,SAEI,4BAAA,CAAA,2BjEutPR,CiEztPI,SAEI,uBAAA,CAAA,0BjE4tPR,CiE9tPI,SAEI,4BAAA,CAAA,+BjEiuPR,CiEnuPI,SAEI,2BAAA,CAAA,8BjEsuPR,CiExuPI,SAEI,0BAAA,CAAA,6BjE2uPR,CiE7uPI,SAEI,4BAAA,CAAA,+BjEgvPR,CiElvPI,SAEI,0BAAA,CAAA,6BjEqvPR,CiEvvPI,SAEI,uBjEyvPR,CiE3vPI,SAEI,4BjE6vPR,CiE/vPI,SAEI,2BjEiwPR,CiEnwPI,SAEI,0BjEqwPR,CiEvwPI,SAEI,4BjEywPR,CiE3wPI,SAEI,0BjE6wPR,CiE/wPI,SAEI,yBjEixPR,CiEnxPI,SAEI,8BjEqxPR,CiEvxPI,SAEI,6BjEyxPR,CiE3xPI,SAEI,4BjE6xPR,CiE/xPI,SAEI,8BjEiyPR,CiEnyPI,SAEI,4BjEqyPR,CiEvyPI,SAEI,0BjEyyPR,CiE3yPI,SAEI,+BjE6yPR,CiE/yPI,SAEI,8BjEizPR,CiEnzPI,SAEI,6BjEqzPR,CiEvzPI,SAEI,+BjEyzPR,CiE3zPI,SAEI,6BjE6zPR,CiE/zPI,SAEI,wBjEi0PR,CiEn0PI,SAEI,6BjEq0PR,CiEv0PI,SAEI,4BjEy0PR,CiE30PI,SAEI,2BjE60PR,CiE/0PI,SAEI,6BjEi1PR,CiEn1PI,SAEI,2BjEq1PR,CiEv1PI,eAEI,yBjEy1PR,CiE31PI,aAEI,0BjE61PR,CiE/1PI,gBAEI,2BjEi2PR,CACF,CYt1PI,yBqDdE,gBAEI,oBjEs2PR,CiEx2PI,cAEI,qBjE02PR,CiE52PI,eAEI,oBjE82PR,CiEh3PI,aAEI,wBjEk3PR,CiEp3PI,mBAEI,8BjEs3PR,CiEx3PI,YAEI,uBjE03PR,CiE53PI,WAEI,sBjE83PR,CiEh4PI,YAEI,uBjEk4PR,CiEp4PI,gBAEI,2BjEs4PR,CiEx4PI,iBAEI,4BjE04PR,CiE54PI,WAEI,sBjE84PR,CiEh5PI,kBAEI,6BjEk5PR,CiEp5PI,WAEI,sBjEs5PR,CiEx5PI,cAEI,uBjE05PR,CiE55PI,aAEI,4BjE85PR,CiEh6PI,gBAEI,+BjEk6PR,CiEp6PI,qBAEI,oCjEs6PR,CiEx6PI,wBAEI,uCjE06PR,CiE56PI,gBAEI,qBjE86PR,CiEh7PI,gBAEI,qBjEk7PR,CiEp7PI,kBAEI,uBjEs7PR,CiEx7PI,kBAEI,uBjE07PR,CiE57PI,cAEI,wBjE87PR,CiEh8PI,gBAEI,0BjEk8PR,CiEp8PI,sBAEI,gCjEs8PR,CiEx8PI,UAEI,oBAAA,CAAA,ejE08PR,CiE58PI,UAEI,yBAAA,CAAA,oBjE88PR,CiEh9PI,UAEI,wBAAA,CAAA,mBjEk9PR,CiEp9PI,UAEI,uBAAA,CAAA,kBjEs9PR,CiEx9PI,UAEI,yBAAA,CAAA,oBjE09PR,CiE59PI,UAEI,uBAAA,CAAA,kBjE89PR,CiEh+PI,0BAEI,oCjEk+PR,CiEp+PI,wBAEI,kCjEs+PR,CiEx+PI,2BAEI,gCjE0+PR,CiE5+PI,4BAEI,uCjE8+PR,CiEh/PI,2BAEI,sCjEk/PR,CiEp/PI,2BAEI,sCjEs/PR,CiEx/PI,sBAEI,gCjE0/PR,CiE5/PI,oBAEI,8BjE8/PR,CiEhgQI,uBAEI,4BjEkgQR,CiEpgQI,yBAEI,8BjEsgQR,CiExgQI,wBAEI,6BjE0gQR,CiE5gQI,wBAEI,kCjE8gQR,CiEhhQI,sBAEI,gCjEkhQR,CiEphQI,yBAEI,8BjEshQR,CiExhQI,0BAEI,qCjE0hQR,CiE5hQI,yBAEI,oCjE8hQR,CiEhiQI,0BAEI,+BjEkiQR,CiEpiQI,oBAEI,yBjEsiQR,CiExiQI,qBAEI,+BjE0iQR,CiE5iQI,mBAEI,6BjE8iQR,CiEhjQI,sBAEI,2BjEkjQR,CiEpjQI,wBAEI,6BjEsjQR,CiExjQI,uBAEI,4BjE0jQR,CiE5jQI,gBAEI,kBjE8jQR,CiEhkQI,YAEI,iBjEkkQR,CiEpkQI,YAEI,iBjEskQR,CiExkQI,YAEI,iBjE0kQR,CiE5kQI,YAEI,iBjE8kQR,CiEhlQI,YAEI,iBjEklQR,CiEplQI,YAEI,iBjEslQR,CiExlQI,eAEI,iBjE0lQR,CiE5lQI,QAEI,kBjE8lQR,CiEhmQI,QAEI,uBjEkmQR,CiEpmQI,QAEI,sBjEsmQR,CiExmQI,QAEI,qBjE0mQR,CiE5mQI,QAEI,uBjE8mQR,CiEhnQI,QAEI,qBjEknQR,CiEpnQI,WAEI,qBjEsnQR,CiExnQI,SAEI,wBAAA,CAAA,uBjE2nQR,CiE7nQI,SAEI,6BAAA,CAAA,4BjEgoQR,CiEloQI,SAEI,4BAAA,CAAA,2BjEqoQR,CiEvoQI,SAEI,2BAAA,CAAA,0BjE0oQR,CiE5oQI,SAEI,6BAAA,CAAA,4BjE+oQR,CiEjpQI,SAEI,2BAAA,CAAA,0BjEopQR,CiEtpQI,YAEI,2BAAA,CAAA,0BjEypQR,CiE3pQI,SAEI,sBAAA,CAAA,yBjE8pQR,CiEhqQI,SAEI,2BAAA,CAAA,8BjEmqQR,CiErqQI,SAEI,0BAAA,CAAA,6BjEwqQR,CiE1qQI,SAEI,yBAAA,CAAA,4BjE6qQR,CiE/qQI,SAEI,2BAAA,CAAA,8BjEkrQR,CiEprQI,SAEI,yBAAA,CAAA,4BjEurQR,CiEzrQI,YAEI,yBAAA,CAAA,4BjE4rQR,CiE9rQI,SAEI,sBjEgsQR,CiElsQI,SAEI,2BjEosQR,CiEtsQI,SAEI,0BjEwsQR,CiE1sQI,SAEI,yBjE4sQR,CiE9sQI,SAEI,2BjEgtQR,CiEltQI,SAEI,yBjEotQR,CiEttQI,YAEI,yBjEwtQR,CiE1tQI,SAEI,wBjE4tQR,CiE9tQI,SAEI,6BjEguQR,CiEluQI,SAEI,4BjEouQR,CiEtuQI,SAEI,2BjEwuQR,CiE1uQI,SAEI,6BjE4uQR,CiE9uQI,SAEI,2BjEgvQR,CiElvQI,YAEI,2BjEovQR,CiEtvQI,SAEI,yBjEwvQR,CiE1vQI,SAEI,8BjE4vQR,CiE9vQI,SAEI,6BjEgwQR,CiElwQI,SAEI,4BjEowQR,CiEtwQI,SAEI,8BjEwwQR,CiE1wQI,SAEI,4BjE4wQR,CiE9wQI,YAEI,4BjEgxQR,CiElxQI,SAEI,uBjEoxQR,CiEtxQI,SAEI,4BjEwxQR,CiE1xQI,SAEI,2BjE4xQR,CiE9xQI,SAEI,0BjEgyQR,CiElyQI,SAEI,4BjEoyQR,CiEtyQI,SAEI,0BjEwyQR,CiE1yQI,YAEI,0BjE4yQR,CiE9yQI,QAEI,mBjEgzQR,CiElzQI,QAEI,wBjEozQR,CiEtzQI,QAEI,uBjEwzQR,CiE1zQI,QAEI,sBjE4zQR,CiE9zQI,QAEI,wBjEg0QR,CiEl0QI,QAEI,sBjEo0QR,CiEt0QI,SAEI,yBAAA,CAAA,wBjEy0QR,CiE30QI,SAEI,8BAAA,CAAA,6BjE80QR,CiEh1QI,SAEI,6BAAA,CAAA,4BjEm1QR,CiEr1QI,SAEI,4BAAA,CAAA,2BjEw1QR,CiE11QI,SAEI,8BAAA,CAAA,6BjE61QR,CiE/1QI,SAEI,4BAAA,CAAA,2BjEk2QR,CiEp2QI,SAEI,uBAAA,CAAA,0BjEu2QR,CiEz2QI,SAEI,4BAAA,CAAA,+BjE42QR,CiE92QI,SAEI,2BAAA,CAAA,8BjEi3QR,CiEn3QI,SAEI,0BAAA,CAAA,6BjEs3QR,CiEx3QI,SAEI,4BAAA,CAAA,+BjE23QR,CiE73QI,SAEI,0BAAA,CAAA,6BjEg4QR,CiEl4QI,SAEI,uBjEo4QR,CiEt4QI,SAEI,4BjEw4QR,CiE14QI,SAEI,2BjE44QR,CiE94QI,SAEI,0BjEg5QR,CiEl5QI,SAEI,4BjEo5QR,CiEt5QI,SAEI,0BjEw5QR,CiE15QI,SAEI,yBjE45QR,CiE95QI,SAEI,8BjEg6QR,CiEl6QI,SAEI,6BjEo6QR,CiEt6QI,SAEI,4BjEw6QR,CiE16QI,SAEI,8BjE46QR,CiE96QI,SAEI,4BjEg7QR,CiEl7QI,SAEI,0BjEo7QR,CiEt7QI,SAEI,+BjEw7QR,CiE17QI,SAEI,8BjE47QR,CiE97QI,SAEI,6BjEg8QR,CiEl8QI,SAEI,+BjEo8QR,CiEt8QI,SAEI,6BjEw8QR,CiE18QI,SAEI,wBjE48QR,CiE98QI,SAEI,6BjEg9QR,CiEl9QI,SAEI,4BjEo9QR,CiEt9QI,SAEI,2BjEw9QR,CiE19QI,SAEI,6BjE49QR,CiE99QI,SAEI,2BjEg+QR,CiEl+QI,eAEI,yBjEo+QR,CiEt+QI,aAEI,0BjEw+QR,CiE1+QI,gBAEI,2BjE4+QR,CACF,CYj+QI,yBqDdE,gBAEI,oBjEi/QR,CiEn/QI,cAEI,qBjEq/QR,CiEv/QI,eAEI,oBjEy/QR,CiE3/QI,aAEI,wBjE6/QR,CiE//QI,mBAEI,8BjEigRR,CiEngRI,YAEI,uBjEqgRR,CiEvgRI,WAEI,sBjEygRR,CiE3gRI,YAEI,uBjE6gRR,CiE/gRI,gBAEI,2BjEihRR,CiEnhRI,iBAEI,4BjEqhRR,CiEvhRI,WAEI,sBjEyhRR,CiE3hRI,kBAEI,6BjE6hRR,CiE/hRI,WAEI,sBjEiiRR,CiEniRI,cAEI,uBjEqiRR,CiEviRI,aAEI,4BjEyiRR,CiE3iRI,gBAEI,+BjE6iRR,CiE/iRI,qBAEI,oCjEijRR,CiEnjRI,wBAEI,uCjEqjRR,CiEvjRI,gBAEI,qBjEyjRR,CiE3jRI,gBAEI,qBjE6jRR,CiE/jRI,kBAEI,uBjEikRR,CiEnkRI,kBAEI,uBjEqkRR,CiEvkRI,cAEI,wBjEykRR,CiE3kRI,gBAEI,0BjE6kRR,CiE/kRI,sBAEI,gCjEilRR,CiEnlRI,UAEI,oBAAA,CAAA,ejEqlRR,CiEvlRI,UAEI,yBAAA,CAAA,oBjEylRR,CiE3lRI,UAEI,wBAAA,CAAA,mBjE6lRR,CiE/lRI,UAEI,uBAAA,CAAA,kBjEimRR,CiEnmRI,UAEI,yBAAA,CAAA,oBjEqmRR,CiEvmRI,UAEI,uBAAA,CAAA,kBjEymRR,CiE3mRI,0BAEI,oCjE6mRR,CiE/mRI,wBAEI,kCjEinRR,CiEnnRI,2BAEI,gCjEqnRR,CiEvnRI,4BAEI,uCjEynRR,CiE3nRI,2BAEI,sCjE6nRR,CiE/nRI,2BAEI,sCjEioRR,CiEnoRI,sBAEI,gCjEqoRR,CiEvoRI,oBAEI,8BjEyoRR,CiE3oRI,uBAEI,4BjE6oRR,CiE/oRI,yBAEI,8BjEipRR,CiEnpRI,wBAEI,6BjEqpRR,CiEvpRI,wBAEI,kCjEypRR,CiE3pRI,sBAEI,gCjE6pRR,CiE/pRI,yBAEI,8BjEiqRR,CiEnqRI,0BAEI,qCjEqqRR,CiEvqRI,yBAEI,oCjEyqRR,CiE3qRI,0BAEI,+BjE6qRR,CiE/qRI,oBAEI,yBjEirRR,CiEnrRI,qBAEI,+BjEqrRR,CiEvrRI,mBAEI,6BjEyrRR,CiE3rRI,sBAEI,2BjE6rRR,CiE/rRI,wBAEI,6BjEisRR,CiEnsRI,uBAEI,4BjEqsRR,CiEvsRI,gBAEI,kBjEysRR,CiE3sRI,YAEI,iBjE6sRR,CiE/sRI,YAEI,iBjEitRR,CiEntRI,YAEI,iBjEqtRR,CiEvtRI,YAEI,iBjEytRR,CiE3tRI,YAEI,iBjE6tRR,CiE/tRI,YAEI,iBjEiuRR,CiEnuRI,eAEI,iBjEquRR,CiEvuRI,QAEI,kBjEyuRR,CiE3uRI,QAEI,uBjE6uRR,CiE/uRI,QAEI,sBjEivRR,CiEnvRI,QAEI,qBjEqvRR,CiEvvRI,QAEI,uBjEyvRR,CiE3vRI,QAEI,qBjE6vRR,CiE/vRI,WAEI,qBjEiwRR,CiEnwRI,SAEI,wBAAA,CAAA,uBjEswRR,CiExwRI,SAEI,6BAAA,CAAA,4BjE2wRR,CiE7wRI,SAEI,4BAAA,CAAA,2BjEgxRR,CiElxRI,SAEI,2BAAA,CAAA,0BjEqxRR,CiEvxRI,SAEI,6BAAA,CAAA,4BjE0xRR,CiE5xRI,SAEI,2BAAA,CAAA,0BjE+xRR,CiEjyRI,YAEI,2BAAA,CAAA,0BjEoyRR,CiEtyRI,SAEI,sBAAA,CAAA,yBjEyyRR,CiE3yRI,SAEI,2BAAA,CAAA,8BjE8yRR,CiEhzRI,SAEI,0BAAA,CAAA,6BjEmzRR,CiErzRI,SAEI,yBAAA,CAAA,4BjEwzRR,CiE1zRI,SAEI,2BAAA,CAAA,8BjE6zRR,CiE/zRI,SAEI,yBAAA,CAAA,4BjEk0RR,CiEp0RI,YAEI,yBAAA,CAAA,4BjEu0RR,CiEz0RI,SAEI,sBjE20RR,CiE70RI,SAEI,2BjE+0RR,CiEj1RI,SAEI,0BjEm1RR,CiEr1RI,SAEI,yBjEu1RR,CiEz1RI,SAEI,2BjE21RR,CiE71RI,SAEI,yBjE+1RR,CiEj2RI,YAEI,yBjEm2RR,CiEr2RI,SAEI,wBjEu2RR,CiEz2RI,SAEI,6BjE22RR,CiE72RI,SAEI,4BjE+2RR,CiEj3RI,SAEI,2BjEm3RR,CiEr3RI,SAEI,6BjEu3RR,CiEz3RI,SAEI,2BjE23RR,CiE73RI,YAEI,2BjE+3RR,CiEj4RI,SAEI,yBjEm4RR,CiEr4RI,SAEI,8BjEu4RR,CiEz4RI,SAEI,6BjE24RR,CiE74RI,SAEI,4BjE+4RR,CiEj5RI,SAEI,8BjEm5RR,CiEr5RI,SAEI,4BjEu5RR,CiEz5RI,YAEI,4BjE25RR,CiE75RI,SAEI,uBjE+5RR,CiEj6RI,SAEI,4BjEm6RR,CiEr6RI,SAEI,2BjEu6RR,CiEz6RI,SAEI,0BjE26RR,CiE76RI,SAEI,4BjE+6RR,CiEj7RI,SAEI,0BjEm7RR,CiEr7RI,YAEI,0BjEu7RR,CiEz7RI,QAEI,mBjE27RR,CiE77RI,QAEI,wBjE+7RR,CiEj8RI,QAEI,uBjEm8RR,CiEr8RI,QAEI,sBjEu8RR,CiEz8RI,QAEI,wBjE28RR,CiE78RI,QAEI,sBjE+8RR,CiEj9RI,SAEI,yBAAA,CAAA,wBjEo9RR,CiEt9RI,SAEI,8BAAA,CAAA,6BjEy9RR,CiE39RI,SAEI,6BAAA,CAAA,4BjE89RR,CiEh+RI,SAEI,4BAAA,CAAA,2BjEm+RR,CiEr+RI,SAEI,8BAAA,CAAA,6BjEw+RR,CiE1+RI,SAEI,4BAAA,CAAA,2BjE6+RR,CiE/+RI,SAEI,uBAAA,CAAA,0BjEk/RR,CiEp/RI,SAEI,4BAAA,CAAA,+BjEu/RR,CiEz/RI,SAEI,2BAAA,CAAA,8BjE4/RR,CiE9/RI,SAEI,0BAAA,CAAA,6BjEigSR,CiEngSI,SAEI,4BAAA,CAAA,+BjEsgSR,CiExgSI,SAEI,0BAAA,CAAA,6BjE2gSR,CiE7gSI,SAEI,uBjE+gSR,CiEjhSI,SAEI,4BjEmhSR,CiErhSI,SAEI,2BjEuhSR,CiEzhSI,SAEI,0BjE2hSR,CiE7hSI,SAEI,4BjE+hSR,CiEjiSI,SAEI,0BjEmiSR,CiEriSI,SAEI,yBjEuiSR,CiEziSI,SAEI,8BjE2iSR,CiE7iSI,SAEI,6BjE+iSR,CiEjjSI,SAEI,4BjEmjSR,CiErjSI,SAEI,8BjEujSR,CiEzjSI,SAEI,4BjE2jSR,CiE7jSI,SAEI,0BjE+jSR,CiEjkSI,SAEI,+BjEmkSR,CiErkSI,SAEI,8BjEukSR,CiEzkSI,SAEI,6BjE2kSR,CiE7kSI,SAEI,+BjE+kSR,CiEjlSI,SAEI,6BjEmlSR,CiErlSI,SAEI,wBjEulSR,CiEzlSI,SAEI,6BjE2lSR,CiE7lSI,SAEI,4BjE+lSR,CiEjmSI,SAEI,2BjEmmSR,CiErmSI,SAEI,6BjEumSR,CiEzmSI,SAEI,2BjE2mSR,CiE7mSI,eAEI,yBjE+mSR,CiEjnSI,aAEI,0BjEmnSR,CiErnSI,gBAEI,2BjEunSR,CACF,CY5mSI,0BqDdE,gBAEI,oBjE4nSR,CiE9nSI,cAEI,qBjEgoSR,CiEloSI,eAEI,oBjEooSR,CiEtoSI,aAEI,wBjEwoSR,CiE1oSI,mBAEI,8BjE4oSR,CiE9oSI,YAEI,uBjEgpSR,CiElpSI,WAEI,sBjEopSR,CiEtpSI,YAEI,uBjEwpSR,CiE1pSI,gBAEI,2BjE4pSR,CiE9pSI,iBAEI,4BjEgqSR,CiElqSI,WAEI,sBjEoqSR,CiEtqSI,kBAEI,6BjEwqSR,CiE1qSI,WAEI,sBjE4qSR,CiE9qSI,cAEI,uBjEgrSR,CiElrSI,aAEI,4BjEorSR,CiEtrSI,gBAEI,+BjEwrSR,CiE1rSI,qBAEI,oCjE4rSR,CiE9rSI,wBAEI,uCjEgsSR,CiElsSI,gBAEI,qBjEosSR,CiEtsSI,gBAEI,qBjEwsSR,CiE1sSI,kBAEI,uBjE4sSR,CiE9sSI,kBAEI,uBjEgtSR,CiEltSI,cAEI,wBjEotSR,CiEttSI,gBAEI,0BjEwtSR,CiE1tSI,sBAEI,gCjE4tSR,CiE9tSI,UAEI,oBAAA,CAAA,ejEguSR,CiEluSI,UAEI,yBAAA,CAAA,oBjEouSR,CiEtuSI,UAEI,wBAAA,CAAA,mBjEwuSR,CiE1uSI,UAEI,uBAAA,CAAA,kBjE4uSR,CiE9uSI,UAEI,yBAAA,CAAA,oBjEgvSR,CiElvSI,UAEI,uBAAA,CAAA,kBjEovSR,CiEtvSI,0BAEI,oCjEwvSR,CiE1vSI,wBAEI,kCjE4vSR,CiE9vSI,2BAEI,gCjEgwSR,CiElwSI,4BAEI,uCjEowSR,CiEtwSI,2BAEI,sCjEwwSR,CiE1wSI,2BAEI,sCjE4wSR,CiE9wSI,sBAEI,gCjEgxSR,CiElxSI,oBAEI,8BjEoxSR,CiEtxSI,uBAEI,4BjEwxSR,CiE1xSI,yBAEI,8BjE4xSR,CiE9xSI,wBAEI,6BjEgySR,CiElySI,wBAEI,kCjEoySR,CiEtySI,sBAEI,gCjEwySR,CiE1ySI,yBAEI,8BjE4ySR,CiE9ySI,0BAEI,qCjEgzSR,CiElzSI,yBAEI,oCjEozSR,CiEtzSI,0BAEI,+BjEwzSR,CiE1zSI,oBAEI,yBjE4zSR,CiE9zSI,qBAEI,+BjEg0SR,CiEl0SI,mBAEI,6BjEo0SR,CiEt0SI,sBAEI,2BjEw0SR,CiE10SI,wBAEI,6BjE40SR,CiE90SI,uBAEI,4BjEg1SR,CiEl1SI,gBAEI,kBjEo1SR,CiEt1SI,YAEI,iBjEw1SR,CiE11SI,YAEI,iBjE41SR,CiE91SI,YAEI,iBjEg2SR,CiEl2SI,YAEI,iBjEo2SR,CiEt2SI,YAEI,iBjEw2SR,CiE12SI,YAEI,iBjE42SR,CiE92SI,eAEI,iBjEg3SR,CiEl3SI,QAEI,kBjEo3SR,CiEt3SI,QAEI,uBjEw3SR,CiE13SI,QAEI,sBjE43SR,CiE93SI,QAEI,qBjEg4SR,CiEl4SI,QAEI,uBjEo4SR,CiEt4SI,QAEI,qBjEw4SR,CiE14SI,WAEI,qBjE44SR,CiE94SI,SAEI,wBAAA,CAAA,uBjEi5SR,CiEn5SI,SAEI,6BAAA,CAAA,4BjEs5SR,CiEx5SI,SAEI,4BAAA,CAAA,2BjE25SR,CiE75SI,SAEI,2BAAA,CAAA,0BjEg6SR,CiEl6SI,SAEI,6BAAA,CAAA,4BjEq6SR,CiEv6SI,SAEI,2BAAA,CAAA,0BjE06SR,CiE56SI,YAEI,2BAAA,CAAA,0BjE+6SR,CiEj7SI,SAEI,sBAAA,CAAA,yBjEo7SR,CiEt7SI,SAEI,2BAAA,CAAA,8BjEy7SR,CiE37SI,SAEI,0BAAA,CAAA,6BjE87SR,CiEh8SI,SAEI,yBAAA,CAAA,4BjEm8SR,CiEr8SI,SAEI,2BAAA,CAAA,8BjEw8SR,CiE18SI,SAEI,yBAAA,CAAA,4BjE68SR,CiE/8SI,YAEI,yBAAA,CAAA,4BjEk9SR,CiEp9SI,SAEI,sBjEs9SR,CiEx9SI,SAEI,2BjE09SR,CiE59SI,SAEI,0BjE89SR,CiEh+SI,SAEI,yBjEk+SR,CiEp+SI,SAEI,2BjEs+SR,CiEx+SI,SAEI,yBjE0+SR,CiE5+SI,YAEI,yBjE8+SR,CiEh/SI,SAEI,wBjEk/SR,CiEp/SI,SAEI,6BjEs/SR,CiEx/SI,SAEI,4BjE0/SR,CiE5/SI,SAEI,2BjE8/SR,CiEhgTI,SAEI,6BjEkgTR,CiEpgTI,SAEI,2BjEsgTR,CiExgTI,YAEI,2BjE0gTR,CiE5gTI,SAEI,yBjE8gTR,CiEhhTI,SAEI,8BjEkhTR,CiEphTI,SAEI,6BjEshTR,CiExhTI,SAEI,4BjE0hTR,CiE5hTI,SAEI,8BjE8hTR,CiEhiTI,SAEI,4BjEkiTR,CiEpiTI,YAEI,4BjEsiTR,CiExiTI,SAEI,uBjE0iTR,CiE5iTI,SAEI,4BjE8iTR,CiEhjTI,SAEI,2BjEkjTR,CiEpjTI,SAEI,0BjEsjTR,CiExjTI,SAEI,4BjE0jTR,CiE5jTI,SAEI,0BjE8jTR,CiEhkTI,YAEI,0BjEkkTR,CiEpkTI,QAEI,mBjEskTR,CiExkTI,QAEI,wBjE0kTR,CiE5kTI,QAEI,uBjE8kTR,CiEhlTI,QAEI,sBjEklTR,CiEplTI,QAEI,wBjEslTR,CiExlTI,QAEI,sBjE0lTR,CiE5lTI,SAEI,yBAAA,CAAA,wBjE+lTR,CiEjmTI,SAEI,8BAAA,CAAA,6BjEomTR,CiEtmTI,SAEI,6BAAA,CAAA,4BjEymTR,CiE3mTI,SAEI,4BAAA,CAAA,2BjE8mTR,CiEhnTI,SAEI,8BAAA,CAAA,6BjEmnTR,CiErnTI,SAEI,4BAAA,CAAA,2BjEwnTR,CiE1nTI,SAEI,uBAAA,CAAA,0BjE6nTR,CiE/nTI,SAEI,4BAAA,CAAA,+BjEkoTR,CiEpoTI,SAEI,2BAAA,CAAA,8BjEuoTR,CiEzoTI,SAEI,0BAAA,CAAA,6BjE4oTR,CiE9oTI,SAEI,4BAAA,CAAA,+BjEipTR,CiEnpTI,SAEI,0BAAA,CAAA,6BjEspTR,CiExpTI,SAEI,uBjE0pTR,CiE5pTI,SAEI,4BjE8pTR,CiEhqTI,SAEI,2BjEkqTR,CiEpqTI,SAEI,0BjEsqTR,CiExqTI,SAEI,4BjE0qTR,CiE5qTI,SAEI,0BjE8qTR,CiEhrTI,SAEI,yBjEkrTR,CiEprTI,SAEI,8BjEsrTR,CiExrTI,SAEI,6BjE0rTR,CiE5rTI,SAEI,4BjE8rTR,CiEhsTI,SAEI,8BjEksTR,CiEpsTI,SAEI,4BjEssTR,CiExsTI,SAEI,0BjE0sTR,CiE5sTI,SAEI,+BjE8sTR,CiEhtTI,SAEI,8BjEktTR,CiEptTI,SAEI,6BjEstTR,CiExtTI,SAEI,+BjE0tTR,CiE5tTI,SAEI,6BjE8tTR,CiEhuTI,SAEI,wBjEkuTR,CiEpuTI,SAEI,6BjEsuTR,CiExuTI,SAEI,4BjE0uTR,CiE5uTI,SAEI,2BjE8uTR,CiEhvTI,SAEI,6BjEkvTR,CiEpvTI,SAEI,2BjEsvTR,CiExvTI,eAEI,yBjE0vTR,CiE5vTI,aAEI,0BjE8vTR,CiEhwTI,gBAEI,2BjEkwTR,CACF,CYvvTI,0BqDdE,iBAEI,oBjEuwTR,CiEzwTI,eAEI,qBjE2wTR,CiE7wTI,gBAEI,oBjE+wTR,CiEjxTI,cAEI,wBjEmxTR,CiErxTI,oBAEI,8BjEuxTR,CiEzxTI,aAEI,uBjE2xTR,CiE7xTI,YAEI,sBjE+xTR,CiEjyTI,aAEI,uBjEmyTR,CiEryTI,iBAEI,2BjEuyTR,CiEzyTI,kBAEI,4BjE2yTR,CiE7yTI,YAEI,sBjE+yTR,CiEjzTI,mBAEI,6BjEmzTR,CiErzTI,YAEI,sBjEuzTR,CiEzzTI,eAEI,uBjE2zTR,CiE7zTI,cAEI,4BjE+zTR,CiEj0TI,iBAEI,+BjEm0TR,CiEr0TI,sBAEI,oCjEu0TR,CiEz0TI,yBAEI,uCjE20TR,CiE70TI,iBAEI,qBjE+0TR,CiEj1TI,iBAEI,qBjEm1TR,CiEr1TI,mBAEI,uBjEu1TR,CiEz1TI,mBAEI,uBjE21TR,CiE71TI,eAEI,wBjE+1TR,CiEj2TI,iBAEI,0BjEm2TR,CiEr2TI,uBAEI,gCjEu2TR,CiEz2TI,WAEI,oBAAA,CAAA,ejE22TR,CiE72TI,WAEI,yBAAA,CAAA,oBjE+2TR,CiEj3TI,WAEI,wBAAA,CAAA,mBjEm3TR,CiEr3TI,WAEI,uBAAA,CAAA,kBjEu3TR,CiEz3TI,WAEI,yBAAA,CAAA,oBjE23TR,CiE73TI,WAEI,uBAAA,CAAA,kBjE+3TR,CiEj4TI,2BAEI,oCjEm4TR,CiEr4TI,yBAEI,kCjEu4TR,CiEz4TI,4BAEI,gCjE24TR,CiE74TI,6BAEI,uCjE+4TR,CiEj5TI,4BAEI,sCjEm5TR,CiEr5TI,4BAEI,sCjEu5TR,CiEz5TI,uBAEI,gCjE25TR,CiE75TI,qBAEI,8BjE+5TR,CiEj6TI,wBAEI,4BjEm6TR,CiEr6TI,0BAEI,8BjEu6TR,CiEz6TI,yBAEI,6BjE26TR,CiE76TI,yBAEI,kCjE+6TR,CiEj7TI,uBAEI,gCjEm7TR,CiEr7TI,0BAEI,8BjEu7TR,CiEz7TI,2BAEI,qCjE27TR,CiE77TI,0BAEI,oCjE+7TR,CiEj8TI,2BAEI,+BjEm8TR,CiEr8TI,qBAEI,yBjEu8TR,CiEz8TI,sBAEI,+BjE28TR,CiE78TI,oBAEI,6BjE+8TR,CiEj9TI,uBAEI,2BjEm9TR,CiEr9TI,yBAEI,6BjEu9TR,CiEz9TI,wBAEI,4BjE29TR,CiE79TI,iBAEI,kBjE+9TR,CiEj+TI,aAEI,iBjEm+TR,CiEr+TI,aAEI,iBjEu+TR,CiEz+TI,aAEI,iBjE2+TR,CiE7+TI,aAEI,iBjE++TR,CiEj/TI,aAEI,iBjEm/TR,CiEr/TI,aAEI,iBjEu/TR,CiEz/TI,gBAEI,iBjE2/TR,CiE7/TI,SAEI,kBjE+/TR,CiEjgUI,SAEI,uBjEmgUR,CiErgUI,SAEI,sBjEugUR,CiEzgUI,SAEI,qBjE2gUR,CiE7gUI,SAEI,uBjE+gUR,CiEjhUI,SAEI,qBjEmhUR,CiErhUI,YAEI,qBjEuhUR,CiEzhUI,UAEI,wBAAA,CAAA,uBjE4hUR,CiE9hUI,UAEI,6BAAA,CAAA,4BjEiiUR,CiEniUI,UAEI,4BAAA,CAAA,2BjEsiUR,CiExiUI,UAEI,2BAAA,CAAA,0BjE2iUR,CiE7iUI,UAEI,6BAAA,CAAA,4BjEgjUR,CiEljUI,UAEI,2BAAA,CAAA,0BjEqjUR,CiEvjUI,aAEI,2BAAA,CAAA,0BjE0jUR,CiE5jUI,UAEI,sBAAA,CAAA,yBjE+jUR,CiEjkUI,UAEI,2BAAA,CAAA,8BjEokUR,CiEtkUI,UAEI,0BAAA,CAAA,6BjEykUR,CiE3kUI,UAEI,yBAAA,CAAA,4BjE8kUR,CiEhlUI,UAEI,2BAAA,CAAA,8BjEmlUR,CiErlUI,UAEI,yBAAA,CAAA,4BjEwlUR,CiE1lUI,aAEI,yBAAA,CAAA,4BjE6lUR,CiE/lUI,UAEI,sBjEimUR,CiEnmUI,UAEI,2BjEqmUR,CiEvmUI,UAEI,0BjEymUR,CiE3mUI,UAEI,yBjE6mUR,CiE/mUI,UAEI,2BjEinUR,CiEnnUI,UAEI,yBjEqnUR,CiEvnUI,aAEI,yBjEynUR,CiE3nUI,UAEI,wBjE6nUR,CiE/nUI,UAEI,6BjEioUR,CiEnoUI,UAEI,4BjEqoUR,CiEvoUI,UAEI,2BjEyoUR,CiE3oUI,UAEI,6BjE6oUR,CiE/oUI,UAEI,2BjEipUR,CiEnpUI,aAEI,2BjEqpUR,CiEvpUI,UAEI,yBjEypUR,CiE3pUI,UAEI,8BjE6pUR,CiE/pUI,UAEI,6BjEiqUR,CiEnqUI,UAEI,4BjEqqUR,CiEvqUI,UAEI,8BjEyqUR,CiE3qUI,UAEI,4BjE6qUR,CiE/qUI,aAEI,4BjEirUR,CiEnrUI,UAEI,uBjEqrUR,CiEvrUI,UAEI,4BjEyrUR,CiE3rUI,UAEI,2BjE6rUR,CiE/rUI,UAEI,0BjEisUR,CiEnsUI,UAEI,4BjEqsUR,CiEvsUI,UAEI,0BjEysUR,CiE3sUI,aAEI,0BjE6sUR,CiE/sUI,SAEI,mBjEitUR,CiEntUI,SAEI,wBjEqtUR,CiEvtUI,SAEI,uBjEytUR,CiE3tUI,SAEI,sBjE6tUR,CiE/tUI,SAEI,wBjEiuUR,CiEnuUI,SAEI,sBjEquUR,CiEvuUI,UAEI,yBAAA,CAAA,wBjE0uUR,CiE5uUI,UAEI,8BAAA,CAAA,6BjE+uUR,CiEjvUI,UAEI,6BAAA,CAAA,4BjEovUR,CiEtvUI,UAEI,4BAAA,CAAA,2BjEyvUR,CiE3vUI,UAEI,8BAAA,CAAA,6BjE8vUR,CiEhwUI,UAEI,4BAAA,CAAA,2BjEmwUR,CiErwUI,UAEI,uBAAA,CAAA,0BjEwwUR,CiE1wUI,UAEI,4BAAA,CAAA,+BjE6wUR,CiE/wUI,UAEI,2BAAA,CAAA,8BjEkxUR,CiEpxUI,UAEI,0BAAA,CAAA,6BjEuxUR,CiEzxUI,UAEI,4BAAA,CAAA,+BjE4xUR,CiE9xUI,UAEI,0BAAA,CAAA,6BjEiyUR,CiEnyUI,UAEI,uBjEqyUR,CiEvyUI,UAEI,4BjEyyUR,CiE3yUI,UAEI,2BjE6yUR,CiE/yUI,UAEI,0BjEizUR,CiEnzUI,UAEI,4BjEqzUR,CiEvzUI,UAEI,0BjEyzUR,CiE3zUI,UAEI,yBjE6zUR,CiE/zUI,UAEI,8BjEi0UR,CiEn0UI,UAEI,6BjEq0UR,CiEv0UI,UAEI,4BjEy0UR,CiE30UI,UAEI,8BjE60UR,CiE/0UI,UAEI,4BjEi1UR,CiEn1UI,UAEI,0BjEq1UR,CiEv1UI,UAEI,+BjEy1UR,CiE31UI,UAEI,8BjE61UR,CiE/1UI,UAEI,6BjEi2UR,CiEn2UI,UAEI,+BjEq2UR,CiEv2UI,UAEI,6BjEy2UR,CiE32UI,UAEI,wBjE62UR,CiE/2UI,UAEI,6BjEi3UR,CiEn3UI,UAEI,4BjEq3UR,CiEv3UI,UAEI,2BjEy3UR,CiE33UI,UAEI,6BjE63UR,CiE/3UI,UAEI,2BjEi4UR,CiEn4UI,gBAEI,yBjEq4UR,CiEv4UI,cAEI,0BjEy4UR,CiE34UI,iBAEI,2BjE64UR,CACF,CkE96UA,0BD8BM,MAEI,0BjEk5UR,CiEp5UI,MAEI,wBjEs5UR,CiEx5UI,MAEI,2BjE05UR,CiE55UI,MAEI,0BjE85UR,CACF,CkE56UA,aDWM,gBAEI,wBjEm6UR,CiEr6UI,sBAEI,8BjEu6UR,CiEz6UI,eAEI,uBjE26UR,CiE76UI,cAEI,sBjE+6UR,CiEj7UI,eAEI,uBjEm7UR,CiEr7UI,mBAEI,2BjEu7UR,CiEz7UI,oBAEI,4BjE27UR,CiE77UI,cAEI,sBjE+7UR,CiEj8UI,qBAEI,6BjEm8UR,CiEr8UI,cAEI,sBjEu8UR,CACF","file":"2.3ab4f688.chunk.css","sourcesContent":["//\n// Headings\n//\n.h1 {\n @extend h1;\n}\n\n.h2 {\n @extend h2;\n}\n\n.h3 {\n @extend h3;\n}\n\n.h4 {\n @extend h4;\n}\n\n.h5 {\n @extend h5;\n}\n\n.h6 {\n @extend h6;\n}\n\n\n.lead {\n @include font-size($lead-font-size);\n font-weight: $lead-font-weight;\n}\n\n// Type display classes\n@each $display, $font-size in $display-font-sizes {\n .display-#{$display} {\n @include font-size($font-size);\n font-weight: $display-font-weight;\n line-height: $display-line-height;\n }\n}\n\n//\n// Emphasis\n//\n.small {\n @extend small;\n}\n\n.mark {\n @extend mark;\n}\n\n//\n// Lists\n//\n\n.list-unstyled {\n @include list-unstyled();\n}\n\n// Inline turns list items into inline-block\n.list-inline {\n @include list-unstyled();\n}\n.list-inline-item {\n display: inline-block;\n\n &:not(:last-child) {\n margin-right: $list-inline-padding;\n }\n}\n\n\n//\n// Misc\n//\n\n// Builds on `abbr`\n.initialism {\n @include font-size($initialism-font-size);\n text-transform: uppercase;\n}\n\n// Blockquotes\n.blockquote {\n margin-bottom: $blockquote-margin-y;\n @include font-size($blockquote-font-size);\n\n > :last-child {\n margin-bottom: 0;\n }\n}\n\n.blockquote-footer {\n margin-top: -$blockquote-margin-y;\n margin-bottom: $blockquote-margin-y;\n @include font-size($blockquote-footer-font-size);\n color: $blockquote-footer-color;\n\n &::before {\n content: \"\\2014\\00A0\"; // em dash, nbsp\n }\n}\n","@charset \"UTF-8\";\n/*!\n * Bootstrap v5.0.2 (https://getbootstrap.com/)\n * Copyright 2011-2021 The Bootstrap Authors\n * Copyright 2011-2021 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n */\n:root {\n --bs-blue: #0d6efd;\n --bs-indigo: #6610f2;\n --bs-purple: #6f42c1;\n --bs-pink: #d63384;\n --bs-red: #dc3545;\n --bs-orange: #fd7e14;\n --bs-yellow: #ffc107;\n --bs-green: #198754;\n --bs-teal: #20c997;\n --bs-cyan: #0dcaf0;\n --bs-white: #fff;\n --bs-gray: #6c757d;\n --bs-gray-dark: #343a40;\n --bs-primary: #0d6efd;\n --bs-secondary: #6c757d;\n --bs-success: #198754;\n --bs-info: #0dcaf0;\n --bs-warning: #ffc107;\n --bs-danger: #dc3545;\n --bs-light: #f8f9fa;\n --bs-dark: #212529;\n --bs-font-sans-serif: system-ui, -apple-system, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, \"Noto Sans\", \"Liberation Sans\", sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\";\n --bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace;\n --bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));\n}\n\n*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\n@media (prefers-reduced-motion: no-preference) {\n :root {\n scroll-behavior: smooth;\n }\n}\n\nbody {\n margin: 0;\n font-family: var(--bs-font-sans-serif);\n font-size: 1rem;\n font-weight: 400;\n line-height: 1.5;\n color: #212529;\n background-color: #fff;\n -webkit-text-size-adjust: 100%;\n -webkit-tap-highlight-color: rgba(0, 0, 0, 0);\n}\n\nhr {\n margin: 1rem 0;\n color: inherit;\n background-color: currentColor;\n border: 0;\n opacity: 0.25;\n}\n\nhr:not([size]) {\n height: 1px;\n}\n\nh6, .h6, h5, .h5, h4, .h4, h3, .h3, h2, .h2, h1, .h1 {\n margin-top: 0;\n margin-bottom: 0.5rem;\n font-weight: 500;\n line-height: 1.2;\n}\n\nh1, .h1 {\n font-size: calc(1.375rem + 1.5vw);\n}\n@media (min-width: 1200px) {\n h1, .h1 {\n font-size: 2.5rem;\n }\n}\n\nh2, .h2 {\n font-size: calc(1.325rem + 0.9vw);\n}\n@media (min-width: 1200px) {\n h2, .h2 {\n font-size: 2rem;\n }\n}\n\nh3, .h3 {\n font-size: calc(1.3rem + 0.6vw);\n}\n@media (min-width: 1200px) {\n h3, .h3 {\n font-size: 1.75rem;\n }\n}\n\nh4, .h4 {\n font-size: calc(1.275rem + 0.3vw);\n}\n@media (min-width: 1200px) {\n h4, .h4 {\n font-size: 1.5rem;\n }\n}\n\nh5, .h5 {\n font-size: 1.25rem;\n}\n\nh6, .h6 {\n font-size: 1rem;\n}\n\np {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nabbr[title],\nabbr[data-bs-original-title] {\n text-decoration: underline dotted;\n cursor: help;\n text-decoration-skip-ink: none;\n}\n\naddress {\n margin-bottom: 1rem;\n font-style: normal;\n line-height: inherit;\n}\n\nol,\nul {\n padding-left: 2rem;\n}\n\nol,\nul,\ndl {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nol ol,\nul ul,\nol ul,\nul ol {\n margin-bottom: 0;\n}\n\ndt {\n font-weight: 700;\n}\n\ndd {\n margin-bottom: 0.5rem;\n margin-left: 0;\n}\n\nblockquote {\n margin: 0 0 1rem;\n}\n\nb,\nstrong {\n font-weight: bolder;\n}\n\nsmall, .small {\n font-size: 0.875em;\n}\n\nmark, .mark {\n padding: 0.2em;\n background-color: #fcf8e3;\n}\n\nsub,\nsup {\n position: relative;\n font-size: 0.75em;\n line-height: 0;\n vertical-align: baseline;\n}\n\nsub {\n bottom: -0.25em;\n}\n\nsup {\n top: -0.5em;\n}\n\na {\n color: #0d6efd;\n text-decoration: underline;\n}\na:hover {\n color: #0a58ca;\n}\n\na:not([href]):not([class]), a:not([href]):not([class]):hover {\n color: inherit;\n text-decoration: none;\n}\n\npre,\ncode,\nkbd,\nsamp {\n font-family: var(--bs-font-monospace);\n font-size: 1em;\n direction: ltr /* rtl:ignore */;\n unicode-bidi: bidi-override;\n}\n\npre {\n display: block;\n margin-top: 0;\n margin-bottom: 1rem;\n overflow: auto;\n font-size: 0.875em;\n}\npre code {\n font-size: inherit;\n color: inherit;\n word-break: normal;\n}\n\ncode {\n font-size: 0.875em;\n color: #d63384;\n word-wrap: break-word;\n}\na > code {\n color: inherit;\n}\n\nkbd {\n padding: 0.2rem 0.4rem;\n font-size: 0.875em;\n color: #fff;\n background-color: #212529;\n border-radius: 0.2rem;\n}\nkbd kbd {\n padding: 0;\n font-size: 1em;\n font-weight: 700;\n}\n\nfigure {\n margin: 0 0 1rem;\n}\n\nimg,\nsvg {\n vertical-align: middle;\n}\n\ntable {\n caption-side: bottom;\n border-collapse: collapse;\n}\n\ncaption {\n padding-top: 0.5rem;\n padding-bottom: 0.5rem;\n color: #6c757d;\n text-align: left;\n}\n\nth {\n text-align: inherit;\n text-align: -webkit-match-parent;\n}\n\nthead,\ntbody,\ntfoot,\ntr,\ntd,\nth {\n border-color: inherit;\n border-style: solid;\n border-width: 0;\n}\n\nlabel {\n display: inline-block;\n}\n\nbutton {\n border-radius: 0;\n}\n\nbutton:focus:not(:focus-visible) {\n outline: 0;\n}\n\ninput,\nbutton,\nselect,\noptgroup,\ntextarea {\n margin: 0;\n font-family: inherit;\n font-size: inherit;\n line-height: inherit;\n}\n\nbutton,\nselect {\n text-transform: none;\n}\n\n[role=button] {\n cursor: pointer;\n}\n\nselect {\n word-wrap: normal;\n}\nselect:disabled {\n opacity: 1;\n}\n\n[list]::-webkit-calendar-picker-indicator {\n display: none;\n}\n\nbutton,\n[type=button],\n[type=reset],\n[type=submit] {\n -webkit-appearance: button;\n}\nbutton:not(:disabled),\n[type=button]:not(:disabled),\n[type=reset]:not(:disabled),\n[type=submit]:not(:disabled) {\n cursor: pointer;\n}\n\n::-moz-focus-inner {\n padding: 0;\n border-style: none;\n}\n\ntextarea {\n resize: vertical;\n}\n\nfieldset {\n min-width: 0;\n padding: 0;\n margin: 0;\n border: 0;\n}\n\nlegend {\n float: left;\n width: 100%;\n padding: 0;\n margin-bottom: 0.5rem;\n font-size: calc(1.275rem + 0.3vw);\n line-height: inherit;\n}\n@media (min-width: 1200px) {\n legend {\n font-size: 1.5rem;\n }\n}\nlegend + * {\n clear: left;\n}\n\n::-webkit-datetime-edit-fields-wrapper,\n::-webkit-datetime-edit-text,\n::-webkit-datetime-edit-minute,\n::-webkit-datetime-edit-hour-field,\n::-webkit-datetime-edit-day-field,\n::-webkit-datetime-edit-month-field,\n::-webkit-datetime-edit-year-field {\n padding: 0;\n}\n\n::-webkit-inner-spin-button {\n height: auto;\n}\n\n[type=search] {\n outline-offset: -2px;\n -webkit-appearance: textfield;\n}\n\n/* rtl:raw:\n[type=\"tel\"],\n[type=\"url\"],\n[type=\"email\"],\n[type=\"number\"] {\n direction: ltr;\n}\n*/\n::-webkit-search-decoration {\n -webkit-appearance: none;\n}\n\n::-webkit-color-swatch-wrapper {\n padding: 0;\n}\n\n::file-selector-button {\n font: inherit;\n}\n\n::-webkit-file-upload-button {\n font: inherit;\n -webkit-appearance: button;\n}\n\noutput {\n display: inline-block;\n}\n\niframe {\n border: 0;\n}\n\nsummary {\n display: list-item;\n cursor: pointer;\n}\n\nprogress {\n vertical-align: baseline;\n}\n\n[hidden] {\n display: none !important;\n}\n\n.lead {\n font-size: 1.25rem;\n font-weight: 300;\n}\n\n.display-1 {\n font-size: calc(1.625rem + 4.5vw);\n font-weight: 300;\n line-height: 1.2;\n}\n@media (min-width: 1200px) {\n .display-1 {\n font-size: 5rem;\n }\n}\n\n.display-2 {\n font-size: calc(1.575rem + 3.9vw);\n font-weight: 300;\n line-height: 1.2;\n}\n@media (min-width: 1200px) {\n .display-2 {\n font-size: 4.5rem;\n }\n}\n\n.display-3 {\n font-size: calc(1.525rem + 3.3vw);\n font-weight: 300;\n line-height: 1.2;\n}\n@media (min-width: 1200px) {\n .display-3 {\n font-size: 4rem;\n }\n}\n\n.display-4 {\n font-size: calc(1.475rem + 2.7vw);\n font-weight: 300;\n line-height: 1.2;\n}\n@media (min-width: 1200px) {\n .display-4 {\n font-size: 3.5rem;\n }\n}\n\n.display-5 {\n font-size: calc(1.425rem + 2.1vw);\n font-weight: 300;\n line-height: 1.2;\n}\n@media (min-width: 1200px) {\n .display-5 {\n font-size: 3rem;\n }\n}\n\n.display-6 {\n font-size: calc(1.375rem + 1.5vw);\n font-weight: 300;\n line-height: 1.2;\n}\n@media (min-width: 1200px) {\n .display-6 {\n font-size: 2.5rem;\n }\n}\n\n.list-unstyled {\n padding-left: 0;\n list-style: none;\n}\n\n.list-inline {\n padding-left: 0;\n list-style: none;\n}\n\n.list-inline-item {\n display: inline-block;\n}\n.list-inline-item:not(:last-child) {\n margin-right: 0.5rem;\n}\n\n.initialism {\n font-size: 0.875em;\n text-transform: uppercase;\n}\n\n.blockquote {\n margin-bottom: 1rem;\n font-size: 1.25rem;\n}\n.blockquote > :last-child {\n margin-bottom: 0;\n}\n\n.blockquote-footer {\n margin-top: -1rem;\n margin-bottom: 1rem;\n font-size: 0.875em;\n color: #6c757d;\n}\n.blockquote-footer::before {\n content: \"— \";\n}\n\n.img-fluid {\n max-width: 100%;\n height: auto;\n}\n\n.img-thumbnail {\n padding: 0.25rem;\n background-color: #fff;\n border: 1px solid #dee2e6;\n border-radius: 0.25rem;\n max-width: 100%;\n height: auto;\n}\n\n.figure {\n display: inline-block;\n}\n\n.figure-img {\n margin-bottom: 0.5rem;\n line-height: 1;\n}\n\n.figure-caption {\n font-size: 0.875em;\n color: #6c757d;\n}\n\n.container,\n.container-fluid,\n.container-xxl,\n.container-xl,\n.container-lg,\n.container-md,\n.container-sm {\n width: 100%;\n padding-right: var(--bs-gutter-x, 0.75rem);\n padding-left: var(--bs-gutter-x, 0.75rem);\n margin-right: auto;\n margin-left: auto;\n}\n\n@media (min-width: 576px) {\n .container-sm, .container {\n max-width: 540px;\n }\n}\n@media (min-width: 768px) {\n .container-md, .container-sm, .container {\n max-width: 720px;\n }\n}\n@media (min-width: 992px) {\n .container-lg, .container-md, .container-sm, .container {\n max-width: 960px;\n }\n}\n@media (min-width: 1200px) {\n .container-xl, .container-lg, .container-md, .container-sm, .container {\n max-width: 1140px;\n }\n}\n@media (min-width: 1400px) {\n .container-xxl, .container-xl, .container-lg, .container-md, .container-sm, .container {\n max-width: 1320px;\n }\n}\n.row {\n --bs-gutter-x: 1.5rem;\n --bs-gutter-y: 0;\n display: flex;\n flex-wrap: wrap;\n margin-top: calc(var(--bs-gutter-y) * -1);\n margin-right: calc(var(--bs-gutter-x) * -.5);\n margin-left: calc(var(--bs-gutter-x) * -.5);\n}\n.row > * {\n flex-shrink: 0;\n width: 100%;\n max-width: 100%;\n padding-right: calc(var(--bs-gutter-x) * .5);\n padding-left: calc(var(--bs-gutter-x) * .5);\n margin-top: var(--bs-gutter-y);\n}\n\n.col {\n flex: 1 0 0%;\n}\n\n.row-cols-auto > * {\n flex: 0 0 auto;\n width: auto;\n}\n\n.row-cols-1 > * {\n flex: 0 0 auto;\n width: 100%;\n}\n\n.row-cols-2 > * {\n flex: 0 0 auto;\n width: 50%;\n}\n\n.row-cols-3 > * {\n flex: 0 0 auto;\n width: 33.3333333333%;\n}\n\n.row-cols-4 > * {\n flex: 0 0 auto;\n width: 25%;\n}\n\n.row-cols-5 > * {\n flex: 0 0 auto;\n width: 20%;\n}\n\n.row-cols-6 > * {\n flex: 0 0 auto;\n width: 16.6666666667%;\n}\n\n@media (min-width: 576px) {\n .col-sm {\n flex: 1 0 0%;\n }\n\n .row-cols-sm-auto > * {\n flex: 0 0 auto;\n width: auto;\n }\n\n .row-cols-sm-1 > * {\n flex: 0 0 auto;\n width: 100%;\n }\n\n .row-cols-sm-2 > * {\n flex: 0 0 auto;\n width: 50%;\n }\n\n .row-cols-sm-3 > * {\n flex: 0 0 auto;\n width: 33.3333333333%;\n }\n\n .row-cols-sm-4 > * {\n flex: 0 0 auto;\n width: 25%;\n }\n\n .row-cols-sm-5 > * {\n flex: 0 0 auto;\n width: 20%;\n }\n\n .row-cols-sm-6 > * {\n flex: 0 0 auto;\n width: 16.6666666667%;\n }\n}\n@media (min-width: 768px) {\n .col-md {\n flex: 1 0 0%;\n }\n\n .row-cols-md-auto > * {\n flex: 0 0 auto;\n width: auto;\n }\n\n .row-cols-md-1 > * {\n flex: 0 0 auto;\n width: 100%;\n }\n\n .row-cols-md-2 > * {\n flex: 0 0 auto;\n width: 50%;\n }\n\n .row-cols-md-3 > * {\n flex: 0 0 auto;\n width: 33.3333333333%;\n }\n\n .row-cols-md-4 > * {\n flex: 0 0 auto;\n width: 25%;\n }\n\n .row-cols-md-5 > * {\n flex: 0 0 auto;\n width: 20%;\n }\n\n .row-cols-md-6 > * {\n flex: 0 0 auto;\n width: 16.6666666667%;\n }\n}\n@media (min-width: 992px) {\n .col-lg {\n flex: 1 0 0%;\n }\n\n .row-cols-lg-auto > * {\n flex: 0 0 auto;\n width: auto;\n }\n\n .row-cols-lg-1 > * {\n flex: 0 0 auto;\n width: 100%;\n }\n\n .row-cols-lg-2 > * {\n flex: 0 0 auto;\n width: 50%;\n }\n\n .row-cols-lg-3 > * {\n flex: 0 0 auto;\n width: 33.3333333333%;\n }\n\n .row-cols-lg-4 > * {\n flex: 0 0 auto;\n width: 25%;\n }\n\n .row-cols-lg-5 > * {\n flex: 0 0 auto;\n width: 20%;\n }\n\n .row-cols-lg-6 > * {\n flex: 0 0 auto;\n width: 16.6666666667%;\n }\n}\n@media (min-width: 1200px) {\n .col-xl {\n flex: 1 0 0%;\n }\n\n .row-cols-xl-auto > * {\n flex: 0 0 auto;\n width: auto;\n }\n\n .row-cols-xl-1 > * {\n flex: 0 0 auto;\n width: 100%;\n }\n\n .row-cols-xl-2 > * {\n flex: 0 0 auto;\n width: 50%;\n }\n\n .row-cols-xl-3 > * {\n flex: 0 0 auto;\n width: 33.3333333333%;\n }\n\n .row-cols-xl-4 > * {\n flex: 0 0 auto;\n width: 25%;\n }\n\n .row-cols-xl-5 > * {\n flex: 0 0 auto;\n width: 20%;\n }\n\n .row-cols-xl-6 > * {\n flex: 0 0 auto;\n width: 16.6666666667%;\n }\n}\n@media (min-width: 1400px) {\n .col-xxl {\n flex: 1 0 0%;\n }\n\n .row-cols-xxl-auto > * {\n flex: 0 0 auto;\n width: auto;\n }\n\n .row-cols-xxl-1 > * {\n flex: 0 0 auto;\n width: 100%;\n }\n\n .row-cols-xxl-2 > * {\n flex: 0 0 auto;\n width: 50%;\n }\n\n .row-cols-xxl-3 > * {\n flex: 0 0 auto;\n width: 33.3333333333%;\n }\n\n .row-cols-xxl-4 > * {\n flex: 0 0 auto;\n width: 25%;\n }\n\n .row-cols-xxl-5 > * {\n flex: 0 0 auto;\n width: 20%;\n }\n\n .row-cols-xxl-6 > * {\n flex: 0 0 auto;\n width: 16.6666666667%;\n }\n}\n.col-auto {\n flex: 0 0 auto;\n width: auto;\n}\n\n.col-1 {\n flex: 0 0 auto;\n width: 8.33333333%;\n}\n\n.col-2 {\n flex: 0 0 auto;\n width: 16.66666667%;\n}\n\n.col-3 {\n flex: 0 0 auto;\n width: 25%;\n}\n\n.col-4 {\n flex: 0 0 auto;\n width: 33.33333333%;\n}\n\n.col-5 {\n flex: 0 0 auto;\n width: 41.66666667%;\n}\n\n.col-6 {\n flex: 0 0 auto;\n width: 50%;\n}\n\n.col-7 {\n flex: 0 0 auto;\n width: 58.33333333%;\n}\n\n.col-8 {\n flex: 0 0 auto;\n width: 66.66666667%;\n}\n\n.col-9 {\n flex: 0 0 auto;\n width: 75%;\n}\n\n.col-10 {\n flex: 0 0 auto;\n width: 83.33333333%;\n}\n\n.col-11 {\n flex: 0 0 auto;\n width: 91.66666667%;\n}\n\n.col-12 {\n flex: 0 0 auto;\n width: 100%;\n}\n\n.offset-1 {\n margin-left: 8.33333333%;\n}\n\n.offset-2 {\n margin-left: 16.66666667%;\n}\n\n.offset-3 {\n margin-left: 25%;\n}\n\n.offset-4 {\n margin-left: 33.33333333%;\n}\n\n.offset-5 {\n margin-left: 41.66666667%;\n}\n\n.offset-6 {\n margin-left: 50%;\n}\n\n.offset-7 {\n margin-left: 58.33333333%;\n}\n\n.offset-8 {\n margin-left: 66.66666667%;\n}\n\n.offset-9 {\n margin-left: 75%;\n}\n\n.offset-10 {\n margin-left: 83.33333333%;\n}\n\n.offset-11 {\n margin-left: 91.66666667%;\n}\n\n.g-0,\n.gx-0 {\n --bs-gutter-x: 0;\n}\n\n.g-0,\n.gy-0 {\n --bs-gutter-y: 0;\n}\n\n.g-1,\n.gx-1 {\n --bs-gutter-x: 0.25rem;\n}\n\n.g-1,\n.gy-1 {\n --bs-gutter-y: 0.25rem;\n}\n\n.g-2,\n.gx-2 {\n --bs-gutter-x: 0.5rem;\n}\n\n.g-2,\n.gy-2 {\n --bs-gutter-y: 0.5rem;\n}\n\n.g-3,\n.gx-3 {\n --bs-gutter-x: 1rem;\n}\n\n.g-3,\n.gy-3 {\n --bs-gutter-y: 1rem;\n}\n\n.g-4,\n.gx-4 {\n --bs-gutter-x: 1.5rem;\n}\n\n.g-4,\n.gy-4 {\n --bs-gutter-y: 1.5rem;\n}\n\n.g-5,\n.gx-5 {\n --bs-gutter-x: 3rem;\n}\n\n.g-5,\n.gy-5 {\n --bs-gutter-y: 3rem;\n}\n\n@media (min-width: 576px) {\n .col-sm-auto {\n flex: 0 0 auto;\n width: auto;\n }\n\n .col-sm-1 {\n flex: 0 0 auto;\n width: 8.33333333%;\n }\n\n .col-sm-2 {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n\n .col-sm-3 {\n flex: 0 0 auto;\n width: 25%;\n }\n\n .col-sm-4 {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n\n .col-sm-5 {\n flex: 0 0 auto;\n width: 41.66666667%;\n }\n\n .col-sm-6 {\n flex: 0 0 auto;\n width: 50%;\n }\n\n .col-sm-7 {\n flex: 0 0 auto;\n width: 58.33333333%;\n }\n\n .col-sm-8 {\n flex: 0 0 auto;\n width: 66.66666667%;\n }\n\n .col-sm-9 {\n flex: 0 0 auto;\n width: 75%;\n }\n\n .col-sm-10 {\n flex: 0 0 auto;\n width: 83.33333333%;\n }\n\n .col-sm-11 {\n flex: 0 0 auto;\n width: 91.66666667%;\n }\n\n .col-sm-12 {\n flex: 0 0 auto;\n width: 100%;\n }\n\n .offset-sm-0 {\n margin-left: 0;\n }\n\n .offset-sm-1 {\n margin-left: 8.33333333%;\n }\n\n .offset-sm-2 {\n margin-left: 16.66666667%;\n }\n\n .offset-sm-3 {\n margin-left: 25%;\n }\n\n .offset-sm-4 {\n margin-left: 33.33333333%;\n }\n\n .offset-sm-5 {\n margin-left: 41.66666667%;\n }\n\n .offset-sm-6 {\n margin-left: 50%;\n }\n\n .offset-sm-7 {\n margin-left: 58.33333333%;\n }\n\n .offset-sm-8 {\n margin-left: 66.66666667%;\n }\n\n .offset-sm-9 {\n margin-left: 75%;\n }\n\n .offset-sm-10 {\n margin-left: 83.33333333%;\n }\n\n .offset-sm-11 {\n margin-left: 91.66666667%;\n }\n\n .g-sm-0,\n.gx-sm-0 {\n --bs-gutter-x: 0;\n }\n\n .g-sm-0,\n.gy-sm-0 {\n --bs-gutter-y: 0;\n }\n\n .g-sm-1,\n.gx-sm-1 {\n --bs-gutter-x: 0.25rem;\n }\n\n .g-sm-1,\n.gy-sm-1 {\n --bs-gutter-y: 0.25rem;\n }\n\n .g-sm-2,\n.gx-sm-2 {\n --bs-gutter-x: 0.5rem;\n }\n\n .g-sm-2,\n.gy-sm-2 {\n --bs-gutter-y: 0.5rem;\n }\n\n .g-sm-3,\n.gx-sm-3 {\n --bs-gutter-x: 1rem;\n }\n\n .g-sm-3,\n.gy-sm-3 {\n --bs-gutter-y: 1rem;\n }\n\n .g-sm-4,\n.gx-sm-4 {\n --bs-gutter-x: 1.5rem;\n }\n\n .g-sm-4,\n.gy-sm-4 {\n --bs-gutter-y: 1.5rem;\n }\n\n .g-sm-5,\n.gx-sm-5 {\n --bs-gutter-x: 3rem;\n }\n\n .g-sm-5,\n.gy-sm-5 {\n --bs-gutter-y: 3rem;\n }\n}\n@media (min-width: 768px) {\n .col-md-auto {\n flex: 0 0 auto;\n width: auto;\n }\n\n .col-md-1 {\n flex: 0 0 auto;\n width: 8.33333333%;\n }\n\n .col-md-2 {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n\n .col-md-3 {\n flex: 0 0 auto;\n width: 25%;\n }\n\n .col-md-4 {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n\n .col-md-5 {\n flex: 0 0 auto;\n width: 41.66666667%;\n }\n\n .col-md-6 {\n flex: 0 0 auto;\n width: 50%;\n }\n\n .col-md-7 {\n flex: 0 0 auto;\n width: 58.33333333%;\n }\n\n .col-md-8 {\n flex: 0 0 auto;\n width: 66.66666667%;\n }\n\n .col-md-9 {\n flex: 0 0 auto;\n width: 75%;\n }\n\n .col-md-10 {\n flex: 0 0 auto;\n width: 83.33333333%;\n }\n\n .col-md-11 {\n flex: 0 0 auto;\n width: 91.66666667%;\n }\n\n .col-md-12 {\n flex: 0 0 auto;\n width: 100%;\n }\n\n .offset-md-0 {\n margin-left: 0;\n }\n\n .offset-md-1 {\n margin-left: 8.33333333%;\n }\n\n .offset-md-2 {\n margin-left: 16.66666667%;\n }\n\n .offset-md-3 {\n margin-left: 25%;\n }\n\n .offset-md-4 {\n margin-left: 33.33333333%;\n }\n\n .offset-md-5 {\n margin-left: 41.66666667%;\n }\n\n .offset-md-6 {\n margin-left: 50%;\n }\n\n .offset-md-7 {\n margin-left: 58.33333333%;\n }\n\n .offset-md-8 {\n margin-left: 66.66666667%;\n }\n\n .offset-md-9 {\n margin-left: 75%;\n }\n\n .offset-md-10 {\n margin-left: 83.33333333%;\n }\n\n .offset-md-11 {\n margin-left: 91.66666667%;\n }\n\n .g-md-0,\n.gx-md-0 {\n --bs-gutter-x: 0;\n }\n\n .g-md-0,\n.gy-md-0 {\n --bs-gutter-y: 0;\n }\n\n .g-md-1,\n.gx-md-1 {\n --bs-gutter-x: 0.25rem;\n }\n\n .g-md-1,\n.gy-md-1 {\n --bs-gutter-y: 0.25rem;\n }\n\n .g-md-2,\n.gx-md-2 {\n --bs-gutter-x: 0.5rem;\n }\n\n .g-md-2,\n.gy-md-2 {\n --bs-gutter-y: 0.5rem;\n }\n\n .g-md-3,\n.gx-md-3 {\n --bs-gutter-x: 1rem;\n }\n\n .g-md-3,\n.gy-md-3 {\n --bs-gutter-y: 1rem;\n }\n\n .g-md-4,\n.gx-md-4 {\n --bs-gutter-x: 1.5rem;\n }\n\n .g-md-4,\n.gy-md-4 {\n --bs-gutter-y: 1.5rem;\n }\n\n .g-md-5,\n.gx-md-5 {\n --bs-gutter-x: 3rem;\n }\n\n .g-md-5,\n.gy-md-5 {\n --bs-gutter-y: 3rem;\n }\n}\n@media (min-width: 992px) {\n .col-lg-auto {\n flex: 0 0 auto;\n width: auto;\n }\n\n .col-lg-1 {\n flex: 0 0 auto;\n width: 8.33333333%;\n }\n\n .col-lg-2 {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n\n .col-lg-3 {\n flex: 0 0 auto;\n width: 25%;\n }\n\n .col-lg-4 {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n\n .col-lg-5 {\n flex: 0 0 auto;\n width: 41.66666667%;\n }\n\n .col-lg-6 {\n flex: 0 0 auto;\n width: 50%;\n }\n\n .col-lg-7 {\n flex: 0 0 auto;\n width: 58.33333333%;\n }\n\n .col-lg-8 {\n flex: 0 0 auto;\n width: 66.66666667%;\n }\n\n .col-lg-9 {\n flex: 0 0 auto;\n width: 75%;\n }\n\n .col-lg-10 {\n flex: 0 0 auto;\n width: 83.33333333%;\n }\n\n .col-lg-11 {\n flex: 0 0 auto;\n width: 91.66666667%;\n }\n\n .col-lg-12 {\n flex: 0 0 auto;\n width: 100%;\n }\n\n .offset-lg-0 {\n margin-left: 0;\n }\n\n .offset-lg-1 {\n margin-left: 8.33333333%;\n }\n\n .offset-lg-2 {\n margin-left: 16.66666667%;\n }\n\n .offset-lg-3 {\n margin-left: 25%;\n }\n\n .offset-lg-4 {\n margin-left: 33.33333333%;\n }\n\n .offset-lg-5 {\n margin-left: 41.66666667%;\n }\n\n .offset-lg-6 {\n margin-left: 50%;\n }\n\n .offset-lg-7 {\n margin-left: 58.33333333%;\n }\n\n .offset-lg-8 {\n margin-left: 66.66666667%;\n }\n\n .offset-lg-9 {\n margin-left: 75%;\n }\n\n .offset-lg-10 {\n margin-left: 83.33333333%;\n }\n\n .offset-lg-11 {\n margin-left: 91.66666667%;\n }\n\n .g-lg-0,\n.gx-lg-0 {\n --bs-gutter-x: 0;\n }\n\n .g-lg-0,\n.gy-lg-0 {\n --bs-gutter-y: 0;\n }\n\n .g-lg-1,\n.gx-lg-1 {\n --bs-gutter-x: 0.25rem;\n }\n\n .g-lg-1,\n.gy-lg-1 {\n --bs-gutter-y: 0.25rem;\n }\n\n .g-lg-2,\n.gx-lg-2 {\n --bs-gutter-x: 0.5rem;\n }\n\n .g-lg-2,\n.gy-lg-2 {\n --bs-gutter-y: 0.5rem;\n }\n\n .g-lg-3,\n.gx-lg-3 {\n --bs-gutter-x: 1rem;\n }\n\n .g-lg-3,\n.gy-lg-3 {\n --bs-gutter-y: 1rem;\n }\n\n .g-lg-4,\n.gx-lg-4 {\n --bs-gutter-x: 1.5rem;\n }\n\n .g-lg-4,\n.gy-lg-4 {\n --bs-gutter-y: 1.5rem;\n }\n\n .g-lg-5,\n.gx-lg-5 {\n --bs-gutter-x: 3rem;\n }\n\n .g-lg-5,\n.gy-lg-5 {\n --bs-gutter-y: 3rem;\n }\n}\n@media (min-width: 1200px) {\n .col-xl-auto {\n flex: 0 0 auto;\n width: auto;\n }\n\n .col-xl-1 {\n flex: 0 0 auto;\n width: 8.33333333%;\n }\n\n .col-xl-2 {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n\n .col-xl-3 {\n flex: 0 0 auto;\n width: 25%;\n }\n\n .col-xl-4 {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n\n .col-xl-5 {\n flex: 0 0 auto;\n width: 41.66666667%;\n }\n\n .col-xl-6 {\n flex: 0 0 auto;\n width: 50%;\n }\n\n .col-xl-7 {\n flex: 0 0 auto;\n width: 58.33333333%;\n }\n\n .col-xl-8 {\n flex: 0 0 auto;\n width: 66.66666667%;\n }\n\n .col-xl-9 {\n flex: 0 0 auto;\n width: 75%;\n }\n\n .col-xl-10 {\n flex: 0 0 auto;\n width: 83.33333333%;\n }\n\n .col-xl-11 {\n flex: 0 0 auto;\n width: 91.66666667%;\n }\n\n .col-xl-12 {\n flex: 0 0 auto;\n width: 100%;\n }\n\n .offset-xl-0 {\n margin-left: 0;\n }\n\n .offset-xl-1 {\n margin-left: 8.33333333%;\n }\n\n .offset-xl-2 {\n margin-left: 16.66666667%;\n }\n\n .offset-xl-3 {\n margin-left: 25%;\n }\n\n .offset-xl-4 {\n margin-left: 33.33333333%;\n }\n\n .offset-xl-5 {\n margin-left: 41.66666667%;\n }\n\n .offset-xl-6 {\n margin-left: 50%;\n }\n\n .offset-xl-7 {\n margin-left: 58.33333333%;\n }\n\n .offset-xl-8 {\n margin-left: 66.66666667%;\n }\n\n .offset-xl-9 {\n margin-left: 75%;\n }\n\n .offset-xl-10 {\n margin-left: 83.33333333%;\n }\n\n .offset-xl-11 {\n margin-left: 91.66666667%;\n }\n\n .g-xl-0,\n.gx-xl-0 {\n --bs-gutter-x: 0;\n }\n\n .g-xl-0,\n.gy-xl-0 {\n --bs-gutter-y: 0;\n }\n\n .g-xl-1,\n.gx-xl-1 {\n --bs-gutter-x: 0.25rem;\n }\n\n .g-xl-1,\n.gy-xl-1 {\n --bs-gutter-y: 0.25rem;\n }\n\n .g-xl-2,\n.gx-xl-2 {\n --bs-gutter-x: 0.5rem;\n }\n\n .g-xl-2,\n.gy-xl-2 {\n --bs-gutter-y: 0.5rem;\n }\n\n .g-xl-3,\n.gx-xl-3 {\n --bs-gutter-x: 1rem;\n }\n\n .g-xl-3,\n.gy-xl-3 {\n --bs-gutter-y: 1rem;\n }\n\n .g-xl-4,\n.gx-xl-4 {\n --bs-gutter-x: 1.5rem;\n }\n\n .g-xl-4,\n.gy-xl-4 {\n --bs-gutter-y: 1.5rem;\n }\n\n .g-xl-5,\n.gx-xl-5 {\n --bs-gutter-x: 3rem;\n }\n\n .g-xl-5,\n.gy-xl-5 {\n --bs-gutter-y: 3rem;\n }\n}\n@media (min-width: 1400px) {\n .col-xxl-auto {\n flex: 0 0 auto;\n width: auto;\n }\n\n .col-xxl-1 {\n flex: 0 0 auto;\n width: 8.33333333%;\n }\n\n .col-xxl-2 {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n\n .col-xxl-3 {\n flex: 0 0 auto;\n width: 25%;\n }\n\n .col-xxl-4 {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n\n .col-xxl-5 {\n flex: 0 0 auto;\n width: 41.66666667%;\n }\n\n .col-xxl-6 {\n flex: 0 0 auto;\n width: 50%;\n }\n\n .col-xxl-7 {\n flex: 0 0 auto;\n width: 58.33333333%;\n }\n\n .col-xxl-8 {\n flex: 0 0 auto;\n width: 66.66666667%;\n }\n\n .col-xxl-9 {\n flex: 0 0 auto;\n width: 75%;\n }\n\n .col-xxl-10 {\n flex: 0 0 auto;\n width: 83.33333333%;\n }\n\n .col-xxl-11 {\n flex: 0 0 auto;\n width: 91.66666667%;\n }\n\n .col-xxl-12 {\n flex: 0 0 auto;\n width: 100%;\n }\n\n .offset-xxl-0 {\n margin-left: 0;\n }\n\n .offset-xxl-1 {\n margin-left: 8.33333333%;\n }\n\n .offset-xxl-2 {\n margin-left: 16.66666667%;\n }\n\n .offset-xxl-3 {\n margin-left: 25%;\n }\n\n .offset-xxl-4 {\n margin-left: 33.33333333%;\n }\n\n .offset-xxl-5 {\n margin-left: 41.66666667%;\n }\n\n .offset-xxl-6 {\n margin-left: 50%;\n }\n\n .offset-xxl-7 {\n margin-left: 58.33333333%;\n }\n\n .offset-xxl-8 {\n margin-left: 66.66666667%;\n }\n\n .offset-xxl-9 {\n margin-left: 75%;\n }\n\n .offset-xxl-10 {\n margin-left: 83.33333333%;\n }\n\n .offset-xxl-11 {\n margin-left: 91.66666667%;\n }\n\n .g-xxl-0,\n.gx-xxl-0 {\n --bs-gutter-x: 0;\n }\n\n .g-xxl-0,\n.gy-xxl-0 {\n --bs-gutter-y: 0;\n }\n\n .g-xxl-1,\n.gx-xxl-1 {\n --bs-gutter-x: 0.25rem;\n }\n\n .g-xxl-1,\n.gy-xxl-1 {\n --bs-gutter-y: 0.25rem;\n }\n\n .g-xxl-2,\n.gx-xxl-2 {\n --bs-gutter-x: 0.5rem;\n }\n\n .g-xxl-2,\n.gy-xxl-2 {\n --bs-gutter-y: 0.5rem;\n }\n\n .g-xxl-3,\n.gx-xxl-3 {\n --bs-gutter-x: 1rem;\n }\n\n .g-xxl-3,\n.gy-xxl-3 {\n --bs-gutter-y: 1rem;\n }\n\n .g-xxl-4,\n.gx-xxl-4 {\n --bs-gutter-x: 1.5rem;\n }\n\n .g-xxl-4,\n.gy-xxl-4 {\n --bs-gutter-y: 1.5rem;\n }\n\n .g-xxl-5,\n.gx-xxl-5 {\n --bs-gutter-x: 3rem;\n }\n\n .g-xxl-5,\n.gy-xxl-5 {\n --bs-gutter-y: 3rem;\n }\n}\n.table {\n --bs-table-bg: transparent;\n --bs-table-accent-bg: transparent;\n --bs-table-striped-color: #212529;\n --bs-table-striped-bg: rgba(0, 0, 0, 0.05);\n --bs-table-active-color: #212529;\n --bs-table-active-bg: rgba(0, 0, 0, 0.1);\n --bs-table-hover-color: #212529;\n --bs-table-hover-bg: rgba(0, 0, 0, 0.075);\n width: 100%;\n margin-bottom: 1rem;\n color: #212529;\n vertical-align: top;\n border-color: #dee2e6;\n}\n.table > :not(caption) > * > * {\n padding: 0.5rem 0.5rem;\n background-color: var(--bs-table-bg);\n border-bottom-width: 1px;\n box-shadow: inset 0 0 0 9999px var(--bs-table-accent-bg);\n}\n.table > tbody {\n vertical-align: inherit;\n}\n.table > thead {\n vertical-align: bottom;\n}\n.table > :not(:last-child) > :last-child > * {\n border-bottom-color: currentColor;\n}\n\n.caption-top {\n caption-side: top;\n}\n\n.table-sm > :not(caption) > * > * {\n padding: 0.25rem 0.25rem;\n}\n\n.table-bordered > :not(caption) > * {\n border-width: 1px 0;\n}\n.table-bordered > :not(caption) > * > * {\n border-width: 0 1px;\n}\n\n.table-borderless > :not(caption) > * > * {\n border-bottom-width: 0;\n}\n\n.table-striped > tbody > tr:nth-of-type(odd) {\n --bs-table-accent-bg: var(--bs-table-striped-bg);\n color: var(--bs-table-striped-color);\n}\n\n.table-active {\n --bs-table-accent-bg: var(--bs-table-active-bg);\n color: var(--bs-table-active-color);\n}\n\n.table-hover > tbody > tr:hover {\n --bs-table-accent-bg: var(--bs-table-hover-bg);\n color: var(--bs-table-hover-color);\n}\n\n.table-primary {\n --bs-table-bg: #cfe2ff;\n --bs-table-striped-bg: #c5d7f2;\n --bs-table-striped-color: #000;\n --bs-table-active-bg: #bacbe6;\n --bs-table-active-color: #000;\n --bs-table-hover-bg: #bfd1ec;\n --bs-table-hover-color: #000;\n color: #000;\n border-color: #bacbe6;\n}\n\n.table-secondary {\n --bs-table-bg: #e2e3e5;\n --bs-table-striped-bg: #d7d8da;\n --bs-table-striped-color: #000;\n --bs-table-active-bg: #cbccce;\n --bs-table-active-color: #000;\n --bs-table-hover-bg: #d1d2d4;\n --bs-table-hover-color: #000;\n color: #000;\n border-color: #cbccce;\n}\n\n.table-success {\n --bs-table-bg: #d1e7dd;\n --bs-table-striped-bg: #c7dbd2;\n --bs-table-striped-color: #000;\n --bs-table-active-bg: #bcd0c7;\n --bs-table-active-color: #000;\n --bs-table-hover-bg: #c1d6cc;\n --bs-table-hover-color: #000;\n color: #000;\n border-color: #bcd0c7;\n}\n\n.table-info {\n --bs-table-bg: #cff4fc;\n --bs-table-striped-bg: #c5e8ef;\n --bs-table-striped-color: #000;\n --bs-table-active-bg: #badce3;\n --bs-table-active-color: #000;\n --bs-table-hover-bg: #bfe2e9;\n --bs-table-hover-color: #000;\n color: #000;\n border-color: #badce3;\n}\n\n.table-warning {\n --bs-table-bg: #fff3cd;\n --bs-table-striped-bg: #f2e7c3;\n --bs-table-striped-color: #000;\n --bs-table-active-bg: #e6dbb9;\n --bs-table-active-color: #000;\n --bs-table-hover-bg: #ece1be;\n --bs-table-hover-color: #000;\n color: #000;\n border-color: #e6dbb9;\n}\n\n.table-danger {\n --bs-table-bg: #f8d7da;\n --bs-table-striped-bg: #eccccf;\n --bs-table-striped-color: #000;\n --bs-table-active-bg: #dfc2c4;\n --bs-table-active-color: #000;\n --bs-table-hover-bg: #e5c7ca;\n --bs-table-hover-color: #000;\n color: #000;\n border-color: #dfc2c4;\n}\n\n.table-light {\n --bs-table-bg: #f8f9fa;\n --bs-table-striped-bg: #ecedee;\n --bs-table-striped-color: #000;\n --bs-table-active-bg: #dfe0e1;\n --bs-table-active-color: #000;\n --bs-table-hover-bg: #e5e6e7;\n --bs-table-hover-color: #000;\n color: #000;\n border-color: #dfe0e1;\n}\n\n.table-dark {\n --bs-table-bg: #212529;\n --bs-table-striped-bg: #2c3034;\n --bs-table-striped-color: #fff;\n --bs-table-active-bg: #373b3e;\n --bs-table-active-color: #fff;\n --bs-table-hover-bg: #323539;\n --bs-table-hover-color: #fff;\n color: #fff;\n border-color: #373b3e;\n}\n\n.table-responsive {\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n}\n\n@media (max-width: 575.98px) {\n .table-responsive-sm {\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n }\n}\n@media (max-width: 767.98px) {\n .table-responsive-md {\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n }\n}\n@media (max-width: 991.98px) {\n .table-responsive-lg {\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n }\n}\n@media (max-width: 1199.98px) {\n .table-responsive-xl {\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n }\n}\n@media (max-width: 1399.98px) {\n .table-responsive-xxl {\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n }\n}\n.form-label {\n margin-bottom: 0.5rem;\n}\n\n.col-form-label {\n padding-top: calc(0.375rem + 1px);\n padding-bottom: calc(0.375rem + 1px);\n margin-bottom: 0;\n font-size: inherit;\n line-height: 1.5;\n}\n\n.col-form-label-lg {\n padding-top: calc(0.5rem + 1px);\n padding-bottom: calc(0.5rem + 1px);\n font-size: 1.25rem;\n}\n\n.col-form-label-sm {\n padding-top: calc(0.25rem + 1px);\n padding-bottom: calc(0.25rem + 1px);\n font-size: 0.875rem;\n}\n\n.form-text {\n margin-top: 0.25rem;\n font-size: 0.875em;\n color: #6c757d;\n}\n\n.form-control {\n display: block;\n width: 100%;\n padding: 0.375rem 0.75rem;\n font-size: 1rem;\n font-weight: 400;\n line-height: 1.5;\n color: #212529;\n background-color: #fff;\n background-clip: padding-box;\n border: 1px solid #ced4da;\n appearance: none;\n border-radius: 0.25rem;\n transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n}\n@media (prefers-reduced-motion: reduce) {\n .form-control {\n transition: none;\n }\n}\n.form-control[type=file] {\n overflow: hidden;\n}\n.form-control[type=file]:not(:disabled):not([readonly]) {\n cursor: pointer;\n}\n.form-control:focus {\n color: #212529;\n background-color: #fff;\n border-color: #86b7fe;\n outline: 0;\n box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);\n}\n.form-control::-webkit-date-and-time-value {\n height: 1.5em;\n}\n.form-control::placeholder {\n color: #6c757d;\n opacity: 1;\n}\n.form-control:disabled, .form-control[readonly] {\n background-color: #e9ecef;\n opacity: 1;\n}\n.form-control::file-selector-button {\n padding: 0.375rem 0.75rem;\n margin: -0.375rem -0.75rem;\n margin-inline-end: 0.75rem;\n color: #212529;\n background-color: #e9ecef;\n pointer-events: none;\n border-color: inherit;\n border-style: solid;\n border-width: 0;\n border-inline-end-width: 1px;\n border-radius: 0;\n transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n}\n@media (prefers-reduced-motion: reduce) {\n .form-control::file-selector-button {\n transition: none;\n }\n}\n.form-control:hover:not(:disabled):not([readonly])::file-selector-button {\n background-color: #dde0e3;\n}\n.form-control::-webkit-file-upload-button {\n padding: 0.375rem 0.75rem;\n margin: -0.375rem -0.75rem;\n margin-inline-end: 0.75rem;\n color: #212529;\n background-color: #e9ecef;\n pointer-events: none;\n border-color: inherit;\n border-style: solid;\n border-width: 0;\n border-inline-end-width: 1px;\n border-radius: 0;\n transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n}\n@media (prefers-reduced-motion: reduce) {\n .form-control::-webkit-file-upload-button {\n transition: none;\n }\n}\n.form-control:hover:not(:disabled):not([readonly])::-webkit-file-upload-button {\n background-color: #dde0e3;\n}\n\n.form-control-plaintext {\n display: block;\n width: 100%;\n padding: 0.375rem 0;\n margin-bottom: 0;\n line-height: 1.5;\n color: #212529;\n background-color: transparent;\n border: solid transparent;\n border-width: 1px 0;\n}\n.form-control-plaintext.form-control-sm, .form-control-plaintext.form-control-lg {\n padding-right: 0;\n padding-left: 0;\n}\n\n.form-control-sm {\n min-height: calc(1.5em + (0.5rem + 2px));\n padding: 0.25rem 0.5rem;\n font-size: 0.875rem;\n border-radius: 0.2rem;\n}\n.form-control-sm::file-selector-button {\n padding: 0.25rem 0.5rem;\n margin: -0.25rem -0.5rem;\n margin-inline-end: 0.5rem;\n}\n.form-control-sm::-webkit-file-upload-button {\n padding: 0.25rem 0.5rem;\n margin: -0.25rem -0.5rem;\n margin-inline-end: 0.5rem;\n}\n\n.form-control-lg {\n min-height: calc(1.5em + (1rem + 2px));\n padding: 0.5rem 1rem;\n font-size: 1.25rem;\n border-radius: 0.3rem;\n}\n.form-control-lg::file-selector-button {\n padding: 0.5rem 1rem;\n margin: -0.5rem -1rem;\n margin-inline-end: 1rem;\n}\n.form-control-lg::-webkit-file-upload-button {\n padding: 0.5rem 1rem;\n margin: -0.5rem -1rem;\n margin-inline-end: 1rem;\n}\n\ntextarea.form-control {\n min-height: calc(1.5em + (0.75rem + 2px));\n}\ntextarea.form-control-sm {\n min-height: calc(1.5em + (0.5rem + 2px));\n}\ntextarea.form-control-lg {\n min-height: calc(1.5em + (1rem + 2px));\n}\n\n.form-control-color {\n max-width: 3rem;\n height: auto;\n padding: 0.375rem;\n}\n.form-control-color:not(:disabled):not([readonly]) {\n cursor: pointer;\n}\n.form-control-color::-moz-color-swatch {\n height: 1.5em;\n border-radius: 0.25rem;\n}\n.form-control-color::-webkit-color-swatch {\n height: 1.5em;\n border-radius: 0.25rem;\n}\n\n.form-select {\n display: block;\n width: 100%;\n padding: 0.375rem 2.25rem 0.375rem 0.75rem;\n -moz-padding-start: calc(0.75rem - 3px);\n font-size: 1rem;\n font-weight: 400;\n line-height: 1.5;\n color: #212529;\n background-color: #fff;\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e\");\n background-repeat: no-repeat;\n background-position: right 0.75rem center;\n background-size: 16px 12px;\n border: 1px solid #ced4da;\n border-radius: 0.25rem;\n transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n appearance: none;\n}\n@media (prefers-reduced-motion: reduce) {\n .form-select {\n transition: none;\n }\n}\n.form-select:focus {\n border-color: #86b7fe;\n outline: 0;\n box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);\n}\n.form-select[multiple], .form-select[size]:not([size=\"1\"]) {\n padding-right: 0.75rem;\n background-image: none;\n}\n.form-select:disabled {\n background-color: #e9ecef;\n}\n.form-select:-moz-focusring {\n color: transparent;\n text-shadow: 0 0 0 #212529;\n}\n\n.form-select-sm {\n padding-top: 0.25rem;\n padding-bottom: 0.25rem;\n padding-left: 0.5rem;\n font-size: 0.875rem;\n}\n\n.form-select-lg {\n padding-top: 0.5rem;\n padding-bottom: 0.5rem;\n padding-left: 1rem;\n font-size: 1.25rem;\n}\n\n.form-check {\n display: block;\n min-height: 1.5rem;\n padding-left: 1.5em;\n margin-bottom: 0.125rem;\n}\n.form-check .form-check-input {\n float: left;\n margin-left: -1.5em;\n}\n\n.form-check-input {\n width: 1em;\n height: 1em;\n margin-top: 0.25em;\n vertical-align: top;\n background-color: #fff;\n background-repeat: no-repeat;\n background-position: center;\n background-size: contain;\n border: 1px solid rgba(0, 0, 0, 0.25);\n appearance: none;\n color-adjust: exact;\n}\n.form-check-input[type=checkbox] {\n border-radius: 0.25em;\n}\n.form-check-input[type=radio] {\n border-radius: 50%;\n}\n.form-check-input:active {\n filter: brightness(90%);\n}\n.form-check-input:focus {\n border-color: #86b7fe;\n outline: 0;\n box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);\n}\n.form-check-input:checked {\n background-color: #0d6efd;\n border-color: #0d6efd;\n}\n.form-check-input:checked[type=checkbox] {\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10l3 3l6-6'/%3e%3c/svg%3e\");\n}\n.form-check-input:checked[type=radio] {\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='2' fill='%23fff'/%3e%3c/svg%3e\");\n}\n.form-check-input[type=checkbox]:indeterminate {\n background-color: #0d6efd;\n border-color: #0d6efd;\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e\");\n}\n.form-check-input:disabled {\n pointer-events: none;\n filter: none;\n opacity: 0.5;\n}\n.form-check-input[disabled] ~ .form-check-label, .form-check-input:disabled ~ .form-check-label {\n opacity: 0.5;\n}\n\n.form-switch {\n padding-left: 2.5em;\n}\n.form-switch .form-check-input {\n width: 2em;\n margin-left: -2.5em;\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%280, 0, 0, 0.25%29'/%3e%3c/svg%3e\");\n background-position: left center;\n border-radius: 2em;\n transition: background-position 0.15s ease-in-out;\n}\n@media (prefers-reduced-motion: reduce) {\n .form-switch .form-check-input {\n transition: none;\n }\n}\n.form-switch .form-check-input:focus {\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%2386b7fe'/%3e%3c/svg%3e\");\n}\n.form-switch .form-check-input:checked {\n background-position: right center;\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e\");\n}\n\n.form-check-inline {\n display: inline-block;\n margin-right: 1rem;\n}\n\n.btn-check {\n position: absolute;\n clip: rect(0, 0, 0, 0);\n pointer-events: none;\n}\n.btn-check[disabled] + .btn, .btn-check:disabled + .btn {\n pointer-events: none;\n filter: none;\n opacity: 0.65;\n}\n\n.form-range {\n width: 100%;\n height: 1.5rem;\n padding: 0;\n background-color: transparent;\n appearance: none;\n}\n.form-range:focus {\n outline: 0;\n}\n.form-range:focus::-webkit-slider-thumb {\n box-shadow: 0 0 0 1px #fff, 0 0 0 0.25rem rgba(13, 110, 253, 0.25);\n}\n.form-range:focus::-moz-range-thumb {\n box-shadow: 0 0 0 1px #fff, 0 0 0 0.25rem rgba(13, 110, 253, 0.25);\n}\n.form-range::-moz-focus-outer {\n border: 0;\n}\n.form-range::-webkit-slider-thumb {\n width: 1rem;\n height: 1rem;\n margin-top: -0.25rem;\n background-color: #0d6efd;\n border: 0;\n border-radius: 1rem;\n transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n appearance: none;\n}\n@media (prefers-reduced-motion: reduce) {\n .form-range::-webkit-slider-thumb {\n transition: none;\n }\n}\n.form-range::-webkit-slider-thumb:active {\n background-color: #b6d4fe;\n}\n.form-range::-webkit-slider-runnable-track {\n width: 100%;\n height: 0.5rem;\n color: transparent;\n cursor: pointer;\n background-color: #dee2e6;\n border-color: transparent;\n border-radius: 1rem;\n}\n.form-range::-moz-range-thumb {\n width: 1rem;\n height: 1rem;\n background-color: #0d6efd;\n border: 0;\n border-radius: 1rem;\n transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n appearance: none;\n}\n@media (prefers-reduced-motion: reduce) {\n .form-range::-moz-range-thumb {\n transition: none;\n }\n}\n.form-range::-moz-range-thumb:active {\n background-color: #b6d4fe;\n}\n.form-range::-moz-range-track {\n width: 100%;\n height: 0.5rem;\n color: transparent;\n cursor: pointer;\n background-color: #dee2e6;\n border-color: transparent;\n border-radius: 1rem;\n}\n.form-range:disabled {\n pointer-events: none;\n}\n.form-range:disabled::-webkit-slider-thumb {\n background-color: #adb5bd;\n}\n.form-range:disabled::-moz-range-thumb {\n background-color: #adb5bd;\n}\n\n.form-floating {\n position: relative;\n}\n.form-floating > .form-control,\n.form-floating > .form-select {\n height: calc(3.5rem + 2px);\n line-height: 1.25;\n}\n.form-floating > label {\n position: absolute;\n top: 0;\n left: 0;\n height: 100%;\n padding: 1rem 0.75rem;\n pointer-events: none;\n border: 1px solid transparent;\n transform-origin: 0 0;\n transition: opacity 0.1s ease-in-out, transform 0.1s ease-in-out;\n}\n@media (prefers-reduced-motion: reduce) {\n .form-floating > label {\n transition: none;\n }\n}\n.form-floating > .form-control {\n padding: 1rem 0.75rem;\n}\n.form-floating > .form-control::placeholder {\n color: transparent;\n}\n.form-floating > .form-control:focus, .form-floating > .form-control:not(:placeholder-shown) {\n padding-top: 1.625rem;\n padding-bottom: 0.625rem;\n}\n.form-floating > .form-control:-webkit-autofill {\n padding-top: 1.625rem;\n padding-bottom: 0.625rem;\n}\n.form-floating > .form-select {\n padding-top: 1.625rem;\n padding-bottom: 0.625rem;\n}\n.form-floating > .form-control:focus ~ label,\n.form-floating > .form-control:not(:placeholder-shown) ~ label,\n.form-floating > .form-select ~ label {\n opacity: 0.65;\n transform: scale(0.85) translateY(-0.5rem) translateX(0.15rem);\n}\n.form-floating > .form-control:-webkit-autofill ~ label {\n opacity: 0.65;\n transform: scale(0.85) translateY(-0.5rem) translateX(0.15rem);\n}\n\n.input-group {\n position: relative;\n display: flex;\n flex-wrap: wrap;\n align-items: stretch;\n width: 100%;\n}\n.input-group > .form-control,\n.input-group > .form-select {\n position: relative;\n flex: 1 1 auto;\n width: 1%;\n min-width: 0;\n}\n.input-group > .form-control:focus,\n.input-group > .form-select:focus {\n z-index: 3;\n}\n.input-group .btn {\n position: relative;\n z-index: 2;\n}\n.input-group .btn:focus {\n z-index: 3;\n}\n\n.input-group-text {\n display: flex;\n align-items: center;\n padding: 0.375rem 0.75rem;\n font-size: 1rem;\n font-weight: 400;\n line-height: 1.5;\n color: #212529;\n text-align: center;\n white-space: nowrap;\n background-color: #e9ecef;\n border: 1px solid #ced4da;\n border-radius: 0.25rem;\n}\n\n.input-group-lg > .form-control,\n.input-group-lg > .form-select,\n.input-group-lg > .input-group-text,\n.input-group-lg > .btn {\n padding: 0.5rem 1rem;\n font-size: 1.25rem;\n border-radius: 0.3rem;\n}\n\n.input-group-sm > .form-control,\n.input-group-sm > .form-select,\n.input-group-sm > .input-group-text,\n.input-group-sm > .btn {\n padding: 0.25rem 0.5rem;\n font-size: 0.875rem;\n border-radius: 0.2rem;\n}\n\n.input-group-lg > .form-select,\n.input-group-sm > .form-select {\n padding-right: 3rem;\n}\n\n.input-group:not(.has-validation) > :not(:last-child):not(.dropdown-toggle):not(.dropdown-menu),\n.input-group:not(.has-validation) > .dropdown-toggle:nth-last-child(n+3) {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n}\n.input-group.has-validation > :nth-last-child(n+3):not(.dropdown-toggle):not(.dropdown-menu),\n.input-group.has-validation > .dropdown-toggle:nth-last-child(n+4) {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n}\n.input-group > :not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback) {\n margin-left: -1px;\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n}\n\n.valid-feedback {\n display: none;\n width: 100%;\n margin-top: 0.25rem;\n font-size: 0.875em;\n color: #198754;\n}\n\n.valid-tooltip {\n position: absolute;\n top: 100%;\n z-index: 5;\n display: none;\n max-width: 100%;\n padding: 0.25rem 0.5rem;\n margin-top: 0.1rem;\n font-size: 0.875rem;\n color: #fff;\n background-color: rgba(25, 135, 84, 0.9);\n border-radius: 0.25rem;\n}\n\n.was-validated :valid ~ .valid-feedback,\n.was-validated :valid ~ .valid-tooltip,\n.is-valid ~ .valid-feedback,\n.is-valid ~ .valid-tooltip {\n display: block;\n}\n\n.was-validated .form-control:valid, .form-control.is-valid {\n border-color: #198754;\n padding-right: calc(1.5em + 0.75rem);\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e\");\n background-repeat: no-repeat;\n background-position: right calc(0.375em + 0.1875rem) center;\n background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);\n}\n.was-validated .form-control:valid:focus, .form-control.is-valid:focus {\n border-color: #198754;\n box-shadow: 0 0 0 0.25rem rgba(25, 135, 84, 0.25);\n}\n\n.was-validated textarea.form-control:valid, textarea.form-control.is-valid {\n padding-right: calc(1.5em + 0.75rem);\n background-position: top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem);\n}\n\n.was-validated .form-select:valid, .form-select.is-valid {\n border-color: #198754;\n}\n.was-validated .form-select:valid:not([multiple]):not([size]), .was-validated .form-select:valid:not([multiple])[size=\"1\"], .form-select.is-valid:not([multiple]):not([size]), .form-select.is-valid:not([multiple])[size=\"1\"] {\n padding-right: 4.125rem;\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e\"), url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e\");\n background-position: right 0.75rem center, center right 2.25rem;\n background-size: 16px 12px, calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);\n}\n.was-validated .form-select:valid:focus, .form-select.is-valid:focus {\n border-color: #198754;\n box-shadow: 0 0 0 0.25rem rgba(25, 135, 84, 0.25);\n}\n\n.was-validated .form-check-input:valid, .form-check-input.is-valid {\n border-color: #198754;\n}\n.was-validated .form-check-input:valid:checked, .form-check-input.is-valid:checked {\n background-color: #198754;\n}\n.was-validated .form-check-input:valid:focus, .form-check-input.is-valid:focus {\n box-shadow: 0 0 0 0.25rem rgba(25, 135, 84, 0.25);\n}\n.was-validated .form-check-input:valid ~ .form-check-label, .form-check-input.is-valid ~ .form-check-label {\n color: #198754;\n}\n\n.form-check-inline .form-check-input ~ .valid-feedback {\n margin-left: 0.5em;\n}\n\n.was-validated .input-group .form-control:valid, .input-group .form-control.is-valid,\n.was-validated .input-group .form-select:valid,\n.input-group .form-select.is-valid {\n z-index: 1;\n}\n.was-validated .input-group .form-control:valid:focus, .input-group .form-control.is-valid:focus,\n.was-validated .input-group .form-select:valid:focus,\n.input-group .form-select.is-valid:focus {\n z-index: 3;\n}\n\n.invalid-feedback {\n display: none;\n width: 100%;\n margin-top: 0.25rem;\n font-size: 0.875em;\n color: #dc3545;\n}\n\n.invalid-tooltip {\n position: absolute;\n top: 100%;\n z-index: 5;\n display: none;\n max-width: 100%;\n padding: 0.25rem 0.5rem;\n margin-top: 0.1rem;\n font-size: 0.875rem;\n color: #fff;\n background-color: rgba(220, 53, 69, 0.9);\n border-radius: 0.25rem;\n}\n\n.was-validated :invalid ~ .invalid-feedback,\n.was-validated :invalid ~ .invalid-tooltip,\n.is-invalid ~ .invalid-feedback,\n.is-invalid ~ .invalid-tooltip {\n display: block;\n}\n\n.was-validated .form-control:invalid, .form-control.is-invalid {\n border-color: #dc3545;\n padding-right: calc(1.5em + 0.75rem);\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e\");\n background-repeat: no-repeat;\n background-position: right calc(0.375em + 0.1875rem) center;\n background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);\n}\n.was-validated .form-control:invalid:focus, .form-control.is-invalid:focus {\n border-color: #dc3545;\n box-shadow: 0 0 0 0.25rem rgba(220, 53, 69, 0.25);\n}\n\n.was-validated textarea.form-control:invalid, textarea.form-control.is-invalid {\n padding-right: calc(1.5em + 0.75rem);\n background-position: top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem);\n}\n\n.was-validated .form-select:invalid, .form-select.is-invalid {\n border-color: #dc3545;\n}\n.was-validated .form-select:invalid:not([multiple]):not([size]), .was-validated .form-select:invalid:not([multiple])[size=\"1\"], .form-select.is-invalid:not([multiple]):not([size]), .form-select.is-invalid:not([multiple])[size=\"1\"] {\n padding-right: 4.125rem;\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e\"), url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e\");\n background-position: right 0.75rem center, center right 2.25rem;\n background-size: 16px 12px, calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);\n}\n.was-validated .form-select:invalid:focus, .form-select.is-invalid:focus {\n border-color: #dc3545;\n box-shadow: 0 0 0 0.25rem rgba(220, 53, 69, 0.25);\n}\n\n.was-validated .form-check-input:invalid, .form-check-input.is-invalid {\n border-color: #dc3545;\n}\n.was-validated .form-check-input:invalid:checked, .form-check-input.is-invalid:checked {\n background-color: #dc3545;\n}\n.was-validated .form-check-input:invalid:focus, .form-check-input.is-invalid:focus {\n box-shadow: 0 0 0 0.25rem rgba(220, 53, 69, 0.25);\n}\n.was-validated .form-check-input:invalid ~ .form-check-label, .form-check-input.is-invalid ~ .form-check-label {\n color: #dc3545;\n}\n\n.form-check-inline .form-check-input ~ .invalid-feedback {\n margin-left: 0.5em;\n}\n\n.was-validated .input-group .form-control:invalid, .input-group .form-control.is-invalid,\n.was-validated .input-group .form-select:invalid,\n.input-group .form-select.is-invalid {\n z-index: 2;\n}\n.was-validated .input-group .form-control:invalid:focus, .input-group .form-control.is-invalid:focus,\n.was-validated .input-group .form-select:invalid:focus,\n.input-group .form-select.is-invalid:focus {\n z-index: 3;\n}\n\n.btn {\n display: inline-block;\n font-weight: 400;\n line-height: 1.5;\n color: #212529;\n text-align: center;\n text-decoration: none;\n vertical-align: middle;\n cursor: pointer;\n user-select: none;\n background-color: transparent;\n border: 1px solid transparent;\n padding: 0.375rem 0.75rem;\n font-size: 1rem;\n border-radius: 0.25rem;\n transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n}\n@media (prefers-reduced-motion: reduce) {\n .btn {\n transition: none;\n }\n}\n.btn:hover {\n color: #212529;\n}\n.btn-check:focus + .btn, .btn:focus {\n outline: 0;\n box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);\n}\n.btn:disabled, .btn.disabled, fieldset:disabled .btn {\n pointer-events: none;\n opacity: 0.65;\n}\n\n.btn-primary {\n color: #fff;\n background-color: #0d6efd;\n border-color: #0d6efd;\n}\n.btn-primary:hover {\n color: #fff;\n background-color: #0b5ed7;\n border-color: #0a58ca;\n}\n.btn-check:focus + .btn-primary, .btn-primary:focus {\n color: #fff;\n background-color: #0b5ed7;\n border-color: #0a58ca;\n box-shadow: 0 0 0 0.25rem rgba(49, 132, 253, 0.5);\n}\n.btn-check:checked + .btn-primary, .btn-check:active + .btn-primary, .btn-primary:active, .btn-primary.active, .show > .btn-primary.dropdown-toggle {\n color: #fff;\n background-color: #0a58ca;\n border-color: #0a53be;\n}\n.btn-check:checked + .btn-primary:focus, .btn-check:active + .btn-primary:focus, .btn-primary:active:focus, .btn-primary.active:focus, .show > .btn-primary.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.25rem rgba(49, 132, 253, 0.5);\n}\n.btn-primary:disabled, .btn-primary.disabled {\n color: #fff;\n background-color: #0d6efd;\n border-color: #0d6efd;\n}\n\n.btn-secondary {\n color: #fff;\n background-color: #6c757d;\n border-color: #6c757d;\n}\n.btn-secondary:hover {\n color: #fff;\n background-color: #5c636a;\n border-color: #565e64;\n}\n.btn-check:focus + .btn-secondary, .btn-secondary:focus {\n color: #fff;\n background-color: #5c636a;\n border-color: #565e64;\n box-shadow: 0 0 0 0.25rem rgba(130, 138, 145, 0.5);\n}\n.btn-check:checked + .btn-secondary, .btn-check:active + .btn-secondary, .btn-secondary:active, .btn-secondary.active, .show > .btn-secondary.dropdown-toggle {\n color: #fff;\n background-color: #565e64;\n border-color: #51585e;\n}\n.btn-check:checked + .btn-secondary:focus, .btn-check:active + .btn-secondary:focus, .btn-secondary:active:focus, .btn-secondary.active:focus, .show > .btn-secondary.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.25rem rgba(130, 138, 145, 0.5);\n}\n.btn-secondary:disabled, .btn-secondary.disabled {\n color: #fff;\n background-color: #6c757d;\n border-color: #6c757d;\n}\n\n.btn-success {\n color: #fff;\n background-color: #198754;\n border-color: #198754;\n}\n.btn-success:hover {\n color: #fff;\n background-color: #157347;\n border-color: #146c43;\n}\n.btn-check:focus + .btn-success, .btn-success:focus {\n color: #fff;\n background-color: #157347;\n border-color: #146c43;\n box-shadow: 0 0 0 0.25rem rgba(60, 153, 110, 0.5);\n}\n.btn-check:checked + .btn-success, .btn-check:active + .btn-success, .btn-success:active, .btn-success.active, .show > .btn-success.dropdown-toggle {\n color: #fff;\n background-color: #146c43;\n border-color: #13653f;\n}\n.btn-check:checked + .btn-success:focus, .btn-check:active + .btn-success:focus, .btn-success:active:focus, .btn-success.active:focus, .show > .btn-success.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.25rem rgba(60, 153, 110, 0.5);\n}\n.btn-success:disabled, .btn-success.disabled {\n color: #fff;\n background-color: #198754;\n border-color: #198754;\n}\n\n.btn-info {\n color: #000;\n background-color: #0dcaf0;\n border-color: #0dcaf0;\n}\n.btn-info:hover {\n color: #000;\n background-color: #31d2f2;\n border-color: #25cff2;\n}\n.btn-check:focus + .btn-info, .btn-info:focus {\n color: #000;\n background-color: #31d2f2;\n border-color: #25cff2;\n box-shadow: 0 0 0 0.25rem rgba(11, 172, 204, 0.5);\n}\n.btn-check:checked + .btn-info, .btn-check:active + .btn-info, .btn-info:active, .btn-info.active, .show > .btn-info.dropdown-toggle {\n color: #000;\n background-color: #3dd5f3;\n border-color: #25cff2;\n}\n.btn-check:checked + .btn-info:focus, .btn-check:active + .btn-info:focus, .btn-info:active:focus, .btn-info.active:focus, .show > .btn-info.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.25rem rgba(11, 172, 204, 0.5);\n}\n.btn-info:disabled, .btn-info.disabled {\n color: #000;\n background-color: #0dcaf0;\n border-color: #0dcaf0;\n}\n\n.btn-warning {\n color: #000;\n background-color: #ffc107;\n border-color: #ffc107;\n}\n.btn-warning:hover {\n color: #000;\n background-color: #ffca2c;\n border-color: #ffc720;\n}\n.btn-check:focus + .btn-warning, .btn-warning:focus {\n color: #000;\n background-color: #ffca2c;\n border-color: #ffc720;\n box-shadow: 0 0 0 0.25rem rgba(217, 164, 6, 0.5);\n}\n.btn-check:checked + .btn-warning, .btn-check:active + .btn-warning, .btn-warning:active, .btn-warning.active, .show > .btn-warning.dropdown-toggle {\n color: #000;\n background-color: #ffcd39;\n border-color: #ffc720;\n}\n.btn-check:checked + .btn-warning:focus, .btn-check:active + .btn-warning:focus, .btn-warning:active:focus, .btn-warning.active:focus, .show > .btn-warning.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.25rem rgba(217, 164, 6, 0.5);\n}\n.btn-warning:disabled, .btn-warning.disabled {\n color: #000;\n background-color: #ffc107;\n border-color: #ffc107;\n}\n\n.btn-danger {\n color: #fff;\n background-color: #dc3545;\n border-color: #dc3545;\n}\n.btn-danger:hover {\n color: #fff;\n background-color: #bb2d3b;\n border-color: #b02a37;\n}\n.btn-check:focus + .btn-danger, .btn-danger:focus {\n color: #fff;\n background-color: #bb2d3b;\n border-color: #b02a37;\n box-shadow: 0 0 0 0.25rem rgba(225, 83, 97, 0.5);\n}\n.btn-check:checked + .btn-danger, .btn-check:active + .btn-danger, .btn-danger:active, .btn-danger.active, .show > .btn-danger.dropdown-toggle {\n color: #fff;\n background-color: #b02a37;\n border-color: #a52834;\n}\n.btn-check:checked + .btn-danger:focus, .btn-check:active + .btn-danger:focus, .btn-danger:active:focus, .btn-danger.active:focus, .show > .btn-danger.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.25rem rgba(225, 83, 97, 0.5);\n}\n.btn-danger:disabled, .btn-danger.disabled {\n color: #fff;\n background-color: #dc3545;\n border-color: #dc3545;\n}\n\n.btn-light {\n color: #000;\n background-color: #f8f9fa;\n border-color: #f8f9fa;\n}\n.btn-light:hover {\n color: #000;\n background-color: #f9fafb;\n border-color: #f9fafb;\n}\n.btn-check:focus + .btn-light, .btn-light:focus {\n color: #000;\n background-color: #f9fafb;\n border-color: #f9fafb;\n box-shadow: 0 0 0 0.25rem rgba(211, 212, 213, 0.5);\n}\n.btn-check:checked + .btn-light, .btn-check:active + .btn-light, .btn-light:active, .btn-light.active, .show > .btn-light.dropdown-toggle {\n color: #000;\n background-color: #f9fafb;\n border-color: #f9fafb;\n}\n.btn-check:checked + .btn-light:focus, .btn-check:active + .btn-light:focus, .btn-light:active:focus, .btn-light.active:focus, .show > .btn-light.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.25rem rgba(211, 212, 213, 0.5);\n}\n.btn-light:disabled, .btn-light.disabled {\n color: #000;\n background-color: #f8f9fa;\n border-color: #f8f9fa;\n}\n\n.btn-dark {\n color: #fff;\n background-color: #212529;\n border-color: #212529;\n}\n.btn-dark:hover {\n color: #fff;\n background-color: #1c1f23;\n border-color: #1a1e21;\n}\n.btn-check:focus + .btn-dark, .btn-dark:focus {\n color: #fff;\n background-color: #1c1f23;\n border-color: #1a1e21;\n box-shadow: 0 0 0 0.25rem rgba(66, 70, 73, 0.5);\n}\n.btn-check:checked + .btn-dark, .btn-check:active + .btn-dark, .btn-dark:active, .btn-dark.active, .show > .btn-dark.dropdown-toggle {\n color: #fff;\n background-color: #1a1e21;\n border-color: #191c1f;\n}\n.btn-check:checked + .btn-dark:focus, .btn-check:active + .btn-dark:focus, .btn-dark:active:focus, .btn-dark.active:focus, .show > .btn-dark.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.25rem rgba(66, 70, 73, 0.5);\n}\n.btn-dark:disabled, .btn-dark.disabled {\n color: #fff;\n background-color: #212529;\n border-color: #212529;\n}\n\n.btn-outline-primary {\n color: #0d6efd;\n border-color: #0d6efd;\n}\n.btn-outline-primary:hover {\n color: #fff;\n background-color: #0d6efd;\n border-color: #0d6efd;\n}\n.btn-check:focus + .btn-outline-primary, .btn-outline-primary:focus {\n box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.5);\n}\n.btn-check:checked + .btn-outline-primary, .btn-check:active + .btn-outline-primary, .btn-outline-primary:active, .btn-outline-primary.active, .btn-outline-primary.dropdown-toggle.show {\n color: #fff;\n background-color: #0d6efd;\n border-color: #0d6efd;\n}\n.btn-check:checked + .btn-outline-primary:focus, .btn-check:active + .btn-outline-primary:focus, .btn-outline-primary:active:focus, .btn-outline-primary.active:focus, .btn-outline-primary.dropdown-toggle.show:focus {\n box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.5);\n}\n.btn-outline-primary:disabled, .btn-outline-primary.disabled {\n color: #0d6efd;\n background-color: transparent;\n}\n\n.btn-outline-secondary {\n color: #6c757d;\n border-color: #6c757d;\n}\n.btn-outline-secondary:hover {\n color: #fff;\n background-color: #6c757d;\n border-color: #6c757d;\n}\n.btn-check:focus + .btn-outline-secondary, .btn-outline-secondary:focus {\n box-shadow: 0 0 0 0.25rem rgba(108, 117, 125, 0.5);\n}\n.btn-check:checked + .btn-outline-secondary, .btn-check:active + .btn-outline-secondary, .btn-outline-secondary:active, .btn-outline-secondary.active, .btn-outline-secondary.dropdown-toggle.show {\n color: #fff;\n background-color: #6c757d;\n border-color: #6c757d;\n}\n.btn-check:checked + .btn-outline-secondary:focus, .btn-check:active + .btn-outline-secondary:focus, .btn-outline-secondary:active:focus, .btn-outline-secondary.active:focus, .btn-outline-secondary.dropdown-toggle.show:focus {\n box-shadow: 0 0 0 0.25rem rgba(108, 117, 125, 0.5);\n}\n.btn-outline-secondary:disabled, .btn-outline-secondary.disabled {\n color: #6c757d;\n background-color: transparent;\n}\n\n.btn-outline-success {\n color: #198754;\n border-color: #198754;\n}\n.btn-outline-success:hover {\n color: #fff;\n background-color: #198754;\n border-color: #198754;\n}\n.btn-check:focus + .btn-outline-success, .btn-outline-success:focus {\n box-shadow: 0 0 0 0.25rem rgba(25, 135, 84, 0.5);\n}\n.btn-check:checked + .btn-outline-success, .btn-check:active + .btn-outline-success, .btn-outline-success:active, .btn-outline-success.active, .btn-outline-success.dropdown-toggle.show {\n color: #fff;\n background-color: #198754;\n border-color: #198754;\n}\n.btn-check:checked + .btn-outline-success:focus, .btn-check:active + .btn-outline-success:focus, .btn-outline-success:active:focus, .btn-outline-success.active:focus, .btn-outline-success.dropdown-toggle.show:focus {\n box-shadow: 0 0 0 0.25rem rgba(25, 135, 84, 0.5);\n}\n.btn-outline-success:disabled, .btn-outline-success.disabled {\n color: #198754;\n background-color: transparent;\n}\n\n.btn-outline-info {\n color: #0dcaf0;\n border-color: #0dcaf0;\n}\n.btn-outline-info:hover {\n color: #000;\n background-color: #0dcaf0;\n border-color: #0dcaf0;\n}\n.btn-check:focus + .btn-outline-info, .btn-outline-info:focus {\n box-shadow: 0 0 0 0.25rem rgba(13, 202, 240, 0.5);\n}\n.btn-check:checked + .btn-outline-info, .btn-check:active + .btn-outline-info, .btn-outline-info:active, .btn-outline-info.active, .btn-outline-info.dropdown-toggle.show {\n color: #000;\n background-color: #0dcaf0;\n border-color: #0dcaf0;\n}\n.btn-check:checked + .btn-outline-info:focus, .btn-check:active + .btn-outline-info:focus, .btn-outline-info:active:focus, .btn-outline-info.active:focus, .btn-outline-info.dropdown-toggle.show:focus {\n box-shadow: 0 0 0 0.25rem rgba(13, 202, 240, 0.5);\n}\n.btn-outline-info:disabled, .btn-outline-info.disabled {\n color: #0dcaf0;\n background-color: transparent;\n}\n\n.btn-outline-warning {\n color: #ffc107;\n border-color: #ffc107;\n}\n.btn-outline-warning:hover {\n color: #000;\n background-color: #ffc107;\n border-color: #ffc107;\n}\n.btn-check:focus + .btn-outline-warning, .btn-outline-warning:focus {\n box-shadow: 0 0 0 0.25rem rgba(255, 193, 7, 0.5);\n}\n.btn-check:checked + .btn-outline-warning, .btn-check:active + .btn-outline-warning, .btn-outline-warning:active, .btn-outline-warning.active, .btn-outline-warning.dropdown-toggle.show {\n color: #000;\n background-color: #ffc107;\n border-color: #ffc107;\n}\n.btn-check:checked + .btn-outline-warning:focus, .btn-check:active + .btn-outline-warning:focus, .btn-outline-warning:active:focus, .btn-outline-warning.active:focus, .btn-outline-warning.dropdown-toggle.show:focus {\n box-shadow: 0 0 0 0.25rem rgba(255, 193, 7, 0.5);\n}\n.btn-outline-warning:disabled, .btn-outline-warning.disabled {\n color: #ffc107;\n background-color: transparent;\n}\n\n.btn-outline-danger {\n color: #dc3545;\n border-color: #dc3545;\n}\n.btn-outline-danger:hover {\n color: #fff;\n background-color: #dc3545;\n border-color: #dc3545;\n}\n.btn-check:focus + .btn-outline-danger, .btn-outline-danger:focus {\n box-shadow: 0 0 0 0.25rem rgba(220, 53, 69, 0.5);\n}\n.btn-check:checked + .btn-outline-danger, .btn-check:active + .btn-outline-danger, .btn-outline-danger:active, .btn-outline-danger.active, .btn-outline-danger.dropdown-toggle.show {\n color: #fff;\n background-color: #dc3545;\n border-color: #dc3545;\n}\n.btn-check:checked + .btn-outline-danger:focus, .btn-check:active + .btn-outline-danger:focus, .btn-outline-danger:active:focus, .btn-outline-danger.active:focus, .btn-outline-danger.dropdown-toggle.show:focus {\n box-shadow: 0 0 0 0.25rem rgba(220, 53, 69, 0.5);\n}\n.btn-outline-danger:disabled, .btn-outline-danger.disabled {\n color: #dc3545;\n background-color: transparent;\n}\n\n.btn-outline-light {\n color: #f8f9fa;\n border-color: #f8f9fa;\n}\n.btn-outline-light:hover {\n color: #000;\n background-color: #f8f9fa;\n border-color: #f8f9fa;\n}\n.btn-check:focus + .btn-outline-light, .btn-outline-light:focus {\n box-shadow: 0 0 0 0.25rem rgba(248, 249, 250, 0.5);\n}\n.btn-check:checked + .btn-outline-light, .btn-check:active + .btn-outline-light, .btn-outline-light:active, .btn-outline-light.active, .btn-outline-light.dropdown-toggle.show {\n color: #000;\n background-color: #f8f9fa;\n border-color: #f8f9fa;\n}\n.btn-check:checked + .btn-outline-light:focus, .btn-check:active + .btn-outline-light:focus, .btn-outline-light:active:focus, .btn-outline-light.active:focus, .btn-outline-light.dropdown-toggle.show:focus {\n box-shadow: 0 0 0 0.25rem rgba(248, 249, 250, 0.5);\n}\n.btn-outline-light:disabled, .btn-outline-light.disabled {\n color: #f8f9fa;\n background-color: transparent;\n}\n\n.btn-outline-dark {\n color: #212529;\n border-color: #212529;\n}\n.btn-outline-dark:hover {\n color: #fff;\n background-color: #212529;\n border-color: #212529;\n}\n.btn-check:focus + .btn-outline-dark, .btn-outline-dark:focus {\n box-shadow: 0 0 0 0.25rem rgba(33, 37, 41, 0.5);\n}\n.btn-check:checked + .btn-outline-dark, .btn-check:active + .btn-outline-dark, .btn-outline-dark:active, .btn-outline-dark.active, .btn-outline-dark.dropdown-toggle.show {\n color: #fff;\n background-color: #212529;\n border-color: #212529;\n}\n.btn-check:checked + .btn-outline-dark:focus, .btn-check:active + .btn-outline-dark:focus, .btn-outline-dark:active:focus, .btn-outline-dark.active:focus, .btn-outline-dark.dropdown-toggle.show:focus {\n box-shadow: 0 0 0 0.25rem rgba(33, 37, 41, 0.5);\n}\n.btn-outline-dark:disabled, .btn-outline-dark.disabled {\n color: #212529;\n background-color: transparent;\n}\n\n.btn-link {\n font-weight: 400;\n color: #0d6efd;\n text-decoration: underline;\n}\n.btn-link:hover {\n color: #0a58ca;\n}\n.btn-link:disabled, .btn-link.disabled {\n color: #6c757d;\n}\n\n.btn-lg, .btn-group-lg > .btn {\n padding: 0.5rem 1rem;\n font-size: 1.25rem;\n border-radius: 0.3rem;\n}\n\n.btn-sm, .btn-group-sm > .btn {\n padding: 0.25rem 0.5rem;\n font-size: 0.875rem;\n border-radius: 0.2rem;\n}\n\n.fade {\n transition: opacity 0.15s linear;\n}\n@media (prefers-reduced-motion: reduce) {\n .fade {\n transition: none;\n }\n}\n.fade:not(.show) {\n opacity: 0;\n}\n\n.collapse:not(.show) {\n display: none;\n}\n\n.collapsing {\n height: 0;\n overflow: hidden;\n transition: height 0.35s ease;\n}\n@media (prefers-reduced-motion: reduce) {\n .collapsing {\n transition: none;\n }\n}\n\n.dropup,\n.dropend,\n.dropdown,\n.dropstart {\n position: relative;\n}\n\n.dropdown-toggle {\n white-space: nowrap;\n}\n.dropdown-toggle::after {\n display: inline-block;\n margin-left: 0.255em;\n vertical-align: 0.255em;\n content: \"\";\n border-top: 0.3em solid;\n border-right: 0.3em solid transparent;\n border-bottom: 0;\n border-left: 0.3em solid transparent;\n}\n.dropdown-toggle:empty::after {\n margin-left: 0;\n}\n\n.dropdown-menu {\n position: absolute;\n z-index: 1000;\n display: none;\n min-width: 10rem;\n padding: 0.5rem 0;\n margin: 0;\n font-size: 1rem;\n color: #212529;\n text-align: left;\n list-style: none;\n background-color: #fff;\n background-clip: padding-box;\n border: 1px solid rgba(0, 0, 0, 0.15);\n border-radius: 0.25rem;\n}\n.dropdown-menu[data-bs-popper] {\n top: 100%;\n left: 0;\n margin-top: 0.125rem;\n}\n\n.dropdown-menu-start {\n --bs-position: start;\n}\n.dropdown-menu-start[data-bs-popper] {\n right: auto;\n left: 0;\n}\n\n.dropdown-menu-end {\n --bs-position: end;\n}\n.dropdown-menu-end[data-bs-popper] {\n right: 0;\n left: auto;\n}\n\n@media (min-width: 576px) {\n .dropdown-menu-sm-start {\n --bs-position: start;\n }\n .dropdown-menu-sm-start[data-bs-popper] {\n right: auto;\n left: 0;\n }\n\n .dropdown-menu-sm-end {\n --bs-position: end;\n }\n .dropdown-menu-sm-end[data-bs-popper] {\n right: 0;\n left: auto;\n }\n}\n@media (min-width: 768px) {\n .dropdown-menu-md-start {\n --bs-position: start;\n }\n .dropdown-menu-md-start[data-bs-popper] {\n right: auto;\n left: 0;\n }\n\n .dropdown-menu-md-end {\n --bs-position: end;\n }\n .dropdown-menu-md-end[data-bs-popper] {\n right: 0;\n left: auto;\n }\n}\n@media (min-width: 992px) {\n .dropdown-menu-lg-start {\n --bs-position: start;\n }\n .dropdown-menu-lg-start[data-bs-popper] {\n right: auto;\n left: 0;\n }\n\n .dropdown-menu-lg-end {\n --bs-position: end;\n }\n .dropdown-menu-lg-end[data-bs-popper] {\n right: 0;\n left: auto;\n }\n}\n@media (min-width: 1200px) {\n .dropdown-menu-xl-start {\n --bs-position: start;\n }\n .dropdown-menu-xl-start[data-bs-popper] {\n right: auto;\n left: 0;\n }\n\n .dropdown-menu-xl-end {\n --bs-position: end;\n }\n .dropdown-menu-xl-end[data-bs-popper] {\n right: 0;\n left: auto;\n }\n}\n@media (min-width: 1400px) {\n .dropdown-menu-xxl-start {\n --bs-position: start;\n }\n .dropdown-menu-xxl-start[data-bs-popper] {\n right: auto;\n left: 0;\n }\n\n .dropdown-menu-xxl-end {\n --bs-position: end;\n }\n .dropdown-menu-xxl-end[data-bs-popper] {\n right: 0;\n left: auto;\n }\n}\n.dropup .dropdown-menu[data-bs-popper] {\n top: auto;\n bottom: 100%;\n margin-top: 0;\n margin-bottom: 0.125rem;\n}\n.dropup .dropdown-toggle::after {\n display: inline-block;\n margin-left: 0.255em;\n vertical-align: 0.255em;\n content: \"\";\n border-top: 0;\n border-right: 0.3em solid transparent;\n border-bottom: 0.3em solid;\n border-left: 0.3em solid transparent;\n}\n.dropup .dropdown-toggle:empty::after {\n margin-left: 0;\n}\n\n.dropend .dropdown-menu[data-bs-popper] {\n top: 0;\n right: auto;\n left: 100%;\n margin-top: 0;\n margin-left: 0.125rem;\n}\n.dropend .dropdown-toggle::after {\n display: inline-block;\n margin-left: 0.255em;\n vertical-align: 0.255em;\n content: \"\";\n border-top: 0.3em solid transparent;\n border-right: 0;\n border-bottom: 0.3em solid transparent;\n border-left: 0.3em solid;\n}\n.dropend .dropdown-toggle:empty::after {\n margin-left: 0;\n}\n.dropend .dropdown-toggle::after {\n vertical-align: 0;\n}\n\n.dropstart .dropdown-menu[data-bs-popper] {\n top: 0;\n right: 100%;\n left: auto;\n margin-top: 0;\n margin-right: 0.125rem;\n}\n.dropstart .dropdown-toggle::after {\n display: inline-block;\n margin-left: 0.255em;\n vertical-align: 0.255em;\n content: \"\";\n}\n.dropstart .dropdown-toggle::after {\n display: none;\n}\n.dropstart .dropdown-toggle::before {\n display: inline-block;\n margin-right: 0.255em;\n vertical-align: 0.255em;\n content: \"\";\n border-top: 0.3em solid transparent;\n border-right: 0.3em solid;\n border-bottom: 0.3em solid transparent;\n}\n.dropstart .dropdown-toggle:empty::after {\n margin-left: 0;\n}\n.dropstart .dropdown-toggle::before {\n vertical-align: 0;\n}\n\n.dropdown-divider {\n height: 0;\n margin: 0.5rem 0;\n overflow: hidden;\n border-top: 1px solid rgba(0, 0, 0, 0.15);\n}\n\n.dropdown-item {\n display: block;\n width: 100%;\n padding: 0.25rem 1rem;\n clear: both;\n font-weight: 400;\n color: #212529;\n text-align: inherit;\n text-decoration: none;\n white-space: nowrap;\n background-color: transparent;\n border: 0;\n}\n.dropdown-item:hover, .dropdown-item:focus {\n color: #1e2125;\n background-color: #e9ecef;\n}\n.dropdown-item.active, .dropdown-item:active {\n color: #fff;\n text-decoration: none;\n background-color: #0d6efd;\n}\n.dropdown-item.disabled, .dropdown-item:disabled {\n color: #adb5bd;\n pointer-events: none;\n background-color: transparent;\n}\n\n.dropdown-menu.show {\n display: block;\n}\n\n.dropdown-header {\n display: block;\n padding: 0.5rem 1rem;\n margin-bottom: 0;\n font-size: 0.875rem;\n color: #6c757d;\n white-space: nowrap;\n}\n\n.dropdown-item-text {\n display: block;\n padding: 0.25rem 1rem;\n color: #212529;\n}\n\n.dropdown-menu-dark {\n color: #dee2e6;\n background-color: #343a40;\n border-color: rgba(0, 0, 0, 0.15);\n}\n.dropdown-menu-dark .dropdown-item {\n color: #dee2e6;\n}\n.dropdown-menu-dark .dropdown-item:hover, .dropdown-menu-dark .dropdown-item:focus {\n color: #fff;\n background-color: rgba(255, 255, 255, 0.15);\n}\n.dropdown-menu-dark .dropdown-item.active, .dropdown-menu-dark .dropdown-item:active {\n color: #fff;\n background-color: #0d6efd;\n}\n.dropdown-menu-dark .dropdown-item.disabled, .dropdown-menu-dark .dropdown-item:disabled {\n color: #adb5bd;\n}\n.dropdown-menu-dark .dropdown-divider {\n border-color: rgba(0, 0, 0, 0.15);\n}\n.dropdown-menu-dark .dropdown-item-text {\n color: #dee2e6;\n}\n.dropdown-menu-dark .dropdown-header {\n color: #adb5bd;\n}\n\n.btn-group,\n.btn-group-vertical {\n position: relative;\n display: inline-flex;\n vertical-align: middle;\n}\n.btn-group > .btn,\n.btn-group-vertical > .btn {\n position: relative;\n flex: 1 1 auto;\n}\n.btn-group > .btn-check:checked + .btn,\n.btn-group > .btn-check:focus + .btn,\n.btn-group > .btn:hover,\n.btn-group > .btn:focus,\n.btn-group > .btn:active,\n.btn-group > .btn.active,\n.btn-group-vertical > .btn-check:checked + .btn,\n.btn-group-vertical > .btn-check:focus + .btn,\n.btn-group-vertical > .btn:hover,\n.btn-group-vertical > .btn:focus,\n.btn-group-vertical > .btn:active,\n.btn-group-vertical > .btn.active {\n z-index: 1;\n}\n\n.btn-toolbar {\n display: flex;\n flex-wrap: wrap;\n justify-content: flex-start;\n}\n.btn-toolbar .input-group {\n width: auto;\n}\n\n.btn-group > .btn:not(:first-child),\n.btn-group > .btn-group:not(:first-child) {\n margin-left: -1px;\n}\n.btn-group > .btn:not(:last-child):not(.dropdown-toggle),\n.btn-group > .btn-group:not(:last-child) > .btn {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n}\n.btn-group > .btn:nth-child(n+3),\n.btn-group > :not(.btn-check) + .btn,\n.btn-group > .btn-group:not(:first-child) > .btn {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n}\n\n.dropdown-toggle-split {\n padding-right: 0.5625rem;\n padding-left: 0.5625rem;\n}\n.dropdown-toggle-split::after, .dropup .dropdown-toggle-split::after, .dropend .dropdown-toggle-split::after {\n margin-left: 0;\n}\n.dropstart .dropdown-toggle-split::before {\n margin-right: 0;\n}\n\n.btn-sm + .dropdown-toggle-split, .btn-group-sm > .btn + .dropdown-toggle-split {\n padding-right: 0.375rem;\n padding-left: 0.375rem;\n}\n\n.btn-lg + .dropdown-toggle-split, .btn-group-lg > .btn + .dropdown-toggle-split {\n padding-right: 0.75rem;\n padding-left: 0.75rem;\n}\n\n.btn-group-vertical {\n flex-direction: column;\n align-items: flex-start;\n justify-content: center;\n}\n.btn-group-vertical > .btn,\n.btn-group-vertical > .btn-group {\n width: 100%;\n}\n.btn-group-vertical > .btn:not(:first-child),\n.btn-group-vertical > .btn-group:not(:first-child) {\n margin-top: -1px;\n}\n.btn-group-vertical > .btn:not(:last-child):not(.dropdown-toggle),\n.btn-group-vertical > .btn-group:not(:last-child) > .btn {\n border-bottom-right-radius: 0;\n border-bottom-left-radius: 0;\n}\n.btn-group-vertical > .btn ~ .btn,\n.btn-group-vertical > .btn-group:not(:first-child) > .btn {\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n}\n\n.nav {\n display: flex;\n flex-wrap: wrap;\n padding-left: 0;\n margin-bottom: 0;\n list-style: none;\n}\n\n.nav-link {\n display: block;\n padding: 0.5rem 1rem;\n color: #0d6efd;\n text-decoration: none;\n transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out;\n}\n@media (prefers-reduced-motion: reduce) {\n .nav-link {\n transition: none;\n }\n}\n.nav-link:hover, .nav-link:focus {\n color: #0a58ca;\n}\n.nav-link.disabled {\n color: #6c757d;\n pointer-events: none;\n cursor: default;\n}\n\n.nav-tabs {\n border-bottom: 1px solid #dee2e6;\n}\n.nav-tabs .nav-link {\n margin-bottom: -1px;\n background: none;\n border: 1px solid transparent;\n border-top-left-radius: 0.25rem;\n border-top-right-radius: 0.25rem;\n}\n.nav-tabs .nav-link:hover, .nav-tabs .nav-link:focus {\n border-color: #e9ecef #e9ecef #dee2e6;\n isolation: isolate;\n}\n.nav-tabs .nav-link.disabled {\n color: #6c757d;\n background-color: transparent;\n border-color: transparent;\n}\n.nav-tabs .nav-link.active,\n.nav-tabs .nav-item.show .nav-link {\n color: #495057;\n background-color: #fff;\n border-color: #dee2e6 #dee2e6 #fff;\n}\n.nav-tabs .dropdown-menu {\n margin-top: -1px;\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n}\n\n.nav-pills .nav-link {\n background: none;\n border: 0;\n border-radius: 0.25rem;\n}\n.nav-pills .nav-link.active,\n.nav-pills .show > .nav-link {\n color: #fff;\n background-color: #0d6efd;\n}\n\n.nav-fill > .nav-link,\n.nav-fill .nav-item {\n flex: 1 1 auto;\n text-align: center;\n}\n\n.nav-justified > .nav-link,\n.nav-justified .nav-item {\n flex-basis: 0;\n flex-grow: 1;\n text-align: center;\n}\n\n.nav-fill .nav-item .nav-link,\n.nav-justified .nav-item .nav-link {\n width: 100%;\n}\n\n.tab-content > .tab-pane {\n display: none;\n}\n.tab-content > .active {\n display: block;\n}\n\n.navbar {\n position: relative;\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n justify-content: space-between;\n padding-top: 0.5rem;\n padding-bottom: 0.5rem;\n}\n.navbar > .container,\n.navbar > .container-fluid,\n.navbar > .container-sm,\n.navbar > .container-md,\n.navbar > .container-lg,\n.navbar > .container-xl,\n.navbar > .container-xxl {\n display: flex;\n flex-wrap: inherit;\n align-items: center;\n justify-content: space-between;\n}\n.navbar-brand {\n padding-top: 0.3125rem;\n padding-bottom: 0.3125rem;\n margin-right: 1rem;\n font-size: 1.25rem;\n text-decoration: none;\n white-space: nowrap;\n}\n.navbar-nav {\n display: flex;\n flex-direction: column;\n padding-left: 0;\n margin-bottom: 0;\n list-style: none;\n}\n.navbar-nav .nav-link {\n padding-right: 0;\n padding-left: 0;\n}\n.navbar-nav .dropdown-menu {\n position: static;\n}\n\n.navbar-text {\n padding-top: 0.5rem;\n padding-bottom: 0.5rem;\n}\n\n.navbar-collapse {\n flex-basis: 100%;\n flex-grow: 1;\n align-items: center;\n}\n\n.navbar-toggler {\n padding: 0.25rem 0.75rem;\n font-size: 1.25rem;\n line-height: 1;\n background-color: transparent;\n border: 1px solid transparent;\n border-radius: 0.25rem;\n transition: box-shadow 0.15s ease-in-out;\n}\n@media (prefers-reduced-motion: reduce) {\n .navbar-toggler {\n transition: none;\n }\n}\n.navbar-toggler:hover {\n text-decoration: none;\n}\n.navbar-toggler:focus {\n text-decoration: none;\n outline: 0;\n box-shadow: 0 0 0 0.25rem;\n}\n\n.navbar-toggler-icon {\n display: inline-block;\n width: 1.5em;\n height: 1.5em;\n vertical-align: middle;\n background-repeat: no-repeat;\n background-position: center;\n background-size: 100%;\n}\n\n.navbar-nav-scroll {\n max-height: var(--bs-scroll-height, 75vh);\n overflow-y: auto;\n}\n\n@media (min-width: 576px) {\n .navbar-expand-sm {\n flex-wrap: nowrap;\n justify-content: flex-start;\n }\n .navbar-expand-sm .navbar-nav {\n flex-direction: row;\n }\n .navbar-expand-sm .navbar-nav .dropdown-menu {\n position: absolute;\n }\n .navbar-expand-sm .navbar-nav .nav-link {\n padding-right: 0.5rem;\n padding-left: 0.5rem;\n }\n .navbar-expand-sm .navbar-nav-scroll {\n overflow: visible;\n }\n .navbar-expand-sm .navbar-collapse {\n display: flex !important;\n flex-basis: auto;\n }\n .navbar-expand-sm .navbar-toggler {\n display: none;\n }\n}\n@media (min-width: 768px) {\n .navbar-expand-md {\n flex-wrap: nowrap;\n justify-content: flex-start;\n }\n .navbar-expand-md .navbar-nav {\n flex-direction: row;\n }\n .navbar-expand-md .navbar-nav .dropdown-menu {\n position: absolute;\n }\n .navbar-expand-md .navbar-nav .nav-link {\n padding-right: 0.5rem;\n padding-left: 0.5rem;\n }\n .navbar-expand-md .navbar-nav-scroll {\n overflow: visible;\n }\n .navbar-expand-md .navbar-collapse {\n display: flex !important;\n flex-basis: auto;\n }\n .navbar-expand-md .navbar-toggler {\n display: none;\n }\n}\n@media (min-width: 992px) {\n .navbar-expand-lg {\n flex-wrap: nowrap;\n justify-content: flex-start;\n }\n .navbar-expand-lg .navbar-nav {\n flex-direction: row;\n }\n .navbar-expand-lg .navbar-nav .dropdown-menu {\n position: absolute;\n }\n .navbar-expand-lg .navbar-nav .nav-link {\n padding-right: 0.5rem;\n padding-left: 0.5rem;\n }\n .navbar-expand-lg .navbar-nav-scroll {\n overflow: visible;\n }\n .navbar-expand-lg .navbar-collapse {\n display: flex !important;\n flex-basis: auto;\n }\n .navbar-expand-lg .navbar-toggler {\n display: none;\n }\n}\n@media (min-width: 1200px) {\n .navbar-expand-xl {\n flex-wrap: nowrap;\n justify-content: flex-start;\n }\n .navbar-expand-xl .navbar-nav {\n flex-direction: row;\n }\n .navbar-expand-xl .navbar-nav .dropdown-menu {\n position: absolute;\n }\n .navbar-expand-xl .navbar-nav .nav-link {\n padding-right: 0.5rem;\n padding-left: 0.5rem;\n }\n .navbar-expand-xl .navbar-nav-scroll {\n overflow: visible;\n }\n .navbar-expand-xl .navbar-collapse {\n display: flex !important;\n flex-basis: auto;\n }\n .navbar-expand-xl .navbar-toggler {\n display: none;\n }\n}\n@media (min-width: 1400px) {\n .navbar-expand-xxl {\n flex-wrap: nowrap;\n justify-content: flex-start;\n }\n .navbar-expand-xxl .navbar-nav {\n flex-direction: row;\n }\n .navbar-expand-xxl .navbar-nav .dropdown-menu {\n position: absolute;\n }\n .navbar-expand-xxl .navbar-nav .nav-link {\n padding-right: 0.5rem;\n padding-left: 0.5rem;\n }\n .navbar-expand-xxl .navbar-nav-scroll {\n overflow: visible;\n }\n .navbar-expand-xxl .navbar-collapse {\n display: flex !important;\n flex-basis: auto;\n }\n .navbar-expand-xxl .navbar-toggler {\n display: none;\n }\n}\n.navbar-expand {\n flex-wrap: nowrap;\n justify-content: flex-start;\n}\n.navbar-expand .navbar-nav {\n flex-direction: row;\n}\n.navbar-expand .navbar-nav .dropdown-menu {\n position: absolute;\n}\n.navbar-expand .navbar-nav .nav-link {\n padding-right: 0.5rem;\n padding-left: 0.5rem;\n}\n.navbar-expand .navbar-nav-scroll {\n overflow: visible;\n}\n.navbar-expand .navbar-collapse {\n display: flex !important;\n flex-basis: auto;\n}\n.navbar-expand .navbar-toggler {\n display: none;\n}\n\n.navbar-light .navbar-brand {\n color: rgba(0, 0, 0, 0.9);\n}\n.navbar-light .navbar-brand:hover, .navbar-light .navbar-brand:focus {\n color: rgba(0, 0, 0, 0.9);\n}\n.navbar-light .navbar-nav .nav-link {\n color: rgba(0, 0, 0, 0.55);\n}\n.navbar-light .navbar-nav .nav-link:hover, .navbar-light .navbar-nav .nav-link:focus {\n color: rgba(0, 0, 0, 0.7);\n}\n.navbar-light .navbar-nav .nav-link.disabled {\n color: rgba(0, 0, 0, 0.3);\n}\n.navbar-light .navbar-nav .show > .nav-link,\n.navbar-light .navbar-nav .nav-link.active {\n color: rgba(0, 0, 0, 0.9);\n}\n.navbar-light .navbar-toggler {\n color: rgba(0, 0, 0, 0.55);\n border-color: rgba(0, 0, 0, 0.1);\n}\n.navbar-light .navbar-toggler-icon {\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%280, 0, 0, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e\");\n}\n.navbar-light .navbar-text {\n color: rgba(0, 0, 0, 0.55);\n}\n.navbar-light .navbar-text a,\n.navbar-light .navbar-text a:hover,\n.navbar-light .navbar-text a:focus {\n color: rgba(0, 0, 0, 0.9);\n}\n\n.navbar-dark .navbar-brand {\n color: #fff;\n}\n.navbar-dark .navbar-brand:hover, .navbar-dark .navbar-brand:focus {\n color: #fff;\n}\n.navbar-dark .navbar-nav .nav-link {\n color: rgba(255, 255, 255, 0.55);\n}\n.navbar-dark .navbar-nav .nav-link:hover, .navbar-dark .navbar-nav .nav-link:focus {\n color: rgba(255, 255, 255, 0.75);\n}\n.navbar-dark .navbar-nav .nav-link.disabled {\n color: rgba(255, 255, 255, 0.25);\n}\n.navbar-dark .navbar-nav .show > .nav-link,\n.navbar-dark .navbar-nav .nav-link.active {\n color: #fff;\n}\n.navbar-dark .navbar-toggler {\n color: rgba(255, 255, 255, 0.55);\n border-color: rgba(255, 255, 255, 0.1);\n}\n.navbar-dark .navbar-toggler-icon {\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e\");\n}\n.navbar-dark .navbar-text {\n color: rgba(255, 255, 255, 0.55);\n}\n.navbar-dark .navbar-text a,\n.navbar-dark .navbar-text a:hover,\n.navbar-dark .navbar-text a:focus {\n color: #fff;\n}\n\n.card {\n position: relative;\n display: flex;\n flex-direction: column;\n min-width: 0;\n word-wrap: break-word;\n background-color: #fff;\n background-clip: border-box;\n border: 1px solid rgba(0, 0, 0, 0.125);\n border-radius: 0.25rem;\n}\n.card > hr {\n margin-right: 0;\n margin-left: 0;\n}\n.card > .list-group {\n border-top: inherit;\n border-bottom: inherit;\n}\n.card > .list-group:first-child {\n border-top-width: 0;\n border-top-left-radius: calc(0.25rem - 1px);\n border-top-right-radius: calc(0.25rem - 1px);\n}\n.card > .list-group:last-child {\n border-bottom-width: 0;\n border-bottom-right-radius: calc(0.25rem - 1px);\n border-bottom-left-radius: calc(0.25rem - 1px);\n}\n.card > .card-header + .list-group,\n.card > .list-group + .card-footer {\n border-top: 0;\n}\n\n.card-body {\n flex: 1 1 auto;\n padding: 1rem 1rem;\n}\n\n.card-title {\n margin-bottom: 0.5rem;\n}\n\n.card-subtitle {\n margin-top: -0.25rem;\n margin-bottom: 0;\n}\n\n.card-text:last-child {\n margin-bottom: 0;\n}\n\n.card-link:hover {\n text-decoration: none;\n}\n.card-link + .card-link {\n margin-left: 1rem;\n}\n\n.card-header {\n padding: 0.5rem 1rem;\n margin-bottom: 0;\n background-color: rgba(0, 0, 0, 0.03);\n border-bottom: 1px solid rgba(0, 0, 0, 0.125);\n}\n.card-header:first-child {\n border-radius: calc(0.25rem - 1px) calc(0.25rem - 1px) 0 0;\n}\n\n.card-footer {\n padding: 0.5rem 1rem;\n background-color: rgba(0, 0, 0, 0.03);\n border-top: 1px solid rgba(0, 0, 0, 0.125);\n}\n.card-footer:last-child {\n border-radius: 0 0 calc(0.25rem - 1px) calc(0.25rem - 1px);\n}\n\n.card-header-tabs {\n margin-right: -0.5rem;\n margin-bottom: -0.5rem;\n margin-left: -0.5rem;\n border-bottom: 0;\n}\n\n.card-header-pills {\n margin-right: -0.5rem;\n margin-left: -0.5rem;\n}\n\n.card-img-overlay {\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n padding: 1rem;\n border-radius: calc(0.25rem - 1px);\n}\n\n.card-img,\n.card-img-top,\n.card-img-bottom {\n width: 100%;\n}\n\n.card-img,\n.card-img-top {\n border-top-left-radius: calc(0.25rem - 1px);\n border-top-right-radius: calc(0.25rem - 1px);\n}\n\n.card-img,\n.card-img-bottom {\n border-bottom-right-radius: calc(0.25rem - 1px);\n border-bottom-left-radius: calc(0.25rem - 1px);\n}\n\n.card-group > .card {\n margin-bottom: 0.75rem;\n}\n@media (min-width: 576px) {\n .card-group {\n display: flex;\n flex-flow: row wrap;\n }\n .card-group > .card {\n flex: 1 0 0%;\n margin-bottom: 0;\n }\n .card-group > .card + .card {\n margin-left: 0;\n border-left: 0;\n }\n .card-group > .card:not(:last-child) {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n }\n .card-group > .card:not(:last-child) .card-img-top,\n.card-group > .card:not(:last-child) .card-header {\n border-top-right-radius: 0;\n }\n .card-group > .card:not(:last-child) .card-img-bottom,\n.card-group > .card:not(:last-child) .card-footer {\n border-bottom-right-radius: 0;\n }\n .card-group > .card:not(:first-child) {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n }\n .card-group > .card:not(:first-child) .card-img-top,\n.card-group > .card:not(:first-child) .card-header {\n border-top-left-radius: 0;\n }\n .card-group > .card:not(:first-child) .card-img-bottom,\n.card-group > .card:not(:first-child) .card-footer {\n border-bottom-left-radius: 0;\n }\n}\n\n.accordion-button {\n position: relative;\n display: flex;\n align-items: center;\n width: 100%;\n padding: 1rem 1.25rem;\n font-size: 1rem;\n color: #212529;\n text-align: left;\n background-color: #fff;\n border: 0;\n border-radius: 0;\n overflow-anchor: none;\n transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, border-radius 0.15s ease;\n}\n@media (prefers-reduced-motion: reduce) {\n .accordion-button {\n transition: none;\n }\n}\n.accordion-button:not(.collapsed) {\n color: #0c63e4;\n background-color: #e7f1ff;\n box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.125);\n}\n.accordion-button:not(.collapsed)::after {\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%230c63e4'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e\");\n transform: rotate(-180deg);\n}\n.accordion-button::after {\n flex-shrink: 0;\n width: 1.25rem;\n height: 1.25rem;\n margin-left: auto;\n content: \"\";\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23212529'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e\");\n background-repeat: no-repeat;\n background-size: 1.25rem;\n transition: transform 0.2s ease-in-out;\n}\n@media (prefers-reduced-motion: reduce) {\n .accordion-button::after {\n transition: none;\n }\n}\n.accordion-button:hover {\n z-index: 2;\n}\n.accordion-button:focus {\n z-index: 3;\n border-color: #86b7fe;\n outline: 0;\n box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);\n}\n\n.accordion-header {\n margin-bottom: 0;\n}\n\n.accordion-item {\n background-color: #fff;\n border: 1px solid rgba(0, 0, 0, 0.125);\n}\n.accordion-item:first-of-type {\n border-top-left-radius: 0.25rem;\n border-top-right-radius: 0.25rem;\n}\n.accordion-item:first-of-type .accordion-button {\n border-top-left-radius: calc(0.25rem - 1px);\n border-top-right-radius: calc(0.25rem - 1px);\n}\n.accordion-item:not(:first-of-type) {\n border-top: 0;\n}\n.accordion-item:last-of-type {\n border-bottom-right-radius: 0.25rem;\n border-bottom-left-radius: 0.25rem;\n}\n.accordion-item:last-of-type .accordion-button.collapsed {\n border-bottom-right-radius: calc(0.25rem - 1px);\n border-bottom-left-radius: calc(0.25rem - 1px);\n}\n.accordion-item:last-of-type .accordion-collapse {\n border-bottom-right-radius: 0.25rem;\n border-bottom-left-radius: 0.25rem;\n}\n\n.accordion-body {\n padding: 1rem 1.25rem;\n}\n\n.accordion-flush .accordion-collapse {\n border-width: 0;\n}\n.accordion-flush .accordion-item {\n border-right: 0;\n border-left: 0;\n border-radius: 0;\n}\n.accordion-flush .accordion-item:first-child {\n border-top: 0;\n}\n.accordion-flush .accordion-item:last-child {\n border-bottom: 0;\n}\n.accordion-flush .accordion-item .accordion-button {\n border-radius: 0;\n}\n\n.breadcrumb {\n display: flex;\n flex-wrap: wrap;\n padding: 0 0;\n margin-bottom: 1rem;\n list-style: none;\n}\n\n.breadcrumb-item + .breadcrumb-item {\n padding-left: 0.5rem;\n}\n.breadcrumb-item + .breadcrumb-item::before {\n float: left;\n padding-right: 0.5rem;\n color: #6c757d;\n content: var(--bs-breadcrumb-divider, \"/\") /* rtl: var(--bs-breadcrumb-divider, \"/\") */;\n}\n.breadcrumb-item.active {\n color: #6c757d;\n}\n\n.pagination {\n display: flex;\n padding-left: 0;\n list-style: none;\n}\n\n.page-link {\n position: relative;\n display: block;\n color: #0d6efd;\n text-decoration: none;\n background-color: #fff;\n border: 1px solid #dee2e6;\n transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n}\n@media (prefers-reduced-motion: reduce) {\n .page-link {\n transition: none;\n }\n}\n.page-link:hover {\n z-index: 2;\n color: #0a58ca;\n background-color: #e9ecef;\n border-color: #dee2e6;\n}\n.page-link:focus {\n z-index: 3;\n color: #0a58ca;\n background-color: #e9ecef;\n outline: 0;\n box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);\n}\n\n.page-item:not(:first-child) .page-link {\n margin-left: -1px;\n}\n.page-item.active .page-link {\n z-index: 3;\n color: #fff;\n background-color: #0d6efd;\n border-color: #0d6efd;\n}\n.page-item.disabled .page-link {\n color: #6c757d;\n pointer-events: none;\n background-color: #fff;\n border-color: #dee2e6;\n}\n\n.page-link {\n padding: 0.375rem 0.75rem;\n}\n\n.page-item:first-child .page-link {\n border-top-left-radius: 0.25rem;\n border-bottom-left-radius: 0.25rem;\n}\n.page-item:last-child .page-link {\n border-top-right-radius: 0.25rem;\n border-bottom-right-radius: 0.25rem;\n}\n\n.pagination-lg .page-link {\n padding: 0.75rem 1.5rem;\n font-size: 1.25rem;\n}\n.pagination-lg .page-item:first-child .page-link {\n border-top-left-radius: 0.3rem;\n border-bottom-left-radius: 0.3rem;\n}\n.pagination-lg .page-item:last-child .page-link {\n border-top-right-radius: 0.3rem;\n border-bottom-right-radius: 0.3rem;\n}\n\n.pagination-sm .page-link {\n padding: 0.25rem 0.5rem;\n font-size: 0.875rem;\n}\n.pagination-sm .page-item:first-child .page-link {\n border-top-left-radius: 0.2rem;\n border-bottom-left-radius: 0.2rem;\n}\n.pagination-sm .page-item:last-child .page-link {\n border-top-right-radius: 0.2rem;\n border-bottom-right-radius: 0.2rem;\n}\n\n.badge {\n display: inline-block;\n padding: 0.35em 0.65em;\n font-size: 0.75em;\n font-weight: 700;\n line-height: 1;\n color: #fff;\n text-align: center;\n white-space: nowrap;\n vertical-align: baseline;\n border-radius: 0.25rem;\n}\n.badge:empty {\n display: none;\n}\n\n.btn .badge {\n position: relative;\n top: -1px;\n}\n\n.alert {\n position: relative;\n padding: 1rem 1rem;\n margin-bottom: 1rem;\n border: 1px solid transparent;\n border-radius: 0.25rem;\n}\n\n.alert-heading {\n color: inherit;\n}\n\n.alert-link {\n font-weight: 700;\n}\n\n.alert-dismissible {\n padding-right: 3rem;\n}\n.alert-dismissible .btn-close {\n position: absolute;\n top: 0;\n right: 0;\n z-index: 2;\n padding: 1.25rem 1rem;\n}\n\n.alert-primary {\n color: #084298;\n background-color: #cfe2ff;\n border-color: #b6d4fe;\n}\n.alert-primary .alert-link {\n color: #06357a;\n}\n\n.alert-secondary {\n color: #41464b;\n background-color: #e2e3e5;\n border-color: #d3d6d8;\n}\n.alert-secondary .alert-link {\n color: #34383c;\n}\n\n.alert-success {\n color: #0f5132;\n background-color: #d1e7dd;\n border-color: #badbcc;\n}\n.alert-success .alert-link {\n color: #0c4128;\n}\n\n.alert-info {\n color: #055160;\n background-color: #cff4fc;\n border-color: #b6effb;\n}\n.alert-info .alert-link {\n color: #04414d;\n}\n\n.alert-warning {\n color: #664d03;\n background-color: #fff3cd;\n border-color: #ffecb5;\n}\n.alert-warning .alert-link {\n color: #523e02;\n}\n\n.alert-danger {\n color: #842029;\n background-color: #f8d7da;\n border-color: #f5c2c7;\n}\n.alert-danger .alert-link {\n color: #6a1a21;\n}\n\n.alert-light {\n color: #636464;\n background-color: #fefefe;\n border-color: #fdfdfe;\n}\n.alert-light .alert-link {\n color: #4f5050;\n}\n\n.alert-dark {\n color: #141619;\n background-color: #d3d3d4;\n border-color: #bcbebf;\n}\n.alert-dark .alert-link {\n color: #101214;\n}\n\n@keyframes progress-bar-stripes {\n 0% {\n background-position-x: 1rem;\n }\n}\n.progress {\n display: flex;\n height: 1rem;\n overflow: hidden;\n font-size: 0.75rem;\n background-color: #e9ecef;\n border-radius: 0.25rem;\n}\n\n.progress-bar {\n display: flex;\n flex-direction: column;\n justify-content: center;\n overflow: hidden;\n color: #fff;\n text-align: center;\n white-space: nowrap;\n background-color: #0d6efd;\n transition: width 0.6s ease;\n}\n@media (prefers-reduced-motion: reduce) {\n .progress-bar {\n transition: none;\n }\n}\n\n.progress-bar-striped {\n background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-size: 1rem 1rem;\n}\n\n.progress-bar-animated {\n animation: 1s linear infinite progress-bar-stripes;\n}\n@media (prefers-reduced-motion: reduce) {\n .progress-bar-animated {\n animation: none;\n }\n}\n\n.list-group {\n display: flex;\n flex-direction: column;\n padding-left: 0;\n margin-bottom: 0;\n border-radius: 0.25rem;\n}\n\n.list-group-numbered {\n list-style-type: none;\n counter-reset: section;\n}\n.list-group-numbered > li::before {\n content: counters(section, \".\") \". \";\n counter-increment: section;\n}\n\n.list-group-item-action {\n width: 100%;\n color: #495057;\n text-align: inherit;\n}\n.list-group-item-action:hover, .list-group-item-action:focus {\n z-index: 1;\n color: #495057;\n text-decoration: none;\n background-color: #f8f9fa;\n}\n.list-group-item-action:active {\n color: #212529;\n background-color: #e9ecef;\n}\n\n.list-group-item {\n position: relative;\n display: block;\n padding: 0.5rem 1rem;\n color: #212529;\n text-decoration: none;\n background-color: #fff;\n border: 1px solid rgba(0, 0, 0, 0.125);\n}\n.list-group-item:first-child {\n border-top-left-radius: inherit;\n border-top-right-radius: inherit;\n}\n.list-group-item:last-child {\n border-bottom-right-radius: inherit;\n border-bottom-left-radius: inherit;\n}\n.list-group-item.disabled, .list-group-item:disabled {\n color: #6c757d;\n pointer-events: none;\n background-color: #fff;\n}\n.list-group-item.active {\n z-index: 2;\n color: #fff;\n background-color: #0d6efd;\n border-color: #0d6efd;\n}\n.list-group-item + .list-group-item {\n border-top-width: 0;\n}\n.list-group-item + .list-group-item.active {\n margin-top: -1px;\n border-top-width: 1px;\n}\n\n.list-group-horizontal {\n flex-direction: row;\n}\n.list-group-horizontal > .list-group-item:first-child {\n border-bottom-left-radius: 0.25rem;\n border-top-right-radius: 0;\n}\n.list-group-horizontal > .list-group-item:last-child {\n border-top-right-radius: 0.25rem;\n border-bottom-left-radius: 0;\n}\n.list-group-horizontal > .list-group-item.active {\n margin-top: 0;\n}\n.list-group-horizontal > .list-group-item + .list-group-item {\n border-top-width: 1px;\n border-left-width: 0;\n}\n.list-group-horizontal > .list-group-item + .list-group-item.active {\n margin-left: -1px;\n border-left-width: 1px;\n}\n\n@media (min-width: 576px) {\n .list-group-horizontal-sm {\n flex-direction: row;\n }\n .list-group-horizontal-sm > .list-group-item:first-child {\n border-bottom-left-radius: 0.25rem;\n border-top-right-radius: 0;\n }\n .list-group-horizontal-sm > .list-group-item:last-child {\n border-top-right-radius: 0.25rem;\n border-bottom-left-radius: 0;\n }\n .list-group-horizontal-sm > .list-group-item.active {\n margin-top: 0;\n }\n .list-group-horizontal-sm > .list-group-item + .list-group-item {\n border-top-width: 1px;\n border-left-width: 0;\n }\n .list-group-horizontal-sm > .list-group-item + .list-group-item.active {\n margin-left: -1px;\n border-left-width: 1px;\n }\n}\n@media (min-width: 768px) {\n .list-group-horizontal-md {\n flex-direction: row;\n }\n .list-group-horizontal-md > .list-group-item:first-child {\n border-bottom-left-radius: 0.25rem;\n border-top-right-radius: 0;\n }\n .list-group-horizontal-md > .list-group-item:last-child {\n border-top-right-radius: 0.25rem;\n border-bottom-left-radius: 0;\n }\n .list-group-horizontal-md > .list-group-item.active {\n margin-top: 0;\n }\n .list-group-horizontal-md > .list-group-item + .list-group-item {\n border-top-width: 1px;\n border-left-width: 0;\n }\n .list-group-horizontal-md > .list-group-item + .list-group-item.active {\n margin-left: -1px;\n border-left-width: 1px;\n }\n}\n@media (min-width: 992px) {\n .list-group-horizontal-lg {\n flex-direction: row;\n }\n .list-group-horizontal-lg > .list-group-item:first-child {\n border-bottom-left-radius: 0.25rem;\n border-top-right-radius: 0;\n }\n .list-group-horizontal-lg > .list-group-item:last-child {\n border-top-right-radius: 0.25rem;\n border-bottom-left-radius: 0;\n }\n .list-group-horizontal-lg > .list-group-item.active {\n margin-top: 0;\n }\n .list-group-horizontal-lg > .list-group-item + .list-group-item {\n border-top-width: 1px;\n border-left-width: 0;\n }\n .list-group-horizontal-lg > .list-group-item + .list-group-item.active {\n margin-left: -1px;\n border-left-width: 1px;\n }\n}\n@media (min-width: 1200px) {\n .list-group-horizontal-xl {\n flex-direction: row;\n }\n .list-group-horizontal-xl > .list-group-item:first-child {\n border-bottom-left-radius: 0.25rem;\n border-top-right-radius: 0;\n }\n .list-group-horizontal-xl > .list-group-item:last-child {\n border-top-right-radius: 0.25rem;\n border-bottom-left-radius: 0;\n }\n .list-group-horizontal-xl > .list-group-item.active {\n margin-top: 0;\n }\n .list-group-horizontal-xl > .list-group-item + .list-group-item {\n border-top-width: 1px;\n border-left-width: 0;\n }\n .list-group-horizontal-xl > .list-group-item + .list-group-item.active {\n margin-left: -1px;\n border-left-width: 1px;\n }\n}\n@media (min-width: 1400px) {\n .list-group-horizontal-xxl {\n flex-direction: row;\n }\n .list-group-horizontal-xxl > .list-group-item:first-child {\n border-bottom-left-radius: 0.25rem;\n border-top-right-radius: 0;\n }\n .list-group-horizontal-xxl > .list-group-item:last-child {\n border-top-right-radius: 0.25rem;\n border-bottom-left-radius: 0;\n }\n .list-group-horizontal-xxl > .list-group-item.active {\n margin-top: 0;\n }\n .list-group-horizontal-xxl > .list-group-item + .list-group-item {\n border-top-width: 1px;\n border-left-width: 0;\n }\n .list-group-horizontal-xxl > .list-group-item + .list-group-item.active {\n margin-left: -1px;\n border-left-width: 1px;\n }\n}\n.list-group-flush {\n border-radius: 0;\n}\n.list-group-flush > .list-group-item {\n border-width: 0 0 1px;\n}\n.list-group-flush > .list-group-item:last-child {\n border-bottom-width: 0;\n}\n\n.list-group-item-primary {\n color: #084298;\n background-color: #cfe2ff;\n}\n.list-group-item-primary.list-group-item-action:hover, .list-group-item-primary.list-group-item-action:focus {\n color: #084298;\n background-color: #bacbe6;\n}\n.list-group-item-primary.list-group-item-action.active {\n color: #fff;\n background-color: #084298;\n border-color: #084298;\n}\n\n.list-group-item-secondary {\n color: #41464b;\n background-color: #e2e3e5;\n}\n.list-group-item-secondary.list-group-item-action:hover, .list-group-item-secondary.list-group-item-action:focus {\n color: #41464b;\n background-color: #cbccce;\n}\n.list-group-item-secondary.list-group-item-action.active {\n color: #fff;\n background-color: #41464b;\n border-color: #41464b;\n}\n\n.list-group-item-success {\n color: #0f5132;\n background-color: #d1e7dd;\n}\n.list-group-item-success.list-group-item-action:hover, .list-group-item-success.list-group-item-action:focus {\n color: #0f5132;\n background-color: #bcd0c7;\n}\n.list-group-item-success.list-group-item-action.active {\n color: #fff;\n background-color: #0f5132;\n border-color: #0f5132;\n}\n\n.list-group-item-info {\n color: #055160;\n background-color: #cff4fc;\n}\n.list-group-item-info.list-group-item-action:hover, .list-group-item-info.list-group-item-action:focus {\n color: #055160;\n background-color: #badce3;\n}\n.list-group-item-info.list-group-item-action.active {\n color: #fff;\n background-color: #055160;\n border-color: #055160;\n}\n\n.list-group-item-warning {\n color: #664d03;\n background-color: #fff3cd;\n}\n.list-group-item-warning.list-group-item-action:hover, .list-group-item-warning.list-group-item-action:focus {\n color: #664d03;\n background-color: #e6dbb9;\n}\n.list-group-item-warning.list-group-item-action.active {\n color: #fff;\n background-color: #664d03;\n border-color: #664d03;\n}\n\n.list-group-item-danger {\n color: #842029;\n background-color: #f8d7da;\n}\n.list-group-item-danger.list-group-item-action:hover, .list-group-item-danger.list-group-item-action:focus {\n color: #842029;\n background-color: #dfc2c4;\n}\n.list-group-item-danger.list-group-item-action.active {\n color: #fff;\n background-color: #842029;\n border-color: #842029;\n}\n\n.list-group-item-light {\n color: #636464;\n background-color: #fefefe;\n}\n.list-group-item-light.list-group-item-action:hover, .list-group-item-light.list-group-item-action:focus {\n color: #636464;\n background-color: #e5e5e5;\n}\n.list-group-item-light.list-group-item-action.active {\n color: #fff;\n background-color: #636464;\n border-color: #636464;\n}\n\n.list-group-item-dark {\n color: #141619;\n background-color: #d3d3d4;\n}\n.list-group-item-dark.list-group-item-action:hover, .list-group-item-dark.list-group-item-action:focus {\n color: #141619;\n background-color: #bebebf;\n}\n.list-group-item-dark.list-group-item-action.active {\n color: #fff;\n background-color: #141619;\n border-color: #141619;\n}\n\n.btn-close {\n box-sizing: content-box;\n width: 1em;\n height: 1em;\n padding: 0.25em 0.25em;\n color: #000;\n background: transparent url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e\") center/1em auto no-repeat;\n border: 0;\n border-radius: 0.25rem;\n opacity: 0.5;\n}\n.btn-close:hover {\n color: #000;\n text-decoration: none;\n opacity: 0.75;\n}\n.btn-close:focus {\n outline: 0;\n box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);\n opacity: 1;\n}\n.btn-close:disabled, .btn-close.disabled {\n pointer-events: none;\n user-select: none;\n opacity: 0.25;\n}\n\n.btn-close-white {\n filter: invert(1) grayscale(100%) brightness(200%);\n}\n\n.toast {\n width: 350px;\n max-width: 100%;\n font-size: 0.875rem;\n pointer-events: auto;\n background-color: rgba(255, 255, 255, 0.85);\n background-clip: padding-box;\n border: 1px solid rgba(0, 0, 0, 0.1);\n box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);\n border-radius: 0.25rem;\n}\n.toast:not(.showing):not(.show) {\n opacity: 0;\n}\n.toast.hide {\n display: none;\n}\n\n.toast-container {\n width: max-content;\n max-width: 100%;\n pointer-events: none;\n}\n.toast-container > :not(:last-child) {\n margin-bottom: 0.75rem;\n}\n\n.toast-header {\n display: flex;\n align-items: center;\n padding: 0.5rem 0.75rem;\n color: #6c757d;\n background-color: rgba(255, 255, 255, 0.85);\n background-clip: padding-box;\n border-bottom: 1px solid rgba(0, 0, 0, 0.05);\n border-top-left-radius: calc(0.25rem - 1px);\n border-top-right-radius: calc(0.25rem - 1px);\n}\n.toast-header .btn-close {\n margin-right: -0.375rem;\n margin-left: 0.75rem;\n}\n\n.toast-body {\n padding: 0.75rem;\n word-wrap: break-word;\n}\n\n.modal {\n position: fixed;\n top: 0;\n left: 0;\n z-index: 1060;\n display: none;\n width: 100%;\n height: 100%;\n overflow-x: hidden;\n overflow-y: auto;\n outline: 0;\n}\n\n.modal-dialog {\n position: relative;\n width: auto;\n margin: 0.5rem;\n pointer-events: none;\n}\n.modal.fade .modal-dialog {\n transition: transform 0.3s ease-out;\n transform: translate(0, -50px);\n}\n@media (prefers-reduced-motion: reduce) {\n .modal.fade .modal-dialog {\n transition: none;\n }\n}\n.modal.show .modal-dialog {\n transform: none;\n}\n.modal.modal-static .modal-dialog {\n transform: scale(1.02);\n}\n\n.modal-dialog-scrollable {\n height: calc(100% - 1rem);\n}\n.modal-dialog-scrollable .modal-content {\n max-height: 100%;\n overflow: hidden;\n}\n.modal-dialog-scrollable .modal-body {\n overflow-y: auto;\n}\n\n.modal-dialog-centered {\n display: flex;\n align-items: center;\n min-height: calc(100% - 1rem);\n}\n\n.modal-content {\n position: relative;\n display: flex;\n flex-direction: column;\n width: 100%;\n pointer-events: auto;\n background-color: #fff;\n background-clip: padding-box;\n border: 1px solid rgba(0, 0, 0, 0.2);\n border-radius: 0.3rem;\n outline: 0;\n}\n\n.modal-backdrop {\n position: fixed;\n top: 0;\n left: 0;\n z-index: 1040;\n width: 100vw;\n height: 100vh;\n background-color: #000;\n}\n.modal-backdrop.fade {\n opacity: 0;\n}\n.modal-backdrop.show {\n opacity: 0.5;\n}\n\n.modal-header {\n display: flex;\n flex-shrink: 0;\n align-items: center;\n justify-content: space-between;\n padding: 1rem 1rem;\n border-bottom: 1px solid #dee2e6;\n border-top-left-radius: calc(0.3rem - 1px);\n border-top-right-radius: calc(0.3rem - 1px);\n}\n.modal-header .btn-close {\n padding: 0.5rem 0.5rem;\n margin: -0.5rem -0.5rem -0.5rem auto;\n}\n\n.modal-title {\n margin-bottom: 0;\n line-height: 1.5;\n}\n\n.modal-body {\n position: relative;\n flex: 1 1 auto;\n padding: 1rem;\n}\n\n.modal-footer {\n display: flex;\n flex-wrap: wrap;\n flex-shrink: 0;\n align-items: center;\n justify-content: flex-end;\n padding: 0.75rem;\n border-top: 1px solid #dee2e6;\n border-bottom-right-radius: calc(0.3rem - 1px);\n border-bottom-left-radius: calc(0.3rem - 1px);\n}\n.modal-footer > * {\n margin: 0.25rem;\n}\n\n@media (min-width: 576px) {\n .modal-dialog {\n max-width: 500px;\n margin: 1.75rem auto;\n }\n\n .modal-dialog-scrollable {\n height: calc(100% - 3.5rem);\n }\n\n .modal-dialog-centered {\n min-height: calc(100% - 3.5rem);\n }\n\n .modal-sm {\n max-width: 300px;\n }\n}\n@media (min-width: 992px) {\n .modal-lg,\n.modal-xl {\n max-width: 800px;\n }\n}\n@media (min-width: 1200px) {\n .modal-xl {\n max-width: 1140px;\n }\n}\n.modal-fullscreen {\n width: 100vw;\n max-width: none;\n height: 100%;\n margin: 0;\n}\n.modal-fullscreen .modal-content {\n height: 100%;\n border: 0;\n border-radius: 0;\n}\n.modal-fullscreen .modal-header {\n border-radius: 0;\n}\n.modal-fullscreen .modal-body {\n overflow-y: auto;\n}\n.modal-fullscreen .modal-footer {\n border-radius: 0;\n}\n\n@media (max-width: 575.98px) {\n .modal-fullscreen-sm-down {\n width: 100vw;\n max-width: none;\n height: 100%;\n margin: 0;\n }\n .modal-fullscreen-sm-down .modal-content {\n height: 100%;\n border: 0;\n border-radius: 0;\n }\n .modal-fullscreen-sm-down .modal-header {\n border-radius: 0;\n }\n .modal-fullscreen-sm-down .modal-body {\n overflow-y: auto;\n }\n .modal-fullscreen-sm-down .modal-footer {\n border-radius: 0;\n }\n}\n@media (max-width: 767.98px) {\n .modal-fullscreen-md-down {\n width: 100vw;\n max-width: none;\n height: 100%;\n margin: 0;\n }\n .modal-fullscreen-md-down .modal-content {\n height: 100%;\n border: 0;\n border-radius: 0;\n }\n .modal-fullscreen-md-down .modal-header {\n border-radius: 0;\n }\n .modal-fullscreen-md-down .modal-body {\n overflow-y: auto;\n }\n .modal-fullscreen-md-down .modal-footer {\n border-radius: 0;\n }\n}\n@media (max-width: 991.98px) {\n .modal-fullscreen-lg-down {\n width: 100vw;\n max-width: none;\n height: 100%;\n margin: 0;\n }\n .modal-fullscreen-lg-down .modal-content {\n height: 100%;\n border: 0;\n border-radius: 0;\n }\n .modal-fullscreen-lg-down .modal-header {\n border-radius: 0;\n }\n .modal-fullscreen-lg-down .modal-body {\n overflow-y: auto;\n }\n .modal-fullscreen-lg-down .modal-footer {\n border-radius: 0;\n }\n}\n@media (max-width: 1199.98px) {\n .modal-fullscreen-xl-down {\n width: 100vw;\n max-width: none;\n height: 100%;\n margin: 0;\n }\n .modal-fullscreen-xl-down .modal-content {\n height: 100%;\n border: 0;\n border-radius: 0;\n }\n .modal-fullscreen-xl-down .modal-header {\n border-radius: 0;\n }\n .modal-fullscreen-xl-down .modal-body {\n overflow-y: auto;\n }\n .modal-fullscreen-xl-down .modal-footer {\n border-radius: 0;\n }\n}\n@media (max-width: 1399.98px) {\n .modal-fullscreen-xxl-down {\n width: 100vw;\n max-width: none;\n height: 100%;\n margin: 0;\n }\n .modal-fullscreen-xxl-down .modal-content {\n height: 100%;\n border: 0;\n border-radius: 0;\n }\n .modal-fullscreen-xxl-down .modal-header {\n border-radius: 0;\n }\n .modal-fullscreen-xxl-down .modal-body {\n overflow-y: auto;\n }\n .modal-fullscreen-xxl-down .modal-footer {\n border-radius: 0;\n }\n}\n.tooltip {\n position: absolute;\n z-index: 1080;\n display: block;\n margin: 0;\n font-family: var(--bs-font-sans-serif);\n font-style: normal;\n font-weight: 400;\n line-height: 1.5;\n text-align: left;\n text-align: start;\n text-decoration: none;\n text-shadow: none;\n text-transform: none;\n letter-spacing: normal;\n word-break: normal;\n word-spacing: normal;\n white-space: normal;\n line-break: auto;\n font-size: 0.875rem;\n word-wrap: break-word;\n opacity: 0;\n}\n.tooltip.show {\n opacity: 0.9;\n}\n.tooltip .tooltip-arrow {\n position: absolute;\n display: block;\n width: 0.8rem;\n height: 0.4rem;\n}\n.tooltip .tooltip-arrow::before {\n position: absolute;\n content: \"\";\n border-color: transparent;\n border-style: solid;\n}\n\n.bs-tooltip-top, .bs-tooltip-auto[data-popper-placement^=top] {\n padding: 0.4rem 0;\n}\n.bs-tooltip-top .tooltip-arrow, .bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow {\n bottom: 0;\n}\n.bs-tooltip-top .tooltip-arrow::before, .bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow::before {\n top: -1px;\n border-width: 0.4rem 0.4rem 0;\n border-top-color: #000;\n}\n\n.bs-tooltip-end, .bs-tooltip-auto[data-popper-placement^=right] {\n padding: 0 0.4rem;\n}\n.bs-tooltip-end .tooltip-arrow, .bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow {\n left: 0;\n width: 0.4rem;\n height: 0.8rem;\n}\n.bs-tooltip-end .tooltip-arrow::before, .bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow::before {\n right: -1px;\n border-width: 0.4rem 0.4rem 0.4rem 0;\n border-right-color: #000;\n}\n\n.bs-tooltip-bottom, .bs-tooltip-auto[data-popper-placement^=bottom] {\n padding: 0.4rem 0;\n}\n.bs-tooltip-bottom .tooltip-arrow, .bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow {\n top: 0;\n}\n.bs-tooltip-bottom .tooltip-arrow::before, .bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow::before {\n bottom: -1px;\n border-width: 0 0.4rem 0.4rem;\n border-bottom-color: #000;\n}\n\n.bs-tooltip-start, .bs-tooltip-auto[data-popper-placement^=left] {\n padding: 0 0.4rem;\n}\n.bs-tooltip-start .tooltip-arrow, .bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow {\n right: 0;\n width: 0.4rem;\n height: 0.8rem;\n}\n.bs-tooltip-start .tooltip-arrow::before, .bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow::before {\n left: -1px;\n border-width: 0.4rem 0 0.4rem 0.4rem;\n border-left-color: #000;\n}\n\n.tooltip-inner {\n max-width: 200px;\n padding: 0.25rem 0.5rem;\n color: #fff;\n text-align: center;\n background-color: #000;\n border-radius: 0.25rem;\n}\n\n.popover {\n position: absolute;\n top: 0;\n left: 0 /* rtl:ignore */;\n z-index: 1070;\n display: block;\n max-width: 276px;\n font-family: var(--bs-font-sans-serif);\n font-style: normal;\n font-weight: 400;\n line-height: 1.5;\n text-align: left;\n text-align: start;\n text-decoration: none;\n text-shadow: none;\n text-transform: none;\n letter-spacing: normal;\n word-break: normal;\n word-spacing: normal;\n white-space: normal;\n line-break: auto;\n font-size: 0.875rem;\n word-wrap: break-word;\n background-color: #fff;\n background-clip: padding-box;\n border: 1px solid rgba(0, 0, 0, 0.2);\n border-radius: 0.3rem;\n}\n.popover .popover-arrow {\n position: absolute;\n display: block;\n width: 1rem;\n height: 0.5rem;\n}\n.popover .popover-arrow::before, .popover .popover-arrow::after {\n position: absolute;\n display: block;\n content: \"\";\n border-color: transparent;\n border-style: solid;\n}\n\n.bs-popover-top > .popover-arrow, .bs-popover-auto[data-popper-placement^=top] > .popover-arrow {\n bottom: calc(-0.5rem - 1px);\n}\n.bs-popover-top > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=top] > .popover-arrow::before {\n bottom: 0;\n border-width: 0.5rem 0.5rem 0;\n border-top-color: rgba(0, 0, 0, 0.25);\n}\n.bs-popover-top > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=top] > .popover-arrow::after {\n bottom: 1px;\n border-width: 0.5rem 0.5rem 0;\n border-top-color: #fff;\n}\n\n.bs-popover-end > .popover-arrow, .bs-popover-auto[data-popper-placement^=right] > .popover-arrow {\n left: calc(-0.5rem - 1px);\n width: 0.5rem;\n height: 1rem;\n}\n.bs-popover-end > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=right] > .popover-arrow::before {\n left: 0;\n border-width: 0.5rem 0.5rem 0.5rem 0;\n border-right-color: rgba(0, 0, 0, 0.25);\n}\n.bs-popover-end > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=right] > .popover-arrow::after {\n left: 1px;\n border-width: 0.5rem 0.5rem 0.5rem 0;\n border-right-color: #fff;\n}\n\n.bs-popover-bottom > .popover-arrow, .bs-popover-auto[data-popper-placement^=bottom] > .popover-arrow {\n top: calc(-0.5rem - 1px);\n}\n.bs-popover-bottom > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=bottom] > .popover-arrow::before {\n top: 0;\n border-width: 0 0.5rem 0.5rem 0.5rem;\n border-bottom-color: rgba(0, 0, 0, 0.25);\n}\n.bs-popover-bottom > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=bottom] > .popover-arrow::after {\n top: 1px;\n border-width: 0 0.5rem 0.5rem 0.5rem;\n border-bottom-color: #fff;\n}\n.bs-popover-bottom .popover-header::before, .bs-popover-auto[data-popper-placement^=bottom] .popover-header::before {\n position: absolute;\n top: 0;\n left: 50%;\n display: block;\n width: 1rem;\n margin-left: -0.5rem;\n content: \"\";\n border-bottom: 1px solid #f0f0f0;\n}\n\n.bs-popover-start > .popover-arrow, .bs-popover-auto[data-popper-placement^=left] > .popover-arrow {\n right: calc(-0.5rem - 1px);\n width: 0.5rem;\n height: 1rem;\n}\n.bs-popover-start > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=left] > .popover-arrow::before {\n right: 0;\n border-width: 0.5rem 0 0.5rem 0.5rem;\n border-left-color: rgba(0, 0, 0, 0.25);\n}\n.bs-popover-start > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=left] > .popover-arrow::after {\n right: 1px;\n border-width: 0.5rem 0 0.5rem 0.5rem;\n border-left-color: #fff;\n}\n\n.popover-header {\n padding: 0.5rem 1rem;\n margin-bottom: 0;\n font-size: 1rem;\n background-color: #f0f0f0;\n border-bottom: 1px solid rgba(0, 0, 0, 0.2);\n border-top-left-radius: calc(0.3rem - 1px);\n border-top-right-radius: calc(0.3rem - 1px);\n}\n.popover-header:empty {\n display: none;\n}\n\n.popover-body {\n padding: 1rem 1rem;\n color: #212529;\n}\n\n.carousel {\n position: relative;\n}\n\n.carousel.pointer-event {\n touch-action: pan-y;\n}\n\n.carousel-inner {\n position: relative;\n width: 100%;\n overflow: hidden;\n}\n.carousel-inner::after {\n display: block;\n clear: both;\n content: \"\";\n}\n\n.carousel-item {\n position: relative;\n display: none;\n float: left;\n width: 100%;\n margin-right: -100%;\n backface-visibility: hidden;\n transition: transform 0.6s ease-in-out;\n}\n@media (prefers-reduced-motion: reduce) {\n .carousel-item {\n transition: none;\n }\n}\n\n.carousel-item.active,\n.carousel-item-next,\n.carousel-item-prev {\n display: block;\n}\n\n/* rtl:begin:ignore */\n.carousel-item-next:not(.carousel-item-start),\n.active.carousel-item-end {\n transform: translateX(100%);\n}\n\n.carousel-item-prev:not(.carousel-item-end),\n.active.carousel-item-start {\n transform: translateX(-100%);\n}\n\n/* rtl:end:ignore */\n.carousel-fade .carousel-item {\n opacity: 0;\n transition-property: opacity;\n transform: none;\n}\n.carousel-fade .carousel-item.active,\n.carousel-fade .carousel-item-next.carousel-item-start,\n.carousel-fade .carousel-item-prev.carousel-item-end {\n z-index: 1;\n opacity: 1;\n}\n.carousel-fade .active.carousel-item-start,\n.carousel-fade .active.carousel-item-end {\n z-index: 0;\n opacity: 0;\n transition: opacity 0s 0.6s;\n}\n@media (prefers-reduced-motion: reduce) {\n .carousel-fade .active.carousel-item-start,\n.carousel-fade .active.carousel-item-end {\n transition: none;\n }\n}\n\n.carousel-control-prev,\n.carousel-control-next {\n position: absolute;\n top: 0;\n bottom: 0;\n z-index: 1;\n display: flex;\n align-items: center;\n justify-content: center;\n width: 15%;\n padding: 0;\n color: #fff;\n text-align: center;\n background: none;\n border: 0;\n opacity: 0.5;\n transition: opacity 0.15s ease;\n}\n@media (prefers-reduced-motion: reduce) {\n .carousel-control-prev,\n.carousel-control-next {\n transition: none;\n }\n}\n.carousel-control-prev:hover, .carousel-control-prev:focus,\n.carousel-control-next:hover,\n.carousel-control-next:focus {\n color: #fff;\n text-decoration: none;\n outline: 0;\n opacity: 0.9;\n}\n\n.carousel-control-prev {\n left: 0;\n}\n\n.carousel-control-next {\n right: 0;\n}\n\n.carousel-control-prev-icon,\n.carousel-control-next-icon {\n display: inline-block;\n width: 2rem;\n height: 2rem;\n background-repeat: no-repeat;\n background-position: 50%;\n background-size: 100% 100%;\n}\n\n/* rtl:options: {\n \"autoRename\": true,\n \"stringMap\":[ {\n \"name\" : \"prev-next\",\n \"search\" : \"prev\",\n \"replace\" : \"next\"\n } ]\n} */\n.carousel-control-prev-icon {\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z'/%3e%3c/svg%3e\");\n}\n\n.carousel-control-next-icon {\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e\");\n}\n\n.carousel-indicators {\n position: absolute;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 2;\n display: flex;\n justify-content: center;\n padding: 0;\n margin-right: 15%;\n margin-bottom: 1rem;\n margin-left: 15%;\n list-style: none;\n}\n.carousel-indicators [data-bs-target] {\n box-sizing: content-box;\n flex: 0 1 auto;\n width: 30px;\n height: 3px;\n padding: 0;\n margin-right: 3px;\n margin-left: 3px;\n text-indent: -999px;\n cursor: pointer;\n background-color: #fff;\n background-clip: padding-box;\n border: 0;\n border-top: 10px solid transparent;\n border-bottom: 10px solid transparent;\n opacity: 0.5;\n transition: opacity 0.6s ease;\n}\n@media (prefers-reduced-motion: reduce) {\n .carousel-indicators [data-bs-target] {\n transition: none;\n }\n}\n.carousel-indicators .active {\n opacity: 1;\n}\n\n.carousel-caption {\n position: absolute;\n right: 15%;\n bottom: 1.25rem;\n left: 15%;\n padding-top: 1.25rem;\n padding-bottom: 1.25rem;\n color: #fff;\n text-align: center;\n}\n\n.carousel-dark .carousel-control-prev-icon,\n.carousel-dark .carousel-control-next-icon {\n filter: invert(1) grayscale(100);\n}\n.carousel-dark .carousel-indicators [data-bs-target] {\n background-color: #000;\n}\n.carousel-dark .carousel-caption {\n color: #000;\n}\n\n@keyframes spinner-border {\n to {\n transform: rotate(360deg) /* rtl:ignore */;\n }\n}\n.spinner-border {\n display: inline-block;\n width: 2rem;\n height: 2rem;\n vertical-align: -0.125em;\n border: 0.25em solid currentColor;\n border-right-color: transparent;\n border-radius: 50%;\n animation: 0.75s linear infinite spinner-border;\n}\n\n.spinner-border-sm {\n width: 1rem;\n height: 1rem;\n border-width: 0.2em;\n}\n\n@keyframes spinner-grow {\n 0% {\n transform: scale(0);\n }\n 50% {\n opacity: 1;\n transform: none;\n }\n}\n.spinner-grow {\n display: inline-block;\n width: 2rem;\n height: 2rem;\n vertical-align: -0.125em;\n background-color: currentColor;\n border-radius: 50%;\n opacity: 0;\n animation: 0.75s linear infinite spinner-grow;\n}\n\n.spinner-grow-sm {\n width: 1rem;\n height: 1rem;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .spinner-border,\n.spinner-grow {\n animation-duration: 1.5s;\n }\n}\n.offcanvas {\n position: fixed;\n bottom: 0;\n z-index: 1050;\n display: flex;\n flex-direction: column;\n max-width: 100%;\n visibility: hidden;\n background-color: #fff;\n background-clip: padding-box;\n outline: 0;\n transition: transform 0.3s ease-in-out;\n}\n@media (prefers-reduced-motion: reduce) {\n .offcanvas {\n transition: none;\n }\n}\n\n.offcanvas-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 1rem 1rem;\n}\n.offcanvas-header .btn-close {\n padding: 0.5rem 0.5rem;\n margin-top: -0.5rem;\n margin-right: -0.5rem;\n margin-bottom: -0.5rem;\n}\n\n.offcanvas-title {\n margin-bottom: 0;\n line-height: 1.5;\n}\n\n.offcanvas-body {\n flex-grow: 1;\n padding: 1rem 1rem;\n overflow-y: auto;\n}\n\n.offcanvas-start {\n top: 0;\n left: 0;\n width: 400px;\n border-right: 1px solid rgba(0, 0, 0, 0.2);\n transform: translateX(-100%);\n}\n\n.offcanvas-end {\n top: 0;\n right: 0;\n width: 400px;\n border-left: 1px solid rgba(0, 0, 0, 0.2);\n transform: translateX(100%);\n}\n\n.offcanvas-top {\n top: 0;\n right: 0;\n left: 0;\n height: 30vh;\n max-height: 100%;\n border-bottom: 1px solid rgba(0, 0, 0, 0.2);\n transform: translateY(-100%);\n}\n\n.offcanvas-bottom {\n right: 0;\n left: 0;\n height: 30vh;\n max-height: 100%;\n border-top: 1px solid rgba(0, 0, 0, 0.2);\n transform: translateY(100%);\n}\n\n.offcanvas.show {\n transform: none;\n}\n\n.clearfix::after {\n display: block;\n clear: both;\n content: \"\";\n}\n\n.link-primary {\n color: #0d6efd;\n}\n.link-primary:hover, .link-primary:focus {\n color: #0a58ca;\n}\n\n.link-secondary {\n color: #6c757d;\n}\n.link-secondary:hover, .link-secondary:focus {\n color: #565e64;\n}\n\n.link-success {\n color: #198754;\n}\n.link-success:hover, .link-success:focus {\n color: #146c43;\n}\n\n.link-info {\n color: #0dcaf0;\n}\n.link-info:hover, .link-info:focus {\n color: #3dd5f3;\n}\n\n.link-warning {\n color: #ffc107;\n}\n.link-warning:hover, .link-warning:focus {\n color: #ffcd39;\n}\n\n.link-danger {\n color: #dc3545;\n}\n.link-danger:hover, .link-danger:focus {\n color: #b02a37;\n}\n\n.link-light {\n color: #f8f9fa;\n}\n.link-light:hover, .link-light:focus {\n color: #f9fafb;\n}\n\n.link-dark {\n color: #212529;\n}\n.link-dark:hover, .link-dark:focus {\n color: #1a1e21;\n}\n\n.ratio {\n position: relative;\n width: 100%;\n}\n.ratio::before {\n display: block;\n padding-top: var(--bs-aspect-ratio);\n content: \"\";\n}\n.ratio > * {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n}\n\n.ratio-1x1 {\n --bs-aspect-ratio: 100%;\n}\n\n.ratio-4x3 {\n --bs-aspect-ratio: calc(3 / 4 * 100%);\n}\n\n.ratio-16x9 {\n --bs-aspect-ratio: calc(9 / 16 * 100%);\n}\n\n.ratio-21x9 {\n --bs-aspect-ratio: calc(9 / 21 * 100%);\n}\n\n.fixed-top {\n position: fixed;\n top: 0;\n right: 0;\n left: 0;\n z-index: 1030;\n}\n\n.fixed-bottom {\n position: fixed;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 1030;\n}\n\n.sticky-top {\n position: sticky;\n top: 0;\n z-index: 1020;\n}\n\n@media (min-width: 576px) {\n .sticky-sm-top {\n position: sticky;\n top: 0;\n z-index: 1020;\n }\n}\n@media (min-width: 768px) {\n .sticky-md-top {\n position: sticky;\n top: 0;\n z-index: 1020;\n }\n}\n@media (min-width: 992px) {\n .sticky-lg-top {\n position: sticky;\n top: 0;\n z-index: 1020;\n }\n}\n@media (min-width: 1200px) {\n .sticky-xl-top {\n position: sticky;\n top: 0;\n z-index: 1020;\n }\n}\n@media (min-width: 1400px) {\n .sticky-xxl-top {\n position: sticky;\n top: 0;\n z-index: 1020;\n }\n}\n.visually-hidden,\n.visually-hidden-focusable:not(:focus):not(:focus-within) {\n position: absolute !important;\n width: 1px !important;\n height: 1px !important;\n padding: 0 !important;\n margin: -1px !important;\n overflow: hidden !important;\n clip: rect(0, 0, 0, 0) !important;\n white-space: nowrap !important;\n border: 0 !important;\n}\n\n.stretched-link::after {\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 1;\n content: \"\";\n}\n\n.text-truncate {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.align-baseline {\n vertical-align: baseline !important;\n}\n\n.align-top {\n vertical-align: top !important;\n}\n\n.align-middle {\n vertical-align: middle !important;\n}\n\n.align-bottom {\n vertical-align: bottom !important;\n}\n\n.align-text-bottom {\n vertical-align: text-bottom !important;\n}\n\n.align-text-top {\n vertical-align: text-top !important;\n}\n\n.float-start {\n float: left !important;\n}\n\n.float-end {\n float: right !important;\n}\n\n.float-none {\n float: none !important;\n}\n\n.overflow-auto {\n overflow: auto !important;\n}\n\n.overflow-hidden {\n overflow: hidden !important;\n}\n\n.overflow-visible {\n overflow: visible !important;\n}\n\n.overflow-scroll {\n overflow: scroll !important;\n}\n\n.d-inline {\n display: inline !important;\n}\n\n.d-inline-block {\n display: inline-block !important;\n}\n\n.d-block {\n display: block !important;\n}\n\n.d-grid {\n display: grid !important;\n}\n\n.d-table {\n display: table !important;\n}\n\n.d-table-row {\n display: table-row !important;\n}\n\n.d-table-cell {\n display: table-cell !important;\n}\n\n.d-flex {\n display: flex !important;\n}\n\n.d-inline-flex {\n display: inline-flex !important;\n}\n\n.d-none {\n display: none !important;\n}\n\n.shadow {\n box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important;\n}\n\n.shadow-sm {\n box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075) !important;\n}\n\n.shadow-lg {\n box-shadow: 0 1rem 3rem rgba(0, 0, 0, 0.175) !important;\n}\n\n.shadow-none {\n box-shadow: none !important;\n}\n\n.position-static {\n position: static !important;\n}\n\n.position-relative {\n position: relative !important;\n}\n\n.position-absolute {\n position: absolute !important;\n}\n\n.position-fixed {\n position: fixed !important;\n}\n\n.position-sticky {\n position: sticky !important;\n}\n\n.top-0 {\n top: 0 !important;\n}\n\n.top-50 {\n top: 50% !important;\n}\n\n.top-100 {\n top: 100% !important;\n}\n\n.bottom-0 {\n bottom: 0 !important;\n}\n\n.bottom-50 {\n bottom: 50% !important;\n}\n\n.bottom-100 {\n bottom: 100% !important;\n}\n\n.start-0 {\n left: 0 !important;\n}\n\n.start-50 {\n left: 50% !important;\n}\n\n.start-100 {\n left: 100% !important;\n}\n\n.end-0 {\n right: 0 !important;\n}\n\n.end-50 {\n right: 50% !important;\n}\n\n.end-100 {\n right: 100% !important;\n}\n\n.translate-middle {\n transform: translate(-50%, -50%) !important;\n}\n\n.translate-middle-x {\n transform: translateX(-50%) !important;\n}\n\n.translate-middle-y {\n transform: translateY(-50%) !important;\n}\n\n.border {\n border: 1px solid #dee2e6 !important;\n}\n\n.border-0 {\n border: 0 !important;\n}\n\n.border-top {\n border-top: 1px solid #dee2e6 !important;\n}\n\n.border-top-0 {\n border-top: 0 !important;\n}\n\n.border-end {\n border-right: 1px solid #dee2e6 !important;\n}\n\n.border-end-0 {\n border-right: 0 !important;\n}\n\n.border-bottom {\n border-bottom: 1px solid #dee2e6 !important;\n}\n\n.border-bottom-0 {\n border-bottom: 0 !important;\n}\n\n.border-start {\n border-left: 1px solid #dee2e6 !important;\n}\n\n.border-start-0 {\n border-left: 0 !important;\n}\n\n.border-primary {\n border-color: #0d6efd !important;\n}\n\n.border-secondary {\n border-color: #6c757d !important;\n}\n\n.border-success {\n border-color: #198754 !important;\n}\n\n.border-info {\n border-color: #0dcaf0 !important;\n}\n\n.border-warning {\n border-color: #ffc107 !important;\n}\n\n.border-danger {\n border-color: #dc3545 !important;\n}\n\n.border-light {\n border-color: #f8f9fa !important;\n}\n\n.border-dark {\n border-color: #212529 !important;\n}\n\n.border-white {\n border-color: #fff !important;\n}\n\n.border-1 {\n border-width: 1px !important;\n}\n\n.border-2 {\n border-width: 2px !important;\n}\n\n.border-3 {\n border-width: 3px !important;\n}\n\n.border-4 {\n border-width: 4px !important;\n}\n\n.border-5 {\n border-width: 5px !important;\n}\n\n.w-25 {\n width: 25% !important;\n}\n\n.w-50 {\n width: 50% !important;\n}\n\n.w-75 {\n width: 75% !important;\n}\n\n.w-100 {\n width: 100% !important;\n}\n\n.w-auto {\n width: auto !important;\n}\n\n.mw-100 {\n max-width: 100% !important;\n}\n\n.vw-100 {\n width: 100vw !important;\n}\n\n.min-vw-100 {\n min-width: 100vw !important;\n}\n\n.h-25 {\n height: 25% !important;\n}\n\n.h-50 {\n height: 50% !important;\n}\n\n.h-75 {\n height: 75% !important;\n}\n\n.h-100 {\n height: 100% !important;\n}\n\n.h-auto {\n height: auto !important;\n}\n\n.mh-100 {\n max-height: 100% !important;\n}\n\n.vh-100 {\n height: 100vh !important;\n}\n\n.min-vh-100 {\n min-height: 100vh !important;\n}\n\n.flex-fill {\n flex: 1 1 auto !important;\n}\n\n.flex-row {\n flex-direction: row !important;\n}\n\n.flex-column {\n flex-direction: column !important;\n}\n\n.flex-row-reverse {\n flex-direction: row-reverse !important;\n}\n\n.flex-column-reverse {\n flex-direction: column-reverse !important;\n}\n\n.flex-grow-0 {\n flex-grow: 0 !important;\n}\n\n.flex-grow-1 {\n flex-grow: 1 !important;\n}\n\n.flex-shrink-0 {\n flex-shrink: 0 !important;\n}\n\n.flex-shrink-1 {\n flex-shrink: 1 !important;\n}\n\n.flex-wrap {\n flex-wrap: wrap !important;\n}\n\n.flex-nowrap {\n flex-wrap: nowrap !important;\n}\n\n.flex-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n}\n\n.gap-0 {\n gap: 0 !important;\n}\n\n.gap-1 {\n gap: 0.25rem !important;\n}\n\n.gap-2 {\n gap: 0.5rem !important;\n}\n\n.gap-3 {\n gap: 1rem !important;\n}\n\n.gap-4 {\n gap: 1.5rem !important;\n}\n\n.gap-5 {\n gap: 3rem !important;\n}\n\n.justify-content-start {\n justify-content: flex-start !important;\n}\n\n.justify-content-end {\n justify-content: flex-end !important;\n}\n\n.justify-content-center {\n justify-content: center !important;\n}\n\n.justify-content-between {\n justify-content: space-between !important;\n}\n\n.justify-content-around {\n justify-content: space-around !important;\n}\n\n.justify-content-evenly {\n justify-content: space-evenly !important;\n}\n\n.align-items-start {\n align-items: flex-start !important;\n}\n\n.align-items-end {\n align-items: flex-end !important;\n}\n\n.align-items-center {\n align-items: center !important;\n}\n\n.align-items-baseline {\n align-items: baseline !important;\n}\n\n.align-items-stretch {\n align-items: stretch !important;\n}\n\n.align-content-start {\n align-content: flex-start !important;\n}\n\n.align-content-end {\n align-content: flex-end !important;\n}\n\n.align-content-center {\n align-content: center !important;\n}\n\n.align-content-between {\n align-content: space-between !important;\n}\n\n.align-content-around {\n align-content: space-around !important;\n}\n\n.align-content-stretch {\n align-content: stretch !important;\n}\n\n.align-self-auto {\n align-self: auto !important;\n}\n\n.align-self-start {\n align-self: flex-start !important;\n}\n\n.align-self-end {\n align-self: flex-end !important;\n}\n\n.align-self-center {\n align-self: center !important;\n}\n\n.align-self-baseline {\n align-self: baseline !important;\n}\n\n.align-self-stretch {\n align-self: stretch !important;\n}\n\n.order-first {\n order: -1 !important;\n}\n\n.order-0 {\n order: 0 !important;\n}\n\n.order-1 {\n order: 1 !important;\n}\n\n.order-2 {\n order: 2 !important;\n}\n\n.order-3 {\n order: 3 !important;\n}\n\n.order-4 {\n order: 4 !important;\n}\n\n.order-5 {\n order: 5 !important;\n}\n\n.order-last {\n order: 6 !important;\n}\n\n.m-0 {\n margin: 0 !important;\n}\n\n.m-1 {\n margin: 0.25rem !important;\n}\n\n.m-2 {\n margin: 0.5rem !important;\n}\n\n.m-3 {\n margin: 1rem !important;\n}\n\n.m-4 {\n margin: 1.5rem !important;\n}\n\n.m-5 {\n margin: 3rem !important;\n}\n\n.m-auto {\n margin: auto !important;\n}\n\n.mx-0 {\n margin-right: 0 !important;\n margin-left: 0 !important;\n}\n\n.mx-1 {\n margin-right: 0.25rem !important;\n margin-left: 0.25rem !important;\n}\n\n.mx-2 {\n margin-right: 0.5rem !important;\n margin-left: 0.5rem !important;\n}\n\n.mx-3 {\n margin-right: 1rem !important;\n margin-left: 1rem !important;\n}\n\n.mx-4 {\n margin-right: 1.5rem !important;\n margin-left: 1.5rem !important;\n}\n\n.mx-5 {\n margin-right: 3rem !important;\n margin-left: 3rem !important;\n}\n\n.mx-auto {\n margin-right: auto !important;\n margin-left: auto !important;\n}\n\n.my-0 {\n margin-top: 0 !important;\n margin-bottom: 0 !important;\n}\n\n.my-1 {\n margin-top: 0.25rem !important;\n margin-bottom: 0.25rem !important;\n}\n\n.my-2 {\n margin-top: 0.5rem !important;\n margin-bottom: 0.5rem !important;\n}\n\n.my-3 {\n margin-top: 1rem !important;\n margin-bottom: 1rem !important;\n}\n\n.my-4 {\n margin-top: 1.5rem !important;\n margin-bottom: 1.5rem !important;\n}\n\n.my-5 {\n margin-top: 3rem !important;\n margin-bottom: 3rem !important;\n}\n\n.my-auto {\n margin-top: auto !important;\n margin-bottom: auto !important;\n}\n\n.mt-0 {\n margin-top: 0 !important;\n}\n\n.mt-1 {\n margin-top: 0.25rem !important;\n}\n\n.mt-2 {\n margin-top: 0.5rem !important;\n}\n\n.mt-3 {\n margin-top: 1rem !important;\n}\n\n.mt-4 {\n margin-top: 1.5rem !important;\n}\n\n.mt-5 {\n margin-top: 3rem !important;\n}\n\n.mt-auto {\n margin-top: auto !important;\n}\n\n.me-0 {\n margin-right: 0 !important;\n}\n\n.me-1 {\n margin-right: 0.25rem !important;\n}\n\n.me-2 {\n margin-right: 0.5rem !important;\n}\n\n.me-3 {\n margin-right: 1rem !important;\n}\n\n.me-4 {\n margin-right: 1.5rem !important;\n}\n\n.me-5 {\n margin-right: 3rem !important;\n}\n\n.me-auto {\n margin-right: auto !important;\n}\n\n.mb-0 {\n margin-bottom: 0 !important;\n}\n\n.mb-1 {\n margin-bottom: 0.25rem !important;\n}\n\n.mb-2 {\n margin-bottom: 0.5rem !important;\n}\n\n.mb-3 {\n margin-bottom: 1rem !important;\n}\n\n.mb-4 {\n margin-bottom: 1.5rem !important;\n}\n\n.mb-5 {\n margin-bottom: 3rem !important;\n}\n\n.mb-auto {\n margin-bottom: auto !important;\n}\n\n.ms-0 {\n margin-left: 0 !important;\n}\n\n.ms-1 {\n margin-left: 0.25rem !important;\n}\n\n.ms-2 {\n margin-left: 0.5rem !important;\n}\n\n.ms-3 {\n margin-left: 1rem !important;\n}\n\n.ms-4 {\n margin-left: 1.5rem !important;\n}\n\n.ms-5 {\n margin-left: 3rem !important;\n}\n\n.ms-auto {\n margin-left: auto !important;\n}\n\n.p-0 {\n padding: 0 !important;\n}\n\n.p-1 {\n padding: 0.25rem !important;\n}\n\n.p-2 {\n padding: 0.5rem !important;\n}\n\n.p-3 {\n padding: 1rem !important;\n}\n\n.p-4 {\n padding: 1.5rem !important;\n}\n\n.p-5 {\n padding: 3rem !important;\n}\n\n.px-0 {\n padding-right: 0 !important;\n padding-left: 0 !important;\n}\n\n.px-1 {\n padding-right: 0.25rem !important;\n padding-left: 0.25rem !important;\n}\n\n.px-2 {\n padding-right: 0.5rem !important;\n padding-left: 0.5rem !important;\n}\n\n.px-3 {\n padding-right: 1rem !important;\n padding-left: 1rem !important;\n}\n\n.px-4 {\n padding-right: 1.5rem !important;\n padding-left: 1.5rem !important;\n}\n\n.px-5 {\n padding-right: 3rem !important;\n padding-left: 3rem !important;\n}\n\n.py-0 {\n padding-top: 0 !important;\n padding-bottom: 0 !important;\n}\n\n.py-1 {\n padding-top: 0.25rem !important;\n padding-bottom: 0.25rem !important;\n}\n\n.py-2 {\n padding-top: 0.5rem !important;\n padding-bottom: 0.5rem !important;\n}\n\n.py-3 {\n padding-top: 1rem !important;\n padding-bottom: 1rem !important;\n}\n\n.py-4 {\n padding-top: 1.5rem !important;\n padding-bottom: 1.5rem !important;\n}\n\n.py-5 {\n padding-top: 3rem !important;\n padding-bottom: 3rem !important;\n}\n\n.pt-0 {\n padding-top: 0 !important;\n}\n\n.pt-1 {\n padding-top: 0.25rem !important;\n}\n\n.pt-2 {\n padding-top: 0.5rem !important;\n}\n\n.pt-3 {\n padding-top: 1rem !important;\n}\n\n.pt-4 {\n padding-top: 1.5rem !important;\n}\n\n.pt-5 {\n padding-top: 3rem !important;\n}\n\n.pe-0 {\n padding-right: 0 !important;\n}\n\n.pe-1 {\n padding-right: 0.25rem !important;\n}\n\n.pe-2 {\n padding-right: 0.5rem !important;\n}\n\n.pe-3 {\n padding-right: 1rem !important;\n}\n\n.pe-4 {\n padding-right: 1.5rem !important;\n}\n\n.pe-5 {\n padding-right: 3rem !important;\n}\n\n.pb-0 {\n padding-bottom: 0 !important;\n}\n\n.pb-1 {\n padding-bottom: 0.25rem !important;\n}\n\n.pb-2 {\n padding-bottom: 0.5rem !important;\n}\n\n.pb-3 {\n padding-bottom: 1rem !important;\n}\n\n.pb-4 {\n padding-bottom: 1.5rem !important;\n}\n\n.pb-5 {\n padding-bottom: 3rem !important;\n}\n\n.ps-0 {\n padding-left: 0 !important;\n}\n\n.ps-1 {\n padding-left: 0.25rem !important;\n}\n\n.ps-2 {\n padding-left: 0.5rem !important;\n}\n\n.ps-3 {\n padding-left: 1rem !important;\n}\n\n.ps-4 {\n padding-left: 1.5rem !important;\n}\n\n.ps-5 {\n padding-left: 3rem !important;\n}\n\n.font-monospace {\n font-family: var(--bs-font-monospace) !important;\n}\n\n.fs-1 {\n font-size: calc(1.375rem + 1.5vw) !important;\n}\n\n.fs-2 {\n font-size: calc(1.325rem + 0.9vw) !important;\n}\n\n.fs-3 {\n font-size: calc(1.3rem + 0.6vw) !important;\n}\n\n.fs-4 {\n font-size: calc(1.275rem + 0.3vw) !important;\n}\n\n.fs-5 {\n font-size: 1.25rem !important;\n}\n\n.fs-6 {\n font-size: 1rem !important;\n}\n\n.fst-italic {\n font-style: italic !important;\n}\n\n.fst-normal {\n font-style: normal !important;\n}\n\n.fw-light {\n font-weight: 300 !important;\n}\n\n.fw-lighter {\n font-weight: lighter !important;\n}\n\n.fw-normal {\n font-weight: 400 !important;\n}\n\n.fw-bold {\n font-weight: 700 !important;\n}\n\n.fw-bolder {\n font-weight: bolder !important;\n}\n\n.lh-1 {\n line-height: 1 !important;\n}\n\n.lh-sm {\n line-height: 1.25 !important;\n}\n\n.lh-base {\n line-height: 1.5 !important;\n}\n\n.lh-lg {\n line-height: 2 !important;\n}\n\n.text-start {\n text-align: left !important;\n}\n\n.text-end {\n text-align: right !important;\n}\n\n.text-center {\n text-align: center !important;\n}\n\n.text-decoration-none {\n text-decoration: none !important;\n}\n\n.text-decoration-underline {\n text-decoration: underline !important;\n}\n\n.text-decoration-line-through {\n text-decoration: line-through !important;\n}\n\n.text-lowercase {\n text-transform: lowercase !important;\n}\n\n.text-uppercase {\n text-transform: uppercase !important;\n}\n\n.text-capitalize {\n text-transform: capitalize !important;\n}\n\n.text-wrap {\n white-space: normal !important;\n}\n\n.text-nowrap {\n white-space: nowrap !important;\n}\n\n/* rtl:begin:remove */\n.text-break {\n word-wrap: break-word !important;\n word-break: break-word !important;\n}\n\n/* rtl:end:remove */\n.text-primary {\n color: #0d6efd !important;\n}\n\n.text-secondary {\n color: #6c757d !important;\n}\n\n.text-success {\n color: #198754 !important;\n}\n\n.text-info {\n color: #0dcaf0 !important;\n}\n\n.text-warning {\n color: #ffc107 !important;\n}\n\n.text-danger {\n color: #dc3545 !important;\n}\n\n.text-light {\n color: #f8f9fa !important;\n}\n\n.text-dark {\n color: #212529 !important;\n}\n\n.text-white {\n color: #fff !important;\n}\n\n.text-body {\n color: #212529 !important;\n}\n\n.text-muted {\n color: #6c757d !important;\n}\n\n.text-black-50 {\n color: rgba(0, 0, 0, 0.5) !important;\n}\n\n.text-white-50 {\n color: rgba(255, 255, 255, 0.5) !important;\n}\n\n.text-reset {\n color: inherit !important;\n}\n\n.bg-primary {\n background-color: #0d6efd !important;\n}\n\n.bg-secondary {\n background-color: #6c757d !important;\n}\n\n.bg-success {\n background-color: #198754 !important;\n}\n\n.bg-info {\n background-color: #0dcaf0 !important;\n}\n\n.bg-warning {\n background-color: #ffc107 !important;\n}\n\n.bg-danger {\n background-color: #dc3545 !important;\n}\n\n.bg-light {\n background-color: #f8f9fa !important;\n}\n\n.bg-dark {\n background-color: #212529 !important;\n}\n\n.bg-body {\n background-color: #fff !important;\n}\n\n.bg-white {\n background-color: #fff !important;\n}\n\n.bg-transparent {\n background-color: transparent !important;\n}\n\n.bg-gradient {\n background-image: var(--bs-gradient) !important;\n}\n\n.user-select-all {\n user-select: all !important;\n}\n\n.user-select-auto {\n user-select: auto !important;\n}\n\n.user-select-none {\n user-select: none !important;\n}\n\n.pe-none {\n pointer-events: none !important;\n}\n\n.pe-auto {\n pointer-events: auto !important;\n}\n\n.rounded {\n border-radius: 0.25rem !important;\n}\n\n.rounded-0 {\n border-radius: 0 !important;\n}\n\n.rounded-1 {\n border-radius: 0.2rem !important;\n}\n\n.rounded-2 {\n border-radius: 0.25rem !important;\n}\n\n.rounded-3 {\n border-radius: 0.3rem !important;\n}\n\n.rounded-circle {\n border-radius: 50% !important;\n}\n\n.rounded-pill {\n border-radius: 50rem !important;\n}\n\n.rounded-top {\n border-top-left-radius: 0.25rem !important;\n border-top-right-radius: 0.25rem !important;\n}\n\n.rounded-end {\n border-top-right-radius: 0.25rem !important;\n border-bottom-right-radius: 0.25rem !important;\n}\n\n.rounded-bottom {\n border-bottom-right-radius: 0.25rem !important;\n border-bottom-left-radius: 0.25rem !important;\n}\n\n.rounded-start {\n border-bottom-left-radius: 0.25rem !important;\n border-top-left-radius: 0.25rem !important;\n}\n\n.visible {\n visibility: visible !important;\n}\n\n.invisible {\n visibility: hidden !important;\n}\n\n@media (min-width: 576px) {\n .float-sm-start {\n float: left !important;\n }\n\n .float-sm-end {\n float: right !important;\n }\n\n .float-sm-none {\n float: none !important;\n }\n\n .d-sm-inline {\n display: inline !important;\n }\n\n .d-sm-inline-block {\n display: inline-block !important;\n }\n\n .d-sm-block {\n display: block !important;\n }\n\n .d-sm-grid {\n display: grid !important;\n }\n\n .d-sm-table {\n display: table !important;\n }\n\n .d-sm-table-row {\n display: table-row !important;\n }\n\n .d-sm-table-cell {\n display: table-cell !important;\n }\n\n .d-sm-flex {\n display: flex !important;\n }\n\n .d-sm-inline-flex {\n display: inline-flex !important;\n }\n\n .d-sm-none {\n display: none !important;\n }\n\n .flex-sm-fill {\n flex: 1 1 auto !important;\n }\n\n .flex-sm-row {\n flex-direction: row !important;\n }\n\n .flex-sm-column {\n flex-direction: column !important;\n }\n\n .flex-sm-row-reverse {\n flex-direction: row-reverse !important;\n }\n\n .flex-sm-column-reverse {\n flex-direction: column-reverse !important;\n }\n\n .flex-sm-grow-0 {\n flex-grow: 0 !important;\n }\n\n .flex-sm-grow-1 {\n flex-grow: 1 !important;\n }\n\n .flex-sm-shrink-0 {\n flex-shrink: 0 !important;\n }\n\n .flex-sm-shrink-1 {\n flex-shrink: 1 !important;\n }\n\n .flex-sm-wrap {\n flex-wrap: wrap !important;\n }\n\n .flex-sm-nowrap {\n flex-wrap: nowrap !important;\n }\n\n .flex-sm-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n\n .gap-sm-0 {\n gap: 0 !important;\n }\n\n .gap-sm-1 {\n gap: 0.25rem !important;\n }\n\n .gap-sm-2 {\n gap: 0.5rem !important;\n }\n\n .gap-sm-3 {\n gap: 1rem !important;\n }\n\n .gap-sm-4 {\n gap: 1.5rem !important;\n }\n\n .gap-sm-5 {\n gap: 3rem !important;\n }\n\n .justify-content-sm-start {\n justify-content: flex-start !important;\n }\n\n .justify-content-sm-end {\n justify-content: flex-end !important;\n }\n\n .justify-content-sm-center {\n justify-content: center !important;\n }\n\n .justify-content-sm-between {\n justify-content: space-between !important;\n }\n\n .justify-content-sm-around {\n justify-content: space-around !important;\n }\n\n .justify-content-sm-evenly {\n justify-content: space-evenly !important;\n }\n\n .align-items-sm-start {\n align-items: flex-start !important;\n }\n\n .align-items-sm-end {\n align-items: flex-end !important;\n }\n\n .align-items-sm-center {\n align-items: center !important;\n }\n\n .align-items-sm-baseline {\n align-items: baseline !important;\n }\n\n .align-items-sm-stretch {\n align-items: stretch !important;\n }\n\n .align-content-sm-start {\n align-content: flex-start !important;\n }\n\n .align-content-sm-end {\n align-content: flex-end !important;\n }\n\n .align-content-sm-center {\n align-content: center !important;\n }\n\n .align-content-sm-between {\n align-content: space-between !important;\n }\n\n .align-content-sm-around {\n align-content: space-around !important;\n }\n\n .align-content-sm-stretch {\n align-content: stretch !important;\n }\n\n .align-self-sm-auto {\n align-self: auto !important;\n }\n\n .align-self-sm-start {\n align-self: flex-start !important;\n }\n\n .align-self-sm-end {\n align-self: flex-end !important;\n }\n\n .align-self-sm-center {\n align-self: center !important;\n }\n\n .align-self-sm-baseline {\n align-self: baseline !important;\n }\n\n .align-self-sm-stretch {\n align-self: stretch !important;\n }\n\n .order-sm-first {\n order: -1 !important;\n }\n\n .order-sm-0 {\n order: 0 !important;\n }\n\n .order-sm-1 {\n order: 1 !important;\n }\n\n .order-sm-2 {\n order: 2 !important;\n }\n\n .order-sm-3 {\n order: 3 !important;\n }\n\n .order-sm-4 {\n order: 4 !important;\n }\n\n .order-sm-5 {\n order: 5 !important;\n }\n\n .order-sm-last {\n order: 6 !important;\n }\n\n .m-sm-0 {\n margin: 0 !important;\n }\n\n .m-sm-1 {\n margin: 0.25rem !important;\n }\n\n .m-sm-2 {\n margin: 0.5rem !important;\n }\n\n .m-sm-3 {\n margin: 1rem !important;\n }\n\n .m-sm-4 {\n margin: 1.5rem !important;\n }\n\n .m-sm-5 {\n margin: 3rem !important;\n }\n\n .m-sm-auto {\n margin: auto !important;\n }\n\n .mx-sm-0 {\n margin-right: 0 !important;\n margin-left: 0 !important;\n }\n\n .mx-sm-1 {\n margin-right: 0.25rem !important;\n margin-left: 0.25rem !important;\n }\n\n .mx-sm-2 {\n margin-right: 0.5rem !important;\n margin-left: 0.5rem !important;\n }\n\n .mx-sm-3 {\n margin-right: 1rem !important;\n margin-left: 1rem !important;\n }\n\n .mx-sm-4 {\n margin-right: 1.5rem !important;\n margin-left: 1.5rem !important;\n }\n\n .mx-sm-5 {\n margin-right: 3rem !important;\n margin-left: 3rem !important;\n }\n\n .mx-sm-auto {\n margin-right: auto !important;\n margin-left: auto !important;\n }\n\n .my-sm-0 {\n margin-top: 0 !important;\n margin-bottom: 0 !important;\n }\n\n .my-sm-1 {\n margin-top: 0.25rem !important;\n margin-bottom: 0.25rem !important;\n }\n\n .my-sm-2 {\n margin-top: 0.5rem !important;\n margin-bottom: 0.5rem !important;\n }\n\n .my-sm-3 {\n margin-top: 1rem !important;\n margin-bottom: 1rem !important;\n }\n\n .my-sm-4 {\n margin-top: 1.5rem !important;\n margin-bottom: 1.5rem !important;\n }\n\n .my-sm-5 {\n margin-top: 3rem !important;\n margin-bottom: 3rem !important;\n }\n\n .my-sm-auto {\n margin-top: auto !important;\n margin-bottom: auto !important;\n }\n\n .mt-sm-0 {\n margin-top: 0 !important;\n }\n\n .mt-sm-1 {\n margin-top: 0.25rem !important;\n }\n\n .mt-sm-2 {\n margin-top: 0.5rem !important;\n }\n\n .mt-sm-3 {\n margin-top: 1rem !important;\n }\n\n .mt-sm-4 {\n margin-top: 1.5rem !important;\n }\n\n .mt-sm-5 {\n margin-top: 3rem !important;\n }\n\n .mt-sm-auto {\n margin-top: auto !important;\n }\n\n .me-sm-0 {\n margin-right: 0 !important;\n }\n\n .me-sm-1 {\n margin-right: 0.25rem !important;\n }\n\n .me-sm-2 {\n margin-right: 0.5rem !important;\n }\n\n .me-sm-3 {\n margin-right: 1rem !important;\n }\n\n .me-sm-4 {\n margin-right: 1.5rem !important;\n }\n\n .me-sm-5 {\n margin-right: 3rem !important;\n }\n\n .me-sm-auto {\n margin-right: auto !important;\n }\n\n .mb-sm-0 {\n margin-bottom: 0 !important;\n }\n\n .mb-sm-1 {\n margin-bottom: 0.25rem !important;\n }\n\n .mb-sm-2 {\n margin-bottom: 0.5rem !important;\n }\n\n .mb-sm-3 {\n margin-bottom: 1rem !important;\n }\n\n .mb-sm-4 {\n margin-bottom: 1.5rem !important;\n }\n\n .mb-sm-5 {\n margin-bottom: 3rem !important;\n }\n\n .mb-sm-auto {\n margin-bottom: auto !important;\n }\n\n .ms-sm-0 {\n margin-left: 0 !important;\n }\n\n .ms-sm-1 {\n margin-left: 0.25rem !important;\n }\n\n .ms-sm-2 {\n margin-left: 0.5rem !important;\n }\n\n .ms-sm-3 {\n margin-left: 1rem !important;\n }\n\n .ms-sm-4 {\n margin-left: 1.5rem !important;\n }\n\n .ms-sm-5 {\n margin-left: 3rem !important;\n }\n\n .ms-sm-auto {\n margin-left: auto !important;\n }\n\n .p-sm-0 {\n padding: 0 !important;\n }\n\n .p-sm-1 {\n padding: 0.25rem !important;\n }\n\n .p-sm-2 {\n padding: 0.5rem !important;\n }\n\n .p-sm-3 {\n padding: 1rem !important;\n }\n\n .p-sm-4 {\n padding: 1.5rem !important;\n }\n\n .p-sm-5 {\n padding: 3rem !important;\n }\n\n .px-sm-0 {\n padding-right: 0 !important;\n padding-left: 0 !important;\n }\n\n .px-sm-1 {\n padding-right: 0.25rem !important;\n padding-left: 0.25rem !important;\n }\n\n .px-sm-2 {\n padding-right: 0.5rem !important;\n padding-left: 0.5rem !important;\n }\n\n .px-sm-3 {\n padding-right: 1rem !important;\n padding-left: 1rem !important;\n }\n\n .px-sm-4 {\n padding-right: 1.5rem !important;\n padding-left: 1.5rem !important;\n }\n\n .px-sm-5 {\n padding-right: 3rem !important;\n padding-left: 3rem !important;\n }\n\n .py-sm-0 {\n padding-top: 0 !important;\n padding-bottom: 0 !important;\n }\n\n .py-sm-1 {\n padding-top: 0.25rem !important;\n padding-bottom: 0.25rem !important;\n }\n\n .py-sm-2 {\n padding-top: 0.5rem !important;\n padding-bottom: 0.5rem !important;\n }\n\n .py-sm-3 {\n padding-top: 1rem !important;\n padding-bottom: 1rem !important;\n }\n\n .py-sm-4 {\n padding-top: 1.5rem !important;\n padding-bottom: 1.5rem !important;\n }\n\n .py-sm-5 {\n padding-top: 3rem !important;\n padding-bottom: 3rem !important;\n }\n\n .pt-sm-0 {\n padding-top: 0 !important;\n }\n\n .pt-sm-1 {\n padding-top: 0.25rem !important;\n }\n\n .pt-sm-2 {\n padding-top: 0.5rem !important;\n }\n\n .pt-sm-3 {\n padding-top: 1rem !important;\n }\n\n .pt-sm-4 {\n padding-top: 1.5rem !important;\n }\n\n .pt-sm-5 {\n padding-top: 3rem !important;\n }\n\n .pe-sm-0 {\n padding-right: 0 !important;\n }\n\n .pe-sm-1 {\n padding-right: 0.25rem !important;\n }\n\n .pe-sm-2 {\n padding-right: 0.5rem !important;\n }\n\n .pe-sm-3 {\n padding-right: 1rem !important;\n }\n\n .pe-sm-4 {\n padding-right: 1.5rem !important;\n }\n\n .pe-sm-5 {\n padding-right: 3rem !important;\n }\n\n .pb-sm-0 {\n padding-bottom: 0 !important;\n }\n\n .pb-sm-1 {\n padding-bottom: 0.25rem !important;\n }\n\n .pb-sm-2 {\n padding-bottom: 0.5rem !important;\n }\n\n .pb-sm-3 {\n padding-bottom: 1rem !important;\n }\n\n .pb-sm-4 {\n padding-bottom: 1.5rem !important;\n }\n\n .pb-sm-5 {\n padding-bottom: 3rem !important;\n }\n\n .ps-sm-0 {\n padding-left: 0 !important;\n }\n\n .ps-sm-1 {\n padding-left: 0.25rem !important;\n }\n\n .ps-sm-2 {\n padding-left: 0.5rem !important;\n }\n\n .ps-sm-3 {\n padding-left: 1rem !important;\n }\n\n .ps-sm-4 {\n padding-left: 1.5rem !important;\n }\n\n .ps-sm-5 {\n padding-left: 3rem !important;\n }\n\n .text-sm-start {\n text-align: left !important;\n }\n\n .text-sm-end {\n text-align: right !important;\n }\n\n .text-sm-center {\n text-align: center !important;\n }\n}\n@media (min-width: 768px) {\n .float-md-start {\n float: left !important;\n }\n\n .float-md-end {\n float: right !important;\n }\n\n .float-md-none {\n float: none !important;\n }\n\n .d-md-inline {\n display: inline !important;\n }\n\n .d-md-inline-block {\n display: inline-block !important;\n }\n\n .d-md-block {\n display: block !important;\n }\n\n .d-md-grid {\n display: grid !important;\n }\n\n .d-md-table {\n display: table !important;\n }\n\n .d-md-table-row {\n display: table-row !important;\n }\n\n .d-md-table-cell {\n display: table-cell !important;\n }\n\n .d-md-flex {\n display: flex !important;\n }\n\n .d-md-inline-flex {\n display: inline-flex !important;\n }\n\n .d-md-none {\n display: none !important;\n }\n\n .flex-md-fill {\n flex: 1 1 auto !important;\n }\n\n .flex-md-row {\n flex-direction: row !important;\n }\n\n .flex-md-column {\n flex-direction: column !important;\n }\n\n .flex-md-row-reverse {\n flex-direction: row-reverse !important;\n }\n\n .flex-md-column-reverse {\n flex-direction: column-reverse !important;\n }\n\n .flex-md-grow-0 {\n flex-grow: 0 !important;\n }\n\n .flex-md-grow-1 {\n flex-grow: 1 !important;\n }\n\n .flex-md-shrink-0 {\n flex-shrink: 0 !important;\n }\n\n .flex-md-shrink-1 {\n flex-shrink: 1 !important;\n }\n\n .flex-md-wrap {\n flex-wrap: wrap !important;\n }\n\n .flex-md-nowrap {\n flex-wrap: nowrap !important;\n }\n\n .flex-md-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n\n .gap-md-0 {\n gap: 0 !important;\n }\n\n .gap-md-1 {\n gap: 0.25rem !important;\n }\n\n .gap-md-2 {\n gap: 0.5rem !important;\n }\n\n .gap-md-3 {\n gap: 1rem !important;\n }\n\n .gap-md-4 {\n gap: 1.5rem !important;\n }\n\n .gap-md-5 {\n gap: 3rem !important;\n }\n\n .justify-content-md-start {\n justify-content: flex-start !important;\n }\n\n .justify-content-md-end {\n justify-content: flex-end !important;\n }\n\n .justify-content-md-center {\n justify-content: center !important;\n }\n\n .justify-content-md-between {\n justify-content: space-between !important;\n }\n\n .justify-content-md-around {\n justify-content: space-around !important;\n }\n\n .justify-content-md-evenly {\n justify-content: space-evenly !important;\n }\n\n .align-items-md-start {\n align-items: flex-start !important;\n }\n\n .align-items-md-end {\n align-items: flex-end !important;\n }\n\n .align-items-md-center {\n align-items: center !important;\n }\n\n .align-items-md-baseline {\n align-items: baseline !important;\n }\n\n .align-items-md-stretch {\n align-items: stretch !important;\n }\n\n .align-content-md-start {\n align-content: flex-start !important;\n }\n\n .align-content-md-end {\n align-content: flex-end !important;\n }\n\n .align-content-md-center {\n align-content: center !important;\n }\n\n .align-content-md-between {\n align-content: space-between !important;\n }\n\n .align-content-md-around {\n align-content: space-around !important;\n }\n\n .align-content-md-stretch {\n align-content: stretch !important;\n }\n\n .align-self-md-auto {\n align-self: auto !important;\n }\n\n .align-self-md-start {\n align-self: flex-start !important;\n }\n\n .align-self-md-end {\n align-self: flex-end !important;\n }\n\n .align-self-md-center {\n align-self: center !important;\n }\n\n .align-self-md-baseline {\n align-self: baseline !important;\n }\n\n .align-self-md-stretch {\n align-self: stretch !important;\n }\n\n .order-md-first {\n order: -1 !important;\n }\n\n .order-md-0 {\n order: 0 !important;\n }\n\n .order-md-1 {\n order: 1 !important;\n }\n\n .order-md-2 {\n order: 2 !important;\n }\n\n .order-md-3 {\n order: 3 !important;\n }\n\n .order-md-4 {\n order: 4 !important;\n }\n\n .order-md-5 {\n order: 5 !important;\n }\n\n .order-md-last {\n order: 6 !important;\n }\n\n .m-md-0 {\n margin: 0 !important;\n }\n\n .m-md-1 {\n margin: 0.25rem !important;\n }\n\n .m-md-2 {\n margin: 0.5rem !important;\n }\n\n .m-md-3 {\n margin: 1rem !important;\n }\n\n .m-md-4 {\n margin: 1.5rem !important;\n }\n\n .m-md-5 {\n margin: 3rem !important;\n }\n\n .m-md-auto {\n margin: auto !important;\n }\n\n .mx-md-0 {\n margin-right: 0 !important;\n margin-left: 0 !important;\n }\n\n .mx-md-1 {\n margin-right: 0.25rem !important;\n margin-left: 0.25rem !important;\n }\n\n .mx-md-2 {\n margin-right: 0.5rem !important;\n margin-left: 0.5rem !important;\n }\n\n .mx-md-3 {\n margin-right: 1rem !important;\n margin-left: 1rem !important;\n }\n\n .mx-md-4 {\n margin-right: 1.5rem !important;\n margin-left: 1.5rem !important;\n }\n\n .mx-md-5 {\n margin-right: 3rem !important;\n margin-left: 3rem !important;\n }\n\n .mx-md-auto {\n margin-right: auto !important;\n margin-left: auto !important;\n }\n\n .my-md-0 {\n margin-top: 0 !important;\n margin-bottom: 0 !important;\n }\n\n .my-md-1 {\n margin-top: 0.25rem !important;\n margin-bottom: 0.25rem !important;\n }\n\n .my-md-2 {\n margin-top: 0.5rem !important;\n margin-bottom: 0.5rem !important;\n }\n\n .my-md-3 {\n margin-top: 1rem !important;\n margin-bottom: 1rem !important;\n }\n\n .my-md-4 {\n margin-top: 1.5rem !important;\n margin-bottom: 1.5rem !important;\n }\n\n .my-md-5 {\n margin-top: 3rem !important;\n margin-bottom: 3rem !important;\n }\n\n .my-md-auto {\n margin-top: auto !important;\n margin-bottom: auto !important;\n }\n\n .mt-md-0 {\n margin-top: 0 !important;\n }\n\n .mt-md-1 {\n margin-top: 0.25rem !important;\n }\n\n .mt-md-2 {\n margin-top: 0.5rem !important;\n }\n\n .mt-md-3 {\n margin-top: 1rem !important;\n }\n\n .mt-md-4 {\n margin-top: 1.5rem !important;\n }\n\n .mt-md-5 {\n margin-top: 3rem !important;\n }\n\n .mt-md-auto {\n margin-top: auto !important;\n }\n\n .me-md-0 {\n margin-right: 0 !important;\n }\n\n .me-md-1 {\n margin-right: 0.25rem !important;\n }\n\n .me-md-2 {\n margin-right: 0.5rem !important;\n }\n\n .me-md-3 {\n margin-right: 1rem !important;\n }\n\n .me-md-4 {\n margin-right: 1.5rem !important;\n }\n\n .me-md-5 {\n margin-right: 3rem !important;\n }\n\n .me-md-auto {\n margin-right: auto !important;\n }\n\n .mb-md-0 {\n margin-bottom: 0 !important;\n }\n\n .mb-md-1 {\n margin-bottom: 0.25rem !important;\n }\n\n .mb-md-2 {\n margin-bottom: 0.5rem !important;\n }\n\n .mb-md-3 {\n margin-bottom: 1rem !important;\n }\n\n .mb-md-4 {\n margin-bottom: 1.5rem !important;\n }\n\n .mb-md-5 {\n margin-bottom: 3rem !important;\n }\n\n .mb-md-auto {\n margin-bottom: auto !important;\n }\n\n .ms-md-0 {\n margin-left: 0 !important;\n }\n\n .ms-md-1 {\n margin-left: 0.25rem !important;\n }\n\n .ms-md-2 {\n margin-left: 0.5rem !important;\n }\n\n .ms-md-3 {\n margin-left: 1rem !important;\n }\n\n .ms-md-4 {\n margin-left: 1.5rem !important;\n }\n\n .ms-md-5 {\n margin-left: 3rem !important;\n }\n\n .ms-md-auto {\n margin-left: auto !important;\n }\n\n .p-md-0 {\n padding: 0 !important;\n }\n\n .p-md-1 {\n padding: 0.25rem !important;\n }\n\n .p-md-2 {\n padding: 0.5rem !important;\n }\n\n .p-md-3 {\n padding: 1rem !important;\n }\n\n .p-md-4 {\n padding: 1.5rem !important;\n }\n\n .p-md-5 {\n padding: 3rem !important;\n }\n\n .px-md-0 {\n padding-right: 0 !important;\n padding-left: 0 !important;\n }\n\n .px-md-1 {\n padding-right: 0.25rem !important;\n padding-left: 0.25rem !important;\n }\n\n .px-md-2 {\n padding-right: 0.5rem !important;\n padding-left: 0.5rem !important;\n }\n\n .px-md-3 {\n padding-right: 1rem !important;\n padding-left: 1rem !important;\n }\n\n .px-md-4 {\n padding-right: 1.5rem !important;\n padding-left: 1.5rem !important;\n }\n\n .px-md-5 {\n padding-right: 3rem !important;\n padding-left: 3rem !important;\n }\n\n .py-md-0 {\n padding-top: 0 !important;\n padding-bottom: 0 !important;\n }\n\n .py-md-1 {\n padding-top: 0.25rem !important;\n padding-bottom: 0.25rem !important;\n }\n\n .py-md-2 {\n padding-top: 0.5rem !important;\n padding-bottom: 0.5rem !important;\n }\n\n .py-md-3 {\n padding-top: 1rem !important;\n padding-bottom: 1rem !important;\n }\n\n .py-md-4 {\n padding-top: 1.5rem !important;\n padding-bottom: 1.5rem !important;\n }\n\n .py-md-5 {\n padding-top: 3rem !important;\n padding-bottom: 3rem !important;\n }\n\n .pt-md-0 {\n padding-top: 0 !important;\n }\n\n .pt-md-1 {\n padding-top: 0.25rem !important;\n }\n\n .pt-md-2 {\n padding-top: 0.5rem !important;\n }\n\n .pt-md-3 {\n padding-top: 1rem !important;\n }\n\n .pt-md-4 {\n padding-top: 1.5rem !important;\n }\n\n .pt-md-5 {\n padding-top: 3rem !important;\n }\n\n .pe-md-0 {\n padding-right: 0 !important;\n }\n\n .pe-md-1 {\n padding-right: 0.25rem !important;\n }\n\n .pe-md-2 {\n padding-right: 0.5rem !important;\n }\n\n .pe-md-3 {\n padding-right: 1rem !important;\n }\n\n .pe-md-4 {\n padding-right: 1.5rem !important;\n }\n\n .pe-md-5 {\n padding-right: 3rem !important;\n }\n\n .pb-md-0 {\n padding-bottom: 0 !important;\n }\n\n .pb-md-1 {\n padding-bottom: 0.25rem !important;\n }\n\n .pb-md-2 {\n padding-bottom: 0.5rem !important;\n }\n\n .pb-md-3 {\n padding-bottom: 1rem !important;\n }\n\n .pb-md-4 {\n padding-bottom: 1.5rem !important;\n }\n\n .pb-md-5 {\n padding-bottom: 3rem !important;\n }\n\n .ps-md-0 {\n padding-left: 0 !important;\n }\n\n .ps-md-1 {\n padding-left: 0.25rem !important;\n }\n\n .ps-md-2 {\n padding-left: 0.5rem !important;\n }\n\n .ps-md-3 {\n padding-left: 1rem !important;\n }\n\n .ps-md-4 {\n padding-left: 1.5rem !important;\n }\n\n .ps-md-5 {\n padding-left: 3rem !important;\n }\n\n .text-md-start {\n text-align: left !important;\n }\n\n .text-md-end {\n text-align: right !important;\n }\n\n .text-md-center {\n text-align: center !important;\n }\n}\n@media (min-width: 992px) {\n .float-lg-start {\n float: left !important;\n }\n\n .float-lg-end {\n float: right !important;\n }\n\n .float-lg-none {\n float: none !important;\n }\n\n .d-lg-inline {\n display: inline !important;\n }\n\n .d-lg-inline-block {\n display: inline-block !important;\n }\n\n .d-lg-block {\n display: block !important;\n }\n\n .d-lg-grid {\n display: grid !important;\n }\n\n .d-lg-table {\n display: table !important;\n }\n\n .d-lg-table-row {\n display: table-row !important;\n }\n\n .d-lg-table-cell {\n display: table-cell !important;\n }\n\n .d-lg-flex {\n display: flex !important;\n }\n\n .d-lg-inline-flex {\n display: inline-flex !important;\n }\n\n .d-lg-none {\n display: none !important;\n }\n\n .flex-lg-fill {\n flex: 1 1 auto !important;\n }\n\n .flex-lg-row {\n flex-direction: row !important;\n }\n\n .flex-lg-column {\n flex-direction: column !important;\n }\n\n .flex-lg-row-reverse {\n flex-direction: row-reverse !important;\n }\n\n .flex-lg-column-reverse {\n flex-direction: column-reverse !important;\n }\n\n .flex-lg-grow-0 {\n flex-grow: 0 !important;\n }\n\n .flex-lg-grow-1 {\n flex-grow: 1 !important;\n }\n\n .flex-lg-shrink-0 {\n flex-shrink: 0 !important;\n }\n\n .flex-lg-shrink-1 {\n flex-shrink: 1 !important;\n }\n\n .flex-lg-wrap {\n flex-wrap: wrap !important;\n }\n\n .flex-lg-nowrap {\n flex-wrap: nowrap !important;\n }\n\n .flex-lg-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n\n .gap-lg-0 {\n gap: 0 !important;\n }\n\n .gap-lg-1 {\n gap: 0.25rem !important;\n }\n\n .gap-lg-2 {\n gap: 0.5rem !important;\n }\n\n .gap-lg-3 {\n gap: 1rem !important;\n }\n\n .gap-lg-4 {\n gap: 1.5rem !important;\n }\n\n .gap-lg-5 {\n gap: 3rem !important;\n }\n\n .justify-content-lg-start {\n justify-content: flex-start !important;\n }\n\n .justify-content-lg-end {\n justify-content: flex-end !important;\n }\n\n .justify-content-lg-center {\n justify-content: center !important;\n }\n\n .justify-content-lg-between {\n justify-content: space-between !important;\n }\n\n .justify-content-lg-around {\n justify-content: space-around !important;\n }\n\n .justify-content-lg-evenly {\n justify-content: space-evenly !important;\n }\n\n .align-items-lg-start {\n align-items: flex-start !important;\n }\n\n .align-items-lg-end {\n align-items: flex-end !important;\n }\n\n .align-items-lg-center {\n align-items: center !important;\n }\n\n .align-items-lg-baseline {\n align-items: baseline !important;\n }\n\n .align-items-lg-stretch {\n align-items: stretch !important;\n }\n\n .align-content-lg-start {\n align-content: flex-start !important;\n }\n\n .align-content-lg-end {\n align-content: flex-end !important;\n }\n\n .align-content-lg-center {\n align-content: center !important;\n }\n\n .align-content-lg-between {\n align-content: space-between !important;\n }\n\n .align-content-lg-around {\n align-content: space-around !important;\n }\n\n .align-content-lg-stretch {\n align-content: stretch !important;\n }\n\n .align-self-lg-auto {\n align-self: auto !important;\n }\n\n .align-self-lg-start {\n align-self: flex-start !important;\n }\n\n .align-self-lg-end {\n align-self: flex-end !important;\n }\n\n .align-self-lg-center {\n align-self: center !important;\n }\n\n .align-self-lg-baseline {\n align-self: baseline !important;\n }\n\n .align-self-lg-stretch {\n align-self: stretch !important;\n }\n\n .order-lg-first {\n order: -1 !important;\n }\n\n .order-lg-0 {\n order: 0 !important;\n }\n\n .order-lg-1 {\n order: 1 !important;\n }\n\n .order-lg-2 {\n order: 2 !important;\n }\n\n .order-lg-3 {\n order: 3 !important;\n }\n\n .order-lg-4 {\n order: 4 !important;\n }\n\n .order-lg-5 {\n order: 5 !important;\n }\n\n .order-lg-last {\n order: 6 !important;\n }\n\n .m-lg-0 {\n margin: 0 !important;\n }\n\n .m-lg-1 {\n margin: 0.25rem !important;\n }\n\n .m-lg-2 {\n margin: 0.5rem !important;\n }\n\n .m-lg-3 {\n margin: 1rem !important;\n }\n\n .m-lg-4 {\n margin: 1.5rem !important;\n }\n\n .m-lg-5 {\n margin: 3rem !important;\n }\n\n .m-lg-auto {\n margin: auto !important;\n }\n\n .mx-lg-0 {\n margin-right: 0 !important;\n margin-left: 0 !important;\n }\n\n .mx-lg-1 {\n margin-right: 0.25rem !important;\n margin-left: 0.25rem !important;\n }\n\n .mx-lg-2 {\n margin-right: 0.5rem !important;\n margin-left: 0.5rem !important;\n }\n\n .mx-lg-3 {\n margin-right: 1rem !important;\n margin-left: 1rem !important;\n }\n\n .mx-lg-4 {\n margin-right: 1.5rem !important;\n margin-left: 1.5rem !important;\n }\n\n .mx-lg-5 {\n margin-right: 3rem !important;\n margin-left: 3rem !important;\n }\n\n .mx-lg-auto {\n margin-right: auto !important;\n margin-left: auto !important;\n }\n\n .my-lg-0 {\n margin-top: 0 !important;\n margin-bottom: 0 !important;\n }\n\n .my-lg-1 {\n margin-top: 0.25rem !important;\n margin-bottom: 0.25rem !important;\n }\n\n .my-lg-2 {\n margin-top: 0.5rem !important;\n margin-bottom: 0.5rem !important;\n }\n\n .my-lg-3 {\n margin-top: 1rem !important;\n margin-bottom: 1rem !important;\n }\n\n .my-lg-4 {\n margin-top: 1.5rem !important;\n margin-bottom: 1.5rem !important;\n }\n\n .my-lg-5 {\n margin-top: 3rem !important;\n margin-bottom: 3rem !important;\n }\n\n .my-lg-auto {\n margin-top: auto !important;\n margin-bottom: auto !important;\n }\n\n .mt-lg-0 {\n margin-top: 0 !important;\n }\n\n .mt-lg-1 {\n margin-top: 0.25rem !important;\n }\n\n .mt-lg-2 {\n margin-top: 0.5rem !important;\n }\n\n .mt-lg-3 {\n margin-top: 1rem !important;\n }\n\n .mt-lg-4 {\n margin-top: 1.5rem !important;\n }\n\n .mt-lg-5 {\n margin-top: 3rem !important;\n }\n\n .mt-lg-auto {\n margin-top: auto !important;\n }\n\n .me-lg-0 {\n margin-right: 0 !important;\n }\n\n .me-lg-1 {\n margin-right: 0.25rem !important;\n }\n\n .me-lg-2 {\n margin-right: 0.5rem !important;\n }\n\n .me-lg-3 {\n margin-right: 1rem !important;\n }\n\n .me-lg-4 {\n margin-right: 1.5rem !important;\n }\n\n .me-lg-5 {\n margin-right: 3rem !important;\n }\n\n .me-lg-auto {\n margin-right: auto !important;\n }\n\n .mb-lg-0 {\n margin-bottom: 0 !important;\n }\n\n .mb-lg-1 {\n margin-bottom: 0.25rem !important;\n }\n\n .mb-lg-2 {\n margin-bottom: 0.5rem !important;\n }\n\n .mb-lg-3 {\n margin-bottom: 1rem !important;\n }\n\n .mb-lg-4 {\n margin-bottom: 1.5rem !important;\n }\n\n .mb-lg-5 {\n margin-bottom: 3rem !important;\n }\n\n .mb-lg-auto {\n margin-bottom: auto !important;\n }\n\n .ms-lg-0 {\n margin-left: 0 !important;\n }\n\n .ms-lg-1 {\n margin-left: 0.25rem !important;\n }\n\n .ms-lg-2 {\n margin-left: 0.5rem !important;\n }\n\n .ms-lg-3 {\n margin-left: 1rem !important;\n }\n\n .ms-lg-4 {\n margin-left: 1.5rem !important;\n }\n\n .ms-lg-5 {\n margin-left: 3rem !important;\n }\n\n .ms-lg-auto {\n margin-left: auto !important;\n }\n\n .p-lg-0 {\n padding: 0 !important;\n }\n\n .p-lg-1 {\n padding: 0.25rem !important;\n }\n\n .p-lg-2 {\n padding: 0.5rem !important;\n }\n\n .p-lg-3 {\n padding: 1rem !important;\n }\n\n .p-lg-4 {\n padding: 1.5rem !important;\n }\n\n .p-lg-5 {\n padding: 3rem !important;\n }\n\n .px-lg-0 {\n padding-right: 0 !important;\n padding-left: 0 !important;\n }\n\n .px-lg-1 {\n padding-right: 0.25rem !important;\n padding-left: 0.25rem !important;\n }\n\n .px-lg-2 {\n padding-right: 0.5rem !important;\n padding-left: 0.5rem !important;\n }\n\n .px-lg-3 {\n padding-right: 1rem !important;\n padding-left: 1rem !important;\n }\n\n .px-lg-4 {\n padding-right: 1.5rem !important;\n padding-left: 1.5rem !important;\n }\n\n .px-lg-5 {\n padding-right: 3rem !important;\n padding-left: 3rem !important;\n }\n\n .py-lg-0 {\n padding-top: 0 !important;\n padding-bottom: 0 !important;\n }\n\n .py-lg-1 {\n padding-top: 0.25rem !important;\n padding-bottom: 0.25rem !important;\n }\n\n .py-lg-2 {\n padding-top: 0.5rem !important;\n padding-bottom: 0.5rem !important;\n }\n\n .py-lg-3 {\n padding-top: 1rem !important;\n padding-bottom: 1rem !important;\n }\n\n .py-lg-4 {\n padding-top: 1.5rem !important;\n padding-bottom: 1.5rem !important;\n }\n\n .py-lg-5 {\n padding-top: 3rem !important;\n padding-bottom: 3rem !important;\n }\n\n .pt-lg-0 {\n padding-top: 0 !important;\n }\n\n .pt-lg-1 {\n padding-top: 0.25rem !important;\n }\n\n .pt-lg-2 {\n padding-top: 0.5rem !important;\n }\n\n .pt-lg-3 {\n padding-top: 1rem !important;\n }\n\n .pt-lg-4 {\n padding-top: 1.5rem !important;\n }\n\n .pt-lg-5 {\n padding-top: 3rem !important;\n }\n\n .pe-lg-0 {\n padding-right: 0 !important;\n }\n\n .pe-lg-1 {\n padding-right: 0.25rem !important;\n }\n\n .pe-lg-2 {\n padding-right: 0.5rem !important;\n }\n\n .pe-lg-3 {\n padding-right: 1rem !important;\n }\n\n .pe-lg-4 {\n padding-right: 1.5rem !important;\n }\n\n .pe-lg-5 {\n padding-right: 3rem !important;\n }\n\n .pb-lg-0 {\n padding-bottom: 0 !important;\n }\n\n .pb-lg-1 {\n padding-bottom: 0.25rem !important;\n }\n\n .pb-lg-2 {\n padding-bottom: 0.5rem !important;\n }\n\n .pb-lg-3 {\n padding-bottom: 1rem !important;\n }\n\n .pb-lg-4 {\n padding-bottom: 1.5rem !important;\n }\n\n .pb-lg-5 {\n padding-bottom: 3rem !important;\n }\n\n .ps-lg-0 {\n padding-left: 0 !important;\n }\n\n .ps-lg-1 {\n padding-left: 0.25rem !important;\n }\n\n .ps-lg-2 {\n padding-left: 0.5rem !important;\n }\n\n .ps-lg-3 {\n padding-left: 1rem !important;\n }\n\n .ps-lg-4 {\n padding-left: 1.5rem !important;\n }\n\n .ps-lg-5 {\n padding-left: 3rem !important;\n }\n\n .text-lg-start {\n text-align: left !important;\n }\n\n .text-lg-end {\n text-align: right !important;\n }\n\n .text-lg-center {\n text-align: center !important;\n }\n}\n@media (min-width: 1200px) {\n .float-xl-start {\n float: left !important;\n }\n\n .float-xl-end {\n float: right !important;\n }\n\n .float-xl-none {\n float: none !important;\n }\n\n .d-xl-inline {\n display: inline !important;\n }\n\n .d-xl-inline-block {\n display: inline-block !important;\n }\n\n .d-xl-block {\n display: block !important;\n }\n\n .d-xl-grid {\n display: grid !important;\n }\n\n .d-xl-table {\n display: table !important;\n }\n\n .d-xl-table-row {\n display: table-row !important;\n }\n\n .d-xl-table-cell {\n display: table-cell !important;\n }\n\n .d-xl-flex {\n display: flex !important;\n }\n\n .d-xl-inline-flex {\n display: inline-flex !important;\n }\n\n .d-xl-none {\n display: none !important;\n }\n\n .flex-xl-fill {\n flex: 1 1 auto !important;\n }\n\n .flex-xl-row {\n flex-direction: row !important;\n }\n\n .flex-xl-column {\n flex-direction: column !important;\n }\n\n .flex-xl-row-reverse {\n flex-direction: row-reverse !important;\n }\n\n .flex-xl-column-reverse {\n flex-direction: column-reverse !important;\n }\n\n .flex-xl-grow-0 {\n flex-grow: 0 !important;\n }\n\n .flex-xl-grow-1 {\n flex-grow: 1 !important;\n }\n\n .flex-xl-shrink-0 {\n flex-shrink: 0 !important;\n }\n\n .flex-xl-shrink-1 {\n flex-shrink: 1 !important;\n }\n\n .flex-xl-wrap {\n flex-wrap: wrap !important;\n }\n\n .flex-xl-nowrap {\n flex-wrap: nowrap !important;\n }\n\n .flex-xl-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n\n .gap-xl-0 {\n gap: 0 !important;\n }\n\n .gap-xl-1 {\n gap: 0.25rem !important;\n }\n\n .gap-xl-2 {\n gap: 0.5rem !important;\n }\n\n .gap-xl-3 {\n gap: 1rem !important;\n }\n\n .gap-xl-4 {\n gap: 1.5rem !important;\n }\n\n .gap-xl-5 {\n gap: 3rem !important;\n }\n\n .justify-content-xl-start {\n justify-content: flex-start !important;\n }\n\n .justify-content-xl-end {\n justify-content: flex-end !important;\n }\n\n .justify-content-xl-center {\n justify-content: center !important;\n }\n\n .justify-content-xl-between {\n justify-content: space-between !important;\n }\n\n .justify-content-xl-around {\n justify-content: space-around !important;\n }\n\n .justify-content-xl-evenly {\n justify-content: space-evenly !important;\n }\n\n .align-items-xl-start {\n align-items: flex-start !important;\n }\n\n .align-items-xl-end {\n align-items: flex-end !important;\n }\n\n .align-items-xl-center {\n align-items: center !important;\n }\n\n .align-items-xl-baseline {\n align-items: baseline !important;\n }\n\n .align-items-xl-stretch {\n align-items: stretch !important;\n }\n\n .align-content-xl-start {\n align-content: flex-start !important;\n }\n\n .align-content-xl-end {\n align-content: flex-end !important;\n }\n\n .align-content-xl-center {\n align-content: center !important;\n }\n\n .align-content-xl-between {\n align-content: space-between !important;\n }\n\n .align-content-xl-around {\n align-content: space-around !important;\n }\n\n .align-content-xl-stretch {\n align-content: stretch !important;\n }\n\n .align-self-xl-auto {\n align-self: auto !important;\n }\n\n .align-self-xl-start {\n align-self: flex-start !important;\n }\n\n .align-self-xl-end {\n align-self: flex-end !important;\n }\n\n .align-self-xl-center {\n align-self: center !important;\n }\n\n .align-self-xl-baseline {\n align-self: baseline !important;\n }\n\n .align-self-xl-stretch {\n align-self: stretch !important;\n }\n\n .order-xl-first {\n order: -1 !important;\n }\n\n .order-xl-0 {\n order: 0 !important;\n }\n\n .order-xl-1 {\n order: 1 !important;\n }\n\n .order-xl-2 {\n order: 2 !important;\n }\n\n .order-xl-3 {\n order: 3 !important;\n }\n\n .order-xl-4 {\n order: 4 !important;\n }\n\n .order-xl-5 {\n order: 5 !important;\n }\n\n .order-xl-last {\n order: 6 !important;\n }\n\n .m-xl-0 {\n margin: 0 !important;\n }\n\n .m-xl-1 {\n margin: 0.25rem !important;\n }\n\n .m-xl-2 {\n margin: 0.5rem !important;\n }\n\n .m-xl-3 {\n margin: 1rem !important;\n }\n\n .m-xl-4 {\n margin: 1.5rem !important;\n }\n\n .m-xl-5 {\n margin: 3rem !important;\n }\n\n .m-xl-auto {\n margin: auto !important;\n }\n\n .mx-xl-0 {\n margin-right: 0 !important;\n margin-left: 0 !important;\n }\n\n .mx-xl-1 {\n margin-right: 0.25rem !important;\n margin-left: 0.25rem !important;\n }\n\n .mx-xl-2 {\n margin-right: 0.5rem !important;\n margin-left: 0.5rem !important;\n }\n\n .mx-xl-3 {\n margin-right: 1rem !important;\n margin-left: 1rem !important;\n }\n\n .mx-xl-4 {\n margin-right: 1.5rem !important;\n margin-left: 1.5rem !important;\n }\n\n .mx-xl-5 {\n margin-right: 3rem !important;\n margin-left: 3rem !important;\n }\n\n .mx-xl-auto {\n margin-right: auto !important;\n margin-left: auto !important;\n }\n\n .my-xl-0 {\n margin-top: 0 !important;\n margin-bottom: 0 !important;\n }\n\n .my-xl-1 {\n margin-top: 0.25rem !important;\n margin-bottom: 0.25rem !important;\n }\n\n .my-xl-2 {\n margin-top: 0.5rem !important;\n margin-bottom: 0.5rem !important;\n }\n\n .my-xl-3 {\n margin-top: 1rem !important;\n margin-bottom: 1rem !important;\n }\n\n .my-xl-4 {\n margin-top: 1.5rem !important;\n margin-bottom: 1.5rem !important;\n }\n\n .my-xl-5 {\n margin-top: 3rem !important;\n margin-bottom: 3rem !important;\n }\n\n .my-xl-auto {\n margin-top: auto !important;\n margin-bottom: auto !important;\n }\n\n .mt-xl-0 {\n margin-top: 0 !important;\n }\n\n .mt-xl-1 {\n margin-top: 0.25rem !important;\n }\n\n .mt-xl-2 {\n margin-top: 0.5rem !important;\n }\n\n .mt-xl-3 {\n margin-top: 1rem !important;\n }\n\n .mt-xl-4 {\n margin-top: 1.5rem !important;\n }\n\n .mt-xl-5 {\n margin-top: 3rem !important;\n }\n\n .mt-xl-auto {\n margin-top: auto !important;\n }\n\n .me-xl-0 {\n margin-right: 0 !important;\n }\n\n .me-xl-1 {\n margin-right: 0.25rem !important;\n }\n\n .me-xl-2 {\n margin-right: 0.5rem !important;\n }\n\n .me-xl-3 {\n margin-right: 1rem !important;\n }\n\n .me-xl-4 {\n margin-right: 1.5rem !important;\n }\n\n .me-xl-5 {\n margin-right: 3rem !important;\n }\n\n .me-xl-auto {\n margin-right: auto !important;\n }\n\n .mb-xl-0 {\n margin-bottom: 0 !important;\n }\n\n .mb-xl-1 {\n margin-bottom: 0.25rem !important;\n }\n\n .mb-xl-2 {\n margin-bottom: 0.5rem !important;\n }\n\n .mb-xl-3 {\n margin-bottom: 1rem !important;\n }\n\n .mb-xl-4 {\n margin-bottom: 1.5rem !important;\n }\n\n .mb-xl-5 {\n margin-bottom: 3rem !important;\n }\n\n .mb-xl-auto {\n margin-bottom: auto !important;\n }\n\n .ms-xl-0 {\n margin-left: 0 !important;\n }\n\n .ms-xl-1 {\n margin-left: 0.25rem !important;\n }\n\n .ms-xl-2 {\n margin-left: 0.5rem !important;\n }\n\n .ms-xl-3 {\n margin-left: 1rem !important;\n }\n\n .ms-xl-4 {\n margin-left: 1.5rem !important;\n }\n\n .ms-xl-5 {\n margin-left: 3rem !important;\n }\n\n .ms-xl-auto {\n margin-left: auto !important;\n }\n\n .p-xl-0 {\n padding: 0 !important;\n }\n\n .p-xl-1 {\n padding: 0.25rem !important;\n }\n\n .p-xl-2 {\n padding: 0.5rem !important;\n }\n\n .p-xl-3 {\n padding: 1rem !important;\n }\n\n .p-xl-4 {\n padding: 1.5rem !important;\n }\n\n .p-xl-5 {\n padding: 3rem !important;\n }\n\n .px-xl-0 {\n padding-right: 0 !important;\n padding-left: 0 !important;\n }\n\n .px-xl-1 {\n padding-right: 0.25rem !important;\n padding-left: 0.25rem !important;\n }\n\n .px-xl-2 {\n padding-right: 0.5rem !important;\n padding-left: 0.5rem !important;\n }\n\n .px-xl-3 {\n padding-right: 1rem !important;\n padding-left: 1rem !important;\n }\n\n .px-xl-4 {\n padding-right: 1.5rem !important;\n padding-left: 1.5rem !important;\n }\n\n .px-xl-5 {\n padding-right: 3rem !important;\n padding-left: 3rem !important;\n }\n\n .py-xl-0 {\n padding-top: 0 !important;\n padding-bottom: 0 !important;\n }\n\n .py-xl-1 {\n padding-top: 0.25rem !important;\n padding-bottom: 0.25rem !important;\n }\n\n .py-xl-2 {\n padding-top: 0.5rem !important;\n padding-bottom: 0.5rem !important;\n }\n\n .py-xl-3 {\n padding-top: 1rem !important;\n padding-bottom: 1rem !important;\n }\n\n .py-xl-4 {\n padding-top: 1.5rem !important;\n padding-bottom: 1.5rem !important;\n }\n\n .py-xl-5 {\n padding-top: 3rem !important;\n padding-bottom: 3rem !important;\n }\n\n .pt-xl-0 {\n padding-top: 0 !important;\n }\n\n .pt-xl-1 {\n padding-top: 0.25rem !important;\n }\n\n .pt-xl-2 {\n padding-top: 0.5rem !important;\n }\n\n .pt-xl-3 {\n padding-top: 1rem !important;\n }\n\n .pt-xl-4 {\n padding-top: 1.5rem !important;\n }\n\n .pt-xl-5 {\n padding-top: 3rem !important;\n }\n\n .pe-xl-0 {\n padding-right: 0 !important;\n }\n\n .pe-xl-1 {\n padding-right: 0.25rem !important;\n }\n\n .pe-xl-2 {\n padding-right: 0.5rem !important;\n }\n\n .pe-xl-3 {\n padding-right: 1rem !important;\n }\n\n .pe-xl-4 {\n padding-right: 1.5rem !important;\n }\n\n .pe-xl-5 {\n padding-right: 3rem !important;\n }\n\n .pb-xl-0 {\n padding-bottom: 0 !important;\n }\n\n .pb-xl-1 {\n padding-bottom: 0.25rem !important;\n }\n\n .pb-xl-2 {\n padding-bottom: 0.5rem !important;\n }\n\n .pb-xl-3 {\n padding-bottom: 1rem !important;\n }\n\n .pb-xl-4 {\n padding-bottom: 1.5rem !important;\n }\n\n .pb-xl-5 {\n padding-bottom: 3rem !important;\n }\n\n .ps-xl-0 {\n padding-left: 0 !important;\n }\n\n .ps-xl-1 {\n padding-left: 0.25rem !important;\n }\n\n .ps-xl-2 {\n padding-left: 0.5rem !important;\n }\n\n .ps-xl-3 {\n padding-left: 1rem !important;\n }\n\n .ps-xl-4 {\n padding-left: 1.5rem !important;\n }\n\n .ps-xl-5 {\n padding-left: 3rem !important;\n }\n\n .text-xl-start {\n text-align: left !important;\n }\n\n .text-xl-end {\n text-align: right !important;\n }\n\n .text-xl-center {\n text-align: center !important;\n }\n}\n@media (min-width: 1400px) {\n .float-xxl-start {\n float: left !important;\n }\n\n .float-xxl-end {\n float: right !important;\n }\n\n .float-xxl-none {\n float: none !important;\n }\n\n .d-xxl-inline {\n display: inline !important;\n }\n\n .d-xxl-inline-block {\n display: inline-block !important;\n }\n\n .d-xxl-block {\n display: block !important;\n }\n\n .d-xxl-grid {\n display: grid !important;\n }\n\n .d-xxl-table {\n display: table !important;\n }\n\n .d-xxl-table-row {\n display: table-row !important;\n }\n\n .d-xxl-table-cell {\n display: table-cell !important;\n }\n\n .d-xxl-flex {\n display: flex !important;\n }\n\n .d-xxl-inline-flex {\n display: inline-flex !important;\n }\n\n .d-xxl-none {\n display: none !important;\n }\n\n .flex-xxl-fill {\n flex: 1 1 auto !important;\n }\n\n .flex-xxl-row {\n flex-direction: row !important;\n }\n\n .flex-xxl-column {\n flex-direction: column !important;\n }\n\n .flex-xxl-row-reverse {\n flex-direction: row-reverse !important;\n }\n\n .flex-xxl-column-reverse {\n flex-direction: column-reverse !important;\n }\n\n .flex-xxl-grow-0 {\n flex-grow: 0 !important;\n }\n\n .flex-xxl-grow-1 {\n flex-grow: 1 !important;\n }\n\n .flex-xxl-shrink-0 {\n flex-shrink: 0 !important;\n }\n\n .flex-xxl-shrink-1 {\n flex-shrink: 1 !important;\n }\n\n .flex-xxl-wrap {\n flex-wrap: wrap !important;\n }\n\n .flex-xxl-nowrap {\n flex-wrap: nowrap !important;\n }\n\n .flex-xxl-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n\n .gap-xxl-0 {\n gap: 0 !important;\n }\n\n .gap-xxl-1 {\n gap: 0.25rem !important;\n }\n\n .gap-xxl-2 {\n gap: 0.5rem !important;\n }\n\n .gap-xxl-3 {\n gap: 1rem !important;\n }\n\n .gap-xxl-4 {\n gap: 1.5rem !important;\n }\n\n .gap-xxl-5 {\n gap: 3rem !important;\n }\n\n .justify-content-xxl-start {\n justify-content: flex-start !important;\n }\n\n .justify-content-xxl-end {\n justify-content: flex-end !important;\n }\n\n .justify-content-xxl-center {\n justify-content: center !important;\n }\n\n .justify-content-xxl-between {\n justify-content: space-between !important;\n }\n\n .justify-content-xxl-around {\n justify-content: space-around !important;\n }\n\n .justify-content-xxl-evenly {\n justify-content: space-evenly !important;\n }\n\n .align-items-xxl-start {\n align-items: flex-start !important;\n }\n\n .align-items-xxl-end {\n align-items: flex-end !important;\n }\n\n .align-items-xxl-center {\n align-items: center !important;\n }\n\n .align-items-xxl-baseline {\n align-items: baseline !important;\n }\n\n .align-items-xxl-stretch {\n align-items: stretch !important;\n }\n\n .align-content-xxl-start {\n align-content: flex-start !important;\n }\n\n .align-content-xxl-end {\n align-content: flex-end !important;\n }\n\n .align-content-xxl-center {\n align-content: center !important;\n }\n\n .align-content-xxl-between {\n align-content: space-between !important;\n }\n\n .align-content-xxl-around {\n align-content: space-around !important;\n }\n\n .align-content-xxl-stretch {\n align-content: stretch !important;\n }\n\n .align-self-xxl-auto {\n align-self: auto !important;\n }\n\n .align-self-xxl-start {\n align-self: flex-start !important;\n }\n\n .align-self-xxl-end {\n align-self: flex-end !important;\n }\n\n .align-self-xxl-center {\n align-self: center !important;\n }\n\n .align-self-xxl-baseline {\n align-self: baseline !important;\n }\n\n .align-self-xxl-stretch {\n align-self: stretch !important;\n }\n\n .order-xxl-first {\n order: -1 !important;\n }\n\n .order-xxl-0 {\n order: 0 !important;\n }\n\n .order-xxl-1 {\n order: 1 !important;\n }\n\n .order-xxl-2 {\n order: 2 !important;\n }\n\n .order-xxl-3 {\n order: 3 !important;\n }\n\n .order-xxl-4 {\n order: 4 !important;\n }\n\n .order-xxl-5 {\n order: 5 !important;\n }\n\n .order-xxl-last {\n order: 6 !important;\n }\n\n .m-xxl-0 {\n margin: 0 !important;\n }\n\n .m-xxl-1 {\n margin: 0.25rem !important;\n }\n\n .m-xxl-2 {\n margin: 0.5rem !important;\n }\n\n .m-xxl-3 {\n margin: 1rem !important;\n }\n\n .m-xxl-4 {\n margin: 1.5rem !important;\n }\n\n .m-xxl-5 {\n margin: 3rem !important;\n }\n\n .m-xxl-auto {\n margin: auto !important;\n }\n\n .mx-xxl-0 {\n margin-right: 0 !important;\n margin-left: 0 !important;\n }\n\n .mx-xxl-1 {\n margin-right: 0.25rem !important;\n margin-left: 0.25rem !important;\n }\n\n .mx-xxl-2 {\n margin-right: 0.5rem !important;\n margin-left: 0.5rem !important;\n }\n\n .mx-xxl-3 {\n margin-right: 1rem !important;\n margin-left: 1rem !important;\n }\n\n .mx-xxl-4 {\n margin-right: 1.5rem !important;\n margin-left: 1.5rem !important;\n }\n\n .mx-xxl-5 {\n margin-right: 3rem !important;\n margin-left: 3rem !important;\n }\n\n .mx-xxl-auto {\n margin-right: auto !important;\n margin-left: auto !important;\n }\n\n .my-xxl-0 {\n margin-top: 0 !important;\n margin-bottom: 0 !important;\n }\n\n .my-xxl-1 {\n margin-top: 0.25rem !important;\n margin-bottom: 0.25rem !important;\n }\n\n .my-xxl-2 {\n margin-top: 0.5rem !important;\n margin-bottom: 0.5rem !important;\n }\n\n .my-xxl-3 {\n margin-top: 1rem !important;\n margin-bottom: 1rem !important;\n }\n\n .my-xxl-4 {\n margin-top: 1.5rem !important;\n margin-bottom: 1.5rem !important;\n }\n\n .my-xxl-5 {\n margin-top: 3rem !important;\n margin-bottom: 3rem !important;\n }\n\n .my-xxl-auto {\n margin-top: auto !important;\n margin-bottom: auto !important;\n }\n\n .mt-xxl-0 {\n margin-top: 0 !important;\n }\n\n .mt-xxl-1 {\n margin-top: 0.25rem !important;\n }\n\n .mt-xxl-2 {\n margin-top: 0.5rem !important;\n }\n\n .mt-xxl-3 {\n margin-top: 1rem !important;\n }\n\n .mt-xxl-4 {\n margin-top: 1.5rem !important;\n }\n\n .mt-xxl-5 {\n margin-top: 3rem !important;\n }\n\n .mt-xxl-auto {\n margin-top: auto !important;\n }\n\n .me-xxl-0 {\n margin-right: 0 !important;\n }\n\n .me-xxl-1 {\n margin-right: 0.25rem !important;\n }\n\n .me-xxl-2 {\n margin-right: 0.5rem !important;\n }\n\n .me-xxl-3 {\n margin-right: 1rem !important;\n }\n\n .me-xxl-4 {\n margin-right: 1.5rem !important;\n }\n\n .me-xxl-5 {\n margin-right: 3rem !important;\n }\n\n .me-xxl-auto {\n margin-right: auto !important;\n }\n\n .mb-xxl-0 {\n margin-bottom: 0 !important;\n }\n\n .mb-xxl-1 {\n margin-bottom: 0.25rem !important;\n }\n\n .mb-xxl-2 {\n margin-bottom: 0.5rem !important;\n }\n\n .mb-xxl-3 {\n margin-bottom: 1rem !important;\n }\n\n .mb-xxl-4 {\n margin-bottom: 1.5rem !important;\n }\n\n .mb-xxl-5 {\n margin-bottom: 3rem !important;\n }\n\n .mb-xxl-auto {\n margin-bottom: auto !important;\n }\n\n .ms-xxl-0 {\n margin-left: 0 !important;\n }\n\n .ms-xxl-1 {\n margin-left: 0.25rem !important;\n }\n\n .ms-xxl-2 {\n margin-left: 0.5rem !important;\n }\n\n .ms-xxl-3 {\n margin-left: 1rem !important;\n }\n\n .ms-xxl-4 {\n margin-left: 1.5rem !important;\n }\n\n .ms-xxl-5 {\n margin-left: 3rem !important;\n }\n\n .ms-xxl-auto {\n margin-left: auto !important;\n }\n\n .p-xxl-0 {\n padding: 0 !important;\n }\n\n .p-xxl-1 {\n padding: 0.25rem !important;\n }\n\n .p-xxl-2 {\n padding: 0.5rem !important;\n }\n\n .p-xxl-3 {\n padding: 1rem !important;\n }\n\n .p-xxl-4 {\n padding: 1.5rem !important;\n }\n\n .p-xxl-5 {\n padding: 3rem !important;\n }\n\n .px-xxl-0 {\n padding-right: 0 !important;\n padding-left: 0 !important;\n }\n\n .px-xxl-1 {\n padding-right: 0.25rem !important;\n padding-left: 0.25rem !important;\n }\n\n .px-xxl-2 {\n padding-right: 0.5rem !important;\n padding-left: 0.5rem !important;\n }\n\n .px-xxl-3 {\n padding-right: 1rem !important;\n padding-left: 1rem !important;\n }\n\n .px-xxl-4 {\n padding-right: 1.5rem !important;\n padding-left: 1.5rem !important;\n }\n\n .px-xxl-5 {\n padding-right: 3rem !important;\n padding-left: 3rem !important;\n }\n\n .py-xxl-0 {\n padding-top: 0 !important;\n padding-bottom: 0 !important;\n }\n\n .py-xxl-1 {\n padding-top: 0.25rem !important;\n padding-bottom: 0.25rem !important;\n }\n\n .py-xxl-2 {\n padding-top: 0.5rem !important;\n padding-bottom: 0.5rem !important;\n }\n\n .py-xxl-3 {\n padding-top: 1rem !important;\n padding-bottom: 1rem !important;\n }\n\n .py-xxl-4 {\n padding-top: 1.5rem !important;\n padding-bottom: 1.5rem !important;\n }\n\n .py-xxl-5 {\n padding-top: 3rem !important;\n padding-bottom: 3rem !important;\n }\n\n .pt-xxl-0 {\n padding-top: 0 !important;\n }\n\n .pt-xxl-1 {\n padding-top: 0.25rem !important;\n }\n\n .pt-xxl-2 {\n padding-top: 0.5rem !important;\n }\n\n .pt-xxl-3 {\n padding-top: 1rem !important;\n }\n\n .pt-xxl-4 {\n padding-top: 1.5rem !important;\n }\n\n .pt-xxl-5 {\n padding-top: 3rem !important;\n }\n\n .pe-xxl-0 {\n padding-right: 0 !important;\n }\n\n .pe-xxl-1 {\n padding-right: 0.25rem !important;\n }\n\n .pe-xxl-2 {\n padding-right: 0.5rem !important;\n }\n\n .pe-xxl-3 {\n padding-right: 1rem !important;\n }\n\n .pe-xxl-4 {\n padding-right: 1.5rem !important;\n }\n\n .pe-xxl-5 {\n padding-right: 3rem !important;\n }\n\n .pb-xxl-0 {\n padding-bottom: 0 !important;\n }\n\n .pb-xxl-1 {\n padding-bottom: 0.25rem !important;\n }\n\n .pb-xxl-2 {\n padding-bottom: 0.5rem !important;\n }\n\n .pb-xxl-3 {\n padding-bottom: 1rem !important;\n }\n\n .pb-xxl-4 {\n padding-bottom: 1.5rem !important;\n }\n\n .pb-xxl-5 {\n padding-bottom: 3rem !important;\n }\n\n .ps-xxl-0 {\n padding-left: 0 !important;\n }\n\n .ps-xxl-1 {\n padding-left: 0.25rem !important;\n }\n\n .ps-xxl-2 {\n padding-left: 0.5rem !important;\n }\n\n .ps-xxl-3 {\n padding-left: 1rem !important;\n }\n\n .ps-xxl-4 {\n padding-left: 1.5rem !important;\n }\n\n .ps-xxl-5 {\n padding-left: 3rem !important;\n }\n\n .text-xxl-start {\n text-align: left !important;\n }\n\n .text-xxl-end {\n text-align: right !important;\n }\n\n .text-xxl-center {\n text-align: center !important;\n }\n}\n@media (min-width: 1200px) {\n .fs-1 {\n font-size: 2.5rem !important;\n }\n\n .fs-2 {\n font-size: 2rem !important;\n }\n\n .fs-3 {\n font-size: 1.75rem !important;\n }\n\n .fs-4 {\n font-size: 1.5rem !important;\n }\n}\n@media print {\n .d-print-inline {\n display: inline !important;\n }\n\n .d-print-inline-block {\n display: inline-block !important;\n }\n\n .d-print-block {\n display: block !important;\n }\n\n .d-print-grid {\n display: grid !important;\n }\n\n .d-print-table {\n display: table !important;\n }\n\n .d-print-table-row {\n display: table-row !important;\n }\n\n .d-print-table-cell {\n display: table-cell !important;\n }\n\n .d-print-flex {\n display: flex !important;\n }\n\n .d-print-inline-flex {\n display: inline-flex !important;\n }\n\n .d-print-none {\n display: none !important;\n }\n}\n\n/*# sourceMappingURL=bootstrap.css.map */\n","/*!\n * Bootstrap v5.0.2 (https://getbootstrap.com/)\n * Copyright 2011-2021 The Bootstrap Authors\n * Copyright 2011-2021 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n */\n\n// scss-docs-start import-stack\n// Configuration\n@import \"functions\";\n@import \"variables\";\n@import \"mixins\";\n@import \"utilities\";\n\n// Layout & components\n@import \"root\";\n@import \"reboot\";\n@import \"type\";\n@import \"images\";\n@import \"containers\";\n@import \"grid\";\n@import \"tables\";\n@import \"forms\";\n@import \"buttons\";\n@import \"transitions\";\n@import \"dropdown\";\n@import \"button-group\";\n@import \"nav\";\n@import \"navbar\";\n@import \"card\";\n@import \"accordion\";\n@import \"breadcrumb\";\n@import \"pagination\";\n@import \"badge\";\n@import \"alert\";\n@import \"progress\";\n@import \"list-group\";\n@import \"close\";\n@import \"toasts\";\n@import \"modal\";\n@import \"tooltip\";\n@import \"popover\";\n@import \"carousel\";\n@import \"spinners\";\n@import \"offcanvas\";\n\n// Helpers\n@import \"helpers\";\n\n// Utilities\n@import \"utilities/api\";\n// scss-docs-end import-stack\n",":root {\n // Custom variable values only support SassScript inside `#{}`.\n @each $color, $value in $colors {\n --#{$variable-prefix}#{$color}: #{$value};\n }\n\n @each $color, $value in $theme-colors {\n --#{$variable-prefix}#{$color}: #{$value};\n }\n\n // Use `inspect` for lists so that quoted items keep the quotes.\n // See https://github.com/sass/sass/issues/2383#issuecomment-336349172\n --#{$variable-prefix}font-sans-serif: #{inspect($font-family-sans-serif)};\n --#{$variable-prefix}font-monospace: #{inspect($font-family-monospace)};\n --#{$variable-prefix}gradient: #{$gradient};\n}\n","// stylelint-disable declaration-no-important, selector-no-qualifying-type, property-no-vendor-prefix\n\n\n// Reboot\n//\n// Normalization of HTML elements, manually forked from Normalize.css to remove\n// styles targeting irrelevant browsers while applying new styles.\n//\n// Normalize is licensed MIT. https://github.com/necolas/normalize.css\n\n\n// Document\n//\n// Change from `box-sizing: content-box` so that `width` is not affected by `padding` or `border`.\n\n*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\n\n// Root\n//\n// Ability to the value of the root font sizes, affecting the value of `rem`.\n// null by default, thus nothing is generated.\n\n:root {\n font-size: $font-size-root;\n\n @if $enable-smooth-scroll {\n @media (prefers-reduced-motion: no-preference) {\n scroll-behavior: smooth;\n }\n }\n}\n\n\n// Body\n//\n// 1. Remove the margin in all browsers.\n// 2. As a best practice, apply a default `background-color`.\n// 3. Prevent adjustments of font size after orientation changes in iOS.\n// 4. Change the default tap highlight to be completely transparent in iOS.\n\nbody {\n margin: 0; // 1\n font-family: $font-family-base;\n @include font-size($font-size-base);\n font-weight: $font-weight-base;\n line-height: $line-height-base;\n color: $body-color;\n text-align: $body-text-align;\n background-color: $body-bg; // 2\n -webkit-text-size-adjust: 100%; // 3\n -webkit-tap-highlight-color: rgba($black, 0); // 4\n}\n\n\n// Content grouping\n//\n// 1. Reset Firefox's gray color\n// 2. Set correct height and prevent the `size` attribute to make the `hr` look like an input field\n\nhr {\n margin: $hr-margin-y 0;\n color: $hr-color; // 1\n background-color: currentColor;\n border: 0;\n opacity: $hr-opacity;\n}\n\nhr:not([size]) {\n height: $hr-height; // 2\n}\n\n\n// Typography\n//\n// 1. Remove top margins from headings\n// By default, `

`-`

` all receive top and bottom margins. We nuke the top\n// margin for easier control within type scales as it avoids margin collapsing.\n\n%heading {\n margin-top: 0; // 1\n margin-bottom: $headings-margin-bottom;\n font-family: $headings-font-family;\n font-style: $headings-font-style;\n font-weight: $headings-font-weight;\n line-height: $headings-line-height;\n color: $headings-color;\n}\n\nh1 {\n @extend %heading;\n @include font-size($h1-font-size);\n}\n\nh2 {\n @extend %heading;\n @include font-size($h2-font-size);\n}\n\nh3 {\n @extend %heading;\n @include font-size($h3-font-size);\n}\n\nh4 {\n @extend %heading;\n @include font-size($h4-font-size);\n}\n\nh5 {\n @extend %heading;\n @include font-size($h5-font-size);\n}\n\nh6 {\n @extend %heading;\n @include font-size($h6-font-size);\n}\n\n\n// Reset margins on paragraphs\n//\n// Similarly, the top margin on `

`s get reset. However, we also reset the\n// bottom margin to use `rem` units instead of `em`.\n\np {\n margin-top: 0;\n margin-bottom: $paragraph-margin-bottom;\n}\n\n\n// Abbreviations\n//\n// 1. Duplicate behavior to the data-bs-* attribute for our tooltip plugin\n// 2. Add the correct text decoration in Chrome, Edge, Opera, and Safari.\n// 3. Add explicit cursor to indicate changed behavior.\n// 4. Prevent the text-decoration to be skipped.\n\nabbr[title],\nabbr[data-bs-original-title] { // 1\n text-decoration: underline dotted; // 2\n cursor: help; // 3\n text-decoration-skip-ink: none; // 4\n}\n\n\n// Address\n\naddress {\n margin-bottom: 1rem;\n font-style: normal;\n line-height: inherit;\n}\n\n\n// Lists\n\nol,\nul {\n padding-left: 2rem;\n}\n\nol,\nul,\ndl {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nol ol,\nul ul,\nol ul,\nul ol {\n margin-bottom: 0;\n}\n\ndt {\n font-weight: $dt-font-weight;\n}\n\n// 1. Undo browser default\n\ndd {\n margin-bottom: .5rem;\n margin-left: 0; // 1\n}\n\n\n// Blockquote\n\nblockquote {\n margin: 0 0 1rem;\n}\n\n\n// Strong\n//\n// Add the correct font weight in Chrome, Edge, and Safari\n\nb,\nstrong {\n font-weight: $font-weight-bolder;\n}\n\n\n// Small\n//\n// Add the correct font size in all browsers\n\nsmall {\n @include font-size($small-font-size);\n}\n\n\n// Mark\n\nmark {\n padding: $mark-padding;\n background-color: $mark-bg;\n}\n\n\n// Sub and Sup\n//\n// Prevent `sub` and `sup` elements from affecting the line height in\n// all browsers.\n\nsub,\nsup {\n position: relative;\n @include font-size($sub-sup-font-size);\n line-height: 0;\n vertical-align: baseline;\n}\n\nsub { bottom: -.25em; }\nsup { top: -.5em; }\n\n\n// Links\n\na {\n color: $link-color;\n text-decoration: $link-decoration;\n\n &:hover {\n color: $link-hover-color;\n text-decoration: $link-hover-decoration;\n }\n}\n\n// And undo these styles for placeholder links/named anchors (without href).\n// It would be more straightforward to just use a[href] in previous block, but that\n// causes specificity issues in many other styles that are too complex to fix.\n// See https://github.com/twbs/bootstrap/issues/19402\n\na:not([href]):not([class]) {\n &,\n &:hover {\n color: inherit;\n text-decoration: none;\n }\n}\n\n\n// Code\n\npre,\ncode,\nkbd,\nsamp {\n font-family: $font-family-code;\n @include font-size(1em); // Correct the odd `em` font sizing in all browsers.\n direction: ltr #{\"/* rtl:ignore */\"};\n unicode-bidi: bidi-override;\n}\n\n// 1. Remove browser default top margin\n// 2. Reset browser default of `1em` to use `rem`s\n// 3. Don't allow content to break outside\n\npre {\n display: block;\n margin-top: 0; // 1\n margin-bottom: 1rem; // 2\n overflow: auto; // 3\n @include font-size($code-font-size);\n color: $pre-color;\n\n // Account for some code outputs that place code tags in pre tags\n code {\n @include font-size(inherit);\n color: inherit;\n word-break: normal;\n }\n}\n\ncode {\n @include font-size($code-font-size);\n color: $code-color;\n word-wrap: break-word;\n\n // Streamline the style when inside anchors to avoid broken underline and more\n a > & {\n color: inherit;\n }\n}\n\nkbd {\n padding: $kbd-padding-y $kbd-padding-x;\n @include font-size($kbd-font-size);\n color: $kbd-color;\n background-color: $kbd-bg;\n @include border-radius($border-radius-sm);\n\n kbd {\n padding: 0;\n @include font-size(1em);\n font-weight: $nested-kbd-font-weight;\n }\n}\n\n\n// Figures\n//\n// Apply a consistent margin strategy (matches our type styles).\n\nfigure {\n margin: 0 0 1rem;\n}\n\n\n// Images and content\n\nimg,\nsvg {\n vertical-align: middle;\n}\n\n\n// Tables\n//\n// Prevent double borders\n\ntable {\n caption-side: bottom;\n border-collapse: collapse;\n}\n\ncaption {\n padding-top: $table-cell-padding-y;\n padding-bottom: $table-cell-padding-y;\n color: $table-caption-color;\n text-align: left;\n}\n\n// 1. Removes font-weight bold by inheriting\n// 2. Matches default `` alignment by inheriting `text-align`.\n// 3. Fix alignment for Safari\n\nth {\n font-weight: $table-th-font-weight; // 1\n text-align: inherit; // 2\n text-align: -webkit-match-parent; // 3\n}\n\nthead,\ntbody,\ntfoot,\ntr,\ntd,\nth {\n border-color: inherit;\n border-style: solid;\n border-width: 0;\n}\n\n\n// Forms\n//\n// 1. Allow labels to use `margin` for spacing.\n\nlabel {\n display: inline-block; // 1\n}\n\n// Remove the default `border-radius` that macOS Chrome adds.\n// See https://github.com/twbs/bootstrap/issues/24093\n\nbutton {\n // stylelint-disable-next-line property-disallowed-list\n border-radius: 0;\n}\n\n// Explicitly remove focus outline in Chromium when it shouldn't be\n// visible (e.g. as result of mouse click or touch tap). It already\n// should be doing this automatically, but seems to currently be\n// confused and applies its very visible two-tone outline anyway.\n\nbutton:focus:not(:focus-visible) {\n outline: 0;\n}\n\n// 1. Remove the margin in Firefox and Safari\n\ninput,\nbutton,\nselect,\noptgroup,\ntextarea {\n margin: 0; // 1\n font-family: inherit;\n @include font-size(inherit);\n line-height: inherit;\n}\n\n// Remove the inheritance of text transform in Firefox\nbutton,\nselect {\n text-transform: none;\n}\n// Set the cursor for non-` + + + + diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address/_verify_other_explorer.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address/_verify_other_explorer.html.eex new file mode 100644 index 0000000..4f656c1 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address/_verify_other_explorer.html.eex @@ -0,0 +1,15 @@ +<%= if @type=="address" do %> + +<% else %> + +<% end %> +

+
+

<%= @header %>

+ <%= if @type=="address" do %> +
<%= address_link_to_other_explorer(@address_link, @hash ,true) %>
+ <% else %> +
<%= address_link_to_other_explorer(@transaction_link, @hash ,true) %>
+ <% end %> +
+
\ No newline at end of file diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address/_verify_other_explorer_modal.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address/_verify_other_explorer_modal.html.eex new file mode 100644 index 0000000..462ec8e --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address/_verify_other_explorer_modal.html.eex @@ -0,0 +1,22 @@ +
+
+ +
+
+ + <%= if @type=="address" do %> + <%= link( + address_link_to_other_explorer(@address_link, @hash, false), + to: address_link_to_other_explorer(@address_link, @hash ,true) + ) %> + <% else %> + <%= link( + address_link_to_other_explorer(@transaction_link, @hash, false), + to: address_link_to_other_explorer(@transaction_link, @hash ,true) + ) %> + <% end %> + +
+
\ No newline at end of file diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address/_verify_other_explorers.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address/_verify_other_explorers.html.eex new file mode 100644 index 0000000..ca0af97 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address/_verify_other_explorers.html.eex @@ -0,0 +1,38 @@ +
+ +
+

Verify with other Explorers:

+
+ <%= render "_verify_other_explorer.html", hash: @hash, type: @type, header: "Etherscan.io", class: "etherscan", address_link: "https://etherscan.io/address/", transaction_link: "https://etherscan.io/tx/" %> + <%= render "_verify_other_explorer.html", hash: @hash, type: @type, header: "Blockchair.com", class: "blockchair", address_link: "https://blockchair.com/ethereum/address/", transaction_link: "https://blockchair.com/ethereum/transaction/" %> + <%= render "_verify_other_explorer.html", hash: @hash, type: @type, header: "Etherchain.org", class: "etherchain", address_link: "https://www.etherchain.org/account/", transaction_link: "https://www.etherchain.org/tx/" %> + + + + + + +
+
+ + + +
diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address/index.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address/index.html.eex new file mode 100644 index 0000000..57dba32 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address/index.html.eex @@ -0,0 +1,58 @@ +
+ <%= render BlockScoutWeb.Advertisement.TextAdView, "index.html", conn: @conn %> +
+
+

<%= Explorer.coin_name() %> <%= gettext "Addresses" %>

+ +
+ <%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "top", cur_page_number: "1", show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %> +
+ +
+
+ + + + + + + <%= if balance_percentage_enabled?(@total_supply) do %> + + <% end %> + + + + + <% columns_num = if balance_percentage_enabled?(@total_supply), do: 5, else: 4 %> + <%= render BlockScoutWeb.CommonComponentsView, "_table-loader.html", columns_num: columns_num %> + +
+
+   +
+
+
+ Address +
+
+
+ Balance +
+
+
+ Percentage +
+
+
+ Txn Count +
+
+
+
+ + <%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "bottom", cur_page_number: "1", show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %> +
+
+ + +
diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address/overview.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address/overview.html.eex new file mode 100644 index 0000000..3deb067 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address/overview.html.eex @@ -0,0 +1,293 @@ +
+ <%= render BlockScoutWeb.Advertisement.TextAdView, "index.html", conn: @conn %> + <% dark_forest_addresses_list_0_4 = CustomContractsHelper.get_custom_addresses_list(:dark_forest_addresses) %> + <% dark_forest_addresses_list_0_5 = CustomContractsHelper.get_custom_addresses_list(:dark_forest_addresses_v_0_5) %> + <% circles_addresses_list = CustomContractsHelper.get_custom_addresses_list(:circles_addresses) %> + <% current_address = "0x" <> Base.encode16(@address.hash.bytes, case: :lower) %> + <% created_from_address_hash = if from_address_hash(@address), do: "0x" <> Base.encode16(from_address_hash(@address).bytes, case: :lower), else: nil %> +
+ +
+
+
+ <%= cond do %> + <% Enum.member?(dark_forest_addresses_list_0_4, current_address) -> %> + <%= render BlockScoutWeb.AddressView, "_custom_view_df_title.html", title: "zkSnark space warfare (v0.4)" %> + <% Enum.member?(dark_forest_addresses_list_0_5, current_address) -> %> + <%= render BlockScoutWeb.AddressView, "_custom_view_df_title.html", title: "zkSnark space warfare (v0.5)" %> + <% Enum.member?(circles_addresses_list, current_address) -> %> +
+ +
+ <% Enum.member?(circles_addresses_list, created_from_address_hash) -> %> +
+ +
+ <% true -> %> + <%= nil %> + <% end %> +

+
<%= address_title(@address) %> <%= gettext "Details" %>
+ <%= render BlockScoutWeb.AddressView, "_labels.html", address_hash: @address.hash, tags: @tags %> + + + <%= render BlockScoutWeb.CommonComponentsView, "_btn_copy.html", + id: "tx-raw-input", + additional_classes: ["overview-title-item"], + clipboard_text: @address, + aria_label: gettext("Copy Address"), + title: gettext("Copy Address") %> + <%= render BlockScoutWeb.CommonComponentsView, "_btn_qr_code.html" %> + +

+

<%= @address %>

+ + + <% address_name = primary_name(@address) %> + <%= cond do %> + <% @address.token -> %> +
+
+ <%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip_2.html", + text: gettext("Token name and symbol.") %> + <%= gettext("Token") %> +
+
+ <%= link( + token_title(@address.token), + to: token_path(@conn, + :show, + @address.hash), + "data-test": + "token_hash_link" + ) + %> +
+
+ <% address_name -> %> + <%= if Address.smart_contract?(@address) do %> +
+
+ <%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip_2.html", + text: gettext("The name found in the source code of the Contract.") %> + <%= gettext("Contract Name") %> +
+
+ <%= short_contract_name(address_name, 30) %> +
+
+ <% else %> +
+
+ <%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip_2.html", + text: gettext("The name of the validator.") %> + <%= gettext("Validator Name") %> +
+
+ <%= short_contract_name(address_name, 30) %> +
+
+ <% end %> + <% true -> %> + <% end %> + + <% from_address_hash = from_address_hash(@address) %> + <%= if Address.smart_contract?(@address) do %> +
+
+ <%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip_2.html", + text: gettext("Transactions and address of creation.") %> + <%= gettext("Creator") %> +
+
+ <%= if from_address_hash do %> + <%= link( + trimmed_hash(from_address_hash(@address)), + to: address_path(@conn, :show, from_address_hash(@address)) + ) %> + + <%= gettext "at" %> + + <%= link( + trimmed_hash(transaction_hash(@address)), + to: transaction_path(@conn, :show, transaction_hash(@address)), + "data-test": "transaction_hash_link" + ) %> + <% else %> + + <% end %> +
+
+ <% end %> + + <%= if @is_proxy do %> + <% implementation = Implementation.get_implementation(@address.smart_contract) %> + <% implementation_address_ = implementation && Enum.at(implementation.address_hashes, 0) %> + <% name = implementation && Enum.at(implementation.names, 0) %> + <% implementation_address = if is_nil(implementation_address_), do: "0x0000000000000000000000000000000000000000", else: to_string(implementation_address_) %> +
+
+ <%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip_2.html", + text: gettext("Implementation address of the proxy contract.") %> + <%= gettext("Implementation") %> +
+
+ <%= link( + (if name, do: name <> " | " <> implementation_address, else: implementation_address), + to: address_path(@conn, :show, implementation_address), + class: "contract-address" + ) + %> +
+
+ <% end %> + +
+
+ <%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip_2.html", + text: gettext("Address balance in") <> " " <> Explorer.coin_name() <> " " <> gettext("doesn't include ERC20, ERC721, ERC1155 tokens).") %> + <%= gettext("Balance") %> +
+
+ <%= balance(@address) %> + <%= if !match?({:pending, _}, @coin_balance_status) && !empty_exchange_rate?(@exchange_rate) do %> + <% fiat_value = to_string(@exchange_rate.fiat_value) %> + + ( + ) + + <% end %> +
+
+ +
+
+ <%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip_2.html", + text: gettext("All tokens in the account and total value.") %> + <%= gettext("Tokens") %> +
+
+ <%= render BlockScoutWeb.AddressView, "_balance_dropdown.html", conn: @conn, address: @address %> +
+
+ +
+
+ <%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip_2.html", + text: gettext("Number of transactions related to this address.") %> + <%= gettext("Transactions") %> +
+
+ <%= if @conn.request_path |> String.contains?("/transactions") do %> + + <%= if @address.transactions_count do %> + <%= Number.Delimit.number_to_delimited(@address.transactions_count, precision: 0) %> <%= gettext("Transactions") %> + <% else %> + <%= render BlockScoutWeb.CommonComponentsView, "_loading_spinner.html", loading_text: gettext("Fetching transactions...") %> + <% end %> + + <% else %> + + <%= if @address.token_transfers_count do %> + <%= Number.Delimit.number_to_delimited(@address.transactions_count, precision: 0) %> <%= gettext("Transactions") %> + <% else %> + <%= render BlockScoutWeb.CommonComponentsView, "_loading_spinner.html", loading_text: gettext("Fetching transactions...") %> + <% end %> + + <% end %> +
+
+ +
+
+ <%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip_2.html", + text: gettext("Number of transfers to/from this address.") %> + <%= gettext("Transfers") %> +
+
+ <%= if @conn.request_path |> String.contains?("/token-transfers") do %> + + <%= if @address.token_transfers_count do %> + <%= Number.Delimit.number_to_delimited(@address.token_transfers_count, precision: 0) %> <%= gettext("Transfers") %> + <% else %> + <%= render BlockScoutWeb.CommonComponentsView, "_loading_spinner.html", loading_text: gettext("Fetching transfers...") %> + <% end %> + + <% else %> + + <%= if @address.token_transfers_count do %> + <%= Number.Delimit.number_to_delimited(@address.token_transfers_count, precision: 0) %> <%= gettext("Transfers") %> + <% else %> + <%= render BlockScoutWeb.CommonComponentsView, "_loading_spinner.html", loading_text: gettext("Fetching transfers...") %> + <% end %> + + <% end %> +
+
+ +
+
+ <%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip_2.html", + text: gettext("Gas used by the address.") %> + <%= gettext("Gas Used") %> +
+
+ + <%= if @address.gas_used do %> + <%= Number.Delimit.number_to_delimited(@address.gas_used, precision: 0) %> + <% else %> + <%= render BlockScoutWeb.CommonComponentsView, "_loading_spinner.html", loading_text: gettext("Fetching gas used...") %> + <% end %> + +
+
+ + <%= if @address.fetched_coin_balance_block_number do %> +
+
+ <%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip_2.html", + text: gettext("Block number in which the address was updated.") %> + <%= gettext("Last Balance Update") %> +
+
+ <%= link( + @address.fetched_coin_balance_block_number, + to: block_path(@conn, :show, @address.fetched_coin_balance_block_number), + class: "tile-title-lg" + ) %> +
+
+ <% end %> +
+ + +
+
+
+
+
+ + +<%= render BlockScoutWeb.CommonComponentsView, "_modal_qr_code.html", qr_code: qr_code(@address), title: @address %> + +<%= render BlockScoutWeb.Advertisement.BannersAdView, "_banner_728.html", conn: @conn %> diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_coin_balance/_coin_balances.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_coin_balance/_coin_balances.html.eex new file mode 100644 index 0000000..74e974b --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_coin_balance/_coin_balances.html.eex @@ -0,0 +1,31 @@ +
+
+
+ <%= link( + to: block_path(@conn, :show, @coin_balance.block_number), + class: "tile-title-lg" + ) do %> + <%= gettext "Block" %> <%= @coin_balance.block_number %> + <% end %> + <%= if @coin_balance.transaction_hash do %> + <%= link( + to: transaction_path(@conn, :show, @coin_balance.transaction_hash) + ) do %> + <%= @coin_balance.transaction_hash %> + <% end %> + <% end %> + +
+
+ + <%= delta_arrow(@coin_balance.delta) %> + <%= format_delta(@coin_balance.delta) %> + +
+
+ + <%= format(@coin_balance.value) %> + +
+
+
diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_coin_balance/index.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_coin_balance/index.html.eex new file mode 100644 index 0000000..27c10ec --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_coin_balance/index.html.eex @@ -0,0 +1,48 @@ +
+ + + <% is_proxy = SmartContractHelper.address_is_proxy?(@address) %> + + <%= render BlockScoutWeb.AddressView, "overview.html", address: @address, is_proxy: is_proxy, conn: @conn, exchange_rate: @exchange_rate, coin_balance_status: @coin_balance_status, counters_path: @counters_path, tags: @tags %> + +
+
+ <%= render BlockScoutWeb.AddressView, "_tabs.html", address: @address, is_proxy: is_proxy, conn: @conn %> +
+ <%= render BlockScoutWeb.CommonComponentsView, "_channel_disconnected_message.html", text: gettext("Connection Lost, click to load newer blocks") %> + +

<%= gettext "Balances" %>

+ + +
+ <%= render BlockScoutWeb.CommonComponentsView, "_loading_spinner.html", loading_text: gettext("Loading chart...") %> +
+ + + + <%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "top", cur_page_number: "1", show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %> + + + +
+
+ <%= gettext "There is no coin history for this address." %> +
+
+ +
+ <%= render BlockScoutWeb.CommonComponentsView, "_tile-loader.html" %> +
+ + <%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "bottom", cur_page_number: "1", show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %> + +
+ +
+
diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_contract/_metatags.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_contract/_metatags.html.eex new file mode 100644 index 0000000..3ef2a67 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_contract/_metatags.html.eex @@ -0,0 +1 @@ +<%= render BlockScoutWeb.AddressView, "_metatags.html", conn: @conn, address: @address %> diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_contract/index.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_contract/index.html.eex new file mode 100644 index 0000000..f0cbbff --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_contract/index.html.eex @@ -0,0 +1,260 @@ +<% contract_creation_code = contract_creation_code(@address) %> +<% minimal_proxy_template = EIP1167.get_implementation_smart_contract(@address.hash) %> +<% bytecode_twin_contract = SmartContract.get_address_verified_bytecode_twin_contract(@address.hash) %> +<% smart_contract_verified = BlockScoutWeb.API.V2.Helper.smart_contract_verified?(@address) %> +<% fully_verified = SmartContract.verified_with_full_match?(@address.hash)%> +<% additional_sources = BlockScoutWeb.API.V2.SmartContractView.get_additional_sources(@address.smart_contract, smart_contract_verified, bytecode_twin_contract) %> +<% visualize_sol2uml_enabled = Explorer.Visualize.Sol2uml.enabled?() %> +
+ <% is_proxy = SmartContractHelper.address_is_proxy?(@address) %> + + <%= render BlockScoutWeb.AddressView, "overview.html", address: @address, is_proxy: is_proxy, conn: @conn, exchange_rate: @exchange_rate, coin_balance_status: @coin_balance_status, counters_path: @counters_path, tags: @tags %> + +
+ <%= render BlockScoutWeb.AddressView, "_tabs.html", address: @address, is_proxy: is_proxy, conn: @conn %> +
+ <%= unless smart_contract_verified do %> + <%= if minimal_proxy_template do %> + <%= render BlockScoutWeb.CommonComponentsView, "_minimal_proxy_pattern.html", address_hash: bytecode_twin_contract.address_hash, conn: @conn %> + <% else %> + <%= if bytecode_twin_contract do %> + <% path = address_verify_contract_path(@conn, :new, @address.hash) %> +
+
+ <%= render BlockScoutWeb.CommonComponentsView, "_info.html" %> + <%= gettext("Contract is not verified. However, we found a verified contract with the same bytecode in Blockscout DB") %> <%= link( + bytecode_twin_contract.address_hash, + to: address_contract_path(@conn, :index, bytecode_twin_contract.address_hash)) %>.
<%= gettext("All metadata displayed below is from that contract. In order to verify current contract, click") %> <%= gettext("Verify & Publish") %> <%= gettext("button") %>
+
+ <%= link(gettext("Verify & Publish"), to: path, class: "button button-primary button-sm float-right ml-3", "data-test": "verify_and_publish") %> +
+ <% end %> + <% end %> + <% end %> + <%= if smart_contract_verified do %> + <%= if @address.smart_contract.is_changed_bytecode do %> + <%= render BlockScoutWeb.CommonComponentsView, "_changed_bytecode_warning.html" %> + <% else %> +
+ <%= render BlockScoutWeb.CommonComponentsView, "_changed_bytecode_warning.html" %> +
+ <% end %> + <% end %> + <%= if smart_contract_verified || (!smart_contract_verified && bytecode_twin_contract) do %> + <% target_contract = if smart_contract_verified, do: @address.smart_contract, else: bytecode_twin_contract %> + <%= if @address.smart_contract && @address.smart_contract.verified_via_sourcify && @address.smart_contract.partially_verified && smart_contract_verified do %> +
+ <%= gettext("This contract has been partially verified via Sourcify.") %> + <% else %> + <%= if @address.smart_contract && @address.smart_contract.verified_via_sourcify && smart_contract_verified do %> +
+ <%= gettext("This contract has been verified via Sourcify.") %> + <% end %> + <% end %> + <%= if @address.smart_contract && @address.smart_contract.verified_via_sourcify && smart_contract_verified do %> + target="_blank"> + View contract in Sourcify repository <%= render BlockScoutWeb.IconsView, "_external_link.html" %> + +
+ + <% end %> +
+
+
<%= gettext "Contract name:" %>
+
<%= target_contract.name %>
+


+


+
<%= gettext "Optimization enabled" %>
+
<%= format_optimization_text(target_contract.optimization) %>
+
+
+
<%= gettext "Compiler version" %>
+
<%= target_contract.compiler_version %>
+


+


+ <%= if target_contract.optimization && target_contract.optimization_runs do %> +
<%= gettext "Optimization runs" %>
+
<%= target_contract.optimization_runs %>
+ <% end %> +
+
+ <%= if smart_contract_verified && target_contract.evm_version do %> +
<%= gettext "EVM Version" %>
+
<%= target_contract.evm_version %>
+


+


+ <% end %> + <%= if target_contract.inserted_at do %> +
<%= gettext "Verified at" %>
+
<%= target_contract.inserted_at %>
+ <% end %> +
+
+ <%= if smart_contract_verified && target_contract.constructor_arguments do %> +
+
+

<%= gettext "Constructor Arguments" %>

+
+
+
<%= format_constructor_arguments(target_contract, @conn) %>
+              
+
+
+ <% end %> +
+
+

<%= target_contract.file_path || gettext "Contract source code" %>

+
+ <%= if visualize_sol2uml_enabled && !target_contract.is_vyper_contract && !is_nil(target_contract.abi) do %> + + +
+ + Sol2uml +
new
+
+
+
+ <% end %> + +
+
+
><%= target_contract.contract_source_code %>
+        
+ + <%= additional_sources |> Enum.with_index() |> Enum.map(fn {additional_source, index} -> %> +
+
+

<%= additional_source.file_name %>

+ +
+
<%= additional_source.contract_source_code %>
+          
+ <% end)%> + + <%= if !is_nil(target_contract.compiler_settings) do %> +
+
+

<%= gettext "Compiler Settings" %>

+ +
+
+
<%= format_smart_contract_abi(target_contract.compiler_settings) %>
+              
+
+
+ <% end %> + + <%= if !is_nil(target_contract.abi) do %> +
+
+

<%= gettext "Contract ABI" %>

+ +
+
+
<%= format_smart_contract_abi(target_contract.abi) %>
+              
+
+
+ <% end %> + + <% end %> +
+ <%= case contract_creation_code do %> + <% {:selfdestructed, transaction_init} -> %> +
+

<%= gettext "Contract Creation Code" %>

+ +
+
+

<%= gettext "Contracts that self destruct in their constructors have no contract code published and cannot be verified." %>

+

<%= gettext "Displaying the init data provided of the creating transaction." %>

+
+
+
<%= transaction_init %>
+
+ <% {:ok, contract_code} -> %> + <%= if creation_code(@address) do %> +
+

<%= gettext "Contract Creation Code" %>

+
+ + <%= if !fully_verified do %> + <% path = address_verify_contract_path(@conn, :new, @address.hash) %> + <%= link( + gettext("Verify & Publish"), + to: path, + class: "button button-primary button-sm float-right ml-3", + "data-test": "verify_and_publish" + ) %> + <% end %> +
+
+
+
<%= creation_code(@address) %>
+
+ <% end %> + <%= if fully_verified do %> +
+

<%= gettext "Deployed ByteCode" %>

+ +
+ <% else %> +
+
+

<%= gettext "Deployed ByteCode" %>

+
+
+ + <%= if !fully_verified and !creation_code(@address) do %> + <% path = address_verify_contract_path(@conn, :new, @address.hash) %> + <%= link( + gettext("Verify & Publish"), + to: path, + class: "button button-primary button-sm float-right ml-3", + "data-test": "verify_and_publish" + ) %> + <% end %> +
+
+ <% end %> +
+
<%= contract_code %>
+
+ <% end %> +
+ + <%= if smart_contract_verified || (!smart_contract_verified && bytecode_twin_contract) do %> + <% target_contract = if smart_contract_verified, do: @address.smart_contract, else: bytecode_twin_contract %> + <%= if target_contract.external_libraries && target_contract.external_libraries != [] do %> +
+
+

<%= gettext "External libraries" %>

+
+
+
<%= format_external_libraries(target_contract.external_libraries, @conn) %>
+              
+
+
+ <% end %> + <% end %> +
+
+ +
diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification/new.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification/new.html.eex new file mode 100644 index 0000000..c8969a8 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification/new.html.eex @@ -0,0 +1,130 @@ +
+ <%= render BlockScoutWeb.CommonComponentsView, "_channel_disconnected_message.html", text: gettext("Connection Lost") %> + +
+

<%= gettext "New Smart Contract Verification" %>

+ + <%= form_for @changeset, + address_contract_verification_path(@conn, :create), + [], + fn f -> %> + + <%= render BlockScoutWeb.AddressContractVerificationCommonFieldsView, "_contract_address_field.html", f: f %> + +
+
+ <%= label f, "Verify" %> +
+
+
+ <%= radio_button f, :verify_via, true, checked: true, class: "form-check-input verify-via-flattened-code", "aria-describedby": "verify_via-help-block" %> +
+ <%= label :verify_via, :true, gettext("Via flattened source code"), class: "radio-text" %> +
+
+ <%= radio_button f, :verify_via, false, class: "form-check-input verify-via-standard-json-input" %> +
+ <%= label :verify_via, :false, gettext("Via Standard Input JSON"), class: "radio-text" %> +
+ <%= if Application.get_env(:explorer, Explorer.ThirdPartyIntegrations.Sourcify)[:enabled] do %> +
+ <%= radio_button f, :verify_via, false, class: "form-check-input verify-via-sourcify" %> +
+ <%= label :verify_via, :false, gettext("Via Sourcify: Sources and metadata JSON file"), class: "radio-text" %> +
+ <% end %> + <%= if RustVerifierInterface.enabled?() do %> +
+ <%= radio_button f, :verify_via, false, class: "form-check-input verify-via-multi-part-files" %> +
+ <%= label :verify_via, :false, gettext("Via multi-part files"), class: "radio-text" %> +
+ <% end %> +
+ <%= radio_button f, :verify_via, false, class: "form-check-input verify-vyper-contract" %> +
+ <%= label :verify_via, :false, gettext("Vyper contract"), class: "radio-text" %> +
+
+ <%= error_tag f, :verify_via, id: "verify_via-help-block", class: "text-danger form-error" %> +
+
Choose a smart-contract verification method. Currently, Blockscout supports 2 methods:
+ 1. Verification through flattened source code. +
+ 2. Verification using Standard input JSON file.
+ 3. Verification through Sourcify.
+ a) if smart-contract already verified on Sourcify, it will automatically fetch the data from the repo
+ b) otherwise you will be asked to upload source files and JSON metadata file(s).
+ 4. Verification of Vyper contract. +
+ +
+
+ +
+ + <%= link( + gettext("Next"), + to: address_verify_contract_via_flattened_code_path(@conn, :new, @address_hash), + id: "verify_via_flattened_code_button", + class: "btn-full-primary mr-2", + "data-button-loading": "animation" + ) %> + <%= link( + gettext("Next"), + to: address_verify_contract_via_json_path(@conn, :new, @address_hash), + id: "verify_via_sourcify_button", + class: "btn-full-primary mr-2", + style: "display: none;", + "data-button-loading": "animation" + ) %> + <%= link( + gettext("Next"), + to: address_verify_vyper_contract_path(@conn, :new, @address_hash), + id: "verify_vyper_contract_button", + class: "btn-full-primary mr-2", + style: "display: none;", + "data-button-loading": "animation" + ) %> + <%= link( + gettext("Next"), + to: address_verify_contract_via_standard_json_input_path(@conn, :new, @address_hash), + id: "verify_via_standard_json_input_button", + class: "btn-full-primary mr-2", + style: "display: none;", + "data-button-loading": "animation" + ) %> + <%= link( + gettext("Next"), + to: address_verify_contract_via_multi_part_files_path(@conn, :new, @address_hash), + id: "verify_via_multi_part_files_button", + class: "btn-full-primary mr-2", + style: "display: none;", + "data-button-loading": "animation" + ) %> + <%= + link( + gettext("Cancel"), + class: "btn-no-border", + to: address_contract_path(@conn, :index, @address_hash) + ) + %> +
+ <% end %> +
+ +
+ diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_common_fields/_compiler_field.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_common_fields/_compiler_field.html.eex new file mode 100644 index 0000000..bd2a179 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_common_fields/_compiler_field.html.eex @@ -0,0 +1,10 @@ +
+
+ <%= label :smart_contract, :compiler_version, gettext("Compiler") %> +
+ <%= select @f, :compiler_version, @compiler_versions, class: "form-control border-rounded", "aria-describedby": "compiler-help-block", id: "smart_contract_compiler_version" %> + <%= error_tag @f, :compiler_version, id: "compiler-help-block", class: "text-danger form-error" %> +
+
<%= raw @tooltip %>
+
+
diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_common_fields/_constructor_args.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_common_fields/_constructor_args.html.eex new file mode 100644 index 0000000..5fa7785 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_common_fields/_constructor_args.html.eex @@ -0,0 +1,10 @@ +
+
+ <%= label @f, :constructor_arguments, gettext("ABI-encoded Constructor Arguments (if required by the contract)") %> +
+ <%= textarea @f, :constructor_arguments, class: "form-control border-rounded monospace", rows: 3, "aria-describedby": "contract-constructor-arguments-help-block" %> + <%= error_tag @f, :constructor_arguments, id: "contract-constructor-arguments-help-block", class: "text-danger form-error", "data-test": "contract-constructor-arguments-error" %> +
+
Add arguments in ABI hex encoded form. Constructor arguments are written right to left, and will be found at the end of the input created bytecode. They may also be parsed here.
+
+
\ No newline at end of file diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_common_fields/_contract_address_field.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_common_fields/_contract_address_field.html.eex new file mode 100644 index 0000000..6084b3b --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_common_fields/_contract_address_field.html.eex @@ -0,0 +1,10 @@ +
+
+ <%= label @f, :address_hash, gettext("Contract Address") %> +
+ <%= text_input @f, :address_hash, class: "form-control border-rounded", id: "smart_contract_address_hash", "aria-describedby": "contract-address-help-block", readonly: true %> + <%= error_tag @f, :address_hash, id: "contract-address-help-block", class: "text-danger form-error" %> +
+
The 0x address supplied on contract creation.
+
+
\ No newline at end of file diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_common_fields/_contract_name_field.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_common_fields/_contract_name_field.html.eex new file mode 100644 index 0000000..f4c39dd --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_common_fields/_contract_name_field.html.eex @@ -0,0 +1,10 @@ +
+
+ <%= label @f, :name, gettext("Contract Name") %> +
+ <%= text_input @f, :name, class: "form-control border-rounded", "aria-describedby": "contract-name-help-block", "data-test": "contract_name" %> + <%= error_tag @f, :name, id: "contract-name-help-block", class: "text-danger form-error" %> +
+
<%= raw @tooltip %>
+
+
\ No newline at end of file diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_common_fields/_fetch_constructor_args.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_common_fields/_fetch_constructor_args.html.eex new file mode 100644 index 0000000..c269a84 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_common_fields/_fetch_constructor_args.html.eex @@ -0,0 +1,20 @@ +
+
+ <%= label @f, :autodetect_constructor_args, gettext("Try to fetch constructor arguments automatically") %> +
+
+
+ <%= radio_button @f, :autodetect_constructor_args, false, class: "form-check-input autodetectfalse" %> +
+ <%= label @f, :autodetect_constructor_args_false, gettext("No"), class: "radio-text" %> +
+
+ <%= radio_button @f, :autodetect_constructor_args, true, class: "form-check-input autodetecttrue", "aria-describedby": "autodetect_constructor_args-help-block" %> +
+ <%= label @f, :autodetect_constructor_args_true, gettext("Yes"), class: "radio-text" %> +
+
+ <%= error_tag @f, :autodetect_constructor_args, id: "autodetect_constructor_args-help-block", class: "text-danger form-error" %> +
+
+
\ No newline at end of file diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_common_fields/_include_nightly_builds_field.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_common_fields/_include_nightly_builds_field.html.eex new file mode 100644 index 0000000..c452549 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_common_fields/_include_nightly_builds_field.html.eex @@ -0,0 +1,21 @@ +
+
+ <%= label @f, :nightly_builds, gettext("Include nightly builds") %> +
+
+
+ <%= radio_button @f, :nightly_builds, false, checked: true, class: "form-check-input nightly-builds-false" %> +
+ <%= label @f, :nightly_builds_false, gettext("No"), class: "radio-text" %> +
+
+ <%= radio_button @f, :nightly_builds, true, class: "form-check-input nightly-builds-true", "aria-describedby": "nightly_builds-help-block" %> +
+ <%= label @f, :nightly_builds_true, gettext("Yes"), class: "radio-text" %> +
+
+ <%= error_tag @f, :nightly_builds, id: "nightly_builds-help-block", class: "text-danger form-error" %> +
+
<%= gettext("Select yes if you want to show nightly builds.") %>
+
+
\ No newline at end of file diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_common_fields/_libraries_other.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_common_fields/_libraries_other.html.eex new file mode 100644 index 0000000..0397ede --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_common_fields/_libraries_other.html.eex @@ -0,0 +1,7 @@ +<%= for library_index <- 2..Application.get_env(:block_scout_web, :contract)[:verification_max_libraries] do %> +
+ <%= render BlockScoutWeb.AddressContractVerificationCommonFieldsView, "_library_name.html", index: library_index %> + + <%= render BlockScoutWeb.AddressContractVerificationCommonFieldsView, "_library_address.html", index: library_index %> +
+<% end %> \ No newline at end of file diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_common_fields/_library_address.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_common_fields/_library_address.html.eex new file mode 100644 index 0000000..9145188 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_common_fields/_library_address.html.eex @@ -0,0 +1,10 @@ +<% library_address = "library" <> to_string(@index) <> "_address" |> String.to_atom() %> +
+
+ <%= label :external_libraries, library_address, gettext("Library") <> " " <> to_string(@index) <> " " <> gettext("Address") %> +
+ <%= text_input :external_libraries, library_address, class: "form-control border-rounded", "aria-describedby": "contract-name-help-block" %> +
+
<%= if assigns[:tooltip_text] do @tooltip_text end %>
+
+
\ No newline at end of file diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_common_fields/_library_first.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_common_fields/_library_first.html.eex new file mode 100644 index 0000000..b49583e --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_common_fields/_library_first.html.eex @@ -0,0 +1,11 @@ +
+ <%= render BlockScoutWeb.AddressContractVerificationCommonFieldsView, "_library_name.html", + index: 1, + tooltip_text: gettext("A library name called in the .sol file. Multiple libraries (up to ") <> to_string(Application.get_env(:block_scout_web, :contract)[:verification_max_libraries]) <> gettext(") may be added for each contract. Click the Add Library button to add an additional one.") + %> + + <%= render BlockScoutWeb.AddressContractVerificationCommonFieldsView, "_library_address.html", + index: 1, + tooltip_text: gettext "The 0x library address. This can be found in the generated json file or Truffle output (if using truffle)." + %> +
\ No newline at end of file diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_common_fields/_library_name.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_common_fields/_library_name.html.eex new file mode 100644 index 0000000..fc15114 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_common_fields/_library_name.html.eex @@ -0,0 +1,10 @@ +<% library_name = "library" <> to_string(@index) <> "_name" |> String.to_atom() %> +
+
+ <%= label :external_libraries, library_name, gettext("Library") <> " " <> to_string(@index) <> " " <> gettext("Name") %> +
+ <%= text_input :external_libraries, library_name, class: "form-control border-rounded", "aria-describedby": "contract-name-help-block" %> +
+
<%= if assigns[:tooltip_text] do @tooltip_text end %>
+
+
\ No newline at end of file diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_common_fields/_yul_contracts_switcher.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_common_fields/_yul_contracts_switcher.html.eex new file mode 100644 index 0000000..b77093a --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_common_fields/_yul_contracts_switcher.html.eex @@ -0,0 +1,21 @@ +
+
+ <%= label @f, :is_yul, gettext("Is Yul contract") %> +
+
+
+ <%= radio_button @f, :is_yul, false, class: "form-check-input autodetectfalse" %> +
+ <%= label @f, :is_yul_false, gettext("No"), class: "radio-text" %> +
+
+ <%= radio_button @f, :is_yul, true, class: "form-check-input autodetecttrue", "aria-describedby": "is_yul-help-block" %> +
+ <%= label @f, :is_yul_true, gettext("Yes"), class: "radio-text" %> +
+
+ <%= error_tag @f, :is_yul, id: "is_yul-help-block", class: "text-danger form-error" %> +
+
<%= gettext("Select Yes if you want to verify Yul contract.") %>
+
+
diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex new file mode 100644 index 0000000..4c62213 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex @@ -0,0 +1,123 @@ +<% metadata_for_verification = if assigns[:retrying], do: nil, else: SmartContract.get_address_verified_bytecode_twin_contract(@address_hash) %> +<% changeset = (if assigns[:retrying], do: @changeset, else: SmartContract.merge_twin_contract_with_changeset(metadata_for_verification, @changeset)) |> SmartContract.address_to_checksum_address() %> +<% fetch_constructor_arguments_automatically = if metadata_for_verification, do: true, else: changeset.changes[:autodetect_constructor_args] || true %> +<% display_constructor_arguments_text_area = if fetch_constructor_arguments_automatically, do: "none", else: "block" %> +
+ <%= render BlockScoutWeb.CommonComponentsView, "_channel_disconnected_message.html", text: gettext("Connection Lost") %> + +
+

<%= gettext "New Solidity Smart Contract Verification" %>

+ + <%= form_for changeset, + address_contract_verification_path(@conn, :create), + [], + fn f -> %> + + <%= render BlockScoutWeb.AddressContractVerificationCommonFieldsView, "_contract_address_field.html", f: f %> + + <%= if RustVerifierInterface.enabled?() do %> + <%= render BlockScoutWeb.AddressContractVerificationCommonFieldsView, "_yul_contracts_switcher.html", f: f %> + <% end %> + + <%= render BlockScoutWeb.AddressContractVerificationCommonFieldsView, "_contract_name_field.html", f: f, tooltip: gettext "Must match the name specified in the code. For example, in contract MyContract {..} MyContract is the contract name." %> + + <%= render BlockScoutWeb.AddressContractVerificationCommonFieldsView, "_include_nightly_builds_field.html", f: f %> + + <%= render BlockScoutWeb.AddressContractVerificationCommonFieldsView, "_compiler_field.html", f: f, compiler_versions: @compiler_versions, tooltip: gettext "The compiler version is specified in pragma solidity X.X.X. Use the compiler version rather than the nightly build. If using the Solidity compiler, run solc —version to check." %> + +
+
+ <%= label f, :evm_version, gettext("EVM Version") %> +
+ <%= select f, :evm_version, @evm_versions, class: "form-control border-rounded", "aria-describedby": "evm-version-help-block" %> +
+
<%= gettext "The EVM version the contract is written for. If the bytecode does not match the version, we try to verify using the latest EVM version." %> <%= gettext "EVM version details" %>.
+
+
+ +
+
+ <%= label f, :optimization, gettext("Optimization") %> +
+
+
+ <%= radio_button f, :optimization, false, class: "form-check-input optimization-false" %> +
+ <%= label :smart_contract_optimization, :false, gettext("No"), class: "radio-text" %> +
+
+ <%= radio_button f, :optimization, true, class: "form-check-input optimization-true", "aria-describedby": "optimization-help-block" %> +
+ <%= label :smart_contract_optimization, :true, gettext("Yes"), class: "radio-text" %> +
+
+ <%= error_tag f, :optimization, id: "optimization-help-block", class: "text-danger form-error" %> +
+
<%= gettext "If you enabled optimization during compilation, select yes." %>
+
+
+ +
"> +
+ <%= label f, :optimization_runs, gettext("Optimization runs") %> +
+ <%= text_input f, :optimization_runs, class: "form-control border-rounded", "aria-describedby": "optimization-runs-help-block", "data-test": "optimization-runs" %> +
+
+
+
+ +
+
+ <%= label f, :contract_source_code, gettext("Enter the Solidity Contract Code") %> +
+ <%= textarea f, :contract_source_code, class: "form-control border-rounded monospace", rows: 3, "aria-describedby": "contract-source-code-help-block" %> + <%= error_tag f, :contract_source_code, id: "contract-source-code-help-block", class: "text-danger form-error", "data-test": "contract-source-code-error" %> +
+
<%= gettext "We recommend using flattened code. This is necessary if your code utilizes a library or inherits dependencies. Use the" %> <%= gettext "POA solidity flattener or the" %> <%= gettext "truffle flattener" %>.
+
+
+ + <%= render BlockScoutWeb.AddressContractVerificationCommonFieldsView, "_fetch_constructor_args.html", f: f %> + + <%= render BlockScoutWeb.AddressContractVerificationCommonFieldsView, "_constructor_args.html", f: f, display_constructor_arguments_text_area: display_constructor_arguments_text_area %> + +
+ <%= gettext "Add Contract Libraries" %> +
+ +
+

<%= gettext "Contract Libraries" %>

+ + <%= render BlockScoutWeb.AddressContractVerificationCommonFieldsView, "_library_first.html" %> + + <%= render BlockScoutWeb.AddressContractVerificationCommonFieldsView, "_libraries_other.html" %> + +
+ <%= gettext "Add Library" %> +
+
+ +
+ + <%= submit gettext("Verify & publish"), class: "btn-full-primary mr-2", "data-button-loading": "animation", "data-submit-button": "" %> + <%= reset gettext("Reset"), class: "btn-line mr-2 js-smart-contract-form-reset" %> + <%= + link( + gettext("Cancel"), + class: "btn-no-border", + to: address_contract_path(@conn, :index, @address_hash) + ) + %> +
+ <% end %> +
+
diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_via_json/new.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_via_json/new.html.eex new file mode 100644 index 0000000..ee72a62 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_via_json/new.html.eex @@ -0,0 +1,50 @@ +
+ <%= render BlockScoutWeb.CommonComponentsView, "_channel_disconnected_message.html", text: gettext("Connection Lost") %> + +
+

<%= gettext "New Smart Contract Verification via metadata JSON" %>

+ <%= form_for @changeset, + address_contract_verification_path(@conn, :create), + [id: "metadata-json-dropzone-form"], + fn f -> %> + <%= render BlockScoutWeb.AddressContractVerificationCommonFieldsView, "_contract_address_field.html", f: f %> + +
+
+ +
+
+
+ <%= gettext("Drop sources and metadata JSON file or click here") %> + <%= error_tag f, :files, id: "file-help-block", class: "text-danger form-error", style: "max-width: 600px;" %> +
+
+
+
Drop all Solidity contract source files and JSON metadata file(s) created during contract compilation into the drop zone.
+
+
+ +
+ + + <%= reset gettext("Reset"), class: "btn-line mr-2 js-smart-contract-form-reset" %> + <%= + link( + gettext("Cancel"), + class: "btn-no-border", + to: address_contract_path(@conn, :index, @address_hash) + ) + %> +
+ <% end %> +
+
+ diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex new file mode 100644 index 0000000..bd60af3 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex @@ -0,0 +1,116 @@ +<% metadata_for_verification = if assigns[:retrying], do: nil, else: SmartContract.get_address_verified_bytecode_twin_contract(@address_hash) %> +<% changeset = (if assigns[:retrying], do: @changeset, else: SmartContract.merge_twin_contract_with_changeset(metadata_for_verification, @changeset)) |> SmartContract.address_to_checksum_address() %> +
+ <%= render BlockScoutWeb.CommonComponentsView, "_channel_disconnected_message.html", text: gettext("Connection Lost") %> + +
+

<%= if RustVerifierInterface.enabled?(), do: gettext("New Solidity/Yul Smart Contract Verification"), else: gettext("New Solidity Smart Contract Verification") %>

+ + <%= form_for changeset, + address_contract_verification_path(@conn, :create), + [id: "multi-part-dropzone-form"], + fn f -> %> + + <%= render BlockScoutWeb.AddressContractVerificationCommonFieldsView, "_contract_address_field.html", f: f %> + + <%= render BlockScoutWeb.AddressContractVerificationCommonFieldsView, "_include_nightly_builds_field.html", f: f %> + + <%= render BlockScoutWeb.AddressContractVerificationCommonFieldsView, "_compiler_field.html", f: f, compiler_versions: @compiler_versions, tooltip: gettext "The compiler version is specified in pragma solidity X.X.X. Use the compiler version rather than the nightly build. If using the Solidity compiler, run solc —version to check." %> + +
+
+ <%= label f, :evm_version, gettext("EVM Version") %> +
+ <%= select f, :evm_version, @evm_versions, class: "form-control border-rounded", "aria-describedby": "evm-version-help-block" %> +
+
<%= gettext "The EVM version the contract is written for. If the bytecode does not match the version, we try to verify using the latest EVM version." %> <%= gettext "EVM version details" %>.
+
+
+ +
+
+ <%= label f, :optimization, gettext("Optimization") %> +
+
+
+ <%= radio_button f, :optimization, false, class: "form-check-input optimization-false" %> +
+ <%= label f, :optimization_false, gettext("No"), class: "radio-text" %> +
+
+ <%= radio_button f, :optimization, true, class: "form-check-input optimization-true", "aria-describedby": "optimization-help-block" %> +
+ <%= label f, :optimization_true, gettext("Yes"), class: "radio-text" %> +
+
+ <%= error_tag f, :optimization, id: "optimization-help-block", class: "text-danger form-error" %> +
+
<%= gettext "If you enabled optimization during compilation, select yes." %>
+
+
+ +
"> +
+ <%= label f, :optimization_runs, gettext("Optimization runs") %> +
+ <%= text_input f, :optimization_runs, class: "form-control border-rounded", "aria-describedby": "optimization-runs-help-block", "data-test": "optimization-runs" %> +
+
+
+
+ +
+ +
+
+
+ <%= gettext("Drop sources or click here") %> + <%= error_tag f, :files, id: "file-help-block", class: "text-danger form-error", style: "max-width: 600px;" %> +
+
+
+
<%= if RustVerifierInterface.enabled?(), do: gettext("Drop all Solidity or Yul contract source files into the drop zone."), else: gettext("Drop all Solidity contract source files into the drop zone.") %>
+
+ +
+ <%= gettext "Add Contract Libraries" %> +
+ +
+

<%= gettext "Contract Libraries" %>

+ + <%= render BlockScoutWeb.AddressContractVerificationCommonFieldsView, "_library_first.html" %> + + <%= render BlockScoutWeb.AddressContractVerificationCommonFieldsView, "_libraries_other.html" %> + +
+ <%= gettext "Add Library" %> +
+
+ +
+ + <%#= submit gettext("Verify & publish"), class: "btn-full-primary mr-2", "data-button-loading": "animation", "data-submit-button": "" %> + <%# verify-via-multi-part-files-submit %> + + <%= reset gettext("Reset"), class: "btn-line mr-2 js-smart-contract-form-reset" %> + <%= + link( + gettext("Cancel"), + class: "btn-no-border", + to: address_contract_path(@conn, :index, @address_hash) + ) + %> +
+ <% end %> +
+
+ diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_via_standard_json_input/new.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_via_standard_json_input/new.html.eex new file mode 100644 index 0000000..7a80dfe --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_via_standard_json_input/new.html.eex @@ -0,0 +1,64 @@ +<% metadata_for_verification = SmartContract.get_address_verified_bytecode_twin_contract(@address_hash) %> +<% changeset = (if assigns[:retrying], do: @changeset, else: SmartContract.merge_twin_contract_with_changeset(metadata_for_verification, @changeset)) |> SmartContract.address_to_checksum_address() %> +<% fetch_constructor_arguments_automatically = if metadata_for_verification, do: true, else: changeset.changes[:autodetect_constructor_args] || true %> +<% display_constructor_arguments_text_area = if fetch_constructor_arguments_automatically, do: "none", else: "block" %> +
+ <%= render BlockScoutWeb.CommonComponentsView, "_channel_disconnected_message.html", text: gettext("Connection Lost") %> + +
+

<%= gettext "New Smart Contract Verification via Standard input JSON" %>

+ <%= form_for changeset, + address_contract_verification_path(@conn, :create), + [id: "standard-json-dropzone-form"], + fn f -> %> + <%= render BlockScoutWeb.AddressContractVerificationCommonFieldsView, "_contract_address_field.html", f: f %> + + <%= render BlockScoutWeb.AddressContractVerificationCommonFieldsView, "_contract_name_field.html", f: f, tooltip: "Must match the name specified in the code. For example, in contract MyContract {..} MyContract is the contract name. Also contract name could be: path/to/file.sol:MyContract" %> + + <%= render BlockScoutWeb.AddressContractVerificationCommonFieldsView, "_include_nightly_builds_field.html", f: f %> + + <%= render BlockScoutWeb.AddressContractVerificationCommonFieldsView, "_compiler_field.html", f: f, compiler_versions: @compiler_versions, tooltip: "The compiler version is specified in pragma solidity X.X.X. Use the compiler version rather than the nightly build. If using the Solidity compiler, run solc —version to check." %> + +
+
+ +
+
+
+ <%= gettext("Drop the standard input JSON file or click here") %> + <%= error_tag f, :files, id: "file-help-block", class: "text-danger form-error", style: "max-width: 600px;" %> +
+
+
+
Drop the standard input JSON file created during contract compilation into the drop zone.
+
+
+ + <%= render BlockScoutWeb.AddressContractVerificationCommonFieldsView, "_fetch_constructor_args.html", f: f %> + + <%= render BlockScoutWeb.AddressContractVerificationCommonFieldsView, "_constructor_args.html", f: f, display_constructor_arguments_text_area: display_constructor_arguments_text_area %> + +
+ + + <%= reset gettext("Reset"), class: "btn-line mr-2 js-smart-contract-form-reset" %> + <%= + link( + gettext("Cancel"), + class: "btn-no-border", + to: address_contract_path(@conn, :index, @address_hash) + ) + %> +
+ <% end %> +
+
+ diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_vyper/new.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_vyper/new.html.eex new file mode 100644 index 0000000..9d78e6b --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_vyper/new.html.eex @@ -0,0 +1,59 @@ +<% metadata_for_verification = SmartContract.get_address_verified_bytecode_twin_contract(@address_hash) %> +<% changeset = (if assigns[:retrying], do: @changeset, else: SmartContract.merge_twin_vyper_contract_with_changeset(metadata_for_verification, @changeset)) |> SmartContract.address_to_checksum_address() %> +
+ <%= render BlockScoutWeb.CommonComponentsView, "_channel_disconnected_message.html", text: gettext("Connection Lost") %> + +
+

<%= gettext "New Vyper Smart Contract Verification" %>

+ + <%= form_for changeset, + address_contract_verification_path(@conn, :create), + [], + fn f -> %> + + <%= render BlockScoutWeb.AddressContractVerificationCommonFieldsView, "_contract_address_field.html", f: f %> + + <%= render BlockScoutWeb.AddressContractVerificationCommonFieldsView, "_contract_name_field.html", f: f, tooltip: "Must match the name specified in the code." %> + + <%= render BlockScoutWeb.AddressContractVerificationCommonFieldsView, "_compiler_field.html", f: f, compiler_versions: @compiler_versions, tooltip: "" %> + +
+
+ <%= label f, :contract_source_code, gettext("Enter the Vyper Contract Code") %> +
+ <%= textarea f, :contract_source_code, class: "form-control border-rounded monospace", rows: 3, "aria-describedby": "contract-source-code-help-block" %> + <%= error_tag f, :contract_source_code, id: "contract-source-code-help-block", class: "text-danger form-error", "data-test": "contract-source-code-error" %> +
+
+
+
+ + <%= render BlockScoutWeb.AddressContractVerificationCommonFieldsView, "_constructor_args.html", f: f, display_constructor_arguments_text_area: "block" %> + +
+ +
+ +
+ + <%= submit gettext("Verify & publish"), class: "btn-full-primary mr-2", "data-button-loading": "animation", "data-submit-button": "" %> + <%= reset gettext("Reset"), class: "btn-line mr-2 js-smart-contract-form-reset" %> + <%= + link( + gettext("Cancel"), + class: "btn-no-border", + to: address_contract_path(@conn, :index, @address_hash) + ) + %> +
+ <% end %> +
+
diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_internal_transaction/_metatags.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_internal_transaction/_metatags.html.eex new file mode 100644 index 0000000..3ef2a67 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_internal_transaction/_metatags.html.eex @@ -0,0 +1 @@ +<%= render BlockScoutWeb.AddressView, "_metatags.html", conn: @conn, address: @address %> diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_internal_transaction/index.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_internal_transaction/index.html.eex new file mode 100644 index 0000000..fe847a2 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_internal_transaction/index.html.eex @@ -0,0 +1,72 @@ +
+ <% is_proxy = SmartContractHelper.address_is_proxy?(@address) %> + + <%= render BlockScoutWeb.AddressView, "overview.html", address: @address, is_proxy: is_proxy, conn: @conn, exchange_rate: @exchange_rate, coin_balance_status: @coin_balance_status, counters_path: @counters_path, tags: @tags %> + +
+
+ <%= render BlockScoutWeb.AddressView, "_tabs.html", address: @address, is_proxy: is_proxy, conn: @conn %> +
+ + <%= render BlockScoutWeb.CommonComponentsView, "_channel_disconnected_message.html", text: gettext("Connection Lost, click to load newer internal transactions") %> +
+

<%= gettext "Internal Transactions" %>

+
+ + <%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "top", cur_page_number: "1", show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %> +
+
+ + +
+
+ <%= gettext "There are no internal transactions for this address." %> +
+
+ +
+ <%= render BlockScoutWeb.CommonComponentsView, "_tile-loader.html" %> +
+ +
+ <%= render BlockScoutWeb.CommonComponentsView, "_csv_export_button.html", address: Address.checksum(@address.hash), type: "internal-transactions", filter_type: :address, filter_value: @filter, conn: @conn %> + <%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "bottom", cur_page_number: "1", show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %> +
+ +
+
+ + +
+
diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_logs/_logs.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_logs/_logs.html.eex new file mode 100644 index 0000000..9e213f9 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_logs/_logs.html.eex @@ -0,0 +1,108 @@ +
"> + <% decoded_result = decode(@log, @log.transaction) %> + <%= case decoded_result do %> + <% {:error, :contract_not_verified, _candidates} -> %> +
+ <%= gettext "To see accurate decoded input data, the contract must be verified." %> + <%= case @log.transaction do %> + <% %{to_address: %{hash: hash}} -> %> + <% path = address_verify_contract_path(@conn, :new, hash) %> + <%= gettext "Verify the contract " %><%= gettext "here" %> + <% _ -> %> + <%= nil %> + <% end %> +
+ <% _ -> %> + <%= nil %> + <% end %> +
+
<%= gettext "Transaction" %>
+
+

+ <%= link( + @log.transaction, + to: transaction_path(@conn, :show, @log.transaction), + "data-test": "log_address_link", + "data-address-hash": @log.transaction + ) %> +

+
+ <%= case decoded_result do %> + <% {:error, :could_not_decode} -> %> +
<%= gettext "Decoded" %>
+
+
+ <%= gettext "Failed to decode log data." %> +
+ <% {:ok, method_id, text, mapping} -> %> +
<%= gettext "Decoded" %>
+
+ + + + + + + + + +
Method Id0x<%= method_id %>
Call<%= text %>
+ <%= render BlockScoutWeb.LogView, "_data_decoded_view.html", mapping: mapping %> + <% {:error, :contract_not_verified, results} -> %> + <%= for {:ok, method_id, text, mapping} <- results do %> +
<%= gettext "Decoded" %>
+
+ + + + + + + + + +
Method Id0x<%= method_id %>
Call<%= text %>
+ <%= render BlockScoutWeb.LogView, "_data_decoded_view.html", mapping: mapping %> + <% end %> + <% end %> +
<%= gettext "Topics" %>
+
+
+ <%= unless is_nil(@log.first_topic) do %> +
+ [0] + <%= @log.first_topic %> +
+ <% end %> + <%= unless is_nil(@log.second_topic) do %> +
+ [1] + <%= @log.second_topic %> +
+ <% end %> + <%= unless is_nil(@log.third_topic) do %> +
+ [2] + <%= @log.third_topic %> +
+ <% end %> + <%= unless is_nil(@log.fourth_topic) do %> +
+ [3] + <%= @log.fourth_topic %> +
+ <% end %> +
+
+
+ <%= gettext "Data" %> +
+
+ <%= unless is_nil(@log.data) do %> +
+ <%= @log.data %> +
+ <% end %> +
+
+
diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_logs/index.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_logs/index.html.eex new file mode 100644 index 0000000..5b8058f --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_logs/index.html.eex @@ -0,0 +1,46 @@ +
+ <% is_proxy = SmartContractHelper.address_is_proxy?(@address) %> + + <%= render BlockScoutWeb.AddressView, "overview.html", address: @address, is_proxy: is_proxy, conn: @conn, exchange_rate: @exchange_rate, coin_balance_status: @coin_balance_status, counters_path: @counters_path, tags: @tags %> +
+
+ <%= render BlockScoutWeb.AddressView, "_tabs.html", address: @address, is_proxy: is_proxy, conn: @conn %> + +
+

<%= gettext "Logs" %>

+
+ + + + <%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "top", show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %> +
+ + + +
+
+ <%= gettext "There are no logs for this address." %> +
+
+ +
+ <%= render BlockScoutWeb.CommonComponentsView, "_tile-loader.html" %> +
+ +
+ <%= render BlockScoutWeb.CommonComponentsView, "_csv_export_button.html", address: Address.checksum(@address.hash), type: "logs", conn: @conn %> + <%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "bottom", cur_page_number: "1", show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %> +
+ +
+ +
+ +
+
diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_read_contract/_metatags.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_read_contract/_metatags.html.eex new file mode 100644 index 0000000..3ef2a67 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_read_contract/_metatags.html.eex @@ -0,0 +1 @@ +<%= render BlockScoutWeb.AddressView, "_metatags.html", conn: @conn, address: @address %> diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_read_contract/index.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_read_contract/index.html.eex new file mode 100644 index 0000000..afd3ab3 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_read_contract/index.html.eex @@ -0,0 +1,58 @@ +
+ + <% is_proxy = SmartContractHelper.address_is_proxy?(@address) %> + + <%= render BlockScoutWeb.AddressView, "overview.html", address: @address, is_proxy: is_proxy, conn: @conn, exchange_rate: @exchange_rate, coin_balance_status: @coin_balance_status, counters_path: @counters_path, tags: @tags %> + +
+ <%= render BlockScoutWeb.AddressView, "_tabs.html", address: @address, is_proxy: is_proxy, conn: @conn %> + <%= if @need_wallet do %> +
+ <%= render BlockScoutWeb.SmartContractView, "_connect_container.html" %> +
+ <% end %> + <%= if @non_custom_abi && assigns[:custom_abi] do %> + + <% else %> + <%= if assigns[:custom_abi] do %> +

<%= gettext "Custom ABI from account" %>

+ <% end %> + <% end %> + <%= + for status <- ["error", "warning", "success", "question"] do + render BlockScoutWeb.CommonComponentsView, "_modal_status.html", status: status + end + %> + <%= render BlockScoutWeb.SmartContractView, "_pending_contract_write.html" %> + <%= if @non_custom_abi && assigns[:custom_abi] do %> +
+ <% end %> + <%= if @non_custom_abi do %> + +
+
+ <%= render BlockScoutWeb.CommonComponentsView, "_loading_spinner.html", loading_text: gettext("Loading...") %> +
+
+ <% end %> + <%= if assigns[:custom_abi] do %> + +
" id="custom" role="tabpanel" aria-labelledby="custom-tab" data-smart-contract-functions-custom data-hash="<%= to_string(@address.hash) %>" data-type="<%= @type %>" data-action="<%= @action %>" data-url="<%= smart_contract_path(@conn, :index) %>"> +
+ <%= render BlockScoutWeb.CommonComponentsView, "_loading_spinner.html", loading_text: gettext("Loading...") %> +
+
+ <% end %> + <%= if @non_custom_abi && assigns[:custom_abi] do %> +
+ <% end %> +
+ +
diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_read_proxy/_metatags.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_read_proxy/_metatags.html.eex new file mode 100644 index 0000000..3ef2a67 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_read_proxy/_metatags.html.eex @@ -0,0 +1 @@ +<%= render BlockScoutWeb.AddressView, "_metatags.html", conn: @conn, address: @address %> diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_read_proxy/index.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_read_proxy/index.html.eex new file mode 100644 index 0000000..efe8695 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_read_proxy/index.html.eex @@ -0,0 +1,17 @@ +
+ + <% is_proxy = SmartContractHelper.address_is_proxy?(@address) %> + + <%= render BlockScoutWeb.AddressView, "overview.html", address: @address, is_proxy: is_proxy, conn: @conn, exchange_rate: @exchange_rate, coin_balance_status: @coin_balance_status, counters_path: @counters_path, tags: @tags %> + +
+ <%= render BlockScoutWeb.AddressView, "_tabs.html", address: @address, is_proxy: is_proxy, conn: @conn %> + +
+
+ <%= render BlockScoutWeb.CommonComponentsView, "_loading_spinner.html", loading_text: gettext("Loading...") %> +
+
+
+ +
diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_token/_metatags.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_token/_metatags.html.eex new file mode 100644 index 0000000..3ef2a67 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_token/_metatags.html.eex @@ -0,0 +1 @@ +<%= render BlockScoutWeb.AddressView, "_metatags.html", conn: @conn, address: @address %> diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_token/_tokens.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_token/_tokens.html.eex new file mode 100644 index 0000000..bb5ac9d --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_token/_tokens.html.eex @@ -0,0 +1,58 @@ + + + + + <%= if Application.get_env(:block_scout_web, :display_token_icons) do %> + <% chain_id_for_token_icon = Application.get_env(:block_scout_web, :chain_id) %> + <% address_hash = @token.contract_address_hash %> + <%= + render BlockScoutWeb.TokensView, + "_token_icon.html", + chain_id: chain_id_for_token_icon, + address: Address.checksum(address_hash) + %> + <% end %> + + + <%= link( + to: address_token_transfers_path(@conn, :index, to_string(@address.hash), to_string(@token.contract_address_hash)), + class: "tile-title-lg", + "data-test": "token_transfers_#{@token.contract_address_hash}" + ) do %> + <%= token_name(@token) %> + <% end %> + + + <%= @token.type %> + + + <%= format_according_to_decimals(@token_balance.value, @token.decimals) %> + + + <%= @token.symbol %> + + +

+ <% token_price = @token.fiat_value %> + <%= ChainView.format_currency_value(token_price, "@") %> +

+ + + <%= if token_price && @token.decimals do %> +

+ <%= ChainView.format_usd_value(Chain.balance_in_fiat(@token_balance)) %> +

+ <% end %> + + + <%= with {:ok, address} <- Chain.hash_to_address(@token.contract_address_hash) do + render BlockScoutWeb.AddressView, + "_link.html", + address: address, + contract: false, + use_custom_tooltip: false, + no_tooltip: true + end + %> + + diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_token/index.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_token/index.html.eex new file mode 100644 index 0000000..4ecd808 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_token/index.html.eex @@ -0,0 +1,75 @@ +
+ <% is_proxy = SmartContractHelper.address_is_proxy?(@address) %> + + <%= render BlockScoutWeb.AddressView, "overview.html", address: @address, is_proxy: is_proxy, conn: @conn, exchange_rate: @exchange_rate, coin_balance_status: @coin_balance_status, counters_path: @counters_path, tags: @tags %> + +
+
+ <%= render BlockScoutWeb.AddressView, "_tabs.html", address: @address, is_proxy: is_proxy, conn: @conn %> +
+ <%= render BlockScoutWeb.AddressTokenView, + "overview.html", + address: @address, + exchange_rate: @exchange_rate + %> + +
+ <%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "top", show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %> +
+ +
+ + + + + + + + + + + + + + + + <%= render BlockScoutWeb.CommonComponentsView, "_table-loader.html", columns_num: 9 %> + +
+
 
+
+
 
+
+
Asset
+
+
Type
+
+
Amount
+
+
Symbol
+
+
Price
+
+
Value
+
+
Contract Address
+
+
+ + + +
+
+ <%= gettext "There are no tokens for this address." %> +
+
+ + <%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "bottom", cur_page_number: "1", show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %> + +
+
+ +
+
diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_token/overview.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_token/overview.html.eex new file mode 100644 index 0000000..a8c4ad4 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_token/overview.html.eex @@ -0,0 +1,73 @@ +<% native_coin = Explorer.coin_name() %> + +<% wei_value = if @address.fetched_coin_balance, do: @address.fetched_coin_balance.value %> +<% raw_usd_value = + case @exchange_rate.fiat_value do + %Decimal{} -> + if wei_value do + %Wei{value: Decimal.new(wei_value)} + |> Wei.to(:ether) + |> Decimal.mult(@exchange_rate.fiat_value) + else + Decimal.new(0) + end + _ -> Decimal.new(0) + end +%> +<% data_usd_exchange_rate = + unless AddressView.empty_exchange_rate?(@exchange_rate) do + "data-usd-exchange-rate='#{@exchange_rate.fiat_value}' data-raw-usd-value='#{raw_usd_value}'" + end +%> +<% native_coin_balance_token = AddressView.balance(@address) +%> +<% native_coin_balance_usd = + if AddressView.empty_exchange_rate?(@exchange_rate) do + nil + else + " + " + end +%> +<% native_coin_balance = + if native_coin_balance_usd do + native_coin_balance_usd <> " | " <> native_coin_balance_token + else + native_coin_balance_token + end +%> +
+ <%= render BlockScoutWeb.AddressTokenView, "overview_item.html", + title: gettext("Net Worth"), + tooltip: gettext("Shows total assets held in the address"), + value: "N/A", + data_test: "address-tokens-panel-net-worth", + classes: ["fs-14"] + %> + <%= render BlockScoutWeb.AddressTokenView, "overview_item.html", + title: "#{native_coin} #{gettext("Balance")}", + tooltip: "#{gettext("Shows the current")} #{native_coin} #{gettext("balance of the address")}", + value: raw(native_coin_balance), + data_test: "address-tokens-panel-native-worth", + classes: ["fs-14"] + %> + <%= render BlockScoutWeb.AddressTokenView, "overview_item.html", + title: gettext("Tokens"), + tooltip: gettext("Shows the tokens held in the address (includes ERC-20, ERC-721 and ERC-1155)."), + value: "N/A", + data_test: "address-tokens-panel-tokens-worth", + classes: ["fs-14"] + %> + <%= render BlockScoutWeb.AddressTokenView, "overview_item.html", + title: gettext("CRC Worth"), + tooltip: gettext("Shows the total CRC balance in the address."), + value: "0 CRC", + data_test: "address-tokens-panel-crc-total-worth", + data_test_container: "address-tokens-panel-crc-total-worth-container", + classes: ["fs-14"], + container_classes: ["d-none"] + %> +
diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_token/overview_item.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_token/overview_item.html.eex new file mode 100644 index 0000000..c038254 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_token/overview_item.html.eex @@ -0,0 +1,14 @@ +
" data-test="<%= if assigns[:data_test_container], do: @data_test_container %>" style="padding: 10px;"> +
+
+

<%= @title %>

+ <%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip.html", + text: @tooltip, + additional_classes: ["ml-2"] + %> +
+
""> + <%= @value %> +
+
+
\ No newline at end of file diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_token_balance/_token_balances.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_token_balance/_token_balances.html.eex new file mode 100644 index 0000000..2096d38 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_token_balance/_token_balances.html.eex @@ -0,0 +1,78 @@ +
+ <%= if Enum.any?(@token_balances) do %> + +
+ (>) +
+ <%= if @conn.request_path |> String.contains?("/tokens") do %> + + + + + + <% else %> + + + + + + <% end %> + <% else %> + <%= tokens_count_title(@token_balances) %> + <% end %> + + +
diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_token_balance/_tokens.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_token_balance/_tokens.html.eex new file mode 100644 index 0000000..1350b79 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_token_balance/_tokens.html.eex @@ -0,0 +1,65 @@ +
+ + + <%= for token_balance <- @token_balances do %> +
+ <% path = cond do + token_balance.token_type == "ERC-721" && !is_nil(token_balance.token_id) -> token_instance_path(@conn, :show, token_balance.token.contract_address_hash, to_string(token_balance.token_id)) + token_balance.token_type == "ERC-1155" && !is_nil(token_balance.token_id) -> token_instance_path(@conn, :show, token_balance.token.contract_address_hash, to_string(token_balance.token_id)) + true -> token_path(@conn, :show, to_string(token_balance.token.contract_address_hash)) + end + %> + <%= link( + to: path, + class: "dropdown-item" + ) do %> + + + + <% end %> +
+ <% end %> +
diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_token_transfer/_metatags.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_token_transfer/_metatags.html.eex new file mode 100644 index 0000000..3ef2a67 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_token_transfer/_metatags.html.eex @@ -0,0 +1 @@ +<%= render BlockScoutWeb.AddressView, "_metatags.html", conn: @conn, address: @address %> diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_token_transfer/index.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_token_transfer/index.html.eex new file mode 100644 index 0000000..3d8e519 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_token_transfer/index.html.eex @@ -0,0 +1,76 @@ +
+ <% is_proxy = SmartContractHelper.address_is_proxy?(@address) %> + + <%= render BlockScoutWeb.AddressView, "overview.html", address: @address, is_proxy: is_proxy, conn: @conn, exchange_rate: @exchange_rate, coin_balance_status: @coin_balance_status, counters_path: @counters_path, tags: @tags %> + +
+
+ <%= render BlockScoutWeb.AddressView, "_tabs.html", address: @address, is_proxy: is_proxy, conn: @conn %> +
+ + <%= if assigns[:token] do %> +

+ <%= gettext "Tokens" %> / <%= token_name(@token) %> +

+ <% end %> + + <%= if !assigns[:token] do %> +
+

<%= gettext "Token Transfers" %>

+
+ + <%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "top", cur_page_number: "1", show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %> +
+
+ <% end %> + + + + + +
+ <%= render BlockScoutWeb.CommonComponentsView, "_tile-loader.html" %> +
+ +
+ <%= render BlockScoutWeb.CommonComponentsView, "_csv_export_button.html", address: Address.checksum(@address.hash), type: "token-transfers", filter_type: :address, filter_value: @filter, conn: @conn %> + <%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "bottom", cur_page_number: "1", show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %> +
+ +
+
+ + +
+
diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_transaction/_metatags.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_transaction/_metatags.html.eex new file mode 100644 index 0000000..3ef2a67 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_transaction/_metatags.html.eex @@ -0,0 +1 @@ +<%= render BlockScoutWeb.AddressView, "_metatags.html", conn: @conn, address: @address %> diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_transaction/index.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_transaction/index.html.eex new file mode 100644 index 0000000..7d8d541 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_transaction/index.html.eex @@ -0,0 +1,72 @@ +
+ + <% is_proxy = SmartContractHelper.address_is_proxy?(@address) %> + + <%= render BlockScoutWeb.AddressView, "overview.html", address: @address, is_proxy: is_proxy, conn: @conn, exchange_rate: @exchange_rate, coin_balance_status: @coin_balance_status, counters_path: @counters_path, tags: @tags %> + +
+
+ <%= render BlockScoutWeb.AddressView, "_tabs.html", address: @address, is_proxy: is_proxy, conn: @conn %> +
+ <%= render BlockScoutWeb.CommonComponentsView, "_channel_disconnected_message.html", text: gettext("Connection Lost, click to load newer transactions") %> +
+

<%= gettext "Transactions" %>

+
+ + <%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "top", show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %> +
+
+ + + +
+
+ <%= gettext "There are no transactions for this address." %> +
+
+ +
+ <%= render BlockScoutWeb.CommonComponentsView, "_tile-loader.html" %> +
+ +
+ <%= render BlockScoutWeb.CommonComponentsView, "_csv_export_button.html", address: Address.checksum(@address.hash), type: "transactions", filter_type: :address, filter_value: @filter, conn: @conn %> + <%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "bottom", cur_page_number: "1", show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %> +
+ +
+
+ +
+
diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_validation/_metatags.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_validation/_metatags.html.eex new file mode 100644 index 0000000..3ef2a67 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_validation/_metatags.html.eex @@ -0,0 +1 @@ +<%= render BlockScoutWeb.AddressView, "_metatags.html", conn: @conn, address: @address %> diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_validation/index.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_validation/index.html.eex new file mode 100644 index 0000000..9c2947a --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_validation/index.html.eex @@ -0,0 +1,33 @@ +
+ <% is_proxy = SmartContractHelper.address_is_proxy?(@address) %> + + <%= render BlockScoutWeb.AddressView, "overview.html", address: @address, is_proxy: is_proxy, conn: @conn, exchange_rate: @exchange_rate, coin_balance_status: @coin_balance_status, counters_path: @counters_path, tags: @tags %> + +
+
+ <%= render BlockScoutWeb.AddressView, "_tabs.html", address: @address, is_proxy: is_proxy, conn: @conn %> +
+ <%= render BlockScoutWeb.CommonComponentsView, "_channel_disconnected_message.html", text: gettext("Connection Lost, click to load newer validations") %> +

<%=gettext("Blocks Validated")%>

+ + <%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "top", cur_page_number: "1", show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %> + + + +
+ <%= render BlockScoutWeb.CommonComponentsView, "_tile-loader.html" %> +
+ + <%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "bottom", cur_page_number: "1", show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %> + +
+
+ +
+
diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_withdrawal/_metatags.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_withdrawal/_metatags.html.eex new file mode 100644 index 0000000..3ef2a67 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_withdrawal/_metatags.html.eex @@ -0,0 +1 @@ +<%= render BlockScoutWeb.AddressView, "_metatags.html", conn: @conn, address: @address %> diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_withdrawal/_withdrawal.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_withdrawal/_withdrawal.html.eex new file mode 100644 index 0000000..1bae359 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_withdrawal/_withdrawal.html.eex @@ -0,0 +1,25 @@ + + + + <%= @withdrawal.index %> + + + + <%= @withdrawal.validator_index %> + + + + <%= render BlockScoutWeb.BlockView, + "_number_link.html", + block: @withdrawal.block + %> + + + + + + + + <%= format_wei_value(@withdrawal.amount, :ether) %> + + diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_withdrawal/index.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_withdrawal/index.html.eex new file mode 100644 index 0000000..c11adfd --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_withdrawal/index.html.eex @@ -0,0 +1,66 @@ +
+ + <% is_proxy = SmartContractHelper.address_is_proxy?(@address) %> + + <%= render BlockScoutWeb.AddressView, "overview.html", address: @address, is_proxy: is_proxy, conn: @conn, exchange_rate: @exchange_rate, coin_balance_status: @coin_balance_status, counters_path: @counters_path, tags: @tags %> + +
+
+ <%= render BlockScoutWeb.AddressView, "_tabs.html", address: @address, is_proxy: is_proxy, conn: @conn %> +
+
+ +

<%= gettext "Withdrawals" %>

+
+ <%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "top", show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %> +
+
+ + + +
+
+ + + + + + + + + + + + <%= render BlockScoutWeb.CommonComponentsView, "_table-loader.html", columns_num: 5 %> + +
+
<%= gettext "Index" %>
+
+
<%= gettext "Validator index" %>
+
+
<%= gettext "Block" %>
+
+
<%= gettext "Age" %>
+
+
<%= gettext "Amount" %>
+
+
+
+ +
+
+ <%= gettext "There are no withdrawals for this address." %> +
+
+ +
+ <%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "bottom", show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %> +
+ +
+
+ +
+
diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_write_contract/_metatags.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_write_contract/_metatags.html.eex new file mode 100644 index 0000000..3ef2a67 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_write_contract/_metatags.html.eex @@ -0,0 +1 @@ +<%= render BlockScoutWeb.AddressView, "_metatags.html", conn: @conn, address: @address %> diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_write_contract/index.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_write_contract/index.html.eex new file mode 100644 index 0000000..a538e98 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_write_contract/index.html.eex @@ -0,0 +1,56 @@ +
+ + <% is_proxy = SmartContractHelper.address_is_proxy?(@address) %> + + <%= render BlockScoutWeb.AddressView, "overview.html", address: @address, is_proxy: is_proxy, conn: @conn, exchange_rate: @exchange_rate, coin_balance_status: @coin_balance_status, counters_path: @counters_path, tags: @tags %> + +
+ <%= render BlockScoutWeb.AddressView, "_tabs.html", address: @address, is_proxy: is_proxy, conn: @conn %> +
+ <%= render BlockScoutWeb.SmartContractView, "_connect_container.html" %> +
+ <%= if @non_custom_abi && assigns[:custom_abi] do %> + + <% else %> + <%= if assigns[:custom_abi] do %> +

<%= gettext "Custom ABI from account" %>

+ <% end %> + <% end %> + <%= + for status <- ["error", "warning", "success", "question"] do + render BlockScoutWeb.CommonComponentsView, "_modal_status.html", status: status + end + %> + <%= render BlockScoutWeb.SmartContractView, "_pending_contract_write.html" %> + <%= if @non_custom_abi && assigns[:custom_abi] do %> +
+ <% end %> + <%= if @non_custom_abi do %> + +
+
+ <%= render BlockScoutWeb.CommonComponentsView, "_loading_spinner.html", loading_text: gettext("Loading...") %> +
+
+ <% end %> + <%= if assigns[:custom_abi] do %> + +
" id="custom" role="tabpanel" aria-labelledby="custom-tab" data-smart-contract-functions-custom data-hash="<%= to_string(@address.hash) %>" data-type="<%= @type %>" data-action="<%= @action %>" data-url="<%= smart_contract_path(@conn, :index) %>"> +
+ <%= render BlockScoutWeb.CommonComponentsView, "_loading_spinner.html", loading_text: gettext("Loading...") %> +
+
+ <% end %> + <%= if @non_custom_abi && assigns[:custom_abi] do %> +
+ <% end %> +
+ +
diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_write_proxy/_metatags.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_write_proxy/_metatags.html.eex new file mode 100644 index 0000000..3ef2a67 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_write_proxy/_metatags.html.eex @@ -0,0 +1 @@ +<%= render BlockScoutWeb.AddressView, "_metatags.html", conn: @conn, address: @address %> diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_write_proxy/index.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_write_proxy/index.html.eex new file mode 100644 index 0000000..efe8695 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/address_write_proxy/index.html.eex @@ -0,0 +1,17 @@ +
+ + <% is_proxy = SmartContractHelper.address_is_proxy?(@address) %> + + <%= render BlockScoutWeb.AddressView, "overview.html", address: @address, is_proxy: is_proxy, conn: @conn, exchange_rate: @exchange_rate, coin_balance_status: @coin_balance_status, counters_path: @counters_path, tags: @tags %> + +
+ <%= render BlockScoutWeb.AddressView, "_tabs.html", address: @address, is_proxy: is_proxy, conn: @conn %> + +
+
+ <%= render BlockScoutWeb.CommonComponentsView, "_loading_spinner.html", loading_text: gettext("Loading...") %> +
+
+
+ +
diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/admin/dashboard/index.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/admin/dashboard/index.html.eex new file mode 100644 index 0000000..af91a91 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/admin/dashboard/index.html.eex @@ -0,0 +1,37 @@ +
+
+
+

Tasks

+
+ +
+
+
+
+
+

Create Contract Methods

+
+
+

+ <%= gettext("For any existing contracts in the database, insert all ABI entries into the contract_methods table. Use this in case you have verified smart contracts before early March 2019 and you want other contracts with the same functions to show those ABI's as candidate matches.") %> +

+
+ + +
+
+
+
+
+ +
diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/admin/session/login_form.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/admin/session/login_form.html.eex new file mode 100644 index 0000000..b2ed066 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/admin/session/login_form.html.eex @@ -0,0 +1,17 @@ +
+
+
+
+
+

Administrator Login

+ + <%= form_for @changeset, session_path(@conn, :create), [], fn f -> %> + <%= FormView.text_field(f, :username, :text, id: "username", required: true, label: "Username") %> + <%= FormView.text_field(f, :password, :password, id: "password", required: true, label: "Password") %> + + <% end %> +
+
+
+
+
diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/admin/setup/admin_registration.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/admin/setup/admin_registration.html.eex new file mode 100644 index 0000000..6019a0a --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/admin/setup/admin_registration.html.eex @@ -0,0 +1,19 @@ +
+
+
+
+
+

Administrator Setup

+ + <%= form_for @changeset, setup_path(@conn, :configure_admin, %{state: @conn.query_params["state"]}), [], fn f -> %> + <%= FormView.text_field(f, :username, :text, id: "username", required: true, label: "Username") %> + <%= FormView.text_field(f, :email, :email, id: "email", required: true, label: "Email Address") %> + <%= FormView.text_field(f, :password, :password, id: "password", required: true, label: "Password") %> + <%= FormView.text_field(f, :password_confirmation, :password, id: "password-confirmation", required: true, label: "Password Confirmation") %> + + <% end %> +
+
+
+
+
diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/admin/setup/verify.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/admin/setup/verify.html.eex new file mode 100644 index 0000000..12ba89c --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/admin/setup/verify.html.eex @@ -0,0 +1,25 @@ +
+
+
+
+
+

Administrator Setup

+ +

You have not setup the administrator account for BlockScout. Run the following command in your terminal at the root directory of where your application is deployed. Paste the value inside of the Recovery Key field. Do not share this key!

+ +
+
pbcopy < apps/explorer/priv/.recovery
+
+ <%= form_for @conn, setup_path(@conn, :configure_admin), [as: "verify"], fn f -> %> +
+ + <%= password_input(f, :recovery_key, class: "form-control", type: "password", id: "recovery-key", placeholder: "JRAJpuEGNKM1XQK3zpWMdAAHVzQtJfDyW/sN/Zn1Ev8=", required: true) %> +
+ + + <% end %> +
+
+
+
+
diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/advertisement/banners_ad/_banner_728.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/advertisement/banners_ad/_banner_728.html.eex new file mode 100644 index 0000000..550a857 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/advertisement/banners_ad/_banner_728.html.eex @@ -0,0 +1,6 @@ + + + + diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/advertisement/text_ad/index.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/advertisement/text_ad/index.html.eex new file mode 100644 index 0000000..66eedb9 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/advertisement/text_ad/index.html.eex @@ -0,0 +1,4 @@ + + diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/api_docs/_action_tile.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/api_docs/_action_tile.html.eex new file mode 100644 index 0000000..240294c --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/api_docs/_action_tile.html.eex @@ -0,0 +1,259 @@ +
+ + +
+

+ <%= gettext "Parameters" %> + + +

+ +
+
+

<%= gettext "Name" %>

+

<%= gettext "Description" %>

+
+ +
+
+
<%= gettext "Module" %> *<%= gettext "required" %>
+

<%= gettext "string" %> <%= gettext "(query)" %>

+
+
+

<%= gettext "A string with the name of the module to be invoked." %>

+

<%= gettext "Must be set to:" %> <%= @module_name %> +

+
+ +
+
+
<%= gettext "Action" %> *<%= gettext "required" %>
+

<%= gettext "string" %> <%= gettext "(query)" %>

+
+
+

<%= gettext "A string with the name of the action to be invoked." %>

+

<%= gettext "Must be set to:" %> <%= @action.name %>

+
+
+ + <%= for required_param <- @action.required_params do %> +
+
+
<%= required_param.key %> *<%= gettext "required" %>
+

<%= required_param.type %> <%= gettext "(query)" %>

+
+
+

<%= required_param.description %>

+
+ +
+
+
+ <% end %> + + <%= for optional_param <- @action.optional_params do %> +
+
+
<%= optional_param.key %>
+

<%= optional_param.type %> <%= gettext "(query)" %>

+
+
+

<%= optional_param.description %>

+ +
+
+ <% end %> + +
+
+ +
+
+ +
+
+ +
+
+
<%= gettext "Curl" %>
+
+

+          
+
+
+
<%= gettext "Request URL" %>
+
+

+          
+
+
<%= gettext "Server Response" %>
+
+

<%= gettext "Code" %>

+

<%= gettext "Details" %>

+
+
+
+
+

<%= gettext "Response Body" %>

+
+

+            
+
+
+
+
+ +

<%= gettext "Responses" %>

+
+

<%= gettext "Code" %>

+
<%= gettext "Description" %>
+
+ <%= for {response, index} <- Enum.with_index(@action.responses) do %> +
+
<%= response.code %>
+
+
+
<%= response.description %>
+
+ + + +
+ +
+
+

+            
+
+ <%= if index == 0 do %> + +
+ <%= render "_model_table.html", model: response.model %> +
+ <% end %> +
+
+
+ <% end %> +
+
diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex new file mode 100644 index 0000000..3330a3a --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex @@ -0,0 +1,182 @@ +
+
+
+

<%= @action %>

+

<%= raw @info.notes %>

+ + curl -X POST --data '{"id":0,"jsonrpc":"2.0","method": "<%= @action %>", "params": []}' + +

+

+

+        
+

+
+ +
+ +
+

+ <%= gettext "Parameters" %> + + +

+ +
+
+

<%= gettext "Name" %>

+

<%= gettext "Description" %>

+
+ + <%= for param <- @info.params do %> +
+
+
+ <%= param.name %> + <%= if param.required do %> + + *<%= gettext "required" %> + + <% end %> +
+
+
+

<%= param.description %>

+ " + data-parameter-type='<%= param.type %>' + data-required='<%= if param.required, do: "true", else: "false" %>' + data-selector='<%= "eth-#{@action}-try-api-ui" %>' + type="text", + value='<%= param.default %>' + /> +
+
+ <% end %> + + +
+
+ + +
+
+ + +
+
+
<%= gettext "Curl" %>
+
+

+          
+
+
<%= gettext "Server Response" %>
+
+

<%= gettext "Code" %>

+

<%= gettext "Details" %>

+
+
+
+
+

<%= gettext "Response Body" %>

+
+

+            
+
+
+
+
+ + +

<%= gettext "Responses" %>

+
+

<%= gettext "Code" %>

+
<%= gettext "Description" %>
+
+
+
200
+
+
+
successful operation
+
+ + + +
+ +
+
+

+            
+
+
+
+
+
+
diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/api_docs/_metatags.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/api_docs/_metatags.html.eex new file mode 100644 index 0000000..9264738 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/api_docs/_metatags.html.eex @@ -0,0 +1,5 @@ + + <%= gettext "API for the %{subnetwork} - BlockScout", subnetwork: LayoutView.subnetwork_title() %> + +"> + diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/api_docs/_model_table.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/api_docs/_model_table.html.eex new file mode 100644 index 0000000..1452cc9 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/api_docs/_model_table.html.eex @@ -0,0 +1,65 @@ +
+

<%= @model.name %> {

+ <%= for {key, details} <- @model.fields do %> +
+
+ <%= key %> +
+
+ + <%= if details[:type] != "model", do: details.type %> + <%= if details[:definition] do %> + + <% end %> + + <%= if details[:type] == "model" do %> + + <%= details.model.name %> + + <% end %> + <%= if details[:type] == "array" do %> +
+ [<%= details.array_type.name %>] +
+ <% end %> + <%= if details[:enum] do %> +
enum: <%= details.enum %>
+
+
enum +
interpretation +
+ <%= for {enum, interpretation} <- details[:enum_interpretation] do %> +
+
+ "<%= enum %>" +
+
+ <%= interpretation %> +
+
+ <% end %> + <% end %> + <%= if details[:example] do %> +
example: <%= details.example %>
+ <% end %> + <%= if details[:description] do %> +
description: <%= details.description %>
+ <% end %> +
+ +
+ <% end %> +

}

+
+ +<%= if @model.fields[:result][:type] == "array" do %> + <%= render "_model_table.html", model: @model.fields[:result].array_type %> +<% end %> + +<%= if @model.fields[:result][:type] == "model" do %> + <%= render "_model_table.html", model: @model.fields[:result].model %> +<% end %> diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/api_docs/_module_card.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/api_docs/_module_card.html.eex new file mode 100644 index 0000000..5def65b --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/api_docs/_module_card.html.eex @@ -0,0 +1,12 @@ +
+
+

+ <%= "#{String.capitalize(@module.name)}" %> + ?module=<%= @module.name %> +

+
+ <%= for action <- @module.actions do %> + <%= render "_action_tile.html", module_name: @module.name, action: action %> + <% end %> +
+ diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/api_docs/_module_item.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/api_docs/_module_item.html.eex new file mode 100644 index 0000000..2224c2a --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/api_docs/_module_item.html.eex @@ -0,0 +1,4 @@ + + <%= "#{String.capitalize(@module.name)}" %> + ?module=<%= @module.name %> + \ No newline at end of file diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/api_docs/eth_rpc.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/api_docs/eth_rpc.html.eex new file mode 100644 index 0000000..e3a879d --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/api_docs/eth_rpc.html.eex @@ -0,0 +1,25 @@ +
+
+
+

<%= gettext("ETH RPC API Documentation") %>

+

[ <%= gettext "Base URL:" %> <%= eth_rpc_api_url()%> ]

+

+ <%= gettext "This API is provided to support some rpc methods in the exact format specified for ethereum nodes, which can be found " %> + + <%= gettext "here." %> + <%= gettext "This is useful to allow sending requests to blockscout without having to change anything about the request." %> + <%= gettext "However, in general, the" %> <%= link( + gettext("custom RPC"), + to: api_docs_path(@conn, :index) + ) %> <%= gettext " is recommended." %> + <%= gettext "Anything not in this list is not supported. Click on the method to be taken to the documentation for that method, and check the notes section for any potential differences." %> +

+
+
+
+ <%= for {method, info} <- Map.to_list(@documentation) do %> + <%= render "_eth_rpc_item.html", action: method, info: info %> + <% end %> +
+ +
diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/api_docs/index.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/api_docs/index.html.eex new file mode 100644 index 0000000..6f27e0b --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/api_docs/index.html.eex @@ -0,0 +1,18 @@ +
+
+
+

<%= gettext("API Documentation") %>

+

[ <%= gettext "Base URL:" %> <%= api_url()%> ]

+

<%= gettext "This API is provided for developers transitioning their applications from Etherscan to BlockScout. It supports GET and POST requests." %>

+
+
+ <%= for module <- @documentation do %> + <%= render "_module_item.html", module: module %> + <% end %> +
+
+ <%= for module <- @documentation do %> + <%= render "_module_card.html", module: module %> + <% end %> + +
diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/block/_link.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/block/_link.html.eex new file mode 100644 index 0000000..c498a39 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/block/_link.html.eex @@ -0,0 +1,4 @@ +<%= link( + gettext("Block #%{number}", number: to_string(@block.number)), + to: block_path(BlockScoutWeb.Endpoint, :show, @block.hash) +) %> diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/block/_metatags.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/block/_metatags.html.eex new file mode 100644 index 0000000..c83fffd --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/block/_metatags.html.eex @@ -0,0 +1,13 @@ +<%= if assigns[:block] do %> + + <%= gettext( + "Block %{block_number} - %{subnetwork} Explorer", + block_number: @block.number, + subnetwork: BlockScoutWeb.LayoutView.subnetwork_title() + ) %> + + "> + "> +<% else %> + <%= BlockScoutWeb.LayoutView.render("_default_title.html") %> +<% end %> diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/block/_number_link.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/block/_number_link.html.eex new file mode 100644 index 0000000..6046e0e --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/block/_number_link.html.eex @@ -0,0 +1,4 @@ +<%= link( + to_string(@block.number), + to: block_path(BlockScoutWeb.Endpoint, :show, @block) +) %> diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/block/_tabs.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/block/_tabs.html.eex new file mode 100644 index 0000000..885aa9b --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/block/_tabs.html.eex @@ -0,0 +1,19 @@ +
+ <%= + link( + gettext("Transactions"), + class: "card-tab #{tab_status("transactions", @conn.request_path)}", + to: block_transaction_path(@conn, :index, @conn.params["block_hash_or_number"]) + ) + %> + + <%= if Chain.check_if_withdrawals_in_block(@block.hash) do %> + <%= + link( + gettext("Withdrawals"), + class: "card-tab #{tab_status("withdrawals", @conn.request_path)}", + to: block_withdrawal_path(@conn, :index, @conn.params["block_hash_or_number"]) + ) + %> + <% end %> +
diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/block/_tile.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/block/_tile.html.eex new file mode 100644 index 0000000..4e62a4e --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/block/_tile.html.eex @@ -0,0 +1,82 @@ +<% burnt_fees = if !is_nil(@block.base_fee_per_gas), do: Wei.mult(@block.base_fee_per_gas, BlockBurntFeeCount.fetch(@block.hash)), else: nil %> +<% priority_fee = if !is_nil(@block.base_fee_per_gas), do: BlockPriorityFeeCount.fetch(@block.hash), else: nil %> +
+
+
+ <%= if @block_type == "Block" do %> + <%= link( + class: "tile-label", + to: block_path(BlockScoutWeb.Endpoint, :show, @block), + "data-selector": "block-number" + ) do %> + #<%= @block %> + <% end %> + <% else %> + <%= link( + class: "tile-label", + to: block_path(BlockScoutWeb.Endpoint, :show, @block.hash), + "data-selector": "block-number" + ) do %> + #<%= @block %> + <% end %> + <% end %> + <%= @block_type %> +
+
+
+ + + <%= ngettext("%{count} transaction", "%{count} transactions", Enum.count(@block.transactions)) %> + + <%= if @block.size do %> + + <%= Cldr.Unit.new!(:byte, @block.size) |> cldr_unit_to_string!() %> + <% end %> + + +
+ <%= if !Application.get_env(:block_scout_web, :hide_block_miner) do %> +
+ + <%= gettext "Miner" %> + <%= render BlockScoutWeb.AddressView, + "_link.html", + address: @block.miner, + contract: false, + use_custom_tooltip: false %> +
+ <% end %> + <%= if show_reward?(@block.rewards) do %> +
+ + <%= gettext "Reward" %> + + <%= combined_rewards_value(@block) %> + +
+ <% end %> +
+
+ <%= if !is_nil(@block.base_fee_per_gas) do %> + + <%= format_wei_value(%Wei{value: priority_fee}, :ether) %> <%= gettext "Priority Fees" %> + + <%= format_wei_value(burnt_fees, :ether) %> <%= gettext "Burnt Fees" %> + <% end %> + + <%= formatted_gas(@block.gas_limit) %> <%= gettext "Gas Limit" %> + +
+ <%= formatted_gas(@block.gas_used) %> + <% gas = if Decimal.compare(@block.gas_limit, 0) == :gt, do: Decimal.to_integer(@block.gas_used) / Decimal.to_integer(@block.gas_limit), else: 0 %> + (<%= formatted_gas(gas, format: "#.#%") %>) + <%= gettext "Gas Used" %> +
+ +
+
;" aria-valuenow="50" aria-valuemin="0" aria-valuemax="100"> +
+
+
+
+
diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/block/index.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/block/index.html.eex new file mode 100644 index 0000000..dcee144 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/block/index.html.eex @@ -0,0 +1,25 @@ +
+ <%= render BlockScoutWeb.Advertisement.TextAdView, "index.html", conn: @conn %> +
+
+ <%= render BlockScoutWeb.CommonComponentsView, "_channel_disconnected_message.html", text: gettext("Connection Lost, click to load newer blocks") %> + +

<%= gettext("%{block_type}s", block_type: @block_type) %>

+ +
+ <%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "top", cur_page_number: "1", show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %> +
+ +
+ <%= render BlockScoutWeb.CommonComponentsView, "_tile-loader.html" %> +
+
+ <%= gettext "There are no blocks." %> +
+ + <%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "bottom", cur_page_number: "1", show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %> + +
+
+ +
diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/block/overview.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/block/overview.html.eex new file mode 100644 index 0000000..5545a6b --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/block/overview.html.eex @@ -0,0 +1,273 @@ +<% burnt_fees = if !is_nil(@block.base_fee_per_gas), do: Wei.mult(@block.base_fee_per_gas, BlockBurntFeeCount.fetch(@block.hash)), else: nil %> +<% priority_fee = if !is_nil(@block.base_fee_per_gas), do: BlockPriorityFeeCount.fetch(@block.hash), else: nil %> +
+ <%= render BlockScoutWeb.Advertisement.TextAdView, "index.html", conn: @conn %> +
+
+ +
+
+
+

+ <%= gettext("%{block_type} Details", block_type: block_type(@block)) %> +

+ +
+ +
+
+ <%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip_2.html", + text: gettext("The block height of a particular block is defined as the number of blocks preceding it in the blockchain.") %> + <%= if block_type(@block) == "Block" do %> + <%= gettext("Block Height") %> + <% else %> + <%= gettext("%{block_type} Height", block_type: block_type(@block)) %> + <% end %> +
+
+ <%= if block_type(@block) == "Block" do %> + <%= @block.number %> <%= if @block.number == 0, do: " - " <> gettext("Genesis Block")%> + <% else %> + <%= link(@block, to: block_path(BlockScoutWeb.Endpoint, :show, @block.number)) %> + <% end %> +
+
+ +
+
+ <%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip_2.html", + text: gettext("Date & time at which block was produced.") %> + <%= gettext("Timestamp") %> +
+
+
+ +
+
+ <%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip_2.html", + text: gettext("The number of transactions in the block.") %> + <%= gettext("Transactions") %> +
+
+ + <%= if @block_transaction_count == 1 do %> + <%= gettext "%{count} Transaction", count: @block_transaction_count %> + <% else %> + <%= gettext "%{count} Transactions", count: @block_transaction_count %> + <% end %> + +
+
+ + <%= if !Application.get_env(:block_scout_web, :hide_block_miner) do %> +
+
+ <%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip_2.html", + text: gettext("A block producer who successfully included the block onto the blockchain.") %> + <%= gettext("Miner") %> +
+
<%= render BlockScoutWeb.AddressView, "_link.html", address: @block.miner, contract: false, class: "", use_custom_tooltip: false, show_full_hash: true %> + <%= render BlockScoutWeb.CommonComponentsView, "_btn_copy.html", + additional_classes: ["btn-copy-icon-small", "btn-copy-icon-custom", "btn-copy-icon-no-borders"], + clipboard_text: @block.miner, + aria_label: gettext("Copy Address"), + title: gettext("Copy Address") %> +
+
+ <% end %> + +
+
+ <%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip_2.html", + text: gettext("Size of the block in bytes.") %> + <%= gettext("Size") %> +
+
<%= if !is_nil(@block.size), do: (Cldr.Unit.new!(:byte, @block.size) |> cldr_unit_to_string!()), else: gettext("N/A bytes") %>
+
+ +
+
+ <%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip_2.html", + text: gettext("The SHA256 hash of the block.") %> + <%= gettext("Hash") %> +
+
<%= to_string(@block.hash) %> + <%= render BlockScoutWeb.CommonComponentsView, "_btn_copy.html", + additional_classes: ["btn-copy-icon-small", "btn-copy-icon-custom", "btn-copy-icon-no-borders"], + clipboard_text: to_string(@block.hash), + aria_label: gettext("Copy Hash"), + title: gettext("Copy Hash") %> +
+
+ <%= unless @block.number == 0 do %> + +
+
+ <%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip_2.html", + text: gettext("The hash of the block from which this block was generated.") %> + <%= gettext("Parent Hash") %> +
+
<%= link( + @block.parent_hash, + class: "transaction__link", + to: block_path(@conn, :show, @block.number - 1) + ) %> + <%= render BlockScoutWeb.CommonComponentsView, "_btn_copy.html", + additional_classes: ["btn-copy-icon-small", "btn-copy-icon-custom", "btn-copy-icon-no-borders"], + clipboard_text: to_string(@block.parent_hash), + aria_label: gettext("Copy Parent Hash"), + title: gettext("Copy Parent Hash") %> +
+
+ <% end %> + +
+
+ <%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip_2.html", + text: gettext("Block difficulty for miner, used to calibrate block generation time (Note: constant in POA based networks).") %> + <%= gettext("Difficulty") %> +
+
<%= @block.difficulty |> Decimal.to_integer() |> BlockScoutWeb.Cldr.Number.to_string! %>
+
+ <%= if block_type(@block) == "Block" do %> + <%= if !is_nil(@block.total_difficulty) do %> + +
+
+ <%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip_2.html", + text: gettext("Total difficulty of the chain until this block.") %> + <%= gettext("Total Difficulty") %> +
+
<%= @block.total_difficulty |> Decimal.to_integer() |> BlockScoutWeb.Cldr.Number.to_string! %>
+
+ <% end %> + +
+
+ <%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip_2.html", + text: gettext("The total gas amount used in the block and its percentage of gas filled in the block.") %> + <%= gettext("Gas Used") %> +
+
<%= @block.gas_used |> BlockScoutWeb.Cldr.Number.to_string! %> | <%= if Decimal.compare(@block.gas_limit, 0) == :eq, do: "0%", else: ((Decimal.to_integer(@block.gas_used) / Decimal.to_integer(@block.gas_limit)) |> BlockScoutWeb.Cldr.Number.to_string!(format: "#.#%")) %>
+
+ +
+
+ <%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip_2.html", + text: gettext("Total gas limit provided by all transactions in the block.") %> + <%= gettext("Gas Limit") %> +
+
<%= BlockScoutWeb.Cldr.Number.to_string!(@block.gas_limit) %>
+
+ +
+
+ <%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip_2.html", + text: gettext("64-bit hash of value verifying proof-of-work (note: null for POA chains).") %> + <%= gettext("Nonce") %> +
+
<%= to_string(@block.nonce) %>
+
+ <% end %> + <%= if !is_nil(@block.base_fee_per_gas) do%> + +
+
+ <%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip_2.html", + text: gettext("Minimum fee required per unit of gas. Fee adjusts based on network congestion.") %> + <%= gettext("Base Fee per Gas") %> +
+
<%= format_wei_value(@block.base_fee_per_gas, :gwei) %>
+
+ +
+
+ <%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip_2.html", + text: Explorer.coin_name() <> " " <> gettext("burnt from transactions included in the block (Base fee (per unit of gas) * Gas Used).") %> + <%= gettext("Burnt Fees") %> +
+
<%= format_wei_value(burnt_fees, :ether) %>
+
+ +
+
+ <%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip_2.html", + text: gettext("User-defined tips sent to validator for transaction priority/inclusion.") %> + <%= gettext("Priority Fee / Tip") %> +
+
<%= format_wei_value(%Wei{value: priority_fee}, :ether) %>
+
+ <% end %> + <%= if show_reward?(@block.rewards) do %> +
+ <%= for block_reward <- @block.rewards do %> +
+
+ <%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip_2.html", + text: gettext("Amount of distributed reward. Miners receive a static block reward + Tx fees + uncle fees.") %> + <%= block_reward_text(block_reward, @block.miner.hash) %> +
+
<%= format_wei_value(block_reward.reward, :ether) %>
+
+ <% end %> + <% end %> + <%= if block_type(@block) == "Block" do %> + <%= if length(@block.uncle_relations) > 0 do %> + +
+
+ <%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip_2.html", + text: gettext("Index position(s) of referenced stale blocks.") %> + <%= gettext("Uncles") %> +
+
<%= for {relation, index} <- Enum.with_index(@block.uncle_relations) do %> + <%= link( + gettext("Position %{index}", index: index), + class: "transaction__link", + "data-toggle": "tooltip", + "data-placement": "top" , + "data-test": "uncle_link", + "data-uncle-hash": to_string(relation.uncle_hash), + to: block_path(@conn, :show, relation.uncle_hash) + ) %><%= if index < length(@block.uncle_relations) - 1 do %>,<% end %> + <% end %>
+
+ <% end %> + <% end %> +
+
+
+
+ +<%= render BlockScoutWeb.Advertisement.BannersAdView, "_banner_728.html", conn: @conn %> diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/block_transaction/404.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/block_transaction/404.html.eex new file mode 100644 index 0000000..84fe3df --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/block_transaction/404.html.eex @@ -0,0 +1,13 @@ +
+
+
+ Block Not Found +
+
+

<%= gettext("Block Details") %>

+

<%= block_not_found_message(@block_above_tip) %>

+ Back Home +
+
+
+ diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/block_transaction/_metatags.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/block_transaction/_metatags.html.eex new file mode 100644 index 0000000..bff7f94 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/block_transaction/_metatags.html.eex @@ -0,0 +1 @@ +<%= render BlockScoutWeb.BlockView, "_metatags.html", conn: @conn, block: @block %> diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/block_transaction/index.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/block_transaction/index.html.eex new file mode 100644 index 0000000..d4c91ef --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/block_transaction/index.html.eex @@ -0,0 +1,33 @@ +
+ + <%= render BlockScoutWeb.BlockView, "overview.html", assigns %> + +
+
+ <%= render BlockScoutWeb.BlockView, "_tabs.html", assigns %> + +
+ + <%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "top", show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %> + + + +
+
+ <%= gettext "There are no transactions for this block." %> +
+
+ +
+ <%= render BlockScoutWeb.CommonComponentsView, "_tile-loader.html" %> +
+ + <%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "bottom", cur_page_number: "1", show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %> + +
+ + +
+
diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/block_withdrawal/_metatags.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/block_withdrawal/_metatags.html.eex new file mode 100644 index 0000000..bff7f94 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/block_withdrawal/_metatags.html.eex @@ -0,0 +1 @@ +<%= render BlockScoutWeb.BlockView, "_metatags.html", conn: @conn, block: @block %> diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/block_withdrawal/_withdrawal.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/block_withdrawal/_withdrawal.html.eex new file mode 100644 index 0000000..5a9ff7c --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/block_withdrawal/_withdrawal.html.eex @@ -0,0 +1,23 @@ + + + + <%= @withdrawal.index %> + + + + <%= @withdrawal.validator_index %> + + + + <%= render BlockScoutWeb.AddressView, + "_link.html", + address: @withdrawal.address, + contract: Address.smart_contract?(@withdrawal.address), + use_custom_tooltip: false + %> + + + + <%= format_wei_value(@withdrawal.amount, :ether) %> + + diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/block_withdrawal/index.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/block_withdrawal/index.html.eex new file mode 100644 index 0000000..43c4515 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/block_withdrawal/index.html.eex @@ -0,0 +1,54 @@ +
+ + <%= render BlockScoutWeb.BlockView, "overview.html", assigns %> + +
+
+ <%= render BlockScoutWeb.BlockView, "_tabs.html", assigns %> + +
+ + <%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "top", show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %> + + + +
+
+ + + + + + + + + + + <%= render BlockScoutWeb.CommonComponentsView, "_table-loader.html", columns_num: 4 %> + +
+
<%= gettext "Index" %>
+
+
<%= gettext "Validator index" %>
+
+
<%= gettext "To" %>
+
+
<%= gettext "Amount" %>
+
+
+
+ +
+
+ <%= gettext "There are no withdrawals for this block." %> +
+
+ + <%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "bottom", cur_page_number: "1", show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %> + +
+ +
+
diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/chain/_block.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/chain/_block.html.eex new file mode 100644 index 0000000..ff64455 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/chain/_block.html.eex @@ -0,0 +1,32 @@ +
+
+ <%= link( + @block, + class: "tile-title", + to: block_path(BlockScoutWeb.Endpoint, :show, @block), + "data-selector": "block-number" + ) %> +
+
+ <%= gettext("%{count} Transactions", count: Enum.count(@block.transactions)) %> + +
+ <%= if !Application.get_env(:block_scout_web, :hide_block_miner) do %> +
+ <%= gettext "Miner" %> + <%= render BlockScoutWeb.AddressView, + "_link.html", + address: @block.miner, + contract: false, + use_custom_tooltip: false, + custom_classes_tooltip: ["miner-address-tooltip"] %> +
+ <% end %> + <%= if BlockScoutWeb.BlockView.show_reward?(@block.rewards) do %> +
+ <%= gettext "Reward" %> <%= BlockScoutWeb.BlockView.combined_rewards_value(@block) %> +
+ <% end %> +
+
+
diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/chain/_metatags.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/chain/_metatags.html.eex new file mode 100644 index 0000000..a46a91b --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/chain/_metatags.html.eex @@ -0,0 +1,5 @@ + + <%= gettext("%{subnetwork} %{network} Explorer", subnetwork: LayoutView.subnetwork_title(), network: LayoutView.network_title()) %> + +"> + diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex new file mode 100644 index 0000000..f07899e --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex @@ -0,0 +1,43 @@ +
+ + <%= gettext "Gas tracker" %> + +
+ <% gas_prices_from_oracle = gas_prices() %> + <%= if gas_prices_from_oracle do %> + +
+
+
<%= "#{gas_prices_from_oracle[:average]}" <> " " %><%= gettext "Gwei" %>
+
+
+
<%= gettext "Slow" %><%= gas_prices_from_oracle[:slow] %> <%= gettext "Gwei" %>
+
<%= gettext "Average" %><%= gas_prices_from_oracle[:average] %> <%= gettext "Gwei" %>
+
<%= gettext "Fast" %><%= gas_prices_from_oracle[:fast] %> <%= gettext "Gwei" %>
+
+ " + > + + +
+
+ + <% else %> + <%= if @gas_price do %> + + + <%= render BlockScoutWeb.IconsView, "_gas_price_icon.html" %> + + <%= @gas_price <> " " %> + <%= gettext "Gwei" %> + + <% end %> + <% end %> +
+ diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/chain/show.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/chain/show.html.eex new file mode 100644 index 0000000..c0156a2 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/chain/show.html.eex @@ -0,0 +1,234 @@ +
+
+
+ +
+ +
+ + + "<%= key %>":"<%= value %>" + <%= if x<(map_size(@chart_data_paths)-1) do %> + , + <% end %> + <% end %>}' + data-history_chart_config = '<%= @chart_config_json %>' + width="350" height="152"> + +
+ + +
+ <% price_chart_legend_enabled? = Application.get_env(:block_scout_web, :chart)[:price_chart_legend_enabled?] %> + <%= if Map.has_key?(@chart_config, :market) || price_chart_legend_enabled? do %> + + <%# THE FOLLOWING LINE PREVENTS COPY/PASTE ERRORS %> + <%# Explicitly put @chart_config.market in a variable %> + <%# This is done so that when people add a new chart source, x, %> + <%# They wont just access @chart_config.x w/o first checking if x exists %> + <% market_chart_config = Map.has_key?(@chart_config, :market) && @chart_config.market%> + + <%= if price_chart_legend_enabled? || Enum.member?(market_chart_config, :price) do %> +
+ + <%= Explorer.coin_name() %> <%= gettext "Price" %> + +
+ + +
+
+ <% end %> + <%= if price_chart_legend_enabled? || Enum.member?(@chart_config.market, :market_cap) do %> +
+ + <%= gettext "Market Cap" %> + +
+ <% total_market_cap = market_cap(@market_cap_calculation, @exchange_rate) %> + + +
+
+ <% end %> + <% end %> + <%= render BlockScoutWeb.ChainView, "gas_price_oracle_legend_item.html", gas_price: @gas_price %> + <%= if Map.has_key?(@chart_config, :transactions) do %> + + <% transaction_chart_config = @chart_config.transactions%> + <%= if Enum.member?(transaction_chart_config, :transactions_per_day) do %> +
+ + <%= gettext "Daily Transactions" %> + + + <% num_of_transactions = BlockScoutWeb.Cldr.Number.to_string!(Enum.at(@transaction_stats, 0).number_of_transactions, format: "#,###") %> + <%= num_of_transactions %> + <% gas_used = Enum.at(@transaction_stats, 0).gas_used %> + <%= if gas_used && gas_used > 0 do %> +
"> + + + <% end %> + +
+ <% end %> + <% end %> +
+
+ +
+
+ <%= unless Application.get_env(:explorer, :chain_type) == :optimism do %> + <%= case @average_block_time do %> + <% {:error, :disabled} -> %> + <%= nil %> + <% average_block_time -> %> +
+ + <%= gettext "Average block time" %> + + + <%= Timex.format_duration(average_block_time, Explorer.Chain.Cache.Counters.Helper.AverageBlockTimeDurationFormat) %> + +
+ <% end %> + <% end %> +
+ + <%= gettext "Total transactions" %> + +
+ + <%= BlockScoutWeb.Cldr.Number.to_string!(@transaction_estimated_count, format: "#,###") %> + + <%= if @total_gas_usage > 0 do %> +
" + class="custom-tooltip-total-transactions"> + + + <% end %> +
+
+
+ + <%= gettext "Total blocks" %> + + + <%= BlockScoutWeb.Cldr.Number.to_string!(@block_count, format: "#,###") %> + +
+
+ + <%= gettext "Wallet addresses" %> + + + <%= BlockScoutWeb.Cldr.Number.to_string!(@address_count, format: "#,###") %> + +
+
+
+
+ + +
+
+
+ <%= link(gettext("View All Blocks"), to: blocks_path(BlockScoutWeb.Endpoint, :index), class: "btn-line float-right") %> +

<%= gettext "Blocks" %>

+
+ + + + + +
+
+
+ + <%= render BlockScoutWeb.Advertisement.BannersAdView, "_banner_728.html", conn: @conn %> + +
+
+ <%= link(gettext("View All Transactions"), to: transaction_path(BlockScoutWeb.Endpoint, :index), class: "btn-line float-right") %> +

<%= gettext "Transactions" %>

+ + + + + +
+
+
diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_btn_add_full.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_btn_add_full.html.eex new file mode 100644 index 0000000..deae2f8 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_btn_add_full.html.eex @@ -0,0 +1,9 @@ + \ No newline at end of file diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_btn_add_line.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_btn_add_line.html.eex new file mode 100644 index 0000000..586d5da --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_btn_add_line.html.eex @@ -0,0 +1,9 @@ + \ No newline at end of file diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_btn_copy.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_btn_copy.html.eex new file mode 100644 index 0000000..135fe48 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_btn_copy.html.eex @@ -0,0 +1,14 @@ + " + data-placement='top' + data-toggle='tooltip' + title='<%= @title %>' + style='<%= if assigns[:style] do @style end %>' + > + + + + diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_btn_copy_for_table.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_btn_copy_for_table.html.eex new file mode 100644 index 0000000..486ef4f --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_btn_copy_for_table.html.eex @@ -0,0 +1,15 @@ + " + style='<%= if assigns[:style] do @style end %>' + > + + + + \ No newline at end of file diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_btn_external_link.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_btn_external_link.html.eex new file mode 100644 index 0000000..45081b8 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_btn_external_link.html.eex @@ -0,0 +1,8 @@ + + <%= render BlockScoutWeb.IconsView, "_external_link.html" %> + <%= @text %> + \ No newline at end of file diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_btn_line.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_btn_line.html.eex new file mode 100644 index 0000000..4c1444c --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_btn_line.html.eex @@ -0,0 +1,6 @@ + \ No newline at end of file diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_btn_qr_code.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_btn_qr_code.html.eex new file mode 100644 index 0000000..183a55c --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_btn_qr_code.html.eex @@ -0,0 +1,17 @@ + + + + + + + \ No newline at end of file diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_changed_bytecode_warning.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_changed_bytecode_warning.html.eex new file mode 100644 index 0000000..665733f --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_changed_bytecode_warning.html.eex @@ -0,0 +1,4 @@ +
+ <%= render BlockScoutWeb.CommonComponentsView, "_info.html" %> + <%= gettext("Warning! Contract bytecode has been changed and doesn't match the verified one. Therefore, interaction with this smart contract may be risky.") %> +
\ No newline at end of file diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_channel_disconnected_message.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_channel_disconnected_message.html.eex new file mode 100644 index 0000000..4dd8e27 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_channel_disconnected_message.html.eex @@ -0,0 +1,5 @@ +
+
+ <%= @text %> +
+
\ No newline at end of file diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_check_tooltip.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_check_tooltip.html.eex new file mode 100644 index 0000000..7b7b1de --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_check_tooltip.html.eex @@ -0,0 +1,14 @@ +
+ + + + +
\ No newline at end of file diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_csv_export_button.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_csv_export_button.html.eex new file mode 100644 index 0000000..b0d98d3 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_csv_export_button.html.eex @@ -0,0 +1,9 @@ +<% filter_type = if assigns[:filter_type], do: @filter_type, else: "" %> +<% filter_value = if assigns[:filter_value], do: @filter_value, else: "" %> + \ No newline at end of file diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_i_tooltip.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_i_tooltip.html.eex new file mode 100644 index 0000000..b72928c --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_i_tooltip.html.eex @@ -0,0 +1,14 @@ +
" + data-boundary="window" + data-container="body" + data-html="true" + data-placement="top" + data-toggle="tooltip" + title="<%= @text %>" +> + " height="<%= if assigns[:height] do @height else "16" end %>"> + + + +
\ No newline at end of file diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_i_tooltip_2.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_i_tooltip_2.html.eex new file mode 100644 index 0000000..87a55cf --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_i_tooltip_2.html.eex @@ -0,0 +1,11 @@ +" + data-boundary="window" + data-container="body" + data-html="true" + data-placement="top" + data-toggle="tooltip" + title="<%= @text %>" +> + + \ No newline at end of file diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_icon_error_modal.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_icon_error_modal.html.eex new file mode 100644 index 0000000..8e1242d --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_icon_error_modal.html.eex @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_icon_question_modal.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_icon_question_modal.html.eex new file mode 100644 index 0000000..14c5089 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_icon_question_modal.html.eex @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_icon_success_modal.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_icon_success_modal.html.eex new file mode 100644 index 0000000..71779f2 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_icon_success_modal.html.eex @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_icon_warning_modal.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_icon_warning_modal.html.eex new file mode 100644 index 0000000..a8b4011 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_icon_warning_modal.html.eex @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_info.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_info.html.eex new file mode 100644 index 0000000..0ab5088 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_info.html.eex @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_input_group.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_input_group.html.eex new file mode 100644 index 0000000..5c9c929 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_input_group.html.eex @@ -0,0 +1,17 @@ +
+ + type="<%= if assigns[:type] do @type end %>" + class="<%= if assigns[:input_classes] do @input_classes end %>" + placeholder="<%= if assigns[:placeholder] do @placeholder end %>" + value="<%= if assigns[:value] do @value end %>" + <%= if assigns[:disabled] do "disabled" end %> + /> + <%= if assigns[:prepend] do %> +
+
<%= @prepend %>
+
+ <% end %> +
<%= if assigns[:message] do @message end %>
+
\ No newline at end of file diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_loading_spinner.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_loading_spinner.html.eex new file mode 100644 index 0000000..d501012 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_loading_spinner.html.eex @@ -0,0 +1,6 @@ + + + + +<%= if assigns[:loading_text], do: @loading_text %> + \ No newline at end of file diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_minimal_proxy_pattern.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_minimal_proxy_pattern.html.eex new file mode 100644 index 0000000..c3f069b --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_minimal_proxy_pattern.html.eex @@ -0,0 +1,10 @@ +
+ <%= render BlockScoutWeb.CommonComponentsView, "_info.html" %> + <%= gettext("Minimal Proxy Contract for") %> <%= link( +@address_hash, +to: address_contract_path(@conn, :index, @address_hash)) %>.
<%= link( + gettext("EIP-1167"), + to: "https://eips.ethereum.org/EIPS/eip-1167", + target: "_blank" +) %><%= gettext(" - minimal bytecode implementation that delegates all calls to a known address") %>
+
\ No newline at end of file diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_modal_bottom_disclaimer.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_modal_bottom_disclaimer.html.eex new file mode 100644 index 0000000..2846db4 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_modal_bottom_disclaimer.html.eex @@ -0,0 +1,8 @@ + \ No newline at end of file diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_modal_close_button.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_modal_close_button.html.eex new file mode 100644 index 0000000..681d1c4 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_modal_close_button.html.eex @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_modal_qr_code.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_modal_qr_code.html.eex new file mode 100644 index 0000000..e95d088 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_modal_qr_code.html.eex @@ -0,0 +1,18 @@ + \ No newline at end of file diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_modal_status.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_modal_status.html.eex new file mode 100644 index 0000000..afb09a4 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_modal_status.html.eex @@ -0,0 +1,32 @@ + \ No newline at end of file diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_pagination_container.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_pagination_container.html.eex new file mode 100644 index 0000000..7fd9f01 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_pagination_container.html.eex @@ -0,0 +1,66 @@ +
+ <%= if false do %> + +
+ <%= gettext "Show" %> + + <%= gettext "Records" %> +
+ <% end %> + + +
diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_progress_from_to.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_progress_from_to.html.eex new file mode 100644 index 0000000..4ae44e5 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_progress_from_to.html.eex @@ -0,0 +1,9 @@ +
+
+
<%= @from %>
+
<%= @to %>
+
+
+
+
+
\ No newline at end of file diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_rap_pagination_container.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_rap_pagination_container.html.eex new file mode 100644 index 0000000..efd1ce6 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_rap_pagination_container.html.eex @@ -0,0 +1,15 @@ +
+
    +
+
    + + +
  • +
+
+<%= if assigns[:showing_limit] do %> +
(<%= gettext("Only the first")%> <%= assigns[:showing_limit] |> BlockScoutWeb.Cldr.Number.to_string! %> <%= gettext("elements are displayed")%>) +
+<% end %> \ No newline at end of file diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_status_icon.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_status_icon.html.eex new file mode 100644 index 0000000..bda361e --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_status_icon.html.eex @@ -0,0 +1,10 @@ +<%= case @status do %> + <% :success -> %> + + <% {:error, _} -> %> + + <% :awaiting_internal_transactions -> %> + + <% :pending -> %> + <% _ -> %> +<% end %> \ No newline at end of file diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_svg_minus.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_svg_minus.html.eex new file mode 100644 index 0000000..1c08c85 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_svg_minus.html.eex @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_svg_pen.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_svg_pen.html.eex new file mode 100644 index 0000000..559468d --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_svg_pen.html.eex @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_svg_plus.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_svg_plus.html.eex new file mode 100644 index 0000000..4f93df8 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_svg_plus.html.eex @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_svg_trash.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_svg_trash.html.eex new file mode 100644 index 0000000..7d83186 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_svg_trash.html.eex @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_table-loader.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_table-loader.html.eex new file mode 100644 index 0000000..ecaf52d --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_table-loader.html.eex @@ -0,0 +1,9 @@ +<%= for _r <- 1..5 do %> + + <%= for _c <- 1..@columns_num do %> + + + + <% end %> + +<% end %> diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_tenderly_link.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_tenderly_link.html.eex new file mode 100644 index 0000000..665a567 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_tenderly_link.html.eex @@ -0,0 +1,4 @@ +<% tenderly_link = "https://dashboard.tenderly.co/tx#{@tenderly_chain_path}/" <> "0x" <> Base.encode16(@transaction_hash.bytes, case: :lower) %> + + Open in Tenderly <%= render BlockScoutWeb.IconsView, "_external_link.html" %> + \ No newline at end of file diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_tile-loader.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_tile-loader.html.eex new file mode 100644 index 0000000..91195d5 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_tile-loader.html.eex @@ -0,0 +1,120 @@ +
+
+
+ + + + + + +
+
+ + +
+
+ + + + + + +
+
+
+
+
+
+ + + + + + +
+
+ + +
+
+ + + + + + +
+
+
+
+
+
+ + + + + + +
+
+ + +
+
+ + + + + + +
+
+
+
+
+
+ + + + + + +
+
+ + +
+
+ + + + + + +
+
+
+
+
+
+ + + + + + +
+
+ + +
+
+ + + + + + +
+
+
\ No newline at end of file diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_token_transfer_type_display_name.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_token_transfer_type_display_name.html.eex new file mode 100644 index 0000000..d79aff6 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/common_components/_token_transfer_type_display_name.html.eex @@ -0,0 +1,12 @@ +<%= case @type do %> + <% :token_burning -> %> + <%= gettext("Token Burning") %> + <% :token_minting -> %> + <%= gettext("Token Minting") %> + <% :token_spawning -> %> + <%= gettext("Token Creation") %> + <% :token_transfer -> %> + <%= gettext("Token Transfer") %> + <% _ -> %> + <%= gettext("Token Transfer") %> +<% end %> \ No newline at end of file diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/csv_export/index.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/csv_export/index.html.eex new file mode 100644 index 0000000..1f94af1 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/csv_export/index.html.eex @@ -0,0 +1,45 @@ +<%= + for status <- ["error", "warning", "success", "question"] do + render BlockScoutWeb.CommonComponentsView, "_modal_status.html", status: status + end + %> +"> +
+
+
+

<%= gettext "Export Data" %>

+ +
+ <% filter_text = if Helper.valid_filter?(@filter_type, @filter_value, @type), do: " with applied filter by #{@filter_type} (#{@filter_value})", else: "" %> +

<%= gettext("Export") %> <%= type_display_name(@type) %> <%= gettext("for address") %> <%= link( + @address_hash_string, + to: address_path(@conn, :show, @address_hash_string) + ) %><%= filter_text %> <%= gettext("to CSV file") %>

+
+ +
+ from to +
+ +
+ + +
+
+ + <%= cond do %> + <% Application.get_env(:block_scout_web, :recaptcha)[:v2_client_key] -> %> + + <% Application.get_env(:block_scout_web, :recaptcha)[:v3_client_key] -> %> + + <% true -> %> + <% end %> +
diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/error422/index.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/error422/index.html.eex new file mode 100644 index 0000000..fb188f6 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/error422/index.html.eex @@ -0,0 +1,12 @@ +
+
+
+ Request cannot be processed +
+
+

<%= gettext "Request cannot be processed" %>

+

<%= gettext "Your request contained an error, perhaps a mistyped tx/block/address hash. Try again, and check the developer tools console for more info." %>

+ <%= gettext "Back to home" %> +
+
+
diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/form/_tag.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/form/_tag.html.eex new file mode 100644 index 0000000..962284f --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/form/_tag.html.eex @@ -0,0 +1,3 @@ +
"> + <%= @text %> +
diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/form/text_field.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/form/text_field.html.eex new file mode 100644 index 0000000..c0a307c --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/form/text_field.html.eex @@ -0,0 +1,15 @@ +
+ <%= if @label do %> + <%= if @id do %> + + <% else %> + + <% end %> + <% end %> + <%= @input_field %> + <%= for error <- @errors do %> +
+ <%= error %> +
+ <% end %> +
diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/icons/_accounts_icon.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/icons/_accounts_icon.html.eex new file mode 100644 index 0000000..9b30e9e --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/icons/_accounts_icon.html.eex @@ -0,0 +1,4 @@ + + + + diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/icons/_active_icon.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/icons/_active_icon.html.eex new file mode 100644 index 0000000..6da126f --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/icons/_active_icon.html.eex @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/icons/_api_icon.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/icons/_api_icon.html.eex new file mode 100644 index 0000000..5f7da6f --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/icons/_api_icon.html.eex @@ -0,0 +1,5 @@ + + + + + diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/icons/_apps_icon.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/icons/_apps_icon.html.eex new file mode 100644 index 0000000..af60841 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/icons/_apps_icon.html.eex @@ -0,0 +1,5 @@ + + + + + diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/icons/_block_icon.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/icons/_block_icon.html.eex new file mode 100644 index 0000000..fa782fe --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/icons/_block_icon.html.eex @@ -0,0 +1,5 @@ + + + + + diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/icons/_blockchain_icon.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/icons/_blockchain_icon.html.eex new file mode 100644 index 0000000..f823473 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/icons/_blockchain_icon.html.eex @@ -0,0 +1,3 @@ + + + diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/icons/_check_dark_forest_icon.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/icons/_check_dark_forest_icon.html.eex new file mode 100644 index 0000000..56a4854 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/icons/_check_dark_forest_icon.html.eex @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/icons/_external_link.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/icons/_external_link.html.eex new file mode 100644 index 0000000..5f9f06e --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/icons/_external_link.html.eex @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/icons/_gas_price_icon.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/icons/_gas_price_icon.html.eex new file mode 100644 index 0000000..01cdad2 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/icons/_gas_price_icon.html.eex @@ -0,0 +1,3 @@ + + + diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/icons/_guage_icon.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/icons/_guage_icon.html.eex new file mode 100644 index 0000000..e5cae29 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/icons/_guage_icon.html.eex @@ -0,0 +1,18 @@ + + + + + diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/icons/_hourglass_icon.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/icons/_hourglass_icon.html.eex new file mode 100644 index 0000000..761b37c --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/icons/_hourglass_icon.html.eex @@ -0,0 +1,17 @@ + + + + diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/icons/_inactive_icon.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/icons/_inactive_icon.html.eex new file mode 100644 index 0000000..93b7a65 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/icons/_inactive_icon.html.eex @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/icons/_network_icon.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/icons/_network_icon.html.eex new file mode 100644 index 0000000..cf668d0 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/icons/_network_icon.html.eex @@ -0,0 +1,3 @@ + + + diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/icons/_search_icon.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/icons/_search_icon.html.eex new file mode 100644 index 0000000..b93ed63 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/icons/_search_icon.html.eex @@ -0,0 +1,3 @@ + + + diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/icons/_smart_contract.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/icons/_smart_contract.html.eex new file mode 100644 index 0000000..372858f --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/icons/_smart_contract.html.eex @@ -0,0 +1,22 @@ + + + + + + + + diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/icons/_test_network_icon.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/icons/_test_network_icon.html.eex new file mode 100644 index 0000000..111fc1c --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/icons/_test_network_icon.html.eex @@ -0,0 +1,3 @@ + + + diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/icons/_tokens_icon.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/icons/_tokens_icon.html.eex new file mode 100644 index 0000000..6d808a1 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/icons/_tokens_icon.html.eex @@ -0,0 +1,4 @@ + + + + diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/icons/_transaction_icon.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/icons/_transaction_icon.html.eex new file mode 100644 index 0000000..179613d --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/icons/_transaction_icon.html.eex @@ -0,0 +1,4 @@ + + + + diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/internal_server_error/index.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/internal_server_error/index.html.eex new file mode 100644 index 0000000..df7ff8c --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/internal_server_error/index.html.eex @@ -0,0 +1,12 @@ +
+
+
+ Internal server error +
+
+

<%= gettext "Internal server error" %>

+

<%= gettext "An unexpected error has occurred. Try reloading the page, or come back soon and try again." %>

+ <%= gettext "Back to home" %> +
+
+
diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/internal_transaction/_tile.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/internal_transaction/_tile.html.eex new file mode 100644 index 0000000..3c23ba7 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/internal_transaction/_tile.html.eex @@ -0,0 +1,48 @@ +<% error = @internal_transaction.error %> +
" data-test="internal_transaction" data-key="<%= @internal_transaction.transaction_hash %>_<%= @internal_transaction.index %>" data-internal-transaction-transaction-hash="<%= @internal_transaction.transaction_hash %>" data-internal-transaction-index="<%= @internal_transaction.index %>"> +
+ +
+ + <%= gettext("Internal Transaction") %> + + <%= type(@internal_transaction) %> + <%= if error do %> + <%= gettext "Error" %>: <%= error %> + <% end %> +
+ +
+ <%= render BlockScoutWeb.TransactionView, "_link.html", transaction_hash: @internal_transaction.transaction_hash %> + + <%= @internal_transaction |> BlockScoutWeb.AddressView.address_partial_selector(:from, assigns[:current_address]) |> (&(if is_list(&1), do: Keyword.put(&1, :ignore_implementation_name, true), else: &1)).() |> BlockScoutWeb.RenderHelper.render_partial() %> + → + <%= @internal_transaction |> BlockScoutWeb.AddressView.address_partial_selector(:to, assigns[:current_address]) |> (&(if is_list(&1), do: Keyword.put(&1, :ignore_implementation_name, true), else: &1)).() |> BlockScoutWeb.RenderHelper.render_partial() %> + + + + <%= BlockScoutWeb.TransactionView.value(@internal_transaction, include_label: false) %> <%= Explorer.coin_name() %> + + +
+ +
+ + <%= link( + gettext("Block #%{number}", number: to_string(@internal_transaction.block_number)), + to: block_path(BlockScoutWeb.Endpoint, :show, @internal_transaction.block_number) + ) %> + + + <%= if assigns[:current_address] do %> + + <%= if assigns[:current_address].hash == @internal_transaction.from_address_hash do %> + <%= gettext "OUT" %> + <% else %> + <%= gettext "IN" %> + <% end %> + + <% end %> +
+
+
diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/layout/_account_menu_item.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/layout/_account_menu_item.html.eex new file mode 100644 index 0000000..e7742d2 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/layout/_account_menu_item.html.eex @@ -0,0 +1,37 @@ +<%= if Explorer.Account.enabled?() do %> + <%= if @current_user do %> + + <% else %> +
  • + +
  • + <% end %> +<% end %> diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/layout/_add_chain_to_mm.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/layout/_add_chain_to_mm.html.eex new file mode 100644 index 0000000..469a398 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/layout/_add_chain_to_mm.html.eex @@ -0,0 +1,13 @@ +<%= unless Application.get_env(:block_scout_web, :disable_add_to_mm_button) do %> + + <% sub_network = Keyword.get(Application.get_env(:block_scout_web, BlockScoutWeb.Chain), :subnetwork) %> + +
  • + + <%= gettext("Add") <> " #{sub_network}" %> + +
  • +<% end %> \ No newline at end of file diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/layout/_default_title.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/layout/_default_title.html.eex new file mode 100644 index 0000000..38affa0 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/layout/_default_title.html.eex @@ -0,0 +1,3 @@ + + <%= gettext("%{subnetwork} Explorer - BlockScout", subnetwork: subnetwork_title()) %> + diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/layout/_footer.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/layout/_footer.html.eex new file mode 100644 index 0000000..065f6d9 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/layout/_footer.html.eex @@ -0,0 +1,97 @@ +
    + +
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/layout/_search.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/layout/_search.html.eex new file mode 100644 index 0000000..153e972 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/layout/_search.html.eex @@ -0,0 +1,35 @@ + +
    "> +
    +
    "> + +
    +
    + +
    +
    +
    + / +
    +
    +
    + +
    \ No newline at end of file diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/layout/_topnav.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/layout/_topnav.html.eex new file mode 100644 index 0000000..36b3b1f --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/layout/_topnav.html.eex @@ -0,0 +1,198 @@ +<% apps_menu = Application.get_env(:block_scout_web, :apps_menu) %> +<% other_nets = dropdown_other_nets() %> +<% test_nets = test_nets(dropdown_nets()) %> +<% main_nets = dropdown_head_main_nets() %> + diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/layout/app.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/layout/app.html.eex new file mode 100644 index 0000000..996abc8 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/layout/app.html.eex @@ -0,0 +1,181 @@ + + + + + + + + <%= case @view_module do %> + <% Elixir.BlockScoutWeb.ChainView -> %> + "> + " as="script"> + " as="script"> + " as="script"> + <% Elixir.BlockScoutWeb.TransactionView -> %> + "> + "> + " as="script"> + <% _ -> %> + "> + <% end %> + " as="script"> + " as="image" crossorigin> + " as="image" crossorigin> + " as="image" crossorigin> + " as="image" crossorigin> + " as="image" crossorigin> + " as="image" crossorigin> + " as="style" onload="this.onload=null;this.rel='stylesheet'"> + "> + <%= render_existing(@view_module, "styles.html", assigns) %> + + "> + "> + "> + "> + " color="#5bbad5"> + "> + + "> + + + <%= render_existing(@view_module, "_metatags.html", assigns) || render("_default_title.html") %> + + + + + +
    + +
    <%= Application.get_env(:block_scout_web, :permanent_dark_mode_enabled) %>
    +
    <%= Application.get_env(:block_scout_web, :permanent_light_mode_enabled) %>
    +
    <%= Application.get_env(:indexer, :first_block) %>
    + + + + + + + + +
    + <%= raw("The new Blockscout UI is now open source! Learn how to deploy it here") %> +
    + <% show_maintenance_alert = Application.get_env(:block_scout_web, BlockScoutWeb.Chain)[:show_maintenance_alert] %> + <%= if show_maintenance_alert do %> +
    + <%= raw(System.get_env("MAINTENANCE_ALERT_MESSAGE")) %> +
    + <% end %> + <% hide_indexing_progress_alert = Application.get_env(:indexer, :hide_indexing_progress_alert) %> + <%= unless hide_indexing_progress_alert do %> + <% indexed_ratio_blocks = Chain.indexed_ratio_blocks() %> + <% indexed_ratio = + case Chain.finished_indexing_from_ratio?(indexed_ratio_blocks) do + false -> indexed_ratio_blocks + _ -> Chain.indexed_ratio_internal_transactions() + end %> + <%= if not Chain.finished_indexing_from_ratio?(indexed_ratio) do %> +
    + <%= render BlockScoutWeb.CommonComponentsView, "_loading_spinner.html" %> + + <%= gettext("- We're indexing this chain right now. Some of the counts may be inaccurate.") %> +
    + <% end %> + <% end %> + <% session = Explorer.Account.enabled?() && Map.get(@conn.private, :plug_session) && Plug.Conn.get_session(@conn, :current_user) %> + <%= render BlockScoutWeb.LayoutView, "_topnav.html", current_user: session, conn: @conn %> + <%= if session && !session[:email_verified] do %> + + <% else %> + + <% end %> + +
    + <%= @inner_content %> +
    + <%= render BlockScoutWeb.LayoutView, "_footer.html", assigns %> +
    + <%= if ( + @view_module != Elixir.BlockScoutWeb.ChainView && + @view_module != Elixir.BlockScoutWeb.BlockView && + @view_module != Elixir.BlockScoutWeb.BlockTransactionView && + @view_module != Elixir.BlockScoutWeb.BlockWithdrawalView && + @view_module != Elixir.BlockScoutWeb.AddressView && + @view_module != Elixir.BlockScoutWeb.TokensView && + @view_module != Elixir.BlockScoutWeb.TransactionView && + @view_module != Elixir.BlockScoutWeb.PendingTransactionView && + @view_module != Elixir.BlockScoutWeb.TransactionInternalTransactionView && + @view_module != Elixir.BlockScoutWeb.TransactionLogView && + @view_module != Elixir.BlockScoutWeb.TransactionRawTraceView && + @view_module != Elixir.BlockScoutWeb.TransactionTokenTransferView && + @view_module != Elixir.BlockScoutWeb.TransactionStateView && + @view_module != Elixir.BlockScoutWeb.AddressTransactionView && + @view_module != Elixir.BlockScoutWeb.AddressTokenTransferView && + @view_module != Elixir.BlockScoutWeb.AddressTokenView && + @view_module != Elixir.BlockScoutWeb.AddressWithdrawalView && + @view_module != Elixir.BlockScoutWeb.AddressInternalTransactionView && + @view_module != Elixir.BlockScoutWeb.AddressCoinBalanceView && + @view_module != Elixir.BlockScoutWeb.AddressLogsView && + @view_module != Elixir.BlockScoutWeb.AddressValidationView && + @view_module != Elixir.BlockScoutWeb.AddressContractView && + @view_module != Elixir.BlockScoutWeb.AddressContractVerificationView && + @view_module != Elixir.BlockScoutWeb.AddressContractVerificationViaFlattenedCodeView && + @view_module != Elixir.BlockScoutWeb.AddressContractVerificationVyperView && + @view_module != Elixir.BlockScoutWeb.AddressReadContractView && + @view_module != Elixir.BlockScoutWeb.AddressReadProxyView && + @view_module != Elixir.BlockScoutWeb.AddressWriteContractView && + @view_module != Elixir.BlockScoutWeb.AddressWriteProxyView && + @view_module != Elixir.BlockScoutWeb.Tokens.TransferView && + @view_module != Elixir.BlockScoutWeb.Tokens.ContractView && + @view_module != Elixir.BlockScoutWeb.Tokens.HolderView && + @view_module != Elixir.BlockScoutWeb.Tokens.InventoryView && + @view_module != Elixir.BlockScoutWeb.Tokens.InstanceView && + @view_module != Elixir.BlockScoutWeb.Tokens.Instance.MetadataView && + @view_module != Elixir.BlockScoutWeb.Tokens.Instance.OverviewView && + @view_module != Elixir.BlockScoutWeb.Tokens.Instance.TransferView && + @view_module != Elixir.BlockScoutWeb.VerifiedContractsView && + @view_module != Elixir.BlockScoutWeb.APIDocsView && + @view_module != Elixir.BlockScoutWeb.Admin.DashboardView && + @view_module != Elixir.BlockScoutWeb.SearchView && + @view_module != Elixir.BlockScoutWeb.StakesView && + @view_module != Elixir.BlockScoutWeb.WithdrawalView + ) do %> + + <% end %> + <%= + for status <- ["error", "warning", "success", "question"] do + render BlockScoutWeb.CommonComponentsView, "_modal_status.html", status: status + end + %> + <%= render_existing(@view_module, "scripts.html", assigns) %> + <%= if @view_module == Elixir.BlockScoutWeb.ChainView do %> + + + + <% end %> + <%= if @view_module == Elixir.BlockScoutWeb.TransactionView do %> + + <% end %> + + <%= if @view_module in [Elixir.BlockScoutWeb.AddressContractVerificationView, Elixir.BlockScoutWeb.AddressContractVerificationVyperView, Elixir.BlockScoutWeb.AddressContractVerificationViaFlattenedCodeView] do %> + + <% end %> + <%= if @view_module in [Elixir.BlockScoutWeb.AddressContractVerificationViaMultiPartFilesView, Elixir.BlockScoutWeb.AddressContractVerificationViaJsonView, Elixir.BlockScoutWeb.AddressContractVerificationViaStandardJsonInputView] do %> + + + <% end %> + + + diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/log/_data_decoded_view.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/log/_data_decoded_view.html.eex new file mode 100644 index 0000000..3c0fdaa --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/log/_data_decoded_view.html.eex @@ -0,0 +1,37 @@ +
    + " class="table thead-light table-bordered"> + + + + + + + <%= for {name, type, indexed?, value} <- @mapping do %> + + + + + + + <% end %> +
    <%= gettext "Name" %><%= gettext "Type" %><%= gettext "Indexed?" %><%= gettext "Data" %>
    <%= name %><%= type %><%= indexed? %> + <%= case BlockScoutWeb.ABIEncodedValueView.copy_text(type, value) do %> + <% :error -> %> + <%= nil %> + <% copy_text -> %> + + + + + + <% end %> +
    <%= BlockScoutWeb.ABIEncodedValueView.value_html(type, value) %>
    +
    +
    \ No newline at end of file diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/page_not_found/index.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/page_not_found/index.html.eex new file mode 100644 index 0000000..72d1962 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/page_not_found/index.html.eex @@ -0,0 +1,12 @@ +
    +
    +
    + Page not found +
    +
    +

    <%= gettext "Page not found" %>

    +

    <%= gettext "This page is no longer explorable! If you are lost, use the search bar to find what you are looking for." %>

    + <%= gettext "Back to home" %> +
    +
    +
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/page_not_found/index.json.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/page_not_found/index.json.eex new file mode 100644 index 0000000..98b8502 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/page_not_found/index.json.eex @@ -0,0 +1 @@ +Page not found \ No newline at end of file diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/pending_transaction/index.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/pending_transaction/index.html.eex new file mode 100644 index 0000000..57a9545 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/pending_transaction/index.html.eex @@ -0,0 +1,35 @@ +
    + <%= render BlockScoutWeb.Advertisement.TextAdView, "index.html", conn: @conn %> +
    +
    +

    <%= gettext "Pending Transactions" %>

    + +
    + <%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "top", cur_page_number: "1", show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %> +
    + + + <%= render BlockScoutWeb.CommonComponentsView, "_channel_disconnected_message.html", text: gettext("Connection Lost, click to load newer transactions") %> + + +
    + <%= render BlockScoutWeb.CommonComponentsView, "_tile-loader.html" %> +
    + + <%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "bottom", cur_page_number: "1", show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %> + +
    +
    + + +
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/robots/robots.txt.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/robots/robots.txt.eex new file mode 100644 index 0000000..e2f1434 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/robots/robots.txt.eex @@ -0,0 +1,6 @@ +# See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file +# +# To ban all spiders from the entire site uncomment the next two lines: +# User-agent: * +# Disallow: / +Sitemap: <%= APIDocsView.blockscout_url(true) %>/sitemap.xml diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/robots/sitemap.xml.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/robots/sitemap.xml.eex new file mode 100644 index 0000000..e633bc2 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/robots/sitemap.xml.eex @@ -0,0 +1,53 @@ + +<% host = APIDocsView.blockscout_url(true) %> +<% date = to_string(Date.utc_today()) %> +<% non_parameterized_urls = ["/", "/txs", "/blocks", "/accounts", "/verified-contracts", "/tokens", "/apps", "/stats", "/api-docs", "/graphiql", "/search-results", "/withdrawals", "/l2-deposits", "/l2-output-roots", "/l2-txn-batches", "/l2-withdrawals"] %> +<% params = [paging_options: %PagingOptions{page_size: limit()}, necessity_by_association: %{}] %> + + <%= for url <- non_parameterized_urls do %> + + <%= host %><%= url %> + <%= date %> + + <% end %> + + <% addresses = Address.list_top_addresses(params) %> + <%= for {address, _} <- addresses do %> + + <%= host %>/address/<%= to_string(address) %> + <%= date %> + + <% end %> + + <% transactions = Chain.recent_transactions(params, [:validated]) %> + <%= for transaction <- transactions do %> + + <%= host %>/tx/<%= to_string(transaction.hash) %> + <%= date %> + + <% end %> + + <% blocks = Chain.list_blocks(params) %> + <%= for block <- blocks do %> + + <%= host %>/block/<%= to_string(block.number) %> + <%= date %> + + <% end %> + + <% tokens = Token.list_top(nil, params) %> + <%= for token <- tokens do %> + + <%= host %>/token/<%= to_string(token.contract_address_hash) %> + <%= date %> + + <% end %> + + <% smart_contracts_hashes = Chain.verified_contracts_top(limit()) %> + <%= for hash <- smart_contracts_hashes do %> + + <%= host %>/address/<%= Address.checksum(hash) %>?tab=contract + <%= date %> + + <% end %> + diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/search/_empty_td.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/search/_empty_td.html.eex new file mode 100644 index 0000000..ce2107a --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/search/_empty_td.html.eex @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/search/_name_td.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/search/_name_td.html.eex new file mode 100644 index 0000000..7a98114 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/search/_name_td.html.eex @@ -0,0 +1,7 @@ + + <%= if @result.name do %> + + <%= highlight_search_result(@result.name, @query) %> + + <% end %> + \ No newline at end of file diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/search/_tile.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/search/_tile.html.eex new file mode 100644 index 0000000..981d1dd --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/search/_tile.html.eex @@ -0,0 +1,98 @@ +-<%= if @result.block_hash, do: Base.encode16(@result.block_hash, case: :lower), else: "" %>"> + + <%= render BlockScoutWeb.SearchView, "_empty_td.html" %> + <%= case @result.type do %> + <% "token" -> %> + + + <%= if Application.get_env(:block_scout_web, :display_token_icons) do %> + <% chain_id_for_token_icon = Application.get_env(:block_scout_web, :chain_id) %> + <% address_hash = @result.address_hash %> + <%= + render BlockScoutWeb.TokensView, + "_token_icon.html", + chain_id: chain_id_for_token_icon, + address: address_hash + %> + <% end %> + + + + <% res = @result.name <> " (" <> @result.symbol <> ")" %> + + <%= highlight_search_result(res, @query) %> + + + <% "address" -> %> + <%= render BlockScoutWeb.SearchView, "_empty_td.html" %> + <%= render BlockScoutWeb.SearchView, "_name_td.html", result: @result, query: @query, conn: @conn %> + <% "contract" -> %> + <%= render BlockScoutWeb.SearchView, "_empty_td.html" %> + <%= render BlockScoutWeb.SearchView, "_name_td.html", result: @result, query: @query, conn: @conn %> + <% "block" -> %> + <%= render BlockScoutWeb.SearchView, "_empty_td.html" %> + + <%= link( + highlight_search_result(to_string(@result.block_number), @query), + to: block_path(@conn, :show, @result.block_number) + ) %> + + <% _ -> %> + <%= render BlockScoutWeb.SearchView, "_empty_td.html" %> + <%= render BlockScoutWeb.SearchView, "_empty_td.html" %> + <% end %> + + <%= case @result.type do %> + <% "token" -> %> + <%= with {:ok, address_hash} = Chain.string_to_address_hash(@result.address_hash), + {:ok, address} <- Chain.hash_to_address(address_hash) do %> + <%= render BlockScoutWeb.AddressView, + "_link.html", + address: address, + contract: false, + use_custom_tooltip: false + %> + <% end %> + <% "address" -> %> + <%= with {:ok, address_hash} = Chain.string_to_address_hash(@result.address_hash), + {:ok, address} <- Chain.hash_to_address(address_hash) do %> + <%= render BlockScoutWeb.AddressView, + "_link.html", + address: address, + contract: false, + use_custom_tooltip: false + %> + <% end %> + <% "contract" -> %> + <%= with {:ok, address_hash} = Chain.string_to_address_hash(@result.address_hash), + {:ok, address} <- Chain.hash_to_address(address_hash) do %> + <%= render BlockScoutWeb.AddressView, + "_link.html", + address: address, + contract: true, + use_custom_tooltip: false + %> + <% end %> + <% "transaction" -> %> + <%= render BlockScoutWeb.TransactionView, + "_link.html", + transaction_hash: "0x" <> Base.encode16(@result.transaction_hash, case: :lower) %> + <% "user_operation" -> %> + <%= "0x" <> Base.encode16(@result.user_operation_hash, case: :lower) %> + <% "blob" -> %> + <%= "0x" <> Base.encode16(@result.blob_hash, case: :lower) %> + <% "block" -> %> + <%= link( + "0x" <> Base.encode16(@result.block_hash, case: :lower), + to: block_path(@conn, :show, @result.block_number) + ) %> + <% _ -> %> + <% end %> + + +
    <%= @result.type %>
    + + diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/search/results.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/search/results.html.eex new file mode 100644 index 0000000..d472577 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/search/results.html.eex @@ -0,0 +1,51 @@ +
    + <%= render BlockScoutWeb.Advertisement.TextAdView, "index.html", conn: @conn %> +
    +
    +
    + " placeholder="Search" id="search-text-input"> +
    + +
    + <%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "top", cur_page_number: "1", show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %> +
    +

    <%= gettext "Search Results" %>: <%= @query %>

    + +
    +
    + + + + + + + + + + + + <%= render BlockScoutWeb.CommonComponentsView, "_table-loader.html", columns_num: 5 %> + +
    +
     
    +
    +
     
    +
    +
    Search Result
    +
    +
    Hash
    +
    +
    Category
    +
    +
    +
    + <%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "bottom", cur_page_number: "1", show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %> +
    +
    + +
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/smart_contract/_connect_container.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/smart_contract/_connect_container.html.eex new file mode 100644 index 0000000..2438dd2 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/smart_contract/_connect_container.html.eex @@ -0,0 +1,16 @@ +
    + + <%= render BlockScoutWeb.IconsView, "_inactive_icon.html" %> + +

    Disconnected

    + +
    + \ No newline at end of file diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/smart_contract/_function_response.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/smart_contract/_function_response.html.eex new file mode 100644 index 0000000..90d2da9 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/smart_contract/_function_response.html.eex @@ -0,0 +1,44 @@ +
    +
    +[ <%= @function_name %> <%= gettext("method Response") %> ]
    +
    +
    +<%= case @outputs do %> + <% {:error, %{code: code, message: message, data: _data} = error} -> %> + <% revert_reason = Chain.parse_revert_reason_from_error(error) %> + <%= case decode_revert_reason(@smart_contract_address, revert_reason) do %> + <% {:ok, _identifier, text, mapping} -> %> +
    <%= raw(values_with_type(text, :error, nil)) %>
    +
    + <%= for {name, type, value} <- mapping do %> +
    <%= raw(values_with_type(value, type, name, 1)) %>
    + <% end %> +
    + <% {:error, _contract_verified, []} -> %> + <% decoded_revert_reason = BlockScoutWeb.TransactionView.decode_hex_revert_reason_as_utf8(revert_reason) %> +
    <%= "(#{code}) #{message} (#{if String.valid?(decoded_revert_reason), do: decoded_revert_reason, else: revert_reason})" %>
    + <% {:error, _contract_verified, candidates} -> %> + <% {:ok, _identifier, text, mapping} = Enum.at(candidates, 0) %> +
    <%= raw(values_with_type(text, :error, nil)) %>
    +
    + <%= for {name, type, value} <- mapping do %> +
    <%= raw(values_with_type(value, type, name, 1)) %>
    + <% end %> +
    + <% _ -> %> + <% decoded_revert_reason = BlockScoutWeb.TransactionView.decode_hex_revert_reason_as_utf8(revert_reason) %> +
    <%= "(#{code}) #{message} (#{if String.valid?(decoded_revert_reason), do: decoded_revert_reason, else: revert_reason})" %>
    + <% end %> + <% {:error, %{code: code, message: message}} -> %> +
    (error) : <%= "(#{code}) #{message}" %>
    + <% {:error, error} -> %> +
    (error) : <%= cut_rpc_url(error) %>
    + <% _ -> %> +
    +[<%= for {item, index} <- Enum.with_index(@outputs) do %>
    +<%= if named_argument?(item) do %><%= item["name"] %><% end %>
    +<%= raw(values_with_type(item["value"], item["type"], fetch_name(@names, index), 0)) %>
    +<% end %>]
    +<% end %>
    +
    +
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/smart_contract/_functions.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/smart_contract/_functions.html.eex new file mode 100644 index 0000000..495ee84 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/smart_contract/_functions.html.eex @@ -0,0 +1,161 @@ +<% minimal_proxy_template = if assigns[:custom_abi], do: nil, else: EIP1167.get_implementation_smart_contract(@address.hash) %> +<% metadata_for_verification = if assigns[:custom_abi], do: nil, else: minimal_proxy_template || SmartContract.get_address_verified_bytecode_twin_contract(@address.hash) %> +<% smart_contract_verified = if assigns[:custom_abi], do: false, else: BlockScoutWeb.API.V2.Helper.smart_contract_verified?(@address) %> +<%= unless smart_contract_verified do %> + <%= if metadata_for_verification do %> + <%= if minimal_proxy_template do %> + <%= render BlockScoutWeb.CommonComponentsView, "_minimal_proxy_pattern.html", address_hash: metadata_for_verification.address_hash, conn: @conn %> + <% else %> + <% path = address_verify_contract_path(@conn, :new, @address.hash) %> +
    + <%= render BlockScoutWeb.CommonComponentsView, "_info.html" %> <%= gettext("Contract is not verified. However, we found a verified contract with the same bytecode in Blockscout DB") %> <%= link( + metadata_for_verification.address_hash, + to: address_contract_path(@conn, :index, metadata_for_verification.address_hash)) %>.
    <%= gettext("All functions displayed below are from ABI of that contract. In order to verify current contract, proceed with") %> <%= link( + gettext("Verify & Publish"), + to: path + ) %> <%= gettext("page") %>
    +
    + <% end %> + <% end %> +<% end %> +<%= if smart_contract_verified do %> + <%= if @address.smart_contract.is_changed_bytecode do %> + <%= render BlockScoutWeb.CommonComponentsView, "_changed_bytecode_warning.html" %> + <% else %> +
    + <%= render BlockScoutWeb.CommonComponentsView, "_changed_bytecode_warning.html" %> +
    + <% end %> +<% end %> +<%= if @contract_type == "proxy" do %> +
    +

    Implementation address:

    <%= link( + @implementation_address, + to: address_path(@conn, :show, @implementation_address) + ) %>

    +
    +<% end %> +<%= for {function, counter} <- Enum.with_index(@read_only_functions ++ @read_functions_required_wallet, 1) do %> +
    > +
    + <%= counter %>. + <%= case function["type"] do %> + <% "fallback" -> %> + <%= gettext "fallback" %><%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip_2.html", text: gettext("The fallback function is executed on a call to the contract if none of the other functions match the given function signature, or if no data was supplied at all and there is no receive Ether function. The fallback function always receives data, but in order to also receive Ether it must be marked payable.") %> + <% "receive" -> %> + <%= gettext "receive" %><%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip_2.html", text: gettext("The receive function is executed on a call to the contract with empty calldata. This is the function that is executed on plain Ether transfers (e.g. via .send() or .transfer()). If no such function exists, but a payable fallback function exists, the fallback function will be called on a plain Ether transfer. If neither a receive Ether nor a payable fallback function is present, the contract cannot receive Ether through regular transactions and throws an exception.") %> + <% _ -> %> + <%= function["name"] %> + <% end %> + → +
    + + <%= if queryable?(function["inputs"]) || writable?(function) || Helper.read_with_wallet_method?(function) do %> +
    + <% function_abi = + case Jason.encode([function]) do + {:ok, abi_string} -> + abi_string + _ -> + if @contract_type == "proxy" do + @implementation_abi + else + @contract_abi + end + end %> +
    " data-type="<%= @contract_type %>" data-url="<%= smart_contract_path(@conn, :show, Address.checksum(@address.hash)) %>" data-contract-address="<%= @address.hash %>" data-contract-abi="<%= function_abi %>" data-implementation-abi="<%= function_abi %>" data-chain-id="<%= Application.get_env(:block_scout_web, :chain_id) %>" data-custom-abi="<%= if assigns[:custom_abi], do: true, else: false %>"> + + + + <%= if queryable?(function["inputs"]) do %> + <%= for input <- function["inputs"] do %> +
    + <%= if int?(input["type"]) do %> + px;"/> + + + + + + + + <% else %> + " /> + <% end %> +
    + <% end %> + <% end %> + + <%= if Helper.payable?(function) do %> +
    + +
    + <% end %> + +
    + +
    +
    + + + <%= if outputs?(function["outputs"]) do %> +
    + <%= if (queryable?(function["inputs"])), do: raw "↳" %> + + <%= for output <- function["outputs"] do %> + <%= if output["name"] && output["name"] !== "", do: "#{output["name"]}(#{output["type"]})", else: output["type"] %> + <% end %> +
    + <% end %> +
    +
    + <% else %> + <%= cond do %> + <% outputs?(function["outputs"]) -> %> +
    + <% length = Enum.count(function["outputs"]) %> + <%= for {output, index} <- Enum.with_index(function["outputs"]) do %> + <%= if address?(output["type"]) do %> +
    + <%= link( + output["value"], + to: address_path(@conn, :show, output["value"])) %><%= if not_last_element?(length, index) do %>, <% end %> +
    + <% else %> + <%= if output["type"] == "uint256" do %> +
    +
    + (uint256): + "><%= output["value"] %> + + + <%= gettext("WEI")%> + <%= Explorer.coin_name() %> + +
    +
    + <% else %> +
    "><%= raw(values_with_type(output["value"], output["type"], fetch_name(function["names"], index), 0)) %>
    + <% end %> + <% end %> + <% end %> +
    + <% error?(function["outputs"]) -> %> + <% {:error, text_error} = function["outputs"] %> +
    <%= cut_rpc_url(text_error) %>
    + <% true -> %> + <% nil %> + <% end %> + <% end %> +
    +<% end %> diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/smart_contract/_pending_contract_write.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/smart_contract/_pending_contract_write.html.eex new file mode 100644 index 0000000..316f571 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/smart_contract/_pending_contract_write.html.eex @@ -0,0 +1,20 @@ + \ No newline at end of file diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/tokens/_tile.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/tokens/_tile.html.eex new file mode 100644 index 0000000..8c0846e --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/tokens/_tile.html.eex @@ -0,0 +1,56 @@ + + + + + <%= @index %> + + + + <%= if Application.get_env(:block_scout_web, :display_token_icons) do %> + <% chain_id_for_token_icon = Application.get_env(:block_scout_web, :chain_id) %> + <% foreign_token_contract_address_hash = nil %> + <% token_hash_for_token_icon = if foreign_token_contract_address_hash, do: foreign_token_contract_address_hash, else: Address.checksum(@token.contract_address_hash) %> + <%= + render BlockScoutWeb.TokensView, + "_token_icon.html", + chain_id: chain_id_for_token_icon, + address: token_hash_for_token_icon + %> + <% end %> + + + <% token = token_display_name(@token) %> + <%= link(token, + to: token_path(BlockScoutWeb.Endpoint, :show, @token.contract_address_hash), + "data-test": "token_link", + class: "text-truncate") %> + + + <%= render BlockScoutWeb.AddressView, + "_link.html", + address: @token.contract_address, + contract: true, + use_custom_tooltip: false + %> + + + <%= if @token.circulating_market_cap do %> + + <% else %> + <%= gettext "N/A" %> + <% end %> + + + <%= if decimals?(@token) do %> + <%= format_according_to_decimals(@token.total_supply, @token.decimals) %> + <% else %> + <%= format_integer_to_currency(@token.total_supply) %> + <% end %> <%= @token.symbol %> + + + + + <%= @token.holder_count %> + + + diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/tokens/_token_icon.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/tokens/_token_icon.html.eex new file mode 100644 index 0000000..9a4de75 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/tokens/_token_icon.html.eex @@ -0,0 +1,2 @@ +<% token_icon_url = Explorer.Chain.get_token_icon_url_by(@chain_id, @address) %> +" alt="" onerror="if (this.src != '/images/icons/token_icon_default.svg') this.src = '/images/icons/token_icon_default.svg';"/> diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/tokens/_token_icon_default.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/tokens/_token_icon_default.html.eex new file mode 100644 index 0000000..dd944ad --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/tokens/_token_icon_default.html.eex @@ -0,0 +1 @@ +" alt=""/> \ No newline at end of file diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/tokens/contract/_metatags.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/tokens/contract/_metatags.html.eex new file mode 100644 index 0000000..e3754d4 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/tokens/contract/_metatags.html.eex @@ -0,0 +1 @@ +<%= BlockScoutWeb.Tokens.OverviewView.render "_metatags.html", conn: @conn, token: @token %> diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/tokens/contract/index.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/tokens/contract/index.html.eex new file mode 100644 index 0000000..99bb263 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/tokens/contract/index.html.eex @@ -0,0 +1,23 @@ +
    + <%= render( + OverviewView, + "_details.html", + token: @token, + counters_path: @counters_path, + tags: @tags, + conn: @conn + ) %> + +
    +
    + <%= render OverviewView, "_tabs.html", assigns %> + +
    +
    + <%= render BlockScoutWeb.CommonComponentsView, "_loading_spinner.html", loading_text: gettext("Loading...") %> +
    +
    +
    +
    + +
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/tokens/holder/_metatags.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/tokens/holder/_metatags.html.eex new file mode 100644 index 0000000..e3754d4 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/tokens/holder/_metatags.html.eex @@ -0,0 +1 @@ +<%= BlockScoutWeb.Tokens.OverviewView.render "_metatags.html", conn: @conn, token: @token %> diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/tokens/holder/_token_balances.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/tokens/holder/_token_balances.html.eex new file mode 100644 index 0000000..324741d --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/tokens/holder/_token_balances.html.eex @@ -0,0 +1,19 @@ +
    +
    +
    + + <%= render BlockScoutWeb.AddressView, "_link.html", address: @token_balance.address, contract: Address.smart_contract?(@token_balance.address), use_custom_tooltip: false %> + + + + + <%= format_token_balance_value(@token_balance.value, @token_balance.token_id, @token) %> <%= @token.symbol %> + + + <%= if show_total_supply_percentage?(@token.total_supply) do %> + <%= total_supply_percentage(@token_balance.value, @token.total_supply) %> + <% end %> + +
    +
    +
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/tokens/holder/index.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/tokens/holder/index.html.eex new file mode 100644 index 0000000..9d78dcc --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/tokens/holder/index.html.eex @@ -0,0 +1,44 @@ +
    + <%= render( + OverviewView, + "_details.html", + token: @token, + counters_path: @counters_path, + tags: @tags, + conn: @conn + ) %> + +
    +
    + <%= render OverviewView, "_tabs.html", assigns %> + +
    + <%= render BlockScoutWeb.CommonComponentsView, "_channel_disconnected_message.html", text: gettext("Connection Lost") %> +

    <%= gettext "Token Holders" %>

    + +
    + <%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "top", cur_page_number: "1", show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %> +
    + + +
    +
    + + <%= gettext "There are no holders for this Token." %> + +
    +
    + +
    + <%= render BlockScoutWeb.CommonComponentsView, "_tile-loader.html" %> +
    + + <%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "bottom", cur_page_number: "1", show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %> + +
    +
    +
    + +
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/tokens/index.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/tokens/index.html.eex new file mode 100644 index 0000000..fbbc853 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/tokens/index.html.eex @@ -0,0 +1,65 @@ +
    + <%= render BlockScoutWeb.Advertisement.TextAdView, "index.html", conn: @conn %> +
    +
    +

    <%= gettext "Tokens" %>

    + +
    + + <%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "top", cur_page_number: "1", show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %> +
    + +
    +
    + + + + + + + + + + + + + + <%= render BlockScoutWeb.CommonComponentsView, "_table-loader.html", columns_num: 7 %> + +
    +
     
    +
    +
     
    +
    +
    <%= gettext "Token" %>
    +
    +
    <%= gettext "Address" %>
    +
    +
    + <%= gettext "Circulating Market Cap" %> +
    +
    +
    + <%= gettext "Total Supply" %> +
    +
    +
    + <%= gettext "Holders Count" %> +
    +
    +
    +
    + + <%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "bottom", cur_page_number: "1", show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %> +
    +
    + + +
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/tokens/instance/holder/index.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/tokens/instance/holder/index.html.eex new file mode 100644 index 0000000..7fe594a --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/tokens/instance/holder/index.html.eex @@ -0,0 +1,41 @@ +
    + <%= render( + OverviewView, + "_details.html", + token: @token, + total_token_transfers: @total_token_transfers, + token_id: @token_instance.token_id, + token_instance: @token_instance, + conn: @conn + ) %> + +
    +
    + <%= render OverviewView, "_tabs.html", assigns %> +
    +

    <%= gettext "Token Holders" %>

    + +
    + <%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "top", cur_page_number: "1", show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %> +
    + + + + + +
    + <%= render BlockScoutWeb.CommonComponentsView, "_tile-loader.html" %> +
    + + <%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "bottom", cur_page_number: "1", show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %> + +
    +
    +
    +
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/tokens/instance/metadata/index.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/tokens/instance/metadata/index.html.eex new file mode 100644 index 0000000..ca5cc7e --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/tokens/instance/metadata/index.html.eex @@ -0,0 +1,30 @@ +
    + <%= render( + OverviewView, + "_details.html", + token: @token, + total_token_transfers: @total_token_transfers, + token_id: @token_instance.token_id, + token_instance: @token_instance, + conn: @conn + ) %> + +
    +
    + <%= render OverviewView, "_tabs.html", assigns %> +
    +
    +
    +

    <%= gettext "Metadata" %>

    + +
    +
    +
    <%= format_metadata(@token_instance.instance.metadata) %>
    +            
    +
    +
    +
    +
    +
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex new file mode 100644 index 0000000..bb2b4d7 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex @@ -0,0 +1,97 @@ +
    +
    +
    +
    +
    +

    + <%= if token_name?(@token) do %> + <%= link(@token.name, to: token_path(BlockScoutWeb.Endpoint, :show, @token.contract_address_hash)) %> + <% else %> + <%= gettext("Token Details") %> + <% end %> + + + + ' + > + + + + + + + <%= render BlockScoutWeb.CommonComponentsView, "_btn_copy.html", + additional_classes: ["overview-title-item"], + clipboard_text: @token_id, + aria_label: gettext("Copy Token ID"), + title: gettext("Copy Token ID") %> + <%= render BlockScoutWeb.CommonComponentsView, "_btn_qr_code.html" %> + +

    + +

    <%= gettext "Token ID" %>: <%= to_string(@token_id) %>

    + +
    +
    + <%= if external_url(@token_instance.instance) do %> + + target="_blank"> + View In App <%= render BlockScoutWeb.IconsView, "_external_link.html" %> + + + <% end %> + <%= @token.type %> + <%= @total_token_transfers %> <%= gettext "Transfers" %> + <%= if decimals?(@token) do %> + <%= @token.decimals %> <%= gettext "Decimals" %> + <% end %> +
    +
    +
    +
    +
    + +
    +
    +
    +
    + <%= if media_type(media_src(@token_instance.instance, true)) == "video" do %> + + <% else %> + /> + <% end %> +
    +
    +
    +
    +
    + +
    + + +<%= render BlockScoutWeb.Advertisement.BannersAdView, "_banner_728.html", conn: @conn %> diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/tokens/instance/overview/_tabs.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/tokens/instance/overview/_tabs.html.eex new file mode 100644 index 0000000..b4dbb3b --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/tokens/instance/overview/_tabs.html.eex @@ -0,0 +1,22 @@ +
    + <%= link( + gettext("Token Transfers"), + class: "card-tab #{tab_status("token-transfers", @conn.request_path)}", + to: token_instance_path(@conn, :show, @token.contract_address_hash, to_string(@token_instance.token_id)) + ) + %> + <%= if @token_instance.instance && @token_instance.instance.metadata do %> + <%= link( + gettext("Metadata"), + to: token_instance_metadata_path(@conn, :index, Address.checksum(@token.contract_address_hash), to_string(@token_instance.token_id)), + class: "card-tab #{tab_status("metadata", @conn.request_path)}") + %> + <% end %> + <%= if @token.type == "ERC-1155" and !Chain.token_id_1155_is_unique?(@token.contract_address_hash, @token_instance.token_id) do %> + <%= link( + gettext("Token Holders"), + to: token_instance_holder_path(@conn, :index, Address.checksum(@token.contract_address_hash), to_string(@token_instance.token_id)), + class: "card-tab #{tab_status("token-holders", @conn.request_path)}") + %> + <% end %> +
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/tokens/instance/transfer/index.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/tokens/instance/transfer/index.html.eex new file mode 100644 index 0000000..c49b042 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/tokens/instance/transfer/index.html.eex @@ -0,0 +1,41 @@ +
    + <%= render( + OverviewView, + "_details.html", + token: @token, + total_token_transfers: @total_token_transfers, + token_id: @token_instance.token_id, + token_instance: @token_instance, + conn: @conn + ) %> + +
    +
    + <%= render OverviewView, "_tabs.html", assigns %> +
    +

    <%= gettext "Token Transfers" %>

    + +
    + <%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "top", cur_page_number: "1", show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %> +
    + + + + + +
    + <%= render BlockScoutWeb.CommonComponentsView, "_tile-loader.html" %> +
    + + <%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "bottom", cur_page_number: "1", show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %> + +
    +
    +
    +
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/tokens/inventory/_metatags.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/tokens/inventory/_metatags.html.eex new file mode 100644 index 0000000..e3754d4 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/tokens/inventory/_metatags.html.eex @@ -0,0 +1 @@ +<%= BlockScoutWeb.Tokens.OverviewView.render "_metatags.html", conn: @conn, token: @token %> diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/tokens/inventory/_token.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/tokens/inventory/_token.html.eex new file mode 100644 index 0000000..2b19e2d --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/tokens/inventory/_token.html.eex @@ -0,0 +1,58 @@ +<% is_1155 = @token.type == "ERC-1155" %> +<% is_unique = not is_1155 or Chain.token_id_1155_is_unique?(@token.contract_address_hash, @instance.token_id) %> + +
    +
    +
    + + <%= if is_unique do%> + <%= gettext "Unique Token" %> + <% else %> + <%= gettext "Not unique Token" %> + <% end %> +
    + + <%= if is_unique do %> +
    + + <%= gettext "Token ID" %>: + + <%= link(@instance.token_id, to: token_instance_path(@conn, :show, "#{@token.contract_address_hash}", "#{@instance.token_id}")) %> + + + + <%= gettext "Owner Address" %>: + + <%= render BlockScoutWeb.AddressView, + "_link.html", + address: @instance.owner, + contract: false, + use_custom_tooltip: false %> + + +
    + <% else %> +
    + + <%= gettext "Token ID" %>: + + <%= link(@instance.token_id, to: token_instance_path(@conn, :show, "#{@token.contract_address_hash}", "#{@instance.token_id}")) %> + + +
    + <% end %> + +
    + +
    + <%= if media_type(media_src(@instance)) == "video" do %> + + <% else %> + /> + <% end %> +
    +
    +
    +
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/tokens/inventory/index.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/tokens/inventory/index.html.eex new file mode 100644 index 0000000..e41c420 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/tokens/inventory/index.html.eex @@ -0,0 +1,42 @@ +
    + <%= render( + OverviewView, + "_details.html", + token: @token, + counters_path: @counters_path, + tags: @tags, + conn: @conn + ) %> + +
    +
    + <%= render OverviewView, "_tabs.html", assigns %> + +
    +

    <%= gettext "Inventory" %>

    + +
    + <%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "top", show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %> +
    + + + +
    +
    + <%= gettext "There are no tokens." %> +
    +
    + +
    + <%= render BlockScoutWeb.CommonComponentsView, "_tile-loader.html" %> +
    + + <%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "bottom", cur_page_number: "1", show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %> + +
    +
    +
    + +
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/tokens/overview/_details.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/tokens/overview/_details.html.eex new file mode 100644 index 0000000..d5df2db --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/tokens/overview/_details.html.eex @@ -0,0 +1,156 @@ +<% circles_addresses_list = CustomContractsHelper.get_custom_addresses_list(:circles_addresses) %> +<% address_hash_string = "0x" <> Base.encode16(@token.contract_address_hash.bytes, case: :lower) %> +<% {:ok, created_from_address} = if @token.contract_address_hash, do: Chain.hash_to_address(@token.contract_address_hash), else: {:ok, nil} %> +<% created_from_address_hash = if from_address_hash(created_from_address), do: "0x" <> Base.encode16(from_address_hash(created_from_address).bytes, case: :lower), else: nil %> +
    + <%= render BlockScoutWeb.Advertisement.TextAdView, "index.html", conn: @conn %> +
    +
    +
    +
    +

    +
    + <%= cond do %> + <% Enum.member?(circles_addresses_list, address_hash_string) -> %> +
    + +
    + <% Enum.member?(circles_addresses_list, created_from_address_hash) -> %> +
    + +
    + <% true -> %> + <%= nil %> + <% end %> + <%= if token_name?(@token) do %> + + +
    <%= @token.name %>
    + <% else %> + <%= gettext("Token Details") %> + <% end %> + <%= render BlockScoutWeb.AddressView, "_labels.html", address_hash: @token.contract_address_hash, tags: @tags %> +
    + + + <%= render BlockScoutWeb.CommonComponentsView, "_btn_copy.html", + additional_classes: ["overview-title-item"], + clipboard_text: Address.checksum(@token.contract_address_hash), + aria_label: gettext("Copy Address"), + title: gettext("Copy Address") %> + <%= render BlockScoutWeb.CommonComponentsView, "_btn_qr_code.html" %> + +

    +
    +
    + <%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip_2.html", + text: gettext("Address of the token contract") %> + <%= gettext("Contract") %> +
    +
    + <%= link( + @token.contract_address_hash, + to: AccessHelper.get_path(@conn, :address_path, :show, + Address.checksum(@token.contract_address_hash)), + "data-test": "token_contract_address" + ) + %> +
    +
    +
    " data-selector="total-supply-row"> +
    + <%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip_2.html", + text: gettext("The total amount of tokens issued") %> + <%= gettext("Total supply") %> +
    +
    + <%= if total_supply?(@token) do %> + <%= if decimals?(@token) do %> + <%= format_according_to_decimals(@token.total_supply, @token.decimals) %> + <% else %> + <%= format_integer_to_currency(@token.total_supply) %> + <% end %> <%= @token.symbol %> + <% end %> +
    +
    + <%= if @token.total_supply && @token.fiat_value do %> +
    +
    + <%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip_2.html", + text: gettext("Total Supply * Price") %> + <%= gettext("Market Cap") %> +
    +
    + +
    +
    + <%= unless Map.has_key?(@token, :custom_cap) && @token.custom_cap do %> +
    +
    + <%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip_2.html", + text: gettext("Price per token on the exchanges") %> + <%= gettext("Price") %> +
    +
    + +
    +
    + <% end %> + <% end %> +
    +
    + <%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip_2.html", + text: gettext("Number of accounts holding the token") %> + <%= gettext("Holders") %> +
    +
    + <% link = if @conn.request_path |> String.contains?("/token-holders"), do: "", else: AccessHelper.get_path(@conn, :token_holder_path, :index, @token.contract_address_hash) %> + <%= render BlockScoutWeb.CommonComponentsView, "_loading_spinner.html", loading_text: gettext("Fetching holders...") %> +
    +
    +
    +
    + <%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip_2.html", + text: gettext("Number of transfers for the token") %> + <%= gettext("Transfers") %> +
    +
    + <% link = if @conn.request_path |> String.contains?("/token-transfers"), do: "", else: AccessHelper.get_path(@conn, :token_transfer_path, :index, @token.contract_address_hash) %> + <%= render BlockScoutWeb.CommonComponentsView, "_loading_spinner.html", loading_text: gettext("Fetching transfers...") %> +
    +
    + <%= if decimals?(@token) do %> +
    +
    + <%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip_2.html", + text: gettext("Number of digits that come after the decimal place when displaying token value") %> + <%= gettext("Decimals") %> +
    +
    + <%= @token.decimals %> +
    +
    + <% end %> +
    +
    + <%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip_2.html", + text: gettext("Type of the token standard") %> + <%= gettext("Token type") %> +
    +
    + <%= @token.type %> +
    +
    +
    +
    +
    +
    +
    + +<%= render BlockScoutWeb.CommonComponentsView, "_modal_qr_code.html", qr_code: BlockScoutWeb.AddressView.qr_code(Address.checksum(@token.contract_address_hash)), title: @token.contract_address %> +<%= render BlockScoutWeb.Advertisement.BannersAdView, "_banner_728.html", conn: @conn %> + diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/tokens/overview/_metatags.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/tokens/overview/_metatags.html.eex new file mode 100644 index 0000000..7c4a80f --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/tokens/overview/_metatags.html.eex @@ -0,0 +1,5 @@ + + <%= "#{token_name(@token)} (#{token_symbol(@token)}) - #{LayoutView.subnetwork_title()} - BlockScout" %> + + +"> diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/tokens/overview/_tabs.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/tokens/overview/_tabs.html.eex new file mode 100644 index 0000000..518c161 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/tokens/overview/_tabs.html.eex @@ -0,0 +1,53 @@ +<% address_hash = Address.checksum(@token.contract_address_hash) %> +<% is_proxy = BlockScoutWeb.Tokens.OverviewView.token_smart_contract_is_proxy?(@token) %> +
    + <%= link( + gettext("Token Transfers"), + class: "card-tab #{tab_status("token-transfers", @conn.request_path)}", + to: AccessHelper.get_path(@conn, :token_path, :show, @token.contract_address_hash) + ) + %> + <%= link( + gettext("Token Holders"), + class: "card-tab #{tab_status("token-holders", @conn.request_path)}", + "data-test": "token_holders_tab", + to: AccessHelper.get_path(@conn, :token_holder_path, :index, address_hash) + ) + %> + <%= if display_inventory?(@token) do %> + <%= link( + gettext("Inventory"), + class: "card-tab #{tab_status("inventory", @conn.request_path)}", + to: AccessHelper.get_path(@conn, :token_inventory_path, :index, address_hash) + ) + %> + <% end %> + <%= if smart_contract_with_read_only_functions?(@token) do %> + <%= link( + gettext("Read Contract"), + to: AccessHelper.get_path(@conn, :token_read_contract_path, :index, address_hash), + class: "card-tab #{tab_status("read-contract", @conn.request_path)}") + %> + <% end %> + <%= if smart_contract_with_write_functions?(@token) do %> + <%= link( + gettext("Write Contract"), + to: AccessHelper.get_path(@conn, :token_write_contract_path, :index, address_hash), + class: "card-tab #{tab_status("write-contract", @conn.request_path)}") + %> + <% end %> + <%= if is_proxy do %> + <%= link( + gettext("Read Proxy"), + to: AccessHelper.get_path(@conn, :token_read_proxy_path, :index, address_hash), + class: "card-tab #{tab_status("read-proxy", @conn.request_path)}") + %> + <% end %> + <%= if smart_contract_with_write_functions?(@token) && is_proxy do %> + <%= link( + gettext("Write Proxy"), + to: AccessHelper.get_path(@conn, :token_write_proxy_path, :index, address_hash), + class: "card-tab #{tab_status("write-proxy", @conn.request_path)}") + %> + <% end %> +
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/tokens/transfer/_metatags.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/tokens/transfer/_metatags.html.eex new file mode 100644 index 0000000..e3754d4 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/tokens/transfer/_metatags.html.eex @@ -0,0 +1 @@ +<%= BlockScoutWeb.Tokens.OverviewView.render "_metatags.html", conn: @conn, token: @token %> diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/tokens/transfer/_token_transfer.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/tokens/transfer/_token_transfer.html.eex new file mode 100644 index 0000000..1e019df --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/tokens/transfer/_token_transfer.html.eex @@ -0,0 +1,50 @@ +
    +
    + +
    + + <%= render(BlockScoutWeb.CommonComponentsView, "_token_transfer_type_display_name.html", type: Chain.get_token_transfer_type(@token_transfer)) %> + +
    + +
    + <%= render BlockScoutWeb.TransactionView, "_link.html", transaction_hash: @token_transfer.transaction_hash %> + + <%= link to: address_token_transfers_path(@conn, :index, Address.checksum(@token_transfer.from_address), Address.checksum(@token.contract_address_hash)), "data-test": "address_hash_link" do %> + <%= render( + BlockScoutWeb.AddressView, + "_responsive_hash.html", + address: @token_transfer.from_address, + contract: Address.smart_contract?(@token_transfer.from_address), + use_custom_tooltip: false + ) %> + <% end %> + → + <%= link to: address_token_transfers_path(@conn, :index, Address.checksum(@token_transfer.to_address), Address.checksum(@token.contract_address_hash)), "data-test": "address_hash_link" do %> + <%= render( + BlockScoutWeb.AddressView, + "_responsive_hash.html", + address: @token_transfer.to_address, + contract: Address.smart_contract?(@token_transfer.to_address), + use_custom_tooltip: false + ) %> + <% end %> + + + + <%= render BlockScoutWeb.TransactionView, "_total_transfers.html", Map.put(assigns, :transfer, @token_transfer) %> + + +
    + +
    + + <%= link( + gettext("Block #%{number}", number: @token_transfer.block_number), + to: block_path(BlockScoutWeb.Endpoint, :show, @token_transfer.block_number) + ) %> + + +
    +
    +
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/tokens/transfer/index.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/tokens/transfer/index.html.eex new file mode 100644 index 0000000..5ea6121 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/tokens/transfer/index.html.eex @@ -0,0 +1,42 @@ +
    + <%= render( + OverviewView, + "_details.html", + token: @token, + counters_path: @counters_path, + tags: @tags, + conn: @conn + ) %> + +
    +
    + <%= render OverviewView, "_tabs.html", assigns %> +
    +

    <%= gettext "Token Transfers" %>

    + +
    + <%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "top", cur_page_number: "1", show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %> +
    + + + + + +
    + <%= render BlockScoutWeb.CommonComponentsView, "_tile-loader.html" %> +
    + + <%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "bottom", cur_page_number: "1", show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %> + +
    +
    + + +
    +
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction/_actions.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction/_actions.html.eex new file mode 100644 index 0000000..391b912 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction/_actions.html.eex @@ -0,0 +1,140 @@ +<%= if @action.protocol == :aave_v3 do %> + <%= if Enum.member?([:borrow, :supply, :withdraw, :repay, :flash_loan], @action.type) do %> +
    "> + + <% amount = formatted_action_amount(@action.data, "amount") %> + <% symbol = Map.get(@action.data, "symbol") %> + <% address = Map.get(@action.data, "address") %> + + + + <% symbol = if symbol != "Ether", do: link(symbol, to: token_path(BlockScoutWeb.Endpoint, :show, address), "data-test": "token_link"), else: raw(symbol) %> + + <%= if @action.type == :borrow do %> + <%= render BlockScoutWeb.TransactionView, "_actions_aave.html", action: "Borrow", amount: amount, symbol: symbol, tail: "From Aave Protocol V3" %> + <% end %> + <%= if @action.type == :supply do %> + <%= render BlockScoutWeb.TransactionView, "_actions_aave.html", action: "Supply", amount: amount, symbol: symbol, tail: "To Aave Protocol V3" %> + <% end %> + <%= if @action.type == :withdraw do %> + <%= render BlockScoutWeb.TransactionView, "_actions_aave.html", action: "Withdraw", amount: amount, symbol: symbol, tail: "From Aave Protocol V3" %> + <% end %> + <%= if @action.type == :repay do %> + <%= render BlockScoutWeb.TransactionView, "_actions_aave.html", action: "Repay", amount: amount, symbol: symbol, tail: "To Aave Protocol V3" %> + <% end %> + <%= if @action.type == :flash_loan do %> + <%= render BlockScoutWeb.TransactionView, "_actions_aave.html", action: "Flash Loan", amount: amount, symbol: symbol, tail: "From Aave Protocol V3" %> + <% end %> + +
    +
    + <% end %> + <%= if Enum.member?([:enable_collateral, :disable_collateral], @action.type) do %> +
    "> + + <% symbol = Map.get(@action.data, "symbol") %> + <% address = Map.get(@action.data, "address") %> + + + + <% symbol = if symbol != "Ether", do: link(symbol, to: token_path(BlockScoutWeb.Endpoint, :show, address), "data-test": "token_link"), else: raw(symbol) %> + + <%= if @action.type == :enable_collateral do %> + Enable + <% else %> + Disable + <% end %> + + <%= symbol %> as Collateral on Aave Protocol V3 + +
    +
    + <% end %> + <%= if @action.type == :liquidation_call do %> +
    "> + <% debt_amount = formatted_action_amount(@action.data, "debt_amount") %> + <% debt_symbol = Map.get(@action.data, "debt_symbol") %> + <% debt_address = Map.get(@action.data, "debt_address") %> + <% debt_symbol = if debt_symbol != "Ether", do: link(debt_symbol, to: token_path(BlockScoutWeb.Endpoint, :show, debt_address), "data-test": "token_link"), else: raw(debt_symbol) %> + + + + <%= render BlockScoutWeb.TransactionView, "_actions_aave.html", action: "Liquidator Repay", amount: debt_amount, symbol: debt_symbol, tail: "To Aave Protocol V3" %> + +
    + + <% collateral_amount = formatted_action_amount(@action.data, "collateral_amount") %> + <% collateral_symbol = Map.get(@action.data, "collateral_symbol") %> + <% collateral_address = Map.get(@action.data, "collateral_address") %> + <% collateral_symbol = if collateral_symbol != "Ether", do: link(collateral_symbol, to: token_path(BlockScoutWeb.Endpoint, :show, collateral_address), "data-test": "token_link"), else: raw(collateral_symbol) %> + + + + <%= render BlockScoutWeb.TransactionView, "_actions_aave.html", action: "Liquidation", amount: collateral_amount, symbol: collateral_symbol, tail: "On Aave Protocol V3" %> + +
    +
    + <% end %> +<% end %> +<%= if @action.protocol == :uniswap_v3 do %> + <%= if @action.type == :mint_nft do %> +
    "> + + + + <% address_string = Map.get(@action.data, "address") %> + <% {address_status, address} = transaction_action_string_to_address(address_string) %> + <% address = if address_status == :ok, do: render_to_string(BlockScoutWeb.AddressView, "_link.html", address: address, contract: Address.smart_contract?(address), use_custom_tooltip: false, trimmed: false), else: render_to_string(BlockScoutWeb.TransactionView, "_actions_address.html", address_string: address_string, action: @action) %> + <% to_address = Map.get(@action.data, "to") %> + <% to_content = raw(render_to_string BlockScoutWeb.TransactionView, "_actions_to.html", address: to_address) %> + <% to = link(to_content, to: address_path(BlockScoutWeb.Endpoint, :show, to_address), "data-test": "address_hash_link") %> + + <%= gettext("Mint of %{address} To %{to}", address: address, to: safe_to_string(to)) |> raw() %> + +
    + + <% token_ids = Map.get(@action.data, "ids") %> + <%= for id <- token_ids do %> + + + <% link_to_id = link id, to: token_instance_path(BlockScoutWeb.Endpoint, :show, address_string, id), "data-test": "token_link" %> + <%= gettext("%{qty} of Token ID [%{link_to_id}]", qty: 1, link_to_id: safe_to_string(link_to_id)) |> raw() %> + +
    + <% end %> +
    + <% end %> + <%= if Enum.member?([:mint, :burn, :collect, :swap], @action.type) do %> +
    "> + + <% amount0 = formatted_action_amount(@action.data, "amount0") %> + <% amount1 = formatted_action_amount(@action.data, "amount1") %> + + <% symbol0 = Map.get(@action.data, "symbol0") %> + <% address0 = Map.get(@action.data, "address0") %> + <% symbol1 = Map.get(@action.data, "symbol1") %> + <% address1 = Map.get(@action.data, "address1") %> + + + + <% symbol0 = if symbol0 != "Ether", do: link(symbol0, to: token_path(BlockScoutWeb.Endpoint, :show, address0), "data-test": "token_link"), else: raw(symbol0) %> + + <% symbol1 = if symbol1 != "Ether", do: link(symbol1, to: token_path(BlockScoutWeb.Endpoint, :show, address1), "data-test": "token_link"), else: raw(symbol1) %> + + <%= if @action.type == :mint do %> + <%= render BlockScoutWeb.TransactionView, "_actions_uniswap.html", action: "Add", amount0: amount0, symbol0: symbol0, conjunction: "And", amount1: amount1, symbol1: symbol1, tail: "Liquidity To Uniswap V3" %> + <% end %> + <%= if @action.type == :burn do %> + <%= render BlockScoutWeb.TransactionView, "_actions_uniswap.html", action: "Remove", amount0: amount0, symbol0: symbol0, conjunction: "And", amount1: amount1, symbol1: symbol1, tail: "Liquidity From Uniswap V3" %> + <% end %> + <%= if @action.type == :collect do %> + <%= render BlockScoutWeb.TransactionView, "_actions_uniswap.html", action: "Collect", amount0: amount0, symbol0: symbol0, conjunction: "And", amount1: amount1, symbol1: symbol1, tail: "From Uniswap V3" %> + <% end %> + <%= if @action.type == :swap do %> + <%= render BlockScoutWeb.TransactionView, "_actions_uniswap.html", action: "Swap", amount0: amount0, symbol0: symbol0, conjunction: "For", amount1: amount1, symbol1: symbol1, tail: "On Uniswap V3" %> + <% end %> + +
    +
    + <% end %> +<% end %> diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction/_actions_aave.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction/_actions_aave.html.eex new file mode 100644 index 0000000..7e1b70b --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction/_actions_aave.html.eex @@ -0,0 +1 @@ +<%= @action %> <%= @amount %> <%= @symbol %> <%= @tail %> \ No newline at end of file diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction/_actions_address.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction/_actions_address.html.eex new file mode 100644 index 0000000..ecb6fb7 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction/_actions_address.html.eex @@ -0,0 +1,13 @@ +<%= link to: address_path(BlockScoutWeb.Endpoint, :show, @address_string), "data-test": "address_hash_link" do %> + <% name = Map.get(@action.data, "name") %> + + <%= AddressView.short_string(name, 15) %> + <%= AddressView.short_string(name, 5) %> + + + <% symbol = Map.get(@action.data, "symbol") %> + + (<%= AddressView.short_string(symbol, 15) %>) + (<%= AddressView.short_string(symbol, 5) %>) + +<% end %> \ No newline at end of file diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction/_actions_to.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction/_actions_to.html.eex new file mode 100644 index 0000000..e370dbb --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction/_actions_to.html.eex @@ -0,0 +1,4 @@ + + <%= @address %> + <%= BlockScoutWeb.AddressView.trimmed_hash(@address) %> + \ No newline at end of file diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction/_actions_uniswap.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction/_actions_uniswap.html.eex new file mode 100644 index 0000000..f9b6b48 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction/_actions_uniswap.html.eex @@ -0,0 +1 @@ +<%= @action %> <%= @amount0 %> <%= @symbol0 %> <%= @conjunction %> <%= @amount1 %> <%= @symbol1 %> <%= @tail %> \ No newline at end of file diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction/_decoded_input.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction/_decoded_input.html.eex new file mode 100644 index 0000000..3afb743 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction/_decoded_input.html.eex @@ -0,0 +1,44 @@ +
    +
    +

    <%= gettext "Input" %>

    + + <%= case @decoded_input_data do %> + <% {:error, :contract_not_verified, candidates} -> %> +
    + <%= gettext "To see accurate decoded input data, the contract must be verified." %> + <%= case @transaction do %> + <% %{to_address: %{hash: hash}} -> %> + <% path = address_verify_contract_path(@conn, :new, hash) %> + <%= gettext "Verify the contract " %><%= gettext "here" %> + <% _ -> %> + <%= nil %> + <% end %> +
    + <%= unless Enum.empty?(candidates) do %> +

    <%= gettext "Potential matches from contract method database:" %>

    + <%= gettext "IMPORTANT: This information is a best guess based on similar functions from other verified contracts." %> + <%= gettext "To have guaranteed accuracy, use the link above to verify the contract's source code." %> + + <%= for {:ok, method_id, text, mapping} <- candidates do %> +
    +

    <%= text %>:

    + + <%= render(BlockScoutWeb.TransactionView, "_decoded_input_body.html", method_id: method_id, text: text, mapping: mapping) %> + <% end %> + <% end %> + <% {:ok, method_id, text, mapping} -> %> + <%= render(BlockScoutWeb.TransactionView, "_decoded_input_body.html", method_id: method_id, text: text, mapping: mapping) %> + <% {:error, :contract_verified, candidates} -> %> +

    <%= gettext "Potential matches from our contract method database:" %>

    + <%= for {:ok, method_id, text, mapping} <- candidates do %> +
    +

    <%= text %>:

    + <%= render(BlockScoutWeb.TransactionView, "_decoded_input_body.html", method_id: method_id, text: text, mapping: mapping) %> + <% end %> + <% _ -> %> +
    + <%= gettext "Failed to decode input data." %> +
    + <% end %> +
    +
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction/_decoded_input_body.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction/_decoded_input_body.html.eex new file mode 100644 index 0000000..ad1d04b --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction/_decoded_input_body.html.eex @@ -0,0 +1,61 @@ +
    + " class="table thead-light table-bordered"> + <%= if !assigns[:error] do %> + + + + + <% end %> + + + + +
    <%= gettext "Method Id" %>0x<%= @method_id %>
    <%= if assigns[:error], do: gettext("Error"), else: gettext("Call") %><%= @text %>
    +
    + +<% max_length = get_max_length() %> +<%= unless Enum.empty?(@mapping) do %> +
    + " class="table thead-light table-bordered"> + + + + + + <%= for {name, type, value} <- @mapping do %> + + + + + + <% end %> +
    <%= gettext "Name" %><%= gettext "Type" %><%= gettext "Data" %>
    <%= name %><%= type %> + <%= case BlockScoutWeb.ABIEncodedValueView.value_html(type, value, true) do %> + <% :error -> %> +
    + <%= gettext "Error rendering value" %> +
    + <% value_with_no_links -> %> + <%= case BlockScoutWeb.ABIEncodedValueView.copy_text(type, value) do %> + <% :error -> %> + <%= nil %> + <% copy_text -> %> + + + + + + <% end %> + <% value_with_links = BlockScoutWeb.ABIEncodedValueView.value_html(type, value, false) %> +
    <%= if String.length(value_with_no_links) > max_length do %>
    <% input = trim(max_length, value_with_no_links) %><%= input[:show] %>...<%= input[:hide] %>
    <% else %><%= value_with_links %><% end %>
    + <% end %> +
    +
    +<% end %> diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction/_emission_reward_tile.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction/_emission_reward_tile.html.eex new file mode 100644 index 0000000..c43c0e2 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction/_emission_reward_tile.html.eex @@ -0,0 +1,34 @@ +
    +
    +
    + + <%= gettext("Emission Contract") %> + + + <%= gettext("Success") %> + +
    +
    + <%= link( + @validator.block.hash, + to: block_path(BlockScoutWeb.Endpoint, :show, @validator.block.hash), + class: "text-truncate") %> + + <%= @emission_funds |> BlockScoutWeb.AddressView.address_partial_selector(nil, @current_address) |> BlockScoutWeb.RenderHelper.render_partial() %> + → + <%= @validator |> BlockScoutWeb.AddressView.address_partial_selector(nil, @current_address) |> BlockScoutWeb.RenderHelper.render_partial() %> + + + + <%= format_wei_value(@emission_funds.reward, :ether) %> + + +
    +
    + + <%= @validator |> block_number() |> BlockScoutWeb.RenderHelper.render_partial() %> + + +
    +
    +
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction/_link.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction/_link.html.eex new file mode 100644 index 0000000..9b0333f --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction/_link.html.eex @@ -0,0 +1,4 @@ +<%= link(@transaction_hash, + to: transaction_path(BlockScoutWeb.Endpoint, :show, @transaction_hash), + "data-test": "transaction_hash_link", + class: "text-truncate") %> diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction/_link_to_token_instance.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction/_link_to_token_instance.html.eex new file mode 100644 index 0000000..88273f4 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction/_link_to_token_instance.html.eex @@ -0,0 +1 @@ +<%= "[" %><%= link(short_token_id(@token_id, 30), to: token_instance_path(BlockScoutWeb.Endpoint, :show, @transfer.token.contract_address_hash, to_string(@token_id)), "data-test": "token_link") %><%= "]" %> \ No newline at end of file diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction/_link_to_token_symbol.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction/_link_to_token_symbol.html.eex new file mode 100644 index 0000000..73cb4a2 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction/_link_to_token_symbol.html.eex @@ -0,0 +1 @@ +<%= link(token_symbol(@transfer.token), to: token_path(BlockScoutWeb.Endpoint, :show, @transfer.token.contract_address_hash), "data-test": "token_link") %> \ No newline at end of file diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction/_metatags.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction/_metatags.html.eex new file mode 100644 index 0000000..920306f --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction/_metatags.html.eex @@ -0,0 +1,14 @@ +<%= if assigns[:transaction] do %> + + <%= gettext( + "Transaction %{transaction} - %{subnetwork} Explorer", + transaction: to_string(@transaction.hash), + subnetwork: BlockScoutWeb.LayoutView.subnetwork_title() + ) %> + + + "> + "> +<% else %> + <%= BlockScoutWeb.LayoutView.render("_default_title.html") %> +<% end %> diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction/_pending_tile.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction/_pending_tile.html.eex new file mode 100644 index 0000000..e3b68d7 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction/_pending_tile.html.eex @@ -0,0 +1,25 @@ +<% status = BlockScoutWeb.TransactionView.transaction_status(@transaction) %> +
    +
    +
    + <%= BlockScoutWeb.TransactionView.transaction_display_type(@transaction) %> +
    <%= BlockScoutWeb.TransactionView.formatted_result(status) %>
    +
    +
    + <%= render BlockScoutWeb.TransactionView, "_link.html", transaction_hash: @transaction.hash %> + + <%= render BlockScoutWeb.AddressView, "_link.html", address: @transaction.from_address, contract: Address.smart_contract?(@transaction.from_address), use_custom_tooltip: false %> + → + <%= if @transaction.to_address_hash do %> + <%= render BlockScoutWeb.AddressView, "_link.html", address: @transaction.to_address, contract: Address.smart_contract?(@transaction.to_address), use_custom_tooltip: false %> + <% else %> + <%= gettext("Contract Address Pending") %> + <% end %> + + + <%= BlockScoutWeb.TransactionView.value(@transaction, include_label: false) %> <%= Explorer.coin_name() %> + <%= BlockScoutWeb.TransactionView.formatted_fee(@transaction, denomination: :ether, include_label: false) %> <%= gettext "TX Fee" %> + +
    +
    +
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction/_tabs.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction/_tabs.html.eex new file mode 100644 index 0000000..75f2820 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction/_tabs.html.eex @@ -0,0 +1,34 @@ +
    + <%= if @show_token_transfers do %> + <%= link( + gettext("Token Transfers"), + class: "card-tab #{tab_status("token-transfers", @conn.request_path, @show_token_transfers)}", + to: AccessHelper.get_path(@conn, :transaction_token_transfer_path, :index, @transaction) + ) + %> + <% end %> + <%= link( + gettext("Internal Transactions"), + class: "card-tab #{tab_status("internal-transactions", @conn.request_path, @show_token_transfers)}", + to: AccessHelper.get_path(@conn, :transaction_internal_transaction_path, :index, @transaction) + ) + %> + <%= link( + gettext("Logs"), + class: "card-tab #{tab_status("logs", @conn.request_path)}", + to: AccessHelper.get_path(@conn, :transaction_log_path, :index, @transaction), + "data-test": "transaction_logs_link" + ) + %> + <%= link( + gettext("Raw Trace"), + class: "card-tab #{tab_status("raw-trace", @conn.request_path)}", + to: AccessHelper.get_path(@conn, :transaction_raw_trace_path, :index, @transaction) + ) %> + <%= link( + gettext("State changes"), + class: "card-tab #{tab_status("state", @conn.request_path)}", + to: AccessHelper.get_path(@conn, :transaction_state_path, :index, @transaction) + ) + %> +
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction/_tile.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction/_tile.html.eex new file mode 100644 index 0000000..78118d1 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction/_tile.html.eex @@ -0,0 +1,99 @@ +<% status = transaction_status(@transaction) %> +<% error_in_internal_transaction = @transaction.has_error_in_internal_transactions %> +<% current_user = AuthController.current_user(@conn) %> +<% transaction_tags = BlockScoutWeb.Models.GetTransactionTags.get_transaction_with_addresses_tags(@transaction, current_user) %> +
    +
    + +
    +
    + <%= if error_in_internal_transaction do %> + <%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip_2.html", text: gettext("Error in internal transactions"), additional_classes: ["color-inherit"] %> + <% end %> + + <%= transaction_display_type(@transaction) %> + +
    + + <%= if status_class(@transaction) == "tile-status--pending" do %> +
    + + +
    + <% end %> + <%= formatted_result(status) %> +
    +
    + +
    + +
    + <%= render "_link.html", transaction_hash: @transaction.hash, data_test: "address_hash_link" %> + <% method_name = Transaction.get_method_name(@transaction) %> + <%= if method_name do %> + <%= render BlockScoutWeb.FormView, "_tag.html", text: method_name, additional_classes: ["method", "ml-1"] %> + <% end %> + <%= if transaction_tags.personal_transaction_tag && transaction_tags.personal_transaction_tag.name !== :error do %> + <%= render BlockScoutWeb.FormView, "_tag.html", text: transaction_tags.personal_transaction_tag.name, additional_classes: [tag_name_to_label(transaction_tags.personal_transaction_tag.name), "ml-1"] %> + <% end %> + <%= render BlockScoutWeb.AddressView, "_labels.html", tags: transaction_tags %> +
    +
    + + <%= @transaction |> BlockScoutWeb.AddressView.address_partial_selector(:from, assigns[:current_address]) |> BlockScoutWeb.RenderHelper.render_partial() %> + → + <%= @transaction |> BlockScoutWeb.AddressView.address_partial_selector(:to, assigns[:current_address]) |> BlockScoutWeb.RenderHelper.render_partial() %> + + + + <%= value(@transaction, include_label: false) %> <%= Explorer.coin_name() %> + + + <%= formatted_fee(@transaction, denomination: :ether, include_label: false) %> <%= gettext "TX Fee" %> + + + + + <%= if involves_token_transfers?(@transaction) do %> +
    + <% [first_token_transfer | remaining_token_transfers] = @transaction.token_transfers %> + + <%= render "_token_transfer.html", address: assigns[:current_address], token_transfer: first_token_transfer %> + +
    + <%= for token_transfer <- remaining_token_transfers do %> + <%= render "_token_transfer.html", address: assigns[:current_address], token_transfer: token_transfer, burn_address_hash: @burn_address_hash %> + <% end %> +
    +
    + + <%= if Enum.any?(remaining_token_transfers) do %> +
    + <%= link gettext("View More Transfers"), to: "#transaction-#{@transaction.hash}", "data-toggle": "collapse", "data-selector": "token-transfer-open", "data-test": "token_transfers_expansion" %> + <%= link gettext("View Less Transfers"), class: "d-none", to: "#transaction-#{@transaction.hash}", "data-toggle": "collapse", "data-selector": "token-transfer-close" %> +
    + <% end %> + <% end %> +
    + +
    + + <%= @transaction |> block_number() |> BlockScoutWeb.RenderHelper.render_partial() %> + + + <%= if from_or_to_address?(@transaction, assigns[:current_address]) do %> + + <%= if @transaction.from_address_hash == assigns[:current_address].hash do %> + + <%= gettext "OUT" %> + + <% else %> + + <%= gettext "IN" %> + + <% end %> + + <% end %> +
    +
    +
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction/_token_transfer.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction/_token_transfer.html.eex new file mode 100644 index 0000000..31e8531 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction/_token_transfer.html.eex @@ -0,0 +1,25 @@ +
    + + <%= if from_or_to_address?(@token_transfer, @address) do %> + <%= if @token_transfer.from_address_hash == @address.hash do %> + + ↳ + + <% else %> + + ↳ + + <% end %> + <% end %> + + <%= @token_transfer |> BlockScoutWeb.AddressView.address_partial_selector(:from, @address, true) |> BlockScoutWeb.RenderHelper.render_partial() %> + + → + + <%= @token_transfer |> BlockScoutWeb.AddressView.address_partial_selector(:to, @address, true) |> BlockScoutWeb.RenderHelper.render_partial() %> + + + + <%= render BlockScoutWeb.TransactionView, "_total_transfers.html", Map.put(assigns, :transfer, @token_transfer) %> + +
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction/_total_transfers.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction/_total_transfers.html.eex new file mode 100644 index 0000000..e293390 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction/_total_transfers.html.eex @@ -0,0 +1,23 @@ +<%= case token_transfer_amount(@transfer) do %> + <% {:ok, :erc721_instance} -> %> + <%= render BlockScoutWeb.TransactionView, "_transfer_token_with_id.html", transfer: @transfer, token_id: List.first(@transfer.token_ids) %> + <% {:ok, :erc1155_erc404_instance, value} -> %> + <% transfer_type = Chain.get_token_transfer_type(@transfer) %> + <%= if transfer_type == :token_spawning do %> + <%= render BlockScoutWeb.TransactionView, "_transfer_token_with_id.html", transfer: @transfer, token_id: List.first(@transfer.token_ids) %> + <% else %> + <%= "#{value} " %> + <%= render BlockScoutWeb.TransactionView, "_transfer_token_with_id.html", transfer: @transfer, token_id: List.first(@transfer.token_ids) %> + <% end %> + <% {:ok, :erc1155_erc404_instance, values, token_ids, _decimals} -> %> + <% values_ids = Enum.zip(values, token_ids) %> + <%= for {value, token_id} <- values_ids do %> +
    + <%= "#{value} "%> + <%= render BlockScoutWeb.TransactionView, "_transfer_token_with_id.html", transfer: @transfer, token_id: token_id %> +
    + <% end %> + <% {:ok, value} -> %> + <%= value %> + <%= " " %><%= render BlockScoutWeb.TransactionView, "_link_to_token_symbol.html", transfer: @transfer %> +<% end %> diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction/_total_transfers_from_to.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction/_total_transfers_from_to.html.eex new file mode 100644 index 0000000..db68b6f --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction/_total_transfers_from_to.html.eex @@ -0,0 +1,48 @@ +<%= with {:ok, from_address} <- Chain.hash_to_address(@transfer.from_address_hash), +{:ok, to_address} <- Chain.hash_to_address(@transfer.to_address_hash) do %> +<% from_tags = BlockScoutWeb.Models.GetAddressTags.get_address_tags(@transfer.from_address_hash, @current_user) %> +<% to_tags = BlockScoutWeb.Models.GetAddressTags.get_address_tags(@transfer.to_address_hash, @current_user) %> + + + From + + + <%= render BlockScoutWeb.AddressView, "_link.html", address: from_address, contract: Address.smart_contract?(from_address), use_custom_tooltip: false, trimmed: false %> + <%= render BlockScoutWeb.AddressView, "_labels.html", tags: from_tags %> + + + <%= render BlockScoutWeb.CommonComponentsView, "_btn_copy_for_table.html", +additional_classes: ["btn-copy-icon-small", "btn-copy-icon-custom", "btn-copy-icon-no-borders", "btn-copy-token-transfer"], +clipboard_text: from_address, +aria_label: gettext("Copy From Address"), +title: gettext("Copy From Address"), +style: "position: relative;" %> + + + + + To + + + <%= render BlockScoutWeb.AddressView, "_link.html", address: to_address, contract: Address.smart_contract?(to_address), use_custom_tooltip: false, trimmed: false %> + <%= render BlockScoutWeb.AddressView, "_labels.html", tags: to_tags %> + + + <%= render BlockScoutWeb.CommonComponentsView, "_btn_copy_for_table.html", +additional_classes: ["btn-copy-icon-small", "btn-copy-icon-custom", "btn-copy-icon-no-borders", "btn-copy-token-transfer"], +clipboard_text: to_address, +aria_label: gettext("Copy To Address"), +title: gettext("Copy To Address"), +style: "position: relative;"%> + + + + + For + +<% end %> + + <%= render BlockScoutWeb.TransactionView, "_total_transfers.html", transfer: @transfer %> + + + diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction/_transfer_token_with_id.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction/_transfer_token_with_id.html.eex new file mode 100644 index 0000000..44a3444 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction/_transfer_token_with_id.html.eex @@ -0,0 +1,2 @@ +<%= "TokenID " %><%= render BlockScoutWeb.TransactionView, "_link_to_token_instance.html", transfer: @transfer, token_id: @token_id %> +<%= " " %><%= render BlockScoutWeb.TransactionView, "_link_to_token_symbol.html", transfer: @transfer %> \ No newline at end of file diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction/index.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction/index.html.eex new file mode 100644 index 0000000..dad9e73 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction/index.html.eex @@ -0,0 +1,45 @@ + +
    + <%= render BlockScoutWeb.Advertisement.TextAdView, "index.html", conn: @conn %> +
    +
    +

    <%= gettext "Validated Transactions" %>

    +
    + <%= render BlockScoutWeb.CommonComponentsView, "_rap_pagination_container.html", position: "top", showing_limit: if(Chain.transactions_available_count() == Chain.limit_showing_transactions(), do: Chain.limit_showing_transactions(), else: nil) %> +
    + + + <%= render BlockScoutWeb.CommonComponentsView, "_channel_disconnected_message.html", text: gettext("Connection Lost, click to load newer transactions") %> + + + +
    +
    + + <%= gettext "There are no transactions." %> + +
    +
    + +
    + <%= render BlockScoutWeb.CommonComponentsView, "_tile-loader.html" %> +
    + + <%= render BlockScoutWeb.CommonComponentsView, "_rap_pagination_container.html", position: "bottom" %> + +
    + + +
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction/not_found.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction/not_found.html.eex new file mode 100644 index 0000000..4402062 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction/not_found.html.eex @@ -0,0 +1,16 @@ +
    +
    +
    + Transaction not found +
    +

    <%= gettext("Sorry, we are unable to locate this transaction hash") %>

    +
    +

    <%= gettext("1. If you have just submitted this transaction please wait for at least 30 seconds before refreshing this page.") %>

    +

    <%= gettext("2. It could still be in the TX Pool of a different node, waiting to be broadcasted.") %>

    +

    <%= gettext("3. During times when the network is busy (i.e during ICOs) it can take a while for your transaction to propagate through the network and for us to index it.") %>

    +

    <%= gettext("4. If it still does not show up after 1 hour, please check with your sender/exchange/wallet/transaction provider for additional information.") %>

    +
    + <%= gettext("Back to home") %> +
    + +
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction/overview.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction/overview.html.eex new file mode 100644 index 0000000..2caf404 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction/overview.html.eex @@ -0,0 +1,611 @@ +<% block = @transaction.block %> +<% from_address_hash = @transaction.from_address_hash %> +<% from_address = @transaction.from_address %> +<% to_address_hash = @transaction.to_address_hash %> +<% created_address_hash = @transaction.created_contract_address_hash %> +<% type = if @transaction.type == 2, do: "2 (EIP-1559)", else: @transaction.type %> +<% base_fee_per_gas = if block, do: block.base_fee_per_gas, else: nil %> +<% max_priority_fee_per_gas = @transaction.max_priority_fee_per_gas %> +<% max_fee_per_gas = @transaction.max_fee_per_gas %> +<% burnt_fees = + if !is_nil(max_fee_per_gas) and !is_nil(@transaction.gas_used) and !is_nil(base_fee_per_gas) do + if Decimal.compare(max_fee_per_gas.value, 0) == :eq do + %Wei{value: Decimal.new(0)} + else + Wei.mult(base_fee_per_gas, @transaction.gas_used) + end + else + nil + end %> +<% %Wei{value: burnt_fee_decimal} = if is_nil(burnt_fees), do: %Wei{value: Decimal.new(0)}, else: burnt_fees %> +<% priority_fee_per_gas = if is_nil(max_priority_fee_per_gas) or is_nil(base_fee_per_gas), do: nil, else: Enum.min_by([max_priority_fee_per_gas, Wei.sub(max_fee_per_gas, base_fee_per_gas)], fn x -> Wei.to(x, :wei) end) %> +<% priority_fee = if is_nil(priority_fee_per_gas), do: nil, else: Wei.mult(priority_fee_per_gas, @transaction.gas_used) %> +<% decoded_input_data = decoded_input_data(@transaction) %> +<% status = transaction_status(@transaction) %> +<% circles_addresses_list = CustomContractsHelper.get_custom_addresses_list(:circles_addresses) %> +<% address_hash_string = if to_address_hash, do: "0x" <> Base.encode16(to_address_hash.bytes, case: :lower), else: nil %> +<% {:ok, created_from_address} = if to_address_hash, do: Chain.hash_to_address(to_address_hash), else: {:ok, nil} %> +<% created_from_address_hash_str = if from_address_hash(created_from_address), do: "0x" <> Base.encode16(from_address_hash(created_from_address).bytes, case: :lower), else: nil %> +<%= render BlockScoutWeb.Advertisement.TextAdView, "index.html", conn: @conn %> +
    +
    +
    + +
    +
    + <%= cond do %> + <% Enum.member?(circles_addresses_list, address_hash_string) -> %> +
    + +
    + <% Enum.member?(circles_addresses_list, created_from_address_hash_str) -> %> +
    + +
    + <% true -> %> + <%= nil %> + <% end %> +

    +
    + <%= gettext "Transaction Details" %> + <% personal_transaction_tag = if assigns[:transaction_tags], do: @transaction_tags.personal_transaction_tag, else: nil %> + <%= if personal_transaction_tag && personal_transaction_tag.name !== :error do %> + <%= render BlockScoutWeb.FormView, "_tag.html", text: personal_transaction_tag.name, additional_classes: [tag_name_to_label(personal_transaction_tag.name), "ml-1"] %> + <% end %> + <%= render BlockScoutWeb.AddressView, "_labels.html", tags: @transaction_tags %> +
    +

    + <%= if status == :pending do %> +
    +
    + + +
    + <%= gettext("This transaction is pending confirmation.") %> +
    + <% end %> +
    + <%= if show_tenderly_link?() do %> +
    + <%= render BlockScoutWeb.CommonComponentsView, "_tenderly_link.html", + transaction_hash: @transaction.hash, + tenderly_chain_path: tenderly_chain_path() %> +
    + <% end %> +
    +
    +
    + <%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip_2.html", + text: gettext("Unique character string (TxID) assigned to every verified transaction.") %> + <%= gettext "Transaction Hash" %> +
    +
    + <%= @transaction %> + <%= render BlockScoutWeb.CommonComponentsView, "_btn_copy.html", + additional_classes: ["btn-copy-icon-small", "btn-copy-icon-custom", "btn-copy-icon-no-borders"], + clipboard_text: @transaction, + aria_label: gettext("Copy Transaction Hash"), + title: gettext("Copy Txn Hash") %> +
    +
    + + + +
    +
    + <%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip_2.html", + text: gettext("Current transaction state: Success, Failed (Error), or Pending (In Process)") %> + <%= gettext "Result" %> +
    +
    + <% formatted_result = BlockScoutWeb.TransactionView.formatted_result(status) %> + <%= render BlockScoutWeb.CommonComponentsView, "_status_icon.html", status: status %><%= formatted_result %> +
    +
    + +
    +
    + <%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip_2.html", + text: gettext("The status of the transaction: Confirmed or Unconfirmed.") %> + <%= gettext "Status" %> +
    +
    + <% formatted_status = BlockScoutWeb.TransactionView.formatted_status(status) %> + <% confirmations = confirmations(@transaction, block_height: @block_height) %> + + + <%= cond do %> + <% status == :pending -> %> + <%= render BlockScoutWeb.FormView, "_tag.html", text: formatted_status, additional_classes: ["large"] %> + <% @transaction.error == "dropped/replaced" -> %> + <%= render BlockScoutWeb.FormView, "_tag.html", text: @transaction.error, additional_classes: ["large"] %> + <% true -> %> + <%= render BlockScoutWeb.FormView, "_tag.html", text: formatted_status, additional_classes: ["success", "large"] %> + <% end %> + + <%= if confirmations > 0 do %> + <%= gettext "Confirmed by " %><%= confirmations %><%= " " <> confirmations_ds_name(confirmations) %> + <% end %> + +
    +
    + + <%= if status == {:error, "Reverted"} || status == {:error, "execution reverted"} do %> +
    +
    + <%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip_2.html", + text: gettext("The revert reason of the transaction.") %> + <%= gettext "Revert reason" %>
    +
    + <%= case BlockScoutWeb.TransactionView.transaction_revert_reason(@transaction) do %> + <% {:error, _contract_not_verified, candidates} when candidates != [] -> %> + <% {:ok, method_id, text, mapping} = Enum.at(candidates, 0) %> + <%= render(BlockScoutWeb.TransactionView, "_decoded_input_body.html", method_id: method_id, text: text, mapping: mapping, error: true) %> + <% {:ok, method_id, text, mapping} -> %> + <%= render(BlockScoutWeb.TransactionView, "_decoded_input_body.html", method_id: method_id, text: text, mapping: mapping, error: true) %> + <% _ -> %> + <% hex = BlockScoutWeb.TransactionView.get_pure_transaction_revert_reason(@transaction) %> + <% utf8 = BlockScoutWeb.TransactionView.decode_revert_reason_as_utf8(hex) %> +
    +
    Raw:<%= raw("\t") %><%= hex %><%= raw("\n") %>UTF-8:<%= raw("\t") %><%= utf8 %>
    +
    + <% end %> +
    +
    + <% end %> + +
    +
    + <%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip_2.html", + text: gettext("Block number containing the transaction.") %> + <%= gettext "Block" %>
    +
    + <%= if block do %> + <%= link( + block, + class: "transaction__link", + to: block_path(@conn, :show, block) + ) %> + <% else %> + <%= formatted_result(status) %> + <% end %> +
    +
    + + <%= if block && block.timestamp do %> +
    +
    + <%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip_2.html", + text: gettext("Date & time of transaction inclusion, including length of time for confirmation.") %> + <%= gettext "Timestamp" %> +
    +
    + + + + + <%= case processing_time_duration(@transaction) do %> + <% :pending -> %> + <% nil %> + <% :unknown -> %> + <% nil %> + <% {:ok, interval_string} -> %> + | <%= gettext("Confirmed within") %> <%= interval_string %> + <% end %> +
    +
    + <% end %> + <%= if Application.get_env(:explorer, :chain_type) == :optimism && @transaction.l1_block_number do %> + +
    +
    + <%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip_2.html", + text: gettext("Block number containing the transaction on L1.") %> + <%= gettext "L1 Block" %>
    +
    + <%= if block do %> + <%= link( + @transaction.l1_block_number, + class: "transaction__link", + to: "https://eth-goerli.blockscout.com/block/#{@transaction.l1_block_number}" + ) %> + <% else %> + <%= formatted_result(status) %> + <% end %> +
    +
    + <% end %> + + <% %{transaction_actions: transaction_actions} = transaction_actions(@transaction) %> + <%= unless Enum.empty?(transaction_actions) do %> +
    +
    + <%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip_2.html", + text: gettext("Highlighted events of the transaction.") %> + <%= gettext "Transaction Action" %> +

    <%= gettext "Scroll to see more" %>

    +
    +
    +
    + <% transaction_actions_indexed = Enum.with_index(transaction_actions) %> + <% transaction_actions_length = Enum.count(transaction_actions) %> + <%= for {action, i} <- transaction_actions_indexed do %> + <% action_assigns = Map.put(assigns, :action, action) %> + <% action_assigns = Map.put(action_assigns, :isLast, (i == transaction_actions_length - 1)) %> + <%= render BlockScoutWeb.TransactionView, "_actions.html", action_assigns %> + <% end %> +
    +
    +
    + <% end %> + +
    +
    + <%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip_2.html", + text: gettext("Address (external or contract) sending the transaction.") %> + <%= gettext "From" %>
    +
    + <%= render BlockScoutWeb.AddressView, "_link.html", address: from_address, contract: Address.smart_contract?(from_address), use_custom_tooltip: false, trimmed: false %> + <%= render BlockScoutWeb.AddressView, "_labels.html", tags: @from_tags %> + <%= render BlockScoutWeb.CommonComponentsView, "_btn_copy.html", + additional_classes: ["btn-copy-icon-small", "btn-copy-icon-custom", "btn-copy-icon-no-borders"], + clipboard_text: Address.checksum(from_address_hash), + aria_label: gettext("Copy From Address"), + title: gettext("Copy From Address") %> +
    +
    + + <% to_address = @transaction |> Map.get(:to_address) || @transaction |> Map.get(:created_contract_address) %> + <% recipient_address_hash = to_address_hash || created_address_hash %> +
    +
    + <%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip_2.html", + text: gettext("Address (external or contract) receiving the transaction.") %> + <%= if Address.smart_contract?(to_address) && !created_address_hash do %> + <%= gettext "Interacted With (To)" %> + <% else %> + <%= gettext "To" %> + <% end %> +
    +
    + <%= cond do %> + <% created_address_hash -> %> + [<%= gettext("Contract") %>  + <%= render BlockScoutWeb.AddressView, "_link.html", address: to_address, contract: Address.smart_contract?(to_address), use_custom_tooltip: false, trimmed: false %> + <%= render BlockScoutWeb.AddressView, "_labels.html", tags: @to_tags %> +  <%= gettext("created") %>] + <%= render BlockScoutWeb.CommonComponentsView, "_btn_copy.html", + additional_classes: ["btn-copy-icon-small", "btn-copy-icon-custom", "btn-copy-icon-no-borders"], + clipboard_text: Address.checksum(recipient_address_hash), + aria_label: gettext("Copy To Address"), + title: gettext("Copy To Address") %> + <% recipient_address_hash -> %> + <%= render BlockScoutWeb.AddressView, "_link.html", address: to_address, contract: Address.smart_contract?(to_address), use_custom_tooltip: false, trimmed: false %> + <%= render BlockScoutWeb.AddressView, "_labels.html", tags: @to_tags %> + <%= render BlockScoutWeb.CommonComponentsView, "_btn_copy.html", + additional_classes: ["btn-copy-icon-small", "btn-copy-icon-custom", "btn-copy-icon-no-borders"], + clipboard_text: Address.checksum(recipient_address_hash), + aria_label: gettext("Copy To Address"), + title: gettext("Copy To Address") %> + <% true -> %> + <% end %> +
    +
    + <%= case token_transfer_type(@transaction) do %> + <% {_type, %{token_transfers: token_transfers} = transaction_with_transfers} when is_list(token_transfers) and token_transfers != [] -> %> + + <% %{transfers: transfers, mintings: mintings, burnings: burnings, creations: creations} = aggregate_token_transfers(transaction_with_transfers.token_transfers) %> + <%= unless Enum.empty?(transfers) do %> +
    +
    + <%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip_2.html", + text: gettext("List of token transferred in the transaction.") %> + <%= gettext "Tokens Transferred" %>
    +
    + + <%= for transfer <- transfers do %> + <%= render BlockScoutWeb.TransactionView, "_total_transfers_from_to.html", Map.put(assigns, :transfer, transfer) %> + <% end %> +
    +
    +
    + <% end %> + + <%= unless Enum.empty?(mintings) do %> +
    +
    + <%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip_2.html", + text: gettext("List of token minted in the transaction.") %> + <%= gettext "Tokens Minted" %> +
    +
    + + <%= for minting <- mintings do %> + <%= render BlockScoutWeb.TransactionView, "_total_transfers_from_to.html", Map.put(assigns, :transfer, minting) %> + <% end %> +
    +
    +
    + <% end %> + + <%= unless Enum.empty?(burnings) do %> +
    +
    + <%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip_2.html", + text: gettext("List of token burnt in the transaction.") %> + <%= gettext "Tokens Burnt" %>
    +
    + <%= for burning <- burnings do %> + + <%= render BlockScoutWeb.TransactionView, "_total_transfers_from_to.html", Map.put(assigns, :transfer, burning) %> +
    + <% end %> +
    +
    + <% end %> + + <%= unless Enum.empty?(creations) do %> +
    +
    + <%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip_2.html", + text: gettext("List of ERC-1155 tokens created in the transaction.") %> + <%= gettext "Tokens Created" %>
    +
    + <%= for creation <- creations do %> + + <%= render BlockScoutWeb.TransactionView, "_total_transfers_from_to.html", Map.put(assigns, :transfer, creation) %> +
    + <% end %> +
    +
    + <% end %> + <% _ -> %> + <% end %> + +
    +
    + <%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip_2.html", + text: gettext("Value sent in the native token (and USD) if applicable.") %> + <%= gettext "Value" %> +
    +
    <%= value(@transaction) %> + <%= if !empty_exchange_rate?(@exchange_rate) do %> + ( + data-usd-exchange-rate=<%= @exchange_rate.fiat_value %>> + ) + <% end %> +
    +
    + +
    +
    + <%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip_2.html", + text: gettext("Total transaction fee.") %> + <%= gettext "Transaction Fee" %> +
    +
    + <%= formatted_fee(@transaction, denomination: :ether) %> + + <%= if !empty_exchange_rate?(@exchange_rate) do %> + ( data-usd-exchange-rate=<%= @exchange_rate.fiat_value %>>) + <% end %> +
    +
    + +
    +
    + <%= if Application.get_env(:explorer, :chain_type) == :optimism do %> + <%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip_2.html", + text: gettext("Price per unit of gas specified by the sender on L2. Higher gas prices can prioritize transaction inclusion during times of high usage.") %> + <%= gettext "L2 Gas Price" %> + <% else %> + <%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip_2.html", + text: gettext("Price per unit of gas specified by the sender. Higher gas prices can prioritize transaction inclusion during times of high usage.") %> + <%= gettext "Gas Price" %> + <% end %> +
    +
    <%= gas_price(@transaction, :gwei) %>
    +
    + <%= if !is_nil(type) do %> +
    +
    + <%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip_2.html", + text: gettext("Transaction type, introduced in EIP-2718.") %> + <%= gettext "Transaction Type" %> +
    +
    <%= type %>
    +
    + <% end %> +
    + +
    +
    + <%= if Application.get_env(:explorer, :chain_type) == :optimism do %> + <%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip_2.html", + text: gettext("Maximum gas amount approved for the transaction on L2.") %> + <%= gettext "L2 Gas Limit" %> + <% else %> + <%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip_2.html", + text: gettext("Maximum gas amount approved for the transaction.") %> + <%= gettext "Gas Limit" %> + <% end %> +
    +
    <%= format_gas_limit(@transaction.gas) %>
    +
    + <%= if !is_nil(max_fee_per_gas) do %> +
    +
    + <%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip_2.html", + text: gettext("Maximum total amount per unit of gas a user is willing to pay for a transaction, including base fee and priority fee.") %> + <%= gettext "Max Fee per Gas" %> +
    +
    <%= format_wei_value(max_fee_per_gas, :gwei) %>
    +
    + <% end %> + <%= if !is_nil(max_priority_fee_per_gas) do %> +
    +
    + <%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip_2.html", + text: gettext("User defined maximum fee (tip) per unit of gas paid to validator for transaction prioritization.") %> + <%= gettext "Max Priority Fee per Gas" %> +
    +
    <%= format_wei_value(max_priority_fee_per_gas, :gwei) %>
    +
    + <% end %> + <%= if !is_nil(priority_fee) do %> +
    +
    + <%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip_2.html", + text: gettext("User-defined tip sent to validator for transaction priority/inclusion.") %> + <%= gettext "Priority Fee / Tip" %> +
    +
    <%= format_wei_value(priority_fee, :ether) %>
    +
    + <% end %> + <%= if !is_nil(burnt_fees) do %> +
    +
    + <%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip_2.html", + text: gettext("Amount of") <> " " <> Explorer.coin_name() <> " " <> gettext("burnt for this transaction. Equals Block Base Fee per Gas * Gas Used.") %> + <%= gettext "Transaction Burnt Fee" %> +
    +
    <%= format_wei_value(burnt_fees, :ether) %> + <%= unless empty_exchange_rate?(@exchange_rate) do %> + ( data-usd-exchange-rate=<%= @exchange_rate.fiat_value %>>) + <% end %> +
    +
    + <% end %> + +
    +
    + <%= if Application.get_env(:explorer, :chain_type) == :optimism do %> + <%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip_2.html", + text: gettext("Actual gas amount used by the transaction on L2.") %> + <%= gettext "L2 Gas Used by Transaction" %> + <% else %> + <%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip_2.html", + text: gettext("Actual gas amount used by the transaction.") %> + <%= gettext "Gas Used by Transaction" %> + <% end %> +
    + <% gas_used_perc = gas_used_perc(@transaction) %> +
    <%= gas_used(@transaction) %> <%= if gas_used_perc, do: "| #{gas_used_perc}%" %>
    +
    + <%= if Application.get_env(:explorer, :chain_type) == :optimism do %> + <%= if @transaction.l1_gas_used do %> + +
    +
    + <%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip_2.html", + text: gettext("L1 Gas Used by Transaction") %> + <%= gettext "L1 Gas Used by Transaction" %> +
    +
    <%= l1_gas_used(@transaction) %>
    +
    + <% end %> + <%= if @transaction.l1_gas_used do %> + +
    +
    + <%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip_2.html", + text: gettext("L1 Gas Price") %> + <%= gettext "L1 Gas Price" %> +
    +
    <%= l1_gas_price(@transaction, :gwei) %>
    +
    + <% end %> + <%= if @transaction.l1_fee_scalar do %> + +
    +
    + <%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip_2.html", + text: gettext("L1 Fee Scalar") %> + <%= gettext "L1 Fee Scalar" %> +
    +
    <%= @transaction.l1_fee_scalar %>
    +
    + <% end %> + <% end %> + +
    +
    + <%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip_2.html", + text: gettext("Transaction number from the sending address. Each transaction sent from an address increments the nonce by 1.") %> + <%= gettext "Nonce" %>"><%= gettext "Position" %> +
    +
    <%= @transaction.nonce %><%= if block, do: @transaction.index, else: formatted_result(status) %>
    +
    + <%= unless value_transfer?(@transaction) do %> +
    +
    + <%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip_2.html", + text: gettext("Binary data included with the transaction. See input / logs below for additional info.") %> + <%= gettext "Raw Input" %> +
    +
    +
    + + +
    + + <%= render BlockScoutWeb.CommonComponentsView, "_btn_copy.html", + id: "tx-raw-input", + additional_classes: ["btn-copy-icon-small", "btn-copy-icon-no-borders", "btn-copy-icon-ml-0", "btn-copy-tx-raw-input", "tx-raw-input"], + clipboard_text: @transaction.input, + aria_label: gettext("Copy Value"), + title: gettext("Copy Txn Hex Input") %> + + <%= render BlockScoutWeb.CommonComponentsView, "_btn_copy.html", + additional_classes: ["btn-copy-icon-small", "btn-copy-icon-no-borders", "btn-copy-icon-ml-0", "btn-copy-tx-raw-input", "tx-utf8-input"], + clipboard_text: @transaction.input.bytes, + aria_label: gettext("Copy Value"), + title: gettext("Copy Txn UTF-8 Input"), + style: "display: none;" %> +
    +
    + +
    +
    +
    <%= @transaction.input %>
    +
    +
    + + +
    +
    + <% end %> +
    +
    +
    +
    + + <%= render BlockScoutWeb.Advertisement.BannersAdView, "_banner_728.html", conn: @conn %> + + <%= unless skip_decoding?(@transaction) do %> +
    +
    + <%= render BlockScoutWeb.TransactionView, "_decoded_input.html", Map.put(assigns, :decoded_input_data, decoded_input_data) %> +
    +
    + <% end %> + +
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction/show_internal_transactions.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction/show_internal_transactions.html.eex new file mode 100644 index 0000000..cbfb925 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction/show_internal_transactions.html.eex @@ -0,0 +1 @@ +<%= render BlockScoutWeb.TransactionInternalTransactionView, "index.html", assigns %> \ No newline at end of file diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction/show_token_transfers.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction/show_token_transfers.html.eex new file mode 100644 index 0000000..b524c8e --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction/show_token_transfers.html.eex @@ -0,0 +1 @@ +<%= render BlockScoutWeb.TransactionTokenTransferView, "index.html", assigns %> \ No newline at end of file diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction_internal_transaction/_metatags.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction_internal_transaction/_metatags.html.eex new file mode 100644 index 0000000..85c3d66 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction_internal_transaction/_metatags.html.eex @@ -0,0 +1 @@ +<%= render BlockScoutWeb.TransactionView, "_metatags.html", conn: @conn, transaction: @transaction %> diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex new file mode 100644 index 0000000..0194017 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex @@ -0,0 +1,29 @@ +
    + <%= render BlockScoutWeb.TransactionView, "overview.html", assigns %> +
    + <%= render BlockScoutWeb.TransactionView, "_tabs.html", assigns %> +
    +

    <%= gettext "Internal Transactions" %>

    + +
    + <%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "top", cur_page_number: "1", show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %> +
    + + +
    +
    + <%= gettext "There are no internal transactions for this transaction." %> +
    +
    + +
    + <%= render BlockScoutWeb.CommonComponentsView, "_tile-loader.html" %> +
    + + <%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "bottom", cur_page_number: "1", show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %> +
    +
    + +
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction_log/_logs.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction_log/_logs.html.eex new file mode 100644 index 0000000..fe36dc6 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction_log/_logs.html.eex @@ -0,0 +1,148 @@ +
    + <% decoded_result = decode(@log, @transaction) %> + <%= case decoded_result do %> + <% {:error, :contract_not_verified, _candidates} -> %> +
    + <%= gettext "To see accurate decoded input data, the contract must be verified." %> + <%= case @log do %> + <% %{address_hash: %Explorer.Chain.Hash{} = address_hash} -> %> + <% path = address_verify_contract_path(@conn, :new, address_hash) %> + <%= gettext "Verify the contract " %><%= gettext "here" %> + <% _ -> %> + <%= nil %> + <% end %> +
    + <% _ -> %> + <%= nil %> + <% end %> + + <% implementation_names = Implementation.names(@log.address) %> + <% implementation_name = + if Enum.empty?(implementation_names) do + nil + else + implementation_names |> Enum.at(0) + end + %> + +
    +
    <%= gettext "Address" %>
    +
    +

    + <% name = implementation_name || primary_name(@log.address)%> + <%= link( + (if name, do: name <> " | "<> to_string(@log.address), else: @log.address), + to: address_path(@conn, :show, @log.address), + "data-test": "log_address_link", + "data-address-hash": @log.address + ) %> +

    +
    + <%= case decoded_result do %> + <% {:error, :could_not_decode} -> %> +
    <%= gettext "Decoded" %>
    +
    +
    + <%= gettext "Failed to decode log data." %> +
    + <% {:error, :no_matching_function} -> %> + <%= nil %> + <% {:ok, method_id, text, mapping} -> %> +
    <%= gettext "Decoded" %>
    +
    + + + + + + + + + +
    Method Id0x<%= method_id %>
    Call<%= text %>
    + <%= render BlockScoutWeb.LogView, "_data_decoded_view.html", mapping: mapping %> + <% {:error, :contract_not_verified, results} -> %> + <%= for {:ok, method_id, text, mapping} <- results do %> +
    <%= gettext "Decoded" %>
    +
    + + + + + + + + + +
    Method Id0x<%= method_id %>
    Call<%= text %>
    + <%= render BlockScoutWeb.LogView, "_data_decoded_view.html", mapping: mapping %> + <% end %> + <% {:error, :contract_verified, results} -> %> + <%= for {:ok, method_id, text, mapping} <- results do %> +
    <%= gettext "Decoded" %>
    +
    + + + + + + + + + +
    Method Id0x<%= method_id %>
    Call<%= text %>
    + <%= render BlockScoutWeb.LogView, "_data_decoded_view.html", mapping: mapping %> + <% end %> + <% _ -> %> + <%= nil %> + <% end %> + +
    <%= gettext "Topics" %>
    +
    +
    + <%= unless is_nil(@log.first_topic) do %> +
    + [0] + <%= @log.first_topic %> +
    + <% end %> + <%= unless is_nil(@log.second_topic) do %> +
    + [1] + <%= @log.second_topic %> +
    + <% end %> + <%= unless is_nil(@log.third_topic) do %> +
    + [2] + <%= @log.third_topic %> +
    + <% end %> + <%= unless is_nil(@log.fourth_topic) do %> +
    + [3] + <%= @log.fourth_topic %> +
    + <% end %> +
    +
    +
    + <%= gettext "Data" %> +
    +
    + <%= unless is_nil(@log.data) do %> +
    + <%= @log.data %> +
    + <% end %> +
    +
    + <%= gettext "Log Index" %> +
    +
    +
    + <%= @log.index %> +
    +
    +
    +
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction_log/_metatags.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction_log/_metatags.html.eex new file mode 100644 index 0000000..85c3d66 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction_log/_metatags.html.eex @@ -0,0 +1 @@ +<%= render BlockScoutWeb.TransactionView, "_metatags.html", conn: @conn, transaction: @transaction %> diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction_log/index.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction_log/index.html.eex new file mode 100644 index 0000000..88db746 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction_log/index.html.eex @@ -0,0 +1,32 @@ +
    + <%= render BlockScoutWeb.TransactionView, "overview.html", assigns %> + +
    + <%= render BlockScoutWeb.TransactionView, "_tabs.html", assigns %> + +
    +

    <%= gettext "Logs" %>

    + +
    + <%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "top", show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %> +
    + + + +
    +
    + <%= gettext "There are no logs for this transaction." %> +
    +
    + +
    + <%= render BlockScoutWeb.CommonComponentsView, "_tile-loader.html" %> +
    + + <%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "bottom", cur_page_number: "1", show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %> + +
    +
    +
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction_raw_trace/_card_body.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction_raw_trace/_card_body.html.eex new file mode 100644 index 0000000..55cbe19 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction_raw_trace/_card_body.html.eex @@ -0,0 +1,19 @@ +

    <%= gettext "Raw Trace" %> + <%= unless Enum.empty?(@raw_traces) do %> + <% raw_trace_text = for {line, _} <- raw_traces_with_lines(@raw_traces), do: line %> + <%= render BlockScoutWeb.CommonComponentsView, "_btn_copy.html", + id: "tx-raw-trace-input", + additional_classes: ["tx-raw-input", "transaction-input"], + clipboard_text: raw_trace_text, + aria_label: gettext("Copy Value"), + title: gettext("Copy Raw Trace"), + style: "float: right;" %> + <% end %> +

    +<%= if Enum.empty?(@raw_traces) do %> +
    + <%= gettext "No trace entries found." %> +
    +<% else %> +
    <%= for {line, number} <- raw_traces_with_lines(@raw_traces) do %>
    <%= line %>
    <% end %>
    +<% end %> diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction_raw_trace/_metatags.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction_raw_trace/_metatags.html.eex new file mode 100644 index 0000000..85c3d66 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction_raw_trace/_metatags.html.eex @@ -0,0 +1 @@ +<%= render BlockScoutWeb.TransactionView, "_metatags.html", conn: @conn, transaction: @transaction %> diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction_raw_trace/index.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction_raw_trace/index.html.eex new file mode 100644 index 0000000..7da4ff5 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction_raw_trace/index.html.eex @@ -0,0 +1,10 @@ +
    + <%= render BlockScoutWeb.TransactionView, "overview.html", assigns %> + +
    + <%= render BlockScoutWeb.TransactionView, "_tabs.html", assigns %> +
    + <%= render "_card_body.html", assigns %> +
    +
    +
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction_state/_metatags.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction_state/_metatags.html.eex new file mode 100644 index 0000000..85c3d66 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction_state/_metatags.html.eex @@ -0,0 +1 @@ +<%= render BlockScoutWeb.TransactionView, "_metatags.html", conn: @conn, transaction: @transaction %> diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction_state/_state_change.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction_state/_state_change.html.eex new file mode 100644 index 0000000..bd4abe9 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction_state/_state_change.html.eex @@ -0,0 +1,64 @@ +<% coin_or_transfer = if @coin_or_token_transfers == :coin, do: :coin, else: elem(List.first(@coin_or_token_transfers), 1) %> + + <%= if @address.hash == @burn_address_hash do %> + +
    + <%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip_2.html", + text: gettext("Address used in token mintings and burnings.") %> + <%= gettext("Burn address") %> +
    + + + <%= render BlockScoutWeb.AddressView, "_link.html", address: @address, contract: Address.smart_contract?(@address), use_custom_tooltip: false %> + + + + <% else %> + <%= if Map.get(assigns, :miner) do %> + +
    + <%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip_2.html", + text: gettext("A block producer who successfully included the block onto the blockchain.") %> + <%= gettext("Miner") %> +
    + + + <%= render BlockScoutWeb.AddressView, "_link.html", address: @address, contract: false, use_custom_tooltip: false %> + + <% else %> + + + <%= render BlockScoutWeb.AddressView, "_link.html", address: @address, contract: Address.smart_contract?(@address), use_custom_tooltip: false %> + + <% end %> + <%= if not_negative?(@balance_before) and not_negative?(@balance_after) do %> + + <%= display_value(@balance_before, coin_or_transfer, @token_id) %> + + + <%= display_value(@balance_after, coin_or_transfer, @token_id) %> + + <% else %> + + + <% end %> + <% end %> + + <%= if is_list(@coin_or_token_transfers) and coin_or_transfer.token.type == "ERC-721" do %> + <%= for {type, transfer} <- @coin_or_token_transfers do %> + <%= case type do %> + <% :from -> %> +
    ▼ <%= display_erc_721(transfer) %>
    + <% :to -> %> +
    ▲ <%= display_erc_721(transfer) %>
    + <% end %> + <% end %> + <% else %> + <%= if not_negative?(@balance_diff) do %> + ▲ <%= display_value(@balance_diff, coin_or_transfer, @token_id) %> + <% else %> + ▼ <%= display_value(absolute_value_of(@balance_diff), coin_or_transfer, @token_id) %> + <% end %> + <% end %> + + diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction_state/_token_balance.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction_state/_token_balance.html.eex new file mode 100644 index 0000000..17458e2 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction_state/_token_balance.html.eex @@ -0,0 +1,5 @@ +<%= if @token_id do %> + <%= BlockScoutWeb.Cldr.Number.to_string!(@balance, format: "#,###") %> <%= render BlockScoutWeb.TransactionView, "_transfer_token_with_id.html", transfer: @transfer, token_id: @token_id %> +<% else %> + <%= format_according_to_decimals(@balance, @transfer.token.decimals) %><%= " " %><%= render BlockScoutWeb.TransactionView, "_link_to_token_symbol.html", transfer: @transfer %> +<% end %> diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction_state/index.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction_state/index.html.eex new file mode 100644 index 0000000..4e846ff --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction_state/index.html.eex @@ -0,0 +1,58 @@ +
    + <%= render BlockScoutWeb.TransactionView, "overview.html", assigns %> +
    + <%= render BlockScoutWeb.TransactionView, "_tabs.html", assigns %> +
    +

    <%= gettext "State changes" %>

    + +
    + <%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "top", show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %> +
    + + + <%= cond do %> + <% Chain.transaction_to_status(@transaction) == :pending -> %> +
    + <%= gettext "The changes from this transaction have not yet happened since the transaction is still pending." %> +
    + <% not has_state_changes?(@transaction) -> %> +
    + <%= gettext "This transaction hasn't changed state." %> +
    + <% true -> %> +
    +
    + + + + + + + + + + + + <%= render BlockScoutWeb.CommonComponentsView, "_table-loader.html", columns_num: 5 %> + +
    +
     
    +
    +
    <%= gettext "Address" %>
    +
    +
    <%= gettext "Balance before" %>
    +
    +
    <%= gettext "Balance after" %>
    +
    +
    <%= gettext "Change" %>
    +
    +
    +
    + <% end %> + + <%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "bottom", cur_page_number: "1", show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %> +
    +
    +
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction_token_transfer/_metatags.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction_token_transfer/_metatags.html.eex new file mode 100644 index 0000000..85c3d66 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction_token_transfer/_metatags.html.eex @@ -0,0 +1 @@ +<%= render BlockScoutWeb.TransactionView, "_metatags.html", conn: @conn, transaction: @transaction %> diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction_token_transfer/_token_transfer.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction_token_transfer/_token_transfer.html.eex new file mode 100644 index 0000000..8611cb5 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction_token_transfer/_token_transfer.html.eex @@ -0,0 +1,20 @@ +
    +
    +
    + <%= render(BlockScoutWeb.CommonComponentsView, "_token_transfer_type_display_name.html", type: Chain.get_token_transfer_type(@token_transfer)) %> +
    + +
    + <%= render BlockScoutWeb.TransactionView, "_link.html", transaction_hash: @token_transfer.transaction_hash %> + + <%= render BlockScoutWeb.AddressView, "_link.html", address: @token_transfer.from_address, contract: Address.smart_contract?(@token_transfer.from_address), use_custom_tooltip: false %> + → + <%= render BlockScoutWeb.AddressView, "_link.html", address: @token_transfer.to_address, contract: Address.smart_contract?(@token_transfer.to_address), use_custom_tooltip: false %> + + + + <%= render BlockScoutWeb.TransactionView, "_total_transfers.html", Map.put(assigns, :transfer, @token_transfer) %> + +
    +
    +
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction_token_transfer/index.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction_token_transfer/index.html.eex new file mode 100644 index 0000000..46f8214 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/transaction_token_transfer/index.html.eex @@ -0,0 +1,32 @@ +
    + <%= render BlockScoutWeb.TransactionView, "overview.html", assigns %> + +
    + <%= render BlockScoutWeb.TransactionView, "_tabs.html", assigns %> +
    +

    <%= gettext "Token Transfers" %>

    + +
    + <%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "top", show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %> +
    + + + +
    +
    + <%= gettext "There are no token transfers for this transaction" %> +
    +
    + +
    + <%= render BlockScoutWeb.CommonComponentsView, "_tile-loader.html" %> +
    + + <%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "bottom", cur_page_number: "1", show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %> + +
    +
    + +
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/verified_contracts/_contract.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/verified_contracts/_contract.html.eex new file mode 100644 index 0000000..37e0145 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/verified_contracts/_contract.html.eex @@ -0,0 +1,64 @@ + + + + <%= render BlockScoutWeb.AddressView, + "_link.html", + address: @contract.address, + contract: true, + use_custom_tooltip: false + %> + + + + <%= balance(@contract.address) %> + + + + + <%= if @contract.address.transactions_count do %> + <%= Number.Delimit.number_to_delimited(@contract.address.transactions_count, precision: 0) %> + <% else %> + <%= gettext "N/A" %> + <% end %> + + + + + + <%= if @contract.is_vyper_contract, do: gettext("Vyper"), else: gettext("Solidity") %> + + + + + <%= @contract.compiler_version %> + + + + + <%= if @contract.optimization do %> + + <% else %> + + <% end %> + + + + <%= if @contract.constructor_arguments do %> + + <% else %> + + <% end %> + + + + + + + + <%= if @token && @token.decimals && @token.total_supply && @token.fiat_value do %> + + <% else %> + <%= gettext "N/A" %> + <% end %> + + diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/verified_contracts/_metatags.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/verified_contracts/_metatags.html.eex new file mode 100644 index 0000000..440c9bf --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/verified_contracts/_metatags.html.eex @@ -0,0 +1,8 @@ + + <%= gettext( + "Verified contracts - %{subnetwork} Explorer", + subnetwork: BlockScoutWeb.LayoutView.subnetwork_title() + ) %> + +"> +"> diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/verified_contracts/_stats.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/verified_contracts/_stats.html.eex new file mode 100644 index 0000000..d69ebe0 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/verified_contracts/_stats.html.eex @@ -0,0 +1,33 @@ +
    +
    +
    +
    +

    <%= gettext "Contracts" %>

    +
    +
    +

    <%= BlockScoutWeb.Cldr.Number.to_string!(@contracts_count, format: "#,###") %>

    + <%= gettext ("Total") %> +
    +
    +

    <%= if 0 |> Decimal.new() |> Decimal.lt?(@new_contracts_count), do: "+" %><%= BlockScoutWeb.Cldr.Number.to_string!(@new_contracts_count, format: "#,###") %>

    + <%= gettext ("Last 24h") %> +
    +
    +
    +
    +

    <%= gettext "Verified Contracts" %>

    +
    +
    +

    <%= BlockScoutWeb.Cldr.Number.to_string!(@verified_contracts_count, format: "#,###") %>

    + <%= gettext ("Total") %> +
    +
    +

    <%= if 0 |> Decimal.new() |> Decimal.lt?(@new_verified_contracts_count), do: "+" %><%= BlockScoutWeb.Cldr.Number.to_string!(@new_verified_contracts_count, format: "#,###") %>

    + <%= gettext ("Last 24h") %> +
    +
    +
    + +
    +
    +
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/verified_contracts/index.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/verified_contracts/index.html.eex new file mode 100644 index 0000000..1149eea --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/verified_contracts/index.html.eex @@ -0,0 +1,107 @@ +<%= render "_stats.html", assigns %> +
    + <%= render BlockScoutWeb.Advertisement.TextAdView, "index.html", conn: @conn %> +
    +
    +

    <%= gettext "Verified Contracts" %>

    + +
    + <%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "top", cur_page_number: @page_number, show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %> +
    + + + +
    + " placeholder="<%= gettext "Contract name or address" %>" id="search-text-input"> +
    + + + +
    +
    + + + + + + + + + + + + + + + + <%= render BlockScoutWeb.CommonComponentsView, "_table-loader.html", columns_num: 9 %> + +
    +
    <%= gettext "Address" %>
    +
    +
    <%= gettext "Balance" %>
    +
    +
    <%= gettext "Txns" %>
    +
    +
    <%= gettext "Compiler" %>
    +
    +
    <%= gettext "Version" %>
    +
    +
    <%= gettext "Optimization" %>
    +
    +
    <%= gettext "Constructor args" %>
    +
    +
    <%= gettext "Verified" %>
    +
    +
    <%= gettext "Market cap" %>
    +
    +
    +
    + +
    +
    +
    + + <%= gettext "There are no verified contracts." %> + +
    +
    + + <%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "bottom", cur_page_number: @page_number, show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %> +
    + +
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/visualize_sol2uml/index.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/visualize_sol2uml/index.html.eex new file mode 100644 index 0000000..b78d3ef --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/visualize_sol2uml/index.html.eex @@ -0,0 +1,36 @@ +
    +
    +
    +
    +
    <%= gettext("UML diagram") %>
    +

    + <%= gettext("For contract") %> + <%= link to: address_contract_path(@conn, :index, @address), "data-test": "address_hash_link" do %> + <%= render( + BlockScoutWeb.AddressView, + "_responsive_hash.html", + address: @address, + contract: true, + use_custom_tooltip: false + ) %> + <% end %> +

    +
    +
    + + + + + +
    + + + +
    +
    +
    +
    + +
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/withdrawal/_metatags.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/withdrawal/_metatags.html.eex new file mode 100644 index 0000000..b179839 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/withdrawal/_metatags.html.eex @@ -0,0 +1,8 @@ + + <%= gettext( + "Beacon chain withdrawals - %{subnetwork} Explorer", + subnetwork: BlockScoutWeb.LayoutView.subnetwork_title() + ) %> + +"> +"> diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/withdrawal/_withdrawal.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/withdrawal/_withdrawal.html.eex new file mode 100644 index 0000000..cb3928b --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/withdrawal/_withdrawal.html.eex @@ -0,0 +1,34 @@ + + + + <%= @withdrawal.index %> + + + + <%= @withdrawal.validator_index %> + + + + <%= render BlockScoutWeb.BlockView, + "_number_link.html", + block: @withdrawal.block + %> + + + + <%= render BlockScoutWeb.AddressView, + "_link.html", + address: @withdrawal.address, + contract: Address.smart_contract?(@withdrawal.address), + use_custom_tooltip: false + %> + + + + + + + + <%= format_wei_value(@withdrawal.amount, :ether) %> + + diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/withdrawal/index.html.eex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/withdrawal/index.html.eex new file mode 100644 index 0000000..33e6809 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/templates/withdrawal/index.html.eex @@ -0,0 +1,61 @@ +
    + <%= render BlockScoutWeb.Advertisement.TextAdView, "index.html", conn: @conn %> +
    +
    +

    <%= gettext "Withdrawals" %>

    + +
    + <%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "top", cur_page_number: @page_number, show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %> +
    + +

    <%= gettext("%{withdrawals_count} withdrawals processed and %{withdrawals_sum} withdrawn.", withdrawals_count: BlockScoutWeb.Cldr.Number.to_string!(@withdrawals_count, format: "#,###"), withdrawals_sum: format_wei_value(@withdrawals_sum, :ether)) %>

    + + + +
    +
    + + + + + + + + + + + + + <%= render BlockScoutWeb.CommonComponentsView, "_table-loader.html", columns_num: 6 %> + +
    +
    <%= gettext "Index" %>
    +
    +
    <%= gettext "Validator index" %>
    +
    +
    <%= gettext "Block" %>
    +
    +
    <%= gettext "To" %>
    +
    +
    <%= gettext "Age" %>
    +
    +
    <%= gettext "Amount" %>
    +
    +
    +
    + +
    +
    +
    + + <%= gettext "There are no withdrawals." %> + +
    +
    + + <%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "bottom", cur_page_number: @page_number, show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %> +
    + +
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/tracer.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/tracer.ex new file mode 100644 index 0000000..6bca34d --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/tracer.ex @@ -0,0 +1,5 @@ +defmodule BlockScoutWeb.Tracer do + @moduledoc false + + use Spandex.Tracer, otp_app: :block_scout_web +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/utility/event_handlers_metrics.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/utility/event_handlers_metrics.ex new file mode 100644 index 0000000..ed2709b --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/utility/event_handlers_metrics.ex @@ -0,0 +1,45 @@ +defmodule BlockScoutWeb.Utility.EventHandlersMetrics do + @moduledoc """ + Module responsible for periodically setting current event handlers queue length metrics. + """ + + use GenServer + + alias BlockScoutWeb.{MainPageRealtimeEventHandler, RealtimeEventHandler, SmartContractRealtimeEventHandler} + alias BlockScoutWeb.Prometheus.Instrumenter + + @interval :timer.minutes(1) + + @spec start_link(term()) :: GenServer.on_start() + def start_link(_) do + GenServer.start_link(__MODULE__, :ok, name: __MODULE__) + end + + def init(_) do + schedule_next_run() + + {:ok, %{}} + end + + def handle_info(:set_metrics, state) do + set_metrics() + schedule_next_run() + + {:noreply, state} + end + + defp set_metrics do + set_handler_metric(MainPageRealtimeEventHandler, :main_page) + set_handler_metric(RealtimeEventHandler, :common) + set_handler_metric(SmartContractRealtimeEventHandler, :smart_contract) + end + + defp set_handler_metric(handler, label) do + {_, queue_length} = Process.info(Process.whereis(handler), :message_queue_len) + Instrumenter.event_handler_queue_length(label, queue_length) + end + + defp schedule_next_run do + Process.send_after(self(), :set_metrics, @interval) + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/abi_encoded_value_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/abi_encoded_value_view.ex new file mode 100644 index 0000000..d4d31fd --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/abi_encoded_value_view.ex @@ -0,0 +1,193 @@ +defmodule BlockScoutWeb.ABIEncodedValueView do + @moduledoc """ + Renders a decoded value that is encoded according to an ABI. + + Does not leverage an eex template because it renders formatted + values via `
    ` tags, and that is hard to do in an eex template.
    +  """
    +  use BlockScoutWeb, :view
    +
    +  import Phoenix.LiveView.Helpers, only: [sigil_H: 2]
    +
    +  alias ABI.FunctionSelector
    +  alias Explorer.Helper, as: ExplorerHelper
    +  alias Phoenix.HTML
    +  alias Phoenix.HTML.Safe
    +
    +  require Logger
    +
    +  def value_html(type, value, no_links \\ false)
    +
    +  def value_html(type, value, no_links) do
    +    decoded_type = FunctionSelector.decode_type(type)
    +
    +    do_value_html(decoded_type, value, no_links)
    +  rescue
    +    exception ->
    +      Logger.warning(fn ->
    +        ["Error determining value html for #{inspect(type)}: ", Exception.format(:error, exception, __STACKTRACE__)]
    +      end)
    +
    +      :error
    +  end
    +
    +  def copy_text(type, value) do
    +    decoded_type = FunctionSelector.decode_type(type)
    +
    +    do_copy_text(decoded_type, value)
    +  rescue
    +    exception ->
    +      Logger.warning(fn ->
    +        ["Error determining copy text for #{inspect(type)}: ", Exception.format(:error, exception, __STACKTRACE__)]
    +      end)
    +
    +      :error
    +  end
    +
    +  defp do_copy_text({:bytes, _type}, value) do
    +    ExplorerHelper.add_0x_prefix(value)
    +  end
    +
    +  defp do_copy_text({:array, type, _}, value) do
    +    do_copy_text({:array, type}, value)
    +  end
    +
    +  defp do_copy_text({:array, type}, value) do
    +    values =
    +      value
    +      |> Enum.map(&do_copy_text(type, &1))
    +      |> Enum.intersperse(", ")
    +
    +    assigns = %{values: values}
    +
    +    ~H|[<%= @values %>]|
    +    |> Safe.to_iodata()
    +    |> List.to_string()
    +  end
    +
    +  defp do_copy_text(_, {:dynamic, value}) do
    +    ExplorerHelper.add_0x_prefix(value)
    +  end
    +
    +  defp do_copy_text(type, value) when type in [:bytes, :address] do
    +    ExplorerHelper.add_0x_prefix(value)
    +  end
    +
    +  defp do_copy_text({:tuple, types}, value) do
    +    values =
    +      value
    +      |> Tuple.to_list()
    +      |> Enum.with_index()
    +      |> Enum.map(fn {val, ind} -> do_copy_text(Enum.at(types, ind), val) end)
    +      |> Enum.intersperse(", ")
    +
    +    assigns = %{values: values}
    +
    +    ~H|(<%= @values %>)|
    +    |> Safe.to_iodata()
    +    |> List.to_string()
    +  end
    +
    +  defp do_copy_text(_type, value) do
    +    to_string(value)
    +  end
    +
    +  defp do_value_html(type, value, no_links, depth \\ 0)
    +
    +  defp do_value_html({:bytes, _}, value, no_links, depth) do
    +    do_value_html(:bytes, value, no_links, depth)
    +  end
    +
    +  defp do_value_html({:array, type, _}, value, no_links, depth) do
    +    do_value_html({:array, type}, value, no_links, depth)
    +  end
    +
    +  defp do_value_html({:array, type}, value, no_links, depth) do
    +    values =
    +      Enum.map(value, fn inner_value ->
    +        do_value_html(type, inner_value, no_links, depth + 1)
    +      end)
    +
    +    spacing = String.duplicate(" ", depth * 2)
    +    delimited = Enum.intersperse(values, ",\n")
    +
    +    assigns = %{spacing: spacing, delimited: delimited}
    +
    +    elements =
    +      Enum.reduce(delimited, "", fn value, acc ->
    +        assigns = %{value: value}
    +
    +        html = ~H|<%= raw(@value) %>| |> Safe.to_iodata() |> List.to_string()
    +        acc <> html
    +      end)
    +
    +    (~H|<%= @spacing %>[<%= "\n" %>|
    +     |> Safe.to_iodata()
    +     |> List.to_string()) <>
    +      elements <>
    +      (~H|<%= "\n" %><%= @spacing %>]|
    +       |> Safe.to_iodata()
    +       |> List.to_string())
    +  end
    +
    +  defp do_value_html({:tuple, types}, values, no_links, _) do
    +    values_list =
    +      values
    +      |> Tuple.to_list()
    +      |> Enum.with_index()
    +      |> Enum.map(fn {value, i} ->
    +        do_value_html(Enum.at(types, i), value, no_links)
    +      end)
    +
    +    delimited = Enum.intersperse(values_list, ",")
    +    assigns = %{delimited: delimited}
    +
    +    ~H|(<%= for value <- @delimited, do: raw(value) %>)|
    +    |> Safe.to_iodata()
    +    |> List.to_string()
    +  end
    +
    +  defp do_value_html(type, value, no_links, depth) do
    +    spacing = String.duplicate(" ", depth * 2)
    +    html = base_value_html(type, value, no_links)
    +
    +    assigns = %{html: html, spacing: spacing}
    +
    +    ~H|<%= @spacing %><%= @html %>|
    +    |> Safe.to_iodata()
    +    |> List.to_string()
    +  end
    +
    +  defp base_value_html(_, {:dynamic, value}, _no_links) do
    +    assigns = %{value: value}
    +
    +    ~H|<%= ExplorerHelper.add_0x_prefix(@value) %>|
    +  end
    +
    +  defp base_value_html(:address, value, no_links) do
    +    if no_links do
    +      base_value_html(:address_text, value, no_links)
    +    else
    +      address = ExplorerHelper.add_0x_prefix(value)
    +      path = address_path(BlockScoutWeb.Endpoint, :show, address)
    +
    +      assigns = %{address: address, path: path}
    +
    +      ~H|<%= @address %>|
    +    end
    +  end
    +
    +  defp base_value_html(:address_text, value, _no_links) do
    +    assigns = %{value: value}
    +
    +    ~H|<%= ExplorerHelper.add_0x_prefix(@value) %>|
    +  end
    +
    +  defp base_value_html(:bytes, value, _no_links) do
    +    assigns = %{value: value}
    +
    +    ~H|<%= ExplorerHelper.add_0x_prefix(@value) %>|
    +  end
    +
    +  defp base_value_html(_, value, _no_links), do: HTML.html_escape(value)
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/access_helper.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/access_helper.ex
    new file mode 100644
    index 0000000..c8c2743
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/access_helper.ex
    @@ -0,0 +1,279 @@
    +defmodule BlockScoutWeb.AccessHelper do
    +  @moduledoc """
    +  Helper to restrict access to some pages filtering by address
    +  """
    +
    +  import Phoenix.Controller
    +
    +  alias BlockScoutWeb.API.APILogger
    +  alias BlockScoutWeb.API.RPC.RPCView
    +  alias BlockScoutWeb.API.V2.ApiView
    +  alias BlockScoutWeb.Routers.WebRouter.Helpers
    +  alias Explorer.{AccessHelper, Chain}
    +  alias Explorer.Account.Api.Key, as: ApiKey
    +  alias Plug.Conn
    +
    +  alias RemoteIp
    +
    +  require Logger
    +
    +  @invalid_address_hash "Invalid address hash"
    +  @restricted_access "Restricted access"
    +
    +  def restricted_access?(address_hash, params) do
    +    AccessHelper.restricted_access?(address_hash, params)
    +  end
    +
    +  @doc """
    +  Checks if the given address hash string is valid and not restricted.
    +
    +  ## Parameters
    +  - address_hash_string: A string representing the address hash to be validated.
    +
    +  ## Returns
    +  - :ok if the address hash is valid and access is not restricted.
    +  - binary with reason otherwise.
    +  """
    +  @spec valid_address_hash_and_not_restricted_access?(binary()) :: :ok | binary()
    +  def valid_address_hash_and_not_restricted_access?(address_hash_string) do
    +    with address_hash when not is_nil(address_hash) <- Chain.string_to_address_hash_or_nil(address_hash_string),
    +         {:ok, false} <- AccessHelper.restricted_access?(address_hash_string, %{}) do
    +      :ok
    +    else
    +      nil ->
    +        @invalid_address_hash
    +
    +      {:restricted_access, true} ->
    +        @restricted_access
    +    end
    +  end
    +
    +  def get_path(conn, path, template, address_hash) do
    +    basic_args = [conn, template, address_hash]
    +    key = get_restricted_key(conn)
    +    # credo:disable-for-next-line
    +    full_args = if key, do: basic_args ++ [%{:key => key}], else: basic_args
    +
    +    apply(Helpers, path, full_args)
    +  end
    +
    +  def get_path(conn, path, template, address_hash, additional_params) do
    +    basic_args = [conn, template, address_hash]
    +    key = get_restricted_key(conn)
    +    full_additional_params = if key, do: Map.put(additional_params, :key, key), else: additional_params
    +    # credo:disable-for-next-line
    +    full_args = basic_args ++ [full_additional_params]
    +
    +    apply(Helpers, path, full_args)
    +  end
    +
    +  def handle_rate_limit_deny(conn, api_v2? \\ false) do
    +    APILogger.message("API rate limit reached")
    +
    +    view = if api_v2?, do: ApiView, else: RPCView
    +    tag = if api_v2?, do: :message, else: :error
    +
    +    conn
    +    |> Conn.put_status(429)
    +    |> put_view(view)
    +    |> render(tag, %{tag => "429 Too Many Requests"})
    +    |> Conn.halt()
    +  end
    +
    +  @doc """
    +  Checks, if rate limit reached before making a new request. It is applied to GraphQL API.
    +  """
    +  @spec check_rate_limit(Plug.Conn.t(), list()) :: :ok | :rate_limit_reached | true | false
    +  def check_rate_limit(conn, graphql?: true) do
    +    rate_limit_config = Application.get_env(:block_scout_web, Api.GraphQL)
    +    no_rate_limit_api_key = rate_limit_config[:no_rate_limit_api_key]
    +
    +    cond do
    +      rate_limit_config[:rate_limit_disabled?] ->
    +        :ok
    +
    +      check_no_rate_limit_api_key(conn, no_rate_limit_api_key) ->
    +        :ok
    +
    +      true ->
    +        check_graphql_rate_limit_inner(conn, rate_limit_config)
    +    end
    +  end
    +
    +  defp check_graphql_rate_limit_inner(conn, rate_limit_config) do
    +    global_limit = rate_limit_config[:global_limit]
    +    limit_by_key = rate_limit_config[:limit_by_key]
    +    time_interval_limit = rate_limit_config[:time_interval_limit]
    +    static_api_key = rate_limit_config[:static_api_key]
    +    limit_by_ip = rate_limit_config[:limit_by_ip]
    +    time_interval_by_ip = rate_limit_config[:time_interval_limit_by_ip]
    +
    +    ip_string = conn_to_ip_string(conn)
    +    plan = get_plan(conn.query_params)
    +
    +    user_api_key = get_api_key(conn)
    +
    +    cond do
    +      check_api_key(conn) && user_api_key == static_api_key ->
    +        rate_limit(static_api_key, time_interval_limit, limit_by_key)
    +
    +      check_api_key(conn) && !is_nil(plan) ->
    +        rate_limit(user_api_key, time_interval_limit, min(plan.max_req_per_second, limit_by_key))
    +
    +      true ->
    +        rate_limit("graphql_#{ip_string}", limit_by_ip, time_interval_by_ip) == :ok &&
    +          rate_limit("graphql", time_interval_limit, global_limit) == :ok
    +    end
    +  end
    +
    +  @doc """
    +  Checks, if rate limit reached before making a new request. It is applied to API v1, ETH RPC API.
    +  """
    +  @spec check_rate_limit(Plug.Conn.t()) :: :ok | :rate_limit_reached
    +  def check_rate_limit(conn) do
    +    rate_limit_config = Application.get_env(:block_scout_web, :api_rate_limit)
    +    no_rate_limit_api_key = rate_limit_config[:no_rate_limit_api_key]
    +
    +    cond do
    +      rate_limit_config[:disabled] ->
    +        :ok
    +
    +      check_no_rate_limit_api_key(conn, no_rate_limit_api_key) ->
    +        :ok
    +
    +      true ->
    +        check_rate_limit_inner(conn, rate_limit_config)
    +    end
    +  end
    +
    +  defp check_no_rate_limit_api_key(conn, no_rate_limit_api_key) do
    +    user_api_key = get_api_key(conn)
    +
    +    check_api_key(conn) && !is_nil(user_api_key) && String.trim(user_api_key) !== "" &&
    +      user_api_key == no_rate_limit_api_key
    +  end
    +
    +  # credo:disable-for-next-line /Complexity/
    +  defp check_rate_limit_inner(conn, rate_limit_config) do
    +    global_limit = rate_limit_config[:global_limit]
    +    limit_by_key = rate_limit_config[:limit_by_key]
    +    limit_by_whitelisted_ip = rate_limit_config[:limit_by_whitelisted_ip]
    +    time_interval_limit = rate_limit_config[:time_interval_limit]
    +    static_api_key = rate_limit_config[:static_api_key]
    +    limit_by_ip = rate_limit_config[:limit_by_ip]
    +    api_v2_ui_limit = rate_limit_config[:api_v2_ui_limit]
    +    time_interval_by_ip = rate_limit_config[:time_interval_limit_by_ip]
    +
    +    ip_string = conn_to_ip_string(conn)
    +
    +    plan = get_plan(conn.query_params)
    +    token = get_ui_v2_token(conn, ip_string)
    +
    +    user_agent = get_user_agent(conn)
    +
    +    user_api_key = get_api_key(conn)
    +
    +    cond do
    +      check_api_key(conn) && user_api_key == static_api_key ->
    +        rate_limit(static_api_key, time_interval_limit, limit_by_key)
    +
    +      check_api_key(conn) && !is_nil(plan) ->
    +        rate_limit(user_api_key, time_interval_limit, plan.max_req_per_second)
    +
    +      Enum.member?(whitelisted_ips(rate_limit_config), ip_string) ->
    +        rate_limit(ip_string, time_interval_limit, limit_by_whitelisted_ip)
    +
    +      api_v2_request?(conn) && !is_nil(token) && !is_nil(user_agent) ->
    +        rate_limit(token, time_interval_limit, api_v2_ui_limit)
    +
    +      api_v2_request?(conn) && !is_nil(user_agent) ->
    +        rate_limit(ip_string, time_interval_by_ip, limit_by_ip)
    +
    +      true ->
    +        rate_limit("api", time_interval_limit, global_limit)
    +    end
    +  end
    +
    +  defp check_api_key(conn), do: Map.has_key?(conn.query_params, "apikey")
    +
    +  defp get_api_key(conn), do: Map.get(conn.query_params, "apikey")
    +
    +  defp get_plan(query_params) do
    +    with true <- query_params && Map.has_key?(query_params, "apikey"),
    +         api_key_value <- Map.get(query_params, "apikey"),
    +         api_key <- ApiKey.api_key_with_plan_by_value(api_key_value),
    +         false <- is_nil(api_key) do
    +      api_key.identity.plan
    +    else
    +      _ ->
    +        nil
    +    end
    +  end
    +
    +  @spec rate_limit(String.t(), integer(), integer()) :: :ok | :rate_limit_reached
    +  defp rate_limit(key, time_interval, limit) do
    +    case Hammer.check_rate(key, time_interval, limit) do
    +      {:allow, _count} ->
    +        :ok
    +
    +      {:deny, _limit} ->
    +        :rate_limit_reached
    +
    +      {:error, error} ->
    +        Logger.error(fn -> ["Rate limit check error: ", inspect(error)] end)
    +        :ok
    +    end
    +  end
    +
    +  defp get_restricted_key(%Phoenix.Socket{}), do: nil
    +
    +  defp get_restricted_key(conn) do
    +    conn_with_params = Conn.fetch_query_params(conn)
    +    conn_with_params.query_params["key"]
    +  end
    +
    +  defp whitelisted_ips(api_rate_limit_object) do
    +    case api_rate_limit_object && api_rate_limit_object |> Keyword.fetch(:whitelisted_ips) do
    +      {:ok, whitelisted_ips_string} ->
    +        if whitelisted_ips_string, do: String.split(whitelisted_ips_string, ","), else: []
    +
    +      _ ->
    +        []
    +    end
    +  end
    +
    +  defp api_v2_request?(%Plug.Conn{request_path: "/api/v2/" <> _}), do: true
    +  defp api_v2_request?(_), do: false
    +
    +  def conn_to_ip_string(conn) do
    +    is_blockscout_behind_proxy = Application.get_env(:block_scout_web, :api_rate_limit)[:is_blockscout_behind_proxy]
    +
    +    remote_ip_from_headers = is_blockscout_behind_proxy && RemoteIp.from(conn.req_headers)
    +    ip = remote_ip_from_headers || conn.remote_ip
    +
    +    to_string(:inet_parse.ntoa(ip))
    +  end
    +
    +  defp get_ui_v2_token(conn, ip_string) do
    +    api_v2_temp_token_key = Application.get_env(:block_scout_web, :api_v2_temp_token_key)
    +    conn = Conn.fetch_cookies(conn, signed: [api_v2_temp_token_key])
    +
    +    case api_v2_request?(conn) && conn.cookies[api_v2_temp_token_key] do
    +      %{ip: ^ip_string} ->
    +        conn.req_cookies[api_v2_temp_token_key]
    +
    +      _ ->
    +        nil
    +    end
    +  end
    +
    +  defp get_user_agent(conn) do
    +    case Conn.get_req_header(conn, "user-agent") do
    +      [agent] ->
    +        agent
    +
    +      _ ->
    +        nil
    +    end
    +  end
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/account/api/v2/account_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/account/api/v2/account_view.ex
    new file mode 100644
    index 0000000..3a41871
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/account/api/v2/account_view.ex
    @@ -0,0 +1,7 @@
    +defmodule BlockScoutWeb.Account.API.V2.AccountView do
    +  def render("message.json", %{message: message}) do
    +    %{
    +      "message" => message
    +    }
    +  end
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/account/api/v2/tags_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/account/api/v2/tags_view.ex
    new file mode 100644
    index 0000000..50403cf
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/account/api/v2/tags_view.ex
    @@ -0,0 +1,27 @@
    +defmodule BlockScoutWeb.Account.API.V2.TagsView do
    +  def render("address_tags.json", %{tags_map: tags_map}) do
    +    tags_map
    +  end
    +
    +  def render("transaction_tags.json", %{
    +        tags_map: %{
    +          personal_tags: personal_tags,
    +          watchlist_names: watchlist_names,
    +          personal_transaction_tag: personal_transaction_tag,
    +          common_tags: common_tags
    +        }
    +      }) do
    +    %{
    +      personal_transaction_tag: prepare_transaction_tag(personal_transaction_tag),
    +      personal_tags: personal_tags,
    +      watchlist_names: watchlist_names,
    +      common_tags: common_tags
    +    }
    +  end
    +
    +  def prepare_transaction_tag(nil), do: nil
    +
    +  def prepare_transaction_tag(transaction_tag) do
    +    %{"label" => transaction_tag.name}
    +  end
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/account/api/v2/user_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/account/api/v2/user_view.ex
    new file mode 100644
    index 0000000..18f4c17
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/account/api/v2/user_view.ex
    @@ -0,0 +1,220 @@
    +defmodule BlockScoutWeb.Account.API.V2.UserView do
    +  use BlockScoutWeb, :view
    +
    +  alias BlockScoutWeb.Account.API.V2.AccountView
    +  alias BlockScoutWeb.API.V2.Helper
    +  alias Ecto.Changeset
    +  alias Explorer.Account.WatchlistAddress
    +  alias Explorer.Chain
    +  alias Explorer.Chain.Address
    +
    +  def render("message.json", assigns) do
    +    AccountView.render("message.json", assigns)
    +  end
    +
    +  def render("user_info.json", %{identity: identity}) do
    +    %{
    +      "name" => identity.name,
    +      "email" => identity.email,
    +      "avatar" => identity.avatar,
    +      "nickname" => identity.nickname,
    +      "address_hash" => identity.address_hash
    +    }
    +  end
    +
    +  def render("watchlist_addresses.json", %{
    +        watchlist_addresses: watchlist_addresses,
    +        exchange_rate: exchange_rate,
    +        next_page_params: next_page_params
    +      }) do
    +    prepared_watchlist_addresses = prepare_watchlist_addresses(watchlist_addresses, exchange_rate)
    +
    +    %{
    +      "items" => prepared_watchlist_addresses,
    +      "next_page_params" => next_page_params
    +    }
    +  end
    +
    +  def render("watchlist_addresses.json", %{watchlist_addresses: watchlist_addresses, exchange_rate: exchange_rate}) do
    +    prepare_watchlist_addresses(watchlist_addresses, exchange_rate)
    +  end
    +
    +  def render("watchlist_address.json", %{watchlist_address: watchlist_address, exchange_rate: exchange_rate}) do
    +    address = Address.get_by_hash(watchlist_address.address_hash)
    +    prepare_watchlist_address(watchlist_address, address, exchange_rate)
    +  end
    +
    +  def render("address_tags.json", %{address_tags: address_tags, next_page_params: next_page_params}) do
    +    %{"items" => Enum.map(address_tags, &prepare_address_tag/1), "next_page_params" => next_page_params}
    +  end
    +
    +  def render("address_tags.json", %{address_tags: address_tags}) do
    +    Enum.map(address_tags, &prepare_address_tag/1)
    +  end
    +
    +  def render("address_tag.json", %{address_tag: address_tag}) do
    +    prepare_address_tag(address_tag)
    +  end
    +
    +  def render("transaction_tags.json", %{transaction_tags: transaction_tags, next_page_params: next_page_params}) do
    +    %{"items" => Enum.map(transaction_tags, &prepare_transaction_tag/1), "next_page_params" => next_page_params}
    +  end
    +
    +  def render("transaction_tags.json", %{transaction_tags: transaction_tags}) do
    +    Enum.map(transaction_tags, &prepare_transaction_tag/1)
    +  end
    +
    +  def render("transaction_tag.json", %{transaction_tag: transaction_tag}) do
    +    prepare_transaction_tag(transaction_tag)
    +  end
    +
    +  def render("api_keys.json", %{api_keys: api_keys}) do
    +    Enum.map(api_keys, &prepare_api_key/1)
    +  end
    +
    +  def render("api_key.json", %{api_key: api_key}) do
    +    prepare_api_key(api_key)
    +  end
    +
    +  def render("custom_abis.json", %{custom_abis: custom_abis}) do
    +    Enum.map(custom_abis, &prepare_custom_abi/1)
    +  end
    +
    +  def render("custom_abi.json", %{custom_abi: custom_abi}) do
    +    prepare_custom_abi(custom_abi)
    +  end
    +
    +  def render("public_tags_requests.json", %{public_tags_requests: public_tags_requests}) do
    +    Enum.map(public_tags_requests, &prepare_public_tags_request/1)
    +  end
    +
    +  def render("public_tags_request.json", %{public_tags_request: public_tags_request}) do
    +    prepare_public_tags_request(public_tags_request)
    +  end
    +
    +  def render("changeset_errors.json", %{changeset: changeset}) do
    +    %{
    +      "errors" =>
    +        Changeset.traverse_errors(changeset, fn {msg, opts} ->
    +          Regex.replace(~r"%{(\w+)}", msg, fn _, key ->
    +            opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string()
    +          end)
    +        end)
    +    }
    +  end
    +
    +  @spec prepare_watchlist_address(WatchlistAddress.t(), Chain.Address.t(), map()) :: map
    +  defp prepare_watchlist_address(watchlist, address, exchange_rate) do
    +    %{
    +      "id" => watchlist.id,
    +      "address" => Helper.address_with_info(nil, address, watchlist.address_hash, false),
    +      "address_hash" => watchlist.address_hash,
    +      "name" => watchlist.name,
    +      "address_balance" => if(address && address.fetched_coin_balance, do: address.fetched_coin_balance.value),
    +      "exchange_rate" => exchange_rate.fiat_value,
    +      "notification_settings" => %{
    +        "native" => %{
    +          "incoming" => watchlist.watch_coin_input,
    +          "outcoming" => watchlist.watch_coin_output
    +        },
    +        "ERC-20" => %{
    +          "incoming" => watchlist.watch_erc_20_input,
    +          "outcoming" => watchlist.watch_erc_20_output
    +        },
    +        "ERC-721" => %{
    +          "incoming" => watchlist.watch_erc_721_input,
    +          "outcoming" => watchlist.watch_erc_721_output
    +        },
    +        # "ERC-1155" => %{
    +        #   "incoming" => watchlist.watch_erc_1155_input,
    +        #   "outcoming" => watchlist.watch_erc_1155_output
    +        # },
    +        "ERC-404" => %{
    +          "incoming" => watchlist.watch_erc_404_input,
    +          "outcoming" => watchlist.watch_erc_404_output
    +        }
    +      },
    +      "notification_methods" => %{
    +        "email" => watchlist.notify_email
    +      },
    +      "tokens_fiat_value" => watchlist.tokens_fiat_value,
    +      "tokens_count" => watchlist.tokens_count,
    +      "tokens_overflow" => watchlist.tokens_overflow
    +    }
    +  end
    +
    +  @spec prepare_watchlist_addresses([WatchlistAddress.t()], map()) :: [map()]
    +  defp prepare_watchlist_addresses(watchlist_addresses, exchange_rate) do
    +    address_hashes =
    +      watchlist_addresses
    +      |> Enum.map(& &1.address_hash)
    +
    +    addresses = Address.get_addresses_by_hashes(address_hashes)
    +
    +    watchlist_addresses
    +    |> Enum.zip(addresses)
    +    |> Enum.map(fn {watchlist, address} ->
    +      prepare_watchlist_address(watchlist, address, exchange_rate)
    +    end)
    +  end
    +
    +  defp prepare_custom_abi(custom_abi) do
    +    address = Address.get_by_hash(custom_abi.address_hash)
    +
    +    %{
    +      "id" => custom_abi.id,
    +      "contract_address_hash" => custom_abi.address_hash,
    +      "contract_address" => Helper.address_with_info(nil, address, custom_abi.address_hash, false),
    +      "name" => custom_abi.name,
    +      "abi" => custom_abi.abi
    +    }
    +  end
    +
    +  defp prepare_api_key(api_key) do
    +    %{"api_key" => api_key.value, "name" => api_key.name}
    +  end
    +
    +  defp prepare_address_tag(address_tag) do
    +    address = Address.get_by_hash(address_tag.address_hash)
    +
    +    %{
    +      "id" => address_tag.id,
    +      "address_hash" => address_tag.address_hash,
    +      "address" => Helper.address_with_info(nil, address, address_tag.address_hash, false),
    +      "name" => address_tag.name
    +    }
    +  end
    +
    +  defp prepare_transaction_tag(nil), do: nil
    +
    +  defp prepare_transaction_tag(transaction_tag) do
    +    %{
    +      "id" => transaction_tag.id,
    +      "transaction_hash" => transaction_tag.transaction_hash,
    +      "name" => transaction_tag.name
    +    }
    +  end
    +
    +  defp prepare_public_tags_request(public_tags_request) do
    +    addresses = Address.get_addresses_by_hashes(public_tags_request.addresses)
    +
    +    addresses_with_info =
    +      Enum.map(addresses, fn address ->
    +        Helper.address_with_info(nil, address, address.hash, false)
    +      end)
    +
    +    %{
    +      "id" => public_tags_request.id,
    +      "full_name" => public_tags_request.full_name,
    +      "email" => public_tags_request.email,
    +      "company" => public_tags_request.company,
    +      "website" => public_tags_request.website,
    +      "tags" => public_tags_request.tags,
    +      "addresses" => public_tags_request.addresses,
    +      "addresses_with_info" => addresses_with_info,
    +      "additional_comment" => public_tags_request.additional_comment,
    +      "is_owner" => public_tags_request.is_owner,
    +      "submission_date" => public_tags_request.inserted_at
    +    }
    +  end
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/account/api_key_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/account/api_key_view.ex
    new file mode 100644
    index 0000000..a0b21b7
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/account/api_key_view.ex
    @@ -0,0 +1,5 @@
    +defmodule BlockScoutWeb.Account.ApiKeyView do
    +  use BlockScoutWeb, :view
    +
    +  alias Explorer.Account.Api.Key
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/account/auth_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/account/auth_view.ex
    new file mode 100644
    index 0000000..cfbeb00
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/account/auth_view.ex
    @@ -0,0 +1,3 @@
    +defmodule BlockScoutWeb.Account.AuthView do
    +  use BlockScoutWeb, :view
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/account/common_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/account/common_view.ex
    new file mode 100644
    index 0000000..e296ca9
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/account/common_view.ex
    @@ -0,0 +1,11 @@
    +defmodule BlockScoutWeb.Account.CommonView do
    +  use BlockScoutWeb, :view
    +
    +  def nav_class(active_item, item) do
    +    if active_item == item do
    +      "dropdown-item active fs-14"
    +    else
    +      "dropdown-item fs-14"
    +    end
    +  end
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/account/custom_abi_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/account/custom_abi_view.ex
    new file mode 100644
    index 0000000..3b38eff
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/account/custom_abi_view.ex
    @@ -0,0 +1,22 @@
    +defmodule BlockScoutWeb.Account.CustomABIView do
    +  use BlockScoutWeb, :view
    +
    +  alias Ecto.Changeset
    +  alias Explorer.Account.CustomABI
    +
    +  def format_abi(custom_abi) do
    +    with {_type, abi} <- Changeset.fetch_field(custom_abi, :abi),
    +         false <- is_nil(abi),
    +         {:binary, false} <- {:binary, is_binary(abi)},
    +         {:ok, encoded_abi} <- Poison.encode(abi) do
    +      encoded_abi
    +    else
    +      {:binary, true} ->
    +        {_type, abi} = Changeset.fetch_field(custom_abi, :abi)
    +        abi
    +
    +      _ ->
    +        ""
    +    end
    +  end
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/account/public_tags_request_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/account/public_tags_request_view.ex
    new file mode 100644
    index 0000000..2a13dd8
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/account/public_tags_request_view.ex
    @@ -0,0 +1,70 @@
    +defmodule BlockScoutWeb.Account.PublicTagsRequestView do
    +  use BlockScoutWeb, :view
    +  use Phoenix.HTML
    +
    +  alias Explorer.Account.PublicTagsRequest
    +  alias Phoenix.HTML.Form
    +
    +  def array_input(form, field, attrs \\ []) do
    +    values = Form.input_value(form, field) || [""]
    +    id = Form.input_id(form, field)
    +
    +    content_tag :ul,
    +      id: container_id(id),
    +      data: [index: Enum.count(values), multiple_input_field_container: ""],
    +      class: "multiple-input-fields-container" do
    +      values
    +      |> Enum.map(fn v ->
    +        form_elements(form, field, to_string(v), attrs)
    +      end)
    +    end
    +  end
    +
    +  def array_add_button(form, field, attrs \\ []) do
    +    id = Form.input_id(form, field)
    +
    +    content =
    +      form
    +      |> form_elements(field, "", attrs)
    +      |> safe_to_string
    +
    +    data = [
    +      prototype: content,
    +      container: container_id(id)
    +    ]
    +
    +    content_tag(:button, render(BlockScoutWeb.CommonComponentsView, "_svg_plus.html"),
    +      data: data,
    +      class: "add-form-field"
    +    )
    +  end
    +
    +  defp form_elements(form, field, k, attrs) do
    +    type = Form.input_type(form, field)
    +    id = Form.input_id(form, field)
    +
    +    input_opts =
    +      [
    +        name: new_field_name(form, field),
    +        value: k,
    +        id: id,
    +        class: "form-control public-tags-address"
    +      ] ++ attrs
    +
    +    content_tag :li, class: "public-tags-address form-group" do
    +      [
    +        apply(Form, type, [form, field, input_opts]),
    +        content_tag(:button, render(BlockScoutWeb.CommonComponentsView, "_svg_minus.html"),
    +          data: [container: container_id(id)],
    +          class: "remove-form-field ml-1"
    +        )
    +      ]
    +    end
    +  end
    +
    +  defp container_id(id), do: id <> "_container"
    +
    +  defp new_field_name(form, field) do
    +    Form.input_name(form, field) <> "[]"
    +  end
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/account/tag_address_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/account/tag_address_view.ex
    new file mode 100644
    index 0000000..74886c3
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/account/tag_address_view.ex
    @@ -0,0 +1,7 @@
    +defmodule BlockScoutWeb.Account.TagAddressView do
    +  use BlockScoutWeb, :view
    +
    +  import BlockScoutWeb.AddressView, only: [trimmed_hash: 1]
    +
    +  alias Explorer.Account.TagAddress
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/account/tag_transaction_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/account/tag_transaction_view.ex
    new file mode 100644
    index 0000000..7edfa1e
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/account/tag_transaction_view.ex
    @@ -0,0 +1,5 @@
    +defmodule BlockScoutWeb.Account.TagTransactionView do
    +  use BlockScoutWeb, :view
    +
    +  alias Explorer.Account.TagTransaction
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/account/watchlist_address_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/account/watchlist_address_view.ex
    new file mode 100644
    index 0000000..29246e3
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/account/watchlist_address_view.ex
    @@ -0,0 +1,11 @@
    +defmodule BlockScoutWeb.Account.WatchlistAddressView do
    +  use BlockScoutWeb, :view
    +  import BlockScoutWeb.AddressView, only: [trimmed_hash: 1]
    +  import BlockScoutWeb.WeiHelper, only: [format_wei_value: 2]
    +
    +  def balance_ether(nil), do: ""
    +
    +  def balance_ether(balance) do
    +    format_wei_value(balance, :ether)
    +  end
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/account/watchlist_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/account/watchlist_view.ex
    new file mode 100644
    index 0000000..b30802d
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/account/watchlist_view.ex
    @@ -0,0 +1,11 @@
    +defmodule BlockScoutWeb.Account.WatchlistView do
    +  use BlockScoutWeb, :view
    +
    +  alias BlockScoutWeb.Account.WatchlistAddressView
    +  alias Explorer.Account.WatchlistAddress
    +  alias Explorer.Market
    +
    +  def exchange_rate do
    +    Market.get_coin_exchange_rate()
    +  end
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/address_coin_balance_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/address_coin_balance_view.ex
    new file mode 100644
    index 0000000..6e0363d
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/address_coin_balance_view.ex
    @@ -0,0 +1,34 @@
    +defmodule BlockScoutWeb.AddressCoinBalanceView do
    +  use BlockScoutWeb, :view
    +
    +  alias BlockScoutWeb.AccessHelper
    +  alias Explorer.Chain.Wei
    +  alias Explorer.SmartContract.Helper, as: SmartContractHelper
    +
    +  def format(%Wei{} = value) do
    +    format_wei_value(value, :ether)
    +  end
    +
    +  def delta_arrow(value) do
    +    if value.sign == 1 do
    +      "▲"
    +    else
    +      "▼"
    +    end
    +  end
    +
    +  def delta_sign(value) do
    +    if value.sign == 1 do
    +      "Positive"
    +    else
    +      "Negative"
    +    end
    +  end
    +
    +  def format_delta(%Decimal{} = value) do
    +    value
    +    |> Decimal.abs()
    +    |> Wei.from(:wei)
    +    |> format_wei_value(:ether)
    +  end
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/address_contract_verification_common_fields_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/address_contract_verification_common_fields_view.ex
    new file mode 100644
    index 0000000..9339807
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/address_contract_verification_common_fields_view.ex
    @@ -0,0 +1,3 @@
    +defmodule BlockScoutWeb.AddressContractVerificationCommonFieldsView do
    +  use BlockScoutWeb, :view
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/address_contract_verification_via_flattened_code_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/address_contract_verification_via_flattened_code_view.ex
    new file mode 100644
    index 0000000..e5adf9b
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/address_contract_verification_via_flattened_code_view.ex
    @@ -0,0 +1,6 @@
    +defmodule BlockScoutWeb.AddressContractVerificationViaFlattenedCodeView do
    +  use BlockScoutWeb, :view
    +
    +  alias Explorer.Chain.SmartContract
    +  alias Explorer.SmartContract.RustVerifierInterface
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/address_contract_verification_via_json_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/address_contract_verification_via_json_view.ex
    new file mode 100644
    index 0000000..79f8681
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/address_contract_verification_via_json_view.ex
    @@ -0,0 +1,3 @@
    +defmodule BlockScoutWeb.AddressContractVerificationViaJsonView do
    +  use BlockScoutWeb, :view
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/address_contract_verification_via_multi_part_files_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/address_contract_verification_via_multi_part_files_view.ex
    new file mode 100644
    index 0000000..76f88f0
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/address_contract_verification_via_multi_part_files_view.ex
    @@ -0,0 +1,6 @@
    +defmodule BlockScoutWeb.AddressContractVerificationViaMultiPartFilesView do
    +  use BlockScoutWeb, :view
    +
    +  alias Explorer.Chain.SmartContract
    +  alias Explorer.SmartContract.RustVerifierInterface
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/address_contract_verification_via_standard_json_input_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/address_contract_verification_via_standard_json_input_view.ex
    new file mode 100644
    index 0000000..9a4298a
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/address_contract_verification_via_standard_json_input_view.ex
    @@ -0,0 +1,5 @@
    +defmodule BlockScoutWeb.AddressContractVerificationViaStandardJsonInputView do
    +  use BlockScoutWeb, :view
    +
    +  alias Explorer.Chain.SmartContract
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/address_contract_verification_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/address_contract_verification_view.ex
    new file mode 100644
    index 0000000..385d76c
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/address_contract_verification_view.ex
    @@ -0,0 +1,5 @@
    +defmodule BlockScoutWeb.AddressContractVerificationView do
    +  use BlockScoutWeb, :view
    +
    +  alias Explorer.SmartContract.RustVerifierInterface
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/address_contract_verification_vyper_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/address_contract_verification_vyper_view.ex
    new file mode 100644
    index 0000000..47da5ea
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/address_contract_verification_vyper_view.ex
    @@ -0,0 +1,5 @@
    +defmodule BlockScoutWeb.AddressContractVerificationVyperView do
    +  use BlockScoutWeb, :view
    +
    +  alias Explorer.Chain.SmartContract
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/address_contract_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/address_contract_view.ex
    new file mode 100644
    index 0000000..bb47c8a
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/address_contract_view.ex
    @@ -0,0 +1,171 @@
    +defmodule BlockScoutWeb.AddressContractView do
    +  use BlockScoutWeb, :view
    +
    +  require Logger
    +
    +  import Explorer.Helper, only: [decode_data: 2]
    +  import Phoenix.LiveView.Helpers, only: [sigil_H: 2]
    +
    +  alias ABI.FunctionSelector
    +  alias Explorer.Chain
    +  alias Explorer.Chain.{Address, Data, InternalTransaction, Transaction}
    +  alias Explorer.Chain.SmartContract
    +  alias Explorer.Chain.SmartContract.Proxy.EIP1167
    +  alias Explorer.Helper, as: ExplorerHelper
    +  alias Explorer.SmartContract.Helper, as: SmartContractHelper
    +  alias Phoenix.HTML.Safe
    +
    +  def render("scripts.html", %{conn: conn}) do
    +    render_scripts(conn, "address_contract/code_highlighting.js")
    +  end
    +
    +  def format_smart_contract_abi(abi) when not is_nil(abi), do: Poison.encode!(abi, %{pretty: false})
    +
    +  @doc """
    +  Returns the correct format for the optimization text.
    +
    +    iex> BlockScoutWeb.AddressContractView.format_optimization_text(true)
    +    "true"
    +
    +    iex> BlockScoutWeb.AddressContractView.format_optimization_text(false)
    +    "false"
    +  """
    +  def format_optimization_text(true), do: gettext("true")
    +  def format_optimization_text(false), do: gettext("false")
    +
    +  def format_constructor_arguments(contract, conn) do
    +    constructor_abi = Enum.find(contract.abi, fn el -> el["type"] == "constructor" && el["inputs"] != [] end)
    +
    +    input_types = Enum.map(constructor_abi["inputs"], &FunctionSelector.parse_specification_type/1)
    +
    +    {_, result} =
    +      contract.constructor_arguments
    +      |> decode_data(input_types)
    +      |> Enum.zip(constructor_abi["inputs"])
    +      |> Enum.reduce({0, "#{contract.constructor_arguments}\n\n"}, fn {val, %{"type" => type}}, {count, acc} ->
    +        formatted_val = val_to_string(val, type, conn)
    +        assigns = %{acc: acc, count: count, type: type, formatted_val: formatted_val}
    +
    +        {count + 1,
    +         ~H"""
    +         <%= @acc %> Arg [<%= @count %>] (<%= @type %>) : <%= @formatted_val %>
    +         """
    +         |> Safe.to_iodata()
    +         |> List.to_string()}
    +      end)
    +
    +    result
    +  rescue
    +    _ -> contract.constructor_arguments
    +  end
    +
    +  defp val_to_string(val, type, conn) do
    +    cond do
    +      type =~ "[]" ->
    +        val_to_string_if_array(val, type, conn)
    +
    +      type =~ "address" ->
    +        address_hash = ExplorerHelper.add_0x_prefix(val)
    +
    +        address = Chain.string_to_address_hash_or_nil(address_hash)
    +
    +        get_formatted_address_data(address, address_hash, conn)
    +
    +      type =~ "bytes" ->
    +        Base.encode16(val, case: :lower)
    +
    +      true ->
    +        to_string(val)
    +    end
    +  end
    +
    +  defp val_to_string_if_array(val, type, conn) do
    +    if is_list(val) or is_tuple(val) do
    +      "[" <>
    +        Enum.map_join(val, ", ", fn el -> val_to_string(el, String.replace_suffix(type, "[]", ""), conn) end) <> "]"
    +    else
    +      to_string(val)
    +    end
    +  end
    +
    +  defp get_formatted_address_data(address, address_hash, conn) do
    +    if address != nil do
    +      assigns = %{address: address, address_hash: address_hash, conn: conn}
    +
    +      ~H"""
    +      <%= @address_hash %>
    +      """
    +    else
    +      address_hash
    +    end
    +  end
    +
    +  def format_external_libraries(libraries, conn) do
    +    Enum.reduce(libraries, "", fn %{name: name, address_hash: address_hash}, acc ->
    +      address = Chain.string_to_address_hash_or_nil(address_hash)
    +      assigns = %{acc: acc, name: name, address: address, address_hash: address_hash, conn: conn}
    +
    +      ~H"""
    +      <%= @acc %><%= @name %> : <%= get_formatted_address_data(@address, @address_hash, @conn) %>
    +      """
    +      |> Safe.to_iodata()
    +      |> List.to_string()
    +    end)
    +  end
    +
    +  def contract_creation_code(%Address{
    +        contract_creation_transaction: %Transaction{
    +          status: :error,
    +          input: creation_code
    +        }
    +      }) do
    +    {:failed, creation_code}
    +  end
    +
    +  def contract_creation_code(%Address{
    +        contract_creation_internal_transaction: %InternalTransaction{
    +          error: error,
    +          init: init
    +        }
    +      })
    +      when not is_nil(error) do
    +    {:failed, init}
    +  end
    +
    +  def contract_creation_code(%Address{
    +        contract_code: %Data{bytes: <<>>},
    +        contract_creation_internal_transaction: %InternalTransaction{init: init}
    +      }) do
    +    {:selfdestructed, init}
    +  end
    +
    +  def contract_creation_code(%Address{contract_code: contract_code}) do
    +    {:ok, contract_code}
    +  end
    +
    +  def creation_code(%Address{contract_creation_transaction: %Transaction{}} = address) do
    +    address.contract_creation_transaction.input
    +  end
    +
    +  def creation_code(%Address{contract_creation_internal_transaction: %InternalTransaction{}} = address) do
    +    address.contract_creation_internal_transaction.init
    +  end
    +
    +  def creation_code(%Address{contract_creation_transaction: nil}) do
    +    nil
    +  end
    +
    +  def sourcify_repo_url(address_hash, partial_match) do
    +    checksummed_hash = Address.checksum(address_hash)
    +    chain_id = Application.get_env(:explorer, Explorer.ThirdPartyIntegrations.Sourcify)[:chain_id]
    +    repo_url = Application.get_env(:explorer, Explorer.ThirdPartyIntegrations.Sourcify)[:repo_url]
    +    match = if partial_match, do: "/partial_match/", else: "/full_match/"
    +
    +    if chain_id do
    +      repo_url <> match <> chain_id <> "/" <> checksummed_hash <> "/"
    +    else
    +      Logger.warning("chain_id is nil. Please set CHAIN_ID env variable.")
    +      nil
    +    end
    +  end
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/address_internal_transaction_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/address_internal_transaction_view.ex
    new file mode 100644
    index 0000000..81cb6ca
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/address_internal_transaction_view.ex
    @@ -0,0 +1,15 @@
    +defmodule BlockScoutWeb.AddressInternalTransactionView do
    +  use BlockScoutWeb, :view
    +
    +  alias BlockScoutWeb.AccessHelper
    +  alias Explorer.Chain.Address
    +  alias Explorer.SmartContract.Helper, as: SmartContractHelper
    +
    +  def format_current_filter(filter) do
    +    case filter do
    +      "to" -> gettext("To")
    +      "from" -> gettext("From")
    +      _ -> gettext("All")
    +    end
    +  end
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/address_logs_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/address_logs_view.ex
    new file mode 100644
    index 0000000..e1a4862
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/address_logs_view.ex
    @@ -0,0 +1,8 @@
    +defmodule BlockScoutWeb.AddressLogsView do
    +  use BlockScoutWeb, :view
    +
    +  alias Explorer.Chain.Address
    +  alias Explorer.SmartContract.Helper, as: SmartContractHelper
    +
    +  import BlockScoutWeb.AddressView, only: [decode: 2]
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/address_read_contract_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/address_read_contract_view.ex
    new file mode 100644
    index 0000000..e13b147
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/address_read_contract_view.ex
    @@ -0,0 +1,5 @@
    +defmodule BlockScoutWeb.AddressReadContractView do
    +  use BlockScoutWeb, :view
    +
    +  alias Explorer.SmartContract.Helper, as: SmartContractHelper
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/address_read_proxy_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/address_read_proxy_view.ex
    new file mode 100644
    index 0000000..447f362
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/address_read_proxy_view.ex
    @@ -0,0 +1,5 @@
    +defmodule BlockScoutWeb.AddressReadProxyView do
    +  use BlockScoutWeb, :view
    +
    +  alias Explorer.SmartContract.Helper, as: SmartContractHelper
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/address_token_balance_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/address_token_balance_view.ex
    new file mode 100644
    index 0000000..46f0e6a
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/address_token_balance_view.ex
    @@ -0,0 +1,20 @@
    +defmodule BlockScoutWeb.AddressTokenBalanceView do
    +  use BlockScoutWeb, :view
    +
    +  alias BlockScoutWeb.AccessHelper
    +  alias Explorer.Chain
    +  alias Explorer.Chain.Address
    +  alias Explorer.Chain.Cache.Counters.AddressTokensUsdSum
    +
    +  def tokens_count_title(token_balances) do
    +    ngettext("%{count} token", "%{count} tokens", Enum.count(token_balances))
    +  end
    +
    +  def filter_by_type(token_balances, type) do
    +    Enum.filter(token_balances, fn token_balance -> token_balance.token.type == type end)
    +  end
    +
    +  def address_tokens_usd_sum_cache(address, token_balances) do
    +    AddressTokensUsdSum.fetch(address, token_balances)
    +  end
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/address_token_transfer_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/address_token_transfer_view.ex
    new file mode 100644
    index 0000000..c053e54
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/address_token_transfer_view.ex
    @@ -0,0 +1,15 @@
    +defmodule BlockScoutWeb.AddressTokenTransferView do
    +  use BlockScoutWeb, :view
    +
    +  alias BlockScoutWeb.AccessHelper
    +  alias Explorer.Chain.Address
    +  alias Explorer.SmartContract.Helper, as: SmartContractHelper
    +
    +  def format_current_filter(filter) do
    +    case filter do
    +      "to" -> gettext("To")
    +      "from" -> gettext("From")
    +      _ -> gettext("All")
    +    end
    +  end
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/address_token_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/address_token_view.ex
    new file mode 100644
    index 0000000..941cfe2
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/address_token_view.ex
    @@ -0,0 +1,8 @@
    +defmodule BlockScoutWeb.AddressTokenView do
    +  use BlockScoutWeb, :view
    +
    +  alias BlockScoutWeb.{AddressView, ChainView}
    +  alias Explorer.Chain
    +  alias Explorer.Chain.{Address, Wei}
    +  alias Explorer.SmartContract.Helper, as: SmartContractHelper
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/address_transaction_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/address_transaction_view.ex
    new file mode 100644
    index 0000000..b6539a4
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/address_transaction_view.ex
    @@ -0,0 +1,15 @@
    +defmodule BlockScoutWeb.AddressTransactionView do
    +  use BlockScoutWeb, :view
    +
    +  alias BlockScoutWeb.AccessHelper
    +  alias Explorer.Chain.Address
    +  alias Explorer.SmartContract.Helper, as: SmartContractHelper
    +
    +  def format_current_filter(filter) do
    +    case filter do
    +      "to" -> gettext("To")
    +      "from" -> gettext("From")
    +      _ -> gettext("All")
    +    end
    +  end
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/address_validation_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/address_validation_view.ex
    new file mode 100644
    index 0000000..1625fb3
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/address_validation_view.ex
    @@ -0,0 +1,5 @@
    +defmodule BlockScoutWeb.AddressValidationView do
    +  use BlockScoutWeb, :view
    +
    +  alias Explorer.SmartContract.Helper, as: SmartContractHelper
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/address_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/address_view.ex
    new file mode 100644
    index 0000000..7b7ae76
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/address_view.ex
    @@ -0,0 +1,481 @@
    +defmodule BlockScoutWeb.AddressView do
    +  use BlockScoutWeb, :view
    +
    +  require Logger
    +
    +  alias BlockScoutWeb.{AccessHelper, LayoutView}
    +  alias BlockScoutWeb.API.V2.Helper, as: APIV2Helper
    +  alias Explorer.Account.CustomABI
    +  alias Explorer.{Chain, CustomContractsHelper, Repo}
    +  alias Explorer.Chain.Address.Counters
    +  alias Explorer.Chain.{Address, Hash, InternalTransaction, Log, SmartContract, Token, TokenTransfer, Transaction, Wei}
    +  alias Explorer.Chain.Block.Reward
    +  alias Explorer.Chain.SmartContract.Proxy.Models.Implementation
    +  alias Explorer.Market.Token, as: TokenExchangeRate
    +  alias Explorer.SmartContract.{Helper, Writer}
    +
    +  import BlockScoutWeb.Account.AuthController, only: [current_user: 1]
    +
    +  @dialyzer :no_match
    +
    +  @tabs [
    +    "coin-balances",
    +    "contracts",
    +    "internal-transactions",
    +    "token-transfers",
    +    "read-contract",
    +    "read-proxy",
    +    "write-contract",
    +    "write-proxy",
    +    "tokens",
    +    "transactions",
    +    "validations"
    +  ]
    +
    +  def address_partial_selector(struct_to_render_from, direction, current_address, truncate \\ false)
    +
    +  def address_partial_selector(%Address{} = address, _, current_address, truncate) do
    +    matching_address_check(current_address, address, Address.smart_contract?(address), truncate)
    +  end
    +
    +  def address_partial_selector(
    +        %InternalTransaction{to_address_hash: nil, created_contract_address_hash: nil},
    +        :to,
    +        _current_address,
    +        _truncate
    +      ) do
    +    gettext("Contract Address Pending")
    +  end
    +
    +  def address_partial_selector(
    +        %InternalTransaction{to_address: nil, created_contract_address: contract_address},
    +        :to,
    +        current_address,
    +        truncate
    +      ) do
    +    matching_address_check(current_address, contract_address, true, truncate)
    +  end
    +
    +  def address_partial_selector(%InternalTransaction{to_address: address}, :to, current_address, truncate) do
    +    matching_address_check(current_address, address, Address.smart_contract?(address), truncate)
    +  end
    +
    +  def address_partial_selector(%InternalTransaction{from_address: address}, :from, current_address, truncate) do
    +    matching_address_check(current_address, address, Address.smart_contract?(address), truncate)
    +  end
    +
    +  def address_partial_selector(%TokenTransfer{to_address: address}, :to, current_address, truncate) do
    +    matching_address_check(current_address, address, Address.smart_contract?(address), truncate)
    +  end
    +
    +  def address_partial_selector(%TokenTransfer{from_address: address}, :from, current_address, truncate) do
    +    matching_address_check(current_address, address, Address.smart_contract?(address), truncate)
    +  end
    +
    +  def address_partial_selector(
    +        %Transaction{to_address_hash: nil, created_contract_address_hash: nil},
    +        :to,
    +        _current_address,
    +        _truncate
    +      ) do
    +    gettext("Contract Address Pending")
    +  end
    +
    +  def address_partial_selector(
    +        %Transaction{to_address: nil, created_contract_address: contract_address},
    +        :to,
    +        current_address,
    +        truncate
    +      ) do
    +    matching_address_check(current_address, contract_address, true, truncate)
    +  end
    +
    +  def address_partial_selector(%Transaction{to_address: address}, :to, current_address, truncate) do
    +    matching_address_check(current_address, address, Address.smart_contract?(address), truncate)
    +  end
    +
    +  def address_partial_selector(%Transaction{from_address: address}, :from, current_address, truncate) do
    +    matching_address_check(current_address, address, Address.smart_contract?(address), truncate)
    +  end
    +
    +  def address_partial_selector(%Reward{address: address}, _, current_address, truncate) do
    +    matching_address_check(current_address, address, false, truncate)
    +  end
    +
    +  def address_title(%Address{} = address) do
    +    if Address.smart_contract?(address) do
    +      gettext("Contract Address")
    +    else
    +      gettext("Address")
    +    end
    +  end
    +
    +  @doc """
    +  Returns a formatted address balance and includes the unit.
    +  """
    +  def balance(%Address{fetched_coin_balance: nil}), do: ""
    +
    +  def balance(%Address{fetched_coin_balance: balance}) do
    +    format_wei_value(balance, :ether)
    +  end
    +
    +  def balance_percentage_enabled?(total_supply) do
    +    Application.get_env(:block_scout_web, :show_percentage) && total_supply > 0
    +  end
    +
    +  def balance_percentage(_, nil), do: ""
    +
    +  def balance_percentage(
    +        %Address{
    +          hash: %Explorer.Chain.Hash{
    +            byte_count: 20,
    +            bytes: <<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>>
    +          }
    +        },
    +        _
    +      ),
    +      do: ""
    +
    +  def balance_percentage(%Address{fetched_coin_balance: balance}, total_supply) do
    +    if Decimal.compare(total_supply, 0) == :gt do
    +      balance
    +      |> Wei.to(:ether)
    +      |> Decimal.div(Decimal.new(total_supply))
    +      |> Decimal.mult(100)
    +      |> Decimal.round(4)
    +      |> Decimal.to_string(:normal)
    +      |> Kernel.<>("% #{gettext("Market Cap")}")
    +    else
    +      balance
    +      |> Wei.to(:ether)
    +      |> Decimal.to_string(:normal)
    +    end
    +  end
    +
    +  def empty_exchange_rate?(exchange_rate) do
    +    TokenExchangeRate.null?(exchange_rate)
    +  end
    +
    +  def balance_percentage(%Address{fetched_coin_balance: _} = address) do
    +    balance_percentage(address, Chain.total_supply())
    +  end
    +
    +  def balance_block_number(%Address{fetched_coin_balance_block_number: nil}), do: ""
    +
    +  def balance_block_number(%Address{fetched_coin_balance_block_number: fetched_coin_balance_block_number}) do
    +    to_string(fetched_coin_balance_block_number)
    +  end
    +
    +  def validator?(val) when val > 0, do: true
    +
    +  def validator?(_), do: false
    +
    +  def hash(%Address{hash: hash}) do
    +    to_string(hash)
    +  end
    +
    +  @doc """
    +  Returns the primary name of an address if available. If there is no names on address function performs preload of names association.
    +  """
    +  def primary_name(nil), do: nil
    +
    +  def primary_name(%Address{names: [_ | _]} = address) do
    +    APIV2Helper.address_name(address)
    +  end
    +
    +  def primary_name(%Address{names: %Ecto.Association.NotLoaded{}} = address) do
    +    primary_name(Repo.preload(address, [:names]))
    +  end
    +
    +  def primary_name(%Address{names: _} = address) do
    +    with true <- Address.smart_contract_with_nonempty_code?(address),
    +         bytecode_twin <- SmartContract.get_verified_bytecode_twin_contract(address),
    +         false <- is_nil(bytecode_twin) do
    +      bytecode_twin.name
    +    else
    +      _ ->
    +        nil
    +    end
    +  end
    +
    +  def primary_validator_metadata(%Address{names: [_ | _] = address_names}) do
    +    case Enum.find(address_names, &(&1.primary == true)) do
    +      %Address.Name{
    +        metadata:
    +          metadata = %{
    +            "license_id" => _,
    +            "address" => _,
    +            "state" => _,
    +            "zipcode" => _,
    +            "expiration_date" => _,
    +            "created_date" => _
    +          }
    +      } ->
    +        metadata
    +
    +      _ ->
    +        nil
    +    end
    +  end
    +
    +  def primary_validator_metadata(%Address{names: _}), do: nil
    +
    +  def format_datetime_string(unix_date) do
    +    unix_date
    +    |> DateTime.from_unix!()
    +    |> Timex.format!("{M}-{D}-{YYYY}")
    +  end
    +
    +  def qr_code(address_hash) do
    +    address_hash
    +    |> to_string()
    +    |> QRCode.to_png()
    +    |> Base.encode64()
    +  end
    +
    +  def smart_contract_with_read_only_functions?(%Address{smart_contract: %SmartContract{}} = address) do
    +    Enum.any?(address.smart_contract.abi || [], &read_function?(&1))
    +  end
    +
    +  def smart_contract_with_read_only_functions?(%Address{smart_contract: _}), do: false
    +
    +  def read_function?(function), do: Helper.queryable_method?(function) || Helper.read_with_wallet_method?(function)
    +
    +  def smart_contract_with_write_functions?(%Address{smart_contract: %SmartContract{}} = address) do
    +    !contract_interaction_disabled?() &&
    +      Enum.any?(
    +        address.smart_contract.abi || [],
    +        &Writer.write_function?(&1)
    +      )
    +  end
    +
    +  def smart_contract_with_write_functions?(%Address{smart_contract: _}), do: false
    +
    +  def token_title(%Token{name: nil, contract_address_hash: contract_address_hash}) do
    +    short_hash_left_right(contract_address_hash)
    +  end
    +
    +  def token_title(%Token{name: name, symbol: symbol}), do: "#{name} (#{symbol})"
    +
    +  def trimmed_hash(%Hash{} = hash) do
    +    string_hash = to_string(hash)
    +    trimmed_hash(string_hash)
    +  end
    +
    +  def trimmed_hash(address) when is_binary(address) do
    +    "#{String.slice(address, 0..7)}–#{String.slice(address, -6..-1)}"
    +  end
    +
    +  def trimmed_hash(_), do: ""
    +
    +  def trimmed_verify_link(hash) do
    +    string_hash = to_string(hash)
    +    "#{String.slice(string_hash, 0..21)}..."
    +  end
    +
    +  def transaction_hash(%Address{contract_creation_internal_transaction: %InternalTransaction{}} = address) do
    +    address.contract_creation_internal_transaction.transaction_hash
    +  end
    +
    +  def transaction_hash(%Address{contract_creation_transaction: %Transaction{}} = address) do
    +    address.contract_creation_transaction.hash
    +  end
    +
    +  def from_address_hash(%Address{contract_creation_internal_transaction: %InternalTransaction{}} = address) do
    +    address.contract_creation_internal_transaction.from_address_hash
    +  end
    +
    +  def from_address_hash(%Address{contract_creation_transaction: %Transaction{}} = address) do
    +    address.contract_creation_transaction.from_address_hash
    +  end
    +
    +  def from_address_hash(_address), do: nil
    +
    +  def address_link_to_other_explorer(link, address, full) do
    +    if full do
    +      link <> to_string(address)
    +    else
    +      trimmed_verify_link(link <> to_string(address))
    +    end
    +  end
    +
    +  defp matching_address_check(%Address{hash: hash} = current_address, %Address{hash: hash}, contract?, truncate) do
    +    [
    +      view_module: __MODULE__,
    +      partial: "_responsive_hash.html",
    +      address: current_address,
    +      contract: contract?,
    +      truncate: truncate,
    +      use_custom_tooltip: false
    +    ]
    +  end
    +
    +  defp matching_address_check(_current_address, %Address{} = address, contract?, truncate) do
    +    [
    +      view_module: __MODULE__,
    +      partial: "_link.html",
    +      address: address,
    +      contract: contract?,
    +      truncate: truncate,
    +      use_custom_tooltip: false
    +    ]
    +  end
    +
    +  defp matching_address_check(current_address, nil, contract?, truncate) do
    +    [
    +      view_module: __MODULE__,
    +      partial: "_responsive_hash.html",
    +      address: current_address,
    +      contract: contract?,
    +      truncate: truncate,
    +      use_custom_tooltip: false
    +    ]
    +  end
    +
    +  @doc """
    +  Get the current tab name/title from the request path and possible tab names.
    +
    +  The tabs on mobile are represented by a dropdown list, which has a title. This title is the
    +  currently selected tab name. This function returns that name, properly gettext'ed.
    +
    +  The list of possible tab names for this page is represented by the attribute @tab.
    +
    +  Raises error if there is no match, so a developer of a new tab must include it in the list.
    +  """
    +  def current_tab_name(request_path) do
    +    @tabs
    +    |> Enum.filter(&tab_active?(&1, request_path))
    +    |> tab_name()
    +  end
    +
    +  defp tab_name(["tokens"]), do: gettext("Tokens")
    +  defp tab_name(["internal-transactions"]), do: gettext("Internal Transactions")
    +  defp tab_name(["transactions"]), do: gettext("Transactions")
    +  defp tab_name(["token-transfers"]), do: gettext("Token Transfers")
    +  defp tab_name(["contracts"]), do: gettext("Code")
    +  defp tab_name(["read-contract"]), do: gettext("Read Contract")
    +  defp tab_name(["read-proxy"]), do: gettext("Read Proxy")
    +  defp tab_name(["write-contract"]), do: gettext("Write Contract")
    +  defp tab_name(["write-proxy"]), do: gettext("Write Proxy")
    +  defp tab_name(["coin-balances"]), do: gettext("Coin Balance History")
    +  defp tab_name(["validations"]), do: gettext("Blocks Validated")
    +  defp tab_name(["logs"]), do: gettext("Logs")
    +
    +  def short_hash(%Address{hash: hash}) do
    +    <<
    +      "0x",
    +      short_address::binary-size(6),
    +      _rest::binary
    +    >> = to_string(hash)
    +
    +    "0x" <> short_address
    +  end
    +
    +  def short_hash_left_right(hash) when not is_nil(hash) do
    +    case hash do
    +      "0x" <> rest ->
    +        shortify_hash_string(rest)
    +
    +      %Chain.Hash{
    +        byte_count: _,
    +        bytes: bytes
    +      } ->
    +        shortify_hash_string(Base.encode16(bytes, case: :lower))
    +
    +      hash ->
    +        shortify_hash_string(hash)
    +    end
    +  end
    +
    +  def short_hash_left_right(hash) when is_nil(hash), do: ""
    +
    +  defp shortify_hash_string(hash) do
    +    <<
    +      left::binary-size(6),
    +      _middle::binary-size(28),
    +      right::binary-size(6)
    +    >> = to_string(hash)
    +
    +    "0x" <> left <> "-" <> right
    +  end
    +
    +  def short_contract_name(name, max_length) do
    +    short_string(name, max_length)
    +  end
    +
    +  def short_token_id(%Decimal{} = token_id, max_length) do
    +    token_id
    +    |> Decimal.to_string()
    +    |> short_string(max_length)
    +  end
    +
    +  def short_token_id(token_id, max_length) do
    +    short_string(token_id, max_length)
    +  end
    +
    +  def short_string(nil, _max_length), do: ""
    +
    +  def short_string(name, max_length) do
    +    part_length = Kernel.trunc(max_length / 4)
    +
    +    if String.length(name) <= max_length,
    +      do: name,
    +      else: "#{String.slice(name, 0, max_length - part_length)}..#{String.slice(name, -part_length, part_length)}"
    +  end
    +
    +  def address_page_title(address) do
    +    cond do
    +      APIV2Helper.smart_contract_verified?(address) -> "#{address.smart_contract.name} (#{to_string(address)})"
    +      Address.smart_contract?(address) -> "Contract #{to_string(address)}"
    +      true -> "#{to_string(address)}"
    +    end
    +  end
    +
    +  def tag_name_to_label(tag_name) do
    +    tag_name
    +    |> String.replace(" ", "-")
    +  end
    +
    +  def fetch_custom_abi(conn, address_hash) do
    +    if current_user = current_user(conn) do
    +      CustomABI.get_custom_abi_by_identity_id_and_address_hash(address_hash, current_user.id)
    +    end
    +  end
    +
    +  def has_address_custom_abi_with_read_functions?(conn, address_hash) do
    +    custom_abi = fetch_custom_abi(conn, address_hash)
    +
    +    check_custom_abi_for_having_read_functions(custom_abi)
    +  end
    +
    +  def check_custom_abi_for_having_read_functions(custom_abi),
    +    do: !is_nil(custom_abi) && Enum.any?(custom_abi.abi, &read_function?(&1))
    +
    +  def has_address_custom_abi_with_write_functions?(conn, address_hash) do
    +    if contract_interaction_disabled?() do
    +      false
    +    else
    +      custom_abi = fetch_custom_abi(conn, address_hash)
    +
    +      check_custom_abi_for_having_write_functions(custom_abi)
    +    end
    +  end
    +
    +  def check_custom_abi_for_having_write_functions(custom_abi),
    +    do: !is_nil(custom_abi) && Enum.any?(custom_abi.abi, &Writer.write_function?(&1))
    +
    +  def contract_interaction_disabled?, do: Application.get_env(:block_scout_web, :contract)[:disable_interaction]
    +
    +  @doc """
    +    Decodes given log
    +  """
    +  @spec decode(Log.t(), Transaction.t()) ::
    +          {:ok, String.t(), String.t(), map()}
    +          | {:error, atom()}
    +          | {:error, atom(), list()}
    +          | {{:error, :contract_not_verified, list()}, any()}
    +  def decode(log, transaction) do
    +    {result, _full_abi_per_address_hash_contracts_acc, _events_acc} = Log.decode(log, transaction, [], true, false)
    +    result
    +  end
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/address_withdrawal_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/address_withdrawal_view.ex
    new file mode 100644
    index 0000000..80572ba
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/address_withdrawal_view.ex
    @@ -0,0 +1,5 @@
    +defmodule BlockScoutWeb.AddressWithdrawalView do
    +  use BlockScoutWeb, :view
    +
    +  alias Explorer.SmartContract.Helper, as: SmartContractHelper
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/address_write_contract_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/address_write_contract_view.ex
    new file mode 100644
    index 0000000..dc8d88c
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/address_write_contract_view.ex
    @@ -0,0 +1,5 @@
    +defmodule BlockScoutWeb.AddressWriteContractView do
    +  use BlockScoutWeb, :view
    +
    +  alias Explorer.SmartContract.Helper, as: SmartContractHelper
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/address_write_proxy_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/address_write_proxy_view.ex
    new file mode 100644
    index 0000000..b27e5df
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/address_write_proxy_view.ex
    @@ -0,0 +1,5 @@
    +defmodule BlockScoutWeb.AddressWriteProxyView do
    +  use BlockScoutWeb, :view
    +
    +  alias Explorer.SmartContract.Helper, as: SmartContractHelper
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/admin/dashboard_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/admin/dashboard_view.ex
    new file mode 100644
    index 0000000..536ef37
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/admin/dashboard_view.ex
    @@ -0,0 +1,3 @@
    +defmodule BlockScoutWeb.Admin.DashboardView do
    +  use BlockScoutWeb, :view
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/admin/session_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/admin/session_view.ex
    new file mode 100644
    index 0000000..0c00801
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/admin/session_view.ex
    @@ -0,0 +1,7 @@
    +defmodule BlockScoutWeb.Admin.SessionView do
    +  use BlockScoutWeb, :view
    +
    +  import BlockScoutWeb.Routers.AdminRouter.Helpers
    +
    +  alias BlockScoutWeb.FormView
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/admin/setup_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/admin/setup_view.ex
    new file mode 100644
    index 0000000..ab2df94
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/admin/setup_view.ex
    @@ -0,0 +1,7 @@
    +defmodule BlockScoutWeb.Admin.SetupView do
    +  use BlockScoutWeb, :view
    +
    +  import BlockScoutWeb.Routers.AdminRouter.Helpers
    +
    +  alias BlockScoutWeb.FormView
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/advertisement/banners_ad_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/advertisement/banners_ad_view.ex
    new file mode 100644
    index 0000000..47f461f
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/advertisement/banners_ad_view.ex
    @@ -0,0 +1,3 @@
    +defmodule BlockScoutWeb.Advertisement.BannersAdView do
    +  use BlockScoutWeb, :view
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/advertisement/text_ad_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/advertisement/text_ad_view.ex
    new file mode 100644
    index 0000000..8d73eb3
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/advertisement/text_ad_view.ex
    @@ -0,0 +1,3 @@
    +defmodule BlockScoutWeb.Advertisement.TextAdView do
    +  use BlockScoutWeb, :view
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/eth_rpc/view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/eth_rpc/view.ex
    new file mode 100644
    index 0000000..0e22ee5
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/eth_rpc/view.ex
    @@ -0,0 +1,116 @@
    +defmodule BlockScoutWeb.API.EthRPC.View do
    +  @moduledoc """
    +  Views for /eth-rpc API endpoints
    +  """
    +  use BlockScoutWeb, :view
    +
    +  @jsonrpc_2_0 ~s("jsonrpc":"2.0")
    +
    +  defstruct [:result, :id, :error]
    +
    +  def render("show.json", %{result: result, id: id}) do
    +    %__MODULE__{
    +      result: result,
    +      id: id
    +    }
    +  end
    +
    +  def render("error.json", %{error: message, id: id}) do
    +    %__MODULE__{
    +      error: message,
    +      id: id
    +    }
    +  end
    +
    +  def render("response.json", %{response: %{error: error, id: id}}) do
    +    %__MODULE__{
    +      error: error,
    +      id: id
    +    }
    +  end
    +
    +  def render("response.json", %{response: %{result: result, id: id}}) do
    +    %__MODULE__{
    +      result: result,
    +      id: id
    +    }
    +  end
    +
    +  def render("responses.json", %{responses: responses}) do
    +    Enum.map(responses, fn
    +      %{error: error, id: id} ->
    +        %__MODULE__{
    +          error: error,
    +          id: id
    +        }
    +
    +      %{result: result, id: id} ->
    +        %__MODULE__{
    +          result: result,
    +          id: id
    +        }
    +    end)
    +  end
    +
    +  @doc """
    +  Encodes id into JSON string
    +  """
    +  @spec sanitize_id(any()) :: non_neg_integer() | String.t()
    +  def sanitize_id(id) do
    +    if is_integer(id), do: id, else: "\"#{id}\""
    +  end
    +
    +  @doc """
    +  Encodes error into JSON string
    +  """
    +  @spec sanitize_error(any(), :jason | :poison) :: String.t()
    +  def sanitize_error(error, json_encoder) do
    +    case json_encoder do
    +      :jason -> if is_map(error), do: Jason.encode!(error), else: "\"#{error}\""
    +      :poison -> if is_map(error), do: Poison.encode!(error), else: "\"#{error}\""
    +    end
    +  end
    +
    +  @doc """
    +  Pass "jsonrpc":"2.0" to use in Poison.Encoder and Jason.Encoder below
    +  """
    +  @spec jsonrpc_2_0() :: String.t()
    +  def jsonrpc_2_0, do: @jsonrpc_2_0
    +
    +  defimpl Poison.Encoder, for: BlockScoutWeb.API.EthRPC.View do
    +    alias BlockScoutWeb.API.EthRPC.View
    +
    +    def encode(%View{result: result, id: id, error: error}, _options) when is_nil(error) do
    +      result = Poison.encode!(result)
    +
    +      """
    +      {#{View.jsonrpc_2_0()},"result": #{result},"id": #{View.sanitize_id(id)}}
    +      """
    +    end
    +
    +    def encode(%View{id: id, error: error}, _options) do
    +      """
    +      {#{View.jsonrpc_2_0()},"error": #{View.sanitize_error(error, :poison)},"id": #{View.sanitize_id(id)}}
    +      """
    +    end
    +  end
    +
    +  defimpl Jason.Encoder, for: BlockScoutWeb.API.EthRPC.View do
    +    # credo:disable-for-next-line
    +    alias BlockScoutWeb.API.EthRPC.View
    +
    +    def encode(%View{result: result, id: id, error: error}, _options) when is_nil(error) do
    +      result = Jason.encode!(result)
    +
    +      """
    +      {#{View.jsonrpc_2_0()},"result": #{result},"id": #{View.sanitize_id(id)}}
    +      """
    +    end
    +
    +    def encode(%View{id: id, error: error}, _options) do
    +      """
    +      {#{View.jsonrpc_2_0()},"error": #{View.sanitize_error(error, :jason)},"id": #{View.sanitize_id(id)}}
    +      """
    +    end
    +  end
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/rpc/address_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/rpc/address_view.ex
    new file mode 100644
    index 0000000..dc7f394
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/rpc/address_view.ex
    @@ -0,0 +1,271 @@
    +defmodule BlockScoutWeb.API.RPC.AddressView do
    +  use BlockScoutWeb, :view
    +
    +  alias BlockScoutWeb.API.EthRPC.View, as: EthRPCView
    +  alias BlockScoutWeb.API.RPC.RPCView
    +  alias Explorer.Chain.DenormalizationHelper
    +
    +  def render("listaccounts.json", %{accounts: accounts}) do
    +    accounts = Enum.map(accounts, &prepare_account/1)
    +    RPCView.render("show.json", data: accounts)
    +  end
    +
    +  def render("balance.json", %{addresses: [address]}) do
    +    RPCView.render("show.json", data: balance(address))
    +  end
    +
    +  def render("balance.json", assigns) do
    +    render("balancemulti.json", assigns)
    +  end
    +
    +  def render("balancemulti.json", %{addresses: addresses}) do
    +    data = Enum.map(addresses, &render_address/1)
    +
    +    RPCView.render("show.json", data: data)
    +  end
    +
    +  def render("pendingtxlist.json", %{transactions: transactions}) do
    +    data = Enum.map(transactions, &prepare_pending_transaction/1)
    +    RPCView.render("show.json", data: data)
    +  end
    +
    +  def render("txlist.json", %{transactions: transactions}) do
    +    data = Enum.map(transactions, &prepare_transaction/1)
    +    RPCView.render("show.json", data: data)
    +  end
    +
    +  def render("txlistinternal.json", %{internal_transactions: internal_transactions}) do
    +    data = Enum.map(internal_transactions, &prepare_internal_transaction/1)
    +    RPCView.render("show.json", data: data)
    +  end
    +
    +  def render("tokentx.json", %{token_transfers: token_transfers}) do
    +    data = Enum.map(token_transfers, &prepare_token_transfer/1)
    +    RPCView.render("show.json", data: data)
    +  end
    +
    +  def render("tokennfttx.json", %{token_transfers: token_transfers, max_block_number: max_block_number}) do
    +    data = Enum.map(token_transfers, &prepare_nft_transfer(&1, max_block_number))
    +    RPCView.render("show.json", data: data)
    +  end
    +
    +  def render("tokenbalance.json", %{token_balance: token_balance}) do
    +    RPCView.render("show.json", data: to_string(token_balance))
    +  end
    +
    +  def render("token_list.json", %{token_list: token_list}) do
    +    data = Enum.map(token_list, &prepare_token/1)
    +    RPCView.render("show.json", data: data)
    +  end
    +
    +  def render("getminedblocks.json", %{blocks: blocks}) do
    +    data = Enum.map(blocks, &prepare_block/1)
    +    RPCView.render("show.json", data: data)
    +  end
    +
    +  def render("eth_get_balance.json", %{balance: balance}) do
    +    EthRPCView.render("show.json", %{result: balance, id: 0})
    +  end
    +
    +  def render("eth_get_balance_error.json", %{error: message}) do
    +    EthRPCView.render("error.json", %{error: message, id: 0})
    +  end
    +
    +  def render("error.json", assigns) do
    +    RPCView.render("error.json", assigns)
    +  end
    +
    +  defp render_address(address) do
    +    %{
    +      "account" => "#{address.hash}",
    +      "balance" => balance(address),
    +      "stale" => address.stale? || false
    +    }
    +  end
    +
    +  defp prepare_account(address) do
    +    %{
    +      "balance" => to_string(address.fetched_coin_balance && address.fetched_coin_balance.value),
    +      "address" => to_string(address.hash),
    +      "stale" => address.stale? || false
    +    }
    +  end
    +
    +  defp prepare_pending_transaction(transaction) do
    +    %{
    +      "hash" => "#{transaction.hash}",
    +      "nonce" => "#{transaction.nonce}",
    +      "from" => "#{transaction.from_address_hash}",
    +      "to" => "#{transaction.to_address_hash}",
    +      "value" => "#{transaction.value.value}",
    +      "gas" => "#{transaction.gas}",
    +      "gasPrice" => "#{transaction.gas_price.value}",
    +      "input" => "#{transaction.input}",
    +      "contractAddress" => "#{transaction.created_contract_address_hash}",
    +      "cumulativeGasUsed" => "#{transaction.cumulative_gas_used}",
    +      "gasUsed" => "#{transaction.gas_used}"
    +    }
    +  end
    +
    +  defp prepare_transaction(transaction) do
    +    %{
    +      "blockNumber" => "#{transaction.block_number}",
    +      "timeStamp" => "#{DateTime.to_unix(transaction.block_timestamp)}",
    +      "hash" => "#{transaction.hash}",
    +      "nonce" => "#{transaction.nonce}",
    +      "blockHash" => "#{transaction.block_hash}",
    +      "transactionIndex" => "#{transaction.index}",
    +      "from" => "#{transaction.from_address_hash}",
    +      "to" => "#{transaction.to_address_hash}",
    +      "value" => "#{transaction.value.value}",
    +      "gas" => "#{transaction.gas}",
    +      "gasPrice" => "#{transaction.gas_price && transaction.gas_price.value}",
    +      "isError" => if(transaction.status == :ok, do: "0", else: "1"),
    +      "txreceipt_status" => if(transaction.status == :ok, do: "1", else: "0"),
    +      "input" => "#{transaction.input}",
    +      "contractAddress" => "#{transaction.created_contract_address_hash}",
    +      "cumulativeGasUsed" => "#{transaction.cumulative_gas_used}",
    +      "gasUsed" => "#{transaction.gas_used}",
    +      "confirmations" => "#{transaction.confirmations}"
    +    }
    +  end
    +
    +  defp prepare_internal_transaction(internal_transaction) do
    +    %{
    +      "blockNumber" => "#{internal_transaction.block_number}",
    +      "timeStamp" => "#{DateTime.to_unix(internal_transaction.block_timestamp)}",
    +      "from" => "#{internal_transaction.from_address_hash}",
    +      "to" => "#{internal_transaction.to_address_hash}",
    +      "value" => "#{internal_transaction.value.value}",
    +      "contractAddress" => "#{internal_transaction.created_contract_address_hash}",
    +      "transactionHash" => to_string(internal_transaction.transaction_hash),
    +      "index" => to_string(internal_transaction.index),
    +      "input" => "#{internal_transaction.input}",
    +      "type" => "#{internal_transaction.type}",
    +      "callType" => "#{internal_transaction.call_type}",
    +      "gas" => "#{internal_transaction.gas}",
    +      "gasUsed" => "#{internal_transaction.gas_used}",
    +      "isError" => if(internal_transaction.error, do: "1", else: "0"),
    +      "errCode" => "#{internal_transaction.error}"
    +    }
    +  end
    +
    +  defp prepare_common_token_transfer(token_transfer) do
    +    %{
    +      "blockNumber" => to_string(token_transfer.block_number),
    +      "timeStamp" => to_string(DateTime.to_unix(token_transfer.block_timestamp)),
    +      "hash" => to_string(token_transfer.transaction_hash),
    +      "nonce" => to_string(token_transfer.transaction_nonce),
    +      "blockHash" => to_string(token_transfer.block_hash),
    +      "from" => to_string(token_transfer.from_address_hash),
    +      "contractAddress" => to_string(token_transfer.token_contract_address_hash),
    +      "to" => to_string(token_transfer.to_address_hash),
    +      "logIndex" => to_string(token_transfer.token_log_index),
    +      "tokenName" => token_transfer.token_name,
    +      "tokenSymbol" => token_transfer.token_symbol,
    +      "tokenDecimal" => to_string(token_transfer.token_decimals),
    +      "transactionIndex" => to_string(token_transfer.transaction_index),
    +      "gas" => to_string(token_transfer.transaction_gas),
    +      "gasPrice" => to_string(token_transfer.transaction_gas_price && token_transfer.transaction_gas_price.value),
    +      "gasUsed" => to_string(token_transfer.transaction_gas_used),
    +      "cumulativeGasUsed" => to_string(token_transfer.transaction_cumulative_gas_used),
    +      "input" => to_string(token_transfer.transaction_input),
    +      "confirmations" => to_string(token_transfer.confirmations)
    +    }
    +  end
    +
    +  defp prepare_token_transfer(%{token_type: "ERC-721"} = token_transfer) do
    +    token_transfer
    +    |> prepare_common_token_transfer()
    +    |> Map.put_new(:tokenID, List.first(token_transfer.token_ids))
    +  end
    +
    +  # todo: Mark tokenID field as deprecated in release notes, and delete it in the next release
    +  # when tokenID will be deleted, merge this, and next `prepare_token_transfer/1` clauses
    +  defp prepare_token_transfer(%{token_type: "ERC-1155", token_ids: [token_id]} = token_transfer) do
    +    token_transfer
    +    |> prepare_common_token_transfer()
    +    |> Map.put_new(:tokenID, token_id)
    +    |> Map.put_new(:tokenIDs, token_transfer.token_ids)
    +    |> Map.put_new(:values, token_transfer.amounts)
    +  end
    +
    +  defp prepare_token_transfer(%{token_type: "ERC-1155"} = token_transfer) do
    +    token_transfer
    +    |> prepare_common_token_transfer()
    +    |> Map.put_new(:tokenIDs, token_transfer.token_ids)
    +    |> Map.put_new(:values, token_transfer.amounts)
    +  end
    +
    +  defp prepare_token_transfer(%{token_type: "ERC-404"} = token_transfer) do
    +    token_transfer
    +    |> prepare_common_token_transfer()
    +    |> Map.put_new(:tokenIDs, token_transfer.token_ids)
    +    |> Map.put_new(:values, token_transfer.amounts)
    +  end
    +
    +  defp prepare_token_transfer(%{token_type: "ERC-20"} = token_transfer) do
    +    token_transfer
    +    |> prepare_common_token_transfer()
    +    |> Map.put_new(:value, to_string(token_transfer.amount))
    +  end
    +
    +  defp prepare_token_transfer(token_transfer) do
    +    prepare_common_token_transfer(token_transfer)
    +  end
    +
    +  defp prepare_nft_transfer(token_transfer, max_block_number) do
    +    timestamp =
    +      if DenormalizationHelper.tt_denormalization_finished?() do
    +        to_string(DateTime.to_unix(token_transfer.transaction.block_timestamp))
    +      else
    +        to_string(DateTime.to_unix(token_transfer.block.timestamp))
    +      end
    +
    +    %{
    +      "blockNumber" => to_string(token_transfer.block_number),
    +      "timeStamp" => timestamp,
    +      "hash" => to_string(token_transfer.transaction_hash),
    +      "nonce" => to_string(token_transfer.transaction.nonce),
    +      "blockHash" => to_string(token_transfer.block_hash),
    +      "from" => to_string(token_transfer.from_address_hash),
    +      "contractAddress" => to_string(token_transfer.token_contract_address_hash),
    +      "to" => to_string(token_transfer.to_address_hash),
    +      "tokenID" => to_string(List.first(token_transfer.token_ids)),
    +      "logIndex" => to_string(token_transfer.log_index),
    +      "tokenName" => token_transfer.token.name,
    +      "tokenSymbol" => token_transfer.token.symbol,
    +      "tokenDecimal" => to_string(token_transfer.token.decimals || 0),
    +      "transactionIndex" => to_string(token_transfer.transaction.index),
    +      "gas" => to_string(token_transfer.transaction.gas),
    +      "gasPrice" => to_string(token_transfer.transaction.gas_price && token_transfer.transaction.gas_price.value),
    +      "gasUsed" => to_string(token_transfer.transaction.gas_used),
    +      "cumulativeGasUsed" => to_string(token_transfer.transaction.cumulative_gas_used),
    +      "input" => "deprecated",
    +      "confirmations" => to_string(max_block_number - token_transfer.block_number)
    +    }
    +  end
    +
    +  defp prepare_block(block) do
    +    %{
    +      "blockNumber" => to_string(block.number),
    +      "timeStamp" => to_string(block.timestamp)
    +    }
    +  end
    +
    +  defp prepare_token(token) do
    +    %{
    +      "balance" => to_string(token.balance),
    +      "contractAddress" => to_string(token.contract_address_hash),
    +      "name" => token.name,
    +      "decimals" => to_string(token.decimals),
    +      "symbol" => token.symbol,
    +      "type" => token.type
    +    }
    +    |> (&if(is_nil(token.id), do: &1, else: Map.put(&1, "id", token.id))).()
    +  end
    +
    +  defp balance(address) do
    +    address.fetched_coin_balance && address.fetched_coin_balance.value && "#{address.fetched_coin_balance.value}"
    +  end
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/rpc/block_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/rpc/block_view.ex
    new file mode 100644
    index 0000000..b0262aa
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/rpc/block_view.ex
    @@ -0,0 +1,97 @@
    +defmodule BlockScoutWeb.API.RPC.BlockView do
    +  use BlockScoutWeb, :view
    +
    +  alias BlockScoutWeb.API.EthRPC.View, as: EthRPCView
    +  alias BlockScoutWeb.API.RPC.RPCView
    +  alias Explorer.Chain.{Block, Hash, Wei}
    +  alias Explorer.EthRPC, as: EthRPC
    +
    +  def render("block_reward.json", %{block: %Block{rewards: [_ | _]} = block}) do
    +    reward_as_string =
    +      block.rewards
    +      |> Enum.find(%{reward: %Wei{value: Decimal.new(0)}}, &(&1.address_type == :validator))
    +      |> Map.get(:reward)
    +      |> Wei.to(:wei)
    +      |> Decimal.to_string(:normal)
    +
    +    static_reward =
    +      block.rewards
    +      |> Enum.find(%{reward: %Wei{value: Decimal.new(0)}}, &(&1.address_type == :emission_funds))
    +      |> Map.get(:reward)
    +      |> Wei.to(:wei)
    +
    +    uncles =
    +      block.rewards
    +      |> Stream.filter(&(&1.address_type == :uncle))
    +      |> Stream.with_index()
    +      |> Enum.map(fn {reward, index} ->
    +        %{
    +          "unclePosition" => to_string(index),
    +          "miner" => Hash.to_string(reward.address_hash),
    +          "blockreward" => reward.reward |> Wei.to(:wei) |> Decimal.to_string(:normal)
    +        }
    +      end)
    +
    +    data = %{
    +      "blockNumber" => to_string(block.number),
    +      "timeStamp" => DateTime.to_unix(block.timestamp),
    +      "blockMiner" => Hash.to_string(block.miner_hash),
    +      "blockReward" => reward_as_string,
    +      "uncles" => uncles,
    +      "uncleInclusionReward" =>
    +        static_reward
    +        |> Decimal.mult(Enum.count(uncles))
    +        |> Decimal.div(Block.uncle_reward_coef())
    +        |> Decimal.to_string(:normal)
    +    }
    +
    +    RPCView.render("show.json", data: data)
    +  end
    +
    +  def render("block_reward.json", %{block: block}) do
    +    data = %{
    +      "blockNumber" => to_string(block.number),
    +      "timeStamp" => DateTime.to_unix(block.timestamp),
    +      "blockMiner" => Hash.to_string(block.miner_hash),
    +      "blockReward" => "0",
    +      "uncles" => [],
    +      "uncleInclusionReward" => "0"
    +    }
    +
    +    RPCView.render("show.json", data: data)
    +  end
    +
    +  def render("block_countdown.json", %{
    +        current_block: current_block,
    +        countdown_block: countdown_block,
    +        remaining_blocks: remaining_blocks,
    +        estimated_time_in_sec: estimated_time_in_sec
    +      }) do
    +    data = %{
    +      "CurrentBlock" => to_string(current_block),
    +      "CountdownBlock" => to_string(countdown_block),
    +      "RemainingBlock" => to_string(remaining_blocks),
    +      "EstimateTimeInSec" => to_string(estimated_time_in_sec)
    +    }
    +
    +    RPCView.render("show.json", data: data)
    +  end
    +
    +  def render("getblocknobytime.json", %{block_number: block_number}) do
    +    data = %{
    +      "blockNumber" => to_string(block_number)
    +    }
    +
    +    RPCView.render("show.json", data: data)
    +  end
    +
    +  def render("eth_block_number.json", %{number: number, id: id}) do
    +    result = EthRPC.encode_quantity(number)
    +
    +    EthRPCView.render("show.json", %{result: result, id: id})
    +  end
    +
    +  def render("error.json", %{error: error}) do
    +    RPCView.render("error.json", error: error)
    +  end
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/rpc/contract_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/rpc/contract_view.ex
    new file mode 100644
    index 0000000..02cbbe6
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/rpc/contract_view.ex
    @@ -0,0 +1,252 @@
    +defmodule BlockScoutWeb.API.RPC.ContractView do
    +  use BlockScoutWeb, :view
    +
    +  alias BlockScoutWeb.AddressView
    +  alias BlockScoutWeb.API.RPC.RPCView
    +  alias BlockScoutWeb.API.V2.Helper, as: APIV2Helper
    +  alias Explorer.Chain.{Address, SmartContract}
    +
    +  defguardp is_empty_string(input) when input == "" or input == nil
    +
    +  def render("getcontractcreation.json", %{addresses: addresses}) do
    +    contracts = addresses |> Enum.map(&address_to_response/1) |> Enum.reject(&is_nil/1)
    +
    +    RPCView.render("show.json", data: contracts)
    +  end
    +
    +  def render("listcontracts.json", %{contracts: contracts}) do
    +    contracts = Enum.map(contracts, &prepare_contract/1)
    +
    +    RPCView.render("show.json", data: contracts)
    +  end
    +
    +  def render("getabi.json", %{abi: abi}) do
    +    RPCView.render("show.json", data: Jason.encode!(abi))
    +  end
    +
    +  def render("getsourcecode.json", %{contract: contract}) do
    +    RPCView.render("show.json", data: [prepare_source_code_contract(contract)])
    +  end
    +
    +  def render("error.json", assigns) do
    +    RPCView.render("error.json", assigns)
    +  end
    +
    +  def render("verify.json", %{contract: contract}) do
    +    RPCView.render("show.json", data: prepare_source_code_contract(contract))
    +  end
    +
    +  def render("show.json", %{result: result}) do
    +    RPCView.render("show.json", data: result)
    +  end
    +
    +  defp prepare_source_code_contract(address) do
    +    contract = address.smart_contract || %{}
    +
    +    optimization = Map.get(contract, :optimization, "")
    +
    +    contract_output = %{
    +      "Address" => to_string(address.hash)
    +    }
    +
    +    contract_output
    +    |> set_optimization_runs(contract, optimization)
    +    |> set_constructor_arguments(contract)
    +    |> set_external_libraries(contract)
    +    |> set_verified_contract_data(contract, address, optimization)
    +    |> set_proxy_info(contract)
    +    |> set_compiler_settings(contract)
    +  end
    +
    +  defp set_compiler_settings(contract_output, contract) when contract == %{}, do: contract_output
    +
    +  defp set_compiler_settings(contract_output, contract) do
    +    if is_nil(contract.compiler_settings) do
    +      contract_output
    +    else
    +      contract_output
    +      |> Map.put(:CompilerSettings, contract.compiler_settings)
    +    end
    +  end
    +
    +  defp set_proxy_info(contract_output, contract) when contract == %{} do
    +    contract_output
    +  end
    +
    +  defp set_proxy_info(contract_output, contract) do
    +    result =
    +      if contract.is_proxy do
    +        implementation_address_hash_string = List.first(contract.implementation_address_hash_strings)
    +
    +        # todo: `ImplementationAddress` is kept for backward compatibility,
    +        # remove when clients unbound from these props
    +        contract_output
    +        |> Map.put_new(:ImplementationAddress, implementation_address_hash_string)
    +        |> Map.put_new(:ImplementationAddresses, contract.implementation_address_hash_strings)
    +      else
    +        contract_output
    +      end
    +
    +    is_proxy_string = if contract.is_proxy, do: "true", else: "false"
    +
    +    result
    +    |> Map.put_new(:IsProxy, is_proxy_string)
    +  end
    +
    +  defp set_optimization_runs(contract_output, contract, optimization) do
    +    optimization_runs = Map.get(contract, :optimization_runs, "")
    +
    +    if optimization && optimization != "" do
    +      contract_output
    +      |> Map.put_new(:OptimizationRuns, optimization_runs)
    +    else
    +      contract_output
    +    end
    +  end
    +
    +  defp set_constructor_arguments(contract_output, %{constructor_arguments: arguments}) when is_empty_string(arguments),
    +    do: contract_output
    +
    +  defp set_constructor_arguments(contract_output, %{constructor_arguments: arguments}) do
    +    contract_output
    +    |> Map.put_new(:ConstructorArguments, arguments)
    +  end
    +
    +  defp set_constructor_arguments(contract_output, _), do: contract_output
    +
    +  defp set_external_libraries(contract_output, contract) do
    +    external_libraries = Map.get(contract, :external_libraries, [])
    +
    +    if Enum.empty?(external_libraries) do
    +      contract_output
    +    else
    +      external_libraries_without_id =
    +        Enum.map(external_libraries, fn %{name: name, address_hash: address_hash} ->
    +          %{"name" => name, "address_hash" => address_hash}
    +        end)
    +
    +      contract_output
    +      |> Map.put_new(:ExternalLibraries, external_libraries_without_id)
    +    end
    +  end
    +
    +  defp set_verified_contract_data(contract_output, contract, address, optimization) do
    +    contract_abi =
    +      if is_nil(address.smart_contract) do
    +        "Contract source code not verified"
    +      else
    +        Jason.encode!(contract.abi)
    +      end
    +
    +    contract_optimization =
    +      case optimization do
    +        true ->
    +          "true"
    +
    +        false ->
    +          "false"
    +
    +        "" ->
    +          ""
    +      end
    +
    +    if Map.equal?(contract, %{}) do
    +      contract_output
    +    else
    +      contract_output
    +      |> Map.put_new(:SourceCode, Map.get(contract, :contract_source_code, ""))
    +      |> Map.put_new(:ABI, contract_abi)
    +      |> Map.put_new(:ContractName, Map.get(contract, :name, ""))
    +      |> Map.put_new(:CompilerVersion, Map.get(contract, :compiler_version, ""))
    +      |> Map.put_new(:OptimizationUsed, contract_optimization)
    +      |> Map.put_new(:EVMVersion, Map.get(contract, :evm_version, ""))
    +      |> Map.put_new(:FileName, Map.get(contract, :file_path, "") || "")
    +      |> insert_additional_sources(address)
    +      |> add_zksync_info(contract)
    +    end
    +  end
    +
    +  defp add_zksync_info(smart_contract_info, contract) do
    +    if Application.get_env(:explorer, :chain_type) == :zksync do
    +      smart_contract_info
    +      |> Map.put_new(:ZkCompilerVersion, Map.get(contract, :zk_compiler_version, ""))
    +    else
    +      smart_contract_info
    +    end
    +  end
    +
    +  defp insert_additional_sources(output, address) do
    +    bytecode_twin_smart_contract = SmartContract.get_address_verified_bytecode_twin_contract(address)
    +
    +    additional_sources_from_bytecode_twin =
    +      bytecode_twin_smart_contract && bytecode_twin_smart_contract.smart_contract_additional_sources
    +
    +    additional_sources =
    +      if APIV2Helper.smart_contract_verified?(address),
    +        do: address.smart_contract.smart_contract_additional_sources,
    +        else: additional_sources_from_bytecode_twin
    +
    +    additional_sources_array =
    +      if additional_sources,
    +        do:
    +          Enum.map(additional_sources, fn src ->
    +            %{
    +              Filename: src.file_name,
    +              SourceCode: src.contract_source_code
    +            }
    +          end),
    +        else: []
    +
    +    if additional_sources_array == [],
    +      do: output,
    +      else: Map.put_new(output, :AdditionalSources, additional_sources_array)
    +  end
    +
    +  defp prepare_contract(%Address{
    +         hash: hash,
    +         smart_contract: nil
    +       }) do
    +    %{
    +      "Address" => to_string(hash),
    +      "ABI" => "Contract source code not verified"
    +    }
    +  end
    +
    +  defp prepare_contract(%Address{
    +         hash: hash,
    +         smart_contract: %SmartContract{} = contract
    +       }) do
    +    smart_contract_info =
    +      %{
    +        "Address" => to_string(hash),
    +        "ABI" => Jason.encode!(contract.abi),
    +        "ContractName" => contract.name,
    +        "CompilerVersion" => contract.compiler_version,
    +        "OptimizationUsed" => if(contract.optimization, do: "1", else: "0")
    +      }
    +
    +    smart_contract_info
    +    |> merge_zksync_info(contract)
    +  end
    +
    +  defp merge_zksync_info(smart_contract_info, contract) do
    +    if Application.get_env(:explorer, :chain_type) == :zksync do
    +      smart_contract_info
    +      |> Map.merge(%{"ZkCompilerVersion" => contract.zk_compiler_version})
    +    else
    +      smart_contract_info
    +    end
    +  end
    +
    +  defp address_to_response(address) do
    +    creator_hash = AddressView.from_address_hash(address)
    +    creation_transaction = creator_hash && AddressView.transaction_hash(address)
    +
    +    creation_transaction &&
    +      %{
    +        "contractAddress" => to_string(address.hash),
    +        "contractCreator" => to_string(creator_hash),
    +        "txHash" => to_string(creation_transaction)
    +      }
    +  end
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/rpc/logs_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/rpc/logs_view.ex
    new file mode 100644
    index 0000000..e5bbe19
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/rpc/logs_view.ex
    @@ -0,0 +1,39 @@
    +defmodule BlockScoutWeb.API.RPC.LogsView do
    +  use BlockScoutWeb, :view
    +
    +  alias BlockScoutWeb.API.RPC.RPCView
    +  alias Explorer.Helper
    +
    +  def render("getlogs.json", %{logs: logs}) do
    +    data = Enum.map(logs, &prepare_log/1)
    +    RPCView.render("show.json", data: data)
    +  end
    +
    +  def render("error.json", assigns) do
    +    RPCView.render("error.json", assigns)
    +  end
    +
    +  defp prepare_log(log) do
    +    %{
    +      "address" => "#{log.address_hash}",
    +      "topics" => get_topics(log),
    +      "data" => "#{log.data}",
    +      "blockNumber" => Helper.integer_to_hex(log.block_number),
    +      "timeStamp" => Helper.datetime_to_hex(log.block_timestamp),
    +      "gasPrice" => Helper.decimal_to_hex(log.gas_price.value),
    +      "gasUsed" => Helper.decimal_to_hex(log.gas_used),
    +      "logIndex" => Helper.integer_to_hex(log.index),
    +      "transactionHash" => "#{log.transaction_hash}",
    +      "transactionIndex" => Helper.integer_to_hex(log.transaction_index)
    +    }
    +  end
    +
    +  defp get_topics(%{
    +         first_topic: first_topic,
    +         second_topic: second_topic,
    +         third_topic: third_topic,
    +         fourth_topic: fourth_topic
    +       }) do
    +    [first_topic, second_topic, third_topic, fourth_topic]
    +  end
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/rpc/rpc_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/rpc/rpc_view.ex
    new file mode 100644
    index 0000000..f877d94
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/rpc/rpc_view.ex
    @@ -0,0 +1,27 @@
    +defmodule BlockScoutWeb.API.RPC.RPCView do
    +  use BlockScoutWeb, :view
    +
    +  def render("show.json", %{data: data}) do
    +    %{
    +      "status" => "1",
    +      "message" => "OK",
    +      "result" => data
    +    }
    +  end
    +
    +  def render("show_value.json", %{data: data}) do
    +    {value, _} =
    +      data
    +      |> Float.parse()
    +
    +    value
    +  end
    +
    +  def render("error.json", %{error: message} = assigns) do
    +    %{
    +      "status" => "0",
    +      "message" => message,
    +      "result" => Map.get(assigns, :data)
    +    }
    +  end
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/rpc/stats_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/rpc/stats_view.ex
    new file mode 100644
    index 0000000..7b83e55
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/rpc/stats_view.ex
    @@ -0,0 +1,57 @@
    +defmodule BlockScoutWeb.API.RPC.StatsView do
    +  use BlockScoutWeb, :view
    +
    +  alias BlockScoutWeb.API.RPC.RPCView
    +
    +  def render("tokensupply.json", %{total_supply: token_supply}) do
    +    RPCView.render("show.json", data: token_supply)
    +  end
    +
    +  def render("ethsupplyexchange.json", %{total_supply: total_supply}) do
    +    RPCView.render("show.json", data: total_supply)
    +  end
    +
    +  def render("ethsupply.json", %{total_supply: total_supply}) do
    +    RPCView.render("show.json", data: total_supply)
    +  end
    +
    +  def render("coinsupply.json", %{total_supply: total_supply}) do
    +    RPCView.render("show_value.json", data: total_supply)
    +  end
    +
    +  def render("ethprice.json", %{rates: rates}) do
    +    RPCView.render("show.json", data: prepare_rates(rates, "eth"))
    +  end
    +
    +  def render("coinprice.json", %{rates: rates}) do
    +    RPCView.render("show.json", data: prepare_rates(rates, "coin_"))
    +  end
    +
    +  def render("totalfees.json", %{total_fees: total_fees}) do
    +    RPCView.render("show.json", data: total_fees)
    +  end
    +
    +  def render("error.json", assigns) do
    +    RPCView.render("error.json", assigns)
    +  end
    +
    +  defp prepare_rates(rates, prefix) do
    +    if rates do
    +      timestamp = rates.last_updated && rates.last_updated |> DateTime.to_unix() |> to_string()
    +
    +      %{
    +        (prefix <> "btc") => rates.btc_value && to_string(rates.btc_value),
    +        (prefix <> "btc_timestamp") => timestamp,
    +        (prefix <> "usd") => rates.fiat_value && to_string(rates.fiat_value),
    +        (prefix <> "usd_timestamp") => timestamp
    +      }
    +    else
    +      %{
    +        (prefix <> "btc") => nil,
    +        (prefix <> "btc_timestamp") => nil,
    +        (prefix <> "usd") => nil,
    +        (prefix <> "usd_timestamp") => nil
    +      }
    +    end
    +  end
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/rpc/token_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/rpc/token_view.ex
    new file mode 100644
    index 0000000..f2c8de8
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/rpc/token_view.ex
    @@ -0,0 +1,61 @@
    +defmodule BlockScoutWeb.API.RPC.TokenView do
    +  use BlockScoutWeb, :view
    +
    +  alias BlockScoutWeb.API.RPC.RPCView
    +  alias BlockScoutWeb.BridgedTokensView
    +  alias Explorer.Chain.CurrencyHelper
    +
    +  def render("gettoken.json", %{token: token}) do
    +    RPCView.render("show.json", data: prepare_token(token))
    +  end
    +
    +  def render("gettokenholders.json", %{token_holders: token_holders}) do
    +    data = Enum.map(token_holders, &prepare_token_holder/1)
    +    RPCView.render("show.json", data: data)
    +  end
    +
    +  def render("bridgedtokenlist.json", %{bridged_tokens: bridged_tokens}) do
    +    data = Enum.map(bridged_tokens, &prepare_bridged_token/1)
    +    RPCView.render("show.json", data: data)
    +  end
    +
    +  def render("error.json", assigns) do
    +    RPCView.render("error.json", assigns)
    +  end
    +
    +  defp prepare_token(token) do
    +    %{
    +      "type" => token.type,
    +      "name" => token.name,
    +      "symbol" => token.symbol,
    +      "totalSupply" => to_string(token.total_supply),
    +      "decimals" => to_string(token.decimals),
    +      "contractAddress" => to_string(token.contract_address_hash),
    +      "cataloged" => token.cataloged
    +    }
    +  end
    +
    +  defp prepare_token_holder(token_holder) do
    +    %{
    +      "address" => to_string(token_holder.address_hash),
    +      "value" => token_holder.value
    +    }
    +  end
    +
    +  defp prepare_bridged_token({token, bridged_token}) do
    +    total_supply = CurrencyHelper.divide_decimals(token.total_supply, token.decimals)
    +    usd_value = BridgedTokensView.bridged_token_usd_cap(bridged_token, token)
    +
    +    %{
    +      "foreignChainId" => bridged_token.foreign_chain_id,
    +      "foreignTokenContractAddressHash" => bridged_token.foreign_token_contract_address_hash,
    +      "homeContractAddressHash" => token.contract_address_hash,
    +      "homeDecimals" => token.decimals,
    +      "homeHolderCount" => if(token.holder_count, do: to_string(token.holder_count), else: "0"),
    +      "homeName" => token.name,
    +      "homeSymbol" => token.symbol,
    +      "homeTotalSupply" => total_supply,
    +      "homeUsdValue" => usd_value
    +    }
    +  end
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/rpc/transaction_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/rpc/transaction_view.ex
    new file mode 100644
    index 0000000..74c8ba5
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/rpc/transaction_view.ex
    @@ -0,0 +1,91 @@
    +defmodule BlockScoutWeb.API.RPC.TransactionView do
    +  use BlockScoutWeb, :view
    +
    +  alias BlockScoutWeb.API.RPC.RPCView
    +  alias Explorer.Chain.Transaction
    +
    +  def render("gettxinfo.json", %{
    +        transaction: transaction,
    +        block_height: block_height,
    +        logs: logs,
    +        next_page_params: next_page_params
    +      }) do
    +    data = prepare_transaction(transaction, block_height, logs, next_page_params)
    +    RPCView.render("show.json", data: data)
    +  end
    +
    +  def render("gettxreceiptstatus.json", %{status: status}) do
    +    prepared_status = prepare_transaction_receipt_status(status)
    +    RPCView.render("show.json", data: %{"status" => prepared_status})
    +  end
    +
    +  def render("getstatus.json", %{error: error}) do
    +    RPCView.render("show.json", data: prepare_error(error))
    +  end
    +
    +  def render("error.json", assigns) do
    +    RPCView.render("error.json", assigns)
    +  end
    +
    +  defp prepare_transaction_receipt_status(""), do: ""
    +
    +  defp prepare_transaction_receipt_status(nil), do: ""
    +
    +  defp prepare_transaction_receipt_status(:ok), do: "1"
    +
    +  defp prepare_transaction_receipt_status(_), do: "0"
    +
    +  defp prepare_error("") do
    +    %{
    +      "isError" => "0",
    +      "errDescription" => ""
    +    }
    +  end
    +
    +  defp prepare_error(error) when is_binary(error) do
    +    %{
    +      "isError" => "1",
    +      "errDescription" => error
    +    }
    +  end
    +
    +  defp prepare_error(error) when is_atom(error) do
    +    %{
    +      "isError" => "1",
    +      "errDescription" => error |> Atom.to_string() |> String.replace("_", " ")
    +    }
    +  end
    +
    +  defp prepare_transaction(transaction, block_height, logs, next_page_params) do
    +    %{
    +      "hash" => "#{transaction.hash}",
    +      "timeStamp" => "#{DateTime.to_unix(Transaction.block_timestamp(transaction))}",
    +      "blockNumber" => "#{transaction.block_number}",
    +      "confirmations" => "#{block_height - transaction.block_number}",
    +      "success" => if(transaction.status == :ok, do: true, else: false),
    +      "from" => "#{transaction.from_address_hash}",
    +      "to" => "#{transaction.to_address_hash}",
    +      "value" => "#{transaction.value.value}",
    +      "input" => "#{transaction.input}",
    +      "gasLimit" => "#{transaction.gas}",
    +      "gasUsed" => "#{transaction.gas_used}",
    +      "gasPrice" => "#{transaction.gas_price.value}",
    +      "logs" => Enum.map(logs, &prepare_log/1),
    +      "revertReason" => "#{transaction.revert_reason}",
    +      "next_page_params" => next_page_params
    +    }
    +  end
    +
    +  defp prepare_log(log) do
    +    %{
    +      "address" => "#{log.address_hash}",
    +      "topics" => get_topics(log),
    +      "data" => "#{log.data}",
    +      "index" => "#{log.index}"
    +    }
    +  end
    +
    +  defp get_topics(log) do
    +    [log.first_topic, log.second_topic, log.third_topic, log.fourth_topic]
    +  end
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v1/supply_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v1/supply_view.ex
    new file mode 100644
    index 0000000..df76632
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v1/supply_view.ex
    @@ -0,0 +1,10 @@
    +defmodule BlockScoutWeb.API.V1.SupplyView do
    +  use BlockScoutWeb, :view
    +
    +  def render("supply.json", %{total: total_supply, circulating: circulating_supply}) do
    +    %{
    +      "total_supply" => total_supply,
    +      "circulating_supply" => circulating_supply
    +    }
    +  end
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v2/address_badge_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v2/address_badge_view.ex
    new file mode 100644
    index 0000000..4e17eda
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v2/address_badge_view.ex
    @@ -0,0 +1,35 @@
    +defmodule BlockScoutWeb.API.V2.AddressBadgeView do
    +  use BlockScoutWeb, :view
    +
    +  alias Explorer.Helper, as: ExplorerHelper
    +
    +  def render("badge_to_address.json", %{badge_to_address_list: badge_to_address_list, status: status}) do
    +    prepare_badge_to_address(badge_to_address_list, status)
    +  end
    +
    +  def render("badge_to_address.json", %{badge_to_address_list: badge_to_address_list}) do
    +    prepare_badge_to_address(badge_to_address_list)
    +  end
    +
    +  defp prepare_badge_to_address(badge_to_address_list) do
    +    %{
    +      badge_to_address_list: format_badge_to_address_list(badge_to_address_list)
    +    }
    +  end
    +
    +  defp prepare_badge_to_address(badge_to_address_list, status) do
    +    %{
    +      badge_to_address_list: format_badge_to_address_list(badge_to_address_list),
    +      status: status
    +    }
    +  end
    +
    +  defp format_badge_to_address_list(badge_to_address_list) do
    +    badge_to_address_list
    +    |> Enum.map(fn badge_to_address ->
    +      %{
    +        address_hash: ExplorerHelper.add_0x_prefix(badge_to_address.address_hash)
    +      }
    +    end)
    +  end
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v2/address_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v2/address_view.ex
    new file mode 100644
    index 0000000..40cd38c
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v2/address_view.ex
    @@ -0,0 +1,283 @@
    +defmodule BlockScoutWeb.API.V2.AddressView do
    +  use BlockScoutWeb, :view
    +  use Utils.CompileTimeEnvHelper, chain_type: [:explorer, :chain_type]
    +
    +  import BlockScoutWeb.Account.AuthController, only: [current_user: 1]
    +
    +  alias BlockScoutWeb.AddressView
    +  alias BlockScoutWeb.API.V2.{ApiView, Helper, TokenView}
    +  alias Explorer.{Chain, Market}
    +  alias Explorer.Chain.Address
    +  alias Explorer.Chain.Address.Counters
    +  alias Explorer.Chain.Token.Instance
    +
    +  @api_true [api?: true]
    +
    +  def render("message.json", assigns) do
    +    ApiView.render("message.json", assigns)
    +  end
    +
    +  def render("address.json", %{address: address, conn: conn}) do
    +    prepare_address(address, conn)
    +  end
    +
    +  def render("token_balances.json", %{token_balances: token_balances}) do
    +    Enum.map(token_balances, &prepare_token_balance/1)
    +  end
    +
    +  def render("coin_balance.json", %{coin_balance: coin_balance}) do
    +    prepare_coin_balance_history_entry(coin_balance)
    +  end
    +
    +  def render("coin_balances.json", %{coin_balances: coin_balances, next_page_params: next_page_params}) do
    +    %{"items" => Enum.map(coin_balances, &prepare_coin_balance_history_entry/1), "next_page_params" => next_page_params}
    +  end
    +
    +  def render("coin_balances_by_day.json", %{coin_balances_by_day: coin_balances_by_day}) do
    +    %{
    +      :items => Enum.map(coin_balances_by_day, &prepare_coin_balance_history_by_day_entry/1),
    +      :days =>
    +        Application.get_env(:block_scout_web, BlockScoutWeb.Chain.Address.CoinBalance)[:coin_balance_history_days]
    +    }
    +  end
    +
    +  def render("tokens.json", %{tokens: tokens, next_page_params: next_page_params}) do
    +    %{"items" => Enum.map(tokens, &prepare_token_balance(&1, true)), "next_page_params" => next_page_params}
    +  end
    +
    +  def render("addresses.json", %{
    +        addresses: addresses,
    +        next_page_params: next_page_params,
    +        exchange_rate: exchange_rate,
    +        total_supply: total_supply
    +      }) do
    +    %{
    +      items: Enum.map(addresses, &prepare_address_for_list/1),
    +      next_page_params: next_page_params,
    +      exchange_rate: exchange_rate.fiat_value,
    +      total_supply: total_supply && to_string(total_supply)
    +    }
    +  end
    +
    +  def render("nft_list.json", %{token_instances: token_instances, token: token, next_page_params: next_page_params}) do
    +    %{"items" => Enum.map(token_instances, &prepare_nft(&1, token)), "next_page_params" => next_page_params}
    +  end
    +
    +  def render("nft_list.json", %{token_instances: token_instances, next_page_params: next_page_params}) do
    +    %{"items" => Enum.map(token_instances, &prepare_nft(&1)), "next_page_params" => next_page_params}
    +  end
    +
    +  def render("nft_collections.json", %{collections: nft_collections, next_page_params: next_page_params}) do
    +    %{"items" => Enum.map(nft_collections, &prepare_nft_collection(&1)), "next_page_params" => next_page_params}
    +  end
    +
    +  @doc """
    +  Prepares an address for display in the addresses list.
    +
    +  ## Parameters
    +    - address: Address struct containing:
    +      - `:hash` - address hash
    +      - `:fetched_coin_balance` - current coin balance
    +      - `:transactions_count` - number of transactions
    +
    +  ## Returns
    +    - Map containing:
    +      - `:hash` - address hash
    +      - `:coin_balance` - current coin balance value
    +      - `:transaction_count` - number of transactions as string
    +      - Additional address info fields from Helper.address_with_info/4
    +  """
    +  @spec prepare_address_for_list(Address.t()) :: map()
    +  def prepare_address_for_list(address) do
    +    nil
    +    |> Helper.address_with_info(address, address.hash, true)
    +    |> Map.put(:transactions_count, to_string(address.transactions_count))
    +    # todo: It should be removed in favour `transaction_count` property with the next release after 8.0.0
    +    |> Map.put(:transaction_count, to_string(address.transactions_count))
    +    |> Map.put(:coin_balance, if(address.fetched_coin_balance, do: address.fetched_coin_balance.value))
    +  end
    +
    +  @spec prepare_address(Address.t(), Plug.Conn.t()) :: map()
    +  defp prepare_address(address, conn) do
    +    base_info = Helper.address_with_info(conn, address, address.hash, true)
    +
    +    balance = address.fetched_coin_balance && address.fetched_coin_balance.value
    +    exchange_rate = Market.get_coin_exchange_rate().fiat_value
    +
    +    creation_transaction = Address.creation_transaction(address)
    +    creator_hash = creation_transaction && creation_transaction.from_address_hash
    +    creation_transaction_hash = creator_hash && AddressView.transaction_hash(address)
    +    token = address.token && TokenView.render("token.json", %{token: address.token})
    +
    +    extended_info =
    +      Map.merge(base_info, %{
    +        "creator_address_hash" => creator_hash && Address.checksum(creator_hash),
    +        "creation_transaction_hash" => creation_transaction_hash,
    +        "token" => token,
    +        "coin_balance" => balance,
    +        "exchange_rate" => exchange_rate,
    +        "block_number_balance_updated_at" => address.fetched_coin_balance_block_number,
    +        "has_validated_blocks" => Counters.check_if_validated_blocks_at_address(address.hash, @api_true),
    +        "has_logs" => Counters.check_if_logs_at_address(address.hash, @api_true),
    +        "has_tokens" => Counters.check_if_tokens_at_address(address.hash, @api_true),
    +        "has_token_transfers" => Counters.check_if_token_transfers_at_address(address.hash, @api_true),
    +        "watchlist_address_id" => Chain.select_watchlist_address_id(get_watchlist_id(conn), address.hash),
    +        "has_beacon_chain_withdrawals" => Counters.check_if_withdrawals_at_address(address.hash, @api_true)
    +      })
    +
    +    extended_info
    +    |> chain_type_fields(%{
    +      address: address,
    +      creation_transaction_from_address: creation_transaction && creation_transaction.from_address
    +    })
    +  end
    +
    +  @spec prepare_token_balance(Chain.Address.TokenBalance.t(), boolean()) :: map()
    +  defp prepare_token_balance(token_balance, fetch_token_instance? \\ false) do
    +    %{
    +      "value" => token_balance.value,
    +      "token" => TokenView.render("token.json", %{token: token_balance.token}),
    +      "token_id" => token_balance.token_id,
    +      "token_instance" =>
    +        if(fetch_token_instance? && token_balance.token_id,
    +          do:
    +            fetch_and_render_token_instance(
    +              token_balance.token_id,
    +              token_balance.token,
    +              token_balance.address_hash,
    +              token_balance
    +            )
    +        )
    +    }
    +  end
    +
    +  def prepare_coin_balance_history_entry(coin_balance) do
    +    %{
    +      "transaction_hash" => coin_balance.transaction_hash,
    +      "block_number" => coin_balance.block_number,
    +      "delta" => coin_balance.delta,
    +      "value" => coin_balance.value,
    +      "block_timestamp" => coin_balance.block_timestamp
    +    }
    +  end
    +
    +  def prepare_coin_balance_history_by_day_entry(coin_balance_by_day) do
    +    %{
    +      "date" => coin_balance_by_day.date,
    +      "value" => coin_balance_by_day.value
    +    }
    +  end
    +
    +  def get_watchlist_id(conn) do
    +    case current_user(conn) do
    +      %{watchlist_id: wl_id} ->
    +        wl_id
    +
    +      _ ->
    +        nil
    +    end
    +  end
    +
    +  defp prepare_nft(nft) do
    +    prepare_nft(nft, nft.token)
    +  end
    +
    +  defp prepare_nft(nft, token) do
    +    Map.merge(
    +      %{"token_type" => token.type, "value" => value(token.type, nft)},
    +      TokenView.prepare_token_instance(nft, token)
    +    )
    +  end
    +
    +  defp prepare_nft_collection(collection) do
    +    %{
    +      "token" => TokenView.render("token.json", token: collection.token),
    +      "amount" => string_or_null(collection.distinct_token_instances_count || collection.value),
    +      "token_instances" =>
    +        Enum.map(collection.preloaded_token_instances, fn instance ->
    +          prepare_nft_for_collection(collection.token.type, instance)
    +        end)
    +    }
    +  end
    +
    +  defp prepare_nft_for_collection(token_type, instance) do
    +    Map.merge(
    +      %{"token_type" => token_type, "value" => value(token_type, instance)},
    +      TokenView.prepare_token_instance(instance, nil)
    +    )
    +  end
    +
    +  defp value("ERC-721", _), do: "1"
    +  defp value(_, nft), do: nft.current_token_balance && to_string(nft.current_token_balance.value)
    +
    +  defp string_or_null(nil), do: nil
    +  defp string_or_null(other), do: to_string(other)
    +
    +  # TODO think about this approach mb refactor or mark deprecated for example.
    +  # Suggested solution: batch preload
    +  @spec fetch_and_render_token_instance(
    +          Decimal.t(),
    +          Ecto.Schema.belongs_to(Chain.Token.t()) | nil,
    +          Chain.Hash.Address.t(),
    +          Chain.Address.TokenBalance.t()
    +        ) :: map()
    +  def fetch_and_render_token_instance(token_id, token, address_hash, token_balance) do
    +    token_instance =
    +      case Instance.nft_instance_by_token_id_and_token_address(
    +             token_id,
    +             token.contract_address_hash,
    +             @api_true
    +           ) do
    +        # `%{hash: address_hash}` will match with `address_with_info(_, address_hash)` clause in `BlockScoutWeb.API.V2.Helper`
    +        {:ok, token_instance} ->
    +          %Instance{
    +            token_instance
    +            | owner: %{hash: address_hash},
    +              owner_address_hash: address_hash,
    +              current_token_balance: token_balance
    +          }
    +
    +        {:error, :not_found} ->
    +          %Instance{
    +            token_id: token_id,
    +            metadata: nil,
    +            owner: %Address{hash: address_hash},
    +            owner_address_hash: address_hash,
    +            current_token_balance: token_balance,
    +            token_contract_address_hash: token.contract_address_hash
    +          }
    +          |> Instance.put_is_unique(token, @api_true)
    +      end
    +
    +    TokenView.render("token_instance.json", %{
    +      token_instance: token_instance,
    +      token: token
    +    })
    +  end
    +
    +  @spec chain_type_fields(
    +          map(),
    +          %{address: Address.t(), creation_transaction_from_address: Address.t()}
    +        ) :: map()
    +  case @chain_type do
    +    :filecoin ->
    +      defp chain_type_fields(result, %{creation_transaction_from_address: creation_transaction_from_address}) do
    +        # credo:disable-for-next-line Credo.Check.Design.AliasUsage
    +        BlockScoutWeb.API.V2.FilecoinView.put_filecoin_robust_address(result, %{
    +          address: creation_transaction_from_address,
    +          field_prefix: "creator"
    +        })
    +      end
    +
    +    :zilliqa ->
    +      defp chain_type_fields(result, %{address: address}) do
    +        # credo:disable-for-next-line Credo.Check.Design.AliasUsage
    +        BlockScoutWeb.API.V2.ZilliqaView.extend_address_json_response(result, address)
    +      end
    +
    +    _ ->
    +      defp chain_type_fields(result, _params) do
    +        result
    +      end
    +  end
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v2/advanced_filter_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v2/advanced_filter_view.ex
    new file mode 100644
    index 0000000..4cd0ecd
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v2/advanced_filter_view.ex
    @@ -0,0 +1,188 @@
    +defmodule BlockScoutWeb.API.V2.AdvancedFilterView do
    +  use BlockScoutWeb, :view
    +
    +  alias BlockScoutWeb.API.V2.{Helper, TokenTransferView, TokenView}
    +  alias Explorer.Chain.{Address, Data, Transaction}
    +  alias Explorer.Helper, as: ExplorerHelper
    +  alias Explorer.Market
    +  alias Explorer.Market.MarketHistory
    +
    +  def render("advanced_filters.json", %{
    +        advanced_filters: advanced_filters,
    +        decoded_transactions: decoded_transactions,
    +        search_params: %{
    +          method_ids: method_ids,
    +          tokens: tokens
    +        },
    +        next_page_params: next_page_params
    +      }) do
    +    %{
    +      items:
    +        advanced_filters
    +        |> Enum.zip(decoded_transactions)
    +        |> Enum.map(fn {af, decoded_input} -> prepare_advanced_filter(af, decoded_input) end),
    +      search_params: prepare_search_params(method_ids, tokens),
    +      next_page_params: next_page_params
    +    }
    +  end
    +
    +  def render("methods.json", %{methods: methods}) do
    +    methods
    +  end
    +
    +  def to_csv_format(advanced_filters) do
    +    exchange_rate = Market.get_coin_exchange_rate()
    +
    +    date_to_prices =
    +      Enum.reduce(advanced_filters, %{}, fn af, acc ->
    +        date = DateTime.to_date(af.timestamp)
    +
    +        if Map.has_key?(acc, date) do
    +          acc
    +        else
    +          market_history = MarketHistory.price_at_date(date)
    +
    +          Map.put(
    +            acc,
    +            date,
    +            {market_history && market_history.opening_price, market_history && market_history.closing_price}
    +          )
    +        end
    +      end)
    +
    +    row_names = [
    +      "TxHash",
    +      "Type",
    +      "MethodId",
    +      "UtcTimestamp",
    +      "FromAddress",
    +      "ToAddress",
    +      "CreatedContractAddress",
    +      "Value",
    +      "TokenContractAddressHash",
    +      "TokenDecimals",
    +      "TokenSymbol",
    +      "BlockNumber",
    +      "Fee",
    +      "CurrentPrice",
    +      "TxDateOpeningPrice",
    +      "TxDateClosingPrice"
    +    ]
    +
    +    af_lists =
    +      advanced_filters
    +      |> Stream.map(fn advanced_filter ->
    +        method_id =
    +          case advanced_filter.input do
    +            %{bytes: <>} -> ExplorerHelper.add_0x_prefix(method_id)
    +            _ -> nil
    +          end
    +
    +        {opening_price, closing_price} = date_to_prices[DateTime.to_date(advanced_filter.timestamp)]
    +
    +        [
    +          to_string(advanced_filter.hash),
    +          advanced_filter.type,
    +          method_id,
    +          advanced_filter.timestamp,
    +          Address.checksum(advanced_filter.from_address_hash),
    +          Address.checksum(advanced_filter.to_address_hash),
    +          Address.checksum(advanced_filter.created_contract_address_hash),
    +          decimal_to_string_xsd(advanced_filter.value),
    +          if(advanced_filter.type != "coin_transfer",
    +            do: Address.checksum(advanced_filter.token_transfer.token.contract_address_hash),
    +            else: nil
    +          ),
    +          if(advanced_filter.type != "coin_transfer",
    +            do: decimal_to_string_xsd(advanced_filter.token_transfer.token.decimals),
    +            else: nil
    +          ),
    +          if(advanced_filter.type != "coin_transfer", do: advanced_filter.token_transfer.token.symbol, else: nil),
    +          advanced_filter.block_number,
    +          decimal_to_string_xsd(advanced_filter.fee),
    +          decimal_to_string_xsd(exchange_rate.fiat_value),
    +          decimal_to_string_xsd(opening_price),
    +          decimal_to_string_xsd(closing_price)
    +        ]
    +      end)
    +
    +    Stream.concat([row_names], af_lists)
    +  end
    +
    +  defp prepare_advanced_filter(advanced_filter, decoded_input) do
    +    %{
    +      hash: advanced_filter.hash,
    +      type: advanced_filter.type,
    +      method:
    +        if(advanced_filter.type != "coin_transfer",
    +          do:
    +            Transaction.method_name(
    +              %Transaction{
    +                to_address: %Address{
    +                  hash: advanced_filter.token_transfer.token.contract_address_hash,
    +                  contract_code: "0x" |> Data.cast() |> elem(1)
    +                },
    +                input: advanced_filter.input
    +              },
    +              decoded_input
    +            ),
    +          else:
    +            Transaction.method_name(
    +              %Transaction{to_address: advanced_filter.to_address, input: advanced_filter.input},
    +              decoded_input
    +            )
    +        ),
    +      from:
    +        Helper.address_with_info(
    +          nil,
    +          advanced_filter.from_address,
    +          advanced_filter.from_address_hash,
    +          false
    +        ),
    +      to:
    +        Helper.address_with_info(
    +          nil,
    +          advanced_filter.to_address,
    +          advanced_filter.to_address_hash,
    +          false
    +        ),
    +      created_contract:
    +        Helper.address_with_info(
    +          nil,
    +          advanced_filter.created_contract_address,
    +          advanced_filter.created_contract_address_hash,
    +          false
    +        ),
    +      value: advanced_filter.value,
    +      total:
    +        if(advanced_filter.type != "coin_transfer",
    +          do: TokenTransferView.prepare_token_transfer_total(advanced_filter.token_transfer),
    +          else: nil
    +        ),
    +      token:
    +        if(advanced_filter.type != "coin_transfer",
    +          do: TokenView.render("token.json", %{token: advanced_filter.token_transfer.token}),
    +          else: nil
    +        ),
    +      timestamp: advanced_filter.timestamp,
    +      block_number: advanced_filter.block_number,
    +      transaction_index: advanced_filter.transaction_index,
    +      internal_transaction_index: advanced_filter.internal_transaction_index,
    +      token_transfer_index: advanced_filter.token_transfer_index,
    +      token_transfer_batch_index: advanced_filter.token_transfer_batch_index,
    +      fee: advanced_filter.fee
    +    }
    +  end
    +
    +  defp prepare_search_params(method_ids, tokens) do
    +    tokens_map =
    +      Map.new(tokens, fn {contract_address_hash, token} ->
    +        {contract_address_hash, TokenView.render("token.json", %{token: token})}
    +      end)
    +
    +    %{methods: method_ids, tokens: tokens_map}
    +  end
    +
    +  defp decimal_to_string_xsd(nil), do: nil
    +  defp decimal_to_string_xsd(decimal), do: Decimal.to_string(decimal, :xsd)
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v2/api_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v2/api_view.ex
    new file mode 100644
    index 0000000..1fde984
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v2/api_view.ex
    @@ -0,0 +1,7 @@
    +defmodule BlockScoutWeb.API.V2.ApiView do
    +  def render("message.json", %{message: message}) do
    +    %{
    +      "message" => message
    +    }
    +  end
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v2/arbitrum_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v2/arbitrum_view.ex
    new file mode 100644
    index 0000000..005e54b
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v2/arbitrum_view.ex
    @@ -0,0 +1,732 @@
    +defmodule BlockScoutWeb.API.V2.ArbitrumView do
    +  use BlockScoutWeb, :view
    +
    +  alias BlockScoutWeb.API.V2.ApiView
    +  alias BlockScoutWeb.API.V2.Helper, as: APIV2Helper
    +  alias Explorer.Chain.{Block, Hash, Transaction, Wei}
    +  alias Explorer.Chain.Arbitrum.{L1Batch, LifecycleTransaction}
    +  alias Explorer.Chain.Arbitrum.Reader.API.Settlement, as: SettlementReader
    +
    +  @doc """
    +    Function to render error\\text responses for GET requests
    +    to `/api/v2/arbitrum/messages/claim/:position` endpoint.
    +  """
    +  def render("message.json", assigns) do
    +    ApiView.render("message.json", assigns)
    +  end
    +
    +  @doc """
    +    Function to render GET requests to `/api/v2/arbitrum/messages/:direction` endpoint.
    +  """
    +  @spec render(binary(), map()) :: map() | non_neg_integer()
    +  def render("arbitrum_messages.json", %{
    +        messages: messages,
    +        next_page_params: next_page_params
    +      }) do
    +    messages_out =
    +      messages
    +      |> Enum.map(fn msg ->
    +        %{
    +          "id" => msg.message_id,
    +          "origination_address_hash" => msg.originator_address,
    +          # todo: It should be removed in favour `origination_address_hash` property with the next release after 8.0.0
    +          "origination_address" => msg.originator_address,
    +          "origination_transaction_hash" => msg.originating_transaction_hash,
    +          "origination_timestamp" => msg.origination_timestamp,
    +          "origination_transaction_block_number" => msg.originating_transaction_block_number,
    +          "completion_transaction_hash" => msg.completion_transaction_hash,
    +          "status" => msg.status
    +        }
    +      end)
    +
    +    %{
    +      items: messages_out,
    +      next_page_params: next_page_params
    +    }
    +  end
    +
    +  @doc """
    +    Function to render GET requests to `/api/v2/main-page/arbitrum/messages/to-rollup` endpoint.
    +  """
    +  def render("arbitrum_messages.json", %{messages: messages}) do
    +    messages_out =
    +      messages
    +      |> Enum.map(fn msg ->
    +        %{
    +          "origination_transaction_hash" => msg.originating_transaction_hash,
    +          "origination_timestamp" => msg.origination_timestamp,
    +          "origination_transaction_block_number" => msg.originating_transaction_block_number,
    +          "completion_transaction_hash" => msg.completion_transaction_hash
    +        }
    +      end)
    +
    +    %{items: messages_out}
    +  end
    +
    +  @doc """
    +    Function to render GET requests to `/api/v2/arbitrum/messages/:direction/count` endpoint.
    +  """
    +  def render("arbitrum_messages_count.json", %{count: count}) do
    +    count
    +  end
    +
    +  @doc """
    +    Function to render GET requests to `/api/v2/arbitrum/messages/claim/:message_id` endpoint.
    +  """
    +  def render("arbitrum_claim_message.json", %{calldata: calldata, address: address}) do
    +    %{
    +      "calldata" => calldata,
    +      "outbox_address_hash" => address,
    +      # todo: It should be removed in favour `contract_address_hash` property with the next release after 8.0.0
    +      "outbox_address" => address
    +    }
    +  end
    +
    +  @doc """
    +    Function to render GET requests to `/api/v2/arbitrum/messages/withdrawals/:transaction_hash` endpoint.
    +  """
    +  def render("arbitrum_withdrawals.json", %{withdrawals: withdrawals}) do
    +    withdrawals_out =
    +      withdrawals
    +      |> Enum.map(fn withdraw ->
    +        %{
    +          "id" => withdraw.message_id,
    +          "status" => withdraw.status,
    +          "caller_address_hash" => withdraw.caller,
    +          # todo: "caller"" should be removed in favour `caller_address_hash` property with the next release after 8.0.0
    +          "caller" => withdraw.caller,
    +          "destination_address_hash" => withdraw.destination,
    +          # todo: "destination" should be removed in favour `destination_address_hash` property with the next release after 8.0.0
    +          "destination" => withdraw.destination,
    +          "arb_block_number" => withdraw.arb_block_number,
    +          "eth_block_number" => withdraw.eth_block_number,
    +          "l2_timestamp" => withdraw.l2_timestamp,
    +          "callvalue" => Integer.to_string(withdraw.callvalue),
    +          "data" => withdraw.data,
    +          "token" =>
    +            case withdraw.token do
    +              %{} -> Map.update!(withdraw.token, :amount, &Integer.to_string/1)
    +              _ -> nil
    +            end,
    +          "completion_transaction_hash" => withdraw.completion_transaction_hash
    +        }
    +      end)
    +
    +    %{items: withdrawals_out}
    +  end
    +
    +  @doc """
    +    Function to render GET requests to `/api/v2/arbitrum/batches/:batch_number` endpoint.
    +  """
    +  def render("arbitrum_batch.json", %{batch: batch}) do
    +    %{
    +      "number" => batch.number,
    +      "transactions_count" => batch.transactions_count,
    +      "start_block_number" => batch.start_block,
    +      "end_block_number" => batch.end_block,
    +      # todo: It should be removed in favour `start_block_number` property with the next release after 8.0.0
    +      "start_block" => batch.start_block,
    +      # todo: It should be removed in favour `end_block_number` property with the next release after 8.0.0
    +      "end_block" => batch.end_block,
    +      "before_acc_hash" => batch.before_acc,
    +      # todo: It should be removed in favour `before_acc_hash` property with the next release after 8.0.0
    +      "before_acc" => batch.before_acc,
    +      "after_acc_hash" => batch.after_acc,
    +      # todo: It should be removed in favour `after_acc_hash` property with the next release after 8.0.0
    +      "after_acc" => batch.after_acc
    +    }
    +    |> add_l1_transaction_info(batch)
    +    |> add_da_info(batch)
    +  end
    +
    +  @doc """
    +    Function to render GET requests to `/api/v2/arbitrum/batches` endpoint.
    +  """
    +  def render("arbitrum_batches.json", %{
    +        batches: batches,
    +        next_page_params: next_page_params
    +      }) do
    +    %{
    +      items: render_arbitrum_batches(batches),
    +      next_page_params: next_page_params
    +    }
    +  end
    +
    +  @doc """
    +    Function to render GET requests to `/api/v2/main-page/arbitrum/batches/committed` endpoint.
    +  """
    +  def render("arbitrum_batches.json", %{batches: batches}) do
    +    %{items: render_arbitrum_batches(batches)}
    +  end
    +
    +  @doc """
    +    Function to render GET requests to `/api/v2/arbitrum/batches/count` endpoint.
    +  """
    +  def render("arbitrum_batches_count.json", %{count: count}) do
    +    count
    +  end
    +
    +  @doc """
    +    Function to render GET requests to `/api/v2/main-page/arbitrum/batches/latest-number` endpoint.
    +  """
    +  def render("arbitrum_batch_latest_number.json", %{number: number}) do
    +    number
    +  end
    +
    +  # Transforms a list of L1 batches into a map format for HTTP response.
    +  #
    +  # This function processes a list of Arbitrum L1 batches and converts each batch into
    +  # a map that includes basic batch information and details of the associated
    +  # transaction that committed the batch to L1.
    +  #
    +  # ## Parameters
    +  # - `batches`: A list of `Explorer.Chain.Arbitrum.L1Batch` entries or a list of maps
    +  #              with the corresponding fields.
    +  #
    +  # ## Returns
    +  # - A list of maps with detailed information about each batch, formatted for use
    +  #   in JSON HTTP responses.
    +  @spec render_arbitrum_batches(
    +          [L1Batch.t()]
    +          | [
    +              %{
    +                :number => non_neg_integer(),
    +                :transactions_count => non_neg_integer(),
    +                :start_block => non_neg_integer(),
    +                :end_block => non_neg_integer(),
    +                :batch_container => atom() | nil,
    +                :commitment_transaction => LifecycleTransaction.to_import(),
    +                optional(any()) => any()
    +              }
    +            ]
    +        ) :: [map()]
    +  defp render_arbitrum_batches(batches) do
    +    Enum.map(batches, &render_base_info_for_batch/1)
    +  end
    +
    +  # Transforms a L1 batch into a map format for HTTP response.
    +  #
    +  # This function processes an Arbitrum L1 batch and converts it into a map that
    +  # includes basic batch information and details of the associated transaction
    +  # that committed the batch to L1.
    +  #
    +  # ## Parameters
    +  # - `batch`: Either an `Explorer.Chain.Arbitrum.L1Batch` entry or a map with
    +  #            the corresponding fields.
    +  #
    +  # ## Returns
    +  # - A  map with detailed information about the batch, formatted for use in JSON HTTP responses.
    +  @spec render_base_info_for_batch(
    +          L1Batch.t()
    +          | %{
    +              :number => non_neg_integer(),
    +              :transactions_count => non_neg_integer(),
    +              :start_block => non_neg_integer(),
    +              :end_block => non_neg_integer(),
    +              :batch_container => atom() | nil,
    +              :commitment_transaction => LifecycleTransaction.to_import(),
    +              optional(any()) => any()
    +            }
    +        ) :: map()
    +  def render_base_info_for_batch(batch) do
    +    %{
    +      "number" => batch.number,
    +      "transactions_count" => batch.transactions_count,
    +      "blocks_count" => batch.end_block - batch.start_block + 1,
    +      "batch_data_container" => batch.batch_container
    +    }
    +    |> add_l1_transaction_info(batch)
    +  end
    +
    +  @doc """
    +    Extends the json output with a sub-map containing information related Arbitrum.
    +
    +    ## Parameters
    +    - `out_json`: a map defining output json which will be extended
    +    - `transaction`: transaction structure containing Arbitrum related data
    +
    +    ## Returns
    +    A map extended with data related Arbitrum rollup
    +  """
    +  @spec extend_transaction_json_response(map(), %{
    +          :__struct__ => Transaction,
    +          optional(:arbitrum_batch) => any(),
    +          optional(:arbitrum_commitment_transaction) => any(),
    +          optional(:arbitrum_confirmation_transaction) => any(),
    +          optional(:arbitrum_message_to_l2) => any(),
    +          optional(:arbitrum_message_from_l2) => any(),
    +          optional(:gas_used_for_l1) => Decimal.t(),
    +          optional(:gas_used) => Decimal.t(),
    +          optional(:gas_price) => Wei.t(),
    +          optional(any()) => any()
    +        }) :: map()
    +  def extend_transaction_json_response(out_json, %Transaction{} = transaction) do
    +    arbitrum_info =
    +      %{}
    +      |> extend_with_settlement_info(transaction)
    +      |> extend_if_message(transaction)
    +      |> extend_with_transaction_info(transaction)
    +
    +    Map.put(out_json, "arbitrum", arbitrum_info)
    +  end
    +
    +  @doc """
    +    Extends the json output with a sub-map containing information related Arbitrum.
    +
    +    ## Parameters
    +    - `out_json`: a map defining output json which will be extended
    +    - `block`: block structure containing Arbitrum related data
    +
    +    ## Returns
    +    A map extended with data related Arbitrum rollup
    +  """
    +  @spec extend_block_json_response(map(), %{
    +          :__struct__ => Block,
    +          optional(:arbitrum_batch) => any(),
    +          optional(:arbitrum_commitment_transaction) => any(),
    +          optional(:arbitrum_confirmation_transaction) => any(),
    +          optional(:send_count) => non_neg_integer(),
    +          optional(:send_root) => Hash.Full.t(),
    +          optional(:l1_block_number) => non_neg_integer(),
    +          optional(any()) => any()
    +        }) :: map()
    +  def extend_block_json_response(out_json, %Block{} = block) do
    +    arbitrum_info =
    +      %{}
    +      |> extend_with_settlement_info(block)
    +      |> extend_with_block_info(block)
    +
    +    Map.put(out_json, "arbitrum", arbitrum_info)
    +  end
    +
    +  # Augments an output JSON with settlement-related information such as batch number and L1 transaction details to JSON.
    +  @spec extend_with_settlement_info(map(), %{
    +          :__struct__ => Block | Transaction,
    +          optional(:arbitrum_batch) => any(),
    +          optional(:arbitrum_commitment_transaction) => any(),
    +          optional(:arbitrum_confirmation_transaction) => any(),
    +          optional(any()) => any()
    +        }) :: map()
    +  defp extend_with_settlement_info(out_json, arbitrum_entity) do
    +    out_json
    +    |> add_l1_transactions_info_and_status(%{
    +      batch_number: get_batch_number(arbitrum_entity),
    +      commitment_transaction: arbitrum_entity.arbitrum_commitment_transaction,
    +      confirmation_transaction: arbitrum_entity.arbitrum_confirmation_transaction
    +    })
    +    |> Map.put("batch_data_container", get_batch_data_container(arbitrum_entity))
    +    |> Map.put("batch_number", get_batch_number(arbitrum_entity))
    +  end
    +
    +  # Retrieves the batch number from an Arbitrum block or transaction if the batch
    +  # data is loaded.
    +  @spec get_batch_number(%{
    +          :__struct__ => Block | Transaction,
    +          optional(:arbitrum_batch) => any(),
    +          optional(any()) => any()
    +        }) :: nil | non_neg_integer()
    +  defp get_batch_number(arbitrum_entity) do
    +    case Map.get(arbitrum_entity, :arbitrum_batch) do
    +      nil -> nil
    +      %Ecto.Association.NotLoaded{} -> nil
    +      value -> value.number
    +    end
    +  end
    +
    +  # Retrieves the batch data container label from an Arbitrum block or transaction
    +  # if the batch data is loaded.
    +  @spec get_batch_data_container(%{
    +          :__struct__ => Block | Transaction,
    +          optional(:arbitrum_batch) => any(),
    +          optional(any()) => any()
    +        }) :: nil | String.t()
    +  defp get_batch_data_container(arbitrum_entity) do
    +    case Map.get(arbitrum_entity, :arbitrum_batch) do
    +      nil -> nil
    +      %Ecto.Association.NotLoaded{} -> nil
    +      value -> to_string(value.batch_container)
    +    end
    +  end
    +
    +  # Augments an output JSON with commit transaction details and its status.
    +  @spec add_l1_transaction_info(map(), %{
    +          :commitment_transaction => LifecycleTransaction.t() | LifecycleTransaction.to_import(),
    +          optional(any()) => any()
    +        }) :: map()
    +  defp add_l1_transaction_info(out_json, %L1Batch{} = batch) do
    +    l1_transaction = %{commitment_transaction: handle_associated_l1_transactions_properly(batch.commitment_transaction)}
    +
    +    out_json
    +    |> Map.merge(%{
    +      "commitment_transaction" => %{
    +        "hash" => APIV2Helper.get_2map_data(l1_transaction, :commitment_transaction, :hash),
    +        "block_number" => APIV2Helper.get_2map_data(l1_transaction, :commitment_transaction, :block),
    +        "timestamp" => APIV2Helper.get_2map_data(l1_transaction, :commitment_transaction, :ts),
    +        "status" => APIV2Helper.get_2map_data(l1_transaction, :commitment_transaction, :status)
    +      }
    +    })
    +  end
    +
    +  defp add_l1_transaction_info(out_json, %{
    +         commitment_transaction: %{
    +           hash: hash,
    +           block_number: block_number,
    +           timestamp: ts,
    +           status: status
    +         }
    +       }) do
    +    out_json
    +    |> Map.merge(%{
    +      "commitment_transaction" => %{
    +        "hash" => %Hash{byte_count: 32, bytes: hash},
    +        "block_number" => block_number,
    +        "timestamp" => ts,
    +        "status" => status
    +      }
    +    })
    +  end
    +
    +  # Adds data availability (DA) information to the given output JSON based on the batch container type.
    +  #
    +  # This function enriches the output JSON with data availability information based on
    +  # the type of batch container. It handles different DA types, including AnyTrust and
    +  # Celestia, and generates the appropriate DA data for inclusion in the output.
    +  #
    +  # ## Parameters
    +  # - `out_json`: The initial JSON map to be enriched with DA information.
    +  # - `batch`: The batch struct containing information about the rollup batch.
    +  #
    +  # ## Returns
    +  # - An updated JSON map containing the data availability information.
    +  @spec add_da_info(map(), %{
    +          :__struct__ => L1Batch,
    +          :batch_container => :in_anytrust | :in_celestia | atom() | nil,
    +          :number => non_neg_integer(),
    +          optional(any()) => any()
    +        }) :: map()
    +  defp add_da_info(out_json, %L1Batch{} = batch) do
    +    da_info =
    +      case batch.batch_container do
    +        nil -> %{"batch_data_container" => nil}
    +        :in_anytrust -> generate_anytrust_certificate(batch.number)
    +        :in_celestia -> generate_celestia_da_info(batch.number)
    +        value -> %{"batch_data_container" => to_string(value)}
    +      end
    +
    +    out_json
    +    |> Map.put("data_availability", da_info)
    +  end
    +
    +  # Generates an AnyTrust certificate for the specified batch number.
    +  @spec generate_anytrust_certificate(non_neg_integer()) :: map()
    +  defp generate_anytrust_certificate(batch_number) do
    +    out = %{"batch_data_container" => "in_anytrust"}
    +
    +    da_info =
    +      with raw_info <- SettlementReader.get_da_info_by_batch_number(batch_number),
    +           false <- Enum.empty?(raw_info) do
    +        prepare_anytrust_certificate(raw_info)
    +      else
    +        _ -> %{"data_hash" => nil, "timeout" => nil, "bls_signature" => nil, "signers" => []}
    +      end
    +
    +    out
    +    |> Map.merge(da_info)
    +  end
    +
    +  # Prepares an AnyTrust certificate from the given DA information.
    +  #
    +  # This function retrieves the corresponding AnyTrust keyset based on the provided
    +  # DA information, constructs a list of signers and the signers' mask, and assembles
    +  # the certificate data.
    +  #
    +  # ## Parameters
    +  # - `da_info`: A map containing the DA information, including the keyset hash, data
    +  #   hash, timeout, aggregated BLS signature, and signers' mask.
    +  #
    +  # ## Returns
    +  # - A map representing the AnyTrust certificate, containing the data hash, data
    +  #   availability timeout, aggregated BLS signature, and the list of committee
    +  #   members who guaranteed availability of data for the specified timeout.
    +  @spec prepare_anytrust_certificate(map()) :: map()
    +  defp prepare_anytrust_certificate(da_info) do
    +    keyset = SettlementReader.get_anytrust_keyset(da_info["keyset_hash"])
    +
    +    signers =
    +      if Enum.empty?(keyset) do
    +        []
    +      else
    +        signers_mask = da_info["signers_mask"]
    +
    +        # Matches the signers' mask with the keyset to extract the list of signers.
    +        keyset["pubkeys"]
    +        |> Enum.with_index()
    +        |> Enum.filter(fn {_, index} -> Bitwise.band(signers_mask, Bitwise.bsl(1, index)) != 0 end)
    +        |> Enum.map(fn {pubkey, _} -> pubkey end)
    +      end
    +
    +    %{
    +      "data_hash" => da_info["data_hash"],
    +      "timeout" => da_info["timeout"],
    +      "bls_signature" => da_info["bls_signature"],
    +      "signers" => signers
    +    }
    +  end
    +
    +  # Generates Celestia DA information for the given batch number.
    +  @spec generate_celestia_da_info(non_neg_integer()) :: map()
    +  defp generate_celestia_da_info(batch_number) do
    +    out = %{"batch_data_container" => "in_celestia"}
    +
    +    da_info = SettlementReader.get_da_info_by_batch_number(batch_number)
    +
    +    out
    +    |> Map.merge(%{
    +      "height" => Map.get(da_info, "height"),
    +      "transaction_commitment" => Map.get(da_info, "transaction_commitment")
    +    })
    +  end
    +
    +  # Augments an output JSON with commit and confirm transaction details and their statuses.
    +  @spec add_l1_transactions_info_and_status(map(), %{
    +          optional(:commitment_transaction) => any(),
    +          optional(:confirmation_transaction) => any(),
    +          optional(:batch_number) => any()
    +        }) :: map()
    +  defp add_l1_transactions_info_and_status(out_json, arbitrum_item)
    +       when is_map(arbitrum_item) and
    +              is_map_key(arbitrum_item, :commitment_transaction) and
    +              is_map_key(arbitrum_item, :confirmation_transaction) do
    +    l1_transactions = get_associated_l1_transactions(arbitrum_item)
    +
    +    out_json
    +    |> Map.merge(%{
    +      "status" => block_or_transaction_status(arbitrum_item),
    +      "commitment_transaction" => %{
    +        "hash" => APIV2Helper.get_2map_data(l1_transactions, :commitment_transaction, :hash),
    +        "timestamp" => APIV2Helper.get_2map_data(l1_transactions, :commitment_transaction, :ts),
    +        "status" => APIV2Helper.get_2map_data(l1_transactions, :commitment_transaction, :status)
    +      },
    +      "confirmation_transaction" => %{
    +        "hash" => APIV2Helper.get_2map_data(l1_transactions, :confirmation_transaction, :hash),
    +        "timestamp" => APIV2Helper.get_2map_data(l1_transactions, :confirmation_transaction, :ts),
    +        "status" => APIV2Helper.get_2map_data(l1_transactions, :confirmation_transaction, :status)
    +      }
    +    })
    +  end
    +
    +  # Extract transaction hash and block number, timestamp, finalization status for
    +  # L1 transactions associated with an Arbitrum rollup entity: transaction or block.
    +  #
    +  # ## Parameters
    +  # - `arbitrum_item`: a short description of a transaction, or block.
    +  #
    +  # ## Returns
    +  # A map containing nesting maps describing corresponding L1 transactions
    +  @spec get_associated_l1_transactions(%{
    +          optional(:commitment_transaction) => any(),
    +          optional(:confirmation_transaction) => any(),
    +          optional(any()) => any()
    +        }) :: %{
    +          :commitment_transaction =>
    +            nil
    +            | %{
    +                :hash => nil | binary(),
    +                :block_number => nil | non_neg_integer(),
    +                :ts => nil | DateTime.t(),
    +                :status => nil | :finalized | :unfinalized
    +              },
    +          :confirmation_transaction =>
    +            nil
    +            | %{
    +                :hash => nil | binary(),
    +                :block_number => nil | non_neg_integer(),
    +                :ts => nil | DateTime.t(),
    +                :status => nil | :finalized | :unfinalized
    +              }
    +        }
    +  defp get_associated_l1_transactions(arbitrum_item) do
    +    [:commitment_transaction, :confirmation_transaction]
    +    |> Enum.reduce(%{}, fn key, l1_transactions ->
    +      Map.put(l1_transactions, key, handle_associated_l1_transactions_properly(Map.get(arbitrum_item, key)))
    +    end)
    +  end
    +
    +  # Returns details of an associated L1 transaction or nil if not loaded or not available.
    +  @spec handle_associated_l1_transactions_properly(LifecycleTransaction | Ecto.Association.NotLoaded.t() | nil) ::
    +          nil
    +          | %{
    +              :hash => nil | binary(),
    +              :block => nil | non_neg_integer(),
    +              :ts => nil | DateTime.t(),
    +              :status => nil | :finalized | :unfinalized
    +            }
    +  defp handle_associated_l1_transactions_properly(associated_l1_transaction) do
    +    case associated_l1_transaction do
    +      nil -> nil
    +      %Ecto.Association.NotLoaded{} -> nil
    +      value -> %{hash: value.hash, block: value.block_number, ts: value.timestamp, status: value.status}
    +    end
    +  end
    +
    +  # Inspects L1 transactions of a rollup block or transaction to determine its status.
    +  #
    +  # ## Parameters
    +  # - `arbitrum_item`: An Arbitrum transaction or block.
    +  #
    +  # ## Returns
    +  # A string with one of predefined statuses
    +  @spec block_or_transaction_status(%{
    +          optional(:commitment_transaction) => any(),
    +          optional(:confirmation_transaction) => any(),
    +          optional(:batch_number) => any()
    +        }) :: String.t()
    +  defp block_or_transaction_status(arbitrum_item) do
    +    cond do
    +      APIV2Helper.specified?(arbitrum_item.confirmation_transaction) -> "Confirmed on base"
    +      APIV2Helper.specified?(arbitrum_item.commitment_transaction) -> "Sent to base"
    +      not is_nil(arbitrum_item.batch_number) -> "Sealed on rollup"
    +      true -> "Processed on rollup"
    +    end
    +  end
    +
    +  # Determines if an Arbitrum transaction contains a cross-chain message and extends
    +  # the incoming map with fields related to the cross-chain message to reflect the
    +  # direction of the message, its status and the associated L1 transaction.
    +  #
    +  # ## Parameters
    +  # - `arbitrum_transaction`: An Arbitrum transaction.
    +  #
    +  # ## Returns
    +  # - A map extended with fields indicating the direction of the message, its status
    +  #   and the associated L1 transaction.
    +  @spec extend_if_message(map(), %{
    +          :__struct__ => Transaction,
    +          optional(:arbitrum_message_to_l2) => any(),
    +          optional(:arbitrum_message_from_l2) => any(),
    +          optional(any()) => any()
    +        }) :: map()
    +  defp extend_if_message(arbitrum_json, %Transaction{} = arbitrum_transaction) do
    +    {message_type, message_data} =
    +      case {APIV2Helper.specified?(Map.get(arbitrum_transaction, :arbitrum_message_to_l2)),
    +            APIV2Helper.specified?(Map.get(arbitrum_transaction, :arbitrum_message_from_l2))} do
    +        {true, false} ->
    +          {"incoming", l1_transaction_and_status_for_message(arbitrum_transaction, :incoming)}
    +
    +        {false, true} ->
    +          {"outcoming", l1_transaction_and_status_for_message(arbitrum_transaction, :outcoming)}
    +
    +        _ ->
    +          {nil, %{}}
    +      end
    +
    +    arbitrum_json
    +    |> Map.put("contains_message", message_type)
    +    |> Map.put("message_related_info", message_data)
    +  end
    +
    +  # Determines the associated L1 transaction and its status for the given message direction.
    +  # TODO: it's need to take into account the tx on L2 may initiate several withdrawals.
    +  #       The current architecture doesn't support that.
    +  @spec l1_transaction_and_status_for_message(
    +          %{
    +            :__struct__ => Transaction,
    +            optional(:arbitrum_message_to_l2) => any(),
    +            optional(:arbitrum_message_from_l2) => any(),
    +            optional(any()) => any()
    +          },
    +          :incoming | :outcoming
    +        ) :: map()
    +  defp l1_transaction_and_status_for_message(arbitrum_transaction, message_direction) do
    +    {l1_transaction, status} =
    +      case message_direction do
    +        :incoming ->
    +          l1_transaction =
    +            APIV2Helper.get_2map_data(arbitrum_transaction, :arbitrum_message_to_l2, :originating_transaction_hash)
    +
    +          if is_nil(l1_transaction) do
    +            {nil, "Syncing with base layer"}
    +          else
    +            {l1_transaction, "Relayed"}
    +          end
    +
    +        :outcoming ->
    +          case APIV2Helper.get_2map_data(arbitrum_transaction, :arbitrum_message_from_l2, :status) do
    +            :initiated ->
    +              {nil, "Settlement pending"}
    +
    +            :sent ->
    +              {nil, "Waiting for confirmation"}
    +
    +            :confirmed ->
    +              {nil, "Ready for relay"}
    +
    +            :relayed ->
    +              {APIV2Helper.get_2map_data(arbitrum_transaction, :arbitrum_message_from_l2, :completion_transaction_hash),
    +               "Relayed"}
    +          end
    +      end
    +
    +    %{
    +      "message_id" => APIV2Helper.get_2map_data(arbitrum_transaction, :arbitrum_message_from_l2, :message_id),
    +      "associated_l1_transaction_hash" => l1_transaction,
    +      # todo: It should be removed in favour `associated_l1_transaction_hash` property with the next release after 8.0.0
    +      "associated_l1_transaction" => l1_transaction,
    +      "message_status" => status
    +    }
    +  end
    +
    +  # Extends the output JSON with information from Arbitrum-specific fields of the transaction.
    +  @spec extend_with_transaction_info(map(), %{
    +          :__struct__ => Transaction,
    +          optional(:gas_used_for_l1) => Decimal.t(),
    +          optional(any()) => any()
    +        }) :: map()
    +  defp extend_with_transaction_info(out_json, %Transaction{} = arbitrum_transaction) do
    +    # Map.get is only needed for the case when the module is compiled with
    +    # chain_type different from "arbitrum", `|| 0` is used to avoid nil values
    +    # for the transaction prior to the migration to Arbitrum specific BS build.
    +    gas_used_for_l1 = Map.get(arbitrum_transaction, :gas_used_for_l1, 0) || 0
    +
    +    gas_used = Map.get(arbitrum_transaction, :gas_used, 0) || 0
    +    gas_price = Map.get(arbitrum_transaction, :gas_price, 0) || 0
    +
    +    gas_used_for_l2 =
    +      gas_used
    +      |> Decimal.sub(gas_used_for_l1)
    +
    +    poster_fee =
    +      gas_price
    +      |> Wei.to(:wei)
    +      |> Decimal.mult(gas_used_for_l1)
    +
    +    network_fee =
    +      gas_price
    +      |> Wei.to(:wei)
    +      |> Decimal.mult(gas_used_for_l2)
    +
    +    out_json
    +    |> Map.put("gas_used_for_l1", gas_used_for_l1)
    +    |> Map.put("gas_used_for_l2", gas_used_for_l2)
    +    |> Map.put("poster_fee", poster_fee)
    +    |> Map.put("network_fee", network_fee)
    +  end
    +
    +  # Extends the output JSON with information from the Arbitrum-specific fields of the block.
    +  @spec extend_with_block_info(map(), %{
    +          :__struct__ => Block,
    +          optional(:send_count) => non_neg_integer(),
    +          optional(:send_root) => Hash.Full.t(),
    +          optional(:l1_block_number) => non_neg_integer(),
    +          optional(any()) => any()
    +        }) :: map()
    +  defp extend_with_block_info(out_json, %Block{} = arbitrum_block) do
    +    out_json
    +    |> Map.put("delayed_messages", Hash.to_integer(arbitrum_block.nonce))
    +    |> Map.put("l1_block_number", Map.get(arbitrum_block, :l1_block_number))
    +    # todo: It should be removed in favour `l1_block_number` property with the next release after 8.0.0
    +    |> Map.put("l1_block_height", Map.get(arbitrum_block, :l1_block_number))
    +    |> Map.put("send_count", Map.get(arbitrum_block, :send_count))
    +    |> Map.put("send_root", Map.get(arbitrum_block, :send_root))
    +  end
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v2/blob_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v2/blob_view.ex
    new file mode 100644
    index 0000000..fcb792a
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v2/blob_view.ex
    @@ -0,0 +1,23 @@
    +defmodule BlockScoutWeb.API.V2.BlobView do
    +  use BlockScoutWeb, :view
    +
    +  alias Explorer.Chain.Beacon.Blob
    +
    +  def render("blob.json", %{blob: blob, transaction_hashes: transaction_hashes}) do
    +    blob |> prepare_blob() |> Map.put("transaction_hashes", transaction_hashes)
    +  end
    +
    +  def render("blobs.json", %{blobs: blobs}) do
    +    %{"items" => Enum.map(blobs, &prepare_blob(&1))}
    +  end
    +
    +  @spec prepare_blob(Blob.t()) :: map()
    +  def prepare_blob(blob) do
    +    %{
    +      "hash" => blob.hash,
    +      "blob_data" => blob.blob_data,
    +      "kzg_commitment" => blob.kzg_commitment,
    +      "kzg_proof" => blob.kzg_proof
    +    }
    +  end
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v2/block_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v2/block_view.ex
    new file mode 100644
    index 0000000..157fdc0
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v2/block_view.ex
    @@ -0,0 +1,169 @@
    +defmodule BlockScoutWeb.API.V2.BlockView do
    +  use BlockScoutWeb, :view
    +  use Utils.CompileTimeEnvHelper, chain_type: [:explorer, :chain_type]
    +
    +  alias BlockScoutWeb.BlockView
    +  alias BlockScoutWeb.API.V2.{ApiView, Helper}
    +  alias Explorer.Chain.Block
    +  alias Explorer.Chain.Cache.Counters.BlockPriorityFeeCount
    +
    +  def render("message.json", assigns) do
    +    ApiView.render("message.json", assigns)
    +  end
    +
    +  def render("blocks.json", %{blocks: blocks, next_page_params: next_page_params}) do
    +    %{"items" => Enum.map(blocks, &prepare_block(&1, nil)), "next_page_params" => next_page_params}
    +  end
    +
    +  def render("blocks.json", %{blocks: blocks}) do
    +    Enum.map(blocks, &prepare_block(&1, nil))
    +  end
    +
    +  def render("block.json", %{block: block, conn: conn}) do
    +    prepare_block(block, conn, true)
    +  end
    +
    +  def render("block.json", %{block: block, socket: _socket}) do
    +    # single_block? set to true in order to prevent heavy fetching of reward type
    +    prepare_block(block, nil, false)
    +  end
    +
    +  def prepare_block(block, _conn, single_block? \\ false) do
    +    burnt_fees = Block.burnt_fees(block.transactions, block.base_fee_per_gas)
    +    priority_fee = block.base_fee_per_gas && BlockPriorityFeeCount.fetch(block.hash)
    +
    +    transaction_fees = Block.transaction_fees(block.transactions)
    +
    +    %{
    +      "height" => block.number,
    +      "timestamp" => block.timestamp,
    +      "transactions_count" => count_transactions(block),
    +      # todo: It should be removed in favour `transactions_count` property with the next release after 8.0.0
    +      "transaction_count" => count_transactions(block),
    +      "miner" => Helper.address_with_info(nil, block.miner, block.miner_hash, false),
    +      "size" => block.size,
    +      "hash" => block.hash,
    +      "parent_hash" => block.parent_hash,
    +      "difficulty" => block.difficulty,
    +      "total_difficulty" => block.total_difficulty,
    +      "gas_used" => block.gas_used,
    +      "gas_limit" => block.gas_limit,
    +      "nonce" => block.nonce,
    +      "base_fee_per_gas" => block.base_fee_per_gas,
    +      "burnt_fees" => burnt_fees,
    +      "priority_fee" => priority_fee,
    +      # "extra_data" => "TODO",
    +      "uncles_hashes" => prepare_uncles(block.uncle_relations),
    +      # "state_root" => "TODO",
    +      "rewards" => prepare_rewards(block.rewards, block, single_block?),
    +      "gas_target_percentage" => Block.gas_target(block),
    +      "gas_used_percentage" => Block.gas_used_percentage(block),
    +      "burnt_fees_percentage" => burnt_fees_percentage(burnt_fees, transaction_fees),
    +      "type" => block |> BlockView.block_type() |> String.downcase(),
    +      "transaction_fees" => transaction_fees,
    +      "withdrawals_count" => count_withdrawals(block)
    +    }
    +    |> chain_type_fields(block, single_block?)
    +  end
    +
    +  def prepare_rewards(rewards, block, single_block?) do
    +    Enum.map(rewards, &prepare_reward(&1, block, single_block?))
    +  end
    +
    +  def prepare_reward(reward, block, single_block?) do
    +    %{
    +      "reward" => reward.reward,
    +      "type" => if(single_block?, do: BlockView.block_reward_text(reward, block.miner.hash), else: reward.address_type)
    +    }
    +  end
    +
    +  def prepare_uncles(uncles_relations) when is_list(uncles_relations) do
    +    Enum.map(uncles_relations, &prepare_uncle/1)
    +  end
    +
    +  def prepare_uncles(_), do: []
    +
    +  def prepare_uncle(uncle_relation) do
    +    %{"hash" => uncle_relation.uncle_hash}
    +  end
    +
    +  def burnt_fees_percentage(_, %Decimal{coef: 0}), do: nil
    +
    +  def burnt_fees_percentage(burnt_fees, transaction_fees)
    +      when not is_nil(transaction_fees) and not is_nil(burnt_fees) do
    +    burnt_fees |> Decimal.div(transaction_fees) |> Decimal.mult(100) |> Decimal.to_float()
    +  end
    +
    +  def burnt_fees_percentage(_, _), do: nil
    +
    +  def count_transactions(%Block{transactions: transactions}) when is_list(transactions), do: Enum.count(transactions)
    +  def count_transactions(_), do: nil
    +
    +  def count_withdrawals(%Block{withdrawals: withdrawals}) when is_list(withdrawals), do: Enum.count(withdrawals)
    +  def count_withdrawals(_), do: nil
    +
    +  case @chain_type do
    +    :rsk ->
    +      defp chain_type_fields(result, block, single_block?) do
    +        if single_block? do
    +          # credo:disable-for-next-line Credo.Check.Design.AliasUsage
    +          BlockScoutWeb.API.V2.RootstockView.extend_block_json_response(result, block)
    +        else
    +          result
    +        end
    +      end
    +
    +    :optimism ->
    +      defp chain_type_fields(result, block, single_block?) do
    +        if single_block? do
    +          # credo:disable-for-next-line Credo.Check.Design.AliasUsage
    +          BlockScoutWeb.API.V2.OptimismView.extend_block_json_response(result, block)
    +        else
    +          result
    +        end
    +      end
    +
    +    :zksync ->
    +      defp chain_type_fields(result, block, single_block?) do
    +        if single_block? do
    +          # credo:disable-for-next-line Credo.Check.Design.AliasUsage
    +          BlockScoutWeb.API.V2.ZkSyncView.extend_block_json_response(result, block)
    +        else
    +          result
    +        end
    +      end
    +
    +    :arbitrum ->
    +      defp chain_type_fields(result, block, single_block?) do
    +        if single_block? do
    +          # credo:disable-for-next-line Credo.Check.Design.AliasUsage
    +          BlockScoutWeb.API.V2.ArbitrumView.extend_block_json_response(result, block)
    +        else
    +          result
    +        end
    +      end
    +
    +    :ethereum ->
    +      defp chain_type_fields(result, block, single_block?) do
    +        # credo:disable-for-next-line Credo.Check.Design.AliasUsage
    +        BlockScoutWeb.API.V2.EthereumView.extend_block_json_response(result, block, single_block?)
    +      end
    +
    +    :celo ->
    +      defp chain_type_fields(result, block, single_block?) do
    +        # credo:disable-for-next-line Credo.Check.Design.AliasUsage
    +        BlockScoutWeb.API.V2.CeloView.extend_block_json_response(result, block, single_block?)
    +      end
    +
    +    :zilliqa ->
    +      defp chain_type_fields(result, block, single_block?) do
    +        # credo:disable-for-next-line Credo.Check.Design.AliasUsage
    +        BlockScoutWeb.API.V2.ZilliqaView.extend_block_json_response(result, block, single_block?)
    +      end
    +
    +    _ ->
    +      defp chain_type_fields(result, _block, _single_block?) do
    +        result
    +      end
    +  end
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v2/celo_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v2/celo_view.ex
    new file mode 100644
    index 0000000..ebde664
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v2/celo_view.ex
    @@ -0,0 +1,403 @@
    +defmodule BlockScoutWeb.API.V2.CeloView do
    +  @moduledoc """
    +  View functions for rendering Celo-related data in JSON format.
    +  """
    +  use BlockScoutWeb, :view
    +
    +  require Logger
    +
    +  import Explorer.Chain.SmartContract, only: [dead_address_hash_string: 0]
    +
    +  alias BlockScoutWeb.API.V2.{Helper, TokenView, TransactionView}
    +  alias Ecto.Association.NotLoaded
    +  alias Explorer.Chain
    +  alias Explorer.Chain.Cache.CeloCoreContracts
    +  alias Explorer.Chain.Celo.Helper, as: CeloHelper
    +  alias Explorer.Chain.Celo.{ElectionReward, EpochReward}
    +  alias Explorer.Chain.Hash
    +  alias Explorer.Chain.{Block, Token, Transaction, Wei}
    +
    +  @address_params [
    +    necessity_by_association: %{
    +      :names => :optional,
    +      :smart_contract => :optional,
    +      proxy_implementations_association() => :optional
    +    },
    +    api?: true
    +  ]
    +
    +  def render("celo_epoch.json", %{epoch_number: epoch_number, epoch_distribution: nil}) do
    +    %{
    +      number: epoch_number,
    +      distribution: nil,
    +      aggregated_election_rewards: nil
    +    }
    +  end
    +
    +  def render(
    +        "celo_epoch.json",
    +        %{
    +          epoch_number: epoch_number,
    +          epoch_distribution: %EpochReward{
    +            reserve_bolster_transfer: reserve_bolster_transfer,
    +            community_transfer: community_transfer,
    +            carbon_offsetting_transfer: carbon_offsetting_transfer
    +          },
    +          aggregated_election_rewards: aggregated_election_rewards
    +        }
    +      ) do
    +    distribution_json =
    +      Map.new(
    +        [
    +          reserve_bolster_transfer: reserve_bolster_transfer,
    +          community_transfer: community_transfer,
    +          carbon_offsetting_transfer: carbon_offsetting_transfer
    +        ],
    +        fn {field, token_transfer} ->
    +          token_transfer_json =
    +            token_transfer &&
    +              TransactionView.render(
    +                "token_transfer.json",
    +                %{token_transfer: token_transfer, conn: nil}
    +              )
    +
    +          {field, token_transfer_json}
    +        end
    +      )
    +
    +    aggregated_election_rewards_json =
    +      Map.new(
    +        aggregated_election_rewards,
    +        fn {type, %{total: total, count: count, token: token}} ->
    +          {type,
    +           %{
    +             total: total,
    +             count: count,
    +             token:
    +               TokenView.render("token.json", %{
    +                 token: token,
    +                 contract_address_hash: token && token.contract_address_hash
    +               })
    +           }}
    +        end
    +      )
    +
    +    %{
    +      number: epoch_number,
    +      distribution: distribution_json,
    +      aggregated_election_rewards: aggregated_election_rewards_json
    +    }
    +  end
    +
    +  def render("celo_base_fee.json", %Block{} = block) do
    +    block.transactions
    +    |> Block.burnt_fees(block.base_fee_per_gas)
    +    |> Wei.cast()
    +    |> case do
    +      {:ok, base_fee} ->
    +        # For the blocks, where both FeeHandler and Governance contracts aren't
    +        # deployed, the base fee is not burnt, but refunded to transaction sender,
    +        # so we return nil in this case.
    +        fee_handler_base_fee_breakdown(
    +          base_fee,
    +          block.number
    +        ) ||
    +          governance_base_fee_breakdown(
    +            base_fee,
    +            block.number
    +          )
    +
    +      _ ->
    +        nil
    +    end
    +  end
    +
    +  def render("celo_election_rewards.json", %{
    +        rewards: rewards,
    +        next_page_params: next_page_params
    +      }) do
    +    %{
    +      "items" => Enum.map(rewards, &prepare_election_reward/1),
    +      "next_page_params" => next_page_params
    +    }
    +  end
    +
    +  @doc """
    +  Extends the JSON output with a sub-map containing information related to Celo,
    +  such as the epoch number, whether the block is an epoch block, and the routing
    +  of the base fee.
    +
    +  ## Parameters
    +  - `out_json`: A map defining the output JSON which will be extended.
    +  - `block`: The block structure containing Celo-related data.
    +  - `single_block?`: A boolean indicating if it is a single block.
    +
    +  ## Returns
    +  - A map extended with data related to Celo.
    +  """
    +  def extend_block_json_response(out_json, %Block{} = block, single_block?) do
    +    celo_json =
    +      %{
    +        "is_epoch_block" => CeloHelper.epoch_block_number?(block.number),
    +        "epoch_number" => CeloHelper.block_number_to_epoch_number(block.number)
    +      }
    +      |> maybe_add_base_fee_info(block, single_block?)
    +
    +    Map.put(out_json, "celo", celo_json)
    +  end
    +
    +  @doc """
    +  Extends the JSON output with a sub-map containing information about the gas
    +  token used to pay for the transaction fees.
    +
    +  ## Parameters
    +  - `out_json`: A map defining the output JSON which will be extended.
    +  - `transaction`: The transaction structure containing Celo-related data.
    +
    +  ## Returns
    +  - A map extended with data related to the gas token.
    +  """
    +  def extend_transaction_json_response(out_json, %Transaction{} = transaction) do
    +    token_json =
    +      case {
    +        Map.get(transaction, :gas_token_contract_address),
    +        Map.get(transaction, :gas_token)
    +      } do
    +        # {_, %NotLoaded{}} ->
    +        #   nil
    +
    +        {nil, _} ->
    +          nil
    +
    +        {gas_token_contract_address, gas_token} ->
    +          if is_nil(gas_token) do
    +            Logger.error(fn ->
    +              [
    +                "Transaction #{transaction.hash} has a ",
    +                "gas token contract address #{gas_token_contract_address} ",
    +                "but no associated token found in the database"
    +              ]
    +            end)
    +          end
    +
    +          TokenView.render("token.json", %{
    +            token: gas_token,
    +            contract_address_hash: gas_token_contract_address
    +          })
    +      end
    +
    +    Map.put(out_json, "celo", %{"gas_token" => token_json})
    +  end
    +
    +  @spec prepare_election_reward(Explorer.Chain.Celo.ElectionReward.t()) :: %{
    +          :account => nil | %{optional(String.t()) => any()},
    +          :amount => Decimal.t(),
    +          :associated_account => nil | %{optional(String.t()) => any()},
    +          optional(:block_hash) => Hash.Full.t(),
    +          optional(:block_number) => Block.block_number(),
    +          optional(:epoch_number) => non_neg_integer(),
    +          optional(:type) => ElectionReward.type()
    +        }
    +  defp prepare_election_reward(%ElectionReward{block: %NotLoaded{}} = reward) do
    +    %{
    +      amount: reward.amount,
    +      account:
    +        Helper.address_with_info(
    +          reward.account_address,
    +          reward.account_address_hash
    +        ),
    +      associated_account:
    +        Helper.address_with_info(
    +          reward.associated_account_address,
    +          reward.associated_account_address_hash
    +        )
    +    }
    +  end
    +
    +  defp prepare_election_reward(%ElectionReward{token: %Token{}, block: %Block{}} = reward) do
    +    %{
    +      amount: reward.amount,
    +      block_number: reward.block.number,
    +      block_hash: reward.block_hash,
    +      block_timestamp: reward.block.timestamp,
    +      epoch_number: reward.block.number |> CeloHelper.block_number_to_epoch_number(),
    +      account:
    +        Helper.address_with_info(
    +          reward.account_address,
    +          reward.account_address_hash
    +        ),
    +      associated_account:
    +        Helper.address_with_info(
    +          reward.associated_account_address,
    +          reward.associated_account_address_hash
    +        ),
    +      type: reward.type,
    +      token:
    +        TokenView.render("token.json", %{
    +          token: reward.token,
    +          contract_address_hash: reward.token.contract_address_hash
    +        })
    +    }
    +  end
    +
    +  # Get the breakdown of the base fee for the case when FeeHandler is a contract
    +  # that receives the base fee.
    +  @spec fee_handler_base_fee_breakdown(Wei.t(), Block.block_number()) ::
    +          %{
    +            :recipient => %{optional(String.t()) => any()},
    +            :amount => float(),
    +            :breakdown => [
    +              %{
    +                :address => %{optional(String.t()) => any()},
    +                :amount => float(),
    +                :percentage => float()
    +              }
    +            ]
    +          }
    +          | nil
    +  defp fee_handler_base_fee_breakdown(base_fee, block_number) do
    +    with {:ok, fee_handler_contract_address_hash} <-
    +           CeloCoreContracts.get_address(:fee_handler, block_number),
    +         {:ok, %{"address" => fee_beneficiary_address_hash}} <-
    +           CeloCoreContracts.get_event(:fee_handler, :fee_beneficiary_set, block_number),
    +         {:ok, %{"value" => burn_fraction_fixidity_lib}} <-
    +           CeloCoreContracts.get_event(:fee_handler, :burn_fraction_set, block_number),
    +         {:ok, celo_token_address_hash} <- CeloCoreContracts.get_address(:celo_token, block_number) do
    +      burn_fraction = CeloHelper.burn_fraction_decimal(burn_fraction_fixidity_lib)
    +
    +      burnt_amount = Wei.mult(base_fee, burn_fraction)
    +      burnt_percentage = Decimal.mult(burn_fraction, 100)
    +
    +      carbon_offsetting_amount = Wei.sub(base_fee, burnt_amount)
    +      carbon_offsetting_percentage = Decimal.sub(100, burnt_percentage)
    +
    +      celo_burn_address_hash_string = dead_address_hash_string()
    +
    +      address_hashes_to_fetch_from_db = [
    +        fee_handler_contract_address_hash,
    +        fee_beneficiary_address_hash,
    +        celo_burn_address_hash_string
    +      ]
    +
    +      address_hash_string_to_address =
    +        address_hashes_to_fetch_from_db
    +        |> Enum.map(&(&1 |> Chain.string_to_address_hash() |> elem(1)))
    +        # todo: Querying database in the view is not a good practice. Consider
    +        # refactoring.
    +        |> Chain.hashes_to_addresses(@address_params)
    +        |> Map.new(fn address ->
    +          {
    +            to_string(address.hash),
    +            address
    +          }
    +        end)
    +
    +      %{
    +        ^fee_handler_contract_address_hash => fee_handler_contract_address_info,
    +        ^fee_beneficiary_address_hash => fee_beneficiary_address_info,
    +        ^celo_burn_address_hash_string => burn_address_info
    +      } =
    +        Map.new(
    +          address_hashes_to_fetch_from_db,
    +          &{
    +            &1,
    +            Helper.address_with_info(
    +              Map.get(address_hash_string_to_address, &1),
    +              &1
    +            )
    +          }
    +        )
    +
    +      celo_token = Token.get_by_contract_address_hash(celo_token_address_hash, api?: true)
    +
    +      %{
    +        recipient: fee_handler_contract_address_info,
    +        amount: base_fee,
    +        token:
    +          TokenView.render("token.json", %{
    +            token: celo_token,
    +            contract_address_hash: celo_token.contract_address_hash
    +          }),
    +        breakdown: [
    +          %{
    +            address: burn_address_info,
    +            amount: burnt_amount,
    +            percentage: Decimal.to_float(burnt_percentage)
    +          },
    +          %{
    +            address: fee_beneficiary_address_info,
    +            amount: carbon_offsetting_amount,
    +            percentage: Decimal.to_float(carbon_offsetting_percentage)
    +          }
    +        ]
    +      }
    +    else
    +      _ -> nil
    +    end
    +  end
    +
    +  # Get the breakdown of the base fee for the case when Governance is a contract
    +  # that receives the base fee.
    +  #
    +  # Note that the base fee is not burnt in this case, but simply kept on the
    +  # contract balance.
    +  @spec governance_base_fee_breakdown(Wei.t(), Block.block_number()) ::
    +          %{
    +            :recipient => %{optional(String.t()) => any()},
    +            :amount => float(),
    +            :breakdown => [
    +              %{
    +                :address => %{optional(String.t()) => any()},
    +                :amount => float(),
    +                :percentage => float()
    +              }
    +            ]
    +          }
    +          | nil
    +  defp governance_base_fee_breakdown(base_fee, block_number) do
    +    with {:ok, address_hash_string} when not is_nil(address_hash_string) <-
    +           CeloCoreContracts.get_address(:governance, block_number),
    +         {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string),
    +         {:ok, celo_token_address_hash} <- CeloCoreContracts.get_address(:celo_token, block_number) do
    +      address =
    +        address_hash
    +        # todo: Querying database in the view is not a good practice. Consider
    +        # refactoring.
    +        |> Chain.hash_to_address(@address_params)
    +        |> case do
    +          {:ok, address} -> address
    +          {:error, :not_found} -> nil
    +        end
    +
    +      address_with_info =
    +        Helper.address_with_info(
    +          address,
    +          address_hash
    +        )
    +
    +      celo_token = Token.get_by_contract_address_hash(celo_token_address_hash, api?: true)
    +
    +      %{
    +        recipient: address_with_info,
    +        amount: base_fee,
    +        token:
    +          TokenView.render("token.json", %{
    +            token: celo_token,
    +            contract_address_hash: celo_token.contract_address_hash
    +          }),
    +        breakdown: []
    +      }
    +    else
    +      _ ->
    +        nil
    +    end
    +  end
    +
    +  defp maybe_add_base_fee_info(celo_json, block_or_transaction, true) do
    +    base_fee_breakdown_json = render("celo_base_fee.json", block_or_transaction)
    +    Map.put(celo_json, "base_fee", base_fee_breakdown_json)
    +  end
    +
    +  defp maybe_add_base_fee_info(celo_json, _block_or_transaction, false),
    +    do: celo_json
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v2/config_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v2/config_view.ex
    new file mode 100644
    index 0000000..3e744cc
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v2/config_view.ex
    @@ -0,0 +1,7 @@
    +defmodule BlockScoutWeb.API.V2.ConfigView do
    +  def render("backend_version.json", %{version: version}) do
    +    %{
    +      "backend_version" => version
    +    }
    +  end
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v2/ethereum_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v2/ethereum_view.ex
    new file mode 100644
    index 0000000..1285aea
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v2/ethereum_view.ex
    @@ -0,0 +1,53 @@
    +defmodule BlockScoutWeb.API.V2.EthereumView do
    +  alias Explorer.Chain.{Block, Transaction}
    +
    +  defp count_blob_transactions(%Block{transactions: transactions}) when is_list(transactions),
    +    # EIP-2718 blob transaction type
    +    do: Enum.count(transactions, &(&1.type == 3))
    +
    +  defp count_blob_transactions(_), do: nil
    +
    +  def extend_transaction_json_response(out_json, %Transaction{} = transaction) do
    +    case Map.get(transaction, :beacon_blob_transaction) do
    +      nil ->
    +        out_json
    +
    +      %Ecto.Association.NotLoaded{} ->
    +        out_json
    +
    +      item ->
    +        out_json
    +        |> Map.put("max_fee_per_blob_gas", item.max_fee_per_blob_gas)
    +        |> Map.put("blob_versioned_hashes", item.blob_versioned_hashes)
    +        |> Map.put("blob_gas_used", item.blob_gas_used)
    +        |> Map.put("blob_gas_price", item.blob_gas_price)
    +        |> Map.put("burnt_blob_fee", Decimal.mult(item.blob_gas_used, item.blob_gas_price))
    +    end
    +  end
    +
    +  def extend_block_json_response(out_json, %Block{} = block, single_block?) do
    +    blob_gas_used = Map.get(block, :blob_gas_used)
    +    excess_blob_gas = Map.get(block, :excess_blob_gas)
    +
    +    blob_transaction_count = count_blob_transactions(block)
    +
    +    extended_out_json =
    +      out_json
    +      |> Map.put("blob_transactions_count", blob_transaction_count)
    +      # todo: It should be removed in favour `blob_transactions_count` property with the next release after 8.0.0
    +      |> Map.put("blob_transaction_count", blob_transaction_count)
    +      |> Map.put("blob_gas_used", blob_gas_used)
    +      |> Map.put("excess_blob_gas", excess_blob_gas)
    +
    +    if single_block? do
    +      blob_gas_price = Block.transaction_blob_gas_price(block.transactions)
    +      burnt_blob_transaction_fees = Decimal.mult(blob_gas_used || 0, blob_gas_price || 0)
    +
    +      extended_out_json
    +      |> Map.put("blob_gas_price", blob_gas_price)
    +      |> Map.put("burnt_blob_fees", burnt_blob_transaction_fees)
    +    else
    +      extended_out_json
    +    end
    +  end
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v2/filecoin_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v2/filecoin_view.ex
    new file mode 100644
    index 0000000..e6c09d8
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v2/filecoin_view.ex
    @@ -0,0 +1,114 @@
    +defmodule BlockScoutWeb.API.V2.FilecoinView do
    +  @moduledoc """
    +  View functions for rendering Filecoin-related data in JSON format.
    +  """
    +  use Utils.CompileTimeEnvHelper, chain_type: [:explorer, :chain_type]
    +
    +  if @chain_type == :filecoin do
    +    # TODO: remove when https://github.com/elixir-lang/elixir/issues/13975 comes to elixir release
    +    alias Explorer.Chain, warn: false
    +    alias Explorer.Chain.Address, warn: false
    +
    +    @api_true [api?: true]
    +
    +    @doc """
    +    Extends the json output with a sub-map containing information related to
    +    Filecoin native addressing.
    +    """
    +    @spec extend_address_json_response(map(), Address.t()) :: map()
    +    def extend_address_json_response(
    +          result,
    +          %Address{filecoin_id: filecoin_id, filecoin_robust: filecoin_robust, filecoin_actor_type: filecoin_actor_type}
    +        ) do
    +      Map.put(result, :filecoin, %{
    +        id: filecoin_id,
    +        robust: filecoin_robust,
    +        actor_type: filecoin_actor_type
    +      })
    +    end
    +
    +    @spec preload_and_put_filecoin_robust_address(map(), %{
    +            optional(:address_hash) => String.t() | nil,
    +            optional(:field_prefix) => String.t() | nil,
    +            optional(any) => any
    +          }) ::
    +            map()
    +    def preload_and_put_filecoin_robust_address(result, %{address_hash: address_hash} = params) do
    +      address = address_hash && Address.get(address_hash, @api_true)
    +
    +      put_filecoin_robust_address(result, Map.put(params, :address, address))
    +    end
    +
    +    def preload_and_put_filecoin_robust_address(result, _params) do
    +      result
    +    end
    +
    +    @doc """
    +    Adds a Filecoin robust address to the given result.
    +
    +    ## Parameters
    +
    +      - result: The initial result to which the Filecoin robust address will be added.
    +      - opts: A map containing the following keys:
    +        - `:address` - A struct containing the `filecoin_robust` address.
    +        - `:field_prefix` - A prefix to be used for the field name in the result.
    +
    +    ## Returns
    +
    +    The updated result with the Filecoin robust address added.
    +    """
    +    @spec put_filecoin_robust_address(map(), %{
    +            required(:address) => Address.t(),
    +            required(:field_prefix) => String.t() | nil,
    +            optional(any) => any
    +          }) :: map()
    +    def put_filecoin_robust_address(result, %{
    +          address: %Address{filecoin_robust: filecoin_robust},
    +          field_prefix: field_prefix
    +        }) do
    +      put_filecoin_robust_address_internal(result, filecoin_robust, field_prefix)
    +    end
    +
    +    def put_filecoin_robust_address(result, %{field_prefix: field_prefix}) do
    +      put_filecoin_robust_address_internal(result, nil, field_prefix)
    +    end
    +
    +    defp put_filecoin_robust_address_internal(result, filecoin_robust, field_prefix) do
    +      field_name = (field_prefix && "#{field_prefix}_filecoin_robust_address") || "filecoin_robust_address"
    +      Map.put(result, field_name, filecoin_robust)
    +    end
    +
    +    @doc """
    +    Preloads and inserts Filecoin robust addresses into the search results.
    +
    +    ## Parameters
    +
    +      - search_results: The search results that need to be enriched with Filecoin robust addresses.
    +
    +    ## Returns
    +
    +      - The search results with preloaded Filecoin robust addresses.
    +    """
    +    @spec preload_and_put_filecoin_robust_address_to_search_results(list()) :: list()
    +    def preload_and_put_filecoin_robust_address_to_search_results(search_results) do
    +      addresses_map =
    +        search_results
    +        |> Enum.map(& &1["address_hash"])
    +        |> Enum.reject(&is_nil/1)
    +        |> Chain.hashes_to_addresses(@api_true)
    +        |> Enum.into(%{}, &{to_string(&1.hash), &1})
    +
    +      search_results
    +      |> Enum.map(fn
    +        %{"address_hash" => address_hash} = result when not is_nil(address_hash) ->
    +          address = addresses_map[String.downcase(address_hash)]
    +          put_filecoin_robust_address(result, %{address: address, field_prefix: nil})
    +
    +        other ->
    +          other
    +      end)
    +    end
    +  end
    +end
    +
    +# end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v2/helper.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v2/helper.ex
    new file mode 100644
    index 0000000..1183765
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v2/helper.ex
    @@ -0,0 +1,256 @@
    +defmodule BlockScoutWeb.API.V2.Helper do
    +  @moduledoc """
    +    API V2 helper
    +  """
    +  use Utils.CompileTimeEnvHelper, chain_type: [:explorer, :chain_type]
    +
    +  alias Ecto.Association.NotLoaded
    +  alias Explorer.Chain.{Address, SmartContract}
    +  alias Explorer.Chain.SmartContract.Proxy
    +  alias Explorer.Chain.Transaction.History.TransactionStats
    +
    +  import BlockScoutWeb.Account.AuthController, only: [current_user: 1]
    +  import BlockScoutWeb.Models.GetAddressTags, only: [get_address_tags: 3]
    +
    +  def address_with_info(conn, address, address_hash, tags_needed?, watchlist_names_cached \\ nil)
    +
    +  def address_with_info(_, _, nil, _, _) do
    +    nil
    +  end
    +
    +  def address_with_info(conn, address, address_hash, true, nil) do
    +    %{
    +      common_tags: public_tags,
    +      personal_tags: private_tags,
    +      watchlist_names: watchlist_names
    +    } = get_address_tags(address_hash, current_user(conn), api?: true)
    +
    +    Map.merge(address_with_info(address, address_hash), %{
    +      "private_tags" => private_tags,
    +      "watchlist_names" => watchlist_names,
    +      "public_tags" => public_tags
    +    })
    +  end
    +
    +  def address_with_info(_conn, address, address_hash, false, nil) do
    +    Map.merge(address_with_info(address, address_hash), %{
    +      "private_tags" => [],
    +      "watchlist_names" => [],
    +      "public_tags" => []
    +    })
    +  end
    +
    +  def address_with_info(_conn, address, address_hash, _, watchlist_names_cached) do
    +    watchlist_name = watchlist_names_cached[address_hash]
    +
    +    Map.merge(address_with_info(address, address_hash), %{
    +      "private_tags" => [],
    +      "watchlist_names" => if(watchlist_name, do: [watchlist_name], else: []),
    +      "public_tags" => []
    +    })
    +  end
    +
    +  @doc """
    +  Gets address with the additional info for api v2
    +  """
    +  @spec address_with_info(any(), any()) :: nil | %{optional(String.t()) => any()}
    +  def address_with_info(
    +        %Address{proxy_implementations: %NotLoaded{}, contract_code: contract_code} = _address,
    +        _address_hash
    +      )
    +      when not is_nil(contract_code) do
    +    raise "proxy_implementations is not loaded for address"
    +  end
    +
    +  def address_with_info(%Address{} = address, _address_hash) do
    +    smart_contract? = Address.smart_contract?(address)
    +
    +    proxy_implementations =
    +      case address.proxy_implementations do
    +        %NotLoaded{} ->
    +          nil
    +
    +        nil ->
    +          nil
    +
    +        proxy_implementations ->
    +          proxy_implementations
    +      end
    +
    +    %{
    +      "hash" => Address.checksum(address),
    +      "is_contract" => smart_contract?,
    +      "name" => address_name(address),
    +      "is_scam" => address_marked_as_scam?(address),
    +      "proxy_type" => proxy_implementations && proxy_implementations.proxy_type,
    +      "implementations" => Proxy.proxy_object_info(proxy_implementations),
    +      "is_verified" => smart_contract_verified?(address) || verified_as_proxy?(proxy_implementations),
    +      "ens_domain_name" => address.ens_domain_name,
    +      "metadata" => address.metadata
    +    }
    +    |> address_chain_type_fields(address)
    +  end
    +
    +  def address_with_info(%NotLoaded{}, address_hash) do
    +    address_with_info(nil, address_hash)
    +  end
    +
    +  def address_with_info(address_info, address_hash) when is_map(address_info) do
    +    nil
    +    |> address_with_info(address_hash)
    +    |> Map.put("ens_domain_name", address_info[:ens_domain_name])
    +    |> Map.put("metadata", address_info[:metadata])
    +  end
    +
    +  def address_with_info(nil, nil) do
    +    nil
    +  end
    +
    +  def address_with_info(_, address_hash) do
    +    %{
    +      "hash" => Address.checksum(address_hash),
    +      "is_contract" => false,
    +      "name" => nil,
    +      "proxy_type" => nil,
    +      "implementations" => [],
    +      "is_verified" => nil,
    +      "ens_domain_name" => nil,
    +      "metadata" => nil
    +    }
    +  end
    +
    +  case @chain_type do
    +    :filecoin ->
    +      defp address_chain_type_fields(result, address) do
    +        # credo:disable-for-next-line Credo.Check.Design.AliasUsage
    +        BlockScoutWeb.API.V2.FilecoinView.extend_address_json_response(result, address)
    +      end
    +
    +    _ ->
    +      defp address_chain_type_fields(result, _address) do
    +        result
    +      end
    +  end
    +
    +  # We treat contracts with minimal proxy or similar standards as verified if all their implementations are verified
    +  defp verified_as_proxy?(%{proxy_type: proxy_type, names: names})
    +       when proxy_type in [:eip1167, :eip7702, :clone_with_immutable_arguments, :erc7760] do
    +    !Enum.empty?(names) && Enum.all?(names)
    +  end
    +
    +  defp verified_as_proxy?(_), do: false
    +
    +  def address_name(%Address{names: [_ | _] = address_names}) do
    +    case Enum.find(address_names, &(&1.primary == true)) do
    +      nil ->
    +        # take last created address name, if there is no `primary` one.
    +        %Address.Name{name: name} = Enum.max_by(address_names, & &1.id)
    +        name
    +
    +      %Address.Name{name: name} ->
    +        name
    +    end
    +  end
    +
    +  def address_name(_), do: nil
    +
    +  def address_marked_as_scam?(%Address{scam_badge: %Ecto.Association.NotLoaded{}}) do
    +    false
    +  end
    +
    +  def address_marked_as_scam?(%Address{scam_badge: scam_badge}) when not is_nil(scam_badge) do
    +    true
    +  end
    +
    +  def address_marked_as_scam?(_), do: false
    +
    +  @doc """
    +  Determines if a smart contract is verified.
    +
    +  ## Parameters
    +    - address: An `%Address{}` struct containing smart contract information.
    +
    +  ## Returns
    +    - `false` if the smart contract has metadata from a verified bytecode twin.
    +    - `false` if the smart contract is `nil`.
    +    - `false` if the smart contract is `NotLoaded`.
    +    - `true` if the smart contract is present and does not have metadata from a verified bytecode twin.
    +  """
    +  @spec smart_contract_verified?(Address.t()) :: boolean()
    +  def smart_contract_verified?(%Address{smart_contract: nil}), do: false
    +  def smart_contract_verified?(%Address{smart_contract: %{metadata_from_verified_bytecode_twin: true}}), do: false
    +  def smart_contract_verified?(%Address{smart_contract: %NotLoaded{}}), do: nil
    +  def smart_contract_verified?(%Address{smart_contract: %SmartContract{}}), do: true
    +
    +  def market_cap(:standard, %{available_supply: available_supply, fiat_value: fiat_value, market_cap: market_cap})
    +      when is_nil(available_supply) or is_nil(fiat_value) do
    +    max(Decimal.new(0), market_cap)
    +  end
    +
    +  def market_cap(:standard, %{available_supply: available_supply, fiat_value: fiat_value}) do
    +    Decimal.mult(available_supply, fiat_value)
    +  end
    +
    +  def market_cap(module, exchange_rate) do
    +    module.market_cap(exchange_rate)
    +  end
    +
    +  def get_transaction_stats do
    +    stats_scale = date_range(1)
    +    transaction_stats = TransactionStats.by_date_range(stats_scale.earliest, stats_scale.latest)
    +
    +    # Need datapoint for legend if none currently available.
    +    if Enum.empty?(transaction_stats) do
    +      [%{number_of_transactions: 0, gas_used: 0}]
    +    else
    +      transaction_stats
    +    end
    +  end
    +
    +  def date_range(num_days) do
    +    today = Date.utc_today()
    +    latest = Date.add(today, -1)
    +    x_days_back = Date.add(latest, -1 * (num_days - 1))
    +    %{earliest: x_days_back, latest: latest}
    +  end
    +
    +  @doc """
    +    Checks if an item associated with a DB entity has actual value
    +
    +    ## Parameters
    +    - `associated_item`: an item associated with a DB entity
    +
    +    ## Returns
    +    - `false`: if the item is nil or not loaded
    +    - `true`: if the item has actual value
    +  """
    +  @spec specified?(any()) :: boolean()
    +  def specified?(associated_item) do
    +    case associated_item do
    +      nil -> false
    +      %Ecto.Association.NotLoaded{} -> false
    +      _ -> true
    +    end
    +  end
    +
    +  @doc """
    +    Gets the value of an element nested in a map using two keys.
    +
    +    Clarification: Returns `map[key1][key2]`
    +
    +    ## Parameters
    +    - `map`: The high-level map.
    +    - `key1`: The key of the element in `map`.
    +    - `key2`: The key of the element in the map accessible by `map[key1]`.
    +
    +    ## Returns
    +    The value of the element, or `nil` if the map accessible by `key1` does not exist.
    +  """
    +  @spec get_2map_data(map(), any(), any()) :: any()
    +  def get_2map_data(map, key1, key2) do
    +    case Map.get(map, key1) do
    +      nil -> nil
    +      inner_map -> Map.get(inner_map, key2)
    +    end
    +  end
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v2/internal_transaction_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v2/internal_transaction_view.ex
    new file mode 100644
    index 0000000..d67b9f5
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v2/internal_transaction_view.ex
    @@ -0,0 +1,58 @@
    +defmodule BlockScoutWeb.API.V2.InternalTransactionView do
    +  use BlockScoutWeb, :view
    +
    +  alias BlockScoutWeb.API.V2.Helper
    +  alias Explorer.Chain.{Block, InternalTransaction}
    +
    +  def render("internal_transaction.json", %{internal_transaction: nil}) do
    +    nil
    +  end
    +
    +  def render("internal_transaction.json", %{
    +        internal_transaction: internal_transaction,
    +        block: block
    +      }) do
    +    prepare_internal_transaction(internal_transaction, block)
    +  end
    +
    +  def render("internal_transactions.json", %{
    +        internal_transactions: internal_transactions,
    +        next_page_params: next_page_params
    +      }) do
    +    %{
    +      "items" => Enum.map(internal_transactions, &prepare_internal_transaction(&1, &1.block)),
    +      "next_page_params" => next_page_params
    +    }
    +  end
    +
    +  @doc """
    +    Prepares internal transaction object to be returned in the API v2 endpoints.
    +  """
    +  @spec prepare_internal_transaction(InternalTransaction.t(), Block.t() | nil) :: map()
    +  def prepare_internal_transaction(internal_transaction, block \\ nil) do
    +    %{
    +      "error" => internal_transaction.error,
    +      "success" => is_nil(internal_transaction.error),
    +      "type" => internal_transaction.call_type || internal_transaction.type,
    +      "transaction_hash" => internal_transaction.transaction_hash,
    +      "transaction_index" => internal_transaction.transaction_index,
    +      "from" =>
    +        Helper.address_with_info(nil, internal_transaction.from_address, internal_transaction.from_address_hash, false),
    +      "to" =>
    +        Helper.address_with_info(nil, internal_transaction.to_address, internal_transaction.to_address_hash, false),
    +      "created_contract" =>
    +        Helper.address_with_info(
    +          nil,
    +          internal_transaction.created_contract_address,
    +          internal_transaction.created_contract_address_hash,
    +          false
    +        ),
    +      "value" => internal_transaction.value,
    +      "block_number" => internal_transaction.block_number,
    +      "timestamp" => (block && block.timestamp) || internal_transaction.block.timestamp,
    +      "index" => internal_transaction.index,
    +      "gas_limit" => internal_transaction.gas,
    +      "block_index" => internal_transaction.block_index
    +    }
    +  end
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v2/mud_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v2/mud_view.ex
    new file mode 100644
    index 0000000..6a49913
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v2/mud_view.ex
    @@ -0,0 +1,124 @@
    +defmodule BlockScoutWeb.API.V2.MudView do
    +  use BlockScoutWeb, :view
    +
    +  alias BlockScoutWeb.API.V2.Helper
    +  alias Explorer.Chain.{Address, Mud, Mud.Table}
    +
    +  @doc """
    +    Function to render GET requests to `/api/v2/mud/worlds` endpoint.
    +  """
    +  @spec render(String.t(), map()) :: map()
    +  def render("worlds.json", %{worlds: worlds, next_page_params: next_page_params}) do
    +    %{
    +      items: worlds |> Enum.map(&prepare_world_for_list/1),
    +      next_page_params: next_page_params
    +    }
    +  end
    +
    +  @doc """
    +    Function to render GET requests to `/api/v2/mud/worlds/count` endpoint.
    +  """
    +  def render("count.json", %{count: count}) do
    +    count
    +  end
    +
    +  @doc """
    +    Function to render GET requests to `/api/v2/mud/worlds/:world/tables` endpoint.
    +  """
    +  def render("tables.json", %{tables: tables, next_page_params: next_page_params}) do
    +    %{
    +      items: tables |> Enum.map(&%{table: Table.from(&1 |> elem(0)), schema: &1 |> elem(1)}),
    +      next_page_params: next_page_params
    +    }
    +  end
    +
    +  @doc """
    +    Function to render GET requests to `/api/v2/mud/worlds/:world/systems` endpoint.
    +  """
    +  def render("systems.json", %{systems: systems}) do
    +    %{
    +      items: systems |> Enum.map(&prepare_system_for_list/1)
    +    }
    +  end
    +
    +  @doc """
    +    Function to render GET requests to `/api/v2/mud/worlds/:world/systems/:system` endpoint.
    +  """
    +  def render("system.json", %{system_id: system_id, abi: abi}) do
    +    %{
    +      name: system_id |> Table.from() |> Map.get(:table_full_name),
    +      abi: abi
    +    }
    +  end
    +
    +  @doc """
    +    Function to render GET requests to `/api/v2/mud/worlds/:world/tables/:table_id/records` endpoint.
    +  """
    +  def render("records.json", %{
    +        records: records,
    +        table_id: table_id,
    +        schema: schema,
    +        blocks: blocks,
    +        next_page_params: next_page_params
    +      }) do
    +    %{
    +      items: records |> Enum.map(&format_record(&1, schema, blocks)),
    +      table: table_id |> Table.from(),
    +      schema: schema,
    +      next_page_params: next_page_params
    +    }
    +  end
    +
    +  @doc """
    +    Function to render GET requests to `/api/v2/mud/worlds/:world/tables/:table_id/records/:record_id` endpoint.
    +  """
    +  def render("record.json", %{record: record, table_id: table_id, blocks: blocks, schema: schema}) do
    +    %{
    +      record: record |> format_record(schema, blocks),
    +      table: table_id |> Table.from(),
    +      schema: schema
    +    }
    +  end
    +
    +  defp prepare_world_for_list(%Address{} = address) do
    +    %{
    +      "address_hash" => Helper.address_with_info(address, address.hash),
    +      # todo: "address" should be removed in favour `address_hash` property with the next release after 8.0.0
    +      "address" => Helper.address_with_info(address, address.hash),
    +      "transactions_count" => address.transactions_count,
    +      # todo: It should be removed in favour `transactions_count` property with the next release after 8.0.0
    +      "transaction_count" => address.transactions_count,
    +      "coin_balance" => if(address.fetched_coin_balance, do: address.fetched_coin_balance.value)
    +    }
    +  end
    +
    +  defp prepare_system_for_list({system_id, system}) do
    +    %{
    +      name: system_id |> Table.from() |> Map.get(:table_full_name),
    +      address_hash: system,
    +      # todo: "address" should be removed in favour `address_hash` property with the next release after 8.0.0
    +      address: system
    +    }
    +  end
    +
    +  defp format_record(nil, _schema, _blocks), do: nil
    +
    +  defp format_record(record, schema, blocks) do
    +    %{
    +      id: record.key_bytes,
    +      raw: %{
    +        key_bytes: record.key_bytes,
    +        key0: record.key0,
    +        key1: record.key1,
    +        static_data: record.static_data,
    +        encoded_lengths: record.encoded_lengths,
    +        dynamic_data: record.dynamic_data,
    +        block_number: record.block_number,
    +        log_index: record.log_index
    +      },
    +      is_deleted: record.is_deleted,
    +      decoded: Mud.decode_record(record, schema),
    +      timestamp: blocks |> Map.get(Decimal.to_integer(record.block_number), nil)
    +    }
    +  end
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v2/optimism_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v2/optimism_view.ex
    new file mode 100644
    index 0000000..c412214
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v2/optimism_view.ex
    @@ -0,0 +1,468 @@
    +defmodule BlockScoutWeb.API.V2.OptimismView do
    +  use BlockScoutWeb, :view
    +
    +  import Ecto.Query, only: [from: 2]
    +
    +  alias BlockScoutWeb.API.V2.Helper
    +  alias Explorer.{Chain, Repo}
    +  alias Explorer.Helper, as: ExplorerHelper
    +  alias Explorer.Chain.{Block, Transaction}
    +  alias Explorer.Chain.Optimism.{FrameSequence, FrameSequenceBlob, InteropMessage, Withdrawal}
    +
    +  @doc """
    +    Function to render GET requests to `/api/v2/optimism/txn-batches` endpoint.
    +  """
    +  @spec render(binary(), map()) :: map() | list() | non_neg_integer()
    +  def render("optimism_transaction_batches.json", %{
    +        batches: batches,
    +        next_page_params: next_page_params
    +      }) do
    +    items =
    +      batches
    +      |> Enum.map(fn batch ->
    +        Task.async(fn ->
    +          transaction_count =
    +            Repo.replica().aggregate(
    +              from(
    +                t in Transaction,
    +                inner_join: b in Block,
    +                on: b.hash == t.block_hash and b.consensus == true,
    +                where: t.block_number == ^batch.l2_block_number
    +              ),
    +              :count,
    +              timeout: :infinity
    +            )
    +
    +          %{
    +            "l2_block_number" => batch.l2_block_number,
    +            "transactions_count" => transaction_count,
    +            # todo: It should be removed in favour `transactions_count` property with the next release after 8.0.0
    +            "transaction_count" => transaction_count,
    +            "l1_transaction_hashes" => batch.frame_sequence.l1_transaction_hashes,
    +            "l1_timestamp" => batch.frame_sequence.l1_timestamp
    +          }
    +        end)
    +      end)
    +      |> Task.yield_many(:infinity)
    +      |> Enum.map(fn {_task, {:ok, item}} -> item end)
    +
    +    %{
    +      items: items,
    +      next_page_params: next_page_params
    +    }
    +  end
    +
    +  @doc """
    +    Function to render GET requests to `/api/v2/optimism/batches` endpoint.
    +  """
    +  def render("optimism_batches.json", %{
    +        batches: batches,
    +        next_page_params: next_page_params
    +      }) do
    +    items =
    +      batches
    +      |> Enum.map(fn batch ->
    +        from..to//_ = batch.l2_block_range
    +
    +        render_base_info_for_batch(batch.id, from, to, batch.transactions_count, batch)
    +      end)
    +
    +    %{
    +      items: items,
    +      next_page_params: next_page_params
    +    }
    +  end
    +
    +  @doc """
    +    Function to render GET requests to `/api/v2/optimism/batches/da/celestia/:height/:commitment`
    +    and `/api/v2/optimism/batches/:number` endpoints.
    +  """
    +  def render("optimism_batch.json", %{batch: batch}) do
    +    batch
    +  end
    +
    +  @doc """
    +    Function to render GET requests to `/api/v2/optimism/output-roots` endpoint.
    +  """
    +  def render("optimism_output_roots.json", %{
    +        roots: roots,
    +        next_page_params: next_page_params
    +      }) do
    +    %{
    +      items:
    +        Enum.map(roots, fn r ->
    +          %{
    +            "l2_output_index" => r.l2_output_index,
    +            "l2_block_number" => r.l2_block_number,
    +            "l1_transaction_hash" => r.l1_transaction_hash,
    +            "l1_timestamp" => r.l1_timestamp,
    +            "l1_block_number" => r.l1_block_number,
    +            "output_root" => r.output_root
    +          }
    +        end),
    +      next_page_params: next_page_params
    +    }
    +  end
    +
    +  @doc """
    +    Function to render GET requests to `/api/v2/optimism/games` endpoint.
    +  """
    +  def render("optimism_games.json", %{
    +        games: games,
    +        next_page_params: next_page_params
    +      }) do
    +    %{
    +      items:
    +        Enum.map(games, fn g ->
    +          status =
    +            case g.status do
    +              0 -> "In progress"
    +              1 -> "Challenger wins"
    +              2 -> "Defender wins"
    +            end
    +
    +          [l2_block_number] = ExplorerHelper.decode_data(g.extra_data, [{:uint, 256}])
    +
    +          %{
    +            "index" => g.index,
    +            "game_type" => g.game_type,
    +            # todo: It should be removed in favour `contract_address_hash` property with the next release after 8.0.0
    +            "contract_address" => g.address,
    +            "contract_address_hash" => g.address,
    +            "l2_block_number" => l2_block_number,
    +            "created_at" => g.created_at,
    +            "status" => status,
    +            "resolved_at" => g.resolved_at
    +          }
    +        end),
    +      next_page_params: next_page_params
    +    }
    +  end
    +
    +  @doc """
    +    Function to render GET requests to `/api/v2/optimism/deposits` endpoint.
    +  """
    +  def render("optimism_deposits.json", %{
    +        deposits: deposits,
    +        next_page_params: next_page_params
    +      }) do
    +    %{
    +      items:
    +        Enum.map(deposits, fn deposit ->
    +          %{
    +            "l1_block_number" => deposit.l1_block_number,
    +            "l2_transaction_hash" => deposit.l2_transaction_hash,
    +            "l1_block_timestamp" => deposit.l1_block_timestamp,
    +            "l1_transaction_hash" => deposit.l1_transaction_hash,
    +            "l1_transaction_origin" => deposit.l1_transaction_origin,
    +            "l2_transaction_gas_limit" => deposit.l2_transaction.gas
    +          }
    +        end),
    +      next_page_params: next_page_params
    +    }
    +  end
    +
    +  @doc """
    +    Function to render GET requests to `/api/v2/main-page/optimism-deposits` endpoint.
    +  """
    +  def render("optimism_deposits.json", %{deposits: deposits}) do
    +    Enum.map(deposits, fn deposit ->
    +      %{
    +        "l1_block_number" => deposit.l1_block_number,
    +        "l1_block_timestamp" => deposit.l1_block_timestamp,
    +        "l1_transaction_hash" => deposit.l1_transaction_hash,
    +        "l2_transaction_hash" => deposit.l2_transaction_hash
    +      }
    +    end)
    +  end
    +
    +  @doc """
    +    Function to render GET requests to `/api/v2/optimism/withdrawals` endpoint.
    +  """
    +  def render("optimism_withdrawals.json", %{
    +        withdrawals: withdrawals,
    +        next_page_params: next_page_params,
    +        conn: conn
    +      }) do
    +    respected_games = Withdrawal.respected_games()
    +
    +    %{
    +      items:
    +        Enum.map(withdrawals, fn w ->
    +          msg_nonce =
    +            Bitwise.band(
    +              Decimal.to_integer(w.msg_nonce),
    +              0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
    +            )
    +
    +          msg_nonce_version = Bitwise.bsr(Decimal.to_integer(w.msg_nonce), 240)
    +
    +          {from_address, from_address_hash} =
    +            with false <- is_nil(w.from),
    +                 {:ok, address} <-
    +                   Chain.hash_to_address(
    +                     w.from,
    +                     necessity_by_association: %{
    +                       :names => :optional,
    +                       :smart_contract => :optional,
    +                       proxy_implementations_association() => :optional
    +                     },
    +                     api?: true
    +                   ) do
    +              {address, address.hash}
    +            else
    +              _ -> {nil, nil}
    +            end
    +
    +          {status, challenge_period_end} = Withdrawal.status(w, respected_games)
    +
    +          %{
    +            "msg_nonce_raw" => Decimal.to_string(w.msg_nonce, :normal),
    +            "msg_nonce" => msg_nonce,
    +            "msg_nonce_version" => msg_nonce_version,
    +            "from" => Helper.address_with_info(conn, from_address, from_address_hash, w.from),
    +            "l2_transaction_hash" => w.l2_transaction_hash,
    +            "l2_timestamp" => w.l2_timestamp,
    +            "status" => status,
    +            "l1_transaction_hash" => w.l1_transaction_hash,
    +            "challenge_period_end" => challenge_period_end
    +          }
    +        end),
    +      next_page_params: next_page_params
    +    }
    +  end
    +
    +  @doc """
    +    Function to render GET requests to `/api/v2/optimism/:entity/count` endpoints.
    +  """
    +  def render("optimism_items_count.json", %{count: count}) do
    +    count
    +  end
    +
    +  @doc """
    +    Function to render GET requests to `/api/v2/optimism/interop/messages` endpoint.
    +  """
    +  def render("optimism_interop_messages.json", %{
    +        messages: messages,
    +        next_page_params: next_page_params
    +      }) do
    +    %{
    +      items:
    +        Enum.map(messages, fn message ->
    +          msg =
    +            %{
    +              "nonce" => message.nonce,
    +              "timestamp" => message.timestamp,
    +              "status" => message.status,
    +              "init_transaction_hash" => message.init_transaction_hash,
    +              "relay_transaction_hash" => message.relay_transaction_hash,
    +              "sender_address_hash" => message.sender_address_hash,
    +              # todo: keep next line for compatibility with frontend and remove when new frontend is bound to `sender_address_hash` property
    +              "sender" => message.sender_address_hash,
    +              "target_address_hash" => message.target_address_hash,
    +              # todo: keep next line for compatibility with frontend and remove when new frontend is bound to `target_address_hash` property
    +              "target" => message.target_address_hash,
    +              "payload" => ExplorerHelper.add_0x_prefix(message.payload)
    +            }
    +
    +          # add chain info depending on whether this is incoming or outgoing message
    +          msg
    +          |> maybe_add_chain(:init_chain, message)
    +          |> maybe_add_chain(:relay_chain, message)
    +        end),
    +      next_page_params: next_page_params
    +    }
    +  end
    +
    +  @doc """
    +    Function to render GET requests to `/api/v2/optimism/interop/public-key` endpoint.
    +  """
    +  def render("optimism_interop_public_key.json", %{public_key: public_key}) do
    +    %{"public_key" => public_key}
    +  end
    +
    +  @doc """
    +    Function to render `relay` response for the POST request to `/api/v2/import/optimism/interop/` endpoint.
    +  """
    +  def render("optimism_interop_response.json", %{relay_transaction_hash: relay_transaction_hash, failed: failed}) do
    +    %{
    +      "relay_transaction_hash" => relay_transaction_hash,
    +      "failed" => failed
    +    }
    +  end
    +
    +  @doc """
    +    Function to render `init` response for the POST request to `/api/v2/import/optimism/interop/` endpoint.
    +  """
    +  def render("optimism_interop_response.json", %{
    +        sender_address_hash: sender_address_hash,
    +        target_address_hash: target_address_hash,
    +        init_transaction_hash: init_transaction_hash,
    +        timestamp: timestamp,
    +        payload: payload
    +      }) do
    +    %{
    +      "sender_address_hash" => sender_address_hash,
    +      "target_address_hash" => target_address_hash,
    +      "init_transaction_hash" => init_transaction_hash,
    +      "timestamp" => if(not is_nil(timestamp), do: DateTime.to_unix(timestamp)),
    +      "payload" => ExplorerHelper.add_0x_prefix(payload)
    +    }
    +  end
    +
    +  # Transforms an L1 batch into a map format for HTTP response.
    +  #
    +  # This function processes an Optimism L1 batch and converts it into a map that
    +  # includes basic batch information.
    +  #
    +  # ## Parameters
    +  # - `internal_id`: The internal ID of the batch.
    +  # - `l2_block_number_from`: Start L2 block number of the batch block range.
    +  # - `l2_block_number_to`: End L2 block number of the batch block range.
    +  # - `transaction_count`: The L2 transaction count included into the blocks of the range.
    +  # - `batch`: Either an `Explorer.Chain.Optimism.FrameSequence` entry or a map with
    +  #            the corresponding fields.
    +  #
    +  # ## Returns
    +  # - A map with detailed information about the batch formatted for use in JSON HTTP responses.
    +  @spec render_base_info_for_batch(
    +          non_neg_integer(),
    +          non_neg_integer(),
    +          non_neg_integer(),
    +          non_neg_integer(),
    +          FrameSequence.t()
    +          | %{:l1_timestamp => DateTime.t(), :l1_transaction_hashes => list(), optional(any()) => any()}
    +        ) :: %{
    +          :number => non_neg_integer(),
    +          :internal_id => non_neg_integer(),
    +          :l1_timestamp => DateTime.t(),
    +          :l2_start_block_number => non_neg_integer(),
    +          :l2_block_start => non_neg_integer(),
    +          :l2_end_block_number => non_neg_integer(),
    +          :l2_block_end => non_neg_integer(),
    +          :transactions_count => non_neg_integer(),
    +          :transaction_count => non_neg_integer(),
    +          :l1_transaction_hashes => list(),
    +          :batch_data_container => :in_blob4844 | :in_celestia | :in_calldata | nil
    +        }
    +  defp render_base_info_for_batch(internal_id, l2_block_number_from, l2_block_number_to, transaction_count, batch) do
    +    FrameSequence.prepare_base_info_for_batch(
    +      internal_id,
    +      l2_block_number_from,
    +      l2_block_number_to,
    +      transaction_count,
    +      batch.batch_data_container,
    +      batch
    +    )
    +  end
    +
    +  @doc """
    +    Extends the json output for a block using Optimism frame sequence (bound
    +    with the provided L2 block) - adds info about L1 batch to the output.
    +
    +    ## Parameters
    +    - `out_json`: A map defining output json which will be extended.
    +    - `block`: block structure containing frame sequence info related to the block.
    +
    +    ## Returns
    +    An extended map containing `optimism` item with the Optimism batch info
    +    (L1 transaction hashes, timestamp, related blobs).
    +  """
    +  @spec extend_block_json_response(map(), %{
    +          :__struct__ => Explorer.Chain.Block,
    +          :op_frame_sequence => any(),
    +          optional(any()) => any()
    +        }) :: map()
    +  def extend_block_json_response(out_json, %Block{} = block) do
    +    frame_sequence = Map.get(block, :op_frame_sequence)
    +
    +    if is_nil(frame_sequence) do
    +      out_json
    +    else
    +      {batch_data_container, blobs} = FrameSequenceBlob.list(frame_sequence.id, api?: true)
    +
    +      batch_info =
    +        %{
    +          "number" => frame_sequence.id,
    +          # todo: It should be removed in favour `number` property with the next release after 8.0.0
    +          "internal_id" => frame_sequence.id,
    +          "l1_timestamp" => frame_sequence.l1_timestamp,
    +          "l1_transaction_hashes" => frame_sequence.l1_transaction_hashes,
    +          "batch_data_container" => batch_data_container
    +        }
    +        |> extend_batch_info_by_blobs(blobs, "blobs")
    +
    +      Map.put(out_json, "optimism", batch_info)
    +    end
    +  end
    +
    +  defp extend_batch_info_by_blobs(batch_info, blobs, field_name) do
    +    if Enum.empty?(blobs) do
    +      batch_info
    +    else
    +      Map.put(batch_info, field_name, blobs)
    +    end
    +  end
    +
    +  @doc """
    +    Extends the json output for a transaction adding Optimism-related info to the output.
    +
    +    ## Parameters
    +    - `out_json`: A map defining output json which will be extended.
    +    - `transaction`: transaction structure containing extra Optimism-related info.
    +
    +    ## Returns
    +    An extended map containing `l1_*` and `op_withdrawals` items related to Optimism.
    +  """
    +  @spec extend_transaction_json_response(map(), %{
    +          :__struct__ => Explorer.Chain.Transaction,
    +          optional(any()) => any()
    +        }) :: map()
    +  def extend_transaction_json_response(out_json, %Transaction{} = transaction) do
    +    out_json
    +    |> add_optional_transaction_field(transaction, :l1_fee)
    +    |> add_optional_transaction_field(transaction, :l1_fee_scalar)
    +    |> add_optional_transaction_field(transaction, :l1_gas_price)
    +    |> add_optional_transaction_field(transaction, :l1_gas_used)
    +    |> add_optimism_fields(transaction.hash)
    +  end
    +
    +  defp add_optional_transaction_field(out_json, transaction, field) do
    +    case Map.get(transaction, field) do
    +      nil -> out_json
    +      value -> Map.put(out_json, Atom.to_string(field), value)
    +    end
    +  end
    +
    +  defp add_optimism_fields(out_json, transaction_hash) do
    +    withdrawals =
    +      transaction_hash
    +      |> Withdrawal.transaction_statuses()
    +      |> Enum.map(fn {nonce, status, l1_transaction_hash} ->
    +        %{
    +          "nonce" => nonce,
    +          "status" => status,
    +          "l1_transaction_hash" => l1_transaction_hash
    +        }
    +      end)
    +
    +    interop_message =
    +      transaction_hash
    +      |> InteropMessage.message_by_transaction()
    +
    +    out_json = Map.put(out_json, "op_withdrawals", withdrawals)
    +
    +    if is_nil(interop_message) do
    +      out_json
    +    else
    +      Map.put(out_json, "op_interop", interop_message)
    +    end
    +  end
    +
    +  defp maybe_add_chain(msg, chain_key, message) do
    +    case Map.fetch(message, chain_key) do
    +      {:ok, chain} -> Map.put(msg, Atom.to_string(chain_key), chain)
    +      _ -> msg
    +    end
    +  end
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v2/polygon_edge_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v2/polygon_edge_view.ex
    new file mode 100644
    index 0000000..7bdef24
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v2/polygon_edge_view.ex
    @@ -0,0 +1,101 @@
    +defmodule BlockScoutWeb.API.V2.PolygonEdgeView do
    +  use BlockScoutWeb, :view
    +
    +  alias BlockScoutWeb.API.V2.Helper
    +  alias Explorer.Chain
    +  alias Explorer.Chain.PolygonEdge.Reader
    +
    +  @spec render(String.t(), map()) :: map()
    +  def render("polygon_edge_deposits.json", %{
    +        deposits: deposits,
    +        next_page_params: next_page_params
    +      }) do
    +    %{
    +      items:
    +        Enum.map(deposits, fn deposit ->
    +          %{
    +            "msg_id" => deposit.msg_id,
    +            "from" => deposit.from,
    +            "to" => deposit.to,
    +            "l1_transaction_hash" => deposit.l1_transaction_hash,
    +            "l1_timestamp" => deposit.l1_timestamp,
    +            "success" => deposit.success,
    +            "l2_transaction_hash" => deposit.l2_transaction_hash
    +          }
    +        end),
    +      next_page_params: next_page_params
    +    }
    +  end
    +
    +  def render("polygon_edge_withdrawals.json", %{
    +        withdrawals: withdrawals,
    +        next_page_params: next_page_params
    +      }) do
    +    %{
    +      items:
    +        Enum.map(withdrawals, fn withdrawal ->
    +          %{
    +            "msg_id" => withdrawal.msg_id,
    +            "from" => withdrawal.from,
    +            "to" => withdrawal.to,
    +            "l2_transaction_hash" => withdrawal.l2_transaction_hash,
    +            "l2_timestamp" => withdrawal.l2_timestamp,
    +            "success" => withdrawal.success,
    +            "l1_transaction_hash" => withdrawal.l1_transaction_hash
    +          }
    +        end),
    +      next_page_params: next_page_params
    +    }
    +  end
    +
    +  def render("polygon_edge_items_count.json", %{count: count}) do
    +    count
    +  end
    +
    +  def extend_transaction_json_response(out_json, transaction_hash, connection) do
    +    out_json
    +    |> Map.put("polygon_edge_deposit", polygon_edge_deposit(transaction_hash, connection))
    +    |> Map.put("polygon_edge_withdrawal", polygon_edge_withdrawal(transaction_hash, connection))
    +  end
    +
    +  defp polygon_edge_deposit(transaction_hash, conn) do
    +    transaction_hash
    +    |> Reader.deposit_by_transaction_hash()
    +    |> polygon_edge_deposit_or_withdrawal(conn)
    +  end
    +
    +  defp polygon_edge_withdrawal(transaction_hash, conn) do
    +    transaction_hash
    +    |> Reader.withdrawal_by_transaction_hash()
    +    |> polygon_edge_deposit_or_withdrawal(conn)
    +  end
    +
    +  defp polygon_edge_deposit_or_withdrawal(item, conn) do
    +    if not is_nil(item) do
    +      {from_address, from_address_hash} = hash_to_address_and_hash(item.from)
    +      {to_address, to_address_hash} = hash_to_address_and_hash(item.to)
    +
    +      item
    +      |> Map.put(:from, Helper.address_with_info(conn, from_address, from_address_hash, item.from))
    +      |> Map.put(:to, Helper.address_with_info(conn, to_address, to_address_hash, item.to))
    +    end
    +  end
    +
    +  defp hash_to_address_and_hash(hash) do
    +    with false <- is_nil(hash),
    +         {:ok, address} <-
    +           Chain.hash_to_address(
    +             hash,
    +             necessity_by_association: %{
    +               :names => :optional,
    +               :smart_contract => :optional,
    +               proxy_implementations_association() => :optional
    +             },
    +             api?: true
    +           ) do
    +      {address, address.hash}
    +    else
    +      _ -> {nil, nil}
    +    end
    +  end
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v2/polygon_zkevm_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v2/polygon_zkevm_view.ex
    new file mode 100644
    index 0000000..112c36c
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v2/polygon_zkevm_view.ex
    @@ -0,0 +1,192 @@
    +defmodule BlockScoutWeb.API.V2.PolygonZkevmView do
    +  use BlockScoutWeb, :view
    +
    +  alias Explorer.Chain.PolygonZkevm.Reader
    +  alias Explorer.Chain.Transaction
    +
    +  @doc """
    +    Function to render GET requests to `/api/v2/zkevm/batches/:batch_number` endpoint.
    +  """
    +  @spec render(binary(), map()) :: map() | non_neg_integer()
    +  def render("zkevm_batch.json", %{batch: batch}) do
    +    sequence_transaction_hash =
    +      if Map.has_key?(batch, :sequence_transaction) and not is_nil(batch.sequence_transaction) do
    +        batch.sequence_transaction.hash
    +      end
    +
    +    verify_transaction_hash =
    +      if Map.has_key?(batch, :verify_transaction) and not is_nil(batch.verify_transaction) do
    +        batch.verify_transaction.hash
    +      end
    +
    +    l2_transactions =
    +      if Map.has_key?(batch, :l2_transactions) do
    +        Enum.map(batch.l2_transactions, fn transaction -> transaction.hash end)
    +      end
    +
    +    %{
    +      "number" => batch.number,
    +      "status" => batch_status(batch),
    +      "timestamp" => batch.timestamp,
    +      "transactions" => l2_transactions,
    +      "global_exit_root" => batch.global_exit_root,
    +      "acc_input_hash" => batch.acc_input_hash,
    +      "sequence_transaction_hash" => sequence_transaction_hash,
    +      "verify_transaction_hash" => verify_transaction_hash,
    +      "state_root" => batch.state_root
    +    }
    +  end
    +
    +  @doc """
    +    Function to render GET requests to `/api/v2/zkevm/batches` endpoint.
    +  """
    +  def render("zkevm_batches.json", %{
    +        batches: batches,
    +        next_page_params: next_page_params
    +      }) do
    +    %{
    +      items: render_zkevm_batches(batches),
    +      next_page_params: next_page_params
    +    }
    +  end
    +
    +  @doc """
    +    Function to render GET requests to `/api/v2/main-page/zkevm/batches/confirmed` endpoint.
    +  """
    +  def render("zkevm_batches.json", %{batches: batches}) do
    +    %{items: render_zkevm_batches(batches)}
    +  end
    +
    +  @doc """
    +    Function to render GET requests to `/api/v2/zkevm/batches/count` endpoint.
    +  """
    +  def render("zkevm_batches_count.json", %{count: count}) do
    +    count
    +  end
    +
    +  @doc """
    +    Function to render GET requests to `/api/v2/main-page/zkevm/batches/latest-number` endpoint.
    +  """
    +  def render("zkevm_batch_latest_number.json", %{number: number}) do
    +    number
    +  end
    +
    +  @doc """
    +    Function to render GET requests to `/api/v2/zkevm/deposits` and `/api/v2/zkevm/withdrawals` endpoints.
    +  """
    +  def render("polygon_zkevm_bridge_items.json", %{
    +        items: items,
    +        next_page_params: next_page_params
    +      }) do
    +    env = Application.get_all_env(:indexer)[Indexer.Fetcher.PolygonZkevm.BridgeL1]
    +
    +    %{
    +      items:
    +        Enum.map(items, fn item ->
    +          l1_token = if is_nil(Map.get(item, :l1_token)), do: %{}, else: Map.get(item, :l1_token)
    +          l2_token = if is_nil(Map.get(item, :l2_token)), do: %{}, else: Map.get(item, :l2_token)
    +
    +          decimals =
    +            cond do
    +              not is_nil(Map.get(l1_token, :decimals)) -> Reader.sanitize_decimals(Map.get(l1_token, :decimals))
    +              not is_nil(Map.get(l2_token, :decimals)) -> Reader.sanitize_decimals(Map.get(l2_token, :decimals))
    +              true -> env[:native_decimals]
    +            end
    +
    +          symbol =
    +            cond do
    +              not is_nil(Map.get(l1_token, :symbol)) -> Map.get(l1_token, :symbol)
    +              not is_nil(Map.get(l2_token, :symbol)) -> Map.get(l2_token, :symbol)
    +              true -> env[:native_symbol]
    +            end
    +
    +          %{
    +            "block_number" => item.block_number,
    +            "index" => item.index,
    +            "l1_transaction_hash" => item.l1_transaction_hash,
    +            "timestamp" => item.block_timestamp,
    +            "l2_transaction_hash" => item.l2_transaction_hash,
    +            "value" => fractional(Decimal.new(item.amount), Decimal.new(decimals)),
    +            "symbol" => symbol
    +          }
    +        end),
    +      next_page_params: next_page_params
    +    }
    +  end
    +
    +  @doc """
    +    Function to render GET requests to `/api/v2/zkevm/deposits/count` and `/api/v2/zkevm/withdrawals/count` endpoints.
    +  """
    +  def render("polygon_zkevm_bridge_items_count.json", %{count: count}) do
    +    count
    +  end
    +
    +  defp batch_status(batch) do
    +    sequence_id = Map.get(batch, :sequence_id)
    +    verify_id = Map.get(batch, :verify_id)
    +
    +    cond do
    +      is_nil(sequence_id) && is_nil(verify_id) -> "Unfinalized"
    +      !is_nil(sequence_id) && is_nil(verify_id) -> "L1 Sequence Confirmed"
    +      !is_nil(verify_id) -> "Finalized"
    +    end
    +  end
    +
    +  defp fractional(%Decimal{} = amount, %Decimal{} = decimals) do
    +    amount.sign
    +    |> Decimal.new(amount.coef, amount.exp - Decimal.to_integer(decimals))
    +    |> Decimal.normalize()
    +    |> Decimal.to_string(:normal)
    +  end
    +
    +  defp render_zkevm_batches(batches) do
    +    Enum.map(batches, fn batch ->
    +      sequence_transaction_hash =
    +        if not is_nil(batch.sequence_transaction) do
    +          batch.sequence_transaction.hash
    +        end
    +
    +      verify_transaction_hash =
    +        if not is_nil(batch.verify_transaction) do
    +          batch.verify_transaction.hash
    +        end
    +
    +      %{
    +        "number" => batch.number,
    +        "status" => batch_status(batch),
    +        "timestamp" => batch.timestamp,
    +        "transactions_count" => batch.l2_transactions_count,
    +        # todo: It should be removed in favour `transactions_count` property with the next release after 8.0.0
    +        "transaction_count" => batch.l2_transactions_count,
    +        "sequence_transaction_hash" => sequence_transaction_hash,
    +        "verify_transaction_hash" => verify_transaction_hash
    +      }
    +    end)
    +  end
    +
    +  def extend_transaction_json_response(out_json, %Transaction{} = transaction) do
    +    extended_result =
    +      out_json
    +      |> add_optional_transaction_field(transaction, "zkevm_batch_number", :zkevm_batch, :number)
    +      |> add_optional_transaction_field(transaction, "zkevm_sequence_hash", :zkevm_sequence_transaction, :hash)
    +      |> add_optional_transaction_field(transaction, "zkevm_verify_hash", :zkevm_verify_transaction, :hash)
    +
    +    Map.put(extended_result, "zkevm_status", zkevm_status(extended_result))
    +  end
    +
    +  defp zkevm_status(result_map) do
    +    if is_nil(Map.get(result_map, "zkevm_sequence_hash")) do
    +      "Confirmed by Sequencer"
    +    else
    +      "L1 Confirmed"
    +    end
    +  end
    +
    +  defp add_optional_transaction_field(out_json, transaction, out_field, association, association_field) do
    +    case Map.get(transaction, association) do
    +      nil -> out_json
    +      %Ecto.Association.NotLoaded{} -> out_json
    +      item -> Map.put(out_json, out_field, Map.get(item, association_field))
    +    end
    +  end
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v2/proxy/metadata_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v2/proxy/metadata_view.ex
    new file mode 100644
    index 0000000..b8de924
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v2/proxy/metadata_view.ex
    @@ -0,0 +1,13 @@
    +defmodule BlockScoutWeb.API.V2.Proxy.MetadataView do
    +  use BlockScoutWeb, :view
    +
    +  alias BlockScoutWeb.API.V2.AddressView
    +
    +  def render("addresses.json", %{result: {:ok, %{"items" => addresses} = body}}) do
    +    Map.put(body, "items", Enum.map(addresses, &AddressView.prepare_address_for_list/1))
    +  end
    +
    +  def render("addresses.json", %{result: :error}) do
    +    %{error: "Decoding error"}
    +  end
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v2/rootstock_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v2/rootstock_view.ex
    new file mode 100644
    index 0000000..06d4d8e
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v2/rootstock_view.ex
    @@ -0,0 +1,19 @@
    +defmodule BlockScoutWeb.API.V2.RootstockView do
    +  alias Explorer.Chain.Block
    +
    +  def extend_block_json_response(out_json, %Block{} = block) do
    +    out_json
    +    |> add_optional_transaction_field(block, :minimum_gas_price)
    +    |> add_optional_transaction_field(block, :bitcoin_merged_mining_header)
    +    |> add_optional_transaction_field(block, :bitcoin_merged_mining_coinbase_transaction)
    +    |> add_optional_transaction_field(block, :bitcoin_merged_mining_merkle_proof)
    +    |> add_optional_transaction_field(block, :hash_for_merged_mining)
    +  end
    +
    +  defp add_optional_transaction_field(out_json, block, field) do
    +    case Map.get(block, field) do
    +      nil -> out_json
    +      value -> Map.put(out_json, Atom.to_string(field), value)
    +    end
    +  end
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v2/scroll_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v2/scroll_view.ex
    new file mode 100644
    index 0000000..f0614e5
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v2/scroll_view.ex
    @@ -0,0 +1,218 @@
    +defmodule BlockScoutWeb.API.V2.ScrollView do
    +  use BlockScoutWeb, :view
    +
    +  alias BlockScoutWeb.API.V2.TransactionView
    +  alias Explorer.Chain.Scroll.{Batch, L1FeeParam, Reader}
    +  alias Explorer.Chain.{Data, Transaction}
    +
    +  @api_true [api?: true]
    +
    +  @doc """
    +    Function to render GET requests to `/api/v2/scroll/deposits` and `/api/v2/scroll/withdrawals` endpoints.
    +  """
    +  @spec render(binary(), map()) :: map() | non_neg_integer()
    +  def render("scroll_bridge_items.json", %{
    +        items: items,
    +        next_page_params: next_page_params,
    +        type: type
    +      }) do
    +    %{
    +      items:
    +        Enum.map(items, fn item ->
    +          {origination_transaction_hash, completion_transaction_hash} =
    +            if type == :deposits do
    +              {item.l1_transaction_hash, item.l2_transaction_hash}
    +            else
    +              {item.l2_transaction_hash, item.l1_transaction_hash}
    +            end
    +
    +          %{
    +            "id" => item.index,
    +            "origination_transaction_hash" => origination_transaction_hash,
    +            "origination_timestamp" => item.block_timestamp,
    +            "origination_transaction_block_number" => item.block_number,
    +            "completion_transaction_hash" => completion_transaction_hash,
    +            "value" => item.amount
    +          }
    +        end),
    +      next_page_params: next_page_params
    +    }
    +  end
    +
    +  @doc """
    +    Function to render GET requests to `/api/v2/scroll/deposits/count` and `/api/v2/scroll/withdrawals/count` endpoints.
    +  """
    +  def render("scroll_bridge_items_count.json", %{count: count}) do
    +    count
    +  end
    +
    +  @doc """
    +    Function to render GET requests to `/api/v2/scroll/batches/:number` endpoint.
    +  """
    +  def render("scroll_batch.json", %{batch: batch}) do
    +    render_batch(batch)
    +  end
    +
    +  @doc """
    +    Function to render GET requests to `/api/v2/scroll/batches` endpoint.
    +  """
    +  def render("scroll_batches.json", %{
    +        batches: batches,
    +        next_page_params: next_page_params
    +      }) do
    +    items =
    +      batches
    +      |> Enum.map(fn batch ->
    +        Task.async(fn -> render_batch(batch) end)
    +      end)
    +      |> Task.yield_many(:infinity)
    +      |> Enum.map(fn {_task, {:ok, item}} -> item end)
    +
    +    %{
    +      items: items,
    +      next_page_params: next_page_params
    +    }
    +  end
    +
    +  @doc """
    +    Function to render GET requests to `/api/v2/scroll/batches/count` endpoint.
    +  """
    +  def render("scroll_batches_count.json", %{count: count}) do
    +    count
    +  end
    +
    +  # Transforms the batch info into a map format for HTTP response.
    +  #
    +  # ## Parameters
    +  # - `batch`: An instance of `Explorer.Chain.Scroll.Batch` entry.
    +  #
    +  # ## Returns
    +  # - A map with detailed information about the batch formatted for use
    +  #   in JSON HTTP response.
    +  @spec render_batch(Batch.t()) :: map()
    +  defp render_batch(batch) do
    +    {finalize_block_number, finalize_transaction_hash, finalize_timestamp} =
    +      if is_nil(batch.bundle) do
    +        {nil, nil, nil}
    +      else
    +        {batch.bundle.finalize_block_number, batch.bundle.finalize_transaction_hash, batch.bundle.finalize_timestamp}
    +      end
    +
    +    {start_block_number, end_block_number, transactions_count} =
    +      if is_nil(batch.l2_block_range) do
    +        {nil, nil, nil}
    +      else
    +        {
    +          batch.l2_block_range.from,
    +          batch.l2_block_range.to,
    +          Transaction.transaction_count_for_block_range(batch.l2_block_range.from..batch.l2_block_range.to)
    +        }
    +      end
    +
    +    %{
    +      "number" => batch.number,
    +      "commitment_transaction" => %{
    +        "block_number" => batch.commit_block_number,
    +        "hash" => batch.commit_transaction_hash,
    +        "timestamp" => batch.commit_timestamp
    +      },
    +      "confirmation_transaction" => %{
    +        "block_number" => finalize_block_number,
    +        "hash" => finalize_transaction_hash,
    +        "timestamp" => finalize_timestamp
    +      },
    +      "data_availability" => %{
    +        "batch_data_container" => batch.container
    +      },
    +      "start_block_number" => start_block_number,
    +      "end_block_number" => end_block_number,
    +      # todo: It should be removed in favour `start_block_number` property with the next release after 8.0.0
    +      "start_block" => start_block_number,
    +      # todo: It should be removed in favour `end_block_number` property with the next release after 8.0.0
    +      "end_block" => end_block_number,
    +      "transactions_count" => transactions_count,
    +      # todo: It should be removed in favour `transactions_count` property with the next release after 8.0.0
    +      "transaction_count" => transactions_count
    +    }
    +  end
    +
    +  @doc """
    +    Extends the json output with a sub-map containing information related Scroll.
    +
    +    ## Parameters
    +    - `out_json`: A map defining output json which will be extended.
    +    - `transaction`: Transaction structure containing Scroll related data
    +
    +    ## Returns
    +    - A map extended with the data related to Scroll rollup.
    +  """
    +  @spec extend_transaction_json_response(map(), %{
    +          :__struct__ => Transaction,
    +          :block_number => non_neg_integer(),
    +          :index => non_neg_integer(),
    +          :input => Data.t(),
    +          optional(any()) => any()
    +        }) :: map()
    +  def extend_transaction_json_response(out_json, %Transaction{} = transaction) do
    +    config = Application.get_all_env(:explorer)[L1FeeParam]
    +
    +    l1_fee_scalar = get_param(:scalar, transaction, config)
    +    l1_fee_commit_scalar = get_param(:commit_scalar, transaction, config)
    +    l1_fee_blob_scalar = get_param(:blob_scalar, transaction, config)
    +    l1_fee_overhead = get_param(:overhead, transaction, config)
    +    l1_base_fee = get_param(:l1_base_fee, transaction, config)
    +    l1_blob_base_fee = get_param(:l1_blob_base_fee, transaction, config)
    +
    +    l1_gas_used = L1FeeParam.l1_gas_used(transaction, l1_fee_overhead)
    +
    +    l2_fee =
    +      transaction
    +      |> Transaction.l2_fee(:wei)
    +      |> TransactionView.format_fee()
    +
    +    l2_block_status = l2_block_status(transaction.block_number)
    +
    +    params =
    +      %{}
    +      |> add_optional_transaction_field(transaction, :l1_fee)
    +      |> add_optional_transaction_field(transaction, :queue_index)
    +      |> Map.put("l1_fee_scalar", l1_fee_scalar)
    +      |> Map.put("l1_fee_commit_scalar", l1_fee_commit_scalar)
    +      |> Map.put("l1_fee_blob_scalar", l1_fee_blob_scalar)
    +      |> Map.put("l1_fee_overhead", l1_fee_overhead)
    +      |> Map.put("l1_base_fee", l1_base_fee)
    +      |> Map.put("l1_blob_base_fee", l1_blob_base_fee)
    +      |> Map.put("l1_gas_used", l1_gas_used)
    +      |> Map.put("l2_fee", l2_fee)
    +      |> Map.put("l2_block_status", l2_block_status)
    +
    +    Map.put(out_json, "scroll", params)
    +  end
    +
    +  defp add_optional_transaction_field(out_json, transaction, field) do
    +    case Map.get(transaction, field) do
    +      nil -> out_json
    +      value -> Map.put(out_json, Atom.to_string(field), value)
    +    end
    +  end
    +
    +  # sobelow_skip ["DOS.BinToAtom"]
    +  defp get_param(name, transaction, config)
    +       when name in [:scalar, :commit_scalar, :blob_scalar, :overhead, :l1_base_fee, :l1_blob_base_fee] do
    +    name_init = :"#{name}#{:_init}"
    +
    +    case Reader.get_l1_fee_param_for_transaction(name, transaction, @api_true) do
    +      nil -> config[name_init]
    +      value -> value
    +    end
    +  end
    +
    +  @spec l2_block_status(non_neg_integer()) :: binary()
    +  defp l2_block_status(block_number) do
    +    case Reader.batch_by_l2_block_number(block_number, @api_true) do
    +      {_batch_number, nil} -> "Committed"
    +      {_batch_number, _bundle_id} -> "Finalized"
    +      nil -> "Confirmed by Sequencer"
    +    end
    +  end
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v2/search_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v2/search_view.ex
    new file mode 100644
    index 0000000..65a6fc7
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v2/search_view.ex
    @@ -0,0 +1,217 @@
    +defmodule BlockScoutWeb.API.V2.SearchView do
    +  use BlockScoutWeb, :view
    +  use Utils.CompileTimeEnvHelper, chain_type: [:explorer, :chain_type]
    +
    +  alias BlockScoutWeb.{BlockView, Endpoint}
    +  alias Explorer.Chain
    +  alias Explorer.Chain.{Address, Beacon.Blob, Block, Hash, Transaction, UserOperation}
    +  alias Explorer.Helper, as: ExplorerHelper
    +  alias Plug.Conn.Query
    +
    +  def render("search_results.json", %{search_results: search_results, next_page_params: next_page_params}) do
    +    %{
    +      "items" => search_results |> Enum.map(&prepare_search_result/1) |> chain_type_fields(),
    +      "next_page_params" => next_page_params |> encode_next_page_params()
    +    }
    +  end
    +
    +  def render("search_results.json", %{search_results: search_results}) do
    +    search_results |> Enum.map(&prepare_search_result/1) |> chain_type_fields()
    +  end
    +
    +  def render("search_results.json", %{result: {:ok, result}}) do
    +    Map.merge(%{"redirect" => true}, redirect_search_results(result))
    +  end
    +
    +  def render("search_results.json", %{result: {:error, :not_found}}) do
    +    %{"redirect" => false, "type" => nil, "parameter" => nil}
    +  end
    +
    +  def prepare_search_result(%{type: "token"} = search_result) do
    +    %{
    +      "type" => search_result.type,
    +      "name" => search_result.name,
    +      "symbol" => search_result.symbol,
    +      "address_hash" => search_result.address_hash,
    +      # todo: It should be removed in favour `address_hash` property with the next release after 8.0.0
    +      "address" => search_result.address_hash,
    +      "token_url" => token_path(Endpoint, :show, search_result.address_hash),
    +      "address_url" => address_path(Endpoint, :show, search_result.address_hash),
    +      "icon_url" => search_result.icon_url,
    +      "token_type" => search_result.token_type,
    +      "is_smart_contract_verified" => search_result.verified,
    +      "exchange_rate" => search_result.exchange_rate && to_string(search_result.exchange_rate),
    +      "total_supply" => search_result.total_supply,
    +      "circulating_market_cap" =>
    +        search_result.circulating_market_cap && to_string(search_result.circulating_market_cap),
    +      "is_verified_via_admin_panel" => search_result.is_verified_via_admin_panel,
    +      "certified" => search_result.certified || false,
    +      "priority" => search_result.priority
    +    }
    +  end
    +
    +  def prepare_search_result(%{type: "contract"} = search_result) do
    +    %{
    +      "type" => search_result.type,
    +      "name" => search_result.name,
    +      "address_hash" => search_result.address_hash,
    +      # todo: It should be removed in favour `address_hash` property with the next release after 8.0.0
    +      "address" => search_result.address_hash,
    +      "url" => address_path(Endpoint, :show, search_result.address_hash),
    +      "is_smart_contract_verified" => search_result.verified,
    +      "ens_info" => search_result[:ens_info],
    +      "certified" => if(search_result.certified, do: search_result.certified, else: false),
    +      "priority" => search_result.priority
    +    }
    +  end
    +
    +  def prepare_search_result(%{type: address_or_contract_or_label} = search_result)
    +      when address_or_contract_or_label in ["address", "label", "ens_domain"] do
    +    %{
    +      "type" => search_result.type,
    +      "name" => search_result.name,
    +      "address_hash" => search_result.address_hash,
    +      # todo: It should be removed in favour `address_hash` property with the next release after 8.0.0
    +      "address" => search_result.address_hash,
    +      "url" => address_path(Endpoint, :show, search_result.address_hash),
    +      "is_smart_contract_verified" => search_result.verified,
    +      "ens_info" => search_result[:ens_info],
    +      "certified" => if(search_result.certified, do: search_result.certified, else: false),
    +      "priority" => search_result.priority
    +    }
    +  end
    +
    +  def prepare_search_result(%{type: "metadata_tag"} = search_result) do
    +    %{
    +      "type" => search_result.type,
    +      "name" => search_result.name,
    +      "address_hash" => search_result.address_hash,
    +      # todo: It should be removed in favour `address_hash` property with the next release after 8.0.0
    +      "address" => search_result.address_hash,
    +      "url" => address_path(Endpoint, :show, search_result.address_hash),
    +      "is_smart_contract_verified" => search_result.verified,
    +      "ens_info" => search_result[:ens_info],
    +      "certified" => if(search_result.certified, do: search_result.certified, else: false),
    +      "priority" => search_result.priority,
    +      "metadata" => search_result.metadata
    +    }
    +  end
    +
    +  def prepare_search_result(%{type: "block"} = search_result) do
    +    block_hash = ExplorerHelper.add_0x_prefix(search_result.block_hash)
    +
    +    {:ok, block} =
    +      Chain.hash_to_block(hash(search_result.block_hash),
    +        necessity_by_association: %{
    +          :nephews => :optional
    +        },
    +        api?: true
    +      )
    +
    +    %{
    +      "type" => search_result.type,
    +      "block_number" => search_result.block_number,
    +      "block_hash" => block_hash,
    +      "url" => block_path(Endpoint, :show, block_hash),
    +      "timestamp" => search_result.timestamp,
    +      "block_type" => block |> BlockView.block_type() |> String.downcase(),
    +      "priority" => search_result.priority
    +    }
    +  end
    +
    +  def prepare_search_result(%{type: "transaction"} = search_result) do
    +    transaction_hash = ExplorerHelper.add_0x_prefix(search_result.transaction_hash)
    +
    +    %{
    +      "type" => search_result.type,
    +      "transaction_hash" => transaction_hash,
    +      "url" => transaction_path(Endpoint, :show, transaction_hash),
    +      "timestamp" => search_result.timestamp,
    +      "priority" => search_result.priority
    +    }
    +  end
    +
    +  def prepare_search_result(%{type: "user_operation"} = search_result) do
    +    user_operation_hash = ExplorerHelper.add_0x_prefix(search_result.user_operation_hash)
    +
    +    %{
    +      "type" => search_result.type,
    +      "user_operation_hash" => user_operation_hash,
    +      "timestamp" => search_result.timestamp,
    +      "priority" => search_result.priority
    +    }
    +  end
    +
    +  def prepare_search_result(%{type: "blob"} = search_result) do
    +    blob_hash = ExplorerHelper.add_0x_prefix(search_result.blob_hash)
    +
    +    %{
    +      "type" => search_result.type,
    +      "blob_hash" => blob_hash,
    +      "timestamp" => search_result.timestamp,
    +      "priority" => search_result.priority
    +    }
    +  end
    +
    +  defp hash(%Hash{} = hash), do: hash
    +
    +  defp hash(bytes),
    +    do: %Hash{
    +      byte_count: 32,
    +      bytes: bytes
    +    }
    +
    +  defp redirect_search_results(%Address{} = item) do
    +    %{"type" => "address", "parameter" => Address.checksum(item.hash)}
    +  end
    +
    +  defp redirect_search_results(%{address_hash: address_hash}) do
    +    %{"type" => "address", "parameter" => address_hash}
    +  end
    +
    +  defp redirect_search_results(%Block{} = item) do
    +    %{"type" => "block", "parameter" => to_string(item.hash)}
    +  end
    +
    +  defp redirect_search_results(%Transaction{} = item) do
    +    %{"type" => "transaction", "parameter" => to_string(item.hash)}
    +  end
    +
    +  defp redirect_search_results(%UserOperation{} = item) do
    +    %{"type" => "user_operation", "parameter" => to_string(item.hash)}
    +  end
    +
    +  defp redirect_search_results(%Blob{} = item) do
    +    %{"type" => "blob", "parameter" => to_string(item.hash)}
    +  end
    +
    +  case @chain_type do
    +    :filecoin ->
    +      defp chain_type_fields(result) do
    +        # credo:disable-for-next-line Credo.Check.Design.AliasUsage
    +        BlockScoutWeb.API.V2.FilecoinView.preload_and_put_filecoin_robust_address_to_search_results(result)
    +      end
    +
    +    _ ->
    +      defp chain_type_fields(result) do
    +        result
    +      end
    +  end
    +
    +  defp encode_next_page_params(next_page_params) when is_map(next_page_params) do
    +    result =
    +      next_page_params
    +      |> Query.encode()
    +      |> URI.decode_query()
    +      |> Enum.map(fn {k, v} ->
    +        {k, unless(v == "", do: v)}
    +      end)
    +      |> Enum.into(%{})
    +
    +    unless result == %{} do
    +      result
    +    end
    +  end
    +
    +  defp encode_next_page_params(next_page_params), do: next_page_params
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v2/shibarium_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v2/shibarium_view.ex
    new file mode 100644
    index 0000000..de91019
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v2/shibarium_view.ex
    @@ -0,0 +1,71 @@
    +defmodule BlockScoutWeb.API.V2.ShibariumView do
    +  use BlockScoutWeb, :view
    +
    +  alias BlockScoutWeb.API.V2.Helper
    +  alias Explorer.Chain
    +
    +  @spec render(String.t(), map()) :: map()
    +  def render("shibarium_deposits.json", %{
    +        deposits: deposits,
    +        next_page_params: next_page_params,
    +        conn: conn
    +      }) do
    +    user_addresses = get_user_addresses(deposits, conn)
    +
    +    %{
    +      items:
    +        Enum.map(deposits, fn deposit ->
    +          %{
    +            "l1_block_number" => deposit.l1_block_number,
    +            "l1_transaction_hash" => deposit.l1_transaction_hash,
    +            "l2_transaction_hash" => deposit.l2_transaction_hash,
    +            "user" => Map.get(user_addresses, deposit.user, deposit.user),
    +            "timestamp" => deposit.timestamp
    +          }
    +        end),
    +      next_page_params: next_page_params
    +    }
    +  end
    +
    +  def render("shibarium_withdrawals.json", %{
    +        withdrawals: withdrawals,
    +        next_page_params: next_page_params,
    +        conn: conn
    +      }) do
    +    user_addresses = get_user_addresses(withdrawals, conn)
    +
    +    %{
    +      items:
    +        Enum.map(withdrawals, fn withdrawal ->
    +          %{
    +            "l2_block_number" => withdrawal.l2_block_number,
    +            "l2_transaction_hash" => withdrawal.l2_transaction_hash,
    +            "l1_transaction_hash" => withdrawal.l1_transaction_hash,
    +            "user" => Map.get(user_addresses, withdrawal.user, withdrawal.user),
    +            "timestamp" => withdrawal.timestamp
    +          }
    +        end),
    +      next_page_params: next_page_params
    +    }
    +  end
    +
    +  def render("shibarium_items_count.json", %{count: count}) do
    +    count
    +  end
    +
    +  defp get_user_addresses(items, conn) do
    +    items
    +    |> Enum.map(& &1.user)
    +    |> Enum.reject(&is_nil(&1))
    +    |> Enum.uniq()
    +    |> Chain.hashes_to_addresses(
    +      necessity_by_association: %{
    +        :names => :optional,
    +        :smart_contract => :optional,
    +        proxy_implementations_association() => :optional
    +      },
    +      api?: true
    +    )
    +    |> Enum.into(%{}, &{&1.hash, Helper.address_with_info(conn, &1, &1.hash, true)})
    +  end
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v2/smart_contract_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v2/smart_contract_view.ex
    new file mode 100644
    index 0000000..4a12957
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v2/smart_contract_view.ex
    @@ -0,0 +1,428 @@
    +defmodule BlockScoutWeb.API.V2.SmartContractView do
    +  use BlockScoutWeb, :view
    +  use Utils.CompileTimeEnvHelper, chain_type: [:explorer, :chain_type]
    +
    +  import Explorer.SmartContract.Reader, only: [zip_tuple_values_with_types: 2]
    +
    +  alias ABI.FunctionSelector
    +  alias BlockScoutWeb.API.V2.Helper, as: APIV2Helper
    +  alias BlockScoutWeb.API.V2.TransactionView
    +  alias BlockScoutWeb.{AddressContractView, SmartContractView}
    +  alias Ecto.Changeset
    +  alias Explorer.Chain
    +  alias Explorer.Chain.{Address, SmartContract, SmartContractAdditionalSource}
    +  alias Explorer.Chain.SmartContract.Proxy
    +  alias Explorer.Visualize.Sol2uml
    +
    +  require Logger
    +
    +  @api_true [api?: true]
    +
    +  def render("smart_contracts.json", %{addresses: addresses, next_page_params: next_page_params}) do
    +    %{
    +      "items" => Enum.map(addresses, &prepare_smart_contract_address_for_list/1),
    +      "next_page_params" => next_page_params
    +    }
    +  end
    +
    +  def render("smart_contract.json", %{address: address, conn: conn}) do
    +    prepare_smart_contract(address, conn)
    +  end
    +
    +  def render("read_functions.json", %{functions: functions}) do
    +    Enum.map(functions, &prepare_read_function/1)
    +  end
    +
    +  def render("function_response.json", %{output: output, names: names, contract_address_hash: contract_address_hash}) do
    +    prepare_function_response(output, names, contract_address_hash)
    +  end
    +
    +  def render("changeset_errors.json", %{changeset: changeset}) do
    +    Changeset.traverse_errors(changeset, fn {msg, opts} ->
    +      Regex.replace(~r"%{(\w+)}", msg, fn _, key ->
    +        opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string()
    +      end)
    +    end)
    +  end
    +
    +  def render("audit_reports.json", %{reports: reports}) do
    +    %{"items" => Enum.map(reports, &prepare_audit_report/1), "next_page_params" => nil}
    +  end
    +
    +  defp prepare_audit_report(report) do
    +    %{
    +      "audit_company_name" => report.audit_company_name,
    +      "audit_report_url" => report.audit_report_url,
    +      "audit_publish_date" => report.audit_publish_date
    +    }
    +  end
    +
    +  def prepare_function_response(outputs, names, contract_address_hash) do
    +    case outputs do
    +      {:error, %{code: code, message: message, data: _data} = error} ->
    +        revert_reason = Chain.parse_revert_reason_from_error(error)
    +
    +        case SmartContractView.decode_revert_reason(contract_address_hash, revert_reason, @api_true) do
    +          {:ok, method_id, text, mapping} ->
    +            %{
    +              result:
    +                render(TransactionView, "decoded_input.json",
    +                  method_id: method_id,
    +                  text: text,
    +                  mapping: mapping,
    +                  error?: true
    +                ),
    +              is_error: true
    +            }
    +
    +          {:error, _contract_verified, []} ->
    +            %{
    +              result:
    +                Map.merge(render(TransactionView, "revert_reason.json", raw: revert_reason), %{
    +                  code: code,
    +                  message: message
    +                }),
    +              is_error: true
    +            }
    +
    +          {:error, _contract_verified, candidates} ->
    +            {:ok, method_id, text, mapping} = Enum.at(candidates, 0)
    +
    +            %{
    +              result:
    +                render(TransactionView, "decoded_input.json",
    +                  method_id: method_id,
    +                  text: text,
    +                  mapping: mapping,
    +                  error?: true
    +                ),
    +              is_error: true
    +            }
    +
    +          _ ->
    +            %{
    +              result:
    +                Map.merge(render(TransactionView, "revert_reason.json", raw: revert_reason), %{
    +                  code: code,
    +                  message: message
    +                }),
    +              is_error: true
    +            }
    +        end
    +
    +      {:error, %{code: code, message: message}} ->
    +        %{result: %{code: code, message: message}, is_error: true}
    +
    +      {:error, error} ->
    +        %{result: %{error: error}, is_error: true}
    +
    +      _ ->
    +        %{result: %{output: Enum.map(outputs, &render_json/1), names: names}, is_error: false}
    +    end
    +  end
    +
    +  def prepare_read_function(function) do
    +    case function["outputs"] do
    +      {:error, text_error} ->
    +        function
    +        |> Map.put("error", text_error)
    +        |> Map.replace("outputs", function["abi_outputs"])
    +        |> Map.drop(["abi_outputs"])
    +
    +      nil ->
    +        function
    +
    +      _ ->
    +        result =
    +          function
    +          |> Map.drop(["abi_outputs"])
    +
    +        outputs = result["outputs"] |> Enum.map(&prepare_output/1)
    +        Map.replace(result, "outputs", outputs)
    +    end
    +  end
    +
    +  defp prepare_output(%{"type" => type, "value" => value} = output) do
    +    Map.replace(output, "value", render_json(value, type))
    +  end
    +
    +  defp prepare_output(output), do: output
    +
    +  # credo:disable-for-next-line
    +  defp prepare_smart_contract(
    +         %Address{smart_contract: %SmartContract{} = smart_contract, proxy_implementations: implementations} = address,
    +         _conn
    +       ) do
    +    smart_contract_verified = APIV2Helper.smart_contract_verified?(address)
    +
    +    bytecode_twin_contract =
    +      if smart_contract_verified,
    +        do: nil,
    +        else: address.smart_contract
    +
    +    additional_sources =
    +      get_additional_sources(
    +        smart_contract,
    +        smart_contract_verified,
    +        bytecode_twin_contract
    +      )
    +
    +    fully_verified = SmartContract.verified_with_full_match?(address.hash, @api_true)
    +    visualize_sol2uml_enabled = Sol2uml.enabled?()
    +
    +    proxy_type = implementations && implementations.proxy_type
    +
    +    minimal_proxy? = proxy_type in ["eip1167", "clone_with_immutable_arguments", "erc7760"]
    +
    +    target_contract =
    +      if smart_contract_verified, do: smart_contract, else: bytecode_twin_contract
    +
    +    # don't return verified_bytecode_twin_address_hash if smart contract is verified or minimal proxy
    +    verified_bytecode_twin_address_hash =
    +      (!smart_contract_verified && !minimal_proxy? &&
    +         bytecode_twin_contract && Address.checksum(bytecode_twin_contract.verified_bytecode_twin_address_hash)) || nil
    +
    +    smart_contract_verified_via_sourcify = smart_contract_verified && smart_contract.verified_via_sourcify
    +
    +    %{
    +      "verified_twin_address_hash" => verified_bytecode_twin_address_hash,
    +      "is_verified" => smart_contract_verified,
    +      "is_changed_bytecode" => smart_contract_verified && smart_contract.is_changed_bytecode,
    +      "is_partially_verified" => smart_contract_verified && smart_contract.partially_verified,
    +      "is_fully_verified" => fully_verified,
    +      "is_verified_via_sourcify" => smart_contract_verified_via_sourcify,
    +      "is_verified_via_eth_bytecode_db" => smart_contract.verified_via_eth_bytecode_db,
    +      "is_verified_via_verifier_alliance" => smart_contract.verified_via_verifier_alliance,
    +      "proxy_type" => proxy_type,
    +      "implementations" => Proxy.proxy_object_info(implementations),
    +      "sourcify_repo_url" =>
    +        if(smart_contract_verified_via_sourcify,
    +          do: AddressContractView.sourcify_repo_url(address.hash, smart_contract.partially_verified)
    +        ),
    +      "can_be_visualized_via_sol2uml" =>
    +        visualize_sol2uml_enabled && target_contract && SmartContract.language(target_contract) == :solidity,
    +      "name" => target_contract && target_contract.name,
    +      "compiler_version" => target_contract && target_contract.compiler_version,
    +      "optimization_enabled" => target_contract && target_contract.optimization,
    +      "optimization_runs" => target_contract && target_contract.optimization_runs,
    +      "evm_version" => target_contract && target_contract.evm_version,
    +      "verified_at" => target_contract && target_contract.inserted_at,
    +      "abi" => target_contract && target_contract.abi,
    +      "source_code" => target_contract && target_contract.contract_source_code,
    +      "file_path" => target_contract && target_contract.file_path,
    +      "additional_sources" => Enum.map(additional_sources, &prepare_additional_source/1),
    +      "compiler_settings" => target_contract && target_contract.compiler_settings,
    +      "external_libraries" => (target_contract && prepare_external_libraries(target_contract.external_libraries)) || [],
    +      "constructor_args" => if(smart_contract_verified, do: smart_contract.constructor_arguments),
    +      "decoded_constructor_args" =>
    +        if(smart_contract_verified,
    +          do: SmartContract.format_constructor_arguments(smart_contract.abi, smart_contract.constructor_arguments)
    +        ),
    +      "language" => SmartContract.language(smart_contract),
    +      "license_type" => smart_contract.license_type,
    +      "certified" => if(smart_contract.certified, do: smart_contract.certified, else: false),
    +      "is_blueprint" => if(smart_contract.is_blueprint, do: smart_contract.is_blueprint, else: false)
    +    }
    +    |> Map.merge(bytecode_info(address))
    +    |> chain_type_fields(
    +      %{
    +        address_hash: verified_bytecode_twin_address_hash,
    +        field_prefix: "verified_twin",
    +        target_contract: target_contract
    +      },
    +      true
    +    )
    +  end
    +
    +  defp prepare_smart_contract(%Address{proxy_implementations: implementations} = address, _conn) do
    +    %{
    +      "proxy_type" => implementations && implementations.proxy_type,
    +      "implementations" => Proxy.proxy_object_info(implementations)
    +    }
    +    |> Map.merge(bytecode_info(address))
    +  end
    +
    +  @doc """
    +  Returns additional sources of the smart-contract or from its bytecode twin
    +  """
    +  @spec get_additional_sources(SmartContract.t(), boolean, SmartContract.t() | nil) ::
    +          [SmartContractAdditionalSource.t()]
    +  def get_additional_sources(%{smart_contract_additional_sources: original_smart_contract_additional_sources}, true, _)
    +      when is_list(original_smart_contract_additional_sources) do
    +    original_smart_contract_additional_sources
    +  end
    +
    +  def get_additional_sources(_, false, %{
    +        smart_contract_additional_sources: bytecode_twin_smart_contract_additional_sources
    +      })
    +      when is_list(bytecode_twin_smart_contract_additional_sources) do
    +    bytecode_twin_smart_contract_additional_sources
    +  end
    +
    +  def get_additional_sources(_smart_contract, _smart_contract_verified, _bytecode_twin_contract), do: []
    +
    +  defp bytecode_info(address) do
    +    case AddressContractView.contract_creation_code(address) do
    +      {:selfdestructed, init} ->
    +        %{
    +          "is_self_destructed" => true,
    +          "deployed_bytecode" => nil,
    +          "creation_bytecode" => init,
    +          "status" => "selfdestructed"
    +        }
    +
    +      {:failed, creation_code} ->
    +        %{
    +          "is_self_destructed" => false,
    +          "deployed_bytecode" => "0x",
    +          "creation_bytecode" => creation_code,
    +          "status" => "failed"
    +        }
    +
    +      {:ok, contract_code} ->
    +        %{
    +          "is_self_destructed" => false,
    +          "deployed_bytecode" => contract_code,
    +          "creation_bytecode" => AddressContractView.creation_code(address),
    +          "status" => "success"
    +        }
    +    end
    +  end
    +
    +  defp prepare_external_libraries(libraries) when is_list(libraries) do
    +    Enum.map(libraries, fn %Explorer.Chain.SmartContract.ExternalLibrary{name: name, address_hash: address_hash} ->
    +      {:ok, hash} = Chain.string_to_address_hash(address_hash)
    +
    +      %{name: name, address_hash: Address.checksum(hash)}
    +    end)
    +  end
    +
    +  defp prepare_additional_source(source) do
    +    %{
    +      "source_code" => source.contract_source_code,
    +      "file_path" => source.file_name
    +    }
    +  end
    +
    +  defp prepare_smart_contract_address_for_list(
    +         %Address{
    +           smart_contract: %SmartContract{} = smart_contract,
    +           token: token
    +         } = address
    +       ) do
    +    smart_contract_info =
    +      %{
    +        "address" => APIV2Helper.address_with_info(nil, address, address.hash, false),
    +        "compiler_version" => smart_contract.compiler_version,
    +        "optimization_enabled" => smart_contract.optimization,
    +        "transactions_count" => address.transactions_count,
    +        # todo: It should be removed in favour `transactions_count` property with the next release after 8.0.0
    +        "transaction_count" => address.transactions_count,
    +        "language" => SmartContract.language(smart_contract),
    +        "verified_at" => smart_contract.inserted_at,
    +        "market_cap" => token && token.circulating_market_cap,
    +        "has_constructor_args" => !is_nil(smart_contract.constructor_arguments),
    +        "coin_balance" => if(address.fetched_coin_balance, do: address.fetched_coin_balance.value),
    +        "license_type" => smart_contract.license_type,
    +        "certified" => if(smart_contract.certified, do: smart_contract.certified, else: false)
    +      }
    +
    +    smart_contract_info
    +    |> chain_type_fields(
    +      %{target_contract: smart_contract},
    +      false
    +    )
    +  end
    +
    +  def render_json(%{"type" => type, "value" => value}) do
    +    %{"type" => type, "value" => render_json(value, type)}
    +  end
    +
    +  def render_json(value, type) when is_tuple(value) do
    +    value
    +    |> zip_tuple_values_with_types(type)
    +    |> Enum.map(fn {type, value} ->
    +      render_json(value, type)
    +    end)
    +  end
    +
    +  def render_json(value, type) when is_list(value) and is_tuple(type) do
    +    item_type =
    +      case type do
    +        {:array, item_type, _} -> item_type
    +        {:array, item_type} -> item_type
    +      end
    +
    +    value |> Enum.map(&render_json(&1, item_type))
    +  end
    +
    +  def render_json(value, type) when is_list(value) and not is_tuple(type) do
    +    sanitized_type =
    +      case type do
    +        "tuple[" <> rest ->
    +          # we need to convert tuple[...][] or tuple[...][n] into (...)[] or (...)[n]
    +          # before sending it to the `FunctionSelector.decode_type/1`. See https://github.com/poanetwork/ex_abi/issues/168.
    +          tuple_item_types =
    +            rest
    +            |> String.split("]")
    +            |> Enum.slice(0..-3//1)
    +            |> Enum.join("]")
    +
    +          array_str = "[" <> (rest |> String.split("[") |> List.last())
    +
    +          "(" <> tuple_item_types <> ")" <> array_str
    +
    +        _ ->
    +          type
    +      end
    +
    +    item_type =
    +      case FunctionSelector.decode_type(sanitized_type) do
    +        {:array, item_type, _} -> item_type
    +        {:array, item_type} -> item_type
    +      end
    +
    +    value |> Enum.map(&render_json(&1, item_type))
    +  end
    +
    +  def render_json(value, type) when type in [:address, "address", "address payable"] do
    +    SmartContractView.cast_address(value)
    +  end
    +
    +  def render_json(value, type) when type in [:string, "string"] do
    +    to_string(value)
    +  end
    +
    +  def render_json(value, _type) do
    +    to_string(value)
    +  end
    +
    +  case @chain_type do
    +    :filecoin ->
    +      defp chain_type_fields(result, params, true) do
    +        # credo:disable-for-next-line Credo.Check.Design.AliasUsage
    +        BlockScoutWeb.API.V2.FilecoinView.preload_and_put_filecoin_robust_address(result, params)
    +      end
    +
    +      defp chain_type_fields(result, _params, false),
    +        do: result
    +
    +    :arbitrum ->
    +      defp chain_type_fields(result, %{target_contract: target_contract}, _single?) do
    +        result
    +        |> Map.put("package_name", target_contract.package_name)
    +        |> Map.put("github_repository_metadata", target_contract.github_repository_metadata)
    +      end
    +
    +    :zksync ->
    +      defp chain_type_fields(result, %{target_contract: target_contract}, _single?) do
    +        result
    +        |> Map.put("zk_compiler_version", target_contract.zk_compiler_version)
    +      end
    +
    +    _ ->
    +      defp chain_type_fields(result, _params, _single?) do
    +        result
    +      end
    +  end
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v2/stability_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v2/stability_view.ex
    new file mode 100644
    index 0000000..3bd0c2a
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v2/stability_view.ex
    @@ -0,0 +1,130 @@
    +defmodule BlockScoutWeb.API.V2.StabilityView do
    +  alias BlockScoutWeb.API.V2.{Helper, TokenView}
    +  alias Explorer.Chain.{Log, Token, Transaction}
    +
    +  @api_true [api?: true]
    +  @transaction_fee_event_signature "0x99e7b0ba56da2819c37c047f0511fd2bf6c9b4e27b4a979a19d6da0f74be8155"
    +  @transaction_fee_event_abi [
    +    %{
    +      "anonymous" => false,
    +      "inputs" => [
    +        %{
    +          "indexed" => false,
    +          "internalType" => "address",
    +          "name" => "token",
    +          "type" => "address"
    +        },
    +        %{
    +          "indexed" => false,
    +          "internalType" => "uint256",
    +          "name" => "totalFee",
    +          "type" => "uint256"
    +        },
    +        %{
    +          "indexed" => false,
    +          "internalType" => "address",
    +          "name" => "validator",
    +          "type" => "address"
    +        },
    +        %{
    +          "indexed" => false,
    +          "internalType" => "uint256",
    +          "name" => "validatorFee",
    +          "type" => "uint256"
    +        },
    +        %{
    +          "indexed" => false,
    +          "internalType" => "address",
    +          "name" => "dapp",
    +          "type" => "address"
    +        },
    +        %{
    +          "indexed" => false,
    +          "internalType" => "uint256",
    +          "name" => "dappFee",
    +          "type" => "uint256"
    +        }
    +      ],
    +      "name" => "TransactionFee",
    +      "type" => "event"
    +    }
    +  ]
    +
    +  def extend_transaction_json_response(out_json, %Transaction{} = transaction) do
    +    case transaction.transaction_fee_log do
    +      [
    +        {"token", "address", false, token_address_hash},
    +        {"totalFee", "uint256", false, total_fee},
    +        {"validator", "address", false, validator_address_hash},
    +        {"validatorFee", "uint256", false, validator_fee},
    +        {"dapp", "address", false, dapp_address_hash},
    +        {"dappFee", "uint256", false, dapp_fee}
    +      ] ->
    +        stability_fee = %{
    +          "token" =>
    +            TokenView.render("token.json", %{
    +              token: transaction.transaction_fee_token,
    +              contract_address_hash: Transaction.bytes_to_address_hash(token_address_hash)
    +            }),
    +          "validator_address" =>
    +            Helper.address_with_info(nil, nil, Transaction.bytes_to_address_hash(validator_address_hash), false),
    +          "dapp_address" =>
    +            Helper.address_with_info(nil, nil, Transaction.bytes_to_address_hash(dapp_address_hash), false),
    +          "total_fee" => to_string(total_fee),
    +          "dapp_fee" => to_string(dapp_fee),
    +          "validator_fee" => to_string(validator_fee)
    +        }
    +
    +        out_json
    +        |> Map.put("stability_fee", stability_fee)
    +
    +      _ ->
    +        out_json
    +    end
    +  end
    +
    +  def transform_transactions(transactions) do
    +    do_extend_with_stability_fees_info(transactions)
    +  end
    +
    +  defp do_extend_with_stability_fees_info(transactions) when is_list(transactions) do
    +    {transactions, _tokens_acc} =
    +      Enum.map_reduce(transactions, %{}, fn transaction, tokens_acc ->
    +        case Log.fetch_log_by_transaction_hash_and_first_topic(
    +               transaction.hash,
    +               @transaction_fee_event_signature,
    +               @api_true
    +             ) do
    +          fee_log when not is_nil(fee_log) ->
    +            {:ok, _selector, mapping} = Log.find_and_decode(@transaction_fee_event_abi, fee_log, transaction.hash)
    +
    +            [{"token", "address", false, token_address_hash}, _, _, _, _, _] = mapping
    +
    +            {token, new_tokens_acc} =
    +              check_tokens_acc(Transaction.bytes_to_address_hash(token_address_hash), tokens_acc)
    +
    +            {%Transaction{transaction | transaction_fee_log: mapping, transaction_fee_token: token}, new_tokens_acc}
    +
    +          _ ->
    +            {transaction, tokens_acc}
    +        end
    +      end)
    +
    +    transactions
    +  end
    +
    +  defp do_extend_with_stability_fees_info(transaction) do
    +    [transaction] = do_extend_with_stability_fees_info([transaction])
    +    transaction
    +  end
    +
    +  defp check_tokens_acc(token_address_hash, tokens_acc) do
    +    if Map.has_key?(tokens_acc, token_address_hash) do
    +      {tokens_acc[token_address_hash], tokens_acc}
    +    else
    +      token = Token.get_by_contract_address_hash(token_address_hash, @api_true)
    +
    +      {token, Map.put(tokens_acc, token_address_hash, token)}
    +    end
    +  end
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v2/suave_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v2/suave_view.ex
    new file mode 100644
    index 0000000..a267486
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v2/suave_view.ex
    @@ -0,0 +1,137 @@
    +defmodule BlockScoutWeb.API.V2.SuaveView do
    +  alias BlockScoutWeb.API.V2.Helper, as: APIHelper
    +  alias BlockScoutWeb.API.V2.TransactionView
    +
    +  alias Explorer.Helper, as: ExplorerHelper
    +
    +  alias Ecto.Association.NotLoaded
    +  alias Explorer.Chain.{Hash, Transaction}
    +
    +  @suave_bid_event "0x83481d5b04dea534715acad673a8177a46fc93882760f36bdc16ccac439d504e"
    +
    +  def extend_transaction_json_response(
    +        %Transaction{} = transaction,
    +        out_json,
    +        single_transaction?,
    +        conn,
    +        watchlist_names
    +      ) do
    +    if is_nil(Map.get(transaction, :execution_node_hash)) do
    +      out_json
    +    else
    +      wrapped_to_address = Map.get(transaction, :wrapped_to_address)
    +      wrapped_to_address_hash = Map.get(transaction, :wrapped_to_address_hash)
    +      wrapped_input = Map.get(transaction, :wrapped_input)
    +      wrapped_hash = Map.get(transaction, :wrapped_hash)
    +      execution_node = Map.get(transaction, :execution_node)
    +      execution_node_hash = Map.get(transaction, :execution_node_hash)
    +      wrapped_type = Map.get(transaction, :wrapped_type)
    +      wrapped_nonce = Map.get(transaction, :wrapped_nonce)
    +      wrapped_gas = Map.get(transaction, :wrapped_gas)
    +      wrapped_gas_price = Map.get(transaction, :wrapped_gas_price)
    +      wrapped_max_priority_fee_per_gas = Map.get(transaction, :wrapped_max_priority_fee_per_gas)
    +      wrapped_max_fee_per_gas = Map.get(transaction, :wrapped_max_fee_per_gas)
    +      wrapped_value = Map.get(transaction, :wrapped_value)
    +
    +      [wrapped_decoded_input] =
    +        Transaction.decode_transactions(
    +          [
    +            %Transaction{
    +              to_address: wrapped_to_address,
    +              input: wrapped_input,
    +              hash: wrapped_hash
    +            }
    +          ],
    +          false,
    +          api?: true
    +        )
    +
    +      out_json
    +      |> Map.put("allowed_peekers", suave_parse_allowed_peekers(transaction.logs))
    +      |> Map.put(
    +        "execution_node",
    +        APIHelper.address_with_info(
    +          conn,
    +          execution_node,
    +          execution_node_hash,
    +          single_transaction?,
    +          watchlist_names
    +        )
    +      )
    +      |> Map.put("wrapped", %{
    +        "type" => wrapped_type,
    +        "nonce" => wrapped_nonce,
    +        "to" =>
    +          APIHelper.address_with_info(
    +            conn,
    +            wrapped_to_address,
    +            wrapped_to_address_hash,
    +            single_transaction?,
    +            watchlist_names
    +          ),
    +        "gas_limit" => wrapped_gas,
    +        "gas_price" => wrapped_gas_price,
    +        "fee" =>
    +          TransactionView.format_fee(
    +            Transaction.fee(
    +              %Transaction{gas: wrapped_gas, gas_price: wrapped_gas_price, gas_used: nil},
    +              :wei
    +            )
    +          ),
    +        "max_priority_fee_per_gas" => wrapped_max_priority_fee_per_gas,
    +        "max_fee_per_gas" => wrapped_max_fee_per_gas,
    +        "value" => wrapped_value,
    +        "hash" => wrapped_hash,
    +        "method" =>
    +          Transaction.method_name(
    +            %Transaction{to_address: wrapped_to_address, input: wrapped_input},
    +            wrapped_decoded_input
    +          ),
    +        "decoded_input" => TransactionView.decoded_input(wrapped_decoded_input),
    +        "raw_input" => wrapped_input
    +      })
    +    end
    +  end
    +
    +  # @spec suave_parse_allowed_peekers(Ecto.Schema.has_many(Log.t())) :: [String.t()]
    +  defp suave_parse_allowed_peekers(%NotLoaded{}), do: []
    +
    +  defp suave_parse_allowed_peekers(logs) do
    +    suave_bid_contracts =
    +      Application.get_all_env(:explorer)[Transaction][:suave_bid_contracts]
    +      |> String.split(",")
    +      |> Enum.map(fn sbc -> String.downcase(String.trim(sbc)) end)
    +
    +    bid_event =
    +      Enum.find(logs, fn log ->
    +        sanitize_log_first_topic(log.first_topic) == @suave_bid_event &&
    +          Enum.member?(suave_bid_contracts, String.downcase(Hash.to_string(log.address_hash)))
    +      end)
    +
    +    if is_nil(bid_event) do
    +      []
    +    else
    +      [_bid_id, _decryption_condition, allowed_peekers] =
    +        ExplorerHelper.decode_data(bid_event.data, [{:bytes, 16}, {:uint, 64}, {:array, :address}])
    +
    +      Enum.map(allowed_peekers, fn peeker ->
    +        ExplorerHelper.add_0x_prefix(peeker)
    +      end)
    +    end
    +  end
    +
    +  defp sanitize_log_first_topic(first_topic) do
    +    if is_nil(first_topic) do
    +      ""
    +    else
    +      sanitized =
    +        if is_binary(first_topic) do
    +          first_topic
    +        else
    +          Hash.to_string(first_topic)
    +        end
    +
    +      String.downcase(sanitized)
    +    end
    +  end
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v2/token_transfer_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v2/token_transfer_view.ex
    new file mode 100644
    index 0000000..81c28e3
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v2/token_transfer_view.ex
    @@ -0,0 +1,108 @@
    +defmodule BlockScoutWeb.API.V2.TokenTransferView do
    +  use BlockScoutWeb, :view
    +
    +  alias BlockScoutWeb.API.V2.{Helper, TokenView, TransactionView}
    +  alias BlockScoutWeb.Tokens.Helper, as: TokensHelper
    +  alias Ecto.Association.NotLoaded
    +  alias Explorer.Chain
    +  alias Explorer.Chain.{TokenTransfer, Transaction}
    +
    +  def render("token_transfer.json", %{token_transfer: nil}) do
    +    nil
    +  end
    +
    +  def render("token_transfer.json", %{
    +        token_transfer: token_transfer,
    +        decoded_transaction_input: decoded_transaction_input,
    +        conn: conn
    +      }) do
    +    prepare_token_transfer(token_transfer, conn, decoded_transaction_input)
    +  end
    +
    +  def render("token_transfers.json", %{
    +        token_transfers: token_transfers,
    +        decoded_transactions_map: decoded_transactions_map,
    +        next_page_params: next_page_params,
    +        conn: conn
    +      }) do
    +    %{
    +      "items" =>
    +        Enum.map(
    +          token_transfers,
    +          &render("token_transfer.json", %{
    +            token_transfer: &1,
    +            decoded_transaction_input: &1.transaction && decoded_transactions_map[&1.transaction.hash],
    +            conn: conn
    +          })
    +        ),
    +      "next_page_params" => next_page_params
    +    }
    +  end
    +
    +  @doc """
    +    Prepares token transfer object to be returned in the API v2 endpoints.
    +  """
    +  @spec prepare_token_transfer(TokenTransfer.t(), Plug.Conn.t() | nil, any()) :: map()
    +  def prepare_token_transfer(token_transfer, _conn, decoded_input) do
    +    %{
    +      "transaction_hash" => token_transfer.transaction_hash,
    +      "from" => Helper.address_with_info(nil, token_transfer.from_address, token_transfer.from_address_hash, false),
    +      "to" => Helper.address_with_info(nil, token_transfer.to_address, token_transfer.to_address_hash, false),
    +      "total" => prepare_token_transfer_total(token_transfer),
    +      "token" => TokenView.render("token.json", %{token: token_transfer.token}),
    +      "type" => Chain.get_token_transfer_type(token_transfer),
    +      "timestamp" =>
    +        if(match?(%NotLoaded{}, token_transfer.block),
    +          do: TransactionView.block_timestamp(token_transfer.transaction),
    +          else: TransactionView.block_timestamp(token_transfer.block)
    +        ),
    +      "method" => Transaction.method_name(token_transfer.transaction, decoded_input, true),
    +      "block_hash" => to_string(token_transfer.block_hash),
    +      "block_number" => token_transfer.block_number,
    +      "log_index" => token_transfer.log_index
    +    }
    +  end
    +
    +  @doc """
    +    Prepares token transfer total value/id transferred to be returned in the API v2 endpoints.
    +  """
    +  @spec prepare_token_transfer_total(TokenTransfer.t()) :: map()
    +  # credo:disable-for-next-line /Complexity/
    +  def prepare_token_transfer_total(token_transfer) do
    +    case TokensHelper.token_transfer_amount_for_api(token_transfer) do
    +      {:ok, :erc721_instance} ->
    +        %{
    +          "token_id" => token_transfer.token_ids && List.first(token_transfer.token_ids),
    +          "token_instance" =>
    +            token_transfer.token_instance &&
    +              TokenView.prepare_token_instance(token_transfer.token_instance, token_transfer.token)
    +        }
    +
    +      {:ok, :erc1155_erc404_instance, value, decimals} ->
    +        %{
    +          "token_id" => token_transfer.token_ids && List.first(token_transfer.token_ids),
    +          "value" => value,
    +          "decimals" => decimals,
    +          "token_instance" =>
    +            token_transfer.token_instance &&
    +              TokenView.prepare_token_instance(token_transfer.token_instance, token_transfer.token)
    +        }
    +
    +      {:ok, :erc1155_erc404_instance, values, token_ids, decimals} ->
    +        %{
    +          "token_id" => token_ids && List.first(token_ids),
    +          "value" => values && List.first(values),
    +          "decimals" => decimals,
    +          "token_instance" =>
    +            token_transfer.token_instance &&
    +              TokenView.prepare_token_instance(token_transfer.token_instance, token_transfer.token)
    +        }
    +
    +      {:ok, value, decimals} ->
    +        %{"value" => value, "decimals" => decimals}
    +
    +      _ ->
    +        nil
    +    end
    +  end
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v2/token_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v2/token_view.ex
    new file mode 100644
    index 0000000..b896ea8
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v2/token_view.ex
    @@ -0,0 +1,166 @@
    +defmodule BlockScoutWeb.API.V2.TokenView do
    +  use BlockScoutWeb, :view
    +  use Utils.CompileTimeEnvHelper, chain_type: [:explorer, :chain_type]
    +
    +  alias BlockScoutWeb.API.V2.Helper
    +  alias BlockScoutWeb.NFTHelper
    +  alias Ecto.Association.NotLoaded
    +  alias Explorer.Chain.{Address, BridgedToken}
    +  alias Explorer.Chain.Token.Instance
    +
    +  def render("token.json", %{token: nil = token, contract_address_hash: contract_address_hash}) do
    +    %{
    +      "address_hash" => Address.checksum(contract_address_hash),
    +      # todo: It should be removed in favour `address_hash` property with the next release after 8.0.0
    +      "address" => Address.checksum(contract_address_hash),
    +      "symbol" => nil,
    +      "name" => nil,
    +      "decimals" => nil,
    +      "type" => nil,
    +      "holders_count" => nil,
    +      # todo: It should be removed in favour `holders_count` property with the next release after 8.0.0
    +      "holders" => nil,
    +      "exchange_rate" => nil,
    +      "total_supply" => nil,
    +      "icon_url" => nil,
    +      "circulating_market_cap" => nil
    +    }
    +    |> maybe_append_bridged_info(token)
    +  end
    +
    +  def render("token.json", %{token: nil}) do
    +    nil
    +  end
    +
    +  def render("token.json", %{token: token}) do
    +    %{
    +      "address_hash" => Address.checksum(token.contract_address_hash),
    +      # todo: It should be removed in favour `address_hash` property with the next release after 8.0.0
    +      "address" => Address.checksum(token.contract_address_hash),
    +      "symbol" => token.symbol,
    +      "name" => token.name,
    +      "decimals" => token.decimals,
    +      "type" => token.type,
    +      "holders_count" => prepare_holders_count(token.holder_count),
    +      # todo: It should be removed in favour `holders_count` property with the next release after 8.0.0
    +      "holders" => prepare_holders_count(token.holder_count),
    +      "exchange_rate" => exchange_rate(token),
    +      "volume_24h" => token.volume_24h,
    +      "total_supply" => token.total_supply,
    +      "icon_url" => token.icon_url,
    +      "circulating_market_cap" => token.circulating_market_cap
    +    }
    +    |> maybe_append_bridged_info(token)
    +    |> chain_type_fields(%{address: token.contract_address, field_prefix: nil})
    +  end
    +
    +  def render("token_holders.json", %{
    +        token_balances: token_balances,
    +        next_page_params: next_page_params
    +      }) do
    +    %{
    +      "items" => Enum.map(token_balances, &prepare_token_holder(&1)),
    +      "next_page_params" => next_page_params
    +    }
    +  end
    +
    +  def render("token_instance.json", %{token_instance: token_instance, token: token}) do
    +    prepare_token_instance(token_instance, token)
    +  end
    +
    +  def render("tokens.json", %{tokens: tokens, next_page_params: next_page_params}) do
    +    %{"items" => Enum.map(tokens, &render("token.json", %{token: &1})), "next_page_params" => next_page_params}
    +  end
    +
    +  def render("token_instances.json", %{
    +        token_instances: token_instances,
    +        next_page_params: next_page_params,
    +        token: token
    +      }) do
    +    %{
    +      "items" => Enum.map(token_instances, &render("token_instance.json", %{token_instance: &1, token: token})),
    +      "next_page_params" => next_page_params
    +    }
    +  end
    +
    +  def render("bridged_tokens.json", %{tokens: tokens, next_page_params: next_page_params}) do
    +    %{"items" => Enum.map(tokens, &render("bridged_token.json", %{token: &1})), "next_page_params" => next_page_params}
    +  end
    +
    +  def render("bridged_token.json", %{token: {token, bridged_token}}) do
    +    "token.json"
    +    |> render(%{token: token})
    +    |> Map.merge(%{
    +      foreign_address: Address.checksum(bridged_token.foreign_token_contract_address_hash),
    +      bridge_type: bridged_token.type,
    +      origin_chain_id: bridged_token.foreign_chain_id
    +    })
    +  end
    +
    +  def exchange_rate(%{fiat_value: fiat_value}) when not is_nil(fiat_value), do: to_string(fiat_value)
    +  def exchange_rate(_), do: nil
    +
    +  defp prepare_token_holder(token_balance) do
    +    %{
    +      "address" => Helper.address_with_info(nil, token_balance.address, token_balance.address_hash, false),
    +      "value" => token_balance.value,
    +      "token_id" => token_balance.token_id
    +    }
    +  end
    +
    +  @doc """
    +    Internal json rendering function
    +  """
    +  def prepare_token_instance(instance, token) do
    +    %{
    +      "id" => instance.token_id,
    +      "metadata" => instance.metadata,
    +      "owner" => token_instance_owner(instance.is_unique, instance),
    +      "token" => render("token.json", %{token: token}),
    +      "external_app_url" => NFTHelper.external_url(instance),
    +      "animation_url" => instance.metadata && NFTHelper.retrieve_image(instance.metadata["animation_url"]),
    +      "image_url" => instance.metadata && NFTHelper.get_media_src(instance.metadata, false),
    +      "is_unique" => instance.is_unique,
    +      "thumbnails" => instance.thumbnails,
    +      "media_type" => instance.media_type,
    +      "media_url" => Instance.get_media_url_from_metadata_for_nft_media_handler(instance.metadata)
    +    }
    +  end
    +
    +  defp token_instance_owner(false, _instance), do: nil
    +  defp token_instance_owner(nil, _instance), do: nil
    +
    +  defp token_instance_owner(_is_unique, %Instance{owner: %NotLoaded{}} = instance),
    +    do: Helper.address_with_info(nil, nil, instance.owner_address_hash, false)
    +
    +  defp token_instance_owner(_is_unique, %Instance{owner: nil} = instance),
    +    do: Helper.address_with_info(nil, nil, instance.owner_address_hash, false)
    +
    +  defp token_instance_owner(_is_unique, instance),
    +    do: instance.owner && Helper.address_with_info(nil, instance.owner, instance.owner.hash, false)
    +
    +  defp prepare_holders_count(nil), do: nil
    +  defp prepare_holders_count(count) when count < 0, do: prepare_holders_count(0)
    +  defp prepare_holders_count(count), do: to_string(count)
    +
    +  defp maybe_append_bridged_info(map, token) do
    +    if BridgedToken.enabled?() do
    +      (token && Map.put(map, "is_bridged", token.bridged || false)) || map
    +    else
    +      map
    +    end
    +  end
    +
    +  case @chain_type do
    +    :filecoin ->
    +      defp chain_type_fields(result, params) do
    +        # credo:disable-for-next-line Credo.Check.Design.AliasUsage
    +        BlockScoutWeb.API.V2.FilecoinView.put_filecoin_robust_address(result, params)
    +      end
    +
    +    _ ->
    +      defp chain_type_fields(result, _params) do
    +        result
    +      end
    +  end
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v2/transaction_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v2/transaction_view.ex
    new file mode 100644
    index 0000000..f9420d9
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v2/transaction_view.ex
    @@ -0,0 +1,938 @@
    +defmodule BlockScoutWeb.API.V2.TransactionView do
    +  use BlockScoutWeb, :view
    +  use Utils.CompileTimeEnvHelper, chain_type: [:explorer, :chain_type]
    +
    +  alias BlockScoutWeb.API.V2.{ApiView, Helper, InternalTransactionView, TokenTransferView, TokenView}
    +
    +  alias BlockScoutWeb.Models.GetTransactionTags
    +  alias BlockScoutWeb.{TransactionStateView, TransactionView}
    +  alias Ecto.Association.NotLoaded
    +  alias Explorer.{Chain, Market}
    +  alias Explorer.Chain.{Address, Block, DecodingHelper, Log, SignedAuthorization, Token, Transaction, Wei}
    +  alias Explorer.Chain.Block.Reward
    +  alias Explorer.Chain.Cache.Counters.AverageBlockTime
    +  alias Explorer.Chain.Transaction.StateChange
    +  alias Timex.Duration
    +
    +  import BlockScoutWeb.Account.AuthController, only: [current_user: 1]
    +
    +  @api_true [api?: true]
    +
    +  def render("message.json", assigns) do
    +    ApiView.render("message.json", assigns)
    +  end
    +
    +  def render("transactions_watchlist.json", %{
    +        transactions: transactions,
    +        next_page_params: next_page_params,
    +        conn: conn,
    +        watchlist_names: watchlist_names
    +      }) do
    +    block_height = Chain.block_height(@api_true)
    +    decoded_transactions = Transaction.decode_transactions(transactions, true, @api_true)
    +
    +    %{
    +      "items" =>
    +        transactions
    +        |> with_chain_type_transformations()
    +        |> Enum.zip(decoded_transactions)
    +        |> Enum.map(fn {transaction, decoded_input} ->
    +          prepare_transaction(transaction, conn, false, block_height, watchlist_names, decoded_input)
    +        end),
    +      "next_page_params" => next_page_params
    +    }
    +  end
    +
    +  def render("transactions_watchlist.json", %{
    +        transactions: transactions,
    +        conn: conn,
    +        watchlist_names: watchlist_names
    +      }) do
    +    block_height = Chain.block_height(@api_true)
    +    decoded_transactions = Transaction.decode_transactions(transactions, true, @api_true)
    +
    +    transactions
    +    |> with_chain_type_transformations()
    +    |> Enum.zip(decoded_transactions)
    +    |> Enum.map(fn {transaction, decoded_input} ->
    +      prepare_transaction(transaction, conn, false, block_height, watchlist_names, decoded_input)
    +    end)
    +  end
    +
    +  def render("transactions.json", %{transactions: transactions, next_page_params: next_page_params, conn: conn}) do
    +    block_height = Chain.block_height(@api_true)
    +    decoded_transactions = Transaction.decode_transactions(transactions, true, @api_true)
    +
    +    %{
    +      "items" =>
    +        transactions
    +        |> with_chain_type_transformations()
    +        |> Enum.zip(decoded_transactions)
    +        |> Enum.map(fn {transaction, decoded_input} ->
    +          prepare_transaction(transaction, conn, false, block_height, decoded_input)
    +        end),
    +      "next_page_params" => next_page_params
    +    }
    +  end
    +
    +  def render("transactions.json", %{transactions: transactions, items: true, conn: conn}) do
    +    %{
    +      "items" => render("transactions.json", %{transactions: transactions, conn: conn})
    +    }
    +  end
    +
    +  def render("transactions.json", %{transactions: transactions, conn: conn}) do
    +    block_height = Chain.block_height(@api_true)
    +    decoded_transactions = Transaction.decode_transactions(transactions, true, @api_true)
    +
    +    transactions
    +    |> with_chain_type_transformations()
    +    |> Enum.zip(decoded_transactions)
    +    |> Enum.map(fn {transaction, decoded_input} ->
    +      prepare_transaction(transaction, conn, false, block_height, decoded_input)
    +    end)
    +  end
    +
    +  def render("transaction.json", %{transaction: transaction, conn: conn}) do
    +    block_height = Chain.block_height(@api_true)
    +    [decoded_input] = Transaction.decode_transactions([transaction], false, @api_true)
    +
    +    transaction
    +    |> with_chain_type_transformations()
    +    |> prepare_transaction(conn, true, block_height, decoded_input)
    +  end
    +
    +  def render("raw_trace.json", %{raw_traces: raw_traces}) do
    +    raw_traces
    +  end
    +
    +  def render("decoded_log_input.json", %{method_id: method_id, text: text, mapping: mapping}) do
    +    %{"method_id" => method_id, "method_call" => text, "parameters" => prepare_log_mapping(mapping)}
    +  end
    +
    +  def render("decoded_input.json", %{method_id: method_id, text: text, mapping: mapping, error?: _error}) do
    +    %{"method_id" => method_id, "method_call" => text, "parameters" => prepare_method_mapping(mapping)}
    +  end
    +
    +  def render("revert_reason.json", %{raw: raw}) do
    +    %{"raw" => raw}
    +  end
    +
    +  def render("token_transfers.json", %{token_transfers: token_transfers, next_page_params: next_page_params, conn: conn}) do
    +    decoded_transactions =
    +      Transaction.decode_transactions(Enum.map(token_transfers, fn tt -> tt.transaction end), true, @api_true)
    +
    +    %{
    +      "items" =>
    +        token_transfers
    +        |> Enum.zip(decoded_transactions)
    +        |> Enum.map(fn {tt, decoded_input} -> TokenTransferView.prepare_token_transfer(tt, conn, decoded_input) end),
    +      "next_page_params" => next_page_params
    +    }
    +  end
    +
    +  def render("token_transfers.json", %{token_transfers: token_transfers, conn: conn}) do
    +    decoded_transactions =
    +      Transaction.decode_transactions(Enum.map(token_transfers, fn tt -> tt.transaction end), true, @api_true)
    +
    +    token_transfers
    +    |> Enum.zip(decoded_transactions)
    +    |> Enum.map(fn {tt, decoded_input} -> TokenTransferView.prepare_token_transfer(tt, conn, decoded_input) end)
    +  end
    +
    +  def render("token_transfer.json", %{token_transfer: token_transfer, conn: conn}) do
    +    [decoded_transaction] = Transaction.decode_transactions([token_transfer.transaction], true, @api_true)
    +    TokenTransferView.prepare_token_transfer(token_transfer, conn, decoded_transaction)
    +  end
    +
    +  def render("transaction_actions.json", %{actions: actions}) do
    +    Enum.map(actions, &prepare_transaction_action(&1))
    +  end
    +
    +  def render("internal_transactions.json", %{
    +        internal_transactions: internal_transactions,
    +        next_page_params: next_page_params,
    +        block: block
    +      }) do
    +    %{
    +      "items" => Enum.map(internal_transactions, &InternalTransactionView.prepare_internal_transaction(&1, block)),
    +      "next_page_params" => next_page_params
    +    }
    +  end
    +
    +  def render("internal_transactions.json", %{
    +        internal_transactions: internal_transactions,
    +        next_page_params: next_page_params
    +      }) do
    +    %{
    +      "items" => Enum.map(internal_transactions, &InternalTransactionView.prepare_internal_transaction(&1)),
    +      "next_page_params" => next_page_params
    +    }
    +  end
    +
    +  def render("logs.json", %{logs: logs, next_page_params: next_page_params, transaction_hash: transaction_hash}) do
    +    decoded_logs = decode_logs(logs, false)
    +
    +    %{
    +      "items" =>
    +        logs
    +        |> Enum.zip(decoded_logs)
    +        |> Enum.map(fn {log, decoded_log} -> prepare_log(log, transaction_hash, decoded_log) end),
    +      "next_page_params" => next_page_params
    +    }
    +  end
    +
    +  def render("logs.json", %{logs: logs, next_page_params: next_page_params}) do
    +    decoded_logs = decode_logs(logs, false)
    +
    +    %{
    +      "items" =>
    +        logs
    +        |> Enum.zip(decoded_logs)
    +        |> Enum.map(fn {log, decoded_log} -> prepare_log(log, log.transaction, decoded_log) end),
    +      "next_page_params" => next_page_params
    +    }
    +  end
    +
    +  def render("state_changes.json", %{state_changes: state_changes, next_page_params: next_page_params}) do
    +    %{
    +      "items" => Enum.map(state_changes, &prepare_state_change(&1)),
    +      "next_page_params" => next_page_params
    +    }
    +  end
    +
    +  def render("stats.json", %{
    +        transactions_count_24h: transactions_count,
    +        pending_transactions_count: pending_transactions_count,
    +        transaction_fees_sum_24h: transaction_fees_sum,
    +        transaction_fees_avg_24h: transaction_fees_avg
    +      }) do
    +    %{
    +      "transactions_count_24h" => transactions_count,
    +      "pending_transactions_count" => pending_transactions_count,
    +      "transaction_fees_sum_24h" => transaction_fees_sum,
    +      "transaction_fees_avg_24h" => transaction_fees_avg
    +    }
    +  end
    +
    +  def render("authorization_list.json", %{signed_authorizations: signed_authorizations}) do
    +    signed_authorizations
    +    |> Enum.sort_by(& &1.index, :asc)
    +    |> Enum.map(&prepare_signed_authorization/1)
    +  end
    +
    +  @doc """
    +    Decodes list of logs
    +  """
    +  @spec decode_logs([Log.t()], boolean) :: [tuple]
    +  def decode_logs(logs, skip_sig_provider?) do
    +    unique_log_address_hashes =
    +      logs
    +      |> Enum.map(fn log -> log.address_hash end)
    +      |> Enum.uniq()
    +
    +    full_abi_per_address_hash =
    +      Log.accumulate_abi_by_address_hashes(%{}, unique_log_address_hashes, @api_true)
    +
    +    {all_logs, _, _} =
    +      Enum.reduce(logs, {[], full_abi_per_address_hash, %{}}, fn log,
    +                                                                 {results, full_abi_per_address_hash_acc, events_acc} ->
    +        {result, full_abi_per_address_hash_acc, events_acc} =
    +          Log.decode(
    +            log,
    +            %Transaction{hash: log.transaction_hash},
    +            @api_true,
    +            skip_sig_provider?,
    +            true,
    +            full_abi_per_address_hash_acc[log.address_hash],
    +            full_abi_per_address_hash_acc,
    +            events_acc
    +          )
    +
    +        {[result | results], full_abi_per_address_hash_acc, events_acc}
    +      end)
    +
    +    all_logs_with_index =
    +      all_logs
    +      |> Enum.reverse()
    +      |> Enum.with_index(fn element, index -> {index, element} end)
    +
    +    %{
    +      :already_decoded_logs => already_decoded_logs,
    +      :input_for_sig_provider_batched_request => input_for_sig_provider_batched_request
    +    } =
    +      all_logs_with_index
    +      |> Enum.reduce(
    +        %{
    +          :already_decoded_logs => [],
    +          :input_for_sig_provider_batched_request => []
    +        },
    +        fn {index, result}, acc ->
    +          case result do
    +            {:error, :try_with_sig_provider, {log, transaction_hash}} ->
    +              Map.put(acc, :input_for_sig_provider_batched_request, [
    +                {index,
    +                 %{
    +                   :log => log,
    +                   :transaction_hash => transaction_hash
    +                 }}
    +                | acc.input_for_sig_provider_batched_request
    +              ])
    +
    +            _ ->
    +              Map.put(acc, :already_decoded_logs, [{index, result} | acc.already_decoded_logs])
    +          end
    +        end
    +      )
    +
    +    decoded_with_sig_provider_logs =
    +      Log.decode_events_batch_via_sig_provider(input_for_sig_provider_batched_request, skip_sig_provider?)
    +
    +    full_logs = already_decoded_logs ++ decoded_with_sig_provider_logs
    +
    +    full_logs
    +    |> Enum.sort_by(fn {index, _log} -> index end, :asc)
    +    |> Enum.map(fn {_index, log} ->
    +      format_decoded_log_input(log)
    +    end)
    +  end
    +
    +  def prepare_transaction_action(action) do
    +    %{
    +      "protocol" => action.protocol,
    +      "type" => action.type,
    +      "data" => action.data
    +    }
    +  end
    +
    +  def prepare_log(log, transaction_or_hash, decoded_log, tags_for_address_needed? \\ false) do
    +    decoded = process_decoded_log(decoded_log)
    +
    +    %{
    +      "transaction_hash" => get_transaction_hash(transaction_or_hash),
    +      "address" => Helper.address_with_info(nil, log.address, log.address_hash, tags_for_address_needed?),
    +      "topics" => [
    +        log.first_topic,
    +        log.second_topic,
    +        log.third_topic,
    +        log.fourth_topic
    +      ],
    +      "data" => log.data,
    +      "index" => log.index,
    +      "decoded" => decoded,
    +      "smart_contract" => smart_contract_info(transaction_or_hash),
    +      "block_number" => log.block_number,
    +      "block_hash" => log.block_hash
    +    }
    +  end
    +
    +  @doc """
    +    Extracts the necessary fields from the signed authorization for the API response.
    +
    +    ## Parameters
    +    - `signed_authorization`: A `SignedAuthorization.t()` struct containing the signed authorization data.
    +
    +    ## Returns
    +    - A map with the necessary fields for the API response.
    +  """
    +  @spec prepare_signed_authorization(SignedAuthorization.t()) :: map()
    +  def prepare_signed_authorization(signed_authorization) do
    +    %{
    +      "address_hash" => Address.checksum(signed_authorization.address),
    +      # todo: It should be removed in favour `address_hash` property with the next release after 8.0.0
    +      "address" => Address.checksum(signed_authorization.address),
    +      "chain_id" => signed_authorization.chain_id,
    +      "nonce" => signed_authorization.nonce,
    +      "r" => signed_authorization.r,
    +      "s" => signed_authorization.s,
    +      "v" => signed_authorization.v,
    +      "authority" => Address.checksum(signed_authorization.authority)
    +    }
    +  end
    +
    +  defp get_transaction_hash(%Transaction{} = transaction), do: to_string(transaction.hash)
    +  defp get_transaction_hash(hash), do: to_string(hash)
    +
    +  defp smart_contract_info(%Transaction{} = transaction),
    +    do: Helper.address_with_info(nil, transaction.to_address, transaction.to_address_hash, false)
    +
    +  defp smart_contract_info(_), do: nil
    +
    +  defp process_decoded_log(decoded_log) do
    +    case decoded_log do
    +      {:ok, method_id, text, mapping} ->
    +        render(__MODULE__, "decoded_log_input.json", method_id: method_id, text: text, mapping: mapping)
    +
    +      _ ->
    +        nil
    +    end
    +  end
    +
    +  defp prepare_transaction(transaction, conn, single_transaction?, block_height, watchlist_names \\ nil, decoded_input)
    +
    +  defp prepare_transaction(
    +         {%Reward{} = emission_reward, %Reward{} = validator_reward},
    +         conn,
    +         single_transaction?,
    +         _block_height,
    +         _watchlist_names,
    +         _decoded_input
    +       ) do
    +    %{
    +      "emission_reward" => emission_reward.reward,
    +      "block_hash" => validator_reward.block_hash,
    +      "from" =>
    +        Helper.address_with_info(
    +          single_transaction? && conn,
    +          emission_reward.address,
    +          emission_reward.address_hash,
    +          single_transaction?
    +        ),
    +      "to" =>
    +        Helper.address_with_info(
    +          single_transaction? && conn,
    +          validator_reward.address,
    +          validator_reward.address_hash,
    +          single_transaction?
    +        ),
    +      "types" => [:reward]
    +    }
    +  end
    +
    +  defp prepare_transaction(
    +         %Transaction{} = transaction,
    +         conn,
    +         single_transaction?,
    +         block_height,
    +         watchlist_names,
    +         decoded_input
    +       ) do
    +    base_fee_per_gas = transaction.block && transaction.block.base_fee_per_gas
    +    max_priority_fee_per_gas = transaction.max_priority_fee_per_gas
    +    max_fee_per_gas = transaction.max_fee_per_gas
    +
    +    priority_fee_per_gas = Transaction.priority_fee_per_gas(max_priority_fee_per_gas, base_fee_per_gas, max_fee_per_gas)
    +
    +    burnt_fees = burnt_fees(transaction, max_fee_per_gas, base_fee_per_gas)
    +
    +    status = transaction |> Chain.transaction_to_status() |> format_status()
    +
    +    revert_reason = revert_reason(status, transaction, single_transaction?)
    +
    +    decoded_input_data = decoded_input(decoded_input)
    +
    +    result = %{
    +      "hash" => transaction.hash,
    +      "result" => status,
    +      "status" => transaction.status,
    +      "block_number" => transaction.block_number,
    +      "timestamp" => block_timestamp(transaction),
    +      "from" =>
    +        Helper.address_with_info(
    +          single_transaction? && conn,
    +          transaction.from_address,
    +          transaction.from_address_hash,
    +          single_transaction?,
    +          watchlist_names
    +        ),
    +      "to" =>
    +        Helper.address_with_info(
    +          single_transaction? && conn,
    +          transaction.to_address,
    +          transaction.to_address_hash,
    +          single_transaction?,
    +          watchlist_names
    +        ),
    +      "created_contract" =>
    +        Helper.address_with_info(
    +          single_transaction? && conn,
    +          transaction.created_contract_address,
    +          transaction.created_contract_address_hash,
    +          single_transaction?,
    +          watchlist_names
    +        ),
    +      "confirmations" => transaction.block |> Chain.confirmations(block_height: block_height) |> format_confirmations(),
    +      "confirmation_duration" => processing_time_duration(transaction),
    +      "value" => transaction.value,
    +      "fee" => transaction |> Transaction.fee(:wei) |> format_fee(),
    +      "gas_price" => transaction.gas_price || Transaction.effective_gas_price(transaction),
    +      "type" => transaction.type,
    +      "gas_used" => transaction.gas_used,
    +      "gas_limit" => transaction.gas,
    +      "max_fee_per_gas" => transaction.max_fee_per_gas,
    +      "max_priority_fee_per_gas" => transaction.max_priority_fee_per_gas,
    +      "base_fee_per_gas" => base_fee_per_gas,
    +      "priority_fee" => priority_fee_per_gas && Wei.mult(priority_fee_per_gas, transaction.gas_used),
    +      "transaction_burnt_fee" => burnt_fees,
    +      "nonce" => transaction.nonce,
    +      "position" => transaction.index,
    +      "revert_reason" => revert_reason,
    +      "raw_input" => transaction.input,
    +      "decoded_input" => decoded_input_data,
    +      "token_transfers" => token_transfers(transaction.token_transfers, conn, single_transaction?),
    +      "token_transfers_overflow" => token_transfers_overflow(transaction.token_transfers, single_transaction?),
    +      "actions" => transaction_actions(transaction.transaction_actions),
    +      "exchange_rate" => Market.get_coin_exchange_rate().fiat_value,
    +      "historic_exchange_rate" =>
    +        Market.get_coin_exchange_rate_at_date(block_timestamp(transaction), @api_true).fiat_value,
    +      "method" => Transaction.method_name(transaction, decoded_input),
    +      "transaction_types" => transaction_types(transaction),
    +      "transaction_tag" =>
    +        GetTransactionTags.get_transaction_tags(transaction.hash, current_user(single_transaction? && conn)),
    +      "has_error_in_internal_transactions" => transaction.has_error_in_internal_transactions,
    +      "authorization_list" => authorization_list(transaction.signed_authorizations)
    +    }
    +
    +    result
    +    |> with_chain_type_fields(transaction, single_transaction?, conn, watchlist_names)
    +  end
    +
    +  def token_transfers(_, _conn, false), do: nil
    +  def token_transfers(%NotLoaded{}, _conn, _), do: nil
    +
    +  def token_transfers(token_transfers, conn, _) do
    +    render("token_transfers.json", %{
    +      token_transfers: Enum.take(token_transfers, Chain.get_token_transfers_per_transaction_preview_count()),
    +      conn: conn
    +    })
    +  end
    +
    +  def token_transfers_overflow(_, false), do: nil
    +  def token_transfers_overflow(%NotLoaded{}, _), do: false
    +
    +  def token_transfers_overflow(token_transfers, _),
    +    do: Enum.count(token_transfers) > Chain.get_token_transfers_per_transaction_preview_count()
    +
    +  def transaction_actions(%NotLoaded{}), do: []
    +
    +  @doc """
    +    Renders transaction actions
    +  """
    +  def transaction_actions(actions) do
    +    render("transaction_actions.json", %{actions: actions})
    +  end
    +
    +  @doc """
    +    Renders the authorization list for a transaction.
    +
    +    ## Parameters
    +    - `signed_authorizations`: A list of `SignedAuthorization.t()` structs.
    +
    +    ## Returns
    +    - A list of maps with the necessary fields for the API response.
    +  """
    +  @spec authorization_list(nil | NotLoaded.t() | [SignedAuthorization.t()]) :: [map()]
    +  def authorization_list(nil), do: []
    +  def authorization_list(%NotLoaded{}), do: []
    +
    +  def authorization_list(signed_authorizations) do
    +    render("authorization_list.json", %{signed_authorizations: signed_authorizations})
    +  end
    +
    +  defp burnt_fees(transaction, max_fee_per_gas, base_fee_per_gas) do
    +    if !is_nil(max_fee_per_gas) and !is_nil(transaction.gas_used) and !is_nil(base_fee_per_gas) do
    +      if Decimal.compare(max_fee_per_gas.value, 0) == :eq do
    +        %Wei{value: Decimal.new(0)}
    +      else
    +        Wei.mult(base_fee_per_gas, transaction.gas_used)
    +      end
    +    else
    +      nil
    +    end
    +  end
    +
    +  defp revert_reason(status, transaction, single_transaction?) do
    +    reverted? = is_binary(status) && status |> String.downcase() |> String.contains?("reverted")
    +
    +    cond do
    +      reverted? && single_transaction? ->
    +        prepare_revert_reason_for_single_transaction(transaction)
    +
    +      reverted? && !single_transaction? ->
    +        %Transaction{revert_reason: revert_reason} = transaction
    +        render(__MODULE__, "revert_reason.json", raw: revert_reason)
    +
    +      true ->
    +        nil
    +    end
    +  rescue
    +    _ ->
    +      nil
    +  end
    +
    +  defp prepare_revert_reason_for_single_transaction(transaction) do
    +    case TransactionView.transaction_revert_reason(transaction, @api_true) do
    +      {:error, _contract_not_verified, candidates} when candidates != [] ->
    +        {:ok, method_id, text, mapping} = Enum.at(candidates, 0)
    +        render(__MODULE__, "decoded_input.json", method_id: method_id, text: text, mapping: mapping, error?: true)
    +
    +      {:ok, method_id, text, mapping} ->
    +        render(__MODULE__, "decoded_input.json", method_id: method_id, text: text, mapping: mapping, error?: true)
    +
    +      _ ->
    +        hex = TransactionView.get_pure_transaction_revert_reason(transaction)
    +        render(__MODULE__, "revert_reason.json", raw: hex)
    +    end
    +  end
    +
    +  @doc """
    +    Prepares decoded transaction info
    +  """
    +  @spec decoded_input(any()) :: map() | nil
    +  def decoded_input(decoded_input) do
    +    case decoded_input do
    +      {:ok, method_id, text, mapping} ->
    +        render(__MODULE__, "decoded_input.json", method_id: method_id, text: text, mapping: mapping, error?: false)
    +
    +      _ ->
    +        nil
    +    end
    +  end
    +
    +  def prepare_method_mapping(mapping) do
    +    Enum.map(mapping, fn {name, type, value} ->
    +      %{"name" => name, "type" => type, "value" => DecodingHelper.value_json(type, value)}
    +    end)
    +  end
    +
    +  def prepare_log_mapping(mapping) do
    +    Enum.map(mapping, fn {name, type, indexed?, value} ->
    +      %{"name" => name, "type" => type, "indexed" => indexed?, "value" => DecodingHelper.value_json(type, value)}
    +    end)
    +  end
    +
    +  defp format_status({:error, reason}), do: reason
    +  defp format_status(status), do: status
    +
    +  defp format_decoded_log_input({:error, :could_not_decode}), do: nil
    +  defp format_decoded_log_input({:ok, _method_id, _text, _mapping} = decoded), do: decoded
    +  defp format_decoded_log_input({:error, _, candidates}), do: Enum.at(candidates, 0)
    +
    +  def format_confirmations({:ok, confirmations}), do: confirmations
    +  def format_confirmations(_), do: 0
    +
    +  def format_fee({type, value}), do: %{"type" => type, "value" => value}
    +
    +  def processing_time_duration(%Transaction{block: nil}) do
    +    []
    +  end
    +
    +  def processing_time_duration(%Transaction{earliest_processing_start: nil}) do
    +    avg_time = AverageBlockTime.average_block_time()
    +
    +    if avg_time == {:error, :disabled} do
    +      []
    +    else
    +      [
    +        0,
    +        avg_time
    +        |> Duration.to_milliseconds()
    +      ]
    +    end
    +  end
    +
    +  def processing_time_duration(%Transaction{
    +        block: %Block{timestamp: end_time},
    +        earliest_processing_start: earliest_processing_start,
    +        inserted_at: inserted_at
    +      }) do
    +    long_interval = abs(diff(earliest_processing_start, end_time))
    +    short_interval = abs(diff(inserted_at, end_time))
    +    merge_intervals(short_interval, long_interval)
    +  end
    +
    +  def merge_intervals(short, long) when short == long, do: [short]
    +
    +  def merge_intervals(short, long) do
    +    [short, long]
    +  end
    +
    +  def diff(left, right) do
    +    left
    +    |> Timex.diff(right, :milliseconds)
    +  end
    +
    +  @doc """
    +    Returns array of token types for transaction.
    +  """
    +  @spec transaction_types(
    +          Explorer.Chain.Transaction.t(),
    +          [transaction_type],
    +          transaction_type
    +        ) :: [transaction_type]
    +        when transaction_type:
    +               :coin_transfer
    +               | :contract_call
    +               | :contract_creation
    +               | :rootstock_bridge
    +               | :rootstock_remasc
    +               | :token_creation
    +               | :token_transfer
    +               | :blob_transaction
    +               | :set_code_transaction
    +  def transaction_types(transaction, types \\ [], stage \\ :set_code_transaction)
    +
    +  def transaction_types(%Transaction{type: type} = transaction, types, :set_code_transaction) do
    +    # EIP-7702 set code transaction type
    +    types =
    +      if type == 4 do
    +        [:set_code_transaction | types]
    +      else
    +        types
    +      end
    +
    +    transaction_types(transaction, types, :blob_transaction)
    +  end
    +
    +  def transaction_types(%Transaction{type: type} = transaction, types, :blob_transaction) do
    +    # EIP-2718 blob transaction type
    +    types =
    +      if type == 3 do
    +        [:blob_transaction | types]
    +      else
    +        types
    +      end
    +
    +    transaction_types(transaction, types, :token_transfer)
    +  end
    +
    +  def transaction_types(%Transaction{token_transfers: token_transfers} = transaction, types, :token_transfer) do
    +    types =
    +      if (!is_nil(token_transfers) && token_transfers != [] && !match?(%NotLoaded{}, token_transfers)) ||
    +           transaction.has_token_transfers do
    +        [:token_transfer | types]
    +      else
    +        types
    +      end
    +
    +    transaction_types(transaction, types, :token_creation)
    +  end
    +
    +  def transaction_types(
    +        %Transaction{created_contract_address: created_contract_address} = transaction,
    +        types,
    +        :token_creation
    +      ) do
    +    types =
    +      if match?(%Address{}, created_contract_address) && match?(%Token{}, created_contract_address.token) do
    +        [:token_creation | types]
    +      else
    +        types
    +      end
    +
    +    transaction_types(transaction, types, :contract_creation)
    +  end
    +
    +  def transaction_types(
    +        %Transaction{to_address_hash: to_address_hash} = transaction,
    +        types,
    +        :contract_creation
    +      ) do
    +    types =
    +      if is_nil(to_address_hash) do
    +        [:contract_creation | types]
    +      else
    +        types
    +      end
    +
    +    transaction_types(transaction, types, :contract_call)
    +  end
    +
    +  def transaction_types(%Transaction{to_address: to_address} = transaction, types, :contract_call) do
    +    types =
    +      if Address.smart_contract?(to_address) do
    +        [:contract_call | types]
    +      else
    +        types
    +      end
    +
    +    transaction_types(transaction, types, :coin_transfer)
    +  end
    +
    +  def transaction_types(%Transaction{value: value} = transaction, types, :coin_transfer) do
    +    types =
    +      if Decimal.compare(value.value, 0) == :gt do
    +        [:coin_transfer | types]
    +      else
    +        types
    +      end
    +
    +    transaction_types(transaction, types, :rootstock_remasc)
    +  end
    +
    +  def transaction_types(transaction, types, :rootstock_remasc) do
    +    types =
    +      if Transaction.rootstock_remasc_transaction?(transaction) do
    +        [:rootstock_remasc | types]
    +      else
    +        types
    +      end
    +
    +    transaction_types(transaction, types, :rootstock_bridge)
    +  end
    +
    +  def transaction_types(transaction, types, :rootstock_bridge) do
    +    if Transaction.rootstock_bridge_transaction?(transaction) do
    +      [:rootstock_bridge | types]
    +    else
    +      types
    +    end
    +  end
    +
    +  @doc """
    +  Returns block's timestamp from Block/Transaction
    +  """
    +  @spec block_timestamp(any()) :: :utc_datetime_usec | nil
    +  def block_timestamp(%Transaction{block_timestamp: block_ts}) when not is_nil(block_ts), do: block_ts
    +  def block_timestamp(%Transaction{block: %Block{} = block}), do: block.timestamp
    +  def block_timestamp(%Block{} = block), do: block.timestamp
    +  def block_timestamp(_), do: nil
    +
    +  defp prepare_state_change(%StateChange{} = state_change) do
    +    coin_or_transfer =
    +      if state_change.coin_or_token_transfers == :coin,
    +        do: :coin,
    +        else: elem(List.first(state_change.coin_or_token_transfers), 1)
    +
    +    type = if coin_or_transfer == :coin, do: "coin", else: "token"
    +
    +    %{
    +      "address" =>
    +        Helper.address_with_info(nil, state_change.address, state_change.address && state_change.address.hash, false),
    +      "is_miner" => state_change.miner?,
    +      "type" => type,
    +      "token" => if(type == "token", do: TokenView.render("token.json", %{token: coin_or_transfer.token})),
    +      "token_id" => state_change.token_id
    +    }
    +    |> append_balances(state_change.balance_before, state_change.balance_after)
    +    |> append_balance_change(state_change, coin_or_transfer)
    +  end
    +
    +  defp append_balances(map, balance_before, balance_after) do
    +    balances =
    +      if TransactionStateView.not_negative?(balance_before) and TransactionStateView.not_negative?(balance_after) do
    +        %{
    +          "balance_before" => balance_before,
    +          "balance_after" => balance_after
    +        }
    +      else
    +        %{
    +          "balance_before" => nil,
    +          "balance_after" => nil
    +        }
    +      end
    +
    +    Map.merge(map, balances)
    +  end
    +
    +  defp append_balance_change(map, state_change, coin_or_transfer) do
    +    change =
    +      if is_list(state_change.coin_or_token_transfers) and coin_or_transfer.token.type == "ERC-721" do
    +        for {direction, token_transfer} <- state_change.coin_or_token_transfers do
    +          %{"total" => TokenTransferView.prepare_token_transfer_total(token_transfer), "direction" => direction}
    +        end
    +      else
    +        state_change.balance_diff
    +      end
    +
    +    Map.merge(map, %{"change" => change})
    +  end
    +
    +  defp with_chain_type_transformations(transactions) do
    +    chain_type = Application.get_env(:explorer, :chain_type)
    +    do_with_chain_type_transformations(chain_type, transactions)
    +  end
    +
    +  defp do_with_chain_type_transformations(:stability, transactions) do
    +    # credo:disable-for-next-line Credo.Check.Design.AliasUsage
    +    BlockScoutWeb.API.V2.StabilityView.transform_transactions(transactions)
    +  end
    +
    +  defp do_with_chain_type_transformations(_chain_type, transactions) do
    +    transactions
    +  end
    +
    +  defp with_chain_type_fields(result, transaction, single_transaction?, conn, watchlist_names) do
    +    chain_type = Application.get_env(:explorer, :chain_type)
    +    do_with_chain_type_fields(chain_type, result, transaction, single_transaction?, conn, watchlist_names)
    +  end
    +
    +  defp do_with_chain_type_fields(
    +         :polygon_edge,
    +         result,
    +         transaction,
    +         true = _single_transaction?,
    +         conn,
    +         _watchlist_names
    +       ) do
    +    # credo:disable-for-next-line Credo.Check.Design.AliasUsage
    +    BlockScoutWeb.API.V2.PolygonEdgeView.extend_transaction_json_response(result, transaction.hash, conn)
    +  end
    +
    +  defp do_with_chain_type_fields(
    +         :polygon_zkevm,
    +         result,
    +         transaction,
    +         true = _single_transaction?,
    +         _conn,
    +         _watchlist_names
    +       ) do
    +    # credo:disable-for-next-line Credo.Check.Design.AliasUsage
    +    BlockScoutWeb.API.V2.PolygonZkevmView.extend_transaction_json_response(result, transaction)
    +  end
    +
    +  defp do_with_chain_type_fields(:zksync, result, transaction, true = _single_transaction?, _conn, _watchlist_names) do
    +    # credo:disable-for-next-line Credo.Check.Design.AliasUsage
    +    BlockScoutWeb.API.V2.ZkSyncView.extend_transaction_json_response(result, transaction)
    +  end
    +
    +  defp do_with_chain_type_fields(:arbitrum, result, transaction, true = _single_transaction?, _conn, _watchlist_names) do
    +    # credo:disable-for-next-line Credo.Check.Design.AliasUsage
    +    BlockScoutWeb.API.V2.ArbitrumView.extend_transaction_json_response(result, transaction)
    +  end
    +
    +  defp do_with_chain_type_fields(:optimism, result, transaction, true = _single_transaction?, _conn, _watchlist_names) do
    +    # credo:disable-for-next-line Credo.Check.Design.AliasUsage
    +    BlockScoutWeb.API.V2.OptimismView.extend_transaction_json_response(result, transaction)
    +  end
    +
    +  defp do_with_chain_type_fields(:scroll, result, transaction, true = _single_transaction?, _conn, _watchlist_names) do
    +    # credo:disable-for-next-line Credo.Check.Design.AliasUsage
    +    BlockScoutWeb.API.V2.ScrollView.extend_transaction_json_response(result, transaction)
    +  end
    +
    +  defp do_with_chain_type_fields(:suave, result, transaction, true = single_transaction?, conn, watchlist_names) do
    +    # credo:disable-for-next-line Credo.Check.Design.AliasUsage
    +    BlockScoutWeb.API.V2.SuaveView.extend_transaction_json_response(
    +      transaction,
    +      result,
    +      single_transaction?,
    +      conn,
    +      watchlist_names
    +    )
    +  end
    +
    +  defp do_with_chain_type_fields(:stability, result, transaction, _single_transaction?, _conn, _watchlist_names) do
    +    # credo:disable-for-next-line Credo.Check.Design.AliasUsage
    +    BlockScoutWeb.API.V2.StabilityView.extend_transaction_json_response(result, transaction)
    +  end
    +
    +  defp do_with_chain_type_fields(:ethereum, result, transaction, _single_transaction?, _conn, _watchlist_names) do
    +    # credo:disable-for-next-line Credo.Check.Design.AliasUsage
    +    BlockScoutWeb.API.V2.EthereumView.extend_transaction_json_response(result, transaction)
    +  end
    +
    +  defp do_with_chain_type_fields(:celo, result, transaction, _single_transaction?, _conn, _watchlist_names) do
    +    # credo:disable-for-next-line Credo.Check.Design.AliasUsage
    +    BlockScoutWeb.API.V2.CeloView.extend_transaction_json_response(result, transaction)
    +  end
    +
    +  defp do_with_chain_type_fields(:zilliqa, result, transaction, _single_tx?, _conn, _watchlist_names) do
    +    # credo:disable-for-next-line Credo.Check.Design.AliasUsage
    +    BlockScoutWeb.API.V2.ZilliqaView.extend_transaction_json_response(result, transaction)
    +  end
    +
    +  defp do_with_chain_type_fields(_chain_type, result, _transaction, _single_transaction?, _conn, _watchlist_names) do
    +    result
    +  end
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v2/validator_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v2/validator_view.ex
    new file mode 100644
    index 0000000..354340b
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v2/validator_view.ex
    @@ -0,0 +1,68 @@
    +defmodule BlockScoutWeb.API.V2.ValidatorView do
    +  use BlockScoutWeb, :view
    +
    +  alias BlockScoutWeb.API.V2.Helper
    +
    +  def render("stability_validators.json", %{validators: validators, next_page_params: next_page_params}) do
    +    %{"items" => Enum.map(validators, &prepare_stability_validator(&1)), "next_page_params" => next_page_params}
    +  end
    +
    +  def render("blackfort_validators.json", %{validators: validators, next_page_params: next_page_params}) do
    +    %{"items" => Enum.map(validators, &prepare_blackfort_validator(&1)), "next_page_params" => next_page_params}
    +  end
    +
    +  def render("zilliqa_validators.json", %{validators: validators, next_page_params: next_page_params}) do
    +    %{"items" => Enum.map(validators, &prepare_zilliqa_validator(&1)), "next_page_params" => next_page_params}
    +  end
    +
    +  def render("zilliqa_validator.json", %{validator: validator}) do
    +    validator
    +    |> prepare_zilliqa_validator()
    +    |> Map.merge(%{
    +      "peer_id" => validator.peer_id,
    +      "control_address" =>
    +        Helper.address_with_info(nil, validator.control_address, validator.control_address_hash, true),
    +      "reward_address" => Helper.address_with_info(nil, validator.reward_address, validator.reward_address_hash, true),
    +      "signing_address" =>
    +        Helper.address_with_info(nil, validator.signing_address, validator.signing_address_hash, true),
    +      "added_at_block_number" => validator.added_at_block_number,
    +      "stake_updated_at_block_number" => validator.stake_updated_at_block_number
    +    })
    +  end
    +
    +  defp prepare_stability_validator(validator) do
    +    %{
    +      "address" => Helper.address_with_info(nil, validator.address, validator.address_hash, true),
    +      "state" => validator.state,
    +      "blocks_validated_count" => validator.blocks_validated
    +    }
    +  end
    +
    +  defp prepare_blackfort_validator(validator) do
    +    %{
    +      "address" => Helper.address_with_info(nil, validator.address, validator.address_hash, true),
    +      "name" => validator.name,
    +      "commission" => validator.commission,
    +      "self_bonded_amount" => validator.self_bonded_amount,
    +      "delegated_amount" => validator.delegated_amount,
    +      "slashing_status" => %{
    +        "slashed" => validator.slashing_status_is_slashed,
    +        "block_number" => validator.slashing_status_by_block,
    +        "multiplier" => validator.slashing_status_multiplier
    +      },
    +      # todo: Next 3 props should be removed in favour `slashing_status` property with the next release after 8.0.0
    +      "slashing_status_is_slashed" => validator.slashing_status_is_slashed,
    +      "slashing_status_by_block" => validator.slashing_status_by_block,
    +      "slashing_status_multiplier" => validator.slashing_status_multiplier
    +    }
    +  end
    +
    +  @spec prepare_zilliqa_validator(Explorer.Chain.Zilliqa.Staker.t()) :: map()
    +  defp prepare_zilliqa_validator(validator) do
    +    %{
    +      "bls_public_key" => validator.bls_public_key,
    +      "index" => validator.index,
    +      "balance" => to_string(validator.balance)
    +    }
    +  end
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v2/withdrawal_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v2/withdrawal_view.ex
    new file mode 100644
    index 0000000..252a36f
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v2/withdrawal_view.ex
    @@ -0,0 +1,41 @@
    +defmodule BlockScoutWeb.API.V2.WithdrawalView do
    +  use BlockScoutWeb, :view
    +
    +  alias BlockScoutWeb.API.V2.Helper
    +  alias Explorer.Chain.Withdrawal
    +
    +  def render("withdrawals.json", %{withdrawals: withdrawals, next_page_params: next_page_params}) do
    +    %{"items" => Enum.map(withdrawals, &prepare_withdrawal(&1)), "next_page_params" => next_page_params}
    +  end
    +
    +  @spec prepare_withdrawal(Withdrawal.t()) :: map()
    +  def prepare_withdrawal(%Withdrawal{block: %Ecto.Association.NotLoaded{}} = withdrawal) do
    +    %{
    +      "index" => withdrawal.index,
    +      "validator_index" => withdrawal.validator_index,
    +      "receiver" => Helper.address_with_info(nil, withdrawal.address, withdrawal.address_hash, false),
    +      "amount" => withdrawal.amount
    +    }
    +  end
    +
    +  def prepare_withdrawal(%Withdrawal{address: %Ecto.Association.NotLoaded{}} = withdrawal) do
    +    %{
    +      "index" => withdrawal.index,
    +      "validator_index" => withdrawal.validator_index,
    +      "block_number" => withdrawal.block.number,
    +      "amount" => withdrawal.amount,
    +      "timestamp" => withdrawal.block.timestamp
    +    }
    +  end
    +
    +  def prepare_withdrawal(%Withdrawal{} = withdrawal) do
    +    %{
    +      "index" => withdrawal.index,
    +      "validator_index" => withdrawal.validator_index,
    +      "block_number" => withdrawal.block.number,
    +      "receiver" => Helper.address_with_info(nil, withdrawal.address, withdrawal.address_hash, false),
    +      "amount" => withdrawal.amount,
    +      "timestamp" => withdrawal.block.timestamp
    +    }
    +  end
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v2/zilliqa_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v2/zilliqa_view.ex
    new file mode 100644
    index 0000000..2b9ad89
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v2/zilliqa_view.ex
    @@ -0,0 +1,147 @@
    +defmodule BlockScoutWeb.API.V2.ZilliqaView do
    +  @moduledoc """
    +  View functions for rendering Zilliqa-related data in JSON format.
    +  """
    +  use Utils.CompileTimeEnvHelper, chain_type: [:explorer, :chain_type]
    +
    +  if @chain_type == :zilliqa do
    +    # TODO: remove when https://github.com/elixir-lang/elixir/issues/13975 comes to elixir release
    +    import Explorer.Chain.Zilliqa.Helper, only: [scilla_transaction?: 1], warn: false
    +    alias Explorer.Chain.{Address, Block, Transaction}, warn: false
    +    alias Explorer.Chain.Zilliqa.{AggregateQuorumCertificate, QuorumCertificate}, warn: false
    +
    +    @doc """
    +    Extends the JSON output with a sub-map containing information related to Zilliqa,
    +    such as the quorum certificate and aggregate quorum certificate.
    +
    +    ## Parameters
    +    - `out_json`: A map defining the output JSON which will be extended.
    +    - `block`: The block structure containing Zilliqa-related data.
    +    - `single_block?`: A boolean indicating if it is a single block.
    +
    +    ## Returns
    +    - A map extended with data related to Zilliqa.
    +    """
    +    @spec extend_block_json_response(map(), Block.t(), boolean()) :: map()
    +    def extend_block_json_response(out_json, %Block{}, false),
    +      do: out_json
    +
    +    def extend_block_json_response(out_json, %Block{zilliqa_view: zilliqa_view} = block, true) do
    +      zilliqa_json =
    +        %{view: zilliqa_view}
    +        |> add_quorum_certificate(block)
    +        |> add_aggregate_quorum_certificate(block)
    +
    +      Map.put(out_json, :zilliqa, zilliqa_json)
    +    end
    +
    +    @doc """
    +    Extends the JSON output with a sub-map containing information related to Zilliqa,
    +    such as if the transaction is a Scilla transaction.
    +
    +    ## Parameters
    +    - `out_json`: A map defining the output JSON which will be extended.
    +    - `transaction`: The transaction structure.
    +
    +    ## Returns
    +    - A map extended with data related to Zilliqa.
    +    """
    +    @spec extend_transaction_json_response(map(), Transaction.t()) :: map()
    +    def extend_transaction_json_response(out_json, %Transaction{} = transaction) do
    +      Map.put(out_json, :zilliqa, %{
    +        is_scilla: scilla_transaction?(transaction)
    +      })
    +    end
    +
    +    @doc """
    +    Extends the JSON output with a sub-map containing information related to
    +    Zilliqa, such as if the address is a Scilla smart contract.
    +
    +    ## Parameters
    +    - `out_json`: A map defining the output JSON which will be extended.
    +    - `address`: The address structure.
    +
    +    ## Returns
    +    - A map extended with data related to Zilliqa.
    +    """
    +    @spec extend_address_json_response(map(), Address.t()) :: map()
    +    def extend_address_json_response(out_json, %Address{} = address) do
    +      is_scilla_contract =
    +        case address do
    +          %Address{
    +            contract_creation_transaction: transaction
    +          } ->
    +            scilla_transaction?(transaction)
    +
    +          _ ->
    +            false
    +        end
    +
    +      Map.put(out_json, :zilliqa, %{
    +        is_scilla_contract: is_scilla_contract
    +      })
    +    end
    +
    +    def extend_address_json_response(out_json, _address), do: out_json
    +
    +    @spec add_quorum_certificate(map(), Block.t()) :: map()
    +    defp add_quorum_certificate(
    +           zilliqa_json,
    +           %Block{
    +             zilliqa_quorum_certificate: %QuorumCertificate{
    +               view: view,
    +               signature: signature,
    +               signers: signers
    +             }
    +           }
    +         ) do
    +      zilliqa_json
    +      |> Map.put(:quorum_certificate, %{
    +        view: view,
    +        signature: signature,
    +        signers: signers
    +      })
    +    end
    +
    +    defp add_quorum_certificate(zilliqa_json, _block), do: zilliqa_json
    +
    +    @spec add_aggregate_quorum_certificate(map(), Block.t()) :: map()
    +    defp add_aggregate_quorum_certificate(zilliqa_json, %Block{
    +           zilliqa_aggregate_quorum_certificate: %AggregateQuorumCertificate{
    +             view: view,
    +             signature: signature,
    +             nested_quorum_certificates: nested_quorum_certificates
    +           }
    +         })
    +         when is_list(nested_quorum_certificates) do
    +      zilliqa_json
    +      |> Map.put(:aggregate_quorum_certificate, %{
    +        view: view,
    +        signature: signature,
    +        signers:
    +          Enum.map(
    +            nested_quorum_certificates,
    +            & &1.proposed_by_validator_index
    +          ),
    +        nested_quorum_certificates:
    +          Enum.map(
    +            nested_quorum_certificates,
    +            &%{
    +              view: &1.view,
    +              signature: &1.signature,
    +              proposed_by_validator_index: &1.proposed_by_validator_index,
    +              signers: &1.signers
    +            }
    +          )
    +      })
    +    end
    +
    +    defp add_aggregate_quorum_certificate(zilliqa_json, _block), do: zilliqa_json
    +  else
    +    def extend_block_json_response(out_json, _, _),
    +      do: out_json
    +
    +    def extend_transaction_json_response(out_json, _),
    +      do: out_json
    +  end
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v2/zksync_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v2/zksync_view.ex
    new file mode 100644
    index 0000000..0332704
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api/v2/zksync_view.ex
    @@ -0,0 +1,207 @@
    +defmodule BlockScoutWeb.API.V2.ZkSyncView do
    +  use BlockScoutWeb, :view
    +
    +  alias Explorer.Chain.{Block, Transaction}
    +  alias Explorer.Chain.ZkSync.TransactionBatch
    +
    +  alias BlockScoutWeb.API.V2.Helper, as: APIV2Helper
    +
    +  @doc """
    +    Function to render GET requests to `/api/v2/zksync/batches/:batch_number` endpoint.
    +  """
    +  @spec render(binary(), map()) :: map() | non_neg_integer()
    +  def render("zksync_batch.json", %{batch: batch}) do
    +    %{
    +      "number" => batch.number,
    +      "timestamp" => batch.timestamp,
    +      "root_hash" => batch.root_hash,
    +      "l1_transactions_count" => batch.l1_transaction_count,
    +      # todo: It should be removed in favour `l1_transactions_count` property with the next release after 8.0.0
    +      "l1_transaction_count" => batch.l1_transaction_count,
    +      "l2_transactions_count" => batch.l2_transaction_count,
    +      # todo: It should be removed in favour `l2_transactions_count` property with the next release after 8.0.0
    +      "l2_transaction_count" => batch.l2_transaction_count,
    +      "l1_gas_price" => batch.l1_gas_price,
    +      "l2_fair_gas_price" => batch.l2_fair_gas_price,
    +      "start_block_number" => batch.start_block,
    +      "end_block_number" => batch.end_block,
    +      # todo: It should be removed in favour `start_block_number` property with the next release after 8.0.0
    +      "start_block" => batch.start_block,
    +      # todo: It should be removed in favour `end_block_number` property with the next release after 8.0.0
    +      "end_block" => batch.end_block
    +    }
    +    |> add_l1_transactions_info_and_status(batch)
    +  end
    +
    +  @doc """
    +    Function to render GET requests to `/api/v2/zksync/batches` endpoint.
    +  """
    +  def render("zksync_batches.json", %{
    +        batches: batches,
    +        next_page_params: next_page_params
    +      }) do
    +    %{
    +      items: render_zksync_batches(batches),
    +      next_page_params: next_page_params
    +    }
    +  end
    +
    +  @doc """
    +    Function to render GET requests to `/api/v2/main-page/zksync/batches/confirmed` endpoint.
    +  """
    +  def render("zksync_batches.json", %{batches: batches}) do
    +    %{items: render_zksync_batches(batches)}
    +  end
    +
    +  @doc """
    +    Function to render GET requests to `/api/v2/zksync/batches/count` endpoint.
    +  """
    +  def render("zksync_batches_count.json", %{count: count}) do
    +    count
    +  end
    +
    +  @doc """
    +    Function to render GET requests to `/api/v2/main-page/zksync/batches/latest-number` endpoint.
    +  """
    +  def render("zksync_batch_latest_number.json", %{number: number}) do
    +    number
    +  end
    +
    +  defp render_zksync_batches(batches) do
    +    Enum.map(batches, fn batch ->
    +      %{
    +        "number" => batch.number,
    +        "timestamp" => batch.timestamp,
    +        "transactions_count" => batch.l1_transaction_count + batch.l2_transaction_count,
    +        # todo: It should be removed in favour `transactions_count` property with the next release after 8.0.0
    +        "transaction_count" => batch.l1_transaction_count + batch.l2_transaction_count
    +      }
    +      |> add_l1_transactions_info_and_status(batch)
    +    end)
    +  end
    +
    +  @doc """
    +    Extends the json output with a sub-map containing information related
    +    zksync: batch number and associated L1 transactions and their timestamps.
    +
    +    ## Parameters
    +    - `out_json`: a map defining output json which will be extended
    +    - `transaction`: transaction structure containing zksync related data
    +
    +    ## Returns
    +    A map extended with data related zksync rollup
    +  """
    +  @spec extend_transaction_json_response(map(), %{
    +          :__struct__ => Explorer.Chain.Transaction,
    +          optional(:zksync_batch) => any(),
    +          optional(:zksync_commit_transaction) => any(),
    +          optional(:zksync_execute_transaction) => any(),
    +          optional(:zksync_prove_transaction) => any(),
    +          optional(any()) => any()
    +        }) :: map()
    +  def extend_transaction_json_response(out_json, %Transaction{} = transaction) do
    +    do_add_zksync_info(out_json, transaction)
    +  end
    +
    +  @doc """
    +    Extends the json output with a sub-map containing information related
    +    zksync: batch number and associated L1 transactions and their timestamps.
    +
    +    ## Parameters
    +    - `out_json`: a map defining output json which will be extended
    +    - `block`: block structure containing zksync related data
    +
    +    ## Returns
    +    A map extended with data related zksync rollup
    +  """
    +  @spec extend_block_json_response(map(), %{
    +          :__struct__ => Explorer.Chain.Block,
    +          optional(:zksync_batch) => any(),
    +          optional(:zksync_commit_transaction) => any(),
    +          optional(:zksync_execute_transaction) => any(),
    +          optional(:zksync_prove_transaction) => any(),
    +          optional(any()) => any()
    +        }) :: map()
    +  def extend_block_json_response(out_json, %Block{} = block) do
    +    do_add_zksync_info(out_json, block)
    +  end
    +
    +  defp do_add_zksync_info(out_json, zksync_entity) do
    +    res =
    +      %{}
    +      |> do_add_l1_transactions_info_and_status(%{
    +        batch_number: get_batch_number(zksync_entity),
    +        commit_transaction: zksync_entity.zksync_commit_transaction,
    +        prove_transaction: zksync_entity.zksync_prove_transaction,
    +        execute_transaction: zksync_entity.zksync_execute_transaction
    +      })
    +      |> Map.put("batch_number", get_batch_number(zksync_entity))
    +
    +    Map.put(out_json, "zksync", res)
    +  end
    +
    +  defp get_batch_number(zksync_entity) do
    +    case Map.get(zksync_entity, :zksync_batch) do
    +      nil -> nil
    +      %Ecto.Association.NotLoaded{} -> nil
    +      value -> value.number
    +    end
    +  end
    +
    +  defp add_l1_transactions_info_and_status(out_json, %TransactionBatch{} = batch) do
    +    do_add_l1_transactions_info_and_status(out_json, batch)
    +  end
    +
    +  defp do_add_l1_transactions_info_and_status(out_json, zksync_item) do
    +    l1_transactions = get_associated_l1_transactions(zksync_item)
    +
    +    out_json
    +    |> Map.merge(%{
    +      "status" => batch_status(zksync_item),
    +      "commit_transaction_hash" => APIV2Helper.get_2map_data(l1_transactions, :commit_transaction, :hash),
    +      "commit_transaction_timestamp" => APIV2Helper.get_2map_data(l1_transactions, :commit_transaction, :ts),
    +      "prove_transaction_hash" => APIV2Helper.get_2map_data(l1_transactions, :prove_transaction, :hash),
    +      "prove_transaction_timestamp" => APIV2Helper.get_2map_data(l1_transactions, :prove_transaction, :ts),
    +      "execute_transaction_hash" => APIV2Helper.get_2map_data(l1_transactions, :execute_transaction, :hash),
    +      "execute_transaction_timestamp" => APIV2Helper.get_2map_data(l1_transactions, :execute_transaction, :ts)
    +    })
    +  end
    +
    +  # Extract transaction hash and timestamp for L1 transactions associated with
    +  # a zksync rollup entity: batch, transaction or block.
    +  #
    +  # ## Parameters
    +  # - `zksync_item`: A batch, transaction, or block.
    +  #
    +  # ## Returns
    +  # A map containing nesting maps describing corresponding L1 transactions
    +  defp get_associated_l1_transactions(zksync_item) do
    +    [:commit_transaction, :prove_transaction, :execute_transaction]
    +    |> Enum.reduce(%{}, fn key, l1_transactions ->
    +      case Map.get(zksync_item, key) do
    +        nil -> Map.put(l1_transactions, key, nil)
    +        %Ecto.Association.NotLoaded{} -> Map.put(l1_transactions, key, nil)
    +        value -> Map.put(l1_transactions, key, %{hash: value.hash, ts: value.timestamp})
    +      end
    +    end)
    +  end
    +
    +  # Inspects L1 transactions of the batch to determine the batch status.
    +  #
    +  # ## Parameters
    +  # - `zksync_item`: A batch, transaction, or block.
    +  #
    +  # ## Returns
    +  # A string with one of predefined statuses
    +  defp batch_status(zksync_item) do
    +    cond do
    +      APIV2Helper.specified?(zksync_item.execute_transaction) -> "Executed on L1"
    +      APIV2Helper.specified?(zksync_item.prove_transaction) -> "Validated on L1"
    +      APIV2Helper.specified?(zksync_item.commit_transaction) -> "Sent to L1"
    +      # Batch entity itself has no batch_number
    +      not Map.has_key?(zksync_item, :batch_number) -> "Sealed on L2"
    +      not is_nil(zksync_item.batch_number) -> "Sealed on L2"
    +      true -> "Processed on L2"
    +    end
    +  end
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api_docs_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api_docs_view.ex
    new file mode 100644
    index 0000000..77a1b36
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/api_docs_view.ex
    @@ -0,0 +1,79 @@
    +defmodule BlockScoutWeb.APIDocsView do
    +  use BlockScoutWeb, :view
    +
    +  alias BlockScoutWeb.LayoutView
    +  alias Explorer
    +
    +  def action_tile_id(module, action) do
    +    "#{module}-#{action}"
    +  end
    +
    +  def query_params(module, action) do
    +    module_and_action(module, action) <> Enum.join(required_params(action))
    +  end
    +
    +  def input_placeholder(param) do
    +    "#{param.key} - #{param.description}"
    +  end
    +
    +  def model_type_definition(definition) when is_binary(definition) do
    +    definition
    +  end
    +
    +  def model_type_definition(definition_func) when is_function(definition_func, 1) do
    +    coin = Explorer.coin()
    +    definition_func.(coin)
    +  end
    +
    +  defp module_and_action(module, action) do
    +    "?module=#{module}&action=#{action.name}"
    +  end
    +
    +  defp required_params(action) do
    +    Enum.map(action.required_params, fn param ->
    +      "&#{param.key}=" <> "{#{param.placeholder}}"
    +    end)
    +  end
    +
    +  def blockscout_url(set_path) when set_path == false do
    +    url_params = Application.get_env(:block_scout_web, BlockScoutWeb.Endpoint)[:url]
    +    host = url_params[:host]
    +
    +    scheme = Keyword.get(url_params, :scheme, "http")
    +
    +    if host != "localhost" do
    +      "#{scheme}://#{host}"
    +    else
    +      port = Application.get_env(:block_scout_web, BlockScoutWeb.Endpoint)[:http][:port]
    +      "#{scheme}://#{host}:#{to_string(port)}"
    +    end
    +  end
    +
    +  def blockscout_url(set_path) when set_path == true do
    +    url_params = Application.get_env(:block_scout_web, BlockScoutWeb.Endpoint)[:url]
    +    host = url_params[:host]
    +
    +    path = url_params[:path]
    +
    +    scheme = Keyword.get(url_params, :scheme, "http")
    +
    +    if host != "localhost" do
    +      "#{scheme}://#{host}#{path}"
    +    else
    +      port = Application.get_env(:block_scout_web, BlockScoutWeb.Endpoint)[:http][:port]
    +      "#{scheme}://#{host}:#{to_string(port)}"
    +    end
    +  end
    +
    +  def api_url do
    +    true
    +    |> blockscout_url()
    +    |> Path.join("api")
    +  end
    +
    +  def eth_rpc_api_url do
    +    true
    +    |> blockscout_url()
    +    |> Path.join("api/eth-rpc")
    +  end
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/block_transaction_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/block_transaction_view.ex
    new file mode 100644
    index 0000000..025f278
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/block_transaction_view.ex
    @@ -0,0 +1,17 @@
    +defmodule BlockScoutWeb.BlockTransactionView do
    +  use BlockScoutWeb, :view
    +
    +  use Gettext, backend: BlockScoutWeb.Gettext
    +
    +  def block_not_found_message({:ok, true}) do
    +    gettext("Easy Cowboy! This block does not exist yet!")
    +  end
    +
    +  def block_not_found_message({:ok, false}) do
    +    gettext("This block has not been processed yet.")
    +  end
    +
    +  def block_not_found_message({:error, :hash}) do
    +    gettext("Block not found, please try again later.")
    +  end
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/block_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/block_view.ex
    new file mode 100644
    index 0000000..11311a7
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/block_view.ex
    @@ -0,0 +1,85 @@
    +defmodule BlockScoutWeb.BlockView do
    +  use BlockScoutWeb, :view
    +
    +  import Math.Enum, only: [mean: 1]
    +
    +  alias Ecto.Association.NotLoaded
    +  alias Explorer.Chain
    +  alias Explorer.Chain.Cache.Counters.{BlockBurntFeeCount, BlockPriorityFeeCount}
    +  alias Explorer.Chain.{Block, Wei}
    +  alias Explorer.Chain.Block.Reward
    +
    +  @dialyzer :no_match
    +
    +  def average_gas_price(%Block{transactions: transactions}) do
    +    average =
    +      transactions
    +      |> Enum.map(&Decimal.to_float(Wei.to(&1.gas_price, :gwei)))
    +      |> mean()
    +      |> Kernel.||(0)
    +      |> BlockScoutWeb.Cldr.Number.to_string!()
    +
    +    unit_text = gettext("Gwei")
    +
    +    "#{average} #{unit_text}"
    +  end
    +
    +  def block_type(%Block{consensus: false, nephews: %NotLoaded{}}), do: "Reorg"
    +  def block_type(%Block{consensus: false, nephews: []}), do: "Reorg"
    +  def block_type(%Block{consensus: false}), do: "Uncle"
    +  def block_type(_block), do: "Block"
    +
    +  @doc """
    +  Work-around for spec issue in `Cldr.Unit.to_string!/1`
    +  """
    +  def cldr_unit_to_string!(unit) do
    +    # We do this to trick Dialyzer to not complain about non-local returns caused by bug in Cldr.Unit.to_string! spec
    +    case :erlang.phash2(1, 1) do
    +      0 ->
    +        BlockScoutWeb.Cldr.Unit.to_string!(unit)
    +
    +      1 ->
    +        # does not occur
    +        ""
    +    end
    +  end
    +
    +  def formatted_gas(gas, format \\ []) do
    +    BlockScoutWeb.Cldr.Number.to_string!(gas, format)
    +  end
    +
    +  def formatted_timestamp(%Block{timestamp: timestamp}) do
    +    Timex.format!(timestamp, "%b-%d-%Y %H:%M:%S %p %Z", :strftime)
    +  end
    +
    +  def show_reward?([]), do: false
    +  def show_reward?(_), do: true
    +
    +  def block_reward_text(%Reward{address_hash: beneficiary_address, address_type: :validator}, block_miner_address) do
    +    if Application.get_env(:explorer, Explorer.Chain.Block.Reward, %{})[:keys_manager_contract_address] do
    +      %{payout_key: block_miner_payout_address} = Reward.get_validator_payout_key_by_mining_from_db(block_miner_address)
    +
    +      if beneficiary_address == block_miner_payout_address do
    +        gettext("Miner Reward")
    +      else
    +        gettext("Chore Reward")
    +      end
    +    else
    +      gettext("Miner Reward")
    +    end
    +  end
    +
    +  def block_reward_text(%Reward{address_type: :emission_funds}, _block_miner_address) do
    +    gettext("Emission Reward")
    +  end
    +
    +  def block_reward_text(%Reward{address_type: :uncle}, _block_miner_address) do
    +    gettext("Uncle Reward")
    +  end
    +
    +  def combined_rewards_value(block) do
    +    block
    +    |> Chain.block_combined_rewards()
    +    |> format_wei_value(:ether)
    +  end
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/block_withdrawal_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/block_withdrawal_view.ex
    new file mode 100644
    index 0000000..f6b22b3
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/block_withdrawal_view.ex
    @@ -0,0 +1,5 @@
    +defmodule BlockScoutWeb.BlockWithdrawalView do
    +  use BlockScoutWeb, :view
    +
    +  alias Explorer.Chain.Address
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/bridged_tokens_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/bridged_tokens_view.ex
    new file mode 100644
    index 0000000..3cf7e32
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/bridged_tokens_view.ex
    @@ -0,0 +1,24 @@
    +defmodule BlockScoutWeb.BridgedTokensView do
    +  use BlockScoutWeb, :view
    +
    +  alias Explorer.Chain.{BridgedToken, CurrencyHelper, Token}
    +
    +  @doc """
    +  Calculates capitalization of the bridged token in USD.
    +  """
    +  @spec bridged_token_usd_cap(BridgedToken.t(), Token.t()) :: String.t()
    +  def bridged_token_usd_cap(bridged_token, token) do
    +    usd_cap =
    +      if bridged_token.custom_cap do
    +        bridged_token.custom_cap
    +      else
    +        if bridged_token.exchange_rate && token.total_supply do
    +          Decimal.mult(bridged_token.exchange_rate, CurrencyHelper.divide_decimals(token.total_supply, token.decimals))
    +        else
    +          Decimal.new(0)
    +        end
    +      end
    +
    +    usd_cap |> Decimal.to_float() |> :erlang.float_to_binary([:compact, decimals: 20])
    +  end
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/chain_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/chain_view.ex
    new file mode 100644
    index 0000000..bbbcdeb
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/chain_view.ex
    @@ -0,0 +1,69 @@
    +defmodule BlockScoutWeb.ChainView do
    +  use BlockScoutWeb, :view
    +
    +  require Decimal
    +  import Number.Currency, only: [number_to_currency: 2]
    +  import BlockScoutWeb.API.V2.Helper, only: [market_cap: 2]
    +
    +  alias BlockScoutWeb.LayoutView
    +  alias Explorer.Chain.Cache.GasPriceOracle
    +
    +  def format_usd_value(nil), do: ""
    +
    +  def format_usd_value(value) do
    +    if Decimal.is_decimal(value) do
    +      "#{format_currency_value(Decimal.to_float(value))} USD"
    +    else
    +      "#{format_currency_value(value)} USD"
    +    end
    +  end
    +
    +  def format_currency_value(value, symbol \\ "$")
    +
    +  def format_currency_value(nil, _symbol), do: ""
    +
    +  def format_currency_value(%Decimal{} = value, symbol) do
    +    value
    +    |> Decimal.to_float()
    +    |> format_currency_value(symbol)
    +  end
    +
    +  def format_currency_value(value, _symbol) when not is_float(value) do
    +    "N/A"
    +  end
    +
    +  def format_currency_value(value, symbol) when is_float(value) and value < 0 do
    +    "#{symbol}0.00"
    +  end
    +
    +  def format_currency_value(value, symbol) when is_float(value) and value < 0.000001 do
    +    "Less than #{symbol}0.000001"
    +  end
    +
    +  def format_currency_value(value, symbol) when is_float(value) and value < 1 do
    +    "#{number_to_currency(value, unit: symbol, precision: 6)}"
    +  end
    +
    +  def format_currency_value(value, symbol) when is_float(value) and value < 100_000 do
    +    "#{number_to_currency(value, unit: symbol)}"
    +  end
    +
    +  def format_currency_value(value, _symbol) when value >= 1_000_000 and value <= 999_000_000 do
    +    {:ok, value} = Cldr.Number.to_string(value, format: :short, currency: :USD, fractional_digits: 2)
    +    value
    +  end
    +
    +  def format_currency_value(value, symbol) when is_float(value) do
    +    "#{number_to_currency(value, unit: symbol, precision: 0)}"
    +  end
    +
    +  defp gas_prices do
    +    case GasPriceOracle.get_gas_prices() do
    +      {:ok, gas_prices} ->
    +        %{slow: gas_prices[:slow][:price], average: gas_prices[:average][:price], fast: gas_prices[:fast][:price]}
    +
    +      _ ->
    +        nil
    +    end
    +  end
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/cldr_helper/number.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/cldr_helper/number.ex
    new file mode 100644
    index 0000000..0d1c0e1
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/cldr_helper/number.ex
    @@ -0,0 +1,45 @@
    +defmodule BlockScoutWeb.CldrHelper.Number do
    +  @moduledoc """
    +  Work-arounds for `Cldr.Number` bugs
    +  """
    +
    +  alias BlockScoutWeb.Cldr.Number
    +
    +  def to_string(decimal, options) do
    +    # We do this to trick Dialyzer to not complain about non-local returns caused by bug in Cldr.Number.to_string spec
    +    case :erlang.phash2(1, 1) do
    +      0 ->
    +        Number.to_string(decimal, options)
    +
    +      1 ->
    +        # does not occur
    +        ""
    +    end
    +  end
    +
    +  def to_string!(nil), do: ""
    +
    +  def to_string!(decimal) do
    +    # We do this to trick Dialyzer to not complain about non-local returns caused by bug in Cldr.Number.to_string! spec
    +    case :erlang.phash2(1, 1) do
    +      0 ->
    +        Number.to_string!(decimal)
    +
    +      1 ->
    +        # does not occur
    +        ""
    +    end
    +  end
    +
    +  def to_string!(decimal, options) do
    +    # We do this to trick Dialyzer to not complain about non-local returns caused by bug in Cldr.Number.to_string! spec
    +    case :erlang.phash2(1, 1) do
    +      0 ->
    +        Number.to_string!(decimal, options)
    +
    +      1 ->
    +        # does not occur
    +        ""
    +    end
    +  end
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/common_components_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/common_components_view.ex
    new file mode 100644
    index 0000000..f950e79
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/common_components_view.ex
    @@ -0,0 +1,7 @@
    +defmodule BlockScoutWeb.CommonComponentsView do
    +  use BlockScoutWeb, :view
    +
    +  def balance_percentage_enabled?(total_supply) do
    +    Application.get_env(:block_scout_web, :show_percentage) && total_supply > 0
    +  end
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/csv_export.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/csv_export.ex
    new file mode 100644
    index 0000000..9551584
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/csv_export.ex
    @@ -0,0 +1,31 @@
    +defmodule BlockScoutWeb.CsvExportView do
    +  use BlockScoutWeb, :view
    +
    +  alias BlockScoutWeb.Controller, as: BlockScoutWebController
    +  alias Explorer.Chain
    +  alias Explorer.Chain.Address
    +  alias Explorer.Chain.CsvExport.Helper
    +
    +  defp type_display_name(type) do
    +    case type do
    +      "internal-transactions" -> "internal transactions"
    +      "transactions" -> "transactions"
    +      "token-transfers" -> "token transfers"
    +      "logs" -> "logs"
    +      _ -> ""
    +    end
    +  end
    +
    +  defp type_download_path(nil), do: ""
    +
    +  defp type_download_path(type) do
    +    type <> "/csv"
    +  end
    +
    +  defp address_checksum(address_hash_string) do
    +    with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string) do
    +      address_hash
    +      |> Address.checksum()
    +    end
    +  end
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/currency_helper.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/currency_helper.ex
    new file mode 100644
    index 0000000..7dc513f
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/currency_helper.ex
    @@ -0,0 +1,95 @@
    +defmodule BlockScoutWeb.CurrencyHelper do
    +  @moduledoc """
    +  Helper functions for interacting with `t:BlockScoutWeb.ExchangeRates.USD.t/0` values.
    +  """
    +
    +  alias BlockScoutWeb.CldrHelper.Number
    +  alias Explorer.Chain.CurrencyHelper
    +
    +  @doc """
    +  Formats the given integer value to a currency format.
    +
    +  ## Examples
    +
    +      iex> BlockScoutWeb.CurrencyHelper.format_integer_to_currency(1000000)
    +      "1,000,000"
    +  """
    +  @spec format_integer_to_currency(non_neg_integer() | nil) :: String.t()
    +  def format_integer_to_currency(value)
    +
    +  def format_integer_to_currency(nil) do
    +    "-"
    +  end
    +
    +  def format_integer_to_currency(value) do
    +    {:ok, formatted} = Number.to_string(value, format: "#,##0")
    +
    +    formatted
    +  end
    +
    +  @doc """
    +  Formats the given amount according to given decimals.
    +
    +  ## Examples
    +
    +      iex> format_according_to_decimals(nil, Decimal.new(5))
    +      "-"
    +
    +      iex> format_according_to_decimals(Decimal.new(20500000), Decimal.new(5))
    +      "205"
    +
    +      iex> format_according_to_decimals(Decimal.new(20500000), Decimal.new(7))
    +      "2.05"
    +
    +      iex> format_according_to_decimals(Decimal.new(205000), Decimal.new(12))
    +      "0.000000205"
    +
    +      iex> format_according_to_decimals(Decimal.new(205000), Decimal.new(2))
    +      "2,050"
    +
    +      iex> format_according_to_decimals(205000, Decimal.new(2))
    +      "2,050"
    +
    +      iex> format_according_to_decimals(105000, Decimal.new(0))
    +      "105,000"
    +
    +      iex> format_according_to_decimals(105000000000000000000, Decimal.new(100500))
    +      "105"
    +
    +      iex> format_according_to_decimals(105000000000000000000, nil)
    +      "105,000,000,000,000,000,000"
    +  """
    +  @spec format_according_to_decimals(non_neg_integer() | nil, nil) :: String.t()
    +  def format_according_to_decimals(nil, _) do
    +    "-"
    +  end
    +
    +  def format_according_to_decimals(value, nil) do
    +    format_according_to_decimals(value, Decimal.new(0))
    +  end
    +
    +  def format_according_to_decimals(value, decimals) when is_integer(value) do
    +    value
    +    |> Decimal.new()
    +    |> format_according_to_decimals(decimals)
    +  end
    +
    +  @spec format_according_to_decimals(Decimal.t(), Decimal.t()) :: String.t()
    +  def format_according_to_decimals(value, decimals) do
    +    if Decimal.compare(decimals, 24) == :gt do
    +      format_according_to_decimals(value, Decimal.new(18))
    +    else
    +      value
    +      |> CurrencyHelper.divide_decimals(decimals)
    +      |> thousands_separator()
    +    end
    +  end
    +
    +  defp thousands_separator(value) do
    +    if Decimal.to_float(value) > 999 do
    +      Number.to_string!(value)
    +    else
    +      Decimal.to_string(value, :normal)
    +    end
    +  end
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/error_422.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/error_422.ex
    new file mode 100644
    index 0000000..b813dc9
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/error_422.ex
    @@ -0,0 +1,5 @@
    +defmodule BlockScoutWeb.Error422View do
    +  use BlockScoutWeb, :view
    +
    +  @dialyzer :no_match
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/error_helper.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/error_helper.ex
    new file mode 100644
    index 0000000..916746e
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/error_helper.ex
    @@ -0,0 +1,59 @@
    +defmodule BlockScoutWeb.ErrorHelper do
    +  @moduledoc """
    +  Conveniences for translating and building error messages.
    +  """
    +
    +  use Phoenix.HTML
    +
    +  alias Ecto.Changeset
    +  alias Phoenix.HTML.Form
    +  alias Plug.Conn
    +
    +  @doc """
    +  Generates tag for inlined form input errors.
    +  """
    +  def error_tag(form, field, opts \\ []) do
    +    Enum.map(Keyword.get_values(form.errors, field), fn error ->
    +      content_tag(:span, translate_error(error), Keyword.merge([class: "has-error"], opts))
    +    end)
    +  end
    +
    +  @doc """
    +  Gets the errors for a form's input.
    +  """
    +  def errors_for_field(%Form{source: %Conn{}}, _), do: []
    +
    +  def errors_for_field(%Form{source: %Changeset{action: nil}}, _), do: []
    +
    +  def errors_for_field(%Form{source: %Changeset{action: :ignore}}, _), do: []
    +
    +  def errors_for_field(%Form{source: %Changeset{errors: errors}}, field) do
    +    for error <- Keyword.get_values(errors, field) do
    +      translate_error(error)
    +    end
    +  end
    +
    +  @doc """
    +  Translates an error message using gettext.
    +  """
    +  def translate_error({msg, opts}) do
    +    # Because error messages were defined within Ecto, we must
    +    # call the Gettext module passing our Gettext backend. We
    +    # also use the "errors" domain as translations are placed
    +    # in the errors.po file.
    +    # Ecto will pass the :count keyword if the error message is
    +    # meant to be pluralized.
    +    # On your own code and templates, depending on whether you
    +    # need the message to be pluralized or not, this could be
    +    # written simply as:
    +    #
    +    #     dngettext "errors", "1 file", "%{count} files", count
    +    #     dgettext "errors", "is invalid"
    +    #
    +    if count = opts[:count] do
    +      Gettext.dngettext(BlockScoutWeb.Gettext, "errors", msg, msg, count, opts)
    +    else
    +      Gettext.dgettext(BlockScoutWeb.Gettext, "errors", msg, opts)
    +    end
    +  end
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/error_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/error_view.ex
    new file mode 100644
    index 0000000..d800b71
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/error_view.ex
    @@ -0,0 +1,41 @@
    +defmodule BlockScoutWeb.ErrorView do
    +  use BlockScoutWeb, :view
    +
    +  # when type in ["json", "html"]
    +  def render("404." <> _type, _assigns) do
    +    "Page not found"
    +  end
    +
    +  def render("400." <> _type, _assigns) do
    +    "Bad request"
    +  end
    +
    +  def render("401." <> _type, _assigns) do
    +    "Unauthorized"
    +  end
    +
    +  def render("403." <> _type, _assigns) do
    +    "Forbidden"
    +  end
    +
    +  def render("422." <> _type, _assigns) do
    +    "Unprocessable entity"
    +  end
    +
    +  def render("500.html", %{conn: conn}) do
    +    render(BlockScoutWeb.InternalServerErrorView, "index.html",
    +      layout: {BlockScoutWeb.LayoutView, "app.html"},
    +      conn: conn
    +    )
    +  end
    +
    +  def render("500." <> _type, _assigns) do
    +    "Internal server error"
    +  end
    +
    +  # In case no render clause matches or no
    +  # template is found, let's render it as 500
    +  def template_not_found(_template, assigns) do
    +    render("500.html", assigns)
    +  end
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/form_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/form_view.ex
    new file mode 100644
    index 0000000..369fc47
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/form_view.ex
    @@ -0,0 +1,66 @@
    +defmodule BlockScoutWeb.FormView do
    +  use BlockScoutWeb, :view
    +
    +  alias Phoenix.HTML.Form
    +
    +  @type text_input_type :: :email | :hidden | :password | :text
    +  @type text_field_option ::
    +          {:default_value, String.t()}
    +          | {:id, String.t()}
    +          | {:label, String.t()}
    +          | {:placeholder, String.t()}
    +          | {:required, boolean()}
    +  defguard is_text_input(type) when type in ~w(email hidden password text)a
    +
    +  @doc """
    +  Renders a text input field with certain properties.
    +
    +  ## Supported Options
    +
    +  * `:label` - Label for the input field
    +
    +  ## Options as HTML 5 Attributes
    +
    +  The following options will be applied as HTML 5 attributes on the
    +  `` element:
    +
    +  * `:default_value` - Default value to attach to the input field
    +  * `:id` - ID to attach to the input field
    +  * `:placeholder` - Placeholder text for the input field
    +  * `:required` - Mark the input field as required
    +  * `:type` - Input field type
    +  """
    +  @spec text_field(Form.t(), atom(), text_input_type(), [text_field_option()]) :: Phoenix.HTML.safe()
    +  def text_field(%Form{} = form, form_key, input_type, opts \\ [])
    +      when is_text_input(input_type) and is_atom(form_key) do
    +    errors = errors_for_field(form, form_key)
    +    label = Keyword.get(opts, :label)
    +    id = Keyword.get(opts, :id)
    +
    +    supported_input_field_attrs = ~w(default_value id placeholder required)a
    +    base_input_field_opts = Keyword.take(opts, supported_input_field_attrs)
    +
    +    input_field_class =
    +      case errors do
    +        [_ | _] -> "form-control is-invalid"
    +        _ -> "form-control"
    +      end
    +
    +    input_field_opts = Keyword.put(base_input_field_opts, :class, input_field_class)
    +    input_field = input_for_type(input_type).(form, form_key, input_field_opts)
    +
    +    render_opts = [
    +      errors: errors,
    +      id: id,
    +      input_field: input_field,
    +      label: label
    +    ]
    +
    +    render("text_field.html", render_opts)
    +  end
    +
    +  defp input_for_type(:email), do: &email_input/3
    +  defp input_for_type(:text), do: &text_input/3
    +  defp input_for_type(:hidden), do: &hidden_input/3
    +  defp input_for_type(:password), do: &password_input/3
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/icons_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/icons_view.ex
    new file mode 100644
    index 0000000..40c5ee3
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/icons_view.ex
    @@ -0,0 +1,3 @@
    +defmodule BlockScoutWeb.IconsView do
    +  use BlockScoutWeb, :view
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/internal_server_error_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/internal_server_error_view.ex
    new file mode 100644
    index 0000000..837ecb3
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/internal_server_error_view.ex
    @@ -0,0 +1,5 @@
    +defmodule BlockScoutWeb.InternalServerErrorView do
    +  use BlockScoutWeb, :view
    +
    +  @dialyzer :no_match
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/internal_transaction_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/internal_transaction_view.ex
    new file mode 100644
    index 0000000..3d4cafd
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/internal_transaction_view.ex
    @@ -0,0 +1,30 @@
    +defmodule BlockScoutWeb.InternalTransactionView do
    +  use BlockScoutWeb, :view
    +
    +  alias Explorer.Chain.InternalTransaction
    +
    +  use Gettext, backend: BlockScoutWeb.Gettext
    +
    +  @doc """
    +  Returns the formatted string for the type of the internal transaction.
    +
    +  When the type is `call`, we return the formatted string for the call type.
    +
    +  Examples:
    +
    +  iex> BlockScoutWeb.InternalTransactionView.type(%Explorer.Chain.InternalTransaction{type: :reward})
    +  "Reward"
    +
    +  iex> BlockScoutWeb.InternalTransactionView.type(%Explorer.Chain.InternalTransaction{type: :call, call_type: :delegatecall})
    +  "Delegate Call"
    +  """
    +  def type(%InternalTransaction{type: :call, call_type: :call}), do: gettext("Call")
    +  def type(%InternalTransaction{type: :call, call_type: :callcode}), do: gettext("Call Code")
    +  def type(%InternalTransaction{type: :call, call_type: :delegatecall}), do: gettext("Delegate Call")
    +  def type(%InternalTransaction{type: :call, call_type: :staticcall}), do: gettext("Static Call")
    +  def type(%InternalTransaction{type: :call, call_type: :invalid}), do: gettext("Invalid")
    +  def type(%InternalTransaction{type: :create}), do: gettext("Create")
    +  def type(%InternalTransaction{type: :create2}), do: gettext("Create2")
    +  def type(%InternalTransaction{type: :selfdestruct}), do: gettext("Self-Destruct")
    +  def type(%InternalTransaction{type: :reward}), do: gettext("Reward")
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/layout_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/layout_view.ex
    new file mode 100644
    index 0000000..46977c4
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/layout_view.ex
    @@ -0,0 +1,263 @@
    +defmodule BlockScoutWeb.LayoutView do
    +  use BlockScoutWeb, :view
    +
    +  alias EthereumJSONRPC.Variant
    +  alias Explorer.{Chain, Helper}
    +  alias Poison.Parser
    +
    +  import BlockScoutWeb.APIDocsView, only: [blockscout_url: 1]
    +
    +  @default_other_networks [
    +    %{
    +      title: "POA",
    +      url: "https://blockscout.com/poa/core"
    +    },
    +    %{
    +      title: "Sokol",
    +      url: "https://blockscout.com/poa/sokol",
    +      test_net?: true
    +    },
    +    %{
    +      title: "Gnosis Chain",
    +      url: "https://blockscout.com/xdai/mainnet"
    +    },
    +    %{
    +      title: "Ethereum Classic",
    +      url: "https://blockscout.com/etc/mainnet",
    +      other?: true
    +    },
    +    %{
    +      title: "RSK",
    +      url: "https://blockscout.com/rsk/mainnet",
    +      other?: true
    +    }
    +  ]
    +
    +  alias BlockScoutWeb.SocialMedia
    +
    +  def logo do
    +    Keyword.get(application_config(), :logo)
    +  end
    +
    +  def logo_footer do
    +    Keyword.get(application_config(), :footer)[:logo] || Keyword.get(application_config(), :logo)
    +  end
    +
    +  def logo_text do
    +    Keyword.get(application_config(), :logo_text) || nil
    +  end
    +
    +  def subnetwork_title do
    +    Keyword.get(application_config(), :subnetwork) || "Sokol"
    +  end
    +
    +  def network_title do
    +    Keyword.get(application_config(), :network) || "POA"
    +  end
    +
    +  defp application_config do
    +    Application.get_env(:block_scout_web, BlockScoutWeb.Chain)
    +  end
    +
    +  def configured_social_media_services do
    +    SocialMedia.links()
    +  end
    +
    +  @doc """
    +  Generates URL for new issue creation on Github
    +  """
    +  @spec issue_link() :: [term()]
    +  def issue_link do
    +    {os_family, os_name} = :os.type()
    +
    +    params = [
    +      template: "bug_report.yml",
    +      labels: "triage",
    +      "backend-version": version(),
    +      "elixir-version": "Elixir #{System.version()} Erlang/OTP #{System.otp_release()}",
    +      "os-version": "#{os_family} #{os_name}",
    +      "archive-node-type": Variant.get(),
    +      "additional-information": "The issue happened at #{subnetwork_title()} Blockscout instance"
    +    ]
    +
    +    issue_url = "#{Application.get_env(:block_scout_web, :footer)[:github_link]}/issues/new"
    +
    +    [issue_url, "?", URI.encode_query(params)]
    +  end
    +
    +  def version do
    +    BlockScoutWeb.version()
    +  end
    +
    +  def release_link(""), do: ""
    +  def release_link(nil), do: ""
    +
    +  def release_link(version) do
    +    release_link_env_var = Application.get_env(:block_scout_web, :release_link)
    +
    +    release_link =
    +      if release_link_env_var == "" || release_link_env_var == nil do
    +        release_link_from_version(version)
    +      else
    +        release_link_env_var
    +      end
    +
    +    html_escape({:safe, "#{version}"})
    +  end
    +
    +  def release_link_from_version(version) do
    +    repo = "https://github.com/blockscout/blockscout"
    +
    +    if String.contains?(version, "+commit.") do
    +      commit_hash =
    +        version
    +        |> String.split("+commit.")
    +        |> List.last()
    +
    +      repo <> "/commit/" <> commit_hash
    +    else
    +      repo <> "/releases/tag/" <> version
    +    end
    +  end
    +
    +  def ignore_version?("unknown"), do: true
    +  def ignore_version?(_), do: false
    +
    +  def other_networks do
    +    get_other_networks =
    +      if Application.get_env(:block_scout_web, :other_networks) do
    +        try do
    +          :block_scout_web
    +          |> Application.get_env(:other_networks)
    +          |> Parser.parse!(%{keys: :atoms!})
    +        rescue
    +          _ ->
    +            []
    +        end
    +      else
    +        @default_other_networks
    +      end
    +
    +    get_other_networks
    +    |> Enum.reject(fn %{title: title} ->
    +      title == subnetwork_title()
    +    end)
    +    |> Enum.sort()
    +  end
    +
    +  def main_nets(nets) do
    +    nets
    +    |> Enum.reject(&Map.get(&1, :test_net?))
    +  end
    +
    +  def test_nets(nets) do
    +    nets
    +    |> Enum.filter(&Map.get(&1, :test_net?))
    +  end
    +
    +  def dropdown_nets do
    +    other_networks()
    +    |> Enum.reject(&Map.get(&1, :hide_in_dropdown?))
    +  end
    +
    +  def dropdown_main_nets do
    +    dropdown_nets()
    +    |> main_nets()
    +  end
    +
    +  def dropdown_test_nets do
    +    dropdown_nets()
    +    |> test_nets()
    +  end
    +
    +  def dropdown_head_main_nets do
    +    dropdown_nets()
    +    |> main_nets()
    +    |> Enum.reject(&Map.get(&1, :other?))
    +  end
    +
    +  def dropdown_other_nets do
    +    dropdown_nets()
    +    |> main_nets()
    +    |> Enum.filter(&Map.get(&1, :other?))
    +  end
    +
    +  @spec other_explorers() :: map()
    +  def other_explorers do
    +    if Application.get_env(:block_scout_web, :footer)[:link_to_other_explorers] do
    +      decode_other_explorers_json(Application.get_env(:block_scout_web, :footer)[:other_explorers])
    +    else
    +      %{}
    +    end
    +  end
    +
    +  @spec decode_other_explorers_json(nil | String.t()) :: map()
    +  defp decode_other_explorers_json(nil), do: %{}
    +
    +  defp decode_other_explorers_json(data) do
    +    Jason.decode!(~s(#{data}))
    +  rescue
    +    _ -> %{}
    +  end
    +
    +  def webapp_url(conn) do
    +    :block_scout_web
    +    |> Application.get_env(:webapp_url)
    +    |> Helper.validate_url()
    +    |> case do
    +      :error -> chain_path(conn, :show)
    +      {:ok, url} -> url
    +    end
    +  end
    +
    +  def api_url do
    +    :block_scout_web
    +    |> Application.get_env(:api_url)
    +    |> Helper.validate_url()
    +    |> case do
    +      :error -> ""
    +      {:ok, url} -> url
    +    end
    +  end
    +
    +  def apps_list do
    +    apps = Application.get_env(:block_scout_web, :apps)
    +
    +    if apps do
    +      try do
    +        apps
    +        |> Parser.parse!(%{keys: :atoms!})
    +      rescue
    +        _ ->
    +          []
    +      end
    +    else
    +      []
    +    end
    +  end
    +
    +  def sign_in_link do
    +    if Mix.env() == :test do
    +      "/auth/auth0"
    +    else
    +      Application.get_env(:block_scout_web, BlockScoutWeb.Endpoint)[:url][:path] <> "/auth/auth0"
    +    end
    +  end
    +
    +  def sign_out_link do
    +    client_id = Application.get_env(:ueberauth, Ueberauth.Strategy.Auth0.OAuth)[:client_id]
    +    return_to = blockscout_url(true) <> "/auth/logout"
    +    logout_url = Application.get_env(:ueberauth, Ueberauth)[:logout_url]
    +
    +    if client_id && return_to && logout_url do
    +      params = [
    +        client_id: client_id,
    +        returnTo: return_to
    +      ]
    +
    +      [logout_url, "?", URI.encode_query(params)]
    +    else
    +      []
    +    end
    +  end
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/log_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/log_view.ex
    new file mode 100644
    index 0000000..fe12ba9
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/log_view.ex
    @@ -0,0 +1,3 @@
    +defmodule BlockScoutWeb.LogView do
    +  use BlockScoutWeb, :view
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/nft_helper.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/nft_helper.ex
    new file mode 100644
    index 0000000..ee86d1f
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/nft_helper.ex
    @@ -0,0 +1,108 @@
    +defmodule BlockScoutWeb.NFTHelper do
    +  @moduledoc """
    +    Module with functions for NFT view
    +  """
    +  alias Explorer.Token.MetadataRetriever
    +
    +  def get_media_src(nil, _), do: nil
    +
    +  # credo:disable-for-next-line /Complexity/
    +  def get_media_src(metadata, high_quality_media?) do
    +    result =
    +      cond do
    +        metadata["animation_url"] && high_quality_media? ->
    +          retrieve_image(metadata["animation_url"])
    +
    +        metadata["image_url"] ->
    +          retrieve_image(metadata["image_url"])
    +
    +        metadata["image"] ->
    +          retrieve_image(metadata["image"])
    +
    +        image = metadata["properties"]["image"] ->
    +          if is_map(image), do: image["description"], else: image
    +
    +        true ->
    +          nil
    +      end
    +
    +    if result && String.trim(result) == "", do: nil, else: result
    +  end
    +
    +  def external_url(nil), do: nil
    +
    +  def external_url(instance) do
    +    result =
    +      if instance.metadata && instance.metadata["external_url"] do
    +        instance.metadata["external_url"]
    +      else
    +        external_url(nil)
    +      end
    +
    +    if !result || (result && String.trim(result)) == "", do: external_url(nil), else: result
    +  end
    +
    +  def retrieve_image(image) when is_nil(image), do: nil
    +
    +  def retrieve_image(image) when is_map(image) do
    +    image["description"]
    +  end
    +
    +  def retrieve_image(image) when is_list(image) do
    +    image_url = image |> Enum.at(0)
    +    retrieve_image(image_url)
    +  end
    +
    +  def retrieve_image(image_url) do
    +    image_url
    +    |> URI.decode()
    +    |> URI.encode()
    +    |> compose_resource_url()
    +  end
    +
    +  @doc """
    +  Composes a full IPFS URL from the given image URL.
    +
    +  ## Parameters
    +
    +    - image_url: The URL of the image to be composed into an IPFS URL. It can be nil.
    +
    +  ## Returns
    +
    +    - A string representing the full IPFS URL or nil.
    +
    +  ## Examples
    +
    +      iex> compose_resource_url("ipfs://QmTzQ1e1Y1e1Y1e1Y1e1Y1e1Y1e1Y1e1Y1e1Y1e1Y1")
    +      "https://ipfs.io/ipfs/QmTzQ1e1Y1e1Y1e1Y1e1Y1e1Y1e1Y1e1Y1e1Y1e1Y1"
    +
    +  """
    +  @spec compose_resource_url(String.t() | nil) :: String.t() | nil
    +  def compose_resource_url(nil), do: nil
    +
    +  def compose_resource_url(image_url) do
    +    image_url_downcase =
    +      image_url
    +      |> String.downcase()
    +
    +    cond do
    +      image_url_downcase =~ ~r/^ipfs:\/\/ipfs/ ->
    +        # take resource id after "ipfs://ipfs/" prefix
    +        resource_id = image_url |> String.slice(12..-1//1)
    +        MetadataRetriever.ipfs_link(resource_id, true)
    +
    +      image_url_downcase =~ ~r/^ipfs:\/\// ->
    +        # take resource id after "ipfs://" prefix
    +        resource_id = image_url |> String.slice(7..-1//1)
    +        MetadataRetriever.ipfs_link(resource_id, true)
    +
    +      image_url_downcase =~ ~r/^ar:\/\// ->
    +        # take resource id after "ar://" prefix
    +        resource_id = image_url |> String.slice(5..-1//1)
    +        MetadataRetriever.arweave_link(resource_id)
    +
    +      true ->
    +        image_url
    +    end
    +  end
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/page_not_found.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/page_not_found.ex
    new file mode 100644
    index 0000000..b5a18f0
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/page_not_found.ex
    @@ -0,0 +1,5 @@
    +defmodule BlockScoutWeb.PageNotFoundView do
    +  use BlockScoutWeb, :view
    +
    +  @dialyzer :no_match
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/pending_transaction_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/pending_transaction_view.ex
    new file mode 100644
    index 0000000..12ba2fe
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/pending_transaction_view.ex
    @@ -0,0 +1,5 @@
    +defmodule BlockScoutWeb.PendingTransactionView do
    +  use BlockScoutWeb, :view
    +
    +  @dialyzer :no_match
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/render_helper.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/render_helper.ex
    new file mode 100644
    index 0000000..b367dc7
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/render_helper.ex
    @@ -0,0 +1,21 @@
    +defmodule BlockScoutWeb.RenderHelper do
    +  @moduledoc """
    +  Helper functions to render partials from view modules
    +  """
    +  use BlockScoutWeb, :view
    +
    +  @doc """
    +  Renders html using:
    +  * A list of args including `:view_module` and `:partial` to render a partial with the required keyword list.
    +  * Text that will pass directly through to the template
    +  """
    +  def render_partial(args) when is_list(args) do
    +    render(
    +      Keyword.get(args, :view_module),
    +      Keyword.get(args, :partial),
    +      args
    +    )
    +  end
    +
    +  def render_partial(text), do: text
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/robots_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/robots_view.ex
    new file mode 100644
    index 0000000..628ed67
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/robots_view.ex
    @@ -0,0 +1,10 @@
    +defmodule BlockScoutWeb.RobotsView do
    +  use BlockScoutWeb, :view
    +
    +  alias BlockScoutWeb.APIDocsView
    +  alias Explorer.{Chain, PagingOptions}
    +  alias Explorer.Chain.{Address, Token}
    +
    +  @limit 50
    +  defp limit, do: @limit
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/script_helper.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/script_helper.ex
    new file mode 100644
    index 0000000..f3d0c6c
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/script_helper.ex
    @@ -0,0 +1,34 @@
    +defmodule BlockScoutWeb.Views.ScriptHelper do
    +  @moduledoc """
    +  Helper for rendering view specific script tags.
    +  """
    +
    +  import Phoenix.LiveView.Helpers, only: [sigil_H: 2]
    +  import BlockScoutWeb.Router.Helpers, only: [static_path: 2]
    +
    +  alias Phoenix.HTML.Safe
    +
    +  def render_scripts(conn, file_names) do
    +    conn
    +    |> files(file_names)
    +    |> Enum.map(fn file ->
    +      assigns = %{file: file}
    +
    +      ~H"""
    +        
    +      """
    +      |> Safe.to_iodata()
    +      |> List.to_string()
    +    end)
    +  end
    +
    +  defp files(conn, file_names) do
    +    file_names
    +    |> List.wrap()
    +    |> Enum.map(fn file ->
    +      path = "/" <> Path.join("js", file)
    +
    +      static_path(conn, path)
    +    end)
    +  end
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/search_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/search_view.ex
    new file mode 100644
    index 0000000..51bf1b8
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/search_view.ex
    @@ -0,0 +1,19 @@
    +defmodule BlockScoutWeb.SearchView do
    +  use BlockScoutWeb, :view
    +
    +  alias Explorer.Chain
    +  alias Floki
    +
    +  def highlight_search_result(result, query) do
    +    re = ~r/#{query}/i
    +
    +    safe_result =
    +      result
    +      |> html_escape()
    +      |> safe_to_string()
    +
    +    re
    +    |> Regex.replace(safe_result, "\\g{0}", global: true)
    +    |> raw()
    +  end
    +end
    diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/smart_contract_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/smart_contract_view.ex
    new file mode 100644
    index 0000000..eaef178
    --- /dev/null
    +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/smart_contract_view.ex
    @@ -0,0 +1,236 @@
    +defmodule BlockScoutWeb.SmartContractView do
    +  use BlockScoutWeb, :view
    +
    +  import Explorer.SmartContract.Reader, only: [zip_tuple_values_with_types: 2]
    +
    +  alias Explorer.Chain
    +  alias Explorer.Helper, as: ExplorerHelper
    +  alias Explorer.Chain.{Address, Transaction}
    +  alias Explorer.Chain.Hash.Address, as: HashAddress
    +  alias Explorer.Chain.SmartContract
    +  alias Explorer.Chain.SmartContract.Proxy.EIP1167
    +  alias Explorer.SmartContract.Helper
    +
    +  require Logger
    +
    +  def queryable?(inputs) when not is_nil(inputs), do: Enum.any?(inputs)
    +
    +  def queryable?(inputs) when is_nil(inputs), do: false
    +
    +  def writable?(function) when not is_nil(function),
    +    do:
    +      !Helper.constructor?(function) && !Helper.event?(function) &&
    +        (Helper.payable?(function) || Helper.nonpayable?(function))
    +
    +  def writable?(function) when is_nil(function), do: false
    +
    +  def outputs?(outputs) when not is_nil(outputs) do
    +    case outputs do
    +      {:error, _} -> false
    +      _ -> Enum.any?(outputs)
    +    end
    +  end
    +
    +  def outputs?(outputs) when is_nil(outputs), do: false
    +
    +  def error?(outputs) when not is_nil(outputs) do
    +    case outputs do
    +      {:error, _} -> true
    +      _ -> false
    +    end
    +  end
    +
    +  def error?(outputs) when is_nil(outputs), do: false
    +
    +  def address?(type), do: type in ["address", "address payable"]
    +  def int?(type), do: String.contains?(type, "int") && !String.contains?(type, "[")
    +
    +  def named_argument?(%{"name" => ""}), do: false
    +  def named_argument?(%{"name" => nil}), do: false
    +  def named_argument?(%{"name" => _}), do: true
    +  def named_argument?(_), do: false
    +
    +  def values_with_type(value, type, names, index, components \\ nil)
    +
    +  def values_with_type(value, type, names, index, components) when is_list(value) do
    +    cond do
    +      String.starts_with?(type, "tuple") ->
    +        tuple_types =
    +          type
    +          |> String.slice(0..-3//1)
    +          |> supplement_type_with_components(components)
    +
    +        values =
    +          value
    +          |> tuple_array_to_array(tuple_types, fetch_name(names, index + 1))
    +          |> Enum.join("),\n(")
    +
    +        render_array_type_value(type, "(\n" <> values <> ")", fetch_name(names, index))
    +
    +      String.starts_with?(type, "address") ->
    +        values =
    +          value
    +          |> Enum.map_join(", ", &cast_address(&1))
    +
    +        render_array_type_value(type, values, fetch_name(names, index))
    +
    +      String.starts_with?(type, "bytes") ->
    +        values =
    +          value
    +          |> Enum.join(", ")
    +
    +        render_array_type_value(type, values, fetch_name(names, index))
    +
    +      true ->
    +        values =
    +          value
    +          |> Enum.join("),\n(")
    +
    +        render_array_type_value(type, "(\n" <> values <> ")", fetch_name(names, index))
    +    end
    +  end
    +
    +  def values_with_type(value, type, names, index, _components) when is_tuple(value) do
    +    values =
    +      value
    +      |> tuple_to_array(type, fetch_name(names, index + 1))
    +      |> Enum.join("")
    +
    +    render_type_value(type, values, fetch_name(names, index))
    +  end
    +
    +  def values_with_type(value, type, names, index, _components) when type in [:address, "address", "address payable"] do
    +    case HashAddress.cast(value) do
    +      {:ok, address} ->
    +        render_type_value("address", to_string(address), fetch_name(names, index))
    +
    +      _ ->
    +        ""
    +    end
    +  end
    +
    +  def values_with_type(value, string, names, index, _components) when string in ["string", :string],
    +    do: render_type_value("string", Helper.sanitize_input(value), fetch_name(names, index))
    +
    +  def values_with_type(value, "bytes" <> _ = bytes_type, names, index, _components),
    +    do: render_type_value(bytes_type, Helper.sanitize_input(value), fetch_name(names, index))
    +
    +  def values_with_type(value, bytes, names, index, _components) when bytes in [:bytes],
    +    do: render_type_value("bytes", Helper.sanitize_input(value), fetch_name(names, index))
    +
    +  def values_with_type(value, bool, names, index, _components) when bool in ["bool", :bool],
    +    do: render_type_value("bool", Helper.sanitize_input(to_string(value)), fetch_name(names, index))
    +
    +  def values_with_type(value, type, names, index, _components),
    +    do: render_type_value(type, Helper.sanitize_input(value), fetch_name(names, index))
    +
    +  def values_with_type(value, :error, _components),
    +    do: render_type_value("error", Helper.sanitize_input(value), "error")
    +
    +  def cast_address(value) do
    +    case HashAddress.cast(value) do
    +      {:ok, address} ->
    +        to_string(address)
    +
    +      _ ->
    +        Logger.warning(fn -> ["Error decoding address value: #{inspect(value)}"] end)
    +        "(decoding error)"
    +    end
    +  end
    +
    +  defp fetch_name(nil, _index), do: nil
    +
    +  defp fetch_name([], _index), do: nil
    +
    +  defp fetch_name(names, index) when is_list(names) do
    +    Enum.at(names, index)
    +  end
    +
    +  defp fetch_name(name, _index) when is_binary(name) do
    +    name
    +  end
    +
    +  defp tuple_array_to_array(value, type, names) do
    +    value
    +    |> Enum.map(fn item ->
    +      tuple_to_array(item, type, names)
    +    end)
    +  end
    +
    +  defp tuple_to_array(value, type, names) do
    +    value
    +    |> zip_tuple_values_with_types(type)
    +    |> Enum.with_index()
    +    |> Enum.map(fn {{type, value}, index} ->
    +      values_with_type(value, type, fetch_name(names, index), 0)
    +    end)
    +  end
    +
    +  def binary_to_utf_string(item) do
    +    case Integer.parse(to_string(item)) do
    +      {item_integer, ""} ->
    +        to_string(item_integer)
    +
    +      _ ->
    +        if is_binary(item) do
    +          ExplorerHelper.add_0x_prefix(item)
    +        else
    +          to_string(item)
    +        end
    +    end
    +  end
    +
    +  defp render_type_value(type, value, type) do
    +    "
    (#{Helper.sanitize_input(type)}) : #{value}
    " + end + + defp render_type_value(type, value, name) do + "
    #{Helper.sanitize_input(name)} (#{Helper.sanitize_input(type)}) : #{value}
    " + end + + defp render_array_type_value(type, values, name) do + value_to_display = "[" <> values <> "]" + + render_type_value(type, value_to_display, name) + end + + def supplement_type_with_components(type, components) do + if type == "tuple" && components do + types = + components + |> Enum.map_join(",", fn component -> + Map.get(component, "type") + end) + + "tuple[" <> types <> "]" + else + type + end + end + + def decode_revert_reason(to_address, revert_reason, options \\ []) do + {smart_contract, _} = SmartContract.address_hash_to_smart_contract_with_bytecode_twin(to_address, options) + + Transaction.decoded_revert_reason( + %Transaction{to_address: %{smart_contract: smart_contract}, hash: to_address}, + revert_reason, + options + ) + end + + def not_last_element?(length, index), do: length > 1 and index < length - 1 + + def cut_rpc_url(error) do + transport_options = Application.get_env(:explorer, :json_rpc_named_arguments)[:transport_options] + + all_urls = + (transport_options[:urls] || []) ++ + (transport_options[:trace_urls] || []) ++ + (transport_options[:eth_call_urls] || []) ++ + (transport_options[:fallback_urls] || []) ++ + (transport_options[:fallback_trace_urls] || []) ++ + (transport_options[:fallback_eth_call_urls] || []) + + String.replace(error, Enum.reject(all_urls, &(&1 in [nil, ""])), "rpc_url") + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/tab_helper.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/tab_helper.ex new file mode 100644 index 0000000..8e2ec70 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/tab_helper.ex @@ -0,0 +1,70 @@ +defmodule BlockScoutWeb.TabHelper do + @moduledoc """ + Helper functions for dealing with tabs, which are very common between pages. + """ + + @doc """ + Get the current status of a tab by its name and the request path. + + A tab is considered active if its name responds true to active?/2. + + * returns the string "active" if the tab active. + * returns nil if the tab is not active. + + ## Examples + + iex> BlockScoutWeb.TabHelper.tab_status("token", "/page/0xSom3tH1ng/token") + "active" + + iex> BlockScoutWeb.TabHelper.tab_status("token", "/page/0xSom3tH1ng/token_transfer") + nil + """ + def tab_status(tab_name, request_path, show_token_transfers \\ false) do + if tab_active?(tab_name, request_path) do + "active" + else + case request_path do + "/tx/" <> "0x" <> <<_transaction_hash::binary-size(64)>> -> + tab_status_selector(tab_name, show_token_transfers) + + _ -> + nil + end + end + end + + defp tab_status_selector(tab_name, show_token_transfers) do + cond do + tab_name == "token-transfers" && show_token_transfers -> + "active" + + tab_name == "internal-transactions" && !show_token_transfers -> + "active" + + true -> + nil + end + end + + @doc """ + Check if the given tab is the current tab given the request path. + + It is considered active if there is a substring that exactly matches the tab name in the path. + + * returns true if the tab name is in the path. + * returns nil if the tab name is not in the path. + + ## Examples + + iex> BlockScoutWeb.TabHelper.tab_active?("token", "/page/0xSom3tH1ng/token") + true + + iex> BlockScoutWeb.TabHelper.tab_active?("token", "/page/0xSom3tH1ng/token_transfer") + false + """ + def tab_active?("transactions", "/address/" <> "0x" <> <<_address_hash::binary-size(40)>>), do: true + + def tab_active?(tab_name, request_path) do + String.match?(request_path, ~r/\/\b#{tab_name}\b/) + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/tokens/contract_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/tokens/contract_view.ex new file mode 100644 index 0000000..0b90df1 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/tokens/contract_view.ex @@ -0,0 +1,6 @@ +defmodule BlockScoutWeb.Tokens.ContractView do + use BlockScoutWeb, :view + + alias BlockScoutWeb.Tokens.OverviewView + alias Explorer.Chain.Address +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/tokens/helper.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/tokens/helper.ex new file mode 100644 index 0000000..b8757f4 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/tokens/helper.ex @@ -0,0 +1,208 @@ +defmodule BlockScoutWeb.Tokens.Helper do + @moduledoc """ + Helper functions for interacting with `t:BlockScoutWeb.Chain.Token` attributes. + """ + + alias BlockScoutWeb.{AddressView, CurrencyHelper} + alias Explorer.Chain.{Address, Token} + + @doc """ + Returns the token transfers' amount according to the token's type and decimals. + + When the token's type is ERC-20, then we are going to format the amount according to the token's + decimals considering 0 when the decimals is nil. Case the amount is nil, this function will + return the symbol `--`. + + When the token's type is ERC-721, the function will return a string with the token_id that + represents the ERC-721 token since this kind of token doesn't have amount and decimals. + """ + def token_transfer_amount(%{ + token: token, + token_type: token_type, + amount: amount, + amounts: amounts, + token_ids: token_ids + }) do + do_token_transfer_amount(token, token_type, amount, amounts, token_ids) + end + + def token_transfer_amount(%{token: token, token_type: token_type, amount: amount, token_ids: token_ids}) do + do_token_transfer_amount(token, token_type, amount, nil, token_ids) + end + + # TODO: remove this clause along with token transfer denormalization + defp do_token_transfer_amount(%Token{type: "ERC-20"}, nil, nil, nil, _token_ids) do + {:ok, "--"} + end + + defp do_token_transfer_amount(_token, "ERC-20", nil, nil, _token_ids) do + {:ok, "--"} + end + + # TODO: remove this clause along with token transfer denormalization + defp do_token_transfer_amount(%Token{type: "ERC-20", decimals: nil}, nil, amount, _amounts, _token_ids) do + {:ok, CurrencyHelper.format_according_to_decimals(amount, Decimal.new(0))} + end + + defp do_token_transfer_amount(%Token{decimals: nil}, "ERC-20", amount, _amounts, _token_ids) do + {:ok, CurrencyHelper.format_according_to_decimals(amount, Decimal.new(0))} + end + + # TODO: remove this clause along with token transfer denormalization + defp do_token_transfer_amount(%Token{type: "ERC-20", decimals: decimals}, nil, amount, _amounts, _token_ids) do + {:ok, CurrencyHelper.format_according_to_decimals(amount, decimals)} + end + + defp do_token_transfer_amount(%Token{decimals: decimals}, "ERC-20", amount, _amounts, _token_ids) do + {:ok, CurrencyHelper.format_according_to_decimals(amount, decimals)} + end + + # TODO: remove this clause along with token transfer denormalization + defp do_token_transfer_amount(%Token{type: "ERC-721"}, nil, _amount, _amounts, _token_ids) do + {:ok, :erc721_instance} + end + + defp do_token_transfer_amount(_token, "ERC-721", _amount, _amounts, _token_ids) do + {:ok, :erc721_instance} + end + + # TODO: remove this clause along with token transfer denormalization + defp do_token_transfer_amount(%Token{type: type, decimals: decimals}, nil, amount, amounts, token_ids) + when type in ["ERC-1155", "ERC-404"] do + if amount do + {:ok, :erc1155_erc404_instance, CurrencyHelper.format_according_to_decimals(amount, decimals)} + else + {:ok, :erc1155_erc404_instance, amounts, token_ids, decimals} + end + end + + defp do_token_transfer_amount(%Token{decimals: decimals}, type, amount, amounts, token_ids) + when type in ["ERC-1155", "ERC-404"] do + if amount do + {:ok, :erc1155_erc404_instance, CurrencyHelper.format_according_to_decimals(amount, decimals)} + else + {:ok, :erc1155_erc404_instance, amounts, token_ids, decimals} + end + end + + defp do_token_transfer_amount(_token, _token_type, _amount, _amounts, _token_ids) do + nil + end + + def token_transfer_amount_for_api(%{ + token: token, + token_type: token_type, + amount: amount, + amounts: amounts, + token_ids: token_ids + }) do + do_token_transfer_amount_for_api(token, token_type, amount, amounts, token_ids) + end + + def token_transfer_amount_for_api(%{token: token, token_type: token_type, amount: amount, token_ids: token_ids}) do + do_token_transfer_amount_for_api(token, token_type, amount, nil, token_ids) + end + + # TODO: remove this clause along with token transfer denormalization + defp do_token_transfer_amount_for_api(%Token{type: "ERC-20"}, nil, nil, nil, _token_ids) do + {:ok, nil} + end + + defp do_token_transfer_amount_for_api(_token, "ERC-20", nil, nil, _token_ids) do + {:ok, nil} + end + + # TODO: remove this clause along with token transfer denormalization + defp do_token_transfer_amount_for_api( + %Token{type: "ERC-20", decimals: decimals}, + nil, + amount, + _amounts, + _token_ids + ) do + {:ok, amount, decimals} + end + + defp do_token_transfer_amount_for_api( + %Token{decimals: decimals}, + "ERC-20", + amount, + _amounts, + _token_ids + ) do + {:ok, amount, decimals} + end + + # TODO: remove this clause along with token transfer denormalization + defp do_token_transfer_amount_for_api(%Token{type: "ERC-721"}, nil, _amount, _amounts, _token_ids) do + {:ok, :erc721_instance} + end + + defp do_token_transfer_amount_for_api(_token, "ERC-721", _amount, _amounts, _token_ids) do + {:ok, :erc721_instance} + end + + # TODO: remove this clause along with token transfer denormalization + defp do_token_transfer_amount_for_api( + %Token{type: type, decimals: decimals}, + nil, + amount, + amounts, + token_ids + ) + when type in ["ERC-1155", "ERC-404"] do + if amount do + {:ok, :erc1155_erc404_instance, amount, decimals} + else + {:ok, :erc1155_erc404_instance, amounts, token_ids, decimals} + end + end + + defp do_token_transfer_amount_for_api( + %Token{decimals: decimals}, + type, + amount, + amounts, + token_ids + ) + when type in ["ERC-1155", "ERC-404"] do + if amount do + {:ok, :erc1155_erc404_instance, amount, decimals} + else + {:ok, :erc1155_erc404_instance, amounts, token_ids, decimals} + end + end + + defp do_token_transfer_amount_for_api(_token, _token_type, _amount, _amounts, _token_ids) do + nil + end + + @doc """ + Returns the token's symbol. + + When the token's symbol is nil, the function will return the contract address hash. + """ + def token_symbol(%Token{symbol: nil, contract_address_hash: address_hash}) do + AddressView.short_hash_left_right(address_hash) + end + + def token_symbol(%Token{symbol: symbol}) do + symbol + end + + @doc """ + Returns the token's name. + + When the token's name is nil, the function will return the contract address hash. + """ + def token_name(%Token{} = token), do: build_token_name(token) + def token_name(%Address.Token{} = address_token), do: build_token_name(address_token) + + defp build_token_name(%{name: nil, contract_address_hash: address_hash}) do + AddressView.short_hash_left_right(address_hash) + end + + defp build_token_name(%{name: name}) do + name + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/tokens/holder_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/tokens/holder_view.ex new file mode 100644 index 0000000..e916a69 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/tokens/holder_view.ex @@ -0,0 +1,86 @@ +defmodule BlockScoutWeb.Tokens.HolderView do + use BlockScoutWeb, :view + + alias BlockScoutWeb.Tokens.OverviewView + alias Explorer.Chain.{Address, Token} + + @doc """ + Checks if the total supply percentage must be shown. + + ## Examples + + iex> BlockScoutWeb.Tokens.HolderView.show_total_supply_percentage?(nil) + false + + iex> BlockScoutWeb.Tokens.HolderView.show_total_supply_percentage?(0) + false + + iex> BlockScoutWeb.Tokens.HolderView.show_total_supply_percentage?(100) + true + + """ + def show_total_supply_percentage?(nil), do: false + def show_total_supply_percentage?(total_supply), do: total_supply > 0 + + @doc """ + Calculates the percentage of the value from the given total supply. + + ## Examples + + iex> value = Decimal.new(200) + iex> total_supply = Decimal.new(1000) + iex> BlockScoutWeb.Tokens.HolderView.total_supply_percentage(value, total_supply) + "20.0000%" + + """ + def total_supply_percentage(_, 0), do: "N/A%" + + def total_supply_percentage(_, %Decimal{coef: 0}), do: "N/A%" + + def total_supply_percentage(value, total_supply) do + result = + value + |> Decimal.div(total_supply) + |> Decimal.mult(100) + |> Decimal.round(4) + |> Decimal.to_string() + + result <> "%" + end + + @doc """ + Formats the token balance value according to the Token's type. + + ## Examples + + iex> token = build(:token, type: "ERC-20", decimals: Decimal.new(2)) + iex> BlockScoutWeb.Tokens.HolderView.format_token_balance_value(100000, nil, token) + "1,000" + + iex> token = build(:token, type: "ERC-721") + iex> BlockScoutWeb.Tokens.HolderView.format_token_balance_value(1, nil, token) + 1 + + """ + def format_token_balance_value(value, _id, %Token{type: "ERC-20", decimals: decimals}) do + format_according_to_decimals(value, decimals) + end + + def format_token_balance_value(value, id, %Token{type: "ERC-1155", decimals: decimals}) do + to_string(format_according_to_decimals(value, decimals)) <> " TokenID " <> to_string(id) + end + + def format_token_balance_value(value, id, %Token{type: "ERC-404", decimals: decimals}) do + base = to_string(format_according_to_decimals(value, decimals)) + + if id do + base <> " TokenID " <> to_string(id) + else + base + end + end + + def format_token_balance_value(value, _id, _token) do + value + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/tokens/instance/holder_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/tokens/instance/holder_view.ex new file mode 100644 index 0000000..38cf207 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/tokens/instance/holder_view.ex @@ -0,0 +1,5 @@ +defmodule BlockScoutWeb.Tokens.Instance.HolderView do + use BlockScoutWeb, :view + + alias BlockScoutWeb.Tokens.Instance.OverviewView +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/tokens/instance/metadata_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/tokens/instance/metadata_view.ex new file mode 100644 index 0000000..758e6a3 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/tokens/instance/metadata_view.ex @@ -0,0 +1,9 @@ +defmodule BlockScoutWeb.Tokens.Instance.MetadataView do + use BlockScoutWeb, :view + + alias BlockScoutWeb.Tokens.Instance.OverviewView + + def format_metadata(nil), do: "" + + def format_metadata(metadata), do: Poison.encode!(metadata, %{pretty: true}) +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/tokens/instance/overview_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/tokens/instance/overview_view.ex new file mode 100644 index 0000000..717353e --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/tokens/instance/overview_view.ex @@ -0,0 +1,76 @@ +defmodule BlockScoutWeb.Tokens.Instance.OverviewView do + use BlockScoutWeb, :view + + alias BlockScoutWeb.NFTHelper + alias Explorer.Chain + alias Explorer.Chain.{Address, CurrencyHelper, SmartContract, Token} + alias Explorer.SmartContract.Helper + alias Utils.TokenInstanceHelper + + import BlockScoutWeb.APIDocsView, only: [blockscout_url: 1] + import BlockScoutWeb.NFTHelper, only: [external_url: 1] + + @tabs ["token-transfers", "metadata"] + @stub_image "/images/controller.svg" + + def token_name?(%Token{name: nil}), do: false + def token_name?(%Token{name: _}), do: true + + def decimals?(%Token{decimals: nil}), do: false + def decimals?(%Token{decimals: _}), do: true + + def total_supply?(%Token{total_supply: nil}), do: false + def total_supply?(%Token{total_supply: _}), do: true + + def media_src(instance, high_quality_media? \\ nil) + def media_src(nil, _), do: @stub_image + + def media_src(instance, high_quality_media?) do + NFTHelper.get_media_src(instance.metadata, high_quality_media?) || media_src(nil) + end + + def media_type(media_src) do + case TokenInstanceHelper.media_type(media_src) do + {type, _} -> + type + + other -> + other + end + end + + def total_supply_usd(token) do + tokens = CurrencyHelper.divide_decimals(token.total_supply, token.decimals) + price = token.fiat_value + Decimal.mult(tokens, price) + end + + def smart_contract_with_read_only_functions?( + %Token{contract_address: %Address{smart_contract: %SmartContract{}}} = token + ) do + Enum.any?(token.contract_address.smart_contract.abi || [], &Helper.queryable_method?(&1)) + end + + def smart_contract_with_read_only_functions?(%Token{contract_address: %Address{smart_contract: nil}}), do: false + + def qr_code(conn, token_id, hash) do + token_instance_path = token_instance_path(conn, :show, to_string(hash), to_string(token_id)) + + url_prefix = blockscout_url(false) + + url = Path.join(url_prefix, token_instance_path) + + url + |> QRCode.to_png() + |> Base.encode64() + end + + def current_tab_name(request_path) do + @tabs + |> Enum.filter(&tab_active?(&1, request_path)) + |> tab_name() + end + + defp tab_name(["token-transfers"]), do: gettext("Token Transfers") + defp tab_name(["metadata"]), do: gettext("Metadata") +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/tokens/instance/transfer_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/tokens/instance/transfer_view.ex new file mode 100644 index 0000000..2cf314e --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/tokens/instance/transfer_view.ex @@ -0,0 +1,5 @@ +defmodule BlockScoutWeb.Tokens.Instance.TransferView do + use BlockScoutWeb, :view + + alias BlockScoutWeb.Tokens.Instance.OverviewView +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/tokens/instance_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/tokens/instance_view.ex new file mode 100644 index 0000000..c18c5b5 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/tokens/instance_view.ex @@ -0,0 +1,3 @@ +defmodule BlockScoutWeb.Tokens.InstanceView do + use BlockScoutWeb, :view +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/tokens/inventory_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/tokens/inventory_view.ex new file mode 100644 index 0000000..547d6dd --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/tokens/inventory_view.ex @@ -0,0 +1,8 @@ +defmodule BlockScoutWeb.Tokens.InventoryView do + use BlockScoutWeb, :view + + import BlockScoutWeb.Tokens.Instance.OverviewView, only: [media_src: 1, media_type: 1] + + alias BlockScoutWeb.Tokens.OverviewView + alias Explorer.Chain +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/tokens/overview_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/tokens/overview_view.ex new file mode 100644 index 0000000..d9dbe6e --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/tokens/overview_view.ex @@ -0,0 +1,89 @@ +defmodule BlockScoutWeb.Tokens.OverviewView do + use BlockScoutWeb, :view + + alias BlockScoutWeb.{AccessHelper, LayoutView} + alias Explorer.{Chain, CustomContractsHelper} + alias Explorer.Chain.{Address, CurrencyHelper, SmartContract, Token} + alias Explorer.Chain.SmartContract.Proxy + alias Explorer.SmartContract.{Helper, Writer} + + import BlockScoutWeb.AddressView, only: [from_address_hash: 1, contract_interaction_disabled?: 0] + + @tabs ["token-transfers", "token-holders", "read-contract", "inventory"] + + def decimals?(%Token{decimals: nil}), do: false + def decimals?(%Token{decimals: _}), do: true + + def token_name?(%Token{name: nil}), do: false + def token_name?(%Token{name: _}), do: true + + def total_supply?(%Token{total_supply: nil}), do: false + def total_supply?(%Token{total_supply: _}), do: true + + @doc """ + Get the current tab name/title from the request path and possible tab names. + + The tabs on mobile are represented by a dropdown list, which has a title. This title is the + currently selected tab name. This function returns that name, properly gettext'ed. + + The list of possible tab names for this page is represented by the attribute @tab. + + Raises error if there is no match, so a developer of a new tab must include it in the list. + """ + def current_tab_name(request_path) do + @tabs + |> Enum.filter(&tab_active?(&1, request_path)) + |> tab_name() + end + + defp tab_name(["token-transfers"]), do: gettext("Token Transfers") + defp tab_name(["token-holders"]), do: gettext("Token Holders") + defp tab_name(["read-contract"]), do: gettext("Read Contract") + defp tab_name(["inventory"]), do: gettext("Inventory") + + def display_inventory?(%Token{type: "ERC-721"}), do: true + def display_inventory?(%Token{type: "ERC-1155"}), do: true + def display_inventory?(%Token{type: "ERC-404"}), do: true + def display_inventory?(_), do: false + + def smart_contract_with_read_only_functions?( + %Token{contract_address: %Address{smart_contract: %SmartContract{}}} = token + ) do + Enum.any?(token.contract_address.smart_contract.abi || [], &Helper.queryable_method?(&1)) + end + + def smart_contract_with_read_only_functions?(%Token{contract_address: %Address{smart_contract: nil}}), do: false + + def token_smart_contract_is_proxy?(%Token{ + contract_address: %Address{smart_contract: %SmartContract{} = smart_contract} + }) do + Proxy.proxy_contract?(smart_contract) + end + + def token_smart_contract_is_proxy?(%Token{contract_address: %Address{smart_contract: nil}}), do: false + + def smart_contract_with_write_functions?(%Token{ + contract_address: %Address{smart_contract: %SmartContract{}} = address + }) do + !contract_interaction_disabled?() && + Enum.any?( + address.smart_contract.abi || [], + &Writer.write_function?(&1) + ) + end + + def smart_contract_with_write_functions?(%Token{contract_address: %Address{smart_contract: nil}}), do: false + + @doc """ + Get the total value of the token supply in USD. + """ + def total_supply_usd(token) do + if Map.has_key?(token, :custom_cap) && token.custom_cap do + token.custom_cap + else + tokens = CurrencyHelper.divide_decimals(token.total_supply, token.decimals) + price = token.fiat_value + Decimal.mult(tokens, price) + end + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/tokens/transfer_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/tokens/transfer_view.ex new file mode 100644 index 0000000..3ea5d84 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/tokens/transfer_view.ex @@ -0,0 +1,7 @@ +defmodule BlockScoutWeb.Tokens.TransferView do + use BlockScoutWeb, :view + + alias BlockScoutWeb.Tokens.OverviewView + alias Explorer.Chain + alias Explorer.Chain.Address +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/tokens_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/tokens_view.ex new file mode 100644 index 0000000..704d7a9 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/tokens_view.ex @@ -0,0 +1,22 @@ +defmodule BlockScoutWeb.TokensView do + use BlockScoutWeb, :view + + alias Explorer.Chain.{Address, Token} + + def decimals?(%Token{decimals: nil}), do: false + def decimals?(%Token{decimals: _}), do: true + + def token_display_name(%Token{name: nil, symbol: nil}), do: "" + + def token_display_name(%Token{name: "", symbol: ""}), do: "" + + def token_display_name(%Token{name: name, symbol: nil}), do: name + + def token_display_name(%Token{name: name, symbol: ""}), do: name + + def token_display_name(%Token{name: nil, symbol: symbol}), do: symbol + + def token_display_name(%Token{name: "", symbol: symbol}), do: symbol + + def token_display_name(%Token{name: name, symbol: symbol}), do: "#{name} (#{symbol})" +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/transaction_internal_transaction_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/transaction_internal_transaction_view.ex new file mode 100644 index 0000000..74ff604 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/transaction_internal_transaction_view.ex @@ -0,0 +1,4 @@ +defmodule BlockScoutWeb.TransactionInternalTransactionView do + use BlockScoutWeb, :view + @dialyzer :no_match +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/transaction_log_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/transaction_log_view.ex new file mode 100644 index 0000000..1a7357a --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/transaction_log_view.ex @@ -0,0 +1,7 @@ +defmodule BlockScoutWeb.TransactionLogView do + use BlockScoutWeb, :view + @dialyzer :no_match + + alias Explorer.Chain.SmartContract.Proxy.Models.Implementation + import BlockScoutWeb.AddressView, only: [decode: 2, primary_name: 1] +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/transaction_raw_trace_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/transaction_raw_trace_view.ex new file mode 100644 index 0000000..09fd461 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/transaction_raw_trace_view.ex @@ -0,0 +1,15 @@ +defmodule BlockScoutWeb.TransactionRawTraceView do + use BlockScoutWeb, :view + @dialyzer :no_match + + def render("scripts.html", %{conn: conn}) do + render_scripts(conn, "raw-trace/code_highlighting.js") + end + + def raw_traces_with_lines(raw_traces) do + raw_traces + |> Jason.encode!(pretty: true) + |> String.split("\n") + |> Enum.with_index(1) + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/transaction_state_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/transaction_state_view.ex new file mode 100644 index 0000000..88fc209 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/transaction_state_view.ex @@ -0,0 +1,40 @@ +defmodule BlockScoutWeb.TransactionStateView do + use BlockScoutWeb, :view + + alias Explorer.Chain + alias Explorer.Chain.{Address, Wei} + + import Explorer.Chain.Transaction.StateChange, only: [from_loss: 1, has_diff?: 1, to_profit: 1] + + def not_negative?(%Wei{value: val}) do + not Decimal.negative?(val) + end + + def not_negative?(val) do + not Decimal.negative?(val) + end + + def absolute_value_of(%Wei{value: val}) do + %Wei{value: Decimal.abs(val)} + end + + def absolute_value_of(val) do + Decimal.abs(val) + end + + def has_state_changes?(transaction) do + has_diff?(from_loss(transaction)) or has_diff?(to_profit(transaction)) + end + + def display_value(balance, :coin, _token_id) do + format_wei_value(balance, :ether) + end + + def display_value(balance, token_transfer, token_id) do + render("_token_balance.html", transfer: token_transfer, balance: balance, token_id: token_id) + end + + def display_erc_721(token_transfer) do + render(BlockScoutWeb.TransactionView, "_total_transfers.html", transfer: token_transfer) + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/transaction_token_transfer_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/transaction_token_transfer_view.ex new file mode 100644 index 0000000..3e31c0f --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/transaction_token_transfer_view.ex @@ -0,0 +1,6 @@ +defmodule BlockScoutWeb.TransactionTokenTransferView do + use BlockScoutWeb, :view + + alias Explorer.Chain + alias Explorer.Chain.Address +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/transaction_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/transaction_view.ex new file mode 100644 index 0000000..f5dcb2c --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/transaction_view.ex @@ -0,0 +1,628 @@ +defmodule BlockScoutWeb.TransactionView do + use BlockScoutWeb, :view + + alias BlockScoutWeb.{AccessHelper, AddressView, BlockView, TabHelper} + alias BlockScoutWeb.Account.AuthController + alias BlockScoutWeb.Cldr.Number + alias Explorer.{Chain, CustomContractsHelper, Repo} + alias Explorer.Chain.Block.Reward + alias Explorer.Chain.{Address, Block, InternalTransaction, Transaction, Wei} + alias Explorer.Chain.Cache.Counters.AverageBlockTime + alias Explorer.Market.Token + alias Timex.Duration + + use Gettext, backend: BlockScoutWeb.Gettext + import BlockScoutWeb.AddressView, only: [from_address_hash: 1, short_token_id: 2, tag_name_to_label: 1] + import BlockScoutWeb.Tokens.Helper + + @tabs ["token-transfers", "internal-transactions", "logs", "raw-trace", "state"] + + @token_burning_title "Token Burning" + @token_minting_title "Token Minting" + @token_transfer_title "Token Transfer" + @token_creation_title "Token Creation" + + @token_burning_type :token_burning + @token_minting_type :token_minting + @token_creation_type :token_spawning + @token_transfer_type :token_transfer + + defguardp is_transaction_type(mod) when mod in [InternalTransaction, Transaction] + + defdelegate formatted_timestamp(block), to: BlockView + + def block_number(%Transaction{block_number: nil}), do: gettext("Block Pending") + + def block_number(%Transaction{block_number: number, block_hash: hash}), + do: [view_module: BlockView, partial: "_link.html", block: %Block{number: number, hash: hash}] + + def block_number(%Reward{block: block}), do: [view_module: BlockView, partial: "_link.html", block: block] + + def block_timestamp(%Transaction{} = transaction), do: Transaction.block_timestamp(transaction) + def block_timestamp(%Reward{block: %Block{timestamp: time}}), do: time + + def value_transfer?(%Transaction{input: %{bytes: bytes}}) when bytes in [<<>>, nil] do + true + end + + def value_transfer?(_), do: false + + def token_transfer_type(transaction) do + transaction_with_transfers = Repo.preload(transaction, token_transfers: :token) + + token_transfers_filtered_by_block_hash = + transaction_with_transfers + |> Map.get(:token_transfers, []) + |> Enum.filter(fn token_transfer -> + token_transfer.block_hash == transaction.block_hash + end) + + transaction_with_transfers_filtered = + Map.put(transaction_with_transfers, :token_transfers, token_transfers_filtered_by_block_hash) + + type = Chain.transaction_token_transfer_type(transaction) + if type, do: {type, transaction_with_transfers_filtered}, else: {nil, transaction_with_transfers_filtered} + end + + def transaction_actions(transaction) do + Repo.preload(transaction, :transaction_actions) + end + + def aggregate_token_transfers(token_transfers) do + %{ + transfers: {ft_transfers, nft_transfers}, + mintings: {ft_mintings, nft_mintings}, + burnings: {ft_burnings, nft_burnings}, + creations: {ft_creations, nft_creations} + } = + token_transfers + |> Enum.reduce( + %{ + transfers: {[], []}, + mintings: {[], []}, + burnings: {[], []}, + creations: {[], []} + }, + fn token_transfer, acc -> + token_transfer_type = Chain.get_token_transfer_type(token_transfer) + + case token_transfer_type do + :token_transfer -> + transfers = aggregate_reducer(token_transfer, acc.transfers) + + %{ + transfers: transfers, + mintings: acc.mintings, + burnings: acc.burnings, + creations: acc.creations + } + + :token_burning -> + burnings = aggregate_reducer(token_transfer, acc.burnings) + + %{ + transfers: acc.transfers, + mintings: acc.mintings, + burnings: burnings, + creations: acc.creations + } + + :token_minting -> + mintings = aggregate_reducer(token_transfer, acc.mintings) + + %{ + transfers: acc.transfers, + mintings: mintings, + burnings: acc.burnings, + creations: acc.creations + } + + :token_spawning -> + creations = aggregate_reducer(token_transfer, acc.creations) + + %{ + transfers: acc.transfers, + mintings: acc.mintings, + burnings: acc.burnings, + creations: creations + } + end + end + ) + + transfers = ft_transfers ++ nft_transfers + + mintings = ft_mintings ++ nft_mintings + + burnings = ft_burnings ++ nft_burnings + + creations = ft_creations ++ nft_creations + + %{transfers: transfers, mintings: mintings, burnings: burnings, creations: creations} + end + + defp aggregate_reducer(%{amount: amount, amounts: amounts} = token_transfer, {acc1, acc2}) + when is_nil(amount) and is_nil(amounts) do + new_entry = %{ + token: token_transfer.token, + amount: nil, + amounts: [], + token_ids: token_transfer.token_ids, + token_type: token_transfer.token_type, + to_address_hash: token_transfer.to_address_hash, + from_address_hash: token_transfer.from_address_hash + } + + {acc1, [new_entry | acc2]} + end + + defp aggregate_reducer(%{amount: amount, amounts: amounts} = token_transfer, {acc1, acc2}) + when is_nil(amount) and not is_nil(amounts) do + new_entry = %{ + token: token_transfer.token, + amount: nil, + amounts: amounts, + token_ids: token_transfer.token_ids, + token_type: token_transfer.token_type, + to_address_hash: token_transfer.to_address_hash, + from_address_hash: token_transfer.from_address_hash + } + + {acc1, [new_entry | acc2]} + end + + defp aggregate_reducer(token_transfer, {acc1, acc2}) do + new_entry = %{ + token: token_transfer.token, + amount: token_transfer.amount, + amounts: [], + token_ids: token_transfer.token_ids, + token_type: token_transfer.token_type, + to_address_hash: token_transfer.to_address_hash, + from_address_hash: token_transfer.from_address_hash + } + + existing_entry = + acc1 + |> Enum.find(fn entry -> + entry.to_address_hash == token_transfer.to_address_hash && + entry.from_address_hash == token_transfer.from_address_hash && + entry.token == token_transfer.token + end) + + new_acc1 = + if existing_entry do + acc1 + |> Enum.map(fn entry -> + process_entry(entry, new_entry, token_transfer) + end) + else + [new_entry | acc1] + end + + {new_acc1, acc2} + end + + def process_entry(entry, new_entry, token_transfer) do + if entry.to_address_hash == token_transfer.to_address_hash && + entry.from_address_hash == token_transfer.from_address_hash && + entry.token == token_transfer.token do + updated_entry = %{ + entry + | amount: Decimal.add(new_entry.amount, entry.amount) + } + + updated_entry + else + entry + end + end + + def token_type_name(type) do + case type do + :erc20 -> gettext("ERC-20 ") + :erc721 -> gettext("ERC-721 ") + :erc1155 -> gettext("ERC-1155 ") + :erc404 -> gettext("ERC-404 ") + _ -> "" + end + end + + def processing_time_duration(%Transaction{block: nil}) do + :pending + end + + def processing_time_duration(%Transaction{earliest_processing_start: nil}) do + avg_time = AverageBlockTime.average_block_time() + + if avg_time == {:error, :disabled} do + :unknown + else + avg_time_in_secs = + avg_time + |> Duration.to_seconds() + + {:ok, "<= #{avg_time_in_secs} seconds"} + end + end + + def processing_time_duration(%Transaction{ + block: %Block{timestamp: end_time}, + earliest_processing_start: earliest_processing_start, + inserted_at: inserted_at + }) do + with {:ok, long_interval} <- humanized_diff(earliest_processing_start, end_time), + {:ok, short_interval} <- humanized_diff(inserted_at, end_time) do + {:ok, merge_intervals(short_interval, long_interval)} + else + _ -> + :ignore + end + end + + defp merge_intervals(short, long) when short == long, do: short + + defp merge_intervals(short, long) do + [short_time, short_unit] = String.split(short, " ") + [long_time, long_unit] = String.split(long, " ") + + if short_unit == long_unit do + short_time <> "-" <> long_time <> " " <> short_unit + else + short <> " - " <> long + end + end + + defp humanized_diff(left, right) do + left + |> Timex.diff(right, :milliseconds) + |> Duration.from_milliseconds() + |> Timex.format_duration(Explorer.Chain.Cache.Counters.Helper.AverageBlockTimeDurationFormat) + |> case do + {:error, _} = error -> error + duration -> {:ok, duration} + end + end + + def confirmations(%Transaction{block: block}, named_arguments) when is_list(named_arguments) do + case block do + %Block{consensus: true} -> + {:ok, confirmations} = Chain.confirmations(block, named_arguments) + Number.to_string!(confirmations, format: "#,###") + + _ -> + 0 + end + end + + def confirmations_ds_name(blocks_amount_str) do + case Integer.parse(blocks_amount_str) do + {blocks_amount, ""} -> + if rem(blocks_amount, 10) == 1 do + "block" + else + "blocks" + end + + _ -> + "" + end + end + + def contract_creation?(%Transaction{to_address: nil}), do: true + + def contract_creation?(_), do: false + + def fee(%Transaction{} = transaction) do + {_, value} = Transaction.fee(transaction, :wei) + value + end + + def format_gas_limit(gas) do + Number.to_string!(gas) + end + + def formatted_fee(%Transaction{} = transaction, opts) do + transaction + |> Transaction.fee(:wei) + |> fee_to_denomination(opts) + |> case do + {:actual, value} -> value + {:maximum, value} -> "#{gettext("Max of")} #{value}" + end + end + + def formatted_action_amount(data, field_name) do + data + |> Map.get(field_name) + |> Decimal.new() + |> Number.to_string!(format: "#,##0.##################") + end + + def transaction_action_string_to_address(address) do + case Chain.string_to_address_hash(address) do + {:ok, address_hash} -> Chain.hash_to_address(address_hash) + _ -> {:error, nil} + end + end + + def transaction_status(transaction) do + Chain.transaction_to_status(transaction) + end + + def transaction_revert_reason(transaction, options \\ []) do + transaction |> Chain.transaction_to_revert_reason() |> decoded_revert_reason(transaction, options) + end + + def get_pure_transaction_revert_reason(nil), do: nil + + def get_pure_transaction_revert_reason(transaction), do: Chain.transaction_to_revert_reason(transaction) + + def empty_exchange_rate?(exchange_rate) do + Token.null?(exchange_rate) + end + + def formatted_status(status) do + case status do + :pending -> gettext("Unconfirmed") + _ -> gettext("Confirmed") + end + end + + def formatted_result(status) do + case status do + :pending -> gettext("Pending") + :awaiting_internal_transactions -> gettext("(Awaiting internal transactions for status)") + :success -> gettext("Success") + {:error, :awaiting_internal_transactions} -> gettext("Error: (Awaiting internal transactions for reason)") + # The pool of possible error reasons is unknown or even if it is enumerable, so we can't translate them + {:error, reason} when is_binary(reason) -> gettext("Error: %{reason}", reason: reason) + end + end + + def from_or_to_address?(_token_transfer, nil), do: false + + def from_or_to_address?(%{from_address_hash: from_hash, to_address_hash: to_hash}, %Address{hash: hash}) do + from_hash == hash || to_hash == hash + end + + def gas(%type{gas: gas}) when is_transaction_type(type) do + Number.to_string!(gas) + end + + def skip_decoding?(transaction) do + contract_creation?(transaction) || value_transfer?(transaction) + end + + def decoded_input_data(transaction) do + Transaction.decoded_input_data(transaction, []) + end + + def decoded_revert_reason(revert_reason, transaction, options) do + Transaction.decoded_revert_reason(transaction, revert_reason, options) + end + + @doc """ + Converts a transaction's gas price to a displayable value. + """ + def gas_price(%Transaction{gas_price: gas_price}, unit) when unit in ~w(wei gwei ether)a do + format_wei_value(gas_price, unit) + end + + def l1_gas_price(transaction, unit) when unit in ~w(wei gwei ether)a do + case Map.get(transaction, :l1_gas_price) do + nil -> nil + value -> format_wei_value(value, unit) + end + end + + def gas_used(%Transaction{gas_used: nil}), do: gettext("Pending") + + def gas_used(%Transaction{gas_used: gas_used}) do + Number.to_string!(gas_used) + end + + def l1_gas_used(transaction) do + case Map.get(transaction, :l1_gas_used) do + nil -> gettext("Pending") + value -> Number.to_string!(value) + end + end + + def gas_used_perc(%Transaction{gas_used: nil}), do: nil + + def gas_used_perc(%Transaction{gas_used: gas_used, gas: gas}) do + if Decimal.compare(gas, 0) == :gt do + gas_used + |> Decimal.div(gas) + |> Decimal.mult(100) + |> Decimal.round(2) + |> Number.to_string!() + else + nil + end + end + + def hash(%Transaction{hash: hash}) do + to_string(hash) + end + + def involves_contract?(%Transaction{from_address: from_address, to_address: to_address}) do + Address.smart_contract?(from_address) || Address.smart_contract?(to_address) + end + + def involves_token_transfers?(%Transaction{token_transfers: []}), do: false + def involves_token_transfers?(%Transaction{token_transfers: transfers}) when is_list(transfers), do: true + def involves_token_transfers?(_), do: false + + def qr_code(%Transaction{hash: hash}) do + hash + |> to_string() + |> QRCode.to_png() + |> Base.encode64() + end + + def status_class(transaction) do + case Chain.transaction_to_status(transaction) do + :pending -> "tile-status--pending" + :awaiting_internal_transactions -> "tile-status--awaiting-internal-transactions" + :success -> "tile-status--success" + {:error, :awaiting_internal_transactions} -> "tile-status--error--awaiting-internal-transactions" + {:error, reason} when is_binary(reason) -> "tile-status--error--reason" + end + end + + # This is the address to be shown in the to field + def to_address_hash(%Transaction{to_address_hash: nil, created_contract_address_hash: address_hash}), + do: address_hash + + def to_address_hash(%Transaction{to_address_hash: address_hash}), do: address_hash + + def transaction_display_type(%Transaction{} = transaction) do + cond do + involves_token_transfers?(transaction) -> + token_transfer_type = get_transaction_type_from_token_transfers(transaction.token_transfers) + + case token_transfer_type do + @token_minting_type -> gettext(@token_minting_title) + @token_burning_type -> gettext(@token_burning_title) + @token_creation_type -> gettext(@token_creation_title) + @token_transfer_type -> gettext(@token_transfer_title) + end + + contract_creation?(transaction) -> + gettext("Contract Creation") + + involves_contract?(transaction) -> + gettext("Contract Call") + + true -> + gettext("Transaction") + end + end + + def type_suffix(%Transaction{} = transaction) do + cond do + involves_token_transfers?(transaction) -> "token-transfer" + contract_creation?(transaction) -> "contract-creation" + involves_contract?(transaction) -> "contract-call" + true -> "transaction" + end + end + + @doc """ + Converts a transaction's Wei value to Ether and returns a formatted display value. + + ## Options + + * `:include_label` - Boolean. Defaults to true. Flag for displaying unit with value. + """ + def value(%mod{value: value}, opts \\ []) when is_transaction_type(mod) do + include_label? = Keyword.get(opts, :include_label, true) + format_wei_value(value, :ether, include_unit_label: include_label?) + end + + def format_wei_value(value) do + format_wei_value(value, :ether, include_unit_label: false) + end + + defp fee_to_denomination({fee_type, fee}, opts) do + denomination = Keyword.get(opts, :denomination) + include_label? = Keyword.get(opts, :include_label, true) + {fee_type, format_wei_value(Wei.from(fee, :wei), denomination, include_unit_label: include_label?)} + end + + @doc """ + Get the current tab name/title from the request path and possible tab names. + + The tabs on mobile are represented by a dropdown list, which has a title. This title is the currently selected tab name. This function returns that name, properly gettext'ed. + + The list of possible tab names for this page is represented by the attribute @tab. + + Raises an error if there is no match, so a developer of a new tab must include it in the list. + + """ + def current_tab_name(request_path) do + @tabs + |> Enum.filter(&TabHelper.tab_active?(&1, request_path)) + |> tab_name() + end + + defp tab_name(["token-transfers"]), do: gettext("Token Transfers") + defp tab_name(["internal-transactions"]), do: gettext("Internal Transactions") + defp tab_name(["logs"]), do: gettext("Logs") + defp tab_name(["raw-trace"]), do: gettext("Raw Trace") + defp tab_name(["state"]), do: gettext("State changes") + + defp get_transaction_type_from_token_transfers(token_transfers) do + token_transfers_types = + token_transfers + |> Enum.map(fn token_transfer -> + Chain.get_token_transfer_type(token_transfer) + end) + + burnings_count = + Enum.count(token_transfers_types, fn token_transfers_type -> token_transfers_type == @token_burning_type end) + + mintings_count = + Enum.count(token_transfers_types, fn token_transfers_type -> token_transfers_type == @token_minting_type end) + + creations_count = + Enum.count(token_transfers_types, fn token_transfers_type -> token_transfers_type == @token_creation_type end) + + cond do + Enum.count(token_transfers_types) == burnings_count -> @token_burning_type + Enum.count(token_transfers_types) == mintings_count -> @token_minting_type + Enum.count(token_transfers_types) == creations_count -> @token_creation_type + true -> @token_transfer_type + end + end + + defp show_tenderly_link? do + Application.get_env(:block_scout_web, :show_tenderly_link) + end + + defp tenderly_chain_path do + System.get_env("TENDERLY_CHAIN_PATH") || "/" + end + + def get_max_length do + string_value = Application.get_env(:block_scout_web, :contract)[:max_length_to_show_string_without_trimming] + + case Integer.parse(string_value) do + {integer, ""} -> integer + _ -> 2040 + end + end + + def trim(length, string) do + %{show: String.slice(string, 0..length), hide: String.slice(string, (length + 1)..-1//1)} + end + + # Function decodes revert reason of the transaction + @spec decode_revert_reason_as_utf8(binary() | nil) :: binary() | nil + def decode_revert_reason_as_utf8(revert_reason) do + case revert_reason do + nil -> + nil + + "0x" <> hex_part -> + decode_hex_revert_reason_as_utf8(hex_part) + + hex_part -> + decode_hex_revert_reason_as_utf8(hex_part) + end + end + + # Function converts hex revert reason to the utf8 binary + @spec decode_hex_revert_reason_as_utf8(binary()) :: binary() + def decode_hex_revert_reason_as_utf8(hex_revert_reason) do + case Base.decode16(hex_revert_reason, case: :mixed) do + {:ok, revert_reason} -> + revert_reason + + _ -> + hex_revert_reason + end + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/verified_contracts_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/verified_contracts_view.ex new file mode 100644 index 0000000..50934a2 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/verified_contracts_view.ex @@ -0,0 +1,16 @@ +defmodule BlockScoutWeb.VerifiedContractsView do + use BlockScoutWeb, :view + + import BlockScoutWeb.AddressView, only: [balance: 1] + import BlockScoutWeb.Tokens.OverviewView, only: [total_supply_usd: 1] + alias BlockScoutWeb.Routers.WebRouter.Helpers + + def format_current_filter(filter) do + case filter do + "solidity" -> gettext("Solidity") + "vyper" -> gettext("Vyper") + "yul" -> gettext("Yul") + _ -> gettext("All") + end + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/visualize_sol2uml_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/visualize_sol2uml_view.ex new file mode 100644 index 0000000..827deee --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/visualize_sol2uml_view.ex @@ -0,0 +1,3 @@ +defmodule BlockScoutWeb.VisualizeSol2umlView do + use BlockScoutWeb, :view +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/wei_helper.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/wei_helper.ex new file mode 100644 index 0000000..188303e --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/wei_helper.ex @@ -0,0 +1,83 @@ +defmodule BlockScoutWeb.WeiHelper do + @moduledoc """ + Helper functions for interacting with `t:Explorer.Chain.Wei.t/0` values. + """ + + use Gettext, backend: BlockScoutWeb.Gettext + + alias BlockScoutWeb.CldrHelper + alias Explorer.Chain.Wei + + @valid_units ~w(wei gwei ether)a + + @type format_option :: {:include_unit_label, boolean()} + + @type format_options :: [format_option()] + + @doc """ + Converts a `t:Explorer.Wei.t/0` value to the specified unit including a + translated unit label. + + ## Supported Formatting Options + + The third argument allows for keyword options to be passed for formatting the + converted number. + + * `:include_unit_label` - Boolean (Defaults to `true`). Flag for if the unit + label should be included in the returned string + + ## Examples + + iex> format_wei_value(%Wei{value: Decimal.new(1)}, :wei) + "1 Wei" + + iex> format_wei_value(%Wei{value: Decimal.new(1, 10, 12)}, :gwei) + "10,000 Gwei" + + iex> format_wei_value(%Wei{value: Decimal.new(1, 10, 21)}, :ether) + "10,000 ETH" + + # With formatting options + + iex> format_wei_value( + ...> %Wei{value: Decimal.new(1000500000000000000)}, + ...> :ether + ...> ) + "1.0005 ETH" + + iex> format_wei_value( + ...> %Wei{value: Decimal.new(10)}, + ...> :wei, + ...> include_unit_label: false + ...> ) + "10" + """ + @spec format_wei_value(Wei.t() | nil, Wei.unit(), format_options()) :: String.t() | nil + def format_wei_value(_wei, _unit, _options \\ []) + + def format_wei_value(nil, _unit, _options), do: nil + + def format_wei_value(%Wei{} = wei, unit, options) when unit in @valid_units do + converted_value = + wei + |> Wei.to(unit) + + formatted_value = + if Decimal.compare(converted_value, 1_000_000_000_000) == :gt do + CldrHelper.Number.to_string!(converted_value, format: "0.###E+0") + else + CldrHelper.Number.to_string!(converted_value, format: "#,##0.##################") + end + + if Keyword.get(options, :include_unit_label, true) do + display_unit = display_unit(unit) + "#{formatted_value} #{display_unit}" + else + formatted_value + end + end + + defp display_unit(:wei), do: gettext("Wei") + defp display_unit(:gwei), do: gettext("Gwei") + defp display_unit(:ether), do: Explorer.coin_name() +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/withdrawal_view.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/withdrawal_view.ex new file mode 100644 index 0000000..296304d --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/block_scout_web/views/withdrawal_view.ex @@ -0,0 +1,5 @@ +defmodule BlockScoutWeb.WithdrawalView do + use BlockScoutWeb, :view + + alias Explorer.Chain.Address +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/phoenix/html/safe.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/phoenix/html/safe.ex new file mode 100644 index 0000000..cddc3d5 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/phoenix/html/safe.ex @@ -0,0 +1,32 @@ +alias Explorer.Chain +alias Explorer.Chain.{Address, Block, Data, Hash, Transaction} + +defimpl Phoenix.HTML.Safe, for: Address do + def to_iodata(%@for{} = address) do + @for.checksum(address, true) + end +end + +defimpl Phoenix.HTML.Safe, for: Transaction do + def to_iodata(%@for{hash: hash}) do + @protocol.to_iodata(hash) + end +end + +defimpl Phoenix.HTML.Safe, for: Block do + def to_iodata(%@for{number: number}) do + @protocol.to_iodata(number) + end +end + +defimpl Phoenix.HTML.Safe, for: Data do + def to_iodata(data) do + Chain.data_to_iodata(data) + end +end + +defimpl Phoenix.HTML.Safe, for: Hash do + def to_iodata(hash) do + Chain.hash_to_iodata(hash) + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/phoenix/param.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/phoenix/param.ex new file mode 100644 index 0000000..f9c138e --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/lib/phoenix/param.ex @@ -0,0 +1,35 @@ +alias Explorer.Chain.{Address, Block, Hash, Transaction} + +defimpl Phoenix.Param, for: Transaction do + def to_param(%@for{hash: hash}) do + @protocol.to_param(hash) + end +end + +defimpl Phoenix.Param, for: Address do + def to_param(%@for{} = address) do + @for.checksum(address) + end +end + +defimpl Phoenix.Param, for: Block do + def to_param(%@for{consensus: true, number: number}) do + to_string(number) + end + + def to_param(%@for{consensus: false, hash: hash}) do + to_string(hash) + end +end + +defimpl Phoenix.Param, for: Hash do + def to_param(hash) do + to_string(hash) + end +end + +defimpl Phoenix.Param, for: Decimal do + def to_param(decimal) do + to_string(decimal) + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/mix.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/mix.exs new file mode 100644 index 0000000..c43b97e --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/mix.exs @@ -0,0 +1,194 @@ +defmodule BlockScoutWeb.Mixfile do + use Mix.Project + + def project do + [ + aliases: aliases(), + app: :block_scout_web, + build_path: "../../_build", + config_path: "../../config/config.exs", + deps: deps(), + deps_path: "../../deps", + description: "Web interface for BlockScout.", + dialyzer: [ + plt_add_deps: :app_tree, + ignore_warnings: "../../.dialyzer_ignore.exs" + ], + elixir: "~> 1.17", + elixirc_paths: elixirc_paths(Mix.env(), Application.get_env(:block_scout_web, :disable_api?)), + lockfile: "../../mix.lock", + package: package(), + preferred_cli_env: [ + credo: :test, + dialyzer: :test + ], + start_permanent: Mix.env() == :prod, + version: "8.0.2", + xref: [ + exclude: [ + Explorer.Chain.PolygonZkevm.Reader, + Explorer.Chain.Beacon.Reader, + Explorer.Chain.Cache.OptimismFinalizationPeriod, + Explorer.Chain.Optimism.OutputRoot, + Explorer.Chain.Optimism.WithdrawalEvent, + Explorer.Chain.ZkSync.Reader, + Explorer.Chain.Arbitrum.Reader + ] + ] + ] + end + + # Configuration for the OTP application. + # + # Type `mix help compile.app` for more information. + def application do + [ + mod: {BlockScoutWeb.Application, []}, + extra_applications: extra_applications() + ] + end + + # Specifies which paths to compile per environment. + defp elixirc_paths(:test, _), do: ["test/support", "test/block_scout_web/features/pages"] ++ elixirc_paths() + + defp elixirc_paths(_, true), + do: [ + "lib/phoenix", + "lib/block_scout_web.ex", + "lib/block_scout_web/application.ex", + "lib/block_scout_web/endpoint.ex", + "lib/block_scout_web/health_router.ex", + "lib/block_scout_web/controllers/api/health_controller.ex", + "lib/block_scout_web/prometheus/exporter.ex" + ] + + defp elixirc_paths(_, _), do: elixirc_paths() + defp elixirc_paths, do: ["lib"] + + defp extra_applications, + do: [ + :ueberauth_auth0, + :logger, + :runtime_tools + ] + + # Specifies your project dependencies. + # + # Type `mix help deps` for examples and options. + defp deps do + [ + # GraphQL toolkit + {:absinthe, "~> 1.5"}, + # Integrates Absinthe subscriptions with Phoenix + {:absinthe_phoenix, "~> 2.0.0"}, + # Plug support for Absinthe + {:absinthe_plug, git: "https://github.com/blockscout/absinthe_plug.git", tag: "1.5.8", override: true}, + # Absinthe support for the Relay framework + {:absinthe_relay, "~> 1.5"}, + {:bypass, "~> 2.1", only: :test}, + # To add (CORS)(https://www.w3.org/TR/cors/) + {:cors_plug, "~> 3.0"}, + # For Absinthe to load data in batches + {:dataloader, "~> 2.0.0"}, + {:dialyxir, "~> 1.1", only: [:dev, :test], runtime: false}, + # Need until https://github.com/absinthe-graphql/absinthe_relay/pull/125 is released, then can be removed + # The current `absinthe_relay` is compatible though as shown from that PR + {:ecto, "~> 3.3", override: true}, + {:ex_cldr, "~> 2.38"}, + {:ex_cldr_numbers, "~> 2.33"}, + {:ex_cldr_units, "~> 3.17"}, + {:ex_keccak, "~> 0.7.5"}, + {:cldr_utils, "~> 2.3"}, + {:ex_machina, "~> 2.1", only: [:test]}, + {:explorer, in_umbrella: true}, + {:exvcr, "~> 0.10", only: :test}, + {:file_info, "~> 0.0.4"}, + # HTML CSS selectors for Phoenix controller tests + {:floki, "~> 0.31"}, + {:flow, "~> 1.2"}, + {:gettext, "~> 0.26.1"}, + {:hammer, "~> 6.0"}, + {:httpoison, "~> 2.0"}, + {:indexer, in_umbrella: true, runtime: false}, + # JSON parser and generator + {:jason, "~> 1.3"}, + {:junit_formatter, ">= 0.0.0", only: [:test], runtime: false}, + # Log errors and application output to separate files + {:logger_file_backend, "~> 0.0.10"}, + {:math, "~> 0.7.0"}, + {:mock, "~> 0.3.0", only: [:test], runtime: false}, + {:number, "~> 1.0.1"}, + {:phoenix, "== 1.5.14"}, + {:phoenix_ecto, "~> 4.1"}, + {:phoenix_html, "== 3.3.4"}, + {:phoenix_live_reload, "~> 1.2", only: [:dev]}, + {:phoenix_live_view, "~> 0.17"}, + {:phoenix_pubsub, "~> 2.0"}, + {:prometheus_ex, git: "https://github.com/lanodan/prometheus.ex", branch: "fix/elixir-1.14", override: true}, + # use `:cowboy` for WebServer with `:plug` + {:plug_cowboy, "~> 2.2"}, + # Waiting for the Pretty Print to be implemented at the Jason lib + # https://github.com/michalmuskala/jason/issues/15 + {:poison, "~> 4.0.1"}, + {:postgrex, ">= 0.0.0"}, + # For compatibility with `prometheus_process_collector`, which hasn't been updated yet + {:prometheus, "~> 4.0", override: true}, + # Gather methods for Phoenix requests + {:prometheus_phoenix, "~> 1.2"}, + # Expose metrics from URL Prometheus server can scrape + {:prometheus_plugs, "~> 1.1"}, + # OS process metrics for Prometheus, custom ref to include https://github.com/deadtrickster/prometheus_process_collector/pull/30 + {:prometheus_process_collector, + git: "https://github.com/Phybbit/prometheus_process_collector.git", + ref: "3dc94dcff422d7b9cbd7ddf6bf2a896446705f3f", + override: true}, + {:remote_ip, "~> 1.0"}, + {:qrcode, "~> 0.1.0"}, + {:sobelow, ">= 0.7.0", only: [:dev, :test], runtime: false}, + # Tracing + {:spandex, "~> 3.0"}, + # `:spandex` integration with Datadog + {:spandex_datadog, "~> 1.0"}, + # `:spandex` tracing of `:phoenix` + {:spandex_phoenix, "~> 1.0"}, + {:timex, "~> 3.7.1"}, + {:wallaby, "~> 0.30", only: :test, runtime: false}, + # `:cowboy` `~> 2.0` and Phoenix 1.4 compatibility + {:ex_json_schema, "~> 0.10.1"}, + {:ueberauth, "~> 0.7"}, + {:ueberauth_auth0, "~> 2.0"}, + {:utils, in_umbrella: true}, + {:bureaucrat, "~> 0.2.9", only: :test}, + {:logger_json, "~> 5.1"} + ] + end + + # Aliases are shortcuts or tasks specific to the current project. + # For example, to create, migrate and run the seeds file at once: + # + # $ mix ecto.setup + # + # See the documentation for `Mix` for more info on aliases. + defp aliases do + [ + compile: "compile --warnings-as-errors", + "ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"], + "ecto.reset": ["ecto.drop", "ecto.setup"], + test: [ + "ecto.create --quiet", + "ecto.migrate", + # to match behavior of `mix test` from project root, which needs to not start applications for `indexer` to + # prevent its supervision tree from starting, which is undesirable in test + "test --no-start" + ] + ] + end + + defp package do + [ + maintainers: ["Blockscout"], + licenses: ["GPL 3.0"], + links: %{"GitHub" => "https://github.com/blockscout/blockscout"} + ] + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/priv/gettext/default.pot b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/priv/gettext/default.pot new file mode 100644 index 0000000..7d24722 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/priv/gettext/default.pot @@ -0,0 +1,3736 @@ +## This file is a PO Template file. +## +## "msgid"s here are often extracted from source code. +## Add new messages manually only if they're dynamic +## messages that can't be statically extracted. +## +## Run "mix gettext.extract" to bring this file up to +## date. Leave "msgstr"s empty as changing them here has no +## effect: edit them in PO (.po) files instead. +# +msgid "" +msgstr "" + +#: lib/block_scout_web/templates/common_components/_minimal_proxy_pattern.html.eex:9 +#, elixir-autogen, elixir-format +msgid " - minimal bytecode implementation that delegates all calls to a known address" +msgstr "" + +#: lib/block_scout_web/templates/api_docs/eth_rpc.html.eex:14 +#, elixir-autogen, elixir-format +msgid " is recommended." +msgstr "" + +#: lib/block_scout_web/templates/address/_metatags.html.eex:3 +#, elixir-autogen, elixir-format +msgid "%{address} - %{subnetwork} Explorer" +msgstr "" + +#: lib/block_scout_web/templates/block/overview.html.eex:12 +#, elixir-autogen, elixir-format +msgid "%{block_type} Details" +msgstr "" + +#: lib/block_scout_web/templates/block/overview.html.eex:55 +#, elixir-autogen, elixir-format +msgid "%{block_type} Height" +msgstr "" + +#: lib/block_scout_web/templates/block/index.html.eex:7 +#, elixir-autogen, elixir-format +msgid "%{block_type}s" +msgstr "" + +#: lib/block_scout_web/templates/block/overview.html.eex:85 +#, elixir-autogen, elixir-format +msgid "%{count} Transaction" +msgstr "" + +#: lib/block_scout_web/templates/block/overview.html.eex:87 +#: lib/block_scout_web/templates/chain/_block.html.eex:11 +#, elixir-autogen, elixir-format +msgid "%{count} Transactions" +msgstr "" + +#: lib/block_scout_web/views/address_token_balance_view.ex:10 +#, elixir-autogen, elixir-format +msgid "%{count} token" +msgid_plural "%{count} tokens" +msgstr[0] "" +msgstr[1] "" + +#: lib/block_scout_web/templates/block/_tile.html.eex:29 +#, elixir-autogen, elixir-format +msgid "%{count} transaction" +msgid_plural "%{count} transactions" +msgstr[0] "" +msgstr[1] "" + +#: lib/block_scout_web/templates/transaction/_actions.html.eex:101 +#, elixir-autogen, elixir-format +msgid "%{qty} of Token ID [%{link_to_id}]" +msgstr "" + +#: lib/block_scout_web/templates/chain/_metatags.html.eex:2 +#, elixir-autogen, elixir-format +msgid "%{subnetwork} %{network} Explorer" +msgstr "" + +#: lib/block_scout_web/templates/layout/_default_title.html.eex:2 +#, elixir-autogen, elixir-format +msgid "%{subnetwork} Explorer - BlockScout" +msgstr "" + +#: lib/block_scout_web/templates/withdrawal/index.html.eex:11 +#, elixir-autogen, elixir-format +msgid "%{withdrawals_count} withdrawals processed and %{withdrawals_sum} withdrawn." +msgstr "" + +#: lib/block_scout_web/views/transaction_view.ex:375 +#, elixir-autogen, elixir-format +msgid "(Awaiting internal transactions for status)" +msgstr "" + +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:59 +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:70 +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:82 +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:104 +#, elixir-autogen, elixir-format +msgid "(query)" +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification_common_fields/_library_first.html.eex:4 +#, elixir-autogen, elixir-format +msgid ") may be added for each contract. Click the Add Library button to add an additional one." +msgstr "" + +#: lib/block_scout_web/templates/layout/app.html.eex:93 +#, elixir-autogen, elixir-format +msgid "- We're indexing this chain right now. Some of the counts may be inaccurate." +msgstr "" + +#: lib/block_scout_web/templates/transaction/not_found.html.eex:8 +#, elixir-autogen, elixir-format +msgid "1. If you have just submitted this transaction please wait for at least 30 seconds before refreshing this page." +msgstr "" + +#: lib/block_scout_web/templates/transaction/not_found.html.eex:9 +#, elixir-autogen, elixir-format +msgid "2. It could still be in the TX Pool of a different node, waiting to be broadcasted." +msgstr "" + +#: lib/block_scout_web/templates/transaction/not_found.html.eex:10 +#, elixir-autogen, elixir-format +msgid "3. During times when the network is busy (i.e during ICOs) it can take a while for your transaction to propagate through the network and for us to index it." +msgstr "" + +#: lib/block_scout_web/templates/transaction/not_found.html.eex:11 +#, elixir-autogen, elixir-format +msgid "4. If it still does not show up after 1 hour, please check with your sender/exchange/wallet/transaction provider for additional information." +msgstr "" + +#: lib/block_scout_web/templates/block/overview.html.eex:197 +#, elixir-autogen, elixir-format +msgid "64-bit hash of value verifying proof-of-work (note: null for POA chains)." +msgstr "" + +#: lib/block_scout_web/templates/block/overview.html.eex:97 +#: lib/block_scout_web/templates/transaction_state/_state_change.html.eex:21 +#, elixir-autogen, elixir-format +msgid "A block producer who successfully included the block onto the blockchain." +msgstr "" + +#: lib/block_scout_web/templates/layout/app.html.eex:100 +#, elixir-autogen, elixir-format +msgid "A confirmation email was sent to" +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification_common_fields/_library_first.html.eex:4 +#, elixir-autogen, elixir-format +msgid "A library name called in the .sol file. Multiple libraries (up to " +msgstr "" + +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:73 +#, elixir-autogen, elixir-format +msgid "A string with the name of the action to be invoked." +msgstr "" + +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:62 +#, elixir-autogen, elixir-format +msgid "A string with the name of the module to be invoked." +msgstr "" + +#: lib/block_scout_web/templates/account/custom_abi/form.html.eex:24 +#, elixir-autogen, elixir-format +msgid "ABI" +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification_common_fields/_constructor_args.html.eex:3 +#, elixir-autogen, elixir-format +msgid "ABI-encoded Constructor Arguments (if required by the contract)" +msgstr "" + +#: lib/block_scout_web/templates/api_docs/index.html.eex:4 +#, elixir-autogen, elixir-format +msgid "API Documentation" +msgstr "" + +#: lib/block_scout_web/templates/api_docs/_metatags.html.eex:4 +#, elixir-autogen, elixir-format +msgid "API endpoints for the %{subnetwork}" +msgstr "" + +#: lib/block_scout_web/templates/api_docs/_metatags.html.eex:2 +#, elixir-autogen, elixir-format +msgid "API for the %{subnetwork} - BlockScout" +msgstr "" + +#: lib/block_scout_web/templates/account/api_key/form.html.eex:7 +#: lib/block_scout_web/templates/account/api_key/form.html.eex:13 +#: lib/block_scout_web/templates/account/api_key/form.html.eex:14 +#: lib/block_scout_web/templates/account/api_key/index.html.eex:29 +#, elixir-autogen, elixir-format +msgid "API key" +msgstr "" + +#: lib/block_scout_web/templates/account/api_key/index.html.eex:7 +#: lib/block_scout_web/templates/account/common/_nav.html.eex:16 +#: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:18 +#, elixir-autogen, elixir-format +msgid "API keys" +msgstr "" + +#: lib/block_scout_web/templates/layout/_topnav.html.eex:106 +#, elixir-autogen, elixir-format +msgid "APIs" +msgstr "" + +#: lib/block_scout_web/templates/account/tag_address/index.html.eex:24 +#: lib/block_scout_web/templates/account/tag_transaction/index.html.eex:24 +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:69 +#, elixir-autogen, elixir-format +msgid "Action" +msgstr "" + +#: lib/block_scout_web/templates/account/watchlist/show.html.eex:25 +#, elixir-autogen, elixir-format +msgid "Actions" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:484 +#, elixir-autogen, elixir-format +msgid "Actual gas amount used by the transaction on L2." +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:488 +#, elixir-autogen, elixir-format +msgid "Actual gas amount used by the transaction." +msgstr "" + +#: lib/block_scout_web/templates/account/api_key/form.html.eex:7 +#: lib/block_scout_web/templates/account/custom_abi/form.html.eex:8 +#: lib/block_scout_web/templates/layout/_add_chain_to_mm.html.eex:10 +#, elixir-autogen, elixir-format +msgid "Add" +msgstr "" + +#: lib/block_scout_web/templates/account/api_key/index.html.eex:44 +#, elixir-autogen, elixir-format +msgid "Add API key" +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:86 +#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:76 +#, elixir-autogen, elixir-format +msgid "Add Contract Libraries" +msgstr "" + +#: lib/block_scout_web/templates/account/custom_abi/index.html.eex:44 +#, elixir-autogen, elixir-format +msgid "Add Custom ABI" +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:97 +#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:87 +#, elixir-autogen, elixir-format +msgid "Add Library" +msgstr "" + +#: lib/block_scout_web/templates/account/watchlist/show.html.eex:38 +#, elixir-autogen, elixir-format +msgid "Add address" +msgstr "" + +#: lib/block_scout_web/templates/account/tag_address/form.html.eex:7 +#: lib/block_scout_web/templates/account/tag_address/index.html.eex:37 +#, elixir-autogen, elixir-format +msgid "Add address tag" +msgstr "" + +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:7 +#, elixir-autogen, elixir-format +msgid "Add address to the Watch list" +msgstr "" + +#: lib/block_scout_web/templates/account/tag_transaction/form.html.eex:7 +#: lib/block_scout_web/templates/account/tag_transaction/index.html.eex:37 +#, elixir-autogen, elixir-format +msgid "Add transaction tag" +msgstr "" + +#: lib/block_scout_web/templates/account/tag_address/form.html.eex:11 +#: lib/block_scout_web/templates/account/tag_address/index.html.eex:23 +#: lib/block_scout_web/templates/account/watchlist/show.html.eex:23 +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:12 +#: lib/block_scout_web/templates/address/_validator_metadata_modal.html.eex:16 +#: lib/block_scout_web/templates/address_contract_verification_common_fields/_library_address.html.eex:4 +#: lib/block_scout_web/templates/tokens/index.html.eex:34 +#: lib/block_scout_web/templates/transaction_log/_logs.html.eex:29 +#: lib/block_scout_web/templates/transaction_state/index.html.eex:34 +#: lib/block_scout_web/templates/verified_contracts/index.html.eex:60 +#: lib/block_scout_web/views/address_view.ex:109 +#, elixir-autogen, elixir-format +msgid "Address" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:263 +#, elixir-autogen, elixir-format +msgid "Address (external or contract) receiving the transaction." +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:245 +#, elixir-autogen, elixir-format +msgid "Address (external or contract) sending the transaction." +msgstr "" + +#: lib/block_scout_web/templates/account/common/_nav.html.eex:10 +#: lib/block_scout_web/templates/account/tag_address/index.html.eex:7 +#: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:16 +#, elixir-autogen, elixir-format +msgid "Address Tags" +msgstr "" + +#: lib/block_scout_web/templates/address/overview.html.eex:151 +#, elixir-autogen, elixir-format +msgid "Address balance in" +msgstr "" + +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:51 +#, elixir-autogen, elixir-format +msgid "Address of the token contract" +msgstr "" + +#: lib/block_scout_web/templates/transaction_state/_state_change.html.eex:7 +#, elixir-autogen, elixir-format +msgid "Address used in token mintings and burnings." +msgstr "" + +#: lib/block_scout_web/templates/account/public_tags_request/address_field.html.eex:2 +#, elixir-autogen, elixir-format +msgid "Address*" +msgstr "" + +#: lib/block_scout_web/templates/address/index.html.eex:5 +#, elixir-autogen, elixir-format +msgid "Addresses" +msgstr "" + +#: lib/block_scout_web/templates/address_withdrawal/index.html.eex:38 +#: lib/block_scout_web/templates/withdrawal/index.html.eex:35 +#, elixir-autogen, elixir-format +msgid "Age" +msgstr "" + +#: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:26 +#: lib/block_scout_web/templates/address_token_transfer/index.html.eex:28 +#: lib/block_scout_web/templates/address_transaction/index.html.eex:22 +#: lib/block_scout_web/templates/layout/_topnav.html.eex:88 +#: lib/block_scout_web/templates/verified_contracts/index.html.eex:20 +#: lib/block_scout_web/views/address_internal_transaction_view.ex:12 +#: lib/block_scout_web/views/address_token_transfer_view.ex:12 +#: lib/block_scout_web/views/address_transaction_view.ex:12 +#: lib/block_scout_web/views/verified_contracts_view.ex:13 +#, elixir-autogen, elixir-format +msgid "All" +msgstr "" + +#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:13 +#, elixir-autogen, elixir-format +msgid "All functions displayed below are from ABI of that contract. In order to verify current contract, proceed with" +msgstr "" + +#: lib/block_scout_web/templates/address_contract/index.html.eex:27 +#, elixir-autogen, elixir-format +msgid "All metadata displayed below is from that contract. In order to verify current contract, click" +msgstr "" + +#: lib/block_scout_web/templates/address/overview.html.eex:176 +#, elixir-autogen, elixir-format +msgid "All tokens in the account and total value." +msgstr "" + +#: lib/block_scout_web/templates/address_withdrawal/index.html.eex:41 +#: lib/block_scout_web/templates/block_withdrawal/index.html.eex:32 +#: lib/block_scout_web/templates/withdrawal/index.html.eex:38 +#, elixir-autogen, elixir-format +msgid "Amount" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:469 +#, elixir-autogen, elixir-format +msgid "Amount of" +msgstr "" + +#: lib/block_scout_web/templates/block/overview.html.eex:238 +#, elixir-autogen, elixir-format +msgid "Amount of distributed reward. Miners receive a static block reward + Tx fees + uncle fees." +msgstr "" + +#: lib/block_scout_web/templates/internal_server_error/index.html.eex:8 +#, elixir-autogen, elixir-format +msgid "An unexpected error has occurred. Try reloading the page, or come back soon and try again." +msgstr "" + +#: lib/block_scout_web/templates/api_docs/eth_rpc.html.eex:15 +#, elixir-autogen, elixir-format +msgid "Anything not in this list is not supported. Click on the method to be taken to the documentation for that method, and check the notes section for any potential differences." +msgstr "" + +#: lib/block_scout_web/templates/layout/_topnav.html.eex:134 +#, elixir-autogen, elixir-format +msgid "Apps" +msgstr "" + +#: lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex:21 +#, elixir-autogen, elixir-format +msgid "Average" +msgstr "" + +#: lib/block_scout_web/templates/chain/show.html.eex:102 +#, elixir-autogen, elixir-format +msgid "Average block time" +msgstr "" + +#: lib/block_scout_web/templates/account/api_key/form.html.eex:25 +#, elixir-autogen, elixir-format +msgid "Back to API keys (Cancel)" +msgstr "" + +#: lib/block_scout_web/templates/account/tag_address/form.html.eex:24 +#, elixir-autogen, elixir-format +msgid "Back to Address Tags (Cancel)" +msgstr "" + +#: lib/block_scout_web/templates/account/custom_abi/form.html.eex:30 +#, elixir-autogen, elixir-format +msgid "Back to Custom ABI (Cancel)" +msgstr "" + +#: lib/block_scout_web/templates/account/tag_transaction/form.html.eex:24 +#, elixir-autogen, elixir-format +msgid "Back to Transaction Tags (Cancel)" +msgstr "" + +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:81 +#, elixir-autogen, elixir-format +msgid "Back to Watch list (Cancel)" +msgstr "" + +#: lib/block_scout_web/templates/error422/index.html.eex:9 +#: lib/block_scout_web/templates/internal_server_error/index.html.eex:9 +#: lib/block_scout_web/templates/page_not_found/index.html.eex:9 +#: lib/block_scout_web/templates/transaction/not_found.html.eex:13 +#, elixir-autogen, elixir-format +msgid "Back to home" +msgstr "" + +#: lib/block_scout_web/templates/account/watchlist/show.html.eex:24 +#: lib/block_scout_web/templates/address/overview.html.eex:152 +#: lib/block_scout_web/templates/address_token/overview.html.eex:51 +#: lib/block_scout_web/templates/verified_contracts/index.html.eex:63 +#, elixir-autogen, elixir-format +msgid "Balance" +msgstr "" + +#: lib/block_scout_web/templates/transaction_state/index.html.eex:40 +#, elixir-autogen, elixir-format +msgid "Balance after" +msgstr "" + +#: lib/block_scout_web/templates/transaction_state/index.html.eex:37 +#, elixir-autogen, elixir-format +msgid "Balance before" +msgstr "" + +#: lib/block_scout_web/templates/address_coin_balance/index.html.eex:14 +#, elixir-autogen, elixir-format +msgid "Balances" +msgstr "" + +#: lib/block_scout_web/templates/block/overview.html.eex:209 +#, elixir-autogen, elixir-format +msgid "Base Fee per Gas" +msgstr "" + +#: lib/block_scout_web/templates/api_docs/eth_rpc.html.eex:5 +#: lib/block_scout_web/templates/api_docs/index.html.eex:5 +#, elixir-autogen, elixir-format +msgid "Base URL:" +msgstr "" + +#: lib/block_scout_web/templates/withdrawal/_metatags.html.eex:2 +#, elixir-autogen, elixir-format +msgid "Beacon chain withdrawals - %{subnetwork} Explorer" +msgstr "" + +#: lib/block_scout_web/templates/withdrawal/_metatags.html.eex:7 +#, elixir-autogen, elixir-format +msgid "Beacon chain, Withdrawals, %{subnetwork}, %{coin}" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:545 +#, elixir-autogen, elixir-format +msgid "Binary data included with the transaction. See input / logs below for additional info." +msgstr "" + +#: lib/block_scout_web/templates/address_coin_balance/_coin_balances.html.eex:8 +#: lib/block_scout_web/templates/address_withdrawal/index.html.eex:35 +#: lib/block_scout_web/templates/block/overview.html.eex:29 +#: lib/block_scout_web/templates/transaction/overview.html.eex:161 +#: lib/block_scout_web/templates/withdrawal/index.html.eex:29 +#, elixir-autogen, elixir-format +msgid "Block" +msgstr "" + +#: lib/block_scout_web/templates/block/_link.html.eex:2 +#: lib/block_scout_web/templates/internal_transaction/_tile.html.eex:32 +#: lib/block_scout_web/templates/tokens/transfer/_token_transfer.html.eex:43 +#, elixir-autogen, elixir-format +msgid "Block #%{number}" +msgstr "" + +#: lib/block_scout_web/templates/block/_metatags.html.eex:3 +#, elixir-autogen, elixir-format +msgid "Block %{block_number} - %{subnetwork} Explorer" +msgstr "" + +#: lib/block_scout_web/templates/block_transaction/404.html.eex:7 +#, elixir-autogen, elixir-format +msgid "Block Details" +msgstr "" + +#: lib/block_scout_web/templates/block/overview.html.eex:53 +#, elixir-autogen, elixir-format +msgid "Block Height" +msgstr "" + +#: lib/block_scout_web/templates/layout/app.html.eex:47 +#, elixir-autogen, elixir-format +msgid "Block Mined, awaiting import..." +msgstr "" + +#: lib/block_scout_web/views/transaction_view.ex:34 +#, elixir-autogen, elixir-format +msgid "Block Pending" +msgstr "" + +#: lib/block_scout_web/templates/block/overview.html.eex:158 +#, elixir-autogen, elixir-format +msgid "Block difficulty for miner, used to calibrate block generation time (Note: constant in POA based networks)." +msgstr "" + +#: lib/block_scout_web/views/block_transaction_view.ex:15 +#, elixir-autogen, elixir-format +msgid "Block not found, please try again later." +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:203 +#, elixir-autogen, elixir-format +msgid "Block number containing the transaction on L1." +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:160 +#, elixir-autogen, elixir-format +msgid "Block number containing the transaction." +msgstr "" + +#: lib/block_scout_web/templates/address/overview.html.eex:259 +#, elixir-autogen, elixir-format +msgid "Block number in which the address was updated." +msgstr "" + +#: lib/block_scout_web/templates/chain/_metatags.html.eex:4 +#, elixir-autogen, elixir-format +msgid "BlockScout provides analytics data, API, and Smart Contract tools for the %{subnetwork}" +msgstr "" + +#: lib/block_scout_web/templates/layout/_topnav.html.eex:29 +#, elixir-autogen, elixir-format +msgid "Blockchain" +msgstr "" + +#: lib/block_scout_web/templates/chain/show.html.eex:156 +#: lib/block_scout_web/templates/layout/_topnav.html.eex:34 +#: lib/block_scout_web/templates/layout/_topnav.html.eex:38 +#, elixir-autogen, elixir-format +msgid "Blocks" +msgstr "" + +#: lib/block_scout_web/templates/layout/app.html.eex:46 +#, elixir-autogen, elixir-format +msgid "Blocks Indexed" +msgstr "" + +#: lib/block_scout_web/templates/address/_tabs.html.eex:56 +#: lib/block_scout_web/templates/address/overview.html.eex:277 +#: lib/block_scout_web/templates/address_validation/index.html.eex:11 +#: lib/block_scout_web/views/address_view.ex:356 +#, elixir-autogen, elixir-format +msgid "Blocks Validated" +msgstr "" + +#: lib/block_scout_web/templates/layout/app.html.eex:48 +#, elixir-autogen, elixir-format +msgid "Blocks With Internal Transactions Indexed" +msgstr "" + +#: lib/block_scout_web/templates/layout/_footer.html.eex:22 +#, elixir-autogen, elixir-format +msgid "Blockscout is a tool for inspecting and analyzing EVM based blockchains. Blockchain explorer for Ethereum Networks." +msgstr "" + +#: lib/block_scout_web/templates/transaction_state/_state_change.html.eex:8 +#, elixir-autogen, elixir-format +msgid "Burn address" +msgstr "" + +#: lib/block_scout_web/templates/block/_tile.html.eex:64 +#: lib/block_scout_web/templates/block/overview.html.eex:218 +#, elixir-autogen, elixir-format +msgid "Burnt Fees" +msgstr "" + +#: lib/block_scout_web/templates/address_token/overview.html.eex:65 +#, elixir-autogen, elixir-format +msgid "CRC Worth" +msgstr "" + +#: lib/block_scout_web/templates/common_components/_csv_export_button.html.eex:4 +#, elixir-autogen, elixir-format +msgid "CSV" +msgstr "" + +#: lib/block_scout_web/templates/transaction/_decoded_input_body.html.eex:10 +#: lib/block_scout_web/views/internal_transaction_view.ex:21 +#, elixir-autogen, elixir-format +msgid "Call" +msgstr "" + +#: lib/block_scout_web/views/internal_transaction_view.ex:22 +#, elixir-autogen, elixir-format +msgid "Call Code" +msgstr "" + +#: lib/block_scout_web/templates/account/public_tags_request/form.html.eex:62 +#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:120 +#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:115 +#: lib/block_scout_web/templates/address_contract_verification_via_json/new.html.eex:41 +#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:107 +#: lib/block_scout_web/templates/address_contract_verification_via_standard_json_input/new.html.eex:55 +#: lib/block_scout_web/templates/address_contract_verification_vyper/new.html.eex:51 +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:47 +#: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:54 +#, elixir-autogen, elixir-format +msgid "Cancel" +msgstr "" + +#: lib/block_scout_web/templates/transaction_state/index.html.eex:43 +#, elixir-autogen, elixir-format +msgid "Change" +msgstr "" + +#: lib/block_scout_web/templates/layout/_footer.html.eex:43 +#, elixir-autogen, elixir-format +msgid "Chat (#blockscout)" +msgstr "" + +#: lib/block_scout_web/views/block_view.ex:65 +#, elixir-autogen, elixir-format +msgid "Chore Reward" +msgstr "" + +#: lib/block_scout_web/templates/tokens/index.html.eex:38 +#, elixir-autogen, elixir-format +msgid "Circulating Market Cap" +msgstr "" + +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:137 +#: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:106 +#, elixir-autogen, elixir-format +msgid "Clear" +msgstr "" + +#: lib/block_scout_web/templates/address/_validator_metadata_modal.html.eex:37 +#: lib/block_scout_web/templates/common_components/_modal_qr_code.html.eex:6 +#: lib/block_scout_web/templates/common_components/_modal_qr_code.html.eex:14 +#: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:84 +#: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:92 +#, elixir-autogen, elixir-format +msgid "Close" +msgstr "" + +#: lib/block_scout_web/templates/address/_tabs.html.eex:66 +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:165 +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:187 +#: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:126 +#: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:149 +#: lib/block_scout_web/views/address_view.ex:350 +#, elixir-autogen, elixir-format +msgid "Code" +msgstr "" + +#: lib/block_scout_web/templates/address/_tabs.html.eex:42 +#: lib/block_scout_web/views/address_view.ex:355 +#, elixir-autogen, elixir-format +msgid "Coin Balance History" +msgstr "" + +#: lib/block_scout_web/templates/transaction/_decoded_input_body.html.eex:54 +#, elixir-autogen, elixir-format +msgid "Collapse" +msgstr "" + +#: lib/block_scout_web/templates/account/public_tags_request/form.html.eex:20 +#, elixir-autogen, elixir-format +msgid "Company name" +msgstr "" + +#: lib/block_scout_web/templates/account/public_tags_request/form.html.eex:32 +#, elixir-autogen, elixir-format +msgid "Company website" +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification_common_fields/_compiler_field.html.eex:3 +#: lib/block_scout_web/templates/verified_contracts/index.html.eex:69 +#, elixir-autogen, elixir-format +msgid "Compiler" +msgstr "" + +#: lib/block_scout_web/templates/address_contract/index.html.eex:142 +#, elixir-autogen, elixir-format +msgid "Compiler Settings" +msgstr "" + +#: lib/block_scout_web/templates/address_contract/index.html.eex:71 +#, elixir-autogen, elixir-format +msgid "Compiler version" +msgstr "" + +#: lib/block_scout_web/views/transaction_view.ex:368 +#, elixir-autogen, elixir-format +msgid "Confirmed" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:127 +#, elixir-autogen, elixir-format +msgid "Confirmed by " +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:193 +#, elixir-autogen, elixir-format +msgid "Confirmed within" +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:2 +#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:6 +#: lib/block_scout_web/templates/address_contract_verification_via_json/new.html.eex:2 +#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:4 +#: lib/block_scout_web/templates/address_contract_verification_via_standard_json_input/new.html.eex:6 +#: lib/block_scout_web/templates/address_contract_verification_vyper/new.html.eex:4 +#: lib/block_scout_web/templates/tokens/holder/index.html.eex:16 +#, elixir-autogen, elixir-format +msgid "Connection Lost" +msgstr "" + +#: lib/block_scout_web/templates/address_coin_balance/index.html.eex:12 +#: lib/block_scout_web/templates/block/index.html.eex:5 +#, elixir-autogen, elixir-format +msgid "Connection Lost, click to load newer blocks" +msgstr "" + +#: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:15 +#, elixir-autogen, elixir-format +msgid "Connection Lost, click to load newer internal transactions" +msgstr "" + +#: lib/block_scout_web/templates/address_transaction/index.html.eex:11 +#: lib/block_scout_web/templates/pending_transaction/index.html.eex:16 +#: lib/block_scout_web/templates/transaction/index.html.eex:22 +#, elixir-autogen, elixir-format +msgid "Connection Lost, click to load newer transactions" +msgstr "" + +#: lib/block_scout_web/templates/address_validation/index.html.eex:10 +#, elixir-autogen, elixir-format +msgid "Connection Lost, click to load newer validations" +msgstr "" + +#: lib/block_scout_web/templates/address_contract/index.html.eex:96 +#, elixir-autogen, elixir-format +msgid "Constructor Arguments" +msgstr "" + +#: lib/block_scout_web/templates/verified_contracts/index.html.eex:78 +#, elixir-autogen, elixir-format +msgid "Constructor args" +msgstr "" + +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:52 +#: lib/block_scout_web/templates/transaction/overview.html.eex:273 +#, elixir-autogen, elixir-format +msgid "Contract" +msgstr "" + +#: lib/block_scout_web/templates/address_contract/index.html.eex:157 +#, elixir-autogen, elixir-format +msgid "Contract ABI" +msgstr "" + +#: lib/block_scout_web/templates/account/custom_abi/form.html.eex:18 +#: lib/block_scout_web/templates/account/custom_abi/index.html.eex:29 +#: lib/block_scout_web/templates/address_contract_verification_common_fields/_contract_address_field.html.eex:3 +#: lib/block_scout_web/views/address_view.ex:107 +#, elixir-autogen, elixir-format +msgid "Contract Address" +msgstr "" + +#: lib/block_scout_web/templates/transaction/_pending_tile.html.eex:16 +#: lib/block_scout_web/views/address_view.ex:47 +#: lib/block_scout_web/views/address_view.ex:81 +#, elixir-autogen, elixir-format +msgid "Contract Address Pending" +msgstr "" + +#: lib/block_scout_web/views/transaction_view.ex:497 +#, elixir-autogen, elixir-format +msgid "Contract Call" +msgstr "" + +#: lib/block_scout_web/views/transaction_view.ex:494 +#, elixir-autogen, elixir-format +msgid "Contract Creation" +msgstr "" + +#: lib/block_scout_web/templates/address_contract/index.html.eex:174 +#: lib/block_scout_web/templates/address_contract/index.html.eex:189 +#, elixir-autogen, elixir-format +msgid "Contract Creation Code" +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:90 +#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:80 +#, elixir-autogen, elixir-format +msgid "Contract Libraries" +msgstr "" + +#: lib/block_scout_web/templates/address/overview.html.eex:75 +#: lib/block_scout_web/templates/address_contract_verification_common_fields/_contract_name_field.html.eex:3 +#, elixir-autogen, elixir-format +msgid "Contract Name" +msgstr "" + +#: lib/block_scout_web/templates/address_contract/index.html.eex:25 +#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:11 +#, elixir-autogen, elixir-format +msgid "Contract is not verified. However, we found a verified contract with the same bytecode in Blockscout DB" +msgstr "" + +#: lib/block_scout_web/templates/verified_contracts/index.html.eex:47 +#, elixir-autogen, elixir-format +msgid "Contract name or address" +msgstr "" + +#: lib/block_scout_web/templates/address_contract/index.html.eex:63 +#, elixir-autogen, elixir-format +msgid "Contract name:" +msgstr "" + +#: lib/block_scout_web/templates/address_contract/index.html.eex:106 +#, elixir-autogen, elixir-format +msgid "Contract source code" +msgstr "" + +#: lib/block_scout_web/templates/address/overview.html.eex:120 +#, elixir-autogen, elixir-format +msgid "Contract was precompiled and created at genesis or contract creation transaction is missing" +msgstr "" + +#: lib/block_scout_web/templates/verified_contracts/_stats.html.eex:5 +#, elixir-autogen, elixir-format +msgid "Contracts" +msgstr "" + +#: lib/block_scout_web/templates/address_contract/index.html.eex:180 +#, elixir-autogen, elixir-format +msgid "Contracts that self destruct in their constructors have no contract code published and cannot be verified." +msgstr "" + +#: lib/block_scout_web/templates/layout/_footer.html.eex:42 +#, elixir-autogen, elixir-format +msgid "Contribute" +msgstr "" + +#: lib/block_scout_web/templates/address_contract/index.html.eex:159 +#, elixir-autogen, elixir-format +msgid "Copy ABI" +msgstr "" + +#: lib/block_scout_web/templates/account/api_key/row.html.eex:6 +#: lib/block_scout_web/templates/account/api_key/row.html.eex:6 +#, elixir-autogen, elixir-format +msgid "Copy API key" +msgstr "" + +#: lib/block_scout_web/templates/account/tag_address/row.html.eex:8 +#: lib/block_scout_web/templates/account/tag_address/row.html.eex:8 +#: lib/block_scout_web/templates/account/tag_transaction/row.html.eex:11 +#: lib/block_scout_web/templates/account/tag_transaction/row.html.eex:11 +#: lib/block_scout_web/templates/account/watchlist_address/row.html.eex:7 +#: lib/block_scout_web/templates/address/overview.html.eex:38 +#: lib/block_scout_web/templates/address/overview.html.eex:39 +#: lib/block_scout_web/templates/block/overview.html.eex:104 +#: lib/block_scout_web/templates/block/overview.html.eex:105 +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:43 +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:44 +#, elixir-autogen, elixir-format +msgid "Copy Address" +msgstr "" + +#: lib/block_scout_web/templates/address_contract/index.html.eex:144 +#, elixir-autogen, elixir-format +msgid "Copy Compiler Settings" +msgstr "" + +#: lib/block_scout_web/templates/account/custom_abi/row.html.eex:6 +#: lib/block_scout_web/templates/account/custom_abi/row.html.eex:6 +#, elixir-autogen, elixir-format +msgid "Copy Contract Address" +msgstr "" + +#: lib/block_scout_web/templates/address_contract/index.html.eex:176 +#: lib/block_scout_web/templates/address_contract/index.html.eex:192 +#, elixir-autogen, elixir-format +msgid "Copy Contract Creation Code" +msgstr "" + +#: lib/block_scout_web/templates/address_contract/index.html.eex:213 +#: lib/block_scout_web/templates/address_contract/index.html.eex:223 +#, elixir-autogen, elixir-format +msgid "Copy Deployed ByteCode" +msgstr "" + +#: lib/block_scout_web/templates/account/watchlist_address/row.html.eex:7 +#: lib/block_scout_web/templates/transaction/_total_transfers_from_to.html.eex:17 +#: lib/block_scout_web/templates/transaction/_total_transfers_from_to.html.eex:18 +#: lib/block_scout_web/templates/transaction/overview.html.eex:253 +#: lib/block_scout_web/templates/transaction/overview.html.eex:254 +#, elixir-autogen, elixir-format +msgid "Copy From Address" +msgstr "" + +#: lib/block_scout_web/templates/block/overview.html.eex:129 +#: lib/block_scout_web/templates/block/overview.html.eex:130 +#, elixir-autogen, elixir-format +msgid "Copy Hash" +msgstr "" + +#: lib/block_scout_web/templates/tokens/instance/metadata/index.html.eex:20 +#, elixir-autogen, elixir-format +msgid "Copy Metadata" +msgstr "" + +#: lib/block_scout_web/templates/block/overview.html.eex:149 +#: lib/block_scout_web/templates/block/overview.html.eex:150 +#, elixir-autogen, elixir-format +msgid "Copy Parent Hash" +msgstr "" + +#: lib/block_scout_web/templates/transaction_raw_trace/_card_body.html.eex:9 +#, elixir-autogen, elixir-format +msgid "Copy Raw Trace" +msgstr "" + +#: lib/block_scout_web/templates/address_contract/index.html.eex:120 +#: lib/block_scout_web/templates/address_contract/index.html.eex:132 +#, elixir-autogen, elixir-format +msgid "Copy Source Code" +msgstr "" + +#: lib/block_scout_web/templates/transaction/_total_transfers_from_to.html.eex:34 +#: lib/block_scout_web/templates/transaction/_total_transfers_from_to.html.eex:35 +#: lib/block_scout_web/templates/transaction/overview.html.eex:280 +#: lib/block_scout_web/templates/transaction/overview.html.eex:281 +#: lib/block_scout_web/templates/transaction/overview.html.eex:288 +#: lib/block_scout_web/templates/transaction/overview.html.eex:289 +#, elixir-autogen, elixir-format +msgid "Copy To Address" +msgstr "" + +#: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:32 +#: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:33 +#, elixir-autogen, elixir-format +msgid "Copy Token ID" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:87 +#, elixir-autogen, elixir-format +msgid "Copy Transaction Hash" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:88 +#, elixir-autogen, elixir-format +msgid "Copy Txn Hash" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:571 +#, elixir-autogen, elixir-format +msgid "Copy Txn Hex Input" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:577 +#, elixir-autogen, elixir-format +msgid "Copy Txn UTF-8 Input" +msgstr "" + +#: lib/block_scout_web/templates/log/_data_decoded_view.html.eex:20 +#: lib/block_scout_web/templates/transaction/_decoded_input_body.html.eex:41 +#: lib/block_scout_web/templates/transaction/overview.html.eex:570 +#: lib/block_scout_web/templates/transaction/overview.html.eex:576 +#: lib/block_scout_web/templates/transaction_raw_trace/_card_body.html.eex:8 +#, elixir-autogen, elixir-format +msgid "Copy Value" +msgstr "" + +#: lib/block_scout_web/views/internal_transaction_view.ex:26 +#, elixir-autogen, elixir-format +msgid "Create" +msgstr "" + +#: lib/block_scout_web/templates/account/custom_abi/index.html.eex:12 +#, elixir-autogen, elixir-format +msgid "Create a Custom ABI to interact with contracts." +msgstr "" + +#: lib/block_scout_web/templates/account/api_key/index.html.eex:12 +#, elixir-autogen, elixir-format +msgid "Create an API key to use with your RPC and EthRPC API requests." +msgstr "" + +#: lib/block_scout_web/views/internal_transaction_view.ex:27 +#, elixir-autogen, elixir-format +msgid "Create2" +msgstr "" + +#: lib/block_scout_web/templates/address/overview.html.eex:102 +#, elixir-autogen, elixir-format +msgid "Creator" +msgstr "" + +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:146 +#: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:116 +#, elixir-autogen, elixir-format +msgid "Curl" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:97 +#, elixir-autogen, elixir-format +msgid "Current transaction state: Success, Failed (Error), or Pending (In Process)" +msgstr "" + +#: lib/block_scout_web/templates/address_read_contract/index.html.eex:20 +#: lib/block_scout_web/templates/address_write_contract/index.html.eex:18 +#, elixir-autogen, elixir-format +msgid "Custom" +msgstr "" + +#: lib/block_scout_web/templates/account/common/_nav.html.eex:19 +#: lib/block_scout_web/templates/account/custom_abi/form.html.eex:8 +#: lib/block_scout_web/templates/account/custom_abi/index.html.eex:7 +#: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:19 +#, elixir-autogen, elixir-format +msgid "Custom ABI" +msgstr "" + +#: lib/block_scout_web/templates/address_read_contract/index.html.eex:25 +#: lib/block_scout_web/templates/address_write_contract/index.html.eex:23 +#, elixir-autogen, elixir-format +msgid "Custom ABI from account" +msgstr "" + +#: lib/block_scout_web/templates/chain/show.html.eex:70 +#, elixir-autogen, elixir-format +msgid "Daily Transactions" +msgstr "" + +#: lib/block_scout_web/templates/address_logs/_logs.html.eex:98 +#: lib/block_scout_web/templates/log/_data_decoded_view.html.eex:7 +#: lib/block_scout_web/templates/transaction/_decoded_input_body.html.eex:23 +#: lib/block_scout_web/templates/transaction_log/_logs.html.eex:130 +#, elixir-autogen, elixir-format +msgid "Data" +msgstr "" + +#: lib/block_scout_web/templates/block/overview.html.eex:70 +#, elixir-autogen, elixir-format +msgid "Date & time at which block was produced." +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:179 +#, elixir-autogen, elixir-format +msgid "Date & time of transaction inclusion, including length of time for confirmation." +msgstr "" + +#: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:52 +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:131 +#, elixir-autogen, elixir-format +msgid "Decimals" +msgstr "" + +#: lib/block_scout_web/templates/address_logs/_logs.html.eex:32 +#: lib/block_scout_web/templates/address_logs/_logs.html.eex:38 +#: lib/block_scout_web/templates/address_logs/_logs.html.eex:53 +#: lib/block_scout_web/templates/transaction_log/_logs.html.eex:43 +#: lib/block_scout_web/templates/transaction_log/_logs.html.eex:51 +#: lib/block_scout_web/templates/transaction_log/_logs.html.eex:66 +#: lib/block_scout_web/templates/transaction_log/_logs.html.eex:82 +#, elixir-autogen, elixir-format +msgid "Decoded" +msgstr "" + +#: lib/block_scout_web/views/internal_transaction_view.ex:23 +#, elixir-autogen, elixir-format +msgid "Delegate Call" +msgstr "" + +#: lib/block_scout_web/templates/address_contract/index.html.eex:211 +#: lib/block_scout_web/templates/address_contract/index.html.eex:219 +#, elixir-autogen, elixir-format +msgid "Deployed ByteCode" +msgstr "" + +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:53 +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:188 +#: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:60 +#: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:150 +#, elixir-autogen, elixir-format +msgid "Description" +msgstr "" + +#: lib/block_scout_web/templates/account/public_tags_request/form.html.eex:56 +#, elixir-autogen, elixir-format +msgid "Description*" +msgstr "" + +#: lib/block_scout_web/templates/address/overview.html.eex:30 +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:166 +#: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:127 +#, elixir-autogen, elixir-format +msgid "Details" +msgstr "" + +#: lib/block_scout_web/templates/block/overview.html.eex:159 +#, elixir-autogen, elixir-format +msgid "Difficulty" +msgstr "" + +#: lib/block_scout_web/templates/address_contract/index.html.eex:181 +#, elixir-autogen, elixir-format +msgid "Displaying the init data provided of the creating transaction." +msgstr "" + +#: lib/block_scout_web/templates/common_components/_csv_export_button.html.eex:4 +#: lib/block_scout_web/templates/csv_export/index.html.eex:33 +#, elixir-autogen, elixir-format +msgid "Download" +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:72 +#, elixir-autogen, elixir-format +msgid "Drop all Solidity contract source files into the drop zone." +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:72 +#, elixir-autogen, elixir-format +msgid "Drop all Solidity or Yul contract source files into the drop zone." +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification_via_json/new.html.eex:18 +#, elixir-autogen, elixir-format +msgid "Drop sources and metadata JSON file or click here" +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:67 +#, elixir-autogen, elixir-format +msgid "Drop sources or click here" +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification_via_standard_json_input/new.html.eex:28 +#, elixir-autogen, elixir-format +msgid "Drop the standard input JSON file or click here" +msgstr "" + +#: lib/block_scout_web/templates/account/public_tags_request/form.html.eex:27 +#, elixir-autogen, elixir-format +msgid "E-mail*" +msgstr "" + +#: lib/block_scout_web/templates/common_components/_minimal_proxy_pattern.html.eex:6 +#, elixir-autogen, elixir-format +msgid "EIP-1167" +msgstr "" + +#: lib/block_scout_web/views/transaction_view.ex:225 +#, elixir-autogen, elixir-format +msgid "ERC-1155 " +msgstr "" + +#: lib/block_scout_web/views/transaction_view.ex:223 +#, elixir-autogen, elixir-format +msgid "ERC-20 " +msgstr "" + +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:40 +#, elixir-autogen, elixir-format +msgid "ERC-20 tokens (beta)" +msgstr "" + +#: lib/block_scout_web/views/transaction_view.ex:226 +#, elixir-autogen, elixir-format +msgid "ERC-404 " +msgstr "" + +#: lib/block_scout_web/views/transaction_view.ex:224 +#, elixir-autogen, elixir-format +msgid "ERC-721 " +msgstr "" + +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:53 +#, elixir-autogen, elixir-format +msgid "ERC-721, ERC-1155 tokens (NFT) (beta)" +msgstr "" + +#: lib/block_scout_web/templates/api_docs/eth_rpc.html.eex:4 +#, elixir-autogen, elixir-format +msgid "ETH RPC API Documentation" +msgstr "" + +#: lib/block_scout_web/templates/address_contract/index.html.eex:82 +#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:30 +#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:22 +#, elixir-autogen, elixir-format +msgid "EVM Version" +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:34 +#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:26 +#, elixir-autogen, elixir-format +msgid "EVM version details" +msgstr "" + +#: lib/block_scout_web/views/block_transaction_view.ex:7 +#, elixir-autogen, elixir-format +msgid "Easy Cowboy! This block does not exist yet!" +msgstr "" + +#: lib/block_scout_web/templates/account/api_key/row.html.eex:16 +#: lib/block_scout_web/templates/account/custom_abi/row.html.eex:16 +#: lib/block_scout_web/templates/account/watchlist_address/row.html.eex:27 +#, elixir-autogen, elixir-format +msgid "Edit" +msgstr "" + +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:7 +#, elixir-autogen, elixir-format +msgid "Edit Watch list address" +msgstr "" + +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:71 +#, elixir-autogen, elixir-format +msgid "Email notifications" +msgstr "" + +#: lib/block_scout_web/templates/transaction/_emission_reward_tile.html.eex:5 +#, elixir-autogen, elixir-format +msgid "Emission Contract" +msgstr "" + +#: lib/block_scout_web/views/block_view.ex:73 +#, elixir-autogen, elixir-format +msgid "Emission Reward" +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:72 +#, elixir-autogen, elixir-format +msgid "Enter the Solidity Contract Code" +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification_vyper/new.html.eex:22 +#, elixir-autogen, elixir-format +msgid "Enter the Vyper Contract Code" +msgstr "" + +#: lib/block_scout_web/templates/internal_transaction/_tile.html.eex:11 +#: lib/block_scout_web/templates/transaction/_decoded_input_body.html.eex:10 +#, elixir-autogen, elixir-format +msgid "Error" +msgstr "" + +#: lib/block_scout_web/templates/transaction/_tile.html.eex:11 +#, elixir-autogen, elixir-format +msgid "Error in internal transactions" +msgstr "" + +#: lib/block_scout_web/templates/transaction/_decoded_input_body.html.eex:33 +#, elixir-autogen, elixir-format +msgid "Error rendering value" +msgstr "" + +#: lib/block_scout_web/templates/address/_balance_dropdown.html.eex:10 +#, elixir-autogen, elixir-format +msgid "Error trying to fetch balances." +msgstr "" + +#: lib/block_scout_web/views/transaction_view.ex:379 +#, elixir-autogen, elixir-format +msgid "Error: %{reason}" +msgstr "" + +#: lib/block_scout_web/views/transaction_view.ex:377 +#, elixir-autogen, elixir-format +msgid "Error: (Awaiting internal transactions for reason)" +msgstr "" + +#: lib/block_scout_web/templates/address/overview.html.eex:120 +#, elixir-autogen, elixir-format +msgid "Error: Could not determine contract creator." +msgstr "" + +#: lib/block_scout_web/templates/layout/_topnav.html.eex:120 +#, elixir-autogen, elixir-format +msgid "Eth RPC" +msgstr "" + +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:211 +#: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:164 +#, elixir-autogen, elixir-format +msgid "Example Value" +msgstr "" + +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:128 +#: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:99 +#, elixir-autogen, elixir-format +msgid "Execute" +msgstr "" + +#: lib/block_scout_web/templates/transaction/_decoded_input_body.html.eex:54 +#, elixir-autogen, elixir-format +msgid "Expand" +msgstr "" + +#: lib/block_scout_web/templates/csv_export/index.html.eex:14 +#, elixir-autogen, elixir-format +msgid "Export" +msgstr "" + +#: lib/block_scout_web/templates/csv_export/index.html.eex:10 +#, elixir-autogen, elixir-format +msgid "Export Data" +msgstr "" + +#: lib/block_scout_web/templates/address_contract/index.html.eex:248 +#, elixir-autogen, elixir-format +msgid "External libraries" +msgstr "" + +#: lib/block_scout_web/templates/transaction/_decoded_input.html.eex:40 +#, elixir-autogen, elixir-format +msgid "Failed to decode input data." +msgstr "" + +#: lib/block_scout_web/templates/address_logs/_logs.html.eex:35 +#: lib/block_scout_web/templates/transaction_log/_logs.html.eex:46 +#, elixir-autogen, elixir-format +msgid "Failed to decode log data." +msgstr "" + +#: lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex:22 +#, elixir-autogen, elixir-format +msgid "Fast" +msgstr "" + +#: lib/block_scout_web/templates/address/overview.html.eex:249 +#, elixir-autogen, elixir-format +msgid "Fetching gas used..." +msgstr "" + +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:112 +#, elixir-autogen, elixir-format +msgid "Fetching holders..." +msgstr "" + +#: lib/block_scout_web/templates/address/_balance_dropdown.html.eex:7 +#, elixir-autogen, elixir-format +msgid "Fetching tokens..." +msgstr "" + +#: lib/block_scout_web/templates/address/overview.html.eex:196 +#: lib/block_scout_web/templates/address/overview.html.eex:204 +#, elixir-autogen, elixir-format +msgid "Fetching transactions..." +msgstr "" + +#: lib/block_scout_web/templates/address/overview.html.eex:223 +#: lib/block_scout_web/templates/address/overview.html.eex:231 +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:123 +#, elixir-autogen, elixir-format +msgid "Fetching transfers..." +msgstr "" + +#: lib/block_scout_web/templates/verified_contracts/index.html.eex:15 +#, elixir-autogen, elixir-format +msgid "Filter by compiler:" +msgstr "" + +#: lib/block_scout_web/templates/admin/dashboard/index.html.eex:16 +#, elixir-autogen, elixir-format +msgid "For any existing contracts in the database, insert all ABI entries into the contract_methods table. Use this in case you have verified smart contracts before early March 2019 and you want other contracts with the same functions to show those ABI's as candidate matches." +msgstr "" + +#: lib/block_scout_web/templates/visualize_sol2uml/index.html.eex:7 +#, elixir-autogen, elixir-format +msgid "For contract" +msgstr "" + +#: lib/block_scout_web/templates/layout/_topnav.html.eex:44 +#, elixir-autogen, elixir-format +msgid "Forked Blocks (Reorgs)" +msgstr "" + +#: lib/block_scout_web/templates/layout/_footer.html.eex:45 +#, elixir-autogen, elixir-format +msgid "Forum" +msgstr "" + +#: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:38 +#: lib/block_scout_web/templates/address_token_transfer/index.html.eex:40 +#: lib/block_scout_web/templates/address_transaction/index.html.eex:34 +#: lib/block_scout_web/templates/transaction/overview.html.eex:246 +#: lib/block_scout_web/views/address_internal_transaction_view.ex:11 +#: lib/block_scout_web/views/address_token_transfer_view.ex:11 +#: lib/block_scout_web/views/address_transaction_view.ex:11 +#, elixir-autogen, elixir-format +msgid "From" +msgstr "" + +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:18 +#, elixir-autogen, elixir-format +msgid "GET" +msgstr "" + +#: lib/block_scout_web/templates/block/_tile.html.eex:67 +#: lib/block_scout_web/templates/block/overview.html.eex:189 +#: lib/block_scout_web/templates/transaction/overview.html.eex:430 +#, elixir-autogen, elixir-format +msgid "Gas Limit" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:404 +#, elixir-autogen, elixir-format +msgid "Gas Price" +msgstr "" + +#: lib/block_scout_web/templates/address/overview.html.eex:242 +#: lib/block_scout_web/templates/block/_tile.html.eex:73 +#: lib/block_scout_web/templates/block/overview.html.eex:180 +#, elixir-autogen, elixir-format +msgid "Gas Used" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:489 +#, elixir-autogen, elixir-format +msgid "Gas Used by Transaction" +msgstr "" + +#: lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex:3 +#: lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex:18 +#, elixir-autogen, elixir-format +msgid "Gas tracker" +msgstr "" + +#: lib/block_scout_web/templates/address/overview.html.eex:241 +#, elixir-autogen, elixir-format +msgid "Gas used by the address." +msgstr "" + +#: lib/block_scout_web/templates/block/overview.html.eex:60 +#, elixir-autogen, elixir-format +msgid "Genesis Block" +msgstr "" + +#: lib/block_scout_web/templates/layout/_footer.html.eex:24 +#, elixir-autogen, elixir-format +msgid "Github" +msgstr "" + +#: lib/block_scout_web/templates/common_components/_rap_pagination_container.html.eex:8 +#, elixir-autogen, elixir-format +msgid "Go to" +msgstr "" + +#: lib/block_scout_web/templates/layout/_topnav.html.eex:110 +#, elixir-autogen, elixir-format +msgid "GraphQL" +msgstr "" + +#: lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex:11 +#: lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex:20 +#: lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex:21 +#: lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex:22 +#: lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex:38 +#: lib/block_scout_web/views/block_view.ex:22 +#: lib/block_scout_web/views/wei_helper.ex:81 +#, elixir-autogen, elixir-format +msgid "Gwei" +msgstr "" + +#: lib/block_scout_web/templates/block/overview.html.eex:123 +#, elixir-autogen, elixir-format +msgid "Hash" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:553 +#: lib/block_scout_web/templates/transaction/overview.html.eex:557 +#, elixir-autogen, elixir-format +msgid "Hex (Default)" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:224 +#, elixir-autogen, elixir-format +msgid "Highlighted events of the transaction." +msgstr "" + +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:108 +#, elixir-autogen, elixir-format +msgid "Holders" +msgstr "" + +#: lib/block_scout_web/templates/tokens/index.html.eex:48 +#, elixir-autogen, elixir-format +msgid "Holders Count" +msgstr "" + +#: lib/block_scout_web/templates/api_docs/eth_rpc.html.eex:11 +#, elixir-autogen, elixir-format +msgid "However, in general, the" +msgstr "" + +#: lib/block_scout_web/templates/transaction/_decoded_input.html.eex:19 +#, elixir-autogen, elixir-format +msgid "IMPORTANT: This information is a best guess based on similar functions from other verified contracts." +msgstr "" + +#: lib/block_scout_web/templates/internal_transaction/_tile.html.eex:42 +#: lib/block_scout_web/templates/transaction/_tile.html.eex:92 +#, elixir-autogen, elixir-format +msgid "IN" +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:56 +#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:48 +#, elixir-autogen, elixir-format +msgid "If you enabled optimization during compilation, select yes." +msgstr "" + +#: lib/block_scout_web/templates/address/overview.html.eex:135 +#, elixir-autogen, elixir-format +msgid "Implementation" +msgstr "" + +#: lib/block_scout_web/templates/address/overview.html.eex:134 +#, elixir-autogen, elixir-format +msgid "Implementation address of the proxy contract." +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification_common_fields/_include_nightly_builds_field.html.eex:3 +#, elixir-autogen, elixir-format +msgid "Include nightly builds" +msgstr "" + +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:30 +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:43 +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:56 +#, elixir-autogen, elixir-format +msgid "Incoming" +msgstr "" + +#: lib/block_scout_web/templates/address_withdrawal/index.html.eex:29 +#: lib/block_scout_web/templates/block_withdrawal/index.html.eex:23 +#: lib/block_scout_web/templates/withdrawal/index.html.eex:23 +#, elixir-autogen, elixir-format +msgid "Index" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:537 +#, elixir-autogen, elixir-format +msgid "Index position of Transaction in the block." +msgstr "" + +#: lib/block_scout_web/templates/block/overview.html.eex:251 +#, elixir-autogen, elixir-format +msgid "Index position(s) of referenced stale blocks." +msgstr "" + +#: lib/block_scout_web/templates/log/_data_decoded_view.html.eex:6 +#, elixir-autogen, elixir-format +msgid "Indexed?" +msgstr "" + +#: lib/block_scout_web/templates/transaction/_decoded_input.html.eex:3 +#, elixir-autogen, elixir-format +msgid "Input" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:265 +#, elixir-autogen, elixir-format +msgid "Interacted With (To)" +msgstr "" + +#: lib/block_scout_web/templates/internal_transaction/_tile.html.eex:7 +#, elixir-autogen, elixir-format +msgid "Internal Transaction" +msgstr "" + +#: lib/block_scout_web/templates/address/_tabs.html.eex:36 +#: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:17 +#: lib/block_scout_web/templates/transaction/_tabs.html.eex:11 +#: lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex:6 +#: lib/block_scout_web/views/address_view.ex:347 +#: lib/block_scout_web/views/transaction_view.ex:552 +#, elixir-autogen, elixir-format +msgid "Internal Transactions" +msgstr "" + +#: lib/block_scout_web/templates/internal_server_error/index.html.eex:7 +#, elixir-autogen, elixir-format +msgid "Internal server error" +msgstr "" + +#: lib/block_scout_web/views/internal_transaction_view.ex:25 +#, elixir-autogen, elixir-format +msgid "Invalid" +msgstr "" + +#: lib/block_scout_web/templates/tokens/inventory/index.html.eex:16 +#: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:19 +#: lib/block_scout_web/views/tokens/overview_view.ex:42 +#, elixir-autogen, elixir-format +msgid "Inventory" +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification_common_fields/_yul_contracts_switcher.html.eex:3 +#, elixir-autogen, elixir-format +msgid "Is Yul contract" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:204 +#, elixir-autogen, elixir-format +msgid "L1 Block" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:523 +#: lib/block_scout_web/templates/transaction/overview.html.eex:524 +#, elixir-autogen, elixir-format +msgid "L1 Fee Scalar" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:512 +#: lib/block_scout_web/templates/transaction/overview.html.eex:513 +#, elixir-autogen, elixir-format +msgid "L1 Gas Price" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:501 +#: lib/block_scout_web/templates/transaction/overview.html.eex:502 +#, elixir-autogen, elixir-format +msgid "L1 Gas Used by Transaction" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:426 +#, elixir-autogen, elixir-format +msgid "L2 Gas Limit" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:400 +#, elixir-autogen, elixir-format +msgid "L2 Gas Price" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:485 +#, elixir-autogen, elixir-format +msgid "L2 Gas Used by Transaction" +msgstr "" + +#: lib/block_scout_web/templates/verified_contracts/_stats.html.eex:13 +#: lib/block_scout_web/templates/verified_contracts/_stats.html.eex:26 +#, elixir-autogen, elixir-format +msgid "Last 24h" +msgstr "" + +#: lib/block_scout_web/templates/address/overview.html.eex:260 +#, elixir-autogen, elixir-format +msgid "Last Balance Update" +msgstr "" + +#: lib/block_scout_web/templates/account/api_key/index.html.eex:12 +#: lib/block_scout_web/templates/account/api_key/index.html.eex:18 +#, elixir-autogen, elixir-format +msgid "Learn more" +msgstr "" + +#: lib/block_scout_web/templates/layout/app.html.eex:49 +#, elixir-autogen, elixir-format +msgid "Less than" +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification_common_fields/_library_address.html.eex:4 +#: lib/block_scout_web/templates/address_contract_verification_common_fields/_library_name.html.eex:4 +#, elixir-autogen, elixir-format +msgid "Library" +msgstr "" + +#: lib/block_scout_web/templates/address/_validator_metadata_modal.html.eex:24 +#, elixir-autogen, elixir-format +msgid "License Expires" +msgstr "" + +#: lib/block_scout_web/templates/address/_validator_metadata_modal.html.eex:10 +#, elixir-autogen, elixir-format +msgid "License ID" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:351 +#, elixir-autogen, elixir-format +msgid "List of ERC-1155 tokens created in the transaction." +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:335 +#, elixir-autogen, elixir-format +msgid "List of token burnt in the transaction." +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:318 +#, elixir-autogen, elixir-format +msgid "List of token minted in the transaction." +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:302 +#, elixir-autogen, elixir-format +msgid "List of token transferred in the transaction." +msgstr "" + +#: lib/block_scout_web/templates/address_coin_balance/index.html.eex:18 +#, elixir-autogen, elixir-format +msgid "Loading chart..." +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:77 +#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:109 +#: lib/block_scout_web/templates/address_contract_verification_via_json/new.html.eex:35 +#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:99 +#: lib/block_scout_web/templates/address_contract_verification_via_standard_json_input/new.html.eex:49 +#: lib/block_scout_web/templates/address_contract_verification_vyper/new.html.eex:45 +#: lib/block_scout_web/templates/address_read_contract/index.html.eex:41 +#: lib/block_scout_web/templates/address_read_contract/index.html.eex:49 +#: lib/block_scout_web/templates/address_read_proxy/index.html.eex:12 +#: lib/block_scout_web/templates/address_write_contract/index.html.eex:39 +#: lib/block_scout_web/templates/address_write_contract/index.html.eex:47 +#: lib/block_scout_web/templates/address_write_proxy/index.html.eex:12 +#: lib/block_scout_web/templates/tokens/contract/index.html.eex:17 +#, elixir-autogen, elixir-format +msgid "Loading..." +msgstr "" + +#: lib/block_scout_web/templates/log/_data_decoded_view.html.eex:2 +#, elixir-autogen, elixir-format +msgid "Log Data" +msgstr "" + +#: lib/block_scout_web/templates/transaction_log/_logs.html.eex:140 +#, elixir-autogen, elixir-format +msgid "Log Index" +msgstr "" + +#: lib/block_scout_web/templates/address/_tabs.html.eex:49 +#: lib/block_scout_web/templates/address_logs/index.html.eex:10 +#: lib/block_scout_web/templates/transaction/_tabs.html.eex:17 +#: lib/block_scout_web/templates/transaction_log/index.html.eex:8 +#: lib/block_scout_web/views/address_view.ex:357 +#: lib/block_scout_web/views/transaction_view.ex:553 +#, elixir-autogen, elixir-format +msgid "Logs" +msgstr "" + +#: lib/block_scout_web/templates/layout/_footer.html.eex:54 +#, elixir-autogen, elixir-format +msgid "Main Networks" +msgstr "" + +#: lib/block_scout_web/templates/chain/show.html.eex:53 +#: lib/block_scout_web/templates/layout/app.html.eex:50 +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:85 +#: lib/block_scout_web/views/address_view.ex:147 +#, elixir-autogen, elixir-format +msgid "Market Cap" +msgstr "" + +#: lib/block_scout_web/templates/verified_contracts/index.html.eex:84 +#, elixir-autogen, elixir-format +msgid "Market cap" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:440 +#, elixir-autogen, elixir-format +msgid "Max Fee per Gas" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:450 +#, elixir-autogen, elixir-format +msgid "Max Priority Fee per Gas" +msgstr "" + +#: lib/block_scout_web/views/transaction_view.ex:331 +#, elixir-autogen, elixir-format +msgid "Max of" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:425 +#, elixir-autogen, elixir-format +msgid "Maximum gas amount approved for the transaction on L2." +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:429 +#, elixir-autogen, elixir-format +msgid "Maximum gas amount approved for the transaction." +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:439 +#, elixir-autogen, elixir-format +msgid "Maximum total amount per unit of gas a user is willing to pay for a transaction, including base fee and priority fee." +msgstr "" + +#: lib/block_scout_web/templates/tokens/instance/metadata/index.html.eex:18 +#: lib/block_scout_web/templates/tokens/instance/overview/_tabs.html.eex:10 +#: lib/block_scout_web/views/tokens/instance/overview_view.ex:75 +#, elixir-autogen, elixir-format +msgid "Metadata" +msgstr "" + +#: lib/block_scout_web/templates/transaction/_decoded_input_body.html.eex:5 +#, elixir-autogen, elixir-format +msgid "Method Id" +msgstr "" + +#: lib/block_scout_web/templates/block/_tile.html.eex:41 +#: lib/block_scout_web/templates/block/overview.html.eex:98 +#: lib/block_scout_web/templates/chain/_block.html.eex:16 +#: lib/block_scout_web/templates/transaction_state/_state_change.html.eex:22 +#, elixir-autogen, elixir-format +msgid "Miner" +msgstr "" + +#: lib/block_scout_web/views/block_view.ex:63 +#: lib/block_scout_web/views/block_view.ex:68 +#, elixir-autogen, elixir-format +msgid "Miner Reward" +msgstr "" + +#: lib/block_scout_web/templates/common_components/_minimal_proxy_pattern.html.eex:3 +#, elixir-autogen, elixir-format +msgid "Minimal Proxy Contract for" +msgstr "" + +#: lib/block_scout_web/templates/block/overview.html.eex:208 +#, elixir-autogen, elixir-format +msgid "Minimum fee required per unit of gas. Fee adjusts based on network congestion." +msgstr "" + +#: lib/block_scout_web/templates/transaction/_actions.html.eex:92 +#, elixir-autogen, elixir-format +msgid "Mint of %{address} To %{to}" +msgstr "" + +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:223 +#, elixir-autogen, elixir-format +msgid "Model" +msgstr "" + +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:58 +#, elixir-autogen, elixir-format +msgid "Module" +msgstr "" + +#: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:12 +#, elixir-autogen, elixir-format +msgid "More internal transactions have come in" +msgstr "" + +#: lib/block_scout_web/templates/address_transaction/index.html.eex:46 +#: lib/block_scout_web/templates/chain/show.html.eex:219 +#: lib/block_scout_web/templates/pending_transaction/index.html.eex:13 +#: lib/block_scout_web/templates/transaction/index.html.eex:19 +#, elixir-autogen, elixir-format +msgid "More transactions have come in" +msgstr "" + +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:63 +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:74 +#, elixir-autogen, elixir-format +msgid "Must be set to:" +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:22 +#, elixir-autogen, elixir-format +msgid "Must match the name specified in the code. For example, in contract MyContract {..} MyContract is the contract name." +msgstr "" + +#: lib/block_scout_web/templates/tokens/_tile.html.eex:40 +#: lib/block_scout_web/templates/verified_contracts/_contract.html.eex:21 +#: lib/block_scout_web/templates/verified_contracts/_contract.html.eex:61 +#, elixir-autogen, elixir-format +msgid "N/A" +msgstr "" + +#: lib/block_scout_web/templates/block/overview.html.eex:116 +#, elixir-autogen, elixir-format +msgid "N/A bytes" +msgstr "" + +#: lib/block_scout_web/templates/account/api_key/form.html.eex:19 +#: lib/block_scout_web/templates/account/api_key/index.html.eex:28 +#: lib/block_scout_web/templates/account/custom_abi/form.html.eex:13 +#: lib/block_scout_web/templates/account/custom_abi/index.html.eex:28 +#: lib/block_scout_web/templates/account/tag_address/form.html.eex:18 +#: lib/block_scout_web/templates/account/tag_address/index.html.eex:22 +#: lib/block_scout_web/templates/account/tag_transaction/form.html.eex:18 +#: lib/block_scout_web/templates/account/tag_transaction/index.html.eex:22 +#: lib/block_scout_web/templates/account/watchlist/show.html.eex:22 +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:19 +#: lib/block_scout_web/templates/address_contract_verification_common_fields/_library_name.html.eex:4 +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:52 +#: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:59 +#: lib/block_scout_web/templates/log/_data_decoded_view.html.eex:4 +#: lib/block_scout_web/templates/transaction/_decoded_input_body.html.eex:21 +#, elixir-autogen, elixir-format +msgid "Name" +msgstr "" + +#: lib/block_scout_web/templates/account/api_key/form.html.eex:20 +#, elixir-autogen, elixir-format +msgid "Name this API key" +msgstr "" + +#: lib/block_scout_web/templates/account/custom_abi/form.html.eex:14 +#, elixir-autogen, elixir-format +msgid "Name this Custom ABI" +msgstr "" + +#: lib/block_scout_web/templates/account/tag_address/form.html.eex:19 +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:20 +#, elixir-autogen, elixir-format +msgid "Name this address" +msgstr "" + +#: lib/block_scout_web/templates/account/tag_transaction/form.html.eex:19 +#, elixir-autogen, elixir-format +msgid "Name this transaction" +msgstr "" + +#: lib/block_scout_web/templates/address_token/overview.html.eex:44 +#, elixir-autogen, elixir-format +msgid "Net Worth" +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:5 +#, elixir-autogen, elixir-format +msgid "New Smart Contract Verification" +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification_via_standard_json_input/new.html.eex:9 +#, elixir-autogen, elixir-format +msgid "New Smart Contract Verification via Standard input JSON" +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification_via_json/new.html.eex:5 +#, elixir-autogen, elixir-format +msgid "New Smart Contract Verification via metadata JSON" +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:9 +#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:7 +#, elixir-autogen, elixir-format +msgid "New Solidity Smart Contract Verification" +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:7 +#, elixir-autogen, elixir-format +msgid "New Solidity/Yul Smart Contract Verification" +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification_vyper/new.html.eex:7 +#, elixir-autogen, elixir-format +msgid "New Vyper Smart Contract Verification" +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:80 +#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:87 +#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:95 +#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:103 +#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:111 +#, elixir-autogen, elixir-format +msgid "Next" +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification_common_fields/_fetch_constructor_args.html.eex:9 +#: lib/block_scout_web/templates/address_contract_verification_common_fields/_include_nightly_builds_field.html.eex:9 +#: lib/block_scout_web/templates/address_contract_verification_common_fields/_yul_contracts_switcher.html.eex:9 +#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:46 +#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:38 +#, elixir-autogen, elixir-format +msgid "No" +msgstr "" + +#: lib/block_scout_web/templates/transaction_raw_trace/_card_body.html.eex:15 +#, elixir-autogen, elixir-format +msgid "No trace entries found." +msgstr "" + +#: lib/block_scout_web/templates/block/overview.html.eex:198 +#: lib/block_scout_web/templates/transaction/overview.html.eex:535 +#, elixir-autogen, elixir-format +msgid "Nonce" +msgstr "" + +#: lib/block_scout_web/templates/tokens/inventory/_token.html.eex:11 +#, elixir-autogen, elixir-format +msgid "Not unique Token" +msgstr "" + +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:107 +#, elixir-autogen, elixir-format +msgid "Number of accounts holding the token" +msgstr "" + +#: lib/block_scout_web/templates/address/overview.html.eex:276 +#, elixir-autogen, elixir-format +msgid "Number of blocks validated by this validator." +msgstr "" + +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:130 +#, elixir-autogen, elixir-format +msgid "Number of digits that come after the decimal place when displaying token value" +msgstr "" + +#: lib/block_scout_web/templates/address/overview.html.eex:187 +#, elixir-autogen, elixir-format +msgid "Number of transactions related to this address." +msgstr "" + +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:118 +#, elixir-autogen, elixir-format +msgid "Number of transfers for the token" +msgstr "" + +#: lib/block_scout_web/templates/address/overview.html.eex:214 +#, elixir-autogen, elixir-format +msgid "Number of transfers to/from this address." +msgstr "" + +#: lib/block_scout_web/templates/internal_transaction/_tile.html.eex:40 +#: lib/block_scout_web/templates/transaction/_tile.html.eex:88 +#, elixir-autogen, elixir-format +msgid "OUT" +msgstr "" + +#: lib/block_scout_web/templates/common_components/_rap_pagination_container.html.eex:13 +#, elixir-autogen, elixir-format +msgid "Only the first" +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:40 +#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:32 +#: lib/block_scout_web/templates/verified_contracts/index.html.eex:75 +#, elixir-autogen, elixir-format +msgid "Optimization" +msgstr "" + +#: lib/block_scout_web/templates/address_contract/index.html.eex:67 +#, elixir-autogen, elixir-format +msgid "Optimization enabled" +msgstr "" + +#: lib/block_scout_web/templates/address_contract/index.html.eex:76 +#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:62 +#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:54 +#, elixir-autogen, elixir-format +msgid "Optimization runs" +msgstr "" + +#: lib/block_scout_web/templates/layout/_footer.html.eex:78 +#, elixir-autogen, elixir-format +msgid "Other Explorers" +msgstr "" + +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:35 +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:48 +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:61 +#, elixir-autogen, elixir-format +msgid "Outgoing" +msgstr "" + +#: lib/block_scout_web/templates/tokens/inventory/_token.html.eex:24 +#, elixir-autogen, elixir-format +msgid "Owner Address" +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:77 +#, elixir-autogen, elixir-format +msgid "POA solidity flattener or the" +msgstr "" + +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:19 +#: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:26 +#, elixir-autogen, elixir-format +msgid "POST" +msgstr "" + +#: lib/block_scout_web/templates/common_components/_pagination_container.html.eex:41 +#, elixir-autogen, elixir-format +msgid "Page" +msgstr "" + +#: lib/block_scout_web/templates/page_not_found/index.html.eex:7 +#, elixir-autogen, elixir-format +msgid "Page not found" +msgstr "" + +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:33 +#: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:40 +#, elixir-autogen, elixir-format +msgid "Parameters" +msgstr "" + +#: lib/block_scout_web/templates/block/overview.html.eex:139 +#, elixir-autogen, elixir-format +msgid "Parent Hash" +msgstr "" + +#: lib/block_scout_web/templates/layout/_topnav.html.eex:63 +#: lib/block_scout_web/views/transaction_view.ex:374 +#: lib/block_scout_web/views/transaction_view.ex:419 +#: lib/block_scout_web/views/transaction_view.ex:427 +#, elixir-autogen, elixir-format +msgid "Pending" +msgstr "" + +#: lib/block_scout_web/templates/pending_transaction/index.html.eex:5 +#, elixir-autogen, elixir-format +msgid "Pending Transactions" +msgstr "" + +#: lib/block_scout_web/templates/address/_custom_view_df_title.html.eex:9 +#: lib/block_scout_web/templates/address/_custom_view_df_title.html.eex:13 +#, elixir-autogen, elixir-format +msgid "Play" +msgstr "" + +#: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:22 +#: lib/block_scout_web/templates/layout/app.html.eex:100 +#, elixir-autogen, elixir-format +msgid "Please confirm your email address to use the My Account feature." +msgstr "" + +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:68 +#, elixir-autogen, elixir-format +msgid "Please select notification methods:" +msgstr "" + +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:24 +#, elixir-autogen, elixir-format +msgid "Please select what types of notifications you will receive:" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:537 +#, elixir-autogen, elixir-format +msgid "Position" +msgstr "" + +#: lib/block_scout_web/templates/block/overview.html.eex:256 +#, elixir-autogen, elixir-format +msgid "Position %{index}" +msgstr "" + +#: lib/block_scout_web/templates/transaction/_decoded_input.html.eex:18 +#, elixir-autogen, elixir-format +msgid "Potential matches from contract method database:" +msgstr "" + +#: lib/block_scout_web/templates/transaction/_decoded_input.html.eex:32 +#, elixir-autogen, elixir-format +msgid "Potential matches from our contract method database:" +msgstr "" + +#: lib/block_scout_web/templates/layout/_search.html.eex:27 +#, elixir-autogen, elixir-format +msgid "Press / and focus will be moved to the search field" +msgstr "" + +#: lib/block_scout_web/templates/chain/show.html.eex:42 +#: lib/block_scout_web/templates/layout/app.html.eex:51 +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:96 +#, elixir-autogen, elixir-format +msgid "Price" +msgstr "" + +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:95 +#, elixir-autogen, elixir-format +msgid "Price per token on the exchanges" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:399 +#, elixir-autogen, elixir-format +msgid "Price per unit of gas specified by the sender on L2. Higher gas prices can prioritize transaction inclusion during times of high usage." +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:403 +#, elixir-autogen, elixir-format +msgid "Price per unit of gas specified by the sender. Higher gas prices can prioritize transaction inclusion during times of high usage." +msgstr "" + +#: lib/block_scout_web/templates/block/overview.html.eex:227 +#: lib/block_scout_web/templates/transaction/overview.html.eex:460 +#, elixir-autogen, elixir-format +msgid "Priority Fee / Tip" +msgstr "" + +#: lib/block_scout_web/templates/block/_tile.html.eex:62 +#, elixir-autogen, elixir-format +msgid "Priority Fees" +msgstr "" + +#: lib/block_scout_web/templates/account/common/_nav.html.eex:4 +#: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:14 +#, elixir-autogen, elixir-format +msgid "Profile" +msgstr "" + +#: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:20 +#, elixir-autogen, elixir-format +msgid "Public Tags" +msgstr "" + +#: lib/block_scout_web/templates/account/public_tags_request/index.html.eex:20 +#, elixir-autogen, elixir-format +msgid "Public tag" +msgstr "" + +#: lib/block_scout_web/templates/account/common/_nav.html.eex:22 +#: lib/block_scout_web/templates/account/public_tags_request/index.html.eex:7 +#, elixir-autogen, elixir-format +msgid "Public tags" +msgstr "" + +#: lib/block_scout_web/templates/account/public_tags_request/form.html.eex:50 +#, elixir-autogen, elixir-format +msgid "Public tags* (2 tags maximum, please use \";\" as a divider)" +msgstr "" + +#: lib/block_scout_web/templates/common_components/_btn_qr_code.html.eex:10 +#: lib/block_scout_web/templates/common_components/_modal_qr_code.html.eex:5 +#: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:83 +#, elixir-autogen, elixir-format +msgid "QR Code" +msgstr "" + +#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:106 +#, elixir-autogen, elixir-format +msgid "Query" +msgstr "" + +#: lib/block_scout_web/templates/layout/_topnav.html.eex:115 +#, elixir-autogen, elixir-format +msgid "RPC" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:546 +#, elixir-autogen, elixir-format +msgid "Raw Input" +msgstr "" + +#: lib/block_scout_web/templates/transaction/_tabs.html.eex:24 +#: lib/block_scout_web/templates/transaction_raw_trace/_card_body.html.eex:1 +#: lib/block_scout_web/views/transaction_view.ex:554 +#, elixir-autogen, elixir-format +msgid "Raw Trace" +msgstr "" + +#: lib/block_scout_web/templates/address/_tabs.html.eex:81 +#: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:27 +#: lib/block_scout_web/views/address_view.ex:351 +#: lib/block_scout_web/views/tokens/overview_view.ex:41 +#, elixir-autogen, elixir-format +msgid "Read Contract" +msgstr "" + +#: lib/block_scout_web/templates/address/_tabs.html.eex:88 +#: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:41 +#: lib/block_scout_web/views/address_view.ex:352 +#, elixir-autogen, elixir-format +msgid "Read Proxy" +msgstr "" + +#: lib/block_scout_web/templates/common_components/_pagination_container.html.eex:13 +#, elixir-autogen, elixir-format +msgid "Records" +msgstr "" + +#: lib/block_scout_web/templates/account/api_key/row.html.eex:13 +#: lib/block_scout_web/templates/account/custom_abi/row.html.eex:13 +#, elixir-autogen, elixir-format +msgid "Remove" +msgstr "" + +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:77 +#, elixir-autogen, elixir-format +msgid "Remove from Watch list" +msgstr "" + +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:155 +#, elixir-autogen, elixir-format +msgid "Request URL" +msgstr "" + +#: lib/block_scout_web/templates/account/public_tags_request/form.html.eex:7 +#, elixir-autogen, elixir-format +msgid "Request a public tag/label" +msgstr "" + +#: lib/block_scout_web/templates/error422/index.html.eex:7 +#, elixir-autogen, elixir-format +msgid "Request cannot be processed" +msgstr "" + +#: lib/block_scout_web/templates/account/public_tags_request/index.html.eex:37 +#, elixir-autogen, elixir-format +msgid "Request to add public tag" +msgstr "" + +#: lib/block_scout_web/templates/account/public_tags_request/form.html.eex:7 +#, elixir-autogen, elixir-format +msgid "Request to edit a public tag/label" +msgstr "" + +#: lib/block_scout_web/templates/layout/app.html.eex:100 +#, elixir-autogen, elixir-format +msgid "Resend verification email" +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:112 +#: lib/block_scout_web/templates/address_contract_verification_via_json/new.html.eex:38 +#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:104 +#: lib/block_scout_web/templates/address_contract_verification_via_standard_json_input/new.html.eex:52 +#: lib/block_scout_web/templates/address_contract_verification_vyper/new.html.eex:48 +#, elixir-autogen, elixir-format +msgid "Reset" +msgstr "" + +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:173 +#: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:134 +#, elixir-autogen, elixir-format +msgid "Response Body" +msgstr "" + +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:185 +#: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:147 +#, elixir-autogen, elixir-format +msgid "Responses" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:98 +#, elixir-autogen, elixir-format +msgid "Result" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:138 +#, elixir-autogen, elixir-format +msgid "Revert reason" +msgstr "" + +#: lib/block_scout_web/templates/block/_tile.html.eex:52 +#: lib/block_scout_web/templates/chain/_block.html.eex:27 +#: lib/block_scout_web/views/internal_transaction_view.ex:29 +#, elixir-autogen, elixir-format +msgid "Reward" +msgstr "" + +#: lib/block_scout_web/templates/admin/dashboard/index.html.eex:21 +#, elixir-autogen, elixir-format +msgid "Run" +msgstr "" + +#: lib/block_scout_web/templates/account/api_key/form.html.eex:26 +#: lib/block_scout_web/templates/account/custom_abi/form.html.eex:31 +#: lib/block_scout_web/templates/account/tag_address/form.html.eex:25 +#: lib/block_scout_web/templates/account/tag_transaction/form.html.eex:25 +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:83 +#, elixir-autogen, elixir-format +msgid "Save" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:226 +#, elixir-autogen, elixir-format +msgid "Scroll to see more" +msgstr "" + +#: lib/block_scout_web/templates/address_logs/index.html.eex:16 +#: lib/block_scout_web/templates/layout/_search.html.eex:34 +#, elixir-autogen, elixir-format +msgid "Search" +msgstr "" + +#: lib/block_scout_web/templates/search/results.html.eex:17 +#, elixir-autogen, elixir-format +msgid "Search Results" +msgstr "" + +#: lib/block_scout_web/templates/layout/_search.html.eex:3 +#, elixir-autogen, elixir-format +msgid "Search by address, token symbol name, transaction hash, or block number" +msgstr "" + +#: lib/block_scout_web/templates/address_token_balance/_token_balances.html.eex:47 +#, elixir-autogen, elixir-format +msgid "Search tokens" +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification_common_fields/_yul_contracts_switcher.html.eex:19 +#, elixir-autogen, elixir-format +msgid "Select Yes if you want to verify Yul contract." +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification_common_fields/_include_nightly_builds_field.html.eex:19 +#, elixir-autogen, elixir-format +msgid "Select yes if you want to show nightly builds." +msgstr "" + +#: lib/block_scout_web/views/internal_transaction_view.ex:28 +#, elixir-autogen, elixir-format +msgid "Self-Destruct" +msgstr "" + +#: lib/block_scout_web/templates/account/public_tags_request/form.html.eex:63 +#, elixir-autogen, elixir-format +msgid "Send request" +msgstr "" + +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:163 +#: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:124 +#, elixir-autogen, elixir-format +msgid "Server Response" +msgstr "" + +#: lib/block_scout_web/templates/common_components/_pagination_container.html.eex:7 +#, elixir-autogen, elixir-format +msgid "Show" +msgstr "" + +#: lib/block_scout_web/templates/common_components/_btn_qr_code.html.eex:11 +#, elixir-autogen, elixir-format +msgid "Show QR Code" +msgstr "" + +#: lib/block_scout_web/templates/address_token/overview.html.eex:52 +#, elixir-autogen, elixir-format +msgid "Shows the current" +msgstr "" + +#: lib/block_scout_web/templates/address_token/overview.html.eex:59 +#, elixir-autogen, elixir-format +msgid "Shows the tokens held in the address (includes ERC-20, ERC-721 and ERC-1155)." +msgstr "" + +#: lib/block_scout_web/templates/address_token/overview.html.eex:66 +#, elixir-autogen, elixir-format +msgid "Shows the total CRC balance in the address." +msgstr "" + +#: lib/block_scout_web/templates/address_token/overview.html.eex:45 +#, elixir-autogen, elixir-format +msgid "Shows total assets held in the address" +msgstr "" + +#: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:33 +#, elixir-autogen, elixir-format +msgid "Sign in" +msgstr "" + +#: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:24 +#, elixir-autogen, elixir-format +msgid "Sign out" +msgstr "" + +#: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:11 +#, elixir-autogen, elixir-format +msgid "Signed in as " +msgstr "" + +#: lib/block_scout_web/templates/block/overview.html.eex:114 +#, elixir-autogen, elixir-format +msgid "Size" +msgstr "" + +#: lib/block_scout_web/templates/block/overview.html.eex:113 +#, elixir-autogen, elixir-format +msgid "Size of the block in bytes." +msgstr "" + +#: lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex:20 +#, elixir-autogen, elixir-format +msgid "Slow" +msgstr "" + +#: lib/block_scout_web/templates/account/public_tags_request/index.html.eex:21 +#, elixir-autogen, elixir-format +msgid "Smart contract / Address" +msgstr "" + +#: lib/block_scout_web/templates/account/public_tags_request/address_field.html.eex:4 +#: lib/block_scout_web/templates/account/public_tags_request/address_field.html.eex:5 +#, elixir-autogen, elixir-format +msgid "Smart contract / Address (0x...)" +msgstr "" + +#: lib/block_scout_web/templates/verified_contracts/_contract.html.eex:28 +#: lib/block_scout_web/templates/verified_contracts/index.html.eex:26 +#: lib/block_scout_web/views/verified_contracts_view.ex:10 +#, elixir-autogen, elixir-format +msgid "Solidity" +msgstr "" + +#: lib/block_scout_web/templates/address_coin_balance/index.html.eex:30 +#: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:50 +#: lib/block_scout_web/templates/address_logs/index.html.eex:23 +#: lib/block_scout_web/templates/address_token/index.html.eex:60 +#: lib/block_scout_web/templates/address_token_transfer/index.html.eex:58 +#: lib/block_scout_web/templates/address_transaction/index.html.eex:50 +#: lib/block_scout_web/templates/address_validation/index.html.eex:20 +#: lib/block_scout_web/templates/address_withdrawal/index.html.eex:20 +#: lib/block_scout_web/templates/block_transaction/index.html.eex:14 +#: lib/block_scout_web/templates/block_withdrawal/index.html.eex:14 +#: lib/block_scout_web/templates/chain/show.html.eex:160 +#: lib/block_scout_web/templates/pending_transaction/index.html.eex:18 +#: lib/block_scout_web/templates/tokens/holder/index.html.eex:24 +#: lib/block_scout_web/templates/tokens/instance/holder/index.html.eex:23 +#: lib/block_scout_web/templates/tokens/instance/transfer/index.html.eex:23 +#: lib/block_scout_web/templates/tokens/inventory/index.html.eex:23 +#: lib/block_scout_web/templates/tokens/transfer/index.html.eex:22 +#: lib/block_scout_web/templates/transaction/index.html.eex:25 +#: lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex:13 +#: lib/block_scout_web/templates/transaction_log/index.html.eex:15 +#: lib/block_scout_web/templates/transaction_state/index.html.eex:13 +#: lib/block_scout_web/templates/transaction_token_transfer/index.html.eex:14 +#: lib/block_scout_web/templates/verified_contracts/index.html.eex:51 +#: lib/block_scout_web/templates/withdrawal/index.html.eex:14 +#, elixir-autogen, elixir-format +msgid "Something went wrong, click to reload." +msgstr "" + +#: lib/block_scout_web/templates/chain/show.html.eex:225 +#, elixir-autogen, elixir-format +msgid "Something went wrong, click to retry." +msgstr "" + +#: lib/block_scout_web/templates/transaction/not_found.html.eex:6 +#, elixir-autogen, elixir-format +msgid "Sorry, we are unable to locate this transaction hash" +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:63 +#, elixir-autogen, elixir-format +msgid "Sources *.sol files" +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:63 +#, elixir-autogen, elixir-format +msgid "Sources *.sol or *.yul files" +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification_via_json/new.html.eex:14 +#, elixir-autogen, elixir-format +msgid "Sources and Metadata JSON" +msgstr "" + +#: lib/block_scout_web/templates/layout/_topnav.html.eex:136 +#, elixir-autogen, elixir-format +msgid "Stakes" +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification_via_standard_json_input/new.html.eex:24 +#, elixir-autogen, elixir-format +msgid "Standard Input JSON" +msgstr "" + +#: lib/block_scout_web/templates/transaction/_tabs.html.eex:29 +#: lib/block_scout_web/templates/transaction_state/index.html.eex:6 +#: lib/block_scout_web/views/transaction_view.ex:555 +#, elixir-autogen, elixir-format +msgid "State changes" +msgstr "" + +#: lib/block_scout_web/views/internal_transaction_view.ex:24 +#, elixir-autogen, elixir-format +msgid "Static Call" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:110 +#, elixir-autogen, elixir-format +msgid "Status" +msgstr "" + +#: lib/block_scout_web/templates/account/public_tags_request/index.html.eex:22 +#, elixir-autogen, elixir-format +msgid "Submission date" +msgstr "" + +#: lib/block_scout_web/templates/layout/_footer.html.eex:41 +#, elixir-autogen, elixir-format +msgid "Submit an Issue" +msgstr "" + +#: lib/block_scout_web/templates/transaction/_emission_reward_tile.html.eex:8 +#: lib/block_scout_web/views/transaction_view.ex:376 +#, elixir-autogen, elixir-format +msgid "Success" +msgstr "" + +#: lib/block_scout_web/templates/transaction/_pending_tile.html.eex:21 +#: lib/block_scout_web/templates/transaction/_tile.html.eex:52 +#, elixir-autogen, elixir-format +msgid "TX Fee" +msgstr "" + +#: lib/block_scout_web/templates/layout/_footer.html.eex:31 +#, elixir-autogen, elixir-format +msgid "Telegram" +msgstr "" + +#: lib/block_scout_web/templates/layout/_footer.html.eex:67 +#, elixir-autogen, elixir-format +msgid "Test Networks" +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification_common_fields/_library_first.html.eex:9 +#, elixir-autogen, elixir-format +msgid "The 0x library address. This can be found in the generated json file or Truffle output (if using truffle)." +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:34 +#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:26 +#, elixir-autogen, elixir-format +msgid "The EVM version the contract is written for. If the bytecode does not match the version, we try to verify using the latest EVM version." +msgstr "" + +#: lib/block_scout_web/templates/block/overview.html.eex:122 +#, elixir-autogen, elixir-format +msgid "The SHA256 hash of the block." +msgstr "" + +#: lib/block_scout_web/templates/block/overview.html.eex:51 +#, elixir-autogen, elixir-format +msgid "The block height of a particular block is defined as the number of blocks preceding it in the blockchain." +msgstr "" + +#: lib/block_scout_web/templates/transaction_state/index.html.eex:18 +#, elixir-autogen, elixir-format +msgid "The changes from this transaction have not yet happened since the transaction is still pending." +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:26 +#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:18 +#, elixir-autogen, elixir-format +msgid "The compiler version is specified in pragma solidity X.X.X. Use the compiler version rather than the nightly build. If using the Solidity compiler, run solc —version to check." +msgstr "" + +#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:44 +#, elixir-autogen, elixir-format +msgid "The fallback function is executed on a call to the contract if none of the other functions match the given function signature, or if no data was supplied at all and there is no receive Ether function. The fallback function always receives data, but in order to also receive Ether it must be marked payable." +msgstr "" + +#: lib/block_scout_web/templates/block/overview.html.eex:138 +#, elixir-autogen, elixir-format +msgid "The hash of the block from which this block was generated." +msgstr "" + +#: lib/block_scout_web/templates/address/overview.html.eex:74 +#, elixir-autogen, elixir-format +msgid "The name found in the source code of the Contract." +msgstr "" + +#: lib/block_scout_web/templates/address/overview.html.eex:85 +#, elixir-autogen, elixir-format +msgid "The name of the validator." +msgstr "" + +#: lib/block_scout_web/templates/block/overview.html.eex:79 +#, elixir-autogen, elixir-format +msgid "The number of transactions in the block." +msgstr "" + +#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:46 +#, elixir-autogen, elixir-format +msgid "The receive function is executed on a call to the contract with empty calldata. This is the function that is executed on plain Ether transfers (e.g. via .send() or .transfer()). If no such function exists, but a payable fallback function exists, the fallback function will be called on a plain Ether transfer. If neither a receive Ether nor a payable fallback function is present, the contract cannot receive Ether through regular transactions and throws an exception." +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:137 +#, elixir-autogen, elixir-format +msgid "The revert reason of the transaction." +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:109 +#, elixir-autogen, elixir-format +msgid "The status of the transaction: Confirmed or Unconfirmed." +msgstr "" + +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:67 +#, elixir-autogen, elixir-format +msgid "The total amount of tokens issued" +msgstr "" + +#: lib/block_scout_web/templates/block/overview.html.eex:179 +#, elixir-autogen, elixir-format +msgid "The total gas amount used in the block and its percentage of gas filled in the block." +msgstr "" + +#: lib/block_scout_web/templates/address_validation/index.html.eex:16 +#, elixir-autogen, elixir-format +msgid "There are no blocks validated by this address." +msgstr "" + +#: lib/block_scout_web/templates/block/index.html.eex:17 +#, elixir-autogen, elixir-format +msgid "There are no blocks." +msgstr "" + +#: lib/block_scout_web/templates/tokens/holder/index.html.eex:29 +#, elixir-autogen, elixir-format +msgid "There are no holders for this Token." +msgstr "" + +#: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:54 +#, elixir-autogen, elixir-format +msgid "There are no internal transactions for this address." +msgstr "" + +#: lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex:17 +#, elixir-autogen, elixir-format +msgid "There are no internal transactions for this transaction." +msgstr "" + +#: lib/block_scout_web/templates/address_logs/index.html.eex:28 +#, elixir-autogen, elixir-format +msgid "There are no logs for this address." +msgstr "" + +#: lib/block_scout_web/templates/transaction_log/index.html.eex:20 +#, elixir-autogen, elixir-format +msgid "There are no logs for this transaction." +msgstr "" + +#: lib/block_scout_web/templates/pending_transaction/index.html.eex:22 +#, elixir-autogen, elixir-format +msgid "There are no pending transactions." +msgstr "" + +#: lib/block_scout_web/templates/address_token_transfer/index.html.eex:53 +#, elixir-autogen, elixir-format +msgid "There are no token transfers for this address." +msgstr "" + +#: lib/block_scout_web/templates/transaction_token_transfer/index.html.eex:19 +#, elixir-autogen, elixir-format +msgid "There are no token transfers for this transaction" +msgstr "" + +#: lib/block_scout_web/templates/address_token/index.html.eex:65 +#, elixir-autogen, elixir-format +msgid "There are no tokens for this address." +msgstr "" + +#: lib/block_scout_web/templates/tokens/inventory/index.html.eex:28 +#, elixir-autogen, elixir-format +msgid "There are no tokens." +msgstr "" + +#: lib/block_scout_web/templates/address_transaction/index.html.eex:55 +#, elixir-autogen, elixir-format +msgid "There are no transactions for this address." +msgstr "" + +#: lib/block_scout_web/templates/block_transaction/index.html.eex:19 +#, elixir-autogen, elixir-format +msgid "There are no transactions for this block." +msgstr "" + +#: lib/block_scout_web/templates/transaction/index.html.eex:31 +#, elixir-autogen, elixir-format +msgid "There are no transactions." +msgstr "" + +#: lib/block_scout_web/templates/tokens/instance/holder/index.html.eex:28 +#: lib/block_scout_web/templates/tokens/instance/transfer/index.html.eex:28 +#: lib/block_scout_web/templates/tokens/transfer/index.html.eex:27 +#, elixir-autogen, elixir-format +msgid "There are no transfers for this Token." +msgstr "" + +#: lib/block_scout_web/templates/verified_contracts/index.html.eex:99 +#, elixir-autogen, elixir-format +msgid "There are no verified contracts." +msgstr "" + +#: lib/block_scout_web/templates/address_withdrawal/index.html.eex:54 +#, elixir-autogen, elixir-format +msgid "There are no withdrawals for this address." +msgstr "" + +#: lib/block_scout_web/templates/block_withdrawal/index.html.eex:45 +#, elixir-autogen, elixir-format +msgid "There are no withdrawals for this block." +msgstr "" + +#: lib/block_scout_web/templates/withdrawal/index.html.eex:53 +#, elixir-autogen, elixir-format +msgid "There are no withdrawals." +msgstr "" + +#: lib/block_scout_web/templates/address_coin_balance/index.html.eex:35 +#, elixir-autogen, elixir-format +msgid "There is no coin history for this address." +msgstr "" + +#: lib/block_scout_web/templates/address_coin_balance/index.html.eex:21 +#: lib/block_scout_web/templates/chain/show.html.eex:9 +#, elixir-autogen, elixir-format +msgid "There was a problem loading the chart." +msgstr "" + +#: lib/block_scout_web/templates/api_docs/index.html.eex:6 +#, elixir-autogen, elixir-format +msgid "This API is provided for developers transitioning their applications from Etherscan to BlockScout. It supports GET and POST requests." +msgstr "" + +#: lib/block_scout_web/templates/api_docs/eth_rpc.html.eex:7 +#, elixir-autogen, elixir-format +msgid "This API is provided to support some rpc methods in the exact format specified for ethereum nodes, which can be found " +msgstr "" + +#: lib/block_scout_web/views/block_transaction_view.ex:11 +#, elixir-autogen, elixir-format +msgid "This block has not been processed yet." +msgstr "" + +#: lib/block_scout_web/templates/address_contract/index.html.eex:47 +#, elixir-autogen, elixir-format +msgid "This contract has been partially verified via Sourcify." +msgstr "" + +#: lib/block_scout_web/templates/address_contract/index.html.eex:51 +#, elixir-autogen, elixir-format +msgid "This contract has been verified via Sourcify." +msgstr "" + +#: lib/block_scout_web/templates/api_docs/eth_rpc.html.eex:10 +#, elixir-autogen, elixir-format +msgid "This is useful to allow sending requests to blockscout without having to change anything about the request." +msgstr "" + +#: lib/block_scout_web/templates/page_not_found/index.html.eex:8 +#, elixir-autogen, elixir-format +msgid "This page is no longer explorable! If you are lost, use the search bar to find what you are looking for." +msgstr "" + +#: lib/block_scout_web/templates/transaction_state/index.html.eex:22 +#, elixir-autogen, elixir-format +msgid "This transaction hasn't changed state." +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:64 +#, elixir-autogen, elixir-format +msgid "This transaction is pending confirmation." +msgstr "" + +#: lib/block_scout_web/templates/block/overview.html.eex:71 +#: lib/block_scout_web/templates/transaction/overview.html.eex:180 +#, elixir-autogen, elixir-format +msgid "Timestamp" +msgstr "" + +#: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:32 +#: lib/block_scout_web/templates/address_token_transfer/index.html.eex:34 +#: lib/block_scout_web/templates/address_transaction/index.html.eex:28 +#: lib/block_scout_web/templates/block_withdrawal/index.html.eex:29 +#: lib/block_scout_web/templates/transaction/overview.html.eex:267 +#: lib/block_scout_web/templates/withdrawal/index.html.eex:32 +#: lib/block_scout_web/views/address_internal_transaction_view.ex:10 +#: lib/block_scout_web/views/address_token_transfer_view.ex:10 +#: lib/block_scout_web/views/address_transaction_view.ex:10 +#, elixir-autogen, elixir-format +msgid "To" +msgstr "" + +#: lib/block_scout_web/templates/transaction/_decoded_input.html.eex:20 +#, elixir-autogen, elixir-format +msgid "To have guaranteed accuracy, use the link above to verify the contract's source code." +msgstr "" + +#: lib/block_scout_web/templates/address_logs/_logs.html.eex:6 +#: lib/block_scout_web/templates/transaction/_decoded_input.html.eex:8 +#: lib/block_scout_web/templates/transaction_log/_logs.html.eex:6 +#, elixir-autogen, elixir-format +msgid "To see accurate decoded input data, the contract must be verified." +msgstr "" + +#: lib/block_scout_web/templates/layout/_topnav.html.eex:18 +#, elixir-autogen, elixir-format +msgid "Toggle navigation" +msgstr "" + +#: lib/block_scout_web/templates/address/overview.html.eex:55 +#: lib/block_scout_web/templates/tokens/index.html.eex:31 +#, elixir-autogen, elixir-format +msgid "Token" +msgstr "" + +#: lib/block_scout_web/templates/common_components/_token_transfer_type_display_name.html.eex:3 +#: lib/block_scout_web/views/transaction_view.ex:488 +#, elixir-autogen, elixir-format +msgid "Token Burning" +msgstr "" + +#: lib/block_scout_web/templates/common_components/_token_transfer_type_display_name.html.eex:7 +#: lib/block_scout_web/views/transaction_view.ex:489 +#, elixir-autogen, elixir-format +msgid "Token Creation" +msgstr "" + +#: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:10 +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:34 +#, elixir-autogen, elixir-format +msgid "Token Details" +msgstr "" + +#: lib/block_scout_web/templates/tokens/holder/index.html.eex:17 +#: lib/block_scout_web/templates/tokens/instance/holder/index.html.eex:16 +#: lib/block_scout_web/templates/tokens/instance/overview/_tabs.html.eex:17 +#: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:11 +#: lib/block_scout_web/views/tokens/overview_view.ex:40 +#, elixir-autogen, elixir-format +msgid "Token Holders" +msgstr "" + +#: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:38 +#: lib/block_scout_web/templates/tokens/inventory/_token.html.eex:18 +#: lib/block_scout_web/templates/tokens/inventory/_token.html.eex:37 +#, elixir-autogen, elixir-format +msgid "Token ID" +msgstr "" + +#: lib/block_scout_web/templates/common_components/_token_transfer_type_display_name.html.eex:5 +#: lib/block_scout_web/views/transaction_view.ex:487 +#, elixir-autogen, elixir-format +msgid "Token Minting" +msgstr "" + +#: lib/block_scout_web/templates/common_components/_token_transfer_type_display_name.html.eex:9 +#: lib/block_scout_web/templates/common_components/_token_transfer_type_display_name.html.eex:11 +#: lib/block_scout_web/views/transaction_view.ex:490 +#, elixir-autogen, elixir-format +msgid "Token Transfer" +msgstr "" + +#: lib/block_scout_web/templates/address/_tabs.html.eex:13 +#: lib/block_scout_web/templates/address_token_transfer/index.html.eex:19 +#: lib/block_scout_web/templates/tokens/instance/overview/_tabs.html.eex:3 +#: lib/block_scout_web/templates/tokens/instance/transfer/index.html.eex:16 +#: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:5 +#: lib/block_scout_web/templates/tokens/transfer/index.html.eex:15 +#: lib/block_scout_web/templates/transaction/_tabs.html.eex:4 +#: lib/block_scout_web/templates/transaction_token_transfer/index.html.eex:7 +#: lib/block_scout_web/views/address_view.ex:349 +#: lib/block_scout_web/views/tokens/instance/overview_view.ex:74 +#: lib/block_scout_web/views/tokens/overview_view.ex:39 +#: lib/block_scout_web/views/transaction_view.ex:551 +#, elixir-autogen, elixir-format +msgid "Token Transfers" +msgstr "" + +#: lib/block_scout_web/templates/address/overview.html.eex:54 +#, elixir-autogen, elixir-format +msgid "Token name and symbol." +msgstr "" + +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:142 +#, elixir-autogen, elixir-format +msgid "Token type" +msgstr "" + +#: lib/block_scout_web/templates/address/_tabs.html.eex:21 +#: lib/block_scout_web/templates/address/overview.html.eex:177 +#: lib/block_scout_web/templates/address_token/overview.html.eex:58 +#: lib/block_scout_web/templates/address_token_transfer/index.html.eex:13 +#: lib/block_scout_web/templates/layout/_topnav.html.eex:84 +#: lib/block_scout_web/templates/tokens/index.html.eex:10 +#: lib/block_scout_web/views/address_view.ex:346 +#, elixir-autogen, elixir-format +msgid "Tokens" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:336 +#, elixir-autogen, elixir-format +msgid "Tokens Burnt" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:352 +#, elixir-autogen, elixir-format +msgid "Tokens Created" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:319 +#, elixir-autogen, elixir-format +msgid "Tokens Minted" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:303 +#, elixir-autogen, elixir-format +msgid "Tokens Transferred" +msgstr "" + +#: lib/block_scout_web/templates/address/_metatags.html.eex:13 +#, elixir-autogen, elixir-format +msgid "Top Accounts - %{subnetwork} Explorer" +msgstr "" + +#: lib/block_scout_web/templates/address_logs/index.html.eex:14 +#, elixir-autogen, elixir-format +msgid "Topic" +msgstr "" + +#: lib/block_scout_web/templates/address_logs/_logs.html.eex:68 +#: lib/block_scout_web/templates/transaction_log/_logs.html.eex:100 +#, elixir-autogen, elixir-format +msgid "Topics" +msgstr "" + +#: lib/block_scout_web/templates/verified_contracts/_stats.html.eex:9 +#: lib/block_scout_web/templates/verified_contracts/_stats.html.eex:22 +#, elixir-autogen, elixir-format +msgid "Total" +msgstr "" + +#: lib/block_scout_web/templates/block/overview.html.eex:170 +#, elixir-autogen, elixir-format +msgid "Total Difficulty" +msgstr "" + +#: lib/block_scout_web/templates/tokens/index.html.eex:43 +#, elixir-autogen, elixir-format +msgid "Total Supply" +msgstr "" + +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:84 +#, elixir-autogen, elixir-format +msgid "Total Supply * Price" +msgstr "" + +#: lib/block_scout_web/templates/chain/show.html.eex:133 +#, elixir-autogen, elixir-format +msgid "Total blocks" +msgstr "" + +#: lib/block_scout_web/templates/block/overview.html.eex:169 +#, elixir-autogen, elixir-format +msgid "Total difficulty of the chain until this block." +msgstr "" + +#: lib/block_scout_web/templates/block/overview.html.eex:188 +#, elixir-autogen, elixir-format +msgid "Total gas limit provided by all transactions in the block." +msgstr "" + +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:68 +#, elixir-autogen, elixir-format +msgid "Total supply" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:383 +#, elixir-autogen, elixir-format +msgid "Total transaction fee." +msgstr "" + +#: lib/block_scout_web/templates/chain/show.html.eex:112 +#, elixir-autogen, elixir-format +msgid "Total transactions" +msgstr "" + +#: lib/block_scout_web/templates/account/tag_transaction/form.html.eex:11 +#: lib/block_scout_web/templates/account/tag_transaction/index.html.eex:23 +#: lib/block_scout_web/templates/address_logs/_logs.html.eex:19 +#: lib/block_scout_web/views/transaction_view.ex:500 +#, elixir-autogen, elixir-format +msgid "Transaction" +msgstr "" + +#: lib/block_scout_web/templates/transaction/_metatags.html.eex:3 +#, elixir-autogen, elixir-format +msgid "Transaction %{transaction} - %{subnetwork} Explorer" +msgstr "" + +#: lib/block_scout_web/templates/transaction/_metatags.html.eex:11 +#, elixir-autogen, elixir-format +msgid "Transaction %{transaction}, %{subnetwork} %{transaction}" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:225 +#, elixir-autogen, elixir-format +msgid "Transaction Action" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:470 +#, elixir-autogen, elixir-format +msgid "Transaction Burnt Fee" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:50 +#, elixir-autogen, elixir-format +msgid "Transaction Details" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:384 +#, elixir-autogen, elixir-format +msgid "Transaction Fee" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:80 +#, elixir-autogen, elixir-format +msgid "Transaction Hash" +msgstr "" + +#: lib/block_scout_web/templates/transaction/_decoded_input_body.html.eex:2 +#: lib/block_scout_web/templates/transaction/_decoded_input_body.html.eex:19 +#, elixir-autogen, elixir-format +msgid "Transaction Inputs" +msgstr "" + +#: lib/block_scout_web/templates/account/common/_nav.html.eex:13 +#: lib/block_scout_web/templates/account/tag_transaction/index.html.eex:7 +#: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:17 +#, elixir-autogen, elixir-format +msgid "Transaction Tags" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:414 +#, elixir-autogen, elixir-format +msgid "Transaction Type" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:534 +#, elixir-autogen, elixir-format +msgid "Transaction number from the sending address. Each transaction sent from an address increments the nonce by 1." +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:413 +#, elixir-autogen, elixir-format +msgid "Transaction type, introduced in EIP-2718." +msgstr "" + +#: lib/block_scout_web/templates/address/_tabs.html.eex:7 +#: lib/block_scout_web/templates/address/_tile.html.eex:31 +#: lib/block_scout_web/templates/address/overview.html.eex:188 +#: lib/block_scout_web/templates/address/overview.html.eex:194 +#: lib/block_scout_web/templates/address/overview.html.eex:202 +#: lib/block_scout_web/templates/address_transaction/index.html.eex:13 +#: lib/block_scout_web/templates/block/_tabs.html.eex:4 +#: lib/block_scout_web/templates/block/overview.html.eex:80 +#: lib/block_scout_web/templates/chain/show.html.eex:216 +#: lib/block_scout_web/templates/layout/_topnav.html.eex:49 +#: lib/block_scout_web/views/address_view.ex:348 +#, elixir-autogen, elixir-format +msgid "Transactions" +msgstr "" + +#: lib/block_scout_web/templates/address/overview.html.eex:101 +#, elixir-autogen, elixir-format +msgid "Transactions and address of creation." +msgstr "" + +#: lib/block_scout_web/templates/address/overview.html.eex:215 +#: lib/block_scout_web/templates/address/overview.html.eex:221 +#: lib/block_scout_web/templates/address/overview.html.eex:229 +#: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:50 +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:119 +#, elixir-autogen, elixir-format +msgid "Transfers" +msgstr "" + +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:40 +#: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:47 +#, elixir-autogen, elixir-format +msgid "Try it out" +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification_common_fields/_fetch_constructor_args.html.eex:3 +#, elixir-autogen, elixir-format +msgid "Try to fetch constructor arguments automatically" +msgstr "" + +#: lib/block_scout_web/templates/layout/_footer.html.eex:27 +#, elixir-autogen, elixir-format +msgid "Twitter" +msgstr "" + +#: lib/block_scout_web/templates/layout/app.html.eex:53 +#, elixir-autogen, elixir-format +msgid "Tx/day" +msgstr "" + +#: lib/block_scout_web/templates/verified_contracts/index.html.eex:66 +#, elixir-autogen, elixir-format +msgid "Txns" +msgstr "" + +#: lib/block_scout_web/templates/log/_data_decoded_view.html.eex:5 +#: lib/block_scout_web/templates/transaction/_decoded_input_body.html.eex:22 +#, elixir-autogen, elixir-format +msgid "Type" +msgstr "" + +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:141 +#, elixir-autogen, elixir-format +msgid "Type of the token standard" +msgstr "" + +#: lib/block_scout_web/templates/visualize_sol2uml/index.html.eex:5 +#, elixir-autogen, elixir-format +msgid "UML diagram" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:560 +#, elixir-autogen, elixir-format +msgid "UTF-8" +msgstr "" + +#: lib/block_scout_web/views/block_view.ex:77 +#, elixir-autogen, elixir-format +msgid "Uncle Reward" +msgstr "" + +#: lib/block_scout_web/templates/block/overview.html.eex:252 +#: lib/block_scout_web/templates/layout/_topnav.html.eex:41 +#, elixir-autogen, elixir-format +msgid "Uncles" +msgstr "" + +#: lib/block_scout_web/views/transaction_view.ex:367 +#, elixir-autogen, elixir-format +msgid "Unconfirmed" +msgstr "" + +#: lib/block_scout_web/templates/tokens/inventory/_token.html.eex:9 +#, elixir-autogen, elixir-format +msgid "Unique Token" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:79 +#, elixir-autogen, elixir-format +msgid "Unique character string (TxID) assigned to every verified transaction." +msgstr "" + +#: lib/block_scout_web/templates/account/api_key/form.html.eex:7 +#: lib/block_scout_web/templates/account/custom_abi/form.html.eex:8 +#, elixir-autogen, elixir-format +msgid "Update" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:449 +#, elixir-autogen, elixir-format +msgid "User defined maximum fee (tip) per unit of gas paid to validator for transaction prioritization." +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:459 +#, elixir-autogen, elixir-format +msgid "User-defined tip sent to validator for transaction priority/inclusion." +msgstr "" + +#: lib/block_scout_web/templates/block/overview.html.eex:226 +#, elixir-autogen, elixir-format +msgid "User-defined tips sent to validator for transaction priority/inclusion." +msgstr "" + +#: lib/block_scout_web/templates/layout/_topnav.html.eex:56 +#, elixir-autogen, elixir-format +msgid "Validated" +msgstr "" + +#: lib/block_scout_web/templates/transaction/index.html.eex:12 +#, elixir-autogen, elixir-format +msgid "Validated Transactions" +msgstr "" + +#: lib/block_scout_web/templates/address/_validator_metadata_modal.html.eex:30 +#, elixir-autogen, elixir-format +msgid "Validator Creation Date" +msgstr "" + +#: lib/block_scout_web/templates/address/_validator_metadata_modal.html.eex:5 +#, elixir-autogen, elixir-format +msgid "Validator Data" +msgstr "" + +#: lib/block_scout_web/templates/address/overview.html.eex:86 +#, elixir-autogen, elixir-format +msgid "Validator Name" +msgstr "" + +#: lib/block_scout_web/templates/address_withdrawal/index.html.eex:32 +#: lib/block_scout_web/templates/block_withdrawal/index.html.eex:26 +#: lib/block_scout_web/templates/withdrawal/index.html.eex:26 +#, elixir-autogen, elixir-format +msgid "Validator index" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:369 +#, elixir-autogen, elixir-format +msgid "Value" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:368 +#, elixir-autogen, elixir-format +msgid "Value sent in the native token (and USD) if applicable." +msgstr "" + +#: lib/block_scout_web/templates/address_read_contract/index.html.eex:17 +#: lib/block_scout_web/templates/address_write_contract/index.html.eex:15 +#: lib/block_scout_web/templates/verified_contracts/index.html.eex:81 +#, elixir-autogen, elixir-format +msgid "Verified" +msgstr "" + +#: lib/block_scout_web/templates/verified_contracts/_stats.html.eex:18 +#: lib/block_scout_web/templates/verified_contracts/index.html.eex:6 +#, elixir-autogen, elixir-format +msgid "Verified Contracts" +msgstr "" + +#: lib/block_scout_web/templates/address_contract/index.html.eex:88 +#, elixir-autogen, elixir-format +msgid "Verified at" +msgstr "" + +#: lib/block_scout_web/templates/layout/_topnav.html.eex:69 +#, elixir-autogen, elixir-format +msgid "Verified contracts" +msgstr "" + +#: lib/block_scout_web/templates/verified_contracts/_metatags.html.eex:2 +#, elixir-autogen, elixir-format +msgid "Verified contracts - %{subnetwork} Explorer" +msgstr "" + +#: lib/block_scout_web/templates/verified_contracts/_metatags.html.eex:7 +#, elixir-autogen, elixir-format +msgid "Verified contracts, %{subnetwork}, %{coin}" +msgstr "" + +#: lib/block_scout_web/templates/address_contract/index.html.eex:27 +#: lib/block_scout_web/templates/address_contract/index.html.eex:29 +#: lib/block_scout_web/templates/address_contract/index.html.eex:197 +#: lib/block_scout_web/templates/address_contract/index.html.eex:228 +#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:14 +#, elixir-autogen, elixir-format +msgid "Verify & Publish" +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:111 +#: lib/block_scout_web/templates/address_contract_verification_via_json/new.html.eex:37 +#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:103 +#: lib/block_scout_web/templates/address_contract_verification_via_standard_json_input/new.html.eex:51 +#: lib/block_scout_web/templates/address_contract_verification_vyper/new.html.eex:47 +#, elixir-autogen, elixir-format +msgid "Verify & publish" +msgstr "" + +#: lib/block_scout_web/templates/address_logs/_logs.html.eex:10 +#: lib/block_scout_web/templates/transaction/_decoded_input.html.eex:12 +#: lib/block_scout_web/templates/transaction_log/_logs.html.eex:10 +#, elixir-autogen, elixir-format +msgid "Verify the contract " +msgstr "" + +#: lib/block_scout_web/templates/layout/_footer.html.eex:93 +#: lib/block_scout_web/templates/verified_contracts/index.html.eex:72 +#, elixir-autogen, elixir-format +msgid "Version" +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:33 +#, elixir-autogen, elixir-format +msgid "Via Sourcify: Sources and metadata JSON file" +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:27 +#, elixir-autogen, elixir-format +msgid "Via Standard Input JSON" +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:22 +#, elixir-autogen, elixir-format +msgid "Via flattened source code" +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:40 +#, elixir-autogen, elixir-format +msgid "Via multi-part files" +msgstr "" + +#: lib/block_scout_web/templates/chain/show.html.eex:155 +#, elixir-autogen, elixir-format +msgid "View All Blocks" +msgstr "" + +#: lib/block_scout_web/templates/chain/show.html.eex:215 +#, elixir-autogen, elixir-format +msgid "View All Transactions" +msgstr "" + +#: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:16 +#: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:20 +#, elixir-autogen, elixir-format +msgid "View Contract" +msgstr "" + +#: lib/block_scout_web/templates/transaction/_tile.html.eex:73 +#, elixir-autogen, elixir-format +msgid "View Less Transfers" +msgstr "" + +#: lib/block_scout_web/templates/transaction/_tile.html.eex:72 +#, elixir-autogen, elixir-format +msgid "View More Transfers" +msgstr "" + +#: lib/block_scout_web/templates/block/overview.html.eex:39 +#, elixir-autogen, elixir-format +msgid "View next block" +msgstr "" + +#: lib/block_scout_web/templates/block/overview.html.eex:23 +#, elixir-autogen, elixir-format +msgid "View previous block" +msgstr "" + +#: lib/block_scout_web/templates/address/_metatags.html.eex:9 +#, elixir-autogen, elixir-format +msgid "View the account balance, transactions, and other data for %{address} on the %{network}" +msgstr "" + +#: lib/block_scout_web/templates/withdrawal/_metatags.html.eex:8 +#, elixir-autogen, elixir-format +msgid "View the beacon chain withdrawals on %{subnetwork}" +msgstr "" + +#: lib/block_scout_web/templates/block/_metatags.html.eex:10 +#, elixir-autogen, elixir-format +msgid "View the transactions, token transfers, and uncles for block number %{block_number}" +msgstr "" + +#: lib/block_scout_web/templates/verified_contracts/_metatags.html.eex:8 +#, elixir-autogen, elixir-format +msgid "View the verified contracts on %{subnetwork}" +msgstr "" + +#: lib/block_scout_web/templates/transaction/_metatags.html.eex:10 +#, elixir-autogen, elixir-format +msgid "View transaction %{transaction} on %{subnetwork}" +msgstr "" + +#: lib/block_scout_web/templates/verified_contracts/_contract.html.eex:28 +#: lib/block_scout_web/templates/verified_contracts/index.html.eex:32 +#: lib/block_scout_web/views/verified_contracts_view.ex:11 +#, elixir-autogen, elixir-format +msgid "Vyper" +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:46 +#, elixir-autogen, elixir-format +msgid "Vyper contract" +msgstr "" + +#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:142 +#, elixir-autogen, elixir-format +msgid "WEI" +msgstr "" + +#: lib/block_scout_web/templates/smart_contract/_pending_contract_write.html.eex:9 +#, elixir-autogen, elixir-format +msgid "Waiting for transaction's confirmation..." +msgstr "" + +#: lib/block_scout_web/templates/chain/show.html.eex:141 +#, elixir-autogen, elixir-format +msgid "Wallet addresses" +msgstr "" + +#: lib/block_scout_web/templates/common_components/_changed_bytecode_warning.html.eex:3 +#, elixir-autogen, elixir-format +msgid "Warning! Contract bytecode has been changed and doesn't match the verified one. Therefore, interaction with this smart contract may be risky." +msgstr "" + +#: lib/block_scout_web/templates/account/common/_nav.html.eex:7 +#: lib/block_scout_web/templates/account/watchlist/show.html.eex:7 +#: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:15 +#, elixir-autogen, elixir-format +msgid "Watch list" +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:77 +#, elixir-autogen, elixir-format +msgid "We recommend using flattened code. This is necessary if your code utilizes a library or inherits dependencies. Use the" +msgstr "" + +#: lib/block_scout_web/views/wei_helper.ex:80 +#, elixir-autogen, elixir-format +msgid "Wei" +msgstr "" + +#: lib/block_scout_web/templates/address/_tabs.html.eex:29 +#: lib/block_scout_web/templates/address_withdrawal/index.html.eex:13 +#: lib/block_scout_web/templates/block/_tabs.html.eex:13 +#: lib/block_scout_web/templates/layout/_topnav.html.eex:73 +#: lib/block_scout_web/templates/withdrawal/index.html.eex:5 +#, elixir-autogen, elixir-format +msgid "Withdrawals" +msgstr "" + +#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:106 +#, elixir-autogen, elixir-format +msgid "Write" +msgstr "" + +#: lib/block_scout_web/templates/address/_tabs.html.eex:95 +#: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:34 +#: lib/block_scout_web/views/address_view.ex:353 +#, elixir-autogen, elixir-format +msgid "Write Contract" +msgstr "" + +#: lib/block_scout_web/templates/address/_tabs.html.eex:102 +#: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:48 +#: lib/block_scout_web/views/address_view.ex:354 +#, elixir-autogen, elixir-format +msgid "Write Proxy" +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification_common_fields/_fetch_constructor_args.html.eex:14 +#: lib/block_scout_web/templates/address_contract_verification_common_fields/_include_nightly_builds_field.html.eex:14 +#: lib/block_scout_web/templates/address_contract_verification_common_fields/_yul_contracts_switcher.html.eex:14 +#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:51 +#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:43 +#, elixir-autogen, elixir-format +msgid "Yes" +msgstr "" + +#: lib/block_scout_web/templates/account/api_key/index.html.eex:18 +#, elixir-autogen, elixir-format +msgid "You can create 3 API keys per account." +msgstr "" + +#: lib/block_scout_web/templates/account/custom_abi/index.html.eex:18 +#, elixir-autogen, elixir-format +msgid "You can create up to 15 Custom ABIs per account." +msgstr "" + +#: lib/block_scout_web/templates/account/public_tags_request/index.html.eex:11 +#, elixir-autogen, elixir-format +msgid "You can request a public category tag which is displayed to all Blockscout users. Public tags may be added to contract or external addresses, and any associated transactions will inherit that tag. Clicking a tag opens a page with related information and helps provide context and data organization. Requests are sent to a moderator for review and approval. This process can take several days." +msgstr "" + +#: lib/block_scout_web/templates/account/tag_address/index.html.eex:14 +#, elixir-autogen, elixir-format +msgid "You don't have address tags yet" +msgstr "" + +#: lib/block_scout_web/templates/account/watchlist/show.html.eex:14 +#, elixir-autogen, elixir-format +msgid "You don't have addresses on you watchlist yet" +msgstr "" + +#: lib/block_scout_web/templates/account/tag_transaction/index.html.eex:14 +#, elixir-autogen, elixir-format +msgid "You don't have transaction tags yet" +msgstr "" + +#: lib/block_scout_web/templates/account/public_tags_request/form.html.eex:15 +#, elixir-autogen, elixir-format +msgid "Your name" +msgstr "" + +#: lib/block_scout_web/templates/account/public_tags_request/form.html.eex:14 +#, elixir-autogen, elixir-format +msgid "Your name*" +msgstr "" + +#: lib/block_scout_web/templates/error422/index.html.eex:8 +#, elixir-autogen, elixir-format +msgid "Your request contained an error, perhaps a mistyped tx/block/address hash. Try again, and check the developer tools console for more info." +msgstr "" + +#: lib/block_scout_web/templates/verified_contracts/index.html.eex:38 +#: lib/block_scout_web/views/verified_contracts_view.ex:12 +#, elixir-autogen, elixir-format +msgid "Yul" +msgstr "" + +#: lib/block_scout_web/templates/address/overview.html.eex:111 +#, elixir-autogen, elixir-format +msgid "at" +msgstr "" + +#: lib/block_scout_web/templates/address_token/overview.html.eex:52 +#, elixir-autogen, elixir-format +msgid "balance of the address" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:469 +#, elixir-autogen, elixir-format +msgid "burnt for this transaction. Equals Block Base Fee per Gas * Gas Used." +msgstr "" + +#: lib/block_scout_web/templates/block/overview.html.eex:217 +#, elixir-autogen, elixir-format +msgid "burnt from transactions included in the block (Base fee (per unit of gas) * Gas Used)." +msgstr "" + +#: lib/block_scout_web/templates/address_contract/index.html.eex:27 +#, elixir-autogen, elixir-format +msgid "button" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:276 +#, elixir-autogen, elixir-format +msgid "created" +msgstr "" + +#: lib/block_scout_web/templates/api_docs/eth_rpc.html.eex:12 +#, elixir-autogen, elixir-format +msgid "custom RPC" +msgstr "" + +#: lib/block_scout_web/templates/address/overview.html.eex:151 +#, elixir-autogen, elixir-format +msgid "doesn't include ERC20, ERC721, ERC1155 tokens)." +msgstr "" + +#: lib/block_scout_web/templates/common_components/_rap_pagination_container.html.eex:13 +#, elixir-autogen, elixir-format +msgid "elements are displayed" +msgstr "" + +#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:44 +#, elixir-autogen, elixir-format +msgid "fallback" +msgstr "" + +#: lib/block_scout_web/views/address_contract_view.ex:33 +#, elixir-autogen, elixir-format +msgid "false" +msgstr "" + +#: lib/block_scout_web/templates/csv_export/index.html.eex:14 +#, elixir-autogen, elixir-format +msgid "for address" +msgstr "" + +#: lib/block_scout_web/templates/address_logs/_logs.html.eex:10 +#: lib/block_scout_web/templates/transaction/_decoded_input.html.eex:12 +#: lib/block_scout_web/templates/transaction_log/_logs.html.eex:10 +#, elixir-autogen, elixir-format +msgid "here" +msgstr "" + +#: lib/block_scout_web/templates/api_docs/eth_rpc.html.eex:9 +#, elixir-autogen, elixir-format +msgid "here." +msgstr "" + +#: lib/block_scout_web/templates/smart_contract/_function_response.html.eex:3 +#, elixir-autogen, elixir-format +msgid "method Response" +msgstr "" + +#: lib/block_scout_web/templates/common_components/_pagination_container.html.eex:41 +#, elixir-autogen, elixir-format +msgid "of" +msgstr "" + +#: lib/block_scout_web/templates/layout/app.html.eex:100 +#, elixir-autogen, elixir-format +msgid "on sign up. Didn’t receive?" +msgstr "" + +#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:16 +#, elixir-autogen, elixir-format +msgid "page" +msgstr "" + +#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:46 +#, elixir-autogen, elixir-format +msgid "receive" +msgstr "" + +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:58 +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:69 +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:81 +#: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:70 +#, elixir-autogen, elixir-format +msgid "required" +msgstr "" + +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:59 +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:70 +#, elixir-autogen, elixir-format +msgid "string" +msgstr "" + +#: lib/block_scout_web/templates/csv_export/index.html.eex:17 +#, elixir-autogen, elixir-format +msgid "to CSV file" +msgstr "" + +#: lib/block_scout_web/views/address_contract_view.ex:32 +#, elixir-autogen, elixir-format +msgid "true" +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:77 +#, elixir-autogen, elixir-format +msgid "truffle flattener" +msgstr "" diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po new file mode 100644 index 0000000..2499baa --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po @@ -0,0 +1,3736 @@ +## "msgid"s in this file come from POT (.pot) files. +### +### Do not add, change, or remove "msgid"s manually here as +### they're tied to the ones in the corresponding POT file +### (with the same domain). +### +### Use "mix gettext.extract --merge" or "mix gettext.merge" +### to merge POT files into PO files. +msgid "" +msgstr "" +"Language: en\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: lib/block_scout_web/templates/common_components/_minimal_proxy_pattern.html.eex:9 +#, elixir-autogen, elixir-format +msgid " - minimal bytecode implementation that delegates all calls to a known address" +msgstr "" + +#: lib/block_scout_web/templates/api_docs/eth_rpc.html.eex:14 +#, elixir-autogen, elixir-format +msgid " is recommended." +msgstr "" + +#: lib/block_scout_web/templates/address/_metatags.html.eex:3 +#, elixir-autogen, elixir-format +msgid "%{address} - %{subnetwork} Explorer" +msgstr "" + +#: lib/block_scout_web/templates/block/overview.html.eex:12 +#, elixir-autogen, elixir-format +msgid "%{block_type} Details" +msgstr "" + +#: lib/block_scout_web/templates/block/overview.html.eex:55 +#, elixir-autogen, elixir-format +msgid "%{block_type} Height" +msgstr "" + +#: lib/block_scout_web/templates/block/index.html.eex:7 +#, elixir-autogen, elixir-format +msgid "%{block_type}s" +msgstr "" + +#: lib/block_scout_web/templates/block/overview.html.eex:85 +#, elixir-autogen, elixir-format +msgid "%{count} Transaction" +msgstr "" + +#: lib/block_scout_web/templates/block/overview.html.eex:87 +#: lib/block_scout_web/templates/chain/_block.html.eex:11 +#, elixir-autogen, elixir-format +msgid "%{count} Transactions" +msgstr "" + +#: lib/block_scout_web/views/address_token_balance_view.ex:10 +#, elixir-autogen, elixir-format +msgid "%{count} token" +msgid_plural "%{count} tokens" +msgstr[0] "" +msgstr[1] "" + +#: lib/block_scout_web/templates/block/_tile.html.eex:29 +#, elixir-autogen, elixir-format +msgid "%{count} transaction" +msgid_plural "%{count} transactions" +msgstr[0] "" +msgstr[1] "" + +#: lib/block_scout_web/templates/transaction/_actions.html.eex:101 +#, elixir-autogen, elixir-format +msgid "%{qty} of Token ID [%{link_to_id}]" +msgstr "" + +#: lib/block_scout_web/templates/chain/_metatags.html.eex:2 +#, elixir-autogen, elixir-format +msgid "%{subnetwork} %{network} Explorer" +msgstr "" + +#: lib/block_scout_web/templates/layout/_default_title.html.eex:2 +#, elixir-autogen, elixir-format +msgid "%{subnetwork} Explorer - BlockScout" +msgstr "" + +#: lib/block_scout_web/templates/withdrawal/index.html.eex:11 +#, elixir-autogen, elixir-format +msgid "%{withdrawals_count} withdrawals processed and %{withdrawals_sum} withdrawn." +msgstr "" + +#: lib/block_scout_web/views/transaction_view.ex:375 +#, elixir-autogen, elixir-format +msgid "(Awaiting internal transactions for status)" +msgstr "" + +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:59 +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:70 +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:82 +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:104 +#, elixir-autogen, elixir-format +msgid "(query)" +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification_common_fields/_library_first.html.eex:4 +#, elixir-autogen, elixir-format +msgid ") may be added for each contract. Click the Add Library button to add an additional one." +msgstr "" + +#: lib/block_scout_web/templates/layout/app.html.eex:93 +#, elixir-autogen, elixir-format +msgid "- We're indexing this chain right now. Some of the counts may be inaccurate." +msgstr "" + +#: lib/block_scout_web/templates/transaction/not_found.html.eex:8 +#, elixir-autogen, elixir-format +msgid "1. If you have just submitted this transaction please wait for at least 30 seconds before refreshing this page." +msgstr "" + +#: lib/block_scout_web/templates/transaction/not_found.html.eex:9 +#, elixir-autogen, elixir-format +msgid "2. It could still be in the TX Pool of a different node, waiting to be broadcasted." +msgstr "" + +#: lib/block_scout_web/templates/transaction/not_found.html.eex:10 +#, elixir-autogen, elixir-format +msgid "3. During times when the network is busy (i.e during ICOs) it can take a while for your transaction to propagate through the network and for us to index it." +msgstr "" + +#: lib/block_scout_web/templates/transaction/not_found.html.eex:11 +#, elixir-autogen, elixir-format +msgid "4. If it still does not show up after 1 hour, please check with your sender/exchange/wallet/transaction provider for additional information." +msgstr "" + +#: lib/block_scout_web/templates/block/overview.html.eex:197 +#, elixir-autogen, elixir-format +msgid "64-bit hash of value verifying proof-of-work (note: null for POA chains)." +msgstr "" + +#: lib/block_scout_web/templates/block/overview.html.eex:97 +#: lib/block_scout_web/templates/transaction_state/_state_change.html.eex:21 +#, elixir-autogen, elixir-format +msgid "A block producer who successfully included the block onto the blockchain." +msgstr "" + +#: lib/block_scout_web/templates/layout/app.html.eex:100 +#, elixir-autogen, elixir-format +msgid "A confirmation email was sent to" +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification_common_fields/_library_first.html.eex:4 +#, elixir-autogen, elixir-format +msgid "A library name called in the .sol file. Multiple libraries (up to " +msgstr "" + +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:73 +#, elixir-autogen, elixir-format +msgid "A string with the name of the action to be invoked." +msgstr "" + +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:62 +#, elixir-autogen, elixir-format +msgid "A string with the name of the module to be invoked." +msgstr "" + +#: lib/block_scout_web/templates/account/custom_abi/form.html.eex:24 +#, elixir-autogen, elixir-format +msgid "ABI" +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification_common_fields/_constructor_args.html.eex:3 +#, elixir-autogen, elixir-format +msgid "ABI-encoded Constructor Arguments (if required by the contract)" +msgstr "" + +#: lib/block_scout_web/templates/api_docs/index.html.eex:4 +#, elixir-autogen, elixir-format +msgid "API Documentation" +msgstr "" + +#: lib/block_scout_web/templates/api_docs/_metatags.html.eex:4 +#, elixir-autogen, elixir-format +msgid "API endpoints for the %{subnetwork}" +msgstr "" + +#: lib/block_scout_web/templates/api_docs/_metatags.html.eex:2 +#, elixir-autogen, elixir-format +msgid "API for the %{subnetwork} - BlockScout" +msgstr "" + +#: lib/block_scout_web/templates/account/api_key/form.html.eex:7 +#: lib/block_scout_web/templates/account/api_key/form.html.eex:13 +#: lib/block_scout_web/templates/account/api_key/form.html.eex:14 +#: lib/block_scout_web/templates/account/api_key/index.html.eex:29 +#, elixir-autogen, elixir-format +msgid "API key" +msgstr "" + +#: lib/block_scout_web/templates/account/api_key/index.html.eex:7 +#: lib/block_scout_web/templates/account/common/_nav.html.eex:16 +#: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:18 +#, elixir-autogen, elixir-format +msgid "API keys" +msgstr "" + +#: lib/block_scout_web/templates/layout/_topnav.html.eex:106 +#, elixir-autogen, elixir-format +msgid "APIs" +msgstr "" + +#: lib/block_scout_web/templates/account/tag_address/index.html.eex:24 +#: lib/block_scout_web/templates/account/tag_transaction/index.html.eex:24 +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:69 +#, elixir-autogen, elixir-format +msgid "Action" +msgstr "" + +#: lib/block_scout_web/templates/account/watchlist/show.html.eex:25 +#, elixir-autogen, elixir-format +msgid "Actions" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:484 +#, elixir-autogen, elixir-format +msgid "Actual gas amount used by the transaction on L2." +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:488 +#, elixir-autogen, elixir-format, fuzzy +msgid "Actual gas amount used by the transaction." +msgstr "" + +#: lib/block_scout_web/templates/account/api_key/form.html.eex:7 +#: lib/block_scout_web/templates/account/custom_abi/form.html.eex:8 +#: lib/block_scout_web/templates/layout/_add_chain_to_mm.html.eex:10 +#, elixir-autogen, elixir-format +msgid "Add" +msgstr "" + +#: lib/block_scout_web/templates/account/api_key/index.html.eex:44 +#, elixir-autogen, elixir-format +msgid "Add API key" +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:86 +#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:76 +#, elixir-autogen, elixir-format +msgid "Add Contract Libraries" +msgstr "" + +#: lib/block_scout_web/templates/account/custom_abi/index.html.eex:44 +#, elixir-autogen, elixir-format +msgid "Add Custom ABI" +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:97 +#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:87 +#, elixir-autogen, elixir-format +msgid "Add Library" +msgstr "" + +#: lib/block_scout_web/templates/account/watchlist/show.html.eex:38 +#, elixir-autogen, elixir-format +msgid "Add address" +msgstr "" + +#: lib/block_scout_web/templates/account/tag_address/form.html.eex:7 +#: lib/block_scout_web/templates/account/tag_address/index.html.eex:37 +#, elixir-autogen, elixir-format +msgid "Add address tag" +msgstr "" + +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:7 +#, elixir-autogen, elixir-format +msgid "Add address to the Watch list" +msgstr "" + +#: lib/block_scout_web/templates/account/tag_transaction/form.html.eex:7 +#: lib/block_scout_web/templates/account/tag_transaction/index.html.eex:37 +#, elixir-autogen, elixir-format +msgid "Add transaction tag" +msgstr "" + +#: lib/block_scout_web/templates/account/tag_address/form.html.eex:11 +#: lib/block_scout_web/templates/account/tag_address/index.html.eex:23 +#: lib/block_scout_web/templates/account/watchlist/show.html.eex:23 +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:12 +#: lib/block_scout_web/templates/address/_validator_metadata_modal.html.eex:16 +#: lib/block_scout_web/templates/address_contract_verification_common_fields/_library_address.html.eex:4 +#: lib/block_scout_web/templates/tokens/index.html.eex:34 +#: lib/block_scout_web/templates/transaction_log/_logs.html.eex:29 +#: lib/block_scout_web/templates/transaction_state/index.html.eex:34 +#: lib/block_scout_web/templates/verified_contracts/index.html.eex:60 +#: lib/block_scout_web/views/address_view.ex:109 +#, elixir-autogen, elixir-format +msgid "Address" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:263 +#, elixir-autogen, elixir-format +msgid "Address (external or contract) receiving the transaction." +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:245 +#, elixir-autogen, elixir-format +msgid "Address (external or contract) sending the transaction." +msgstr "" + +#: lib/block_scout_web/templates/account/common/_nav.html.eex:10 +#: lib/block_scout_web/templates/account/tag_address/index.html.eex:7 +#: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:16 +#, elixir-autogen, elixir-format +msgid "Address Tags" +msgstr "" + +#: lib/block_scout_web/templates/address/overview.html.eex:151 +#, elixir-autogen, elixir-format +msgid "Address balance in" +msgstr "" + +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:51 +#, elixir-autogen, elixir-format +msgid "Address of the token contract" +msgstr "" + +#: lib/block_scout_web/templates/transaction_state/_state_change.html.eex:7 +#, elixir-autogen, elixir-format +msgid "Address used in token mintings and burnings." +msgstr "" + +#: lib/block_scout_web/templates/account/public_tags_request/address_field.html.eex:2 +#, elixir-autogen, elixir-format +msgid "Address*" +msgstr "" + +#: lib/block_scout_web/templates/address/index.html.eex:5 +#, elixir-autogen, elixir-format +msgid "Addresses" +msgstr "" + +#: lib/block_scout_web/templates/address_withdrawal/index.html.eex:38 +#: lib/block_scout_web/templates/withdrawal/index.html.eex:35 +#, elixir-autogen, elixir-format +msgid "Age" +msgstr "" + +#: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:26 +#: lib/block_scout_web/templates/address_token_transfer/index.html.eex:28 +#: lib/block_scout_web/templates/address_transaction/index.html.eex:22 +#: lib/block_scout_web/templates/layout/_topnav.html.eex:88 +#: lib/block_scout_web/templates/verified_contracts/index.html.eex:20 +#: lib/block_scout_web/views/address_internal_transaction_view.ex:12 +#: lib/block_scout_web/views/address_token_transfer_view.ex:12 +#: lib/block_scout_web/views/address_transaction_view.ex:12 +#: lib/block_scout_web/views/verified_contracts_view.ex:13 +#, elixir-autogen, elixir-format +msgid "All" +msgstr "" + +#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:13 +#, elixir-autogen, elixir-format +msgid "All functions displayed below are from ABI of that contract. In order to verify current contract, proceed with" +msgstr "" + +#: lib/block_scout_web/templates/address_contract/index.html.eex:27 +#, elixir-autogen, elixir-format +msgid "All metadata displayed below is from that contract. In order to verify current contract, click" +msgstr "" + +#: lib/block_scout_web/templates/address/overview.html.eex:176 +#, elixir-autogen, elixir-format +msgid "All tokens in the account and total value." +msgstr "" + +#: lib/block_scout_web/templates/address_withdrawal/index.html.eex:41 +#: lib/block_scout_web/templates/block_withdrawal/index.html.eex:32 +#: lib/block_scout_web/templates/withdrawal/index.html.eex:38 +#, elixir-autogen, elixir-format, fuzzy +msgid "Amount" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:469 +#, elixir-autogen, elixir-format +msgid "Amount of" +msgstr "" + +#: lib/block_scout_web/templates/block/overview.html.eex:238 +#, elixir-autogen, elixir-format +msgid "Amount of distributed reward. Miners receive a static block reward + Tx fees + uncle fees." +msgstr "" + +#: lib/block_scout_web/templates/internal_server_error/index.html.eex:8 +#, elixir-autogen, elixir-format +msgid "An unexpected error has occurred. Try reloading the page, or come back soon and try again." +msgstr "" + +#: lib/block_scout_web/templates/api_docs/eth_rpc.html.eex:15 +#, elixir-autogen, elixir-format +msgid "Anything not in this list is not supported. Click on the method to be taken to the documentation for that method, and check the notes section for any potential differences." +msgstr "" + +#: lib/block_scout_web/templates/layout/_topnav.html.eex:134 +#, elixir-autogen, elixir-format +msgid "Apps" +msgstr "" + +#: lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex:21 +#, elixir-autogen, elixir-format +msgid "Average" +msgstr "" + +#: lib/block_scout_web/templates/chain/show.html.eex:102 +#, elixir-autogen, elixir-format +msgid "Average block time" +msgstr "" + +#: lib/block_scout_web/templates/account/api_key/form.html.eex:25 +#, elixir-autogen, elixir-format +msgid "Back to API keys (Cancel)" +msgstr "" + +#: lib/block_scout_web/templates/account/tag_address/form.html.eex:24 +#, elixir-autogen, elixir-format +msgid "Back to Address Tags (Cancel)" +msgstr "" + +#: lib/block_scout_web/templates/account/custom_abi/form.html.eex:30 +#, elixir-autogen, elixir-format +msgid "Back to Custom ABI (Cancel)" +msgstr "" + +#: lib/block_scout_web/templates/account/tag_transaction/form.html.eex:24 +#, elixir-autogen, elixir-format +msgid "Back to Transaction Tags (Cancel)" +msgstr "" + +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:81 +#, elixir-autogen, elixir-format +msgid "Back to Watch list (Cancel)" +msgstr "" + +#: lib/block_scout_web/templates/error422/index.html.eex:9 +#: lib/block_scout_web/templates/internal_server_error/index.html.eex:9 +#: lib/block_scout_web/templates/page_not_found/index.html.eex:9 +#: lib/block_scout_web/templates/transaction/not_found.html.eex:13 +#, elixir-autogen, elixir-format +msgid "Back to home" +msgstr "" + +#: lib/block_scout_web/templates/account/watchlist/show.html.eex:24 +#: lib/block_scout_web/templates/address/overview.html.eex:152 +#: lib/block_scout_web/templates/address_token/overview.html.eex:51 +#: lib/block_scout_web/templates/verified_contracts/index.html.eex:63 +#, elixir-autogen, elixir-format +msgid "Balance" +msgstr "" + +#: lib/block_scout_web/templates/transaction_state/index.html.eex:40 +#, elixir-autogen, elixir-format +msgid "Balance after" +msgstr "" + +#: lib/block_scout_web/templates/transaction_state/index.html.eex:37 +#, elixir-autogen, elixir-format +msgid "Balance before" +msgstr "" + +#: lib/block_scout_web/templates/address_coin_balance/index.html.eex:14 +#, elixir-autogen, elixir-format +msgid "Balances" +msgstr "" + +#: lib/block_scout_web/templates/block/overview.html.eex:209 +#, elixir-autogen, elixir-format +msgid "Base Fee per Gas" +msgstr "" + +#: lib/block_scout_web/templates/api_docs/eth_rpc.html.eex:5 +#: lib/block_scout_web/templates/api_docs/index.html.eex:5 +#, elixir-autogen, elixir-format +msgid "Base URL:" +msgstr "" + +#: lib/block_scout_web/templates/withdrawal/_metatags.html.eex:2 +#, elixir-autogen, elixir-format +msgid "Beacon chain withdrawals - %{subnetwork} Explorer" +msgstr "" + +#: lib/block_scout_web/templates/withdrawal/_metatags.html.eex:7 +#, elixir-autogen, elixir-format +msgid "Beacon chain, Withdrawals, %{subnetwork}, %{coin}" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:545 +#, elixir-autogen, elixir-format +msgid "Binary data included with the transaction. See input / logs below for additional info." +msgstr "" + +#: lib/block_scout_web/templates/address_coin_balance/_coin_balances.html.eex:8 +#: lib/block_scout_web/templates/address_withdrawal/index.html.eex:35 +#: lib/block_scout_web/templates/block/overview.html.eex:29 +#: lib/block_scout_web/templates/transaction/overview.html.eex:161 +#: lib/block_scout_web/templates/withdrawal/index.html.eex:29 +#, elixir-autogen, elixir-format +msgid "Block" +msgstr "" + +#: lib/block_scout_web/templates/block/_link.html.eex:2 +#: lib/block_scout_web/templates/internal_transaction/_tile.html.eex:32 +#: lib/block_scout_web/templates/tokens/transfer/_token_transfer.html.eex:43 +#, elixir-autogen, elixir-format +msgid "Block #%{number}" +msgstr "" + +#: lib/block_scout_web/templates/block/_metatags.html.eex:3 +#, elixir-autogen, elixir-format +msgid "Block %{block_number} - %{subnetwork} Explorer" +msgstr "" + +#: lib/block_scout_web/templates/block_transaction/404.html.eex:7 +#, elixir-autogen, elixir-format +msgid "Block Details" +msgstr "" + +#: lib/block_scout_web/templates/block/overview.html.eex:53 +#, elixir-autogen, elixir-format +msgid "Block Height" +msgstr "" + +#: lib/block_scout_web/templates/layout/app.html.eex:47 +#, elixir-autogen, elixir-format +msgid "Block Mined, awaiting import..." +msgstr "" + +#: lib/block_scout_web/views/transaction_view.ex:34 +#, elixir-autogen, elixir-format +msgid "Block Pending" +msgstr "" + +#: lib/block_scout_web/templates/block/overview.html.eex:158 +#, elixir-autogen, elixir-format +msgid "Block difficulty for miner, used to calibrate block generation time (Note: constant in POA based networks)." +msgstr "" + +#: lib/block_scout_web/views/block_transaction_view.ex:15 +#, elixir-autogen, elixir-format +msgid "Block not found, please try again later." +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:203 +#, elixir-autogen, elixir-format, fuzzy +msgid "Block number containing the transaction on L1." +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:160 +#, elixir-autogen, elixir-format +msgid "Block number containing the transaction." +msgstr "" + +#: lib/block_scout_web/templates/address/overview.html.eex:259 +#, elixir-autogen, elixir-format +msgid "Block number in which the address was updated." +msgstr "" + +#: lib/block_scout_web/templates/chain/_metatags.html.eex:4 +#, elixir-autogen, elixir-format +msgid "BlockScout provides analytics data, API, and Smart Contract tools for the %{subnetwork}" +msgstr "" + +#: lib/block_scout_web/templates/layout/_topnav.html.eex:29 +#, elixir-autogen, elixir-format +msgid "Blockchain" +msgstr "" + +#: lib/block_scout_web/templates/chain/show.html.eex:156 +#: lib/block_scout_web/templates/layout/_topnav.html.eex:34 +#: lib/block_scout_web/templates/layout/_topnav.html.eex:38 +#, elixir-autogen, elixir-format +msgid "Blocks" +msgstr "" + +#: lib/block_scout_web/templates/layout/app.html.eex:46 +#, elixir-autogen, elixir-format +msgid "Blocks Indexed" +msgstr "" + +#: lib/block_scout_web/templates/address/_tabs.html.eex:56 +#: lib/block_scout_web/templates/address/overview.html.eex:277 +#: lib/block_scout_web/templates/address_validation/index.html.eex:11 +#: lib/block_scout_web/views/address_view.ex:356 +#, elixir-autogen, elixir-format +msgid "Blocks Validated" +msgstr "" + +#: lib/block_scout_web/templates/layout/app.html.eex:48 +#, elixir-autogen, elixir-format +msgid "Blocks With Internal Transactions Indexed" +msgstr "" + +#: lib/block_scout_web/templates/layout/_footer.html.eex:22 +#, elixir-autogen, elixir-format +msgid "Blockscout is a tool for inspecting and analyzing EVM based blockchains. Blockchain explorer for Ethereum Networks." +msgstr "" + +#: lib/block_scout_web/templates/transaction_state/_state_change.html.eex:8 +#, elixir-autogen, elixir-format +msgid "Burn address" +msgstr "" + +#: lib/block_scout_web/templates/block/_tile.html.eex:64 +#: lib/block_scout_web/templates/block/overview.html.eex:218 +#, elixir-autogen, elixir-format +msgid "Burnt Fees" +msgstr "" + +#: lib/block_scout_web/templates/address_token/overview.html.eex:65 +#, elixir-autogen, elixir-format +msgid "CRC Worth" +msgstr "" + +#: lib/block_scout_web/templates/common_components/_csv_export_button.html.eex:4 +#, elixir-autogen, elixir-format +msgid "CSV" +msgstr "" + +#: lib/block_scout_web/templates/transaction/_decoded_input_body.html.eex:10 +#: lib/block_scout_web/views/internal_transaction_view.ex:21 +#, elixir-autogen, elixir-format +msgid "Call" +msgstr "" + +#: lib/block_scout_web/views/internal_transaction_view.ex:22 +#, elixir-autogen, elixir-format +msgid "Call Code" +msgstr "" + +#: lib/block_scout_web/templates/account/public_tags_request/form.html.eex:62 +#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:120 +#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:115 +#: lib/block_scout_web/templates/address_contract_verification_via_json/new.html.eex:41 +#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:107 +#: lib/block_scout_web/templates/address_contract_verification_via_standard_json_input/new.html.eex:55 +#: lib/block_scout_web/templates/address_contract_verification_vyper/new.html.eex:51 +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:47 +#: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:54 +#, elixir-autogen, elixir-format +msgid "Cancel" +msgstr "" + +#: lib/block_scout_web/templates/transaction_state/index.html.eex:43 +#, elixir-autogen, elixir-format +msgid "Change" +msgstr "" + +#: lib/block_scout_web/templates/layout/_footer.html.eex:43 +#, elixir-autogen, elixir-format +msgid "Chat (#blockscout)" +msgstr "" + +#: lib/block_scout_web/views/block_view.ex:65 +#, elixir-autogen, elixir-format +msgid "Chore Reward" +msgstr "" + +#: lib/block_scout_web/templates/tokens/index.html.eex:38 +#, elixir-autogen, elixir-format +msgid "Circulating Market Cap" +msgstr "" + +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:137 +#: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:106 +#, elixir-autogen, elixir-format +msgid "Clear" +msgstr "" + +#: lib/block_scout_web/templates/address/_validator_metadata_modal.html.eex:37 +#: lib/block_scout_web/templates/common_components/_modal_qr_code.html.eex:6 +#: lib/block_scout_web/templates/common_components/_modal_qr_code.html.eex:14 +#: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:84 +#: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:92 +#, elixir-autogen, elixir-format +msgid "Close" +msgstr "" + +#: lib/block_scout_web/templates/address/_tabs.html.eex:66 +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:165 +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:187 +#: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:126 +#: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:149 +#: lib/block_scout_web/views/address_view.ex:350 +#, elixir-autogen, elixir-format +msgid "Code" +msgstr "" + +#: lib/block_scout_web/templates/address/_tabs.html.eex:42 +#: lib/block_scout_web/views/address_view.ex:355 +#, elixir-autogen, elixir-format +msgid "Coin Balance History" +msgstr "" + +#: lib/block_scout_web/templates/transaction/_decoded_input_body.html.eex:54 +#, elixir-autogen, elixir-format +msgid "Collapse" +msgstr "" + +#: lib/block_scout_web/templates/account/public_tags_request/form.html.eex:20 +#, elixir-autogen, elixir-format +msgid "Company name" +msgstr "" + +#: lib/block_scout_web/templates/account/public_tags_request/form.html.eex:32 +#, elixir-autogen, elixir-format +msgid "Company website" +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification_common_fields/_compiler_field.html.eex:3 +#: lib/block_scout_web/templates/verified_contracts/index.html.eex:69 +#, elixir-autogen, elixir-format +msgid "Compiler" +msgstr "" + +#: lib/block_scout_web/templates/address_contract/index.html.eex:142 +#, elixir-autogen, elixir-format +msgid "Compiler Settings" +msgstr "" + +#: lib/block_scout_web/templates/address_contract/index.html.eex:71 +#, elixir-autogen, elixir-format +msgid "Compiler version" +msgstr "" + +#: lib/block_scout_web/views/transaction_view.ex:368 +#, elixir-autogen, elixir-format +msgid "Confirmed" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:127 +#, elixir-autogen, elixir-format +msgid "Confirmed by " +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:193 +#, elixir-autogen, elixir-format +msgid "Confirmed within" +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:2 +#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:6 +#: lib/block_scout_web/templates/address_contract_verification_via_json/new.html.eex:2 +#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:4 +#: lib/block_scout_web/templates/address_contract_verification_via_standard_json_input/new.html.eex:6 +#: lib/block_scout_web/templates/address_contract_verification_vyper/new.html.eex:4 +#: lib/block_scout_web/templates/tokens/holder/index.html.eex:16 +#, elixir-autogen, elixir-format +msgid "Connection Lost" +msgstr "" + +#: lib/block_scout_web/templates/address_coin_balance/index.html.eex:12 +#: lib/block_scout_web/templates/block/index.html.eex:5 +#, elixir-autogen, elixir-format +msgid "Connection Lost, click to load newer blocks" +msgstr "" + +#: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:15 +#, elixir-autogen, elixir-format +msgid "Connection Lost, click to load newer internal transactions" +msgstr "" + +#: lib/block_scout_web/templates/address_transaction/index.html.eex:11 +#: lib/block_scout_web/templates/pending_transaction/index.html.eex:16 +#: lib/block_scout_web/templates/transaction/index.html.eex:22 +#, elixir-autogen, elixir-format +msgid "Connection Lost, click to load newer transactions" +msgstr "" + +#: lib/block_scout_web/templates/address_validation/index.html.eex:10 +#, elixir-autogen, elixir-format +msgid "Connection Lost, click to load newer validations" +msgstr "" + +#: lib/block_scout_web/templates/address_contract/index.html.eex:96 +#, elixir-autogen, elixir-format +msgid "Constructor Arguments" +msgstr "" + +#: lib/block_scout_web/templates/verified_contracts/index.html.eex:78 +#, elixir-autogen, elixir-format +msgid "Constructor args" +msgstr "" + +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:52 +#: lib/block_scout_web/templates/transaction/overview.html.eex:273 +#, elixir-autogen, elixir-format +msgid "Contract" +msgstr "" + +#: lib/block_scout_web/templates/address_contract/index.html.eex:157 +#, elixir-autogen, elixir-format +msgid "Contract ABI" +msgstr "" + +#: lib/block_scout_web/templates/account/custom_abi/form.html.eex:18 +#: lib/block_scout_web/templates/account/custom_abi/index.html.eex:29 +#: lib/block_scout_web/templates/address_contract_verification_common_fields/_contract_address_field.html.eex:3 +#: lib/block_scout_web/views/address_view.ex:107 +#, elixir-autogen, elixir-format +msgid "Contract Address" +msgstr "" + +#: lib/block_scout_web/templates/transaction/_pending_tile.html.eex:16 +#: lib/block_scout_web/views/address_view.ex:47 +#: lib/block_scout_web/views/address_view.ex:81 +#, elixir-autogen, elixir-format +msgid "Contract Address Pending" +msgstr "" + +#: lib/block_scout_web/views/transaction_view.ex:497 +#, elixir-autogen, elixir-format +msgid "Contract Call" +msgstr "" + +#: lib/block_scout_web/views/transaction_view.ex:494 +#, elixir-autogen, elixir-format +msgid "Contract Creation" +msgstr "" + +#: lib/block_scout_web/templates/address_contract/index.html.eex:174 +#: lib/block_scout_web/templates/address_contract/index.html.eex:189 +#, elixir-autogen, elixir-format +msgid "Contract Creation Code" +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:90 +#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:80 +#, elixir-autogen, elixir-format +msgid "Contract Libraries" +msgstr "" + +#: lib/block_scout_web/templates/address/overview.html.eex:75 +#: lib/block_scout_web/templates/address_contract_verification_common_fields/_contract_name_field.html.eex:3 +#, elixir-autogen, elixir-format +msgid "Contract Name" +msgstr "" + +#: lib/block_scout_web/templates/address_contract/index.html.eex:25 +#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:11 +#, elixir-autogen, elixir-format +msgid "Contract is not verified. However, we found a verified contract with the same bytecode in Blockscout DB" +msgstr "" + +#: lib/block_scout_web/templates/verified_contracts/index.html.eex:47 +#, elixir-autogen, elixir-format +msgid "Contract name or address" +msgstr "" + +#: lib/block_scout_web/templates/address_contract/index.html.eex:63 +#, elixir-autogen, elixir-format +msgid "Contract name:" +msgstr "" + +#: lib/block_scout_web/templates/address_contract/index.html.eex:106 +#, elixir-autogen, elixir-format +msgid "Contract source code" +msgstr "" + +#: lib/block_scout_web/templates/address/overview.html.eex:120 +#, elixir-autogen, elixir-format +msgid "Contract was precompiled and created at genesis or contract creation transaction is missing" +msgstr "" + +#: lib/block_scout_web/templates/verified_contracts/_stats.html.eex:5 +#, elixir-autogen, elixir-format +msgid "Contracts" +msgstr "" + +#: lib/block_scout_web/templates/address_contract/index.html.eex:180 +#, elixir-autogen, elixir-format +msgid "Contracts that self destruct in their constructors have no contract code published and cannot be verified." +msgstr "" + +#: lib/block_scout_web/templates/layout/_footer.html.eex:42 +#, elixir-autogen, elixir-format +msgid "Contribute" +msgstr "" + +#: lib/block_scout_web/templates/address_contract/index.html.eex:159 +#, elixir-autogen, elixir-format +msgid "Copy ABI" +msgstr "" + +#: lib/block_scout_web/templates/account/api_key/row.html.eex:6 +#: lib/block_scout_web/templates/account/api_key/row.html.eex:6 +#, elixir-autogen, elixir-format +msgid "Copy API key" +msgstr "" + +#: lib/block_scout_web/templates/account/tag_address/row.html.eex:8 +#: lib/block_scout_web/templates/account/tag_address/row.html.eex:8 +#: lib/block_scout_web/templates/account/tag_transaction/row.html.eex:11 +#: lib/block_scout_web/templates/account/tag_transaction/row.html.eex:11 +#: lib/block_scout_web/templates/account/watchlist_address/row.html.eex:7 +#: lib/block_scout_web/templates/address/overview.html.eex:38 +#: lib/block_scout_web/templates/address/overview.html.eex:39 +#: lib/block_scout_web/templates/block/overview.html.eex:104 +#: lib/block_scout_web/templates/block/overview.html.eex:105 +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:43 +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:44 +#, elixir-autogen, elixir-format +msgid "Copy Address" +msgstr "" + +#: lib/block_scout_web/templates/address_contract/index.html.eex:144 +#, elixir-autogen, elixir-format +msgid "Copy Compiler Settings" +msgstr "" + +#: lib/block_scout_web/templates/account/custom_abi/row.html.eex:6 +#: lib/block_scout_web/templates/account/custom_abi/row.html.eex:6 +#, elixir-autogen, elixir-format +msgid "Copy Contract Address" +msgstr "" + +#: lib/block_scout_web/templates/address_contract/index.html.eex:176 +#: lib/block_scout_web/templates/address_contract/index.html.eex:192 +#, elixir-autogen, elixir-format +msgid "Copy Contract Creation Code" +msgstr "" + +#: lib/block_scout_web/templates/address_contract/index.html.eex:213 +#: lib/block_scout_web/templates/address_contract/index.html.eex:223 +#, elixir-autogen, elixir-format +msgid "Copy Deployed ByteCode" +msgstr "" + +#: lib/block_scout_web/templates/account/watchlist_address/row.html.eex:7 +#: lib/block_scout_web/templates/transaction/_total_transfers_from_to.html.eex:17 +#: lib/block_scout_web/templates/transaction/_total_transfers_from_to.html.eex:18 +#: lib/block_scout_web/templates/transaction/overview.html.eex:253 +#: lib/block_scout_web/templates/transaction/overview.html.eex:254 +#, elixir-autogen, elixir-format +msgid "Copy From Address" +msgstr "" + +#: lib/block_scout_web/templates/block/overview.html.eex:129 +#: lib/block_scout_web/templates/block/overview.html.eex:130 +#, elixir-autogen, elixir-format +msgid "Copy Hash" +msgstr "" + +#: lib/block_scout_web/templates/tokens/instance/metadata/index.html.eex:20 +#, elixir-autogen, elixir-format +msgid "Copy Metadata" +msgstr "" + +#: lib/block_scout_web/templates/block/overview.html.eex:149 +#: lib/block_scout_web/templates/block/overview.html.eex:150 +#, elixir-autogen, elixir-format +msgid "Copy Parent Hash" +msgstr "" + +#: lib/block_scout_web/templates/transaction_raw_trace/_card_body.html.eex:9 +#, elixir-autogen, elixir-format +msgid "Copy Raw Trace" +msgstr "" + +#: lib/block_scout_web/templates/address_contract/index.html.eex:120 +#: lib/block_scout_web/templates/address_contract/index.html.eex:132 +#, elixir-autogen, elixir-format +msgid "Copy Source Code" +msgstr "" + +#: lib/block_scout_web/templates/transaction/_total_transfers_from_to.html.eex:34 +#: lib/block_scout_web/templates/transaction/_total_transfers_from_to.html.eex:35 +#: lib/block_scout_web/templates/transaction/overview.html.eex:280 +#: lib/block_scout_web/templates/transaction/overview.html.eex:281 +#: lib/block_scout_web/templates/transaction/overview.html.eex:288 +#: lib/block_scout_web/templates/transaction/overview.html.eex:289 +#, elixir-autogen, elixir-format +msgid "Copy To Address" +msgstr "" + +#: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:32 +#: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:33 +#, elixir-autogen, elixir-format +msgid "Copy Token ID" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:87 +#, elixir-autogen, elixir-format +msgid "Copy Transaction Hash" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:88 +#, elixir-autogen, elixir-format +msgid "Copy Txn Hash" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:571 +#, elixir-autogen, elixir-format +msgid "Copy Txn Hex Input" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:577 +#, elixir-autogen, elixir-format +msgid "Copy Txn UTF-8 Input" +msgstr "" + +#: lib/block_scout_web/templates/log/_data_decoded_view.html.eex:20 +#: lib/block_scout_web/templates/transaction/_decoded_input_body.html.eex:41 +#: lib/block_scout_web/templates/transaction/overview.html.eex:570 +#: lib/block_scout_web/templates/transaction/overview.html.eex:576 +#: lib/block_scout_web/templates/transaction_raw_trace/_card_body.html.eex:8 +#, elixir-autogen, elixir-format +msgid "Copy Value" +msgstr "" + +#: lib/block_scout_web/views/internal_transaction_view.ex:26 +#, elixir-autogen, elixir-format +msgid "Create" +msgstr "" + +#: lib/block_scout_web/templates/account/custom_abi/index.html.eex:12 +#, elixir-autogen, elixir-format +msgid "Create a Custom ABI to interact with contracts." +msgstr "" + +#: lib/block_scout_web/templates/account/api_key/index.html.eex:12 +#, elixir-autogen, elixir-format, fuzzy +msgid "Create an API key to use with your RPC and EthRPC API requests." +msgstr "" + +#: lib/block_scout_web/views/internal_transaction_view.ex:27 +#, elixir-autogen, elixir-format +msgid "Create2" +msgstr "" + +#: lib/block_scout_web/templates/address/overview.html.eex:102 +#, elixir-autogen, elixir-format +msgid "Creator" +msgstr "" + +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:146 +#: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:116 +#, elixir-autogen, elixir-format +msgid "Curl" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:97 +#, elixir-autogen, elixir-format +msgid "Current transaction state: Success, Failed (Error), or Pending (In Process)" +msgstr "" + +#: lib/block_scout_web/templates/address_read_contract/index.html.eex:20 +#: lib/block_scout_web/templates/address_write_contract/index.html.eex:18 +#, elixir-autogen, elixir-format +msgid "Custom" +msgstr "" + +#: lib/block_scout_web/templates/account/common/_nav.html.eex:19 +#: lib/block_scout_web/templates/account/custom_abi/form.html.eex:8 +#: lib/block_scout_web/templates/account/custom_abi/index.html.eex:7 +#: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:19 +#, elixir-autogen, elixir-format +msgid "Custom ABI" +msgstr "" + +#: lib/block_scout_web/templates/address_read_contract/index.html.eex:25 +#: lib/block_scout_web/templates/address_write_contract/index.html.eex:23 +#, elixir-autogen, elixir-format +msgid "Custom ABI from account" +msgstr "" + +#: lib/block_scout_web/templates/chain/show.html.eex:70 +#, elixir-autogen, elixir-format +msgid "Daily Transactions" +msgstr "" + +#: lib/block_scout_web/templates/address_logs/_logs.html.eex:98 +#: lib/block_scout_web/templates/log/_data_decoded_view.html.eex:7 +#: lib/block_scout_web/templates/transaction/_decoded_input_body.html.eex:23 +#: lib/block_scout_web/templates/transaction_log/_logs.html.eex:130 +#, elixir-autogen, elixir-format +msgid "Data" +msgstr "" + +#: lib/block_scout_web/templates/block/overview.html.eex:70 +#, elixir-autogen, elixir-format +msgid "Date & time at which block was produced." +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:179 +#, elixir-autogen, elixir-format +msgid "Date & time of transaction inclusion, including length of time for confirmation." +msgstr "" + +#: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:52 +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:131 +#, elixir-autogen, elixir-format +msgid "Decimals" +msgstr "" + +#: lib/block_scout_web/templates/address_logs/_logs.html.eex:32 +#: lib/block_scout_web/templates/address_logs/_logs.html.eex:38 +#: lib/block_scout_web/templates/address_logs/_logs.html.eex:53 +#: lib/block_scout_web/templates/transaction_log/_logs.html.eex:43 +#: lib/block_scout_web/templates/transaction_log/_logs.html.eex:51 +#: lib/block_scout_web/templates/transaction_log/_logs.html.eex:66 +#: lib/block_scout_web/templates/transaction_log/_logs.html.eex:82 +#, elixir-autogen, elixir-format +msgid "Decoded" +msgstr "" + +#: lib/block_scout_web/views/internal_transaction_view.ex:23 +#, elixir-autogen, elixir-format +msgid "Delegate Call" +msgstr "" + +#: lib/block_scout_web/templates/address_contract/index.html.eex:211 +#: lib/block_scout_web/templates/address_contract/index.html.eex:219 +#, elixir-autogen, elixir-format +msgid "Deployed ByteCode" +msgstr "" + +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:53 +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:188 +#: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:60 +#: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:150 +#, elixir-autogen, elixir-format +msgid "Description" +msgstr "" + +#: lib/block_scout_web/templates/account/public_tags_request/form.html.eex:56 +#, elixir-autogen, elixir-format +msgid "Description*" +msgstr "" + +#: lib/block_scout_web/templates/address/overview.html.eex:30 +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:166 +#: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:127 +#, elixir-autogen, elixir-format +msgid "Details" +msgstr "" + +#: lib/block_scout_web/templates/block/overview.html.eex:159 +#, elixir-autogen, elixir-format +msgid "Difficulty" +msgstr "" + +#: lib/block_scout_web/templates/address_contract/index.html.eex:181 +#, elixir-autogen, elixir-format +msgid "Displaying the init data provided of the creating transaction." +msgstr "" + +#: lib/block_scout_web/templates/common_components/_csv_export_button.html.eex:4 +#: lib/block_scout_web/templates/csv_export/index.html.eex:33 +#, elixir-autogen, elixir-format +msgid "Download" +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:72 +#, elixir-autogen, elixir-format +msgid "Drop all Solidity contract source files into the drop zone." +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:72 +#, elixir-autogen, elixir-format +msgid "Drop all Solidity or Yul contract source files into the drop zone." +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification_via_json/new.html.eex:18 +#, elixir-autogen, elixir-format +msgid "Drop sources and metadata JSON file or click here" +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:67 +#, elixir-autogen, elixir-format +msgid "Drop sources or click here" +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification_via_standard_json_input/new.html.eex:28 +#, elixir-autogen, elixir-format +msgid "Drop the standard input JSON file or click here" +msgstr "" + +#: lib/block_scout_web/templates/account/public_tags_request/form.html.eex:27 +#, elixir-autogen, elixir-format +msgid "E-mail*" +msgstr "" + +#: lib/block_scout_web/templates/common_components/_minimal_proxy_pattern.html.eex:6 +#, elixir-autogen, elixir-format +msgid "EIP-1167" +msgstr "" + +#: lib/block_scout_web/views/transaction_view.ex:225 +#, elixir-autogen, elixir-format +msgid "ERC-1155 " +msgstr "" + +#: lib/block_scout_web/views/transaction_view.ex:223 +#, elixir-autogen, elixir-format +msgid "ERC-20 " +msgstr "" + +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:40 +#, elixir-autogen, elixir-format +msgid "ERC-20 tokens (beta)" +msgstr "" + +#: lib/block_scout_web/views/transaction_view.ex:226 +#, elixir-autogen, elixir-format, fuzzy +msgid "ERC-404 " +msgstr "" + +#: lib/block_scout_web/views/transaction_view.ex:224 +#, elixir-autogen, elixir-format +msgid "ERC-721 " +msgstr "" + +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:53 +#, elixir-autogen, elixir-format +msgid "ERC-721, ERC-1155 tokens (NFT) (beta)" +msgstr "" + +#: lib/block_scout_web/templates/api_docs/eth_rpc.html.eex:4 +#, elixir-autogen, elixir-format +msgid "ETH RPC API Documentation" +msgstr "" + +#: lib/block_scout_web/templates/address_contract/index.html.eex:82 +#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:30 +#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:22 +#, elixir-autogen, elixir-format +msgid "EVM Version" +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:34 +#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:26 +#, elixir-autogen, elixir-format +msgid "EVM version details" +msgstr "" + +#: lib/block_scout_web/views/block_transaction_view.ex:7 +#, elixir-autogen, elixir-format +msgid "Easy Cowboy! This block does not exist yet!" +msgstr "" + +#: lib/block_scout_web/templates/account/api_key/row.html.eex:16 +#: lib/block_scout_web/templates/account/custom_abi/row.html.eex:16 +#: lib/block_scout_web/templates/account/watchlist_address/row.html.eex:27 +#, elixir-autogen, elixir-format +msgid "Edit" +msgstr "" + +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:7 +#, elixir-autogen, elixir-format +msgid "Edit Watch list address" +msgstr "" + +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:71 +#, elixir-autogen, elixir-format +msgid "Email notifications" +msgstr "" + +#: lib/block_scout_web/templates/transaction/_emission_reward_tile.html.eex:5 +#, elixir-autogen, elixir-format +msgid "Emission Contract" +msgstr "" + +#: lib/block_scout_web/views/block_view.ex:73 +#, elixir-autogen, elixir-format +msgid "Emission Reward" +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:72 +#, elixir-autogen, elixir-format +msgid "Enter the Solidity Contract Code" +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification_vyper/new.html.eex:22 +#, elixir-autogen, elixir-format +msgid "Enter the Vyper Contract Code" +msgstr "" + +#: lib/block_scout_web/templates/internal_transaction/_tile.html.eex:11 +#: lib/block_scout_web/templates/transaction/_decoded_input_body.html.eex:10 +#, elixir-autogen, elixir-format +msgid "Error" +msgstr "" + +#: lib/block_scout_web/templates/transaction/_tile.html.eex:11 +#, elixir-autogen, elixir-format +msgid "Error in internal transactions" +msgstr "" + +#: lib/block_scout_web/templates/transaction/_decoded_input_body.html.eex:33 +#, elixir-autogen, elixir-format +msgid "Error rendering value" +msgstr "" + +#: lib/block_scout_web/templates/address/_balance_dropdown.html.eex:10 +#, elixir-autogen, elixir-format +msgid "Error trying to fetch balances." +msgstr "" + +#: lib/block_scout_web/views/transaction_view.ex:379 +#, elixir-autogen, elixir-format +msgid "Error: %{reason}" +msgstr "" + +#: lib/block_scout_web/views/transaction_view.ex:377 +#, elixir-autogen, elixir-format +msgid "Error: (Awaiting internal transactions for reason)" +msgstr "" + +#: lib/block_scout_web/templates/address/overview.html.eex:120 +#, elixir-autogen, elixir-format +msgid "Error: Could not determine contract creator." +msgstr "" + +#: lib/block_scout_web/templates/layout/_topnav.html.eex:120 +#, elixir-autogen, elixir-format +msgid "Eth RPC" +msgstr "" + +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:211 +#: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:164 +#, elixir-autogen, elixir-format +msgid "Example Value" +msgstr "" + +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:128 +#: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:99 +#, elixir-autogen, elixir-format +msgid "Execute" +msgstr "" + +#: lib/block_scout_web/templates/transaction/_decoded_input_body.html.eex:54 +#, elixir-autogen, elixir-format +msgid "Expand" +msgstr "" + +#: lib/block_scout_web/templates/csv_export/index.html.eex:14 +#, elixir-autogen, elixir-format +msgid "Export" +msgstr "" + +#: lib/block_scout_web/templates/csv_export/index.html.eex:10 +#, elixir-autogen, elixir-format +msgid "Export Data" +msgstr "" + +#: lib/block_scout_web/templates/address_contract/index.html.eex:248 +#, elixir-autogen, elixir-format +msgid "External libraries" +msgstr "" + +#: lib/block_scout_web/templates/transaction/_decoded_input.html.eex:40 +#, elixir-autogen, elixir-format +msgid "Failed to decode input data." +msgstr "" + +#: lib/block_scout_web/templates/address_logs/_logs.html.eex:35 +#: lib/block_scout_web/templates/transaction_log/_logs.html.eex:46 +#, elixir-autogen, elixir-format +msgid "Failed to decode log data." +msgstr "" + +#: lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex:22 +#, elixir-autogen, elixir-format +msgid "Fast" +msgstr "" + +#: lib/block_scout_web/templates/address/overview.html.eex:249 +#, elixir-autogen, elixir-format +msgid "Fetching gas used..." +msgstr "" + +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:112 +#, elixir-autogen, elixir-format +msgid "Fetching holders..." +msgstr "" + +#: lib/block_scout_web/templates/address/_balance_dropdown.html.eex:7 +#, elixir-autogen, elixir-format +msgid "Fetching tokens..." +msgstr "" + +#: lib/block_scout_web/templates/address/overview.html.eex:196 +#: lib/block_scout_web/templates/address/overview.html.eex:204 +#, elixir-autogen, elixir-format +msgid "Fetching transactions..." +msgstr "" + +#: lib/block_scout_web/templates/address/overview.html.eex:223 +#: lib/block_scout_web/templates/address/overview.html.eex:231 +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:123 +#, elixir-autogen, elixir-format +msgid "Fetching transfers..." +msgstr "" + +#: lib/block_scout_web/templates/verified_contracts/index.html.eex:15 +#, elixir-autogen, elixir-format +msgid "Filter by compiler:" +msgstr "" + +#: lib/block_scout_web/templates/admin/dashboard/index.html.eex:16 +#, elixir-autogen, elixir-format +msgid "For any existing contracts in the database, insert all ABI entries into the contract_methods table. Use this in case you have verified smart contracts before early March 2019 and you want other contracts with the same functions to show those ABI's as candidate matches." +msgstr "" + +#: lib/block_scout_web/templates/visualize_sol2uml/index.html.eex:7 +#, elixir-autogen, elixir-format +msgid "For contract" +msgstr "" + +#: lib/block_scout_web/templates/layout/_topnav.html.eex:44 +#, elixir-autogen, elixir-format +msgid "Forked Blocks (Reorgs)" +msgstr "" + +#: lib/block_scout_web/templates/layout/_footer.html.eex:45 +#, elixir-autogen, elixir-format +msgid "Forum" +msgstr "" + +#: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:38 +#: lib/block_scout_web/templates/address_token_transfer/index.html.eex:40 +#: lib/block_scout_web/templates/address_transaction/index.html.eex:34 +#: lib/block_scout_web/templates/transaction/overview.html.eex:246 +#: lib/block_scout_web/views/address_internal_transaction_view.ex:11 +#: lib/block_scout_web/views/address_token_transfer_view.ex:11 +#: lib/block_scout_web/views/address_transaction_view.ex:11 +#, elixir-autogen, elixir-format +msgid "From" +msgstr "" + +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:18 +#, elixir-autogen, elixir-format +msgid "GET" +msgstr "" + +#: lib/block_scout_web/templates/block/_tile.html.eex:67 +#: lib/block_scout_web/templates/block/overview.html.eex:189 +#: lib/block_scout_web/templates/transaction/overview.html.eex:430 +#, elixir-autogen, elixir-format +msgid "Gas Limit" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:404 +#, elixir-autogen, elixir-format, fuzzy +msgid "Gas Price" +msgstr "" + +#: lib/block_scout_web/templates/address/overview.html.eex:242 +#: lib/block_scout_web/templates/block/_tile.html.eex:73 +#: lib/block_scout_web/templates/block/overview.html.eex:180 +#, elixir-autogen, elixir-format +msgid "Gas Used" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:489 +#, elixir-autogen, elixir-format, fuzzy +msgid "Gas Used by Transaction" +msgstr "" + +#: lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex:3 +#: lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex:18 +#, elixir-autogen, elixir-format +msgid "Gas tracker" +msgstr "" + +#: lib/block_scout_web/templates/address/overview.html.eex:241 +#, elixir-autogen, elixir-format +msgid "Gas used by the address." +msgstr "" + +#: lib/block_scout_web/templates/block/overview.html.eex:60 +#, elixir-autogen, elixir-format +msgid "Genesis Block" +msgstr "" + +#: lib/block_scout_web/templates/layout/_footer.html.eex:24 +#, elixir-autogen, elixir-format +msgid "Github" +msgstr "" + +#: lib/block_scout_web/templates/common_components/_rap_pagination_container.html.eex:8 +#, elixir-autogen, elixir-format +msgid "Go to" +msgstr "" + +#: lib/block_scout_web/templates/layout/_topnav.html.eex:110 +#, elixir-autogen, elixir-format +msgid "GraphQL" +msgstr "" + +#: lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex:11 +#: lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex:20 +#: lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex:21 +#: lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex:22 +#: lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex:38 +#: lib/block_scout_web/views/block_view.ex:22 +#: lib/block_scout_web/views/wei_helper.ex:81 +#, elixir-autogen, elixir-format +msgid "Gwei" +msgstr "" + +#: lib/block_scout_web/templates/block/overview.html.eex:123 +#, elixir-autogen, elixir-format +msgid "Hash" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:553 +#: lib/block_scout_web/templates/transaction/overview.html.eex:557 +#, elixir-autogen, elixir-format +msgid "Hex (Default)" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:224 +#, elixir-autogen, elixir-format +msgid "Highlighted events of the transaction." +msgstr "" + +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:108 +#, elixir-autogen, elixir-format +msgid "Holders" +msgstr "" + +#: lib/block_scout_web/templates/tokens/index.html.eex:48 +#, elixir-autogen, elixir-format +msgid "Holders Count" +msgstr "" + +#: lib/block_scout_web/templates/api_docs/eth_rpc.html.eex:11 +#, elixir-autogen, elixir-format +msgid "However, in general, the" +msgstr "" + +#: lib/block_scout_web/templates/transaction/_decoded_input.html.eex:19 +#, elixir-autogen, elixir-format +msgid "IMPORTANT: This information is a best guess based on similar functions from other verified contracts." +msgstr "" + +#: lib/block_scout_web/templates/internal_transaction/_tile.html.eex:42 +#: lib/block_scout_web/templates/transaction/_tile.html.eex:92 +#, elixir-autogen, elixir-format +msgid "IN" +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:56 +#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:48 +#, elixir-autogen, elixir-format +msgid "If you enabled optimization during compilation, select yes." +msgstr "" + +#: lib/block_scout_web/templates/address/overview.html.eex:135 +#, elixir-autogen, elixir-format +msgid "Implementation" +msgstr "" + +#: lib/block_scout_web/templates/address/overview.html.eex:134 +#, elixir-autogen, elixir-format +msgid "Implementation address of the proxy contract." +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification_common_fields/_include_nightly_builds_field.html.eex:3 +#, elixir-autogen, elixir-format +msgid "Include nightly builds" +msgstr "" + +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:30 +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:43 +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:56 +#, elixir-autogen, elixir-format +msgid "Incoming" +msgstr "" + +#: lib/block_scout_web/templates/address_withdrawal/index.html.eex:29 +#: lib/block_scout_web/templates/block_withdrawal/index.html.eex:23 +#: lib/block_scout_web/templates/withdrawal/index.html.eex:23 +#, elixir-autogen, elixir-format, fuzzy +msgid "Index" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:537 +#, elixir-autogen, elixir-format +msgid "Index position of Transaction in the block." +msgstr "" + +#: lib/block_scout_web/templates/block/overview.html.eex:251 +#, elixir-autogen, elixir-format +msgid "Index position(s) of referenced stale blocks." +msgstr "" + +#: lib/block_scout_web/templates/log/_data_decoded_view.html.eex:6 +#, elixir-autogen, elixir-format +msgid "Indexed?" +msgstr "" + +#: lib/block_scout_web/templates/transaction/_decoded_input.html.eex:3 +#, elixir-autogen, elixir-format +msgid "Input" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:265 +#, elixir-autogen, elixir-format +msgid "Interacted With (To)" +msgstr "" + +#: lib/block_scout_web/templates/internal_transaction/_tile.html.eex:7 +#, elixir-autogen, elixir-format +msgid "Internal Transaction" +msgstr "" + +#: lib/block_scout_web/templates/address/_tabs.html.eex:36 +#: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:17 +#: lib/block_scout_web/templates/transaction/_tabs.html.eex:11 +#: lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex:6 +#: lib/block_scout_web/views/address_view.ex:347 +#: lib/block_scout_web/views/transaction_view.ex:552 +#, elixir-autogen, elixir-format +msgid "Internal Transactions" +msgstr "" + +#: lib/block_scout_web/templates/internal_server_error/index.html.eex:7 +#, elixir-autogen, elixir-format +msgid "Internal server error" +msgstr "" + +#: lib/block_scout_web/views/internal_transaction_view.ex:25 +#, elixir-autogen, elixir-format +msgid "Invalid" +msgstr "" + +#: lib/block_scout_web/templates/tokens/inventory/index.html.eex:16 +#: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:19 +#: lib/block_scout_web/views/tokens/overview_view.ex:42 +#, elixir-autogen, elixir-format +msgid "Inventory" +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification_common_fields/_yul_contracts_switcher.html.eex:3 +#, elixir-autogen, elixir-format +msgid "Is Yul contract" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:204 +#, elixir-autogen, elixir-format, fuzzy +msgid "L1 Block" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:523 +#: lib/block_scout_web/templates/transaction/overview.html.eex:524 +#, elixir-autogen, elixir-format +msgid "L1 Fee Scalar" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:512 +#: lib/block_scout_web/templates/transaction/overview.html.eex:513 +#, elixir-autogen, elixir-format, fuzzy +msgid "L1 Gas Price" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:501 +#: lib/block_scout_web/templates/transaction/overview.html.eex:502 +#, elixir-autogen, elixir-format +msgid "L1 Gas Used by Transaction" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:426 +#, elixir-autogen, elixir-format, fuzzy +msgid "L2 Gas Limit" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:400 +#, elixir-autogen, elixir-format, fuzzy +msgid "L2 Gas Price" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:485 +#, elixir-autogen, elixir-format +msgid "L2 Gas Used by Transaction" +msgstr "" + +#: lib/block_scout_web/templates/verified_contracts/_stats.html.eex:13 +#: lib/block_scout_web/templates/verified_contracts/_stats.html.eex:26 +#, elixir-autogen, elixir-format +msgid "Last 24h" +msgstr "" + +#: lib/block_scout_web/templates/address/overview.html.eex:260 +#, elixir-autogen, elixir-format +msgid "Last Balance Update" +msgstr "" + +#: lib/block_scout_web/templates/account/api_key/index.html.eex:12 +#: lib/block_scout_web/templates/account/api_key/index.html.eex:18 +#, elixir-autogen, elixir-format +msgid "Learn more" +msgstr "" + +#: lib/block_scout_web/templates/layout/app.html.eex:49 +#, elixir-autogen, elixir-format +msgid "Less than" +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification_common_fields/_library_address.html.eex:4 +#: lib/block_scout_web/templates/address_contract_verification_common_fields/_library_name.html.eex:4 +#, elixir-autogen, elixir-format +msgid "Library" +msgstr "" + +#: lib/block_scout_web/templates/address/_validator_metadata_modal.html.eex:24 +#, elixir-autogen, elixir-format +msgid "License Expires" +msgstr "" + +#: lib/block_scout_web/templates/address/_validator_metadata_modal.html.eex:10 +#, elixir-autogen, elixir-format +msgid "License ID" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:351 +#, elixir-autogen, elixir-format +msgid "List of ERC-1155 tokens created in the transaction." +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:335 +#, elixir-autogen, elixir-format +msgid "List of token burnt in the transaction." +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:318 +#, elixir-autogen, elixir-format +msgid "List of token minted in the transaction." +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:302 +#, elixir-autogen, elixir-format +msgid "List of token transferred in the transaction." +msgstr "" + +#: lib/block_scout_web/templates/address_coin_balance/index.html.eex:18 +#, elixir-autogen, elixir-format +msgid "Loading chart..." +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:77 +#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:109 +#: lib/block_scout_web/templates/address_contract_verification_via_json/new.html.eex:35 +#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:99 +#: lib/block_scout_web/templates/address_contract_verification_via_standard_json_input/new.html.eex:49 +#: lib/block_scout_web/templates/address_contract_verification_vyper/new.html.eex:45 +#: lib/block_scout_web/templates/address_read_contract/index.html.eex:41 +#: lib/block_scout_web/templates/address_read_contract/index.html.eex:49 +#: lib/block_scout_web/templates/address_read_proxy/index.html.eex:12 +#: lib/block_scout_web/templates/address_write_contract/index.html.eex:39 +#: lib/block_scout_web/templates/address_write_contract/index.html.eex:47 +#: lib/block_scout_web/templates/address_write_proxy/index.html.eex:12 +#: lib/block_scout_web/templates/tokens/contract/index.html.eex:17 +#, elixir-autogen, elixir-format +msgid "Loading..." +msgstr "" + +#: lib/block_scout_web/templates/log/_data_decoded_view.html.eex:2 +#, elixir-autogen, elixir-format +msgid "Log Data" +msgstr "" + +#: lib/block_scout_web/templates/transaction_log/_logs.html.eex:140 +#, elixir-autogen, elixir-format +msgid "Log Index" +msgstr "" + +#: lib/block_scout_web/templates/address/_tabs.html.eex:49 +#: lib/block_scout_web/templates/address_logs/index.html.eex:10 +#: lib/block_scout_web/templates/transaction/_tabs.html.eex:17 +#: lib/block_scout_web/templates/transaction_log/index.html.eex:8 +#: lib/block_scout_web/views/address_view.ex:357 +#: lib/block_scout_web/views/transaction_view.ex:553 +#, elixir-autogen, elixir-format +msgid "Logs" +msgstr "" + +#: lib/block_scout_web/templates/layout/_footer.html.eex:54 +#, elixir-autogen, elixir-format +msgid "Main Networks" +msgstr "" + +#: lib/block_scout_web/templates/chain/show.html.eex:53 +#: lib/block_scout_web/templates/layout/app.html.eex:50 +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:85 +#: lib/block_scout_web/views/address_view.ex:147 +#, elixir-autogen, elixir-format +msgid "Market Cap" +msgstr "" + +#: lib/block_scout_web/templates/verified_contracts/index.html.eex:84 +#, elixir-autogen, elixir-format +msgid "Market cap" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:440 +#, elixir-autogen, elixir-format +msgid "Max Fee per Gas" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:450 +#, elixir-autogen, elixir-format +msgid "Max Priority Fee per Gas" +msgstr "" + +#: lib/block_scout_web/views/transaction_view.ex:331 +#, elixir-autogen, elixir-format +msgid "Max of" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:425 +#, elixir-autogen, elixir-format +msgid "Maximum gas amount approved for the transaction on L2." +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:429 +#, elixir-autogen, elixir-format, fuzzy +msgid "Maximum gas amount approved for the transaction." +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:439 +#, elixir-autogen, elixir-format +msgid "Maximum total amount per unit of gas a user is willing to pay for a transaction, including base fee and priority fee." +msgstr "" + +#: lib/block_scout_web/templates/tokens/instance/metadata/index.html.eex:18 +#: lib/block_scout_web/templates/tokens/instance/overview/_tabs.html.eex:10 +#: lib/block_scout_web/views/tokens/instance/overview_view.ex:75 +#, elixir-autogen, elixir-format +msgid "Metadata" +msgstr "" + +#: lib/block_scout_web/templates/transaction/_decoded_input_body.html.eex:5 +#, elixir-autogen, elixir-format +msgid "Method Id" +msgstr "" + +#: lib/block_scout_web/templates/block/_tile.html.eex:41 +#: lib/block_scout_web/templates/block/overview.html.eex:98 +#: lib/block_scout_web/templates/chain/_block.html.eex:16 +#: lib/block_scout_web/templates/transaction_state/_state_change.html.eex:22 +#, elixir-autogen, elixir-format +msgid "Miner" +msgstr "" + +#: lib/block_scout_web/views/block_view.ex:63 +#: lib/block_scout_web/views/block_view.ex:68 +#, elixir-autogen, elixir-format +msgid "Miner Reward" +msgstr "" + +#: lib/block_scout_web/templates/common_components/_minimal_proxy_pattern.html.eex:3 +#, elixir-autogen, elixir-format +msgid "Minimal Proxy Contract for" +msgstr "" + +#: lib/block_scout_web/templates/block/overview.html.eex:208 +#, elixir-autogen, elixir-format +msgid "Minimum fee required per unit of gas. Fee adjusts based on network congestion." +msgstr "" + +#: lib/block_scout_web/templates/transaction/_actions.html.eex:92 +#, elixir-autogen, elixir-format +msgid "Mint of %{address} To %{to}" +msgstr "" + +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:223 +#, elixir-autogen, elixir-format +msgid "Model" +msgstr "" + +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:58 +#, elixir-autogen, elixir-format +msgid "Module" +msgstr "" + +#: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:12 +#, elixir-autogen, elixir-format +msgid "More internal transactions have come in" +msgstr "" + +#: lib/block_scout_web/templates/address_transaction/index.html.eex:46 +#: lib/block_scout_web/templates/chain/show.html.eex:219 +#: lib/block_scout_web/templates/pending_transaction/index.html.eex:13 +#: lib/block_scout_web/templates/transaction/index.html.eex:19 +#, elixir-autogen, elixir-format +msgid "More transactions have come in" +msgstr "" + +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:63 +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:74 +#, elixir-autogen, elixir-format +msgid "Must be set to:" +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:22 +#, elixir-autogen, elixir-format +msgid "Must match the name specified in the code. For example, in contract MyContract {..} MyContract is the contract name." +msgstr "" + +#: lib/block_scout_web/templates/tokens/_tile.html.eex:40 +#: lib/block_scout_web/templates/verified_contracts/_contract.html.eex:21 +#: lib/block_scout_web/templates/verified_contracts/_contract.html.eex:61 +#, elixir-autogen, elixir-format +msgid "N/A" +msgstr "" + +#: lib/block_scout_web/templates/block/overview.html.eex:116 +#, elixir-autogen, elixir-format +msgid "N/A bytes" +msgstr "" + +#: lib/block_scout_web/templates/account/api_key/form.html.eex:19 +#: lib/block_scout_web/templates/account/api_key/index.html.eex:28 +#: lib/block_scout_web/templates/account/custom_abi/form.html.eex:13 +#: lib/block_scout_web/templates/account/custom_abi/index.html.eex:28 +#: lib/block_scout_web/templates/account/tag_address/form.html.eex:18 +#: lib/block_scout_web/templates/account/tag_address/index.html.eex:22 +#: lib/block_scout_web/templates/account/tag_transaction/form.html.eex:18 +#: lib/block_scout_web/templates/account/tag_transaction/index.html.eex:22 +#: lib/block_scout_web/templates/account/watchlist/show.html.eex:22 +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:19 +#: lib/block_scout_web/templates/address_contract_verification_common_fields/_library_name.html.eex:4 +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:52 +#: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:59 +#: lib/block_scout_web/templates/log/_data_decoded_view.html.eex:4 +#: lib/block_scout_web/templates/transaction/_decoded_input_body.html.eex:21 +#, elixir-autogen, elixir-format +msgid "Name" +msgstr "" + +#: lib/block_scout_web/templates/account/api_key/form.html.eex:20 +#, elixir-autogen, elixir-format +msgid "Name this API key" +msgstr "" + +#: lib/block_scout_web/templates/account/custom_abi/form.html.eex:14 +#, elixir-autogen, elixir-format +msgid "Name this Custom ABI" +msgstr "" + +#: lib/block_scout_web/templates/account/tag_address/form.html.eex:19 +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:20 +#, elixir-autogen, elixir-format +msgid "Name this address" +msgstr "" + +#: lib/block_scout_web/templates/account/tag_transaction/form.html.eex:19 +#, elixir-autogen, elixir-format +msgid "Name this transaction" +msgstr "" + +#: lib/block_scout_web/templates/address_token/overview.html.eex:44 +#, elixir-autogen, elixir-format +msgid "Net Worth" +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:5 +#, elixir-autogen, elixir-format +msgid "New Smart Contract Verification" +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification_via_standard_json_input/new.html.eex:9 +#, elixir-autogen, elixir-format +msgid "New Smart Contract Verification via Standard input JSON" +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification_via_json/new.html.eex:5 +#, elixir-autogen, elixir-format +msgid "New Smart Contract Verification via metadata JSON" +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:9 +#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:7 +#, elixir-autogen, elixir-format +msgid "New Solidity Smart Contract Verification" +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:7 +#, elixir-autogen, elixir-format +msgid "New Solidity/Yul Smart Contract Verification" +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification_vyper/new.html.eex:7 +#, elixir-autogen, elixir-format +msgid "New Vyper Smart Contract Verification" +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:80 +#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:87 +#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:95 +#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:103 +#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:111 +#, elixir-autogen, elixir-format +msgid "Next" +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification_common_fields/_fetch_constructor_args.html.eex:9 +#: lib/block_scout_web/templates/address_contract_verification_common_fields/_include_nightly_builds_field.html.eex:9 +#: lib/block_scout_web/templates/address_contract_verification_common_fields/_yul_contracts_switcher.html.eex:9 +#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:46 +#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:38 +#, elixir-autogen, elixir-format +msgid "No" +msgstr "" + +#: lib/block_scout_web/templates/transaction_raw_trace/_card_body.html.eex:15 +#, elixir-autogen, elixir-format +msgid "No trace entries found." +msgstr "" + +#: lib/block_scout_web/templates/block/overview.html.eex:198 +#: lib/block_scout_web/templates/transaction/overview.html.eex:535 +#, elixir-autogen, elixir-format +msgid "Nonce" +msgstr "" + +#: lib/block_scout_web/templates/tokens/inventory/_token.html.eex:11 +#, elixir-autogen, elixir-format +msgid "Not unique Token" +msgstr "" + +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:107 +#, elixir-autogen, elixir-format +msgid "Number of accounts holding the token" +msgstr "" + +#: lib/block_scout_web/templates/address/overview.html.eex:276 +#, elixir-autogen, elixir-format +msgid "Number of blocks validated by this validator." +msgstr "" + +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:130 +#, elixir-autogen, elixir-format +msgid "Number of digits that come after the decimal place when displaying token value" +msgstr "" + +#: lib/block_scout_web/templates/address/overview.html.eex:187 +#, elixir-autogen, elixir-format +msgid "Number of transactions related to this address." +msgstr "" + +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:118 +#, elixir-autogen, elixir-format +msgid "Number of transfers for the token" +msgstr "" + +#: lib/block_scout_web/templates/address/overview.html.eex:214 +#, elixir-autogen, elixir-format +msgid "Number of transfers to/from this address." +msgstr "" + +#: lib/block_scout_web/templates/internal_transaction/_tile.html.eex:40 +#: lib/block_scout_web/templates/transaction/_tile.html.eex:88 +#, elixir-autogen, elixir-format +msgid "OUT" +msgstr "" + +#: lib/block_scout_web/templates/common_components/_rap_pagination_container.html.eex:13 +#, elixir-autogen, elixir-format +msgid "Only the first" +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:40 +#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:32 +#: lib/block_scout_web/templates/verified_contracts/index.html.eex:75 +#, elixir-autogen, elixir-format +msgid "Optimization" +msgstr "" + +#: lib/block_scout_web/templates/address_contract/index.html.eex:67 +#, elixir-autogen, elixir-format +msgid "Optimization enabled" +msgstr "" + +#: lib/block_scout_web/templates/address_contract/index.html.eex:76 +#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:62 +#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:54 +#, elixir-autogen, elixir-format +msgid "Optimization runs" +msgstr "" + +#: lib/block_scout_web/templates/layout/_footer.html.eex:78 +#, elixir-autogen, elixir-format +msgid "Other Explorers" +msgstr "" + +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:35 +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:48 +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:61 +#, elixir-autogen, elixir-format +msgid "Outgoing" +msgstr "" + +#: lib/block_scout_web/templates/tokens/inventory/_token.html.eex:24 +#, elixir-autogen, elixir-format +msgid "Owner Address" +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:77 +#, elixir-autogen, elixir-format +msgid "POA solidity flattener or the" +msgstr "" + +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:19 +#: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:26 +#, elixir-autogen, elixir-format +msgid "POST" +msgstr "" + +#: lib/block_scout_web/templates/common_components/_pagination_container.html.eex:41 +#, elixir-autogen, elixir-format +msgid "Page" +msgstr "" + +#: lib/block_scout_web/templates/page_not_found/index.html.eex:7 +#, elixir-autogen, elixir-format +msgid "Page not found" +msgstr "" + +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:33 +#: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:40 +#, elixir-autogen, elixir-format +msgid "Parameters" +msgstr "" + +#: lib/block_scout_web/templates/block/overview.html.eex:139 +#, elixir-autogen, elixir-format +msgid "Parent Hash" +msgstr "" + +#: lib/block_scout_web/templates/layout/_topnav.html.eex:63 +#: lib/block_scout_web/views/transaction_view.ex:374 +#: lib/block_scout_web/views/transaction_view.ex:419 +#: lib/block_scout_web/views/transaction_view.ex:427 +#, elixir-autogen, elixir-format +msgid "Pending" +msgstr "" + +#: lib/block_scout_web/templates/pending_transaction/index.html.eex:5 +#, elixir-autogen, elixir-format +msgid "Pending Transactions" +msgstr "" + +#: lib/block_scout_web/templates/address/_custom_view_df_title.html.eex:9 +#: lib/block_scout_web/templates/address/_custom_view_df_title.html.eex:13 +#, elixir-autogen, elixir-format +msgid "Play" +msgstr "" + +#: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:22 +#: lib/block_scout_web/templates/layout/app.html.eex:100 +#, elixir-autogen, elixir-format +msgid "Please confirm your email address to use the My Account feature." +msgstr "" + +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:68 +#, elixir-autogen, elixir-format +msgid "Please select notification methods:" +msgstr "" + +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:24 +#, elixir-autogen, elixir-format +msgid "Please select what types of notifications you will receive:" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:537 +#, elixir-autogen, elixir-format +msgid "Position" +msgstr "" + +#: lib/block_scout_web/templates/block/overview.html.eex:256 +#, elixir-autogen, elixir-format +msgid "Position %{index}" +msgstr "" + +#: lib/block_scout_web/templates/transaction/_decoded_input.html.eex:18 +#, elixir-autogen, elixir-format +msgid "Potential matches from contract method database:" +msgstr "" + +#: lib/block_scout_web/templates/transaction/_decoded_input.html.eex:32 +#, elixir-autogen, elixir-format +msgid "Potential matches from our contract method database:" +msgstr "" + +#: lib/block_scout_web/templates/layout/_search.html.eex:27 +#, elixir-autogen, elixir-format +msgid "Press / and focus will be moved to the search field" +msgstr "" + +#: lib/block_scout_web/templates/chain/show.html.eex:42 +#: lib/block_scout_web/templates/layout/app.html.eex:51 +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:96 +#, elixir-autogen, elixir-format +msgid "Price" +msgstr "" + +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:95 +#, elixir-autogen, elixir-format +msgid "Price per token on the exchanges" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:399 +#, elixir-autogen, elixir-format +msgid "Price per unit of gas specified by the sender on L2. Higher gas prices can prioritize transaction inclusion during times of high usage." +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:403 +#, elixir-autogen, elixir-format, fuzzy +msgid "Price per unit of gas specified by the sender. Higher gas prices can prioritize transaction inclusion during times of high usage." +msgstr "" + +#: lib/block_scout_web/templates/block/overview.html.eex:227 +#: lib/block_scout_web/templates/transaction/overview.html.eex:460 +#, elixir-autogen, elixir-format +msgid "Priority Fee / Tip" +msgstr "" + +#: lib/block_scout_web/templates/block/_tile.html.eex:62 +#, elixir-autogen, elixir-format +msgid "Priority Fees" +msgstr "" + +#: lib/block_scout_web/templates/account/common/_nav.html.eex:4 +#: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:14 +#, elixir-autogen, elixir-format +msgid "Profile" +msgstr "" + +#: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:20 +#, elixir-autogen, elixir-format +msgid "Public Tags" +msgstr "" + +#: lib/block_scout_web/templates/account/public_tags_request/index.html.eex:20 +#, elixir-autogen, elixir-format +msgid "Public tag" +msgstr "" + +#: lib/block_scout_web/templates/account/common/_nav.html.eex:22 +#: lib/block_scout_web/templates/account/public_tags_request/index.html.eex:7 +#, elixir-autogen, elixir-format +msgid "Public tags" +msgstr "" + +#: lib/block_scout_web/templates/account/public_tags_request/form.html.eex:50 +#, elixir-autogen, elixir-format +msgid "Public tags* (2 tags maximum, please use \";\" as a divider)" +msgstr "" + +#: lib/block_scout_web/templates/common_components/_btn_qr_code.html.eex:10 +#: lib/block_scout_web/templates/common_components/_modal_qr_code.html.eex:5 +#: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:83 +#, elixir-autogen, elixir-format +msgid "QR Code" +msgstr "" + +#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:106 +#, elixir-autogen, elixir-format +msgid "Query" +msgstr "" + +#: lib/block_scout_web/templates/layout/_topnav.html.eex:115 +#, elixir-autogen, elixir-format +msgid "RPC" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:546 +#, elixir-autogen, elixir-format +msgid "Raw Input" +msgstr "" + +#: lib/block_scout_web/templates/transaction/_tabs.html.eex:24 +#: lib/block_scout_web/templates/transaction_raw_trace/_card_body.html.eex:1 +#: lib/block_scout_web/views/transaction_view.ex:554 +#, elixir-autogen, elixir-format +msgid "Raw Trace" +msgstr "" + +#: lib/block_scout_web/templates/address/_tabs.html.eex:81 +#: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:27 +#: lib/block_scout_web/views/address_view.ex:351 +#: lib/block_scout_web/views/tokens/overview_view.ex:41 +#, elixir-autogen, elixir-format +msgid "Read Contract" +msgstr "" + +#: lib/block_scout_web/templates/address/_tabs.html.eex:88 +#: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:41 +#: lib/block_scout_web/views/address_view.ex:352 +#, elixir-autogen, elixir-format +msgid "Read Proxy" +msgstr "" + +#: lib/block_scout_web/templates/common_components/_pagination_container.html.eex:13 +#, elixir-autogen, elixir-format +msgid "Records" +msgstr "" + +#: lib/block_scout_web/templates/account/api_key/row.html.eex:13 +#: lib/block_scout_web/templates/account/custom_abi/row.html.eex:13 +#, elixir-autogen, elixir-format +msgid "Remove" +msgstr "" + +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:77 +#, elixir-autogen, elixir-format +msgid "Remove from Watch list" +msgstr "" + +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:155 +#, elixir-autogen, elixir-format +msgid "Request URL" +msgstr "" + +#: lib/block_scout_web/templates/account/public_tags_request/form.html.eex:7 +#, elixir-autogen, elixir-format +msgid "Request a public tag/label" +msgstr "" + +#: lib/block_scout_web/templates/error422/index.html.eex:7 +#, elixir-autogen, elixir-format +msgid "Request cannot be processed" +msgstr "" + +#: lib/block_scout_web/templates/account/public_tags_request/index.html.eex:37 +#, elixir-autogen, elixir-format +msgid "Request to add public tag" +msgstr "" + +#: lib/block_scout_web/templates/account/public_tags_request/form.html.eex:7 +#, elixir-autogen, elixir-format +msgid "Request to edit a public tag/label" +msgstr "" + +#: lib/block_scout_web/templates/layout/app.html.eex:100 +#, elixir-autogen, elixir-format +msgid "Resend verification email" +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:112 +#: lib/block_scout_web/templates/address_contract_verification_via_json/new.html.eex:38 +#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:104 +#: lib/block_scout_web/templates/address_contract_verification_via_standard_json_input/new.html.eex:52 +#: lib/block_scout_web/templates/address_contract_verification_vyper/new.html.eex:48 +#, elixir-autogen, elixir-format +msgid "Reset" +msgstr "" + +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:173 +#: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:134 +#, elixir-autogen, elixir-format +msgid "Response Body" +msgstr "" + +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:185 +#: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:147 +#, elixir-autogen, elixir-format +msgid "Responses" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:98 +#, elixir-autogen, elixir-format +msgid "Result" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:138 +#, elixir-autogen, elixir-format +msgid "Revert reason" +msgstr "" + +#: lib/block_scout_web/templates/block/_tile.html.eex:52 +#: lib/block_scout_web/templates/chain/_block.html.eex:27 +#: lib/block_scout_web/views/internal_transaction_view.ex:29 +#, elixir-autogen, elixir-format +msgid "Reward" +msgstr "" + +#: lib/block_scout_web/templates/admin/dashboard/index.html.eex:21 +#, elixir-autogen, elixir-format +msgid "Run" +msgstr "" + +#: lib/block_scout_web/templates/account/api_key/form.html.eex:26 +#: lib/block_scout_web/templates/account/custom_abi/form.html.eex:31 +#: lib/block_scout_web/templates/account/tag_address/form.html.eex:25 +#: lib/block_scout_web/templates/account/tag_transaction/form.html.eex:25 +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:83 +#, elixir-autogen, elixir-format +msgid "Save" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:226 +#, elixir-autogen, elixir-format +msgid "Scroll to see more" +msgstr "" + +#: lib/block_scout_web/templates/address_logs/index.html.eex:16 +#: lib/block_scout_web/templates/layout/_search.html.eex:34 +#, elixir-autogen, elixir-format +msgid "Search" +msgstr "" + +#: lib/block_scout_web/templates/search/results.html.eex:17 +#, elixir-autogen, elixir-format +msgid "Search Results" +msgstr "" + +#: lib/block_scout_web/templates/layout/_search.html.eex:3 +#, elixir-autogen, elixir-format +msgid "Search by address, token symbol name, transaction hash, or block number" +msgstr "" + +#: lib/block_scout_web/templates/address_token_balance/_token_balances.html.eex:47 +#, elixir-autogen, elixir-format +msgid "Search tokens" +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification_common_fields/_yul_contracts_switcher.html.eex:19 +#, elixir-autogen, elixir-format +msgid "Select Yes if you want to verify Yul contract." +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification_common_fields/_include_nightly_builds_field.html.eex:19 +#, elixir-autogen, elixir-format +msgid "Select yes if you want to show nightly builds." +msgstr "" + +#: lib/block_scout_web/views/internal_transaction_view.ex:28 +#, elixir-autogen, elixir-format +msgid "Self-Destruct" +msgstr "" + +#: lib/block_scout_web/templates/account/public_tags_request/form.html.eex:63 +#, elixir-autogen, elixir-format +msgid "Send request" +msgstr "" + +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:163 +#: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:124 +#, elixir-autogen, elixir-format +msgid "Server Response" +msgstr "" + +#: lib/block_scout_web/templates/common_components/_pagination_container.html.eex:7 +#, elixir-autogen, elixir-format +msgid "Show" +msgstr "" + +#: lib/block_scout_web/templates/common_components/_btn_qr_code.html.eex:11 +#, elixir-autogen, elixir-format +msgid "Show QR Code" +msgstr "" + +#: lib/block_scout_web/templates/address_token/overview.html.eex:52 +#, elixir-autogen, elixir-format +msgid "Shows the current" +msgstr "" + +#: lib/block_scout_web/templates/address_token/overview.html.eex:59 +#, elixir-autogen, elixir-format +msgid "Shows the tokens held in the address (includes ERC-20, ERC-721 and ERC-1155)." +msgstr "" + +#: lib/block_scout_web/templates/address_token/overview.html.eex:66 +#, elixir-autogen, elixir-format +msgid "Shows the total CRC balance in the address." +msgstr "" + +#: lib/block_scout_web/templates/address_token/overview.html.eex:45 +#, elixir-autogen, elixir-format +msgid "Shows total assets held in the address" +msgstr "" + +#: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:33 +#, elixir-autogen, elixir-format +msgid "Sign in" +msgstr "" + +#: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:24 +#, elixir-autogen, elixir-format +msgid "Sign out" +msgstr "" + +#: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:11 +#, elixir-autogen, elixir-format +msgid "Signed in as " +msgstr "" + +#: lib/block_scout_web/templates/block/overview.html.eex:114 +#, elixir-autogen, elixir-format +msgid "Size" +msgstr "" + +#: lib/block_scout_web/templates/block/overview.html.eex:113 +#, elixir-autogen, elixir-format +msgid "Size of the block in bytes." +msgstr "" + +#: lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex:20 +#, elixir-autogen, elixir-format +msgid "Slow" +msgstr "" + +#: lib/block_scout_web/templates/account/public_tags_request/index.html.eex:21 +#, elixir-autogen, elixir-format +msgid "Smart contract / Address" +msgstr "" + +#: lib/block_scout_web/templates/account/public_tags_request/address_field.html.eex:4 +#: lib/block_scout_web/templates/account/public_tags_request/address_field.html.eex:5 +#, elixir-autogen, elixir-format +msgid "Smart contract / Address (0x...)" +msgstr "" + +#: lib/block_scout_web/templates/verified_contracts/_contract.html.eex:28 +#: lib/block_scout_web/templates/verified_contracts/index.html.eex:26 +#: lib/block_scout_web/views/verified_contracts_view.ex:10 +#, elixir-autogen, elixir-format +msgid "Solidity" +msgstr "" + +#: lib/block_scout_web/templates/address_coin_balance/index.html.eex:30 +#: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:50 +#: lib/block_scout_web/templates/address_logs/index.html.eex:23 +#: lib/block_scout_web/templates/address_token/index.html.eex:60 +#: lib/block_scout_web/templates/address_token_transfer/index.html.eex:58 +#: lib/block_scout_web/templates/address_transaction/index.html.eex:50 +#: lib/block_scout_web/templates/address_validation/index.html.eex:20 +#: lib/block_scout_web/templates/address_withdrawal/index.html.eex:20 +#: lib/block_scout_web/templates/block_transaction/index.html.eex:14 +#: lib/block_scout_web/templates/block_withdrawal/index.html.eex:14 +#: lib/block_scout_web/templates/chain/show.html.eex:160 +#: lib/block_scout_web/templates/pending_transaction/index.html.eex:18 +#: lib/block_scout_web/templates/tokens/holder/index.html.eex:24 +#: lib/block_scout_web/templates/tokens/instance/holder/index.html.eex:23 +#: lib/block_scout_web/templates/tokens/instance/transfer/index.html.eex:23 +#: lib/block_scout_web/templates/tokens/inventory/index.html.eex:23 +#: lib/block_scout_web/templates/tokens/transfer/index.html.eex:22 +#: lib/block_scout_web/templates/transaction/index.html.eex:25 +#: lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex:13 +#: lib/block_scout_web/templates/transaction_log/index.html.eex:15 +#: lib/block_scout_web/templates/transaction_state/index.html.eex:13 +#: lib/block_scout_web/templates/transaction_token_transfer/index.html.eex:14 +#: lib/block_scout_web/templates/verified_contracts/index.html.eex:51 +#: lib/block_scout_web/templates/withdrawal/index.html.eex:14 +#, elixir-autogen, elixir-format +msgid "Something went wrong, click to reload." +msgstr "" + +#: lib/block_scout_web/templates/chain/show.html.eex:225 +#, elixir-autogen, elixir-format +msgid "Something went wrong, click to retry." +msgstr "" + +#: lib/block_scout_web/templates/transaction/not_found.html.eex:6 +#, elixir-autogen, elixir-format +msgid "Sorry, we are unable to locate this transaction hash" +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:63 +#, elixir-autogen, elixir-format +msgid "Sources *.sol files" +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:63 +#, elixir-autogen, elixir-format +msgid "Sources *.sol or *.yul files" +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification_via_json/new.html.eex:14 +#, elixir-autogen, elixir-format +msgid "Sources and Metadata JSON" +msgstr "" + +#: lib/block_scout_web/templates/layout/_topnav.html.eex:136 +#, elixir-autogen, elixir-format +msgid "Stakes" +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification_via_standard_json_input/new.html.eex:24 +#, elixir-autogen, elixir-format +msgid "Standard Input JSON" +msgstr "" + +#: lib/block_scout_web/templates/transaction/_tabs.html.eex:29 +#: lib/block_scout_web/templates/transaction_state/index.html.eex:6 +#: lib/block_scout_web/views/transaction_view.ex:555 +#, elixir-autogen, elixir-format +msgid "State changes" +msgstr "" + +#: lib/block_scout_web/views/internal_transaction_view.ex:24 +#, elixir-autogen, elixir-format +msgid "Static Call" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:110 +#, elixir-autogen, elixir-format +msgid "Status" +msgstr "" + +#: lib/block_scout_web/templates/account/public_tags_request/index.html.eex:22 +#, elixir-autogen, elixir-format +msgid "Submission date" +msgstr "" + +#: lib/block_scout_web/templates/layout/_footer.html.eex:41 +#, elixir-autogen, elixir-format +msgid "Submit an Issue" +msgstr "" + +#: lib/block_scout_web/templates/transaction/_emission_reward_tile.html.eex:8 +#: lib/block_scout_web/views/transaction_view.ex:376 +#, elixir-autogen, elixir-format +msgid "Success" +msgstr "" + +#: lib/block_scout_web/templates/transaction/_pending_tile.html.eex:21 +#: lib/block_scout_web/templates/transaction/_tile.html.eex:52 +#, elixir-autogen, elixir-format +msgid "TX Fee" +msgstr "" + +#: lib/block_scout_web/templates/layout/_footer.html.eex:31 +#, elixir-autogen, elixir-format +msgid "Telegram" +msgstr "" + +#: lib/block_scout_web/templates/layout/_footer.html.eex:67 +#, elixir-autogen, elixir-format +msgid "Test Networks" +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification_common_fields/_library_first.html.eex:9 +#, elixir-autogen, elixir-format +msgid "The 0x library address. This can be found in the generated json file or Truffle output (if using truffle)." +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:34 +#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:26 +#, elixir-autogen, elixir-format +msgid "The EVM version the contract is written for. If the bytecode does not match the version, we try to verify using the latest EVM version." +msgstr "" + +#: lib/block_scout_web/templates/block/overview.html.eex:122 +#, elixir-autogen, elixir-format +msgid "The SHA256 hash of the block." +msgstr "" + +#: lib/block_scout_web/templates/block/overview.html.eex:51 +#, elixir-autogen, elixir-format +msgid "The block height of a particular block is defined as the number of blocks preceding it in the blockchain." +msgstr "" + +#: lib/block_scout_web/templates/transaction_state/index.html.eex:18 +#, elixir-autogen, elixir-format +msgid "The changes from this transaction have not yet happened since the transaction is still pending." +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:26 +#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:18 +#, elixir-autogen, elixir-format +msgid "The compiler version is specified in pragma solidity X.X.X. Use the compiler version rather than the nightly build. If using the Solidity compiler, run solc —version to check." +msgstr "" + +#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:44 +#, elixir-autogen, elixir-format +msgid "The fallback function is executed on a call to the contract if none of the other functions match the given function signature, or if no data was supplied at all and there is no receive Ether function. The fallback function always receives data, but in order to also receive Ether it must be marked payable." +msgstr "" + +#: lib/block_scout_web/templates/block/overview.html.eex:138 +#, elixir-autogen, elixir-format +msgid "The hash of the block from which this block was generated." +msgstr "" + +#: lib/block_scout_web/templates/address/overview.html.eex:74 +#, elixir-autogen, elixir-format +msgid "The name found in the source code of the Contract." +msgstr "" + +#: lib/block_scout_web/templates/address/overview.html.eex:85 +#, elixir-autogen, elixir-format +msgid "The name of the validator." +msgstr "" + +#: lib/block_scout_web/templates/block/overview.html.eex:79 +#, elixir-autogen, elixir-format +msgid "The number of transactions in the block." +msgstr "" + +#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:46 +#, elixir-autogen, elixir-format +msgid "The receive function is executed on a call to the contract with empty calldata. This is the function that is executed on plain Ether transfers (e.g. via .send() or .transfer()). If no such function exists, but a payable fallback function exists, the fallback function will be called on a plain Ether transfer. If neither a receive Ether nor a payable fallback function is present, the contract cannot receive Ether through regular transactions and throws an exception." +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:137 +#, elixir-autogen, elixir-format +msgid "The revert reason of the transaction." +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:109 +#, elixir-autogen, elixir-format +msgid "The status of the transaction: Confirmed or Unconfirmed." +msgstr "" + +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:67 +#, elixir-autogen, elixir-format +msgid "The total amount of tokens issued" +msgstr "" + +#: lib/block_scout_web/templates/block/overview.html.eex:179 +#, elixir-autogen, elixir-format +msgid "The total gas amount used in the block and its percentage of gas filled in the block." +msgstr "" + +#: lib/block_scout_web/templates/address_validation/index.html.eex:16 +#, elixir-autogen, elixir-format +msgid "There are no blocks validated by this address." +msgstr "" + +#: lib/block_scout_web/templates/block/index.html.eex:17 +#, elixir-autogen, elixir-format +msgid "There are no blocks." +msgstr "" + +#: lib/block_scout_web/templates/tokens/holder/index.html.eex:29 +#, elixir-autogen, elixir-format +msgid "There are no holders for this Token." +msgstr "" + +#: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:54 +#, elixir-autogen, elixir-format +msgid "There are no internal transactions for this address." +msgstr "" + +#: lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex:17 +#, elixir-autogen, elixir-format +msgid "There are no internal transactions for this transaction." +msgstr "" + +#: lib/block_scout_web/templates/address_logs/index.html.eex:28 +#, elixir-autogen, elixir-format +msgid "There are no logs for this address." +msgstr "" + +#: lib/block_scout_web/templates/transaction_log/index.html.eex:20 +#, elixir-autogen, elixir-format +msgid "There are no logs for this transaction." +msgstr "" + +#: lib/block_scout_web/templates/pending_transaction/index.html.eex:22 +#, elixir-autogen, elixir-format +msgid "There are no pending transactions." +msgstr "" + +#: lib/block_scout_web/templates/address_token_transfer/index.html.eex:53 +#, elixir-autogen, elixir-format +msgid "There are no token transfers for this address." +msgstr "" + +#: lib/block_scout_web/templates/transaction_token_transfer/index.html.eex:19 +#, elixir-autogen, elixir-format +msgid "There are no token transfers for this transaction" +msgstr "" + +#: lib/block_scout_web/templates/address_token/index.html.eex:65 +#, elixir-autogen, elixir-format +msgid "There are no tokens for this address." +msgstr "" + +#: lib/block_scout_web/templates/tokens/inventory/index.html.eex:28 +#, elixir-autogen, elixir-format +msgid "There are no tokens." +msgstr "" + +#: lib/block_scout_web/templates/address_transaction/index.html.eex:55 +#, elixir-autogen, elixir-format +msgid "There are no transactions for this address." +msgstr "" + +#: lib/block_scout_web/templates/block_transaction/index.html.eex:19 +#, elixir-autogen, elixir-format +msgid "There are no transactions for this block." +msgstr "" + +#: lib/block_scout_web/templates/transaction/index.html.eex:31 +#, elixir-autogen, elixir-format +msgid "There are no transactions." +msgstr "" + +#: lib/block_scout_web/templates/tokens/instance/holder/index.html.eex:28 +#: lib/block_scout_web/templates/tokens/instance/transfer/index.html.eex:28 +#: lib/block_scout_web/templates/tokens/transfer/index.html.eex:27 +#, elixir-autogen, elixir-format +msgid "There are no transfers for this Token." +msgstr "" + +#: lib/block_scout_web/templates/verified_contracts/index.html.eex:99 +#, elixir-autogen, elixir-format +msgid "There are no verified contracts." +msgstr "" + +#: lib/block_scout_web/templates/address_withdrawal/index.html.eex:54 +#, elixir-autogen, elixir-format +msgid "There are no withdrawals for this address." +msgstr "" + +#: lib/block_scout_web/templates/block_withdrawal/index.html.eex:45 +#, elixir-autogen, elixir-format +msgid "There are no withdrawals for this block." +msgstr "" + +#: lib/block_scout_web/templates/withdrawal/index.html.eex:53 +#, elixir-autogen, elixir-format +msgid "There are no withdrawals." +msgstr "" + +#: lib/block_scout_web/templates/address_coin_balance/index.html.eex:35 +#, elixir-autogen, elixir-format +msgid "There is no coin history for this address." +msgstr "" + +#: lib/block_scout_web/templates/address_coin_balance/index.html.eex:21 +#: lib/block_scout_web/templates/chain/show.html.eex:9 +#, elixir-autogen, elixir-format +msgid "There was a problem loading the chart." +msgstr "" + +#: lib/block_scout_web/templates/api_docs/index.html.eex:6 +#, elixir-autogen, elixir-format +msgid "This API is provided for developers transitioning their applications from Etherscan to BlockScout. It supports GET and POST requests." +msgstr "" + +#: lib/block_scout_web/templates/api_docs/eth_rpc.html.eex:7 +#, elixir-autogen, elixir-format +msgid "This API is provided to support some rpc methods in the exact format specified for ethereum nodes, which can be found " +msgstr "" + +#: lib/block_scout_web/views/block_transaction_view.ex:11 +#, elixir-autogen, elixir-format +msgid "This block has not been processed yet." +msgstr "" + +#: lib/block_scout_web/templates/address_contract/index.html.eex:47 +#, elixir-autogen, elixir-format +msgid "This contract has been partially verified via Sourcify." +msgstr "" + +#: lib/block_scout_web/templates/address_contract/index.html.eex:51 +#, elixir-autogen, elixir-format +msgid "This contract has been verified via Sourcify." +msgstr "" + +#: lib/block_scout_web/templates/api_docs/eth_rpc.html.eex:10 +#, elixir-autogen, elixir-format +msgid "This is useful to allow sending requests to blockscout without having to change anything about the request." +msgstr "" + +#: lib/block_scout_web/templates/page_not_found/index.html.eex:8 +#, elixir-autogen, elixir-format +msgid "This page is no longer explorable! If you are lost, use the search bar to find what you are looking for." +msgstr "" + +#: lib/block_scout_web/templates/transaction_state/index.html.eex:22 +#, elixir-autogen, elixir-format +msgid "This transaction hasn't changed state." +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:64 +#, elixir-autogen, elixir-format +msgid "This transaction is pending confirmation." +msgstr "" + +#: lib/block_scout_web/templates/block/overview.html.eex:71 +#: lib/block_scout_web/templates/transaction/overview.html.eex:180 +#, elixir-autogen, elixir-format +msgid "Timestamp" +msgstr "" + +#: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:32 +#: lib/block_scout_web/templates/address_token_transfer/index.html.eex:34 +#: lib/block_scout_web/templates/address_transaction/index.html.eex:28 +#: lib/block_scout_web/templates/block_withdrawal/index.html.eex:29 +#: lib/block_scout_web/templates/transaction/overview.html.eex:267 +#: lib/block_scout_web/templates/withdrawal/index.html.eex:32 +#: lib/block_scout_web/views/address_internal_transaction_view.ex:10 +#: lib/block_scout_web/views/address_token_transfer_view.ex:10 +#: lib/block_scout_web/views/address_transaction_view.ex:10 +#, elixir-autogen, elixir-format +msgid "To" +msgstr "" + +#: lib/block_scout_web/templates/transaction/_decoded_input.html.eex:20 +#, elixir-autogen, elixir-format +msgid "To have guaranteed accuracy, use the link above to verify the contract's source code." +msgstr "" + +#: lib/block_scout_web/templates/address_logs/_logs.html.eex:6 +#: lib/block_scout_web/templates/transaction/_decoded_input.html.eex:8 +#: lib/block_scout_web/templates/transaction_log/_logs.html.eex:6 +#, elixir-autogen, elixir-format +msgid "To see accurate decoded input data, the contract must be verified." +msgstr "" + +#: lib/block_scout_web/templates/layout/_topnav.html.eex:18 +#, elixir-autogen, elixir-format +msgid "Toggle navigation" +msgstr "" + +#: lib/block_scout_web/templates/address/overview.html.eex:55 +#: lib/block_scout_web/templates/tokens/index.html.eex:31 +#, elixir-autogen, elixir-format +msgid "Token" +msgstr "" + +#: lib/block_scout_web/templates/common_components/_token_transfer_type_display_name.html.eex:3 +#: lib/block_scout_web/views/transaction_view.ex:488 +#, elixir-autogen, elixir-format +msgid "Token Burning" +msgstr "" + +#: lib/block_scout_web/templates/common_components/_token_transfer_type_display_name.html.eex:7 +#: lib/block_scout_web/views/transaction_view.ex:489 +#, elixir-autogen, elixir-format +msgid "Token Creation" +msgstr "" + +#: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:10 +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:34 +#, elixir-autogen, elixir-format +msgid "Token Details" +msgstr "" + +#: lib/block_scout_web/templates/tokens/holder/index.html.eex:17 +#: lib/block_scout_web/templates/tokens/instance/holder/index.html.eex:16 +#: lib/block_scout_web/templates/tokens/instance/overview/_tabs.html.eex:17 +#: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:11 +#: lib/block_scout_web/views/tokens/overview_view.ex:40 +#, elixir-autogen, elixir-format +msgid "Token Holders" +msgstr "" + +#: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:38 +#: lib/block_scout_web/templates/tokens/inventory/_token.html.eex:18 +#: lib/block_scout_web/templates/tokens/inventory/_token.html.eex:37 +#, elixir-autogen, elixir-format +msgid "Token ID" +msgstr "" + +#: lib/block_scout_web/templates/common_components/_token_transfer_type_display_name.html.eex:5 +#: lib/block_scout_web/views/transaction_view.ex:487 +#, elixir-autogen, elixir-format +msgid "Token Minting" +msgstr "" + +#: lib/block_scout_web/templates/common_components/_token_transfer_type_display_name.html.eex:9 +#: lib/block_scout_web/templates/common_components/_token_transfer_type_display_name.html.eex:11 +#: lib/block_scout_web/views/transaction_view.ex:490 +#, elixir-autogen, elixir-format +msgid "Token Transfer" +msgstr "" + +#: lib/block_scout_web/templates/address/_tabs.html.eex:13 +#: lib/block_scout_web/templates/address_token_transfer/index.html.eex:19 +#: lib/block_scout_web/templates/tokens/instance/overview/_tabs.html.eex:3 +#: lib/block_scout_web/templates/tokens/instance/transfer/index.html.eex:16 +#: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:5 +#: lib/block_scout_web/templates/tokens/transfer/index.html.eex:15 +#: lib/block_scout_web/templates/transaction/_tabs.html.eex:4 +#: lib/block_scout_web/templates/transaction_token_transfer/index.html.eex:7 +#: lib/block_scout_web/views/address_view.ex:349 +#: lib/block_scout_web/views/tokens/instance/overview_view.ex:74 +#: lib/block_scout_web/views/tokens/overview_view.ex:39 +#: lib/block_scout_web/views/transaction_view.ex:551 +#, elixir-autogen, elixir-format +msgid "Token Transfers" +msgstr "" + +#: lib/block_scout_web/templates/address/overview.html.eex:54 +#, elixir-autogen, elixir-format +msgid "Token name and symbol." +msgstr "" + +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:142 +#, elixir-autogen, elixir-format +msgid "Token type" +msgstr "" + +#: lib/block_scout_web/templates/address/_tabs.html.eex:21 +#: lib/block_scout_web/templates/address/overview.html.eex:177 +#: lib/block_scout_web/templates/address_token/overview.html.eex:58 +#: lib/block_scout_web/templates/address_token_transfer/index.html.eex:13 +#: lib/block_scout_web/templates/layout/_topnav.html.eex:84 +#: lib/block_scout_web/templates/tokens/index.html.eex:10 +#: lib/block_scout_web/views/address_view.ex:346 +#, elixir-autogen, elixir-format +msgid "Tokens" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:336 +#, elixir-autogen, elixir-format +msgid "Tokens Burnt" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:352 +#, elixir-autogen, elixir-format +msgid "Tokens Created" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:319 +#, elixir-autogen, elixir-format +msgid "Tokens Minted" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:303 +#, elixir-autogen, elixir-format +msgid "Tokens Transferred" +msgstr "" + +#: lib/block_scout_web/templates/address/_metatags.html.eex:13 +#, elixir-autogen, elixir-format +msgid "Top Accounts - %{subnetwork} Explorer" +msgstr "" + +#: lib/block_scout_web/templates/address_logs/index.html.eex:14 +#, elixir-autogen, elixir-format +msgid "Topic" +msgstr "" + +#: lib/block_scout_web/templates/address_logs/_logs.html.eex:68 +#: lib/block_scout_web/templates/transaction_log/_logs.html.eex:100 +#, elixir-autogen, elixir-format +msgid "Topics" +msgstr "" + +#: lib/block_scout_web/templates/verified_contracts/_stats.html.eex:9 +#: lib/block_scout_web/templates/verified_contracts/_stats.html.eex:22 +#, elixir-autogen, elixir-format +msgid "Total" +msgstr "" + +#: lib/block_scout_web/templates/block/overview.html.eex:170 +#, elixir-autogen, elixir-format +msgid "Total Difficulty" +msgstr "" + +#: lib/block_scout_web/templates/tokens/index.html.eex:43 +#, elixir-autogen, elixir-format +msgid "Total Supply" +msgstr "" + +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:84 +#, elixir-autogen, elixir-format +msgid "Total Supply * Price" +msgstr "" + +#: lib/block_scout_web/templates/chain/show.html.eex:133 +#, elixir-autogen, elixir-format +msgid "Total blocks" +msgstr "" + +#: lib/block_scout_web/templates/block/overview.html.eex:169 +#, elixir-autogen, elixir-format +msgid "Total difficulty of the chain until this block." +msgstr "" + +#: lib/block_scout_web/templates/block/overview.html.eex:188 +#, elixir-autogen, elixir-format +msgid "Total gas limit provided by all transactions in the block." +msgstr "" + +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:68 +#, elixir-autogen, elixir-format +msgid "Total supply" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:383 +#, elixir-autogen, elixir-format +msgid "Total transaction fee." +msgstr "" + +#: lib/block_scout_web/templates/chain/show.html.eex:112 +#, elixir-autogen, elixir-format +msgid "Total transactions" +msgstr "" + +#: lib/block_scout_web/templates/account/tag_transaction/form.html.eex:11 +#: lib/block_scout_web/templates/account/tag_transaction/index.html.eex:23 +#: lib/block_scout_web/templates/address_logs/_logs.html.eex:19 +#: lib/block_scout_web/views/transaction_view.ex:500 +#, elixir-autogen, elixir-format +msgid "Transaction" +msgstr "" + +#: lib/block_scout_web/templates/transaction/_metatags.html.eex:3 +#, elixir-autogen, elixir-format +msgid "Transaction %{transaction} - %{subnetwork} Explorer" +msgstr "" + +#: lib/block_scout_web/templates/transaction/_metatags.html.eex:11 +#, elixir-autogen, elixir-format +msgid "Transaction %{transaction}, %{subnetwork} %{transaction}" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:225 +#, elixir-autogen, elixir-format, fuzzy +msgid "Transaction Action" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:470 +#, elixir-autogen, elixir-format +msgid "Transaction Burnt Fee" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:50 +#, elixir-autogen, elixir-format +msgid "Transaction Details" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:384 +#, elixir-autogen, elixir-format +msgid "Transaction Fee" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:80 +#, elixir-autogen, elixir-format +msgid "Transaction Hash" +msgstr "" + +#: lib/block_scout_web/templates/transaction/_decoded_input_body.html.eex:2 +#: lib/block_scout_web/templates/transaction/_decoded_input_body.html.eex:19 +#, elixir-autogen, elixir-format +msgid "Transaction Inputs" +msgstr "" + +#: lib/block_scout_web/templates/account/common/_nav.html.eex:13 +#: lib/block_scout_web/templates/account/tag_transaction/index.html.eex:7 +#: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:17 +#, elixir-autogen, elixir-format +msgid "Transaction Tags" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:414 +#, elixir-autogen, elixir-format +msgid "Transaction Type" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:534 +#, elixir-autogen, elixir-format +msgid "Transaction number from the sending address. Each transaction sent from an address increments the nonce by 1." +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:413 +#, elixir-autogen, elixir-format +msgid "Transaction type, introduced in EIP-2718." +msgstr "" + +#: lib/block_scout_web/templates/address/_tabs.html.eex:7 +#: lib/block_scout_web/templates/address/_tile.html.eex:31 +#: lib/block_scout_web/templates/address/overview.html.eex:188 +#: lib/block_scout_web/templates/address/overview.html.eex:194 +#: lib/block_scout_web/templates/address/overview.html.eex:202 +#: lib/block_scout_web/templates/address_transaction/index.html.eex:13 +#: lib/block_scout_web/templates/block/_tabs.html.eex:4 +#: lib/block_scout_web/templates/block/overview.html.eex:80 +#: lib/block_scout_web/templates/chain/show.html.eex:216 +#: lib/block_scout_web/templates/layout/_topnav.html.eex:49 +#: lib/block_scout_web/views/address_view.ex:348 +#, elixir-autogen, elixir-format +msgid "Transactions" +msgstr "" + +#: lib/block_scout_web/templates/address/overview.html.eex:101 +#, elixir-autogen, elixir-format +msgid "Transactions and address of creation." +msgstr "" + +#: lib/block_scout_web/templates/address/overview.html.eex:215 +#: lib/block_scout_web/templates/address/overview.html.eex:221 +#: lib/block_scout_web/templates/address/overview.html.eex:229 +#: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:50 +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:119 +#, elixir-autogen, elixir-format +msgid "Transfers" +msgstr "" + +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:40 +#: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:47 +#, elixir-autogen, elixir-format +msgid "Try it out" +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification_common_fields/_fetch_constructor_args.html.eex:3 +#, elixir-autogen, elixir-format +msgid "Try to fetch constructor arguments automatically" +msgstr "" + +#: lib/block_scout_web/templates/layout/_footer.html.eex:27 +#, elixir-autogen, elixir-format +msgid "Twitter" +msgstr "" + +#: lib/block_scout_web/templates/layout/app.html.eex:53 +#, elixir-autogen, elixir-format +msgid "Tx/day" +msgstr "" + +#: lib/block_scout_web/templates/verified_contracts/index.html.eex:66 +#, elixir-autogen, elixir-format +msgid "Txns" +msgstr "" + +#: lib/block_scout_web/templates/log/_data_decoded_view.html.eex:5 +#: lib/block_scout_web/templates/transaction/_decoded_input_body.html.eex:22 +#, elixir-autogen, elixir-format +msgid "Type" +msgstr "" + +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:141 +#, elixir-autogen, elixir-format +msgid "Type of the token standard" +msgstr "" + +#: lib/block_scout_web/templates/visualize_sol2uml/index.html.eex:5 +#, elixir-autogen, elixir-format +msgid "UML diagram" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:560 +#, elixir-autogen, elixir-format +msgid "UTF-8" +msgstr "" + +#: lib/block_scout_web/views/block_view.ex:77 +#, elixir-autogen, elixir-format +msgid "Uncle Reward" +msgstr "" + +#: lib/block_scout_web/templates/block/overview.html.eex:252 +#: lib/block_scout_web/templates/layout/_topnav.html.eex:41 +#, elixir-autogen, elixir-format +msgid "Uncles" +msgstr "" + +#: lib/block_scout_web/views/transaction_view.ex:367 +#, elixir-autogen, elixir-format +msgid "Unconfirmed" +msgstr "" + +#: lib/block_scout_web/templates/tokens/inventory/_token.html.eex:9 +#, elixir-autogen, elixir-format +msgid "Unique Token" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:79 +#, elixir-autogen, elixir-format +msgid "Unique character string (TxID) assigned to every verified transaction." +msgstr "" + +#: lib/block_scout_web/templates/account/api_key/form.html.eex:7 +#: lib/block_scout_web/templates/account/custom_abi/form.html.eex:8 +#, elixir-autogen, elixir-format +msgid "Update" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:449 +#, elixir-autogen, elixir-format +msgid "User defined maximum fee (tip) per unit of gas paid to validator for transaction prioritization." +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:459 +#, elixir-autogen, elixir-format +msgid "User-defined tip sent to validator for transaction priority/inclusion." +msgstr "" + +#: lib/block_scout_web/templates/block/overview.html.eex:226 +#, elixir-autogen, elixir-format +msgid "User-defined tips sent to validator for transaction priority/inclusion." +msgstr "" + +#: lib/block_scout_web/templates/layout/_topnav.html.eex:56 +#, elixir-autogen, elixir-format +msgid "Validated" +msgstr "" + +#: lib/block_scout_web/templates/transaction/index.html.eex:12 +#, elixir-autogen, elixir-format +msgid "Validated Transactions" +msgstr "" + +#: lib/block_scout_web/templates/address/_validator_metadata_modal.html.eex:30 +#, elixir-autogen, elixir-format +msgid "Validator Creation Date" +msgstr "" + +#: lib/block_scout_web/templates/address/_validator_metadata_modal.html.eex:5 +#, elixir-autogen, elixir-format +msgid "Validator Data" +msgstr "" + +#: lib/block_scout_web/templates/address/overview.html.eex:86 +#, elixir-autogen, elixir-format +msgid "Validator Name" +msgstr "" + +#: lib/block_scout_web/templates/address_withdrawal/index.html.eex:32 +#: lib/block_scout_web/templates/block_withdrawal/index.html.eex:26 +#: lib/block_scout_web/templates/withdrawal/index.html.eex:26 +#, elixir-autogen, elixir-format, fuzzy +msgid "Validator index" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:369 +#, elixir-autogen, elixir-format +msgid "Value" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:368 +#, elixir-autogen, elixir-format +msgid "Value sent in the native token (and USD) if applicable." +msgstr "" + +#: lib/block_scout_web/templates/address_read_contract/index.html.eex:17 +#: lib/block_scout_web/templates/address_write_contract/index.html.eex:15 +#: lib/block_scout_web/templates/verified_contracts/index.html.eex:81 +#, elixir-autogen, elixir-format +msgid "Verified" +msgstr "" + +#: lib/block_scout_web/templates/verified_contracts/_stats.html.eex:18 +#: lib/block_scout_web/templates/verified_contracts/index.html.eex:6 +#, elixir-autogen, elixir-format +msgid "Verified Contracts" +msgstr "" + +#: lib/block_scout_web/templates/address_contract/index.html.eex:88 +#, elixir-autogen, elixir-format +msgid "Verified at" +msgstr "" + +#: lib/block_scout_web/templates/layout/_topnav.html.eex:69 +#, elixir-autogen, elixir-format +msgid "Verified contracts" +msgstr "" + +#: lib/block_scout_web/templates/verified_contracts/_metatags.html.eex:2 +#, elixir-autogen, elixir-format +msgid "Verified contracts - %{subnetwork} Explorer" +msgstr "" + +#: lib/block_scout_web/templates/verified_contracts/_metatags.html.eex:7 +#, elixir-autogen, elixir-format +msgid "Verified contracts, %{subnetwork}, %{coin}" +msgstr "" + +#: lib/block_scout_web/templates/address_contract/index.html.eex:27 +#: lib/block_scout_web/templates/address_contract/index.html.eex:29 +#: lib/block_scout_web/templates/address_contract/index.html.eex:197 +#: lib/block_scout_web/templates/address_contract/index.html.eex:228 +#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:14 +#, elixir-autogen, elixir-format +msgid "Verify & Publish" +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:111 +#: lib/block_scout_web/templates/address_contract_verification_via_json/new.html.eex:37 +#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:103 +#: lib/block_scout_web/templates/address_contract_verification_via_standard_json_input/new.html.eex:51 +#: lib/block_scout_web/templates/address_contract_verification_vyper/new.html.eex:47 +#, elixir-autogen, elixir-format +msgid "Verify & publish" +msgstr "" + +#: lib/block_scout_web/templates/address_logs/_logs.html.eex:10 +#: lib/block_scout_web/templates/transaction/_decoded_input.html.eex:12 +#: lib/block_scout_web/templates/transaction_log/_logs.html.eex:10 +#, elixir-autogen, elixir-format +msgid "Verify the contract " +msgstr "" + +#: lib/block_scout_web/templates/layout/_footer.html.eex:93 +#: lib/block_scout_web/templates/verified_contracts/index.html.eex:72 +#, elixir-autogen, elixir-format +msgid "Version" +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:33 +#, elixir-autogen, elixir-format +msgid "Via Sourcify: Sources and metadata JSON file" +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:27 +#, elixir-autogen, elixir-format +msgid "Via Standard Input JSON" +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:22 +#, elixir-autogen, elixir-format +msgid "Via flattened source code" +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:40 +#, elixir-autogen, elixir-format +msgid "Via multi-part files" +msgstr "" + +#: lib/block_scout_web/templates/chain/show.html.eex:155 +#, elixir-autogen, elixir-format +msgid "View All Blocks" +msgstr "" + +#: lib/block_scout_web/templates/chain/show.html.eex:215 +#, elixir-autogen, elixir-format +msgid "View All Transactions" +msgstr "" + +#: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:16 +#: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:20 +#, elixir-autogen, elixir-format +msgid "View Contract" +msgstr "" + +#: lib/block_scout_web/templates/transaction/_tile.html.eex:73 +#, elixir-autogen, elixir-format +msgid "View Less Transfers" +msgstr "" + +#: lib/block_scout_web/templates/transaction/_tile.html.eex:72 +#, elixir-autogen, elixir-format +msgid "View More Transfers" +msgstr "" + +#: lib/block_scout_web/templates/block/overview.html.eex:39 +#, elixir-autogen, elixir-format +msgid "View next block" +msgstr "" + +#: lib/block_scout_web/templates/block/overview.html.eex:23 +#, elixir-autogen, elixir-format +msgid "View previous block" +msgstr "" + +#: lib/block_scout_web/templates/address/_metatags.html.eex:9 +#, elixir-autogen, elixir-format +msgid "View the account balance, transactions, and other data for %{address} on the %{network}" +msgstr "" + +#: lib/block_scout_web/templates/withdrawal/_metatags.html.eex:8 +#, elixir-autogen, elixir-format +msgid "View the beacon chain withdrawals on %{subnetwork}" +msgstr "" + +#: lib/block_scout_web/templates/block/_metatags.html.eex:10 +#, elixir-autogen, elixir-format +msgid "View the transactions, token transfers, and uncles for block number %{block_number}" +msgstr "" + +#: lib/block_scout_web/templates/verified_contracts/_metatags.html.eex:8 +#, elixir-autogen, elixir-format +msgid "View the verified contracts on %{subnetwork}" +msgstr "" + +#: lib/block_scout_web/templates/transaction/_metatags.html.eex:10 +#, elixir-autogen, elixir-format +msgid "View transaction %{transaction} on %{subnetwork}" +msgstr "" + +#: lib/block_scout_web/templates/verified_contracts/_contract.html.eex:28 +#: lib/block_scout_web/templates/verified_contracts/index.html.eex:32 +#: lib/block_scout_web/views/verified_contracts_view.ex:11 +#, elixir-autogen, elixir-format +msgid "Vyper" +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:46 +#, elixir-autogen, elixir-format +msgid "Vyper contract" +msgstr "" + +#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:142 +#, elixir-autogen, elixir-format +msgid "WEI" +msgstr "" + +#: lib/block_scout_web/templates/smart_contract/_pending_contract_write.html.eex:9 +#, elixir-autogen, elixir-format +msgid "Waiting for transaction's confirmation..." +msgstr "" + +#: lib/block_scout_web/templates/chain/show.html.eex:141 +#, elixir-autogen, elixir-format +msgid "Wallet addresses" +msgstr "" + +#: lib/block_scout_web/templates/common_components/_changed_bytecode_warning.html.eex:3 +#, elixir-autogen, elixir-format +msgid "Warning! Contract bytecode has been changed and doesn't match the verified one. Therefore, interaction with this smart contract may be risky." +msgstr "" + +#: lib/block_scout_web/templates/account/common/_nav.html.eex:7 +#: lib/block_scout_web/templates/account/watchlist/show.html.eex:7 +#: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:15 +#, elixir-autogen, elixir-format +msgid "Watch list" +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:77 +#, elixir-autogen, elixir-format +msgid "We recommend using flattened code. This is necessary if your code utilizes a library or inherits dependencies. Use the" +msgstr "" + +#: lib/block_scout_web/views/wei_helper.ex:80 +#, elixir-autogen, elixir-format +msgid "Wei" +msgstr "" + +#: lib/block_scout_web/templates/address/_tabs.html.eex:29 +#: lib/block_scout_web/templates/address_withdrawal/index.html.eex:13 +#: lib/block_scout_web/templates/block/_tabs.html.eex:13 +#: lib/block_scout_web/templates/layout/_topnav.html.eex:73 +#: lib/block_scout_web/templates/withdrawal/index.html.eex:5 +#, elixir-autogen, elixir-format +msgid "Withdrawals" +msgstr "" + +#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:106 +#, elixir-autogen, elixir-format +msgid "Write" +msgstr "" + +#: lib/block_scout_web/templates/address/_tabs.html.eex:95 +#: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:34 +#: lib/block_scout_web/views/address_view.ex:353 +#, elixir-autogen, elixir-format +msgid "Write Contract" +msgstr "" + +#: lib/block_scout_web/templates/address/_tabs.html.eex:102 +#: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:48 +#: lib/block_scout_web/views/address_view.ex:354 +#, elixir-autogen, elixir-format +msgid "Write Proxy" +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification_common_fields/_fetch_constructor_args.html.eex:14 +#: lib/block_scout_web/templates/address_contract_verification_common_fields/_include_nightly_builds_field.html.eex:14 +#: lib/block_scout_web/templates/address_contract_verification_common_fields/_yul_contracts_switcher.html.eex:14 +#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:51 +#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:43 +#, elixir-autogen, elixir-format +msgid "Yes" +msgstr "" + +#: lib/block_scout_web/templates/account/api_key/index.html.eex:18 +#, elixir-autogen, elixir-format +msgid "You can create 3 API keys per account." +msgstr "" + +#: lib/block_scout_web/templates/account/custom_abi/index.html.eex:18 +#, elixir-autogen, elixir-format +msgid "You can create up to 15 Custom ABIs per account." +msgstr "" + +#: lib/block_scout_web/templates/account/public_tags_request/index.html.eex:11 +#, elixir-autogen, elixir-format +msgid "You can request a public category tag which is displayed to all Blockscout users. Public tags may be added to contract or external addresses, and any associated transactions will inherit that tag. Clicking a tag opens a page with related information and helps provide context and data organization. Requests are sent to a moderator for review and approval. This process can take several days." +msgstr "" + +#: lib/block_scout_web/templates/account/tag_address/index.html.eex:14 +#, elixir-autogen, elixir-format +msgid "You don't have address tags yet" +msgstr "" + +#: lib/block_scout_web/templates/account/watchlist/show.html.eex:14 +#, elixir-autogen, elixir-format +msgid "You don't have addresses on you watchlist yet" +msgstr "" + +#: lib/block_scout_web/templates/account/tag_transaction/index.html.eex:14 +#, elixir-autogen, elixir-format +msgid "You don't have transaction tags yet" +msgstr "" + +#: lib/block_scout_web/templates/account/public_tags_request/form.html.eex:15 +#, elixir-autogen, elixir-format +msgid "Your name" +msgstr "" + +#: lib/block_scout_web/templates/account/public_tags_request/form.html.eex:14 +#, elixir-autogen, elixir-format +msgid "Your name*" +msgstr "" + +#: lib/block_scout_web/templates/error422/index.html.eex:8 +#, elixir-autogen, elixir-format +msgid "Your request contained an error, perhaps a mistyped tx/block/address hash. Try again, and check the developer tools console for more info." +msgstr "" + +#: lib/block_scout_web/templates/verified_contracts/index.html.eex:38 +#: lib/block_scout_web/views/verified_contracts_view.ex:12 +#, elixir-autogen, elixir-format +msgid "Yul" +msgstr "" + +#: lib/block_scout_web/templates/address/overview.html.eex:111 +#, elixir-autogen, elixir-format +msgid "at" +msgstr "" + +#: lib/block_scout_web/templates/address_token/overview.html.eex:52 +#, elixir-autogen, elixir-format +msgid "balance of the address" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:469 +#, elixir-autogen, elixir-format +msgid "burnt for this transaction. Equals Block Base Fee per Gas * Gas Used." +msgstr "" + +#: lib/block_scout_web/templates/block/overview.html.eex:217 +#, elixir-autogen, elixir-format +msgid "burnt from transactions included in the block (Base fee (per unit of gas) * Gas Used)." +msgstr "" + +#: lib/block_scout_web/templates/address_contract/index.html.eex:27 +#, elixir-autogen, elixir-format +msgid "button" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:276 +#, elixir-autogen, elixir-format +msgid "created" +msgstr "" + +#: lib/block_scout_web/templates/api_docs/eth_rpc.html.eex:12 +#, elixir-autogen, elixir-format +msgid "custom RPC" +msgstr "" + +#: lib/block_scout_web/templates/address/overview.html.eex:151 +#, elixir-autogen, elixir-format +msgid "doesn't include ERC20, ERC721, ERC1155 tokens)." +msgstr "" + +#: lib/block_scout_web/templates/common_components/_rap_pagination_container.html.eex:13 +#, elixir-autogen, elixir-format +msgid "elements are displayed" +msgstr "" + +#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:44 +#, elixir-autogen, elixir-format +msgid "fallback" +msgstr "" + +#: lib/block_scout_web/views/address_contract_view.ex:33 +#, elixir-autogen, elixir-format +msgid "false" +msgstr "" + +#: lib/block_scout_web/templates/csv_export/index.html.eex:14 +#, elixir-autogen, elixir-format +msgid "for address" +msgstr "" + +#: lib/block_scout_web/templates/address_logs/_logs.html.eex:10 +#: lib/block_scout_web/templates/transaction/_decoded_input.html.eex:12 +#: lib/block_scout_web/templates/transaction_log/_logs.html.eex:10 +#, elixir-autogen, elixir-format +msgid "here" +msgstr "" + +#: lib/block_scout_web/templates/api_docs/eth_rpc.html.eex:9 +#, elixir-autogen, elixir-format +msgid "here." +msgstr "" + +#: lib/block_scout_web/templates/smart_contract/_function_response.html.eex:3 +#, elixir-autogen, elixir-format +msgid "method Response" +msgstr "" + +#: lib/block_scout_web/templates/common_components/_pagination_container.html.eex:41 +#, elixir-autogen, elixir-format +msgid "of" +msgstr "" + +#: lib/block_scout_web/templates/layout/app.html.eex:100 +#, elixir-autogen, elixir-format +msgid "on sign up. Didn’t receive?" +msgstr "" + +#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:16 +#, elixir-autogen, elixir-format +msgid "page" +msgstr "" + +#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:46 +#, elixir-autogen, elixir-format +msgid "receive" +msgstr "" + +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:58 +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:69 +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:81 +#: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:70 +#, elixir-autogen, elixir-format +msgid "required" +msgstr "" + +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:59 +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:70 +#, elixir-autogen, elixir-format +msgid "string" +msgstr "" + +#: lib/block_scout_web/templates/csv_export/index.html.eex:17 +#, elixir-autogen, elixir-format +msgid "to CSV file" +msgstr "" + +#: lib/block_scout_web/views/address_contract_view.ex:32 +#, elixir-autogen, elixir-format +msgid "true" +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:77 +#, elixir-autogen, elixir-format +msgid "truffle flattener" +msgstr "" diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/errors.po b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/errors.po new file mode 100644 index 0000000..cdaaac6 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/errors.po @@ -0,0 +1,94 @@ +## This file is a PO Template file. +## +## `msgid`s here are often extracted from source code. +## Add new translations manually only if they're dynamic +## translations that can't be statically extracted. +## +## Run `mix gettext.extract` to bring this file up to +## date. Leave `msgstr`s empty as changing them here as no +## effect: edit them in PO (`.po`) files instead. +## From Ecto.Changeset.cast/4 +msgid "can't be blank" +msgstr "" + +## From Ecto.Changeset.unique_constraint/3 +msgid "has already been taken" +msgstr "" + +## From Ecto.Changeset.put_change/3 +msgid "is invalid" +msgstr "" + +## From Ecto.Changeset.validate_acceptance/3 +msgid "must be accepted" +msgstr "" + +## From Ecto.Changeset.validate_format/3 +msgid "has invalid format" +msgstr "" + +## From Ecto.Changeset.validate_subset/3 +msgid "has an invalid entry" +msgstr "" + +## From Ecto.Changeset.validate_exclusion/3 +msgid "is reserved" +msgstr "" + +## From Ecto.Changeset.validate_confirmation/3 +msgid "does not match confirmation" +msgstr "" + +## From Ecto.Changeset.no_assoc_constraint/3 +msgid "is still associated with this entry" +msgstr "" + +msgid "are still associated with this entry" +msgstr "" + +## From Ecto.Changeset.validate_length/3 +msgid "should be %{count} character(s)" +msgid_plural "should be %{count} character(s)" +msgstr[0] "" +msgstr[1] "" + +msgid "should have %{count} item(s)" +msgid_plural "should have %{count} item(s)" +msgstr[0] "" +msgstr[1] "" + +msgid "should be at least %{count} character(s)" +msgid_plural "should be at least %{count} character(s)" +msgstr[0] "" +msgstr[1] "" + +msgid "should have at least %{count} item(s)" +msgid_plural "should have at least %{count} item(s)" +msgstr[0] "" +msgstr[1] "" + +msgid "should be at most %{count} character(s)" +msgid_plural "should be at most %{count} character(s)" +msgstr[0] "" +msgstr[1] "" + +msgid "should have at most %{count} item(s)" +msgid_plural "should have at most %{count} item(s)" +msgstr[0] "" +msgstr[1] "" + +## From Ecto.Changeset.validate_number/3 +msgid "must be less than %{number}" +msgstr "" + +msgid "must be greater than %{number}" +msgstr "" + +msgid "must be less than or equal to %{number}" +msgstr "" + +msgid "must be greater than or equal to %{number}" +msgstr "" + +msgid "must be equal to %{number}" +msgstr "" diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/priv/gettext/errors.pot b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/priv/gettext/errors.pot new file mode 100644 index 0000000..cdaaac6 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/priv/gettext/errors.pot @@ -0,0 +1,94 @@ +## This file is a PO Template file. +## +## `msgid`s here are often extracted from source code. +## Add new translations manually only if they're dynamic +## translations that can't be statically extracted. +## +## Run `mix gettext.extract` to bring this file up to +## date. Leave `msgstr`s empty as changing them here as no +## effect: edit them in PO (`.po`) files instead. +## From Ecto.Changeset.cast/4 +msgid "can't be blank" +msgstr "" + +## From Ecto.Changeset.unique_constraint/3 +msgid "has already been taken" +msgstr "" + +## From Ecto.Changeset.put_change/3 +msgid "is invalid" +msgstr "" + +## From Ecto.Changeset.validate_acceptance/3 +msgid "must be accepted" +msgstr "" + +## From Ecto.Changeset.validate_format/3 +msgid "has invalid format" +msgstr "" + +## From Ecto.Changeset.validate_subset/3 +msgid "has an invalid entry" +msgstr "" + +## From Ecto.Changeset.validate_exclusion/3 +msgid "is reserved" +msgstr "" + +## From Ecto.Changeset.validate_confirmation/3 +msgid "does not match confirmation" +msgstr "" + +## From Ecto.Changeset.no_assoc_constraint/3 +msgid "is still associated with this entry" +msgstr "" + +msgid "are still associated with this entry" +msgstr "" + +## From Ecto.Changeset.validate_length/3 +msgid "should be %{count} character(s)" +msgid_plural "should be %{count} character(s)" +msgstr[0] "" +msgstr[1] "" + +msgid "should have %{count} item(s)" +msgid_plural "should have %{count} item(s)" +msgstr[0] "" +msgstr[1] "" + +msgid "should be at least %{count} character(s)" +msgid_plural "should be at least %{count} character(s)" +msgstr[0] "" +msgstr[1] "" + +msgid "should have at least %{count} item(s)" +msgid_plural "should have at least %{count} item(s)" +msgstr[0] "" +msgstr[1] "" + +msgid "should be at most %{count} character(s)" +msgid_plural "should be at most %{count} character(s)" +msgstr[0] "" +msgstr[1] "" + +msgid "should have at most %{count} item(s)" +msgid_plural "should have at most %{count} item(s)" +msgstr[0] "" +msgstr[1] "" + +## From Ecto.Changeset.validate_number/3 +msgid "must be less than %{number}" +msgstr "" + +msgid "must be greater than %{number}" +msgstr "" + +msgid "must be less than or equal to %{number}" +msgstr "" + +msgid "must be greater than or equal to %{number}" +msgstr "" + +msgid "must be equal to %{number}" +msgstr "" diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/chain_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/chain_test.exs new file mode 100644 index 0000000..efbde82 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/chain_test.exs @@ -0,0 +1,68 @@ +defmodule BlockScoutWeb.ChainTest do + use Explorer.DataCase + + alias Explorer.Chain.{Address, Block, Transaction} + alias BlockScoutWeb.Chain + + describe "current_filter/1" do + test "sets direction based on to filter" do + assert [direction: :to] = Chain.current_filter(%{"filter" => "to"}) + end + + test "sets direction based on from filter" do + assert [direction: :from] = Chain.current_filter(%{"filter" => "from"}) + end + + test "no direction set" do + assert [] = Chain.current_filter(%{}) + end + + test "no direction set with paging_options" do + assert [paging_options: "test"] = Chain.current_filter(%{paging_options: "test"}) + end + end + + describe "from_param/1" do + test "finds a block by block number with a valid block number" do + %Block{number: number} = insert(:block, number: 37) + + assert {:ok, %Block{number: ^number}} = + number + |> to_string() + |> Chain.from_param() + end + + test "finds a transaction by hash string" do + transaction = %Transaction{hash: hash} = insert(:transaction) + + assert {:ok, %Transaction{hash: ^hash}} = transaction |> Phoenix.Param.to_param() |> Chain.from_param() + end + + test "finds an address by hash string" do + address = %Address{hash: hash} = insert(:address) + + assert {:ok, %Address{hash: ^hash}} = address |> Phoenix.Param.to_param() |> Chain.from_param() + end + + test "returns {:error, :not_found} when garbage is passed in" do + assert {:error, :not_found} = Chain.from_param("any ol' thing") + end + + test "returns {:error, :not_found} when it does not find a match" do + transaction_hash = String.pad_trailing("0xnonsense", 43, "0") + address_hash = String.pad_trailing("0xbaddress", 42, "0") + + assert {:error, :not_found} = Chain.from_param("38999") + assert {:error, :not_found} = Chain.from_param(transaction_hash) + assert {:error, :not_found} = Chain.from_param(address_hash) + end + end + + describe "Poison.encode!" do + test "correctly encodes decimal values" do + val = Decimal.from_float(5.55) + + assert "\"5.55\"" == Poison.encode!(val) + end + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/channels/address_channel_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/channels/address_channel_test.exs new file mode 100644 index 0000000..d4da423 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/channels/address_channel_test.exs @@ -0,0 +1,257 @@ +defmodule BlockScoutWeb.AddressChannelTest do + use BlockScoutWeb.ChannelCase, + # ETS tables are shared in `Explorer.Chain.Cache.Counters.AddressesCount` + async: false + + alias BlockScoutWeb.UserSocket + alias BlockScoutWeb.Notifier + alias Explorer.Chain.Wei + alias Explorer.Chain.Cache.Counters.AddressesCount + + test "subscribed user is notified of new_address count event" do + topic = "addresses_old:new_address" + @endpoint.subscribe(topic) + + address = insert(:address) + + start_supervised!(AddressesCount) + AddressesCount.consolidate() + + Notifier.handle_event({:chain_event, :addresses, :realtime, [address]}) + + assert_receive %Phoenix.Socket.Broadcast{topic: ^topic, event: "count", payload: %{count: _}}, :timer.seconds(5) + end + + describe "user pushing to channel" do + setup do + address = insert(:address, fetched_coin_balance: 100_000, fetched_coin_balance_block_number: 1) + topic = "addresses_old:#{address.hash}" + + {:ok, _, socket} = + UserSocket + |> socket("no_id", %{locale: "en"}) + |> subscribe_and_join(topic) + + {:ok, %{address: address, topic: topic, socket: socket}} + end + + test "can retrieve current balance card of the address", %{socket: socket, address: address} do + ref = push(socket, "get_balance", %{}) + + assert_reply(ref, :ok, %{balance: sent_balance, balance_card: _balance_card}) + + assert sent_balance == address.fetched_coin_balance.value + # assert balance_card =~ "/address/#{address.hash}/token-balances" + end + end + + describe "user subscribed to address" do + setup do + address = insert(:address) + topic = "addresses_old:#{address.hash}" + @endpoint.subscribe(topic) + {:ok, %{address: address, topic: topic}} + end + + test "notified of balance_update for matching address", %{address: address, topic: topic} do + {:ok, balance} = Wei.cast(1) + address_with_balance = %{address | fetched_coin_balance: balance} + + start_supervised!(AddressesCount) + AddressesCount.consolidate() + + Notifier.handle_event({:chain_event, :addresses, :realtime, [address_with_balance]}) + + assert_receive %Phoenix.Socket.Broadcast{topic: ^topic, event: "balance_update", payload: payload}, + :timer.seconds(5) + + assert payload.address.hash == address_with_balance.hash + end + + test "not notified of balance_update if fetched_coin_balance is nil", %{address: address} do + start_supervised!(AddressesCount) + AddressesCount.consolidate() + + Notifier.handle_event({:chain_event, :addresses, :realtime, [address]}) + + refute_receive _, 100, "Message was broadcast for nil fetched_coin_balance." + end + + test "notified of new_pending_transaction for matching from_address", %{address: address, topic: topic} do + pending = insert(:transaction, from_address: address) + + Notifier.handle_event({:chain_event, :transactions, :realtime, [pending]}) + + assert_receive %Phoenix.Socket.Broadcast{ + topic: ^topic, + event: "pending_transaction", + payload: %{transaction: _} = payload + }, + :timer.seconds(5) + + assert payload.address.hash == address.hash + assert payload.transaction.hash == pending.hash + end + + test "notified of new_transaction for matching from_address", %{address: address, topic: topic} do + transaction = + :transaction + |> insert(from_address: address) + |> with_block() + + Notifier.handle_event({:chain_event, :transactions, :realtime, [transaction]}) + + assert_receive %Phoenix.Socket.Broadcast{ + topic: ^topic, + event: "transaction", + payload: %{transaction: _} = payload + }, + :timer.seconds(5) + + assert payload.address.hash == address.hash + assert payload.transaction.hash == transaction.hash + end + + test "notified of new_transaction for matching to_address", %{address: address, topic: topic} do + transaction = + :transaction + |> insert(to_address: address) + |> with_block() + + Notifier.handle_event({:chain_event, :transactions, :realtime, [transaction]}) + + assert_receive %Phoenix.Socket.Broadcast{ + topic: ^topic, + event: "transaction", + payload: %{transaction: _} = payload + }, + :timer.seconds(5) + + assert payload.address.hash == address.hash + assert payload.transaction.hash == transaction.hash + end + + test "not notified twice of new_transaction if to and from address are equal", %{address: address, topic: topic} do + transaction = + :transaction + |> insert(from_address: address, to_address: address) + |> with_block() + + Notifier.handle_event({:chain_event, :transactions, :realtime, [transaction]}) + + assert_receive %Phoenix.Socket.Broadcast{ + topic: ^topic, + event: "transaction", + payload: %{transaction: _} = payload + }, + :timer.seconds(5) + + assert payload.address.hash == address.hash + assert payload.transaction.hash == transaction.hash + + refute_receive %Phoenix.Socket.Broadcast{topic: ^topic, event: "transaction", payload: %{transaction: _}}, + 100, + "Received duplicate broadcast." + end + + test "notified of new_internal_transaction for matching from_address", %{address: address, topic: topic} do + transaction = + :transaction + |> insert(from_address: address) + |> with_block() + + internal_transaction = + insert( + :internal_transaction, + transaction: transaction, + from_address: address, + index: 0, + block_hash: transaction.block_hash, + block_index: 0 + ) + + Notifier.handle_event({:chain_event, :internal_transactions, :realtime, [internal_transaction]}) + + assert_receive %Phoenix.Socket.Broadcast{ + topic: ^topic, + event: "internal_transaction", + payload: %{ + address: %{hash: address_hash}, + internal_transaction: %{transaction_hash: transaction_hash, index: index} + } + }, + :timer.seconds(5) + + assert address_hash == address.hash + assert {transaction_hash, index} == {internal_transaction.transaction_hash, internal_transaction.index} + end + + test "notified of new_internal_transaction for matching to_address", %{address: address, topic: topic} do + transaction = + :transaction + |> insert(to_address: address) + |> with_block() + + internal_transaction = + insert(:internal_transaction, + transaction: transaction, + to_address: address, + index: 0, + block_hash: transaction.block_hash, + block_index: 0 + ) + + Notifier.handle_event({:chain_event, :internal_transactions, :realtime, [internal_transaction]}) + + assert_receive %Phoenix.Socket.Broadcast{ + topic: ^topic, + event: "internal_transaction", + payload: %{ + address: %{hash: address_hash}, + internal_transaction: %{transaction_hash: transaction_hash, index: index} + } + }, + :timer.seconds(5) + + assert address_hash == address.hash + assert {transaction_hash, index} == {internal_transaction.transaction_hash, internal_transaction.index} + end + + test "not notified twice of new_internal_transaction if to and from address are equal", %{ + address: address, + topic: topic + } do + transaction = + :transaction + |> insert(from_address: address, to_address: address) + |> with_block() + + internal_transaction = + insert(:internal_transaction, + transaction: transaction, + from_address: address, + to_address: address, + index: 0, + block_hash: transaction.block_hash, + block_index: 0 + ) + + Notifier.handle_event({:chain_event, :internal_transactions, :realtime, [internal_transaction]}) + + assert_receive %Phoenix.Socket.Broadcast{ + topic: ^topic, + event: "internal_transaction", + payload: %{ + address: %{hash: address_hash}, + internal_transaction: %{transaction_hash: transaction_hash, index: index} + } + }, + :timer.seconds(5) + + assert address_hash == address.hash + assert {transaction_hash, index} == {internal_transaction.transaction_hash, internal_transaction.index} + + refute_receive _, 100, "Received duplicate broadcast." + end + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/channels/block_channel_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/channels/block_channel_test.exs new file mode 100644 index 0000000..bde9e58 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/channels/block_channel_test.exs @@ -0,0 +1,30 @@ +defmodule BlockScoutWeb.BlockChannelTest do + use BlockScoutWeb.ChannelCase + + alias BlockScoutWeb.Notifier + alias Explorer.Chain.Cache.Counters.AverageBlockTime + + test "subscribed user is notified of new_block event" do + topic = "blocks_old:new_block" + @endpoint.subscribe(topic) + + block = insert(:block, number: 1) + + start_supervised!(AverageBlockTime) + Application.put_env(:explorer, AverageBlockTime, enabled: true, cache_period: 1_800_000) + + on_exit(fn -> + Application.put_env(:explorer, AverageBlockTime, enabled: false, cache_period: 1_800_000) + end) + + Notifier.handle_event({:chain_event, :blocks, :realtime, [block]}) + + receive do + %Phoenix.Socket.Broadcast{topic: ^topic, event: "new_block", payload: %{block: _}} -> + assert true + after + :timer.seconds(5) -> + assert false, "Expected message received nothing." + end + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/channels/exchange_rate_channel_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/channels/exchange_rate_channel_test.exs new file mode 100644 index 0000000..87bd370 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/channels/exchange_rate_channel_test.exs @@ -0,0 +1,103 @@ +defmodule BlockScoutWeb.ExchangeRateChannelTest do + use BlockScoutWeb.ChannelCase + + import Mox + + alias BlockScoutWeb.Notifier + alias Explorer.Market + alias Explorer.Market.Fetcher.Coin + alias Explorer.Market.{MarketHistory, Token} + alias Explorer.Market.Source.TestSource + + setup :verify_on_exit! + + setup do + # Use TestSource mock and ets table for this test set + coin_fetcher_configuration = Application.get_env(:explorer, Coin) + market_source_configuration = Application.get_env(:explorer, Explorer.Market.Source) + + Application.put_env(:explorer, Explorer.Market.Source, native_coin_source: TestSource) + Application.put_env(:explorer, Coin, Keyword.merge(coin_fetcher_configuration, table_name: :rates, enabled: true)) + + Coin.init([]) + + token = %Token{ + available_supply: Decimal.new("1000000.0"), + total_supply: Decimal.new("1000000.0"), + btc_value: Decimal.new("1.000"), + last_updated: DateTime.utc_now(), + market_cap: Decimal.new("1000000.0"), + tvl: Decimal.new("2000000.0"), + name: "test", + symbol: Explorer.coin(), + fiat_value: Decimal.new("2.5"), + volume_24h: Decimal.new("1000.0"), + image_url: nil + } + + on_exit(fn -> + Application.put_env(:explorer, Coin, coin_fetcher_configuration) + Application.put_env(:explorer, Explorer.Market.Source, market_source_configuration) + end) + + {:ok, %{token: token}} + end + + describe "new_rate" do + test "subscribed user is notified", %{token: token} do + Coin.handle_info({nil, {{:ok, token}, false}}, %{}) + Supervisor.terminate_child(Explorer.Supervisor, {ConCache, Explorer.Market.MarketHistoryCache.cache_name()}) + Supervisor.restart_child(Explorer.Supervisor, {ConCache, Explorer.Market.MarketHistoryCache.cache_name()}) + + topic = "exchange_rate_old:new_rate" + @endpoint.subscribe(topic) + + Notifier.handle_event({:chain_event, :exchange_rate}) + + receive do + %Phoenix.Socket.Broadcast{topic: ^topic, event: "new_rate", payload: payload} -> + assert payload.exchange_rate == Map.from_struct(token) + assert payload.market_history_data == [] + after + :timer.seconds(5) -> + assert false, "Expected message received nothing." + end + end + + test "subscribed user is notified with market history", %{token: token} do + Coin.handle_info({nil, {{:ok, token}, false}}, %{}) + Supervisor.terminate_child(Explorer.Supervisor, {ConCache, Explorer.Market.MarketHistoryCache.cache_name()}) + Supervisor.restart_child(Explorer.Supervisor, {ConCache, Explorer.Market.MarketHistoryCache.cache_name()}) + + today = Date.utc_today() + + old_records = + for i <- 1..29 do + %{ + date: Timex.shift(today, days: i * -1), + closing_price: Decimal.new(1) + } + end + + records = [%{date: today, closing_price: token.fiat_value} | old_records] + + MarketHistory.bulk_insert(records) + + Market.fetch_recent_history() + + topic = "exchange_rate_old:new_rate" + @endpoint.subscribe(topic) + + Notifier.handle_event({:chain_event, :exchange_rate}) + + receive do + %Phoenix.Socket.Broadcast{topic: ^topic, event: "new_rate", payload: payload} -> + assert payload.exchange_rate == Map.from_struct(token) + assert payload.market_history_data == records + after + :timer.seconds(5) -> + assert false, "Expected message received nothing." + end + end + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/channels/reward_channel_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/channels/reward_channel_test.exs new file mode 100644 index 0000000..06d6241 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/channels/reward_channel_test.exs @@ -0,0 +1,57 @@ +defmodule BlockScoutWeb.RewardChannelTest do + use BlockScoutWeb.ChannelCase, async: false + + alias BlockScoutWeb.Notifier + + describe "user subscribed to rewards" do + test "does nothing if the configuration is turned off" do + Application.put_env(:block_scout_web, BlockScoutWeb.Chain, has_emission_funds: false) + + address = insert(:address) + block = insert(:block) + reward = insert(:reward, address_hash: address.hash, block_hash: block.hash) + + topic = "rewards_old:#{address.hash}" + @endpoint.subscribe(topic) + + Notifier.handle_event({:chain_event, :block_rewards, :realtime, [reward]}) + + refute_receive _, :timer.seconds(2) + + Application.put_env(:block_scout_web, BlockScoutWeb.Chain, has_emission_funds: false) + end + + test "notified of new reward for matching address" do + Application.put_env(:block_scout_web, BlockScoutWeb.Chain, has_emission_funds: true) + address = insert(:address) + block = insert(:block) + reward = insert(:reward, address_hash: address.hash, block_hash: block.hash) + + topic = "rewards_old:#{address.hash}" + @endpoint.subscribe(topic) + + Notifier.handle_event({:chain_event, :block_rewards, :realtime, [reward]}) + + assert_receive %Phoenix.Socket.Broadcast{topic: ^topic, event: "new_reward", payload: _}, :timer.seconds(5) + + Application.put_env(:block_scout_web, BlockScoutWeb.Chain, has_emission_funds: false) + end + + test "not notified of new reward for other address" do + Application.put_env(:block_scout_web, BlockScoutWeb.Chain, has_emission_funds: true) + + address = insert(:address) + block = insert(:block) + reward = insert(:reward, address_hash: address.hash, block_hash: block.hash) + + topic = "rewards_old:0x0" + @endpoint.subscribe(topic) + + Notifier.handle_event({:chain_event, :block_rewards, :realtime, [reward]}) + + refute_receive _, :timer.seconds(2) + + Application.put_env(:block_scout_web, BlockScoutWeb.Chain, has_emission_funds: false) + end + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/channels/transaction_channel_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/channels/transaction_channel_test.exs new file mode 100644 index 0000000..b41454e --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/channels/transaction_channel_test.exs @@ -0,0 +1,67 @@ +defmodule BlockScoutWeb.TransactionChannelTest do + use BlockScoutWeb.ChannelCase + + alias Explorer.Chain.Hash + alias BlockScoutWeb.Notifier + + test "subscribed user is notified of new_transaction topic" do + topic = "transactions_old:new_transaction" + @endpoint.subscribe(topic) + + transaction = + :transaction + |> insert() + |> with_block() + + Notifier.handle_event({:chain_event, :transactions, :realtime, [transaction]}) + + receive do + %Phoenix.Socket.Broadcast{topic: ^topic, event: "transaction", payload: %{transaction: _transaction} = payload} -> + assert payload.transaction.hash == transaction.hash + after + :timer.seconds(5) -> + assert false, "Expected message received nothing." + end + end + + test "subscribed user is notified of new_pending_transaction topic" do + topic = "transactions_old:new_pending_transaction" + @endpoint.subscribe(topic) + + pending = insert(:transaction) + + Notifier.handle_event({:chain_event, :transactions, :realtime, [pending]}) + + receive do + %Phoenix.Socket.Broadcast{ + topic: ^topic, + event: "pending_transaction", + payload: %{transaction: _transaction} = payload + } -> + assert payload.transaction.hash == pending.hash + after + :timer.seconds(5) -> + assert false, "Expected message received nothing." + end + end + + test "subscribed user is notified of transaction_hash collated event" do + transaction = + :transaction + |> insert() + |> with_block() + + topic = "transactions_old:#{Hash.to_string(transaction.hash)}" + @endpoint.subscribe(topic) + + Notifier.handle_event({:chain_event, :transactions, :realtime, [transaction]}) + + receive do + %Phoenix.Socket.Broadcast{topic: ^topic, event: "collated", payload: %{}} -> + assert true + after + :timer.seconds(5) -> + assert false, "Expected message received nothing." + end + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/channels/v2/address_channel_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/channels/v2/address_channel_test.exs new file mode 100644 index 0000000..b7a245e --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/channels/v2/address_channel_test.exs @@ -0,0 +1,129 @@ +defmodule BlockScoutWeb.V2.AddressChannelTest do + use BlockScoutWeb.ChannelCase, + # ETS tables are shared in `Explorer.Chain.Cache.Counters.AddressesCount` + async: false + + alias BlockScoutWeb.Notifier + alias Explorer.Chain.Wei + alias Explorer.Chain.Cache.Counters.AddressesCount + + test "subscribed user is notified of new_address count event" do + topic = "addresses:new_address" + @endpoint.subscribe(topic) + + address = insert(:address) + + start_supervised!(AddressesCount) + AddressesCount.consolidate() + + Notifier.handle_event({:chain_event, :addresses, :realtime, [address]}) + + assert_receive %Phoenix.Socket.Broadcast{topic: ^topic, event: "count", payload: %{count: _}}, :timer.seconds(5) + end + + describe "user subscribed to address" do + setup do + address = insert(:address) + topic = "addresses:#{address.hash}" + @endpoint.subscribe(topic) + {:ok, %{address: address, topic: topic}} + end + + test "notified of balance_update for matching address", %{address: address, topic: topic} do + {:ok, balance} = Wei.cast(1) + address_with_balance = %{address | fetched_coin_balance: balance} + + start_supervised!(AddressesCount) + AddressesCount.consolidate() + + Notifier.handle_event({:chain_event, :addresses, :realtime, [address_with_balance]}) + + assert_receive %Phoenix.Socket.Broadcast{topic: ^topic, event: "balance", payload: payload}, + :timer.seconds(5) + + assert payload.balance == balance.value + end + + test "not notified of balance_update if fetched_coin_balance is nil", %{address: address} do + start_supervised!(AddressesCount) + AddressesCount.consolidate() + + Notifier.handle_event({:chain_event, :addresses, :realtime, [address]}) + + refute_receive _, 100, "Message was broadcast for nil fetched_coin_balance." + end + + test "notified of new_pending_transaction for matching from_address", %{address: address, topic: topic} do + pending = insert(:transaction, from_address: address) + + Notifier.handle_event({:chain_event, :transactions, :realtime, [pending]}) + + assert_receive %Phoenix.Socket.Broadcast{ + topic: ^topic, + event: "pending_transaction", + payload: %{transactions: _} = payload + }, + :timer.seconds(5) + + assert List.first(payload.transactions)["hash"] == pending.hash + end + + test "notified of new_transaction for matching from_address", %{address: address, topic: topic} do + transaction = + :transaction + |> insert(from_address: address) + |> with_block() + + Notifier.handle_event({:chain_event, :transactions, :realtime, [transaction]}) + + assert_receive %Phoenix.Socket.Broadcast{ + topic: ^topic, + event: "transaction", + payload: %{transactions: _} = payload + }, + :timer.seconds(5) + + assert List.first(payload.transactions)["hash"] == transaction.hash + end + + test "notified of new_transaction for matching to_address", %{address: address, topic: topic} do + transaction = + :transaction + |> insert(to_address: address) + |> with_block() + + Notifier.handle_event({:chain_event, :transactions, :realtime, [transaction]}) + + assert_receive %Phoenix.Socket.Broadcast{ + topic: ^topic, + event: "transaction", + payload: %{transactions: _} = payload + }, + :timer.seconds(5) + + assert List.first(payload.transactions)["hash"] == transaction.hash + end + + test "not notified twice of new_transaction if to and from address are equal", %{address: address, topic: topic} do + transaction = + :transaction + |> insert(from_address: address, to_address: address) + |> with_block() + + Notifier.handle_event({:chain_event, :transactions, :realtime, [transaction]}) + + assert_receive %Phoenix.Socket.Broadcast{ + topic: ^topic, + event: "transaction", + payload: %{transactions: _} = payload + }, + :timer.seconds(5) + + assert List.first(payload.transactions)["hash"] == transaction.hash + + refute_receive %Phoenix.Socket.Broadcast{topic: ^topic, event: "transaction", payload: %{transactions: _}}, + 100, + "Received duplicate broadcast." + end + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/channels/v2/block_channel_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/channels/v2/block_channel_test.exs new file mode 100644 index 0000000..2610a73 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/channels/v2/block_channel_test.exs @@ -0,0 +1,30 @@ +defmodule BlockScoutWeb.V2.BlockChannelTest do + use BlockScoutWeb.ChannelCase + + alias BlockScoutWeb.Notifier + alias Explorer.Chain.Cache.Counters.AverageBlockTime + + test "subscribed user is notified of new_block event" do + topic = "blocks:new_block" + @endpoint.subscribe(topic) + + block = insert(:block, number: 1) + + start_supervised!(AverageBlockTime) + Application.put_env(:explorer, AverageBlockTime, enabled: true, cache_period: 1_800_000) + + on_exit(fn -> + Application.put_env(:explorer, AverageBlockTime, enabled: false, cache_period: 1_800_000) + end) + + Notifier.handle_event({:chain_event, :blocks, :realtime, [block]}) + + receive do + %Phoenix.Socket.Broadcast{topic: ^topic, event: "new_block", payload: %{block: _}} -> + assert true + after + :timer.seconds(5) -> + assert false, "Expected message received nothing." + end + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/channels/v2/exchange_rate_channel_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/channels/v2/exchange_rate_channel_test.exs new file mode 100644 index 0000000..bb4df68 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/channels/v2/exchange_rate_channel_test.exs @@ -0,0 +1,102 @@ +defmodule BlockScoutWeb.V2.ExchangeRateChannelTest do + use BlockScoutWeb.ChannelCase + + import Mox + + alias BlockScoutWeb.Notifier + alias Explorer.Market.Fetcher.{Coin, History} + alias Explorer.Market.{MarketHistory, Token} + alias Explorer.Market.Source.OneCoinSource + alias Explorer.Market + + setup :verify_on_exit! + + setup do + # Use TestSource mock and ets table for this test set + coin_fetcher_configuration = Application.get_env(:explorer, Coin) + market_configuration = Application.get_env(:explorer, Market) + Application.put_env(:explorer, Market, native_coin_source: OneCoinSource) + Application.put_env(:explorer, Coin, enabled: true, store: :ets) + + Coin.init([]) + + token = %Token{ + available_supply: Decimal.new(10_000_000), + total_supply: Decimal.new(10_000_000_000), + btc_value: Decimal.new(1), + last_updated: Timex.now(), + market_cap: Decimal.new(10_000_000), + tvl: Decimal.new(100_500_000), + name: "", + symbol: Explorer.coin(), + fiat_value: Decimal.new(1), + volume_24h: Decimal.new(1), + image_url: nil + } + + on_exit(fn -> + Application.put_env(:explorer, Coin, coin_fetcher_configuration) + Application.put_env(:explorer, Market, market_configuration) + end) + + {:ok, %{token: token}} + end + + describe "new_rate" do + test "subscribed user is notified", %{token: token} do + Coin.handle_info({nil, {{:ok, token}, false}}, %{}) + Supervisor.terminate_child(Explorer.Supervisor, {ConCache, Explorer.Market.MarketHistoryCache.cache_name()}) + Supervisor.restart_child(Explorer.Supervisor, {ConCache, Explorer.Market.MarketHistoryCache.cache_name()}) + + topic = "exchange_rate:new_rate" + @endpoint.subscribe(topic) + + Notifier.handle_event({:chain_event, :exchange_rate}) + + receive do + %Phoenix.Socket.Broadcast{topic: ^topic, event: "new_rate", payload: payload} -> + assert payload.exchange_rate == token.fiat_value + assert payload.chart_data == [] + after + :timer.seconds(5) -> + assert false, "Expected message received nothing." + end + end + + test "subscribed user is notified with market history", %{token: token} do + Coin.handle_info({nil, {{:ok, token}, false}}, %{}) + Supervisor.terminate_child(Explorer.Supervisor, {ConCache, Explorer.Market.MarketHistoryCache.cache_name()}) + Supervisor.restart_child(Explorer.Supervisor, {ConCache, Explorer.Market.MarketHistoryCache.cache_name()}) + + today = Date.utc_today() + + old_records = + for i <- 1..29 do + %{ + date: Timex.shift(today, days: i * -1), + closing_price: Decimal.new(1) + } + end + + records = [%{date: today, closing_price: token.fiat_value} | old_records] + + MarketHistory.bulk_insert(records) + + Market.fetch_recent_history() + + topic = "exchange_rate:new_rate" + @endpoint.subscribe(topic) + + Notifier.handle_event({:chain_event, :exchange_rate}) + + receive do + %Phoenix.Socket.Broadcast{topic: ^topic, event: "new_rate", payload: payload} -> + assert payload.exchange_rate == token.fiat_value + assert payload.chart_data == records + after + :timer.seconds(5) -> + assert false, "Expected message received nothing." + end + end + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/channels/v2/reward_channel_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/channels/v2/reward_channel_test.exs new file mode 100644 index 0000000..41709a8 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/channels/v2/reward_channel_test.exs @@ -0,0 +1,57 @@ +defmodule BlockScoutWeb.V2.RewardChannelTest do + use BlockScoutWeb.ChannelCase, async: false + + alias BlockScoutWeb.Notifier + + describe "user subscribed to rewards" do + test "does nothing if the configuration is turned off" do + Application.put_env(:block_scout_web, BlockScoutWeb.Chain, has_emission_funds: false) + + address = insert(:address) + block = insert(:block) + reward = insert(:reward, address_hash: address.hash, block_hash: block.hash) + + topic = "rewards:#{address.hash}" + @endpoint.subscribe(topic) + + Notifier.handle_event({:chain_event, :block_rewards, :realtime, [reward]}) + + refute_receive _, :timer.seconds(2) + + Application.put_env(:block_scout_web, BlockScoutWeb.Chain, has_emission_funds: false) + end + + test "notified of new reward for matching address" do + Application.put_env(:block_scout_web, BlockScoutWeb.Chain, has_emission_funds: true) + address = insert(:address) + block = insert(:block) + reward = insert(:reward, address_hash: address.hash, block_hash: block.hash) + + topic = "rewards:#{address.hash}" + @endpoint.subscribe(topic) + + Notifier.handle_event({:chain_event, :block_rewards, :realtime, [reward]}) + + assert_receive %Phoenix.Socket.Broadcast{topic: ^topic, event: "new_reward", payload: _}, :timer.seconds(5) + + Application.put_env(:block_scout_web, BlockScoutWeb.Chain, has_emission_funds: false) + end + + test "not notified of new reward for other address" do + Application.put_env(:block_scout_web, BlockScoutWeb.Chain, has_emission_funds: true) + + address = insert(:address) + block = insert(:block) + reward = insert(:reward, address_hash: address.hash, block_hash: block.hash) + + topic = "rewards:0x0" + @endpoint.subscribe(topic) + + Notifier.handle_event({:chain_event, :block_rewards, :realtime, [reward]}) + + refute_receive _, :timer.seconds(2) + + Application.put_env(:block_scout_web, BlockScoutWeb.Chain, has_emission_funds: false) + end + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/channels/v2/transaction_channel_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/channels/v2/transaction_channel_test.exs new file mode 100644 index 0000000..c1c7c42 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/channels/v2/transaction_channel_test.exs @@ -0,0 +1,46 @@ +defmodule BlockScoutWeb.V2.TransactionChannelTest do + use BlockScoutWeb.ChannelCase + + alias BlockScoutWeb.Notifier + + test "subscribed user is notified of new_transaction topic" do + topic = "transactions:new_transaction" + @endpoint.subscribe(topic) + + transaction = + :transaction + |> insert() + |> with_block() + + Notifier.handle_event({:chain_event, :transactions, :realtime, [transaction]}) + + receive do + %Phoenix.Socket.Broadcast{topic: ^topic, event: "transaction", payload: %{transaction: _transaction} = payload} -> + assert payload.transaction == 1 + after + :timer.seconds(5) -> + assert false, "Expected message received nothing." + end + end + + test "subscribed user is notified of new_pending_transaction topic" do + topic = "transactions:new_pending_transaction" + @endpoint.subscribe(topic) + + pending = insert(:transaction) + + Notifier.handle_event({:chain_event, :transactions, :realtime, [pending]}) + + receive do + %Phoenix.Socket.Broadcast{ + topic: ^topic, + event: "pending_transaction", + payload: %{pending_transaction: _transaction} = payload + } -> + assert payload.pending_transaction == 1 + after + :timer.seconds(5) -> + assert false, "Expected message received nothing." + end + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/channels/v2/websocket_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/channels/v2/websocket_test.exs new file mode 100644 index 0000000..6b974d7 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/channels/v2/websocket_test.exs @@ -0,0 +1,403 @@ +defmodule BlockScoutWeb.V2.WebsocketTest do + use BlockScoutWeb.ChannelCase, async: false + + alias BlockScoutWeb.Notifier + alias Explorer.Chain.Events.Subscriber + alias Explorer.Chain.{Address, Import, Token, TokenTransfer, Transaction} + alias Explorer.Repo + + @first_topic_hex_string "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef" + @second_topic_hex_string "0x000000000000000000000000e8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca" + @third_topic_hex_string "0x000000000000000000000000515c09c5bba1ed566b02a5b0599ec5d5d0aee73d" + + describe "websocket v2" do + {:ok, first_topic} = Explorer.Chain.Hash.Full.cast(@first_topic_hex_string) + {:ok, second_topic} = Explorer.Chain.Hash.Full.cast(@second_topic_hex_string) + {:ok, third_topic} = Explorer.Chain.Hash.Full.cast(@third_topic_hex_string) + + @import_data %{ + blocks: %{ + params: [ + %{ + consensus: true, + difficulty: 340_282_366_920_938_463_463_374_607_431_768_211_454, + gas_limit: 6_946_336, + gas_used: 50450, + hash: "0xf6b4b8c88df3ebd252ec476328334dc026cf66606a84fb769b3d3cbccc8471bd", + miner_hash: "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca", + nonce: 0, + number: 37, + parent_hash: "0xc37bbad7057945d1bf128c1ff009fb1ad632110bf6a000aac025a80f7766b66e", + size: 719, + timestamp: Timex.parse!("2017-12-15T21:06:30.000000Z", "{ISO:Extended:Z}"), + total_difficulty: 12_590_447_576_074_723_148_144_860_474_975_121_280_509 + } + ], + timeout: 5 + }, + broadcast: :realtime, + logs: %{ + params: [ + %{ + block_hash: "0xf6b4b8c88df3ebd252ec476328334dc026cf66606a84fb769b3d3cbccc8471bd", + address_hash: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b", + data: "0x0000000000000000000000000000000000000000000000000de0b6b3a7640000", + first_topic: first_topic, + second_topic: second_topic, + third_topic: third_topic, + fourth_topic: nil, + index: 0, + transaction_hash: "0x53bd884872de3e488692881baeec262e7b95234d3965248c39fe992fffd433e5" + }, + %{ + block_hash: "0xf6b4b8c88df3ebd252ec476328334dc026cf66606a84fb769b3d3cbccc8471bd", + address_hash: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b", + data: "0x0000000000000000000000000000000000000000000000000de0b6b3a7640000", + first_topic: first_topic, + second_topic: second_topic, + third_topic: third_topic, + fourth_topic: nil, + index: 1, + transaction_hash: "0x53bd884872de3e488692881baeec262e7b95234d3965248c39fe992fffd433e5" + }, + %{ + block_hash: "0xf6b4b8c88df3ebd252ec476328334dc026cf66606a84fb769b3d3cbccc8471bd", + address_hash: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b", + data: "0x0000000000000000000000000000000000000000000000000de0b6b3a7640000", + first_topic: first_topic, + second_topic: second_topic, + third_topic: third_topic, + fourth_topic: nil, + index: 2, + transaction_hash: "0x53bd884872de3e488692881baeec262e7b95234d3965248c39fe992fffd433e5" + } + ], + timeout: 5 + }, + transactions: %{ + params: [ + %{ + block_hash: "0xf6b4b8c88df3ebd252ec476328334dc026cf66606a84fb769b3d3cbccc8471bd", + block_number: 37, + block_timestamp: Timex.parse!("2017-12-15T21:06:30.000000Z", "{ISO:Extended:Z}"), + cumulative_gas_used: 50450, + from_address_hash: "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca", + gas: 4_700_000, + gas_price: 100_000_000_000, + gas_used: 50450, + hash: "0x53bd884872de3e488692881baeec262e7b95234d3965248c39fe992fffd433e5", + index: 0, + input: "0x10855269000000000000000000000000862d67cb0773ee3f8ce7ea89b328ffea861ab3ef", + nonce: 4, + public_key: + "0xe5d196ad4ceada719d9e592f7166d0c75700f6eab2e3c3de34ba751ea786527cb3f6eb96ad9fdfdb9989ff572df50f1c42ef800af9c5207a38b929aff969b5c9", + r: 0xA7F8F45CCE375BB7AF8750416E1B03E0473F93C256DA2285D1134FC97A700E01, + s: 0x1F87A076F13824F4BE8963E3DFFD7300DAE64D5F23C9A062AF0C6EAD347C135F, + standard_v: 1, + status: :ok, + to_address_hash: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b", + v: 0xBE, + value: 0 + }, + %{ + block_hash: "0xf6b4b8c88df3ebd252ec476328334dc026cf66606a84fb769b3d3cbccc8471bd", + block_number: 37, + block_timestamp: Timex.parse!("2017-12-15T21:06:30.000000Z", "{ISO:Extended:Z}"), + cumulative_gas_used: 50450, + from_address_hash: "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca", + gas: 4_700_000, + gas_price: 100_000_000_000, + gas_used: 50450, + hash: "0x00bd884872de3e488692881baeec262e7b95234d3965248c39fe992fffd433e1", + index: 1, + input: "0x10855269000000000000000000000000862d67cb0773ee3f8ce7ea89b328ffea861ab3ef", + nonce: 5, + public_key: + "0xe5d196ad4ceada719d9e592f7166d0c75700f6eab2e3c3de34ba751ea786527cb3f6eb96ad9fdfdb9989ff572df50f1c42ef800af9c5207a38b929aff969b5c9", + r: 0xA7F8F45CCE375BB7AF8750416E1B03E0473F93C256DA2285D1134FC97A700E09, + s: 0x1F87A076F13824F4BE8963E3DFFD7300DAE64D5F23C9A062AF0C6EAD347C1354, + standard_v: 1, + status: :ok, + to_address_hash: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b", + v: 0xBE, + value: 0 + }, + %{ + from_address_hash: "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca", + gas: 4_700_000, + gas_price: 100_000_000_000, + hash: "0x53bd884872de3e488692881baeec262e7b95234d3965248c39fe992fffd433e0", + input: "0x10855269000000000000000000000000862d67cb0773ee3f8ce7ea89b328ffea861ab3ef", + nonce: 6, + public_key: + "0xe5d196ad4ceada719d9e592f7166d0c75700f6eab2e3c3de34ba751ea786527cb3f6eb96ad9fdfdb9989ff572df50f1c42ef800af9c5207a38b929aff969b5c9", + r: 0xA7F8F45CCE375BB7AF8750416E1B03E0473F93C256DA2285D1134FC97A700E09, + s: 0x1F87A076F13824F4BE8963E3DFFD7300DAE64D5F23C9A062AF0C6EAD347C1354, + standard_v: 1, + to_address_hash: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b", + v: 0xBE, + value: 0 + }, + %{ + from_address_hash: "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca", + gas: 4_700_000, + gas_price: 100_000_000_000, + hash: "0x00bd884872de3e488692881baeec262e7b95234d3965248c39fe992fffd43312", + input: "0x10855269000000000000000000000000862d67cb0773ee3f8ce7ea89b328ffea861ab3ef", + nonce: 7, + public_key: + "0xe5d196ad4ceada719d9e592f7166d0c75700f6eab2e3c3de34ba751ea786527cb3f6eb96ad9fdfdb9989ff572df50f1c42ef800af9c5207a38b929aff969b5c9", + r: 0xA7F8F45CCE375BB7AF8750416E1B03E0473F93C256DA2285D1134FC97A700E09, + s: 0x1F87A076F13824F4BE8963E3DFFD7300DAE64D5F23C9A062AF0C6EAD347C1354, + standard_v: 1, + to_address_hash: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b", + v: 0xBE, + value: 0 + } + ], + timeout: 5 + }, + addresses: %{ + params: [ + %{hash: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b"}, + %{hash: "0x00f38d4764929064f2d4d3a56520a76ab3df4151"}, + %{hash: "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca"}, + %{hash: "0x515c09c5bba1ed566b02a5b0599ec5d5d0aee73d"} + ], + timeout: 5 + }, + tokens: %{ + on_conflict: :nothing, + params: [ + %{ + contract_address_hash: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b", + type: "ERC-20" + }, + %{ + contract_address_hash: "0x00f38d4764929064f2d4d3a56520a76ab3df4151", + type: "ERC-20" + } + ], + timeout: 5 + }, + token_transfers: %{ + params: [ + %{ + amount: Decimal.new(1_000_000_000_000_000_000), + block_number: 37, + block_hash: "0xf6b4b8c88df3ebd252ec476328334dc026cf66606a84fb769b3d3cbccc8471bd", + log_index: 0, + from_address_hash: "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca", + to_address_hash: "0x515c09c5bba1ed566b02a5b0599ec5d5d0aee73d", + token_contract_address_hash: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b", + token_type: "ERC-20", + transaction_hash: "0x53bd884872de3e488692881baeec262e7b95234d3965248c39fe992fffd433e5" + }, + %{ + amount: Decimal.new(1_000_000_000_000_000_000), + block_number: 37, + block_hash: "0xf6b4b8c88df3ebd252ec476328334dc026cf66606a84fb769b3d3cbccc8471bd", + log_index: 1, + from_address_hash: "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca", + to_address_hash: "0x515c09c5bba1ed566b02a5b0599ec5d5d0aee73d", + token_contract_address_hash: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b", + token_type: "ERC-20", + transaction_hash: "0x53bd884872de3e488692881baeec262e7b95234d3965248c39fe992fffd433e5" + }, + %{ + amount: Decimal.new(1_000_000_000_000_000_000), + block_number: 37, + block_hash: "0xf6b4b8c88df3ebd252ec476328334dc026cf66606a84fb769b3d3cbccc8471bd", + log_index: 2, + from_address_hash: "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca", + to_address_hash: "0x515c09c5bba1ed566b02a5b0599ec5d5d0aee73d", + token_contract_address_hash: "0x00f38d4764929064f2d4d3a56520a76ab3df4151", + token_type: "ERC-20", + transaction_hash: "0x53bd884872de3e488692881baeec262e7b95234d3965248c39fe992fffd433e5" + } + ], + timeout: 5 + } + } + + test "broadcasted several transactions in one message" do + topic = "transactions:new_transaction" + + {:ok, _reply, _socket} = + BlockScoutWeb.V2.UserSocket + |> socket("no_id", %{}) + |> subscribe_and_join(topic) + + topic_pending = "transactions:new_pending_transaction" + + {:ok, _reply, _socket} = + BlockScoutWeb.V2.UserSocket + |> socket("no_id", %{}) + |> subscribe_and_join(topic_pending) + + Subscriber.to(:transactions, :realtime) + Import.all(@import_data) + assert_receive {:chain_event, :transactions, :realtime, transactions}, :timer.seconds(5) + + Notifier.handle_event({:chain_event, :transactions, :realtime, transactions}) + + assert_receive %Phoenix.Socket.Message{ + payload: %{transaction: 2}, + event: "transaction", + topic: ^topic + }, + :timer.seconds(5) + + assert_receive %Phoenix.Socket.Message{ + payload: %{pending_transaction: 2}, + event: "pending_transaction", + topic: ^topic_pending + }, + :timer.seconds(5) + end + + test "broadcast token transfers" do + topic_token = "tokens:0x8bf38d4764929064f2d4d3a56520a76ab3df415b" + + {:ok, _reply, _socket} = + BlockScoutWeb.V2.UserSocket + |> socket("no_id", %{}) + |> subscribe_and_join(topic_token) + + Subscriber.to(:token_transfers, :realtime) + + Import.all(@import_data) + + assert_receive {:chain_event, :token_transfers, :realtime, token_transfers}, :timer.seconds(5) + + Notifier.handle_event({:chain_event, :token_transfers, :realtime, token_transfers}) + + assert_receive %Phoenix.Socket.Message{ + payload: %{token_transfer: 2}, + event: "token_transfer", + topic: ^topic_token + }, + :timer.seconds(5) + end + + test "broadcast array of transactions to address" do + topic = "addresses:0x8bf38d4764929064f2d4d3a56520a76ab3df415b" + + {:ok, _reply, _socket} = + BlockScoutWeb.V2.UserSocket + |> socket("no_id", %{}) + |> subscribe_and_join(topic) + + Subscriber.to(:transactions, :realtime) + Import.all(@import_data) + + assert_receive {:chain_event, :transactions, :realtime, transactions}, :timer.seconds(5) + Notifier.handle_event({:chain_event, :transactions, :realtime, transactions}) + + assert_receive %Phoenix.Socket.Message{ + payload: %{transactions: [transaction_1, transaction_2]}, + event: "transaction", + topic: ^topic + }, + :timer.seconds(5) + + transaction_1 = transaction_1 |> Jason.encode!() |> Jason.decode!() + compare_item(Repo.get_by(Transaction, %{hash: transaction_1["hash"]}), transaction_1) + + transaction_2 = transaction_2 |> Jason.encode!() |> Jason.decode!() + compare_item(Repo.get_by(Transaction, %{hash: transaction_2["hash"]}), transaction_2) + + assert_receive %Phoenix.Socket.Message{ + payload: %{transactions: [transaction_1, transaction_2]}, + event: "pending_transaction", + topic: ^topic + }, + :timer.seconds(5) + + transaction_1 = transaction_1 |> Jason.encode!() |> Jason.decode!() + compare_item(Repo.get_by(Transaction, %{hash: transaction_1["hash"]}), transaction_1) + + transaction_2 = transaction_2 |> Jason.encode!() |> Jason.decode!() + compare_item(Repo.get_by(Transaction, %{hash: transaction_2["hash"]}), transaction_2) + end + + test "broadcast array of transfers to address" do + topic = "addresses:0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca" + + {:ok, _reply, _socket} = + BlockScoutWeb.V2.UserSocket + |> socket("no_id", %{}) + |> subscribe_and_join(topic) + + topic_1 = "addresses:0x515c09c5bba1ed566b02a5b0599ec5d5d0aee73d" + + {:ok, _reply, _socket} = + BlockScoutWeb.V2.UserSocket + |> socket("no_id", %{}) + |> subscribe_and_join(topic_1) + + Subscriber.to(:token_transfers, :realtime) + Import.all(@import_data) + + assert_receive {:chain_event, :token_transfers, :realtime, token_transfers}, :timer.seconds(5) + Notifier.handle_event({:chain_event, :token_transfers, :realtime, token_transfers}) + + assert_receive %Phoenix.Socket.Message{ + payload: %{token_transfers: [_, _, _] = transfers}, + event: "token_transfer", + topic: ^topic + }, + :timer.seconds(5) + + token_transfers + |> Enum.zip(transfers) + |> Enum.each(fn {transfer, json} -> compare_item(transfer, json |> Jason.encode!() |> Jason.decode!()) end) + + assert_receive %Phoenix.Socket.Message{ + payload: %{token_transfers: [_, _, _] = transfers}, + event: "token_transfer", + topic: ^topic_1 + }, + :timer.seconds(5) + + token_transfers + |> Enum.zip(transfers) + |> Enum.each(fn {transfer, json} -> compare_item(transfer, json |> Jason.encode!() |> Jason.decode!()) end) + end + end + + defp compare_item(%TokenTransfer{} = token_transfer, json) do + assert Address.checksum(token_transfer.from_address_hash) == json["from"]["hash"] + assert Address.checksum(token_transfer.to_address_hash) == json["to"]["hash"] + assert to_string(token_transfer.transaction_hash) == json["transaction_hash"] + assert json["timestamp"] != nil + assert json["method"] != nil + assert to_string(token_transfer.block_hash) == json["block_hash"] + assert token_transfer.log_index == json["log_index"] + assert check_total(Repo.preload(token_transfer, [{:token, :contract_address}]).token, json["total"], token_transfer) + end + + defp compare_item(%Transaction{} = transaction, json) do + assert to_string(transaction.hash) == json["hash"] + assert transaction.block_number == json["block_number"] + assert to_string(transaction.value.value) == json["value"] + assert Address.checksum(transaction.from_address_hash) == json["from"]["hash"] + assert Address.checksum(transaction.to_address_hash) == json["to"]["hash"] + end + + # with the current implementation no transfers should come with list in totals + defp check_total(%Token{type: nft}, json, _token_transfer) when nft in ["ERC-721", "ERC-1155"] and is_list(json) do + false + end + + defp check_total(%Token{type: nft}, json, token_transfer) when nft in ["ERC-1155"] do + json["token_id"] in Enum.map(token_transfer.token_ids, fn x -> to_string(x) end) and + json["value"] == to_string(token_transfer.amount) + end + + defp check_total(%Token{type: nft}, json, token_transfer) when nft in ["ERC-721"] do + json["token_id"] in Enum.map(token_transfer.token_ids, fn x -> to_string(x) end) + end + + defp check_total(_, _, _), do: true +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/account/api/v2/user_controller_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/account/api/v2/user_controller_test.exs new file mode 100644 index 0000000..339be0b --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/account/api/v2/user_controller_test.exs @@ -0,0 +1,1284 @@ +defmodule BlockScoutWeb.Account.Api.V2.UserControllerTest do + use BlockScoutWeb.ConnCase + + alias Explorer.Account.{ + Identity, + TagAddress, + TagTransaction, + WatchlistAddress + } + + alias Explorer.Chain.Address + alias Explorer.Repo + + setup %{conn: conn} do + auth = build(:auth) + + {:ok, user} = Identity.find_or_create(auth) + + {:ok, user: user, conn: Plug.Test.init_test_session(conn, current_user: user)} + end + + describe "Test account/api/account/v2/user" do + test "get user info", %{conn: conn, user: user} do + result_conn = + conn + |> get("/api/account/v2/user/info") + |> doc(description: "Get info about user") + + assert json_response(result_conn, 200) == %{ + "nickname" => user.nickname, + "name" => user.name, + "email" => user.email, + "avatar" => user.avatar, + "address_hash" => user.address_hash + } + end + + test "post private address tag", %{conn: conn} do + tag_address_response = + conn + |> post("/api/account/v2/user/tags/address", %{ + "address_hash" => "0x3e9ac8f16c92bc4f093357933b5befbf1e16987b", + "name" => "MyName" + }) + |> doc(description: "Add private address tag") + |> json_response(200) + + conn + |> get("/api/account/v2/tags/address/0x3e9ac8f16c92bc4f093357933b5befbf1e16987b") + |> doc(description: "Get tags for address") + |> json_response(200) + + assert tag_address_response["address_hash"] == "0x3e9ac8f16c92bc4f093357933b5befbf1e16987b" + assert tag_address_response["name"] == "MyName" + assert tag_address_response["id"] + end + + test "can't insert private address tags more than limit", %{conn: conn, user: user} do + old_env = Application.get_env(:explorer, Explorer.Account) + + new_env = + old_env + |> Keyword.replace(:private_tags_limit, 5) + |> Keyword.replace(:watchlist_addresses_limit, 5) + + Application.put_env(:explorer, Explorer.Account, new_env) + + for _ <- 0..3 do + build(:tag_address_db, user: user) |> Repo.account_repo().insert() + end + + assert conn + |> post("/api/account/v2/user/tags/address", build(:tag_address)) + |> json_response(200) + + assert conn + |> post("/api/account/v2/user/tags/address", build(:tag_address)) + |> json_response(422) + + Application.put_env(:explorer, Explorer.Account, old_env) + end + + test "check address tags pagination", %{conn: conn, user: user} do + tags_address = + for _ <- 0..50 do + build(:tag_address_db, user: user) |> Repo.account_repo().insert!() + end + + assert response = + conn + |> get("/api/account/v2/user/tags/address") + |> json_response(200) + + response_1 = + conn + |> get("/api/account/v2/user/tags/address", response["next_page_params"]) + |> json_response(200) + + check_paginated_response(response, response_1, tags_address) + end + + test "edit private address tag", %{conn: conn} do + address_tag = build(:tag_address) + + tag_address_response = + conn + |> post("/api/account/v2/user/tags/address", address_tag) + |> json_response(200) + + _response = + conn + |> get("/api/account/v2/user/tags/address") + |> json_response(200) + |> Map.get("items") == [tag_address_response] + + assert tag_address_response["address_hash"] == address_tag["address_hash"] + assert tag_address_response["name"] == address_tag["name"] + assert tag_address_response["id"] + + new_address_tag = build(:tag_address) + + new_tag_address_response = + conn + |> put("/api/account/v2/user/tags/address/#{tag_address_response["id"]}", new_address_tag) + |> doc(description: "Edit private address tag") + |> json_response(200) + + assert new_tag_address_response["address_hash"] == new_address_tag["address_hash"] + assert new_tag_address_response["name"] == new_address_tag["name"] + assert new_tag_address_response["id"] == tag_address_response["id"] + end + + test "get address tags after inserting one private tags", %{conn: conn} do + addresses = Enum.map(0..2, fn _x -> to_string(build(:address).hash) end) + names = Enum.map(0..2, fn x -> "name#{x}" end) + zipped = Enum.zip(addresses, names) + + created = + Enum.map(zipped, fn {addr, name} -> + id = + (conn + |> post("/api/account/v2/user/tags/address", %{ + "address_hash" => addr, + "name" => name + }) + |> json_response(200))["id"] + + {addr, %{"display_name" => name, "label" => name, "address_hash" => addr}, + %{ + "address_hash" => addr, + "id" => id, + "name" => name, + "address" => %{ + "hash" => Address.checksum(addr), + "proxy_type" => nil, + "implementations" => [], + "is_contract" => false, + "is_verified" => false, + "name" => nil, + "private_tags" => [], + "public_tags" => [], + "watchlist_names" => [], + "ens_domain_name" => nil, + "metadata" => nil + } + }} + end) + + assert Enum.all?(created, fn {addr, map_tag, map} -> + response = + conn + |> get("/api/account/v2/tags/address/#{addr}") + |> json_response(200) + + response["personal_tags"] == [map_tag] + end) + + response = + conn + |> get("/api/account/v2/user/tags/address") + |> doc(description: "Get private addresses tags") + |> json_response(200) + |> Map.get("items") + + assert Enum.all?(created, fn {_, _, map} -> + Enum.any?(response, fn item -> + addresses_json_match?(map, item) + end) + end) + end + + test "delete address tag", %{conn: conn} do + addresses = Enum.map(0..2, fn _x -> to_string(build(:address).hash) end) + names = Enum.map(0..2, fn x -> "name#{x}" end) + zipped = Enum.zip(addresses, names) + + created = + Enum.map(zipped, fn {addr, name} -> + id = + (conn + |> post("/api/account/v2/user/tags/address", %{ + "address_hash" => addr, + "name" => name + }) + |> json_response(200))["id"] + + {addr, %{"display_name" => name, "label" => name, "address_hash" => addr}, + %{ + "address_hash" => addr, + "id" => id, + "name" => name, + "address" => %{ + "hash" => Address.checksum(addr), + "proxy_type" => nil, + "implementations" => [], + "is_contract" => false, + "is_verified" => false, + "name" => nil, + "private_tags" => [], + "public_tags" => [], + "watchlist_names" => [], + "ens_domain_name" => nil, + "metadata" => nil + } + }} + end) + + assert Enum.all?(created, fn {addr, map_tag, map} -> + response = + conn + |> get("/api/account/v2/tags/address/#{addr}") + |> json_response(200) + + response["personal_tags"] == [map_tag] + end) + + response = + conn + |> get("/api/account/v2/user/tags/address") + |> json_response(200) + |> Map.get("items") + + assert Enum.all?(created, fn {_, _, map} -> + Enum.any?(response, fn item -> + addresses_json_match?(map, item) + end) + end) + + {_, _, %{"id" => id}} = Enum.at(created, 0) + + assert conn + |> delete("/api/account/v2/user/tags/address/#{id}") + |> doc("Delete private address tag") + |> json_response(200) == %{"message" => "OK"} + + assert Enum.all?(Enum.drop(created, 1), fn {_, _, %{"id" => id}} -> + conn + |> delete("/api/account/v2/user/tags/address/#{id}") + |> json_response(200) == %{"message" => "OK"} + end) + + assert conn + |> get("/api/account/v2/user/tags/address") + |> json_response(200) + |> Map.get("items") == [] + + assert Enum.all?(created, fn {addr, _, _} -> + response = + conn + |> get("/api/account/v2/tags/address/#{addr}") + |> json_response(200) + + response["personal_tags"] == [] + end) + end + + test "post private transaction tag", %{conn: conn} do + transaction_hash_non_existing = to_string(build(:transaction).hash) + transaction_hash = to_string(insert(:transaction).hash) + + assert conn + |> post("/api/account/v2/user/tags/transaction", %{ + "transaction_hash" => transaction_hash_non_existing, + "name" => "MyName" + }) + |> doc(description: "Error on try to create private transaction tag for transaction does not exist") + |> json_response(422) == %{"errors" => %{"transaction_hash" => ["Transaction does not exist"]}} + + tag_transaction_response = + conn + |> post("/api/account/v2/user/tags/transaction", %{ + "transaction_hash" => transaction_hash, + "name" => "MyName" + }) + |> doc(description: "Create private transaction tag") + |> json_response(200) + + conn + |> get("/api/account/v2/tags/transaction/#{transaction_hash}") + |> doc(description: "Get tags for transaction") + |> json_response(200) + + assert tag_transaction_response["transaction_hash"] == transaction_hash + assert tag_transaction_response["name"] == "MyName" + assert tag_transaction_response["id"] + end + + test "can't insert private transaction tags more than limit", %{conn: conn, user: user} do + old_env = Application.get_env(:explorer, Explorer.Account) + + new_env = + old_env + |> Keyword.replace(:private_tags_limit, 5) + |> Keyword.replace(:watchlist_addresses_limit, 5) + + Application.put_env(:explorer, Explorer.Account, new_env) + + for _ <- 0..3 do + build(:tag_transaction_db, user: user) |> Repo.account_repo().insert() + end + + assert conn + |> post("/api/account/v2/user/tags/transaction", build(:tag_transaction)) + |> json_response(200) + + assert conn + |> post("/api/account/v2/user/tags/transaction", build(:tag_transaction)) + |> json_response(422) + + Application.put_env(:explorer, Explorer.Account, old_env) + end + + test "check transaction tags pagination", %{conn: conn, user: user} do + tags_address = + for _ <- 0..50 do + build(:tag_transaction_db, user: user) |> Repo.account_repo().insert!() + end + + assert response = + conn + |> get("/api/account/v2/user/tags/transaction") + |> json_response(200) + + response_1 = + conn + |> get("/api/account/v2/user/tags/transaction", response["next_page_params"]) + |> json_response(200) + + check_paginated_response(response, response_1, tags_address) + end + + test "edit private transaction tag", %{conn: conn} do + transaction_tag = build(:tag_transaction) + + tag_response = + conn + |> post("/api/account/v2/user/tags/transaction", transaction_tag) + |> json_response(200) + + _response = + conn + |> get("/api/account/v2/user/tags/transaction") + |> json_response(200) == [tag_response] + + assert tag_response["address_hash"] == transaction_tag["address_hash"] + assert tag_response["name"] == transaction_tag["name"] + assert tag_response["id"] + + new_transaction_tag = build(:tag_transaction) + + new_tag_response = + conn + |> put("/api/account/v2/user/tags/transaction/#{tag_response["id"]}", new_transaction_tag) + |> doc(description: "Edit private transaction tag") + |> json_response(200) + + assert new_tag_response["address_hash"] == new_transaction_tag["address_hash"] + assert new_tag_response["name"] == new_transaction_tag["name"] + assert new_tag_response["id"] == tag_response["id"] + end + + test "get transaction tags after inserting one private tags", %{conn: conn} do + transactions = Enum.map(0..2, fn _x -> to_string(insert(:transaction).hash) end) + names = Enum.map(0..2, fn x -> "name#{x}" end) + zipped = Enum.zip(transactions, names) + + created = + Enum.map(zipped, fn {transaction_hash, name} -> + id = + (conn + |> post("/api/account/v2/user/tags/transaction", %{ + "transaction_hash" => transaction_hash, + "name" => name + }) + |> json_response(200))["id"] + + {transaction_hash, %{"label" => name}, %{"transaction_hash" => transaction_hash, "id" => id, "name" => name}} + end) + + assert Enum.all?(created, fn {transaction_hash, map_tag, _} -> + response = + conn + |> get("/api/account/v2/tags/transaction/#{transaction_hash}") + |> json_response(200) + + response["personal_transaction_tag"] == map_tag + end) + + response = + conn + |> get("/api/account/v2/user/tags/transaction") + |> doc(description: "Get private transactions tags") + |> json_response(200) + |> Map.get("items") + + assert Enum.all?(created, fn {_, _, map} -> map in response end) + end + + test "delete transaction tag", %{conn: conn} do + transactions = Enum.map(0..2, fn _x -> to_string(insert(:transaction).hash) end) + names = Enum.map(0..2, fn x -> "name#{x}" end) + zipped = Enum.zip(transactions, names) + + created = + Enum.map(zipped, fn {transaction_hash, name} -> + id = + (conn + |> post("/api/account/v2/user/tags/transaction", %{ + "transaction_hash" => transaction_hash, + "name" => name + }) + |> json_response(200))["id"] + + {transaction_hash, %{"label" => name}, %{"transaction_hash" => transaction_hash, "id" => id, "name" => name}} + end) + + assert Enum.all?(created, fn {transaction_hash, map_tag, _} -> + response = + conn + |> get("/api/account/v2/tags/transaction/#{transaction_hash}") + |> json_response(200) + + response["personal_transaction_tag"] == map_tag + end) + + response = + conn + |> get("/api/account/v2/user/tags/transaction") + |> json_response(200) + |> Map.get("items") + + assert Enum.all?(created, fn {_, _, map} -> map in response end) + + {_, _, %{"id" => id}} = Enum.at(created, 0) + + assert conn + |> delete("/api/account/v2/user/tags/transaction/#{id}") + |> doc("Delete private transaction tag") + |> json_response(200) == %{"message" => "OK"} + + assert Enum.all?(Enum.drop(created, 1), fn {_, _, %{"id" => id}} -> + conn + |> delete("/api/account/v2/user/tags/transaction/#{id}") + |> json_response(200) == %{"message" => "OK"} + end) + + assert conn + |> get("/api/account/v2/user/tags/transaction") + |> json_response(200) + |> Map.get("items") == [] + + assert Enum.all?(created, fn {addr, _, _} -> + response = + conn + |> get("/api/account/v2/tags/transaction/#{addr}") + |> json_response(200) + |> Map.get("items") + + response["personal_transaction_tag"] == nil + end) + end + + test "post && get watchlist address", %{conn: conn} do + watchlist_address_map = build(:watchlist_address) + + post_watchlist_address_response = + conn + |> post( + "/api/account/v2/user/watchlist", + watchlist_address_map + ) + |> doc(description: "Add address to watch list") + |> json_response(200) + + assert post_watchlist_address_response["notification_settings"] == watchlist_address_map["notification_settings"] + assert post_watchlist_address_response["name"] == watchlist_address_map["name"] + assert post_watchlist_address_response["notification_methods"] == watchlist_address_map["notification_methods"] + assert post_watchlist_address_response["address_hash"] == watchlist_address_map["address_hash"] + + get_watchlist_address_response = + conn |> get("/api/account/v2/user/watchlist") |> json_response(200) |> Map.get("items") |> Enum.at(0) + + assert get_watchlist_address_response["notification_settings"] == watchlist_address_map["notification_settings"] + assert get_watchlist_address_response["name"] == watchlist_address_map["name"] + assert get_watchlist_address_response["notification_methods"] == watchlist_address_map["notification_methods"] + assert get_watchlist_address_response["address_hash"] == watchlist_address_map["address_hash"] + assert get_watchlist_address_response["id"] == post_watchlist_address_response["id"] + + watchlist_address_map_1 = build(:watchlist_address) + + post_watchlist_address_response_1 = + conn + |> post( + "/api/account/v2/user/watchlist", + watchlist_address_map_1 + ) + |> json_response(200) + + get_watchlist_address_response_1_0 = + conn + |> get("/api/account/v2/user/watchlist") + |> doc(description: "Get addresses from watchlists") + |> json_response(200) + |> Map.get("items") + |> Enum.at(1) + + get_watchlist_address_response_1_1 = + conn |> get("/api/account/v2/user/watchlist") |> json_response(200) |> Map.get("items") |> Enum.at(0) + + assert get_watchlist_address_response_1_0 == get_watchlist_address_response + + assert get_watchlist_address_response_1_1["notification_settings"] == + watchlist_address_map_1["notification_settings"] + + assert get_watchlist_address_response_1_1["name"] == watchlist_address_map_1["name"] + + assert get_watchlist_address_response_1_1["notification_methods"] == + watchlist_address_map_1["notification_methods"] + + assert get_watchlist_address_response_1_1["address_hash"] == watchlist_address_map_1["address_hash"] + assert get_watchlist_address_response_1_1["id"] == post_watchlist_address_response_1["id"] + end + + test "can't insert watchlist addresses more than limit", %{conn: conn, user: user} do + old_env = Application.get_env(:explorer, Explorer.Account) + + new_env = + old_env + |> Keyword.replace(:private_tags_limit, 5) + |> Keyword.replace(:watchlist_addresses_limit, 5) + + Application.put_env(:explorer, Explorer.Account, new_env) + + for _ <- 0..3 do + build(:watchlist_address_db, wl_id: user.watchlist_id) |> Repo.account_repo().insert() + end + + assert conn + |> post("/api/account/v2/user/watchlist", build(:watchlist_address)) + |> json_response(200) + + assert conn + |> post("/api/account/v2/user/watchlist", build(:watchlist_address)) + |> json_response(422) + + Application.put_env(:explorer, Explorer.Account, old_env) + end + + test "check watchlist tags pagination", %{conn: conn, user: user} do + tags_address = + for _ <- 0..50 do + build(:watchlist_address_db, wl_id: user.watchlist_id) |> Repo.account_repo().insert!() + end + + assert response = + conn + |> get("/api/account/v2/user/watchlist") + |> json_response(200) + + response_1 = + conn + |> get("/api/account/v2/user/watchlist", response["next_page_params"]) + |> json_response(200) + + check_paginated_response(response, response_1, tags_address) + end + + test "delete watchlist address", %{conn: conn} do + watchlist_address_map = build(:watchlist_address) + + post_watchlist_address_response = + conn + |> post( + "/api/account/v2/user/watchlist", + watchlist_address_map + ) + |> json_response(200) + + assert post_watchlist_address_response["notification_settings"] == watchlist_address_map["notification_settings"] + assert post_watchlist_address_response["name"] == watchlist_address_map["name"] + assert post_watchlist_address_response["notification_methods"] == watchlist_address_map["notification_methods"] + assert post_watchlist_address_response["address_hash"] == watchlist_address_map["address_hash"] + + get_watchlist_address_response = + conn |> get("/api/account/v2/user/watchlist") |> json_response(200) |> Map.get("items") |> Enum.at(0) + + assert get_watchlist_address_response["notification_settings"] == watchlist_address_map["notification_settings"] + assert get_watchlist_address_response["name"] == watchlist_address_map["name"] + assert get_watchlist_address_response["notification_methods"] == watchlist_address_map["notification_methods"] + assert get_watchlist_address_response["address_hash"] == watchlist_address_map["address_hash"] + assert get_watchlist_address_response["id"] == post_watchlist_address_response["id"] + + watchlist_address_map_1 = build(:watchlist_address) + + post_watchlist_address_response_1 = + conn + |> post( + "/api/account/v2/user/watchlist", + watchlist_address_map_1 + ) + |> json_response(200) + + get_watchlist_address_response_1 = conn |> get("/api/account/v2/user/watchlist") |> json_response(200) + + get_watchlist_address_response_1_0 = get_watchlist_address_response_1 |> Map.get("items") |> Enum.at(1) + + get_watchlist_address_response_1_1 = get_watchlist_address_response_1 |> Map.get("items") |> Enum.at(0) + + assert get_watchlist_address_response_1_0 == get_watchlist_address_response + + assert get_watchlist_address_response_1_1["notification_settings"] == + watchlist_address_map_1["notification_settings"] + + assert get_watchlist_address_response_1_1["name"] == watchlist_address_map_1["name"] + + assert get_watchlist_address_response_1_1["notification_methods"] == + watchlist_address_map_1["notification_methods"] + + assert get_watchlist_address_response_1_1["address_hash"] == watchlist_address_map_1["address_hash"] + assert get_watchlist_address_response_1_1["id"] == post_watchlist_address_response_1["id"] + + assert conn + |> delete("/api/account/v2/user/watchlist/#{get_watchlist_address_response_1_1["id"]}") + |> doc(description: "Delete address from watchlist by id") + |> json_response(200) == %{"message" => "OK"} + + assert conn + |> delete("/api/account/v2/user/watchlist/#{get_watchlist_address_response_1_0["id"]}") + |> json_response(200) == %{"message" => "OK"} + + assert conn |> get("/api/account/v2/user/watchlist") |> json_response(200) |> Map.get("items") == [] + end + + test "put watchlist address", %{conn: conn} do + watchlist_address_map = build(:watchlist_address) + + post_watchlist_address_response = + conn + |> post( + "/api/account/v2/user/watchlist", + watchlist_address_map + ) + |> json_response(200) + + assert post_watchlist_address_response["notification_settings"] == watchlist_address_map["notification_settings"] + assert post_watchlist_address_response["name"] == watchlist_address_map["name"] + assert post_watchlist_address_response["notification_methods"] == watchlist_address_map["notification_methods"] + assert post_watchlist_address_response["address_hash"] == watchlist_address_map["address_hash"] + + get_watchlist_address_response = + conn |> get("/api/account/v2/user/watchlist") |> json_response(200) |> Map.get("items") |> Enum.at(0) + + assert get_watchlist_address_response["notification_settings"] == watchlist_address_map["notification_settings"] + assert get_watchlist_address_response["name"] == watchlist_address_map["name"] + assert get_watchlist_address_response["notification_methods"] == watchlist_address_map["notification_methods"] + assert get_watchlist_address_response["address_hash"] == watchlist_address_map["address_hash"] + assert get_watchlist_address_response["id"] == post_watchlist_address_response["id"] + + new_watchlist_address_map = build(:watchlist_address) + + put_watchlist_address_response = + conn + |> put( + "/api/account/v2/user/watchlist/#{post_watchlist_address_response["id"]}", + new_watchlist_address_map + ) + |> doc(description: "Edit watchlist address") + |> json_response(200) + + assert put_watchlist_address_response["notification_settings"] == + new_watchlist_address_map["notification_settings"] + + assert put_watchlist_address_response["name"] == new_watchlist_address_map["name"] + assert put_watchlist_address_response["notification_methods"] == new_watchlist_address_map["notification_methods"] + assert put_watchlist_address_response["address_hash"] == new_watchlist_address_map["address_hash"] + assert get_watchlist_address_response["id"] == put_watchlist_address_response["id"] + end + + test "cannot create duplicate of watchlist address", %{conn: conn} do + watchlist_address_map = build(:watchlist_address) + + post_watchlist_address_response = + conn + |> post( + "/api/account/v2/user/watchlist", + watchlist_address_map + ) + |> json_response(200) + + assert post_watchlist_address_response["notification_settings"] == watchlist_address_map["notification_settings"] + assert post_watchlist_address_response["name"] == watchlist_address_map["name"] + assert post_watchlist_address_response["notification_methods"] == watchlist_address_map["notification_methods"] + assert post_watchlist_address_response["address_hash"] == watchlist_address_map["address_hash"] + + assert conn + |> post( + "/api/account/v2/user/watchlist", + watchlist_address_map + ) + |> doc(description: "Example of error on creating watchlist address") + |> json_response(422) == %{"errors" => %{"watchlist_id" => ["Address already added to the watch list"]}} + + new_watchlist_address_map = build(:watchlist_address) + + post_watchlist_address_response_1 = + conn + |> post( + "/api/account/v2/user/watchlist", + new_watchlist_address_map + ) + |> json_response(200) + + assert conn + |> put( + "/api/account/v2/user/watchlist/#{post_watchlist_address_response_1["id"]}", + watchlist_address_map + ) + |> doc(description: "Example of error on editing watchlist address") + |> json_response(422) == %{"errors" => %{"watchlist_id" => ["Address already added to the watch list"]}} + end + + test "watchlist address returns with token balances info", %{conn: conn} do + watchlist_address_map = build(:watchlist_address) + + conn + |> post( + "/api/account/v2/user/watchlist", + watchlist_address_map + ) + |> json_response(200) + |> Map.get("items") + + watchlist_address_map_1 = build(:watchlist_address) + + conn + |> post( + "/api/account/v2/user/watchlist", + watchlist_address_map_1 + ) + |> json_response(200) + |> Map.get("items") + + values = + for _i <- 0..149 do + ctb = + insert(:address_current_token_balance_with_token_id, + address: Repo.get_by(Address, hash: watchlist_address_map["address_hash"]) + ) + |> Repo.preload([:token]) + + ctb.value + |> Decimal.mult(ctb.token.fiat_value) + |> Decimal.div(Decimal.new(10 ** Decimal.to_integer(ctb.token.decimals))) + |> Decimal.round(16) + end + + values_1 = + for _i <- 0..200 do + ctb = + insert(:address_current_token_balance_with_token_id, + address: Repo.get_by(Address, hash: watchlist_address_map_1["address_hash"]) + ) + |> Repo.preload([:token]) + + ctb.value + |> Decimal.mult(ctb.token.fiat_value) + |> Decimal.div(Decimal.new(10 ** Decimal.to_integer(ctb.token.decimals))) + |> Decimal.round(16) + end + |> Enum.sort(fn x1, x2 -> Decimal.compare(x1, x2) in [:gt, :eq] end) + |> Enum.take(150) + + [wa2, wa1] = conn |> get("/api/account/v2/user/watchlist") |> json_response(200) |> Map.get("items") + + assert wa1["tokens_fiat_value"] |> Decimal.new() == + values |> Enum.reduce(Decimal.new(0), fn x, acc -> Decimal.add(x, acc) end) + + assert wa1["tokens_count"] == 150 + assert wa1["tokens_overflow"] == false + + assert wa2["tokens_fiat_value"] |> Decimal.new() == + values_1 |> Enum.reduce(Decimal.new(0), fn x, acc -> Decimal.add(x, acc) end) + + assert wa2["tokens_count"] == 150 + assert wa2["tokens_overflow"] == true + end + + test "watchlist address returns with token balances info + handle nil fiat values", %{conn: conn} do + watchlist_address_map = build(:watchlist_address) + + conn + |> post( + "/api/account/v2/user/watchlist", + watchlist_address_map + ) + |> json_response(200) + |> Map.get("items") + + values = + for _i <- 0..148 do + ctb = + insert(:address_current_token_balance_with_token_id, + address: Repo.get_by(Address, hash: watchlist_address_map["address_hash"]) + ) + |> Repo.preload([:token]) + + ctb.value + |> Decimal.mult(ctb.token.fiat_value) + |> Decimal.div(Decimal.new(10 ** Decimal.to_integer(ctb.token.decimals))) + |> Decimal.round(16) + end + + token = insert(:token, fiat_value: nil) + + insert(:address_current_token_balance_with_token_id, + address: Repo.get_by(Address, hash: watchlist_address_map["address_hash"]), + token: token, + token_contract_address_hash: token.contract_address_hash + ) + + [wa1] = conn |> get("/api/account/v2/user/watchlist") |> json_response(200) |> Map.get("items") + + assert wa1["tokens_fiat_value"] |> Decimal.new() == + values |> Enum.reduce(Decimal.new(0), fn x, acc -> Decimal.add(x, acc) end) + + assert wa1["tokens_count"] == 150 + assert wa1["tokens_overflow"] == false + end + + test "post api key", %{conn: conn} do + post_api_key_response = + conn + |> post( + "/api/account/v2/user/api_keys", + %{"name" => "test"} + ) + |> doc(description: "Add api key") + |> json_response(200) + + assert post_api_key_response["name"] == "test" + assert post_api_key_response["api_key"] + end + + test "can create not more than 3 api keys + get api keys", %{conn: conn} do + Enum.each(0..2, fn _x -> + conn + |> post( + "/api/account/v2/user/api_keys", + %{"name" => "test"} + ) + |> json_response(200) + end) + + assert conn + |> post( + "/api/account/v2/user/api_keys", + %{"name" => "test"} + ) + |> doc(description: "Example of error on creating api key") + |> json_response(422) == %{"errors" => %{"name" => ["Max 3 keys per account"]}} + + assert conn + |> get("/api/account/v2/user/api_keys") + |> doc(description: "Get api keys list") + |> json_response(200) + |> Enum.count() == 3 + end + + test "edit api key", %{conn: conn} do + post_api_key_response = + conn + |> post( + "/api/account/v2/user/api_keys", + %{"name" => "test"} + ) + |> json_response(200) + + assert post_api_key_response["name"] == "test" + assert post_api_key_response["api_key"] + + put_api_key_response = + conn + |> put( + "/api/account/v2/user/api_keys/#{post_api_key_response["api_key"]}", + %{"name" => "test_1"} + ) + |> doc(description: "Edit api key") + |> json_response(200) + + assert put_api_key_response["api_key"] == post_api_key_response["api_key"] + assert put_api_key_response["name"] == "test_1" + + assert conn + |> get("/api/account/v2/user/api_keys") + |> json_response(200) == [put_api_key_response] + end + + test "delete api key", %{conn: conn} do + post_api_key_response = + conn + |> post( + "/api/account/v2/user/api_keys", + %{"name" => "test"} + ) + |> json_response(200) + + assert post_api_key_response["name"] == "test" + assert post_api_key_response["api_key"] + + assert conn + |> get("/api/account/v2/user/api_keys") + |> json_response(200) + |> Enum.count() == 1 + + assert conn + |> delete("/api/account/v2/user/api_keys/#{post_api_key_response["api_key"]}") + |> doc(description: "Delete api key") + |> json_response(200) == %{"message" => "OK"} + + assert conn + |> get("/api/account/v2/user/api_keys") + |> json_response(200) == [] + end + + test "post custom abi", %{conn: conn} do + custom_abi = build(:custom_abi) + + post_custom_abi_response = + conn + |> post( + "/api/account/v2/user/custom_abis", + custom_abi + ) + |> doc(description: "Add custom abi") + |> json_response(200) + + assert post_custom_abi_response["name"] == custom_abi["name"] + assert post_custom_abi_response["abi"] == custom_abi["abi"] + assert post_custom_abi_response["contract_address_hash"] == custom_abi["contract_address_hash"] + assert post_custom_abi_response["id"] + end + + test "can create not more than 15 custom abis + get custom abi", %{conn: conn} do + Enum.each(0..14, fn _x -> + conn + |> post( + "/api/account/v2/user/custom_abis", + build(:custom_abi) + ) + |> json_response(200) + end) + + assert conn + |> post( + "/api/account/v2/user/custom_abis", + build(:custom_abi) + ) + |> doc(description: "Example of error on creating custom abi") + |> json_response(422) == %{"errors" => %{"name" => ["Max 15 ABIs per account"]}} + + assert conn + |> get("/api/account/v2/user/custom_abis") + |> doc(description: "Get custom abis list") + |> json_response(200) + |> Enum.count() == 15 + end + + test "edit custom abi", %{conn: conn} do + custom_abi = build(:custom_abi) + + post_custom_abi_response = + conn + |> post( + "/api/account/v2/user/custom_abis", + custom_abi + ) + |> json_response(200) + + assert post_custom_abi_response["name"] == custom_abi["name"] + assert post_custom_abi_response["abi"] == custom_abi["abi"] + assert post_custom_abi_response["contract_address_hash"] == custom_abi["contract_address_hash"] + assert post_custom_abi_response["id"] + + custom_abi_1 = build(:custom_abi) + + put_custom_abi_response = + conn + |> put( + "/api/account/v2/user/custom_abis/#{post_custom_abi_response["id"]}", + custom_abi_1 + ) + |> doc(description: "Edit custom abi") + |> json_response(200) + + assert put_custom_abi_response["name"] == custom_abi_1["name"] + assert put_custom_abi_response["id"] == post_custom_abi_response["id"] + assert put_custom_abi_response["contract_address_hash"] == custom_abi_1["contract_address_hash"] + assert put_custom_abi_response["abi"] == custom_abi_1["abi"] + + assert conn + |> get("/api/account/v2/user/custom_abis") + |> json_response(200) == [put_custom_abi_response] + end + + test "delete custom abi", %{conn: conn} do + custom_abi = build(:custom_abi) + + post_custom_abi_response = + conn + |> post( + "/api/account/v2/user/custom_abis", + custom_abi + ) + |> json_response(200) + + assert post_custom_abi_response["name"] == custom_abi["name"] + assert post_custom_abi_response["id"] + + assert conn + |> get("/api/account/v2/user/custom_abis") + |> json_response(200) + |> Enum.count() == 1 + + assert conn + |> delete("/api/account/v2/user/custom_abis/#{post_custom_abi_response["id"]}") + |> doc(description: "Delete custom abi") + |> json_response(200) == %{"message" => "OK"} + + assert conn + |> get("/api/account/v2/user/custom_abis") + |> json_response(200) == [] + end + end + + describe "public tags" do + test "create public tags request", %{conn: conn} do + public_tags_request = build(:public_tags_request) + + post_public_tags_request_response = + conn + |> post( + "/api/account/v2/user/public_tags", + public_tags_request + ) + |> doc(description: "Submit request to add a public tag") + |> json_response(200) + + assert post_public_tags_request_response["full_name"] == public_tags_request["full_name"] + assert post_public_tags_request_response["email"] == public_tags_request["email"] + assert post_public_tags_request_response["tags"] == public_tags_request["tags"] + assert post_public_tags_request_response["website"] == public_tags_request["website"] + assert post_public_tags_request_response["additional_comment"] == public_tags_request["additional_comment"] + assert post_public_tags_request_response["addresses"] == public_tags_request["addresses"] + assert post_public_tags_request_response["company"] == public_tags_request["company"] + assert post_public_tags_request_response["is_owner"] == public_tags_request["is_owner"] + assert post_public_tags_request_response["id"] + end + + test "get one public tags requests", %{conn: conn} do + public_tags_request = build(:public_tags_request) + + post_public_tags_request_response = + conn + |> post( + "/api/account/v2/user/public_tags", + public_tags_request + ) + |> json_response(200) + + assert post_public_tags_request_response["full_name"] == public_tags_request["full_name"] + assert post_public_tags_request_response["email"] == public_tags_request["email"] + assert post_public_tags_request_response["tags"] == public_tags_request["tags"] + assert post_public_tags_request_response["website"] == public_tags_request["website"] + assert post_public_tags_request_response["additional_comment"] == public_tags_request["additional_comment"] + assert post_public_tags_request_response["addresses"] == public_tags_request["addresses"] + assert post_public_tags_request_response["company"] == public_tags_request["company"] + assert post_public_tags_request_response["is_owner"] == public_tags_request["is_owner"] + assert post_public_tags_request_response["id"] + + assert conn + |> get("/api/account/v2/user/public_tags") + |> json_response(200) + |> Enum.map(&convert_date/1) == + [post_public_tags_request_response] + |> Enum.map(&convert_date/1) + end + + test "get and delete several public tags requests", %{conn: conn} do + public_tags_list = build_list(10, :public_tags_request) + + final_list = + public_tags_list + |> Enum.map(fn request -> + response = + conn + |> post( + "/api/account/v2/user/public_tags", + request + ) + |> json_response(200) + + assert response["full_name"] == request["full_name"] + assert response["email"] == request["email"] + assert response["tags"] == request["tags"] + assert response["website"] == request["website"] + assert response["additional_comment"] == request["additional_comment"] + assert response["addresses"] == request["addresses"] + assert response["company"] == request["company"] + assert response["is_owner"] == request["is_owner"] + assert response["id"] + + convert_date(response) + end) + |> Enum.reverse() + + assert conn + |> get("/api/account/v2/user/public_tags") + |> doc(description: "Get list of requests to add a public tag") + |> json_response(200) + |> Enum.map(&convert_date/1) == final_list + + %{"id" => id} = Enum.at(final_list, 0) + + assert conn + |> delete("/api/account/v2/user/public_tags/#{id}", %{"remove_reason" => "reason"}) + |> doc(description: "Delete public tags request") + |> json_response(200) == %{"message" => "OK"} + + Enum.each(Enum.drop(final_list, 1), fn request -> + assert conn + |> delete("/api/account/v2/user/public_tags/#{request["id"]}", %{"remove_reason" => "reason"}) + |> json_response(200) == %{"message" => "OK"} + end) + + assert conn + |> get("/api/account/v2/user/public_tags") + |> json_response(200) == [] + end + + test "edit public tags request", %{conn: conn} do + public_tags_request = build(:public_tags_request) + + post_public_tags_request_response = + conn + |> post( + "/api/account/v2/user/public_tags", + public_tags_request + ) + |> json_response(200) + + assert post_public_tags_request_response["full_name"] == public_tags_request["full_name"] + assert post_public_tags_request_response["email"] == public_tags_request["email"] + assert post_public_tags_request_response["tags"] == public_tags_request["tags"] + assert post_public_tags_request_response["website"] == public_tags_request["website"] + assert post_public_tags_request_response["additional_comment"] == public_tags_request["additional_comment"] + assert post_public_tags_request_response["addresses"] == public_tags_request["addresses"] + assert post_public_tags_request_response["company"] == public_tags_request["company"] + assert post_public_tags_request_response["is_owner"] == public_tags_request["is_owner"] + assert post_public_tags_request_response["id"] + + assert conn + |> get("/api/account/v2/user/public_tags") + |> json_response(200) + |> Enum.map(&convert_date/1) == + [post_public_tags_request_response] + |> Enum.map(&convert_date/1) + + new_public_tags_request = build(:public_tags_request) + + put_public_tags_request_response = + conn + |> put( + "/api/account/v2/user/public_tags/#{post_public_tags_request_response["id"]}", + new_public_tags_request + ) + |> doc(description: "Edit request to add a public tag") + |> json_response(200) + + assert put_public_tags_request_response["full_name"] == new_public_tags_request["full_name"] + assert put_public_tags_request_response["email"] == new_public_tags_request["email"] + assert put_public_tags_request_response["tags"] == new_public_tags_request["tags"] + assert put_public_tags_request_response["website"] == new_public_tags_request["website"] + assert put_public_tags_request_response["additional_comment"] == new_public_tags_request["additional_comment"] + assert put_public_tags_request_response["addresses"] == new_public_tags_request["addresses"] + assert put_public_tags_request_response["company"] == new_public_tags_request["company"] + assert put_public_tags_request_response["is_owner"] == new_public_tags_request["is_owner"] + assert put_public_tags_request_response["id"] == post_public_tags_request_response["id"] + + assert conn + |> get("/api/account/v2/user/public_tags") + |> json_response(200) + |> Enum.map(&convert_date/1) == + [put_public_tags_request_response] + |> Enum.map(&convert_date/1) + end + end + + def convert_date(request) do + {:ok, time, _} = DateTime.from_iso8601(request["submission_date"]) + %{request | "submission_date" => Calendar.strftime(time, "%b %d, %Y")} + end + + defp compare_item(%TagAddress{} = tag_address, json) do + assert json["address_hash"] == to_string(tag_address.address_hash) + assert json["name"] == tag_address.name + assert json["id"] == tag_address.id + assert json["address"]["hash"] == Address.checksum(tag_address.address_hash) + end + + defp compare_item(%TagTransaction{} = tag_transaction, json) do + assert json["transaction_hash"] == to_string(tag_transaction.transaction_hash) + assert json["name"] == tag_transaction.name + assert json["id"] == tag_transaction.id + end + + defp compare_item(%WatchlistAddress{} = watchlist, json) do + notification_settings = %{ + "native" => %{ + "incoming" => watchlist.watch_coin_input, + "outcoming" => watchlist.watch_coin_output + }, + "ERC-20" => %{ + "incoming" => watchlist.watch_erc_20_input, + "outcoming" => watchlist.watch_erc_20_output + }, + "ERC-721" => %{ + "incoming" => watchlist.watch_erc_721_input, + "outcoming" => watchlist.watch_erc_721_output + }, + "ERC-404" => %{ + "incoming" => watchlist.watch_erc_404_input, + "outcoming" => watchlist.watch_erc_404_output + } + } + + assert json["address_hash"] == to_string(watchlist.address_hash) + assert json["name"] == watchlist.name + assert json["id"] == watchlist.id + assert json["address"]["hash"] == Address.checksum(watchlist.address_hash) + assert json["notification_methods"]["email"] == watchlist.notify_email + assert json["notification_settings"] == notification_settings + end + + defp check_paginated_response(first_page_resp, second_page_resp, list) do + assert Enum.count(first_page_resp["items"]) == 50 + assert first_page_resp["next_page_params"] != nil + compare_item(Enum.at(list, 50), Enum.at(first_page_resp["items"], 0)) + compare_item(Enum.at(list, 1), Enum.at(first_page_resp["items"], 49)) + + assert Enum.count(second_page_resp["items"]) == 1 + assert second_page_resp["next_page_params"] == nil + compare_item(Enum.at(list, 0), Enum.at(second_page_resp["items"], 0)) + end + + defp addresses_json_match?(expected, actual) do + Enum.all?(expected, fn {key, value} -> + case value do + # Recursively compare nested maps + %{} -> addresses_json_match?(value, actual[key]) + _ -> actual[key] == value + end + end) + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/account/custom_abi_controller_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/account/custom_abi_controller_test.exs new file mode 100644 index 0000000..6f0608f --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/account/custom_abi_controller_test.exs @@ -0,0 +1,189 @@ +defmodule BlockScoutWeb.Account.CustomABIControllerTest do + use BlockScoutWeb.ConnCase + + alias Explorer.Account.Identity + alias Explorer.TestHelper + + @custom_abi "[{\"type\":\"function\",\"outputs\":[{\"type\":\"string\",\"name\":\"\"}],\"name\":\"name\",\"inputs\":[],\"constant\":true}]" + + setup %{conn: conn} do + auth = build(:auth) + + {:ok, user} = Identity.find_or_create(auth) + + {:ok, conn: Plug.Test.init_test_session(conn, current_user: user)} + end + + describe "test custom ABI functionality" do + test "custom ABI page opens correctly", %{conn: conn} do + result_conn = + conn + |> get("/account/custom_abi") + + assert html_response(result_conn, 200) =~ "Create a Custom ABI to interact with contracts." + end + + test "do not add custom ABI with wrong ABI", %{conn: conn} do + contract_address = insert(:address, contract_code: "0x0102") + + custom_abi = %{ + "name" => "1", + "address_hash" => to_string(contract_address), + "abi" => "" + } + + result_conn = + conn + |> post("/account/custom_abi", %{"custom_abi" => custom_abi}) + + assert html_response(result_conn, 200) =~ "Add Custom ABI" + assert html_response(result_conn, 200) =~ to_string(contract_address.hash) + assert html_response(result_conn, 200) =~ "Required" + + result_conn_1 = + conn + |> post("/account/custom_abi", %{"custom_abi" => Map.put(custom_abi, "abi", "123")}) + + assert html_response(result_conn_1, 200) =~ "Add Custom ABI" + assert html_response(result_conn_1, 200) =~ to_string(contract_address.hash) + assert html_response(result_conn_1, 200) =~ "Invalid format" + + result_conn_2 = + conn + |> get("/account/custom_abi") + + assert html_response(result_conn_2, 200) =~ "Create a Custom ABI to interact with contracts." + refute html_response(result_conn_2, 200) =~ to_string(contract_address.hash) + end + + test "add one custom abi and do not allow to create duplicates", %{conn: conn} do + contract_address = insert(:contract_address, contract_code: "0x0102") + + custom_abi = %{ + "name" => "1", + "address_hash" => to_string(contract_address), + "abi" => @custom_abi + } + + result_conn = + conn + |> post("/account/custom_abi", %{"custom_abi" => custom_abi}) + + assert redirected_to(result_conn) == "/account/custom_abi" + + result_conn_2 = get(result_conn, "/account/custom_abi") + assert html_response(result_conn_2, 200) =~ to_string(contract_address.hash) + assert html_response(result_conn_2, 200) =~ "Create a Custom ABI to interact with contracts." + + result_conn_1 = + conn + |> post("/account/custom_abi", %{"custom_abi" => custom_abi}) + + assert html_response(result_conn_1, 200) =~ "Add Custom ABI" + assert html_response(result_conn_1, 200) =~ to_string(contract_address.hash) + assert html_response(result_conn_1, 200) =~ "Custom ABI for this address has already been added before" + end + + test "show error on address which is not smart contract", %{conn: conn} do + contract_address = insert(:address) + + custom_abi = %{ + "name" => "1", + "address_hash" => to_string(contract_address), + "abi" => @custom_abi + } + + result_conn = + conn + |> post("/account/custom_abi", %{"custom_abi" => custom_abi}) + + assert html_response(result_conn, 200) =~ "Add Custom ABI" + assert html_response(result_conn, 200) =~ to_string(contract_address.hash) + assert html_response(result_conn, 200) =~ "Address is not a smart contract" + end + + test "user can add up to 15 custom ABIs", %{conn: conn} do + addresses = + Enum.map(1..15, fn _x -> + address = insert(:contract_address, contract_code: "0x0102") + + custom_abi = %{ + "name" => "1", + "address_hash" => to_string(address), + "abi" => @custom_abi + } + + assert conn + |> post("/account/custom_abi", %{"custom_abi" => custom_abi}) + |> redirected_to() == "/account/custom_abi" + + to_string(address.hash) + end) + + assert abi_list = + conn + |> get("/account/custom_abi") + |> html_response(200) + + Enum.each(addresses, fn address -> assert abi_list =~ address end) + + address = insert(:contract_address, contract_code: "0x0102") + + custom_abi = %{ + "name" => "1", + "address_hash" => to_string(address), + "abi" => @custom_abi + } + + assert error_form = + conn + |> post("/account/custom_abi", %{"custom_abi" => custom_abi}) + |> html_response(200) + + assert error_form =~ "Add Custom ABI" + assert error_form =~ "Max 15 ABIs per account" + assert error_form =~ to_string(address.hash) + + assert abi_list_new = + conn + |> get("/account/custom_abi") + |> html_response(200) + + Enum.each(addresses, fn address -> assert abi_list_new =~ address end) + + refute abi_list_new =~ to_string(address.hash) + assert abi_list_new =~ "You can create up to 15 Custom ABIs per account." + end + + test "after adding custom ABI on address page appear Read/Write Contract tab", %{conn: conn} do + contract_address = insert(:contract_address, contract_code: "0x0102") + + custom_abi = %{ + "name" => "1", + "address_hash" => to_string(contract_address), + "abi" => + "[{\"type\":\"function\",\"outputs\":[{\"type\":\"string\",\"name\":\"\"}],\"name\":\"name\",\"inputs\":[],\"constant\":true},{\"type\":\"function\",\"outputs\":[{\"type\":\"bool\",\"name\":\"success\"}],\"name\":\"approve\",\"inputs\":[{\"type\":\"address\",\"name\":\"_spender\"},{\"type\":\"uint256\",\"name\":\"_value\"}],\"constant\":false}]" + } + + TestHelper.get_all_proxies_implementation_zero_addresses() + + result_conn = + conn + |> post("/account/custom_abi", %{"custom_abi" => custom_abi}) + + assert redirected_to(result_conn) == "/account/custom_abi" + + result_conn_2 = get(result_conn, "/account/custom_abi") + assert html_response(result_conn_2, 200) =~ to_string(contract_address.hash) + assert html_response(result_conn_2, 200) =~ "Create a Custom ABI to interact with contracts." + + assert contract_page = + result_conn + |> get(address_contract_path(result_conn, :index, to_string(contract_address))) + |> html_response(200) + + assert contract_page =~ "Write Contract" + assert contract_page =~ "Read Contract" + end + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/address_coin_balance_by_day_controller_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/address_coin_balance_by_day_controller_test.exs new file mode 100644 index 0000000..8d94957 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/address_coin_balance_by_day_controller_test.exs @@ -0,0 +1,27 @@ +defmodule BlockScoutWeb.AddressCoinBalanceByDayControllerTest do + use BlockScoutWeb.ConnCase + + alias Explorer.Chain.Address + + describe "GET index/2" do + test "returns the coin balance history grouped by date", %{conn: conn} do + address = insert(:address) + noon = Timex.now() |> Timex.beginning_of_day() |> Timex.set(hour: 12) + block = insert(:block, timestamp: noon, number: 2) + block_one_day_ago = insert(:block, timestamp: Timex.shift(noon, days: -1), number: 1) + insert(:fetched_balance, address_hash: address.hash, value: 1000, block_number: block.number) + insert(:fetched_balance, address_hash: address.hash, value: 2000, block_number: block_one_day_ago.number) + insert(:fetched_balance_daily, address_hash: address.hash, value: 1000, day: noon) + insert(:fetched_balance_daily, address_hash: address.hash, value: 2000, day: Timex.shift(noon, days: -1)) + + conn = get(conn, address_coin_balance_by_day_path(conn, :index, Address.checksum(address)), %{"type" => "JSON"}) + + response = json_response(conn, 200) + + assert [ + %{"date" => _, "value" => 2.0e-15}, + %{"date" => _, "value" => 1.0e-15} + ] = response + end + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/address_contract_controller_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/address_contract_controller_test.exs new file mode 100644 index 0000000..62e23e2 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/address_contract_controller_test.exs @@ -0,0 +1,59 @@ +defmodule BlockScoutWeb.AddressContractControllerTest do + use BlockScoutWeb.ConnCase, async: true + + import BlockScoutWeb.Routers.WebRouter.Helpers, only: [address_contract_path: 3] + + alias Explorer.Chain.{Address, Hash} + alias Explorer.Market.Token + alias Explorer.{Factory, TestHelper} + + describe "GET index/3" do + test "returns not found for nonexistent address", %{conn: conn} do + nonexistent_address_hash = Hash.to_string(Factory.address_hash()) + + conn = + get(conn, address_contract_path(BlockScoutWeb.Endpoint, :index, Address.checksum(nonexistent_address_hash))) + + assert html_response(conn, 404) + end + + test "returns not found given an invalid address hash ", %{conn: conn} do + invalid_hash = "invalid_hash" + + conn = get(conn, address_contract_path(BlockScoutWeb.Endpoint, :index, invalid_hash)) + + assert html_response(conn, 404) + end + + test "returns not found when the address isn't a contract", %{conn: conn} do + address = insert(:address) + + conn = get(conn, address_contract_path(BlockScoutWeb.Endpoint, :index, Address.checksum(address))) + + assert html_response(conn, 404) + end + + test "successfully renders the page when the address is a contract", %{conn: conn} do + address = insert(:address, contract_code: Factory.data("contract_code"), smart_contract: nil) + + transaction = insert(:transaction, from_address: address) |> with_block() + + insert( + :internal_transaction_create, + index: 0, + transaction: transaction, + created_contract_address: address, + block_hash: transaction.block_hash, + block_index: 0 + ) + + TestHelper.get_all_proxies_implementation_zero_addresses() + + conn = get(conn, address_contract_path(BlockScoutWeb.Endpoint, :index, Address.checksum(address))) + + assert html_response(conn, 200) + assert address.hash == conn.assigns.address.hash + assert %Token{} = conn.assigns.exchange_rate + end + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/address_controller_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/address_controller_test.exs new file mode 100644 index 0000000..d896d7b --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/address_controller_test.exs @@ -0,0 +1,86 @@ +defmodule BlockScoutWeb.AddressControllerTest do + use BlockScoutWeb.ConnCase, + # ETS tables are shared in `Explorer.Counters.*` + async: false + + import Mox + + alias Explorer.Chain.Address + alias Explorer.Chain.Cache.Counters.AddressesCount + + describe "GET index/2" do + setup :set_mox_global + + setup do + # Use TestSource mock for this test set + configuration = Application.get_env(:block_scout_web, :show_percentage) + Application.put_env(:block_scout_web, :show_percentage, false) + + :ok + + on_exit(fn -> + Application.put_env(:block_scout_web, :show_percentage, configuration) + end) + end + + test "returns top addresses", %{conn: conn} do + address_hashes = + 4..1 + |> Enum.map(&insert(:address, fetched_coin_balance: &1)) + |> Enum.map(& &1.hash) + + start_supervised!(AddressesCount) + AddressesCount.consolidate() + + conn = get(conn, address_path(conn, :index, %{type: "JSON"})) + {:ok, %{"items" => items}} = Poison.decode(conn.resp_body) + + assert Enum.count(items) == Enum.count(address_hashes) + end + + test "returns an address's primary name when present", %{conn: conn} do + address = insert(:address, fetched_coin_balance: 1) + insert(:address_name, address: address, primary: true, name: "POA Wallet") + + start_supervised!(AddressesCount) + AddressesCount.consolidate() + + conn = get(conn, address_path(conn, :index, %{type: "JSON"})) + + {:ok, %{"items" => [item]}} = Poison.decode(conn.resp_body) + + assert String.contains?(item, "POA Wallet") + end + end + + describe "GET show/3" do + setup :set_mox_global + + test "redirects to address/:address_id/transactions", %{conn: conn} do + insert(:address, hash: "0x5aaeb6053f3e94c9b9a09f33669435e7ef1beaed") + + conn = get(conn, "/address/#{Address.checksum("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed")}") + + assert html_response(conn, 200) + end + end + + describe "GET address-counters/2" do + test "returns address counters", %{conn: conn} do + address = insert(:address) + + conn = get(conn, "/address-counters", %{"id" => Address.checksum(address.hash)}) + + assert conn.status == 200 + {:ok, response} = Jason.decode(conn.resp_body) + + assert %{ + "transaction_count" => 0, + "token_transfer_count" => 0, + "validation_count" => 0, + "gas_usage_count" => 0 + } == + response + end + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/address_internal_transaction_controller_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/address_internal_transaction_controller_test.exs new file mode 100644 index 0000000..bfb0e2c --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/address_internal_transaction_controller_test.exs @@ -0,0 +1,572 @@ +defmodule BlockScoutWeb.AddressInternalTransactionControllerTest do + use BlockScoutWeb.ConnCase, async: true + + import BlockScoutWeb.Routers.WebRouter.Helpers, + only: [address_internal_transaction_path: 3, address_internal_transaction_path: 4] + + alias Explorer.Chain.{Address, Block, InternalTransaction, Transaction} + alias Explorer.Market.Token + + describe "GET index/3" do + test "with invalid address hash", %{conn: conn} do + conn = + conn + |> get(address_internal_transaction_path(BlockScoutWeb.Endpoint, :index, "invalid_address")) + + assert html_response(conn, 404) + end + + if Application.compile_env(:explorer, :chain_type) !== :rsk do + test "with valid address hash without address", %{conn: conn} do + conn = + get( + conn, + address_internal_transaction_path( + conn, + :index, + Address.checksum("0x8bf38d4764929064f2d4d3a56520a76ab3df415b") + ) + ) + + assert html_response(conn, 200) + end + end + + test "includes USD exchange rate value for address in assigns", %{conn: conn} do + address = insert(:address) + + conn = + get(conn, address_internal_transaction_path(BlockScoutWeb.Endpoint, :index, Address.checksum(address.hash))) + + assert %Token{} = conn.assigns.exchange_rate + end + + test "returns internal transactions for the address", %{conn: conn} do + address = insert(:address) + + transaction = + :transaction + |> insert() + |> with_block(insert(:block, number: 1)) + + from_internal_transaction = + insert(:internal_transaction, + transaction: transaction, + from_address: address, + index: 1, + block_number: transaction.block_number, + transaction_index: transaction.index, + block_hash: transaction.block_hash, + block_index: 1 + ) + + to_internal_transaction = + insert(:internal_transaction, + transaction: transaction, + to_address: address, + index: 2, + block_number: transaction.block_number, + transaction_index: transaction.index, + block_hash: transaction.block_hash, + block_index: 2 + ) + + path = address_internal_transaction_path(conn, :index, Address.checksum(address), %{"type" => "JSON"}) + conn = get(conn, path) + + internal_transaction_tiles = json_response(conn, 200)["items"] + + assert Enum.all?([from_internal_transaction, to_internal_transaction], fn internal_transaction -> + Enum.any?(internal_transaction_tiles, fn tile -> + String.contains?(tile, to_string(internal_transaction.transaction_hash)) && + String.contains?(tile, "data-internal-transaction-index=\"#{internal_transaction.index}\"") + end) + end) + end + + test "returns internal transactions coming from the address", %{conn: conn} do + address = insert(:address) + + transaction = + :transaction + |> insert() + |> with_block(insert(:block, number: 1)) + + from_internal_transaction = + insert(:internal_transaction, + transaction: transaction, + from_address: address, + index: 1, + block_number: transaction.block_number, + transaction_index: transaction.index, + block_hash: transaction.block_hash, + block_index: 1 + ) + + to_internal_transaction = + insert(:internal_transaction, + transaction: transaction, + to_address: address, + index: 2, + block_number: transaction.block_number, + transaction_index: transaction.index, + block_hash: transaction.block_hash, + block_index: 2 + ) + + path = + address_internal_transaction_path(conn, :index, Address.checksum(address), %{ + "filter" => "from", + "type" => "JSON" + }) + + conn = get(conn, path) + + internal_transaction_tiles = json_response(conn, 200)["items"] + + assert Enum.any?(internal_transaction_tiles, fn tile -> + String.contains?(tile, to_string(from_internal_transaction.transaction_hash)) && + String.contains?(tile, "data-internal-transaction-index=\"#{from_internal_transaction.index}\"") + end) + + refute Enum.any?(internal_transaction_tiles, fn tile -> + String.contains?(tile, to_string(to_internal_transaction.transaction_hash)) && + String.contains?(tile, "data-internal-transaction-index=\"#{to_internal_transaction.index}\"") + end) + end + + test "returns internal transactions going to the address", %{conn: conn} do + address = insert(:address) + + transaction = + :transaction + |> insert() + |> with_block(insert(:block, number: 1)) + + from_internal_transaction = + insert(:internal_transaction, + transaction: transaction, + from_address: address, + index: 1, + block_number: transaction.block_number, + transaction_index: transaction.index, + block_hash: transaction.block_hash, + block_index: 1 + ) + + to_internal_transaction = + insert(:internal_transaction, + transaction: transaction, + to_address: address, + index: 2, + block_number: transaction.block_number, + transaction_index: transaction.index, + block_hash: transaction.block_hash, + block_index: 2 + ) + + path = + address_internal_transaction_path(conn, :index, Address.checksum(address), %{"filter" => "to", "type" => "JSON"}) + + conn = get(conn, path) + + internal_transaction_tiles = json_response(conn, 200)["items"] + + assert Enum.any?(internal_transaction_tiles, fn tile -> + String.contains?(tile, to_string(to_internal_transaction.transaction_hash)) && + String.contains?(tile, "data-internal-transaction-index=\"#{to_internal_transaction.index}\"") + end) + + refute Enum.any?(internal_transaction_tiles, fn tile -> + String.contains?(tile, to_string(from_internal_transaction.transaction_hash)) && + String.contains?(tile, "data-internal-transaction-index=\"#{from_internal_transaction.index}\"") + end) + end + + test "returns internal an transaction that created the address", %{conn: conn} do + address = insert(:address) + + transaction = + :transaction + |> insert() + |> with_block(insert(:block, number: 1)) + + from_internal_transaction = + insert(:internal_transaction, + transaction: transaction, + from_address: address, + index: 1, + block_number: transaction.block_number, + transaction_index: transaction.index, + block_hash: transaction.block_hash, + block_index: 1 + ) + + to_internal_transaction = + insert(:internal_transaction, + transaction: transaction, + to_address: nil, + created_contract_address: address, + index: 2, + block_number: transaction.block_number, + transaction_index: transaction.index, + block_hash: transaction.block_hash, + block_index: 2 + ) + + path = + address_internal_transaction_path(conn, :index, Address.checksum(address), %{"filter" => "to", "type" => "JSON"}) + + conn = get(conn, path) + + internal_transaction_tiles = json_response(conn, 200)["items"] + + assert Enum.any?(internal_transaction_tiles, fn tile -> + String.contains?(tile, to_string(to_internal_transaction.transaction_hash)) && + String.contains?(tile, "data-internal-transaction-index=\"#{to_internal_transaction.index}\"") + end) + + refute Enum.any?(internal_transaction_tiles, fn tile -> + String.contains?(tile, to_string(from_internal_transaction.transaction_hash)) && + String.contains?(tile, "data-internal-transaction-index=\"#{from_internal_transaction.index}\"") + end) + end + + test "returns next page of results based on last seen internal transaction", %{conn: conn} do + address = insert(:address) + + a_block = insert(:block, number: 1000) + b_block = insert(:block, number: 2000) + + transaction_1 = + :transaction + |> insert() + |> with_block(a_block) + + transaction_2 = + :transaction + |> insert() + |> with_block(a_block) + + transaction_3 = + :transaction + |> insert() + |> with_block(b_block) + + transaction_1_hashes = + 1..20 + |> Enum.map(fn index -> + insert( + :internal_transaction, + transaction: transaction_1, + from_address: address, + index: index, + block_number: transaction_1.block_number, + transaction_index: transaction_1.index, + block_hash: a_block.hash, + block_index: index + ) + end) + + transaction_2_hashes = + 1..20 + |> Enum.map(fn index -> + insert( + :internal_transaction, + transaction: transaction_2, + from_address: address, + index: index, + block_number: transaction_2.block_number, + transaction_index: transaction_2.index, + block_hash: a_block.hash, + block_index: 20 + index + ) + end) + + transaction_3_hashes = + 1..10 + |> Enum.map(fn index -> + insert( + :internal_transaction, + transaction: transaction_3, + from_address: address, + index: index, + block_number: transaction_3.block_number, + transaction_index: transaction_3.index, + block_hash: b_block.hash, + block_index: index + ) + end) + + second_page = transaction_1_hashes ++ transaction_2_hashes ++ transaction_3_hashes + + %InternalTransaction{index: index} = + insert( + :internal_transaction, + transaction: transaction_3, + from_address: address, + index: 11, + block_number: transaction_3.block_number, + transaction_index: transaction_3.index, + block_hash: b_block.hash, + block_index: 11 + ) + + conn = + get(conn, address_internal_transaction_path(BlockScoutWeb.Endpoint, :index, Address.checksum(address.hash)), %{ + "block_number" => Integer.to_string(b_block.number), + "transaction_index" => Integer.to_string(transaction_3.index), + "index" => Integer.to_string(index), + "type" => "JSON" + }) + + internal_transaction_tiles = json_response(conn, 200)["items"] + + assert Enum.all?(second_page, fn internal_transaction -> + Enum.any?(internal_transaction_tiles, fn tile -> + String.contains?(tile, to_string(internal_transaction.transaction_hash)) && + String.contains?(tile, "data-internal-transaction-index=\"#{internal_transaction.index}\"") + end) + end) + end + + test "next page doesn't miss internal transactions", %{conn: conn} do + address = insert(:address) + + a_block = insert(:block, number: 1000) + b_block = insert(:block, number: 2000) + + transaction_1 = + :transaction + |> insert() + |> with_block(a_block) + + transaction_2 = + :transaction + |> insert() + |> with_block(a_block) + + transaction_3 = + :transaction + |> insert() + |> with_block(b_block) + + from_internal_transactions = + 1..55 + |> Enum.map(fn index -> + insert( + :internal_transaction, + transaction: transaction_1, + from_address: address, + index: index, + block_number: transaction_1.block_number, + transaction_index: transaction_1.index, + block_hash: a_block.hash, + block_index: index + ) + end) + + to_internal_transactions = + 1..55 + |> Enum.map(fn index -> + insert( + :internal_transaction, + transaction: transaction_2, + to_address: address, + index: index, + block_number: transaction_2.block_number, + transaction_index: transaction_2.index, + block_hash: a_block.hash, + block_index: 55 + index + ) + end) + + created_contract_internal_transactions = + 1..55 + |> Enum.map(fn index -> + insert( + :internal_transaction, + transaction: transaction_3, + created_contract_address: address, + index: index, + block_number: transaction_3.block_number, + transaction_index: transaction_3.index, + block_hash: b_block.hash, + block_index: index + ) + end) + + {second_page_contract_items, first_page_items} = Enum.split(created_contract_internal_transactions, 5) + {third_page_to_items, second_page_to_items} = Enum.split(to_internal_transactions, 10) + {fourth_page_items, third_page_from_items} = Enum.split(from_internal_transactions, 15) + + second_page_items = second_page_contract_items ++ second_page_to_items + third_page_items = third_page_to_items ++ third_page_from_items + + path = address_internal_transaction_path(BlockScoutWeb.Endpoint, :index, Address.checksum(address.hash)) + + empty_page_response = + conn + |> get(path, %{ + "block_number" => Integer.to_string(0), + "transaction_index" => Integer.to_string(0), + "index" => "0", + "type" => "JSON" + }) + |> json_response(200) + |> Map.get("items") + + assert Enum.count(empty_page_response) == 0 + + first_page_response = + conn + |> get(path, %{"type" => "JSON"}) + |> json_response(200) + |> Map.get("items") + + second_page_response = + conn + |> get(path, %{ + "block_number" => Integer.to_string(b_block.number), + "transaction_index" => Integer.to_string(transaction_3.index), + "index" => "6", + "type" => "JSON" + }) + |> json_response(200) + |> Map.get("items") + + third_page_response = + conn + |> get(path, %{ + "block_number" => Integer.to_string(a_block.number), + "transaction_index" => Integer.to_string(transaction_2.index), + "index" => "11", + "type" => "JSON" + }) + |> json_response(200) + |> Map.get("items") + + fourth_page_response = + conn + |> get(path, %{ + "block_number" => Integer.to_string(a_block.number), + "transaction_index" => Integer.to_string(transaction_1.index), + "index" => "16", + "type" => "JSON" + }) + |> json_response(200) + |> Map.get("items") + + assert Enum.count(first_page_response) == 50 + + assert Enum.all?(first_page_items, fn internal_transaction -> + Enum.any?(first_page_response, fn tile -> + String.contains?(tile, to_string(internal_transaction.transaction_hash)) && + String.contains?(tile, "data-internal-transaction-index=\"#{internal_transaction.index}\"") + end) + end) + + assert Enum.count(second_page_response) == 50 + + assert Enum.all?(second_page_items, fn internal_transaction -> + Enum.any?(second_page_response, fn tile -> + String.contains?(tile, to_string(internal_transaction.transaction_hash)) && + String.contains?(tile, "data-internal-transaction-index=\"#{internal_transaction.index}\"") + end) + end) + + assert Enum.count(third_page_response) == 50 + + assert Enum.all?(third_page_items, fn internal_transaction -> + Enum.any?(third_page_response, fn tile -> + String.contains?(tile, to_string(internal_transaction.transaction_hash)) && + String.contains?(tile, "data-internal-transaction-index=\"#{internal_transaction.index}\"") + end) + end) + + assert Enum.count(fourth_page_response) == 15 + + assert Enum.all?(fourth_page_items, fn internal_transaction -> + Enum.any?(fourth_page_response, fn tile -> + String.contains?(tile, to_string(internal_transaction.transaction_hash)) && + String.contains?(tile, "data-internal-transaction-index=\"#{internal_transaction.index}\"") + end) + end) + end + + test "next_page_params exist if not on last page", %{conn: conn} do + address = insert(:address) + block = %Block{number: number} = insert(:block, number: 7000) + + transaction = + %Transaction{index: transaction_index} = + :transaction + |> insert() + |> with_block(block) + + 1..60 + |> Enum.map(fn index -> + insert( + :internal_transaction, + transaction: transaction, + from_address: address, + index: index, + block_number: transaction.block_number, + transaction_index: transaction.index, + block_hash: transaction.block_hash, + block_index: index + ) + end) + + conn = + get( + conn, + address_internal_transaction_path(BlockScoutWeb.Endpoint, :index, Address.checksum(address.hash), %{ + "type" => "JSON" + }) + ) + + expected_response = + address_internal_transaction_path(BlockScoutWeb.Endpoint, :index, address.hash, %{ + "block_number" => number, + "index" => 11, + "transaction_index" => transaction_index, + "items_count" => "50" + }) + + assert expected_response == json_response(conn, 200)["next_page_path"] + end + + test "next_page_params are empty if on last page", %{conn: conn} do + address = insert(:address) + + transaction = + :transaction + |> insert() + |> with_block() + + 1..2 + |> Enum.map(fn index -> + insert( + :internal_transaction, + transaction: transaction, + from_address: address, + index: index, + block_hash: transaction.block_hash, + block_index: index, + block_number: transaction.block_number + ) + end) + + conn = + get( + conn, + address_internal_transaction_path(BlockScoutWeb.Endpoint, :index, Address.checksum(address.hash), %{ + "type" => "JSON" + }) + ) + + assert %{"next_page_path" => nil} = json_response(conn, 200) + end + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/address_read_contract_controller_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/address_read_contract_controller_test.exs new file mode 100644 index 0000000..b982eea --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/address_read_contract_controller_test.exs @@ -0,0 +1,76 @@ +defmodule BlockScoutWeb.AddressReadContractControllerTest do + use BlockScoutWeb.ConnCase, async: true + use ExUnit.Case, async: false + + alias Explorer.Market.Token + alias Explorer.Chain.Address + alias Explorer.TestHelper + + import Mox + + setup :verify_on_exit! + + describe "GET index/3" do + setup :set_mox_global + + test "with invalid address hash", %{conn: conn} do + conn = get(conn, address_read_contract_path(BlockScoutWeb.Endpoint, :index, "invalid_address")) + + assert html_response(conn, 404) + end + + test "with valid address that is not a contract", %{conn: conn} do + address = insert(:address) + + conn = get(conn, address_read_contract_path(BlockScoutWeb.Endpoint, :index, Address.checksum(address.hash))) + + assert html_response(conn, 404) + end + + test "successfully renders the page when the address is a contract", %{conn: conn} do + contract_address = insert(:contract_address) + + transaction = insert(:transaction, from_address: contract_address) |> with_block() + + insert( + :internal_transaction_create, + index: 0, + transaction: transaction, + created_contract_address: contract_address, + block_hash: transaction.block_hash, + block_index: 0 + ) + + insert(:smart_contract, address_hash: contract_address.hash, contract_code_md5: "123") + + TestHelper.get_all_proxies_implementation_zero_addresses() + + conn = + get(conn, address_read_contract_path(BlockScoutWeb.Endpoint, :index, Address.checksum(contract_address.hash))) + + assert html_response(conn, 200) + assert contract_address.hash == conn.assigns.address.hash + assert %Token{} = conn.assigns.exchange_rate + end + + test "returns not found for an unverified contract", %{conn: conn} do + contract_address = insert(:contract_address) + + transaction = insert(:transaction, from_address: contract_address) |> with_block() + + insert( + :internal_transaction_create, + index: 0, + transaction: transaction, + created_contract_address: contract_address, + block_hash: transaction.block_hash, + block_index: 0 + ) + + conn = + get(conn, address_read_contract_path(BlockScoutWeb.Endpoint, :index, Address.checksum(contract_address.hash))) + + assert html_response(conn, 404) + end + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/address_read_proxy_controller_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/address_read_proxy_controller_test.exs new file mode 100644 index 0000000..e9ce067 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/address_read_proxy_controller_test.exs @@ -0,0 +1,74 @@ +defmodule BlockScoutWeb.AddressReadProxyControllerTest do + use BlockScoutWeb.ConnCase, async: true + use ExUnit.Case, async: false + + alias Explorer.Market.Token + alias Explorer.Chain.Address + alias Explorer.TestHelper + + import Mox + + setup :verify_on_exit! + + describe "GET index/3" do + setup :set_mox_global + + test "with invalid address hash", %{conn: conn} do + conn = get(conn, address_read_proxy_path(BlockScoutWeb.Endpoint, :index, "invalid_address")) + + assert html_response(conn, 404) + end + + test "with valid address that is not a contract", %{conn: conn} do + address = insert(:address) + + conn = get(conn, address_read_proxy_path(BlockScoutWeb.Endpoint, :index, Address.checksum(address.hash))) + + assert html_response(conn, 404) + end + + test "successfully renders the page when the address is a contract", %{conn: conn} do + contract_address = insert(:contract_address) + + transaction = insert(:transaction, from_address: contract_address) |> with_block() + + insert( + :internal_transaction_create, + index: 0, + transaction: transaction, + created_contract_address: contract_address, + block_hash: transaction.block_hash, + block_index: 0 + ) + + insert(:smart_contract, address_hash: contract_address.hash, contract_code_md5: "123") + + TestHelper.get_all_proxies_implementation_zero_addresses() + + conn = get(conn, address_read_proxy_path(BlockScoutWeb.Endpoint, :index, Address.checksum(contract_address.hash))) + + assert html_response(conn, 200) + assert contract_address.hash == conn.assigns.address.hash + assert %Token{} = conn.assigns.exchange_rate + end + + test "returns not found for an unverified contract", %{conn: conn} do + contract_address = insert(:contract_address) + + transaction = insert(:transaction, from_address: contract_address) |> with_block() + + insert( + :internal_transaction_create, + index: 0, + transaction: transaction, + created_contract_address: contract_address, + block_hash: transaction.block_hash, + block_index: 0 + ) + + conn = get(conn, address_read_proxy_path(BlockScoutWeb.Endpoint, :index, Address.checksum(contract_address.hash))) + + assert html_response(conn, 404) + end + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/address_token_balance_controller_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/address_token_balance_controller_test.exs new file mode 100644 index 0000000..cdafe88 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/address_token_balance_controller_test.exs @@ -0,0 +1,46 @@ +defmodule BlockScoutWeb.AddressTokenBalanceControllerTest do + use BlockScoutWeb.ConnCase + + alias Explorer.Chain.Address + alias Explorer.Factory + + describe "GET index/3" do + test "without AJAX", %{conn: conn} do + %Address{hash: hash} = Factory.insert(:address) + + response_conn = get(conn, address_token_balance_path(conn, :index, Address.checksum(hash))) + + assert html_response(response_conn, 404) + end + + test "with AJAX without valid address", %{conn: conn} do + ajax_conn = ajax(conn) + + response_conn = get(ajax_conn, address_token_balance_path(ajax_conn, :index, "invalid_address")) + + assert html_response(response_conn, 404) + end + + test "with AJAX with valid address without address still returns token balances", %{conn: conn} do + ajax_conn = ajax(conn) + + response_conn = get(ajax_conn, address_token_balance_path(ajax_conn, :index, Address.checksum(address_hash()))) + + assert html_response(response_conn, 200) + end + + test "with AJAX with valid address with address returns token balances", %{conn: conn} do + %Address{hash: hash} = Factory.insert(:address) + + ajax_conn = ajax(conn) + + response_conn = get(ajax_conn, address_token_balance_path(ajax_conn, :index, Address.checksum(hash))) + + assert html_response(response_conn, 200) + end + end + + defp ajax(conn) do + put_req_header(conn, "x-requested-with", "XMLHttpRequest") + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/address_token_controller_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/address_token_controller_test.exs new file mode 100644 index 0000000..bfbc172 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/address_token_controller_test.exs @@ -0,0 +1,223 @@ +defmodule BlockScoutWeb.AddressTokenControllerTest do + use BlockScoutWeb.ConnCase, async: true + use ExUnit.Case, async: false + + import BlockScoutWeb.Routers.WebRouter.Helpers, only: [address_token_path: 3] + import Mox + + alias Explorer.Chain.{Address, Token} + + describe "GET index/2" do + setup :set_mox_global + + test "with invalid address hash", %{conn: conn} do + conn = get(conn, address_token_path(conn, :index, "invalid_address")) + + assert html_response(conn, 422) + end + + test "with valid address hash without address", %{conn: conn} do + conn = get(conn, address_token_path(conn, :index, Address.checksum("0x8bf38d4764929064f2d4d3a56520a76ab3df415b"))) + + assert html_response(conn, 404) + end + + test "returns tokens that have balance for the address", %{conn: conn} do + address = insert(:address) + + token1 = + :token + |> insert(name: "token1") + + token2 = + :token + |> insert(name: "token2") + + insert( + :address_current_token_balance, + address: address, + token_contract_address_hash: token1.contract_address_hash, + value: 1000 + ) + + insert( + :address_current_token_balance, + address: address, + token_contract_address_hash: token2.contract_address_hash, + value: 0 + ) + + insert( + :token_transfer, + token_contract_address: token1.contract_address, + from_address: address, + to_address: build(:address) + ) + + insert( + :token_transfer, + token_contract_address: token2.contract_address, + from_address: build(:address), + to_address: address + ) + + conn = get(conn, address_token_path(conn, :index, Address.checksum(address)), type: "JSON") + + {:ok, %{"items" => items}} = + conn.resp_body + |> Poison.decode() + + assert json_response(conn, 200) + assert Enum.any?(items, fn item -> String.contains?(item, to_string(token1.contract_address_hash)) end) + refute Enum.any?(items, fn item -> String.contains?(item, to_string(token2.contract_address_hash)) end) + end + + test "returns next page of results based on last seen token", %{conn: conn} do + address = insert(:address) + + second_page_tokens = + 1..50 + |> Enum.reduce([], fn i, acc -> + token = insert(:token, name: "A Token#{i}", type: "ERC-20") + + insert( + :address_current_token_balance, + token_contract_address_hash: token.contract_address_hash, + address: address, + value: 1000 + ) + + acc ++ [token.name] + end) + |> Enum.sort() + + token = insert(:token, name: "Another Token", type: "ERC-721") + + insert( + :address_current_token_balance, + token_contract_address_hash: token.contract_address_hash, + address: address, + value: 1000 + ) + + %Token{name: name, type: type, inserted_at: _inserted_at} = token + + conn = + get(conn, address_token_path(BlockScoutWeb.Endpoint, :index, Address.checksum(address.hash)), %{ + "token_name" => name, + "token_type" => type, + "value" => 1000, + "type" => "JSON" + }) + + {:ok, %{"items" => items}} = + conn.resp_body + |> Poison.decode() + + assert Enum.any?(items, fn item -> + Enum.any?(second_page_tokens, fn token_name -> String.contains?(item, token_name) end) + end) + end + + test "returns next page of results based on last seen token for erc-1155", %{conn: conn} do + address = insert(:address) + + 1..51 + |> Enum.reduce([], fn _i, acc -> + token = insert(:token, name: "FN2 Token", type: "ERC-1155") + + insert( + :address_current_token_balance, + token_contract_address_hash: token.contract_address_hash, + address: address, + value: 3 + ) + + acc ++ [token.name] + end) + + conn = + get(conn, address_token_path(BlockScoutWeb.Endpoint, :index, Address.checksum(address.hash)), %{ + "type" => "JSON" + }) + + assert response = json_response(conn, 200) + + request_2nd_page = get(conn, response["next_page_path"], %{"type" => "JSON"}) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + assert 1 = length(response_2nd_page["items"]) + end + + test "returns next page of results based on last seen token for erc-404", %{conn: conn} do + address = insert(:address) + + 1..51 + |> Enum.reduce([], fn _i, acc -> + token = insert(:token, name: "FN2 Token", type: "ERC-404") + + insert( + :address_current_token_balance, + token_contract_address_hash: token.contract_address_hash, + address: address, + value: 3 + ) + + acc ++ [token.name] + end) + + conn = + get(conn, address_token_path(BlockScoutWeb.Endpoint, :index, Address.checksum(address.hash)), %{ + "type" => "JSON" + }) + + assert response = json_response(conn, 200) + + request_2nd_page = get(conn, response["next_page_path"], %{"type" => "JSON"}) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + assert 1 = length(response_2nd_page["items"]) + end + + test "next_page_params exists if not on last page", %{conn: conn} do + address = insert(:address) + + Enum.each(1..51, fn i -> + token = insert(:token, name: "A Token#{i}", type: "ERC-20") + + insert( + :address_current_token_balance, + token_contract_address_hash: token.contract_address_hash, + address: address, + value: 1000 + ) + + insert(:token_transfer, token_contract_address: token.contract_address, from_address: address) + end) + + conn = get(conn, address_token_path(BlockScoutWeb.Endpoint, :index, Address.checksum(address.hash)), type: "JSON") + + {:ok, %{"next_page_path" => next_page_path}} = + conn.resp_body + |> Poison.decode() + + assert next_page_path + end + + test "next_page_params are empty if on last page", %{conn: conn} do + address = insert(:address) + token = insert(:token) + insert(:token_transfer, token_contract_address: token.contract_address, from_address: address) + + conn = get(conn, address_token_path(BlockScoutWeb.Endpoint, :index, Address.checksum(address.hash)), type: "JSON") + + {:ok, %{"next_page_path" => next_page_path}} = + conn.resp_body + |> Poison.decode() + + refute next_page_path + end + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/address_token_transfer_controller_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/address_token_transfer_controller_test.exs new file mode 100644 index 0000000..e6ea850 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/address_token_transfer_controller_test.exs @@ -0,0 +1,272 @@ +defmodule BlockScoutWeb.AddressTokenTransferControllerTest do + use BlockScoutWeb.ConnCase + + import BlockScoutWeb.Routers.WebRouter.Helpers, + only: [address_token_transfers_path: 4, address_token_transfers_path: 5] + + alias Explorer.Chain.{Address, Token} + + describe "GET index/2" do + test "with invalid address hash", %{conn: conn} do + token_hash = "0xc8982771dd50285389c352c175ada74d074427c7" + conn = get(conn, address_token_transfers_path(conn, :index, "invalid_address", token_hash)) + + assert html_response(conn, 422) + end + + test "with invalid token hash", %{conn: conn} do + address_hash = "0x8bf38d4764929064f2d4d3a56520a76ab3df415b" + + conn = get(conn, address_token_transfers_path(conn, :index, Address.checksum(address_hash), "invalid_address")) + + assert html_response(conn, 422) + end + + test "with an address that doesn't exist in our database", %{conn: conn} do + address_hash = "0x8bf38d4764929064f2d4d3a56520a76ab3df415b" + %Token{contract_address_hash: token_hash} = insert(:token) + + conn = + get( + conn, + address_token_transfers_path(conn, :index, Address.checksum(address_hash), Address.checksum(token_hash)) + ) + + assert html_response(conn, 404) + end + + test "with an token that doesn't exist in our database", %{conn: conn} do + %Address{hash: address_hash} = insert(:address) + token_hash = Address.checksum("0x8bf38d4764929064f2d4d3a56520a76ab3df415b") + conn = get(conn, address_token_transfers_path(conn, :index, Address.checksum(address_hash), token_hash)) + + assert html_response(conn, 404) + end + end + + describe "GET index/2 JSON" do + test "without token transfers for a token", %{conn: conn} do + %Address{hash: address_hash} = insert(:address) + %Token{contract_address_hash: token_hash} = insert(:token) + + conn = + get( + conn, + address_token_transfers_path(conn, :index, Address.checksum(address_hash), Address.checksum(token_hash)), + %{ + type: "JSON" + } + ) + + assert json_response(conn, 200) == %{"items" => [], "next_page_path" => nil} + end + + test "returns the correct number of transactions", %{conn: conn} do + address = insert(:address) + token = insert(:token) + + inserted_transactions = + Enum.map(1..5, fn index -> + block = insert(:block, number: 1000 - index) + + transaction = + :transaction + |> insert() + |> with_block(block) + + insert( + :token_transfer, + to_address: address, + transaction: transaction, + token_contract_address: token.contract_address + ) + + transaction + end) + + conn = + get( + conn, + address_token_transfers_path( + conn, + :index, + Address.checksum(address.hash), + Address.checksum(token.contract_address_hash) + ), + %{type: "JSON"} + ) + + response_items = + conn + |> json_response(200) + |> Map.get("items") + + items_length = length(response_items) + + assert items_length == 5 + + assert Enum.all?(inserted_transactions, fn transaction -> + Enum.any?(response_items, fn item -> + String.contains?( + item, + "data-identifier-hash=\"#{to_string(transaction.hash)}\"" + ) + end) + end) + end + + test "returns next_page_path as null when there are no more pages", %{conn: conn} do + address = insert(:address) + token = insert(:token) + + block = insert(:block, number: 1000) + + transaction = + :transaction + |> insert() + |> with_block(block) + + insert( + :token_transfer, + to_address: address, + transaction: transaction, + token_contract_address: token.contract_address + ) + + conn = + get( + conn, + address_token_transfers_path( + conn, + :index, + Address.checksum(address.hash), + Address.checksum(token.contract_address_hash) + ), + %{type: "JSON"} + ) + + assert Map.get(json_response(conn, 200), "next_page_path") == nil + end + + test "returns next_page_path when there are more items", %{conn: conn} do + address = insert(:address) + token = insert(:token) + + page_last_transfer = + 1..50 + |> Enum.map(fn index -> + block = insert(:block, number: 1000 - index) + + transaction = + :transaction + |> insert() + |> with_block(block) + + insert( + :token_transfer, + to_address: address, + transaction: transaction, + token_contract_address: token.contract_address + ) + + transaction + end) + |> List.last() + + Enum.each(51..60, fn index -> + block = insert(:block, number: 1000 - index) + + transaction = + :transaction + |> insert() + |> with_block(block) + + insert( + :token_transfer, + to_address: address, + transaction: transaction, + token_contract_address: token.contract_address + ) + end) + + conn = + get( + conn, + address_token_transfers_path( + conn, + :index, + Address.checksum(address.hash), + Address.checksum(token.contract_address_hash) + ), + %{type: "JSON"} + ) + + expected_path = + address_token_transfers_path( + conn, + :index, + Address.checksum(address.hash), + Address.checksum(token.contract_address_hash), + block_number: page_last_transfer.block_number, + index: page_last_transfer.index, + items_count: "50" + ) + + assert Map.get(json_response(conn, 200), "next_page_path") == expected_path + end + + test "with invalid address hash", %{conn: conn} do + token_hash = "0xc8982771dd50285389c352c175ada74d074427c7" + + conn = + get(conn, address_token_transfers_path(conn, :index, "invalid_address", token_hash), %{ + type: "JSON" + }) + + assert html_response(conn, 422) + end + + test "with invalid token hash", %{conn: conn} do + address_hash = "0x8bf38d4764929064f2d4d3a56520a76ab3df415b" + + conn = + get(conn, address_token_transfers_path(conn, :index, Address.checksum(address_hash), "invalid_address"), %{ + type: "JSON" + }) + + assert html_response(conn, 422) + end + + test "with an address that doesn't exist in our database", %{conn: conn} do + address_hash = "0x8bf38d4764929064f2d4d3a56520a76ab3df415b" + %Token{contract_address_hash: token_hash} = insert(:token) + + conn = + get( + conn, + address_token_transfers_path(conn, :index, Address.checksum(address_hash), Address.checksum(token_hash)), + %{ + type: "JSON" + } + ) + + assert html_response(conn, 404) + end + + test "with a token that doesn't exist in our database", %{conn: conn} do + %Address{hash: address_hash} = insert(:address) + token_hash = "0x8bf38d4764929064f2d4d3a56520a76ab3df415b" + + conn = + get( + conn, + address_token_transfers_path(conn, :index, Address.checksum(address_hash), Address.checksum(token_hash)), + %{ + type: "JSON" + } + ) + + assert html_response(conn, 404) + end + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/address_transaction_controller_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/address_transaction_controller_test.exs new file mode 100644 index 0000000..57e6c0f --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/address_transaction_controller_test.exs @@ -0,0 +1,153 @@ +defmodule BlockScoutWeb.AddressTransactionControllerTest do + use BlockScoutWeb.ConnCase, async: true + use ExUnit.Case, async: false + + import BlockScoutWeb.Routers.WebRouter.Helpers, only: [address_transaction_path: 3, address_transaction_path: 4] + import Mox + + alias Explorer.Chain.{Address, Transaction} + alias Explorer.Market.Token + + setup :verify_on_exit! + + describe "GET index/2" do + setup :set_mox_global + + test "with invalid address hash", %{conn: conn} do + conn = get(conn, address_transaction_path(conn, :index, "invalid_address")) + + assert html_response(conn, 422) + end + + if Application.compile_env(:explorer, :chain_type) !== :rsk do + test "with valid address hash without address in the DB", %{conn: conn} do + conn = + get( + conn, + address_transaction_path(conn, :index, Address.checksum("0x8bf38d4764929064f2d4d3a56520a76ab3df415b"), %{ + "type" => "JSON" + }) + ) + + assert json_response(conn, 200) + transaction_tiles = json_response(conn, 200)["items"] + assert transaction_tiles |> length() == 0 + end + end + + test "returns transactions for the address", %{conn: conn} do + address = insert(:address) + + block = insert(:block) + + from_transaction = + :transaction + |> insert(from_address: address) + |> with_block(block) + + to_transaction = + :transaction + |> insert(to_address: address) + |> with_block(block) + + conn = get(conn, address_transaction_path(conn, :index, Address.checksum(address), %{"type" => "JSON"})) + + transaction_tiles = json_response(conn, 200)["items"] + transaction_hashes = Enum.map([to_transaction.hash, from_transaction.hash], &to_string(&1)) + + assert Enum.all?(transaction_hashes, fn transaction_hash -> + Enum.any?(transaction_tiles, &String.contains?(&1, transaction_hash)) + end) + end + + test "includes USD exchange rate value for address in assigns", %{conn: conn} do + address = insert(:address) + + conn = get(conn, address_transaction_path(BlockScoutWeb.Endpoint, :index, Address.checksum(address.hash))) + + assert %Token{} = conn.assigns.exchange_rate + end + + test "returns next page of results based on last seen transaction", %{conn: conn} do + address = insert(:address) + + second_page_hashes = + 50 + |> insert_list(:transaction, from_address: address) + |> with_block() + |> Enum.map(& &1.hash) + + %Transaction{block_number: block_number, index: index} = + :transaction + |> insert(from_address: address) + |> with_block() + + conn = + get(conn, address_transaction_path(BlockScoutWeb.Endpoint, :index, Address.checksum(address.hash)), %{ + "block_number" => Integer.to_string(block_number), + "index" => Integer.to_string(index), + "type" => "JSON" + }) + + transaction_tiles = json_response(conn, 200)["items"] + + assert Enum.all?(second_page_hashes, fn address_hash -> + Enum.any?(transaction_tiles, &String.contains?(&1, to_string(address_hash))) + end) + end + + test "next_page_params exist if not on last page", %{conn: conn} do + address = insert(:address) + block = insert(:block) + + 60 + |> insert_list(:transaction, from_address: address) + |> with_block(block) + + conn = get(conn, address_transaction_path(conn, :index, Address.checksum(address.hash), %{"type" => "JSON"})) + + assert json_response(conn, 200)["next_page_path"] + end + + test "next_page_params are empty if on last page", %{conn: conn} do + address = insert(:address) + + :transaction + |> insert(from_address: address) + |> with_block() + + conn = get(conn, address_transaction_path(conn, :index, Address.checksum(address.hash), %{"type" => "JSON"})) + + refute json_response(conn, 200)["next_page_path"] + end + + test "returns parent transaction for a contract address", %{conn: conn} do + address = insert(:address, contract_code: data(:address_contract_code)) + block = insert(:block) + + transaction = + :transaction + |> insert(to_address: nil, created_contract_address_hash: address.hash) + |> with_block(block) + |> Explorer.Repo.preload([[created_contract_address: :names], [from_address: :names], :token_transfers]) + + insert( + :internal_transaction_create, + index: 0, + created_contract_address: address, + to_address: nil, + transaction: transaction, + block_hash: block.hash, + block_index: 0 + ) + + conn = get(conn, address_transaction_path(conn, :index, Address.checksum(address)), %{"type" => "JSON"}) + + transaction_tiles = json_response(conn, 200)["items"] + + assert Enum.all?([transaction.hash], fn transaction_hash -> + Enum.any?(transaction_tiles, &String.contains?(&1, to_string(transaction_hash))) + end) + end + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/address_withdrawal_controller_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/address_withdrawal_controller_test.exs new file mode 100644 index 0000000..c4cb831 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/address_withdrawal_controller_test.exs @@ -0,0 +1,116 @@ +defmodule BlockScoutWeb.AddressWithdrawalControllerTest do + use BlockScoutWeb.ConnCase, async: true + use ExUnit.Case, async: false + + import BlockScoutWeb.Routers.WebRouter.Helpers, only: [address_withdrawal_path: 3, address_withdrawal_path: 4] + import BlockScoutWeb.WeiHelper, only: [format_wei_value: 2] + import Mox + + alias Explorer.Chain.Address + alias Explorer.Market.Token + + setup :verify_on_exit! + + describe "GET index/2" do + setup :set_mox_global + + test "with invalid address hash", %{conn: conn} do + conn = get(conn, address_withdrawal_path(conn, :index, "invalid_address")) + + assert html_response(conn, 422) + end + + if Application.compile_env(:explorer, :chain_type) !== :rsk do + test "with valid address hash without address in the DB", %{conn: conn} do + conn = + get( + conn, + address_withdrawal_path(conn, :index, Address.checksum("0x8bf38d4764929064f2d4d3a56520a76ab3df415b"), %{ + "type" => "JSON" + }) + ) + + assert json_response(conn, 200) + tiles = json_response(conn, 200)["items"] + assert tiles |> length() == 0 + end + end + + test "returns withdrawals for the address", %{conn: conn} do + address = insert(:address, withdrawals: insert_list(30, :withdrawal)) + + # to check that we can correctly render address overview + get(conn, address_withdrawal_path(conn, :index, Address.checksum(address))) + + conn = get(conn, address_withdrawal_path(conn, :index, Address.checksum(address), %{"type" => "JSON"})) + + tiles = json_response(conn, 200)["items"] + indexes = Enum.map(address.withdrawals, &to_string(&1.index)) + + assert Enum.all?(indexes, fn index -> + Enum.any?(tiles, &String.contains?(&1, index)) + end) + end + + test "includes USD exchange rate value for address in assigns", %{conn: conn} do + address = insert(:address) + + conn = get(conn, address_withdrawal_path(BlockScoutWeb.Endpoint, :index, Address.checksum(address.hash))) + + assert %Token{} = conn.assigns.exchange_rate + end + + test "returns next page of results based on last seen withdrawal", %{conn: conn} do + address = insert(:address, withdrawals: insert_list(60, :withdrawal)) + + {first_page, second_page} = + address.withdrawals + |> Enum.sort(&(&1.index >= &2.index)) + |> Enum.split(51) + + conn = + get(conn, address_withdrawal_path(BlockScoutWeb.Endpoint, :index, Address.checksum(address.hash)), %{ + "index" => first_page |> List.last() |> (& &1.index).() |> Integer.to_string(), + "type" => "JSON" + }) + + tiles = json_response(conn, 200)["items"] + + assert Enum.all?(second_page, fn withdrawal -> + Enum.any?(tiles, fn tile -> + # more strict check since simple index could occur in the tile accidentally + String.contains?(tile, to_string(withdrawal.index)) and + String.contains?(tile, to_string(withdrawal.validator_index)) and + String.contains?(tile, to_string(withdrawal.block.number)) and + String.contains?(tile, format_wei_value(withdrawal.amount, :ether)) + end) + end) + + refute Enum.any?(first_page, fn withdrawal -> + Enum.any?(tiles, fn tile -> + # more strict check since simple index could occur in the tile accidentally + String.contains?(tile, to_string(withdrawal.index)) and + String.contains?(tile, to_string(withdrawal.validator_index)) and + String.contains?(tile, to_string(withdrawal.block.number)) and + String.contains?(tile, format_wei_value(withdrawal.amount, :ether)) + end) + end) + end + + test "next_page_params exist if not on last page", %{conn: conn} do + address = insert(:address, withdrawals: insert_list(51, :withdrawal)) + + conn = get(conn, address_withdrawal_path(conn, :index, Address.checksum(address.hash), %{"type" => "JSON"})) + + assert json_response(conn, 200)["next_page_path"] + end + + test "next_page_params are empty if on last page", %{conn: conn} do + address = insert(:address, withdrawals: insert_list(1, :withdrawal)) + + conn = get(conn, address_withdrawal_path(conn, :index, Address.checksum(address.hash), %{"type" => "JSON"})) + + refute json_response(conn, 200)["next_page_path"] + end + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/address_write_contract_controller_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/address_write_contract_controller_test.exs new file mode 100644 index 0000000..2b13b6a --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/address_write_contract_controller_test.exs @@ -0,0 +1,78 @@ +defmodule BlockScoutWeb.AddressWriteContractControllerTest do + use BlockScoutWeb.ConnCase, async: true + use ExUnit.Case, async: false + + alias Explorer.Market.Token + alias Explorer.Chain.Address + alias Explorer.TestHelper + + use EthereumJSONRPC.Case, async: false + + import Mox + + setup :verify_on_exit! + + describe "GET index/3" do + setup :set_mox_global + + test "with invalid address hash", %{conn: conn} do + conn = get(conn, address_write_contract_path(BlockScoutWeb.Endpoint, :index, "invalid_address")) + + assert html_response(conn, 404) + end + + test "with valid address that is not a contract", %{conn: conn} do + address = insert(:address) + + conn = get(conn, address_write_contract_path(BlockScoutWeb.Endpoint, :index, Address.checksum(address.hash))) + + assert html_response(conn, 404) + end + + test "successfully renders the page when the address is a contract", %{conn: conn} do + contract_address = insert(:contract_address) + + transaction = insert(:transaction, from_address: contract_address) |> with_block() + + insert( + :internal_transaction_create, + index: 0, + transaction: transaction, + created_contract_address: contract_address, + block_hash: transaction.block_hash, + block_index: 0 + ) + + insert(:smart_contract, address_hash: contract_address.hash, contract_code_md5: "123") + + TestHelper.get_all_proxies_implementation_zero_addresses() + + conn = + get(conn, address_write_contract_path(BlockScoutWeb.Endpoint, :index, Address.checksum(contract_address.hash))) + + assert html_response(conn, 200) + assert contract_address.hash == conn.assigns.address.hash + assert %Token{} = conn.assigns.exchange_rate + end + + test "returns not found for an unverified contract", %{conn: conn} do + contract_address = insert(:contract_address) + + transaction = insert(:transaction, from_address: contract_address) |> with_block() + + insert( + :internal_transaction_create, + index: 0, + transaction: transaction, + created_contract_address: contract_address, + block_hash: transaction.block_hash, + block_index: 0 + ) + + conn = + get(conn, address_write_contract_path(BlockScoutWeb.Endpoint, :index, Address.checksum(contract_address.hash))) + + assert html_response(conn, 404) + end + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/address_write_proxy_controller_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/address_write_proxy_controller_test.exs new file mode 100644 index 0000000..1b0f775 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/address_write_proxy_controller_test.exs @@ -0,0 +1,76 @@ +defmodule BlockScoutWeb.AddressWriteProxyControllerTest do + use BlockScoutWeb.ConnCase, async: true + use ExUnit.Case, async: false + + alias Explorer.Market.Token + alias Explorer.Chain.Address + alias Explorer.TestHelper + + import Mox + + setup :verify_on_exit! + + describe "GET index/3" do + setup :set_mox_global + + test "with invalid address hash", %{conn: conn} do + conn = get(conn, address_write_proxy_path(BlockScoutWeb.Endpoint, :index, "invalid_address")) + + assert html_response(conn, 404) + end + + test "with valid address that is not a contract", %{conn: conn} do + address = insert(:address) + + conn = get(conn, address_write_proxy_path(BlockScoutWeb.Endpoint, :index, Address.checksum(address.hash))) + + assert html_response(conn, 404) + end + + test "successfully renders the page when the address is a contract", %{conn: conn} do + contract_address = insert(:contract_address) + + transaction = insert(:transaction, from_address: contract_address) |> with_block() + + insert( + :internal_transaction_create, + index: 0, + transaction: transaction, + created_contract_address: contract_address, + block_hash: transaction.block_hash, + block_index: 0 + ) + + insert(:smart_contract, address_hash: contract_address.hash, contract_code_md5: "123") + + TestHelper.get_all_proxies_implementation_zero_addresses() + + conn = + get(conn, address_write_proxy_path(BlockScoutWeb.Endpoint, :index, Address.checksum(contract_address.hash))) + + assert html_response(conn, 200) + assert contract_address.hash == conn.assigns.address.hash + assert %Token{} = conn.assigns.exchange_rate + end + + test "returns not found for an unverified contract", %{conn: conn} do + contract_address = insert(:contract_address) + + transaction = insert(:transaction, from_address: contract_address) |> with_block() + + insert( + :internal_transaction_create, + index: 0, + transaction: transaction, + created_contract_address: contract_address, + block_hash: transaction.block_hash, + block_index: 0 + ) + + conn = + get(conn, address_write_proxy_path(BlockScoutWeb.Endpoint, :index, Address.checksum(contract_address.hash))) + + assert html_response(conn, 404) + end + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/admin/dashboard_controller_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/admin/dashboard_controller_test.exs new file mode 100644 index 0000000..03cc0a3 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/admin/dashboard_controller_test.exs @@ -0,0 +1,26 @@ +defmodule BlockScoutWeb.Admin.DashboardControllerTest do + use BlockScoutWeb.ConnCase + + alias BlockScoutWeb.Router + + describe "index/2" do + setup %{conn: conn} do + admin = insert(:administrator) + + conn = + conn + |> bypass_through(Router, [:browser]) + |> get("/") + |> put_session(:user_id, admin.user.id) + |> send_resp(200, "") + |> recycle() + + {:ok, conn: conn} + end + + test "shows the dashboard page", %{conn: conn} do + result = get(conn, "/admin" <> AdminRoutes.dashboard_path(conn, :index)) + assert html_response(result, 200) =~ "administrator_dashboard" + end + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/admin/session_controller_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/admin/session_controller_test.exs new file mode 100644 index 0000000..ce6e4e5 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/admin/session_controller_test.exs @@ -0,0 +1,83 @@ +defmodule BlockScoutWeb.Admin.SessionControllerTest do + use BlockScoutWeb.ConnCase + + setup %{conn: conn} do + conn = + conn + |> bypass_through() + |> get("/") + + {:ok, conn: conn} + end + + describe "new/2" do + test "redirects to setup page if not configured", %{conn: conn} do + result = get(conn, AdminRoutes.session_path(conn, :new)) + assert redirected_to(result) == AdminRoutes.setup_path(conn, :configure) + end + + test "shows the admin login page", %{conn: conn} do + insert(:administrator) + result = get(conn, AdminRoutes.session_path(conn, :new)) + assert html_response(result, 200) =~ "administrator_login" + end + end + + describe "create/2" do + test "redirects to setup page if not configured", %{conn: conn} do + result = post(conn, AdminRoutes.session_path(conn, :create), %{}) + assert redirected_to(result) == AdminRoutes.setup_path(conn, :configure) + end + + test "redirects to dashboard on successful admin login", %{conn: conn} do + admin = insert(:administrator) + + params = %{ + "authenticate" => %{ + username: admin.user.username, + password: "password" + } + } + + result = post(conn, AdminRoutes.session_path(conn, :create), params) + assert redirected_to(result) == AdminRoutes.dashboard_path(conn, :index) + end + + test "reshows form if params are invalid", %{conn: conn} do + insert(:administrator) + params = %{"authenticate" => %{}} + + result = post(conn, AdminRoutes.session_path(conn, :create), params) + assert html_response(result, 200) =~ "administrator_login" + end + + test "reshows form if credentials are invalid", %{conn: conn} do + admin = insert(:administrator) + + params = %{ + "authenticate" => %{ + username: admin.user.username, + password: "badpassword" + } + } + + result = post(conn, AdminRoutes.session_path(conn, :create), params) + assert html_response(result, 200) =~ "administrator_login" + end + + test "reshows form if user is not an admin", %{conn: conn} do + insert(:administrator) + user = insert(:user) + + params = %{ + "authenticate" => %{ + username: user.username, + password: "password" + } + } + + result = post(conn, AdminRoutes.session_path(conn, :create), params) + assert html_response(result, 200) =~ "administrator_login" + end + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/admin/setup_controller_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/admin/setup_controller_test.exs new file mode 100644 index 0000000..55c4753 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/admin/setup_controller_test.exs @@ -0,0 +1,109 @@ +defmodule BlockScoutWeb.Admin.SetupControllerTest do + use BlockScoutWeb.ConnCase + + alias BlockScoutWeb.Admin.SetupController + alias Explorer.Admin + + setup %{conn: conn} do + conn = + conn + |> bypass_through() + |> get("/") + + {:ok, conn: conn} + end + + describe "configure/2" do + test "redirects to session page if already configured", %{conn: conn} do + insert(:administrator) + result = get(conn, AdminRoutes.setup_path(conn, :configure)) + assert redirected_to(result) == AdminRoutes.session_path(conn, :new) + end + end + + describe "configure/2 with no params" do + test "shows the verification page", %{conn: conn} do + result = get(conn, AdminRoutes.setup_path(conn, :configure)) + assert html_response(result, 200) =~ "administrator_verify" + end + end + + describe "configure/2 with state param" do + test "shows verification page when state is invalid", %{conn: conn} do + result = get(conn, AdminRoutes.setup_path(conn, :configure), %{state: ""}) + assert html_response(result, 200) =~ "administrator_verify" + end + + test "shows registration page when state is valid", %{conn: conn} do + state = SetupController.generate_secure_token() + result = get(conn, AdminRoutes.setup_path(conn, :configure), %{state: state}) + assert html_response(result, 200) =~ "administrator_registration" + end + end + + describe "configure_admin/2" do + test "redirects to session page if already configured", %{conn: conn} do + insert(:administrator) + result = post(conn, AdminRoutes.setup_path(conn, :configure), %{}) + assert redirected_to(result) == AdminRoutes.session_path(conn, :new) + end + end + + describe "configure_admin/2 with no params" do + test "reshows the verification page", %{conn: conn} do + result = post(conn, AdminRoutes.setup_path(conn, :configure_admin), %{}) + assert html_response(result, 200) =~ "administrator_verify" + end + end + + describe "configure_admin/2 with verify param" do + test "redirects with valid recovery key", %{conn: conn} do + key = Admin.recovery_key() + params = %{verify: %{recovery_key: key}} + result = post(conn, AdminRoutes.setup_path(conn, :configure_admin), params) + assert redirected_to(result) =~ AdminRoutes.setup_path(conn, :configure, %{state: ""}) + end + + test "reshows the verification page with invalid key", %{conn: conn} do + params = %{verify: %{recovery_key: "bad_key"}} + result = post(conn, AdminRoutes.setup_path(conn, :configure_admin), params) + assert html_response(result, 200) =~ "administrator_verify" + end + end + + describe "configure_admin with state and registration params" do + setup do + [state: SetupController.generate_secure_token()] + end + + test "reshows the verification page when state is invalid", %{conn: conn} do + params = %{state: "invalid_state", registration: %{}} + result = post(conn, AdminRoutes.setup_path(conn, :configure_admin), params) + assert html_response(result, 200) =~ "administrator_verify" + end + + test "reshows the registration page when registration is invalid", %{conn: conn, state: state} do + params = %{state: state, registration: %{}} + result = post(conn, AdminRoutes.setup_path(conn, :configure_admin), params) + response = html_response(result, 200) + assert response =~ "administrator_registration" + assert response =~ "invalid-feedback" + assert response =~ "is-invalid" + end + + test "redirects to dashboard when state and registration are valid", %{conn: conn, state: state} do + params = %{ + state: state, + registration: %{ + username: "admin_user", + email: "admin_user@blockscout", + password: "testpassword", + password_confirmation: "testpassword" + } + } + + result = post(conn, AdminRoutes.setup_path(conn, :configure_admin), params) + assert redirected_to(result) == AdminRoutes.dashboard_path(conn, :index) + end + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/address_controller_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/address_controller_test.exs new file mode 100644 index 0000000..f04f026 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/address_controller_test.exs @@ -0,0 +1,3553 @@ +defmodule BlockScoutWeb.API.RPC.AddressControllerTest do + use BlockScoutWeb.ConnCase, async: false + + import Mox + + alias BlockScoutWeb.API.RPC.AddressController + alias Explorer.Chain + alias Explorer.Chain.Cache.BackgroundMigrations + alias Explorer.Chain.{Events.Subscriber, Transaction, Wei} + alias Explorer.Chain.Cache.Counters.{AddressesCount, AverageBlockTime} + alias Indexer.Fetcher.OnDemand.CoinBalance, as: CoinBalanceOnDemand + alias Explorer.Repo + + setup :set_mox_global + setup :verify_on_exit! + + setup do + mocked_json_rpc_named_arguments = [ + transport: EthereumJSONRPC.Mox, + transport_options: [] + ] + + start_supervised!({Task.Supervisor, name: Indexer.TaskSupervisor}) + start_supervised!(AverageBlockTime) + + configuration = Application.get_env(:indexer, CoinBalanceOnDemand.Supervisor) + Application.put_env(:indexer, CoinBalanceOnDemand.Supervisor, disabled?: false) + + CoinBalanceOnDemand.Supervisor.Case.start_supervised!(json_rpc_named_arguments: mocked_json_rpc_named_arguments) + + start_supervised!(AddressesCount) + + Application.put_env(:explorer, AverageBlockTime, enabled: true, cache_period: 1_800_000) + + on_exit(fn -> + Application.put_env(:indexer, CoinBalanceOnDemand.Supervisor, configuration) + Application.put_env(:explorer, AverageBlockTime, enabled: false, cache_period: 1_800_000) + end) + + :ok + end + + describe "listaccounts" do + setup do + Subscriber.to(:addresses, :on_demand) + Subscriber.to(:address_coin_balances, :on_demand) + + %{params: %{"module" => "account", "action" => "listaccounts"}} + end + + test "with no addresses", %{params: params, conn: conn} do + response = + conn + |> get("/api", params) + |> json_response(200) + + schema = listaccounts_schema() + assert :ok = ExJsonSchema.Validator.validate(schema, response) + assert response["message"] == "OK" + assert response["status"] == "1" + assert response["result"] == [] + end + + test "with existing addresses", %{params: params, conn: conn} do + first_address = insert(:address, fetched_coin_balance: 10, inserted_at: Timex.shift(Timex.now(), minutes: -10)) + second_address = insert(:address, fetched_coin_balance: 100, inserted_at: Timex.shift(Timex.now(), minutes: -5)) + first_address_hash = to_string(first_address.hash) + second_address_hash = to_string(second_address.hash) + + response = + conn + |> get("/api", params) + |> json_response(200) + + schema = listaccounts_schema() + assert :ok = ExJsonSchema.Validator.validate(schema, response) + assert response["message"] == "OK" + assert response["status"] == "1" + + assert [ + %{ + "address" => ^first_address_hash, + "balance" => "10" + }, + %{ + "address" => ^second_address_hash, + "balance" => "100" + } + ] = response["result"] + end + + test "sort by hash", %{params: params, conn: conn} do + inserted_at = Timex.shift(Timex.now(), minutes: -10) + + first_address = + insert(:address, + hash: "0x0000000000000000000000000000000000000001", + fetched_coin_balance: 10, + inserted_at: inserted_at + ) + + second_address = + insert(:address, + hash: "0x0000000000000000000000000000000000000002", + fetched_coin_balance: 100, + inserted_at: inserted_at + ) + + first_address_hash = to_string(first_address.hash) + second_address_hash = to_string(second_address.hash) + + _first_address_inserted_at = to_string(first_address.inserted_at) + _second_address_inserted_at = to_string(second_address.inserted_at) + + response = + conn + |> get("/api", params) + |> json_response(200) + + schema = listaccounts_schema() + assert :ok = ExJsonSchema.Validator.validate(schema, response) + assert response["message"] == "OK" + assert response["status"] == "1" + + assert [ + %{ + "address" => ^first_address_hash + }, + %{ + "address" => ^second_address_hash + } + ] = response["result"] + end + + test "with a stale balance", %{conn: conn, params: params} do + now = Timex.now() + + mining_address = + insert(:address, + fetched_coin_balance: 0, + fetched_coin_balance_block_number: 103, + inserted_at: Timex.shift(now, minutes: -10) + ) + + mining_address_hash = to_string(mining_address.hash) + # we space these very far apart so that we know it will consider the 0th block stale (it calculates how far + # back we'd need to go to get 24 hours in the past) + Enum.each(0..101, fn i -> + insert(:block, number: i, timestamp: Timex.shift(now, hours: -(103 - i) * 25), miner: mining_address) + end) + + insert(:block, number: 102, timestamp: Timex.shift(now, hours: -25), miner: mining_address) + AverageBlockTime.refresh() + + address = + insert(:address, + fetched_coin_balance: 100, + fetched_coin_balance_block_number: 101, + inserted_at: Timex.shift(now, minutes: -5) + ) + + address_hash = to_string(address.hash) + + expect(EthereumJSONRPC.Mox, :json_rpc, 1, fn [ + %{ + id: id, + method: "eth_getBalance", + params: [^address_hash, "0x66"] + } + ], + _options -> + {:ok, [%{id: id, jsonrpc: "2.0", result: "0x02"}]} + end) + + res = eth_block_number_fake_response("0x66") + + expect(EthereumJSONRPC.Mox, :json_rpc, 1, fn [ + %{ + id: 0, + method: "eth_getBlockByNumber", + params: ["0x66", true] + } + ], + _ -> + {:ok, [res]} + end) + + expect(EthereumJSONRPC.Mox, :json_rpc, 1, fn [ + %{ + id: id, + method: "eth_getBalance", + params: [^mining_address_hash, "0x66"] + } + ], + _options -> + {:ok, [%{id: id, jsonrpc: "2.0", result: "0x02"}]} + end) + + expect(EthereumJSONRPC.Mox, :json_rpc, 1, fn [ + %{ + id: 0, + method: "eth_getBlockByNumber", + params: ["0x66", true] + } + ], + _ -> + {:ok, [res]} + end) + + response = + conn + |> get("/api", params) + |> json_response(200) + + Process.sleep(100) + + schema = listaccounts_schema() + assert :ok = ExJsonSchema.Validator.validate(schema, response) + assert response["message"] == "OK" + assert response["status"] == "1" + + assert [ + %{ + "address" => ^mining_address_hash, + "balance" => "0", + "stale" => false + }, + %{ + "address" => ^address_hash, + "balance" => "100", + "stale" => true + } + ] = response["result"] + + {:ok, expected_wei} = Wei.cast(2) + + assert_receive({:chain_event, :addresses, :on_demand, [received_address]}) + + assert received_address.hash == address.hash + assert received_address.fetched_coin_balance == expected_wei + assert received_address.fetched_coin_balance_block_number == 102 + end + end + + describe "balance" do + test "with missing address hash", %{conn: conn} do + params = %{ + "module" => "account", + "action" => "balance" + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + schema = balance_schema() + assert :ok = ExJsonSchema.Validator.validate(schema, response) + assert response["message"] =~ "'address' is required" + assert response["status"] == "0" + assert Map.has_key?(response, "result") + refute response["result"] + end + + test "with an invalid address hash", %{conn: conn} do + params = %{ + "module" => "account", + "action" => "balance", + "address" => "badhash" + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + schema = balance_schema() + assert :ok = ExJsonSchema.Validator.validate(schema, response) + assert response["message"] =~ "Invalid address hash" + assert response["status"] == "0" + assert Map.has_key?(response, "result") + refute response["result"] + end + + test "with an address that doesn't exist", %{conn: conn} do + params = %{ + "module" => "account", + "action" => "balance", + "address" => "0x8bf38d4764929064f2d4d3a56520a76ab3df415b" + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + schema = balance_schema() + assert :ok = ExJsonSchema.Validator.validate(schema, response) + assert response["result"] == "0" + assert response["status"] == "1" + assert response["message"] == "OK" + end + + test "with a valid address", %{conn: conn} do + address = insert(:address, fetched_coin_balance: 100) + + params = %{ + "module" => "account", + "action" => "balance", + "address" => "#{address.hash}" + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + schema = balance_schema() + assert :ok = ExJsonSchema.Validator.validate(schema, response) + assert response["result"] == "#{address.fetched_coin_balance.value}" + assert response["status"] == "1" + assert response["message"] == "OK" + end + + test "with multiple valid addresses", %{conn: conn} do + addresses = + for _ <- 1..2 do + insert(:address, fetched_coin_balance: Enum.random(1..1_000)) + end + + address_param = + addresses + |> Enum.map(&"#{&1.hash}") + |> Enum.join(",") + + params = %{ + "module" => "account", + "action" => "balance", + "address" => address_param + } + + expected_result = + Enum.map(addresses, fn address -> + %{"account" => "#{address.hash}", "balance" => "#{address.fetched_coin_balance.value}", "stale" => false} + end) + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + schema = balance_schema() + assert :ok = ExJsonSchema.Validator.validate(schema, response) + assert response["result"] == expected_result + assert response["status"] == "1" + assert response["message"] == "OK" + end + + test "supports GET and POST requests", %{conn: conn} do + address = insert(:address, fetched_coin_balance: 100) + + params = %{ + "module" => "account", + "action" => "balance", + "address" => "#{address.hash}" + } + + assert get_response = + conn + |> get("/api", params) + |> json_response(200) + + assert post_response = + conn + |> post("/api", params) + |> json_response(200) + + assert get_response == post_response + end + end + + describe "balancemulti" do + test "with an invalid and a valid address hash", %{conn: conn} do + address1 = "invalidhash" + address2 = "0x9bf49d5875030175f3d5d4a67631a87ab4df526b" + + params = %{ + "module" => "account", + "action" => "balancemulti", + "address" => "#{address1},#{address2}" + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + schema = balance_schema() + assert :ok = ExJsonSchema.Validator.validate(schema, response) + assert response["message"] =~ "Invalid address hash" + assert response["status"] == "0" + assert Map.has_key?(response, "result") + refute response["result"] + end + + test "with multiple addresses that don't exist", %{conn: conn} do + address1 = "0x8bf38d4764929064f2d4d3a56520a76ab3df415b" + address2 = "0x9bf49d5875030175f3d5d4a67631a87ab4df526b" + + params = %{ + "module" => "account", + "action" => "balancemulti", + "address" => "#{address1},#{address2}" + } + + expected_result = [ + %{"account" => address1, "balance" => "0", "stale" => false}, + %{"account" => address2, "balance" => "0", "stale" => false} + ] + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + schema = balance_schema() + assert :ok = ExJsonSchema.Validator.validate(schema, response) + assert :ok = ExJsonSchema.Validator.validate(schema, response) + assert response["result"] == expected_result + assert response["status"] == "1" + assert response["message"] == "OK" + end + + test "with multiple valid addresses", %{conn: conn} do + addresses = + for _ <- 1..4 do + insert(:address, fetched_coin_balance: Enum.random(1..1_000)) + end + + address_param = + addresses + |> Enum.map(&"#{&1.hash}") + |> Enum.join(",") + + params = %{ + "module" => "account", + "action" => "balancemulti", + "address" => address_param + } + + expected_result = + Enum.map(addresses, fn address -> + %{"account" => "#{address.hash}", "balance" => "#{address.fetched_coin_balance.value}", "stale" => false} + end) + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["result"] == expected_result + assert response["status"] == "1" + assert response["message"] == "OK" + + schema = balance_schema() + assert :ok = ExJsonSchema.Validator.validate(schema, response) + end + + test "with an address that exists and one that doesn't", %{conn: conn} do + address1 = insert(:address, fetched_coin_balance: 100) + address2_hash = "0x9bf49d5875030175f3d5d4a67631a87ab4df526b" + + params = %{ + "module" => "account", + "action" => "balancemulti", + "address" => "#{address1.hash},#{address2_hash}" + } + + expected_result = [ + %{"account" => address2_hash, "balance" => "0", "stale" => false}, + %{"account" => "#{address1.hash}", "balance" => "#{address1.fetched_coin_balance.value}", "stale" => false} + ] + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["result"] == expected_result + assert response["status"] == "1" + assert response["message"] == "OK" + + schema = balance_schema() + assert :ok = ExJsonSchema.Validator.validate(schema, response) + end + + test "up to a maximum of 20 addresses in a single request", %{conn: conn} do + addresses = insert_list(25, :address, fetched_coin_balance: 0) + + address_param = + addresses + |> Enum.map(&"#{&1.hash}") + |> Enum.join(",") + + params = %{ + "module" => "account", + "action" => "balancemulti", + "address" => address_param + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert length(response["result"]) == 20 + assert response["status"] == "1" + assert response["message"] == "OK" + + schema = balance_schema() + assert :ok = ExJsonSchema.Validator.validate(schema, response) + end + + test "with a single address", %{conn: conn} do + address = insert(:address, fetched_coin_balance: 100) + + params = %{ + "module" => "account", + "action" => "balancemulti", + "address" => "#{address.hash}" + } + + expected_result = [ + %{"account" => "#{address.hash}", "balance" => "#{address.fetched_coin_balance.value}", "stale" => false} + ] + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["result"] == expected_result + assert response["status"] == "1" + assert response["message"] == "OK" + + schema = balance_schema() + assert :ok = ExJsonSchema.Validator.validate(schema, response) + end + + test "supports GET and POST requests", %{conn: conn} do + addresses = + for _ <- 1..4 do + insert(:address, fetched_coin_balance: Enum.random(1..1_000)) + end + + address_param = + addresses + |> Enum.map(&"#{&1.hash}") + |> Enum.join(",") + + params = %{ + "module" => "account", + "action" => "balancemulti", + "address" => address_param + } + + assert get_response = + conn + |> get("/api", params) + |> json_response(200) + + assert post_response = + conn + |> post("/api", params) + |> json_response(200) + + assert get_response == post_response + end + end + + describe "txlist" do + test "with missing address hash", %{conn: conn} do + params = %{ + "module" => "account", + "action" => "txlist" + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["message"] =~ "'address' is required" + assert response["status"] == "0" + assert Map.has_key?(response, "result") + refute response["result"] + assert :ok = ExJsonSchema.Validator.validate(txlist_schema(), response) + end + + test "with an invalid address hash", %{conn: conn} do + params = %{ + "module" => "account", + "action" => "txlist", + "address" => "badhash" + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["message"] =~ "Invalid address format" + assert response["status"] == "0" + assert Map.has_key?(response, "result") + refute response["result"] + assert :ok = ExJsonSchema.Validator.validate(txlist_schema(), response) + end + + test "with an address that doesn't exist", %{conn: conn} do + params = %{ + "module" => "account", + "action" => "txlist", + "address" => "0x8bf38d4764929064f2d4d3a56520a76ab3df415b" + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["result"] == [] + assert response["status"] == "0" + assert response["message"] == "No transactions found" + assert :ok = ExJsonSchema.Validator.validate(txlist_schema(), response) + end + + test "with a valid address", %{conn: conn} do + address = insert(:address) + + transaction = + %Transaction{block: block} = + :transaction + |> insert(from_address: address) + |> with_block(status: :ok) + + # ^ 'status: :ok' means `isError` in response should be '0' + + params = %{ + "module" => "account", + "action" => "txlist", + "address" => "#{address.hash}" + } + + expected_result = [ + %{ + "blockNumber" => "#{transaction.block_number}", + "timeStamp" => "#{DateTime.to_unix(block.timestamp)}", + "hash" => "#{transaction.hash}", + "nonce" => "#{transaction.nonce}", + "blockHash" => "#{block.hash}", + "transactionIndex" => "#{transaction.index}", + "from" => "#{transaction.from_address_hash}", + "to" => "#{transaction.to_address_hash}", + "value" => "#{transaction.value.value}", + "gas" => "#{transaction.gas}", + "gasPrice" => "#{transaction.gas_price.value}", + "isError" => "0", + "txreceipt_status" => "1", + "input" => "#{transaction.input}", + "contractAddress" => "#{transaction.created_contract_address_hash}", + "cumulativeGasUsed" => "#{transaction.cumulative_gas_used}", + "gasUsed" => "#{transaction.gas_used}", + "confirmations" => "0" + } + ] + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["result"] == expected_result + assert response["status"] == "1" + assert response["message"] == "OK" + assert :ok = ExJsonSchema.Validator.validate(txlist_schema(), response) + end + + test "includes correct confirmations value", %{conn: conn} do + insert(:block) + address = insert(:address) + + transaction = + %Transaction{hash: hash} = + :transaction + |> insert(from_address: address) + |> with_block() + + insert(:block) + + params = %{ + "module" => "account", + "action" => "txlist", + "address" => "#{address.hash}" + } + + block_height = Chain.block_height() + expected_confirmations = block_height - transaction.block_number + + assert %{"result" => [returned_transaction]} = + response = + conn + |> get("/api", params) + |> json_response(200) + + assert returned_transaction["confirmations"] == "#{expected_confirmations}" + assert returned_transaction["hash"] == "#{hash}" + assert :ok = ExJsonSchema.Validator.validate(txlist_schema(), response) + end + + test "returns '1' for 'isError' with failed transaction", %{conn: conn} do + address = insert(:address) + + %Transaction{hash: hash} = + :transaction + |> insert(from_address: address) + |> with_block(status: :error) + + # ^ 'status: :error' means `isError` in response should be '1' + + params = %{ + "module" => "account", + "action" => "txlist", + "address" => "#{address.hash}" + } + + assert %{"result" => [returned_transaction]} = + response = + conn + |> get("/api", params) + |> json_response(200) + + assert returned_transaction["isError"] == "1" + assert returned_transaction["txreceipt_status"] == "0" + assert returned_transaction["hash"] == "#{hash}" + assert :ok = ExJsonSchema.Validator.validate(txlist_schema(), response) + end + + test "with address with multiple transactions", %{conn: conn} do + address1 = insert(:address) + address2 = insert(:address) + + transactions = + 3 + |> insert_list(:transaction, from_address: address1) + |> with_block() + + :transaction + |> insert(from_address: address2) + |> with_block() + + params = %{ + "module" => "account", + "action" => "txlist", + "address" => "#{address1.hash}" + } + + expected_transaction_hashes = Enum.map(transactions, &"#{&1.hash}") + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert length(response["result"]) == 3 + + for returned_transaction <- response["result"] do + assert returned_transaction["hash"] in expected_transaction_hashes + end + + assert response["status"] == "1" + assert response["message"] == "OK" + assert :ok = ExJsonSchema.Validator.validate(txlist_schema(), response) + end + + test "orders transactions by block, in ascending order", %{conn: conn} do + first_block = insert(:block) + second_block = insert(:block) + third_block = insert(:block) + address = insert(:address) + + 2 + |> insert_list(:transaction, from_address: address) + |> with_block(second_block) + + 2 + |> insert_list(:transaction, from_address: address) + |> with_block(third_block) + + 2 + |> insert_list(:transaction, from_address: address) + |> with_block(first_block) + + params = %{ + "module" => "account", + "action" => "txlist", + "address" => "#{address.hash}", + "sort" => "asc" + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + block_numbers_order = + Enum.map(response["result"], fn transaction -> + String.to_integer(transaction["blockNumber"]) + end) + + assert block_numbers_order == Enum.sort(block_numbers_order, &(&1 <= &2)) + assert response["status"] == "1" + assert response["message"] == "OK" + assert :ok = ExJsonSchema.Validator.validate(txlist_schema(), response) + end + + test "orders transactions by block, in descending order", %{conn: conn} do + first_block = insert(:block) + second_block = insert(:block) + third_block = insert(:block) + address = insert(:address) + + 2 + |> insert_list(:transaction, from_address: address) + |> with_block(second_block) + + 2 + |> insert_list(:transaction, from_address: address) + |> with_block(third_block) + + 2 + |> insert_list(:transaction, from_address: address) + |> with_block(first_block) + + params = %{ + "module" => "account", + "action" => "txlist", + "address" => "#{address.hash}", + "sort" => "desc" + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + block_numbers_order = + Enum.map(response["result"], fn transaction -> + String.to_integer(transaction["blockNumber"]) + end) + + assert block_numbers_order == Enum.sort(block_numbers_order, &(&1 >= &2)) + assert response["status"] == "1" + assert response["message"] == "OK" + assert :ok = ExJsonSchema.Validator.validate(txlist_schema(), response) + end + + test "ignores invalid sort option, defaults to ascending", %{conn: conn} do + first_block = insert(:block) + second_block = insert(:block) + third_block = insert(:block) + address = insert(:address) + + 2 + |> insert_list(:transaction, from_address: address) + |> with_block(second_block) + + 2 + |> insert_list(:transaction, from_address: address) + |> with_block(third_block) + + 2 + |> insert_list(:transaction, from_address: address) + |> with_block(first_block) + + params = %{ + "module" => "account", + "action" => "txlist", + "address" => "#{address.hash}", + "sort" => "invalidsortoption" + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + block_numbers_order = + Enum.map(response["result"], fn transaction -> + String.to_integer(transaction["blockNumber"]) + end) + + assert block_numbers_order == Enum.sort(block_numbers_order, &(&1 >= &2)) + assert response["status"] == "1" + assert response["message"] == "OK" + assert :ok = ExJsonSchema.Validator.validate(txlist_schema(), response) + end + + test "with valid pagination params", %{conn: conn} do + # To get paginated results on this endpoint Etherscan's docs say: + # + # "(To get paginated results use page= and offset=)" + + first_block = insert(:block) + second_block = insert(:block) + third_block = insert(:block) + address = insert(:address) + + _second_block_transactions = + 2 + |> insert_list(:transaction, from_address: address) + |> with_block(second_block) + + first_block_transactions = + 2 + |> insert_list(:transaction, from_address: address) + |> with_block(third_block) + + _third_block_transactions = + 2 + |> insert_list(:transaction, from_address: address) + |> with_block(first_block) + + params = %{ + "module" => "account", + "action" => "txlist", + "address" => "#{address.hash}", + # page number + "page" => "1", + # page size + "offset" => "2" + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + page1_hashes = Enum.map(response["result"], & &1["hash"]) + + assert length(response["result"]) == 2 + + for transaction <- first_block_transactions do + assert "#{transaction.hash}" in page1_hashes + end + + assert response["status"] == "1" + assert response["message"] == "OK" + assert :ok = ExJsonSchema.Validator.validate(txlist_schema(), response) + end + + test "ignores pagination params when invalid", %{conn: conn} do + first_block = insert(:block) + second_block = insert(:block) + third_block = insert(:block) + address = insert(:address) + + _second_block_transactions = + 2 + |> insert_list(:transaction, from_address: address) + |> with_block(second_block) + + _third_block_transactions = + 2 + |> insert_list(:transaction, from_address: address) + |> with_block(third_block) + + _first_block_transactions = + 2 + |> insert_list(:transaction, from_address: address) + |> with_block(first_block) + + params = %{ + "module" => "account", + "action" => "txlist", + "address" => "#{address.hash}", + # page number + "page" => "invalidpage", + # page size + "offset" => "invalidoffset" + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert length(response["result"]) == 6 + assert response["status"] == "1" + assert response["message"] == "OK" + assert :ok = ExJsonSchema.Validator.validate(txlist_schema(), response) + end + + test "ignores offset param if offset is less than 1", %{conn: conn} do + address = insert(:address) + + 6 + |> insert_list(:transaction, from_address: address) + |> with_block() + + params = %{ + "module" => "account", + "action" => "txlist", + "address" => "#{address.hash}", + # page number + "page" => "1", + # page size + "offset" => "0" + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert length(response["result"]) == 6 + assert response["status"] == "1" + assert response["message"] == "OK" + assert :ok = ExJsonSchema.Validator.validate(txlist_schema(), response) + end + + test "ignores offset param if offset is over 10,000", %{conn: conn} do + address = insert(:address) + + 6 + |> insert_list(:transaction, from_address: address) + |> with_block() + + params = %{ + "module" => "account", + "action" => "txlist", + "address" => "#{address.hash}", + # page number + "page" => "1", + # page size + "offset" => "10_500" + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert length(response["result"]) == 6 + assert response["status"] == "1" + assert response["message"] == "OK" + assert :ok = ExJsonSchema.Validator.validate(txlist_schema(), response) + end + + test "with page number with no results", %{conn: conn} do + first_block = insert(:block) + second_block = insert(:block) + third_block = insert(:block) + address = insert(:address) + + _second_block_transactions = + 2 + |> insert_list(:transaction, from_address: address) + |> with_block(second_block) + + _third_block_transactions = + 2 + |> insert_list(:transaction, from_address: address) + |> with_block(third_block) + + _first_block_transactions = + 2 + |> insert_list(:transaction, from_address: address) + |> with_block(first_block) + + params = %{ + "module" => "account", + "action" => "txlist", + "address" => "#{address.hash}", + # page number + "page" => "5", + # page size + "offset" => "2" + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["result"] == [] + assert response["status"] == "0" + assert response["message"] == "No transactions found" + assert :ok = ExJsonSchema.Validator.validate(txlist_schema(), response) + end + + test "with startblock and endblock params", %{conn: conn} do + blocks = [_, second_block, third_block, _] = insert_list(4, :block) + address = insert(:address) + + for block <- blocks do + 2 + |> insert_list(:transaction, from_address: address) + |> with_block(block) + end + + params = %{ + "module" => "account", + "action" => "txlist", + "address" => "#{address.hash}", + "startblock" => "#{second_block.number}", + "endblock" => "#{third_block.number}" + } + + expected_block_numbers = [ + "#{second_block.number}", + "#{third_block.number}" + ] + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert length(response["result"]) == 4 + + for transaction <- response["result"] do + assert transaction["blockNumber"] in expected_block_numbers + end + + assert response["status"] == "1" + assert response["message"] == "OK" + assert :ok = ExJsonSchema.Validator.validate(txlist_schema(), response) + end + + test "with startblock but without endblock", %{conn: conn} do + blocks = [_, _, third_block, fourth_block] = insert_list(4, :block) + address = insert(:address) + + for block <- blocks do + 2 + |> insert_list(:transaction, from_address: address) + |> with_block(block) + end + + params = %{ + "module" => "account", + "action" => "txlist", + "address" => "#{address.hash}", + "startblock" => "#{third_block.number}" + } + + expected_block_numbers = [ + "#{third_block.number}", + "#{fourth_block.number}" + ] + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert length(response["result"]) == 4 + + for transaction <- response["result"] do + assert transaction["blockNumber"] in expected_block_numbers + end + + assert response["status"] == "1" + assert response["message"] == "OK" + assert :ok = ExJsonSchema.Validator.validate(txlist_schema(), response) + end + + test "with endblock but without startblock", %{conn: conn} do + blocks = [first_block, second_block, _, _] = insert_list(4, :block) + address = insert(:address) + + for block <- blocks do + 2 + |> insert_list(:transaction, from_address: address) + |> with_block(block) + end + + params = %{ + "module" => "account", + "action" => "txlist", + "address" => "#{address.hash}", + "endblock" => "#{second_block.number}" + } + + expected_block_numbers = [ + "#{first_block.number}", + "#{second_block.number}" + ] + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert length(response["result"]) == 4 + + for transaction <- response["result"] do + assert transaction["blockNumber"] in expected_block_numbers + end + + assert response["status"] == "1" + assert response["message"] == "OK" + assert :ok = ExJsonSchema.Validator.validate(txlist_schema(), response) + end + + test "ignores invalid startblock and endblock", %{conn: conn} do + blocks = [_, _, _, _] = insert_list(4, :block) + address = insert(:address) + + for block <- blocks do + 2 + |> insert_list(:transaction, from_address: address) + |> with_block(block) + end + + params = %{ + "module" => "account", + "action" => "txlist", + "address" => "#{address.hash}", + "startblock" => "invalidstart", + "endblock" => "invalidend" + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert length(response["result"]) == 8 + assert response["status"] == "1" + assert response["message"] == "OK" + assert :ok = ExJsonSchema.Validator.validate(txlist_schema(), response) + end + + test "with start_timestamp and end_timestamp params", %{conn: conn} do + now = Timex.now() + timestamp1 = Timex.shift(now, hours: -6) + timestamp2 = Timex.shift(now, hours: -3) + timestamp3 = Timex.shift(now, hours: -1) + blocks1 = insert_list(2, :block, timestamp: timestamp1) + blocks2 = [third_block, fourth_block] = insert_list(2, :block, timestamp: timestamp2) + blocks3 = insert_list(2, :block, timestamp: timestamp3) + address = insert(:address) + + for block <- Enum.concat([blocks1, blocks2, blocks3]) do + 2 + |> insert_list(:transaction, from_address: address, block_timestamp: block.timestamp) + |> with_block(block) + end + + start_timestamp = now |> Timex.shift(hours: -4) |> Timex.to_unix() + end_timestamp = now |> Timex.shift(hours: -2) |> Timex.to_unix() + + params = %{ + "module" => "account", + "action" => "txlist", + "address" => "#{address.hash}", + "start_timestamp" => "#{start_timestamp}", + "end_timestamp" => "#{end_timestamp}" + } + + expected_block_numbers = [ + "#{third_block.number}", + "#{fourth_block.number}" + ] + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert length(response["result"]) == 4 + + for transaction <- response["result"] do + assert transaction["blockNumber"] in expected_block_numbers + end + + assert response["status"] == "1" + assert response["message"] == "OK" + assert :ok = ExJsonSchema.Validator.validate(txlist_schema(), response) + end + + test "with start_timestamp but without end_timestamp", %{conn: conn} do + now = Timex.now() + timestamp1 = Timex.shift(now, hours: -6) + timestamp2 = Timex.shift(now, hours: -3) + timestamp3 = Timex.shift(now, hours: -1) + blocks1 = insert_list(2, :block, timestamp: timestamp1) + blocks2 = [third_block, fourth_block] = insert_list(2, :block, timestamp: timestamp2) + blocks3 = [fifth_block, sixth_block] = insert_list(2, :block, timestamp: timestamp3) + address = insert(:address) + + for block <- Enum.concat([blocks1, blocks2, blocks3]) do + 2 + |> insert_list(:transaction, from_address: address, block_timestamp: block.timestamp) + |> with_block(block) + end + + start_timestamp = now |> Timex.shift(hours: -4) |> Timex.to_unix() + + params = %{ + "module" => "account", + "action" => "txlist", + "address" => "#{address.hash}", + "start_timestamp" => "#{start_timestamp}" + } + + expected_block_numbers = [ + "#{third_block.number}", + "#{fourth_block.number}", + "#{fifth_block.number}", + "#{sixth_block.number}" + ] + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert length(response["result"]) == 8 + + for transaction <- response["result"] do + assert transaction["blockNumber"] in expected_block_numbers + end + + assert response["status"] == "1" + assert response["message"] == "OK" + assert :ok = ExJsonSchema.Validator.validate(txlist_schema(), response) + end + + test "with end_timestamp but without start_timestamp", %{conn: conn} do + now = Timex.now() + timestamp1 = Timex.shift(now, hours: -6) + timestamp2 = Timex.shift(now, hours: -3) + timestamp3 = Timex.shift(now, hours: -1) + blocks1 = [first_block, second_block] = insert_list(2, :block, timestamp: timestamp1) + blocks2 = insert_list(2, :block, timestamp: timestamp2) + blocks3 = insert_list(2, :block, timestamp: timestamp3) + address = insert(:address) + + for block <- Enum.concat([blocks1, blocks2, blocks3]) do + 2 + |> insert_list(:transaction, from_address: address, block_timestamp: block.timestamp) + |> with_block(block) + end + + end_timestamp = now |> Timex.shift(hours: -5) |> Timex.to_unix() + + params = %{ + "module" => "account", + "action" => "txlist", + "address" => "#{address.hash}", + "end_timestamp" => "#{end_timestamp}" + } + + expected_block_numbers = [ + "#{first_block.number}", + "#{second_block.number}" + ] + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert length(response["result"]) == 4 + + for transaction <- response["result"] do + assert transaction["blockNumber"] in expected_block_numbers + end + + assert response["status"] == "1" + assert response["message"] == "OK" + assert :ok = ExJsonSchema.Validator.validate(txlist_schema(), response) + end + + test "with filter_by=to option", %{conn: conn} do + block = insert(:block) + address = insert(:address) + + insert(:transaction, from_address: address) + |> with_block(block) + + insert(:transaction, to_address: address) + |> with_block(block) + + params = %{ + "module" => "account", + "action" => "txlist", + "address" => "#{address.hash}", + "filter_by" => "to" + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert length(response["result"]) == 1 + assert response["status"] == "1" + assert response["message"] == "OK" + assert :ok = ExJsonSchema.Validator.validate(txlist_schema(), response) + end + + test "with filter_by=from option", %{conn: conn} do + block = insert(:block) + address = insert(:address) + + insert(:transaction, from_address: address) + |> with_block(block) + + insert(:transaction, from_address: address) + |> with_block(block) + + insert(:transaction, to_address: address) + |> with_block(block) + + params = %{ + "module" => "account", + "action" => "txlist", + "address" => "#{address.hash}", + "filter_by" => "from" + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert length(response["result"]) == 2 + assert response["status"] == "1" + assert response["message"] == "OK" + assert :ok = ExJsonSchema.Validator.validate(txlist_schema(), response) + end + + test "supports GET and POST requests", %{conn: conn} do + address = insert(:address) + + :transaction + |> insert(from_address: address) + |> with_block() + + params = %{ + "module" => "account", + "action" => "txlist", + "address" => "#{address.hash}" + } + + assert get_response = + conn + |> get("/api", params) + |> json_response(200) + + assert post_response = + conn + |> post("/api", params) + |> json_response(200) + + assert get_response == post_response + end + end + + describe "pendingtxlist" do + test "with missing address hash", %{conn: conn} do + params = %{ + "module" => "account", + "action" => "pendingtxlist" + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["message"] =~ "'address' is required" + assert response["status"] == "0" + assert Map.has_key?(response, "result") + refute response["result"] + assert :ok = ExJsonSchema.Validator.validate(txlist_schema(), response) + end + + test "with an invalid address hash", %{conn: conn} do + params = %{ + "module" => "account", + "action" => "pendingtxlist", + "address" => "badhash" + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["message"] =~ "Invalid address format" + assert response["status"] == "0" + assert Map.has_key?(response, "result") + refute response["result"] + assert :ok = ExJsonSchema.Validator.validate(txlist_schema(), response) + end + + test "with an address that doesn't exist", %{conn: conn} do + params = %{ + "module" => "account", + "action" => "pendingtxlist", + "address" => "0x8bf38d4764929064f2d4d3a56520a76ab3df415b" + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["result"] == [] + assert response["status"] == "0" + assert response["message"] == "No transactions found" + assert :ok = ExJsonSchema.Validator.validate(txlist_schema(), response) + end + + test "with a valid address", %{conn: conn} do + address = insert(:address) + + transaction = + :transaction + |> insert(from_address: address) + + params = %{ + "module" => "account", + "action" => "pendingtxlist", + "address" => "#{address.hash}" + } + + expected_result = [ + %{ + "hash" => "#{transaction.hash}", + "nonce" => "#{transaction.nonce}", + "from" => "#{transaction.from_address_hash}", + "to" => "#{transaction.to_address_hash}", + "value" => "#{transaction.value.value}", + "gas" => "#{transaction.gas}", + "gasPrice" => "#{transaction.gas_price.value}", + "input" => "#{transaction.input}", + "contractAddress" => "#{transaction.created_contract_address_hash}", + "cumulativeGasUsed" => "#{transaction.cumulative_gas_used}", + "gasUsed" => "#{transaction.gas_used}" + } + ] + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["result"] == expected_result + assert response["status"] == "1" + assert response["message"] == "OK" + assert :ok = ExJsonSchema.Validator.validate(txlist_schema(), response) + end + + test "with address with multiple transactions", %{conn: conn} do + address1 = insert(:address) + address2 = insert(:address) + + transactions = + 3 + |> insert_list(:transaction, from_address: address1) + + :transaction + |> insert(from_address: address2) + + params = %{ + "module" => "account", + "action" => "pendingtxlist", + "address" => "#{address1.hash}" + } + + expected_transaction_hashes = Enum.map(transactions, &"#{&1.hash}") + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert length(response["result"]) == 3 + + for returned_transaction <- response["result"] do + assert returned_transaction["hash"] in expected_transaction_hashes + end + + assert response["status"] == "1" + assert response["message"] == "OK" + assert :ok = ExJsonSchema.Validator.validate(txlist_schema(), response) + end + + test "with valid pagination params", %{conn: conn} do + address = insert(:address) + + _transactions_1 = + 2 + |> insert_list(:transaction, from_address: address) + + _transactions_2 = + 2 + |> insert_list(:transaction, from_address: address) + + transactions_3 = + 2 + |> insert_list(:transaction, from_address: address) + + params = %{ + "module" => "account", + "action" => "pendingtxlist", + "address" => "#{address.hash}", + # page number + "page" => "1", + # page size + "offset" => "2" + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + page1_hashes = Enum.map(response["result"], & &1["hash"]) + + assert length(response["result"]) == 2 + + for transaction <- transactions_3 do + assert "#{transaction.hash}" in page1_hashes + end + + assert response["status"] == "1" + assert response["message"] == "OK" + assert :ok = ExJsonSchema.Validator.validate(txlist_schema(), response) + end + + test "ignores pagination params when invalid", %{conn: conn} do + address = insert(:address) + + _transactions_1 = + 2 + |> insert_list(:transaction, from_address: address) + + _transactions_2 = + 2 + |> insert_list(:transaction, from_address: address) + + _transactions_3 = + 2 + |> insert_list(:transaction, from_address: address) + + params = %{ + "module" => "account", + "action" => "pendingtxlist", + "address" => "#{address.hash}", + # page number + "page" => "invalidpage", + # page size + "offset" => "invalidoffset" + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert length(response["result"]) == 6 + assert response["status"] == "1" + assert response["message"] == "OK" + assert :ok = ExJsonSchema.Validator.validate(txlist_schema(), response) + end + + test "ignores offset param if offset is less than 1", %{conn: conn} do + address = insert(:address) + + 6 + |> insert_list(:transaction, from_address: address) + + params = %{ + "module" => "account", + "action" => "pendingtxlist", + "address" => "#{address.hash}", + # page number + "page" => "1", + # page size + "offset" => "0" + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert length(response["result"]) == 6 + assert response["status"] == "1" + assert response["message"] == "OK" + assert :ok = ExJsonSchema.Validator.validate(txlist_schema(), response) + end + + test "ignores offset param if offset is over 10,000", %{conn: conn} do + address = insert(:address) + + 6 + |> insert_list(:transaction, from_address: address) + + params = %{ + "module" => "account", + "action" => "pendingtxlist", + "address" => "#{address.hash}", + # page number + "page" => "1", + # page size + "offset" => "10_500" + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert length(response["result"]) == 6 + assert response["status"] == "1" + assert response["message"] == "OK" + assert :ok = ExJsonSchema.Validator.validate(txlist_schema(), response) + end + + test "with page number with no results", %{conn: conn} do + address = insert(:address) + + _transactions_1 = + 2 + |> insert_list(:transaction, from_address: address) + + _transactions_2 = + 2 + |> insert_list(:transaction, from_address: address) + + _transactions_3 = + 2 + |> insert_list(:transaction, from_address: address) + + params = %{ + "module" => "account", + "action" => "pendingtxlist", + "address" => "#{address.hash}", + # page number + "page" => "5", + # page size + "offset" => "2" + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["result"] == [] + assert response["status"] == "0" + assert response["message"] == "No transactions found" + assert :ok = ExJsonSchema.Validator.validate(txlist_schema(), response) + end + + test "supports GET and POST requests", %{conn: conn} do + address = insert(:address) + + :transaction + |> insert(from_address: address) + + params = %{ + "module" => "account", + "action" => "pendingtxlist", + "address" => "#{address.hash}" + } + + assert get_response = + conn + |> get("/api", params) + |> json_response(200) + + assert post_response = + conn + |> post("/api", params) + |> json_response(200) + + assert get_response == post_response + end + end + + describe "txlistinternal with no address or transaction hash" do + setup do + params = %{ + "module" => "account", + "action" => "txlistinternal" + } + + {:ok, %{params: params}} + end + + test "returns empty result, if no internal transactions", %{conn: conn, params: params} do + response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["message"] =~ "No internal transactions found" + assert response["status"] == "0" + assert Map.has_key?(response, "result") + assert response["result"] == [] + assert :ok = ExJsonSchema.Validator.validate(txlistinternal_schema(), response) + end + + test "returns internal transaction", %{conn: conn, params: params} do + address = insert(:address) + address_2 = insert(:address) + + block = insert(:block) + + transaction = + :transaction + |> insert(from_address: address, to_address: address_2) + |> with_block(block) + + :internal_transaction + |> insert( + transaction: transaction, + index: 0, + from_address: address, + to_address: address_2, + block_hash: transaction.block_hash, + block_index: 0, + block_number: block.number + ) + + internal_transaction = + :internal_transaction + |> insert( + transaction: transaction, + index: 1, + from_address: address, + to_address: address_2, + block_hash: transaction.block_hash, + block_index: 1, + block_number: block.number + ) + + expected_result = [ + %{ + "blockNumber" => "#{transaction.block_number}", + "timeStamp" => "#{DateTime.to_unix(block.timestamp)}", + "from" => "#{internal_transaction.from_address_hash}", + "to" => "#{internal_transaction.to_address_hash}", + "value" => "#{internal_transaction.value.value}", + "contractAddress" => "", + "input" => "#{internal_transaction.input}", + "type" => "#{internal_transaction.type}", + "callType" => "#{internal_transaction.call_type}", + "gas" => "#{internal_transaction.gas}", + "gasUsed" => "#{internal_transaction.gas_used}", + "index" => "#{internal_transaction.index}", + "transactionHash" => "#{transaction.hash}", + "isError" => "0", + "errCode" => "#{internal_transaction.error}" + } + ] + + assert response = + conn + |> get("/api/v1", params) + |> json_response(200) + + assert response["result"] == expected_result + assert response["status"] == "1" + assert response["message"] == "OK" + assert :ok = ExJsonSchema.Validator.validate(txlistinternal_schema(), response) + end + end + + describe "txlistinternal with txhash" do + test "with an invalid txhash", %{conn: conn} do + params = %{ + "module" => "account", + "action" => "txlistinternal", + "txhash" => "badhash" + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["message"] =~ "Invalid txhash format" + assert response["status"] == "0" + assert Map.has_key?(response, "result") + refute response["result"] + assert :ok = ExJsonSchema.Validator.validate(txlistinternal_schema(), response) + end + + test "with a txhash that doesn't exist", %{conn: conn} do + params = %{ + "module" => "account", + "action" => "txlistinternal", + "txhash" => "0x40eb908387324f2b575b4879cd9d7188f69c8fc9d87c901b9e2daaea4b442170" + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["result"] == [] + assert response["status"] == "0" + assert response["message"] == "No internal transactions found" + assert :ok = ExJsonSchema.Validator.validate(txlistinternal_schema(), response) + end + + test "response includes all the expected fields", %{conn: conn} do + address = insert(:address) + contract_address = insert(:contract_address) + + block = insert(:block) + + transaction = + :transaction + |> insert(from_address: address, to_address: nil) + |> with_contract_creation(contract_address) + |> with_block(block) + + internal_transaction = + :internal_transaction_create + |> insert( + transaction: transaction, + index: 0, + from_address: address, + block_hash: transaction.block_hash, + block_index: 0 + ) + |> with_contract_creation(contract_address) + + params = %{ + "module" => "account", + "action" => "txlistinternal", + "txhash" => "#{transaction.hash}" + } + + expected_result = [ + %{ + "blockNumber" => "#{transaction.block_number}", + "timeStamp" => "#{DateTime.to_unix(block.timestamp)}", + "from" => "#{internal_transaction.from_address_hash}", + "to" => "#{internal_transaction.to_address_hash}", + "value" => "#{internal_transaction.value.value}", + "contractAddress" => "#{contract_address.hash}", + "input" => "", + "type" => "#{internal_transaction.type}", + "callType" => "#{internal_transaction.call_type}", + "gas" => "#{internal_transaction.gas}", + "gasUsed" => "#{internal_transaction.gas_used}", + "index" => "#{internal_transaction.index}", + "transactionHash" => "#{transaction.hash}", + "isError" => "0", + "errCode" => "#{internal_transaction.error}" + } + ] + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["result"] == expected_result + assert response["status"] == "1" + assert response["message"] == "OK" + assert :ok = ExJsonSchema.Validator.validate(txlistinternal_schema(), response) + end + + test "isError is true if internal transaction has an error", %{conn: conn} do + transaction = + :transaction + |> insert() + |> with_block() + + internal_transaction_details = [ + transaction: transaction, + index: 0, + type: :reward, + error: "some error", + block_hash: transaction.block_hash, + block_index: 0 + ] + + insert(:internal_transaction_create, internal_transaction_details) + + params = %{ + "module" => "account", + "action" => "txlistinternal", + "txhash" => "#{transaction.hash}" + } + + assert %{"result" => [found_internal_transaction]} = + response = + conn + |> get("/api", params) + |> json_response(200) + + assert found_internal_transaction["isError"] == "1" + assert response["status"] == "1" + assert response["message"] == "OK" + assert :ok = ExJsonSchema.Validator.validate(txlistinternal_schema(), response) + end + + test "with transaction with multiple internal transactions", %{conn: conn} do + transaction = + :transaction + |> insert() + |> with_block() + + for index <- 0..2 do + insert(:internal_transaction_create, + transaction: transaction, + index: index, + block_hash: transaction.block_hash, + block_index: index + ) + end + + params = %{ + "module" => "account", + "action" => "txlistinternal", + "txhash" => "#{transaction.hash}" + } + + assert %{"result" => found_internal_transactions} = + response = + conn + |> get("/api", params) + |> json_response(200) + + assert length(found_internal_transactions) == 3 + assert response["status"] == "1" + assert response["message"] == "OK" + assert :ok = ExJsonSchema.Validator.validate(txlistinternal_schema(), response) + end + end + + describe "txlistinternal with address" do + test "with an invalid address", %{conn: conn} do + params = %{ + "module" => "account", + "action" => "txlistinternal", + "address" => "badhash" + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["message"] =~ "Invalid address format" + assert response["status"] == "0" + assert Map.has_key?(response, "result") + refute response["result"] + assert :ok = ExJsonSchema.Validator.validate(txlistinternal_schema(), response) + end + + test "with a address that doesn't exist", %{conn: conn} do + params = %{ + "module" => "account", + "action" => "txlistinternal", + "address" => "0x8bf38d4764929064f2d4d3a56520a76ab3df415b" + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["result"] == [] + assert response["status"] == "0" + assert response["message"] == "No internal transactions found" + assert :ok = ExJsonSchema.Validator.validate(txlistinternal_schema(), response) + end + + test "response includes all the expected fields", %{conn: conn} do + address = insert(:address) + contract_address = insert(:contract_address) + + block = insert(:block) + + transaction = + :transaction + |> insert(from_address: address, to_address: nil) + |> with_contract_creation(contract_address) + |> with_block(block) + + internal_transaction = + :internal_transaction_create + |> insert( + transaction: transaction, + index: 0, + from_address: address, + block_number: block.number, + block_hash: transaction.block_hash, + block_index: 0 + ) + |> with_contract_creation(contract_address) + + params = %{ + "module" => "account", + "action" => "txlistinternal", + "address" => "#{address.hash}" + } + + expected_result = [ + %{ + "blockNumber" => "#{transaction.block_number}", + "timeStamp" => "#{DateTime.to_unix(block.timestamp)}", + "from" => "#{internal_transaction.from_address_hash}", + "to" => "#{internal_transaction.to_address_hash}", + "value" => "#{internal_transaction.value.value}", + "contractAddress" => "#{contract_address.hash}", + "input" => "", + "type" => "#{internal_transaction.type}", + "callType" => "#{internal_transaction.call_type}", + "gas" => "#{internal_transaction.gas}", + "gasUsed" => "#{internal_transaction.gas_used}", + "isError" => "0", + "index" => "#{internal_transaction.index}", + "transactionHash" => "#{transaction.hash}", + "errCode" => "#{internal_transaction.error}" + } + ] + + response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["result"] == expected_result + assert response["status"] == "1" + assert response["message"] == "OK" + assert :ok = ExJsonSchema.Validator.validate(txlistinternal_schema(), response) + end + + test "isError is true if internal transaction has an error", %{conn: conn} do + address = insert(:address) + + transaction = + :transaction + |> insert() + |> with_block() + + internal_transaction_details = [ + from_address: address, + transaction: transaction, + index: 0, + type: :reward, + error: "some error", + block_number: transaction.block_number, + block_hash: transaction.block_hash, + block_index: 0 + ] + + insert(:internal_transaction_create, internal_transaction_details) + + params = %{ + "module" => "account", + "action" => "txlistinternal", + "address" => "#{address.hash}" + } + + assert %{"result" => [found_internal_transaction]} = + response = + conn + |> get("/api", params) + |> json_response(200) + + assert found_internal_transaction["isError"] == "1" + assert response["status"] == "1" + assert response["message"] == "OK" + assert :ok = ExJsonSchema.Validator.validate(txlistinternal_schema(), response) + end + + test "with transaction with multiple internal transactions", %{conn: conn} do + address = insert(:address) + + transaction = + :transaction + |> insert() + |> with_block() + + for index <- 0..2 do + internal_transaction_details = %{ + from_address: address, + transaction: transaction, + index: index, + block_number: transaction.block_number, + block_hash: transaction.block_hash, + block_index: index + } + + insert(:internal_transaction_create, internal_transaction_details) + end + + params = %{ + "module" => "account", + "action" => "txlistinternal", + "address" => "#{address.hash}" + } + + assert %{"result" => found_internal_transactions} = + response = + conn + |> get("/api", params) + |> json_response(200) + + assert length(found_internal_transactions) == 3 + assert response["status"] == "1" + assert response["message"] == "OK" + assert :ok = ExJsonSchema.Validator.validate(txlistinternal_schema(), response) + end + end + + describe "tokentx" do + test "with missing address hash", %{conn: conn} do + params = %{ + "module" => "account", + "action" => "tokentx" + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["message"] =~ "address is required" + assert response["status"] == "0" + assert Map.has_key?(response, "result") + refute response["result"] + assert :ok = ExJsonSchema.Validator.validate(tokentx_schema(), response) + end + + test "with an invalid address hash", %{conn: conn} do + params = %{ + "module" => "account", + "action" => "tokentx", + "address" => "badhash" + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["message"] =~ "Invalid address format" + assert response["status"] == "0" + assert Map.has_key?(response, "result") + refute response["result"] + assert :ok = ExJsonSchema.Validator.validate(tokentx_schema(), response) + end + + test "with an address that doesn't exist", %{conn: conn} do + params = %{ + "module" => "account", + "action" => "tokentx", + "address" => "0x8bf38d4764929064f2d4d3a56520a76ab3df415b" + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["result"] == [] + assert response["status"] == "0" + assert response["message"] == "No token transfers found" + assert :ok = ExJsonSchema.Validator.validate(tokentx_schema(), response) + end + + test "has correct value for ERC-721", %{conn: conn} do + transaction = + :transaction + |> insert() + |> with_block() + + token_address = insert(:contract_address) + insert(:token, %{contract_address: token_address, type: "ERC-721"}) + + token_transfer = + insert(:token_transfer, %{ + token_contract_address: token_address, + token_ids: [666], + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number + }) + + {:ok, _} = Chain.token_from_address_hash(token_transfer.token_contract_address_hash) + + params = %{ + "module" => "account", + "action" => "tokentx", + "address" => to_string(token_transfer.from_address.hash) + } + + assert response = + %{"result" => [result]} = + conn + |> get("/api", params) + |> json_response(200) + + assert result["tokenID"] == to_string(List.first(token_transfer.token_ids)) + assert response["status"] == "1" + assert response["message"] == "OK" + assert :ok = ExJsonSchema.Validator.validate(tokentx_schema(), response) + end + + test "returns all the required fields", %{conn: conn} do + transaction = + %{block: block} = + :transaction + |> insert() + |> with_block() + + token_transfer = + insert(:token_transfer, block: transaction.block, transaction: transaction, block_number: block.number) + + {:ok, token} = Chain.token_from_address_hash(token_transfer.token_contract_address_hash) + + params = %{ + "module" => "account", + "action" => "tokentx", + "address" => to_string(token_transfer.from_address.hash) + } + + expected_result = [ + %{ + "blockNumber" => to_string(transaction.block_number), + "timeStamp" => to_string(DateTime.to_unix(block.timestamp)), + "hash" => to_string(token_transfer.transaction_hash), + "nonce" => to_string(transaction.nonce), + "blockHash" => to_string(block.hash), + "from" => to_string(token_transfer.from_address_hash), + "contractAddress" => to_string(token_transfer.token_contract_address_hash), + "to" => to_string(token_transfer.to_address_hash), + "value" => to_string(token_transfer.amount), + "tokenName" => token.name, + "tokenSymbol" => token.symbol, + "tokenDecimal" => to_string(token.decimals), + "transactionIndex" => to_string(transaction.index), + "gas" => to_string(transaction.gas), + "gasPrice" => to_string(transaction.gas_price.value), + "gasUsed" => to_string(transaction.gas_used), + "cumulativeGasUsed" => to_string(transaction.cumulative_gas_used), + "logIndex" => to_string(token_transfer.log_index), + "input" => to_string(transaction.input), + "confirmations" => "0" + } + ] + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["result"] == expected_result + assert response["status"] == "1" + assert response["message"] == "OK" + assert :ok = ExJsonSchema.Validator.validate(tokentx_schema(), response) + end + + test "with an invalid contract address", %{conn: conn} do + params = %{ + "module" => "account", + "action" => "tokentx", + "address" => "0x8bf38d4764929064f2d4d3a56520a76ab3df415b", + "contractaddress" => "invalid" + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["message"] =~ "Invalid contract address format" + assert response["status"] == "0" + assert Map.has_key?(response, "result") + refute response["result"] + assert :ok = ExJsonSchema.Validator.validate(tokentx_schema(), response) + end + + test "filters results by contract address", %{conn: conn} do + address = insert(:address) + + contract_address = insert(:contract_address) + + insert(:token, contract_address: contract_address) + + transaction = + :transaction + |> insert() + |> with_block() + + insert(:token_transfer, + from_address: address, + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number + ) + + insert(:token_transfer, + from_address: address, + token_contract_address: contract_address, + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number + ) + + params = %{ + "module" => "account", + "action" => "tokentx", + "address" => to_string(address.hash), + "contractaddress" => to_string(contract_address.hash) + } + + assert response = + %{"result" => [result]} = + conn + |> get("/api", params) + |> json_response(200) + + assert result["contractAddress"] == to_string(contract_address.hash) + assert response["status"] == "1" + assert response["message"] == "OK" + assert :ok = ExJsonSchema.Validator.validate(tokentx_schema(), response) + end + end + + describe "tokennfttx" do + setup do + %{params: %{"module" => "account", "action" => "tokennfttx"}} + end + + test "API endpoint works after `transactions` table denormalization finished", %{conn: conn, params: params} do + BackgroundMigrations.set_tt_denormalization_finished(true) + + address = insert(:address) + + transaction = + :transaction + |> insert(from_address: address) + |> with_block() + + token = insert(:token, name: "NFT", type: "ERC-721") + + insert(:token_transfer, + transaction: transaction, + from_address: address, + block_number: transaction.block_number, + token_contract_address: token.contract_address, + token_type: token.type, + token_ids: [100_500] + ) + + new_params = + params + |> Map.put("address", Explorer.Chain.Hash.to_string(address.hash)) + + response = + conn + |> get("/api", new_params) + + assert response.status == 200 + BackgroundMigrations.set_tt_denormalization_finished(false) + end + + test "with missing address and contract address hash", %{conn: conn, params: params} do + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["message"] == "Query parameter address or contractaddress is required" + assert response["status"] == "0" + assert Map.has_key?(response, "result") + refute response["result"] + assert :ok = ExJsonSchema.Validator.validate(tokentx_schema(), response) + end + + test "with an invalid address hash", %{conn: conn, params: params} do + params = Map.merge(params, %{"address" => "badhash"}) + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["message"] =~ "Invalid address format" + assert response["status"] == "0" + assert Map.has_key?(response, "result") + refute response["result"] + assert :ok = ExJsonSchema.Validator.validate(tokentx_schema(), response) + end + + test "with an address that doesn't exist", %{conn: conn, params: params} do + params = Map.merge(params, %{"address" => "0x8bf38d4764929064f2d4d3a56520a76ab3df415b"}) + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["result"] == [] + assert response["status"] == "0" + assert response["message"] == "No token transfers found" + assert :ok = ExJsonSchema.Validator.validate(tokentx_schema(), response) + end + + test "has correct value for ERC-721", %{conn: conn, params: params} do + transaction = + :transaction + |> insert() + |> with_block() + + token_address = insert(:contract_address) + insert(:token, %{contract_address: token_address, type: "ERC-721"}) + + token_transfer = + insert(:token_transfer, %{ + token_contract_address: token_address, + token_ids: [666], + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number + }) + + {:ok, _} = Chain.token_from_address_hash(token_transfer.token_contract_address_hash) + + params = Map.merge(params, %{"address" => to_string(token_transfer.from_address.hash)}) + + assert response = + %{"result" => [result]} = + conn + |> get("/api", params) + |> json_response(200) + + assert result["tokenID"] == to_string(List.first(token_transfer.token_ids)) + assert response["status"] == "1" + assert response["message"] == "OK" + assert :ok = ExJsonSchema.Validator.validate(tokentx_schema(), response) + end + + test "returns all the required fields", %{conn: conn, params: params} do + transaction = + %{block: block} = + :transaction + |> insert() + |> with_block() + + token_address = insert(:contract_address) + token = insert(:token, contract_address: token_address, type: "ERC-721") + + token_transfer = + insert(:token_transfer, + block: transaction.block, + transaction: transaction, + block_number: block.number, + token_ids: [1010], + token_contract_address: token_address + ) + + params = Map.merge(params, %{"address" => to_string(token_transfer.from_address.hash)}) + + expected_result = [ + %{ + "blockNumber" => to_string(transaction.block_number), + "timeStamp" => to_string(DateTime.to_unix(block.timestamp)), + "hash" => to_string(token_transfer.transaction_hash), + "nonce" => to_string(transaction.nonce), + "blockHash" => to_string(block.hash), + "from" => to_string(token_transfer.from_address_hash), + "contractAddress" => to_string(token_transfer.token_contract_address_hash), + "to" => to_string(token_transfer.to_address_hash), + "tokenName" => token.name, + "tokenSymbol" => token.symbol, + "tokenDecimal" => to_string(token.decimals), + "transactionIndex" => to_string(transaction.index), + "gas" => to_string(transaction.gas), + "gasPrice" => to_string(transaction.gas_price.value), + "gasUsed" => to_string(transaction.gas_used), + "cumulativeGasUsed" => to_string(transaction.cumulative_gas_used), + "logIndex" => to_string(token_transfer.log_index), + "input" => "deprecated", + "confirmations" => "0", + "tokenID" => "1010" + } + ] + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["result"] == expected_result + assert response["status"] == "1" + assert response["message"] == "OK" + assert :ok = ExJsonSchema.Validator.validate(tokentx_schema(), response) + end + + test "with an invalid contract address", %{conn: conn, params: params} do + params = + Map.merge(params, %{"address" => "0x8bf38d4764929064f2d4d3a56520a76ab3df415b", "contractaddress" => "invalid"}) + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["message"] =~ "Invalid contract address format" + assert response["status"] == "0" + assert Map.has_key?(response, "result") + refute response["result"] + assert :ok = ExJsonSchema.Validator.validate(tokentx_schema(), response) + end + + test "filters results by contract address", %{conn: conn, params: params} do + address = insert(:address) + + contract_address = insert(:contract_address) + + insert(:token, contract_address: contract_address, type: "ERC-721") + + transaction = + :transaction + |> insert() + |> with_block() + + insert(:token_transfer, + from_address: address, + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number + ) + + insert(:token_transfer, + from_address: address, + token_contract_address: contract_address, + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number, + token_ids: [123] + ) + + params = + Map.merge(params, %{"address" => to_string(address.hash), "contractaddress" => to_string(contract_address.hash)}) + + assert response = + %{"result" => [result]} = + conn + |> get("/api", params) + |> json_response(200) + + assert result["contractAddress"] == to_string(contract_address.hash) + assert response["status"] == "1" + assert response["message"] == "OK" + assert :ok = ExJsonSchema.Validator.validate(tokentx_schema(), response) + end + + test "Check pagination and ordering (page, offset, sort parameters)", %{conn: conn, params: params} do + address = insert(:address) + + erc_721_token = insert(:token, type: "ERC-721") + + erc_721_tt = + for x <- 0..50 do + transaction = insert(:transaction, input: "0xabcd010203040506") |> with_block() + + insert(:token_transfer, + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number, + from_address: address, + token_contract_address: erc_721_token.contract_address, + token_ids: [x] + ) + end + + # sort: asc + params = Map.merge(params, %{"address" => to_string(address.hash), "offset" => 25, "page" => 1, "sort" => "asc"}) + + assert %{"result" => token_transfers_1} = + conn + |> get("/api", params) + |> json_response(200) + + params = Map.merge(params, %{"address" => to_string(address.hash), "offset" => 25, "page" => 2, "sort" => "asc"}) + + assert %{"result" => token_transfers_2} = + conn + |> get("/api", params) + |> json_response(200) + + params = Map.merge(params, %{"address" => to_string(address.hash), "offset" => 25, "page" => 3, "sort" => "asc"}) + + assert %{"result" => token_transfers_3} = + conn + |> get("/api", params) + |> json_response(200) + + assert Enum.at(token_transfers_1, 0)["hash"] == to_string(Enum.at(erc_721_tt, 0).transaction_hash) + assert Enum.at(token_transfers_1, 24)["hash"] == to_string(Enum.at(erc_721_tt, 24).transaction_hash) + assert Enum.at(token_transfers_2, 0)["hash"] == to_string(Enum.at(erc_721_tt, 25).transaction_hash) + assert Enum.at(token_transfers_2, 24)["hash"] == to_string(Enum.at(erc_721_tt, 49).transaction_hash) + assert Enum.at(token_transfers_3, 0)["hash"] == to_string(Enum.at(erc_721_tt, 50).transaction_hash) + assert Enum.count(token_transfers_3) == 1 + + # sort: desc + erc_721_tt_reversed = Enum.reverse(erc_721_tt) + + params = Map.merge(params, %{"address" => to_string(address.hash), "offset" => 25, "page" => 1, "sort" => "desc"}) + + assert %{"result" => token_transfers_1} = + conn + |> get("/api", params) + |> json_response(200) + + params = Map.merge(params, %{"address" => to_string(address.hash), "offset" => 25, "page" => 2, "sort" => "desc"}) + + assert %{"result" => token_transfers_2} = + conn + |> get("/api", params) + |> json_response(200) + + params = Map.merge(params, %{"address" => to_string(address.hash), "offset" => 25, "page" => 3, "sort" => "desc"}) + + assert %{"result" => token_transfers_3} = + conn + |> get("/api", params) + |> json_response(200) + + assert Enum.at(token_transfers_1, 0)["hash"] == to_string(Enum.at(erc_721_tt_reversed, 0).transaction_hash) + assert Enum.at(token_transfers_1, 24)["hash"] == to_string(Enum.at(erc_721_tt_reversed, 24).transaction_hash) + assert Enum.at(token_transfers_2, 0)["hash"] == to_string(Enum.at(erc_721_tt_reversed, 25).transaction_hash) + assert Enum.at(token_transfers_2, 24)["hash"] == to_string(Enum.at(erc_721_tt_reversed, 49).transaction_hash) + assert Enum.at(token_transfers_3, 0)["hash"] == to_string(Enum.at(erc_721_tt_reversed, 50).transaction_hash) + assert Enum.count(token_transfers_3) == 1 + end + end + + describe "tokenbalance" do + test "without required params", %{conn: conn} do + params = %{ + "module" => "account", + "action" => "tokenbalance" + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["message"] =~ "missing: address, contractaddress" + assert response["status"] == "0" + assert Map.has_key?(response, "result") + refute response["result"] + assert :ok = ExJsonSchema.Validator.validate(tokenbalance_schema(), response) + end + + test "with contract address but without address", %{conn: conn} do + params = %{ + "module" => "account", + "action" => "tokenbalance", + "contractaddress" => "0x8bf38d4764929064f2d4d3a56520a76ab3df415b" + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["message"] =~ "missing: address" + assert response["status"] == "0" + assert Map.has_key?(response, "result") + refute response["result"] + assert :ok = ExJsonSchema.Validator.validate(tokenbalance_schema(), response) + end + + test "with address but without contract address", %{conn: conn} do + params = %{ + "module" => "account", + "action" => "tokenbalance", + "address" => "0x8bf38d4764929064f2d4d3a56520a76ab3df415b" + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["message"] =~ "missing: contractaddress" + assert response["status"] == "0" + assert Map.has_key?(response, "result") + refute response["result"] + assert :ok = ExJsonSchema.Validator.validate(tokenbalance_schema(), response) + end + + test "with an invalid contract address hash", %{conn: conn} do + params = %{ + "module" => "account", + "action" => "tokenbalance", + "contractaddress" => "badhash", + "address" => "badhash" + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["message"] =~ "Invalid contractaddress format" + assert response["status"] == "0" + assert Map.has_key?(response, "result") + refute response["result"] + assert :ok = ExJsonSchema.Validator.validate(tokenbalance_schema(), response) + end + + test "with an invalid address hash", %{conn: conn} do + params = %{ + "module" => "account", + "action" => "tokenbalance", + "contractaddress" => "0x8bf38d4764929064f2d4d3a56520a76ab3df415b", + "address" => "badhash" + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["message"] =~ "Invalid address format" + assert response["status"] == "0" + assert Map.has_key?(response, "result") + refute response["result"] + assert :ok = ExJsonSchema.Validator.validate(tokenbalance_schema(), response) + end + + test "with a contract address and address that doesn't exist", %{conn: conn} do + params = %{ + "module" => "account", + "action" => "tokenbalance", + "contractaddress" => "0x8bf38d4764929064f2d4d3a56520a76ab3df415b", + "address" => "0x9bf38d4764929064f2d4d3a56520a76ab3df415b" + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["result"] == "0" + assert response["status"] == "1" + assert response["message"] == "OK" + assert :ok = ExJsonSchema.Validator.validate(tokenbalance_schema(), response) + end + + test "with contract address and address without row in token_balances table", %{conn: conn} do + token = insert(:token) + address = insert(:address) + + params = %{ + "module" => "account", + "action" => "tokenbalance", + "contractaddress" => to_string(token.contract_address_hash), + "address" => to_string(address.hash) + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["result"] == "0" + assert response["status"] == "1" + assert response["message"] == "OK" + assert :ok = ExJsonSchema.Validator.validate(tokenbalance_schema(), response) + end + + test "with contract address and address with existing balance in token_balances table", %{conn: conn} do + current_token_balance = insert(:address_current_token_balance) + + params = %{ + "module" => "account", + "action" => "tokenbalance", + "contractaddress" => to_string(current_token_balance.token_contract_address_hash), + "address" => to_string(current_token_balance.address_hash) + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["result"] == to_string(current_token_balance.value) + assert response["status"] == "1" + assert response["message"] == "OK" + assert :ok = ExJsonSchema.Validator.validate(tokenbalance_schema(), response) + end + end + + describe "tokenlist" do + test "without address param", %{conn: conn} do + params = %{ + "module" => "account", + "action" => "tokenlist" + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["message"] =~ "address is required" + assert response["status"] == "0" + assert Map.has_key?(response, "result") + refute response["result"] + assert :ok = ExJsonSchema.Validator.validate(tokenlist_schema(), response) + end + + test "with an invalid address hash", %{conn: conn} do + params = %{ + "module" => "account", + "action" => "tokenlist", + "address" => "badhash" + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["message"] =~ "Invalid address format" + assert response["status"] == "0" + assert Map.has_key?(response, "result") + refute response["result"] + assert :ok = ExJsonSchema.Validator.validate(tokenlist_schema(), response) + end + + test "with an address that doesn't exist", %{conn: conn} do + params = %{ + "module" => "account", + "action" => "tokenlist", + "address" => "0x9bf38d4764929064f2d4d3a56520a76ab3df415b" + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["result"] == [] + assert response["status"] == "0" + assert response["message"] == "No tokens found" + assert :ok = ExJsonSchema.Validator.validate(tokenlist_schema(), response) + end + + test "with an address without row in token_balances table", %{conn: conn} do + address = insert(:address) + + params = %{ + "module" => "account", + "action" => "tokenlist", + "address" => to_string(address.hash) + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["result"] == [] + assert response["status"] == "0" + assert response["message"] == "No tokens found" + assert :ok = ExJsonSchema.Validator.validate(tokenlist_schema(), response) + end + + test "with address with existing balance in token_balances table", %{conn: conn} do + token_balance = :address_current_token_balance |> insert() |> Repo.preload(:token) + + params = %{ + "module" => "account", + "action" => "tokenlist", + "address" => to_string(token_balance.address_hash) + } + + expected_result = [ + %{ + "balance" => to_string(token_balance.value), + "contractAddress" => to_string(token_balance.token_contract_address_hash), + "name" => token_balance.token.name, + "decimals" => to_string(token_balance.token.decimals), + "symbol" => token_balance.token.symbol, + "type" => token_balance.token.type + } + ] + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["result"] == expected_result + assert response["status"] == "1" + assert response["message"] == "OK" + assert :ok = ExJsonSchema.Validator.validate(tokenlist_schema(), response) + end + + test "with address with multiple tokens", %{conn: conn} do + address = insert(:address) + other_address = insert(:address) + insert(:address_current_token_balance, address: address) + insert(:address_current_token_balance, address: address) + insert(:address_current_token_balance, address: other_address) + + params = %{ + "module" => "account", + "action" => "tokenlist", + "address" => to_string(address.hash) + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert length(response["result"]) == 2 + assert response["status"] == "1" + assert response["message"] == "OK" + assert :ok = ExJsonSchema.Validator.validate(tokenlist_schema(), response) + end + end + + describe "getminedblocks" do + test "with missing address hash", %{conn: conn} do + params = %{ + "module" => "account", + "action" => "getminedblocks" + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["message"] =~ "'address' is required" + assert response["status"] == "0" + assert Map.has_key?(response, "result") + refute response["result"] + assert :ok = ExJsonSchema.Validator.validate(block_schema(), response) + end + + test "with an invalid address hash", %{conn: conn} do + params = %{ + "module" => "account", + "action" => "getminedblocks", + "address" => "badhash" + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["message"] =~ "Invalid address format" + assert response["status"] == "0" + assert Map.has_key?(response, "result") + refute response["result"] + assert :ok = ExJsonSchema.Validator.validate(block_schema(), response) + end + + test "with an address that doesn't exist", %{conn: conn} do + params = %{ + "module" => "account", + "action" => "getminedblocks", + "address" => "0x8bf38d4764929064f2d4d3a56520a76ab3df415b" + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["result"] == [] + assert response["status"] == "0" + assert response["message"] == "No blocks found" + assert :ok = ExJsonSchema.Validator.validate(block_schema(), response) + end + + test "returns all the required fields", %{conn: conn} do + %{block_range: range} = insert(:emission_reward) + + block = insert(:block, number: Enum.random(Range.new(range.from, range.to))) + + :transaction + |> insert(gas_price: 1) + |> with_block(block, gas_used: 1) + + expected_result = [ + %{ + "blockNumber" => to_string(block.number), + "timeStamp" => to_string(block.timestamp) + } + ] + + params = %{ + "module" => "account", + "action" => "getminedblocks", + "address" => to_string(block.miner_hash) + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["result"] == expected_result + assert response["status"] == "1" + assert response["message"] == "OK" + assert :ok = ExJsonSchema.Validator.validate(block_schema(), response) + end + + test "with a block with one transaction", %{conn: conn} do + %{block_range: range} = insert(:emission_reward) + + block = insert(:block, number: Enum.random(Range.new(range.from, range.to))) + + :transaction + |> insert(gas_price: 1) + |> with_block(block, gas_used: 1) + + params = %{ + "module" => "account", + "action" => "getminedblocks", + "address" => to_string(block.miner_hash) + } + + expected_result = [ + %{ + "blockNumber" => to_string(block.number), + "timeStamp" => to_string(block.timestamp) + } + ] + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["result"] == expected_result + assert response["status"] == "1" + assert response["message"] == "OK" + assert :ok = ExJsonSchema.Validator.validate(block_schema(), response) + end + + test "with pagination options", %{conn: conn} do + %{block_range: range} = insert(:emission_reward) + + block_numbers = Range.new(range.from, range.to) + + [block_number1, block_number2] = Enum.take(block_numbers, 2) + + address = insert(:address) + + _block1 = insert(:block, number: block_number1, miner: address) + block2 = insert(:block, number: block_number2, miner: address) + + :transaction + |> insert(gas_price: 2) + |> with_block(block2, gas_used: 2) + + params = %{ + "module" => "account", + "action" => "getminedblocks", + "address" => to_string(address.hash), + # page number + "page" => "1", + # page size + "offset" => "1" + } + + expected_result = [ + %{ + "blockNumber" => to_string(block2.number), + "timeStamp" => to_string(block2.timestamp) + } + ] + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["result"] == expected_result + assert response["status"] == "1" + assert response["message"] == "OK" + assert :ok = ExJsonSchema.Validator.validate(block_schema(), response) + end + end + + describe "optional_params/1" do + test "includes valid optional params in the required format" do + params = %{ + "startblock" => "100", + "endblock" => "120", + "sort" => "asc", + # page number + "page" => "1", + # page size + "offset" => "2", + "filter_by" => "to", + "start_timestamp" => "1539186474", + "end_timestamp" => "1539186474" + } + + optional_params = AddressController.optional_params(params) + + # 1539186474 equals "2018-10-10 15:47:54Z" + {:ok, expected_timestamp, _} = DateTime.from_iso8601("2018-10-10 15:47:54Z") + + assert optional_params.page_number == 1 + assert optional_params.page_size == 2 + assert optional_params.order_by_direction == :asc + assert optional_params.startblock == 100 + assert optional_params.endblock == 120 + assert optional_params.filter_by == "to" + assert optional_params.start_timestamp == expected_timestamp + assert optional_params.end_timestamp == expected_timestamp + end + + test "'sort' values can be 'asc' or 'desc'" do + params1 = %{"sort" => "asc"} + + optional_params = AddressController.optional_params(params1) + + assert optional_params.order_by_direction == :asc + + params2 = %{"sort" => "desc"} + + optional_params = AddressController.optional_params(params2) + + assert optional_params.order_by_direction == :desc + + params3 = %{"sort" => "invalid"} + + assert AddressController.optional_params(params3) == %{} + end + + test "'filter_by' value can be 'to' or 'from'" do + params1 = %{"filter_by" => "to"} + + optional_params1 = AddressController.optional_params(params1) + + assert optional_params1.filter_by == "to" + + params2 = %{"filter_by" => "from"} + + optional_params2 = AddressController.optional_params(params2) + + assert optional_params2.filter_by == "from" + + params3 = %{"filter_by" => "invalid"} + + assert AddressController.optional_params(params3) == %{} + end + + test "only includes optional params when they're given" do + assert AddressController.optional_params(%{}) == %{} + end + + test "ignores invalid optional params, keeps valid ones" do + params1 = %{ + "startblock" => "invalid", + "endblock" => "invalid", + "sort" => "invalid", + "page" => "invalid", + "offset" => "invalid", + "start_timestamp" => "invalid", + "end_timestamp" => "invalid" + } + + assert AddressController.optional_params(params1) == %{} + + params2 = %{ + "startblock" => "4", + "endblock" => "10", + "sort" => "invalid", + "page" => "invalid", + "offset" => "invalid", + "start_timestamp" => "invalid", + "end_timestamp" => "invalid" + } + + optional_params = AddressController.optional_params(params2) + + assert optional_params.startblock == 4 + assert optional_params.endblock == 10 + end + + test "ignores 'page' if less than 1" do + params = %{"page" => "0"} + + assert AddressController.optional_params(params) == %{} + end + + test "ignores 'offset' if less than 1" do + params = %{"offset" => "0"} + + assert AddressController.optional_params(params) == %{} + end + + test "ignores 'offset' if more than 10,000" do + params = %{"offset" => "10001"} + + assert AddressController.optional_params(params) == %{} + end + end + + describe "fetch_required_params/2" do + test "returns error with missing param" do + params = %{"address" => "some address"} + + required_params = ~w(address contractaddress) + + result = AddressController.fetch_required_params(params, required_params) + + assert result == {:required_params, {:error, ["contractaddress"]}} + end + + test "returns ok with all required params" do + params = %{"address" => "some address", "contractaddress" => "some contract"} + + required_params = ~w(address contractaddress) + + result = AddressController.fetch_required_params(params, required_params) + + assert result == {:required_params, {:ok, params}} + end + end + + defp listaccounts_schema do + resolve_schema(%{ + "type" => "array", + "items" => %{ + "type" => "object", + "properties" => %{ + "address" => %{"type" => "string"}, + "balance" => %{"type" => "string"}, + "stale" => %{"type" => "boolean"} + } + } + }) + end + + defp balance_schema do + resolve_schema(%{ + "type" => ["string", "null", "array"], + "items" => %{ + "type" => "object", + "properties" => %{ + "account" => %{"type" => "string"}, + "balance" => %{"type" => "string"}, + "stale" => %{"type" => "boolean"} + } + } + }) + end + + defp txlist_schema do + resolve_schema(%{ + "type" => ["null", "array"], + "items" => %{ + "type" => "object", + "properties" => %{ + "blockNumber" => %{"type" => "string"}, + "timeStamp" => %{"type" => "string"}, + "hash" => %{"type" => "string"}, + "nonce" => %{"type" => "string"}, + "blockHash" => %{"type" => "string"}, + "transactionIndex" => %{"type" => "string"}, + "from" => %{"type" => "string"}, + "to" => %{"type" => "string"}, + "value" => %{"type" => "string"}, + "gas" => %{"type" => "string"}, + "gasPrice" => %{"type" => "string"}, + "isError" => %{"type" => "string"}, + "txreceipt_status" => %{"type" => "string"}, + "input" => %{"type" => "string"}, + "contractAddress" => %{"type" => "string"}, + "cumulativeGasUsed" => %{"type" => "string"}, + "gasUsed" => %{"type" => "string"}, + "confirmations" => %{"type" => "string"} + } + } + }) + end + + defp txlistinternal_schema do + resolve_schema(%{ + "type" => ["array", "null"], + "items" => %{ + "type" => "object", + "properties" => %{ + "blockNumber" => %{"type" => "string"}, + "timeStamp" => %{"type" => "string"}, + "from" => %{"type" => "string"}, + "to" => %{"type" => "string"}, + "value" => %{"type" => "string"}, + "contractAddress" => %{"type" => "string"}, + "transactionHash" => %{"type" => "string"}, + "index" => %{"type" => "string"}, + "input" => %{"type" => "string"}, + "type" => %{"type" => "string"}, + "gas" => %{"type" => "string"}, + "gasUsed" => %{"type" => "string"}, + "isError" => %{"type" => "string"}, + "errCode" => %{"type" => "string"} + } + } + }) + end + + defp tokentx_schema do + resolve_schema(%{ + "type" => ["array", "null"], + "items" => %{ + "type" => "object", + "properties" => %{ + "blockNumber" => %{"type" => "string"}, + "timeStamp" => %{"type" => "string"}, + "hash" => %{"type" => "string"}, + "nonce" => %{"type" => "string"}, + "blockHash" => %{"type" => "string"}, + "from" => %{"type" => "string"}, + "contractAddress" => %{"type" => "string"}, + "to" => %{"type" => "string"}, + "logIndex" => %{"type" => "string"}, + "value" => %{"type" => "string"}, + "tokenName" => %{"type" => "string"}, + "tokenID" => %{"type" => "string"}, + "tokenSymbol" => %{"type" => "string"}, + "tokenDecimal" => %{"type" => "string"}, + "transactionIndex" => %{"type" => "string"}, + "gas" => %{"type" => "string"}, + "gasPrice" => %{"type" => "string"}, + "gasUsed" => %{"type" => "string"}, + "cumulativeGasUsed" => %{"type" => "string"}, + "input" => %{"type" => "string"}, + "confirmations" => %{"type" => "string"} + } + } + }) + end + + defp tokenbalance_schema, do: resolve_schema(%{"type" => ["string", "null"]}) + + defp tokenlist_schema do + resolve_schema(%{ + "type" => ["array", "null"], + "items" => %{ + "type" => "object", + "properties" => %{ + "balance" => %{"type" => "string"}, + "contractAddress" => %{"type" => "string"}, + "name" => %{"type" => "string"}, + "decimals" => %{"type" => "string"}, + "symbol" => %{"type" => "string"}, + "type" => %{"type" => "string"} + } + } + }) + end + + defp block_schema do + resolve_schema(%{ + "type" => ["array", "null"], + "items" => %{ + "type" => "object", + "properties" => %{ + "blockNumber" => %{"type" => "string"}, + "timeStamp" => %{"type" => "string"}, + "blockReward" => %{"type" => "string"} + } + } + }) + end + + defp resolve_schema(result) do + %{ + "type" => "object", + "properties" => %{ + "message" => %{"type" => "string"}, + "status" => %{"type" => "string"} + } + } + |> put_in(["properties", "result"], result) + |> ExJsonSchema.Schema.resolve() + end + + defp eth_block_number_fake_response(block_quantity) do + %{ + id: 0, + jsonrpc: "2.0", + result: %{ + "author" => "0x0000000000000000000000000000000000000000", + "difficulty" => "0x20000", + "extraData" => "0x", + "gasLimit" => "0x663be0", + "gasUsed" => "0x0", + "hash" => "0x5b28c1bfd3a15230c9a46b399cd0f9a6920d432e85381cc6a140b06e8410112f", + "logsBloom" => + "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "miner" => "0x0000000000000000000000000000000000000000", + "number" => block_quantity, + "parentHash" => "0x0000000000000000000000000000000000000000000000000000000000000000", + "receiptsRoot" => "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", + "sealFields" => [ + "0x80", + "0xb8410000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + ], + "sha3Uncles" => "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", + "signature" => + "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "size" => "0x215", + "stateRoot" => "0xfad4af258fd11939fae0c6c6eec9d340b1caac0b0196fd9a1bc3f489c5bf00b3", + "step" => "0", + "timestamp" => "0x0", + "totalDifficulty" => "0x20000", + "transactions" => [], + "transactionsRoot" => "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", + "uncles" => [] + } + } + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/block_controller_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/block_controller_test.exs new file mode 100644 index 0000000..9ee058f --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/block_controller_test.exs @@ -0,0 +1,430 @@ +defmodule BlockScoutWeb.API.RPC.BlockControllerTest do + use BlockScoutWeb.ConnCase + + alias BlockScoutWeb.Chain + alias Explorer.Chain.{Hash, Wei} + alias Explorer.Chain.Cache.Counters.AverageBlockTime + + describe "getblockreward" do + test "with missing block number", %{conn: conn} do + response = + conn + |> get("/api", %{"module" => "block", "action" => "getblockreward"}) + |> json_response(200) + + assert response["message"] =~ "'blockno' is required" + assert response["status"] == "0" + assert Map.has_key?(response, "result") + refute response["result"] + schema = resolve_getblockreward_schema() + assert :ok = ExJsonSchema.Validator.validate(schema, response) + end + + test "with an invalid block number", %{conn: conn} do + response = + conn + |> get("/api", %{"module" => "block", "action" => "getblockreward", "blockno" => "badnumber"}) + |> json_response(200) + + assert response["message"] =~ "Invalid block number" + assert response["status"] == "0" + assert Map.has_key?(response, "result") + refute response["result"] + schema = resolve_getblockreward_schema() + assert :ok = ExJsonSchema.Validator.validate(schema, response) + end + + test "with a block that doesn't exist", %{conn: conn} do + response = + conn + |> get("/api", %{"module" => "block", "action" => "getblockreward", "blockno" => "42"}) + |> json_response(200) + + assert response["message"] =~ "Block does not exist" + assert response["status"] == "0" + assert Map.has_key?(response, "result") + refute response["result"] + schema = resolve_getblockreward_schema() + assert :ok = ExJsonSchema.Validator.validate(schema, response) + end + + test "with a valid block", %{conn: conn} do + %{block_range: range} = emission_reward = insert(:emission_reward) + block = insert(:block, number: Enum.random(Range.new(range.from, range.to))) + + :transaction + |> insert(gas_price: 1) + |> with_block(block, gas_used: 1) + + expected_reward = + emission_reward.reward + |> Wei.to(:wei) + |> Decimal.add(Decimal.new(1)) + + insert(:reward, address_hash: block.miner_hash, block_hash: block.hash, reward: expected_reward) + + expected_result = %{ + "blockNumber" => "#{block.number}", + "timeStamp" => DateTime.to_unix(block.timestamp), + "blockMiner" => Hash.to_string(block.miner_hash), + "blockReward" => expected_reward |> Decimal.to_string(:normal), + "uncles" => [], + "uncleInclusionReward" => "0" + } + + assert response = + conn + |> get("/api", %{"module" => "block", "action" => "getblockreward", "blockno" => "#{block.number}"}) + |> json_response(200) + + assert response["result"] == expected_result + assert response["status"] == "1" + assert response["message"] == "OK" + schema = resolve_getblockreward_schema() + assert :ok = ExJsonSchema.Validator.validate(schema, response) + end + + test "with a valid block and uncles", %{conn: conn} do + %{block_range: range} = emission_reward = insert(:emission_reward) + block = insert(:block, number: Enum.random(Range.new(range.from + 2, range.to))) + uncle1 = insert(:block, number: block.number - 1) + uncle2 = insert(:block, number: block.number - 2) + + insert(:block_second_degree_relation, nephew: block, uncle_hash: uncle1.hash, index: 0) + insert(:block_second_degree_relation, nephew: block, uncle_hash: uncle2.hash, index: 1) + + :transaction + |> insert(gas_price: 1) + |> with_block(block, gas_used: 1) + + decimal_emission_reward = Wei.to(emission_reward.reward, :wei) + + uncle1_reward = + decimal_emission_reward |> Decimal.div(8) |> Decimal.mult(Decimal.new(uncle1.number + 8 - block.number)) + + uncle2_reward = + decimal_emission_reward |> Decimal.div(8) |> Decimal.mult(Decimal.new(uncle2.number + 8 - block.number)) + + uncle_inclusion_reward = + decimal_emission_reward + |> Decimal.div(Decimal.new(32)) + |> Decimal.mult(Decimal.new(2)) + + block_reward = + decimal_emission_reward + |> Decimal.add(Decimal.new(1)) + |> Decimal.add(uncle_inclusion_reward) + + insert(:reward, address_hash: block.miner_hash, block_hash: block.hash, reward: block_reward) + + insert(:reward, + address_hash: uncle1.miner_hash, + block_hash: block.hash, + reward: uncle1_reward, + address_type: :uncle + ) + + insert(:reward, + address_hash: uncle2.miner_hash, + block_hash: block.hash, + reward: uncle2_reward, + address_type: :uncle + ) + + expected_result = %{ + "blockNumber" => "#{block.number}", + "timeStamp" => DateTime.to_unix(block.timestamp), + "blockMiner" => Hash.to_string(block.miner_hash), + "blockReward" => block_reward |> Decimal.to_string(:normal), + "uncles" => [ + %{ + "blockreward" => uncle1_reward |> Decimal.to_string(:normal), + "miner" => uncle1.miner_hash |> Hash.to_string(), + "unclePosition" => "0" + }, + %{ + "blockreward" => uncle2_reward |> Decimal.to_string(:normal), + "miner" => uncle2.miner_hash |> Hash.to_string(), + "unclePosition" => "1" + } + ], + "uncleInclusionReward" => "0" + } + + assert response = + conn + |> get("/api", %{"module" => "block", "action" => "getblockreward", "blockno" => "#{block.number}"}) + |> json_response(200) + + assert response["result"] == expected_result + assert response["status"] == "1" + assert response["message"] == "OK" + schema = resolve_getblockreward_schema() + assert :ok = ExJsonSchema.Validator.validate(schema, response) + end + end + + describe "getblockcountdown" do + setup do + start_supervised!(AverageBlockTime) + Application.put_env(:explorer, AverageBlockTime, enabled: true, cache_period: 1_800_000) + + on_exit(fn -> + Application.put_env(:explorer, AverageBlockTime, enabled: false, cache_period: 1_800_000) + end) + end + + test "returns countdown information when valid block number is provided", %{conn: conn} do + unsafe_target_block_number = "120" + current_block_number = 110 + average_block_time = 15 + remaining_blocks = 10 + + first_timestamp = Timex.now() + + for i <- 1..current_block_number do + insert(:block, number: i, timestamp: Timex.shift(first_timestamp, seconds: i * average_block_time)) + end + + AverageBlockTime.refresh() + + estimated_time_in_sec = Float.round(remaining_blocks * average_block_time * 1.0, 1) + + expected_result = %{ + "CurrentBlock" => "#{current_block_number}", + "CountdownBlock" => unsafe_target_block_number, + "RemainingBlock" => "#{remaining_blocks}", + "EstimateTimeInSec" => "#{estimated_time_in_sec}" + } + + response = + conn + |> get("/api", %{ + "module" => "block", + "action" => "getblockcountdown", + "blockno" => unsafe_target_block_number + }) + |> json_response(200) + + assert response["result"] == expected_result + assert response["status"] == "1" + assert response["message"] == "OK" + schema = resolve_getblockcountdown_schema() + assert :ok = ExJsonSchema.Validator.validate(schema, response) + end + end + + describe "getblocknobytime" do + test "with missing timestamp param", %{conn: conn} do + response = + conn + |> get("/api", %{"module" => "block", "action" => "getblocknobytime", "closest" => "after"}) + |> json_response(200) + + assert response["message"] =~ "Query parameter 'timestamp' is required" + assert response["status"] == "0" + assert Map.has_key?(response, "result") + refute response["result"] + schema = resolve_getblockreward_schema() + assert :ok = ExJsonSchema.Validator.validate(schema, response) + end + + test "with missing closest param", %{conn: conn} do + response = + conn + |> get("/api", %{"module" => "block", "action" => "getblocknobytime", "timestamp" => "1617019505"}) + |> json_response(200) + + assert response["message"] =~ "Query parameter 'closest' is required" + assert response["status"] == "0" + assert Map.has_key?(response, "result") + refute response["result"] + schema = resolve_getblockreward_schema() + assert :ok = ExJsonSchema.Validator.validate(schema, response) + end + + test "with an invalid timestamp param", %{conn: conn} do + response = + conn + |> get("/api", %{ + "module" => "block", + "action" => "getblocknobytime", + "timestamp" => "invalid", + "closest" => " before" + }) + |> json_response(200) + + assert response["message"] =~ "Invalid `timestamp` param" + assert response["status"] == "0" + assert Map.has_key?(response, "result") + refute response["result"] + schema = resolve_getblockreward_schema() + assert :ok = ExJsonSchema.Validator.validate(schema, response) + end + + test "with an invalid closest param", %{conn: conn} do + response = + conn + |> get("/api", %{ + "module" => "block", + "action" => "getblocknobytime", + "timestamp" => "1617019505", + "closest" => "invalid" + }) + |> json_response(200) + + assert response["message"] =~ "Invalid `closest` param" + assert response["status"] == "0" + assert Map.has_key?(response, "result") + refute response["result"] + schema = resolve_getblockreward_schema() + assert :ok = ExJsonSchema.Validator.validate(schema, response) + end + + test "with valid params and before", %{conn: conn} do + timestamp_string = "1617020209" + {:ok, timestamp} = Chain.param_to_block_timestamp(timestamp_string) + block = insert(:block, timestamp: timestamp) + + {timestamp_int, _} = Integer.parse(timestamp_string) + + timestamp_in_the_future_str = + (timestamp_int + 1) + |> to_string() + + expected_result = %{ + "blockNumber" => "#{block.number}" + } + + assert response = + conn + |> get("/api", %{ + "module" => "block", + "action" => "getblocknobytime", + "timestamp" => "#{timestamp_in_the_future_str}", + "closest" => "before" + }) + |> json_response(200) + + assert response["result"] == expected_result + assert response["status"] == "1" + assert response["message"] == "OK" + schema = resolve_getblockreward_schema() + assert :ok = ExJsonSchema.Validator.validate(schema, response) + end + + test "with valid params and after", %{conn: conn} do + timestamp_string = "1617020209" + {:ok, timestamp} = Chain.param_to_block_timestamp(timestamp_string) + block = insert(:block, timestamp: timestamp) + + {timestamp_int, _} = Integer.parse(timestamp_string) + + timestamp_in_the_past_str = + (timestamp_int - 1) + |> to_string() + + expected_result = %{ + "blockNumber" => "#{block.number}" + } + + assert response = + conn + |> get("/api", %{ + "module" => "block", + "action" => "getblocknobytime", + "timestamp" => "#{timestamp_in_the_past_str}", + "closest" => "after" + }) + |> json_response(200) + + assert response["result"] == expected_result + assert response["status"] == "1" + assert response["message"] == "OK" + schema = resolve_getblockreward_schema() + assert :ok = ExJsonSchema.Validator.validate(schema, response) + end + + test "returns any nearest block within arbitrary range of time", %{conn: conn} do + timestamp_string = "1617020209" + {:ok, timestamp} = Chain.param_to_block_timestamp(timestamp_string) + block = insert(:block, timestamp: timestamp) + + {timestamp_int, _} = Integer.parse(timestamp_string) + + timestamp_in_the_past_str = + (timestamp_int - 2 * 60) + |> to_string() + + expected_result = %{ + "blockNumber" => "#{block.number}" + } + + assert response = + conn + |> get("/api", %{ + "module" => "block", + "action" => "getblocknobytime", + "timestamp" => "#{timestamp_in_the_past_str}", + "closest" => "after" + }) + |> json_response(200) + + assert response["result"] == expected_result + assert response["status"] == "1" + assert response["message"] == "OK" + schema = resolve_getblockreward_schema() + assert :ok = ExJsonSchema.Validator.validate(schema, response) + end + end + + defp resolve_getblockreward_schema() do + ExJsonSchema.Schema.resolve(%{ + "type" => "object", + "properties" => %{ + "message" => %{"type" => "string"}, + "status" => %{"type" => "string"}, + "result" => %{ + "type" => ["object", "null"], + "properties" => %{ + "blockNumber" => %{"type" => "string"}, + "timeStamp" => %{"type" => "number"}, + "blockMiner" => %{"type" => "string"}, + "blockReward" => %{"type" => "string"}, + "uncles" => %{ + "type" => "array", + "items" => %{ + "type" => "object", + "properties" => %{ + "miner" => %{"type" => "string"}, + "unclePosition" => %{"type" => "string"}, + "blockreward" => %{"type" => "string"} + } + } + }, + "uncleInclusionReward" => %{"type" => "string"} + } + } + } + }) + end + + defp resolve_getblockcountdown_schema() do + ExJsonSchema.Schema.resolve(%{ + "type" => "object", + "properties" => %{ + "message" => %{"type" => "string"}, + "status" => %{"type" => "string"}, + "result" => %{ + "type" => "object", + "properties" => %{ + "CurrentBlock" => %{"type" => "string"}, + "CountdownBlock" => %{"type" => "string"}, + "RemainingBlock" => %{"type" => "string"}, + "EstimateTimeInSec" => %{"type" => "string"} + } + } + } + }) + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/contract_controller_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/contract_controller_test.exs new file mode 100644 index 0000000..04e2eb9 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/contract_controller_test.exs @@ -0,0 +1,1340 @@ +defmodule BlockScoutWeb.API.RPC.ContractControllerTest do + use BlockScoutWeb.ConnCase + + import Mox + import Ecto.Query + + alias Explorer.{Repo, TestHelper} + alias Explorer.Chain.SmartContract.Proxy.Models.Implementation + alias Explorer.Chain.{Address, SmartContract} + + setup :verify_on_exit! + + if Application.compile_env(:explorer, :chain_type) == :zksync do + @optimization_runs "0" + else + @optimization_runs 200 + end + + def prepare_contracts do + insert(:contract_address) + {:ok, dt_1, _} = DateTime.from_iso8601("2022-09-20 10:00:00Z") + + contract_1 = + insert(:smart_contract, + contract_code_md5: "123", + name: "Test 1", + optimization: "1", + compiler_version: "v0.6.8+commit.0bbfe453", + abi: [%{foo: "bar"}], + inserted_at: dt_1 + ) + + insert(:contract_address) + {:ok, dt_2, _} = DateTime.from_iso8601("2022-09-22 10:00:00Z") + + contract_2 = + insert(:smart_contract, + contract_code_md5: "12345", + name: "Test 2", + optimization: "0", + compiler_version: "v0.7.5+commit.eb77ed08", + abi: [%{foo: "bar-2"}], + inserted_at: dt_2 + ) + + insert(:contract_address) + {:ok, dt_3, _} = DateTime.from_iso8601("2022-09-24 10:00:00Z") + + contract_3 = + insert(:smart_contract, + contract_code_md5: "1234567", + name: "Test 3", + optimization: "1", + compiler_version: "v0.4.26+commit.4563c3fc", + abi: [%{foo: "bar-3"}], + inserted_at: dt_3 + ) + + [contract_1, contract_2, contract_3] + end + + def result(contract) do + %{ + "ABI" => Jason.encode!(contract.abi), + "Address" => to_string(contract.address_hash), + "CompilerVersion" => contract.compiler_version, + "ContractName" => contract.name, + "OptimizationUsed" => if(contract.optimization, do: "1", else: "0") + } + end + + defp result_not_verified(address_hash) do + %{ + "ABI" => "Contract source code not verified", + "Address" => to_string(address_hash) + } + end + + describe "listcontracts" do + setup do + %{params: %{"module" => "contract", "action" => "listcontracts"}} + end + + test "with an invalid filter value", %{conn: conn, params: params} do + response = + conn + |> get("/api", Map.put(params, "filter", "invalid")) + |> json_response(400) + + assert response["message"] == + "invalid is not a valid value for `filter`. Please use one of: verified, unverified, 1, 2." + + assert response["status"] == "0" + assert :ok = ExJsonSchema.Validator.validate(listcontracts_schema(), response) + end + + test "with no contracts", %{conn: conn, params: params} do + response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["message"] == "OK" + assert response["status"] == "1" + assert response["result"] == [] + assert :ok = ExJsonSchema.Validator.validate(listcontracts_schema(), response) + end + + test "with a verified smart contract, all contract information is shown", %{conn: conn, params: params} do + contract = insert(:smart_contract, contract_code_md5: "123") + + response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["message"] == "OK" + assert response["status"] == "1" + + result_props = result(contract) |> Map.keys() + + for prop <- result_props do + assert Enum.at(response["result"], 0)[prop] == result(contract)[prop] + end + + assert :ok = ExJsonSchema.Validator.validate(listcontracts_schema(), response) + end + + test "with an unverified contract address, only basic information is shown", %{conn: conn, params: params} do + address = insert(:contract_address) + + response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["message"] == "OK" + assert response["status"] == "1" + + assert response["result"] == [result_not_verified(address.hash)] + + assert :ok = ExJsonSchema.Validator.validate(listcontracts_schema(), response) + end + + test "filtering for only unverified contracts shows only unverified contracts", %{params: params, conn: conn} do + address = insert(:contract_address) + insert(:smart_contract, contract_code_md5: "123") + + response = + conn + |> get("/api", Map.put(params, "filter", "unverified")) + |> json_response(200) + + assert response["message"] == "OK" + assert response["status"] == "1" + + assert response["result"] == [result_not_verified(address.hash)] + + assert :ok = ExJsonSchema.Validator.validate(listcontracts_schema(), response) + end + + test "filtering for only unverified contracts does not show self destructed contracts", %{ + params: params, + conn: conn + } do + address = insert(:contract_address) + insert(:smart_contract, contract_code_md5: "123") + insert(:contract_address, contract_code: "0x") + + response = + conn + |> get("/api", Map.put(params, "filter", "unverified")) + |> json_response(200) + + assert response["message"] == "OK" + assert response["status"] == "1" + + assert response["result"] == [result_not_verified(address.hash)] + + assert :ok = ExJsonSchema.Validator.validate(listcontracts_schema(), response) + end + + test "filtering for only verified contracts shows only verified contracts", %{params: params, conn: conn} do + insert(:contract_address) + contract = insert(:smart_contract, contract_code_md5: "123") + + response = + conn + |> get("/api", Map.put(params, "filter", "verified")) + |> json_response(200) + + assert response["message"] == "OK" + assert response["status"] == "1" + + result_props = result(contract) |> Map.keys() + + for prop <- result_props do + assert Enum.at(response["result"], 0)[prop] == result(contract)[prop] + end + + assert :ok = ExJsonSchema.Validator.validate(listcontracts_schema(), response) + end + + test "filtering for only verified contracts in the date range shows only verified contracts in that range", %{ + params: params, + conn: conn + } do + [_contract_1, contract_2, _contract_3] = prepare_contracts() + + filter_params = + params + |> Map.put("filter", "verified") + |> Map.put("verified_at_start_timestamp", "1663749418") + |> Map.put("verified_at_end_timestamp", "1663922218") + + response = + conn + |> get("/api", filter_params) + |> json_response(200) + + assert response["message"] == "OK" + assert response["status"] == "1" + + result_props = result(contract_2) |> Map.keys() + + for prop <- result_props do + assert Enum.at(response["result"], 0)[prop] == result(contract_2)[prop] + end + + assert :ok = ExJsonSchema.Validator.validate(listcontracts_schema(), response) + end + + test "filtering for only verified contracts with start created_at timestamp >= given timestamp shows only verified contracts in that range", + %{ + params: params, + conn: conn + } do + [_contract_1, contract_2, contract_3] = prepare_contracts() + + filter_params = + params + |> Map.put("filter", "verified") + |> Map.put("verified_at_start_timestamp", "1663749418") + + response = + conn + |> get("/api", filter_params) + |> json_response(200) + + assert response["message"] == "OK" + assert response["status"] == "1" + + result_props = result(contract_2) |> Map.keys() + + for prop <- result_props do + assert Enum.at(response["result"], 0)[prop] == result(contract_2)[prop] + assert Enum.at(response["result"], 1)[prop] == result(contract_3)[prop] + end + + assert :ok = ExJsonSchema.Validator.validate(listcontracts_schema(), response) + end + + test "filtering for only verified contracts with end created_at timestamp < given timestamp shows only verified contracts in that range", + %{ + params: params, + conn: conn + } do + [contract_1, contract_2, _contract_3] = prepare_contracts() + + filter_params = + params + |> Map.put("filter", "verified") + |> Map.put("verified_at_end_timestamp", "1663922218") + + response = + conn + |> get("/api", filter_params) + |> json_response(200) + + assert response["message"] == "OK" + assert response["status"] == "1" + + result_props = result(contract_1) |> Map.keys() + + for prop <- result_props do + assert Enum.at(response["result"], 0)[prop] == result(contract_1)[prop] + assert Enum.at(response["result"], 1)[prop] == result(contract_2)[prop] + end + + assert :ok = ExJsonSchema.Validator.validate(listcontracts_schema(), response) + end + end + + describe "getabi" do + test "with missing address hash", %{conn: conn} do + params = %{ + "module" => "contract", + "action" => "getabi" + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["message"] =~ "address is required" + assert response["status"] == "0" + assert Map.has_key?(response, "result") + refute response["result"] + assert :ok = ExJsonSchema.Validator.validate(getabi_schema(), response) + end + + test "with an invalid address hash", %{conn: conn} do + params = %{ + "module" => "contract", + "action" => "getabi", + "address" => "badhash" + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["message"] =~ "Invalid address hash" + assert response["status"] == "0" + assert Map.has_key?(response, "result") + refute response["result"] + assert :ok = ExJsonSchema.Validator.validate(getabi_schema(), response) + end + + test "with an address that doesn't exist", %{conn: conn} do + params = %{ + "module" => "contract", + "action" => "getabi", + "address" => "0x8bf38d4764929064f2d4d3a56520a76ab3df415b" + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["result"] == nil + assert response["status"] == "0" + assert response["message"] == "Contract source code not verified" + assert :ok = ExJsonSchema.Validator.validate(getabi_schema(), response) + end + + test "with a verified contract address", %{conn: conn} do + contract = insert(:smart_contract, contract_code_md5: "123") + + params = %{ + "module" => "contract", + "action" => "getabi", + "address" => to_string(contract.address_hash) + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["result"] == Jason.encode!(contract.abi) + assert response["status"] == "1" + assert response["message"] == "OK" + assert :ok = ExJsonSchema.Validator.validate(getabi_schema(), response) + end + end + + describe "getsourcecode" do + test "with missing address hash", %{conn: conn} do + params = %{ + "module" => "contract", + "action" => "getsourcecode" + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["message"] =~ "address is required" + assert response["status"] == "0" + assert Map.has_key?(response, "result") + refute response["result"] + assert :ok = ExJsonSchema.Validator.validate(getsourcecode_schema(), response) + end + + test "with an invalid address hash", %{conn: conn} do + params = %{ + "module" => "contract", + "action" => "getsourcecode", + "address" => "badhash" + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["message"] =~ "Invalid address hash" + assert response["status"] == "0" + assert Map.has_key?(response, "result") + refute response["result"] + assert :ok = ExJsonSchema.Validator.validate(getsourcecode_schema(), response) + end + + test "with an address that doesn't exist", %{conn: conn} do + params = %{ + "module" => "contract", + "action" => "getsourcecode", + "address" => "0x8bf38d4764929064f2d4d3a56520a76ab3df415b" + } + + expected_result = [ + %{ + "Address" => "0x8bf38d4764929064f2d4d3a56520a76ab3df415b" + } + ] + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["result"] == expected_result + assert response["status"] == "1" + assert response["message"] == "OK" + assert :ok = ExJsonSchema.Validator.validate(getsourcecode_schema(), response) + end + + test "with a verified contract address", %{conn: conn} do + contract = + insert(:smart_contract, + optimization: true, + optimization_runs: @optimization_runs, + evm_version: "default", + contract_code_md5: "123" + ) + + params = %{ + "module" => "contract", + "action" => "getsourcecode", + "address" => to_string(contract.address_hash) + } + + expected_result = [ + %{ + "Address" => to_string(contract.address_hash), + "SourceCode" => contract.contract_source_code, + "ABI" => Jason.encode!(contract.abi), + "ContractName" => contract.name, + "CompilerVersion" => contract.compiler_version, + # The contract's optimization value is true, so the expected value + # for `OptimizationUsed` is "1". If it was false, the expected value + # would be "0". + "OptimizationUsed" => "true", + "OptimizationRuns" => @optimization_runs, + "EVMVersion" => "default", + "FileName" => "", + "IsProxy" => "false" + } + ] + + TestHelper.get_all_proxies_implementation_zero_addresses() + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + result_props = Enum.at(expected_result, 0) |> Map.keys() + + for prop <- result_props do + assert Enum.at(response["result"], 0)[prop] == Enum.at(expected_result, 0)[prop] + end + + assert response["status"] == "1" + assert response["message"] == "OK" + assert :ok = ExJsonSchema.Validator.validate(getsourcecode_schema(), response) + end + + test "with a verified proxy contract address", %{conn: conn} do + implementation_contract = + insert(:smart_contract, + name: "Implementation", + external_libraries: [], + constructor_arguments: "", + abi: [ + %{ + "type" => "constructor", + "inputs" => [ + %{"type" => "address", "name" => "_proxyStorage"}, + %{"type" => "address", "name" => "_implementationAddress"} + ] + }, + %{ + "constant" => false, + "inputs" => [%{"name" => "x", "type" => "uint256"}], + "name" => "set", + "outputs" => [], + "payable" => false, + "stateMutability" => "nonpayable", + "type" => "function" + }, + %{ + "constant" => true, + "inputs" => [], + "name" => "get", + "outputs" => [%{"name" => "", "type" => "uint256"}], + "payable" => false, + "stateMutability" => "view", + "type" => "function" + } + ], + license_type: 9 + ) + + implementation_contract_address_hash_string = + Base.encode16(implementation_contract.address_hash.bytes, case: :lower) + + proxy_transaction_input = + "0x11b804ab000000000000000000000000" <> + implementation_contract_address_hash_string <> + "000000000000000000000000000000000000000000000000000000000000006035323031313537360000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000284e159163400000000000000000000000034420c13696f4ac650b9fafe915553a1abcd7dd30000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000001c00000000000000000000000000000000000000000000000000000000000000220000000000000000000000000ff5ae9b0a7522736299d797d80b8fc6f31d61100000000000000000000000000ff5ae9b0a7522736299d797d80b8fc6f31d6110000000000000000000000000000000000000000000000000000000000000003e8000000000000000000000000000000000000000000000000000000000000000000000000000000000000000034420c13696f4ac650b9fafe915553a1abcd7dd300000000000000000000000000000000000000000000000000000000000000184f7074696d69736d2053756273637269626572204e465473000000000000000000000000000000000000000000000000000000000000000000000000000000054f504e46540000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000037697066733a2f2f516d66544e504839765651334b5952346d6b52325a6b757756424266456f5a5554545064395538666931503332752f300000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000c82bbe41f2cf04e3a8efa18f7032bdd7f6d98a81000000000000000000000000efba8a2a82ec1fb1273806174f5e28fbb917cf9500000000000000000000000000000000000000000000000000000000" + + proxy_deployed_bytecode = + "0x363d3d373d3d3d363d73" <> implementation_contract_address_hash_string <> "5af43d82803e903d91602b57fd5bf3" + + proxy_address = + insert(:contract_address, + contract_code: proxy_deployed_bytecode, + verified: true + ) + + proxy_abi = [ + %{ + "type" => "function", + "stateMutability" => "nonpayable", + "payable" => false, + "outputs" => [%{"type" => "bool", "name" => ""}], + "name" => "upgradeTo", + "inputs" => [%{"type" => "address", "name" => "newImplementation"}], + "constant" => false + }, + %{ + "type" => "function", + "stateMutability" => "view", + "payable" => false, + "outputs" => [%{"type" => "uint256", "name" => ""}], + "name" => "version", + "inputs" => [], + "constant" => true + }, + %{ + "type" => "function", + "stateMutability" => "view", + "payable" => false, + "outputs" => [%{"type" => "address", "name" => ""}], + "name" => "implementation", + "inputs" => [], + "constant" => true + }, + %{ + "type" => "function", + "stateMutability" => "nonpayable", + "payable" => false, + "outputs" => [], + "name" => "renounceOwnership", + "inputs" => [], + "constant" => false + }, + %{ + "type" => "function", + "stateMutability" => "view", + "payable" => false, + "outputs" => [%{"type" => "address", "name" => ""}], + "name" => "getOwner", + "inputs" => [], + "constant" => true + }, + %{ + "type" => "function", + "stateMutability" => "view", + "payable" => false, + "outputs" => [%{"type" => "address", "name" => ""}], + "name" => "getProxyStorage", + "inputs" => [], + "constant" => true + }, + %{ + "type" => "function", + "stateMutability" => "nonpayable", + "payable" => false, + "outputs" => [], + "name" => "transferOwnership", + "inputs" => [%{"type" => "address", "name" => "_newOwner"}], + "constant" => false + }, + %{ + "type" => "constructor", + "stateMutability" => "nonpayable", + "payable" => false, + "inputs" => [ + %{"type" => "address", "name" => "_proxyStorage"}, + %{"type" => "address", "name" => "_implementationAddress"} + ] + }, + %{"type" => "fallback", "stateMutability" => "nonpayable", "payable" => false}, + %{ + "type" => "event", + "name" => "Upgraded", + "inputs" => [ + %{"type" => "uint256", "name" => "version", "indexed" => false}, + %{"type" => "address", "name" => "implementation", "indexed" => true} + ], + "anonymous" => false + }, + %{ + "type" => "event", + "name" => "OwnershipRenounced", + "inputs" => [%{"type" => "address", "name" => "previousOwner", "indexed" => true}], + "anonymous" => false + }, + %{ + "type" => "event", + "name" => "OwnershipTransferred", + "inputs" => [ + %{"type" => "address", "name" => "previousOwner", "indexed" => true}, + %{"type" => "address", "name" => "newOwner", "indexed" => true} + ], + "anonymous" => false + } + ] + + proxy_contract = + insert(:smart_contract, + address_hash: proxy_address.hash, + name: "Proxy", + abi: proxy_abi + ) + + insert(:transaction, + created_contract_address_hash: proxy_address.hash, + input: proxy_transaction_input + ) + |> with_block(status: :ok) + + name = implementation_contract.name + + insert(:proxy_implementation, + proxy_address_hash: proxy_address.hash, + proxy_type: "eip1167", + address_hashes: [implementation_contract.address_hash], + names: [name] + ) + + params = %{ + "module" => "contract", + "action" => "getsourcecode", + "address" => Address.checksum(proxy_address.hash) + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + expected_result = [ + %{ + "Address" => to_string(proxy_contract.address_hash), + "SourceCode" => proxy_contract.contract_source_code, + "ABI" => Jason.encode!(proxy_contract.abi), + "ContractName" => proxy_contract.name, + "CompilerVersion" => proxy_contract.compiler_version, + "FileName" => "", + "IsProxy" => "true", + "ImplementationAddress" => to_string(implementation_contract.address_hash), + "ImplementationAddresses" => [to_string(implementation_contract.address_hash)], + "EVMVersion" => nil, + "OptimizationUsed" => "false" + } + ] + + result_props = Enum.at(expected_result, 0) |> Map.keys() + + for prop <- result_props do + assert Enum.at(response["result"], 0)[prop] == Enum.at(expected_result, 0)[prop] + end + + assert response["status"] == "1" + assert response["message"] == "OK" + assert :ok = ExJsonSchema.Validator.validate(getsourcecode_schema(), response) + end + + test "with constructor arguments", %{conn: conn} do + contract = + insert(:smart_contract, + optimization: true, + optimization_runs: @optimization_runs, + evm_version: "default", + constructor_arguments: + "00000000000000000000000008e7592ce0d7ebabf42844b62ee6a878d4e1913e000000000000000000000000e1b6037da5f1d756499e184ca15254a981c92546", + contract_code_md5: "123" + ) + + params = %{ + "module" => "contract", + "action" => "getsourcecode", + "address" => to_string(contract.address_hash) + } + + expected_result = [ + %{ + "Address" => to_string(contract.address_hash), + "SourceCode" => contract.contract_source_code, + "ABI" => Jason.encode!(contract.abi), + "ContractName" => contract.name, + "CompilerVersion" => contract.compiler_version, + "OptimizationUsed" => "true", + "OptimizationRuns" => @optimization_runs, + "EVMVersion" => "default", + "ConstructorArguments" => + "00000000000000000000000008e7592ce0d7ebabf42844b62ee6a878d4e1913e000000000000000000000000e1b6037da5f1d756499e184ca15254a981c92546", + "FileName" => "", + "IsProxy" => "false" + } + ] + + TestHelper.get_all_proxies_implementation_zero_addresses() + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + result_props = Enum.at(expected_result, 0) |> Map.keys() + + for prop <- result_props do + assert Enum.at(response["result"], 0)[prop] == Enum.at(expected_result, 0)[prop] + end + + assert response["status"] == "1" + assert response["message"] == "OK" + assert :ok = ExJsonSchema.Validator.validate(getsourcecode_schema(), response) + end + + test "with external library", %{conn: conn} do + smart_contract_bytecode = + "0x608060405234801561001057600080fd5b5060df8061001f6000396000f3006080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c146078575b600080fd5b348015605957600080fd5b5060766004803603810190808035906020019092919050505060a0565b005b348015608357600080fd5b50608a60aa565b6040518082815260200191505060405180910390f35b8060008190555050565b600080549050905600a165627a7a7230582040d82a7379b1ee1632ad4d8a239954fd940277b25628ead95259a85c5eddb2120029" + + created_contract_address = + insert( + :address, + hash: "0x0f95fa9bc0383e699325f2658d04e8d96d87b90c", + contract_code: smart_contract_bytecode + ) + + transaction = + :transaction + |> insert() + |> with_block() + + insert( + :internal_transaction_create, + transaction: transaction, + index: 0, + created_contract_address: created_contract_address, + created_contract_code: smart_contract_bytecode, + block_number: transaction.block_number, + block_hash: transaction.block_hash, + block_index: 0, + transaction_index: transaction.index + ) + + valid_attrs = %{ + address_hash: "0x0f95fa9bc0383e699325f2658d04e8d96d87b90c", + name: "Test", + compiler_version: "0.4.23", + contract_source_code: + "pragma solidity ^0.4.23; contract SimpleStorage {uint storedData; function set(uint x) public {storedData = x; } function get() public constant returns (uint) {return storedData; } }", + abi: [ + %{ + "constant" => false, + "inputs" => [%{"name" => "x", "type" => "uint256"}], + "name" => "set", + "outputs" => [], + "payable" => false, + "stateMutability" => "nonpayable", + "type" => "function" + }, + %{ + "constant" => true, + "inputs" => [], + "name" => "get", + "outputs" => [%{"name" => "", "type" => "uint256"}], + "payable" => false, + "stateMutability" => "view", + "type" => "function" + } + ], + optimization: true, + optimization_runs: @optimization_runs, + evm_version: "default" + } + + external_libraries = [ + %SmartContract.ExternalLibrary{:address_hash => "0xb18aed9518d735482badb4e8b7fd8d2ba425ce95", :name => "Test"}, + %SmartContract.ExternalLibrary{:address_hash => "0x283539e1b1daf24cdd58a3e934d55062ea663c3f", :name => "Test2"} + ] + + {:ok, %SmartContract{} = contract} = SmartContract.create_smart_contract(valid_attrs, external_libraries) + + params = %{ + "module" => "contract", + "action" => "getsourcecode", + "address" => to_string(contract.address_hash) + } + + expected_result = [ + %{ + "Address" => to_string(contract.address_hash), + "SourceCode" => contract.contract_source_code, + "ABI" => Jason.encode!(contract.abi), + "ContractName" => contract.name, + "CompilerVersion" => contract.compiler_version, + "OptimizationUsed" => "true", + "OptimizationRuns" => @optimization_runs, + "EVMVersion" => "default", + "ExternalLibraries" => [ + %{"name" => "Test", "address_hash" => "0xb18aed9518d735482badb4e8b7fd8d2ba425ce95"}, + %{"name" => "Test2", "address_hash" => "0x283539e1b1daf24cdd58a3e934d55062ea663c3f"} + ], + "FileName" => "", + "IsProxy" => "false" + } + ] + + TestHelper.get_all_proxies_implementation_zero_addresses() + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + result_props = Enum.at(expected_result, 0) |> Map.keys() + + for prop <- result_props do + assert Enum.at(response["result"], 0)[prop] == Enum.at(expected_result, 0)[prop] + end + + assert response["status"] == "1" + assert response["message"] == "OK" + assert :ok = ExJsonSchema.Validator.validate(getsourcecode_schema(), response) + end + end + + describe "verify" do + test "verify known on sourcify repo contract", %{conn: conn} do + response = verify(conn) + + assert response["message"] == "OK" + assert response["status"] == "1" + + assert response["result"]["ABI"] == + "[{\"inputs\":[],\"name\":\"retrieve\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"_number\",\"type\":\"uint256\"}],\"name\":\"store\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"}]" + + assert response["result"]["CompilerVersion"] == "v0.7.6+commit.7338295f" + assert response["result"]["ContractName"] == "Storage" + assert response["result"]["EVMVersion"] == "istanbul" + assert response["result"]["OptimizationUsed"] == "false" + end + + test "verify already verified contract", %{conn: conn} do + _response = verify(conn) + + params = %{ + "module" => "contract", + "action" => "verify_via_sourcify", + "addressHash" => "0xf26594F585De4EB0Ae9De865d9053FEe02ac6eF1" + } + + response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["message"] == "Smart-contract already verified." + assert response["status"] == "0" + assert response["result"] == nil + end + + defp verify(conn) do + smart_contract_bytecode = + "0x6080604052348015600f57600080fd5b506004361060325760003560e01c80632e64cec11460375780636057361d146053575b600080fd5b603d607e565b6040518082815260200191505060405180910390f35b607c60048036036020811015606757600080fd5b81019080803590602001909291905050506087565b005b60008054905090565b806000819055505056fea26469706673582212205afbc4864a2486ec80f10e5eceeaac30e88c9b3dfcd1bfadd6cdf6e6cb6e1fd364736f6c63430007060033" + + _created_contract_address = + insert( + :address, + hash: "0xf26594F585De4EB0Ae9De865d9053FEe02ac6eF1", + contract_code: smart_contract_bytecode + ) + + params = %{ + "module" => "contract", + "action" => "verify_via_sourcify", + "addressHash" => "0xf26594F585De4EB0Ae9De865d9053FEe02ac6eF1" + } + + TestHelper.get_all_proxies_implementation_zero_addresses() + + conn + |> get("/api", params) + |> json_response(200) + end + + # flaky test + # test "with an address that doesn't exist", %{conn: conn} do + # contract_code_info = Factory.contract_code_info() + + # contract_address = insert(:contract_address, contract_code: contract_code_info.bytecode) + # insert(:transaction, created_contract_address_hash: contract_address.hash, input: contract_code_info.tx_input) + + # params = %{ + # "module" => "contract", + # "action" => "verify", + # "addressHash" => to_string(contract_address.hash), + # "name" => contract_code_info.name, + # "compilerVersion" => contract_code_info.version, + # "optimization" => contract_code_info.optimized, + # "contractSourceCode" => contract_code_info.source_code + # } + + # response = + # conn + # |> get("/api", params) + # |> json_response(200) + + # verified_contract = SmartContract.address_hash_to_smart_contract(contract_address.hash) + + # expected_result = %{ + # "Address" => to_string(contract_address.hash), + # "SourceCode" => + # "/**\n* Submitted for verification at blockscout.com on #{verified_contract.inserted_at}\n*/\n" <> + # contract_code_info.source_code, + # "ABI" => Jason.encode!(contract_code_info.abi), + # "ContractName" => contract_code_info.name, + # "CompilerVersion" => contract_code_info.version, + # "OptimizationUsed" => "false", + # "EVMVersion" => nil + # } + + # assert response["status"] == "1" + # assert response["result"] == expected_result + # assert response["message"] == "OK" + # assert :ok = ExJsonSchema.Validator.validate(verify_schema(), response) + # end + + # flaky test + # test "with external libraries", %{conn: conn} do + # contract_data = + # "#{File.cwd!()}/test/support/fixture/smart_contract/contract_with_lib.json" + # |> File.read!() + # |> Jason.decode!() + # |> List.first() + + # %{ + # "compiler_version" => compiler_version, + # "external_libraries" => external_libraries, + # "name" => name, + # "optimize" => optimize, + # "contract" => contract_source_code, + # "expected_bytecode" => expected_bytecode, + # "tx_input" => tx_input + # } = contract_data + + # contract_address = insert(:contract_address, contract_code: "0x" <> expected_bytecode) + # insert(:transaction, created_contract_address_hash: contract_address.hash, input: "0x" <> tx_input) + + # params = %{ + # "module" => "contract", + # "action" => "verify", + # "addressHash" => to_string(contract_address.hash), + # "name" => name, + # "compilerVersion" => compiler_version, + # "optimization" => optimize, + # "contractSourceCode" => contract_source_code + # } + + # params_with_external_libraries = + # external_libraries + # |> Enum.with_index() + # |> Enum.reduce(params, fn {{name, address}, index}, acc -> + # name_key = "library#{index + 1}Name" + # address_key = "library#{index + 1}Address" + + # acc + # |> Map.put(name_key, name) + # |> Map.put(address_key, address) + # end) + + # response = + # conn + # |> get("/api", params_with_external_libraries) + # |> json_response(200) + + # assert response["status"] == "1" + # assert response["message"] == "OK" + + # result = response["result"] + + # verified_contract = SmartContract.address_hash_to_smart_contract(contract_address.hash) + + # assert result["Address"] == to_string(contract_address.hash) + + # assert result["SourceCode"] == + # "/**\n* Submitted for verification at blockscout.com on #{verified_contract.inserted_at}\n*/\n" <> + # contract_source_code + + # assert result["ContractName"] == name + # assert result["OptimizationUsed"] == "true" + # assert :ok = ExJsonSchema.Validator.validate(verify_schema(), response) + # end + end + + describe "getcontractcreation" do + setup do + %{params: %{"module" => "contract", "action" => "getcontractcreation"}} + end + + test "return error", %{conn: conn, params: params} do + %{ + "status" => "0", + "message" => "Query parameter contractaddresses is required", + "result" => "Query parameter contractaddresses is required" + } = + conn + |> get("/api", params) + |> json_response(200) + end + + test "get empty list", %{conn: conn, params: params} do + address = build(:address) + address_1 = insert(:address) + + %{ + "status" => "1", + "message" => "OK", + "result" => [] + } = + conn + |> get("/api", Map.put(params, "contractaddresses", "#{to_string(address)},#{to_string(address_1)}")) + |> json_response(200) + end + + test "get not empty list", %{conn: conn, params: params} do + address_1 = build(:address) + address = insert(:contract_address) + + transaction = insert(:transaction, created_contract_address: address) + + %{ + "status" => "1", + "message" => "OK", + "result" => [ + %{ + "contractAddress" => contract_address, + "contractCreator" => contract_creator, + "txHash" => transaction_hash + } + ] + } = + conn + |> get("/api", Map.put(params, "contractaddresses", "#{to_string(address)},#{to_string(address_1)}")) + |> json_response(200) + + assert contract_address == to_string(address.hash) + assert contract_creator == to_string(transaction.from_address_hash) + assert transaction_hash == to_string(transaction.hash) + end + end + + describe "verifyproxycontract & checkproxyverification" do + setup do + %{params: %{"module" => "contract"}} + end + + @proxy_abi [ + %{ + "type" => "function", + "stateMutability" => "nonpayable", + "payable" => false, + "outputs" => [%{"type" => "bool", "name" => ""}], + "name" => "upgradeTo", + "inputs" => [%{"type" => "address", "name" => "newImplementation"}], + "constant" => false + }, + %{ + "type" => "function", + "stateMutability" => "view", + "payable" => false, + "outputs" => [%{"type" => "uint256", "name" => ""}], + "name" => "version", + "inputs" => [], + "constant" => true + }, + %{ + "type" => "function", + "stateMutability" => "view", + "payable" => false, + "outputs" => [%{"type" => "address", "name" => ""}], + "name" => "implementation", + "inputs" => [], + "constant" => true + }, + %{ + "type" => "function", + "stateMutability" => "nonpayable", + "payable" => false, + "outputs" => [], + "name" => "renounceOwnership", + "inputs" => [], + "constant" => false + }, + %{ + "type" => "function", + "stateMutability" => "view", + "payable" => false, + "outputs" => [%{"type" => "address", "name" => ""}], + "name" => "getOwner", + "inputs" => [], + "constant" => true + }, + %{ + "type" => "function", + "stateMutability" => "view", + "payable" => false, + "outputs" => [%{"type" => "address", "name" => ""}], + "name" => "getProxyStorage", + "inputs" => [], + "constant" => true + }, + %{ + "type" => "function", + "stateMutability" => "nonpayable", + "payable" => false, + "outputs" => [], + "name" => "transferOwnership", + "inputs" => [%{"type" => "address", "name" => "_newOwner"}], + "constant" => false + }, + %{ + "type" => "constructor", + "stateMutability" => "nonpayable", + "payable" => false, + "inputs" => [ + %{"type" => "address", "name" => "_proxyStorage"}, + %{"type" => "address", "name" => "_implementationAddress"} + ] + }, + %{"type" => "fallback", "stateMutability" => "nonpayable", "payable" => false}, + %{ + "type" => "event", + "name" => "Upgraded", + "inputs" => [ + %{"type" => "uint256", "name" => "version", "indexed" => false}, + %{"type" => "address", "name" => "implementation", "indexed" => true} + ], + "anonymous" => false + }, + %{ + "type" => "event", + "name" => "OwnershipRenounced", + "inputs" => [%{"type" => "address", "name" => "previousOwner", "indexed" => true}], + "anonymous" => false + }, + %{ + "type" => "event", + "name" => "OwnershipTransferred", + "inputs" => [ + %{"type" => "address", "name" => "previousOwner", "indexed" => true}, + %{"type" => "address", "name" => "newOwner", "indexed" => true} + ], + "anonymous" => false + } + ] + @implementation_abi [ + %{ + "constant" => false, + "inputs" => [%{"name" => "x", "type" => "uint256"}], + "name" => "set", + "outputs" => [], + "payable" => false, + "stateMutability" => "nonpayable", + "type" => "function" + }, + %{ + "constant" => true, + "inputs" => [], + "name" => "get", + "outputs" => [%{"name" => "", "type" => "uint256"}], + "payable" => false, + "stateMutability" => "view", + "type" => "function" + } + ] + test "verify", %{conn: conn, params: params} do + proxy_contract_address = insert(:contract_address) + + insert(:smart_contract, address_hash: proxy_contract_address.hash, abi: @proxy_abi, contract_code_md5: "123") + + implementation_contract_address = insert(:contract_address) + + insert(:smart_contract, + address_hash: implementation_contract_address.hash, + abi: @implementation_abi, + contract_code_md5: "123" + ) + + implementation_contract_address_hash_string = + Base.encode16(implementation_contract_address.hash.bytes, case: :lower) + + TestHelper.get_all_proxies_implementation_zero_addresses() + + expect( + EthereumJSONRPC.Mox, + :json_rpc, + fn [%{id: id, method: _, params: [%{data: _, to: _}, _]}], _options -> + {:ok, + [ + %{ + id: id, + jsonrpc: "2.0", + result: "0x000000000000000000000000" <> implementation_contract_address_hash_string + } + ]} + end + ) + + %{ + "message" => "OK", + "result" => uid, + "status" => "1" + } = + conn + |> get( + "/api", + Map.merge(params, %{"action" => "verifyproxycontract", "address" => to_string(proxy_contract_address.hash)}) + ) + |> json_response(200) + + :timer.sleep(333) + + result = + "The proxy's (#{to_string(proxy_contract_address.hash)}) implementation contract is found at #{to_string(implementation_contract_address.hash)} and is successfully updated." + + %{ + "message" => "OK", + "result" => ^result, + "status" => "1" + } = + conn + |> get("/api", Map.merge(params, %{"action" => "checkproxyverification", "guid" => uid})) + |> json_response(200) + + assert %Implementation{address_hashes: implementations} = + Implementation + |> where([i], i.proxy_address_hash == ^proxy_contract_address.hash) + |> Repo.one() + + assert implementations == [implementation_contract_address.hash] + end + end + + defp listcontracts_schema do + resolve_schema(%{ + "type" => ["array", "null"], + "items" => %{ + "type" => "object", + "properties" => %{ + "Address" => %{"type" => "string"}, + "ABI" => %{"type" => "string"}, + "ContractName" => %{"type" => "string"}, + "CompilerVersion" => %{"type" => "string"}, + "OptimizationUsed" => %{"type" => "string"} + } + } + }) + end + + defp getabi_schema do + resolve_schema(%{ + "type" => ["string", "null"] + }) + end + + defp getsourcecode_schema do + resolve_schema(%{ + "type" => ["array", "null"], + "items" => %{ + "type" => "object", + "properties" => %{ + "Address" => %{"type" => "string"}, + "SourceCode" => %{"type" => "string"}, + "ABI" => %{"type" => "string"}, + "ContractName" => %{"type" => "string"}, + "CompilerVersion" => %{"type" => "string"}, + "OptimizationUsed" => %{"type" => "string"} + } + } + }) + end + + # defp verify_schema do + # resolve_schema(%{ + # "type" => "object", + # "properties" => %{ + # "Address" => %{"type" => "string"}, + # "SourceCode" => %{"type" => "string"}, + # "ABI" => %{"type" => "string"}, + # "ContractName" => %{"type" => "string"}, + # "CompilerVersion" => %{"type" => "string"}, + # "OptimizationUsed" => %{"type" => "string"} + # } + # }) + # end + + defp resolve_schema(result) do + %{ + "type" => "object", + "properties" => %{ + "message" => %{"type" => "string"}, + "status" => %{"type" => "string"} + } + } + |> put_in(["properties", "result"], result) + |> ExJsonSchema.Schema.resolve() + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/eth_controller_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/eth_controller_test.exs new file mode 100644 index 0000000..f8191ba --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/eth_controller_test.exs @@ -0,0 +1,752 @@ +defmodule BlockScoutWeb.API.RPC.EthControllerTest do + use BlockScoutWeb.ConnCase, async: false + + alias Explorer.Chain.Cache.Counters.{AddressesCount, AverageBlockTime} + alias Explorer.Repo + alias Indexer.Fetcher.OnDemand.CoinBalance, as: CoinBalanceOnDemand + + @first_topic_hex_string_1 "0x7fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b65" + @first_topic_hex_string_2 "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef" + + @second_topic_hex_string_1 "0x00000000000000000000000098a9dc37d3650b5b30d6c12789b3881ee0b70c16" + @second_topic_hex_string_2 "0x000000000000000000000000e2680fd7cdbb04e9087a647ad4d023ef6c8fb4e2" + + setup do + mocked_json_rpc_named_arguments = [ + transport: EthereumJSONRPC.Mox, + transport_options: [] + ] + + start_supervised!({Task.Supervisor, name: Indexer.TaskSupervisor}) + start_supervised!(AverageBlockTime) + + Indexer.Fetcher.OnDemand.CoinBalance.Supervisor.Case.start_supervised!( + json_rpc_named_arguments: mocked_json_rpc_named_arguments + ) + + start_supervised!(AddressesCount) + + Application.put_env(:explorer, AverageBlockTime, enabled: true, cache_period: 1_800_000) + + on_exit(fn -> + Application.put_env(:explorer, AverageBlockTime, enabled: false, cache_period: 1_800_000) + end) + + :ok + end + + defp params(api_params, params), do: Map.put(api_params, "params", params) + + defp topic(topic_hex_string) do + {:ok, topic} = Explorer.Chain.Hash.Full.cast(topic_hex_string) + topic + end + + test "handles request without params if possible", %{conn: conn} do + assert response = + conn + |> post("/api/eth-rpc", %{ + "method" => "eth_blockNumber", + "jsonrpc" => "2.0", + "id" => 0 + }) + |> json_response(200) + + assert %{"id" => 0, "jsonrpc" => "2.0", "result" => "0x0"} == response + end + + describe "eth_get_logs" do + setup do + %{ + api_params: %{ + "method" => "eth_getLogs", + "jsonrpc" => "2.0", + "id" => 0 + } + } + end + + test "with an invalid address", %{conn: conn, api_params: api_params} do + assert response = + conn + |> post("/api/eth-rpc", params(api_params, [%{"address" => "badhash"}])) + |> json_response(200) + + assert %{"error" => "invalid address"} = response + end + + test "address with no logs", %{conn: conn, api_params: api_params} do + insert(:block) + address = insert(:address) + + assert response = + conn + |> post("/api/eth-rpc", params(api_params, [%{"address" => to_string(address.hash)}])) + |> json_response(200) + + assert %{"result" => []} = response + end + + test "address but no logs and no toBlock provided", %{conn: conn, api_params: api_params} do + address = insert(:address) + + assert response = + conn + |> post("/api/eth-rpc", params(api_params, [%{"address" => to_string(address.hash)}])) + |> json_response(200) + + assert %{"result" => []} = response + end + + test "with a matching address", %{conn: conn, api_params: api_params} do + address = insert(:address) + + block = insert(:block, number: 0) + + transaction = insert(:transaction, from_address: address) |> with_block(block) + + insert(:log, + block: block, + block_number: block.number, + address: address, + transaction: transaction, + data: "0x010101" + ) + + params = params(api_params, [%{"address" => to_string(address.hash)}]) + + assert response = + conn + |> post("/api/eth-rpc", params) + |> json_response(200) + + assert %{"result" => [%{"data" => "0x010101"}]} = response + end + + test "with a matching address and matching topic", %{conn: conn, api_params: api_params} do + address = insert(:address) + + block = insert(:block, number: 0) + + transaction = insert(:transaction, from_address: address) |> with_block(block) + + insert(:log, + block: block, + block_number: block.number, + address: address, + transaction: transaction, + data: "0x010101", + first_topic: topic(@first_topic_hex_string_1) + ) + + params = params(api_params, [%{"address" => to_string(address.hash), "topics" => [@first_topic_hex_string_1]}]) + + assert response = + conn + |> post("/api/eth-rpc", params) + |> json_response(200) + + assert %{"result" => [%{"data" => "0x010101"}]} = response + end + + test "with a matching address and multiple topic matches", %{conn: conn, api_params: api_params} do + address = insert(:address) + + block = insert(:block, number: 0) + + transaction = insert(:transaction, from_address: address) |> with_block(block) + + insert(:log, + address: address, + block: block, + block_number: block.number, + transaction: transaction, + data: "0x010101", + first_topic: topic(@first_topic_hex_string_1) + ) + + insert(:log, + address: address, + block: block, + block_number: block.number, + transaction: transaction, + data: "0x020202", + first_topic: topic(@first_topic_hex_string_2) + ) + + params = + params(api_params, [ + %{"address" => to_string(address.hash), "topics" => [[@first_topic_hex_string_1, @first_topic_hex_string_2]]} + ]) + + assert response = + conn + |> post("/api/eth-rpc", params) + |> json_response(200) + + assert [%{"data" => "0x010101"}, %{"data" => "0x020202"}] = Enum.sort_by(response["result"], &Map.get(&1, "data")) + end + + test "paginates logs", %{conn: conn, api_params: api_params} do + contract_address = insert(:contract_address) + block = insert(:block) + + transaction = + :transaction + |> insert(to_address: contract_address) + |> with_block(block) + + inserted_records = + insert_list(2000, :log, + block: block, + block_number: block.number, + address: contract_address, + transaction: transaction, + first_topic: topic(@first_topic_hex_string_1) + ) + + params = + params(api_params, [%{"address" => to_string(contract_address), "topics" => [[@first_topic_hex_string_1]]}]) + + assert response = + conn + |> post("/api/eth-rpc", params) + |> json_response(200) + + assert Enum.count(response["result"]) == 1000 + + "0x" <> hexadecimal_digits = List.last(response["result"])["logIndex"] + {last_log_index, ""} = Integer.parse(hexadecimal_digits, 16) + + next_page_params = %{ + "blockNumber" => Integer.to_string(transaction.block_number, 16), + "logIndex" => Integer.to_string(last_log_index, 16) + } + + new_params = + params(api_params, [ + %{ + "paging_options" => next_page_params, + "address" => to_string(contract_address), + "topics" => [[@first_topic_hex_string_1]] + } + ]) + + assert new_response = + conn + |> post("/api/eth-rpc", new_params) + |> json_response(200) + + assert Enum.count(response["result"]) == 1000 + + all_found_logs = response["result"] ++ new_response["result"] + + assert Enum.all?(inserted_records, fn record -> + Enum.any?(all_found_logs, fn found_log -> + "0x" <> hexadecimal_digits = found_log["logIndex"] + {index, ""} = Integer.parse(hexadecimal_digits, 16) + + record.index == index + end) + end) + end + + test "with a matching address and multiple topic matches in different positions", %{ + conn: conn, + api_params: api_params + } do + address = insert(:address) + + block = insert(:block, number: 0) + + transaction = insert(:transaction, from_address: address) |> with_block(block) + + insert(:log, + address: address, + transaction: transaction, + data: "0x010101", + first_topic: topic(@first_topic_hex_string_1), + second_topic: topic(@second_topic_hex_string_1), + block: block, + block_number: block.number + ) + + insert(:log, + block: block, + block_number: block.number, + address: address, + transaction: transaction, + data: "0x020202", + first_topic: topic(@first_topic_hex_string_1) + ) + + params = + params(api_params, [ + %{"address" => to_string(address.hash), "topics" => [@first_topic_hex_string_1, @second_topic_hex_string_1]} + ]) + + assert response = + conn + |> post("/api/eth-rpc", params) + |> json_response(200) + + assert [%{"data" => "0x010101"}] = response["result"] + end + + test "with a matching address and multiple topic matches in different positions and multiple matches in the second position", + %{conn: conn, api_params: api_params} do + address = insert(:address) + + block = insert(:block, number: 0) + + transaction = insert(:transaction, from_address: address) |> with_block(block) + + insert(:log, + address: address, + transaction: transaction, + data: "0x010101", + first_topic: topic(@first_topic_hex_string_1), + second_topic: topic(@second_topic_hex_string_1), + block: block, + block_number: block.number + ) + + insert(:log, + address: address, + transaction: transaction, + data: "0x020202", + first_topic: topic(@first_topic_hex_string_1), + second_topic: topic(@second_topic_hex_string_2), + block: block, + block_number: block.number + ) + + params = + params(api_params, [ + %{ + "address" => to_string(address.hash), + "topics" => [@first_topic_hex_string_1, [@second_topic_hex_string_1, @second_topic_hex_string_2]] + } + ]) + + assert response = + conn + |> post("/api/eth-rpc", params) + |> json_response(200) + + assert [%{"data" => "0x010101"}, %{"data" => "0x020202"}] = Enum.sort_by(response["result"], &Map.get(&1, "data")) + end + + test "with a block range filter", + %{conn: conn, api_params: api_params} do + address = insert(:address) + + block1 = insert(:block, number: 0) + block2 = insert(:block, number: 1) + block3 = insert(:block, number: 2) + block4 = insert(:block, number: 3) + + transaction1 = insert(:transaction, from_address: address) |> with_block(block1) + transaction2 = insert(:transaction, from_address: address) |> with_block(block2) + transaction3 = insert(:transaction, from_address: address) |> with_block(block3) + transaction4 = insert(:transaction, from_address: address) |> with_block(block4) + + insert(:log, + address: address, + transaction: transaction1, + data: "0x010101", + block: block1, + block_number: block1.number + ) + + insert(:log, + address: address, + transaction: transaction2, + data: "0x020202", + block: block2, + block_number: block2.number + ) + + insert(:log, + address: address, + transaction: transaction3, + data: "0x030303", + block: block3, + block_number: block3.number + ) + + insert(:log, + address: address, + transaction: transaction4, + data: "0x040404", + block: block4, + block_number: block4.number + ) + + params = params(api_params, [%{"address" => to_string(address.hash), "fromBlock" => 1, "toBlock" => 2}]) + + assert response = + conn + |> post("/api/eth-rpc", params) + |> json_response(200) + + assert [%{"data" => "0x020202"}, %{"data" => "0x030303"}] = Enum.sort_by(response["result"], &Map.get(&1, "data")) + end + + test "with a block hash filter", + %{conn: conn, api_params: api_params} do + address = insert(:address) + + block1 = insert(:block, number: 0) + block2 = insert(:block, number: 1) + block3 = insert(:block, number: 2) + + transaction1 = insert(:transaction, from_address: address) |> with_block(block1) + transaction2 = insert(:transaction, from_address: address) |> with_block(block2) + transaction3 = insert(:transaction, from_address: address) |> with_block(block3) + + insert(:log, + address: address, + transaction: transaction1, + data: "0x010101", + block: block1, + block_number: block1.number + ) + + insert(:log, + address: address, + transaction: transaction2, + data: "0x020202", + block: block2, + block_number: block2.number + ) + + insert(:log, + address: address, + transaction: transaction3, + data: "0x030303", + block: block3, + block_number: block3.number + ) + + params = params(api_params, [%{"address" => to_string(address.hash), "blockHash" => to_string(block2.hash)}]) + + assert response = + conn + |> post("/api/eth-rpc", params) + |> json_response(200) + + assert [%{"data" => "0x020202"}] = response["result"] + end + + test "with an earliest block filter", + %{conn: conn, api_params: api_params} do + address = insert(:address) + + block1 = insert(:block, number: 0) + block2 = insert(:block, number: 1) + block3 = insert(:block, number: 2) + + transaction1 = insert(:transaction, from_address: address) |> with_block(block1) + transaction2 = insert(:transaction, from_address: address) |> with_block(block2) + transaction3 = insert(:transaction, from_address: address) |> with_block(block3) + + insert(:log, + address: address, + transaction: transaction1, + data: "0x010101", + block: block1, + block_number: block1.number + ) + + insert(:log, + address: address, + transaction: transaction2, + data: "0x020202", + block: block2, + block_number: block2.number + ) + + insert(:log, + address: address, + transaction: transaction3, + data: "0x030303", + block: block3, + block_number: block3.number + ) + + params = + params(api_params, [%{"address" => to_string(address.hash), "fromBlock" => "earliest", "toBlock" => "earliest"}]) + + assert response = + conn + |> post("/api/eth-rpc", params) + |> json_response(200) + + assert [%{"data" => "0x010101"}] = response["result"] + end + + test "with a pending block filter", + %{conn: conn, api_params: api_params} do + address = insert(:address) + + block1 = insert(:block, number: 0) + block2 = insert(:block, number: 1) + block3 = insert(:block, number: 2) + + transaction1 = insert(:transaction, from_address: address) |> with_block(block1) + transaction2 = insert(:transaction, from_address: address) |> with_block(block2) + transaction3 = insert(:transaction, from_address: address) |> with_block(block3) + + insert(:log, + block: block1, + block_number: block1.number, + address: address, + transaction: transaction1, + data: "0x010101" + ) + + insert(:log, + block: block2, + block_number: block2.number, + address: address, + transaction: transaction2, + data: "0x020202" + ) + + insert(:log, + block: block3, + block_number: block3.number, + address: address, + transaction: transaction3, + data: "0x030303" + ) + + changeset = Ecto.Changeset.change(block3, %{consensus: false}) + + Repo.update!(changeset) + + params = + params(api_params, [%{"address" => to_string(address.hash), "fromBlock" => "pending", "toBlock" => "pending"}]) + + assert response = + conn + |> post("/api/eth-rpc", params) + |> json_response(200) + + assert [%{"data" => "0x020202"}] = response["result"] + end + + test "numerical fields are hexadecimals with 0x prefix", + %{conn: conn, api_params: api_params} do + address = insert(:address) + block = insert(:block, number: 0) + transaction = insert(:transaction, from_address: address) |> with_block(block) + + insert(:log, + block: block, + block_number: block.number, + address: address, + transaction: transaction, + data: "0x010101", + first_topic: topic(@first_topic_hex_string_1) + ) + + params = + params(api_params, [ + %{ + "address" => to_string(address.hash), + "topics" => [@first_topic_hex_string_1] + } + ]) + + response = + conn + |> post("/api/eth-rpc", params) + |> json_response(200) + + [result] = response["result"] + + assert result + |> Map.take([ + "address", + "blockHash", + "blockNumber", + "data", + "transactionIndex", + "logIndex", + "transactionHash" + ]) + |> Enum.all?(fn {_, v} -> String.starts_with?(v, "0x") end) + end + end + + describe "eth_get_balance" do + setup do + %{ + api_params: %{ + "method" => "eth_getBalance", + "jsonrpc" => "2.0", + "id" => 0 + } + } + end + + test "with an invalid address", %{conn: conn, api_params: api_params} do + assert response = + conn + |> post("/api/eth-rpc", params(api_params, ["badHash"])) + |> json_response(200) + + assert %{"error" => "Query parameter 'address' is invalid"} = response + end + + test "with a valid address that has no balance", %{conn: conn, api_params: api_params} do + address = insert(:address) + + assert response = + conn + |> post("/api/eth-rpc", params(api_params, [to_string(address.hash)])) + |> json_response(200) + + assert %{"error" => "Balance not found"} = response + end + + test "with a valid address that has a balance", %{conn: conn, api_params: api_params} do + block = insert(:block) + address = insert(:address, fetched_coin_balance: 1, fetched_coin_balance_block_number: block.number) + + assert response = + conn + |> post("/api/eth-rpc", params(api_params, [to_string(address.hash)])) + |> json_response(200) + + assert %{"result" => "0x1"} = response + end + + test "with a valid address that has no earliest balance", %{conn: conn, api_params: api_params} do + block = insert(:block, number: 1) + address = insert(:address) + + insert(:fetched_balance, block_number: block.number, address_hash: address.hash, value: 1) + + assert response = + conn + |> post("/api/eth-rpc", params(api_params, [to_string(address.hash), "earliest"])) + |> json_response(200) + + assert response["error"] == "Balance not found" + end + + test "with a valid address that has an earliest balance", %{conn: conn, api_params: api_params} do + block = insert(:block, number: 0) + address = insert(:address) + + insert(:fetched_balance, block_number: block.number, address_hash: address.hash, value: 1) + + assert response = + conn + |> post("/api/eth-rpc", params(api_params, [to_string(address.hash), "earliest"])) + |> json_response(200) + + assert response["result"] == "0x1" + end + + test "with a valid address and no pending balance", %{conn: conn, api_params: api_params} do + block = insert(:block, number: 1, consensus: true) + address = insert(:address) + + insert(:fetched_balance, block_number: block.number, address_hash: address.hash, value: 1) + + assert response = + conn + |> post("/api/eth-rpc", params(api_params, [to_string(address.hash), "pending"])) + |> json_response(200) + + assert response["error"] == "Balance not found" + end + + test "with a valid address and a pending balance", %{conn: conn, api_params: api_params} do + block = insert(:block, number: 1, consensus: false) + address = insert(:address) + + insert(:fetched_balance, block_number: block.number, address_hash: address.hash, value: 1) + + assert response = + conn + |> post("/api/eth-rpc", params(api_params, [to_string(address.hash), "pending"])) + |> json_response(200) + + assert response["result"] == "0x1" + end + + test "with a valid address and a pending balance after a consensus block", %{conn: conn, api_params: api_params} do + insert(:block, number: 1, consensus: true) + block = insert(:block, number: 2, consensus: false) + address = insert(:address) + + insert(:fetched_balance, block_number: block.number, address_hash: address.hash, value: 1) + + assert response = + conn + |> post("/api/eth-rpc", params(api_params, [to_string(address.hash), "pending"])) + |> json_response(200) + + assert response["result"] == "0x1" + end + + test "with a block provided", %{conn: conn, api_params: api_params} do + address = insert(:address) + + insert(:fetched_balance, block_number: 1, address_hash: address.hash, value: 1) + insert(:fetched_balance, block_number: 2, address_hash: address.hash, value: 2) + insert(:fetched_balance, block_number: 3, address_hash: address.hash, value: 3) + + assert response = + conn + |> post("/api/eth-rpc", params(api_params, [to_string(address.hash), "2"])) + |> json_response(200) + + assert response["result"] == "0x2" + end + + test "with a block provided and no balance", %{conn: conn, api_params: api_params} do + address = insert(:address) + + insert(:fetched_balance, block_number: 3, address_hash: address.hash, value: 3) + + assert response = + conn + |> post("/api/eth-rpc", params(api_params, [to_string(address.hash), "2"])) + |> json_response(200) + + assert response["error"] == "Balance not found" + end + + test "with a batch of requests", %{conn: conn} do + address = insert(:address) + + insert(:fetched_balance, block_number: 1, address_hash: address.hash, value: 1) + insert(:fetched_balance, block_number: 2, address_hash: address.hash, value: 2) + insert(:fetched_balance, block_number: 3, address_hash: address.hash, value: 3) + + params = [ + %{"id" => 0, "params" => [to_string(address.hash), "1"], "jsonrpc" => "2.0", "method" => "eth_getBalance"}, + %{"id" => 1, "params" => [to_string(address.hash), "2"], "jsonrpc" => "2.0", "method" => "eth_getBalance"}, + %{"id" => 2, "params" => [to_string(address.hash), "3"], "jsonrpc" => "2.0", "method" => "eth_getBalance"} + ] + + assert response = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/eth-rpc", Jason.encode!(params)) + |> json_response(200) + + assert [ + %{"id" => 0, "result" => "0x1"}, + %{"id" => 1, "result" => "0x2"}, + %{"id" => 2, "result" => "0x3"} + ] = response + end + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/logs_controller_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/logs_controller_test.exs new file mode 100644 index 0000000..df82a98 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/logs_controller_test.exs @@ -0,0 +1,885 @@ +defmodule BlockScoutWeb.API.RPC.LogsControllerTest do + use BlockScoutWeb.ConnCase + + alias BlockScoutWeb.API.RPC.LogsController + alias Explorer.Chain.{Log, Transaction} + + @first_topic_hex_string_1 "0x7fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b65" + @first_topic_hex_string_2 "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef" + + @second_topic_hex_string_1 "0x00000000000000000000000098a9dc37d3650b5b30d6c12789b3881ee0b70c16" + @second_topic_hex_string_2 "0x000000000000000000000000e2680fd7cdbb04e9087a647ad4d023ef6c8fb4e2" + + @third_topic_hex_string_1 "0x0000000000000000000000005079fc00f00f30000e0c8c083801cfde000008b6" + + @fourth_topic_hex_string_1 "0x8c9b7729443a4444242342b2ca385a239a5c1d76a88473e1cd2ab0c70dd1b9c7" + @fourth_topic_hex_string_2 "0x232b688786cc0d24a11e07563c1bfa129537cec9385dc5b1fb8f86462977239b" + + defp topic(topic_hex_string) do + {:ok, topic} = Explorer.Chain.Hash.Full.cast(topic_hex_string) + topic + end + + describe "getLogs" do + test "without fromBlock, toBlock, address, and topic{x}", %{conn: conn} do + params = %{ + "module" => "logs", + "action" => "getLogs" + } + + expected_message = "Required query parameters missing: fromBlock, toBlock, address and/or topic{x}" + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["message"] == expected_message + assert response["status"] == "0" + assert Map.has_key?(response, "result") + refute response["result"] + assert :ok = ExJsonSchema.Validator.validate(getlogs_schema(), response) + end + + test "without fromBlock", %{conn: conn} do + params = %{ + "module" => "logs", + "action" => "getLogs", + "toBlock" => "10", + "address" => "0x8bf38d4764929064f2d4d3a56520a76ab3df415b" + } + + expected_message = "Required query parameters missing: fromBlock" + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["message"] == expected_message + assert response["status"] == "0" + assert Map.has_key?(response, "result") + refute response["result"] + assert :ok = ExJsonSchema.Validator.validate(getlogs_schema(), response) + end + + test "without toBlock", %{conn: conn} do + params = %{ + "module" => "logs", + "action" => "getLogs", + "fromBlock" => "5", + "address" => "0x8bf38d4764929064f2d4d3a56520a76ab3df415b" + } + + expected_message = "Required query parameters missing: toBlock" + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["message"] == expected_message + assert response["status"] == "0" + assert Map.has_key?(response, "result") + refute response["result"] + assert :ok = ExJsonSchema.Validator.validate(getlogs_schema(), response) + end + + test "without address and topic{x}", %{conn: conn} do + params = %{ + "module" => "logs", + "action" => "getLogs", + "fromBlock" => "5", + "toBlock" => "10" + } + + expected_message = "Required query parameters missing: address and/or topic{x}" + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["message"] == expected_message + assert response["status"] == "0" + assert Map.has_key?(response, "result") + refute response["result"] + assert :ok = ExJsonSchema.Validator.validate(getlogs_schema(), response) + end + + test "without topic{x}_{x}_opr", %{conn: conn} do + conditions = %{ + ["topic0", "topic1"] => "topic0_1_opr", + ["topic0", "topic2"] => "topic0_2_opr", + ["topic0", "topic3"] => "topic0_3_opr", + ["topic1", "topic2"] => "topic1_2_opr", + ["topic1", "topic3"] => "topic1_3_opr", + ["topic2", "topic3"] => "topic2_3_opr" + } + + for {[key1, key2], expectation} <- conditions do + params = %{ + "module" => "logs", + "action" => "getLogs", + "fromBlock" => "5", + "toBlock" => "10", + key1 => "some topic", + key2 => "some other topic" + } + + expected_message = "Required query parameters missing: #{expectation}" + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["message"] == expected_message + assert response["status"] == "0" + assert Map.has_key?(response, "result") + refute response["result"] + assert :ok = ExJsonSchema.Validator.validate(getlogs_schema(), response) + end + end + + test "without multiple topic{x}_{x}_opr", %{conn: conn} do + params = %{ + "module" => "logs", + "action" => "getLogs", + "fromBlock" => "5", + "toBlock" => "10", + "topic0" => "some topic", + "topic1" => "some other topic", + "topic2" => "some extra topic", + "topic3" => "some different topic" + } + + expected_message = + "Required query parameters missing: " <> + "topic0_1_opr, topic0_2_opr, topic0_3_opr, topic1_2_opr, topic1_3_opr, topic2_3_opr" + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["message"] == expected_message + assert response["status"] == "0" + assert Map.has_key?(response, "result") + refute response["result"] + assert :ok = ExJsonSchema.Validator.validate(getlogs_schema(), response) + end + + test "with invalid fromBlock", %{conn: conn} do + params = %{ + "module" => "logs", + "action" => "getLogs", + "fromBlock" => "invalid", + "toBlock" => "10", + "address" => "0x8bf38d4764929064f2d4d3a56520a76ab3df415b" + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["message"] =~ "Invalid fromBlock format" + assert response["status"] == "0" + assert Map.has_key?(response, "result") + refute response["result"] + assert :ok = ExJsonSchema.Validator.validate(getlogs_schema(), response) + end + + test "with invalid toBlock", %{conn: conn} do + params = %{ + "module" => "logs", + "action" => "getLogs", + "fromBlock" => "5", + "toBlock" => "invalid", + "address" => "0x8bf38d4764929064f2d4d3a56520a76ab3df415b" + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["message"] =~ "Invalid toBlock format" + assert response["status"] == "0" + assert Map.has_key?(response, "result") + refute response["result"] + assert :ok = ExJsonSchema.Validator.validate(getlogs_schema(), response) + end + + test "with an invalid address hash", %{conn: conn} do + params = %{ + "module" => "logs", + "action" => "getLogs", + "fromBlock" => "5", + "toBlock" => "10", + "address" => "badhash" + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["message"] =~ "Invalid address format" + assert response["status"] == "0" + assert Map.has_key?(response, "result") + refute response["result"] + assert :ok = ExJsonSchema.Validator.validate(getlogs_schema(), response) + end + + test "with invalid topic{x}_{x}_opr", %{conn: conn} do + conditions = %{ + ["topic0", "topic1"] => "topic0_1_opr", + ["topic0", "topic2"] => "topic0_2_opr", + ["topic0", "topic3"] => "topic0_3_opr", + ["topic1", "topic2"] => "topic1_2_opr", + ["topic1", "topic3"] => "topic1_3_opr", + ["topic2", "topic3"] => "topic2_3_opr" + } + + for {[key1, key2], expectation} <- conditions do + params = %{ + "module" => "logs", + "action" => "getLogs", + "fromBlock" => "5", + "toBlock" => "10", + key1 => "some topic", + key2 => "some other topic", + expectation => "invalid" + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["message"] =~ "Invalid #{expectation} format" + assert response["status"] == "0" + assert Map.has_key?(response, "result") + refute response["result"] + assert :ok = ExJsonSchema.Validator.validate(getlogs_schema(), response) + end + end + + test "with an address that doesn't exist", %{conn: conn} do + params = %{ + "module" => "logs", + "action" => "getLogs", + "fromBlock" => "5", + "toBlock" => "10", + "address" => "0x8bf38d4764929064f2d4d3a56520a76ab3df415b" + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["result"] == [] + assert response["status"] == "0" + assert response["message"] == "No logs found" + assert :ok = ExJsonSchema.Validator.validate(getlogs_schema(), response) + end + + test "with a valid contract address", %{conn: conn} do + contract_address = insert(:contract_address) + + transaction = + %Transaction{block: block} = + :transaction + |> insert(to_address: contract_address) + |> with_block() + + log = + insert(:log, + address: contract_address, + transaction: transaction, + block: block, + block_number: transaction.block_number + ) + + params = %{ + "module" => "logs", + "action" => "getLogs", + "fromBlock" => "#{block.number}", + "toBlock" => "#{block.number}", + "address" => "#{contract_address.hash}" + } + + expected_result = [ + %{ + "address" => "#{contract_address.hash}", + "topics" => get_topics(log), + "data" => "#{log.data}", + "blockNumber" => integer_to_hex(transaction.block_number), + "timeStamp" => datetime_to_hex(block.timestamp), + "gasPrice" => decimal_to_hex(transaction.gas_price.value), + "gasUsed" => decimal_to_hex(transaction.gas_used), + "logIndex" => integer_to_hex(log.index), + "transactionHash" => "#{transaction.hash}", + "transactionIndex" => integer_to_hex(transaction.index) + } + ] + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["result"] == expected_result + assert response["status"] == "1" + assert response["message"] == "OK" + assert :ok = ExJsonSchema.Validator.validate(getlogs_schema(), response) + end + + test "ignores logs with block below fromBlock", %{conn: conn} do + first_block = insert(:block) + second_block = insert(:block) + + contract_address = insert(:contract_address) + + transaction_block1 = + %Transaction{} = + :transaction + |> insert(to_address: contract_address) + |> with_block(first_block) + + transaction_block2 = + %Transaction{} = + :transaction + |> insert(to_address: contract_address) + |> with_block(second_block) + + insert(:log, + address: contract_address, + transaction: transaction_block1, + block: transaction_block1.block, + block_number: transaction_block1.block_number + ) + + insert(:log, + address: contract_address, + transaction: transaction_block2, + block: transaction_block2.block, + block_number: transaction_block2.block_number + ) + + params = %{ + "module" => "logs", + "action" => "getLogs", + "fromBlock" => "#{second_block.number}", + "toBlock" => "#{second_block.number}", + "address" => "#{contract_address.hash}" + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["status"] == "1" + assert response["message"] == "OK" + assert :ok = ExJsonSchema.Validator.validate(getlogs_schema(), response) + + [found_log] = response["result"] + + assert found_log["blockNumber"] == integer_to_hex(second_block.number) + assert found_log["transactionHash"] == "#{transaction_block2.hash}" + end + + test "ignores logs with block above toBlock", %{conn: conn} do + first_block = insert(:block) + second_block = insert(:block) + + contract_address = insert(:contract_address) + + transaction_block1 = + %Transaction{} = + :transaction + |> insert(to_address: contract_address) + |> with_block(first_block) + + transaction_block2 = + %Transaction{} = + :transaction + |> insert(to_address: contract_address) + |> with_block(second_block) + + insert(:log, + address: contract_address, + transaction: transaction_block1, + block: transaction_block1.block, + block_number: transaction_block1.block_number + ) + + insert(:log, + address: contract_address, + transaction: transaction_block2, + block: transaction_block2.block, + block_number: transaction_block2.block_number + ) + + params = %{ + "module" => "logs", + "action" => "getLogs", + "fromBlock" => "#{first_block.number}", + "toBlock" => "#{first_block.number}", + "address" => "#{contract_address.hash}" + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["status"] == "1" + assert response["message"] == "OK" + assert :ok = ExJsonSchema.Validator.validate(getlogs_schema(), response) + + [found_log] = response["result"] + + assert found_log["blockNumber"] == integer_to_hex(first_block.number) + assert found_log["transactionHash"] == "#{transaction_block1.hash}" + end + + test "with a valid topic{x}", %{conn: conn} do + contract_address = insert(:contract_address) + + transaction = + %Transaction{block: block} = + :transaction + |> insert() + |> with_block() + + log1_details = [ + address: contract_address, + transaction: transaction, + block: block, + block_number: block.number, + first_topic: topic(@first_topic_hex_string_1) + ] + + log2_details = [ + address: contract_address, + transaction: transaction, + block: block, + block_number: block.number, + first_topic: topic(@first_topic_hex_string_2) + ] + + log1 = insert(:log, log1_details) + _log2 = insert(:log, log2_details) + + params = %{ + "module" => "logs", + "action" => "getLogs", + "fromBlock" => "#{block.number}", + "toBlock" => "#{block.number}", + "topic0" => log1.first_topic + } + + expected_result = [ + %{ + "address" => "#{contract_address.hash}", + "topics" => get_topics(log1), + "data" => "#{log1.data}", + "blockNumber" => integer_to_hex(transaction.block_number), + "timeStamp" => datetime_to_hex(block.timestamp), + "gasPrice" => decimal_to_hex(transaction.gas_price.value), + "gasUsed" => decimal_to_hex(transaction.gas_used), + "logIndex" => integer_to_hex(log1.index), + "transactionHash" => "#{transaction.hash}", + "transactionIndex" => integer_to_hex(transaction.index) + } + ] + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["result"] == expected_result + assert response["status"] == "1" + assert response["message"] == "OK" + assert :ok = ExJsonSchema.Validator.validate(getlogs_schema(), response) + end + + test "with a topic{x} AND another", %{conn: conn} do + contract_address = insert(:contract_address) + + transaction = + %Transaction{block: block} = + :transaction + |> insert() + |> with_block() + + log1_details = [ + address: contract_address, + transaction: transaction, + block: block, + block_number: block.number, + first_topic: topic(@first_topic_hex_string_1), + second_topic: topic(@second_topic_hex_string_1) + ] + + log2_details = [ + address: contract_address, + transaction: transaction, + block: block, + block_number: block.number, + first_topic: topic(@first_topic_hex_string_2), + second_topic: topic(@second_topic_hex_string_2) + ] + + log1 = insert(:log, log1_details) + _log2 = insert(:log, log2_details) + + params = %{ + "module" => "logs", + "action" => "getLogs", + "fromBlock" => "#{block.number}", + "toBlock" => "#{block.number}", + "topic0" => log1.first_topic, + "topic1" => log1.second_topic, + "topic0_1_opr" => "and" + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert [found_log] = response["result"] + assert found_log["logIndex"] == integer_to_hex(log1.index) + assert found_log["topics"] == get_topics(log1) + assert response["status"] == "1" + assert response["message"] == "OK" + assert :ok = ExJsonSchema.Validator.validate(getlogs_schema(), response) + end + + test "with a topic{x} OR another", %{conn: conn} do + contract_address = insert(:contract_address) + + transaction = + %Transaction{block: block} = + :transaction + |> insert() + |> with_block() + + log1_details = [ + address: contract_address, + transaction: transaction, + block: block, + block_number: block.number, + first_topic: topic(@first_topic_hex_string_1), + second_topic: topic(@second_topic_hex_string_1) + ] + + log2_details = [ + address: contract_address, + transaction: transaction, + block: block, + block_number: block.number, + first_topic: topic(@first_topic_hex_string_2), + second_topic: topic(@second_topic_hex_string_2) + ] + + log1 = insert(:log, log1_details) + log2 = insert(:log, log2_details) + + params = %{ + "module" => "logs", + "action" => "getLogs", + "fromBlock" => "#{block.number}", + "toBlock" => "#{block.number}", + "topic0" => log1.first_topic, + "topic1" => log2.second_topic, + "topic0_1_opr" => "or" + } + + assert %{"result" => result} = + response = + conn + |> get("/api", params) + |> json_response(200) + + assert length(result) == 2 + assert response["status"] == "1" + assert response["message"] == "OK" + assert :ok = ExJsonSchema.Validator.validate(getlogs_schema(), response) + end + + test "with all available 'topic{x}'s and 'topic{x}_{x}_opr's", %{conn: conn} do + contract_address = insert(:contract_address) + + transaction = + %Transaction{block: block} = + :transaction + |> insert() + |> with_block() + + log1_details = [ + address: contract_address, + transaction: transaction, + block: block, + block_number: block.number, + first_topic: topic(@first_topic_hex_string_1), + second_topic: topic(@second_topic_hex_string_1), + third_topic: topic(@third_topic_hex_string_1), + fourth_topic: topic(@fourth_topic_hex_string_1) + ] + + log2_details = [ + address: contract_address, + transaction: transaction, + block: block, + block_number: block.number, + first_topic: topic(@first_topic_hex_string_1), + second_topic: topic(@second_topic_hex_string_1), + third_topic: topic(@third_topic_hex_string_1), + fourth_topic: topic(@fourth_topic_hex_string_2) + ] + + log1 = insert(:log, log1_details) + log2 = insert(:log, log2_details) + + params = %{ + "module" => "logs", + "action" => "getLogs", + "fromBlock" => "#{block.number}", + "toBlock" => "#{block.number}", + "topic0" => log1.first_topic, + "topic1" => log1.second_topic, + "topic2" => log1.third_topic, + "topic3" => log2.fourth_topic, + "topic0_1_opr" => "and", + "topic0_2_opr" => "and", + "topic0_3_opr" => "or", + "topic1_2_opr" => "and", + "topic1_3_opr" => "or", + "topic2_3_opr" => "or" + } + + assert %{"result" => result} = + response = + conn + |> get("/api", params) + |> json_response(200) + + assert length(result) == 2 + assert response["status"] == "1" + assert response["message"] == "OK" + assert :ok = ExJsonSchema.Validator.validate(getlogs_schema(), response) + end + end + + describe "fetch_required_params/1" do + test "without any required params" do + params = %{} + + {_, {:error, missing_params}} = LogsController.fetch_required_params(params) + + assert missing_params == ["fromBlock", "toBlock", "address and/or topic{x}"] + end + + test "without fromBlock" do + params = %{ + "toBlock" => "5", + "address" => "some address" + } + + {_, {:error, [missing_param]}} = LogsController.fetch_required_params(params) + + assert missing_param == "fromBlock" + end + + test "without toBlock" do + params = %{ + "fromBlock" => "5", + "address" => "some address" + } + + {_, {:error, [missing_param]}} = LogsController.fetch_required_params(params) + + assert missing_param == "toBlock" + end + + test "without fromBlock or toBlock" do + params = %{ + "address" => "some address" + } + + {_, {:error, missing_params}} = LogsController.fetch_required_params(params) + + assert missing_params == ["fromBlock", "toBlock"] + end + + test "without address or topic{x}" do + params = %{ + "toBlock" => "5", + "fromBlock" => "5" + } + + {_, {:error, [missing_param]}} = LogsController.fetch_required_params(params) + + assert missing_param == "address and/or topic{x}" + end + + test "with address" do + params = %{ + "fromBlock" => "5", + "toBlock" => "5", + "address" => "some address" + } + + {_, {:ok, fetched_params}} = LogsController.fetch_required_params(params) + + assert fetched_params == params + end + + test "with topic{x}" do + for topic <- ["topic0", "topic1", "topic2", "topic3"] do + params = %{ + "fromBlock" => "5", + "toBlock" => "5", + topic => "some topic" + } + + {_, {:ok, fetched_params}} = LogsController.fetch_required_params(params) + + assert fetched_params == params + end + end + + test "with address and topic{x}" do + params = %{ + "fromBlock" => "5", + "toBlock" => "5", + "address" => "some address", + "topic0" => "some topic" + } + + {_, {:ok, fetched_params}} = LogsController.fetch_required_params(params) + + assert fetched_params == params + end + end + + describe "to_valid_format/1" do + test "with invalid fromBlock" do + params = %{"fromBlock" => "invalid"} + + assert {_, {:error, "fromBlock"}} = LogsController.to_valid_format(params) + end + + test "with invalid toBlock" do + params = %{ + "fromBlock" => "5", + "toBlock" => "invalid" + } + + assert {_, {:error, "toBlock"}} = LogsController.to_valid_format(params) + end + + test "with invalid address" do + params = %{ + "fromBlock" => "5", + "toBlock" => "10", + "address" => "invalid" + } + + assert {_, {:error, "address"}} = LogsController.to_valid_format(params) + end + + test "address_hash returns as nil when missing" do + params = %{ + "fromBlock" => "5", + "toBlock" => "10" + } + + assert {_, {:ok, validated_params}} = LogsController.to_valid_format(params) + refute validated_params.address_hash + end + + test "fromBlock and toBlock support use of 'latest'" do + params = %{ + "fromBlock" => "latest", + "toBlock" => "latest" + } + + # Without any blocks in the db we want to return {:error, :not_found} + assert {_, {:error, :not_found}} = LogsController.to_valid_format(params) + + # We insert a block, try again, and assert 'latest' points to the latest + # block number. + insert(:block) + {:ok, max_consensus_block_number} = Explorer.Chain.max_consensus_block_number() + + assert {_, {:ok, validated_params}} = LogsController.to_valid_format(params) + assert validated_params.from_block == max_consensus_block_number + assert validated_params.to_block == max_consensus_block_number + end + end + + defp get_topics(%Log{ + first_topic: first_topic, + second_topic: second_topic, + third_topic: third_topic, + fourth_topic: fourth_topic + }) do + [ + first_topic && Explorer.Chain.Hash.to_string(first_topic), + second_topic && Explorer.Chain.Hash.to_string(second_topic), + third_topic && Explorer.Chain.Hash.to_string(third_topic), + fourth_topic && Explorer.Chain.Hash.to_string(fourth_topic) + ] + end + + defp integer_to_hex(integer), do: "0x" <> String.downcase(Integer.to_string(integer, 16)) + + defp decimal_to_hex(decimal) do + decimal + |> Decimal.to_integer() + |> integer_to_hex() + end + + defp datetime_to_hex(datetime) do + datetime + |> DateTime.to_unix() + |> integer_to_hex() + end + + defp getlogs_schema do + ExJsonSchema.Schema.resolve(%{ + "type" => "object", + "properties" => %{ + "message" => %{"type" => "string"}, + "status" => %{"type" => "string"}, + "result" => %{ + "type" => ["array", "null"], + "items" => %{ + "type" => "object", + "properties" => %{ + "address" => %{"type" => "string"}, + "topics" => %{"type" => "array", "items" => %{"type" => ["string", "null"]}}, + "data" => %{"type" => "string"}, + "blockNumber" => %{"type" => "string"}, + "timeStamp" => %{"type" => "string"}, + "gasPrice" => %{"type" => "string"}, + "gasUsed" => %{"type" => "string"}, + "logIndex" => %{"type" => "string"}, + "transactionHash" => %{"type" => "string"}, + "transactionIndex" => %{"type" => "string"} + } + } + } + } + }) + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/rpc_translator_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/rpc_translator_test.exs new file mode 100644 index 0000000..bf410d6 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/rpc_translator_test.exs @@ -0,0 +1,89 @@ +defmodule BlockScoutWeb.API.RPC.RPCTranslatorTest do + use BlockScoutWeb.ConnCase + + alias BlockScoutWeb.API.RPC.RPCTranslator + alias Plug.Conn + + defmodule TestController do + use BlockScoutWeb, :controller + + def test_action(conn, _) do + json(conn, %{}) + end + end + + setup %{conn: conn} do + conn = Phoenix.Controller.accepts(conn, ["json"]) + {:ok, conn: conn} + end + + test "init/1" do + assert RPCTranslator.init([]) == [] + end + + describe "call" do + test "with a bad module", %{conn: conn} do + conn = %Conn{conn | params: %{"module" => "test", "action" => "test"}, request_path: "/api"} + + result = RPCTranslator.call(conn, %{}) + assert result.halted + assert response = json_response(result, 400) + assert response["message"] =~ "Unknown module" + assert response["status"] == "0" + assert Map.has_key?(response, "result") + refute response["result"] + end + + test "with a bad action atom", %{conn: conn} do + conn = %Conn{ + conn + | params: %{"module" => "test", "action" => "some_atom_that_should_not_exist"}, + request_path: "/api" + } + + result = RPCTranslator.call(conn, %{"test" => {TestController, []}}) + assert result.halted + assert response = json_response(result, 400) + assert response["message"] =~ "Unknown action" + assert response["status"] == "0" + assert Map.has_key?(response, "result") + refute response["result"] + end + + test "with an invalid controller action", %{conn: conn} do + conn = %Conn{conn | params: %{"module" => "test", "action" => "index"}, request_path: "/api"} + + result = RPCTranslator.call(conn, %{"test" => {TestController, []}}) + assert result.halted + assert response = json_response(result, 400) + assert response["message"] =~ "Unknown action" + assert response["status"] == "0" + assert Map.has_key?(response, "result") + refute response["result"] + end + + test "with missing params", %{conn: conn} do + result = RPCTranslator.call(conn, %{"test" => {TestController, []}}) + assert result.halted + assert response = json_response(result, 400) + assert response["message"] =~ "'module' and 'action' are required" + assert response["status"] == "0" + assert Map.has_key?(response, "result") + refute response["result"] + end + + test "with a valid request", %{conn: conn} do + conn = %Conn{conn | params: %{"module" => "test", "action" => "test_action"}, request_path: "/api"} + + result = RPCTranslator.call(conn, %{"test" => {TestController, []}}) + assert json_response(result, 200) == %{} + end + + test "allow multiple '/' before api", %{conn: conn} do + conn = %Conn{conn | params: %{"module" => "test", "action" => "test_action"}, request_path: "//api"} + + result = RPCTranslator.call(conn, %{"test" => {TestController, []}}) + assert json_response(result, 200) == %{} + end + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/stats_controller_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/stats_controller_test.exs new file mode 100644 index 0000000..c6c0822 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/stats_controller_test.exs @@ -0,0 +1,312 @@ +defmodule BlockScoutWeb.API.RPC.StatsControllerTest do + use BlockScoutWeb.ConnCase + + import Mox + + alias Explorer.Market.Fetcher.Coin + alias Explorer.Market.Token + alias Explorer.Market.Source.TestSource + + describe "tokensupply" do + test "with missing contract address", %{conn: conn} do + params = %{ + "module" => "stats", + "action" => "tokensupply" + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["message"] =~ "contract address is required" + assert response["status"] == "0" + assert Map.has_key?(response, "result") + refute response["result"] + assert :ok = ExJsonSchema.Validator.validate(tokensupply_schema(), response) + end + + test "with an invalid contract address hash", %{conn: conn} do + params = %{ + "module" => "stats", + "action" => "tokensupply", + "contractaddress" => "badhash" + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["message"] =~ "Invalid contract address format" + assert response["status"] == "0" + assert Map.has_key?(response, "result") + refute response["result"] + assert :ok = ExJsonSchema.Validator.validate(tokensupply_schema(), response) + end + + test "with a contract address that doesn't exist", %{conn: conn} do + params = %{ + "module" => "stats", + "action" => "tokensupply", + "contractaddress" => "0x8bf38d4764929064f2d4d3a56520a76ab3df415b" + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["message"] =~ "Contract address not found" + assert response["status"] == "0" + assert Map.has_key?(response, "result") + refute response["result"] + assert :ok = ExJsonSchema.Validator.validate(tokensupply_schema(), response) + end + + test "with valid contract address", %{conn: conn} do + token = insert(:token) + + params = %{ + "module" => "stats", + "action" => "tokensupply", + "contractaddress" => to_string(token.contract_address_hash) + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["result"] == to_string(token.total_supply) + assert response["status"] == "1" + assert response["message"] == "OK" + assert :ok = ExJsonSchema.Validator.validate(tokensupply_schema(), response) + end + + test "with valid contract address and cmc format", %{conn: conn} do + token = insert(:token, total_supply: 110_052_089_716_627_912_057_222_572) + + params = %{ + "module" => "stats", + "action" => "tokensupply", + "contractaddress" => to_string(token.contract_address_hash), + "cmc" => "true" + } + + assert response = + conn + |> get("/api", params) + |> text_response(200) + + assert response == "110052089.716627912" + end + + test "with custom decimals and cmc format", %{conn: conn} do + token = + insert(:token, + total_supply: 1_234_567_890, + decimals: 6 + ) + + params = %{ + "module" => "stats", + "action" => "tokensupply", + "contractaddress" => to_string(token.contract_address_hash), + "cmc" => "true" + } + + assert response = + conn + |> get("/api", params) + |> text_response(200) + + assert response == "1234.567890000" + end + end + + test "with null decimals and cmc format", %{conn: conn} do + token = + insert(:token, + total_supply: 1_234_567_890, + decimals: nil + ) + + params = %{ + "module" => "stats", + "action" => "tokensupply", + "contractaddress" => to_string(token.contract_address_hash), + "cmc" => "true" + } + + assert response = + conn + |> get("/api", params) + |> text_response(200) + + assert response == "1234567890.000000000" + end + + describe "ethsupplyexchange" do + test "returns total supply from exchange", %{conn: conn} do + params = %{ + "module" => "stats", + "action" => "ethsupplyexchange" + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["result"] == "252460800000000000000000000" + assert response["status"] == "1" + assert response["message"] == "OK" + assert :ok = ExJsonSchema.Validator.validate(ethsupplyexchange_schema(), response) + end + end + + # todo: Temporarily disable this test because of unstable work in CI + # describe "ethsupply" do + # test "returns total supply from DB", %{conn: conn} do + # params = %{ + # "module" => "stats", + # "action" => "ethsupply" + # } + + # assert response = + # conn + # |> get("/api", params) + # |> json_response(200) + + # assert response["result"] == "0" + # assert response["status"] == "1" + # assert response["message"] == "OK" + # assert :ok = ExJsonSchema.Validator.validate(ethsupply_schema(), response) + # end + # end + + # describe "coinsupply" do + # test "returns total supply minus a burnt number from DB in coins denomination", %{conn: conn} do + # params = %{ + # "module" => "stats", + # "action" => "coinsupply" + # } + + # assert response = + # conn + # |> get("/api", params) + # |> json_response(200) + + # assert response == 0.0 + # end + # end + + describe "coinprice" do + setup :set_mox_global + + setup do + # Use TestSource mock for this test set + coin_fetcher_configuration = Application.get_env(:explorer, Coin) + market_source_configuration = Application.get_env(:explorer, Explorer.Market.Source) + + Application.put_env(:explorer, Explorer.Market.Source, native_coin_source: TestSource) + Application.put_env(:explorer, Coin, Keyword.merge(coin_fetcher_configuration, table_name: :rates, enabled: true)) + + Coin.init([]) + + on_exit(fn -> + Application.put_env(:explorer, Coin, coin_fetcher_configuration) + Application.put_env(:explorer, Explorer.Market.Source, market_source_configuration) + end) + + :ok + end + + test "returns the configured coin's price information", %{conn: conn} do + symbol = Application.get_env(:explorer, :coin) + + eth = %Token{ + available_supply: Decimal.new("1000000.0"), + total_supply: Decimal.new("1000000.0"), + btc_value: Decimal.new("1.000"), + last_updated: DateTime.utc_now(), + market_cap: Decimal.new("1000000.0"), + tvl: Decimal.new("2000000.0"), + name: "test", + symbol: symbol, + fiat_value: Decimal.new("1.0"), + volume_24h: Decimal.new("1000.0"), + image_url: nil + } + + Coin.handle_info({nil, {{:ok, eth}, false}}, %{}) + + params = %{ + "module" => "stats", + "action" => "coinprice" + } + + expected_timestamp = eth.last_updated |> DateTime.to_unix() |> to_string() + + expected_result = %{ + "coin_btc" => to_string(eth.btc_value), + "coin_btc_timestamp" => expected_timestamp, + "coin_usd" => to_string(eth.fiat_value), + "coin_usd_timestamp" => expected_timestamp + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["result"] == expected_result + assert response["status"] == "1" + assert response["message"] == "OK" + assert :ok = ExJsonSchema.Validator.validate(coinprice_schema(), response) + end + end + + defp tokensupply_schema do + resolve_schema(%{ + "type" => ["string", "null"] + }) + end + + # defp ethsupply_schema do + # resolve_schema(%{ + # "type" => ["string", "null"] + # }) + # end + + defp ethsupplyexchange_schema do + resolve_schema(%{ + "type" => ["string", "null"] + }) + end + + defp coinprice_schema do + resolve_schema(%{ + "type" => "object", + "properties" => %{ + "coin_btc" => %{"type" => "string"}, + "coin_btc_timestamp" => %{"type" => "string"}, + "coin_usd" => %{"type" => "string"}, + "coin_usd_timestamp" => %{"type" => "string"} + } + }) + end + + defp resolve_schema(result) do + %{ + "type" => "object", + "properties" => %{ + "message" => %{"type" => "string"}, + "status" => %{"type" => "string"} + } + } + |> put_in(["properties", "result"], result) + |> ExJsonSchema.Schema.resolve() + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/token_controller_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/token_controller_test.exs new file mode 100644 index 0000000..2257fe1 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/token_controller_test.exs @@ -0,0 +1,109 @@ +defmodule BlockScoutWeb.API.RPC.TokenControllerTest do + use BlockScoutWeb.ConnCase + + describe "gettoken" do + test "with missing contract address", %{conn: conn} do + params = %{ + "module" => "token", + "action" => "getToken" + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["message"] =~ "contract address is required" + assert response["status"] == "0" + assert Map.has_key?(response, "result") + refute response["result"] + end + + test "with an invalid contract address hash", %{conn: conn} do + params = %{ + "module" => "token", + "action" => "getToken", + "contractaddress" => "badhash" + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["message"] =~ "Invalid contract address hash" + assert response["status"] == "0" + assert Map.has_key?(response, "result") + refute response["result"] + end + + test "with a contract address that doesn't exist", %{conn: conn} do + params = %{ + "module" => "token", + "action" => "getToken", + "contractaddress" => "0x8bf38d4764929064f2d4d3a56520a76ab3df415b" + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["message"] =~ "Contract address not found" + assert response["status"] == "0" + assert Map.has_key?(response, "result") + refute response["result"] + end + + test "response includes all required fields", %{conn: conn} do + token = insert(:token) + + params = %{ + "module" => "token", + "action" => "getToken", + "contractaddress" => to_string(token.contract_address_hash) + } + + expected_result = %{ + "name" => token.name, + "symbol" => token.symbol, + "totalSupply" => to_string(token.total_supply), + "decimals" => to_string(token.decimals), + "type" => token.type, + "cataloged" => token.cataloged, + "contractAddress" => to_string(token.contract_address_hash) + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["result"] == expected_result + assert response["status"] == "1" + assert response["message"] == "OK" + end + end + + # defp gettoken_schema do + # ExJsonSchema.Schema.resolve(%{ + # "type" => "object", + # "properties" => %{ + # "message" => %{"type" => "string"}, + # "status" => %{"type" => "string"}, + # "result" => %{ + # "type" => "object", + # "properties" => %{ + # "name" => %{"type" => "string"}, + # "symbol" => %{"type" => "string"}, + # "totalSupply" => %{"type" => "string"}, + # "decimals" => %{"type" => "string"}, + # "type" => %{"type" => "string"}, + # "cataloged" => %{"type" => "string"}, + # "contractAddress" => %{"type" => "string"} + # } + # } + # } + # }) + # end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/transaction_controller_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/transaction_controller_test.exs new file mode 100644 index 0000000..0ecd2fd --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/transaction_controller_test.exs @@ -0,0 +1,870 @@ +defmodule BlockScoutWeb.API.RPC.TransactionControllerTest do + use BlockScoutWeb.ConnCase + + import Mox + + @moduletag capture_log: true + + @first_topic_hex_string_1 "0x7fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b65" + @second_topic_hex_string_1 "0x00000000000000000000000098a9dc37d3650b5b30d6c12789b3881ee0b70c16" + + setup :verify_on_exit! + + defp topic(topic_hex_string) do + {:ok, topic} = Explorer.Chain.Hash.Full.cast(topic_hex_string) + topic + end + + describe "gettxreceiptstatus" do + test "with missing txhash", %{conn: conn} do + params = %{ + "module" => "transaction", + "action" => "gettxreceiptstatus" + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + schema = resolve_schema() + assert ExJsonSchema.Validator.valid?(schema, response) + assert response["message"] =~ "txhash is required" + assert response["status"] == "0" + assert Map.has_key?(response, "result") + refute response["result"] + end + + test "with an invalid txhash", %{conn: conn} do + params = %{ + "module" => "transaction", + "action" => "gettxreceiptstatus", + "txhash" => "badhash" + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + schema = resolve_schema() + assert ExJsonSchema.Validator.valid?(schema, response) + assert response["message"] =~ "Invalid txhash format" + assert response["status"] == "0" + assert Map.has_key?(response, "result") + refute response["result"] + end + + test "with a txhash that doesn't exist", %{conn: conn} do + params = %{ + "module" => "transaction", + "action" => "gettxreceiptstatus", + "txhash" => "0x40eb908387324f2b575b4879cd9d7188f69c8fc9d87c901b9e2daaea4b442170" + } + + schema = + resolve_schema(%{ + "type" => "object", + "properties" => %{ + "status" => %{"type" => "string"} + } + }) + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert ExJsonSchema.Validator.valid?(schema, response) + assert response["result"] == %{"status" => ""} + assert response["status"] == "1" + assert response["message"] == "OK" + end + + test "with a txhash with ok status", %{conn: conn} do + block = insert(:block) + + transaction = + :transaction + |> insert() + |> with_block(block, status: :ok) + + params = %{ + "module" => "transaction", + "action" => "gettxreceiptstatus", + "txhash" => "#{transaction.hash}" + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["result"] == %{"status" => "1"} + assert response["status"] == "1" + assert response["message"] == "OK" + end + + test "with a txhash with error status", %{conn: conn} do + block = insert(:block) + + transaction = + :transaction + |> insert() + |> with_block(block, status: :error) + + params = %{ + "module" => "transaction", + "action" => "gettxreceiptstatus", + "txhash" => "#{transaction.hash}" + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["result"] == %{"status" => "0"} + assert response["status"] == "1" + assert response["message"] == "OK" + end + + test "with a txhash with nil status", %{conn: conn} do + transaction = insert(:transaction, status: nil) + + params = %{ + "module" => "transaction", + "action" => "gettxreceiptstatus", + "txhash" => "#{transaction.hash}" + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["result"] == %{"status" => ""} + assert response["status"] == "1" + assert response["message"] == "OK" + end + end + + describe "getstatus" do + test "with missing txhash", %{conn: conn} do + params = %{ + "module" => "transaction", + "action" => "getstatus" + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + schema = resolve_schema() + assert ExJsonSchema.Validator.valid?(schema, response) + assert response["message"] =~ "txhash is required" + assert response["status"] == "0" + assert Map.has_key?(response, "result") + refute response["result"] + end + + test "with an invalid txhash", %{conn: conn} do + params = %{ + "module" => "transaction", + "action" => "getstatus", + "txhash" => "badhash" + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + schema = resolve_schema() + assert ExJsonSchema.Validator.valid?(schema, response) + assert response["message"] =~ "Invalid txhash format" + assert response["status"] == "0" + assert Map.has_key?(response, "result") + refute response["result"] + end + + test "with a txhash that doesn't exist", %{conn: conn} do + params = %{ + "module" => "transaction", + "action" => "getstatus", + "txhash" => "0x40eb908387324f2b575b4879cd9d7188f69c8fc9d87c901b9e2daaea4b442170" + } + + expected_result = %{ + "isError" => "0", + "errDescription" => "" + } + + schema = + resolve_schema(%{ + "type" => "object", + "properties" => %{ + "isError" => %{"type" => "string"}, + "errDescription" => %{"type" => "string"} + } + }) + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert ExJsonSchema.Validator.valid?(schema, response) + assert response["result"] == expected_result + assert response["status"] == "1" + assert response["message"] == "OK" + end + + test "with a txhash with ok status", %{conn: conn} do + block = insert(:block) + + transaction = + :transaction + |> insert() + |> with_block(block, status: :ok) + + params = %{ + "module" => "transaction", + "action" => "getstatus", + "txhash" => "#{transaction.hash}" + } + + expected_result = %{ + "isError" => "0", + "errDescription" => "" + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["result"] == expected_result + assert response["status"] == "1" + assert response["message"] == "OK" + end + + test "with a txhash with error", %{conn: conn} do + error = "some error" + + transaction_details = [ + status: :error, + error: error + ] + + transaction = + :transaction + |> insert() + |> with_block(transaction_details) + + internal_transaction_details = [ + transaction: transaction, + index: 0, + type: :reward, + error: error, + block_hash: transaction.block_hash, + block_index: 0 + ] + + insert(:internal_transaction, internal_transaction_details) + + params = %{ + "module" => "transaction", + "action" => "getstatus", + "txhash" => "#{transaction.hash}" + } + + expected_result = %{ + "isError" => "1", + "errDescription" => error + } + + response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["result"] == expected_result + assert response["status"] == "1" + assert response["message"] == "OK" + end + + test "with a txhash with failed status but awaiting internal transactions", %{conn: conn} do + transaction_details = [ + status: :error, + error: nil + ] + + transaction = + :transaction + |> insert() + |> with_block(transaction_details) + + params = %{ + "module" => "transaction", + "action" => "getstatus", + "txhash" => "#{transaction.hash}" + } + + expected_result = %{ + "isError" => "1", + "errDescription" => "awaiting internal transactions" + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["result"] == expected_result + assert response["status"] == "1" + assert response["message"] == "OK" + end + + test "with a txhash with nil status", %{conn: conn} do + transaction = insert(:transaction, status: nil) + + params = %{ + "module" => "transaction", + "action" => "getstatus", + "txhash" => "#{transaction.hash}" + } + + expected_result = %{ + "isError" => "0", + "errDescription" => "" + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["result"] == expected_result + assert response["status"] == "1" + assert response["message"] == "OK" + end + end + + describe "gettxinfo" do + test "with missing txhash", %{conn: conn} do + params = %{ + "module" => "transaction", + "action" => "gettxinfo" + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + schema = resolve_schema() + assert ExJsonSchema.Validator.valid?(schema, response) + assert response["message"] =~ "txhash is required" + assert response["status"] == "0" + assert Map.has_key?(response, "result") + refute response["result"] + end + + test "with an invalid txhash", %{conn: conn} do + params = %{ + "module" => "transaction", + "action" => "gettxinfo", + "txhash" => "badhash" + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["message"] =~ "Invalid txhash format" + assert response["status"] == "0" + assert Map.has_key?(response, "result") + refute response["result"] + end + + test "with a txhash that doesn't exist", %{conn: conn} do + params = %{ + "module" => "transaction", + "action" => "gettxinfo", + "txhash" => "0x40eb908387324f2b575b4879cd9d7188f69c8fc9d87c901b9e2daaea4b442170" + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["message"] =~ "Transaction not found" + assert response["status"] == "0" + assert Map.has_key?(response, "result") + refute response["result"] + end + + test "paginates logs", %{conn: conn} do + block = insert(:block, hash: "0x30d522bcf2d8e0cabc286e6e40623c475c3bc05d0ec484ea239c103b1ac0ded9", number: 99) + + transaction = + :transaction + |> insert(hash: "0x13b6bb8e06322096dc83e8d7e6332ca19919ea642212cd259c6b20e7523a0599") + |> with_block(block, status: :ok) + + address = insert(:address) + + Enum.each(1..100, fn _ -> + insert(:log, + address: address, + transaction: transaction, + first_topic: topic(@first_topic_hex_string_1), + second_topic: topic(@second_topic_hex_string_1), + block: block, + block_number: block.number + ) + end) + + params1 = %{ + "module" => "transaction", + "action" => "gettxinfo", + "txhash" => "#{transaction.hash}" + } + + schema = + resolve_schema(%{ + "type" => "object", + "properties" => %{ + "next_page_params" => %{ + "type" => ["object", "null"], + "properties" => %{ + "action" => %{"type" => "string"}, + "index" => %{"type" => "number"}, + "module" => %{"type" => "string"}, + "txhash" => %{"type" => "string"} + } + }, + "logs" => %{ + "type" => "array", + "items" => %{"type" => "object"} + } + } + }) + + assert response1 = + conn + |> get("/api", params1) + |> json_response(200) + + assert ExJsonSchema.Validator.valid?(schema, response1) + assert response1["status"] == "1" + assert response1["message"] == "OK" + + assert %{ + "action" => "gettxinfo", + "index" => _, + "module" => "transaction", + "txhash" => _ + } = response1["result"]["next_page_params"] + + params2 = response1["result"]["next_page_params"] + + assert response2 = + conn + |> get("/api", params2) + |> json_response(200) + + assert ExJsonSchema.Validator.valid?(schema, response2) + assert response2["status"] == "1" + assert response2["message"] == "OK" + assert is_nil(response2["result"]["next_page_params"]) + assert response1["result"]["logs"] != response2["result"]["logs"] + end + + test "with a txhash with ok status", %{conn: conn} do + block = insert(:block) + + transaction = + :transaction + |> insert() + |> with_block(block, status: :ok) + + address = insert(:address) + + log = + insert(:log, + address: address, + transaction: transaction, + first_topic: topic(@first_topic_hex_string_1), + second_topic: topic(@second_topic_hex_string_1), + block: block, + block_number: block.number + ) + + params = %{ + "module" => "transaction", + "action" => "gettxinfo", + "txhash" => "#{transaction.hash}" + } + + expected_result = %{ + "hash" => "#{transaction.hash}", + "timeStamp" => "#{DateTime.to_unix(transaction.block.timestamp)}", + "blockNumber" => "#{transaction.block_number}", + "confirmations" => "0", + "success" => true, + "from" => "#{transaction.from_address_hash}", + "to" => "#{transaction.to_address_hash}", + "value" => "#{transaction.value.value}", + "input" => "#{transaction.input}", + "gasLimit" => "#{transaction.gas}", + "gasUsed" => "#{transaction.gas_used}", + "gasPrice" => "#{transaction.gas_price.value}", + "logs" => [ + %{ + "address" => "#{address.hash}", + "data" => "#{log.data}", + "topics" => [@first_topic_hex_string_1, @second_topic_hex_string_1, nil, nil], + "index" => "#{log.index}" + } + ], + "next_page_params" => nil, + "revertReason" => "" + } + + schema = + resolve_schema(%{ + "type" => "object", + "properties" => %{ + "hash" => %{"type" => "string"}, + "timeStamp" => %{"type" => "string"}, + "blockNumber" => %{"type" => "string"}, + "confirmations" => %{"type" => "string"}, + "success" => %{"type" => "boolean"}, + "from" => %{"type" => "string"}, + "to" => %{"type" => "string"}, + "value" => %{"type" => "string"}, + "input" => %{"type" => "string"}, + "gasLimit" => %{"type" => "string"}, + "gasUsed" => %{"type" => "string"}, + "gasPrice" => %{"type" => "string"}, + "logs" => %{ + "type" => "array", + "items" => %{ + "type" => "object", + "properties" => %{ + "address" => %{"type" => "string"}, + "data" => %{"type" => "string"}, + "topics" => %{ + "type" => "array", + "items" => %{"type" => ["string", "null"]} + }, + "index" => %{"type" => "string"} + } + } + }, + "next_page_params" => %{ + "type" => ["object", "null"], + "properties" => %{ + "action" => %{"type" => "string"}, + "index" => %{"type" => "number"}, + "module" => %{"type" => "string"}, + "txhash" => %{"type" => "string"} + } + } + } + }) + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert ExJsonSchema.Validator.valid?(schema, response) + assert response["result"] == expected_result + assert response["status"] == "1" + assert response["message"] == "OK" + end + + test "with a txhash with revert reason from DB", %{conn: conn} do + block = insert(:block, number: 100) + + transaction = + :transaction + |> insert(revert_reason: "No credit of that type") + |> with_block(block) + + insert(:address) + + params = %{ + "module" => "transaction", + "action" => "gettxinfo", + "txhash" => "#{transaction.hash}" + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["result"]["revertReason"] == "No credit of that type" + assert response["status"] == "1" + assert response["message"] == "OK" + end + + test "with a txhash with empty revert reason from DB", %{conn: conn} do + block = insert(:block, number: 100) + + transaction = + :transaction + |> insert(revert_reason: "") + |> with_block(block) + + insert(:address) + + params = %{ + "module" => "transaction", + "action" => "gettxinfo", + "txhash" => "#{transaction.hash}" + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["result"]["revertReason"] == "" + assert response["status"] == "1" + assert response["message"] == "OK" + end + + test "with a txhash with revert reason from the archive node", %{conn: conn} do + block = insert(:block, number: 100, hash: "0x3e51328bccedee581e8ba35190216a61a5d67fd91ca528f3553142c0c7d18391") + + transaction = + :transaction + |> insert( + error: "Reverted", + status: :error, + block_hash: block.hash, + block_number: block.number, + cumulative_gas_used: 884_322, + gas_used: 106_025, + index: 0, + hash: "0xac2a7dab94d965893199e7ee01649e2d66f0787a4c558b3118c09e80d4df8269" + ) + + insert(:address) + + # Error("No credit of that type") + hex_reason = + "0x08c379a0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000164e6f20637265646974206f662074686174207479706500000000000000000000" + + expect( + EthereumJSONRPC.Mox, + :json_rpc, + fn + [%{method: "debug_traceTransaction"}], _options -> + {:ok, + [ + %{ + id: 0, + result: %{ + "from" => "0x6a17ca3bbf83764791f4a9f2b4dbbaebbc8b3e0d", + "gas" => "0x5208", + "gasUsed" => "0x5208", + "input" => "0x01", + "output" => hex_reason, + "to" => "0x7ed1e469fcb3ee19c0366d829e291451be638e59", + "type" => "CALL", + "value" => "0x86b3" + } + } + ]} + + [%{method: "trace_replayTransaction"}], _options -> + {:ok, + [ + %{ + id: 0, + result: %{ + "output" => "0x", + "stateDiff" => nil, + "trace" => [ + %{ + "action" => %{ + "callType" => "call", + "from" => "0x6a17ca3bbf83764791f4a9f2b4dbbaebbc8b3e0d", + "gas" => "0x5208", + "input" => "0x01", + "to" => "0x7ed1e469fcb3ee19c0366d829e291451be638e59", + "value" => "0x86b3" + }, + "error" => "Reverted", + "result" => %{ + "gasUsed" => "0x5208", + "output" => hex_reason + }, + "subtraces" => 0, + "traceAddress" => [], + "type" => "call" + } + ], + "transactionHash" => "0xdf5574290913659a1ac404ccf2d216c40587f819400a52405b081dda728ac120", + "vmTrace" => nil + } + } + ]} + + %{method: "eth_call"}, _options -> + {:error, + %{ + code: 3, + data: hex_reason, + message: "execution reverted" + }} + end + ) + + params = %{ + "module" => "transaction", + "action" => "gettxinfo", + "txhash" => "#{transaction.hash}" + } + + init_config = Application.get_env(:ethereum_jsonrpc, EthereumJSONRPC.Geth) + Application.put_env(:ethereum_jsonrpc, EthereumJSONRPC.Geth, tracer: "call_tracer", debug_trace_timeout: "5s") + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["result"]["revertReason"] == hex_reason + assert response["status"] == "1" + assert response["message"] == "OK" + + Application.put_env(:ethereum_jsonrpc, EthereumJSONRPC.Geth, init_config) + end + end + + test "with a txhash with empty revert reason from the archive node", %{conn: conn} do + block = insert(:block, number: 100, hash: "0x3e51328bccedee581e8ba35190216a61a5d67fd91ca528f3553142c0c7d18391") + + transaction = + :transaction + |> insert( + error: "Reverted", + status: :error, + block_hash: block.hash, + block_number: block.number, + cumulative_gas_used: 884_322, + gas_used: 106_025, + index: 0, + hash: "0xac2a7dab94d965893199e7ee01649e2d66f0787a4c558b3118c09e80d4df8269" + ) + + insert(:address) + + expect( + EthereumJSONRPC.Mox, + :json_rpc, + fn + [%{method: "debug_traceTransaction"}], _options -> + {:ok, + [ + %{ + id: 0, + result: %{ + "error" => "Reverted", + "from" => "0x6a17ca3bbf83764791f4a9f2b4dbbaebbc8b3e0d", + "gas" => "0x5208", + "gasUsed" => "0x5208", + "input" => "0x01", + "to" => "0x7ed1e469fcb3ee19c0366d829e291451be638e59", + "type" => "CALL", + "value" => "0x86b3" + } + } + ]} + + [%{method: "trace_replayTransaction"}], _options -> + {:ok, + [ + %{ + id: 0, + result: %{ + "output" => "0x", + "stateDiff" => nil, + "trace" => [ + %{ + "action" => %{ + "callType" => "call", + "from" => "0x6a17ca3bbf83764791f4a9f2b4dbbaebbc8b3e0d", + "gas" => "0x5208", + "input" => "0x01", + "to" => "0x7ed1e469fcb3ee19c0366d829e291451be638e59", + "value" => "0x86b3" + }, + "error" => "Reverted", + "result" => %{ + "gasUsed" => "0x5208", + "output" => "0x" + }, + "subtraces" => 0, + "traceAddress" => [], + "type" => "call" + } + ], + "transactionHash" => "0xdf5574290913659a1ac404ccf2d216c40587f819400a52405b081dda728ac120", + "vmTrace" => nil + } + } + ]} + + %{method: "eth_call"}, _options -> + {:error, + %{ + code: 3, + message: "execution reverted" + }} + end + ) + + params = %{ + "module" => "transaction", + "action" => "gettxinfo", + "txhash" => "#{transaction.hash}" + } + + init_config = Application.get_env(:ethereum_jsonrpc, EthereumJSONRPC.Geth) + Application.put_env(:ethereum_jsonrpc, EthereumJSONRPC.Geth, tracer: "call_tracer", debug_trace_timeout: "5s") + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["result"]["revertReason"] in ["", "0x"] + assert response["status"] == "1" + assert response["message"] == "OK" + + Application.put_env(:ethereum_jsonrpc, EthereumJSONRPC.Geth, init_config) + end + + defp resolve_schema(result \\ %{}) do + %{ + "type" => "object", + "properties" => %{ + "message" => %{"type" => "string"}, + "status" => %{"type" => "string"} + } + } + |> put_in(["properties", "result"], result) + |> ExJsonSchema.Schema.resolve() + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/api/v1/health_controller_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/api/v1/health_controller_test.exs new file mode 100644 index 0000000..d38f513 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/api/v1/health_controller_test.exs @@ -0,0 +1,159 @@ +defmodule BlockScoutWeb.API.V1.HealthControllerTest do + use BlockScoutWeb.ConnCase + + import Mox + import EthereumJSONRPC, only: [integer_to_quantity: 1] + + alias Explorer.{Chain, PagingOptions} + alias Explorer.Chain.Cache.Blocks + + setup :set_mox_from_context + + setup do + old_env = Application.get_env(:explorer, Explorer.Chain.Health.Monitor) + Supervisor.terminate_child(Explorer.Supervisor, Explorer.Chain.Health.Monitor) + Supervisor.terminate_child(Explorer.Supervisor, Blocks.child_id()) + Supervisor.restart_child(Explorer.Supervisor, Blocks.child_id()) + + new_env = + old_env + |> Keyword.replace(:check_interval, 100) + + Application.put_env(:explorer, Explorer.Chain.Health.Monitor, new_env) + start_supervised!(Explorer.Chain.Health.Monitor) + + current_block_number = 100_500 + current_block_number_hex = integer_to_quantity(current_block_number) + + expect(EthereumJSONRPC.Mox, :json_rpc, fn %{method: "eth_blockNumber"}, _options -> + {:ok, current_block_number_hex} + end) + + on_exit(fn -> + Application.put_env(:explorer, Explorer.Chain.Health.Monitor, old_env) + end) + + %{current_block_number: current_block_number} + end + + describe "GET last_block_status/0" do + test "returns error when there are no blocks in db", %{conn: conn} do + request = get(conn, api_health_path(conn, :health)) + + assert request.status == 500 + + expected_error = + %{ + "code" => 5002, + "message" => "There are no blocks in the DB." + } + + decoded_response = request.resp_body |> Jason.decode!() + + assert decoded_response["metadata"]["blocks"]["healthy"] == false + assert decoded_response["metadata"]["blocks"]["error"] == expected_error + end + + test "returns error when last block is stale", %{conn: conn, current_block_number: current_block_number} do + block = insert(:block, consensus: true, timestamp: Timex.shift(DateTime.utc_now(), hours: -50)) + Blocks.update(block) + + :timer.sleep(150) + + request = get(conn, api_health_path(conn, :health)) + + assert request.status == 500 + + assert %{ + "latest_block" => %{ + "cache" => %{ + "number" => to_string(block.number), + "timestamp" => to_string(DateTime.truncate(block.timestamp, :second)) + }, + "db" => %{ + "number" => to_string(block.number), + "timestamp" => to_string(DateTime.truncate(block.timestamp, :second)) + }, + "node" => %{"number" => to_string(current_block_number)} + }, + "healthy" => false, + "error" => %{ + "code" => 5001, + "message" => + "There are no new blocks in the DB for the last 3000 mins. Check the healthiness of the JSON RPC archive node or the DB." + } + } == Poison.decode!(request.resp_body)["metadata"]["blocks"] + end + + test "returns ok when last block is not stale", %{conn: conn, current_block_number: current_block_number} do + block1 = insert(:block, consensus: true, timestamp: DateTime.utc_now(), number: 2) + Blocks.update(block1) + block2 = insert(:block, consensus: true, timestamp: DateTime.utc_now(), number: 1) + Blocks.update(block2) + + :timer.sleep(150) + + request = get(conn, api_health_path(conn, :health)) + + result = Poison.decode!(request.resp_body) + + assert %{ + "latest_block" => %{ + "db" => %{ + "number" => to_string(block1.number), + "timestamp" => to_string(DateTime.truncate(block1.timestamp, :second)) + }, + "cache" => %{ + "number" => to_string(block1.number), + "timestamp" => to_string(DateTime.truncate(block1.timestamp, :second)) + }, + "node" => %{"number" => to_string(current_block_number)} + }, + "healthy" => true + } == result["metadata"]["blocks"] + end + end + + test "return error when cache is stale", %{conn: conn, current_block_number: current_block_number} do + stale_block = insert(:block, consensus: true, timestamp: Timex.shift(DateTime.utc_now(), hours: -50), number: 3) + Blocks.update(stale_block) + stale_block_hash = stale_block.hash + + assert [%{hash: ^stale_block_hash}] = Chain.list_blocks(paging_options: %PagingOptions{page_size: 1}) + + block = insert(:block, consensus: true, timestamp: DateTime.utc_now(), number: 1) + Blocks.update(block) + + assert [%{hash: ^stale_block_hash}] = Chain.list_blocks(paging_options: %PagingOptions{page_size: 1}) + + :timer.sleep(150) + + request = get(conn, api_health_path(conn, :health)) + + assert request.status == 500 + + assert %{ + "latest_block" => %{ + "cache" => %{ + "number" => to_string(stale_block.number), + "timestamp" => to_string(DateTime.truncate(stale_block.timestamp, :second)) + }, + "db" => %{ + "number" => to_string(stale_block.number), + "timestamp" => to_string(DateTime.truncate(stale_block.timestamp, :second)) + }, + "node" => %{"number" => to_string(current_block_number)} + }, + "healthy" => false, + "error" => %{ + "code" => 5001, + "message" => + "There are no new blocks in the DB for the last 3000 mins. Check the healthiness of the JSON RPC archive node or the DB." + } + } == Poison.decode!(request.resp_body)["metadata"]["blocks"] + end + + defp api_health_path(conn, action) do + "/api" <> ApiRoutes.health_path(conn, action) + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/api/v1/supply_controller_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/api/v1/supply_controller_test.exs new file mode 100644 index 0000000..b521838 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/api/v1/supply_controller_test.exs @@ -0,0 +1,17 @@ +defmodule BlockScoutWeb.API.V1.SupplyControllerTest do + use BlockScoutWeb.ConnCase + + alias Explorer.Chain + + test "supply", %{conn: conn} do + request = get(conn, api_v1_supply_path(conn, :supply)) + assert response = json_response(request, 200) + + assert response["total_supply"] == Chain.total_supply() + assert response["circulating_supply"] == Chain.circulating_supply() + end + + def api_v1_supply_path(conn, action) do + "/api" <> ApiRoutes.api_v1_supply_path(conn, action) + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/api/v1/verified_smart_contract_controller_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/api/v1/verified_smart_contract_controller_test.exs new file mode 100644 index 0000000..095a7e4 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/api/v1/verified_smart_contract_controller_test.exs @@ -0,0 +1,80 @@ +defmodule BlockScoutWeb.API.V1.VerifiedControllerTest do + use BlockScoutWeb.ConnCase + + # alias Explorer.Factory + + # import Ecto.Query, + # only: [from: 2] + + # flaky test + # test "verifying a standard smart contract", %{conn: conn} do + # contract_code_info = Factory.contract_code_info() + + # contract_address = insert(:contract_address, contract_code: contract_code_info.bytecode) + # insert(:transaction, created_contract_address_hash: contract_address.hash, input: contract_code_info.tx_input) + + # params = %{ + # "address_hash" => to_string(contract_address.hash), + # "name" => contract_code_info.name, + # "compiler_version" => contract_code_info.version, + # "optimization" => contract_code_info.optimized, + # "contract_source_code" => contract_code_info.source_code + # } + + # response = post(conn, api_v1_verified_smart_contract_path(conn, :create), params) + + # assert response.status == 201 + # assert Jason.decode!(response.resp_body) == %{"status" => "success"} + # end + + # flaky test + # test "verifying a smart contract with external libraries", %{conn: conn} do + # contract_data = + # "#{File.cwd!()}/test/support/fixture/smart_contract/contract_with_lib.json" + # |> File.read!() + # |> Jason.decode!() + # |> List.first() + + # %{ + # "compiler_version" => compiler_version, + # "external_libraries" => external_libraries, + # "name" => name, + # "optimize" => optimize, + # "contract" => contract_source_code, + # "tx_input" => tx_input, + # "expected_bytecode" => expected_bytecode + # } = contract_data + + # contract_address = insert(:contract_address, contract_code: "0x" <> expected_bytecode) + # insert(:transaction, created_contract_address_hash: contract_address.hash, input: "0x" <> tx_input) + + # params = %{ + # "address_hash" => to_string(contract_address.hash), + # "name" => name, + # "compiler_version" => compiler_version, + # "optimization" => optimize, + # "contract_source_code" => contract_source_code + # } + + # params_with_external_libraries = + # external_libraries + # |> Enum.with_index() + # |> Enum.reduce(params, fn {{name, address}, index}, acc -> + # name_key = "library#{index + 1}_name" + # address_key = "library#{index + 1}_address" + + # acc + # |> Map.put(name_key, name) + # |> Map.put(address_key, address) + # end) + + # response = post(conn, api_v1_verified_smart_contract_path(conn, :create), params_with_external_libraries) + + # assert response.status == 201 + # assert Jason.decode!(response.resp_body) == %{"status" => "success"} + # end + + # defp api_v1_verified_smart_contract_path(conn, action) do + # "/api" <> ApiRoutes.api_v1_verified_smart_contract_path(conn, action) + # end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs new file mode 100644 index 0000000..2a1ff83 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs @@ -0,0 +1,3896 @@ +defmodule BlockScoutWeb.API.V2.AddressControllerTest do + use BlockScoutWeb.ConnCase + use EthereumJSONRPC.Case, async: false + use BlockScoutWeb.ChannelCase + + alias ABI.{TypeDecoder, TypeEncoder} + alias Explorer.{Chain, Repo, TestHelper} + alias Explorer.Chain.Address.Counters + + alias Explorer.Chain.{ + Address, + Address.CoinBalance, + Block, + InternalTransaction, + Log, + Token, + Token.Instance, + TokenTransfer, + Transaction, + Wei, + Withdrawal + } + + alias Explorer.Account.{Identity, WatchlistAddress} + alias Explorer.Chain.Address.CurrentTokenBalance + alias Explorer.Chain.SmartContract.Proxy.ResolvedDelegateProxy + alias Indexer.Fetcher.OnDemand.ContractCode, as: ContractCodeOnDemand + alias Plug.Conn + + import Explorer.Chain, only: [hash_to_lower_case_string: 1] + import Mox + + @first_topic_hex_string_1 "0x7fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b65" + @instances_amount_in_collection 9 + + setup :set_mox_global + + setup :verify_on_exit! + + setup %{json_rpc_named_arguments: json_rpc_named_arguments} do + mocked_json_rpc_named_arguments = Keyword.put(json_rpc_named_arguments, :transport, EthereumJSONRPC.Mox) + + start_supervised!({Task.Supervisor, name: Indexer.TaskSupervisor}) + + start_supervised!({ContractCodeOnDemand, [mocked_json_rpc_named_arguments, [name: ContractCodeOnDemand]]}) + + %{json_rpc_named_arguments: mocked_json_rpc_named_arguments} + + :ok + end + + defp topic(topic_hex_string) do + {:ok, topic} = Explorer.Chain.Hash.Full.cast(topic_hex_string) + topic + end + + describe "/addresses/{address_hash}" do + test "get 200 on non existing address", %{conn: conn} do + address = build(:address) + + correct_response = %{ + "hash" => Address.checksum(address.hash), + "is_contract" => false, + "is_verified" => false, + "name" => nil, + "private_tags" => [], + "public_tags" => [], + "watchlist_names" => [], + "creator_address_hash" => nil, + "creation_transaction_hash" => nil, + "token" => nil, + "coin_balance" => nil, + "proxy_type" => nil, + "implementations" => [], + "block_number_balance_updated_at" => nil, + "has_validated_blocks" => false, + "has_logs" => false, + "has_tokens" => false, + "has_token_transfers" => false, + "watchlist_address_id" => nil, + "has_beacon_chain_withdrawals" => false, + "ens_domain_name" => nil, + "metadata" => nil + } + + stub(EthereumJSONRPC.Mox, :json_rpc, fn _, _ -> + {:ok, []} + end) + + request = get(conn, "/api/v2/addresses/#{Address.checksum(address.hash)}") + check_response(correct_response, json_response(request, 200)) + end + + test "get 422 on invalid address", %{conn: conn} do + request = get(conn, "/api/v2/addresses/0x") + + assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + end + + test "get address & get the same response for checksummed and downcased parameter", %{conn: conn} do + address = insert(:address) + + correct_response = %{ + "hash" => Address.checksum(address.hash), + "is_contract" => false, + "is_verified" => false, + "name" => nil, + "private_tags" => [], + "public_tags" => [], + "watchlist_names" => [], + "creator_address_hash" => nil, + "creation_transaction_hash" => nil, + "token" => nil, + "coin_balance" => nil, + "proxy_type" => nil, + "implementations" => [], + "block_number_balance_updated_at" => nil, + "has_validated_blocks" => false, + "has_logs" => false, + "has_tokens" => false, + "has_token_transfers" => false, + "watchlist_address_id" => nil, + "has_beacon_chain_withdrawals" => false, + "ens_domain_name" => nil, + "metadata" => nil + } + + stub(EthereumJSONRPC.Mox, :json_rpc, fn _, _ -> + {:ok, []} + end) + + request = get(conn, "/api/v2/addresses/#{Address.checksum(address.hash)}") + check_response(correct_response, json_response(request, 200)) + + request = get(conn, "/api/v2/addresses/#{String.downcase(to_string(address.hash))}") + check_response(correct_response, json_response(request, 200)) + end + + test "returns successful creation transaction for a contract when both failed and successful transactions exist", + %{conn: conn} do + contract_address = insert(:address, contract_code: "0x") + + failed_transaction = + insert(:transaction, + created_contract_address_hash: contract_address.hash + ) + |> with_block(status: :error) + + succeeded_transaction = + insert(:transaction, + created_contract_address_hash: contract_address.hash + ) + |> with_block(status: :ok) + + assert failed_transaction.block_number < succeeded_transaction.block_number + + stub(EthereumJSONRPC.Mox, :json_rpc, fn _, _ -> + {:ok, []} + end) + + request = get(conn, "/api/v2/addresses/#{Address.checksum(contract_address.hash)}") + response = json_response(request, 200) + assert response["is_contract"] + assert response["creation_transaction_hash"] == to_string(succeeded_transaction.hash) + end + + defp check_response(pattern_response, response) do + assert pattern_response["hash"] == response["hash"] + assert pattern_response["is_contract"] == response["is_contract"] + assert pattern_response["is_verified"] == response["is_verified"] + assert pattern_response["name"] == response["name"] + assert pattern_response["private_tags"] == response["private_tags"] + assert pattern_response["public_tags"] == response["public_tags"] + assert pattern_response["watchlist_names"] == response["watchlist_names"] + assert pattern_response["creator_address_hash"] == response["creator_address_hash"] + assert pattern_response["creation_transaction_hash"] == response["creation_transaction_hash"] + assert pattern_response["token"] == response["token"] + assert pattern_response["coin_balance"] == response["coin_balance"] + assert pattern_response["implementation_address"] == response["implementation_address"] + assert pattern_response["implementation_name"] == response["implementation_name"] + assert pattern_response["implementations"] == response["implementations"] + assert pattern_response["block_number_balance_updated_at"] == response["block_number_balance_updated_at"] + assert pattern_response["has_validated_blocks"] == response["has_validated_blocks"] + assert pattern_response["has_logs"] == response["has_logs"] + assert pattern_response["has_tokens"] == response["has_tokens"] + assert pattern_response["has_token_transfers"] == response["has_token_transfers"] + assert pattern_response["watchlist_address_id"] == response["watchlist_address_id"] + assert pattern_response["has_beacon_chain_withdrawals"] == response["has_beacon_chain_withdrawals"] + assert pattern_response["ens_domain_name"] == response["ens_domain_name"] + assert pattern_response["metadata"] == response["metadata"] + end + + test "get EIP-1167 proxy contract info", %{conn: conn} do + implementation_contract = + insert(:smart_contract, + name: "Implementation", + external_libraries: [], + constructor_arguments: "", + abi: [ + %{ + "type" => "constructor", + "inputs" => [ + %{"type" => "address", "name" => "_proxyStorage"}, + %{"type" => "address", "name" => "_implementationAddress"} + ] + }, + %{ + "constant" => false, + "inputs" => [%{"name" => "x", "type" => "uint256"}], + "name" => "set", + "outputs" => [], + "payable" => false, + "stateMutability" => "nonpayable", + "type" => "function" + }, + %{ + "constant" => true, + "inputs" => [], + "name" => "get", + "outputs" => [%{"name" => "", "type" => "uint256"}], + "payable" => false, + "stateMutability" => "view", + "type" => "function" + } + ], + license_type: 9 + ) + + implementation_contract_address_hash_string = + Base.encode16(implementation_contract.address_hash.bytes, case: :lower) + + proxy_transaction_input = + "0x11b804ab000000000000000000000000" <> + implementation_contract_address_hash_string <> + "000000000000000000000000000000000000000000000000000000000000006035323031313537360000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000284e159163400000000000000000000000034420c13696f4ac650b9fafe915553a1abcd7dd30000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000001c00000000000000000000000000000000000000000000000000000000000000220000000000000000000000000ff5ae9b0a7522736299d797d80b8fc6f31d61100000000000000000000000000ff5ae9b0a7522736299d797d80b8fc6f31d6110000000000000000000000000000000000000000000000000000000000000003e8000000000000000000000000000000000000000000000000000000000000000000000000000000000000000034420c13696f4ac650b9fafe915553a1abcd7dd300000000000000000000000000000000000000000000000000000000000000184f7074696d69736d2053756273637269626572204e465473000000000000000000000000000000000000000000000000000000000000000000000000000000054f504e46540000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000037697066733a2f2f516d66544e504839765651334b5952346d6b52325a6b757756424266456f5a5554545064395538666931503332752f300000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000c82bbe41f2cf04e3a8efa18f7032bdd7f6d98a81000000000000000000000000efba8a2a82ec1fb1273806174f5e28fbb917cf9500000000000000000000000000000000000000000000000000000000" + + proxy_deployed_bytecode = + "0x363d3d373d3d3d363d73" <> implementation_contract_address_hash_string <> "5af43d82803e903d91602b57fd5bf3" + + proxy_address = + insert(:contract_address, + contract_code: proxy_deployed_bytecode + ) + + transaction = + insert(:transaction, + created_contract_address_hash: proxy_address.hash, + input: proxy_transaction_input + ) + |> with_block(status: :ok) + + name = implementation_contract.name + from = Address.checksum(transaction.from_address_hash) + transaction_hash = to_string(transaction.hash) + address_hash = Address.checksum(proxy_address.hash) + + {:ok, implementation_contract_address_hash} = + Chain.string_to_address_hash("0x" <> implementation_contract_address_hash_string) + + checksummed_implementation_contract_address_hash = + implementation_contract_address_hash && Address.checksum(implementation_contract_address_hash) + + insert(:proxy_implementation, + proxy_address_hash: proxy_address.hash, + proxy_type: "eip1167", + address_hashes: [implementation_contract.address_hash], + names: [name] + ) + + request = get(conn, "/api/v2/addresses/#{Address.checksum(proxy_address.hash)}") + + assert %{ + "hash" => ^address_hash, + "is_contract" => true, + "is_verified" => true, + "private_tags" => [], + "public_tags" => [], + "watchlist_names" => [], + "creator_address_hash" => ^from, + "creation_transaction_hash" => ^transaction_hash, + "proxy_type" => "eip1167", + "implementations" => [ + %{ + "address_hash" => ^checksummed_implementation_contract_address_hash, + "address" => ^checksummed_implementation_contract_address_hash, + "name" => ^name + } + ] + } = json_response(request, 200) + end + + test "get EIP-1967 proxy contract info", %{conn: conn} do + smart_contract = insert(:smart_contract) + + transaction = + insert(:transaction, + to_address_hash: nil, + to_address: nil, + created_contract_address_hash: smart_contract.address_hash, + created_contract_address: smart_contract.address + ) + + insert(:address_name, + address: smart_contract.address, + primary: true, + name: smart_contract.name, + address_hash: smart_contract.address_hash + ) + + name = smart_contract.name + from = Address.checksum(transaction.from_address_hash) + transaction_hash = to_string(transaction.hash) + address_hash = Address.checksum(smart_contract.address_hash) + + implementation_address = insert(:address) + implementation_address_hash_string = to_string(Address.checksum(implementation_address.hash)) + TestHelper.get_eip1967_implementation_non_zero_address(implementation_address_hash_string) + + request = get(conn, "/api/v2/addresses/#{Address.checksum(smart_contract.address_hash)}") + + assert %{ + "hash" => ^address_hash, + "is_contract" => true, + "is_verified" => true, + "name" => ^name, + "private_tags" => [], + "public_tags" => [], + "watchlist_names" => [], + "creator_address_hash" => ^from, + "creation_transaction_hash" => ^transaction_hash, + "proxy_type" => "eip1967", + "implementations" => [ + %{ + "address_hash" => ^implementation_address_hash_string, + "address" => ^implementation_address_hash_string, + "name" => nil + } + ] + } = json_response(request, 200) + end + + test "get Resolved Delegate Proxy contract info", %{conn: conn} do + address_manager_address = insert(:address) + + "0x" <> address_manager_address_hash_string_without_0x = to_string(address_manager_address.hash) + + owner_address = insert(:address) + + "0x" <> owner_address_hash_string_without_0x = to_string(owner_address.hash) + + proxy_smart_contract = + insert(:smart_contract, + abi: ResolvedDelegateProxy.resolved_delegate_proxy_abi(), + constructor_arguments: + "0x000000000000000000000000" <> + address_manager_address_hash_string_without_0x <> + "0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000001a4f564d5f4c3143726f7373446f6d61696e4d657373656e676572000000000000" + ) + + transaction = + insert(:transaction, + to_address_hash: nil, + to_address: nil, + created_contract_address_hash: proxy_smart_contract.address_hash, + created_contract_address: proxy_smart_contract.address + ) + + insert(:address_name, + address: proxy_smart_contract.address, + primary: true, + name: proxy_smart_contract.name, + address_hash: proxy_smart_contract.address_hash + ) + + name = proxy_smart_contract.name + from = Address.checksum(transaction.from_address_hash) + transaction_hash = to_string(transaction.hash) + checksummed_proxy_address_hash = Address.checksum(proxy_smart_contract.address_hash) + "0x" <> proxy_address_hash_string_without_0x = to_string(proxy_smart_contract.address_hash) + + implementation_address = insert(:address) + + "0x" <> implementation_address_hash_string_without_0x = to_string(implementation_address.hash) + implementation_address_hash_string = to_string(Address.checksum(implementation_address.hash)) + + TestHelper.get_resolved_delegate_proxy_implementation_non_zero_address( + owner_address_hash_string_without_0x, + implementation_address_hash_string_without_0x, + proxy_address_hash_string_without_0x + ) + + request = get(conn, "/api/v2/addresses/#{checksummed_proxy_address_hash}") + + assert %{ + "hash" => ^checksummed_proxy_address_hash, + "is_contract" => true, + "is_verified" => true, + "name" => ^name, + "private_tags" => [], + "public_tags" => [], + "watchlist_names" => [], + "creator_address_hash" => ^from, + "creation_transaction_hash" => ^transaction_hash, + "proxy_type" => "resolved_delegate_proxy", + "implementations" => [ + %{ + "address_hash" => ^implementation_address_hash_string, + "address" => ^implementation_address_hash_string, + "name" => nil + } + ] + } = json_response(request, 200) + end + + test "get watchlist id", %{conn: conn} do + auth = build(:auth) + address = insert(:address) + {:ok, user} = Identity.find_or_create(auth) + + conn = Plug.Test.init_test_session(conn, current_user: user) + + watchlist_address = + Repo.account_repo().insert!(%WatchlistAddress{ + name: "wallet", + watchlist_id: user.watchlist_id, + address_hash: address.hash, + address_hash_hash: hash_to_lower_case_string(address.hash), + watch_coin_input: true, + watch_coin_output: true, + watch_erc_20_input: true, + watch_erc_20_output: true, + watch_erc_721_input: true, + watch_erc_721_output: true, + watch_erc_1155_input: true, + watch_erc_1155_output: true, + notify_email: true + }) + + stub(EthereumJSONRPC.Mox, :json_rpc, fn _, _ -> + {:ok, []} + end) + + request = get(conn, "/api/v2/addresses/#{Address.checksum(address.hash)}") + assert response = json_response(request, 200) + + assert response["watchlist_address_id"] == watchlist_address.id + end + + test "broadcasts fetched_bytecode event", %{conn: conn} do + address = insert(:address) + address_hash = address.hash + string_address_hash = to_string(address.hash) + + contract_code = "0x6080" + + EthereumJSONRPC.Mox + |> expect(:json_rpc, fn [ + %{ + id: id, + jsonrpc: "2.0", + method: "eth_getCode", + params: [^string_address_hash, "latest"] + } + ], + _ -> + {:ok, [%{id: id, result: contract_code}]} + end) + + topic = "addresses:#{address_hash}" + + {:ok, _reply, _socket} = + BlockScoutWeb.V2.UserSocket + |> socket("no_id", %{}) + |> subscribe_and_join(topic) + + request = get(conn, "/api/v2/addresses/#{address.hash}") + assert _response = json_response(request, 200) + + assert_receive %Phoenix.Socket.Message{ + payload: %{fetched_bytecode: ^contract_code}, + event: "fetched_bytecode", + topic: ^topic + }, + :timer.seconds(1) + end + end + + describe "/addresses/{address_hash}/counters" do + test "get 200 on non existing address", %{conn: conn} do + address = build(:address) + + request = get(conn, "/api/v2/addresses/#{address.hash}/counters") + + assert %{ + "transactions_count" => "0", + "token_transfers_count" => "0", + "gas_usage_count" => "0", + "validations_count" => "0" + } = json_response(request, 200) + end + + test "get 422 on invalid address", %{conn: conn} do + request = get(conn, "/api/v2/addresses/0x/counters") + + assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + end + + test "get counters with 0s", %{conn: conn} do + address = insert(:address) + + request = get(conn, "/api/v2/addresses/#{address.hash}/counters") + + assert %{ + "transactions_count" => "0", + "token_transfers_count" => "0", + "gas_usage_count" => "0", + "validations_count" => "0" + } = json_response(request, 200) + end + + test "get counters", %{conn: conn} do + address = insert(:address) + + transaction_from = insert(:transaction, from_address: address) |> with_block() + insert(:transaction, to_address: address) |> with_block() + another_transaction = insert(:transaction) |> with_block() + + insert(:token_transfer, + from_address: address, + transaction: another_transaction, + block: another_transaction.block, + block_number: another_transaction.block_number + ) + + insert(:token_transfer, + to_address: address, + transaction: another_transaction, + block: another_transaction.block, + block_number: another_transaction.block_number + ) + + insert(:block, miner: address) + + Counters.transaction_count(address) + Counters.token_transfers_count(address) + Counters.gas_usage_count(address) + + request = get(conn, "/api/v2/addresses/#{address.hash}/counters") + + gas_used = to_string(transaction_from.gas_used) + + assert %{ + "transactions_count" => "2", + "token_transfers_count" => "2", + "gas_usage_count" => ^gas_used, + "validations_count" => "1" + } = json_response(request, 200) + end + end + + describe "/addresses/{address_hash}/transactions" do + test "get empty list on non existing address", %{conn: conn} do + address = build(:address) + + request = get(conn, "/api/v2/addresses/#{address.hash}/transactions") + + assert %{"items" => [], "next_page_params" => nil} = json_response(request, 200) + end + + test "get 422 on invalid address", %{conn: conn} do + request = get(conn, "/api/v2/addresses/0x/transactions") + + assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + end + + test "get relevant transaction", %{conn: conn} do + address = insert(:address) + + transaction = insert(:transaction, from_address: address) |> with_block() + + insert(:transaction) |> with_block() + + request = get(conn, "/api/v2/addresses/#{address.hash}/transactions") + + assert response = json_response(request, 200) + assert Enum.count(response["items"]) == 1 + assert response["next_page_params"] == nil + compare_item(transaction, Enum.at(response["items"], 0)) + end + + test "get pending transaction", %{conn: conn} do + address = insert(:address) + + transaction = insert(:transaction, from_address: address) |> with_block() + pending_transaction = insert(:transaction, from_address: address) + + insert(:transaction) |> with_block() + insert(:transaction) + + request = get(conn, "/api/v2/addresses/#{address.hash}/transactions") + + assert response = json_response(request, 200) + assert Enum.count(response["items"]) == 2 + assert response["next_page_params"] == nil + compare_item(pending_transaction, Enum.at(response["items"], 0)) + compare_item(transaction, Enum.at(response["items"], 1)) + end + + test "get only :to transaction", %{conn: conn} do + address = insert(:address) + + insert(:transaction, from_address: address) |> with_block() + transaction = insert(:transaction, to_address: address) |> with_block() + + request = get(conn, "/api/v2/addresses/#{address.hash}/transactions", %{"filter" => "to"}) + + assert response = json_response(request, 200) + assert Enum.count(response["items"]) == 1 + assert response["next_page_params"] == nil + compare_item(transaction, Enum.at(response["items"], 0)) + end + + test "get only :from transactions", %{conn: conn} do + address = insert(:address) + + transaction = insert(:transaction, from_address: address) |> with_block() + insert(:transaction, to_address: address) |> with_block() + + request = get(conn, "/api/v2/addresses/#{address.hash}/transactions", %{"filter" => "from"}) + + assert response = json_response(request, 200) + assert Enum.count(response["items"]) == 1 + assert response["next_page_params"] == nil + compare_item(transaction, Enum.at(response["items"], 0)) + end + + test "validated transactions can paginate", %{conn: conn} do + address = insert(:address) + + transactions = insert_list(51, :transaction, from_address: address) |> with_block() + + request = get(conn, "/api/v2/addresses/#{address.hash}/transactions") + assert response = json_response(request, 200) + + request_2nd_page = get(conn, "/api/v2/addresses/#{address.hash}/transactions", response["next_page_params"]) + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, transactions) + end + + test "pending transactions can paginate", %{conn: conn} do + address = insert(:address) + + transactions = insert_list(51, :transaction, from_address: address) + + request = get(conn, "/api/v2/addresses/#{address.hash}/transactions") + assert response = json_response(request, 200) + + request_2nd_page = get(conn, "/api/v2/addresses/#{address.hash}/transactions", response["next_page_params"]) + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, transactions) + end + + test "pending + validated transactions can paginate", %{conn: conn} do + address = insert(:address) + + transactions_pending = insert_list(51, :transaction, from_address: address) + transactions_validated = insert_list(50, :transaction, to_address: address) |> with_block() + + request = get(conn, "/api/v2/addresses/#{address.hash}/transactions") + assert response = json_response(request, 200) + + request_2nd_page = get(conn, "/api/v2/addresses/#{address.hash}/transactions", response["next_page_params"]) + assert response_2nd_page = json_response(request_2nd_page, 200) + + assert Enum.count(response["items"]) == 50 + assert response["next_page_params"] != nil + compare_item(Enum.at(transactions_pending, 50), Enum.at(response["items"], 0)) + compare_item(Enum.at(transactions_pending, 1), Enum.at(response["items"], 49)) + + assert Enum.count(response_2nd_page["items"]) == 50 + assert response_2nd_page["next_page_params"] != nil + compare_item(Enum.at(transactions_pending, 0), Enum.at(response_2nd_page["items"], 0)) + compare_item(Enum.at(transactions_validated, 49), Enum.at(response_2nd_page["items"], 1)) + compare_item(Enum.at(transactions_validated, 1), Enum.at(response_2nd_page["items"], 49)) + + request = get(conn, "/api/v2/addresses/#{address.hash}/transactions", response_2nd_page["next_page_params"]) + assert response = json_response(request, 200) + + check_paginated_response( + response_2nd_page, + response, + transactions_validated ++ [Enum.at(transactions_pending, 0)] + ) + end + + test ":to transactions can paginate", %{conn: conn} do + address = insert(:address) + + transactions = insert_list(51, :transaction, to_address: address) |> with_block() + insert_list(51, :transaction, from_address: address) |> with_block() + + filter = %{"filter" => "to"} + request = get(conn, "/api/v2/addresses/#{address.hash}/transactions", filter) + assert response = json_response(request, 200) + + request_2nd_page = + get(conn, "/api/v2/addresses/#{address.hash}/transactions", Map.merge(response["next_page_params"], filter)) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, transactions) + end + + test ":from transactions can paginate", %{conn: conn} do + address = insert(:address) + + insert_list(51, :transaction, to_address: address) |> with_block() + transactions = insert_list(51, :transaction, from_address: address) |> with_block() + + filter = %{"filter" => "from"} + request = get(conn, "/api/v2/addresses/#{address.hash}/transactions", filter) + assert response = json_response(request, 200) + + request_2nd_page = + get(conn, "/api/v2/addresses/#{address.hash}/transactions", Map.merge(response["next_page_params"], filter)) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, transactions) + end + + test ":from + :to transactions can paginate", %{conn: conn} do + address = insert(:address) + + transactions_from = insert_list(50, :transaction, from_address: address) |> with_block() + transactions_to = insert_list(51, :transaction, to_address: address) |> with_block() + + request = get(conn, "/api/v2/addresses/#{address.hash}/transactions") + assert response = json_response(request, 200) + + request_2nd_page = get(conn, "/api/v2/addresses/#{address.hash}/transactions", response["next_page_params"]) + assert response_2nd_page = json_response(request_2nd_page, 200) + + assert Enum.count(response["items"]) == 50 + assert response["next_page_params"] != nil + compare_item(Enum.at(transactions_to, 50), Enum.at(response["items"], 0)) + compare_item(Enum.at(transactions_to, 1), Enum.at(response["items"], 49)) + + assert Enum.count(response_2nd_page["items"]) == 50 + assert response_2nd_page["next_page_params"] != nil + compare_item(Enum.at(transactions_to, 0), Enum.at(response_2nd_page["items"], 0)) + compare_item(Enum.at(transactions_from, 49), Enum.at(response_2nd_page["items"], 1)) + compare_item(Enum.at(transactions_from, 1), Enum.at(response_2nd_page["items"], 49)) + + request = get(conn, "/api/v2/addresses/#{address.hash}/transactions", response_2nd_page["next_page_params"]) + assert response = json_response(request, 200) + + check_paginated_response(response_2nd_page, response, transactions_from ++ [Enum.at(transactions_to, 0)]) + end + + test "ignores wrong ordering params", %{conn: conn} do + address = insert(:address) + + transactions = insert_list(51, :transaction, from_address: address) |> with_block() + + request = get(conn, "/api/v2/addresses/#{address.hash}/transactions", %{"sort" => "foo", "order" => "bar"}) + assert response = json_response(request, 200) + + request_2nd_page = + get( + conn, + "/api/v2/addresses/#{address.hash}/transactions", + %{"sort" => "foo", "order" => "bar"} |> Map.merge(response["next_page_params"]) + ) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, transactions) + end + + test "backward compatible with legacy paging params", %{conn: conn} do + address = insert(:address) + block = insert(:block) + + transactions = insert_list(51, :transaction, from_address: address) |> with_block(block) + + [_, transaction_before_last | _] = transactions + + request = get(conn, "/api/v2/addresses/#{address.hash}/transactions") + assert response = json_response(request, 200) + + request_2nd_page = + get( + conn, + "/api/v2/addresses/#{address.hash}/transactions", + %{"block_number" => to_string(block.number), "index" => to_string(transaction_before_last.index)} + ) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, transactions) + end + + test "backward compatible with legacy paging params for pending transactions", %{conn: conn} do + address = insert(:address) + + transactions = insert_list(51, :transaction, from_address: address) + + [_, transaction_before_last | _] = transactions + + request = get(conn, "/api/v2/addresses/#{address.hash}/transactions") + assert response = json_response(request, 200) + + request_2nd_page_pending = + get( + conn, + "/api/v2/addresses/#{address.hash}/transactions", + %{ + "inserted_at" => to_string(transaction_before_last.inserted_at), + "hash" => to_string(transaction_before_last.hash) + } + ) + + assert response_2nd_page_pending = json_response(request_2nd_page_pending, 200) + + check_paginated_response(response, response_2nd_page_pending, transactions) + end + + test "can order and paginate by fee ascending", %{conn: conn} do + address = insert(:address) + + transactions_from = insert_list(25, :transaction, from_address: address) |> with_block() + transactions_to = insert_list(26, :transaction, to_address: address) |> with_block() + + transactions = + (transactions_from ++ transactions_to) + |> Enum.sort( + &(Decimal.compare(&1 |> Transaction.fee(:wei) |> elem(1), &2 |> Transaction.fee(:wei) |> elem(1)) in [ + :eq, + :lt + ]) + ) + + request = get(conn, "/api/v2/addresses/#{address.hash}/transactions", %{"sort" => "fee", "order" => "asc"}) + assert response = json_response(request, 200) + + request_2nd_page = + get( + conn, + "/api/v2/addresses/#{address.hash}/transactions", + %{"sort" => "fee", "order" => "asc"} |> Map.merge(response["next_page_params"]) + ) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + assert Enum.count(response["items"]) == 50 + assert response["next_page_params"] != nil + compare_item(Enum.at(transactions, 0), Enum.at(response["items"], 0)) + compare_item(Enum.at(transactions, 49), Enum.at(response["items"], 49)) + + assert Enum.count(response_2nd_page["items"]) == 1 + assert response_2nd_page["next_page_params"] == nil + compare_item(Enum.at(transactions, 50), Enum.at(response_2nd_page["items"], 0)) + + check_paginated_response(response, response_2nd_page, transactions |> Enum.reverse()) + end + + test "can order and paginate by fee descending", %{conn: conn} do + address = insert(:address) + + transactions_from = insert_list(25, :transaction, from_address: address) |> with_block() + transactions_to = insert_list(26, :transaction, to_address: address) |> with_block() + + transactions = + (transactions_from ++ transactions_to) + |> Enum.sort( + &(Decimal.compare(&1 |> Transaction.fee(:wei) |> elem(1), &2 |> Transaction.fee(:wei) |> elem(1)) in [ + :eq, + :gt + ]) + ) + + request = get(conn, "/api/v2/addresses/#{address.hash}/transactions", %{"sort" => "fee", "order" => "desc"}) + assert response = json_response(request, 200) + + request_2nd_page = + get( + conn, + "/api/v2/addresses/#{address.hash}/transactions", + %{"sort" => "fee", "order" => "desc"} |> Map.merge(response["next_page_params"]) + ) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + assert Enum.count(response["items"]) == 50 + assert response["next_page_params"] != nil + compare_item(Enum.at(transactions, 0), Enum.at(response["items"], 0)) + compare_item(Enum.at(transactions, 49), Enum.at(response["items"], 49)) + + assert Enum.count(response_2nd_page["items"]) == 1 + assert response_2nd_page["next_page_params"] == nil + compare_item(Enum.at(transactions, 50), Enum.at(response_2nd_page["items"], 0)) + + check_paginated_response(response, response_2nd_page, transactions |> Enum.reverse()) + end + + test "can order and paginate by value ascending", %{conn: conn} do + address = insert(:address) + + transactions_from = insert_list(25, :transaction, from_address: address) |> with_block() + transactions_to = insert_list(26, :transaction, to_address: address) |> with_block() + + transactions = + (transactions_from ++ transactions_to) + |> Enum.sort(&(Decimal.compare(Wei.to(&1.value, :wei), Wei.to(&2.value, :wei)) in [:eq, :lt])) + + request = get(conn, "/api/v2/addresses/#{address.hash}/transactions", %{"sort" => "value", "order" => "asc"}) + assert response = json_response(request, 200) + + request_2nd_page = + get( + conn, + "/api/v2/addresses/#{address.hash}/transactions", + %{"sort" => "value", "order" => "asc"} |> Map.merge(response["next_page_params"]) + ) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + assert Enum.count(response["items"]) == 50 + assert response["next_page_params"] != nil + compare_item(Enum.at(transactions, 0), Enum.at(response["items"], 0)) + compare_item(Enum.at(transactions, 49), Enum.at(response["items"], 49)) + + assert Enum.count(response_2nd_page["items"]) == 1 + assert response_2nd_page["next_page_params"] == nil + compare_item(Enum.at(transactions, 50), Enum.at(response_2nd_page["items"], 0)) + + check_paginated_response(response, response_2nd_page, transactions |> Enum.reverse()) + end + + test "can order and paginate by value descending", %{conn: conn} do + address = insert(:address) + + transactions_from = insert_list(25, :transaction, from_address: address) |> with_block() + transactions_to = insert_list(26, :transaction, to_address: address) |> with_block() + + transactions = + (transactions_from ++ transactions_to) + |> Enum.sort(&(Decimal.compare(Wei.to(&1.value, :wei), Wei.to(&2.value, :wei)) in [:eq, :gt])) + + request = get(conn, "/api/v2/addresses/#{address.hash}/transactions", %{"sort" => "value", "order" => "desc"}) + assert response = json_response(request, 200) + + request_2nd_page = + get( + conn, + "/api/v2/addresses/#{address.hash}/transactions", + %{"sort" => "value", "order" => "desc"} |> Map.merge(response["next_page_params"]) + ) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + assert Enum.count(response["items"]) == 50 + assert response["next_page_params"] != nil + compare_item(Enum.at(transactions, 0), Enum.at(response["items"], 0)) + compare_item(Enum.at(transactions, 49), Enum.at(response["items"], 49)) + + assert Enum.count(response_2nd_page["items"]) == 1 + assert response_2nd_page["next_page_params"] == nil + compare_item(Enum.at(transactions, 50), Enum.at(response_2nd_page["items"], 0)) + + check_paginated_response(response, response_2nd_page, transactions |> Enum.reverse()) + end + + test "can order and paginate by block number ascending", %{conn: conn} do + address = insert(:address) + + transactions_from = + for _ <- 0..24, do: insert(:transaction, from_address: address) |> with_block() + + transactions_to = for _ <- 0..25, do: insert(:transaction, to_address: address) |> with_block() + + transactions = + (transactions_from ++ transactions_to) + |> Enum.sort_by(& &1.block.number) + + request = + get(conn, "/api/v2/addresses/#{address.hash}/transactions", %{"sort" => "block_number", "order" => "asc"}) + + assert response = json_response(request, 200) + + request_2nd_page = + get( + conn, + "/api/v2/addresses/#{address.hash}/transactions", + %{"sort" => "block_number", "order" => "asc"} |> Map.merge(response["next_page_params"]) + ) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + assert Enum.count(response["items"]) == 50 + assert response["next_page_params"] != nil + compare_item(Enum.at(transactions, 0), Enum.at(response["items"], 0)) + compare_item(Enum.at(transactions, 49), Enum.at(response["items"], 49)) + + assert Enum.count(response_2nd_page["items"]) == 1 + assert response_2nd_page["next_page_params"] == nil + compare_item(Enum.at(transactions, 50), Enum.at(response_2nd_page["items"], 0)) + + check_paginated_response(response, response_2nd_page, transactions |> Enum.reverse()) + end + + test "can order and paginate by block number descending", %{conn: conn} do + address = insert(:address) + + transactions_from = + for _ <- 0..24, do: insert(:transaction, from_address: address) |> with_block() + + transactions_to = for _ <- 0..25, do: insert(:transaction, to_address: address) |> with_block() + + transactions = + (transactions_from ++ transactions_to) + |> Enum.sort_by(& &1.block.number, :desc) + + request = + get(conn, "/api/v2/addresses/#{address.hash}/transactions", %{"sort" => "block_number", "order" => "desc"}) + + assert response = json_response(request, 200) + + request_2nd_page = + get( + conn, + "/api/v2/addresses/#{address.hash}/transactions", + %{"sort" => "block_number", "order" => "desc"} |> Map.merge(response["next_page_params"]) + ) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + assert Enum.count(response["items"]) == 50 + assert response["next_page_params"] != nil + compare_item(Enum.at(transactions, 0), Enum.at(response["items"], 0)) + compare_item(Enum.at(transactions, 49), Enum.at(response["items"], 49)) + + assert Enum.count(response_2nd_page["items"]) == 1 + assert response_2nd_page["next_page_params"] == nil + compare_item(Enum.at(transactions, 50), Enum.at(response_2nd_page["items"], 0)) + + check_paginated_response(response, response_2nd_page, transactions |> Enum.reverse()) + end + end + + describe "/addresses/{address_hash}/token-transfers" do + test "get 200 on non existing address", %{conn: conn} do + address = build(:address) + + request = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers") + + assert %{"items" => [], "next_page_params" => nil} = json_response(request, 200) + end + + test "get 422 on invalid address", %{conn: conn} do + request = get(conn, "/api/v2/addresses/0x/token-transfers") + + assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + end + + test "get 200 on non existing address of token", %{conn: conn} do + address = insert(:address) + + token = build(:address) + + request = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", %{"token" => to_string(token.hash)}) + + assert %{"items" => [], "next_page_params" => nil} = json_response(request, 200) + end + + test "get 422 on invalid token address hash", %{conn: conn} do + address = insert(:address) + + request = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", %{"token" => "0x"}) + + assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + end + + test "get relevant token transfer", %{conn: conn} do + address = insert(:address) + + transaction = insert(:transaction, input: "0xabcd010203040506") |> with_block() + + insert(:token_transfer, + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number + ) + + token_transfer = + insert(:token_transfer, + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number, + from_address: address + ) + + request = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers") + + assert response = json_response(request, 200) + assert Enum.count(response["items"]) == 1 + assert response["next_page_params"] == nil + compare_item(token_transfer, Enum.at(response["items"], 0)) + end + + test "method in token transfer could be decoded", %{conn: conn} do + insert(:contract_method, + identifier: Base.decode16!("731133e9", case: :lower), + abi: %{ + "constant" => false, + "inputs" => [ + %{"name" => "account", "type" => "address"}, + %{"name" => "id", "type" => "uint256"}, + %{"name" => "amount", "type" => "uint256"}, + %{"name" => "data", "type" => "bytes"} + ], + "name" => "mint", + "outputs" => [], + "payable" => false, + "stateMutability" => "nonpayable", + "type" => "function" + } + ) + + address = insert(:address) + + transaction = + insert(:transaction, + input: + "0x731133e9000000000000000000000000bb36c792b9b45aaf8b848a1392b0d6559202729e000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000001700000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000000" + ) + |> with_block() + + insert(:token_transfer, + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number + ) + + token_transfer = + insert(:token_transfer, + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number, + from_address: address + ) + + request = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers") + + assert response = json_response(request, 200) + assert Enum.count(response["items"]) == 1 + assert response["next_page_params"] == nil + compare_item(token_transfer, Enum.at(response["items"], 0)) + assert Enum.at(response["items"], 0)["method"] == "mint" + end + + test "get relevant token transfer filtered by token", %{conn: conn} do + token = insert(:token) + + address = insert(:address) + + transaction = insert(:transaction, input: "0xabcd010203040506") |> with_block() + + insert(:token_transfer, + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number + ) + + insert(:token_transfer, + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number, + from_address: address + ) + + token_transfer = + insert(:token_transfer, + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number, + from_address: address, + token_contract_address: token.contract_address + ) + + request = + get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", %{ + "token" => to_string(token.contract_address) + }) + + assert response = json_response(request, 200) + assert Enum.count(response["items"]) == 1 + assert response["next_page_params"] == nil + compare_item(token_transfer, Enum.at(response["items"], 0)) + end + + test "token transfers by token can paginate", %{conn: conn} do + address = insert(:address) + + token = insert(:token) + + token_transfers = + for _ <- 0..50 do + transaction = insert(:transaction, input: "0xabcd010203040506") |> with_block() + + insert(:token_transfer, + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number, + from_address: address + ) + + insert(:token_transfer, + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number, + from_address: address, + token_contract_address: token.contract_address + ) + end + + params = %{"token" => to_string(token.contract_address)} + request = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", params) + assert response = json_response(request, 200) + + request_2nd_page = + get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", Map.merge(params, response["next_page_params"])) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, token_transfers) + end + + test "get only :to token transfer", %{conn: conn} do + address = insert(:address) + + transaction = insert(:transaction, input: "0xabcd010203040506") |> with_block() + + insert(:token_transfer, + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number, + from_address: address + ) + + token_transfer = + insert(:token_transfer, + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number, + to_address: address + ) + + request = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", %{"filter" => "to"}) + + assert response = json_response(request, 200) + assert Enum.count(response["items"]) == 1 + assert response["next_page_params"] == nil + compare_item(token_transfer, Enum.at(response["items"], 0)) + end + + test "get only :from token transfer", %{conn: conn} do + address = insert(:address) + + transaction = insert(:transaction, input: "0xabcd010203040506") |> with_block() + + token_transfer = + insert(:token_transfer, + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number, + from_address: address + ) + + insert(:token_transfer, + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number, + to_address: address + ) + + request = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", %{"filter" => "from"}) + + assert response = json_response(request, 200) + assert Enum.count(response["items"]) == 1 + assert response["next_page_params"] == nil + compare_item(token_transfer, Enum.at(response["items"], 0)) + end + + test "token transfers can paginate", %{conn: conn} do + address = insert(:address) + + token_transfers = + for _ <- 0..50 do + transaction = insert(:transaction, input: "0xabcd010203040506") |> with_block() + + insert(:token_transfer, + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number, + from_address: address + ) + end + + request = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers") + assert response = json_response(request, 200) + + request_2nd_page = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", response["next_page_params"]) + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, token_transfers) + end + + test ":to token transfers can paginate", %{conn: conn} do + address = insert(:address) + + for _ <- 0..50 do + transaction = insert(:transaction, input: "0xabcd010203040506") |> with_block() + + insert(:token_transfer, + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number, + from_address: address + ) + end + + token_transfers = + for _ <- 0..50 do + transaction = insert(:transaction, input: "0xabcd010203040506") |> with_block() + + insert(:token_transfer, + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number, + to_address: address + ) + end + + filter = %{"filter" => "to"} + request = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", filter) + assert response = json_response(request, 200) + + request_2nd_page = + get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", Map.merge(response["next_page_params"], filter)) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, token_transfers) + end + + test ":from token transfers can paginate", %{conn: conn} do + address = insert(:address) + + token_transfers = + for _ <- 0..50 do + transaction = insert(:transaction, input: "0xabcd010203040506") |> with_block() + + insert(:token_transfer, + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number, + from_address: address + ) + end + + for _ <- 0..50 do + transaction = insert(:transaction, input: "0xabcd010203040506") |> with_block() + + insert(:token_transfer, + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number, + to_address: address + ) + end + + filter = %{"filter" => "from"} + request = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", filter) + assert response = json_response(request, 200) + + request_2nd_page = + get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", Map.merge(response["next_page_params"], filter)) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, token_transfers) + end + + test ":from + :to tt can paginate", %{conn: conn} do + address = insert(:address) + + tt_from = + for _ <- 0..49 do + transaction = insert(:transaction, input: "0xabcd010203040506") |> with_block() + + insert(:token_transfer, + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number, + from_address: address + ) + end + + tt_to = + for _ <- 0..50 do + transaction = insert(:transaction, input: "0xabcd010203040506") |> with_block() + + insert(:token_transfer, + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number, + to_address: address + ) + end + + request = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers") + assert response = json_response(request, 200) + + request_2nd_page = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", response["next_page_params"]) + assert response_2nd_page = json_response(request_2nd_page, 200) + + assert Enum.count(response["items"]) == 50 + assert response["next_page_params"] != nil + compare_item(Enum.at(tt_to, 50), Enum.at(response["items"], 0)) + compare_item(Enum.at(tt_to, 1), Enum.at(response["items"], 49)) + + assert Enum.count(response_2nd_page["items"]) == 50 + assert response_2nd_page["next_page_params"] != nil + compare_item(Enum.at(tt_to, 0), Enum.at(response_2nd_page["items"], 0)) + compare_item(Enum.at(tt_from, 49), Enum.at(response_2nd_page["items"], 1)) + compare_item(Enum.at(tt_from, 1), Enum.at(response_2nd_page["items"], 49)) + + request = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", response_2nd_page["next_page_params"]) + assert response = json_response(request, 200) + + check_paginated_response(response_2nd_page, response, tt_from ++ [Enum.at(tt_to, 0)]) + end + + test "check token type filters", %{conn: conn} do + address = insert(:address) + + erc_20_token = insert(:token, type: "ERC-20") + + erc_20_tt = + for _ <- 0..50 do + transaction = insert(:transaction, input: "0xabcd010203040506") |> with_block() + + insert(:token_transfer, + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number, + from_address: address, + token_contract_address: erc_20_token.contract_address, + token_type: "ERC-20" + ) + end + + erc_721_token = insert(:token, type: "ERC-721") + + erc_721_tt = + for x <- 0..50 do + transaction = insert(:transaction, input: "0xabcd010203040506") |> with_block() + + insert(:token_transfer, + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number, + from_address: address, + token_contract_address: erc_721_token.contract_address, + token_ids: [x], + token_type: "ERC-721" + ) + end + + erc_1155_token = insert(:token, type: "ERC-1155") + + erc_1155_tt = + for x <- 0..50 do + transaction = insert(:transaction, input: "0xabcd010203040506") |> with_block() + + insert(:token_transfer, + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number, + from_address: address, + token_contract_address: erc_1155_token.contract_address, + token_ids: [x], + token_type: "ERC-1155" + ) + end + + # -- ERC-20 -- + filter = %{"type" => "ERC-20"} + request = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", filter) + assert response = json_response(request, 200) + + request_2nd_page = + get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", Map.merge(response["next_page_params"], filter)) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, erc_20_tt) + # -- ------ -- + + # -- ERC-721 -- + filter = %{"type" => "ERC-721"} + request = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", filter) + assert response = json_response(request, 200) + + request_2nd_page = + get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", Map.merge(response["next_page_params"], filter)) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, erc_721_tt) + # -- ------ -- + + # -- ERC-1155 -- + filter = %{"type" => "ERC-1155"} + request = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", filter) + assert response = json_response(request, 200) + + request_2nd_page = + get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", Map.merge(response["next_page_params"], filter)) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, erc_1155_tt) + # -- ------ -- + + # two filters simultaneously + filter = %{"type" => "ERC-1155,ERC-20"} + request = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", filter) + assert response = json_response(request, 200) + + request_2nd_page = + get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", Map.merge(response["next_page_params"], filter)) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + assert Enum.count(response["items"]) == 50 + assert response["next_page_params"] != nil + compare_item(Enum.at(erc_1155_tt, 50), Enum.at(response["items"], 0)) + compare_item(Enum.at(erc_1155_tt, 1), Enum.at(response["items"], 49)) + + assert Enum.count(response_2nd_page["items"]) == 50 + assert response_2nd_page["next_page_params"] != nil + compare_item(Enum.at(erc_1155_tt, 0), Enum.at(response_2nd_page["items"], 0)) + compare_item(Enum.at(erc_20_tt, 50), Enum.at(response_2nd_page["items"], 1)) + compare_item(Enum.at(erc_20_tt, 2), Enum.at(response_2nd_page["items"], 49)) + + request_3rd_page = + get( + conn, + "/api/v2/addresses/#{address.hash}/token-transfers", + Map.merge(response_2nd_page["next_page_params"], filter) + ) + + assert response_3rd_page = json_response(request_3rd_page, 200) + assert Enum.count(response_3rd_page["items"]) == 2 + assert response_3rd_page["next_page_params"] == nil + compare_item(Enum.at(erc_20_tt, 1), Enum.at(response_3rd_page["items"], 0)) + compare_item(Enum.at(erc_20_tt, 0), Enum.at(response_3rd_page["items"], 1)) + # -- ------ -- + end + + test "type and direction filters at the same time", %{conn: conn} do + address = insert(:address) + + erc_20_token = insert(:token, type: "ERC-20") + + erc_20_tt = + for _ <- 0..50 do + transaction = insert(:transaction, input: "0xabcd010203040506") |> with_block() + + insert(:token_transfer, + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number, + from_address: address, + token_contract_address: erc_20_token.contract_address, + token_type: "ERC-20" + ) + end + + erc_721_token = insert(:token, type: "ERC-721") + + erc_721_tt = + for x <- 0..50 do + transaction = insert(:transaction, input: "0xabcd010203040506") |> with_block() + + insert(:token_transfer, + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number, + to_address: address, + token_contract_address: erc_721_token.contract_address, + token_ids: [x], + token_type: "ERC-721" + ) + end + + filter = %{"type" => "ERC-721", "filter" => "from"} + request = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", filter) + assert response = json_response(request, 200) + assert response["items"] == [] + assert response["next_page_params"] == nil + + filter = %{"type" => "ERC-721", "filter" => "to"} + request = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", filter) + assert response = json_response(request, 200) + + request_2nd_page = + get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", Map.merge(response["next_page_params"], filter)) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, erc_721_tt) + + filter = %{"type" => "ERC-721,ERC-20", "filter" => "to"} + request = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", filter) + assert response = json_response(request, 200) + + request_2nd_page = + get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", Map.merge(response["next_page_params"], filter)) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, erc_721_tt) + + filter = %{"type" => "ERC-721,ERC-20", "filter" => "from"} + request = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", filter) + assert response = json_response(request, 200) + + request_2nd_page = + get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", Map.merge(response["next_page_params"], filter)) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, erc_20_tt) + end + + test "check that same token_ids within batch squashes", %{conn: conn} do + address = insert(:address) + + token = insert(:token, type: "ERC-1155") + + id = 0 + + insert(:token_instance, token_id: id, token_contract_address_hash: token.contract_address_hash) + + tt = + for _ <- 0..50 do + transaction = insert(:transaction, input: "0xabcd010203040506") |> with_block() + + insert(:token_transfer, + to_address: address, + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number, + token_contract_address: token.contract_address, + token_ids: Enum.map(0..50, fn _x -> id end), + token_type: "ERC-1155", + amounts: Enum.map(0..50, fn x -> x end) + ) + end + + token_transfers = + for i <- tt do + %TokenTransfer{i | token_ids: [id], amount: Decimal.new(1275)} + end + + request = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers") + assert response = json_response(request, 200) + + request_2nd_page = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", response["next_page_params"]) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, token_transfers) + end + + test "check that pagination works for 721 tokens", %{conn: conn} do + address = insert(:address) + + token = insert(:token, type: "ERC-721") + + token_transfers = + for i <- 0..50 do + transaction = insert(:transaction, input: "0xabcd010203040506") |> with_block() + + insert(:token_transfer, + transaction: transaction, + to_address: address, + block: transaction.block, + block_number: transaction.block_number, + token_contract_address: token.contract_address, + token_ids: [i], + token_type: "ERC-721" + ) + end + + request = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers") + assert response = json_response(request, 200) + + request_2nd_page = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", response["next_page_params"]) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, token_transfers) + end + + test "check that pagination works fine with 1155 batches #1 (large batch) + check filters", %{conn: conn} do + address = insert(:address) + + token = insert(:token, type: "ERC-1155") + transaction = insert(:transaction, input: "0xabcd010203040506") |> with_block() + + tt = + insert(:token_transfer, + transaction: transaction, + to_address: address, + block: transaction.block, + block_number: transaction.block_number, + token_contract_address: token.contract_address, + token_ids: Enum.map(0..50, fn x -> x end), + token_type: "ERC-1155", + amounts: Enum.map(0..50, fn x -> x end) + ) + + token_transfers = + for i <- 0..50 do + %TokenTransfer{tt | token_ids: [i], amount: i} + end + + filter = %{"type" => "ERC-1155", "filter" => "to"} + + request = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", filter) + assert response = json_response(request, 200) + + request_2nd_page = + get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", Map.merge(response["next_page_params"], filter)) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, token_transfers) + + filter = %{"type" => "ERC-1155", "filter" => "from"} + + request = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", filter) + assert %{"items" => [], "next_page_params" => nil} = json_response(request, 200) + end + + test "check that pagination works fine with 1155 batches #2 some batches on the first page and one on the second", + %{conn: conn} do + address = insert(:address) + + token = insert(:token, type: "ERC-1155") + + transaction_1 = insert(:transaction, input: "0xabcd010203040506") |> with_block() + + tt_1 = + insert(:token_transfer, + transaction: transaction_1, + to_address: address, + block: transaction_1.block, + block_number: transaction_1.block_number, + token_contract_address: token.contract_address, + token_ids: Enum.map(0..24, fn x -> x end), + token_type: "ERC-1155", + amounts: Enum.map(0..24, fn x -> x end) + ) + + token_transfers_1 = + for i <- 0..24 do + %TokenTransfer{tt_1 | token_ids: [i], amount: i} + end + + transaction_2 = insert(:transaction, input: "0xabcd010203040506") |> with_block() + + tt_2 = + insert(:token_transfer, + transaction: transaction_2, + to_address: address, + block: transaction_2.block, + block_number: transaction_2.block_number, + token_contract_address: token.contract_address, + token_ids: Enum.map(25..49, fn x -> x end), + token_type: "ERC-1155", + amounts: Enum.map(25..49, fn x -> x end) + ) + + token_transfers_2 = + for i <- 25..49 do + %TokenTransfer{tt_2 | token_ids: [i], amount: i} + end + + tt_3 = + insert(:token_transfer, + transaction: transaction_2, + from_address: address, + block: transaction_2.block, + block_number: transaction_2.block_number, + token_contract_address: token.contract_address, + token_ids: [50], + token_type: "ERC-1155", + amounts: [50] + ) + + request = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers") + assert response = json_response(request, 200) + + request_2nd_page = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", response["next_page_params"]) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, token_transfers_1 ++ token_transfers_2 ++ [tt_3]) + end + + test "check that pagination works fine with 1155 batches #3", %{conn: conn} do + address = insert(:address) + + token = insert(:token, type: "ERC-1155") + + transaction_1 = insert(:transaction, input: "0xabcd010203040506") |> with_block() + + tt_1 = + insert(:token_transfer, + transaction: transaction_1, + from_address: address, + block: transaction_1.block, + block_number: transaction_1.block_number, + token_contract_address: token.contract_address, + token_ids: Enum.map(0..24, fn x -> x end), + token_type: "ERC-1155", + amounts: Enum.map(0..24, fn x -> x end) + ) + + token_transfers_1 = + for i <- 0..24 do + %TokenTransfer{tt_1 | token_ids: [i], amount: i} + end + + transaction_2 = insert(:transaction, input: "0xabcd010203040506") |> with_block() + + tt_2 = + insert(:token_transfer, + transaction: transaction_2, + to_address: address, + block: transaction_2.block, + block_number: transaction_2.block_number, + token_contract_address: token.contract_address, + token_ids: Enum.map(25..50, fn x -> x end), + token_type: "ERC-1155", + amounts: Enum.map(25..50, fn x -> x end) + ) + + token_transfers_2 = + for i <- 25..50 do + %TokenTransfer{tt_2 | token_ids: [i], amount: i} + end + + request = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers") + assert response = json_response(request, 200) + + request_2nd_page = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", response["next_page_params"]) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, token_transfers_1 ++ token_transfers_2) + end + end + + describe "/addresses/{address_hash}/internal-transactions" do + test "get empty list on non existing address", %{conn: conn} do + address = build(:address) + + request = get(conn, "/api/v2/addresses/#{address.hash}/internal-transactions") + + assert %{"items" => [], "next_page_params" => nil} = json_response(request, 200) + end + + test "get 422 on invalid address", %{conn: conn} do + request = get(conn, "/api/v2/addresses/0x/internal-transactions") + + assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + end + + test "get internal transaction and filter working", %{conn: conn} do + address = insert(:address) + + transaction = + :transaction + |> insert() + |> with_block() + + internal_transaction_from = + insert(:internal_transaction, + transaction: transaction, + index: 1, + block_number: transaction.block_number, + transaction_index: transaction.index, + block_hash: transaction.block_hash, + block_index: 1, + from_address: address + ) + + internal_transaction_to = + insert(:internal_transaction, + transaction: transaction, + index: 2, + block_number: transaction.block_number, + transaction_index: transaction.index, + block_hash: transaction.block_hash, + block_index: 2, + to_address: address + ) + + request = get(conn, "/api/v2/addresses/#{address.hash}/internal-transactions") + + assert response = json_response(request, 200) + assert Enum.count(response["items"]) == 2 + assert response["next_page_params"] == nil + + compare_item(internal_transaction_from, Enum.at(response["items"], 1)) + compare_item(internal_transaction_to, Enum.at(response["items"], 0)) + + request = get(conn, "/api/v2/addresses/#{address.hash}/internal-transactions", %{"filter" => "from"}) + + assert response = json_response(request, 200) + assert Enum.count(response["items"]) == 1 + assert response["next_page_params"] == nil + compare_item(internal_transaction_from, Enum.at(response["items"], 0)) + + request = get(conn, "/api/v2/addresses/#{address.hash}/internal-transactions", %{"filter" => "to"}) + + assert response = json_response(request, 200) + assert Enum.count(response["items"]) == 1 + assert response["next_page_params"] == nil + compare_item(internal_transaction_to, Enum.at(response["items"], 0)) + end + + test "internal transactions can paginate", %{conn: conn} do + address = insert(:address) + + transaction = + :transaction + |> insert() + |> with_block() + + internal_transactions_from = + for i <- 1..51 do + insert(:internal_transaction, + transaction: transaction, + index: i, + block_number: transaction.block_number, + transaction_index: transaction.index, + block_hash: transaction.block_hash, + block_index: i, + from_address: address + ) + end + + request = get(conn, "/api/v2/addresses/#{address.hash}/internal-transactions") + assert response = json_response(request, 200) + + request_2nd_page = + get(conn, "/api/v2/addresses/#{address.hash}/internal-transactions", response["next_page_params"]) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, internal_transactions_from) + + internal_transactions_to = + for i <- 52..102 do + insert(:internal_transaction, + transaction: transaction, + index: i, + block_number: transaction.block_number, + transaction_index: transaction.index, + block_hash: transaction.block_hash, + block_index: i, + to_address: address + ) + end + + filter = %{"filter" => "to"} + request = get(conn, "/api/v2/addresses/#{address.hash}/internal-transactions", filter) + assert response = json_response(request, 200) + + request_2nd_page = + get( + conn, + "/api/v2/addresses/#{address.hash}/internal-transactions", + Map.merge(response["next_page_params"], filter) + ) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, internal_transactions_to) + + filter = %{"filter" => "from"} + request = get(conn, "/api/v2/addresses/#{address.hash}/internal-transactions", filter) + assert response = json_response(request, 200) + + request_2nd_page = + get( + conn, + "/api/v2/addresses/#{address.hash}/internal-transactions", + Map.merge(response["next_page_params"], filter) + ) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, internal_transactions_from) + end + end + + describe "/addresses/{address_hash}/blocks-validated" do + test "get empty list on non existing address", %{conn: conn} do + address = build(:address) + + request = get(conn, "/api/v2/addresses/#{address.hash}/blocks-validated") + + assert %{"items" => [], "next_page_params" => nil} = json_response(request, 200) + end + + test "get 422 on invalid address", %{conn: conn} do + request = get(conn, "/api/v2/addresses/0x/blocks-validated") + + assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + end + + test "get relevant block validated", %{conn: conn} do + address = insert(:address) + insert(:block) + block = insert(:block, miner: address) + + request = get(conn, "/api/v2/addresses/#{address.hash}/blocks-validated") + + assert response = json_response(request, 200) + assert Enum.count(response["items"]) == 1 + assert response["next_page_params"] == nil + + compare_item(block, Enum.at(response["items"], 0)) + end + + test "blocks validated can be paginated", %{conn: conn} do + address = insert(:address) + insert(:block) + blocks = insert_list(51, :block, miner: address) + + request = get(conn, "/api/v2/addresses/#{address.hash}/blocks-validated") + assert response = json_response(request, 200) + + request_2nd_page = get(conn, "/api/v2/addresses/#{address.hash}/blocks-validated", response["next_page_params"]) + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, blocks) + end + end + + describe "/addresses/{address_hash}/token-balances" do + test "get empty list on non existing address", %{conn: conn} do + address = build(:address) + + request = get(conn, "/api/v2/addresses/#{address.hash}/token-balances") + + assert json_response(request, 200) == [] + end + + test "get 422 on invalid address", %{conn: conn} do + request = get(conn, "/api/v2/addresses/0x/token-balances") + + assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + end + + test "get token balance", %{conn: conn} do + address = insert(:address) + + ctbs = + for _ <- 0..50 do + insert(:address_current_token_balance_with_token_id, address: address) |> Repo.preload([:token]) + end + |> Enum.sort_by(fn x -> x.value end, :desc) + + request = get(conn, "/api/v2/addresses/#{address.hash}/token-balances") + + assert response = json_response(request, 200) + + for i <- 0..50 do + compare_item(Enum.at(ctbs, i), Enum.at(response, i)) + end + end + end + + describe "/addresses/{address_hash}/coin-balance-history" do + test "get empty list on non existing address", %{conn: conn} do + address = build(:address) + + request = get(conn, "/api/v2/addresses/#{address.hash}/coin-balance-history") + + assert %{"items" => [], "next_page_params" => nil} = json_response(request, 200) + end + + test "get 422 on invalid address", %{conn: conn} do + request = get(conn, "/api/v2/addresses/0x/coin-balance-history") + + assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + end + + test "get coin balance history", %{conn: conn} do + address = insert(:address) + + insert(:address_coin_balance) + acb = insert(:address_coin_balance, address: address) + + request = get(conn, "/api/v2/addresses/#{address.hash}/coin-balance-history") + + assert response = json_response(request, 200) + assert Enum.count(response["items"]) == 1 + assert response["next_page_params"] == nil + + compare_item(acb, Enum.at(response["items"], 0)) + end + + test "coin balance history can paginate", %{conn: conn} do + address = insert(:address) + + acbs = insert_list(51, :address_coin_balance, address: address) + + request = get(conn, "/api/v2/addresses/#{address.hash}/coin-balance-history") + assert response = json_response(request, 200) + + request_2nd_page = + get(conn, "/api/v2/addresses/#{address.hash}/coin-balance-history", response["next_page_params"]) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, acbs) + end + end + + describe "/addresses/{address_hash}/coin-balance-history-by-day" do + test "get empty list on non existing address", %{conn: conn} do + address = build(:address) + + request = get(conn, "/api/v2/addresses/#{address.hash}/coin-balance-history-by-day") + + days_count = + Application.get_env(:block_scout_web, BlockScoutWeb.Chain.Address.CoinBalance)[:coin_balance_history_days] + + assert %{ + "days" => ^days_count, + "items" => [] + } = json_response(request, 200) + end + + test "get 422 on invalid address", %{conn: conn} do + request = get(conn, "/api/v2/addresses/0x/coin-balance-history-by-day") + + assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + end + + test "get coin balance history by day", %{conn: conn} do + address = insert(:address) + noon = Timex.now() |> Timex.beginning_of_day() |> Timex.set(hour: 12) + block = insert(:block, timestamp: noon, number: 2) + block_one_day_ago = insert(:block, timestamp: Timex.shift(noon, days: -1), number: 1) + insert(:fetched_balance, address_hash: address.hash, value: 1000, block_number: block.number) + insert(:fetched_balance, address_hash: address.hash, value: 2000, block_number: block_one_day_ago.number) + insert(:fetched_balance_daily, address_hash: address.hash, value: 1000, day: noon) + insert(:fetched_balance_daily, address_hash: address.hash, value: 2000, day: Timex.shift(noon, days: -1)) + + request = get(conn, "/api/v2/addresses/#{address.hash}/coin-balance-history-by-day") + + response = json_response(request, 200) + + assert %{ + "days" => 10, + "items" => [ + %{"date" => _, "value" => "2000"}, + %{"date" => _, "value" => "1000"}, + %{"date" => _, "value" => "1000"} + ] + } = response + end + end + + describe "/addresses/{address_hash}/logs" do + test "get empty list on non existing address", %{conn: conn} do + address = build(:address) + + request = get(conn, "/api/v2/addresses/#{address.hash}/logs") + + assert %{"items" => [], "next_page_params" => nil} = json_response(request, 200) + end + + test "get 422 on invalid address", %{conn: conn} do + request = get(conn, "/api/v2/addresses/0x/logs") + + assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + end + + test "get log", %{conn: conn} do + address = insert(:address) + + transaction = + :transaction + |> insert() + |> with_block() + + log = + insert(:log, + transaction: transaction, + index: 1, + block: transaction.block, + block_number: transaction.block_number, + address: address + ) + + request = get(conn, "/api/v2/addresses/#{address.hash}/logs") + + assert response = json_response(request, 200) + assert Enum.count(response["items"]) == 1 + assert response["next_page_params"] == nil + compare_item(log, Enum.at(response["items"], 0)) + end + + # for some reasons test does not work if run as single test + test "logs can paginate", %{conn: conn} do + address = insert(:address) + + logs = + for x <- 0..50 do + transaction = + :transaction + |> insert() + |> with_block() + + insert(:log, + transaction: transaction, + index: x, + block: transaction.block, + block_number: transaction.block_number, + address: address + ) + end + + request = get(conn, "/api/v2/addresses/#{address.hash}/logs") + assert response = json_response(request, 200) + + request_2nd_page = get(conn, "/api/v2/addresses/#{address.hash}/logs", response["next_page_params"]) + assert response_2nd_page = json_response(request_2nd_page, 200) + check_paginated_response(response, response_2nd_page, logs) + end + + # https://github.com/blockscout/blockscout/issues/9926 + test "regression test for 9926", %{conn: conn} do + address = insert(:address, hash: "0x036cec1a199234fC02f72d29e596a09440825f1C") + + transaction = + :transaction + |> insert() + |> with_block() + + log = + insert(:log, + transaction: transaction, + index: 1, + block: transaction.block, + block_number: transaction.block_number, + address: address + ) + + bypass = Bypass.open() + + old_chain_id = Application.get_env(:block_scout_web, :chain_id) + chain_id = 1 + Application.put_env(:block_scout_web, :chain_id, chain_id) + + old_env_bens = Application.get_env(:explorer, Explorer.MicroserviceInterfaces.BENS) + + Application.put_env(:explorer, Explorer.MicroserviceInterfaces.BENS, + service_url: "http://localhost:#{bypass.port}", + enabled: true + ) + + old_env_metadata = Application.get_env(:explorer, Explorer.MicroserviceInterfaces.Metadata) + + Application.put_env(:explorer, Explorer.MicroserviceInterfaces.Metadata, + service_url: "http://localhost:#{bypass.port}", + enabled: true + ) + + Bypass.expect_once(bypass, "POST", "api/v1/#{chain_id}/addresses:batch_resolve_names", fn conn -> + Conn.resp( + conn, + 200, + Jason.encode!(%{ + "names" => %{ + to_string(address) => "test.eth" + } + }) + ) + end) + + Bypass.expect_once(bypass, "GET", "api/v1/metadata", fn conn -> + Conn.resp( + conn, + 200, + Jason.encode!(%{ + "addresses" => %{ + to_string(address) => %{"tags" => [%{"slug" => "tag", "meta" => "{\"styles\":\"danger_high\"}"}]} + } + }) + ) + end) + + request = get(conn, "/api/v2/addresses/#{address.hash}/logs") + + assert response = json_response(request, 200) + assert Enum.count(response["items"]) == 1 + assert response["next_page_params"] == nil + compare_item(log, Enum.at(response["items"], 0)) + log = Enum.at(response["items"], 0) + assert log["address"]["ens_domain_name"] == "test.eth" + assert log["address"]["metadata"] == %{"tags" => [%{"slug" => "tag", "meta" => %{"styles" => "danger_high"}}]} + + Application.put_env(:block_scout_web, :chain_id, old_chain_id) + Application.put_env(:explorer, Explorer.MicroserviceInterfaces.BENS, old_env_bens) + Application.put_env(:explorer, Explorer.MicroserviceInterfaces.Metadata, old_env_metadata) + Bypass.down(bypass) + end + + test "logs can be filtered by topic", %{conn: conn} do + address = insert(:address) + + for x <- 0..20 do + transaction = + :transaction + |> insert() + |> with_block() + + insert(:log, + transaction: transaction, + index: x, + block: transaction.block, + block_number: transaction.block_number, + address: address + ) + end + + transaction = + :transaction + |> insert() + |> with_block() + + log = + insert(:log, + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number, + address: address, + first_topic: topic(@first_topic_hex_string_1) + ) + + request = get(conn, "/api/v2/addresses/#{address.hash}/logs?topic=#{@first_topic_hex_string_1}") + assert response = json_response(request, 200) + assert Enum.count(response["items"]) == 1 + assert response["next_page_params"] == nil + compare_item(log, Enum.at(response["items"], 0)) + end + end + + describe "/addresses/{address_hash}/tokens" do + test "get empty list on non existing address", %{conn: conn} do + address = build(:address) + + request = get(conn, "/api/v2/addresses/#{address.hash}/tokens") + + assert %{"items" => [], "next_page_params" => nil} = json_response(request, 200) + end + + test "get 422 on invalid address", %{conn: conn} do + request = get(conn, "/api/v2/addresses/0x/tokens") + + assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + end + + test "get tokens", %{conn: conn} do + address = insert(:address) + + ctbs_erc_20 = + for _ <- 0..50 do + insert(:address_current_token_balance_with_token_id_and_fixed_token_type, + address: address, + token_type: "ERC-20", + token_id: nil + ) + |> Repo.preload([:token]) + end + |> Enum.sort_by(fn x -> Decimal.to_float(Decimal.mult(x.value, x.token.fiat_value)) end, :asc) + + ctbs_erc_721 = + for _ <- 0..50 do + insert(:address_current_token_balance_with_token_id_and_fixed_token_type, + address: address, + token_type: "ERC-721", + token_id: nil + ) + |> Repo.preload([:token]) + end + |> Enum.sort_by(fn x -> Decimal.to_integer(x.value) end, :asc) + + ctbs_erc_1155 = + for _ <- 0..50 do + insert(:address_current_token_balance_with_token_id_and_fixed_token_type, + address: address, + token_type: "ERC-1155", + token_id: Enum.random(1..100_000) + ) + |> Repo.preload([:token]) + end + |> Enum.sort_by(fn x -> Decimal.to_integer(x.value) end, :asc) + + filter = %{"type" => "ERC-20"} + request = get(conn, "/api/v2/addresses/#{address.hash}/tokens", filter) + assert response = json_response(request, 200) + + request_2nd_page = + get(conn, "/api/v2/addresses/#{address.hash}/tokens", Map.merge(response["next_page_params"], filter)) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, ctbs_erc_20) + + filter = %{"type" => "ERC-721"} + request = get(conn, "/api/v2/addresses/#{address.hash}/tokens", filter) + assert response = json_response(request, 200) + + request_2nd_page = + get(conn, "/api/v2/addresses/#{address.hash}/tokens", Map.merge(response["next_page_params"], filter)) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, ctbs_erc_721) + + filter = %{"type" => "ERC-1155"} + request = get(conn, "/api/v2/addresses/#{address.hash}/tokens", filter) + assert response = json_response(request, 200) + + request_2nd_page = + get(conn, "/api/v2/addresses/#{address.hash}/tokens", Map.merge(response["next_page_params"], filter)) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, ctbs_erc_1155) + end + end + + describe "checks Indexer.Fetcher.OnDemand.TokenBalance" do + setup do + Supervisor.terminate_child(Explorer.Supervisor, Explorer.Chain.Cache.BlockNumber.child_id()) + Supervisor.restart_child(Explorer.Supervisor, Explorer.Chain.Cache.BlockNumber.child_id()) + old_env = Application.get_env(:indexer, Indexer.Fetcher.OnDemand.TokenBalance) + configuration = Application.get_env(:indexer, Indexer.Fetcher.OnDemand.TokenBalance.Supervisor) + Application.put_env(:indexer, Indexer.Fetcher.OnDemand.TokenBalance.Supervisor, disabled?: false) + Indexer.Fetcher.OnDemand.TokenBalance.Supervisor.Case.start_supervised!() + + Application.put_env( + :indexer, + Indexer.Fetcher.OnDemand.TokenBalance, + Keyword.put(old_env, :fallback_threshold_in_blocks, 0) + ) + + on_exit(fn -> + Application.put_env(:indexer, Indexer.Fetcher.OnDemand.TokenBalance.Supervisor, configuration) + Application.put_env(:indexer, Indexer.Fetcher.OnDemand.TokenBalance, old_env) + end) + end + + test "Indexer.Fetcher.OnDemand.TokenBalance broadcasts only updated balances", %{conn: conn} do + address = insert(:address) + + ctbs_erc_20 = + for i <- 0..1 do + ctb = + insert(:address_current_token_balance_with_token_id_and_fixed_token_type, + address: address, + token_type: "ERC-20", + token_id: nil + ) + + {to_string(ctb.token_contract_address_hash), + Decimal.to_integer(ctb.value) + if(rem(i, 2) == 0, do: 1, else: 0)} + end + |> Enum.into(%{}) + + ctbs_erc_721 = + for i <- 0..1 do + ctb = + insert(:address_current_token_balance_with_token_id_and_fixed_token_type, + address: address, + token_type: "ERC-721", + token_id: nil + ) + + {to_string(ctb.token_contract_address_hash), + Decimal.to_integer(ctb.value) + if(rem(i, 2) == 0, do: 1, else: 0)} + end + |> Enum.into(%{}) + + other_balances = Map.merge(ctbs_erc_20, ctbs_erc_721) + + balances_erc_1155 = + for i <- 0..1 do + ctb = + insert(:address_current_token_balance_with_token_id_and_fixed_token_type, + address: address, + token_type: "ERC-1155", + token_id: Enum.random(1..100_000) + ) + + {{to_string(ctb.token_contract_address_hash), to_string(ctb.token_id)}, + Decimal.to_integer(ctb.value) + if(rem(i, 2) == 0, do: 1, else: 0)} + end + |> Enum.into(%{}) + + block_number_hex = "0x" <> (Integer.to_string(insert(:block).number, 16) |> String.upcase()) + + expect(EthereumJSONRPC.Mox, :json_rpc, fn [ + %{ + id: id_1, + jsonrpc: "2.0", + method: "eth_call", + params: [ + %{ + data: "0x70a08231" <> request_1, + to: contract_address_1 + }, + ^block_number_hex + ] + }, + %{ + id: id_2, + jsonrpc: "2.0", + method: "eth_call", + params: [ + %{ + data: "0x70a08231" <> request_2, + to: contract_address_2 + }, + ^block_number_hex + ] + }, + %{ + id: id_3, + jsonrpc: "2.0", + method: "eth_call", + params: [ + %{ + data: "0x70a08231" <> request_3, + to: contract_address_3 + }, + ^block_number_hex + ] + }, + %{ + id: id_4, + jsonrpc: "2.0", + method: "eth_call", + params: [ + %{ + data: "0x70a08231" <> request_4, + to: contract_address_4 + }, + ^block_number_hex + ] + }, + %{ + id: id_5, + jsonrpc: "2.0", + method: "eth_call", + params: [ + %{ + data: "0x00fdd58e" <> request_5, + to: contract_address_5 + }, + ^block_number_hex + ] + }, + %{ + id: id_6, + jsonrpc: "2.0", + method: "eth_call", + params: [ + %{ + data: "0x00fdd58e" <> request_6, + to: contract_address_6 + }, + ^block_number_hex + ] + } + ], + _options -> + types_list = [:address] + + assert request_1 |> Base.decode16!(case: :lower) |> TypeDecoder.decode_raw(types_list) == [address.hash.bytes] + + assert request_2 |> Base.decode16!(case: :lower) |> TypeDecoder.decode_raw(types_list) == [address.hash.bytes] + + assert request_3 |> Base.decode16!(case: :lower) |> TypeDecoder.decode_raw(types_list) == [address.hash.bytes] + + assert request_4 |> Base.decode16!(case: :lower) |> TypeDecoder.decode_raw(types_list) == [address.hash.bytes] + + result_1 = + other_balances[contract_address_1 |> String.downcase()] + |> List.wrap() + |> TypeEncoder.encode_raw([{:uint, 256}], :standard) + |> Base.encode16(case: :lower) + + result_2 = + other_balances[contract_address_2 |> String.downcase()] + |> List.wrap() + |> TypeEncoder.encode_raw([{:uint, 256}], :standard) + |> Base.encode16(case: :lower) + + result_3 = + other_balances[contract_address_3 |> String.downcase()] + |> List.wrap() + |> TypeEncoder.encode_raw([{:uint, 256}], :standard) + |> Base.encode16(case: :lower) + + result_4 = + other_balances[contract_address_4 |> String.downcase()] + |> List.wrap() + |> TypeEncoder.encode_raw([{:uint, 256}], :standard) + |> Base.encode16(case: :lower) + + types_list = [:address, {:uint, 256}] + + [address_5, token_id_5] = request_5 |> Base.decode16!(case: :lower) |> TypeDecoder.decode_raw(types_list) + + assert address_5 == address.hash.bytes + + result_5 = + balances_erc_1155[{contract_address_5 |> String.downcase(), to_string(token_id_5)}] + |> List.wrap() + |> TypeEncoder.encode_raw([{:uint, 256}], :standard) + |> Base.encode16(case: :lower) + + [address_6, token_id_6] = request_6 |> Base.decode16!(case: :lower) |> TypeDecoder.decode_raw(types_list) + + assert address_6 == address.hash.bytes + + result_6 = + balances_erc_1155[{contract_address_6 |> String.downcase(), to_string(token_id_6)}] + |> List.wrap() + |> TypeEncoder.encode_raw([{:uint, 256}], :standard) + |> Base.encode16(case: :lower) + + {:ok, + [ + %{ + id: id_1, + jsonrpc: "2.0", + result: "0x" <> result_1 + }, + %{ + id: id_2, + jsonrpc: "2.0", + result: "0x" <> result_2 + }, + %{ + id: id_3, + jsonrpc: "2.0", + result: "0x" <> result_3 + }, + %{ + id: id_4, + jsonrpc: "2.0", + result: "0x" <> result_4 + }, + %{ + id: id_5, + jsonrpc: "2.0", + result: "0x" <> result_5 + }, + %{ + id: id_6, + jsonrpc: "2.0", + result: "0x" <> result_6 + } + ]} + end) + + topic = "addresses:#{address.hash}" + + {:ok, _reply, _socket} = + BlockScoutWeb.V2.UserSocket + |> socket("no_id", %{}) + |> subscribe_and_join(topic) + + request = get(conn, "/api/v2/addresses/#{address.hash}/tokens") + assert _response = json_response(request, 200) + overflow = false + + assert_receive %Phoenix.Socket.Message{ + payload: %{token_balances: [ctb_erc_20], overflow: ^overflow}, + event: "updated_token_balances_erc_20", + topic: ^topic + }, + :timer.seconds(1) + + assert_receive %Phoenix.Socket.Message{ + payload: %{token_balances: [ctb_erc_721], overflow: ^overflow}, + event: "updated_token_balances_erc_721", + topic: ^topic + }, + :timer.seconds(1) + + assert_receive %Phoenix.Socket.Message{ + payload: %{token_balances: [ctb_erc_1155], overflow: ^overflow}, + event: "updated_token_balances_erc_1155", + topic: ^topic + }, + :timer.seconds(1) + + assert Decimal.to_integer(ctb_erc_20["value"]) == + other_balances[ctb_erc_20["token"]["address"] |> String.downcase()] + + assert Decimal.to_integer(ctb_erc_721["value"]) == + other_balances[ctb_erc_721["token"]["address"] |> String.downcase()] + + assert Decimal.to_integer(ctb_erc_1155["value"]) == + balances_erc_1155[ + {ctb_erc_1155["token"]["address"] |> String.downcase(), to_string(ctb_erc_1155["token_id"])} + ] + end + end + + describe "/addresses/{address_hash}/withdrawals" do + test "get empty list on non existing address", %{conn: conn} do + address = build(:address) + + request = get(conn, "/api/v2/addresses/#{address.hash}/withdrawals") + + assert %{"items" => [], "next_page_params" => nil} = json_response(request, 200) + end + + test "get 422 on invalid address", %{conn: conn} do + request = get(conn, "/api/v2/addresses/0x/withdrawals") + + assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + end + + test "get withdrawals", %{conn: conn} do + address = insert(:address, withdrawals: insert_list(51, :withdrawal)) + + request = get(conn, "/api/v2/addresses/#{address.hash}/withdrawals") + assert response = json_response(request, 200) + + request_2nd_page = get(conn, "/api/v2/addresses/#{address.hash}/withdrawals", response["next_page_params"]) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, address.withdrawals) + end + end + + describe "/addresses" do + test "get empty list", %{conn: conn} do + request = get(conn, "/api/v2/addresses") + + total_supply = to_string(Chain.total_supply()) + + pattern_response = %{"items" => [], "next_page_params" => nil, "total_supply" => total_supply} + response = json_response(request, 200) + + assert pattern_response["items"] == response["items"] + assert pattern_response["next_page_params"] == response["next_page_params"] + assert pattern_response["total_supply"] == response["total_supply"] + end + + test "check pagination", %{conn: conn} do + addresses = + for i <- 0..50 do + insert(:address, nonce: i, fetched_coin_balance: i + 1) + end + + request = get(conn, "/api/v2/addresses") + assert response = json_response(request, 200) + assert not is_nil(response["next_page_params"]) + request_2nd_page = get(conn, "/api/v2/addresses", response["next_page_params"]) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, addresses) + + assert Enum.at(response["items"], 0)["coin_balance"] == + to_string(Enum.at(addresses, 50).fetched_coin_balance.value) + end + + test "check nil", %{conn: conn} do + address = insert(:address, transactions_count: 2, fetched_coin_balance: 1) + + request = get(conn, "/api/v2/addresses") + + assert %{"items" => [address_json], "next_page_params" => nil} = json_response(request, 200) + + compare_item(address, address_json) + end + + test "check smart contract preload", %{conn: conn} do + smart_contract = insert(:smart_contract, address_hash: insert(:contract_address, fetched_coin_balance: 1).hash) + + request = get(conn, "/api/v2/addresses") + assert %{"items" => [address]} = json_response(request, 200) + + assert String.downcase(address["hash"]) == to_string(smart_contract.address_hash) + assert address["is_contract"] == true + assert address["is_verified"] == true + end + + test "check sorting by balance asc", %{conn: conn} do + addresses = + for i <- 0..50 do + insert(:address, nonce: i, fetched_coin_balance: i + 1, transactions_count: 100 - i) + end + + sort_options = %{"sort" => "balance", "order" => "asc"} + request = get(conn, "/api/v2/addresses", sort_options) + assert response = json_response(request, 200) + assert not is_nil(response["next_page_params"]) + + request_2nd_page = get(conn, "/api/v2/addresses", Map.merge(response["next_page_params"], sort_options)) + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response( + response, + response_2nd_page, + Enum.sort_by(addresses, &Decimal.to_integer(&1.fetched_coin_balance.value), :desc) + ) + end + + test "check sorting by transactions count asc", %{conn: conn} do + addresses = + for i <- 0..50 do + insert(:address, nonce: i, transactions_count: i + 1, fetched_coin_balance: 100 - i) + end + + sort_options = %{"sort" => "transactions_count", "order" => "asc"} + request = get(conn, "/api/v2/addresses", sort_options) + assert response = json_response(request, 200) + assert not is_nil(response["next_page_params"]) + + request_2nd_page = get(conn, "/api/v2/addresses", Map.merge(response["next_page_params"], sort_options)) + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, Enum.sort_by(addresses, & &1.transactions_count, :desc)) + end + + test "check sorting by balance desc", %{conn: conn} do + addresses = + for i <- 0..50 do + insert(:address, nonce: i, fetched_coin_balance: i + 1, transactions_count: 100 - i) + end + + sort_options = %{"sort" => "balance", "order" => "desc"} + request = get(conn, "/api/v2/addresses", sort_options) + assert response = json_response(request, 200) + assert not is_nil(response["next_page_params"]) + + request_2nd_page = get(conn, "/api/v2/addresses", Map.merge(response["next_page_params"], sort_options)) + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response( + response, + response_2nd_page, + Enum.sort_by(addresses, &Decimal.to_integer(&1.fetched_coin_balance.value), :asc) + ) + end + + test "check sorting by transactions count desc", %{conn: conn} do + addresses = + for i <- 0..50 do + insert(:address, nonce: i, transactions_count: i + 1, fetched_coin_balance: 100 - i) + end + + sort_options = %{"sort" => "transactions_count", "order" => "desc"} + request = get(conn, "/api/v2/addresses", sort_options) + assert response = json_response(request, 200) + assert not is_nil(response["next_page_params"]) + + request_2nd_page = get(conn, "/api/v2/addresses", Map.merge(response["next_page_params"], sort_options)) + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, Enum.sort_by(addresses, & &1.transactions_count, :asc)) + end + end + + describe "/addresses/{address_hash}/tabs-counters" do + test "get 200 on non existing address", %{conn: conn} do + address = build(:address) + + request = get(conn, "/api/v2/addresses/#{address.hash}/tabs-counters") + + assert %{ + "validations_count" => 0, + "transactions_count" => 0, + "token_transfers_count" => 0, + "token_balances_count" => 0, + "logs_count" => 0, + "withdrawals_count" => 0, + "internal_transactions_count" => 0, + "celo_election_rewards_count" => 0 + } = json_response(request, 200) + end + + test "get 422 on invalid address", %{conn: conn} do + request = get(conn, "/api/v2/addresses/0x/tabs-counters") + + assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + end + + test "get counters with 0s", %{conn: conn} do + address = insert(:address) + + request = get(conn, "/api/v2/addresses/#{address.hash}/tabs-counters") + + assert %{ + "validations_count" => 0, + "transactions_count" => 0, + "token_transfers_count" => 0, + "token_balances_count" => 0, + "logs_count" => 0, + "withdrawals_count" => 0, + "internal_transactions_count" => 0 + } = json_response(request, 200) + end + + test "get counters and check that cache works", %{conn: conn} do + address = insert(:address, withdrawals: insert_list(60, :withdrawal)) + + insert(:transaction, from_address: address) |> with_block() + insert(:transaction, to_address: address) |> with_block() + another_transaction = insert(:transaction) |> with_block() + + insert(:token_transfer, + from_address: address, + transaction: another_transaction, + block: another_transaction.block, + block_number: another_transaction.block_number + ) + + insert(:token_transfer, + to_address: address, + transaction: another_transaction, + block: another_transaction.block, + block_number: another_transaction.block_number + ) + + insert(:block, miner: address) + + transaction = + :transaction + |> insert() + |> with_block() + + for x <- 1..2 do + insert(:internal_transaction, + transaction: transaction, + index: x, + block_number: transaction.block_number, + transaction_index: transaction.index, + block_hash: transaction.block_hash, + block_index: x, + from_address: address + ) + end + + for _ <- 0..60 do + insert(:address_current_token_balance_with_token_id, address: address) + end + + for x <- 0..60 do + transaction = + :transaction + |> insert() + |> with_block() + + insert(:log, + transaction: transaction, + index: x, + block: transaction.block, + block_number: transaction.block_number, + address: address + ) + end + + request = get(conn, "/api/v2/addresses/#{address.hash}/tabs-counters") + + assert %{ + "validations_count" => 1, + "transactions_count" => 2, + "token_transfers_count" => 2, + "token_balances_count" => 51, + "logs_count" => 51, + "withdrawals_count" => 51, + "internal_transactions_count" => 2 + } = json_response(request, 200) + + for x <- 3..4 do + insert(:internal_transaction, + transaction: transaction, + index: x, + block_number: transaction.block_number, + transaction_index: transaction.index, + block_hash: transaction.block_hash, + block_index: x, + from_address: address + ) + end + + request = get(conn, "/api/v2/addresses/#{address.hash}/tabs-counters") + + assert %{ + "validations_count" => 1, + "transactions_count" => 2, + "token_transfers_count" => 2, + "token_balances_count" => 51, + "logs_count" => 51, + "withdrawals_count" => 51, + "internal_transactions_count" => 2 + } = json_response(request, 200) + end + + test "check counters cache ttl", %{conn: conn} do + address = insert(:address, withdrawals: insert_list(60, :withdrawal)) + + insert(:transaction, from_address: address) |> with_block() + insert(:transaction, to_address: address) |> with_block() + another_transaction = insert(:transaction) |> with_block() + + insert(:token_transfer, + from_address: address, + transaction: another_transaction, + block: another_transaction.block, + block_number: another_transaction.block_number + ) + + insert(:token_transfer, + to_address: address, + transaction: another_transaction, + block: another_transaction.block, + block_number: another_transaction.block_number + ) + + insert(:block, miner: address) + + transaction = + :transaction + |> insert() + |> with_block() + + for x <- 1..2 do + insert(:internal_transaction, + transaction: transaction, + index: x, + block_number: transaction.block_number, + transaction_index: transaction.index, + block_hash: transaction.block_hash, + block_index: x, + from_address: address + ) + end + + for _ <- 0..60 do + insert(:address_current_token_balance_with_token_id, address: address) + end + + for x <- 0..60 do + transaction = + :transaction + |> insert() + |> with_block() + + insert(:log, + transaction: transaction, + index: x, + block: transaction.block, + block_number: transaction.block_number, + address: address + ) + end + + request = get(conn, "/api/v2/addresses/#{address.hash}/tabs-counters") + + assert %{ + "validations_count" => 1, + "transactions_count" => 2, + "token_transfers_count" => 2, + "token_balances_count" => 51, + "logs_count" => 51, + "withdrawals_count" => 51, + "internal_transactions_count" => 2 + } = json_response(request, 200) + + old_env = Application.get_env(:explorer, Explorer.Chain.Cache.Counters.AddressTabsElementsCount) + Application.put_env(:explorer, Explorer.Chain.Cache.Counters.AddressTabsElementsCount, ttl: 200) + :timer.sleep(200) + + for x <- 3..4 do + insert(:internal_transaction, + transaction: transaction, + index: x, + block_number: transaction.block_number, + transaction_index: transaction.index, + block_hash: transaction.block_hash, + block_index: x, + from_address: address + ) + end + + insert(:transaction, from_address: address) |> with_block() + insert(:transaction, to_address: address) |> with_block() + + request = get(conn, "/api/v2/addresses/#{address.hash}/tabs-counters") + + assert %{ + "validations_count" => 1, + "transactions_count" => 4, + "token_transfers_count" => 2, + "token_balances_count" => 51, + "logs_count" => 51, + "withdrawals_count" => 51, + "internal_transactions_count" => 4 + } = json_response(request, 200) + + Application.put_env(:explorer, Explorer.Chain.Cache.Counters.AddressTabsElementsCount, old_env) + end + end + + describe "/addresses/{address_hash}/nft" do + setup do + {:ok, endpoint: &"/api/v2/addresses/#{&1}/nft"} + end + + test "get 200 on non existing address", %{conn: conn, endpoint: endpoint} do + address = build(:address) + + request = get(conn, endpoint.(address.hash)) + + assert %{"items" => [], "next_page_params" => nil} = json_response(request, 200) + end + + test "get 422 on invalid address", %{conn: conn, endpoint: endpoint} do + request = get(conn, endpoint.("0x")) + + assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + end + + test "get paginated ERC-721 nft", %{conn: conn, endpoint: endpoint} do + address = insert(:address) + + insert_list(51, :token_instance) + + token_instances = + for _ <- 0..50 do + erc_721_token = insert(:token, type: "ERC-721") + + insert(:token_instance, + owner_address_hash: address.hash, + token_contract_address_hash: erc_721_token.contract_address_hash + ) + |> Repo.preload([:token]) + end + # works because one token_id per token, despite ordering in DB: [asc: ti.token_contract_address_hash, desc: ti.token_id] + |> Enum.sort_by(&{&1.token_contract_address_hash, &1.token_id}, :desc) + + request = get(conn, endpoint.(address.hash)) + assert response = json_response(request, 200) + + request_2nd_page = get(conn, endpoint.(address.hash), response["next_page_params"]) + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, token_instances) + end + + test "get paginated ERC-1155 nft", %{conn: conn, endpoint: endpoint} do + address = insert(:address) + + insert_list(51, :address_current_token_balance_with_token_id) + + token_instances = + for _ <- 0..50 do + token = insert(:token, type: "ERC-1155") + + ti = + insert(:token_instance, + token_contract_address_hash: token.contract_address_hash + ) + |> Repo.preload([:token]) + + current_token_balance = + insert(:address_current_token_balance_with_token_id_and_fixed_token_type, + address: address, + token_type: "ERC-1155", + token_id: ti.token_id, + token_contract_address_hash: token.contract_address_hash + ) + + %Instance{ti | current_token_balance: current_token_balance} + end + |> Enum.sort_by(&{&1.token_contract_address_hash, &1.token_id}, :desc) + + request = get(conn, endpoint.(address.hash)) + assert response = json_response(request, 200) + + request_2nd_page = get(conn, endpoint.(address.hash), response["next_page_params"]) + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, token_instances) + end + + test "get paginated ERC-404 nft", %{conn: conn, endpoint: endpoint} do + address = insert(:address) + + insert_list(51, :address_current_token_balance_with_token_id) + + token_instances = + for _ <- 0..50 do + token = insert(:token, type: "ERC-404") + + ti = + insert(:token_instance, + token_contract_address_hash: token.contract_address_hash + ) + |> Repo.preload([:token]) + + current_token_balance = + insert(:address_current_token_balance_with_token_id_and_fixed_token_type, + address: address, + token_type: "ERC-404", + token_id: ti.token_id, + token_contract_address_hash: token.contract_address_hash + ) + + %Instance{ti | current_token_balance: current_token_balance} + end + |> Enum.sort_by(&{&1.token_contract_address_hash, &1.token_id}, :desc) + + request = get(conn, endpoint.(address.hash)) + assert response = json_response(request, 200) + + request_2nd_page = get(conn, endpoint.(address.hash), response["next_page_params"]) + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, token_instances) + end + + test "test filters", %{conn: conn, endpoint: endpoint} do + address = insert(:address) + + insert_list(51, :token_instance) + + token_instances_721 = + for _ <- 0..50 do + erc_721_token = insert(:token, type: "ERC-721") + + insert(:token_instance, + owner_address_hash: address.hash, + token_contract_address_hash: erc_721_token.contract_address_hash + ) + |> Repo.preload([:token]) + end + |> Enum.sort_by(&{&1.token_contract_address_hash, &1.token_id}, :desc) + + insert_list(51, :address_current_token_balance_with_token_id) + + token_instances_1155 = + for _ <- 0..50 do + token = insert(:token, type: "ERC-1155") + + ti = + insert(:token_instance, + token_contract_address_hash: token.contract_address_hash + ) + |> Repo.preload([:token]) + + current_token_balance = + insert(:address_current_token_balance_with_token_id_and_fixed_token_type, + address: address, + token_type: "ERC-1155", + token_id: ti.token_id, + token_contract_address_hash: token.contract_address_hash + ) + + %Instance{ti | current_token_balance: current_token_balance} + end + |> Enum.sort_by(&{&1.token_contract_address_hash, &1.token_id}, :desc) + + filter = %{"type" => "ERC-721"} + request = get(conn, endpoint.(address.hash), filter) + assert response = json_response(request, 200) + + request_2nd_page = get(conn, endpoint.(address.hash), Map.merge(response["next_page_params"], filter)) + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, token_instances_721) + + filter = %{"type" => "ERC-1155"} + request = get(conn, endpoint.(address.hash), filter) + assert response = json_response(request, 200) + + request_2nd_page = get(conn, endpoint.(address.hash), Map.merge(response["next_page_params"], filter)) + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, token_instances_1155) + end + + test "return all token instances", %{conn: conn, endpoint: endpoint} do + address = insert(:address) + + insert_list(51, :token_instance) + + token_instances_721 = + for _ <- 0..50 do + erc_721_token = insert(:token, type: "ERC-721") + + insert(:token_instance, + owner_address_hash: address.hash, + token_contract_address_hash: erc_721_token.contract_address_hash + ) + |> Repo.preload([:token]) + end + |> Enum.sort_by(&{&1.token_contract_address_hash, &1.token_id}, :desc) + + insert_list(51, :address_current_token_balance_with_token_id) + + token_instances_1155 = + for _ <- 0..50 do + token = insert(:token, type: "ERC-1155") + + ti = + insert(:token_instance, + token_contract_address_hash: token.contract_address_hash + ) + |> Repo.preload([:token]) + + current_token_balance = + insert(:address_current_token_balance_with_token_id_and_fixed_token_type, + address: address, + token_type: "ERC-1155", + token_id: ti.token_id, + token_contract_address_hash: token.contract_address_hash + ) + + %Instance{ti | current_token_balance: current_token_balance} + end + |> Enum.sort_by(&{&1.token_contract_address_hash, &1.token_id}, :desc) + + request = get(conn, endpoint.(address.hash)) + assert response = json_response(request, 200) + + request_2nd_page = get(conn, endpoint.(address.hash), response["next_page_params"]) + assert response_2nd_page = json_response(request_2nd_page, 200) + + request_3rd_page = get(conn, endpoint.(address.hash), response_2nd_page["next_page_params"]) + assert response_3rd_page = json_response(request_3rd_page, 200) + + assert response["next_page_params"] != nil + assert response_2nd_page["next_page_params"] != nil + assert response_3rd_page["next_page_params"] == nil + + assert Enum.count(response["items"]) == 50 + assert Enum.count(response_2nd_page["items"]) == 50 + assert Enum.count(response_3rd_page["items"]) == 2 + + compare_item(Enum.at(token_instances_721, 50), Enum.at(response["items"], 0)) + compare_item(Enum.at(token_instances_721, 1), Enum.at(response["items"], 49)) + + compare_item(Enum.at(token_instances_721, 0), Enum.at(response_2nd_page["items"], 0)) + compare_item(Enum.at(token_instances_1155, 50), Enum.at(response_2nd_page["items"], 1)) + compare_item(Enum.at(token_instances_1155, 2), Enum.at(response_2nd_page["items"], 49)) + + compare_item(Enum.at(token_instances_1155, 1), Enum.at(response_3rd_page["items"], 0)) + compare_item(Enum.at(token_instances_1155, 0), Enum.at(response_3rd_page["items"], 1)) + end + end + + describe "/addresses/{address_hash}/nft/collections" do + setup do + {:ok, endpoint: &"/api/v2/addresses/#{&1}/nft/collections"} + end + + test "get 200 on non existing address", %{conn: conn, endpoint: endpoint} do + address = build(:address) + + request = get(conn, endpoint.(address.hash)) + + assert %{"items" => [], "next_page_params" => nil} = json_response(request, 200) + end + + test "get 422 on invalid address", %{conn: conn, endpoint: endpoint} do + request = get(conn, endpoint.("0x")) + + assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + end + + test "get paginated erc-721 collection", %{conn: conn, endpoint: endpoint} do + address = insert(:address) + + insert_list(51, :address_current_token_balance_with_token_id) + insert_list(51, :token_instance) + + ctbs = + for _ <- 0..50 do + token = insert(:token, type: "ERC-721") + amount = Enum.random(16..50) + + current_token_balance = + insert(:address_current_token_balance, + address: address, + token_type: "ERC-721", + token_id: nil, + token_contract_address_hash: token.contract_address_hash, + value: amount + ) + |> Repo.preload([:token]) + + token_instances = + for _ <- 0..(amount - 1) do + ti = + insert(:token_instance, + token_contract_address_hash: token.contract_address_hash, + owner_address_hash: address.hash + ) + |> Repo.preload([:token]) + + %Instance{ti | current_token_balance: current_token_balance} + end + |> Enum.sort_by(&{&1.token_contract_address_hash, &1.token_id}, :desc) + + {current_token_balance, token_instances} + end + |> Enum.sort_by(&elem(&1, 0).token_contract_address_hash, :desc) + + request = get(conn, endpoint.(address.hash)) + assert response = json_response(request, 200) + + request_2nd_page = get(conn, endpoint.(address.hash), response["next_page_params"]) + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, ctbs) + end + + test "get paginated erc-1155 collection", %{conn: conn, endpoint: endpoint} do + address = insert(:address) + + insert_list(51, :address_current_token_balance_with_token_id) + insert_list(51, :token_instance) + + collections = + for _ <- 0..50 do + token = insert(:token, type: "ERC-1155") + amount = Enum.random(16..50) + + token_instances = + for _ <- 0..(amount - 1) do + ti = + insert(:token_instance, + token_contract_address_hash: token.contract_address_hash + ) + |> Repo.preload([:token]) + + current_token_balance = + insert(:address_current_token_balance, + address: address, + token_type: "ERC-1155", + token_id: ti.token_id, + token_contract_address_hash: token.contract_address_hash, + value: Enum.random(1..100_000) + ) + |> Repo.preload([:token]) + + %Instance{ti | current_token_balance: current_token_balance} + end + |> Enum.sort_by(& &1.token_id, :desc) + + {token, amount, token_instances} + end + |> Enum.sort_by(&elem(&1, 0).contract_address_hash, :desc) + + request = get(conn, endpoint.(address.hash)) + assert response = json_response(request, 200) + + request_2nd_page = get(conn, endpoint.(address.hash), response["next_page_params"]) + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, collections) + end + + test "test filters", %{conn: conn, endpoint: endpoint} do + address = insert(:address) + + insert_list(51, :address_current_token_balance_with_token_id) + insert_list(51, :token_instance) + + ctbs = + for _ <- 0..50 do + token = insert(:token, type: "ERC-721") + amount = Enum.random(16..50) + + current_token_balance = + insert(:address_current_token_balance, + address: address, + token_type: "ERC-721", + token_id: nil, + token_contract_address_hash: token.contract_address_hash, + value: amount + ) + |> Repo.preload([:token]) + + token_instances = + for _ <- 0..(amount - 1) do + ti = + insert(:token_instance, + token_contract_address_hash: token.contract_address_hash, + owner_address_hash: address.hash + ) + |> Repo.preload([:token]) + + %Instance{ti | current_token_balance: current_token_balance} + end + |> Enum.sort_by(& &1.token_id, :desc) + + {current_token_balance, token_instances} + end + |> Enum.sort_by(&elem(&1, 0).token_contract_address_hash, :desc) + + collections = + for _ <- 0..50 do + token = insert(:token, type: "ERC-1155") + amount = Enum.random(16..50) + + token_instances = + for _ <- 0..(amount - 1) do + ti = + insert(:token_instance, + token_contract_address_hash: token.contract_address_hash, + owner_address_hash: address.hash + ) + |> Repo.preload([:token]) + + current_token_balance = + insert(:address_current_token_balance, + address: address, + token_type: "ERC-1155", + token_id: ti.token_id, + token_contract_address_hash: token.contract_address_hash, + value: Enum.random(1..100_000) + ) + |> Repo.preload([:token]) + + %Instance{ti | current_token_balance: current_token_balance} + end + |> Enum.sort_by(& &1.token_id, :desc) + + {token, amount, token_instances} + end + |> Enum.sort_by(&elem(&1, 0).contract_address_hash, :desc) + + filter = %{"type" => "ERC-721"} + request = get(conn, endpoint.(address.hash), filter) + assert response = json_response(request, 200) + + request_2nd_page = get(conn, endpoint.(address.hash), Map.merge(response["next_page_params"], filter)) + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, ctbs) + + filter = %{"type" => "ERC-1155"} + request = get(conn, endpoint.(address.hash), filter) + assert response = json_response(request, 200) + + request_2nd_page = get(conn, endpoint.(address.hash), Map.merge(response["next_page_params"], filter)) + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, collections) + end + + test "return all collections", %{conn: conn, endpoint: endpoint} do + address = insert(:address) + + insert_list(51, :address_current_token_balance_with_token_id) + insert_list(51, :token_instance) + + collections_721 = + for _ <- 0..50 do + token = insert(:token, type: "ERC-721") + amount = Enum.random(16..50) + + current_token_balance = + insert(:address_current_token_balance, + address: address, + token_type: "ERC-721", + token_id: nil, + token_contract_address_hash: token.contract_address_hash, + value: amount + ) + |> Repo.preload([:token]) + + token_instances = + for _ <- 0..(amount - 1) do + ti = + insert(:token_instance, + token_contract_address_hash: token.contract_address_hash, + owner_address_hash: address.hash + ) + |> Repo.preload([:token]) + + %Instance{ti | current_token_balance: current_token_balance} + end + |> Enum.sort_by(& &1.token_id, :desc) + + {current_token_balance, token_instances} + end + |> Enum.sort_by(&elem(&1, 0).token_contract_address_hash, :desc) + + collections_1155 = + for _ <- 0..50 do + token = insert(:token, type: "ERC-1155") + amount = Enum.random(16..50) + + token_instances = + for _ <- 0..(amount - 1) do + ti = + insert(:token_instance, + token_contract_address_hash: token.contract_address_hash, + owner_address_hash: address.hash + ) + |> Repo.preload([:token]) + + current_token_balance = + insert(:address_current_token_balance, + address: address, + token_type: "ERC-1155", + token_id: ti.token_id, + token_contract_address_hash: token.contract_address_hash, + value: Enum.random(1..100_000) + ) + |> Repo.preload([:token]) + + %Instance{ti | current_token_balance: current_token_balance} + end + |> Enum.sort_by(& &1.token_id, :desc) + + {token, amount, token_instances} + end + |> Enum.sort_by(&elem(&1, 0).contract_address_hash, :desc) + + request = get(conn, endpoint.(address.hash)) + assert response = json_response(request, 200) + + request_2nd_page = get(conn, endpoint.(address.hash), response["next_page_params"]) + assert response_2nd_page = json_response(request_2nd_page, 200) + + request_3rd_page = get(conn, endpoint.(address.hash), response_2nd_page["next_page_params"]) + assert response_3rd_page = json_response(request_3rd_page, 200) + + assert response["next_page_params"] != nil + assert response_2nd_page["next_page_params"] != nil + assert response_3rd_page["next_page_params"] == nil + + assert Enum.count(response["items"]) == 50 + assert Enum.count(response_2nd_page["items"]) == 50 + assert Enum.count(response_3rd_page["items"]) == 2 + + compare_item(Enum.at(collections_721, 50), Enum.at(response["items"], 0)) + compare_item(Enum.at(collections_721, 1), Enum.at(response["items"], 49)) + + compare_item(Enum.at(collections_721, 0), Enum.at(response_2nd_page["items"], 0)) + compare_item(Enum.at(collections_1155, 50), Enum.at(response_2nd_page["items"], 1)) + compare_item(Enum.at(collections_1155, 2), Enum.at(response_2nd_page["items"], 49)) + + compare_item(Enum.at(collections_1155, 1), Enum.at(response_3rd_page["items"], 0)) + compare_item(Enum.at(collections_1155, 0), Enum.at(response_3rd_page["items"], 1)) + end + end + + defp compare_item(%Address{} = address, json) do + assert Address.checksum(address.hash) == json["hash"] + assert to_string(address.transactions_count) == json["transaction_count"] + end + + defp compare_item(%Transaction{} = transaction, json) do + assert to_string(transaction.hash) == json["hash"] + assert transaction.block_number == json["block_number"] + assert to_string(transaction.value.value) == json["value"] + assert Address.checksum(transaction.from_address_hash) == json["from"]["hash"] + assert Address.checksum(transaction.to_address_hash) == json["to"]["hash"] + end + + defp compare_item(%TokenTransfer{} = token_transfer, json) do + assert Address.checksum(token_transfer.from_address_hash) == json["from"]["hash"] + assert Address.checksum(token_transfer.to_address_hash) == json["to"]["hash"] + assert to_string(token_transfer.transaction_hash) == json["transaction_hash"] + assert json["timestamp"] != nil + assert json["method"] != nil + assert to_string(token_transfer.block_hash) == json["block_hash"] + assert token_transfer.log_index == json["log_index"] + assert check_total(Repo.preload(token_transfer, [{:token, :contract_address}]).token, json["total"], token_transfer) + end + + defp compare_item(%InternalTransaction{} = internal_transaction, json) do + assert internal_transaction.block_number == json["block_number"] + assert to_string(internal_transaction.gas) == json["gas_limit"] + assert internal_transaction.index == json["index"] + assert to_string(internal_transaction.transaction_hash) == json["transaction_hash"] + assert Address.checksum(internal_transaction.from_address_hash) == json["from"]["hash"] + assert Address.checksum(internal_transaction.to_address_hash) == json["to"]["hash"] + end + + defp compare_item(%Block{} = block, json) do + assert to_string(block.hash) == json["hash"] + assert block.number == json["height"] + end + + defp compare_item(%CurrentTokenBalance{} = ctb, json) do + assert to_string(ctb.value) == json["value"] + assert (ctb.token_id && to_string(ctb.token_id)) == json["token_id"] + compare_item(ctb.token, json["token"]) + end + + defp compare_item(%CoinBalance{} = cb, json) do + assert to_string(cb.value.value) == json["value"] + assert cb.block_number == json["block_number"] + + assert Jason.encode!(Repo.get_by(Block, number: cb.block_number).timestamp) =~ + String.replace(json["block_timestamp"], "Z", "") + end + + defp compare_item(%Token{} = token, json) do + assert Address.checksum(token.contract_address_hash) == json["address"] + assert to_string(token.symbol) == json["symbol"] + assert to_string(token.name) == json["name"] + assert to_string(token.type) == json["type"] + assert to_string(token.decimals) == json["decimals"] + assert (token.holder_count && to_string(token.holder_count)) == json["holders"] + assert Map.has_key?(json, "exchange_rate") + end + + defp compare_item(%Log{} = log, json) do + assert log.index == json["index"] + assert to_string(log.data) == json["data"] + assert Address.checksum(log.address_hash) == json["address"]["hash"] + assert to_string(log.transaction_hash) == json["transaction_hash"] + assert json["block_number"] == log.block_number + assert json["block_hash"] == to_string(log.block_hash) + end + + defp compare_item(%Withdrawal{} = withdrawal, json) do + assert withdrawal.index == json["index"] + end + + defp compare_item(%Instance{token: %Token{} = token} = instance, json) do + token_type = token.type + value = to_string(value(token.type, instance)) + id = to_string(instance.token_id) + metadata = instance.metadata + token_address_hash = Address.checksum(token.contract_address_hash) + app_url = instance.metadata["external_url"] + animation_url = instance.metadata["animation_url"] + image_url = instance.metadata["image_url"] + token_name = token.name + + assert %{ + "token_type" => ^token_type, + "value" => ^value, + "id" => ^id, + "metadata" => ^metadata, + "owner" => nil, + "token" => %{"address" => ^token_address_hash, "name" => ^token_name, "type" => ^token_type}, + "external_app_url" => ^app_url, + "animation_url" => ^animation_url, + "image_url" => ^image_url, + "is_unique" => nil + } = json + end + + defp compare_item({%CurrentTokenBalance{token: token} = ctb, token_instances}, json) do + token_type = token.type + token_address_hash = Address.checksum(token.contract_address_hash) + token_name = token.name + amount = to_string(ctb.distinct_token_instances_count || ctb.value) + + assert Enum.count(json["token_instances"]) == @instances_amount_in_collection + + token_instances + |> Enum.take(@instances_amount_in_collection) + |> Enum.with_index() + |> Enum.each(fn {instance, index} -> + compare_token_instance_in_collection(instance, Enum.at(json["token_instances"], index)) + end) + + assert %{ + "token" => %{"address" => ^token_address_hash, "name" => ^token_name, "type" => ^token_type}, + "amount" => ^amount + } = json + end + + defp compare_item({token, amount, token_instances}, json) do + token_type = token.type + token_address_hash = Address.checksum(token.contract_address_hash) + token_name = token.name + amount = to_string(amount) + + assert Enum.count(json["token_instances"]) == @instances_amount_in_collection + + token_instances + |> Enum.take(@instances_amount_in_collection) + |> Enum.with_index() + |> Enum.each(fn {instance, index} -> + compare_token_instance_in_collection(instance, Enum.at(json["token_instances"], index)) + end) + + assert %{ + "token" => %{"address" => ^token_address_hash, "name" => ^token_name, "type" => ^token_type}, + "amount" => ^amount + } = json + end + + defp compare_token_instance_in_collection(%Instance{token: %Token{} = token} = instance, json) do + token_type = token.type + value = to_string(value(token.type, instance)) + id = to_string(instance.token_id) + metadata = instance.metadata + app_url = instance.metadata["external_url"] + animation_url = instance.metadata["animation_url"] + image_url = instance.metadata["image_url"] + + assert %{ + "token_type" => ^token_type, + "value" => ^value, + "id" => ^id, + "metadata" => ^metadata, + "owner" => nil, + "token" => nil, + "external_app_url" => ^app_url, + "animation_url" => ^animation_url, + "image_url" => ^image_url, + "is_unique" => nil + } = json + end + + defp value("ERC-721", _), do: 1 + defp value(_, nft), do: nft.current_token_balance.value + + defp check_paginated_response(first_page_resp, second_page_resp, list) do + assert Enum.count(first_page_resp["items"]) == 50 + assert first_page_resp["next_page_params"] != nil + compare_item(Enum.at(list, 50), Enum.at(first_page_resp["items"], 0)) + compare_item(Enum.at(list, 1), Enum.at(first_page_resp["items"], 49)) + + assert Enum.count(second_page_resp["items"]) == 1 + assert second_page_resp["next_page_params"] == nil + compare_item(Enum.at(list, 0), Enum.at(second_page_resp["items"], 0)) + end + + # with the current implementation no transfers should come with list in totals + def check_total(%Token{type: nft}, json, _token_transfer) when nft in ["ERC-721", "ERC-1155"] and is_list(json) do + false + end + + def check_total(%Token{type: nft}, json, token_transfer) when nft in ["ERC-1155"] do + json["token_id"] in Enum.map(token_transfer.token_ids, fn x -> to_string(x) end) and + json["value"] == to_string(token_transfer.amount) + end + + def check_total(%Token{type: nft}, json, token_transfer) when nft in ["ERC-721"] do + json["token_id"] in Enum.map(token_transfer.token_ids, fn x -> to_string(x) end) + end + + def check_total(_, _, _), do: true +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/api/v2/advanced_filter_controller_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/api/v2/advanced_filter_controller_test.exs new file mode 100644 index 0000000..17c301b --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/api/v2/advanced_filter_controller_test.exs @@ -0,0 +1,1060 @@ +defmodule BlockScoutWeb.API.V2.AdvancedFilterControllerTest do + use BlockScoutWeb.ConnCase + + import Mox + + alias Explorer.Chain.SmartContract + alias Explorer.Chain.{AdvancedFilter, Data, Hash} + alias Explorer.{Factory, TestHelper} + + describe "/advanced_filters" do + test "empty list", %{conn: conn} do + request = get(conn, "/api/v2/advanced-filters") + assert response = json_response(request, 200) + assert response["items"] == [] + assert response["next_page_params"] == nil + end + + test "get and paginate advanced filter (transactions split between pages)", %{conn: conn} do + first_transaction = :transaction |> insert() |> with_block() + insert_list(3, :token_transfer, transaction: first_transaction) + + for i <- 1..3 do + insert(:internal_transaction, + transaction: first_transaction, + block_hash: first_transaction.block_hash, + index: i, + block_index: i + ) + end + + insert_list(51, :transaction) |> with_block() + + request = get(conn, "/api/v2/advanced-filters") + assert response = json_response(request, 200) + request_2nd_page = get(conn, "/api/v2/advanced-filters", response["next_page_params"]) + + assert response_2nd_page = json_response(request_2nd_page, 200) + check_paginated_response(AdvancedFilter.list(), response["items"], response_2nd_page["items"]) + end + + test "get and paginate advanced filter (token transfers split between pages)", %{conn: conn} do + first_transaction = :transaction |> insert() |> with_block() + insert_list(3, :token_transfer, transaction: first_transaction) + + for i <- 1..3 do + insert(:internal_transaction, + transaction: first_transaction, + block_hash: first_transaction.block_hash, + index: i, + block_index: i + ) + end + + second_transaction = :transaction |> insert() |> with_block() + insert_list(50, :token_transfer, transaction: second_transaction, block_number: second_transaction.block_number) + + request = get(conn, "/api/v2/advanced-filters") + assert response = json_response(request, 200) + request_2nd_page = get(conn, "/api/v2/advanced-filters", response["next_page_params"]) + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(AdvancedFilter.list(), response["items"], response_2nd_page["items"]) + end + + test "get and paginate advanced filter (batch token transfers split between pages)", %{conn: conn} do + first_transaction = :transaction |> insert() |> with_block() + insert_list(3, :token_transfer, transaction: first_transaction) + + for i <- 1..3 do + insert(:internal_transaction, + transaction: first_transaction, + block_hash: first_transaction.block_hash, + index: i, + block_index: i + ) + end + + second_transaction = :transaction |> insert() |> with_block() + + insert_list(5, :token_transfer, + transaction: second_transaction, + block_number: second_transaction.block_number, + token_type: "ERC-1155", + token_ids: 0..10 |> Enum.to_list(), + amounts: 10..20 |> Enum.to_list() + ) + + request = get(conn, "/api/v2/advanced-filters") + assert response = json_response(request, 200) + request_2nd_page = get(conn, "/api/v2/advanced-filters", response["next_page_params"]) + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(AdvancedFilter.list(), response["items"], response_2nd_page["items"]) + end + + test "get and paginate advanced filter (internal transactions split between pages)", %{conn: conn} do + first_transaction = :transaction |> insert() |> with_block() + insert_list(3, :token_transfer, transaction: first_transaction) + + for i <- 1..3 do + insert(:internal_transaction, + transaction: first_transaction, + block_hash: first_transaction.block_hash, + index: i, + block_index: i + ) + end + + second_transaction = :transaction |> insert() |> with_block() + + for i <- 1..50 do + insert(:internal_transaction, + transaction: second_transaction, + block_hash: second_transaction.block_hash, + index: i, + block_index: i + ) + end + + request = get(conn, "/api/v2/advanced-filters") + assert response = json_response(request, 200) + request_2nd_page = get(conn, "/api/v2/advanced-filters", response["next_page_params"]) + + assert response_2nd_page = json_response(request_2nd_page, 200) + check_paginated_response(AdvancedFilter.list(), response["items"], response_2nd_page["items"]) + end + + test "filter by transaction_type", %{conn: conn} do + 30 |> insert_list(:transaction) |> with_block() + + transaction = insert(:transaction) |> with_block() + + for token_type <- ~w(ERC-20 ERC-404 ERC-721 ERC-1155), + token = insert(:token, type: token_type), + _ <- 0..4 do + insert(:token_transfer, + transaction: transaction, + token_type: token_type, + token: token, + token_contract_address_hash: token.contract_address_hash, + token_contract_address: token.contract_address + ) + end + + transaction = :transaction |> insert() |> with_block() + + for i <- 1..30 do + insert(:internal_transaction, + transaction: transaction, + block_hash: transaction.block_hash, + index: i, + block_index: i + ) + end + + for transaction_type_filter_string <- + ~w(COIN_TRANSFER COIN_TRANSFER,ERC-404 ERC-721,ERC-1155 ERC-20,COIN_TRANSFER,ERC-1155) do + transaction_type_filter = transaction_type_filter_string |> String.split(",") + request = get(conn, "/api/v2/advanced-filters", %{"transaction_types" => transaction_type_filter_string}) + assert response = json_response(request, 200) + + assert Enum.all?(response["items"], fn item -> String.upcase(item["type"]) in transaction_type_filter end) + + if response["next_page_params"] do + request_2nd_page = + get( + conn, + "/api/v2/advanced-filters", + Map.merge(%{"transaction_types" => transaction_type_filter_string}, response["next_page_params"]) + ) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + assert Enum.all?(response_2nd_page["items"], fn item -> + String.upcase(item["type"]) in transaction_type_filter + end) + + check_paginated_response( + AdvancedFilter.list(transaction_types: transaction_type_filter), + response["items"], + response_2nd_page["items"] + ) + end + end + end + + test "filter by methods", %{conn: conn} do + TestHelper.get_all_proxies_implementation_zero_addresses() + + transaction = :transaction |> insert() |> with_block() + + smart_contract = build(:smart_contract) + + contract_address = + insert(:address, + hash: address_hash(), + verified: true, + contract_code: Factory.contract_code_info().bytecode, + smart_contract: smart_contract + ) + + method_id1_string = "0xa9059cbb" + method_id2_string = "0xa0712d68" + method_id3_string = "0x095ea7b3" + method_id4_string = "0x40993b26" + + {:ok, method1} = Data.cast(method_id1_string <> "ab0ba0") + {:ok, method2} = Data.cast(method_id2_string <> "ab0ba0") + {:ok, method3} = Data.cast(method_id3_string <> "ab0ba0") + {:ok, method4} = Data.cast(method_id4_string <> "ab0ba0") + + for i <- 1..5 do + insert(:internal_transaction, + transaction: transaction, + to_address_hash: contract_address.hash, + to_address: contract_address, + block_hash: transaction.block_hash, + index: i, + block_index: i, + input: method1 + ) + end + + for i <- 6..10 do + insert(:internal_transaction, + transaction: transaction, + to_address_hash: contract_address.hash, + to_address: contract_address, + block_hash: transaction.block_hash, + index: i, + block_index: i, + input: method2 + ) + end + + 5 + |> insert_list(:transaction, to_address_hash: contract_address.hash, to_address: contract_address, input: method2) + |> with_block() + + 5 + |> insert_list(:transaction, to_address_hash: contract_address.hash, to_address: contract_address, input: method3) + |> with_block() + + method3_transaction = + :transaction + |> insert(to_address_hash: contract_address.hash, to_address: contract_address, input: method3) + |> with_block() + + method4_transaction = + :transaction + |> insert(to_address_hash: contract_address.hash, to_address: contract_address, input: method4) + |> with_block() + + 5 |> insert_list(:token_transfer, transaction: method3_transaction) + 5 |> insert_list(:token_transfer, transaction: method4_transaction) + + request = get(conn, "/api/v2/advanced-filters", %{"methods" => "0xa0712d68,0x095ea7b3"}) + assert response = json_response(request, 200) + + assert Enum.all?(response["items"], fn item -> + String.slice(item["method"], 0..9) in [method_id2_string, method_id3_string] + end) + + assert Enum.count(response["items"]) == 21 + end + + test "filter by age", %{conn: conn} do + [_, transaction_a, _, transaction_b, _] = + for i <- 0..4 do + tx = :transaction |> insert() |> with_block(status: :ok) + + insert(:internal_transaction, + transaction: tx, + index: i + 1, + block_index: i + 1, + block_hash: tx.block_hash, + block: tx.block + ) + + insert(:token_transfer, + transaction: tx, + block_number: tx.block_number, + log_index: i, + block_hash: tx.block_hash, + block: tx.block + ) + + tx + end + + request = + get(conn, "/api/v2/advanced-filters", %{ + "age_from" => DateTime.to_iso8601(transaction_a.block.timestamp), + "age_to" => DateTime.to_iso8601(transaction_b.block.timestamp) + }) + + assert response = json_response(request, 200) + + assert Enum.count(response["items"]) == 9 + end + + test "filter by from address include", %{conn: conn} do + address = insert(:address) + + for i <- 0..4 do + transaction = :transaction |> insert() |> with_block() + + if i < 2 do + :transaction |> insert(from_address_hash: address.hash, from_address: address) |> with_block() + + insert(:internal_transaction, + transaction: transaction, + from_address_hash: address.hash, + from_address: address, + block_hash: transaction.block_hash, + index: i + 1, + block_index: i + 1 + ) + + insert(:token_transfer, + from_address_hash: address.hash, + from_address: address, + transaction: transaction, + block_number: transaction.block_number, + log_index: i + ) + else + insert(:internal_transaction, + transaction: transaction, + block_hash: transaction.block_hash, + index: i + 1, + block_index: i + 1 + ) + + insert(:token_transfer, transaction: transaction, block_number: transaction.block_number, log_index: i) + end + end + + request = get(conn, "/api/v2/advanced-filters", %{"from_address_hashes_to_include" => to_string(address.hash)}) + + assert response = json_response(request, 200) + + assert Enum.count(response["items"]) == 6 + end + + test "filter by from address exclude", %{conn: conn} do + address = insert(:address) + + for i <- 0..4 do + transaction = :transaction |> insert() |> with_block() + + if i < 4 do + :transaction |> insert(from_address_hash: address.hash, from_address: address) |> with_block() + + insert(:internal_transaction, + transaction: transaction, + from_address_hash: address.hash, + from_address: address, + block_hash: transaction.block_hash, + index: i + 1, + block_index: i + 1 + ) + + insert(:token_transfer, + from_address_hash: address.hash, + from_address: address, + transaction: transaction, + block_number: transaction.block_number, + log_index: i + ) + else + insert(:internal_transaction, + transaction: transaction, + block_hash: transaction.block_hash, + index: i + 1, + block_index: i + 1 + ) + + insert(:token_transfer, transaction: transaction, block_number: transaction.block_number, log_index: i) + end + end + + request = get(conn, "/api/v2/advanced-filters", %{"from_address_hashes_to_exclude" => to_string(address.hash)}) + + assert response = json_response(request, 200) + + assert Enum.count(response["items"]) == 7 + end + + test "filter by from address include and exclude", %{conn: conn} do + address_to_include = insert(:address) + address_to_exclude = insert(:address) + + for i <- 0..2 do + transaction = + :transaction + |> insert(from_address_hash: address_to_exclude.hash, from_address: address_to_exclude) + |> with_block() + + if i < 4 do + :transaction + |> insert(from_address_hash: address_to_include.hash, from_address: address_to_include) + |> with_block() + + insert(:internal_transaction, + transaction: transaction, + from_address_hash: address_to_include.hash, + from_address: address_to_include, + block_hash: transaction.block_hash, + index: i + 1, + block_index: i + 1 + ) + + insert(:token_transfer, + from_address_hash: address_to_include.hash, + from_address: address_to_include, + transaction: transaction, + block_number: transaction.block_number, + log_index: i + ) + else + insert(:internal_transaction, + transaction: transaction, + block_hash: transaction.block_hash, + index: i + 1, + block_index: i + 1 + ) + + insert(:token_transfer, transaction: transaction, block_number: transaction.block_number, log_index: i) + end + end + + request = + get(conn, "/api/v2/advanced-filters", %{ + "from_address_hashes_to_include" => to_string(address_to_include.hash), + "from_address_hashes_to_exclude" => to_string(address_to_exclude.hash) + }) + + assert response = json_response(request, 200) + + assert Enum.count(response["items"]) == 9 + end + + test "filter by to address include", %{conn: conn} do + address = insert(:address) + + for i <- 0..4 do + transaction = :transaction |> insert() |> with_block() + + if i < 2 do + :transaction |> insert(to_address_hash: address.hash, to_address: address) |> with_block() + + insert(:internal_transaction, + transaction: transaction, + to_address_hash: address.hash, + to_address: address, + block_hash: transaction.block_hash, + index: i + 1, + block_index: i + 1 + ) + + insert(:token_transfer, + to_address_hash: address.hash, + to_address: address, + transaction: transaction, + block_number: transaction.block_number, + log_index: i + ) + else + insert(:internal_transaction, + transaction: transaction, + block_hash: transaction.block_hash, + index: i + 1, + block_index: i + 1 + ) + + insert(:token_transfer, transaction: transaction, block_number: transaction.block_number, log_index: i) + end + end + + request = get(conn, "/api/v2/advanced-filters", %{"to_address_hashes_to_include" => to_string(address.hash)}) + + assert response = json_response(request, 200) + + assert Enum.count(response["items"]) == 6 + end + + test "filter by to address exclude", %{conn: conn} do + address = insert(:address) + + for i <- 0..4 do + transaction = :transaction |> insert() |> with_block() + + if i < 4 do + :transaction |> insert(to_address_hash: address.hash, to_address: address) |> with_block() + + insert(:internal_transaction, + transaction: transaction, + to_address_hash: address.hash, + to_address: address, + block_hash: transaction.block_hash, + index: i + 1, + block_index: i + 1 + ) + + insert(:token_transfer, + to_address_hash: address.hash, + to_address: address, + transaction: transaction, + block_number: transaction.block_number, + log_index: i + ) + else + insert(:internal_transaction, + transaction: transaction, + block_hash: transaction.block_hash, + index: i + 1, + block_index: i + 1 + ) + + insert(:token_transfer, transaction: transaction, block_number: transaction.block_number, log_index: i) + end + end + + request = get(conn, "/api/v2/advanced-filters", %{"to_address_hashes_to_exclude" => to_string(address.hash)}) + + assert response = json_response(request, 200) + + assert Enum.count(response["items"]) == 7 + end + + test "filter by to address include and exclude", %{conn: conn} do + address_to_include = insert(:address) + address_to_exclude = insert(:address) + + for i <- 0..2 do + transaction = + :transaction + |> insert(to_address_hash: address_to_exclude.hash, to_address: address_to_exclude) + |> with_block() + + if i < 4 do + :transaction + |> insert(to_address_hash: address_to_include.hash, to_address: address_to_include) + |> with_block() + + insert(:internal_transaction, + transaction: transaction, + to_address_hash: address_to_include.hash, + to_address: address_to_include, + block_hash: transaction.block_hash, + index: i + 1, + block_index: i + 1 + ) + + insert(:token_transfer, + to_address_hash: address_to_include.hash, + to_address: address_to_include, + transaction: transaction, + block_number: transaction.block_number, + log_index: i + ) + else + insert(:internal_transaction, + transaction: transaction, + block_hash: transaction.block_hash, + index: i + 1, + block_index: i + 1 + ) + + insert(:token_transfer, transaction: transaction, block_number: transaction.block_number, log_index: i) + end + end + + request = + get(conn, "/api/v2/advanced-filters", %{ + "to_address_hashes_to_include" => to_string(address_to_include.hash), + "to_address_hashes_to_exclude" => to_string(address_to_exclude.hash) + }) + + assert response = json_response(request, 200) + + assert Enum.count(response["items"]) == 9 + end + + test "filter by from and to address", %{conn: conn} do + from_address = insert(:address) + to_address = insert(:address) + + for i <- 0..8 do + transaction = :transaction |> insert() |> with_block() + + cond do + i < 2 -> + :transaction |> insert(from_address_hash: from_address.hash, from_address: from_address) |> with_block() + + insert(:internal_transaction, + transaction: transaction, + from_address_hash: from_address.hash, + from_address: from_address, + block_hash: transaction.block_hash, + index: i + 1, + block_index: i + 1 + ) + + insert(:token_transfer, + from_address_hash: from_address.hash, + from_address: from_address, + transaction: transaction, + block_number: transaction.block_number, + log_index: i + ) + + i < 4 -> + :transaction |> insert(to_address_hash: to_address.hash, to_address: to_address) |> with_block() + + insert(:internal_transaction, + transaction: transaction, + to_address_hash: to_address.hash, + to_address: to_address, + block_hash: transaction.block_hash, + index: i + 1, + block_index: i + 1 + ) + + insert(:token_transfer, + to_address_hash: to_address.hash, + to_address: to_address, + transaction: transaction, + block_number: transaction.block_number, + log_index: i + ) + + i < 6 -> + :transaction + |> insert( + to_address_hash: to_address.hash, + to_address: to_address, + from_address_hash: from_address.hash, + from_address: from_address + ) + |> with_block() + + insert(:internal_transaction, + transaction: transaction, + to_address_hash: to_address.hash, + to_address: to_address, + from_address_hash: from_address.hash, + from_address: from_address, + block_hash: transaction.block_hash, + index: i + 1, + block_index: i + 1 + ) + + insert(:token_transfer, + to_address_hash: to_address.hash, + to_address: to_address, + from_address_hash: from_address.hash, + from_address: from_address, + transaction: transaction, + block_number: transaction.block_number, + log_index: i + ) + + true -> + insert(:internal_transaction, + transaction: transaction, + block_hash: transaction.block_hash, + index: i + 1, + block_index: i + 1 + ) + + insert(:token_transfer, transaction: transaction, block_number: transaction.block_number, log_index: i) + end + end + + request = + get(conn, "/api/v2/advanced-filters", %{ + "from_address_hashes_to_include" => to_string(from_address.hash), + "to_address_hashes_to_include" => to_string(to_address.hash), + "address_relation" => "AnD" + }) + + assert response = json_response(request, 200) + + assert Enum.count(response["items"]) == 6 + end + + test "filter by from or to address", %{conn: conn} do + from_address = insert(:address) + to_address = insert(:address) + + for i <- 0..8 do + transaction = :transaction |> insert() |> with_block() + + cond do + i < 2 -> + :transaction |> insert(from_address_hash: from_address.hash, from_address: from_address) |> with_block() + + insert(:internal_transaction, + transaction: transaction, + from_address_hash: from_address.hash, + from_address: from_address, + block_hash: transaction.block_hash, + index: i + 1, + block_index: i + 1 + ) + + insert(:token_transfer, + from_address_hash: from_address.hash, + from_address: from_address, + transaction: transaction, + block_number: transaction.block_number, + log_index: i + ) + + i < 4 -> + :transaction |> insert(to_address_hash: to_address.hash, to_address: to_address) |> with_block() + + insert(:internal_transaction, + transaction: transaction, + to_address_hash: to_address.hash, + to_address: to_address, + block_hash: transaction.block_hash, + index: i + 1, + block_index: i + 1 + ) + + insert(:token_transfer, + to_address_hash: to_address.hash, + to_address: to_address, + transaction: transaction, + block_number: transaction.block_number, + log_index: i + ) + + i < 6 -> + :transaction + |> insert( + to_address_hash: to_address.hash, + to_address: to_address, + from_address_hash: from_address.hash, + from_address: from_address + ) + |> with_block() + + insert(:internal_transaction, + transaction: transaction, + to_address_hash: to_address.hash, + to_address: to_address, + from_address_hash: from_address.hash, + from_address: from_address, + block_hash: transaction.block_hash, + index: i + 1, + block_index: i + 1 + ) + + insert(:token_transfer, + to_address_hash: to_address.hash, + to_address: to_address, + from_address_hash: from_address.hash, + from_address: from_address, + transaction: transaction, + block_number: transaction.block_number, + log_index: i + ) + + true -> + insert(:internal_transaction, + transaction: transaction, + block_hash: transaction.block_hash, + index: i + 1, + block_index: i + 1 + ) + + insert(:token_transfer, transaction: transaction, block_number: transaction.block_number, log_index: i) + end + end + + request = + get(conn, "/api/v2/advanced-filters", %{ + "from_address_hashes_to_include" => to_string(from_address.hash), + "to_address_hashes_to_include" => to_string(to_address.hash) + }) + + assert response = json_response(request, 200) + + assert Enum.count(response["items"]) == 18 + end + + test "filter by amount", %{conn: conn} do + for i <- 0..4 do + transaction = :transaction |> insert(value: i * 10 ** 18) |> with_block() + + insert(:internal_transaction, + transaction: transaction, + block_hash: transaction.block_hash, + index: 1, + block_index: 1, + value: i * 10 ** 18 + ) + + token = insert(:token, decimals: 10) + + insert(:token_transfer, + amount: i * 10 ** 10, + token_contract_address: token.contract_address, + transaction: transaction, + block_number: transaction.block_number, + log_index: 0 + ) + end + + request = get(conn, "/api/v2/advanced-filters", %{"amount_from" => "0.5", "amount_to" => "2.99"}) + assert response = json_response(request, 200) + + assert Enum.count(response["items"]) == 6 + end + + test "filter by token contract address include", %{conn: conn} do + token_a = insert(:token) + token_b = insert(:token) + token_c = insert(:token) + + transaction = :transaction |> insert() |> with_block() + + for token <- [token_a, token_b, token_c, token_a, token_b, token_c, token_a, token_b, token_c] do + insert(:token_transfer, + token_contract_address: token.contract_address, + transaction: transaction, + block_number: transaction.block_number, + log_index: 0 + ) + end + + request = + get(conn, "/api/v2/advanced-filters", %{ + "token_contract_address_hashes_to_include" => + "#{token_b.contract_address_hash},#{token_c.contract_address_hash}" + }) + + assert response = json_response(request, 200) + + assert Enum.count(response["items"]) == 6 + end + + test "filter by token contract address exclude", %{conn: conn} do + token_a = insert(:token) + token_b = insert(:token) + token_c = insert(:token) + + transaction = :transaction |> insert() |> with_block() + + for token <- [token_a, token_b, token_c, token_a, token_b, token_c, token_a, token_b, token_c] do + insert(:token_transfer, + token_contract_address: token.contract_address, + transaction: transaction, + block_number: transaction.block_number, + log_index: 0 + ) + end + + request = + get(conn, "/api/v2/advanced-filters", %{ + "token_contract_address_hashes_to_exclude" => + "#{token_b.contract_address_hash},#{token_c.contract_address_hash}" + }) + + assert response = json_response(request, 200) + + assert Enum.count(response["items"]) == 4 + end + + test "filter by token contract address include with native", %{conn: conn} do + token_a = insert(:token) + token_b = insert(:token) + token_c = insert(:token) + + transaction = :transaction |> insert() |> with_block() + + for token <- [token_a, token_b, token_c, token_a, token_b, token_c, token_a, token_b, token_c] do + insert(:token_transfer, + token_contract_address: token.contract_address, + transaction: transaction, + block_number: transaction.block_number, + log_index: 0 + ) + end + + request = + get(conn, "/api/v2/advanced-filters", %{ + "token_contract_address_hashes_to_include" => + "#{token_b.contract_address_hash},#{token_c.contract_address_hash},native" + }) + + assert response = json_response(request, 200) + + assert Enum.count(response["items"]) == 7 + end + + test "filter by token contract address exclude with native", %{conn: conn} do + token_a = insert(:token) + token_b = insert(:token) + token_c = insert(:token) + + transaction = :transaction |> insert() |> with_block() + + for token <- [token_a, token_b, token_c, token_a, token_b, token_c, token_a, token_b, token_c] do + insert(:token_transfer, + token_contract_address: token.contract_address, + transaction: transaction, + block_number: transaction.block_number, + log_index: 0 + ) + end + + request = + get(conn, "/api/v2/advanced-filters", %{ + "token_contract_address_hashes_to_exclude" => + "#{token_b.contract_address_hash},#{token_c.contract_address_hash},native" + }) + + assert response = json_response(request, 200) + + assert Enum.count(response["items"]) == 3 + end + + test "correct query with all filters and pagination", %{conn: conn} do + for address_relation <- [:or, :and] do + method_id_string = "0xa9059cbb" + {:ok, method} = Data.cast(method_id_string <> "ab0ba0") + transaction_from_address = insert(:address) + transaction_to_address = insert(:address) + token_transfer_from_address = insert(:address) + token_transfer_to_address = insert(:address) + token = insert(:token) + {:ok, burn_address_hash} = Hash.Address.cast(SmartContract.burn_address_hash_string()) + + insert_list(5, :transaction) + + transactions = + for _ <- 0..29 do + transaction = + insert(:transaction, + from_address: transaction_from_address, + from_address_hash: transaction_from_address.hash, + to_address: transaction_to_address, + to_address_hash: transaction_to_address.hash, + value: Enum.random(0..1_000_000), + input: method + ) + |> with_block() + + insert(:token_transfer, + transaction: transaction, + block_number: transaction.block_number, + amount: Enum.random(0..1_000_000), + from_address: token_transfer_from_address, + from_address_hash: token_transfer_from_address.hash, + to_address: token_transfer_to_address, + to_address_hash: token_transfer_to_address.hash, + token_contract_address: token.contract_address, + token_contract_address_hash: token.contract_address_hash + ) + + transaction + end + + insert_list(5, :transaction) + + from_timestamp = List.first(transactions).block.timestamp + to_timestamp = List.last(transactions).block.timestamp + + params = %{ + "tx_types" => "coin_transfer,ERC-20", + "methods" => method_id_string, + "age_from" => from_timestamp |> DateTime.to_iso8601(), + "age_to" => to_timestamp |> DateTime.to_iso8601(), + "from_address_hashes_to_include" => "#{transaction_from_address.hash},#{token_transfer_from_address.hash}", + "to_address_hashes_to_include" => "#{transaction_to_address.hash},#{token_transfer_to_address.hash}", + "address_relation" => to_string(address_relation), + "amount_from" => "0", + "amount_to" => "1000000", + "token_contract_address_hashes_to_include" => "native,#{token.contract_address_hash}", + "token_contract_address_hashes_to_exclude" => "#{burn_address_hash}" + } + + request = + get(conn, "/api/v2/advanced-filters", params) + + assert response = json_response(request, 200) + request_2nd_page = get(conn, "/api/v2/advanced-filters", Map.merge(params, response["next_page_params"])) + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response( + AdvancedFilter.list( + tx_types: ["COIN_TRANSFER", "ERC-20"], + methods: ["0xa9059cbb"], + age: [from: from_timestamp, to: to_timestamp], + from_address_hashes: [ + include: [transaction_from_address.hash, token_transfer_from_address.hash], + exclude: nil + ], + to_address_hashes: [ + include: [transaction_to_address.hash, token_transfer_to_address.hash], + exclude: nil + ], + address_relation: address_relation, + amount: [from: Decimal.new("0"), to: Decimal.new("1000000")], + token_contract_address_hashes: [ + include: [ + "native", + token.contract_address_hash + ], + exclude: [burn_address_hash] + ], + api?: true + ), + response["items"], + response_2nd_page["items"] + ) + end + end + end + + describe "/advanced_filters/methods?q=" do + test "returns empty list if method does not exist", %{conn: conn} do + request = get(conn, "/api/v2/advanced-filters/methods", %{"q" => "foo"}) + assert response = json_response(request, 200) + assert response == [] + end + + test "finds method by name", %{conn: conn} do + insert(:contract_method) + request = get(conn, "/api/v2/advanced-filters/methods", %{"q" => "set"}) + assert response = json_response(request, 200) + assert response == [%{"method_id" => "0x60fe47b1", "name" => "set"}] + end + + test "finds method by id", %{conn: conn} do + insert(:contract_method) + request = get(conn, "/api/v2/advanced-filters/methods", %{"q" => "0x60fe47b1"}) + assert response = json_response(request, 200) + assert response == [%{"method_id" => "0x60fe47b1", "name" => "set"}] + end + end + + defp check_paginated_response(all_advanced_filters, first_page, second_page) do + assert all_advanced_filters + |> Enum.map( + &{&1.block_number, &1.transaction_index, &1.internal_transaction_index, &1.token_transfer_index, + &1.token_transfer_batch_index} + ) == + Enum.map( + first_page ++ second_page, + &{&1["block_number"], &1["transaction_index"], &1["internal_transaction_index"], + &1["token_transfer_index"], &1["token_transfer_batch_index"]} + ) + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/api/v2/block_controller_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/api/v2/block_controller_test.exs new file mode 100644 index 0000000..cdbd546 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/api/v2/block_controller_test.exs @@ -0,0 +1,525 @@ +defmodule BlockScoutWeb.API.V2.BlockControllerTest do + use BlockScoutWeb.ConnCase + + alias Explorer.Chain.{Address, Block, InternalTransaction, Transaction, Withdrawal} + + setup do + Supervisor.terminate_child(Explorer.Supervisor, Explorer.Chain.Cache.Blocks.child_id()) + Supervisor.restart_child(Explorer.Supervisor, Explorer.Chain.Cache.Blocks.child_id()) + Supervisor.terminate_child(Explorer.Supervisor, Explorer.Chain.Cache.Uncles.child_id()) + Supervisor.restart_child(Explorer.Supervisor, Explorer.Chain.Cache.Uncles.child_id()) + + Application.put_env(:explorer, Explorer.Chain.Cache.CeloCoreContracts, + contracts: %{ + "addresses" => %{ + "Accounts" => [], + "Election" => [], + "EpochRewards" => [], + "FeeHandler" => [], + "GasPriceMinimum" => [], + "GoldToken" => [], + "Governance" => [], + "LockedGold" => [], + "Reserve" => [], + "StableToken" => [], + "Validators" => [] + } + } + ) + + on_exit(fn -> + Application.put_env(:explorer, Explorer.Chain.Cache.CeloCoreContracts, contracts: %{}) + end) + + :ok + end + + describe "/blocks" do + test "empty lists", %{conn: conn} do + request = get(conn, "/api/v2/blocks") + assert response = json_response(request, 200) + assert response["items"] == [] + assert response["next_page_params"] == nil + + request = get(conn, "/api/v2/blocks", %{"type" => "uncle"}) + assert response = json_response(request, 200) + assert response["items"] == [] + assert response["next_page_params"] == nil + + request = get(conn, "/api/v2/blocks", %{"type" => "reorg"}) + assert response = json_response(request, 200) + assert response["items"] == [] + assert response["next_page_params"] == nil + + request = get(conn, "/api/v2/blocks", %{"type" => "block"}) + assert response = json_response(request, 200) + assert response["items"] == [] + assert response["next_page_params"] == nil + end + + test "get block", %{conn: conn} do + block = insert(:block) + + request = get(conn, "/api/v2/blocks") + + assert response = json_response(request, 200) + assert Enum.count(response["items"]) == 1 + assert response["next_page_params"] == nil + compare_item(block, Enum.at(response["items"], 0)) + end + + test "type=block returns only consensus blocks", %{conn: conn} do + blocks = + 4 + |> insert_list(:block) + |> Enum.reverse() + + for index <- 0..3 do + uncle = insert(:block, consensus: false) + insert(:block_second_degree_relation, uncle_hash: uncle.hash, nephew: Enum.at(blocks, index)) + end + + 2 + |> insert_list(:block, consensus: false) + + request = get(conn, "/api/v2/blocks", %{"type" => "block"}) + + assert response = json_response(request, 200) + assert Enum.count(response["items"]) == 4 + assert response["next_page_params"] == nil + + for index <- 0..3 do + compare_item(Enum.at(blocks, index), Enum.at(response["items"], index)) + end + end + + test "type=block can paginate", %{conn: conn} do + blocks = + 51 + |> insert_list(:block) + + filter = %{"type" => "block"} + + request = get(conn, "/api/v2/blocks", filter) + assert response = json_response(request, 200) + + request_2nd_page = get(conn, "/api/v2/blocks", Map.merge(response["next_page_params"], filter)) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, blocks) + end + + test "type=reorg returns only non consensus blocks", %{conn: conn} do + blocks = + 5 + |> insert_list(:block) + + for index <- 0..3 do + uncle = insert(:block, consensus: false) + insert(:block_second_degree_relation, uncle_hash: uncle.hash, nephew: Enum.at(blocks, index)) + end + + reorgs = + 4 + |> insert_list(:block, consensus: false) + |> Enum.reverse() + + Enum.each(reorgs, fn b -> insert(:block, number: b.number, consensus: true) end) + + request = get(conn, "/api/v2/blocks", %{"type" => "reorg"}) + + assert response = json_response(request, 200) + assert Enum.count(response["items"]) == 4 + assert response["next_page_params"] == nil + + for index <- 0..3 do + compare_item(Enum.at(reorgs, index), Enum.at(response["items"], index)) + end + end + + test "type=reorg can paginate", %{conn: conn} do + reorgs = + 51 + |> insert_list(:block, consensus: false) + + Enum.each(reorgs, fn b -> insert(:block, number: b.number, consensus: true) end) + + filter = %{"type" => "reorg"} + request = get(conn, "/api/v2/blocks", filter) + assert response = json_response(request, 200) + + request_2nd_page = get(conn, "/api/v2/blocks", Map.merge(response["next_page_params"], filter)) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, reorgs) + end + + test "type=uncle returns only uncle blocks", %{conn: conn} do + blocks = + 4 + |> insert_list(:block) + |> Enum.reverse() + + uncles = + for index <- 0..3 do + uncle = insert(:block, consensus: false) + insert(:block_second_degree_relation, uncle_hash: uncle.hash, nephew: Enum.at(blocks, index)) + uncle + end + |> Enum.reverse() + + 4 + |> insert_list(:block, consensus: false) + + request = get(conn, "/api/v2/blocks", %{"type" => "uncle"}) + + assert response = json_response(request, 200) + assert Enum.count(response["items"]) == 4 + assert response["next_page_params"] == nil + + for index <- 0..3 do + compare_item(Enum.at(uncles, index), Enum.at(response["items"], index)) + end + end + + test "type=uncle can paginate", %{conn: conn} do + blocks = + 51 + |> insert_list(:block) + + uncles = + for index <- 0..50 do + uncle = insert(:block, consensus: false) + insert(:block_second_degree_relation, uncle_hash: uncle.hash, nephew: Enum.at(blocks, index)) + uncle + end + + filter = %{"type" => "uncle"} + request = get(conn, "/api/v2/blocks", filter) + assert response = json_response(request, 200) + + request_2nd_page = get(conn, "/api/v2/blocks", Map.merge(response["next_page_params"], filter)) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, uncles) + end + end + + describe "/blocks/{block_hash_or_number}" do + test "return 422 on invalid parameter", %{conn: conn} do + request_1 = get(conn, "/api/v2/blocks/0x123123") + assert %{"message" => "Invalid hash"} = json_response(request_1, 422) + + request_2 = get(conn, "/api/v2/blocks/123qwe") + assert %{"message" => "Invalid number"} = json_response(request_2, 422) + end + + test "return 404 on non existing block", %{conn: conn} do + block = build(:block) + + request_1 = get(conn, "/api/v2/blocks/#{block.number}") + assert %{"message" => "Not found"} = json_response(request_1, 404) + + request_2 = get(conn, "/api/v2/blocks/#{block.hash}") + assert %{"message" => "Not found"} = json_response(request_2, 404) + end + + test "get 'Block lost consensus' message", %{conn: conn} do + block = insert(:block, consensus: false) + hash = to_string(block.hash) + + request_1 = get(conn, "/api/v2/blocks/#{block.number}") + assert %{"message" => "Block lost consensus", "hash" => ^hash} = json_response(request_1, 404) + end + + test "get the same blocks by hash and number", %{conn: conn} do + block = insert(:block) + + request_1 = get(conn, "/api/v2/blocks/#{block.number}") + assert response_1 = json_response(request_1, 200) + + request_2 = get(conn, "/api/v2/blocks/#{block.hash}") + assert response_2 = json_response(request_2, 200) + + assert response_2 == response_1 + compare_item(block, response_2) + end + end + + describe "/blocks/{block_hash_or_number}/transactions" do + test "return 422 on invalid parameter", %{conn: conn} do + request_1 = get(conn, "/api/v2/blocks/0x123123/transactions") + assert %{"message" => "Invalid hash"} = json_response(request_1, 422) + + request_2 = get(conn, "/api/v2/blocks/123qwe/transactions") + assert %{"message" => "Invalid number"} = json_response(request_2, 422) + end + + test "return 404 on non existing block", %{conn: conn} do + block = build(:block) + + request_1 = get(conn, "/api/v2/blocks/#{block.number}/transactions") + assert %{"message" => "Not found"} = json_response(request_1, 404) + + request_2 = get(conn, "/api/v2/blocks/#{block.hash}/transactions") + assert %{"message" => "Not found"} = json_response(request_2, 404) + end + + test "get empty list", %{conn: conn} do + block = insert(:block) + + request = get(conn, "/api/v2/blocks/#{block.number}/transactions") + assert response = json_response(request, 200) + assert response["items"] == [] + assert response["next_page_params"] == nil + + request = get(conn, "/api/v2/blocks/#{block.hash}/transactions") + assert response = json_response(request, 200) + assert response["items"] == [] + assert response["next_page_params"] == nil + end + + test "get relevant transaction", %{conn: conn} do + 10 + |> insert_list(:transaction) + |> with_block() + + block = insert(:block) + + transaction = + :transaction + |> insert() + |> with_block(block) + + request = get(conn, "/api/v2/blocks/#{block.number}/transactions") + assert response = json_response(request, 200) + assert Enum.count(response["items"]) == 1 + assert response["next_page_params"] == nil + compare_item(transaction, Enum.at(response["items"], 0)) + + request = get(conn, "/api/v2/blocks/#{block.hash}/transactions") + assert response_1 = json_response(request, 200) + assert response_1 == response + end + + test "get transactions with working next_page_params", %{conn: conn} do + 2 + |> insert_list(:transaction) + |> with_block() + + block = insert(:block) + + transactions = + 51 + |> insert_list(:transaction) + |> with_block(block) + |> Enum.reverse() + + request = get(conn, "/api/v2/blocks/#{block.number}/transactions") + assert response = json_response(request, 200) + + request_2nd_page = get(conn, "/api/v2/blocks/#{block.number}/transactions", response["next_page_params"]) + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, transactions) + + request_1 = get(conn, "/api/v2/blocks/#{block.hash}/transactions") + assert response_1 = json_response(request_1, 200) + + assert response_1 == response + + request_2 = get(conn, "/api/v2/blocks/#{block.hash}/transactions", response_1["next_page_params"]) + assert response_2 = json_response(request_2, 200) + assert response_2 == response_2nd_page + end + end + + describe "/blocks/{block_hash_or_number}/withdrawals" do + test "return 422 on invalid parameter", %{conn: conn} do + request_1 = get(conn, "/api/v2/blocks/0x123123/withdrawals") + assert %{"message" => "Invalid hash"} = json_response(request_1, 422) + + request_2 = get(conn, "/api/v2/blocks/123qwe/withdrawals") + assert %{"message" => "Invalid number"} = json_response(request_2, 422) + end + + test "return 404 on non existing block", %{conn: conn} do + block = build(:block) + + request_1 = get(conn, "/api/v2/blocks/#{block.number}/withdrawals") + assert %{"message" => "Not found"} = json_response(request_1, 404) + + request_2 = get(conn, "/api/v2/blocks/#{block.hash}/withdrawals") + assert %{"message" => "Not found"} = json_response(request_2, 404) + end + + test "get empty list", %{conn: conn} do + block = insert(:block) + + request = get(conn, "/api/v2/blocks/#{block.number}/withdrawals") + assert response = json_response(request, 200) + assert response["items"] == [] + assert response["next_page_params"] == nil + + request = get(conn, "/api/v2/blocks/#{block.hash}/withdrawals") + assert response = json_response(request, 200) + assert response["items"] == [] + assert response["next_page_params"] == nil + end + + test "get withdrawals", %{conn: conn} do + block = insert(:block, withdrawals: insert_list(3, :withdrawal)) + + [withdrawal | _] = Enum.reverse(block.withdrawals) + + request = get(conn, "/api/v2/blocks/#{block.number}/withdrawals") + assert response = json_response(request, 200) + assert Enum.count(response["items"]) == 3 + assert response["next_page_params"] == nil + compare_item(withdrawal, Enum.at(response["items"], 0)) + + request = get(conn, "/api/v2/blocks/#{block.hash}/withdrawals") + assert response_1 = json_response(request, 200) + assert response_1 == response + end + + test "get withdrawals with working next_page_params", %{conn: conn} do + block = insert(:block, withdrawals: insert_list(51, :withdrawal)) + + request = get(conn, "/api/v2/blocks/#{block.number}/withdrawals") + assert response = json_response(request, 200) + + request_2nd_page = get(conn, "/api/v2/blocks/#{block.number}/withdrawals", response["next_page_params"]) + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, block.withdrawals) + + request_1 = get(conn, "/api/v2/blocks/#{block.hash}/withdrawals") + assert response_1 = json_response(request_1, 200) + + assert response_1 == response + + request_2 = get(conn, "/api/v2/blocks/#{block.hash}/withdrawals", response_1["next_page_params"]) + assert response_2 = json_response(request_2, 200) + assert response_2 == response_2nd_page + end + end + + describe "/blocks/{block_hash_or_number}/internal-transactions" do + test "returns 422 on invalid parameter", %{conn: conn} do + request_1 = get(conn, "/api/v2/blocks/0x123123/internal-transactions") + assert %{"message" => "Invalid hash"} = json_response(request_1, 422) + + request_2 = get(conn, "/api/v2/blocks/123qwe/internal-transactions") + assert %{"message" => "Invalid number"} = json_response(request_2, 422) + end + + test "returns 404 on non existing block", %{conn: conn} do + block = build(:block) + + request_1 = get(conn, "/api/v2/blocks/#{block.number}/internal-transactions") + assert %{"message" => "Not found"} = json_response(request_1, 404) + + request_2 = get(conn, "/api/v2/blocks/#{block.hash}/internal-transactions") + assert %{"message" => "Not found"} = json_response(request_2, 404) + end + + test "returns empty list", %{conn: conn} do + block = insert(:block) + + request = get(conn, "/api/v2/blocks/#{block.hash}/internal-transactions") + assert %{"items" => [], "next_page_params" => nil} = json_response(request, 200) + + request = get(conn, "/api/v2/blocks/#{block.number}/internal-transactions") + assert %{"items" => [], "next_page_params" => nil} = json_response(request, 200) + end + + test "can paginate internal transactions", %{conn: conn} do + block = insert(:block) + + request = get(conn, "/api/v2/blocks/#{block.hash}/internal-transactions") + assert %{"items" => [], "next_page_params" => nil} = json_response(request, 200) + + transaction = + :transaction + |> insert() + |> with_block(block) + + insert(:internal_transaction, + transaction: transaction, + index: 0, + block_number: transaction.block_number, + transaction_index: transaction.index, + block_hash: transaction.block_hash, + block_index: 0 + ) + + internal_transactions = + 51..1 + |> Enum.map(fn index -> + transaction = + :transaction + |> insert() + |> with_block(block) + + insert(:internal_transaction, + transaction: transaction, + index: index, + block_number: transaction.block_number, + transaction_index: transaction.index, + block_hash: transaction.block_hash, + block_index: index + ) + end) + + request = get(conn, "/api/v2/blocks/#{block.hash}/internal-transactions") + assert response = json_response(request, 200) + + request_2nd_page = get(conn, "/api/v2/blocks/#{block.hash}/internal-transactions", response["next_page_params"]) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, internal_transactions) + end + end + + defp compare_item(%Block{} = block, json) do + assert to_string(block.hash) == json["hash"] + assert block.number == json["height"] + end + + defp compare_item(%Transaction{} = transaction, json) do + assert to_string(transaction.hash) == json["hash"] + assert transaction.block_number == json["block_number"] + assert to_string(transaction.value.value) == json["value"] + assert Address.checksum(transaction.from_address_hash) == json["from"]["hash"] + assert Address.checksum(transaction.to_address_hash) == json["to"]["hash"] + end + + defp compare_item(%Withdrawal{} = withdrawal, json) do + assert withdrawal.index == json["index"] + end + + defp compare_item(%InternalTransaction{} = internal_transaction, json) do + assert internal_transaction.block_number == json["block_number"] + assert to_string(internal_transaction.gas) == json["gas_limit"] + assert internal_transaction.index == json["index"] + assert to_string(internal_transaction.transaction_hash) == json["transaction_hash"] + assert Address.checksum(internal_transaction.from_address_hash) == json["from"]["hash"] + assert Address.checksum(internal_transaction.to_address_hash) == json["to"]["hash"] + end + + defp check_paginated_response(first_page_resp, second_page_resp, list) do + assert Enum.count(first_page_resp["items"]) == 50 + assert first_page_resp["next_page_params"] != nil + compare_item(Enum.at(list, 50), Enum.at(first_page_resp["items"], 0)) + compare_item(Enum.at(list, 1), Enum.at(first_page_resp["items"], 49)) + + assert Enum.count(second_page_resp["items"]) == 1 + assert second_page_resp["next_page_params"] == nil + compare_item(Enum.at(list, 0), Enum.at(second_page_resp["items"], 0)) + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/api/v2/config_controller_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/api/v2/config_controller_test.exs new file mode 100644 index 0000000..eb55d12 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/api/v2/config_controller_test.exs @@ -0,0 +1,22 @@ +defmodule BlockScoutWeb.API.V2.ConfigControllerTest do + use BlockScoutWeb.ConnCase + + describe "/config/backend-version" do + test "get json rps url if set", %{conn: conn} do + version = "v6.3.0-beta" + Application.put_env(:block_scout_web, :version, version) + + request = get(conn, "/api/v2/config/backend-version") + + assert %{"backend_version" => ^version} = json_response(request, 200) + end + + test "get nil backend version if not set", %{conn: conn} do + Application.put_env(:block_scout_web, :version, nil) + + request = get(conn, "/api/v2/config/backend-version") + + assert %{"backend_version" => nil} = json_response(request, 200) + end + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/api/v2/csv_export_controller_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/api/v2/csv_export_controller_test.exs new file mode 100644 index 0000000..9c9168c --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/api/v2/csv_export_controller_test.exs @@ -0,0 +1,435 @@ +defmodule BlockScoutWeb.Api.V2.CsvExportControllerTest do + use BlockScoutWeb.ConnCase, async: true + use ExUnit.Case, async: false + alias Explorer.Chain.Address + + import Mox + + setup :verify_on_exit! + + describe "GET token-transfers-csv/2" do + setup do + csv_setup() + end + + test "do not export token transfers to csv without recaptcha recaptcha_response provided", %{conn: conn} do + address = insert(:address) + + transaction = + :transaction + |> insert(from_address: address) + |> with_block() + + insert(:token_transfer, transaction: transaction, from_address: address, block_number: transaction.block_number) + insert(:token_transfer, transaction: transaction, to_address: address, block_number: transaction.block_number) + + {:ok, now} = DateTime.now("Etc/UTC") + + from_period = DateTime.add(now, -1, :minute) |> DateTime.to_iso8601() + to_period = now |> DateTime.to_iso8601() + + conn = + get(conn, "/api/v2/addresses/#{Address.checksum(address.hash)}/token-transfers/csv", %{ + "address_id" => Address.checksum(address.hash), + "from_period" => from_period, + "to_period" => to_period + }) + + assert conn.status == 403 + end + + test "do not export token transfers to csv without recaptcha passed", %{ + conn: conn, + v2_secret_key: recaptcha_secret_key + } do + expected_body = "secret=#{recaptcha_secret_key}&response=123" + + Explorer.Mox.HTTPoison + |> expect(:post, fn _url, ^expected_body, _headers, _options -> + {:ok, %HTTPoison.Response{status_code: 200, body: Jason.encode!(%{"success" => false})}} + end) + + address = insert(:address) + + transaction = + :transaction + |> insert(from_address: address) + |> with_block() + + insert(:token_transfer, transaction: transaction, from_address: address, block_number: transaction.block_number) + insert(:token_transfer, transaction: transaction, to_address: address, block_number: transaction.block_number) + + {:ok, now} = DateTime.now("Etc/UTC") + + from_period = DateTime.add(now, -1, :minute) |> DateTime.to_iso8601() + to_period = now |> DateTime.to_iso8601() + + conn = + get(conn, "/api/v2/addresses/#{Address.checksum(address.hash)}/token-transfers/csv", %{ + "address_id" => Address.checksum(address.hash), + "from_period" => from_period, + "to_period" => to_period, + "recaptcha_response" => "123" + }) + + assert conn.status == 403 + end + + test "exports token transfers to csv without recaptcha if recaptcha is disabled", %{conn: conn} do + init_config = Application.get_env(:block_scout_web, :recaptcha) + Application.put_env(:block_scout_web, :recaptcha, is_disabled: true) + + address = insert(:address) + + transaction = + :transaction + |> insert(from_address: address) + |> with_block() + + insert(:token_transfer, transaction: transaction, from_address: address, block_number: transaction.block_number) + insert(:token_transfer, transaction: transaction, to_address: address, block_number: transaction.block_number) + + {:ok, now} = DateTime.now("Etc/UTC") + + from_period = DateTime.add(now, -1, :minute) |> DateTime.to_iso8601() + to_period = now |> DateTime.to_iso8601() + + conn = + get(conn, "/api/v2/addresses/#{Address.checksum(address.hash)}/token-transfers/csv", %{ + "address_id" => Address.checksum(address.hash), + "from_period" => from_period, + "to_period" => to_period + }) + + assert conn.resp_body |> String.split("\n") |> Enum.count() == 4 + + Application.put_env(:block_scout_web, :recaptcha, init_config) + end + + test "exports token transfers to csv", %{conn: conn, v2_secret_key: recaptcha_secret_key} do + expected_body = "secret=#{recaptcha_secret_key}&response=123" + + Explorer.Mox.HTTPoison + |> expect(:post, fn _url, ^expected_body, _headers, _options -> + {:ok, + %HTTPoison.Response{ + status_code: 200, + body: + Jason.encode!(%{ + "success" => true, + "hostname" => Application.get_env(:block_scout_web, BlockScoutWeb.Endpoint)[:url][:host] + }) + }} + end) + + address = insert(:address) + + transaction = + :transaction + |> insert(from_address: address) + |> with_block() + + insert(:token_transfer, transaction: transaction, from_address: address, block_number: transaction.block_number) + insert(:token_transfer, transaction: transaction, to_address: address, block_number: transaction.block_number) + + {:ok, now} = DateTime.now("Etc/UTC") + + from_period = DateTime.add(now, -1, :minute) |> DateTime.to_iso8601() + to_period = now |> DateTime.to_iso8601() + + conn = + get(conn, "/api/v2/addresses/#{Address.checksum(address.hash)}/token-transfers/csv", %{ + "address_id" => Address.checksum(address.hash), + "from_period" => from_period, + "to_period" => to_period, + "recaptcha_response" => "123" + }) + + assert conn.resp_body |> String.split("\n") |> Enum.count() == 4 + end + end + + describe "GET transactions_csv/2" do + setup do + csv_setup() + end + + test "download csv file with transactions", %{conn: conn, v2_secret_key: recaptcha_secret_key} do + expected_body = "secret=#{recaptcha_secret_key}&response=123" + + Explorer.Mox.HTTPoison + |> expect(:post, fn _url, ^expected_body, _headers, _options -> + {:ok, + %HTTPoison.Response{ + status_code: 200, + body: + Jason.encode!(%{ + "success" => true, + "hostname" => Application.get_env(:block_scout_web, BlockScoutWeb.Endpoint)[:url][:host] + }) + }} + end) + + address = insert(:address) + + :transaction + |> insert(from_address: address) + |> with_block() + + :transaction + |> insert(from_address: address) + |> with_block() + + {:ok, now} = DateTime.now("Etc/UTC") + + from_period = DateTime.add(now, -1, :minute) |> DateTime.to_iso8601() + to_period = now |> DateTime.to_iso8601() + + conn = + get(conn, "/api/v2/addresses/#{Address.checksum(address.hash)}/transactions/csv", %{ + "address_id" => Address.checksum(address.hash), + "from_period" => from_period, + "to_period" => to_period, + "recaptcha_response" => "123" + }) + + assert conn.resp_body |> String.split("\n") |> Enum.count() == 4 + end + end + + describe "GET internal_transactions_csv/2" do + setup do + csv_setup() + end + + test "download csv file with internal transactions", %{conn: conn, v2_secret_key: recaptcha_secret_key} do + expected_body = "secret=#{recaptcha_secret_key}&response=123" + + Explorer.Mox.HTTPoison + |> expect(:post, fn _url, ^expected_body, _headers, _options -> + {:ok, + %HTTPoison.Response{ + status_code: 200, + body: + Jason.encode!(%{ + "success" => true, + "hostname" => Application.get_env(:block_scout_web, BlockScoutWeb.Endpoint)[:url][:host] + }) + }} + end) + + address = insert(:address) + + transaction_1 = + :transaction + |> insert() + |> with_block() + + transaction_2 = + :transaction + |> insert() + |> with_block() + + transaction_3 = + :transaction + |> insert() + |> with_block() + + insert(:internal_transaction, + index: 3, + transaction: transaction_1, + from_address: address, + block_number: transaction_1.block_number, + block_hash: transaction_1.block_hash, + block_index: 0, + transaction_index: transaction_1.index + ) + + insert(:internal_transaction, + index: 1, + transaction: transaction_2, + to_address: address, + block_number: transaction_2.block_number, + block_hash: transaction_2.block_hash, + block_index: 1, + transaction_index: transaction_2.index + ) + + insert(:internal_transaction, + index: 2, + transaction: transaction_3, + created_contract_address: address, + block_number: transaction_3.block_number, + block_hash: transaction_3.block_hash, + block_index: 2, + transaction_index: transaction_3.index + ) + + {:ok, now} = DateTime.now("Etc/UTC") + + from_period = DateTime.add(now, -1, :day) |> DateTime.to_iso8601() + to_period = now |> DateTime.to_iso8601() + + conn = + get(conn, "/api/v2/addresses/#{Address.checksum(address.hash)}/internal-transactions/csv", %{ + "address_id" => Address.checksum(address.hash), + "from_period" => from_period, + "to_period" => to_period, + "recaptcha_response" => "123" + }) + + assert conn.resp_body |> String.split("\n") |> Enum.count() == 5 + end + end + + describe "GET logs_csv/2" do + setup do + csv_setup() + end + + test "download csv file with logs", %{conn: conn, v2_secret_key: recaptcha_secret_key} do + expected_body = "secret=#{recaptcha_secret_key}&response=123" + + Explorer.Mox.HTTPoison + |> expect(:post, fn _url, ^expected_body, _headers, _options -> + {:ok, + %HTTPoison.Response{ + status_code: 200, + body: + Jason.encode!(%{ + "success" => true, + "hostname" => Application.get_env(:block_scout_web, BlockScoutWeb.Endpoint)[:url][:host] + }) + }} + end) + + address = insert(:address) + + transaction_1 = + :transaction + |> insert() + |> with_block() + + insert(:log, + address: address, + index: 3, + transaction: transaction_1, + block: transaction_1.block, + block_number: transaction_1.block_number + ) + + transaction_2 = + :transaction + |> insert() + |> with_block() + + insert(:log, + address: address, + index: 1, + transaction: transaction_2, + block: transaction_2.block, + block_number: transaction_2.block_number + ) + + transaction_3 = + :transaction + |> insert() + |> with_block() + + insert(:log, + address: address, + index: 2, + transaction: transaction_3, + block: transaction_3.block, + block_number: transaction_3.block_number + ) + + {:ok, now} = DateTime.now("Etc/UTC") + + from_period = DateTime.add(now, -1, :minute) |> DateTime.to_iso8601() + to_period = now |> DateTime.to_iso8601() + + conn = + get(conn, "/api/v2/addresses/#{Address.checksum(address.hash)}/logs/csv", %{ + "address_id" => Address.checksum(address.hash), + "from_period" => from_period, + "to_period" => to_period, + "recaptcha_response" => "123" + }) + + assert conn.resp_body |> String.split("\n") |> Enum.count() == 5 + end + + test "handles null filter", %{conn: conn, v2_secret_key: recaptcha_secret_key} do + expected_body = "secret=#{recaptcha_secret_key}&response=123" + + Explorer.Mox.HTTPoison + |> expect(:post, fn _url, ^expected_body, _headers, _options -> + {:ok, + %HTTPoison.Response{ + status_code: 200, + body: + Jason.encode!(%{ + "success" => true, + "hostname" => Application.get_env(:block_scout_web, BlockScoutWeb.Endpoint)[:url][:host] + }) + }} + end) + + address = insert(:address) + + transaction = + :transaction + |> insert() + |> with_block() + + insert(:log, + address: address, + index: 3, + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number + ) + + {:ok, now} = DateTime.now("Etc/UTC") + + from_period = DateTime.add(now, -1, :minute) |> DateTime.to_iso8601() + to_period = now |> DateTime.to_iso8601() + + conn = + get(conn, "/api/v2/addresses/#{Address.checksum(address.hash)}/logs/csv", %{ + "address_id" => Address.checksum(address.hash), + "filter_type" => "null", + "filter_value" => "null", + "from_period" => from_period, + "to_period" => to_period, + "recaptcha_response" => "123" + }) + + assert conn.resp_body |> String.split("\n") |> Enum.count() == 3 + end + end + + defp csv_setup() do + old_recaptcha_env = Application.get_env(:block_scout_web, :recaptcha) + old_http_adapter = Application.get_env(:block_scout_web, :http_adapter) + + v2_secret_key = "v2_secret_key" + v3_secret_key = "v3_secret_key" + + Application.put_env(:block_scout_web, :recaptcha, + v2_secret_key: v2_secret_key, + v3_secret_key: v3_secret_key, + is_disabled: false + ) + + Application.put_env(:block_scout_web, :http_adapter, Explorer.Mox.HTTPoison) + + on_exit(fn -> + Application.put_env(:block_scout_web, :recaptcha, old_recaptcha_env) + Application.put_env(:block_scout_web, :http_adapter, old_http_adapter) + end) + + {:ok, %{v2_secret_key: v2_secret_key, v3_secret_key: v3_secret_key}} + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/api/v2/import_controller_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/api/v2/import_controller_test.exs new file mode 100644 index 0000000..1c4d6d7 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/api/v2/import_controller_test.exs @@ -0,0 +1,166 @@ +defmodule BlockScoutWeb.API.V2.ImportControllerTest do + use BlockScoutWeb.ConnCase + + import Mox + + describe "POST /import/token-info" do + test "return error on misconfigured api key", %{conn: conn} do + request = + post(conn, "/api/v2/import/token-info", %{ + "iconUrl" => "abc", + "tokenAddress" => build(:address).hash, + "tokenSymbol" => "", + "tokenName" => "" + }) + + assert %{"message" => "API key not configured on the server"} = json_response(request, 403) + end + + test "return error on wrong api key", %{conn: conn} do + Application.put_env(:block_scout_web, :sensitive_endpoints_api_key, "abc") + body = %{"iconUrl" => "abc", "tokenAddress" => build(:address).hash, "tokenSymbol" => "", "tokenName" => ""} + request = post(conn, "/api/v2/import/token-info", Map.merge(body, %{"api_key" => "123"})) + + assert %{"message" => "Wrong API key"} = json_response(request, 401) + + Application.put_env(:block_scout_web, :sensitive_endpoints_api_key, nil) + end + + test "do not import token info with wrong url", %{conn: conn} do + api_key = "abc123" + icon_url = "icon_url" + + Application.put_env(:block_scout_web, :sensitive_endpoints_api_key, api_key) + + token = insert(:token, icon_url: nil) + token_address = to_string(token.contract_address_hash) + + body = %{"iconUrl" => icon_url, "tokenAddress" => token_address, "tokenSymbol" => "", "tokenName" => ""} + + request = post(conn, "/api/v2/import/token-info", Map.merge(body, %{"api_key" => api_key})) + assert %{"message" => "Success"} = json_response(request, 200) + + request = get(conn, "/api/v2/tokens/#{token_address}") + + name = token.name + symbol = token.symbol + assert %{"icon_url" => nil, "name" => ^name, "symbol" => ^symbol} = json_response(request, 200) + + Application.put_env(:block_scout_web, :sensitive_endpoints_api_key, nil) + end + + test "success import token info", %{conn: conn} do + api_key = "abc123" + icon_url = "http://example.com/image?a=0&b=1" + token_symbol = "UPD" + token_name = "UPDATED" + + Application.put_env(:block_scout_web, :sensitive_endpoints_api_key, api_key) + + token_address = to_string(insert(:token).contract_address_hash) + + body = %{ + "iconUrl" => icon_url, + "tokenAddress" => token_address, + "tokenSymbol" => token_symbol, + "tokenName" => token_name + } + + request = post(conn, "/api/v2/import/token-info", Map.merge(body, %{"api_key" => api_key})) + assert %{"message" => "Success"} = json_response(request, 200) + + request = get(conn, "/api/v2/tokens/#{token_address}") + assert %{"icon_url" => ^icon_url, "name" => ^token_name, "symbol" => ^token_symbol} = json_response(request, 200) + + Application.put_env(:block_scout_web, :sensitive_endpoints_api_key, nil) + end + end + + describe "DELETE /import/token-info" do + test "return error on misconfigured api key", %{conn: conn} do + request = + delete(conn, "/api/v2/import/token-info", %{ + "token_address_hash" => build(:address).hash + }) + + assert %{"message" => "API key not configured on the server"} = json_response(request, 403) + end + + test "return error on wrong api key", %{conn: conn} do + Application.put_env(:block_scout_web, :sensitive_endpoints_api_key, "abc") + body = %{"token_address_hash" => build(:address).hash} + request = delete(conn, "/api/v2/import/token-info", Map.merge(body, %{"api_key" => "123"})) + + assert %{"message" => "Wrong API key"} = json_response(request, 401) + + Application.put_env(:block_scout_web, :sensitive_endpoints_api_key, nil) + end + + test "success delete token info", %{conn: conn} do + insert(:token, + name: "old", + symbol: "OLD", + decimals: 10, + cataloged: true, + updated_at: DateTime.add(DateTime.utc_now(), -:timer.hours(50), :millisecond) + ) + + expect( + EthereumJSONRPC.Mox, + :json_rpc, + 1, + fn requests, _opts -> + {:ok, + Enum.map(requests, fn + %{id: id, method: "eth_call", params: [%{data: "0x313ce567", to: _}, "latest"]} -> + %{ + id: id, + result: "0x0000000000000000000000000000000000000000000000000000000000000012" + } + + %{id: id, method: "eth_call", params: [%{data: "0x06fdde03", to: _}, "latest"]} -> + %{ + id: id, + result: + "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000096e657720746f6b656e0000000000000000000000000000000000000000000000" + } + + %{id: id, method: "eth_call", params: [%{data: "0x95d89b41", to: _}, "latest"]} -> + %{ + id: id, + result: + "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000034e45570000000000000000000000000000000000000000000000000000000000" + } + + %{id: id, method: "eth_call", params: [%{data: "0x18160ddd", to: _}, "latest"]} -> + %{ + id: id, + result: "0x0000000000000000000000000000000000000000000000000de0b6b3a7640000" + } + end)} + end + ) + + api_key = "abc123" + icon_url = nil + token_symbol = "NEW" + token_name = "new token" + + Application.put_env(:block_scout_web, :sensitive_endpoints_api_key, api_key) + + token_address = to_string(insert(:token).contract_address_hash) + + body = %{ + "token_address_hash" => token_address + } + + request = delete(conn, "/api/v2/import/token-info", Map.merge(body, %{"api_key" => api_key})) + assert %{"message" => "Success"} = json_response(request, 200) + + request = get(conn, "/api/v2/tokens/#{token_address}") + assert %{"icon_url" => ^icon_url, "name" => ^token_name, "symbol" => ^token_symbol} = json_response(request, 200) + + Application.put_env(:block_scout_web, :sensitive_endpoints_api_key, nil) + end + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/api/v2/internal_transaction_controller_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/api/v2/internal_transaction_controller_test.exs new file mode 100644 index 0000000..cee0215 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/api/v2/internal_transaction_controller_test.exs @@ -0,0 +1,94 @@ +defmodule BlockScoutWeb.API.V2.InternalTransactionControllerTest do + use BlockScoutWeb.ConnCase + + alias Explorer.Chain.{Address, InternalTransaction} + + # todo: enable when /internal-transactions API endpoint will be enabled + # describe "/internal-transactions" do + # test "empty list", %{conn: conn} do + # request = get(conn, "/api/v2/internal-transactions") + + # assert response = json_response(request, 200) + # assert response["items"] == [] + # assert response["next_page_params"] == nil + # end + + # test "non empty list", %{conn: conn} do + # tx = + # :transaction + # |> insert() + # |> with_block() + + # insert(:internal_transaction, + # transaction: tx, + # block_hash: tx.block_hash, + # index: 0, + # block_index: 0 + # ) + + # request = get(conn, "/api/v2/internal-transactions") + + # assert response = json_response(request, 200) + # assert Enum.count(response["items"]) == 1 + # assert response["next_page_params"] == nil + # end + + # test "internal transactions with next_page_params", %{conn: conn} do + # transaction = insert(:transaction) |> with_block() + + # internal_transaction = + # insert(:internal_transaction, + # transaction: transaction, + # transaction_index: 0, + # block_number: transaction.block_number, + # block_hash: transaction.block_hash, + # index: 0, + # block_index: 0 + # ) + + # transaction_2 = insert(:transaction) |> with_block() + + # internal_transactions = + # for i <- 0..49 do + # insert(:internal_transaction, + # transaction: transaction_2, + # transaction_index: 0, + # block_number: transaction_2.block_number, + # block_hash: transaction_2.block_hash, + # index: i, + # block_index: i + # ) + # end + + # internal_transactions = [internal_transaction | internal_transactions] + + # request = get(conn, "/api/v2/internal-transactions") + # assert response = json_response(request, 200) + + # request_2nd_page = get(conn, "/api/v2/internal-transactions", response["next_page_params"]) + # assert response_2nd_page = json_response(request_2nd_page, 200) + + # check_paginated_response(response, response_2nd_page, internal_transactions) + # end + # end + + # defp compare_item(%InternalTransaction{} = internal_transaction, json) do + # assert Address.checksum(internal_transaction.from_address_hash) == json["from"]["hash"] + # assert Address.checksum(internal_transaction.to_address_hash) == json["to"]["hash"] + # assert to_string(internal_transaction.transaction_hash) == json["transaction_hash"] + # assert internal_transaction.block_number == json["block_number"] + # assert internal_transaction.block_index == json["block_index"] + # end + + # defp check_paginated_response(first_page_resp, second_page_resp, internal_transactions) do + # assert Enum.count(first_page_resp["items"]) == 50 + # assert first_page_resp["next_page_params"] != nil + # compare_item(Enum.at(internal_transactions, 50), Enum.at(first_page_resp["items"], 0)) + + # compare_item(Enum.at(internal_transactions, 1), Enum.at(first_page_resp["items"], 49)) + + # assert Enum.count(second_page_resp["items"]) == 1 + # assert second_page_resp["next_page_params"] == nil + # compare_item(Enum.at(internal_transactions, 0), Enum.at(second_page_resp["items"], 0)) + # end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/api/v2/main_page_controller_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/api/v2/main_page_controller_test.exs new file mode 100644 index 0000000..3445aa5 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/api/v2/main_page_controller_test.exs @@ -0,0 +1,181 @@ +defmodule BlockScoutWeb.API.V2.MainPageControllerTest do + use BlockScoutWeb.ConnCase + + alias Explorer.Account.{Identity, WatchlistAddress} + alias Explorer.Chain.{Address, Block, Transaction} + alias Explorer.Repo + + import Explorer.Chain, only: [hash_to_lower_case_string: 1] + + setup do + Supervisor.terminate_child(Explorer.Supervisor, Explorer.Chain.Cache.Blocks.child_id()) + Supervisor.restart_child(Explorer.Supervisor, Explorer.Chain.Cache.Blocks.child_id()) + Supervisor.terminate_child(Explorer.Supervisor, Explorer.Chain.Cache.TransactionsApiV2.child_id()) + Supervisor.restart_child(Explorer.Supervisor, Explorer.Chain.Cache.TransactionsApiV2.child_id()) + + :ok + end + + describe "/main-page/blocks" do + test "get empty list when no blocks", %{conn: conn} do + request = get(conn, "/api/v2/main-page/blocks") + assert [] = json_response(request, 200) + end + + test "get last 4 blocks", %{conn: conn} do + blocks = insert_list(10, :block) |> Enum.take(-4) |> Enum.reverse() + + request = get(conn, "/api/v2/main-page/blocks") + assert response = json_response(request, 200) + assert Enum.count(response) == 4 + + for i <- 0..3 do + compare_item(Enum.at(blocks, i), Enum.at(response, i)) + end + end + end + + describe "/main-page/transactions" do + test "get empty list when no transactions", %{conn: conn} do + request = get(conn, "/api/v2/main-page/transactions") + assert [] = json_response(request, 200) + end + + test "get last 6 transactions", %{conn: conn} do + transactions = insert_list(10, :transaction) |> with_block() |> Enum.take(-6) |> Enum.reverse() + + request = get(conn, "/api/v2/main-page/transactions") + assert response = json_response(request, 200) + assert Enum.count(response) == 6 + + for i <- 0..5 do + compare_item(Enum.at(transactions, i), Enum.at(response, i)) + end + end + end + + describe "/main-page/transactions/watchlist" do + test "unauthorized", %{conn: conn} do + request = get(conn, "/api/v2/main-page/transactions/watchlist") + assert %{"message" => "Unauthorized"} = json_response(request, 401) + end + + test "get last 6 transactions", %{conn: conn} do + insert_list(10, :transaction) |> with_block() + + auth = build(:auth) + {:ok, user} = Identity.find_or_create(auth) + + conn = Plug.Test.init_test_session(conn, current_user: user) + + address_1 = insert(:address) + + watchlist_address_1 = + Repo.account_repo().insert!(%WatchlistAddress{ + name: "wallet_1", + watchlist_id: user.watchlist_id, + address_hash: address_1.hash, + address_hash_hash: hash_to_lower_case_string(address_1.hash), + watch_coin_input: true, + watch_coin_output: true, + watch_erc_20_input: true, + watch_erc_20_output: true, + watch_erc_721_input: true, + watch_erc_721_output: true, + watch_erc_1155_input: true, + watch_erc_1155_output: true, + notify_email: true + }) + + address_2 = insert(:address) + + watchlist_address_2 = + Repo.account_repo().insert!(%WatchlistAddress{ + name: "wallet_2", + watchlist_id: user.watchlist_id, + address_hash: address_2.hash, + address_hash_hash: hash_to_lower_case_string(address_2.hash), + watch_coin_input: true, + watch_coin_output: true, + watch_erc_20_input: true, + watch_erc_20_output: true, + watch_erc_721_input: true, + watch_erc_721_output: true, + watch_erc_1155_input: true, + watch_erc_1155_output: true, + notify_email: true + }) + + transactions_1 = insert_list(2, :transaction, from_address: address_1) |> with_block() + transactions_2 = insert_list(1, :transaction, from_address: address_2, to_address: address_1) |> with_block() + transactions_3 = insert_list(3, :transaction, to_address: address_2) |> with_block() + transactions = (transactions_1 ++ transactions_2 ++ transactions_3) |> Enum.reverse() + + request = get(conn, "/api/v2/main-page/transactions/watchlist") + assert response = json_response(request, 200) + assert Enum.count(response) == 6 + + for i <- 0..5 do + compare_item(Enum.at(transactions, i), Enum.at(response, i), %{ + address_1.hash => watchlist_address_1.name, + address_2.hash => watchlist_address_2.name + }) + end + end + end + + describe "/main-page/indexing-status" do + test "get indexing status", %{conn: conn} do + request = get(conn, "/api/v2/main-page/indexing-status") + assert request = json_response(request, 200) + + assert Map.has_key?(request, "finished_indexing_blocks") + assert Map.has_key?(request, "finished_indexing") + assert Map.has_key?(request, "indexed_blocks_ratio") + assert Map.has_key?(request, "indexed_internal_transactions_ratio") + end + end + + defp compare_item(%Block{} = block, json) do + assert to_string(block.hash) == json["hash"] + assert block.number == json["height"] + end + + defp compare_item(%Transaction{} = transaction, json) do + assert to_string(transaction.hash) == json["hash"] + assert transaction.block_number == json["block_number"] + assert to_string(transaction.value.value) == json["value"] + assert Address.checksum(transaction.from_address_hash) == json["from"]["hash"] + assert Address.checksum(transaction.to_address_hash) == json["to"]["hash"] + end + + defp compare_item(%Transaction{} = transaction, json, wl_names) do + assert to_string(transaction.hash) == json["hash"] + assert transaction.block_number == json["block_number"] + assert to_string(transaction.value.value) == json["value"] + assert Address.checksum(transaction.from_address_hash) == json["from"]["hash"] + assert Address.checksum(transaction.to_address_hash) == json["to"]["hash"] + + assert json["to"]["watchlist_names"] == + if(wl_names[transaction.to_address_hash], + do: [ + %{ + "display_name" => wl_names[transaction.to_address_hash], + "label" => wl_names[transaction.to_address_hash] + } + ], + else: [] + ) + + assert json["from"]["watchlist_names"] == + if(wl_names[transaction.from_address_hash], + do: [ + %{ + "display_name" => wl_names[transaction.from_address_hash], + "label" => wl_names[transaction.from_address_hash] + } + ], + else: [] + ) + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/api/v2/search_controller_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/api/v2/search_controller_test.exs new file mode 100644 index 0000000..63e9d8f --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/api/v2/search_controller_test.exs @@ -0,0 +1,1418 @@ +defmodule BlockScoutWeb.API.V2.SearchControllerTest do + use BlockScoutWeb.ConnCase + + alias Explorer.Chain.{Address, Block} + alias Explorer.Tags.AddressTag + alias Plug.Conn.Query + + describe "/search" do + test "search block", %{conn: conn} do + block = insert(:block) + + request = get(conn, "/api/v2/search?q=#{block.hash}") + assert response = json_response(request, 200) + + assert Enum.count(response["items"]) == 1 + assert response["next_page_params"] == nil + + item = Enum.at(response["items"], 0) + + assert item["type"] == "block" + assert item["block_type"] == "block" + assert item["block_number"] == block.number + assert item["block_hash"] == to_string(block.hash) + assert item["url"] =~ to_string(block.hash) + + request = get(conn, "/api/v2/search?q=#{block.number}") + assert response = json_response(request, 200) + + assert Enum.count(response["items"]) == 1 + assert response["next_page_params"] == nil + + item = Enum.at(response["items"], 0) + + assert item["type"] == "block" + assert item["block_number"] == block.number + assert item["block_hash"] == to_string(block.hash) + assert item["url"] =~ to_string(block.hash) + assert item["timestamp"] == block.timestamp |> to_string() |> String.replace(" ", "T") + end + + test "search block with small and short number", %{conn: conn} do + block = insert(:block, number: 1) + + request = get(conn, "/api/v2/search?q=#{block.number}") + assert response = json_response(request, 200) + + assert Enum.count(response["items"]) == 1 + assert response["next_page_params"] == nil + + item = Enum.at(response["items"], 0) + + assert item["type"] == "block" + assert item["block_number"] == block.number + assert item["block_hash"] == to_string(block.hash) + assert item["url"] =~ to_string(block.hash) + assert item["timestamp"] == block.timestamp |> to_string() |> String.replace(" ", "T") + end + + test "search reorg", %{conn: conn} do + block = insert(:block, consensus: false) + + request = get(conn, "/api/v2/search?q=#{block.hash}") + assert response = json_response(request, 200) + + assert Enum.count(response["items"]) == 1 + assert response["next_page_params"] == nil + + item = Enum.at(response["items"], 0) + + assert item["type"] == "block" + assert item["block_type"] == "reorg" + assert item["block_number"] == block.number + assert item["block_hash"] == to_string(block.hash) + assert item["url"] =~ to_string(block.hash) + + request = get(conn, "/api/v2/search?q=#{block.number}") + assert response = json_response(request, 200) + + assert Enum.count(response["items"]) == 1 + assert response["next_page_params"] == nil + + item = Enum.at(response["items"], 0) + + assert item["type"] == "block" + assert item["block_type"] == "reorg" + assert item["block_number"] == block.number + assert item["block_hash"] == to_string(block.hash) + assert item["url"] =~ to_string(block.hash) + end + + test "search address", %{conn: conn} do + address = insert(:address) + name = insert(:unique_address_name, address: address) + + request = get(conn, "/api/v2/search?q=#{address.hash}") + assert response = json_response(request, 200) + + assert Enum.count(response["items"]) == 1 + assert response["next_page_params"] == nil + + item = Enum.at(response["items"], 0) + + assert item["type"] == "address" + assert item["name"] == name.name + assert item["address"] == Address.checksum(address.hash) + assert item["url"] =~ Address.checksum(address.hash) + assert item["is_smart_contract_verified"] == address.verified + end + + test "search contract", %{conn: conn} do + contract = insert(:unique_smart_contract) + + request = get(conn, "/api/v2/search?q=#{contract.name}") + assert response = json_response(request, 200) + + assert Enum.count(response["items"]) == 1 + assert response["next_page_params"] == nil + + item = Enum.at(response["items"], 0) + + assert item["type"] == "contract" + assert item["name"] == contract.name + assert item["address"] == Address.checksum(contract.address_hash) + assert item["url"] =~ Address.checksum(contract.address_hash) + assert item["is_smart_contract_verified"] == true + end + + test "check pagination", %{conn: conn} do + name = "contract" + _contracts = insert_list(51, :smart_contract, name: name) + + request = get(conn, "/api/v2/search?q=#{name}") + assert response = json_response(request, 200) + + assert Enum.count(response["items"]) == 50 + assert response["next_page_params"] != nil + + item = Enum.at(response["items"], 0) + + assert item["type"] == "contract" + assert item["name"] == name + + request_2 = get(conn, "/api/v2/search", response["next_page_params"] |> Query.encode() |> Query.decode()) + assert response_2 = json_response(request_2, 200) + + assert Enum.count(response_2["items"]) == 1 + assert response_2["next_page_params"] == nil + + item = Enum.at(response_2["items"], 0) + + assert item["type"] == "contract" + assert item["name"] == name + + assert item not in response["items"] + end + + test "check pagination #1", %{conn: conn} do + name = "contract" + contracts = for(i <- 0..50, do: insert(:smart_contract, name: "#{name} #{i}")) |> Enum.sort_by(fn x -> x.name end) + + tokens = + for i <- 0..50, do: insert(:token, name: "#{name} #{i}", circulating_market_cap: 10000 - i, holder_count: 0) + + labels = + for(i <- 0..50, do: insert(:address_to_tag, tag: build(:address_tag, display_name: "#{name} #{i}"))) + |> Enum.sort_by(fn x -> x.tag.display_name end) + + request = get(conn, "/api/v2/search?q=#{name}") + assert response = json_response(request, 200) + + assert Enum.count(response["items"]) == 50 + assert response["next_page_params"] != nil + assert Enum.at(response["items"], 0)["type"] == "label" + assert Enum.at(response["items"], 49)["type"] == "label" + + request_2 = get(conn, "/api/v2/search", response["next_page_params"] |> Query.encode() |> Query.decode()) + assert response_2 = json_response(request_2, 200) + + assert Enum.count(response_2["items"]) == 50 + assert response_2["next_page_params"] != nil + assert Enum.at(response_2["items"], 0)["type"] == "label" + assert Enum.at(response_2["items"], 1)["type"] == "token" + assert Enum.at(response_2["items"], 49)["type"] == "token" + + request_3 = get(conn, "/api/v2/search", response_2["next_page_params"] |> Query.encode() |> Query.decode()) + assert response_3 = json_response(request_3, 200) + + assert Enum.count(response_3["items"]) == 50 + assert response_3["next_page_params"] != nil + assert Enum.at(response_3["items"], 0)["type"] == "token" + assert Enum.at(response_3["items"], 1)["type"] == "token" + assert Enum.at(response_3["items"], 2)["type"] == "contract" + assert Enum.at(response_3["items"], 49)["type"] == "contract" + + request_4 = get(conn, "/api/v2/search", response_3["next_page_params"] |> Query.encode() |> Query.decode()) + assert response_4 = json_response(request_4, 200) + + assert Enum.count(response_4["items"]) == 3 + assert response_4["next_page_params"] == nil + assert Enum.all?(response_4["items"], fn x -> x["type"] == "contract" end) + + labels_from_api = response["items"] ++ [Enum.at(response_2["items"], 0)] + + assert labels + |> Enum.zip(labels_from_api) + |> Enum.all?(fn {label, item} -> + label.tag.display_name == item["name"] && item["type"] == "label" && + item["address"] == Address.checksum(label.address_hash) + end) + + tokens_from_api = Enum.slice(response_2["items"], 1, 49) ++ Enum.slice(response_3["items"], 0, 2) + + assert tokens + |> Enum.zip(tokens_from_api) + |> Enum.all?(fn {token, item} -> + token.name == item["name"] && item["type"] == "token" && + item["address"] == Address.checksum(token.contract_address_hash) + end) + + contracts_from_api = Enum.slice(response_3["items"], 2, 48) ++ response_4["items"] + + assert contracts + |> Enum.zip(contracts_from_api) + |> Enum.all?(fn {contract, item} -> + contract.name == item["name"] && item["type"] == "contract" && + item["address"] == Address.checksum(contract.address_hash) + end) + end + + test "check pagination #2 (token should be ranged by fiat_value)", %{conn: conn} do + name = "contract" + contracts = for(i <- 0..50, do: insert(:smart_contract, name: "#{name} #{i}")) |> Enum.sort_by(fn x -> x.name end) + + tokens = + for i <- 0..50, do: insert(:token, name: "#{name} #{i}", fiat_value: 10000 - i, holder_count: 0) + + labels = + for(i <- 0..50, do: insert(:address_to_tag, tag: build(:address_tag, display_name: "#{name} #{i}"))) + |> Enum.sort_by(fn x -> x.tag.display_name end) + + request = get(conn, "/api/v2/search?q=#{name}") + assert response = json_response(request, 200) + + assert Enum.count(response["items"]) == 50 + assert response["next_page_params"] != nil + assert Enum.at(response["items"], 0)["type"] == "label" + assert Enum.at(response["items"], 49)["type"] == "label" + + request_2 = get(conn, "/api/v2/search", response["next_page_params"] |> Query.encode() |> Query.decode()) + assert response_2 = json_response(request_2, 200) + + assert Enum.count(response_2["items"]) == 50 + assert response_2["next_page_params"] != nil + assert Enum.at(response_2["items"], 0)["type"] == "label" + assert Enum.at(response_2["items"], 1)["type"] == "token" + assert Enum.at(response_2["items"], 49)["type"] == "token" + + request_3 = get(conn, "/api/v2/search", response_2["next_page_params"] |> Query.encode() |> Query.decode()) + assert response_3 = json_response(request_3, 200) + + assert Enum.count(response_3["items"]) == 50 + assert response_3["next_page_params"] != nil + assert Enum.at(response_3["items"], 0)["type"] == "token" + assert Enum.at(response_3["items"], 1)["type"] == "token" + assert Enum.at(response_3["items"], 2)["type"] == "contract" + assert Enum.at(response_3["items"], 49)["type"] == "contract" + + request_4 = get(conn, "/api/v2/search", response_3["next_page_params"] |> Query.encode() |> Query.decode()) + assert response_4 = json_response(request_4, 200) + + assert Enum.count(response_4["items"]) == 3 + assert response_4["next_page_params"] == nil + assert Enum.all?(response_4["items"], fn x -> x["type"] == "contract" end) + + labels_from_api = response["items"] ++ [Enum.at(response_2["items"], 0)] + + assert labels + |> Enum.zip(labels_from_api) + |> Enum.all?(fn {label, item} -> + label.tag.display_name == item["name"] && item["type"] == "label" && + item["address"] == Address.checksum(label.address_hash) + end) + + tokens_from_api = Enum.slice(response_2["items"], 1, 49) ++ Enum.slice(response_3["items"], 0, 2) + + assert tokens + |> Enum.zip(tokens_from_api) + |> Enum.all?(fn {token, item} -> + token.name == item["name"] && item["type"] == "token" && + item["address"] == Address.checksum(token.contract_address_hash) + end) + + contracts_from_api = Enum.slice(response_3["items"], 2, 48) ++ response_4["items"] + + assert contracts + |> Enum.zip(contracts_from_api) + |> Enum.all?(fn {contract, item} -> + contract.name == item["name"] && item["type"] == "contract" && + item["address"] == Address.checksum(contract.address_hash) + end) + end + + test "search token", %{conn: conn} do + token = insert(:unique_token) + + request = get(conn, "/api/v2/search?q=#{token.name}") + assert response = json_response(request, 200) + + assert Enum.count(response["items"]) == 1 + assert response["next_page_params"] == nil + + item = Enum.at(response["items"], 0) + + assert item["type"] == "token" + assert item["name"] == token.name + assert item["symbol"] == token.symbol + assert item["address"] == Address.checksum(token.contract_address_hash) + assert item["token_url"] =~ Address.checksum(token.contract_address_hash) + assert item["address_url"] =~ Address.checksum(token.contract_address_hash) + assert item["token_type"] == token.type + assert item["is_smart_contract_verified"] == token.contract_address.verified + assert item["exchange_rate"] == (token.fiat_value && to_string(token.fiat_value)) + assert item["total_supply"] == to_string(token.total_supply) + assert item["icon_url"] == token.icon_url + assert item["is_verified_via_admin_panel"] == token.is_verified_via_admin_panel + end + + test "search token by hash", %{conn: conn} do + token = insert(:unique_token) + + request = get(conn, "/api/v2/search?q=#{token.contract_address_hash}") + assert response = json_response(request, 200) + + assert Enum.count(response["items"]) == 2 + assert response["next_page_params"] == nil + + item = Enum.at(response["items"], 0) + + assert item["type"] == "token" + assert item["name"] == token.name + assert item["symbol"] == token.symbol + assert item["address"] == Address.checksum(token.contract_address_hash) + assert item["token_url"] =~ Address.checksum(token.contract_address_hash) + assert item["address_url"] =~ Address.checksum(token.contract_address_hash) + assert item["token_type"] == token.type + assert item["is_smart_contract_verified"] == token.contract_address.verified + assert item["exchange_rate"] == (token.fiat_value && to_string(token.fiat_value)) + assert item["total_supply"] == to_string(token.total_supply) + assert item["icon_url"] == token.icon_url + assert item["is_verified_via_admin_panel"] == token.is_verified_via_admin_panel + + item_1 = Enum.at(response["items"], 1) + + assert item_1["type"] == "address" + end + + test "search transaction", %{conn: conn} do + transaction = insert(:transaction, block_timestamp: nil) + + request = get(conn, "/api/v2/search?q=#{transaction.hash}") + assert response = json_response(request, 200) + + assert Enum.count(response["items"]) == 1 + assert response["next_page_params"] == nil + + item = Enum.at(response["items"], 0) + + assert item["type"] == "transaction" + assert item["transaction_hash"] == to_string(transaction.hash) + assert item["url"] =~ to_string(transaction.hash) + assert item["timestamp"] == nil + end + + test "search transaction with timestamp", %{conn: conn} do + transaction = :transaction |> insert() + block = insert(:block, hash: transaction.hash) + transaction |> with_block(block) + + request = get(conn, "/api/v2/search?q=#{transaction.hash}") + assert response = json_response(request, 200) + + assert Enum.count(response["items"]) == 2 + assert response["next_page_params"] == nil + + transaction_item = Enum.find(response["items"], fn x -> x["type"] == "transaction" end) + + assert transaction_item["type"] == "transaction" + assert transaction_item["transaction_hash"] == to_string(transaction.hash) + assert transaction_item["url"] =~ to_string(transaction.hash) + + assert transaction_item["timestamp"] == + block.timestamp |> to_string() |> String.replace(" ", "T") + + block_item = Enum.find(response["items"], fn x -> x["type"] == "block" end) + assert block_item["type"] == "block" + assert block_item["block_hash"] == to_string(block.hash) + assert block_item["url"] =~ to_string(block.hash) + assert transaction_item["timestamp"] == block_item["timestamp"] + end + + test "search tags", %{conn: conn} do + tag = insert(:address_to_tag) + + request = get(conn, "/api/v2/search?q=#{tag.tag.display_name}") + assert response = json_response(request, 200) + + assert Enum.count(response["items"]) == 1 + assert response["next_page_params"] == nil + + item = Enum.at(response["items"], 0) + + assert item["type"] == "label" + assert item["address"] == Address.checksum(tag.address.hash) + assert item["name"] == tag.tag.display_name + assert item["url"] =~ Address.checksum(tag.address.hash) + assert item["is_smart_contract_verified"] == tag.address.verified + end + + test "check that simultaneous search of ", %{conn: conn} do + block = insert(:block, number: 10000) + + insert(:smart_contract, name: to_string(block.number)) + insert(:token, name: to_string(block.number)) + + insert(:address_to_tag, + tag: %AddressTag{ + label: "qwerty", + display_name: to_string(block.number) + } + ) + + request = get(conn, "/api/v2/search?q=#{block.number}") + assert response = json_response(request, 200) + + assert Enum.count(response["items"]) == 4 + assert response["next_page_params"] == nil + end + + test "search for a big positive integer", %{conn: conn} do + big_integer = :math.pow(2, 64) |> round |> :erlang.integer_to_binary() + request = get(conn, "/api/v2/search?q=#{big_integer}") + assert response = json_response(request, 200) + + assert Enum.count(response["items"]) == 0 + assert response["next_page_params"] == nil + end + + test "search for a big negative integer", %{conn: conn} do + big_integer = (:math.pow(2, 64) - 1) |> round |> :erlang.integer_to_binary() + request = get(conn, "/api/v2/search?q=#{big_integer}") + assert response = json_response(request, 200) + + assert Enum.count(response["items"]) == 0 + assert response["next_page_params"] == nil + end + + test "check pagination #3 (ens and metadata tags added)", %{conn: conn} do + bypass = Bypass.open() + metadata_envs = Application.get_env(:explorer, Explorer.MicroserviceInterfaces.Metadata) + bens_envs = Application.get_env(:explorer, Explorer.MicroserviceInterfaces.BENS) + old_chain_id = Application.get_env(:block_scout_web, :chain_id) + chain_id = 1 + Application.put_env(:block_scout_web, :chain_id, chain_id) + old_hide_scam_addresses = Application.get_env(:block_scout_web, :hide_scam_addresses) + Application.put_env(:block_scout_web, :hide_scam_addresses, true) + + on_exit(fn -> + Bypass.down(bypass) + Application.put_env(:explorer, Explorer.MicroserviceInterfaces.Metadata, metadata_envs) + Application.put_env(:explorer, Explorer.MicroserviceInterfaces.BENS, bens_envs) + Application.put_env(:block_scout_web, :chain_id, old_chain_id) + Application.put_env(:block_scout_web, :hide_scam_addresses, old_hide_scam_addresses) + end) + + Application.put_env(:explorer, Explorer.MicroserviceInterfaces.Metadata, + service_url: "http://localhost:#{bypass.port}", + enabled: true + ) + + Application.put_env(:explorer, Explorer.MicroserviceInterfaces.BENS, + service_url: "http://localhost:#{bypass.port}", + enabled: true + ) + + name = "contract.eth" + + contracts = + for(i <- 0..50, do: insert(:smart_contract, name: "#{name |> String.replace(".", " ")} #{i}")) + |> Enum.sort_by(fn x -> x.name end) + + tokens = + for i <- 0..50, + do: insert(:token, name: "#{name |> String.replace(".", " ")} #{i}", fiat_value: 10000 - i, holder_count: 0) + + labels = + for( + i <- 0..50, + do: + insert(:address_to_tag, tag: build(:address_tag, display_name: "#{name |> String.replace(".", " ")} #{i}")) + ) + |> Enum.sort_by(fn x -> x.tag.display_name end) + + address_1 = insert(:address) + address_2 = insert(:address) + address_3 = build(:address) + address_4 = build(:address) + address_5 = insert(:address) + + metadata_response_1 = + %{ + "items" => + for( + i <- 0..24, + do: %{ + "tag" => %{ + "slug" => "#{name} #{i}", + "name" => "#{name} #{i}", + "tagType" => "name", + "ordinal" => 0, + "meta" => "{}" + }, + "addresses" => [ + to_string(address_1) + ] + } + ) ++ + for( + i <- 0..23, + do: %{ + "tag" => %{ + "slug" => "#{name} #{25 + i}", + "name" => "#{name} #{25 + i}", + "tagType" => "name", + "ordinal" => 0, + "meta" => "{}" + }, + "addresses" => [ + to_string(address_2) + ] + } + ) ++ + [ + %{ + "tag" => %{ + "slug" => "#{name} 49", + "name" => "#{name} 49", + "tagType" => "name", + "ordinal" => 0, + "meta" => "{}" + }, + "addresses" => [ + to_string(address_3), + to_string(address_4) + ] + } + ], + "next_page_params" => %{ + "page_token" => "0,celo:_eth_helper,name", + "page_size" => 50 + } + } + + metadata_response_2 = %{ + "items" => + for( + i <- 22..23, + do: %{ + "tag" => %{ + "slug" => "#{name} #{25 + i}", + "name" => "#{name} #{25 + i}", + "tagType" => "name", + "ordinal" => 0, + "meta" => "{}" + }, + "addresses" => [ + to_string(address_2) + ] + } + ) ++ + [ + %{ + "tag" => %{ + "slug" => "#{name} 49", + "name" => "#{name} 49", + "tagType" => "name", + "ordinal" => 0, + "meta" => "{}" + }, + "addresses" => [ + to_string(address_3), + to_string(address_4) + ] + } + ] ++ + [ + %{ + "tag" => %{ + "slug" => "#{name} 0", + "name" => "#{name} 0", + "tagType" => "name", + "ordinal" => 0, + "meta" => "{}" + }, + "addresses" => [ + to_string(address_5) + ] + } + ], + "next_page_params" => nil + } + + page_token_1 = "0,#{name} #{47},name" + + Bypass.expect( + bypass, + "GET", + "/api/v1/tags%3Asearch", + fn conn -> + assert conn.params["name"] == name + + case conn.params["page_token"] do + nil -> Plug.Conn.resp(conn, 200, Jason.encode!(metadata_response_1)) + ^page_token_1 -> Plug.Conn.resp(conn, 200, Jason.encode!(metadata_response_2)) + _ -> raise "Unexpected page_token" + end + end + ) + + ens_address = insert(:address) + + ens_response = """ + { + "items": [ + { + "id": "0xee6c4522aab0003e8d14cd40a6af439055fd2577951148c14b6cea9a53475835", + "name": "#{name}", + "resolved_address": { + "hash": "#{to_string(ens_address)}" + }, + "owner": { + "hash": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045" + }, + "wrapped_owner": null, + "registration_date": "2017-06-18T08:39:14.000Z", + "expiry_date": null, + "protocol": { + "id": "ens", + "short_name": "ENS", + "title": "Ethereum Name Service", + "description": "The Ethereum Name Service (ENS) is a distributed, open, and extensible naming system based on the Ethereum blockchain.", + "deployment_blockscout_base_url": "https://eth.blockscout.com/", + "tld_list": [ + "eth" + ], + "icon_url": "https://i.imgur.com/GOfUwCb.jpeg", + "docs_url": "https://docs.ens.domains/" + } + } + ], + "next_page_params": null + } + """ + + Bypass.expect( + bypass, + "GET", + "/api/v1/1/domains%3Alookup", + fn conn -> + assert conn.params["name"] == name + + Plug.Conn.resp(conn, 200, ens_response) + end + ) + + request = get(conn, "/api/v2/search?q=#{name}") + assert response = json_response(request, 200) + + assert Enum.count(response["items"]) == 50 + assert response["next_page_params"] != nil + assert Enum.at(response["items"], 0)["type"] == "ens_domain" + assert Enum.slice(response["items"], 1, 49) |> Enum.all?(fn x -> x["type"] == "label" end) + + request_2 = get(conn, "/api/v2/search", response["next_page_params"] |> Query.encode() |> Query.decode()) + assert response_2 = json_response(request_2, 200) + + assert Enum.count(response_2["items"]) == 50 + assert response_2["next_page_params"] != nil + assert Enum.at(response_2["items"], 0)["type"] == "label" + assert Enum.at(response_2["items"], 1)["type"] == "label" + assert Enum.slice(response_2["items"], 2, 48) |> Enum.all?(fn x -> x["type"] == "token" end) + + request_3 = get(conn, "/api/v2/search", response_2["next_page_params"] |> Query.encode() |> Query.decode()) + assert response_3 = json_response(request_3, 200) + + assert Enum.count(response_3["items"]) == 50 + assert response_3["next_page_params"] != nil + + assert Enum.slice(response_3["items"], 0, 3) |> Enum.all?(fn x -> x["type"] == "token" end) + assert Enum.slice(response_3["items"], 3, 47) |> Enum.all?(fn x -> x["type"] == "metadata_tag" end) + + request_4 = get(conn, "/api/v2/search", response_3["next_page_params"] |> Query.encode() |> Query.decode()) + assert response_4 = json_response(request_4, 200) + + assert Enum.count(response_4["items"]) == 50 + assert response_4["next_page_params"] != nil + + assert Enum.slice(response_4["items"], 0, 5) |> Enum.all?(fn x -> x["type"] == "metadata_tag" end) + assert Enum.slice(response_4["items"], 5, 45) |> Enum.all?(fn x -> x["type"] == "contract" end) + + request_5 = get(conn, "/api/v2/search", response_4["next_page_params"] |> Query.encode() |> Query.decode()) + assert response_5 = json_response(request_5, 200) + + assert Enum.count(response_5["items"]) == 6 + assert response_5["next_page_params"] == nil + + assert Enum.all?(response_5["items"], fn x -> x["type"] == "contract" end) + + labels_from_api = Enum.slice(response["items"], 1, 49) ++ Enum.slice(response_2["items"], 0, 2) + + assert labels + |> Enum.zip(labels_from_api) + |> Enum.all?(fn {label, item} -> + label.tag.display_name == item["name"] && item["type"] == "label" && + item["address"] == Address.checksum(label.address_hash) + end) + + tokens_from_api = Enum.slice(response_2["items"], 2, 48) ++ Enum.slice(response_3["items"], 0, 3) + + assert tokens + |> Enum.zip(tokens_from_api) + |> Enum.all?(fn {token, item} -> + token.name == item["name"] && item["type"] == "token" && + item["address"] == Address.checksum(token.contract_address_hash) + end) + + contracts_from_api = Enum.slice(response_4["items"], 5, 45) ++ response_5["items"] + + assert contracts + |> Enum.zip(contracts_from_api) + |> Enum.all?(fn {contract, item} -> + contract.name == item["name"] && item["type"] == "contract" && + item["address"] == Address.checksum(contract.address_hash) + end) + + metadata_tags_from_api = Enum.slice(response_3["items"], 3, 47) ++ Enum.slice(response_4["items"], 0, 5) + + metadata_tags = + ((metadata_response_1["items"] |> Enum.drop(-3)) ++ metadata_response_2["items"]) + |> Enum.reduce([], fn x, acc -> + acc ++ + Enum.map(x["addresses"], fn addr -> + {addr, x["tag"]} + end) + end) + + assert metadata_tags + |> Enum.zip(metadata_tags_from_api) + |> Enum.all?(fn {{address_hash, tag}, api_item} -> + tag["name"] == api_item["metadata"]["name"] && tag["slug"] == api_item["metadata"]["slug"] && + api_item["type"] == "metadata_tag" && + api_item["address"] == address_hash + end) + + ens = Enum.at(response["items"], 0) + assert ens["address"] == to_string(ens_address) + assert ens["ens_info"]["name"] == name + end + + test "check pagination #4 (ens and metadata tags (complex case) added)", %{conn: conn} do + bypass = Bypass.open() + metadata_envs = Application.get_env(:explorer, Explorer.MicroserviceInterfaces.Metadata) + bens_envs = Application.get_env(:explorer, Explorer.MicroserviceInterfaces.BENS) + old_chain_id = Application.get_env(:block_scout_web, :chain_id) + chain_id = 1 + Application.put_env(:block_scout_web, :chain_id, chain_id) + + on_exit(fn -> + Bypass.down(bypass) + Application.put_env(:explorer, Explorer.MicroserviceInterfaces.Metadata, metadata_envs) + Application.put_env(:explorer, Explorer.MicroserviceInterfaces.BENS, bens_envs) + Application.put_env(:block_scout_web, :chain_id, old_chain_id) + end) + + Application.put_env(:explorer, Explorer.MicroserviceInterfaces.Metadata, + service_url: "http://localhost:#{bypass.port}", + enabled: true + ) + + Application.put_env(:explorer, Explorer.MicroserviceInterfaces.BENS, + service_url: "http://localhost:#{bypass.port}", + enabled: true + ) + + name = "contract.eth" + + contracts = + for(i <- 0..50, do: insert(:smart_contract, name: "#{name |> String.replace(".", " ")} #{i}")) + |> Enum.sort_by(fn x -> x.name end) + + tokens = + for i <- 0..50, + do: insert(:token, name: "#{name |> String.replace(".", " ")} #{i}", fiat_value: 10000 - i, holder_count: 0) + + labels = + for( + i <- 0..50, + do: + insert(:address_to_tag, tag: build(:address_tag, display_name: "#{name |> String.replace(".", " ")} #{i}")) + ) + |> Enum.sort_by(fn x -> x.tag.display_name end) + + address_1 = insert(:address) + address_2 = insert(:address) + address_3 = build(:address) + address_4 = build(:address) + address_5 = insert(:address) + + metadata_response_1 = + %{ + "items" => + for( + i <- 0..24, + do: %{ + "tag" => %{ + "slug" => "#{name} #{i}", + "name" => "#{name} #{i}", + "tagType" => "name", + "ordinal" => 0, + "meta" => "{}" + }, + "addresses" => [ + to_string(address_1) + ] + } + ) ++ + for( + i <- 0..20, + do: %{ + "tag" => %{ + "slug" => "#{name} #{25 + i}", + "name" => "#{name} #{25 + i}", + "tagType" => "name", + "ordinal" => 0, + "meta" => "{}" + }, + "addresses" => [ + to_string(address_2) + ] + } + ) ++ + [ + %{ + "tag" => %{ + "slug" => "#{name} #{25 + 21}", + "name" => "#{name} #{25 + 21}", + "tagType" => "name", + "ordinal" => 0, + "meta" => "{}" + }, + "addresses" => [ + to_string(address_2), + to_string(address_3) + ] + } + ] ++ + for( + i <- 22..23, + do: %{ + "tag" => %{ + "slug" => "#{name} #{25 + i}", + "name" => "#{name} #{25 + i}", + "tagType" => "name", + "ordinal" => 0, + "meta" => "{}" + }, + "addresses" => [ + to_string(address_2) + ] + } + ) ++ + [ + %{ + "tag" => %{ + "slug" => "#{name} 49", + "name" => "#{name} 49", + "tagType" => "name", + "ordinal" => 0, + "meta" => "{}" + }, + "addresses" => [ + to_string(address_4) + ] + } + ], + "next_page_params" => %{ + "page_token" => "0,celo:_eth_helper,name", + "page_size" => 50 + } + } + + metadata_response_2 = %{ + "items" => + [ + %{ + "tag" => %{ + "slug" => "#{name} #{25 + 21}", + "name" => "#{name} #{25 + 21}", + "tagType" => "name", + "ordinal" => 0, + "meta" => "{}" + }, + "addresses" => [ + to_string(address_2), + to_string(address_3) + ] + } + ] ++ + for( + i <- 22..23, + do: %{ + "tag" => %{ + "slug" => "#{name} #{25 + i}", + "name" => "#{name} #{25 + i}", + "tagType" => "name", + "ordinal" => 0, + "meta" => "{}" + }, + "addresses" => [ + to_string(address_2) + ] + } + ) ++ + [ + %{ + "tag" => %{ + "slug" => "#{name} 49", + "name" => "#{name} 49", + "tagType" => "name", + "ordinal" => 0, + "meta" => "{}" + }, + "addresses" => [ + to_string(address_4) + ] + } + ] ++ + [ + %{ + "tag" => %{ + "slug" => "#{name} 0", + "name" => "#{name} 0", + "tagType" => "name", + "ordinal" => 0, + "meta" => "{}" + }, + "addresses" => [ + to_string(address_5) + ] + } + ], + "next_page_params" => nil + } + + page_token_1 = "0,#{name} #{46},name" + + Bypass.expect( + bypass, + "GET", + "/api/v1/tags%3Asearch", + fn conn -> + assert conn.params["name"] == name + + case conn.params["page_token"] do + nil -> Plug.Conn.resp(conn, 200, Jason.encode!(metadata_response_1)) + ^page_token_1 -> Plug.Conn.resp(conn, 200, Jason.encode!(metadata_response_2)) + _ -> raise "Unexpected page_token" + end + end + ) + + ens_address = insert(:address) + + ens_response = """ + { + "items": [ + { + "id": "0xee6c4522aab0003e8d14cd40a6af439055fd2577951148c14b6cea9a53475835", + "name": "#{name}", + "resolved_address": { + "hash": "#{to_string(ens_address)}" + }, + "owner": { + "hash": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045" + }, + "wrapped_owner": null, + "registration_date": "2017-06-18T08:39:14.000Z", + "expiry_date": null, + "protocol": { + "id": "ens", + "short_name": "ENS", + "title": "Ethereum Name Service", + "description": "The Ethereum Name Service (ENS) is a distributed, open, and extensible naming system based on the Ethereum blockchain.", + "deployment_blockscout_base_url": "https://eth.blockscout.com/", + "tld_list": [ + "eth" + ], + "icon_url": "https://i.imgur.com/GOfUwCb.jpeg", + "docs_url": "https://docs.ens.domains/" + } + } + ], + "next_page_params": null + } + """ + + Bypass.expect( + bypass, + "GET", + "/api/v1/1/domains%3Alookup", + fn conn -> + assert conn.params["name"] == name + + Plug.Conn.resp(conn, 200, ens_response) + end + ) + + request = get(conn, "/api/v2/search?q=#{name}") + assert response = json_response(request, 200) + + assert Enum.count(response["items"]) == 50 + assert response["next_page_params"] != nil + assert Enum.at(response["items"], 0)["type"] == "ens_domain" + assert Enum.slice(response["items"], 1, 49) |> Enum.all?(fn x -> x["type"] == "label" end) + + request_2 = get(conn, "/api/v2/search", response["next_page_params"] |> Query.encode() |> Query.decode()) + assert response_2 = json_response(request_2, 200) + + assert Enum.count(response_2["items"]) == 50 + assert response_2["next_page_params"] != nil + assert Enum.at(response_2["items"], 0)["type"] == "label" + assert Enum.at(response_2["items"], 1)["type"] == "label" + assert Enum.slice(response_2["items"], 2, 48) |> Enum.all?(fn x -> x["type"] == "token" end) + + request_3 = get(conn, "/api/v2/search", response_2["next_page_params"] |> Query.encode() |> Query.decode()) + assert response_3 = json_response(request_3, 200) + + assert Enum.count(response_3["items"]) == 50 + assert response_3["next_page_params"] != nil + + assert Enum.slice(response_3["items"], 0, 3) |> Enum.all?(fn x -> x["type"] == "token" end) + assert Enum.slice(response_3["items"], 3, 47) |> Enum.all?(fn x -> x["type"] == "metadata_tag" end) + + request_4 = get(conn, "/api/v2/search", response_3["next_page_params"] |> Query.encode() |> Query.decode()) + assert response_4 = json_response(request_4, 200) + + assert Enum.count(response_4["items"]) == 50 + assert response_4["next_page_params"] != nil + + assert Enum.slice(response_4["items"], 0, 5) |> Enum.all?(fn x -> x["type"] == "metadata_tag" end) + assert Enum.slice(response_4["items"], 5, 45) |> Enum.all?(fn x -> x["type"] == "contract" end) + + request_5 = get(conn, "/api/v2/search", response_4["next_page_params"] |> Query.encode() |> Query.decode()) + assert response_5 = json_response(request_5, 200) + + assert Enum.count(response_5["items"]) == 6 + assert response_5["next_page_params"] == nil + + assert Enum.all?(response_5["items"], fn x -> x["type"] == "contract" end) + + labels_from_api = Enum.slice(response["items"], 1, 49) ++ Enum.slice(response_2["items"], 0, 2) + + assert labels + |> Enum.zip(labels_from_api) + |> Enum.all?(fn {label, item} -> + label.tag.display_name == item["name"] && item["type"] == "label" && + item["address"] == Address.checksum(label.address_hash) + end) + + tokens_from_api = Enum.slice(response_2["items"], 2, 48) ++ Enum.slice(response_3["items"], 0, 3) + + assert tokens + |> Enum.zip(tokens_from_api) + |> Enum.all?(fn {token, item} -> + token.name == item["name"] && item["type"] == "token" && + item["address"] == Address.checksum(token.contract_address_hash) + end) + + contracts_from_api = Enum.slice(response_4["items"], 5, 45) ++ response_5["items"] + + assert contracts + |> Enum.zip(contracts_from_api) + |> Enum.all?(fn {contract, item} -> + contract.name == item["name"] && item["type"] == "contract" && + item["address"] == Address.checksum(contract.address_hash) + end) + + metadata_tags_from_api = Enum.slice(response_3["items"], 3, 47) ++ Enum.slice(response_4["items"], 0, 5) + + metadata_tags = + ((metadata_response_1["items"] |> Enum.drop(-4)) ++ metadata_response_2["items"]) + |> Enum.reduce([], fn x, acc -> + acc ++ + Enum.map(x["addresses"], fn addr -> + {addr, x["tag"]} + end) + end) + + assert metadata_tags + |> Enum.zip(metadata_tags_from_api) + |> Enum.all?(fn {{address_hash, tag}, api_item} -> + tag["name"] == api_item["metadata"]["name"] && tag["slug"] == api_item["metadata"]["slug"] && + api_item["type"] == "metadata_tag" && + api_item["address"] == address_hash + end) + + ens = Enum.at(response["items"], 0) + assert ens["address"] == to_string(ens_address) + assert ens["ens_info"]["name"] == name + end + end + + describe "/search/check-redirect" do + test "finds a consensus block by block number", %{conn: conn} do + block = insert(:block) + + hash = to_string(block.hash) + + request = get(conn, "/api/v2/search/check-redirect?q=#{block.number}") + + assert %{"redirect" => true, "type" => "block", "parameter" => ^hash} = json_response(request, 200) + end + + test "redirects to search results page even for searching non-consensus block by number", %{conn: conn} do + %Block{number: number} = insert(:block, consensus: false) + + request = get(conn, "/api/v2/search/check-redirect?q=#{number}") + + %{"redirect" => false, "type" => nil, "parameter" => nil} = json_response(request, 200) + end + + test "finds non-consensus block by hash", %{conn: conn} do + %Block{hash: hash} = insert(:block, consensus: false) + + conn = get(conn, "/search?q=#{hash}") + + hash = to_string(hash) + + request = get(conn, "/api/v2/search/check-redirect?q=#{hash}") + + assert %{"redirect" => true, "type" => "block", "parameter" => ^hash} = json_response(request, 200) + end + + test "finds a transaction by hash", %{conn: conn} do + transaction = + :transaction + |> insert() + |> with_block() + + hash = to_string(transaction.hash) + + request = get(conn, "/api/v2/search/check-redirect?q=#{hash}") + + assert %{"redirect" => true, "type" => "transaction", "parameter" => ^hash} = json_response(request, 200) + end + + test "finds a transaction by hash when there are not 0x prefix", %{conn: conn} do + transaction = + :transaction + |> insert() + |> with_block() + + hash = to_string(transaction.hash) + + "0x" <> non_prefix_hash = to_string(transaction.hash) + + request = get(conn, "/api/v2/search/check-redirect?q=#{non_prefix_hash}") + + assert %{"redirect" => true, "type" => "transaction", "parameter" => ^hash} = json_response(request, 200) + end + + test "finds an address by hash", %{conn: conn} do + address = insert(:address) + + hash = Address.checksum(address.hash) + + request = get(conn, "/api/v2/search/check-redirect?q=#{to_string(address.hash)}") + + assert %{"redirect" => true, "type" => "address", "parameter" => ^hash} = json_response(request, 200) + end + + test "finds an address by hash when there are extra spaces", %{conn: conn} do + address = insert(:address) + + hash = Address.checksum(address.hash) + + request = get(conn, "/api/v2/search/check-redirect?q=#{to_string(address.hash)} ") + + assert %{"redirect" => true, "type" => "address", "parameter" => ^hash} = json_response(request, 200) + end + + test "finds an address by hash when there are not 0x prefix", %{conn: conn} do + address = insert(:address) + + "0x" <> non_prefix_hash = to_string(address.hash) + + hash = Address.checksum(address.hash) + + request = get(conn, "/api/v2/search/check-redirect?q=#{non_prefix_hash}") + + assert %{"redirect" => true, "type" => "address", "parameter" => ^hash} = json_response(request, 200) + end + + test "redirects to result page when it finds nothing", %{conn: conn} do + request = get(conn, "/api/v2/search/check-redirect?q=qwerty") + + %{"redirect" => false, "type" => nil, "parameter" => nil} = json_response(request, 200) + end + end + + describe "/search/quick" do + test "check that all categories are in response list", %{conn: conn} do + name = "156000" + + tags = + for _ <- 0..50 do + insert(:address_to_tag, tag: build(:address_tag, display_name: name)) + end + + contracts = insert_list(50, :smart_contract, name: name) + tokens = insert_list(50, :token, name: name) + blocks = [insert(:block, number: name, consensus: false), insert(:block, number: name)] + + request = get(conn, "/api/v2/search/quick?q=#{name}") + assert response = json_response(request, 200) + assert Enum.count(response) == 50 + + assert response |> Enum.filter(fn x -> x["type"] == "label" end) |> Enum.map(fn x -> x["address"] end) == + tags |> Enum.reverse() |> Enum.take(16) |> Enum.map(fn tag -> Address.checksum(tag.address.hash) end) + + assert response |> Enum.filter(fn x -> x["type"] == "contract" end) |> Enum.map(fn x -> x["address"] end) == + contracts + |> Enum.reverse() + |> Enum.take(16) + |> Enum.map(fn contract -> Address.checksum(contract.address_hash) end) + + assert response |> Enum.filter(fn x -> x["type"] == "token" end) |> Enum.map(fn x -> x["address"] end) == + tokens + |> Enum.reverse() + |> Enum.sort_by(fn x -> x.is_verified_via_admin_panel end, :desc) + |> Enum.take(16) + |> Enum.map(fn token -> Address.checksum(token.contract_address_hash) end) + + block_hashes = response |> Enum.filter(fn x -> x["type"] == "block" end) |> Enum.map(fn x -> x["block_hash"] end) + + assert block_hashes == blocks |> Enum.reverse() |> Enum.map(fn block -> to_string(block.hash) end) || + block_hashes == blocks |> Enum.map(fn block -> to_string(block.hash) end) + + assert response |> Enum.filter(fn x -> x["block_type"] == "block" end) |> Enum.count() == 1 + assert response |> Enum.filter(fn x -> x["block_type"] == "reorg" end) |> Enum.count() == 1 + end + + test "check that all categories are in response list (ens + metadata included)", %{conn: conn} do + bypass = Bypass.open() + metadata_envs = Application.get_env(:explorer, Explorer.MicroserviceInterfaces.Metadata) + bens_envs = Application.get_env(:explorer, Explorer.MicroserviceInterfaces.BENS) + old_chain_id = Application.get_env(:block_scout_web, :chain_id) + chain_id = 1 + Application.put_env(:block_scout_web, :chain_id, chain_id) + + on_exit(fn -> + Bypass.down(bypass) + Application.put_env(:explorer, Explorer.MicroserviceInterfaces.Metadata, metadata_envs) + Application.put_env(:explorer, Explorer.MicroserviceInterfaces.BENS, bens_envs) + Application.put_env(:block_scout_web, :chain_id, old_chain_id) + end) + + Application.put_env(:explorer, Explorer.MicroserviceInterfaces.Metadata, + service_url: "http://localhost:#{bypass.port}", + enabled: true + ) + + Application.put_env(:explorer, Explorer.MicroserviceInterfaces.BENS, + service_url: "http://localhost:#{bypass.port}", + enabled: true + ) + + name = "qwe.eth" + + tags = + for _ <- 0..50 do + insert(:address_to_tag, tag: build(:address_tag, display_name: name |> String.replace(".", " "))) + end + + contracts = insert_list(50, :smart_contract, name: name |> String.replace(".", " ")) + tokens = insert_list(50, :token, name: name |> String.replace(".", " ")) + ens_address = insert(:address) + address_1 = build(:address) + + metadata_response = + %{ + "items" => + for( + i <- 0..49, + do: %{ + "tag" => %{ + "slug" => "#{name} #{i}", + "name" => "#{name} #{i}", + "tagType" => "name", + "ordinal" => 0, + "meta" => "{}" + }, + "addresses" => [ + to_string(address_1) + ] + } + ), + "next_page_params" => %{ + "page_token" => "0,celo:_eth_helper,name", + "page_size" => 50 + } + } + + Bypass.expect( + bypass, + "GET", + "/api/v1/tags%3Asearch", + fn conn -> + assert conn.params["name"] == name + + Plug.Conn.resp(conn, 200, Jason.encode!(metadata_response)) + end + ) + + ens_response = """ + { + "items": [ + { + "id": "0xee6c4522aab0003e8d14cd40a6af439055fd2577951148c14b6cea9a53475835", + "name": "#{name}", + "resolved_address": { + "hash": "#{to_string(ens_address)}" + }, + "owner": { + "hash": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045" + }, + "wrapped_owner": null, + "registration_date": "2017-06-18T08:39:14.000Z", + "expiry_date": null, + "protocol": { + "id": "ens", + "short_name": "ENS", + "title": "Ethereum Name Service", + "description": "The Ethereum Name Service (ENS) is a distributed, open, and extensible naming system based on the Ethereum blockchain.", + "deployment_blockscout_base_url": "https://eth.blockscout.com/", + "tld_list": [ + "eth" + ], + "icon_url": "https://i.imgur.com/GOfUwCb.jpeg", + "docs_url": "https://docs.ens.domains/" + } + } + ], + "next_page_params": null + } + """ + + Bypass.expect( + bypass, + "GET", + "/api/v1/1/domains%3Alookup", + fn conn -> + assert conn.params["name"] == name + + Plug.Conn.resp(conn, 200, ens_response) + end + ) + + request = get(conn, "/api/v2/search/quick?q=#{name}") + assert response = json_response(request, 200) + assert Enum.count(response) == 50 + + assert response |> Enum.filter(fn x -> x["type"] == "label" end) |> Enum.map(fn x -> x["address"] end) == + tags |> Enum.reverse() |> Enum.take(12) |> Enum.map(fn tag -> Address.checksum(tag.address.hash) end) + + assert response |> Enum.filter(fn x -> x["type"] == "contract" end) |> Enum.map(fn x -> x["address"] end) == + contracts + |> Enum.reverse() + |> Enum.take(12) + |> Enum.map(fn contract -> Address.checksum(contract.address_hash) end) + + assert response |> Enum.filter(fn x -> x["type"] == "token" end) |> Enum.map(fn x -> x["address"] end) == + tokens + |> Enum.reverse() + |> Enum.sort_by(fn x -> x.is_verified_via_admin_panel end, :desc) + |> Enum.take(13) + |> Enum.map(fn token -> Address.checksum(token.contract_address_hash) end) + + assert response |> Enum.filter(fn x -> x["type"] == "ens_domain" end) |> Enum.map(fn x -> x["address"] end) == [ + to_string(ens_address) + ] + + metadata_tags = response |> Enum.filter(fn x -> x["type"] == "metadata_tag" end) + + assert Enum.count(metadata_tags) == 12 + + assert metadata_tags + |> Enum.with_index() + |> Enum.all?(fn {x, index} -> + x["address"] == to_string(address_1) && x["metadata"]["name"] == "#{name} #{index}" + end) + end + + test "returns empty list and don't crash", %{conn: conn} do + request = get(conn, "/api/v2/search/quick?q=qwertyuioiuytrewertyuioiuytrertyuio") + assert [] = json_response(request, 200) + end + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/api/v2/smart_contract_controller_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/api/v2/smart_contract_controller_test.exs new file mode 100644 index 0000000..74d1fb6 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/api/v2/smart_contract_controller_test.exs @@ -0,0 +1,1905 @@ +defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do + use BlockScoutWeb.ConnCase, async: false + use BlockScoutWeb.ChannelCase, async: false + + import Mox + + alias BlockScoutWeb.AddressContractView + alias Explorer.Chain.{Address, SmartContract} + alias Explorer.TestHelper + alias Plug.Conn + + setup :set_mox_from_context + + setup :verify_on_exit! + + setup_all do + # Create the mock safely with try-catch to avoid errors if already mocked + try do + :meck.new(Explorer.Chain.SmartContract, [:passthrough]) + catch + :error, {:already_started, _pid} -> :ok + end + + on_exit(fn -> + try do + :meck.unload(Explorer.Chain.SmartContract) + catch + # Ignore any errors when unloading + _, _ -> :ok + end + end) + + :ok + end + + describe "/smart-contracts/{address_hash}" do + test "get 404 on non existing SC", %{conn: conn} do + address = build(:address) + + request = get(conn, "/api/v2/smart-contracts/#{address.hash}") + + assert %{"message" => "Not found"} = json_response(request, 404) + end + + test "get 422 on invalid address", %{conn: conn} do + request = get(conn, "/api/v2/smart-contracts/0x") + + assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + end + + test "get unverified smart-contract info", %{conn: conn} do + address = insert(:contract_address) + + TestHelper.get_eip1967_implementation_error_response() + + request = get(conn, "/api/v2/smart-contracts/#{Address.checksum(address.hash)}") + response = json_response(request, 200) + + assert response == + %{ + "proxy_type" => nil, + "implementations" => [], + "is_self_destructed" => false, + "deployed_bytecode" => to_string(address.contract_code), + "creation_bytecode" => nil, + "status" => "success" + } + + insert(:transaction, + created_contract_address_hash: address.hash, + input: + "0x608060405234801561001057600080fd5b5060df8061001f6000396000f3006080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c146078575b600080fd5b348015605957600080fd5b5060766004803603810190808035906020019092919050505060a0565b005b348015608357600080fd5b50608a60aa565b6040518082815260200191505060405180910390f35b8060008190555050565b600080549050905600a165627a7a7230582061b7676067d537e410bb704932a9984739a959416170ea17bda192ac1218d2790029" + ) + |> with_block(status: :ok) + + TestHelper.get_eip1967_implementation_error_response() + + request = get(conn, "/api/v2/smart-contracts/#{Address.checksum(address.hash)}") + response = json_response(request, 200) + + assert response == + %{ + "proxy_type" => nil, + "implementations" => [], + "is_self_destructed" => false, + "deployed_bytecode" => to_string(address.contract_code), + "creation_bytecode" => + "0x608060405234801561001057600080fd5b5060df8061001f6000396000f3006080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c146078575b600080fd5b348015605957600080fd5b5060766004803603810190808035906020019092919050505060a0565b005b348015608357600080fd5b50608a60aa565b6040518082815260200191505060405180910390f35b8060008190555050565b600080549050905600a165627a7a7230582061b7676067d537e410bb704932a9984739a959416170ea17bda192ac1218d2790029", + "status" => "success" + } + end + + test "get an eip1967 proxy contract", %{conn: conn} do + implementation_address = insert(:contract_address) + proxy_address = insert(:contract_address) + + _proxy_smart_contract = + insert(:smart_contract, + address_hash: proxy_address.hash, + contract_code_md5: "123" + ) + + implementation = + insert(:proxy_implementation, + proxy_address_hash: proxy_address.hash, + proxy_type: "eip1967", + address_hashes: [implementation_address.hash], + names: [nil] + ) + + assert implementation.proxy_type == :eip1967 + + request = get(conn, "/api/v2/smart-contracts/#{Address.checksum(proxy_address.hash)}") + response = json_response(request, 200) + end + + test "get smart-contract", %{conn: conn} do + lib_address = build(:address) + lib_address_string = to_string(lib_address) + + target_contract = + insert(:smart_contract, + external_libraries: [%{name: "ABC", address_hash: lib_address_string}], + constructor_arguments: + "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000002cf6e7c9ec35d0b08a1062e13854f74b1aaae54e" + ) + + insert(:transaction, + created_contract_address_hash: target_contract.address_hash, + input: + "0x608060405234801561001057600080fd5b5060df8061001f6000396000f3006080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c146078575b600080fd5b348015605957600080fd5b5060766004803603810190808035906020019092919050505060a0565b005b348015608357600080fd5b50608a60aa565b6040518082815260200191505060405180910390f35b8060008190555050565b600080549050905600a165627a7a7230582061b7676067d537e410bb704932a9984739a959416170ea17bda192ac1218d2790029" + ) + |> with_block(status: :ok) + + implementation_address = insert(:address) + implementation_address_hash_string = to_string(implementation_address.hash) + formatted_implementation_address_hash_string = to_string(Address.checksum(implementation_address.hash)) + + correct_response = %{ + "verified_twin_address_hash" => nil, + "is_verified" => true, + "is_changed_bytecode" => false, + "is_partially_verified" => target_contract.partially_verified, + "is_fully_verified" => true, + "is_verified_via_sourcify" => target_contract.verified_via_sourcify, + "minimal_proxy_address_hash" => nil, + "sourcify_repo_url" => + if(target_contract.verified_via_sourcify, + do: AddressContractView.sourcify_repo_url(target_contract.address_hash, target_contract.partially_verified) + ), + "can_be_visualized_via_sol2uml" => false, + "name" => target_contract && target_contract.name, + "compiler_version" => target_contract.compiler_version, + "optimization_enabled" => target_contract.optimization, + "optimization_runs" => target_contract.optimization_runs, + "evm_version" => target_contract.evm_version, + "verified_at" => target_contract.inserted_at |> to_string() |> String.replace(" ", "T"), + "source_code" => target_contract.contract_source_code, + "file_path" => target_contract.file_path, + "additional_sources" => [], + "compiler_settings" => target_contract.compiler_settings, + "external_libraries" => [%{"name" => "ABC", "address_hash" => Address.checksum(lib_address)}], + "constructor_args" => target_contract.constructor_arguments, + "decoded_constructor_args" => nil, + "is_self_destructed" => false, + "deployed_bytecode" => + "0x6080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c146078575b600080fd5b348015605957600080fd5b5060766004803603810190808035906020019092919050505060a0565b005b348015608357600080fd5b50608a60aa565b6040518082815260200191505060405180910390f35b8060008190555050565b600080549050905600a165627a7a7230582061b7676067d537e410bb704932a9984739a959416170ea17bda192ac1218d2790029", + "creation_bytecode" => + "0x608060405234801561001057600080fd5b5060df8061001f6000396000f3006080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c146078575b600080fd5b348015605957600080fd5b5060766004803603810190808035906020019092919050505060a0565b005b348015608357600080fd5b50608a60aa565b6040518082815260200191505060405180910390f35b8060008190555050565b600080549050905600a165627a7a7230582061b7676067d537e410bb704932a9984739a959416170ea17bda192ac1218d2790029", + "abi" => target_contract.abi, + "proxy_type" => "eip1967", + "implementations" => [ + %{ + "address_hash" => formatted_implementation_address_hash_string, + "address" => formatted_implementation_address_hash_string, + "name" => nil + } + ], + "is_verified_via_eth_bytecode_db" => target_contract.verified_via_eth_bytecode_db, + "is_verified_via_verifier_alliance" => target_contract.verified_via_verifier_alliance, + "language" => target_contract |> SmartContract.language() |> to_string(), + "license_type" => "none", + "certified" => false, + "is_blueprint" => false, + "status" => "success" + } + + TestHelper.get_eip1967_implementation_non_zero_address(implementation_address_hash_string) + + request = get(conn, "/api/v2/smart-contracts/#{Address.checksum(target_contract.address_hash)}") + response = json_response(request, 200) + + result_props = correct_response |> Map.keys() + + for prop <- result_props do + assert prepare_implementation(correct_response[prop]) == response[prop] + end + end + + test "get smart-contract with decoded constructor", %{conn: conn} do + lib_address = build(:address) + lib_address_string = to_string(lib_address) + + target_contract = + insert(:smart_contract, + external_libraries: [%{name: "ABC", address_hash: lib_address_string}], + constructor_arguments: + "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000002cf6e7c9ec35d0b08a1062e13854f74b1aaae54e", + abi: [ + %{ + "type" => "constructor", + "inputs" => [ + %{"type" => "address", "name" => "_proxyStorage"}, + %{"type" => "address", "name" => "_implementationAddress"} + ] + }, + %{ + "constant" => false, + "inputs" => [%{"name" => "x", "type" => "uint256"}], + "name" => "set", + "outputs" => [], + "payable" => false, + "stateMutability" => "nonpayable", + "type" => "function" + }, + %{ + "constant" => true, + "inputs" => [], + "name" => "get", + "outputs" => [%{"name" => "", "type" => "uint256"}], + "payable" => false, + "stateMutability" => "view", + "type" => "function" + } + ], + license_type: 13 + ) + + insert(:transaction, + created_contract_address_hash: target_contract.address_hash, + input: + "0x608060405234801561001057600080fd5b5060df8061001f6000396000f3006080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c146078575b600080fd5b348015605957600080fd5b5060766004803603810190808035906020019092919050505060a0565b005b348015608357600080fd5b50608a60aa565b6040518082815260200191505060405180910390f35b8060008190555050565b600080549050905600a165627a7a7230582061b7676067d537e410bb704932a9984739a959416170ea17bda192ac1218d2790029" + ) + |> with_block(status: :ok) + + correct_response = %{ + "verified_twin_address_hash" => nil, + "is_verified" => true, + "is_changed_bytecode" => false, + "is_partially_verified" => target_contract.partially_verified, + "is_fully_verified" => true, + "is_verified_via_sourcify" => target_contract.verified_via_sourcify, + "minimal_proxy_address_hash" => nil, + "sourcify_repo_url" => + if(target_contract.verified_via_sourcify, + do: AddressContractView.sourcify_repo_url(target_contract.address_hash, target_contract.partially_verified) + ), + "can_be_visualized_via_sol2uml" => false, + "name" => target_contract && target_contract.name, + "compiler_version" => target_contract.compiler_version, + "optimization_enabled" => target_contract.optimization, + "optimization_runs" => target_contract.optimization_runs, + "evm_version" => target_contract.evm_version, + "verified_at" => target_contract.inserted_at |> to_string() |> String.replace(" ", "T"), + "source_code" => target_contract.contract_source_code, + "file_path" => target_contract.file_path, + "additional_sources" => [], + "compiler_settings" => target_contract.compiler_settings, + "external_libraries" => [%{"name" => "ABC", "address_hash" => Address.checksum(lib_address)}], + "constructor_args" => target_contract.constructor_arguments, + "decoded_constructor_args" => [ + ["0x0000000000000000000000000000000000000000", %{"name" => "_proxyStorage", "type" => "address"}], + [ + Address.checksum("0x2Cf6E7c9eC35D0B08A1062e13854f74b1aaae54e"), + %{"name" => "_implementationAddress", "type" => "address"} + ] + ], + "is_self_destructed" => false, + "deployed_bytecode" => + "0x6080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c146078575b600080fd5b348015605957600080fd5b5060766004803603810190808035906020019092919050505060a0565b005b348015608357600080fd5b50608a60aa565b6040518082815260200191505060405180910390f35b8060008190555050565b600080549050905600a165627a7a7230582061b7676067d537e410bb704932a9984739a959416170ea17bda192ac1218d2790029", + "creation_bytecode" => + "0x608060405234801561001057600080fd5b5060df8061001f6000396000f3006080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c146078575b600080fd5b348015605957600080fd5b5060766004803603810190808035906020019092919050505060a0565b005b348015608357600080fd5b50608a60aa565b6040518082815260200191505060405180910390f35b8060008190555050565b600080549050905600a165627a7a7230582061b7676067d537e410bb704932a9984739a959416170ea17bda192ac1218d2790029", + "abi" => target_contract.abi, + "proxy_type" => nil, + "implementations" => [], + "is_verified_via_eth_bytecode_db" => target_contract.verified_via_eth_bytecode_db, + "is_verified_via_verifier_alliance" => target_contract.verified_via_verifier_alliance, + "language" => target_contract |> SmartContract.language() |> to_string(), + "license_type" => "gnu_agpl_v3", + "certified" => false, + "is_blueprint" => false + } + + TestHelper.get_eip1967_implementation_error_response() + + request = get(conn, "/api/v2/smart-contracts/#{Address.checksum(target_contract.address_hash)}") + response = json_response(request, 200) + + result_props = correct_response |> Map.keys() + + for prop <- result_props do + assert correct_response[prop] == response[prop] + end + end + + test "get smart-contract data from bytecode twin without constructor args", %{conn: conn} do + lib_address = build(:address) + lib_address_string = to_string(lib_address) + + target_contract = + insert(:smart_contract, + external_libraries: [%{name: "ABC", address_hash: lib_address_string}], + constructor_arguments: + "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000002cf6e7c9ec35d0b08a1062e13854f74b1aaae54e", + abi: [ + %{ + "type" => "constructor", + "inputs" => [ + %{"type" => "address", "name" => "_proxyStorage"}, + %{"type" => "address", "name" => "_implementationAddress"} + ] + }, + %{ + "constant" => false, + "inputs" => [%{"name" => "x", "type" => "uint256"}], + "name" => "set", + "outputs" => [], + "payable" => false, + "stateMutability" => "nonpayable", + "type" => "function" + }, + %{ + "constant" => true, + "inputs" => [], + "name" => "get", + "outputs" => [%{"name" => "", "type" => "uint256"}], + "payable" => false, + "stateMutability" => "view", + "type" => "function" + } + ] + ) + + insert(:transaction, + created_contract_address_hash: target_contract.address_hash, + input: + "0x608060405234801561001057600080fd5b5060df8061001f6000396000f3006080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c146078575b600080fd5b348015605957600080fd5b5060766004803603810190808035906020019092919050505060a0565b005b348015608357600080fd5b50608a60aa565b6040518082815260200191505060405180910390f35b8060008190555050565b600080549050905600a165627a7a7230582061b7676067d537e410bb704932a9984739a959416170ea17bda192ac1218d2790029" + ) + |> with_block(status: :ok) + + address = insert(:contract_address) + + insert(:transaction, + created_contract_address_hash: address.hash, + input: + "0x608060405234801561001057600080fd5b5060df8061001f6000396000f3006080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c146078575b600080fd5b348015605957600080fd5b5060766004803603810190808035906020019092919050505060a0565b005b348015608357600080fd5b50608a60aa565b6040518082815260200191505060405180910390f35b8060008190555050565b600080549050905600a165627a7a7230582061b7676067d537e410bb704932a9984739a959416170ea17bda192ac1218d2790029" + ) + |> with_block(status: :ok) + + correct_response = %{ + "verified_twin_address_hash" => Address.checksum(target_contract.address_hash), + "is_verified" => false, + "is_changed_bytecode" => false, + "is_partially_verified" => false, + "is_fully_verified" => false, + "is_verified_via_sourcify" => false, + "minimal_proxy_address_hash" => nil, + "sourcify_repo_url" => nil, + "can_be_visualized_via_sol2uml" => false, + "name" => target_contract && target_contract.name, + "compiler_version" => target_contract.compiler_version, + "optimization_enabled" => target_contract.optimization, + "optimization_runs" => target_contract.optimization_runs, + "evm_version" => target_contract.evm_version, + "verified_at" => target_contract.inserted_at |> to_string() |> String.replace(" ", "T"), + "source_code" => target_contract.contract_source_code, + "file_path" => target_contract.file_path, + "additional_sources" => [], + "compiler_settings" => target_contract.compiler_settings, + "external_libraries" => [%{"name" => "ABC", "address_hash" => Address.checksum(lib_address)}], + "constructor_args" => nil, + "decoded_constructor_args" => nil, + "is_self_destructed" => false, + "deployed_bytecode" => + "0x6080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c146078575b600080fd5b348015605957600080fd5b5060766004803603810190808035906020019092919050505060a0565b005b348015608357600080fd5b50608a60aa565b6040518082815260200191505060405180910390f35b8060008190555050565b600080549050905600a165627a7a7230582061b7676067d537e410bb704932a9984739a959416170ea17bda192ac1218d2790029", + "creation_bytecode" => + "0x608060405234801561001057600080fd5b5060df8061001f6000396000f3006080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c146078575b600080fd5b348015605957600080fd5b5060766004803603810190808035906020019092919050505060a0565b005b348015608357600080fd5b50608a60aa565b6040518082815260200191505060405180910390f35b8060008190555050565b600080549050905600a165627a7a7230582061b7676067d537e410bb704932a9984739a959416170ea17bda192ac1218d2790029", + "abi" => target_contract.abi, + "proxy_type" => nil, + "implementations" => [], + "is_verified_via_eth_bytecode_db" => target_contract.verified_via_eth_bytecode_db, + "is_verified_via_verifier_alliance" => target_contract.verified_via_verifier_alliance, + "language" => target_contract |> SmartContract.language() |> to_string(), + "license_type" => "none", + "certified" => false, + "is_blueprint" => false + } + + TestHelper.get_all_proxies_implementation_zero_addresses() + + request = get(conn, "/api/v2/smart-contracts/#{Address.checksum(address.hash)}") + response = json_response(request, 200) + + result_props = correct_response |> Map.keys() + + for prop <- result_props do + assert correct_response[prop] == response[prop] + end + end + + test "doesn't get smart-contract multiple additional sources from EIP-1167 implementation", %{conn: conn} do + implementation_contract = + insert(:smart_contract, + external_libraries: [], + constructor_arguments: "", + abi: [ + %{ + "type" => "constructor", + "inputs" => [ + %{"type" => "address", "name" => "_proxyStorage"}, + %{"type" => "address", "name" => "_implementationAddress"} + ] + }, + %{ + "constant" => false, + "inputs" => [%{"name" => "x", "type" => "uint256"}], + "name" => "set", + "outputs" => [], + "payable" => false, + "stateMutability" => "nonpayable", + "type" => "function" + }, + %{ + "constant" => true, + "inputs" => [], + "name" => "get", + "outputs" => [%{"name" => "", "type" => "uint256"}], + "payable" => false, + "stateMutability" => "view", + "type" => "function" + } + ], + license_type: 9 + ) + + insert(:smart_contract_additional_source, + file_name: "test1", + contract_source_code: "test2", + address_hash: implementation_contract.address_hash + ) + + insert(:smart_contract_additional_source, + file_name: "test3", + contract_source_code: "test4", + address_hash: implementation_contract.address_hash + ) + + implementation_contract_address_hash_string = + Base.encode16(implementation_contract.address_hash.bytes, case: :lower) + + proxy_transaction_input = + "0x11b804ab000000000000000000000000" <> + implementation_contract_address_hash_string <> + "000000000000000000000000000000000000000000000000000000000000006035323031313537360000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000284e159163400000000000000000000000034420c13696f4ac650b9fafe915553a1abcd7dd30000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000001c00000000000000000000000000000000000000000000000000000000000000220000000000000000000000000ff5ae9b0a7522736299d797d80b8fc6f31d61100000000000000000000000000ff5ae9b0a7522736299d797d80b8fc6f31d6110000000000000000000000000000000000000000000000000000000000000003e8000000000000000000000000000000000000000000000000000000000000000000000000000000000000000034420c13696f4ac650b9fafe915553a1abcd7dd300000000000000000000000000000000000000000000000000000000000000184f7074696d69736d2053756273637269626572204e465473000000000000000000000000000000000000000000000000000000000000000000000000000000054f504e46540000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000037697066733a2f2f516d66544e504839765651334b5952346d6b52325a6b757756424266456f5a5554545064395538666931503332752f300000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000c82bbe41f2cf04e3a8efa18f7032bdd7f6d98a81000000000000000000000000efba8a2a82ec1fb1273806174f5e28fbb917cf9500000000000000000000000000000000000000000000000000000000" + + proxy_deployed_bytecode = + "0x363d3d373d3d3d363d73" <> implementation_contract_address_hash_string <> "5af43d82803e903d91602b57fd5bf3" + + proxy_address = + insert(:contract_address, + contract_code: proxy_deployed_bytecode + ) + + insert(:transaction, + created_contract_address_hash: proxy_address.hash, + input: proxy_transaction_input + ) + |> with_block(status: :ok) + + correct_response = %{ + "is_self_destructed" => false, + "deployed_bytecode" => proxy_deployed_bytecode, + "creation_bytecode" => proxy_transaction_input, + "proxy_type" => "eip1167", + "implementations" => [ + %{ + "address_hash" => Address.checksum(implementation_contract.address_hash), + "address" => Address.checksum(implementation_contract.address_hash), + "name" => implementation_contract.name + } + ] + } + + request = get(conn, "/api/v2/smart-contracts/#{Address.checksum(proxy_address.hash)}") + response = json_response(request, 200) + + result_props = correct_response |> Map.keys() + + for prop <- result_props do + assert prepare_implementation(correct_response[prop]) == response[prop] + end + end + + test "get smart-contract which is blueprint", %{conn: conn} do + target_contract = + insert(:smart_contract, + is_blueprint: true + ) + + insert(:transaction, + created_contract_address_hash: target_contract.address_hash, + input: + "0x608060405234801561001057600080fd5b5060df8061001f6000396000f3006080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c146078575b600080fd5b348015605957600080fd5b5060766004803603810190808035906020019092919050505060a0565b005b348015608357600080fd5b50608a60aa565b6040518082815260200191505060405180910390f35b8060008190555050565b600080549050905600a165627a7a7230582061b7676067d537e410bb704932a9984739a959416170ea17bda192ac1218d2790029" + ) + |> with_block(status: :ok) + + correct_response = %{ + "verified_twin_address_hash" => nil, + "is_verified" => true, + "is_changed_bytecode" => false, + "is_partially_verified" => target_contract.partially_verified, + "is_fully_verified" => true, + "is_verified_via_sourcify" => target_contract.verified_via_sourcify, + "minimal_proxy_address_hash" => nil, + "sourcify_repo_url" => + if(target_contract.verified_via_sourcify, + do: AddressContractView.sourcify_repo_url(target_contract.address_hash, target_contract.partially_verified) + ), + "can_be_visualized_via_sol2uml" => false, + "name" => target_contract && target_contract.name, + "compiler_version" => target_contract.compiler_version, + "optimization_enabled" => target_contract.optimization, + "optimization_runs" => target_contract.optimization_runs, + "evm_version" => target_contract.evm_version, + "verified_at" => target_contract.inserted_at |> to_string() |> String.replace(" ", "T"), + "source_code" => target_contract.contract_source_code, + "file_path" => target_contract.file_path, + "additional_sources" => [], + "compiler_settings" => target_contract.compiler_settings, + "external_libraries" => target_contract.external_libraries, + "constructor_args" => target_contract.constructor_arguments, + "decoded_constructor_args" => nil, + "is_self_destructed" => false, + "deployed_bytecode" => + "0x6080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c146078575b600080fd5b348015605957600080fd5b5060766004803603810190808035906020019092919050505060a0565b005b348015608357600080fd5b50608a60aa565b6040518082815260200191505060405180910390f35b8060008190555050565b600080549050905600a165627a7a7230582061b7676067d537e410bb704932a9984739a959416170ea17bda192ac1218d2790029", + "creation_bytecode" => + "0x608060405234801561001057600080fd5b5060df8061001f6000396000f3006080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c146078575b600080fd5b348015605957600080fd5b5060766004803603810190808035906020019092919050505060a0565b005b348015608357600080fd5b50608a60aa565b6040518082815260200191505060405180910390f35b8060008190555050565b600080549050905600a165627a7a7230582061b7676067d537e410bb704932a9984739a959416170ea17bda192ac1218d2790029", + "abi" => target_contract.abi, + "proxy_type" => nil, + "implementations" => [], + "is_verified_via_eth_bytecode_db" => target_contract.verified_via_eth_bytecode_db, + "is_verified_via_verifier_alliance" => target_contract.verified_via_verifier_alliance, + "language" => target_contract |> SmartContract.language() |> to_string(), + "license_type" => "none", + "certified" => false, + "is_blueprint" => true, + "status" => "success" + } + + TestHelper.get_all_proxies_implementation_zero_addresses() + + request = get(conn, "/api/v2/smart-contracts/#{Address.checksum(target_contract.address_hash)}") + response = json_response(request, 200) + + result_props = correct_response |> Map.keys() + + for prop <- result_props do + assert correct_response[prop] == response[prop] + end + end + end + + test "doesn't get smart-contract implementation for 'Clones with immutable arguments' pattern", %{conn: conn} do + implementation_contract = + insert(:smart_contract, + external_libraries: [], + constructor_arguments: "", + abi: [ + %{ + "type" => "constructor", + "inputs" => [ + %{"type" => "address", "name" => "_proxyStorage"}, + %{"type" => "address", "name" => "_implementationAddress"} + ] + }, + %{ + "constant" => false, + "inputs" => [%{"name" => "x", "type" => "uint256"}], + "name" => "set", + "outputs" => [], + "payable" => false, + "stateMutability" => "nonpayable", + "type" => "function" + }, + %{ + "constant" => true, + "inputs" => [], + "name" => "get", + "outputs" => [%{"name" => "", "type" => "uint256"}], + "payable" => false, + "stateMutability" => "view", + "type" => "function" + } + ], + license_type: 9 + ) + + insert(:smart_contract_additional_source, + file_name: "test1", + contract_source_code: "test2", + address_hash: implementation_contract.address_hash + ) + + implementation_contract_address_hash_string = + Base.encode16(implementation_contract.address_hash.bytes, case: :lower) + + proxy_transaction_input = + "0x684fbe55000000000000000000000000af1caf51d49b0e63d1ff7e5d4ed6ea26d15f3f9d000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000003" + + proxy_deployed_bytecode = + "0x3d3d3d3d363d3d3761003f603736393661003f013d73" <> + implementation_contract_address_hash_string <> + "5af43d3d93803e603557fd5bf3af1caf51d49b0e63d1ff7e5d4ed6ea26d15f3f9d0000000000000000000000000000000000000000000000000000000000000001000000000000000203003d" + + proxy_address = + insert(:contract_address, + contract_code: proxy_deployed_bytecode + ) + + insert(:transaction, + created_contract_address_hash: proxy_address.hash, + input: proxy_transaction_input + ) + |> with_block(status: :ok) + + formatted_implementation_address_hash_string = to_string(Address.checksum(implementation_contract.address_hash)) + + correct_response = %{ + "proxy_type" => "clone_with_immutable_arguments", + "implementations" => [ + %{ + "address_hash" => formatted_implementation_address_hash_string, + "address" => formatted_implementation_address_hash_string, + "name" => implementation_contract.name + } + ], + "is_self_destructed" => false, + "deployed_bytecode" => proxy_deployed_bytecode, + "creation_bytecode" => proxy_transaction_input + } + + request = get(conn, "/api/v2/smart-contracts/#{Address.checksum(proxy_address.hash)}") + response = json_response(request, 200) + + result_props = correct_response |> Map.keys() + + for prop <- result_props do + assert prepare_implementation(correct_response[prop]) == response[prop] + end + end + + if Application.compile_env(:explorer, :chain_type) !== :zksync do + describe "/smart-contracts/{address_hash} <> eth_bytecode_db" do + setup do + old_interval_env = Application.get_env(:explorer, Explorer.Chain.Fetcher.LookUpSmartContractSourcesOnDemand) + + :ok + + on_exit(fn -> + Application.put_env(:explorer, Explorer.Chain.Fetcher.LookUpSmartContractSourcesOnDemand, old_interval_env) + end) + end + + test "automatically verify contract", %{conn: conn} do + {:ok, pid} = Explorer.Chain.Fetcher.LookUpSmartContractSourcesOnDemand.start_link([]) + old_chain_id = Application.get_env(:block_scout_web, :chain_id) + + Application.put_env(:block_scout_web, :chain_id, 5) + + bypass = Bypass.open() + eth_bytecode_response = File.read!("./test/support/fixture/smart_contract/eth_bytecode_db_search_response.json") + + old_env = Application.get_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour) + + Application.put_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour, + service_url: "http://localhost:#{bypass.port}", + enabled: true, + type: "eth_bytecode_db", + eth_bytecode_db?: true + ) + + address = insert(:contract_address) + + insert(:transaction, + created_contract_address_hash: address.hash, + input: + "0x608060405234801561001057600080fd5b5060df8061001f6000396000f3006080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c146078575b600080fd5b348015605957600080fd5b5060766004803603810190808035906020019092919050505060a0565b005b348015608357600080fd5b50608a60aa565b6040518082815260200191505060405180910390f35b8060008190555050565b600080549050905600a165627a7a7230582061b7676067d537e410bb704932a9984739a959416170ea17bda192ac1218d2790029" + ) + |> with_block(status: :ok) + + topic = "addresses:#{address.hash}" + + {:ok, _reply, _socket} = + BlockScoutWeb.V2.UserSocket + |> socket("no_id", %{}) + |> subscribe_and_join(topic) + + Bypass.expect_once(bypass, "POST", "/api/v2/bytecodes/sources_search_all", fn conn -> + Conn.resp(conn, 200, eth_bytecode_response) + end) + + TestHelper.get_eip1967_implementation_error_response() + + request = get(conn, "/api/v2/smart-contracts/#{Address.checksum(address.hash)}") + + assert_receive %Phoenix.Socket.Message{ + payload: %{}, + event: "eth_bytecode_db_lookup_started", + topic: ^topic + }, + :timer.seconds(1) + + assert_receive %Phoenix.Socket.Message{ + payload: %{}, + event: "smart_contract_was_verified", + topic: ^topic + }, + :timer.seconds(1) + + response = json_response(request, 200) + + assert response == + %{ + "proxy_type" => nil, + "implementations" => [], + "is_self_destructed" => false, + "deployed_bytecode" => to_string(address.contract_code), + "creation_bytecode" => + "0x608060405234801561001057600080fd5b5060df8061001f6000396000f3006080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c146078575b600080fd5b348015605957600080fd5b5060766004803603810190808035906020019092919050505060a0565b005b348015608357600080fd5b50608a60aa565b6040518082815260200191505060405180910390f35b8060008190555050565b600080549050905600a165627a7a7230582061b7676067d537e410bb704932a9984739a959416170ea17bda192ac1218d2790029", + "status" => "success" + } + + TestHelper.get_all_proxies_implementation_zero_addresses() + + request = get(conn, "/api/v2/smart-contracts/#{Address.checksum(address.hash)}") + assert response = json_response(request, 200) + assert %{"is_verified" => true} = response + assert %{"is_verified_via_eth_bytecode_db" => true} = response + assert %{"is_partially_verified" => true} = response + assert %{"is_fully_verified" => false} = response + + Application.put_env(:block_scout_web, :chain_id, old_chain_id) + Application.put_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour, old_env) + Bypass.down(bypass) + GenServer.stop(pid) + end + + test "automatically verify contract using search-all (ethBytecodeDbSources) endpoint", %{conn: conn} do + {:ok, pid} = Explorer.Chain.Fetcher.LookUpSmartContractSourcesOnDemand.start_link([]) + old_chain_id = Application.get_env(:block_scout_web, :chain_id) + + Application.put_env(:block_scout_web, :chain_id, 5) + + bypass = Bypass.open() + + eth_bytecode_response = + File.read!("./test/support/fixture/smart_contract/eth_bytecode_db_search_all_local_sources_response.json") + + old_env = Application.get_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour) + + Application.put_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour, + service_url: "http://localhost:#{bypass.port}", + enabled: true, + type: "eth_bytecode_db", + eth_bytecode_db?: true + ) + + address = insert(:contract_address) + + insert(:transaction, + created_contract_address_hash: address.hash, + input: + "0x608060405234801561001057600080fd5b5060df8061001f6000396000f3006080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c146078575b600080fd5b348015605957600080fd5b5060766004803603810190808035906020019092919050505060a0565b005b348015608357600080fd5b50608a60aa565b6040518082815260200191505060405180910390f35b8060008190555050565b600080549050905600a165627a7a7230582061b7676067d537e410bb704932a9984739a959416170ea17bda192ac1218d2790029" + ) + |> with_block(status: :ok) + + topic = "addresses:#{address.hash}" + + {:ok, _reply, _socket} = + BlockScoutWeb.V2.UserSocket + |> socket("no_id", %{}) + |> subscribe_and_join(topic) + + Bypass.expect_once(bypass, "POST", "/api/v2/bytecodes/sources_search_all", fn conn -> + Conn.resp(conn, 200, eth_bytecode_response) + end) + + TestHelper.get_eip1967_implementation_error_response() + + request = get(conn, "/api/v2/smart-contracts/#{Address.checksum(address.hash)}") + + assert_receive %Phoenix.Socket.Message{ + payload: %{}, + event: "eth_bytecode_db_lookup_started", + topic: ^topic + }, + :timer.seconds(1) + + assert_receive %Phoenix.Socket.Message{ + payload: %{}, + event: "smart_contract_was_verified", + topic: ^topic + }, + :timer.seconds(1) + + response = json_response(request, 200) + + assert response == + %{ + "proxy_type" => nil, + "implementations" => [], + "is_self_destructed" => false, + "deployed_bytecode" => to_string(address.contract_code), + "creation_bytecode" => + "0x608060405234801561001057600080fd5b5060df8061001f6000396000f3006080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c146078575b600080fd5b348015605957600080fd5b5060766004803603810190808035906020019092919050505060a0565b005b348015608357600080fd5b50608a60aa565b6040518082815260200191505060405180910390f35b8060008190555050565b600080549050905600a165627a7a7230582061b7676067d537e410bb704932a9984739a959416170ea17bda192ac1218d2790029", + "status" => "success" + } + + TestHelper.get_all_proxies_implementation_zero_addresses() + + request = get(conn, "/api/v2/smart-contracts/#{Address.checksum(address.hash)}") + assert response = json_response(request, 200) + assert %{"is_verified" => true} = response + assert %{"is_verified_via_eth_bytecode_db" => true} = response + assert %{"is_partially_verified" => true} = response + assert %{"is_fully_verified" => false} = response + + smart_contract = Jason.decode!(eth_bytecode_response)["ethBytecodeDbSources"] |> List.first() + assert response["compiler_settings"] == Jason.decode!(smart_contract["compilerSettings"]) + assert response["name"] == smart_contract["contractName"] + assert response["compiler_version"] == smart_contract["compilerVersion"] + assert response["file_path"] == smart_contract["fileName"] + assert response["constructor_args"] == smart_contract["constructorArguments"] + assert response["abi"] == Jason.decode!(smart_contract["abi"]) + + assert response["decoded_constructor_args"] == [ + [ + Address.checksum("0xc35DADB65012eC5796536bD9864eD8773aBc74C4"), + %{ + "internalType" => "address", + "name" => "_factory", + "type" => "address" + } + ], + [ + Address.checksum("0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6"), + %{ + "internalType" => "address", + "name" => "_WETH", + "type" => "address" + } + ] + ] + + assert response["source_code"] == smart_contract["sourceFiles"][smart_contract["fileName"]] + + assert response["external_libraries"] == [ + %{ + "address_hash" => Address.checksum("0x00000000D41867734BBee4C6863D9255b2b06aC1"), + "name" => "__CACHE_BREAKER__" + } + ] + + additional_sources = + for file_name <- Map.keys(smart_contract["sourceFiles"]), smart_contract["fileName"] != file_name do + %{ + "source_code" => smart_contract["sourceFiles"][file_name], + "file_path" => file_name + } + end + + assert response["additional_sources"] |> Enum.sort_by(fn x -> x["file_path"] end) == + additional_sources |> Enum.sort_by(fn x -> x["file_path"] end) + + Application.put_env(:block_scout_web, :chain_id, old_chain_id) + Application.put_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour, old_env) + Bypass.down(bypass) + GenServer.stop(pid) + end + + test "automatically verify contract using search-all (sourcifySources) endpoint", %{conn: conn} do + {:ok, pid} = Explorer.Chain.Fetcher.LookUpSmartContractSourcesOnDemand.start_link([]) + old_chain_id = Application.get_env(:block_scout_web, :chain_id) + + Application.put_env(:block_scout_web, :chain_id, 5) + + bypass = Bypass.open() + + eth_bytecode_response = + File.read!("./test/support/fixture/smart_contract/eth_bytecode_db_search_all_sourcify_sources_response.json") + + old_env = Application.get_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour) + + Application.put_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour, + service_url: "http://localhost:#{bypass.port}", + enabled: true, + type: "eth_bytecode_db", + eth_bytecode_db?: true + ) + + address = insert(:contract_address) + + insert(:transaction, + created_contract_address_hash: address.hash, + input: + "0x608060405234801561001057600080fd5b5060df8061001f6000396000f3006080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c146078575b600080fd5b348015605957600080fd5b5060766004803603810190808035906020019092919050505060a0565b005b348015608357600080fd5b50608a60aa565b6040518082815260200191505060405180910390f35b8060008190555050565b600080549050905600a165627a7a7230582061b7676067d537e410bb704932a9984739a959416170ea17bda192ac1218d2790029" + ) + |> with_block(status: :ok) + + topic = "addresses:#{address.hash}" + + {:ok, _reply, _socket} = + BlockScoutWeb.V2.UserSocket + |> socket("no_id", %{}) + |> subscribe_and_join(topic) + + Bypass.expect_once(bypass, "POST", "/api/v2/bytecodes/sources_search_all", fn conn -> + Conn.resp(conn, 200, eth_bytecode_response) + end) + + TestHelper.get_all_proxies_implementation_zero_addresses() + + request = get(conn, "/api/v2/smart-contracts/#{Address.checksum(address.hash)}") + + assert_receive %Phoenix.Socket.Message{ + payload: %{}, + event: "eth_bytecode_db_lookup_started", + topic: ^topic + }, + :timer.seconds(1) + + assert_receive %Phoenix.Socket.Message{ + payload: %{}, + event: "smart_contract_was_verified", + topic: ^topic + }, + :timer.seconds(1) + + response = json_response(request, 200) + + assert response == + %{ + "proxy_type" => "unknown", + "implementations" => [], + "is_self_destructed" => false, + "deployed_bytecode" => to_string(address.contract_code), + "creation_bytecode" => + "0x608060405234801561001057600080fd5b5060df8061001f6000396000f3006080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c146078575b600080fd5b348015605957600080fd5b5060766004803603810190808035906020019092919050505060a0565b005b348015608357600080fd5b50608a60aa565b6040518082815260200191505060405180910390f35b8060008190555050565b600080549050905600a165627a7a7230582061b7676067d537e410bb704932a9984739a959416170ea17bda192ac1218d2790029", + "status" => "success" + } + + request = get(conn, "/api/v2/smart-contracts/#{Address.checksum(address.hash)}") + assert response = json_response(request, 200) + assert %{"is_verified" => true} = response + assert %{"is_verified_via_eth_bytecode_db" => true} = response + assert %{"is_verified_via_sourcify" => true} = response + assert %{"is_partially_verified" => true} = response + assert %{"is_fully_verified" => false} = response + assert response["file_path"] == "Test.sol" + + Application.put_env(:block_scout_web, :chain_id, old_chain_id) + Application.put_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour, old_env) + Bypass.down(bypass) + GenServer.stop(pid) + end + + test "automatically verify contract using search-all (sourcifySources with libraries) endpoint", %{conn: conn} do + {:ok, pid} = Explorer.Chain.Fetcher.LookUpSmartContractSourcesOnDemand.start_link([]) + old_chain_id = Application.get_env(:block_scout_web, :chain_id) + + Application.put_env(:block_scout_web, :chain_id, 5) + + bypass = Bypass.open() + + eth_bytecode_response = + File.read!( + "./test/support/fixture/smart_contract/eth_bytecode_db_search_all_sourcify_sources_with_libs_response.json" + ) + + old_env = Application.get_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour) + + Application.put_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour, + service_url: "http://localhost:#{bypass.port}", + enabled: true, + type: "eth_bytecode_db", + eth_bytecode_db?: true + ) + + address = insert(:contract_address) + + insert(:transaction, + created_contract_address_hash: address.hash, + input: + "0x608060405234801561001057600080fd5b5060df8061001f6000396000f3006080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c146078575b600080fd5b348015605957600080fd5b5060766004803603810190808035906020019092919050505060a0565b005b348015608357600080fd5b50608a60aa565b6040518082815260200191505060405180910390f35b8060008190555050565b600080549050905600a165627a7a7230582061b7676067d537e410bb704932a9984739a959416170ea17bda192ac1218d2790029" + ) + |> with_block(status: :ok) + + topic = "addresses:#{address.hash}" + + {:ok, _reply, _socket} = + BlockScoutWeb.V2.UserSocket + |> socket("no_id", %{}) + |> subscribe_and_join(topic) + + Bypass.expect_once(bypass, "POST", "/api/v2/bytecodes/sources_search_all", fn conn -> + Conn.resp(conn, 200, eth_bytecode_response) + end) + + TestHelper.get_eip1967_implementation_error_response() + + request = get(conn, "/api/v2/smart-contracts/#{Address.checksum(address.hash)}") + + assert_receive %Phoenix.Socket.Message{ + payload: %{}, + event: "eth_bytecode_db_lookup_started", + topic: ^topic + }, + :timer.seconds(1) + + assert_receive %Phoenix.Socket.Message{ + payload: %{}, + event: "smart_contract_was_verified", + topic: ^topic + }, + :timer.seconds(1) + + response = json_response(request, 200) + + assert response == + %{ + "proxy_type" => nil, + "implementations" => [], + "is_self_destructed" => false, + "deployed_bytecode" => to_string(address.contract_code), + "creation_bytecode" => + "0x608060405234801561001057600080fd5b5060df8061001f6000396000f3006080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c146078575b600080fd5b348015605957600080fd5b5060766004803603810190808035906020019092919050505060a0565b005b348015608357600080fd5b50608a60aa565b6040518082815260200191505060405180910390f35b8060008190555050565b600080549050905600a165627a7a7230582061b7676067d537e410bb704932a9984739a959416170ea17bda192ac1218d2790029", + "status" => "success" + } + + TestHelper.get_all_proxies_implementation_zero_addresses() + + request = get(conn, "/api/v2/smart-contracts/#{Address.checksum(address.hash)}") + assert response = json_response(request, 200) + + smart_contract = Jason.decode!(eth_bytecode_response)["sourcifySources"] |> List.first() + assert %{"is_verified" => true} = response + assert %{"is_verified_via_eth_bytecode_db" => true} = response + assert %{"is_verified_via_sourcify" => true} = response + assert %{"is_partially_verified" => true} = response + assert %{"is_fully_verified" => false} = response + assert response["file_path"] == "src/zkbob/ZkBobPool.sol" + + assert response["external_libraries"] == [ + %{ + "address_hash" => Address.checksum("0x22DE6B06544Ee5Cd907813a04bcdEd149A2f49D2"), + "name" => "lib/base58-solidity/contracts/Base58.sol:Base58" + }, + %{ + "address_hash" => Address.checksum("0x019d3788F00a7087234f3844CB1ceCe1F9982B7A"), + "name" => "src/libraries/ZkAddress.sol:ZkAddress" + } + ] + + additional_sources = + for file_name <- Map.keys(smart_contract["sourceFiles"]), smart_contract["fileName"] != file_name do + %{ + "source_code" => smart_contract["sourceFiles"][file_name], + "file_path" => file_name + } + end + + assert response["additional_sources"] |> Enum.sort_by(fn x -> x["file_path"] end) == + additional_sources |> Enum.sort_by(fn x -> x["file_path"] end) + + Application.put_env(:block_scout_web, :chain_id, old_chain_id) + Application.put_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour, old_env) + Bypass.down(bypass) + GenServer.stop(pid) + end + + test "automatically verify contract using search-all (allianceSources) endpoint", %{conn: conn} do + {:ok, pid} = Explorer.Chain.Fetcher.LookUpSmartContractSourcesOnDemand.start_link([]) + old_chain_id = Application.get_env(:block_scout_web, :chain_id) + + Application.put_env(:block_scout_web, :chain_id, 5) + + bypass = Bypass.open() + + eth_bytecode_response = + File.read!("./test/support/fixture/smart_contract/eth_bytecode_db_search_all_alliance_sources_response.json") + + old_env = Application.get_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour) + + Application.put_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour, + service_url: "http://localhost:#{bypass.port}", + enabled: true, + type: "eth_bytecode_db", + eth_bytecode_db?: true + ) + + address = insert(:contract_address) + + insert(:transaction, + created_contract_address_hash: address.hash, + input: + "0x608060405234801561001057600080fd5b5060df8061001f6000396000f3006080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c146078575b600080fd5b348015605957600080fd5b5060766004803603810190808035906020019092919050505060a0565b005b348015608357600080fd5b50608a60aa565b6040518082815260200191505060405180910390f35b8060008190555050565b600080549050905600a165627a7a7230582061b7676067d537e410bb704932a9984739a959416170ea17bda192ac1218d2790029" + ) + |> with_block(status: :ok) + + topic = "addresses:#{address.hash}" + + {:ok, _reply, _socket} = + BlockScoutWeb.V2.UserSocket + |> socket("no_id", %{}) + |> subscribe_and_join(topic) + + Bypass.expect_once(bypass, "POST", "/api/v2/bytecodes/sources_search_all", fn conn -> + Conn.resp(conn, 200, eth_bytecode_response) + end) + + implementation_address = insert(:address) + implementation_address_hash_string = to_string(implementation_address.hash) + formatted_implementation_address_hash_string = to_string(Address.checksum(implementation_address.hash)) + TestHelper.get_eip1967_implementation_non_zero_address(implementation_address_hash_string) + + request = get(conn, "/api/v2/smart-contracts/#{Address.checksum(address.hash)}") + + assert_receive %Phoenix.Socket.Message{ + payload: %{}, + event: "eth_bytecode_db_lookup_started", + topic: ^topic + }, + :timer.seconds(1) + + assert_receive %Phoenix.Socket.Message{ + payload: %{}, + event: "smart_contract_was_verified", + topic: ^topic + }, + :timer.seconds(1) + + response = json_response(request, 200) + + assert response == + %{ + "proxy_type" => "eip1967", + "implementations" => [ + prepare_implementation(%{ + "address_hash" => formatted_implementation_address_hash_string, + "address" => formatted_implementation_address_hash_string, + "name" => nil + }) + ], + "is_self_destructed" => false, + "deployed_bytecode" => to_string(address.contract_code), + "creation_bytecode" => + "0x608060405234801561001057600080fd5b5060df8061001f6000396000f3006080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c146078575b600080fd5b348015605957600080fd5b5060766004803603810190808035906020019092919050505060a0565b005b348015608357600080fd5b50608a60aa565b6040518082815260200191505060405180910390f35b8060008190555050565b600080549050905600a165627a7a7230582061b7676067d537e410bb704932a9984739a959416170ea17bda192ac1218d2790029", + "status" => "success" + } + + request = get(conn, "/api/v2/smart-contracts/#{Address.checksum(address.hash)}") + assert response = json_response(request, 200) + assert %{"proxy_type" => "eip1967"} = response + + assert %{ + "implementations" => [ + %{ + "address_hash" => ^formatted_implementation_address_hash_string, + "address" => ^formatted_implementation_address_hash_string, + "name" => nil + } + ] + } = + response + + assert %{"is_verified" => true} = response + assert %{"is_verified_via_eth_bytecode_db" => true} = response + assert %{"is_partially_verified" => true} = response + assert %{"is_verified_via_sourcify" => false} = response + assert %{"is_verified_via_verifier_alliance" => true} = response + assert %{"is_fully_verified" => false} = response + + smart_contract = Jason.decode!(eth_bytecode_response)["allianceSources"] |> List.first() + assert response["compiler_settings"] == Jason.decode!(smart_contract["compilerSettings"]) + assert response["name"] == smart_contract["contractName"] + assert response["compiler_version"] == smart_contract["compilerVersion"] + assert response["file_path"] == smart_contract["fileName"] + assert response["constructor_args"] == smart_contract["constructorArguments"] + assert response["abi"] == Jason.decode!(smart_contract["abi"]) + + assert response["source_code"] == smart_contract["sourceFiles"][smart_contract["fileName"]] + + assert response["external_libraries"] == [ + %{ + "address_hash" => Address.checksum("0x00000000D41867734BBee4C6863D9255b2b06aC1"), + "name" => "__CACHE_BREAKER__" + } + ] + + additional_sources = + for file_name <- Map.keys(smart_contract["sourceFiles"]), smart_contract["fileName"] != file_name do + %{ + "source_code" => smart_contract["sourceFiles"][file_name], + "file_path" => file_name + } + end + + assert response["additional_sources"] |> Enum.sort_by(fn x -> x["file_path"] end) == + additional_sources |> Enum.sort_by(fn x -> x["file_path"] end) + + Application.put_env(:block_scout_web, :chain_id, old_chain_id) + Application.put_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour, old_env) + Bypass.down(bypass) + GenServer.stop(pid) + end + + test "automatically verify contract using search-all (prefer sourcify FULL match) endpoint", %{conn: conn} do + {:ok, pid} = Explorer.Chain.Fetcher.LookUpSmartContractSourcesOnDemand.start_link([]) + old_chain_id = Application.get_env(:block_scout_web, :chain_id) + + Application.put_env(:block_scout_web, :chain_id, 5) + + bypass = Bypass.open() + + eth_bytecode_response = + File.read!( + "./test/support/fixture/smart_contract/eth_bytecode_db_search_all_alliance_sources_partial_response.json" + ) + + old_env = Application.get_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour) + + Application.put_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour, + service_url: "http://localhost:#{bypass.port}", + enabled: true, + type: "eth_bytecode_db", + eth_bytecode_db?: true + ) + + address = insert(:contract_address) + + insert(:transaction, + created_contract_address_hash: address.hash, + input: + "0x608060405234801561001057600080fd5b5060df8061001f6000396000f3006080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c146078575b600080fd5b348015605957600080fd5b5060766004803603810190808035906020019092919050505060a0565b005b348015608357600080fd5b50608a60aa565b6040518082815260200191505060405180910390f35b8060008190555050565b600080549050905600a165627a7a7230582061b7676067d537e410bb704932a9984739a959416170ea17bda192ac1218d2790029" + ) + |> with_block(status: :ok) + + topic = "addresses:#{address.hash}" + + {:ok, _reply, _socket} = + BlockScoutWeb.V2.UserSocket + |> socket("no_id", %{}) + |> subscribe_and_join(topic) + + Bypass.expect_once(bypass, "POST", "/api/v2/bytecodes/sources_search_all", fn conn -> + Conn.resp(conn, 200, eth_bytecode_response) + end) + + implementation_address = insert(:address) + implementation_address_hash_string = to_string(implementation_address.hash) + formatted_implementation_address_hash_string = to_string(Address.checksum(implementation_address.hash)) + TestHelper.get_eip1967_implementation_non_zero_address(implementation_address_hash_string) + + request = get(conn, "/api/v2/smart-contracts/#{Address.checksum(address.hash)}") + + assert_receive %Phoenix.Socket.Message{ + payload: %{}, + event: "eth_bytecode_db_lookup_started", + topic: ^topic + }, + :timer.seconds(1) + + assert_receive %Phoenix.Socket.Message{ + payload: %{}, + event: "smart_contract_was_verified", + topic: ^topic + }, + :timer.seconds(1) + + response = json_response(request, 200) + + assert response == + %{ + "proxy_type" => "eip1967", + "implementations" => [ + prepare_implementation(%{ + "address_hash" => formatted_implementation_address_hash_string, + "address" => formatted_implementation_address_hash_string, + "name" => nil + }) + ], + "is_self_destructed" => false, + "deployed_bytecode" => to_string(address.contract_code), + "creation_bytecode" => + "0x608060405234801561001057600080fd5b5060df8061001f6000396000f3006080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c146078575b600080fd5b348015605957600080fd5b5060766004803603810190808035906020019092919050505060a0565b005b348015608357600080fd5b50608a60aa565b6040518082815260200191505060405180910390f35b8060008190555050565b600080549050905600a165627a7a7230582061b7676067d537e410bb704932a9984739a959416170ea17bda192ac1218d2790029", + "status" => "success" + } + + request = get(conn, "/api/v2/smart-contracts/#{Address.checksum(address.hash)}") + assert response = json_response(request, 200) + assert %{"proxy_type" => "eip1967"} = response + + assert %{ + "implementations" => [ + %{ + "address_hash" => ^formatted_implementation_address_hash_string, + "address" => ^formatted_implementation_address_hash_string, + "name" => nil + } + ] + } = + response + + assert %{"is_verified" => true} = response + assert %{"is_verified_via_eth_bytecode_db" => true} = response + assert %{"is_partially_verified" => false} = response + assert %{"is_verified_via_sourcify" => true} = response + assert %{"is_verified_via_verifier_alliance" => false} = response + assert %{"is_fully_verified" => true} = response + + smart_contract = Jason.decode!(eth_bytecode_response)["sourcifySources"] |> List.first() + assert response["compiler_settings"] == Jason.decode!(smart_contract["compilerSettings"]) + assert response["name"] == smart_contract["contractName"] + assert response["compiler_version"] == smart_contract["compilerVersion"] + assert response["file_path"] == smart_contract["fileName"] + assert response["constructor_args"] == smart_contract["constructorArguments"] + assert response["abi"] == Jason.decode!(smart_contract["abi"]) + + assert response["source_code"] == smart_contract["sourceFiles"][smart_contract["fileName"]] + + assert response["external_libraries"] == [ + %{ + "address_hash" => Address.checksum("0x00000000D41867734BBee4C6863D9255b2b06aC1"), + "name" => "__CACHE_BREAKER__" + } + ] + + additional_sources = + for file_name <- Map.keys(smart_contract["sourceFiles"]), smart_contract["fileName"] != file_name do + %{ + "source_code" => smart_contract["sourceFiles"][file_name], + "file_path" => file_name + } + end + + assert response["additional_sources"] |> Enum.sort_by(fn x -> x["file_path"] end) == + additional_sources |> Enum.sort_by(fn x -> x["file_path"] end) + + Application.put_env(:block_scout_web, :chain_id, old_chain_id) + Application.put_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour, old_env) + Bypass.down(bypass) + GenServer.stop(pid) + end + + test "automatically verify contract using search-all (take eth bytecode db FULL match) endpoint", %{conn: conn} do + {:ok, pid} = Explorer.Chain.Fetcher.LookUpSmartContractSourcesOnDemand.start_link([]) + old_chain_id = Application.get_env(:block_scout_web, :chain_id) + + Application.put_env(:block_scout_web, :chain_id, 5) + + bypass = Bypass.open() + + eth_bytecode_response = + File.read!( + "./test/support/fixture/smart_contract/eth_bytecode_db_search_all_alliance_sources_partial_response_eth_bdb_full.json" + ) + + old_env = Application.get_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour) + + Application.put_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour, + service_url: "http://localhost:#{bypass.port}", + enabled: true, + type: "eth_bytecode_db", + eth_bytecode_db?: true + ) + + address = insert(:contract_address) + + insert(:transaction, + created_contract_address_hash: address.hash, + input: + "0x608060405234801561001057600080fd5b5060df8061001f6000396000f3006080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c146078575b600080fd5b348015605957600080fd5b5060766004803603810190808035906020019092919050505060a0565b005b348015608357600080fd5b50608a60aa565b6040518082815260200191505060405180910390f35b8060008190555050565b600080549050905600a165627a7a7230582061b7676067d537e410bb704932a9984739a959416170ea17bda192ac1218d2790029" + ) + |> with_block(status: :ok) + + topic = "addresses:#{address.hash}" + + {:ok, _reply, _socket} = + BlockScoutWeb.V2.UserSocket + |> socket("no_id", %{}) + |> subscribe_and_join(topic) + + Bypass.expect_once(bypass, "POST", "/api/v2/bytecodes/sources_search_all", fn conn -> + Conn.resp(conn, 200, eth_bytecode_response) + end) + + implementation_address = insert(:address) + implementation_address_hash_string = to_string(implementation_address.hash) + formatted_implementation_address_hash_string = to_string(Address.checksum(implementation_address.hash)) + TestHelper.get_eip1967_implementation_non_zero_address(implementation_address_hash_string) + + request = get(conn, "/api/v2/smart-contracts/#{Address.checksum(address.hash)}") + + assert_receive %Phoenix.Socket.Message{ + payload: %{}, + event: "eth_bytecode_db_lookup_started", + topic: ^topic + }, + :timer.seconds(1) + + assert_receive %Phoenix.Socket.Message{ + payload: %{}, + event: "smart_contract_was_verified", + topic: ^topic + }, + :timer.seconds(1) + + response = json_response(request, 200) + + assert response == + %{ + "proxy_type" => "eip1967", + "implementations" => [ + prepare_implementation(%{ + "address_hash" => formatted_implementation_address_hash_string, + "address" => formatted_implementation_address_hash_string, + "name" => nil + }) + ], + "is_self_destructed" => false, + "deployed_bytecode" => to_string(address.contract_code), + "creation_bytecode" => + "0x608060405234801561001057600080fd5b5060df8061001f6000396000f3006080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c146078575b600080fd5b348015605957600080fd5b5060766004803603810190808035906020019092919050505060a0565b005b348015608357600080fd5b50608a60aa565b6040518082815260200191505060405180910390f35b8060008190555050565b600080549050905600a165627a7a7230582061b7676067d537e410bb704932a9984739a959416170ea17bda192ac1218d2790029", + "status" => "success" + } + + request = get(conn, "/api/v2/smart-contracts/#{Address.checksum(address.hash)}") + assert response = json_response(request, 200) + assert %{"proxy_type" => "eip1967"} = response + + assert %{ + "implementations" => [ + %{ + "address_hash" => ^formatted_implementation_address_hash_string, + "address" => ^formatted_implementation_address_hash_string, + "name" => nil + } + ] + } = + response + + assert %{"is_verified" => true} = response + assert %{"is_verified_via_eth_bytecode_db" => true} = response + assert %{"is_partially_verified" => false} = response + assert %{"is_verified_via_sourcify" => false} = response + assert %{"is_verified_via_verifier_alliance" => false} = response + assert %{"is_fully_verified" => true} = response + + smart_contract = Jason.decode!(eth_bytecode_response)["ethBytecodeDbSources"] |> List.first() + assert response["compiler_settings"] == Jason.decode!(smart_contract["compilerSettings"]) + assert response["name"] == smart_contract["contractName"] + assert response["compiler_version"] == smart_contract["compilerVersion"] + assert response["file_path"] == smart_contract["fileName"] + assert response["constructor_args"] == smart_contract["constructorArguments"] + assert response["abi"] == Jason.decode!(smart_contract["abi"]) + + assert response["source_code"] == smart_contract["sourceFiles"][smart_contract["fileName"]] + + assert response["external_libraries"] == [ + %{ + "address_hash" => Address.checksum("0x00000000D41867734BBee4C6863D9255b2b06aC1"), + "name" => "__CACHE_BREAKER__" + } + ] + + additional_sources = + for file_name <- Map.keys(smart_contract["sourceFiles"]), smart_contract["fileName"] != file_name do + %{ + "source_code" => smart_contract["sourceFiles"][file_name], + "file_path" => file_name + } + end + + assert response["additional_sources"] |> Enum.sort_by(fn x -> x["file_path"] end) == + additional_sources |> Enum.sort_by(fn x -> x["file_path"] end) + + Application.put_env(:block_scout_web, :chain_id, old_chain_id) + Application.put_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour, old_env) + Bypass.down(bypass) + GenServer.stop(pid) + end + + test "check fetch interval for LookUpSmartContractSourcesOnDemand and use sources:search endpoint since chain_id is unset", + %{conn: conn} do + {:ok, pid} = Explorer.Chain.Fetcher.LookUpSmartContractSourcesOnDemand.start_link([]) + old_chain_id = Application.get_env(:block_scout_web, :chain_id) + + Application.put_env(:block_scout_web, :chain_id, nil) + + bypass = Bypass.open() + address = insert(:contract_address) + topic = "addresses:#{address.hash}" + + {:ok, _reply, _socket} = + BlockScoutWeb.V2.UserSocket + |> socket("no_id", %{}) + |> subscribe_and_join(topic) + + insert(:transaction, + created_contract_address_hash: address.hash, + input: + "0x608060405234801561001057600080fd5b5060df8061001f6000396000f3006080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c146078575b600080fd5b348015605957600080fd5b5060766004803603810190808035906020019092919050505060a0565b005b348015608357600080fd5b50608a60aa565b6040518082815260200191505060405180910390f35b8060008190555050565b600080549050905600a165627a7a7230582061b7676067d537e410bb704932a9984739a959416170ea17bda192ac1218d2790029" + ) + |> with_block(status: :ok) + + old_env = Application.get_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour) + + Application.put_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour, + service_url: "http://localhost:#{bypass.port}", + enabled: true, + type: "eth_bytecode_db", + eth_bytecode_db?: true + ) + + old_interval_env = Application.get_env(:explorer, Explorer.Chain.Fetcher.LookUpSmartContractSourcesOnDemand) + + Application.put_env(:explorer, Explorer.Chain.Fetcher.LookUpSmartContractSourcesOnDemand, fetch_interval: 0) + + Bypass.expect_once(bypass, "POST", "/api/v2/bytecodes/sources_search", fn conn -> + Conn.resp(conn, 200, "{\"sources\": []}") + end) + + TestHelper.get_all_proxies_implementation_zero_addresses() + + _request = get(conn, "/api/v2/smart-contracts/#{Address.checksum(address.hash)}") + + assert_receive %Phoenix.Socket.Message{ + payload: %{}, + event: "eth_bytecode_db_lookup_started", + topic: ^topic + }, + :timer.seconds(1) + + assert_receive %Phoenix.Socket.Message{ + payload: %{}, + event: "smart_contract_was_not_verified", + topic: ^topic + }, + :timer.seconds(1) + + :timer.sleep(10) + + Bypass.expect_once(bypass, "POST", "/api/v2/bytecodes/sources_search", fn conn -> + Conn.resp(conn, 200, "{\"sources\": []}") + end) + + _request = get(conn, "/api/v2/smart-contracts/#{Address.checksum(address.hash)}") + + assert_receive %Phoenix.Socket.Message{ + payload: %{}, + event: "eth_bytecode_db_lookup_started", + topic: ^topic + }, + :timer.seconds(1) + + assert_receive %Phoenix.Socket.Message{ + payload: %{}, + event: "smart_contract_was_not_verified", + topic: ^topic + }, + :timer.seconds(1) + + :timer.sleep(10) + + Bypass.expect_once(bypass, "POST", "/api/v2/bytecodes/sources_search", fn conn -> + Conn.resp(conn, 200, "{\"sources\": []}") + end) + + _request = get(conn, "/api/v2/smart-contracts/#{Address.checksum(address.hash)}") + + assert_receive %Phoenix.Socket.Message{ + payload: %{}, + event: "eth_bytecode_db_lookup_started", + topic: ^topic + }, + :timer.seconds(1) + + assert_receive %Phoenix.Socket.Message{ + payload: %{}, + event: "smart_contract_was_not_verified", + topic: ^topic + }, + :timer.seconds(1) + + :timer.sleep(10) + + Application.put_env(:explorer, Explorer.Chain.Fetcher.LookUpSmartContractSourcesOnDemand, fetch_interval: 10000) + + _request = get(conn, "/api/v2/smart-contracts/#{Address.checksum(address.hash)}") + + refute_receive %Phoenix.Socket.Message{ + payload: %{}, + event: "eth_bytecode_db_lookup_started", + topic: ^topic + }, + :timer.seconds(1) + + refute_receive %Phoenix.Socket.Message{ + payload: %{}, + event: "smart_contract_was_not_verified", + topic: ^topic + }, + :timer.seconds(1) + + refute_receive %Phoenix.Socket.Message{ + payload: %{}, + event: "smart_contract_was_verified", + topic: ^topic + }, + :timer.seconds(1) + + Application.put_env(:block_scout_web, :chain_id, old_chain_id) + Application.put_env(:explorer, Explorer.Chain.Fetcher.LookUpSmartContractSourcesOnDemand, old_interval_env) + Application.put_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour, old_env) + Bypass.down(bypass) + GenServer.stop(pid) + end + end + end + + for {state_name, migrations_finished?} <- [ + {"completed migrations", true}, + {"migrations in progress", false} + ] do + describe "/smart-contracts" <> " (with #{state_name})" do + setup do + :meck.expect( + Explorer.Chain.SmartContract, + :background_migrations_finished?, + fn -> + unquote(migrations_finished?) + end + ) + + :ok + end + + test "get [] on empty db", %{conn: conn} do + request = get(conn, "/api/v2/smart-contracts") + + assert %{"items" => [], "next_page_params" => nil} = json_response(request, 200) + end + + test "get correct smart contract", %{conn: conn} do + smart_contract = insert(:smart_contract) + request = get(conn, "/api/v2/smart-contracts") + + assert %{"items" => [sc], "next_page_params" => nil} = json_response(request, 200) + compare_item(smart_contract, sc) + assert sc["address"]["is_verified"] == true + assert sc["address"]["is_contract"] == true + end + + test "get filtered smart contracts when flag is set and language is not set", %{conn: conn} do + smart_contracts = [ + {"solidity", insert(:smart_contract, is_vyper_contract: false, language: nil)}, + {"vyper", insert(:smart_contract, is_vyper_contract: true, language: nil)}, + {"yul", insert(:smart_contract, abi: nil, is_vyper_contract: false, language: nil)} + ] + + for {filter, smart_contract} <- smart_contracts do + request = get(conn, "/api/v2/smart-contracts", %{"filter" => filter}) + + assert %{"items" => [sc], "next_page_params" => nil} = json_response(request, 200) + compare_item(smart_contract, sc) + assert sc["address"]["is_verified"] == true + assert sc["address"]["is_contract"] == true + end + end + + test "get filtered smart contracts when flag is set and language is set", %{conn: conn} do + smart_contract = insert(:smart_contract, is_vyper_contract: true, language: :vyper) + insert(:smart_contract, is_vyper_contract: false, language: :solidity) + request = get(conn, "/api/v2/smart-contracts", %{"filter" => "vyper"}) + + assert %{"items" => [sc], "next_page_params" => nil} = json_response(request, 200) + compare_item(smart_contract, sc) + assert sc["address"]["is_verified"] == true + assert sc["address"]["is_contract"] == true + end + + test "get filtered smart contracts when flag is not set and language is set", %{conn: conn} do + smart_contract = insert(:smart_contract, is_vyper_contract: nil, abi: nil, language: :yul) + insert(:smart_contract, is_vyper_contract: nil, language: :vyper) + insert(:smart_contract, is_vyper_contract: nil, language: :solidity) + request = get(conn, "/api/v2/smart-contracts", %{"filter" => "yul"}) + + assert %{"items" => [sc], "next_page_params" => nil} = json_response(request, 200) + compare_item(smart_contract, sc) + assert sc["address"]["is_verified"] == true + assert sc["address"]["is_contract"] == true + end + + if Application.compile_env(:explorer, :chain_type) == :zilliqa do + test "get filtered scilla smart contracts when language is set", %{conn: conn} do + smart_contract = insert(:smart_contract, language: :scilla, abi: nil) + insert(:smart_contract) + request = get(conn, "/api/v2/smart-contracts", %{"filter" => "scilla"}) + + assert %{"items" => [sc], "next_page_params" => nil} = json_response(request, 200) + compare_item(smart_contract, sc) + assert sc["address"]["is_verified"] == true + assert sc["address"]["is_contract"] == true + end + + test "scilla contracts are not returned when yul filter is applied", %{conn: conn} do + insert(:smart_contract, language: :scilla, abi: nil) + request = get(conn, "/api/v2/smart-contracts", %{"filter" => "yul"}) + + assert %{"items" => [], "next_page_params" => nil} = json_response(request, 200) + end + end + + test "check pagination", %{conn: conn} do + smart_contracts = + for _ <- 0..50 do + insert(:smart_contract) + end + |> Enum.sort_by(& &1.address_hash.bytes, :desc) + + request = get(conn, "/api/v2/smart-contracts") + assert response = json_response(request, 200) + + request_2nd_page = get(conn, "/api/v2/smart-contracts", response["next_page_params"]) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, smart_contracts) + end + + test "ignores wrong ordering params", %{conn: conn} do + smart_contracts = + for _ <- 0..50 do + insert(:smart_contract) + end + |> Enum.sort_by(& &1.address_hash.bytes, :desc) + + ordering_params = %{"sort" => "foo", "order" => "bar"} + + request = get(conn, "/api/v2/smart-contracts", ordering_params) + assert response = json_response(request, 200) + + request_2nd_page = + get(conn, "/api/v2/smart-contracts", ordering_params |> Map.merge(response["next_page_params"])) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, smart_contracts) + end + + test "can order by balance ascending", %{conn: conn} do + smart_contracts = + for i <- 0..50 do + address = insert(:address, fetched_coin_balance: i, verified: true) + insert(:smart_contract, address_hash: address.hash, address: address) + end + |> Enum.reverse() + + ordering_params = %{"sort" => "balance", "order" => "asc"} + + request = get(conn, "/api/v2/smart-contracts", ordering_params) + assert response = json_response(request, 200) + + request_2nd_page = + get(conn, "/api/v2/smart-contracts", ordering_params |> Map.merge(response["next_page_params"])) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, smart_contracts) + end + + test "can order by balance descending", %{conn: conn} do + smart_contracts = + for i <- 0..50 do + address = insert(:address, fetched_coin_balance: i, verified: true) + insert(:smart_contract, address_hash: address.hash, address: address) + end + + ordering_params = %{"sort" => "balance", "order" => "desc"} + + request = get(conn, "/api/v2/smart-contracts", ordering_params) + assert response = json_response(request, 200) + + request_2nd_page = + get(conn, "/api/v2/smart-contracts", ordering_params |> Map.merge(response["next_page_params"])) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, smart_contracts) + end + + test "can order by transaction count ascending", %{conn: conn} do + smart_contracts = + for i <- 0..50 do + address = insert(:address, transactions_count: i, verified: true) + insert(:smart_contract, address_hash: address.hash, address: address) + end + |> Enum.reverse() + + ordering_params = %{"sort" => "transactions_count", "order" => "asc"} + + request = get(conn, "/api/v2/smart-contracts", ordering_params) + assert response = json_response(request, 200) + + request_2nd_page = + get(conn, "/api/v2/smart-contracts", ordering_params |> Map.merge(response["next_page_params"])) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, smart_contracts) + end + + test "can order by transaction count descending", %{conn: conn} do + smart_contracts = + for i <- 0..50 do + address = insert(:address, transactions_count: i, verified: true) + insert(:smart_contract, address_hash: address.hash, address: address) + end + + ordering_params = %{"sort" => "transactions_count", "order" => "desc"} + + request = get(conn, "/api/v2/smart-contracts", ordering_params) + assert response = json_response(request, 200) + + request_2nd_page = + get(conn, "/api/v2/smart-contracts", ordering_params |> Map.merge(response["next_page_params"])) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, smart_contracts) + end + end + end + + describe "/smart-contracts/counters" do + test "fetch counters", %{conn: conn} do + request = get(conn, "/api/v2/smart-contracts/counters") + + assert %{ + "smart_contracts" => _, + "new_smart_contracts_24h" => _, + "verified_smart_contracts" => _, + "new_verified_smart_contracts_24h" => _ + } = json_response(request, 200) + end + end + + defp compare_item(%SmartContract{} = smart_contract, json) do + assert smart_contract.compiler_version == json["compiler_version"] + + assert smart_contract.optimization == json["optimization_enabled"] + + assert json["language"] == smart_contract |> SmartContract.language() |> to_string() + assert json["verified_at"] + assert !is_nil(smart_contract.constructor_arguments) == json["has_constructor_args"] + assert Address.checksum(smart_contract.address_hash) == json["address"]["hash"] + end + + defp check_paginated_response(first_page_resp, second_page_resp, list) do + assert Enum.count(first_page_resp["items"]) == 50 + assert first_page_resp["next_page_params"] != nil + compare_item(Enum.at(list, 50), Enum.at(first_page_resp["items"], 0)) + compare_item(Enum.at(list, 1), Enum.at(first_page_resp["items"], 49)) + + assert Enum.count(second_page_resp["items"]) == 1 + assert second_page_resp["next_page_params"] == nil + compare_item(Enum.at(list, 0), Enum.at(second_page_resp["items"], 0)) + end + + defp prepare_implementation(items) when is_list(items) do + Enum.map(items, &prepare_implementation/1) + end + + defp prepare_implementation(%{"address" => _, "name" => _} = implementation) do + case Application.get_env(:explorer, :chain_type) do + :filecoin -> + Map.put(implementation, "filecoin_robust_address", nil) + + _ -> + implementation + end + end + + defp prepare_implementation(other), do: other +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/api/v2/stats_controller_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/api/v2/stats_controller_test.exs new file mode 100644 index 0000000..683605e --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/api/v2/stats_controller_test.exs @@ -0,0 +1,67 @@ +defmodule BlockScoutWeb.API.V2.StatsControllerTest do + use BlockScoutWeb.ConnCase + + alias Explorer.Chain.Cache.Counters.{AddressesCount, AverageBlockTime} + + describe "/stats" do + setup do + start_supervised!(AddressesCount) + start_supervised!(AverageBlockTime) + + Application.put_env(:explorer, AverageBlockTime, enabled: true, cache_period: 1_800_000) + + on_exit(fn -> + Application.put_env(:explorer, AverageBlockTime, enabled: false, cache_period: 1_800_000) + end) + + :ok + end + + test "get all fields", %{conn: conn} do + request = get(conn, "/api/v2/stats") + assert response = json_response(request, 200) + + assert Map.has_key?(response, "total_blocks") + assert Map.has_key?(response, "total_addresses") + assert Map.has_key?(response, "total_transactions") + assert Map.has_key?(response, "average_block_time") + assert Map.has_key?(response, "coin_price") + assert Map.has_key?(response, "total_gas_used") + assert Map.has_key?(response, "transactions_today") + assert Map.has_key?(response, "gas_used_today") + assert Map.has_key?(response, "gas_prices") + assert Map.has_key?(response, "static_gas_price") + assert Map.has_key?(response, "market_cap") + assert Map.has_key?(response, "network_utilization_percentage") + end + end + + describe "/stats/charts/market" do + setup do + configuration = Application.get_env(:explorer, Explorer.Market.MarketHistoryCache) + Application.put_env(:explorer, Explorer.Market.MarketHistoryCache, cache_period: 0) + + :ok + + on_exit(fn -> + Application.put_env(:explorer, Explorer.Market.MarketHistoryCache, configuration) + end) + end + + test "get empty data", %{conn: conn} do + request = get(conn, "/api/v2/stats/charts/market") + assert response = json_response(request, 200) + + assert response["chart_data"] == [] + end + end + + describe "/stats/charts/transactions" do + test "get empty data", %{conn: conn} do + request = get(conn, "/api/v2/stats/charts/transactions") + assert response = json_response(request, 200) + + assert response["chart_data"] == [] + end + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/api/v2/token_controller_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/api/v2/token_controller_test.exs new file mode 100644 index 0000000..1757af1 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/api/v2/token_controller_test.exs @@ -0,0 +1,2224 @@ +defmodule BlockScoutWeb.API.V2.TokenControllerTest do + use EthereumJSONRPC.Case, async: false + use BlockScoutWeb.ConnCase + use BlockScoutWeb.ChannelCase, async: false + + import Mox + + alias Explorer.{Repo, TestHelper} + + alias Explorer.Chain.{Address, Token, Token.Instance, TokenTransfer} + alias Explorer.Chain.Address.CurrentTokenBalance + alias Explorer.Chain.Events.Subscriber + + alias Indexer.Fetcher.OnDemand.TokenInstanceMetadataRefetch, as: TokenInstanceMetadataRefetchOnDemand + alias Indexer.Fetcher.OnDemand.NFTCollectionMetadataRefetch, as: NFTCollectionMetadataRefetchOnDemand + + describe "/tokens/{address_hash}" do + test "get 404 on non existing address", %{conn: conn} do + token = build(:token) + + request = get(conn, "/api/v2/tokens/#{token.contract_address.hash}") + + assert %{"message" => "Not found"} = json_response(request, 404) + end + + test "get 422 on invalid address", %{conn: conn} do + request = get(conn, "/api/v2/tokens/0x") + + assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + end + + test "get token", %{conn: conn} do + token = insert(:token) + + request = get(conn, "/api/v2/tokens/#{token.contract_address.hash}") + + assert response = json_response(request, 200) + + compare_item(token, response) + end + end + + describe "/tokens/{address_hash}/counters" do + test "get 404 on non existing address", %{conn: conn} do + token = build(:token) + + request = get(conn, "/api/v2/tokens/#{token.contract_address.hash}/counters") + + assert %{"message" => "Not found"} = json_response(request, 404) + end + + test "get 422 on invalid address", %{conn: conn} do + request = get(conn, "/api/v2/tokens/0x/counters") + + assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + end + + test "get counters", %{conn: conn} do + token = insert(:token) + + request = get(conn, "/api/v2/tokens/#{token.contract_address.hash}/counters") + + assert response = json_response(request, 200) + + assert response["transfers_count"] == "0" + assert response["token_holders_count"] == "0" + end + + test "get not zero counters", %{conn: conn} do + contract_token_address = insert(:contract_address) + token = insert(:token, contract_address: contract_token_address) + + transaction = + :transaction + |> insert(to_address: contract_token_address) + |> with_block() + + insert_list( + 3, + :token_transfer, + transaction: transaction, + token_contract_address: contract_token_address + ) + + _second_page_token_balances = + 1..5 + |> Enum.map( + &insert( + :address_current_token_balance, + token_contract_address_hash: token.contract_address_hash, + value: &1 + 1000 + ) + ) + + request = get(conn, "/api/v2/tokens/#{token.contract_address.hash}/counters") + assert json_response(request, 200) + + Process.sleep(500) + request = get(conn, "/api/v2/tokens/#{token.contract_address.hash}/counters") + assert response = json_response(request, 200) + + assert response["transfers_count"] == "3" + assert response["token_holders_count"] == "5" + end + end + + describe "/tokens/{address_hash}/transfers" do + test "get 200 on non existing address", %{conn: conn} do + token = build(:token) + + request = get(conn, "/api/v2/tokens/#{token.contract_address.hash}/transfers") + + assert %{"message" => "Not found"} = json_response(request, 404) + end + + test "get 422 on invalid address", %{conn: conn} do + request = get(conn, "/api/v2/tokens/0x/transfers") + + assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + end + + test "get empty list", %{conn: conn} do + token = insert(:token) + + request = get(conn, "/api/v2/tokens/#{token.contract_address.hash}/transfers") + + assert %{"items" => [], "next_page_params" => nil} = json_response(request, 200) + end + + test "check pagination", %{conn: conn} do + token = insert(:token) + + token_transfers = + for _ <- 0..50 do + transaction = insert(:transaction, input: "0xabcd010203040506") |> with_block() + + insert(:token_transfer, + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number, + token_contract_address: token.contract_address + ) + end + + request = get(conn, "/api/v2/tokens/#{token.contract_address.hash}/transfers") + assert response = json_response(request, 200) + + request_2nd_page = + get(conn, "/api/v2/tokens/#{token.contract_address.hash}/transfers", response["next_page_params"]) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, token_transfers) + end + + test "check that same token_ids within batch squashes", %{conn: conn} do + token = insert(:token, type: "ERC-1155") + + id = 0 + + insert(:token_instance, token_id: id, token_contract_address_hash: token.contract_address_hash) + + tt = + for _ <- 0..50 do + transaction = insert(:transaction, input: "0xabcd010203040506") |> with_block() + + insert(:token_transfer, + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number, + token_contract_address: token.contract_address, + token_ids: Enum.map(0..50, fn _x -> id end), + token_type: "ERC-1155", + amounts: Enum.map(0..50, fn x -> x end) + ) + end + + token_transfers = + for i <- tt do + %TokenTransfer{i | token_ids: [id], amount: Decimal.new(1275)} + end + + request = get(conn, "/api/v2/tokens/#{token.contract_address.hash}/transfers") + assert response = json_response(request, 200) + + request_2nd_page = + get(conn, "/api/v2/tokens/#{token.contract_address.hash}/transfers", response["next_page_params"]) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, token_transfers) + end + + test "check that pagination works for 721 tokens", %{conn: conn} do + token = insert(:token, type: "ERC-721") + + token_transfers = + for i <- 0..50 do + transaction = insert(:transaction, input: "0xabcd010203040506") |> with_block() + + insert(:token_transfer, + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number, + token_contract_address: token.contract_address, + token_ids: [i], + token_type: "ERC-721" + ) + end + + request = get(conn, "/api/v2/tokens/#{token.contract_address.hash}/transfers") + assert response = json_response(request, 200) + + request_2nd_page = + get(conn, "/api/v2/tokens/#{token.contract_address.hash}/transfers", response["next_page_params"]) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, token_transfers) + end + + test "check that pagination works fine with 1155 batches #1 (large batch)", %{conn: conn} do + token = insert(:token, type: "ERC-1155") + transaction = insert(:transaction, input: "0xabcd010203040506") |> with_block() + + tt = + insert(:token_transfer, + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number, + token_contract_address: token.contract_address, + token_ids: Enum.map(0..50, fn x -> x end), + token_type: "ERC-1155", + amounts: Enum.map(0..50, fn x -> x end) + ) + + token_transfers = + for i <- 0..50 do + %TokenTransfer{tt | token_ids: [i], amount: i} + end + + request = get(conn, "/api/v2/tokens/#{token.contract_address.hash}/transfers") + assert response = json_response(request, 200) + + request_2nd_page = + get(conn, "/api/v2/tokens/#{token.contract_address.hash}/transfers", response["next_page_params"]) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, token_transfers) + end + + test "check that pagination works fine with 1155 batches #2 some batches on the first page and one on the second", + %{conn: conn} do + token = insert(:token, type: "ERC-1155") + + transaction_1 = insert(:transaction, input: "0xabcd010203040506") |> with_block() + + tt_1 = + insert(:token_transfer, + transaction: transaction_1, + block: transaction_1.block, + block_number: transaction_1.block_number, + token_contract_address: token.contract_address, + token_ids: Enum.map(0..24, fn x -> x end), + token_type: "ERC-1155", + amounts: Enum.map(0..24, fn x -> x end) + ) + + token_transfers_1 = + for i <- 0..24 do + %TokenTransfer{tt_1 | token_ids: [i], amount: i} + end + + transaction_2 = insert(:transaction, input: "0xabcd010203040506") |> with_block() + + tt_2 = + insert(:token_transfer, + transaction: transaction_2, + block: transaction_2.block, + block_number: transaction_2.block_number, + token_contract_address: token.contract_address, + token_ids: Enum.map(25..49, fn x -> x end), + token_type: "ERC-1155", + amounts: Enum.map(25..49, fn x -> x end) + ) + + token_transfers_2 = + for i <- 25..49 do + %TokenTransfer{tt_2 | token_ids: [i], amount: i} + end + + tt_3 = + insert(:token_transfer, + transaction: transaction_2, + block: transaction_2.block, + block_number: transaction_2.block_number, + token_contract_address: token.contract_address, + token_ids: [50], + token_type: "ERC-1155", + amounts: [50] + ) + + request = get(conn, "/api/v2/tokens/#{token.contract_address.hash}/transfers") + assert response = json_response(request, 200) + + request_2nd_page = + get(conn, "/api/v2/tokens/#{token.contract_address.hash}/transfers", response["next_page_params"]) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, token_transfers_1 ++ token_transfers_2 ++ [tt_3]) + end + + test "check that pagination works fine with 1155 batches #3", %{conn: conn} do + token = insert(:token, type: "ERC-1155") + + transaction_1 = insert(:transaction, input: "0xabcd010203040506") |> with_block() + + tt_1 = + insert(:token_transfer, + transaction: transaction_1, + block: transaction_1.block, + block_number: transaction_1.block_number, + token_contract_address: token.contract_address, + token_ids: Enum.map(0..24, fn x -> x end), + token_type: "ERC-1155", + amounts: Enum.map(0..24, fn x -> x end) + ) + + token_transfers_1 = + for i <- 0..24 do + %TokenTransfer{tt_1 | token_ids: [i], amount: i} + end + + transaction_2 = insert(:transaction, input: "0xabcd010203040506") |> with_block() + + tt_2 = + insert(:token_transfer, + transaction: transaction_2, + block: transaction_2.block, + block_number: transaction_2.block_number, + token_contract_address: token.contract_address, + token_ids: Enum.map(25..50, fn x -> x end), + token_type: "ERC-1155", + amounts: Enum.map(25..50, fn x -> x end) + ) + + token_transfers_2 = + for i <- 25..50 do + %TokenTransfer{tt_2 | token_ids: [i], amount: i} + end + + request = get(conn, "/api/v2/tokens/#{token.contract_address.hash}/transfers") + assert response = json_response(request, 200) + + request_2nd_page = + get(conn, "/api/v2/tokens/#{token.contract_address.hash}/transfers", response["next_page_params"]) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, token_transfers_1 ++ token_transfers_2) + end + end + + describe "/tokens/{address_hash}/holders" do + test "get 200 on non existing address", %{conn: conn} do + token = build(:token) + + request = get(conn, "/api/v2/tokens/#{token.contract_address.hash}/holders") + + assert %{"message" => "Not found"} = json_response(request, 404) + end + + test "get 422 on invalid address", %{conn: conn} do + request = get(conn, "/api/v2/tokens/0x/holders") + + assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + end + + test "get empty list", %{conn: conn} do + token = insert(:token) + + request = get(conn, "/api/v2/tokens/#{token.contract_address.hash}/holders") + + assert %{"items" => [], "next_page_params" => nil} = json_response(request, 200) + end + + test "check pagination", %{conn: conn} do + token = insert(:token) + + token_balances = + for i <- 0..50 do + insert( + :address_current_token_balance, + token_contract_address_hash: token.contract_address_hash, + value: i + 1000 + ) + end + + request = get(conn, "/api/v2/tokens/#{token.contract_address.hash}/holders") + assert response = json_response(request, 200) + + request_2nd_page = + get(conn, "/api/v2/tokens/#{token.contract_address.hash}/holders", response["next_page_params"]) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_holders_paginated_response(response, response_2nd_page, token_balances) + end + + test "check pagination with the same values", %{conn: conn} do + token = insert(:token) + + token_balances = + for _ <- 0..50 do + insert( + :address_current_token_balance, + token_contract_address_hash: token.contract_address_hash, + value: 1000 + ) + end + |> Enum.sort_by(fn x -> x.address_hash end, :asc) + + request = get(conn, "/api/v2/tokens/#{token.contract_address.hash}/holders") + assert response = json_response(request, 200) + + request_2nd_page = + get(conn, "/api/v2/tokens/#{token.contract_address.hash}/holders", response["next_page_params"]) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_holders_paginated_response(response, response_2nd_page, token_balances) + end + end + + describe "/tokens" do + defp check_tokens_pagination(tokens, conn, additional_params \\ %{}) do + request = get(conn, "/api/v2/tokens", additional_params) + assert response = json_response(request, 200) + request_2nd_page = get(conn, "/api/v2/tokens", additional_params |> Map.merge(response["next_page_params"])) + assert response_2nd_page = json_response(request_2nd_page, 200) + check_paginated_response(response, response_2nd_page, tokens) + + # by fiat_value + tokens_ordered_by_fiat_value = Enum.sort(tokens, &(Decimal.compare(&1.fiat_value, &2.fiat_value) in [:eq, :lt])) + + request_ordered_by_fiat_value = + get(conn, "/api/v2/tokens", additional_params |> Map.merge(%{"sort" => "fiat_value", "order" => "desc"})) + + assert response_ordered_by_fiat_value = json_response(request_ordered_by_fiat_value, 200) + + request_ordered_by_fiat_value_2nd_page = + get( + conn, + "/api/v2/tokens", + additional_params + |> Map.merge(%{"sort" => "fiat_value", "order" => "desc"}) + |> Map.merge(response_ordered_by_fiat_value["next_page_params"]) + ) + + assert response_ordered_by_fiat_value_2nd_page = json_response(request_ordered_by_fiat_value_2nd_page, 200) + + check_paginated_response( + response_ordered_by_fiat_value, + response_ordered_by_fiat_value_2nd_page, + tokens_ordered_by_fiat_value + ) + + tokens_ordered_by_fiat_value_asc = + Enum.sort(tokens, &(Decimal.compare(&1.fiat_value, &2.fiat_value) in [:eq, :gt])) + + request_ordered_by_fiat_value_asc = + get(conn, "/api/v2/tokens", additional_params |> Map.merge(%{"sort" => "fiat_value", "order" => "asc"})) + + assert response_ordered_by_fiat_value_asc = json_response(request_ordered_by_fiat_value_asc, 200) + + request_ordered_by_fiat_value_asc_2nd_page = + get( + conn, + "/api/v2/tokens", + additional_params + |> Map.merge(%{"sort" => "fiat_value", "order" => "asc"}) + |> Map.merge(response_ordered_by_fiat_value_asc["next_page_params"]) + ) + + assert response_ordered_by_fiat_value_asc_2nd_page = + json_response(request_ordered_by_fiat_value_asc_2nd_page, 200) + + check_paginated_response( + response_ordered_by_fiat_value_asc, + response_ordered_by_fiat_value_asc_2nd_page, + tokens_ordered_by_fiat_value_asc + ) + + # by holders + tokens_ordered_by_holders = Enum.sort(tokens, &(&1.holder_count <= &2.holder_count)) + + request_ordered_by_holders = + get(conn, "/api/v2/tokens", additional_params |> Map.merge(%{"sort" => "holder_count", "order" => "desc"})) + + assert response_ordered_by_holders = json_response(request_ordered_by_holders, 200) + + request_ordered_by_holders_2nd_page = + get( + conn, + "/api/v2/tokens", + additional_params + |> Map.merge(%{"sort" => "holder_count", "order" => "desc"}) + |> Map.merge(response_ordered_by_holders["next_page_params"]) + ) + + assert response_ordered_by_holders_2nd_page = json_response(request_ordered_by_holders_2nd_page, 200) + + check_paginated_response( + response_ordered_by_holders, + response_ordered_by_holders_2nd_page, + tokens_ordered_by_holders + ) + + tokens_ordered_by_holders_asc = Enum.sort(tokens, &(&1.holder_count >= &2.holder_count)) + + request_ordered_by_holders_asc = + get(conn, "/api/v2/tokens", additional_params |> Map.merge(%{"sort" => "holder_count", "order" => "asc"})) + + assert response_ordered_by_holders_asc = json_response(request_ordered_by_holders_asc, 200) + + request_ordered_by_holders_asc_2nd_page = + get( + conn, + "/api/v2/tokens", + additional_params + |> Map.merge(%{"sort" => "holder_count", "order" => "asc"}) + |> Map.merge(response_ordered_by_holders_asc["next_page_params"]) + ) + + assert response_ordered_by_holders_asc_2nd_page = json_response(request_ordered_by_holders_asc_2nd_page, 200) + + check_paginated_response( + response_ordered_by_holders_asc, + response_ordered_by_holders_asc_2nd_page, + tokens_ordered_by_holders_asc + ) + + :timer.sleep(200) + + # by circulating_market_cap + tokens_ordered_by_circulating_market_cap = + Enum.sort(tokens, &(&1.circulating_market_cap <= &2.circulating_market_cap)) + + request_ordered_by_circulating_market_cap = + get( + conn, + "/api/v2/tokens", + additional_params |> Map.merge(%{"sort" => "circulating_market_cap", "order" => "desc"}) + ) + + assert response_ordered_by_circulating_market_cap = json_response(request_ordered_by_circulating_market_cap, 200) + + request_ordered_by_circulating_market_cap_2nd_page = + get( + conn, + "/api/v2/tokens", + additional_params + |> Map.merge(%{"sort" => "circulating_market_cap", "order" => "desc"}) + |> Map.merge(response_ordered_by_circulating_market_cap["next_page_params"]) + ) + + assert response_ordered_by_circulating_market_cap_2nd_page = + json_response(request_ordered_by_circulating_market_cap_2nd_page, 200) + + check_paginated_response( + response_ordered_by_circulating_market_cap, + response_ordered_by_circulating_market_cap_2nd_page, + tokens_ordered_by_circulating_market_cap + ) + + tokens_ordered_by_circulating_market_cap_asc = + Enum.sort(tokens, &(&1.circulating_market_cap >= &2.circulating_market_cap)) + + request_ordered_by_circulating_market_cap_asc = + get( + conn, + "/api/v2/tokens", + additional_params |> Map.merge(%{"sort" => "circulating_market_cap", "order" => "asc"}) + ) + + assert response_ordered_by_circulating_market_cap_asc = + json_response(request_ordered_by_circulating_market_cap_asc, 200) + + request_ordered_by_circulating_market_cap_asc_2nd_page = + get( + conn, + "/api/v2/tokens", + additional_params + |> Map.merge(%{"sort" => "circulating_market_cap", "order" => "asc"}) + |> Map.merge(response_ordered_by_circulating_market_cap_asc["next_page_params"]) + ) + + assert response_ordered_by_circulating_market_cap_asc_2nd_page = + json_response(request_ordered_by_circulating_market_cap_asc_2nd_page, 200) + + check_paginated_response( + response_ordered_by_circulating_market_cap_asc, + response_ordered_by_circulating_market_cap_asc_2nd_page, + tokens_ordered_by_circulating_market_cap_asc + ) + end + + test "get empty list", %{conn: conn} do + request = get(conn, "/api/v2/tokens") + + assert %{"items" => [], "next_page_params" => nil} = json_response(request, 200) + end + + test "ignores wrong ordering params", %{conn: conn} do + tokens = + for i <- 0..50 do + insert(:token, fiat_value: i) + end + + request = get(conn, "/api/v2/tokens", %{"sort" => "foo", "order" => "bar"}) + + assert response = json_response(request, 200) + + request_2nd_page = + get(conn, "/api/v2/tokens", %{"sort" => "foo", "order" => "bar"} |> Map.merge(response["next_page_params"])) + + assert response_2nd_page = json_response(request_2nd_page, 200) + check_paginated_response(response, response_2nd_page, tokens) + end + + test "tokens are filtered by single type", %{conn: conn} do + erc_20_tokens = + for i <- 0..50 do + insert(:token, fiat_value: i) + end + + erc_721_tokens = + for _i <- 0..50 do + insert(:token, type: "ERC-721") + end + + erc_1155_tokens = + for _i <- 0..50 do + insert(:token, type: "ERC-1155") + end + + erc_404_tokens = + for _i <- 0..50 do + insert(:token, type: "ERC-404") + end + + check_tokens_pagination(erc_20_tokens, conn, %{"type" => "ERC-20"}) + check_tokens_pagination(erc_721_tokens |> Enum.reverse(), conn, %{"type" => "ERC-721"}) + check_tokens_pagination(erc_1155_tokens |> Enum.reverse(), conn, %{"type" => "ERC-1155"}) + check_tokens_pagination(erc_404_tokens |> Enum.reverse(), conn, %{"type" => "ERC-404"}) + end + + test "tokens are filtered by multiple type", %{conn: conn} do + erc_20_tokens = + for i <- 11..36 do + insert(:token, fiat_value: i) + end + + erc_721_tokens = + for _i <- 0..25 do + insert(:token, type: "ERC-721") + end + + erc_1155_tokens = + for _i <- 0..24 do + insert(:token, type: "ERC-1155") + end + + erc_404_tokens = + for _i <- 0..24 do + insert(:token, type: "ERC-404") + end + + check_tokens_pagination( + erc_721_tokens |> Kernel.++(erc_1155_tokens) |> Enum.reverse(), + conn, + %{ + "type" => "ERC-1155,ERC-721" + } + ) + + check_tokens_pagination( + erc_1155_tokens |> Enum.reverse() |> Kernel.++(erc_20_tokens), + conn, + %{ + "type" => "[erc-20,ERC-1155]" + } + ) + + check_tokens_pagination( + erc_404_tokens |> Enum.reverse() |> Kernel.++(erc_20_tokens), + conn, + %{ + "type" => "[erc-20,ERC-404]" + } + ) + end + + test "sorting by fiat_value", %{conn: conn} do + tokens = + for i <- 0..50 do + insert(:token, fiat_value: i) + end + + check_tokens_pagination(tokens, conn) + end + + # these tests that tokens paginates by each parameter separately and by any combination of them + test "pagination by address", %{conn: conn} do + tokens = + for _i <- 0..50 do + insert(:token, name: nil) + end + |> Enum.reverse() + + check_tokens_pagination(tokens, conn) + end + + test "pagination by name", %{conn: conn} do + named_token = insert(:token, holder_count: 0) + empty_named_token = insert(:token, name: "", holder_count: 0) + + tokens = + for i <- 1..49 do + insert(:token, holder_count: i) + end + + tokens = [named_token, empty_named_token | tokens] + + check_tokens_pagination(tokens, conn) + end + + test "pagination by holders", %{conn: conn} do + tokens = + for i <- 0..50 do + insert(:token, holder_count: i, name: nil) + end + + check_tokens_pagination(tokens, conn) + end + + test "pagination by circulating_market_cap", %{conn: conn} do + tokens = + for i <- 0..50 do + insert(:token, circulating_market_cap: i, name: nil) + end + + check_tokens_pagination(tokens, conn) + end + + test "pagination by name and address", %{conn: conn} do + tokens = + for _i <- 0..50 do + insert(:token) + end + |> Enum.reverse() + + check_tokens_pagination(tokens, conn) + end + + test "pagination by holders and address", %{conn: conn} do + tokens = + for _i <- 0..50 do + insert(:token, holder_count: 1, name: nil) + end + |> Enum.reverse() + + check_tokens_pagination(tokens, conn) + end + + test "pagination by circulating_market_cap and address", %{conn: conn} do + tokens = + for _i <- 0..50 do + insert(:token, circulating_market_cap: 1, name: nil) + end + |> Enum.reverse() + + check_tokens_pagination(tokens, conn) + end + + test "pagination by holders and name", %{conn: conn} do + tokens = + for i <- 1..51 do + insert(:token, holder_count: 1, name: List.to_string([i])) + end + |> Enum.reverse() + + check_tokens_pagination(tokens, conn) + end + + test "pagination by circulating_market_cap and name", %{conn: conn} do + tokens = + for i <- 1..51 do + insert(:token, circulating_market_cap: 1, name: List.to_string([i])) + end + |> Enum.reverse() + + check_tokens_pagination(tokens, conn) + end + + test "pagination by circulating_market_cap and holders", %{conn: conn} do + tokens = + for i <- 0..50 do + insert(:token, circulating_market_cap: 1, holder_count: i, name: nil) + end + + check_tokens_pagination(tokens, conn) + end + + test "pagination by holders, name and address", %{conn: conn} do + tokens = + for _i <- 0..50 do + insert(:token, holder_count: 1) + end + |> Enum.reverse() + + check_tokens_pagination(tokens, conn) + end + + test "pagination by circulating_market_cap, name and address", %{conn: conn} do + tokens = + for _i <- 0..50 do + insert(:token, circulating_market_cap: 1) + end + |> Enum.reverse() + + check_tokens_pagination(tokens, conn) + end + + test "pagination by circulating_market_cap, holders and address", %{conn: conn} do + tokens = + for _i <- 0..50 do + insert(:token, circulating_market_cap: 1, holder_count: 1, name: nil) + end + |> Enum.reverse() + + check_tokens_pagination(tokens, conn) + end + + test "pagination by circulating_market_cap, holders and name", %{conn: conn} do + tokens = + for i <- 1..51 do + insert(:token, circulating_market_cap: 1, holder_count: 1, name: List.to_string([i])) + end + |> Enum.reverse() + + check_tokens_pagination(tokens, conn) + end + + test "pagination by circulating_market_cap, holders, name and address", %{conn: conn} do + tokens = + for _i <- 0..50 do + insert(:token, holder_count: 1, circulating_market_cap: 1) + end + |> Enum.reverse() + + check_tokens_pagination(tokens, conn) + end + + test "check nil", %{conn: conn} do + token = insert(:token) + + request = get(conn, "/api/v2/tokens") + + assert %{"items" => [token_json], "next_page_params" => nil} = json_response(request, 200) + + compare_item(token, token_json) + end + end + + describe "/tokens/{address_hash}/instances" do + test "get 404 on non existing address", %{conn: conn} do + token = build(:token) + + request = get(conn, "/api/v2/tokens/#{token.contract_address.hash}/instances") + + assert %{"message" => "Not found"} = json_response(request, 404) + end + + test "get 422 on invalid address", %{conn: conn} do + request = get(conn, "/api/v2/tokens/0x/instances") + + assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + end + + test "get empty list", %{conn: conn} do + token = insert(:token) + + request = get(conn, "/api/v2/tokens/#{token.contract_address.hash}/instances") + + assert %{"items" => [], "next_page_params" => nil} = json_response(request, 200) + end + + test "get instances list", %{conn: conn} do + token = insert(:token) + + for _ <- 0..50 do + insert(:token_instance) + end + + instances = + for _ <- 0..50 do + insert(:token_instance, token_contract_address_hash: token.contract_address_hash) + end + + request = get(conn, "/api/v2/tokens/#{token.contract_address_hash}/instances") + assert response = json_response(request, 200) + + request_2nd_page = + get(conn, "/api/v2/tokens/#{token.contract_address_hash}/instances", response["next_page_params"]) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, instances) + end + + test "get instances list by holder erc-721", %{conn: conn} do + token = insert(:token, type: "ERC-721") + + insert_list(51, :token_instance, token_contract_address_hash: token.contract_address_hash) + + address = insert(:address, contract_code: Enum.random([nil, "0x010101"])) + + insert_list(51, :token_instance) + + token_instances = + for _ <- 0..50 do + insert(:token_instance, + owner_address_hash: address.hash, + token_contract_address_hash: token.contract_address_hash + ) + |> Repo.preload([:token, :owner]) + end + + filter = %{"holder_address_hash" => to_string(address.hash)} + + request = get(conn, "/api/v2/tokens/#{token.contract_address_hash}/instances", filter) + assert response = json_response(request, 200) + + request_2nd_page = + get( + conn, + "/api/v2/tokens/#{token.contract_address_hash}/instances", + Map.merge(response["next_page_params"], filter) + ) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, token_instances) + end + + test "get instances list by holder erc-1155", %{conn: conn} do + token = insert(:token, type: "ERC-1155") + + insert_list(51, :token_instance, token_contract_address_hash: token.contract_address_hash) + + address = insert(:address, contract_code: Enum.random([nil, "0x010101"])) + + insert_list(51, :token_instance) + + token_instances = + for _ <- 0..50 do + ti = + insert(:token_instance, + token_contract_address_hash: token.contract_address_hash + ) + |> Repo.preload([:token]) + + current_token_balance = + insert(:address_current_token_balance_with_token_id_and_fixed_token_type, + address: address, + token_type: "ERC-1155", + token_id: ti.token_id, + token_contract_address_hash: token.contract_address_hash, + value: Enum.random(1..2) + ) + + %Instance{ti | current_token_balance: current_token_balance, owner: address} + end + + filter = %{"holder_address_hash" => to_string(address.hash)} + + request = get(conn, "/api/v2/tokens/#{token.contract_address_hash}/instances", filter) + assert response = json_response(request, 200) + + request_2nd_page = + get( + conn, + "/api/v2/tokens/#{token.contract_address_hash}/instances", + Map.merge(response["next_page_params"], filter) + ) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, token_instances) + end + end + + describe "/tokens/{address_hash}/instances/{token_id}" do + test "get 404 on non existing address", %{conn: conn} do + token = build(:token) + + request = get(conn, "/api/v2/tokens/#{token.contract_address.hash}/instances/12") + + assert %{"message" => "Not found"} = json_response(request, 404) + end + + test "get 422 on invalid address", %{conn: conn} do + request = get(conn, "/api/v2/tokens/0x/instances/12") + + assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + end + + test "get token instance by token id", %{conn: conn} do + token = insert(:token, type: "ERC-721") + + for _ <- 0..50 do + insert(:token_instance, token_id: 0) + end + + transaction = + :transaction + |> insert() + |> with_block() + + instance = insert(:token_instance, token_id: 0, token_contract_address_hash: token.contract_address_hash) + + _transfer = + insert(:token_transfer, + token_contract_address: token.contract_address, + transaction: transaction, + token_ids: [0], + token_type: "ERC-721" + ) + + for _ <- 1..50 do + insert(:token_instance, token_contract_address_hash: token.contract_address_hash) + end + + request = get(conn, "/api/v2/tokens/#{token.contract_address.hash}/instances/0") + + assert data = json_response(request, 200) + assert compare_item(instance, data) + assert Address.checksum(instance.owner_address_hash) == data["owner"]["hash"] + end + + test "get 404 on token instance which is not presented in DB", %{conn: conn} do + token = insert(:token, type: "ERC-721") + + request = get(conn, "/api/v2/tokens/#{token.contract_address.hash}/instances/0") + + assert %{"message" => "Not found"} = json_response(request, 404) + end + + # https://github.com/blockscout/blockscout/issues/9906 + test "regression for #9906", %{conn: conn} do + token = insert(:token, type: "ERC-721") + + insert(:token_instance, + token_id: 0, + token_contract_address_hash: token.contract_address_hash, + metadata: %{ + "image_url" => "ipfs://QmTQBtvkCQKnxbUejwYHrs2G74JR2qFwxPUqRb3BQ6BM3S/gm%20gm%20feelin%20blue%204k.png" + } + ) + + request = get(conn, "/api/v2/tokens/#{token.contract_address.hash}/instances/0") + + assert %{ + "image_url" => + "https://ipfs.io/ipfs/QmTQBtvkCQKnxbUejwYHrs2G74JR2qFwxPUqRb3BQ6BM3S/gm%20gm%20feelin%20blue%204k.png" + } = json_response(request, 200) + end + + # https://github.com/blockscout/blockscout/issues/11149 + test "regression for #11149", %{conn: conn} do + token = insert(:token, type: "ERC-721") + + old_env = Application.get_env(:indexer, :ipfs) + + public_ipfs_gateway = "https://ipfs_custom.io/ipfs" + + Application.put_env( + :indexer, + :ipfs, + Keyword.merge(old_env, + gateway_url_param_key: "secret_key", + gateway_url_param_value: "secret_value", + gateway_url_param_location: :query, + gateway_url: "http://localhost/", + public_gateway_url: public_ipfs_gateway + ) + ) + + insert(:token_instance, + token_id: 0, + token_contract_address_hash: token.contract_address_hash, + metadata: %{ + "image_url" => "ipfs://QmTQBtvkCQKnxbUejwYHrs2G74JR2qFwxPUqRb3BQ6BM3S/123.png" + } + ) + + request = get(conn, "/api/v2/tokens/#{token.contract_address.hash}/instances/0") + + assert %{ + "image_url" => "https://ipfs_custom.io/ipfs/QmTQBtvkCQKnxbUejwYHrs2G74JR2qFwxPUqRb3BQ6BM3S/123.png" + } = json_response(request, 200) + + Application.put_env(:indexer, :ipfs, old_env) + end + + test "metadata dropped on token uri on demand filler", %{conn: conn} do + token = insert(:token, type: "ERC-721") + + insert(:token_instance, + token_id: 0, + token_contract_address_hash: token.contract_address_hash, + metadata: %{"awesome" => "metadata"} + ) + + encoded_url_1 = + "0x" <> + (ABI.TypeEncoder.encode(["http://240.0.0.0/api/metadata.json"], %ABI.FunctionSelector{ + function: nil, + types: [ + :string + ] + }) + |> Base.encode16(case: :lower)) + + token_contract_address_hash_string = to_string(token.contract_address_hash) + + expect( + EthereumJSONRPC.Mox, + :json_rpc, + fn [ + %{ + id: id_1, + jsonrpc: "2.0", + method: "eth_call", + params: [ + %{ + data: "0xc87b56dd0000000000000000000000000000000000000000000000000000000000000000", + to: ^token_contract_address_hash_string + }, + "latest" + ] + } + ], + _options -> + {:ok, [%{id: id_1, jsonrpc: "2.0", result: encoded_url_1}]} + end + ) + + request = get(conn, "/api/v2/tokens/#{token.contract_address.hash}/instances/0") + + assert data = json_response(request, 200) + assert data["metadata"] == nil + + instance = Repo.one(Instance) + assert instance.metadata == nil + assert instance.error == "blacklist" + assert instance.skip_metadata_url == false + end + end + + describe "/tokens/{address_hash}/instances/{token_id}/transfers" do + test "get 404 on non existing address", %{conn: conn} do + token = build(:token) + + request = get(conn, "/api/v2/tokens/#{token.contract_address.hash}/instances/12/transfers") + + assert %{"message" => "Not found"} = json_response(request, 404) + end + + test "get 422 on invalid address", %{conn: conn} do + request = get(conn, "/api/v2/tokens/0x/instances/12/transfers") + + assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + end + + test "get token transfers by instance", %{conn: conn} do + token = insert(:token, type: "ERC-1155") + + for _ <- 0..50 do + insert(:token_instance, token_id: 0) + end + + id = :rand.uniform(1_000_000) + + transaction = + :transaction + |> insert(input: "0xabcd010203040506") + |> with_block() + + insert(:token_instance, token_id: id, token_contract_address_hash: token.contract_address_hash) + + insert_list(100, :token_transfer, + token_contract_address: token.contract_address, + transaction: transaction, + token_ids: [id + 1], + token_type: "ERC-1155", + amounts: [1] + ) + + transfers_0 = + insert_list(26, :token_transfer, + token_contract_address: token.contract_address, + transaction: transaction, + token_ids: [id, id + 1], + token_type: "ERC-1155", + amounts: [1, 2] + ) + + transfers_1 = + for _ <- 26..50 do + transaction = + :transaction + |> insert(input: "0xabcd010203040506") + |> with_block() + + insert(:token_transfer, + token_contract_address: token.contract_address, + transaction: transaction, + token_ids: [id], + token_type: "ERC-1155" + ) + end + + request = get(conn, "/api/v2/tokens/#{token.contract_address_hash}/instances/#{id}/transfers") + assert response = json_response(request, 200) + + request_2nd_page = + get( + conn, + "/api/v2/tokens/#{token.contract_address_hash}/instances/#{id}/transfers", + response["next_page_params"] + ) + + assert response_2nd_page = json_response(request_2nd_page, 200) + check_paginated_response(response, response_2nd_page, transfers_0 ++ transfers_1) + end + + test "check that pagination works for 404 tokens", %{conn: conn} do + token = insert(:token, type: "ERC-404") + + for _ <- 0..50 do + insert(:token_instance, token_id: 0) + end + + id = :rand.uniform(1_000_000) + + transaction = + :transaction + |> insert(input: "0xabcd010203040506") + |> with_block() + + insert(:token_instance, token_id: id, token_contract_address_hash: token.contract_address_hash) + + insert_list(100, :token_transfer, + token_contract_address: token.contract_address, + transaction: transaction, + token_ids: [id + 1], + token_type: "ERC-404", + amounts: [1] + ) + + transfers_0 = + insert_list(26, :token_transfer, + token_contract_address: token.contract_address, + transaction: transaction, + token_ids: [id, id + 1], + token_type: "ERC-404", + amounts: [1, 2] + ) + + transfers_1 = + for _ <- 26..50 do + transaction = + :transaction + |> insert(input: "0xabcd010203040506") + |> with_block() + + insert(:token_transfer, + token_contract_address: token.contract_address, + transaction: transaction, + token_ids: [id], + token_type: "ERC-404" + ) + end + + request = get(conn, "/api/v2/tokens/#{token.contract_address_hash}/instances/#{id}/transfers") + assert response = json_response(request, 200) + + request_2nd_page = + get( + conn, + "/api/v2/tokens/#{token.contract_address_hash}/instances/#{id}/transfers", + response["next_page_params"] + ) + + assert response_2nd_page = json_response(request_2nd_page, 200) + check_paginated_response(response, response_2nd_page, transfers_0 ++ transfers_1) + end + + test "check that pagination works for 721 tokens", %{conn: conn} do + token = insert(:token, type: "ERC-721") + id = 0 + insert(:token_instance, token_id: id, token_contract_address_hash: token.contract_address_hash) + + token_transfers = + for _i <- 0..50 do + transaction = insert(:transaction, input: "0xabcd010203040506") |> with_block() + + insert(:token_transfer, + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number, + token_contract_address: token.contract_address, + token_ids: [id], + token_type: "ERC-721" + ) + end + + request = get(conn, "/api/v2/tokens/#{token.contract_address.hash}/instances/#{id}/transfers") + assert response = json_response(request, 200) + + request_2nd_page = + get( + conn, + "/api/v2/tokens/#{token.contract_address.hash}/instances/#{id}/transfers", + response["next_page_params"] + ) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, token_transfers) + end + + test "check that same token_ids within batch squashes", %{conn: conn} do + token = insert(:token, type: "ERC-1155") + + id = 0 + + insert(:token_instance, token_id: id, token_contract_address_hash: token.contract_address_hash) + + transaction = insert(:transaction, input: "0xabcd010203040506") |> with_block() + + tt = + insert(:token_transfer, + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number, + token_contract_address: token.contract_address, + token_ids: Enum.map(0..50, fn _x -> id end), + token_type: "ERC-1155", + amounts: Enum.map(0..50, fn x -> x end) + ) + + token_transfer = %TokenTransfer{tt | token_ids: [id], amount: Decimal.new(1275)} + + request = get(conn, "/api/v2/tokens/#{token.contract_address.hash}/instances/#{id}/transfers") + assert %{"next_page_params" => nil, "items" => [item]} = json_response(request, 200) + compare_item(token_transfer, item) + end + + test "check that pagination works fine with 1155 batches #1 (51 batch with twice repeated id. Repeated id squashed into one element)", + %{conn: conn} do + token = insert(:token, type: "ERC-1155") + + id = 0 + amount = 101 + insert(:token_instance, token_id: id, token_contract_address_hash: token.contract_address_hash) + + transaction = insert(:transaction, input: "0xabcd010203040506") |> with_block() + + tt = + for _ <- 0..50 do + insert(:token_transfer, + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number, + token_contract_address: token.contract_address, + token_ids: Enum.map(0..50, fn x -> x end) ++ [id], + token_type: "ERC-1155", + amounts: Enum.map(1..51, fn x -> x end) ++ [amount] + ) + end + + token_transfers = + for i <- tt do + %TokenTransfer{i | token_ids: [id], amount: amount + 1} + end + + request = get(conn, "/api/v2/tokens/#{token.contract_address.hash}/instances/#{id}/transfers") + assert response = json_response(request, 200) + + request_2nd_page = + get( + conn, + "/api/v2/tokens/#{token.contract_address.hash}/instances/#{id}/transfers", + response["next_page_params"] + ) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, token_transfers) + end + end + + describe "/tokens/{address_hash}/instances/{token_id}/holders" do + test "get 404 on non existing address", %{conn: conn} do + token = build(:token) + + request = get(conn, "/api/v2/tokens/#{token.contract_address.hash}/instances/12/holders") + + assert %{"message" => "Not found"} = json_response(request, 404) + end + + test "get 422 on invalid address", %{conn: conn} do + request = get(conn, "/api/v2/tokens/0x/instances/12/holders") + + assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + end + + test "get 422 on invalid id", %{conn: conn} do + token = insert(:token, type: "ERC-1155") + + request = get(conn, "/api/v2/tokens/#{token.contract_address_hash}/instances/123ab/holders") + + assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + end + + test "get token transfers by instance", %{conn: conn} do + token = insert(:token, type: "ERC-1155") + + id = :rand.uniform(1_000_000) + insert(:token_instance, token_id: id - 1, token_contract_address_hash: token.contract_address_hash) + + insert( + :address_current_token_balance, + token_contract_address_hash: token.contract_address_hash, + value: 1000, + token_id: id - 1 + ) + + insert(:token_instance, token_id: id, token_contract_address_hash: token.contract_address_hash) + + token_balances = + for i <- 0..50 do + insert( + :address_current_token_balance_with_token_id, + token_contract_address_hash: token.contract_address_hash, + value: i + 1000, + token_id: id + ) + end + + request = get(conn, "/api/v2/tokens/#{token.contract_address.hash}/instances/#{id}/holders") + assert response = json_response(request, 200) + + request_2nd_page = + get(conn, "/api/v2/tokens/#{token.contract_address.hash}/instances/#{id}/holders", response["next_page_params"]) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_holders_paginated_response(response, response_2nd_page, token_balances) + end + end + + describe "/tokens/{address_hash}/instances/{token_id}/transfers-count" do + test "get 404 on non existing address", %{conn: conn} do + token = build(:token) + + request = get(conn, "/api/v2/tokens/#{token.contract_address.hash}/instances/12/transfers-count") + + assert %{"message" => "Not found"} = json_response(request, 404) + end + + test "get 422 on invalid address", %{conn: conn} do + request = get(conn, "/api/v2/tokens/0x/instances/12/transfers-count") + + assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + end + + test "receive 0 count", %{conn: conn} do + token = insert(:token, type: "ERC-721") + + insert(:token_instance, token_id: 0, token_contract_address_hash: token.contract_address_hash) + + request = get(conn, "/api/v2/tokens/#{token.contract_address.hash}/instances/0/transfers-count") + + assert %{"transfers_count" => 0} = json_response(request, 200) + end + + test "get count > 0", %{conn: conn} do + token = insert(:token, type: "ERC-721") + + for _ <- 0..50 do + insert(:token_instance, token_id: 0) + end + + transaction = + :transaction + |> insert() + |> with_block() + + insert(:token_instance, token_id: 0, token_contract_address_hash: token.contract_address_hash) + + count = :rand.uniform(1000) + + insert_list(count, :token_transfer, + token_contract_address: token.contract_address, + transaction: transaction, + token_ids: [0], + token_type: "ERC-721" + ) + + request = get(conn, "/api/v2/tokens/#{token.contract_address.hash}/instances/0/transfers-count") + + assert %{"transfers_count" => ^count} = json_response(request, 200) + end + end + + describe "/tokens/{address_hash}/instances/{token_id}/refetch-metadata" do + setup :set_mox_from_context + + setup :verify_on_exit! + + setup %{json_rpc_named_arguments: json_rpc_named_arguments} do + old_recaptcha_env = Application.get_env(:block_scout_web, :recaptcha) + old_http_adapter = Application.get_env(:block_scout_web, :http_adapter) + + v2_secret_key = "v2_secret_key" + v3_secret_key = "v3_secret_key" + + Application.put_env(:block_scout_web, :recaptcha, + v2_secret_key: v2_secret_key, + v3_secret_key: v3_secret_key, + is_disabled: false + ) + + Application.put_env(:block_scout_web, :http_adapter, Explorer.Mox.HTTPoison) + + mocked_json_rpc_named_arguments = Keyword.put(json_rpc_named_arguments, :transport, EthereumJSONRPC.Mox) + + start_supervised!({Task.Supervisor, name: Indexer.TaskSupervisor}) + + start_supervised!( + {TokenInstanceMetadataRefetchOnDemand, + [mocked_json_rpc_named_arguments, [name: TokenInstanceMetadataRefetchOnDemand]]} + ) + + %{json_rpc_named_arguments: mocked_json_rpc_named_arguments} + + Subscriber.to(:fetched_token_instance_metadata, :on_demand) + + on_exit(fn -> + Application.put_env(:block_scout_web, :recaptcha, old_recaptcha_env) + Application.put_env(:block_scout_web, :http_adapter, old_http_adapter) + end) + + {:ok, %{v2_secret_key: v2_secret_key, v3_secret_key: v3_secret_key}} + end + + test "token instance metadata on-demand re-fetcher is called", %{conn: conn, v2_secret_key: v2_secret_key} do + expected_body = "secret=#{v2_secret_key}&response=123" + + Explorer.Mox.HTTPoison + |> expect(:post, fn _url, ^expected_body, _headers, _options -> + {:ok, + %HTTPoison.Response{ + status_code: 200, + body: + Jason.encode!(%{ + "success" => true, + "hostname" => Application.get_env(:block_scout_web, BlockScoutWeb.Endpoint)[:url][:host] + }) + }} + end) + + token = insert(:token, type: "ERC-721") + token_id = 1 + + insert(:token_instance, + token_id: token_id, + token_contract_address_hash: token.contract_address_hash, + metadata: %{} + ) + + metadata = %{"name" => "Super Token"} + url = "http://metadata.endpoint.com" + token_contract_address_hash_string = to_string(token.contract_address_hash) + + TestHelper.fetch_token_uri_mock(url, token_contract_address_hash_string) + + Application.put_env(:explorer, :http_adapter, Explorer.Mox.HTTPoison) + + Explorer.Mox.HTTPoison + |> expect(:get, fn ^url, _headers, _options -> + {:ok, %HTTPoison.Response{status_code: 200, body: Jason.encode!(metadata)}} + end) + + topic = "token_instances:#{token_contract_address_hash_string}" + + {:ok, _reply, _socket} = + BlockScoutWeb.V2.UserSocket + |> socket("no_id", %{}) + |> subscribe_and_join(topic) + + request = + patch(conn, "/api/v2/tokens/#{token.contract_address.hash}/instances/#{token_id}/refetch-metadata", %{ + "recaptcha_response" => "123" + }) + + assert %{"message" => "OK"} = json_response(request, 200) + + :timer.sleep(100) + + assert_receive( + {:chain_event, :fetched_token_instance_metadata, :on_demand, + [^token_contract_address_hash_string, ^token_id, ^metadata]} + ) + + assert_receive %Phoenix.Socket.Message{ + payload: %{token_id: ^token_id, fetched_metadata: ^metadata}, + event: "fetched_token_instance_metadata", + topic: ^topic + }, + :timer.seconds(1) + + token_instance_from_db = + Repo.get_by(Instance, token_id: token_id, token_contract_address_hash: token.contract_address_hash) + + assert(token_instance_from_db) + assert token_instance_from_db.metadata == metadata + + Application.put_env(:explorer, :http_adapter, HTTPoison) + end + + test "don't fetch token instance metadata for non-existent token instance", %{ + conn: conn, + v2_secret_key: v2_secret_key + } do + expected_body = "secret=#{v2_secret_key}&response=123" + + Explorer.Mox.HTTPoison + |> expect(:post, fn _url, ^expected_body, _headers, _options -> + {:ok, + %HTTPoison.Response{ + status_code: 200, + body: + Jason.encode!(%{ + "success" => true, + "hostname" => Application.get_env(:block_scout_web, BlockScoutWeb.Endpoint)[:url][:host] + }) + }} + end) + + token = insert(:token, type: "ERC-721") + token_id = 0 + + insert(:token_instance, token_id: token_id, token_contract_address_hash: token.contract_address_hash) + + request = + patch(conn, "/api/v2/tokens/#{token.contract_address.hash}/instances/1/refetch-metadata", %{ + "recaptcha_response" => "123" + }) + + assert %{"message" => "Not found"} = json_response(request, 404) + end + + test "fetch token instance metadata for existing token instance with no metadata", %{ + conn: conn, + v2_secret_key: v2_secret_key + } do + expected_body = "secret=#{v2_secret_key}&response=123" + + Explorer.Mox.HTTPoison + |> expect(:post, fn _url, ^expected_body, _headers, _options -> + {:ok, + %HTTPoison.Response{ + status_code: 200, + body: + Jason.encode!(%{ + "success" => true, + "hostname" => Application.get_env(:block_scout_web, BlockScoutWeb.Endpoint)[:url][:host] + }) + }} + end) + + token = insert(:token, type: "ERC-721") + token_id = 1 + + insert(:token_instance, + token_id: token_id, + token_contract_address_hash: token.contract_address_hash, + metadata: nil + ) + + metadata = %{"name" => "Super Token"} + url = "http://metadata.endpoint.com" + token_contract_address_hash_string = to_string(token.contract_address_hash) + + TestHelper.fetch_token_uri_mock(url, token_contract_address_hash_string) + + Application.put_env(:explorer, :http_adapter, Explorer.Mox.HTTPoison) + + Explorer.Mox.HTTPoison + |> expect(:get, fn ^url, _headers, _options -> + {:ok, %HTTPoison.Response{status_code: 200, body: Jason.encode!(metadata)}} + end) + + topic = "token_instances:#{token_contract_address_hash_string}" + + {:ok, _reply, _socket} = + BlockScoutWeb.V2.UserSocket + |> socket("no_id", %{}) + |> subscribe_and_join(topic) + + request = + patch(conn, "/api/v2/tokens/#{token.contract_address.hash}/instances/#{token_id}/refetch-metadata", %{ + "recaptcha_response" => "123" + }) + + assert %{"message" => "OK"} = json_response(request, 200) + + :timer.sleep(100) + + assert_receive( + {:chain_event, :fetched_token_instance_metadata, :on_demand, + [^token_contract_address_hash_string, ^token_id, ^metadata]} + ) + + assert_receive %Phoenix.Socket.Message{ + payload: %{token_id: ^token_id, fetched_metadata: ^metadata}, + event: "fetched_token_instance_metadata", + topic: ^topic + }, + :timer.seconds(1) + + token_instance_from_db = + Repo.get_by(Instance, token_id: token_id, token_contract_address_hash: token.contract_address_hash) + + assert(token_instance_from_db) + assert token_instance_from_db.metadata == metadata + + Application.put_env(:explorer, :http_adapter, HTTPoison) + end + + test "fetch token instance metadata using scoped bypass api key", %{conn: conn} do + # Configure scoped bypass api key for this test + old_recaptcha_env = Application.get_env(:block_scout_web, :recaptcha) + scoped_bypass_token = "test_scoped_token_123" + + Application.put_env( + :block_scout_web, + :recaptcha, + Keyword.merge(old_recaptcha_env, + scoped_bypass_tokens: [ + token_instance_refetch_metadata: scoped_bypass_token + ] + ) + ) + + Application.put_env(:explorer, :http_adapter, Explorer.Mox.HTTPoison) + + on_exit(fn -> + Application.put_env(:block_scout_web, :recaptcha, old_recaptcha_env) + Application.put_env(:explorer, :http_adapter, HTTPoison) + end) + + token = insert(:token, type: "ERC-721") + token_id = 1 + + insert(:token_instance, + token_id: token_id, + token_contract_address_hash: token.contract_address_hash, + metadata: %{} + ) + + metadata = %{"name" => "Super Token"} + url = "http://metadata.endpoint.com" + token_contract_address_hash_string = to_string(token.contract_address_hash) + + TestHelper.fetch_token_uri_mock(url, token_contract_address_hash_string) + + Explorer.Mox.HTTPoison + |> expect(:get, fn ^url, _headers, _options -> + {:ok, %HTTPoison.Response{status_code: 200, body: Jason.encode!(metadata)}} + end) + + topic = "token_instances:#{token_contract_address_hash_string}" + + {:ok, _reply, _socket} = + BlockScoutWeb.V2.UserSocket + |> socket("no_id", %{}) + |> subscribe_and_join(topic) + + request = + patch(conn, "/api/v2/tokens/#{token.contract_address.hash}/instances/#{token_id}/refetch-metadata", %{ + "scoped_recaptcha_bypass_token" => scoped_bypass_token + }) + + assert %{"message" => "OK"} = json_response(request, 200) + + :timer.sleep(100) + + assert_receive( + {:chain_event, :fetched_token_instance_metadata, :on_demand, + [^token_contract_address_hash_string, ^token_id, ^metadata]} + ) + + assert_receive %Phoenix.Socket.Message{ + payload: %{token_id: ^token_id, fetched_metadata: ^metadata}, + event: "fetched_token_instance_metadata", + topic: ^topic + }, + :timer.seconds(1) + + token_instance_from_db = + Repo.get_by(Instance, token_id: token_id, token_contract_address_hash: token.contract_address_hash) + + assert(token_instance_from_db) + assert token_instance_from_db.metadata == metadata + end + + test "falls back to normal reCAPTCHA when incorrect scoped bypass api key is supplied", %{ + conn: conn, + v2_secret_key: v2_secret_key + } do + # Configure scoped bypass api key for this test + old_recaptcha_env = Application.get_env(:block_scout_web, :recaptcha) + scoped_bypass_token = "test_scoped_token_123" + + Application.put_env( + :block_scout_web, + :recaptcha, + Keyword.merge(old_recaptcha_env, + scoped_bypass_tokens: [ + token_instance_refetch_metadata: scoped_bypass_token + ] + ) + ) + + Application.put_env(:explorer, :http_adapter, Explorer.Mox.HTTPoison) + + on_exit(fn -> + Application.put_env(:block_scout_web, :recaptcha, old_recaptcha_env) + Application.put_env(:explorer, :http_adapter, HTTPoison) + end) + + token = insert(:token, type: "ERC-721") + token_id = 1 + + insert(:token_instance, + token_id: token_id, + token_contract_address_hash: token.contract_address_hash, + metadata: %{} + ) + + metadata = %{"name" => "Super Token"} + url = "http://metadata.endpoint.com" + token_contract_address_hash_string = to_string(token.contract_address_hash) + + TestHelper.fetch_token_uri_mock(url, token_contract_address_hash_string) + + # First request with wrong scoped token - should fail + request = + patch(conn, "/api/v2/tokens/#{token.contract_address.hash}/instances/#{token_id}/refetch-metadata", %{ + "scoped_recaptcha_bypass_token" => "wrong_scoped_token" + }) + + assert %{"message" => "Invalid reCAPTCHA response"} = json_response(request, 403) + + # Set up normal reCAPTCHA validation for the second request + expected_body = "secret=#{v2_secret_key}&response=correct_recaptcha_token" + + Explorer.Mox.HTTPoison + |> expect(:post, fn _url, ^expected_body, _headers, _options -> + {:ok, + %HTTPoison.Response{ + status_code: 200, + body: + Jason.encode!(%{ + "success" => true, + "hostname" => Application.get_env(:block_scout_web, BlockScoutWeb.Endpoint)[:url][:host] + }) + }} + end) + + Explorer.Mox.HTTPoison + |> expect(:get, fn ^url, _headers, _options -> + {:ok, %HTTPoison.Response{status_code: 200, body: Jason.encode!(metadata)}} + end) + + topic = "token_instances:#{token_contract_address_hash_string}" + + {:ok, _reply, _socket} = + BlockScoutWeb.V2.UserSocket + |> socket("no_id", %{}) + |> subscribe_and_join(topic) + + # Second request with correct reCAPTCHA token - should work + request = + patch(conn, "/api/v2/tokens/#{token.contract_address.hash}/instances/#{token_id}/refetch-metadata", %{ + "recaptcha_response" => "correct_recaptcha_token" + }) + + assert %{"message" => "OK"} = json_response(request, 200) + + :timer.sleep(100) + + assert_receive( + {:chain_event, :fetched_token_instance_metadata, :on_demand, + [^token_contract_address_hash_string, ^token_id, ^metadata]} + ) + + token_instance_from_db = + Repo.get_by(Instance, token_id: token_id, token_contract_address_hash: token.contract_address_hash) + + assert(token_instance_from_db) + assert token_instance_from_db.metadata == metadata + end + + test "rejects scoped bypass api key when scoped tokens are not configured", %{ + conn: conn, + v2_secret_key: v2_secret_key + } do + # Make sure we don't have scoped tokens configured + old_recaptcha_env = Application.get_env(:block_scout_web, :recaptcha) + + # Ensure there are no scoped_bypass_tokens in the configuration + Application.put_env( + :block_scout_web, + :recaptcha, + Keyword.merge(old_recaptcha_env, + scoped_bypass_tokens: [ + token_instance_refetch_metadata: nil + ] + ) + ) + + Application.put_env(:explorer, :http_adapter, Explorer.Mox.HTTPoison) + + on_exit(fn -> + Application.put_env(:block_scout_web, :recaptcha, old_recaptcha_env) + Application.put_env(:explorer, :http_adapter, HTTPoison) + end) + + token = insert(:token, type: "ERC-721") + token_id = 1 + + insert(:token_instance, + token_id: token_id, + token_contract_address_hash: token.contract_address_hash, + metadata: %{} + ) + + metadata = %{"name" => "Super Token"} + url = "http://metadata.endpoint.com" + token_contract_address_hash_string = to_string(token.contract_address_hash) + + # First request with a scoped token that isn't configured - should fail + request = + patch(conn, "/api/v2/tokens/#{token.contract_address.hash}/instances/#{token_id}/refetch-metadata", %{ + "scoped_recaptcha_bypass_token" => "some_token_that_does_not_exist" + }) + + assert %{"message" => "Invalid reCAPTCHA response"} = json_response(request, 403) + + request = + patch(conn, "/api/v2/tokens/#{token.contract_address.hash}/instances/#{token_id}/refetch-metadata", %{ + "scoped_recaptcha_bypass_token" => "" + }) + + assert %{"message" => "Invalid reCAPTCHA response"} = json_response(request, 403) + + request = + patch(conn, "/api/v2/tokens/#{token.contract_address.hash}/instances/#{token_id}/refetch-metadata", %{ + "scoped_recaptcha_bypass_token" => nil + }) + + assert %{"message" => "Invalid reCAPTCHA response"} = json_response(request, 403) + + # Set up normal reCAPTCHA validation for the second request + expected_body = "secret=#{v2_secret_key}&response=correct_recaptcha_token" + + Explorer.Mox.HTTPoison + |> expect(:post, fn _url, ^expected_body, _headers, _options -> + {:ok, + %HTTPoison.Response{ + status_code: 200, + body: + Jason.encode!(%{ + "success" => true, + "hostname" => Application.get_env(:block_scout_web, BlockScoutWeb.Endpoint)[:url][:host] + }) + }} + end) + + TestHelper.fetch_token_uri_mock(url, token_contract_address_hash_string) + + Explorer.Mox.HTTPoison + |> expect(:get, fn ^url, _headers, _options -> + {:ok, %HTTPoison.Response{status_code: 200, body: Jason.encode!(metadata)}} + end) + + # Second request with correct reCAPTCHA token - should work + request = + patch(conn, "/api/v2/tokens/#{token.contract_address.hash}/instances/#{token_id}/refetch-metadata", %{ + "recaptcha_response" => "correct_recaptcha_token" + }) + + assert %{"message" => "OK"} = json_response(request, 200) + + :timer.sleep(100) + + token_instance_from_db = + Repo.get_by(Instance, token_id: token_id, token_contract_address_hash: token.contract_address.hash) + + assert(token_instance_from_db) + assert token_instance_from_db.metadata == metadata + end + end + + describe "/tokens/{address_hash}/instances/refetch-metadata" do + setup :set_mox_from_context + + setup :verify_on_exit! + + setup %{json_rpc_named_arguments: json_rpc_named_arguments} do + Application.put_env(:block_scout_web, :sensitive_endpoints_api_key, "abc") + mocked_json_rpc_named_arguments = Keyword.put(json_rpc_named_arguments, :transport, EthereumJSONRPC.Mox) + + start_supervised!({Task.Supervisor, name: Indexer.TaskSupervisor}) + + start_supervised!( + {NFTCollectionMetadataRefetchOnDemand, + [mocked_json_rpc_named_arguments, [name: NFTCollectionMetadataRefetchOnDemand]]} + ) + + %{json_rpc_named_arguments: mocked_json_rpc_named_arguments} + + on_exit(fn -> + Application.put_env(:block_scout_web, :sensitive_endpoints_api_key, nil) + end) + + :ok + end + + test "token instance metadata on-demand re-fetcher is called", %{conn: conn} do + token = insert(:token, type: "ERC-721") + + for id <- 1..5 do + insert(:token_instance, + token_id: id, + token_contract_address_hash: token.contract_address_hash, + metadata: %{} + ) + end + + request = + patch( + conn, + "/api/v2/tokens/#{token.contract_address.hash}/instances/refetch-metadata", + %{ + "api_key" => "abc" + } + ) + + assert %{"message" => "OK"} = json_response(request, 200) + + :timer.sleep(100) + + token_instances_from_db = Repo.all(Instance, token_contract_address_hash: token.contract_address_hash) + + assert(token_instances_from_db) + + for token_instance_from_db <- token_instances_from_db do + assert token_instance_from_db.metadata == nil + assert token_instance_from_db.error == ":marked_to_refetch" + end + end + + test "don't trigger metadata re-fetch, if no admin api key is provided", %{conn: conn} do + token = insert(:token, type: "ERC-721") + + request = + patch(conn, "/api/v2/tokens/#{token.contract_address.hash}/instances/refetch-metadata") + + assert %{"message" => "Wrong API key"} = json_response(request, 401) + end + end + + defp compare_holders_item(%CurrentTokenBalance{} = ctb, json) do + assert Address.checksum(ctb.address_hash) == json["address"]["hash"] + assert (ctb.token_id && to_string(ctb.token_id)) == json["token_id"] + assert to_string(ctb.value) == json["value"] + end + + def compare_item(%Address{} = address, json) do + assert Address.checksum(address.hash) == json["hash"] + end + + def compare_item(%Token{} = token, json) do + assert Address.checksum(token.contract_address.hash) == json["address"] + assert token.symbol == json["symbol"] + assert token.name == json["name"] + assert to_string(token.decimals) == json["decimals"] + assert token.type == json["type"] + + assert (is_nil(token.holder_count) and is_nil(json["holders"])) or + (to_string(token.holder_count) == json["holders"] and !is_nil(token.holder_count)) + + assert to_string(token.total_supply) == json["total_supply"] + assert Map.has_key?(json, "exchange_rate") + end + + def compare_item(%TokenTransfer{} = token_transfer, json) do + assert Address.checksum(token_transfer.from_address_hash) == json["from"]["hash"] + assert Address.checksum(token_transfer.to_address_hash) == json["to"]["hash"] + assert to_string(token_transfer.transaction_hash) == json["transaction_hash"] + assert json["timestamp"] != nil + assert json["method"] != nil + assert to_string(token_transfer.block_hash) == json["block_hash"] + assert token_transfer.log_index == json["log_index"] + assert check_total(Repo.preload(token_transfer, [{:token, :contract_address}]).token, json["total"], token_transfer) + end + + def compare_item(%CurrentTokenBalance{} = ctb, json) do + compare_holders_item(ctb, json) + compare_item(Repo.preload(ctb, [{:token, :contract_address}]).token, json["token"]) + end + + def compare_item(%Instance{token: %Token{} = token} = instance, json) do + token_type = token.type + value = to_string(value(token.type, instance)) + id = to_string(instance.token_id) + metadata = instance.metadata + token_address_hash = Address.checksum(token.contract_address_hash) + app_url = instance.metadata["external_url"] + animation_url = instance.metadata["animation_url"] + image_url = instance.metadata["image_url"] + token_name = token.name + owner_address_hash = Address.checksum(instance.owner.hash) + is_contract = !is_nil(instance.owner.contract_code) + is_unique = value == "1" + + assert %{ + "token_type" => ^token_type, + "value" => ^value, + "id" => ^id, + "metadata" => ^metadata, + "token" => %{"address" => ^token_address_hash, "name" => ^token_name, "type" => ^token_type}, + "external_app_url" => ^app_url, + "animation_url" => ^animation_url, + "image_url" => ^image_url, + "is_unique" => ^is_unique + } = json + + if is_unique do + assert owner_address_hash == json["owner"]["hash"] + assert is_contract == json["owner"]["is_contract"] + else + assert json["owner"] == nil + end + end + + def compare_item(%Instance{} = instance, json) do + assert to_string(instance.token_id) == json["id"] + assert Jason.decode!(Jason.encode!(instance.metadata)) == json["metadata"] + assert json["is_unique"] + compare_item(Repo.preload(instance, [{:token, :contract_address}]).token, json["token"]) + end + + defp value("ERC-721", _), do: 1 + defp value(_, nft), do: nft.current_token_balance.value + + # with the current implementation no transfers should come with list in totals + def check_total(%Token{type: nft}, json, _token_transfer) when nft in ["ERC-721", "ERC-1155"] and is_list(json) do + false + end + + def check_total(%Token{type: nft}, json, token_transfer) when nft in ["ERC-1155"] do + json["token_id"] in Enum.map(token_transfer.token_ids, fn x -> to_string(x) end) and + json["value"] == to_string(token_transfer.amount) + end + + def check_total(%Token{type: nft}, json, token_transfer) when nft in ["ERC-721"] do + json["token_id"] in Enum.map(token_transfer.token_ids, fn x -> to_string(x) end) + end + + def check_total(_, _, _), do: true + + defp check_paginated_response(first_page_resp, second_page_resp, list) do + assert Enum.count(first_page_resp["items"]) == 50 + assert first_page_resp["next_page_params"] != nil + compare_item(Enum.at(list, 50), Enum.at(first_page_resp["items"], 0)) + compare_item(Enum.at(list, 1), Enum.at(first_page_resp["items"], 49)) + + assert Enum.count(second_page_resp["items"]) == 1 + assert second_page_resp["next_page_params"] == nil + compare_item(Enum.at(list, 0), Enum.at(second_page_resp["items"], 0)) + end + + defp check_holders_paginated_response(first_page_resp, second_page_resp, list) do + assert Enum.count(first_page_resp["items"]) == 50 + assert first_page_resp["next_page_params"] != nil + compare_holders_item(Enum.at(list, 50), Enum.at(first_page_resp["items"], 0)) + compare_holders_item(Enum.at(list, 1), Enum.at(first_page_resp["items"], 49)) + + assert Enum.count(second_page_resp["items"]) == 1 + assert second_page_resp["next_page_params"] == nil + compare_holders_item(Enum.at(list, 0), Enum.at(second_page_resp["items"], 0)) + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/api/v2/token_transfer_controller_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/api/v2/token_transfer_controller_test.exs new file mode 100644 index 0000000..fe293c2 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/api/v2/token_transfer_controller_test.exs @@ -0,0 +1,189 @@ +defmodule BlockScoutWeb.API.V2.TokenTransferControllerTest do + use BlockScoutWeb.ConnCase + + alias Explorer.Chain.{Address, TokenTransfer} + + describe "/token-transfers" do + test "empty list", %{conn: conn} do + request = get(conn, "/api/v2/token-transfers") + + assert response = json_response(request, 200) + assert response["items"] == [] + assert response["next_page_params"] == nil + end + + test "non empty list", %{conn: conn} do + transaction = + :transaction + |> insert() + |> with_block() + + 1 |> insert_list(:token_transfer, transaction: transaction) + + request = get(conn, "/api/v2/token-transfers") + + assert response = json_response(request, 200) + assert Enum.count(response["items"]) == 1 + assert response["next_page_params"] == nil + end + + test "filters by type", %{conn: conn} do + transaction = + :transaction + |> insert() + |> with_block() + + token = insert(:token, type: "ERC-721") + + insert(:token_transfer, + transaction: transaction, + token: token, + token_type: "ERC-721" + ) + + request = get(conn, "/api/v2/token-transfers?type=ERC-1155") + + assert response = json_response(request, 200) + assert Enum.count(response["items"]) == 0 + assert response["next_page_params"] == nil + end + + test "returns all transfers if filter is incorrect", %{conn: conn} do + transaction = + :transaction + |> insert() + |> with_block() + + token = insert(:token, type: "ERC-100500") + + insert(:token_transfer, + transaction: transaction, + token: token, + token_type: "ERC-721", + token_ids: [1] + ) + + insert(:token_transfer, + transaction: transaction, + token: token, + token_type: "ERC-20" + ) + + request = get(conn, "/api/v2/token-transfers?type=ERC-20") + + assert response = json_response(request, 200) + assert Enum.count(response["items"]) == 2 + assert response["next_page_params"] == nil + end + + test "token transfers with next_page_params", %{conn: conn} do + token_transfers = + for _i <- 0..50 do + transaction = insert(:transaction) |> with_block() + + insert(:token_transfer, + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number + ) + end + + request = get(conn, "/api/v2/token-transfers") + assert response = json_response(request, 200) + + request_2nd_page = get(conn, "/api/v2/token-transfers", response["next_page_params"]) + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, nil, token_transfers) + end + + test "flatten erc1155 batch token transfer", %{conn: conn} do + transaction = insert(:transaction) |> with_block() + + transfer = + insert(:token_transfer, + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number, + token_ids: [1, 2, 3], + amounts: [500, 600, 700], + token_type: "ERC-1155" + ) + + insert(:token_instance, + token_id: 3, + token_contract_address_hash: transfer.token_contract_address_hash, + metadata: %{test: "test"} + ) + + request = get(conn, "/api/v2/token-transfers") + assert response = json_response(request, 200) + assert Enum.count(response["items"]) == 3 + + assert %{"decimals" => "18", "value" => "700", "token_id" => "3", "token_instance" => token_instance} = + Enum.at(response["items"], 0)["total"] + + assert token_instance["metadata"] == %{"test" => "test"} + end + + test "paginates erc1155 batch token transfers", %{conn: conn} do + token_transfers = + for _i <- 0..50 do + transaction = insert(:transaction) |> with_block() + + insert(:token_transfer, + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number, + token_ids: [1, 2], + amounts: [500, 600], + token_type: "ERC-1155" + ) + end + + request = get(conn, "/api/v2/token-transfers") + assert response = json_response(request, 200) + + request_2nd_page = get(conn, "/api/v2/token-transfers", response["next_page_params"]) + assert response_2nd_page = json_response(request_2nd_page, 200) + + request_3d_page = get(conn, "/api/v2/token-transfers", response_2nd_page["next_page_params"]) + assert response_3d_page = json_response(request_3d_page, 200) + + check_paginated_response(response, response_2nd_page, response_3d_page, token_transfers) + end + end + + defp compare_item(%TokenTransfer{} = token_transfer, json) do + assert Address.checksum(token_transfer.from_address_hash) == json["from"]["hash"] + assert Address.checksum(token_transfer.to_address_hash) == json["to"]["hash"] + assert to_string(token_transfer.transaction_hash) == json["transaction_hash"] + assert token_transfer.transaction.block_timestamp == Timex.parse!(json["timestamp"], "{ISO:Extended:Z}") + assert json["method"] == nil + assert token_transfer.block_number == json["block_number"] + assert token_transfer.log_index == json["log_index"] + end + + defp check_paginated_response(first_page_resp, second_page_resp, third_page_resp, token_transfers) do + assert Enum.count(first_page_resp["items"]) == 50 + assert first_page_resp["next_page_params"] != nil + compare_item(Enum.at(token_transfers, 50), Enum.at(first_page_resp["items"], 0)) + + if is_nil(third_page_resp) do + compare_item(Enum.at(token_transfers, 1), Enum.at(first_page_resp["items"], 49)) + + assert Enum.count(second_page_resp["items"]) == 1 + assert second_page_resp["next_page_params"] == nil + compare_item(Enum.at(token_transfers, 0), Enum.at(second_page_resp["items"], 0)) + else + assert Enum.count(second_page_resp["items"]) == 50 + assert second_page_resp["next_page_params"] !== nil + + compare_item(Enum.at(token_transfers, 1), Enum.at(second_page_resp["items"], 49)) + + assert Enum.count(third_page_resp["items"]) == 2 + assert third_page_resp["next_page_params"] == nil + compare_item(Enum.at(token_transfers, 0), Enum.at(third_page_resp["items"], 0)) + end + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs new file mode 100644 index 0000000..f3179ef --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs @@ -0,0 +1,1695 @@ +defmodule BlockScoutWeb.API.V2.TransactionControllerTest do + use BlockScoutWeb.ConnCase + + import Explorer.Chain, only: [hash_to_lower_case_string: 1] + import Mox + + require Logger + alias Explorer.Account.{Identity, WatchlistAddress} + alias Explorer.Chain.{Address, InternalTransaction, Log, Token, TokenTransfer, Transaction, Wei} + alias Explorer.Repo + import Ecto.Query, only: [from: 2] + use Utils.CompileTimeEnvHelper, chain_type: [:explorer, :chain_type] + + setup do + Supervisor.terminate_child(Explorer.Supervisor, Explorer.Chain.Cache.TransactionsApiV2.child_id()) + Supervisor.restart_child(Explorer.Supervisor, Explorer.Chain.Cache.TransactionsApiV2.child_id()) + + :ok + end + + describe "/transactions" do + test "empty list", %{conn: conn} do + request = get(conn, "/api/v2/transactions") + + assert response = json_response(request, 200) + assert response["items"] == [] + assert response["next_page_params"] == nil + end + + test "non empty list", %{conn: conn} do + 1 + |> insert_list(:transaction) + |> with_block() + + request = get(conn, "/api/v2/transactions") + + assert response = json_response(request, 200) + assert Enum.count(response["items"]) == 1 + assert response["next_page_params"] == nil + end + + test "transactions with next_page_params", %{conn: conn} do + transactions = + 51 + |> insert_list(:transaction) + |> with_block() + + request = get(conn, "/api/v2/transactions") + assert response = json_response(request, 200) + + request_2nd_page = get(conn, "/api/v2/transactions", response["next_page_params"]) + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, transactions) + end + + test "filter=pending", %{conn: conn} do + pending_transactions = + 51 + |> insert_list(:transaction) + + _mined_transactions = + 51 + |> insert_list(:transaction) + |> with_block() + + filter = %{"filter" => "pending"} + + request = get(conn, "/api/v2/transactions", filter) + assert response = json_response(request, 200) + + request_2nd_page = get(conn, "/api/v2/transactions", Map.merge(response["next_page_params"], filter)) + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, pending_transactions) + end + + test "filter=validated", %{conn: conn} do + _pending_transactions = + 51 + |> insert_list(:transaction) + + mined_transactions = + 51 + |> insert_list(:transaction) + |> with_block() + + filter = %{"filter" => "validated"} + + request = get(conn, "/api/v2/transactions", filter) + assert response = json_response(request, 200) + + request_2nd_page = get(conn, "/api/v2/transactions", Map.merge(response["next_page_params"], filter)) + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, mined_transactions) + end + end + + describe "/transactions/watchlist" do + test "unauthorized", %{conn: conn} do + request = get(conn, "/api/v2/transactions/watchlist") + + assert %{"message" => "Unauthorized"} = json_response(request, 401) + end + + test "empty list", %{conn: conn} do + 51 + |> insert_list(:transaction) + |> with_block() + + auth = build(:auth) + insert(:address) + {:ok, user} = Identity.find_or_create(auth) + + conn = Plug.Test.init_test_session(conn, current_user: user) + + request = get(conn, "/api/v2/transactions/watchlist") + assert response = json_response(request, 200) + + assert response["items"] == [] + assert response["next_page_params"] == nil + end + + test "watchlist transactions can paginate", %{conn: conn} do + auth = build(:auth) + {:ok, user} = Identity.find_or_create(auth) + + conn = Plug.Test.init_test_session(conn, current_user: user) + + address_1 = insert(:address) + + watchlist_address_1 = + Repo.account_repo().insert!(%WatchlistAddress{ + name: "wallet_1", + watchlist_id: user.watchlist_id, + address_hash: address_1.hash, + address_hash_hash: hash_to_lower_case_string(address_1.hash), + watch_coin_input: true, + watch_coin_output: true, + watch_erc_20_input: true, + watch_erc_20_output: true, + watch_erc_721_input: true, + watch_erc_721_output: true, + watch_erc_1155_input: true, + watch_erc_1155_output: true, + notify_email: true + }) + + address_2 = insert(:address) + + watchlist_address_2 = + Repo.account_repo().insert!(%WatchlistAddress{ + name: "wallet_2", + watchlist_id: user.watchlist_id, + address_hash: address_2.hash, + address_hash_hash: hash_to_lower_case_string(address_2.hash), + watch_coin_input: true, + watch_coin_output: true, + watch_erc_20_input: true, + watch_erc_20_output: true, + watch_erc_721_input: true, + watch_erc_721_output: true, + watch_erc_1155_input: true, + watch_erc_1155_output: true, + notify_email: true + }) + + 51 + |> insert_list(:transaction) + + 51 + |> insert_list(:transaction) + |> with_block() + + transactions_1 = + 25 + |> insert_list(:transaction, from_address: address_1) + |> with_block() + + transactions_2 = + 1 + |> insert_list(:transaction, from_address: address_2, to_address: address_1) + |> with_block() + + transactions_3 = + 25 + |> insert_list(:transaction, from_address: address_2) + |> with_block() + + request = get(conn, "/api/v2/transactions/watchlist") + assert response = json_response(request, 200) + + request_2nd_page = get(conn, "/api/v2/transactions/watchlist", response["next_page_params"]) + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, transactions_1 ++ transactions_2 ++ transactions_3, %{ + address_1.hash => watchlist_address_1.name, + address_2.hash => watchlist_address_2.name + }) + end + end + + describe "/transactions/{transaction_hash}" do + test "return 404 on non existing transaction", %{conn: conn} do + transaction = build(:transaction) + request = get(conn, "/api/v2/transactions/#{to_string(transaction.hash)}") + + assert %{"message" => "Not found"} = json_response(request, 404) + end + + test "return 422 on invalid transaction hash", %{conn: conn} do + request = get(conn, "/api/v2/transactions/0x") + + assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + end + + test "return existing transaction", %{conn: conn} do + transaction = + :transaction + |> insert() + |> with_block() + + request = get(conn, "/api/v2/transactions/" <> to_string(transaction.hash)) + + assert response = json_response(request, 200) + compare_item(transaction, response) + end + + test "batch 1155 flattened", %{conn: conn} do + token = insert(:token, type: "ERC-1155") + + transaction = + :transaction + |> insert() + |> with_block() + + insert(:token_transfer, + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number, + token_contract_address: token.contract_address, + token_ids: Enum.map(0..50, fn x -> x end), + token_type: "ERC-1155", + amounts: Enum.map(0..50, fn x -> x end) + ) + + request = get(conn, "/api/v2/transactions/" <> to_string(transaction.hash)) + + assert response = json_response(request, 200) + compare_item(transaction, response) + + assert Enum.count(response["token_transfers"]) == 10 + end + + test "single 1155 flattened", %{conn: conn} do + token = insert(:token, type: "ERC-1155") + + transaction = + :transaction + |> insert() + |> with_block() + + tt = + insert(:token_transfer, + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number, + token_contract_address: token.contract_address, + token_ids: [1], + token_type: "ERC-1155", + amounts: [2], + amount: nil + ) + + request = get(conn, "/api/v2/transactions/" <> to_string(transaction.hash)) + + assert response = json_response(request, 200) + compare_item(transaction, response) + + assert Enum.count(response["token_transfers"]) == 1 + assert is_map(Enum.at(response["token_transfers"], 0)["total"]) + assert compare_item(%TokenTransfer{tt | amount: 2}, Enum.at(response["token_transfers"], 0)) + end + end + + describe "/transactions/{transaction_hash}/internal-transactions" do + test "return 404 on non existing transaction", %{conn: conn} do + transaction = build(:transaction) + request = get(conn, "/api/v2/transactions/#{to_string(transaction.hash)}/internal-transactions") + + assert %{"message" => "Not found"} = json_response(request, 404) + end + + test "return 422 on invalid transaction hash", %{conn: conn} do + request = get(conn, "/api/v2/transactions/0x/internal-transactions") + + assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + end + + test "return empty list", %{conn: conn} do + transaction = + :transaction + |> insert() + |> with_block() + + request = get(conn, "/api/v2/transactions/#{to_string(transaction.hash)}/internal-transactions") + + assert response = json_response(request, 200) + assert response["items"] == [] + assert response["next_page_params"] == nil + end + + test "return relevant internal transaction", %{conn: conn} do + transaction = + :transaction + |> insert() + |> with_block() + + insert(:internal_transaction, + transaction: transaction, + index: 0, + block_number: transaction.block_number, + transaction_index: transaction.index, + block_hash: transaction.block_hash, + block_index: 0 + ) + + internal_transaction = + insert(:internal_transaction, + transaction: transaction, + index: 1, + block_number: transaction.block_number, + transaction_index: transaction.index, + block_hash: transaction.block_hash, + block_index: 1 + ) + + transaction_1 = + :transaction + |> insert() + |> with_block() + + 0..5 + |> Enum.map(fn index -> + insert(:internal_transaction, + transaction: transaction_1, + index: index, + block_number: transaction_1.block_number, + transaction_index: transaction_1.index, + block_hash: transaction_1.block_hash, + block_index: index + ) + end) + + request = get(conn, "/api/v2/transactions/#{to_string(transaction.hash)}/internal-transactions") + + assert response = json_response(request, 200) + assert Enum.count(response["items"]) == 1 + assert response["next_page_params"] == nil + compare_item(internal_transaction, Enum.at(response["items"], 0)) + end + + test "return list with next_page_params", %{conn: conn} do + transaction = + :transaction + |> insert() + |> with_block() + + insert(:internal_transaction, + transaction: transaction, + index: 0, + block_number: transaction.block_number, + transaction_index: transaction.index, + block_hash: transaction.block_hash, + block_index: 0 + ) + + internal_transactions = + 51..1 + |> Enum.map(fn index -> + insert(:internal_transaction, + transaction: transaction, + index: index, + block_number: transaction.block_number, + transaction_index: transaction.index, + block_hash: transaction.block_hash, + block_index: index + ) + end) + + request = get(conn, "/api/v2/transactions/#{to_string(transaction.hash)}/internal-transactions") + assert response = json_response(request, 200) + + request_2nd_page = + get( + conn, + "/api/v2/transactions/#{to_string(transaction.hash)}/internal-transactions", + response["next_page_params"] + ) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, internal_transactions) + end + end + + describe "/transactions/{transaction_hash}/logs" do + test "return 404 on non existing transaction", %{conn: conn} do + transaction = build(:transaction) + request = get(conn, "/api/v2/transactions/#{to_string(transaction.hash)}/logs") + + assert %{"message" => "Not found"} = json_response(request, 404) + end + + test "return 422 on invalid transaction hash", %{conn: conn} do + request = get(conn, "/api/v2/transactions/0x/logs") + + assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + end + + test "return empty list", %{conn: conn} do + transaction = + :transaction + |> insert() + |> with_block() + + request = get(conn, "/api/v2/transactions/#{to_string(transaction.hash)}/logs") + + assert response = json_response(request, 200) + assert response["items"] == [] + assert response["next_page_params"] == nil + end + + test "return relevant log", %{conn: conn} do + transaction = + :transaction + |> insert() + |> with_block() + + log = + insert(:log, + transaction: transaction, + index: 1, + block: transaction.block, + block_number: transaction.block_number + ) + + transaction_1 = + :transaction + |> insert() + |> with_block() + + 0..5 + |> Enum.map(fn index -> + insert(:log, + transaction: transaction_1, + index: index, + block: transaction_1.block, + block_number: transaction_1.block_number + ) + end) + + request = get(conn, "/api/v2/transactions/#{to_string(transaction.hash)}/logs") + + assert response = json_response(request, 200) + assert Enum.count(response["items"]) == 1 + assert response["next_page_params"] == nil + compare_item(log, Enum.at(response["items"], 0)) + end + + test "return list with next_page_params", %{conn: conn} do + transaction = + :transaction + |> insert() + |> with_block() + + logs = + 50..0 + |> Enum.map(fn index -> + insert(:log, + transaction: transaction, + index: index, + block: transaction.block, + block_number: transaction.block_number + ) + end) + + request = get(conn, "/api/v2/transactions/#{to_string(transaction.hash)}/logs") + assert response = json_response(request, 200) + + request_2nd_page = + get(conn, "/api/v2/transactions/#{to_string(transaction.hash)}/logs", response["next_page_params"]) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, logs) + end + end + + describe "/transactions/{transaction_hash}/token-transfers" do + test "return 404 on non existing transaction", %{conn: conn} do + transaction = build(:transaction) + request = get(conn, "/api/v2/transactions/#{to_string(transaction.hash)}/token-transfers") + + assert %{"message" => "Not found"} = json_response(request, 404) + end + + test "return 422 on invalid transaction hash", %{conn: conn} do + request = get(conn, "/api/v2/transactions/0x/token-transfers") + + assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + end + + test "return empty list", %{conn: conn} do + transaction = + :transaction + |> insert() + |> with_block() + + request = get(conn, "/api/v2/transactions/#{to_string(transaction.hash)}/token-transfers") + + assert response = json_response(request, 200) + assert response["items"] == [] + assert response["next_page_params"] == nil + end + + test "return relevant token transfer", %{conn: conn} do + transaction = + :transaction + |> insert() + |> with_block() + + token_transfer = + insert(:token_transfer, + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number + ) + + transaction_1 = + :transaction + |> insert() + |> with_block() + + insert_list(6, :token_transfer, + transaction: transaction_1, + block: transaction_1.block, + block_number: transaction_1.block_number + ) + + request = get(conn, "/api/v2/transactions/#{to_string(transaction.hash)}/token-transfers") + + assert response = json_response(request, 200) + assert Enum.count(response["items"]) == 1 + assert response["next_page_params"] == nil + compare_item(token_transfer, Enum.at(response["items"], 0)) + end + + test "return list with next_page_params", %{conn: conn} do + transaction = + :transaction + |> insert() + |> with_block() + + token_transfers = + insert_list(51, :token_transfer, + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number + ) + |> Enum.reverse() + + request = get(conn, "/api/v2/transactions/#{to_string(transaction.hash)}/token-transfers") + assert response = json_response(request, 200) + + request_2nd_page = + get(conn, "/api/v2/transactions/#{to_string(transaction.hash)}/token-transfers", response["next_page_params"]) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, token_transfers) + end + + test "check filters", %{conn: conn} do + transaction = + :transaction + |> insert() + |> with_block() + + erc_1155_token = insert(:token, type: "ERC-1155") + + erc_1155_tt = + for x <- 0..50 do + insert(:token_transfer, + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number, + token_contract_address: erc_1155_token.contract_address, + token_ids: [x], + token_type: "ERC-1155" + ) + end + |> Enum.reverse() + + erc_721_token = insert(:token, type: "ERC-721") + + erc_721_tt = + for x <- 0..50 do + insert(:token_transfer, + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number, + token_contract_address: erc_721_token.contract_address, + token_ids: [x], + token_type: "ERC-721" + ) + end + |> Enum.reverse() + + erc_20_token = insert(:token, type: "ERC-20") + + erc_20_tt = + for _ <- 0..50 do + insert(:token_transfer, + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number, + token_contract_address: erc_20_token.contract_address, + token_type: "ERC-20" + ) + end + |> Enum.reverse() + + # -- ERC-20 -- + filter = %{"type" => "ERC-20"} + request = get(conn, "/api/v2/transactions/#{to_string(transaction.hash)}/token-transfers", filter) + assert response = json_response(request, 200) + + request_2nd_page = + get( + conn, + "/api/v2/transactions/#{to_string(transaction.hash)}/token-transfers", + Map.merge(response["next_page_params"], filter) + ) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, erc_20_tt) + # -- ------ -- + + # -- ERC-721 -- + filter = %{"type" => "ERC-721"} + request = get(conn, "/api/v2/transactions/#{to_string(transaction.hash)}/token-transfers", filter) + assert response = json_response(request, 200) + + request_2nd_page = + get( + conn, + "/api/v2/transactions/#{to_string(transaction.hash)}/token-transfers", + Map.merge(response["next_page_params"], filter) + ) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, erc_721_tt) + # -- ------ -- + + # -- ERC-1155 -- + filter = %{"type" => "ERC-1155"} + request = get(conn, "/api/v2/transactions/#{to_string(transaction.hash)}/token-transfers", filter) + assert response = json_response(request, 200) + + request_2nd_page = + get( + conn, + "/api/v2/transactions/#{to_string(transaction.hash)}/token-transfers", + Map.merge(response["next_page_params"], filter) + ) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, erc_1155_tt) + # -- ------ -- + + # two filters simultaneously + filter = %{"type" => "ERC-1155,ERC-20"} + request = get(conn, "/api/v2/transactions/#{to_string(transaction.hash)}/token-transfers", filter) + assert response = json_response(request, 200) + + request_2nd_page = + get( + conn, + "/api/v2/transactions/#{to_string(transaction.hash)}/token-transfers", + Map.merge(response["next_page_params"], filter) + ) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + assert Enum.count(response["items"]) == 50 + assert response["next_page_params"] != nil + compare_item(Enum.at(erc_1155_tt, 50), Enum.at(response["items"], 0)) + compare_item(Enum.at(erc_1155_tt, 1), Enum.at(response["items"], 49)) + + assert Enum.count(response_2nd_page["items"]) == 50 + assert response_2nd_page["next_page_params"] != nil + compare_item(Enum.at(erc_1155_tt, 0), Enum.at(response_2nd_page["items"], 0)) + compare_item(Enum.at(erc_20_tt, 50), Enum.at(response_2nd_page["items"], 1)) + compare_item(Enum.at(erc_20_tt, 2), Enum.at(response_2nd_page["items"], 49)) + + request_3rd_page = + get( + conn, + "/api/v2/transactions/#{to_string(transaction.hash)}/token-transfers", + Map.merge(response_2nd_page["next_page_params"], filter) + ) + + assert response_3rd_page = json_response(request_3rd_page, 200) + assert Enum.count(response_3rd_page["items"]) == 2 + assert response_3rd_page["next_page_params"] == nil + compare_item(Enum.at(erc_20_tt, 1), Enum.at(response_3rd_page["items"], 0)) + compare_item(Enum.at(erc_20_tt, 0), Enum.at(response_3rd_page["items"], 1)) + end + + test "check that same token_ids within batch squashes", %{conn: conn} do + token = insert(:token, type: "ERC-1155") + + id = 0 + + insert(:token_instance, token_id: id, token_contract_address_hash: token.contract_address_hash) + + transaction = + :transaction + |> insert() + |> with_block() + + tt = + for _ <- 0..50 do + insert(:token_transfer, + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number, + token_contract_address: token.contract_address, + token_ids: Enum.map(0..50, fn _x -> id end), + token_type: "ERC-1155", + amounts: Enum.map(0..50, fn x -> x end) + ) + end + + token_transfers = + for i <- tt do + %TokenTransfer{i | token_ids: [id], amount: Decimal.new(1275)} + end + + request = get(conn, "/api/v2/transactions/#{to_string(transaction.hash)}/token-transfers") + assert response = json_response(request, 200) + + request_2nd_page = + get(conn, "/api/v2/transactions/#{to_string(transaction.hash)}/token-transfers", response["next_page_params"]) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, Enum.reverse(token_transfers)) + end + + test "check that pagination works for 721 tokens", %{conn: conn} do + token = insert(:token, type: "ERC-721") + + transaction = + :transaction + |> insert() + |> with_block() + + token_transfers = + for i <- 0..50 do + insert(:token_transfer, + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number, + token_contract_address: token.contract_address, + token_ids: [i], + token_type: "ERC-721" + ) + end + + request = get(conn, "/api/v2/transactions/#{to_string(transaction.hash)}/token-transfers") + assert response = json_response(request, 200) + + request_2nd_page = + get(conn, "/api/v2/transactions/#{to_string(transaction.hash)}/token-transfers", response["next_page_params"]) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, Enum.reverse(token_transfers)) + end + + test "check that pagination works fine with 1155 batches #1 (large batch)", %{conn: conn} do + token = insert(:token, type: "ERC-1155") + + transaction = + :transaction + |> insert() + |> with_block() + + tt = + insert(:token_transfer, + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number, + token_contract_address: token.contract_address, + token_ids: Enum.map(0..50, fn x -> x end), + token_type: "ERC-1155", + amounts: Enum.map(0..50, fn x -> x end) + ) + + token_transfers = + for i <- 0..50 do + %TokenTransfer{tt | token_ids: [i], amount: i} + end + + request = get(conn, "/api/v2/transactions/#{to_string(transaction.hash)}/token-transfers") + assert response = json_response(request, 200) + + request_2nd_page = + get(conn, "/api/v2/transactions/#{to_string(transaction.hash)}/token-transfers", response["next_page_params"]) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, token_transfers) + end + + test "check that pagination works fine with 1155 batches #2 some batches on the first page and one on the second", + %{conn: conn} do + token = insert(:token, type: "ERC-1155") + + transaction = + :transaction + |> insert() + |> with_block() + + tt_1 = + insert(:token_transfer, + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number, + token_contract_address: token.contract_address, + token_ids: Enum.map(0..24, fn x -> x end), + token_type: "ERC-1155", + amounts: Enum.map(0..24, fn x -> x end) + ) + + token_transfers_1 = + for i <- 0..24 do + %TokenTransfer{tt_1 | token_ids: [i], amount: i} + end + + tt_2 = + insert(:token_transfer, + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number, + token_contract_address: token.contract_address, + token_ids: Enum.map(25..49, fn x -> x end), + token_type: "ERC-1155", + amounts: Enum.map(25..49, fn x -> x end) + ) + + token_transfers_2 = + for i <- 25..49 do + %TokenTransfer{tt_2 | token_ids: [i], amount: i} + end + + tt_3 = + insert(:token_transfer, + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number, + token_contract_address: token.contract_address, + token_ids: [50], + token_type: "ERC-1155", + amounts: [50] + ) + + request = get(conn, "/api/v2/transactions/#{to_string(transaction.hash)}/token-transfers") + assert response = json_response(request, 200) + + request_2nd_page = + get(conn, "/api/v2/transactions/#{to_string(transaction.hash)}/token-transfers", response["next_page_params"]) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, [tt_3] ++ token_transfers_2 ++ token_transfers_1) + end + + test "check that pagination works fine with 1155 batches #3", %{conn: conn} do + token = insert(:token, type: "ERC-1155") + + transaction = insert(:transaction, input: "0xabcd010203040506") |> with_block() + + tt_1 = + insert(:token_transfer, + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number, + token_contract_address: token.contract_address, + token_ids: Enum.map(0..24, fn x -> x end), + token_type: "ERC-1155", + amounts: Enum.map(0..24, fn x -> x end) + ) + + token_transfers_1 = + for i <- 0..24 do + %TokenTransfer{tt_1 | token_ids: [i], amount: i} + end + + tt_2 = + insert(:token_transfer, + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number, + token_contract_address: token.contract_address, + token_ids: Enum.map(25..50, fn x -> x end), + token_type: "ERC-1155", + amounts: Enum.map(25..50, fn x -> x end) + ) + + token_transfers_2 = + for i <- 25..50 do + %TokenTransfer{tt_2 | token_ids: [i], amount: i} + end + + request = get(conn, "/api/v2/transactions/#{to_string(transaction.hash)}/token-transfers") + assert response = json_response(request, 200) + + request_2nd_page = + get(conn, "/api/v2/transactions/#{to_string(transaction.hash)}/token-transfers", response["next_page_params"]) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, token_transfers_2 ++ token_transfers_1) + end + end + + describe "/transactions/{transaction_hash}/state-changes" do + test "return 404 on non existing transaction", %{conn: conn} do + transaction = build(:transaction) + request = get(conn, "/api/v2/transactions/#{to_string(transaction.hash)}/state-changes") + + assert %{"message" => "Not found"} = json_response(request, 404) + end + + test "return 422 on invalid transaction hash", %{conn: conn} do + request = get(conn, "/api/v2/transactions/0x/state-changes") + + assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + end + + test "return existing transaction", %{conn: conn} do + block_before = insert(:block) + + transaction = + :transaction + |> insert() + |> with_block(status: :ok) + + insert(:address_coin_balance, + address: transaction.from_address, + address_hash: transaction.from_address_hash, + block_number: block_before.number + ) + + insert(:address_coin_balance, + address: transaction.to_address, + address_hash: transaction.to_address_hash, + block_number: block_before.number + ) + + insert(:address_coin_balance, + address: transaction.block.miner, + address_hash: transaction.block.miner_hash, + block_number: block_before.number + ) + + request = get(conn, "/api/v2/transactions/#{to_string(transaction.hash)}/state-changes") + + assert response = json_response(request, 200) + assert Enum.count(response["items"]) == 3 + end + + test "does not include internal transaction with index 0", %{conn: conn} do + block_before = insert(:block) + + transaction = + :transaction + |> insert() + |> with_block(status: :ok) + + internal_transaction_from = insert(:address) + internal_transaction_to = insert(:address) + + insert(:internal_transaction, + transaction: transaction, + index: 0, + block_number: transaction.block_number, + transaction_index: transaction.index, + block_hash: transaction.block_hash, + block_index: 0, + value: %Wei{value: Decimal.new(7)}, + from_address_hash: internal_transaction_from.hash, + from_address: internal_transaction_from, + to_address_hash: internal_transaction_to.hash, + to_address: internal_transaction_to + ) + + insert(:address_coin_balance, + address: transaction.from_address, + address_hash: transaction.from_address_hash, + block_number: block_before.number + ) + + insert(:address_coin_balance, + address: transaction.to_address, + address_hash: transaction.to_address_hash, + block_number: block_before.number + ) + + insert(:address_coin_balance, + address: transaction.block.miner, + address_hash: transaction.block.miner_hash, + block_number: block_before.number + ) + + insert(:address_coin_balance, + address: internal_transaction_from, + address_hash: internal_transaction_from.hash, + block_number: block_before.number + ) + + insert(:address_coin_balance, + address: internal_transaction_to, + address_hash: internal_transaction_to.hash, + block_number: block_before.number + ) + + request = get(conn, "/api/v2/transactions/#{to_string(transaction.hash)}/state-changes") + + assert response = json_response(request, 200) + assert Enum.count(response["items"]) == 3 + end + + test "return entries from internal transaction", %{conn: conn} do + block_before = insert(:block) + + transaction = + :transaction + |> insert() + |> with_block(status: :ok) + + internal_transaction_from = insert(:address) + internal_transaction_to = insert(:address) + + internal_transaction_from_delegatecall = insert(:address) + internal_transaction_to_delegatecall = insert(:address) + + insert(:internal_transaction, + call_type: :call, + transaction: transaction, + index: 0, + block_number: transaction.block_number, + transaction_index: transaction.index, + block_hash: transaction.block_hash, + block_index: 0, + value: %Wei{value: Decimal.new(7)}, + from_address_hash: internal_transaction_from.hash, + from_address: internal_transaction_from, + to_address_hash: internal_transaction_to.hash, + to_address: internal_transaction_to + ) + + # must be ignored, hence we expect only 5 state changes + insert(:internal_transaction, + call_type: :delegatecall, + transaction: transaction, + index: 1, + block_number: transaction.block_number, + transaction_index: transaction.index, + block_hash: transaction.block_hash, + block_index: 1, + value: %Wei{value: Decimal.new(7)}, + from_address_hash: internal_transaction_from_delegatecall.hash, + from_address: internal_transaction_from_delegatecall, + to_address_hash: internal_transaction_to_delegatecall.hash, + to_address: internal_transaction_to_delegatecall + ) + + insert(:internal_transaction, + call_type: :call, + transaction: transaction, + index: 2, + block_number: transaction.block_number, + transaction_index: transaction.index, + block_hash: transaction.block_hash, + block_index: 2, + value: %Wei{value: Decimal.new(7)}, + from_address_hash: internal_transaction_from.hash, + from_address: internal_transaction_from, + to_address_hash: internal_transaction_to.hash, + to_address: internal_transaction_to + ) + + insert(:address_coin_balance, + address: transaction.from_address, + address_hash: transaction.from_address_hash, + block_number: block_before.number, + value: %Wei{value: Decimal.new(1000)} + ) + + insert(:address_coin_balance, + address: transaction.to_address, + address_hash: transaction.to_address_hash, + block_number: block_before.number, + value: %Wei{value: Decimal.new(1000)} + ) + + insert(:address_coin_balance, + address: transaction.block.miner, + address_hash: transaction.block.miner_hash, + block_number: block_before.number, + value: %Wei{value: Decimal.new(1000)} + ) + + insert(:address_coin_balance, + address: internal_transaction_from, + address_hash: internal_transaction_from.hash, + block_number: block_before.number, + value: %Wei{value: Decimal.new(1000)} + ) + + insert(:address_coin_balance, + address: internal_transaction_to, + address_hash: internal_transaction_to.hash, + block_number: block_before.number, + value: %Wei{value: Decimal.new(1000)} + ) + + request = get(conn, "/api/v2/transactions/#{to_string(transaction.hash)}/state-changes") + + assert response = json_response(request, 200) + assert Enum.count(response["items"]) == 5 + end + end + + if Application.compile_env(:explorer, :chain_type) == :celo do + describe "celo gas token" do + test "when gas is paid with token and token is present in db", %{conn: conn} do + token = insert(:token) + + transaction = + :transaction + |> insert(gas_token_contract_address: token.contract_address) + |> with_block() + + request = get(conn, "/api/v2/transactions") + + token_address_hash = Address.checksum(token.contract_address_hash) + token_type = token.type + token_name = token.name + token_symbol = token.symbol + + assert %{ + "items" => [ + %{ + "celo" => %{ + "gas_token" => %{ + "address" => ^token_address_hash, + "name" => ^token_name, + "symbol" => ^token_symbol, + "type" => ^token_type + } + } + } + ] + } = json_response(request, 200) + + request = get(conn, "/api/v2/transactions/#{to_string(transaction.hash)}") + + assert %{ + "celo" => %{ + "gas_token" => %{ + "address" => ^token_address_hash, + "name" => ^token_name, + "symbol" => ^token_symbol, + "type" => ^token_type + } + } + } = json_response(request, 200) + + request = get(conn, "/api/v2/addresses/#{to_string(transaction.from_address_hash)}/transactions") + + assert %{ + "items" => [ + %{ + "celo" => %{ + "gas_token" => %{ + "address" => ^token_address_hash, + "name" => ^token_name, + "symbol" => ^token_symbol, + "type" => ^token_type + } + } + } + ] + } = json_response(request, 200) + + request = get(conn, "/api/v2/main-page/transactions") + + assert [ + %{ + "celo" => %{ + "gas_token" => %{ + "address" => ^token_address_hash, + "name" => ^token_name, + "symbol" => ^token_symbol, + "type" => ^token_type + } + } + } + ] = json_response(request, 200) + end + + test "when gas is paid with token and token is not present in db", %{conn: conn} do + unknown_token_address = insert(:address) + + transaction = + :transaction + |> insert(gas_token_contract_address: unknown_token_address) + |> with_block() + + unknown_token_address_hash = Address.checksum(unknown_token_address.hash) + + request = get(conn, "/api/v2/transactions") + + assert %{ + "items" => [ + %{ + "celo" => %{ + "gas_token" => %{ + "address" => ^unknown_token_address_hash + } + } + } + ] + } = json_response(request, 200) + + request = get(conn, "/api/v2/transactions/#{to_string(transaction.hash)}") + + assert %{ + "celo" => %{ + "gas_token" => %{ + "address" => ^unknown_token_address_hash + } + } + } = json_response(request, 200) + + request = get(conn, "/api/v2/addresses/#{to_string(transaction.from_address_hash)}/transactions") + + assert %{ + "items" => [ + %{ + "celo" => %{ + "gas_token" => %{ + "address" => ^unknown_token_address_hash + } + } + } + ] + } = json_response(request, 200) + + request = get(conn, "/api/v2/main-page/transactions") + + assert [ + %{ + "celo" => %{ + "gas_token" => %{ + "address" => ^unknown_token_address_hash + } + } + } + ] = json_response(request, 200) + end + + test "when gas is paid in native coin", %{conn: conn} do + transaction = :transaction |> insert() |> with_block() + + request = get(conn, "/api/v2/transactions") + + assert %{ + "items" => [ + %{ + "celo" => %{"gas_token" => nil} + } + ] + } = json_response(request, 200) + + request = get(conn, "/api/v2/transactions/#{to_string(transaction.hash)}") + + assert %{ + "celo" => %{"gas_token" => nil} + } = json_response(request, 200) + + request = get(conn, "/api/v2/addresses/#{to_string(transaction.from_address_hash)}/transactions") + + assert %{ + "items" => [ + %{ + "celo" => %{"gas_token" => nil} + } + ] + } = json_response(request, 200) + + request = get(conn, "/api/v2/main-page/transactions") + + assert [ + %{ + "celo" => %{"gas_token" => nil} + } + ] = json_response(request, 200) + end + end + end + + describe "/transactions/{transaction_hash}/raw-trace" do + test "returns raw trace from node", %{conn: conn} do + transaction = + :transaction + |> insert() + |> with_block(status: :ok) + + raw_trace = %{ + "traceAddress" => [], + "type" => "call", + "callType" => "call", + "from" => "0xa931c862e662134b85e4dc4baf5c70cc9ba74db4", + "to" => "0x1469b17ebf82fedf56f04109e5207bdc4554288c", + "gas" => "0x8600", + "gasUsed" => "0x7d37", + "input" => "0xb118e2db0000000000000000000000000000000000000000000000000000000000000008", + "output" => "0x", + "value" => "0x174876e800", + "transactionHash" => to_string(transaction.hash) + } + + expect(EthereumJSONRPC.Mox, :json_rpc, fn _, _ -> {:ok, [raw_trace]} end) + + request = get(conn, "/api/v2/transactions/#{to_string(transaction.hash)}/raw-trace") + + assert response = json_response(request, 200) + assert response == [raw_trace] + end + + test "returns correct error", %{conn: conn} do + transaction = + :transaction + |> insert() + |> with_block(status: :ok) + + expect(EthereumJSONRPC.Mox, :json_rpc, fn _, _ -> {:error, "error"} end) + + request = get(conn, "/api/v2/transactions/#{to_string(transaction.hash)}/raw-trace") + + assert response = json_response(request, 500) + assert response == "Error while raw trace fetching" + end + end + + if Application.compile_env(:explorer, :chain_type) == :stability do + @first_topic_hex_string_1 "0x99e7b0ba56da2819c37c047f0511fd2bf6c9b4e27b4a979a19d6da0f74be8155" + + defp topic(topic_hex_string) do + {:ok, topic} = Explorer.Chain.Hash.Full.cast(topic_hex_string) + topic + end + + describe "stability fees" do + test "check stability fees", %{conn: conn} do + transaction = insert(:transaction) |> with_block() + + _log = + insert(:log, + transaction: transaction, + index: 1, + block: transaction.block, + block_number: transaction.block_number, + first_topic: topic(@first_topic_hex_string_1), + data: + "0x000000000000000000000000dc2b93f3291030f3f7a6d9363ac37757f7ad5c4300000000000000000000000000000000000000000000000000002824369a100000000000000000000000000046b555cb3962bf9533c437cbd04a2f702dfdb999000000000000000000000000000000000000000000000000000014121b4d0800000000000000000000000000faf7a981360c2fab3a5ab7b3d6d8d0cf97a91eb9000000000000000000000000000000000000000000000000000014121b4d0800" + ) + + insert(:token, contract_address: build(:address, hash: "0xDc2B93f3291030F3F7a6D9363ac37757f7AD5C43")) + request = get(conn, "/api/v2/transactions") + + assert %{ + "items" => [ + %{ + "stability_fee" => %{ + "token" => %{"address" => "0xDc2B93f3291030F3F7a6D9363ac37757f7AD5C43"}, + "validator_address" => %{"hash" => "0x46B555CB3962bF9533c437cBD04A2f702dfdB999"}, + "dapp_address" => %{"hash" => "0xFAf7a981360c2FAb3a5Ab7b3D6d8D0Cf97a91Eb9"}, + "total_fee" => "44136000000000", + "dapp_fee" => "22068000000000", + "validator_fee" => "22068000000000" + } + } + ] + } = json_response(request, 200) + + request = get(conn, "/api/v2/transactions/#{to_string(transaction.hash)}") + + assert %{ + "stability_fee" => %{ + "token" => %{"address" => "0xDc2B93f3291030F3F7a6D9363ac37757f7AD5C43"}, + "validator_address" => %{"hash" => "0x46B555CB3962bF9533c437cBD04A2f702dfdB999"}, + "dapp_address" => %{"hash" => "0xFAf7a981360c2FAb3a5Ab7b3D6d8D0Cf97a91Eb9"}, + "total_fee" => "44136000000000", + "dapp_fee" => "22068000000000", + "validator_fee" => "22068000000000" + } + } = json_response(request, 200) + + request = get(conn, "/api/v2/addresses/#{to_string(transaction.from_address_hash)}/transactions") + + assert %{ + "items" => [ + %{ + "stability_fee" => %{ + "token" => %{"address" => "0xDc2B93f3291030F3F7a6D9363ac37757f7AD5C43"}, + "validator_address" => %{"hash" => "0x46B555CB3962bF9533c437cBD04A2f702dfdB999"}, + "dapp_address" => %{"hash" => "0xFAf7a981360c2FAb3a5Ab7b3D6d8D0Cf97a91Eb9"}, + "total_fee" => "44136000000000", + "dapp_fee" => "22068000000000", + "validator_fee" => "22068000000000" + } + } + ] + } = json_response(request, 200) + end + + test "check stability if token absent in DB", %{conn: conn} do + transaction = insert(:transaction) |> with_block() + + _log = + insert(:log, + transaction: transaction, + index: 1, + block: transaction.block, + block_number: transaction.block_number, + first_topic: topic(@first_topic_hex_string_1), + data: + "0x000000000000000000000000dc2b93f3291030f3f7a6d9363ac37757f7ad5c4300000000000000000000000000000000000000000000000000002824369a100000000000000000000000000046b555cb3962bf9533c437cbd04a2f702dfdb999000000000000000000000000000000000000000000000000000014121b4d0800000000000000000000000000faf7a981360c2fab3a5ab7b3d6d8d0cf97a91eb9000000000000000000000000000000000000000000000000000014121b4d0800" + ) + + request = get(conn, "/api/v2/transactions") + + assert %{ + "items" => [ + %{ + "stability_fee" => %{ + "token" => %{"address" => "0xDc2B93f3291030F3F7a6D9363ac37757f7AD5C43"}, + "validator_address" => %{"hash" => "0x46B555CB3962bF9533c437cBD04A2f702dfdB999"}, + "dapp_address" => %{"hash" => "0xFAf7a981360c2FAb3a5Ab7b3D6d8D0Cf97a91Eb9"}, + "total_fee" => "44136000000000", + "dapp_fee" => "22068000000000", + "validator_fee" => "22068000000000" + } + } + ] + } = json_response(request, 200) + + request = get(conn, "/api/v2/transactions/#{to_string(transaction.hash)}") + + assert %{ + "stability_fee" => %{ + "token" => %{"address" => "0xDc2B93f3291030F3F7a6D9363ac37757f7AD5C43"}, + "validator_address" => %{"hash" => "0x46B555CB3962bF9533c437cBD04A2f702dfdB999"}, + "dapp_address" => %{"hash" => "0xFAf7a981360c2FAb3a5Ab7b3D6d8D0Cf97a91Eb9"}, + "total_fee" => "44136000000000", + "dapp_fee" => "22068000000000", + "validator_fee" => "22068000000000" + } + } = json_response(request, 200) + + request = get(conn, "/api/v2/addresses/#{to_string(transaction.from_address_hash)}/transactions") + + assert %{ + "items" => [ + %{ + "stability_fee" => %{ + "token" => %{"address" => "0xDc2B93f3291030F3F7a6D9363ac37757f7AD5C43"}, + "validator_address" => %{"hash" => "0x46B555CB3962bF9533c437cBD04A2f702dfdB999"}, + "dapp_address" => %{"hash" => "0xFAf7a981360c2FAb3a5Ab7b3D6d8D0Cf97a91Eb9"}, + "total_fee" => "44136000000000", + "dapp_fee" => "22068000000000", + "validator_fee" => "22068000000000" + } + } + ] + } = json_response(request, 200) + end + end + end + + defp compare_item(%Transaction{} = transaction, json) do + assert to_string(transaction.hash) == json["hash"] + assert transaction.block_number == json["block_number"] + assert to_string(transaction.value.value) == json["value"] + assert Address.checksum(transaction.from_address_hash) == json["from"]["hash"] + assert Address.checksum(transaction.to_address_hash) == json["to"]["hash"] + end + + defp compare_item(%InternalTransaction{} = internal_transaction, json) do + assert internal_transaction.block_number == json["block_number"] + assert to_string(internal_transaction.gas) == json["gas_limit"] + assert internal_transaction.index == json["index"] + assert to_string(internal_transaction.transaction_hash) == json["transaction_hash"] + assert Address.checksum(internal_transaction.from_address_hash) == json["from"]["hash"] + assert Address.checksum(internal_transaction.to_address_hash) == json["to"]["hash"] + end + + defp compare_item(%Log{} = log, json) do + assert to_string(log.data) == json["data"] + assert log.index == json["index"] + assert Address.checksum(log.address_hash) == json["address"]["hash"] + assert to_string(log.transaction_hash) == json["transaction_hash"] + assert json["block_number"] == log.block_number + end + + defp compare_item(%TokenTransfer{} = token_transfer, json) do + assert Address.checksum(token_transfer.from_address_hash) == json["from"]["hash"] + assert Address.checksum(token_transfer.to_address_hash) == json["to"]["hash"] + assert to_string(token_transfer.transaction_hash) == json["transaction_hash"] + assert json["timestamp"] == nil + assert json["method"] == nil + assert to_string(token_transfer.block_hash) == json["block_hash"] + assert token_transfer.log_index == json["log_index"] + assert check_total(Repo.preload(token_transfer, [{:token, :contract_address}]).token, json["total"], token_transfer) + end + + defp compare_item(%Transaction{} = transaction, json, wl_names) do + assert to_string(transaction.hash) == json["hash"] + assert transaction.block_number == json["block_number"] + assert to_string(transaction.value.value) == json["value"] + assert Address.checksum(transaction.from_address_hash) == json["from"]["hash"] + assert Address.checksum(transaction.to_address_hash) == json["to"]["hash"] + + assert json["to"]["watchlist_names"] == + if(wl_names[transaction.to_address_hash], + do: [ + %{ + "display_name" => wl_names[transaction.to_address_hash], + "label" => wl_names[transaction.to_address_hash] + } + ], + else: [] + ) + + assert json["from"]["watchlist_names"] == + if(wl_names[transaction.from_address_hash], + do: [ + %{ + "display_name" => wl_names[transaction.from_address_hash], + "label" => wl_names[transaction.from_address_hash] + } + ], + else: [] + ) + end + + defp check_paginated_response(first_page_resp, second_page_resp, transactions) do + assert Enum.count(first_page_resp["items"]) == 50 + assert first_page_resp["next_page_params"] != nil + compare_item(Enum.at(transactions, 50), Enum.at(first_page_resp["items"], 0)) + compare_item(Enum.at(transactions, 1), Enum.at(first_page_resp["items"], 49)) + + assert Enum.count(second_page_resp["items"]) == 1 + assert second_page_resp["next_page_params"] == nil + compare_item(Enum.at(transactions, 0), Enum.at(second_page_resp["items"], 0)) + end + + defp check_paginated_response(first_page_resp, second_page_resp, transactions, wl_names) do + assert Enum.count(first_page_resp["items"]) == 50 + assert first_page_resp["next_page_params"] != nil + compare_item(Enum.at(transactions, 50), Enum.at(first_page_resp["items"], 0), wl_names) + compare_item(Enum.at(transactions, 1), Enum.at(first_page_resp["items"], 49), wl_names) + + assert Enum.count(second_page_resp["items"]) == 1 + assert second_page_resp["next_page_params"] == nil + compare_item(Enum.at(transactions, 0), Enum.at(second_page_resp["items"], 0), wl_names) + end + + # with the current implementation no transfers should come with list in totals + defp check_total(%Token{type: nft}, json, _token_transfer) when nft in ["ERC-721", "ERC-1155"] and is_list(json) do + false + end + + defp check_total(%Token{type: nft}, json, token_transfer) when nft in ["ERC-1155"] do + json["token_id"] in Enum.map(token_transfer.token_ids, fn x -> to_string(x) end) and + json["value"] == to_string(token_transfer.amount) + end + + defp check_total(%Token{type: nft}, json, token_transfer) when nft in ["ERC-721"] do + json["token_id"] in Enum.map(token_transfer.token_ids, fn x -> to_string(x) end) + end + + defp check_total(_, _, _), do: true + + if @chain_type == :neon do + describe "neon linked transactions service" do + test "fetches data from the node and caches in the db", %{conn: conn} do + transaction = insert(:transaction) + transaction_hash = to_string(transaction.hash) + + dummy_response = + Enum.map(1..:rand.uniform(10), fn _ -> + :crypto.strong_rand_bytes(64) |> Base.encode64() + end) + + EthereumJSONRPC.Mox + |> expect( + :json_rpc, + fn + %{id: 1, params: [^transaction_hash], method: "neon_getSolanaTransactionByNeonTransaction", jsonrpc: "2.0"}, + _options -> + {:ok, dummy_response} + end + ) + + request = get(conn, "/api/v2/transactions/#{transaction_hash}/external-transactions") + assert response = json_response(request, 200) + assert ^response = dummy_response + + records = + from( + solanaTransaction in Explorer.Chain.Neon.LinkedSolanaTransactions, + where: solanaTransaction.neon_transaction_hash == ^transaction.hash.bytes, + select: solanaTransaction.solana_transaction_hash + ) + |> Repo.all() + + assert length(dummy_response) == length(records) and + Enum.all?(dummy_response, fn dummy -> Enum.member?(records, dummy) end) + + EthereumJSONRPC.Mox + |> expect(:json_rpc, fn _, _ -> {:error, "must use DB cache"} end) + + request = get(conn, "/api/v2/transactions/#{transaction_hash}/external-transactions") + assert response = json_response(request, 200) + + assert length(response) == length(dummy_response) and + Enum.all?(dummy_response, fn dummy -> Enum.member?(response, dummy) end) + end + + test "returns an error when RPC node request fails", %{conn: conn} do + transaction = insert(:transaction) + + EthereumJSONRPC.Mox + |> expect(:json_rpc, fn _, _ -> {:error, "must fail"} end) + + request = get(conn, "/api/v2/transactions/#{to_string(transaction.hash)}/external-transactions") + assert response = json_response(request, 500) + + assert response == %{ + "error" => "Unable to fetch external linked transactions", + "reason" => "\"Unable to fetch data from the node: \\\"must fail\\\"\"" + } + end + + test "returns empty list when RPC returns empty list", %{conn: conn} do + transaction = insert(:transaction) + + EthereumJSONRPC.Mox + |> expect(:json_rpc, fn _, _ -> {:ok, []} end) + + request = get(conn, "/api/v2/transactions/#{to_string(transaction.hash)}/external-transactions") + assert response = json_response(request, 200) + assert ^response = [] + end + + test "returns 422 for invalid transaction hash", %{conn: conn} do + request = get(conn, "/api/v2/transactions/invalid_hash/external-transactions") + assert response = json_response(request, 422) + assert response["message"] == "Invalid parameter(s)" + end + end + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/api/v2/utils_controller_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/api/v2/utils_controller_test.exs new file mode 100644 index 0000000..d9c23fd --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/api/v2/utils_controller_test.exs @@ -0,0 +1,78 @@ +defmodule BlockScoutWeb.API.V2.UtilsControllerTest do + use BlockScoutWeb.ConnCase + + alias Explorer.TestHelper + + describe "/api/v2/utils/decode-calldata" do + test "success decodes calldata", %{conn: conn} do + transaction = + :transaction_to_verified_contract + |> insert() + + TestHelper.get_all_proxies_implementation_zero_addresses() + + assert conn + |> get("/api/v2/utils/decode-calldata", %{ + "calldata" => to_string(transaction.input), + "address_hash" => to_string(transaction.to_address) + }) + |> json_response(200) == + %{ + "result" => %{ + "method_call" => "set(uint256 x)", + "method_id" => "60fe47b1", + "parameters" => [%{"name" => "x", "type" => "uint256", "value" => "50"}] + } + } + + TestHelper.get_all_proxies_implementation_zero_addresses() + + assert conn + |> post("/api/v2/utils/decode-calldata", %{ + "calldata" => to_string(transaction.input), + "address_hash" => to_string(transaction.to_address) + }) + |> json_response(200) == + %{ + "result" => %{ + "method_call" => "set(uint256 x)", + "method_id" => "60fe47b1", + "parameters" => [%{"name" => "x", "type" => "uint256", "value" => "50"}] + } + } + end + + test "return nil in case of failed decoding", %{conn: conn} do + assert conn + |> post("/api/v2/utils/decode-calldata", %{ + "calldata" => "0x010101" + }) + |> json_response(200) == + %{ + "result" => nil + } + end + + test "decodes using ABI from smart_contracts_methods table", %{conn: conn} do + insert(:contract_method) + + input_data = + "set(uint)" + |> ABI.encode([50]) + |> Base.encode16(case: :lower) + + assert conn + |> post("/api/v2/utils/decode-calldata", %{ + "calldata" => "0x" <> input_data + }) + |> json_response(200) == + %{ + "result" => %{ + "method_call" => "set(uint256 x)", + "method_id" => "60fe47b1", + "parameters" => [%{"name" => "x", "type" => "uint256", "value" => "50"}] + } + } + end + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/api/v2/validator_controller_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/api/v2/validator_controller_test.exs new file mode 100644 index 0000000..a0915ce --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/api/v2/validator_controller_test.exs @@ -0,0 +1,295 @@ +defmodule BlockScoutWeb.API.V2.ValidatorControllerTest do + use BlockScoutWeb.ConnCase + use Utils.CompileTimeEnvHelper, chain_type: [:explorer, :chain_type] + + if @chain_type == :stability do + alias Explorer.Chain.Address + alias Explorer.Chain.Cache.Counters.Stability.ValidatorsCount + alias Explorer.Chain.Stability.Validator, as: ValidatorStability + alias Explorer.Helper + + defp check_paginated_response(first_page_resp, second_page_resp, list) do + assert Enum.count(first_page_resp["items"]) == 50 + assert first_page_resp["next_page_params"] != nil + compare_item(Enum.at(list, 50), Enum.at(first_page_resp["items"], 0)) + compare_item(Enum.at(list, 1), Enum.at(first_page_resp["items"], 49)) + + assert Enum.count(second_page_resp["items"]) == 1 + assert second_page_resp["next_page_params"] == nil + compare_item(Enum.at(list, 0), Enum.at(second_page_resp["items"], 0)) + end + + defp compare_default_sorting_for_asc({validator_1, blocks_count_1}, {validator_2, blocks_count_2}) do + case { + Helper.compare(blocks_count_1, blocks_count_2), + Helper.compare( + Keyword.fetch!(ValidatorStability.state_enum(), validator_1.state), + Keyword.fetch!(ValidatorStability.state_enum(), validator_2.state) + ), + Helper.compare(validator_1.address_hash.bytes, validator_2.address_hash.bytes) + } do + {:lt, _, _} -> false + {:eq, :lt, _} -> false + {:eq, :eq, :lt} -> false + _ -> true + end + end + + defp compare_default_sorting_for_desc({validator_1, blocks_count_1}, {validator_2, blocks_count_2}) do + case { + Helper.compare(blocks_count_1, blocks_count_2), + Helper.compare( + Keyword.fetch!(ValidatorStability.state_enum(), validator_1.state), + Keyword.fetch!(ValidatorStability.state_enum(), validator_2.state) + ), + Helper.compare(validator_1.address_hash.bytes, validator_2.address_hash.bytes) + } do + {:gt, _, _} -> false + {:eq, :lt, _} -> false + {:eq, :eq, :lt} -> false + _ -> true + end + end + + defp compare_item(%ValidatorStability{} = validator, json) do + assert Address.checksum(validator.address_hash) == json["address"]["hash"] + assert to_string(validator.state) == json["state"] + end + + defp compare_item({%ValidatorStability{} = validator, count}, json) do + assert json["blocks_validated_count"] == count + 1 + assert compare_item(validator, json) + end + + describe "/validators/stability" do + test "get paginated list of the validators", %{conn: conn} do + validators = + insert_list(51, :validator_stability) + |> Enum.sort_by( + fn validator -> + {Keyword.fetch!(ValidatorStability.state_enum(), validator.state), validator.address_hash.bytes} + end, + :desc + ) + + request = get(conn, "/api/v2/validators/stability") + assert response = json_response(request, 200) + + request_2nd_page = get(conn, "/api/v2/validators/stability", response["next_page_params"]) + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, validators) + end + + test "sort by blocks_validated asc", %{conn: conn} do + validators = + for _ <- 0..50 do + validator = insert(:validator_stability) + blocks_count = Enum.random(0..50) + + _ = + for _ <- 0..blocks_count do + insert(:block, miner_hash: validator.address_hash, miner: nil) + end + + {validator, blocks_count} + end + |> Enum.sort(&compare_default_sorting_for_asc/2) + + init_params = %{"sort" => "blocks_validated", "order" => "asc"} + request = get(conn, "/api/v2/validators/stability", init_params) + assert response = json_response(request, 200) + + request_2nd_page = + get(conn, "/api/v2/validators/stability", Map.merge(init_params, response["next_page_params"])) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, validators) + end + + test "sort by blocks_validated desc", %{conn: conn} do + validators = + for _ <- 0..50 do + validator = insert(:validator_stability) + blocks_count = Enum.random(0..50) + + _ = + for _ <- 0..blocks_count do + insert(:block, miner_hash: validator.address_hash, miner: nil) + end + + {validator, blocks_count} + end + |> Enum.sort(&compare_default_sorting_for_desc/2) + + init_params = %{"sort" => "blocks_validated", "order" => "desc"} + request = get(conn, "/api/v2/validators/stability", init_params) + assert response = json_response(request, 200) + + request_2nd_page = + get(conn, "/api/v2/validators/stability", Map.merge(init_params, response["next_page_params"])) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, validators) + end + + test "state_filter=probation", %{conn: conn} do + insert_list(51, :validator_stability, state: Enum.random([:active, :inactive])) + + validators = + insert_list(51, :validator_stability, state: :probation) + |> Enum.sort_by( + fn validator -> + {Keyword.fetch!(ValidatorStability.state_enum(), validator.state), validator.address_hash.bytes} + end, + :desc + ) + + init_params = %{"state_filter" => "probation"} + + request = get(conn, "/api/v2/validators/stability", init_params) + assert response = json_response(request, 200) + + request_2nd_page = + get(conn, "/api/v2/validators/stability", Map.merge(init_params, response["next_page_params"])) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, validators) + end + end + + describe "/validators/stability/counters" do + test "get counters", %{conn: conn} do + _validator_active1 = + insert(:validator_stability, state: :active, inserted_at: DateTime.add(DateTime.utc_now(), -2, :day)) + + _validator_active2 = insert(:validator_stability, state: :active) + _validator_active3 = insert(:validator_stability, state: :active) + + _validator_inactive1 = + insert(:validator_stability, state: :inactive, inserted_at: DateTime.add(DateTime.utc_now(), -2, :day)) + + _validator_inactive2 = insert(:validator_stability, state: :inactive) + _validator_inactive3 = insert(:validator_stability, state: :inactive) + + _validator_probation1 = + insert(:validator_stability, state: :probation, inserted_at: DateTime.add(DateTime.utc_now(), -2, :day)) + + _validator_probation2 = insert(:validator_stability, state: :probation) + _validator_probation3 = insert(:validator_stability, state: :probation) + + ValidatorsCount.consolidate() + :timer.sleep(500) + + percentage = (3 / 9 * 100) |> Float.floor(2) + request = get(conn, "/api/v2/validators/stability/counters") + + assert %{ + "active_validators_counter" => "3", + "active_validators_percentage" => ^percentage, + "new_validators_counter_24h" => "6", + "validators_counter" => "9" + } = json_response(request, 200) + end + end + end + + if @chain_type == :zilliqa do + alias Explorer.Chain.Zilliqa.Staker + alias Explorer.Chain.Zilliqa.Hash.BLSPublicKey + alias Explorer.Chain.Cache.BlockNumber + + @page_limit 50 + + # A helper to verify the JSON structure for a single validator. + # Adjust the expectations based on what your prepare functions return. + defp check_validator_json(%Staker{} = validator, json) do + assert json["peer_id"] == validator.peer_id + assert json["added_at_block_number"] == validator.added_at_block_number + assert json["stake_updated_at_block_number"] == validator.stake_updated_at_block_number + assert json["control_address"]["hash"] |> String.downcase() == validator.control_address_hash |> to_string() + assert json["reward_address"]["hash"] |> String.downcase() == validator.reward_address_hash |> to_string() + assert json["signing_address"]["hash"] |> String.downcase() == validator.signing_address_hash |> to_string() + end + + describe "GET /api/v2/validators/zilliqa" do + test "returns a paginated list of validators", %{conn: conn} do + total_validators = @page_limit + 1 + # Insert enough validators to force pagination. + for _ <- 1..total_validators do + insert(:zilliqa_staker) + end + + # First page request. + request = get(conn, "/api/v2/validators/zilliqa") + first_page = json_response(request, 200) + + # Verify that the view returns the expected keys. + assert is_list(first_page["items"]) + assert Map.has_key?(first_page, "next_page_params") + + # # Check that the first page contains the page limit number of items. + assert length(first_page["items"]) == @page_limit + + # Second page request using next_page_params. + request = get(conn, "/api/v2/validators/zilliqa", first_page["next_page_params"]) + second_page = json_response(request, 200) + + # Since we inserted one more than the page limit, the second page should have one item + # and no further page. + assert length(second_page["items"]) == total_validators - @page_limit + assert second_page["next_page_params"] == nil + end + end + + test "returns only active stakers", %{conn: conn} do + staker = insert(:zilliqa_staker) + insert(:zilliqa_staker, balance: 0) + insert(:zilliqa_staker, added_at_block_number: 2 ** 31 - 1) + + bls_key = to_string(staker.bls_public_key) + index = staker.index + balance = to_string(staker.balance) + + request = get(conn, "/api/v2/validators/zilliqa", %{"filter" => "active"}) + + assert %{ + "items" => [ + %{ + "bls_public_key" => ^bls_key, + "index" => ^index, + "balance" => ^balance + } + ] + } = json_response(request, 200) + end + + describe "GET /api/v2/validators/zilliqa/:bls_public_key" do + test "returns validator details for a valid BLS public key", %{conn: conn} do + # Insert a validator and get its BLS public key as a string. + validator = insert(:zilliqa_staker) + bls_public_key_str = to_string(validator.bls_public_key) + + conn = get(conn, "/api/v2/validators/zilliqa/#{bls_public_key_str}") + response = json_response(conn, 200) + + # The view for "zilliqa_validator.json" returns a map with extra keys. + assert is_map(response) + check_validator_json(validator, response) + end + + test "returns an error for an invalid BLS public key", %{conn: conn} do + invalid_bls_key = "invalid_key" + + conn = get(conn, "/api/v2/validators/zilliqa/#{invalid_bls_key}") + response = json_response(conn, 400) + + # The controller returns a 400 with a JSON message for an invalid BLS public key. + assert response["message"] == "Invalid bls public key" + end + end + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/api/v2/verification_controller_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/api/v2/verification_controller_test.exs new file mode 100644 index 0000000..2a83fcf --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/api/v2/verification_controller_test.exs @@ -0,0 +1,452 @@ +defmodule BlockScoutWeb.API.V2.VerificationControllerTest do + use BlockScoutWeb.ConnCase + use BlockScoutWeb.ChannelCase, async: false + + alias BlockScoutWeb.V2.UserSocket + alias Explorer.Chain.Address + alias Explorer.TestHelper + alias Tesla.Multipart + alias Plug.Conn + + @moduletag timeout: :infinity + + setup do + configuration = Application.get_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour) + Application.put_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour, enabled: false) + + on_exit(fn -> + Application.put_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour, configuration) + end) + end + + describe "/api/v2/smart-contracts/verification/config" do + test "get cfg", %{conn: conn} do + request = get(conn, "/api/v2/smart-contracts/verification/config") + + assert response = json_response(request, 200) + + assert is_list(response["solidity_evm_versions"]) + assert is_list(response["solidity_compiler_versions"]) + assert is_list(response["vyper_compiler_versions"]) + assert is_list(response["verification_options"]) + assert is_list(response["vyper_evm_versions"]) + assert response["is_rust_verifier_microservice_enabled"] == false + end + end + + if Application.compile_env(:explorer, :chain_type) !== :zksync do + describe "/api/v2/smart-contracts/{address_hash}/verification/via/flattened-code" do + test "get 200 for verified contract", %{conn: conn} do + contract = insert(:smart_contract) + + params = %{"compiler_version" => "", "source_code" => ""} + request = post(conn, "/api/v2/smart-contracts/#{contract.address_hash}/verification/via/flattened-code", params) + + assert %{"message" => "Already verified"} = json_response(request, 200) + end + + test "success verification", %{conn: conn} do + before = Application.get_env(:explorer, :solc_bin_api_url) + + Application.put_env(:explorer, :solc_bin_api_url, "https://solc-bin.ethereum.org") + + path = File.cwd!() <> "/../explorer/test/support/fixture/smart_contract/solidity_0.5.9_smart_contract.sol" + contract = File.read!(path) + + constructor_arguments = + "0000000000000000000000000000000000000000000000000003635c9adc5dea0000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000954657374546f6b656e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005546f6b656e000000000000000000000000000000000000000000000000000000" + + bytecode = + "0x608060405234801561001057600080fd5b50600436106100a95760003560e01c80633177029f116100715780633177029f1461025f57806354fd4d50146102c557806370a082311461034857806395d89b41146103a0578063a9059cbb14610423578063dd62ed3e14610489576100a9565b806306fdde03146100ae578063095ea7b31461013157806318160ddd1461019757806323b872dd146101b5578063313ce5671461023b575b600080fd5b6100b6610501565b6040518080602001828103825283818151815260200191508051906020019080838360005b838110156100f65780820151818401526020810190506100db565b50505050905090810190601f1680156101235780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b61017d6004803603604081101561014757600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff1690602001909291908035906020019092919050505061059f565b604051808215151515815260200191505060405180910390f35b61019f610691565b6040518082815260200191505060405180910390f35b610221600480360360608110156101cb57600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff169060200190929190803573ffffffffffffffffffffffffffffffffffffffff16906020019092919080359060200190929190505050610696565b604051808215151515815260200191505060405180910390f35b61024361090f565b604051808260ff1660ff16815260200191505060405180910390f35b6102ab6004803603604081101561027557600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff16906020019092919080359060200190929190505050610922565b604051808215151515815260200191505060405180910390f35b6102cd610a14565b6040518080602001828103825283818151815260200191508051906020019080838360005b8381101561030d5780820151818401526020810190506102f2565b50505050905090810190601f16801561033a5780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b61038a6004803603602081101561035e57600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff169060200190929190505050610ab2565b6040518082815260200191505060405180910390f35b6103a8610afa565b6040518080602001828103825283818151815260200191508051906020019080838360005b838110156103e85780820151818401526020810190506103cd565b50505050905090810190601f1680156104155780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b61046f6004803603604081101561043957600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff16906020019092919080359060200190929190505050610b98565b604051808215151515815260200191505060405180910390f35b6104eb6004803603604081101561049f57600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff169060200190929190803573ffffffffffffffffffffffffffffffffffffffff169060200190929190505050610cfe565b6040518082815260200191505060405180910390f35b60038054600181600116156101000203166002900480601f0160208091040260200160405190810160405280929190818152602001828054600181600116156101000203166002900480156105975780601f1061056c57610100808354040283529160200191610597565b820191906000526020600020905b81548152906001019060200180831161057a57829003601f168201915b505050505081565b600081600160003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055508273ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925846040518082815260200191505060405180910390a36001905092915050565b600090565b6000816000808673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205410158015610762575081600160008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205410155b801561076e5750600082115b1561090357816000808573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008282540192505081905550816000808673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000206000828254039250508190555081600160008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825403925050819055508273ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef846040518082815260200191505060405180910390a360019050610908565b600090505b9392505050565b600460009054906101000a900460ff1681565b600081600160003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055508273ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925846040518082815260200191505060405180910390a36001905092915050565b60068054600181600116156101000203166002900480601f016020809104026020016040519081016040528092919081815260200182805460018160011615610100020316600290048015610aaa5780601f10610a7f57610100808354040283529160200191610aaa565b820191906000526020600020905b815481529060010190602001808311610a8d57829003601f168201915b505050505081565b60008060008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020549050919050565b60058054600181600116156101000203166002900480601f016020809104026020016040519081016040528092919081815260200182805460018160011615610100020316600290048015610b905780601f10610b6557610100808354040283529160200191610b90565b820191906000526020600020905b815481529060010190602001808311610b7357829003601f168201915b505050505081565b6000816000803373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205410158015610be85750600082115b15610cf357816000803373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008282540392505081905550816000808573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825401925050819055508273ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef846040518082815260200191505060405180910390a360019050610cf8565b600090505b92915050565b6000600160008473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205490509291505056fea265627a7a723058202bede3d06720cdf63e8e43fa1d96f228a476cc899ae007bf684e802f2484ce7664736f6c63430005090032" + + input = + "0x60806040526040518060400160405280600381526020017f302e3100000000000000000000000000000000000000000000000000000000008152506006908051906020019062000051929190620001e2565b503480156200005f57600080fd5b506040516200105b3803806200105b833981810160405260808110156200008557600080fd5b81019080805190602001909291908051640100000000811115620000a857600080fd5b82810190506020810184811115620000bf57600080fd5b8151856001820283011164010000000082111715620000dd57600080fd5b50509291906020018051906020019092919080516401000000008111156200010457600080fd5b828101905060208101848111156200011b57600080fd5b81518560018202830111640100000000821117156200013957600080fd5b5050929190505050836000803373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002081905550836002819055508260039080519060200190620001a3929190620001e2565b5081600460006101000a81548160ff021916908360ff1602179055508060059080519060200190620001d7929190620001e2565b505050505062000291565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f106200022557805160ff191683800117855562000256565b8280016001018555821562000256579182015b828111156200025557825182559160200191906001019062000238565b5b50905062000265919062000269565b5090565b6200028e91905b808211156200028a57600081600090555060010162000270565b5090565b90565b610dba80620002a16000396000f3fe608060405234801561001057600080fd5b50600436106100a95760003560e01c80633177029f116100715780633177029f1461025f57806354fd4d50146102c557806370a082311461034857806395d89b41146103a0578063a9059cbb14610423578063dd62ed3e14610489576100a9565b806306fdde03146100ae578063095ea7b31461013157806318160ddd1461019757806323b872dd146101b5578063313ce5671461023b575b600080fd5b6100b6610501565b6040518080602001828103825283818151815260200191508051906020019080838360005b838110156100f65780820151818401526020810190506100db565b50505050905090810190601f1680156101235780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b61017d6004803603604081101561014757600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff1690602001909291908035906020019092919050505061059f565b604051808215151515815260200191505060405180910390f35b61019f610691565b6040518082815260200191505060405180910390f35b610221600480360360608110156101cb57600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff169060200190929190803573ffffffffffffffffffffffffffffffffffffffff16906020019092919080359060200190929190505050610696565b604051808215151515815260200191505060405180910390f35b61024361090f565b604051808260ff1660ff16815260200191505060405180910390f35b6102ab6004803603604081101561027557600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff16906020019092919080359060200190929190505050610922565b604051808215151515815260200191505060405180910390f35b6102cd610a14565b6040518080602001828103825283818151815260200191508051906020019080838360005b8381101561030d5780820151818401526020810190506102f2565b50505050905090810190601f16801561033a5780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b61038a6004803603602081101561035e57600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff169060200190929190505050610ab2565b6040518082815260200191505060405180910390f35b6103a8610afa565b6040518080602001828103825283818151815260200191508051906020019080838360005b838110156103e85780820151818401526020810190506103cd565b50505050905090810190601f1680156104155780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b61046f6004803603604081101561043957600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff16906020019092919080359060200190929190505050610b98565b604051808215151515815260200191505060405180910390f35b6104eb6004803603604081101561049f57600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff169060200190929190803573ffffffffffffffffffffffffffffffffffffffff169060200190929190505050610cfe565b6040518082815260200191505060405180910390f35b60038054600181600116156101000203166002900480601f0160208091040260200160405190810160405280929190818152602001828054600181600116156101000203166002900480156105975780601f1061056c57610100808354040283529160200191610597565b820191906000526020600020905b81548152906001019060200180831161057a57829003601f168201915b505050505081565b600081600160003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055508273ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925846040518082815260200191505060405180910390a36001905092915050565b600090565b6000816000808673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205410158015610762575081600160008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205410155b801561076e5750600082115b1561090357816000808573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008282540192505081905550816000808673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000206000828254039250508190555081600160008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825403925050819055508273ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef846040518082815260200191505060405180910390a360019050610908565b600090505b9392505050565b600460009054906101000a900460ff1681565b600081600160003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055508273ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925846040518082815260200191505060405180910390a36001905092915050565b60068054600181600116156101000203166002900480601f016020809104026020016040519081016040528092919081815260200182805460018160011615610100020316600290048015610aaa5780601f10610a7f57610100808354040283529160200191610aaa565b820191906000526020600020905b815481529060010190602001808311610a8d57829003601f168201915b505050505081565b60008060008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020549050919050565b60058054600181600116156101000203166002900480601f016020809104026020016040519081016040528092919081815260200182805460018160011615610100020316600290048015610b905780601f10610b6557610100808354040283529160200191610b90565b820191906000526020600020905b815481529060010190602001808311610b7357829003601f168201915b505050505081565b6000816000803373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205410158015610be85750600082115b15610cf357816000803373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008282540392505081905550816000808573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825401925050819055508273ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef846040518082815260200191505060405180910390a360019050610cf8565b600090505b92915050565b6000600160008473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205490509291505056fea265627a7a723058202bede3d06720cdf63e8e43fa1d96f228a476cc899ae007bf684e802f2484ce7664736f6c63430005090032" + + contract_address = insert(:contract_address, contract_code: bytecode) + + :transaction + |> insert( + created_contract_address_hash: contract_address.hash, + input: input <> constructor_arguments + ) + |> with_block(status: :ok) + + topic = "addresses:#{contract_address.hash}" + + {:ok, _reply, _socket} = + UserSocket + |> socket("no_id", %{}) + |> subscribe_and_join(topic) + + params = %{ + "source_code" => contract, + "compiler_version" => "v0.5.9+commit.e560f70d", + "evm_version" => "petersburg", + "contract_name" => "TestToken", + "is_optimization_enabled" => false + } + + request = post(conn, "/api/v2/smart-contracts/#{contract_address.hash}/verification/via/flattened-code", params) + + assert %{"message" => "Smart-contract verification started"} = json_response(request, 200) + + assert_receive %Phoenix.Socket.Message{ + payload: %{status: "success"}, + event: "verification_result", + topic: ^topic + }, + :timer.seconds(300) + + Application.put_env(:explorer, :solc_bin_api_url, before) + end + + test "get error on empty contract name", %{conn: conn} do + before = Application.get_env(:explorer, :solc_bin_api_url) + + Application.put_env(:explorer, :solc_bin_api_url, "https://solc-bin.ethereum.org") + + contract_address = insert(:contract_address, contract_code: "0x01") + + :transaction + |> insert( + created_contract_address_hash: contract_address.hash, + input: "0x" + ) + |> with_block(status: :ok) + + topic = "addresses:#{contract_address.hash}" + + {:ok, _reply, _socket} = + UserSocket + |> socket("no_id", %{}) + |> subscribe_and_join(topic) + + params = %{ + "source_code" => "123", + "compiler_version" => "v0.5.9+commit.e560f70d", + "evm_version" => "petersburg", + "contract_name" => "", + "is_optimization_enabled" => false + } + + request = post(conn, "/api/v2/smart-contracts/#{contract_address.hash}/verification/via/flattened-code", params) + + assert %{"message" => "Smart-contract verification started"} = json_response(request, 200) + + assert_receive %Phoenix.Socket.Message{ + payload: %{status: "error", errors: %{name: ["Wrong contract name, please try again."]}}, + event: "verification_result", + topic: ^topic + }, + :timer.seconds(2) + + Application.put_env(:explorer, :solc_bin_api_url, before) + end + end + + describe "/api/v2/smart-contracts/{address_hash}/verification/via/sourcify" do + test "get 200 for verified contract", %{conn: conn} do + contract = insert(:smart_contract) + + params = %{"files" => ""} + request = post(conn, "/api/v2/smart-contracts/#{contract.address_hash}/verification/via/sourcify", params) + + assert %{"message" => "Already verified"} = json_response(request, 200) + end + + test "verify contract from sourcify repo", %{conn: conn} do + address = "0xf26594F585De4EB0Ae9De865d9053FEe02ac6eF1" + + _contract = insert(:address, hash: address, contract_code: "0x01") + + topic = "addresses:#{String.downcase(address)}" + + {:ok, _reply, _socket} = + UserSocket + |> socket("no_id", %{}) + |> subscribe_and_join(topic) + + multipart = + Multipart.new() + |> Multipart.add_file_content("content", "name.json", + name: "files[0]", + headers: [{"content-type", "application/json"}] + ) + + body = + multipart + |> Multipart.body() + |> Enum.to_list() + |> to_str() + + [{name, value}] = Multipart.headers(multipart) + + request = + post( + conn + |> Plug.Conn.put_req_header( + name, + value + ), + "/api/v2/smart-contracts/#{address}/verification/via/sourcify", + body + ) + + assert %{"message" => "Smart-contract verification started"} = json_response(request, 200) + + assert_receive %Phoenix.Socket.Message{ + payload: %{status: "success"}, + event: "verification_result", + topic: ^topic + }, + :timer.seconds(120) + end + end + + describe "/api/v2/smart-contracts/{address_hash}/verification/via/multi-part" do + test "get 404", %{conn: conn} do + contract = insert(:smart_contract) + + params = %{"compiler_version" => "", "files" => ""} + request = post(conn, "/api/v2/smart-contracts/#{contract.address_hash}/verification/via/multi-part", params) + + assert %{"message" => "Not found"} = json_response(request, 404) + end + end + + describe "/api/v2/smart-contracts/{address_hash}/verification/via/vyper-code" do + test "get 200 for verified contract", %{conn: conn} do + contract = insert(:smart_contract) + + params = %{"compiler_version" => "", "source_code" => ""} + request = post(conn, "/api/v2/smart-contracts/#{contract.address_hash}/verification/via/vyper-code", params) + + assert %{"message" => "Already verified"} = json_response(request, 200) + end + + test "success verification", %{conn: conn} do + before = Application.get_env(:explorer, :solc_bin_api_url) + + Application.put_env(:explorer, :solc_bin_api_url, "https://solc-bin.ethereum.org") + + path = File.cwd!() <> "/../explorer/test/support/fixture/smart_contract/vyper.vy" + contract = File.read!(path) + + bytecode = + "0x600436101561000d57610572565b600035601c52600051341561002157600080fd5b63a9059cbb8114156100785760043560a01c1561003d57600080fd5b3361014052600435610160526024356101805261018051610160516101405160065801610578565b6101e0526101e050600160005260206000f35b6323b872dd8114156101195760043560a01c1561009457600080fd5b60243560a01c156100a457600080fd5b60043561014052602435610160526044356101805261018051610160516101405160065801610578565b6101e0526101e050600460043560e05260c052604060c0203360e05260c052604060c02080546044358082101561010457600080fd5b80820390509050815550600160005260206000f35b63095ea7b381141561020a5760043560a01c1561013557600080fd5b600854602435111515156101ad576308c379a061014052602061016052603a610180527f43616e7420417070726f7665206d6f7265207468616e20312528313030204d696101a0527f6c6c696f6e2920546f6b656e7320666f72207472616e736665720000000000006101c05261018050608461015cfd5b60243560043360e05260c052604060c02060043560e05260c052604060c0205560243561014052600435337f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b9256020610140a3600160005260206000f35b6340c10f198114156102c65760043560a01c1561022657600080fd5b600654331461023457600080fd5b60006004351861024357600080fd5b6005805460243581818301101561025957600080fd5b80820190509050815550600360043560e05260c052604060c020805460243581818301101561028757600080fd5b808201905090508155506024356101405260043560007fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef6020610140a3005b6342966c688114156102f45733610140526004356101605261016051610140516006580161074d565b600050005b6379cc679081141561036c5760043560a01c1561031057600080fd5b600460043560e05260c052604060c0203360e05260c052604060c02080546024358082101561033e57600080fd5b80820390509050815550600435610140526024356101605261016051610140516006580161074d565b600050005b6306fdde038114156104115760008060c052602060c020610180602082540161012060006003818352015b826101205160200211156103aa576103cc565b61012051850154610120516020028501525b8151600101808352811415610397575b50505050505061018051806101a001818260206001820306601f82010390500336823750506020610160526040610180510160206001820306601f8201039050610160f35b6395d89b418114156104b65760018060c052602060c020610180602082540161012060006002818352015b8261012051602002111561044f57610471565b61012051850154610120516020028501525b815160010180835281141561043c575b50505050505061018051806101a001818260206001820306601f82010390500336823750506020610160526040610180510160206001820306601f8201039050610160f35b63313ce5678114156104ce5760025460005260206000f35b6370a082318114156105045760043560a01c156104ea57600080fd5b600360043560e05260c052604060c0205460005260206000f35b63dd62ed3e8114156105585760043560a01c1561052057600080fd5b60243560a01c1561053057600080fd5b600460043560e05260c052604060c02060243560e05260c052604060c0205460005260206000f35b6318160ddd8114156105705760055460005260206000f35b505b60006000fd5b6101a0526101405261016052610180526008546101805111151515610601576308c379a06101c05260206101e0526028610200527f5472616e73666572206c696d6974206f6620312528313030204d696c6c696f6e610220527f2920546f6b656e73000000000000000000000000000000000000000000000000610240526102005060846101dcfd5b60036101605160e05260c052604060c020546101805181818301101561062657600080fd5b808201905090506101c0526008546101c051111515156106aa576308c379a06101e052602061020052603a610220527f53696e676c652077616c6c65742063616e6e6f7420686f6c64206d6f72652074610240527f68616e20312528313030204d696c6c696f6e2920546f6b656e73000000000000610260526102205060846101fcfd5b60036101405160e05260c052604060c020805461018051808210156106ce57600080fd5b8082039050905081555060036101605160e05260c052604060c0208054610180518181830110156106fe57600080fd5b80820190509050815550610180516101e05261016051610140517fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef60206101e0a360016000526000516101a051565b6101805261014052610160526000610140511861076957600080fd5b60058054610160518082101561077e57600080fd5b8082039050905081555060036101405160e05260c052604060c020805461016051808210156107ac57600080fd5b80820390509050815550610160516101a0526000610140517fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef60206101a0a36101805156" + + input = + "0x6402540be4006007556305f5e10060085560126002556006610140527f4b6f6f6f706100000000000000000000000000000000000000000000000000006101605261014080600060c052602060c020602082510161012060006002818352015b8261012051602002111561007257610094565b61012051602002850151610120518501555b815160010180835281141561005f575b5050505050506003610140527f4b4f4f00000000000000000000000000000000000000000000000000000000006101605261014080600160c052602060c020602082510161012060006002818352015b826101205160200211156100f757610119565b61012051602002850151610120518501555b81516001018083528114156100e4575b505050505050600754604e6002541061013157600080fd5b600254600a0a808202821582848304141761014b57600080fd5b80905090509050610140526101405160033360e05260c052604060c02055610140516005553360065561014051610160523360007fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef6020610160a361099c56600436101561000d57610572565b600035601c52600051341561002157600080fd5b63a9059cbb8114156100785760043560a01c1561003d57600080fd5b3361014052600435610160526024356101805261018051610160516101405160065801610578565b6101e0526101e050600160005260206000f35b6323b872dd8114156101195760043560a01c1561009457600080fd5b60243560a01c156100a457600080fd5b60043561014052602435610160526044356101805261018051610160516101405160065801610578565b6101e0526101e050600460043560e05260c052604060c0203360e05260c052604060c02080546044358082101561010457600080fd5b80820390509050815550600160005260206000f35b63095ea7b381141561020a5760043560a01c1561013557600080fd5b600854602435111515156101ad576308c379a061014052602061016052603a610180527f43616e7420417070726f7665206d6f7265207468616e20312528313030204d696101a0527f6c6c696f6e2920546f6b656e7320666f72207472616e736665720000000000006101c05261018050608461015cfd5b60243560043360e05260c052604060c02060043560e05260c052604060c0205560243561014052600435337f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b9256020610140a3600160005260206000f35b6340c10f198114156102c65760043560a01c1561022657600080fd5b600654331461023457600080fd5b60006004351861024357600080fd5b6005805460243581818301101561025957600080fd5b80820190509050815550600360043560e05260c052604060c020805460243581818301101561028757600080fd5b808201905090508155506024356101405260043560007fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef6020610140a3005b6342966c688114156102f45733610140526004356101605261016051610140516006580161074d565b600050005b6379cc679081141561036c5760043560a01c1561031057600080fd5b600460043560e05260c052604060c0203360e05260c052604060c02080546024358082101561033e57600080fd5b80820390509050815550600435610140526024356101605261016051610140516006580161074d565b600050005b6306fdde038114156104115760008060c052602060c020610180602082540161012060006003818352015b826101205160200211156103aa576103cc565b61012051850154610120516020028501525b8151600101808352811415610397575b50505050505061018051806101a001818260206001820306601f82010390500336823750506020610160526040610180510160206001820306601f8201039050610160f35b6395d89b418114156104b65760018060c052602060c020610180602082540161012060006002818352015b8261012051602002111561044f57610471565b61012051850154610120516020028501525b815160010180835281141561043c575b50505050505061018051806101a001818260206001820306601f82010390500336823750506020610160526040610180510160206001820306601f8201039050610160f35b63313ce5678114156104ce5760025460005260206000f35b6370a082318114156105045760043560a01c156104ea57600080fd5b600360043560e05260c052604060c0205460005260206000f35b63dd62ed3e8114156105585760043560a01c1561052057600080fd5b60243560a01c1561053057600080fd5b600460043560e05260c052604060c02060243560e05260c052604060c0205460005260206000f35b6318160ddd8114156105705760055460005260206000f35b505b60006000fd5b6101a0526101405261016052610180526008546101805111151515610601576308c379a06101c05260206101e0526028610200527f5472616e73666572206c696d6974206f6620312528313030204d696c6c696f6e610220527f2920546f6b656e73000000000000000000000000000000000000000000000000610240526102005060846101dcfd5b60036101605160e05260c052604060c020546101805181818301101561062657600080fd5b808201905090506101c0526008546101c051111515156106aa576308c379a06101e052602061020052603a610220527f53696e676c652077616c6c65742063616e6e6f7420686f6c64206d6f72652074610240527f68616e20312528313030204d696c6c696f6e2920546f6b656e73000000000000610260526102205060846101fcfd5b60036101405160e05260c052604060c020805461018051808210156106ce57600080fd5b8082039050905081555060036101605160e05260c052604060c0208054610180518181830110156106fe57600080fd5b80820190509050815550610180516101e05261016051610140517fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef60206101e0a360016000526000516101a051565b6101805261014052610160526000610140511861076957600080fd5b60058054610160518082101561077e57600080fd5b8082039050905081555060036101405160e05260c052604060c020805461016051808210156107ac57600080fd5b80820390509050815550610160516101a0526000610140517fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef60206101a0a361018051565b6101ab61099c036101ab6000396101ab61099c036000f3" + + contract_address = insert(:contract_address, contract_code: bytecode) + + :transaction + |> insert( + created_contract_address_hash: contract_address.hash, + input: input + ) + |> with_block(status: :ok) + + topic = "addresses:#{contract_address.hash}" + + {:ok, _reply, _socket} = + UserSocket + |> socket("no_id", %{}) + |> subscribe_and_join(topic) + + params = %{ + "source_code" => contract, + "compiler_version" => "v0.2.12", + "contract_name" => "abc" + } + + request = post(conn, "/api/v2/smart-contracts/#{contract_address.hash}/verification/via/vyper-code", params) + + assert %{"message" => "Smart-contract verification started"} = json_response(request, 200) + + assert_receive %Phoenix.Socket.Message{ + payload: %{status: "success"}, + event: "verification_result", + topic: ^topic + }, + :timer.seconds(300) + + Application.put_env(:explorer, :solc_bin_api_url, before) + end + + test "blueprint contract verification", %{conn: conn} do + bypass = Bypass.open() + + sc_verifier_response = + File.read!( + "./test/support/fixture/smart_contract/smart_contract_verifier_vyper_multi_part_blueprint_response.json" + ) + + old_env = Application.get_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour) + + Application.put_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour, + service_url: "http://localhost:#{bypass.port}", + enabled: true, + type: "sc_verifier", + eth_bytecode_db?: true + ) + + Bypass.expect_once(bypass, "POST", "/api/v2//verifier/vyper/sources%3Averify-multi-part", fn conn -> + Conn.resp(conn, 200, sc_verifier_response) + end) + + bytecode = + "0xfe7100346100235760206100995f395f516001555f5f5561005f61002760003961005f6000f35b5f80fd5f3560e01c60026001821660011b61005b01601e395f51565b63158ef93e81186100535734610057575f5460405260206040f3610053565b633fa4f245811861005357346100575760015460405260206040f35b5f5ffd5b5f80fd0018003784185f810400a16576797065728300030a0013" + + input = + "0x61009c3d81600a3d39f3fe7100346100235760206100995f395f516001555f5f5561005f61002760003961005f6000f35b5f80fd5f3560e01c60026001821660011b61005b01601e395f51565b63158ef93e81186100535734610057575f5460405260206040f3610053565b633fa4f245811861005357346100575760015460405260206040f35b5f5ffd5b5f80fd0018003784185f810400a16576797065728300030a0013" + + contract_address = insert(:contract_address, contract_code: bytecode) + + :transaction + |> insert( + created_contract_address_hash: contract_address.hash, + input: input + ) + |> with_block(status: :ok) + + topic = "addresses:#{contract_address.hash}" + + {:ok, _reply, _socket} = + UserSocket + |> socket("no_id", %{}) + |> subscribe_and_join(topic) + + # We can actually use any params here, as verification service response is defined in `sc_verifier_response` + params = %{ + "source_code" => "some_valid_source_code", + "compiler_version" => "v0.3.10", + "contract_name" => "abc" + } + + request = post(conn, "/api/v2/smart-contracts/#{contract_address.hash}/verification/via/vyper-code", params) + + assert %{"message" => "Smart-contract verification started"} = json_response(request, 200) + + assert_receive %Phoenix.Socket.Message{ + payload: %{status: "success"}, + event: "verification_result", + topic: ^topic + }, + :timer.seconds(300) + + # Assert that the `is_blueprint=true` is stored in the database after verification + TestHelper.get_all_proxies_implementation_zero_addresses() + + request = get(conn, "/api/v2/smart-contracts/#{Address.checksum(contract_address.hash)}") + response = json_response(request, 200) + + assert response["is_blueprint"] == true + + Application.put_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour, old_env) + Bypass.down(bypass) + end + end + + describe "/api/v2/smart-contracts/{address_hash}/verification/via/vyper-multi-part" do + test "get 404", %{conn: conn} do + contract = insert(:smart_contract) + + params = %{"compiler_version" => "", "files" => ""} + + request = + post(conn, "/api/v2/smart-contracts/#{contract.address_hash}/verification/via/vyper-multi-part", params) + + assert %{"message" => "Not found"} = json_response(request, 404) + end + end + end + + describe "/api/v2/smart-contracts/{address_hash}/verification/via/standard-input" do + test "get 200 for verified contract", %{conn: conn} do + contract = insert(:smart_contract) + + params = %{"compiler_version" => "", "files" => ""} + request = post(conn, "/api/v2/smart-contracts/#{contract.address_hash}/verification/via/standard-input", params) + + assert %{"message" => "Already verified"} = json_response(request, 200) + end + + test "success verification", %{conn: conn} do + before = Application.get_env(:explorer, :solc_bin_api_url) + + Application.put_env(:explorer, :solc_bin_api_url, "https://solc-bin.ethereum.org") + + path = File.cwd!() <> "/../explorer/test/support/fixture/smart_contract/standard_input.json" + json = File.read!(path) + + bytecode = + "0x608060405234801561001057600080fd5b50600436106100365760003560e01c8063893d20e81461003b578063a6f9dae11461005a575b600080fd5b600054604080516001600160a01b039092168252519081900360200190f35b61006d61006836600461011e565b61006f565b005b6000546001600160a01b031633146100c35760405162461bcd60e51b815260206004820152601360248201527221b0b63632b91034b9903737ba1037bbb732b960691b604482015260640160405180910390fd5b600080546040516001600160a01b03808516939216917f342827c97908e5e2f71151c08502a66d44b6f758e3ac2f1de95f02eb95f0a73591a3600080546001600160a01b0319166001600160a01b0392909216919091179055565b60006020828403121561013057600080fd5b81356001600160a01b038116811461014757600080fd5b939250505056fea26469706673582212206570365ac95ba46c8d0928763befe51fb6fc8a93499f7dabfda28d18ee673a3e64736f6c63430008110033" + + input = + "0x608060405234801561001057600080fd5b5060405161026438038061026483398101604081905261002f91610076565b600080546001600160a01b0319163390811782556040519091907f342827c97908e5e2f71151c08502a66d44b6f758e3ac2f1de95f02eb95f0a735908290a35050506100d1565b60008060006060848603121561008b57600080fd5b83516001600160701b03811681146100a257600080fd5b60208501519093506001600160a01b03811681146100bf57600080fd5b80925050604084015190509250925092565b610184806100e06000396000f3fe608060405234801561001057600080fd5b50600436106100365760003560e01c8063893d20e81461003b578063a6f9dae11461005a575b600080fd5b600054604080516001600160a01b039092168252519081900360200190f35b61006d61006836600461011e565b61006f565b005b6000546001600160a01b031633146100c35760405162461bcd60e51b815260206004820152601360248201527221b0b63632b91034b9903737ba1037bbb732b960691b604482015260640160405180910390fd5b600080546040516001600160a01b03808516939216917f342827c97908e5e2f71151c08502a66d44b6f758e3ac2f1de95f02eb95f0a73591a3600080546001600160a01b0319166001600160a01b0392909216919091179055565b60006020828403121561013057600080fd5b81356001600160a01b038116811461014757600080fd5b939250505056fea26469706673582212206570365ac95ba46c8d0928763befe51fb6fc8a93499f7dabfda28d18ee673a3e64736f6c6343000811003300000000000000000000000000000000000000000000000000000002d2982db3000000000000000000000000bb36c792b9b45aaf8b848a1392b0d6559202729e666f6f0000000000000000000000000000000000000000000000000000000000" + + contract_address = insert(:contract_address, contract_code: bytecode) + + :transaction + |> insert( + created_contract_address_hash: contract_address.hash, + input: input + ) + |> with_block(status: :ok) + + topic = "addresses:#{contract_address.hash}" + + {:ok, _reply, _socket} = + UserSocket + |> socket("no_id", %{}) + |> subscribe_and_join(topic) + + multipart = + Multipart.new() + |> Multipart.add_field("compiler_version", "v0.8.17+commit.8df45f5f") + |> Multipart.add_file_content(json, "name.json", + name: "files[0]", + headers: [{"content-type", "application/json"}] + ) + + body = + multipart + |> Multipart.body() + |> Enum.to_list() + |> to_str() + + [{name, value}] = Multipart.headers(multipart) + + request = + post( + conn + |> Plug.Conn.put_req_header( + name, + value + ), + "/api/v2/smart-contracts/#{contract_address.hash}/verification/via/standard-input", + body + ) + + assert %{"message" => "Smart-contract verification started"} = json_response(request, 200) + + assert_receive %Phoenix.Socket.Message{ + payload: %{status: "success"}, + event: "verification_result", + topic: ^topic + }, + :timer.seconds(300) + + Application.put_env(:explorer, :solc_bin_api_url, before) + end + end + + defp to_str(list) when is_list(list) do + Enum.reduce(list, "", fn x, acc -> acc <> to_str(x) end) + end + + defp to_str(str) when is_binary(str) do + str + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/api/v2/withdrawal_controller_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/api/v2/withdrawal_controller_test.exs new file mode 100644 index 0000000..88320ec --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/api/v2/withdrawal_controller_test.exs @@ -0,0 +1,66 @@ +defmodule BlockScoutWeb.API.V2.WithdrawalControllerTest do + use BlockScoutWeb.ConnCase + + alias Explorer.Chain.Withdrawal + + describe "/withdrawals" do + test "empty lists", %{conn: conn} do + request = get(conn, "/api/v2/withdrawals") + assert response = json_response(request, 200) + assert response["items"] == [] + assert response["next_page_params"] == nil + end + + test "get withdrawal", %{conn: conn} do + block = insert(:withdrawal) + + request = get(conn, "/api/v2/withdrawals") + + assert response = json_response(request, 200) + assert Enum.count(response["items"]) == 1 + assert response["next_page_params"] == nil + compare_item(block, Enum.at(response["items"], 0)) + end + + test "can paginate", %{conn: conn} do + withdrawals = + 51 + |> insert_list(:withdrawal) + + request = get(conn, "/api/v2/withdrawals") + assert response = json_response(request, 200) + + request_2nd_page = get(conn, "/api/v2/withdrawals", response["next_page_params"]) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, withdrawals) + end + end + + describe "/withdrawals/counters" do + test "fetch counters", %{conn: conn} do + request = get(conn, "/api/v2/withdrawals/counters") + + assert %{ + "withdrawal_count" => _, + "withdrawal_sum" => _ + } = json_response(request, 200) + end + end + + defp compare_item(%Withdrawal{} = withdrawal, json) do + assert withdrawal.index == json["index"] + end + + defp check_paginated_response(first_page_resp, second_page_resp, list) do + assert Enum.count(first_page_resp["items"]) == 50 + assert first_page_resp["next_page_params"] != nil + compare_item(Enum.at(list, 50), Enum.at(first_page_resp["items"], 0)) + compare_item(Enum.at(list, 1), Enum.at(first_page_resp["items"], 49)) + + assert Enum.count(second_page_resp["items"]) == 1 + assert second_page_resp["next_page_params"] == nil + compare_item(Enum.at(list, 0), Enum.at(second_page_resp["items"], 0)) + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/api_docs_controller_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/api_docs_controller_test.exs new file mode 100644 index 0000000..3b2cb99 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/api_docs_controller_test.exs @@ -0,0 +1,18 @@ +defmodule BlockScoutWeb.APIDocsControllerTest do + use BlockScoutWeb.ConnCase + + import BlockScoutWeb.Router.Helpers, only: [api_docs_path: 2] + + describe "GET index/2" do + test "renders documentation tiles for each API module#action", %{conn: conn} do + conn = get(conn, api_docs_path(BlockScoutWeb.Endpoint, :index)) + + documentation = BlockScoutWeb.Etherscan.get_documentation() + + for module <- documentation, action <- module.actions do + assert html_response(conn, 200) =~ action.name + assert html_response(conn, 200) =~ action.description + end + end + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/block_controller_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/block_controller_test.exs new file mode 100644 index 0000000..0ebc98b --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/block_controller_test.exs @@ -0,0 +1,188 @@ +defmodule BlockScoutWeb.BlockControllerTest do + use BlockScoutWeb.ConnCase + alias Explorer.Chain.Block + + setup do + Supervisor.terminate_child(Explorer.Supervisor, Explorer.Chain.Cache.Blocks.child_id()) + Supervisor.restart_child(Explorer.Supervisor, Explorer.Chain.Cache.Blocks.child_id()) + Supervisor.terminate_child(Explorer.Supervisor, Explorer.Chain.Cache.Uncles.child_id()) + Supervisor.restart_child(Explorer.Supervisor, Explorer.Chain.Cache.Uncles.child_id()) + + :ok + end + + describe "GET show/2" do + test "with block redirects to block transactions route", %{conn: conn} do + insert(:block, number: 3) + conn = get(conn, "/blocks/3") + assert redirected_to(conn) =~ "/block/3/transactions" + end + + test "with uncle block redirects to block_hash route", %{conn: conn} do + uncle = insert(:block, consensus: false) + + conn = get(conn, block_path(conn, :show, uncle)) + assert redirected_to(conn) =~ "/block/#{to_string(uncle.hash)}/transactions" + end + end + + describe "GET index/2" do + test "returns all blocks", %{conn: conn} do + 4 + |> insert_list(:block) + |> Stream.map(& &1.number) + |> Enum.reverse() + + conn = get(conn, blocks_path(conn, :index), %{"type" => "JSON"}) + + items = Map.get(json_response(conn, 200), "items") + + assert length(items) == 4 + end + + test "does not include uncles", %{conn: conn} do + blocks = + 4 + |> insert_list(:block) + |> Enum.reverse() + + for index <- 0..3 do + uncle = insert(:block, consensus: false) + insert(:block_second_degree_relation, uncle_hash: uncle.hash, nephew: Enum.at(blocks, index)) + end + + conn = get(conn, blocks_path(conn, :index), %{"type" => "JSON"}) + + items = Map.get(json_response(conn, 200), "items") + + assert length(items) == 4 + end + + test "returns a block with two transactions", %{conn: conn} do + block = insert(:block) + + 2 + |> insert_list(:transaction) + |> with_block(block) + + conn = get(conn, blocks_path(conn, :index), %{"type" => "JSON"}) + + items = Map.get(json_response(conn, 200), "items") + + assert length(items) == 1 + end + + test "returns next page of results based on last seen block", %{conn: conn} do + 50 + |> insert_list(:block) + |> Enum.map(& &1.number) + + block = insert(:block) + + conn = + get(conn, blocks_path(conn, :index), %{ + "type" => "JSON", + "block_number" => Integer.to_string(block.number) + }) + + items = Map.get(json_response(conn, 200), "items") + + assert length(items) == 50 + end + + test "next_page_path exist if not on last page", %{conn: conn} do + %Block{number: number} = + 60 + |> insert_list(:block) + |> Enum.fetch!(10) + + conn = get(conn, blocks_path(conn, :index), %{"type" => "JSON"}) + + expected_path = blocks_path(conn, :index, block_number: number, block_type: "Block", items_count: "50") + + assert Map.get(json_response(conn, 200), "next_page_path") == expected_path + end + + test "next_page_path is empty if on last page", %{conn: conn} do + insert(:block) + + conn = get(conn, blocks_path(conn, :index), %{"type" => "JSON"}) + + refute conn |> json_response(200) |> Map.get("next_page_path") + end + + test "displays miner primary address name", %{conn: conn} do + miner_name = "POA Miner Pool" + %{address: miner_address} = insert(:address_name, name: miner_name, primary: true) + + insert(:block, miner: miner_address, miner_hash: nil) + + conn = get(conn, blocks_path(conn, :index), %{"type" => "JSON"}) + + items = Map.get(json_response(conn, 200), "items") + + assert List.first(items) =~ miner_name + end + end + + describe "GET reorgs/2" do + test "returns all reorgs", %{conn: conn} do + 4 + |> insert_list(:block, consensus: false) + |> Enum.each(fn b -> insert(:block, number: b.number, consensus: true) end) + + conn = get(conn, reorg_path(conn, :reorg), %{"type" => "JSON"}) + + items = Map.get(json_response(conn, 200), "items") + + assert length(items) == 4 + end + + test "does not include blocks or uncles", %{conn: conn} do + 4 + |> insert_list(:block, consensus: false) + |> Enum.each(fn b -> insert(:block, number: b.number, consensus: true) end) + + insert(:block) + uncle = insert(:block, consensus: false) + insert(:block_second_degree_relation, uncle_hash: uncle.hash) + + conn = get(conn, reorg_path(conn, :reorg), %{"type" => "JSON"}) + + items = Map.get(json_response(conn, 200), "items") + + assert length(items) == 4 + end + end + + describe "GET uncle/2" do + test "returns all uncles", %{conn: conn} do + for _index <- 1..4 do + uncle = insert(:block, consensus: false) + insert(:block_second_degree_relation, uncle_hash: uncle.hash) + end + + conn = get(conn, uncle_path(conn, :uncle), %{"type" => "JSON"}) + + items = Map.get(json_response(conn, 200), "items") + + assert length(items) == 4 + end + + test "does not include blocks or reorgs", %{conn: conn} do + for _index <- 1..4 do + uncle = insert(:block, consensus: false) + insert(:block_second_degree_relation, uncle_hash: uncle.hash) + end + + insert(:block) + insert(:block, consensus: false) + + conn = get(conn, uncle_path(conn, :uncle), %{"type" => "JSON"}) + + items = Map.get(json_response(conn, 200), "items") + + assert length(items) == 4 + end + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/block_transaction_controller_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/block_transaction_controller_test.exs new file mode 100644 index 0000000..494dc0d --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/block_transaction_controller_test.exs @@ -0,0 +1,204 @@ +defmodule BlockScoutWeb.BlockTransactionControllerTest do + use BlockScoutWeb.ConnCase + + import BlockScoutWeb.Routers.WebRouter.Helpers, only: [block_transaction_path: 3] + + describe "GET index/2" do + test "with invalid block number", %{conn: conn} do + conn = get(conn, block_transaction_path(conn, :index, "unknown")) + + assert html_response(conn, 404) + end + + test "with valid block number below the tip", %{conn: conn} do + insert(:block, number: 666) + + conn = get(conn, block_transaction_path(conn, :index, "1")) + + assert html_response(conn, 404) =~ "This block has not been processed yet." + end + + test "with valid block number above the tip", %{conn: conn} do + block = insert(:block) + + conn = get(conn, block_transaction_path(conn, :index, block.number + 1)) + + assert html_response(conn, 404) =~ "Easy Cowboy! This block does not exist yet!" + end + + test "returns transactions for the block", %{conn: conn} do + block = insert(:block) + + :transaction + |> insert() + |> with_block(block) + + :transaction + |> insert(to_address: nil) + |> with_block(block) + |> with_contract_creation(insert(:contract_address)) + + conn = get(conn, block_transaction_path(BlockScoutWeb.Endpoint, :index, block), %{type: "JSON"}) + + assert json_response(conn, 200) + + {:ok, %{"items" => items}} = + conn.resp_body + |> Poison.decode() + + assert Enum.count(items) == 2 + end + + test "non-consensus block number without consensus blocks is treated as consensus number above tip", %{conn: conn} do + block = insert(:block, consensus: false) + + transaction = insert(:transaction) + insert(:transaction_fork, hash: transaction.hash, uncle_hash: block.hash) + + conn = get(conn, block_transaction_path(conn, :index, block.number)) + + assert_block_above_tip(conn) + end + + test "non-consensus block number above consensus block number is treated as consensus number above tip", %{ + conn: conn + } do + consensus_block = insert(:block, consensus: true, number: 1) + block = insert(:block, consensus: false, number: consensus_block.number + 1) + + transaction = insert(:transaction) + insert(:transaction_fork, hash: transaction.hash, uncle_hash: block.hash) + + conn = get(conn, block_transaction_path(conn, :index, block.number)) + + assert_block_above_tip(conn) + end + + test "returns transactions for consensus block hash", %{conn: conn} do + block = insert(:block, consensus: true) + + :transaction + |> insert() + |> with_block(block) + + conn = get(conn, block_transaction_path(BlockScoutWeb.Endpoint, :index, block), %{type: "JSON"}) + + assert json_response(conn, 200) + + {:ok, %{"items" => items}} = + conn.resp_body + |> Poison.decode() + + assert Enum.count(items) == 1 + end + + test "does not return transactions for non-consensus block hash", %{conn: conn} do + block = insert(:block, consensus: false) + + transaction = insert(:transaction) + insert(:transaction_fork, hash: transaction.hash, uncle_hash: block.hash) + + conn = get(conn, block_transaction_path(BlockScoutWeb.Endpoint, :index, block), %{type: "JSON"}) + + assert json_response(conn, 200) + + {:ok, %{"items" => items}} = + conn.resp_body + |> Poison.decode() + + assert Enum.empty?(items) + end + + test "does not return transactions for invalid block hash", %{conn: conn} do + conn = get(conn, block_transaction_path(conn, :index, "0x0")) + + assert html_response(conn, 404) + end + + test "with valid not-indexed hash", %{conn: conn} do + conn = get(conn, block_transaction_path(conn, :index, block_hash())) + + assert html_response(conn, 404) =~ "Block not found, please try again later." + end + + test "does not return unrelated transactions", %{conn: conn} do + insert(:transaction) + block = insert(:block) + + conn = get(conn, block_transaction_path(BlockScoutWeb.Endpoint, :index, block), %{type: "JSON"}) + + assert json_response(conn, 200) + + {:ok, %{"items" => items}} = + conn.resp_body + |> Poison.decode() + + assert Enum.empty?(items) + end + + test "does not return related transactions without a block", %{conn: conn} do + block = insert(:block) + insert(:transaction) + + conn = get(conn, block_transaction_path(BlockScoutWeb.Endpoint, :index, block), %{type: "JSON"}) + + assert json_response(conn, 200) + + {:ok, %{"items" => items}} = + conn.resp_body + |> Poison.decode() + + assert Enum.empty?(items) + end + + test "next_page_path exists if not on last page", %{conn: conn} do + block = insert(:block) + + 60 + |> insert_list(:transaction) + |> with_block(block) + + conn = get(conn, block_transaction_path(BlockScoutWeb.Endpoint, :index, block), %{type: "JSON"}) + + {:ok, %{"next_page_path" => next_page_path}} = + conn.resp_body + |> Poison.decode() + + assert next_page_path + end + + test "next_page_path is empty if on last page", %{conn: conn} do + block = insert(:block) + + :transaction + |> insert() + |> with_block(block) + + conn = get(conn, block_transaction_path(BlockScoutWeb.Endpoint, :index, block), %{type: "JSON"}) + + {:ok, %{"next_page_path" => next_page_path}} = + conn.resp_body + |> Poison.decode() + + refute next_page_path + end + + test "displays miner primary address name", %{conn: conn} do + miner_name = "POA Miner Pool" + %{address: miner_address} = insert(:address_name, name: miner_name, primary: true) + + block = insert(:block, miner: miner_address, miner_hash: nil) + + conn = get(conn, block_transaction_path(conn, :index, block)) + assert html_response(conn, 200) =~ miner_name + end + end + + defp assert_block_above_tip(conn) do + assert conn + |> html_response(404) + |> Floki.find(~S|.error-descr|) + |> Floki.text() + |> String.trim() == "Easy Cowboy! This block does not exist yet!" + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/block_withdrawal_controller_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/block_withdrawal_controller_test.exs new file mode 100644 index 0000000..831d088 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/block_withdrawal_controller_test.exs @@ -0,0 +1,139 @@ +defmodule BlockScoutWeb.BlockWithdrawalControllerTest do + use BlockScoutWeb.ConnCase + + import BlockScoutWeb.Routers.WebRouter.Helpers, only: [block_withdrawal_path: 3] + + describe "GET index/2" do + test "with invalid block number", %{conn: conn} do + conn = get(conn, block_withdrawal_path(conn, :index, "unknown")) + + assert html_response(conn, 404) + end + + test "with valid block number below the tip", %{conn: conn} do + insert(:block, number: 666) + + conn = get(conn, block_withdrawal_path(conn, :index, "1")) + + assert html_response(conn, 404) =~ "This block has not been processed yet." + end + + test "with valid block number above the tip", %{conn: conn} do + block = insert(:block) + + conn = get(conn, block_withdrawal_path(conn, :index, block.number + 1)) + + assert_block_above_tip(conn) + end + + test "returns withdrawals for the block", %{conn: conn} do + block = insert(:block, withdrawals: insert_list(3, :withdrawal)) + + # to check that we can render a block overview + get(conn, block_withdrawal_path(BlockScoutWeb.Endpoint, :index, block)) + conn = get(conn, block_withdrawal_path(BlockScoutWeb.Endpoint, :index, block), %{type: "JSON"}) + + assert json_response(conn, 200) + + {:ok, %{"items" => items}} = + conn.resp_body + |> Poison.decode() + + assert Enum.count(items) == 3 + end + + test "non-consensus block number without consensus blocks is treated as consensus number above tip", %{conn: conn} do + block = insert(:block, consensus: false) + + transaction = insert(:transaction) + insert(:transaction_fork, hash: transaction.hash, uncle_hash: block.hash) + + conn = get(conn, block_withdrawal_path(conn, :index, block.number)) + + assert_block_above_tip(conn) + end + + test "non-consensus block number above consensus block number is treated as consensus number above tip", %{ + conn: conn + } do + consensus_block = insert(:block, consensus: true, number: 1) + block = insert(:block, consensus: false, number: consensus_block.number + 1) + + transaction = insert(:transaction) + insert(:transaction_fork, hash: transaction.hash, uncle_hash: block.hash) + + conn = get(conn, block_withdrawal_path(conn, :index, block.number)) + + assert_block_above_tip(conn) + end + + test "does not return transactions for invalid block hash", %{conn: conn} do + conn = get(conn, block_withdrawal_path(conn, :index, "0x0")) + + assert html_response(conn, 404) + end + + test "with valid not-indexed hash", %{conn: conn} do + conn = get(conn, block_withdrawal_path(conn, :index, block_hash())) + + assert html_response(conn, 404) =~ "Block not found, please try again later." + end + + test "does not return unrelated transactions", %{conn: conn} do + insert(:withdrawal) + block = insert(:block) + + conn = get(conn, block_withdrawal_path(BlockScoutWeb.Endpoint, :index, block), %{type: "JSON"}) + + assert json_response(conn, 200) + + {:ok, %{"items" => items}} = + conn.resp_body + |> Poison.decode() + + assert Enum.empty?(items) + end + + test "next_page_path exists if not on last page", %{conn: conn} do + block = insert(:block, withdrawals: insert_list(60, :withdrawal)) + + conn = get(conn, block_withdrawal_path(BlockScoutWeb.Endpoint, :index, block), %{type: "JSON"}) + + {:ok, %{"next_page_path" => next_page_path}} = + conn.resp_body + |> Poison.decode() + + assert next_page_path + end + + test "next_page_path is empty if on last page", %{conn: conn} do + block = insert(:block, withdrawals: insert_list(1, :withdrawal)) + + conn = get(conn, block_withdrawal_path(BlockScoutWeb.Endpoint, :index, block), %{type: "JSON"}) + + {:ok, %{"next_page_path" => next_page_path}} = + conn.resp_body + |> Poison.decode() + + refute next_page_path + end + + test "displays miner primary address name", %{conn: conn} do + miner_name = "POA Miner Pool" + %{address: miner_address} = insert(:address_name, name: miner_name, primary: true) + + block = insert(:block, miner: miner_address, miner_hash: nil) + + conn = get(conn, block_withdrawal_path(conn, :index, block)) + assert html_response(conn, 200) =~ miner_name + end + end + + defp assert_block_above_tip(conn) do + assert conn + |> html_response(404) + |> Floki.find(~S|.error-descr|) + |> Floki.text() + |> String.trim() == "Easy Cowboy! This block does not exist yet!" + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/chain/market_history_chart_controller_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/chain/market_history_chart_controller_test.exs new file mode 100644 index 0000000..e295880 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/chain/market_history_chart_controller_test.exs @@ -0,0 +1,24 @@ +defmodule BlockScoutWeb.Chain.MarketHistoryChartControllerTest do + use BlockScoutWeb.ConnCase + + describe "GET show/2" do + test "returns error when not an ajax request" do + path = market_history_chart_path(BlockScoutWeb.Endpoint, :show) + + conn = get(build_conn(), path) + + assert conn.status == 422 + end + + test "returns ok when request is ajax" do + path = market_history_chart_path(BlockScoutWeb.Endpoint, :show) + + conn = + build_conn() + |> put_req_header("x-requested-with", "xmlhttprequest") + |> get(path) + + assert json_response(conn, 200) + end + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/chain/transaction_history_chart_controller_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/chain/transaction_history_chart_controller_test.exs new file mode 100644 index 0000000..7de3976 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/chain/transaction_history_chart_controller_test.exs @@ -0,0 +1,57 @@ +defmodule BlockScoutWeb.Chain.TransactionHistoryChartControllerTest do + use BlockScoutWeb.ConnCase + + alias BlockScoutWeb.Chain.TransactionHistoryChartController + alias Explorer.Chain.Transaction.History.TransactionStats + alias Explorer.Repo + + describe "GET show/2" do + test "returns error when not an ajax request" do + path = transaction_history_chart_path(BlockScoutWeb.Endpoint, :show) + + conn = get(build_conn(), path) + + assert conn.status == 422 + end + + test "returns ok when request is ajax" do + path = transaction_history_chart_path(BlockScoutWeb.Endpoint, :show) + + conn = + build_conn() + |> put_req_header("x-requested-with", "xmlhttprequest") + |> get(path) + + assert json_response(conn, 200) + end + + test "returns appropriate json data" do + latest = Date.utc_today() + dts = [latest, Date.add(latest, -1), Date.add(latest, -2)] + + some_transaction_stats = [ + %{date: Enum.at(dts, 1), number_of_transactions: 20}, + %{date: Enum.at(dts, 2), number_of_transactions: 30} + ] + + {num, _} = Repo.insert_all(TransactionStats, some_transaction_stats) + assert num == 2 + + conn = + build_conn() + |> put_req_header("x-requested-with", "xmlhttprequest") + |> TransactionHistoryChartController.show([]) + + # turn conn.resp_body into a map using JSON + response_data = Jason.decode!(conn.resp_body, keys: :atoms) + history_data = Jason.decode!(response_data.history_data, keys: :atoms) + + history_data_with_elixir_dates = + Enum.map(history_data, fn stat -> + Map.put(stat, :date, Date.from_iso8601!(stat.date)) + end) + + assert history_data_with_elixir_dates == some_transaction_stats + end + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/chain_controller_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/chain_controller_test.exs new file mode 100644 index 0000000..5cf77df --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/chain_controller_test.exs @@ -0,0 +1,242 @@ +defmodule BlockScoutWeb.ChainControllerTest do + use BlockScoutWeb.ConnCase, + # ETS table is shared in `Explorer.Chain.Cache.Counters.AddressesCount` + async: false + + import BlockScoutWeb.Routers.WebRouter.Helpers, + only: [chain_path: 2, block_path: 3, transaction_path: 3, address_path: 3] + + alias Explorer.Chain.Block + alias Explorer.Chain.Cache.Counters.AddressesCount + + setup do + Supervisor.terminate_child(Explorer.Supervisor, Explorer.Chain.Cache.Blocks.child_id()) + Supervisor.restart_child(Explorer.Supervisor, Explorer.Chain.Cache.Blocks.child_id()) + Supervisor.terminate_child(Explorer.Supervisor, Explorer.Chain.Cache.Uncles.child_id()) + Supervisor.restart_child(Explorer.Supervisor, Explorer.Chain.Cache.Uncles.child_id()) + start_supervised!(AddressesCount) + AddressesCount.consolidate() + + :ok + end + + describe "GET index/2" do + test "returns a welcome message", %{conn: conn} do + conn = get(conn, chain_path(BlockScoutWeb.Endpoint, :show)) + + assert(html_response(conn, 200) =~ "POA") + end + + test "returns a block" do + insert(:block, %{number: 23}) + + conn = + build_conn() + |> put_req_header("x-requested-with", "xmlhttprequest") + |> get("/chain-blocks") + + response = json_response(conn, 200) + + assert(List.first(response["blocks"])["block_number"] == 23) + end + + test "excludes all but the most recent five blocks" do + old_block = insert(:block) + insert_list(5, :block) + + conn = + build_conn() + |> put_req_header("x-requested-with", "xmlhttprequest") + |> get("/chain-blocks") + + response = json_response(conn, 200) + + refute(Enum.member?(response["blocks"], old_block)) + end + + test "displays miner primary address names" do + miner_name = "POA Miner Pool" + %{address: miner_address} = insert(:address_name, name: miner_name, primary: true) + + insert(:block, miner: miner_address, miner_hash: nil) + + conn = + build_conn() + |> put_req_header("x-requested-with", "xmlhttprequest") + |> get("/chain-blocks") + + response = List.first(json_response(conn, 200)["blocks"]) + + assert response["chain_block_html"] =~ miner_name + end + end + + describe "GET token_autocomplete/2" do + test "finds matching tokens" do + insert(:token, name: "MaGiC") + insert(:token, name: "Evil") + + conn = + build_conn() + |> get("/token-autocomplete?q=magic") + + assert Enum.count(json_response(conn, 200)) == 1 + end + + test "finds two matching tokens" do + insert(:token, name: "MaGiC") + insert(:token, name: "magic") + + conn = + build_conn() + |> get("/token-autocomplete?q=magic") + + assert Enum.count(json_response(conn, 200)) == 2 + end + + test "finds verified contract" do + insert(:smart_contract, name: "SuperToken", contract_code_md5: "123") + + conn = + build_conn() + |> get("/token-autocomplete?q=sup") + + assert Enum.count(json_response(conn, 200)) == 1 + end + + test "finds verified contract and token" do + insert(:smart_contract, name: "MagicContract", contract_code_md5: "123") + insert(:token, name: "magicToken") + + conn = + build_conn() + |> get("/token-autocomplete?q=mag") + + assert Enum.count(json_response(conn, 200)) == 2 + end + + test "finds verified contracts and tokens" do + insert(:smart_contract, name: "something", contract_code_md5: "123") + insert(:smart_contract, name: "MagicContract", contract_code_md5: "123") + insert(:token, name: "Magic3") + insert(:smart_contract, name: "magicContract2", contract_code_md5: "123") + insert(:token, name: "magicToken") + insert(:token, name: "OneMoreToken") + + conn = + build_conn() + |> get("/token-autocomplete?q=mag") + + assert Enum.count(json_response(conn, 200)) == 4 + end + + test "find by several words" do + insert(:token, name: "first Token") + insert(:token, name: "second Token") + + conn = + build_conn() + |> get("/token-autocomplete?q=fir+tok") + + assert Enum.count(json_response(conn, 200)) == 1 + end + + test "find by empty query" do + insert(:token, name: "MaGiCt0k3n") + insert(:smart_contract, name: "MagicContract", contract_code_md5: "123") + + conn = + build_conn() + |> get("/token-autocomplete?q=") + + assert Enum.count(json_response(conn, 200)) == 0 + end + + test "find by non-latin characters" do + insert(:token, name: "someToken") + + conn = + build_conn() + |> get("/token-autocomplete?q=%E0%B8%B5%E0%B8%AB") + + assert Enum.count(json_response(conn, 200)) == 0 + end + end + + describe "GET search/2" do + test "finds a consensus block by block number", %{conn: conn} do + insert(:block, number: 37) + conn = get(conn, "/search?q=37") + + assert redirected_to(conn) == block_path(conn, :show, "37") + end + + test "redirects to search results page even for searching non-consensus block by number", %{conn: conn} do + %Block{number: number} = insert(:block, consensus: false) + + conn = get(conn, "/search?q=#{number}") + + assert conn.status == 302 + end + + test "finds non-consensus block by hash", %{conn: conn} do + %Block{hash: hash} = insert(:block, consensus: false) + + conn = get(conn, "/search?q=#{hash}") + + assert redirected_to(conn) == block_path(conn, :show, hash) + end + + test "finds a transaction by hash", %{conn: conn} do + transaction = + :transaction + |> insert() + |> with_block() + + conn = get(conn, "/search?q=#{to_string(transaction.hash)}") + + assert redirected_to(conn) == transaction_path(conn, :show, transaction) + end + + test "finds a transaction by hash when there are not 0x prefix", %{conn: conn} do + transaction = + :transaction + |> insert() + |> with_block() + + "0x" <> non_prefix_hash = to_string(transaction.hash) + + conn = get(conn, "/search?q=#{to_string(non_prefix_hash)}") + + assert redirected_to(conn) == transaction_path(conn, :show, transaction) + end + + test "finds an address by hash", %{conn: conn} do + address = insert(:address) + conn = get(conn, "/search?q=#{to_string(address.hash)}") + + assert redirected_to(conn) == address_path(conn, :show, address) + end + + test "finds an address by hash when there are extra spaces", %{conn: conn} do + address = insert(:address) + conn = get(conn, "/search?q=#{to_string(address.hash)}") + + assert redirected_to(conn) == address_path(conn, :show, address) + end + + test "finds an address by hash when there are not 0x prefix", %{conn: conn} do + address = insert(:address) + "0x" <> non_prefix_hash = to_string(address.hash) + + conn = get(conn, "/search?q=#{to_string(non_prefix_hash)}") + + assert redirected_to(conn) == address_path(conn, :show, address) + end + + test "redirects to result page when it finds nothing", %{conn: conn} do + conn = get(conn, "/search?q=zaphod") + assert conn.status == 302 + end + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/metrics_contoller_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/metrics_contoller_test.exs new file mode 100644 index 0000000..e9ea7e7 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/metrics_contoller_test.exs @@ -0,0 +1,11 @@ +defmodule BlockScoutWeb.MetricsControllerTest do + use BlockScoutWeb.ConnCase + + describe "/metrics page" do + test "renders /metrics page", %{conn: conn} do + conn = get(conn, "/metrics") + + assert text_response(conn, 200) + end + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/page_not_found_controller_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/page_not_found_controller_test.exs new file mode 100644 index 0000000..8540358 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/page_not_found_controller_test.exs @@ -0,0 +1,11 @@ +defmodule BlockScoutWeb.PageNotFoundControllerTest do + use BlockScoutWeb.ConnCase + + describe "GET index/2" do + test "returns 404 status", %{conn: conn} do + conn = get(conn, "/wrong", %{}) + + assert html_response(conn, 404) + end + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/pending_transaction_controller_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/pending_transaction_controller_test.exs new file mode 100644 index 0000000..c8e57dd --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/pending_transaction_controller_test.exs @@ -0,0 +1,84 @@ +defmodule BlockScoutWeb.PendingTransactionControllerTest do + use BlockScoutWeb.ConnCase + alias Explorer.Chain.{Hash, Transaction} + + import BlockScoutWeb.Routers.WebRouter.Helpers, only: [pending_transaction_path: 2, pending_transaction_path: 3] + + describe "GET index/2" do + test "returns no transactions that are in a block", %{conn: conn} do + :transaction + |> insert() + |> with_block() + + conn = get(conn, pending_transaction_path(BlockScoutWeb.Endpoint, :index, %{"type" => "JSON"})) + + assert conn |> json_response(200) |> Map.get("items") |> Enum.empty?() + end + + test "returns pending transactions", %{conn: conn} do + transaction = insert(:transaction) + + conn = get(conn, pending_transaction_path(BlockScoutWeb.Endpoint, :index, %{"type" => "JSON"})) + + assert hd(json_response(conn, 200)["items"]) =~ to_string(transaction.hash) + end + + test "does not show dropped/replaced transactions", %{conn: conn} do + transaction = insert(:transaction) + + dropped_replaced = + :transaction + |> insert(status: 0, error: "dropped/replaced") + |> with_block() + + conn = get(conn, pending_transaction_path(BlockScoutWeb.Endpoint, :index, %{"type" => "JSON"})) + + assert hd(json_response(conn, 200)["items"]) =~ to_string(transaction.hash) + refute hd(json_response(conn, 200)["items"]) =~ to_string(dropped_replaced.hash) + end + + test "works when there are no transactions", %{conn: conn} do + conn = get(conn, pending_transaction_path(conn, :index)) + + assert html_response(conn, 200) + end + + test "returns next page of results based on last seen pending transaction", %{conn: conn} do + second_page_hashes = + 50 + |> insert_list(:transaction) + |> Enum.map(&to_string(&1.hash)) + + %Transaction{inserted_at: inserted_at, hash: hash} = insert(:transaction) + + conn = + get(conn, pending_transaction_path(BlockScoutWeb.Endpoint, :index), %{ + "type" => "JSON", + "inserted_at" => DateTime.to_iso8601(inserted_at), + "hash" => Hash.to_string(hash) + }) + + {:ok, %{"items" => pending_transactions}} = Poison.decode(conn.resp_body) + + assert length(pending_transactions) == length(second_page_hashes) + end + + test "next_page_path exist if not on last page", %{conn: conn} do + 60 + |> insert_list(:transaction) + |> Enum.fetch!(10) + + conn = get(conn, pending_transaction_path(BlockScoutWeb.Endpoint, :index, %{"type" => "JSON"})) + + assert json_response(conn, 200)["next_page_path"] + end + + test "next_page_path is empty if on last page", %{conn: conn} do + insert(:transaction) + + conn = get(conn, pending_transaction_path(BlockScoutWeb.Endpoint, :index, %{"type" => "JSON"})) + + refute json_response(conn, 200)["next_page_path"] + end + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/recent_transactions_controller_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/recent_transactions_controller_test.exs new file mode 100644 index 0000000..e0d2e56 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/recent_transactions_controller_test.exs @@ -0,0 +1,54 @@ +defmodule BlockScoutWeb.RecentTransactionsControllerTest do + use BlockScoutWeb.ConnCase + + import BlockScoutWeb.Routers.WebRouter.Helpers, only: [recent_transactions_path: 2] + + alias Explorer.Chain.Hash + + describe "GET index/2" do + test "returns a transaction", %{conn: conn} do + transaction = + :transaction + |> insert() + |> with_block() + + conn = + conn + |> put_req_header("x-requested-with", "xmlhttprequest") + |> get(recent_transactions_path(conn, :index)) + + assert response = json_response(conn, 200)["transactions"] + + response_hashes = Enum.map(response, & &1["transaction_hash"]) + + assert Enum.member?(response_hashes, Hash.to_string(transaction.hash)) + end + + test "only returns transactions with an associated block", %{conn: conn} do + associated = + :transaction + |> insert() + |> with_block() + + unassociated = insert(:transaction) + + conn = + conn + |> put_req_header("x-requested-with", "xmlhttprequest") + |> get(recent_transactions_path(conn, :index)) + + assert response = json_response(conn, 200)["transactions"] + + response_hashes = Enum.map(response, & &1["transaction_hash"]) + + assert Enum.member?(response_hashes, Hash.to_string(associated.hash)) + refute Enum.member?(response_hashes, Hash.to_string(unassociated.hash)) + end + + test "only responds to ajax requests", %{conn: conn} do + conn = get(conn, recent_transactions_path(conn, :index)) + + assert conn.status == 422 + end + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/smart_contract_controller_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/smart_contract_controller_test.exs new file mode 100644 index 0000000..12bc66f --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/smart_contract_controller_test.exs @@ -0,0 +1,292 @@ +defmodule BlockScoutWeb.SmartContractControllerTest do + use BlockScoutWeb.ConnCase + + import Mox + + alias Explorer.Chain.{Address, Hash} + alias Explorer.{Factory, TestHelper} + + setup :set_mox_from_context + + setup :verify_on_exit! + + describe "GET index/3" do + test "returns not found for nonexistent address" do + nonexistent_address_hash = Hash.to_string(Factory.address_hash()) + path = smart_contract_path(BlockScoutWeb.Endpoint, :index, hash: nonexistent_address_hash) + + conn = + build_conn() + |> put_req_header("x-requested-with", "xmlhttprequest") + |> get(path) + + assert html_response(conn, 404) + end + + test "error for invalid address" do + path = smart_contract_path(BlockScoutWeb.Endpoint, :index, hash: "0x00", type: :regular, action: :read) + + conn = + build_conn() + |> put_req_header("x-requested-with", "xmlhttprequest") + |> get(path) + + assert conn.status == 422 + end + + test "only responds to ajax requests", %{conn: conn} do + smart_contract = insert(:smart_contract, contract_code_md5: "123") + + path = smart_contract_path(BlockScoutWeb.Endpoint, :index, hash: smart_contract.address_hash) + + conn = get(conn, path) + + assert conn.status == 404 + end + + test "lists the smart contract read only functions" do + token_contract_address = insert(:contract_address) + + insert(:smart_contract, address_hash: token_contract_address.hash, contract_code_md5: "123") + + blockchain_get_function_mock() + + path = + smart_contract_path(BlockScoutWeb.Endpoint, :index, + hash: token_contract_address.hash, + type: :regular, + action: :read + ) + + conn = + build_conn() + |> put_req_header("x-requested-with", "xmlhttprequest") + |> get(path) + + assert conn.status == 200 + refute conn.assigns.read_only_functions == [] + end + + test "lists [] proxy read only functions if no verified implementation" do + token_contract_address = insert(:contract_address) + + insert(:smart_contract, + address_hash: token_contract_address.hash, + abi: [ + %{ + "type" => "function", + "stateMutability" => "view", + "payable" => false, + "outputs" => [%{"type" => "address", "name" => ""}], + "name" => "implementation", + "inputs" => [], + "constant" => true + } + ], + contract_code_md5: "123" + ) + + TestHelper.get_all_proxies_implementation_zero_addresses() + + path = + smart_contract_path(BlockScoutWeb.Endpoint, :index, + hash: token_contract_address.hash, + type: :proxy, + action: :read + ) + + conn = + build_conn() + |> put_req_header("x-requested-with", "xmlhttprequest") + |> get(path) + + assert conn.status == 200 + assert conn.assigns.read_only_functions == [] + end + + test "lists [] proxy read only functions if no verified eip-1967 implementation" do + token_contract_address = insert(:contract_address) + + insert(:smart_contract, + address_hash: token_contract_address.hash, + abi: [ + %{ + "type" => "function", + "stateMutability" => "nonpayable", + "payable" => false, + "outputs" => [%{"type" => "address", "name" => "", "internalType" => "address"}], + "name" => "implementation", + "inputs" => [], + "constant" => false + } + ], + contract_code_md5: "123" + ) + + blockchain_get_implementation_mock() + + path = + smart_contract_path(BlockScoutWeb.Endpoint, :index, + hash: token_contract_address.hash, + type: :proxy, + action: :read + ) + + conn = + build_conn() + |> put_req_header("x-requested-with", "xmlhttprequest") + |> get(path) + + assert conn.status == 200 + assert conn.assigns.read_only_functions == [] + end + + test "lists [] proxy read only functions if no verified eip-1967 implementation and eth_getStorageAt returns not normalized address hash" do + token_contract_address = insert(:contract_address) + + insert(:smart_contract, + address_hash: token_contract_address.hash, + abi: [ + %{ + "type" => "function", + "stateMutability" => "nonpayable", + "payable" => false, + "outputs" => [%{"type" => "address", "name" => "", "internalType" => "address"}], + "name" => "implementation", + "inputs" => [], + "constant" => false + } + ], + contract_code_md5: "123" + ) + + blockchain_get_implementation_mock_2() + + path = + smart_contract_path(BlockScoutWeb.Endpoint, :index, + hash: token_contract_address.hash, + type: :proxy, + action: :read + ) + + conn = + build_conn() + |> put_req_header("x-requested-with", "xmlhttprequest") + |> get(path) + + assert conn.status == 200 + assert conn.assigns.read_only_functions == [] + end + end + + describe "GET show/3" do + test "returns not found for nonexistent address" do + nonexistent_address_hash = Address.checksum(Hash.to_string(Factory.address_hash())) + + path = + smart_contract_path( + BlockScoutWeb.Endpoint, + :show, + nonexistent_address_hash, + function_name: "get", + args: [] + ) + + conn = + build_conn() + |> put_req_header("x-requested-with", "xmlhttprequest") + |> get(path) + + assert html_response(conn, 404) + end + + test "error for invalid address" do + path = + smart_contract_path( + BlockScoutWeb.Endpoint, + :show, + "0x00", + function_name: "get", + args: [] + ) + + conn = + build_conn() + |> put_req_header("x-requested-with", "xmlhttprequest") + |> get(path) + + assert conn.status == 422 + end + + test "only responds to ajax requests", %{conn: conn} do + smart_contract = insert(:smart_contract, contract_code_md5: "123") + + path = + smart_contract_path( + BlockScoutWeb.Endpoint, + :show, + Address.checksum(smart_contract.address_hash), + function_name: "get", + args: [] + ) + + conn = get(conn, path) + + assert conn.status == 404 + end + + test "fetch the function value from the blockchain" do + address = insert(:contract_address) + smart_contract = insert(:smart_contract, address_hash: address.hash, contract_code_md5: "123") + + blockchain_get_function_mock() + + path = + smart_contract_path( + BlockScoutWeb.Endpoint, + :show, + Address.checksum(smart_contract.address_hash), + function_name: "get", + method_id: "6d4ce63c", + args_count: 0, + args: [] + ) + + conn = + build_conn() + |> put_req_header("x-requested-with", "xmlhttprequest") + |> get(path) + + assert conn.status == 200 + + assert %{ + function_name: "get", + layout: false, + outputs: [%{"type" => "uint256", "value" => 0}] + } = conn.assigns + end + end + + defp blockchain_get_function_mock do + expect( + EthereumJSONRPC.Mox, + :json_rpc, + fn [%{id: id, method: _, params: [%{data: _, to: _}, _]}], _options -> + {:ok, [%{id: id, jsonrpc: "2.0", result: "0x0000000000000000000000000000000000000000000000000000000000000000"}]} + end + ) + end + + defp blockchain_get_implementation_mock do + EthereumJSONRPC.Mox + |> TestHelper.mock_logic_storage_pointer_request(false, "0xcebb2CCCFe291F0c442841cBE9C1D06EED61Ca02") + end + + defp blockchain_get_implementation_mock_2 do + EthereumJSONRPC.Mox + |> TestHelper.mock_logic_storage_pointer_request( + false, + "0x000000000000000000000000cebb2CCCFe291F0c442841cBE9C1D06EED61Ca02" + ) + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/tokens/holder_controller_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/tokens/holder_controller_test.exs new file mode 100644 index 0000000..45c23d6 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/tokens/holder_controller_test.exs @@ -0,0 +1,93 @@ +defmodule BlockScoutWeb.Tokens.HolderControllerTest do + use BlockScoutWeb.ConnCase, async: true + + alias Explorer.Chain.{Address, Hash} + + describe "GET index/3" do + test "with invalid address hash", %{conn: conn} do + conn = get(conn, token_holder_path(BlockScoutWeb.Endpoint, :index, "invalid_address")) + + assert html_response(conn, 404) + end + + test "with a token that doesn't exist", %{conn: conn} do + address = build(:address) + conn = get(conn, token_holder_path(BlockScoutWeb.Endpoint, :index, address.hash)) + + assert html_response(conn, 404) + end + + test "successfully renders the page", %{conn: conn} do + token = insert(:token) + + insert_list( + 2, + :address_current_token_balance, + token_contract_address_hash: token.contract_address_hash + ) + + conn = + get( + conn, + token_holder_path(BlockScoutWeb.Endpoint, :index, token.contract_address_hash) + ) + + assert html_response(conn, 200) + end + + test "returns next page of results based on last seen token balance", %{conn: conn} do + contract_address = build(:contract_address, hash: "0x6937cb25eb54bc013b9c13c47ab38eb63edd1493") + token = insert(:token, contract_address: contract_address) + + second_page_token_balances = + 1..50 + |> Enum.map( + &insert( + :address_current_token_balance, + token_contract_address_hash: token.contract_address_hash, + value: &1 + 1000 + ) + ) + + token_balance = + insert( + :address_current_token_balance, + token_contract_address_hash: token.contract_address_hash, + value: 50000 + ) + + conn = + get(conn, token_holder_path(conn, :index, token.contract_address_hash), %{ + "value" => Decimal.to_integer(token_balance.value), + "address_hash" => Hash.to_string(token_balance.address_hash), + "type" => "JSON" + }) + + token_balance_tiles = json_response(conn, 200)["items"] + + assert Enum.all?(second_page_token_balances, fn token_balance -> + Enum.any?(token_balance_tiles, fn tile -> + String.contains?(tile, Address.checksum(token_balance.address_hash)) + end) + end) + end + + test "next_page_params exists if not on last page", %{conn: conn} do + contract_address = build(:contract_address, hash: "0x6937cb25eb54bc013b9c13c47ab38eb63edd1493") + token = insert(:token, contract_address: contract_address) + + Enum.each( + 1..51, + &insert( + :address_current_token_balance, + token_contract_address_hash: token.contract_address_hash, + value: &1 + 1000 + ) + ) + + conn = get(conn, token_holder_path(conn, :index, token.contract_address_hash, %{"type" => "JSON"})) + + assert json_response(conn, 200)["next_page_path"] + end + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/tokens/instance/transfer_controller_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/tokens/instance/transfer_controller_test.exs new file mode 100644 index 0000000..d2a3f0e --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/tokens/instance/transfer_controller_test.exs @@ -0,0 +1,28 @@ +defmodule BlockScoutWeb.Tokens.Instance.TransferControllerTest do + use BlockScoutWeb.ConnCase, async: false + + describe "GET token-transfers/2" do + test "fetches the instance", %{conn: conn} do + token = insert(:token, type: "ERC-721") + + contract_address_hash = token.contract_address_hash + + token_id = Decimal.new(10) + + insert(:token_instance, + token_contract_address_hash: contract_address_hash, + token_id: token_id + ) + + conn = get(conn, "/token/#{contract_address_hash}/instance/#{token_id}/token-transfers") + + assert %{ + assigns: %{ + token_instance: %{ + instance: %{token_contract_address_hash: ^contract_address_hash, token_id: ^token_id} + } + } + } = conn + end + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/tokens/instance_controller_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/tokens/instance_controller_test.exs new file mode 100644 index 0000000..65acc5b --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/tokens/instance_controller_test.exs @@ -0,0 +1,26 @@ +defmodule BlockScoutWeb.Tokens.InstanceControllerTest do + use BlockScoutWeb.ConnCase, async: false + + describe "GET show/2" do + test "redirects with valid params", %{conn: conn} do + token = insert(:token, type: "ERC-721") + + contract_address_hash = token.contract_address_hash + + token_id = 10 + + insert(:token_instance, + token_contract_address_hash: contract_address_hash, + token_id: token_id + ) + + conn = get(conn, token_instance_path(BlockScoutWeb.Endpoint, :show, to_string(contract_address_hash), token_id)) + + assert conn.status == 302 + + assert get_resp_header(conn, "location") == [ + "/token/#{contract_address_hash}/instance/#{token_id}/token-transfers" + ] + end + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/tokens/inventory_controller_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/tokens/inventory_controller_test.exs new file mode 100644 index 0000000..e15f855 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/tokens/inventory_controller_test.exs @@ -0,0 +1,141 @@ +defmodule BlockScoutWeb.Tokens.InventoryControllerTest do + use BlockScoutWeb.ConnCase, async: false + + describe "GET index/3" do + test "with invalid address hash", %{conn: conn} do + conn = get(conn, token_inventory_path(conn, :index, "invalid_address")) + + assert html_response(conn, 404) + end + + test "with a token that doesn't exist", %{conn: conn} do + address = build(:address) + conn = get(conn, token_inventory_path(conn, :index, address.hash)) + + assert html_response(conn, 404) + end + + test "successfully renders the page", %{conn: conn} do + token_contract_address = insert(:contract_address) + token = insert(:token, type: "ERC-721", contract_address: token_contract_address) + + transaction = + :transaction + |> insert() + |> with_block() + + insert( + :token_transfer, + transaction: transaction, + token_contract_address: token_contract_address, + token: token + ) + + conn = + get( + conn, + token_inventory_path(conn, :index, token_contract_address.hash) + ) + + assert html_response(conn, 200) + end + + test "returns next page of results based on last seen token balance", %{conn: conn} do + token = insert(:token, type: "ERC-721") + + transaction = + :transaction + |> insert() + |> with_block() + + second_page_token_balances = + Enum.map(1..50, fn i -> + insert( + :token_transfer, + transaction: transaction, + token_contract_address: token.contract_address, + token: token, + token_ids: [i + 1000] + ) + + insert( + :token_instance, + token_contract_address_hash: token.contract_address.hash, + token_id: i + 1000 + ) + end) + + conn = + get(conn, token_inventory_path(conn, :index, token.contract_address_hash), %{ + "token_id" => "999", + "type" => "JSON" + }) + + conn = get(conn, token_inventory_path(conn, :index, token.contract_address_hash), %{type: "JSON"}) + + {:ok, %{"items" => items}} = + conn.resp_body + |> Poison.decode() + + assert Enum.count(items) == Enum.count(second_page_token_balances) + end + + test "next_page_path exists if not on last page", %{conn: conn} do + token = insert(:token, type: "ERC-721") + + transaction = + :transaction + |> insert() + |> with_block() + + Enum.each(1..51, fn i -> + insert( + :token_transfer, + transaction: transaction, + token_contract_address: token.contract_address, + token: token, + token_ids: [i + 1000] + ) + + insert( + :token_instance, + token_contract_address_hash: token.contract_address.hash, + token_id: i + 1000 + ) + end) + + conn = get(conn, token_inventory_path(conn, :index, token.contract_address_hash), %{type: "JSON"}) + + {:ok, %{"next_page_path" => next_page_path}} = + conn.resp_body + |> Poison.decode() + + assert next_page_path + end + + test "next_page_path is empty if on last page", %{conn: conn} do + token = insert(:token, type: "ERC-721") + + transaction = + :transaction + |> insert() + |> with_block() + + insert( + :token_transfer, + transaction: transaction, + token_contract_address: token.contract_address, + token: token, + token_ids: [1000] + ) + + conn = get(conn, token_inventory_path(conn, :index, token.contract_address_hash), %{type: "JSON"}) + + {:ok, %{"next_page_path" => next_page_path}} = + conn.resp_body + |> Poison.decode() + + refute next_page_path + end + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/tokens/read_contract_controller_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/tokens/read_contract_controller_test.exs new file mode 100644 index 0000000..fbebeae --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/tokens/read_contract_controller_test.exs @@ -0,0 +1,68 @@ +defmodule BlockScoutWeb.Tokens.ContractControllerTest do + use BlockScoutWeb.ConnCase, async: false + + import Mox + + alias Explorer.TestHelper + + setup :verify_on_exit! + + describe "GET index/3" do + test "with invalid address hash", %{conn: conn} do + conn = get(conn, token_read_contract_path(BlockScoutWeb.Endpoint, :index, "invalid_address")) + + assert html_response(conn, 404) + end + + test "with unverified address hash returns not found", %{conn: conn} do + address = insert(:address) + + token = insert(:token, contract_address: address) + + transaction = + :transaction + |> insert() + |> with_block() + + insert( + :token_transfer, + to_address: build(:address), + transaction: transaction, + token_contract_address: address, + token: token + ) + + conn = get(conn, token_read_contract_path(BlockScoutWeb.Endpoint, :index, token.contract_address_hash)) + + assert html_response(conn, 404) + end + + test "successfully renders the page when the token is a verified smart contract", %{conn: conn} do + token_contract_address = insert(:contract_address) + + insert(:smart_contract, address_hash: token_contract_address.hash, contract_code_md5: "123") + + token = insert(:token, contract_address: token_contract_address) + + transaction = + :transaction + |> insert() + |> with_block() + + insert( + :token_transfer, + to_address: build(:address), + transaction: transaction, + token_contract_address: token_contract_address, + token: token + ) + + TestHelper.get_all_proxies_implementation_zero_addresses() + + conn = get(conn, token_read_contract_path(BlockScoutWeb.Endpoint, :index, token.contract_address_hash)) + + assert html_response(conn, 200) + assert token.contract_address_hash == conn.assigns.token.contract_address_hash + end + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/tokens/token_controller_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/tokens/token_controller_test.exs new file mode 100644 index 0000000..ab5d93f --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/tokens/token_controller_test.exs @@ -0,0 +1,39 @@ +defmodule BlockScoutWeb.Tokens.TokenControllerTest do + use BlockScoutWeb.ConnCase, async: true + + alias Explorer.Chain.Address + + describe "GET show/2" do + test "redirects to transfers page", %{conn: conn} do + contract_address = insert(:address) + + conn = get(conn, token_path(conn, :show, Address.checksum(contract_address.hash))) + + assert conn.status == 302 + end + end + + # describe "GET token-counters/2" do + # # flaky test + # test "returns token counters", %{conn: conn} do + # contract_address = insert(:address) + + # insert(:token, contract_address: contract_address) + + # token_id = 10 + + # insert(:token_transfer, + # from_address: contract_address, + # token_contract_address: contract_address, + # token_id: token_id + # ) + + # conn = get(conn, "/token-counters", %{"id" => Address.checksum(contract_address.hash)}) + + # assert conn.status == 200 + # {:ok, response} = Jason.decode(conn.resp_body) + + # assert %{"token_holder_count" => 0, "transfer_count" => 1} == response + # end + # end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/transaction_controller_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/transaction_controller_test.exs new file mode 100644 index 0000000..99d09fc --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/transaction_controller_test.exs @@ -0,0 +1,137 @@ +defmodule BlockScoutWeb.TransactionControllerTest do + use BlockScoutWeb.ConnCase + + import BlockScoutWeb.Routers.WebRouter.Helpers, + only: [transaction_path: 3] + + alias Explorer.Chain.Transaction + + describe "GET index/2" do + test "returns a collated transactions", %{conn: conn} do + :transaction + |> insert() + |> with_block() + + conn = get(conn, transaction_path(conn, :index, %{"type" => "JSON"})) + + transactions_html = + conn + |> json_response(200) + |> Map.get("items") + + assert length(transactions_html) == 1 + end + + test "returns a count of transactions", %{conn: conn} do + :transaction + |> insert() + |> with_block() + + conn = get(conn, "/txs") + + assert is_integer(conn.assigns.transaction_estimated_count) + end + + test "excludes pending transactions", %{conn: conn} do + %Transaction{hash: transaction_hash} = + :transaction + |> insert() + |> with_block() + + %Transaction{hash: pending_transaction_hash} = insert(:transaction) + + conn = get(conn, transaction_path(conn, :index, %{"type" => "JSON"})) + + transactions_html = + conn + |> json_response(200) + |> Map.get("items") + + assert Enum.any?(transactions_html, &String.contains?(&1, to_string(transaction_hash))) + refute Enum.any?(transactions_html, &String.contains?(&1, to_string(pending_transaction_hash))) + end + + test "returns next page of results based on last seen transaction", %{conn: conn} do + second_page_hashes = + 50 + |> insert_list(:transaction) + |> with_block() + |> Enum.map(&to_string(&1.hash)) + + %Transaction{block_number: block_number, index: index} = + :transaction + |> insert() + |> with_block() + + conn = + get( + conn, + transaction_path(conn, :index, %{ + "type" => "JSON", + "block_number" => Integer.to_string(block_number), + "index" => Integer.to_string(index) + }) + ) + + transactions_html = + conn + |> json_response(200) + |> Map.get("items") + + assert length(second_page_hashes) == length(transactions_html) + end + + test "next_page_params exist if not on last page", %{conn: conn} do + address = insert(:address) + block = insert(:block) + + 60 + |> insert_list(:transaction, from_address: address) + |> with_block(block) + + conn = get(conn, transaction_path(conn, :index, %{"type" => "JSON"})) + + assert conn |> json_response(200) |> Map.get("next_page_params") + end + + test "next_page_params are empty if on last page", %{conn: conn} do + address = insert(:address) + + :transaction + |> insert(from_address: address) + |> with_block() + + conn = get(conn, transaction_path(conn, :index, %{"type" => "JSON"})) + + refute conn |> json_response(200) |> Map.get("next_page_path") + end + + test "works when there are no transactions", %{conn: conn} do + conn = get(conn, "/txs") + + assert html_response(conn, 200) + end + end + + describe "GET show/3" do + test "responds with 404 with the transaction missing", %{conn: conn} do + hash = transaction_hash() + conn = get(conn, transaction_path(BlockScoutWeb.Endpoint, :show, hash)) + + assert html_response(conn, 404) + end + + test "responds with 422 when the hash is invalid", %{conn: conn} do + conn = get(conn, transaction_path(BlockScoutWeb.Endpoint, :show, "wrong")) + + assert html_response(conn, 422) + end + + test "no redirect from transaction page", %{conn: conn} do + transaction = insert(:transaction) + conn = get(conn, transaction_path(BlockScoutWeb.Endpoint, :show, transaction)) + + assert html_response(conn, 200) + end + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/transaction_internal_transaction_controller_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/transaction_internal_transaction_controller_test.exs new file mode 100644 index 0000000..2d59ad7 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/transaction_internal_transaction_controller_test.exs @@ -0,0 +1,224 @@ +defmodule BlockScoutWeb.TransactionInternalTransactionControllerTest do + use BlockScoutWeb.ConnCase + + import BlockScoutWeb.Routers.WebRouter.Helpers, only: [transaction_internal_transaction_path: 3] + + alias Explorer.Chain.InternalTransaction + alias Explorer.Market.Token + + describe "GET index/3" do + test "with missing transaction", %{conn: conn} do + hash = transaction_hash() + conn = get(conn, transaction_internal_transaction_path(BlockScoutWeb.Endpoint, :index, hash)) + + assert html_response(conn, 404) + end + + test "with invalid transaction hash", %{conn: conn} do + conn = get(conn, transaction_internal_transaction_path(BlockScoutWeb.Endpoint, :index, "nope")) + + assert html_response(conn, 422) + end + + test "includes transaction data", %{conn: conn} do + block = insert(:block, %{number: 777}) + + transaction = + :transaction + |> insert() + |> with_block(block) + + conn = get(conn, transaction_internal_transaction_path(BlockScoutWeb.Endpoint, :index, transaction.hash)) + + assert html_response(conn, 200) + assert conn.assigns.transaction.hash == transaction.hash + end + + test "includes internal transactions for the transaction", %{conn: conn} do + transaction = + :transaction + |> insert() + |> with_block(insert(:block, number: 1)) + + insert(:internal_transaction, + transaction: transaction, + index: 0, + block_number: transaction.block_number, + transaction_index: transaction.index, + block_hash: transaction.block_hash, + block_index: 0 + ) + + insert(:internal_transaction, + transaction: transaction, + index: 1, + transaction_index: transaction.index, + block_number: transaction.block_number, + block_hash: transaction.block_hash, + block_index: 1 + ) + + path = transaction_internal_transaction_path(BlockScoutWeb.Endpoint, :index, transaction.hash) + + conn = get(conn, path, %{type: "JSON"}) + + {:ok, %{"items" => items}} = + conn.resp_body + |> Poison.decode() + + assert json_response(conn, 200) + + # excluding of internal transactions with type=call and index=0 + assert Enum.count(items) == 1 + end + + test "includes USD exchange rate value for address in assigns", %{conn: conn} do + transaction = insert(:transaction) + + conn = get(conn, transaction_internal_transaction_path(BlockScoutWeb.Endpoint, :index, transaction.hash)) + + assert %Token{} = conn.assigns.exchange_rate + end + + test "with no to_address_hash overview contains contract create address", %{conn: conn} do + contract_address = insert(:contract_address) + + transaction = + :transaction + |> insert(to_address: nil) + |> with_contract_creation(contract_address) + |> with_block(insert(:block, number: 7000)) + + internal_transaction = + :internal_transaction_create + |> insert( + transaction: transaction, + index: 0, + block_number: transaction.block_number, + transaction_index: transaction.index, + block_hash: transaction.block_hash, + block_index: 0 + ) + |> with_contract_creation(contract_address) + + conn = + get( + conn, + transaction_internal_transaction_path( + BlockScoutWeb.Endpoint, + :index, + internal_transaction.transaction_hash + ) + ) + + refute is_nil(conn.assigns.transaction.created_contract_address_hash) + end + + test "returns next page of results based on last seen internal transaction", %{conn: conn} do + transaction = + :transaction + |> insert() + |> with_block(insert(:block, number: 7000)) + + %InternalTransaction{index: index} = + insert(:internal_transaction, + transaction: transaction, + index: 0, + block_number: transaction.block_number, + transaction_index: transaction.index, + block_hash: transaction.block_hash, + block_index: 0 + ) + + second_page_indexes = + 1..50 + |> Enum.map(fn index -> + insert(:internal_transaction, + transaction: transaction, + index: index, + block_number: transaction.block_number, + transaction_index: transaction.index, + block_hash: transaction.block_hash, + block_index: index + ) + end) + |> Enum.map(& &1.index) + + conn = + get(conn, transaction_internal_transaction_path(BlockScoutWeb.Endpoint, :index, transaction.hash), %{ + "index" => Integer.to_string(index), + "type" => "JSON" + }) + + {:ok, %{"items" => items}} = + conn.resp_body + |> Poison.decode() + + assert Enum.count(items) == Enum.count(second_page_indexes) + end + + test "next_page_path exists if not on last page", %{conn: conn} do + block = insert(:block, number: 7000) + + transaction = + :transaction + |> insert() + |> with_block(block) + + 1..60 + |> Enum.map(fn index -> + insert( + :internal_transaction, + transaction: transaction, + index: index, + block_number: transaction.block_number, + transaction_index: transaction.index, + block_hash: transaction.block_hash, + block_index: index + ) + end) + + conn = + get(conn, transaction_internal_transaction_path(BlockScoutWeb.Endpoint, :index, transaction.hash), %{ + type: "JSON" + }) + + {:ok, %{"next_page_path" => next_page_path}} = + conn.resp_body + |> Poison.decode() + + assert next_page_path + end + + test "next_page_path is empty if on last page", %{conn: conn} do + transaction = + :transaction + |> insert() + |> with_block(insert(:block, number: 7000)) + + 1..2 + |> Enum.map(fn index -> + insert( + :internal_transaction, + transaction: transaction, + index: index, + block_number: transaction.block_number, + transaction_index: transaction.index, + block_hash: transaction.block_hash, + block_index: index + ) + end) + + conn = + get(conn, transaction_internal_transaction_path(BlockScoutWeb.Endpoint, :index, transaction.hash), %{ + type: "JSON" + }) + + {:ok, %{"next_page_path" => next_page_path}} = + conn.resp_body + |> Poison.decode() + + refute next_page_path + end + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/transaction_log_controller_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/transaction_log_controller_test.exs new file mode 100644 index 0000000..da7d632 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/transaction_log_controller_test.exs @@ -0,0 +1,176 @@ +defmodule BlockScoutWeb.TransactionLogControllerTest do + use BlockScoutWeb.ConnCase + + import BlockScoutWeb.Routers.WebRouter.Helpers, only: [transaction_log_path: 3] + + alias Explorer.Chain.Address + alias Explorer.Market.Token + + describe "GET index/2" do + test "with invalid transaction hash", %{conn: conn} do + conn = get(conn, transaction_log_path(conn, :index, "invalid_transaction_string")) + + assert html_response(conn, 422) + end + + test "with valid transaction hash without transaction", %{conn: conn} do + conn = + get( + conn, + transaction_log_path(conn, :index, "0x3a3eb134e6792ce9403ea4188e5e79693de9e4c94e499db132be086400da79e6") + ) + + assert html_response(conn, 404) + end + + test "returns logs for the transaction", %{conn: conn} do + transaction = + :transaction + |> insert() + |> with_block() + + address = insert(:address) + + insert(:log, + address: address, + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number + ) + + conn = get(conn, transaction_log_path(conn, :index, transaction), %{type: "JSON"}) + + {:ok, %{"items" => items}} = conn.resp_body |> Poison.decode() + first_log = List.first(items) + + assert String.contains?(first_log, Address.checksum(address.hash)) + end + + test "returns logs for the transaction with nil to_address", %{conn: conn} do + transaction = + :transaction + |> insert(to_address: nil) + |> with_block() + + address = insert(:address) + + insert(:log, + address: address, + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number + ) + + conn = get(conn, transaction_log_path(conn, :index, transaction), %{type: "JSON"}) + + {:ok, %{"items" => items}} = conn.resp_body |> Poison.decode() + first_log = List.first(items) + + assert String.contains?(first_log, Address.checksum(address.hash)) + end + + test "assigns no logs when there are none", %{conn: conn} do + transaction = insert(:transaction) + path = transaction_log_path(conn, :index, transaction) + + conn = get(conn, path, %{type: "JSON"}) + + {:ok, %{"items" => items}} = conn.resp_body |> Poison.decode() + + assert Enum.empty?(items) + end + + test "returns next page of results based on last seen transaction log", %{conn: conn} do + transaction = + :transaction + |> insert() + |> with_block() + + log = + insert(:log, + transaction: transaction, + index: 1, + block: transaction.block, + block_number: transaction.block_number + ) + + second_page_indexes = + 2..51 + |> Enum.map(fn index -> + insert(:log, + transaction: transaction, + index: index, + block: transaction.block, + block_number: transaction.block_number + ) + end) + |> Enum.map(& &1.index) + + conn = + get(conn, transaction_log_path(conn, :index, transaction), %{ + "index" => Integer.to_string(log.index), + "type" => "JSON" + }) + + {:ok, %{"items" => items}} = conn.resp_body |> Poison.decode() + + assert Enum.count(items) == Enum.count(second_page_indexes) + end + + test "next_page_path exists if not on last page", %{conn: conn} do + transaction = + :transaction + |> insert() + |> with_block() + + 1..60 + |> Enum.map(fn index -> + insert(:log, + transaction: transaction, + index: index, + block: transaction.block, + block_number: transaction.block_number + ) + end) + + conn = get(conn, transaction_log_path(conn, :index, transaction), %{type: "JSON"}) + + {:ok, %{"next_page_path" => path}} = conn.resp_body |> Poison.decode() + + assert path + end + + test "next_page_path is empty if on last page", %{conn: conn} do + transaction = + :transaction + |> insert() + |> with_block() + + conn = get(conn, transaction_log_path(conn, :index, transaction), %{type: "JSON"}) + + {:ok, %{"next_page_path" => path}} = conn.resp_body |> Poison.decode() + + refute path + end + end + + test "includes USD exchange rate value for address in assigns", %{conn: conn} do + transaction = insert(:transaction) + + conn = get(conn, transaction_log_path(BlockScoutWeb.Endpoint, :index, transaction.hash)) + + assert %Token{} = conn.assigns.exchange_rate + end + + test "loads for transactions that created a contract", %{conn: conn} do + contract_address = insert(:contract_address) + + transaction = + :transaction + |> insert(to_address: nil) + |> with_contract_creation(contract_address) + + conn = get(conn, transaction_log_path(conn, :index, transaction)) + assert html_response(conn, 200) + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/transaction_state_controller_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/transaction_state_controller_test.exs new file mode 100644 index 0000000..3e91da1 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/transaction_state_controller_test.exs @@ -0,0 +1,264 @@ +defmodule BlockScoutWeb.TransactionStateControllerTest do + use BlockScoutWeb.ConnCase + + import Mox + + import BlockScoutWeb.Routers.WebRouter.Helpers, only: [transaction_state_path: 3] + import BlockScoutWeb.WeiHelper, only: [format_wei_value: 2] + import EthereumJSONRPC, only: [integer_to_quantity: 1] + alias Explorer.Chain.Wei + alias Indexer.Fetcher.CoinBalance.Catchup, as: CoinBalanceCatchup + alias Explorer.Chain.Cache.Counters.{AddressesCount, AverageBlockTime} + alias Indexer.Fetcher.OnDemand.CoinBalance, as: CoinBalanceOnDemand + + setup :set_mox_global + + setup :verify_on_exit! + + setup do + mocked_json_rpc_named_arguments = [ + transport: EthereumJSONRPC.Mox, + transport_options: [] + ] + + start_supervised!({Task.Supervisor, name: Indexer.TaskSupervisor}) + start_supervised!(AverageBlockTime) + start_supervised!(AddressesCount) + + configuration = Application.get_env(:indexer, Indexer.Fetcher.OnDemand.CoinBalance.Supervisor) + Application.put_env(:indexer, Indexer.Fetcher.OnDemand.CoinBalance.Supervisor, disabled?: false) + + Application.put_env(:explorer, AverageBlockTime, enabled: true, cache_period: 1_800_000) + + Indexer.Fetcher.OnDemand.CoinBalance.Supervisor.Case.start_supervised!( + json_rpc_named_arguments: mocked_json_rpc_named_arguments + ) + + on_exit(fn -> + Application.put_env(:indexer, Indexer.Fetcher.OnDemand.CoinBalance.Supervisor, configuration) + Application.put_env(:explorer, AverageBlockTime, enabled: false, cache_period: 1_800_000) + end) + end + + describe "GET index/3" do + test "loads existing transaction", %{conn: conn} do + transaction = insert(:transaction) + conn = get(conn, transaction_state_path(conn, :index, transaction.hash)) + + assert html_response(conn, 200) + end + + test "with missing transaction", %{conn: conn} do + hash = transaction_hash() + conn = get(conn, transaction_state_path(BlockScoutWeb.Endpoint, :index, hash)) + + assert html_response(conn, 404) + end + + test "with invalid transaction hash", %{conn: conn} do + conn = get(conn, transaction_state_path(BlockScoutWeb.Endpoint, :index, "nope")) + + assert html_response(conn, 422) + end + + test "with duplicated from, to or miner fields", %{conn: conn} do + address = insert(:address) + to_address = insert(:address) + insert(:block) + block = insert(:block, miner: address) + + insert(:fetched_balance, + address_hash: address.hash, + value: 1_000_000, + block_number: block.number - 1 + ) + + insert(:fetched_balance, + address_hash: to_address.hash, + value: 1_000_000, + block_number: block.number - 1 + ) + + transaction = + insert(:transaction, from_address: address, to_address: to_address) |> with_block(block, status: :ok) + + conn = get(conn, transaction_state_path(conn, :index, transaction), %{type: "JSON"}) + {:ok, %{"items" => items}} = conn.resp_body |> Poison.decode() + + assert(items |> Enum.filter(fn item -> item != nil end) |> length() == 2) + end + + test "returns state changes for the transaction with contract creation", %{conn: conn} do + block = insert(:block) + + contract_address = insert(:contract_address) + + transaction = + :transaction + |> insert(to_address: nil) + |> with_contract_creation(contract_address) + |> with_block(insert(:block)) + + insert(:fetched_balance, + address_hash: transaction.from_address_hash, + value: 1_000_000, + block_number: block.number + ) + + insert(:fetched_balance, + address_hash: transaction.block.miner_hash, + value: 1_000_000, + block_number: block.number + ) + + conn = get(conn, transaction_state_path(conn, :index, transaction), %{type: "JSON"}) + {:ok, %{"items" => items}} = conn.resp_body |> Poison.decode() + + assert(items |> Enum.filter(fn item -> item != nil end) |> length() == 2) + end + + test "returns fetched state changes for the transaction with token transfer", %{conn: conn} do + block = insert(:block) + address_a = insert(:address) + address_b = insert(:address) + token = insert(:token, type: "ERC-20") + + insert(:fetched_balance, + address_hash: address_a.hash, + value: 1_000_000_000_000_000_000, + block_number: block.number + ) + + insert(:fetched_balance, + address_hash: address_b.hash, + value: 2_000_000_000_000_000_000, + block_number: block.number + ) + + transaction = + :transaction + |> insert(from_address: address_a, to_address: address_b, value: 1000) + |> with_block(status: :ok) + + insert(:fetched_balance, + address_hash: transaction.block.miner_hash, + value: 2_500_000, + block_number: block.number + ) + + token_transfer = + insert(:token_transfer, + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number, + token: token, + token_contract_address: token.contract_address + ) + + insert( + :token_balance, + address: token_transfer.from_address, + token: token, + token_contract_address_hash: token.contract_address_hash, + value: 3_000_000, + block_number: block.number + ) + + insert( + :token_balance, + address: token_transfer.to_address, + token: token, + token_contract_address_hash: token.contract_address_hash, + value: 1000, + block_number: block.number + ) + + # to check if we can display transaction overview + get(conn, transaction_state_path(conn, :index, transaction)) + conn = get(conn, transaction_state_path(conn, :index, transaction), %{type: "JSON"}) + + {:ok, %{"items" => items}} = conn.resp_body |> Poison.decode() + full_text = Enum.join(items) + + assert(String.contains?(full_text, format_wei_value(%Wei{value: Decimal.new(1, 1, 18)}, :ether))) + + assert(String.contains?(full_text, format_wei_value(%Wei{value: Decimal.new(1, 2, 18)}, :ether))) + + assert(length(items) == 5) + end + + test "fetch coin balances if needed", %{conn: conn} do + json_rpc_named_arguments = Application.fetch_env!(:indexer, :json_rpc_named_arguments) + CoinBalanceCatchup.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) + + EthereumJSONRPC.Mox + |> stub(:json_rpc, fn + [%{id: id, method: "eth_getBalance", params: _}], _options -> + {:ok, [%{id: id, result: integer_to_quantity(123)}]} + + [%{id: id, method: "eth_getBlockByNumber", params: _}], _options -> + {:ok, + [ + %{ + id: id, + jsonrpc: "2.0", + result: %{ + "author" => "0x0000000000000000000000000000000000000000", + "difficulty" => "0x20000", + "extraData" => "0x", + "gasLimit" => "0x663be0", + "gasUsed" => "0x0", + "hash" => "0x5b28c1bfd3a15230c9a46b399cd0f9a6920d432e85381cc6a140b06e8410112f", + "logsBloom" => + "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "miner" => "0x0000000000000000000000000000000000000000", + "number" => integer_to_quantity(1), + "parentHash" => "0x0000000000000000000000000000000000000000000000000000000000000000", + "receiptsRoot" => "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", + "sealFields" => [ + "0x80", + "0xb8410000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + ], + "sha3Uncles" => "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", + "signature" => + "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "size" => "0x215", + "stateRoot" => "0xfad4af258fd11939fae0c6c6eec9d340b1caac0b0196fd9a1bc3f489c5bf00b3", + "step" => "0", + "timestamp" => "0x0", + "totalDifficulty" => "0x20000", + "transactions" => [], + "transactionsRoot" => "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", + "uncles" => [] + } + } + ]} + end) + + insert(:block) + insert(:block) + address_a = insert(:address) + address_b = insert(:address) + + transaction = + :transaction + |> insert(from_address: address_a, to_address: address_b, value: 1000) + |> with_block(status: :ok) + + conn = get(conn, transaction_state_path(conn, :index, transaction), %{type: "JSON"}) + + {:ok, %{"items" => items}} = conn.resp_body |> Poison.decode() + full_text = Enum.join(items) + + assert(length(items) == 3) + assert(String.contains?(full_text, format_wei_value(%Wei{value: Decimal.new(0)}, :ether))) + + 1 |> :timer.seconds() |> :timer.sleep() + conn = get(conn, transaction_state_path(conn, :index, transaction), %{type: "JSON"}) + {:ok, %{"items" => items}} = conn.resp_body |> Poison.decode() + full_text = Enum.join(items) + assert(String.contains?(full_text, format_wei_value(%Wei{value: Decimal.new(123)}, :ether))) + assert(length(items) == 3) + end + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/transaction_token_transfer_controller_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/transaction_token_transfer_controller_test.exs new file mode 100644 index 0000000..fcff1a3 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/transaction_token_transfer_controller_test.exs @@ -0,0 +1,173 @@ +defmodule BlockScoutWeb.TransactionTokenTransferControllerTest do + use BlockScoutWeb.ConnCase + + import Mox + + import BlockScoutWeb.Routers.WebRouter.Helpers, only: [transaction_token_transfer_path: 3] + + alias Explorer.Market.Token + alias Explorer.TestHelper + + setup :verify_on_exit! + + describe "GET index/3" do + test "load token transfers", %{conn: conn} do + transaction = insert(:transaction) + token_transfer = insert(:token_transfer, transaction: transaction) + + conn = get(conn, transaction_token_transfer_path(BlockScoutWeb.Endpoint, :index, transaction.hash)) + + assigned_token_transfer = List.first(conn.assigns.transaction.token_transfers) + + assert {assigned_token_transfer.transaction_hash, assigned_token_transfer.log_index} == + {token_transfer.transaction_hash, token_transfer.log_index} + end + + test "with missing transaction", %{conn: conn} do + hash = transaction_hash() + conn = get(conn, transaction_token_transfer_path(BlockScoutWeb.Endpoint, :index, hash)) + + assert html_response(conn, 404) + end + + test "with invalid transaction hash", %{conn: conn} do + conn = get(conn, transaction_token_transfer_path(BlockScoutWeb.Endpoint, :index, "nope")) + + assert html_response(conn, 422) + end + + test "includes transaction data", %{conn: conn} do + block = insert(:block, %{number: 777}) + + transaction = + :transaction + |> insert() + |> with_block(block) + + conn = get(conn, transaction_token_transfer_path(BlockScoutWeb.Endpoint, :index, transaction.hash)) + + assert html_response(conn, 200) + assert conn.assigns.transaction.hash == transaction.hash + end + + test "includes token transfers for the transaction", %{conn: conn} do + transaction = insert(:transaction) |> with_block() + + insert(:token_transfer, + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number + ) + + insert(:token_transfer, + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number + ) + + path = transaction_token_transfer_path(BlockScoutWeb.Endpoint, :index, transaction.hash) + + conn = get(conn, path, %{type: "JSON"}) + + assert json_response(conn, 200) + + {:ok, %{"items" => items}} = conn.resp_body |> Poison.decode() + + assert Enum.count(items) == 2 + end + + test "includes USD exchange rate value for address in assigns", %{conn: conn} do + transaction = insert(:transaction) + + conn = get(conn, transaction_token_transfer_path(BlockScoutWeb.Endpoint, :index, transaction.hash)) + + assert %Token{} = conn.assigns.exchange_rate + end + + test "returns next page of results based on last seen token transfer", %{conn: conn} do + transaction = + :transaction + |> insert() + |> with_block() + + insert(:token_transfer, transaction: transaction, block_number: 1000, log_index: 1) + + Enum.each(2..5, fn item -> + insert(:token_transfer, transaction: transaction, block_number: item + 1001, log_index: item + 1) + end) + + conn = + get(conn, transaction_token_transfer_path(BlockScoutWeb.Endpoint, :index, transaction.hash), %{ + "block_number" => "1000", + "index" => "1", + "type" => "JSON" + }) + + {:ok, %{"items" => items}} = conn.resp_body |> Poison.decode() + + refute Enum.count(items) == 3 + end + + test "next_page_path exists if not on last page", %{conn: conn} do + transaction = + :transaction + |> insert() + |> with_block() + + 1..51 + |> Enum.map(fn log_index -> + insert( + :token_transfer, + transaction: transaction, + log_index: log_index, + block: transaction.block, + block_number: transaction.block_number + ) + end) + + conn = + get(conn, transaction_token_transfer_path(BlockScoutWeb.Endpoint, :index, transaction.hash), %{type: "JSON"}) + + {:ok, %{"next_page_path" => path}} = conn.resp_body |> Poison.decode() + + assert path + end + + test "next_page_path is empty if on last page", %{conn: conn} do + transaction = + :transaction + |> insert() + |> with_block() + + 1..2 + |> Enum.map(fn log_index -> + insert( + :token_transfer, + transaction: transaction, + log_index: log_index, + block_number: transaction.block_number, + block: transaction.block + ) + end) + + conn = + get(conn, transaction_token_transfer_path(BlockScoutWeb.Endpoint, :index, transaction.hash), %{type: "JSON"}) + + {:ok, %{"next_page_path" => path}} = conn.resp_body |> Poison.decode() + + refute path + end + + test "preloads to_address smart contract verified", %{conn: conn} do + TestHelper.get_all_proxies_implementation_zero_addresses() + + transaction = insert(:transaction_to_verified_contract) + + conn = get(conn, transaction_token_transfer_path(BlockScoutWeb.Endpoint, :index, transaction.hash)) + + assert html_response(conn, 200) + assert conn.assigns.transaction.hash == transaction.hash + assert conn.assigns.transaction.to_address.smart_contract != nil + end + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/verified_contracts_controller_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/verified_contracts_controller_test.exs new file mode 100644 index 0000000..e0ca5bd --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/verified_contracts_controller_test.exs @@ -0,0 +1,182 @@ +defmodule BlockScoutWeb.VerifiedContractsControllerTest do + use BlockScoutWeb.ConnCase + + import BlockScoutWeb.Routers.WebRouter.Helpers, only: [verified_contracts_path: 2, verified_contracts_path: 3] + + alias Explorer.Chain.SmartContract + + alias Explorer.Chain.Cache.Counters.{ + ContractsCount, + NewContractsCount, + NewVerifiedContractsCount, + VerifiedContractsCount + } + + describe "GET index/2" do + test "returns 200", %{conn: conn} do + start_supervised!(ContractsCount) + ContractsCount.consolidate() + start_supervised!(NewContractsCount) + NewContractsCount.consolidate() + start_supervised!(NewVerifiedContractsCount) + NewVerifiedContractsCount.consolidate() + start_supervised!(VerifiedContractsCount) + VerifiedContractsCount.consolidate() + + conn = get(conn, verified_contracts_path(conn, :index)) + + assert html_response(conn, 200) + end + + test "returns all contracts", %{conn: conn} do + insert_list(4, :smart_contract) + + conn = get(conn, verified_contracts_path(conn, :index), %{"type" => "JSON"}) + + items = Map.get(json_response(conn, 200), "items") + + assert length(items) == 4 + end + + test "returns next page of results based on last verified contract", %{conn: conn} do + insert_list(50, :smart_contract) + + contract = insert(:smart_contract) + + conn = + get(conn, verified_contracts_path(conn, :index), %{ + "type" => "JSON", + "smart_contract_id" => Integer.to_string(contract.id) + }) + + items = Map.get(json_response(conn, 200), "items") + + assert length(items) == 50 + end + + test "next_page_path exist if not on last page", %{conn: conn} do + %SmartContract{address_hash: address_hash} = + 60 + |> insert_list(:smart_contract) + |> Enum.reverse() + |> Enum.fetch!(10) + + conn = get(conn, verified_contracts_path(conn, :index), %{"type" => "JSON"}) + + expected_path = + verified_contracts_path(conn, :index, + coin_balance: nil, + hash: address_hash, + items_count: "50", + transaction_count: nil, + transactions_count: nil + ) + + assert Map.get(json_response(conn, 200), "next_page_path") == expected_path + end + + test "next_page_path is empty if on last page", %{conn: conn} do + insert(:smart_contract) + + conn = get(conn, verified_contracts_path(conn, :index), %{"type" => "JSON"}) + + refute conn |> json_response(200) |> Map.get("next_page_path") + end + + test "returns solidity contracts", %{conn: conn} do + insert(:smart_contract, is_vyper_contract: true, language: nil) + + %SmartContract{address_hash: solidity_hash} = + insert(:smart_contract, is_vyper_contract: false, language: nil) + + path = + verified_contracts_path(conn, :index, %{ + "filter" => "solidity", + "type" => "JSON" + }) + + conn = get(conn, path) + + [smart_contracts_tile] = json_response(conn, 200)["items"] + + assert String.contains?(smart_contracts_tile, "data-identifier-hash=\"#{to_string(solidity_hash)}\"") + end + + test "returns vyper contract", %{conn: conn} do + %SmartContract{address_hash: vyper_hash} = + insert(:smart_contract, is_vyper_contract: true, language: nil) + + insert(:smart_contract, is_vyper_contract: false, language: nil) + + path = + verified_contracts_path(conn, :index, %{ + "filter" => "vyper", + "type" => "JSON" + }) + + conn = get(conn, path) + + [smart_contracts_tile] = json_response(conn, 200)["items"] + + assert String.contains?(smart_contracts_tile, "data-identifier-hash=\"#{to_string(vyper_hash)}\"") + end + + test "returns yul contract", %{conn: conn} do + %SmartContract{address_hash: yul_hash} = + insert(:smart_contract, abi: nil, language: nil) + + insert(:smart_contract, language: nil) + + path = + verified_contracts_path(conn, :index, %{ + "filter" => "yul", + "type" => "JSON" + }) + + conn = get(conn, path) + + [smart_contracts_tile] = json_response(conn, 200)["items"] + + assert String.contains?(smart_contracts_tile, "data-identifier-hash=\"#{to_string(yul_hash)}\"") + end + + test "returns search results by name", %{conn: conn} do + insert(:smart_contract) + insert(:smart_contract) + insert(:smart_contract) + contract_name = "qwertyufhgkhiop" + %SmartContract{address_hash: hash} = insert(:smart_contract, name: contract_name) + + path = + verified_contracts_path(conn, :index, %{ + "search" => contract_name, + "type" => "JSON" + }) + + conn = get(conn, path) + + [smart_contracts_tile] = json_response(conn, 200)["items"] + + assert String.contains?(smart_contracts_tile, "data-identifier-hash=\"#{to_string(hash)}\"") + end + + test "returns search results by address", %{conn: conn} do + insert(:smart_contract) + insert(:smart_contract) + insert(:smart_contract) + %SmartContract{address_hash: hash} = insert(:smart_contract) + + path = + verified_contracts_path(conn, :index, %{ + "search" => to_string(hash), + "type" => "JSON" + }) + + conn = get(conn, path) + + [smart_contracts_tile] = json_response(conn, 200)["items"] + + assert String.contains?(smart_contracts_tile, "data-identifier-hash=\"#{to_string(hash)}\"") + end + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/withdrawal_controller_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/withdrawal_controller_test.exs new file mode 100644 index 0000000..c152318 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/controllers/withdrawal_controller_test.exs @@ -0,0 +1,56 @@ +defmodule BlockScoutWeb.WithdrawalControllerTest do + use BlockScoutWeb.ConnCase + + import BlockScoutWeb.Routers.WebRouter.Helpers, only: [withdrawal_path: 2, withdrawal_path: 3] + + alias Explorer.Chain.Withdrawal + + describe "GET index/2" do + test "returns all withdrawals", %{conn: conn} do + insert_list(4, :withdrawal) + + conn = get(conn, withdrawal_path(conn, :index), %{"type" => "JSON"}) + + items = Map.get(json_response(conn, 200), "items") + + assert length(items) == 4 + end + + test "returns next page of results based on last withdrawal", %{conn: conn} do + insert_list(50, :withdrawal) + + withdrawal = insert(:withdrawal) + + conn = + get(conn, withdrawal_path(conn, :index), %{ + "type" => "JSON", + "index" => Integer.to_string(withdrawal.index) + }) + + items = Map.get(json_response(conn, 200), "items") + + assert length(items) == 50 + end + + test "next_page_path exist if not on last page", %{conn: conn} do + %Withdrawal{index: index} = + 60 + |> insert_list(:withdrawal) + |> Enum.fetch!(10) + + conn = get(conn, withdrawal_path(conn, :index), %{"type" => "JSON"}) + + expected_path = withdrawal_path(conn, :index, index: index, items_count: "50") + + assert Map.get(json_response(conn, 200), "next_page_path") == expected_path + end + + test "next_page_path is empty if on last page", %{conn: conn} do + insert(:withdrawal) + + conn = get(conn, withdrawal_path(conn, :index), %{"type" => "JSON"}) + + refute conn |> json_response(200) |> Map.get("next_page_path") + end + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/features/address_contract_verification_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/features/address_contract_verification_test.exs new file mode 100644 index 0000000..bfad4cf --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/features/address_contract_verification_test.exs @@ -0,0 +1,77 @@ +defmodule BlockScoutWeb.AddressContractVerificationTest do + use BlockScoutWeb.FeatureCase, async: false + + alias BlockScoutWeb.{AddressContractPage, ContractVerifyPage} + alias Explorer.Factory + alias Plug.Conn + + setup do + bypass = Bypass.open() + + configuration = Application.get_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour) + Application.put_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour, enabled: false) + + Application.put_env(:explorer, :solc_bin_api_url, "http://localhost:#{bypass.port}") + + on_exit(fn -> + Application.put_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour, configuration) + end) + + {:ok, bypass: bypass} + end + + # wallaby with chrome headless always fails this test + @tag :skip + test "users validates smart contract", %{session: session, bypass: bypass} do + Bypass.expect(bypass, fn conn -> Conn.resp(conn, 200, solc_bin_versions()) end) + + %{name: name, source_code: source_code, bytecode: bytecode, version: version} = Factory.contract_code_info() + + transaction = :transaction |> insert() |> with_block() + address = insert(:address, contract_code: bytecode) + + insert( + :internal_transaction_create, + created_contract_address: address, + created_contract_code: bytecode, + index: 0, + transaction: transaction + ) + + session + |> AddressContractPage.visit_page(address) + |> AddressContractPage.click_verify_and_publish() + |> ContractVerifyPage.fill_form(%{ + contract_name: name, + version: version, + optimization: false, + source_code: source_code, + evm_version: "byzantium" + }) + |> ContractVerifyPage.verify_and_publish() + + assert AddressContractPage.on_page?(session, address) + end + + test "with invalid data shows error messages", %{session: session, bypass: bypass} do + Bypass.expect(bypass, fn conn -> Conn.resp(conn, 200, solc_bin_versions()) end) + + address = insert(:address) + + session + |> ContractVerifyPage.visit_page(address) + |> ContractVerifyPage.fill_form(%{ + contract_name: "name", + version: "default", + optimization: "true", + source_code: "", + evm_version: "byzantium" + }) + |> ContractVerifyPage.verify_and_publish() + |> ContractVerifyPage.has_message?("There was an error validating your contract, please try again.") + end + + defp solc_bin_versions do + File.read!("./test/support/fixture/smart_contract/solc_bin.json") + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/features/pages/address_contract_page.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/features/pages/address_contract_page.ex new file mode 100644 index 0000000..2930e35 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/features/pages/address_contract_page.ex @@ -0,0 +1,23 @@ +defmodule BlockScoutWeb.AddressContractPage do + @moduledoc false + + use Wallaby.DSL + + import Wallaby.Query, only: [css: 1] + + def on_page?(session, address) do + current_path(session) =~ address_contract_path(address) + end + + def click_verify_and_publish(session) do + click(session, css("[data-test='verify_and_publish']")) + end + + def visit_page(session, address) do + visit(session, address_contract_path(address)) + end + + defp address_contract_path(address) do + "/address/#{address.hash}/contracts" + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/features/pages/address_page.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/features/pages/address_page.ex new file mode 100644 index 0000000..99b66dd --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/features/pages/address_page.ex @@ -0,0 +1,178 @@ +defmodule BlockScoutWeb.AddressPage do + @moduledoc false + + use Wallaby.DSL + import Wallaby.Query, only: [css: 1, css: 2] + alias Explorer.Chain.{Address, InternalTransaction, Hash, Transaction, Token} + + def apply_filter(session, direction) do + session + |> click(css("[data-test='filter_dropdown']", text: "Filter: All")) + |> click(css("[data-test='filter_option']", text: direction)) + end + + def balance do + css("[data-test='address_balance']") + end + + def token_balance(count: count) do + css("[data-dropdown-token-balance-test]", count: count) + end + + def token_balance_counter(text) do + css("[data-tokens-count]", text: "#{text} tokens") + end + + def token_type(count: count) do + css("[data-token-type]", count: count) + end + + def token_type_count(type: type, text: text) do + css("[data-number-of-tokens-by-type='#{type}']", text: text) + end + + def address(%Address{} = address) do + css("[data-address-hash='#{address}']", text: to_string(address)) + end + + def contract_creator do + css("[data-test='address_contract_creator']") + end + + def click_internal_transactions(session) do + click(session, css("[data-test='internal_transactions_tab_link']")) + end + + def click_tokens(session) do + click(session, css("[data-test='tokens_tab_link']")) + end + + def click_coin_balance_history(session) do + click(session, css("[data-test='coin_balance_tab_link']")) + end + + def click_balance_dropdown_toggle(session) do + click(session, css("[data-dropdown-toggle]")) + end + + def fill_balance_dropdown_search(session, text) do + fill_in(session, css("[data-filter-dropdown-tokens]"), with: text) + end + + def click_outside_of_the_dropdown(session) do + click(session, css("[data-test='outside_of_dropdown']")) + end + + def click_token_transfers(session, %Token{contract_address_hash: contract_address_hash}) do + click(session, css("[data-test='token_transfers_#{contract_address_hash}']")) + end + + def click_show_pending_transactions(session) do + click(session, css("[data-selector='pending-transactions-open']")) + end + + def coin_balances(count: count) do + css("[data-test='coin_balance']", count: count) + end + + def contract_creation(%InternalTransaction{created_contract_address_hash: hash}) do + css("[data-address-hash='#{hash}']", text: to_string(hash)) + end + + def detail_hash(address) do + css("[data-test='address_detail_hash']", text: to_string(address)) + end + + def internal_transaction(%InternalTransaction{transaction_hash: transaction_hash, index: index}) do + css( + "[data-test='internal_transaction']" <> + "[data-internal-transaction-transaction-hash='#{transaction_hash}']" <> + "[data-internal-transaction-index='#{index}']" + ) + end + + def internal_transactions(count: count) do + css("[data-test='internal_transaction']", count: count) + end + + def internal_transaction_address_link( + %InternalTransaction{transaction_hash: transaction_hash, index: index, from_address_hash: address_hash}, + :from + ) do + checksum = Address.checksum(address_hash) + + css( + "[data-internal-transaction-transaction-hash='#{transaction_hash}'][data-internal-transaction-index='#{index}']" <> + " [data-test='address_hash_link']" <> " [data-address-hash='#{checksum}']" + ) + end + + def internal_transaction_address_link( + %InternalTransaction{transaction_hash: transaction_hash, index: index, to_address_hash: address_hash}, + :to + ) do + css( + "[data-internal-transaction-transaction-hash='#{transaction_hash}'][data-internal-transaction-index='#{index}']" <> + " [data-test='address_hash_link']" <> " [data-address-hash='#{address_hash}']" + ) + end + + def pending_transaction(%Transaction{hash: transaction_hash}), do: pending_transaction(transaction_hash) + + def pending_transaction(transaction_hash) do + css("[data-selector='pending-transactions-list'] [data-transaction-hash='#{transaction_hash}']") + end + + def transaction(%Transaction{hash: transaction_hash}), do: transaction(transaction_hash) + + def transaction(%Hash{} = hash) do + hash + |> to_string() + |> transaction() + end + + def transaction(transaction_hash) do + css("[data-identifier-hash='#{transaction_hash}']") + end + + def transaction_address_link(%Transaction{hash: hash, from_address_hash: address_hash}, :from) do + checksum = Address.checksum(address_hash) + + css("[data-identifier-hash='#{hash}'] [data-test='address_hash_link'] [data-address-hash='#{checksum}']") + end + + def transaction_address_link(%Transaction{hash: hash, to_address_hash: address_hash}, :to) do + checksum = Address.checksum(address_hash) + + css("[data-identifier-hash='#{hash}'] [data-test='address_hash_link'] [data-address-hash='#{checksum}']") + end + + def transaction_status(%Transaction{hash: transaction_hash}) do + css("[data-identifier-hash='#{transaction_hash}'] [data-test='transaction_status']") + end + + def token_transfer(%Transaction{hash: transaction_hash}, %Address{} = address, count: count) do + css( + "[data-identifier-hash='#{transaction_hash}'] [data-test='token_transfer'] [data-address-hash='#{address}']", + count: count + ) + end + + def token_transfers(%Transaction{hash: transaction_hash}, count: count) do + css("[data-identifier-hash='#{transaction_hash}'] [data-test='token_transfer']", count: count) + end + + def token_transfers_expansion(%Transaction{hash: transaction_hash}) do + css("[data-identifier-hash='#{transaction_hash}'] [data-test='token_transfers_expansion']") + end + + def visit_page(session, %Address{hash: address_hash}), do: visit_page(session, address_hash) + + def visit_page(session, address_hash) do + visit(session, "/address/#{address_hash}") + end + + def visit_page(session) do + visit(session, "/accounts") + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/features/pages/app_page.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/features/pages/app_page.ex new file mode 100644 index 0000000..98b934a --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/features/pages/app_page.ex @@ -0,0 +1,19 @@ +defmodule BlockScoutWeb.AppPage do + @moduledoc false + + use Wallaby.DSL + + import Wallaby.Query, only: [css: 1, css: 2] + + def visit_page(session) do + visit(session, "/") + end + + def indexed_status(text) do + css("[data-selector='indexed-status'] [data-indexed-ratio]", text: text) + end + + def still_indexing?() do + css("[data-selector='indexed-status']") + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/features/pages/block_list_page.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/features/pages/block_list_page.ex new file mode 100644 index 0000000..f59cf42 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/features/pages/block_list_page.ex @@ -0,0 +1,33 @@ +defmodule BlockScoutWeb.BlockListPage do + @moduledoc false + + use Wallaby.DSL + + import Wallaby.Query, only: [css: 1, css: 2] + + alias Explorer.Chain.Block + + def visit_page(session) do + visit(session, "/blocks") + end + + def visit_reorgs_page(session) do + visit(session, "/reorgs") + end + + def visit_uncles_page(session) do + visit(session, "/uncles") + end + + def block(%Block{number: block_number}) do + css("[data-block-number='#{block_number}']") + end + + def place_holder_blocks(count) do + css("[data-selector='place-holder']", count: count) + end + + def blocks(count) do + css("[data-selector='block-tile']", count: count) + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/features/pages/block_page.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/features/pages/block_page.ex new file mode 100644 index 0000000..18289e0 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/features/pages/block_page.ex @@ -0,0 +1,50 @@ +defmodule BlockScoutWeb.BlockPage do + @moduledoc false + + use Wallaby.DSL + + import Wallaby.Query, only: [css: 1, css: 2] + + alias Explorer.Chain.{Address, Block, InternalTransaction, Transaction} + + def contract_creation(%InternalTransaction{created_contract_address_hash: hash}) do + checksum = Address.checksum(hash) + css("[data-address-hash='#{checksum}']") + end + + def detail_number(%Block{number: block_number}) do + css("[data-test='block_detail_number']", text: to_string(block_number)) + end + + def page_type(type) do + css("[data-test='detail_type']", text: type) + end + + def token_transfers(%Transaction{hash: transaction_hash}, count: count) do + css("[data-identifier-hash='#{transaction_hash}'] [data-test='token_transfer']", count: count) + end + + def token_transfers_expansion(%Transaction{hash: transaction_hash}) do + css("[data-identifier-hash='#{transaction_hash}'] [data-test='token_transfers_expansion']") + end + + def transaction(%Transaction{hash: transaction_hash}) do + css("[data-identifier-hash='#{transaction_hash}']") + end + + def transaction_status(%Transaction{hash: transaction_hash}) do + css("[data-identifier-hash='#{transaction_hash}'] [data-test='transaction_status']") + end + + def uncle_link(%Block{hash: hash}) do + css("[data-test='uncle_link'][data-uncle-hash='#{hash}']") + end + + def visit_page(session, %Block{number: block_number, consensus: true}) do + visit(session, "/blocks/#{block_number}") + end + + def visit_page(session, %Block{hash: hash}) do + visit(session, "/blocks/#{hash}") + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/features/pages/chain_page.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/features/pages/chain_page.ex new file mode 100644 index 0000000..7781210 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/features/pages/chain_page.ex @@ -0,0 +1,48 @@ +defmodule BlockScoutWeb.ChainPage do + @moduledoc false + + use Wallaby.DSL + + import Wallaby.Query, only: [css: 1, css: 2] + + alias Explorer.Chain.{Address, Block, Transaction} + + def block(%Block{number: block_number}) do + css("[data-block-number='#{block_number}']") + end + + def blocks(count: count) do + css("[data-selector='chain-block']", count: count) + end + + def contract_creation(%Transaction{created_contract_address_hash: hash}) do + checksum = Address.checksum(hash) + css("[data-test='contract-creation'] [data-address-hash='#{checksum}']") + end + + def place_holder_blocks(count) do + css("[data-selector='place-holder']", count: count) + end + + def search(session, text) do + session + |> fill_in(css("[data-test='search_input']"), with: text) + |> send_keys([:enter]) + end + + def token_transfers(%Transaction{hash: transaction_hash}, count: count) do + css("[data-identifier-hash='#{transaction_hash}'] [data-test='token_transfer']", count: count) + end + + def token_transfers_expansion(%Transaction{hash: transaction_hash}) do + css("[data-identifier-hash='#{transaction_hash}'] [data-test='token_transfers_expansion']") + end + + def transactions(count: count) do + css("[data-test='chain_transaction']", count: count) + end + + def visit_page(session) do + visit(session, "/") + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/features/pages/contract_verify_page.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/features/pages/contract_verify_page.ex new file mode 100644 index 0000000..f57a723 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/features/pages/contract_verify_page.ex @@ -0,0 +1,58 @@ +defmodule BlockScoutWeb.ContractVerifyPage do + @moduledoc false + + use Wallaby.DSL + + import Wallaby.Query + + def visit_page(session, address_hash) do + visit(session, "/address/#{address_hash}/verify-via-flattened-code/new") + end + + def fill_form(session, %{ + contract_name: contract_name, + version: version, + optimization: optimization, + source_code: source_code, + evm_version: evm_version + }) do + session + |> fill_in(css("[data-test='contract_name']"), with: contract_name) + |> fill_in(text_field("Enter the Solidity Contract Code"), with: source_code) + + case version do + nil -> nil + _ -> click(session, option(version)) + end + + case evm_version do + nil -> nil + _ -> click(session, option(evm_version)) + end + + case optimization do + true -> + click(session, radio_button("Yes")) + + false -> + click(session, radio_button("No")) + + _ -> + nil + end + + session + end + + def validation_error do + css("[data-test='contract-source-code-error']") + end + + def has_message?(session, message) do + String.contains?(page_source(session), message) + end + + def verify_and_publish(session) do + click(session, button("Verify & publish")) + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/features/pages/token_page.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/features/pages/token_page.ex new file mode 100644 index 0000000..23eb418 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/features/pages/token_page.ex @@ -0,0 +1,27 @@ +defmodule BlockScoutWeb.TokenPage do + @moduledoc false + + use Wallaby.DSL + import Wallaby.Query, only: [css: 1, css: 2] + alias Explorer.Chain.{Address} + + def visit_page(session, %Address{hash: address_hash}) do + visit_page(session, address_hash) + end + + def visit_page(session, contract_address_hash) do + visit(session, "tokens/#{contract_address_hash}/token-holders") + end + + def token_holders_tab(count: count) do + css("[data-test='token_holders_tab']", count: count) + end + + def click_tokens_holders(session) do + click(session, css("[data-test='token_holders_tab']")) + end + + def token_holders(count: count) do + css("[data-test='token_holders']", count: count) + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/features/pages/transaction_list_page.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/features/pages/transaction_list_page.ex new file mode 100644 index 0000000..9b3c34f --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/features/pages/transaction_list_page.ex @@ -0,0 +1,33 @@ +defmodule BlockScoutWeb.TransactionListPage do + @moduledoc false + + use Wallaby.DSL + + import Wallaby.Query, only: [css: 1, css: 2] + + alias Explorer.Chain.Transaction + + def click_transaction(session, %Transaction{hash: transaction_hash}) do + click(session, css("[data-identifier-hash='#{transaction_hash}'] [data-test='transaction_hash_link']")) + end + + def contract_creation(%Transaction{hash: hash}) do + css("[data-identifier-hash='#{hash}'] [data-test='transaction_type']", text: "Contract Creation") + end + + def transaction(%Transaction{hash: transaction_hash}) do + css("[data-identifier-hash='#{transaction_hash}']") + end + + def transaction_status(%Transaction{hash: transaction_hash}) do + css("[data-identifier-hash='#{transaction_hash}'] [data-test='transaction_status']") + end + + def visit_page(session) do + visit(session, "/txs") + end + + def visit_pending_transactions_page(session) do + visit(session, "/pending-transactions") + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/features/pages/transaction_logs_page.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/features/pages/transaction_logs_page.ex new file mode 100644 index 0000000..cd0dc88 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/features/pages/transaction_logs_page.ex @@ -0,0 +1,23 @@ +defmodule BlockScoutWeb.TransactionLogsPage do + @moduledoc false + + use Wallaby.DSL + + import Wallaby.Query, only: [css: 1, css: 2] + import BlockScoutWeb.Routers.WebRouter.Helpers, only: [transaction_log_path: 3] + + alias Explorer.Chain.Transaction + alias BlockScoutWeb.Endpoint + + def logs(count: count) do + css("[data-test='transaction_log']", count: count) + end + + def visit_page(session, %Transaction{} = transaction) do + visit(session, transaction_log_path(Endpoint, :index, transaction)) + end + + def click_address(session, address) do + click(session, css("[data-test='log_address_link'][data-address-hash='#{address}']")) + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/features/pages/transaction_page.ex b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/features/pages/transaction_page.ex new file mode 100644 index 0000000..19fe499 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/features/pages/transaction_page.ex @@ -0,0 +1,25 @@ +defmodule BlockScoutWeb.TransactionPage do + @moduledoc false + + use Wallaby.DSL + + import Wallaby.Query, only: [css: 1, css: 2] + + alias Explorer.Chain.{Transaction, Hash} + + def click_logs(session) do + click(session, css("[data-test='transaction_logs_link']")) + end + + def detail_hash(%Transaction{hash: transaction_hash}) do + css("[data-test='transaction_detail_hash']", text: Hash.to_string(transaction_hash)) + end + + def is_pending() do + css("[data-selector='block-number']", text: "Pending") + end + + def visit_page(session, %Transaction{hash: transaction_hash}) do + visit(session, "/tx/#{transaction_hash}") + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/features/viewing_addresses_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/features/viewing_addresses_test.exs new file mode 100644 index 0000000..9dac716 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/features/viewing_addresses_test.exs @@ -0,0 +1,496 @@ +defmodule BlockScoutWeb.ViewingAddressesTest do + use BlockScoutWeb.FeatureCase, + # Because ETS tables is shared for `Explorer.Counters.*` + async: false + + alias Explorer.Chain.Cache.Counters.AddressesCount + alias BlockScoutWeb.{AddressPage, AddressView, Notifier} + + setup do + Application.put_env(:block_scout_web, :checksum_address_hashes, false) + + block = insert(:block, number: 42) + + lincoln = insert(:address, fetched_coin_balance: 5) + taft = insert(:address, fetched_coin_balance: 5) + + from_taft = + :transaction + |> insert(from_address: taft, to_address: lincoln) + |> with_block(block) + + from_lincoln = + :transaction + |> insert(from_address: lincoln, to_address: taft) + |> with_block(block) + + lincoln_reward = + :reward + |> insert( + address_hash: lincoln.hash, + block_hash: block.hash, + address_type: :emission_funds + ) + + taft_reward = + :reward + |> insert( + address_hash: taft.hash, + block_hash: block.hash, + address_type: :validator + ) + + on_exit(fn -> + Application.put_env(:block_scout_web, :checksum_address_hashes, true) + end) + + {:ok, + %{ + addresses: %{lincoln: lincoln, taft: taft}, + block: block, + rewards: {lincoln_reward, taft_reward}, + transactions: %{from_lincoln: from_lincoln, from_taft: from_taft} + }} + end + + describe "viewing top addresses" do + setup do + addresses = Enum.map(150..101, &insert(:address, fetched_coin_balance: &1)) + + {:ok, %{addresses: addresses}} + end + + test "lists top addresses", %{session: session, addresses: addresses} do + [first_address | _] = addresses + [last_address | _] = Enum.reverse(addresses) + + start_supervised!(AddressesCount) + AddressesCount.consolidate() + + session + |> AddressPage.visit_page() + |> assert_has(AddressPage.address(first_address)) + |> assert_has(AddressPage.address(last_address)) + end + end + + test "viewing address overview information", %{session: session} do + address = insert(:address, fetched_coin_balance: 500) + + session + |> AddressPage.visit_page(address) + |> assert_text(AddressPage.balance(), "0.0000000000000005 ETH") + end + + describe "viewing contract creator" do + test "see the contract creator and transaction links", %{session: session} do + address = insert(:address) + contract = insert(:contract_address) + transaction = insert(:transaction, from_address: address, created_contract_address: contract) |> with_block() + + internal_transaction = + insert( + :internal_transaction_create, + index: 1, + transaction: transaction, + from_address: address, + created_contract_address: contract, + block_hash: transaction.block_hash, + block_index: 1 + ) + + address_hash = AddressView.trimmed_hash(address.hash) + transaction_hash = AddressView.trimmed_hash(transaction.hash) + + session + |> AddressPage.visit_page(internal_transaction.created_contract_address) + |> assert_text(AddressPage.contract_creator(), "#{address_hash} at #{transaction_hash}") + end + + test "see the contract creator and transaction links even when the creator is another contract", %{session: session} do + lincoln = insert(:address) + contract = insert(:contract_address) + transaction = insert(:transaction) |> with_block() + another_contract = insert(:contract_address) + + insert( + :internal_transaction, + index: 1, + transaction: transaction, + from_address: lincoln, + to_address: contract, + created_contract_address: contract, + type: :call, + block_hash: transaction.block_hash, + block_index: 1 + ) + + internal_transaction = + insert( + :internal_transaction_create, + index: 2, + transaction: transaction, + from_address: contract, + created_contract_address: another_contract, + block_hash: transaction.block_hash, + block_index: 2 + ) + + contract_hash = AddressView.trimmed_hash(contract.hash) + transaction_hash = AddressView.trimmed_hash(transaction.hash) + + session + |> AddressPage.visit_page(internal_transaction.created_contract_address) + |> assert_text(AddressPage.contract_creator(), "#{contract_hash} at #{transaction_hash}") + end + end + + describe "viewing transactions" do + test "sees all addresses transactions by default", %{ + addresses: addresses, + session: session, + transactions: transactions + } do + session + |> AddressPage.visit_page(addresses.lincoln) + |> assert_has(AddressPage.transaction(transactions.from_taft)) + |> assert_has(AddressPage.transaction(transactions.from_lincoln)) + |> assert_has(AddressPage.transaction_status(transactions.from_lincoln)) + end + + test "can filter to only see transactions from an address", %{ + addresses: addresses, + session: session, + transactions: transactions + } do + session + |> AddressPage.visit_page(addresses.lincoln) + |> AddressPage.apply_filter("From") + |> assert_has(AddressPage.transaction(transactions.from_lincoln)) + |> refute_has(AddressPage.transaction(transactions.from_taft)) + end + + test "can filter to only see transactions to an address", %{ + addresses: addresses, + session: session, + transactions: transactions + } do + session + |> AddressPage.visit_page(addresses.lincoln) + |> AddressPage.apply_filter("To") + |> refute_has(AddressPage.transaction(transactions.from_lincoln)) + |> assert_has(AddressPage.transaction(transactions.from_taft)) + end + + test "only addresses not matching the page are links", %{ + addresses: addresses, + session: session, + transactions: transactions + } do + session + |> AddressPage.visit_page(addresses.lincoln) + |> assert_has(AddressPage.transaction_address_link(transactions.from_lincoln, :to)) + |> refute_has(AddressPage.transaction_address_link(transactions.from_lincoln, :from)) + end + + test "sees rewards to and from an address alongside transactions", %{ + addresses: addresses, + session: session, + transactions: transactions + } do + Application.put_env(:block_scout_web, BlockScoutWeb.Chain, has_emission_funds: true) + + session + |> AddressPage.visit_page(addresses.lincoln) + |> assert_has(AddressPage.transaction(transactions.from_taft)) + |> assert_has(AddressPage.transaction(transactions.from_lincoln)) + + Application.put_env(:block_scout_web, BlockScoutWeb.Chain, has_emission_funds: false) + end + end + + describe "viewing internal transactions" do + setup %{addresses: addresses, transactions: transactions} do + address = addresses.lincoln + transaction = transactions.from_lincoln + + internal_transaction_lincoln_to_address = + insert(:internal_transaction, + transaction: transaction, + to_address: address, + index: 1, + block_number: 7000, + transaction_index: 1, + block_hash: transaction.block_hash, + block_index: 1 + ) + + insert(:internal_transaction, + transaction: transaction, + from_address: address, + index: 2, + block_number: 8000, + transaction_index: 2, + block_hash: transaction.block_hash, + block_index: 2 + ) + + {:ok, %{internal_transaction_lincoln_to_address: internal_transaction_lincoln_to_address}} + end + + test "only addresses not matching the page are links", %{ + addresses: addresses, + internal_transaction_lincoln_to_address: internal_transaction, + session: session + } do + session + |> AddressPage.visit_page(addresses.lincoln) + |> AddressPage.click_internal_transactions() + |> assert_has(AddressPage.internal_transaction_address_link(internal_transaction, :from)) + |> refute_has(AddressPage.internal_transaction_address_link(internal_transaction, :to)) + end + + test "viewing new internal transactions via live update", %{addresses: addresses, session: session} do + transaction = + :transaction + |> insert(from_address: addresses.lincoln) + |> with_block(insert(:block, number: 7000)) + + session + |> AddressPage.visit_page(addresses.lincoln) + |> AddressPage.click_internal_transactions() + |> assert_has(AddressPage.internal_transactions(count: 2)) + + internal_transaction = + insert(:internal_transaction, + transaction: transaction, + index: 2, + from_address: addresses.lincoln, + block_number: transaction.block_number, + transaction_index: transaction.index, + block_hash: transaction.block_hash, + block_index: 2 + ) + + Notifier.handle_event({:chain_event, :internal_transactions, :realtime, [internal_transaction]}) + + session + |> assert_has(AddressPage.internal_transactions(count: 3)) + |> assert_has(AddressPage.internal_transaction(internal_transaction)) + end + + test "can filter to see internal transactions from an address only", %{ + addresses: addresses, + session: session + } do + block = insert(:block, number: 7000) + + from_lincoln = + :transaction + |> insert(from_address: addresses.lincoln) + |> with_block(block) + + from_taft = + :transaction + |> insert(from_address: addresses.taft) + |> with_block(block) + + insert(:internal_transaction, + transaction: from_lincoln, + index: 2, + from_address: addresses.lincoln, + block_number: from_lincoln.block_number, + transaction_index: from_lincoln.index, + block_hash: from_lincoln.block_hash, + block_index: 2 + ) + + session + |> AddressPage.visit_page(addresses.lincoln) + |> AddressPage.apply_filter("From") + |> assert_has(AddressPage.transaction(from_lincoln)) + |> refute_has(AddressPage.transaction(from_taft)) + end + + test "can filter to see internal transactions to an address only", %{ + addresses: addresses, + session: session + } do + block = insert(:block, number: 7000) + + from_lincoln = + :transaction + |> insert(to_address: addresses.lincoln) + |> with_block(block) + + from_taft = + :transaction + |> insert(to_address: addresses.taft) + |> with_block(block) + + insert(:internal_transaction, + transaction: from_lincoln, + index: 2, + from_address: addresses.lincoln, + block_number: from_lincoln.block_number, + transaction_index: from_lincoln.index, + block_hash: from_lincoln.block_hash, + block_index: 2 + ) + + session + |> AddressPage.visit_page(addresses.lincoln) + |> AddressPage.apply_filter("To") + |> assert_has(AddressPage.transaction(from_lincoln)) + |> refute_has(AddressPage.transaction(from_taft)) + end + end + + describe "viewing token transfers from a specific token" do + test "list token transfers related to the address", %{ + addresses: addresses, + block: block, + session: session + } do + lincoln = addresses.lincoln + taft = addresses.taft + + contract_address = insert(:contract_address) + token = insert(:token, contract_address: contract_address) + + transaction = + :transaction + |> insert(from_address: lincoln, to_address: contract_address) + |> with_block(block) + + insert( + :token_transfer, + from_address: lincoln, + to_address: taft, + transaction: transaction, + token_contract_address: contract_address + ) + + insert(:address_current_token_balance, address: lincoln, token_contract_address_hash: contract_address.hash) + + session + |> AddressPage.visit_page(lincoln) + |> AddressPage.click_tokens() + |> AddressPage.click_token_transfers(token) + |> assert_has(AddressPage.token_transfers(transaction, count: 1)) + |> assert_has(AddressPage.token_transfer(transaction, lincoln, count: 1)) + |> assert_has(AddressPage.token_transfer(transaction, taft, count: 1)) + |> refute_has(AddressPage.token_transfers_expansion(transaction)) + end + end + + describe "viewing token balances" do + setup do + block = insert(:block) + lincoln = insert(:address, fetched_coin_balance: 5, fetched_coin_balance_block_number: block.number) + taft = insert(:address, fetched_coin_balance: 5) + + contract_address = insert(:contract_address) + insert(:token, name: "atoken", symbol: "AT", contract_address: contract_address, type: "ERC-721") + + transaction = + :transaction + |> insert(from_address: lincoln, to_address: contract_address) + |> with_block(block) + + insert( + :token_transfer, + from_address: lincoln, + to_address: taft, + transaction: transaction, + token_contract_address: contract_address + ) + + insert(:address_current_token_balance, address: lincoln, token_contract_address_hash: contract_address.hash) + + contract_address_2 = insert(:contract_address) + insert(:token, name: "token2", symbol: "T2", contract_address: contract_address_2, type: "ERC-20") + + transaction_2 = + :transaction + |> insert(from_address: lincoln, to_address: contract_address_2) + |> with_block(block) + + insert( + :token_transfer, + from_address: lincoln, + to_address: taft, + transaction: transaction_2, + token_contract_address: contract_address_2 + ) + + insert(:address_current_token_balance, address: lincoln, token_contract_address_hash: contract_address_2.hash) + + {:ok, lincoln: lincoln} + end + + test "filter tokens balances by token name", %{session: session, lincoln: lincoln} do + next = + session + |> AddressPage.visit_page(lincoln) + + Process.sleep(2_000) + + next + |> AddressPage.click_balance_dropdown_toggle() + |> AddressPage.fill_balance_dropdown_search("ato") + |> assert_has(AddressPage.token_balance(count: 1)) + |> assert_has(AddressPage.token_type(count: 1)) + |> assert_has(AddressPage.token_type_count(type: "ERC-721", text: "1")) + end + + test "filter token balances by token symbol", %{session: session, lincoln: lincoln} do + next = + session + |> AddressPage.visit_page(lincoln) + + Process.sleep(2_000) + + next + |> AddressPage.click_balance_dropdown_toggle() + |> AddressPage.fill_balance_dropdown_search("T2") + |> assert_has(AddressPage.token_balance(count: 1)) + |> assert_has(AddressPage.token_type(count: 1)) + |> assert_has(AddressPage.token_type_count(type: "ERC-20", text: "1")) + end + + test "reset token balances filter when dropdown closes", %{session: session, lincoln: lincoln} do + next = + session + |> AddressPage.visit_page(lincoln) + + Process.sleep(2_000) + + next + |> AddressPage.click_balance_dropdown_toggle() + |> AddressPage.fill_balance_dropdown_search("ato") + |> AddressPage.click_outside_of_the_dropdown() + |> assert_has(AddressPage.token_balance_counter("2")) + end + end + + describe "viewing coin balance history" do + setup do + address = insert(:address, fetched_coin_balance: 5) + noon = Timex.now() |> Timex.beginning_of_day() |> Timex.set(hour: 12) + block = insert(:block, timestamp: noon) + block_one_day_ago = insert(:block, timestamp: Timex.shift(noon, days: -1)) + insert(:fetched_balance, address_hash: address.hash, value: 5, block_number: block.number) + insert(:fetched_balance, address_hash: address.hash, value: 10, block_number: block_one_day_ago.number) + + {:ok, address: address} + end + + test "see list of coin balances", %{session: session, address: address} do + session + |> AddressPage.visit_page(address) + |> AddressPage.click_coin_balance_history() + |> assert_has(AddressPage.coin_balances(count: 2)) + end + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/features/viewing_app_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/features/viewing_app_test.exs new file mode 100644 index 0000000..fabb2a5 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/features/viewing_app_test.exs @@ -0,0 +1,139 @@ +defmodule BlockScoutWeb.ViewingAppTest do + @moduledoc false + + # use BlockScoutWeb.FeatureCase, async: true + + # alias BlockScoutWeb.AppPage + # alias BlockScoutWeb.Counters.BlocksIndexedCounter + # alias Explorer.Chain.Cache.Counters.AddressesCount + # alias Explorer.{Repo} + # alias Explorer.Chain.PendingBlockOperation + + # setup do + # start_supervised!(AddressesCount) + # AddressesCount.consolidate() + + # :ok + # end + + # describe "loading bar when indexing" do + # test "shows blocks indexed percentage", %{session: session} do + # [block | _] = + # for index <- 5..9 do + # insert(:block, number: index) + # end + + # :transaction + # |> insert() + # |> with_block(block) + + # assert Decimal.compare(Explorer.Chain.indexed_ratio_blocks(), Decimal.from_float(0.5)) == :eq + + # insert(:pending_block_operation, block_hash: block.hash, block_number: block.number) + + # session + # |> AppPage.visit_page() + # |> assert_has(AppPage.indexed_status("50% Blocks Indexed")) + # end + + # test "shows tokens loading", %{session: session} do + # [block | _] = + # for index <- 0..9 do + # insert(:block, number: index) + # end + + # :transaction + # |> insert() + # |> with_block(block) + + # assert Decimal.compare(Explorer.Chain.indexed_ratio_blocks(), 1) == :eq + + # insert(:pending_block_operation, block_hash: block.hash, block_number: block.number) + + # session + # |> AppPage.visit_page() + # |> assert_has(AppPage.indexed_status("Indexing Internal Transactions")) + # end + + # test "updates blocks indexed percentage", %{session: session} do + # [block | _] = + # for index <- 5..9 do + # insert(:block, number: index) + # end + + # :transaction + # |> insert() + # |> with_block(block) + + # BlocksIndexedCounter.calculate_blocks_indexed() + + # assert Decimal.compare(Explorer.Chain.indexed_ratio_blocks(), Decimal.from_float(0.5)) == :eq + + # insert(:pending_block_operation, block_hash: block.hash, block_number: block.number) + + # session + # |> AppPage.visit_page() + # |> assert_has(AppPage.indexed_status("50% Blocks Indexed")) + + # insert(:block, number: 4) + + # BlocksIndexedCounter.calculate_blocks_indexed() + + # assert_has(session, AppPage.indexed_status("60% Blocks Indexed")) + # end + + # test "updates when blocks are fully indexed", %{session: session} do + # [block | _] = + # for index <- 1..9 do + # insert(:block, number: index) + # end + + # :transaction + # |> insert() + # |> with_block(block) + + # BlocksIndexedCounter.calculate_blocks_indexed() + + # assert Decimal.compare(Explorer.Chain.indexed_ratio(), Decimal.from_float(0.9)) == :eq + + # insert(:pending_block_operation, block_hash: block.hash, block_number: block.number) + + # session + # |> AppPage.visit_page() + # |> assert_has(AppPage.indexed_status("90% Blocks Indexed")) + + # insert(:block, number: 0) + + # BlocksIndexedCounter.calculate_blocks_indexed() + + # assert_has(session, AppPage.indexed_status("Indexing Internal Transactions")) + # end + + # test "removes message when chain is indexed", %{session: session} do + # [block | _] = + # for index <- 0..9 do + # insert(:block, number: index) + # end + + # :transaction + # |> insert() + # |> with_block(block) + + # block_hash = block.hash + + # insert(:pending_block_operation, block_hash: block_hash, block_number: block.number) + + # BlocksIndexedCounter.calculate_blocks_indexed() + + # assert Decimal.compare(Explorer.Chain.indexed_ratio_blocks(), 1) == :eq + + # session + # |> AppPage.visit_page() + # |> assert_has(AppPage.indexed_status("Indexing Internal Transactions")) + + # BlocksIndexedCounter.calculate_blocks_indexed() + + # refute_has(session, AppPage.still_indexing?()) + # end + # end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/features/viewing_blocks_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/features/viewing_blocks_test.exs new file mode 100644 index 0000000..914ef68 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/features/viewing_blocks_test.exs @@ -0,0 +1,186 @@ +defmodule BlockScoutWeb.ViewingBlocksTest do + use BlockScoutWeb.FeatureCase, async: false + + alias BlockScoutWeb.{BlockListPage, BlockPage} + alias Explorer.Chain.Block + + setup do + timestamp = Timex.now() |> Timex.shift(hours: -1) + [oldest_block | _] = Enum.map(308..310, &insert(:block, number: &1, timestamp: timestamp, gas_used: 10)) + + newest_block = + insert(:block, %{ + gas_limit: 5_030_101, + gas_used: 1_010_101, + nonce: 123_456_789, + number: 311, + size: 9_999_999, + timestamp: timestamp + }) + + {:ok, first_shown_block: newest_block, last_shown_block: oldest_block} + end + + describe "block details page" do + test "show block detail page", %{session: session} do + block = insert(:block, number: 42) + + session + |> BlockPage.visit_page(block) + |> assert_has(BlockPage.detail_number(block)) + |> assert_has(BlockPage.page_type("Block Details")) + end + + test "block detail page has transactions", %{session: session} do + block = insert(:block, number: 42) + + transaction = + :transaction + |> insert() + |> with_block(block) + + session + |> BlockPage.visit_page(block) + |> assert_has(BlockPage.detail_number(block)) + |> assert_has(BlockPage.transaction(transaction)) + |> assert_has(BlockPage.transaction_status(transaction)) + end + + test "contract creation is shown for to_address in transaction list", %{session: session} do + block = insert(:block, number: 42) + + contract_address = insert(:contract_address) + + transaction = + :transaction + |> insert(to_address: nil) + |> with_contract_creation(contract_address) + |> with_block(block) + + internal_transaction = + :internal_transaction_create + |> insert( + transaction: transaction, + index: 0, + block_hash: transaction.block_hash, + block_index: 1 + ) + |> with_contract_creation(contract_address) + + session + |> BlockPage.visit_page(block) + |> assert_has(BlockPage.contract_creation(internal_transaction)) + end + + test "transaction with multiple token transfers shows all transfers if expanded", %{ + first_shown_block: block, + session: session + } do + contract_token_address = insert(:contract_address) + insert(:token, contract_address: contract_token_address) + + transaction = + :transaction + |> insert(to_address: contract_token_address) + |> with_block(block) + + insert_list( + 3, + :token_transfer, + transaction: transaction, + token_contract_address: contract_token_address, + block: block + ) + + session + |> BlockPage.visit_page(block) + |> assert_has(BlockPage.token_transfers(transaction, count: 1)) + |> click(BlockPage.token_transfers_expansion(transaction)) + |> assert_has(BlockPage.token_transfers(transaction, count: 3)) + end + + test "show reorg detail page", %{session: session} do + reorg = insert(:block, consensus: false) + + session + |> BlockPage.visit_page(reorg) + |> assert_has(BlockPage.detail_number(reorg)) + |> assert_has(BlockPage.page_type("Reorg Details")) + end + + test "show uncle detail page", %{session: session} do + uncle = insert(:block, consensus: false) + insert(:block_second_degree_relation, uncle_hash: uncle.hash) + + session + |> BlockPage.visit_page(uncle) + |> assert_has(BlockPage.detail_number(uncle)) + |> assert_has(BlockPage.page_type("Uncle Details")) + end + + test "show link to uncle on block detail page", %{session: session} do + block = insert(:block) + uncle = insert(:block, consensus: false) + insert(:block_second_degree_relation, uncle_hash: uncle.hash, nephew: block) + + session + |> BlockPage.visit_page(block) + |> assert_has(BlockPage.detail_number(block)) + |> assert_has(BlockPage.page_type("Block Details")) + |> assert_has(BlockPage.uncle_link(uncle)) + end + end + + describe "viewing blocks list" do + test "viewing the blocks index page", %{first_shown_block: block, session: session} do + session + |> BlockListPage.visit_page() + |> assert_has(BlockListPage.block(block)) + end + + test "inserts place holder blocks on render for out of order blocks", %{session: session} do + insert(:block, number: 315) + + session + |> BlockListPage.visit_page() + |> assert_has(BlockListPage.block(%Block{number: 314})) + |> assert_has(BlockListPage.place_holder_blocks(3)) + end + end + + describe "viewing uncle blocks list" do + setup do + uncles = + for _index <- 1..10 do + uncle = insert(:block, consensus: false) + insert(:block_second_degree_relation, uncle_hash: uncle.hash) + + transaction = insert(:transaction) + insert(:transaction_fork, hash: transaction.hash, uncle_hash: uncle.hash) + + uncle + end + + {:ok, %{uncles: uncles}} + end + + test "lists uncle blocks", %{session: session, uncles: [uncle | _]} do + session + |> BlockListPage.visit_uncles_page() + |> assert_has(BlockListPage.block(uncle)) + |> assert_has(BlockListPage.blocks(10)) + end + end + + describe "viewing reorg blocks list" do + test "lists uncle blocks", %{session: session} do + [reorg | _] = blocks = insert_list(10, :block, consensus: false) + Enum.each(blocks, fn b -> insert(:block, number: b.number, consensus: true) end) + + session + |> BlockListPage.visit_reorgs_page() + |> assert_has(BlockListPage.block(reorg)) + |> assert_has(BlockListPage.blocks(10)) + end + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/features/viewing_chain_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/features/viewing_chain_test.exs new file mode 100644 index 0000000..0b12f01 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/features/viewing_chain_test.exs @@ -0,0 +1,160 @@ +defmodule BlockScoutWeb.ViewingChainTest do + @moduledoc false + + use BlockScoutWeb.FeatureCase, + # MUST Be false because ETS tables for Counters are shared + async: false + + alias BlockScoutWeb.{AddressPage, BlockPage, ChainPage, TransactionPage} + alias Explorer.Chain.Block + alias Explorer.Chain.Cache.Counters.AddressesCount + + setup do + Supervisor.terminate_child(Explorer.Supervisor, Explorer.Chain.Cache.Blocks.child_id()) + Supervisor.restart_child(Explorer.Supervisor, Explorer.Chain.Cache.Blocks.child_id()) + + Enum.map(401..404, &insert(:block, number: &1)) + + block = insert(:block, number: 405) + + 4 + |> insert_list(:transaction) + |> with_block(block) + + :transaction + |> insert() + |> with_block(block) + + {:ok, + %{ + block: block + }} + end + + describe "viewing addresses" do + test "search for address", %{session: session} do + address = insert(:address) + + start_supervised!(AddressesCount) + AddressesCount.consolidate() + + session + |> ChainPage.visit_page() + |> ChainPage.search(to_string(address.hash)) + |> assert_has(AddressPage.detail_hash(address)) + end + end + + describe "viewing blocks" do + test "search for blocks from chain page", %{session: session} do + block = insert(:block, number: 6) + + start_supervised!(AddressesCount) + AddressesCount.consolidate() + + session + |> ChainPage.visit_page() + |> ChainPage.search(to_string(block.number)) + |> assert_has(BlockPage.detail_number(block)) + end + + test "blocks list", %{session: session} do + start_supervised!(AddressesCount) + AddressesCount.consolidate() + + session + |> ChainPage.visit_page() + |> assert_has(ChainPage.blocks(count: 4)) + end + + test "inserts place holder blocks on render for out of order blocks", %{session: session} do + insert(:block, number: 409) + + start_supervised!(AddressesCount) + AddressesCount.consolidate() + + session + |> ChainPage.visit_page() + |> assert_has(ChainPage.block(%Block{number: 408})) + |> assert_has(ChainPage.place_holder_blocks(3)) + end + end + + describe "viewing transactions" do + test "search for transactions", %{session: session} do + block = insert(:block) + + transaction = + insert(:transaction) + |> with_block(block) + + start_supervised!(AddressesCount) + AddressesCount.consolidate() + + session + |> ChainPage.visit_page() + |> ChainPage.search(to_string(transaction.hash)) + |> assert_has(TransactionPage.detail_hash(transaction)) + end + + test "transactions list", %{session: session} do + start_supervised!(AddressesCount) + AddressesCount.consolidate() + + session + |> ChainPage.visit_page() + |> assert_has(ChainPage.transactions(count: 5)) + end + + test "contract creation is shown for to_address", %{session: session, block: block} do + contract_address = insert(:contract_address) + + transaction = + :transaction + |> insert(to_address: nil) + |> with_contract_creation(contract_address) + |> with_block(block) + + start_supervised!(AddressesCount) + AddressesCount.consolidate() + + session + |> ChainPage.visit_page() + |> assert_has(ChainPage.contract_creation(transaction)) + end + + test "transaction with multiple token transfers shows all transfers if expanded", %{ + block: block, + session: session + } do + contract_token_address = insert(:contract_address) + insert(:token, contract_address: contract_token_address) + + transaction = + :transaction + |> insert(to_address: contract_token_address) + |> with_block(block, status: :ok) + + insert_list( + 3, + :token_transfer, + transaction: transaction, + token_contract_address: contract_token_address, + block: block + ) + + start_supervised!(AddressesCount) + AddressesCount.consolidate() + + ChainPage.visit_page(session) + + # wait for the `transactions-list` to load + :timer.sleep(1000) + + session + |> assert_has(ChainPage.token_transfers(transaction, count: 1)) + |> click(ChainPage.token_transfers_expansion(transaction)) + |> assert_has(ChainPage.token_transfers(transaction, count: 3)) + end + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/features/viewing_tokens_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/features/viewing_tokens_test.exs new file mode 100644 index 0000000..d896266 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/features/viewing_tokens_test.exs @@ -0,0 +1,21 @@ +defmodule BlockScoutWeb.ViewingTokensTest do + use BlockScoutWeb.FeatureCase, async: true + + alias BlockScoutWeb.TokenPage + + describe "viewing token holders" do + test "list the token holders", %{session: session} do + token = insert(:token) + + insert_list( + 2, + :address_current_token_balance, + token_contract_address_hash: token.contract_address_hash + ) + + session + |> TokenPage.visit_page(token.contract_address) + |> assert_has(TokenPage.token_holders(count: 2)) + end + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/features/viewing_transactions_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/features/viewing_transactions_test.exs new file mode 100644 index 0000000..9931bd4 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/features/viewing_transactions_test.exs @@ -0,0 +1,163 @@ +defmodule BlockScoutWeb.ViewingTransactionsTest do + @moduledoc false + + import Mox + + use BlockScoutWeb.FeatureCase, async: false + + alias BlockScoutWeb.{AddressPage, TransactionListPage, TransactionLogsPage, TransactionPage} + alias Explorer.Chain.Wei + + setup :set_mox_global + + setup do + block = + insert(:block, %{ + timestamp: Timex.now() |> Timex.shift(hours: -2), + gas_used: 123_987 + }) + + 3 + |> insert_list(:transaction) + |> with_block() + + pending = insert(:transaction, block_hash: nil, gas: 5891, index: nil) + pending_contract = insert(:transaction, to_address: nil, block_hash: nil, gas: 5891, index: nil) + + lincoln = insert(:address) + taft = insert(:address) + + # From Lincoln to Taft. + transaction_from_lincoln = + :transaction + |> insert(from_address: lincoln, to_address: taft) + |> with_block(block) + + transaction = + :transaction + |> insert( + value: Wei.from(Decimal.new(5656), :ether), + gas: Decimal.new(1_230_000_000_000_123_123), + gas_price: Decimal.new(7_890_000_000_898_912_300_045), + input: "0x000012", + nonce: 99045, + inserted_at: Timex.parse!("1970-01-01T00:00:18-00:00", "{ISO:Extended}"), + updated_at: Timex.parse!("1980-01-01T00:00:18-00:00", "{ISO:Extended}"), + from_address: taft, + to_address: lincoln + ) + |> with_block(block, gas_used: Decimal.new(1_230_000_000_000_123_000), status: :ok) + + insert(:log, address: lincoln, index: 0, transaction: transaction, block: block, block_number: block.number) + + internal = + insert(:internal_transaction, + index: 0, + transaction: transaction, + block_hash: transaction.block_hash, + block_index: 0 + ) + + {:ok, + %{ + pending: pending, + pending_contract: pending_contract, + internal: internal, + lincoln: lincoln, + taft: taft, + transaction: transaction, + transaction_from_lincoln: transaction_from_lincoln + }} + end + + describe "viewing transaction lists" do + test "viewing the default transactions tab", %{ + session: session, + transaction: transaction, + pending: pending + } do + session + |> TransactionListPage.visit_page() + |> assert_has(TransactionListPage.transaction(transaction)) + |> assert_has(TransactionListPage.transaction_status(transaction)) + |> refute_has(TransactionListPage.transaction(pending)) + end + + test "viewing the pending transactions list", %{ + pending: pending, + pending_contract: pending_contract, + session: session + } do + session + |> TransactionListPage.visit_pending_transactions_page() + |> assert_has(TransactionListPage.transaction(pending)) + |> assert_has(TransactionListPage.transaction(pending_contract)) + |> assert_has(TransactionListPage.transaction_status(pending_contract)) + end + + test "contract creation is shown for to_address on list page", %{session: session} do + contract_address = insert(:contract_address) + + transaction = + :transaction + |> insert(to_address: nil) + |> with_block() + |> with_contract_creation(contract_address) + + :internal_transaction_create + |> insert(transaction: transaction, index: 0, block_hash: transaction.block_hash, block_index: 0) + |> with_contract_creation(contract_address) + + session + |> TransactionListPage.visit_page() + |> assert_has(TransactionListPage.contract_creation(transaction)) + end + end + + describe "viewing a pending transaction page" do + test "can see a pending transaction's details", %{session: session, pending: pending} do + EthereumJSONRPC.Mox + |> expect(:json_rpc, fn %{id: _id, method: "net_version", params: []}, _options -> + {:ok, "100"} + end) + + session + |> TransactionPage.visit_page(pending) + |> assert_has(TransactionPage.detail_hash(pending)) + |> assert_has(TransactionPage.is_pending()) + end + end + + describe "viewing a transaction page" do + test "can navigate to transaction show from list page", %{session: session, transaction: transaction} do + session + |> TransactionListPage.visit_page() + |> TransactionListPage.click_transaction(transaction) + |> assert_has(TransactionPage.detail_hash(transaction)) + end + + test "can see a transaction's details", %{session: session, transaction: transaction} do + session + |> TransactionPage.visit_page(transaction) + |> assert_has(TransactionPage.detail_hash(transaction)) + end + + test "can view a transaction's logs", %{session: session, transaction: transaction} do + session + |> TransactionPage.visit_page(transaction) + |> TransactionPage.click_logs() + |> assert_has(TransactionLogsPage.logs(count: 1)) + end + + test "can visit an address from the transaction logs page", %{ + lincoln: lincoln, + session: session, + transaction: transaction + } do + session + |> TransactionLogsPage.visit_page(transaction) + |> TransactionLogsPage.click_address(lincoln) + |> assert_has(AddressPage.detail_hash(lincoln)) + end + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/graphql/schema/query/address_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/graphql/schema/query/address_test.exs new file mode 100644 index 0000000..3b0c3fe --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/graphql/schema/query/address_test.exs @@ -0,0 +1,596 @@ +defmodule BlockScoutWeb.GraphQL.Schema.Query.AddressTest do + use BlockScoutWeb.ConnCase + + describe "address field" do + test "with valid argument 'hash', returns all expected fields", %{conn: conn} do + address = insert(:address, fetched_coin_balance: 100) + + query = """ + query ($hash: AddressHash!) { + address(hash: $hash) { + hash + fetched_coin_balance + fetched_coin_balance_block_number + contract_code + } + } + """ + + variables = %{"hash" => to_string(address.hash)} + + conn = get(conn, "/api/v1/graphql", query: query, variables: variables) + + assert json_response(conn, 200) == %{ + "data" => %{ + "address" => %{ + "hash" => to_string(address.hash), + "fetched_coin_balance" => to_string(address.fetched_coin_balance.value), + "fetched_coin_balance_block_number" => address.fetched_coin_balance_block_number, + "contract_code" => nil + } + } + } + end + + test "with contract address, `contract_code` is serialized as expected", %{conn: conn} do + address = insert(:contract_address, fetched_coin_balance: 100) + + query = """ + query ($hash: AddressHash!) { + address(hash: $hash) { + contract_code + } + } + """ + + variables = %{"hash" => to_string(address.hash)} + + conn = get(conn, "/api/v1/graphql", query: query, variables: variables) + + assert json_response(conn, 200) == %{ + "data" => %{ + "address" => %{ + "contract_code" => to_string(address.contract_code) + } + } + } + end + + test "smart_contract returns all expected fields", %{conn: conn} do + address = insert(:address, fetched_coin_balance: 100) + smart_contract = insert(:smart_contract, address_hash: address.hash, contract_code_md5: "123") + + query = """ + query ($hash: AddressHash!) { + address(hash: $hash) { + fetched_coin_balance + smart_contract { + name + compiler_version + optimization + contract_source_code + abi + address_hash + } + } + } + """ + + variables = %{"hash" => to_string(address.hash)} + + conn = get(conn, "/api/v1/graphql", query: query, variables: variables) + + assert json_response(conn, 200) == %{ + "data" => %{ + "address" => %{ + "fetched_coin_balance" => to_string(address.fetched_coin_balance.value), + "smart_contract" => %{ + "name" => smart_contract.name, + "compiler_version" => smart_contract.compiler_version, + "optimization" => smart_contract.optimization, + "contract_source_code" => smart_contract.contract_source_code, + "abi" => Jason.encode!(smart_contract.abi), + "address_hash" => to_string(address.hash) + } + } + } + } + end + + test "errors for non-existent address hash", %{conn: conn} do + address = build(:address) + + query = """ + query ($hash: AddressHash!) { + address(hash: $hash) { + fetched_coin_balance + } + } + """ + + variables = %{"hash" => to_string(address.hash)} + + conn = get(conn, "/api/v1/graphql", query: query, variables: variables) + + assert %{"errors" => [error]} = json_response(conn, 200) + assert error["message"] =~ ~s(Address not found.) + end + + test "errors if argument 'hash' is missing", %{conn: conn} do + query = """ + query { + address { + fetched_coin_balance + } + } + """ + + variables = %{} + + conn = get(conn, "/api/v1/graphql", query: query, variables: variables) + + assert %{"errors" => [error]} = json_response(conn, 200) + assert error["message"] == ~s(In argument "hash": Expected type "AddressHash!", found null.) + end + + test "errors if argument 'hash' is not a valid address hash", %{conn: conn} do + query = """ + query ($hash: AddressHash!) { + address(hash: $hash) { + fetched_coin_balance + } + } + """ + + variables = %{"hash" => "someInvalidHash"} + + conn = get(conn, "/api/v1/graphql", query: query, variables: variables) + + assert %{"errors" => [error]} = json_response(conn, 200) + assert error["message"] =~ ~s(Argument "hash" has invalid value) + end + end + + describe "address transactions field" do + test "returns all expected transaction fields", %{conn: conn} do + address = insert(:address) + + transaction = insert(:transaction, from_address: address) + + query = """ + query ($hash: AddressHash!, $first: Int!) { + address(hash: $hash) { + transactions(first: $first) { + edges { + node { + hash + block_number + cumulative_gas_used + error + gas + gas_price + gas_used + index + input + nonce + r + s + status + v + value + from_address_hash + to_address_hash + created_contract_address_hash + } + } + } + } + } + """ + + variables = %{ + "hash" => to_string(address.hash), + "first" => 1 + } + + conn = post(conn, "/api/v1/graphql", query: query, variables: variables) + + assert json_response(conn, 200) == %{ + "data" => %{ + "address" => %{ + "transactions" => %{ + "edges" => [ + %{ + "node" => %{ + "hash" => to_string(transaction.hash), + "block_number" => transaction.block_number, + "cumulative_gas_used" => nil, + "error" => transaction.error, + "gas" => to_string(transaction.gas), + "gas_price" => to_string(transaction.gas_price.value), + "gas_used" => nil, + "index" => transaction.index, + "input" => to_string(transaction.input), + "nonce" => to_string(transaction.nonce), + "r" => to_string(transaction.r), + "s" => to_string(transaction.s), + "status" => nil, + "v" => to_string(transaction.v), + "value" => to_string(transaction.value.value), + "from_address_hash" => to_string(transaction.from_address_hash), + "to_address_hash" => to_string(transaction.to_address_hash), + "created_contract_address_hash" => nil + } + } + ] + } + } + } + } + end + + test "with address with zero transactions", %{conn: conn} do + address = insert(:address) + + query = """ + query ($hash: AddressHash!, $first: Int!) { + address(hash: $hash) { + transactions(first: $first) { + edges { + node { + hash + } + } + } + } + } + """ + + variables = %{ + "hash" => to_string(address.hash), + "first" => 1 + } + + conn = post(conn, "/api/v1/graphql", query: query, variables: variables) + + assert json_response(conn, 200) == %{ + "data" => %{ + "address" => %{ + "transactions" => %{ + "edges" => [] + } + } + } + } + end + + test "transactions are ordered by descending block and index", %{conn: conn} do + first_block = insert(:block) + second_block = insert(:block) + third_block = insert(:block) + + address = insert(:address) + + 3 + |> insert_list(:transaction, from_address: address) + |> with_block(second_block) + + 3 + |> insert_list(:transaction, from_address: address) + |> with_block(third_block) + + 3 + |> insert_list(:transaction, from_address: address) + |> with_block(first_block) + + query = """ + query ($hash: AddressHash!, $first: Int!) { + address(hash: $hash) { + transactions(first: $first) { + edges { + node { + hash + block_number + index + } + } + } + } + } + """ + + variables = %{ + "hash" => to_string(address.hash), + "first" => 3 + } + + conn = post(conn, "/api/v1/graphql", query: query, variables: variables) + + %{ + "data" => %{ + "address" => %{ + "transactions" => %{ + "edges" => transactions + } + } + } + } = json_response(conn, 200) + + block_number_and_index_order = + Enum.map(transactions, fn transaction -> + {transaction["node"]["block_number"], transaction["node"]["index"]} + end) + + assert block_number_and_index_order == Enum.sort(block_number_and_index_order, &(&1 >= &2)) + assert length(transactions) == 3 + assert Enum.all?(transactions, &(&1["node"]["block_number"] == third_block.number)) + end + + test "transactions are ordered by ascending block and index", %{conn: conn} do + first_block = insert(:block) + second_block = insert(:block) + third_block = insert(:block) + + address = insert(:address) + + 3 + |> insert_list(:transaction, from_address: address) + |> with_block(second_block) + + 3 + |> insert_list(:transaction, from_address: address) + |> with_block(third_block) + + 3 + |> insert_list(:transaction, from_address: address) + |> with_block(first_block) + + query = """ + query ($hash: AddressHash!, $first: Int!) { + address(hash: $hash) { + transactions(first: $first, order: ASC) { + edges { + node { + hash + block_number + index + } + } + } + } + } + """ + + variables = %{ + "hash" => to_string(address.hash), + "first" => 3 + } + + conn = post(conn, "/api/v1/graphql", query: query, variables: variables) + + %{ + "data" => %{ + "address" => %{ + "transactions" => %{ + "edges" => transactions + } + } + } + } = json_response(conn, 200) + + block_number_and_index_order = + Enum.map(transactions, fn transaction -> + {transaction["node"]["block_number"], transaction["node"]["index"]} + end) + + assert block_number_and_index_order == Enum.sort(block_number_and_index_order, &(&1 < &2)) + assert length(transactions) == 3 + assert Enum.all?(transactions, &(&1["node"]["block_number"] == first_block.number)) + end + + test "complexity correlates to 'first' or 'last' arguments", %{conn: conn} do + address = build(:address) + + query = """ + query ($hash: AddressHash!, $first: Int!) { + address(hash: $hash) { + transactions(first: $first) { + edges { + node { + hash + } + } + } + } + } + """ + + variables = %{ + "hash" => to_string(address.hash), + # Add +5 because of the increase in complexity limit. For more info, see + # https://github.com/blockscout/blockscout/pull/9630 + "first" => 67 + 5 + } + + conn = post(conn, "/api/v1/graphql", query: query, variables: variables) + + assert %{"errors" => [error1, error2, error3]} = json_response(conn, 200) + assert error1["message"] =~ ~s(Field transactions is too complex) + assert error2["message"] =~ ~s(Field address is too complex) + assert error3["message"] =~ ~s(Operation is too complex) + end + + test "with 'last' and 'count' arguments", %{conn: conn} do + # "`last: N` must always be accompanied by either a `before:` argument to + # the query, or an explicit `count:` option to the `from_query` call. + # Otherwise it is impossible to derive the required offset." + # https://hexdocs.pm/absinthe_relay/Absinthe.Relay.Connection.html#from_query/4 + # + # This test ensures support of a 'count' argument. + + first_block = insert(:block) + second_block = insert(:block) + third_block = insert(:block) + + address = insert(:address) + + 3 + |> insert_list(:transaction, from_address: address) + |> with_block(second_block) + + 3 + |> insert_list(:transaction, from_address: address) + |> with_block(third_block) + + 3 + |> insert_list(:transaction, from_address: address) + |> with_block(first_block) + + query = """ + query ($hash: AddressHash!, $last: Int!, $count: Int!) { + address(hash: $hash) { + transactions(last: $last, count: $count) { + edges { + node { + hash + block_number + } + } + } + } + } + """ + + variables = %{ + "hash" => to_string(address.hash), + "last" => 3, + "count" => 9 + } + + conn = post(conn, "/api/v1/graphql", query: query, variables: variables) + + %{ + "data" => %{ + "address" => %{ + "transactions" => %{ + "edges" => transactions + } + } + } + } = json_response(conn, 200) + + assert length(transactions) == 3 + assert Enum.all?(transactions, &(&1["node"]["block_number"] == first_block.number)) + end + + test "pagination support with 'first' and 'after' arguments", %{conn: conn} do + first_block = insert(:block) + second_block = insert(:block) + third_block = insert(:block) + + address = insert(:address) + + 3 + |> insert_list(:transaction, from_address: address) + |> with_block(second_block) + + 3 + |> insert_list(:transaction, from_address: address) + |> with_block(third_block) + + 3 + |> insert_list(:transaction, from_address: address) + |> with_block(first_block) + + query1 = """ + query ($hash: AddressHash!, $first: Int!) { + address(hash: $hash) { + transactions(first: $first) { + page_info { + has_next_page + has_previous_page + } + edges { + node { + hash + block_number + } + cursor + } + } + } + } + """ + + variables1 = %{ + "hash" => to_string(address.hash), + "first" => 3 + } + + conn = post(conn, "/api/v1/graphql", query: query1, variables: variables1) + + %{"data" => %{"address" => %{"transactions" => page1}}} = json_response(conn, 200) + + assert page1["page_info"] == %{"has_next_page" => true, "has_previous_page" => false} + assert Enum.all?(page1["edges"], &(&1["node"]["block_number"] == third_block.number)) + + last_cursor_page1 = + page1 + |> Map.get("edges") + |> List.last() + |> Map.get("cursor") + + query2 = """ + query ($hash: AddressHash!, $first: Int!, $after: String!) { + address(hash: $hash) { + transactions(first: $first, after: $after) { + page_info { + has_next_page + has_previous_page + } + edges { + node { + hash + block_number + } + cursor + } + } + } + } + """ + + variables2 = %{ + "hash" => to_string(address.hash), + "first" => 3, + "after" => last_cursor_page1 + } + + conn = post(conn, "/api/v1/graphql", query: query2, variables: variables2) + + %{"data" => %{"address" => %{"transactions" => page2}}} = json_response(conn, 200) + + assert page2["page_info"] == %{"has_next_page" => true, "has_previous_page" => true} + assert Enum.all?(page2["edges"], &(&1["node"]["block_number"] == second_block.number)) + + last_cursor_page2 = + page2 + |> Map.get("edges") + |> List.last() + |> Map.get("cursor") + + variables3 = %{ + "hash" => to_string(address.hash), + "first" => 3, + "after" => last_cursor_page2 + } + + conn = post(conn, "/api/v1/graphql", query: query2, variables: variables3) + + %{"data" => %{"address" => %{"transactions" => page3}}} = json_response(conn, 200) + + assert page3["page_info"] == %{"has_next_page" => false, "has_previous_page" => true} + assert Enum.all?(page3["edges"], &(&1["node"]["block_number"] == first_block.number)) + end + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/graphql/schema/query/addresses_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/graphql/schema/query/addresses_test.exs new file mode 100644 index 0000000..32bbc5f --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/graphql/schema/query/addresses_test.exs @@ -0,0 +1,185 @@ +defmodule BlockScoutWeb.GraphQL.Schema.Query.AddressesTest do + use BlockScoutWeb.ConnCase + + describe "addresses field" do + test "with valid argument 'hashes', returns all expected fields", %{conn: conn} do + address = insert(:address, fetched_coin_balance: 100) + + query = """ + query ($hashes: [AddressHash!]!) { + addresses(hashes: $hashes) { + hash + fetched_coin_balance + fetched_coin_balance_block_number + contract_code + } + } + """ + + variables = %{"hashes" => to_string(address.hash)} + + conn = get(conn, "/api/v1/graphql", query: query, variables: variables) + + assert json_response(conn, 200) == %{ + "data" => %{ + "addresses" => [ + %{ + "hash" => to_string(address.hash), + "fetched_coin_balance" => to_string(address.fetched_coin_balance.value), + "fetched_coin_balance_block_number" => address.fetched_coin_balance_block_number, + "contract_code" => nil + } + ] + } + } + end + + test "with contract address, `contract_code` is serialized as expected", %{conn: conn} do + address = insert(:contract_address, fetched_coin_balance: 100) + + query = """ + query ($hashes: [AddressHash!]!) { + addresses(hashes: $hashes) { + contract_code + } + } + """ + + variables = %{"hashes" => to_string(address.hash)} + + conn = get(conn, "/api/v1/graphql", query: query, variables: variables) + + assert json_response(conn, 200) == %{ + "data" => %{ + "addresses" => [ + %{ + "contract_code" => to_string(address.contract_code) + } + ] + } + } + end + + test "smart_contract returns all expected fields", %{conn: conn} do + address = insert(:address, fetched_coin_balance: 100) + smart_contract = insert(:smart_contract, address_hash: address.hash, contract_code_md5: "123") + + query = """ + query ($hashes: [AddressHash!]!) { + addresses(hashes: $hashes) { + fetched_coin_balance + smart_contract { + name + compiler_version + optimization + contract_source_code + abi + address_hash + } + } + } + """ + + variables = %{"hashes" => to_string(address.hash)} + + conn = get(conn, "/api/v1/graphql", query: query, variables: variables) + + assert json_response(conn, 200) == %{ + "data" => %{ + "addresses" => [ + %{ + "fetched_coin_balance" => to_string(address.fetched_coin_balance.value), + "smart_contract" => %{ + "name" => smart_contract.name, + "compiler_version" => smart_contract.compiler_version, + "optimization" => smart_contract.optimization, + "contract_source_code" => smart_contract.contract_source_code, + "abi" => Jason.encode!(smart_contract.abi), + "address_hash" => to_string(address.hash) + } + } + ] + } + } + end + + test "errors for non-existent address hashes", %{conn: conn} do + address = build(:address) + + query = """ + query ($hashes: [AddressHash!]!) { + addresses(hashes: $hashes) { + fetched_coin_balance + } + } + """ + + variables = %{"hashes" => [to_string(address.hash)]} + + conn = get(conn, "/api/v1/graphql", query: query, variables: variables) + + assert %{"errors" => [error]} = json_response(conn, 200) + assert error["message"] =~ ~s(Addresses not found.) + end + + test "errors if argument 'hashes' is missing", %{conn: conn} do + query = """ + query { + addresses { + fetched_coin_balance + } + } + """ + + variables = %{} + + conn = get(conn, "/api/v1/graphql", query: query, variables: variables) + + assert %{"errors" => [error]} = json_response(conn, 200) + assert error["message"] == ~s(In argument "hashes": Expected type "[AddressHash!]!", found null.) + end + + test "errors if argument 'hashes' is not a list of address hashes", %{conn: conn} do + query = """ + query ($hashes: [AddressHash!]!) { + addresses(hashes: $hashes) { + fetched_coin_balance + } + } + """ + + variables = %{"hashes" => ["someInvalidHash"]} + + conn = get(conn, "/api/v1/graphql", query: query, variables: variables) + + assert %{"errors" => [error]} = json_response(conn, 200) + assert error["message"] =~ ~s(Argument "hashes" has invalid value) + end + + test "correlates complexity to size of 'hashes' argument", %{conn: conn} do + # max of 53 addresses with four fields of complexity 1 can be fetched per + # query: 53 * 4 = 212, which is the upper bound before reaching the max + # complexity limit of 215 + hashes = 54 |> build_list(:address) |> Enum.map(&to_string(&1.hash)) + + query = """ + query ($hashes: [AddressHash!]!) { + addresses(hashes: $hashes) { + hash + fetched_coin_balance + fetched_coin_balance_block_number + contract_code + } + } + """ + + variables = %{"hashes" => hashes} + + conn = get(conn, "/api/v1/graphql", query: query, variables: variables) + + assert %{"errors" => [error1, error2]} = json_response(conn, 200) + assert error1["message"] =~ ~s(Field addresses is too complex) + assert error2["message"] =~ ~s(Operation is too complex) + end + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/graphql/schema/query/block_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/graphql/schema/query/block_test.exs new file mode 100644 index 0000000..8f2f6ba --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/graphql/schema/query/block_test.exs @@ -0,0 +1,108 @@ +defmodule BlockScoutWeb.GraphQL.Schema.Query.BlockTest do + use BlockScoutWeb.ConnCase + + describe "block field" do + test "with valid argument 'number', returns all expected fields", %{conn: conn} do + block = insert(:block) + + query = """ + query ($number: Int!) { + block(number: $number) { + hash + consensus + difficulty + gas_limit + gas_used + nonce + number + size + timestamp + total_difficulty + miner_hash + parent_hash + parent_hash + } + } + """ + + variables = %{"number" => block.number} + + conn = post(conn, "/api/v1/graphql", query: query, variables: variables) + + assert json_response(conn, 200) == %{ + "data" => %{ + "block" => %{ + "hash" => to_string(block.hash), + "consensus" => block.consensus, + "difficulty" => to_string(block.difficulty), + "gas_limit" => to_string(block.gas_limit), + "gas_used" => to_string(block.gas_used), + "nonce" => to_string(block.nonce), + "number" => block.number, + "size" => block.size, + "timestamp" => DateTime.to_iso8601(block.timestamp), + "total_difficulty" => to_string(block.total_difficulty), + "miner_hash" => to_string(block.miner_hash), + "parent_hash" => to_string(block.parent_hash) + } + } + } + end + + test "errors for non-existent block number", %{conn: conn} do + block = insert(:block) + non_existent_block_number = block.number + 1 + + query = """ + query ($number: Int!) { + block(number: $number) { + number + } + } + """ + + variables = %{"number" => non_existent_block_number} + + conn = post(conn, "/api/v1/graphql", query: query, variables: variables) + + assert %{"errors" => [error]} = json_response(conn, 200) + assert error["message"] =~ ~s(Block number #{non_existent_block_number} was not found) + end + + test "errors if argument 'number' is missing", %{conn: conn} do + insert(:block) + + query = """ + { + block { + number + } + } + """ + + conn = get(conn, "/api/v1/graphql", query: query) + + assert %{"errors" => [error]} = json_response(conn, 200) + assert error["message"] == ~s(In argument "number": Expected type "Int!", found null.) + end + + test "errors if argument 'number' is not an integer", %{conn: conn} do + insert(:block) + + query = """ + query ($number: Int!) { + block(number: $number) { + number + } + } + """ + + variables = %{"number" => "invalid"} + + conn = get(conn, "/api/v1/graphql", query: query, variables: variables) + + assert %{"errors" => [error]} = json_response(conn, 200) + assert error["message"] =~ ~s(Argument "number" has invalid value) + end + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/graphql/schema/query/introspection_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/graphql/schema/query/introspection_test.exs new file mode 100644 index 0000000..792d4e7 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/graphql/schema/query/introspection_test.exs @@ -0,0 +1,125 @@ +defmodule BlockScoutWeb.Schema.Query.IntrospectionTest do + use BlockScoutWeb.ConnCase + + test "fetches schema", %{conn: conn} do + introspection_query = ~S""" + query IntrospectionQuery { + __schema { + queryType { + name + } + mutationType { + name + } + + types { + ...FullType + } + directives { + name + description + + locations + args { + ...InputValue + } + } + } + } + + fragment FullType on __Type { + kind + name + description + + fields(includeDeprecated: true) { + name + description + args { + ...InputValue + } + type { + ...TypeRef + } + isDeprecated + deprecationReason + } + inputFields { + ...InputValue + } + interfaces { + ...TypeRef + } + enumValues(includeDeprecated: true) { + name + description + isDeprecated + deprecationReason + } + possibleTypes { + ...TypeRef + } + } + + fragment InputValue on __InputValue { + name + description + type { + ...TypeRef + } + defaultValue + } + + fragment TypeRef on __Type { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + } + } + } + } + } + } + } + } + } + } + """ + + params = %{ + "operationName" => "IntrospectionQuery", + "query" => introspection_query + } + + conn = get(conn, "/api/v1/graphql", params) + response = json_response(conn, 200) + + assert %{"data" => %{"__schema" => %{"directives" => _}}} = response + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/graphql/schema/query/node_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/graphql/schema/query/node_test.exs new file mode 100644 index 0000000..05c38b5 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/graphql/schema/query/node_test.exs @@ -0,0 +1,209 @@ +defmodule BlockScoutWeb.GraphQL.Schema.Query.NodeTest do + use BlockScoutWeb.ConnCase + + describe "node field" do + test "with valid argument 'id' for a transaction", %{conn: conn} do + transaction = insert(:transaction) + + query = """ + query($id: ID!) { + node(id: $id) { + ... on Transaction { + id + hash + } + } + } + """ + + id = Base.encode64("Transaction:#{transaction.hash}") + + variables = %{"id" => id} + + conn = get(conn, "/api/v1/graphql", query: query, variables: variables) + + assert json_response(conn, 200) == %{ + "data" => %{ + "node" => %{ + "id" => id, + "hash" => to_string(transaction.hash) + } + } + } + end + + test "with 'id' for non-existent transaction", %{conn: conn} do + transaction = build(:transaction) + + query = """ + query($id: ID!) { + node(id: $id) { + ... on Transaction { + id + hash + } + } + } + """ + + id = Base.encode64("Transaction:#{transaction.hash}") + + variables = %{"id" => id} + + conn = get(conn, "/api/v1/graphql", query: query, variables: variables) + + %{"errors" => [error]} = json_response(conn, 200) + + assert error["message"] == "Transaction not found." + end + + test "with valid argument 'id' for an internal transaction", %{conn: conn} do + transaction = insert(:transaction) |> with_block() + + internal_transaction = + insert(:internal_transaction, + transaction: transaction, + index: 0, + block_hash: transaction.block_hash, + block_index: 0 + ) + + query = """ + query($id: ID!) { + node(id: $id) { + ... on InternalTransaction { + id + transaction_hash + index + } + } + } + """ + + id = + %{transaction_hash: to_string(transaction.hash), index: internal_transaction.index} + |> Jason.encode!() + |> (fn unique_id -> "InternalTransaction:#{unique_id}" end).() + |> Base.encode64() + + variables = %{"id" => id} + + conn = get(conn, "/api/v1/graphql", query: query, variables: variables) + + assert json_response(conn, 200) == %{ + "data" => %{ + "node" => %{ + "id" => id, + "transaction_hash" => to_string(transaction.hash), + "index" => internal_transaction.index + } + } + } + end + + test "with 'id' for non-existent internal transaction", %{conn: conn} do + transaction = insert(:transaction) |> with_block() + + internal_transaction = + build(:internal_transaction, + transaction: transaction, + index: 0, + block_hash: transaction.block_hash, + block_index: 0 + ) + + query = """ + query($id: ID!) { + node(id: $id) { + ... on InternalTransaction { + id + transaction_hash + index + } + } + } + """ + + id = + %{transaction_hash: to_string(transaction.hash), index: internal_transaction.index} + |> Jason.encode!() + |> (fn unique_id -> "InternalTransaction:#{unique_id}" end).() + |> Base.encode64() + + variables = %{"id" => id} + + conn = get(conn, "/api/v1/graphql", query: query, variables: variables) + + %{"errors" => [error]} = json_response(conn, 200) + + assert error["message"] == "Internal transaction not found." + end + + test "with valid argument 'id' for a token_transfer", %{conn: conn} do + transaction = insert(:transaction) + token_transfer = insert(:token_transfer, transaction: transaction) + + query = """ + query($id: ID!) { + node(id: $id) { + ... on TokenTransfer { + id + transaction_hash + log_index + } + } + } + """ + + id = + %{transaction_hash: to_string(token_transfer.transaction_hash), log_index: token_transfer.log_index} + |> Jason.encode!() + |> (fn unique_id -> "TokenTransfer:#{unique_id}" end).() + |> Base.encode64() + + variables = %{"id" => id} + + conn = get(conn, "/api/v1/graphql", query: query, variables: variables) + + assert json_response(conn, 200) == %{ + "data" => %{ + "node" => %{ + "id" => id, + "transaction_hash" => to_string(token_transfer.transaction_hash), + "log_index" => token_transfer.log_index + } + } + } + end + + test "with id for non-existent token transfer", %{conn: conn} do + transaction = build(:transaction) + + query = """ + query($id: ID!) { + node(id: $id) { + ... on TokenTransfer { + id + transaction_hash + log_index + } + } + } + """ + + id = + %{transaction_hash: to_string(transaction.hash), log_index: 0} + |> Jason.encode!() + |> (fn unique_id -> "TokenTransfer:#{unique_id}" end).() + |> Base.encode64() + + variables = %{"id" => id} + + conn = get(conn, "/api/v1/graphql", query: query, variables: variables) + + %{"errors" => [error]} = json_response(conn, 200) + + assert error["message"] == "Token transfer not found." + end + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/graphql/schema/query/token_transfers_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/graphql/schema/query/token_transfers_test.exs new file mode 100644 index 0000000..4c0d149 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/graphql/schema/query/token_transfers_test.exs @@ -0,0 +1,327 @@ +defmodule BlockScoutWeb.GraphQL.Schema.Query.TokenTransfersTest do + use BlockScoutWeb.ConnCase + + describe "token_transfers field" do + test "with valid argument, returns all expected fields", %{conn: conn} do + transaction = insert(:transaction) + token_transfer = insert(:token_transfer, transaction: transaction, token_ids: [5], amounts: [10]) + address_hash = to_string(token_transfer.token_contract_address_hash) + + query = """ + query ($token_contract_address_hash: AddressHash!, $first: Int!) { + token_transfers(token_contract_address_hash: $token_contract_address_hash, first: $first) { + edges { + node { + amount + amounts + block_number + log_index + token_ids + from_address_hash + to_address_hash + token_contract_address_hash + transaction_hash + } + } + } + } + """ + + variables = %{ + "token_contract_address_hash" => address_hash, + "first" => 1 + } + + conn = post(conn, "/api/v1/graphql", query: query, variables: variables) + + assert json_response(conn, 200) == %{ + "data" => %{ + "token_transfers" => %{ + "edges" => [ + %{ + "node" => %{ + "amount" => to_string(token_transfer.amount), + "amounts" => Enum.map(token_transfer.amounts, &to_string/1), + "block_number" => token_transfer.block_number, + "log_index" => token_transfer.log_index, + "token_ids" => Enum.map(token_transfer.token_ids, &to_string/1), + "from_address_hash" => to_string(token_transfer.from_address_hash), + "to_address_hash" => to_string(token_transfer.to_address_hash), + "token_contract_address_hash" => to_string(token_transfer.token_contract_address_hash), + "transaction_hash" => to_string(token_transfer.transaction_hash) + } + } + ] + } + } + } + end + + test "with token contract address with zero token transfers", %{conn: conn} do + address = insert(:contract_address) + + query = """ + query ($token_contract_address_hash: AddressHash!, $first: Int!) { + token_transfers(token_contract_address_hash: $token_contract_address_hash, first: $first) { + edges { + node { + amount + block_number + log_index + from_address_hash + to_address_hash + token_contract_address_hash + transaction_hash + } + } + } + } + """ + + variables = %{ + "token_contract_address_hash" => to_string(address.hash), + "first" => 10 + } + + conn = post(conn, "/api/v1/graphql", query: query, variables: variables) + + assert json_response(conn, 200) == %{ + "data" => %{ + "token_transfers" => %{ + "edges" => [] + } + } + } + end + + test "complexity correlates to first or last argument", %{conn: conn} do + address = insert(:contract_address) + + query1 = """ + query ($token_contract_address_hash: AddressHash!, $first: Int!) { + token_transfers(token_contract_address_hash: $token_contract_address_hash, first: $first) { + edges { + node { + amount + from_address_hash + to_address_hash + } + } + } + } + """ + + variables1 = %{ + "token_contract_address_hash" => to_string(address.hash), + "first" => 55 + } + + response1 = + conn + |> post("/api/v1/graphql", query: query1, variables: variables1) + |> json_response(200) + + %{"errors" => [response1_error1, response1_error2]} = response1 + + assert response1_error1["message"] =~ ~s(Field token_transfers is too complex) + assert response1_error2["message"] =~ ~s(Operation is too complex) + + query2 = """ + query ($token_contract_address_hash: AddressHash!, $last: Int!) { + token_transfers(token_contract_address_hash: $token_contract_address_hash, last: $last) { + edges { + node { + amount + from_address_hash + to_address_hash + } + } + } + } + """ + + variables2 = %{ + "token_contract_address_hash" => to_string(address.hash), + "last" => 55 + } + + response2 = + conn + |> post("/api/v1/graphql", query: query2, variables: variables2) + |> json_response(200) + + %{"errors" => [response2_error1, response2_error2]} = response2 + assert response2_error1["message"] =~ ~s(Field token_transfers is too complex) + assert response2_error2["message"] =~ ~s(Operation is too complex) + end + + test "with 'last' and 'count' arguments", %{conn: conn} do + # "`last: N` must always be accompanied by either a `before:` argument to + # the query, or an explicit `count:` option to the `from_query` call. + # Otherwise it is impossible to derive the required offset." + # https://hexdocs.pm/absinthe_relay/Absinthe.Relay.Connection.html#from_query/4 + # + # This test ensures support for a 'count' argument. + + address = insert(:contract_address) + + blocks = insert_list(2, :block) + + [transaction1, transaction2] = + for block <- blocks do + :transaction + |> insert() + |> with_block(block) + end + + token_transfer_attrs1 = %{ + block_number: transaction1.block_number, + transaction: transaction1, + token_contract_address: address + } + + token_transfer_attrs2 = %{ + block_number: transaction2.block_number, + transaction: transaction2, + token_contract_address: address + } + + insert(:token_transfer, token_transfer_attrs1) + insert(:token_transfer, token_transfer_attrs2) + + query = """ + query ($token_contract_address_hash: AddressHash!, $last: Int!, $count: Int) { + token_transfers(token_contract_address_hash: $token_contract_address_hash, last: $last, count: $count) { + edges { + node { + transaction_hash + } + } + } + } + """ + + variables = %{ + "token_contract_address_hash" => to_string(address.hash), + "last" => 1, + "count" => 2 + } + + [token_transfer] = + conn + |> post("/api/v1/graphql", query: query, variables: variables) + |> json_response(200) + |> get_in(["data", "token_transfers", "edges"]) + + assert token_transfer["node"]["transaction_hash"] == to_string(transaction1.hash) + end + + test "pagination support with 'first' and 'after' arguments", %{conn: conn} do + address = insert(:contract_address) + + blocks = insert_list(3, :block) + + [transaction1, transaction2, transaction3] = + transactions = + for block <- blocks do + :transaction + |> insert() + |> with_block(block) + end + + for transaction <- transactions do + token_transfer_attrs = %{ + block_number: transaction.block_number, + transaction: transaction, + token_contract_address: address + } + + insert(:token_transfer, token_transfer_attrs) + end + + query1 = """ + query ($token_contract_address_hash: AddressHash!, $first: Int!) { + token_transfers(token_contract_address_hash: $token_contract_address_hash, first: $first) { + page_info { + has_next_page + has_previous_page + } + edges { + node { + transaction_hash + } + cursor + } + } + } + """ + + variables1 = %{ + "token_contract_address_hash" => to_string(address.hash), + "first" => 1 + } + + conn = post(conn, "/api/v1/graphql", query: query1, variables: variables1) + + %{"data" => %{"token_transfers" => page1}} = json_response(conn, 200) + + assert page1["page_info"] == %{"has_next_page" => true, "has_previous_page" => false} + assert Enum.all?(page1["edges"], &(&1["node"]["transaction_hash"] == to_string(transaction3.hash))) + + last_cursor_page1 = + page1 + |> Map.get("edges") + |> List.last() + |> Map.get("cursor") + + query2 = """ + query ($token_contract_address_hash: AddressHash!, $first: Int!, $after: String!) { + token_transfers(token_contract_address_hash: $token_contract_address_hash, first: $first, after: $after) { + page_info { + has_next_page + has_previous_page + } + edges { + node { + transaction_hash + } + cursor + } + } + } + """ + + variables2 = %{ + "token_contract_address_hash" => to_string(address.hash), + "first" => 1, + "after" => last_cursor_page1 + } + + conn = post(conn, "/api/v1/graphql", query: query2, variables: variables2) + + %{"data" => %{"token_transfers" => page2}} = json_response(conn, 200) + + assert page2["page_info"] == %{"has_next_page" => true, "has_previous_page" => true} + assert Enum.all?(page2["edges"], &(&1["node"]["transaction_hash"] == to_string(transaction2.hash))) + + last_cursor_page2 = + page2 + |> Map.get("edges") + |> List.last() + |> Map.get("cursor") + + variables3 = %{ + "token_contract_address_hash" => to_string(address.hash), + "first" => 1, + "after" => last_cursor_page2 + } + + conn = post(conn, "/api/v1/graphql", query: query2, variables: variables3) + + %{"data" => %{"token_transfers" => page3}} = json_response(conn, 200) + + assert page3["page_info"] == %{"has_next_page" => false, "has_previous_page" => true} + assert Enum.all?(page3["edges"], &(&1["node"]["transaction_hash"] == to_string(transaction1.hash))) + end + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/graphql/schema/query/transaction_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/graphql/schema/query/transaction_test.exs new file mode 100644 index 0000000..f13049b --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/graphql/schema/query/transaction_test.exs @@ -0,0 +1,552 @@ +defmodule BlockScoutWeb.GraphQL.Schema.Query.TransactionTest do + use BlockScoutWeb.ConnCase + + describe "transaction field" do + test "with valid argument 'hash', returns all expected fields", %{conn: conn} do + block = insert(:block) + + transaction = + :transaction + |> insert() + |> with_block(block, status: :ok) + + query = """ + query ($hash: FullHash!) { + transaction(hash: $hash) { + hash + block_number + cumulative_gas_used + error + gas + gas_price + gas_used + index + input + nonce + r + s + status + v + value + from_address_hash + to_address_hash + created_contract_address_hash + } + } + """ + + variables = %{"hash" => to_string(transaction.hash)} + + conn = get(conn, "/api/v1/graphql", query: query, variables: variables) + + assert json_response(conn, 200) == %{ + "data" => %{ + "transaction" => %{ + "hash" => to_string(transaction.hash), + "block_number" => transaction.block_number, + "cumulative_gas_used" => to_string(transaction.cumulative_gas_used), + "error" => transaction.error, + "gas" => to_string(transaction.gas), + "gas_price" => to_string(transaction.gas_price.value), + "gas_used" => to_string(transaction.gas_used), + "index" => transaction.index, + "input" => to_string(transaction.input), + "nonce" => to_string(transaction.nonce), + "r" => to_string(transaction.r), + "s" => to_string(transaction.s), + "status" => transaction.status |> to_string() |> String.upcase(), + "v" => to_string(transaction.v), + "value" => to_string(transaction.value.value), + "from_address_hash" => to_string(transaction.from_address_hash), + "to_address_hash" => to_string(transaction.to_address_hash), + "created_contract_address_hash" => nil + } + } + } + end + + test "errors for non-existent transaction hash", %{conn: conn} do + transaction = build(:transaction) + + query = """ + query ($hash: FullHash!) { + transaction(hash: $hash) { + status + } + } + """ + + variables = %{"hash" => to_string(transaction.hash)} + + conn = get(conn, "/api/v1/graphql", query: query, variables: variables) + + assert %{"errors" => [error]} = json_response(conn, 200) + assert error["message"] == "Transaction not found." + end + + test "errors if argument 'hash' is missing", %{conn: conn} do + query = """ + { + transaction { + status + } + } + """ + + conn = get(conn, "/api/v1/graphql", query: query) + + assert %{"errors" => [error]} = json_response(conn, 200) + assert error["message"] == ~s(In argument "hash": Expected type "FullHash!", found null.) + end + + test "errors if argument 'hash' is not a 'FullHash'", %{conn: conn} do + query = """ + query ($hash: FullHash!) { + transaction(hash: $hash) { + status + } + } + """ + + variables = %{"hash" => "0x000"} + + conn = get(conn, "/api/v1/graphql", query: query, variables: variables) + + assert %{"errors" => [error]} = json_response(conn, 200) + assert error["message"] =~ ~s(Argument "hash" has invalid value) + end + end + + describe "transaction internal_transactions field" do + test "returns all expected internal_transaction fields", %{conn: conn} do + address = insert(:address) + contract_address = insert(:contract_address) + + block = insert(:block) + + transaction = + :transaction + |> insert(from_address: address) + |> with_contract_creation(contract_address) + |> with_block(block) + + internal_transaction_attributes = %{ + transaction: transaction, + index: 0, + from_address: address, + call_type: :call, + block_hash: transaction.block_hash, + block_index: 0 + } + + internal_transaction = + :internal_transaction_create + |> insert(internal_transaction_attributes) + |> with_contract_creation(contract_address) + + query = """ + query ($hash: FullHash!, $first: Int!) { + transaction(hash: $hash) { + internal_transactions(first: $first) { + edges { + node { + call_type + created_contract_code + error + gas + gas_used + index + init + input + output + trace_address + type + value + block_number + transaction_index + created_contract_address_hash + from_address_hash + to_address_hash + transaction_hash + } + } + } + } + } + """ + + variables = %{ + "hash" => to_string(transaction.hash), + "first" => 1 + } + + conn = post(conn, "/api/v1/graphql", query: query, variables: variables) + + assert json_response(conn, 200) == %{ + "data" => %{ + "transaction" => %{ + "internal_transactions" => %{ + "edges" => [ + %{ + "node" => %{ + "call_type" => internal_transaction.call_type |> to_string() |> String.upcase(), + "created_contract_code" => to_string(internal_transaction.created_contract_code), + "error" => internal_transaction.error, + "gas" => to_string(internal_transaction.gas), + "gas_used" => to_string(internal_transaction.gas_used), + "index" => internal_transaction.index, + "init" => to_string(internal_transaction.init), + "input" => nil, + "output" => nil, + "trace_address" => Jason.encode!(internal_transaction.trace_address), + "type" => internal_transaction.type |> to_string() |> String.upcase(), + "value" => to_string(internal_transaction.value.value), + "block_number" => internal_transaction.block_number, + "transaction_index" => internal_transaction.transaction_index, + "created_contract_address_hash" => + to_string(internal_transaction.created_contract_address_hash), + "from_address_hash" => to_string(internal_transaction.from_address_hash), + "to_address_hash" => nil, + "transaction_hash" => to_string(internal_transaction.transaction_hash) + } + } + ] + } + } + } + } + end + + test "with transaction with zero internal transactions", %{conn: conn} do + address = insert(:address) + + block = insert(:block) + + transaction = + :transaction + |> insert(from_address: address) + |> with_block(block) + + query = """ + query ($hash: FullHash!, $first: Int!) { + transaction(hash: $hash) { + internal_transactions(first: $first) { + edges { + node { + index + transaction_hash + } + } + } + } + } + """ + + variables = %{ + "hash" => to_string(transaction.hash), + "first" => 1 + } + + conn = post(conn, "/api/v1/graphql", query: query, variables: variables) + + assert json_response(conn, 200) == %{ + "data" => %{ + "transaction" => %{ + "internal_transactions" => %{ + "edges" => [] + } + } + } + } + end + + test "internal transactions are ordered by ascending index", %{conn: conn} do + transaction = insert(:transaction) |> with_block() + + insert(:internal_transaction, + transaction: transaction, + index: 2, + block_hash: transaction.block_hash, + block_index: 2 + ) + + insert(:internal_transaction, + transaction: transaction, + index: 0, + block_hash: transaction.block_hash, + block_index: 0 + ) + + insert(:internal_transaction, + transaction: transaction, + index: 1, + block_hash: transaction.block_hash, + block_index: 1 + ) + + query = """ + query ($hash: FullHash!, $first: Int!) { + transaction(hash: $hash) { + internal_transactions(first: $first) { + edges { + node { + index + transaction_hash + } + } + } + } + } + """ + + variables = %{ + "hash" => to_string(transaction.hash), + "first" => 3 + } + + response = + conn + |> post("/api/v1/graphql", query: query, variables: variables) + |> json_response(200) + + internal_transactions = get_in(response, ["data", "transaction", "internal_transactions", "edges"]) + + index_order = Enum.map(internal_transactions, & &1["node"]["index"]) + + assert index_order == Enum.sort(index_order) + end + + test "complexity correlates to first or last argument", %{conn: conn} do + transaction = insert(:transaction) + + query1 = """ + query ($hash: FullHash!, $first: Int!) { + transaction(hash: $hash) { + internal_transactions(first: $first) { + edges { + node { + index + transaction_hash + } + } + } + } + } + """ + + variables1 = %{ + "hash" => to_string(transaction.hash), + "first" => 55 + } + + response1 = + conn + |> post("/api/v1/graphql", query: query1, variables: variables1) + |> json_response(200) + + assert %{"errors" => [error1, error2, error3]} = response1 + assert error1["message"] =~ ~s(Field internal_transactions is too complex) + assert error2["message"] =~ ~s(Field transaction is too complex) + assert error3["message"] =~ ~s(Operation is too complex) + + query2 = """ + query ($hash: FullHash!, $last: Int!, $count: Int!) { + transaction(hash: $hash) { + internal_transactions(last: $last, count: $count) { + edges { + node { + index + transaction_hash + } + } + } + } + } + """ + + variables2 = %{ + "hash" => to_string(transaction.hash), + "last" => 55, + "count" => 100 + } + + response2 = + conn + |> post("/api/v1/graphql", query: query2, variables: variables2) + |> json_response(200) + + assert %{"errors" => [error1, error2, error3]} = response2 + assert error1["message"] =~ ~s(Field internal_transactions is too complex) + assert error2["message"] =~ ~s(Field transaction is too complex) + assert error3["message"] =~ ~s(Operation is too complex) + end + + test "with 'last' and 'count' arguments", %{conn: conn} do + # "`last: N` must always be accompanied by either a `before:` argument to + # the query, or an explicit `count:` option to the `from_query` call. + # Otherwise it is impossible to derive the required offset." + # https://hexdocs.pm/absinthe_relay/Absinthe.Relay.Connection.html#from_query/4 + # + # This test ensures support for a 'count' argument. + + transaction = insert(:transaction) |> with_block() + + insert(:internal_transaction, + transaction: transaction, + index: 2, + block_hash: transaction.block_hash, + block_index: 2 + ) + + insert(:internal_transaction, + transaction: transaction, + index: 0, + block_hash: transaction.block_hash, + block_index: 0 + ) + + insert(:internal_transaction, + transaction: transaction, + index: 1, + block_hash: transaction.block_hash, + block_index: 1 + ) + + query = """ + query ($hash: FullHash!, $last: Int!, $count: Int!) { + transaction(hash: $hash) { + internal_transactions(last: $last, count: $count) { + edges { + node { + index + transaction_hash + } + } + } + } + } + """ + + variables = %{ + "hash" => to_string(transaction.hash), + "last" => 1, + "count" => 3 + } + + [internal_transaction] = + conn + |> post("/api/v1/graphql", query: query, variables: variables) + |> json_response(200) + |> get_in(["data", "transaction", "internal_transactions", "edges"]) + + assert internal_transaction["node"]["index"] == 2 + end + + test "pagination support with 'first' and 'after' arguments", %{conn: conn} do + transaction = insert(:transaction) |> with_block() + + for index <- 0..5 do + insert(:internal_transaction_create, + transaction: transaction, + index: index, + block_hash: transaction.block_hash, + block_index: index + ) + end + + query1 = """ + query ($hash: FullHash!, $first: Int!) { + transaction(hash: $hash) { + internal_transactions(first: $first) { + page_info { + has_next_page + has_previous_page + } + edges { + node { + index + transaction_hash + } + cursor + } + } + } + } + """ + + variables1 = %{ + "hash" => to_string(transaction.hash), + "first" => 2 + } + + conn = post(conn, "/api/v1/graphql", query: query1, variables: variables1) + + %{"data" => %{"transaction" => %{"internal_transactions" => page1}}} = json_response(conn, 200) + + assert page1["page_info"] == %{"has_next_page" => true, "has_previous_page" => false} + assert Enum.all?(page1["edges"], &(&1["node"]["index"] in 0..1)) + + last_cursor_page1 = + page1 + |> Map.get("edges") + |> List.last() + |> Map.get("cursor") + + query2 = """ + query ($hash: FullHash!, $first: Int!, $after: String!) { + transaction(hash: $hash) { + internal_transactions(first: $first, after: $after) { + page_info { + has_next_page + has_previous_page + } + edges { + node { + index + transaction_hash + } + cursor + } + } + } + } + """ + + variables2 = %{ + "hash" => to_string(transaction.hash), + "first" => 2, + "after" => last_cursor_page1 + } + + page2 = + conn + |> post("/api/v1/graphql", query: query2, variables: variables2) + |> json_response(200) + |> get_in(["data", "transaction", "internal_transactions"]) + + assert page2["page_info"] == %{"has_next_page" => true, "has_previous_page" => true} + assert Enum.all?(page2["edges"], &(&1["node"]["index"] in 2..3)) + + last_cursor_page2 = + page2 + |> Map.get("edges") + |> List.last() + |> Map.get("cursor") + + variables3 = %{ + "hash" => to_string(transaction.hash), + "first" => 2, + "after" => last_cursor_page2 + } + + page3 = + conn + |> post("/api/v1/graphql", query: query2, variables: variables3) + |> json_response(200) + |> get_in(["data", "transaction", "internal_transactions"]) + + assert page3["page_info"] == %{"has_next_page" => false, "has_previous_page" => true} + assert Enum.all?(page3["edges"], &(&1["node"]["index"] in 4..5)) + end + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/graphql/schema/subscription/token_transfers_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/graphql/schema/subscription/token_transfers_test.exs new file mode 100644 index 0000000..4ba16ec --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/graphql/schema/subscription/token_transfers_test.exs @@ -0,0 +1,117 @@ +defmodule BlockScoutWeb.GraphQL.Schema.Subscription.TokenTransfersTest do + use BlockScoutWeb.SubscriptionCase + import Mox + + alias BlockScoutWeb.Notifier + + describe "token_transfers field" do + setup :set_mox_global + + setup do + configuration = Application.get_env(:block_scout_web, BlockScoutWeb.Endpoint) + Application.put_env(:block_scout_web, BlockScoutWeb.Endpoint, pubsub_server: BlockScoutWeb.PubSub) + + :ok + + on_exit(fn -> + Application.put_env(:block_scout_web, BlockScoutWeb.Endpoint, configuration) + end) + end + + test "with valid argument, returns all expected fields", %{socket: socket} do + transaction = insert(:transaction) + token_transfer = insert(:token_transfer, transaction: transaction) + address_hash = to_string(token_transfer.token_contract_address_hash) + + subscription = """ + subscription ($hash: AddressHash!) { + token_transfers(token_contract_address_hash: $hash) { + amount + from_address_hash + to_address_hash + token_contract_address_hash + transaction_hash + } + } + """ + + variables = %{"hash" => address_hash} + + ref = push_doc(socket, subscription, variables: variables) + + assert_reply(ref, :ok, %{subscriptionId: subscription_id}) + + Notifier.handle_event({:chain_event, :token_transfers, :realtime, [token_transfer]}) + + expected = %{ + result: %{ + data: %{ + "token_transfers" => [ + %{ + "amount" => to_string(token_transfer.amount), + "from_address_hash" => to_string(token_transfer.from_address_hash), + "to_address_hash" => to_string(token_transfer.to_address_hash), + "token_contract_address_hash" => to_string(token_transfer.token_contract_address_hash), + "transaction_hash" => to_string(token_transfer.transaction_hash) + } + ] + } + }, + subscriptionId: subscription_id + } + + assert_push("subscription:data", push) + assert push == expected + end + + test "ignores irrelevant tokens", %{socket: socket} do + transaction = insert(:transaction) + [token_transfer1, token_transfer2] = insert_list(2, :token_transfer, transaction: transaction) + address_hash1 = to_string(token_transfer1.token_contract_address_hash) + + subscription = """ + subscription ($hash: AddressHash!) { + token_transfers(token_contract_address_hash: $hash) { + amount + token_contract_address_hash + } + } + """ + + variables = %{"hash" => address_hash1} + + ref = push_doc(socket, subscription, variables: variables) + + assert_reply(ref, :ok, %{subscriptionId: _subscription_id}) + + Notifier.handle_event({:chain_event, :token_transfers, :realtime, [token_transfer2]}) + + refute_push("subscription:data", _push) + end + + test "ignores non-realtime updates", %{socket: socket} do + transaction = insert(:transaction) + token_transfer = insert(:token_transfer, transaction: transaction) + address_hash = to_string(token_transfer.token_contract_address_hash) + + subscription = """ + subscription ($hash: AddressHash!) { + token_transfers(token_contract_address_hash: $hash) { + amount + token_contract_address_hash + } + } + """ + + variables = %{"hash" => address_hash} + + ref = push_doc(socket, subscription, variables: variables) + + assert_reply(ref, :ok, %{subscriptionId: _subscription_id}) + + Notifier.handle_event({:chain_event, :token_transfers, :catchup, [token_transfer]}) + + refute_push("subscription:data", _push) + end + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/plug/admin/check_owner_registered_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/plug/admin/check_owner_registered_test.exs new file mode 100644 index 0000000..2eb7b98 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/plug/admin/check_owner_registered_test.exs @@ -0,0 +1,27 @@ +defmodule BlockScoutWeb.Plug.Admin.CheckOwnerRegisteredTest do + use BlockScoutWeb.ConnCase + + alias BlockScoutWeb.Plug.Admin.CheckOwnerRegistered + alias Explorer.Admin + + test "init/1" do + assert CheckOwnerRegistered.init([]) == [] + end + + describe "call/2" do + test "redirects if owner user isn't configured", %{conn: conn} do + assert {:error, _} = Admin.owner() + result = CheckOwnerRegistered.call(conn, []) + assert redirected_to(result) == AdminRoutes.setup_path(conn, :configure) + assert result.halted + end + + test "continues if owner user is configured", %{conn: conn} do + insert(:administrator) + assert {:ok, _} = Admin.owner() + result = CheckOwnerRegistered.call(conn, []) + assert result.state == :unset + refute result.halted + end + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/plug/admin/require_admin_role_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/plug/admin/require_admin_role_test.exs new file mode 100644 index 0000000..45bcdc2 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/plug/admin/require_admin_role_test.exs @@ -0,0 +1,42 @@ +defmodule BlockScoutWeb.Plug.Admin.RequireAdminRoleTest do + use BlockScoutWeb.ConnCase + + import Plug.Conn, only: [put_session: 3, assign: 3] + + alias BlockScoutWeb.Router + alias BlockScoutWeb.Plug.Admin.RequireAdminRole + + test "init/1" do + assert RequireAdminRole.init([]) == [] + end + + describe "call/2" do + setup %{conn: conn} do + conn = + conn + |> bypass_through(Router, [:browser]) + |> get("/") + + {:ok, conn: conn} + end + + test "redirects if user in conn isn't an admin", %{conn: conn} do + result = RequireAdminRole.call(conn, []) + assert redirected_to(result) == AdminRoutes.session_path(conn, :new) + assert result.halted + end + + test "continues if user in assigns is an admin", %{conn: conn} do + administrator = insert(:administrator) + + result = + conn + |> put_session(:user_id, administrator.user.id) + |> assign(:user, administrator.user) + |> RequireAdminRole.call([]) + + refute result.halted + assert result.state == :unset + end + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/plug/fetch_user_from_session_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/plug/fetch_user_from_session_test.exs new file mode 100644 index 0000000..ed00ce6 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/plug/fetch_user_from_session_test.exs @@ -0,0 +1,46 @@ +defmodule BlockScoutWeb.Plug.FetchUserFromSessionTest do + use BlockScoutWeb.ConnCase + + import Plug.Conn, only: [put_session: 3] + + alias BlockScoutWeb.Plug.FetchUserFromSession + alias BlockScoutWeb.Router + alias Explorer.Accounts.User + + test "init/1" do + assert FetchUserFromSession.init([]) == [] + end + + describe "call/2" do + setup %{conn: conn} do + conn = + conn + |> bypass_through(Router, [:browser]) + |> get("/") + + {:ok, conn: conn} + end + + test "loads user if valid user id in session", %{conn: conn} do + user = insert(:user) + + result = + conn + |> put_session(:user_id, user.id) + |> FetchUserFromSession.call([]) + + assert %User{} = result.assigns.user + end + + test "returns conn if user id is invalid in session", %{conn: conn} do + conn = put_session(conn, :user_id, 1) + result = FetchUserFromSession.call(conn, []) + + assert conn == result + end + + test "returns conn if no user id is in session", %{conn: conn} do + assert FetchUserFromSession.call(conn, []) == conn + end + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/routers/chain_type_scope_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/routers/chain_type_scope_test.exs new file mode 100644 index 0000000..cc0f6ac --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/routers/chain_type_scope_test.exs @@ -0,0 +1,50 @@ +defmodule BlockScoutWeb.Routers.ChainTypeScopeTest do + use BlockScoutWeb.ConnCase + use Utils.CompileTimeEnvHelper, chain_type: [:explorer, :chain_type] + + if @chain_type == :default do + describe "stability validators routes with chain_scope" do + setup do + original_chain_type = Application.get_env(:explorer, :chain_type) + + on_exit(fn -> + Application.put_env(:explorer, :chain_type, original_chain_type) + end) + + :ok + end + + test "stability validators counters are accessible when chain type is stability", %{conn: conn} do + Application.put_env(:explorer, :chain_type, :stability) + + assert response = + conn + |> get("/api/v2/validators/stability/counters") + |> json_response(200) + end + + test "stability validators list are not accessible with different chain type", %{conn: conn} do + Application.put_env(:explorer, :chain_type, :default) + + conn = get(conn, "/api/v2/validators/stability/counters") + response = json_response(conn, 404) + assert response["message"] == "Endpoint not available for current chain type" + end + end + + test "blackfort validators counters are accessible when chain type is blackfort and stability is not", + %{conn: conn} do + Application.put_env(:explorer, :chain_type, :blackfort) + + assert response = + conn + |> get("/api/v2/validators/blackfort/counters") + |> json_response(200) + + assert response = + conn + |> get("/api/v2/validators/stability/counters") + |> json_response(404) + end + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/social_media_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/social_media_test.exs new file mode 100644 index 0000000..1924324 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/social_media_test.exs @@ -0,0 +1,25 @@ +defmodule BlockScoutWeb.SocialMediaTest do + use Explorer.DataCase + + alias BlockScoutWeb.SocialMedia + + test "it filters out unsupported services" do + Application.put_env( + :block_scout_web, + BlockScoutWeb.SocialMedia, + twitter: "MyTwitterProfile", + myspace: "MyAwesomeProfile" + ) + + links = SocialMedia.links() + assert Keyword.has_key?(links, :twitter) + refute Keyword.has_key?(links, :myspace) + end + + test "it prepends the service url" do + Application.put_env(:block_scout_web, BlockScoutWeb.SocialMedia, twitter: "MyTwitterProfile") + + links = SocialMedia.links() + assert links[:twitter] == "https://www.x.com/MyTwitterProfile" + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/views/abi_encoded_value_view_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/views/abi_encoded_value_view_test.exs new file mode 100644 index 0000000..9d29afe --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/views/abi_encoded_value_view_test.exs @@ -0,0 +1,125 @@ +defmodule BlockScoutWeb.ABIEncodedValueViewTest do + use BlockScoutWeb.ConnCase, async: true + + alias BlockScoutWeb.ABIEncodedValueView + + defp value_html(type, value) do + type + |> ABIEncodedValueView.value_html(value) + |> case do + :error -> + raise "failed to generate html" + + other -> + other + end + end + + defp copy_text(type, value) do + type + |> ABIEncodedValueView.copy_text(value) + |> case do + :error -> + raise "failed to generate copy text" + + other -> + other + end + |> Phoenix.HTML.Safe.to_iodata() + |> IO.iodata_to_binary() + end + + describe "value_html/2" do + test "it formats addresses as links" do + address = "0x0000000000000000000000000000000000000000" + address_bytes = address |> String.trim_leading("0x") |> Base.decode16!() + + expected = ~s(#{address}) + + assert value_html("address", address_bytes) == expected + end + + test "it formats lists with newlines and spaces" do + expected = + String.trim(""" + [ + 1, + 2, + 3, + 4 + ] + """) + + assert value_html("uint[]", [1, 2, 3, 4]) == expected + end + + test "it formats nested lists with nested depth" do + expected = + String.trim(""" + [ + [ + 1, + 2 + ], + [ + 3, + 4 + ] + ] + """) + + assert value_html("uint[][]", [[1, 2], [3, 4]]) == expected + end + + test "it formats lists of addresses as a list of links" do + address = "0x0000000000000000000000000000000000000000" + address_link = ~s(#{address}) + + expected = + String.trim(""" + [ + #{address_link}, + #{address_link}, + #{address_link}, + #{address_link} + ] + """) + + address_bytes = "0x0000000000000000000000000000000000000000" |> String.trim_leading("0x") |> Base.decode16!() + + assert value_html("address[4]", [address_bytes, address_bytes, address_bytes, address_bytes]) == expected + end + + test "it renders :dynamic values as bytes" do + assert value_html("uint", {:dynamic, <<1>>}) == "0x01" + end + + test "it renders :tuple values as string" do + assert value_html("(uint256)", {123}) == "(123)" + end + end + + describe "copy_text/2" do + test "it skips link formatting of addresses" do + address = "0x0000000000000000000000000000000000000000" + address_bytes = address |> String.trim_leading("0x") |> Base.decode16!() + + assert copy_text("address", address_bytes) == address + end + + test "it skips the formatting when copying lists" do + assert copy_text("uint[4]", [1, 2, 3, 4]) == "[1, 2, 3, 4]" + end + + test "it copies bytes as their hex representation" do + hex = "0xffffff" + bytes = hex |> String.trim_leading("0x") |> Base.decode16!(case: :lower) + + assert copy_text("bytes", bytes) == hex + end + + test "it copies :dynamic values as bytes" do + assert copy_text("uint", {:dynamic, <<1>>}) == "0x01" + end + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/views/access_helper_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/views/access_helper_test.exs new file mode 100644 index 0000000..c18cb95 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/views/access_helper_test.exs @@ -0,0 +1,70 @@ +defmodule BlockScoutWeb.AccessHelperTest do + alias BlockScoutWeb.AccessHelper + use BlockScoutWeb.ConnCase + import Mox + + setup :verify_on_exit! + + setup do + configuration = Application.get_env(:block_scout_web, :api_rate_limit) + + on_exit(fn -> + Application.put_env(:block_scout_web, :api_rate_limit, configuration) + end) + + :ok + end + + describe "check_rate_limit/1" do + test "rate_limit_disabled", %{conn: conn} do + Application.put_env(:block_scout_web, :api_rate_limit, + global_limit: 0, + limit_by_key: 0, + limit_by_whitelisted_ip: 0, + time_interval_limit: 1_000, + disabled: true + ) + + assert AccessHelper.check_rate_limit(conn) == :ok + end + + test "no_rate_limit_api_key", %{conn: conn} do + Application.put_env(:block_scout_web, :api_rate_limit, + global_limit: 0, + limit_by_key: 0, + limit_by_whitelisted_ip: 0, + time_interval_limit: 1_000, + no_rate_limit_api_key: "123" + ) + + conn = %{conn | query_params: %{"apikey" => "123"}} + assert AccessHelper.check_rate_limit(conn) == :ok + end + + test "rate limit, if no_rate_limit_api_key is nil", %{conn: conn} do + Application.put_env(:block_scout_web, :api_rate_limit, + global_limit: 0, + limit_by_key: 0, + limit_by_whitelisted_ip: 0, + time_interval_limit: 1_000, + no_rate_limit_api_key: nil + ) + + conn = %{conn | query_params: %{"apikey" => nil}} + assert AccessHelper.check_rate_limit(conn) == :rate_limit_reached + end + + test "rate limit, if no_rate_limit_api_key is empty", %{conn: conn} do + Application.put_env(:block_scout_web, :api_rate_limit, + global_limit: 0, + limit_by_key: 0, + limit_by_whitelisted_ip: 0, + time_interval_limit: 1_000, + no_rate_limit_api_key: "" + ) + + conn = %{conn | query_params: %{"apikey" => " "}} + assert AccessHelper.check_rate_limit(conn) == :rate_limit_reached + end + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/views/address_coin_balance_view_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/views/address_coin_balance_view_test.exs new file mode 100644 index 0000000..bc07e1c --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/views/address_coin_balance_view_test.exs @@ -0,0 +1,62 @@ +defmodule BlockScoutWeb.AddressCoinBalanceViewTest do + use BlockScoutWeb.ConnCase, async: true + + alias BlockScoutWeb.AddressCoinBalanceView + alias Explorer.Chain.Wei + + describe "format/1" do + test "format the wei value in ether" do + wei = Wei.from(Decimal.new(1_340_000_000), :gwei) + + assert AddressCoinBalanceView.format(wei) == "1.34 ETH" + end + + test "format negative values" do + wei = Wei.from(Decimal.new(-1_340_000_000), :gwei) + + assert AddressCoinBalanceView.format(wei) == "-1.34 ETH" + end + end + + describe "delta_arrow/1" do + test "return up pointing arrow for positive value" do + value = Decimal.new(123) + + assert AddressCoinBalanceView.delta_arrow(value) == "▲" + end + + test "return down pointing arrow for negative value" do + value = Decimal.new(-123) + + assert AddressCoinBalanceView.delta_arrow(value) == "▼" + end + end + + describe "delta_sign/1" do + test "return Positive for positive value" do + value = Decimal.new(123) + + assert AddressCoinBalanceView.delta_sign(value) == "Positive" + end + + test "return Negative for negative value" do + value = Decimal.new(-123) + + assert AddressCoinBalanceView.delta_sign(value) == "Negative" + end + end + + describe "format_delta/1" do + test "format positive values" do + value = Decimal.new(1_340_000_000_000_000_000) + + assert AddressCoinBalanceView.format_delta(value) == "1.34 ETH" + end + + test "format negative values" do + value = Decimal.new(-1_340_000_000_000_000_000) + + assert AddressCoinBalanceView.format_delta(value) == "1.34 ETH" + end + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/views/address_contract_view_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/views/address_contract_view_test.exs new file mode 100644 index 0000000..e483bfc --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/views/address_contract_view_test.exs @@ -0,0 +1,17 @@ +defmodule BlockScoutWeb.AddressContractViewTest do + use BlockScoutWeb.ConnCase, async: true + + alias BlockScoutWeb.AddressContractView + + doctest BlockScoutWeb.AddressContractView + + describe "format_optimization_text/1" do + test "returns \"true\" for the boolean true" do + assert AddressContractView.format_optimization_text(true) == "true" + end + + test "returns \"false\" for the boolean false" do + assert AddressContractView.format_optimization_text(false) == "false" + end + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/views/address_token_balance_view_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/views/address_token_balance_view_test.exs new file mode 100644 index 0000000..778c1eb --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/views/address_token_balance_view_test.exs @@ -0,0 +1,67 @@ +defmodule BlockScoutWeb.AddressTokenBalanceViewTest do + use BlockScoutWeb.ConnCase, async: true + + alias BlockScoutWeb.AddressTokenBalanceView + alias Explorer.Chain + + describe "tokens_count_title/1" do + test "returns the title pluralized" do + token_balances = [ + build(:token_balance), + build(:token_balance) + ] + + assert AddressTokenBalanceView.tokens_count_title(token_balances) == "2 tokens" + end + end + + describe "filter_by_type/2" do + test "filter tokens by the given type" do + token_balance_a = build(:token_balance, token: build(:token, type: "ERC-20")) + token_balance_b = build(:token_balance, token: build(:token, type: "ERC-721")) + + token_balances = [token_balance_a, token_balance_b] + + assert AddressTokenBalanceView.filter_by_type(token_balances, "ERC-20") == [token_balance_a] + end + end + + describe "balance_in_fiat/1" do + test "return balance in fiat" do + token = + :token + |> build(decimals: Decimal.new(0)) + |> Map.put(:fiat_value, Decimal.new(3)) + + token_balance = build(:token_balance, value: Decimal.new(10), token: token) + + result = Chain.balance_in_fiat(token_balance) + + assert Decimal.compare(result, 30) == :eq + end + + test "return nil if fiat_value is not present" do + token = + :token + |> build(decimals: Decimal.new(0)) + |> Map.put(:fiat_value, nil) + + token_balance = build(:token_balance, value: 10, token: token) + + assert Chain.balance_in_fiat(token_balance) == nil + end + + test "consider decimals when computing value" do + token = + :token + |> build(decimals: Decimal.new(2)) + |> Map.put(:fiat_value, Decimal.new(3)) + + token_balance = build(:token_balance, value: Decimal.new(10), token: token) + + result = Chain.balance_in_fiat(token_balance) + + assert Decimal.compare(result, Decimal.from_float(0.3)) == :eq + end + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/views/address_transaction_view_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/views/address_transaction_view_test.exs new file mode 100644 index 0000000..666f06b --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/views/address_transaction_view_test.exs @@ -0,0 +1,5 @@ +defmodule BlockScoutWeb.AddressTransactionViewTest do + use BlockScoutWeb.ConnCase, async: true + + doctest BlockScoutWeb.AddressTransactionView +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/views/address_view_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/views/address_view_test.exs new file mode 100644 index 0000000..db7ddbe --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/views/address_view_test.exs @@ -0,0 +1,361 @@ +defmodule BlockScoutWeb.AddressViewTest do + use BlockScoutWeb.ConnCase, async: true + + alias Explorer.Chain.{Address, Data, Hash, Transaction} + alias BlockScoutWeb.{AddressView, Endpoint} + + describe "address_partial_selector/4" do + test "for a pending transaction contract creation to address" do + transaction = insert(:transaction, to_address: nil, created_contract_address_hash: nil) + assert AddressView.address_partial_selector(transaction, :to, nil) == "Contract Address Pending" + end + + test "for a pending internal transaction contract creation to address" do + transaction = insert(:transaction, to_address: nil) |> with_block() + + internal_transaction = + insert(:internal_transaction, + index: 1, + transaction: transaction, + to_address: nil, + created_contract_address_hash: nil, + block_hash: transaction.block_hash, + block_index: 1 + ) + + assert "Contract Address Pending" == AddressView.address_partial_selector(internal_transaction, :to, nil) + end + + test "will truncate address" do + transaction = %Transaction{to_address: to_address} = insert(:transaction) + + assert [ + view_module: AddressView, + partial: "_link.html", + address: ^to_address, + contract: false, + truncate: true, + use_custom_tooltip: false + ] = AddressView.address_partial_selector(transaction, :to, nil, true) + end + + test "for a non-contract to address not on address page" do + transaction = %Transaction{to_address: to_address} = insert(:transaction) + + assert [ + view_module: AddressView, + partial: "_link.html", + address: ^to_address, + contract: false, + truncate: false, + use_custom_tooltip: false + ] = AddressView.address_partial_selector(transaction, :to, nil) + end + + test "for a non-contract to address non matching address page" do + transaction = %Transaction{to_address: to_address} = insert(:transaction) + + assert [ + view_module: AddressView, + partial: "_link.html", + address: ^to_address, + contract: false, + truncate: false, + use_custom_tooltip: false + ] = AddressView.address_partial_selector(transaction, :to, nil) + end + + test "for a non-contract to address matching address page" do + transaction = %Transaction{to_address: to_address} = insert(:transaction) + + assert [ + view_module: AddressView, + partial: "_responsive_hash.html", + address: ^to_address, + contract: false, + truncate: false, + use_custom_tooltip: false + ] = AddressView.address_partial_selector(transaction, :to, transaction.to_address) + end + + test "for a contract to address non matching address page" do + contract_address = insert(:contract_address) + transaction = insert(:transaction, to_address: nil, created_contract_address: contract_address) + + assert [ + view_module: AddressView, + partial: "_link.html", + address: ^contract_address, + contract: true, + truncate: false, + use_custom_tooltip: false + ] = AddressView.address_partial_selector(transaction, :to, transaction.to_address) + end + + test "for a contract to address matching address page" do + contract_address = insert(:contract_address) + transaction = insert(:transaction, to_address: nil, created_contract_address: contract_address) + + assert [ + view_module: AddressView, + partial: "_responsive_hash.html", + address: ^contract_address, + contract: true, + truncate: false, + use_custom_tooltip: false + ] = AddressView.address_partial_selector(transaction, :to, contract_address) + end + + test "for a non-contract from address not on address page" do + transaction = %Transaction{to_address: to_address} = insert(:transaction) + + assert [ + view_module: AddressView, + partial: "_link.html", + address: ^to_address, + contract: false, + truncate: false, + use_custom_tooltip: false + ] = AddressView.address_partial_selector(transaction, :to, nil) + end + + test "for a non-contract from address matching address page" do + transaction = %Transaction{from_address: from_address} = insert(:transaction) + + assert [ + view_module: AddressView, + partial: "_responsive_hash.html", + address: ^from_address, + contract: false, + truncate: false, + use_custom_tooltip: false + ] = AddressView.address_partial_selector(transaction, :from, transaction.from_address) + end + end + + describe "balance_block_number/1" do + test "gives empty string with no fetched balance block number present" do + assert AddressView.balance_block_number(%Address{}) == "" + end + + test "gives block number when fetched balance block number is non-nil" do + assert AddressView.balance_block_number(%Address{fetched_coin_balance_block_number: 1_000_000}) == "1000000" + end + end + + describe "balance_percentage_enabled/1" do + test "with non_zero market cap" do + Application.put_env(:block_scout_web, :show_percentage, true) + + assert AddressView.balance_percentage_enabled?(100_500) == true + end + + test "with zero market cap" do + Application.put_env(:block_scout_web, :show_percentage, true) + + assert AddressView.balance_percentage_enabled?(0) == false + end + + test "with switched off show_percentage" do + Application.put_env(:block_scout_web, :show_percentage, false) + + assert AddressView.balance_percentage_enabled?(100_501) == false + end + end + + test "balance_percentage/1" do + Application.put_env(:explorer, :supply, Explorer.Chain.Supply.ProofOfAuthority) + address = insert(:address, fetched_coin_balance: 2_524_608_000_000_000_000_000_000) + + assert "1.0000% Market Cap" = AddressView.balance_percentage(address) + end + + test "balance_percentage with nil total_supply" do + address = insert(:address, fetched_coin_balance: 2_524_608_000_000_000_000_000_000) + + assert "" = AddressView.balance_percentage(address, nil) + end + + describe "hash/1" do + test "gives a string version of an address's hash" do + address = %Address{ + hash: %Hash{ + byte_count: 20, + bytes: <<139, 243, 141, 71, 100, 146, 144, 100, 242, 212, 211, 165, 101, 32, 167, 106, 179, 223, 65, 91>> + } + } + + assert AddressView.hash(address) == "0x8bf38d4764929064f2d4d3a56520a76ab3df415b" + end + end + + describe "primary_name/1" do + test "gives an address's primary name when present" do + address = insert(:address) + + address_name = insert(:address_name, address: address, primary: true, name: "POA Foundation Wallet") + insert(:address_name, address: address, name: "POA Wallet") + + preloaded_address = Explorer.Repo.preload(address, :names) + + assert AddressView.primary_name(preloaded_address) == address_name.name + end + + test "returns any when no primary available" do + address_name = insert(:address_name, name: "POA Wallet") + preloaded_address = Explorer.Repo.preload(address_name.address, :names) + + assert AddressView.primary_name(preloaded_address) == address_name.name + end + end + + describe "qr_code/1" do + test "it returns an encoded value" do + address = build(:address) + assert {:ok, _} = Base.decode64(AddressView.qr_code(address.hash)) + end + end + + describe "smart_contract_with_read_only_functions?/1" do + test "returns true when abi has read only functions" do + smart_contract = + insert( + :smart_contract, + abi: [ + %{ + "constant" => true, + "inputs" => [], + "name" => "get", + "outputs" => [%{"name" => "", "type" => "uint256"}], + "payable" => false, + "stateMutability" => "view", + "type" => "function" + } + ], + contract_code_md5: "123" + ) + + address = insert(:address, smart_contract: smart_contract) + + assert AddressView.smart_contract_with_read_only_functions?(address) + end + + test "returns false when there is no read only functions" do + smart_contract = + insert( + :smart_contract, + abi: [ + %{ + "constant" => false, + "inputs" => [%{"name" => "x", "type" => "uint256"}], + "name" => "set", + "outputs" => [], + "payable" => false, + "stateMutability" => "nonpayable", + "type" => "function" + } + ], + contract_code_md5: "123" + ) + + address = insert(:address, smart_contract: smart_contract) + + refute AddressView.smart_contract_with_read_only_functions?(address) + end + + test "returns false when smart contract is not verified" do + address = insert(:address, smart_contract: nil) + + refute AddressView.smart_contract_with_read_only_functions?(address) + end + end + + describe "token_title/1" do + test "returns the 6 first and 6 last chars of address hash when token has no name" do + token = insert(:token, name: nil) + + hash = to_string(token.contract_address_hash) + expected_hash = String.slice(hash, 0, 8) <> "-" <> String.slice(hash, -6, 6) + assert expected_hash == AddressView.token_title(token) + end + + test "returns name(symbol) when token has name" do + token = insert(:token, name: "super token money", symbol: "ST$") + + assert AddressView.token_title(token) == "super token money (ST$)" + end + end + + describe "current_tab_name/1" do + test "generates the correct tab name for the token path" do + path = address_token_path(Endpoint, :index, "0x4ddr3s") + + assert AddressView.current_tab_name(path) == "Tokens" + end + + test "generates the correct tab name for the transactions path" do + path = address_transaction_path(Endpoint, :index, "0x4ddr3s") + + assert AddressView.current_tab_name(path) == "Transactions" + end + + test "generates the correct tab name for the internal transactions path" do + path = address_internal_transaction_path(Endpoint, :index, "0x4ddr3s") + + assert AddressView.current_tab_name(path) == "Internal Transactions" + end + + test "generates the correct tab name for the contracts path" do + path = address_contract_path(Endpoint, :index, "0x4ddr3s") + + assert AddressView.current_tab_name(path) == "Code" + end + + test "generates the correct tab name for the read_contract path" do + path = address_read_contract_path(Endpoint, :index, "0x4ddr3s") + + assert AddressView.current_tab_name(path) == "Read Contract" + end + + test "generates the correct tab name for the coin_balances path" do + path = address_coin_balance_path(Endpoint, :index, "0x4ddr3s") + + assert AddressView.current_tab_name(path) == "Coin Balance History" + end + + test "generates the correct tab name for the validations path" do + path = address_validation_path(Endpoint, :index, "0x4ddr3s") + + assert AddressView.current_tab_name(path) == "Blocks Validated" + end + end + + describe "short_hash/1" do + test "returns a shortened hash of 6 hex characters" do + address = insert(:address) + assert "0x" <> short_hash = AddressView.short_hash(address) + assert String.length(short_hash) == 6 + end + end + + describe "address_page_title/1" do + test "uses the Smart Contract name when the contract is verified" do + smart_contract = build(:smart_contract, name: "POA") + address = build(:address, smart_contract: smart_contract) + + assert AddressView.address_page_title(address) == "POA (#{address})" + end + + test "uses the string 'Contract' when it's a contract" do + address = build(:contract_address, smart_contract: nil) + + assert AddressView.address_page_title(address) == "Contract #{address}" + end + + test "uses the address hash when it is not a contract" do + address = build(:address, smart_contract: nil) + + assert AddressView.address_page_title(address) == "#{address}" + end + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/views/api/v2/transaction_view_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/views/api/v2/transaction_view_test.exs new file mode 100644 index 0000000..a5ff0b1 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/views/api/v2/transaction_view_test.exs @@ -0,0 +1,163 @@ +defmodule BlockScoutWeb.API.V2.TransactionViewTest do + use BlockScoutWeb.ConnCase, async: true + + alias BlockScoutWeb.API.V2.TransactionView + + describe "decode_logs/2" do + test "doesn't use decoding candidate event with different 2nd, 3d or 4th topic" do + insert(:contract_method, + identifier: Base.decode16!("d20a68b2", case: :lower), + abi: %{ + "name" => "OptionSettled", + "type" => "event", + "inputs" => [ + %{"name" => "accountId", "type" => "uint256", "indexed" => true, "internalType" => "uint256"}, + %{"name" => "option", "type" => "address", "indexed" => false, "internalType" => "address"}, + %{"name" => "subId", "type" => "uint256", "indexed" => false, "internalType" => "uint256"}, + %{"name" => "amount", "type" => "int256", "indexed" => false, "internalType" => "int256"}, + %{"name" => "value", "type" => "int256", "indexed" => false, "internalType" => "int256"} + ], + "anonymous" => false + } + ) + + topic1_bytes = ExKeccak.hash_256("OptionSettled(uint256,address,uint256,int256,int256)") + topic1 = "0x" <> Base.encode16(topic1_bytes, case: :lower) + log1_topic2 = "0x0000000000000000000000000000000000000000000000000000000000005d19" + log2_topic2 = "0x000000000000000000000000000000000000000000000000000000000000634a" + + log1_data = + "0x000000000000000000000000aeb81cbe6b19ceeb0dbe0d230cffe35bb40a13a700000000000000000000000000000000000000000000045d964b80006597b700fffffffffffffffffffffffffffffffffffffffffffffffffe55aca2c2f40000ffffffffffffffffffffffffffffffffffffffffffffffe3a8289da3d7a13ef2" + + log2_data = + "0x000000000000000000000000aeb81cbe6b19ceeb0dbe0d230cffe35bb40a13a700000000000000000000000000000000000000000000045d964b80006597b700000000000000000000000000000000000000000000000000011227ebced227ae00000000000000000000000000000000000000000000001239fdf180a3d6bd85" + + transaction = insert(:transaction) + + log1 = + insert(:log, + transaction: transaction, + first_topic: topic(topic1), + second_topic: topic(log1_topic2), + third_topic: nil, + fourth_topic: nil, + data: log1_data + ) + + log2 = + insert(:log, + transaction: transaction, + first_topic: topic(topic1), + second_topic: topic(log2_topic2), + third_topic: nil, + fourth_topic: nil, + data: log2_data + ) + + logs = [log1, log2] + + assert [ + {:ok, "d20a68b2", + "OptionSettled(uint256 indexed accountId, address option, uint256 subId, int256 amount, int256 value)", + [ + {"accountId", "uint256", true, 23833}, + {"option", "address", false, + <<174, 184, 28, 190, 107, 25, 206, 235, 13, 190, 13, 35, 12, 255, 227, 91, 180, 10, 19, 167>>}, + {"subId", "uint256", false, 20_615_843_020_801_704_441_600}, + {"amount", "int256", false, -120_000_000_000_000_000}, + {"value", "int256", false, -522_838_470_013_113_778_446} + ]}, + {:ok, "d20a68b2", + "OptionSettled(uint256 indexed accountId, address option, uint256 subId, int256 amount, int256 value)", + [ + {"accountId", "uint256", true, 25418}, + {"option", "address", false, + <<174, 184, 28, 190, 107, 25, 206, 235, 13, 190, 13, 35, 12, 255, 227, 91, 180, 10, 19, 167>>}, + {"subId", "uint256", false, 20_615_843_020_801_704_441_600}, + {"amount", "int256", false, 77_168_037_359_396_782}, + {"value", "int256", false, 336_220_154_890_848_484_741} + ]} + ] = TransactionView.decode_logs(logs, false) + end + + test "properly decode logs if they have same topics" do + insert(:contract_method, + identifier: Base.decode16!("d20a68b2", case: :lower), + abi: %{ + "name" => "OptionSettled", + "type" => "event", + "inputs" => [ + %{"name" => "accountId", "type" => "uint256", "indexed" => true, "internalType" => "uint256"}, + %{"name" => "option", "type" => "address", "indexed" => false, "internalType" => "address"}, + %{"name" => "subId", "type" => "uint256", "indexed" => false, "internalType" => "uint256"}, + %{"name" => "amount", "type" => "int256", "indexed" => false, "internalType" => "int256"}, + %{"name" => "value", "type" => "int256", "indexed" => false, "internalType" => "int256"} + ], + "anonymous" => false + } + ) + + topic1_bytes = ExKeccak.hash_256("OptionSettled(uint256,address,uint256,int256,int256)") + topic1 = "0x" <> Base.encode16(topic1_bytes, case: :lower) + topic2 = "0x0000000000000000000000000000000000000000000000000000000000005d19" + + log1_data = + "0x000000000000000000000000aeb81cbe6b19ceeb0dbe0d230cffe35bb40a13a700000000000000000000000000000000000000000000045d964b80006597b700fffffffffffffffffffffffffffffffffffffffffffffffffe55aca2c2f40000ffffffffffffffffffffffffffffffffffffffffffffffe3a8289da3d7a13ef2" + + log2_data = + "0x000000000000000000000000aeb81cbe6b19ceeb0dbe0d230cffe35bb40a13a700000000000000000000000000000000000000000000045d964b80006597b700000000000000000000000000000000000000000000000000011227ebced227ae00000000000000000000000000000000000000000000001239fdf180a3d6bd85" + + transaction = insert(:transaction) + + log1 = + insert(:log, + transaction: transaction, + first_topic: topic(topic1), + second_topic: topic(topic2), + third_topic: nil, + fourth_topic: nil, + data: log1_data + ) + + log2 = + insert(:log, + transaction: transaction, + first_topic: topic(topic1), + second_topic: topic(topic2), + third_topic: nil, + fourth_topic: nil, + data: log2_data + ) + + logs = [log1, log2] + + assert [ + {:ok, "d20a68b2", + "OptionSettled(uint256 indexed accountId, address option, uint256 subId, int256 amount, int256 value)", + [ + {"accountId", "uint256", true, 23833}, + {"option", "address", false, + <<174, 184, 28, 190, 107, 25, 206, 235, 13, 190, 13, 35, 12, 255, 227, 91, 180, 10, 19, 167>>}, + {"subId", "uint256", false, 20_615_843_020_801_704_441_600}, + {"amount", "int256", false, -120_000_000_000_000_000}, + {"value", "int256", false, -522_838_470_013_113_778_446} + ]}, + {:ok, "d20a68b2", + "OptionSettled(uint256 indexed accountId, address option, uint256 subId, int256 amount, int256 value)", + [ + {"accountId", "uint256", true, 23833}, + {"option", "address", false, + <<174, 184, 28, 190, 107, 25, 206, 235, 13, 190, 13, 35, 12, 255, 227, 91, 180, 10, 19, 167>>}, + {"subId", "uint256", false, 20_615_843_020_801_704_441_600}, + {"amount", "int256", false, 77_168_037_359_396_782}, + {"value", "int256", false, 336_220_154_890_848_484_741} + ]} + ] = TransactionView.decode_logs(logs, false) + end + end + + defp topic(topic_hex_string) do + {:ok, topic} = Explorer.Chain.Hash.Full.cast(topic_hex_string) + topic + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/views/api_docs_view_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/views/api_docs_view_test.exs new file mode 100644 index 0000000..1a56d7e --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/views/api_docs_view_test.exs @@ -0,0 +1,101 @@ +defmodule BlockScoutWeb.ApiDocsViewTest do + use BlockScoutWeb.ConnCase, async: false + + alias BlockScoutWeb.APIDocsView + + describe "api_url/1" do + setup do + original = Application.get_env(:block_scout_web, BlockScoutWeb.Endpoint) + + on_exit(fn -> Application.put_env(:block_scout_web, BlockScoutWeb.Endpoint, original) end) + + :ok + end + + test "adds slash before path" do + Application.put_env(:block_scout_web, BlockScoutWeb.Endpoint, + url: [scheme: "https", host: "blockscout.com", port: 9999, path: "/chain/dog"] + ) + + assert APIDocsView.api_url() == "https://blockscout.com/chain/dog/api" + end + + test "does not add slash to empty path" do + Application.put_env(:block_scout_web, BlockScoutWeb.Endpoint, + url: [scheme: "https", host: "blockscout.com", port: 9999, path: ""] + ) + + assert APIDocsView.api_url() == "https://blockscout.com/api" + end + + test "localhost return with port" do + Application.put_env(:block_scout_web, BlockScoutWeb.Endpoint, + url: [scheme: "http", host: "localhost"], + http: [port: 9999] + ) + + assert APIDocsView.api_url() == "http://localhost:9999/api" + end + end + + describe "blockscout_url/1" do + setup do + original = Application.get_env(:block_scout_web, BlockScoutWeb.Endpoint) + + on_exit(fn -> Application.put_env(:block_scout_web, BlockScoutWeb.Endpoint, original) end) + + :ok + end + + test "set_path = true returns url with path" do + Application.put_env(:block_scout_web, BlockScoutWeb.Endpoint, + url: [scheme: "https", host: "blockscout.com", path: "/eth/mainnet"] + ) + + assert APIDocsView.blockscout_url(true) == "https://blockscout.com/eth/mainnet" + end + + test "set_path = false returns url w/out path" do + Application.put_env(:block_scout_web, BlockScoutWeb.Endpoint, + url: [scheme: "https", host: "blockscout.com", path: "/eth/mainnet"] + ) + + assert APIDocsView.blockscout_url(false) == "https://blockscout.com" + end + end + + describe "eth_rpc_api_url/1" do + setup do + original = Application.get_env(:block_scout_web, BlockScoutWeb.Endpoint) + + on_exit(fn -> Application.put_env(:block_scout_web, BlockScoutWeb.Endpoint, original) end) + + :ok + end + + test "adds slash before path" do + Application.put_env(:block_scout_web, BlockScoutWeb.Endpoint, + url: [scheme: "https", host: "blockscout.com", port: 9999, path: "/chain/dog"] + ) + + assert APIDocsView.eth_rpc_api_url() == "https://blockscout.com/chain/dog/api/eth-rpc" + end + + test "does not add slash to empty path" do + Application.put_env(:block_scout_web, BlockScoutWeb.Endpoint, + url: [scheme: "https", host: "blockscout.com", port: 9999, path: ""] + ) + + assert APIDocsView.eth_rpc_api_url() == "https://blockscout.com/api/eth-rpc" + end + + test "localhost return with port" do + Application.put_env(:block_scout_web, BlockScoutWeb.Endpoint, + url: [scheme: "http", host: "localhost"], + http: [port: 9999] + ) + + assert APIDocsView.eth_rpc_api_url() == "http://localhost:9999/api/eth-rpc" + end + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/views/block_view_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/views/block_view_test.exs new file mode 100644 index 0000000..eea5baf --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/views/block_view_test.exs @@ -0,0 +1,98 @@ +defmodule BlockScoutWeb.BlockViewTest do + use BlockScoutWeb.ConnCase, async: true + + alias BlockScoutWeb.BlockView + alias Explorer.Repo + + describe "average_gas_price/1" do + test "returns an average of the gas prices for a block's transactions with the unit value" do + block = insert(:block) + + Enum.each(1..10, fn index -> + :transaction + |> insert(gas_price: 10_000_000_000 * index) + |> with_block(block) + end) + + assert "55 Gwei" == BlockView.average_gas_price(Repo.preload(block, [:transactions])) + end + end + + describe "block_type/1" do + test "returns Block" do + block = insert(:block, nephews: []) + + assert BlockView.block_type(block) == "Block" + end + + test "returns Reorg" do + reorg = insert(:block, consensus: false, nephews: []) + + assert BlockView.block_type(reorg) == "Reorg" + end + + test "returns Uncle" do + uncle = insert(:block, consensus: false) + insert(:block_second_degree_relation, uncle_hash: uncle.hash) + preloaded = Repo.preload(uncle, :nephews) + + assert BlockView.block_type(preloaded) == "Uncle" + end + end + + describe "formatted_timestamp/1" do + test "returns a formatted timestamp string for a block" do + block = insert(:block) + + assert Timex.format!(block.timestamp, "%b-%d-%Y %H:%M:%S %p %Z", :strftime) == + BlockView.formatted_timestamp(block) + end + end + + describe "show_reward?/1" do + test "returns false when list of rewards is empty" do + assert BlockView.show_reward?([]) == false + end + + test "returns true when list of rewards is not empty" do + block = insert(:block) + validator = insert(:reward, address_hash: block.miner_hash, block_hash: block.hash, address_type: :validator) + + assert BlockView.show_reward?([validator]) == true + end + end + + describe "combined_rewards_value/1" do + test "returns all the reward values summed up and formatted into a String" do + block = insert(:block) + + insert( + :reward, + address_hash: block.miner_hash, + block_hash: block.hash, + address_type: :validator, + reward: Decimal.new(1_000_000_000_000_000_000) + ) + + insert( + :reward, + address_hash: block.miner_hash, + block_hash: block.hash, + address_type: :emission_funds, + reward: Decimal.new(1_000_000_000_000_000_000) + ) + + insert( + :reward, + address_hash: block.miner_hash, + block_hash: block.hash, + address_type: :uncle, + reward: Decimal.new(1_000_042_000_000_000_000) + ) + + block = Repo.preload(block, :rewards) + + assert BlockView.combined_rewards_value(block) == "3.000042 ETH" + end + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/views/currency_helper_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/views/currency_helper_test.exs new file mode 100644 index 0000000..432d34e --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/views/currency_helper_test.exs @@ -0,0 +1,64 @@ +defmodule BlockScoutWeb.CurrencyHelperTest do + use ExUnit.Case + + alias BlockScoutWeb.CurrencyHelper + + doctest BlockScoutWeb.CurrencyHelper, import: true + + describe "format_according_to_decimals/1" do + test "formats the amount as value considering the given decimals" do + amount = Decimal.new(205_000_000_000_000) + decimals = Decimal.new(12) + + assert CurrencyHelper.format_according_to_decimals(amount, decimals) == "205" + end + + test "considers the decimal places according to the given decimals" do + amount = Decimal.new(205_000) + decimals = Decimal.new(12) + + assert CurrencyHelper.format_according_to_decimals(amount, decimals) == "0.000000205" + end + + test "does not consider right zeros in decimal places" do + amount = Decimal.new(90_000_000) + decimals = Decimal.new(6) + + assert CurrencyHelper.format_according_to_decimals(amount, decimals) == "90" + end + + test "returns the full number when there is no right zeros in decimal places" do + amount = Decimal.new(9_324_876) + decimals = Decimal.new(6) + + assert CurrencyHelper.format_according_to_decimals(amount, decimals) == "9.324876" + end + + test "formats the value considering thousands separators" do + amount = Decimal.new(1_000_450) + decimals = Decimal.new(2) + + assert CurrencyHelper.format_according_to_decimals(amount, decimals) == "10,004.5" + end + + test "supports value as integer" do + amount = 1_000_450 + decimals = Decimal.new(2) + + assert CurrencyHelper.format_according_to_decimals(amount, decimals) == "10,004.5" + end + + test "considers 0 when decimals is nil" do + amount = 1_000_450 + decimals = nil + + assert CurrencyHelper.format_according_to_decimals(amount, decimals) == "1,000,450" + end + end + + describe "format_integer_to_currency/1" do + test "formats the integer value to a currency format" do + assert CurrencyHelper.format_integer_to_currency(9000) == "9,000" + end + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/views/error_helper_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/views/error_helper_test.exs new file mode 100644 index 0000000..7aac3cf --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/views/error_helper_test.exs @@ -0,0 +1,42 @@ +defmodule BlockScoutWeb.ErrorHelperTest do + use BlockScoutWeb.ConnCase, async: true + import Phoenix.HTML.Tag, only: [content_tag: 3] + + alias BlockScoutWeb.ErrorHelper + + @changeset %{ + errors: [ + contract_code: {"has already been taken", []} + ] + } + + describe "error_tag tests" do + test "error_tag/2 renders spans with default options" do + assert ErrorHelper.error_tag(@changeset, :contract_code) == [ + content_tag(:span, "has already been taken", class: "has-error") + ] + end + + test "error_tag/3 overrides default options" do + assert ErrorHelper.error_tag(@changeset, :contract_code, class: "something-else") == [ + content_tag(:span, "has already been taken", class: "something-else") + ] + end + + test "error_tag/3 merges given options with default ones" do + assert ErrorHelper.error_tag(@changeset, :contract_code, data_hidden: true) == [ + content_tag(:span, "has already been taken", class: "has-error", data_hidden: true) + ] + end + end + + describe "translate_error/1 tests" do + test "returns errors" do + assert ErrorHelper.translate_error({"test", []}) == "test" + end + + test "returns errors with count" do + assert ErrorHelper.translate_error({"%{count} test", [count: 1]}) == "1 test" + end + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/views/error_view_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/views/error_view_test.exs new file mode 100644 index 0000000..6babf7c --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/views/error_view_test.exs @@ -0,0 +1,22 @@ +defmodule BlockScoutWeb.ErrorViewTest do + use BlockScoutWeb.ConnCase, async: true + + # Bring render/3 and render_to_string/3 for testing custom views + import Phoenix.View + + test "renders 404.html" do + assert render_to_string(BlockScoutWeb.ErrorView, "404.html", []) == "Page not found" + end + + test "renders 422.html" do + assert render_to_string(BlockScoutWeb.ErrorView, "422.html", []) == "Unprocessable entity" + end + + test "render 500.html" do + assert render_to_string(BlockScoutWeb.ErrorView, "500.html", []) == "Internal server error" + end + + test "render any other" do + assert render_to_string(BlockScoutWeb.ErrorView, "505.html", []) == "Internal server error" + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/views/internal_transaction_view_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/views/internal_transaction_view_test.exs new file mode 100644 index 0000000..987f89f --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/views/internal_transaction_view_test.exs @@ -0,0 +1,40 @@ +defmodule BlockScoutWeb.InternalTransactionViewTest do + use BlockScoutWeb.ConnCase, async: true + + alias BlockScoutWeb.InternalTransactionView + alias Explorer.Chain.InternalTransaction + + doctest BlockScoutWeb.InternalTransactionView + + describe "type/1" do + test "returns the correct string when the type is :call and call type is :call" do + internal_transaction = %InternalTransaction{type: :call, call_type: :call} + + assert InternalTransactionView.type(internal_transaction) == "Call" + end + + test "returns the correct string when the type is :call and call type is :delegate_call" do + internal_transaction = %InternalTransaction{type: :call, call_type: :delegatecall} + + assert InternalTransactionView.type(internal_transaction) == "Delegate Call" + end + + test "returns the correct string when the type is :create" do + internal_transaction = %InternalTransaction{type: :create} + + assert InternalTransactionView.type(internal_transaction) == "Create" + end + + test "returns the correct string when the type is :selfdestruct" do + internal_transaction = %InternalTransaction{type: :selfdestruct} + + assert InternalTransactionView.type(internal_transaction) == "Self-Destruct" + end + + test "returns the correct string when the type is :reward" do + internal_transaction = %InternalTransaction{type: :reward} + + assert InternalTransactionView.type(internal_transaction) == "Reward" + end + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/views/layout_view_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/views/layout_view_test.exs new file mode 100644 index 0000000..b194934 --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/views/layout_view_test.exs @@ -0,0 +1,203 @@ +defmodule BlockScoutWeb.LayoutViewTest do + use BlockScoutWeb.ConnCase + + alias BlockScoutWeb.LayoutView + + test "configured_social_media_services/0" do + assert length(LayoutView.configured_social_media_services()) > 0 + end + + setup do + on_exit(fn -> + Application.put_env(:block_scout_web, BlockScoutWeb.Chain, []) + end) + end + + describe "logo/0" do + test "use the environment logo when it's configured" do + Application.put_env(:block_scout_web, BlockScoutWeb.Chain, logo: "custom/logo.png") + + assert LayoutView.logo() == "custom/logo.png" + end + + test "logo is nil when there is no env configured for it" do + assert LayoutView.logo() == nil + end + end + + describe "subnetwork_title/0" do + test "use the environment subnetwork title when it's configured" do + Application.put_env(:block_scout_web, BlockScoutWeb.Chain, subnetwork: "Subnetwork Test") + + assert LayoutView.subnetwork_title() == "Subnetwork Test" + end + + test "use the default subnetwork title when there is no env configured for it" do + assert LayoutView.subnetwork_title() == "Sokol" + end + end + + describe "network_title/0" do + test "use the environment network title when it's configured" do + Application.put_env(:block_scout_web, BlockScoutWeb.Chain, network: "Custom Network") + + assert LayoutView.network_title() == "Custom Network" + end + + test "use the default network title when there is no env configured for it" do + assert LayoutView.network_title() == "POA" + end + end + + describe "release_link/1" do + test "set empty string if no blockscout version configured" do + Application.put_env(:block_scout_web, :blockscout_version, nil) + + assert LayoutView.release_link(nil) == "" + end + + test "set empty string if blockscout version is empty string" do + Application.put_env(:block_scout_web, :blockscout_version, "") + + assert LayoutView.release_link("") == "" + end + + test "use the default value when there is no release_link env configured for it" do + Application.put_env(:block_scout_web, :release_link, nil) + + assert LayoutView.release_link("v1.3.4-beta") == + {:safe, + ~s(v1.3.4-beta)} + end + + test "use the default value when empty release_link env configured for it" do + Application.put_env(:block_scout_web, :release_link, "") + + assert LayoutView.release_link("v1.3.4-beta") == + {:safe, + ~s(v1.3.4-beta)} + end + + test "use the environment release link when it's configured" do + Application.put_env( + :block_scout_web, + :release_link, + "https://github.com/poanetwork/blockscout/releases/tag/v1.3.4-beta" + ) + + assert LayoutView.release_link("v1.3.4-beta") == + {:safe, + ~s(v1.3.4-beta)} + end + end + + @supported_chains_pattern ~s([ { "title": "RSK", "url": "https://blockscout.com/rsk/mainnet", "other?": true }, { "title": "Sokol", "url": "https://blockscout.com/poa/sokol", "test_net?": true }, { "title": "POA", "url": "https://blockscout.com/poa/core" }, { "title": "LUKSO L14", "url": "https://blockscout.com/lukso/l14", "test_net?": true, "hide_in_dropdown?": true } ]) + + describe "other_networks/0" do + test "get networks list based on env variables" do + Application.put_env(:block_scout_web, :other_networks, @supported_chains_pattern) + + assert LayoutView.other_networks() == [ + %{ + title: "POA", + url: "https://blockscout.com/poa/core" + }, + %{ + title: "RSK", + url: "https://blockscout.com/rsk/mainnet", + other?: true + }, + %{ + title: "LUKSO L14", + url: "https://blockscout.com/lukso/l14", + test_net?: true, + hide_in_dropdown?: true + } + ] + end + + test "get empty networks list if SUPPORTED_CHAINS is not parsed" do + Application.put_env(:block_scout_web, :other_networks, "not a valid json") + + assert LayoutView.other_networks() == [] + end + end + + describe "main_nets/1" do + test "get all main networks list based on env variables" do + Application.put_env(:block_scout_web, :other_networks, @supported_chains_pattern) + + assert LayoutView.main_nets(LayoutView.other_networks()) == [ + %{ + title: "POA", + url: "https://blockscout.com/poa/core" + }, + %{ + title: "RSK", + url: "https://blockscout.com/rsk/mainnet", + other?: true + } + ] + end + end + + describe "test_nets/1" do + test "get all networks list based on env variables" do + Application.put_env(:block_scout_web, :other_networks, @supported_chains_pattern) + + assert LayoutView.test_nets(LayoutView.other_networks()) == [ + %{ + title: "LUKSO L14", + url: "https://blockscout.com/lukso/l14", + test_net?: true, + hide_in_dropdown?: true + } + ] + end + end + + describe "dropdown_nets/0" do + test "get all dropdown networks list based on env variables" do + Application.put_env(:block_scout_web, :other_networks, @supported_chains_pattern) + + assert LayoutView.dropdown_nets() == [ + %{ + title: "POA", + url: "https://blockscout.com/poa/core" + }, + %{ + title: "RSK", + url: "https://blockscout.com/rsk/mainnet", + other?: true + } + ] + end + end + + describe "dropdown_head_main_nets/0" do + test "get dropdown all main networks except those of 'other' type list based on env variables" do + Application.put_env(:block_scout_web, :other_networks, @supported_chains_pattern) + + assert LayoutView.dropdown_head_main_nets() == [ + %{ + title: "POA", + url: "https://blockscout.com/poa/core" + } + ] + end + end + + describe "dropdown_other_nets/0" do + test "get dropdown networks of 'other' type list based on env variables" do + Application.put_env(:block_scout_web, :other_networks, @supported_chains_pattern) + + assert LayoutView.dropdown_other_nets() == [ + %{ + title: "RSK", + url: "https://blockscout.com/rsk/mainnet", + other?: true + } + ] + end + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/views/nft_helper_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/views/nft_helper_test.exs new file mode 100644 index 0000000..3af434c --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/views/nft_helper_test.exs @@ -0,0 +1,29 @@ +defmodule BlockScoutWeb.NFTHelperTest do + use BlockScoutWeb.ConnCase, async: true + + alias BlockScoutWeb.NFTHelper + + describe "compose_resource_url/1" do + test "transforms ipfs link like ipfs://${id}" do + url = "ipfs://QmYFf7D2UtqnNz8Lu57Gnk3dxgdAiuboPWMEaNNjhr29tS/hidden.png" + + assert "https://ipfs.io/ipfs/QmYFf7D2UtqnNz8Lu57Gnk3dxgdAiuboPWMEaNNjhr29tS/hidden.png" == + NFTHelper.compose_resource_url(url) + end + + test "transforms ipfs link like ipfs://ipfs" do + # cspell:disable-next-line + url = "ipfs://ipfs/Qmbgk4Ps5kiVdeYCHufMFgqzWLFuovFRtenY5P8m9vr9XW/animation.mp4" + + assert "https://ipfs.io/ipfs/Qmbgk4Ps5kiVdeYCHufMFgqzWLFuovFRtenY5P8m9vr9XW/animation.mp4" == + NFTHelper.compose_resource_url(url) + end + + test "transforms ipfs link in different case" do + url = "IpFs://baFybeid4ed2ua7fwupv4nx2ziczr3edhygl7ws3yx6y2juon7xakgj6cfm/51.json" + + assert "https://ipfs.io/ipfs/baFybeid4ed2ua7fwupv4nx2ziczr3edhygl7ws3yx6y2juon7xakgj6cfm/51.json" == + NFTHelper.compose_resource_url(url) + end + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/views/render_helper_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/views/render_helper_test.exs new file mode 100644 index 0000000..491d2fb --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/views/render_helper_test.exs @@ -0,0 +1,22 @@ +defmodule BlockScoutWeb.RenderHelperTest do + use BlockScoutWeb.ConnCase, async: true + + alias BlockScoutWeb.{BlockView, RenderHelper} + + describe "render_partial/1" do + test "renders text" do + assert "test" == RenderHelper.render_partial("test") + end + + test "renders the proper partial when view_module, partial and args are given" do + block = build(:block) + + assert {:safe, _} = + RenderHelper.render_partial( + view_module: BlockView, + partial: "_link.html", + block: block + ) + end + end +end diff --git a/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/views/search_view_test.exs b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/views/search_view_test.exs new file mode 100644 index 0000000..8bc64ad --- /dev/null +++ b/sz-poc-offsite-2025/blockscout/apps/block_scout_web/test/block_scout_web/views/search_view_test.exs @@ -0,0 +1,44 @@ +defmodule BlockScoutWeb.SearchViewTest do + use ExUnit.Case + alias BlockScoutWeb.SearchView + + test "highlight_search_result/2 returns search result if query doesn't match" do + query = "test" + search_result = "qwerty" + res = SearchView.highlight_search_result(search_result, query) + IO.inspect(res) + + assert res == {:safe, search_result} + end + + test "highlight_search_result/2 returns safe HTML of unsafe search result if query doesn't match" do + query = "test" + search_result = "qwe1'\">