Last active
June 22, 2023 14:43
-
-
Save emo-eth/86d2e1a524ffc66eb424770f74165a49 to your computer and use it in GitHub Desktop.
Helper functions for interacting with chains and Foundry tests. Source from .zshrc etc
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
########### | |
# Imports # | |
########### | |
# the RPCs file should include RPC URLs and Etherscan API Keys for relevant networks | |
# (in a separate file so they don't get committed) | |
source "$(dirname "$0")/rpcs.sh" | |
# any useful addresses for various networks for easy reference | |
source "$(dirname "$0")/addresses.sh" | |
# any useful functions and definitions for interacting with Seaport | |
source "$(dirname "$0")/seaport.sh" | |
export ECRECOVER=0x0000000000000000000000000000000000000001 | |
############### | |
# RPC Helpers # | |
############### | |
# block explorer urls for the explore() helper | |
export ETHEREUM_BLOCK_EXPLORER=https://etherscan.io | |
export GOERLI_BLOCK_EXPLORER=https://goerli.etherscan.io | |
export POLYGON_BLOCK_EXPLORER=https://polygonscan.com | |
export MUMBAI_BLOCK_EXPLORER=https://mumbai.polygonscan.com | |
export OPTIMISM_BLOCK_EXPLORER=https://optimistic.etherscan.io | |
export OPTIMISM_GOERLI_BLOCK_EXPLORER=https://goerli-optimism.etherscan.io | |
export ARBITRUM_BLOCK_EXPLORER=https://arbiscan.io | |
export ARBITRUM_NOVA_BLOCK_EXPLORER=https://nova.arbiscan.io | |
export ARBITRUM_GOERLI_BLOCK_EXPLORER=https://goerli.arbiscan.io | |
export AVALANCHE_BLOCK_EXPLORER=https://snowtrace.io | |
export FUJI_BLOCK_EXPLORER=https://testnet.snowtrace.io | |
export BSC_BLOCK_EXPLORER=https://bscscan.com | |
export BSC_TEST_BLOCK_EXPLORER=https://testnet.bscscan.com | |
export GNOSIS_BLOCK_EXPLORER=https://gnosisscan.io | |
export KLAYTN_BLOCK_EXPLORER=https://scope.klaytn.com | |
export BAOBAB_BLOCK_EXPLORER=https://baobab.scope.klaytn.com | |
# Set ETH_RPC_URL and ETHERSCAN_API_KEY (the defaults that forge + cast read) based on chain name | |
# Uses values sourced from rpcs.sh | |
chain() { | |
chain_name=$1 | |
if [[ "$1" == "polygon" ]] | |
then | |
export ETH_RPC_URL=$POLYGON_RPC_URL | |
export ETHERSCAN_API_KEY=$POLYGON_ETHERSCAN_API_KEY | |
export BLOCK_EXPLORER=$POLYGON_BLOCK_EXPLORER | |
elif [[ "$1" == "mumbai" ]] | |
then | |
export ETH_RPC_URL=$MUMBAI_RPC_URL | |
export ETHERSCAN_API_KEY=$POLYGON_ETHERSCAN_API_KEY | |
export BLOCK_EXPLORER=$MUMBAI_BLOCK_EXPLORER | |
elif [[ "$1" == "goerli" ]] | |
then | |
export ETH_RPC_URL=$GOERLI_RPC_URL | |
export ETHERSCAN_API_KEY=$ETHEREUM_ETHERSCAN_API_KEY | |
export BLOCK_EXPLORER=$GOERLI_BLOCK_EXPLORER | |
elif [[ "$1" == "arbitrum" ]] | |
then | |
export ETH_RPC_URL=$ARBITRUM_RPC_URL | |
export ETHERSCAN_API_KEY=$ARBITRUM_ETHERSCAN_API_KEY | |
export BLOCK_EXPLORER=$ARBITRUM_BLOCK_EXPLORER | |
elif [[ "$1" == "arbitrum-nova" ]] | |
then | |
export ETH_RPC_URL=$ARBITRUM_NOVA_RPC_URL | |
export ETHERSCAN_API_KEY=$ARBITRUM_ETHERSCAN_API_KEY | |
export BLOCK_EXPLORER=$ARBITRUM_NOVA_BLOCK_EXPLORER | |
elif [[ "$1" == "arbitrum-goerli" ]] | |
then | |
export ETH_RPC_URL=$ARBITRUM_GOERLI_RPC_URL | |
export ETHERSCAN_API_KEY=$ARBITRUM_ETHERSCAN_API_KEY | |
export BLOCK_EXPLORER=$ARBITRUM_GOERLI_BLOCK_EXPLORER | |
elif [[ "$1" == "optimism" ]] | |
then | |
export ETH_RPC_URL=$OPTIMISM_RPC_URL | |
export ETHERSCAN_API_KEY=$OPTIMISM_ETHERSCAN_API_KEY | |
export BLOCK_EXPLORER=$OPTIMISM_BLOCK_EXPLORER | |
elif [[ "$1" == "optimism-goerli" ]] | |
then | |
export ETH_RPC_URL=$OPTIMISM_GOERLI_RPC_URL | |
export ETHERSCAN_API_KEY=$OPTIMISM_ETHERSCAN_API_KEY | |
export BLOCK_EXPLORER=$OPTIMISM_GOERLI_BLOCK_EXPLORER | |
elif [[ "$1" == "klaytn" ]] | |
then | |
export ETH_RPC_URL=$KLAYTN_RPC_URL | |
export BLOCK_EXPLORER=$KLAYTN_BLOCK_EXPLORER | |
elif [[ "$1" == "baobab" ]] | |
then | |
export ETH_RPC_URL=$BAOBAB_RPC_URL | |
export BLOCK_EXPLORER=$BAOBAB_BLOCK_EXPLORER | |
elif [[ "$1" == "bsc" ]] | |
then | |
export ETH_RPC_URL=$BSC_RPC_URL | |
export BLOCK_EXPLORER=$BSC_BLOCK_EXPLORER | |
elif [[ "$1" == "bsc-test" ]] | |
then | |
export ETH_RPC_URL=$BSC_TEST_RPC_URL | |
export BLOCK_EXPLORER=$BSC_TEST_BLOCK_EXPLORER | |
elif [[ "$1" == "anvil" ]] | |
then | |
export ETH_RPC_URL=$ANVIL_RPC_URL | |
elif [[ "$1" == "avalanche" ]] | |
then | |
export ETH_RPC_URL=$AVALANCHE_RPC_URL | |
export BLOCK_EXPLORER=$AVALANCHE_BLOCK_EXPLORER | |
elif [[ "$1" == "fuji" ]] | |
then | |
export ETH_RPC_URL=$FUJI_RPC_URL | |
export BLOCK_EXPLORER=$FUJI_BLOCK_EXPLORER | |
else | |
# fallback is mainnet | |
export chain_name="mainnet" | |
export ETHERSCAN_API_KEY=$ETHEREUM_ETHERSCAN_API_KEY | |
export ETH_RPC_URL=$ETHEREUM_RPC_URL | |
export BLOCK_EXPLORER=$ETHEREUM_BLOCK_EXPLORER | |
fi | |
} | |
# View an address or transaction hash on the block explorer of | |
# the current active chain (configured with chain() command) | |
# macOS only (probably) | |
explore() { | |
if [[ ${#1} -eq 42 ]]; then | |
arg="${BLOCK_EXPLORER}/address/$1" | |
elif [[ ${#1} -lt 42 ]]; then | |
address=$(ens $1) | |
arg="${BLOCK_EXPLORER}/address/$address" | |
else | |
arg="${BLOCK_EXPLORER}/tx/$1" | |
fi | |
open -n $arg | |
} | |
################ | |
# Cast Helpers # | |
################ | |
ecrecover() { | |
cast call $ECRECOVER $(cast abi-encode "ecrecover(bytes32,uint8,bytes32,bytes32)(address)" $1 $2 $3 $4) | |
} | |
# "Decimal to Hex" | |
d2h() { | |
cast --to-base $1 16 | |
} | |
# "Hex to Decimal" | |
h2d() { | |
cast --to-base $1 10 | |
} | |
# balanceOf(address) | |
balanceof() { | |
cast call $1 "balanceOf(address)(uint256)" $2 | |
} | |
# ERC721::ownerOf(uint256) | |
ownerof() { | |
cast call $1 "ownerOf(uint256)(address)" $2 | |
} | |
# ERC1155::balanceOf(address, uint256) | |
balanceof11() { | |
cast call $1 "balanceOf(address, uint256)(uint256)" $2 $3 | |
} | |
# ERC721:tokenURI(uint256) | |
uri() { | |
cast call $1 "tokenURI(uint256)(string)" $2 | |
} | |
# ERC1155::uri(uint256) | |
uri11() { | |
cast call $1 "uri(uint256)(string)" $2 | |
} | |
# look up ens name (minus .eth suffix) | |
ens() { | |
cast resolve-name $1.eth | |
} | |
# look up the admin slot of a proxy | |
admin() { | |
cast --abi-decode "sig()(address)" $(cast storage $1 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103) | |
} | |
# look up the implementation slot of a proxy | |
impl() { | |
cast --abi-decode "sig()(address)" $(cast storage $1 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc) | |
} | |
# look up the owner of an ownable contract | |
owner() { | |
cast call $1 "owner()(address)" | |
} | |
# abi-encode an address | |
encodeaddr() { | |
cast abi-encode "sig(address)" $1 | |
} | |
################# | |
# Forge Helpers # | |
################# | |
# The verbosity config value in foundry.toml normally takes preference over FOUNDRY_VERBOSITY, but defaults to 0 | |
# The helper functions inline this variable to override the verbosity level set in foundry.toml | |
FOUNDRY_VERBOSITY=3 | |
# "Forge contract" - Run forge tests that match a specific contract | |
fcon() { | |
FOUNDRY_VERBOSITY=$FOUNDRY_VERBOSITY forge test --match-contract $1 | |
} | |
# "Forge contract watch" Run forge tests that match a specific contract and watch for changes | |
fconw() { | |
FOUNDRY_VERBOSITY=$FOUNDRY_VERBOSITY forge test --match-contract $1 --watch | |
} | |
# "Forge test <test>" - Run forge tests that match a specific test | |
ftest() { | |
if [ $# -eq 1 ]; then | |
FOUNDRY_VERBOSITY=$FOUNDRY_VERBOSITY forge test --match-test $1 | |
elif [ $# -eq 0 ]; then | |
FOUNDRY_VERBOSITY=$FOUNDRY_VERBOSITY forge test | |
fi | |
} | |
# "Forge test <test> watch" - Run Forge tests that match a specific test and watch for changes | |
ftestw() { | |
FOUNDRY_VERBOSITY=$FOUNDRY_VERBOSITY forge test --match-test $1 --watch | |
} | |
# "Forge test" - Run all Forge tests for the current active Foundry profile | |
ft() { | |
FOUNDRY_VERBOSITY=$FOUNDRY_VERBOSITY forge test | |
} | |
# "Forge script" - Run a specific Forge script | |
fs() { | |
FOUNDRY_VERBOSITY=$FOUNDRY_VERBOSITY forge snapshot $1 | |
} | |
# "Forge snapshot" - Run a gas snapshot | |
snap() { | |
forge snapshot | |
} | |
# "Forge gas" - Run all tests and generate a gas report | |
fg() { | |
FOUNDRY_VERBOSITY=$FOUNDRY_VERBOSITY forge test --gas-report | |
} | |
# "Forge watch" - Run all tests and watch for changes | |
fw() { | |
FOUNDRY_VERBOSITY=$FOUNDRY_VERBOSITY forge test --watch | |
} | |
# "Forge build" - Build the project with the current active Foundry profile | |
fb() { | |
forge build | |
} | |
# "Forge coverage" - Generate a coverage summary report as well as an lcov.info, and generate an HTML report from the lcov.info | |
# Requires the lcov package to be installed | |
fcov() { | |
forge coverage --report summary --report lcov && genhtml lcov.info -o html --branch | |
} | |
# "Forge debug" - Debug a specific test | |
fdebug() { | |
SEAPORT_COVERAGE=true forge test --debug $1 | |
} | |
# "Checksum address" - Generate a checksum address from a hex address and copy it to the clipboard | |
caddr() { | |
new_addr=$(cast --to-checksum-address $1) | |
echo $new_addr | |
echo $new_addr | pbcopy | |
} | |
# Generate standard JSON input for a contract. Useful for verifying contracts on Etherscan | |
stdjson() { | |
mkdir -p stdjson | |
forge verify-contract $(cast --address-zero) $1 --show-standard-json-input > stdjson/$1.json | |
echo "Standard JSON for $1 written to stdjson/$1.json" | |
} | |
# "Forge new file" - create a new contract file that includes the SPDX license header, pragma, and empty contract with the specified name | |
fnf() { | |
mkdir -p ./$1 | |
echo "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.17;\n\ncontract $2 {\n\n}" > ./$1/$2.sol | |
} | |
# "Forge new base test" - create a new base test file that imports the Forge Test contract and includes an empty virtual setUp() function | |
fnbt() { | |
echo "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.17;\n\nimport {Test} from \"forge-std/Test.sol\";\n\ncontract Base$1Test is Test {\n function setUp() public virtual { }\n}" > ./test/Base$1Test.sol | |
} | |
# "Forge new test" - create a new test file that imports the base test contract and includes an empty test function | |
# Note: Assumes there is a BaseTest contract and that it is located in the same directory as the test contract | |
fnt() { | |
mkdir -p ./test/$1 | |
echo "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.17;\n\nimport {BaseTest} from \"test/BaseTest.sol\";\n\ncontract $2Test is BaseTest {\n\n}" > ./test/$1/$2.t.sol | |
} | |
# "Forge new script" - create a new script file that imports the base Script contract and console2, and includes an empty run() function | |
fns() { | |
mkdir -p ./script/$1 | |
echo "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.17;\n\nimport {Script, console2} from \"forge-std/Script.sol\";\n\ncontract $2 is Script {\n function run() public { }\n}" > ./script/$1/$2.s.sol | |
} | |
# "Forge new CREATE2 script" - create a new script file that imports the base Create2Script contract and console2, and includes an empty run() function | |
fncs() { | |
mkdir -p ./script/$1 | |
echo "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.17;\n\nimport {BaseCreate2Script, console2} from \"create2-scripts/BaseCreate2Script.s.sol\";\n\ncontract $2 is BaseCreate2Script {\n function run() public { }\n}" > ./script/$1/$2.s.sol | |
} | |
# Re-initialize Git submodules when changing between branches with different dependency versions | |
# h/t @PaulRBerg | |
reinit() { | |
git submodule deinit -f . | |
git submodule update --init | |
} | |
# for easily testing forge scripts | |
# eg: in script: `address deployer = vm.addr(vm.envUint('anvilPk'))`, or | |
# `address deployer = vm.envAddress("anvilAddr")` + in bash command: `--private-key $anvilPk`, etc | |
export anvilAddr=0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266 | |
export anvilPk=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 | |
######## | |
# Huff # | |
######## | |
# "Runtime offset" - get the offset of runtime bytecode within a contract's creation bytecode as a hex number | |
rtoffset() { | |
runtime=$(huffc $1 -r) | |
creation=$(huffc $1 -b) | |
diff=$(d2h $(( (${#creation} - ${#runtime}) / 2))) | |
echo $diff | |
echo $diff | pbcopy | |
} | |
# "Constructor argument offset" - get the length of the creation code as a hex number | |
argoffset() { | |
creation=$(huffc $1 -b) | |
len=$(d2h $((${#creation} / 2))) | |
echo $len | |
echo $len | pbcopy | |
} | |
# "Runtime size" - get the size of the runtime bytecode as a hex number | |
rtsize() { | |
runtime=$(huffc $1 -r) | |
echo $(d2h $((${#runtime} / 2))) | |
} | |
########### | |
# Aliases # | |
########### | |
# "good morning" - update Foundry and Huff | |
alias gm="foundryup;huffup;" | |
# Use forge remappings to generate a remappings file | |
alias remap="forge remappings > remappings.txt" | |
# "edit chain functions" - open the chain_funcs.sh file in VS Code | |
alias ecf="code \"$(dirname "$0")/chain_funcs.sh\"" | |
# "edit rpcs" - open the rpcs.sh file in VS Code | |
alias erpc="code \"$(dirname "$0")/rpcs.sh\"" | |
# "edit addresses" - open the addresses.sh file in VS Code | |
alias eaddr="code \"$(dirname "$0")/addresses.sh\"" | |
# "edit seaport" - open the seaport.sh file in VS Code | |
alias esp="code \"$(dirname "$0")/seaport.sh\"" | |
############### | |
# Boilerplate # | |
############### | |
# automatically configure mainnet when opening a new shell, if ETH_RPC_URL is not already configured | |
if [[ -z "$ETH_RPC_URL" ]] then | |
chain | |
fi | |
# optional - add current active chain name to your bash prompt | |
# if using powerlevel10k theme, add this function to your p10k.zsh | |
# and add `chain` to either your POWERLEVEL9K_RIGHT_PROMPT_ELEMENTS or POWERLEVEL9K_LEFT_PROMPT_ELEMENTS | |
# function prompt_chain() { | |
# p10k segment -b 14 -i '🔗' -t "$chain_name" | |
# } | |
# if not using a fancy terminal theme - uncomment to prepend active chain to prompt, ie [mainnet] | |
# export PS1='[$chain_name]'$PS1 |
@mattstam the rpcs.sh
file only needs to export the various RPC URLs and Etherscan API keys, and live in the same directory as chain_functions.sh
– eg
export ETHEREUM_RPC_URL=
export GOERLI_RPC_URL=
export POLYGON_RPC_URL=
export MUMBAI_RPC_URL=
export ARBITRUM_RPC_URL=
export ARBITRUM_NOVA_RPC_URL=
export ARBITRUM_GOERLI_RPC_URL=
export ETHEREUM_ETHERSCAN_API_KEY=
export ARBITRUM_ETHERSCAN_API_KEY=
Alchemy can get you set up with a few archive RPC URLs for free, and Etherscan's various sites have free API keys if you create an account - but you'll need one key per chain (which will also work for that chain's testnets).
Thanks @jameswenzel. It would be super useful to get an example rpc.sh
with the needed keys to fill.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Thanks for sharing! Would you mind also sharing
rpcs.sh
, since it seems required for chain() to work?