diff --git a/cypress/e2e/eip7594.cy.ts b/cypress/e2e/eip7594.cy.ts
deleted file mode 100644
index ce38f7d..0000000
--- a/cypress/e2e/eip7594.cy.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-describe('EIP-7594/PeerDAS Tests', () => {
- it('initialization', () => {
- cy.visit('/eip-7594-peerdas-data-availability-sampling')
-
- // Basic component display
- cy.contains('h1', 'Feel Your Protocol')
- cy.contains('h3', 'Peer Data Availability Sampling')
-
- // Values from inital example
- cy.get('#eip-7594-c textarea', { timeout: 10000 }).should(
- 'contain.value',
- '0a0001fbfc0084bd8494e56454b36510b0adc8aaa1b60',
- )
-
- // Select different example
- cy.get('#eip-7594-c .e-select').click()
- cy.get('#eip-7594-c [role="option"]').eq(1).click()
- cy.get('#eip-7594-c textarea', { timeout: 10000 }).should(
- 'contain.value',
- '00000000000000000000000000000000000000000',
- )
-
- cy.get('.run-button').click()
- let text =
- '0xa522f1be9ec4a02fcb6998b10306e94331311ac29bcaaae357d8d7fbc087a04b5b66dd7fa84cbebabcc45202b8fee57f'
- cy.get('#eip-7594-c .4844-7594-box')
- .find('table tr:first td:nth-child(2)')
- .should('contain.text', text, { timeout: 25000 })
- text =
- '0x8bd1e6e38b2b54735c6f0102022510cf2abc2e4c6c5a437cba9831662c9f112e61e2a6ced8ce63b3de18cb9cc99ae21e'
- cy.get('#eip-7594-c .4844-box').find('p').should('contain.text', text, { timeout: 25000 })
- text =
- '0x8d90ed38068f3561132d9264db8c8dfb5237af24a6e28c3d4e72d3ad8d51d97be5733ddc382c9718822cb29ccc26364e'
- cy.get('#eip-7594-c .7594-box').find('p:first').should('contain.text', text, { timeout: 25000 })
- })
-})
diff --git a/cypress/e2e/eip7883_Precompile_R.cy.ts b/cypress/e2e/eip7883_Precompile_R.cy.ts
deleted file mode 100644
index 018616e..0000000
--- a/cypress/e2e/eip7883_Precompile_R.cy.ts
+++ /dev/null
@@ -1,107 +0,0 @@
-/**
- * EIP-7823 is the representative EIP for the precompile component tests.
- */
-describe('EIP-7823/Precompile Component Tests', () => {
- let bytesLengthsExpected =
- '0000000000000000000000000000000000000000000000000000000000000001' +
- '0000000000000000000000000000000000000000000000000000000000000001' +
- '0000000000000000000000000000000000000000000000000000000000000001'
- const bytesValuesExpected = '030302'
- let bytesExpected = bytesLengthsExpected + bytesValuesExpected
-
- it('initialization', () => {
- cy.visit('/eip-7883-modexp-gas-cost-increase')
-
- cy.contains('h1', 'Feel Your Protocol')
- cy.contains('h3', 'ModExp')
-
- cy.get('#eip-7883-c textarea', { timeout: 10000 }).should('have.value', bytesExpected)
- cy.get('#eip-7883-c input').eq(0).should('have.value', '03')
- cy.get('#eip-7883-c input').eq(1).should('have.value', '03')
- cy.get('#eip-7883-c input').eq(2).should('have.value', '02')
-
- cy.get('.pre-hardfork').find('p').eq(0).should('include.text', '200 Gas')
- cy.get('.post-hardfork').find('p').eq(0).should('include.text', '500 Gas')
- })
-
- it('values -> data', () => {
- cy.visit('/eip-7883-modexp-gas-cost-increase')
-
- // Simple initial case
- bytesExpected = bytesLengthsExpected + '040402'
- cy.get('#eip-7883-c').within(() => {
- cy.get('input').eq(0).clear()
- cy.get('input').eq(1).clear()
- cy.get('input').eq(2).clear()
- cy.get('input').eq(0).type('04')
- cy.get('input').eq(1).type('04')
- cy.get('input').eq(2).type('02')
- })
- cy.get('textarea').should('have.value', bytesExpected)
-
- // Slightly modified values case
- bytesLengthsExpected =
- '0000000000000000000000000000000000000000000000000000000000000002' +
- '0000000000000000000000000000000000000000000000000000000000000002' +
- '0000000000000000000000000000000000000000000000000000000000000002'
- bytesExpected = bytesLengthsExpected + '040404040202'
- cy.get('#eip-7883-c').within(() => {
- cy.get('input').eq(0).type('04')
- cy.get('input').eq(1).type('04')
- cy.get('input').eq(2).type('02')
- })
- cy.get('textarea').should('have.value', bytesExpected)
-
- cy.get('#eip-7883-c').within(() => {
- // Gas changing example
- cy.get('input').eq(0).type('040404040404040404040404040404040404040404')
- cy.get('input').eq(1).type('040404040404040404040404040404040404040404')
- cy.get('input').eq(2).type('02')
-
- cy.get('.pre-hardfork').find('p').eq(0).should('include.text', '534 Gas')
- cy.get('.post-hardfork').find('p').eq(0).should('include.text', '2848 Gas')
- })
- })
-
- it('data -> values, URL sharing, EIP button', () => {
- cy.visit('/eip-7883-modexp-gas-cost-increase')
-
- cy.get('#eip-7883-c').within(() => {
- cy.window().then((win) => {
- cy.stub(win, 'open').callsFake((url) => {
- win.location.href = url // Redirect within the same tab
- })
- })
- // data -> values
- cy.get('textarea').clear()
- cy.get('textarea').type(bytesExpected) // '040404040202'
- cy.get('input').eq(0).should('have.value', '0404')
- cy.get('input').eq(1).should('have.value', '0404')
- cy.get('input').eq(2).should('have.value', '0202')
-
- // URL sharing
- cy.get('.share-url-button').click()
- cy.url().should('include', 'b=0404')
- cy.url().should('include', 'e=0404')
- cy.url().should('include', 'm=0202')
-
- cy.get('textarea').should('have.value', bytesExpected)
- cy.get('input').eq(0).should('have.value', '0404')
- cy.get('input').eq(1).should('have.value', '0404')
- cy.get('input').eq(2).should('have.value', '0202')
-
- // examples
- cy.get('.e-select').click()
- cy.contains('[role="option"]', 'Simple').click()
- cy.get('input').eq(0).should('have.value', '03')
- cy.get('input').eq(1).should('have.value', '03')
- cy.get('input').eq(2).should('have.value', '02')
-
- // EIP button
- cy.get('.visit-exploration-button').invoke('removeAttr', 'target').click()
- cy.origin('https://eips.ethereum.org', () => {
- cy.url().should('eq', 'https://eips.ethereum.org/EIPS/eip-7883')
- })
- })
- })
-})
diff --git a/cypress/e2e/eip7951.cy.ts b/cypress/e2e/eip7951.cy.ts
deleted file mode 100644
index 342baf7..0000000
--- a/cypress/e2e/eip7951.cy.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-describe('EIP-7951/secp256r1 Precompile Support', () => {
- it('initialization', () => {
- cy.visit('/eip-7951-secp256r1-precompile')
-
- // Basic component display
- cy.contains('h1', 'Feel Your Protocol')
- cy.contains('h3', 'secp256r1 Precompile Support')
-
- // Values from inital example
- cy.get('#eip-7951-c textarea', { timeout: 10000 }).should(
- 'contain.value',
- '4dfb1eae8ed41e188b8a44a1109d982d01fc24bb85a933',
- )
- const val = '3b91fedfb22f40063245c621036a040c159f02ae02e6d450ff9b53235e9232c4'
- cy.get('#eip-7951-c input').eq(2).should('have.value', val)
-
- cy.get('.post-hardfork').find('p').eq(0).should('include.text', '6900 Gas')
-
- // Select different example
- cy.get('#eip-7951-c .e-select').click()
- cy.contains('[role="option"]', 'Invalid (Wycheproof), r value too large').click()
- cy.get('#eip-7951-c textarea', { timeout: 10000 }).should(
- 'contain.value',
- '532eaabd9574880dbf76b9b8cc00832c20a6ec113d682299550d7a6e0f345e25',
- )
- cy.get('#eip-7951-c input')
- .eq(2)
- .should('have.value', 'ffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc63254e')
- cy.get('.post-hardfork').find('p').eq(0).should('include.text', '6900 Gas')
- })
-})
diff --git a/cypress/e2e/explorations.cy.ts b/cypress/e2e/explorations.cy.ts
new file mode 100644
index 0000000..2c409f7
--- /dev/null
+++ b/cypress/e2e/explorations.cy.ts
@@ -0,0 +1,85 @@
+describe('EIP-7883 ModExp', () => {
+ it('loads and displays exploration content', () => {
+ cy.visit('/eip-7883-modexp-gas-cost-increase')
+ cy.contains('h1', 'Feel Your Protocol')
+ cy.contains('h3', 'ModExp')
+ cy.get('#eip-7883-c', { timeout: 10000 }).should('exist')
+ })
+
+ it('loads default example with inputs', () => {
+ cy.visit('/eip-7883-modexp-gas-cost-increase')
+ cy.get('#eip-7883-c textarea', { timeout: 10000 }).should('not.have.value', '')
+ cy.get('#eip-7883-c input').should('have.length.gte', 3)
+ })
+
+ it('example selector shows available options', () => {
+ cy.visit('/eip-7883-modexp-gas-cost-increase')
+ cy.get('#eip-7883-c .e-select', { timeout: 10000 }).click()
+ cy.get('[role="option"]').should('have.length.gte', 2)
+ })
+})
+
+describe('EIP-7594 PeerDAS', () => {
+ it('loads and displays exploration content', () => {
+ cy.visit('/eip-7594-peerdas-data-availability-sampling')
+ cy.contains('h1', 'Feel Your Protocol')
+ cy.contains('h3', 'Peer Data Availability Sampling')
+ cy.get('#eip-7594-c', { timeout: 10000 }).should('exist')
+ })
+
+ it('loads default example with blob data', () => {
+ cy.visit('/eip-7594-peerdas-data-availability-sampling')
+ cy.get('#eip-7594-c textarea', { timeout: 10000 }).should('not.have.value', '')
+ })
+
+ it('example selector shows available options', () => {
+ cy.visit('/eip-7594-peerdas-data-availability-sampling')
+ cy.get('#eip-7594-c .e-select', { timeout: 10000 }).click()
+ cy.get('[role="option"]').should('have.length.gte', 2)
+ })
+})
+
+describe('EIP-7951 secp256r1', () => {
+ it('loads and displays exploration content', () => {
+ cy.visit('/eip-7951-secp256r1-precompile')
+ cy.contains('h1', 'Feel Your Protocol')
+ cy.contains('h3', 'secp256r1 Precompile Support')
+ cy.get('#eip-7951-c', { timeout: 10000 }).should('exist')
+ })
+
+ it('loads default example with inputs', () => {
+ cy.visit('/eip-7951-secp256r1-precompile')
+ cy.get('#eip-7951-c textarea', { timeout: 10000 }).should('not.have.value', '')
+ cy.get('#eip-7951-c input').should('have.length.gte', 5)
+ })
+
+ it('example selector shows available options', () => {
+ cy.visit('/eip-7951-secp256r1-precompile')
+ cy.get('#eip-7951-c .e-select', { timeout: 10000 }).click()
+ cy.get('[role="option"]').should('have.length.gte', 2)
+ })
+})
+
+describe('Custom Addition Precompile', () => {
+ it('loads and displays exploration content', () => {
+ cy.visit('/custom-addition-precompile')
+ cy.contains('h1', 'Feel Your Protocol')
+ cy.contains('h3', 'Custom Addition Precompile')
+ cy.get('#custom-addition-precompile-c', { timeout: 10000 }).should('exist')
+ })
+
+ it('loads default example with inputs', () => {
+ cy.visit('/custom-addition-precompile')
+ cy.get('#custom-addition-precompile-c textarea', { timeout: 10000 }).should(
+ 'not.have.value',
+ '',
+ )
+ cy.get('#custom-addition-precompile-c input').should('have.length.gte', 2)
+ })
+
+ it('example selector shows available options', () => {
+ cy.visit('/custom-addition-precompile')
+ cy.get('#custom-addition-precompile-c .e-select', { timeout: 10000 }).click()
+ cy.get('[role="option"]').should('have.length.gte', 2)
+ })
+})
diff --git a/dist/website/index.html b/dist/website/index.html
index 1ba5325..ab1f316 100644
--- a/dist/website/index.html
+++ b/dist/website/index.html
@@ -5,8 +5,8 @@
Feel Your Protocol
-
-
+
+
diff --git a/docs/contributing/adding-an-exploration.md b/docs/contributing/adding-an-exploration.md
index 7177b4f..09fe053 100644
--- a/docs/contributing/adding-an-exploration.md
+++ b/docs/contributing/adding-an-exploration.md
@@ -8,10 +8,12 @@ An exploration folder looks like this:
```
src/explorations/eip-XXXX/
-├── info.ts # Metadata (required)
-├── MyC.vue # Interactive widget (required)
-├── examples.ts # Example presets (recommended)
-└── data/ # Optional data files
+├── info.ts # Metadata (required)
+├── MyC.vue # Interactive widget (required)
+├── examples.ts # Example presets (recommended)
+├── tests.spec.ts # Unit tests (required)
+├── config.ts # Precompile config (for precompile explorations)
+└── data/ # Optional data files
```
## Step 1: Create the Folder
@@ -145,37 +147,58 @@ The `ExplorationC` wrapper renders the title, info link, intro text, and usage t
### Option B: Precompile Interface E-Component
-If your exploration is about a precompile, you can use the Precompile Interface E-Component and get a full-featured widget in ~30 lines:
+If your exploration is about a precompile, you can use the Precompile Interface E-Component. It handles all input management while you provide the execution logic and result display:
-```vue
-
-
+
+
+
+
+
```
-See [Using E-Components](/contributing/e-components) for the full API reference.
+The `useStandardPrecompileRun` helper covers the common EthereumJS pre/post hardfork comparison. For custom execution (different library, custom precompile, etc.), provide your own `run` function and `#result` slot — see [Available E-Components](/contributing/available-e-components) for the full API reference.
## Step 5: Register in the Registry
@@ -204,7 +227,99 @@ Import libraries only in your `MyC.vue` — never in shared code. This keeps eac
If you need a custom library fork (e.g. with experimental features), see [Library Forks](/contributing/library-forks).
-## Step 7: Verify
+## Step 7: Add Tests
+
+Each exploration should have a `tests.spec.ts` file in its folder. Tests verify that your exploration's metadata, examples, and config are correct.
+
+### What to Test
+
+**All explorations** should test:
+
+- `info.ts` — correct `id`, `path`, `topic`, and `poweredBy`
+- `examples.ts` — each example has the right number of values, valid hex data, and a non-empty title
+
+**Precompile explorations** should additionally test:
+
+- `config.ts` — `defaultExample` exists in examples, value field count and URL params match expectations
+- `assembleData`/`parseData` — if defined, verify they produce correct output and are inverse operations
+
+### Example: Custom Exploration Test
+
+```typescript
+import { describe, expect, it } from 'vitest'
+
+import { examples } from '../examples'
+import { INFO } from '../info'
+
+describe('EIP-XXXX Exploration', () => {
+ describe('info', () => {
+ it('has correct metadata', () => {
+ expect(INFO.id).toBe('eip-XXXX')
+ expect(INFO.path).toContain('eip-XXXX')
+ expect(INFO.topic).toBe('fusaka')
+ expect(INFO.poweredBy.length).toBeGreaterThan(0)
+ })
+ })
+
+ describe('examples', () => {
+ it('each example has valid hex data', () => {
+ const hexRegex = /^[0-9a-f]+$/i
+ for (const [key, ex] of Object.entries(examples)) {
+ for (const val of ex.values) {
+ expect(val, `Value in "${key}" should be valid hex`).toMatch(hexRegex)
+ }
+ }
+ })
+ })
+})
+```
+
+### Example: Precompile Exploration Test
+
+```typescript
+import { describe, expect, it } from 'vitest'
+
+import { config } from '../config'
+import { examples } from '../examples'
+import { INFO } from '../info'
+
+describe('EIP-XXXX Exploration', () => {
+ describe('info', () => {
+ it('has correct metadata', () => {
+ expect(INFO.id).toBe('eip-XXXX')
+ expect(INFO.topic).toBe('fusaka')
+ })
+ })
+
+ describe('config', () => {
+ it('references a valid default example', () => {
+ expect(examples[config.defaultExample]).toBeDefined()
+ })
+
+ it('has correct number of value fields', () => {
+ expect(config.values).toHaveLength(2)
+ })
+ })
+
+ describe('examples', () => {
+ it('each example has the right number of values', () => {
+ const editableCount = config.values.filter((v) => v.urlParam).length
+ for (const [key, ex] of Object.entries(examples)) {
+ expect(ex.values, `Example "${key}"`).toHaveLength(editableCount)
+ }
+ })
+ })
+})
+```
+
+### Running Tests
+
+```bash
+npx vitest run # run all unit tests
+npx vitest run src/explorations/eip-XXXX # run tests for one exploration
+```
+
+## Step 8: Verify
```bash
npm run dev # check your exploration locally
@@ -218,7 +333,9 @@ npm run build # verify production build
- [ ] Created `src/explorations//info.ts` with metadata
- [ ] Created `src/explorations//MyC.vue` with interactive widget
- [ ] Created `src/explorations//examples.ts` with example presets
+- [ ] Created `src/explorations//tests.spec.ts` with unit tests
- [ ] Added import and entry in `src/explorations/REGISTRY.ts`
- [ ] Installed library dependencies (if needed)
+- [ ] All unit tests pass
- [ ] Linting and type checking pass
- [ ] Production build succeeds
diff --git a/docs/contributing/available-e-components.md b/docs/contributing/available-e-components.md
index fe57124..6ab727d 100644
--- a/docs/contributing/available-e-components.md
+++ b/docs/contributing/available-e-components.md
@@ -4,47 +4,49 @@ This page lists all E-Components that are ready to use in your explorations. For
## Precompile Interface (`precompileInterfaceEC`)
-A complete interface for exploring EVM precompiles. It handles:
+An interface for exploring EVM precompiles. It handles input management while leaving execution and result display to the exploration:
- Example selection and URL sharing
- Hex data input with parsing and validation
- Individual value inputs with byte length tracking
-- Side-by-side pre/post hardfork comparison (running the precompile on two different EVM versions)
-- Result display with gas cost and output data
+- **Execution and result display** are provided by the exploration via the `run` prop and `#result` slot
**Files:**
```
src/eComponents/precompileInterfaceEC/
├── PrecompileInterfaceEC.vue # Main component
-├── PrecompileInterfaceResultEC.vue # Result display (pre/post comparison)
+├── PrecompileInterfaceResultEC.vue # Result display (reusable, e.g. pre/post comparison)
├── PrecompileValueInputEC.vue # Value input with byte length validation
-├── usePrecompileState.ts # Composable: all state and logic
+├── usePrecompileState.ts # Composable: input state and sync logic
├── types.ts # PrecompileConfig and PrecompileValueDef
-└── run.ts # EVM precompile execution utility
+└── run.ts # EVM precompile execution utility + useStandardPrecompileRun
```
**Used by:** [EIP-7951](https://github.com/feelyourprotocol/website/blob/main/src/explorations/eip-7951/MyC.vue) (secp256r1), [EIP-7883](https://github.com/feelyourprotocol/website/blob/main/src/explorations/eip-7883/MyC.vue) (ModExp gas cost)
### Basic Usage
-A precompile exploration needs just a config object and a single component tag:
+A precompile exploration provides a config for input layout, a `run` function for execution, and a `#result` slot for visualization. For the standard EthereumJS pre/post hardfork comparison, use the `useStandardPrecompileRun` helper:
```vue
-
+
+
+
+
+
```
+### Component Props
+
+| Prop | Required | Description |
+|------|----------|-------------|
+| `config` | Yes | Input configuration (see `PrecompileConfig` below) |
+| `examples` | Yes | Example presets from `examples.ts` |
+| `exploration` | Yes | Exploration metadata from `info.ts` |
+| `run` | Yes | Execution function, called with the assembled hex data (without `0x`) on every valid input change |
+
### `PrecompileConfig` Reference
```typescript
interface PrecompileConfig {
explorationId: string
- precompileAddress: string
- preHardfork: Hardfork
- postHardfork: Hardfork
defaultExample: string
showBigInt?: boolean
values: PrecompileValueDef[]
@@ -77,9 +94,6 @@ interface PrecompileConfig {
| Field | Required | Description |
|-------|----------|-------------|
| `explorationId` | Yes | Matches the exploration's `id` in `info.ts` |
-| `precompileAddress` | Yes | Hex address of the precompile (e.g. `'05'` for ModExp) |
-| `preHardfork` | Yes | Hardfork for the "before" comparison |
-| `postHardfork` | Yes | Hardfork for the "after" comparison |
| `defaultExample` | Yes | Key from `examples.ts` to load on initialization |
| `showBigInt` | No | Show BigInt representations for values (default: per-value setting) |
| `values` | Yes | Array of value definitions (see below) |
@@ -137,3 +151,62 @@ const config: PrecompileConfig = {
},
}
```
+
+### Execution: `run` Prop and `#result` Slot
+
+The E-Component separates input management from execution. The exploration provides:
+
+1. A **`run` function** — called automatically with the assembled hex data on every valid input change
+2. A **`#result` slot** — renders the execution results however the exploration needs
+
+#### Standard: `useStandardPrecompileRun`
+
+For the common pre/post hardfork comparison using the EthereumJS EVM, use the provided helper:
+
+```typescript
+import { useStandardPrecompileRun } from '@/eComponents/precompileInterfaceEC/run'
+
+const { run, execResultPre, execResultPost } = useStandardPrecompileRun(
+ Hardfork.Prague, Hardfork.Osaka, '05',
+)
+```
+
+This returns a `run` function ready to pass as a prop and two reactive refs for the results. Use `PrecompileInterfaceResultEC` in the `#result` slot for the standard gas + hex display.
+
+#### Custom Execution
+
+For explorations that need a different execution mechanism (custom precompile, different library, etc.), define your own `run` function and result state:
+
+```vue
+
+
+
+
+
+
+
+ {{ myResult }}
+ Press enter or change input...
+
+
+
+
+
+```
+
+The `#result` slot template lives in the exploration's scope, so it naturally accesses your own refs and computed properties.
diff --git a/docs/contributing/styling.md b/docs/contributing/styling.md
index 3a5fb13..5a77015 100644
--- a/docs/contributing/styling.md
+++ b/docs/contributing/styling.md
@@ -4,7 +4,7 @@ The project uses [Tailwind CSS v4](https://tailwindcss.com/) for styling with a
## How Topic Colors Work
-Every exploration belongs to a **topic** (e.g. "Fusaka"), and each topic has a color (blue, yellow, green, red). The `ExplorationC` wrapper component sets CSS custom properties on its root element based on the topic color. All child components — UI components, E-Components, and your widget — automatically inherit these colors.
+Every exploration belongs to a **topic** (e.g. "Fusaka"), and each topic has a color (blue, yellow, green, red, orange, purple). The `ExplorationC` wrapper component sets CSS custom properties on its root element based on the topic color. All child components — UI components, E-Components, and your widget — automatically inherit these colors.
```
Topic (e.g. Fusaka = blue)
@@ -20,13 +20,14 @@ These variables are set by `ExplorationC` and available to all child elements:
| Variable | Purpose | Blue topic example |
|----------|---------|-------------------|
-| `--e-text` | Primary text color | `blue-900` |
-| `--e-bg` | Exploration wrapper background | `blue-200` |
-| `--e-bg-light` | Input backgrounds | `blue-50` |
+| `--e-text` | Primary text color | `blue-800` |
+| `--e-bg` | Exploration wrapper background | `blue-100` |
+| `--e-bg-light` | Input backgrounds | `white` |
| `--e-bg-medium` | Button backgrounds | `blue-100` |
| `--e-bg-dark` | Result box backgrounds | `blue-900` |
-| `--e-border` | Light borders | `blue-200` |
-| `--e-border-dark` | Dark borders | `blue-900` |
+| `--e-border` | Borders on coloured backgrounds | `blue-300` |
+| `--e-border-dark` | Dark/emphasis borders | `blue-800` |
+| `--e-accent` | Focus rings, interactive accents | `blue-600` |
If you need the topic color for custom elements, use the `.e-text` utility class or reference the variables directly:
diff --git a/src/eComponents/precompileInterfaceEC/PrecompileInterfaceEC.vue b/src/eComponents/precompileInterfaceEC/PrecompileInterfaceEC.vue
index 6511333..c2606b9 100644
--- a/src/eComponents/precompileInterfaceEC/PrecompileInterfaceEC.vue
+++ b/src/eComponents/precompileInterfaceEC/PrecompileInterfaceEC.vue
@@ -1,4 +1,6 @@
-
@@ -62,10 +67,7 @@ await init()
:bigIntVal="bigIntVals[val.index]"
/>
-
+
diff --git a/src/eComponents/precompileInterfaceEC/__tests__/run.spec.ts b/src/eComponents/precompileInterfaceEC/__tests__/run.spec.ts
new file mode 100644
index 0000000..5c3da9a
--- /dev/null
+++ b/src/eComponents/precompileInterfaceEC/__tests__/run.spec.ts
@@ -0,0 +1,56 @@
+import { describe, expect, it } from 'vitest'
+import { Hardfork } from '@ethereumjs/common'
+import type { PrefixedHexString } from '@ethereumjs/util'
+
+import { runPrecompile, useStandardPrecompileRun } from '../run'
+
+const MODEXP_SIMPLE = ('0x' +
+ '0000000000000000000000000000000000000000000000000000000000000001' +
+ '0000000000000000000000000000000000000000000000000000000000000001' +
+ '0000000000000000000000000000000000000000000000000000000000000001' +
+ '030302') as PrefixedHexString
+
+describe('runPrecompile', () => {
+ it('returns pre and post results for ModExp (address 05)', async () => {
+ const result = await runPrecompile(MODEXP_SIMPLE, Hardfork.Prague, Hardfork.Osaka, '05')
+ expect(result.pre).toBeDefined()
+ expect(result.post).toBeDefined()
+ expect(result.post.executionGasUsed).toBeTypeOf('bigint')
+ })
+
+ it('returns correct gas values for simple ModExp (EIP-7883)', async () => {
+ const result = await runPrecompile(MODEXP_SIMPLE, Hardfork.Prague, Hardfork.Osaka, '05')
+ expect(result.pre!.executionGasUsed).toBe(200n)
+ expect(result.post.executionGasUsed).toBe(500n)
+ })
+
+ it('returns gas for secp256r1 precompile (address 100)', async () => {
+ const data = ('0x' +
+ '4dfb1eae8ed41e188b8a44a1109d982d01fc24bb85a933e6283e8838e46942fd' +
+ 'eb3dc5ce2902f162745057efb7a3308eba992c0d843623603516845ffccd3f10' +
+ '3b91fedfb22f40063245c621036a040c159f02ae02e6d450ff9b53235e9232c4' +
+ 'bfa6d0a419b5bc625939cccb8db65a16f7c30c697928660e9da53eda031e80fa' +
+ 'db5998a893f9b8971a3892aecd132c0eca1bc9622e542f428d8129222f26bdc5') as PrefixedHexString
+ const result = await runPrecompile(data, Hardfork.Prague, Hardfork.Osaka, '100')
+ expect(result.post).toBeDefined()
+ expect(result.post.executionGasUsed).toBeTypeOf('bigint')
+ })
+})
+
+describe('useStandardPrecompileRun', () => {
+ it('returns a run function', () => {
+ const { run } = useStandardPrecompileRun(Hardfork.Prague, Hardfork.Osaka, '05')
+ expect(run).toBeTypeOf('function')
+ })
+
+ it('run returns pre/post results directly', async () => {
+ const { run } = useStandardPrecompileRun(Hardfork.Prague, Hardfork.Osaka, '05')
+
+ const result = await run(MODEXP_SIMPLE)
+
+ expect(result.pre).toBeDefined()
+ expect(result.post).toBeDefined()
+ expect(result.pre!.executionGasUsed).toBe(200n)
+ expect(result.post.executionGasUsed).toBe(500n)
+ })
+})
diff --git a/src/eComponents/precompileInterfaceEC/__tests__/usePrecompileState.spec.ts b/src/eComponents/precompileInterfaceEC/__tests__/usePrecompileState.spec.ts
new file mode 100644
index 0000000..df57ec4
--- /dev/null
+++ b/src/eComponents/precompileInterfaceEC/__tests__/usePrecompileState.spec.ts
@@ -0,0 +1,95 @@
+import { describe, expect, it, vi } from 'vitest'
+
+vi.mock('vue-router', () => ({
+ useRoute: () => ({ query: {} }),
+ useRouter: () => ({ resolve: vi.fn(() => ({ href: '' })) }),
+}))
+
+import type { Examples } from '@/explorations/REGISTRY'
+
+import type { PrecompileConfig } from '../types'
+import { usePrecompileState } from '../usePrecompileState'
+
+const config: PrecompileConfig = {
+ explorationId: 'test',
+ defaultExample: 'ex1',
+ values: [{ title: 'A', urlParam: 'a', expectedLen: 4n }],
+}
+
+const examples: Examples = {
+ ex1: { title: 'Example 1', values: ['deadbeef'] },
+}
+
+describe('usePrecompileState', () => {
+ it('calls run with assembled data on init', async () => {
+ const run = vi.fn().mockResolvedValue(undefined)
+ const state = usePrecompileState(config, examples, run)
+
+ await state.init()
+
+ expect(run).toHaveBeenCalledWith('0xdeadbeef')
+ })
+
+ it('calls run on data input change', async () => {
+ const run = vi.fn().mockResolvedValue(undefined)
+ const state = usePrecompileState(config, examples, run)
+ await state.init()
+ run.mockClear()
+
+ state.data.value = 'cafebabe'
+ await state.onDataInputFormChange()
+
+ expect(run).toHaveBeenCalledWith('0xcafebabe')
+ })
+
+ it('calls run on value input change', async () => {
+ const run = vi.fn().mockResolvedValue(undefined)
+ const state = usePrecompileState(config, examples, run)
+ await state.init()
+ run.mockClear()
+
+ state.hexVals.value[0] = 'aabbccdd'
+ await state.onValueInputFormChange()
+
+ expect(run).toHaveBeenCalledWith('0xaabbccdd')
+ })
+
+ it('clears example on manual data input', async () => {
+ const run = vi.fn().mockResolvedValue(undefined)
+ const state = usePrecompileState(config, examples, run)
+ await state.init()
+ expect(state.example.value).toBe('ex1')
+
+ state.data.value = 'cafebabe'
+ await state.onDataInputFormChange()
+
+ expect(state.example.value).toBe('')
+ })
+
+ it('exposes result ref populated by run return value', async () => {
+ const mockResult = { gas: 42n }
+ const run = vi.fn().mockResolvedValue(mockResult)
+ const state = usePrecompileState(config, examples, run)
+
+ expect(state.result.value).toBeUndefined()
+
+ await state.init()
+
+ expect(state.result.value).toEqual(mockResult)
+ })
+
+ it('exposes input state without legacy execution refs', () => {
+ const run = vi.fn().mockResolvedValue(undefined)
+ const state = usePrecompileState(config, examples, run)
+
+ expect(state.data).toBeDefined()
+ expect(state.hexVals).toBeDefined()
+ expect(state.bigIntVals).toBeDefined()
+ expect(state.byteLengths).toBeDefined()
+ expect(state.example).toBeDefined()
+ expect(state.inputValues).toBeDefined()
+ expect(state.result).toBeDefined()
+ expect(state).not.toHaveProperty('execResultPre')
+ expect(state).not.toHaveProperty('execResultPost')
+ })
+})
diff --git a/src/eComponents/precompileInterfaceEC/__tests__/utils.spec.ts b/src/eComponents/precompileInterfaceEC/__tests__/utils.spec.ts
new file mode 100644
index 0000000..fc40ad9
--- /dev/null
+++ b/src/eComponents/precompileInterfaceEC/__tests__/utils.spec.ts
@@ -0,0 +1,185 @@
+import { describe, expect, it } from 'vitest'
+import { ref } from 'vue'
+
+import {
+ countUpwardsHexStr,
+ dataToValueInput,
+ isValidByteInputForm,
+ padHex,
+ toHex,
+ valueToDataInput,
+} from '../utils'
+
+describe('isValidByteInputForm', () => {
+ it('accepts valid hex without prefix', () => {
+ expect(isValidByteInputForm('deadbeef')).toEqual([])
+ })
+
+ it('accepts empty string', () => {
+ expect(isValidByteInputForm('')).toEqual([])
+ })
+
+ it('rejects 0x prefix', () => {
+ const errors = isValidByteInputForm('0xdeadbeef')
+ expect(errors.some((e) => e.includes('0x'))).toBe(true)
+ })
+
+ it('rejects invalid hex characters', () => {
+ const errors = isValidByteInputForm('zzzz')
+ expect(errors.some((e) => e.toLowerCase().includes('invalid'))).toBe(true)
+ })
+
+ it('validates expected byte length (too short)', () => {
+ const errors = isValidByteInputForm('dead', 4n)
+ expect(errors.some((e) => e.includes('4 bytes expected'))).toBe(true)
+ })
+
+ it('validates expected byte length (too long)', () => {
+ const errors = isValidByteInputForm('deadbeefcafe', 2n)
+ expect(errors.some((e) => e.includes('2 bytes expected'))).toBe(true)
+ })
+
+ it('accepts correct expected length', () => {
+ expect(isValidByteInputForm('deadbeef', 4n)).toEqual([])
+ })
+
+ it('returns multiple errors when applicable', () => {
+ const errors = isValidByteInputForm('0xzz', 4n)
+ expect(errors.length).toBeGreaterThanOrEqual(2)
+ })
+})
+
+describe('toHex', () => {
+ it('converts bigint to padded hex string', () => {
+ expect(toHex(255n, 4)).toBe('00ff')
+ })
+
+ it('converts zero', () => {
+ expect(toHex(0n, 4)).toBe('0000')
+ })
+
+ it('pads to requested length', () => {
+ expect(toHex(1n, 64)).toBe('0'.repeat(63) + '1')
+ })
+
+ it('does not truncate if value exceeds length', () => {
+ expect(toHex(256n, 2)).toBe('100')
+ })
+})
+
+describe('padHex', () => {
+ it('pads odd-length hex strings', () => {
+ expect(padHex('abc')).toBe('0abc')
+ })
+
+ it('leaves even-length hex strings unchanged', () => {
+ expect(padHex('abcd')).toBe('abcd')
+ })
+
+ it('pads single character', () => {
+ expect(padHex('f')).toBe('0f')
+ })
+
+ it('leaves empty string unchanged', () => {
+ expect(padHex('')).toBe('')
+ })
+})
+
+describe('countUpwardsHexStr', () => {
+ it('generates counting hex bytes', () => {
+ expect(countUpwardsHexStr(3)).toBe('010203')
+ })
+
+ it('returns empty string for zero', () => {
+ expect(countUpwardsHexStr(0)).toBe('')
+ })
+
+ it('generates single byte', () => {
+ expect(countUpwardsHexStr(1)).toBe('01')
+ })
+
+ it('pads single-digit numbers', () => {
+ expect(countUpwardsHexStr(9)).toBe('010203040506070809')
+ })
+})
+
+describe('dataToValueInput', () => {
+ it('splits data into value fields by byte lengths', () => {
+ const data = ref('aabbccdd')
+ const hexVals = ref(['', ''])
+ const bigIntVals = ref<(bigint | undefined)[]>([undefined, undefined])
+ const byteLengths = ref([2n, 2n])
+
+ dataToValueInput(data, hexVals, bigIntVals, byteLengths)
+
+ expect(hexVals.value).toEqual(['aabb', 'ccdd'])
+ })
+
+ it('updates bigIntVals when defined', () => {
+ const data = ref('00ff')
+ const hexVals = ref([''])
+ const bigIntVals = ref<(bigint | undefined)[]>([0n])
+ const byteLengths = ref([2n])
+
+ dataToValueInput(data, hexVals, bigIntVals, byteLengths)
+
+ expect(bigIntVals.value[0]).toBe(255n)
+ })
+
+ it('leaves bigIntVals undefined when not tracked', () => {
+ const data = ref('aabb')
+ const hexVals = ref([''])
+ const bigIntVals = ref<(bigint | undefined)[]>([undefined])
+ const byteLengths = ref([2n])
+
+ dataToValueInput(data, hexVals, bigIntVals, byteLengths)
+
+ expect(bigIntVals.value[0]).toBeUndefined()
+ })
+
+ it('handles multiple fields with different lengths', () => {
+ const data = ref('aabbccddee')
+ const hexVals = ref(['', '', ''])
+ const bigIntVals = ref<(bigint | undefined)[]>([undefined, undefined, undefined])
+ const byteLengths = ref([1n, 2n, 2n])
+
+ dataToValueInput(data, hexVals, bigIntVals, byteLengths)
+
+ expect(hexVals.value).toEqual(['aa', 'bbcc', 'ddee'])
+ })
+})
+
+describe('valueToDataInput', () => {
+ it('computes byte lengths from hex values', () => {
+ const hexVals = ref(['aabb', 'ccdd'])
+ const bigIntVals = ref<(bigint | undefined)[]>([undefined, undefined])
+ const lengthsMask = ref<(bigint | undefined)[]>([undefined, undefined])
+ const byteLengths = ref([0n, 0n])
+
+ valueToDataInput(hexVals, bigIntVals, lengthsMask, byteLengths)
+
+ expect(byteLengths.value).toEqual([2n, 2n])
+ })
+
+ it('uses length mask when defined', () => {
+ const hexVals = ref(['aabb'])
+ const bigIntVals = ref<(bigint | undefined)[]>([undefined])
+ const lengthsMask = ref<(bigint | undefined)[]>([32n])
+ const byteLengths = ref([0n])
+
+ valueToDataInput(hexVals, bigIntVals, lengthsMask, byteLengths)
+
+ expect(byteLengths.value[0]).toBe(32n)
+ })
+
+ it('updates bigIntVals when defined', () => {
+ const hexVals = ref(['00ff'])
+ const bigIntVals = ref<(bigint | undefined)[]>([0n])
+ const lengthsMask = ref<(bigint | undefined)[]>([undefined])
+ const byteLengths = ref([0n])
+
+ valueToDataInput(hexVals, bigIntVals, lengthsMask, byteLengths)
+
+ expect(bigIntVals.value[0]).toBe(255n)
+ })
+})
diff --git a/src/eComponents/precompileInterfaceEC/run.ts b/src/eComponents/precompileInterfaceEC/run.ts
index dd95ccd..27a1c59 100644
--- a/src/eComponents/precompileInterfaceEC/run.ts
+++ b/src/eComponents/precompileInterfaceEC/run.ts
@@ -1,17 +1,27 @@
-import type { Ref } from 'vue'
import { Common, type Hardfork, Mainnet } from '@ethereumjs/common'
-import { createEVM, type ExecResult, getActivePrecompiles } from '@ethereumjs/evm'
-import { hexToBytes } from '@ethereumjs/util'
+import {
+ createEVM,
+ type ExecResult,
+ getActivePrecompiles,
+ type PrecompileInput,
+} from '@ethereumjs/evm'
+import { createAddressFromString, hexToBytes, type PrefixedHexString } from '@ethereumjs/util'
+
+type PrecompileFunc = (input: PrecompileInput) => Promise | ExecResult
+
+export interface StandardRunResult {
+ pre?: ExecResult
+ post: ExecResult
+}
export async function runPrecompile(
- data: string,
+ data: PrefixedHexString,
preHF: Hardfork,
postHF: Hardfork,
precompile: string,
- execResultPre: Ref,
- execResultPost: Ref,
-) {
+): Promise {
const gasLimit = BigInt(5000000)
+ const dataBytes = hexToBytes(data)
const commonPre = new Common({ chain: Mainnet, hardfork: preHF })
const evmPre = await createEVM({ common: commonPre })
@@ -21,23 +31,69 @@ export async function runPrecompile(
const evmPost = await createEVM({ common: commonPost })
const precompilePost = getActivePrecompiles(commonPost).get(precompile.padStart(40, '0'))!
- // Pre-HF run
+ let pre: ExecResult | undefined
if (precompilePre) {
- const callDataPre = {
- data: hexToBytes(`0x${data}`),
- gasLimit,
- common: commonPre,
- _EVM: evmPre,
- }
- execResultPre.value = await precompilePre(callDataPre)
+ pre = await precompilePre({ data: dataBytes, gasLimit, common: commonPre, _EVM: evmPre })
}
- // Post-HF run
- const callDataPost = {
- data: hexToBytes(`0x${data}`),
+ const post = await precompilePost({
+ data: dataBytes,
gasLimit,
common: commonPost,
_EVM: evmPost,
+ })
+
+ return { pre, post }
+}
+
+/**
+ * Creates a `run` function for the standard pre/post hardfork comparison pattern.
+ * The returned function is compatible with the `PrecompileInterfaceEC` `run` prop
+ * and returns the result directly (captured automatically by the composable).
+ */
+export function useStandardPrecompileRun(
+ preHF: Hardfork,
+ postHF: Hardfork,
+ precompileAddress: string,
+) {
+ async function run(data: PrefixedHexString): Promise {
+ return runPrecompile(data, preHF, postHF, precompileAddress)
}
- execResultPost.value = await precompilePost(callDataPost)
+
+ return { run }
+}
+
+/**
+ * Runs a custom precompile function against the EVM.
+ * Handles all EVM setup boilerplate (Common, EVM instance, call data assembly).
+ *
+ * @param data - `0x`-prefixed hex input data
+ * @param precompileFn - The precompile implementation function
+ * @param address - `0x`-prefixed hex address to register the precompile at
+ * @param hardfork - Hardfork context (defaults to Prague)
+ * @returns The raw `ExecResult` from the precompile execution
+ */
+export async function runCustomPrecompile(
+ data: PrefixedHexString,
+ precompileFn: PrecompileFunc,
+ address: string,
+ hardfork: Hardfork = 'prague' as Hardfork,
+): Promise {
+ const common = new Common({ chain: Mainnet, hardfork })
+ const addr = createAddressFromString(address)
+ const evm = await createEVM({
+ common,
+ customPrecompiles: [{ address: addr, function: precompileFn }],
+ })
+
+ const fn = getActivePrecompiles(common, [{ address: addr, function: precompileFn }]).get(
+ address.slice(2).padStart(40, '0').toLowerCase(),
+ )!
+
+ return fn({
+ data: hexToBytes(data),
+ gasLimit: BigInt(5000000),
+ common,
+ _EVM: evm,
+ })
}
diff --git a/src/eComponents/precompileInterfaceEC/types.ts b/src/eComponents/precompileInterfaceEC/types.ts
index d8fc29a..de3a0d6 100644
--- a/src/eComponents/precompileInterfaceEC/types.ts
+++ b/src/eComponents/precompileInterfaceEC/types.ts
@@ -1,5 +1,3 @@
-import type { Hardfork } from '@ethereumjs/common'
-
export interface PrecompileValueDef {
title: string
urlParam?: string
@@ -11,9 +9,6 @@ export interface PrecompileValueDef {
export interface PrecompileConfig {
explorationId: string
- precompileAddress: string
- preHardfork: Hardfork
- postHardfork: Hardfork
defaultExample: string
showBigInt?: boolean
values: PrecompileValueDef[]
diff --git a/src/eComponents/precompileInterfaceEC/usePrecompileState.ts b/src/eComponents/precompileInterfaceEC/usePrecompileState.ts
index 9af1096..3a0e155 100644
--- a/src/eComponents/precompileInterfaceEC/usePrecompileState.ts
+++ b/src/eComponents/precompileInterfaceEC/usePrecompileState.ts
@@ -1,10 +1,9 @@
-import { computed, ref } from 'vue'
+import { computed, ref, type ShallowRef, shallowRef } from 'vue'
import { useRoute, useRouter } from 'vue-router'
-import type { ExecResult } from '@ethereumjs/evm'
+import type { PrefixedHexString } from '@ethereumjs/util'
import type { Examples } from '@/explorations/REGISTRY'
-import { runPrecompile } from './run'
import type { PrecompileConfig } from './types'
import { dataToValueInput, isValidByteInputForm, valueToDataInput } from './utils'
@@ -18,22 +17,16 @@ function createState(config: PrecompileConfig) {
lengthsMask: ref<(bigint | undefined)[]>(config.values.map((v) => v.expectedLen)),
byteLengths: ref(config.values.map(() => 0n)),
example: ref(''),
- execResultPre: ref(),
- execResultPost: ref(),
}
}
-export function usePrecompileState(config: PrecompileConfig, examples: Examples) {
- const {
- data,
- hexVals,
- bigIntVals,
- lengthsMask,
- byteLengths,
- example,
- execResultPre,
- execResultPost,
- } = createState(config)
+export function usePrecompileState(
+ config: PrecompileConfig,
+ examples: Examples,
+ run: (data: PrefixedHexString) => Promise,
+) {
+ const { data, hexVals, bigIntVals, lengthsMask, byteLengths, example } = createState(config)
+ const result: ShallowRef = shallowRef()
const router = useRouter()
const route = useRoute()
@@ -46,24 +39,13 @@ export function usePrecompileState(config: PrecompileConfig, examples: Examples)
// --- Data conversion ---
- async function run() {
- await runPrecompile(
- data.value,
- config.preHardfork,
- config.postHardfork,
- config.precompileAddress,
- execResultPre,
- execResultPost,
- )
- }
-
async function data2Values() {
if (isValidByteInputForm(data.value).length > 0) return
if (config.parseData) {
config.parseData(data.value, byteLengths.value)
}
dataToValueInput(data, hexVals, bigIntVals, byteLengths)
- await run()
+ result.value = await run(`0x${data.value}`)
}
async function values2Data() {
@@ -76,7 +58,7 @@ export function usePrecompileState(config: PrecompileConfig, examples: Examples)
data.value = config.assembleData
? config.assembleData(hexVals.value, byteLengths.value)
: hexVals.value.join('')
- await run()
+ result.value = await run(`0x${data.value}`)
}
// --- User interaction ---
@@ -136,9 +118,8 @@ export function usePrecompileState(config: PrecompileConfig, examples: Examples)
hexVals,
bigIntVals,
byteLengths,
- execResultPre,
- execResultPost,
inputValues,
+ result,
selectExample,
shareURL,
onDataInputFormChange,
diff --git a/src/eComponents/ui/ButtonUIC.vue b/src/eComponents/ui/ButtonUIC.vue
index ac65f98..42dd52e 100644
--- a/src/eComponents/ui/ButtonUIC.vue
+++ b/src/eComponents/ui/ButtonUIC.vue
@@ -1,10 +1,12 @@
diff --git a/src/explorations/REGISTRY.ts b/src/explorations/REGISTRY.ts
index dfd3f96..34f926a 100644
--- a/src/explorations/REGISTRY.ts
+++ b/src/explorations/REGISTRY.ts
@@ -1,3 +1,4 @@
+import { INFO as customAdditionPrecompile } from './custom-addition-precompile/info'
import { INFO as eip7594 } from './eip-7594/info'
import { INFO as eip7883 } from './eip-7883/info'
import { INFO as eip7951 } from './eip-7951/info'
@@ -6,6 +7,7 @@ export const EXPLORATIONS: Explorations = {
[eip7594.id]: eip7594,
[eip7883.id]: eip7883,
[eip7951.id]: eip7951,
+ [customAdditionPrecompile.id]: customAdditionPrecompile,
}
export interface Examples {
diff --git a/src/explorations/TOPICS.ts b/src/explorations/TOPICS.ts
index ee1a18a..63270a1 100644
--- a/src/explorations/TOPICS.ts
+++ b/src/explorations/TOPICS.ts
@@ -1,39 +1,147 @@
import { getTopicExplorationIds } from './REGISTRY'
-export type TopicColor = 'blue' | 'yellow' | 'green' | 'red'
+export type TopicColor = 'blue' | 'yellow' | 'green' | 'red' | 'orange' | 'purple'
-export const TOPIC_COLOR_CLASSES: Record<
- TopicColor,
- { bg200: string; text900: string; border200: string }
-> = {
- blue: { bg200: 'bg-blue-50', text900: 'text-blue-800', border200: 'border-blue-200' },
- yellow: { bg200: 'bg-yellow-50', text900: 'text-yellow-800', border200: 'border-yellow-200' },
- green: { bg200: 'bg-green-50', text900: 'text-green-800', border200: 'border-green-200' },
- red: { bg200: 'bg-red-50', text900: 'text-red-800', border200: 'border-red-200' },
+interface TopicColorConfig {
+ /**
+ * Static Tailwind utility class strings serving a dual purpose:
+ *
+ * 1. Used as `:class` bindings in templates (e.g. `borderCard` on TopicIntroView).
+ * 2. **Must cover every shade referenced by `topicCSSVars`** so Tailwind's content
+ * scanner emits the corresponding `--color-*` CSS variable. Since `topicCSSVars`
+ * uses string interpolation (invisible to the scanner), the classes here are the
+ * only anchor that keeps those CSS variables alive in the build output.
+ *
+ * Shade coverage required by `topicCSSVars`: 100 (`bg`), 300 (`borderMid`),
+ * 600 (`accent`), 800 (`text`), 900 (`bgDark`). All others are for template use.
+ */
+ classes: {
+ /** Exploration heading text; anchors --color-*-800. */
+ text: string
+ /** Very subtle item background (e.g. inside exploration sidebar). */
+ bgItem: string
+ /** Main exploration card fill; anchors --color-*-100 for --e-bg / --e-bg-medium. */
+ bg: string
+ /** Dark result-box fill; anchors --color-*-900 for --e-bg-dark. */
+ bgDark: string
+ /** Subtle item borders. */
+ border: string
+ /** Standard border on coloured backgrounds; anchors --color-*-300 for --e-border. */
+ borderMid: string
+ /**
+ * Card border on white/neutral page background.
+ * Uses a heavier shade for perceptually-light colours (yellow, green)
+ * so the border is clearly visible against white.
+ */
+ borderCard: string
+ /** Accent / interactive-focus colour; anchors --color-*-600 for --e-accent. */
+ accent: string
+ }
+}
+
+export const TOPIC_COLORS: Record = {
+ blue: {
+ classes: {
+ text: 'text-blue-800',
+ bgItem: 'bg-blue-50',
+ bg: 'bg-blue-100',
+ bgDark: 'bg-blue-900',
+ border: 'border-blue-200',
+ borderMid: 'border-blue-300',
+ borderCard: 'border-blue-300',
+ accent: 'text-blue-600',
+ },
+ },
+ yellow: {
+ classes: {
+ text: 'text-yellow-800',
+ bgItem: 'bg-yellow-50',
+ bg: 'bg-yellow-100',
+ bgDark: 'bg-yellow-900',
+ border: 'border-yellow-200',
+ borderMid: 'border-yellow-300',
+ borderCard: 'border-yellow-500',
+ accent: 'text-yellow-600',
+ },
+ },
+ green: {
+ classes: {
+ text: 'text-green-800',
+ bgItem: 'bg-green-50',
+ bg: 'bg-green-100',
+ bgDark: 'bg-green-900',
+ border: 'border-green-200',
+ borderMid: 'border-green-300',
+ borderCard: 'border-green-400',
+ accent: 'text-green-600',
+ },
+ },
+ red: {
+ classes: {
+ text: 'text-red-800',
+ bgItem: 'bg-red-50',
+ bg: 'bg-red-100',
+ bgDark: 'bg-red-900',
+ border: 'border-red-200',
+ borderMid: 'border-red-300',
+ borderCard: 'border-red-400',
+ accent: 'text-red-600',
+ },
+ },
+ orange: {
+ classes: {
+ text: 'text-orange-800',
+ bgItem: 'bg-orange-50',
+ bg: 'bg-orange-100',
+ bgDark: 'bg-orange-900',
+ border: 'border-orange-200',
+ borderMid: 'border-orange-300',
+ borderCard: 'border-orange-400',
+ accent: 'text-orange-600',
+ },
+ },
+ purple: {
+ classes: {
+ text: 'text-purple-800',
+ bgItem: 'bg-purple-50',
+ bg: 'bg-purple-100',
+ bgDark: 'bg-purple-900',
+ border: 'border-purple-200',
+ borderMid: 'border-purple-300',
+ borderCard: 'border-purple-400',
+ accent: 'text-purple-600',
+ },
+ },
}
/**
- * CSS custom properties for topic-aware styling inside explorations.
- * Set on ExplorationC's root element; inherited by all child components.
+ * Derives a CSS `var(--color-*)` reference from a Tailwind utility class string.
+ * E.g. `'bg-yellow-100'` → `'var(--color-yellow-100)'`.
+ * The class string must end in `-{colorName}-{shade}`.
*/
-const CSS_COLOR_MAP: Record = {
- blue: 'blue',
- yellow: 'yellow',
- green: 'green',
- red: 'red',
+function classToVar(cls: string): string {
+ const match = cls.match(/-([\w]+-\d+)$/)
+ if (!match) throw new Error(`Cannot derive CSS variable from Tailwind class: ${cls}`)
+ return `var(--color-${match[1]})`
}
+/**
+ * CSS custom properties for topic-aware styling inside explorations.
+ * Set on ExplorationC's root element; inherited by all child components.
+ * Values are derived from `TOPIC_COLORS[color].classes` so the scanner-visible
+ * class strings and the runtime CSS vars always stay in sync.
+ */
export function topicCSSVars(color: TopicColor): Record {
- const c = CSS_COLOR_MAP[color]
+ const c = TOPIC_COLORS[color].classes
return {
- '--e-text': `var(--color-${c}-800)`,
- '--e-bg': `var(--color-${c}-100)`,
- '--e-bg-light': `var(--color-white)`,
- '--e-bg-medium': `var(--color-${c}-100)`,
- '--e-bg-dark': `var(--color-${c}-900)`,
- '--e-border': `var(--color-${c}-300)`,
- '--e-border-dark': `var(--color-${c}-800)`,
- '--e-accent': `var(--color-${c}-600)`,
+ '--e-text': classToVar(c.text),
+ '--e-bg': classToVar(c.bg),
+ '--e-bg-light': 'var(--color-white)',
+ '--e-bg-medium': classToVar(c.bg),
+ '--e-bg-dark': classToVar(c.bgDark),
+ '--e-border': classToVar(c.borderMid),
+ '--e-border-dark': classToVar(c.text),
+ '--e-accent': classToVar(c.accent),
}
}
@@ -50,6 +158,16 @@ export const TOPICS: Topics = {
'seamless wallet interactions from modern devices.',
explorations: getTopicExplorationIds('fusaka'),
},
+ research: {
+ title: 'Research',
+ path: '/research',
+ url: 'https://ethereum.org/en/community/research/',
+ color: 'purple',
+ introText:
+ 'Experimental explorations showcasing custom EVM functionality, protocol research ideas, ' +
+ 'and proof-of-concept implementations beyond mainnet hardfork scope.',
+ explorations: getTopicExplorationIds('research'),
+ },
}
export interface Topic {
diff --git a/src/explorations/custom-addition-precompile/MyC.vue b/src/explorations/custom-addition-precompile/MyC.vue
new file mode 100644
index 0000000..e862bda
--- /dev/null
+++ b/src/explorations/custom-addition-precompile/MyC.vue
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+ {{ result.sum }}
+
+
+ {{ bytesToHex(result.execResult.returnValue) }}
+
+
+ Gas used: {{ result.execResult.executionGasUsed }}
+
+
+ Waiting for input...
+
+
+
+
+
diff --git a/src/explorations/custom-addition-precompile/config.ts b/src/explorations/custom-addition-precompile/config.ts
new file mode 100644
index 0000000..420b6f8
--- /dev/null
+++ b/src/explorations/custom-addition-precompile/config.ts
@@ -0,0 +1,10 @@
+import type { PrecompileConfig } from '@/eComponents/precompileInterfaceEC/types'
+
+export const config: PrecompileConfig = {
+ explorationId: 'custom-addition-precompile',
+ defaultExample: 'simple',
+ values: [
+ { title: 'A', urlParam: 'a', expectedLen: 32n },
+ { title: 'B', urlParam: 'b', expectedLen: 32n },
+ ],
+}
diff --git a/src/explorations/custom-addition-precompile/custom/run.ts b/src/explorations/custom-addition-precompile/custom/run.ts
new file mode 100644
index 0000000..88478f6
--- /dev/null
+++ b/src/explorations/custom-addition-precompile/custom/run.ts
@@ -0,0 +1,41 @@
+import { type ExecResult, type PrecompileInput } from '@ethereumjs/evm'
+import {
+ bigIntToBytes,
+ bytesToBigInt,
+ hexToBytes,
+ type PrefixedHexString,
+ setLengthLeft,
+} from '@ethereumjs/util'
+
+import { runCustomPrecompile } from '@/eComponents/precompileInterfaceEC/run'
+
+const CUSTOM_PRECOMPILE_ADDRESS = '0x0000000000000000000000000000000000ff0001'
+const ADDITION_GAS = 15n
+
+function additionPrecompile(input: PrecompileInput): ExecResult {
+ const a = bytesToBigInt(input.data.subarray(0, 32))
+ const b = bytesToBigInt(input.data.subarray(32, 64))
+ const sum = (a + b) % 2n ** 256n
+ return {
+ executionGasUsed: ADDITION_GAS,
+ returnValue: setLengthLeft(bigIntToBytes(sum), 32),
+ }
+}
+
+export interface RunResult {
+ execResult: ExecResult
+ a: bigint
+ b: bigint
+ sum: bigint
+}
+
+export async function run(data: PrefixedHexString): Promise {
+ const execResult = await runCustomPrecompile(data, additionPrecompile, CUSTOM_PRECOMPILE_ADDRESS)
+
+ const dataBytes = hexToBytes(data)
+ const a = bytesToBigInt(dataBytes.subarray(0, 32))
+ const b = bytesToBigInt(dataBytes.subarray(32, 64))
+ const sum = (a + b) % 2n ** 256n
+
+ return { execResult, a, b, sum }
+}
diff --git a/src/explorations/custom-addition-precompile/examples.ts b/src/explorations/custom-addition-precompile/examples.ts
new file mode 100644
index 0000000..f8e5b65
--- /dev/null
+++ b/src/explorations/custom-addition-precompile/examples.ts
@@ -0,0 +1,32 @@
+import type { Examples } from '@/explorations/REGISTRY'
+
+export const examples: Examples = {
+ simple: {
+ title: 'Simple (2 + 3 = 5)',
+ values: [
+ '0000000000000000000000000000000000000000000000000000000000000002',
+ '0000000000000000000000000000000000000000000000000000000000000003',
+ ],
+ },
+ large: {
+ title: 'Large Numbers',
+ values: [
+ '00000000000000000000000000000000000000000000000000000000ffffffff',
+ '0000000000000000000000000000000000000000000000000000000000000001',
+ ],
+ },
+ overflow: {
+ title: 'Overflow (wraps around)',
+ values: [
+ 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff',
+ '0000000000000000000000000000000000000000000000000000000000000001',
+ ],
+ },
+ zero: {
+ title: 'Zero + Zero',
+ values: [
+ '0000000000000000000000000000000000000000000000000000000000000000',
+ '0000000000000000000000000000000000000000000000000000000000000000',
+ ],
+ },
+}
diff --git a/src/explorations/custom-addition-precompile/image.webp b/src/explorations/custom-addition-precompile/image.webp
new file mode 100644
index 0000000..d550afd
Binary files /dev/null and b/src/explorations/custom-addition-precompile/image.webp differ
diff --git a/src/explorations/custom-addition-precompile/info.ts b/src/explorations/custom-addition-precompile/info.ts
new file mode 100644
index 0000000..8153c2f
--- /dev/null
+++ b/src/explorations/custom-addition-precompile/info.ts
@@ -0,0 +1,23 @@
+import type { Exploration } from '@/explorations/REGISTRY'
+
+import image from './image.webp'
+
+export const INFO: Exploration = {
+ id: 'custom-addition-precompile',
+ path: '/custom-addition-precompile',
+ title: 'Custom Addition Precompile',
+ infoURL: 'https://github.com/ethereumjs/ethereumjs-monorepo/tree/master/packages/evm',
+ topic: 'research',
+ image,
+ introText:
+ 'Can you define your own precompile and run it in the EVM? ' +
+ 'This exploration demonstrates the EthereumJS EVM ' +
+ 'custom precompile ' +
+ 'feature. A simple addition precompile is registered at a custom address and executed ' +
+ 'through the standard EVM precompile call path — including gas metering.',
+ usageText:
+ 'Enter two 32-byte unsigned integers (A and B). The custom precompile adds them together ' +
+ 'and returns the 32-byte result. Gas is charged at a flat rate of 15. Try values that ' +
+ 'overflow 256 bits to see wrapping behavior.',
+ poweredBy: [{ name: 'EthereumJS', href: 'https://github.com/ethereumjs/ethereumjs-monorepo' }],
+}
diff --git a/src/explorations/custom-addition-precompile/tests.spec.ts b/src/explorations/custom-addition-precompile/tests.spec.ts
new file mode 100644
index 0000000..371e25a
--- /dev/null
+++ b/src/explorations/custom-addition-precompile/tests.spec.ts
@@ -0,0 +1,95 @@
+import { describe, expect, it } from 'vitest'
+import type { PrefixedHexString } from '@ethereumjs/util'
+
+import { config } from './config'
+import { run } from './custom/run'
+import { examples } from './examples'
+import { INFO } from './info'
+
+describe('Custom Addition Precompile Exploration', () => {
+ describe('info', () => {
+ it('has correct metadata', () => {
+ expect(INFO.id).toBe('custom-addition-precompile')
+ expect(INFO.path).toContain('custom-addition-precompile')
+ expect(INFO.topic).toBe('research')
+ expect(INFO.poweredBy.length).toBeGreaterThan(0)
+ })
+ })
+
+ describe('config', () => {
+ it('references a valid default example', () => {
+ expect(examples[config.defaultExample]).toBeDefined()
+ })
+
+ it('defines 2 value fields (A and B)', () => {
+ expect(config.values).toHaveLength(2)
+ })
+
+ it('both values have fixed 32-byte expected length', () => {
+ for (const val of config.values) {
+ expect(val.expectedLen).toBe(32n)
+ }
+ })
+
+ it('both values have URL params', () => {
+ expect(config.values.map((v) => v.urlParam)).toEqual(['a', 'b'])
+ })
+ })
+
+ describe('examples', () => {
+ it('each example has 2 values', () => {
+ for (const [key, ex] of Object.entries(examples)) {
+ expect(ex.values, `Example "${key}" should have 2 values`).toHaveLength(2)
+ }
+ })
+
+ it('each value is a 64-character hex string', () => {
+ for (const [key, ex] of Object.entries(examples)) {
+ for (const val of ex.values) {
+ expect(val.length, `Value in "${key}" should be 64 chars`).toBe(64)
+ }
+ }
+ })
+
+ it('values contain valid hex characters', () => {
+ const hexRegex = /^[0-9a-f]+$/i
+ for (const [key, ex] of Object.entries(examples)) {
+ for (const val of ex.values) {
+ expect(val, `Value in "${key}" should be valid hex`).toMatch(hexRegex)
+ }
+ }
+ })
+ })
+
+ describe('custom run', () => {
+ it('adds two small numbers (2 + 3 = 5)', async () => {
+ const data =
+ '0x' +
+ '0000000000000000000000000000000000000000000000000000000000000002' +
+ '0000000000000000000000000000000000000000000000000000000000000003'
+ const result = await run(data as PrefixedHexString)
+ expect(result.a).toBe(2n)
+ expect(result.b).toBe(3n)
+ expect(result.sum).toBe(5n)
+ expect(result.execResult.executionGasUsed).toBe(15n)
+ })
+
+ it('handles overflow (wraps mod 2^256)', async () => {
+ const data =
+ '0x' +
+ 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff' +
+ '0000000000000000000000000000000000000000000000000000000000000001'
+ const result = await run(data as PrefixedHexString)
+ expect(result.sum).toBe(0n)
+ })
+
+ it('adds zeros', async () => {
+ const data =
+ '0x' +
+ '0000000000000000000000000000000000000000000000000000000000000000' +
+ '0000000000000000000000000000000000000000000000000000000000000000'
+ const result = await run(data as PrefixedHexString)
+ expect(result.sum).toBe(0n)
+ })
+ })
+})
diff --git a/src/explorations/eip-7594/MyC.vue b/src/explorations/eip-7594/MyC.vue
index 468da53..1445701 100644
--- a/src/explorations/eip-7594/MyC.vue
+++ b/src/explorations/eip-7594/MyC.vue
@@ -1,6 +1,6 @@
-
+
+
+
+
+
diff --git a/src/explorations/eip-7883/config.ts b/src/explorations/eip-7883/config.ts
new file mode 100644
index 0000000..a5bd574
--- /dev/null
+++ b/src/explorations/eip-7883/config.ts
@@ -0,0 +1,29 @@
+import { hexToBigInt } from '@ethereumjs/util'
+
+import type { PrecompileConfig } from '@/eComponents/precompileInterfaceEC/types'
+import { padHex, toHex } from '@/eComponents/precompileInterfaceEC/utils'
+
+export const config: PrecompileConfig = {
+ explorationId: 'eip-7883',
+ defaultExample: 'simple',
+ values: [
+ { title: 'Blen', expectedLen: 32n, initialHex: '00'.repeat(32), showInput: false },
+ { title: 'Elen', expectedLen: 32n, initialHex: '00'.repeat(32), showInput: false },
+ { title: 'Mlen', expectedLen: 32n, initialHex: '00'.repeat(32), showInput: false },
+ { title: 'B', urlParam: 'b' },
+ { title: 'E', urlParam: 'e' },
+ { title: 'M', urlParam: 'm' },
+ ],
+ assembleData: (hexVals, byteLengths) =>
+ toHex(byteLengths[3], 32 * 2) +
+ toHex(byteLengths[4], 32 * 2) +
+ toHex(byteLengths[5], 32 * 2) +
+ padHex(hexVals[3]) +
+ padHex(hexVals[4]) +
+ padHex(hexVals[5]),
+ parseData: (data, byteLengths) => {
+ byteLengths[3] = hexToBigInt(`0x${data.substring(0, 64)}`)
+ byteLengths[4] = hexToBigInt(`0x${data.substring(64, 128)}`)
+ byteLengths[5] = hexToBigInt(`0x${data.substring(128, 192)}`)
+ },
+}
diff --git a/src/explorations/eip-7883/tests.spec.ts b/src/explorations/eip-7883/tests.spec.ts
new file mode 100644
index 0000000..3aadca7
--- /dev/null
+++ b/src/explorations/eip-7883/tests.spec.ts
@@ -0,0 +1,108 @@
+import { describe, expect, it } from 'vitest'
+
+import { config } from './config'
+import { examples } from './examples'
+import { INFO } from './info'
+
+describe('EIP-7883 Exploration', () => {
+ describe('info', () => {
+ it('has correct metadata', () => {
+ expect(INFO.id).toBe('eip-7883')
+ expect(INFO.path).toContain('eip-7883')
+ expect(INFO.topic).toBe('fusaka')
+ expect(INFO.poweredBy.length).toBeGreaterThan(0)
+ })
+ })
+
+ describe('config', () => {
+ it('references a valid default example', () => {
+ expect(examples[config.defaultExample]).toBeDefined()
+ })
+
+ it('defines 6 value fields (3 lengths + 3 editable)', () => {
+ expect(config.values).toHaveLength(6)
+ })
+
+ it('has 3 editable values with urlParams', () => {
+ const editable = config.values.filter((v) => v.urlParam)
+ expect(editable).toHaveLength(3)
+ expect(editable.map((v) => v.urlParam)).toEqual(['b', 'e', 'm'])
+ })
+
+ it('hides length fields from input display', () => {
+ const hidden = config.values.filter((v) => v.showInput === false)
+ expect(hidden).toHaveLength(3)
+ })
+
+ it('assembleData correctly encodes lengths + values', () => {
+ const hexVals = ['', '', '', '03', '03', '02']
+ const byteLengths = [0n, 0n, 0n, 1n, 1n, 1n]
+ const result = config.assembleData!(hexVals, byteLengths)
+ const expected =
+ '0000000000000000000000000000000000000000000000000000000000000001' +
+ '0000000000000000000000000000000000000000000000000000000000000001' +
+ '0000000000000000000000000000000000000000000000000000000000000001' +
+ '030302'
+ expect(result).toBe(expected)
+ })
+
+ it('assembleData pads odd-length hex values', () => {
+ const hexVals = ['', '', '', 'abc', 'de', 'f']
+ const byteLengths = [0n, 0n, 0n, 2n, 1n, 1n]
+ const result = config.assembleData!(hexVals, byteLengths)
+ expect(result).toContain('0abc')
+ expect(result).toContain('0f')
+ })
+
+ it('parseData correctly extracts byte lengths', () => {
+ const data =
+ '0000000000000000000000000000000000000000000000000000000000000001' +
+ '0000000000000000000000000000000000000000000000000000000000000002' +
+ '0000000000000000000000000000000000000000000000000000000000000003'
+ const byteLengths = [0n, 0n, 0n, 0n, 0n, 0n]
+ config.parseData!(data, byteLengths)
+ expect(byteLengths[3]).toBe(1n)
+ expect(byteLengths[4]).toBe(2n)
+ expect(byteLengths[5]).toBe(3n)
+ })
+
+ it('assembleData and parseData are inverse operations for lengths', () => {
+ const hexVals = ['', '', '', '03', '0102030405', '02']
+ const byteLengths = [0n, 0n, 0n, 1n, 5n, 1n]
+ const assembled = config.assembleData!(hexVals, byteLengths)
+
+ const parsedLengths = [0n, 0n, 0n, 0n, 0n, 0n]
+ config.parseData!(assembled, parsedLengths)
+ expect(parsedLengths[3]).toBe(1n)
+ expect(parsedLengths[4]).toBe(5n)
+ expect(parsedLengths[5]).toBe(1n)
+ })
+ })
+
+ describe('examples', () => {
+ it('has at least one example', () => {
+ expect(Object.keys(examples).length).toBeGreaterThan(0)
+ })
+
+ it('each example has 3 values (B, E, M)', () => {
+ for (const [key, ex] of Object.entries(examples)) {
+ expect(ex.values, `Example "${key}" should have 3 values`).toHaveLength(3)
+ }
+ })
+
+ it('each example has a non-empty title', () => {
+ for (const ex of Object.values(examples)) {
+ expect(ex.title.length).toBeGreaterThan(0)
+ }
+ })
+
+ it('example values contain valid hex characters', () => {
+ const hexRegex = /^[0-9a-f]+$/i
+ for (const [key, ex] of Object.entries(examples)) {
+ for (const val of ex.values) {
+ expect(val, `Value in "${key}" should be valid hex`).toMatch(hexRegex)
+ }
+ }
+ })
+ })
+})
diff --git a/src/explorations/eip-7951/MyC.vue b/src/explorations/eip-7951/MyC.vue
index b2c99f2..2b929ce 100644
--- a/src/explorations/eip-7951/MyC.vue
+++ b/src/explorations/eip-7951/MyC.vue
@@ -2,28 +2,28 @@
import { Hardfork } from '@ethereumjs/common'
import PrecompileInterfaceEC from '@/eComponents/precompileInterfaceEC/PrecompileInterfaceEC.vue'
-import type { PrecompileConfig } from '@/eComponents/precompileInterfaceEC/types'
+import PrecompileInterfaceResultEC from '@/eComponents/precompileInterfaceEC/PrecompileInterfaceResultEC.vue'
+import { useStandardPrecompileRun } from '@/eComponents/precompileInterfaceEC/run'
+import { config } from './config'
import { examples } from './examples'
import { INFO as exploration } from './info'
-const config: PrecompileConfig = {
- explorationId: 'eip-7951',
- precompileAddress: '100',
- preHardfork: Hardfork.Prague,
- postHardfork: Hardfork.Osaka,
- defaultExample: 'valid',
- showBigInt: false,
- values: [
- { title: 'Hash', urlParam: 'hash', expectedLen: 32n },
- { title: 'SigR', urlParam: 'sigr', expectedLen: 32n },
- { title: 'SigS', urlParam: 'sigs', expectedLen: 32n },
- { title: 'PubX', urlParam: 'pubx', expectedLen: 32n },
- { title: 'PubY', urlParam: 'puby', expectedLen: 32n },
- ],
-}
+const { run } = useStandardPrecompileRun(Hardfork.Prague, Hardfork.Osaka, '100')
-
+
+
+
+
+
diff --git a/src/explorations/eip-7951/config.ts b/src/explorations/eip-7951/config.ts
new file mode 100644
index 0000000..6486b3d
--- /dev/null
+++ b/src/explorations/eip-7951/config.ts
@@ -0,0 +1,14 @@
+import type { PrecompileConfig } from '@/eComponents/precompileInterfaceEC/types'
+
+export const config: PrecompileConfig = {
+ explorationId: 'eip-7951',
+ defaultExample: 'valid',
+ showBigInt: false,
+ values: [
+ { title: 'Hash', urlParam: 'hash', expectedLen: 32n },
+ { title: 'SigR', urlParam: 'sigr', expectedLen: 32n },
+ { title: 'SigS', urlParam: 'sigs', expectedLen: 32n },
+ { title: 'PubX', urlParam: 'pubx', expectedLen: 32n },
+ { title: 'PubY', urlParam: 'puby', expectedLen: 32n },
+ ],
+}
diff --git a/src/explorations/eip-7951/tests.spec.ts b/src/explorations/eip-7951/tests.spec.ts
new file mode 100644
index 0000000..b937b6c
--- /dev/null
+++ b/src/explorations/eip-7951/tests.spec.ts
@@ -0,0 +1,82 @@
+import { describe, expect, it } from 'vitest'
+
+import { config } from './config'
+import { examples } from './examples'
+import { INFO } from './info'
+
+describe('EIP-7951 Exploration', () => {
+ describe('info', () => {
+ it('has correct metadata', () => {
+ expect(INFO.id).toBe('eip-7951')
+ expect(INFO.path).toContain('eip-7951')
+ expect(INFO.topic).toBe('fusaka')
+ expect(INFO.poweredBy.length).toBeGreaterThan(0)
+ })
+ })
+
+ describe('config', () => {
+ it('references a valid default example', () => {
+ expect(examples[config.defaultExample]).toBeDefined()
+ })
+
+ it('defines 5 value fields', () => {
+ expect(config.values).toHaveLength(5)
+ })
+
+ it('all values have fixed 32-byte expected length', () => {
+ for (const val of config.values) {
+ expect(val.expectedLen).toBe(32n)
+ }
+ })
+
+ it('all values have URL params', () => {
+ for (const val of config.values) {
+ expect(val.urlParam).toBeDefined()
+ }
+ })
+
+ it('does not define custom assembleData or parseData', () => {
+ expect(config.assembleData).toBeUndefined()
+ expect(config.parseData).toBeUndefined()
+ })
+
+ it('hides bigInt display', () => {
+ expect(config.showBigInt).toBe(false)
+ })
+ })
+
+ describe('examples', () => {
+ it('has at least one example', () => {
+ expect(Object.keys(examples).length).toBeGreaterThan(0)
+ })
+
+ it('each example has 5 values matching the 5 config fields', () => {
+ for (const [key, ex] of Object.entries(examples)) {
+ expect(ex.values, `Example "${key}" should have 5 values`).toHaveLength(5)
+ }
+ })
+
+ it('each value is a 64-character hex string (32 bytes)', () => {
+ for (const [key, ex] of Object.entries(examples)) {
+ for (const val of ex.values) {
+ expect(val.length, `Value in "${key}" should be 64 chars`).toBe(64)
+ }
+ }
+ })
+
+ it('example values contain valid hex characters', () => {
+ const hexRegex = /^[0-9a-f]+$/i
+ for (const [key, ex] of Object.entries(examples)) {
+ for (const val of ex.values) {
+ expect(val, `Value in "${key}" should be valid hex`).toMatch(hexRegex)
+ }
+ }
+ })
+
+ it('includes both valid and invalid signature examples', () => {
+ const titles = Object.values(examples).map((e) => e.title.toLowerCase())
+ expect(titles.some((t) => t.includes('valid') && !t.includes('invalid'))).toBe(true)
+ expect(titles.some((t) => t.includes('invalid'))).toBe(true)
+ })
+ })
+})
diff --git a/src/main.css b/src/main.css
index be07fa5..d61619a 100644
--- a/src/main.css
+++ b/src/main.css
@@ -114,7 +114,7 @@ body {
/* --- Result boxes (comparison panels) --- */
.e-result-box {
background-color: var(--e-bg-dark);
- @apply rounded-md p-3;
+ @apply rounded-md p-3 text-white;
a {
@apply text-white underline;
@@ -233,7 +233,9 @@ body {
transform: translateY(0);
}
-/* --- Topic intro cards (gradient left border) --- */
+/* --- Topic intro cards --- */
+/* Border colour is applied as a Tailwind utility via TOPIC_CARD_BORDER in TopicIntroView.vue */
.topic-intro-card {
- border: 1px solid var(--e-border);
+ border-width: 1px;
+ border-style: solid;
}
diff --git a/src/views/ExplorationView.vue b/src/views/ExplorationView.vue
index 243f9a2..1cda698 100644
--- a/src/views/ExplorationView.vue
+++ b/src/views/ExplorationView.vue
@@ -3,14 +3,14 @@ import { defineAsyncComponent } from 'vue'
import { useRoute } from 'vue-router'
import { EXPLORATIONS } from '@/explorations/REGISTRY'
-import { TOPIC_COLOR_CLASSES, TOPICS } from '@/explorations/TOPICS'
+import { TOPIC_COLORS, TOPICS } from '@/explorations/TOPICS'
import TopicIntroView from './TopicIntroView.vue'
const route = useRoute()
const explorationId = route.name as string
const exploration = EXPLORATIONS[explorationId]
-const cc = TOPIC_COLOR_CLASSES[TOPICS[exploration.topic].color]
+const cc = TOPIC_COLORS[TOPICS[exploration.topic].color].classes
const componentModules = import.meta.glob('../explorations/*/MyC.vue')
const ExplorationComponent = defineAsyncComponent(
@@ -27,7 +27,7 @@ const ExplorationComponent = defineAsyncComponent(
- Loading...
diff --git a/src/views/TopicIntroView.vue b/src/views/TopicIntroView.vue
index ae9c783..7f345e3 100644
--- a/src/views/TopicIntroView.vue
+++ b/src/views/TopicIntroView.vue
@@ -1,5 +1,5 @@
-