Component
Forge, Cast
Describe the feature you would like
I'm currently migrating from dapp.tools and it had this nice UX to handle keystore files:
- Declare the following env vars:
ETH_KEYSTORE: the path to the keystore directory
ETH_FROM: the account from which send the tx from (I believe this was used to find the right keystore file inside the keystore directory).
ETH_PASSWORD: the path to a plain-text password file with the password for the keystore file in the local file system.
- Anything requiring wallet signing pickups the above variables automatically:
dapp create src/MyContract.sol:MyContract
seth send $ADDRESS "myFunc(address)" $ARG
I'm finding it a bit difficult to work with Foundry for that purpose. Specifically regarding contract deployment. Main differences are:
ETH_KEYSTORE works, but it expects a keystore file, not a directory.
ETH_FROM is redundant, since the sender wallet is derived from ETH_KEYSTORE
ETH_PASSWORD doesn't work.
Also if I use CAST_PASSWORD env var, forge create does not pick it up, so I need to be explicit when passing it:
forge create --password=${CAST_PASSWORD} src/MyContract.sol:MyContract.sol
Also ideally such configs could be in foundry.toml as well:
[default]
password-file = '~/.eth-password'
# either
keystore-directory = '~/.ethereum/keystore'
from = '0xdeAD00000000000000000000000000000000dEAd'
# or
keystore-file = '~/.ethereum/keystore/UTC--2022-01-01T00-00-00.000000000Z--dead00000000000000000000000000000000dead'
Furthermore, users should have the liberty to overwrite any configs with environment variables. For example, it's very useful to be able to define a keystore directory containing multiple accounts and define the from address with env vars.
FOUNDRY_ETH_FROM=0x0783...122 FOUNDRY_PASSWORD_FILE=~/.eth-password-0783...122 forge deploy ...
Additional context
As a workaround, I have written a small helper script to allow me to use forge the same way I could use dapp.tools. Perhaps this can help guiding the implementation.
#!/bin/bash
# scripts/deploy.sh
set -eo pipefail
function log() {
echo -e "$@" >&2
}
function die() {
log "$@"
log ""
exit 1
}
function err_msg_keystore_file() {
cat <<MSG
ERROR: could not determine the location of the keystore file.
You should either define:
\t1. The FOUNDRY_ETH_KEYSTORE_FILE env var or;
\t2. Both FOUNDRY_ETH_KEYSTORE_DIR and FOUNDRY_ETH_FROM env vars.
MSG
}
function err_msg_etherscan_api_key() {
cat <<MSG
ERROR: cannot verify contracts without ETHERSCAN_API_KEY being set.
You should either:
\t1. Not use the --verify flag or;
\t2. Define the ETHERSCAN_API_KEY env var.
MSG
}
function usage() {
cat <<MSG
deploy.sh contract_path [--constructor-args ...args]
Examples:
\t# Constructor does not take any arguments
\tdeploy.sh src/MyContract.sol:MyContract
\t# Constructor takes (uint, address) arguments
\tdeploy.sh src/MyContract.sol:MyContract --constructor-args 1 0x0000000000000000000000000000000000000000
MSG
}
function deploy() {
local ENV_FILE="${BASH_SOURCE%/*}/../.env"
[ -f "$ENV_FILE" ] && source "$ENV_FILE"
FOUNDRY_ETH_FROM="${FOUNDRY_ETH_FROM:-$ETH_FROM}"
FOUNDRY_ETHERSCAN_API_KEY="${FOUNDRY_ETHERSCAN_API_KEY:-$ETHERSCAN_API_KEY}"
FOUNDRY_ETH_KEYSTORE_DIRECTORY="${FOUNDRY_ETH_KEYSTORE_DIRECTORY:-$ETH_KEYSTORE}"
if [ -z "$FOUNDRY_ETH_KEYSTORE_FILE" ]; then
[ -z "$FOUNDRY_ETH_KEYSTORE_DIRECTORY" ] && die "$(err_msg_keystore_file)"
# Foundy expects the Ethereum Keystore file, not the directory.
# This step assumes the Keystore file for the deployed wallet includes $ETH_FROM in its name.
FOUNDRY_ETH_KEYSTORE_FILE="${FOUNDRY_ETH_KEYSTORE_DIRECTORY%/}/$(ls -1 $FOUNDRY_ETH_KEYSTORE_DIRECTORY | \
# -i: case insensitive
# #0x: strip the 0x prefix from the the address
grep -i ${FOUNDRY_ETH_FROM#0x})"
fi
[ -z "$FOUNDRY_ETH_KEYSTORE_FILE" ] && die "$(err_msg_keystore_file)"
# Handle reading from the password file
local PASSWORD_OPT=''
if [ -f "$FOUNDRY_ETH_PASSWORD_FILE" ]; then
PASSWORD_OPT="--password=$(cat "$FOUNDRY_ETH_PASSWORD_FILE")"
fi
# Require the Etherscan API Key if --verify option is enabled
set +e
if grep -- '--verify' <<< "$@" > /dev/null; then
[ -z "$FOUNDRY_ETHERSCAN_API_KEY" ] && die "$(err_msg_etherscan_api_key)"
fi
set -e
# Log the command being issued, making sure not to expose the password
log "forge create --keystore="$FOUNDRY_ETH_KEYSTORE_FILE" $(sed 's/=.*$/=[REDACTED]/' <<<${PASSWORD_OPT}) $@"
forge create --keystore="$FOUNDRY_ETH_KEYSTORE_FILE" ${PASSWORD_OPT} $@
}
# Executes the function if it's been called as a script.
# This will evaluate to false if this script is sourced by other script.
if [ "$0" = "$BASH_SOURCE" ]; then
if [ $# -eq 0 ]; then
die "$(usage)"
fi
[ "$1" = '-h' ] || [ "$1" = '--help' ] && {
log "$(usage)"
exit 0
}
deploy $@
fi
# .env
export FOUNDRY_ETHERSCAN_API_KEY=API_KEY
export FOUNDRY_ETH_FROM=WALLET_ADDRESS
export FOUNDRY_ETH_KEYSTORE_DIRECTORY=PATH_OF_ETHEREUM_KEYSTORE_DIR # i.e.: ${HOME}/.ethereum/keystore/
export FOUNDRY_ETH_PASSWORD=PATH_OF_ETHEREUM_PASSWORD_FILE
Component
Forge, Cast
Describe the feature you would like
I'm currently migrating from
dapp.toolsand it had this nice UX to handle keystore files:ETH_KEYSTORE: the path to the keystore directoryETH_FROM: the account from which send the tx from (I believe this was used to find the right keystore file inside the keystore directory).ETH_PASSWORD: the path to a plain-text password file with the password for the keystore file in the local file system.I'm finding it a bit difficult to work with Foundry for that purpose. Specifically regarding contract deployment. Main differences are:
ETH_KEYSTOREworks, but it expects a keystore file, not a directory.ETH_FROMis redundant, since the sender wallet is derived fromETH_KEYSTOREETH_PASSWORDdoesn't work.Also if I use
CAST_PASSWORDenv var,forge createdoes not pick it up, so I need to be explicit when passing it:forge create --password=${CAST_PASSWORD} src/MyContract.sol:MyContract.solAlso ideally such configs could be in
foundry.tomlas well:Furthermore, users should have the liberty to overwrite any configs with environment variables. For example, it's very useful to be able to define a keystore directory containing multiple accounts and define the
fromaddress with env vars.Additional context
As a workaround, I have written a small helper script to allow me to use
forgethe same way I could usedapp.tools. Perhaps this can help guiding the implementation.