FaucetEx

Programmatic testnet funding for integration tests, as one library instead of a helper copied into every repo.

One loop — read the balance, request funding, wait for it to land, read again, bounded by a request budget and serialized per address — over pluggable sources. The loop never knows which provider it is driving.

Source Provider Asset
Faucet.Source.CDP Coinbase Developer Platform faucet API ETH / USDC / EURC / cbBTC on Base Sepolia, Ethereum Sepolia
Faucet.Source.Tempo Tempo Moderato tempo_fundAddress RPC pathUSD fee token + native gas
Faucet.Source.Solana requestAirdrop RPC SOL on devnet / testnet
Faucet.Source.XRPL XRPL testnet faucet HTTP API XRP on altnet
Faucet.Source.ERC20Mint An Aave-style mint(token, to, amount) faucet contract Any mintable test ERC-20
Faucet.ForkOverride None — builds state_overrides for Onchain.EVM fork simulation ETH and ERC-20 balances

Installation

def deps do
[
{:faucet_ex, "~> 0.1", only: :test},
# Optional. Needed for ERC20Mint (signing), fresh keypairs and ForkOverride (keccak).
{:onchain, "~> 0.14", only: :test}
]
end

Usage

# Top up an address to at least 0.001 ETH on Base Sepolia. Existing balance is
# reused; nothing is requested when it already suffices.
{:ok, wei} =
Faucet.ensure_min_balance(Faucet.Source.CDP, address, 1_000_000_000_000_000, network: "base-sepolia")
# Fresh keypair funded with pathUSD on Tempo Moderato.
{:ok, %{private_key: key, address_hex: hex}} =
Faucet.EVM.fresh_funded_wallet(Faucet.Source.Tempo, 1_000_000)
# Aave testUSDC on Base Sepolia, minted by the faucet contract from a gas-funded key.
{:ok, units} =
Faucet.ensure_min_balance(Faucet.Source.ERC20Mint, address, 25_000_000,
faucet: "0xd9145b5f45ad4519c7accd6e0a4a82e83bb8a6dc",
token: "0xba50Cd2A20f6DA35D788639E581bca8d0B5d4D5f",
rpc_url: "https://sepolia.base.org",
chain_id: 84_532,
private_key: key
)
# In ExUnit setup: fail loudly with the provider's reason instead of skipping.
Faucet.ensure_min_balance!(Faucet.Source.Solana, pubkey, 100_000_000)
# Fork simulation: no provider, just the override map Onchain.EVM accepts.
overrides =
Faucet.ForkOverride.new()
|> Faucet.ForkOverride.native(user, 10 ** 18)
|> Faucet.ForkOverride.erc20(weth, user, 3, 5 * 10 ** 18)

Loop options: :max_requests (default 10), :timeout_ms (60_000), :poll_interval_ms (1_000), :lock (true, serializes callers per address via :global.trans/2). Everything else is passed to the source; each source documents its keys.

Writing a source

Implement Faucet.Source: balance/2, fund/2, unit/0, and optionally wait_confirmed/3. Without the last, the loop polls balance/2 until it rises above the pre-request reading. fund/2 receives :deficit in opts so a source that can mint an exact amount does not have to guess.

Credentials

Only Faucet.Source.CDP needs any: CDP_API_KEY_ID and CDP_API_KEY_SECRET (Ed25519 key from https://portal.cdp.coinbase.com/access/api), or the :api_key_id / :api_key_secret options. Missing credentials return {:error, {:missing_credential, name, hint}} before any request is made.

Discoverability

Public modules carry Descripex hints: Faucet.describe(), Faucet.describe(:evm), Faucet.describe(:evm, :wait_receipt).

License

MIT