Skip to content

Migrating from Wake 4.x#

In version 5.0.0, a new testing EVM execution engine based on revm was introduced and made the default. This brings several breaking changes as well as new features.

Many of the breaking changes apply to the new revm engine only; those sections say so explicitly. Regardless of the engine, pytypes generated by Wake 4.x do not load under Wake 5 and have to be regenerated — see pytypes must be regenerated.

Breaking changes#

Execution engine#

Default execution engine changed to revm#

testing.cmd now defaults to revm, so upgrading changes the execution engine even when wake.toml is left untouched. The revm-based engine is built into Wake and runs in-process, meaning there is nothing to install and no development chain process is launched.

Because it runs in-process, this engine exposes no JSON-RPC endpoint. As a result:

  • Chain.connect accepts no URI of a running node; its accounts, chain_id, fork and hardfork arguments are keyword-only
  • External tools cannot attach to the testing chain over JSON-RPC

Forking is still supported through chain.connect(fork=...).

Projects that need a JSON-RPC endpoint, or Chain.connect with a URI of an already running node, can opt out in wake.toml:

[testing]
cmd = "anvil"

Chain history is bounded to the last 256 blocks#

revm chains keep only the newest testing.block_history blocks (default 256) together with the transactions in them. Wake 4 kept every block and transaction for the whole run.

Reaching further back raises HistoryPrunedError, which is not part of from wake.testing import *:

from wake_rs import HistoryPrunedError
HistoryPrunedError: transaction 0 is no longer retained: transactions 131..401 are available.
Raise `testing.block_history` in the configuration (or set it to null to disable pruning) to keep more.

This affects indexing into both chain.txs and chain.blocks. chain.txs[-1] is not safe either: it raises once the newest transaction's own block has aged out of the window, which happens when blocks keep being mined without new transactions.

Keep a reference to the transaction objects you need later:

# Wake 4 - every transaction of the run stays reachable
@chain.connect()
def test_many_txs():
    for _ in range(1000):
        counter.increment()

    first_tx = chain.txs[0]
# Wake 5 - keep the object
@chain.connect()
def test_many_txs():
    first_tx = counter.increment()
    for _ in range(999):
        counter.increment()

Holding the object keeps the metadata (from_, to, status, gas_used, events, return_value) available indefinitely. call_trace and console_logs re-execute the transaction, so they raise once its block is gone:

HistoryPrunedError: block 1 is no longer retained for replay because its block context was pruned:
the retained window starts at block 132. Raise `testing.block_history` in the configuration
(or set it to null to disable pruning) to keep more.

Raise the window, or set block_history = null to restore Wake 4 behaviour at the cost of memory linear in the number of transactions executed:

[testing]
block_history = null

The option applies to revm only. See configuration for the full description.

Dropped development chain support#

Ganache is no longer supported. The testing.ganache configuration namespace was removed and should be deleted from wake.toml, as options left in it are reported as unsupported and ignored.

Anvil (cmd = "anvil") and Hardhat (cmd = "hardhat") are still supported over JSON-RPC. Hardhat's development chain support is considered deprecated.

Chain and transaction API#

Chain members removed under revm#

The revm chain is a different class (wake_rs.Chain) than the JSON-RPC chain, not a subclass of it, so every member below is simply absent. Accessing one raises AttributeError: 'builtins.Chain' object has no attribute 'gas_price'.

Wake 4 Chain member Replacement under revm
gas_price (get/set) pass gas_price= on the individual call
max_priority_fee_per_gas (get/set) none
default_tx_confirmations (get/set) none, execution is synchronous
require_signed_txs (get/set) none, transactions are executed directly and never signed
client_version none
reset() snapshot() right after connect(), then revert() to that id
set_min_gas_price() none
set_next_block_base_fee_per_gas() none, revm exposes no base fee
mine_many(num_blocks, timestamp_change) for _ in range(num_blocks): chain.mine(lambda t: t + timestamp_change)
update_accounts() none needed, the account set is fixed at connect()
connect(min_gas_price=...) none
connect(block_base_fee_per_gas=...) none

All of these are still present when cmd = "anvil" (or cmd = "hardhat") is selected in wake.toml:

[testing]
cmd = "anvil"

Chain members removed on every engine#

Wake 4 Chain member Replacement
default_tx_type (get/set) none, the type is derived from the arguments passed to the call
optimistic_pytypes_resolving Account.pytypes_resolver, see Chain.optimistic_pytypes_resolving removed

Unlike the table above, these are gone for anvil and hardhat too.

Removed type kwarg from functions interacting with contracts#

Transaction type can no longer be specified through the type keyword argument. Instead, the transaction type is derived automatically from the passed arguments.

Transaction arguments not implemented under revm#

Four argument values are still accepted by the signatures but abort with pyo3_runtime.PanicException: not yet implemented:

  • gas_limit="auto"
  • max_fee_per_gas=...
  • max_priority_fee_per_gas=...
  • access_list="auto"

Resolve them explicitly instead:

# instead of gas_limit="auto"
gas = counter.increment(request_type="estimate")
counter.increment(gas_limit=gas)

# instead of access_list="auto"
access_list, _ = counter.increment(request_type="access_list")
counter.increment(access_list=access_list)

gas_limit="max", an integer gas_limit, an explicit access_list mapping, and gas_price all work. request_type="estimate" and request_type="access_list" requests are themselves unaffected.

Transaction attributes removed under revm#

Nothing describing the signed transaction envelope survives.

Wake 4 attribute Replacement under revm
value none
nonce none
type none
gas_limit none
gas_price none
max_fee_per_gas none
max_priority_fee_per_gas none
effective_gas_price none
access_list none
chain_id tx.chain.chain_id
r, s, v, y_parity none
wait() drop the call, execution is synchronous

tx_hash, tx_index, chain, block, block_number, data, from_, to, gas_used, cumulative_gas_used, status, return_value, raw_return_value, events, raw_events, error, raw_error, console_logs and call_trace are unchanged. The type of tx.status was renamed, see Renamed symbols.

tx.events is now a tuple instead of a list, so in-place list operations on it fail:

# before
tx.events.append(x)
# after
events = list(tx.events)

The per-type subclasses are never produced either — every transaction is a bare TransactionAbc, and it is not the TransactionAbc exported from wake.testing (that one is the JSON-RPC implementation). Type checks silently stop matching:

from wake.testing import LegacyTransaction, TransactionAbc

isinstance(tx, TransactionAbc)      # False under revm
isinstance(tx, LegacyTransaction)   # False under revm

chain.blocks and chain.txs are plain sequences under revm#

chain.blocks implements only __getitem__. It is not sized and not properly iterable:

  • len(chain.blocks) raises TypeError: object of type 'builtins.Blocks' has no len()
  • for block in chain.blocks falls back to integer indexing and dies on the block after the latest one with ValueError: Block number out of range instead of stopping
  • an out-of-range number raises ValueError: Block number out of range, not KeyError

Iterate over an explicit range instead. chain.blocks.first_number is the oldest locally mined block still retained.

for number in range(chain.blocks.first_number, chain.blocks["latest"].number + 1):
    block = chain.blocks[number]

chain.txs can no longer be indexed by transaction hash — only by integer index, where chain.txs[hash] raises TypeError: 'str' object cannot be interpreted as an integer. Keep a reference to the transaction object instead of looking it up by hash. len(chain.txs), chain.txs[-1] and for tx in chain.txs all work, within the bounds of history pruning.

Block attributes removed under revm#

revm builds no block header, so only chain, number, timestamp, hash and gas_used remain.

Wake 4 Block attribute Replacement under revm
txs filter chain.txs on tx.block_number
gas_limit chain.block_gas_limit
miner chain.coinbase
base_fee_per_gas none
parent_hash none
nonce none
difficulty none
total_difficulty none
mix_hash none
extra_data none
logs_bloom none
size none
state_root none
transactions_root none
receipts_root none
sha3_uncles none
uncles none
seal_fields none

confirmations has no effect under revm#

confirmations is still accepted by transaction calls, but nothing waits on it — transactions are executed synchronously and the returned object is already mined. confirmations=0 no longer means "do not wait", and tx.status is never ExecutionStatusEnum.PENDING, so polling loops on it never spin.

from wake.development.internal import ExecutionStatusEnum

while tx.status == ExecutionStatusEnum.PENDING:  # dead code under revm
    ...

chain.chain_interface exposes only four members under revm#

There is no JSON-RPC connection behind it. The revm chain interface has type(), get_storage_at, set_storage_at and get_code — everything else on Wake 4's ChainInterfaceAbc (get_block, get_transaction, debug_trace_transaction, send_transaction, get_logs, connection_uri, …) is gone with no replacement.

type also changed from a property to a method, which fails silently rather than raising:

chain.chain_interface.type == "anvil"    # always False under revm
chain.chain_interface.type() == "revm"   # True

Errors and reverts#

Renamed symbols#

  • The default_chain alias no longer exists, use chain instead
  • TransactionRevertedError was renamed to RevertError
  • UnknownTransactionRevertedError was renamed to UnknownRevertError
  • TransactionStatusEnum was renamed to ExecutionStatusEnum and moved to wake.development.internal; wake.development.transactions re-exports it, so only the name has to change in from wake.development.transactions import ...
  • Error types and revert helpers moved from wake.development.transactions to wake.development.errors: RevertError, UnknownRevertError, Error, Panic, PanicCodeEnum, Halt, must_revert, may_revert and on_revert. from wake.testing import * is unaffected; explicit imports fail with e.g. ImportError: cannot import name 'must_revert' from 'wake.development.transactions'

may_revert and must_revert accept variadic arguments#

Both context managers now take the expected errors as separate positional arguments instead of a single tuple or list. Unpack the sequence:

# Wake 4
with must_revert([Counter.NotOwner, Panic(PanicCodeEnum.UNDERFLOW_OVERFLOW)]) as e:
    ...

# Wake 5
with must_revert(Counter.NotOwner, Panic(PanicCodeEnum.UNDERFLOW_OVERFLOW)) as e:
    ...

The tuple/list form is not detected and reported - the sequence is treated as a single expected error, so except receives list/tuple instead of an exception type. must_revert([...]) always fails with TypeError: catching classes that do not inherit from BaseException is not allowed, whether the body reverts or not. may_revert([...]) raises the same TypeError when the body reverts, and silently accepts the body when it does not - a stale may_revert list keeps passing while checking nothing.

Calls with no argument or a single argument are unaffected: must_revert(), must_revert(Counter.NotOwner), must_revert(Counter.NotOwner(addr)), must_revert("some error") and must_revert(PanicCodeEnum.UNDERFLOW_OVERFLOW) all behave as before.

Halt is no longer a revert error#

Halt - raised on exceptional EVM halts such as out of gas or an invalid opcode - used to be a subclass of TransactionRevertedError. It now derives directly from Exception and is unrelated to RevertError. As a result none of the following catch a halt any more:

  • except RevertError
  • with must_revert() / with may_revert() and no arguments
  • must_revert(SomeError) and any other revert type or instance
  • on_revert handlers - the callback is not invoked at all

Instead, the Halt propagates out and the test fails with e.g. Halt(message='OutOfGas(Basic)'). Name Halt explicitly to catch it:

from wake.testing import *

# only halts
with must_revert(Halt) as e:
    ...

# a revert or a halt
with must_revert(RevertError, Halt) as e:
    ...

Halt is exported from wake.testing and wake.deployment, so try/except Halt works too. message holds the halt reason as reported by the execution engine; with revm it is the revm reason, e.g. OutOfGas(Basic) or OpcodeNotFound.

pytypes#

pytypes must be regenerated#

Generated pytypes carry the version of Wake that produced them and refuse to load under any other version. Importing pytypes generated by Wake 4.x fails immediately in pytypes/__init__.py:

RuntimeError: Pytypes generated for a different version of wake. Please regenerate.

Regenerate them:

wake up pytypes

The version comparison is exact, so every Wake upgrade — including release candidates — requires regenerating.

Unnamed parameters renamed#

Unnamed Solidity parameters are generated as param0, param1, ... in all of pytypes. Wake 4 named unnamed function parameters arg1, arg2, ... and unnamed event and error parameters param1, param2, ..., so both the prefix and the index base changed for functions and the index base changed for events and errors. Keyword arguments and attribute access on generated events and errors break accordingly.

The counter only counts unnamed parameters, not positions, so in f(uint256 a, uint256) the second parameter is param0.

contract C {
    event Transfer(address indexed, uint256);
    error Failed(bytes);

    function set(uint256, bool) external { }
}
# Wake 4
c.set(arg1=1, arg2=True)
e.param1  # address
e.param2  # uint256
C.Failed(param1=b"")

# Wake 5
c.set(param0=1, param1=True)
e.param0  # address
e.param1  # uint256
C.Failed(param0=b"")

bytearray replaced with bytes#

All pytypes functions and dataclass attributes that represent Solidity bytes are now represented by Python bytes instead of bytearray. This affects:

  • Low-level Account.call function
  • Return value of Account.transact
  • random_bytes function
  • Other similar API functions

Solidity function values decoded into FunctionPointer#

Values of Solidity function type returned from contracts, or held in event, error and struct members, are now FunctionPointer instances. FunctionPointer is a bytes24 subclass over the 20-byte address followed by the 4-byte selector, exported from wake.testing, exposing address and selector properties.

Wake 4 decoded such values into a bound method of a Contract instance, which required the target contract to be present in pytypes and raised ValueError: Unknown contract: <fqn> when it was not.

# Wake 4 - bound method, directly callable
handler = c.getHandler()
handler(1)

# Wake 5 - FunctionPointer
handler = c.getHandler()
handler.address   # Address
handler.selector  # bytes4
Target(handler.address).callback(1)

Encoding is unchanged for existing code. The generated return type changed from Callable to FunctionPointer, while function-type parameters became Union[Callable, FunctionPointer], so a bound method of a Contract instance is still accepted where a Solidity function argument is expected. FunctionPointer.from_parts(address, selector) builds one, where address is an Account, Address, int or str.

Chain.optimistic_pytypes_resolving removed#

The chain-wide flag that forced an ambiguous event or error selector to resolve to the first available definition (contract fully qualified names sorted alphabetically) no longer exists. On the default revm chain, assigning it raises:

AttributeError: 'builtins.Chain' object has no attribute 'optimistic_pytypes_resolving'

On an anvil or hardhat chain the assignment is silently accepted and has no effect.

Remove the assignment and set Account.pytypes_resolver instead. It names the contract explicitly, per account, rather than picking a definition for the whole chain.

# Wake 4
chain.optimistic_pytypes_resolving = True

# Wake 5
Account("0xdAC17F958D2ee523a2206206994597C13D831ec7").pytypes_resolver = IUSDT

Newly reserved pytypes names#

Struct and FunctionPointer (anywhere), _fqn (contract members) and authorization_list (function parameters) are now reserved in generated pytypes. A Solidity declaration whose name collides with a reserved name gets an underscore appended, so a contract named Struct is generated as Struct_. RevertError replaced TransactionRevertedError in the reserved set.

Types and helpers#

Wei constructors and unit conversions reworked#

Wei.from_ether, Wei.from_gwei and Wei.from_str were removed. Wei now accepts an optional keyword-only unit argument, and wake.testing and wake.deployment export module-level ether() and gwei() helpers.

Wake 4 Wake 5
Wei.from_ether(1) ether(1), Wei.ether(1) or Wei(1, unit="ether")
Wei.from_ether(0.1) ether("0.1") or ether(Decimal("0.1"))
Wei.from_gwei(20) gwei(20), Wei.gwei(20) or Wei(20, unit="gwei")
Wei.from_str("1 ether") Wei("1 ether") or Wei.parse("1 ether")

unit (also available as Wei.from_unit(value, unit)) accepts unit names case-insensitively: wei, kwei, mwei, gwei, twei, pwei, ether and their aliases (szabo, finney, shannon, eth, ...). An unknown name raises ValueError: "..." is not a valid unit.

Wei no longer accepts float. The constructor raises TypeError: Invalid value type: <class 'float'>, where Wake 4 silently truncated (Wei(1.5) was 1). The same applies to Wei(...) + 0.5, because Wei.__add__ converts the other operand through Wei. Pass a str or Decimal together with a unit instead:

# Wake 4
value = Wei.from_ether(0.1)

# Wake 5
value = ether("0.1")

A Decimal without a unit raises ValueError: unit must be specified if value is a Decimal, and a value that is not a whole number of wei raises ValueError: Value multiplied by unit is not an integer.

to_ether() and to_gwei() return Decimal instead of float (to(unit) does too). Mixing the result with a float raises TypeError: unsupported operand type(s) for *: 'decimal.Decimal' and 'float', so wrap it in float(...) or keep the arithmetic in Decimal:

# Wake 4
assert acc.balance.to_ether() * 0.9 > 1

# Wake 5
assert float(acc.balance.to_ether()) * 0.9 > 1

Wei prints with units#

str() and f-string interpolation of a Wei now render a human-readable form instead of the plain integer:

str(Wei(10**18))   # Wake 4: 1000000000000000000
                   # Wake 5: 1 ether (1000000000000000000 wei)
repr(Wei(10**18))  # Wake 5: Wei(1000000000000000000 wei)
str(Wei(1500))     # Wake 5: 1500 wei

Account.balance and every other API returning Wei are affected, so log lines and anything assembled from f"{balance}" change. Use int(value) or the d format spec (f"{value:d}") where the plain number is needed. json.dumps is unaffected.

Address compares only to another Address#

Address is now implemented in Rust and compares only against Address instances:

  • equality with a str returns False, where Wake 4 compared the addresses case-insensitively and returned True
  • equality with an Account returns False, where Wake 4 raised TypeError: Cannot compare Address and Account. Use Account.address instead (Account == Address likewise returns False instead of raising)
  • ordering (<, <=, >, >=) against a str raises TypeError: '<' not supported between instances of 'builtins.Address' and 'str', where Wake 4 compared numerically

Only the ordering case raises, so assertions comparing an address to a string now fail instead of passing, with no hint about why:

# Wake 4
assert token.owner() == "0xdAC17F958D2ee523a2206206994597C13D831ec7"

# Wake 5
assert token.owner() == Address("0xdAC17F958D2ee523a2206206994597C13D831ec7")

hash(Address) is no longer the hash of the lowercase address string either, so string keys no longer match dict and set entries keyed by Address.

str(Address) returns the EIP-55 checksummed address#

Wake 4 kept the string passed to the constructor as-is (and lowercase hex when constructed from an int). Wake 5 always formats the checksummed address:

str(Address("0xdac17f958d2ee523a2206206994597c13d831ec7"))
# Wake 4: 0xdac17f958d2ee523a2206206994597c13d831ec7
# Wake 5: 0xdAC17F958D2ee523a2206206994597C13D831ec7

Comparisons of str(address) against lowercase literals, and dictionaries keyed by str(address), break. Compare Address objects, or lowercase both sides explicitly.

The constructor also no longer accepts an 0X-prefixed string; it raises ValueError: invalid string length. Use 0x or no prefix.

CLI and tooling#

Python 3.10 or newer required#

requires-python is now >=3.10; Wake 4.x supported Python 3.8 and 3.9. pip and uv refuse to install Wake 5 on older interpreters, so CI images and virtual environments pinned to 3.8 or 3.9 must be upgraded. Wake 5 is tested on Python 3.10 through 3.14.

Configured accounts must be re-imported#

All accounts configured through wake accounts CLI commands must be manually deleted and imported again:

  1. Remove existing accounts: wake accounts remove <alias>
  2. Import accounts again: wake accounts import <alias>

Sake removed from the language server#

Sake, the LSP-hosted Deploy & Interact backend behind the VS Code extension panel, was removed. wake/lsp/sake.py is deleted and all wake/sake/* requests (wake/sake/ping, wake/sake/createChain, wake/sake/deploy, wake/sake/transact, wake/sake/call and the rest) are gone from the LSP method enum. Everything else wake lsp serves — diagnostics, detectors, code lens, navigation — is unaffected.

Update Tools for Solidity together with Wake. An extension version that still probes Sake does more than lose its panel: because request methods are validated against the method enum, a wake/sake/* request makes the server drop that client connection with no JSON-RPC error response, so the editor loses language server features until it reconnects. The wake lsp process itself stays up and continues to accept new connections.

Coverage export format changed#

wake test --coverage still writes wake-coverage.cov, but the file is now version 2.1 and statement-based. Version 1.0 files listed one record per function, with modifier and branch records nested inside it:

{"version": "1.0", "data": {"/project/contracts/Counter.sol": [
  {"name": "increment", "startLine": 8, "startColumn": 4, "endLine": 10, "endColumn": 5,
   "coverageHits": 3, "modRecords": [], "branchRecords": []}
]}}

Version 2.1 reports statements per file, split into covered and uncovered entries, and renames coverageHits to count:

{"version": "2.1", "data": {"/project/contracts/Counter.sol": {
  "declarations": {},
  "statements": {"total": 12,
    "covered": [{"startLine": 9, "startColumn": 8, "endLine": 9, "endColumn": 20, "count": 3}],
    "uncovered": [{"startLine": 14, "startColumn": 8, "endLine": 14, "endColumn": 20, "count": 0}]}
}}}

Two consequences beyond the schema: uncovered statements are now listed explicitly, and function, modifier and branch coverage is no longer exported — declarations is always empty.

Update the Tools for Solidity extension before running Tools for Solidity: Show Coverage; an extension that only understands version 1.0 cannot read the new file. Custom scripts that parse wake-coverage.cov must be rewritten for the new schema.

wake test --shrank renamed to --shrunk#

The option that replays a shrunk fuzz sequence is now --shrunk. The -SR short form and the --reproduce alias are unchanged.

# Wake 4
wake test --shrank 0

# Wake 5
wake test --shrunk 0

wake test forwards unknown options to pytest, so the old spelling is not rejected by Wake itself — pytest fails the run instead:

ERROR: usage: wake [options] [file_or_dir] [file_or_dir] [...]
wake: error: unrecognized arguments: --shrank

The output directory moved from .wake/logs/shrank to .wake/logs/shrunk. Only the new directory is searched when the option is given an index, so a project that still has the Wake 4 directory reports:

ERROR: Crash logs directory not found: <project>/.wake/logs/shrunk

The JSON format of the files is unchanged. To use a file written by Wake 4, move it into .wake/logs/shrunk or pass its path instead of an index. The crash log directory used by --shrink (.wake/logs/crashes) is unchanged.

eth-abi and eth-account no longer installed#

Both packages were dropped from Wake's dependencies and are no longer imported anywhere in Wake. Test scripts and detectors that imported eth_abi or eth_account while relying on Wake to pull them in now fail with ModuleNotFoundError. Declare them yourself:

pip3 install eth-abi eth-account

eth-utils is still a Wake dependency, so eth_utils imports keep working.

New features#

Support for EIP-7702 transactions#

A new function was implemented to sign EIP-7702 authorizations:

Account.sign_authorization(
    address: Account | Address | int | str,
    chain_id: int | None = None,
    nonce: int | None = None
) -> SignedAuthorization

All contract interaction functions now support an authorization_list: Optional[List[SignedAuthorization]] keyword argument accepting an optional list of such signed authorizations.

Call object#

Call describes a single non-transaction request (call, estimate or access_list) together with its result, so a request no longer has to be repeated to inspect it from a different angle. It is exported from both wake.testing and wake.deployment and offers:

  • chain, block, data, from_, to, status
  • return_value and raw_return_value
  • error and raw_error
  • call_trace
  • estimated_gas and access_list, regardless of the request type used

Values the request did not already produce are computed on first access and cached.

Pass return_call=True to get one instead of the plain return value:

call = token.balanceOf(alice, request_type="call", return_call=True)
print(call.return_value)
print(call.estimated_gas)
print(call.call_trace)

return_call is accepted by pytypes-generated functions and deploy, and by the low-level Account.call, Account.estimate, Account.access_list and Chain.deploy. It cannot be combined with request_type="tx" - doing so raises ValueError: return_call cannot be specified for tx requests. Use the transaction object returned by return_tx=True instead.

Revert errors now also point back at their origin: RevertError (and Halt) carry a call attribute alongside tx, and RevertError.call_trace resolves through whichever of the two is set.

Request access list or gas estimate for reverting transactions#

access_list and estimate requests can now be made for transactions that revert. Pytypes-generated functions (including deploy) accept a revert keyword argument, set to True by default; setting it to False returns the access list or the estimate instead of raising the revert error.

# Wake 4.x - both raise the revert error
gas = token.transfer(recipient, amount, request_type="estimate")
access_list, gas = token.transfer(recipient, amount, request_type="access_list")

# Wake 5
gas = token.transfer(recipient, amount, request_type="estimate", revert=False)
access_list, gas = token.transfer(
    recipient, amount, request_type="access_list", revert=False
)

The low-level API spells the same option revert_on_failure:

access_list, gas = account.access_list(data=b"...", revert_on_failure=False)
gas = account.estimate(data=b"...", revert_on_failure=False)

Two limitations:

  • The option applies to access_list and estimate requests only. Passing revert=False with any other request type raises ValueError: revert may only be changed for access list and estimate requests.
  • It is supported by the revm engine only. On anvil and hardhat, pytypes-generated functions raise NotImplementedError: revert is not supported with non-revm chains and the low-level API raises NotImplementedError: revert_on_failure is not supported with non-revm chains.

Combine revert=False with return_call=True to get a Call whose error is populated while estimated_gas and access_list remain available.

ExternalEvent and ExternalError API#

Events and errors originating in forked contracts are now resolved into specialized class instances when possible (i.e., when ABI is available either on Sourcify or Etherscan with an API key set).

Both engines implement this - revm and the JSON-RPC anvil/hardhat chains. A forked chain is required in either case, because the ABI is looked up by address on the forked chain.

ExternalEvent class:

  • Provides _event_full_name attribute with the canonical name of the event, and _event_name with the event name alone
  • Other attributes named according to the event parameters in ABI

ExternalError class:

  • Provides _error_full_name attribute with the canonical name of the error, and _error_name with the error name alone
  • Other attributes named according to the error parameters in ABI
  • Subclasses RevertError, so it can be caught with may_revert/must_revert and carries tx, call and call_trace

Unnamed ABI parameters are exposed as param0, param1, and so on.

pytypes_resolver for enforcing event and error resolution#

It is possible to override the resolution of events and errors for any Account using pytypes_resolver.

usdt = Account("0xdAC17F958D2ee523a2206206994597C13D831ec7")
usdt.pytypes_resolver = IUSDT

where IUSDT is a Solidity interface generated into pytypes. The attribute can also be read back, and assigning None clears the override.

This feature can be used to enforce user-friendly resolution of "external contracts" (e.g. forked contracts, low-level deployed contracts with init code) and in cases when implicit resolution doesn't work correctly.

pytypes_resolver must always be assigned to the Account that performs the event emit (LOGn instruction) or performs the revert. In a proxy-implementation setup, pytypes_resolver must be set on the implementation contract to correctly resolve events and errors originating from the code of the implementation contract. It may be set for the proxy as well if it defines its own events or errors.

EIP-712 encoded data for all structs#

All structs generated in pytypes now derive from Struct and offer four helper methods for easier debugging of signatures provided by Account.sign_structured:

  • .encode_eip712_type() returning the EIP-712 encoded type of the struct as a string
  • .encode_eip712_data() returning the EIP-712 encoded data of the struct as bytes
  • .get_eip712_struct_hash() returning keccak256 of the encoded type hash concatenated with the encoded data, as bytes32
  • .get_eip712_signing_hash(domain) returning the full EIP-712 signing hash for the given Eip712Domain, as bytes32

The last two are also available as functions accepting any struct, exported from wake.testing and wake.deployment:

from wake.testing import get_eip712_signing_hash, get_eip712_struct_hash

struct_hash = get_eip712_struct_hash(permit)
signing_hash = get_eip712_signing_hash(permit, domain)

CallTrace.save#

CallTrace.save() renders a call trace into an SVG file instead of the terminal, which keeps colors and full width for traces too wide or too deep to read in a terminal. The file is written to .wake/call_traces/ under the project root and named after the current local time (%Y-%m-%dT%H-%M-%S.svg).

try:
    token.transfer(recipient, amount)
except RevertError as e:
    e.call_trace.save()

Support for Trezor hardware wallets#

An Account instance may be created using a new function:

Account.from_trezor(
    path: str = "m/44'/60'/0'/0/0",
    chain: Optional[Chain] = None
) -> Account

Support for Linux ARM#

ARM Linux (aarch64) is now a supported platform. Wake 4.x refused to run there, raising UnsupportedPlatformError: Solidity compiler binaries are not available for Linux-aarch64.

The official solc-bin repository (binaries.soliditylang.org, mirrored on GitHub under argotorg/solc-bin) is the primary source of linux-arm64 binaries. It publishes them from Solidity 0.8.31 onwards; for older versions Wake falls back to the 3rd party nikitastupin/solc mirror. Versions available from both are always taken from the official source.

Installing a version that only the 3rd party mirror provides logs a warning naming the repository, so it is visible which binaries are not official.

MCP server#

wake mcp starts an MCP server exposing Wake's static analysis of the current project as tools for LLM agents and AI coding assistants - listing contracts, resolving declarations, reading sources, following references, inspecting storage layout. The project is compiled with Wake and recompiled on file changes; no chain, RPC or deployment is involved.

wake mcp                 # stdio transport, for clients that launch the server
wake mcp --http 8000     # streamable HTTP on 127.0.0.1:8000/mcp

The server can also compile once from a project exported with wake open --export json, and can be extended with custom tools. See Using the MCP server.