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

chore!: support new encoding schema #2826

Closed
wants to merge 33 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
9bea8da
squash all changes into one commit
nedsalk Jul 25, 2024
90e770a
fix generic configurables
nedsalk Jul 25, 2024
9de7a60
move from methods to fields
nedsalk Jul 26, 2024
73189e3
rename fields
nedsalk Jul 26, 2024
4aed148
Rename `ResolvableMetadataType` to `ResolvableType`
nedsalk Jul 26, 2024
2d20852
add spec validation
nedsalk Jul 26, 2024
bab9581
Make `metadataType` private
nedsalk Jul 26, 2024
818cf57
use only `ResolvedType` with `AbiCoder` and rest
nedsalk Jul 26, 2024
d3db561
Merge remote-tracking branch 'origin/master' into ns/new-encoding-con…
nedsalk Jul 26, 2024
5e77a5e
simplify `getFunctionSignature`
nedsalk Jul 26, 2024
d5fb298
move `getFunctionSelector` into `functionSignatureUtils`
nedsalk Jul 26, 2024
e6cc1df
simplify type parsing in `Abi`
nedsalk Jul 26, 2024
bcadd20
fix: codeql struct regex
nedsalk Jul 26, 2024
dae1946
fix: codeql enum regex
nedsalk Jul 26, 2024
7cdbbd9
fix: codeql array regex
nedsalk Jul 26, 2024
ddc259b
fix: decoding logs
nedsalk Jul 26, 2024
61bab02
fix comment
nedsalk Jul 26, 2024
c961194
add Resolvable/ResolvedComponent interfaces
nedsalk Jul 26, 2024
0f4dd1a
rename variable
nedsalk Jul 26, 2024
0500627
put metadataType handling into `handleMetadataType`
nedsalk Jul 26, 2024
7e0c424
reorder methods
nedsalk Jul 26, 2024
42316f1
cleaner ternary
nedsalk Jul 26, 2024
ca552b8
cleaner resolve
nedsalk Jul 26, 2024
a5f7426
cleaner fields and constructor
nedsalk Jul 26, 2024
675f28f
simplify by putting type declaration into labels
nedsalk Jul 26, 2024
97e653b
put functionality into test util
nedsalk Jul 26, 2024
fbd64bd
narrow `programType`
nedsalk Jul 26, 2024
5ed2872
Merge branch 'master' into ns/new-encoding-concept
nedsalk Jul 26, 2024
fb274c5
Merge remote-tracking branch 'origin/master' into ns/new-encoding-con…
nedsalk Jul 29, 2024
773a72a
ignore sway-repo in forc checks
nedsalk Jul 29, 2024
9007b12
create `makeTestType` helper
nedsalk Jul 29, 2024
1f273e7
Merge remote-tracking branch 'origin/master' into ns/new-encoding-con…
nedsalk Jul 29, 2024
803bde8
remove leftover
nedsalk Jul 29, 2024
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
7 changes: 7 additions & 0 deletions .changeset/shy-dodos-occur.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@fuel-ts/abi-typegen": minor
"@fuel-ts/abi-coder": minor
"fuels": minor
---

chore!: support new encoding schema
3 changes: 3 additions & 0 deletions apps/demo-typegen/contract/Forc.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,6 @@ license = "Apache-2.0"
name = "demo-contract"

[dependencies]

[patch.'https://github.com/fuellabs/sway']
std = { git = "https://github.com/fuellabs/sway", branch = "esdrubal/abi_changes" }
3 changes: 3 additions & 0 deletions apps/demo-typegen/predicate/Forc.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,6 @@ license = "Apache-2.0"
name = "predicate"

[dependencies]

[patch.'https://github.com/fuellabs/sway']
std = { git = "https://github.com/fuellabs/sway", branch = "esdrubal/abi_changes" }
3 changes: 3 additions & 0 deletions apps/demo-typegen/script/Forc.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,6 @@ license = "Apache-2.0"
name = "script"

[dependencies]

[patch.'https://github.com/fuellabs/sway']
std = { git = "https://github.com/fuellabs/sway", branch = "esdrubal/abi_changes" }
23 changes: 8 additions & 15 deletions apps/docs-snippets/src/guide/encoding/encode-and-decode.test.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import {
FUEL_NETWORK_URL,
Provider,
AbiCoder,
Script,
ReceiptType,
arrayify,
buildFunctionResult,
} from 'fuels';
import type { Account, JsonAbi, JsonAbiArgument, TransactionResultReturnDataReceipt } from 'fuels';
import type { Account, JsonAbi, TransactionResultReturnDataReceipt } from 'fuels';
import { generateTestWallet } from 'fuels/test-utils';

import abiSnippet from '../../../test/fixtures/abi/encode-and-decode.jsonc';
Expand Down Expand Up @@ -52,18 +51,12 @@ describe('encode and decode', () => {
// #endregion encode-and-decode-3

// #region encode-and-decode-4
// #import { JsonAbiArgument, AbiCoder};

// Now we can encode the argument we want to pass to the function. The argument is required
// as a function parameter for all `AbiCoder` functions and we can extract it from the ABI itself
const argument: JsonAbiArgument = abi.functions
.find((f) => f.name === 'main')
?.inputs.find((i) => i.name === 'inputted_amount') as JsonAbiArgument;

// Using the `AbiCoder`'s `encode` method, we can now create the encoding required for
// a u32 which takes 4 bytes up of property space
// Now we can encode the argument we want to pass to the function.
// Using the script's `interface` property, we select the `main` function and call its `encodeArguments` method.
// With this, we can now create the encoding required for a u32 which takes 4 bytes up of property space
const argumentToAdd = 10;
const encodedArguments = AbiCoder.encode(abi, argument, [argumentToAdd]);
const encodedArguments = script.interface.functions.main.encodeArguments([argumentToAdd]);
// Therefore the value of 10 will be encoded to:
// Uint8Array([0, 0, 0, 10]

Expand All @@ -82,7 +75,7 @@ describe('encode and decode', () => {
// #endregion encode-and-decode-4

// #region encode-and-decode-5
// #import { AbiCoder, ReceiptType, TransactionResultReturnDataReceipt, arrayify, buildFunctionResult };
// #import { ReceiptType, TransactionResultReturnDataReceipt, arrayify, buildFunctionResult };

// Get result of the transaction, including the contract call result. For this we'll need
// the previously created invocation scope, the transaction response and the script
Expand All @@ -109,8 +102,8 @@ describe('encode and decode', () => {
// 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 `AbiCoder`
const [decodedReturnData] = AbiCoder.decode(abi, argument, returnData, 0);
// encoded, via the `Interface` instance on the script variable
const [decodedReturnData] = script.interface.functions.main.decodeOutput(returnData);
// 20
// #endregion encode-and-decode-5

Expand Down
27 changes: 9 additions & 18 deletions apps/docs-snippets/test/fixtures/abi/encode-and-decode.jsonc
Original file line number Diff line number Diff line change
@@ -1,29 +1,25 @@
// #region encode-and-decode-2
{
"encoding": "1",
"types": [
"programType": "script",
"specVersion": "1",
"encodingVersion": "1",
"concreteTypes": [
{
"typeId": 0,
"type": "u32",
"components": null,
"typeParameters": null,
"concreteTypeId": "d7649d428b9ff33d188ecbf38a7e4d8fd167fa01b2e10fe9a8f9308e52f1d7cc",
},
],
"metadataTypes": [],
"functions": [
{
"inputs": [
{
"name": "inputted_amount",
"type": 0,
"typeArguments": null,
"concreteTypeId": "d7649d428b9ff33d188ecbf38a7e4d8fd167fa01b2e10fe9a8f9308e52f1d7cc",
},
],
"name": "main",
"output": {
"name": "",
"type": 0,
"typeArguments": null,
},
"output": "d7649d428b9ff33d188ecbf38a7e4d8fd167fa01b2e10fe9a8f9308e52f1d7cc",
"attributes": null,
},
],
Expand All @@ -32,14 +28,9 @@
"configurables": [
{
"name": "AMOUNT",
"configurableType": {
"name": "",
"type": 0,
"typeArguments": null,
},
"concreteTypeId": "d7649d428b9ff33d188ecbf38a7e4d8fd167fa01b2e10fe9a8f9308e52f1d7cc",
"offset": 856,
},
],
}

// #endregion encode-and-decode-2
3 changes: 3 additions & 0 deletions apps/docs-snippets/test/fixtures/forc-projects/Forc.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,6 @@ members = [
"bytecode-input",
"configurable-pin",
]

[patch.'https://github.com/fuellabs/sway']
std = { git = "https://github.com/fuellabs/sway", branch = "esdrubal/abi_changes" }
2 changes: 1 addition & 1 deletion apps/docs/src/guide/encoding/encode-and-decode.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ It will produce the following ABI:

<<< @/../../docs-snippets/test/fixtures/abi/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 `AbiCoder`.
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` instance present on the `Script` instance's `interface property.
Copy link
Member

Choose a reason for hiding this comment

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

IMO the intention here is just to expose the coding/coders. AbiCoder is the entry point and then interface is more opinionated on what is should be given. What is the reasoning here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The reasoning is that I've changed the AbiCoder interface and had this failing, took a look and realized that Interface can be used here as well. If I had continued with using AbiCoder, I'd have to document what ResolvableType and ResolvedType are, and those are definitely internal details that we don't want users to think about.

IMO there is no need for us to provide two ways of interacting with an abi and Interface is sufficient, and if we go with the idea I proposed in #2826 (comment) we wouldn't lose any functionality.


First, let's prepare the transaction:

Expand Down
2 changes: 1 addition & 1 deletion internal/forc/VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.62.0
git:esdrubal/abi_changes
5 changes: 4 additions & 1 deletion internal/forc/lib/shared.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,10 @@ export const buildFromGitBranch = (branchName) => {
const stdioOpts = { stdio: 'inherit' };

if (existsSync(swayRepoDir)) {
execSync(`cd ${swayRepoDir} && git fetch origin && git checkout ${branchName}`, stdioOpts);
execSync(
`cd ${swayRepoDir} && git fetch origin && git checkout ${branchName} && git reset --hard origin/${branchName}`,
Dismissed Show dismissed Hide dismissed
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Somehow, conflicts emerged between my local sway branch and the origin branch. This addition ensures that the sway repository is always on the origin branch's latest commit.

Copy link
Member

Choose a reason for hiding this comment

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

Hmm I might have to try this out. I haven't had conflicts but definitely been out of date.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I saw some force pushes happening in the origin branch so that might have caused it.

stdioOpts
);
execSync(`cd ${swayRepoDir} && cargo build`, stdioOpts);
} else {
execSync(`git clone --branch ${branchName} ${swayRepoUrl} ${swayRepoDir}`, stdioOpts);
Expand Down
26 changes: 7 additions & 19 deletions packages/abi-coder/src/AbiCoder.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,28 @@
import { ResolvedAbiType } from './ResolvedAbiType';
import type { ResolvedType } from './ResolvedType';
import type { DecodedValue, InputValue, Coder } from './encoding/coders/AbstractCoder';
import { getCoderForEncoding } from './encoding/strategies/getCoderForEncoding';
import type { EncodingOptions } from './types/EncodingOptions';
import type { JsonAbi, JsonAbiArgument } from './types/JsonAbi';

export abstract class AbiCoder {
static getCoder(
abi: JsonAbi,
argument: JsonAbiArgument,
type: ResolvedType,
options: EncodingOptions = {
padToWordSize: false,
}
): Coder {
const resolvedAbiType = new ResolvedAbiType(abi, argument);
return getCoderForEncoding(options.encoding)(resolvedAbiType, options);
return getCoderForEncoding(options.encoding)(type, options);
}

static encode(
abi: JsonAbi,
argument: JsonAbiArgument,
value: InputValue,
options?: EncodingOptions
) {
return this.getCoder(abi, argument, options).encode(value);
static encode(type: ResolvedType, value: InputValue, options?: EncodingOptions) {
return this.getCoder(type, options).encode(value);
}

static decode(
abi: JsonAbi,
argument: JsonAbiArgument,
type: ResolvedType,
data: Uint8Array,
offset: number,
options?: EncodingOptions
): [DecodedValue | undefined, number] {
return this.getCoder(abi, argument, options).decode(data, offset) as [
DecodedValue | undefined,
number,
];
return this.getCoder(type, options).decode(data, offset) as [DecodedValue | undefined, number];
}
}
89 changes: 46 additions & 43 deletions packages/abi-coder/src/FunctionFragment.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,16 @@
import { bufferFromString } from '@fuel-ts/crypto';
import { ErrorCode, FuelError } from '@fuel-ts/errors';
import { sha256 } from '@fuel-ts/hasher';
import type { BytesLike } from '@fuel-ts/interfaces';
import { bn } from '@fuel-ts/math';
import { arrayify } from '@fuel-ts/utils';

import { AbiCoder } from './AbiCoder';
import { ResolvedAbiType } from './ResolvedAbiType';
import type { ResolvedType } from './ResolvedType';
import type { DecodedValue, InputValue } from './encoding/coders/AbstractCoder';
import { StdStringCoder } from './encoding/coders/StdStringCoder';
import { TupleCoder } from './encoding/coders/TupleCoder';
import type {
JsonAbi,
JsonAbiArgument,
JsonAbiFunction,
JsonAbiFunctionAttribute,
} from './types/JsonAbi';
import type { AbiFunction, JsonAbi, StorageAttr } from './types/JsonAbi';
import type { EncodingVersion } from './utils/constants';
import { OPTION_CODER_TYPE } from './utils/constants';
import { optionRegEx } from './utils/constants';
import { getFunctionSelector, getFunctionSignature } from './utils/functionSignatureUtils';
import {
findFunctionByName,
findNonEmptyInputs,
Expand All @@ -34,37 +27,29 @@ export class FunctionFragment<
readonly selectorBytes: Uint8Array;
readonly encoding: EncodingVersion;
readonly name: string;
readonly jsonFn: JsonAbiFunction;
readonly attributes: readonly JsonAbiFunctionAttribute[];
readonly jsonFn: AbiFunction;
readonly attributes: AbiFunction['attributes'];

private readonly jsonAbi: JsonAbi;
private readonly jsonAbi: TAbi;

constructor(jsonAbi: JsonAbi, name: FnName) {
constructor(
jsonAbi: TAbi,
private resolvedTypes: ResolvedType[],
name: FnName
) {
this.jsonAbi = jsonAbi;
this.jsonFn = findFunctionByName(this.jsonAbi, name);

this.name = name;
this.signature = FunctionFragment.getSignature(this.jsonAbi, this.jsonFn);
this.selector = FunctionFragment.getFunctionSelector(this.signature);
this.signature = getFunctionSignature(this.jsonFn, resolvedTypes);

this.selector = getFunctionSelector(this.signature);
this.selectorBytes = new StdStringCoder().encode(name);
this.encoding = getEncodingVersion(jsonAbi.encoding);
this.encoding = getEncodingVersion(jsonAbi.encodingVersion);

this.attributes = this.jsonFn.attributes ?? [];
}

private static getSignature(abi: JsonAbi, fn: JsonAbiFunction): string {
const inputsSignatures = fn.inputs.map((input) =>
new ResolvedAbiType(abi, input).getSignature()
);
return `${fn.name}(${inputsSignatures.join(',')})`;
}

private static getFunctionSelector(functionSignature: string) {
const hashedFunctionSignature = sha256(bufferFromString(functionSignature, 'utf-8'));
// get first 4 bytes of signature + 0x prefix. then left-pad it to 8 bytes using toHex(8)
return bn(hashedFunctionSignature.slice(0, 10)).toHex(8);
}

encodeArguments(values: InputValue[]): Uint8Array {
FunctionFragment.verifyArgsAndInputsAlign(values, this.jsonFn.inputs, this.jsonAbi);

Expand All @@ -77,26 +62,34 @@ export class FunctionFragment<
}

const coders = nonEmptyInputs.map((t) =>
AbiCoder.getCoder(this.jsonAbi, t, {
encoding: this.encoding,
})
AbiCoder.getCoder(
this.resolvedTypes.find((rt) => rt.typeId === t.concreteTypeId) as ResolvedType,
{
encoding: this.encoding,
}
)
);

return new TupleCoder(coders).encode(shallowCopyValues);
}

private static verifyArgsAndInputsAlign(
args: InputValue[],
inputs: readonly JsonAbiArgument[],
inputs: AbiFunction['inputs'],
abi: JsonAbi
) {
if (args.length === inputs.length) {
return;
}

const inputTypes = inputs.map((input) => findTypeById(abi, input.type));
const inputTypes = inputs.map((input) => findTypeById(abi, input.concreteTypeId));

const optionalInputs = inputTypes.filter(
(x) => x.type === OPTION_CODER_TYPE || x.type === '()'
(ct) =>
ct.type === '()' ||
optionRegEx.test(
abi.metadataTypes.find((tm) => tm.metadataTypeId === ct.metadataTypeId)?.type || ''
)
);
if (optionalInputs.length === inputTypes.length) {
return;
Expand Down Expand Up @@ -143,7 +136,12 @@ export class FunctionFragment<

const result = nonEmptyInputs.reduce(
(obj: { decoded: unknown[]; offset: number }, input) => {
const coder = AbiCoder.getCoder(this.jsonAbi, input, { encoding: this.encoding });
const coder = AbiCoder.getCoder(
this.resolvedTypes.find((rt) => rt.typeId === input.concreteTypeId) as ResolvedType,
{
encoding: this.encoding,
}
);
const [decodedValue, decodedValueByteSize] = coder.decode(bytes, obj.offset);

return {
Expand All @@ -158,15 +156,18 @@ export class FunctionFragment<
}

decodeOutput(data: BytesLike): [DecodedValue | undefined, number] {
const outputAbiType = findTypeById(this.jsonAbi, this.jsonFn.output.type);
const outputAbiType = findTypeById(this.jsonAbi, this.jsonFn.output);
if (outputAbiType.type === '()') {
return [undefined, 0];
}

const bytes = arrayify(data);
const coder = AbiCoder.getCoder(this.jsonAbi, this.jsonFn.output, {
encoding: this.encoding,
});
const coder = AbiCoder.getCoder(
this.resolvedTypes.find((rt) => rt.typeId === this.jsonFn.output) as ResolvedType,
{
encoding: this.encoding,
}
);

return coder.decode(bytes, 0) as [DecodedValue | undefined, number];
}
Expand All @@ -177,7 +178,9 @@ export class FunctionFragment<
* @returns True if the function is read-only or pure, false otherwise.
*/
isReadOnly(): boolean {
const storageAttribute = this.attributes.find((attr) => attr.name === 'storage');
const storageAttribute = this.attributes?.find((attr) => attr.name === 'storage') as
| StorageAttr
| undefined;
return !storageAttribute?.arguments.includes('write');
}
}
Loading
Loading