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.connectaccepts no URI of a running node; itsaccounts,chain_id,forkandhardforkarguments 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:
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 *:
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:
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:
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:
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)raisesTypeError: object of type 'builtins.Blocks' has no len()for block in chain.blocksfalls back to integer indexing and dies on the block after the latest one withValueError: Block number out of rangeinstead of stopping- an out-of-range number raises
ValueError: Block number out of range, notKeyError
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_chainalias no longer exists, usechaininstead TransactionRevertedErrorwas renamed toRevertErrorUnknownTransactionRevertedErrorwas renamed toUnknownRevertErrorTransactionStatusEnumwas renamed toExecutionStatusEnumand moved towake.development.internal;wake.development.transactionsre-exports it, so only the name has to change infrom wake.development.transactions import ...- Error types and revert helpers moved from
wake.development.transactionstowake.development.errors:RevertError,UnknownRevertError,Error,Panic,PanicCodeEnum,Halt,must_revert,may_revertandon_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 RevertErrorwith must_revert()/with may_revert()and no argumentsmust_revert(SomeError)and any other revert type or instanceon_reverthandlers - 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:
Regenerate them:
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.callfunction - Return value of
Account.transact random_bytesfunction- 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:
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:
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
strreturnsFalse, where Wake 4 compared the addresses case-insensitively and returnedTrue - equality with an
AccountreturnsFalse, where Wake 4 raisedTypeError: Cannot compare Address and Account. Use Account.address instead(Account == Addresslikewise returnsFalseinstead of raising) - ordering (
<,<=,>,>=) against astrraisesTypeError: '<' 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:
- Remove existing accounts:
wake accounts remove <alias> - 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 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:
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:
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,statusreturn_valueandraw_return_valueerrorandraw_errorcall_traceestimated_gasandaccess_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_listandestimaterequests only. Passingrevert=Falsewith any other request type raisesValueError: revert may only be changed for access list and estimate requests. - It is supported by the
revmengine only. Onanvilandhardhat, pytypes-generated functions raiseNotImplementedError: revert is not supported with non-revm chainsand the low-level API raisesNotImplementedError: 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_nameattribute with the canonical name of the event, and_event_namewith the event name alone - Other attributes named according to the event parameters in ABI
ExternalError class:
- Provides
_error_full_nameattribute with the canonical name of the error, and_error_namewith the error name alone - Other attributes named according to the error parameters in ABI
- Subclasses
RevertError, so it can be caught withmay_revert/must_revertand carriestx,callandcall_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.
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()returningkeccak256of the encoded type hash concatenated with the encoded data, asbytes32.get_eip712_signing_hash(domain)returning the full EIP-712 signing hash for the givenEip712Domain, asbytes32
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).
Support for Trezor hardware wallets#
An Account instance may be created using a new function:
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.