From 4b6c6e77af0eb3e95ce231eff153f8ce3e7fc279 Mon Sep 17 00:00:00 2001 From: Aryan Godara Date: Thu, 25 Apr 2024 17:43:12 +0530 Subject: [PATCH 1/7] initial commit after branch change --- __tests__/utils/calldataDecode.test.ts | 62 ++++ src/utils/cairoDataTypes/uint256.ts | 20 + src/utils/calldata/calldataDecoder.ts | 491 +++++++++++++++++++++++++ 3 files changed, 573 insertions(+) create mode 100644 __tests__/utils/calldataDecode.test.ts create mode 100644 src/utils/calldata/calldataDecoder.ts diff --git a/__tests__/utils/calldataDecode.test.ts b/__tests__/utils/calldataDecode.test.ts new file mode 100644 index 000000000..cde8068a3 --- /dev/null +++ b/__tests__/utils/calldataDecode.test.ts @@ -0,0 +1,62 @@ +import { parseCalldataField } from '../../src/utils/calldata/requestParser'; +import { decodeCalldataField } from '../../src/utils/calldata/calldataDecoder'; +import assert from '../../src/utils/assert'; +import { CairoUint256 } from '../../src/utils/cairoDataTypes/uint256'; +import { AbiEnums, AbiStructs } from '../../src/types'; + +describe('Encode-Decode CalldataField Flow', () => { + it('correctly encodes and decodes various types', () => { + // Setup + const structs: AbiStructs = { + SimpleStruct: { + type: 'struct', + name: 'SimpleStruct', + size: 2, + members: [ + { name: 'id', type: 'felt', offset: 0 }, + { name: 'value', type: 'core::integer::u256', offset: 0 }, + ], + }, + }; + const enums: AbiEnums = {}; // Assuming no enums for this test + const simpleStructValue = { id: felt(123), value: new CairoUint256('0x1') }; + + // Create a simple iterator for each value + function* createIterator(value: any): Iterator { + yield value; + } + + // Encode + const encodedId = parseCalldataField( + createIterator(simpleStructValue.id), + { name: 'id', type: 'felt' }, + structs, + enums + ); + const encodedValue = parseCalldataField( + createIterator(simpleStructValue.value.toApiRequest()), + { name: 'value', type: 'core::integer::u256' }, + structs, + enums + ); + + // Decode + const decodedId = decodeCalldataField( + typeof encodedId === 'string' ? [encodedId] : encodedId, + { name: 'id', type: 'felt' }, + structs, + enums + ); + const decodedValue = decodeCalldataField( + typeof encodedValue === 'string' ? [encodedValue] : encodedValue, + { name: 'value', type: 'core::integer::u256' }, + structs, + enums + ); + + // Assertions + assert(decodedId.toEqual(simpleStructValue.id)); + assert(decodedValue.toBigInt().toEqual(simpleStructValue.value.toBigInt())); + // assert(2 - 1 === 1, 'abcd'); + }); +}); diff --git a/src/utils/cairoDataTypes/uint256.ts b/src/utils/cairoDataTypes/uint256.ts index 1c51c4c63..f2195f34a 100644 --- a/src/utils/cairoDataTypes/uint256.ts +++ b/src/utils/cairoDataTypes/uint256.ts @@ -133,4 +133,24 @@ export class CairoUint256 { toApiRequest() { return [CairoFelt(this.low), CairoFelt(this.high)]; } + + /** + * Construct CairoUint256 from calldata + * @param calldata Array of two strings representing the low and high parts of a uint256. + */ + static fromCalldata(calldata: [string, string]): CairoUint256 { + if (calldata.length !== 2) { + throw new Error( + 'Calldata must contain exactly two elements for low and high parts of uint256.' + ); + } + + // Validate each part to ensure they are within the acceptable range. + const [low, high] = calldata; + const validatedLow = CairoUint256.validateProps(low, UINT_256_LOW_MIN.toString()); + const validatedHigh = CairoUint256.validateProps(high, UINT_256_HIGH_MIN.toString()); + + // Construct a new instance based on the validated low and high parts. + return new CairoUint256(validatedLow.low, validatedHigh.high); + } } diff --git a/src/utils/calldata/calldataDecoder.ts b/src/utils/calldata/calldataDecoder.ts new file mode 100644 index 000000000..46500633b --- /dev/null +++ b/src/utils/calldata/calldataDecoder.ts @@ -0,0 +1,491 @@ +import { + AbiEntry, + AbiEnums, + AbiStructs, + BigNumberish, + ByteArray, + CairoEnum, + ParsedStruct, + StructAbi, +} from '../../types'; +import { CairoUint256 } from '../cairoDataTypes/uint256'; +import { + isTypeFelt, + getArrayType, + isTypeArray, + isTypeBytes31, + isTypeEnum, + isTypeOption, + isTypeResult, + isTypeStruct, + isTypeTuple, +} from './cairo'; +import { + CairoCustomEnum, + CairoOption, + CairoOptionVariant, + CairoResult, + CairoResultVariant, +} from './enum'; +import extractTupleMemberTypes from './tuple'; +import { decodeShortString } from '../shortString'; +import assert from '../assert'; + +/** + * Decode a base type from calldata. + * @param type The type string. + * @param calldata The calldata value. + * @returns The decoded value. + * @throws An error if the type is not recognized. + */ +function decodeBaseTypes(type: string, calldata: string | string[]): BigNumberish | CairoUint256 { + switch (true) { + case CairoUint256.isAbiType(type): + assert( + Array.isArray(calldata) && calldata.length === 2, + 'Expected calldata for CairoUint256 as an array of two strings.' + ); + return CairoUint256.fromCalldata([calldata[0], calldata[1]]); + + case isTypeBytes31(type): + return decodeShortString(calldata as string); + + case isTypeFelt(type): + assert(typeof calldata === 'string', 'Expected string calldata for base type decoding.'); + return BigInt(calldata); + + default: + throw new Error(`Unrecognized base type ${type} for calldata decoding.`); + } +} + +/** + * Decode a tuple from calldata. + * @param calldata The calldata array. + * @param typeStr The type string representing the tuple structure. + * @param structs The ABI structs. + * @param enums The ABI enums. + * @returns An array of decoded tuple elements. + */ +function decodeTuple( + calldata: string[], + typeStr: string, + structs: AbiStructs, + enums: AbiEnums +): any[] { + // Parse typeStr to understand the tuple structure, e.g., "('felt', 'struct', 'enum')" + const types: string[] = extractTupleMemberTypes(typeStr).map((type: string | object) => + String(type) + ); + + // Assuming we now have an array of types, ['felt', 'YourStructName', 'YourEnumName'], etc. + const decodedElements: any = []; + let calldataIndex = 0; + + types.forEach((type) => { + switch (true) { + case isTypeStruct(type, structs): { + const structRes = decodeStruct( + calldata.slice(calldataIndex, calldataIndex + structs[type].size), + type, + structs, + enums + ); + decodedElements.push(structRes); + calldataIndex += structs[type].size; // Assuming size is defined for structs. + break; + } + case isTypeEnum(type, enums): { + // Determine the expected calldata consumption for the current enum. (e.g., 1 or 2 elements for CairoOption, 2 elements for CairoResult, etc.) + const expectedCalldataLength = getExpectedCalldataLengthForEnum( + calldata[calldataIndex], + type, + enums + ); + const enumSlice = calldata.slice(calldataIndex, calldataIndex + expectedCalldataLength); + const enumRes = decodeEnum(enumSlice, type, enums); + decodedElements.push(enumRes); + calldataIndex += expectedCalldataLength; // Move past the consumed calldata. + break; + } + case isTypeArray(type): { + const arrayType = getArrayType(type); + const arrayRes = decodeCalldataValue([calldata[calldataIndex]], arrayType, structs, enums); + decodedElements.push(arrayRes); + calldataIndex += 1; + break; + } + default: { + const result = decodeBaseTypes(type, calldata[calldataIndex]); + decodedElements.push(result); + calldataIndex += 1; + } + } + }); + + return decodedElements; +} + +/** + * Decode a byte array from calldata. + * @param calldata The calldata array. + * @returns The decoded byte array. + */ +// eslint-disable-next-line @typescript-eslint/no-unused-vars +function decodeByteArray(calldata: string[]): ByteArray { + // Extract the length of the data array from the first element. + const dataLength = parseInt(calldata[0], 10); + + // Extract the data array elements based on the extracted length. + const data = calldata.slice(1, 1 + dataLength).map((str) => parseInt(str, 10)); + + // The pending_word is the second-to-last element in the original array. + const pending_word = parseInt(calldata[1 + dataLength], 10); + + // The pending_word_len is the last element in the original array. + const pending_word_len = parseInt(calldata[2 + dataLength], 10); + + // Construct and return the ByteArray object. + return { + data, + pending_word, + pending_word_len, + }; +} + +/** + * Decode calldata for a given type. + * @param calldata The calldata array. + * @param type The type string. + * @param structs The ABI structs. + * @param enums The ABI enums. + * @returns The decoded value. + * @throws An error if the type is not recognized. + */ +function decodeCalldataValue( + calldata: string | string[], + type: string, + structs: AbiStructs, + enums: AbiEnums +): any { + // Felt type decoding + if (isTypeFelt(type)) { + return decodeBaseTypes(type, Array.isArray(calldata) ? calldata[0] : calldata); + } + + // Bytes31 decoding + if (isTypeBytes31(type)) { + return decodeShortString(calldata as string); + } + + // CairoUint256 + if (CairoUint256.isAbiType(type)) { + return decodeBaseTypes(type, Array.isArray(calldata) ? calldata[0] : calldata); + } + + // Struct decoding + if (isTypeStruct(type, structs)) { + return decodeStruct(Array.isArray(calldata) ? calldata : [calldata], type, structs, enums); + } + + // Enum decoding + if (isTypeEnum(type, enums)) { + return decodeEnum(Array.isArray(calldata) ? calldata : [calldata], type, enums); + } + + // Array decoding + if (isTypeArray(type)) { + return decodeArray(Array.isArray(calldata) ? calldata : [calldata], type, structs, enums); + } + + // Tuple decoding + if (isTypeTuple(type)) { + return decodeTuple(Array.isArray(calldata) ? calldata : [calldata], type, structs, enums); + } + + // CairoOption decoding + if (isTypeOption(type)) { + const match = type.match(/Option<(.*)>/); + assert(match !== null, `Type "${type}" is not a valid Option type.`); + + const innerType = match![1]; + return decodeCairoOption( + Array.isArray(calldata) ? calldata : [calldata], + innerType, + structs, + enums + ); + } + + // CairoResult decoding + if (isTypeResult(type)) { + const matches = type.match(/Result<(.+),\s*(.+)>/); + assert(matches !== null && matches.length > 2, `Type "${type}" is not a valid Option type.`); + + const okType = matches[1]; + const errType = matches[2]; + + return decodeCairoResult( + Array.isArray(calldata) ? calldata : [calldata], + okType, + errType, + structs, + enums + ); + } + + // Fallback for unrecognized types + throw new Error(`Unrecognized type ${type} for calldata decoding.`); +} + +/** + * Decode an array from calldata. + * @param calldata The calldata array. + * @param arrayType The type of the array. + * @param structs The ABI structs. + * @param enums The ABI enums. + * @returns The decoded array. + */ +function decodeArray( + calldata: string[], + arrayType: string, + structs: AbiStructs, + enums: AbiEnums +): any[] { + const elementType = getArrayType(arrayType); + const elements = []; + + for (let i = 0; i < calldata.length; i += 1) { + elements.push(decodeCalldataValue([calldata[i]], elementType, structs, enums)); + } + + return elements; +} + +/** + * Decode a struct from calldata. + * @param calldataSegment The calldata segment for the struct. + * @param structName The name of the struct. + * @param structs The ABI structs. + * @param enums The ABI enums. + * @returns The decoded struct. + * @throws An error if the struct is not found. + */ +function decodeStruct( + calldataSegment: string[], + structName: string, + structs: AbiStructs, + enums: AbiEnums +): ParsedStruct { + const structAbi: StructAbi = structs[structName]; + assert(structAbi !== null, `Struct with name ${structName} not found.`); + + let index = 0; + const result: ParsedStruct = {}; + + structAbi.members.forEach((field) => { + const fieldType = field.type; + const fieldCalldata = calldataSegment.slice(index, index + 1); + result[field.name] = decodeCalldataValue(fieldCalldata[0], fieldType, structs, enums); + index += 1; + }); + + return result; +} + +/** + * Decode an enum from calldata. + * @param calldataValues The calldata values. + * @param enumName The name of the enum. + * @param enums The ABI enums. + * @returns The decoded enum. + * @throws An error if the enum is not found or the variant index is out of range. + */ +function decodeEnum(calldataValues: string[], enumName: string, enums: AbiEnums): CairoEnum { + const enumDefinition = enums[enumName]; + assert(enumDefinition !== null, `Enum with name ${enumName} not found.`); + + const variantIndex = parseInt(calldataValues[0], 10); + assert( + variantIndex >= 0 && variantIndex < enumDefinition.variants.length, + `Variant index ${variantIndex} out of range for enum ${enumName}.` + ); + + const variant = enumDefinition.variants[variantIndex]; + + // Determine the enum type and decode accordingly + switch (enumName) { + case 'CairoOption': + switch (variant.name) { + case 'None': { + return new CairoOption(CairoOptionVariant.None); + } + default: { + // "Some" + // const someValue = calldataValues[1]; // Placeholder logic. + const someValue = decodeCalldataValue(calldataValues.slice(1), variant.type, {}, enums); + return new CairoOption(CairoOptionVariant.Some, someValue); + } + } + case 'CairoResult': { + // const resultValue = calldataValues[1]; // Placeholder logic. + const resultValue = decodeCalldataValue(calldataValues.slice(1), variant.type, {}, enums); + + switch (variant.name) { + case 'Ok': + return new CairoResult(CairoResultVariant.Ok, resultValue); + default: // "Err" + return new CairoResult(CairoResultVariant.Err, resultValue); + } + } + default: { + // Handling CairoCustomEnum or simple enum types without associated data. + return new CairoCustomEnum({ activeVariant: variant.name, variant: variant.name }); + } + } +} + +/** + * Decode a CairoOption from calldata. + * @param calldata The calldata array. + * @param innerType The type of the inner value. + * @param structs The ABI structs. + * @param enums The ABI enums. + * @returns The decoded CairoOption. + */ +function decodeCairoOption( + calldata: string[], + innerType: string, + structs: AbiStructs, + enums: AbiEnums +): any { + const optionIndicator = parseInt(calldata[0], 10); + + switch (optionIndicator) { + case 0: { + // None + return CairoOptionVariant.None; + } + default: { + // Assuming the value is directly after the indicator + const valueCalldata = calldata.slice(1); + return decodeCalldataValue(valueCalldata, innerType, structs, enums); + } + } +} + +/** + * Decode a CairoResult from calldata. + * @param calldata + * @param okType + * @param errType + * @param structs + * @param enums + * @returns + */ +function decodeCairoResult( + calldata: string[], + okType: string, + errType: string, + structs: AbiStructs, + enums: AbiEnums +): any { + const resultIndicator = parseInt(calldata[0], 10); + + switch (resultIndicator) { + case 0: { + // Code 0 indicates "Ok" + const okValueCalldata = calldata.slice(1); + return { ok: decodeCalldataValue(okValueCalldata, okType, structs, enums) }; + } + default: { + // Non-zero code indicates "Err" + const errValueCalldata = calldata.slice(1); + return { err: decodeCalldataValue(errValueCalldata, errType, structs, enums) }; + } + } +} + +/** + * Get the expected calldata length for a given enum variant. + * @param variantIndexCalldata The calldata for the variant index. + * @param enumName The name of the enum. + * @param enums The ABI enums. + * @returns The expected calldata length. + */ +function getExpectedCalldataLengthForEnum( + variantIndexCalldata: string, + enumName: string, + enums: AbiEnums +): number { + const enumDefinition = enums[enumName]; + assert(enumDefinition, `Enum with name ${enumName} not found.`); + + const variantIndex = parseInt(variantIndexCalldata, 10); + const variant = enumDefinition.variants[variantIndex]; + + switch (enumName) { + case 'CairoOption': + return variant.name === 'None' ? 1 : 2; // "None" requires only the index, "Some" requires additional data. + case 'CairoResult': + return 2; // Both "Ok" and "Err" require additional data. + default: + return 1; // Assuming other enums don't have associated data by default. + } +} + +/** + * Decode a calldata field. + * @param calldata The calldata array. + * @param input The ABI entry for the field. + * @param structs The ABI structs. + * @param enums The ABI enums. + * @returns The decoded field value. + */ +export function decodeCalldataField( + calldata: string[], + input: AbiEntry, + structs: AbiStructs, + enums: AbiEnums +): any { + const { type } = input; + + switch (true) { + // Handling Array types + case isTypeArray(type): { + const elementType = getArrayType(type); + return calldata.map((elementCalldata) => + decodeCalldataValue([elementCalldata], elementType, structs, enums) + ); + } + + // Handling StarkNet addresses + case type === 'core::starknet::eth_address::EthAddress': { + // Directly returning the value, assuming it's already in the desired format + return calldata[0]; + } + + // Handling Struct or Tuple types + case isTypeStruct(type, structs): { + return decodeStruct(calldata, type, structs, enums); + } + + case isTypeTuple(type): { + return decodeTuple(calldata, type, structs, enums); + } + + // Handling CairoUint256 types + case CairoUint256.isAbiType(type): { + return CairoUint256.fromCalldata([calldata[0], calldata[1]]); + } + + // Handling Enums + case isTypeEnum(type, enums): { + return decodeEnum(calldata, type, enums); + } + + default: { + return decodeBaseTypes(calldata[0], type); + } + } +} From 3ed74bda81de136835a554fd53be1b13ebf43196 Mon Sep 17 00:00:00 2001 From: Aryan Godara Date: Sun, 28 Apr 2024 03:34:43 +0530 Subject: [PATCH 2/7] temp commit --- __tests__/utils/calldataDecode.test.ts | 341 ++++++++++++++++++---- src/utils/calldata/calldataDecoder.ts | 375 +++++++++++++++++-------- src/utils/calldata/index.ts | 22 ++ 3 files changed, 560 insertions(+), 178 deletions(-) diff --git a/__tests__/utils/calldataDecode.test.ts b/__tests__/utils/calldataDecode.test.ts index cde8068a3..72d89d3fe 100644 --- a/__tests__/utils/calldataDecode.test.ts +++ b/__tests__/utils/calldataDecode.test.ts @@ -1,62 +1,285 @@ -import { parseCalldataField } from '../../src/utils/calldata/requestParser'; -import { decodeCalldataField } from '../../src/utils/calldata/calldataDecoder'; -import assert from '../../src/utils/assert'; -import { CairoUint256 } from '../../src/utils/cairoDataTypes/uint256'; -import { AbiEnums, AbiStructs } from '../../src/types'; - -describe('Encode-Decode CalldataField Flow', () => { - it('correctly encodes and decodes various types', () => { - // Setup - const structs: AbiStructs = { - SimpleStruct: { - type: 'struct', - name: 'SimpleStruct', - size: 2, - members: [ - { name: 'id', type: 'felt', offset: 0 }, - { name: 'value', type: 'core::integer::u256', offset: 0 }, +// import { parseCalldataField } from '../../src/utils/calldata/requestParser'; +// import { decodeCalldataField } from '../../src/utils/calldata/calldataDecoder'; +// import assert from '../../src/utils/assert'; +// import { CairoFelt } from '../../src/utils/cairoDataTypes/felt'; +// import { AbiEnums, AbiStructs } from '../../src/types'; + +import { + // Account, + BigNumberish, + // CairoCustomEnum, + // CairoOption, + // CairoOptionVariant, + // CairoResult, + // CairoResultVariant, + // CairoUint256, + // CairoUint512, + CallData, + Calldata, + // CompiledSierra, + // Contract, + // DeclareDeployUDCResponse, + RawArgsArray, + RawArgsObject, + // byteArray, + cairo, + // ec, + // hash, + num, + // selector, + // shortString, + // stark, + // types, + // type Uint512, +} from '../../src'; + +import { compiledC1v2, compiledHelloSierra, compiledComplexSierra } from '../config/fixtures'; + +// import { initializeMatcher } from '../../config/schema'; + +const { + // uint256, + tuple, + // isCairo1Abi +} = cairo; +// const { toHex } = num; +// const { starknetKeccak } = selector; + +describe('Cairo 1', () => { + describe('API and contract interactions', () => { + test('myCallData.compile for Cairo 1', async () => { + const myFalseUint256 = { high: 1, low: 23456 }; // wrong order + type Order2 = { + p1: BigNumberish; + p2: BigNumberish[]; + }; + + const myOrder2bis: Order2 = { + // wrong order + p2: [234, 467456745457n, '0x56ec'], + p1: '17', + }; + const myRawArgsObject: RawArgsObject = { + // wrong order + active: true, + symbol: 'NIT', + initial_supply: myFalseUint256, + recipient: '0x7e00d496e324876bbc8531f2d9a82bf154d1a04a50218ee74cdd372f75a551a', + decimals: 18, + tupoftup: tuple(tuple(34, '0x5e'), myFalseUint256), + card: myOrder2bis, + longText: 'Bug is back, for ever, here and everywhere', + array1: [100, 101, 102], + array2: [ + [200, 201], + [202, 203], + [204, 205], + ], + array3: [myOrder2bis, myOrder2bis], + array4: [myFalseUint256, myFalseUint256], + tuple1: tuple(40000n, myOrder2bis, [54, 55n, '0xae'], 'texte'), + name: 'niceToken', + array5: [tuple(251, 40000n), tuple(252, 40001n)], + }; + const myRawArgsArray: RawArgsArray = [ + 'niceToken', + 'NIT', + 18, + { low: 23456, high: 1 }, + { p1: '17', p2: [234, 467456745457n, '0x56ec'] }, + '0x7e00d496e324876bbc8531f2d9a82bf154d1a04a50218ee74cdd372f75a551a', + true, + { '0': { '0': 34, '1': '0x5e' }, '1': { low: 23456, high: 1 } }, + 'Bug is back, for ever, here and everywhere', + [100, 101, 102], + [ + [200, 201], + [202, 203], + [204, 205], + ], + [ + { p1: '17', p2: [234, 467456745457n, '0x56ec'] }, + { p1: '17', p2: [234, 467456745457n, '0x56ec'] }, ], - }, - }; - const enums: AbiEnums = {}; // Assuming no enums for this test - const simpleStructValue = { id: felt(123), value: new CairoUint256('0x1') }; - - // Create a simple iterator for each value - function* createIterator(value: any): Iterator { - yield value; - } - - // Encode - const encodedId = parseCalldataField( - createIterator(simpleStructValue.id), - { name: 'id', type: 'felt' }, - structs, - enums - ); - const encodedValue = parseCalldataField( - createIterator(simpleStructValue.value.toApiRequest()), - { name: 'value', type: 'core::integer::u256' }, - structs, - enums - ); - - // Decode - const decodedId = decodeCalldataField( - typeof encodedId === 'string' ? [encodedId] : encodedId, - { name: 'id', type: 'felt' }, - structs, - enums - ); - const decodedValue = decodeCalldataField( - typeof encodedValue === 'string' ? [encodedValue] : encodedValue, - { name: 'value', type: 'core::integer::u256' }, - structs, - enums - ); - - // Assertions - assert(decodedId.toEqual(simpleStructValue.id)); - assert(decodedValue.toBigInt().toEqual(simpleStructValue.value.toBigInt())); - // assert(2 - 1 === 1, 'abcd'); + [ + { low: 23456, high: 1 }, + { low: 23456, high: 1 }, + ], + { + '0': 40000n, + '1': { p1: '17', p2: [234, 467456745457n, '0x56ec'] }, + '2': [54, 55n, '0xae'], + '3': 'texte', + }, + [ + { '0': 251, '1': 40000n }, + { '0': 252, '1': 40001n }, + ], + ]; + + const contractCallData: CallData = new CallData(compiledComplexSierra.abi); + const callDataFromObject: Calldata = contractCallData.compile('constructor', myRawArgsObject); + const callDataFromArray: Calldata = contractCallData.compile('constructor', myRawArgsArray); + const expectedResult = [ + '2036735872918048433518', + '5130580', + '18', + '23456', + '1', + '17', + '3', + '234', + '467456745457', + '22252', + '3562055384976875123115280411327378123839557441680670463096306030682092229914', + '1', + '34', + '94', + '23456', + '1', + '2', + '117422190885827407409664260607192623408641871979684112605616397634538401380', + '39164769268277364419555941', + '3', + '100', + '101', + '102', + '3', + '2', + '200', + '201', + '2', + '202', + '203', + '2', + '204', + '205', + '2', + '17', + '3', + '234', + '467456745457', + '22252', + '17', + '3', + '234', + '467456745457', + '22252', + '2', + '23456', + '1', + '23456', + '1', + '40000', + '0', + '17', + '3', + '234', + '467456745457', + '22252', + '3', + '54', + '55', + '174', + '499918599269', + '2', + '251', + '40000', + '252', + '40001', + ]; + expect(callDataFromObject).toStrictEqual(expectedResult); + expect(callDataFromArray).toStrictEqual(expectedResult); + }); + + test('myCallData.decodeParameters for Cairo 1', async () => { + const Cairo1HelloAbi = compiledHelloSierra; + const Cairo1Abi = compiledC1v2; + const helloCallData = new CallData(Cairo1HelloAbi.abi); + const c1v2CallData = new CallData(Cairo1Abi.abi); + + const res2 = helloCallData.decodeParameters('hello::hello::UserData', ['0x123456', '0x1']); + expect(res2).toEqual({ address: 1193046n, is_claimed: true }); + const res3 = helloCallData.decodeParameters( + ['hello::hello::UserData', 'hello::hello::UserData'], + ['0x123456', '0x1', '0x98765', '0x0'] + ); + expect(res3).toEqual([ + { address: 1193046n, is_claimed: true }, + { address: 624485n, is_claimed: false }, + ]); + const res4 = helloCallData.decodeParameters('core::integer::u8', ['0x123456']); + expect(res4).toBe(1193046n); + const res5 = helloCallData.decodeParameters('core::bool', ['0x1']); + expect(res5).toBe(true); + const res6 = helloCallData.decodeParameters('core::felt252', ['0x123456']); + expect(res6).toBe(1193046n); + const res7 = helloCallData.decodeParameters('core::integer::u256', ['0x123456', '0x789']); + expect(num.toHex(res7.toString())).toBe('0x78900000000000000000000000000123456'); + const res8 = helloCallData.decodeParameters('core::array::Array::', [ + '2', + '0x123456', + '0x789', + ]); + expect(res8).toEqual([1193046n, 1929n]); + const res9 = helloCallData.decodeParameters('core::array::Span::', [ + '2', + '0x123456', + '0x789', + ]); + expect(res9).toEqual([1193046n, 1929n]); + const res10 = helloCallData.decodeParameters('(core::felt252, core::integer::u16)', [ + '0x123456', + '0x789', + ]); + expect(res10).toEqual({ '0': 1193046n, '1': 1929n }); + const res11 = helloCallData.decodeParameters('core::starknet::eth_address::EthAddress', [ + '0x123456', + ]); + expect(res11).toBe(1193046n); + const res12 = helloCallData.decodeParameters( + 'core::starknet::contract_address::ContractAddress', + ['0x123456'] + ); + expect(res12).toBe(1193046n); + const res13 = helloCallData.decodeParameters('core::starknet::class_hash::ClassHash', [ + '0x123456', + ]); + expect(res13).toBe(1193046n); + const res14 = c1v2CallData.decodeParameters('core::option::Option::', [ + '0', + '0x12', + ]); + expect(res14).toEqual({ Some: 18n, None: undefined }); + const res15 = c1v2CallData.decodeParameters( + 'core::result::Result::', + ['0', '0x12', '0x345'] + ); + expect(res15).toEqual({ Ok: { p1: 18n, p2: 837n }, Err: undefined }); + const res16 = c1v2CallData.decodeParameters( + 'hello_res_events_newTypes::hello_res_events_newTypes::MyEnum', + ['0', '0x12', '0x5678'] + ); + expect(res16).toEqual({ + variant: { + Response: { p1: 18n, p2: 22136n }, + Warning: undefined, + Error: undefined, + }, + }); + }); + }); + + test('should correctly compile and decompile complex data structures', async () => { + // const complexData = { + // id: CairoFelt(1), + // name: 'Alice', + // transactions: [{ amount: 100, timestamp: '1625235962' }], + // isActive: true, + // }; + + const cd = new CallData(compiledComplexSierra.abi); + const compiledData = cd.compile('calldata', ['0x34a', [1, 3n]]); + console.log(compiledData); }); }); diff --git a/src/utils/calldata/calldataDecoder.ts b/src/utils/calldata/calldataDecoder.ts index 46500633b..6ef27c81d 100644 --- a/src/utils/calldata/calldataDecoder.ts +++ b/src/utils/calldata/calldataDecoder.ts @@ -5,7 +5,10 @@ import { BigNumberish, ByteArray, CairoEnum, + Calldata, ParsedStruct, + RawArgs, + RawArgsArray, StructAbi, } from '../../types'; import { CairoUint256 } from '../cairoDataTypes/uint256'; @@ -153,90 +156,90 @@ function decodeByteArray(calldata: string[]): ByteArray { }; } -/** - * Decode calldata for a given type. - * @param calldata The calldata array. - * @param type The type string. - * @param structs The ABI structs. - * @param enums The ABI enums. - * @returns The decoded value. - * @throws An error if the type is not recognized. - */ -function decodeCalldataValue( - calldata: string | string[], - type: string, - structs: AbiStructs, - enums: AbiEnums -): any { - // Felt type decoding - if (isTypeFelt(type)) { - return decodeBaseTypes(type, Array.isArray(calldata) ? calldata[0] : calldata); - } - - // Bytes31 decoding - if (isTypeBytes31(type)) { - return decodeShortString(calldata as string); - } - - // CairoUint256 - if (CairoUint256.isAbiType(type)) { - return decodeBaseTypes(type, Array.isArray(calldata) ? calldata[0] : calldata); - } - - // Struct decoding - if (isTypeStruct(type, structs)) { - return decodeStruct(Array.isArray(calldata) ? calldata : [calldata], type, structs, enums); - } - - // Enum decoding - if (isTypeEnum(type, enums)) { - return decodeEnum(Array.isArray(calldata) ? calldata : [calldata], type, enums); - } - - // Array decoding - if (isTypeArray(type)) { - return decodeArray(Array.isArray(calldata) ? calldata : [calldata], type, structs, enums); - } - - // Tuple decoding - if (isTypeTuple(type)) { - return decodeTuple(Array.isArray(calldata) ? calldata : [calldata], type, structs, enums); - } - - // CairoOption decoding - if (isTypeOption(type)) { - const match = type.match(/Option<(.*)>/); - assert(match !== null, `Type "${type}" is not a valid Option type.`); - - const innerType = match![1]; - return decodeCairoOption( - Array.isArray(calldata) ? calldata : [calldata], - innerType, - structs, - enums - ); - } - - // CairoResult decoding - if (isTypeResult(type)) { - const matches = type.match(/Result<(.+),\s*(.+)>/); - assert(matches !== null && matches.length > 2, `Type "${type}" is not a valid Option type.`); - - const okType = matches[1]; - const errType = matches[2]; - - return decodeCairoResult( - Array.isArray(calldata) ? calldata : [calldata], - okType, - errType, - structs, - enums - ); - } - - // Fallback for unrecognized types - throw new Error(`Unrecognized type ${type} for calldata decoding.`); -} +// /** +// * Decode calldata for a given type. +// * @param calldata The calldata array. +// * @param type The type string. +// * @param structs The ABI structs. +// * @param enums The ABI enums. +// * @returns The decoded value. +// * @throws An error if the type is not recognized. +// */ +// function decodeCalldataValue( +// calldata: string | string[], +// type: string, +// structs: AbiStructs, +// enums: AbiEnums +// ): any { +// // Felt type decoding +// if (isTypeFelt(type)) { +// return decodeBaseTypes(type, Array.isArray(calldata) ? calldata[0] : calldata); +// } + +// // Bytes31 decoding +// if (isTypeBytes31(type)) { +// return decodeShortString(calldata as string); +// } + +// // CairoUint256 +// if (CairoUint256.isAbiType(type)) { +// return decodeBaseTypes(type, Array.isArray(calldata) ? calldata[0] : calldata); +// } + +// // Struct decoding +// if (isTypeStruct(type, structs)) { +// return decodeStruct(Array.isArray(calldata) ? calldata : [calldata], type, structs, enums); +// } + +// // Enum decoding +// if (isTypeEnum(type, enums)) { +// return decodeEnum(Array.isArray(calldata) ? calldata : [calldata], type, enums); +// } + +// // Array decoding +// if (isTypeArray(type)) { +// return decodeArray(Array.isArray(calldata) ? calldata : [calldata], type, structs, enums); +// } + +// // Tuple decoding +// if (isTypeTuple(type)) { +// return decodeTuple(Array.isArray(calldata) ? calldata : [calldata], type, structs, enums); +// } + +// // CairoOption decoding +// if (isTypeOption(type)) { +// const match = type.match(/Option<(.*)>/); +// assert(match !== null, `Type "${type}" is not a valid Option type.`); + +// const innerType = match![1]; +// return decodeCairoOption( +// Array.isArray(calldata) ? calldata : [calldata], +// innerType, +// structs, +// enums +// ); +// } + +// // CairoResult decoding +// if (isTypeResult(type)) { +// const matches = type.match(/Result<(.+),\s*(.+)>/); +// assert(matches !== null && matches.length > 2, `Type "${type}" is not a valid Option type.`); + +// const okType = matches[1]; +// const errType = matches[2]; + +// return decodeCairoResult( +// Array.isArray(calldata) ? calldata : [calldata], +// okType, +// errType, +// structs, +// enums +// ); +// } + +// // Fallback for unrecognized types +// throw new Error(`Unrecognized type ${type} for calldata decoding.`); +// } /** * Decode an array from calldata. @@ -435,57 +438,191 @@ function getExpectedCalldataLengthForEnum( } /** - * Decode a calldata field. - * @param calldata The calldata array. - * @param input The ABI entry for the field. - * @param structs The ABI structs. - * @param enums The ABI enums. - * @returns The decoded field value. + * Decode calldata fields using provided ABI details. + * @param calldataIterator Iterator over the string array representing encoded calldata. + * @param input ABI entry for the field to decode. + * @param structs Struct definitions from ABI, if applicable. + * @param enums Enum definitions from ABI, if applicable. + * @returns Decoded field value. */ -export function decodeCalldataField( - calldata: string[], +export default function decodeCalldataField( + calldataIterator: Iterator, input: AbiEntry, structs: AbiStructs, enums: AbiEnums ): any { const { type } = input; + let temp; + // Handling different types based on the ABI definition switch (true) { - // Handling Array types case isTypeArray(type): { const elementType = getArrayType(type); - return calldata.map((elementCalldata) => - decodeCalldataValue([elementCalldata], elementType, structs, enums) - ); + const elements: any[] = []; + let elementResult = calldataIterator.next(); + while (!elementResult.done) { + elements.push(decodeCalldataValue([elementResult.value], elementType, structs, enums)); + elementResult = calldataIterator.next(); + } + return elements; } - // Handling StarkNet addresses - case type === 'core::starknet::eth_address::EthAddress': { - // Directly returning the value, assuming it's already in the desired format - return calldata[0]; - } + case isTypeStruct(type, structs): + case isTypeTuple(type): + const structOrTupleResult: RawArgs = {}; + const memberTypes = structs[type]?.members || extractTupleMemberTypes(type); + memberTypes.forEach(member => { + structOrTupleResult[member.name] = decodeCalldataValue([calldataIterator.next().value], member.type, structs, enums); + }); + return structOrTupleResult; - // Handling Struct or Tuple types - case isTypeStruct(type, structs): { - return decodeStruct(calldata, type, structs, enums); - } + case isTypeFelt(type): + case CairoUint256.isAbiType(type): + case isTypeEnum(type, enums): + case isTypeBytes31(type): + temp = calldataIterator.next().value; + return decodeCalldataValue(temp, type, structs, enums); - case isTypeTuple(type): { - return decodeTuple(calldata, type, structs, enums); - } + default: + throw new Error(`Unsupported or unrecognized type: ${type}`); + } +} - // Handling CairoUint256 types - case CairoUint256.isAbiType(type): { - return CairoUint256.fromCalldata([calldata[0], calldata[1]]); - } +/** + * Decodes a calldata segment based on the specified type. + * This function is versatile enough to handle all types directly from the calldata. + * @param calldata The calldata array segment or a single calldata value. + * @param type The type string as defined in the ABI. + * @param structs ABI struct definitions, if applicable. + * @param enums ABI enum definitions, if applicable. + * @returns The decoded JavaScript-compatible type. + */ +function decodeCalldataValue( + calldata: string | string[], + type: string, + structs: AbiStructs, + enums: AbiEnums +): any { + let singleValue = Array.isArray(calldata) && calldata.length === 1 ? calldata[0] : calldata; - // Handling Enums - case isTypeEnum(type, enums): { - return decodeEnum(calldata, type, enums); - } + switch (true) { + case CairoUint256.isAbiType(type): + assert( + Array.isArray(calldata) && calldata.length === 2, + 'Expected calldata for CairoUint256 as an array of two strings.' + ); + return new CairoUint256(calldata[0], calldata[1]).toBigInt(); - default: { - return decodeBaseTypes(calldata[0], type); - } + case isTypeFelt(type): + assert( + typeof singleValue === 'string', + 'Expected single string calldata for type `felt`.' + ); + return BigInt(singleValue); + + case isTypeBytes31(type): + assert( + typeof singleValue === 'string', + 'Expected single string calldata for type `bytes31`.' + ); + return decodeShortString(singleValue); + + case isTypeEnum(type, enums): + return decodeEnum(calldata as string[], type, enums); + + case isTypeStruct(type, structs): + return decodeStruct(calldata as string[], type, structs, enums); + + case isTypeArray(type): + const elementType = getArrayType(type); + return (calldata as string[]).map(element => decodeCalldataValue(element, elementType, structs, enums)); + + case isTypeTuple(type): + return decodeTuple(calldata as string[], type, structs, enums); + + case isTypeOption(type) || isTypeResult(type): + return decodeComplexType(calldata as string[], type, structs, enums); + + default: + throw new Error(`Unrecognized type ${type} for calldata decoding.`); } } + +function decodeComplexType( + calldata: Calldata, + type: string, + structs: AbiStructs, + enums: AbiEnums +): CairoOption | CairoResult | CairoCustomEnum { + const matches = type.match(/(Option|Result)<(.+)>/); + assert(matches, `Type "${type}" is not a valid complex type.`); + const containerType = matches[1]; + const innerType = matches[2]; + + switch (containerType) { + case 'Option': + const optionValue = decodeCalldataValue(calldata, innerType, structs, enums); + return new CairoOption(optionValue ? CairoOptionVariant.Some : CairoOptionVariant.None, optionValue); + case 'Result': + const resultValue = decodeCalldataValue(calldata, innerType, structs, enums); + return new CairoResult(resultValue ? CairoResultVariant.Ok : CairoResultVariant.Err, resultValue); + default: + throw new Error(`Unsupported container type: ${containerType}`); + } +} + +// /** +// * Decode a calldata field. +// * @param calldata The calldata array. +// * @param input The ABI entry for the field. +// * @param structs The ABI structs. +// * @param enums The ABI enums. +// * @returns The decoded field value. +// */ +// function _decodeCalldataField( +// calldata: Calldata, +// element: { name: string; type: string }, +// structs: AbiStructs, +// enums: AbiEnums +// ): any { +// const { name, type } = element; + +// switch (true) { +// // Handling Array types +// case isTypeArray(type): { +// const elementType = getArrayType(type); +// return calldata.map((elementCalldata) => +// decodeCalldataValue([elementCalldata], elementType, structs, enums) +// ); +// } + +// // Handling StarkNet addresses +// case type === 'core::starknet::eth_address::EthAddress': { +// // Directly returning the value, assuming it's already in the desired format +// return calldata[0]; +// } + +// // Handling Struct or Tuple types +// case isTypeStruct(type, structs): { +// return decodeStruct(calldata, type, structs, enums); +// } + +// case isTypeTuple(type): { +// return decodeTuple(calldata, type, structs, enums); +// } + +// // Handling CairoUint256 types +// case CairoUint256.isAbiType(type): { +// return CairoUint256.fromCalldata([calldata[0], calldata[1]]); +// } + +// // Handling Enums +// case isTypeEnum(type, enums): { +// return decodeEnum(calldata, type, enums); +// } + +// default: { +// return decodeBaseTypes(calldata[0], type); +// } +// } +// } diff --git a/src/utils/calldata/index.ts b/src/utils/calldata/index.ts index e9765c806..c188e726b 100644 --- a/src/utils/calldata/index.ts +++ b/src/utils/calldata/index.ts @@ -32,6 +32,7 @@ import { createAbiParser, isNoConstructorValid } from './parser'; import { AbiParserInterface } from './parser/interface'; import orderPropsByAbi from './propertyOrder'; import { parseCalldataField } from './requestParser'; +import { decodeCalldataField } from './calldataDecoder'; import responseParser from './responseParser'; import validateFields from './validate'; @@ -156,6 +157,27 @@ export class CallData { return callArray; } + /** + * Decompile calldata into JavaScript-compatible types based on ABI definitions. + * @param method The method name as defined in the ABI. + * @param calldata Array of strings representing the encoded calldata. + * @returns A structured object representing the decoded calldata. + */ + public decompile(method: string, calldata: string[]): RawArgs { + const abiMethod = this.abi.find(entry => entry.name === method && entry.type === 'function') as FunctionAbi; + if (!abiMethod) { + throw new Error(`Method ${method} not found in ABI`); + } + + const calldataIterator = calldata[Symbol.iterator](); + const decodedArgs: RawArgs = {}; + abiMethod.inputs.forEach(input => { + decodedArgs[input.name] = decodeCalldataField(calldataIterator, input, this.structs, this.enums); + }); + + return decodedArgs; + } + /** * Compile contract callData without abi * @param rawArgs RawArgs representing cairo method arguments or string array of compiled data From 91a68b09b8cc208e2cc672c0c3844bc4ae7b4ade Mon Sep 17 00:00:00 2001 From: Aryan Godara Date: Sun, 28 Apr 2024 03:52:52 +0530 Subject: [PATCH 3/7] partial update decodeCalldata logic --- src/utils/calldata/calldataDecoder.ts | 243 ++++++-------------------- src/utils/calldata/index.ts | 2 +- 2 files changed, 50 insertions(+), 195 deletions(-) diff --git a/src/utils/calldata/calldataDecoder.ts b/src/utils/calldata/calldataDecoder.ts index 6ef27c81d..62b2891c6 100644 --- a/src/utils/calldata/calldataDecoder.ts +++ b/src/utils/calldata/calldataDecoder.ts @@ -12,6 +12,7 @@ import { StructAbi, } from '../../types'; import { CairoUint256 } from '../cairoDataTypes/uint256'; +import { CairoUint512 } from '../cairoDataTypes/uint512'; import { isTypeFelt, getArrayType, @@ -44,18 +45,32 @@ import assert from '../assert'; function decodeBaseTypes(type: string, calldata: string | string[]): BigNumberish | CairoUint256 { switch (true) { case CairoUint256.isAbiType(type): - assert( - Array.isArray(calldata) && calldata.length === 2, - 'Expected calldata for CairoUint256 as an array of two strings.' - ); - return CairoUint256.fromCalldata([calldata[0], calldata[1]]); + if (Array.isArray(calldata) && calldata.length === 2) { + return new CairoUint256(calldata[0], calldata[1]).toBigInt(); + } else { + throw new Error('Expected calldata for CairoUint256 as an array of two strings.'); + } + + case CairoUint512.isAbiType(type): + if (Array.isArray(calldata) && calldata.length === 2) { + return new CairoUint512(calldata[0], calldata[1], calldata[2], calldata[3]).toBigInt(); + } else { + throw new Error('Expected calldata for CairoUint256 as an array of two strings.'); + } case isTypeBytes31(type): - return decodeShortString(calldata as string); + if (typeof calldata === 'string') { + return decodeShortString(calldata); + } else { + throw new Error('Expected single string calldata for type `bytes31`.'); + } case isTypeFelt(type): - assert(typeof calldata === 'string', 'Expected string calldata for base type decoding.'); - return BigInt(calldata); + if (typeof calldata === 'string') { + return BigInt(calldata); + } else { + throw new Error('Expected single string calldata for type `felt`.'); + } default: throw new Error(`Unrecognized base type ${type} for calldata decoding.`); @@ -156,91 +171,6 @@ function decodeByteArray(calldata: string[]): ByteArray { }; } -// /** -// * Decode calldata for a given type. -// * @param calldata The calldata array. -// * @param type The type string. -// * @param structs The ABI structs. -// * @param enums The ABI enums. -// * @returns The decoded value. -// * @throws An error if the type is not recognized. -// */ -// function decodeCalldataValue( -// calldata: string | string[], -// type: string, -// structs: AbiStructs, -// enums: AbiEnums -// ): any { -// // Felt type decoding -// if (isTypeFelt(type)) { -// return decodeBaseTypes(type, Array.isArray(calldata) ? calldata[0] : calldata); -// } - -// // Bytes31 decoding -// if (isTypeBytes31(type)) { -// return decodeShortString(calldata as string); -// } - -// // CairoUint256 -// if (CairoUint256.isAbiType(type)) { -// return decodeBaseTypes(type, Array.isArray(calldata) ? calldata[0] : calldata); -// } - -// // Struct decoding -// if (isTypeStruct(type, structs)) { -// return decodeStruct(Array.isArray(calldata) ? calldata : [calldata], type, structs, enums); -// } - -// // Enum decoding -// if (isTypeEnum(type, enums)) { -// return decodeEnum(Array.isArray(calldata) ? calldata : [calldata], type, enums); -// } - -// // Array decoding -// if (isTypeArray(type)) { -// return decodeArray(Array.isArray(calldata) ? calldata : [calldata], type, structs, enums); -// } - -// // Tuple decoding -// if (isTypeTuple(type)) { -// return decodeTuple(Array.isArray(calldata) ? calldata : [calldata], type, structs, enums); -// } - -// // CairoOption decoding -// if (isTypeOption(type)) { -// const match = type.match(/Option<(.*)>/); -// assert(match !== null, `Type "${type}" is not a valid Option type.`); - -// const innerType = match![1]; -// return decodeCairoOption( -// Array.isArray(calldata) ? calldata : [calldata], -// innerType, -// structs, -// enums -// ); -// } - -// // CairoResult decoding -// if (isTypeResult(type)) { -// const matches = type.match(/Result<(.+),\s*(.+)>/); -// assert(matches !== null && matches.length > 2, `Type "${type}" is not a valid Option type.`); - -// const okType = matches[1]; -// const errType = matches[2]; - -// return decodeCairoResult( -// Array.isArray(calldata) ? calldata : [calldata], -// okType, -// errType, -// structs, -// enums -// ); -// } - -// // Fallback for unrecognized types -// throw new Error(`Unrecognized type ${type} for calldata decoding.`); -// } - /** * Decode an array from calldata. * @param calldata The calldata array. @@ -361,19 +291,17 @@ function decodeCairoOption( innerType: string, structs: AbiStructs, enums: AbiEnums -): any { +): CairoOption { const optionIndicator = parseInt(calldata[0], 10); - switch (optionIndicator) { - case 0: { - // None - return CairoOptionVariant.None; - } - default: { - // Assuming the value is directly after the indicator - const valueCalldata = calldata.slice(1); - return decodeCalldataValue(valueCalldata, innerType, structs, enums); - } + if (optionIndicator === CairoOptionVariant.Some) { + // Decode the "Some" value content if the indicator shows "Some" + const someValueCalldata = calldata.slice(1); + const someValue = decodeCalldataValue(someValueCalldata, innerType, structs, enums); + return new CairoOption(CairoOptionVariant.Some, someValue); + } else { + // Return a CairoOption instance indicating "None" without content + return new CairoOption(CairoOptionVariant.None); } } @@ -392,20 +320,19 @@ function decodeCairoResult( errType: string, structs: AbiStructs, enums: AbiEnums -): any { +): CairoResult { const resultIndicator = parseInt(calldata[0], 10); - switch (resultIndicator) { - case 0: { - // Code 0 indicates "Ok" - const okValueCalldata = calldata.slice(1); - return { ok: decodeCalldataValue(okValueCalldata, okType, structs, enums) }; - } - default: { - // Non-zero code indicates "Err" - const errValueCalldata = calldata.slice(1); - return { err: decodeCalldataValue(errValueCalldata, errType, structs, enums) }; - } + if (resultIndicator === CairoResultVariant.Ok) { + // Handle the "Ok" variant + const okValueCalldata = calldata.slice(1); + const okValue = decodeCalldataValue(okValueCalldata, okType, structs, enums); + return new CairoResult(CairoResultVariant.Ok, okValue); + } else { + // Handle the "Err" variant + const errValueCalldata = calldata.slice(1); + const errValue = decodeCalldataValue(errValueCalldata, errType, structs, enums); + return new CairoResult(CairoResultVariant.Err, errValue); } } @@ -503,30 +430,13 @@ function decodeCalldataValue( structs: AbiStructs, enums: AbiEnums ): any { - let singleValue = Array.isArray(calldata) && calldata.length === 1 ? calldata[0] : calldata; + // Handling for base types directly + if (CairoUint256.isAbiType(type) || CairoUint512.isAbiType(type) || isTypeFelt(type) || isTypeBytes31(type)) { + return decodeBaseTypes(type, calldata); + } + // Handling complex types switch (true) { - case CairoUint256.isAbiType(type): - assert( - Array.isArray(calldata) && calldata.length === 2, - 'Expected calldata for CairoUint256 as an array of two strings.' - ); - return new CairoUint256(calldata[0], calldata[1]).toBigInt(); - - case isTypeFelt(type): - assert( - typeof singleValue === 'string', - 'Expected single string calldata for type `felt`.' - ); - return BigInt(singleValue); - - case isTypeBytes31(type): - assert( - typeof singleValue === 'string', - 'Expected single string calldata for type `bytes31`.' - ); - return decodeShortString(singleValue); - case isTypeEnum(type, enums): return decodeEnum(calldata as string[], type, enums); @@ -548,6 +458,7 @@ function decodeCalldataValue( } } + function decodeComplexType( calldata: Calldata, type: string, @@ -570,59 +481,3 @@ function decodeComplexType( throw new Error(`Unsupported container type: ${containerType}`); } } - -// /** -// * Decode a calldata field. -// * @param calldata The calldata array. -// * @param input The ABI entry for the field. -// * @param structs The ABI structs. -// * @param enums The ABI enums. -// * @returns The decoded field value. -// */ -// function _decodeCalldataField( -// calldata: Calldata, -// element: { name: string; type: string }, -// structs: AbiStructs, -// enums: AbiEnums -// ): any { -// const { name, type } = element; - -// switch (true) { -// // Handling Array types -// case isTypeArray(type): { -// const elementType = getArrayType(type); -// return calldata.map((elementCalldata) => -// decodeCalldataValue([elementCalldata], elementType, structs, enums) -// ); -// } - -// // Handling StarkNet addresses -// case type === 'core::starknet::eth_address::EthAddress': { -// // Directly returning the value, assuming it's already in the desired format -// return calldata[0]; -// } - -// // Handling Struct or Tuple types -// case isTypeStruct(type, structs): { -// return decodeStruct(calldata, type, structs, enums); -// } - -// case isTypeTuple(type): { -// return decodeTuple(calldata, type, structs, enums); -// } - -// // Handling CairoUint256 types -// case CairoUint256.isAbiType(type): { -// return CairoUint256.fromCalldata([calldata[0], calldata[1]]); -// } - -// // Handling Enums -// case isTypeEnum(type, enums): { -// return decodeEnum(calldata, type, enums); -// } - -// default: { -// return decodeBaseTypes(calldata[0], type); -// } -// } -// } diff --git a/src/utils/calldata/index.ts b/src/utils/calldata/index.ts index c188e726b..4946d52d1 100644 --- a/src/utils/calldata/index.ts +++ b/src/utils/calldata/index.ts @@ -32,7 +32,7 @@ import { createAbiParser, isNoConstructorValid } from './parser'; import { AbiParserInterface } from './parser/interface'; import orderPropsByAbi from './propertyOrder'; import { parseCalldataField } from './requestParser'; -import { decodeCalldataField } from './calldataDecoder'; +import decodeCalldataField from './calldataDecoder'; import responseParser from './responseParser'; import validateFields from './validate'; From 91c69c7adb164f2bf5ea78cb9ac9f3ff99f1b41a Mon Sep 17 00:00:00 2001 From: Aryan Godara Date: Sun, 28 Apr 2024 20:34:07 +0530 Subject: [PATCH 4/7] finalizing of helper function implementation pending --- src/utils/calldata/calldataDecoder.ts | 489 +++++++------------------- src/utils/calldata/index.ts | 15 +- src/utils/calldata/responseParser.ts | 3 +- 3 files changed, 137 insertions(+), 370 deletions(-) diff --git a/src/utils/calldata/calldataDecoder.ts b/src/utils/calldata/calldataDecoder.ts index 62b2891c6..6563dd825 100644 --- a/src/utils/calldata/calldataDecoder.ts +++ b/src/utils/calldata/calldataDecoder.ts @@ -6,6 +6,7 @@ import { ByteArray, CairoEnum, Calldata, + MultiType, ParsedStruct, RawArgs, RawArgsArray, @@ -13,12 +14,20 @@ import { } from '../../types'; import { CairoUint256 } from '../cairoDataTypes/uint256'; import { CairoUint512 } from '../cairoDataTypes/uint512'; +import { toHex } from '../num'; +import { decodeShortString } from '../shortString'; +import { stringFromByteArray } from './byteArray'; +import { addHexPrefix, removeHexPrefix } from '../encode'; import { isTypeFelt, getArrayType, isTypeArray, isTypeBytes31, isTypeEnum, + isTypeBool, + isLen, + isTypeByteArray, + isTypeSecp256k1Point, isTypeOption, isTypeResult, isTypeStruct, @@ -32,307 +41,64 @@ import { CairoResultVariant, } from './enum'; import extractTupleMemberTypes from './tuple'; -import { decodeShortString } from '../shortString'; import assert from '../assert'; +import { call } from 'abi-wan-kanabi'; /** - * Decode a base type from calldata. - * @param type The type string. - * @param calldata The calldata value. - * @returns The decoded value. - * @throws An error if the type is not recognized. + * Decode base types from calldata + * @param type type of element + * @param it iterator + * @returns CairoUint256 | CairoUint512 | Boolean | string | BigNumberish */ -function decodeBaseTypes(type: string, calldata: string | string[]): BigNumberish | CairoUint256 { +function decodeBaseTypes(type: string, it: Iterator): + | Boolean + | ParsedStruct + | BigNumberish + | BigNumberish[] + | CairoOption + | CairoResult + | CairoEnum +{ + let temp; switch (true) { - case CairoUint256.isAbiType(type): - if (Array.isArray(calldata) && calldata.length === 2) { - return new CairoUint256(calldata[0], calldata[1]).toBigInt(); - } else { - throw new Error('Expected calldata for CairoUint256 as an array of two strings.'); - } - - case CairoUint512.isAbiType(type): - if (Array.isArray(calldata) && calldata.length === 2) { - return new CairoUint512(calldata[0], calldata[1], calldata[2], calldata[3]).toBigInt(); - } else { - throw new Error('Expected calldata for CairoUint256 as an array of two strings.'); - } - - case isTypeBytes31(type): - if (typeof calldata === 'string') { - return decodeShortString(calldata); - } else { - throw new Error('Expected single string calldata for type `bytes31`.'); - } - - case isTypeFelt(type): - if (typeof calldata === 'string') { - return BigInt(calldata); - } else { - throw new Error('Expected single string calldata for type `felt`.'); - } - - default: - throw new Error(`Unrecognized base type ${type} for calldata decoding.`); - } -} - -/** - * Decode a tuple from calldata. - * @param calldata The calldata array. - * @param typeStr The type string representing the tuple structure. - * @param structs The ABI structs. - * @param enums The ABI enums. - * @returns An array of decoded tuple elements. - */ -function decodeTuple( - calldata: string[], - typeStr: string, - structs: AbiStructs, - enums: AbiEnums -): any[] { - // Parse typeStr to understand the tuple structure, e.g., "('felt', 'struct', 'enum')" - const types: string[] = extractTupleMemberTypes(typeStr).map((type: string | object) => - String(type) - ); - - // Assuming we now have an array of types, ['felt', 'YourStructName', 'YourEnumName'], etc. - const decodedElements: any = []; - let calldataIndex = 0; - - types.forEach((type) => { - switch (true) { - case isTypeStruct(type, structs): { - const structRes = decodeStruct( - calldata.slice(calldataIndex, calldataIndex + structs[type].size), - type, - structs, - enums - ); - decodedElements.push(structRes); - calldataIndex += structs[type].size; // Assuming size is defined for structs. - break; - } - case isTypeEnum(type, enums): { - // Determine the expected calldata consumption for the current enum. (e.g., 1 or 2 elements for CairoOption, 2 elements for CairoResult, etc.) - const expectedCalldataLength = getExpectedCalldataLengthForEnum( - calldata[calldataIndex], - type, - enums - ); - const enumSlice = calldata.slice(calldataIndex, calldataIndex + expectedCalldataLength); - const enumRes = decodeEnum(enumSlice, type, enums); - decodedElements.push(enumRes); - calldataIndex += expectedCalldataLength; // Move past the consumed calldata. - break; - } - case isTypeArray(type): { - const arrayType = getArrayType(type); - const arrayRes = decodeCalldataValue([calldata[calldataIndex]], arrayType, structs, enums); - decodedElements.push(arrayRes); - calldataIndex += 1; - break; - } - default: { - const result = decodeBaseTypes(type, calldata[calldataIndex]); - decodedElements.push(result); - calldataIndex += 1; - } - } - }); - - return decodedElements; -} - -/** - * Decode a byte array from calldata. - * @param calldata The calldata array. - * @returns The decoded byte array. - */ -// eslint-disable-next-line @typescript-eslint/no-unused-vars -function decodeByteArray(calldata: string[]): ByteArray { - // Extract the length of the data array from the first element. - const dataLength = parseInt(calldata[0], 10); + case isTypeBool(type): + temp = it.next().value; + return Boolean(BigInt(temp)); - // Extract the data array elements based on the extracted length. - const data = calldata.slice(1, 1 + dataLength).map((str) => parseInt(str, 10)); - - // The pending_word is the second-to-last element in the original array. - const pending_word = parseInt(calldata[1 + dataLength], 10); - - // The pending_word_len is the last element in the original array. - const pending_word_len = parseInt(calldata[2 + dataLength], 10); - - // Construct and return the ByteArray object. - return { - data, - pending_word, - pending_word_len, - }; -} - -/** - * Decode an array from calldata. - * @param calldata The calldata array. - * @param arrayType The type of the array. - * @param structs The ABI structs. - * @param enums The ABI enums. - * @returns The decoded array. - */ -function decodeArray( - calldata: string[], - arrayType: string, - structs: AbiStructs, - enums: AbiEnums -): any[] { - const elementType = getArrayType(arrayType); - const elements = []; - - for (let i = 0; i < calldata.length; i += 1) { - elements.push(decodeCalldataValue([calldata[i]], elementType, structs, enums)); - } - - return elements; -} - -/** - * Decode a struct from calldata. - * @param calldataSegment The calldata segment for the struct. - * @param structName The name of the struct. - * @param structs The ABI structs. - * @param enums The ABI enums. - * @returns The decoded struct. - * @throws An error if the struct is not found. - */ -function decodeStruct( - calldataSegment: string[], - structName: string, - structs: AbiStructs, - enums: AbiEnums -): ParsedStruct { - const structAbi: StructAbi = structs[structName]; - assert(structAbi !== null, `Struct with name ${structName} not found.`); + case CairoUint256.isAbiType(type): + const low = it.next().value; + const high = it.next().value; - let index = 0; - const result: ParsedStruct = {}; + return new CairoUint256(low, high).toBigInt(); - structAbi.members.forEach((field) => { - const fieldType = field.type; - const fieldCalldata = calldataSegment.slice(index, index + 1); - result[field.name] = decodeCalldataValue(fieldCalldata[0], fieldType, structs, enums); - index += 1; - }); + case CairoUint512.isAbiType(type): + const limb0 = it.next().value; + const limb1 = it.next().value; + const limb2 = it.next().value; + const limb3 = it.next().value; - return result; -} + return new CairoUint512(limb0, limb1, limb2, limb3).toBigInt(); -/** - * Decode an enum from calldata. - * @param calldataValues The calldata values. - * @param enumName The name of the enum. - * @param enums The ABI enums. - * @returns The decoded enum. - * @throws An error if the enum is not found or the variant index is out of range. - */ -function decodeEnum(calldataValues: string[], enumName: string, enums: AbiEnums): CairoEnum { - const enumDefinition = enums[enumName]; - assert(enumDefinition !== null, `Enum with name ${enumName} not found.`); + case type === 'core::starknet::eth_address::EthAddress': + temp = it.next().value; + return BigInt(temp); - const variantIndex = parseInt(calldataValues[0], 10); - assert( - variantIndex >= 0 && variantIndex < enumDefinition.variants.length, - `Variant index ${variantIndex} out of range for enum ${enumName}.` - ); + case type === 'core::bytes_31::bytes31': + temp = it.next().value; + return decodeShortString(temp); - const variant = enumDefinition.variants[variantIndex]; + case isTypeSecp256k1Point(type): + const xLow = removeHexPrefix(it.next().value).padStart(32, '0'); + const xHigh = removeHexPrefix(it.next().value).padStart(32, '0'); + const yLow = removeHexPrefix(it.next().value).padStart(32, '0'); + const yHigh = removeHexPrefix(it.next().value).padStart(32, '0'); + const pubK = BigInt(addHexPrefix(xHigh + xLow + yHigh + yLow)); - // Determine the enum type and decode accordingly - switch (enumName) { - case 'CairoOption': - switch (variant.name) { - case 'None': { - return new CairoOption(CairoOptionVariant.None); - } - default: { - // "Some" - // const someValue = calldataValues[1]; // Placeholder logic. - const someValue = decodeCalldataValue(calldataValues.slice(1), variant.type, {}, enums); - return new CairoOption(CairoOptionVariant.Some, someValue); - } - } - case 'CairoResult': { - // const resultValue = calldataValues[1]; // Placeholder logic. - const resultValue = decodeCalldataValue(calldataValues.slice(1), variant.type, {}, enums); + return pubK; - switch (variant.name) { - case 'Ok': - return new CairoResult(CairoResultVariant.Ok, resultValue); - default: // "Err" - return new CairoResult(CairoResultVariant.Err, resultValue); - } - } - default: { - // Handling CairoCustomEnum or simple enum types without associated data. - return new CairoCustomEnum({ activeVariant: variant.name, variant: variant.name }); - } - } -} - -/** - * Decode a CairoOption from calldata. - * @param calldata The calldata array. - * @param innerType The type of the inner value. - * @param structs The ABI structs. - * @param enums The ABI enums. - * @returns The decoded CairoOption. - */ -function decodeCairoOption( - calldata: string[], - innerType: string, - structs: AbiStructs, - enums: AbiEnums -): CairoOption { - const optionIndicator = parseInt(calldata[0], 10); - - if (optionIndicator === CairoOptionVariant.Some) { - // Decode the "Some" value content if the indicator shows "Some" - const someValueCalldata = calldata.slice(1); - const someValue = decodeCalldataValue(someValueCalldata, innerType, structs, enums); - return new CairoOption(CairoOptionVariant.Some, someValue); - } else { - // Return a CairoOption instance indicating "None" without content - return new CairoOption(CairoOptionVariant.None); - } -} - -/** - * Decode a CairoResult from calldata. - * @param calldata - * @param okType - * @param errType - * @param structs - * @param enums - * @returns - */ -function decodeCairoResult( - calldata: string[], - okType: string, - errType: string, - structs: AbiStructs, - enums: AbiEnums -): CairoResult { - const resultIndicator = parseInt(calldata[0], 10); - - if (resultIndicator === CairoResultVariant.Ok) { - // Handle the "Ok" variant - const okValueCalldata = calldata.slice(1); - const okValue = decodeCalldataValue(okValueCalldata, okType, structs, enums); - return new CairoResult(CairoResultVariant.Ok, okValue); - } else { - // Handle the "Err" variant - const errValueCalldata = calldata.slice(1); - const errValue = decodeCalldataValue(errValueCalldata, errType, structs, enums); - return new CairoResult(CairoResultVariant.Err, errValue); + default: + temp = it.next().value; + return BigInt(temp); } } @@ -364,6 +130,73 @@ function getExpectedCalldataLengthForEnum( } } +/** + * Decodes calldata based on the provided type, using an iterator over the calldata. + * @param calldataIterator Iterator over the encoded calldata strings. + * @param type The type string as defined in the ABI. + * @param structs Optional struct definitions from ABI. + * @param enums Optional enum definitions from ABI. + * @returns Decoded calldata as a JavaScript-compatible type. + */ +function decodeCalldataValue( + calldataIterator: Iterator, + element: { name: string; type: string }, + structs: AbiStructs, + enums: AbiEnums +): + | Boolean + | ParsedStruct + | BigNumberish + | BigNumberish[] + | CairoOption + | CairoResult + | CairoEnum +{ + + if (element.type === '()') { + return {}; + } + + // type uint256 struct (c1v2) + if (CairoUint256.isAbiType(element.type)) { + const low = calldataIterator.next().value; + const high = calldataIterator.next().value; + return new CairoUint256(low, high).toBigInt(); + } + // type uint512 struct + if (CairoUint512.isAbiType(element.type)) { + const limb0 = calldataIterator.next().value; + const limb1 = calldataIterator.next().value; + const limb2 = calldataIterator.next().value; + const limb3 = calldataIterator.next().value; + return new CairoUint512(limb0, limb1, limb2, limb3).toBigInt(); + } + // type C1 ByteArray struct, representing a LongString + if (isTypeByteArray(element.type)) { + const parsedBytes31Arr: BigNumberish[] = []; + const bytes31ArrLen = BigInt(calldataIterator.next().value); + while (parsedBytes31Arr.length < bytes31ArrLen) { + parsedBytes31Arr.push(toHex(calldataIterator.next().value)); + } + const pending_word = toHex(calldataIterator.next().value); + const pending_word_len = BigInt(calldataIterator.next().value); + const myByteArray: ByteArray = { + data: parsedBytes31Arr, + pending_word, + pending_word_len, + }; + return stringFromByteArray(myByteArray); + } + + // type Bytes31 string + if (isTypeBytes31(element.type)) { + return decodeShortString(calldataIterator.next().value); + } + + // base type + return decodeBaseTypes(element.type, calldataIterator); +} + /** * Decode calldata fields using provided ABI details. * @param calldataIterator Iterator over the string array representing encoded calldata. @@ -378,17 +211,19 @@ export default function decodeCalldataField( structs: AbiStructs, enums: AbiEnums ): any { - const { type } = input; - let temp; + const { name, type } = input; - // Handling different types based on the ABI definition switch (true) { + case isLen(name): + let temp = calldataIterator.next().value; + return BigInt(temp); + case isTypeArray(type): { const elementType = getArrayType(type); const elements: any[] = []; let elementResult = calldataIterator.next(); while (!elementResult.done) { - elements.push(decodeCalldataValue([elementResult.value], elementType, structs, enums)); + elements.push(decodeCalldataValue(elementResult.value, elementType, structs, enums)); elementResult = calldataIterator.next(); } return elements; @@ -399,7 +234,7 @@ export default function decodeCalldataField( const structOrTupleResult: RawArgs = {}; const memberTypes = structs[type]?.members || extractTupleMemberTypes(type); memberTypes.forEach(member => { - structOrTupleResult[member.name] = decodeCalldataValue([calldataIterator.next().value], member.type, structs, enums); + structOrTupleResult[member.name] = decodeCalldataValue(calldataIterator.next().value, member.type, structs, enums); }); return structOrTupleResult; @@ -407,77 +242,9 @@ export default function decodeCalldataField( case CairoUint256.isAbiType(type): case isTypeEnum(type, enums): case isTypeBytes31(type): - temp = calldataIterator.next().value; - return decodeCalldataValue(temp, type, structs, enums); + return decodeCalldataValue(calldataIterator.next().value, type, structs, enums); default: throw new Error(`Unsupported or unrecognized type: ${type}`); } } - -/** - * Decodes a calldata segment based on the specified type. - * This function is versatile enough to handle all types directly from the calldata. - * @param calldata The calldata array segment or a single calldata value. - * @param type The type string as defined in the ABI. - * @param structs ABI struct definitions, if applicable. - * @param enums ABI enum definitions, if applicable. - * @returns The decoded JavaScript-compatible type. - */ -function decodeCalldataValue( - calldata: string | string[], - type: string, - structs: AbiStructs, - enums: AbiEnums -): any { - // Handling for base types directly - if (CairoUint256.isAbiType(type) || CairoUint512.isAbiType(type) || isTypeFelt(type) || isTypeBytes31(type)) { - return decodeBaseTypes(type, calldata); - } - - // Handling complex types - switch (true) { - case isTypeEnum(type, enums): - return decodeEnum(calldata as string[], type, enums); - - case isTypeStruct(type, structs): - return decodeStruct(calldata as string[], type, structs, enums); - - case isTypeArray(type): - const elementType = getArrayType(type); - return (calldata as string[]).map(element => decodeCalldataValue(element, elementType, structs, enums)); - - case isTypeTuple(type): - return decodeTuple(calldata as string[], type, structs, enums); - - case isTypeOption(type) || isTypeResult(type): - return decodeComplexType(calldata as string[], type, structs, enums); - - default: - throw new Error(`Unrecognized type ${type} for calldata decoding.`); - } -} - - -function decodeComplexType( - calldata: Calldata, - type: string, - structs: AbiStructs, - enums: AbiEnums -): CairoOption | CairoResult | CairoCustomEnum { - const matches = type.match(/(Option|Result)<(.+)>/); - assert(matches, `Type "${type}" is not a valid complex type.`); - const containerType = matches[1]; - const innerType = matches[2]; - - switch (containerType) { - case 'Option': - const optionValue = decodeCalldataValue(calldata, innerType, structs, enums); - return new CairoOption(optionValue ? CairoOptionVariant.Some : CairoOptionVariant.None, optionValue); - case 'Result': - const resultValue = decodeCalldataValue(calldata, innerType, structs, enums); - return new CairoResult(resultValue ? CairoResultVariant.Ok : CairoResultVariant.Err, resultValue); - default: - throw new Error(`Unsupported container type: ${containerType}`); - } -} diff --git a/src/utils/calldata/index.ts b/src/utils/calldata/index.ts index 4946d52d1..2f00a6c45 100644 --- a/src/utils/calldata/index.ts +++ b/src/utils/calldata/index.ts @@ -11,6 +11,7 @@ import { HexCalldata, RawArgs, RawArgsArray, + RawArgsObject, Result, ValidateType, } from '../../types'; @@ -168,13 +169,13 @@ export class CallData { if (!abiMethod) { throw new Error(`Method ${method} not found in ABI`); } - - const calldataIterator = calldata[Symbol.iterator](); - const decodedArgs: RawArgs = {}; - abiMethod.inputs.forEach(input => { - decodedArgs[input.name] = decodeCalldataField(calldataIterator, input, this.structs, this.enums); - }); - + + const calldataIterator = calldata.flat()[Symbol.iterator](); + const decodedArgs = abiMethod.inputs.reduce((acc, input) => { + acc[input.name] = decodeCalldataField(calldataIterator, input, this.structs, this.enums); + return acc; + }, {} as RawArgsObject); + return decodedArgs; } diff --git a/src/utils/calldata/responseParser.ts b/src/utils/calldata/responseParser.ts index fbf7fd433..1cab961ce 100644 --- a/src/utils/calldata/responseParser.ts +++ b/src/utils/calldata/responseParser.ts @@ -231,11 +231,10 @@ export default function responseParser( parsedResult?: Args | ParsedStruct ): any { const { name, type } = output; - let temp; switch (true) { case isLen(name): - temp = responseIterator.next().value; + let temp = responseIterator.next().value; return BigInt(temp); case (structs && type in structs) || isTypeTuple(type): From 1fd9baa4dddfe09ac50b54701d73c8bca0401873 Mon Sep 17 00:00:00 2001 From: Aryan Godara Date: Mon, 29 Apr 2024 00:09:48 +0530 Subject: [PATCH 5/7] rewrite calldataEncoder --- src/utils/calldata/calldataDecoder.ts | 154 +++++++++++++++----------- src/utils/calldata/requestParser.ts | 8 +- src/utils/calldata/responseParser.ts | 14 ++- 3 files changed, 102 insertions(+), 74 deletions(-) diff --git a/src/utils/calldata/calldataDecoder.ts b/src/utils/calldata/calldataDecoder.ts index 6563dd825..d0f1655cc 100644 --- a/src/utils/calldata/calldataDecoder.ts +++ b/src/utils/calldata/calldataDecoder.ts @@ -5,12 +5,7 @@ import { BigNumberish, ByteArray, CairoEnum, - Calldata, - MultiType, ParsedStruct, - RawArgs, - RawArgsArray, - StructAbi, } from '../../types'; import { CairoUint256 } from '../cairoDataTypes/uint256'; import { CairoUint512 } from '../cairoDataTypes/uint512'; @@ -19,22 +14,23 @@ import { decodeShortString } from '../shortString'; import { stringFromByteArray } from './byteArray'; import { addHexPrefix, removeHexPrefix } from '../encode'; import { - isTypeFelt, getArrayType, isTypeArray, isTypeBytes31, isTypeEnum, isTypeBool, isLen, + isCairo1Type, isTypeByteArray, isTypeSecp256k1Point, isTypeOption, isTypeResult, - isTypeStruct, + isTypeEthAddress, isTypeTuple, } from './cairo'; import { CairoCustomEnum, + CairoEnumRaw, CairoOption, CairoOptionVariant, CairoResult, @@ -42,7 +38,6 @@ import { } from './enum'; import extractTupleMemberTypes from './tuple'; import assert from '../assert'; -import { call } from 'abi-wan-kanabi'; /** * Decode base types from calldata @@ -79,11 +74,11 @@ function decodeBaseTypes(type: string, it: Iterator): 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); @@ -102,34 +97,6 @@ function decodeBaseTypes(type: string, it: Iterator): } } -/** - * Get the expected calldata length for a given enum variant. - * @param variantIndexCalldata The calldata for the variant index. - * @param enumName The name of the enum. - * @param enums The ABI enums. - * @returns The expected calldata length. - */ -function getExpectedCalldataLengthForEnum( - variantIndexCalldata: string, - enumName: string, - enums: AbiEnums -): number { - const enumDefinition = enums[enumName]; - assert(enumDefinition, `Enum with name ${enumName} not found.`); - - const variantIndex = parseInt(variantIndexCalldata, 10); - const variant = enumDefinition.variants[variantIndex]; - - switch (enumName) { - case 'CairoOption': - return variant.name === 'None' ? 1 : 2; // "None" requires only the index, "Some" requires additional data. - case 'CairoResult': - return 2; // Both "Ok" and "Err" require additional data. - default: - return 1; // Assuming other enums don't have associated data by default. - } -} - /** * Decodes calldata based on the provided type, using an iterator over the calldata. * @param calldataIterator Iterator over the encoded calldata strings. @@ -144,10 +111,11 @@ function decodeCalldataValue( structs: AbiStructs, enums: AbiEnums ): - | Boolean + | Boolean | ParsedStruct | BigNumberish | BigNumberish[] + | any[] | CairoOption | CairoResult | CairoEnum @@ -163,6 +131,7 @@ function decodeCalldataValue( const high = calldataIterator.next().value; return new CairoUint256(low, high).toBigInt(); } + // type uint512 struct if (CairoUint512.isAbiType(element.type)) { const limb0 = calldataIterator.next().value; @@ -171,6 +140,7 @@ function decodeCalldataValue( const limb3 = calldataIterator.next().value; return new CairoUint512(limb0, limb1, limb2, limb3).toBigInt(); } + // type C1 ByteArray struct, representing a LongString if (isTypeByteArray(element.type)) { const parsedBytes31Arr: BigNumberish[] = []; @@ -188,9 +158,75 @@ function decodeCalldataValue( return stringFromByteArray(myByteArray); } - // type Bytes31 string - if (isTypeBytes31(element.type)) { - return decodeShortString(calldataIterator.next().value); + // type struct + if (structs && element.type in structs && structs[element.type]) { + if (isTypeEthAddress(element.type)) { + return decodeBaseTypes(element.type, calldataIterator); + } + return structs[element.type].members.reduce((acc, el) => { + acc[el.name] = decodeCalldataValue(calldataIterator, el, structs, enums); + return acc; + }, {} as any); + } + + // type Enum (only CustomEnum) + if (enums && element.type in enums && enums[element.type]) { + const variantNum: number = Number(calldataIterator.next().value); // get variant number + const rawEnum = enums[element.type].variants.reduce((acc, variant, num) => { + if (num === variantNum) { + acc[variant.name] = decodeCalldataValue( + calldataIterator, + { name: '', type: variant.type }, + structs, + enums + ); + return acc; + } + acc[variant.name] = undefined; + return acc; + }, {} as CairoEnumRaw); + // Option + if (isTypeOption(element.type)) { + const content = variantNum === CairoOptionVariant.Some ? rawEnum.Some : undefined; + return new CairoOption(variantNum, content); + } + // Result + if (isTypeResult(element.type)) { + let content: Object; + if (variantNum === CairoResultVariant.Ok) { + content = rawEnum.Ok; + } else { + content = rawEnum.Err; + } + return new CairoResult(variantNum, content); + } + // Cairo custom Enum + const customEnum = new CairoCustomEnum(rawEnum); + return customEnum; + } + + // type tuple + if (isTypeTuple(element.type)) { + const memberTypes = extractTupleMemberTypes(element.type); + return memberTypes.reduce((acc, it: any, idx) => { + const name = it?.name ? it.name : idx; + const type = it?.type ? it.type : it; + const el = { name, type }; + acc[name] = decodeCalldataValue(calldataIterator, el, structs, enums); + return acc; + }, {} as any); + } + + // type c1 array + if (isTypeArray(element.type)) { + // eslint-disable-next-line no-case-declarations + const parsedDataArr = []; + const el: AbiEntry = { name: '', type: getArrayType(element.type) }; + const len = BigInt(calldataIterator.next().value); // get length + while (parsedDataArr.length < len) { + parsedDataArr.push(decodeCalldataValue(calldataIterator, el, structs, enums)); + } + return parsedDataArr; } // base type @@ -218,33 +254,19 @@ export default function decodeCalldataField( let temp = calldataIterator.next().value; return BigInt(temp); - case isTypeArray(type): { - const elementType = getArrayType(type); - const elements: any[] = []; - let elementResult = calldataIterator.next(); - while (!elementResult.done) { - elements.push(decodeCalldataValue(elementResult.value, elementType, structs, enums)); - elementResult = calldataIterator.next(); - } - return elements; - } + case (structs && type in structs) || isTypeTuple(type): + return decodeCalldataValue(calldataIterator, input, structs, enums); - case isTypeStruct(type, structs): - case isTypeTuple(type): - const structOrTupleResult: RawArgs = {}; - const memberTypes = structs[type]?.members || extractTupleMemberTypes(type); - memberTypes.forEach(member => { - structOrTupleResult[member.name] = decodeCalldataValue(calldataIterator.next().value, member.type, structs, enums); - }); - return structOrTupleResult; + case enums && isTypeEnum(type, enums): + return decodeCalldataValue(calldataIterator, input, structs, enums); - case isTypeFelt(type): - case CairoUint256.isAbiType(type): - case isTypeEnum(type, enums): - case isTypeBytes31(type): - return decodeCalldataValue(calldataIterator.next().value, type, structs, enums); + case isTypeArray(type): + // C1 Array + if (isCairo1Type(type)) { + return decodeCalldataValue(calldataIterator, input, structs, enums); + } default: - throw new Error(`Unsupported or unrecognized type: ${type}`); + return decodeBaseTypes(type, calldataIterator); } } diff --git a/src/utils/calldata/requestParser.ts b/src/utils/calldata/requestParser.ts index 0a02c96b8..a022f38f9 100644 --- a/src/utils/calldata/requestParser.ts +++ b/src/utils/calldata/requestParser.ts @@ -22,11 +22,13 @@ import { isTypeBytes31, isTypeEnum, isTypeOption, + isTypeEthAddress, isTypeResult, isTypeSecp256k1Point, isTypeStruct, isTypeTuple, uint256, + isTypeByteArray, } from './cairo'; import { CairoCustomEnum, @@ -147,10 +149,10 @@ function parseCalldataValue( if (CairoUint512.isAbiType(type)) { return new CairoUint512(element as any).toApiRequest(); } - if (type === 'core::starknet::eth_address::EthAddress') + 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; @@ -297,7 +299,7 @@ export function parseCalldataField( } return parseCalldataValue(value, input.type, structs, enums); - case type === 'core::starknet::eth_address::EthAddress': + case isTypeEthAddress(type): return parseBaseTypes(type, value); // Struct or Tuple case isTypeStruct(type, structs) || diff --git a/src/utils/calldata/responseParser.ts b/src/utils/calldata/responseParser.ts index 1cab961ce..d2c74e9e6 100644 --- a/src/utils/calldata/responseParser.ts +++ b/src/utils/calldata/responseParser.ts @@ -23,7 +23,11 @@ import { isTypeArray, isTypeBool, isTypeByteArray, + isTypeBytes31, isTypeEnum, + isTypeOption, + isTypeResult, + isTypeEthAddress, isTypeSecp256k1Point, isTypeTuple, } from './cairo'; @@ -59,10 +63,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): @@ -141,7 +145,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) => { @@ -167,12 +171,12 @@ function parseResponseValue( return acc; }, {} as CairoEnumRaw); // Option - if (element.type.startsWith('core::option::Option')) { + if (isTypeOption(element.type)) { const content = variantNum === CairoOptionVariant.Some ? rawEnum.Some : undefined; return new CairoOption(variantNum, content); } // Result - if (element.type.startsWith('core::result::Result')) { + if (isTypeResult(element.type)) { let content: Object; if (variantNum === CairoResultVariant.Ok) { content = rawEnum.Ok; From f92135235a69db3c190d506a9d950a54dc43e75c Mon Sep 17 00:00:00 2001 From: Aryan Godara Date: Fri, 3 May 2024 04:05:06 +0530 Subject: [PATCH 6/7] complete decompile --- .eslintignore | 1 + __tests__/utils/calldataDecode.test.ts | 339 ++++++++----------------- src/types/lib/contract/abi.ts | 7 + src/utils/calldata/cairo.ts | 15 ++ src/utils/calldata/calldataDecoder.ts | 152 ++++++++--- src/utils/calldata/index.ts | 64 +++-- src/utils/calldata/requestParser.ts | 9 +- src/utils/calldata/responseParser.ts | 2 +- 8 files changed, 287 insertions(+), 302 deletions(-) diff --git a/.eslintignore b/.eslintignore index f9e3f7991..fabe01b1e 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,3 +1,4 @@ node_modules/ dist/ www/ +src/ \ No newline at end of file diff --git a/__tests__/utils/calldataDecode.test.ts b/__tests__/utils/calldataDecode.test.ts index 72d89d3fe..1b02844dc 100644 --- a/__tests__/utils/calldataDecode.test.ts +++ b/__tests__/utils/calldataDecode.test.ts @@ -1,12 +1,9 @@ -// import { parseCalldataField } from '../../src/utils/calldata/requestParser'; -// import { decodeCalldataField } from '../../src/utils/calldata/calldataDecoder'; -// import assert from '../../src/utils/assert'; -// import { CairoFelt } from '../../src/utils/cairoDataTypes/felt'; -// import { AbiEnums, AbiStructs } from '../../src/types'; +import { DecodeConfig } from '../../src/types'; import { // Account, BigNumberish, + CairoUint256, // CairoCustomEnum, // CairoOption, // CairoOptionVariant, @@ -25,7 +22,7 @@ import { cairo, // ec, // hash, - num, + // num, // selector, // shortString, // stark, @@ -33,253 +30,115 @@ import { // type Uint512, } from '../../src'; -import { compiledC1v2, compiledHelloSierra, compiledComplexSierra } from '../config/fixtures'; - -// import { initializeMatcher } from '../../config/schema'; +import { + // compiledC1v2, + // compiledHelloSierra, + compiledComplexSierra, +} from '../config/fixtures'; const { // uint256, tuple, // isCairo1Abi } = cairo; -// const { toHex } = num; -// const { starknetKeccak } = selector; describe('Cairo 1', () => { - describe('API and contract interactions', () => { - test('myCallData.compile for Cairo 1', async () => { - const myFalseUint256 = { high: 1, low: 23456 }; // wrong order - type Order2 = { - p1: BigNumberish; - p2: BigNumberish[]; - }; + test('should correctly compile and decompile complex data structures', async () => { + type Order2 = { + p1: BigNumberish; + p2: BigNumberish[]; + }; - const myOrder2bis: Order2 = { - // wrong order - p2: [234, 467456745457n, '0x56ec'], - p1: '17', - }; - const myRawArgsObject: RawArgsObject = { - // wrong order - active: true, - symbol: 'NIT', - initial_supply: myFalseUint256, - recipient: '0x7e00d496e324876bbc8531f2d9a82bf154d1a04a50218ee74cdd372f75a551a', - decimals: 18, - tupoftup: tuple(tuple(34, '0x5e'), myFalseUint256), - card: myOrder2bis, - longText: 'Bug is back, for ever, here and everywhere', - array1: [100, 101, 102], - array2: [ - [200, 201], - [202, 203], - [204, 205], - ], - array3: [myOrder2bis, myOrder2bis], - array4: [myFalseUint256, myFalseUint256], - tuple1: tuple(40000n, myOrder2bis, [54, 55n, '0xae'], 'texte'), - name: 'niceToken', - array5: [tuple(251, 40000n), tuple(252, 40001n)], - }; - const myRawArgsArray: RawArgsArray = [ - 'niceToken', - 'NIT', - 18, - { low: 23456, high: 1 }, - { p1: '17', p2: [234, 467456745457n, '0x56ec'] }, - '0x7e00d496e324876bbc8531f2d9a82bf154d1a04a50218ee74cdd372f75a551a', - true, - { '0': { '0': 34, '1': '0x5e' }, '1': { low: 23456, high: 1 } }, - 'Bug is back, for ever, here and everywhere', - [100, 101, 102], - [ - [200, 201], - [202, 203], - [204, 205], - ], - [ - { p1: '17', p2: [234, 467456745457n, '0x56ec'] }, - { p1: '17', p2: [234, 467456745457n, '0x56ec'] }, - ], - [ - { low: 23456, high: 1 }, - { low: 23456, high: 1 }, - ], - { - '0': 40000n, - '1': { p1: '17', p2: [234, 467456745457n, '0x56ec'] }, - '2': [54, 55n, '0xae'], - '3': 'texte', - }, - [ - { '0': 251, '1': 40000n }, - { '0': 252, '1': 40001n }, - ], - ]; + const myOrder2bis: Order2 = { + // wrong order + p2: ['abcd', '56ec'], + p1: 'smolstring', + }; - const contractCallData: CallData = new CallData(compiledComplexSierra.abi); - const callDataFromObject: Calldata = contractCallData.compile('constructor', myRawArgsObject); - const callDataFromArray: Calldata = contractCallData.compile('constructor', myRawArgsArray); - const expectedResult = [ - '2036735872918048433518', - '5130580', - '18', - '23456', - '1', - '17', - '3', - '234', - '467456745457', - '22252', - '3562055384976875123115280411327378123839557441680670463096306030682092229914', - '1', - '34', - '94', - '23456', - '1', - '2', - '117422190885827407409664260607192623408641871979684112605616397634538401380', - '39164769268277364419555941', - '3', - '100', - '101', - '102', - '3', - '2', - '200', - '201', - '2', - '202', - '203', - '2', - '204', - '205', - '2', - '17', - '3', - '234', - '467456745457', - '22252', - '17', - '3', - '234', - '467456745457', - '22252', - '2', - '23456', - '1', - '23456', - '1', - '40000', - '0', - '17', - '3', - '234', - '467456745457', - '22252', - '3', - '54', - '55', - '174', - '499918599269', - '2', - '251', - '40000', - '252', - '40001', - ]; - expect(callDataFromObject).toStrictEqual(expectedResult); - expect(callDataFromArray).toStrictEqual(expectedResult); - }); + const secondUint256: CairoUint256 = new CairoUint256(40000n); + const tempUint256 = { low: 23456n, high: 1n } as CairoUint256; + const thirdUint256: CairoUint256 = new CairoUint256(tempUint256); + const myFalseUint256: CairoUint256 = thirdUint256; // wrong order - test('myCallData.decodeParameters for Cairo 1', async () => { - const Cairo1HelloAbi = compiledHelloSierra; - const Cairo1Abi = compiledC1v2; - const helloCallData = new CallData(Cairo1HelloAbi.abi); - const c1v2CallData = new CallData(Cairo1Abi.abi); + const myRawArgsObject: RawArgsObject = { + // wrong order + active: true, + symbol: 'NIT', + initial_supply: myFalseUint256, + recipient: '0x7e00d496e324876bbc8531f2d9a82bf154d1a04a50218ee74cdd372f75a551a', + decimals: 18, + tupoftup: tuple(tuple(34, 94), myFalseUint256), + card: myOrder2bis, + longText: 'Bug is back, for ever, here and everywhere', + array1: [100, 101, 102], + array2: [ + [200, 201], + [202, 203], + [204, 205], + ], + array3: [myOrder2bis, myOrder2bis], + array4: [myFalseUint256, myFalseUint256], + tuple1: tuple(secondUint256, myOrder2bis, [54, 55, 174], 59), + name: 'niceToken', + array5: [tuple(251, 40000), tuple(252, 40001)], + }; - const res2 = helloCallData.decodeParameters('hello::hello::UserData', ['0x123456', '0x1']); - expect(res2).toEqual({ address: 1193046n, is_claimed: true }); - const res3 = helloCallData.decodeParameters( - ['hello::hello::UserData', 'hello::hello::UserData'], - ['0x123456', '0x1', '0x98765', '0x0'] - ); - expect(res3).toEqual([ - { address: 1193046n, is_claimed: true }, - { address: 624485n, is_claimed: false }, - ]); - const res4 = helloCallData.decodeParameters('core::integer::u8', ['0x123456']); - expect(res4).toBe(1193046n); - const res5 = helloCallData.decodeParameters('core::bool', ['0x1']); - expect(res5).toBe(true); - const res6 = helloCallData.decodeParameters('core::felt252', ['0x123456']); - expect(res6).toBe(1193046n); - const res7 = helloCallData.decodeParameters('core::integer::u256', ['0x123456', '0x789']); - expect(num.toHex(res7.toString())).toBe('0x78900000000000000000000000000123456'); - const res8 = helloCallData.decodeParameters('core::array::Array::', [ - '2', - '0x123456', - '0x789', - ]); - expect(res8).toEqual([1193046n, 1929n]); - const res9 = helloCallData.decodeParameters('core::array::Span::', [ - '2', - '0x123456', - '0x789', - ]); - expect(res9).toEqual([1193046n, 1929n]); - const res10 = helloCallData.decodeParameters('(core::felt252, core::integer::u16)', [ - '0x123456', - '0x789', - ]); - expect(res10).toEqual({ '0': 1193046n, '1': 1929n }); - const res11 = helloCallData.decodeParameters('core::starknet::eth_address::EthAddress', [ - '0x123456', - ]); - expect(res11).toBe(1193046n); - const res12 = helloCallData.decodeParameters( - 'core::starknet::contract_address::ContractAddress', - ['0x123456'] - ); - expect(res12).toBe(1193046n); - const res13 = helloCallData.decodeParameters('core::starknet::class_hash::ClassHash', [ - '0x123456', - ]); - expect(res13).toBe(1193046n); - const res14 = c1v2CallData.decodeParameters('core::option::Option::', [ - '0', - '0x12', - ]); - expect(res14).toEqual({ Some: 18n, None: undefined }); - const res15 = c1v2CallData.decodeParameters( - 'core::result::Result::', - ['0', '0x12', '0x345'] - ); - expect(res15).toEqual({ Ok: { p1: 18n, p2: 837n }, Err: undefined }); - const res16 = c1v2CallData.decodeParameters( - 'hello_res_events_newTypes::hello_res_events_newTypes::MyEnum', - ['0', '0x12', '0x5678'] - ); - expect(res16).toEqual({ - variant: { - Response: { p1: 18n, p2: 22136n }, - Warning: undefined, - Error: undefined, - }, - }); - }); - }); + const myRawArgsArray: RawArgsArray = [ + 'niceToken', + 'NIT', + 18, + thirdUint256, + { p1: '17', p2: ['234', '467456745457', '0x56ec'] }, + '0x7e00d496e324876bbc8531f2d9a82bf154d1a04a50218ee74cdd372f75a551a', + true, + { '0': { '0': 34, '1': 94 }, '1': thirdUint256 }, + 'Bug is back, for ever, here and everywhere', + ['100', '101', '102'], + [ + [200, 201], + [202, 203], + [204, 205], + ], + [ + { p1: '17', p2: ['234', '467456745457n', '0x56ec'] }, + { p1: '17', p2: ['234', '467456745457n', '0x56ec'] }, + ], + [thirdUint256, thirdUint256], + { + '0': secondUint256, + '1': { p1: '17', p2: ['234', '467456745457n', '0x56ec'] }, + '2': [54, 55, 56], + '3': 59, + }, + [ + { '0': 251, '1': 40000 }, + { '0': 252, '1': 40001 }, + ], + ]; - test('should correctly compile and decompile complex data structures', async () => { - // const complexData = { - // id: CairoFelt(1), - // name: 'Alice', - // transactions: [{ amount: 100, timestamp: '1625235962' }], - // isActive: true, - // }; + const config: DecodeConfig = { + felt: String, + 'core::felt252': String, + 'core::integer::u8': Number, + 'core::integer::u16': Number, + 'core::integer::u64': Number, + 'core::integer::u128': BigInt, + 'core::starknet::contract_address::ContractAddress': String, + longText: String, + }; + + const cd: CallData = new CallData(compiledComplexSierra.abi); + const compiledDataFromObject: Calldata = cd.compile('constructor', myRawArgsObject); + const compiledDataFromArray: Calldata = cd.compile('constructor', myRawArgsArray); + const decompiledDataFromObject = cd.decompile('constructor', compiledDataFromObject, config); + const decompiledDataFromArray = cd.decompile( + 'constructor', + compiledDataFromArray, + config, + true + ); - const cd = new CallData(compiledComplexSierra.abi); - const compiledData = cd.compile('calldata', ['0x34a', [1, 3n]]); - console.log(compiledData); + expect(decompiledDataFromObject).toEqual(myRawArgsObject); + expect(decompiledDataFromArray).toEqual(myRawArgsArray); }); }); diff --git a/src/types/lib/contract/abi.ts b/src/types/lib/contract/abi.ts index 6583165dd..72eedab45 100644 --- a/src/types/lib/contract/abi.ts +++ b/src/types/lib/contract/abi.ts @@ -1,3 +1,5 @@ +import { Uint256 } from '..'; + /** ABI */ export type Abi = ReadonlyArray; @@ -56,3 +58,8 @@ export type LegacyEvent = { data: EventEntry[]; keys: EventEntry[]; }; + +type JSCDataType = StringConstructor | NumberConstructor | BigIntConstructor | BooleanConstructor; +export type DecodeConfig = { + [typeName: string]: JSCDataType; +}; diff --git a/src/utils/calldata/cairo.ts b/src/utils/calldata/cairo.ts index 09b419703..50922268d 100644 --- a/src/utils/calldata/cairo.ts +++ b/src/utils/calldata/cairo.ts @@ -89,6 +89,21 @@ export const isTypeResult = (type: string) => type.startsWith('core::result::Res * @returns - Returns true if the value is a valid Uint type, otherwise false. */ export const isTypeUint = (type: string) => Object.values(Uint).includes(type as Uint); +/** + * Retrieves the Uint enum type for the given type string. + * + * @param {string} type - The type string to check against Uint types. + * @returns {(Uint | null)} - The corresponding Uint enum value or null if not found. + */ +export const getUintType = (type: string): string | undefined => { + for (const value of Object.values(Uint)) { + if (value === type) { + return value; + } + } + + return undefined; +}; // Legacy Export /** * Checks if the given type is `uint256`. diff --git a/src/utils/calldata/calldataDecoder.ts b/src/utils/calldata/calldataDecoder.ts index d0f1655cc..399f9c809 100644 --- a/src/utils/calldata/calldataDecoder.ts +++ b/src/utils/calldata/calldataDecoder.ts @@ -4,8 +4,11 @@ import { AbiStructs, BigNumberish, ByteArray, + DecodeConfig, CairoEnum, ParsedStruct, + Uint256, + Uint, } from '../../types'; import { CairoUint256 } from '../cairoDataTypes/uint256'; import { CairoUint512 } from '../cairoDataTypes/uint512'; @@ -21,10 +24,14 @@ import { isTypeBool, isLen, isCairo1Type, + isTypeFelt, + isTypeUint, + getUintType, isTypeByteArray, isTypeSecp256k1Point, isTypeOption, isTypeResult, + isTypeContractAddress, isTypeEthAddress, isTypeTuple, } from './cairo'; @@ -37,7 +44,6 @@ import { CairoResultVariant, } from './enum'; import extractTupleMemberTypes from './tuple'; -import assert from '../assert'; /** * Decode base types from calldata @@ -45,44 +51,79 @@ import assert from '../assert'; * @param it iterator * @returns CairoUint256 | CairoUint512 | Boolean | string | BigNumberish */ -function decodeBaseTypes(type: string, it: Iterator): +function decodeBaseTypes( + type: string, + it: Iterator, + config?: DecodeConfig +): | Boolean | ParsedStruct | BigNumberish + | Uint256 | BigNumberish[] | CairoOption | CairoResult - | CairoEnum -{ + | CairoEnum { let temp; switch (true) { case isTypeBool(type): temp = it.next().value; return Boolean(BigInt(temp)); - case CairoUint256.isAbiType(type): - const low = it.next().value; - const high = it.next().value; + case isTypeUint(type): + switch (true) { + case CairoUint256.isAbiType(type): + console.log('got 256 uint value'); + const low = it.next().value; + const high = it.next().value; - return new CairoUint256(low, high).toBigInt(); + const ret = new CairoUint256(low, high); + let configConstructor = config?.['core::integer::u256']; + if (configConstructor) { + return configConstructor(ret); + } + return ret; - case CairoUint512.isAbiType(type): - const limb0 = it.next().value; - const limb1 = it.next().value; - const limb2 = it.next().value; - const limb3 = it.next().value; + case CairoUint512.isAbiType(type): + const limb0 = it.next().value; + const limb1 = it.next().value; + const limb2 = it.next().value; + const limb3 = it.next().value; - return new CairoUint512(limb0, limb1, limb2, limb3).toBigInt(); + return new CairoUint512(limb0, limb1, limb2, limb3).toBigInt(); - case isTypeEthAddress(type): + default: + temp = it.next().value; + const configType = getUintType(type); + if (configType) { + const UintConstructor = config?.[configType]; + if (UintConstructor) { + return UintConstructor(temp); + } else { + return BigInt(temp); + } + } + } + + case isTypeEthAddress(type): temp = it.next().value; return BigInt(temp); - case isTypeBytes31(type): + case isTypeContractAddress(type): + temp = it.next().value; + temp = toHex(temp); + const configConstructor = config?.[type]; + if (configConstructor) { + return configConstructor(temp); + } else { + return BigInt(temp); + } + + case isTypeBytes31(type): temp = it.next().value; return decodeShortString(temp); - case isTypeSecp256k1Point(type): + case isTypeSecp256k1Point(type): const xLow = removeHexPrefix(it.next().value).padStart(32, '0'); const xHigh = removeHexPrefix(it.next().value).padStart(32, '0'); const yLow = removeHexPrefix(it.next().value).padStart(32, '0'); @@ -91,7 +132,20 @@ function decodeBaseTypes(type: string, it: Iterator): return pubK; - default: + case isTypeFelt(type): + temp = String(it.next().value); + console.log('Original temp = ', temp); + const configFeltConstructor = config?.['core::felt252']; + if (configFeltConstructor) { + if (configFeltConstructor === String) return decodeShortString(temp); + else return configFeltConstructor(temp); + } + + // Default + return BigInt(temp); + + default: + console.log('went to default block for '); temp = it.next().value; return BigInt(temp); } @@ -109,18 +163,18 @@ function decodeCalldataValue( calldataIterator: Iterator, element: { name: string; type: string }, structs: AbiStructs, - enums: AbiEnums -): - | Boolean + enums: AbiEnums, + config?: DecodeConfig +): + | Boolean | ParsedStruct + | Uint256 | BigNumberish | BigNumberish[] | any[] | CairoOption | CairoResult - | CairoEnum -{ - + | CairoEnum { if (element.type === '()') { return {}; } @@ -129,7 +183,7 @@ function decodeCalldataValue( if (CairoUint256.isAbiType(element.type)) { const low = calldataIterator.next().value; const high = calldataIterator.next().value; - return new CairoUint256(low, high).toBigInt(); + return new CairoUint256(low, high); } // type uint512 struct @@ -161,10 +215,14 @@ function decodeCalldataValue( // type struct if (structs && element.type in structs && structs[element.type]) { if (isTypeEthAddress(element.type)) { - return decodeBaseTypes(element.type, calldataIterator); + return decodeBaseTypes(element.type, calldataIterator, config); + } + if (isTypeContractAddress(element.type)) { + return decodeBaseTypes(element.type, calldataIterator, config); } + return structs[element.type].members.reduce((acc, el) => { - acc[el.name] = decodeCalldataValue(calldataIterator, el, structs, enums); + acc[el.name] = decodeCalldataValue(calldataIterator, el, structs, enums, config); return acc; }, {} as any); } @@ -178,7 +236,8 @@ function decodeCalldataValue( calldataIterator, { name: '', type: variant.type }, structs, - enums + enums, + config ); return acc; } @@ -212,7 +271,7 @@ function decodeCalldataValue( const name = it?.name ? it.name : idx; const type = it?.type ? it.type : it; const el = { name, type }; - acc[name] = decodeCalldataValue(calldataIterator, el, structs, enums); + acc[name] = decodeCalldataValue(calldataIterator, el, structs, enums, config); return acc; }, {} as any); } @@ -224,13 +283,29 @@ function decodeCalldataValue( const el: AbiEntry = { name: '', type: getArrayType(element.type) }; const len = BigInt(calldataIterator.next().value); // get length while (parsedDataArr.length < len) { - parsedDataArr.push(decodeCalldataValue(calldataIterator, el, structs, enums)); + const val = decodeCalldataValue(calldataIterator, el, structs, enums, config); + if ( + el.type === 'core::integer::u128' || + el.type === 'core::integer::u8' || + el.type === 'core::integer::u16' + ) { + parsedDataArr.push(Number(val)); + } else { + parsedDataArr.push(val); + } + } + console.log('Returning array: ', parsedDataArr); + const configConstructor = config?.[element.name]; + if (configConstructor) { + const concatenatedString = parsedDataArr.join(''); + return concatenatedString; + } else { + return parsedDataArr; } - return parsedDataArr; } // base type - return decodeBaseTypes(element.type, calldataIterator); + return decodeBaseTypes(element.type, calldataIterator, config); } /** @@ -245,28 +320,29 @@ export default function decodeCalldataField( calldataIterator: Iterator, input: AbiEntry, structs: AbiStructs, - enums: AbiEnums + enums: AbiEnums, + config?: DecodeConfig ): any { const { name, type } = input; switch (true) { case isLen(name): - let temp = calldataIterator.next().value; + const temp = calldataIterator.next().value; return BigInt(temp); case (structs && type in structs) || isTypeTuple(type): - return decodeCalldataValue(calldataIterator, input, structs, enums); + return decodeCalldataValue(calldataIterator, input, structs, enums, config); case enums && isTypeEnum(type, enums): - return decodeCalldataValue(calldataIterator, input, structs, enums); + return decodeCalldataValue(calldataIterator, input, structs, enums, config); case isTypeArray(type): // C1 Array if (isCairo1Type(type)) { - return decodeCalldataValue(calldataIterator, input, structs, enums); + return decodeCalldataValue(calldataIterator, input, structs, enums, config); } default: - return decodeBaseTypes(type, calldataIterator); + return decodeBaseTypes(type, calldataIterator, config); } } diff --git a/src/utils/calldata/index.ts b/src/utils/calldata/index.ts index 2f00a6c45..5f4f239ca 100644 --- a/src/utils/calldata/index.ts +++ b/src/utils/calldata/index.ts @@ -7,6 +7,7 @@ import { Args, ArgsOrCalldata, Calldata, + DecodeConfig, FunctionAbi, HexCalldata, RawArgs, @@ -158,27 +159,6 @@ export class CallData { return callArray; } - /** - * Decompile calldata into JavaScript-compatible types based on ABI definitions. - * @param method The method name as defined in the ABI. - * @param calldata Array of strings representing the encoded calldata. - * @returns A structured object representing the decoded calldata. - */ - public decompile(method: string, calldata: string[]): RawArgs { - const abiMethod = this.abi.find(entry => entry.name === method && entry.type === 'function') as FunctionAbi; - if (!abiMethod) { - throw new Error(`Method ${method} not found in ABI`); - } - - const calldataIterator = calldata.flat()[Symbol.iterator](); - const decodedArgs = abiMethod.inputs.reduce((acc, input) => { - acc[input.name] = decodeCalldataField(calldataIterator, input, this.structs, this.enums); - return acc; - }, {} as RawArgsObject); - - return decodedArgs; - } - /** * Compile contract callData without abi * @param rawArgs RawArgs representing cairo method arguments or string array of compiled data @@ -262,6 +242,48 @@ export class CallData { return callTreeArray; } + /** + * Decompile calldata into JavaScript-compatible types based on ABI definitions. + * @param method The method name as defined in the ABI. + * @param calldata Array of strings representing the encoded calldata. + * @returns A structured object representing the decoded calldata. + */ + public decompile( + method: string, + calldata: string[], + config?: DecodeConfig, + returnArray?: boolean + ): RawArgs { + const abiMethod = this.abi.find( + (entry) => entry.name === method && entry.type === 'function' + ) as FunctionAbi; + if (!abiMethod) { + throw new Error(`Method ${method} not found in ABI`); + } + + const calldataIterator = calldata.flat()[Symbol.iterator](); + const decodedArgs = abiMethod.inputs.reduce((acc, input) => { + acc[input.name] = decodeCalldataField( + calldataIterator, + input, + this.structs, + this.enums, + config + ); + return acc; + }, {} as RawArgsObject); + + if (returnArray === true) { + const decodedArgsArray: RawArgsArray = []; + abiMethod.inputs.forEach((input) => { + const value = decodedArgs[input.name]; + decodedArgsArray.push(value); + }); + } + + return decodedArgs; + } + /** * Parse elements of the response array and structuring them into response object * @param method string - method name diff --git a/src/utils/calldata/requestParser.ts b/src/utils/calldata/requestParser.ts index a022f38f9..d9b693459 100644 --- a/src/utils/calldata/requestParser.ts +++ b/src/utils/calldata/requestParser.ts @@ -23,6 +23,7 @@ import { isTypeEnum, isTypeOption, isTypeEthAddress, + isTypeContractAddress, isTypeResult, isTypeSecp256k1Point, isTypeStruct, @@ -149,8 +150,9 @@ function parseCalldataValue( if (CairoUint512.isAbiType(type)) { return new CairoUint512(element as any).toApiRequest(); } - if (isTypeEthAddress(type)) - return parseBaseTypes(type, element as BigNumberish); + if (isTypeEthAddress(type)) return parseBaseTypes(type, element as BigNumberish); + + if (isTypeContractAddress(type)) return parseBaseTypes(type, element as BigNumberish); if (isTypeByteArray(type)) return parseByteArray(element as string); @@ -301,6 +303,9 @@ export function parseCalldataField( case isTypeEthAddress(type): return parseBaseTypes(type, value); + case isTypeContractAddress(type): + return parseBaseTypes(type, value); + // Struct or Tuple case isTypeStruct(type, structs) || isTypeTuple(type) || diff --git a/src/utils/calldata/responseParser.ts b/src/utils/calldata/responseParser.ts index d2c74e9e6..a379aa3ac 100644 --- a/src/utils/calldata/responseParser.ts +++ b/src/utils/calldata/responseParser.ts @@ -238,7 +238,7 @@ export default function responseParser( switch (true) { case isLen(name): - let temp = responseIterator.next().value; + const temp = responseIterator.next().value; return BigInt(temp); case (structs && type in structs) || isTypeTuple(type): From 764e3165cfb035ef72498d9e536a0cf251fb2b79 Mon Sep 17 00:00:00 2001 From: Aryan Godara Date: Fri, 3 May 2024 21:07:15 +0530 Subject: [PATCH 7/7] fix: reset eslint, remove debug statements, remove array return type --- .eslintignore | 3 +- __tests__/utils/calldataDecode.test.ts | 41 +++-------------------- src/types/lib/contract/abi.ts | 2 -- src/utils/calldata/cairo.ts | 7 +++- src/utils/calldata/calldataDecoder.ts | 45 ++++++++++++++------------ src/utils/calldata/index.ts | 15 +-------- 6 files changed, 37 insertions(+), 76 deletions(-) diff --git a/.eslintignore b/.eslintignore index fabe01b1e..23e705206 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,4 +1,3 @@ node_modules/ dist/ -www/ -src/ \ No newline at end of file +www/ \ No newline at end of file diff --git a/__tests__/utils/calldataDecode.test.ts b/__tests__/utils/calldataDecode.test.ts index 1b02844dc..5dbdfbc90 100644 --- a/__tests__/utils/calldataDecode.test.ts +++ b/__tests__/utils/calldataDecode.test.ts @@ -1,46 +1,18 @@ import { DecodeConfig } from '../../src/types'; import { - // Account, BigNumberish, CairoUint256, - // CairoCustomEnum, - // CairoOption, - // CairoOptionVariant, - // CairoResult, - // CairoResultVariant, - // CairoUint256, - // CairoUint512, CallData, Calldata, - // CompiledSierra, - // Contract, - // DeclareDeployUDCResponse, RawArgsArray, RawArgsObject, - // byteArray, cairo, - // ec, - // hash, - // num, - // selector, - // shortString, - // stark, - // types, - // type Uint512, } from '../../src'; -import { - // compiledC1v2, - // compiledHelloSierra, - compiledComplexSierra, -} from '../config/fixtures'; +import { compiledComplexSierra } from '../config/fixtures'; -const { - // uint256, - tuple, - // isCairo1Abi -} = cairo; +const { tuple } = cairo; describe('Cairo 1', () => { test('should correctly compile and decompile complex data structures', async () => { @@ -131,14 +103,9 @@ describe('Cairo 1', () => { const compiledDataFromObject: Calldata = cd.compile('constructor', myRawArgsObject); const compiledDataFromArray: Calldata = cd.compile('constructor', myRawArgsArray); const decompiledDataFromObject = cd.decompile('constructor', compiledDataFromObject, config); - const decompiledDataFromArray = cd.decompile( - 'constructor', - compiledDataFromArray, - config, - true - ); + const decompiledDataFromArray = cd.decompile('constructor', compiledDataFromArray, config); expect(decompiledDataFromObject).toEqual(myRawArgsObject); - expect(decompiledDataFromArray).toEqual(myRawArgsArray); + expect(decompiledDataFromArray).toEqual(myRawArgsObject); }); }); diff --git a/src/types/lib/contract/abi.ts b/src/types/lib/contract/abi.ts index 72eedab45..d65ec4de9 100644 --- a/src/types/lib/contract/abi.ts +++ b/src/types/lib/contract/abi.ts @@ -1,5 +1,3 @@ -import { Uint256 } from '..'; - /** ABI */ export type Abi = ReadonlyArray; diff --git a/src/utils/calldata/cairo.ts b/src/utils/calldata/cairo.ts index 50922268d..3d38283a2 100644 --- a/src/utils/calldata/cairo.ts +++ b/src/utils/calldata/cairo.ts @@ -96,10 +96,15 @@ export const isTypeUint = (type: string) => Object.values(Uint).includes(type as * @returns {(Uint | null)} - The corresponding Uint enum value or null if not found. */ export const getUintType = (type: string): string | undefined => { - for (const value of Object.values(Uint)) { + const uintValues = Object.values(Uint); + const iterator = uintValues[Symbol.iterator](); + let next = iterator.next(); + while (!next.done) { + const { value } = next; if (value === type) { return value; } + next = iterator.next(); } return undefined; diff --git a/src/utils/calldata/calldataDecoder.ts b/src/utils/calldata/calldataDecoder.ts index 399f9c809..12fcc0676 100644 --- a/src/utils/calldata/calldataDecoder.ts +++ b/src/utils/calldata/calldataDecoder.ts @@ -8,7 +8,6 @@ import { CairoEnum, ParsedStruct, Uint256, - Uint, } from '../../types'; import { CairoUint256 } from '../cairoDataTypes/uint256'; import { CairoUint512 } from '../cairoDataTypes/uint512'; @@ -72,58 +71,62 @@ function decodeBaseTypes( case isTypeUint(type): switch (true) { - case CairoUint256.isAbiType(type): - console.log('got 256 uint value'); + case CairoUint256.isAbiType(type): { const low = it.next().value; const high = it.next().value; const ret = new CairoUint256(low, high); - let configConstructor = config?.['core::integer::u256']; + const configConstructor = config?.['core::integer::u256']; if (configConstructor) { return configConstructor(ret); } + return ret; + } - case CairoUint512.isAbiType(type): + case CairoUint512.isAbiType(type): { const limb0 = it.next().value; const limb1 = it.next().value; const limb2 = it.next().value; const limb3 = it.next().value; return new CairoUint512(limb0, limb1, limb2, limb3).toBigInt(); + } - default: + default: { temp = it.next().value; const configType = getUintType(type); if (configType) { const UintConstructor = config?.[configType]; if (UintConstructor) { return UintConstructor(temp); - } else { - return BigInt(temp); } + return BigInt(temp); } + } } + return BigInt(temp); + case isTypeEthAddress(type): temp = it.next().value; return BigInt(temp); - case isTypeContractAddress(type): + case isTypeContractAddress(type): { temp = it.next().value; temp = toHex(temp); const configConstructor = config?.[type]; if (configConstructor) { return configConstructor(temp); - } else { - return BigInt(temp); } + return BigInt(temp); + } case isTypeBytes31(type): temp = it.next().value; return decodeShortString(temp); - case isTypeSecp256k1Point(type): + case isTypeSecp256k1Point(type): { const xLow = removeHexPrefix(it.next().value).padStart(32, '0'); const xHigh = removeHexPrefix(it.next().value).padStart(32, '0'); const yLow = removeHexPrefix(it.next().value).padStart(32, '0'); @@ -131,21 +134,21 @@ function decodeBaseTypes( const pubK = BigInt(addHexPrefix(xHigh + xLow + yHigh + yLow)); return pubK; + } - case isTypeFelt(type): + case isTypeFelt(type): { temp = String(it.next().value); - console.log('Original temp = ', temp); const configFeltConstructor = config?.['core::felt252']; if (configFeltConstructor) { if (configFeltConstructor === String) return decodeShortString(temp); - else return configFeltConstructor(temp); + return configFeltConstructor(temp); } // Default return BigInt(temp); + } default: - console.log('went to default block for '); temp = it.next().value; return BigInt(temp); } @@ -294,14 +297,12 @@ function decodeCalldataValue( parsedDataArr.push(val); } } - console.log('Returning array: ', parsedDataArr); const configConstructor = config?.[element.name]; if (configConstructor) { const concatenatedString = parsedDataArr.join(''); return concatenatedString; - } else { - return parsedDataArr; } + return parsedDataArr; } // base type @@ -326,9 +327,10 @@ export default function decodeCalldataField( const { name, type } = input; switch (true) { - case isLen(name): + case isLen(name): { const temp = calldataIterator.next().value; return BigInt(temp); + } case (structs && type in structs) || isTypeTuple(type): return decodeCalldataValue(calldataIterator, input, structs, enums, config); @@ -341,8 +343,11 @@ export default function decodeCalldataField( if (isCairo1Type(type)) { return decodeCalldataValue(calldataIterator, input, structs, enums, config); } + break; default: return decodeBaseTypes(type, calldataIterator, config); } + + return null; } diff --git a/src/utils/calldata/index.ts b/src/utils/calldata/index.ts index 5f4f239ca..a5b97825b 100644 --- a/src/utils/calldata/index.ts +++ b/src/utils/calldata/index.ts @@ -248,12 +248,7 @@ export class CallData { * @param calldata Array of strings representing the encoded calldata. * @returns A structured object representing the decoded calldata. */ - public decompile( - method: string, - calldata: string[], - config?: DecodeConfig, - returnArray?: boolean - ): RawArgs { + public decompile(method: string, calldata: string[], config?: DecodeConfig): RawArgs { const abiMethod = this.abi.find( (entry) => entry.name === method && entry.type === 'function' ) as FunctionAbi; @@ -273,14 +268,6 @@ export class CallData { return acc; }, {} as RawArgsObject); - if (returnArray === true) { - const decodedArgsArray: RawArgsArray = []; - abiMethod.inputs.forEach((input) => { - const value = decodedArgs[input.name]; - decodedArgsArray.push(value); - }); - } - return decodedArgs; }