Skip to content

Commit

Permalink
Merge pull request #665 from 0xs34n/0.12.0/abi-parser
Browse files Browse the repository at this point in the history
abi parser Cario1 Version2
  • Loading branch information
tabaktoni authored Jul 3, 2023
2 parents ffa6d07 + 03ee060 commit 589c948
Show file tree
Hide file tree
Showing 16 changed files with 9,016 additions and 25 deletions.
File renamed without changes.
8,300 changes: 8,300 additions & 0 deletions __mocks__/cairo/helloCairo2/compiled.json

Large diffs are not rendered by default.

This file was deleted.

542 changes: 542 additions & 0 deletions __tests__/cairo1v2.test.ts

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions __tests__/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ export const compiledHelloSierraCasm = readContractSierraCasm('cairo/helloSierra
export const compiledComplexSierra = readContractSierra('cairo/complexInput/complexInput');
export const compiledC1Account = readContractSierra('cairo/account/account');
export const compiledC1AccountCasm = readContractSierraCasm('cairo/account/account');
export const compiledC1v2 = readContractSierra('cairo/helloCairo2/compiled');
export const compiledC1v2Casm = readContractSierraCasm('cairo/helloCairo2/compiled');

/* Default test config based on run `starknet-devnet --seed 0` */
const DEFAULT_TEST_PROVIDER_SEQUENCER_URL = 'http://127.0.0.1:5050/';
Expand Down
4 changes: 3 additions & 1 deletion src/contract/default.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
} from '../types';
import assert from '../utils/assert';
import { CallData, cairo } from '../utils/calldata';
import { createAbiParser } from '../utils/calldata/parser';
import { ContractInterface } from './interface';

export const splitArgsAndOptions = (args: ArgsOrCalldataWithOptions) => {
Expand Down Expand Up @@ -151,7 +152,8 @@ export class Contract implements ContractInterface {
this.providerOrAccount = providerOrAccount;
this.callData = new CallData(abi);
this.structs = CallData.getAbiStruct(abi);
this.abi = abi;
const parser = createAbiParser(abi);
this.abi = parser.getLegacyFormat();

const options = { enumerable: true, value: {}, writable: false };
Object.defineProperties(this, {
Expand Down
4 changes: 4 additions & 0 deletions src/utils/calldata/cairo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ export const getArrayType = (type: string) => {
export function isCairo1Abi(abi: Abi): boolean {
const firstFunction = abi.find((entry) => entry.type === 'function');
if (!firstFunction) {
if (abi.find((it) => it.type === 'interface')) {
// Expected in Cairo1 version 2
return true;
}
throw new Error(`Error in ABI. No function in ABI.`);
}
if (firstFunction.inputs.length) {
Expand Down
21 changes: 8 additions & 13 deletions src/utils/calldata/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
/* eslint-disable no-plusplus */
import {
Abi,
AbiEntry,
AbiStructs,
Args,
ArgsOrCalldata,
Expand All @@ -19,6 +18,8 @@ import { getSelectorFromName } from '../selector';
import { isLongText, splitLongString } from '../shortString';
import { felt, isLen } from './cairo';
import formatter from './formatter';
import { createAbiParser } from './parser';
import { AbiParserInterface } from './parser/interface';
import orderPropsByAbi from './propertyOrder';
import { parseCalldataField } from './requestParser';
import responseParser from './responseParser';
Expand All @@ -29,11 +30,14 @@ export * as cairo from './cairo';
export class CallData {
abi: Abi;

parser: AbiParserInterface;

protected readonly structs: AbiStructs;

constructor(abi: Abi) {
this.abi = abi;
this.structs = CallData.getAbiStruct(abi);
this.parser = createAbiParser(abi);
this.abi = this.parser.getLegacyFormat();
}

/**
Expand Down Expand Up @@ -61,12 +65,12 @@ export class CallData {
// get requested method from abi
const abiMethod = this.abi.find((abi) =>
type === ValidateType.DEPLOY
? abi.name === method && abi.type === method
? abi.name === method && abi.type === 'constructor'
: abi.name === method && abi.type === 'function'
) as FunctionAbi;

// validate arguments length
const inputsLength = CallData.abiInputsLength(abiMethod.inputs);
const inputsLength = this.parser.methodInputsLength(abiMethod);
if (args.length !== inputsLength) {
throw Error(
`Invalid number of arguments, expected ${inputsLength} arguments, but got ${args.length}`
Expand Down Expand Up @@ -194,15 +198,6 @@ export class CallData {
return formatter(parsed, format);
}

/**
* Helper to calculate inputs from abi
* @param inputs AbiEntry
* @returns number
*/
static abiInputsLength(inputs: AbiEntry[]) {
return inputs.reduce((acc, input) => (!isLen(input.name) ? acc + 1 : acc), 0);
}

/**
* Helper to extract structs from abi
* @param abi Abi
Expand Down
22 changes: 22 additions & 0 deletions src/utils/calldata/parser/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Abi } from '../../../types';
import { isCairo1Abi } from '../cairo';
import { AbiParserInterface } from './interface';
import { AbiParser1 } from './parser-0-1.1.0';
import { AbiParser2 } from './parser-2.0.0';

export function createAbiParser(abi: Abi): AbiParserInterface {
const version = getAbiVersion(abi);
if (version === 0 || version === 1) {
return new AbiParser1(abi);
}
if (version === 2) {
return new AbiParser2(abi);
}
throw Error(`Unsupported ABI version ${version}`);
}

export function getAbiVersion(abi: Abi) {
if (abi.find((it) => it.type === 'interface')) return 2;
if (isCairo1Abi(abi)) return 1;
return 0;
}
23 changes: 23 additions & 0 deletions src/utils/calldata/parser/interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Abi, FunctionAbi } from '../../../types';

export abstract class AbiParserInterface {
/**
* Helper to calculate inputs length from abi
* @param abiMethod FunctionAbi
* @return number
*/
public abstract methodInputsLength(abiMethod: FunctionAbi): number;

/**
*
* @param name string
* @return FunctionAbi | undefined
*/
public abstract getMethod(name: string): FunctionAbi | undefined;

/**
* Return Abi in legacy format
* @return Abi
*/
public abstract getLegacyFormat(): Abi;
}
38 changes: 38 additions & 0 deletions src/utils/calldata/parser/parser-0-1.1.0.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Abi, FunctionAbi } from '../../../types';
import { isLen } from '../cairo';
import { AbiParserInterface } from './interface';

export class AbiParser1 implements AbiParserInterface {
abi: Abi;

constructor(abi: Abi) {
this.abi = abi;
}

/**
* abi method inputs length without '_len' inputs
* cairo 0 reducer
* @param abiMethod FunctionAbi
* @returns number
*/
public methodInputsLength(abiMethod: FunctionAbi) {
return abiMethod.inputs.reduce((acc, input) => (!isLen(input.name) ? acc + 1 : acc), 0);
}

/**
* get method definition from abi
* @param name string
* @returns FunctionAbi | undefined
*/
public getMethod(name: string): FunctionAbi | undefined {
return this.abi.find((it) => it.name === name);
}

/**
* Get Abi in legacy format
* @returns Abi
*/
public getLegacyFormat() {
return this.abi;
}
}
42 changes: 42 additions & 0 deletions src/utils/calldata/parser/parser-2.0.0.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { Abi, FunctionAbi } from '../../../types';
import { AbiParserInterface } from './interface';

export class AbiParser2 implements AbiParserInterface {
abi: Abi;

constructor(abi: Abi) {
this.abi = abi;
}

/**
* abi method inputs length
* @param abiMethod FunctionAbi
* @returns number
*/
public methodInputsLength(abiMethod: FunctionAbi) {
return abiMethod.inputs.length;
}

/**
* get method definition from abi
* @param name string
* @returns FunctionAbi | undefined
*/
public getMethod(name: string): FunctionAbi | undefined {
const intf = this.abi.find((it) => it.type === 'interface');
return intf.items.find((it: any) => it.name === name);
}

/**
* Get Abi in legacy format
* @returns Abi
*/
public getLegacyFormat(): Abi {
return this.abi.flatMap((e) => {
if (e.type === 'interface') {
return e.items;
}
return e;
});
}
}
24 changes: 16 additions & 8 deletions src/utils/calldata/requestParser.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AbiEntry, AbiStructs, BigNumberish, ParsedStruct, Tupled } from '../../types';
import { AbiEntry, AbiStructs, BigNumberish, ParsedStruct, Tupled, Uint256 } from '../../types';
import { isText, splitLongString } from '../shortString';
import {
felt,
Expand Down Expand Up @@ -54,6 +54,15 @@ function parseTuple(element: object, typeStr: string): Tupled[] {
});
}

function parseUint256(element: object | BigNumberish) {
if (typeof element === 'object') {
const { low, high } = element as Uint256;
return [felt(low as BigNumberish), felt(high as BigNumberish)];
}
const el_uint256 = uint256(element);
return [felt(el_uint256.low), felt(el_uint256.high)];
}

/**
* Deep parse of the object that has been passed to the method
*
Expand Down Expand Up @@ -84,6 +93,10 @@ function parseCalldataValue(

// checking if the passed element is struct
if (structs[type] && structs[type].members.length) {
if (isTypeUint256(type)) {
return parseUint256(element);
}

const { members } = structs[type];
const subElement = element as any;

Expand All @@ -100,14 +113,9 @@ function parseCalldataValue(
return acc.concat(parsedData);
}, [] as string[]);
}
// check if u256
// check if u256 C1v0
if (isTypeUint256(type)) {
if (typeof element === 'object') {
const { low, high } = element;
return [felt(low as BigNumberish), felt(high as BigNumberish)];
}
const el_uint256 = uint256(element);
return [felt(el_uint256.low), felt(el_uint256.high)];
return parseUint256(element);
}
if (typeof element === 'object') {
throw Error(`Parameter ${element} do not align with abi parameter ${type}`);
Expand Down
7 changes: 7 additions & 0 deletions src/utils/calldata/responseParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,13 @@ function parseResponseValue(
element: { name: string; type: string },
structs: AbiStructs
): BigNumberish | ParsedStruct | boolean | any[] {
// type uint256 struct (c1v2)
if (isTypeUint256(element.type)) {
const low = responseIterator.next().value;
const high = responseIterator.next().value;
return uint256ToBN({ low, high });
}

// type struct
if (element.type in structs && structs[element.type]) {
return structs[element.type].members.reduce((acc, el) => {
Expand Down
6 changes: 6 additions & 0 deletions src/utils/calldata/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,12 @@ const validateBool = (parameter: any, input: AbiEntry) => {
};

const validateStruct = (parameter: any, input: AbiEntry, structs: AbiStructs) => {
// c1v2 uint256 in struct
if (input.type === Uint.u256) {
validateUint(parameter, input);
return;
}

assert(
typeof parameter === 'object' && !Array.isArray(parameter),
`Validate: arg ${input.name} is cairo type struct (${input.type}), and should be defined as js object (not array)`
Expand Down
5 changes: 3 additions & 2 deletions src/utils/hash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,13 +81,14 @@ export function calculateDeployTransactionHash(
contractAddress: BigNumberish,
constructorCalldata: RawCalldata,
version: BigNumberish,
chainId: StarknetChainId
chainId: StarknetChainId,
constructorName: string = 'constructor'
): string {
return calculateTransactionHashCommon(
TransactionHashPrefix.DEPLOY,
version,
contractAddress,
getSelectorFromName('constructor'),
getSelectorFromName(constructorName),
constructorCalldata,
0,
chainId
Expand Down

0 comments on commit 589c948

Please sign in to comment.