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: ABI coder #3402

Open
wants to merge 24 commits into
base: ns/feat/abi-parser
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
0c4ed32
feat: ABI coder
petertonysmith94 Dec 13, 2024
70a93d3
Merge branch 'ns/feat/abi-parser' into ps/feat/abi-coder
nedsalk Dec 15, 2024
b210bec
fix function signature for bytes
nedsalk Dec 15, 2024
7d437fe
remove rawUntypedPtr from swayTypeMatchers
nedsalk Dec 15, 2024
bb4577e
Merge branch 'ns/feat/abi-parser' into ps/feat/abi-coder
nedsalk Dec 15, 2024
601960f
Merge branch 'ns/feat/abi-parser' into ps/feat/abi-coder
nedsalk Dec 15, 2024
c8aff8a
remove rawUntypedPtr
nedsalk Dec 15, 2024
5020a8a
Merge branch 'ns/feat/abi-parser' into ps/feat/abi-coder
nedsalk Dec 16, 2024
f86ec06
fix lint
nedsalk Dec 16, 2024
5102b63
Merge branch 'ns/feat/abi-parser' into ps/feat/abi-coder
nedsalk Dec 17, 2024
63a4c58
Merge branch 'ns/feat/abi-parser' into ps/feat/abi-coder
nedsalk Dec 17, 2024
b6b5c2e
refactor out function
nedsalk Dec 17, 2024
b590e20
fix comments, add comments
nedsalk Dec 17, 2024
3a6b861
Merge branch 'ns/feat/abi-parser' into ps/feat/abi-coder
nedsalk Dec 17, 2024
6862b09
Merge branch 'ns/feat/abi-parser' into ps/feat/abi-coder
nedsalk Dec 31, 2024
9837e3c
Merge branch 'ns/feat/abi-parser' into ps/feat/abi-coder
nedsalk Jan 2, 2025
de84957
Merge branch 'ns/feat/abi-parser' of github.com:FuelLabs/fuels-ts int…
petertonysmith94 Jan 8, 2025
e352512
docs: update variable
petertonysmith94 Jan 8, 2025
0ad91bf
chore: applying unsafe integer validation
petertonysmith94 Jan 8, 2025
7162712
chore: refactored coder matchers
petertonysmith94 Jan 8, 2025
71ce5fa
chore: perform renames
petertonysmith94 Jan 8, 2025
b755501
chore: fix package version
petertonysmith94 Jan 8, 2025
53676b4
chore: missing lock file
petertonysmith94 Jan 9, 2025
d30db1d
Merge branch 'ns/feat/abi-parser' into ps/feat/abi-coder
petertonysmith94 Jan 13, 2025
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
2 changes: 2 additions & 0 deletions .changeset/nice-books-tease.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
arboleya marked this conversation as resolved.
Show resolved Hide resolved
12 changes: 6 additions & 6 deletions apps/docs/src/guide/encoding/encode-and-decode.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
# Encode and Decode

To interact with the FuelVM, types must be encoded and decoded per the [argument encoding specification](https://docs.fuel.network/docs/specs/abi/argument-encoding/). The SDK provides the `Interface` class to encode and decode data.
To interact with the FuelVM, types must be encoded and decoded per the [argument encoding specification](https://docs.fuel.network/docs/specs/abi/argument-encoding/). The SDK provides the `AbiCoder` class to encode and decode data.

The relevant methods of `Interface` are:
To encode and decode types, the `AbiCoder` class provides the `getType` method which returns a `AbiCoderType` instance that provides the following methods:

- `encodeType`
- `decodeType`
- `encode`
- `decode`

The `Interface` class requires you to pass the [ABI](https://docs.fuel.network/docs/specs/abi/json-abi-format/) on initialization. Both methods accept a `concreteTypeId`, which must exist in the ABI's `concreteTypes` array. After that, a suitable coder will be assigned to encode/decode that type.
The `AbiCoder` class requires you to pass the [ABI](https://docs.fuel.network/docs/specs/abi/json-abi-format/) on initialization. Both methods accept a `concreteTypeId`, which must exist in the ABI's `concreteTypes` array. After that, a suitable coder will be assigned to encode/decode that type.
petertonysmith94 marked this conversation as resolved.
Show resolved Hide resolved

Imagine we are working with the following script that returns the sum of two `u32` integers:

Expand All @@ -23,7 +23,7 @@ It will produce the following ABI:

<<< @./snippets/encode-and-decode.jsonc#encode-and-decode-2{json:line-numbers}

Now, let's prepare some data to pass to the `main` function to retrieve the combined integer. The function expects and returns a `u32` integer. So here, we will encode the `u32` to pass it to the function and receive the same `u32` back, as bytes, that we'll use for decoding. We can do both of these with the `Interface`.
Now, let's prepare some data to pass to the `main` function to retrieve the combined integer. The function expects and returns a `u32` integer. So here, we will encode the `u32` to pass it to the function and receive the same `u32` back, as bytes, that we'll use for decoding. We can do both of these with the `AbiCoder`.

First, let's prepare the transaction:

Expand Down
26 changes: 16 additions & 10 deletions apps/docs/src/guide/encoding/snippets/encode-and-decode.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
// #region full
import type { JsonAbi, TransactionResultReturnDataReceipt } from 'fuels';
import type {
AbiSpecification,
TransactionResultReturnDataReceipt,
} from 'fuels';
import {
buildFunctionResult,
ReceiptType,
arrayify,
Script,
Interface,
AbiCoder,
Provider,
Wallet,
} from 'fuels';
Expand All @@ -20,7 +23,7 @@ const wallet = Wallet.fromPrivateKey(WALLET_PVT_KEY, provider);

// First we need to build out the transaction via the script that we want to encode.
// For that we'll need the ABI and the bytecode of the script
const abi: JsonAbi = ScriptSum.abi;
const abi: AbiSpecification = ScriptSum.abi;
Copy link
Member

Choose a reason for hiding this comment

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

This may need to be against the parser instead, but the flow from ABISpecification -> ABI -> coder/gen isn't completely clear to me. So I can still use the coder and gen without normalising the ABI?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This should probably be added to the parser documentation, but the general flow is:

External interface:

  • The forc compiler generates the AbiSpecification
  • This specification is the common format used across the Fuel ecosystem
  • External tools (explorer, forc, etc) all reference this specification format so we keep this as the source of truth

Internal interface:

  • The AbiParser acts as a translation layer
  • It converts the specification into a normalized Abi format
  • This abstraction isolates specification changes to just the parser
  • Both the coder and generator components work with this normalized format internally

cc @nedsalk

Copy link
Member

Choose a reason for hiding this comment

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

Makes sense, please include in #3089

Copy link
Contributor Author

@petertonysmith94 petertonysmith94 Dec 12, 2024

Choose a reason for hiding this comment

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

I've added the following comment, and will add to the PR description as items to do.

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure what documentation to add around this in #3089. The AbiParser itself works directly with the specification. What could perhaps be done is that the AbiCoder can take both the specification and Abi interface in its constructor, which'd clear up the possible confusion.

const bytecode = ScriptSum.bytecode;

// Create the invocation scope for the script call, passing the initial
Expand All @@ -43,13 +46,13 @@ const argument = abi.functions
.find((f) => f.name === 'main')
?.inputs.find((i) => i.name === 'inputted_amount')?.concreteTypeId as string;

// The `Interface` class (imported from `fuels`) is the entry point for encoding and decoding all things abi-related.
// We will use its `encodeType` method and create the encoding required for
// a u32 which takes 4 bytes up of property space.
// The `AbiCoder` class (imported from `fuels`) is the entry point for encoding and decoding all things abi-related.
// We will use its `getType` method to get the coder for the argument and then call its `encode` method to
// create the encoding required for a u32 which takes 4 bytes up of property space.

const abiInterface = new Interface(abi);
const abiCoder = AbiCoder.fromAbi(abi);
const argumentToAdd = 10;
const encodedArguments = abiInterface.encodeType(argument, [argumentToAdd]);
const encodedArguments = abiCoder.getType(argument).encode([argumentToAdd]);
// Therefore the value of 10 will be encoded to:
// Uint8Array([0, 0, 0, 10]

Expand Down Expand Up @@ -91,10 +94,13 @@ const returnData = arrayify(returnDataReceipt.data);
// returnData = new Uint8Array([0, 0, 0, 20]

// And now we can decode the returned bytes in a similar fashion to how they were
// encoded, via the `Interface`
const [decodedReturnData] = abiInterface.decodeType(argument, returnData);
// encoded, via the `AbiCoder`
const decodedReturnData = abiCoder.getType(argument).decode(returnData);
// 20

const totalValue = argumentToAdd + initialValue;
// #endregion encode-and-decode-5
// #endregion full

console.log('decodedReturnData should be 20', decodedReturnData === 20);
console.log('totalValue should be 20', totalValue === 20);
129 changes: 78 additions & 51 deletions apps/docs/src/guide/encoding/snippets/working-with-bytes.ts
Original file line number Diff line number Diff line change
@@ -1,88 +1,115 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
// #region full
import { randomBytes } from 'crypto';
import {
ArrayCoder,
B256Coder,
B512Coder,
BigNumberCoder,
BooleanCoder,
EnumCoder,
NumberCoder,
RawSliceCoder,
StdStringCoder,
StringCoder,
StructCoder,
TupleCoder,
VecCoder,
hexlify,
} from 'fuels';
import { encoding, hexlify } from 'fuels';

// #region working-with-bytes-1
const u8Coder = new NumberCoder('u8');
const encodedU8 = u8Coder.encode(255);
const encodedU8 = encoding.u8.encode(255);

const u16Coder = new NumberCoder('u16');
const encodedU16 = u16Coder.encode(255);
const encodedU16 = encoding.u16.encode(255);

const u32Coder = new NumberCoder('u32');
const encodedU32 = u32Coder.encode(255);
const encodedU32 = encoding.u32.encode(255);

const u64Coder = new BigNumberCoder('u64');
const encodedU64 = u64Coder.encode(255);
const encodedU64 = encoding.u64.encode(255);

const u256Coder = new BigNumberCoder('u256');
const encodedU256 = u256Coder.encode(255);
const encodedU256 = encoding.u256.encode(255);
// #endregion working-with-bytes-1

// #region working-with-bytes-2
const booleanCoder = new BooleanCoder();
const encodedTrue = booleanCoder.encode(true);

const encodedFalse = booleanCoder.encode(false);
const encodedTrue = encoding.bool.encode(true);

const encodedFalse = encoding.bool.encode(false);
// #endregion working-with-bytes-2

// #region working-with-bytes-3
const stringCoder = new StringCoder(5);
const encoded = stringCoder.encode('hello');
const stringCoder = encoding.string(5);
const encodedString = stringCoder.encode('hello');
// #endregion working-with-bytes-3

// #region working-with-bytes-4
const b256Coder = new B256Coder();
const encodedB256 = b256Coder.encode(hexlify(randomBytes(32)));
const b512Coder = new B512Coder();
const encodedB512 = b512Coder.encode(hexlify(randomBytes(64)));
const encodedB256 = encoding.b256.encode(hexlify(randomBytes(32)));

const encodedB512 = encoding.b512.encode(hexlify(randomBytes(64)));
// #endregion working-with-bytes-4

// #region working-with-bytes-5
const tupleCoder = new TupleCoder([
new NumberCoder('u8'),
new NumberCoder('u16'),
]);
const tupleCoder = encoding.tuple([encoding.u8, encoding.u16]);
const encodedTuple = tupleCoder.encode([255, 255]);

const structCoder = new StructCoder('struct', {
a: new NumberCoder('u8'),
b: new NumberCoder('u16'),
const structCoder = encoding.struct({
a: encoding.u8,
b: encoding.u16,
});
const encodedStruct = structCoder.encode({ a: 255, b: 255 });

const arrayCoder = new ArrayCoder(new NumberCoder('u8'), 4);
const arrayCoder = encoding.array(encoding.u8, 4);
const encodedArray = arrayCoder.encode([255, 0, 255, 0]);

const enumCoder = new EnumCoder('enum', { a: new NumberCoder('u32') });
const enumCoder = encoding.enum({ a: encoding.u32 });
const encodedEnum = enumCoder.encode({ a: 255 });
// #endregion working-with-bytes-5

// #region working-with-bytes-6
const vecCoder = new VecCoder(new NumberCoder('u8'));
const vecCoder = encoding.vector(encoding.u8);
const encodedVec = vecCoder.encode([255, 0, 255]);

const stdStringCoder = new StdStringCoder();
const encodedStdString = stdStringCoder.encode('hello');
const encodedStdString = encoding.stdString.encode('hello');

const rawSliceCoder = new RawSliceCoder();
const encodedRawSlice = rawSliceCoder.encode([1, 2, 3, 4]);
const encodedRawSlice = encoding.rawSlice.encode([1, 2, 3, 4]);
// #endregion working-with-bytes-6
// #endregion full

console.log('encodedU8 should be [255]', encodedU8.toString() === '255');
console.log('encodedU16 should be [0, 255]', encodedU16.toString() === '0,255');
console.log(
'encodedU32 should be [0, 0, 0, 255]',
encodedU32.toString() === '0,0,0,255'
);
console.log(
'encodedU64 should be [0, 0, 0, 0, 0, 0, 0, 255]',
encodedU64.toString() === '0,0,0,0,0,0,0,255'
);
console.log(
'encodedU256 should be [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255]',
encodedU256.toString() ===
'0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255'
);

console.log('encodedTrue should be [1]', encodedTrue.toString() === '1');
console.log('encodedFalse should be [0]', encodedFalse.toString() === '0');

console.log(
'encodedString should be [104, 101, 108, 108, 111]',
encodedString.toString() === '104,101,108,108,111'
);

console.log('encodedB256 should be 32', encodedB256.length === 32);
console.log('encodedB512 should be 64', encodedB512.length === 64);

console.log(
'encodedTuple should be [255, 0, 255]',
encodedTuple.toString() === '255,0,255'
);
console.log(
'encodedStruct should be [255, 0, 255]',
encodedStruct.toString() === '255,0,255'
);
console.log(
'encodedArray should be [255, 0, 255, 0]',
encodedArray.toString() === '255,0,255,0'
);
console.log(
'encodedEnum should be [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255]',
encodedEnum.toString() === '0,0,0,0,0,0,0,0,0,0,0,255'
);
console.log(
'encodedVec should be [0, 0, 0, 0, 0, 0, 0, 3, 255, 0, 255]',
encodedVec.toString() === '0,0,0,0,0,0,0,3,255,0,255'
);
console.log(
'encodedStdString should be [0, 0, 0, 0, 0, 0, 0, 5, 104, 101, 108, 108, 111]',
encodedStdString.toString() === '0,0,0,0,0,0,0,5,104,101,108,108,111'
);
console.log(
'encodedRawSlice should be [0, 0, 0, 0, 0, 0, 0, 4, 1, 2, 3, 4]',
encodedRawSlice.toString() === '0,0,0,0,0,0,0,4,1,2,3,4'
);
2 changes: 1 addition & 1 deletion apps/docs/src/guide/encoding/working-with-bytes.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ This guide aims to give a high-level overview of how to work with bytes in the S

We know the sizes of all core types at compile time. They are the building blocks of the more complex types and are the most common types you will encounter.

### Unsigned Integer (`u8` / `u16` / `u32` / `u64` / `u128` / `u256`)
### Unsigned Integer (`u8` / `u16` / `u32` / `u64` / `u256`)

Each type will only contain the number of bits specified in the name. For example, a `u8` will contain 8 bits, and a `u256` will contain 256 bits and take up the exact property space with no additional padding.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,9 @@ const scriptRequest = new ScriptRequest(
throw new Error('fail');
}

const [decodedResult] = script.interface.functions.main.decodeOutput(
return script.interface.functions.main.decodeOutput(
scriptResult.returnReceipt.data
);
return decodedResult;
}
);
// #endregion script-init
Expand Down
1 change: 1 addition & 0 deletions internal/check-imports/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
},
"license": "Apache-2.0",
"dependencies": {
"@fuel-ts/abi": "workspace:*",
"@fuel-ts/abi-coder": "workspace:*",
"@fuel-ts/abi-typegen": "workspace:*",
"@fuel-ts/address": "workspace:*",
Expand Down
2 changes: 2 additions & 0 deletions internal/check-imports/src/imports.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import * as abi from '@fuel-ts/abi';
import * as abiCoder from '@fuel-ts/abi-coder';
import * as abiTypegen from '@fuel-ts/abi-typegen';
import * as account from '@fuel-ts/account';
Expand All @@ -20,6 +21,7 @@ import * as fuels from 'fuels';
const { log } = console;

log([
abi,
abiCoder,
abiTypegen,
address,
Expand Down
5 changes: 5 additions & 0 deletions internal/check-imports/src/references.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { AbiCoder, encoding } from '@fuel-ts/abi';
import { Interface, StringCoder } from '@fuel-ts/abi-coder';
import { AbiTypeGen } from '@fuel-ts/abi-typegen';
import { runCliAction } from '@fuel-ts/abi-typegen/cli';
Expand Down Expand Up @@ -45,6 +46,10 @@ const { log } = console;
/**
* abi
*/
log(AbiCoder);
log(encoding);
log(encoding.v1.string);
log(encoding.v1.string(8));
log(AbiParser);

/**
Expand Down
2 changes: 1 addition & 1 deletion packages/abi-coder/src/FunctionFragment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export class FunctionFragment {
return new TupleCoder(coders).encode(argumentValues);
}

decodeArguments(data: BytesLike) {
decodeArguments(data: BytesLike): unknown[] | undefined {
const bytes = arrayify(data);
const nonVoidInputs = findNonVoidInputs(this.jsonAbiOld, this.jsonFnOld.inputs);

Expand Down
2 changes: 1 addition & 1 deletion packages/abi-coder/src/encoding/coders/B256Coder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ describe('B256Coder', () => {

const coder = new B256Coder();

it('should encode zero as a 256 bit hash string', () => {
it('should encode [zero] as a 256 bit hash string', () => {
const expected = B256_ZERO_ENCODED;
const actual = coder.encode(B256_ZERO_DECODED);

Expand Down
12 changes: 6 additions & 6 deletions packages/abi-coder/src/encoding/coders/OptionCoder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,36 +8,36 @@ import { VoidCoder } from './VoidCoder';
*/
describe('OptionCoder', () => {
const coder = new OptionCoder('std::option::Option', {
Some: new NumberCoder('u8'),
None: new VoidCoder(),
Some: new NumberCoder('u8'),
});

describe('encode', () => {
it('should encode a Some value', () => {
const encoded = coder.encode(100);

const expected = Uint8Array.from([0, 0, 0, 0, 0, 0, 0, 0, 100]);
const expected = Uint8Array.from([0, 0, 0, 0, 0, 0, 0, 1, 100]);
expect(encoded).toEqual(expected);
});

it('should encode a None value', () => {
const encoded = coder.encode(undefined);

const expected = Uint8Array.from([0, 0, 0, 0, 0, 0, 0, 1]);
const expected = Uint8Array.from([0, 0, 0, 0, 0, 0, 0, 0]);
expect(encoded).toEqual(expected);
});

it('should encode a None value [optional]', () => {
const encoded = coder.encode();

const expected = Uint8Array.from([0, 0, 0, 0, 0, 0, 0, 1]);
const expected = Uint8Array.from([0, 0, 0, 0, 0, 0, 0, 0]);
expect(encoded).toEqual(expected);
});
});

describe('decode', () => {
it('should decode a Some value', () => {
const input = Uint8Array.from([0, 0, 0, 0, 0, 0, 0, 0, 100]);
const input = Uint8Array.from([0, 0, 0, 0, 0, 0, 0, 1, 100]);
const expected = [100, 9];

const decoded = coder.decode(input, 0);
Expand All @@ -46,7 +46,7 @@ describe('OptionCoder', () => {
});

it('should decode a None value', () => {
const input = Uint8Array.from([0, 0, 0, 0, 0, 0, 0, 1]);
const input = Uint8Array.from([0, 0, 0, 0, 0, 0, 0, 0]);
const expected = [undefined, 8];

const decoded = coder.decode(input, 0);
Expand Down
Loading
Loading