From 7d61482798f644ae5f6347e32c2aacd3427c16ff Mon Sep 17 00:00:00 2001 From: Luka Saric <32763694+lukasaric@users.noreply.github.com> Date: Fri, 13 Sep 2024 19:50:32 +0200 Subject: [PATCH] chore: introduce calldata `requestParser.test.ts` (#1224) * 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 --- .../utils/calldata/requestParser.test.ts | 260 ++++++++++++++++++ src/utils/calldata/requestParser.ts | 51 +++- src/utils/calldata/responseParser.ts | 8 +- src/utils/calldata/validate.ts | 4 +- 4 files changed, 313 insertions(+), 10 deletions(-) create mode 100644 __tests__/utils/calldata/requestParser.test.ts diff --git a/__tests__/utils/calldata/requestParser.test.ts b/__tests__/utils/calldata/requestParser.test.ts new file mode 100644 index 000000000..49ddbe3ef --- /dev/null +++ b/__tests__/utils/calldata/requestParser.test.ts @@ -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::'), + 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::'), + 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(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(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(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(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(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::'), + getAbiStructs(), + getAbiEnums() + ) + ).toThrow(new Error('ABI expected parameter test to be array or long string, got 256')); + }); + }); +}); diff --git a/src/utils/calldata/requestParser.ts b/src/utils/calldata/requestParser.ts index 913e2ad17..203b17ba4 100644 --- a/src/utils/calldata/requestParser.ts +++ b/src/utils/calldata/requestParser.ts @@ -20,6 +20,7 @@ import { felt, getArrayType, isTypeArray, + isTypeByteArray, isTypeBytes31, isTypeEnum, isTypeEthAddress, @@ -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; @@ -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)) { @@ -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, @@ -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 diff --git a/src/utils/calldata/responseParser.ts b/src/utils/calldata/responseParser.ts index 6f25434c7..07410ccaa 100644 --- a/src/utils/calldata/responseParser.ts +++ b/src/utils/calldata/responseParser.ts @@ -23,7 +23,9 @@ import { isTypeArray, isTypeBool, isTypeByteArray, + isTypeBytes31, isTypeEnum, + isTypeEthAddress, isTypeNonZero, isTypeSecp256k1Point, isTypeTuple, @@ -60,10 +62,10 @@ function parseBaseTypes(type: string, it: Iterator) { 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): @@ -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) => { diff --git a/src/utils/calldata/validate.ts b/src/utils/calldata/validate.ts index c239eb7fd..1ce4bc277 100644 --- a/src/utils/calldata/validate.ts +++ b/src/utils/calldata/validate.ts @@ -3,7 +3,6 @@ import { AbiEnums, AbiStructs, BigNumberish, - ETH_ADDRESS, FunctionAbi, Literal, Uint, @@ -22,6 +21,7 @@ import { isTypeByteArray, isTypeBytes31, isTypeEnum, + isTypeEthAddress, isTypeFelt, isTypeLiteral, isTypeNonZero, @@ -179,7 +179,7 @@ const validateStruct = (parameter: any, input: AbiEntry, structs: AbiStructs) => return; } - if (input.type === ETH_ADDRESS) { + if (isTypeEthAddress(input.type)) { assert(!isObject(parameter), `EthAddress type is waiting a BigNumberish. Got "${parameter}"`); const param = BigInt(parameter.toString(10)); assert(