diff --git a/__tests__/utils/CairoTypes/uint256.test.ts b/__tests__/utils/CairoTypes/uint256.test.ts new file mode 100644 index 000000000..41f59e18a --- /dev/null +++ b/__tests__/utils/CairoTypes/uint256.test.ts @@ -0,0 +1,142 @@ +/* eslint-disable no-new */ +import { + CairoUint256, + UINT_256_HIGH_MAX, + UINT_256_HIGH_MIN, + UINT_256_LOW_MAX, + UINT_256_LOW_MIN, + UINT_256_MAX, + UINT_256_MIN, +} from '../../../src/utils/cairoDataTypes/uint256'; + +describe('CairoUint256 class test', () => { + test('constructor 1 should throw on < UINT_256_MIN', () => { + expect(() => { + new CairoUint256(UINT_256_MIN - 1n); + }).toThrow('bigNumberish is smaller than UINT_256_MIN'); + }); + + test('constructor 1 should throw on > UINT_256_MAX', () => { + expect(() => { + new CairoUint256(UINT_256_MAX + 1n); + }).toThrow('bigNumberish is bigger than UINT_256_MAX'); + }); + + test('constructor 2 (low, high)', () => { + const u256 = new CairoUint256(1000, 1000); + expect(u256.toApiRequest()).toEqual(['1000', '1000']); + }); + + test('constructor 2 should throw out of bounds', () => { + expect(() => { + new CairoUint256(UINT_256_LOW_MIN - 1n, 1000); + }).toThrow('low is our of range UINT_256_LOW_MIN - UINT_256_LOW_MAX'); + }); + + test('constructor 2 should throw out of bounds', () => { + expect(() => { + new CairoUint256(UINT_256_LOW_MAX + 1n, 1000); + }).toThrow('low is our of range UINT_256_LOW_MIN - UINT_256_LOW_MAX'); + }); + + test('constructor 2 should throw out of bounds', () => { + expect(() => { + new CairoUint256(1000, UINT_256_HIGH_MIN - 1n); + }).toThrow('high is our of range UINT_256_HIGH_MIN - UINT_256_HIGH_MAX'); + }); + + test('constructor 2 should throw out of bounds', () => { + expect(() => { + new CairoUint256(1000, UINT_256_HIGH_MAX + 1n); + }).toThrow('high is our of range UINT_256_HIGH_MIN - UINT_256_HIGH_MAX'); + }); + + test('constructor 3 ({low, high})', () => { + const u256 = new CairoUint256({ low: 1000, high: 1000 }); + expect(u256.toApiRequest()).toEqual(['1000', '1000']); + }); + + test('constructor 3 should throw out of bounds', () => { + expect(() => { + new CairoUint256({ low: 1000, high: UINT_256_HIGH_MAX + 1n }); + }).toThrow('high is our of range UINT_256_HIGH_MIN - UINT_256_HIGH_MAX'); + }); + + test('validate should throw on < UINT_256_MIN', () => { + expect(() => { + CairoUint256.validate(UINT_256_MIN - 1n); + }).toThrow('bigNumberish is smaller than UINT_256_MIN'); + }); + + test('validate should throw on > UINT_256_MAX', () => { + expect(() => { + CairoUint256.validate(UINT_256_MAX + 1n); + }).toThrow('bigNumberish is bigger than UINT_256_MAX'); + }); + + test('validate should pass and return bigint', () => { + const validate = CairoUint256.validate(UINT_256_MAX); + expect(typeof validate).toBe('bigint'); + }); + + test('is should return true', () => { + const is = CairoUint256.is(UINT_256_MIN); + expect(is).toBe(true); + }); + + test('is should return false', () => { + const is = CairoUint256.is(UINT_256_MAX + 1n); + expect(is).toBe(false); + }); + + test('constructor 1 should support BigNumberish', () => { + const case1 = new CairoUint256(10n); + const case2 = new CairoUint256(10); + const case3 = new CairoUint256('10'); + const case4 = new CairoUint256('0xA'); + + expect(case1).toEqual(case2); + expect(case3).toEqual(case4); + expect(case1).toEqual(case4); + }); + + test('should convert UINT_256_MAX to Uint256 dec struct', () => { + const u256 = new CairoUint256(UINT_256_MAX); + const u256Hex = u256.toUint256DecimalString(); + expect(u256Hex).toMatchInlineSnapshot(` + Object { + "high": "340282366920938463463374607431768211455", + "low": "340282366920938463463374607431768211455", + } + `); + }); + + test('should convert UINT_256_MAX to Uint256 hex struct', () => { + const u256 = new CairoUint256(UINT_256_MAX); + const u256Decimal = u256.toUint256HexString(); + expect(u256Decimal).toMatchInlineSnapshot(` + Object { + "high": "0xffffffffffffffffffffffffffffffff", + "low": "0xffffffffffffffffffffffffffffffff", + } + `); + }); + + test('isAbiType should return true', () => { + const isAbiType = CairoUint256.isAbiType('core::integer::u256'); + expect(isAbiType).toBe(true); + }); + + test('should convert UINT_256_MAX to BN', () => { + const u256 = new CairoUint256(UINT_256_MAX); + expect(u256.toBigInt()).toEqual(UINT_256_MAX); + }); + + test('should convert UINT_256_MAX to API Request', () => { + const u256 = new CairoUint256(UINT_256_MAX); + expect(u256.toApiRequest()).toEqual([ + '340282366920938463463374607431768211455', + '340282366920938463463374607431768211455', + ]); + }); +}); diff --git a/__tests__/utils/uint256.test.ts b/__tests__/utils/uint256.test.ts index bfcc77052..f78332d95 100644 --- a/__tests__/utils/uint256.test.ts +++ b/__tests__/utils/uint256.test.ts @@ -1,17 +1,18 @@ -import { cairo } from '../../src'; -import { UINT_128_MAX, UINT_256_MAX, bnToUint256, uint256ToBN } from '../../src/utils/uint256'; +import { cairo, uint256 as u256 } from '../../src'; + +const { bnToUint256, UINT_128_MAX, UINT_256_MAX, uint256ToBN } = u256; describe('cairo uint256', () => { test('bnToUint256 should not convert -1 from BN to uint256 hex-string struct', () => { expect(() => { - bnToUint256(-1n); - }).toThrow('uint256 must be positive number'); + u256.bnToUint256(-1n); + }).toThrow('bigNumberish is smaller than UINT_256_MIN'); }); test('uint256 should not convert -1 to uint256 dec struct', () => { expect(() => { cairo.uint256(-1n); - }).toThrow('uint256 must be positive number'); + }).toThrow('bigNumberish is smaller than UINT_256_MIN'); }); test('uint256 should convert 1000 to uint256 dec struct', () => { @@ -121,7 +122,7 @@ describe('cairo uint256', () => { test('should throw if BN over uint256 range', () => { expect(() => bnToUint256(UINT_256_MAX + 1n)).toThrowErrorMatchingInlineSnapshot( - `"Number is too large"` + `"bigNumberish is bigger than UINT_256_MAX"` ); }); }); diff --git a/src/utils/cairoDataTypes/felt.ts b/src/utils/cairoDataTypes/felt.ts new file mode 100644 index 000000000..ffbf448b2 --- /dev/null +++ b/src/utils/cairoDataTypes/felt.ts @@ -0,0 +1,39 @@ +// TODO Convert to CairoFelt base on CairoUint256 and implement it in the codebase in the backward compatible manner + +import { BigNumberish, isBigInt, isHex, isStringWholeNumber } from '../num'; +import { encodeShortString, isShortString, isText } from '../shortString'; + +/** + * Create felt Cairo type (cairo type helper) + * @returns format: felt-string + */ +export function CairoFelt(it: BigNumberish): string { + // BN or number + if (isBigInt(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 BigInt(encoded).toString(); + } + // hex string + if (typeof it === 'string' && isHex(it)) { + // toBN().toString + return BigInt(it).toString(); + } + // string number (already converted), or unhandled type + if (typeof it === 'string' && isStringWholeNumber(it)) { + return it; + } + // bool to felt + if (typeof it === 'boolean') { + return `${+it}`; + } + + throw new Error(`${it} can't be computed by felt()`); +} diff --git a/src/utils/cairoDataTypes/uint256.ts b/src/utils/cairoDataTypes/uint256.ts new file mode 100644 index 000000000..470fb24ed --- /dev/null +++ b/src/utils/cairoDataTypes/uint256.ts @@ -0,0 +1,133 @@ +/* eslint-disable no-bitwise */ +/** + * Singular class handling cairo u256 data type + */ + +import { BigNumberish, Uint256 } from '../../types'; +import { addHexPrefix } from '../encode'; +import { CairoFelt } from './felt'; + +export const UINT_128_MAX = (1n << 128n) - 1n; +export const UINT_256_MAX = (1n << 256n) - 1n; +export const UINT_256_MIN = 0n; +export const UINT_256_LOW_MAX = 340282366920938463463374607431768211455n; +export const UINT_256_HIGH_MAX = 340282366920938463463374607431768211455n; +export const UINT_256_LOW_MIN = 0n; +export const UINT_256_HIGH_MIN = 0n; + +export class CairoUint256 { + public low: bigint; + + public high: bigint; + + static abiSelector = 'core::integer::u256'; + + /** + * Default constructor (Lib usage) + * @param bigNumberish BigNumberish value representing uin256 + */ + public constructor(bigNumberish: BigNumberish); + /** + * Direct props initialization (Api response) + */ + public constructor(low: BigNumberish, high: BigNumberish); + /** + * Initialization from Uint256 object + */ + public constructor(uint256: Uint256); + + public constructor(...arr: any[]) { + if (typeof arr === 'object' && arr.length === 1 && arr[0].low && arr[0].high) { + const props = CairoUint256.validateProps(arr[0].low, arr[0].high); + this.low = props.low; + this.high = props.high; + } else if (arr.length === 1) { + const bigInt = CairoUint256.validate(arr[0]); + this.low = bigInt & UINT_128_MAX; + this.high = bigInt >> 128n; + } else if (arr.length === 2) { + const props = CairoUint256.validateProps(arr[0], arr[1]); + this.low = props.low; + this.high = props.high; + } else { + throw Error('Incorrect constructor parameters'); + } + } + + /** + * Validate if BigNumberish can be represented as Unit256 + */ + static validate(bigNumberish: BigNumberish) { + const bigInt = BigInt(bigNumberish); + if (bigInt < UINT_256_MIN) throw Error('bigNumberish is smaller than UINT_256_MIN'); + if (bigInt > UINT_256_MAX) throw new Error('bigNumberish is bigger than UINT_256_MAX'); + return bigInt; + } + + static validateProps(low: BigNumberish, high: BigNumberish) { + const bigIntLow = BigInt(low); + const bigIntHigh = BigInt(high); + if (bigIntLow < UINT_256_LOW_MIN || bigIntLow > UINT_256_LOW_MAX) { + throw new Error('low is our of range UINT_256_LOW_MIN - UINT_256_LOW_MAX'); + } + if (bigIntHigh < UINT_256_HIGH_MIN || bigIntHigh > UINT_256_HIGH_MAX) { + throw new Error('high is our of range UINT_256_HIGH_MIN - UINT_256_HIGH_MAX'); + } + return { low: bigIntLow, high: bigIntHigh }; + } + + /** + * Check if BigNumberish can be represented as Unit256 + */ + static is(bigNumberish: BigNumberish) { + try { + CairoUint256.validate(bigNumberish); + } catch (error) { + return false; + } + return true; + } + + /** + * Check if provided abi type is this data type + */ + static isAbiType(abiType: string) { + return abiType === CairoUint256.abiSelector; + } + + /** + * Return bigint representation + */ + toBigInt() { + return (this.high << 128n) + this.low; + } + + /** + * Return Uint256 structure with HexString props + * {low: HexString, high: HexString} + */ + toUint256HexString() { + return { + low: addHexPrefix(this.low.toString(16)), + high: addHexPrefix(this.high.toString(16)), + }; + } + + /** + * Return Uint256 structure with DecimalString props + * {low: DecString, high: DecString} + */ + toUint256DecimalString() { + return { + low: this.low.toString(10), + high: this.high.toString(10), + }; + } + + /** + * Return api requests representation witch is felt array + */ + toApiRequest() { + return [CairoFelt(this.low), CairoFelt(this.high)]; + } +} diff --git a/src/utils/calldata/cairo.ts b/src/utils/calldata/cairo.ts index 0e9e311a5..5fbf8f2ac 100644 --- a/src/utils/calldata/cairo.ts +++ b/src/utils/calldata/cairo.ts @@ -8,9 +8,8 @@ import { Uint, Uint256, } from '../../types'; -import { isBigInt, isHex, isStringWholeNumber } from '../num'; -import { encodeShortString, isShortString, isText } from '../shortString'; -import { UINT_128_MAX, isUint256 } from '../uint256'; +import { CairoFelt } from '../cairoDataTypes/felt'; +import { CairoUint256 } from '../cairoDataTypes/uint256'; // Intended for internal usage, maybe should be exported somewhere else and not exported to utils export const isLen = (name: string) => /_len$/.test(name); @@ -27,7 +26,8 @@ export const isTypeOption = (type: string) => type.startsWith('core::option::Opt export const isTypeResult = (type: string) => type.startsWith('core::result::Result::'); export const isTypeUint = (type: string) => Object.values(Uint).includes(type as Uint); export const isTypeLitteral = (type: string) => Object.values(Litteral).includes(type as Litteral); -export const isTypeUint256 = (type: string) => type === 'core::integer::u256'; +// Legacy Export +export const isTypeUint256 = (type: string) => CairoUint256.isAbiType(type); export const isTypeBool = (type: string) => type === 'core::bool'; export const isTypeContractAddress = (type: string) => type === 'core::starknet::contract_address::ContractAddress'; @@ -101,15 +101,7 @@ export function getAbiContractVersion(abi: Abi): ContractVersion { * ``` */ export const uint256 = (it: BigNumberish): Uint256 => { - const bn = BigInt(it); - if (bn < 0) throw Error('uint256 must be positive number'); - if (!isUint256(bn)) throw new Error('Number is too large'); - return { - // eslint-disable-next-line no-bitwise - low: (bn & UINT_128_MAX).toString(10), - // eslint-disable-next-line no-bitwise - high: (bn >> 128n).toString(10), - }; + return new CairoUint256(it).toUint256DecimalString(); }; /** @@ -128,32 +120,5 @@ export const tuple = ( * @returns format: felt-string */ export function felt(it: BigNumberish): string { - // BN or number - if (isBigInt(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 BigInt(encoded).toString(); - } - // hex string - if (typeof it === 'string' && isHex(it)) { - // toBN().toString - return BigInt(it).toString(); - } - // string number (already converted), or unhandled type - if (typeof it === 'string' && isStringWholeNumber(it)) { - return it; - } - // bool to felt - if (typeof it === 'boolean') { - return `${+it}`; - } - - throw new Error(`${it} can't be computed by felt()`); + return CairoFelt(it); } diff --git a/src/utils/calldata/propertyOrder.ts b/src/utils/calldata/propertyOrder.ts index 045c02e07..a53d8d591 100644 --- a/src/utils/calldata/propertyOrder.ts +++ b/src/utils/calldata/propertyOrder.ts @@ -1,4 +1,5 @@ import { AbiEntry, AbiEnums, AbiStructs, CairoEnum, RawArgsObject } from '../../types'; +import { CairoUint256 } from '../cairoDataTypes/uint256'; import { getArrayType, isCairo1Type, @@ -11,7 +12,6 @@ import { isTypeResult, isTypeStruct, isTypeTuple, - isTypeUint256, } from './cairo'; import { CairoCustomEnum, @@ -51,7 +51,7 @@ export default function orderPropsByAbi( if (isTypeByteArray(abiType)) { return unorderedItem; } - if (isTypeUint256(abiType)) { + if (CairoUint256.isAbiType(abiType)) { const u256 = unorderedItem; if (typeof u256 !== 'object') { // BigNumberish --> just copy diff --git a/src/utils/calldata/requestParser.ts b/src/utils/calldata/requestParser.ts index 10b80c31c..6ab4e571c 100644 --- a/src/utils/calldata/requestParser.ts +++ b/src/utils/calldata/requestParser.ts @@ -9,6 +9,7 @@ import { Tupled, Uint256, } from '../../types'; +import { CairoUint256 } from '../cairoDataTypes/uint256'; import { encodeShortString, isText, splitLongString } from '../shortString'; import { byteArrayFromString } from './byteArray'; import { @@ -21,8 +22,6 @@ import { isTypeResult, isTypeStruct, isTypeTuple, - isTypeUint256, - uint256, } from './cairo'; import { CairoCustomEnum, @@ -41,10 +40,8 @@ import extractTupleMemberTypes from './tuple'; */ function parseBaseTypes(type: string, val: BigNumberish) { switch (true) { - case isTypeUint256(type): - // eslint-disable-next-line no-case-declarations - const el_uint256 = uint256(val); - return [felt(el_uint256.low), felt(el_uint256.high)]; + case CairoUint256.isAbiType(type): + return new CairoUint256(val).toApiRequest(); case isTypeBytes31(type): return encodeShortString(val.toString()); default: @@ -79,12 +76,7 @@ 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)]; + return new CairoUint256(element as Uint256).toApiRequest(); } function parseByteArray(element: string): string[] { @@ -135,7 +127,7 @@ function parseCalldataValue( // checking if the passed element is struct if (structs[type] && structs[type].members.length) { - if (isTypeUint256(type)) { + if (CairoUint256.isAbiType(type)) { return parseUint256(element); } @@ -161,7 +153,7 @@ function parseCalldataValue( }, [] as string[]); } // check if u256 C1v0 - if (isTypeUint256(type)) { + if (CairoUint256.isAbiType(type)) { return parseUint256(element); } // check if Enum @@ -288,7 +280,7 @@ export function parseCalldataField( case type === 'core::starknet::eth_address::EthAddress': return parseBaseTypes(type, value); // Struct or Tuple - case isTypeStruct(type, structs) || isTypeTuple(type) || isTypeUint256(type): + case isTypeStruct(type, structs) || isTypeTuple(type) || CairoUint256.isAbiType(type): return parseCalldataValue(value as ParsedStruct | BigNumberish[], type, structs, enums); // Enums diff --git a/src/utils/calldata/responseParser.ts b/src/utils/calldata/responseParser.ts index 943e55582..9744fb25e 100644 --- a/src/utils/calldata/responseParser.ts +++ b/src/utils/calldata/responseParser.ts @@ -10,9 +10,9 @@ import { EventEntry, ParsedStruct, } from '../../types'; +import { CairoUint256 } from '../cairoDataTypes/uint256'; import { toHex } from '../num'; import { decodeShortString } from '../shortString'; -import { uint256ToBN } from '../uint256'; import { stringFromByteArray } from './byteArray'; import { getArrayType, @@ -23,7 +23,6 @@ import { isTypeByteArray, isTypeEnum, isTypeTuple, - isTypeUint256, } from './cairo'; import { CairoCustomEnum, @@ -47,10 +46,10 @@ function parseBaseTypes(type: string, it: Iterator) { case isTypeBool(type): temp = it.next().value; return Boolean(BigInt(temp)); - case isTypeUint256(type): + case CairoUint256.isAbiType(type): const low = it.next().value; const high = it.next().value; - return uint256ToBN({ low, high }); + return new CairoUint256(low, high).toBigInt(); case type === 'core::starknet::eth_address::EthAddress': temp = it.next().value; return BigInt(temp); @@ -81,10 +80,10 @@ function parseResponseValue( return {}; } // type uint256 struct (c1v2) - if (isTypeUint256(element.type)) { + if (CairoUint256.isAbiType(element.type)) { const low = responseIterator.next().value; const high = responseIterator.next().value; - return uint256ToBN({ low, high }); + return new CairoUint256(low, high).toBigInt(); } // type C1 ByteArray struct, representing a LongString diff --git a/src/utils/calldata/validate.ts b/src/utils/calldata/validate.ts index a1def7806..7d1ed3191 100644 --- a/src/utils/calldata/validate.ts +++ b/src/utils/calldata/validate.ts @@ -12,9 +12,9 @@ import { Uint, } from '../../types'; import assert from '../assert'; +import { CairoUint256 } from '../cairoDataTypes/uint256'; import { isHex, toBigInt } from '../num'; import { isLongText } from '../shortString'; -import { uint256ToBN } from '../uint256'; import { getArrayType, isLen, @@ -74,7 +74,8 @@ const validateUint = (parameter: any, input: AbiEntry) => { input.type } should be type (String, Number or BigInt), but is ${typeof parameter} ${parameter}.` ); - const param = typeof parameter === 'object' ? uint256ToBN(parameter) : toBigInt(parameter); + const param = + typeof parameter === 'object' ? new CairoUint256(parameter).toBigInt() : toBigInt(parameter); switch (input.type) { case Uint.u8: diff --git a/src/utils/uint256.ts b/src/utils/uint256.ts index d542620f1..e3c5ae0ca 100644 --- a/src/utils/uint256.ts +++ b/src/utils/uint256.ts @@ -1,37 +1,31 @@ /* eslint-disable no-bitwise */ import { BigNumberish, Uint256 } from '../types'; -import { addHexPrefix } from './encode'; -import { toBigInt } from './num'; +import { CairoUint256, UINT_128_MAX, UINT_256_MAX } from './cairoDataTypes/uint256'; -/** @deprecated prefer importing from 'types' over 'uint256' */ -export type { Uint256 }; - -export const UINT_128_MAX = (1n << 128n) - 1n; -export const UINT_256_MAX = (1n << 256n) - 1n; +// Legacy support Export +export { UINT_128_MAX, UINT_256_MAX }; /** * Convert Uint256 to bigint + * Legacy support Export + * @ */ export function uint256ToBN(uint256: Uint256) { - return (toBigInt(uint256.high) << 128n) + toBigInt(uint256.low); + return new CairoUint256(uint256).toBigInt(); } /** * Test BigNumberish is smaller or equal 2**256-1 + * Legacy support Export */ export function isUint256(bn: BigNumberish): boolean { - return toBigInt(bn) <= UINT_256_MAX; + return CairoUint256.is(bn); } /** * Convert BigNumberish (string | number | bigint) to Uint256 (hex) + * Legacy support Export */ export function bnToUint256(bn: BigNumberish): Uint256 { - const bi = BigInt(bn); - if (bi < 0) throw Error('uint256 must be positive number'); - if (!isUint256(bi)) throw new Error('Number is too large'); - return { - low: addHexPrefix((bi & UINT_128_MAX).toString(16)), - high: addHexPrefix((bi >> 128n).toString(16)), - }; + return new CairoUint256(bn).toUint256HexString(); }