Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ require (
github.com/stretchr/testify v1.11.1
github.com/testcontainers/testcontainers-go v0.42.0
go.yaml.in/yaml/v3 v3.0.4
golang.org/x/crypto v0.51.0
golang.org/x/sys v0.45.0
golang.org/x/time v0.15.0
google.golang.org/protobuf v1.36.11
Expand Down Expand Up @@ -132,7 +133,6 @@ require (
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.1 // indirect
go.yaml.in/yaml/v2 v2.4.4 // indirect
golang.org/x/crypto v0.51.0 // indirect
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // indirect
golang.org/x/mod v0.35.0 // indirect
golang.org/x/net v0.55.0 // indirect
Expand Down
10 changes: 5 additions & 5 deletions modules/evm/examples.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -117,9 +117,9 @@ evm_system_contracts:
])

w = evm.wallet()
print("Fund this address first:", evm.faucet(network, w["address"]))
# after funding:
# txhash = evm.tx(network, instance, w["private_key"], to=None, data=selfdestruct_code)
# faucet() mines + claims and returns the tx hash once the funds land
print("Funded via faucet, tx:", evm.faucet(network, w["address"]))
txhash = evm.tx(network, instance, w["private_key"], to=None, data=selfdestruct_code)

evm_deploy_and_fuzz:
name: Deploy and Fuzz
Expand All @@ -135,9 +135,9 @@ evm_deploy_and_fuzz:

w = evm.wallet()
print("Address:", w["address"])
print("Fund at:", evm.faucet(network, w["address"]))
print("Funded via faucet, tx:", evm.faucet(network, w["address"]))

# After funding, deploy bytecode that LOGs a result (visible in receipt)
# Funds have landed; deploy bytecode that LOGs a result (visible in receipt)
bytecode = evm.assemble([
"PUSH1", 0x42, # value to log
"PUSH1", 0x00, # memory offset
Expand Down
38 changes: 4 additions & 34 deletions modules/evm/module.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,9 @@ package evm

import (
"context"
"encoding/json"
"maps"
"sync"

"github.com/ethpandaops/panda/pkg/cartographoor"
"github.com/ethpandaops/panda/pkg/module"
"github.com/ethpandaops/panda/pkg/types"
)
Expand All @@ -15,7 +13,6 @@ import (
var (
_ module.Module = (*Module)(nil)
_ module.ProxyDiscoverable = (*Module)(nil)
_ module.CartographoorAware = (*Module)(nil)
_ module.SandboxEnvProvider = (*Module)(nil)
_ module.ExamplesProvider = (*Module)(nil)
_ module.PythonAPIDocsProvider = (*Module)(nil)
Expand All @@ -28,8 +25,6 @@ var (
type Module struct {
dsMu sync.RWMutex
datasources []types.DatasourceInfo

cartographoorClient cartographoor.CartographoorClient
}

// New creates a new evm module.
Expand Down Expand Up @@ -60,42 +55,17 @@ func (m *Module) InitFromDiscovery(datasources []types.DatasourceInfo) error {
return nil
}

// SetCartographoorClient implements module.CartographoorAware.
func (m *Module) SetCartographoorClient(client cartographoor.CartographoorClient) {
m.cartographoorClient = client
}

func (m *Module) Init(_ []byte) error { return nil }
func (m *Module) ApplyDefaults() {}
func (m *Module) Validate() error { return nil }

func (m *Module) Start(_ context.Context) error { return nil }
func (m *Module) Stop(_ context.Context) error { return nil }

// SandboxEnv injects EVM availability and per-network faucet URLs.
// SandboxEnv marks EVM availability for the sandbox. Faucet URLs are resolved
// server-side by the evm.faucet operation, so no per-network env is injected.
func (m *Module) SandboxEnv() (map[string]string, error) {
env := map[string]string{"ETHPANDAOPS_EVM_AVAILABLE": "true"}

if m.cartographoorClient == nil {
return env, nil
}

faucets := make(map[string]string)

for name, net := range m.cartographoorClient.GetActiveNetworks() {
if net.ServiceURLs != nil && net.ServiceURLs.Faucet != "" {
faucets[name] = net.ServiceURLs.Faucet
}
}

if len(faucets) > 0 {
data, err := json.Marshal(faucets)
if err == nil {
env["ETHPANDAOPS_EVM_FAUCET_NETWORKS"] = string(data)
}
}

return env, nil
return map[string]string{"ETHPANDAOPS_EVM_AVAILABLE": "true"}, nil
}

// Examples returns query examples for the evm module.
Expand Down Expand Up @@ -142,7 +112,7 @@ func (m *Module) PythonAPIDocs() map[string]types.ModuleDoc {
},
"faucet": {
Signature: "faucet(network, address) -> str",
Description: "Return the PoW faucet URL for the network pre-filled with the given address. Open in a browser to request test ETH.",
Description: "Mine the network's PoW faucet and claim test ETH to address; returns the claim tx hash. Runs the full agent PoW flow server-side (no browser, WebSocket, or captcha). Requires panda auth.",
},
},
},
Expand Down
29 changes: 14 additions & 15 deletions modules/evm/python/evm.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@

from __future__ import annotations

import json
import os
from typing import Any

from ethpandaops import _runtime
from ethpandaops.ethnode import execution_rpc as _rpc

# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -55,9 +55,6 @@
for _name, _byte in _OPS.items():
_OPS_BY_BYTE.setdefault(_byte, _name) # first name wins (e.g. KECCAK256 over SHA3)

_FAUCETS: dict[str, str] = json.loads(os.environ.get("ETHPANDAOPS_EVM_FAUCET_NETWORKS", "{}"))


def _require_available() -> None:
if not os.environ.get("ETHPANDAOPS_EVM_AVAILABLE", "").strip():
raise ValueError("EVM module is not enabled; ethnode datasource is required.")
Expand Down Expand Up @@ -366,16 +363,18 @@ def wallet(private_key: str | None = None) -> dict[str, str]:
# ---------------------------------------------------------------------------

def faucet(network: str, address: str) -> str:
"""Return the PoW faucet URL for the network pre-filled with the given address.
"""Mine the network's PoW faucet and claim test ETH to address.

Open this URL in a browser, complete the PoW challenge, and the address
receives test ETH. Source: https://github.com/pk910/PoWFaucet
Runs the full agent proof-of-work flow server-side — no browser, WebSocket,
or captcha — and returns the claim transaction hash once it confirms.
Requires panda auth (run 'panda auth login'). Source:
https://github.com/pk910/PoWFaucet
"""
url = _FAUCETS.get(network)
if not url:
available = list(_FAUCETS.keys())
raise ValueError(
f"No faucet known for network {network!r}. Available: {available or 'none'}"
)
sep = "&" if "?" in url else "?"
return f"{url}{sep}address={address}"
_require_available()
result = _runtime.invoke_json(
"evm.faucet",
{"network": network, "address": address},
)
if not isinstance(result, dict) or not result.get("claim_hash"):
raise ValueError(f"faucet claim did not return a tx hash: {result!r}")
return result["claim_hash"]
Loading
Loading