Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/cyan-plants-taste.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'accounts': patch
---

Added `error` and `requireFunds` capabilities to `eth_fillTransaction` relay responses.
6 changes: 3 additions & 3 deletions .changeset/social-keys-travel.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
"accounts": minor
'accounts': minor
---

**Breaking:** Removed `Handler.feePayer`.
Updated `Handler.relay` to be backwards compatible with `Handler.feePayer`.
Removed `Handler.feePayer`.
Updated `Handler.relay` to be backwards compatible with `Handler.feePayer`.
8 changes: 1 addition & 7 deletions playgrounds/web/wrangler.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,6 @@
"main": "./worker/index.ts",
"assets": {
"not_found_handling": "single-page-application",
"run_worker_first": [
"/cli-auth/*",
"/webauthn/*",
"/relay",
"/fortune",
"/zero-dollar-auth",
],
"run_worker_first": ["/cli-auth/*", "/webauthn/*", "/relay", "/fortune", "/zero-dollar-auth"],
},
}
413 changes: 329 additions & 84 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion pnpm-workspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ catalog:
tsx: ^4.21.0
typed-query-selector: ^2.12.1
typescript: ^5.9.3
viem: ^2.47.15
viem: ^2.47.18
vite: ^8.0.5
vp: npm:vite-plus@~0.1.14
wagmi: ^3.5.0
Expand Down
205 changes: 205 additions & 0 deletions src/core/ExecutionError.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
import { describe, expect, test } from 'vp/test'

import * as ExecutionError from './ExecutionError.js'

// ABI-encoded revert data fixtures.
const insufficientBalanceData =
'0x832f98b500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005f5e10000000000000000000000000020c0000000000000000000000000000000000001' as const
const unauthorizedData = '0x82b42900' as const
const tokenAlreadyExistsData =
'0x15ef3a5700000000000000000000000020c0000000000000000000000000000000000001' as const

describe('parse', () => {
test('decodes InsufficientBalance from revert data', () => {
const error = Object.assign(new Error('reverted'), {
data: insufficientBalanceData,
})
const result = ExecutionError.parse(error)
expect(result).toMatchInlineSnapshot(`
{
"abiItem": {
"inputs": [
{
"name": "available",
"type": "uint256",
},
{
"name": "required",
"type": "uint256",
},
{
"name": "token",
"type": "address",
},
],
"name": "InsufficientBalance",
"type": "error",
},
"args": [
0n,
100000000n,
"0x20C0000000000000000000000000000000000001",
],
"data": "0x832f98b500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005f5e10000000000000000000000000020c0000000000000000000000000000000000001",
"errorName": "InsufficientBalance",
"message": "Insufficient balance. Required: 100000000, available: 0.",
}
`)
})

test('decodes Unauthorized (no args)', () => {
const error = Object.assign(new Error('reverted'), {
data: unauthorizedData,
})
const result = ExecutionError.parse(error)
expect(result.errorName).toBe('Unauthorized')
expect(result.message).toBe('Unauthorized.')
})

test('decodes TokenAlreadyExists with templated message', () => {
const error = Object.assign(new Error('reverted'), {
data: tokenAlreadyExistsData,
})
const result = ExecutionError.parse(error)
expect(result.errorName).toBe('TokenAlreadyExists')
expect(result.message).toBe('Token 0x20C0000000000000000000000000000000000001 already exists.')
})

test('extracts revert data from nested cause', () => {
const inner = Object.assign(new Error('inner'), {
data: unauthorizedData,
})
const error = Object.assign(new Error('outer'), { cause: inner })
const result = ExecutionError.parse(error)
expect(result.errorName).toBe('Unauthorized')
})

test('extracts revert data from nested error property', () => {
const inner = Object.assign(new Error('inner'), {
data: insufficientBalanceData,
})
const error = Object.assign(new Error('outer'), { error: inner })
const result = ExecutionError.parse(error)
expect(result.errorName).toBe('InsufficientBalance')
})

test('extracts revert data via walk method', () => {
const inner = { data: unauthorizedData }
const error = Object.assign(new Error('walkable'), {
walk: (fn: (e: unknown) => boolean) => (fn(inner) ? inner : null),
})
const result = ExecutionError.parse(error)
expect(result.errorName).toBe('Unauthorized')
})

test('fallback: extracts error name from human-readable revert message', () => {
const error = new Error('execution reverted: Unauthorized(something)')
const result = ExecutionError.parse(error)
expect(result.errorName).toBe('unknown')
expect(result.message).toBe('Unauthorized.')
})

test('fallback: uses details property', () => {
const error = Object.assign(new Error('ignored'), {
details: 'execution reverted: Unauthorized(x)',
})
const result = ExecutionError.parse(error)
expect(result.errorName).toBe('unknown')
expect(result.message).toBe('Unauthorized.')
})

test('fallback: uses shortMessage property', () => {
const error = Object.assign(new Error('ignored'), {
shortMessage: 'execution reverted: Unauthorized(x)',
})
const result = ExecutionError.parse(error)
expect(result.errorName).toBe('unknown')
expect(result.message).toBe('Unauthorized.')
})

test('unknown error: returns raw message', () => {
const error = new Error('something went wrong')
const result = ExecutionError.parse(error)
expect(result.message).toBe('something went wrong')
expect(result.errorName).toBe('unknown')
})

test('unknown error: strips "execution reverted:" prefix', () => {
const error = new Error('execution reverted: mystery failure')
const result = ExecutionError.parse(error)
expect(result.message).toBe('mystery failure')
})

test('undecodable revert data falls back to raw message', () => {
const error = Object.assign(new Error('bad revert'), {
data: '0xdeadbeef',
})
const result = ExecutionError.parse(error)
expect(result.message).toBe('bad revert')
expect(result.errorName).toBe('unknown')
})
})

describe('serialize', () => {
test('serializes InsufficientBalance', () => {
const parsed = ExecutionError.parse(
Object.assign(new Error(''), { data: insufficientBalanceData }),
)
const serialized = ExecutionError.serialize(parsed)
expect(serialized).toMatchInlineSnapshot(`
{
"abiItem": {
"inputs": [
{
"name": "available",
"type": "uint256",
},
{
"name": "required",
"type": "uint256",
},
{
"name": "token",
"type": "address",
},
],
"name": "InsufficientBalance",
"type": "error",
},
"data": "0x832f98b500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005f5e10000000000000000000000000020c0000000000000000000000000000000000001",
"errorName": "InsufficientBalance",
"message": "Insufficient balance. Required: 100000000, available: 0.",
}
`)
})

test('serializes unknown error', () => {
const parsed = ExecutionError.parse(new Error('boom'))
const serialized = ExecutionError.serialize(parsed)
expect(serialized).toMatchInlineSnapshot(`
{
"errorName": "unknown",
"message": "boom",
}
`)
})

test('serializes error with no args', () => {
const parsed = ExecutionError.parse(Object.assign(new Error(''), { data: unauthorizedData }))
const serialized = ExecutionError.serialize(parsed)
expect(serialized.errorName).toBe('Unauthorized')
})
})

describe('messages', () => {
test('all messages end with a period', () => {
for (const [name, msg] of Object.entries(ExecutionError.messages))
expect(msg, `${name} message should end with "."`).toMatch(/\.$/)
})

test('templated messages contain placeholders', () => {
expect(ExecutionError.messages.InsufficientBalance).toContain('{0}')
expect(ExecutionError.messages.InsufficientBalance).toContain('{1}')
expect(ExecutionError.messages.TokenAlreadyExists).toContain('{0}')
})
})
Loading
Loading