Skip to content

Commit

Permalink
chore: introduce calldata requestParser.test.ts (#1224)
Browse files Browse the repository at this point in the history
* chore: introduce calldata `requestParser.test.ts`

* chore: use is type helpers instead of cond comparison

* chore: add enum cases tests

* chore: add js docs example
  • Loading branch information
lukasaric committed Sep 13, 2024
1 parent b4198bd commit 7d61482
Show file tree
Hide file tree
Showing 4 changed files with 313 additions and 10 deletions.
260 changes: 260 additions & 0 deletions __tests__/utils/calldata/requestParser.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
import { parseCalldataField } from '../../../src/utils/calldata/requestParser';
import { getAbiEnums, getAbiStructs, getAbiEntry } from '../../factories/abi';
import {
CairoCustomEnum,
CairoOption,
CairoResult,
ETH_ADDRESS,
NON_ZERO_PREFIX,
} from '../../../src';

describe('requestParser', () => {
describe('parseCalldataField', () => {
test('should return parsed calldata field for base type', () => {
const args = [256n, 128n];
const argsIterator = args[Symbol.iterator]();
const parsedField = parseCalldataField(
argsIterator,
getAbiEntry('felt'),
getAbiStructs(),
getAbiEnums()
);
expect(parsedField).toEqual('256');
});

test('should return parsed calldata field for Array type', () => {
const args = [[256n, 128n]];
const argsIterator = args[Symbol.iterator]();
const parsedField = parseCalldataField(
argsIterator,
getAbiEntry('core::array::Array::<felt>'),
getAbiStructs(),
getAbiEnums()
);
expect(parsedField).toEqual(['2', '256', '128']);
});

test('should return parsed calldata field for Array type(string input)', () => {
const args = ['some_test_value'];
const argsIterator = args[Symbol.iterator]();
const parsedField = parseCalldataField(
argsIterator,
getAbiEntry('core::array::Array::<felt>'),
getAbiStructs(),
getAbiEnums()
);
expect(parsedField).toEqual(['1', '599374153440608178282648329058547045']);
});

test('should return parsed calldata field for NonZero type', () => {
const args = [true];
const argsIterator = args[Symbol.iterator]();
const parsedField = parseCalldataField(
argsIterator,
getAbiEntry(`${NON_ZERO_PREFIX}core::bool`),
getAbiStructs(),
getAbiEnums()
);
expect(parsedField).toEqual('1');
});

test('should return parsed calldata field for EthAddress type', () => {
const args = ['test'];
const argsIterator = args[Symbol.iterator]();
const parsedField = parseCalldataField(
argsIterator,
getAbiEntry(`${ETH_ADDRESS}felt`),
getAbiStructs(),
getAbiEnums()
);
expect(parsedField).toEqual('1952805748');
});

test('should return parsed calldata field for Struct type', () => {
const args = [{ test_name: 'test' }];
const argsIterator = args[Symbol.iterator]();
const parsedField = parseCalldataField(
argsIterator,
getAbiEntry('struct'),
getAbiStructs(),
getAbiEnums()
);
expect(parsedField).toEqual(['1952805748']);
});

test('should return parsed calldata field for Tuple type', () => {
const args = [{ min: true, max: true }];
const argsIterator = args[Symbol.iterator]();
const parsedField = parseCalldataField(
argsIterator,
getAbiEntry('(core::bool, core::bool)'),
getAbiStructs(),
getAbiEnums()
);
expect(parsedField).toEqual(['1', '1']);
});

test('should return parsed calldata field for CairoUint256 abi type', () => {
const args = [252n];
const argsIterator = args[Symbol.iterator]();
const parsedField = parseCalldataField(
argsIterator,
getAbiEntry('core::integer::u256'),
getAbiStructs(),
getAbiEnums()
);
expect(parsedField).toEqual(['252', '0']);
});

test('should return parsed calldata field for Enum Option type None', () => {
const args = [new CairoOption<string>(1, 'content')];
const argsIterator = args[Symbol.iterator]();
const parsedField = parseCalldataField(
argsIterator,
getAbiEntry('core::option::Option::core::bool'),
getAbiStructs(),
{ 'core::option::Option::core::bool': getAbiEnums().enum }
);
expect(parsedField).toEqual('1');
});

test('should return parsed calldata field for Enum Option type Some', () => {
const args = [new CairoOption<string>(0, 'content')];
const argsIterator = args[Symbol.iterator]();
const abiEnum = getAbiEnums().enum;
abiEnum.variants.push({
name: 'Some',
type: 'cairo_struct_variant',
offset: 1,
});
const parsedField = parseCalldataField(
argsIterator,
getAbiEntry('core::option::Option::core::bool'),
getAbiStructs(),
{ 'core::option::Option::core::bool': abiEnum }
);
expect(parsedField).toEqual(['0', '27988542884245108']);
});

test('should throw an error for Enum Option has no "Some" variant', () => {
const args = [new CairoOption<string>(0, 'content')];
const argsIterator = args[Symbol.iterator]();
expect(() =>
parseCalldataField(
argsIterator,
getAbiEntry('core::option::Option::core::bool'),
getAbiStructs(),
{ 'core::option::Option::core::bool': getAbiEnums().enum }
)
).toThrow(new Error(`Error in abi : Option has no 'Some' variant.`));
});

test('should return parsed calldata field for Enum Result type Ok', () => {
const args = [new CairoResult<string, string>(0, 'Ok')];
const argsIterator = args[Symbol.iterator]();
const abiEnum = getAbiEnums().enum;
abiEnum.variants.push({
name: 'Ok',
type: 'cairo_struct_variant',
offset: 1,
});
const parsedField = parseCalldataField(
argsIterator,
getAbiEntry('core::result::Result::core::bool'),
getAbiStructs(),
{ 'core::result::Result::core::bool': abiEnum }
);
expect(parsedField).toEqual(['0', '20331']);
});

test('should throw an error for Enum Result has no "Ok" variant', () => {
const args = [new CairoResult<string, string>(0, 'Ok')];
const argsIterator = args[Symbol.iterator]();
expect(() =>
parseCalldataField(
argsIterator,
getAbiEntry('core::result::Result::core::bool'),
getAbiStructs(),
{ 'core::result::Result::core::bool': getAbiEnums().enum }
)
).toThrow(new Error(`Error in abi : Result has no 'Ok' variant.`));
});

test('should return parsed calldata field for Custom Enum type', () => {
const activeVariantName = 'custom_enum';
const args = [new CairoCustomEnum({ [activeVariantName]: 'content' })];
const argsIterator = args[Symbol.iterator]();
const abiEnum = getAbiEnums().enum;
abiEnum.variants.push({
name: activeVariantName,
type: 'cairo_struct_variant',
offset: 1,
});
const parsedField = parseCalldataField(argsIterator, getAbiEntry('enum'), getAbiStructs(), {
enum: abiEnum,
});
expect(parsedField).toEqual(['1', '27988542884245108']);
});

test('should throw an error for Custon Enum type when there is not active variant', () => {
const args = [new CairoCustomEnum({ test: 'content' })];
const argsIterator = args[Symbol.iterator]();
expect(() =>
parseCalldataField(argsIterator, getAbiEntry('enum'), getAbiStructs(), getAbiEnums())
).toThrow(new Error(`Not find in abi : Enum has no 'test' variant.`));
});

test('should throw an error for CairoUint256 abi type when wrong arg is provided', () => {
const args = ['test'];
const argsIterator = args[Symbol.iterator]();
expect(() =>
parseCalldataField(
argsIterator,
getAbiEntry('core::integer::u256'),
getAbiStructs(),
getAbiEnums()
)
).toThrow(new Error('Cannot convert test to a BigInt'));
});

test('should throw an error if provided tuple size do not match', () => {
const args = [{ min: true }, { max: true }];
const argsIterator = args[Symbol.iterator]();
expect(() =>
parseCalldataField(
argsIterator,
getAbiEntry('(core::bool, core::bool)'),
getAbiStructs(),
getAbiEnums()
)
).toThrow(
new Error(
`ParseTuple: provided and expected abi tuple size do not match.
provided: true
expected: core::bool,core::bool`
)
);
});

test('should throw an error if there is missing parameter for type Struct', () => {
const args = ['test'];
const argsIterator = args[Symbol.iterator]();
expect(() =>
parseCalldataField(argsIterator, getAbiEntry('struct'), getAbiStructs(), getAbiEnums())
).toThrow(new Error('Missing parameter for type test_type'));
});

test('should throw an error if args for array type are not valid', () => {
const args = [256n, 128n];
const argsIterator = args[Symbol.iterator]();
expect(() =>
parseCalldataField(
argsIterator,
getAbiEntry('core::array::Array::<felt>'),
getAbiStructs(),
getAbiEnums()
)
).toThrow(new Error('ABI expected parameter test to be array or long string, got 256'));
});
});
});
51 changes: 46 additions & 5 deletions src/utils/calldata/requestParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
felt,
getArrayType,
isTypeArray,
isTypeByteArray,
isTypeBytes31,
isTypeEnum,
isTypeEthAddress,
Expand Down Expand Up @@ -152,7 +153,7 @@ function parseCalldataValue(
}
if (isTypeEthAddress(type)) return parseBaseTypes(type, element as BigNumberish);

if (type === 'core::byte_array::ByteArray') return parseByteArray(element as string);
if (isTypeByteArray(type)) return parseByteArray(element as string);

const { members } = structs[type];
const subElement = element as any;
Expand Down Expand Up @@ -229,6 +230,7 @@ function parseCalldataValue(
}
return [CairoResultVariant.Ok.toString(), parsedParameter];
}

// is Result::Err
const listTypeVariant = variants.find((variant) => variant.name === 'Err');
if (isUndefined(listTypeVariant)) {
Expand Down Expand Up @@ -281,6 +283,48 @@ function parseCalldataValue(
* @param structs - structs from abi
* @param enums - enums from abi
* @return {string | string[]} - parsed arguments in format that contract is expecting
*
* @example
* const abiEntry = { name: 'test', type: 'struct' };
* const abiStructs: AbiStructs = {
* struct: {
* members: [
* {
* name: 'test_name',
* type: 'test_type',
* offset: 1,
* },
* ],
* size: 2,
* name: 'cairo__struct',
* type: 'struct',
* },
* };
*
* const abiEnums: AbiEnums = {
* enum: {
* variants: [
* {
* name: 'test_name',
* type: 'cairo_struct_variant',
* offset: 1,
* },
* ],
* size: 2,
* name: 'test_cairo',
* type: 'enum',
* },
* };
*
* const args = [{ test_name: 'test' }];
* const argsIterator = args[Symbol.iterator]();
* const parsedField = parseCalldataField(
* argsIterator,
* abiEntry,
* abiStructs,
* abiEnums
* );
* // parsedField === ['1952805748']
*/
export function parseCalldataField(
argsIterator: Iterator<any>,
Expand All @@ -307,10 +351,7 @@ export function parseCalldataField(
case isTypeEthAddress(type):
return parseBaseTypes(type, value);
// Struct or Tuple
case isTypeStruct(type, structs) ||
isTypeTuple(type) ||
CairoUint256.isAbiType(type) ||
CairoUint256.isAbiType(type):
case isTypeStruct(type, structs) || isTypeTuple(type) || CairoUint256.isAbiType(type):
return parseCalldataValue(value as ParsedStruct | BigNumberish[], type, structs, enums);

// Enums
Expand Down
8 changes: 5 additions & 3 deletions src/utils/calldata/responseParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ import {
isTypeArray,
isTypeBool,
isTypeByteArray,
isTypeBytes31,
isTypeEnum,
isTypeEthAddress,
isTypeNonZero,
isTypeSecp256k1Point,
isTypeTuple,
Expand Down Expand Up @@ -60,10 +62,10 @@ function parseBaseTypes(type: string, it: Iterator<string>) {
const limb2 = it.next().value;
const limb3 = it.next().value;
return new CairoUint512(limb0, limb1, limb2, limb3).toBigInt();
case type === 'core::starknet::eth_address::EthAddress':
case isTypeEthAddress(type):
temp = it.next().value;
return BigInt(temp);
case type === 'core::bytes_31::bytes31':
case isTypeBytes31(type):
temp = it.next().value;
return decodeShortString(temp);
case isTypeSecp256k1Point(type):
Expand Down Expand Up @@ -151,7 +153,7 @@ function parseResponseValue(

// type struct
if (structs && element.type in structs && structs[element.type]) {
if (element.type === 'core::starknet::eth_address::EthAddress') {
if (isTypeEthAddress(element.type)) {
return parseBaseTypes(element.type, responseIterator);
}
return structs[element.type].members.reduce((acc, el) => {
Expand Down
Loading

0 comments on commit 7d61482

Please sign in to comment.