Skip to content
Merged
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
19 changes: 19 additions & 0 deletions .changeset/evm2-call-execution.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
"ox": minor
---

Added `ox/evm`, an EVM backed by `alloy-rs/evm2` compiled to WebAssembly, with read-only transaction execution.

```ts

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Remove the extra changeset example

The changeset continues past its one-sentence summary into a fenced code example, so the published release entry does not follow this repository's requirement that changeset entries consist of a single past-tense sentence. Keep the summary and remove the additional block.

AGENTS.md reference: AGENTS.md:L120-L120

Useful? React with 👍 / 👎.

import { Database, Evm, TxResult } from 'ox/evm'

const evm = await Evm.create({
database: Database.fromMemory({
accounts: { '0x0000000000000000000000000000000000000001': { balance: 1n } },
}),
specId: 'osaka',
})

const result = Evm.callTx(evm, { envelope, signer })
TxResult.txGasUsed(result)
```
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ ox.api.json
erc4337.api.json
node.api.json
tempo.api.json
evm.api.json
wasm.api.json
lookup.json
config-generated.ts
Expand All @@ -30,6 +31,7 @@ site/src/pages/node
site/src/pages/tempo/*
!site/src/pages/tempo/guides/
!site/src/pages/tempo/guides/**
site/src/pages/evm
site/src/pages/wasm
site/src/pages/webauthn
site/src/pages/zod
Expand Down
38 changes: 37 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"deps:ci": "pnpx actions-up",
"dev": "pnpm exports:update && zile dev",
"docs:dev": "pnpm --filter site dev",
"docs:extract": "pnpm api-extractor run -c scripts/docgen/api-extractor.json --local && pnpm api-extractor run -c scripts/docgen/api-extractor.erc4337.json --local && pnpm api-extractor run -c scripts/docgen/api-extractor.node.json --local && pnpm api-extractor run -c scripts/docgen/api-extractor.tempo.json --local && pnpm api-extractor run -c scripts/docgen/api-extractor.wasm.json --local && node --import tsx scripts/docgen/build.ts",
"docs:extract": "pnpm api-extractor run -c scripts/docgen/api-extractor.json --local && pnpm api-extractor run -c scripts/docgen/api-extractor.erc4337.json --local && pnpm api-extractor run -c scripts/docgen/api-extractor.evm.json --local && pnpm api-extractor run -c scripts/docgen/api-extractor.node.json --local && pnpm api-extractor run -c scripts/docgen/api-extractor.tempo.json --local && pnpm api-extractor run -c scripts/docgen/api-extractor.wasm.json --local && node --import tsx scripts/docgen/build.ts",
"docs:gen": "pnpm build && pnpm docs:extract",
"docs:build": "pnpm docs:gen && pnpm --filter site build",
"exports:update": "node --import tsx ./scripts/exports:update.ts",
Expand Down Expand Up @@ -135,6 +135,12 @@
"import": "*",
"limit": "225 kB"
},
{
"name": "import * from 'ox/evm'",
"path": "./dist/evm/index.js",
"import": "*",
"limit": "320 kB"
},
{
"name": "import { Hash } from 'ox/wasm'",
"path": "./dist/wasm/index.js",
Expand Down Expand Up @@ -706,6 +712,36 @@
"types": "./dist/erc8021/Attribution.d.ts",
"default": "./dist/erc8021/Attribution.js"
},
"./evm": {
"src": "./src/evm/index.ts",
"types": "./dist/evm/index.d.ts",
"default": "./dist/evm/index.js"
Comment thread
jxom marked this conversation as resolved.
},
"./evm/Database": {
"src": "./src/evm/Database.ts",
"types": "./dist/evm/Database.d.ts",
"default": "./dist/evm/Database.js"
},
"./evm/Ethereum": {
"src": "./src/evm/Ethereum.ts",
"types": "./dist/evm/Ethereum.d.ts",
"default": "./dist/evm/Ethereum.js"
},
"./evm/Evm": {
"src": "./src/evm/Evm.ts",
"types": "./dist/evm/Evm.d.ts",
"default": "./dist/evm/Evm.js"
},
"./evm/SpecId": {
"src": "./src/evm/SpecId.ts",
"types": "./dist/evm/SpecId.d.ts",
"default": "./dist/evm/SpecId.js"
},
"./evm/TxResult": {
"src": "./src/evm/TxResult.ts",
"types": "./dist/evm/TxResult.d.ts",
"default": "./dist/evm/TxResult.js"
},
"./node": {
"src": "./src/node/index.ts",
"types": "./dist/node/index.d.ts",
Expand Down
35 changes: 35 additions & 0 deletions scripts/docgen/api-extractor.evm.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json",
"mainEntryPointFilePath": "../../dist/evm/index.d.ts",
"bundledPackages": [],
"newlineKind": "lf",
"enumMemberOrder": "preserve",
"compiler": {
"tsconfigFilePath": "<projectFolder>/tsconfig.json",
"overrideTsconfig": {},
"skipLibCheck": true
},
"apiReport": {
"enabled": false
},
"docModel": {
"enabled": true,
"apiJsonFilePath": "<projectFolder>/scripts/docgen/evm.api.json",
"includeForgottenExports": true,
"projectFolderUrl": "https://github.com/wevm/ox/blob/main"
},
"dtsRollup": {
"enabled": false
},
"messages": {
"extractorMessageReporting": {
"default": { "logLevel": "warning" },
"ae-different-release-tags": { "logLevel": "none" },
"ae-incompatible-release-tags": { "logLevel": "none" },
"ae-internal-mixed-release-tag": { "logLevel": "none" },
"ae-missing-release-tag": { "logLevel": "none" },
"ae-unresolved-link": { "logLevel": "none" },
"ae-forgotten-export": { "logLevel": "none" }
}
}
}
2 changes: 1 addition & 1 deletion scripts/docgen/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ console.log('Generating API docs.')
/// Clean previously generated pages
////////////////////////////////////////////////////////////

for (const dir of ['api', 'ercs', 'node', 'wasm', 'webauthn', 'zod']) {
for (const dir of ['api', 'ercs', 'evm', 'node', 'wasm', 'webauthn', 'zod']) {
fs.removeSync(`./site/src/pages/${dir}`)
}
// Preserve hand-authored Tempo guides while replacing generated pages.
Expand Down
159 changes: 159 additions & 0 deletions src/evm/Database.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import type * as Address from '../core/Address.js'
import * as Bytes from '../core/Bytes.js'
import * as Errors from '../core/Errors.js'
import * as Hash from '../core/Hash.js'
import * as Hex from '../core/Hex.js'
import type * as internal from './internal/database.js'

/**
* State an EVM reads through.
*
* A database answers reads and nothing else. Writes accumulate inside the EVM,
* never reaching back through this boundary, so a source can be a plain object
* over local state or a view onto a remote node.
*
* Reads are synchronous here. Asynchronous sources arrive with a separate type.
*/
export type Database = internal.Database

/** An account, as a database reports it. */
export type Account = internal.Account

/**
* Creates an in-memory database.
*
* Code is supplied inline with each account, so
* {@link ox#Database.(Database:type)}`.getCodeByHash` is never reached: evm2
* files inline code under its hash when it loads the account.
*
* @example
* ```ts twoslash
* import { Database } from 'ox/evm'
*
* const database = Database.fromMemory({
* accounts: {
* '0x0000000000000000000000000000000000000001': {
* balance: 1n
* },
* '0x0000000000000000000000000000000000000002': {
* code: '0x5f5ff3'
* }
* }
* })
* ```
*
* @param options - Initial state.
* @returns An in-memory database.
*/
export function fromMemory(options: fromMemory.Options = {}): Database {
type Entry = {
balance: bigint
code: Bytes.Bytes | undefined
codeHash: Hex.Hex
nonce: bigint
storage: Map<bigint, bigint>
}

const emptyCodeHash = Hash.keccak256('0x')
const accounts = new Map<string, Entry>()
for (const [address, account] of Object.entries(options.accounts ?? {})) {
// Copied, not referenced: `Bytes.from` hands back a caller's own array, and
// mutating it later would change the code while `codeHash` stayed stale.
const code = account.code
? Uint8Array.from(Bytes.from(account.code))
: undefined
accounts.set(address.toLowerCase(), {
balance: account.balance ?? 0n,
code,
codeHash: code ? Hash.keccak256(Hex.fromBytes(code)) : emptyCodeHash,
nonce: account.nonce ?? 0n,
storage: new Map(
Object.entries(account.storage ?? {}).map(([slot, value]) => [
BigInt(slot),
value,
]),
),
})
}

const blockHashes = new Map(
Object.entries(options.blockHashes ?? {}).map(([number, value]) => [
BigInt(number),
value,
]),
)

return {
getAccount(address) {
const account = accounts.get(address.toLowerCase())
if (!account) return undefined
return {
balance: account.balance,
// Copied on the way out too: handing back the stored array would let a
// caller change the executed code while `codeHash` stayed committed.
code: account.code ? Uint8Array.from(account.code) : undefined,
codeHash: account.codeHash,
nonce: account.nonce,
}
},
getBlockHash(number) {
// evm2 range-checks `BLOCKHASH` before asking, so a number reaching here
// is one the chain has and this database is missing.
const blockHash = blockHashes.get(number)
if (!blockHash) throw new MissingBlockHashError({ number })
return blockHash
},
getCodeByHash(codeHash) {
for (const account of accounts.values())
if (
account.code &&
account.codeHash.toLowerCase() === codeHash.toLowerCase()
)
return Uint8Array.from(account.code)
return new Uint8Array()
},
getStorage(address, key) {
return accounts.get(address.toLowerCase())?.storage.get(key) ?? 0n
},
}
}

export declare namespace fromMemory {
type Options = {
/** Accounts to seed, keyed by address. */
accounts?:
| Record<
Address.Address | (string & {}),
{
/** Balance in wei. @default 0n */
balance?: bigint | undefined
/** Deployed code. @default '0x' */
code?: Hex.Hex | Bytes.Bytes | undefined
/** Nonce. @default 0n */
nonce?: bigint | undefined
/** Storage slots, keyed by slot. */
storage?: Record<string, bigint> | undefined
}
>
| undefined
/**
* Historical block hashes, keyed by block number. A `BLOCKHASH` for a
* height inside the window but absent here fails the read.
*/
blockHashes?: Record<string, Hex.Hex> | undefined
}
}

/** Thrown when a `BLOCKHASH` the chain retains was never seeded. */
export class MissingBlockHashError extends Errors.BaseError {
override readonly name = 'Database.MissingBlockHashError'

constructor({ number }: { number: bigint }) {
super('The database has no hash for a block the chain retains.', {
metaMessages: [
`Block: ${number}`,
'Seed it through `blockHashes` to execute this transaction.',
],
})
}
}
16 changes: 16 additions & 0 deletions src/evm/Ethereum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type * as Address from '../core/Address.js'
import type * as Bytes from '../core/Bytes.js'
import type * as Hex from '../core/Hex.js'

/**
* A transaction with its sender already recovered.
*
* evm2 takes the signer alongside a signature-stripped envelope rather than
* recovering it during execution, so the sender is supplied here.
*/
export type RecoveredTx = {
/** EIP-2718 encoded transaction. */
envelope: Hex.Hex | Bytes.Bytes
/** Sender the signature recovers to. */
signer: Address.Address
}
Loading
Loading