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: 2 additions & 0 deletions packages/battlechain-lib-py/battlechain/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
)
from battlechain.safe_harbor import (
adopt_agreement,
approve_attack_request,
create_agreement,
create_and_adopt_agreement,
request_attack_mode,
Expand Down Expand Up @@ -168,6 +169,7 @@
"track_deployment",
# safe harbor
"adopt_agreement",
"approve_attack_request",
"create_agreement",
"create_and_adopt_agreement",
"request_attack_mode",
Expand Down
10 changes: 10 additions & 0 deletions packages/battlechain-lib-py/battlechain/_boa.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
AGREEMENT_FACTORY_ABI,
ATTACK_REGISTRY_ABI,
DEPLOYER_ABI,
MOCK_REGISTRY_MODERATOR_ABI,
REGISTRY_ABI,
)

Expand Down Expand Up @@ -77,3 +78,12 @@ def deployer(address: str | None = None) -> Any:
"""
addr = address or config.deployer_address(chain_id())
return _load(DEPLOYER_ABI, addr, name="BCDeployer")


def mock_registry_moderator(address: str | None = None) -> Any:
"""Load the permissionless MockRegistryModerator for the active chain.

Only deployed on BattleChain testnet (self-approval of attack requests).
"""
addr = address or config.mock_registry_moderator_address(chain_id())
return _load(MOCK_REGISTRY_MODERATOR_ABI, addr, name="MockRegistryModerator")
12 changes: 12 additions & 0 deletions packages/battlechain-lib-py/battlechain/abi.py
Original file line number Diff line number Diff line change
Expand Up @@ -1048,11 +1048,23 @@
},
]

# Source: abis/registryModerator.json
MOCK_REGISTRY_MODERATOR_ABI: list[dict[str, Any]] = [
{
"type": "function",
"name": "approveAttack",
"inputs": [{"name": "agreementAddress", "type": "address", "internalType": "address"}],
"outputs": [],
"stateMutability": "nonpayable",
}
]


__all__ = [
"AGREEMENT_FACTORY_ABI",
"AGREEMENT_ABI",
"ATTACK_REGISTRY_ABI",
"REGISTRY_ABI",
"DEPLOYER_ABI",
"MOCK_REGISTRY_MODERATOR_ABI",
]
14 changes: 14 additions & 0 deletions packages/battlechain-lib-py/battlechain/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,19 @@ def attack_registry_address(chain_id: int) -> str:
return get_network_config(chain_id).attack_registry


def mock_registry_moderator_address(chain_id: int) -> str | None:
"""Return the permissionless MockRegistryModerator address for a chain.

Only deployed on BattleChain testnet, where it lets an adopter self-approve
a pending attack request (ATTACK_REQUESTED -> UNDER_ATTACK). Returns None on
any other chain — on mainnet, approval is a real DAO governance action.
Mirrors JS `mockRegistryModeratorAddress`.
"""
if chain_id == TESTNET_CHAIN_ID:
return TESTNET_MOCK_REGISTRY_MODERATOR
return None


def deployer_address(chain_id: int) -> str:
"""Return the deployer address for a chain.

Expand Down Expand Up @@ -332,6 +345,7 @@ def explorer_api(chain_id: int) -> str:
"explorer_host",
"get_network_config",
"is_battlechain",
"mock_registry_moderator_address",
"registry_address",
"safe_harbor_uri",
"set_overrides",
Expand Down
33 changes: 6 additions & 27 deletions packages/battlechain-lib-py/battlechain/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,33 +98,12 @@ def is_attackable(contract_address: str) -> bool:
# These work only for *top-level* contracts (those deployed via
# BattleChainDeployer). For child contracts, prefer `is_attackable` above.

_ATTACK_REGISTRY_QUERY_METHODS: list[dict[str, Any]] = [
{
"type": "function",
"name": "getAgreementState",
"stateMutability": "view",
"inputs": [{"name": "agreementAddress", "type": "address"}],
"outputs": [{"name": "", "type": "uint8"}],
},
{
"type": "function",
"name": "getAgreementForContract",
"stateMutability": "view",
"inputs": [{"name": "contractAddress", "type": "address"}],
"outputs": [{"name": "", "type": "address"}],
},
{
"type": "function",
"name": "isTopLevelContractUnderAttack",
"stateMutability": "view",
"inputs": [{"name": "contractAddress", "type": "address"}],
"outputs": [{"name": "", "type": "bool"}],
},
]

ATTACK_REGISTRY_QUERY_ABI: list[dict[str, Any]] = (
ATTACK_REGISTRY_ABI + _ATTACK_REGISTRY_QUERY_METHODS
)
# The generated ATTACK_REGISTRY_ABI already includes the read methods
# (getAgreementState, getAgreementForContract, isTopLevelContractUnderAttack), so
# this is just an alias. It used to concatenate hand-written copies of those three
# methods, which — once the ABI was regenerated to include them — produced duplicate
# entries and made boa raise "Ambiguous call to getAgreementState".
ATTACK_REGISTRY_QUERY_ABI: list[dict[str, Any]] = ATTACK_REGISTRY_ABI


def _query_registry() -> Any:
Expand Down
23 changes: 22 additions & 1 deletion packages/battlechain-lib-py/battlechain/safe_harbor.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

from battlechain import _boa, config
from battlechain.builders import DEFAULT_COMMITMENT_DAYS
from battlechain.errors import NotBattleChainError
from battlechain.errors import BattleChainError, NotBattleChainError
from battlechain.types import AgreementDetails

SECONDS_PER_DAY = 24 * 60 * 60
Expand Down Expand Up @@ -77,6 +77,26 @@ def request_attack_mode(agreement_address: str) -> None:
_boa.attack_registry().requestUnderAttack(agreement_address)


def approve_attack_request(agreement_address: str) -> None:
"""Self-approve a pending attack request via the testnet MockRegistryModerator.

Moves the agreement from ATTACK_REQUESTED (2) to UNDER_ATTACK (3). Mirrors JS
`approveAttackRequest` and the `cast send <moderator> "approveAttack(address)"`
flow from the BattleChain testnet docs. Only available on testnet, where the
moderator is permissionless; on mainnet, approval is a real DAO governance
action, so this raises.
"""
chain_id = _boa.chain_id()
moderator = config.mock_registry_moderator_address(chain_id)
if moderator is None:
raise BattleChainError(
f"approve_attack_request is only available on BattleChain testnet "
f"(chain ID {config.TESTNET_CHAIN_ID}); got chain ID {chain_id}. "
f"On mainnet, approval is a real DAO governance action."
)
_boa.mock_registry_moderator(moderator).approveAttack(agreement_address)


def skip_to_production(agreement_address: str) -> None:
"""Skip an agreement directly to production. Only available on BattleChain.

Expand All @@ -90,6 +110,7 @@ def skip_to_production(agreement_address: str) -> None:

__all__ = [
"adopt_agreement",
"approve_attack_request",
"create_agreement",
"create_and_adopt_agreement",
"request_attack_mode",
Expand Down
38 changes: 35 additions & 3 deletions packages/battlechain-lib-py/battlechain/verify.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,37 @@ def _build_vyper_payload(contract_file: str, source_code: str) -> dict[str, Any]
}


_VYPER_RELEASES_URL = "https://vyper-releases-mirror.hardhat.org/list.json"


def _resolve_vyper_version(version: str) -> str:
"""Resolve a vyper version to the full ``X.Y.Z+commit.<hash>`` form.

The explorer's verifier (Sourcify) fetches the compiler binary by version and
needs the full version *with* commit hash and *no* leading "v" — a bare
"0.4.3" 404s ("Failed fetching vyper 0.4.3 for platform linux"). `vyper`
doesn't expose its commit hash at runtime, so derive it from the same release
list the explorer UI uses. Already-full versions pass through (sans leading "v");
on any lookup failure we fall back to the bare version rather than block.
"""
v = version.lstrip("v")
if "+commit." in v:
return v
try:
with urllib.request.urlopen(_VYPER_RELEASES_URL, timeout=15) as resp:
releases = json.loads(resp.read())
for release in releases:
for asset in release.get("assets", []):
name = asset.get("name", "") # e.g. "vyper.0.4.3+commit.bff19ea2.linux"
if name.startswith("vyper.") and name.endswith(".linux"):
full = name[len("vyper.") : -len(".linux")]
if full.split("+commit.")[0] == v:
return full
except Exception as exc: # noqa: BLE001 - network/parse failure is non-fatal
print(f" ⚠ Could not resolve full vyper version for {v}: {exc}")
return v


def verify_contract(
address: str,
contract_fqn: str,
Expand All @@ -123,8 +154,9 @@ def verify_contract(
Args:
address: Deployed contract address.
contract_fqn: File path and contract name, e.g. "src/MockToken.vy:MockToken".
compiler_version: Compiler version string (e.g. "0.4.0" — the leading
"v" is added automatically).
compiler_version: Compiler version string (e.g. "0.4.3"). Resolved to the
full "X.Y.Z+commit.<hash>" form the verifier needs; full versions and
a leading "v" are also accepted.
source_path: Override path to the source file. Defaults to the path
component of `contract_fqn`.
chain_id: Override chain ID. Defaults to the active boa environment.
Expand Down Expand Up @@ -165,7 +197,7 @@ def verify_contract(
"sourceCode": json.dumps(std_json),
"codeformat": code_format,
"contractname": contract_fqn,
"compilerversion": f"v{compiler_version}",
"compilerversion": _resolve_vyper_version(compiler_version),
})
except urllib.error.URLError as exc:
print(f" ✗ Verification submission failed: {exc}")
Expand Down
1 change: 1 addition & 0 deletions packages/battlechain-lib-py/tools/gen_abi.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"attackRegistry": "ATTACK_REGISTRY_ABI",
"registry": "REGISTRY_ABI",
"deployer": "DEPLOYER_ABI",
"registryModerator": "MOCK_REGISTRY_MODERATOR_ABI",
}

# Package root is two levels up from this file (tools/ -> battlechain-lib-py/).
Expand Down