Initial commit with 🏗️ Scaffold-ETH 2 @ 1.0.2
This commit is contained in:
13
packages/hardhat/.env.example
Normal file
13
packages/hardhat/.env.example
Normal file
@@ -0,0 +1,13 @@
|
||||
# Template for Hardhat environment variables.
|
||||
|
||||
# To use this template, copy this file, rename it .env, and fill in the values.
|
||||
|
||||
# If not set, we provide default values (check `hardhat.config.ts`) so developers can start prototyping out of the box,
|
||||
# but we recommend getting your own API Keys for Production Apps.
|
||||
|
||||
# To access the values stored in this .env file you can use: process.env.VARIABLENAME
|
||||
ALCHEMY_API_KEY=
|
||||
ETHERSCAN_V2_API_KEY=
|
||||
|
||||
# Don't fill this value manually, run yarn generate to generate a new account or yarn account:import to import an existing PK.
|
||||
DEPLOYER_PRIVATE_KEY_ENCRYPTED=
|
||||
30
packages/hardhat/.gitignore
vendored
Normal file
30
packages/hardhat/.gitignore
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
# dependencies
|
||||
node_modules
|
||||
|
||||
# env files
|
||||
.env
|
||||
|
||||
# coverage
|
||||
coverage
|
||||
coverage.json
|
||||
|
||||
# typechain
|
||||
typechain
|
||||
typechain-types
|
||||
|
||||
# hardhat files
|
||||
cache
|
||||
artifacts
|
||||
|
||||
# zkSync files
|
||||
artifacts-zk
|
||||
cache-zk
|
||||
|
||||
# deployments
|
||||
deployments/localhost
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
|
||||
# other
|
||||
temp
|
||||
18
packages/hardhat/.prettierrc.json
Normal file
18
packages/hardhat/.prettierrc.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"plugins": ["prettier-plugin-solidity"],
|
||||
"arrowParens": "avoid",
|
||||
"printWidth": 120,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "all",
|
||||
"overrides": [
|
||||
{
|
||||
"files": "*.sol",
|
||||
"options": {
|
||||
"printWidth": 120,
|
||||
"tabWidth": 4,
|
||||
"singleQuote": false,
|
||||
"bracketSpacing": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
10
packages/hardhat/contracts/Balloons.sol
Normal file
10
packages/hardhat/contracts/Balloons.sol
Normal file
@@ -0,0 +1,10 @@
|
||||
//SPDX-License-Identifier: MIT
|
||||
pragma solidity >=0.8.0 <0.9.0;
|
||||
|
||||
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
||||
|
||||
contract Balloons is ERC20 {
|
||||
constructor() ERC20("Balloons", "BAL") {
|
||||
_mint(msg.sender, 1000 ether); // mints 1000 balloons!
|
||||
}
|
||||
}
|
||||
99
packages/hardhat/contracts/DEX.sol
Normal file
99
packages/hardhat/contracts/DEX.sol
Normal file
@@ -0,0 +1,99 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity >=0.8.0 <0.9.0;
|
||||
|
||||
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||
|
||||
/**
|
||||
* @title DEX Template
|
||||
* @author stevepham.eth and m00npapi.eth
|
||||
* @notice Empty DEX.sol that just outlines what features could be part of the challenge (up to you!)
|
||||
* @dev We want to create an automatic market where our contract will hold reserves of both ETH and 🎈 Balloons. These reserves will provide liquidity that allows anyone to swap between the assets.
|
||||
* NOTE: functions outlined here are what work with the front end of this challenge. Also return variable names need to be specified exactly may be referenced (It may be helpful to cross reference with front-end code function calls).
|
||||
*/
|
||||
contract DEX {
|
||||
/* ========== GLOBAL VARIABLES ========== */
|
||||
|
||||
IERC20 token; //instantiates the imported contract
|
||||
|
||||
/* ========== EVENTS ========== */
|
||||
|
||||
/**
|
||||
* @notice Emitted when ethToToken() swap transacted
|
||||
*/
|
||||
event EthToTokenSwap(address swapper, uint256 tokenOutput, uint256 ethInput);
|
||||
|
||||
/**
|
||||
* @notice Emitted when tokenToEth() swap transacted
|
||||
*/
|
||||
event TokenToEthSwap(address swapper, uint256 tokensInput, uint256 ethOutput);
|
||||
|
||||
/**
|
||||
* @notice Emitted when liquidity provided to DEX and mints LPTs.
|
||||
*/
|
||||
event LiquidityProvided(address liquidityProvider, uint256 liquidityMinted, uint256 ethInput, uint256 tokensInput);
|
||||
|
||||
/**
|
||||
* @notice Emitted when liquidity removed from DEX and decreases LPT count within DEX.
|
||||
*/
|
||||
event LiquidityRemoved(
|
||||
address liquidityRemover,
|
||||
uint256 liquidityWithdrawn,
|
||||
uint256 tokensOutput,
|
||||
uint256 ethOutput
|
||||
);
|
||||
|
||||
/* ========== CONSTRUCTOR ========== */
|
||||
|
||||
constructor(address tokenAddr) {
|
||||
token = IERC20(tokenAddr); //specifies the token address that will hook into the interface and be used through the variable 'token'
|
||||
}
|
||||
|
||||
/* ========== MUTATIVE FUNCTIONS ========== */
|
||||
|
||||
/**
|
||||
* @notice initializes amount of tokens that will be transferred to the DEX itself from the erc20 contract mintee (and only them based on how Balloons.sol is written). Loads contract up with both ETH and Balloons.
|
||||
* @param tokens amount to be transferred to DEX
|
||||
* @return totalLiquidity is the number of LPTs minting as a result of deposits made to DEX contract
|
||||
* NOTE: since ratio is 1:1, this is fine to initialize the totalLiquidity (wrt to balloons) as equal to eth balance of contract.
|
||||
*/
|
||||
function init(uint256 tokens) public payable returns (uint256) {}
|
||||
|
||||
/**
|
||||
* @notice returns yOutput, or yDelta for xInput (or xDelta)
|
||||
* @dev Follow along with the [original tutorial](https://medium.com/@austin_48503/%EF%B8%8F-minimum-viable-exchange-d84f30bd0c90) Price section for an understanding of the DEX's pricing model and for a price function to add to your contract. You may need to update the Solidity syntax (e.g. use + instead of .add, * instead of .mul, etc). Deploy when you are done.
|
||||
*/
|
||||
function price(uint256 xInput, uint256 xReserves, uint256 yReserves) public pure returns (uint256 yOutput) {}
|
||||
|
||||
/**
|
||||
* @notice returns liquidity for a user.
|
||||
* NOTE: this is not needed typically due to the `liquidity()` mapping variable being public and having a getter as a result. This is left though as it is used within the front end code (App.jsx).
|
||||
* NOTE: if you are using a mapping liquidity, then you can use `return liquidity[lp]` to get the liquidity for a user.
|
||||
* NOTE: if you will be submitting the challenge make sure to implement this function as it is used in the tests.
|
||||
*/
|
||||
function getLiquidity(address lp) public view returns (uint256) {}
|
||||
|
||||
/**
|
||||
* @notice sends Ether to DEX in exchange for $BAL
|
||||
*/
|
||||
function ethToToken() public payable returns (uint256 tokenOutput) {}
|
||||
|
||||
/**
|
||||
* @notice sends $BAL tokens to DEX in exchange for Ether
|
||||
*/
|
||||
function tokenToEth(uint256 tokenInput) public returns (uint256 ethOutput) {}
|
||||
|
||||
/**
|
||||
* @notice allows deposits of $BAL and $ETH to liquidity pool
|
||||
* NOTE: parameter is the msg.value sent with this function call. That amount is used to determine the amount of $BAL needed as well and taken from the depositor.
|
||||
* NOTE: user has to make sure to give DEX approval to spend their tokens on their behalf by calling approve function prior to this function call.
|
||||
* NOTE: Equal parts of both assets will be removed from the user's wallet with respect to the price outlined by the AMM.
|
||||
*/
|
||||
function deposit() public payable returns (uint256 tokensDeposited) {}
|
||||
|
||||
/**
|
||||
* @notice allows withdrawal of $BAL and $ETH from liquidity pool
|
||||
* NOTE: with this current code, the msg caller could end up getting very little back if the liquidity is super low in the pool. I guess they could see that with the UI.
|
||||
*/
|
||||
function withdraw(uint256 amount) public returns (uint256 ethAmount, uint256 tokenAmount) {}
|
||||
}
|
||||
72
packages/hardhat/deploy/00_deploy_dex.ts
Normal file
72
packages/hardhat/deploy/00_deploy_dex.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { HardhatRuntimeEnvironment } from "hardhat/types";
|
||||
import { DeployFunction } from "hardhat-deploy/types";
|
||||
import { DEX } from "../typechain-types/contracts/DEX";
|
||||
import { Balloons } from "../typechain-types/contracts/Balloons";
|
||||
|
||||
/**
|
||||
* Deploys a contract named "YourContract" using the deployer account and
|
||||
* constructor arguments set to the deployer address
|
||||
*
|
||||
* @param hre HardhatRuntimeEnvironment object.
|
||||
*/
|
||||
const deployYourContract: DeployFunction = async function (hre: HardhatRuntimeEnvironment) {
|
||||
/*
|
||||
On localhost, the deployer account is the one that comes with Hardhat, which is already funded.
|
||||
|
||||
When deploying to live networks (e.g `yarn deploy --network sepolia`), the deployer account
|
||||
should have sufficient balance to pay for the gas fees for contract creation.
|
||||
|
||||
You can generate a random account with `yarn generate` which will fill DEPLOYER_PRIVATE_KEY
|
||||
with a random private key in the .env file (then used on hardhat.config.ts)
|
||||
You can run the `yarn account` command to check your balance in every network.
|
||||
*/
|
||||
const { deployer } = await hre.getNamedAccounts();
|
||||
const { deploy } = hre.deployments;
|
||||
|
||||
await deploy("Balloons", {
|
||||
from: deployer,
|
||||
// Contract constructor arguments
|
||||
//args: [deployer],
|
||||
log: true,
|
||||
// autoMine: can be passed to the deploy function to make the deployment process faster on local networks by
|
||||
// automatically mining the contract deployment transaction. There is no effect on live networks.
|
||||
autoMine: true,
|
||||
});
|
||||
// Get the deployed contract
|
||||
// const yourContract = await hre.ethers.getContract("YourContract", deployer);
|
||||
const balloons: Balloons = await hre.ethers.getContract("Balloons", deployer);
|
||||
const balloonsAddress = await balloons.getAddress();
|
||||
|
||||
await deploy("DEX", {
|
||||
from: deployer,
|
||||
// Contract constructor arguments
|
||||
args: [balloonsAddress],
|
||||
log: true,
|
||||
// autoMine: can be passed to the deploy function to make the deployment process faster on local networks by
|
||||
// automatically mining the contract deployment transaction. There is no effect on live networks.
|
||||
autoMine: true,
|
||||
});
|
||||
|
||||
const dex = (await hre.ethers.getContract("DEX", deployer)) as DEX;
|
||||
|
||||
// // paste in your front-end address here to get 10 balloons on deploy:
|
||||
// await balloons.transfer("YOUR_FRONTEND_ADDRESS", "" + 10 * 10 ** 18);
|
||||
|
||||
// // uncomment to init DEX on deploy:
|
||||
|
||||
// const dexAddress = await dex.getAddress();
|
||||
// console.log("Approving DEX (" + dexAddress + ") to take Balloons from main account...");
|
||||
// // If you are going to the testnet make sure your deployer account has enough ETH
|
||||
// await balloons.approve(dexAddress, hre.ethers.parseEther("100"));
|
||||
// console.log("INIT exchange...");
|
||||
// await dex.init(hre.ethers.parseEther("5"), {
|
||||
// value: hre.ethers.parseEther("5"),
|
||||
// gasLimit: 200000,
|
||||
// });
|
||||
};
|
||||
|
||||
export default deployYourContract;
|
||||
|
||||
// Tags are useful if you have multiple deploy files and only want to run one of them.
|
||||
// e.g. yarn deploy --tags YourContract
|
||||
deployYourContract.tags = ["Balloons", "DEX"];
|
||||
44
packages/hardhat/eslint.config.mjs
Normal file
44
packages/hardhat/eslint.config.mjs
Normal file
@@ -0,0 +1,44 @@
|
||||
import { defineConfig, globalIgnores } from "eslint/config";
|
||||
import globals from "globals";
|
||||
import tsParser from "@typescript-eslint/parser";
|
||||
import prettierPlugin from "eslint-plugin-prettier";
|
||||
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { FlatCompat } from "@eslint/eslintrc";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
});
|
||||
|
||||
export default defineConfig([
|
||||
globalIgnores(["**/artifacts", "**/cache", "**/contracts", "**/node_modules/", "**/typechain-types", "**/*.json"]),
|
||||
{
|
||||
extends: compat.extends("plugin:@typescript-eslint/recommended", "prettier"),
|
||||
|
||||
plugins: {
|
||||
prettier: prettierPlugin,
|
||||
},
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
},
|
||||
|
||||
parser: tsParser,
|
||||
},
|
||||
|
||||
rules: {
|
||||
"@typescript-eslint/no-unused-vars": "error",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
|
||||
"prettier/prettier": [
|
||||
"warn",
|
||||
{
|
||||
endOfLine: "auto",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
]);
|
||||
152
packages/hardhat/hardhat.config.ts
Normal file
152
packages/hardhat/hardhat.config.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
import * as dotenv from "dotenv";
|
||||
dotenv.config();
|
||||
import { HardhatUserConfig } from "hardhat/config";
|
||||
import "@nomicfoundation/hardhat-ethers";
|
||||
import "@nomicfoundation/hardhat-chai-matchers";
|
||||
import "@typechain/hardhat";
|
||||
import "hardhat-gas-reporter";
|
||||
import "solidity-coverage";
|
||||
import "@nomicfoundation/hardhat-verify";
|
||||
import "hardhat-deploy";
|
||||
import "hardhat-deploy-ethers";
|
||||
import { task } from "hardhat/config";
|
||||
import generateTsAbis from "./scripts/generateTsAbis";
|
||||
|
||||
// If not set, it uses ours Alchemy's default API key.
|
||||
// You can get your own at https://dashboard.alchemyapi.io
|
||||
const providerApiKey = process.env.ALCHEMY_API_KEY || "oKxs-03sij-U_N0iOlrSsZFr29-IqbuF";
|
||||
// If not set, it uses the hardhat account 0 private key.
|
||||
// You can generate a random account with `yarn generate` or `yarn account:import` to import your existing PK
|
||||
const deployerPrivateKey =
|
||||
process.env.__RUNTIME_DEPLOYER_PRIVATE_KEY ?? "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
|
||||
// If not set, it uses our block explorers default API keys.
|
||||
const etherscanApiKey = process.env.ETHERSCAN_V2_API_KEY || "DNXJA8RX2Q3VZ4URQIWP7Z68CJXQZSC6AW";
|
||||
|
||||
const config: HardhatUserConfig = {
|
||||
solidity: {
|
||||
compilers: [
|
||||
{
|
||||
version: "0.8.20",
|
||||
settings: {
|
||||
optimizer: {
|
||||
enabled: true,
|
||||
// https://docs.soliditylang.org/en/latest/using-the-compiler.html#optimizer-options
|
||||
runs: 200,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
defaultNetwork: "localhost",
|
||||
namedAccounts: {
|
||||
deployer: {
|
||||
// By default, it will take the first Hardhat account as the deployer
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
networks: {
|
||||
// View the networks that are pre-configured.
|
||||
// If the network you are looking for is not here you can add new network settings
|
||||
hardhat: {
|
||||
forking: {
|
||||
url: `https://eth-mainnet.alchemyapi.io/v2/${providerApiKey}`,
|
||||
enabled: process.env.MAINNET_FORKING_ENABLED === "true",
|
||||
},
|
||||
},
|
||||
mainnet: {
|
||||
url: "https://mainnet.rpc.buidlguidl.com",
|
||||
accounts: [deployerPrivateKey],
|
||||
},
|
||||
sepolia: {
|
||||
url: `https://eth-sepolia.g.alchemy.com/v2/${providerApiKey}`,
|
||||
accounts: [deployerPrivateKey],
|
||||
},
|
||||
arbitrum: {
|
||||
url: `https://arb-mainnet.g.alchemy.com/v2/${providerApiKey}`,
|
||||
accounts: [deployerPrivateKey],
|
||||
},
|
||||
arbitrumSepolia: {
|
||||
url: `https://arb-sepolia.g.alchemy.com/v2/${providerApiKey}`,
|
||||
accounts: [deployerPrivateKey],
|
||||
},
|
||||
optimism: {
|
||||
url: `https://opt-mainnet.g.alchemy.com/v2/${providerApiKey}`,
|
||||
accounts: [deployerPrivateKey],
|
||||
},
|
||||
optimismSepolia: {
|
||||
url: `https://opt-sepolia.g.alchemy.com/v2/${providerApiKey}`,
|
||||
accounts: [deployerPrivateKey],
|
||||
},
|
||||
polygon: {
|
||||
url: `https://polygon-mainnet.g.alchemy.com/v2/${providerApiKey}`,
|
||||
accounts: [deployerPrivateKey],
|
||||
},
|
||||
polygonAmoy: {
|
||||
url: `https://polygon-amoy.g.alchemy.com/v2/${providerApiKey}`,
|
||||
accounts: [deployerPrivateKey],
|
||||
},
|
||||
polygonZkEvm: {
|
||||
url: `https://polygonzkevm-mainnet.g.alchemy.com/v2/${providerApiKey}`,
|
||||
accounts: [deployerPrivateKey],
|
||||
},
|
||||
polygonZkEvmCardona: {
|
||||
url: `https://polygonzkevm-cardona.g.alchemy.com/v2/${providerApiKey}`,
|
||||
accounts: [deployerPrivateKey],
|
||||
},
|
||||
gnosis: {
|
||||
url: "https://rpc.gnosischain.com",
|
||||
accounts: [deployerPrivateKey],
|
||||
},
|
||||
chiado: {
|
||||
url: "https://rpc.chiadochain.net",
|
||||
accounts: [deployerPrivateKey],
|
||||
},
|
||||
base: {
|
||||
url: "https://mainnet.base.org",
|
||||
accounts: [deployerPrivateKey],
|
||||
},
|
||||
baseSepolia: {
|
||||
url: "https://sepolia.base.org",
|
||||
accounts: [deployerPrivateKey],
|
||||
},
|
||||
scrollSepolia: {
|
||||
url: "https://sepolia-rpc.scroll.io",
|
||||
accounts: [deployerPrivateKey],
|
||||
},
|
||||
scroll: {
|
||||
url: "https://rpc.scroll.io",
|
||||
accounts: [deployerPrivateKey],
|
||||
},
|
||||
celo: {
|
||||
url: "https://forno.celo.org",
|
||||
accounts: [deployerPrivateKey],
|
||||
},
|
||||
celoAlfajores: {
|
||||
url: "https://alfajores-forno.celo-testnet.org",
|
||||
accounts: [deployerPrivateKey],
|
||||
},
|
||||
},
|
||||
// Configuration for harhdat-verify plugin
|
||||
etherscan: {
|
||||
apiKey: etherscanApiKey,
|
||||
},
|
||||
// Configuration for etherscan-verify from hardhat-deploy plugin
|
||||
verify: {
|
||||
etherscan: {
|
||||
apiKey: etherscanApiKey,
|
||||
},
|
||||
},
|
||||
sourcify: {
|
||||
enabled: false,
|
||||
},
|
||||
};
|
||||
|
||||
// Extend the deploy task
|
||||
task("deploy").setAction(async (args, hre, runSuper) => {
|
||||
// Run the original deploy task
|
||||
await runSuper(args);
|
||||
// Force run the generateTsAbis script
|
||||
await generateTsAbis(hre);
|
||||
});
|
||||
|
||||
export default config;
|
||||
63
packages/hardhat/package.json
Normal file
63
packages/hardhat/package.json
Normal file
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"name": "@se-2/hardhat",
|
||||
"version": "0.0.1",
|
||||
"scripts": {
|
||||
"account": "hardhat run scripts/listAccount.ts",
|
||||
"account:generate": "hardhat run scripts/generateAccount.ts",
|
||||
"account:import": "hardhat run scripts/importAccount.ts",
|
||||
"account:reveal-pk": "hardhat run scripts/revealPK.ts",
|
||||
"chain": "hardhat node --network hardhat --no-deploy",
|
||||
"check-types": "tsc --noEmit --incremental",
|
||||
"clean": "hardhat clean",
|
||||
"compile": "hardhat compile",
|
||||
"deploy": "ts-node scripts/runHardhatDeployWithPK.ts",
|
||||
"flatten": "hardhat flatten",
|
||||
"fork": "MAINNET_FORKING_ENABLED=true hardhat node --network hardhat --no-deploy",
|
||||
"format": "prettier --write './**/*.(ts|sol)'",
|
||||
"generate": "yarn account:generate",
|
||||
"hardhat-verify": "hardhat verify",
|
||||
"lint": "eslint",
|
||||
"lint-staged": "eslint",
|
||||
"test": "REPORT_GAS=true hardhat test --network hardhat",
|
||||
"verify": "hardhat etherscan-verify"
|
||||
},
|
||||
"dependencies": {
|
||||
"@inquirer/password": "^4.0.2",
|
||||
"@openzeppelin/contracts": "~5.0.2",
|
||||
"@typechain/ethers-v6": "~0.5.1",
|
||||
"dotenv": "~16.4.5",
|
||||
"envfile": "~7.1.0",
|
||||
"qrcode": "~1.5.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ethersproject/abi": "~5.7.0",
|
||||
"@ethersproject/providers": "~5.7.2",
|
||||
"@nomicfoundation/hardhat-chai-matchers": "~2.0.7",
|
||||
"@nomicfoundation/hardhat-ethers": "~3.0.8",
|
||||
"@nomicfoundation/hardhat-network-helpers": "~1.0.11",
|
||||
"@nomicfoundation/hardhat-verify": "~2.0.10",
|
||||
"@typechain/ethers-v5": "~11.1.2",
|
||||
"@typechain/hardhat": "~9.1.0",
|
||||
"@types/eslint": "~9.6.1",
|
||||
"@types/mocha": "~10.0.10",
|
||||
"@types/prettier": "~3.0.0",
|
||||
"@types/qrcode": "~1.5.5",
|
||||
"@typescript-eslint/eslint-plugin": "~8.27.0",
|
||||
"@typescript-eslint/parser": "~8.27.0",
|
||||
"chai": "~4.5.0",
|
||||
"eslint": "~9.23.0",
|
||||
"eslint-config-prettier": "~10.1.1",
|
||||
"eslint-plugin-prettier": "~5.2.4",
|
||||
"ethers": "~6.13.2",
|
||||
"hardhat": "~2.22.10",
|
||||
"hardhat-deploy": "^1.0.4",
|
||||
"hardhat-deploy-ethers": "~0.4.2",
|
||||
"hardhat-gas-reporter": "~2.2.1",
|
||||
"prettier": "^3.5.3",
|
||||
"prettier-plugin-solidity": "~1.4.1",
|
||||
"solidity-coverage": "~0.8.13",
|
||||
"ts-node": "~10.9.1",
|
||||
"typechain": "~8.3.2",
|
||||
"typescript": "^5.8.2"
|
||||
}
|
||||
}
|
||||
58
packages/hardhat/scripts/generateAccount.ts
Normal file
58
packages/hardhat/scripts/generateAccount.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { ethers } from "ethers";
|
||||
import { parse, stringify } from "envfile";
|
||||
import * as fs from "fs";
|
||||
import password from "@inquirer/password";
|
||||
|
||||
const envFilePath = "./.env";
|
||||
|
||||
const getValidatedPassword = async () => {
|
||||
while (true) {
|
||||
const pass = await password({ message: "Enter a password to encrypt your private key:" });
|
||||
const confirmation = await password({ message: "Confirm password:" });
|
||||
|
||||
if (pass === confirmation) {
|
||||
return pass;
|
||||
}
|
||||
console.log("❌ Passwords don't match. Please try again.");
|
||||
}
|
||||
};
|
||||
|
||||
const setNewEnvConfig = async (existingEnvConfig = {}) => {
|
||||
console.log("👛 Generating new Wallet\n");
|
||||
const randomWallet = ethers.Wallet.createRandom();
|
||||
|
||||
const pass = await getValidatedPassword();
|
||||
const encryptedJson = await randomWallet.encrypt(pass);
|
||||
|
||||
const newEnvConfig = {
|
||||
...existingEnvConfig,
|
||||
DEPLOYER_PRIVATE_KEY_ENCRYPTED: encryptedJson,
|
||||
};
|
||||
|
||||
// Store in .env
|
||||
fs.writeFileSync(envFilePath, stringify(newEnvConfig));
|
||||
console.log("\n📄 Encrypted Private Key saved to packages/hardhat/.env file");
|
||||
console.log("🪄 Generated wallet address:", randomWallet.address, "\n");
|
||||
console.log("⚠️ Make sure to remember your password! You'll need it to decrypt the private key.");
|
||||
};
|
||||
|
||||
async function main() {
|
||||
if (!fs.existsSync(envFilePath)) {
|
||||
// No .env file yet.
|
||||
await setNewEnvConfig();
|
||||
return;
|
||||
}
|
||||
|
||||
const existingEnvConfig = parse(fs.readFileSync(envFilePath).toString());
|
||||
if (existingEnvConfig.DEPLOYER_PRIVATE_KEY_ENCRYPTED) {
|
||||
console.log("⚠️ You already have a deployer account. Check the packages/hardhat/.env file");
|
||||
return;
|
||||
}
|
||||
|
||||
await setNewEnvConfig(existingEnvConfig);
|
||||
}
|
||||
|
||||
main().catch(error => {
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
127
packages/hardhat/scripts/generateTsAbis.ts
Normal file
127
packages/hardhat/scripts/generateTsAbis.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
/**
|
||||
* DON'T MODIFY OR DELETE THIS SCRIPT (unless you know what you're doing)
|
||||
*
|
||||
* This script generates the file containing the contracts Abi definitions.
|
||||
* These definitions are used to derive the types needed in the custom scaffold-eth hooks, for example.
|
||||
* This script should run as the last deploy script.
|
||||
*/
|
||||
|
||||
import * as fs from "fs";
|
||||
import prettier from "prettier";
|
||||
import { DeployFunction } from "hardhat-deploy/types";
|
||||
|
||||
const generatedContractComment = `
|
||||
/**
|
||||
* This file is autogenerated by Scaffold-ETH.
|
||||
* You should not edit it manually or your changes might be overwritten.
|
||||
*/
|
||||
`;
|
||||
|
||||
const DEPLOYMENTS_DIR = "./deployments";
|
||||
const ARTIFACTS_DIR = "./artifacts";
|
||||
|
||||
function getDirectories(path: string) {
|
||||
return fs
|
||||
.readdirSync(path, { withFileTypes: true })
|
||||
.filter(dirent => dirent.isDirectory())
|
||||
.map(dirent => dirent.name);
|
||||
}
|
||||
|
||||
function getContractNames(path: string) {
|
||||
return fs
|
||||
.readdirSync(path, { withFileTypes: true })
|
||||
.filter(dirent => dirent.isFile() && dirent.name.endsWith(".json"))
|
||||
.map(dirent => dirent.name.split(".")[0]);
|
||||
}
|
||||
|
||||
function getActualSourcesForContract(sources: Record<string, any>, contractName: string) {
|
||||
for (const sourcePath of Object.keys(sources)) {
|
||||
const sourceName = sourcePath.split("/").pop()?.split(".sol")[0];
|
||||
if (sourceName === contractName) {
|
||||
const contractContent = sources[sourcePath].content as string;
|
||||
const regex = /contract\s+(\w+)\s+is\s+([^{}]+)\{/;
|
||||
const match = contractContent.match(regex);
|
||||
|
||||
if (match) {
|
||||
const inheritancePart = match[2];
|
||||
// Split the inherited contracts by commas to get the list of inherited contracts
|
||||
const inheritedContracts = inheritancePart.split(",").map(contract => `${contract.trim()}.sol`);
|
||||
|
||||
return inheritedContracts;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function getInheritedFunctions(sources: Record<string, any>, contractName: string) {
|
||||
const actualSources = getActualSourcesForContract(sources, contractName);
|
||||
const inheritedFunctions = {} as Record<string, any>;
|
||||
|
||||
for (const sourceContractName of actualSources) {
|
||||
const sourcePath = Object.keys(sources).find(key => key.includes(`/${sourceContractName}`));
|
||||
if (sourcePath) {
|
||||
const sourceName = sourcePath?.split("/").pop()?.split(".sol")[0];
|
||||
const { abi } = JSON.parse(fs.readFileSync(`${ARTIFACTS_DIR}/${sourcePath}/${sourceName}.json`).toString());
|
||||
for (const functionAbi of abi) {
|
||||
if (functionAbi.type === "function") {
|
||||
inheritedFunctions[functionAbi.name] = sourcePath;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return inheritedFunctions;
|
||||
}
|
||||
|
||||
function getContractDataFromDeployments() {
|
||||
if (!fs.existsSync(DEPLOYMENTS_DIR)) {
|
||||
throw Error("At least one other deployment script should exist to generate an actual contract.");
|
||||
}
|
||||
const output = {} as Record<string, any>;
|
||||
for (const chainName of getDirectories(DEPLOYMENTS_DIR)) {
|
||||
const chainId = fs.readFileSync(`${DEPLOYMENTS_DIR}/${chainName}/.chainId`).toString();
|
||||
const contracts = {} as Record<string, any>;
|
||||
for (const contractName of getContractNames(`${DEPLOYMENTS_DIR}/${chainName}`)) {
|
||||
const { abi, address, metadata, receipt } = JSON.parse(
|
||||
fs.readFileSync(`${DEPLOYMENTS_DIR}/${chainName}/${contractName}.json`).toString(),
|
||||
);
|
||||
const inheritedFunctions = metadata ? getInheritedFunctions(JSON.parse(metadata).sources, contractName) : {};
|
||||
contracts[contractName] = { address, abi, inheritedFunctions, deployedOnBlock: receipt.blockNumber };
|
||||
}
|
||||
output[chainId] = contracts;
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the TypeScript contract definition file based on the json output of the contract deployment scripts
|
||||
* This script should be run last.
|
||||
*/
|
||||
const generateTsAbis: DeployFunction = async function () {
|
||||
const TARGET_DIR = "../nextjs/contracts/";
|
||||
const allContractsData = getContractDataFromDeployments();
|
||||
|
||||
const fileContent = Object.entries(allContractsData).reduce((content, [chainId, chainConfig]) => {
|
||||
return `${content}${parseInt(chainId).toFixed(0)}:${JSON.stringify(chainConfig, null, 2)},`;
|
||||
}, "");
|
||||
|
||||
if (!fs.existsSync(TARGET_DIR)) {
|
||||
fs.mkdirSync(TARGET_DIR);
|
||||
}
|
||||
fs.writeFileSync(
|
||||
`${TARGET_DIR}deployedContracts.ts`,
|
||||
await prettier.format(
|
||||
`${generatedContractComment} import { GenericContractsDeclaration } from "~~/utils/scaffold-eth/contract"; \n\n
|
||||
const deployedContracts = {${fileContent}} as const; \n\n export default deployedContracts satisfies GenericContractsDeclaration`,
|
||||
{
|
||||
parser: "typescript",
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
console.log(`📝 Updated TypeScript contract definition file on ${TARGET_DIR}deployedContracts.ts`);
|
||||
};
|
||||
|
||||
export default generateTsAbis;
|
||||
72
packages/hardhat/scripts/importAccount.ts
Normal file
72
packages/hardhat/scripts/importAccount.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { ethers } from "ethers";
|
||||
import { parse, stringify } from "envfile";
|
||||
import * as fs from "fs";
|
||||
import password from "@inquirer/password";
|
||||
|
||||
const envFilePath = "./.env";
|
||||
|
||||
const getValidatedPassword = async () => {
|
||||
while (true) {
|
||||
const pass = await password({ message: "Enter a password to encrypt your private key:" });
|
||||
const confirmation = await password({ message: "Confirm password:" });
|
||||
|
||||
if (pass === confirmation) {
|
||||
return pass;
|
||||
}
|
||||
console.log("❌ Passwords don't match. Please try again.");
|
||||
}
|
||||
};
|
||||
|
||||
const getWalletFromPrivateKey = async () => {
|
||||
while (true) {
|
||||
const privateKey = await password({ message: "Paste your private key:" });
|
||||
try {
|
||||
const wallet = new ethers.Wallet(privateKey);
|
||||
return wallet;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
} catch (e) {
|
||||
console.log("❌ Invalid private key format. Please try again.");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const setNewEnvConfig = async (existingEnvConfig = {}) => {
|
||||
console.log("👛 Importing Wallet\n");
|
||||
|
||||
const wallet = await getWalletFromPrivateKey();
|
||||
|
||||
const pass = await getValidatedPassword();
|
||||
const encryptedJson = await wallet.encrypt(pass);
|
||||
|
||||
const newEnvConfig = {
|
||||
...existingEnvConfig,
|
||||
DEPLOYER_PRIVATE_KEY_ENCRYPTED: encryptedJson,
|
||||
};
|
||||
|
||||
// Store in .env
|
||||
fs.writeFileSync(envFilePath, stringify(newEnvConfig));
|
||||
console.log("\n📄 Encrypted Private Key saved to packages/hardhat/.env file");
|
||||
console.log("🪄 Imported wallet address:", wallet.address, "\n");
|
||||
console.log("⚠️ Make sure to remember your password! You'll need it to decrypt the private key.");
|
||||
};
|
||||
|
||||
async function main() {
|
||||
if (!fs.existsSync(envFilePath)) {
|
||||
// No .env file yet.
|
||||
await setNewEnvConfig();
|
||||
return;
|
||||
}
|
||||
|
||||
const existingEnvConfig = parse(fs.readFileSync(envFilePath).toString());
|
||||
if (existingEnvConfig.DEPLOYER_PRIVATE_KEY_ENCRYPTED) {
|
||||
console.log("⚠️ You already have a deployer account. Check the packages/hardhat/.env file");
|
||||
return;
|
||||
}
|
||||
|
||||
await setNewEnvConfig(existingEnvConfig);
|
||||
}
|
||||
|
||||
main().catch(error => {
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
52
packages/hardhat/scripts/listAccount.ts
Normal file
52
packages/hardhat/scripts/listAccount.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import * as dotenv from "dotenv";
|
||||
dotenv.config();
|
||||
import { ethers, Wallet } from "ethers";
|
||||
import QRCode from "qrcode";
|
||||
import { config } from "hardhat";
|
||||
import password from "@inquirer/password";
|
||||
|
||||
async function main() {
|
||||
const encryptedKey = process.env.DEPLOYER_PRIVATE_KEY_ENCRYPTED;
|
||||
|
||||
if (!encryptedKey) {
|
||||
console.log("🚫️ You don't have a deployer account. Run `yarn generate` or `yarn account:import` first");
|
||||
return;
|
||||
}
|
||||
|
||||
const pass = await password({ message: "Enter your password to decrypt the private key:" });
|
||||
let wallet: Wallet;
|
||||
try {
|
||||
wallet = (await Wallet.fromEncryptedJson(encryptedKey, pass)) as Wallet;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
} catch (e) {
|
||||
console.log("❌ Failed to decrypt private key. Wrong password?");
|
||||
return;
|
||||
}
|
||||
|
||||
const address = wallet.address;
|
||||
console.log(await QRCode.toString(address, { type: "terminal", small: true }));
|
||||
console.log("Public address:", address, "\n");
|
||||
|
||||
// Balance on each network
|
||||
const availableNetworks = config.networks;
|
||||
for (const networkName in availableNetworks) {
|
||||
try {
|
||||
const network = availableNetworks[networkName];
|
||||
if (!("url" in network)) continue;
|
||||
const provider = new ethers.JsonRpcProvider(network.url);
|
||||
await provider._detectNetwork();
|
||||
const balance = await provider.getBalance(address);
|
||||
console.log("--", networkName, "-- 📡");
|
||||
console.log(" balance:", +ethers.formatEther(balance));
|
||||
console.log(" nonce:", +(await provider.getTransactionCount(address)));
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
} catch (e) {
|
||||
console.log("Can't connect to network", networkName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(error => {
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
31
packages/hardhat/scripts/revealPK.ts
Normal file
31
packages/hardhat/scripts/revealPK.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import * as dotenv from "dotenv";
|
||||
dotenv.config();
|
||||
import { Wallet } from "ethers";
|
||||
import password from "@inquirer/password";
|
||||
|
||||
async function main() {
|
||||
const encryptedKey = process.env.DEPLOYER_PRIVATE_KEY_ENCRYPTED;
|
||||
|
||||
if (!encryptedKey) {
|
||||
console.log("🚫️ You don't have a deployer account. Run `yarn generate` or `yarn account:import` first");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("👀 This will reveal your private key on the console.\n");
|
||||
|
||||
const pass = await password({ message: "Enter your password to decrypt the private key:" });
|
||||
let wallet: Wallet;
|
||||
try {
|
||||
wallet = (await Wallet.fromEncryptedJson(encryptedKey, pass)) as Wallet;
|
||||
} catch {
|
||||
console.log("❌ Failed to decrypt private key. Wrong password?");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("\n🔑 Private key:", wallet.privateKey);
|
||||
}
|
||||
|
||||
main().catch(error => {
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
58
packages/hardhat/scripts/runHardhatDeployWithPK.ts
Normal file
58
packages/hardhat/scripts/runHardhatDeployWithPK.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import * as dotenv from "dotenv";
|
||||
dotenv.config();
|
||||
import { Wallet } from "ethers";
|
||||
import password from "@inquirer/password";
|
||||
import { spawn } from "child_process";
|
||||
import { config } from "hardhat";
|
||||
|
||||
/**
|
||||
* Unencrypts the private key and runs the hardhat deploy command
|
||||
*/
|
||||
async function main() {
|
||||
const networkIndex = process.argv.indexOf("--network");
|
||||
const networkName = networkIndex !== -1 ? process.argv[networkIndex + 1] : config.defaultNetwork;
|
||||
|
||||
if (networkName === "localhost" || networkName === "hardhat") {
|
||||
// Deploy command on the localhost network
|
||||
const hardhat = spawn("hardhat", ["deploy", ...process.argv.slice(2)], {
|
||||
stdio: "inherit",
|
||||
env: process.env,
|
||||
shell: process.platform === "win32",
|
||||
});
|
||||
|
||||
hardhat.on("exit", code => {
|
||||
process.exit(code || 0);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const encryptedKey = process.env.DEPLOYER_PRIVATE_KEY_ENCRYPTED;
|
||||
|
||||
if (!encryptedKey) {
|
||||
console.log("🚫️ You don't have a deployer account. Run `yarn generate` or `yarn account:import` first");
|
||||
return;
|
||||
}
|
||||
|
||||
const pass = await password({ message: "Enter password to decrypt private key:" });
|
||||
|
||||
try {
|
||||
const wallet = await Wallet.fromEncryptedJson(encryptedKey, pass);
|
||||
process.env.__RUNTIME_DEPLOYER_PRIVATE_KEY = wallet.privateKey;
|
||||
|
||||
const hardhat = spawn("hardhat", ["deploy", ...process.argv.slice(2)], {
|
||||
stdio: "inherit",
|
||||
env: process.env,
|
||||
shell: process.platform === "win32",
|
||||
});
|
||||
|
||||
hardhat.on("exit", code => {
|
||||
process.exit(code || 0);
|
||||
});
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
} catch (e) {
|
||||
console.error("Failed to decrypt private key. Wrong password?");
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
2
packages/hardhat/test/.gitkeep
Normal file
2
packages/hardhat/test/.gitkeep
Normal file
@@ -0,0 +1,2 @@
|
||||
# Write tests for your smart contract in this directory
|
||||
# Example: YourContract.ts
|
||||
386
packages/hardhat/test/DEX.ts
Normal file
386
packages/hardhat/test/DEX.ts
Normal file
@@ -0,0 +1,386 @@
|
||||
import { ethers } from "hardhat";
|
||||
import { expect } from "chai";
|
||||
import { ContractTransactionReceipt } from "ethers";
|
||||
import { anyValue } from "@nomicfoundation/hardhat-chai-matchers/withArgs";
|
||||
import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers";
|
||||
import { Balloons, DEX } from "../typechain-types";
|
||||
|
||||
describe("🚩 Challenge: ⚖️ 🪙 DEX", () => {
|
||||
// this.timeout(45000);
|
||||
|
||||
let dexContract: DEX;
|
||||
let balloonsContract: Balloons;
|
||||
let deployer: HardhatEthersSigner;
|
||||
let user2: HardhatEthersSigner;
|
||||
let user3: HardhatEthersSigner;
|
||||
|
||||
const contractAddress = process.env.CONTRACT_ADDRESS;
|
||||
let contractArtifact: string;
|
||||
if (contractAddress) {
|
||||
contractArtifact = `contracts/download-${contractAddress}.sol:DEX`;
|
||||
} else {
|
||||
contractArtifact = "contracts/DEX.sol:DEX";
|
||||
}
|
||||
|
||||
async function getEventValue(txReceipt: ContractTransactionReceipt | null, eventNumber: number) {
|
||||
if (!txReceipt) return;
|
||||
const dexContractAddress = await dexContract.getAddress();
|
||||
const log = txReceipt.logs.find(log => log.address === dexContractAddress);
|
||||
const logDescr = log && dexContract.interface.parseLog(log);
|
||||
const args = logDescr?.args;
|
||||
return args && args[eventNumber]; // index of ethAmount in event
|
||||
}
|
||||
|
||||
async function deployNewInstance() {
|
||||
before("Deploying fresh contracts", async function () {
|
||||
console.log("\t", " 🛫 Deploying new contracts...");
|
||||
|
||||
const BalloonsContract = await ethers.getContractFactory("Balloons");
|
||||
balloonsContract = await BalloonsContract.deploy();
|
||||
const balloonsAddress = await balloonsContract.getAddress();
|
||||
|
||||
const DexContract = await ethers.getContractFactory(contractArtifact);
|
||||
dexContract = (await DexContract.deploy(balloonsAddress)) as DEX;
|
||||
const dexContractAddress = await dexContract.getAddress();
|
||||
|
||||
await balloonsContract.approve(dexContractAddress, ethers.parseEther("100"));
|
||||
|
||||
await dexContract.init(ethers.parseEther("5"), {
|
||||
value: ethers.parseEther("5"),
|
||||
gasLimit: 200000,
|
||||
});
|
||||
|
||||
[deployer, user2, user3] = await ethers.getSigners();
|
||||
|
||||
await balloonsContract.transfer(user2.address, ethers.parseEther("10"));
|
||||
await balloonsContract.transfer(user3.address, ethers.parseEther("10"));
|
||||
});
|
||||
}
|
||||
|
||||
// quick fix to let gas reporter fetch data from gas station & coinmarketcap
|
||||
before(done => {
|
||||
setTimeout(done, 2000);
|
||||
});
|
||||
|
||||
// --------------------- START OF CHECKPOINT 2 ---------------------
|
||||
describe("Checkpoint 2: Reserves", function () {
|
||||
describe("Deploying the contracts and testing the init function", () => {
|
||||
it("Should deploy contracts", async function () {
|
||||
const BalloonsContract = await ethers.getContractFactory("Balloons");
|
||||
balloonsContract = await BalloonsContract.deploy();
|
||||
const balloonsAddress = await balloonsContract.getAddress();
|
||||
|
||||
const DexContract = await ethers.getContractFactory(contractArtifact);
|
||||
dexContract = (await DexContract.deploy(balloonsAddress)) as DEX;
|
||||
const dexContractAddress = await dexContract.getAddress();
|
||||
|
||||
await balloonsContract.approve(dexContractAddress, ethers.parseEther("100"));
|
||||
|
||||
const lpBefore = await dexContract.totalLiquidity();
|
||||
console.log(
|
||||
"\t",
|
||||
" 💦 Expecting total liquidity to be 0 before initializing. Total liquidity:",
|
||||
ethers.formatEther(lpBefore),
|
||||
);
|
||||
expect(lpBefore).to.equal(0);
|
||||
console.log("\t", " 🔰 Calling init with 5 Eth");
|
||||
await dexContract.init(ethers.parseEther("5"), {
|
||||
value: ethers.parseEther("5"),
|
||||
gasLimit: 200000,
|
||||
});
|
||||
const lpAfter = await dexContract.totalLiquidity();
|
||||
console.log("\t", " 💦 Expecting new total liquidity to be 5. Total liquidity:", ethers.formatEther(lpAfter));
|
||||
});
|
||||
});
|
||||
});
|
||||
// ----------------- END OF CHECKPOINT 2 -----------------
|
||||
// ----------------- START OF CHECKPOINT 3 -----------------
|
||||
describe("Checkpoint 3: Price", async () => {
|
||||
describe("price()", async () => {
|
||||
// https://etherscan.io/address/0x7a250d5630b4cf539739df2c5dacb4c659f2488d#readContract
|
||||
// in Uniswap the fee is build in getAmountOut() function
|
||||
it("Should calculate the price correctly", async function () {
|
||||
let xInput = ethers.parseEther("1");
|
||||
let xReserves = ethers.parseEther("5");
|
||||
let yReserves = ethers.parseEther("5");
|
||||
let yOutput = await dexContract.price(xInput, xReserves, yReserves);
|
||||
expect(
|
||||
yOutput.toString(),
|
||||
"Check your price function's calculations. Don't forget the 3% fee for liquidity providers, and the function should be view or pure.",
|
||||
).to.equal("831248957812239453");
|
||||
xInput = ethers.parseEther("1");
|
||||
xReserves = ethers.parseEther("10");
|
||||
yReserves = ethers.parseEther("15");
|
||||
yOutput = await dexContract.price(xInput, xReserves, yReserves);
|
||||
expect(yOutput.toString()).to.equal("1359916340820223697");
|
||||
});
|
||||
});
|
||||
});
|
||||
// ----------------- END OF CHECKPOINT 3 -----------------
|
||||
// ----------------- START OF CHECKPOINT 4 -----------------
|
||||
describe("Checkpoint 4: Trading", function () {
|
||||
deployNewInstance();
|
||||
describe("ethToToken()", function () {
|
||||
it("Should be able to send 1 Ether to DEX in exchange for _ $BAL", async function () {
|
||||
const dexContractAddress = await dexContract.getAddress();
|
||||
|
||||
const dex_eth_start = await ethers.provider.getBalance(dexContractAddress);
|
||||
console.log("\t", " 💵 Dex contract's initial Eth balance:", ethers.formatEther(dex_eth_start));
|
||||
console.log("\t", " 📞 Calling ethToToken with a value of 1 Eth...");
|
||||
const tx1 = await dexContract.ethToToken({
|
||||
value: ethers.parseEther("1"),
|
||||
});
|
||||
|
||||
expect(tx1, "ethToToken should revert before initalization").not.to.be.reverted;
|
||||
|
||||
console.log("\t", " 🔰 Initializing...");
|
||||
const tx1_receipt = await tx1.wait();
|
||||
const ethSent_1 = await getEventValue(tx1_receipt, 2);
|
||||
|
||||
console.log("\t", " 🔼 Expecting the Eth value emitted to be 1. Value:", ethers.formatEther(ethSent_1));
|
||||
expect(ethSent_1, "Check you are emitting the correct Eth value and in the correct order").to.equal(
|
||||
ethers.parseEther("1"),
|
||||
);
|
||||
|
||||
const dex_eth_after = await ethers.provider.getBalance(dexContractAddress);
|
||||
console.log("\t", " 💵 Dex contract's new Eth balance:", ethers.formatEther(dex_eth_after));
|
||||
|
||||
console.log("\t", " 💵 Expecting final Dex balance to have increased by 1...");
|
||||
expect(await ethers.provider.getBalance(dexContractAddress)).to.equal(ethers.parseEther("6"));
|
||||
});
|
||||
|
||||
it("Should revert if 0 ETH sent", async function () {
|
||||
await expect(
|
||||
dexContract.ethToToken({ value: ethers.parseEther("0") }),
|
||||
"ethToToken should revert when sending 0 value...",
|
||||
).to.be.reverted;
|
||||
});
|
||||
|
||||
it("Should send less tokens after the first trade (ethToToken called)", async function () {
|
||||
const user2BalBefore = await balloonsContract.balanceOf(user2.address);
|
||||
console.log("\t", " 💵 User2 initial $BAL balance:", ethers.formatEther(user2BalBefore));
|
||||
console.log("\t", " 🥈 User2 calling ethToToken with value of 1 ETH...");
|
||||
await dexContract.connect(user2).ethToToken({ value: ethers.parseEther("1") });
|
||||
|
||||
const user2BalAfter = await balloonsContract.balanceOf(user2.address);
|
||||
console.log("\t", " 💵 User2 new $BAL balance:", ethers.formatEther(user2BalAfter));
|
||||
|
||||
const user3BalBefore = await balloonsContract.balanceOf(user3.address);
|
||||
console.log("\t", " 💵 User3 initial $BAL balance:", ethers.formatEther(user3BalBefore));
|
||||
console.log("\t", " 🥉 User3 calling ethToToken with value of 1 ETH...");
|
||||
await dexContract.connect(user3).ethToToken({ value: ethers.parseEther("1") });
|
||||
|
||||
const user3BalAfter = await balloonsContract.balanceOf(user3.address);
|
||||
console.log("\t", " 💵 User3 new $BAL balance:", ethers.formatEther(user3BalAfter));
|
||||
|
||||
console.log("\t", " 💵 Expecting User2 to have aquired more $BAL than User3...");
|
||||
expect(user2BalAfter).to.greaterThan(user3BalAfter);
|
||||
});
|
||||
|
||||
it("Should emit an event when ethToToken() called", async function () {
|
||||
await expect(
|
||||
dexContract.ethToToken({ value: ethers.parseEther("1") }),
|
||||
"Make sure you're emitting the EthToTokenSwap event correctly",
|
||||
).to.emit(dexContract, "EthToTokenSwap");
|
||||
});
|
||||
|
||||
it("Should transfer tokens to purchaser after trade", async function () {
|
||||
const user3_token_before = await balloonsContract.balanceOf(user3.address);
|
||||
console.log("\t", " 💵 User3 initial $BAL balance:", ethers.formatEther(user3_token_before));
|
||||
|
||||
console.log("\t", " 🥉 User3 calling ethToToken with value of 1 ETH...");
|
||||
const tx1 = await dexContract.connect(user3).ethToToken({
|
||||
value: ethers.parseEther("1"),
|
||||
});
|
||||
await tx1.wait();
|
||||
|
||||
const user3_token_after = await balloonsContract.balanceOf(user3.address);
|
||||
console.log("\t", " 💵 User3 new $BAL balance:", ethers.formatEther(user3_token_after));
|
||||
|
||||
const tokenDifferece = user3_token_after - user3_token_before;
|
||||
console.log("\t", " 🥉 Expecting user3's $BAL balance to increase by the correct amount...");
|
||||
expect(tokenDifferece).to.be.equal("277481486896167099");
|
||||
});
|
||||
// could insert more tests to show the declining price, and what happens when the pool becomes very imbalanced.
|
||||
});
|
||||
|
||||
describe("tokenToEth()", async () => {
|
||||
it("Should send 1 $BAL to DEX in exchange for _ $ETH", async function () {
|
||||
const dexContractAddress = await dexContract.getAddress();
|
||||
const balloons_bal_start = await balloonsContract.balanceOf(dexContractAddress);
|
||||
console.log("\t", " 💵 Initial Ballons $BAL balance:", ethers.formatEther(balloons_bal_start));
|
||||
const dex_eth_start = await ethers.provider.getBalance(dexContractAddress);
|
||||
console.log("\t", " 💵 Initial DEX Eth balance:", ethers.formatEther(dex_eth_start));
|
||||
|
||||
console.log("\t", " 📞 Calling tokenToEth with 1 Eth...");
|
||||
const tx1 = await dexContract.tokenToEth(ethers.parseEther("1"));
|
||||
|
||||
const balloons_bal_end = await balloonsContract.balanceOf(dexContractAddress);
|
||||
console.log("\t", " 💵 Final Ballons $BAL balance:", ethers.formatEther(balloons_bal_end));
|
||||
const dex_eth_end = await ethers.provider.getBalance(dexContractAddress);
|
||||
console.log("\t", " 💵 Final DEX Eth balance:", ethers.formatEther(dex_eth_end));
|
||||
|
||||
await expect(tx1).not.to.be.revertedWith("Contract not initialized");
|
||||
// Checks that the balance of the DEX contract has decreased by 1 $BAL
|
||||
console.log("\t", " 🎈 Expecting the $BAL balance of the Ballon contract to have increased by 1...");
|
||||
expect(await balloonsContract.balanceOf(dexContractAddress)).to.equal(
|
||||
balloons_bal_start + ethers.parseEther("1"),
|
||||
);
|
||||
// Checks that the balance of the DEX contract has increased
|
||||
console.log("\t", " ⚖️ Expecting the balance of the Dex contract to have decreased...");
|
||||
expect(await ethers.provider.getBalance(dexContractAddress)).to.lessThan(dex_eth_start);
|
||||
});
|
||||
|
||||
it("Should revert if 0 tokens sent to the DEX", async function () {
|
||||
await expect(dexContract.tokenToEth(ethers.parseEther("0"))).to.be.reverted;
|
||||
});
|
||||
|
||||
it("Should emit event TokenToEthSwap when tokenToEth() called", async function () {
|
||||
await expect(
|
||||
dexContract.tokenToEth(ethers.parseEther("1")),
|
||||
"Make sure you're emitting the TokenToEthSwap event correctly",
|
||||
).to.emit(dexContract, "TokenToEthSwap");
|
||||
});
|
||||
|
||||
it("Should send less eth after the first trade (tokenToEth() called)", async function () {
|
||||
const dexContractAddress = await dexContract.getAddress();
|
||||
const dex_eth_start = await ethers.provider.getBalance(dexContractAddress);
|
||||
console.log("\t", " 💵 Initial Dex balance:", ethers.formatEther(dex_eth_start));
|
||||
console.log("\t", " 📞 Calling tokenToEth with 1 Eth...");
|
||||
const tx1 = await dexContract.tokenToEth(ethers.parseEther("1"));
|
||||
await tx1.wait();
|
||||
|
||||
const dex_eth_next = await ethers.provider.getBalance(dexContractAddress);
|
||||
const tx1difference = dex_eth_next - dex_eth_start;
|
||||
console.log(
|
||||
"\t",
|
||||
" 💵 Next Dex balance:",
|
||||
ethers.formatEther(dex_eth_next),
|
||||
" Eth sent:",
|
||||
ethers.formatEther(tx1difference * -1n),
|
||||
);
|
||||
|
||||
console.log("\t", " 📞 Calling tokenToEth with 1 Eth...");
|
||||
const tx2 = await dexContract.tokenToEth(ethers.parseEther("1"));
|
||||
await tx2.wait();
|
||||
|
||||
const dex_eth_end = await ethers.provider.getBalance(dexContractAddress);
|
||||
const tx2difference = dex_eth_end - dex_eth_next;
|
||||
console.log(
|
||||
"\t",
|
||||
" 💵 Final Dex balance:",
|
||||
ethers.formatEther(dex_eth_end),
|
||||
" Eth sent:",
|
||||
ethers.formatEther(tx2difference * -1n),
|
||||
);
|
||||
|
||||
console.log("\t", " ➖ Expecting the first call to get more Eth than the second...");
|
||||
expect(tx2difference).to.greaterThan(tx1difference);
|
||||
});
|
||||
});
|
||||
});
|
||||
// ----------------- END OF CHECKPOINT 4 -----------------
|
||||
// ----------------- START OF CHECKPOINT 5 -----------------
|
||||
describe("Checkpoint 5: Liquidity", async () => {
|
||||
describe("deposit()", async () => {
|
||||
deployNewInstance();
|
||||
it("Should increase liquidity in the pool when ETH is deposited", async function () {
|
||||
const dexContractAddress = await dexContract.getAddress();
|
||||
console.log("\t", " 💵 Approving 100 ETH...");
|
||||
await balloonsContract.connect(user2).approve(dexContractAddress, ethers.parseEther("100"));
|
||||
const liquidity_start = await dexContract.totalLiquidity();
|
||||
console.log("\t", " ⚖️ Starting Dex liquidity", ethers.formatEther(liquidity_start));
|
||||
const user2liquidity = await dexContract.getLiquidity(user2.address);
|
||||
console.log("\t", " ⚖️ Expecting user's liquidity to be 0. Liquidity:", ethers.formatEther(user2liquidity));
|
||||
expect(user2liquidity).to.equal("0");
|
||||
console.log("\t", " 🔼 Expecting the deposit function to emit correctly...");
|
||||
await expect(
|
||||
dexContract.connect(user2).deposit((ethers.parseEther("5"), { value: ethers.parseEther("5") })),
|
||||
"Check the order of the values when emitting LiquidityProvided: msg.sender, liquidityMinted, msg.value, tokenDeposit",
|
||||
)
|
||||
.to.emit(dexContract, "LiquidityProvided")
|
||||
.withArgs(anyValue, ethers.parseEther("5"), ethers.parseEther("5"), anyValue);
|
||||
const liquidity_end = await dexContract.totalLiquidity();
|
||||
console.log(
|
||||
"\t",
|
||||
" 💸 Final liquidity should increase by 5. Final liquidity:",
|
||||
ethers.formatEther(liquidity_end),
|
||||
);
|
||||
expect(liquidity_end, "Total liquidity should increase").to.equal(liquidity_start + ethers.parseEther("5"));
|
||||
const user_lp = await dexContract.getLiquidity(user2.address);
|
||||
console.log("\t", " ⚖️ User's liquidity provided should be 5. LP:", ethers.formatEther(user_lp));
|
||||
expect(user_lp.toString(), "User's liquidity provided should be 5.").to.equal(ethers.parseEther("5"));
|
||||
});
|
||||
|
||||
it("Should revert if 0 ETH deposited", async function () {
|
||||
await expect(
|
||||
dexContract.deposit((ethers.parseEther("0"), { value: ethers.parseEther("0") })),
|
||||
"Should revert if 0 value is sent",
|
||||
).to.be.reverted;
|
||||
});
|
||||
});
|
||||
// pool should have 5:5 ETH:$BAL ratio
|
||||
describe("withdraw()", async () => {
|
||||
deployNewInstance();
|
||||
it("Should withdraw 1 ETH and 1 $BAL when pool is at a 1:1 ratio", async function () {
|
||||
const startingLiquidity = await dexContract.totalLiquidity();
|
||||
console.log("\t", " ⚖️ Starting liquidity:", ethers.formatEther(startingLiquidity));
|
||||
const userBallonsBalance = await balloonsContract.balanceOf(deployer.address);
|
||||
console.log("\t", " 💵 User's starting $BAL balance:", ethers.formatEther(userBallonsBalance));
|
||||
|
||||
console.log("\t", " 🔽 Calling withdraw with with a value of 1 Eth...");
|
||||
const tx1 = await dexContract.withdraw(ethers.parseEther("1"));
|
||||
const userBallonsBalanceAfter = await balloonsContract.balanceOf(deployer.address);
|
||||
const tx1_receipt = await tx1.wait();
|
||||
|
||||
const eth_out = await getEventValue(tx1_receipt, 2);
|
||||
const token_out = await getEventValue(tx1_receipt, 3);
|
||||
|
||||
console.log("\t", " 💵 User's new $BAL balance:", ethers.formatEther(userBallonsBalanceAfter));
|
||||
console.log(
|
||||
"\t",
|
||||
" 🎈 Expecting the balance to have increased by 1",
|
||||
ethers.formatEther(userBallonsBalanceAfter),
|
||||
);
|
||||
expect(userBallonsBalance, "User $BAL balance shoud increase").to.equal(
|
||||
userBallonsBalanceAfter - ethers.parseEther("1"),
|
||||
);
|
||||
|
||||
console.log("\t", " 🔽 Expecting the Eth withdrawn emit to be 1...");
|
||||
expect(eth_out, "ethWithdrawn incorrect in emit").to.be.equal(ethers.parseEther("1"));
|
||||
console.log("\t", " 🔽 Expecting the $BAL withdrawn emit to be 1...");
|
||||
expect(token_out, "tokenAmount incorrect in emit").to.be.equal(ethers.parseEther("1"));
|
||||
});
|
||||
|
||||
it("Should revert if sender does not have enough liqudity", async function () {
|
||||
console.log("\t", " ✋ Expecting withdraw to revert when there is not enough liquidity...");
|
||||
await expect(dexContract.withdraw(ethers.parseEther("100"))).to.be.reverted;
|
||||
});
|
||||
|
||||
it("Should decrease total liquidity", async function () {
|
||||
const totalLpBefore = await dexContract.totalLiquidity();
|
||||
console.log("\t", " ⚖️ Initial liquidity:", ethers.formatEther(totalLpBefore));
|
||||
console.log("\t", " 📞 Calling withdraw with 1 Eth...");
|
||||
const txWithdraw = await dexContract.withdraw(ethers.parseEther("1"));
|
||||
const totalLpAfter = await dexContract.totalLiquidity();
|
||||
console.log("\t", " ⚖️ Final liquidity:", ethers.formatEther(totalLpAfter));
|
||||
const txWithdraw_receipt = await txWithdraw.wait();
|
||||
const liquidityBurned = await getEventValue(txWithdraw_receipt, 2);
|
||||
console.log("\t", " 🔼 Emitted liquidity removed:", ethers.formatEther(liquidityBurned));
|
||||
|
||||
expect(totalLpAfter, "Emitted incorrect liquidity amount burned").to.be.equal(totalLpBefore - liquidityBurned);
|
||||
expect(totalLpBefore, "Emitted total liquidity should decrease").to.be.above(totalLpAfter);
|
||||
});
|
||||
|
||||
it("Should emit event LiquidityWithdrawn when withdraw() called", async function () {
|
||||
await expect(
|
||||
dexContract.withdraw(ethers.parseEther("1.5")),
|
||||
"Make sure you emit the LiquidityRemoved event correctly",
|
||||
)
|
||||
.to.emit(dexContract, "LiquidityRemoved")
|
||||
.withArgs(deployer.address, ethers.parseEther("1.5"), ethers.parseEther("1.5"), ethers.parseEther("1.5"));
|
||||
});
|
||||
});
|
||||
});
|
||||
// ----------------- END OF CHECKPOINT 5 -----------------
|
||||
});
|
||||
11
packages/hardhat/tsconfig.json
Normal file
11
packages/hardhat/tsconfig.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2020",
|
||||
"module": "commonjs",
|
||||
"esModuleInterop": true,
|
||||
"resolveJsonModule": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true
|
||||
}
|
||||
}
|
||||
14
packages/nextjs/.env.example
Normal file
14
packages/nextjs/.env.example
Normal file
@@ -0,0 +1,14 @@
|
||||
# Template for NextJS environment variables.
|
||||
|
||||
# For local development, copy this file, rename it to .env.local, and fill in the values.
|
||||
# When deploying live, you'll need to store the vars in Vercel/System config.
|
||||
|
||||
# If not set, we provide default values (check `scaffold.config.ts`) so developers can start prototyping out of the box,
|
||||
# but we recommend getting your own API Keys for Production Apps.
|
||||
|
||||
# To access the values stored in this env file you can use: process.env.VARIABLENAME
|
||||
# You'll need to prefix the variables names with NEXT_PUBLIC_ if you want to access them on the client side.
|
||||
# More info: https://nextjs.org/docs/pages/building-your-application/configuring/environment-variables
|
||||
NEXT_PUBLIC_ALCHEMY_API_KEY=
|
||||
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=
|
||||
|
||||
38
packages/nextjs/.gitignore
vendored
Normal file
38
packages/nextjs/.gitignore
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
.vercel
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# local env files
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
ipfs-upload.config.json
|
||||
9
packages/nextjs/.prettierrc.js
Normal file
9
packages/nextjs/.prettierrc.js
Normal file
@@ -0,0 +1,9 @@
|
||||
module.exports = {
|
||||
arrowParens: "avoid",
|
||||
printWidth: 120,
|
||||
tabWidth: 2,
|
||||
trailingComma: "all",
|
||||
importOrder: ["^react$", "^next/(.*)$", "<THIRD_PARTY_MODULES>", "^@heroicons/(.*)$", "^~~/(.*)$"],
|
||||
importOrderSortSpecifiers: true,
|
||||
plugins: [require.resolve("@trivago/prettier-plugin-sort-imports")],
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
type AddressCodeTabProps = {
|
||||
bytecode: string;
|
||||
assembly: string;
|
||||
};
|
||||
|
||||
export const AddressCodeTab = ({ bytecode, assembly }: AddressCodeTabProps) => {
|
||||
const formattedAssembly = Array.from(assembly.matchAll(/\w+( 0x[a-fA-F0-9]+)?/g))
|
||||
.map(it => it[0])
|
||||
.join("\n");
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3 p-4">
|
||||
Bytecode
|
||||
<div className="mockup-code -indent-5 overflow-y-auto max-h-[500px]">
|
||||
<pre className="px-5">
|
||||
<code className="whitespace-pre-wrap overflow-auto break-words">{bytecode}</code>
|
||||
</pre>
|
||||
</div>
|
||||
Opcodes
|
||||
<div className="mockup-code -indent-5 overflow-y-auto max-h-[500px]">
|
||||
<pre className="px-5">
|
||||
<code>{formattedAssembly}</code>
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
import { BackButton } from "./BackButton";
|
||||
import { ContractTabs } from "./ContractTabs";
|
||||
import { Address as AddressType } from "viem";
|
||||
import { Address, Balance } from "~~/components/scaffold-eth";
|
||||
|
||||
export const AddressComponent = ({
|
||||
address,
|
||||
contractData,
|
||||
}: {
|
||||
address: AddressType;
|
||||
contractData: { bytecode: string; assembly: string } | null;
|
||||
}) => {
|
||||
return (
|
||||
<div className="m-10 mb-20">
|
||||
<div className="flex justify-start mb-5">
|
||||
<BackButton />
|
||||
</div>
|
||||
<div className="col-span-5 grid grid-cols-1 lg:grid-cols-2 gap-8 lg:gap-10">
|
||||
<div className="col-span-1 flex flex-col">
|
||||
<div className="bg-base-100 border-base-300 border shadow-md shadow-secondary rounded-3xl px-6 lg:px-8 mb-6 space-y-1 py-4 overflow-x-auto">
|
||||
<div className="flex">
|
||||
<div className="flex flex-col gap-1">
|
||||
<Address address={address} format="long" onlyEnsOrAddress />
|
||||
<div className="flex gap-1 items-center">
|
||||
<span className="font-bold text-sm">Balance:</span>
|
||||
<Balance address={address} className="text" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ContractTabs address={address} contractData={contractData} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Address } from "viem";
|
||||
import { useContractLogs } from "~~/hooks/scaffold-eth";
|
||||
import { replacer } from "~~/utils/scaffold-eth/common";
|
||||
|
||||
export const AddressLogsTab = ({ address }: { address: Address }) => {
|
||||
const contractLogs = useContractLogs(address);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3 p-4">
|
||||
<div className="mockup-code overflow-auto max-h-[500px]">
|
||||
<pre className="px-5 whitespace-pre-wrap break-words">
|
||||
{contractLogs.map((log, i) => (
|
||||
<div key={i}>
|
||||
<strong>Log:</strong> {JSON.stringify(log, replacer, 2)}
|
||||
</div>
|
||||
))}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { Address, createPublicClient, http, toHex } from "viem";
|
||||
import { hardhat } from "viem/chains";
|
||||
|
||||
const publicClient = createPublicClient({
|
||||
chain: hardhat,
|
||||
transport: http(),
|
||||
});
|
||||
|
||||
export const AddressStorageTab = ({ address }: { address: Address }) => {
|
||||
const [storage, setStorage] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchStorage = async () => {
|
||||
try {
|
||||
const storageData = [];
|
||||
let idx = 0;
|
||||
|
||||
while (true) {
|
||||
const storageAtPosition = await publicClient.getStorageAt({
|
||||
address: address,
|
||||
slot: toHex(idx),
|
||||
});
|
||||
|
||||
if (storageAtPosition === "0x" + "0".repeat(64)) break;
|
||||
|
||||
if (storageAtPosition) {
|
||||
storageData.push(storageAtPosition);
|
||||
}
|
||||
|
||||
idx++;
|
||||
}
|
||||
setStorage(storageData);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch storage:", error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchStorage();
|
||||
}, [address]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3 p-4">
|
||||
{storage.length > 0 ? (
|
||||
<div className="mockup-code overflow-auto max-h-[500px]">
|
||||
<pre className="px-5 whitespace-pre-wrap break-words">
|
||||
{storage.map((data, i) => (
|
||||
<div key={i}>
|
||||
<strong>Storage Slot {i}:</strong> {data}
|
||||
</div>
|
||||
))}
|
||||
</pre>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-lg">This contract does not have any variables.</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
12
packages/nextjs/app/blockexplorer/_components/BackButton.tsx
Normal file
12
packages/nextjs/app/blockexplorer/_components/BackButton.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
export const BackButton = () => {
|
||||
const router = useRouter();
|
||||
return (
|
||||
<button className="btn btn-sm btn-primary" onClick={() => router.back()}>
|
||||
Back
|
||||
</button>
|
||||
);
|
||||
};
|
||||
102
packages/nextjs/app/blockexplorer/_components/ContractTabs.tsx
Normal file
102
packages/nextjs/app/blockexplorer/_components/ContractTabs.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { AddressCodeTab } from "./AddressCodeTab";
|
||||
import { AddressLogsTab } from "./AddressLogsTab";
|
||||
import { AddressStorageTab } from "./AddressStorageTab";
|
||||
import { PaginationButton } from "./PaginationButton";
|
||||
import { TransactionsTable } from "./TransactionsTable";
|
||||
import { Address, createPublicClient, http } from "viem";
|
||||
import { hardhat } from "viem/chains";
|
||||
import { useFetchBlocks } from "~~/hooks/scaffold-eth";
|
||||
|
||||
type AddressCodeTabProps = {
|
||||
bytecode: string;
|
||||
assembly: string;
|
||||
};
|
||||
|
||||
type PageProps = {
|
||||
address: Address;
|
||||
contractData: AddressCodeTabProps | null;
|
||||
};
|
||||
|
||||
const publicClient = createPublicClient({
|
||||
chain: hardhat,
|
||||
transport: http(),
|
||||
});
|
||||
|
||||
export const ContractTabs = ({ address, contractData }: PageProps) => {
|
||||
const { blocks, transactionReceipts, currentPage, totalBlocks, setCurrentPage } = useFetchBlocks();
|
||||
const [activeTab, setActiveTab] = useState("transactions");
|
||||
const [isContract, setIsContract] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const checkIsContract = async () => {
|
||||
const contractCode = await publicClient.getBytecode({ address: address });
|
||||
setIsContract(contractCode !== undefined && contractCode !== "0x");
|
||||
};
|
||||
|
||||
checkIsContract();
|
||||
}, [address]);
|
||||
|
||||
const filteredBlocks = blocks.filter(block =>
|
||||
block.transactions.some(tx => {
|
||||
if (typeof tx === "string") {
|
||||
return false;
|
||||
}
|
||||
return tx.from.toLowerCase() === address.toLowerCase() || tx.to?.toLowerCase() === address.toLowerCase();
|
||||
}),
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{isContract && (
|
||||
<div role="tablist" className="tabs tabs-lift">
|
||||
<button
|
||||
role="tab"
|
||||
className={`tab ${activeTab === "transactions" ? "tab-active" : ""}`}
|
||||
onClick={() => setActiveTab("transactions")}
|
||||
>
|
||||
Transactions
|
||||
</button>
|
||||
<button
|
||||
role="tab"
|
||||
className={`tab ${activeTab === "code" ? "tab-active" : ""}`}
|
||||
onClick={() => setActiveTab("code")}
|
||||
>
|
||||
Code
|
||||
</button>
|
||||
<button
|
||||
role="tab"
|
||||
className={`tab ${activeTab === "storage" ? "tab-active" : ""}`}
|
||||
onClick={() => setActiveTab("storage")}
|
||||
>
|
||||
Storage
|
||||
</button>
|
||||
<button
|
||||
role="tab"
|
||||
className={`tab ${activeTab === "logs" ? "tab-active" : ""}`}
|
||||
onClick={() => setActiveTab("logs")}
|
||||
>
|
||||
Logs
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{activeTab === "transactions" && (
|
||||
<div className="pt-4">
|
||||
<TransactionsTable blocks={filteredBlocks} transactionReceipts={transactionReceipts} />
|
||||
<PaginationButton
|
||||
currentPage={currentPage}
|
||||
totalItems={Number(totalBlocks)}
|
||||
setCurrentPage={setCurrentPage}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{activeTab === "code" && contractData && (
|
||||
<AddressCodeTab bytecode={contractData.bytecode} assembly={contractData.assembly} />
|
||||
)}
|
||||
{activeTab === "storage" && <AddressStorageTab address={address} />}
|
||||
{activeTab === "logs" && <AddressLogsTab address={address} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
import { ArrowLeftIcon, ArrowRightIcon } from "@heroicons/react/24/outline";
|
||||
|
||||
type PaginationButtonProps = {
|
||||
currentPage: number;
|
||||
totalItems: number;
|
||||
setCurrentPage: (page: number) => void;
|
||||
};
|
||||
|
||||
const ITEMS_PER_PAGE = 20;
|
||||
|
||||
export const PaginationButton = ({ currentPage, totalItems, setCurrentPage }: PaginationButtonProps) => {
|
||||
const isPrevButtonDisabled = currentPage === 0;
|
||||
const isNextButtonDisabled = currentPage + 1 >= Math.ceil(totalItems / ITEMS_PER_PAGE);
|
||||
|
||||
const prevButtonClass = isPrevButtonDisabled ? "btn-disabled cursor-default" : "btn-primary";
|
||||
const nextButtonClass = isNextButtonDisabled ? "btn-disabled cursor-default" : "btn-primary";
|
||||
|
||||
if (isNextButtonDisabled && isPrevButtonDisabled) return null;
|
||||
|
||||
return (
|
||||
<div className="mt-5 justify-end flex gap-3 mx-5">
|
||||
<button
|
||||
className={`btn btn-sm ${prevButtonClass}`}
|
||||
disabled={isPrevButtonDisabled}
|
||||
onClick={() => setCurrentPage(currentPage - 1)}
|
||||
>
|
||||
<ArrowLeftIcon className="h-4 w-4" />
|
||||
</button>
|
||||
<span className="self-center text-primary-content font-medium">Page {currentPage + 1}</span>
|
||||
<button
|
||||
className={`btn btn-sm ${nextButtonClass}`}
|
||||
disabled={isNextButtonDisabled}
|
||||
onClick={() => setCurrentPage(currentPage + 1)}
|
||||
>
|
||||
<ArrowRightIcon className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
49
packages/nextjs/app/blockexplorer/_components/SearchBar.tsx
Normal file
49
packages/nextjs/app/blockexplorer/_components/SearchBar.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { isAddress, isHex } from "viem";
|
||||
import { hardhat } from "viem/chains";
|
||||
import { usePublicClient } from "wagmi";
|
||||
|
||||
export const SearchBar = () => {
|
||||
const [searchInput, setSearchInput] = useState("");
|
||||
const router = useRouter();
|
||||
|
||||
const client = usePublicClient({ chainId: hardhat.id });
|
||||
|
||||
const handleSearch = async (event: React.FormEvent) => {
|
||||
event.preventDefault();
|
||||
if (isHex(searchInput)) {
|
||||
try {
|
||||
const tx = await client?.getTransaction({ hash: searchInput });
|
||||
if (tx) {
|
||||
router.push(`/blockexplorer/transaction/${searchInput}`);
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch transaction:", error);
|
||||
}
|
||||
}
|
||||
|
||||
if (isAddress(searchInput)) {
|
||||
router.push(`/blockexplorer/address/${searchInput}`);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSearch} className="flex items-center justify-end mb-5 space-x-3 mx-5">
|
||||
<input
|
||||
className="border-primary bg-base-100 text-base-content placeholder:text-base-content/50 p-2 mr-2 w-full md:w-1/2 lg:w-1/3 rounded-md shadow-md focus:outline-hidden focus:ring-2 focus:ring-accent"
|
||||
type="text"
|
||||
value={searchInput}
|
||||
placeholder="Search by hash or address"
|
||||
onChange={e => setSearchInput(e.target.value)}
|
||||
/>
|
||||
<button className="btn btn-sm btn-primary" type="submit">
|
||||
Search
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
import Link from "next/link";
|
||||
import { CheckCircleIcon, DocumentDuplicateIcon } from "@heroicons/react/24/outline";
|
||||
import { useCopyToClipboard } from "~~/hooks/scaffold-eth/useCopyToClipboard";
|
||||
|
||||
export const TransactionHash = ({ hash }: { hash: string }) => {
|
||||
const { copyToClipboard: copyAddressToClipboard, isCopiedToClipboard: isAddressCopiedToClipboard } =
|
||||
useCopyToClipboard();
|
||||
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
<Link href={`/blockexplorer/transaction/${hash}`}>
|
||||
{hash?.substring(0, 6)}...{hash?.substring(hash.length - 4)}
|
||||
</Link>
|
||||
{isAddressCopiedToClipboard ? (
|
||||
<CheckCircleIcon
|
||||
className="ml-1.5 text-xl font-normal text-base-content h-5 w-5 cursor-pointer"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
) : (
|
||||
<DocumentDuplicateIcon
|
||||
className="ml-1.5 text-xl font-normal h-5 w-5 cursor-pointer"
|
||||
aria-hidden="true"
|
||||
onClick={() => copyAddressToClipboard(hash)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,71 @@
|
||||
import { TransactionHash } from "./TransactionHash";
|
||||
import { formatEther } from "viem";
|
||||
import { Address } from "~~/components/scaffold-eth";
|
||||
import { useTargetNetwork } from "~~/hooks/scaffold-eth/useTargetNetwork";
|
||||
import { TransactionWithFunction } from "~~/utils/scaffold-eth";
|
||||
import { TransactionsTableProps } from "~~/utils/scaffold-eth/";
|
||||
|
||||
export const TransactionsTable = ({ blocks, transactionReceipts }: TransactionsTableProps) => {
|
||||
const { targetNetwork } = useTargetNetwork();
|
||||
|
||||
return (
|
||||
<div className="flex justify-center px-4 md:px-0">
|
||||
<div className="overflow-x-auto w-full shadow-2xl rounded-xl">
|
||||
<table className="table text-xl bg-base-100 table-zebra w-full md:table-md table-sm">
|
||||
<thead>
|
||||
<tr className="rounded-xl text-sm text-base-content">
|
||||
<th className="bg-primary">Transaction Hash</th>
|
||||
<th className="bg-primary">Function Called</th>
|
||||
<th className="bg-primary">Block Number</th>
|
||||
<th className="bg-primary">Time Mined</th>
|
||||
<th className="bg-primary">From</th>
|
||||
<th className="bg-primary">To</th>
|
||||
<th className="bg-primary text-end">Value ({targetNetwork.nativeCurrency.symbol})</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{blocks.map(block =>
|
||||
(block.transactions as TransactionWithFunction[]).map(tx => {
|
||||
const receipt = transactionReceipts[tx.hash];
|
||||
const timeMined = new Date(Number(block.timestamp) * 1000).toLocaleString();
|
||||
const functionCalled = tx.input.substring(0, 10);
|
||||
|
||||
return (
|
||||
<tr key={tx.hash} className="hover text-sm">
|
||||
<td className="w-1/12 md:py-4">
|
||||
<TransactionHash hash={tx.hash} />
|
||||
</td>
|
||||
<td className="w-2/12 md:py-4">
|
||||
{tx.functionName === "0x" ? "" : <span className="mr-1">{tx.functionName}</span>}
|
||||
{functionCalled !== "0x" && (
|
||||
<span className="badge badge-primary font-bold text-xs">{functionCalled}</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="w-1/12 md:py-4">{block.number?.toString()}</td>
|
||||
<td className="w-2/12 md:py-4">{timeMined}</td>
|
||||
<td className="w-2/12 md:py-4">
|
||||
<Address address={tx.from} size="sm" onlyEnsOrAddress />
|
||||
</td>
|
||||
<td className="w-2/12 md:py-4">
|
||||
{!receipt?.contractAddress ? (
|
||||
tx.to && <Address address={tx.to} size="sm" onlyEnsOrAddress />
|
||||
) : (
|
||||
<div className="relative">
|
||||
<Address address={receipt.contractAddress} size="sm" onlyEnsOrAddress />
|
||||
<small className="absolute top-4 left-4">(Contract Creation)</small>
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
<td className="text-right md:py-4">
|
||||
{formatEther(tx.value)} {targetNetwork.nativeCurrency.symbol}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}),
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
7
packages/nextjs/app/blockexplorer/_components/index.tsx
Normal file
7
packages/nextjs/app/blockexplorer/_components/index.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
export * from "./SearchBar";
|
||||
export * from "./BackButton";
|
||||
export * from "./AddressCodeTab";
|
||||
export * from "./TransactionHash";
|
||||
export * from "./ContractTabs";
|
||||
export * from "./PaginationButton";
|
||||
export * from "./TransactionsTable";
|
||||
101
packages/nextjs/app/blockexplorer/address/[address]/page.tsx
Normal file
101
packages/nextjs/app/blockexplorer/address/[address]/page.tsx
Normal file
@@ -0,0 +1,101 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { Address } from "viem";
|
||||
import { hardhat } from "viem/chains";
|
||||
import { AddressComponent } from "~~/app/blockexplorer/_components/AddressComponent";
|
||||
import deployedContracts from "~~/contracts/deployedContracts";
|
||||
import { isZeroAddress } from "~~/utils/scaffold-eth/common";
|
||||
import { GenericContractsDeclaration } from "~~/utils/scaffold-eth/contract";
|
||||
|
||||
type PageProps = {
|
||||
params: Promise<{ address: Address }>;
|
||||
};
|
||||
|
||||
async function fetchByteCodeAndAssembly(buildInfoDirectory: string, contractPath: string) {
|
||||
const buildInfoFiles = fs.readdirSync(buildInfoDirectory);
|
||||
let bytecode = "";
|
||||
let assembly = "";
|
||||
|
||||
for (let i = 0; i < buildInfoFiles.length; i++) {
|
||||
const filePath = path.join(buildInfoDirectory, buildInfoFiles[i]);
|
||||
|
||||
const buildInfo = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
||||
|
||||
if (buildInfo.output.contracts[contractPath]) {
|
||||
for (const contract in buildInfo.output.contracts[contractPath]) {
|
||||
bytecode = buildInfo.output.contracts[contractPath][contract].evm.bytecode.object;
|
||||
assembly = buildInfo.output.contracts[contractPath][contract].evm.bytecode.opcodes;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (bytecode && assembly) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return { bytecode, assembly };
|
||||
}
|
||||
|
||||
const getContractData = async (address: Address) => {
|
||||
const contracts = deployedContracts as GenericContractsDeclaration | null;
|
||||
const chainId = hardhat.id;
|
||||
|
||||
if (!contracts || !contracts[chainId] || Object.keys(contracts[chainId]).length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let contractPath = "";
|
||||
|
||||
const buildInfoDirectory = path.join(
|
||||
__dirname,
|
||||
"..",
|
||||
"..",
|
||||
"..",
|
||||
"..",
|
||||
"..",
|
||||
"..",
|
||||
"..",
|
||||
"hardhat",
|
||||
"artifacts",
|
||||
"build-info",
|
||||
);
|
||||
|
||||
if (!fs.existsSync(buildInfoDirectory)) {
|
||||
throw new Error(`Directory ${buildInfoDirectory} not found.`);
|
||||
}
|
||||
|
||||
const deployedContractsOnChain = contracts[chainId];
|
||||
for (const [contractName, contractInfo] of Object.entries(deployedContractsOnChain)) {
|
||||
if (contractInfo.address.toLowerCase() === address.toLowerCase()) {
|
||||
contractPath = `contracts/${contractName}.sol`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!contractPath) {
|
||||
// No contract found at this address
|
||||
return null;
|
||||
}
|
||||
|
||||
const { bytecode, assembly } = await fetchByteCodeAndAssembly(buildInfoDirectory, contractPath);
|
||||
|
||||
return { bytecode, assembly };
|
||||
};
|
||||
|
||||
export function generateStaticParams() {
|
||||
// An workaround to enable static exports in Next.js, generating single dummy page.
|
||||
return [{ address: "0x0000000000000000000000000000000000000000" }];
|
||||
}
|
||||
|
||||
const AddressPage = async (props: PageProps) => {
|
||||
const params = await props.params;
|
||||
const address = params?.address as Address;
|
||||
|
||||
if (isZeroAddress(address)) return null;
|
||||
|
||||
const contractData: { bytecode: string; assembly: string } | null = await getContractData(address);
|
||||
return <AddressComponent address={address} contractData={contractData} />;
|
||||
};
|
||||
|
||||
export default AddressPage;
|
||||
12
packages/nextjs/app/blockexplorer/layout.tsx
Normal file
12
packages/nextjs/app/blockexplorer/layout.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import { getMetadata } from "~~/utils/scaffold-eth/getMetadata";
|
||||
|
||||
export const metadata = getMetadata({
|
||||
title: "Block Explorer",
|
||||
description: "Block Explorer created with 🏗 Scaffold-ETH 2",
|
||||
});
|
||||
|
||||
const BlockExplorerLayout = ({ children }: { children: React.ReactNode }) => {
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
export default BlockExplorerLayout;
|
||||
83
packages/nextjs/app/blockexplorer/page.tsx
Normal file
83
packages/nextjs/app/blockexplorer/page.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { PaginationButton, SearchBar, TransactionsTable } from "./_components";
|
||||
import type { NextPage } from "next";
|
||||
import { hardhat } from "viem/chains";
|
||||
import { useFetchBlocks } from "~~/hooks/scaffold-eth";
|
||||
import { useTargetNetwork } from "~~/hooks/scaffold-eth/useTargetNetwork";
|
||||
import { notification } from "~~/utils/scaffold-eth";
|
||||
|
||||
const BlockExplorer: NextPage = () => {
|
||||
const { blocks, transactionReceipts, currentPage, totalBlocks, setCurrentPage, error } = useFetchBlocks();
|
||||
const { targetNetwork } = useTargetNetwork();
|
||||
const [isLocalNetwork, setIsLocalNetwork] = useState(true);
|
||||
const [hasError, setHasError] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (targetNetwork.id !== hardhat.id) {
|
||||
setIsLocalNetwork(false);
|
||||
}
|
||||
}, [targetNetwork.id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (targetNetwork.id === hardhat.id && error) {
|
||||
setHasError(true);
|
||||
}
|
||||
}, [targetNetwork.id, error]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLocalNetwork) {
|
||||
notification.error(
|
||||
<>
|
||||
<p className="font-bold mt-0 mb-1">
|
||||
<code className="italic bg-base-300 text-base font-bold"> targetNetwork </code> is not localhost
|
||||
</p>
|
||||
<p className="m-0">
|
||||
- You are on <code className="italic bg-base-300 text-base font-bold">{targetNetwork.name}</code> .This
|
||||
block explorer is only for <code className="italic bg-base-300 text-base font-bold">localhost</code>.
|
||||
</p>
|
||||
<p className="mt-1 break-normal">
|
||||
- You can use{" "}
|
||||
<a className="text-accent" href={targetNetwork.blockExplorers?.default.url}>
|
||||
{targetNetwork.blockExplorers?.default.name}
|
||||
</a>{" "}
|
||||
instead
|
||||
</p>
|
||||
</>,
|
||||
);
|
||||
}
|
||||
}, [
|
||||
isLocalNetwork,
|
||||
targetNetwork.blockExplorers?.default.name,
|
||||
targetNetwork.blockExplorers?.default.url,
|
||||
targetNetwork.name,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (hasError) {
|
||||
notification.error(
|
||||
<>
|
||||
<p className="font-bold mt-0 mb-1">Cannot connect to local provider</p>
|
||||
<p className="m-0">
|
||||
- Did you forget to run <code className="italic bg-base-300 text-base font-bold">yarn chain</code> ?
|
||||
</p>
|
||||
<p className="mt-1 break-normal">
|
||||
- Or you can change <code className="italic bg-base-300 text-base font-bold">targetNetwork</code> in{" "}
|
||||
<code className="italic bg-base-300 text-base font-bold">scaffold.config.ts</code>
|
||||
</p>
|
||||
</>,
|
||||
);
|
||||
}
|
||||
}, [hasError]);
|
||||
|
||||
return (
|
||||
<div className="container mx-auto my-10">
|
||||
<SearchBar />
|
||||
<TransactionsTable blocks={blocks} transactionReceipts={transactionReceipts} />
|
||||
<PaginationButton currentPage={currentPage} totalItems={Number(totalBlocks)} setCurrentPage={setCurrentPage} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BlockExplorer;
|
||||
@@ -0,0 +1,23 @@
|
||||
import TransactionComp from "../_components/TransactionComp";
|
||||
import type { NextPage } from "next";
|
||||
import { Hash } from "viem";
|
||||
import { isZeroAddress } from "~~/utils/scaffold-eth/common";
|
||||
|
||||
type PageProps = {
|
||||
params: Promise<{ txHash?: Hash }>;
|
||||
};
|
||||
|
||||
export function generateStaticParams() {
|
||||
// An workaround to enable static exports in Next.js, generating single dummy page.
|
||||
return [{ txHash: "0x0000000000000000000000000000000000000000" }];
|
||||
}
|
||||
const TransactionPage: NextPage<PageProps> = async (props: PageProps) => {
|
||||
const params = await props.params;
|
||||
const txHash = params?.txHash as Hash;
|
||||
|
||||
if (isZeroAddress(txHash)) return null;
|
||||
|
||||
return <TransactionComp txHash={txHash} />;
|
||||
};
|
||||
|
||||
export default TransactionPage;
|
||||
@@ -0,0 +1,152 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Hash, Transaction, TransactionReceipt, formatEther, formatUnits } from "viem";
|
||||
import { hardhat } from "viem/chains";
|
||||
import { usePublicClient } from "wagmi";
|
||||
import { Address } from "~~/components/scaffold-eth";
|
||||
import { useTargetNetwork } from "~~/hooks/scaffold-eth/useTargetNetwork";
|
||||
import { decodeTransactionData, getFunctionDetails } from "~~/utils/scaffold-eth";
|
||||
import { replacer } from "~~/utils/scaffold-eth/common";
|
||||
|
||||
const TransactionComp = ({ txHash }: { txHash: Hash }) => {
|
||||
const client = usePublicClient({ chainId: hardhat.id });
|
||||
const router = useRouter();
|
||||
const [transaction, setTransaction] = useState<Transaction>();
|
||||
const [receipt, setReceipt] = useState<TransactionReceipt>();
|
||||
const [functionCalled, setFunctionCalled] = useState<string>();
|
||||
|
||||
const { targetNetwork } = useTargetNetwork();
|
||||
|
||||
useEffect(() => {
|
||||
if (txHash && client) {
|
||||
const fetchTransaction = async () => {
|
||||
const tx = await client.getTransaction({ hash: txHash });
|
||||
const receipt = await client.getTransactionReceipt({ hash: txHash });
|
||||
|
||||
const transactionWithDecodedData = decodeTransactionData(tx);
|
||||
setTransaction(transactionWithDecodedData);
|
||||
setReceipt(receipt);
|
||||
|
||||
const functionCalled = transactionWithDecodedData.input.substring(0, 10);
|
||||
setFunctionCalled(functionCalled);
|
||||
};
|
||||
|
||||
fetchTransaction();
|
||||
}
|
||||
}, [client, txHash]);
|
||||
|
||||
return (
|
||||
<div className="container mx-auto mt-10 mb-20 px-10 md:px-0">
|
||||
<button className="btn btn-sm btn-primary" onClick={() => router.back()}>
|
||||
Back
|
||||
</button>
|
||||
{transaction ? (
|
||||
<div className="overflow-x-auto">
|
||||
<h2 className="text-3xl font-bold mb-4 text-center text-primary-content">Transaction Details</h2>{" "}
|
||||
<table className="table rounded-lg bg-base-100 w-full shadow-lg md:table-lg table-md">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<strong>Transaction Hash:</strong>
|
||||
</td>
|
||||
<td>{transaction.hash}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<strong>Block Number:</strong>
|
||||
</td>
|
||||
<td>{Number(transaction.blockNumber)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<strong>From:</strong>
|
||||
</td>
|
||||
<td>
|
||||
<Address address={transaction.from} format="long" onlyEnsOrAddress />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<strong>To:</strong>
|
||||
</td>
|
||||
<td>
|
||||
{!receipt?.contractAddress ? (
|
||||
transaction.to && <Address address={transaction.to} format="long" onlyEnsOrAddress />
|
||||
) : (
|
||||
<span>
|
||||
Contract Creation:
|
||||
<Address address={receipt.contractAddress} format="long" onlyEnsOrAddress />
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<strong>Value:</strong>
|
||||
</td>
|
||||
<td>
|
||||
{formatEther(transaction.value)} {targetNetwork.nativeCurrency.symbol}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<strong>Function called:</strong>
|
||||
</td>
|
||||
<td>
|
||||
<div className="w-full md:max-w-[600px] lg:max-w-[800px] overflow-x-auto whitespace-nowrap">
|
||||
{functionCalled === "0x" ? (
|
||||
"This transaction did not call any function."
|
||||
) : (
|
||||
<>
|
||||
<span className="mr-2">{getFunctionDetails(transaction)}</span>
|
||||
<span className="badge badge-primary font-bold">{functionCalled}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<strong>Gas Price:</strong>
|
||||
</td>
|
||||
<td>{formatUnits(transaction.gasPrice || 0n, 9)} Gwei</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<strong>Data:</strong>
|
||||
</td>
|
||||
<td className="form-control">
|
||||
<textarea
|
||||
readOnly
|
||||
value={transaction.input}
|
||||
className="p-0 w-full textarea-primary bg-inherit h-[150px]"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<strong>Logs:</strong>
|
||||
</td>
|
||||
<td>
|
||||
<ul>
|
||||
{receipt?.logs?.map((log, i) => (
|
||||
<li key={i}>
|
||||
<strong>Log {i} topics:</strong> {JSON.stringify(log.topics, replacer, 2)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-2xl text-base-content">Loading...</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TransactionComp;
|
||||
73
packages/nextjs/app/debug/_components/DebugContracts.tsx
Normal file
73
packages/nextjs/app/debug/_components/DebugContracts.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { useSessionStorage } from "usehooks-ts";
|
||||
import { BarsArrowUpIcon } from "@heroicons/react/20/solid";
|
||||
import { ContractUI } from "~~/app/debug/_components/contract";
|
||||
import { ContractName, GenericContract } from "~~/utils/scaffold-eth/contract";
|
||||
import { useAllContracts } from "~~/utils/scaffold-eth/contractsData";
|
||||
|
||||
const selectedContractStorageKey = "scaffoldEth2.selectedContract";
|
||||
|
||||
export function DebugContracts() {
|
||||
const contractsData = useAllContracts();
|
||||
const contractNames = useMemo(
|
||||
() =>
|
||||
Object.keys(contractsData).sort((a, b) => {
|
||||
return a.localeCompare(b, undefined, { numeric: true, sensitivity: "base" });
|
||||
}) as ContractName[],
|
||||
[contractsData],
|
||||
);
|
||||
|
||||
const [selectedContract, setSelectedContract] = useSessionStorage<ContractName>(
|
||||
selectedContractStorageKey,
|
||||
contractNames[0],
|
||||
{ initializeWithValue: false },
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!contractNames.includes(selectedContract)) {
|
||||
setSelectedContract(contractNames[0]);
|
||||
}
|
||||
}, [contractNames, selectedContract, setSelectedContract]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-6 lg:gap-y-8 py-8 lg:py-12 justify-center items-center">
|
||||
{contractNames.length === 0 ? (
|
||||
<p className="text-3xl mt-14">No contracts found!</p>
|
||||
) : (
|
||||
<>
|
||||
{contractNames.length > 1 && (
|
||||
<div className="flex flex-row gap-2 w-full max-w-7xl pb-1 px-6 lg:px-10 flex-wrap">
|
||||
{contractNames.map(contractName => (
|
||||
<button
|
||||
className={`btn btn-secondary btn-sm font-light hover:border-transparent ${
|
||||
contractName === selectedContract
|
||||
? "bg-base-300 hover:bg-base-300 no-animation"
|
||||
: "bg-base-100 hover:bg-secondary"
|
||||
}`}
|
||||
key={contractName}
|
||||
onClick={() => setSelectedContract(contractName)}
|
||||
>
|
||||
{contractName}
|
||||
{(contractsData[contractName] as GenericContract)?.external && (
|
||||
<span className="tooltip tooltip-top tooltip-accent" data-tip="External contract">
|
||||
<BarsArrowUpIcon className="h-4 w-4 cursor-pointer" />
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{contractNames.map(contractName => (
|
||||
<ContractUI
|
||||
key={contractName}
|
||||
contractName={contractName}
|
||||
className={contractName === selectedContract ? "" : "hidden"}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
"use client";
|
||||
|
||||
import { Dispatch, SetStateAction } from "react";
|
||||
import { Tuple } from "./Tuple";
|
||||
import { TupleArray } from "./TupleArray";
|
||||
import { AbiParameter } from "abitype";
|
||||
import {
|
||||
AddressInput,
|
||||
Bytes32Input,
|
||||
BytesInput,
|
||||
InputBase,
|
||||
IntegerInput,
|
||||
IntegerVariant,
|
||||
} from "~~/components/scaffold-eth";
|
||||
import { AbiParameterTuple } from "~~/utils/scaffold-eth/contract";
|
||||
|
||||
type ContractInputProps = {
|
||||
setForm: Dispatch<SetStateAction<Record<string, any>>>;
|
||||
form: Record<string, any> | undefined;
|
||||
stateObjectKey: string;
|
||||
paramType: AbiParameter;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generic Input component to handle input's based on their function param type
|
||||
*/
|
||||
export const ContractInput = ({ setForm, form, stateObjectKey, paramType }: ContractInputProps) => {
|
||||
const inputProps = {
|
||||
name: stateObjectKey,
|
||||
value: form?.[stateObjectKey],
|
||||
placeholder: paramType.name ? `${paramType.type} ${paramType.name}` : paramType.type,
|
||||
onChange: (value: any) => {
|
||||
setForm(form => ({ ...form, [stateObjectKey]: value }));
|
||||
},
|
||||
};
|
||||
|
||||
const renderInput = () => {
|
||||
switch (paramType.type) {
|
||||
case "address":
|
||||
return <AddressInput {...inputProps} />;
|
||||
case "bytes32":
|
||||
return <Bytes32Input {...inputProps} />;
|
||||
case "bytes":
|
||||
return <BytesInput {...inputProps} />;
|
||||
case "string":
|
||||
return <InputBase {...inputProps} />;
|
||||
case "tuple":
|
||||
return (
|
||||
<Tuple
|
||||
setParentForm={setForm}
|
||||
parentForm={form}
|
||||
abiTupleParameter={paramType as AbiParameterTuple}
|
||||
parentStateObjectKey={stateObjectKey}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
// Handling 'int' types and 'tuple[]' types
|
||||
if (paramType.type.includes("int") && !paramType.type.includes("[")) {
|
||||
return <IntegerInput {...inputProps} variant={paramType.type as IntegerVariant} />;
|
||||
} else if (paramType.type.startsWith("tuple[")) {
|
||||
return (
|
||||
<TupleArray
|
||||
setParentForm={setForm}
|
||||
parentForm={form}
|
||||
abiTupleParameter={paramType as AbiParameterTuple}
|
||||
parentStateObjectKey={stateObjectKey}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return <InputBase {...inputProps} />;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-1.5 w-full">
|
||||
<div className="flex items-center ml-2">
|
||||
{paramType.name && <span className="text-xs font-medium mr-2 leading-none">{paramType.name}</span>}
|
||||
<span className="block text-xs font-extralight leading-none">{paramType.type}</span>
|
||||
</div>
|
||||
{renderInput()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
import { Abi, AbiFunction } from "abitype";
|
||||
import { ReadOnlyFunctionForm } from "~~/app/debug/_components/contract";
|
||||
import { Contract, ContractName, GenericContract, InheritedFunctions } from "~~/utils/scaffold-eth/contract";
|
||||
|
||||
export const ContractReadMethods = ({ deployedContractData }: { deployedContractData: Contract<ContractName> }) => {
|
||||
if (!deployedContractData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const functionsToDisplay = (
|
||||
((deployedContractData.abi || []) as Abi).filter(part => part.type === "function") as AbiFunction[]
|
||||
)
|
||||
.filter(fn => {
|
||||
const isQueryableWithParams =
|
||||
(fn.stateMutability === "view" || fn.stateMutability === "pure") && fn.inputs.length > 0;
|
||||
return isQueryableWithParams;
|
||||
})
|
||||
.map(fn => {
|
||||
return {
|
||||
fn,
|
||||
inheritedFrom: ((deployedContractData as GenericContract)?.inheritedFunctions as InheritedFunctions)?.[fn.name],
|
||||
};
|
||||
})
|
||||
.sort((a, b) => (b.inheritedFrom ? b.inheritedFrom.localeCompare(a.inheritedFrom) : 1));
|
||||
|
||||
if (!functionsToDisplay.length) {
|
||||
return <>No read methods</>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{functionsToDisplay.map(({ fn, inheritedFrom }) => (
|
||||
<ReadOnlyFunctionForm
|
||||
abi={deployedContractData.abi as Abi}
|
||||
contractAddress={deployedContractData.address}
|
||||
abiFunction={fn}
|
||||
key={fn.name}
|
||||
inheritedFrom={inheritedFrom}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
104
packages/nextjs/app/debug/_components/contract/ContractUI.tsx
Normal file
104
packages/nextjs/app/debug/_components/contract/ContractUI.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
"use client";
|
||||
|
||||
// @refresh reset
|
||||
import { useReducer } from "react";
|
||||
import { ContractReadMethods } from "./ContractReadMethods";
|
||||
import { ContractVariables } from "./ContractVariables";
|
||||
import { ContractWriteMethods } from "./ContractWriteMethods";
|
||||
import { Address, Balance } from "~~/components/scaffold-eth";
|
||||
import { useDeployedContractInfo, useNetworkColor } from "~~/hooks/scaffold-eth";
|
||||
import { useTargetNetwork } from "~~/hooks/scaffold-eth/useTargetNetwork";
|
||||
import { ContractName } from "~~/utils/scaffold-eth/contract";
|
||||
|
||||
type ContractUIProps = {
|
||||
contractName: ContractName;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* UI component to interface with deployed contracts.
|
||||
**/
|
||||
export const ContractUI = ({ contractName, className = "" }: ContractUIProps) => {
|
||||
const [refreshDisplayVariables, triggerRefreshDisplayVariables] = useReducer(value => !value, false);
|
||||
const { targetNetwork } = useTargetNetwork();
|
||||
const { data: deployedContractData, isLoading: deployedContractLoading } = useDeployedContractInfo({ contractName });
|
||||
const networkColor = useNetworkColor();
|
||||
|
||||
if (deployedContractLoading) {
|
||||
return (
|
||||
<div className="mt-14">
|
||||
<span className="loading loading-spinner loading-lg"></span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!deployedContractData) {
|
||||
return (
|
||||
<p className="text-3xl mt-14">
|
||||
{`No contract found by the name of "${contractName}" on chain "${targetNetwork.name}"!`}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`grid grid-cols-1 lg:grid-cols-6 px-6 lg:px-10 lg:gap-12 w-full max-w-7xl my-0 ${className}`}>
|
||||
<div className="col-span-5 grid grid-cols-1 lg:grid-cols-3 gap-8 lg:gap-10">
|
||||
<div className="col-span-1 flex flex-col">
|
||||
<div className="bg-base-100 border-base-300 border shadow-md shadow-secondary rounded-3xl px-6 lg:px-8 mb-6 space-y-1 py-4">
|
||||
<div className="flex">
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="font-bold">{contractName}</span>
|
||||
<Address address={deployedContractData.address} onlyEnsOrAddress />
|
||||
<div className="flex gap-1 items-center">
|
||||
<span className="font-bold text-sm">Balance:</span>
|
||||
<Balance address={deployedContractData.address} className="px-0 h-1.5 min-h-[0.375rem]" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{targetNetwork && (
|
||||
<p className="my-0 text-sm">
|
||||
<span className="font-bold">Network</span>:{" "}
|
||||
<span style={{ color: networkColor }}>{targetNetwork.name}</span>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="bg-base-300 rounded-3xl px-6 lg:px-8 py-4 shadow-lg shadow-base-300">
|
||||
<ContractVariables
|
||||
refreshDisplayVariables={refreshDisplayVariables}
|
||||
deployedContractData={deployedContractData}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-1 lg:col-span-2 flex flex-col gap-6">
|
||||
<div className="z-10">
|
||||
<div className="bg-base-100 rounded-3xl shadow-md shadow-secondary border border-base-300 flex flex-col mt-10 relative">
|
||||
<div className="h-[5rem] w-[5.5rem] bg-base-300 absolute self-start rounded-[22px] -top-[38px] -left-[1px] -z-10 py-[0.65rem] shadow-lg shadow-base-300">
|
||||
<div className="flex items-center justify-center space-x-2">
|
||||
<p className="my-0 text-sm">Read</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-5 divide-y divide-base-300">
|
||||
<ContractReadMethods deployedContractData={deployedContractData} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="z-10">
|
||||
<div className="bg-base-100 rounded-3xl shadow-md shadow-secondary border border-base-300 flex flex-col mt-10 relative">
|
||||
<div className="h-[5rem] w-[5.5rem] bg-base-300 absolute self-start rounded-[22px] -top-[38px] -left-[1px] -z-10 py-[0.65rem] shadow-lg shadow-base-300">
|
||||
<div className="flex items-center justify-center space-x-2">
|
||||
<p className="my-0 text-sm">Write</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-5 divide-y divide-base-300">
|
||||
<ContractWriteMethods
|
||||
deployedContractData={deployedContractData}
|
||||
onChange={triggerRefreshDisplayVariables}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
import { DisplayVariable } from "./DisplayVariable";
|
||||
import { Abi, AbiFunction } from "abitype";
|
||||
import { Contract, ContractName, GenericContract, InheritedFunctions } from "~~/utils/scaffold-eth/contract";
|
||||
|
||||
export const ContractVariables = ({
|
||||
refreshDisplayVariables,
|
||||
deployedContractData,
|
||||
}: {
|
||||
refreshDisplayVariables: boolean;
|
||||
deployedContractData: Contract<ContractName>;
|
||||
}) => {
|
||||
if (!deployedContractData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const functionsToDisplay = (
|
||||
(deployedContractData.abi as Abi).filter(part => part.type === "function") as AbiFunction[]
|
||||
)
|
||||
.filter(fn => {
|
||||
const isQueryableWithNoParams =
|
||||
(fn.stateMutability === "view" || fn.stateMutability === "pure") && fn.inputs.length === 0;
|
||||
return isQueryableWithNoParams;
|
||||
})
|
||||
.map(fn => {
|
||||
return {
|
||||
fn,
|
||||
inheritedFrom: ((deployedContractData as GenericContract)?.inheritedFunctions as InheritedFunctions)?.[fn.name],
|
||||
};
|
||||
})
|
||||
.sort((a, b) => (b.inheritedFrom ? b.inheritedFrom.localeCompare(a.inheritedFrom) : 1));
|
||||
|
||||
if (!functionsToDisplay.length) {
|
||||
return <>No contract variables</>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{functionsToDisplay.map(({ fn, inheritedFrom }) => (
|
||||
<DisplayVariable
|
||||
abi={deployedContractData.abi as Abi}
|
||||
abiFunction={fn}
|
||||
contractAddress={deployedContractData.address}
|
||||
key={fn.name}
|
||||
refreshDisplayVariables={refreshDisplayVariables}
|
||||
inheritedFrom={inheritedFrom}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
import { Abi, AbiFunction } from "abitype";
|
||||
import { WriteOnlyFunctionForm } from "~~/app/debug/_components/contract";
|
||||
import { Contract, ContractName, GenericContract, InheritedFunctions } from "~~/utils/scaffold-eth/contract";
|
||||
|
||||
export const ContractWriteMethods = ({
|
||||
onChange,
|
||||
deployedContractData,
|
||||
}: {
|
||||
onChange: () => void;
|
||||
deployedContractData: Contract<ContractName>;
|
||||
}) => {
|
||||
if (!deployedContractData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const functionsToDisplay = (
|
||||
(deployedContractData.abi as Abi).filter(part => part.type === "function") as AbiFunction[]
|
||||
)
|
||||
.filter(fn => {
|
||||
const isWriteableFunction = fn.stateMutability !== "view" && fn.stateMutability !== "pure";
|
||||
return isWriteableFunction;
|
||||
})
|
||||
.map(fn => {
|
||||
return {
|
||||
fn,
|
||||
inheritedFrom: ((deployedContractData as GenericContract)?.inheritedFunctions as InheritedFunctions)?.[fn.name],
|
||||
};
|
||||
})
|
||||
.sort((a, b) => (b.inheritedFrom ? b.inheritedFrom.localeCompare(a.inheritedFrom) : 1));
|
||||
|
||||
if (!functionsToDisplay.length) {
|
||||
return <>No write methods</>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{functionsToDisplay.map(({ fn, inheritedFrom }, idx) => (
|
||||
<WriteOnlyFunctionForm
|
||||
abi={deployedContractData.abi as Abi}
|
||||
key={`${fn.name}-${idx}}`}
|
||||
abiFunction={fn}
|
||||
onChange={onChange}
|
||||
contractAddress={deployedContractData.address}
|
||||
inheritedFrom={inheritedFrom}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,85 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { InheritanceTooltip } from "./InheritanceTooltip";
|
||||
import { displayTxResult } from "./utilsDisplay";
|
||||
import { Abi, AbiFunction } from "abitype";
|
||||
import { Address } from "viem";
|
||||
import { useReadContract } from "wagmi";
|
||||
import { ArrowPathIcon } from "@heroicons/react/24/outline";
|
||||
import { useAnimationConfig } from "~~/hooks/scaffold-eth";
|
||||
import { useTargetNetwork } from "~~/hooks/scaffold-eth/useTargetNetwork";
|
||||
import { getParsedError, notification } from "~~/utils/scaffold-eth";
|
||||
|
||||
type DisplayVariableProps = {
|
||||
contractAddress: Address;
|
||||
abiFunction: AbiFunction;
|
||||
refreshDisplayVariables: boolean;
|
||||
inheritedFrom?: string;
|
||||
abi: Abi;
|
||||
};
|
||||
|
||||
export const DisplayVariable = ({
|
||||
contractAddress,
|
||||
abiFunction,
|
||||
refreshDisplayVariables,
|
||||
abi,
|
||||
inheritedFrom,
|
||||
}: DisplayVariableProps) => {
|
||||
const { targetNetwork } = useTargetNetwork();
|
||||
|
||||
const {
|
||||
data: result,
|
||||
isFetching,
|
||||
refetch,
|
||||
error,
|
||||
} = useReadContract({
|
||||
address: contractAddress,
|
||||
functionName: abiFunction.name,
|
||||
abi: abi,
|
||||
chainId: targetNetwork.id,
|
||||
query: {
|
||||
retry: false,
|
||||
},
|
||||
});
|
||||
|
||||
const { showAnimation } = useAnimationConfig(result);
|
||||
|
||||
useEffect(() => {
|
||||
refetch();
|
||||
}, [refetch, refreshDisplayVariables]);
|
||||
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
const parsedError = getParsedError(error);
|
||||
notification.error(parsedError);
|
||||
}
|
||||
}, [error]);
|
||||
|
||||
return (
|
||||
<div className="space-y-1 pb-2">
|
||||
<div className="flex items-center">
|
||||
<h3 className="font-medium text-lg mb-0 break-all">{abiFunction.name}</h3>
|
||||
<button className="btn btn-ghost btn-xs" onClick={async () => await refetch()}>
|
||||
{isFetching ? (
|
||||
<span className="loading loading-spinner loading-xs"></span>
|
||||
) : (
|
||||
<ArrowPathIcon className="h-3 w-3 cursor-pointer" aria-hidden="true" />
|
||||
)}
|
||||
</button>
|
||||
<InheritanceTooltip inheritedFrom={inheritedFrom} />
|
||||
</div>
|
||||
<div className="text-base-content/80 flex flex-col items-start">
|
||||
<div>
|
||||
<div
|
||||
className={`break-all block transition bg-transparent ${
|
||||
showAnimation ? "bg-warning rounded-xs animate-pulse-fast" : ""
|
||||
}`}
|
||||
>
|
||||
{displayTxResult(result)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
import { InformationCircleIcon } from "@heroicons/react/20/solid";
|
||||
|
||||
export const InheritanceTooltip = ({ inheritedFrom }: { inheritedFrom?: string }) => (
|
||||
<>
|
||||
{inheritedFrom && (
|
||||
<span
|
||||
className="tooltip tooltip-top tooltip-accent px-2 md:break-normal"
|
||||
data-tip={`Inherited from: ${inheritedFrom}`}
|
||||
>
|
||||
<InformationCircleIcon className="h-4 w-4" aria-hidden="true" />
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
@@ -0,0 +1,102 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { InheritanceTooltip } from "./InheritanceTooltip";
|
||||
import { Abi, AbiFunction } from "abitype";
|
||||
import { Address } from "viem";
|
||||
import { useReadContract } from "wagmi";
|
||||
import {
|
||||
ContractInput,
|
||||
displayTxResult,
|
||||
getFunctionInputKey,
|
||||
getInitialFormState,
|
||||
getParsedContractFunctionArgs,
|
||||
transformAbiFunction,
|
||||
} from "~~/app/debug/_components/contract";
|
||||
import { useTargetNetwork } from "~~/hooks/scaffold-eth/useTargetNetwork";
|
||||
import { getParsedError, notification } from "~~/utils/scaffold-eth";
|
||||
|
||||
type ReadOnlyFunctionFormProps = {
|
||||
contractAddress: Address;
|
||||
abiFunction: AbiFunction;
|
||||
inheritedFrom?: string;
|
||||
abi: Abi;
|
||||
};
|
||||
|
||||
export const ReadOnlyFunctionForm = ({
|
||||
contractAddress,
|
||||
abiFunction,
|
||||
inheritedFrom,
|
||||
abi,
|
||||
}: ReadOnlyFunctionFormProps) => {
|
||||
const [form, setForm] = useState<Record<string, any>>(() => getInitialFormState(abiFunction));
|
||||
const [result, setResult] = useState<unknown>();
|
||||
const { targetNetwork } = useTargetNetwork();
|
||||
|
||||
const { isFetching, refetch, error } = useReadContract({
|
||||
address: contractAddress,
|
||||
functionName: abiFunction.name,
|
||||
abi: abi,
|
||||
args: getParsedContractFunctionArgs(form),
|
||||
chainId: targetNetwork.id,
|
||||
query: {
|
||||
enabled: false,
|
||||
retry: false,
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
const parsedError = getParsedError(error);
|
||||
notification.error(parsedError);
|
||||
}
|
||||
}, [error]);
|
||||
|
||||
const transformedFunction = transformAbiFunction(abiFunction);
|
||||
const inputElements = transformedFunction.inputs.map((input, inputIndex) => {
|
||||
const key = getFunctionInputKey(abiFunction.name, input, inputIndex);
|
||||
return (
|
||||
<ContractInput
|
||||
key={key}
|
||||
setForm={updatedFormValue => {
|
||||
setResult(undefined);
|
||||
setForm(updatedFormValue);
|
||||
}}
|
||||
form={form}
|
||||
stateObjectKey={key}
|
||||
paramType={input}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3 py-5 first:pt-0 last:pb-1">
|
||||
<p className="font-medium my-0 break-words">
|
||||
{abiFunction.name}
|
||||
<InheritanceTooltip inheritedFrom={inheritedFrom} />
|
||||
</p>
|
||||
{inputElements}
|
||||
<div className="flex flex-col md:flex-row justify-between gap-2 flex-wrap">
|
||||
<div className="grow w-full md:max-w-[80%]">
|
||||
{result !== null && result !== undefined && (
|
||||
<div className="bg-secondary rounded-3xl text-sm px-4 py-1.5 break-words overflow-auto">
|
||||
<p className="font-bold m-0 mb-1">Result:</p>
|
||||
<pre className="whitespace-pre-wrap break-words">{displayTxResult(result, "sm")}</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
className="btn btn-secondary btn-sm self-end md:self-start"
|
||||
onClick={async () => {
|
||||
const { data } = await refetch();
|
||||
setResult(data);
|
||||
}}
|
||||
disabled={isFetching}
|
||||
>
|
||||
{isFetching && <span className="loading loading-spinner loading-xs"></span>}
|
||||
Read 📡
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
44
packages/nextjs/app/debug/_components/contract/Tuple.tsx
Normal file
44
packages/nextjs/app/debug/_components/contract/Tuple.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import { Dispatch, SetStateAction, useEffect, useState } from "react";
|
||||
import { ContractInput } from "./ContractInput";
|
||||
import { getFunctionInputKey, getInitialTupleFormState } from "./utilsContract";
|
||||
import { replacer } from "~~/utils/scaffold-eth/common";
|
||||
import { AbiParameterTuple } from "~~/utils/scaffold-eth/contract";
|
||||
|
||||
type TupleProps = {
|
||||
abiTupleParameter: AbiParameterTuple;
|
||||
setParentForm: Dispatch<SetStateAction<Record<string, any>>>;
|
||||
parentStateObjectKey: string;
|
||||
parentForm: Record<string, any> | undefined;
|
||||
};
|
||||
|
||||
export const Tuple = ({ abiTupleParameter, setParentForm, parentStateObjectKey }: TupleProps) => {
|
||||
const [form, setForm] = useState<Record<string, any>>(() => getInitialTupleFormState(abiTupleParameter));
|
||||
|
||||
useEffect(() => {
|
||||
const values = Object.values(form);
|
||||
const argsStruct: Record<string, any> = {};
|
||||
abiTupleParameter.components.forEach((component, componentIndex) => {
|
||||
argsStruct[component.name || `input_${componentIndex}_`] = values[componentIndex];
|
||||
});
|
||||
|
||||
setParentForm(parentForm => ({ ...parentForm, [parentStateObjectKey]: JSON.stringify(argsStruct, replacer) }));
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [JSON.stringify(form, replacer)]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div tabIndex={0} className="collapse collapse-arrow bg-base-200 pl-4 py-1.5 border-2 border-secondary">
|
||||
<input type="checkbox" className="min-h-fit! peer" />
|
||||
<div className="collapse-title after:top-3.5! p-0 min-h-fit! peer-checked:mb-2 text-primary-content/50">
|
||||
<p className="m-0 p-0 text-[1rem]">{abiTupleParameter.internalType}</p>
|
||||
</div>
|
||||
<div className="ml-3 flex-col space-y-4 border-secondary/80 border-l-2 pl-4 collapse-content">
|
||||
{abiTupleParameter?.components?.map((param, index) => {
|
||||
const key = getFunctionInputKey(abiTupleParameter.name || "tuple", param, index);
|
||||
return <ContractInput setForm={setForm} form={form} key={key} stateObjectKey={key} paramType={param} />;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
142
packages/nextjs/app/debug/_components/contract/TupleArray.tsx
Normal file
142
packages/nextjs/app/debug/_components/contract/TupleArray.tsx
Normal file
@@ -0,0 +1,142 @@
|
||||
import { Dispatch, SetStateAction, useEffect, useState } from "react";
|
||||
import { ContractInput } from "./ContractInput";
|
||||
import { getFunctionInputKey, getInitialTupleArrayFormState } from "./utilsContract";
|
||||
import { replacer } from "~~/utils/scaffold-eth/common";
|
||||
import { AbiParameterTuple } from "~~/utils/scaffold-eth/contract";
|
||||
|
||||
type TupleArrayProps = {
|
||||
abiTupleParameter: AbiParameterTuple & { isVirtual?: true };
|
||||
setParentForm: Dispatch<SetStateAction<Record<string, any>>>;
|
||||
parentStateObjectKey: string;
|
||||
parentForm: Record<string, any> | undefined;
|
||||
};
|
||||
|
||||
export const TupleArray = ({ abiTupleParameter, setParentForm, parentStateObjectKey }: TupleArrayProps) => {
|
||||
const [form, setForm] = useState<Record<string, any>>(() => getInitialTupleArrayFormState(abiTupleParameter));
|
||||
const [additionalInputs, setAdditionalInputs] = useState<Array<typeof abiTupleParameter.components>>([
|
||||
abiTupleParameter.components,
|
||||
]);
|
||||
|
||||
const depth = (abiTupleParameter.type.match(/\[\]/g) || []).length;
|
||||
|
||||
useEffect(() => {
|
||||
// Extract and group fields based on index prefix
|
||||
const groupedFields = Object.keys(form).reduce(
|
||||
(acc, key) => {
|
||||
const [indexPrefix, ...restArray] = key.split("_");
|
||||
const componentName = restArray.join("_");
|
||||
if (!acc[indexPrefix]) {
|
||||
acc[indexPrefix] = {};
|
||||
}
|
||||
acc[indexPrefix][componentName] = form[key];
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, Record<string, any>>,
|
||||
);
|
||||
|
||||
let argsArray: Array<Record<string, any>> = [];
|
||||
|
||||
Object.keys(groupedFields).forEach(key => {
|
||||
const currentKeyValues = Object.values(groupedFields[key]);
|
||||
|
||||
const argsStruct: Record<string, any> = {};
|
||||
abiTupleParameter.components.forEach((component, componentIndex) => {
|
||||
argsStruct[component.name || `input_${componentIndex}_`] = currentKeyValues[componentIndex];
|
||||
});
|
||||
|
||||
argsArray.push(argsStruct);
|
||||
});
|
||||
|
||||
if (depth > 1) {
|
||||
argsArray = argsArray.map(args => {
|
||||
return args[abiTupleParameter.components[0].name || "tuple"];
|
||||
});
|
||||
}
|
||||
|
||||
setParentForm(parentForm => {
|
||||
return { ...parentForm, [parentStateObjectKey]: JSON.stringify(argsArray, replacer) };
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [JSON.stringify(form, replacer)]);
|
||||
|
||||
const addInput = () => {
|
||||
setAdditionalInputs(previousValue => {
|
||||
const newAdditionalInputs = [...previousValue, abiTupleParameter.components];
|
||||
|
||||
// Add the new inputs to the form
|
||||
setForm(form => {
|
||||
const newForm = { ...form };
|
||||
abiTupleParameter.components.forEach((component, componentIndex) => {
|
||||
const key = getFunctionInputKey(
|
||||
`${newAdditionalInputs.length - 1}_${abiTupleParameter.name || "tuple"}`,
|
||||
component,
|
||||
componentIndex,
|
||||
);
|
||||
newForm[key] = "";
|
||||
});
|
||||
return newForm;
|
||||
});
|
||||
|
||||
return newAdditionalInputs;
|
||||
});
|
||||
};
|
||||
|
||||
const removeInput = () => {
|
||||
// Remove the last inputs from the form
|
||||
setForm(form => {
|
||||
const newForm = { ...form };
|
||||
abiTupleParameter.components.forEach((component, componentIndex) => {
|
||||
const key = getFunctionInputKey(
|
||||
`${additionalInputs.length - 1}_${abiTupleParameter.name || "tuple"}`,
|
||||
component,
|
||||
componentIndex,
|
||||
);
|
||||
delete newForm[key];
|
||||
});
|
||||
return newForm;
|
||||
});
|
||||
setAdditionalInputs(inputs => inputs.slice(0, -1));
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="collapse collapse-arrow bg-base-200 pl-4 py-1.5 border-2 border-secondary">
|
||||
<input type="checkbox" className="min-h-fit! peer" />
|
||||
<div className="collapse-title after:top-3.5! p-0 min-h-fit! peer-checked:mb-1 text-primary-content/50">
|
||||
<p className="m-0 text-[1rem]">{abiTupleParameter.internalType}</p>
|
||||
</div>
|
||||
<div className="ml-3 flex-col space-y-2 border-secondary/70 border-l-2 pl-4 collapse-content">
|
||||
{additionalInputs.map((additionalInput, additionalIndex) => (
|
||||
<div key={additionalIndex} className="space-y-1">
|
||||
<span className="badge bg-base-300 badge-sm">
|
||||
{depth > 1 ? `${additionalIndex}` : `tuple[${additionalIndex}]`}
|
||||
</span>
|
||||
<div className="space-y-4">
|
||||
{additionalInput.map((param, index) => {
|
||||
const key = getFunctionInputKey(
|
||||
`${additionalIndex}_${abiTupleParameter.name || "tuple"}`,
|
||||
param,
|
||||
index,
|
||||
);
|
||||
return (
|
||||
<ContractInput setForm={setForm} form={form} key={key} stateObjectKey={key} paramType={param} />
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div className="flex space-x-2">
|
||||
<button className="btn btn-sm btn-secondary" onClick={addInput}>
|
||||
+
|
||||
</button>
|
||||
{additionalInputs.length > 0 && (
|
||||
<button className="btn btn-sm btn-secondary" onClick={removeInput}>
|
||||
-
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
42
packages/nextjs/app/debug/_components/contract/TxReceipt.tsx
Normal file
42
packages/nextjs/app/debug/_components/contract/TxReceipt.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import { TransactionReceipt } from "viem";
|
||||
import { CheckCircleIcon, DocumentDuplicateIcon } from "@heroicons/react/24/outline";
|
||||
import { ObjectFieldDisplay } from "~~/app/debug/_components/contract";
|
||||
import { useCopyToClipboard } from "~~/hooks/scaffold-eth/useCopyToClipboard";
|
||||
import { replacer } from "~~/utils/scaffold-eth/common";
|
||||
|
||||
export const TxReceipt = ({ txResult }: { txResult: TransactionReceipt }) => {
|
||||
const { copyToClipboard: copyTxResultToClipboard, isCopiedToClipboard: isTxResultCopiedToClipboard } =
|
||||
useCopyToClipboard();
|
||||
|
||||
return (
|
||||
<div className="flex text-sm rounded-3xl peer-checked:rounded-b-none min-h-0 bg-secondary py-0">
|
||||
<div className="mt-1 pl-2">
|
||||
{isTxResultCopiedToClipboard ? (
|
||||
<CheckCircleIcon
|
||||
className="ml-1.5 text-xl font-normal text-base-content h-5 w-5 cursor-pointer"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
) : (
|
||||
<DocumentDuplicateIcon
|
||||
className="ml-1.5 text-xl font-normal h-5 w-5 cursor-pointer"
|
||||
aria-hidden="true"
|
||||
onClick={() => copyTxResultToClipboard(JSON.stringify(txResult, replacer, 2))}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div tabIndex={0} className="flex-wrap collapse collapse-arrow">
|
||||
<input type="checkbox" className="min-h-0! peer" />
|
||||
<div className="collapse-title text-sm min-h-0! py-1.5 pl-1 after:top-4!">
|
||||
<strong>Transaction Receipt</strong>
|
||||
</div>
|
||||
<div className="collapse-content overflow-auto bg-secondary rounded-t-none rounded-3xl pl-0!">
|
||||
<pre className="text-xs">
|
||||
{Object.entries(txResult).map(([k, v]) => (
|
||||
<ObjectFieldDisplay name={k} value={v} size="xs" leftPad={false} key={k} />
|
||||
))}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,144 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { InheritanceTooltip } from "./InheritanceTooltip";
|
||||
import { Abi, AbiFunction } from "abitype";
|
||||
import { Address, TransactionReceipt } from "viem";
|
||||
import { useAccount, useConfig, useWaitForTransactionReceipt, useWriteContract } from "wagmi";
|
||||
import {
|
||||
ContractInput,
|
||||
TxReceipt,
|
||||
getFunctionInputKey,
|
||||
getInitialFormState,
|
||||
getParsedContractFunctionArgs,
|
||||
transformAbiFunction,
|
||||
} from "~~/app/debug/_components/contract";
|
||||
import { IntegerInput } from "~~/components/scaffold-eth";
|
||||
import { useTransactor } from "~~/hooks/scaffold-eth";
|
||||
import { useTargetNetwork } from "~~/hooks/scaffold-eth/useTargetNetwork";
|
||||
import { simulateContractWriteAndNotifyError } from "~~/utils/scaffold-eth/contract";
|
||||
|
||||
type WriteOnlyFunctionFormProps = {
|
||||
abi: Abi;
|
||||
abiFunction: AbiFunction;
|
||||
onChange: () => void;
|
||||
contractAddress: Address;
|
||||
inheritedFrom?: string;
|
||||
};
|
||||
|
||||
export const WriteOnlyFunctionForm = ({
|
||||
abi,
|
||||
abiFunction,
|
||||
onChange,
|
||||
contractAddress,
|
||||
inheritedFrom,
|
||||
}: WriteOnlyFunctionFormProps) => {
|
||||
const [form, setForm] = useState<Record<string, any>>(() => getInitialFormState(abiFunction));
|
||||
const [txValue, setTxValue] = useState<string>("");
|
||||
const { chain } = useAccount();
|
||||
const writeTxn = useTransactor();
|
||||
const { targetNetwork } = useTargetNetwork();
|
||||
const writeDisabled = !chain || chain?.id !== targetNetwork.id;
|
||||
|
||||
const { data: result, isPending, writeContractAsync } = useWriteContract();
|
||||
|
||||
const wagmiConfig = useConfig();
|
||||
|
||||
const handleWrite = async () => {
|
||||
if (writeContractAsync) {
|
||||
try {
|
||||
const writeContractObj = {
|
||||
address: contractAddress,
|
||||
functionName: abiFunction.name,
|
||||
abi: abi,
|
||||
args: getParsedContractFunctionArgs(form),
|
||||
value: BigInt(txValue),
|
||||
};
|
||||
await simulateContractWriteAndNotifyError({ wagmiConfig, writeContractParams: writeContractObj });
|
||||
|
||||
const makeWriteWithParams = () => writeContractAsync(writeContractObj);
|
||||
await writeTxn(makeWriteWithParams);
|
||||
onChange();
|
||||
} catch (e: any) {
|
||||
console.error("⚡️ ~ file: WriteOnlyFunctionForm.tsx:handleWrite ~ error", e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const [displayedTxResult, setDisplayedTxResult] = useState<TransactionReceipt>();
|
||||
const { data: txResult } = useWaitForTransactionReceipt({
|
||||
hash: result,
|
||||
});
|
||||
useEffect(() => {
|
||||
setDisplayedTxResult(txResult);
|
||||
}, [txResult]);
|
||||
|
||||
// TODO use `useMemo` to optimize also update in ReadOnlyFunctionForm
|
||||
const transformedFunction = transformAbiFunction(abiFunction);
|
||||
const inputs = transformedFunction.inputs.map((input, inputIndex) => {
|
||||
const key = getFunctionInputKey(abiFunction.name, input, inputIndex);
|
||||
return (
|
||||
<ContractInput
|
||||
key={key}
|
||||
setForm={updatedFormValue => {
|
||||
setDisplayedTxResult(undefined);
|
||||
setForm(updatedFormValue);
|
||||
}}
|
||||
form={form}
|
||||
stateObjectKey={key}
|
||||
paramType={input}
|
||||
/>
|
||||
);
|
||||
});
|
||||
const zeroInputs = inputs.length === 0 && abiFunction.stateMutability !== "payable";
|
||||
|
||||
return (
|
||||
<div className="py-5 space-y-3 first:pt-0 last:pb-1">
|
||||
<div className={`flex gap-3 ${zeroInputs ? "flex-row justify-between items-center" : "flex-col"}`}>
|
||||
<p className="font-medium my-0 break-words">
|
||||
{abiFunction.name}
|
||||
<InheritanceTooltip inheritedFrom={inheritedFrom} />
|
||||
</p>
|
||||
{inputs}
|
||||
{abiFunction.stateMutability === "payable" ? (
|
||||
<div className="flex flex-col gap-1.5 w-full">
|
||||
<div className="flex items-center ml-2">
|
||||
<span className="text-xs font-medium mr-2 leading-none">payable value</span>
|
||||
<span className="block text-xs font-extralight leading-none">wei</span>
|
||||
</div>
|
||||
<IntegerInput
|
||||
value={txValue}
|
||||
onChange={updatedTxValue => {
|
||||
setDisplayedTxResult(undefined);
|
||||
setTxValue(updatedTxValue);
|
||||
}}
|
||||
placeholder="value (wei)"
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="flex justify-between gap-2">
|
||||
{!zeroInputs && (
|
||||
<div className="grow basis-0">{displayedTxResult ? <TxReceipt txResult={displayedTxResult} /> : null}</div>
|
||||
)}
|
||||
<div
|
||||
className={`flex ${
|
||||
writeDisabled &&
|
||||
"tooltip tooltip-bottom tooltip-secondary before:content-[attr(data-tip)] before:-translate-x-1/3 before:left-auto before:transform-none"
|
||||
}`}
|
||||
data-tip={`${writeDisabled && "Wallet not connected or in the wrong network"}`}
|
||||
>
|
||||
<button className="btn btn-secondary btn-sm" disabled={writeDisabled || isPending} onClick={handleWrite}>
|
||||
{isPending && <span className="loading loading-spinner loading-xs"></span>}
|
||||
Send 💸
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{zeroInputs && txResult ? (
|
||||
<div className="grow basis-0">
|
||||
<TxReceipt txResult={txResult} />
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
8
packages/nextjs/app/debug/_components/contract/index.tsx
Normal file
8
packages/nextjs/app/debug/_components/contract/index.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
export * from "./ContractInput";
|
||||
export * from "./ContractUI";
|
||||
export * from "./DisplayVariable";
|
||||
export * from "./ReadOnlyFunctionForm";
|
||||
export * from "./TxReceipt";
|
||||
export * from "./utilsContract";
|
||||
export * from "./utilsDisplay";
|
||||
export * from "./WriteOnlyFunctionForm";
|
||||
166
packages/nextjs/app/debug/_components/contract/utilsContract.tsx
Normal file
166
packages/nextjs/app/debug/_components/contract/utilsContract.tsx
Normal file
@@ -0,0 +1,166 @@
|
||||
import { AbiFunction, AbiParameter } from "abitype";
|
||||
import { AbiParameterTuple } from "~~/utils/scaffold-eth/contract";
|
||||
|
||||
/**
|
||||
* Generates a key based on function metadata
|
||||
*/
|
||||
const getFunctionInputKey = (functionName: string, input: AbiParameter, inputIndex: number): string => {
|
||||
const name = input?.name || `input_${inputIndex}_`;
|
||||
return functionName + "_" + name + "_" + input.internalType + "_" + input.type;
|
||||
};
|
||||
|
||||
const isJsonString = (str: string) => {
|
||||
try {
|
||||
JSON.parse(str);
|
||||
return true;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const isBigInt = (str: string) => {
|
||||
if (str.trim().length === 0 || str.startsWith("0")) return false;
|
||||
try {
|
||||
BigInt(str);
|
||||
return true;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Recursive function to deeply parse JSON strings, correctly handling nested arrays and encoded JSON strings
|
||||
const deepParseValues = (value: any): any => {
|
||||
if (typeof value === "string") {
|
||||
// first try with bigInt because we losse precision with JSON.parse
|
||||
if (isBigInt(value)) {
|
||||
return BigInt(value);
|
||||
}
|
||||
|
||||
if (isJsonString(value)) {
|
||||
const parsed = JSON.parse(value);
|
||||
return deepParseValues(parsed);
|
||||
}
|
||||
|
||||
// It's a string but not a JSON string, return as is
|
||||
return value;
|
||||
} else if (Array.isArray(value)) {
|
||||
// If it's an array, recursively parse each element
|
||||
return value.map(element => deepParseValues(element));
|
||||
} else if (typeof value === "object" && value !== null) {
|
||||
// If it's an object, recursively parse each value
|
||||
return Object.entries(value).reduce((acc: any, [key, val]) => {
|
||||
acc[key] = deepParseValues(val);
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
// Handle boolean values represented as strings
|
||||
if (value === "true" || value === "1" || value === "0x1" || value === "0x01" || value === "0x0001") {
|
||||
return true;
|
||||
} else if (value === "false" || value === "0" || value === "0x0" || value === "0x00" || value === "0x0000") {
|
||||
return false;
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
/**
|
||||
* parses form input with array support
|
||||
*/
|
||||
const getParsedContractFunctionArgs = (form: Record<string, any>) => {
|
||||
return Object.keys(form).map(key => {
|
||||
const valueOfArg = form[key];
|
||||
|
||||
// Attempt to deeply parse JSON strings
|
||||
return deepParseValues(valueOfArg);
|
||||
});
|
||||
};
|
||||
|
||||
const getInitialFormState = (abiFunction: AbiFunction) => {
|
||||
const initialForm: Record<string, any> = {};
|
||||
if (!abiFunction.inputs) return initialForm;
|
||||
abiFunction.inputs.forEach((input, inputIndex) => {
|
||||
const key = getFunctionInputKey(abiFunction.name, input, inputIndex);
|
||||
initialForm[key] = "";
|
||||
});
|
||||
return initialForm;
|
||||
};
|
||||
|
||||
const getInitialTupleFormState = (abiTupleParameter: AbiParameterTuple) => {
|
||||
const initialForm: Record<string, any> = {};
|
||||
if (abiTupleParameter.components.length === 0) return initialForm;
|
||||
|
||||
abiTupleParameter.components.forEach((component, componentIndex) => {
|
||||
const key = getFunctionInputKey(abiTupleParameter.name || "tuple", component, componentIndex);
|
||||
initialForm[key] = "";
|
||||
});
|
||||
return initialForm;
|
||||
};
|
||||
|
||||
const getInitialTupleArrayFormState = (abiTupleParameter: AbiParameterTuple) => {
|
||||
const initialForm: Record<string, any> = {};
|
||||
if (abiTupleParameter.components.length === 0) return initialForm;
|
||||
abiTupleParameter.components.forEach((component, componentIndex) => {
|
||||
const key = getFunctionInputKey("0_" + abiTupleParameter.name || "tuple", component, componentIndex);
|
||||
initialForm[key] = "";
|
||||
});
|
||||
return initialForm;
|
||||
};
|
||||
|
||||
const adjustInput = (input: AbiParameterTuple): AbiParameter => {
|
||||
if (input.type.startsWith("tuple[")) {
|
||||
const depth = (input.type.match(/\[\]/g) || []).length;
|
||||
return {
|
||||
...input,
|
||||
components: transformComponents(input.components, depth, {
|
||||
internalType: input.internalType || "struct",
|
||||
name: input.name,
|
||||
}),
|
||||
};
|
||||
} else if (input.components) {
|
||||
return {
|
||||
...input,
|
||||
components: input.components.map(value => adjustInput(value as AbiParameterTuple)),
|
||||
};
|
||||
}
|
||||
return input;
|
||||
};
|
||||
|
||||
const transformComponents = (
|
||||
components: readonly AbiParameter[],
|
||||
depth: number,
|
||||
parentComponentData: { internalType?: string; name?: string },
|
||||
): AbiParameter[] => {
|
||||
// Base case: if depth is 1 or no components, return the original components
|
||||
if (depth === 1 || !components) {
|
||||
return [...components];
|
||||
}
|
||||
|
||||
// Recursive case: wrap components in an additional tuple layer
|
||||
const wrappedComponents: AbiParameter = {
|
||||
internalType: `${parentComponentData.internalType || "struct"}`.replace(/\[\]/g, "") + "[]".repeat(depth - 1),
|
||||
name: `${parentComponentData.name || "tuple"}`,
|
||||
type: `tuple${"[]".repeat(depth - 1)}`,
|
||||
components: transformComponents(components, depth - 1, parentComponentData),
|
||||
};
|
||||
|
||||
return [wrappedComponents];
|
||||
};
|
||||
|
||||
const transformAbiFunction = (abiFunction: AbiFunction): AbiFunction => {
|
||||
return {
|
||||
...abiFunction,
|
||||
inputs: abiFunction.inputs.map(value => adjustInput(value as AbiParameterTuple)),
|
||||
};
|
||||
};
|
||||
|
||||
export {
|
||||
getFunctionInputKey,
|
||||
getInitialFormState,
|
||||
getParsedContractFunctionArgs,
|
||||
getInitialTupleFormState,
|
||||
getInitialTupleArrayFormState,
|
||||
transformAbiFunction,
|
||||
};
|
||||
114
packages/nextjs/app/debug/_components/contract/utilsDisplay.tsx
Normal file
114
packages/nextjs/app/debug/_components/contract/utilsDisplay.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
import { ReactElement, useState } from "react";
|
||||
import { TransactionBase, TransactionReceipt, formatEther, isAddress, isHex } from "viem";
|
||||
import { ArrowsRightLeftIcon } from "@heroicons/react/24/solid";
|
||||
import { Address } from "~~/components/scaffold-eth";
|
||||
import { replacer } from "~~/utils/scaffold-eth/common";
|
||||
|
||||
type DisplayContent =
|
||||
| string
|
||||
| number
|
||||
| bigint
|
||||
| Record<string, any>
|
||||
| TransactionBase
|
||||
| TransactionReceipt
|
||||
| undefined
|
||||
| unknown;
|
||||
|
||||
type ResultFontSize = "sm" | "base" | "xs" | "lg" | "xl" | "2xl" | "3xl";
|
||||
|
||||
export const displayTxResult = (
|
||||
displayContent: DisplayContent | DisplayContent[],
|
||||
fontSize: ResultFontSize = "base",
|
||||
): string | ReactElement | number => {
|
||||
if (displayContent == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (typeof displayContent === "bigint") {
|
||||
return <NumberDisplay value={displayContent} />;
|
||||
}
|
||||
|
||||
if (typeof displayContent === "string") {
|
||||
if (isAddress(displayContent)) {
|
||||
return <Address address={displayContent} size={fontSize} onlyEnsOrAddress />;
|
||||
}
|
||||
|
||||
if (isHex(displayContent)) {
|
||||
return displayContent; // don't add quotes
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(displayContent)) {
|
||||
return <ArrayDisplay values={displayContent} size={fontSize} />;
|
||||
}
|
||||
|
||||
if (typeof displayContent === "object") {
|
||||
return <StructDisplay struct={displayContent} size={fontSize} />;
|
||||
}
|
||||
|
||||
return JSON.stringify(displayContent, replacer, 2);
|
||||
};
|
||||
|
||||
const NumberDisplay = ({ value }: { value: bigint }) => {
|
||||
const [isEther, setIsEther] = useState(false);
|
||||
|
||||
const asNumber = Number(value);
|
||||
if (asNumber <= Number.MAX_SAFE_INTEGER && asNumber >= Number.MIN_SAFE_INTEGER) {
|
||||
return String(value);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-baseline">
|
||||
{isEther ? "Ξ" + formatEther(value) : String(value)}
|
||||
<span
|
||||
className="tooltip tooltip-secondary font-sans ml-2"
|
||||
data-tip={isEther ? "Multiply by 1e18" : "Divide by 1e18"}
|
||||
>
|
||||
<button className="btn btn-ghost btn-circle btn-xs" onClick={() => setIsEther(!isEther)}>
|
||||
<ArrowsRightLeftIcon className="h-3 w-3 opacity-65" />
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const ObjectFieldDisplay = ({
|
||||
name,
|
||||
value,
|
||||
size,
|
||||
leftPad = true,
|
||||
}: {
|
||||
name: string;
|
||||
value: DisplayContent;
|
||||
size: ResultFontSize;
|
||||
leftPad?: boolean;
|
||||
}) => {
|
||||
return (
|
||||
<div className={`flex flex-row items-baseline ${leftPad ? "ml-4" : ""}`}>
|
||||
<span className="text-base-content/60 mr-2">{name}:</span>
|
||||
<span className="text-base-content">{displayTxResult(value, size)}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ArrayDisplay = ({ values, size }: { values: DisplayContent[]; size: ResultFontSize }) => {
|
||||
return (
|
||||
<div className="flex flex-col gap-y-1">
|
||||
{values.length ? "array" : "[]"}
|
||||
{values.map((v, i) => (
|
||||
<ObjectFieldDisplay key={i} name={`[${i}]`} value={v} size={size} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const StructDisplay = ({ struct, size }: { struct: Record<string, any>; size: ResultFontSize }) => {
|
||||
return (
|
||||
<div className="flex flex-col gap-y-1">
|
||||
struct
|
||||
{Object.entries(struct).map(([k, v]) => (
|
||||
<ObjectFieldDisplay key={k} name={k} value={v} size={size} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
28
packages/nextjs/app/debug/page.tsx
Normal file
28
packages/nextjs/app/debug/page.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import { DebugContracts } from "./_components/DebugContracts";
|
||||
import type { NextPage } from "next";
|
||||
import { getMetadata } from "~~/utils/scaffold-eth/getMetadata";
|
||||
|
||||
export const metadata = getMetadata({
|
||||
title: "Debug Contracts",
|
||||
description: "Debug your deployed 🏗 Scaffold-ETH 2 contracts in an easy way",
|
||||
});
|
||||
|
||||
const Debug: NextPage = () => {
|
||||
return (
|
||||
<>
|
||||
<DebugContracts />
|
||||
<div className="text-center mt-8 bg-secondary p-10">
|
||||
<h1 className="text-4xl my-0">Debug Contracts</h1>
|
||||
<p className="text-neutral">
|
||||
You can debug & interact with your deployed contracts here.
|
||||
<br /> Check{" "}
|
||||
<code className="italic bg-base-300 text-base font-bold [word-spacing:-0.5rem] px-1">
|
||||
packages / nextjs / app / debug / page.tsx
|
||||
</code>{" "}
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Debug;
|
||||
198
packages/nextjs/app/dex/_components/Curve.tsx
Normal file
198
packages/nextjs/app/dex/_components/Curve.tsx
Normal file
@@ -0,0 +1,198 @@
|
||||
import { FC, useEffect, useRef } from "react";
|
||||
|
||||
const drawArrow = (ctx: CanvasRenderingContext2D, x1: number, y1: number, x2: number, y2: number) => {
|
||||
const [dx, dy] = [x1 - x2, y1 - y2];
|
||||
const norm = Math.sqrt(dx * dx + dy * dy);
|
||||
const [udx, udy] = [dx / norm, dy / norm];
|
||||
const size = norm / 7;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x1, y1);
|
||||
ctx.lineTo(x2, y2);
|
||||
ctx.stroke();
|
||||
ctx.moveTo(x2, y2);
|
||||
ctx.lineTo(x2 + udx * size - udy * size, y2 + udx * size + udy * size);
|
||||
ctx.moveTo(x2, y2);
|
||||
ctx.lineTo(x2 + udx * size + udy * size, y2 - udx * size + udy * size);
|
||||
ctx.stroke();
|
||||
};
|
||||
|
||||
export interface ICurveProps {
|
||||
ethReserve: number;
|
||||
tokenReserve: number;
|
||||
addingEth: number;
|
||||
addingToken: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
export const Curve: FC<ICurveProps> = (props: ICurveProps) => {
|
||||
const ref = useRef<HTMLCanvasElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = ref.current;
|
||||
if (!canvas) {
|
||||
return;
|
||||
}
|
||||
|
||||
const textSize = 12;
|
||||
|
||||
const width = canvas.width;
|
||||
const height = canvas.height;
|
||||
|
||||
if (props.ethReserve && props.tokenReserve) {
|
||||
const k = props.ethReserve * props.tokenReserve;
|
||||
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (ctx == null) {
|
||||
return;
|
||||
}
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
|
||||
let maxX = k / (props.ethReserve / 4);
|
||||
let minX = 0;
|
||||
|
||||
if (props.addingEth || props.addingToken) {
|
||||
maxX = k / (props.ethReserve * 0.4);
|
||||
//maxX = k/(props.ethReserve*0.8)
|
||||
minX = k / Math.max(0, 500 - props.ethReserve);
|
||||
}
|
||||
|
||||
const maxY = (maxX * height) / width;
|
||||
const minY = (minX * height) / width;
|
||||
|
||||
const plotX = (x: number) => {
|
||||
return ((x - minX) / (maxX - minX)) * width;
|
||||
};
|
||||
|
||||
const plotY = (y: number) => {
|
||||
return height - ((y - minY) / (maxY - minY)) * height;
|
||||
};
|
||||
ctx.strokeStyle = "#000000";
|
||||
ctx.fillStyle = "#000000";
|
||||
ctx.font = textSize + "px Arial";
|
||||
// +Y axis
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(plotX(minX), plotY(0));
|
||||
ctx.lineTo(plotX(minX), plotY(maxY));
|
||||
ctx.stroke();
|
||||
// +X axis
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(plotX(0), plotY(minY));
|
||||
ctx.lineTo(plotX(maxX), plotY(minY));
|
||||
ctx.stroke();
|
||||
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
let first = true;
|
||||
for (let x = minX; x <= maxX; x += maxX / width) {
|
||||
/////
|
||||
const y = k / x;
|
||||
/////
|
||||
if (first) {
|
||||
ctx.moveTo(plotX(x), plotY(y));
|
||||
first = false;
|
||||
} else {
|
||||
ctx.lineTo(plotX(x), plotY(y));
|
||||
}
|
||||
}
|
||||
ctx.stroke();
|
||||
|
||||
ctx.lineWidth = 1;
|
||||
|
||||
if (props.addingEth) {
|
||||
const newEthReserve = props.ethReserve + parseFloat(props.addingEth.toString());
|
||||
|
||||
ctx.fillStyle = "#bbbbbb";
|
||||
ctx.beginPath();
|
||||
ctx.arc(plotX(newEthReserve), plotY(k / newEthReserve), 5, 0, 2 * Math.PI);
|
||||
ctx.fill();
|
||||
|
||||
ctx.strokeStyle = "#009900";
|
||||
drawArrow(
|
||||
ctx,
|
||||
plotX(props.ethReserve),
|
||||
plotY(props.tokenReserve),
|
||||
plotX(newEthReserve),
|
||||
plotY(props.tokenReserve),
|
||||
);
|
||||
|
||||
ctx.fillStyle = "#000000";
|
||||
ctx.fillText(
|
||||
"" + props.addingEth + " ETH input",
|
||||
plotX(props.ethReserve) + textSize,
|
||||
plotY(props.tokenReserve) - textSize,
|
||||
);
|
||||
|
||||
ctx.strokeStyle = "#990000";
|
||||
drawArrow(ctx, plotX(newEthReserve), plotY(props.tokenReserve), plotX(newEthReserve), plotY(k / newEthReserve));
|
||||
|
||||
const amountGained = Math.round((10000 * (props.addingEth * props.tokenReserve)) / newEthReserve) / 10000;
|
||||
ctx.fillStyle = "#000000";
|
||||
ctx.fillText(
|
||||
"" + amountGained + " 🎈 output (-0.3% fee)",
|
||||
plotX(newEthReserve) + textSize,
|
||||
plotY(k / newEthReserve),
|
||||
);
|
||||
} else if (props.addingToken) {
|
||||
const newTokenReserve = props.tokenReserve + parseFloat(props.addingToken.toString());
|
||||
|
||||
ctx.fillStyle = "#bbbbbb";
|
||||
ctx.beginPath();
|
||||
ctx.arc(plotX(k / newTokenReserve), plotY(newTokenReserve), 5, 0, 2 * Math.PI);
|
||||
ctx.fill();
|
||||
|
||||
//console.log("newTokenReserve",newTokenReserve)
|
||||
ctx.strokeStyle = "#990000";
|
||||
drawArrow(
|
||||
ctx,
|
||||
plotX(props.ethReserve),
|
||||
plotY(props.tokenReserve),
|
||||
plotX(props.ethReserve),
|
||||
plotY(newTokenReserve),
|
||||
);
|
||||
|
||||
ctx.fillStyle = "#000000";
|
||||
ctx.fillText(
|
||||
"" + props.addingToken + " 🎈 input",
|
||||
plotX(props.ethReserve) + textSize,
|
||||
plotY(props.tokenReserve),
|
||||
);
|
||||
|
||||
ctx.strokeStyle = "#009900";
|
||||
drawArrow(
|
||||
ctx,
|
||||
plotX(props.ethReserve),
|
||||
plotY(newTokenReserve),
|
||||
plotX(k / newTokenReserve),
|
||||
plotY(newTokenReserve),
|
||||
);
|
||||
|
||||
const amountGained = Math.round((10000 * (props.addingToken * props.ethReserve)) / newTokenReserve) / 10000;
|
||||
//console.log("amountGained",amountGained)
|
||||
ctx.fillStyle = "#000000";
|
||||
ctx.fillText(
|
||||
"" + amountGained + " ETH output (-0.3% fee)",
|
||||
plotX(k / newTokenReserve) + textSize,
|
||||
plotY(newTokenReserve) - textSize,
|
||||
);
|
||||
}
|
||||
|
||||
ctx.fillStyle = "#0000FF";
|
||||
ctx.beginPath();
|
||||
ctx.arc(plotX(props.ethReserve), plotY(props.tokenReserve), 5, 0, 2 * Math.PI);
|
||||
ctx.fill();
|
||||
}
|
||||
}, [props]);
|
||||
|
||||
return (
|
||||
<div style={{ position: "relative", width: props.width, height: props.height }}>
|
||||
<canvas style={{ position: "absolute", left: 0, top: 0 }} ref={ref} width={props.width} height={props.height} />
|
||||
<div style={{ position: "absolute", left: "20%", bottom: -20 }}>-- ETH Reserve --{">"}</div>
|
||||
<div
|
||||
style={{ position: "absolute", left: -20, bottom: "20%", transform: "rotate(-90deg)", transformOrigin: "0 0" }}
|
||||
>
|
||||
-- Token Reserve --{">"}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
1
packages/nextjs/app/dex/_components/index.tsx
Normal file
1
packages/nextjs/app/dex/_components/index.tsx
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./Curve";
|
||||
298
packages/nextjs/app/dex/page.tsx
Normal file
298
packages/nextjs/app/dex/page.tsx
Normal file
@@ -0,0 +1,298 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { Curve } from "./_components";
|
||||
import type { NextPage } from "next";
|
||||
import { Address as AddressType, formatEther, isAddress, parseEther } from "viem";
|
||||
import { useAccount } from "wagmi";
|
||||
import { Address, AddressInput, Balance, EtherInput, IntegerInput } from "~~/components/scaffold-eth";
|
||||
import { useDeployedContractInfo, useScaffoldReadContract, useScaffoldWriteContract } from "~~/hooks/scaffold-eth";
|
||||
import { useWatchBalance } from "~~/hooks/scaffold-eth/useWatchBalance";
|
||||
|
||||
// REGEX for number inputs (only allow numbers and a single decimal point)
|
||||
const NUMBER_REGEX = /^\.?\d+\.?\d*$/;
|
||||
|
||||
const Dex: NextPage = () => {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [ethToTokenAmount, setEthToTokenAmount] = useState("");
|
||||
const [tokenToETHAmount, setTokenToETHAmount] = useState("");
|
||||
const [depositAmount, setDepositAmount] = useState("");
|
||||
const [withdrawAmount, setWithdrawAmount] = useState("");
|
||||
const [approveSpender, setApproveSpender] = useState("");
|
||||
const [approveAmount, setApproveAmount] = useState("");
|
||||
const [accountBalanceOf, setAccountBalanceOf] = useState("");
|
||||
|
||||
const { data: DEXInfo } = useDeployedContractInfo({ contractName: "DEX" });
|
||||
const { data: BalloonsInfo } = useDeployedContractInfo({ contractName: "Balloons" });
|
||||
const { address: connectedAccount } = useAccount();
|
||||
|
||||
const { data: DEXBalloonBalance } = useScaffoldReadContract({
|
||||
contractName: "Balloons",
|
||||
functionName: "balanceOf",
|
||||
args: [DEXInfo?.address?.toString()],
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (DEXBalloonBalance !== undefined) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [DEXBalloonBalance]);
|
||||
|
||||
const { data: DEXtotalLiquidity } = useScaffoldReadContract({
|
||||
contractName: "DEX",
|
||||
functionName: "totalLiquidity",
|
||||
});
|
||||
|
||||
const { writeContractAsync: writeDexContractAsync } = useScaffoldWriteContract({ contractName: "DEX" });
|
||||
|
||||
const { writeContractAsync: writeBalloonsContractAsync } = useScaffoldWriteContract({ contractName: "Balloons" });
|
||||
|
||||
const { data: balanceOfWrite } = useScaffoldReadContract({
|
||||
contractName: "Balloons",
|
||||
functionName: "balanceOf",
|
||||
args: [accountBalanceOf as AddressType],
|
||||
query: {
|
||||
enabled: isAddress(accountBalanceOf),
|
||||
},
|
||||
});
|
||||
|
||||
const { data: contractBalance } = useScaffoldReadContract({
|
||||
contractName: "Balloons",
|
||||
functionName: "balanceOf",
|
||||
args: [DEXInfo?.address],
|
||||
});
|
||||
|
||||
const { data: userBalloons } = useScaffoldReadContract({
|
||||
contractName: "Balloons",
|
||||
functionName: "balanceOf",
|
||||
args: [connectedAccount],
|
||||
});
|
||||
|
||||
const { data: userLiquidity } = useScaffoldReadContract({
|
||||
contractName: "DEX",
|
||||
functionName: "getLiquidity",
|
||||
args: [connectedAccount],
|
||||
});
|
||||
|
||||
const { data: contractETHBalance } = useWatchBalance({ address: DEXInfo?.address });
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1 className="text-center mb-4 mt-5">
|
||||
<span className="block text-xl text-right mr-7">
|
||||
🎈: {parseFloat(formatEther(userBalloons || 0n)).toFixed(4)}
|
||||
</span>
|
||||
<span className="block text-xl text-right mr-7">
|
||||
💦💦: {parseFloat(formatEther(userLiquidity || 0n)).toFixed(4)}
|
||||
</span>
|
||||
<span className="block text-2xl mb-2">SpeedRunEthereum</span>
|
||||
<span className="block text-4xl font-bold">Challenge: ⚖️ Build a DEX </span>
|
||||
</h1>
|
||||
<div className="items-start pt-10 grid grid-cols-1 md:grid-cols-2 content-start">
|
||||
<div className="px-5 py-5">
|
||||
<div className="bg-base-100 shadow-lg shadow-secondary border-8 border-secondary rounded-xl p-8 m-8">
|
||||
<div className="flex flex-col text-center">
|
||||
<span className="text-3xl font-semibold mb-2">DEX Contract</span>
|
||||
<span className="block text-2xl mb-2 mx-auto">
|
||||
<Address size="xl" address={DEXInfo?.address} />
|
||||
</span>
|
||||
<span className="flex flex-row mx-auto mt-5">
|
||||
{" "}
|
||||
<Balance className="text-xl" address={DEXInfo?.address} /> ⚖️
|
||||
{isLoading ? (
|
||||
<span>Loading...</span>
|
||||
) : (
|
||||
<span className="pl-8 text-xl">🎈 {parseFloat(formatEther(DEXBalloonBalance || 0n)).toFixed(4)}</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="py-3 px-4">
|
||||
<div className="flex mb-4 justify-center items-center">
|
||||
<span className="w-1/2">
|
||||
ethToToken{" "}
|
||||
<EtherInput
|
||||
value={ethToTokenAmount}
|
||||
onChange={value => {
|
||||
setTokenToETHAmount("");
|
||||
setEthToTokenAmount(value);
|
||||
}}
|
||||
name="ethToToken"
|
||||
/>
|
||||
</span>
|
||||
<button
|
||||
className="btn btn-primary h-[2.2rem] min-h-[2.2rem] mt-6 mx-5"
|
||||
onClick={async () => {
|
||||
try {
|
||||
await writeDexContractAsync({
|
||||
functionName: "ethToToken",
|
||||
value: NUMBER_REGEX.test(ethToTokenAmount) ? parseEther(ethToTokenAmount) : 0n,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Error calling ethToToken function", err);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Send
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex justify-center items-center">
|
||||
<span className="w-1/2">
|
||||
tokenToETH{" "}
|
||||
<IntegerInput
|
||||
value={tokenToETHAmount}
|
||||
onChange={value => {
|
||||
setEthToTokenAmount("");
|
||||
setTokenToETHAmount(value.toString());
|
||||
}}
|
||||
name="tokenToETH"
|
||||
disableMultiplyBy1e18
|
||||
/>
|
||||
</span>
|
||||
<button
|
||||
className="btn btn-primary h-[2.2rem] min-h-[2.2rem] mt-6 mx-5"
|
||||
onClick={async () => {
|
||||
try {
|
||||
await writeDexContractAsync({
|
||||
functionName: "tokenToEth",
|
||||
// @ts-expect-error - Show error on frontend while sending, if user types invalid number
|
||||
args: [NUMBER_REGEX.test(tokenToETHAmount) ? parseEther(tokenToETHAmount) : tokenToETHAmount],
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Error calling tokenToEth function", err);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Send
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-center text-primary-content text-xl mt-8 -ml-8">
|
||||
Liquidity ({DEXtotalLiquidity ? parseFloat(formatEther(DEXtotalLiquidity || 0n)).toFixed(4) : "None"})
|
||||
</p>
|
||||
<div className="px-4 py-3">
|
||||
<div className="flex mb-4 justify-center items-center">
|
||||
<span className="w-1/2">
|
||||
Deposit <EtherInput value={depositAmount} onChange={value => setDepositAmount(value)} />
|
||||
</span>
|
||||
<button
|
||||
className="btn btn-primary h-[2.2rem] min-h-[2.2rem] mt-6 mx-5"
|
||||
onClick={async () => {
|
||||
try {
|
||||
await writeDexContractAsync({
|
||||
functionName: "deposit",
|
||||
value: NUMBER_REGEX.test(depositAmount) ? parseEther(depositAmount) : 0n,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Error calling deposit function", err);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Send
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center items-center">
|
||||
<span className="w-1/2">
|
||||
Withdraw <EtherInput value={withdrawAmount} onChange={value => setWithdrawAmount(value)} />
|
||||
</span>
|
||||
<button
|
||||
className="btn btn-primary h-[2.2rem] min-h-[2.2rem] mt-6 mx-5"
|
||||
onClick={async () => {
|
||||
try {
|
||||
await writeDexContractAsync({
|
||||
functionName: "withdraw",
|
||||
// @ts-expect-error - Show error on frontend while sending, if user types invalid number
|
||||
args: [NUMBER_REGEX.test(withdrawAmount) ? parseEther(withdrawAmount) : withdrawAmount],
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Error calling withdraw function", err);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Send
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 bg-base-100 shadow-lg shadow-secondary border-8 border-secondary rounded-xl py-5 p-8 m-8">
|
||||
<div className="flex flex-col text-center mt-2 mb-4 px-4">
|
||||
<span className="block text-3xl font-semibold mb-2">Balloons</span>
|
||||
<span className="mx-auto">
|
||||
<Address size="xl" address={BalloonsInfo?.address} />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className=" px-4 py-3">
|
||||
<div className="flex flex-col gap-4 mb-4 justify-center items-center">
|
||||
<span className="w-1/2">
|
||||
Approve{" "}
|
||||
<AddressInput
|
||||
value={approveSpender ?? ""}
|
||||
onChange={value => setApproveSpender(value)}
|
||||
placeholder="Address Spender"
|
||||
/>
|
||||
</span>
|
||||
<span className="w-1/2">
|
||||
<IntegerInput
|
||||
value={approveAmount}
|
||||
onChange={value => setApproveAmount(value.toString())}
|
||||
placeholder="Amount"
|
||||
disableMultiplyBy1e18
|
||||
/>
|
||||
</span>
|
||||
<button
|
||||
className="btn btn-primary h-[2.2rem] min-h-[2.2rem] mt-auto"
|
||||
onClick={async () => {
|
||||
try {
|
||||
await writeBalloonsContractAsync({
|
||||
functionName: "approve",
|
||||
args: [
|
||||
approveSpender as AddressType,
|
||||
// @ts-expect-error - Show error on frontend while sending, if user types invalid number
|
||||
NUMBER_REGEX.test(approveAmount) ? parseEther(approveAmount) : approveAmount,
|
||||
],
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Error calling approve function", err);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Send
|
||||
</button>
|
||||
<span className="w-1/2">
|
||||
balanceOf{" "}
|
||||
<AddressInput
|
||||
value={accountBalanceOf}
|
||||
onChange={value => setAccountBalanceOf(value)}
|
||||
placeholder="address Account"
|
||||
/>
|
||||
</span>
|
||||
{balanceOfWrite === undefined ? (
|
||||
<h1></h1>
|
||||
) : (
|
||||
<span className="font-bold bg-primary px-3 rounded-2xl">
|
||||
BAL Balance: {parseFloat(formatEther(balanceOfWrite || 0n)).toFixed(4)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mx-auto p-8 m-8 md:sticky md:top-0">
|
||||
<Curve
|
||||
addingEth={ethToTokenAmount !== "" ? parseFloat(ethToTokenAmount.toString()) : 0}
|
||||
addingToken={tokenToETHAmount !== "" ? parseFloat(tokenToETHAmount.toString()) : 0}
|
||||
ethReserve={parseFloat(formatEther(contractETHBalance?.value || 0n))}
|
||||
tokenReserve={parseFloat(formatEther(contractBalance || 0n))}
|
||||
width={500}
|
||||
height={500}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dex;
|
||||
216
packages/nextjs/app/events/page.tsx
Normal file
216
packages/nextjs/app/events/page.tsx
Normal file
@@ -0,0 +1,216 @@
|
||||
"use client";
|
||||
|
||||
import type { NextPage } from "next";
|
||||
import { formatEther } from "viem";
|
||||
import { Address } from "~~/components/scaffold-eth";
|
||||
import { useScaffoldEventHistory } from "~~/hooks/scaffold-eth";
|
||||
|
||||
const Events: NextPage = () => {
|
||||
const { data: EthToTokenEvents, isLoading: isEthToTokenEventsLoading } = useScaffoldEventHistory({
|
||||
contractName: "DEX",
|
||||
eventName: "EthToTokenSwap",
|
||||
});
|
||||
|
||||
const { data: tokenToEthEvents, isLoading: isTokenToEthEventsLoading } = useScaffoldEventHistory({
|
||||
contractName: "DEX",
|
||||
eventName: "TokenToEthSwap",
|
||||
});
|
||||
|
||||
const { data: liquidityProvidedEvents, isLoading: isLiquidityProvidedEventsLoading } = useScaffoldEventHistory({
|
||||
contractName: "DEX",
|
||||
eventName: "LiquidityProvided",
|
||||
});
|
||||
|
||||
const { data: liquidityRemovedEvents, isLoading: isLiquidityRemovedEventsLoading } = useScaffoldEventHistory({
|
||||
contractName: "DEX",
|
||||
eventName: "LiquidityRemoved",
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center flex-col flex-grow pt-10">
|
||||
{isEthToTokenEventsLoading ? (
|
||||
<div className="flex justify-center items-center mt-10">
|
||||
<span className="loading loading-spinner loading-lg"></span>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<div className="text-center mb-4">
|
||||
<span className="block text-2xl font-bold">ETH To Balloons Events</span>
|
||||
</div>
|
||||
<div className="overflow-x-auto shadow-lg">
|
||||
<table className="table table-zebra w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="bg-primary">Address</th>
|
||||
<th className="bg-primary">Amount of ETH in</th>
|
||||
<th className="bg-primary">Amount of Balloons out</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{!EthToTokenEvents || EthToTokenEvents.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={3} className="text-center">
|
||||
No events found
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
EthToTokenEvents?.map((event, index) => {
|
||||
return (
|
||||
<tr key={index}>
|
||||
<td className="text-center">
|
||||
<Address address={event.args.swapper} />
|
||||
</td>
|
||||
<td>{parseFloat(formatEther(event.args.ethInput || 0n)).toFixed(4)}</td>
|
||||
<td>{parseFloat(formatEther(event.args.tokenOutput || 0n)).toFixed(4)}</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isTokenToEthEventsLoading ? (
|
||||
<div className="flex justify-center items-center mt-10">
|
||||
<span className="loading loading-spinner loading-lg"></span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-8">
|
||||
<div className="text-center mb-4">
|
||||
<span className="block text-2xl font-bold">Balloons To ETH Events</span>
|
||||
</div>
|
||||
<div className="overflow-x-auto shadow-lg">
|
||||
<table className="table table-zebra w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="bg-primary">Address</th>
|
||||
<th className="bg-primary">Amount of Balloons In</th>
|
||||
<th className="bg-primary">Amount of ETH Out</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{!tokenToEthEvents || tokenToEthEvents.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={3} className="text-center">
|
||||
No events found
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
tokenToEthEvents?.map((event, index) => {
|
||||
return (
|
||||
<tr key={index}>
|
||||
<td className="text-center">
|
||||
<Address address={event.args.swapper} />
|
||||
</td>
|
||||
<td>{parseFloat(formatEther(event.args.tokensInput || 0n)).toFixed(4)}</td>
|
||||
<td>{parseFloat(formatEther(event.args.ethOutput || 0n)).toFixed(4)}</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isLiquidityProvidedEventsLoading ? (
|
||||
<div className="flex justify-center items-center mt-10">
|
||||
<span className="loading loading-spinner loading-lg"></span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-8">
|
||||
<div className="text-center mb-4">
|
||||
<span className="block text-2xl font-bold">Liquidity Provided Events</span>
|
||||
</div>
|
||||
<div className="overflow-x-auto shadow-lg">
|
||||
<table className="table table-zebra w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="bg-primary">Address</th>
|
||||
<th className="bg-primary">Amount of ETH In</th>
|
||||
<th className="bg-primary">Amount of Balloons In</th>
|
||||
<th className="bg-primary">Lİquidity Minted</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{!liquidityProvidedEvents || liquidityProvidedEvents.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={4} className="text-center">
|
||||
No events found
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
liquidityProvidedEvents?.map((event, index) => {
|
||||
return (
|
||||
<tr key={index}>
|
||||
<td className="text-center">
|
||||
<Address address={event.args.liquidityProvider} />
|
||||
</td>
|
||||
<td>{parseFloat(formatEther(event.args.ethInput || 0n)).toFixed(4)}</td>
|
||||
<td>{parseFloat(formatEther(event.args.tokensInput || 0n)).toFixed(4)}</td>
|
||||
<td>{parseFloat(formatEther(event.args.liquidityMinted || 0n)).toFixed(4)}</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isLiquidityRemovedEventsLoading ? (
|
||||
<div className="flex justify-center items-center mt-10">
|
||||
<span className="loading loading-spinner loading-lg"></span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-8 mb-8">
|
||||
<div className="text-center mb-4">
|
||||
<span className="block text-2xl font-bold">Liquidity Removed Events</span>
|
||||
</div>
|
||||
<div className="overflow-x-auto shadow-lg mb-5">
|
||||
<table className="table table-zebra w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="bg-primary">Address</th>
|
||||
<th className="bg-primary">Amount of ETH Out</th>
|
||||
<th className="bg-primary">Amount of Balloons Out</th>
|
||||
<th className="bg-primary">Liquidity Withdrawn</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{!liquidityRemovedEvents || liquidityRemovedEvents.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={4} className="text-center">
|
||||
No events found
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
liquidityRemovedEvents?.map((event, index) => {
|
||||
return (
|
||||
<tr key={index}>
|
||||
<td className="text-center">
|
||||
<Address address={event.args.liquidityRemover} />
|
||||
</td>
|
||||
<td>{parseFloat(formatEther(event.args.ethOutput || 0n)).toFixed(4)}</td>
|
||||
<td>{parseFloat(formatEther(event.args.tokensOutput || 0n)).toFixed(4)}</td>
|
||||
<td>{parseFloat(formatEther(event.args.liquidityWithdrawn || 0n)).toFixed(4)}</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Events;
|
||||
30
packages/nextjs/app/layout.tsx
Normal file
30
packages/nextjs/app/layout.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import { Space_Grotesk } from "next/font/google";
|
||||
import "@rainbow-me/rainbowkit/styles.css";
|
||||
import { ScaffoldEthAppWithProviders } from "~~/components/ScaffoldEthAppWithProviders";
|
||||
import { ThemeProvider } from "~~/components/ThemeProvider";
|
||||
import "~~/styles/globals.css";
|
||||
import { getMetadata } from "~~/utils/scaffold-eth/getMetadata";
|
||||
|
||||
const spaceGrotesk = Space_Grotesk({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-space-grotesk",
|
||||
});
|
||||
|
||||
export const metadata = getMetadata({
|
||||
title: "Dex | SpeedRunEthereum",
|
||||
description: "Built with 🏗 Scaffold-ETH 2",
|
||||
});
|
||||
|
||||
const ScaffoldEthApp = ({ children }: { children: React.ReactNode }) => {
|
||||
return (
|
||||
<html suppressHydrationWarning className={`${spaceGrotesk.variable} font-space-grotesk`}>
|
||||
<body>
|
||||
<ThemeProvider enableSystem>
|
||||
<ScaffoldEthAppWithProviders>{children}</ScaffoldEthAppWithProviders>
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
};
|
||||
|
||||
export default ScaffoldEthApp;
|
||||
16
packages/nextjs/app/not-found.tsx
Normal file
16
packages/nextjs/app/not-found.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import Link from "next/link";
|
||||
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<div className="flex items-center h-full flex-1 justify-center bg-base-200">
|
||||
<div className="text-center">
|
||||
<h1 className="text-6xl font-bold m-0 mb-1">404</h1>
|
||||
<h2 className="text-2xl font-semibold m-0">Page Not Found</h2>
|
||||
<p className="text-base-content/70 m-0 mb-4">The page you're looking for doesn't exist.</p>
|
||||
<Link href="/" className="btn btn-primary">
|
||||
Go Home
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
99
packages/nextjs/app/page.tsx
Normal file
99
packages/nextjs/app/page.tsx
Normal file
@@ -0,0 +1,99 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import type { NextPage } from "next";
|
||||
import { useAccount } from "wagmi";
|
||||
import { BugAntIcon, MagnifyingGlassIcon } from "@heroicons/react/24/outline";
|
||||
import { Address } from "~~/components/scaffold-eth";
|
||||
|
||||
const Home: NextPage = () => {
|
||||
const { address: connectedAddress } = useAccount();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center flex-col grow pt-10">
|
||||
<div className="px-5">
|
||||
<h1 className="text-center">
|
||||
<span className="block text-2xl mb-2">Welcome to</span>
|
||||
<span className="block text-4xl font-bold">Scaffold-ETH 2</span>
|
||||
<span className="block text-xl font-bold">(SpeedRunEthereum Challenge: Dex extension)</span>
|
||||
</h1>
|
||||
<div className="flex justify-center items-center space-x-2 flex-col">
|
||||
<p className="my-2 font-medium">Connected Address:</p>
|
||||
<Address address={connectedAddress} />
|
||||
</div>
|
||||
|
||||
<div className="flex items-center flex-col flex-grow pt-10">
|
||||
<div className="px-5">
|
||||
<h1 className="text-center mb-6">
|
||||
<span className="block text-2xl mb-2">SpeedRunEthereum</span>
|
||||
<span className="block text-4xl font-bold">Challenge: ⚖️ Build a DEX</span>
|
||||
</h1>
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<Image
|
||||
src="/hero.png"
|
||||
width="727"
|
||||
height="231"
|
||||
alt="challenge banner"
|
||||
className="rounded-xl border-4 border-primary"
|
||||
/>
|
||||
<div className="max-w-3xl">
|
||||
<p className="text-center text-lg mt-8">
|
||||
This challenge will help you build/understand a simple decentralized exchange, with one token-pair
|
||||
(ERC20 BALLOONS ($BAL) and ETH). This repo is an updated version of the{" "}
|
||||
<a
|
||||
href="https://medium.com/@austin_48503/%EF%B8%8F-minimum-viable-exchange-d84f30bd0c90"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="underline"
|
||||
>
|
||||
original tutorial
|
||||
</a>{" "}
|
||||
and challenge repos before it. Please read the intro for a background on what we are building first!
|
||||
</p>
|
||||
<p className="text-center text-lg">
|
||||
🌟 The final deliverable is an app that allows users to seamlessly trade ERC20 BALLOONS ($BAL) with
|
||||
ETH in a decentralized manner. Users will be able to connect their wallets, view their token
|
||||
balances, and buy or sell their tokens according to a price formula! Submit the url on{" "}
|
||||
<a href="https://speedrunethereum.com/" target="_blank" rel="noreferrer" className="underline">
|
||||
SpeedRunEthereum.com
|
||||
</a>{" "}
|
||||
!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grow bg-base-300 w-full mt-16 px-8 py-12">
|
||||
<div className="flex justify-center items-center gap-12 flex-col md:flex-row">
|
||||
<div className="flex flex-col bg-base-100 px-10 py-10 text-center items-center max-w-xs rounded-3xl">
|
||||
<BugAntIcon className="h-8 w-8 fill-secondary" />
|
||||
<p>
|
||||
Tinker with your smart contract using the{" "}
|
||||
<Link href="/debug" passHref className="link">
|
||||
Debug Contracts
|
||||
</Link>{" "}
|
||||
tab.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col bg-base-100 px-10 py-10 text-center items-center max-w-xs rounded-3xl">
|
||||
<MagnifyingGlassIcon className="h-8 w-8 fill-secondary" />
|
||||
<p>
|
||||
Explore your local transactions with the{" "}
|
||||
<Link href="/blockexplorer" passHref className="link">
|
||||
Block Explorer
|
||||
</Link>{" "}
|
||||
tab.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Home;
|
||||
80
packages/nextjs/components/Footer.tsx
Normal file
80
packages/nextjs/components/Footer.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
import React from "react";
|
||||
import Link from "next/link";
|
||||
import { hardhat } from "viem/chains";
|
||||
import { CurrencyDollarIcon, MagnifyingGlassIcon } from "@heroicons/react/24/outline";
|
||||
import { HeartIcon } from "@heroicons/react/24/outline";
|
||||
import { SwitchTheme } from "~~/components/SwitchTheme";
|
||||
import { BuidlGuidlLogo } from "~~/components/assets/BuidlGuidlLogo";
|
||||
import { Faucet } from "~~/components/scaffold-eth";
|
||||
import { useTargetNetwork } from "~~/hooks/scaffold-eth/useTargetNetwork";
|
||||
import { useGlobalState } from "~~/services/store/store";
|
||||
|
||||
/**
|
||||
* Site footer
|
||||
*/
|
||||
export const Footer = () => {
|
||||
const nativeCurrencyPrice = useGlobalState(state => state.nativeCurrency.price);
|
||||
const { targetNetwork } = useTargetNetwork();
|
||||
const isLocalNetwork = targetNetwork.id === hardhat.id;
|
||||
|
||||
return (
|
||||
<div className="min-h-0 py-5 px-1 mb-11 lg:mb-0">
|
||||
<div>
|
||||
<div className="fixed flex justify-between items-center w-full z-10 p-4 bottom-0 left-0 pointer-events-none">
|
||||
<div className="flex flex-col md:flex-row gap-2 pointer-events-auto">
|
||||
{nativeCurrencyPrice > 0 && (
|
||||
<div>
|
||||
<div className="btn btn-primary btn-sm font-normal gap-1 cursor-auto">
|
||||
<CurrencyDollarIcon className="h-4 w-4" />
|
||||
<span>{nativeCurrencyPrice.toFixed(2)}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{isLocalNetwork && (
|
||||
<>
|
||||
<Faucet />
|
||||
<Link href="/blockexplorer" passHref className="btn btn-primary btn-sm font-normal gap-1">
|
||||
<MagnifyingGlassIcon className="h-4 w-4" />
|
||||
<span>Block Explorer</span>
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<SwitchTheme className={`pointer-events-auto ${isLocalNetwork ? "self-end md:self-auto" : ""}`} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<ul className="menu menu-horizontal w-full">
|
||||
<div className="flex justify-center items-center gap-2 text-sm w-full">
|
||||
<div className="text-center">
|
||||
<a href="https://github.com/scaffold-eth/se-2" target="_blank" rel="noreferrer" className="link">
|
||||
Fork me
|
||||
</a>
|
||||
</div>
|
||||
<span>·</span>
|
||||
<div className="flex justify-center items-center gap-2">
|
||||
<p className="m-0 text-center">
|
||||
Built with <HeartIcon className="inline-block h-4 w-4" /> at
|
||||
</p>
|
||||
<a
|
||||
className="flex justify-center items-center gap-1"
|
||||
href="https://buidlguidl.com/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<BuidlGuidlLogo className="w-3 h-5 pb-1" />
|
||||
<span className="link">BuidlGuidl</span>
|
||||
</a>
|
||||
</div>
|
||||
<span>·</span>
|
||||
<div className="text-center">
|
||||
<a href="https://t.me/joinchat/KByvmRe5wkR-8F_zz6AjpA" target="_blank" rel="noreferrer" className="link">
|
||||
Support
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
114
packages/nextjs/components/Header.tsx
Normal file
114
packages/nextjs/components/Header.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
"use client";
|
||||
|
||||
import React, { useRef } from "react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { hardhat } from "viem/chains";
|
||||
import { Bars3Icon, BugAntIcon } from "@heroicons/react/24/outline";
|
||||
import { BoltIcon, KeyIcon } from "@heroicons/react/24/outline";
|
||||
import { FaucetButton, RainbowKitCustomConnectButton } from "~~/components/scaffold-eth";
|
||||
import { useOutsideClick, useTargetNetwork } from "~~/hooks/scaffold-eth";
|
||||
|
||||
type HeaderMenuLink = {
|
||||
label: string;
|
||||
href: string;
|
||||
icon?: React.ReactNode;
|
||||
};
|
||||
|
||||
export const menuLinks: HeaderMenuLink[] = [
|
||||
{
|
||||
label: "Home",
|
||||
href: "/",
|
||||
},
|
||||
{
|
||||
label: "DEX",
|
||||
href: "/dex",
|
||||
icon: <KeyIcon className="h-4 w-4" />,
|
||||
},
|
||||
{
|
||||
label: "Events",
|
||||
href: "/events",
|
||||
icon: <BoltIcon className="h-4 w-4" />,
|
||||
},
|
||||
{
|
||||
label: "Debug Contracts",
|
||||
href: "/debug",
|
||||
icon: <BugAntIcon className="h-4 w-4" />,
|
||||
},
|
||||
];
|
||||
|
||||
export const HeaderMenuLinks = () => {
|
||||
const pathname = usePathname();
|
||||
|
||||
return (
|
||||
<>
|
||||
{menuLinks.map(({ label, href, icon }) => {
|
||||
const isActive = pathname === href;
|
||||
return (
|
||||
<li key={href}>
|
||||
<Link
|
||||
href={href}
|
||||
passHref
|
||||
className={`${
|
||||
isActive ? "bg-secondary shadow-md" : ""
|
||||
} hover:bg-secondary hover:shadow-md focus:!bg-secondary active:!text-neutral py-1.5 px-3 text-sm rounded-full gap-2 grid grid-flow-col`}
|
||||
>
|
||||
{icon}
|
||||
<span>{label}</span>
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Site header
|
||||
*/
|
||||
export const Header = () => {
|
||||
const { targetNetwork } = useTargetNetwork();
|
||||
const isLocalNetwork = targetNetwork.id === hardhat.id;
|
||||
|
||||
const burgerMenuRef = useRef<HTMLDetailsElement>(null);
|
||||
useOutsideClick(burgerMenuRef, () => {
|
||||
burgerMenuRef?.current?.removeAttribute("open");
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="sticky lg:static top-0 navbar bg-base-100 min-h-0 shrink-0 justify-between z-20 shadow-md shadow-secondary px-0 sm:px-2">
|
||||
<div className="navbar-start w-auto lg:w-1/2">
|
||||
<details className="dropdown" ref={burgerMenuRef}>
|
||||
<summary className="ml-1 btn btn-ghost lg:hidden hover:bg-transparent">
|
||||
<Bars3Icon className="h-1/2" />
|
||||
</summary>
|
||||
<ul
|
||||
className="menu menu-compact dropdown-content mt-3 p-2 shadow-sm bg-base-100 rounded-box w-52"
|
||||
onClick={() => {
|
||||
burgerMenuRef?.current?.removeAttribute("open");
|
||||
}}
|
||||
>
|
||||
<HeaderMenuLinks />
|
||||
</ul>
|
||||
</details>
|
||||
<Link href="/" passHref className="hidden lg:flex items-center gap-2 ml-4 mr-6 shrink-0">
|
||||
<div className="flex relative w-10 h-10">
|
||||
<Image alt="SE2 logo" className="cursor-pointer" fill src="/logo.svg" />
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-bold leading-tight">SRE Challenges</span>
|
||||
<span className="text-xs">Build a DEX</span>
|
||||
</div>
|
||||
</Link>
|
||||
<ul className="hidden lg:flex lg:flex-nowrap menu menu-horizontal px-1 gap-2">
|
||||
<HeaderMenuLinks />
|
||||
</ul>
|
||||
</div>
|
||||
<div className="navbar-end grow mr-4">
|
||||
<RainbowKitCustomConnectButton />
|
||||
{isLocalNetwork && <FaucetButton />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
61
packages/nextjs/components/ScaffoldEthAppWithProviders.tsx
Normal file
61
packages/nextjs/components/ScaffoldEthAppWithProviders.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { RainbowKitProvider, darkTheme, lightTheme } from "@rainbow-me/rainbowkit";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { AppProgressBar as ProgressBar } from "next-nprogress-bar";
|
||||
import { useTheme } from "next-themes";
|
||||
import { Toaster } from "react-hot-toast";
|
||||
import { WagmiProvider } from "wagmi";
|
||||
import { Footer } from "~~/components/Footer";
|
||||
import { Header } from "~~/components/Header";
|
||||
import { BlockieAvatar } from "~~/components/scaffold-eth";
|
||||
import { useInitializeNativeCurrencyPrice } from "~~/hooks/scaffold-eth";
|
||||
import { wagmiConfig } from "~~/services/web3/wagmiConfig";
|
||||
|
||||
const ScaffoldEthApp = ({ children }: { children: React.ReactNode }) => {
|
||||
useInitializeNativeCurrencyPrice();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={`flex flex-col min-h-screen `}>
|
||||
<Header />
|
||||
<main className="relative flex flex-col flex-1">{children}</main>
|
||||
<Footer />
|
||||
</div>
|
||||
<Toaster />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
refetchOnWindowFocus: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const ScaffoldEthAppWithProviders = ({ children }: { children: React.ReactNode }) => {
|
||||
const { resolvedTheme } = useTheme();
|
||||
const isDarkMode = resolvedTheme === "dark";
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<WagmiProvider config={wagmiConfig}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<RainbowKitProvider
|
||||
avatar={BlockieAvatar}
|
||||
theme={mounted ? (isDarkMode ? darkTheme() : lightTheme()) : lightTheme()}
|
||||
>
|
||||
<ProgressBar height="3px" color="#2299dd" />
|
||||
<ScaffoldEthApp>{children}</ScaffoldEthApp>
|
||||
</RainbowKitProvider>
|
||||
</QueryClientProvider>
|
||||
</WagmiProvider>
|
||||
);
|
||||
};
|
||||
42
packages/nextjs/components/SwitchTheme.tsx
Normal file
42
packages/nextjs/components/SwitchTheme.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTheme } from "next-themes";
|
||||
import { MoonIcon, SunIcon } from "@heroicons/react/24/outline";
|
||||
|
||||
export const SwitchTheme = ({ className }: { className?: string }) => {
|
||||
const { setTheme, resolvedTheme } = useTheme();
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
const isDarkMode = resolvedTheme === "dark";
|
||||
|
||||
const handleToggle = () => {
|
||||
if (isDarkMode) {
|
||||
setTheme("light");
|
||||
return;
|
||||
}
|
||||
setTheme("dark");
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
if (!mounted) return null;
|
||||
|
||||
return (
|
||||
<div className={`flex space-x-2 h-8 items-center justify-center text-sm ${className}`}>
|
||||
<input
|
||||
id="theme-toggle"
|
||||
type="checkbox"
|
||||
className="toggle bg-secondary toggle-primary hover:bg-accent transition-all"
|
||||
onChange={handleToggle}
|
||||
checked={isDarkMode}
|
||||
/>
|
||||
<label htmlFor="theme-toggle" className={`swap swap-rotate ${!isDarkMode ? "swap-active" : ""}`}>
|
||||
<SunIcon className="swap-on h-5 w-5" />
|
||||
<MoonIcon className="swap-off h-5 w-5" />
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
9
packages/nextjs/components/ThemeProvider.tsx
Normal file
9
packages/nextjs/components/ThemeProvider.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { ThemeProvider as NextThemesProvider } from "next-themes";
|
||||
import { type ThemeProviderProps } from "next-themes/dist/types";
|
||||
|
||||
export const ThemeProvider = ({ children, ...props }: ThemeProviderProps) => {
|
||||
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
|
||||
};
|
||||
18
packages/nextjs/components/assets/BuidlGuidlLogo.tsx
Normal file
18
packages/nextjs/components/assets/BuidlGuidlLogo.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
export const BuidlGuidlLogo = ({ className }: { className: string }) => {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
width="53"
|
||||
height="72"
|
||||
viewBox="0 0 53 72"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M25.9 17.434v15.638h3.927v9.04h9.718v-9.04h6.745v18.08l-10.607 19.88-12.11-.182-12.11.183L.856 51.152v-18.08h6.713v9.04h9.75v-9.04h4.329V2.46a2.126 2.126 0 0 1 4.047-.914c1.074.412 2.157 1.5 3.276 2.626 1.33 1.337 2.711 2.726 4.193 3.095 1.496.373 2.605-.026 3.855-.475 1.31-.47 2.776-.997 5.005-.747 1.67.197 2.557 1.289 3.548 2.509 1.317 1.623 2.82 3.473 6.599 3.752l-.024.017c-2.42 1.709-5.726 4.043-10.86 3.587-1.605-.139-2.736-.656-3.82-1.153-1.546-.707-2.997-1.37-5.59-.832-2.809.563-4.227 1.892-5.306 2.903-.236.221-.456.427-.67.606Z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
187
packages/nextjs/components/scaffold-eth/Address/Address.tsx
Normal file
187
packages/nextjs/components/scaffold-eth/Address/Address.tsx
Normal file
@@ -0,0 +1,187 @@
|
||||
"use client";
|
||||
|
||||
import { AddressCopyIcon } from "./AddressCopyIcon";
|
||||
import { AddressLinkWrapper } from "./AddressLinkWrapper";
|
||||
import { Address as AddressType, getAddress, isAddress } from "viem";
|
||||
import { normalize } from "viem/ens";
|
||||
import { useEnsAvatar, useEnsName } from "wagmi";
|
||||
import { BlockieAvatar } from "~~/components/scaffold-eth";
|
||||
import { useTargetNetwork } from "~~/hooks/scaffold-eth/useTargetNetwork";
|
||||
import { getBlockExplorerAddressLink } from "~~/utils/scaffold-eth";
|
||||
|
||||
const textSizeMap = {
|
||||
"3xs": "text-[10px]",
|
||||
"2xs": "text-[11px]",
|
||||
xs: "text-xs",
|
||||
sm: "text-sm",
|
||||
base: "text-base",
|
||||
lg: "text-lg",
|
||||
xl: "text-xl",
|
||||
"2xl": "text-2xl",
|
||||
"3xl": "text-3xl",
|
||||
"4xl": "text-4xl",
|
||||
} as const;
|
||||
|
||||
const blockieSizeMap = {
|
||||
"3xs": 4,
|
||||
"2xs": 5,
|
||||
xs: 6,
|
||||
sm: 7,
|
||||
base: 8,
|
||||
lg: 9,
|
||||
xl: 10,
|
||||
"2xl": 12,
|
||||
"3xl": 15,
|
||||
"4xl": 17,
|
||||
"5xl": 19,
|
||||
"6xl": 21,
|
||||
"7xl": 23,
|
||||
} as const;
|
||||
|
||||
const copyIconSizeMap = {
|
||||
"3xs": "h-2.5 w-2.5",
|
||||
"2xs": "h-3 w-3",
|
||||
xs: "h-3.5 w-3.5",
|
||||
sm: "h-4 w-4",
|
||||
base: "h-[18px] w-[18px]",
|
||||
lg: "h-5 w-5",
|
||||
xl: "h-[22px] w-[22px]",
|
||||
"2xl": "h-6 w-6",
|
||||
"3xl": "h-[26px] w-[26px]",
|
||||
"4xl": "h-7 w-7",
|
||||
} as const;
|
||||
|
||||
type SizeMap = typeof textSizeMap | typeof blockieSizeMap;
|
||||
|
||||
const getNextSize = <T extends SizeMap>(sizeMap: T, currentSize: keyof T, step = 1): keyof T => {
|
||||
const sizes = Object.keys(sizeMap) as Array<keyof T>;
|
||||
const currentIndex = sizes.indexOf(currentSize);
|
||||
const nextIndex = Math.min(currentIndex + step, sizes.length - 1);
|
||||
return sizes[nextIndex];
|
||||
};
|
||||
|
||||
const getPrevSize = <T extends SizeMap>(sizeMap: T, currentSize: keyof T, step = 1): keyof T => {
|
||||
const sizes = Object.keys(sizeMap) as Array<keyof T>;
|
||||
const currentIndex = sizes.indexOf(currentSize);
|
||||
const prevIndex = Math.max(currentIndex - step, 0);
|
||||
return sizes[prevIndex];
|
||||
};
|
||||
|
||||
type AddressProps = {
|
||||
address?: AddressType;
|
||||
disableAddressLink?: boolean;
|
||||
format?: "short" | "long";
|
||||
size?: "xs" | "sm" | "base" | "lg" | "xl" | "2xl" | "3xl";
|
||||
onlyEnsOrAddress?: boolean;
|
||||
};
|
||||
|
||||
export const Address = ({
|
||||
address,
|
||||
disableAddressLink,
|
||||
format,
|
||||
size = "base",
|
||||
onlyEnsOrAddress = false,
|
||||
}: AddressProps) => {
|
||||
const checkSumAddress = address ? getAddress(address) : undefined;
|
||||
|
||||
const { targetNetwork } = useTargetNetwork();
|
||||
|
||||
const { data: ens, isLoading: isEnsNameLoading } = useEnsName({
|
||||
address: checkSumAddress,
|
||||
chainId: 1,
|
||||
query: {
|
||||
enabled: isAddress(checkSumAddress ?? ""),
|
||||
},
|
||||
});
|
||||
const { data: ensAvatar } = useEnsAvatar({
|
||||
name: ens ? normalize(ens) : undefined,
|
||||
chainId: 1,
|
||||
query: {
|
||||
enabled: Boolean(ens),
|
||||
gcTime: 30_000,
|
||||
},
|
||||
});
|
||||
|
||||
const shortAddress = checkSumAddress?.slice(0, 6) + "..." + checkSumAddress?.slice(-4);
|
||||
const displayAddress = format === "long" ? checkSumAddress : shortAddress;
|
||||
const displayEnsOrAddress = ens || displayAddress;
|
||||
|
||||
const showSkeleton = !checkSumAddress || (!onlyEnsOrAddress && (ens || isEnsNameLoading));
|
||||
|
||||
const addressSize = showSkeleton && !onlyEnsOrAddress ? getPrevSize(textSizeMap, size, 2) : size;
|
||||
const ensSize = getNextSize(textSizeMap, addressSize);
|
||||
const blockieSize = showSkeleton && !onlyEnsOrAddress ? getNextSize(blockieSizeMap, addressSize, 4) : addressSize;
|
||||
|
||||
if (!checkSumAddress) {
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
<div
|
||||
className="shrink-0 skeleton rounded-full"
|
||||
style={{
|
||||
width: (blockieSizeMap[blockieSize] * 24) / blockieSizeMap["base"],
|
||||
height: (blockieSizeMap[blockieSize] * 24) / blockieSizeMap["base"],
|
||||
}}
|
||||
></div>
|
||||
<div className="flex flex-col space-y-1">
|
||||
{!onlyEnsOrAddress && (
|
||||
<div className={`ml-1.5 skeleton rounded-lg font-bold ${textSizeMap[ensSize]}`}>
|
||||
<span className="invisible">0x1234...56789</span>
|
||||
</div>
|
||||
)}
|
||||
<div className={`ml-1.5 skeleton rounded-lg ${textSizeMap[addressSize]}`}>
|
||||
<span className="invisible">0x1234...56789</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!isAddress(checkSumAddress)) {
|
||||
return <span className="text-error">Wrong address</span>;
|
||||
}
|
||||
|
||||
const blockExplorerAddressLink = getBlockExplorerAddressLink(targetNetwork, checkSumAddress);
|
||||
|
||||
return (
|
||||
<div className="flex items-center shrink-0">
|
||||
<div className="shrink-0">
|
||||
<BlockieAvatar
|
||||
address={checkSumAddress}
|
||||
ensImage={ensAvatar}
|
||||
size={(blockieSizeMap[blockieSize] * 24) / blockieSizeMap["base"]}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
{showSkeleton &&
|
||||
(isEnsNameLoading ? (
|
||||
<div className={`ml-1.5 skeleton rounded-lg font-bold ${textSizeMap[ensSize]}`}>
|
||||
<span className="invisible">{shortAddress}</span>
|
||||
</div>
|
||||
) : (
|
||||
<span className={`ml-1.5 ${textSizeMap[ensSize]} font-bold`}>
|
||||
<AddressLinkWrapper
|
||||
disableAddressLink={disableAddressLink}
|
||||
blockExplorerAddressLink={blockExplorerAddressLink}
|
||||
>
|
||||
{ens}
|
||||
</AddressLinkWrapper>
|
||||
</span>
|
||||
))}
|
||||
<div className="flex">
|
||||
<span className={`ml-1.5 ${textSizeMap[addressSize]} font-normal`}>
|
||||
<AddressLinkWrapper
|
||||
disableAddressLink={disableAddressLink}
|
||||
blockExplorerAddressLink={blockExplorerAddressLink}
|
||||
>
|
||||
{onlyEnsOrAddress ? displayEnsOrAddress : displayAddress}
|
||||
</AddressLinkWrapper>
|
||||
</span>
|
||||
<AddressCopyIcon
|
||||
className={`ml-1 ${copyIconSizeMap[addressSize]} cursor-pointer`}
|
||||
address={checkSumAddress}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
import { CheckCircleIcon, DocumentDuplicateIcon } from "@heroicons/react/24/outline";
|
||||
import { useCopyToClipboard } from "~~/hooks/scaffold-eth/useCopyToClipboard";
|
||||
|
||||
export const AddressCopyIcon = ({ className, address }: { className?: string; address: string }) => {
|
||||
const { copyToClipboard: copyAddressToClipboard, isCopiedToClipboard: isAddressCopiedToClipboard } =
|
||||
useCopyToClipboard();
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
copyAddressToClipboard(address);
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{isAddressCopiedToClipboard ? (
|
||||
<CheckCircleIcon className={className} aria-hidden="true" />
|
||||
) : (
|
||||
<DocumentDuplicateIcon className={className} aria-hidden="true" />
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
import Link from "next/link";
|
||||
import { hardhat } from "viem/chains";
|
||||
import { useTargetNetwork } from "~~/hooks/scaffold-eth";
|
||||
|
||||
type AddressLinkWrapperProps = {
|
||||
children: React.ReactNode;
|
||||
disableAddressLink?: boolean;
|
||||
blockExplorerAddressLink: string;
|
||||
};
|
||||
|
||||
export const AddressLinkWrapper = ({
|
||||
children,
|
||||
disableAddressLink,
|
||||
blockExplorerAddressLink,
|
||||
}: AddressLinkWrapperProps) => {
|
||||
const { targetNetwork } = useTargetNetwork();
|
||||
|
||||
return disableAddressLink ? (
|
||||
<>{children}</>
|
||||
) : (
|
||||
<Link
|
||||
href={blockExplorerAddressLink}
|
||||
target={targetNetwork.id === hardhat.id ? undefined : "_blank"}
|
||||
rel={targetNetwork.id === hardhat.id ? undefined : "noopener noreferrer"}
|
||||
>
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
75
packages/nextjs/components/scaffold-eth/Balance.tsx
Normal file
75
packages/nextjs/components/scaffold-eth/Balance.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
"use client";
|
||||
|
||||
import { Address, formatEther } from "viem";
|
||||
import { useDisplayUsdMode } from "~~/hooks/scaffold-eth/useDisplayUsdMode";
|
||||
import { useTargetNetwork } from "~~/hooks/scaffold-eth/useTargetNetwork";
|
||||
import { useWatchBalance } from "~~/hooks/scaffold-eth/useWatchBalance";
|
||||
import { useGlobalState } from "~~/services/store/store";
|
||||
|
||||
type BalanceProps = {
|
||||
address?: Address;
|
||||
className?: string;
|
||||
usdMode?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Display (ETH & USD) balance of an ETH address.
|
||||
*/
|
||||
export const Balance = ({ address, className = "", usdMode }: BalanceProps) => {
|
||||
const { targetNetwork } = useTargetNetwork();
|
||||
const nativeCurrencyPrice = useGlobalState(state => state.nativeCurrency.price);
|
||||
const isNativeCurrencyPriceFetching = useGlobalState(state => state.nativeCurrency.isFetching);
|
||||
|
||||
const {
|
||||
data: balance,
|
||||
isError,
|
||||
isLoading,
|
||||
} = useWatchBalance({
|
||||
address,
|
||||
});
|
||||
|
||||
const { displayUsdMode, toggleDisplayUsdMode } = useDisplayUsdMode({ defaultUsdMode: usdMode });
|
||||
|
||||
if (!address || isLoading || balance === null || (isNativeCurrencyPriceFetching && nativeCurrencyPrice === 0)) {
|
||||
return (
|
||||
<div className="animate-pulse flex space-x-4">
|
||||
<div className="rounded-md bg-slate-300 h-6 w-6"></div>
|
||||
<div className="flex items-center space-y-6">
|
||||
<div className="h-2 w-28 bg-slate-300 rounded-sm"></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<div className="border-2 border-base-content/30 rounded-md px-2 flex flex-col items-center max-w-fit cursor-pointer">
|
||||
<div className="text-warning">Error</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const formattedBalance = balance ? Number(formatEther(balance.value)) : 0;
|
||||
|
||||
return (
|
||||
<button
|
||||
className={`btn btn-sm btn-ghost flex flex-col font-normal items-center hover:bg-transparent ${className}`}
|
||||
onClick={toggleDisplayUsdMode}
|
||||
type="button"
|
||||
>
|
||||
<div className="w-full flex items-center justify-center">
|
||||
{displayUsdMode ? (
|
||||
<>
|
||||
<span className="text-[0.8em] font-bold mr-1">$</span>
|
||||
<span>{(formattedBalance * nativeCurrencyPrice).toFixed(2)}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span>{formattedBalance.toFixed(4)}</span>
|
||||
<span className="text-[0.8em] font-bold ml-1">{targetNetwork.nativeCurrency.symbol}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
17
packages/nextjs/components/scaffold-eth/BlockieAvatar.tsx
Normal file
17
packages/nextjs/components/scaffold-eth/BlockieAvatar.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
"use client";
|
||||
|
||||
import { AvatarComponent } from "@rainbow-me/rainbowkit";
|
||||
import { blo } from "blo";
|
||||
|
||||
// Custom Avatar for RainbowKit
|
||||
export const BlockieAvatar: AvatarComponent = ({ address, ensImage, size }) => (
|
||||
// Don't want to use nextJS Image here (and adding remote patterns for the URL)
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
className="rounded-full"
|
||||
src={ensImage || blo(address as `0x${string}`)}
|
||||
width={size}
|
||||
height={size}
|
||||
alt={`${address} avatar`}
|
||||
/>
|
||||
);
|
||||
129
packages/nextjs/components/scaffold-eth/Faucet.tsx
Normal file
129
packages/nextjs/components/scaffold-eth/Faucet.tsx
Normal file
@@ -0,0 +1,129 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { Address as AddressType, createWalletClient, http, parseEther } from "viem";
|
||||
import { hardhat } from "viem/chains";
|
||||
import { useAccount } from "wagmi";
|
||||
import { BanknotesIcon } from "@heroicons/react/24/outline";
|
||||
import { Address, AddressInput, Balance, EtherInput } from "~~/components/scaffold-eth";
|
||||
import { useTransactor } from "~~/hooks/scaffold-eth";
|
||||
import { notification } from "~~/utils/scaffold-eth";
|
||||
|
||||
// Account index to use from generated hardhat accounts.
|
||||
const FAUCET_ACCOUNT_INDEX = 0;
|
||||
|
||||
const localWalletClient = createWalletClient({
|
||||
chain: hardhat,
|
||||
transport: http(),
|
||||
});
|
||||
|
||||
/**
|
||||
* Faucet modal which lets you send ETH to any address.
|
||||
*/
|
||||
export const Faucet = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [inputAddress, setInputAddress] = useState<AddressType>();
|
||||
const [faucetAddress, setFaucetAddress] = useState<AddressType>();
|
||||
const [sendValue, setSendValue] = useState("");
|
||||
|
||||
const { chain: ConnectedChain } = useAccount();
|
||||
|
||||
const faucetTxn = useTransactor(localWalletClient);
|
||||
|
||||
useEffect(() => {
|
||||
const getFaucetAddress = async () => {
|
||||
try {
|
||||
const accounts = await localWalletClient.getAddresses();
|
||||
setFaucetAddress(accounts[FAUCET_ACCOUNT_INDEX]);
|
||||
} catch (error) {
|
||||
notification.error(
|
||||
<>
|
||||
<p className="font-bold mt-0 mb-1">Cannot connect to local provider</p>
|
||||
<p className="m-0">
|
||||
- Did you forget to run <code className="italic bg-base-300 text-base font-bold">yarn chain</code> ?
|
||||
</p>
|
||||
<p className="mt-1 break-normal">
|
||||
- Or you can change <code className="italic bg-base-300 text-base font-bold">targetNetwork</code> in{" "}
|
||||
<code className="italic bg-base-300 text-base font-bold">scaffold.config.ts</code>
|
||||
</p>
|
||||
</>,
|
||||
);
|
||||
console.error("⚡️ ~ file: Faucet.tsx:getFaucetAddress ~ error", error);
|
||||
}
|
||||
};
|
||||
getFaucetAddress();
|
||||
}, []);
|
||||
|
||||
const sendETH = async () => {
|
||||
if (!faucetAddress || !inputAddress) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setLoading(true);
|
||||
await faucetTxn({
|
||||
to: inputAddress,
|
||||
value: parseEther(sendValue as `${number}`),
|
||||
account: faucetAddress,
|
||||
});
|
||||
setLoading(false);
|
||||
setInputAddress(undefined);
|
||||
setSendValue("");
|
||||
} catch (error) {
|
||||
console.error("⚡️ ~ file: Faucet.tsx:sendETH ~ error", error);
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Render only on local chain
|
||||
if (ConnectedChain?.id !== hardhat.id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<label htmlFor="faucet-modal" className="btn btn-primary btn-sm font-normal gap-1">
|
||||
<BanknotesIcon className="h-4 w-4" />
|
||||
<span>Faucet</span>
|
||||
</label>
|
||||
<input type="checkbox" id="faucet-modal" className="modal-toggle" />
|
||||
<label htmlFor="faucet-modal" className="modal cursor-pointer">
|
||||
<label className="modal-box relative">
|
||||
{/* dummy input to capture event onclick on modal box */}
|
||||
<input className="h-0 w-0 absolute top-0 left-0" />
|
||||
<h3 className="text-xl font-bold mb-3">Local Faucet</h3>
|
||||
<label htmlFor="faucet-modal" className="btn btn-ghost btn-sm btn-circle absolute right-3 top-3">
|
||||
✕
|
||||
</label>
|
||||
<div className="space-y-3">
|
||||
<div className="flex space-x-4">
|
||||
<div>
|
||||
<span className="text-sm font-bold">From:</span>
|
||||
<Address address={faucetAddress} onlyEnsOrAddress />
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-sm font-bold pl-3">Available:</span>
|
||||
<Balance address={faucetAddress} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col space-y-3">
|
||||
<AddressInput
|
||||
placeholder="Destination Address"
|
||||
value={inputAddress ?? ""}
|
||||
onChange={value => setInputAddress(value as AddressType)}
|
||||
/>
|
||||
<EtherInput placeholder="Amount to send" value={sendValue} onChange={value => setSendValue(value)} />
|
||||
<button className="h-10 btn btn-primary btn-sm px-2 rounded-full" onClick={sendETH} disabled={loading}>
|
||||
{!loading ? (
|
||||
<BanknotesIcon className="h-6 w-6" />
|
||||
) : (
|
||||
<span className="loading loading-spinner loading-sm"></span>
|
||||
)}
|
||||
<span>Send</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
73
packages/nextjs/components/scaffold-eth/FaucetButton.tsx
Normal file
73
packages/nextjs/components/scaffold-eth/FaucetButton.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { createWalletClient, http, parseEther } from "viem";
|
||||
import { hardhat } from "viem/chains";
|
||||
import { useAccount } from "wagmi";
|
||||
import { BanknotesIcon } from "@heroicons/react/24/outline";
|
||||
import { useTransactor } from "~~/hooks/scaffold-eth";
|
||||
import { useWatchBalance } from "~~/hooks/scaffold-eth/useWatchBalance";
|
||||
|
||||
// Number of ETH faucet sends to an address
|
||||
const NUM_OF_ETH = "1";
|
||||
const FAUCET_ADDRESS = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266";
|
||||
|
||||
const localWalletClient = createWalletClient({
|
||||
chain: hardhat,
|
||||
transport: http(),
|
||||
});
|
||||
|
||||
/**
|
||||
* FaucetButton button which lets you grab eth.
|
||||
*/
|
||||
export const FaucetButton = () => {
|
||||
const { address, chain: ConnectedChain } = useAccount();
|
||||
|
||||
const { data: balance } = useWatchBalance({ address });
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const faucetTxn = useTransactor(localWalletClient);
|
||||
|
||||
const sendETH = async () => {
|
||||
if (!address) return;
|
||||
try {
|
||||
setLoading(true);
|
||||
await faucetTxn({
|
||||
account: FAUCET_ADDRESS,
|
||||
to: address,
|
||||
value: parseEther(NUM_OF_ETH),
|
||||
});
|
||||
setLoading(false);
|
||||
} catch (error) {
|
||||
console.error("⚡️ ~ file: FaucetButton.tsx:sendETH ~ error", error);
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Render only on local chain
|
||||
if (ConnectedChain?.id !== hardhat.id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isBalanceZero = balance && balance.value === 0n;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
!isBalanceZero
|
||||
? "ml-1"
|
||||
: "ml-1 tooltip tooltip-bottom tooltip-primary tooltip-open font-bold before:left-auto before:transform-none before:content-[attr(data-tip)] before:-translate-x-2/5"
|
||||
}
|
||||
data-tip="Grab funds from faucet"
|
||||
>
|
||||
<button className="btn btn-secondary btn-sm px-2 rounded-full" onClick={sendETH} disabled={loading}>
|
||||
{!loading ? (
|
||||
<BanknotesIcon className="h-4 w-4" />
|
||||
) : (
|
||||
<span className="loading loading-spinner loading-xs"></span>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
120
packages/nextjs/components/scaffold-eth/Input/AddressInput.tsx
Normal file
120
packages/nextjs/components/scaffold-eth/Input/AddressInput.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { blo } from "blo";
|
||||
import { useDebounceValue } from "usehooks-ts";
|
||||
import { Address, isAddress } from "viem";
|
||||
import { normalize } from "viem/ens";
|
||||
import { useEnsAddress, useEnsAvatar, useEnsName } from "wagmi";
|
||||
import { CommonInputProps, InputBase, isENS } from "~~/components/scaffold-eth";
|
||||
|
||||
/**
|
||||
* Address input with ENS name resolution
|
||||
*/
|
||||
export const AddressInput = ({ value, name, placeholder, onChange, disabled }: CommonInputProps<Address | string>) => {
|
||||
// Debounce the input to keep clean RPC calls when resolving ENS names
|
||||
// If the input is an address, we don't need to debounce it
|
||||
const [_debouncedValue] = useDebounceValue(value, 500);
|
||||
const debouncedValue = isAddress(value) ? value : _debouncedValue;
|
||||
const isDebouncedValueLive = debouncedValue === value;
|
||||
|
||||
// If the user changes the input after an ENS name is already resolved, we want to remove the stale result
|
||||
const settledValue = isDebouncedValueLive ? debouncedValue : undefined;
|
||||
|
||||
const {
|
||||
data: ensAddress,
|
||||
isLoading: isEnsAddressLoading,
|
||||
isError: isEnsAddressError,
|
||||
isSuccess: isEnsAddressSuccess,
|
||||
} = useEnsAddress({
|
||||
name: settledValue,
|
||||
chainId: 1,
|
||||
query: {
|
||||
gcTime: 30_000,
|
||||
enabled: isDebouncedValueLive && isENS(debouncedValue),
|
||||
},
|
||||
});
|
||||
|
||||
const [enteredEnsName, setEnteredEnsName] = useState<string>();
|
||||
const {
|
||||
data: ensName,
|
||||
isLoading: isEnsNameLoading,
|
||||
isError: isEnsNameError,
|
||||
isSuccess: isEnsNameSuccess,
|
||||
} = useEnsName({
|
||||
address: settledValue as Address,
|
||||
chainId: 1,
|
||||
query: {
|
||||
enabled: isAddress(debouncedValue),
|
||||
gcTime: 30_000,
|
||||
},
|
||||
});
|
||||
|
||||
const { data: ensAvatar, isLoading: isEnsAvatarLoading } = useEnsAvatar({
|
||||
name: ensName ? normalize(ensName) : undefined,
|
||||
chainId: 1,
|
||||
query: {
|
||||
enabled: Boolean(ensName),
|
||||
gcTime: 30_000,
|
||||
},
|
||||
});
|
||||
|
||||
// ens => address
|
||||
useEffect(() => {
|
||||
if (!ensAddress) return;
|
||||
|
||||
// ENS resolved successfully
|
||||
setEnteredEnsName(debouncedValue);
|
||||
onChange(ensAddress);
|
||||
}, [ensAddress, onChange, debouncedValue]);
|
||||
|
||||
useEffect(() => {
|
||||
setEnteredEnsName(undefined);
|
||||
}, [value]);
|
||||
|
||||
const reFocus =
|
||||
isEnsAddressError ||
|
||||
isEnsNameError ||
|
||||
isEnsNameSuccess ||
|
||||
isEnsAddressSuccess ||
|
||||
ensName === null ||
|
||||
ensAddress === null;
|
||||
|
||||
return (
|
||||
<InputBase<Address>
|
||||
name={name}
|
||||
placeholder={placeholder}
|
||||
error={ensAddress === null}
|
||||
value={value as Address}
|
||||
onChange={onChange}
|
||||
disabled={isEnsAddressLoading || isEnsNameLoading || disabled}
|
||||
reFocus={reFocus}
|
||||
prefix={
|
||||
ensName ? (
|
||||
<div className="flex bg-base-300 rounded-l-full items-center">
|
||||
{isEnsAvatarLoading && <div className="skeleton bg-base-200 w-[35px] h-[35px] rounded-full shrink-0"></div>}
|
||||
{ensAvatar ? (
|
||||
<span className="w-[35px]">
|
||||
{
|
||||
// eslint-disable-next-line
|
||||
<img className="w-full rounded-full" src={ensAvatar} alt={`${ensAddress} avatar`} />
|
||||
}
|
||||
</span>
|
||||
) : null}
|
||||
<span className="text-accent px-2">{enteredEnsName ?? ensName}</span>
|
||||
</div>
|
||||
) : (
|
||||
(isEnsNameLoading || isEnsAddressLoading) && (
|
||||
<div className="flex bg-base-300 rounded-l-full items-center gap-2 pr-2">
|
||||
<div className="skeleton bg-base-200 w-[35px] h-[35px] rounded-full shrink-0"></div>
|
||||
<div className="skeleton bg-base-200 h-3 w-20"></div>
|
||||
</div>
|
||||
)
|
||||
)
|
||||
}
|
||||
suffix={
|
||||
// Don't want to use nextJS Image here (and adding remote patterns for the URL)
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
value && <img alt="" className="rounded-full!" src={blo(value as `0x${string}`)} width="35" height="35" />
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
import { useCallback } from "react";
|
||||
import { hexToString, isHex, stringToHex } from "viem";
|
||||
import { CommonInputProps, InputBase } from "~~/components/scaffold-eth";
|
||||
|
||||
export const Bytes32Input = ({ value, onChange, name, placeholder, disabled }: CommonInputProps) => {
|
||||
const convertStringToBytes32 = useCallback(() => {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
onChange(isHex(value) ? hexToString(value, { size: 32 }) : stringToHex(value, { size: 32 }));
|
||||
}, [onChange, value]);
|
||||
|
||||
return (
|
||||
<InputBase
|
||||
name={name}
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
onChange={onChange}
|
||||
disabled={disabled}
|
||||
suffix={
|
||||
<button
|
||||
className="self-center cursor-pointer text-xl font-semibold px-4 text-accent"
|
||||
onClick={convertStringToBytes32}
|
||||
type="button"
|
||||
>
|
||||
#
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
28
packages/nextjs/components/scaffold-eth/Input/BytesInput.tsx
Normal file
28
packages/nextjs/components/scaffold-eth/Input/BytesInput.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import { useCallback } from "react";
|
||||
import { bytesToString, isHex, toBytes, toHex } from "viem";
|
||||
import { CommonInputProps, InputBase } from "~~/components/scaffold-eth";
|
||||
|
||||
export const BytesInput = ({ value, onChange, name, placeholder, disabled }: CommonInputProps) => {
|
||||
const convertStringToBytes = useCallback(() => {
|
||||
onChange(isHex(value) ? bytesToString(toBytes(value)) : toHex(toBytes(value)));
|
||||
}, [onChange, value]);
|
||||
|
||||
return (
|
||||
<InputBase
|
||||
name={name}
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
onChange={onChange}
|
||||
disabled={disabled}
|
||||
suffix={
|
||||
<button
|
||||
className="self-center cursor-pointer text-xl font-semibold px-4 text-accent"
|
||||
onClick={convertStringToBytes}
|
||||
type="button"
|
||||
>
|
||||
#
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
128
packages/nextjs/components/scaffold-eth/Input/EtherInput.tsx
Normal file
128
packages/nextjs/components/scaffold-eth/Input/EtherInput.tsx
Normal file
@@ -0,0 +1,128 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { ArrowsRightLeftIcon } from "@heroicons/react/24/outline";
|
||||
import { CommonInputProps, InputBase, SIGNED_NUMBER_REGEX } from "~~/components/scaffold-eth";
|
||||
import { useDisplayUsdMode } from "~~/hooks/scaffold-eth/useDisplayUsdMode";
|
||||
import { useGlobalState } from "~~/services/store/store";
|
||||
|
||||
const MAX_DECIMALS_USD = 2;
|
||||
|
||||
function etherValueToDisplayValue(usdMode: boolean, etherValue: string, nativeCurrencyPrice: number) {
|
||||
if (usdMode && nativeCurrencyPrice) {
|
||||
const parsedEthValue = parseFloat(etherValue);
|
||||
if (Number.isNaN(parsedEthValue)) {
|
||||
return etherValue;
|
||||
} else {
|
||||
// We need to round the value rather than use toFixed,
|
||||
// since otherwise a user would not be able to modify the decimal value
|
||||
return (
|
||||
Math.round(parsedEthValue * nativeCurrencyPrice * 10 ** MAX_DECIMALS_USD) /
|
||||
10 ** MAX_DECIMALS_USD
|
||||
).toString();
|
||||
}
|
||||
} else {
|
||||
return etherValue;
|
||||
}
|
||||
}
|
||||
|
||||
function displayValueToEtherValue(usdMode: boolean, displayValue: string, nativeCurrencyPrice: number) {
|
||||
if (usdMode && nativeCurrencyPrice) {
|
||||
const parsedDisplayValue = parseFloat(displayValue);
|
||||
if (Number.isNaN(parsedDisplayValue)) {
|
||||
// Invalid number.
|
||||
return displayValue;
|
||||
} else {
|
||||
// Compute the ETH value if a valid number.
|
||||
return (parsedDisplayValue / nativeCurrencyPrice).toString();
|
||||
}
|
||||
} else {
|
||||
return displayValue;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Input for ETH amount with USD conversion.
|
||||
*
|
||||
* onChange will always be called with the value in ETH
|
||||
*/
|
||||
export const EtherInput = ({
|
||||
value,
|
||||
name,
|
||||
placeholder,
|
||||
onChange,
|
||||
disabled,
|
||||
usdMode,
|
||||
}: CommonInputProps & { usdMode?: boolean }) => {
|
||||
const [transitoryDisplayValue, setTransitoryDisplayValue] = useState<string>();
|
||||
const nativeCurrencyPrice = useGlobalState(state => state.nativeCurrency.price);
|
||||
const isNativeCurrencyPriceFetching = useGlobalState(state => state.nativeCurrency.isFetching);
|
||||
|
||||
const { displayUsdMode, toggleDisplayUsdMode } = useDisplayUsdMode({ defaultUsdMode: usdMode });
|
||||
|
||||
// The displayValue is derived from the ether value that is controlled outside of the component
|
||||
// In usdMode, it is converted to its usd value, in regular mode it is unaltered
|
||||
const displayValue = useMemo(() => {
|
||||
const newDisplayValue = etherValueToDisplayValue(displayUsdMode, value, nativeCurrencyPrice || 0);
|
||||
if (transitoryDisplayValue && parseFloat(newDisplayValue) === parseFloat(transitoryDisplayValue)) {
|
||||
return transitoryDisplayValue;
|
||||
}
|
||||
// Clear any transitory display values that might be set
|
||||
setTransitoryDisplayValue(undefined);
|
||||
return newDisplayValue;
|
||||
}, [nativeCurrencyPrice, transitoryDisplayValue, displayUsdMode, value]);
|
||||
|
||||
const handleChangeNumber = (newValue: string) => {
|
||||
if (newValue && !SIGNED_NUMBER_REGEX.test(newValue)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Following condition is a fix to prevent usdMode from experiencing different display values
|
||||
// than what the user entered. This can happen due to floating point rounding errors that are introduced in the back and forth conversion
|
||||
if (displayUsdMode) {
|
||||
const decimals = newValue.split(".")[1];
|
||||
if (decimals && decimals.length > MAX_DECIMALS_USD) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Since the display value is a derived state (calculated from the ether value), usdMode would not allow introducing a decimal point.
|
||||
// This condition handles a transitory state for a display value with a trailing decimal sign
|
||||
if (newValue.endsWith(".") || newValue.endsWith(".0")) {
|
||||
setTransitoryDisplayValue(newValue);
|
||||
} else {
|
||||
setTransitoryDisplayValue(undefined);
|
||||
}
|
||||
|
||||
const newEthValue = displayValueToEtherValue(displayUsdMode, newValue, nativeCurrencyPrice || 0);
|
||||
onChange(newEthValue);
|
||||
};
|
||||
|
||||
return (
|
||||
<InputBase
|
||||
name={name}
|
||||
value={displayValue}
|
||||
placeholder={placeholder}
|
||||
onChange={handleChangeNumber}
|
||||
disabled={disabled}
|
||||
prefix={<span className="pl-4 -mr-2 text-accent self-center">{displayUsdMode ? "$" : "Ξ"}</span>}
|
||||
suffix={
|
||||
<div
|
||||
className={`${
|
||||
nativeCurrencyPrice > 0
|
||||
? ""
|
||||
: "tooltip tooltip-secondary before:content-[attr(data-tip)] before:right-[-10px] before:left-auto before:transform-none"
|
||||
}`}
|
||||
data-tip={isNativeCurrencyPriceFetching ? "Fetching price" : "Unable to fetch price"}
|
||||
>
|
||||
<button
|
||||
className="btn btn-primary h-[2.2rem] min-h-[2.2rem]"
|
||||
onClick={toggleDisplayUsdMode}
|
||||
disabled={!displayUsdMode && !nativeCurrencyPrice}
|
||||
type="button"
|
||||
>
|
||||
<ArrowsRightLeftIcon className="h-3 w-3 cursor-pointer" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
66
packages/nextjs/components/scaffold-eth/Input/InputBase.tsx
Normal file
66
packages/nextjs/components/scaffold-eth/Input/InputBase.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import { ChangeEvent, FocusEvent, ReactNode, useCallback, useEffect, useRef } from "react";
|
||||
import { CommonInputProps } from "~~/components/scaffold-eth";
|
||||
|
||||
type InputBaseProps<T> = CommonInputProps<T> & {
|
||||
error?: boolean;
|
||||
prefix?: ReactNode;
|
||||
suffix?: ReactNode;
|
||||
reFocus?: boolean;
|
||||
};
|
||||
|
||||
export const InputBase = <T extends { toString: () => string } | undefined = string>({
|
||||
name,
|
||||
value,
|
||||
onChange,
|
||||
placeholder,
|
||||
error,
|
||||
disabled,
|
||||
prefix,
|
||||
suffix,
|
||||
reFocus,
|
||||
}: InputBaseProps<T>) => {
|
||||
const inputReft = useRef<HTMLInputElement>(null);
|
||||
|
||||
let modifier = "";
|
||||
if (error) {
|
||||
modifier = "border-error";
|
||||
} else if (disabled) {
|
||||
modifier = "border-disabled bg-base-300";
|
||||
}
|
||||
|
||||
const handleChange = useCallback(
|
||||
(e: ChangeEvent<HTMLInputElement>) => {
|
||||
onChange(e.target.value as unknown as T);
|
||||
},
|
||||
[onChange],
|
||||
);
|
||||
|
||||
// Runs only when reFocus prop is passed, useful for setting the cursor
|
||||
// at the end of the input. Example AddressInput
|
||||
const onFocus = (e: FocusEvent<HTMLInputElement, Element>) => {
|
||||
if (reFocus !== undefined) {
|
||||
e.currentTarget.setSelectionRange(e.currentTarget.value.length, e.currentTarget.value.length);
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
if (reFocus !== undefined && reFocus === true) inputReft.current?.focus();
|
||||
}, [reFocus]);
|
||||
|
||||
return (
|
||||
<div className={`flex border-2 border-base-300 bg-base-200 rounded-full text-accent ${modifier}`}>
|
||||
{prefix}
|
||||
<input
|
||||
className="input input-ghost focus-within:border-transparent focus:outline-hidden focus:bg-transparent h-[2.2rem] min-h-[2.2rem] px-4 border w-full font-medium placeholder:text-accent/70 text-base-content/70 focus:text-base-content/70"
|
||||
placeholder={placeholder}
|
||||
name={name}
|
||||
value={value?.toString()}
|
||||
onChange={handleChange}
|
||||
disabled={disabled}
|
||||
autoComplete="off"
|
||||
ref={inputReft}
|
||||
onFocus={onFocus}
|
||||
/>
|
||||
{suffix}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,63 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { parseEther } from "viem";
|
||||
import { CommonInputProps, InputBase, IntegerVariant, isValidInteger } from "~~/components/scaffold-eth";
|
||||
|
||||
type IntegerInputProps = CommonInputProps<string> & {
|
||||
variant?: IntegerVariant;
|
||||
disableMultiplyBy1e18?: boolean;
|
||||
};
|
||||
|
||||
export const IntegerInput = ({
|
||||
value,
|
||||
onChange,
|
||||
name,
|
||||
placeholder,
|
||||
disabled,
|
||||
variant = IntegerVariant.UINT256,
|
||||
disableMultiplyBy1e18 = false,
|
||||
}: IntegerInputProps) => {
|
||||
const [inputError, setInputError] = useState(false);
|
||||
const multiplyBy1e18 = useCallback(() => {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
return onChange(parseEther(value).toString());
|
||||
}, [onChange, value]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isValidInteger(variant, value)) {
|
||||
setInputError(false);
|
||||
} else {
|
||||
setInputError(true);
|
||||
}
|
||||
}, [value, variant]);
|
||||
|
||||
return (
|
||||
<InputBase
|
||||
name={name}
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
error={inputError}
|
||||
onChange={onChange}
|
||||
disabled={disabled}
|
||||
suffix={
|
||||
!inputError &&
|
||||
!disableMultiplyBy1e18 && (
|
||||
<div
|
||||
className="space-x-4 flex tooltip tooltip-top tooltip-secondary before:content-[attr(data-tip)] before:right-[-10px] before:left-auto before:transform-none"
|
||||
data-tip="Multiply by 1e18 (wei)"
|
||||
>
|
||||
<button
|
||||
className={`${disabled ? "cursor-not-allowed" : "cursor-pointer"} font-semibold px-4 text-accent`}
|
||||
onClick={multiplyBy1e18}
|
||||
disabled={disabled}
|
||||
type="button"
|
||||
>
|
||||
∗
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
9
packages/nextjs/components/scaffold-eth/Input/index.ts
Normal file
9
packages/nextjs/components/scaffold-eth/Input/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
"use client";
|
||||
|
||||
export * from "./AddressInput";
|
||||
export * from "./Bytes32Input";
|
||||
export * from "./BytesInput";
|
||||
export * from "./EtherInput";
|
||||
export * from "./InputBase";
|
||||
export * from "./IntegerInput";
|
||||
export * from "./utils";
|
||||
109
packages/nextjs/components/scaffold-eth/Input/utils.ts
Normal file
109
packages/nextjs/components/scaffold-eth/Input/utils.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
export type CommonInputProps<T = string> = {
|
||||
value: T;
|
||||
onChange: (newValue: T) => void;
|
||||
name?: string;
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export enum IntegerVariant {
|
||||
UINT8 = "uint8",
|
||||
UINT16 = "uint16",
|
||||
UINT24 = "uint24",
|
||||
UINT32 = "uint32",
|
||||
UINT40 = "uint40",
|
||||
UINT48 = "uint48",
|
||||
UINT56 = "uint56",
|
||||
UINT64 = "uint64",
|
||||
UINT72 = "uint72",
|
||||
UINT80 = "uint80",
|
||||
UINT88 = "uint88",
|
||||
UINT96 = "uint96",
|
||||
UINT104 = "uint104",
|
||||
UINT112 = "uint112",
|
||||
UINT120 = "uint120",
|
||||
UINT128 = "uint128",
|
||||
UINT136 = "uint136",
|
||||
UINT144 = "uint144",
|
||||
UINT152 = "uint152",
|
||||
UINT160 = "uint160",
|
||||
UINT168 = "uint168",
|
||||
UINT176 = "uint176",
|
||||
UINT184 = "uint184",
|
||||
UINT192 = "uint192",
|
||||
UINT200 = "uint200",
|
||||
UINT208 = "uint208",
|
||||
UINT216 = "uint216",
|
||||
UINT224 = "uint224",
|
||||
UINT232 = "uint232",
|
||||
UINT240 = "uint240",
|
||||
UINT248 = "uint248",
|
||||
UINT256 = "uint256",
|
||||
INT8 = "int8",
|
||||
INT16 = "int16",
|
||||
INT24 = "int24",
|
||||
INT32 = "int32",
|
||||
INT40 = "int40",
|
||||
INT48 = "int48",
|
||||
INT56 = "int56",
|
||||
INT64 = "int64",
|
||||
INT72 = "int72",
|
||||
INT80 = "int80",
|
||||
INT88 = "int88",
|
||||
INT96 = "int96",
|
||||
INT104 = "int104",
|
||||
INT112 = "int112",
|
||||
INT120 = "int120",
|
||||
INT128 = "int128",
|
||||
INT136 = "int136",
|
||||
INT144 = "int144",
|
||||
INT152 = "int152",
|
||||
INT160 = "int160",
|
||||
INT168 = "int168",
|
||||
INT176 = "int176",
|
||||
INT184 = "int184",
|
||||
INT192 = "int192",
|
||||
INT200 = "int200",
|
||||
INT208 = "int208",
|
||||
INT216 = "int216",
|
||||
INT224 = "int224",
|
||||
INT232 = "int232",
|
||||
INT240 = "int240",
|
||||
INT248 = "int248",
|
||||
INT256 = "int256",
|
||||
}
|
||||
|
||||
export const SIGNED_NUMBER_REGEX = /^-?\d+\.?\d*$/;
|
||||
export const UNSIGNED_NUMBER_REGEX = /^\.?\d+\.?\d*$/;
|
||||
|
||||
export const isValidInteger = (dataType: IntegerVariant, value: string) => {
|
||||
const isSigned = dataType.startsWith("i");
|
||||
const bitcount = Number(dataType.substring(isSigned ? 3 : 4));
|
||||
|
||||
let valueAsBigInt;
|
||||
try {
|
||||
valueAsBigInt = BigInt(value);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
} catch (e) {}
|
||||
if (typeof valueAsBigInt !== "bigint") {
|
||||
if (!value || typeof value !== "string") {
|
||||
return true;
|
||||
}
|
||||
return isSigned ? SIGNED_NUMBER_REGEX.test(value) || value === "-" : UNSIGNED_NUMBER_REGEX.test(value);
|
||||
} else if (!isSigned && valueAsBigInt < 0) {
|
||||
return false;
|
||||
}
|
||||
const hexString = valueAsBigInt.toString(16);
|
||||
const significantHexDigits = hexString.match(/.*x0*(.*)$/)?.[1] ?? "";
|
||||
if (
|
||||
significantHexDigits.length * 4 > bitcount ||
|
||||
(isSigned && significantHexDigits.length * 4 === bitcount && parseInt(significantHexDigits.slice(-1)?.[0], 16) < 8)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
// Treat any dot-separated string as a potential ENS name
|
||||
const ensRegex = /.+\..+/;
|
||||
export const isENS = (address = "") => ensRegex.test(address);
|
||||
@@ -0,0 +1,136 @@
|
||||
import { useRef, useState } from "react";
|
||||
import { NetworkOptions } from "./NetworkOptions";
|
||||
import { getAddress } from "viem";
|
||||
import { Address } from "viem";
|
||||
import { useAccount, useDisconnect } from "wagmi";
|
||||
import {
|
||||
ArrowLeftOnRectangleIcon,
|
||||
ArrowTopRightOnSquareIcon,
|
||||
ArrowsRightLeftIcon,
|
||||
CheckCircleIcon,
|
||||
ChevronDownIcon,
|
||||
DocumentDuplicateIcon,
|
||||
EyeIcon,
|
||||
QrCodeIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import { BlockieAvatar, isENS } from "~~/components/scaffold-eth";
|
||||
import { useCopyToClipboard, useOutsideClick } from "~~/hooks/scaffold-eth";
|
||||
import { getTargetNetworks } from "~~/utils/scaffold-eth";
|
||||
|
||||
const BURNER_WALLET_ID = "burnerWallet";
|
||||
|
||||
const allowedNetworks = getTargetNetworks();
|
||||
|
||||
type AddressInfoDropdownProps = {
|
||||
address: Address;
|
||||
blockExplorerAddressLink: string | undefined;
|
||||
displayName: string;
|
||||
ensAvatar?: string;
|
||||
};
|
||||
|
||||
export const AddressInfoDropdown = ({
|
||||
address,
|
||||
ensAvatar,
|
||||
displayName,
|
||||
blockExplorerAddressLink,
|
||||
}: AddressInfoDropdownProps) => {
|
||||
const { disconnect } = useDisconnect();
|
||||
const { connector } = useAccount();
|
||||
const checkSumAddress = getAddress(address);
|
||||
|
||||
const { copyToClipboard: copyAddressToClipboard, isCopiedToClipboard: isAddressCopiedToClipboard } =
|
||||
useCopyToClipboard();
|
||||
const [selectingNetwork, setSelectingNetwork] = useState(false);
|
||||
const dropdownRef = useRef<HTMLDetailsElement>(null);
|
||||
|
||||
const closeDropdown = () => {
|
||||
setSelectingNetwork(false);
|
||||
dropdownRef.current?.removeAttribute("open");
|
||||
};
|
||||
|
||||
useOutsideClick(dropdownRef, closeDropdown);
|
||||
|
||||
return (
|
||||
<>
|
||||
<details ref={dropdownRef} className="dropdown dropdown-end leading-3">
|
||||
<summary className="btn btn-secondary btn-sm pl-0 pr-2 shadow-md dropdown-toggle gap-0 h-auto!">
|
||||
<BlockieAvatar address={checkSumAddress} size={30} ensImage={ensAvatar} />
|
||||
<span className="ml-2 mr-1">
|
||||
{isENS(displayName) ? displayName : checkSumAddress?.slice(0, 6) + "..." + checkSumAddress?.slice(-4)}
|
||||
</span>
|
||||
<ChevronDownIcon className="h-6 w-4 ml-2 sm:ml-0" />
|
||||
</summary>
|
||||
<ul className="dropdown-content menu z-2 p-2 mt-2 shadow-center shadow-accent bg-base-200 rounded-box gap-1">
|
||||
<NetworkOptions hidden={!selectingNetwork} />
|
||||
<li className={selectingNetwork ? "hidden" : ""}>
|
||||
<div
|
||||
className="h-8 btn-sm rounded-xl! flex gap-3 py-3 cursor-pointer"
|
||||
onClick={() => copyAddressToClipboard(checkSumAddress)}
|
||||
>
|
||||
{isAddressCopiedToClipboard ? (
|
||||
<>
|
||||
<CheckCircleIcon className="text-xl font-normal h-6 w-4 ml-2 sm:ml-0" aria-hidden="true" />
|
||||
<span className="whitespace-nowrap">Copied!</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<DocumentDuplicateIcon className="text-xl font-normal h-6 w-4 ml-2 sm:ml-0" aria-hidden="true" />
|
||||
<span className="whitespace-nowrap">Copy address</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
<li className={selectingNetwork ? "hidden" : ""}>
|
||||
<label htmlFor="qrcode-modal" className="h-8 btn-sm rounded-xl! flex gap-3 py-3">
|
||||
<QrCodeIcon className="h-6 w-4 ml-2 sm:ml-0" />
|
||||
<span className="whitespace-nowrap">View QR Code</span>
|
||||
</label>
|
||||
</li>
|
||||
<li className={selectingNetwork ? "hidden" : ""}>
|
||||
<button className="h-8 btn-sm rounded-xl! flex gap-3 py-3" type="button">
|
||||
<ArrowTopRightOnSquareIcon className="h-6 w-4 ml-2 sm:ml-0" />
|
||||
<a
|
||||
target="_blank"
|
||||
href={blockExplorerAddressLink}
|
||||
rel="noopener noreferrer"
|
||||
className="whitespace-nowrap"
|
||||
>
|
||||
View on Block Explorer
|
||||
</a>
|
||||
</button>
|
||||
</li>
|
||||
{allowedNetworks.length > 1 ? (
|
||||
<li className={selectingNetwork ? "hidden" : ""}>
|
||||
<button
|
||||
className="h-8 btn-sm rounded-xl! flex gap-3 py-3"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setSelectingNetwork(true);
|
||||
}}
|
||||
>
|
||||
<ArrowsRightLeftIcon className="h-6 w-4 ml-2 sm:ml-0" /> <span>Switch Network</span>
|
||||
</button>
|
||||
</li>
|
||||
) : null}
|
||||
{connector?.id === BURNER_WALLET_ID ? (
|
||||
<li>
|
||||
<label htmlFor="reveal-burner-pk-modal" className="h-8 btn-sm rounded-xl! flex gap-3 py-3 text-error">
|
||||
<EyeIcon className="h-6 w-4 ml-2 sm:ml-0" />
|
||||
<span>Reveal Private Key</span>
|
||||
</label>
|
||||
</li>
|
||||
) : null}
|
||||
<li className={selectingNetwork ? "hidden" : ""}>
|
||||
<button
|
||||
className="menu-item text-error h-8 btn-sm rounded-xl! flex gap-3 py-3"
|
||||
type="button"
|
||||
onClick={() => disconnect()}
|
||||
>
|
||||
<ArrowLeftOnRectangleIcon className="h-6 w-4 ml-2 sm:ml-0" /> <span>Disconnect</span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</details>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
import { QRCodeSVG } from "qrcode.react";
|
||||
import { Address as AddressType } from "viem";
|
||||
import { Address } from "~~/components/scaffold-eth";
|
||||
|
||||
type AddressQRCodeModalProps = {
|
||||
address: AddressType;
|
||||
modalId: string;
|
||||
};
|
||||
|
||||
export const AddressQRCodeModal = ({ address, modalId }: AddressQRCodeModalProps) => {
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<input type="checkbox" id={`${modalId}`} className="modal-toggle" />
|
||||
<label htmlFor={`${modalId}`} className="modal cursor-pointer">
|
||||
<label className="modal-box relative">
|
||||
{/* dummy input to capture event onclick on modal box */}
|
||||
<input className="h-0 w-0 absolute top-0 left-0" />
|
||||
<label htmlFor={`${modalId}`} className="btn btn-ghost btn-sm btn-circle absolute right-3 top-3">
|
||||
✕
|
||||
</label>
|
||||
<div className="space-y-3 py-6">
|
||||
<div className="flex flex-col items-center gap-6">
|
||||
<QRCodeSVG value={address} size={256} />
|
||||
<Address address={address} format="long" disableAddressLink onlyEnsOrAddress />
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</label>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
import { useTheme } from "next-themes";
|
||||
import { useAccount, useSwitchChain } from "wagmi";
|
||||
import { ArrowsRightLeftIcon } from "@heroicons/react/24/solid";
|
||||
import { getNetworkColor } from "~~/hooks/scaffold-eth";
|
||||
import { getTargetNetworks } from "~~/utils/scaffold-eth";
|
||||
|
||||
const allowedNetworks = getTargetNetworks();
|
||||
|
||||
type NetworkOptionsProps = {
|
||||
hidden?: boolean;
|
||||
};
|
||||
|
||||
export const NetworkOptions = ({ hidden = false }: NetworkOptionsProps) => {
|
||||
const { switchChain } = useSwitchChain();
|
||||
const { chain } = useAccount();
|
||||
const { resolvedTheme } = useTheme();
|
||||
const isDarkMode = resolvedTheme === "dark";
|
||||
|
||||
return (
|
||||
<>
|
||||
{allowedNetworks
|
||||
.filter(allowedNetwork => allowedNetwork.id !== chain?.id)
|
||||
.map(allowedNetwork => (
|
||||
<li key={allowedNetwork.id} className={hidden ? "hidden" : ""}>
|
||||
<button
|
||||
className="menu-item btn-sm rounded-xl! flex gap-3 py-3 whitespace-nowrap"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
switchChain?.({ chainId: allowedNetwork.id });
|
||||
}}
|
||||
>
|
||||
<ArrowsRightLeftIcon className="h-6 w-4 ml-2 sm:ml-0" />
|
||||
<span>
|
||||
Switch to{" "}
|
||||
<span
|
||||
style={{
|
||||
color: getNetworkColor(allowedNetwork, isDarkMode),
|
||||
}}
|
||||
>
|
||||
{allowedNetwork.name}
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,59 @@
|
||||
import { useRef } from "react";
|
||||
import { rainbowkitBurnerWallet } from "burner-connector";
|
||||
import { ShieldExclamationIcon } from "@heroicons/react/24/outline";
|
||||
import { useCopyToClipboard } from "~~/hooks/scaffold-eth";
|
||||
import { getParsedError, notification } from "~~/utils/scaffold-eth";
|
||||
|
||||
const BURNER_WALLET_PK_KEY = "burnerWallet.pk";
|
||||
|
||||
export const RevealBurnerPKModal = () => {
|
||||
const { copyToClipboard, isCopiedToClipboard } = useCopyToClipboard();
|
||||
const modalCheckboxRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const handleCopyPK = async () => {
|
||||
try {
|
||||
const storage = rainbowkitBurnerWallet.useSessionStorage ? sessionStorage : localStorage;
|
||||
const burnerPK = storage?.getItem(BURNER_WALLET_PK_KEY);
|
||||
if (!burnerPK) throw new Error("Burner wallet private key not found");
|
||||
await copyToClipboard(burnerPK);
|
||||
notification.success("Burner wallet private key copied to clipboard");
|
||||
} catch (e) {
|
||||
const parsedError = getParsedError(e);
|
||||
notification.error(parsedError);
|
||||
if (modalCheckboxRef.current) modalCheckboxRef.current.checked = false;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<input type="checkbox" id="reveal-burner-pk-modal" className="modal-toggle" ref={modalCheckboxRef} />
|
||||
<label htmlFor="reveal-burner-pk-modal" className="modal cursor-pointer">
|
||||
<label className="modal-box relative">
|
||||
{/* dummy input to capture event onclick on modal box */}
|
||||
<input className="h-0 w-0 absolute top-0 left-0" />
|
||||
<label htmlFor="reveal-burner-pk-modal" className="btn btn-ghost btn-sm btn-circle absolute right-3 top-3">
|
||||
✕
|
||||
</label>
|
||||
<div>
|
||||
<p className="text-lg font-semibold m-0 p-0">Copy Burner Wallet Private Key</p>
|
||||
<div role="alert" className="alert alert-warning mt-4">
|
||||
<ShieldExclamationIcon className="h-6 w-6" />
|
||||
<span className="font-semibold">
|
||||
Burner wallets are intended for local development only and are not safe for storing real funds.
|
||||
</span>
|
||||
</div>
|
||||
<p>
|
||||
Your Private Key provides <strong>full access</strong> to your entire wallet and funds. This is
|
||||
currently stored <strong>temporarily</strong> in your browser.
|
||||
</p>
|
||||
<button className="btn btn-outline btn-error" onClick={handleCopyPK} disabled={isCopiedToClipboard}>
|
||||
Copy Private Key To Clipboard
|
||||
</button>
|
||||
</div>
|
||||
</label>
|
||||
</label>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
import { NetworkOptions } from "./NetworkOptions";
|
||||
import { useDisconnect } from "wagmi";
|
||||
import { ArrowLeftOnRectangleIcon, ChevronDownIcon } from "@heroicons/react/24/outline";
|
||||
|
||||
export const WrongNetworkDropdown = () => {
|
||||
const { disconnect } = useDisconnect();
|
||||
|
||||
return (
|
||||
<div className="dropdown dropdown-end mr-2">
|
||||
<label tabIndex={0} className="btn btn-error btn-sm dropdown-toggle gap-1">
|
||||
<span>Wrong network</span>
|
||||
<ChevronDownIcon className="h-6 w-4 ml-2 sm:ml-0" />
|
||||
</label>
|
||||
<ul
|
||||
tabIndex={0}
|
||||
className="dropdown-content menu p-2 mt-1 shadow-center shadow-accent bg-base-200 rounded-box gap-1"
|
||||
>
|
||||
<NetworkOptions />
|
||||
<li>
|
||||
<button
|
||||
className="menu-item text-error btn-sm rounded-xl! flex gap-3 py-3"
|
||||
type="button"
|
||||
onClick={() => disconnect()}
|
||||
>
|
||||
<ArrowLeftOnRectangleIcon className="h-6 w-4 ml-2 sm:ml-0" />
|
||||
<span>Disconnect</span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,69 @@
|
||||
"use client";
|
||||
|
||||
// @refresh reset
|
||||
import { Balance } from "../Balance";
|
||||
import { AddressInfoDropdown } from "./AddressInfoDropdown";
|
||||
import { AddressQRCodeModal } from "./AddressQRCodeModal";
|
||||
import { RevealBurnerPKModal } from "./RevealBurnerPKModal";
|
||||
import { WrongNetworkDropdown } from "./WrongNetworkDropdown";
|
||||
import { ConnectButton } from "@rainbow-me/rainbowkit";
|
||||
import { Address } from "viem";
|
||||
import { useNetworkColor } from "~~/hooks/scaffold-eth";
|
||||
import { useTargetNetwork } from "~~/hooks/scaffold-eth/useTargetNetwork";
|
||||
import { getBlockExplorerAddressLink } from "~~/utils/scaffold-eth";
|
||||
|
||||
/**
|
||||
* Custom Wagmi Connect Button (watch balance + custom design)
|
||||
*/
|
||||
export const RainbowKitCustomConnectButton = () => {
|
||||
const networkColor = useNetworkColor();
|
||||
const { targetNetwork } = useTargetNetwork();
|
||||
|
||||
return (
|
||||
<ConnectButton.Custom>
|
||||
{({ account, chain, openConnectModal, mounted }) => {
|
||||
const connected = mounted && account && chain;
|
||||
const blockExplorerAddressLink = account
|
||||
? getBlockExplorerAddressLink(targetNetwork, account.address)
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<>
|
||||
{(() => {
|
||||
if (!connected) {
|
||||
return (
|
||||
<button className="btn btn-primary btn-sm" onClick={openConnectModal} type="button">
|
||||
Connect Wallet
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
if (chain.unsupported || chain.id !== targetNetwork.id) {
|
||||
return <WrongNetworkDropdown />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col items-center mr-1">
|
||||
<Balance address={account.address as Address} className="min-h-0 h-auto" />
|
||||
<span className="text-xs" style={{ color: networkColor }}>
|
||||
{chain.name}
|
||||
</span>
|
||||
</div>
|
||||
<AddressInfoDropdown
|
||||
address={account.address as Address}
|
||||
displayName={account.displayName}
|
||||
ensAvatar={account.ensAvatar}
|
||||
blockExplorerAddressLink={blockExplorerAddressLink}
|
||||
/>
|
||||
<AddressQRCodeModal address={account.address as Address} modalId="qrcode-modal" />
|
||||
<RevealBurnerPKModal />
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</ConnectButton.Custom>
|
||||
);
|
||||
};
|
||||
7
packages/nextjs/components/scaffold-eth/index.tsx
Normal file
7
packages/nextjs/components/scaffold-eth/index.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
export * from "./Address/Address";
|
||||
export * from "./Balance";
|
||||
export * from "./BlockieAvatar";
|
||||
export * from "./Faucet";
|
||||
export * from "./FaucetButton";
|
||||
export * from "./Input";
|
||||
export * from "./RainbowKitCustomConnectButton";
|
||||
9
packages/nextjs/contracts/deployedContracts.ts
Normal file
9
packages/nextjs/contracts/deployedContracts.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* This file is autogenerated by Scaffold-ETH.
|
||||
* You should not edit it manually or your changes might be overwritten.
|
||||
*/
|
||||
import { GenericContractsDeclaration } from "~~/utils/scaffold-eth/contract";
|
||||
|
||||
const deployedContracts = {} as const;
|
||||
|
||||
export default deployedContracts satisfies GenericContractsDeclaration;
|
||||
16
packages/nextjs/contracts/externalContracts.ts
Normal file
16
packages/nextjs/contracts/externalContracts.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { GenericContractsDeclaration } from "~~/utils/scaffold-eth/contract";
|
||||
|
||||
/**
|
||||
* @example
|
||||
* const externalContracts = {
|
||||
* 1: {
|
||||
* DAI: {
|
||||
* address: "0x...",
|
||||
* abi: [...],
|
||||
* },
|
||||
* },
|
||||
* } as const;
|
||||
*/
|
||||
const externalContracts = {} as const;
|
||||
|
||||
export default externalContracts satisfies GenericContractsDeclaration;
|
||||
32
packages/nextjs/eslint.config.mjs
Normal file
32
packages/nextjs/eslint.config.mjs
Normal file
@@ -0,0 +1,32 @@
|
||||
import { FlatCompat } from "@eslint/eslintrc";
|
||||
import prettierPlugin from "eslint-plugin-prettier";
|
||||
import { defineConfig } from "eslint/config";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
});
|
||||
|
||||
export default defineConfig([
|
||||
{
|
||||
plugins: {
|
||||
prettier: prettierPlugin,
|
||||
},
|
||||
extends: compat.extends("next/core-web-vitals", "next/typescript", "prettier"),
|
||||
|
||||
rules: {
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
|
||||
"prettier/prettier": [
|
||||
"warn",
|
||||
{
|
||||
endOfLine: "auto",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
]);
|
||||
17
packages/nextjs/hooks/scaffold-eth/index.ts
Normal file
17
packages/nextjs/hooks/scaffold-eth/index.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
export * from "./useAnimationConfig";
|
||||
export * from "./useContractLogs";
|
||||
export * from "./useCopyToClipboard";
|
||||
export * from "./useDeployedContractInfo";
|
||||
export * from "./useFetchBlocks";
|
||||
export * from "./useInitializeNativeCurrencyPrice";
|
||||
export * from "./useNetworkColor";
|
||||
export * from "./useOutsideClick";
|
||||
export * from "./useScaffoldContract";
|
||||
export * from "./useScaffoldEventHistory";
|
||||
export * from "./useScaffoldReadContract";
|
||||
export * from "./useScaffoldWatchContractEvent";
|
||||
export * from "./useScaffoldWriteContract";
|
||||
export * from "./useTargetNetwork";
|
||||
export * from "./useTransactor";
|
||||
export * from "./useWatchBalance";
|
||||
export * from "./useSelectedNetwork";
|
||||
20
packages/nextjs/hooks/scaffold-eth/useAnimationConfig.ts
Normal file
20
packages/nextjs/hooks/scaffold-eth/useAnimationConfig.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
const ANIMATION_TIME = 2000;
|
||||
|
||||
export function useAnimationConfig(data: any) {
|
||||
const [showAnimation, setShowAnimation] = useState(false);
|
||||
const [prevData, setPrevData] = useState();
|
||||
|
||||
useEffect(() => {
|
||||
if (prevData !== undefined && prevData !== data) {
|
||||
setShowAnimation(true);
|
||||
setTimeout(() => setShowAnimation(false), ANIMATION_TIME);
|
||||
}
|
||||
setPrevData(data);
|
||||
}, [data, prevData]);
|
||||
|
||||
return {
|
||||
showAnimation,
|
||||
};
|
||||
}
|
||||
40
packages/nextjs/hooks/scaffold-eth/useContractLogs.ts
Normal file
40
packages/nextjs/hooks/scaffold-eth/useContractLogs.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTargetNetwork } from "./useTargetNetwork";
|
||||
import { Address, Log } from "viem";
|
||||
import { usePublicClient } from "wagmi";
|
||||
|
||||
export const useContractLogs = (address: Address) => {
|
||||
const [logs, setLogs] = useState<Log[]>([]);
|
||||
const { targetNetwork } = useTargetNetwork();
|
||||
const client = usePublicClient({ chainId: targetNetwork.id });
|
||||
|
||||
useEffect(() => {
|
||||
const fetchLogs = async () => {
|
||||
if (!client) return console.error("Client not found");
|
||||
try {
|
||||
const existingLogs = await client.getLogs({
|
||||
address: address,
|
||||
fromBlock: 0n,
|
||||
toBlock: "latest",
|
||||
});
|
||||
setLogs(existingLogs);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch logs:", error);
|
||||
}
|
||||
};
|
||||
fetchLogs();
|
||||
|
||||
return client?.watchBlockNumber({
|
||||
onBlockNumber: async (_blockNumber, prevBlockNumber) => {
|
||||
const newLogs = await client.getLogs({
|
||||
address: address,
|
||||
fromBlock: prevBlockNumber,
|
||||
toBlock: "latest",
|
||||
});
|
||||
setLogs(prevLogs => [...prevLogs, ...newLogs]);
|
||||
},
|
||||
});
|
||||
}, [address, client]);
|
||||
|
||||
return logs;
|
||||
};
|
||||
19
packages/nextjs/hooks/scaffold-eth/useCopyToClipboard.ts
Normal file
19
packages/nextjs/hooks/scaffold-eth/useCopyToClipboard.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { useState } from "react";
|
||||
|
||||
export const useCopyToClipboard = () => {
|
||||
const [isCopiedToClipboard, setIsCopiedToClipboard] = useState(false);
|
||||
|
||||
const copyToClipboard = async (text: string) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
setIsCopiedToClipboard(true);
|
||||
setTimeout(() => {
|
||||
setIsCopiedToClipboard(false);
|
||||
}, 800);
|
||||
} catch (err) {
|
||||
console.error("Failed to copy text:", err);
|
||||
}
|
||||
};
|
||||
|
||||
return { copyToClipboard, isCopiedToClipboard };
|
||||
};
|
||||
@@ -0,0 +1,86 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useIsMounted } from "usehooks-ts";
|
||||
import { usePublicClient } from "wagmi";
|
||||
import { useSelectedNetwork } from "~~/hooks/scaffold-eth";
|
||||
import {
|
||||
Contract,
|
||||
ContractCodeStatus,
|
||||
ContractName,
|
||||
UseDeployedContractConfig,
|
||||
contracts,
|
||||
} from "~~/utils/scaffold-eth/contract";
|
||||
|
||||
type DeployedContractData<TContractName extends ContractName> = {
|
||||
data: Contract<TContractName> | undefined;
|
||||
isLoading: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the matching contract info for the provided contract name from the contracts present in deployedContracts.ts
|
||||
* and externalContracts.ts corresponding to targetNetworks configured in scaffold.config.ts
|
||||
*/
|
||||
export function useDeployedContractInfo<TContractName extends ContractName>(
|
||||
config: UseDeployedContractConfig<TContractName>,
|
||||
): DeployedContractData<TContractName>;
|
||||
/**
|
||||
* @deprecated Use object parameter version instead: useDeployedContractInfo({ contractName: "YourContract" })
|
||||
*/
|
||||
export function useDeployedContractInfo<TContractName extends ContractName>(
|
||||
contractName: TContractName,
|
||||
): DeployedContractData<TContractName>;
|
||||
|
||||
export function useDeployedContractInfo<TContractName extends ContractName>(
|
||||
configOrName: UseDeployedContractConfig<TContractName> | TContractName,
|
||||
): DeployedContractData<TContractName> {
|
||||
const isMounted = useIsMounted();
|
||||
|
||||
const finalConfig: UseDeployedContractConfig<TContractName> =
|
||||
typeof configOrName === "string" ? { contractName: configOrName } : (configOrName as any);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof configOrName === "string") {
|
||||
console.warn(
|
||||
"Using `useDeployedContractInfo` with a string parameter is deprecated. Please use the object parameter version instead.",
|
||||
);
|
||||
}
|
||||
}, [configOrName]);
|
||||
const { contractName, chainId } = finalConfig;
|
||||
const selectedNetwork = useSelectedNetwork(chainId);
|
||||
const deployedContract = contracts?.[selectedNetwork.id]?.[contractName as ContractName] as Contract<TContractName>;
|
||||
const [status, setStatus] = useState<ContractCodeStatus>(ContractCodeStatus.LOADING);
|
||||
const publicClient = usePublicClient({ chainId: selectedNetwork.id });
|
||||
|
||||
useEffect(() => {
|
||||
const checkContractDeployment = async () => {
|
||||
try {
|
||||
if (!isMounted() || !publicClient) return;
|
||||
|
||||
if (!deployedContract) {
|
||||
setStatus(ContractCodeStatus.NOT_FOUND);
|
||||
return;
|
||||
}
|
||||
|
||||
const code = await publicClient.getBytecode({
|
||||
address: deployedContract.address,
|
||||
});
|
||||
|
||||
// If contract code is `0x` => no contract deployed on that address
|
||||
if (code === "0x") {
|
||||
setStatus(ContractCodeStatus.NOT_FOUND);
|
||||
return;
|
||||
}
|
||||
setStatus(ContractCodeStatus.DEPLOYED);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setStatus(ContractCodeStatus.NOT_FOUND);
|
||||
}
|
||||
};
|
||||
|
||||
checkContractDeployment();
|
||||
}, [isMounted, contractName, deployedContract, publicClient]);
|
||||
|
||||
return {
|
||||
data: status === ContractCodeStatus.DEPLOYED ? deployedContract : undefined,
|
||||
isLoading: status === ContractCodeStatus.LOADING,
|
||||
};
|
||||
}
|
||||
21
packages/nextjs/hooks/scaffold-eth/useDisplayUsdMode.ts
Normal file
21
packages/nextjs/hooks/scaffold-eth/useDisplayUsdMode.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useGlobalState } from "~~/services/store/store";
|
||||
|
||||
export const useDisplayUsdMode = ({ defaultUsdMode = false }: { defaultUsdMode?: boolean }) => {
|
||||
const nativeCurrencyPrice = useGlobalState(state => state.nativeCurrency.price);
|
||||
const isPriceFetched = nativeCurrencyPrice > 0;
|
||||
const predefinedUsdMode = isPriceFetched ? Boolean(defaultUsdMode) : false;
|
||||
const [displayUsdMode, setDisplayUsdMode] = useState(predefinedUsdMode);
|
||||
|
||||
useEffect(() => {
|
||||
setDisplayUsdMode(predefinedUsdMode);
|
||||
}, [predefinedUsdMode]);
|
||||
|
||||
const toggleDisplayUsdMode = useCallback(() => {
|
||||
if (isPriceFetched) {
|
||||
setDisplayUsdMode(!displayUsdMode);
|
||||
}
|
||||
}, [displayUsdMode, isPriceFetched]);
|
||||
|
||||
return { displayUsdMode, toggleDisplayUsdMode };
|
||||
};
|
||||
133
packages/nextjs/hooks/scaffold-eth/useFetchBlocks.ts
Normal file
133
packages/nextjs/hooks/scaffold-eth/useFetchBlocks.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import {
|
||||
Block,
|
||||
Hash,
|
||||
Transaction,
|
||||
TransactionReceipt,
|
||||
createTestClient,
|
||||
publicActions,
|
||||
walletActions,
|
||||
webSocket,
|
||||
} from "viem";
|
||||
import { hardhat } from "viem/chains";
|
||||
import { decodeTransactionData } from "~~/utils/scaffold-eth";
|
||||
|
||||
const BLOCKS_PER_PAGE = 20;
|
||||
|
||||
export const testClient = createTestClient({
|
||||
chain: hardhat,
|
||||
mode: "hardhat",
|
||||
transport: webSocket("ws://127.0.0.1:8545"),
|
||||
})
|
||||
.extend(publicActions)
|
||||
.extend(walletActions);
|
||||
|
||||
export const useFetchBlocks = () => {
|
||||
const [blocks, setBlocks] = useState<Block[]>([]);
|
||||
const [transactionReceipts, setTransactionReceipts] = useState<{
|
||||
[key: string]: TransactionReceipt;
|
||||
}>({});
|
||||
const [currentPage, setCurrentPage] = useState(0);
|
||||
const [totalBlocks, setTotalBlocks] = useState(0n);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
const fetchBlocks = useCallback(async () => {
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const blockNumber = await testClient.getBlockNumber();
|
||||
setTotalBlocks(blockNumber);
|
||||
|
||||
const startingBlock = blockNumber - BigInt(currentPage * BLOCKS_PER_PAGE);
|
||||
const blockNumbersToFetch = Array.from(
|
||||
{ length: Number(BLOCKS_PER_PAGE < startingBlock + 1n ? BLOCKS_PER_PAGE : startingBlock + 1n) },
|
||||
(_, i) => startingBlock - BigInt(i),
|
||||
);
|
||||
|
||||
const blocksWithTransactions = blockNumbersToFetch.map(async blockNumber => {
|
||||
try {
|
||||
return testClient.getBlock({ blockNumber, includeTransactions: true });
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err : new Error("An error occurred."));
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
const fetchedBlocks = await Promise.all(blocksWithTransactions);
|
||||
|
||||
fetchedBlocks.forEach(block => {
|
||||
block.transactions.forEach(tx => decodeTransactionData(tx as Transaction));
|
||||
});
|
||||
|
||||
const txReceipts = await Promise.all(
|
||||
fetchedBlocks.flatMap(block =>
|
||||
block.transactions.map(async tx => {
|
||||
try {
|
||||
const receipt = await testClient.getTransactionReceipt({ hash: (tx as Transaction).hash });
|
||||
return { [(tx as Transaction).hash]: receipt };
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err : new Error("An error occurred."));
|
||||
throw err;
|
||||
}
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
setBlocks(fetchedBlocks);
|
||||
setTransactionReceipts(prevReceipts => ({ ...prevReceipts, ...Object.assign({}, ...txReceipts) }));
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err : new Error("An error occurred."));
|
||||
}
|
||||
}, [currentPage]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchBlocks();
|
||||
}, [fetchBlocks]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleNewBlock = async (newBlock: any) => {
|
||||
try {
|
||||
if (currentPage === 0) {
|
||||
if (newBlock.transactions.length > 0) {
|
||||
const transactionsDetails = await Promise.all(
|
||||
newBlock.transactions.map((txHash: string) => testClient.getTransaction({ hash: txHash as Hash })),
|
||||
);
|
||||
newBlock.transactions = transactionsDetails;
|
||||
}
|
||||
|
||||
newBlock.transactions.forEach((tx: Transaction) => decodeTransactionData(tx as Transaction));
|
||||
|
||||
const receipts = await Promise.all(
|
||||
newBlock.transactions.map(async (tx: Transaction) => {
|
||||
try {
|
||||
const receipt = await testClient.getTransactionReceipt({ hash: (tx as Transaction).hash });
|
||||
return { [(tx as Transaction).hash]: receipt };
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err : new Error("An error occurred fetching receipt."));
|
||||
throw err;
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
setBlocks(prevBlocks => [newBlock, ...prevBlocks.slice(0, BLOCKS_PER_PAGE - 1)]);
|
||||
setTransactionReceipts(prevReceipts => ({ ...prevReceipts, ...Object.assign({}, ...receipts) }));
|
||||
}
|
||||
if (newBlock.number) {
|
||||
setTotalBlocks(newBlock.number);
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err : new Error("An error occurred."));
|
||||
}
|
||||
};
|
||||
|
||||
return testClient.watchBlocks({ onBlock: handleNewBlock, includeTransactions: true });
|
||||
}, [currentPage]);
|
||||
|
||||
return {
|
||||
blocks,
|
||||
transactionReceipts,
|
||||
currentPage,
|
||||
totalBlocks,
|
||||
setCurrentPage,
|
||||
error,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
import { useCallback, useEffect } from "react";
|
||||
import { useTargetNetwork } from "./useTargetNetwork";
|
||||
import { useInterval } from "usehooks-ts";
|
||||
import scaffoldConfig from "~~/scaffold.config";
|
||||
import { useGlobalState } from "~~/services/store/store";
|
||||
import { fetchPriceFromUniswap } from "~~/utils/scaffold-eth";
|
||||
|
||||
const enablePolling = false;
|
||||
|
||||
/**
|
||||
* Get the price of Native Currency based on Native Token/DAI trading pair from Uniswap SDK
|
||||
*/
|
||||
export const useInitializeNativeCurrencyPrice = () => {
|
||||
const setNativeCurrencyPrice = useGlobalState(state => state.setNativeCurrencyPrice);
|
||||
const setIsNativeCurrencyFetching = useGlobalState(state => state.setIsNativeCurrencyFetching);
|
||||
const { targetNetwork } = useTargetNetwork();
|
||||
|
||||
const fetchPrice = useCallback(async () => {
|
||||
setIsNativeCurrencyFetching(true);
|
||||
const price = await fetchPriceFromUniswap(targetNetwork);
|
||||
setNativeCurrencyPrice(price);
|
||||
setIsNativeCurrencyFetching(false);
|
||||
}, [setIsNativeCurrencyFetching, setNativeCurrencyPrice, targetNetwork]);
|
||||
|
||||
// Get the price of ETH from Uniswap on mount
|
||||
useEffect(() => {
|
||||
fetchPrice();
|
||||
}, [fetchPrice]);
|
||||
|
||||
// Get the price of ETH from Uniswap at a given interval
|
||||
useInterval(fetchPrice, enablePolling ? scaffoldConfig.pollingInterval : null);
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user