├── .github └── workflows │ └── release.yml ├── .gitignore ├── Dockerfile ├── README.md ├── contracts └── SimpleToken.sol ├── data └── config.json ├── hardhat.config.ts ├── package.json ├── scripts ├── common.ts ├── deploy.ts ├── evm-tps-server.ts └── substrate-tps-server.ts ├── test └── SimpleTokenTest.ts ├── tsconfig.json └── yarn.lock /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Publish image to Docker Hub 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | build: 10 | name: Build and publish image 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout sources 14 | uses: actions/checkout@v3 15 | with: 16 | fetch-depth: 1 17 | - name: Login to Dockerhub 18 | uses: docker/login-action@v2 19 | with: 20 | username: ${{ secrets.DOCKERHUB_USERNAME }} 21 | password: ${{ secrets.DOCKERHUB_TOKEN }} 22 | - name: Build and push 23 | id: docker_build 24 | uses: docker/build-push-action@v4 25 | with: 26 | push: true 27 | file: Dockerfile 28 | tags: | 29 | paritytech/evm-tps:latest 30 | - name: Image digest 31 | run: echo ${{ steps.docker_build.outputs.digest }} 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | 132 | # TS 133 | typechain 134 | typechain-types 135 | 136 | # Hardhat files 137 | cache 138 | artifacts 139 | 140 | # Local 141 | data/ 142 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM docker.io/library/node:lts-bullseye-slim 2 | 3 | # The image contains a https://github.com/arturgontijo/evm-tps since it's needed to run the tests 4 | WORKDIR /evm-tps 5 | COPY . /evm-tps 6 | 7 | RUN npm install -g npm@9.7.2 && \ 8 | yarn && \ 9 | yarn cache clean 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Simple EVM TPS tool 2 | 3 | ```shell 4 | git clone https://github.com/paritytech/evm-tps.git 5 | cd evm-tps 6 | 7 | yarn 8 | ``` 9 | 10 | ## Setup: 11 | 12 | Change network's parameters ("local") in [hardhat.config.json](hardhat.config.ts): 13 | 14 | Change test's parameters in [data/config.json](./data/config.json): 15 | 16 | 1. This will deploy the ERC20 contract and will prepare a server to send `transferLoop()` transactions, asserting final Other's token balance: 17 | ```json 18 | { 19 | "tpsServerHost": "0.0.0.0", 20 | "tpsServerPort": 8181, 21 | "variant": "substrate", 22 | "deployer": { 23 | "address": "0x6Be02d1d3665660d22FF9624b7BE0551ee1Ac91b", 24 | "privateKey": "0x99B3C12287537E38C90A9219D4CB074A89A16E9CDB20BF85728EBD97C343E342" 25 | }, 26 | "fundSenders": true, 27 | "accounts": 100, 28 | "workers": 80, 29 | "sendRawTransaction": true, 30 | "timeout": 15000, 31 | "tokenAddress": "", 32 | "tokenMethod": "transferLoop", 33 | "tokenAmountToMint": 1000000000, 34 | "tokenTransferMultiplier": 1, 35 | "tokenAssert": true, 36 | "transactions": 50000, 37 | "gasLimit": "200000", 38 | "txpoolMaxLength": -1, 39 | "txpoolMultiplier": 3, 40 | "txpoolLimit": 7500, 41 | "checkersInterval": 250, 42 | "estimate": false, 43 | "verbose": false 44 | } 45 | ``` 46 | 47 | 2. This one already has the token deployed at `tokenAddress`, so it will wait to send `transferLoop()` (5 * `transfer()`) transactions + tokenAssert: 48 | ```json 49 | { 50 | "tpsServerHost": "0.0.0.0", 51 | "tpsServerPort": 8181, 52 | "variant": "substrate", 53 | "deployer": { 54 | "address": "0x6Be02d1d3665660d22FF9624b7BE0551ee1Ac91b", 55 | "privateKey": "0x99B3C12287537E38C90A9219D4CB074A89A16E9CDB20BF85728EBD97C343E342" 56 | }, 57 | "fundSenders": true, 58 | "accounts": 100, 59 | "workers": 80, 60 | "sendRawTransaction": true, 61 | "timeout": 15000, 62 | "tokenAddress": "", 63 | "tokenMethod": "transferLoop", 64 | "tokenAmountToMint": 1000000000, 65 | "tokenTransferMultiplier": 1, 66 | "tokenAssert": true, 67 | "transactions": 50000, 68 | "gasLimit": "200000", 69 | "txpoolMaxLength": -1, 70 | "txpoolMultiplier": 3, 71 | "txpoolLimit": 7500, 72 | "checkersInterval": 250, 73 | "estimate": false, 74 | "verbose": false 75 | } 76 | ``` 77 | 78 | 79 | 3. This one has a `transfer()` hardcoded in the `payloads` field: 80 | ```json 81 | { 82 | "tpsServerHost": "0.0.0.0", 83 | "tpsServerPort": 8181, 84 | "variant": "substrate", 85 | "deployer": { 86 | "address": "0x6Be02d1d3665660d22FF9624b7BE0551ee1Ac91b", 87 | "privateKey": "0x99B3C12287537E38C90A9219D4CB074A89A16E9CDB20BF85728EBD97C343E342" 88 | }, 89 | "fundSenders": true, 90 | "accounts": 100, 91 | "workers": 80, 92 | "sendRawTransaction": true, 93 | "timeout": 15000, 94 | "tokenAddress": "", 95 | "tokenMethod": "transferLoop", 96 | "tokenAmountToMint": 1000000000, 97 | "tokenTransferMultiplier": 1, 98 | "tokenAssert": true, 99 | "transactions": 50000, 100 | "gasLimit": "200000", 101 | "txpoolMaxLength": -1, 102 | "txpoolMultiplier": 3, 103 | "txpoolLimit": 7500, 104 | "checkersInterval": 250, 105 | "estimate": false, 106 | "verbose": false, 107 | "payloads": [ 108 | { 109 | "data": "0xa9059cbb000000000000000000000000ea8d69db60401a766e1083beba3a34cafa13151c0000000000000000000000000000000000000000000000000000000000000001", 110 | "from": "0x48A78AeA1c4F8C24EDfE7FE0973F05D3f3d1763C", 111 | "to": "0x030c5D377E202F52CF30b7f855e09aC0589D53ab" 112 | } 113 | ] 114 | } 115 | ``` 116 | 117 | 4. This one sends ETH (`send()`) via `payloads` field and assert the destination `"to"` ETH balance at the end: 118 | ```json 119 | { 120 | "tpsServerHost": "0.0.0.0", 121 | "tpsServerPort": 8181, 122 | "variant": "substrate", 123 | "deployer": { 124 | "address": "0x6Be02d1d3665660d22FF9624b7BE0551ee1Ac91b", 125 | "privateKey": "0x99B3C12287537E38C90A9219D4CB074A89A16E9CDB20BF85728EBD97C343E342" 126 | }, 127 | "fundSenders": true, 128 | "accounts": 100, 129 | "workers": 80, 130 | "sendRawTransaction": true, 131 | "timeout": 15000, 132 | "tokenAddress": "", 133 | "tokenMethod": "transferLoop", 134 | "tokenAmountToMint": 1000000000, 135 | "tokenTransferMultiplier": 1, 136 | "tokenAssert": true, 137 | "transactions": 50000, 138 | "gasLimit": "200000", 139 | "txpoolMaxLength": -1, 140 | "txpoolMultiplier": 3, 141 | "txpoolLimit": 7500, 142 | "checkersInterval": 250, 143 | "estimate": false, 144 | "verbose": false, 145 | "payloads": [ 146 | { 147 | "from": "0x6Be02d1d3665660d22FF9624b7BE0551ee1Ac91b", 148 | "to": "0xEA8D69Db60401A766e1083bebA3A34cAfa13151C", 149 | "value": "0x1414" 150 | } 151 | ] 152 | } 153 | ``` 154 | 155 | ## Deployer: 156 | 157 | 1. CI pre funded EVM Account (Frontier) 158 | ```json 159 | "variant": "frontier", 160 | "deployer": { 161 | "address": "0x6Be02d1d3665660d22FF9624b7BE0551ee1Ac91b", 162 | "privateKey": "0x99B3C12287537E38C90A9219D4CB074A89A16E9CDB20BF85728EBD97C343E342" 163 | }, 164 | ``` 165 | 166 | 2. Alith (Substrate/Frontier) 167 | ```json 168 | "variant": "frontier", 169 | "deployer": { 170 | "address": "0xf24FF3a9CF04c71Dbc94D0b566f7A27B94566cac", 171 | "privateKey": "0x5fb92d6e98884f76de468fa3f6278f8807c48bebc13595d45af5bdc4da702133" 172 | }, 173 | ``` 174 | 175 | 3. Alice (Substrate) 176 | ```json 177 | "variant": "substrate", 178 | "deployer": { 179 | "address": "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY", 180 | "privateKey": "0xe5be9a5092b81bca64be81d212e7f2f9eba183bb7a90954f7b76361f6edb5c0a" 181 | }, 182 | ``` 183 | 184 | ## Running: 185 | 186 | To run the TPS server for EVM (Frontier): 187 | 188 | ```shell 189 | yarn evm 190 | ``` 191 | 192 | Or, to run the TPS server for Substrate (remember to set the `deploye.privateKey` properly): 193 | 194 | ```shell 195 | yarn substrate 196 | ``` 197 | 198 | After the initial setup is done, you can trigger an "auto" run by: 199 | ```shell 200 | curl -X GET "http://0.0.0.0:8181/auto" 201 | ``` 202 | 203 | That command will send `50,000` transactions to the target using `80` threads (set by `transactions` and `workers` in the [data/config.json](./data/config.json)). 204 | 205 | 206 | Or sending requests via `artillery`/`wrk` to: 207 | ```shell 208 | artillery quick --count 50 --num 500 http://0.0.0.0:8181/sendRawTransaction 209 | ``` 210 | 211 | That command will send 25,000 (`50` "users" sending `500` requests each) requests to `/sendRawTransaction`. 212 | 213 | ``` 214 | wrk -t 50 -c 50 -d 600 --latency --timeout 1m http://0.0.0.0:8181/sendRawTransaction 215 | ``` 216 | 217 | That command will spawn 50 threads to send requests to `/sendRawTransaction` in 600 seconds. 218 | 219 | To run it using a different JSON files directory (other than `data/`) by setting `EVM_TPS_ROOT_DIR`: 220 | 221 | ```shell 222 | EVM_TPS_ROOT_DIR="path/to/dir" npx hardhat run scripts/tps-server.ts --network local 223 | ``` 224 | 225 | ## TODOs: 226 | 227 | - Create common files for both setups. 228 | - Remove Hardhat. 229 | - Create a step-by-step test option. 230 | -------------------------------------------------------------------------------- /contracts/SimpleToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.9; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 5 | 6 | contract SimpleToken is ERC20 { 7 | address public owner; 8 | bool public paused = true; 9 | mapping(address => uint) public map; 10 | 11 | modifier whenNotPaused() { 12 | require(!paused, "Onwer has not started the contract yet."); 13 | _; 14 | } 15 | 16 | constructor( 17 | string memory _name, 18 | string memory _symbol 19 | ) ERC20(_name, _symbol) { 20 | owner = msg.sender; 21 | } 22 | 23 | // Dummy logic just to get revert msg here and in the mintTo() if wanted. 24 | function start() public { 25 | require(msg.sender == owner, "Only owner can start it."); 26 | paused = false; 27 | } 28 | 29 | function pause() public { 30 | require(msg.sender == owner, "Only owner can pause it."); 31 | paused = true; 32 | } 33 | 34 | function mintTo(address _to, uint _amount) public whenNotPaused { 35 | _mint(_to, _amount); 36 | } 37 | 38 | // n=1 55k 39 | // n=10 66k 40 | // n=100 355k 41 | // n=250 833k 42 | // n=500 1.63M 43 | // n=1000 3.23M 44 | function transferLoop( 45 | uint16 n, 46 | address _to, 47 | uint _amount 48 | ) public whenNotPaused { 49 | for (uint16 i = 0; i < n; i++) { 50 | transfer(_to, _amount); 51 | } 52 | } 53 | 54 | // n=1 27k 55 | // n=10 46k 56 | // n=100 240k 57 | // n=250 561k 58 | // n=500 1.10M 59 | // n=1000 2.17M 60 | function eventLoop( 61 | uint16 n, 62 | address _to, 63 | uint _amount 64 | ) public whenNotPaused { 65 | for (uint16 i = 0; i < n; i++) { 66 | emit Transfer(msg.sender, _to, _amount); 67 | } 68 | } 69 | 70 | // n=1 30k 71 | // n=10 37k 72 | // n=100 100k 73 | // n=250 205k 74 | // n=500 380k 75 | // n=1000 745k 76 | function storageLoop( 77 | uint16 n, 78 | address _to, 79 | uint _amount 80 | ) public whenNotPaused { 81 | for (uint16 i = 0; i < n; i++) { 82 | map[_to] += _amount; 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /data/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "tpsServerHost": "0.0.0.0", 3 | "tpsServerPort": 8181, 4 | "variant": "substrate", 5 | "deployer": { 6 | "address": "0x6Be02d1d3665660d22FF9624b7BE0551ee1Ac91b", 7 | "privateKey": "0x99B3C12287537E38C90A9219D4CB074A89A16E9CDB20BF85728EBD97C343E342" 8 | }, 9 | "fundSenders": true, 10 | "accounts": 100, 11 | "workers": 80, 12 | "sendRawTransaction": true, 13 | "timeout": 15000, 14 | "tokenAddress": "", 15 | "tokenMethod": "transferLoop", 16 | "tokenAmountToMint": 1000000000, 17 | "tokenTransferMultiplier": 1, 18 | "tokenAssert": true, 19 | "transactions": 50000, 20 | "gasLimit": "200000", 21 | "txpoolMaxLength": -1, 22 | "txpoolMultiplier": 3, 23 | "txpoolLimit": 7500, 24 | "checkersInterval": 250, 25 | "estimate": false, 26 | "verbose": false 27 | } -------------------------------------------------------------------------------- /hardhat.config.ts: -------------------------------------------------------------------------------- 1 | import { HardhatUserConfig } from "hardhat/config"; 2 | import "@nomicfoundation/hardhat-toolbox"; 3 | 4 | const optimizerSettings = { 5 | settings: { 6 | optimizer: { 7 | enabled: true, 8 | runs: 200, 9 | }, 10 | outputSelection: { 11 | "*": { 12 | "*": ["devdoc", "userdoc", "metadata"], 13 | "": [], 14 | }, 15 | }, 16 | }, 17 | } 18 | 19 | const config: HardhatUserConfig = { 20 | solidity: { 21 | compilers: [ 22 | { 23 | version: "0.8.18", 24 | ...optimizerSettings 25 | } 26 | ] 27 | }, 28 | defaultNetwork: "local", 29 | networks: { 30 | local: { 31 | url: "http://127.0.0.1:8545", 32 | accounts: [ 33 | "0x99B3C12287537E38C90A9219D4CB074A89A16E9CDB20BF85728EBD97C343E342", 34 | "0xE2033D436CE0614ACC1EE15BD20428B066013F827A15CC78B063F83AC0BAAE64", 35 | ], 36 | }, 37 | }, 38 | }; 39 | 40 | export default config; 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "evm-tps", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "evm": "npx hardhat run scripts/evm-tps-server.ts", 9 | "substrate": "npx hardhat run scripts/substrate-tps-server.ts" 10 | }, 11 | "author": "", 12 | "license": "ISC", 13 | "devDependencies": { 14 | "@ethersproject/abi": "^5.4.7", 15 | "@ethersproject/providers": "^5.4.7", 16 | "@nomicfoundation/hardhat-chai-matchers": "^1.0.0", 17 | "@nomicfoundation/hardhat-network-helpers": "^1.0.0", 18 | "@nomicfoundation/hardhat-toolbox": "^2.0.0", 19 | "@nomiclabs/hardhat-ethers": "^2.0.0", 20 | "@nomiclabs/hardhat-etherscan": "^3.0.0", 21 | "@openzeppelin/contracts": "^4.8.3", 22 | "@typechain/ethers-v5": "^10.1.0", 23 | "@typechain/hardhat": "^6.1.2", 24 | "@types/chai": "^4.2.0", 25 | "@types/mocha": ">=9.1.0", 26 | "@types/node": ">=12.0.0", 27 | "axios": "^1.4.0", 28 | "chai": "^4.2.0", 29 | "ethers": "^5.4.7", 30 | "hardhat": "^2.14.0", 31 | "hardhat-gas-reporter": "^1.0.8", 32 | "solidity-coverage": "^0.8.0", 33 | "ts-node": ">=8.0.0", 34 | "typechain": "^8.1.0", 35 | "typescript": ">=4.5.0" 36 | }, 37 | "dependencies": { 38 | "@polkadot/api": "^10.9.1", 39 | "@polkadot/keyring": "^12.3.2", 40 | "body-parser": "^1.20.2", 41 | "express": "^4.18.2" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /scripts/common.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "hardhat"; 2 | import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; 3 | import { Wallet } from "@ethersproject/wallet"; 4 | 5 | export const deploy = async (deployer: Wallet | SignerWithAddress) => { 6 | console.log(`Deploying SimpleToken contract...`); 7 | const SimpleToken = await ethers.getContractFactory("SimpleToken", deployer); 8 | const token = await SimpleToken.deploy("SimpleToken", "STK", { gasLimit: 10000000, gasPrice: await ethers.provider.getGasPrice() }); 9 | let tx = await token.deployed(); 10 | console.log(`SimpleToken deployed to ${token.address}`); 11 | await token.deployTransaction.wait(); 12 | return token; 13 | } 14 | -------------------------------------------------------------------------------- /scripts/deploy.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "hardhat"; 2 | import { deploy } from "./common"; 3 | 4 | const main = async () => { 5 | const [owner] = await ethers.getSigners(); 6 | const _ = await deploy(owner); 7 | } 8 | 9 | main().catch((error) => { 10 | console.error(error); 11 | process.exitCode = 1; 12 | }); 13 | -------------------------------------------------------------------------------- /scripts/evm-tps-server.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import { promisify } from "util"; 3 | 4 | import axios from "axios"; 5 | import express from "express"; 6 | import BodyParser from "body-parser"; 7 | 8 | import { ethers, network } from "hardhat"; 9 | 10 | import { Wallet } from "@ethersproject/wallet"; 11 | import { BigNumber } from "ethers"; 12 | import { PopulatedTransaction } from "ethers/lib/ethers"; 13 | 14 | import { deploy } from "./common"; 15 | import { Block } from "@ethersproject/providers"; 16 | 17 | const EVM_TPS_ROOT_DIR = process.env.ROOT_DIR || "data"; 18 | const EVM_TPS_CONFIG_FILE = `${EVM_TPS_ROOT_DIR}/config.json`; 19 | const EVM_TPS_SENDERS_FILE = `${EVM_TPS_ROOT_DIR}/senders.json`; 20 | const EVM_TPS_RECEIVERS_FILE = `${EVM_TPS_ROOT_DIR}/receivers.json`; 21 | 22 | interface Balances { 23 | before: number, 24 | after: number, 25 | } 26 | 27 | // Map from key-id to the private key 28 | const sendersMap = new Map(); 29 | const receiversMap = new Map(); 30 | const rcvBalances = new Map(); 31 | 32 | const nonceMap = new Map(); 33 | 34 | const receiptsMap = new Map(); 35 | 36 | const workersMap = new Map(); 37 | const sendersBusyMap = new Map(); 38 | const sendersFreeMap = new Map(); 39 | const sendersTxnMap = new Map(); 40 | const sendersErrMap = new Map(); 41 | 42 | const reqErrorsMap = new Map(); 43 | 44 | let txPoolLength = 0; 45 | 46 | // Should be sufficient to send 100k transactions 47 | let gasPrice = ethers.BigNumber.from(1_000_000_000_000); 48 | let chainGasPrice = ethers.BigNumber.from(0); 49 | 50 | let reqCounter = 0; 51 | let reqErrCounter = 0; 52 | let nextKey = 0; 53 | let lastTxHash = ""; 54 | let hardstop = false; 55 | 56 | const zeroPad = (num: number, places: number) => String(num).padStart(places, '0') 57 | 58 | interface TPSConfig { 59 | tpsServerHost: string, 60 | tpsServerPort: number, 61 | endpoint: string; 62 | variant: string; 63 | deployer: { 64 | address: string, 65 | privateKey: string, 66 | }, 67 | fundSenders: boolean, 68 | accounts: number, 69 | workers: number, 70 | sendRawTransaction: boolean; 71 | timeout: number, 72 | tokenAddress: string; 73 | tokenMethod: string; 74 | tokenAmountToMint: number; 75 | tokenTransferMultiplier: number; 76 | tokenAssert: boolean | undefined; 77 | transactions: number, 78 | gasLimit: string; 79 | txpoolMaxLength: number; 80 | txpoolMultiplier: number; 81 | txpoolLimit: number, 82 | checkersInterval: number; 83 | estimate: boolean | undefined; 84 | payloads: UnsignedTx[] | PopulatedTransaction[] | undefined; 85 | verbose: boolean; 86 | } 87 | 88 | interface UnsignedTx { 89 | from: string; 90 | to: string; 91 | value?: BigNumber | string; 92 | data: string; 93 | gasPrice?: BigNumber | string; 94 | gasLimit?: BigNumber | string; 95 | nonce?: number; 96 | chainId?: number; 97 | } 98 | 99 | const readJSON = async (filename: string) => { 100 | const j = await promisify(fs.readFile)(filename); 101 | return JSON.parse(j.toString()); 102 | } 103 | 104 | const getDeployer = async (configFilename: string) => { 105 | try { 106 | const config = await readJSON(configFilename); 107 | return new ethers.Wallet(config.deployer.privateKey, ethers.provider); 108 | } catch (_) { 109 | return ethers.Wallet.createRandom().connect(ethers.provider); 110 | } 111 | } 112 | 113 | const setConfig = async (configFilename: string, deployer: Wallet) => { 114 | // @ts-ignore 115 | let url = network.config.url; 116 | let config: TPSConfig = { 117 | tpsServerHost: "0.0.0.0", 118 | tpsServerPort: 8181, 119 | endpoint: url || "http://127.0.0.1:9944", 120 | variant: "substrate", 121 | deployer: { 122 | address: deployer.address, 123 | privateKey: deployer.privateKey, 124 | }, 125 | fundSenders: true, 126 | accounts: 100, 127 | workers: 80, 128 | sendRawTransaction: true, 129 | timeout: 5000, 130 | tokenAddress: "", 131 | tokenMethod: "transferLoop", 132 | tokenAmountToMint: 1_000_000_000, 133 | tokenTransferMultiplier: 1, 134 | tokenAssert: true, 135 | transactions: 30_000, 136 | gasLimit: "200000", 137 | txpoolMaxLength: -1, 138 | txpoolMultiplier: 2, 139 | txpoolLimit: 7500, 140 | checkersInterval: 250, 141 | estimate: false, 142 | payloads: undefined, 143 | verbose: false, 144 | }; 145 | 146 | if (fs.existsSync(configFilename)) { 147 | const fromJSON = await readJSON(configFilename); 148 | config = { ...config, ...fromJSON }; 149 | } 150 | 151 | const gasLimit = ethers.BigNumber.from(config.gasLimit); 152 | 153 | chainGasPrice = await ethers.provider.getGasPrice(); 154 | if (chainGasPrice.mul(2).gt(gasPrice)) gasPrice = chainGasPrice.mul(2); 155 | 156 | let tokenAddress = config.tokenAddress || ""; 157 | if (tokenAddress === "" && config.payloads?.length) tokenAddress = config.payloads[0].to ? config.payloads[0].to : tokenAddress; 158 | 159 | if (tokenAddress !== "") { 160 | const bytecode = await ethers.provider.getCode(tokenAddress); 161 | if (bytecode.length <= 2) tokenAddress = ""; // 0x 162 | } 163 | 164 | if (tokenAddress === "" && config.payloads === undefined) { 165 | const token = await deploy(deployer); 166 | let tx = await token.start({ gasLimit, gasPrice }); 167 | await tx.wait(); 168 | tx = await token.mintTo(deployer.address, config.tokenAmountToMint); 169 | await tx.wait(); 170 | config.tokenAddress = token.address; 171 | } 172 | 173 | await promisify(fs.writeFile)(configFilename, JSON.stringify(config, null, 2)); 174 | 175 | return config; 176 | } 177 | 178 | const setTxpool = async (config: TPSConfig, deployer: Wallet) => { 179 | let lastBlock = await ethers.provider.getBlock("latest"); 180 | 181 | const chainGasPrice = await ethers.provider.getGasPrice(); 182 | const gasLimit = ethers.BigNumber.from(config.gasLimit); 183 | 184 | let estimateGasTx; 185 | if (config.payloads?.length) estimateGasTx = await ethers.provider.estimateGas(config.payloads[0]); 186 | else { 187 | const receiver = receiversMap.get(0)!; 188 | const token = (await ethers.getContractFactory("SimpleToken", deployer)).attach(config.tokenAddress); 189 | // @ts-ignore 190 | estimateGasTx = await token.estimateGas[config.tokenMethod]( 191 | config.tokenTransferMultiplier, 192 | receiver.address, 193 | 1, 194 | { gasPrice: chainGasPrice.mul(2), gasLimit: lastBlock.gasLimit.mul(2).div(3) } 195 | ); 196 | } 197 | 198 | if (estimateGasTx.gt(gasLimit)) { 199 | console.log(`\n[ Gas ] estimateGas > config.gasLimit | ${estimateGasTx} > ${config.gasLimit}`); 200 | console.log(`[ Gas ] Updating config.gasLimit: ${estimateGasTx}`); 201 | config.gasLimit = estimateGasTx.toString(); 202 | } 203 | 204 | // We pre calculate the max txn per block we can get and set the txpool max size to * txpoolMultiplier of it. 205 | console.log(`\n[Txpool] Trying to get a proper Txpool max length...`); 206 | console.log(`[Txpool] Block gasLimit : ${lastBlock.gasLimit}`); 207 | console.log(`[Txpool] Txn estimateGas : ${estimateGasTx}`); 208 | let max_txn_block = lastBlock.gasLimit.div(estimateGasTx).toNumber(); 209 | console.log(`[Txpool] Max txn per Block: ${max_txn_block}`); 210 | 211 | if (config.txpoolMaxLength === -1) { 212 | let maxTxnMultiplier = max_txn_block * config.txpoolMultiplier; 213 | if (maxTxnMultiplier > 5000) config.txpoolMaxLength = Math.round(maxTxnMultiplier / 1000) * 1000; 214 | else config.txpoolMaxLength = maxTxnMultiplier; 215 | } 216 | 217 | console.log(`[Txpool] Max length : ${config.txpoolMaxLength}`); 218 | if (config.txpoolMaxLength > config.txpoolLimit) { 219 | config.txpoolMaxLength = config.txpoolLimit; 220 | console.log(`[Txpool] Using pool limit : ${config.txpoolMaxLength} ***`); 221 | } 222 | 223 | return config; 224 | } 225 | 226 | const setupAccounts = async ( 227 | config: TPSConfig, 228 | sendersFilename: string, 229 | receiversFilename: string 230 | ) => { 231 | 232 | const chainId = (await ethers.provider.getNetwork()).chainId; 233 | const staticProvider = new ethers.providers.StaticJsonRpcProvider(config.endpoint, { name: 'tps', chainId }); 234 | 235 | let account: Wallet | null = null; 236 | try { 237 | let keysByIds = await readJSON(sendersFilename); 238 | console.log(`[setupAccounts] Reading ${Object.keys(keysByIds).length} senders' accounts...`); 239 | for (let k of Object.keys(keysByIds)) { 240 | account = new ethers.Wallet(keysByIds[k].privateKey, staticProvider); 241 | sendersMap.set(parseInt(k), account); 242 | } 243 | 244 | keysByIds = await readJSON(receiversFilename); 245 | console.log(`[setupAccounts] Reading ${Object.keys(keysByIds).length} receivers' accounts...`); 246 | for (let k of Object.keys(keysByIds)) { 247 | account = new ethers.Wallet(keysByIds[k].privateKey, staticProvider); 248 | receiversMap.set(parseInt(k), account); 249 | } 250 | 251 | return; 252 | } catch (error: any) { } 253 | 254 | let senders: any = {}; 255 | let receivers: any = {}; 256 | console.log(`[setupAccounts] Creating ${config.accounts} senders and ${config.accounts} receivers accounts...`); 257 | for (let k = 0; k < config.accounts; k++) { 258 | account = ethers.Wallet.createRandom().connect(staticProvider); 259 | sendersMap.set(k, account); 260 | senders[k] = { address: account.address, privateKey: account.privateKey }; 261 | 262 | account = ethers.Wallet.createRandom().connect(staticProvider); 263 | receiversMap.set(k, account); 264 | receivers[k] = { address: account.address, privateKey: account.privateKey }; 265 | } 266 | 267 | await promisify(fs.writeFile)(sendersFilename, JSON.stringify(senders, null, 2)); 268 | await promisify(fs.writeFile)(receiversFilename, JSON.stringify(receivers, null, 2)); 269 | } 270 | 271 | const post = async (config: TPSConfig, method: string, params: any[]) => { 272 | let r = await axios.post( 273 | config.endpoint, 274 | { 275 | jsonrpc: "2.0", 276 | method, 277 | params, 278 | id: 1 279 | }, 280 | { headers: { 'Content-Type': 'application/json' }, timeout: config.timeout }, 281 | ); 282 | return r.data; 283 | } 284 | 285 | const waitForResponse = async (config: TPSConfig, method: string, params: any[], delay: number, retries: number) => { 286 | let result; 287 | for (let counter = 0; counter < retries; counter++) { 288 | try { 289 | let r = await post(config, method, params); 290 | result = r.result; 291 | if (result) break; 292 | } catch (err: any) { console.log(`ERROR: waitForResponse() -> ${err}`) } 293 | counter++; 294 | if (counter >= retries) break; 295 | await new Promise(r => setTimeout(r, delay)); 296 | } 297 | return result; 298 | } 299 | 300 | const batchMintTokens = async (config: TPSConfig, deployer: Wallet) => { 301 | const token = (await ethers.getContractFactory("SimpleToken", deployer)).attach(config.tokenAddress); 302 | 303 | const gasLimit = ethers.BigNumber.from("1000000"); 304 | const chainId = await deployer.getChainId(); 305 | 306 | let nonce = await deployer.getTransactionCount(); 307 | 308 | let txHash; 309 | for (let k = 0; k < sendersMap.size; k++) { 310 | const sender = sendersMap.get(k)!; 311 | let unsigned = await token.populateTransaction.mintTo(sender.address, config.tokenAmountToMint); 312 | unsigned = { 313 | ...unsigned, 314 | gasLimit, 315 | gasPrice, 316 | nonce, 317 | chainId, 318 | }; 319 | let payload = await deployer.signTransaction(unsigned); 320 | let data = await post(config, "eth_sendRawTransaction", [payload]); 321 | let txHash = data.result; 322 | if (!validTxHash(txHash)) throw Error(`[ERROR] batchMintTokens() -> ${JSON.stringify(data)}`); 323 | console.log(`[batchMintTokens] Minting tokens to ${sender.address} -> ${txHash}`); 324 | if ((k + 1) % 500 === 0) await new Promise(r => setTimeout(r, 6000)); 325 | nonce++; 326 | } 327 | await getReceiptLocally(txHash!, 500, 60); 328 | return nonce; 329 | } 330 | 331 | const batchSendEthers = async (config: TPSConfig, deployer: Wallet, nonce: number | null) => { 332 | const gasLimit = ethers.BigNumber.from("1000000"); 333 | const chainId = await deployer.getChainId(); 334 | 335 | if (!nonce) nonce = await deployer.getTransactionCount(); 336 | 337 | let txHash; 338 | for (let k = 0; k < sendersMap.size; k++) { 339 | const sender = sendersMap.get(k)!; 340 | let unsigned = { 341 | from: deployer.address, 342 | to: sender.address, 343 | value: ethers.utils.parseEther("1000"), 344 | gasLimit, 345 | gasPrice, 346 | nonce, 347 | chainId, 348 | }; 349 | let payload = await deployer.signTransaction(unsigned); 350 | let data = await post(config, "eth_sendRawTransaction", [payload]); 351 | let txHash = data.result; 352 | if (!validTxHash(txHash)) throw Error(`[ERROR] batchSendEthers() -> ${JSON.stringify(data)}`); 353 | console.log(`[batchSendEthers] Sending ETH to ${sender.address} -> ${txHash}`); 354 | if ((k + 1) % 500 === 0) await new Promise(r => setTimeout(r, 6000)); 355 | nonce++; 356 | } 357 | await getReceiptLocally(txHash!, 500, 60); 358 | return nonce; 359 | } 360 | 361 | const sendRawTransaction = async ( 362 | config: TPSConfig, 363 | k: number, 364 | nonce: number, 365 | gasLimit: BigNumber, 366 | chainId: number, 367 | ) => { 368 | const sender = sendersMap.get(k)!; 369 | const receiver = receiversMap.get(k)!; 370 | 371 | const token = (await ethers.getContractFactory("SimpleToken", sender)).attach(config.tokenAddress); 372 | 373 | // @ts-ignore 374 | let unsigned = await token.populateTransaction[config.tokenMethod](config.tokenTransferMultiplier, receiver.address, 1); 375 | unsigned = { 376 | ...unsigned, 377 | gasLimit, 378 | gasPrice, 379 | nonce, 380 | chainId, 381 | }; 382 | let payload = await sender.signTransaction(unsigned); 383 | let data = await post(config, "eth_sendRawTransaction", [payload]); 384 | let txHash = data.result; 385 | if (!validTxHash(txHash)) throw Error(`[ERROR] sendRawTransaction() -> ${JSON.stringify(data)}`); 386 | return txHash; 387 | } 388 | 389 | const blockTracker = async (config: TPSConfig) => { 390 | let blockNumber = 0; 391 | while (1) { 392 | 393 | if (receiptsMap.size > 10_000) receiptsMap.clear(); 394 | 395 | try { 396 | let block = await waitForResponse(config, "eth_getBlockByNumber", ["latest", false], 250, 1); 397 | if (block.number != blockNumber) { 398 | let receipts; 399 | if (config.variant === "parity") { 400 | receipts = await waitForResponse(config, "parity_getBlockReceipts", [block.number], 250, 1); 401 | } else { 402 | receipts = []; 403 | for (let txnHash of block.transactions) { 404 | receipts.push(await waitForResponse(config, "eth_getTransactionReceipt", [txnHash], 250, 1)); 405 | } 406 | } 407 | if (receipts === undefined) throw Error(`Not able to fetch receipts using parity_getBlockReceipts for ${block.number}!`); 408 | for (let r of receipts) { 409 | // Storing just (hash, status) to save memory. 410 | receiptsMap.set(r.transactionHash, r.status); 411 | } 412 | const ratio = Math.round((block.gasUsed / block.gasLimit) * 100); 413 | let msg = `[BlockTracker] Block: ${zeroPad(parseInt(block.number, 16), 4)} | `; 414 | msg += `txns: ${zeroPad(receipts.length, 4)} | `; 415 | msg += `gasUsed: ${zeroPad(parseInt(block.gasUsed, 16), 9)} (~${zeroPad(ratio, 3)}%) `; 416 | msg += `[gasPrice: ${printGasPrice(chainGasPrice)} | pool: ${zeroPad(txPoolLength, 5)}]`; 417 | if (lastTxHash && !config.verbose) msg += ` -> txHash: ${lastTxHash} `; 418 | console.log(msg); 419 | } 420 | blockNumber = block.number; 421 | } catch { } 422 | await new Promise(r => setTimeout(r, config.checkersInterval)); 423 | } 424 | } 425 | 426 | const getReceiptLocally = async (txnHash: string, delay: number, retries: number) => { 427 | let receipt; 428 | for (let counter = 0; counter < retries; counter++) { 429 | try { 430 | receipt = receiptsMap.get(txnHash); 431 | if (receipt !== undefined) break; 432 | } catch { } 433 | counter++; 434 | if (counter >= retries) break; 435 | await new Promise(r => setTimeout(r, delay)); 436 | } 437 | return receipt; 438 | } 439 | 440 | const txpoolChecker = async (config: TPSConfig) => { 441 | let method = "author_pendingExtrinsics"; 442 | if (["geth", "frontier"].includes(config.variant)) method = "txpool_status"; 443 | else if (config.variant === "parity") method = "parity_pendingTransactions"; 444 | 445 | while (1) { 446 | try { 447 | let result = await waitForResponse(config, method, [], 250, 1); 448 | if (["geth", "frontier"].includes(config.variant)) { 449 | txPoolLength = parseInt(result.pending, 16) + parseInt(result.queued, 16); 450 | } else txPoolLength = result.length; 451 | } catch { txPoolLength = -1; } 452 | await new Promise(r => setTimeout(r, config.checkersInterval)); 453 | } 454 | } 455 | 456 | const gasPriceChecker = async (config: TPSConfig) => { 457 | while (1) { 458 | try { 459 | let result = await waitForResponse(config, "eth_gasPrice", [], 250, 1); 460 | chainGasPrice = ethers.BigNumber.from(result); 461 | if (chainGasPrice.mul(2).gte(gasPrice)) gasPrice = chainGasPrice.mul(2); 462 | } catch { } 463 | await new Promise(r => setTimeout(r, config.checkersInterval)); 464 | } 465 | } 466 | 467 | const printGasPrice = (value: BigNumber) => { 468 | let normalized = `${Math.round(value.div(1_000_000).toNumber())}M`; 469 | if (value.gte(1_000_000_000)) normalized = `${Math.round(value.div(1_000_000_000).toNumber())}B`; 470 | if (value.gte(1_000_000_000_000)) normalized = `${Math.round(value.div(1_000_000_000_000).toNumber())}T`; 471 | if (value.gte(1_000_000_000_000_000)) normalized = `${Math.round(value.div(1_000_000_000_000_000).toNumber())}Q`; 472 | return normalized; 473 | } 474 | 475 | const checkTxpool = async (config: TPSConfig) => { 476 | if (config.txpoolMaxLength > 0) { 477 | while (txPoolLength === -1 || txPoolLength >= config.txpoolMaxLength) { 478 | await new Promise(r => setTimeout(r, 5)); 479 | } 480 | } 481 | } 482 | 483 | const checkTokenBalances = async (config: TPSConfig, deployer: Wallet) => { 484 | const token = (await ethers.getContractFactory("SimpleToken", deployer)).attach(config.tokenAddress); 485 | const sender = sendersMap.get(0)!; 486 | const balance = await token.balanceOf(sender.address); 487 | console.log(`[checkTokenBalances] ${sender.address} token balance: ${balance}`); 488 | if (balance.isZero()) return await batchMintTokens(config, deployer); 489 | } 490 | 491 | const checkETHBalances = async (config: TPSConfig, deployer: Wallet, nonce: number | null) => { 492 | const sender = sendersMap.get(0)!; 493 | const balance = await sender.getBalance(); 494 | console.log(`[checkETHBalances] ${sender.address} ETH balance: ${balance}`); 495 | if (balance.lte(ethers.utils.parseEther("750"))) await batchSendEthers(config, deployer, nonce); 496 | } 497 | 498 | const assertTokenBalances = async (config: TPSConfig) => { 499 | let diffs = 0; 500 | const receiver = receiversMap.get(0)!; 501 | const token = (await ethers.getContractFactory("SimpleToken", receiver)).attach(config.tokenAddress); 502 | for (let k = 0; k < config.accounts; k++) { 503 | const amounts = rcvBalances.get(k)!; 504 | const receiver = receiversMap.get(k)!; 505 | const amount = await token.balanceOf(receiver.address); 506 | const ok = amounts.after === amount.toNumber(); 507 | if (!ok) diffs++; 508 | } 509 | if (diffs > 0) console.log(`[assertTokenBalances][ERROR] Balance is different for ${diffs} receivers. ***`); 510 | else console.log(`[assertTokenBalances] OK`); 511 | } 512 | 513 | const updateNonces = async (config: TPSConfig) => { 514 | for (let k = 0; k < config.accounts; k++) { 515 | const sender = sendersMap.get(k)!; 516 | const nonce = await sender.getTransactionCount(); 517 | console.log(`[updateNonces] ${sender.address} -> ${nonce}`); 518 | nonceMap.set(k, nonce); 519 | } 520 | } 521 | 522 | const updateBalances = async (config: TPSConfig) => { 523 | const receiver = receiversMap.get(0)!; 524 | const token = (await ethers.getContractFactory("SimpleToken", receiver)).attach(config.tokenAddress); 525 | for (let k = 0; k < config.accounts; k++) { 526 | const receiver = receiversMap.get(k)!; 527 | const amount = await token.balanceOf(receiver.address); 528 | console.log(`[updateBalances] ${receiver.address} -> ${amount}`); 529 | rcvBalances.set(k, { before: amount.toNumber(), after: amount.toNumber() }); 530 | } 531 | } 532 | 533 | const resetMaps = (config: TPSConfig) => { 534 | sendersMap.clear(); 535 | sendersBusyMap.clear(); 536 | sendersFreeMap.clear(); 537 | sendersTxnMap.clear(); 538 | receiversMap.clear(); 539 | rcvBalances.clear(); 540 | receiptsMap.clear(); 541 | nonceMap.clear(); 542 | workersMap.clear(); 543 | sendersErrMap.clear(); 544 | reqErrorsMap.clear(); 545 | initNumberMap(sendersErrMap, config.accounts, 0); 546 | initNumberMap(sendersTxnMap, config.accounts, 0); 547 | initNumberMap(sendersFreeMap, config.accounts, true); 548 | lastTxHash = ""; 549 | } 550 | 551 | const setupDirs = () => { 552 | try { 553 | fs.mkdirSync(EVM_TPS_ROOT_DIR); 554 | } catch (error: any) { 555 | if (error.code !== "EEXIST") { 556 | console.error(`[ERROR] Failed to create directories [${EVM_TPS_ROOT_DIR}]: ${error.message}`); 557 | process.exit(1); 558 | } 559 | } 560 | } 561 | 562 | const calculateTPS = async (config: TPSConfig, chainId: number, startingBlock: Block) => { 563 | let lastBlock = await waitForResponse(config, "eth_getBlockByNumber", ["latest", false], 250, 500); 564 | 565 | let lastBlockNumber = lastBlock.number; 566 | while (lastBlock.transactions.length > 0 || lastBlock.number === startingBlock.number) { 567 | lastBlockNumber = lastBlock.number; 568 | await new Promise(r => setTimeout(r, 200)); 569 | lastBlock = await waitForResponse(config, "eth_getBlockByNumber", ["latest", false], 250, 500); 570 | } 571 | 572 | lastBlock = await waitForResponse(config, "eth_getBlockByNumber", [lastBlockNumber, false], 250, 500); 573 | 574 | let t = lastBlock.timestamp - startingBlock.timestamp; 575 | let err = `[errors=${reqErrorsMap.size}]`; 576 | let blocks = lastBlock.number - startingBlock.number; 577 | return `blocks=${blocks} (${startingBlock.number} -> ${parseInt(lastBlock.number, 16)}) | txns=${config.transactions} t=${t} -> ${(config.transactions / t)} TPS/RPS ${err}`; 578 | } 579 | 580 | const initNumberMap = (m: Map, length: number, value: any) => { 581 | for (let i = 0; i < length; i++) m.set(i, value); 582 | } 583 | 584 | const printNumberMap = (m: Map) => { 585 | let msg = "\n\n"; 586 | for (let i = 0; i < m.size; i++) msg += `\n[printMap][${zeroPad(i, 5)}] ${m.get(i)!}`; 587 | msg += "\n\n" 588 | return msg; 589 | } 590 | 591 | const getAvailSender = async (config: TPSConfig, key: number) => { 592 | const maxTxnPerSender = Math.ceil(config.transactions / config.accounts); 593 | key = key === 0 ? key : key + 1; 594 | if (key >= config.accounts) key = 0; 595 | while (sendersFreeMap.size > 0) { 596 | let availKeys = sendersFreeMap.keys(); 597 | for (let k of availKeys) { 598 | if (sendersTxnMap.get(k)! >= maxTxnPerSender) { 599 | sendersFreeMap.delete(k); 600 | continue; 601 | } 602 | if (k < key) continue; 603 | if (sendersBusyMap.get(k)!) continue; 604 | return k; 605 | } 606 | key++; 607 | if (key >= config.accounts) key = 0; 608 | await new Promise(r => setTimeout(r, 1)); 609 | } 610 | return -1; 611 | } 612 | 613 | const getFreeWorker = async (config: TPSConfig, workerId: number) => { 614 | workerId = workerId === 0 ? workerId : workerId + 1; 615 | if (workerId >= config.workers) workerId = 0; 616 | while (workersMap.get(workerId)!) { 617 | await new Promise(r => setTimeout(r, 1)); 618 | workerId++; 619 | if (workerId >= config.workers) workerId = 0; 620 | } 621 | return workerId; 622 | } 623 | 624 | const validTxHash = (txHash: string | undefined) => { 625 | if (txHash === undefined || txHash === null) return false; 626 | if (!txHash?.startsWith('0x')) return false; 627 | if (txHash?.length !== 66) return false; 628 | return true; 629 | } 630 | 631 | const resendAuto = async ( 632 | config: TPSConfig, 633 | workerId: number, 634 | gasLimit: BigNumber, 635 | chainId: number, 636 | ) => { 637 | const sendersErrMapCopy = new Map(sendersErrMap); 638 | sendersErrMap.clear(); 639 | 640 | console.log(`\n\n----- Resending ${reqErrorsMap.size} Failed Requests -----\n\n`); 641 | console.log(printNumberMap(reqErrorsMap)); 642 | 643 | reqCounter -= reqErrorsMap.size; 644 | reqErrorsMap.clear(); 645 | reqErrCounter = 0; 646 | 647 | for (let k = 0; k < sendersErrMapCopy.size; k++) { 648 | let nonce = nonceMap.get(k)!; 649 | for (let j = 0; j < sendersErrMapCopy.get(k)!; j++) { 650 | await checkTxpool(config); 651 | workerId = await getFreeWorker(config, workerId); 652 | reqCounter++; 653 | autoSendRawTransaction(config, workerId, k, nonce, gasLimit, chainId); 654 | nonce++; 655 | } 656 | nonceMap.set(k, nonce); 657 | } 658 | } 659 | 660 | const autoSendRawTransaction = async ( 661 | config: TPSConfig, 662 | workerId: number, 663 | senderKey: number, 664 | nonce: number, 665 | gasLimit: BigNumber, 666 | chainId: number, 667 | ) => { 668 | sendersBusyMap.set(senderKey, true); 669 | workersMap.set(workerId, true); 670 | 671 | const pre = `[req: ${zeroPad(reqCounter, 5)}][addr: ${zeroPad(senderKey, 5)}]`; 672 | let post = `[wrk: ${zeroPad(workerId, 5)}(len=${zeroPad(workersMap.size, 5)}) `; 673 | post += `nonce: ${zeroPad(nonce, 5)} | `; 674 | post += `gasPrice: ${printGasPrice(gasPrice)} / ${printGasPrice(chainGasPrice)} | `; 675 | post += `pool: ${zeroPad(txPoolLength, 5)} | err=${reqErrorsMap.size}]`; 676 | let msg = ""; 677 | 678 | const start = Date.now(); 679 | try { 680 | const txHash = await sendRawTransaction(config, senderKey, nonce, gasLimit, chainId); 681 | if (validTxHash(txHash)) { 682 | const t = Date.now() - start; 683 | const postWithTime = `${post} [time: ${zeroPad(t, 5)}${t > 12000 ? " ***" : ""}]`; 684 | msg = `${pre} auto: ${txHash} ${postWithTime}`; 685 | if (config.verbose) console.log(msg); 686 | 687 | lastTxHash = txHash; 688 | let nextNonce = nonce + 1; 689 | nonceMap.set(senderKey, nextNonce); 690 | 691 | let amounts = rcvBalances.get(senderKey)!; 692 | amounts.after += config.tokenTransferMultiplier; 693 | rcvBalances.set(senderKey, amounts); 694 | } else { throw Error(`Invalid txHash: ${txHash}`) } 695 | } catch (error: any) { 696 | sendersErrMap.set(senderKey, sendersErrMap.get(senderKey)! + 1); 697 | sendersTxnMap.set(senderKey, sendersTxnMap.get(senderKey)! - 1); 698 | sendersFreeMap.set(senderKey, true); 699 | msg = `${pre} auto: ${error.message} ${post}`; 700 | reqErrorsMap.set(reqErrCounter, msg); 701 | reqErrCounter++; 702 | } 703 | 704 | sendersBusyMap.delete(senderKey); 705 | workersMap.delete(workerId); 706 | } 707 | 708 | const auto = async (config: TPSConfig, gasLimit: BigNumber, chainId: number) => { 709 | const staticProvider = new ethers.providers.StaticJsonRpcProvider(config.endpoint, { name: 'tps', chainId }); 710 | 711 | let status_code = 0; 712 | let msg = ""; 713 | const start = Date.now(); 714 | let workerId = 0; 715 | try { 716 | let startingBlock = await staticProvider.getBlock("latest"); 717 | let initialCounter = reqCounter; 718 | 719 | while ((reqCounter - initialCounter) < config.transactions) { 720 | if (hardstop) { 721 | hardstop = false; 722 | return [0, "HARD_STOP"]; 723 | } 724 | // 5% of errors is too much, something is wrong. 725 | if (reqErrorsMap.size >= (config.transactions * 0.05)) { 726 | console.log(printNumberMap(reqErrorsMap)); 727 | let p = Math.round((reqErrorsMap.size / config.transactions) * 100); 728 | return [0, `TOO_MANY_ERRORS: ${reqErrorsMap.size}/${config.transactions} [~${p}%]`]; 729 | } 730 | await checkTxpool(config); 731 | nextKey = await getAvailSender(config, nextKey); 732 | if (nextKey === -1 || sendersFreeMap.size === 0) break; 733 | workerId = await getFreeWorker(config, workerId); 734 | const nonce = nonceMap.get(nextKey)!; 735 | reqCounter++; 736 | autoSendRawTransaction(config, workerId, nextKey, nonce, gasLimit, chainId); 737 | sendersTxnMap.set(nextKey, sendersTxnMap.get(nextKey)! + 1); 738 | } 739 | 740 | // Wait till no more running workers. 741 | while (workersMap.size > 0) { await new Promise(r => setTimeout(r, 5)) }; 742 | 743 | while (reqErrorsMap.size > 0) await resendAuto(config, workerId, gasLimit, chainId); 744 | 745 | while (txPoolLength > 0) await new Promise(r => setTimeout(r, 100)); 746 | 747 | // Wait till no more running workers. 748 | while (workersMap.size > 0) { await new Promise(r => setTimeout(r, 5)) }; 749 | 750 | let tpsResult = await calculateTPS(config, chainId, startingBlock); 751 | reqErrorsMap.clear(); 752 | reqErrCounter = 0; 753 | 754 | if (config.tokenAssert) await assertTokenBalances(config); 755 | 756 | lastTxHash = ""; 757 | 758 | let t = Date.now() - start; 759 | let pre = `[req: ${zeroPad(reqCounter, 5)}][addr: ${zeroPad(0, 5)}]`; 760 | let post = `[wrk: ${zeroPad(workersMap.size, 5)} | pool: ${zeroPad(txPoolLength, 5)} | time: ${zeroPad(t, 5)}]`; 761 | msg = `${pre} auto: ${tpsResult} ${post}`; 762 | } catch (error: any) { 763 | msg = `[ERROR][req: ${zeroPad(reqCounter, 5)}][wrk: ${zeroPad(workersMap.size, 5)}] auto: ${error.message}`; 764 | } 765 | console.log(msg); 766 | return [status_code, msg]; 767 | } 768 | 769 | const setup = async () => { 770 | setupDirs(); 771 | 772 | let deployer = await getDeployer(EVM_TPS_CONFIG_FILE); 773 | let config = await setConfig(EVM_TPS_CONFIG_FILE, deployer); 774 | 775 | resetMaps(config); 776 | 777 | await setupAccounts(config, EVM_TPS_SENDERS_FILE, EVM_TPS_RECEIVERS_FILE); 778 | 779 | let deployerNonce = await checkTokenBalances(config, deployer); 780 | if (config.fundSenders) await checkETHBalances(config, deployer, deployerNonce!); 781 | 782 | await updateNonces(config); 783 | await updateBalances(config); 784 | 785 | config = await setTxpool(config, deployer); 786 | 787 | console.log(JSON.stringify(config, null, 2)); 788 | 789 | hardstop = false; 790 | 791 | return config!; 792 | } 793 | 794 | const main = async () => { 795 | 796 | let config = await setup(); 797 | 798 | blockTracker(config); 799 | txpoolChecker(config); 800 | 801 | const gasLimit = ethers.BigNumber.from(config.gasLimit); 802 | const chainId = (await ethers.provider.getNetwork()).chainId; 803 | 804 | gasPriceChecker(config); 805 | 806 | const app = express(); 807 | app.use(BodyParser.json()); 808 | 809 | app.get("/auto", async (req: any, res: any) => { 810 | config = await setup(); 811 | console.log(`[Server] Running auto()...`); 812 | const [status, msg] = await auto(config, gasLimit, chainId); 813 | if (status === 0) res.send(msg); 814 | else res.status(500).send(`Internal error: /auto ${msg}`); 815 | }); 816 | 817 | app.get("/sendRawTransaction", async (req: any, res: any) => { 818 | 819 | await checkTxpool(config) 820 | 821 | nextKey = await getAvailSender(config, nextKey); 822 | 823 | const nonce = nonceMap.get(nextKey)!; 824 | let nextNonce = nonce + 1; 825 | nonceMap.set(nextKey, nextNonce); 826 | 827 | const start = Date.now(); 828 | try { 829 | const txHash = await sendRawTransaction(config, nextKey, nonce, gasLimit, chainId); 830 | const t = Date.now() - start; 831 | const pre = `[req: ${zeroPad(reqCounter, 5)}][addr: ${zeroPad(nextKey, 5)}]`; 832 | const post = `[nonce: ${nonce} | pool: ${txPoolLength} | time: ${t}]${t > 12000 ? " ***" : ""}`; 833 | const msg = `${pre} sendRawTransaction: ${txHash} ${post}`; 834 | reqCounter++; 835 | console.log(msg); 836 | res.send(msg); 837 | } catch (error: any) { 838 | console.error(`[ERROR][req: ${zeroPad(reqCounter, 5)}][acc: ${zeroPad(nextKey, 5)}] sendRawTransaction: ${error.message}`); 839 | res.status(500).send(`Internal error: /sendRawTransaction ${error.message}`); 840 | } 841 | }); 842 | 843 | app.get("/getBlock", async (req: any, res: any) => { 844 | nextKey = await getAvailSender(config, nextKey); 845 | try { 846 | const start = Date.now(); 847 | const sender = sendersMap.get(nextKey)!; 848 | let b = await sender.provider.getBlock("latest"); 849 | const t = Date.now() - start; 850 | const msg = `[req: ${zeroPad(reqCounter, 5)}][acc: ${zeroPad(nextKey, 5)}] getBlock: [b: ${b.number} | t: ${t}]${t > 12000 ? " ***" : ""}`; 851 | reqCounter++; 852 | console.log(msg); 853 | res.send(msg); 854 | } catch (error: any) { 855 | console.error(`[ERROR][req: ${zeroPad(reqCounter, 5)}][acc: ${zeroPad(nextKey, 5)}] getBlock: ${error.message}`); 856 | res.status(500).send(`Internal error: /getBlock ${error.message}`); 857 | } 858 | }); 859 | 860 | app.get("/stats", async (req: any, res: any) => { 861 | try { 862 | const stats = { 863 | senders: sendersMap.size, 864 | receivers: receiversMap.size, 865 | receipts: receiptsMap.size, 866 | nonces: nonceMap.size, 867 | workers: workersMap.size, 868 | reqCounter, 869 | errors: reqErrorsMap.size, 870 | } 871 | const msg = `[req: ${zeroPad(reqCounter, 5)}][acc: ${zeroPad(-1, 5)}] status: \n${JSON.stringify(stats, null, 2)}\n`; 872 | console.log(msg); 873 | res.send(msg); 874 | } catch (error: any) { 875 | console.error(`[ERROR][req: ${zeroPad(reqCounter, 5)}][acc: ${zeroPad(-1, 5)}] reset: ${error.message}`); 876 | res.status(500).send(`Internal error: /stats ${error.message}`); 877 | } 878 | }); 879 | 880 | app.get("/reset", async (req: any, res: any) => { 881 | try { 882 | const start = Date.now(); 883 | config = await setup(); 884 | const t = Date.now() - start; 885 | const msg = `[req: ${zeroPad(reqCounter, 5)}][acc: ${zeroPad(-1, 5)}] reset: [b: - | t: ${t}]`; 886 | console.log(msg); 887 | res.send(msg); 888 | } catch (error: any) { 889 | console.error(`[ERROR][req: ${zeroPad(reqCounter, 5)}][acc: ${zeroPad(-1, 5)}] reset: ${error.message}`); 890 | res.status(500).send(`Internal error: /reset ${error.message}`); 891 | } 892 | }); 893 | 894 | app.get("/dumpErrors", async (req: any, res: any) => { 895 | try { 896 | const msg = `----- dumpErrors: ${printNumberMap(reqErrorsMap)}`; 897 | console.log(msg); 898 | res.send(msg); 899 | } catch (error: any) { 900 | res.status(500).send(`Internal error: /dumpErrors ${error.message}`); 901 | } 902 | }); 903 | 904 | app.get("/stop", async (req: any, res: any) => { 905 | try { 906 | const start = Date.now(); 907 | hardstop = true; 908 | const t = Date.now() - start; 909 | const msg = `[req: ${zeroPad(reqCounter, 5)}][acc: ${zeroPad(-1, 5)}] stop: [b: - | t: ${t}]`; 910 | console.log(msg); 911 | res.send(msg); 912 | } catch (error: any) { 913 | res.status(500).send(`Internal error: /stop ${error.message}`); 914 | } 915 | }); 916 | 917 | app.listen(config.tpsServerPort, config.tpsServerHost, () => { 918 | console.log(`> Listening at http://${config.tpsServerHost}:${config.tpsServerPort}`); 919 | }); 920 | } 921 | 922 | main().catch((error) => { 923 | console.error(`[ERROR] ${error.message}`); 924 | process.exit(1); 925 | }); 926 | -------------------------------------------------------------------------------- /scripts/substrate-tps-server.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import { promisify } from "util"; 3 | 4 | import axios from "axios"; 5 | import express from "express"; 6 | import BodyParser from "body-parser"; 7 | 8 | import { ethers, network } from "hardhat"; 9 | 10 | import { BigNumber } from "ethers"; 11 | import { PopulatedTransaction } from "ethers/lib/ethers"; 12 | 13 | import { ApiPromise, WsProvider } from '@polkadot/api'; 14 | import { KeyringPair } from "@polkadot/keyring/types"; 15 | import { Keyring } from '@polkadot/keyring'; 16 | import { blake2AsHex, cryptoWaitReady } from '@polkadot/util-crypto'; 17 | 18 | import { BN } from 'bn.js'; 19 | 20 | const EVM_TPS_ROOT_DIR = process.env.ROOT_DIR || "data"; 21 | const EVM_TPS_CONFIG_FILE = `${EVM_TPS_ROOT_DIR}/config.json`; 22 | const EVM_TPS_SENDERS_FILE = `${EVM_TPS_ROOT_DIR}/senders.json`; 23 | const EVM_TPS_RECEIVERS_FILE = `${EVM_TPS_ROOT_DIR}/receivers.json`; 24 | 25 | interface SimpleBlock { 26 | hash: string, 27 | number: number, 28 | timestamp: number, 29 | extrinsics: string[], 30 | } 31 | 32 | interface Balances { 33 | before: number, 34 | after: number, 35 | } 36 | 37 | // Map from key-id to the private key 38 | const sendersMap = new Map(); 39 | const receiversMap = new Map(); 40 | const rcvBalances = new Map(); 41 | 42 | const nonceMap = new Map(); 43 | 44 | const receiptsMap = new Map(); 45 | 46 | const workersMap = new Map(); 47 | const workersAPIMap = new Map(); 48 | const sendersBusyMap = new Map(); 49 | const sendersFreeMap = new Map(); 50 | const sendersTxnMap = new Map(); 51 | const sendersErrMap = new Map(); 52 | 53 | const reqErrorsMap = new Map(); 54 | 55 | let txPoolLength = 0; 56 | 57 | let chainFee = ethers.BigNumber.from(0); 58 | 59 | let reqCounter = 0; 60 | let reqErrCounter = 0; 61 | let nextKey = 0; 62 | let lastTxHash = ""; 63 | let hardstop = false; 64 | let inherentExtrinsics = 0; 65 | 66 | class SubstrateApi { 67 | wsEndpoint: string; 68 | api: ApiPromise | null; 69 | constructor(wsEndpoint: string) { 70 | this.wsEndpoint = wsEndpoint.replace('http://', 'ws://').replace('https://', 'wss://'); 71 | this.api = null; 72 | } 73 | async get(config: TPSConfig) { 74 | if (!this.wsEndpoint) this.wsEndpoint = config.endpoint.replace('http://', 'ws://').replace('https://', 'wss://'); 75 | if (!this.api) this.api = await ApiPromise.create({ provider: new WsProvider(this.wsEndpoint) }) 76 | return this.api; 77 | } 78 | } 79 | 80 | const substrateApi = new SubstrateApi(''); 81 | 82 | const zeroPad = (num: number, places: number) => String(num).padStart(places, '0') 83 | 84 | interface TPSConfig { 85 | tpsServerHost: string, 86 | tpsServerPort: number, 87 | endpoint: string; 88 | variant: string; 89 | deployer: { 90 | address: string, 91 | privateKey: string, 92 | }, 93 | fundSenders: boolean, 94 | accounts: number, 95 | workers: number, 96 | sendRawTransaction: boolean; 97 | timeout: number, 98 | tokenAddress: string; 99 | tokenMethod: string; 100 | tokenAmountToMint: number; 101 | tokenTransferMultiplier: number; 102 | tokenAssert: boolean | undefined; 103 | transactions: number, 104 | gasLimit: string; 105 | txpoolMaxLength: number; 106 | txpoolMultiplier: number; 107 | txpoolLimit: number; 108 | checkersInterval: number; 109 | estimate: boolean | undefined; 110 | payloads: UnsignedTx[] | PopulatedTransaction[] | undefined; 111 | verbose: boolean; 112 | } 113 | 114 | interface UnsignedTx { 115 | from: string; 116 | to: string; 117 | value?: BigNumber | string; 118 | data: string; 119 | gasPrice?: BigNumber | string; 120 | gasLimit?: BigNumber | string; 121 | nonce?: number; 122 | chainId?: number; 123 | } 124 | 125 | const readJSON = async (filename: string) => { 126 | const j = await promisify(fs.readFile)(filename); 127 | return JSON.parse(j.toString()); 128 | } 129 | 130 | const getDeployer = async (configFilename: string) => { 131 | await cryptoWaitReady(); 132 | let keyring = new Keyring({ type: 'sr25519' }); 133 | try { 134 | const config = await readJSON(configFilename); 135 | if (config.variant === 'frontier') keyring = new Keyring({ type: 'ethereum' }); 136 | return keyring.createFromUri(config.deployer.privateKey); 137 | } catch (_) { 138 | return keyring.createFromUri(ethers.Wallet.createRandom().privateKey); 139 | } 140 | } 141 | 142 | const setConfig = async (configFilename: string, deployer: KeyringPair) => { 143 | // @ts-ignore 144 | let url = network.config.url; 145 | let config: TPSConfig = { 146 | tpsServerHost: "0.0.0.0", 147 | tpsServerPort: 8181, 148 | endpoint: url || "http://127.0.0.1:9944", 149 | variant: "substrate", 150 | deployer: { 151 | address: deployer.address, 152 | privateKey: deployer.address, 153 | }, 154 | fundSenders: true, 155 | accounts: 100, 156 | workers: 80, 157 | sendRawTransaction: true, 158 | timeout: 5000, 159 | tokenAddress: "", 160 | tokenMethod: "transferLoop", 161 | tokenAmountToMint: 1_000_000_000, 162 | tokenTransferMultiplier: 1, 163 | tokenAssert: true, 164 | transactions: 30_000, 165 | gasLimit: "200000", 166 | txpoolMaxLength: -1, 167 | txpoolMultiplier: 2, 168 | txpoolLimit: 7500, 169 | checkersInterval: 250, 170 | estimate: false, 171 | payloads: undefined, 172 | verbose: false, 173 | }; 174 | 175 | if (fs.existsSync(configFilename)) { 176 | const fromJSON = await readJSON(configFilename); 177 | config = { ...config, ...fromJSON }; 178 | } 179 | 180 | await promisify(fs.writeFile)(configFilename, JSON.stringify(config, null, 2)); 181 | 182 | return config; 183 | } 184 | 185 | const setTxpool = async (config: TPSConfig, deployer: KeyringPair) => { 186 | // We pre calculate the max txn per block we can get and set the txpool max size to * txpoolMultiplier of it. 187 | const api = await substrateApi.get(config); 188 | // @ts-ignore 189 | let blockWeight = api.consts.system.blockWeights.maxBlock; 190 | console.log(`\n[Txpool] Trying to get a proper Txpool max length...`); 191 | // @ts-ignore 192 | let blockMaxFee = (await api.call.transactionPaymentApi.queryWeightToFee(blockWeight)).toBigInt(); 193 | blockMaxFee = blockMaxFee * 3n / 4n; 194 | console.log(`[Txpool] Block Max Fee : ${blockMaxFee}`); 195 | const xt = api.tx.balances.transferKeepAlive(deployer.address, 1_000); 196 | // const xt = api.tx.templatePallet.doSomething2(7); 197 | const info = await xt.paymentInfo(deployer); 198 | // @ts-ignore 199 | const xtFee = (await api.call.transactionPaymentApi.queryWeightToFee(info.weight)).toBigInt(); 200 | console.log(`[Txpool] Extrinsic Fee : ${xtFee}`); 201 | let max_txn_block = parseInt((blockMaxFee / xtFee).toString()); 202 | console.log(`[Txpool] Max xts per Block: ${Math.round(max_txn_block)}`); 203 | 204 | if (config.txpoolMaxLength === -1) { 205 | let maxTxnMultiplier = max_txn_block * config.txpoolMultiplier; 206 | if (maxTxnMultiplier > 5000) config.txpoolMaxLength = Math.round(maxTxnMultiplier / 1000) * 1000; 207 | else config.txpoolMaxLength = maxTxnMultiplier; 208 | } 209 | 210 | console.log(`[Txpool] Max length : ${config.txpoolMaxLength}`); 211 | if (config.txpoolMaxLength > config.txpoolLimit) { 212 | config.txpoolMaxLength = config.txpoolLimit; 213 | console.log(`[Txpool] Using pool limit : ${config.txpoolMaxLength} ***`); 214 | } 215 | 216 | return config; 217 | } 218 | 219 | const setupAccounts = async ( 220 | config: TPSConfig, 221 | sendersFilename: string, 222 | receiversFilename: string 223 | ) => { 224 | 225 | await cryptoWaitReady(); 226 | let keyring = new Keyring({ type: 'sr25519' }); 227 | if (config.variant === 'frontier') keyring = new Keyring({ type: 'ethereum' }); 228 | 229 | let account: KeyringPair | null = null; 230 | try { 231 | let keysByIds = await readJSON(sendersFilename); 232 | console.log(`[setupAccounts] Reading ${Object.keys(keysByIds).length} senders' accounts...`); 233 | for (let k of Object.keys(keysByIds)) { 234 | account = keyring.createFromUri(keysByIds[k].privateKey); 235 | sendersMap.set(parseInt(k), account); 236 | } 237 | 238 | keysByIds = await readJSON(receiversFilename); 239 | console.log(`[setupAccounts] Reading ${Object.keys(keysByIds).length} receivers' accounts...`); 240 | for (let k of Object.keys(keysByIds)) { 241 | account = keyring.createFromUri(keysByIds[k].privateKey); 242 | receiversMap.set(parseInt(k), account); 243 | } 244 | 245 | return; 246 | } catch (error: any) { } 247 | 248 | let senders: any = {}; 249 | let receivers: any = {}; 250 | console.log(`[setupAccounts] Creating ${config.accounts} senders and ${config.accounts} receivers accounts...`); 251 | for (let k = 0; k < config.accounts; k++) { 252 | let randomWallet = ethers.Wallet.createRandom(); 253 | account = keyring.createFromUri(randomWallet.privateKey); 254 | sendersMap.set(k, account); 255 | senders[k] = { address: account.address, privateKey: randomWallet.privateKey }; 256 | 257 | randomWallet = ethers.Wallet.createRandom(); 258 | account = keyring.createFromUri(randomWallet.privateKey); 259 | receiversMap.set(k, account); 260 | receivers[k] = { address: account.address, privateKey: randomWallet.privateKey }; 261 | } 262 | 263 | await promisify(fs.writeFile)(sendersFilename, JSON.stringify(senders, null, 2)); 264 | await promisify(fs.writeFile)(receiversFilename, JSON.stringify(receivers, null, 2)); 265 | } 266 | 267 | const post = async (config: TPSConfig, method: string, params: any[]) => { 268 | let r = await axios.post( 269 | config.endpoint, 270 | { 271 | jsonrpc: "2.0", 272 | method, 273 | params, 274 | id: 1 275 | }, 276 | { headers: { 'Content-Type': 'application/json' }, timeout: config.timeout }, 277 | ); 278 | return r.data; 279 | } 280 | 281 | const waitForResponse = async (config: TPSConfig, method: string, params: any[], delay: number, retries: number) => { 282 | let result; 283 | for (let counter = 0; counter < retries; counter++) { 284 | try { 285 | let r = await post(config, method, params); 286 | result = r.result; 287 | if (result) break; 288 | } catch (err: any) { console.log(`ERROR: waitForResponse() -> ${err}`) } 289 | counter++; 290 | if (counter >= retries) break; 291 | await new Promise(r => setTimeout(r, delay)); 292 | } 293 | return result; 294 | } 295 | 296 | const batchSendNativeToken = async (config: TPSConfig, deployer: KeyringPair) => { 297 | const api = await substrateApi.get(config); 298 | let nonce = await api.rpc.system.accountNextIndex(deployer.address); 299 | let txHash; 300 | for (let k = 0; k < sendersMap.size; k++) { 301 | const sender = sendersMap.get(k)!; 302 | txHash = (await api.tx.balances.transfer(sender.address, 1_000_000_000_000_000).signAndSend(deployer, { nonce })).toString(); 303 | if (!validTxHash(txHash)) throw Error(`[ERROR] batchSendNativeToken() -> ${JSON.stringify(txHash)}`); 304 | console.log(`[batchSendNativeToken] Sending Native Token to ${sender.address} -> ${txHash}`); 305 | if ((k + 1) % 500 === 0) await new Promise(r => setTimeout(r, 6000)); 306 | // @ts-ignore 307 | nonce = nonce.add(new BN(1)); 308 | } 309 | await getReceiptLocally(txHash!, 500, 60); 310 | } 311 | 312 | const submitExtrinsic = async (api: ApiPromise, k: number, nonce: number) => { 313 | const sender = sendersMap.get(k)!; 314 | const receiver = receiversMap.get(k)!; 315 | 316 | const txHash = (await api.tx.balances.transferKeepAlive(receiver.address, 1_000).signAndSend(sender, { nonce })).toString(); 317 | // const txHash = (await api.tx.templatePallet.doSomething2(7).signAndSend(sender, { nonce })).toString(); 318 | if (!validTxHash(txHash)) throw Error(`[ERROR] submitExtrinsic() -> ${JSON.stringify(txHash)}`); 319 | 320 | return txHash; 321 | } 322 | 323 | const blockTracker = async (config: TPSConfig) => { 324 | const api = await substrateApi.get(config); 325 | // @ts-ignore 326 | let blockMaxWeights = api.consts.system.blockWeights.maxBlock.refTime; 327 | blockMaxWeights = blockMaxWeights.toNumber() * 0.75; 328 | let blockNumber = 0; 329 | const unsubscribe = await api.rpc.chain.subscribeNewHeads(async (header) => { 330 | if (blockNumber != header.number.toNumber()) { 331 | const block = (await api.rpc.chain.getBlock(header.hash)!).block; 332 | // @ts-ignore 333 | let weight = (await (await api.at(header.hash)).query.system.blockWeight()).normal.refTime.toNumber(); 334 | let ratio = Math.round((weight / blockMaxWeights) * 100); 335 | let msg = `[BlockTracker] Block: ${zeroPad(header.number.toNumber(), 4)} | `; 336 | msg += `xts: ${zeroPad(block.extrinsics.length, 4)} | `; 337 | msg += `weight: ${zeroPad(weight, 13)} (~${zeroPad(ratio, 3)}%) `; 338 | msg += `[fee: ${printGasPrice(chainFee)} | pool: ${zeroPad(txPoolLength, 5)}]`; 339 | if (lastTxHash && !config.verbose) msg += ` -> xtHash: ${lastTxHash} `; 340 | console.log(msg); 341 | blockNumber = header.number.toNumber(); 342 | } 343 | }); 344 | } 345 | 346 | const getReceiptLocally = async (txnHash: string, delay: number, retries: number) => { 347 | let receipt; 348 | for (let counter = 0; counter < retries; counter++) { 349 | try { 350 | receipt = receiptsMap.get(txnHash); 351 | if (receipt !== undefined) break; 352 | } catch { } 353 | counter++; 354 | if (counter >= retries) break; 355 | await new Promise(r => setTimeout(r, delay)); 356 | } 357 | return receipt; 358 | } 359 | 360 | const txpoolChecker = async (config: TPSConfig) => { 361 | let method = "author_pendingExtrinsics"; 362 | while (1) { 363 | try { 364 | let result = await waitForResponse(config, method, [], 250, 1); 365 | txPoolLength = result.length; 366 | } catch { txPoolLength = -1; } 367 | await new Promise(r => setTimeout(r, config.checkersInterval)); 368 | } 369 | } 370 | 371 | const feeChecker = async (config: TPSConfig) => { 372 | const api = await substrateApi.get(config); 373 | let deployer = await getDeployer(EVM_TPS_CONFIG_FILE); 374 | const xt = api.tx.balances.transferKeepAlive(deployer.address, 1_000); 375 | // const xt = api.tx.templatePallet.doSomething2(7); 376 | while (1) { 377 | try { 378 | let { partialFee: fee } = await xt.paymentInfo(deployer); 379 | chainFee = ethers.BigNumber.from(fee.toBigInt()); 380 | } catch { } 381 | await new Promise(r => setTimeout(r, config.checkersInterval)); 382 | } 383 | } 384 | 385 | const printGasPrice = (value: BigNumber) => { 386 | let normalized = `${Math.round(value.div(1_000_000).toNumber())}M`; 387 | if (value.gte(1_000_000_000)) normalized = `${Math.round(value.div(1_000_000_000).toNumber())}B`; 388 | if (value.gte(1_000_000_000_000)) normalized = `${Math.round(value.div(1_000_000_000_000).toNumber())}T`; 389 | if (value.gte(1_000_000_000_000_000)) normalized = `${Math.round(value.div(1_000_000_000_000_000).toNumber())}Q`; 390 | return normalized; 391 | } 392 | 393 | const checkTxpool = async (config: TPSConfig) => { 394 | if (config.txpoolMaxLength > 0) { 395 | while (txPoolLength === -1 || txPoolLength >= config.txpoolMaxLength) { 396 | await new Promise(r => setTimeout(r, 5)); 397 | } 398 | } 399 | } 400 | 401 | const checkBalances = async (config: TPSConfig, deployer: KeyringPair) => { 402 | const api = await substrateApi.get(config); 403 | const sender = sendersMap.get(0)!; 404 | // @ts-ignore 405 | let { data: { free: balance } } = await api.query.system.account(sender.address); 406 | console.log(`[checkBalances] ${sender.address} Native Token balance: ${balance}`); 407 | if (balance == 0) await batchSendNativeToken(config, deployer); 408 | } 409 | 410 | const assertTokenBalances = async (config: TPSConfig) => { 411 | const api = await substrateApi.get(config); 412 | let diffs = 0; 413 | for (let k = 0; k < config.accounts; k++) { 414 | const amounts = rcvBalances.get(k)!; 415 | const receiver = receiversMap.get(k)!; 416 | // @ts-ignore 417 | let { data: { free: amount } } = await api.query.system.account(receiver.address); 418 | const ok = amounts.after === amount.toNumber(); 419 | if (!ok) diffs++; 420 | } 421 | if (diffs > 0) console.log(`[assertTokenBalances][ERROR] Balance is different for ${diffs} receivers. ***`); 422 | else console.log(`[assertTokenBalances] OK`); 423 | } 424 | 425 | const updateNonces = async (config: TPSConfig) => { 426 | const api = await substrateApi.get(config); 427 | for (let k = 0; k < config.accounts; k++) { 428 | const sender = sendersMap.get(k)!; 429 | const nonce = await api.rpc.system.accountNextIndex(sender.address); 430 | console.log(`[updateNonces] ${sender.address} -> ${nonce}`); 431 | nonceMap.set(k, nonce.toNumber()); 432 | } 433 | } 434 | 435 | const updateBalances = async (config: TPSConfig) => { 436 | const api = await substrateApi.get(config); 437 | for (let k = 0; k < config.accounts; k++) { 438 | const receiver = receiversMap.get(k)!; 439 | // @ts-ignore 440 | let { data: { free: balance } } = await api.query.system.account(receiver.address); 441 | console.log(`[updateBalances] ${receiver.address} -> ${balance}`); 442 | rcvBalances.set(k, { before: balance.toNumber(), after: balance.toNumber() }); 443 | } 444 | } 445 | 446 | const resetMaps = (config: TPSConfig) => { 447 | sendersMap.clear(); 448 | sendersBusyMap.clear(); 449 | sendersFreeMap.clear(); 450 | sendersTxnMap.clear(); 451 | receiversMap.clear(); 452 | rcvBalances.clear(); 453 | receiptsMap.clear(); 454 | nonceMap.clear(); 455 | workersMap.clear(); 456 | // workersAPIMap.clear(); 457 | sendersErrMap.clear(); 458 | reqErrorsMap.clear(); 459 | initNumberMap(sendersErrMap, config.accounts, 0); 460 | initNumberMap(sendersTxnMap, config.accounts, 0); 461 | initNumberMap(sendersFreeMap, config.accounts, true); 462 | lastTxHash = ""; 463 | } 464 | 465 | const setupDirs = () => { 466 | try { 467 | fs.mkdirSync(EVM_TPS_ROOT_DIR); 468 | } catch (error: any) { 469 | if (error.code !== "EEXIST") { 470 | console.error(`[ERROR] Failed to create directories [${EVM_TPS_ROOT_DIR}]: ${error.message}`); 471 | process.exit(1); 472 | } 473 | } 474 | } 475 | 476 | const calculateTPS = async (config: TPSConfig, startingBlock: SimpleBlock) => { 477 | const api = await substrateApi.get(config); 478 | 479 | let lastBlock = await getBlockWithExtras(api, null); 480 | 481 | let lastBlockNumber = lastBlock.number; 482 | while (lastBlock.extrinsics.length > inherentExtrinsics || lastBlock.number === startingBlock.number) { 483 | lastBlockNumber = lastBlock.number; 484 | await new Promise(r => setTimeout(r, 200)); 485 | lastBlock = await getBlockWithExtras(api, null); 486 | } 487 | 488 | lastBlock = await getBlockWithExtras(api, lastBlockNumber); 489 | 490 | let t = lastBlock.timestamp - startingBlock.timestamp; 491 | let err = `[errors=${reqErrorsMap.size}]`; 492 | let blocks = lastBlock.number - startingBlock.number; 493 | return `blocks=${blocks} (${startingBlock.number + 1} -> ${lastBlock.number}) | txns=${config.transactions} t=${t} -> ${(config.transactions / t)} TPS/RPS ${err}`; 494 | } 495 | 496 | const initNumberMap = (m: Map, length: number, value: any) => { 497 | for (let i = 0; i < length; i++) m.set(i, value); 498 | } 499 | 500 | const printNumberMap = (m: Map) => { 501 | let msg = "\n\n"; 502 | for (let i = 0; i < m.size; i++) msg += `\n[printMap][${zeroPad(i, 5)}] ${m.get(i)!}`; 503 | msg += "\n\n" 504 | return msg; 505 | } 506 | 507 | const getBlockWithExtras = async (api: ApiPromise, number: number | null): Promise => { 508 | let hash, block, timestamp; 509 | if (number) { 510 | hash = (await api.rpc.chain.getBlockHash(number)!); 511 | block = (await api.rpc.chain.getBlock(hash)!).block; 512 | timestamp = await (await api.at(hash)).query.timestamp.now(); 513 | } else { 514 | block = (await api.rpc.chain.getBlock()!).block; 515 | timestamp = await api.query.timestamp.now(); 516 | hash = block.hash.toHuman(); 517 | } 518 | return { 519 | hash, 520 | number: block.header.number.toNumber(), 521 | timestamp: parseInt(timestamp.toString()) / 1_000, 522 | extrinsics: block.extrinsics.map((xt) => blake2AsHex(xt.toHex())), 523 | } 524 | } 525 | 526 | const getAvailSender = async (config: TPSConfig, key: number) => { 527 | const maxTxnPerSender = Math.ceil(config.transactions / config.accounts); 528 | key = key === 0 ? key : key + 1; 529 | if (key >= config.accounts) key = 0; 530 | while (sendersFreeMap.size > 0) { 531 | let availKeys = sendersFreeMap.keys(); 532 | for (let k of availKeys) { 533 | if (sendersTxnMap.get(k)! >= maxTxnPerSender) { 534 | sendersFreeMap.delete(k); 535 | continue; 536 | } 537 | if (k < key) continue; 538 | if (sendersBusyMap.get(k)!) continue; 539 | return k; 540 | } 541 | key++; 542 | if (key >= config.accounts) key = 0; 543 | await new Promise(r => setTimeout(r, 1)); 544 | } 545 | return -1; 546 | } 547 | 548 | const getFreeWorker = async (config: TPSConfig, workerId: number) => { 549 | workerId = workerId === 0 ? workerId : workerId + 1; 550 | if (workerId >= config.workers) workerId = 0; 551 | while (workersMap.get(workerId)!) { 552 | await new Promise(r => setTimeout(r, 1)); 553 | workerId++; 554 | if (workerId >= config.workers) workerId = 0; 555 | } 556 | return workerId; 557 | } 558 | 559 | const validTxHash = (txHash: string | undefined) => { 560 | if (txHash === undefined || txHash === null) return false; 561 | if (!txHash?.startsWith('0x')) return false; 562 | if (txHash?.length !== 66) return false; 563 | return true; 564 | } 565 | 566 | const resendAuto = async (config: TPSConfig, workerId: number) => { 567 | const sendersErrMapCopy = new Map(sendersErrMap); 568 | sendersErrMap.clear(); 569 | 570 | console.log(`\n\n----- Resending ${reqErrorsMap.size} Failed Requests -----\n\n`); 571 | console.log(printNumberMap(reqErrorsMap)); 572 | 573 | reqCounter -= reqErrorsMap.size; 574 | reqErrorsMap.clear(); 575 | reqErrCounter = 0; 576 | 577 | for (let k = 0; k < sendersErrMapCopy.size; k++) { 578 | let nonce = nonceMap.get(k)!; 579 | for (let j = 0; j < sendersErrMapCopy.get(k)!; j++) { 580 | await checkTxpool(config); 581 | workerId = await getFreeWorker(config, workerId); 582 | reqCounter++; 583 | autoSendRawTransaction(config, workerId, k, nonce); 584 | nonce++; 585 | } 586 | nonceMap.set(k, nonce); 587 | } 588 | } 589 | 590 | const autoSendRawTransaction = async ( 591 | config: TPSConfig, 592 | workerId: number, 593 | senderKey: number, 594 | nonce: number, 595 | ) => { 596 | sendersBusyMap.set(senderKey, true); 597 | workersMap.set(workerId, true); 598 | const api = workersAPIMap.get(workerId)!; 599 | 600 | const pre = `[req: ${zeroPad(reqCounter, 5)}][addr: ${zeroPad(senderKey, 5)}]`; 601 | let post = `[wrk: ${zeroPad(workerId, 5)}(len=${zeroPad(workersMap.size, 5)}) `; 602 | post += `nonce: ${zeroPad(nonce, 5)} | `; 603 | post += `fee: ${printGasPrice(chainFee)} | `; 604 | post += `pool: ${zeroPad(txPoolLength, 5)} | err=${reqErrorsMap.size}]`; 605 | let msg = ""; 606 | 607 | const start = Date.now(); 608 | try { 609 | const txHash = await submitExtrinsic(api, senderKey, nonce); 610 | if (validTxHash(txHash)) { 611 | const t = Date.now() - start; 612 | const postWithTime = `${post} [time: ${zeroPad(t, 5)}${t > 12000 ? " ***" : ""}]`; 613 | msg = `${pre} auto: ${txHash} ${postWithTime}`; 614 | if (config.verbose) console.log(msg); 615 | 616 | lastTxHash = txHash; 617 | let nextNonce = nonce + 1; 618 | nonceMap.set(senderKey, nextNonce); 619 | 620 | let amounts = rcvBalances.get(senderKey)!; 621 | amounts.after += 1_000; 622 | rcvBalances.set(senderKey, amounts); 623 | } else { throw Error(`Invalid txHash: ${txHash}`) } 624 | } catch (error: any) { 625 | sendersErrMap.set(senderKey, sendersErrMap.get(senderKey)! + 1); 626 | sendersTxnMap.set(senderKey, sendersTxnMap.get(senderKey)! - 1); 627 | sendersFreeMap.set(senderKey, true); 628 | msg = `${pre} auto: ${error.message} ${post}`; 629 | reqErrorsMap.set(reqErrCounter, msg); 630 | reqErrCounter++; 631 | } 632 | 633 | sendersBusyMap.delete(senderKey); 634 | workersMap.delete(workerId); 635 | } 636 | 637 | const auto = async (config: TPSConfig) => { 638 | let status_code = 0; 639 | let msg = ""; 640 | const start = Date.now(); 641 | let workerId = 0; 642 | try { 643 | const api = await substrateApi.get(config); 644 | let startingBlock = await getBlockWithExtras(api, null); 645 | let initialCounter = reqCounter; 646 | 647 | while ((reqCounter - initialCounter) < config.transactions) { 648 | if (hardstop) { 649 | hardstop = false; 650 | return [0, "HARD_STOP"]; 651 | } 652 | // 5% of errors is too much, something is wrong. 653 | if (reqErrorsMap.size >= (config.transactions * 0.05)) { 654 | console.log(printNumberMap(reqErrorsMap)); 655 | let p = Math.round((reqErrorsMap.size / config.transactions) * 100); 656 | return [0, `TOO_MANY_ERRORS: ${reqErrorsMap.size}/${config.transactions} [~${p}%]`]; 657 | } 658 | await checkTxpool(config); 659 | nextKey = await getAvailSender(config, nextKey); 660 | if (nextKey === -1 || sendersFreeMap.size === 0) break; 661 | workerId = await getFreeWorker(config, workerId); 662 | const nonce = nonceMap.get(nextKey)!; 663 | reqCounter++; 664 | autoSendRawTransaction(config, workerId, nextKey, nonce); 665 | sendersTxnMap.set(nextKey, sendersTxnMap.get(nextKey)! + 1); 666 | } 667 | 668 | // Wait till no more running workers. 669 | while (workersMap.size > 0) { await new Promise(r => setTimeout(r, 5)) }; 670 | 671 | while (reqErrorsMap.size > 0) await resendAuto(config, workerId); 672 | 673 | while (txPoolLength > 0) await new Promise(r => setTimeout(r, 100)); 674 | 675 | // Wait till no more running workers. 676 | while (workersMap.size > 0) { await new Promise(r => setTimeout(r, 5)) }; 677 | 678 | let tpsResult = await calculateTPS(config, startingBlock); 679 | reqErrorsMap.clear(); 680 | reqErrCounter = 0; 681 | 682 | if (config.tokenAssert) await assertTokenBalances(config); 683 | 684 | lastTxHash = ""; 685 | 686 | let t = Date.now() - start; 687 | let pre = `[req: ${zeroPad(reqCounter, 5)}][addr: ${zeroPad(0, 5)}]`; 688 | let post = `[wrk: ${zeroPad(workersMap.size, 5)} | pool: ${zeroPad(txPoolLength, 5)} | time: ${zeroPad(t, 5)}]`; 689 | msg = `${pre} auto: ${tpsResult} ${post}`; 690 | } catch (error: any) { 691 | msg = `[ERROR][req: ${zeroPad(reqCounter, 5)}][wrk: ${zeroPad(workersMap.size, 5)}] auto: ${error.message}`; 692 | } 693 | console.log(msg); 694 | return [status_code, msg]; 695 | } 696 | 697 | const setup = async () => { 698 | setupDirs(); 699 | 700 | let deployer = await getDeployer(EVM_TPS_CONFIG_FILE); 701 | let config = await setConfig(EVM_TPS_CONFIG_FILE, deployer); 702 | 703 | resetMaps(config); 704 | 705 | if (workersAPIMap.size == 0) { 706 | for (let i = 0; i < config.workers; i++) { 707 | // We need an API for each worker 708 | const subs = new SubstrateApi(''); 709 | const api = await subs.get(config); 710 | workersAPIMap.set(i, api); 711 | } 712 | } 713 | 714 | await setupAccounts(config, EVM_TPS_SENDERS_FILE, EVM_TPS_RECEIVERS_FILE); 715 | 716 | const api = await substrateApi.get(config); 717 | let block = await getBlockWithExtras(api, null); 718 | inherentExtrinsics = block.extrinsics.length; 719 | 720 | if (config.fundSenders) await checkBalances(config, deployer); 721 | 722 | await updateNonces(config); 723 | await updateBalances(config); 724 | 725 | config = await setTxpool(config, deployer); 726 | 727 | console.log(JSON.stringify(config, null, 2)); 728 | 729 | hardstop = false; 730 | 731 | return config!; 732 | } 733 | 734 | const main = async () => { 735 | 736 | let config = await setup(); 737 | 738 | blockTracker(config); 739 | txpoolChecker(config); 740 | 741 | feeChecker(config); 742 | 743 | const app = express(); 744 | app.use(BodyParser.json()); 745 | 746 | app.get("/auto", async (req: any, res: any) => { 747 | config = await setup(); 748 | console.log(`[Server] Running auto()...`); 749 | const [status, msg] = await auto(config); 750 | if (status === 0) res.send(msg); 751 | else res.status(500).send(`Internal error: /auto ${msg}`); 752 | }); 753 | 754 | app.get("/stats", async (req: any, res: any) => { 755 | try { 756 | const stats = { 757 | senders: sendersMap.size, 758 | receivers: receiversMap.size, 759 | receipts: receiptsMap.size, 760 | nonces: nonceMap.size, 761 | workers: workersMap.size, 762 | reqCounter, 763 | errors: reqErrorsMap.size, 764 | } 765 | const msg = `[req: ${zeroPad(reqCounter, 5)}][acc: ${zeroPad(-1, 5)}] status: \n${JSON.stringify(stats, null, 2)}\n`; 766 | console.log(msg); 767 | res.send(msg); 768 | } catch (error: any) { 769 | console.error(`[ERROR][req: ${zeroPad(reqCounter, 5)}][acc: ${zeroPad(-1, 5)}] reset: ${error.message}`); 770 | res.status(500).send(`Internal error: /stats ${error.message}`); 771 | } 772 | }); 773 | 774 | app.get("/reset", async (req: any, res: any) => { 775 | try { 776 | const start = Date.now(); 777 | config = await setup(); 778 | const t = Date.now() - start; 779 | const msg = `[req: ${zeroPad(reqCounter, 5)}][acc: ${zeroPad(-1, 5)}] reset: [b: - | t: ${t}]`; 780 | console.log(msg); 781 | res.send(msg); 782 | } catch (error: any) { 783 | console.error(`[ERROR][req: ${zeroPad(reqCounter, 5)}][acc: ${zeroPad(-1, 5)}] reset: ${error.message}`); 784 | res.status(500).send(`Internal error: /reset ${error.message}`); 785 | } 786 | }); 787 | 788 | app.get("/dumpErrors", async (req: any, res: any) => { 789 | try { 790 | const msg = `----- dumpErrors: ${printNumberMap(reqErrorsMap)}`; 791 | console.log(msg); 792 | res.send(msg); 793 | } catch (error: any) { 794 | res.status(500).send(`Internal error: /dumpErrors ${error.message}`); 795 | } 796 | }); 797 | 798 | app.get("/stop", async (req: any, res: any) => { 799 | try { 800 | const start = Date.now(); 801 | hardstop = true; 802 | const t = Date.now() - start; 803 | const msg = `[req: ${zeroPad(reqCounter, 5)}][acc: ${zeroPad(-1, 5)}] stop: [b: - | t: ${t}]`; 804 | console.log(msg); 805 | res.send(msg); 806 | } catch (error: any) { 807 | res.status(500).send(`Internal error: /stop ${error.message}`); 808 | } 809 | }); 810 | 811 | app.listen(config.tpsServerPort, config.tpsServerHost, () => { 812 | console.log(`> Listening at http://${config.tpsServerHost}:${config.tpsServerPort}`); 813 | }); 814 | } 815 | 816 | main().catch((error) => { 817 | console.error(`[ERROR] ${error.message}`); 818 | process.exit(1); 819 | }); 820 | -------------------------------------------------------------------------------- /test/SimpleTokenTest.ts: -------------------------------------------------------------------------------- 1 | import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; 2 | import { SimpleToken } from "../typechain-types"; 3 | 4 | import { expect } from "chai"; 5 | import { ethers } from "hardhat"; 6 | 7 | describe("Lock", function () { 8 | let token: SimpleToken, alice: SignerWithAddress, bob: SignerWithAddress, owner: SignerWithAddress; 9 | const amountToMint = Math.pow(10, 8); 10 | 11 | beforeEach(async () => { 12 | [owner, alice] = await ethers.getSigners(); 13 | const SimpleToken = await ethers.getContractFactory("SimpleToken", owner); 14 | token = await SimpleToken.deploy("SimpleToken", "STK"); 15 | await token.deployed(); 16 | }); 17 | 18 | describe("Test", function () { 19 | it("One", async () => { 20 | await expect(token.connect(alice).mintTo(alice.address, amountToMint)).to.be.revertedWith( 21 | "Onwer has not started the contract yet." 22 | ); 23 | await expect(token.connect(alice).start()).to.be.revertedWith( 24 | "Only owner can start it." 25 | ); 26 | await token.start(); 27 | expect(await token.connect(alice).mintTo(alice.address, amountToMint)); 28 | }); 29 | 30 | it("Two", async () => { 31 | await token.start(); 32 | expect(await token.mintTo(alice.address, amountToMint)); 33 | let aliceAmount = await token.balanceOf(alice.address); 34 | expect(aliceAmount).to.equal(amountToMint, `alice should have ${amountToMint} tokens but she has ${aliceAmount}.`); 35 | }); 36 | 37 | it("Three", async () => { 38 | await token.start(); 39 | expect(await token.mintTo(owner.address, amountToMint)); 40 | expect(await token.transferLoop(7, alice.address, 5)); 41 | let aliceAmount = await token.balanceOf(alice.address); 42 | expect(aliceAmount).to.equal(7 * 5, `alice should have ${7 * 5} tokens but she has ${aliceAmount}.`); 43 | }); 44 | 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "module": "commonjs", 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "strict": true, 8 | "skipLibCheck": true, 9 | "resolveJsonModule": true 10 | } 11 | } 12 | --------------------------------------------------------------------------------