Skip to content

Commit

Permalink
feat: implement longText aka. long string into calldata, remove toFel…
Browse files Browse the repository at this point in the history
…t and resolve circular ref
  • Loading branch information
tabaktoni committed Feb 3, 2023
1 parent 81c4d8c commit da58b5a
Show file tree
Hide file tree
Showing 12 changed files with 3,947 additions and 2,300 deletions.
6,076 changes: 3,841 additions & 2,235 deletions __mocks__/ERC20-echo.json

Large diffs are not rendered by default.

12 changes: 6 additions & 6 deletions __mocks__/cairo/ERC20-echo.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,9 @@ using TupleX = (t1: felt, t2: StructX, t3: felt);

@view
func echo{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}(
t1:felt, n1: felt, t2: felt, k1_len: felt, k1: Struct3*, k2: TupleX, u1: Uint256, s1: Struct1, s2: Struct2, af1_len:felt, af1: felt*, au1_len: felt, au1: Uint256*, as1_len: felt, as1: Struct1*
) -> (t1:felt, n1: felt, t2: felt, k1_len: felt, k1: Struct3*, k2: TupleX, u1: Uint256, s1: Struct1, s2: Struct2, af1_len:felt, af1: felt*, au1_len: felt, au1: Uint256*, as1_len: felt, as1: Struct1*) {
return (t1, n1, t2, k1_len, k1, k2, u1, s1, s2, af1_len, af1, au1_len, au1, as1_len, as1);
t1:felt, n1: felt, tl2_len: felt, tl2: felt*, k1_len: felt, k1: Struct3*, k2: TupleX, u1: Uint256, s1: Struct1, s2: Struct2, af1_len:felt, af1: felt*, au1_len: felt, au1: Uint256*, as1_len: felt, as1: Struct1*
) -> (t1:felt, n1: felt, tl2_len: felt, tl2: felt*, k1_len: felt, k1: Struct3*, k2: TupleX, u1: Uint256, s1: Struct1, s2: Struct2, af1_len:felt, af1: felt*, au1_len: felt, au1: Uint256*, as1_len: felt, as1: Struct1*) {
return (t1, n1, tl2_len, tl2, k1_len, k1, k2, u1, s1, s2, af1_len, af1, au1_len, au1, as1_len, as1);
}

@view
Expand Down Expand Up @@ -114,11 +114,11 @@ func allowance{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}(
//
@external
func iecho{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}(
t1:felt, n1: felt, t2: felt, k1_len: felt, k1: Struct3*, k2: TupleX, u1: Uint256, s1: Struct1, s2: Struct2, af1_len:felt, af1: felt*, au1_len: felt, au1: Uint256*, as1_len: felt, as1: Struct1*
) -> (t1:felt, n1: felt, t2: felt, k1_len: felt, k1: Struct3*, k2: TupleX, u1: Uint256, s1: Struct1, s2: Struct2, af1_len:felt, af1: felt*, au1_len: felt, au1: Uint256*, as1_len: felt, as1: Struct1*) {
t1:felt, n1: felt, tl2_len: felt, tl2: felt*, k1_len: felt, k1: Struct3*, k2: TupleX, u1: Uint256, s1: Struct1, s2: Struct2, af1_len:felt, af1: felt*, au1_len: felt, au1: Uint256*, as1_len: felt, as1: Struct1*
) -> (t1:felt, n1: felt, tl2_len: felt, tl2: felt*, k1_len: felt, k1: Struct3*, k2: TupleX, u1: Uint256, s1: Struct1, s2: Struct2, af1_len:felt, af1: felt*, au1_len: felt, au1: Uint256*, as1_len: felt, as1: Struct1*) {
let (res) = testStorage.read();
testStorage.write(res + t1);
return (t1, n1, t2, k1_len, k1, k2, u1, s1, s2, af1_len, af1, au1_len, au1, as1_len, as1);
return (t1, n1, tl2_len, tl2, k1_len, k1, k2, u1, s1, s2, af1_len, af1, au1_len, au1, as1_len, as1);
}

@external
Expand Down
12 changes: 6 additions & 6 deletions __tests__/contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ describe('Complex interaction', () => {
let erc20Echo20Contract: Contract;
const provider = getTestProvider();
const account = getTestAccount(provider);
const classHash = '0x01a76ad51ccafca47079059ebf9fd577d8bcbbcded0c497852129f42eaaf6bef';
const classHash = '0x06ea0b95e41a5fb7278ef2737ff81d606bd6e9066fd8973684cd7a6907dcf832';
let factory: ContractFactory;

beforeAll(async () => {
Expand Down Expand Up @@ -391,7 +391,7 @@ describe('Complex interaction', () => {
const request = {
t1: 'demo text1',
n1: 123,
t2: 'some text 2',
tl2: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.',
k1: [{ a: 1, b: { b: 2, c: tuple(3, 4, 5, 6) } }],
k2: {
// named tuple
Expand Down Expand Up @@ -428,7 +428,7 @@ describe('Complex interaction', () => {
const formatResponse = {
t1: 'string',
n1: 'number',
t2: 'string',
tl2: 'string',
k1: [
{
a: 'number',
Expand Down Expand Up @@ -496,7 +496,7 @@ describe('Complex interaction', () => {
const result = await erc20Echo20Contract.echo(
request.t1,
request.n1,
request.t2,
request.tl2,
request.k1,
request.k2,
request.u1,
Expand Down Expand Up @@ -525,7 +525,7 @@ describe('Complex interaction', () => {
const result = await erc20Echo20Contract.iecho(
request.t1,
request.n1,
request.t2,
request.tl2,
request.k1,
request.k2,
request.u1,
Expand All @@ -546,7 +546,7 @@ describe('Complex interaction', () => {
CallData.compile(request)
);
expect(
'["474107654995566025798705","123","139552669917337096297914418","1","1","2","3","4","5","6","1","2","3","4","5","6","7","8","9","10","11","5000","0","1","2","1","2","200","1","2","6","1","2","3","4","5","6","4","1000","0","2000","0","3000","0","4000","0","2","10","11","20","22"]'
'["474107654995566025798705","123","8","135049554883004558383340439742929429255072943744440858662311072577337126766","203887170123222058415354283980421533276985178030994883159827760142323294308","196343614134218459150194337625778954700414868493373034945803514629145850912","191491606203201332235940470946533476219373216944002683254566549675726417440","150983476482645969577707455338206408996455974968365254240526141964709732462","196916864427988120570407658938236398782031728400132565646592333804118761826","196909666192589839125749789377187946419246316474617716408635151520594095469","2259304674248048077001042434290734","1","1","2","3","4","5","6","1","2","3","4","5","6","7","8","9","10","11","5000","0","1","2","1","2","200","1","2","6","1","2","3","4","5","6","4","1000","0","2000","0","3000","0","4000","0","2","10","11","20","22"]'
).toBe(JSON.stringify(populated.calldata));

// mark data as compiled (it can be also done manually check defineProperty compiled in CallData.compile)
Expand Down
32 changes: 30 additions & 2 deletions src/utils/calldata/cairo.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { BigNumberish, toBN, toFelt } from '../number';
import BN from 'bn.js';

import { BigNumberish, isHex, isStringWholeNumber, toBN } from '../number';
import { encodeShortString, isShortString, isText } from '../shortString';
import { Uint256, isUint256 } from '../uint256';

export const isLen = (name: string) => /_len$/.test(name);
Expand Down Expand Up @@ -34,4 +37,29 @@ export const tuple = (...args: (BigNumberish | object)[]) => ({ ...args });
/**
* felt cairo type
*/
export const felt = (it: BigNumberish) => toFelt(it);
export function felt(it: BigNumberish): string {
// BN or number
if (BN.isBN(it) || (typeof it === 'number' && Number.isInteger(it))) {
return it.toString();
}
// string text
if (isText(it)) {
if (!isShortString(it as string))
throw new Error(
`${it} is a long string > 31 chars, felt can store short strings, split it to array of short strings`
);
const encoded = encodeShortString(it as string);
return toBN(encoded).toString();
}
// hex string
if (typeof it === 'string' && isHex(it)) {
// toBN().toString
return toBN(it).toString();
}
// string number (already converted), or unhandled type
if (typeof it === 'string' && isStringWholeNumber(it)) {
return it;
}

throw new Error(`${it} can't be computed by felt()`);
}
11 changes: 10 additions & 1 deletion src/utils/calldata/formatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,17 @@ export default function formatter(data: any, type: any) {
}

if (type[key] === 'string') {
if (Array.isArray(data[key])) {
// long string (felt*)
const arrayStr = formatter(
data[key],
data[key].map((_: any) => type[key])
);
acc[key] = Object.values(arrayStr).join('');
return acc;
}
guard.isBN(data, type, key);
acc[key] = decodeShortString(value.toString(16));
acc[key] = decodeShortString(value);
return acc;
}
if (type[key] === 'number') {
Expand Down
9 changes: 7 additions & 2 deletions src/utils/calldata/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import BN from 'bn.js';
import assert from 'minimalistic-assert';

import { Abi, AbiEntry, Args, Calldata, FunctionAbi, abiStructs } from '../../types';
import { isLongText, splitLongString } from '../shortString';
import { felt, isLen } from './cairo';
import formatter from './formatter';
import { parseCalldataField } from './requestParser';
Expand Down Expand Up @@ -88,9 +89,13 @@ export class CallData {
const getEntries = (o: object, prefix = ''): any => {
const oe = Array.isArray(o) ? [o.length.toString(), ...o] : o;
return Object.entries(oe).flatMap(([k, v]) => {
let value = v;
if (isLongText(value)) value = splitLongString(value);
const kk = Array.isArray(oe) && k === '0' ? '$$len' : k;
if (BN.isBN(v)) return [[`${prefix}${kk}`, felt(v)]];
return Object(v) === v ? getEntries(v, `${prefix}${kk}.`) : [[`${prefix}${kk}`, felt(v)]];
if (BN.isBN(value)) return [[`${prefix}${kk}`, felt(value)]];
return Object(value) === value
? getEntries(value, `${prefix}${kk}.`)
: [[`${prefix}${kk}`, felt(value)]];
});
};
return Object.fromEntries(getEntries(obj));
Expand Down
25 changes: 15 additions & 10 deletions src/utils/calldata/requestParser.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { AbiEntry, ParsedStruct, Tupled, abiStructs } from '../../types';
import { BigNumberish, toFelt } from '../number';
import { isTypeArray, isTypeFeltArray, isTypeTuple } from './cairo';
import { BigNumberish } from '../number';
import { isText, splitLongString } from '../shortString';
import { felt, isTypeArray, isTypeFeltArray, isTypeTuple } from './cairo';
import extractTupleMemberTypes from './tuple';

/**
Expand Down Expand Up @@ -63,7 +64,7 @@ function parseCalldataValue(
if (element.length !== structMemberNum) {
throw Error(`Missing parameter for type ${type}`);
}
return element.map((el) => toFelt(el));
return element.map((el) => felt(el));
}
// checking if the passed element is struct
if (structs[type] && structs[type].members.length) {
Expand All @@ -86,7 +87,7 @@ function parseCalldataValue(
if (typeof element === 'object') {
throw Error(`Parameter ${element} do not align with abi parameter ${type}`);
}
return toFelt(element as BigNumberish);
return felt(element as BigNumberish);
}

/**
Expand All @@ -103,21 +104,25 @@ export function parseCalldataField(
structs: abiStructs
): string | string[] {
const { name, type } = input;
const { value } = argsIterator.next();
let { value } = argsIterator.next();

switch (true) {
// When type is Array
case isTypeArray(type):
if (!Array.isArray(value)) {
throw Error(`ABI expected parameter ${name} to be array, got ${value}`);
if (!Array.isArray(value) && !isText(value)) {
throw Error(`ABI expected parameter ${name} to be array or long string, got ${value}`);
}
if (typeof value === 'string') {
// long string match cairo felt*
value = splitLongString(value);
}
// eslint-disable-next-line no-case-declarations
const result: string[] = [];
result.push(toFelt(value.length)); // Add length to array
result.push(felt(value.length)); // Add length to array

return (value as (BigNumberish | ParsedStruct)[]).reduce((acc, el) => {
if (isTypeFeltArray(type)) {
acc.push(toFelt(el as BigNumberish));
acc.push(felt(el as BigNumberish));
} else {
// structure or tuple
acc.push(...parseCalldataValue(el, type.replace('*', ''), structs));
Expand All @@ -129,6 +134,6 @@ export function parseCalldataField(
return parseCalldataValue(value as ParsedStruct | BigNumberish[], type, structs);
// When type is felt or unhandled
default:
return toFelt(value as BigNumberish);
return felt(value as BigNumberish);
}
}
8 changes: 7 additions & 1 deletion src/utils/calldata/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import assert from 'minimalistic-assert';

import { FunctionAbi, abiStructs } from '../../types';
import { BigNumberish } from '../number';
import { isLongText } from '../shortString';
import { isLen, isTypeFelt } from './cairo';
import { calculateStructMembers } from './requestParser';

Expand All @@ -11,6 +12,7 @@ export default function validateFields(
args: Array<any>,
structs: abiStructs
) {
// TODO: This need to be refactored. It need to test by type and than test by parameter, with else throw unknown
// validate parameters
abiMethod.inputs.reduce((acc, input) => {
const parameter = args[acc];
Expand Down Expand Up @@ -44,7 +46,12 @@ export default function validateFields(
} else if (typeof parameter === 'object' && !Array.isArray(parameter)) {
// TODO: skip tuple validation for now
// Type Array
// TODO: fix, this is array is assumption
} else {
// Long string
if (isLongText(parameter)) {
return acc + 1;
}
assert(Array.isArray(parameter), `arg ${input.name} should be an Array`);
// Array of Felts
if (input.type === 'felt*') {
Expand All @@ -54,7 +61,6 @@ export default function validateFields(
`arg ${input.name} should be an array of string, number or BigNumber`
);
});

// Array of Tuple
} else if (/\(felt/.test(input.type)) {
// TODO: This ex. code validate only most basic tuple structure, skip for validation
Expand Down
2 changes: 1 addition & 1 deletion src/utils/encode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export function buf2hex(buffer: Uint8Array) {
*/

export function removeHexPrefix(hex: string): string {
return hex.replace(/^0x/, '');
return hex.replace(/^0x/i, '');
}

export function addHexPrefix(hex: string): string {
Expand Down
13 changes: 3 additions & 10 deletions src/utils/hash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,10 @@ import {
ZERO,
} from '../constants';
import { RawCalldata } from '../types';
import { felt } from './calldata/cairo';
import { ec } from './ellipticCurve';
import { addHexPrefix, buf2hex, removeHexPrefix, utf8ToArray } from './encode';
import {
BigNumberish,
isHex,
isStringWholeNumber,
toBN,
toFelt,
toHex,
toHexString,
} from './number';
import { BigNumberish, isHex, isStringWholeNumber, toBN, toHex, toHexString } from './number';

export const transactionVersion = 1;
export const feeTransactionVersion = toBN(2).pow(toBN(128)).add(toBN(transactionVersion));
Expand Down Expand Up @@ -221,7 +214,7 @@ export function calculateContractAddressFromHash(
) {
const constructorCalldataHash = computeHashOnElements(constructorCalldata);

const CONTRACT_ADDRESS_PREFIX = toFelt('0x535441524b4e45545f434f4e54524143545f41444452455353'); // Equivalent to 'STARKNET_CONTRACT_ADDRESS'
const CONTRACT_ADDRESS_PREFIX = felt('0x535441524b4e45545f434f4e54524143545f41444452455353'); // Equivalent to 'STARKNET_CONTRACT_ADDRESS'

const dataToHash = [
CONTRACT_ADDRESS_PREFIX,
Expand Down
24 changes: 0 additions & 24 deletions src/utils/number.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import BN from 'bn.js';
import assert from 'minimalistic-assert';

import { addHexPrefix, removeHexPrefix } from './encode';
import { encodeShortString } from './shortString';

export type BigNumberish = string | number | BN;

Expand Down Expand Up @@ -33,29 +32,6 @@ export function hexToDecimalString(hex: string): string {
return toBN(`0x${hex.replace(/^0x/, '')}`).toString();
}

export function toFelt(it: BigNumberish): string {
// BN or number
if (BN.isBN(it) || (typeof it === 'number' && Number.isInteger(it))) {
return it.toString();
}
// string text
if (typeof it === 'string' && !isHex(it) && !isStringWholeNumber(it)) {
const encoded = encodeShortString(it);
return toBN(encoded).toString();
}
// hex string
if (typeof it === 'string' && isHex(it)) {
// toBN().toString
return toBN(it).toString();
}
// string number (already converted), or unhandled type
if (typeof it === 'string' && isStringWholeNumber(it)) {
return it;
}

throw new Error(`${it} can't be computed by toFelt()`);
}

/**
* Remove hex string leading zero and lower case '0x01A'.. -> '0x1a..'
* @param hex string
Expand Down
23 changes: 21 additions & 2 deletions src/utils/shortString.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { addHexPrefix, removeHexPrefix } from './encode';
import { isHex } from './number';
import { isHex, isStringWholeNumber } from './number';

const TEXT_TO_FELT_MAX_LEN = 31;

export function isASCII(str: string) {
// eslint-disable-next-line no-control-regex
Expand All @@ -8,14 +10,31 @@ export function isASCII(str: string) {

// function to check if string has less or equal 31 characters
export function isShortString(str: string) {
return str.length <= 31;
return str.length <= TEXT_TO_FELT_MAX_LEN;
}

// function to check if string is a decimal
export function isDecimalString(decim: string): boolean {
return /^[0-9]*$/i.test(decim);
}

/**
* check if value is string text, and not string-hex, string-number
* @param val any
* @returns boolean
*/
export function isText(val: any) {
return typeof val === 'string' && !isHex(val) && !isStringWholeNumber(val);
}

export const isShortText = (val: any) => isText(val) && isShortString(val);
export const isLongText = (val: any) => isText(val) && !isShortString(val);

export function splitLongString(longStr: string): string[] {
const regex = RegExp(`[^]{1,${TEXT_TO_FELT_MAX_LEN}}`, 'g');
return longStr.match(regex) || [];
}

/**
* Convert an ASCII string to an hexadecimal string.
* @param str - ASCII string -
Expand Down

0 comments on commit da58b5a

Please sign in to comment.