Skip to content

Instantly share code, notes, and snippets.

@emo-eth
Last active June 22, 2023 14:43
Show Gist options
  • Star 47 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save emo-eth/86d2e1a524ffc66eb424770f74165a49 to your computer and use it in GitHub Desktop.
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
###########
# 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
Copy link

Thanks for sharing! Would you mind also sharing rpcs.sh, since it seems required for chain() to work?

@emo-eth
Copy link
Author

emo-eth commented Dec 25, 2022

@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).

@mattstam
Copy link

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