Skip to content

Commit

Permalink
feat: add support for StdString for Contracts, Scripts, and Predica…
Browse files Browse the repository at this point in the history
…tes (#1277)

* prep new bytes

* add setup

* fix test

* refactor

* adjust

* tests

* improve tests

* aDJUST

* refactor

* add tests

* refactor

* adjust

* adjust

* fix length

* correct reference

* refactor

* twenty eagles lick?

* adjust

* fix post rename

* catch magic revert number

* fix mapper

* add sway projects

* add predicate bytes test

* add predicate raw slice tests

* add bytes and raw slice sway projects

* add bytes test for script

* add test for raw slice in script input

* add sway files

* add basic tests

* setup string Struct

* add basics

* clean

* tests

* add tests

* update exhaustive

* add sways for predicates and scripts

* add tests for string

* update

* add to interface test

* fix: linting warning

* add cs

* adjust

* convert errors

* add struct string

* cs

* cs

* pretty

* fix prettier and lint post merge

* fix assertions

* revise

* add additional coverage on input validation

* Update packages/fuel-gauge/src/std-lib-string.test.ts

Co-authored-by: Dhaiwat <dhaiwatpandya@gmail.com>

* Update packages/fuel-gauge/src/std-lib-string.test.ts

Co-authored-by: Dhaiwat <dhaiwatpandya@gmail.com>

* rename

* improve assertions

---------

Co-authored-by: Nedim Salkić <nedim.salkic@fuel.sh>
Co-authored-by: Dhaiwat <dhaiwatpandya@gmail.com>
  • Loading branch information
3 people authored Oct 3, 2023
1 parent 4708765 commit 1e496fe
Show file tree
Hide file tree
Showing 16 changed files with 387 additions and 3 deletions.
6 changes: 6 additions & 0 deletions .changeset/honest-plums-work.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@fuel-ts/abi-coder": minor
"@fuel-ts/program": minor
---

Add StdString dynamic string type
4 changes: 4 additions & 0 deletions packages/abi-coder/src/abi-coder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { EnumCoder } from './coders/enum';
import { NumberCoder } from './coders/number';
import { OptionCoder } from './coders/option';
import { RawSliceCoder } from './coders/raw-slice';
import { StdStringCoder } from './coders/stdString';
import { StringCoder } from './coders/string';
import { StructCoder } from './coders/struct';
import { TupleCoder } from './coders/tuple';
Expand All @@ -25,6 +26,7 @@ import {
OPTION_CODER_TYPE,
VEC_CODER_TYPE,
BYTES_CODER_TYPE,
STD_STRING_CODER_TYPE,
} from './constants';
import type { JsonAbi, JsonAbiArgument } from './json-abi';
import { ResolvedAbiType } from './resolved-abi-type';
Expand Down Expand Up @@ -69,6 +71,8 @@ export abstract class AbiCoder {
return new B512Coder();
case BYTES_CODER_TYPE:
return new ByteCoder();
case STD_STRING_CODER_TYPE:
return new StdStringCoder();
default:
break;
}
Expand Down
92 changes: 92 additions & 0 deletions packages/abi-coder/src/coders/stdString.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import type { Uint8ArrayWithDynamicData } from '../utilities';

import { StdStringCoder } from './stdString';

describe('StdStringCoder', () => {
it('should encode an empty string', () => {
const coder = new StdStringCoder();
const expected: Uint8ArrayWithDynamicData = new Uint8Array([
0, 0, 0, 0, 0, 0, 0, 24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
]);
expected.dynamicData = {
0: new Uint8Array([]),
};

const actual = coder.encode('');
expect(actual).toStrictEqual(expected);
});

it('should encode [hello world]', () => {
const coder = new StdStringCoder();
const expected: Uint8ArrayWithDynamicData = new Uint8Array([
0, 0, 0, 0, 0, 0, 0, 24, 0, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 11,
]);
expected.dynamicData = {
0: new Uint8Array([104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 0, 0, 0, 0, 0]),
};

const actual = coder.encode('hello world');
expect(actual).toStrictEqual(expected);
});

it('should encode [H3llo W0rld]', () => {
const coder = new StdStringCoder();
const expected: Uint8ArrayWithDynamicData = new Uint8Array([
0, 0, 0, 0, 0, 0, 0, 24, 0, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 11,
]);
expected.dynamicData = {
0: new Uint8Array([72, 51, 108, 108, 111, 32, 87, 48, 114, 108, 100, 0, 0, 0, 0, 0]),
};

const actual = coder.encode('H3llo W0rld');
expect(actual).toStrictEqual(expected);
});

it('should encode [abcdefghijklmnopqrstuvwxyz1234567890]', () => {
const coder = new StdStringCoder();
const expected: Uint8ArrayWithDynamicData = new Uint8Array([
0, 0, 0, 0, 0, 0, 0, 24, 0, 0, 0, 0, 0, 0, 0, 40, 0, 0, 0, 0, 0, 0, 0, 36,
]);
expected.dynamicData = {
0: new Uint8Array([
97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115,
116, 117, 118, 119, 120, 121, 122, 49, 50, 51, 52, 53, 54, 55, 56, 57, 48, 0, 0, 0, 0,
]),
};

const actual = coder.encode('abcdefghijklmnopqrstuvwxyz1234567890');
expect(actual).toStrictEqual(expected);
});

it('should decode a string', () => {
const coder = new StdStringCoder();
const input = new Uint8Array([
0, 0, 0, 0, 0, 0, 49, 120, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 11, 72, 101, 108,
108, 111, 32, 87, 111, 114, 108, 100, 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, 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, 0, 0, 0,
]);
const expected = 'Hello World';

const [actual, newOffset] = coder.decode(input, 0);

expect(actual).toEqual(expected);
expect(newOffset).toEqual(24);
});

it('should decode a string [with offset]', () => {
const coder = new StdStringCoder();
const input = new Uint8Array([
0, 0, 0, 0, 0, 0, 49, 120, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 11, 72, 101, 108,
108, 111, 32, 87, 111, 114, 108, 100, 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, 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, 0, 0, 0,
]);
const expected = 'Hello World';

const [actual, newOffset] = coder.decode(input, 16);

expect(actual).toEqual(expected);
expect(newOffset).toEqual(40);
});
});
59 changes: 59 additions & 0 deletions packages/abi-coder/src/coders/stdString.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { concat } from '@ethersproject/bytes';
import { toUtf8Bytes, toUtf8String } from '@ethersproject/strings';
import { bn } from '@fuel-ts/math';

import { WORD_SIZE } from '../constants';
import type { Uint8ArrayWithDynamicData } from '../utilities';
import { BASE_VECTOR_OFFSET, concatWithDynamicData } from '../utilities';

import { Coder } from './abstract-coder';
import { U64Coder } from './u64';

export class StdStringCoder extends Coder<string, string> {
static memorySize = 1;
constructor() {
super('struct', 'struct String', BASE_VECTOR_OFFSET);
}

encode(value: string): Uint8Array {
const parts: Uint8Array[] = [];

// pointer (ptr)
const pointer: Uint8ArrayWithDynamicData = new U64Coder().encode(BASE_VECTOR_OFFSET);

// pointer dynamicData, encode the string vector now and attach to its pointer
const data = this.#getPaddedData(value);
pointer.dynamicData = {
0: concatWithDynamicData([data]),
};

parts.push(pointer);

// capacity (cap)
parts.push(new U64Coder().encode(data.byteLength));

// length (len)
parts.push(new U64Coder().encode(value.length));

return concatWithDynamicData(parts);
}

#getPaddedData(value: string): Uint8Array {
const data: Uint8Array[] = [toUtf8Bytes(value)];

const paddingLength = (WORD_SIZE - (value.length % WORD_SIZE)) % WORD_SIZE;
if (paddingLength) {
data.push(new Uint8Array(paddingLength));
}

return concat(data);
}

decode(data: Uint8Array, offset: number): [string, number] {
const len = data.slice(16, 24);
const length = bn(new U64Coder().decode(len, 0)[0]).toNumber();
const byteData = data.slice(BASE_VECTOR_OFFSET, BASE_VECTOR_OFFSET + length);
const value = toUtf8String(byteData);
return [value, offset + BASE_VECTOR_OFFSET];
}
}
1 change: 1 addition & 0 deletions packages/abi-coder/src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export const OPTION_CODER_TYPE = 'enum Option';
export const VEC_CODER_TYPE = 'struct Vec';
export const BYTES_CODER_TYPE = 'struct Bytes';
export const STD_STRING_CODER_TYPE = 'struct String';
export const stringRegEx = /str\[(?<length>[0-9]+)\]/;
export const arrayRegEx = /\[(?<item>[\w\s\\[\]]+);\s*(?<length>[0-9]+)\]/;
export const structRegEx = /^struct (?<name>\w+)$/;
Expand Down
5 changes: 3 additions & 2 deletions packages/abi-coder/src/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { concat, arrayify } from '@ethersproject/bytes';
import { ErrorCode, FuelError } from '@fuel-ts/errors';

import { U64Coder } from './coders/u64';
import { BYTES_CODER_TYPE, VEC_CODER_TYPE, WORD_SIZE } from './constants';
import { BYTES_CODER_TYPE, VEC_CODER_TYPE, STD_STRING_CODER_TYPE, WORD_SIZE } from './constants';

export type DynamicData = {
[pointerIndex: number]: Uint8ArrayWithDynamicData;
Expand Down Expand Up @@ -143,7 +143,8 @@ export const isPointerType = (type: string) => {
}
};

export const isHeapType = (type: string) => type === VEC_CODER_TYPE || type === BYTES_CODER_TYPE;
export const isHeapType = (type: string) =>
type === VEC_CODER_TYPE || type === BYTES_CODER_TYPE || type === STD_STRING_CODER_TYPE;

export function findOrThrow<T>(
arr: readonly T[],
Expand Down
10 changes: 9 additions & 1 deletion packages/abi-coder/test/interface.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,6 @@ describe('Abi interface', () => {
const data = (decoded as BN[]).slice(0, 3);
return Array.from(data);
},
decodedTransfoarmer: (decoded: unknown | undefined) => Array.from(decoded as Uint8Array),
},
{
fn: exhaustiveExamplesInterface.functions.raw_slice,
Expand All @@ -322,6 +321,15 @@ describe('Abi interface', () => {
return data.map((v: BN) => v.toNumber());
},
},
{
fn: exhaustiveExamplesInterface.functions.dynamic_string,
title: '[struct String]',
value: 'H3llo W0rld',
encodedValue: new Uint8Array([
0, 0, 0, 0, 0, 0, 0, 24, 0, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 11, 72, 51, 108,
108, 111, 32, 87, 48, 114, 108, 100, 0, 0, 0, 0, 0,
]),
},
{
fn: exhaustiveExamplesInterface.functions.tuple_as_param,
title: '[tuple] as param',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
contract;
use std::b512::B512;
use std::bytes::Bytes;
use std::string::String;

enum EnumWithGeneric<T> {
VariantOne: T,
Expand Down Expand Up @@ -135,6 +136,7 @@ abi MyContract {
fn struct_with_implicitGenerics(arg: StructWithImplicitGenerics<b256, u8>) -> StructWithImplicitGenerics<b256, u8>;
fn bytes(arg: Bytes) -> Bytes;
fn raw_slice(arg: raw_slice) -> raw_slice;
fn dynamic_string(arg: String) -> String;

fn tuple_as_param(x: (u8, StructA<StructB<u64>, str[3]>)) -> (u8, StructA<StructB<u64>, str[3]>);
fn array_simple(x: [u8; 4]) -> [u8; 4];
Expand Down Expand Up @@ -253,6 +255,10 @@ impl MyContract for Contract {
fn raw_slice(arg: raw_slice) -> raw_slice {
arg
}

fn dynamic_string(arg: String) -> String {
arg
}

fn two_u8_vectors(x: Vec<u8>, y: Vec<u8>) -> (Vec<u8>, Vec<u8>) {
(x, y)
Expand Down
3 changes: 3 additions & 0 deletions packages/fuel-gauge/fixtures/forc-projects/Forc.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ members = [
"predicate-main-args-vector",
"predicate-multi-args",
"predicate-raw-slice",
"predicate-std-lib-string",
"predicate-struct",
"predicate-triple-sig",
"predicate-true",
Expand All @@ -35,11 +36,13 @@ members = [
"script-main-return-struct",
"script-main-two-args",
"script-raw-slice",
"script-std-lib-string",
"script-with-configurable",
"script-with-array",
"script-with-vector",
"script-with-vector-advanced",
"script-with-vector-mixed",
"std-lib-string",
"storage-test-contract",
"token_abi",
"token_contract",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[project]
authors = ["FuelLabs"]
entry = "main.sw"
license = "Apache-2.0"
name = "predicate-std-lib-string"

[dependencies]
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
predicate;

use std::string::String;

fn validate_string(arg: String) -> bool {
// to be replaced with a simpler assert_eq once
// https://github.com/FuelLabs/sway/issues/4868 is done
let bytes = arg.as_bytes();

let inner = String::from_ascii_str("Hello World");
let expected_bytes = inner.as_bytes();

if expected_bytes.len() != bytes.len() {
return false;
}

let mut i = 0;
while i < bytes.len() {
if expected_bytes.get(i).unwrap() != bytes.get(i).unwrap() {
return false;
}
i += 1;
}

true
}

fn main(_arg_0: u64, _arg_1: u64, arg_2: String) -> bool {
validate_string(arg_2)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[project]
authors = ["FuelLabs"]
entry = "main.sw"
license = "Apache-2.0"
name = "script-std-lib-string"

[dependencies]
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
script;

use std::string::String;

fn validate_string(arg: String) {
// to be replaced with a simpler assert_eq once
// https://github.com/FuelLabs/sway/issues/4868 is done
let bytes = arg.as_bytes();

let inner = String::from_ascii_str("Hello World");
let expected_bytes = inner.as_bytes();

assert_eq(expected_bytes.len(), bytes.len());

let mut i = 0;
while i < bytes.len() {
assert(expected_bytes.get(i).unwrap() == bytes.get(i).unwrap());
i += 1;
}
}

fn main(arg: String) {
validate_string(arg);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[project]
authors = ["FuelLabs"]
entry = "main.sw"
license = "Apache-2.0"
name = "std-lib-string"

[dependencies]
Loading

0 comments on commit 1e496fe

Please sign in to comment.