Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(human-readable): parameter validation and strict mode #138

Closed
wants to merge 24 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
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
6 changes: 6 additions & 0 deletions .changeset/cuddly-donkeys-protect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"abitype": minor
---

Adds parameter validation to `parseAbiParameter` and `parseAbiParameters`.
Changes `StrictAbiType` to `Strict`.
14 changes: 14 additions & 0 deletions docs/api/human.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ title: 'Human-Readable ABI'
Human-Readable ABIs compress [JSON ABIs](https://docs.soliditylang.org/en/latest/abi-spec.html#json) into signatures that are nicer to read and less verbose to write. For example:

::: code-group

```ts [human-readable.ts]
const abi = [
'constructor()',
Expand Down Expand Up @@ -51,6 +52,7 @@ const abi = [
{ inputs: [], name: 'ApprovalCallerNotOwnerNorApproved', type: 'error' },
] as const
```

:::

ABIType contains parallel [type-level](/api/human#types) and [runtime](/api/human#utilities) utilities for parsing and formatting human-readable ABIs, ABI items, and ABI parameters.
Expand Down Expand Up @@ -477,6 +479,10 @@ const abiParameterStruct = parseAbiParameter([
])
```

::: info PARAMETER VALIDATION
When passing a string array as an argument typescript will verify if the parameter strings are valid to safeguard you from the runtime behavior. When strict is disabled the type-checking will be more lax. If you enable strict it will check if the Solidity types are valid.
:::

### `parseAbiParameters`

Parses human-readable ABI parameters into [`AbiParameter`s](/api/types#abiparameter).
Expand All @@ -503,6 +509,13 @@ const abiParametersStruct = parseAbiParameters([
])
```

<<<<<<< HEAD
::: info PARAMETER VALIDATION
When passing a string array as an argument typescript will verify if the parameter strings are valid to safeguard you from the runtime behavior. When strict is disabled the type-checking will be more lax. If you enable strict it will check if the Solidity types are valid.
:::

||||||| 24256d0
=======
### `formatAbi`

Formats [`Abi`](/api/types#abi) into human-readable ABI.
Expand Down Expand Up @@ -602,6 +615,7 @@ const result = formatAbiParameters([
```


>>>>>>> main
## Errors

```ts twoslash
Expand Down
6 changes: 3 additions & 3 deletions docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,17 +151,17 @@ declare module 'abitype' {
}
```

### `StrictAbiType`
### `Strict`

When set, validates `AbiParameter`'s `type` against `AbiType`.
When set, validates `AbiParameter`'s `type` against `AbiType` and enforces stricter checks on human readable ABIs.

- Type `boolean`
- Default `false`

```ts twoslash
declare module 'abitype' {
export interface Config {
StrictAbiType: false
Strict: false
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

With this change it will affect type checking not only on types but also valid modifiers and parameter names. Kept as default so folks can hop in instead of out.

}
}
```
Expand Down
114 changes: 107 additions & 7 deletions src/abi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@ export type Address = ResolvedConfig['AddressType']
// Could use `Range`, but listed out for zero overhead
// rome-ignore format: no formatting
export type MBytes =
| '' | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
| '' | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
| 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19
| 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29
| 30 | 31 | 32
// rome-ignore format: no formatting
export type MBits =
| '' | 8 | 16 | 24 | 32 | 40 | 48 | 56 | 64 | 72
| 80 | 88 | 96 | 104 | 112 | 120 | 128 | 136 | 144 | 152
| '' | 8 | 16 | 24 | 32 | 40 | 48 | 56 | 64 | 72
| 80 | 88 | 96 | 104 | 112 | 120 | 128 | 136 | 144 | 152
| 160 | 168 | 176 | 184 | 192 | 200 | 208 | 216 | 224 | 232
| 240 | 248 | 256

Expand Down Expand Up @@ -84,16 +84,16 @@ export type AbiType =
| SolidityInt
| SolidityString
| SolidityTuple
type ResolvedAbiType = ResolvedConfig['StrictAbiType'] extends true
? AbiType
type ResolvedAbiType = ResolvedConfig['Strict'] extends true
? AbiType & string
: string

export type AbiInternalType =
| ResolvedAbiType
| `address ${string}`
| `contract ${string}`
| `enum ${string}`
| `struct ${string}`
| `enum${string}`
| `struct${string}`

export type AbiParameter = Pretty<
{
Expand Down Expand Up @@ -247,3 +247,103 @@ export type TypedData = Pretty<
[_ in TypedDataType]?: never | undefined
}
>

////////////////////////////////////////////////////////////////////////////////////////////////////
// Inferred Abi Types

export type InferredAbiParameter = Pretty<
{
type: string
name?: string | undefined
/** Representation used by Solidity compiler */
internalType?: string | undefined
} & (
| { type: string }
| {
type: 'tuple' | `tuple[${string}]`
components: readonly InferredAbiParameter[]
}
)
>

export type InferredAbiEventParameter = Pretty<
InferredAbiParameter & { indexed?: boolean | undefined }
>

/** Typescript inferred ABI ["function"](https://docs.soliditylang.org/en/latest/abi-spec.html#json) type */
export type InferredAbiFunction = {
type: string
/**
* @deprecated use `pure` or `view` from {@link AbiStateMutability} instead
* @see https://github.com/ethereum/solidity/issues/992
*/
constant?: boolean | undefined
/**
* @deprecated Vyper used to provide gas estimates
* @see https://github.com/vyperlang/vyper/issues/2151
*/
gas?: number | undefined
inputs: readonly InferredAbiParameter[]
name: string
outputs: readonly InferredAbiParameter[]
/**
* @deprecated use `payable` or `nonpayable` from {@link AbiStateMutability} instead
* @see https://github.com/ethereum/solidity/issues/992
*/
payable?: boolean | undefined
stateMutability: string
}

/** Typescript inferred ABI ["constructor"](https://docs.soliditylang.org/en/latest/abi-spec.html#json) type */
export type InferredAbiConstructor = {
type: string
inputs: readonly InferredAbiParameter[]
/**
* @deprecated use `payable` or `nonpayable` from {@link AbiStateMutability} instead
* @see https://github.com/ethereum/solidity/issues/992
*/
payable?: boolean | undefined
stateMutability: string
}

/** Typescript inferred ABI ["fallback"](https://docs.soliditylang.org/en/latest/abi-spec.html#json) type */
export type InferredAbiFallback = {
type: string
inputs?: [] | undefined
/**
* @deprecated use `payable` or `nonpayable` from {@link AbiStateMutability} instead
* @see https://github.com/ethereum/solidity/issues/992
*/
payable?: boolean | undefined
stateMutability: string
}

/** Typescript inferred ABI ["receive"](https://docs.soliditylang.org/en/latest/contracts.html#receive-ether-function) type */
export type InferredAbiReceive = {
type: string
stateMutability: string
}

/** Typescript inferred ABI ["event"](https://docs.soliditylang.org/en/latest/abi-spec.html#events) type */
export type InferredAbiEvent = {
type: string
anonymous?: boolean | undefined
inputs: readonly InferredAbiEventParameter[]
name: string
}

/** Typescript inferred ABI ["error"](https://docs.soliditylang.org/en/latest/abi-spec.html#errors) type */
export type InferredAbiError = {
type: string
inputs: readonly InferredAbiParameter[]
name: string
}

export type InferredAbi = readonly (
| InferredAbiReceive
| InferredAbiFallback
| InferredAbiConstructor
| InferredAbiError
| InferredAbiEvent
| InferredAbiFunction
)[]
8 changes: 4 additions & 4 deletions src/config.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import { assertType, test } from 'vitest'
import type { ResolvedConfig } from './config.js'

// For testing updates to config properties:
// declare module './config' {
// declare module './config.js' {
// export interface Config {
// FixedArrayMaxLength: 6
// Strict: true
// }
// }

Expand All @@ -29,6 +29,6 @@ test('Config', () => {
type BigIntType = ResolvedConfig['BigIntType']
assertType<BigIntType>(123n)

type StrictAbiType = ResolvedConfig['StrictAbiType']
assertType<StrictAbiType>(false)
type Strict = ResolvedConfig['Strict']
assertType<Strict>(false)
})
14 changes: 7 additions & 7 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ export interface DefaultConfig {
/** TypeScript type to use for `int<M>` and `uint<M>` values, where `M <= 48` */
IntType: number

/** When set, validates {@link AbiParameter}'s `type` against {@link AbiType} */
StrictAbiType: false
/** When set, validates {@link AbiParameter}'s `type` against {@link AbiType} and enforces stricter checks on human readable ABIs */
Strict: false
}

/**
Expand Down Expand Up @@ -110,14 +110,14 @@ export interface ResolvedConfig {
: Config['IntType']

/**
* When set, validates {@link AbiParameter}'s `type` against {@link AbiType}
* When set, validates {@link AbiParameter}'s `type` against {@link AbiType} and enforces stricter checks on human readable ABIs
*
* Note: You probably only want to set this to `true` if parsed types are returning as `unknown`
* and you want to figure out why.
* and you want to figure out why or if you want stricter validation on your types.
*
* @default false
*/
StrictAbiType: Config['StrictAbiType'] extends true
? Config['StrictAbiType']
: DefaultConfig['StrictAbiType']
Strict: Config['Strict'] extends true
? Config['Strict']
: DefaultConfig['Strict']
}
4 changes: 2 additions & 2 deletions src/human-readable/errors/abiParameter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,15 +115,15 @@ test('InvalidFunctionModifierError', () => {
test('InvalidAbiTypeParameterError', () => {
expect(
new InvalidAbiTypeParameterError({
abiParameter: { type: 'addres' },
abiParameter: { type: 'address' },
}),
).toMatchInlineSnapshot(`
[InvalidAbiTypeParameterError: Invalid ABI parameter.

ABI parameter type is invalid.

Details: {
"type": "addres"
"type": "address"
}
Version: [email protected]]
`)
Expand Down
4 changes: 2 additions & 2 deletions src/human-readable/errors/abiParameter.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { AbiItemType, AbiParameter } from '../../abi.js'
import type { AbiItemType, InferredAbiEventParameter } from '../../abi.js'
import { BaseError } from '../../errors.js'
import type { Modifier } from '../types/signatures.js'

Expand Down Expand Up @@ -100,7 +100,7 @@ export class InvalidAbiTypeParameterError extends BaseError {
constructor({
abiParameter,
}: {
abiParameter: AbiParameter & { indexed?: boolean | undefined }
abiParameter: InferredAbiEventParameter
}) {
super('Invalid ABI parameter.', {
details: JSON.stringify(abiParameter, null, 2),
Expand Down
4 changes: 2 additions & 2 deletions src/human-readable/formatAbiParameter.test-d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { expectTypeOf, test } from 'vitest'

import type { AbiParameter } from '../abi.js'
import type { InferredAbiParameter } from '../abi.js'

import type { FormatAbiParameter } from './formatAbiParameter.js'
import { formatAbiParameter } from './formatAbiParameter.js'
Expand Down Expand Up @@ -57,7 +57,7 @@ test('formatAbiParameter', () => {
).toEqualTypeOf<'(string)'>()

const param = { type: 'address' }
const param2: AbiParameter = param
const param2: InferredAbiParameter = param
expectTypeOf(formatAbiParameter(param)).toEqualTypeOf<string>()
expectTypeOf(formatAbiParameter(param2)).toEqualTypeOf<string>()
})
10 changes: 7 additions & 3 deletions src/human-readable/formatAbiParameter.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import type { AbiEventParameter, AbiParameter } from '../abi.js'
import type {
AbiParameter,
InferredAbiEventParameter,
InferredAbiParameter,
} from '../abi.js'
import { execTyped } from '../regex.js'
import type { Join } from '../types.js'

Expand All @@ -13,7 +17,7 @@ import type { Join } from '../types.js'
* // ^? type Result = 'address from'
*/
export type FormatAbiParameter<
TAbiParameter extends AbiParameter | AbiEventParameter,
TAbiParameter extends InferredAbiParameter | InferredAbiParameter,
> = TAbiParameter extends {
name?: infer Name extends string
type: `tuple${infer Array}`
Expand Down Expand Up @@ -51,7 +55,7 @@ const tupleRegex = /^tuple(?<array>(\[(\d*)\])*)$/
* // ^? const result: 'address from'
*/
export function formatAbiParameter<
const TAbiParameter extends AbiParameter | AbiEventParameter,
const TAbiParameter extends InferredAbiParameter | InferredAbiEventParameter,
>(abiParameter: TAbiParameter): FormatAbiParameter<TAbiParameter> {
type Result = FormatAbiParameter<TAbiParameter>

Expand Down
4 changes: 2 additions & 2 deletions src/human-readable/formatAbiParameters.test-d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { expectTypeOf, test } from 'vitest'

import type { AbiParameter } from '../abi.js'
import type { InferredAbiParameter } from '../abi.js'

import type { FormatAbiParameters } from './formatAbiParameters.js'
import { formatAbiParameters } from './formatAbiParameters.js'
Expand Down Expand Up @@ -78,7 +78,7 @@ test('formatAbiParameter', () => {
).toEqualTypeOf<'(string)'>()

const param = { type: 'address' }
const param2: AbiParameter = param
const param2: InferredAbiParameter = param

expectTypeOf(formatAbiParameters([param])).toEqualTypeOf<string>()

Expand Down
10 changes: 5 additions & 5 deletions src/human-readable/formatAbiParameters.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { AbiEventParameter, AbiParameter } from '../abi.js'
import type { InferredAbiEventParameter, InferredAbiParameter } from '../abi.js'
import type { Join } from '../types.js'
import {
type FormatAbiParameter,
Expand All @@ -20,8 +20,8 @@ import {
*/
export type FormatAbiParameters<
TAbiParameters extends readonly [
AbiParameter | AbiEventParameter,
...(readonly (AbiParameter | AbiEventParameter)[]),
InferredAbiParameter | InferredAbiEventParameter,
...(readonly (InferredAbiParameter | InferredAbiEventParameter)[]),
],
> = Join<
{
Expand All @@ -45,8 +45,8 @@ export type FormatAbiParameters<
*/
export function formatAbiParameters<
const TAbiParameters extends readonly [
AbiParameter | AbiEventParameter,
...(readonly (AbiParameter | AbiEventParameter)[]),
InferredAbiParameter | InferredAbiEventParameter,
...(readonly (InferredAbiParameter | InferredAbiEventParameter)[]),
],
>(abiParameters: TAbiParameters): FormatAbiParameters<TAbiParameters> {
let params = ''
Expand Down
Loading