Skip to content

Commit

Permalink
feat: ABI parser (#3089)
Browse files Browse the repository at this point in the history
* feat: ABI parser

* remove casts

* remove unnecessary throw

* centralize abi cleanup logic

* remove rawUntypedPtr from swayTypeMatchers

* rename method

* Update cleanup-abi.ts

* Update cleanup-abi.ts

* refactor abi type mappers

* refactor into using maps for types

* refactor from array of tuples into `Map`

* rename variables

* refactorings, comments

* split test up into multiple tests

---------

Co-authored-by: Peter Smith <[email protected]>
  • Loading branch information
nedsalk and petertonysmith94 authored Jan 22, 2025
1 parent 6962abb commit 44f6590
Show file tree
Hide file tree
Showing 38 changed files with 2,858 additions and 18 deletions.
1 change: 0 additions & 1 deletion .changeset/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
"baseBranch": "master",
"updateInternalDependencies": "patch",
"ignore": [
"@fuel-ts/abi",
"fuel-gauge",
"docs",
"demo-fuels",
Expand Down
7 changes: 7 additions & 0 deletions .changeset/tender-tigers-fry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@fuel-ts/abi": patch
"fuels": patch
"@fuel-ts/errors": patch
---

feat: ABI parser
3 changes: 3 additions & 0 deletions apps/docs-api/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@

# Modules

<!-- TODO: uncomment once deployed -->
<!-- - [abi](https://fuels-ts-docs-api.vercel.app/modules/_fuel_ts_abi.html) -->

- [abi-coder](https://fuels-ts-docs-api.vercel.app/modules/_fuel_ts_abi_coder.html)
- [abi-typegen](https://fuels-ts-docs-api.vercel.app/modules/_fuel_ts_abi_typegen.html)
- [account](https://fuels-ts-docs-api.vercel.app/modules/_fuel_ts_account.html)
Expand Down
1 change: 1 addition & 0 deletions apps/docs-api/typedoc.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"$schema": "https://typedoc.org/schema.json",
"entryPointStrategy": "packages",
"entryPoints": [
"../../packages/abi",
"../../packages/abi-coder",
"../../packages/abi-typegen",
"../../packages/address",
Expand Down
4 changes: 4 additions & 0 deletions apps/docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,10 @@ export default defineConfig({
text: 'Optimized React Example',
link: '/guide/cookbook/optimized-react-example',
},
{
text: 'Working with the ABI',
link: '/guide/cookbook/working-with-the-abi',
},
],
},
{
Expand Down
3 changes: 2 additions & 1 deletion apps/docs/spell-check-custom-words.txt
Original file line number Diff line number Diff line change
Expand Up @@ -343,4 +343,5 @@ Workspaces
WSL
XOR
XORs
matcher
YAML
matcher
9 changes: 9 additions & 0 deletions apps/docs/src/guide/cookbook/snippets/parsing-the-abi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// #region full
import { AbiParser } from 'fuels';
import type { Abi, AbiSpecificationV1 } from 'fuels';

import { Counter } from '../../../typegend';

const parsedAbi: Abi = AbiParser.parse(Counter.abi as AbiSpecificationV1);
// #endregion full
console.log('Parsed ABI:', parsedAbi);
11 changes: 11 additions & 0 deletions apps/docs/src/guide/cookbook/working-with-the-abi.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Working with the ABI

Building a Sway program with `forc build` outputs multiple files, one of which is a JSON representation of the program's ABI. Because ABI specifications can change from one `forc` version to another, working directly with the ABI is cumbersome due to having to manage all ABI specification versions in order to ensure proper functionality.

<!-- TODO: fix links once it's live -->
<!-- AbiParser: https://fuels-ts-docs-api.vercel.app/classes/_fuel_ts_abi.AbiParser.html-->
<!-- ABI: https://fuels-ts-docs-api.vercel.app/interfaces/_fuel_ts_abi.Abi.html -->

To mitigate this, The SDK provides [`AbiParser`](#working-with-the-abi) which can parse all ABI specification versions and output an object that conforms to the [`Abi`](#working-with-the-abi) interface. The SDK also internally uses this `Abi` interface for implementing its encoding/decoding and TS type generation.

<<< @./snippets/parsing-the-abi.ts#full{ts:line-numbers}
6 changes: 6 additions & 0 deletions apps/docs/src/guide/errors/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ When the arguments supplied to the function do not match the minimum required in

Check that the arguments supplied to the function match the required type.

### `ABI_SPECIFICATION_INVALID`

When the ABI specification provided is invalid.

Check that the ABI specification is valid.

### `ACCOUNT_REQUIRED`

When an [`Account`](https://fuels-ts-docs-api.vercel.app/classes/_fuel_ts_account.Account.html) is required for an operation. This will usually be in the form of a [`Wallet`](../wallets/index.md).
Expand Down
6 changes: 6 additions & 0 deletions internal/check-imports/src/references.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,16 @@ import {
arrayify,
hexlify,
createConfig,
AbiParser,
} from 'fuels';

const { log } = console;

/**
* abi
*/
log(AbiParser);

/**
* abi-coder
*/
Expand Down
3 changes: 2 additions & 1 deletion packages/abi/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
"postbuild": "tsx ../../scripts/postbuild.ts"
},
"dependencies": {
"@fuel-ts/errors": "workspace:*"
"@fuel-ts/errors": "workspace:*",
"@fuel-ts/utils": "workspace:*"
},
"devDependencies": {}
}
1 change: 1 addition & 0 deletions packages/abi/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './coder';
export * from './gen';
export * from './parser';
9 changes: 0 additions & 9 deletions packages/abi/src/matchers/sway-type-matchers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ const testMappings: Record<keyof typeof swayTypeMatchers, `${string}-matched`> =
array: 'array-matched',
assetId: 'assetId-matched',
evmAddress: 'evmAddress-matched',
rawUntypedPtr: 'rawUntypedPtr-matched',
rawUntypedSlice: 'rawUntypedSlice-matched',
str: 'str-matched',
};
Expand Down Expand Up @@ -231,14 +230,6 @@ describe('sway type matchers', () => {
await verifyOtherMatchersDontMatch(key, swayType);
});

test('rawUntypedPtr', async () => {
const key = 'rawUntypedPtr';
const swayType = 'raw untyped ptr';

expect(matcher({ swayType })).toEqual(`${key}-matched`);
await verifyOtherMatchersDontMatch(key, swayType);
});

test('rawUntypedSlice', async () => {
const key = 'rawUntypedSlice';
const swayType = 'raw untyped slice';
Expand Down
3 changes: 0 additions & 3 deletions packages/abi/src/matchers/sway-type-matchers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ export type SwayType =
| 'array'
| 'assetId'
| 'evmAddress'
| 'rawUntypedPtr'
| 'rawUntypedSlice';

type Matcher = (type: string) => boolean;
Expand Down Expand Up @@ -69,7 +68,6 @@ const result: Matcher = (type) => type === 'enum std::result::Result';
export const ENUM_REGEX = /^enum (.+::)?(?<name>.+)$/m;
const enumMatcher: Matcher = (type) => !option(type) && !result(type) && ENUM_REGEX.test(type);

const rawUntypedPtr: Matcher = (type) => type === 'raw untyped ptr';
const rawUntypedSlice: Matcher = (type) => type === 'raw untyped slice';

export const swayTypeMatchers: Record<SwayType, Matcher> = {
Expand Down Expand Up @@ -100,7 +98,6 @@ export const swayTypeMatchers: Record<SwayType, Matcher> = {
option,
result,

rawUntypedPtr,
rawUntypedSlice,
};

Expand Down
1 change: 0 additions & 1 deletion packages/abi/src/parse/AbiParser.ts

This file was deleted.

1 change: 0 additions & 1 deletion packages/abi/src/parse/specifications/v1/index.ts

This file was deleted.

1 change: 0 additions & 1 deletion packages/abi/src/parse/types.ts

This file was deleted.

44 changes: 44 additions & 0 deletions packages/abi/src/parser/abi-parser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { FuelError } from '@fuel-ts/errors';

import type { Abi } from './abi';
import type { AbiSpecificationV1 } from './specifications';
import { AbiParserV1 } from './specifications';

/**
* A typed ABI object or a stringified json of a Sway program's ABI
*/
export type AbiSpecification = AbiSpecificationV1;

export class AbiParser {
/**
* ABI specifications transpilers
*/
private static specifications = {
'1': AbiParserV1.parse,
} as const;

/**
* Parses an ABI in JSON format.
*
* @param abi - a JSON ABI of a Sway program
* @returns an public interface for the Abi
*/
static parse(abi: AbiSpecification): Abi {
if (typeof abi.specVersion !== 'string') {
throw new FuelError(
FuelError.CODES.ABI_SPECIFICATION_INVALID,
'Invalid ABI: the specification version is not a string.'
);
}

const parse = AbiParser.specifications[abi.specVersion];
if (!parse) {
throw new FuelError(
FuelError.CODES.ABI_SPECIFICATION_INVALID,
`Invalid ABI: Unsupported ABI specification version ("${abi.specVersion}").`
);
}

return parse(abi);
}
}
142 changes: 142 additions & 0 deletions packages/abi/src/parser/abi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
/**
* This interface serves as a representation of the ABI format outputted by `forc build`
* that won't be changing with the introduction of new abi specifications in Sway.
* Its purpose is to provide a stable interface for users to work with,
* which won't be affected by changing ABI specification versions.
*/
export interface Abi {
encodingVersion: string;
programType: 'contract' | 'predicate' | 'script' | 'library';
/**
* Metadata types describe the structure of the types used in the `concreteTypes` field.
* One metadata type can be referenced multiple times if it is used in multiple concrete types.
*/
metadataTypes: AbiMetadataType[];
/**
* Concrete types are types that are used in:
* function inputs/outputs, configurables, logged types, or message types.
*
* Their structure is fully known and they do not contain any unresolved generic parameters.
*/
concreteTypes: AbiConcreteType[];
functions: AbiFunction[];
loggedTypes: AbiLoggedType[];
messageTypes: AbiMessageType[];
configurables: AbiConfigurable[];
}

export interface AbiConcreteType {
swayType: string;
concreteTypeId: string;
/**
* The components field is populated when the type is any non-primitive type.
* That includes structs, enums, arrays, and tuples.
*/
components?: AbiTypeComponent[];
/**
* A concrete type can be an implementation of a metadata type,
* in which case the `metadata` field is populated.
* If the underlying metadata type has type parameters (is generic),
* the `typeArguments` field corresponds to those type parameters.
*/
metadata?: {
metadataTypeId: number;
/**
* Type arguments used to resolve the type parameters in the metadata type.
* They are ordered in the same way as the type parameters in the metadata type.
*/
typeArguments?: AbiConcreteType[];
};
}

export interface AbiMetadataType {
swayType: string;
metadataTypeId: number;
/**
* The components field is populated when the type is any non-primitive type.
* That includes structs, enums, arrays, and tuples.
*/
components?: AbiTypeComponent[];
/**
* The existence of type parameters indicates that the metadata type is generic.
*/
typeParameters?: AbiMetadataType[];
}

export interface AbiTypeComponent {
name: string;
type: AbiConcreteType | AbiAppliedMetadataType;
}

/**
* AbiAppliedMetadataType point to a metadata type but aren't the same as metadata types,
* as the metadata type describes the structure of the type,
* whereas the component is an actual implementation of that type.
*/
export interface AbiAppliedMetadataType {
swayType: string;
components?: AbiTypeComponent[];
metadata: {
metadataTypeId: number;
typeArguments?: AbiTypeArgument[];
};
}

export type AbiTypeArgument = AbiConcreteType | AbiAppliedMetadataType;

export interface AbiFunctionInput {
name: string;
type: AbiConcreteType;
}

export interface AbiFunction {
name: string;
inputs: AbiFunctionInput[];
output: AbiConcreteType;
attributes?: readonly AbiFunctionAttribute[];
}

export interface AbiLoggedType {
logId: string;
type: AbiConcreteType;
}

export interface AbiMessageType {
messageId: string;
type: AbiConcreteType;
}

export interface AbiConfigurable {
name: string;
offset: number;
type: AbiConcreteType;
}

export type AbiFunctionAttribute =
| StorageAttr
| PayableAttr
| TestAttr
| InlineAttr
| DocCommentAttr;

export interface PayableAttr {
readonly name: 'payable';
}

export interface StorageAttr {
readonly name: 'storage';
readonly arguments: readonly ('read' | 'write')[];
}

export interface TestAttr {
readonly name: 'test';
}
export interface InlineAttr {
readonly name: 'inline';
readonly arguments: 'never' | 'always';
}

export interface DocCommentAttr {
readonly name: 'doc-comment';
readonly arguments: readonly string[];
}
3 changes: 3 additions & 0 deletions packages/abi/src/parser/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { AbiParser, type AbiSpecification } from './abi-parser';
export * from './abi';
export * from './specifications/v1/specification';
2 changes: 2 additions & 0 deletions packages/abi/src/parser/specifications/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { AbiParserV1 } from './v1/parser';
export * from './v1/specification';
Loading

0 comments on commit 44f6590

Please sign in to comment.