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 + + + +``` + +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 @@