diff --git a/.github/workflows/_test.yml b/.github/workflows/_test.yml index 75946c680..31a088e05 100644 --- a/.github/workflows/_test.yml +++ b/.github/workflows/_test.yml @@ -21,7 +21,7 @@ jobs: # TODO - periodically check if conditional services are supported; https://github.com/actions/runner/issues/822 services: devnet: - image: ${{ (inputs.use-devnet) && 'shardlabs/starknet-devnet-rs:0.1.2-seed0' || '' }} + image: ${{ (inputs.use-devnet) && 'shardlabs/starknet-devnet-rs:0.2.0' || '' }} ports: - 5050:5050 diff --git a/CHANGELOG.md b/CHANGELOG.md index cf58a8ea1..2539c6f1d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## [6.14.1](https://github.com/starknet-io/starknet.js/compare/v6.14.0...v6.14.1) (2024-09-30) + +### Bug Fixes + +- adjust module configuration ([47e52cf](https://github.com/starknet-io/starknet.js/commit/47e52cf39a71bf99188edc4991b002018e296504)) + # [6.14.0](https://github.com/starknet-io/starknet.js/compare/v6.13.1...v6.14.0) (2024-09-04) ### Features diff --git a/__tests__/cairo1v2_typed.test.ts b/__tests__/cairo1v2_typed.test.ts index ac01daf1e..a45bbd34e 100644 --- a/__tests__/cairo1v2_typed.test.ts +++ b/__tests__/cairo1v2_typed.test.ts @@ -27,7 +27,8 @@ import { types, } from '../src'; import { hexToDecimalString } from '../src/utils/num'; -import { encodeShortString, isString } from '../src/utils/shortString'; +import { encodeShortString } from '../src/utils/shortString'; +import { isString } from '../src/utils/typed'; import { TEST_TX_VERSION, compiledC1Account, diff --git a/__tests__/config/fixtures.ts b/__tests__/config/fixtures.ts index b32c7e648..818472344 100644 --- a/__tests__/config/fixtures.ts +++ b/__tests__/config/fixtures.ts @@ -118,12 +118,21 @@ export const getTestAccount = (provider: ProviderInterface) => { export const createBlockForDevnet = async (): Promise => { if (!(process.env.IS_DEVNET === 'true')) return; - await fetch(new URL('/create_block', process.env.TEST_RPC_URL), { method: 'POST' }); + const response = await fetch(new URL('/create_block', process.env.TEST_RPC_URL), { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: '{}', + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`DEVNET status ${response.status}: ${errorText}`); + } }; export async function waitNextBlock(provider: RpcProvider, delay: number) { const initBlock = await provider.getBlockNumber(); - createBlockForDevnet(); + await createBlockForDevnet(); let isNewBlock: boolean = false; while (!isNewBlock) { // eslint-disable-next-line no-await-in-loop diff --git a/__tests__/config/schema.ts b/__tests__/config/schema.ts index e985a3f35..e27f4f343 100644 --- a/__tests__/config/schema.ts +++ b/__tests__/config/schema.ts @@ -11,7 +11,7 @@ import componentSchemas from '../schemas/component.json'; import libSchemas from '../schemas/lib.json'; import providerSchemas from '../schemas/provider.json'; import rpcSchemas from '../schemas/rpc.json'; -import { isBigInt } from '../../src/utils/num'; +import { isBigInt } from '../../src/utils/typed'; const matcherSchemas = [accountSchemas, libSchemas, providerSchemas, rpcSchemas]; const starknetSchemas = [ diff --git a/__tests__/factories/abi.ts b/__tests__/factories/abi.ts new file mode 100644 index 000000000..84a6d42d3 --- /dev/null +++ b/__tests__/factories/abi.ts @@ -0,0 +1,47 @@ +import type { InterfaceAbi, AbiEntry, AbiEnums, AbiStructs, FunctionAbi } from '../../src'; + +export const getAbiEntry = (type: string): AbiEntry => ({ name: 'test', type }); + +export const getFunctionAbi = (inputsType: string): FunctionAbi => ({ + inputs: [getAbiEntry(inputsType)], + name: 'test', + outputs: [getAbiEntry(inputsType)], + stateMutability: 'view', + type: 'function', +}); + +export const getInterfaceAbi = (functionAbiType: string = 'struct'): InterfaceAbi => ({ + items: [getFunctionAbi(functionAbiType)], + name: 'test_interface_abi', + type: 'interface', +}); + +export const getAbiStructs = (): AbiStructs => ({ + struct: { + members: [ + { + name: 'test_name', + type: 'test_type', + offset: 1, + }, + ], + size: 2, + name: 'cairo__struct', + type: 'struct', + }, +}); + +export const getAbiEnums = (): AbiEnums => ({ + enum: { + variants: [ + { + name: 'test_name', + type: 'cairo_struct_variant', + offset: 1, + }, + ], + size: 2, + name: 'test_cairo', + type: 'enum', + }, +}); diff --git a/__tests__/utils/assert.test.ts b/__tests__/utils/assert.test.ts new file mode 100644 index 000000000..5e4bb4c7d --- /dev/null +++ b/__tests__/utils/assert.test.ts @@ -0,0 +1,15 @@ +import assert from '../../src/utils/assert'; + +describe('assert', () => { + test('should throw an error if condition is not true', () => { + expect(() => assert(false)).toThrow(new Error('Assertion failure')); + }); + + test('should throw an error with a specific message', () => { + expect(() => assert(false, 'Error message')).toThrow(new Error('Error message')); + }); + + test('should not throw an error if condition is true', () => { + expect(() => assert(true)).toBeTruthy(); + }); +}); diff --git a/__tests__/utils/CairoTypes/CairoFelt.test.ts b/__tests__/utils/cairoDataTypes/CairoFelt.test.ts similarity index 100% rename from __tests__/utils/CairoTypes/CairoFelt.test.ts rename to __tests__/utils/cairoDataTypes/CairoFelt.test.ts diff --git a/__tests__/utils/CairoTypes/CairoUint256.test.ts b/__tests__/utils/cairoDataTypes/CairoUint256.test.ts similarity index 100% rename from __tests__/utils/CairoTypes/CairoUint256.test.ts rename to __tests__/utils/cairoDataTypes/CairoUint256.test.ts diff --git a/__tests__/utils/CairoTypes/CairoUint512.test.ts b/__tests__/utils/cairoDataTypes/CairoUint512.test.ts similarity index 100% rename from __tests__/utils/CairoTypes/CairoUint512.test.ts rename to __tests__/utils/cairoDataTypes/CairoUint512.test.ts diff --git a/__tests__/utils/calldata/byteArray.test.ts b/__tests__/utils/calldata/byteArray.test.ts new file mode 100644 index 000000000..3cd5654c0 --- /dev/null +++ b/__tests__/utils/calldata/byteArray.test.ts @@ -0,0 +1,23 @@ +import { stringFromByteArray, byteArrayFromString } from '../../../src/utils/calldata/byteArray'; + +describe('stringFromByteArray', () => { + test('should return string from Cairo byte array', () => { + const str = stringFromByteArray({ + data: [], + pending_word: '0x414243444546474849', + pending_word_len: 9, + }); + expect(str).toEqual('ABCDEFGHI'); + }); +}); + +describe('byteArrayFromString', () => { + test('should return Cairo byte array from string', () => { + const byteArray = byteArrayFromString('ABCDEFGHI'); + expect(byteArray).toEqual({ + data: [], + pending_word: '0x414243444546474849', + pending_word_len: 9, + }); + }); +}); diff --git a/__tests__/utils/calldata/cairo.test.ts b/__tests__/utils/calldata/cairo.test.ts new file mode 100644 index 000000000..2c43cda3e --- /dev/null +++ b/__tests__/utils/calldata/cairo.test.ts @@ -0,0 +1,318 @@ +import { + isLen, + isTypeFelt, + isTypeUint, + isTypeUint256, + isTypeArray, + uint256, + uint512, + isTypeTuple, + isTypeNamedTuple, + isTypeStruct, + isTypeEnum, + isTypeOption, + isTypeResult, + isTypeLiteral, + isTypeBool, + isTypeContractAddress, + isTypeEthAddress, + isTypeBytes31, + isTypeByteArray, + isTypeSecp256k1Point, + isCairo1Type, + getArrayType, + isCairo1Abi, + isTypeNonZero, + getAbiContractVersion, + tuple, + felt, +} from '../../../src/utils/calldata/cairo'; +import { ETH_ADDRESS, Literal, Uint, type ContractVersion, NON_ZERO_PREFIX } from '../../../src'; +import { getFunctionAbi, getAbiEnums, getAbiStructs, getInterfaceAbi } from '../../factories/abi'; + +describe('isLen', () => { + test('should return true if name ends with "_len"', () => { + expect(isLen('test_len')).toEqual(true); + }); + + test('should return false if name does not end with "_len"', () => { + expect(isLen('test')).toEqual(false); + }); +}); + +describe('isTypeFelt', () => { + test('should return true if given type is Felt', () => { + expect(isTypeFelt('felt')).toEqual(true); + expect(isTypeFelt('core::felt252')).toEqual(true); + }); + + test('should return false if given type is not Felt', () => { + expect(isTypeFelt('core::bool')).toEqual(false); + }); +}); + +describe('isTypeArray', () => { + test('should return true if given type is an Array', () => { + expect(isTypeArray('core::array::Array::')).toEqual(true); + expect(isTypeArray('core::array::Span::')).toEqual(true); + expect(isTypeArray('felt*')).toEqual(true); + }); + + test('should return false if given type is not an Array ', () => { + expect(isTypeArray('core::bool')).toEqual(false); + }); +}); + +describe('isTypeTuple', () => { + test('should return true if given type is Tuple', () => { + expect(isTypeTuple('(core::bool, felt)')).toEqual(true); + }); + + test('should return false if given type is not Tuple ', () => { + expect(isTypeTuple('core::bool')).toEqual(false); + }); +}); + +describe('isTypeNamedTuple', () => { + test('should return true if given type is named Tuple', () => { + expect(isTypeNamedTuple('(core::bool, core::bool)')).toEqual(true); + expect(isTypeNamedTuple('(core::bool, felt)')).toEqual(true); + }); + + test('should return false if given type is not named Tuple ', () => { + expect(isTypeNamedTuple('(felt, felt)')).toEqual(false); + }); +}); + +describe('isTypeStruct', () => { + test('should return true if given type is Struct', () => { + expect(isTypeStruct('struct', getAbiStructs())).toEqual(true); + }); + + test('should return false if given type is not Struct', () => { + expect(isTypeStruct('struct', { test: getAbiStructs().struct })).toEqual(false); + }); +}); + +describe('isTypeEnum', () => { + test('should return true if given type is Enum', () => { + expect(isTypeEnum('enum', getAbiEnums())).toEqual(true); + }); + + test('should return false if given type is not Enum', () => { + expect(isTypeEnum('enum', { test: getAbiEnums().enum })).toEqual(false); + }); +}); + +describe('isTypeOption', () => { + test('should return true if given type is Option', () => { + expect(isTypeOption('core::option::Option::core::bool')).toEqual(true); + }); + + test('should return false if given type is not Option', () => { + expect(isTypeOption('core::bool')).toEqual(false); + }); +}); + +describe('isTypeResult', () => { + test('should return true if given type is Result', () => { + expect(isTypeResult('core::result::Result::core::bool')).toEqual(true); + }); + + test('should return false if given type is not Result', () => { + expect(isTypeResult('core::bool')).toEqual(false); + }); +}); + +describe('isTypeUint', () => { + test('should return true if given type is Uint', () => { + Object.values(Uint).forEach((uint) => { + expect(isTypeUint(uint)).toEqual(true); + }); + }); + + test('should return false if given type is not Uint', () => { + expect(isTypeUint('core::bool')).toEqual(false); + }); +}); + +describe('isTypeUint256', () => { + test('should return true if given type is Uint256', () => { + expect(isTypeUint256('core::integer::u256')).toEqual(true); + }); + + test('should return false if given type is not Uint256', () => { + expect(isTypeUint256('core::bool')).toEqual(false); + }); +}); + +describe('isTypeLiteral', () => { + test('should return true if given type is Literal', () => { + Object.values(Literal).forEach((literal) => { + expect(isTypeLiteral(literal)).toEqual(true); + }); + }); + + test('should return false if given type is not Literal', () => { + expect(isTypeLiteral('core::bool')).toEqual(false); + }); +}); + +describe('isTypeBool', () => { + test('should return true if given type is Bool', () => { + expect(isTypeBool('core::bool')).toEqual(true); + }); + + test('should return false if given type is not Bool', () => { + expect(isTypeBool(Uint.u8)).toEqual(false); + }); +}); + +describe('isTypeContractAddress', () => { + test('should return true if given type is ContractAddress', () => { + expect(isTypeContractAddress(Literal.ContractAddress)).toEqual(true); + }); + + test('should return false if given type is not ContractAddress', () => { + expect(isTypeContractAddress(Uint.u8)).toEqual(false); + }); +}); + +describe('isTypeEthAddress', () => { + test('should return true if given type is EthAddress', () => { + expect(isTypeEthAddress(ETH_ADDRESS)).toEqual(true); + }); + + test('should return false if given type is not EthAddress', () => { + expect(isTypeEthAddress(Literal.ContractAddress)).toEqual(false); + }); +}); + +describe('isTypeBytes31', () => { + test('should return true if given type is Bytes31', () => { + expect(isTypeBytes31('core::bytes_31::bytes31')).toEqual(true); + }); + + test('should return false if given type is not Bytes31', () => { + expect(isTypeBytes31('core::bool')).toEqual(false); + }); +}); + +describe('isTypeByteArray', () => { + test('should return true if given type is ByteArray', () => { + expect(isTypeByteArray('core::byte_array::ByteArray')).toEqual(true); + }); + + test('should return false if given type is not ByteArray', () => { + expect(isTypeByteArray('core::bool')).toEqual(false); + }); +}); + +describe('isTypeSecp256k1Point', () => { + test('should return true if given type is Secp256k1Point', () => { + expect(isTypeSecp256k1Point(Literal.Secp256k1Point)).toEqual(true); + }); + + test('should return false if given type is not Secp256k1Point', () => { + expect(isTypeSecp256k1Point('core::bool')).toEqual(false); + }); +}); + +describe('isCairo1Type', () => { + test('should return true if given type is Cairo1', () => { + expect(isCairo1Type('core::bool')).toEqual(true); + }); + + test('should return false if given type is not Cairo1', () => { + expect(isCairo1Type('felt')).toEqual(false); + }); +}); + +describe('getArrayType', () => { + test('should extract type from an array', () => { + expect(getArrayType('felt*')).toEqual('felt'); + expect(getArrayType('core::array::Array::')).toEqual('core::bool'); + }); +}); + +describe('isTypeNonZero', () => { + test('should return true if given type is NonZero', () => { + expect(isTypeNonZero(`${NON_ZERO_PREFIX}core::bool`)).toEqual(true); + }); + + test('should return false if given type is not NonZero', () => { + expect(isTypeNonZero('core::bool')).toEqual(false); + }); +}); + +describe('isCairo1Abi', () => { + test('should return true if ABI comes from Cairo 1 contract', () => { + expect(isCairo1Abi([getInterfaceAbi()])).toEqual(true); + }); + + test('should return false if ABI comes from Cairo 0 contract', () => { + expect(isCairo1Abi([getFunctionAbi('felt')])).toEqual(false); + }); + + test('should throw an error if ABI does not come from Cairo 1 contract ', () => { + expect(() => isCairo1Abi([{}])).toThrow(new Error('Unable to determine Cairo version')); + }); +}); + +describe('getAbiContractVersion', () => { + test('should return Cairo 0 contract version', () => { + const contractVersion: ContractVersion = getAbiContractVersion([getFunctionAbi('felt')]); + expect(contractVersion).toEqual({ cairo: '0', compiler: '0' }); + }); + + test('should return Cairo 1 with compiler 2 contract version', () => { + const contractVersion: ContractVersion = getAbiContractVersion([getInterfaceAbi()]); + expect(contractVersion).toEqual({ cairo: '1', compiler: '2' }); + }); + + test('should return Cairo 1 with compiler 1 contract version', () => { + const contractVersion: ContractVersion = getAbiContractVersion([getFunctionAbi('core::bool')]); + expect(contractVersion).toEqual({ cairo: '1', compiler: '1' }); + }); + + test('should return undefined values for cairo and compiler', () => { + const contractVersion: ContractVersion = getAbiContractVersion([{}]); + expect(contractVersion).toEqual({ cairo: undefined, compiler: undefined }); + }); +}); + +describe('uint256', () => { + test('should create Uint256 Cairo type', () => { + const uint = uint256('892349863487563453485768723498'); + expect(uint).toEqual({ low: '892349863487563453485768723498', high: '0' }); + }); +}); + +describe('uint512', () => { + test('should create Uint512 Cairo type', () => { + const uint = uint512('345745685892349863487563453485768723498'); + expect(uint).toEqual({ + limb0: '5463318971411400024188846054000512042', + limb1: '1', + limb2: '0', + limb3: '0', + }); + }); +}); + +describe('tuple', () => { + test('should create unnamed Cairo type tuples', () => { + const tuples = [tuple(true, false), tuple(1, '0x101', 16)]; + expect(tuples).toEqual([ + { '0': true, '1': false }, + { '0': 1, '1': '0x101', '2': 16 }, + ]); + }); +}); + +describe('felt', () => { + test('should create Cairo type felts', () => { + const felts = [felt('test'), felt(256n), felt(1234)]; + expect(felts).toEqual(['1952805748', '256', '1234']); + }); +}); diff --git a/__tests__/utils/calldata/enum/CairoCustomEnum.test.ts b/__tests__/utils/calldata/enum/CairoCustomEnum.test.ts new file mode 100644 index 000000000..154e5bed2 --- /dev/null +++ b/__tests__/utils/calldata/enum/CairoCustomEnum.test.ts @@ -0,0 +1,35 @@ +import { CairoCustomEnum } from '../../../../src/utils/calldata/enum'; + +describe('CairoCustomEnum', () => { + describe('constructor', () => { + test('should set "variant" if enum content is provided', () => { + const cairoCustomEnum = new CairoCustomEnum({ test: 'custom_enum' }); + expect(cairoCustomEnum.variant).toEqual({ test: 'custom_enum' }); + }); + + test('should throw an error if enum does not have any variant', () => { + const error = new Error('This Enum must have at least 1 variant'); + expect(() => new CairoCustomEnum({})).toThrow(error); + }); + + test('should throw an error if there is more then one active variant', () => { + const content = { test: 'custom_enum', test2: 'custom_enum_2' }; + const error = new Error('This Enum must have exactly one active variant'); + expect(() => new CairoCustomEnum(content)).toThrow(error); + }); + }); + + describe('unwrap', () => { + test('should return content of the valid variant', () => { + const cairoCustomEnum = new CairoCustomEnum({ test: undefined, test2: 'test_2' }); + expect(cairoCustomEnum.unwrap()).toEqual('test_2'); + }); + }); + + describe('activeVariant', () => { + test('should return the name of the valid variant', () => { + const cairoCustomEnum = new CairoCustomEnum({ test: undefined, test2: 'test_2' }); + expect(cairoCustomEnum.activeVariant()).toEqual('test2'); + }); + }); +}); diff --git a/__tests__/utils/calldata/enum/CairoOption.test.ts b/__tests__/utils/calldata/enum/CairoOption.test.ts new file mode 100644 index 000000000..b1dfaff76 --- /dev/null +++ b/__tests__/utils/calldata/enum/CairoOption.test.ts @@ -0,0 +1,55 @@ +import { CairoOption } from '../../../../src/utils/calldata/enum'; + +describe('CairoOption', () => { + describe('constructor', () => { + test('should set "Some" if variant is 0', () => { + const cairoOption = new CairoOption(0, 'option_content'); + expect(cairoOption.Some).toEqual('option_content'); + expect(cairoOption.None).toBeUndefined(); + }); + + test('should set "None" if variant is 1', () => { + const cairoOption = new CairoOption(1, 'option_content'); + expect(cairoOption.None).toEqual(true); + expect(cairoOption.Some).toBeUndefined(); + }); + + test('should throw an error if wrong variant is provided', () => { + expect(() => new CairoOption(2, 'option_content')).toThrow( + new Error('Wrong variant! It should be CairoOptionVariant.Some or .None.') + ); + }); + + test('should throw an error if content is undefined or not provided', () => { + expect(() => new CairoOption(0)).toThrow( + new Error('The creation of a Cairo Option with "Some" variant needs a content as input.') + ); + }); + }); + + describe('unwrap', () => { + test('should return undefined if "None" value is set', () => { + const cairoOption = new CairoOption(1, 'option_content'); + expect(cairoOption.unwrap()).toBeUndefined(); + }); + + test('should return "Some" value if it is set', () => { + const cairoOption = new CairoOption(0, 'option_content'); + expect(cairoOption.unwrap()).toEqual('option_content'); + }); + }); + + describe('isSome', () => { + test('should return true if "Some" value is set', () => { + const cairoOption = new CairoOption(0, 'option_content'); + expect(cairoOption.isSome()).toEqual(true); + }); + }); + + describe('isNone', () => { + test('should return true if "None" value is set', () => { + const cairoOption = new CairoOption(1, 'option_content'); + expect(cairoOption.isNone()).toEqual(true); + }); + }); +}); diff --git a/__tests__/utils/calldata/enum/CairoResult.test.ts b/__tests__/utils/calldata/enum/CairoResult.test.ts new file mode 100644 index 000000000..a039e5d28 --- /dev/null +++ b/__tests__/utils/calldata/enum/CairoResult.test.ts @@ -0,0 +1,49 @@ +import { CairoResult } from '../../../../src/utils/calldata/enum'; + +describe('CairoResult', () => { + describe('constructor', () => { + test('should set "Ok" if variant is 0', () => { + const cairoResult = new CairoResult(0, 'result_content'); + expect(cairoResult.Ok).toEqual('result_content'); + expect(cairoResult.Err).toBeUndefined(); + }); + + test('should set "Err" if variant is 1', () => { + const cairoResult = new CairoResult(1, 'result_content'); + expect(cairoResult.Err).toEqual('result_content'); + expect(cairoResult.Ok).toBeUndefined(); + }); + + test('should throw an error if wrong variant is provided', () => { + expect(() => new CairoResult(2, 'result_content')).toThrow( + new Error('Wrong variant! It should be CairoResultVariant.Ok or .Err.') + ); + }); + }); + + describe('unwrap', () => { + test('should return "Ok" value', () => { + const cairoResult = new CairoResult(0, 'result_content'); + expect(cairoResult.unwrap()).toEqual('result_content'); + }); + + test('should return "Err" value', () => { + const cairoResult = new CairoResult(1, 'result_content'); + expect(cairoResult.unwrap()).toEqual('result_content'); + }); + }); + + describe('isOk', () => { + test('should return true if "Ok" value is set', () => { + const cairoResult = new CairoResult(0, 'result_content'); + expect(cairoResult.isOk()).toEqual(true); + }); + }); + + describe('isErr', () => { + test('should return true if "Err" value is set', () => { + const cairoResult = new CairoResult(1, 'result_content'); + expect(cairoResult.isErr()).toEqual(true); + }); + }); +}); diff --git a/__tests__/utils/calldata/formatter.test.ts b/__tests__/utils/calldata/formatter.test.ts new file mode 100644 index 000000000..a44cabe44 --- /dev/null +++ b/__tests__/utils/calldata/formatter.test.ts @@ -0,0 +1,43 @@ +import formatter from '../../../src/utils/calldata/formatter'; +import { toBigInt } from '../../../src/utils/num'; + +describe('formatter', () => { + test('should format one level depth object', () => { + const data = { value: toBigInt(1000), name: toBigInt(1) }; + const type = { value: 'number', name: 'string' }; + const formatted = formatter(data, type); + expect(formatted).toEqual({ value: 1000, name: '1' }); + }); + + test('should format nested object', () => { + const data = { test: { id: toBigInt(123), value: toBigInt(30) }, active: toBigInt(1) }; + const type = { test: { id: 'number', value: 'number' }, active: 'number' }; + const formatted = formatter(data, type); + expect(formatted).toEqual({ test: { id: 123, value: 30 }, active: 1 }); + }); + + test('should format object that has arrays in it', () => { + const data = { items: [toBigInt(1), toBigInt(2), toBigInt(3)], value: toBigInt(1) }; + const type = { items: ['number'], value: 'string' }; + const formatted = formatter(data, type); + expect(formatted).toEqual({ items: [1, 2, 3], value: '1' }); + }); + + test('should throw an error if at least one of the value is not Big Int', () => { + const data = { value: '123', name: toBigInt(1) }; + const type = { value: 'number', name: 'string' }; + expect(() => formatter(data, type)).toThrow( + new Error( + 'Data and formatter mismatch on value:number, expected response data value:123 to be BN instead it is string' + ) + ); + }); + + test('should throw an error for unhandled formatter types', () => { + const data = { value: toBigInt(1) }; + const type = { value: 'symbol' }; + expect(() => formatter(data, type)).toThrow( + new Error('Unhandled formatter type on value:symbol for data value:1') + ); + }); +}); diff --git a/__tests__/utils/calldata/parser/parser-0-1.1.0.test.ts b/__tests__/utils/calldata/parser/parser-0-1.1.0.test.ts new file mode 100644 index 000000000..5a33dfd8f --- /dev/null +++ b/__tests__/utils/calldata/parser/parser-0-1.1.0.test.ts @@ -0,0 +1,44 @@ +import { AbiParser1 } from '../../../../src/utils/calldata/parser/parser-0-1.1.0'; +import { getFunctionAbi, getInterfaceAbi } from '../../../factories/abi'; + +describe('AbiParser1', () => { + test('should create an instance', () => { + const abiParser = new AbiParser1([getFunctionAbi('struct')]); + expect(abiParser instanceof AbiParser1).toEqual(true); + expect(abiParser.abi).toStrictEqual([getFunctionAbi('struct')]); + }); + + describe('methodInputsLength', () => { + test('should return inputs length', () => { + const abiParser = new AbiParser1([getFunctionAbi('struct')]); + expect(abiParser.methodInputsLength(getFunctionAbi('felt'))).toEqual(1); + }); + + test('should return 0 if inputs are empty', () => { + const abiParser = new AbiParser1([getFunctionAbi('felt')]); + const functionAbi = getFunctionAbi('felt'); + functionAbi.inputs[0].name = 'test_len'; + expect(abiParser.methodInputsLength(functionAbi)).toEqual(0); + }); + }); + + describe('getMethod', () => { + test('should return method definition from ABI', () => { + const abiParser = new AbiParser1([getFunctionAbi('struct'), getInterfaceAbi()]); + expect(abiParser.getMethod('test')).toEqual(getFunctionAbi('struct')); + }); + + test('should return undefined if method is not found', () => { + const abiParser = new AbiParser1([getFunctionAbi('struct')]); + expect(abiParser.getMethod('struct')).toBeUndefined(); + }); + }); + + describe('getLegacyFormat', () => { + test('should return method definition from ABI', () => { + const abiParser = new AbiParser1([getFunctionAbi('struct'), getInterfaceAbi()]); + const legacyFormat = abiParser.getLegacyFormat(); + expect(legacyFormat).toStrictEqual(abiParser.abi); + }); + }); +}); diff --git a/__tests__/utils/calldata/parser/parser-2.0.0.test.ts b/__tests__/utils/calldata/parser/parser-2.0.0.test.ts new file mode 100644 index 000000000..d49f0f5c8 --- /dev/null +++ b/__tests__/utils/calldata/parser/parser-2.0.0.test.ts @@ -0,0 +1,45 @@ +import { AbiParser2 } from '../../../../src/utils/calldata/parser/parser-2.0.0'; +import { getFunctionAbi, getInterfaceAbi } from '../../../factories/abi'; + +describe('AbiParser2', () => { + test('should create an instance', () => { + const abiParser = new AbiParser2([getFunctionAbi('struct')]); + expect(abiParser instanceof AbiParser2).toEqual(true); + expect(abiParser.abi).toStrictEqual([getFunctionAbi('struct')]); + }); + + describe('methodInputsLength', () => { + test('should return inputs length', () => { + const abiParser = new AbiParser2([getFunctionAbi('struct')]); + expect(abiParser.methodInputsLength(getFunctionAbi('test'))).toEqual(1); + }); + + test('should return 0 if inputs are empty', () => { + const abiParser = new AbiParser2([getFunctionAbi('struct')]); + const functionAbi = getFunctionAbi('test'); + functionAbi.inputs = []; + expect(abiParser.methodInputsLength(functionAbi)).toEqual(0); + }); + }); + + describe('getMethod', () => { + test('should return method definition from ABI', () => { + const abiParser = new AbiParser2([getFunctionAbi('struct'), getInterfaceAbi()]); + expect(abiParser.getMethod('test')).toEqual(getFunctionAbi('struct')); + }); + + test('should return undefined if method is not found', () => { + const abiParser = new AbiParser2([getFunctionAbi('struct')]); + expect(abiParser.getMethod('test')).toBeUndefined(); + }); + }); + + describe('getLegacyFormat', () => { + test('should return method definition from ABI', () => { + const abiParser = new AbiParser2([getFunctionAbi('struct'), getInterfaceAbi()]); + const legacyFormat = abiParser.getLegacyFormat(); + const result = [getFunctionAbi('struct'), getFunctionAbi('struct')]; + expect(legacyFormat).toEqual(result); + }); + }); +}); diff --git a/__tests__/utils/calldata/parser/parser.test.ts b/__tests__/utils/calldata/parser/parser.test.ts new file mode 100644 index 000000000..03674447c --- /dev/null +++ b/__tests__/utils/calldata/parser/parser.test.ts @@ -0,0 +1,44 @@ +import { + createAbiParser, + getAbiVersion, + isNoConstructorValid, +} from '../../../../src/utils/calldata/parser'; +import { AbiParser2 } from '../../../../src/utils/calldata/parser/parser-2.0.0'; +import { AbiParser1 } from '../../../../src/utils/calldata/parser/parser-0-1.1.0'; +import { getFunctionAbi, getInterfaceAbi } from '../../../factories/abi'; + +describe('createAbiParser', () => { + test('should create an AbiParser2 instance', () => { + const abiParser = createAbiParser([getInterfaceAbi()]); + expect(abiParser instanceof AbiParser2).toEqual(true); + }); + + test('should create an AbiParser1 instance', () => { + const abiParser = createAbiParser([getFunctionAbi('struct')]); + expect(abiParser instanceof AbiParser1).toEqual(true); + }); +}); + +describe('getAbiVersion', () => { + test('should return ABI version 2', () => { + expect(getAbiVersion([getInterfaceAbi()])).toEqual(2); + }); + + test('should return ABI version 1', () => { + expect(getAbiVersion([getFunctionAbi('core::bool')])).toEqual(1); + }); + + test('should return ABI version 0', () => { + expect(getAbiVersion([getFunctionAbi('felt')])).toEqual(0); + }); +}); + +describe('isNoConstructorValid', () => { + test('should return true if no constructor valid', () => { + expect(isNoConstructorValid('constructor', [])).toEqual(true); + }); + + test('should return false if constructor valid', () => { + expect(isNoConstructorValid('test', ['test'])).toEqual(false); + }); +}); diff --git a/__tests__/utils/calldata/requestParser.test.ts b/__tests__/utils/calldata/requestParser.test.ts new file mode 100644 index 000000000..49ddbe3ef --- /dev/null +++ b/__tests__/utils/calldata/requestParser.test.ts @@ -0,0 +1,260 @@ +import { parseCalldataField } from '../../../src/utils/calldata/requestParser'; +import { getAbiEnums, getAbiStructs, getAbiEntry } from '../../factories/abi'; +import { + CairoCustomEnum, + CairoOption, + CairoResult, + ETH_ADDRESS, + NON_ZERO_PREFIX, +} from '../../../src'; + +describe('requestParser', () => { + describe('parseCalldataField', () => { + test('should return parsed calldata field for base type', () => { + const args = [256n, 128n]; + const argsIterator = args[Symbol.iterator](); + const parsedField = parseCalldataField( + argsIterator, + getAbiEntry('felt'), + getAbiStructs(), + getAbiEnums() + ); + expect(parsedField).toEqual('256'); + }); + + test('should return parsed calldata field for Array type', () => { + const args = [[256n, 128n]]; + const argsIterator = args[Symbol.iterator](); + const parsedField = parseCalldataField( + argsIterator, + getAbiEntry('core::array::Array::'), + getAbiStructs(), + getAbiEnums() + ); + expect(parsedField).toEqual(['2', '256', '128']); + }); + + test('should return parsed calldata field for Array type(string input)', () => { + const args = ['some_test_value']; + const argsIterator = args[Symbol.iterator](); + const parsedField = parseCalldataField( + argsIterator, + getAbiEntry('core::array::Array::'), + getAbiStructs(), + getAbiEnums() + ); + expect(parsedField).toEqual(['1', '599374153440608178282648329058547045']); + }); + + test('should return parsed calldata field for NonZero type', () => { + const args = [true]; + const argsIterator = args[Symbol.iterator](); + const parsedField = parseCalldataField( + argsIterator, + getAbiEntry(`${NON_ZERO_PREFIX}core::bool`), + getAbiStructs(), + getAbiEnums() + ); + expect(parsedField).toEqual('1'); + }); + + test('should return parsed calldata field for EthAddress type', () => { + const args = ['test']; + const argsIterator = args[Symbol.iterator](); + const parsedField = parseCalldataField( + argsIterator, + getAbiEntry(`${ETH_ADDRESS}felt`), + getAbiStructs(), + getAbiEnums() + ); + expect(parsedField).toEqual('1952805748'); + }); + + test('should return parsed calldata field for Struct type', () => { + const args = [{ test_name: 'test' }]; + const argsIterator = args[Symbol.iterator](); + const parsedField = parseCalldataField( + argsIterator, + getAbiEntry('struct'), + getAbiStructs(), + getAbiEnums() + ); + expect(parsedField).toEqual(['1952805748']); + }); + + test('should return parsed calldata field for Tuple type', () => { + const args = [{ min: true, max: true }]; + const argsIterator = args[Symbol.iterator](); + const parsedField = parseCalldataField( + argsIterator, + getAbiEntry('(core::bool, core::bool)'), + getAbiStructs(), + getAbiEnums() + ); + expect(parsedField).toEqual(['1', '1']); + }); + + test('should return parsed calldata field for CairoUint256 abi type', () => { + const args = [252n]; + const argsIterator = args[Symbol.iterator](); + const parsedField = parseCalldataField( + argsIterator, + getAbiEntry('core::integer::u256'), + getAbiStructs(), + getAbiEnums() + ); + expect(parsedField).toEqual(['252', '0']); + }); + + test('should return parsed calldata field for Enum Option type None', () => { + const args = [new CairoOption(1, 'content')]; + const argsIterator = args[Symbol.iterator](); + const parsedField = parseCalldataField( + argsIterator, + getAbiEntry('core::option::Option::core::bool'), + getAbiStructs(), + { 'core::option::Option::core::bool': getAbiEnums().enum } + ); + expect(parsedField).toEqual('1'); + }); + + test('should return parsed calldata field for Enum Option type Some', () => { + const args = [new CairoOption(0, 'content')]; + const argsIterator = args[Symbol.iterator](); + const abiEnum = getAbiEnums().enum; + abiEnum.variants.push({ + name: 'Some', + type: 'cairo_struct_variant', + offset: 1, + }); + const parsedField = parseCalldataField( + argsIterator, + getAbiEntry('core::option::Option::core::bool'), + getAbiStructs(), + { 'core::option::Option::core::bool': abiEnum } + ); + expect(parsedField).toEqual(['0', '27988542884245108']); + }); + + test('should throw an error for Enum Option has no "Some" variant', () => { + const args = [new CairoOption(0, 'content')]; + const argsIterator = args[Symbol.iterator](); + expect(() => + parseCalldataField( + argsIterator, + getAbiEntry('core::option::Option::core::bool'), + getAbiStructs(), + { 'core::option::Option::core::bool': getAbiEnums().enum } + ) + ).toThrow(new Error(`Error in abi : Option has no 'Some' variant.`)); + }); + + test('should return parsed calldata field for Enum Result type Ok', () => { + const args = [new CairoResult(0, 'Ok')]; + const argsIterator = args[Symbol.iterator](); + const abiEnum = getAbiEnums().enum; + abiEnum.variants.push({ + name: 'Ok', + type: 'cairo_struct_variant', + offset: 1, + }); + const parsedField = parseCalldataField( + argsIterator, + getAbiEntry('core::result::Result::core::bool'), + getAbiStructs(), + { 'core::result::Result::core::bool': abiEnum } + ); + expect(parsedField).toEqual(['0', '20331']); + }); + + test('should throw an error for Enum Result has no "Ok" variant', () => { + const args = [new CairoResult(0, 'Ok')]; + const argsIterator = args[Symbol.iterator](); + expect(() => + parseCalldataField( + argsIterator, + getAbiEntry('core::result::Result::core::bool'), + getAbiStructs(), + { 'core::result::Result::core::bool': getAbiEnums().enum } + ) + ).toThrow(new Error(`Error in abi : Result has no 'Ok' variant.`)); + }); + + test('should return parsed calldata field for Custom Enum type', () => { + const activeVariantName = 'custom_enum'; + const args = [new CairoCustomEnum({ [activeVariantName]: 'content' })]; + const argsIterator = args[Symbol.iterator](); + const abiEnum = getAbiEnums().enum; + abiEnum.variants.push({ + name: activeVariantName, + type: 'cairo_struct_variant', + offset: 1, + }); + const parsedField = parseCalldataField(argsIterator, getAbiEntry('enum'), getAbiStructs(), { + enum: abiEnum, + }); + expect(parsedField).toEqual(['1', '27988542884245108']); + }); + + test('should throw an error for Custon Enum type when there is not active variant', () => { + const args = [new CairoCustomEnum({ test: 'content' })]; + const argsIterator = args[Symbol.iterator](); + expect(() => + parseCalldataField(argsIterator, getAbiEntry('enum'), getAbiStructs(), getAbiEnums()) + ).toThrow(new Error(`Not find in abi : Enum has no 'test' variant.`)); + }); + + test('should throw an error for CairoUint256 abi type when wrong arg is provided', () => { + const args = ['test']; + const argsIterator = args[Symbol.iterator](); + expect(() => + parseCalldataField( + argsIterator, + getAbiEntry('core::integer::u256'), + getAbiStructs(), + getAbiEnums() + ) + ).toThrow(new Error('Cannot convert test to a BigInt')); + }); + + test('should throw an error if provided tuple size do not match', () => { + const args = [{ min: true }, { max: true }]; + const argsIterator = args[Symbol.iterator](); + expect(() => + parseCalldataField( + argsIterator, + getAbiEntry('(core::bool, core::bool)'), + getAbiStructs(), + getAbiEnums() + ) + ).toThrow( + new Error( + `ParseTuple: provided and expected abi tuple size do not match. + provided: true + expected: core::bool,core::bool` + ) + ); + }); + + test('should throw an error if there is missing parameter for type Struct', () => { + const args = ['test']; + const argsIterator = args[Symbol.iterator](); + expect(() => + parseCalldataField(argsIterator, getAbiEntry('struct'), getAbiStructs(), getAbiEnums()) + ).toThrow(new Error('Missing parameter for type test_type')); + }); + + test('should throw an error if args for array type are not valid', () => { + const args = [256n, 128n]; + const argsIterator = args[Symbol.iterator](); + expect(() => + parseCalldataField( + argsIterator, + getAbiEntry('core::array::Array::'), + getAbiStructs(), + getAbiEnums() + ) + ).toThrow(new Error('ABI expected parameter test to be array or long string, got 256')); + }); + }); +}); diff --git a/__tests__/utils/calldata/tuple.test.ts b/__tests__/utils/calldata/tuple.test.ts new file mode 100644 index 000000000..5e2511560 --- /dev/null +++ b/__tests__/utils/calldata/tuple.test.ts @@ -0,0 +1,15 @@ +import extractTupleMemberTypes from '../../../src/utils/calldata/tuple'; + +describe('extractTupleMemberTypes', () => { + test('should return tuple member types for Cairo0', () => { + const tuple = '(u8, u8)'; + const result = extractTupleMemberTypes(tuple); + expect(result).toEqual(['u8', 'u8']); + }); + + test('should return tuple member types for Cairo1', () => { + const tuple = '(core::result::Result::, u8)'; + const result = extractTupleMemberTypes(tuple); + expect(result).toEqual(['core::result::Result::', 'u8']); + }); +}); diff --git a/__tests__/utils/calldata/validate.test.ts b/__tests__/utils/calldata/validate.test.ts new file mode 100644 index 000000000..3110e4d3e --- /dev/null +++ b/__tests__/utils/calldata/validate.test.ts @@ -0,0 +1,721 @@ +import validateFields from '../../../src/utils/calldata/validate'; +import { + CairoOption, + CairoResult, + ETH_ADDRESS, + Literal, + NON_ZERO_PREFIX, + Uint, +} from '../../../src'; +import { getFunctionAbi, getAbiEnums, getAbiStructs } from '../../factories/abi'; + +describe('validateFields', () => { + test('should throw an error if validation is unhandled', () => { + expect(() => { + validateFields(getFunctionAbi('test_test'), [true], getAbiStructs(), getAbiEnums()); + }).toThrow(new Error('Validate Unhandled: argument test, type test_test, value true')); + }); + + describe('felt validation', () => { + test('should return void if felt validation passes', () => { + const result = validateFields( + getFunctionAbi('felt'), + ['test'], + getAbiStructs(), + getAbiEnums() + ); + expect(result).toBeUndefined(); + }); + + test('should throw an error if felt is not the type of string, number or big int', () => { + const validateFelt = (params: unknown[]) => + validateFields(getFunctionAbi('felt'), params, getAbiStructs(), getAbiEnums()); + + const error = new Error( + 'Validate: arg test should be a felt typed as (String, Number or BigInt)' + ); + expect(() => validateFelt([{}])).toThrow(error); + expect(() => validateFelt([new Map()])).toThrow(error); + expect(() => validateFelt([true])).toThrow(error); + expect(() => validateFelt([])).toThrow(error); + expect(() => validateFelt([Symbol('test')])).toThrow(error); + }); + + test('should throw an error if felt is not in the range', () => { + const validateFelt = (params: unknown[]) => + validateFields(getFunctionAbi('felt'), params, getAbiStructs(), getAbiEnums()); + + const error = new Error( + 'Validate: arg test cairo typed felt should be in range [0, 2^252-1]' + ); + expect(() => validateFelt([-1])).toThrow(error); + expect(() => validateFelt([2n ** 252n])).toThrow(error); + }); + }); + + describe('bytes31 validation', () => { + test('should return void if bytes31 validation passes', () => { + const result = validateFields( + getFunctionAbi('core::bytes_31::bytes31'), + ['test'], + getAbiStructs(), + getAbiEnums() + ); + expect(result).toBeUndefined(); + }); + + test('should throw an error if parameter is not the type of string', () => { + const validateBytes31 = (params: unknown[]) => + validateFields( + getFunctionAbi('core::bytes_31::bytes31'), + params, + getAbiStructs(), + getAbiEnums() + ); + + const error = new Error('Validate: arg test should be a string.'); + + expect(() => validateBytes31([0, BigInt(22), new Map(), true, Symbol('test')])).toThrow( + error + ); + }); + + test('should throw an error if parameter is less than 32 chars', () => { + const validateBytes31 = (params: unknown[]) => + validateFields( + getFunctionAbi('core::bytes_31::bytes31'), + params, + getAbiStructs(), + getAbiEnums() + ); + + const error = new Error( + 'Validate: arg test cairo typed core::bytes_31::bytes31 should be a string of less than 32 characters.' + ); + expect(() => validateBytes31(['String_that_is_bigger_than_32_characters'])).toThrow(error); + }); + }); + + describe('Uint validation', () => { + test('should return void if Uint "u8" validation passes', () => { + const result = validateFields( + getFunctionAbi(Uint.u8), + [255n], + getAbiStructs(), + getAbiEnums() + ); + expect(result).toBeUndefined(); + }); + + test('should return void if Uint "u16" validation passes', () => { + const result = validateFields( + getFunctionAbi(Uint.u16), + [65535n], + getAbiStructs(), + getAbiEnums() + ); + expect(result).toBeUndefined(); + }); + + test('should return void if Uint "u32" validation passes', () => { + const result = validateFields( + getFunctionAbi(Uint.u32), + [4294967295n], + getAbiStructs(), + getAbiEnums() + ); + expect(result).toBeUndefined(); + }); + + test('should return void if Uint "u64" validation passes', () => { + const result = validateFields( + getFunctionAbi(Uint.u64), + [2n ** 64n - 1n], + getAbiStructs(), + getAbiEnums() + ); + expect(result).toBeUndefined(); + }); + + test('should return void if Uint "u128" validation passes', () => { + const result = validateFields( + getFunctionAbi(Uint.u128), + [2n ** 128n - 1n], + getAbiStructs(), + getAbiEnums() + ); + expect(result).toBeUndefined(); + }); + + test('should return void if Uint "u256" validation passes', () => { + const result = validateFields( + getFunctionAbi(Uint.u256), + [2n ** 256n - 1n], + getAbiStructs(), + getAbiEnums() + ); + expect(result).toBeUndefined(); + }); + + test('should return void if Uint "u512" validation passes', () => { + const result = validateFields( + getFunctionAbi(Uint.u512), + [2n ** 512n - 1n], + getAbiStructs(), + getAbiEnums() + ); + expect(result).toBeUndefined(); + }); + + test('should throw an error if parameter is too large', () => { + const validateUint = (params: unknown[]) => + validateFields(getFunctionAbi(Uint.u8), params, getAbiStructs(), getAbiEnums()); + + const error = new Error( + 'Validation: Parameter is too large to be typed as Number use (BigInt or String)' + ); + + expect(() => validateUint([Number.MAX_SAFE_INTEGER + 1])).toThrow(error); + }); + + test('should throw an error if parameter type is not valid', () => { + const validateUint = (params: unknown[]) => + validateFields(getFunctionAbi(Uint.u8), params, getAbiStructs(), getAbiEnums()); + + const getError = (param: any) => + new Error( + `Validate: arg test of cairo type ${Uint.u8} should be type (String, Number or BigInt), but is ${typeof param} ${param}.` + ); + + expect(() => validateUint([new Map()])).toThrow(getError(new Map())); + expect(() => validateUint([true])).toThrow(getError(true)); + expect(() => validateUint([{ test: 'test' }])).toThrow(getError({ test: 'test' })); + }); + + test('should throw an error if Uint "u8" is not in range', () => { + const validateUint = (params: unknown[]) => + validateFields(getFunctionAbi(Uint.u8), params, getAbiStructs(), getAbiEnums()); + + const error = new Error( + `Validate: arg test cairo typed ${Uint.u8} should be in range [0 - 255]` + ); + + expect(() => validateUint([-1])).toThrow(error); + expect(() => validateUint([256n])).toThrow(error); + }); + + test('should throw an error if Uint "u16" is not in range', () => { + const validateUint = (params: unknown[]) => + validateFields(getFunctionAbi(Uint.u16), params, getAbiStructs(), getAbiEnums()); + + const error = new Error( + `Validate: arg test cairo typed ${Uint.u16} should be in range [0, 65535]` + ); + + expect(() => validateUint([65536n])).toThrow(error); + }); + + test('should throw an error if Uint "u32" is not in range', () => { + const validateUint = (params: unknown[]) => + validateFields(getFunctionAbi(Uint.u32), params, getAbiStructs(), getAbiEnums()); + + const error = new Error( + `Validate: arg test cairo typed ${Uint.u32} should be in range [0, 4294967295]` + ); + + expect(() => validateUint([4294967296n])).toThrow(error); + }); + + test('should throw an error if Uint "u64" is not in range', () => { + const validateUint = (params: unknown[]) => + validateFields(getFunctionAbi(Uint.u64), params, getAbiStructs(), getAbiEnums()); + + const error = new Error( + `Validate: arg test cairo typed ${Uint.u64} should be in range [0, 2^64-1]` + ); + + expect(() => validateUint([2n ** 64n])).toThrow(error); + }); + + test('should throw an error if Uint "u128" is not in range', () => { + const validateUint = (params: unknown[]) => + validateFields(getFunctionAbi(Uint.u128), params, getAbiStructs(), getAbiEnums()); + + const error = new Error( + `Validate: arg test cairo typed ${Uint.u128} should be in range [0, 2^128-1]` + ); + + expect(() => validateUint([2n ** 128n])).toThrow(error); + }); + + test('should throw an error if Uint "u256" is not in range', () => { + const validateUint = (params: unknown[]) => + validateFields(getFunctionAbi(Uint.u256), params, getAbiStructs(), getAbiEnums()); + + const error = new Error('bigNumberish is bigger than UINT_256_MAX'); + + expect(() => validateUint([2n ** 256n])).toThrow(error); + }); + + test('should throw an error if Uint "u512" is not in range', () => { + const validateUint = (params: unknown[]) => + validateFields(getFunctionAbi(Uint.u512), params, getAbiStructs(), getAbiEnums()); + + const error = new Error('bigNumberish is bigger than UINT_512_MAX.'); + + expect(() => validateUint([2n ** 512n])).toThrow(error); + }); + + test('should throw an error if "Literal.ClassHash" is not in range', () => { + const validateUint = (params: unknown[]) => + validateFields(getFunctionAbi(Literal.ClassHash), params, getAbiStructs(), getAbiEnums()); + + const error = new Error( + `Validate: arg test cairo typed ${Literal.ClassHash} should be in range [0, 2^252-1]` + ); + + expect(() => validateUint([2n ** 252n])).toThrow(error); + }); + + test('should throw an error if "Literal.ContractAddress" is not in range', () => { + const validateUint = (params: unknown[]) => + validateFields( + getFunctionAbi(Literal.ContractAddress), + params, + getAbiStructs(), + getAbiEnums() + ); + + const error = new Error( + `Validate: arg test cairo typed ${Literal.ContractAddress} should be in range [0, 2^252-1]` + ); + + expect(() => validateUint([2n ** 252n])).toThrow(error); + }); + + test('should throw an error if "Literal.Secp256k1Point" is not in range', () => { + const validateUint = (params: unknown[]) => + validateFields( + getFunctionAbi(Literal.Secp256k1Point), + params, + getAbiStructs(), + getAbiEnums() + ); + + const error = new Error( + `Validate: arg test must be ${Literal.Secp256k1Point} : a 512 bits number.` + ); + + expect(() => validateUint([2n ** 512n])).toThrow(error); + }); + }); + + describe('Boolean validation', () => { + test('should return void if boolean validation passes', () => { + const result = validateFields( + getFunctionAbi('core::bool'), + [true], + getAbiStructs(), + getAbiEnums() + ); + expect(result).toBeUndefined(); + }); + + test('should throw an error if boolean validation fails', () => { + const validateBool = (params: unknown[]) => + validateFields(getFunctionAbi('core::bool'), params, getAbiStructs(), getAbiEnums()); + + const error = new Error( + `Validate: arg test of cairo type core::bool should be type (Boolean)` + ); + + expect(() => validateBool(['bool', 22, Symbol('test'), BigInt(2)])).toThrow(error); + }); + }); + + describe('Boolean validation', () => { + test('should return void if boolean validation passes', () => { + const result = validateFields( + getFunctionAbi('core::bool'), + [true], + getAbiStructs(), + getAbiEnums() + ); + expect(result).toBeUndefined(); + }); + + test('should throw an error if boolean validation fails', () => { + const validateBool = (params: unknown[]) => + validateFields(getFunctionAbi('core::bool'), params, getAbiStructs(), getAbiEnums()); + + const error = new Error( + `Validate: arg test of cairo type core::bool should be type (Boolean)` + ); + + expect(() => validateBool(['bool'])).toThrow(error); + }); + }); + + describe('ByteArray validation', () => { + test('should return void if byte array validation passes', () => { + const result = validateFields( + getFunctionAbi('core::byte_array::ByteArray'), + ['byte_array'], + getAbiStructs(), + getAbiEnums() + ); + expect(result).toBeUndefined(); + }); + + test('should throw an error if byte array validation fails', () => { + const validateByteArray = (params: unknown[]) => + validateFields( + getFunctionAbi('core::byte_array::ByteArray'), + params, + getAbiStructs(), + getAbiEnums() + ); + + const error = new Error(`Validate: arg test should be a string.`); + + expect(() => validateByteArray([false, 0, {}, new Map(), Symbol('test')])).toThrow(error); + }); + }); + + describe('Tuple validation', () => { + test('should return void if tuple validation passes', () => { + const result = validateFields( + getFunctionAbi('(core::bool, core::bool)'), + [{ min: true, max: true }], + getAbiStructs(), + getAbiEnums() + ); + expect(result).toBeUndefined(); + }); + + test('should throw an error if tupple validation fails', () => { + const error = new Error(`Validate: arg test should be a tuple (defined as object)`); + + expect(() => + validateFields( + getFunctionAbi('(core::bool, core::bool)'), + [], + getAbiStructs(), + getAbiEnums() + ) + ).toThrow(error); + }); + }); + + describe('Struct validation', () => { + test('should return void if struct validation passes for common struct', () => { + const result = validateFields( + getFunctionAbi('struct'), + [{ test_name: 'test' }], + getAbiStructs(), + getAbiEnums() + ); + + expect(result).toBeUndefined(); + }); + + test('should return void if struct validation passes for Uint 256 or 512', () => { + const abiStructs256 = { + [Uint.u256]: getAbiStructs().struct, + }; + const result256 = validateFields( + getFunctionAbi(Uint.u256), + [2n ** 256n - 1n], + abiStructs256, + getAbiEnums() + ); + + const abiStructs512 = { + [Uint.u512]: getAbiStructs().struct, + }; + const result512 = validateFields( + getFunctionAbi(Uint.u512), + [2n ** 512n - 1n], + abiStructs512, + getAbiEnums() + ); + + expect(result256).toBeUndefined(); + expect(result512).toBeUndefined(); + }); + + test('should return void if struct validation passes for EthAddress', () => { + const abiStructs = { + [ETH_ADDRESS]: getAbiStructs().struct, + }; + const result = validateFields(getFunctionAbi(ETH_ADDRESS), [1n], abiStructs, getAbiEnums()); + + expect(result).toBeUndefined(); + }); + + test('should throw an error for EthAddress struct if type is not a BigNumberish', () => { + const error = new Error('EthAddress type is waiting a BigNumberish. Got "[object Object]"'); + + expect(() => { + const abiStructs = { + [ETH_ADDRESS]: getAbiStructs().struct, + }; + + validateFields(getFunctionAbi(ETH_ADDRESS), [{ test: 1 }], abiStructs, getAbiEnums()); + }).toThrow(error); + }); + + test('should throw an error for EthAddress struct if it is not in range', () => { + const error = new Error( + `Validate: arg test cairo typed ${ETH_ADDRESS} should be in range [0, 2^160-1]` + ); + + expect(() => { + const abiStructs = { + [ETH_ADDRESS]: getAbiStructs().struct, + }; + + validateFields(getFunctionAbi(ETH_ADDRESS), [2n ** 160n], abiStructs, getAbiEnums()); + }).toThrow(error); + }); + + test('should throw an error if arg is not an JS object', () => { + const error = new Error( + 'Validate: arg test is cairo type struct (struct), and should be defined as a js object (not array)' + ); + + expect(() => + validateFields(getFunctionAbi('struct'), [2], getAbiStructs(), getAbiEnums()) + ).toThrow(error); + }); + + test('should throw an error if arg property name does not exist in the struct members', () => { + const error = new Error('Validate: arg test should have a property test_name'); + + expect(() => + validateFields( + getFunctionAbi('struct'), + [{ example: 'test' }], + getAbiStructs(), + getAbiEnums() + ) + ).toThrow(error); + }); + }); + + describe('Enum validation', () => { + test('should return void if enum validation passes for custom enum', () => { + const result = validateFields( + getFunctionAbi('enum'), + [{ variant: 'test', activeVariant: 'test' }], + getAbiStructs(), + getAbiEnums() + ); + + expect(result).toBeUndefined(); + }); + + test('should return void if enum validation passes for type option', () => { + const enumOption = 'core::option::Option::core::bool'; + + const abiEnums = { + [enumOption]: getAbiEnums().enum, + }; + const result = validateFields( + getFunctionAbi(enumOption), + [new CairoOption(0, 'content')], + getAbiStructs(), + abiEnums + ); + + expect(result).toBeUndefined(); + }); + + test('should return void if enum validation passes for type result', () => { + const enumResult = 'core::result::Result::bool'; + + const abiEnums = { + [enumResult]: getAbiEnums().enum, + }; + const result = validateFields( + getFunctionAbi(enumResult), + [new CairoResult(0, 'content')], + getAbiStructs(), + abiEnums + ); + + expect(result).toBeUndefined(); + }); + + test('should throw an error if arg is not an JS object', () => { + const error = new Error( + 'Validate: arg test is cairo type Enum (enum), and should be defined as a js object (not array)' + ); + + expect(() => + validateFields(getFunctionAbi('enum'), [2], getAbiStructs(), getAbiEnums()) + ).toThrow(error); + }); + + test('should throw an error if arg is not an enum', () => { + const error = new Error( + 'Validate Enum: argument test, type enum, value received "[object Object]", is not an Enum.' + ); + + expect(() => + validateFields( + getFunctionAbi('enum'), + [{ example: 'test' }], + getAbiStructs(), + getAbiEnums() + ) + ).toThrow(error); + }); + }); + + describe('NonZero validation', () => { + test('should return void if non zero validation passes for felt', () => { + const result = validateFields( + getFunctionAbi(`${NON_ZERO_PREFIX}`), + [1n], + getAbiStructs(), + getAbiEnums() + ); + + expect(result).toBeUndefined(); + }); + + test('should return void if non zero validation passes for Uint', () => { + const result = validateFields( + getFunctionAbi(`${NON_ZERO_PREFIX}<${Uint.u8}>`), + [1n], + getAbiStructs(), + getAbiEnums() + ); + + expect(result).toBeUndefined(); + }); + + test('should throw an error if type is not authorized', () => { + const error = new Error('Validate: test type is not authorized for NonZero type.'); + + expect(() => + validateFields( + getFunctionAbi(`${NON_ZERO_PREFIX}`), + [true], + getAbiStructs(), + getAbiEnums() + ) + ).toThrow(error); + }); + + test('should throw an error if value 0 iz provided for felt252 type', () => { + const error = new Error('Validate: value 0 is not authorized in NonZero felt252 type.'); + + expect(() => + validateFields( + getFunctionAbi(`${NON_ZERO_PREFIX}`), + [0], + getAbiStructs(), + getAbiEnums() + ) + ).toThrow(error); + }); + + test('should throw an error if value 0 iz provided for uint256 type', () => { + const error = new Error('Validate: value 0 is not authorized in NonZero uint256 type.'); + + expect(() => + validateFields( + getFunctionAbi(`${NON_ZERO_PREFIX}<${Uint.u256}>`), + [0], + getAbiStructs(), + getAbiEnums() + ) + ).toThrow(error); + }); + + test('should throw an error if value 0 iz provided for any uint type', () => { + const error = new Error('Validate: value 0 is not authorized in NonZero uint type.'); + + expect(() => + validateFields( + getFunctionAbi(`${NON_ZERO_PREFIX}<${Uint.u8}>`), + [0], + getAbiStructs(), + getAbiEnums() + ) + ).toThrow(error); + }); + }); + + describe('Array validation', () => { + test('should return void if array validation passes for each type', () => { + const validateArray = (type: string, param: unknown) => + validateFields(getFunctionAbi(type), [[param]], getAbiStructs(), getAbiEnums()); + + expect(validateArray('core::array::Array::', true)).toBeUndefined(); + expect(validateArray('core::array::Array::', 'test')).toBeUndefined(); + expect(validateArray('core::array::Span::', true)).toBeUndefined(); + expect(validateArray('core::array::Array::', 'felt')).toBeUndefined(); + expect(validateArray(`core::array::Array::<${Uint.u8}>`, 2n)).toBeUndefined(); + expect(validateArray('core::array::Array::', 'felt')).toBeUndefined(); + expect( + validateArray('core::array::Array::<(core::bool, core::bool)>', { min: true, max: true }) + ).toBeUndefined(); + expect( + validateArray('core::array::Array::>', [true]) + ).toBeUndefined(); + + const enumArrayResult = 'core::array::Array::'; + + const abiEnums = { 'core::result::Result::core::bool': getAbiEnums().enum }; + const validatedArrayEnum = validateFields( + getFunctionAbi(enumArrayResult), + [[new CairoResult(0, 'content')]], + getAbiStructs(), + abiEnums + ); + + expect(validatedArrayEnum).toBeUndefined(); + + const structArrayEth = `core::array::Array::<${ETH_ADDRESS}>`; + const abiStructs = { [ETH_ADDRESS]: getAbiStructs().struct }; + + const validatedArrayStruct = validateFields( + getFunctionAbi(structArrayEth), + [[1n]], + abiStructs, + getAbiEnums() + ); + + expect(validatedArrayStruct).toBeUndefined(); + }); + + test('should throw an error if parameter is not an array', () => { + expect(() => { + validateFields( + getFunctionAbi('core::array::Span::'), + [true], + getAbiStructs(), + getAbiEnums() + ); + }).toThrow(new Error('Validate: arg test should be an Array')); + }); + + test('should throw an error if array validation is unhandled', () => { + expect(() => { + validateFields( + getFunctionAbi('core::array::Span::'), + [[true]], + getAbiStructs(), + getAbiEnums() + ); + }).toThrow( + new Error( + 'Validate Unhandled: argument test, type core::array::Span::, value true' + ) + ); + }); + }); +}); diff --git a/__tests__/utils/events.test.ts b/__tests__/utils/events.test.ts new file mode 100644 index 000000000..c2dcadf89 --- /dev/null +++ b/__tests__/utils/events.test.ts @@ -0,0 +1,346 @@ +import type { + AbiEntry, + AbiEnums, + AbiEvent, + AbiStructs, + CairoEventVariant, + InvokeTransactionReceiptResponse, + RPC, +} from '../../src'; +import { isAbiEvent, getAbiEvents, parseEvents, parseUDCEvent } from '../../src/utils/events'; +import { getFunctionAbi, getInterfaceAbi, getAbiEntry } from '../factories/abi'; + +const getBaseTxReceiptData = (): InvokeTransactionReceiptResponse => ({ + type: 'INVOKE', + transaction_hash: '0x6eebff0d931f36222268705ca791fd0de8d059eaf01887eecf1ce99a6c27f49', + actual_fee: { unit: 'WEI', amount: '0x33d758c09000' }, + messages_sent: [], + events: [], + execution_status: 'SUCCEEDED', + finality_status: 'ACCEPTED_ON_L2', + block_hash: '0xdfc9b788478b2a2b9bcba19ab7d86996bcc45c4f8a865435469334e9077b24', + block_number: 584, + execution_resources: { + steps: 9490, + memory_holes: 143, + range_check_builtin_applications: 198, + pedersen_builtin_applications: 34, + ec_op_builtin_applications: 3, + data_availability: { l1_gas: 0, l1_data_gas: 544 }, + }, +}); + +describe('isAbiEvent', () => { + test('should return true if it is Abi event', () => { + expect(isAbiEvent(getAbiEntry('event'))).toEqual(true); + }); + + test('should return false if it is not Abi event', () => { + const abiEntry: AbiEntry = { name: 'test', type: 'felt ' }; + expect(isAbiEvent(abiEntry)).toEqual(false); + }); +}); + +describe('getAbiEvents', () => { + test('should get Cairo1 ABI events', () => { + const abiEventAndVariantName = 'cairo_event_struct'; + const abiCairoEventStruct: AbiEvent = { + kind: 'struct', + members: [ + { + name: 'test_name', + type: 'test_type', + kind: 'data', + }, + ], + name: abiEventAndVariantName, + type: 'event', + }; + + const abiCairoEventEnum: CairoEventVariant = { + kind: 'enum', + variants: [ + { + name: 'test_name', + type: abiEventAndVariantName, + kind: 'data', + }, + ], + name: 'test_cairo_event', + type: 'event', + }; + + const abiEvents = getAbiEvents([getInterfaceAbi(), abiCairoEventStruct, abiCairoEventEnum]); + + const result = { + '0x3c719ce4f57dd2d9059b9ffed65417d694a29982d35b188574144d6ae6c3f87': abiCairoEventStruct, + }; + expect(abiEvents).toStrictEqual(result); + }); + + test('should throw and error if Cairo1 ABI events definition is inconsistent', () => { + const abiCairoEventStruct: AbiEvent = { + kind: 'struct', + members: [ + { + name: 'test_name', + type: 'test_type', + kind: 'data', + }, + ], + name: 'cairo_event_struct', + type: 'event', + }; + + const abiCairoEventEnum: CairoEventVariant = { + kind: 'enum', + variants: [ + { + name: 'test_name', + type: 'cairo_event_struct_variant', + kind: 'data', + }, + ], + name: 'test_cairo_event', + type: 'event', + }; + + expect(() => getAbiEvents([getInterfaceAbi(), abiCairoEventStruct, abiCairoEventEnum])).toThrow( + new Error('inconsistency in ABI events definition.') + ); + }); + + test('should return Cairo0 ABI events', () => { + const abiCairoEventStruct: AbiEvent = { + kind: 'struct', + members: [ + { + name: 'test_name', + type: 'test_type', + kind: 'data', + }, + ], + name: 'cairo_event_struct', + type: 'event', + }; + + const abiEvents = getAbiEvents([getFunctionAbi('event'), abiCairoEventStruct]); + const result = { + '0x27b21abc103381e154ea5c557dfe64466e0d25add7ef91a45718f5b8ee8fae3': abiCairoEventStruct, + }; + expect(abiEvents).toStrictEqual(result); + }); +}); + +describe('parseEvents', () => { + test('should return parsed events', () => { + const abiEventAndVariantName = 'cairo_event_struct'; + const abiCairoEventStruct: AbiEvent = { + kind: 'struct', + members: [ + { + name: 'test_name', + type: 'test_type', + kind: 'data', + }, + ], + name: abiEventAndVariantName, + type: 'event', + }; + + const abiCairoEventEnum: CairoEventVariant = { + kind: 'enum', + variants: [ + { + name: 'test_name', + type: abiEventAndVariantName, + kind: 'data', + }, + ], + name: 'test_cairo_event', + type: 'event', + }; + + const abiEvents = getAbiEvents([getInterfaceAbi(), abiCairoEventStruct, abiCairoEventEnum]); + + const abiStructs: AbiStructs = { + abi_structs: { + members: [ + { + name: 'test_name', + type: 'test_type', + offset: 1, + }, + ], + size: 2, + name: 'cairo_event_struct', + type: 'struct', + }, + }; + + const abiEnums: AbiEnums = { + abi_enums: { + variants: [ + { + name: 'test_name', + type: 'cairo_event_struct_variant', + offset: 1, + }, + ], + size: 2, + name: 'test_cairo_event', + type: 'enum', + }, + }; + + const event: RPC.Event = { + from_address: 'test_address', + keys: ['0x3c719ce4f57dd2d9059b9ffed65417d694a29982d35b188574144d6ae6c3f87'], + data: ['0x3c719ce4f57dd2d9059b9ffed65417d694a29982d35b188574144d6ae6c3f87'], + }; + + const parsedEvents = parseEvents([event], abiEvents, abiStructs, abiEnums); + + const result = [ + { + cairo_event_struct: { + test_name: 1708719217404197029088109386680815809747762070431461851150711916567020191623n, + }, + }, + ]; + + expect(parsedEvents).toStrictEqual(result); + }); + + test('should throw if ABI events has not enough data in "keys" property', () => { + const abiEventAndVariantName = 'cairo_event_struct'; + const abiCairoEventStruct: AbiEvent = { + kind: 'struct', + members: [ + { + name: 'test_name', + type: 'test_type', + kind: 'data', + }, + ], + name: abiEventAndVariantName, + type: 'event', + }; + + const abiCairoEventEnum: CairoEventVariant = { + kind: 'enum', + variants: [ + { + name: 'test_name', + type: abiEventAndVariantName, + kind: 'data', + }, + ], + name: 'test_cairo_event', + type: 'event', + }; + + const abiEvents = getAbiEvents([getInterfaceAbi(), abiCairoEventStruct, abiCairoEventEnum]); + + const abiStructs: AbiStructs = { + abi_structs: { + members: [ + { + name: 'test_name', + type: 'test_type', + offset: 1, + }, + ], + size: 2, + name: 'cairo_event_struct', + type: 'struct', + }, + }; + + const abiEnums: AbiEnums = { + abi_enums: { + variants: [ + { + name: 'test_name', + type: 'cairo_event_struct_variant', + offset: 1, + }, + ], + size: 2, + name: 'test_cairo_event', + type: 'enum', + }, + }; + + const event: RPC.Event = { + from_address: 'test_address', + keys: ['0x3c719ce4f57dd2d9059b9ffed65417d694a29982d35b188574144d6ae6c3f87'], + data: ['0x3c719ce4f57dd2d9059b9ffed65417d694a29982d35b188574144d6ae6c3f87'], + }; + + abiEvents['0x3c719ce4f57dd2d9059b9ffed65417d694a29982d35b188574144d6ae6c3f87'].name = ''; + expect(() => parseEvents([event], abiEvents, abiStructs, abiEnums)).toBeTruthy(); + }); +}); + +describe('parseUDCEvent', () => { + test('should return parsed UDC event', () => { + const txReceipt: InvokeTransactionReceiptResponse = { + ...getBaseTxReceiptData(), + events: [ + { + from_address: '0x41a78e741e5af2fec34b695679bc6891742439f7afb8484ecd7766661ad02bf', + keys: ['0x26b160f10156dea0639bec90696772c640b9706a47f5b8c52ea1abe5858b34d'], + data: [ + '0x1f1209f331cda3e84202f5495446028cd8730159ab24e08a5fd96125257673f', + '0x6cee47a1571f83b30b3549fce4aceda18d2533a51b0016b75a50466c708daad', + '0x0', + '0x54328a1075b8820eb43caf0caa233923148c983742402dcfc38541dd843d01a', + '0x3', + '0x546f6b656e', + '0x4552433230', + '0x6cee47a1571f83b30b3549fce4aceda18d2533a51b0016b75a50466c708daad', + '0x76d9fae688efa7dc5defa712c1fa7df537e4c0f5f8b05842a1fd4a6d8d9d3a1', + ], + }, + { + from_address: '0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7', + keys: [ + '0x99cd8bde557814842a3121e8ddfd433a539b8c9f14bf31ebf108d12e6196e9', + '0x6cee47a1571f83b30b3549fce4aceda18d2533a51b0016b75a50466c708daad', + '0x1000', + ], + + data: ['0x33d758c09000', '0x0'], + }, + ], + }; + + const parsedUDCEvent = parseUDCEvent(txReceipt); + const result = { + transaction_hash: '0x6eebff0d931f36222268705ca791fd0de8d059eaf01887eecf1ce99a6c27f49', + contract_address: '0x1f1209f331cda3e84202f5495446028cd8730159ab24e08a5fd96125257673f', + address: '0x1f1209f331cda3e84202f5495446028cd8730159ab24e08a5fd96125257673f', + deployer: '0x6cee47a1571f83b30b3549fce4aceda18d2533a51b0016b75a50466c708daad', + unique: '0x0', + classHash: '0x54328a1075b8820eb43caf0caa233923148c983742402dcfc38541dd843d01a', + calldata_len: '0x3', + calldata: [ + '0x546f6b656e', + '0x4552433230', + '0x6cee47a1571f83b30b3549fce4aceda18d2533a51b0016b75a50466c708daad', + ], + salt: '0x76d9fae688efa7dc5defa712c1fa7df537e4c0f5f8b05842a1fd4a6d8d9d3a1', + }; + expect(parsedUDCEvent).toStrictEqual(result); + }); + + test('should throw an error if events are empty', () => { + const txReceipt: InvokeTransactionReceiptResponse = { + ...getBaseTxReceiptData(), + events: [], + }; + + expect(() => parseUDCEvent(txReceipt)).toThrow(new Error('UDC emitted event is empty')); + }); +}); diff --git a/__tests__/utils/num.test.ts b/__tests__/utils/num.test.ts index 81f0e7bb1..f918f4408 100644 --- a/__tests__/utils/num.test.ts +++ b/__tests__/utils/num.test.ts @@ -1,7 +1,6 @@ import { isHex, toBigInt, - isBigInt, toHex, hexToDecimalString, cleanHex, @@ -15,8 +14,6 @@ import { toCairoBool, hexToBytes, addPercent, - isNumber, - isBoolean, } from '../../src/utils/num'; import { num } from '../../src'; @@ -49,23 +46,6 @@ describe('toBigInt', () => { }); }); -describe('isBigInt', () => { - test('should return true for big integers', () => { - expect(isBigInt(BigInt(10))).toBe(true); - expect(isBigInt(BigInt('9007199254740991'))).toBe(true); - }); - - test('should return false for non-big integers', () => { - expect(isBigInt(10)).toBe(false); - expect(isBigInt('10')).toBe(false); - expect(isBigInt(undefined)).toBe(false); - expect(isBigInt(null)).toBe(false); - expect(isBigInt({})).toBe(false); - expect(isBigInt([])).toBe(false); - expect(isBigInt(true)).toBe(false); - }); -}); - describe('toHex', () => { test('should properly convert to hex-string', () => { expect(toHex(100)).toBe('0x64'); @@ -177,39 +157,6 @@ describe('addPercent', () => { }); }); -describe('isNumber', () => { - test('should correctly determine if value is a number', () => { - expect(isNumber(0)).toBe(true); - expect(isNumber(123)).toBe(true); - expect(isNumber(-123)).toBe(true); - - expect(isNumber(123n)).toBe(false); - expect(isNumber('')).toBe(false); - expect(isNumber('123')).toBe(false); - expect(isNumber(true)).toBe(false); - expect(isNumber(false)).toBe(false); - expect(isNumber(null)).toBe(false); - expect(isBoolean([])).toBe(false); - expect(isBoolean({})).toBe(false); - }); -}); - -describe('isBoolean', () => { - test('should correctly determine if value is a boolean', () => { - expect(isBoolean(true)).toBe(true); - expect(isBoolean(false)).toBe(true); - - expect(isBoolean(0)).toBe(false); - expect(isBoolean(1)).toBe(false); - expect(isBoolean('')).toBe(false); - expect(isBoolean('true')).toBe(false); - expect(isBoolean('false')).toBe(false); - expect(isBoolean(null)).toBe(false); - expect(isBoolean([])).toBe(false); - expect(isBoolean({})).toBe(false); - }); -}); - describe('stringToSha256ToArrayBuff4', () => { test('should correctly hash&encode an utf8 string', () => { const buff = num.stringToSha256ToArrayBuff4('LedgerW'); diff --git a/__tests__/utils/shortString.test.ts b/__tests__/utils/shortString.test.ts index de894d709..41cb739d6 100644 --- a/__tests__/utils/shortString.test.ts +++ b/__tests__/utils/shortString.test.ts @@ -5,7 +5,6 @@ import { encodeShortString, isDecimalString, isShortString, - isString, } from '../../src/utils/shortString'; describe('shortString', () => { @@ -110,22 +109,6 @@ describe('shortString', () => { ).toBe(''); }); -describe('isString', () => { - test('should return true for strings', () => { - expect(isString('test')).toBe(true); - expect(isString('')).toBe(true); - }); - - test('should return false for non-string values', () => { - expect(isString(10)).toBe(false); - expect(isString({})).toBe(false); - expect(isString(null)).toBe(false); - expect(isString(undefined)).toBe(false); - expect(isString([])).toBe(false); - expect(isString(true)).toBe(false); - }); -}); - describe('isShortString', () => { test('should return true for short strings', () => { const shortStr = '1234567890123456789012345678901'; diff --git a/__tests__/utils/typed.test.ts b/__tests__/utils/typed.test.ts new file mode 100644 index 000000000..27b037759 --- /dev/null +++ b/__tests__/utils/typed.test.ts @@ -0,0 +1,100 @@ +import { + isUndefined, + isBigInt, + isBoolean, + isNumber, + isString, + isObject, +} from '../../src/utils/typed'; + +describe('isUndefined', () => { + test('should return true if value is undefined', () => { + expect(isUndefined(undefined)).toBe(true); + }); + + test('should return false if value is not undefined', () => { + const value = 'existing value'; + expect(isUndefined(value)).toBe(false); + }); +}); + +describe('isNumber', () => { + test('should correctly determine if value is a number', () => { + expect(isNumber(0)).toBe(true); + expect(isNumber(123)).toBe(true); + expect(isNumber(-123)).toBe(true); + + expect(isNumber(123n)).toBe(false); + expect(isNumber('')).toBe(false); + expect(isNumber('123')).toBe(false); + expect(isNumber(true)).toBe(false); + expect(isNumber(false)).toBe(false); + expect(isNumber(null)).toBe(false); + expect(isBoolean([])).toBe(false); + expect(isBoolean({})).toBe(false); + }); +}); + +describe('isBoolean', () => { + test('should correctly determine if value is a boolean', () => { + expect(isBoolean(true)).toBe(true); + expect(isBoolean(false)).toBe(true); + + expect(isBoolean(0)).toBe(false); + expect(isBoolean(1)).toBe(false); + expect(isBoolean('')).toBe(false); + expect(isBoolean('true')).toBe(false); + expect(isBoolean('false')).toBe(false); + expect(isBoolean(null)).toBe(false); + expect(isBoolean([])).toBe(false); + expect(isBoolean({})).toBe(false); + }); +}); + +describe('isBigInt', () => { + test('should return true for big integers', () => { + expect(isBigInt(BigInt(10))).toBe(true); + expect(isBigInt(BigInt('9007199254740991'))).toBe(true); + }); + + test('should return false for non-big integers', () => { + expect(isBigInt(10)).toBe(false); + expect(isBigInt('10')).toBe(false); + expect(isBigInt(undefined)).toBe(false); + expect(isBigInt(null)).toBe(false); + expect(isBigInt({})).toBe(false); + expect(isBigInt([])).toBe(false); + expect(isBigInt(true)).toBe(false); + }); +}); + +describe('isString', () => { + test('should return true for strings', () => { + expect(isString('test')).toBe(true); + expect(isString('')).toBe(true); + }); + + test('should return false for non-string values', () => { + expect(isString(10)).toBe(false); + expect(isString({})).toBe(false); + expect(isString(null)).toBe(false); + expect(isString(undefined)).toBe(false); + expect(isString([])).toBe(false); + expect(isString(true)).toBe(false); + }); +}); + +describe('isObject', () => { + test('should return true if value is object', () => { + expect(isObject({ test: 'test' })).toEqual(true); + expect(isObject({})).toEqual(true); + }); + + test('should return false if value is not object', () => { + expect(isObject(10)).toBe(false); + expect(isObject(null)).toBe(false); + expect(isObject(undefined)).toBe(false); + expect(isObject([])).toBe(false); + expect(isObject(true)).toBe(false); + }); +}); diff --git a/package-lock.json b/package-lock.json index aca534791..3b7f50dc1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "starknet", - "version": "6.14.0", + "version": "6.14.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "starknet", - "version": "6.14.0", + "version": "6.14.1", "license": "MIT", "dependencies": { "@noble/curves": "~1.3.0", @@ -19,8 +19,7 @@ "lossless-json": "^4.0.1", "pako": "^2.0.4", "starknet-types-07": "npm:@starknet-io/types-js@^0.7.7", - "ts-mixer": "^6.0.3", - "url-join": "^4.0.1" + "ts-mixer": "^6.0.3" }, "devDependencies": { "@babel/plugin-transform-modules-commonjs": "^7.18.2", @@ -37,7 +36,6 @@ "@types/jest": "^29.5.0", "@types/jest-json-schema": "^6.1.1", "@types/pako": "^2.0.0", - "@types/url-join": "^4.0.1", "@typescript-eslint/eslint-plugin": "^7.4.0", "@typescript-eslint/parser": "^7.4.0", "ajv": "^8.12.0", @@ -5460,13 +5458,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/url-join": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@types/url-join/-/url-join-4.0.3.tgz", - "integrity": "sha512-3l1qMm3wqO0iyC5gkADzT95UVW7C/XXcdvUcShOideKF0ddgVRErEQQJXBd2kvQm+aSgqhBGHGB38TgMeT57Ww==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/yargs": { "version": "17.0.33", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", @@ -20019,12 +20010,6 @@ "punycode": "^2.1.0" } }, - "node_modules/url-join": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", - "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==", - "license": "MIT" - }, "node_modules/url-parse": { "version": "1.5.10", "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", diff --git a/package.json b/package.json index 569c8de92..345ed5930 100644 --- a/package.json +++ b/package.json @@ -1,16 +1,21 @@ { "name": "starknet", - "version": "6.14.0", + "version": "6.14.1", "description": "JavaScript library for Starknet", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/starknet-io/starknet.js.git" + }, "main": "dist/index.js", "module": "dist/index.mjs", "types": "dist/index.d.ts", + "browser": "dist/index.global.js", "jsdelivr": "dist/index.global.js", "unpkg": "dist/index.global.js", "exports": { ".": { "types": "./dist/index.d.ts", - "browser": "./dist/index.global.js", "import": "./dist/index.mjs", "require": "./dist/index.js" } @@ -49,8 +54,6 @@ "zk", "rollup" ], - "repository": "github:starknet-io/starknet.js", - "license": "MIT", "devDependencies": { "@babel/plugin-transform-modules-commonjs": "^7.18.2", "@babel/preset-env": "^7.18.2", @@ -66,7 +69,6 @@ "@types/jest": "^29.5.0", "@types/jest-json-schema": "^6.1.1", "@types/pako": "^2.0.0", - "@types/url-join": "^4.0.1", "@typescript-eslint/eslint-plugin": "^7.4.0", "@typescript-eslint/parser": "^7.4.0", "ajv": "^8.12.0", @@ -105,8 +107,7 @@ "lossless-json": "^4.0.1", "pako": "^2.0.4", "starknet-types-07": "npm:@starknet-io/types-js@^0.7.7", - "ts-mixer": "^6.0.3", - "url-join": "^4.0.1" + "ts-mixer": "^6.0.3" }, "lint-staged": { "*.ts": "eslint --cache --fix", diff --git a/src/account/default.ts b/src/account/default.ts index aa9154748..ad15db5e8 100644 --- a/src/account/default.ts +++ b/src/account/default.ts @@ -1,6 +1,3 @@ -// eslint-disable-next-line @typescript-eslint/no-unused-vars -import type { SPEC } from 'starknet-types-07'; - import { OutsideExecutionCallerAny, SNIP9_V1_INTERFACE_ID, @@ -30,6 +27,7 @@ import { DeployContractUDCResponse, DeployTransactionReceiptResponse, EstimateFee, + UniversalSuggestedFee, EstimateFeeAction, EstimateFeeBulk, Invocation, @@ -47,7 +45,7 @@ import { UniversalDeployerContractPayload, UniversalDetails, } from '../types'; -import { ETransactionVersion, ETransactionVersion3, ResourceBounds } from '../types/api'; +import { ETransactionVersion, ETransactionVersion3, type ResourceBounds } from '../types/api'; import { OutsideExecutionVersion, type OutsideExecution, @@ -58,6 +56,7 @@ import { CallData } from '../utils/calldata'; import { extractContractHashes, isSierra } from '../utils/contract'; import { parseUDCEvent } from '../utils/events'; import { calculateContractAddressFromHash } from '../utils/hash'; +import { isUndefined, isString } from '../utils/typed'; import { isHex, toBigInt, toCairoBool, toHex } from '../utils/num'; import { buildExecuteFromOutsideCallData, @@ -65,7 +64,6 @@ import { getTypedData, } from '../utils/outsideExecution'; import { parseContract } from '../utils/provider'; -import { isString } from '../utils/shortString'; import { supportsInterface } from '../utils/src5'; import { estimateFeeToBounds, @@ -678,7 +676,7 @@ export class Account extends Provider implements AccountInterface { * const call1: Call = { contractAddress: ethAddress, entrypoint: 'transfer', calldata: { * recipient: recipientAccount.address, amount: cairo.uint256(100) } }; * const outsideTransaction1: OutsideTransaction = await signerAccount.getOutsideTransaction(callOptions, call3); - * // result = { + * // result = { * // outsideExecution: { * // caller: '0x64b48806902a367c8598f4f95c305e8c1a1acba5f082d294a43793113115691', * // nonce: '0x28a612590dbc36927933c8ee0f357eee639c8b22b3d3aa86949eed3ada4ac55', @@ -784,9 +782,10 @@ export class Account extends Provider implements AccountInterface { version: ETransactionVersion, { type, payload }: EstimateFeeAction, details: UniversalDetails - ) { + ): Promise { let maxFee: BigNumberish = 0; let resourceBounds: ResourceBounds = estimateFeeToBounds(ZERO); + if (version === ETransactionVersion.V3) { resourceBounds = details.resourceBounds ?? @@ -803,28 +802,25 @@ export class Account extends Provider implements AccountInterface { }; } - public async getSuggestedFee({ type, payload }: EstimateFeeAction, details: UniversalDetails) { - let feeEstimate: EstimateFee; - + public async getSuggestedFee( + { type, payload }: EstimateFeeAction, + details: UniversalDetails + ): Promise { switch (type) { case TransactionType.INVOKE: - feeEstimate = await this.estimateInvokeFee(payload, details); - break; + return this.estimateInvokeFee(payload, details); case TransactionType.DECLARE: - feeEstimate = await this.estimateDeclareFee(payload, details); - break; + return this.estimateDeclareFee(payload, details); case TransactionType.DEPLOY_ACCOUNT: - feeEstimate = await this.estimateAccountDeployFee(payload, details); - break; + return this.estimateAccountDeployFee(payload, details); case TransactionType.DEPLOY: - feeEstimate = await this.estimateDeployFee(payload, details); - break; + return this.estimateDeployFee(payload, details); default: - feeEstimate = { + return { gas_consumed: 0n, gas_price: 0n, overall_fee: ZERO, @@ -834,10 +830,7 @@ export class Account extends Provider implements AccountInterface { data_gas_consumed: 0n, data_gas_price: 0n, }; - break; } - - return feeEstimate; } public async buildInvocation( @@ -863,7 +856,7 @@ export class Account extends Provider implements AccountInterface { const compressedCompiledContract = parseContract(contract); if ( - typeof compiledClassHash === 'undefined' && + isUndefined(compiledClassHash) && (details.version === ETransactionVersion3.F3 || details.version === ETransactionVersion3.V3) ) { throw Error('V3 Transaction work with Cairo1 Contracts and require compiledClassHash'); diff --git a/src/account/interface.ts b/src/account/interface.ts index d86405a2c..42ab2ab27 100644 --- a/src/account/interface.ts +++ b/src/account/interface.ts @@ -119,13 +119,13 @@ export abstract class AccountInterface extends ProviderInterface { /** * Estimate Fee for executing a UDC DEPLOY transaction on starknet * This is different from the normal DEPLOY transaction as it goes through the Universal Deployer Contract (UDC) - + * @param deployContractPayload array or singular * - classHash: computed class hash of compiled contract * - salt: address salt * - unique: bool if true ensure unique salt * - constructorCalldata: constructor calldata - * + * * @param estimateFeeDetails - * - blockIdentifier? * - nonce? diff --git a/src/channel/rpc_0_6.ts b/src/channel/rpc_0_6.ts index 1558f3b85..491ae32f9 100644 --- a/src/channel/rpc_0_6.ts +++ b/src/channel/rpc_0_6.ts @@ -51,13 +51,24 @@ export class RpcChannel { private specVersion?: string; + private transactionRetryIntervalFallback?: number; + readonly waitMode: Boolean; // behave like web2 rpc and return when tx is processed private batchClient?: BatchClient; constructor(optionsOrProvider?: RpcProviderOptions) { - const { nodeUrl, retries, headers, blockIdentifier, chainId, specVersion, waitMode, batch } = - optionsOrProvider || {}; + const { + nodeUrl, + retries, + headers, + blockIdentifier, + chainId, + specVersion, + waitMode, + transactionRetryIntervalFallback, + batch, + } = optionsOrProvider || {}; if (Object.values(NetworkName).includes(nodeUrl as NetworkName)) { this.nodeUrl = getDefaultNodeUrl(nodeUrl as NetworkName, optionsOrProvider?.default); } else if (nodeUrl) { @@ -72,6 +83,7 @@ export class RpcChannel { this.specVersion = specVersion; this.waitMode = waitMode || false; this.requestId = 0; + this.transactionRetryIntervalFallback = transactionRetryIntervalFallback; if (typeof batch === 'number') { this.batchClient = new BatchClient({ @@ -82,6 +94,10 @@ export class RpcChannel { } } + private get transactionRetryIntervalDefault() { + return this.transactionRetryIntervalFallback ?? 5000; + } + public setChainId(chainId: StarknetChainId) { this.chainId = chainId; } @@ -267,7 +283,7 @@ export class RpcChannel { let { retries } = this; let onchain = false; let isErrorState = false; - const retryInterval = options?.retryInterval ?? 5000; + const retryInterval = options?.retryInterval ?? this.transactionRetryIntervalDefault; const errorStates: any = options?.errorStates ?? [ RPC.ETransactionStatus.REJECTED, // TODO: commented out to preserve the long-standing behavior of "reverted" not being treated as an error by default diff --git a/src/index.ts b/src/index.ts index 6bf0681ec..4804e4b9f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -40,7 +40,6 @@ export * from './utils/responseParser'; export * from './utils/cairoDataTypes/uint256'; export * from './utils/cairoDataTypes/uint512'; export * from './utils/address'; -export * from './utils/url'; export * from './utils/calldata'; export * from './utils/calldata/enum'; export * from './utils/contract'; diff --git a/src/types/account.ts b/src/types/account.ts index fec5c0754..2a6022844 100644 --- a/src/types/account.ts +++ b/src/types/account.ts @@ -14,6 +14,11 @@ import { DeclareTransactionReceiptResponse, EstimateFeeResponse } from './provid export interface EstimateFee extends EstimateFeeResponse {} +export type UniversalSuggestedFee = { + maxFee: BigNumberish; + resourceBounds: ResourceBounds; +}; + export type EstimateFeeBulk = Array; // TODO: This is too wide generic with optional params diff --git a/src/types/calldata.ts b/src/types/calldata.ts index 75729969b..de9a53aaa 100644 --- a/src/types/calldata.ts +++ b/src/types/calldata.ts @@ -27,3 +27,6 @@ export const Literal = { } as const; export type Literal = ValuesType; + +export const ETH_ADDRESS = 'core::starknet::eth_address::EthAddress'; +export const NON_ZERO_PREFIX = 'core::zeroable::NonZero::'; diff --git a/src/types/lib/contract/abi.ts b/src/types/lib/contract/abi.ts index 5e0bee453..97cd01b55 100644 --- a/src/types/lib/contract/abi.ts +++ b/src/types/lib/contract/abi.ts @@ -4,7 +4,7 @@ import type { ENUM_EVENT, EVENT_FIELD, STRUCT_EVENT } from 'starknet-types-07'; export type Abi = ReadonlyArray; // Basic elements -export type AbiEntry = { name: string; type: 'felt' | 'felt*' | string }; +export type AbiEntry = { name: string; type: 'felt' | 'felt*' | 'event' | string }; export type EventEntry = { name: string; type: 'felt' | 'felt*' | string; kind: 'key' | 'data' }; diff --git a/src/types/lib/index.ts b/src/types/lib/index.ts index 9bbdcb138..69be02c71 100644 --- a/src/types/lib/index.ts +++ b/src/types/lib/index.ts @@ -2,8 +2,8 @@ import { StarknetChainId } from '../../constants'; import { weierstrass } from '../../utils/ec'; import { EDataAvailabilityMode, ResourceBounds } from '../api'; import { CairoEnum } from '../cairoEnum'; -import { ValuesType } from '../helpers/valuesType'; import { CompiledContract, CompiledSierraCasm, ContractClass } from './contract'; +import { ValuesType } from '../helpers/valuesType'; export type WeierstrassSignatureType = weierstrass.SignatureType; export type ArraySignatureType = string[]; diff --git a/src/utils/assert.ts b/src/utils/assert.ts index 56323961f..5b6ea51c3 100644 --- a/src/utils/assert.ts +++ b/src/utils/assert.ts @@ -1,6 +1,6 @@ /** * Asserts that the given condition is true, otherwise throws an error with an optional message. - * @param {any} condition - The condition to check. + * @param {boolean} condition - The condition to check. * @param {string} [message] - The optional message to include in the error. * @throws {Error} Throws an error if the condition is false. * @example diff --git a/src/utils/cairoDataTypes/felt.ts b/src/utils/cairoDataTypes/felt.ts index 480f7eba1..6bba632a5 100644 --- a/src/utils/cairoDataTypes/felt.ts +++ b/src/utils/cairoDataTypes/felt.ts @@ -1,7 +1,8 @@ // TODO Convert to CairoFelt base on CairoUint256 and implement it in the codebase in the backward compatible manner -import { BigNumberish, isBigInt, isBoolean, isHex, isStringWholeNumber } from '../num'; -import { encodeShortString, isShortString, isString, isText } from '../shortString'; +import { BigNumberish, isHex, isStringWholeNumber } from '../num'; +import { encodeShortString, isShortString, isText } from '../shortString'; +import { isBoolean, isString, isBigInt } from '../typed'; /** * Create felt Cairo type (cairo type helper) diff --git a/src/utils/calldata/cairo.ts b/src/utils/calldata/cairo.ts index aa1c477ec..88ca11801 100644 --- a/src/utils/calldata/cairo.ts +++ b/src/utils/calldata/cairo.ts @@ -4,7 +4,9 @@ import { AbiStructs, BigNumberish, ContractVersion, + ETH_ADDRESS, Literal, + NON_ZERO_PREFIX, Uint, Uint256, Uint512, @@ -116,16 +118,14 @@ export const isTypeBool = (type: string) => type === 'core::bool'; * @param {string} type - The type to be checked. * @returns - true if the type matches 'core::starknet::contract_address::ContractAddress', false otherwise. */ -export const isTypeContractAddress = (type: string) => - type === 'core::starknet::contract_address::ContractAddress'; +export const isTypeContractAddress = (type: string) => type === Literal.ContractAddress; /** * Determines if the given type is an Ethereum address type. * * @param {string} type - The type to check. * @returns - Returns true if the given type is 'core::starknet::eth_address::EthAddress', otherwise false. */ -export const isTypeEthAddress = (type: string) => - type === 'core::starknet::eth_address::EthAddress'; +export const isTypeEthAddress = (type: string) => type === ETH_ADDRESS; /** * Checks if the given type is 'core::bytes_31::bytes31'. * @@ -140,8 +140,9 @@ export const isTypeBytes31 = (type: string) => type === 'core::bytes_31::bytes31 * @returns - True if the given type is equal to 'core::byte_array::ByteArray', false otherwise. */ export const isTypeByteArray = (type: string) => type === 'core::byte_array::ByteArray'; -export const isTypeSecp256k1Point = (type: string) => - type === 'core::starknet::secp256k1::Secp256k1Point'; + +export const isTypeSecp256k1Point = (type: string) => type === Literal.Secp256k1Point; + export const isCairo1Type = (type: string) => type.includes('::'); /** * Retrieves the array type from the given type string. @@ -151,10 +152,9 @@ export const isCairo1Type = (type: string) => type.includes('::'); * @returns - The array type. */ export const getArrayType = (type: string) => { - if (isCairo1Type(type)) { - return type.substring(type.indexOf('<') + 1, type.lastIndexOf('>')); - } - return type.replace('*', ''); + return isCairo1Type(type) + ? type.substring(type.indexOf('<') + 1, type.lastIndexOf('>')) + : type.replace('*', ''); }; /** @@ -186,7 +186,7 @@ export function isCairo1Abi(abi: Abi): boolean { * ``` */ export function isTypeNonZero(type: string): boolean { - return type.startsWith('core::zeroable::NonZero::'); + return type.startsWith(NON_ZERO_PREFIX); } /** @@ -206,6 +206,7 @@ export function getAbiContractVersion(abi: Abi): ContractVersion { const testFunction = abi.find( (it) => it.type === 'function' && (it.inputs.length || it.outputs.length) ); + if (!testFunction) { return { cairo: undefined, compiler: undefined }; } diff --git a/src/utils/calldata/enum/CairoCustomEnum.ts b/src/utils/calldata/enum/CairoCustomEnum.ts index c1c8fd7a7..a1231fa2a 100644 --- a/src/utils/calldata/enum/CairoCustomEnum.ts +++ b/src/utils/calldata/enum/CairoCustomEnum.ts @@ -1,6 +1,6 @@ -export type CairoEnumRaw = { - [key: string]: any; -}; +import { isUndefined } from '../../typed'; + +export type CairoEnumRaw = Record; /** * Class to handle Cairo custom Enum @@ -31,9 +31,7 @@ export class CairoCustomEnum { if (variantsList.length === 0) { throw new Error('This Enum must have at least 1 variant'); } - const nbActiveVariants = variantsList.filter( - (content) => typeof content !== 'undefined' - ).length; + const nbActiveVariants = variantsList.filter((content) => !isUndefined(content)).length; if (nbActiveVariants !== 1) { throw new Error('This Enum must have exactly one active variant'); } @@ -45,12 +43,8 @@ export class CairoCustomEnum { * @returns the content of the valid variant of a Cairo custom Enum. */ public unwrap(): any { - const variants = Object.entries(this.variant); - const activeVariant = variants.find((item) => typeof item[1] !== 'undefined'); - if (typeof activeVariant === 'undefined') { - return undefined; - } - return activeVariant[1]; + const variants = Object.values(this.variant); + return variants.find((item) => !isUndefined(item)); } /** @@ -59,10 +53,7 @@ export class CairoCustomEnum { */ public activeVariant(): string { const variants = Object.entries(this.variant); - const activeVariant = variants.find((item) => typeof item[1] !== 'undefined'); - if (typeof activeVariant === 'undefined') { - return ''; - } - return activeVariant[0]; + const activeVariant = variants.find((item) => !isUndefined(item[1])); + return isUndefined(activeVariant) ? '' : activeVariant[0]; } } diff --git a/src/utils/calldata/enum/CairoOption.ts b/src/utils/calldata/enum/CairoOption.ts index 53e83ba6a..dfed9db92 100644 --- a/src/utils/calldata/enum/CairoOption.ts +++ b/src/utils/calldata/enum/CairoOption.ts @@ -1,4 +1,5 @@ import { ValuesType } from '../../../types/helpers/valuesType'; +import { isUndefined } from '../../typed'; export const CairoOptionVariant = { Some: 0, @@ -10,7 +11,7 @@ export type CairoOptionVariant = ValuesType; /** * Class to handle Cairo Option * @param variant CairoOptionVariant.Some or CairoOptionVariant.None - * @param someContent value of type T. + * @param content value of type T. * @returns an instance representing a Cairo Option. * @example * ```typescript @@ -22,17 +23,17 @@ export class CairoOption { readonly None?: boolean; - constructor(variant: CairoOptionVariant | number, someContent?: T) { + constructor(variant: CairoOptionVariant | number, content?: T) { if (!(variant in Object.values(CairoOptionVariant))) { - throw new Error('Wrong variant : should be CairoOptionVariant.Some or .None.'); + throw new Error('Wrong variant! It should be CairoOptionVariant.Some or .None.'); } if (variant === CairoOptionVariant.Some) { - if (typeof someContent === 'undefined') { + if (isUndefined(content)) { throw new Error( 'The creation of a Cairo Option with "Some" variant needs a content as input.' ); } - this.Some = someContent; + this.Some = content; this.None = undefined; } else { this.Some = undefined; @@ -46,10 +47,7 @@ export class CairoOption { * If None, returns 'undefined'. */ public unwrap(): T | undefined { - if (this.None) { - return undefined; - } - return this.Some; + return this.None ? undefined : this.Some; } /** @@ -57,7 +55,7 @@ export class CairoOption { * @returns true if the valid variant is 'isSome'. */ public isSome(): boolean { - return !(typeof this.Some === 'undefined'); + return !isUndefined(this.Some); } /** diff --git a/src/utils/calldata/enum/CairoResult.ts b/src/utils/calldata/enum/CairoResult.ts index c09a0b46a..e50063cc1 100644 --- a/src/utils/calldata/enum/CairoResult.ts +++ b/src/utils/calldata/enum/CairoResult.ts @@ -1,4 +1,5 @@ import { ValuesType } from '../../../types/helpers/valuesType'; +import { isUndefined } from '../../typed'; export const CairoResultVariant = { Ok: 0, @@ -24,7 +25,7 @@ export class CairoResult { constructor(variant: CairoResultVariant | number, resultContent: T | U) { if (!(variant in Object.values(CairoResultVariant))) { - throw new Error('Wrong variant : should be CairoResultVariant.Ok or .Err.'); + throw new Error('Wrong variant! It should be CairoResultVariant.Ok or .Err.'); } if (variant === CairoResultVariant.Ok) { this.Ok = resultContent as T; @@ -40,10 +41,10 @@ export class CairoResult { * @returns the content of the valid variant of a Cairo Result. */ public unwrap(): T | U { - if (typeof this.Ok !== 'undefined') { + if (!isUndefined(this.Ok)) { return this.Ok; } - if (typeof this.Err !== 'undefined') { + if (!isUndefined(this.Err)) { return this.Err; } throw new Error('Both Result.Ok and .Err are undefined. Not authorized.'); @@ -54,7 +55,7 @@ export class CairoResult { * @returns true if the valid variant is 'Ok'. */ public isOk(): boolean { - return !(typeof this.Ok === 'undefined'); + return !isUndefined(this.Ok); } /** @@ -62,6 +63,6 @@ export class CairoResult { * @returns true if the valid variant is 'isErr'. */ public isErr(): boolean { - return !(typeof this.Err === 'undefined'); + return !isUndefined(this.Err); } } diff --git a/src/utils/calldata/formatter.ts b/src/utils/calldata/formatter.ts index 260299d0c..a7be17866 100644 --- a/src/utils/calldata/formatter.ts +++ b/src/utils/calldata/formatter.ts @@ -1,7 +1,15 @@ -import { isBigInt } from '../num'; +import { isBigInt, isObject } from '../typed'; import { decodeShortString } from '../shortString'; const guard = { + /** + * Checks if the data is a BigInt (BN) and throws an error if not. + * + * @param {Record} data - The data object containing the key to check. + * @param {Record} type - The type definition object. + * @param {string} key - The key in the data object to check. + * @throws {Error} If the data type does not match the expected BigInt (BN) type. + */ isBN: (data: Record, type: Record, key: string) => { if (!isBigInt(data[key])) throw new Error( @@ -10,6 +18,14 @@ const guard = { } to be BN instead it is ${typeof data[key]}` ); }, + /** + * Throws an error for unhandled formatter types. + * + * @param {Record} data - The data object containing the key. + * @param {Record} type - The type definition object. + * @param {string} key - The key in the data object to check. + * @throws {Error} If the formatter encounters an unknown type. + */ unknown: (data: Record, type: Record, key: string) => { throw new Error(`Unhandled formatter type on ${key}:${type[key]} for data ${key}:${data[key]}`); }, @@ -18,16 +34,37 @@ const guard = { /** * Formats the given data based on the provided type definition. * - * @param {any} data - The data to be formatted. - * @param {any} type - The type definition for the data. + * @param {Record} data - The data to be formatted. + * @param {Record} type - The type definition for the data. * @param {any} [sameType] - The same type definition to be used (optional). - * @returns - The formatted data. + * @returns {Record} The formatted data. + * + * @example + * // Example 1: Formatting a simple object + * const data = { value: 1n, name: 2n }; + * const type = { value: 'number', name: 'string' }; + * const formatted = formatter(data, type); + * // formatted: { value: 1n, name: '2n' } + * + * @example + * // Example 2: Formatting an object with nested structures + * const data = { test: { id: 1n, value: 30n }, active: 1n }; + * const type = { test: { id: 'number', value: 'number' }, active: 'number' }; + * const formatted = formatter(data, type); + * // formatted: { test: { id: 1n, value: 30n }, active: 1n } + * + * @example + * // Example 3: Handling arrays in the data object + * const data = { items: [1n, 2n, 3n], value: 4n }; + * const type = { items: ['number'], value: 'string' }; + * const formatted = formatter(data, type); + * // formatted: { items: [1n, 2n, 3n], value: '4n' } */ export default function formatter( data: Record, type: Record, sameType?: any -) { +): Record { // match data element with type element return Object.entries(data).reduce( (acc, [key, value]: [any, any]) => { @@ -67,7 +104,7 @@ export default function formatter( acc[key] = Object.values(arrayObj); return acc; } - if (typeof elType === 'object') { + if (isObject(elType)) { acc[key] = formatter(data[key], elType); return acc; } diff --git a/src/utils/calldata/index.ts b/src/utils/calldata/index.ts index 53336b88c..4be129fc9 100644 --- a/src/utils/calldata/index.ts +++ b/src/utils/calldata/index.ts @@ -15,7 +15,8 @@ import { ValidateType, } from '../../types'; import assert from '../assert'; -import { isBigInt, toHex } from '../num'; +import { toHex } from '../num'; +import { isBigInt } from '../typed'; import { getSelectorFromName } from '../hash/selector'; import { isLongText } from '../shortString'; import { byteArrayFromString } from './byteArray'; diff --git a/src/utils/calldata/parser/index.ts b/src/utils/calldata/parser/index.ts index 76a599378..098999f29 100644 --- a/src/utils/calldata/parser/index.ts +++ b/src/utils/calldata/parser/index.ts @@ -4,6 +4,19 @@ import { AbiParserInterface } from './interface'; import { AbiParser1 } from './parser-0-1.1.0'; import { AbiParser2 } from './parser-2.0.0'; +/** + * Creates ABI parser + * + * @param {Abi} abi + * @returns {AbiParserInterface} abi parser interface + * + * @example + * const abiParser2 = createAbiParser([getInterfaceAbi('struct')]); + * // abiParser2 instanceof AbiParser2 === true + * + * const abiParser1 = createAbiParser([getFunctionAbi('struct')]); + * // abiParser1 instanceof AbiParser1 === true + */ export function createAbiParser(abi: Abi): AbiParserInterface { const version = getAbiVersion(abi); if (version === 0 || version === 1) { @@ -15,17 +28,50 @@ export function createAbiParser(abi: Abi): AbiParserInterface { throw Error(`Unsupported ABI version ${version}`); } -export function getAbiVersion(abi: Abi) { +/** + * Retrieves ABI version + * + * @param {Abi} abi + * @returns {1 | 2 | 0} abi 1, 2 or 0 version + * + * @example + * // Example 1: Return ABI version 2 + * const version = getAbiVersion([getInterfaceAbi()]); + * // version === 2 + * + * // Example 2: Return ABI version 1 + * const version = getAbiVersion([getInterfaceAbi('core::bool')]); + * // version === 1 + * + * // Example 3: Return ABI version 0 + * const version = getAbiVersion([getInterfaceAbi('felt')]); + * // version === 0 + */ +export function getAbiVersion(abi: Abi): 1 | 2 | 0 { if (abi.find((it) => it.type === 'interface')) return 2; if (isCairo1Abi(abi)) return 1; return 0; } +/** + * Checks if no constructor valid + * + * @param {string} method + * @param {RawArgs} argsCalldata + * @param {FunctionAbi} abiMethod + * @returns boolean + * + * @example + * const result1 = isNoConstructorValid('constructor', []) + * // result1 === true + * const result2 = isNoConstructorValid('test', ['test']) + * // result2 === false + */ export function isNoConstructorValid( method: string, argsCalldata: RawArgs, abiMethod?: FunctionAbi -) { +): boolean { // No constructor in abi and validly empty args return method === 'constructor' && !abiMethod && !argsCalldata.length; } diff --git a/src/utils/calldata/parser/parser-2.0.0.ts b/src/utils/calldata/parser/parser-2.0.0.ts index 1a6cbe48e..f80102c2c 100644 --- a/src/utils/calldata/parser/parser-2.0.0.ts +++ b/src/utils/calldata/parser/parser-2.0.0.ts @@ -33,7 +33,7 @@ export class AbiParser2 implements AbiParserInterface { const intf = this.abi.find( (it: FunctionAbi | AbiEvent | AbiStruct | InterfaceAbi) => it.type === 'interface' ) as InterfaceAbi; - return intf.items.find((it) => it.name === name); + return intf?.items?.find((it) => it.name === name); } /** @@ -41,11 +41,8 @@ export class AbiParser2 implements AbiParserInterface { * @returns Abi */ public getLegacyFormat(): Abi { - return this.abi.flatMap((e: FunctionAbi | LegacyEvent | AbiStruct | InterfaceAbi) => { - if (e.type === 'interface') { - return e.items; - } - return e; + return this.abi.flatMap((it: FunctionAbi | LegacyEvent | AbiStruct | InterfaceAbi) => { + return it.type === 'interface' ? it.items : it; }); } } diff --git a/src/utils/calldata/propertyOrder.ts b/src/utils/calldata/propertyOrder.ts index 4619700fa..a6d046cdc 100644 --- a/src/utils/calldata/propertyOrder.ts +++ b/src/utils/calldata/propertyOrder.ts @@ -24,8 +24,7 @@ import { CairoResultVariant, } from './enum'; import extractTupleMemberTypes from './tuple'; - -import { isString } from '../shortString'; +import { isUndefined, isString } from '../typed'; function errorU256(key: string) { return Error( @@ -184,7 +183,7 @@ export default function orderPropsByAbi( const unorderedCustomEnum = unorderedObject2 as CairoCustomEnum; const variants = Object.entries(unorderedCustomEnum.variant); const newEntries = variants.map((variant) => { - if (typeof variant[1] === 'undefined') { + if (isUndefined(variant[1])) { return variant; } const variantType: string = abiObject.type.substring( diff --git a/src/utils/calldata/requestParser.ts b/src/utils/calldata/requestParser.ts index eb6e3ace5..203b17ba4 100644 --- a/src/utils/calldata/requestParser.ts +++ b/src/utils/calldata/requestParser.ts @@ -13,14 +13,17 @@ import { CairoUint256 } from '../cairoDataTypes/uint256'; import { CairoUint512 } from '../cairoDataTypes/uint512'; import { addHexPrefix, removeHexPrefix } from '../encode'; import { toHex } from '../num'; -import { encodeShortString, isString, isText, splitLongString } from '../shortString'; +import { encodeShortString, isText, splitLongString } from '../shortString'; +import { isUndefined, isString } from '../typed'; import { byteArrayFromString } from './byteArray'; import { felt, getArrayType, isTypeArray, + isTypeByteArray, isTypeBytes31, isTypeEnum, + isTypeEthAddress, isTypeNonZero, isTypeOption, isTypeResult, @@ -81,7 +84,7 @@ function parseTuple(element: object, typeStr: string): Tupled[] { if (elements.length !== memberTypes.length) { throw Error( `ParseTuple: provided and expected abi tuple size do not match. - provided: ${elements} + provided: ${elements} expected: ${memberTypes}` ); } @@ -148,10 +151,9 @@ function parseCalldataValue( if (CairoUint512.isAbiType(type)) { return new CairoUint512(element as any).toApiRequest(); } - if (type === 'core::starknet::eth_address::EthAddress') - return parseBaseTypes(type, element as BigNumberish); + 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; @@ -185,7 +187,7 @@ function parseCalldataValue( const myOption = element as CairoOption; if (myOption.isSome()) { const listTypeVariant = variants.find((variant) => variant.name === 'Some'); - if (typeof listTypeVariant === 'undefined') { + if (isUndefined(listTypeVariant)) { throw Error(`Error in abi : Option has no 'Some' variant.`); } const typeVariantSome = listTypeVariant.type; @@ -210,7 +212,7 @@ function parseCalldataValue( const myResult = element as CairoResult; if (myResult.isOk()) { const listTypeVariant = variants.find((variant) => variant.name === 'Ok'); - if (typeof listTypeVariant === 'undefined') { + if (isUndefined(listTypeVariant)) { throw Error(`Error in abi : Result has no 'Ok' variant.`); } const typeVariantOk = listTypeVariant.type; @@ -228,9 +230,10 @@ function parseCalldataValue( } return [CairoResultVariant.Ok.toString(), parsedParameter]; } + // is Result::Err const listTypeVariant = variants.find((variant) => variant.name === 'Err'); - if (typeof listTypeVariant === 'undefined') { + if (isUndefined(listTypeVariant)) { throw Error(`Error in abi : Result has no 'Err' variant.`); } const typeVariantErr = listTypeVariant.type; @@ -247,7 +250,7 @@ function parseCalldataValue( const myEnum = element as CairoCustomEnum; const activeVariant: string = myEnum.activeVariant(); const listTypeVariant = variants.find((variant) => variant.name === activeVariant); - if (typeof listTypeVariant === 'undefined') { + if (isUndefined(listTypeVariant)) { throw Error(`Not find in abi : Enum has no '${activeVariant}' variant.`); } const typeActiveVariant = listTypeVariant.type; @@ -280,6 +283,48 @@ function parseCalldataValue( * @param structs - structs from abi * @param enums - enums from abi * @return {string | string[]} - parsed arguments in format that contract is expecting + * + * @example + * const abiEntry = { name: 'test', type: 'struct' }; + * const abiStructs: AbiStructs = { + * struct: { + * members: [ + * { + * name: 'test_name', + * type: 'test_type', + * offset: 1, + * }, + * ], + * size: 2, + * name: 'cairo__struct', + * type: 'struct', + * }, + * }; + * + * const abiEnums: AbiEnums = { + * enum: { + * variants: [ + * { + * name: 'test_name', + * type: 'cairo_struct_variant', + * offset: 1, + * }, + * ], + * size: 2, + * name: 'test_cairo', + * type: 'enum', + * }, + * }; + * + * const args = [{ test_name: 'test' }]; + * const argsIterator = args[Symbol.iterator](); + * const parsedField = parseCalldataField( + * argsIterator, + * abiEntry, + * abiStructs, + * abiEnums + * ); + * // parsedField === ['1952805748'] */ export function parseCalldataField( argsIterator: Iterator, @@ -303,13 +348,10 @@ export function parseCalldataField( return parseCalldataValue(value, input.type, structs, enums); case isTypeNonZero(type): return parseBaseTypes(getArrayType(type), value); - case type === 'core::starknet::eth_address::EthAddress': + case isTypeEthAddress(type): return parseBaseTypes(type, value); // Struct or Tuple - case isTypeStruct(type, structs) || - isTypeTuple(type) || - CairoUint256.isAbiType(type) || - CairoUint256.isAbiType(type): + case isTypeStruct(type, structs) || isTypeTuple(type) || CairoUint256.isAbiType(type): return parseCalldataValue(value as ParsedStruct | BigNumberish[], type, structs, enums); // Enums diff --git a/src/utils/calldata/responseParser.ts b/src/utils/calldata/responseParser.ts index 6f25434c7..07410ccaa 100644 --- a/src/utils/calldata/responseParser.ts +++ b/src/utils/calldata/responseParser.ts @@ -23,7 +23,9 @@ import { isTypeArray, isTypeBool, isTypeByteArray, + isTypeBytes31, isTypeEnum, + isTypeEthAddress, isTypeNonZero, isTypeSecp256k1Point, isTypeTuple, @@ -60,10 +62,10 @@ function parseBaseTypes(type: string, it: Iterator) { const limb2 = it.next().value; const limb3 = it.next().value; return new CairoUint512(limb0, limb1, limb2, limb3).toBigInt(); - case type === 'core::starknet::eth_address::EthAddress': + case isTypeEthAddress(type): temp = it.next().value; return BigInt(temp); - case type === 'core::bytes_31::bytes31': + case isTypeBytes31(type): temp = it.next().value; return decodeShortString(temp); case isTypeSecp256k1Point(type): @@ -151,7 +153,7 @@ function parseResponseValue( // type struct if (structs && element.type in structs && structs[element.type]) { - if (element.type === 'core::starknet::eth_address::EthAddress') { + if (isTypeEthAddress(element.type)) { return parseBaseTypes(element.type, responseIterator); } return structs[element.type].members.reduce((acc, el) => { diff --git a/src/utils/calldata/tuple.ts b/src/utils/calldata/tuple.ts index 263704d3e..bc0578e5f 100644 --- a/src/utils/calldata/tuple.ts +++ b/src/utils/calldata/tuple.ts @@ -104,13 +104,30 @@ function extractCairo1Tuple(type: string): string[] { } /** - * Convert tuple string definition into object like definition - * @param type tuple string definition - * @returns object like tuple + * Convert a tuple string definition into an object-like definition. + * Supports both Cairo 0 and Cairo 1 tuple formats. + * + * @param type - The tuple string definition (e.g., "(u8, u8)" or "(x:u8, y:u8)"). + * @returns An array of strings or objects representing the tuple components. + * + * @example + * // Cairo 0 Tuple + * const cairo0Tuple = "(u8, u8)"; + * const result = extractTupleMemberTypes(cairo0Tuple); + * // result: ["u8", "u8"] + * + * @example + * // Named Cairo 0 Tuple + * const namedCairo0Tuple = "(x:u8, y:u8)"; + * const namedResult = extractTupleMemberTypes(namedCairo0Tuple); + * // namedResult: [{ name: "x", type: "u8" }, { name: "y", type: "u8" }] + * + * @example + * // Cairo 1 Tuple + * const cairo1Tuple = "(core::result::Result::, u8)"; + * const cairo1Result = extractTupleMemberTypes(cairo1Tuple); + * // cairo1Result: ["core::result::Result::", "u8"] */ export default function extractTupleMemberTypes(type: string): (string | object)[] { - if (isCairo1Type(type)) { - return extractCairo1Tuple(type); - } - return extractCairo0Tuple(type); + return isCairo1Type(type) ? extractCairo1Tuple(type) : extractCairo0Tuple(type); } diff --git a/src/utils/calldata/validate.ts b/src/utils/calldata/validate.ts index 129d71ed1..1ce4bc277 100644 --- a/src/utils/calldata/validate.ts +++ b/src/utils/calldata/validate.ts @@ -1,7 +1,3 @@ -/** - * Validate cairo contract method arguments - * Flow: Determine type from abi and than validate against parameter - */ import { AbiEntry, AbiEnums, @@ -14,8 +10,9 @@ import { import assert from '../assert'; import { CairoUint256 } from '../cairoDataTypes/uint256'; import { CairoUint512 } from '../cairoDataTypes/uint512'; -import { isBigInt, isBoolean, isHex, isNumber, toBigInt } from '../num'; -import { isLongText, isString } from '../shortString'; +import { isHex, toBigInt } from '../num'; +import { isLongText } from '../shortString'; +import { isBoolean, isNumber, isString, isBigInt, isObject } from '../typed'; import { getArrayType, isLen, @@ -24,6 +21,7 @@ import { isTypeByteArray, isTypeBytes31, isTypeEnum, + isTypeEthAddress, isTypeFelt, isTypeLiteral, isTypeNonZero, @@ -64,15 +62,15 @@ const validateUint = (parameter: any, input: AbiEntry) => { if (isNumber(parameter)) { assert( parameter <= Number.MAX_SAFE_INTEGER, - `Validation: Parameter is to large to be typed as Number use (BigInt or String)` + 'Validation: Parameter is too large to be typed as Number use (BigInt or String)' ); } assert( isString(parameter) || isNumber(parameter) || isBigInt(parameter) || - (typeof parameter === 'object' && 'low' in parameter && 'high' in parameter) || - (typeof parameter === 'object' && + (isObject(parameter) && 'low' in parameter && 'high' in parameter) || + (isObject(parameter) && ['limb0', 'limb1', 'limb2', 'limb3'].every((key) => key in parameter)), `Validate: arg ${input.name} of cairo type ${ input.type @@ -128,12 +126,15 @@ const validateUint = (parameter: any, input: AbiEntry) => { case Uint.u256: assert( param >= 0n && param <= 2n ** 256n - 1n, - `Validate: arg ${input.name} is ${input.type} 0 - 2^256-1` + `Validate: arg ${input.name} is ${input.type} should be in range 0 - 2^256-1` ); break; case Uint.u512: - assert(CairoUint512.is(param), `Validate: arg ${input.name} is ${input.type} 0 - 2^512-1`); + assert( + CairoUint512.is(param), + `Validate: arg ${input.name} is ${input.type} should be in range 0 - 2^512-1` + ); break; case Literal.ClassHash: @@ -178,11 +179,8 @@ const validateStruct = (parameter: any, input: AbiEntry, structs: AbiStructs) => return; } - if (input.type === 'core::starknet::eth_address::EthAddress') { - assert( - typeof parameter !== 'object', - `EthAddress type is waiting a BigNumberish. Got ${parameter}` - ); + if (isTypeEthAddress(input.type)) { + assert(!isObject(parameter), `EthAddress type is waiting a BigNumberish. Got "${parameter}"`); const param = BigInt(parameter.toString(10)); assert( // from : https://github.com/starkware-libs/starknet-specs/blob/29bab650be6b1847c92d4461d4c33008b5e50b1a/api/starknet_api_openrpc.json#L1259 @@ -193,8 +191,8 @@ const validateStruct = (parameter: any, input: AbiEntry, structs: AbiStructs) => } 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)` + isObject(parameter), + `Validate: arg ${input.name} is cairo type struct (${input.type}), and should be defined as a js object (not array)` ); // shallow struct validation, only first depth level @@ -208,9 +206,10 @@ const validateStruct = (parameter: any, input: AbiEntry, structs: AbiStructs) => const validateEnum = (parameter: any, input: AbiEntry) => { assert( - typeof parameter === 'object' && !Array.isArray(parameter), - `Validate: arg ${input.name} is cairo type Enum (${input.type}), and should be defined as js object (not array)` + isObject(parameter), + `Validate: arg ${input.name} is cairo type Enum (${input.type}), and should be defined as a js object (not array)` ); + const methodsKeys = Object.getOwnPropertyNames(Object.getPrototypeOf(parameter)); const keys = [...Object.getOwnPropertyNames(parameter), ...methodsKeys]; if (isTypeOption(input.type) && keys.includes('isSome') && keys.includes('isNone')) { @@ -223,15 +222,12 @@ const validateEnum = (parameter: any, input: AbiEntry) => { return; // Custom Enum } throw new Error( - `Validate Enum: argument ${input.name}, type ${input.type}, value received ${parameter}, is not an Enum.` + `Validate Enum: argument ${input.name}, type ${input.type}, value received "${parameter}", is not an Enum.` ); }; const validateTuple = (parameter: any, input: AbiEntry) => { - assert( - typeof parameter === 'object' && !Array.isArray(parameter), - `Validate: arg ${input.name} should be a tuple (defined as object)` - ); + assert(isObject(parameter), `Validate: arg ${input.name} should be a tuple (defined as object)`); // todo: skip tuple structural validation for now }; @@ -289,6 +285,7 @@ const validateNonZero = (parameter: any, input: AbiEntry) => { // so, are authorized here : u8, u16, u32, u64, u128, u256 and felt252. const baseType = getArrayType(input.type); + assert( (isTypeUint(baseType) && baseType !== CairoUint512.abiSelector) || isTypeFelt(baseType), `Validate: ${input.name} type is not authorized for NonZero type.` @@ -303,7 +300,8 @@ const validateNonZero = (parameter: any, input: AbiEntry) => { break; case isTypeUint(baseType): validateUint(parameter, { name: '', type: baseType }); - switch (input.type) { + + switch (baseType) { case Uint.u256: assert( new CairoUint256(parameter).toBigInt() > 0, @@ -319,17 +317,69 @@ const validateNonZero = (parameter: any, input: AbiEntry) => { break; default: throw new Error( - `Validate Unhandled: argument ${input.name}, type ${input.type}, value ${parameter}` + `Validate Unhandled: argument ${input.name}, type ${input.type}, value "${parameter}"` ); } }; +/** + * Validate cairo contract method arguments + * Flow: Determine type from abi and than validate against parameter + * + * @param {FunctionAbi} abiMethod - Abi method. + * @param {any[]} args - Arguments. + * @param {AbiStructs} structs - ABI structs. + * @param {AbiEnums} enums - ABI enums. + * @returns {void} - Return void if validation passes + * + * @example + * const functionAbi: FunctionAbi = { + * inputs: [{ name: 'test', type: 'felt' }], + * name: 'test', + * outputs: [{ name: 'test', type: 'felt' }], + * stateMutability: 'view', + * type: 'function', + * }; + * + * const abiStructs: AbiStructs = { + * abi_structs: { + * members: [ + * { + * name: 'test_name', + * type: 'test_type', + * offset: 1, + * }, + * ], + * size: 2, + * name: 'cairo_event_struct', + * type: 'struct', + * }, + * }; + * + * const abiEnums: AbiEnums = { + * abi_enums: { + * variants: [ + * { + * name: 'test_name', + * type: 'cairo_event_struct_variant', + * offset: 1, + * }, + * ], + * size: 2, + * name: 'test_cairo_event', + * type: 'enum', + * }, + * }; + * + * validateFields(functionAbi, [1n], abiStructs, abiEnums); // Returns void since validation passes + * validateFields(functionAbi, [{}], abiStructs, abiEnums); // Throw an error because paramters are not valid + */ export default function validateFields( abiMethod: FunctionAbi, - args: Array, + args: any[], structs: AbiStructs, enums: AbiEnums -) { +): void { abiMethod.inputs.reduce((acc, input) => { const parameter = args[acc]; diff --git a/src/utils/contract.ts b/src/utils/contract.ts index b93c8c230..0ec3adf7c 100644 --- a/src/utils/contract.ts +++ b/src/utils/contract.ts @@ -10,8 +10,7 @@ import { CompleteDeclareContractPayload, DeclareContractPayload } from '../types import { computeCompiledClassHash, computeContractClassHash } from './hash'; import { parse } from './json'; import { decompressProgram } from './stark'; - -import { isString } from './shortString'; +import { isString } from './typed'; /** * Checks if a given contract is in Sierra (Safe Intermediate Representation) format. diff --git a/src/utils/encode.ts b/src/utils/encode.ts index 6b9878a40..081712114 100644 --- a/src/utils/encode.ts +++ b/src/utils/encode.ts @@ -1,6 +1,5 @@ import { base64 } from '@scure/base'; -/* eslint-disable no-param-reassign */ export const IS_BROWSER = typeof window !== 'undefined'; const STRING_ZERO = '0'; @@ -106,7 +105,7 @@ export function btoaUniversal(b: ArrayBuffer): string { * // result = "48656c6c6f" * ``` */ -export function buf2hex(buffer: Uint8Array) { +export function buf2hex(buffer: Uint8Array): string { return buffer.reduce((r, x) => r + x.toString(16).padStart(2, '0'), ''); } @@ -163,7 +162,12 @@ export function addHexPrefix(hex: string): string { * // result = '00000hello' * ``` */ -function padString(str: string, length: number, left: boolean, padding = STRING_ZERO): string { +function padString( + str: string, + length: number, + left: boolean, + padding: string = STRING_ZERO +): string { const diff = length - str.length; let result = str; if (diff > 0) { @@ -183,7 +187,6 @@ function padString(str: string, length: number, left: boolean, padding = STRING_ * @param {number} length The target length for the padded string. * @param {string} [padding='0'] The string to use for padding. Defaults to '0'. * @returns {string} The padded string. - * * @example * ```typescript * const myString = '1A3F'; @@ -191,7 +194,7 @@ function padString(str: string, length: number, left: boolean, padding = STRING_ * // result: '0000001A3F' * ``` */ -export function padLeft(str: string, length: number, padding = STRING_ZERO): string { +export function padLeft(str: string, length: number, padding: string = STRING_ZERO): string { return padString(str, length, true, padding); } @@ -215,7 +218,7 @@ export function padLeft(str: string, length: number, padding = STRING_ZERO): str * * ``` */ -export function calcByteLength(str: string, byteSize = 8): number { +export function calcByteLength(str: string, byteSize: number = 8): number { const { length } = str; const remainder = length % byteSize; return remainder ? ((length - remainder) / byteSize) * byteSize + byteSize : length; @@ -242,7 +245,11 @@ export function calcByteLength(str: string, byteSize = 8): number { * // result: '00000123' (padded to 8 characters) * ``` */ -export function sanitizeBytes(str: string, byteSize = 8, padding = STRING_ZERO): string { +export function sanitizeBytes( + str: string, + byteSize: number = 8, + padding: string = STRING_ZERO +): string { return padLeft(str, calcByteLength(str, byteSize), padding); } @@ -251,8 +258,8 @@ export function sanitizeBytes(str: string, byteSize = 8, padding = STRING_ZERO): * and then re-adding the '0x' prefix. * * *[no internal usage]* - * @param hex hex-string - * @returns format: hex-string + * @param {string} hex hex-string + * @returns {string} format: hex-string * * @example * ```typescript @@ -262,12 +269,9 @@ export function sanitizeBytes(str: string, byteSize = 8, padding = STRING_ZERO): * ``` */ export function sanitizeHex(hex: string): string { - hex = removeHexPrefix(hex); - hex = sanitizeBytes(hex, 2); - if (hex) { - hex = addHexPrefix(hex); - } - return hex; + const hexWithoutPrefix = removeHexPrefix(hex); + const sanitizedHex = sanitizeBytes(hexWithoutPrefix, 2); + return sanitizedHex ? addHexPrefix(sanitizedHex) : sanitizedHex; } /** @@ -285,7 +289,7 @@ export function sanitizeHex(hex: string): string { * // result: 'PASCAL_CASE_EXAMPLE' * ``` */ -export const pascalToSnake = (text: string) => +export const pascalToSnake = (text: string): string => /[a-z]/.test(text) ? text .split(/(?=[A-Z])/) diff --git a/src/utils/events/index.ts b/src/utils/events/index.ts index 07ef25a44..498d31110 100644 --- a/src/utils/events/index.ts +++ b/src/utils/events/index.ts @@ -14,6 +14,7 @@ import { type CairoEventVariant, type InvokeTransactionReceiptResponse, type AbiEntry, + DeployContractUDCResponse, } from '../../types'; import assert from '../assert'; import { isCairo1Abi } from '../calldata/cairo'; @@ -21,6 +22,7 @@ import responseParser from '../calldata/responseParser'; import { starkCurve } from '../ec'; import { addHexPrefix, utf8ToArray } from '../encode'; import { cleanHex } from '../num'; +import { isUndefined, isObject } from '../typed'; /** * Check if an ABI entry is related to events. @@ -51,7 +53,7 @@ export function isAbiEvent(object: AbiEntry): boolean { } * ``` */ -function getCairo0AbiEvents(abi: Abi) { +function getCairo0AbiEvents(abi: Abi): AbiEvents { return abi .filter((abiEntry) => abiEntry.type === 'event') .reduce((acc, abiEntry) => { @@ -75,11 +77,10 @@ function getCairo0AbiEvents(abi: Abi) { * ```typescript * const result = events.getCairo1AbiEvents(abi1); * // result = { - * // '0x22ea134d4126804c60797e633195f8c9aa5fd6d1567e299f4961d0e96f373ee': + * // '0x22ea134d4126804c60797e633195f8c9aa5fd6d1567e299f4961d0e96f373ee': * // { '0x34e55c1cd55f1338241b50d352f0e91c7e4ffad0e4271d64eb347589ebdfd16': { * // kind: 'struct', type: 'event', * // name: 'ka::ExComponent::ex_logic_component::Mint', - * // members: [{ * // name: 'spender', * // type: 'core::starknet::contract_address::ContractAddress', @@ -88,7 +89,7 @@ function getCairo0AbiEvents(abi: Abi) { * // ... * ``` */ -function getCairo1AbiEvents(abi: Abi) { +function getCairo1AbiEvents(abi: Abi): AbiEvents { const abiEventsStructs = abi.filter((obj) => isAbiEvent(obj) && obj.kind === 'struct'); const abiEventsEnums = abi.filter((obj) => isAbiEvent(obj) && obj.kind === 'enum'); const abiEventsData: AbiEvents = abiEventsStructs.reduce((acc: CairoEvent, event: CairoEvent) => { @@ -99,20 +100,24 @@ function getCairo1AbiEvents(abi: Abi) { // eslint-disable-next-line no-constant-condition while (true) { const eventEnum = abiEventsEnums.find((eventE) => eventE.variants.some(findName)); - if (typeof eventEnum === 'undefined') break; + if (isUndefined(eventEnum)) break; const variant = eventEnum.variants.find(findName); nameList.unshift(variant.name); if (variant.kind === 'flat') flat = true; name = eventEnum.name; } + if (nameList.length === 0) { throw new Error('inconsistency in ABI events definition.'); } + if (flat) nameList = [nameList[nameList.length - 1]]; + const final = nameList.pop(); let result: AbiEvents = { [addHexPrefix(starkCurve.keccak(utf8ToArray(final!)).toString(16))]: event, }; + while (nameList.length > 0) { result = { [addHexPrefix(starkCurve.keccak(utf8ToArray(nameList.pop()!)).toString(16))]: result, @@ -134,11 +139,10 @@ function getCairo1AbiEvents(abi: Abi) { * ```typescript * const result = events.getAbiEvents(abi); * // result = { - * // '0x22ea134d4126804c60797e633195f8c9aa5fd6d1567e299f4961d0e96f373ee': + * // '0x22ea134d4126804c60797e633195f8c9aa5fd6d1567e299f4961d0e96f373ee': * // { '0x34e55c1cd55f1338241b50d352f0e91c7e4ffad0e4271d64eb347589ebdfd16': { * // kind: 'struct', type: 'event', * // name: 'ka::ExComponent::ex_logic_component::Mint', - * // members: [{ * // name: 'spender', * // type: 'core::starknet::contract_address::ContractAddress', @@ -151,20 +155,6 @@ export function getAbiEvents(abi: Abi): AbiEvents { return isCairo1Abi(abi) ? getCairo1AbiEvents(abi) : getCairo0AbiEvents(abi); } -/** - * Checks if a given value is an object (Object or Array) - * @param {any} item the tested item - * @returns {boolean} - * @example - * ```typescript - * const result = events.isObject({event: "pending"}); - * // result = true - * ``` - */ -export function isObject(item: any): boolean { - return item && typeof item === 'object' && !Array.isArray(item); -} - /** * internal function to deep merge 2 event description objects */ @@ -216,7 +206,7 @@ export function parseEvents( } while (!abiEvent.name) { const hashName = recEvent.keys.shift(); - assert(!!hashName, 'Not enough data in "key" property of this event.'); + assert(!!hashName, 'Not enough data in "keys" property of this event.'); abiEvent = (abiEvent as AbiEvents)[hashName]; } // Create our final event object @@ -261,11 +251,14 @@ export function parseEvents( /** * Parse Transaction Receipt Event from UDC invoke transaction and * create DeployContractResponse compatible response with addition of the UDC Event data + * @param {InvokeTransactionReceiptResponse} txReceipt * - * @returns DeployContractResponse | UDC Event Response data + * @returns {DeployContractUDCResponse} parsed UDC event data */ -export function parseUDCEvent(txReceipt: InvokeTransactionReceiptResponse) { - if (!txReceipt.events) { +export function parseUDCEvent( + txReceipt: InvokeTransactionReceiptResponse +): DeployContractUDCResponse { + if (!txReceipt.events?.length) { throw new Error('UDC emitted event is empty'); } const event = txReceipt.events.find( diff --git a/src/utils/fetchPonyfill.ts b/src/utils/fetchPonyfill.ts index 35db800e9..02888b3a6 100644 --- a/src/utils/fetchPonyfill.ts +++ b/src/utils/fetchPonyfill.ts @@ -2,7 +2,9 @@ // @ts-ignore import makeFetchCookie from 'fetch-cookie'; import isomorphicFetch from 'isomorphic-fetch'; +import { IS_BROWSER } from './encode'; +import { isUndefined } from './typed'; -export default (typeof window !== 'undefined' && window.fetch) || // use buildin fetch in browser if available - (typeof global !== 'undefined' && makeFetchCookie(global.fetch)) || // use buildin fetch in node, react-native and service worker if available +export default (IS_BROWSER && window.fetch) || // use built-in fetch in browser if available + (!isUndefined(global) && makeFetchCookie(global.fetch)) || // use built-in fetch in node, react-native and service worker if available isomorphicFetch; // ponyfill fetch in node and browsers that don't have it diff --git a/src/utils/hash/classHash.ts b/src/utils/hash/classHash.ts index 70aae9c56..c1b92cc0a 100644 --- a/src/utils/hash/classHash.ts +++ b/src/utils/hash/classHash.ts @@ -22,7 +22,8 @@ import { starkCurve } from '../ec'; import { addHexPrefix, utf8ToArray } from '../encode'; import { parse, stringify } from '../json'; import { toHex } from '../num'; -import { encodeShortString, isString } from '../shortString'; +import { encodeShortString } from '../shortString'; +import { isString } from '../typed'; export function computePedersenHash(a: BigNumberish, b: BigNumberish): string { return starkCurve.pedersen(BigInt(a), BigInt(b)); @@ -159,7 +160,7 @@ export function computeHintedClassHash(compiledContract: LegacyCompiledContract) * // result = "0x4a5cae61fa8312b0a3d0c44658b403d3e4197be80027fd5020ffcdf0c803331" * ``` */ -export function computeLegacyContractClassHash(contract: LegacyCompiledContract | string) { +export function computeLegacyContractClassHash(contract: LegacyCompiledContract | string): string { const compiledContract = isString(contract) ? (parse(contract) as LegacyCompiledContract) : contract; @@ -242,7 +243,7 @@ export function hashByteCodeSegments(casm: CompiledSierraCasm): bigint { * Compute compiled class hash for contract (Cairo 1) * @param {CompiledSierraCasm} casm Cairo 1 compiled contract content * @returns {string} hex-string of class hash - * @example + * @example * ```typescript * const compiledCasm = json.parse(fs.readFileSync("./cairo260.casm.json").toString("ascii")); * const result = hash.computeCompiledClassHash(compiledCasm); @@ -296,7 +297,7 @@ function hashAbi(sierra: CompiledSierra) { * Compute sierra contract class hash (Cairo 1) * @param {CompiledSierra} sierra Cairo 1 Sierra contract content * @returns {string} hex-string of class hash - * @example + * @example * ```typescript * const compiledSierra = json.parse(fs.readFileSync("./cairo260.sierra.json").toString("ascii")); * const result = hash.computeSierraContractClassHash(compiledSierra); @@ -340,7 +341,7 @@ export function computeSierraContractClassHash(sierra: CompiledSierra): string { * Compute ClassHash (sierra or legacy) based on provided contract * @param {CompiledContract | string} contract Cairo 1 contract content * @returns {string} hex-string of class hash - * @example + * @example * ```typescript * const compiledSierra = json.parse(fs.readFileSync("./cairo260.sierra.json").toString("ascii")); * const result = hash.computeContractClassHash(compiledSierra); diff --git a/src/utils/hash/selector.ts b/src/utils/hash/selector.ts index 87d4dd918..b9fd1ce71 100644 --- a/src/utils/hash/selector.ts +++ b/src/utils/hash/selector.ts @@ -4,7 +4,8 @@ import { bytesToHex } from '@noble/curves/abstract/utils'; import { MASK_250 } from '../../constants'; import { BigNumberish } from '../../types'; import { addHexPrefix, removeHexPrefix, utf8ToArray } from '../encode'; -import { hexToBytes, isBigInt, isHex, isNumber, isStringWholeNumber, toHex } from '../num'; +import { hexToBytes, isHex, isStringWholeNumber, toHex } from '../num'; +import { isBigInt, isNumber } from '../typed'; /** * Calculate the hex-string Starknet Keccak hash for a given BigNumberish diff --git a/src/utils/num.ts b/src/utils/num.ts index d9a097f11..d5e3649c1 100644 --- a/src/utils/num.ts +++ b/src/utils/num.ts @@ -4,6 +4,7 @@ import { BigNumberish } from '../types'; import assert from './assert'; import { addHexPrefix, buf2hex, removeHexPrefix } from './encode'; import { MASK_31 } from '../constants'; +import { isNumber, isBigInt, isString } from './typed'; /** @deprecated prefer importing from 'types' over 'num' */ export type { BigNumberish }; @@ -44,24 +45,6 @@ export function toBigInt(value: BigNumberish): bigint { return BigInt(value); } -/** - * Test if value is bigint - * - * @param value value to test - * @returns {boolean} true if value is bigint, false otherwise - * @example - * ```typescript - * isBigInt(10n); // true - * isBigInt(BigInt('10')); // true - * isBigInt(10); // false - * isBigInt('10'); // false - * isBigInt(null); // false - * ``` - */ -export function isBigInt(value: any): value is bigint { - return typeof value === 'bigint'; -} - /** * Convert BigNumberish to hex-string * @@ -324,7 +307,7 @@ export function hexToBytes(str: string): Uint8Array { * * @param number value to be modified * @param percent integer as percent ex. 50 for 50% - * @returns {BigInt} modified value + * @returns {bigint} modified value * @example * ```typescript * addPercent(100, 50); // 150n @@ -335,49 +318,11 @@ export function hexToBytes(str: string): Uint8Array { * addPercent(200, -150); // -100n * ``` */ -export function addPercent(number: BigNumberish, percent: number) { +export function addPercent(number: BigNumberish, percent: number): bigint { const bigIntNum = BigInt(number); return bigIntNum + (bigIntNum * BigInt(percent)) / 100n; } -/** - * Check if a value is a number. - * - * @param {unknown} value - The value to check. - * @returns {boolean} Returns true if the value is a number, otherwise returns false. - * @example - * ```typescript - * const result = isNumber(123); - * // result = true - * - * const result2 = isNumber("123"); - * // result2 = false - * ``` - * @return {boolean} Returns true if the value is a number, otherwise returns false. - */ -export function isNumber(value: unknown): value is number { - return typeof value === 'number'; -} - -/** - * Checks if a given value is of boolean type. - * - * @param {unknown} value - The value to check. - * @returns {boolean} - True if the value is of boolean type, false otherwise. - * @example - * ```typescript - * const result = isBoolean(true); - * // result = true - * - * const result2 = isBoolean(false); - * // result2 = false - * ``` - * @return {boolean} - True if the value is of boolean type, false otherwise. - */ -export function isBoolean(value: unknown): value is boolean { - return typeof value === 'boolean'; -} - /** * Calculate the sha256 hash of an utf8 string, then encode the * result in an uint8Array of 4 elements. @@ -413,6 +358,6 @@ export function isBigNumberish(input: unknown): input is BigNumberish { return ( isNumber(input) || isBigInt(input) || - (typeof input === 'string' && (isHex(input) || isStringWholeNumber(input))) + (isString(input) && (isHex(input) || isStringWholeNumber(input))) ); } diff --git a/src/utils/provider.ts b/src/utils/provider.ts index 19630e4ef..5623bfc25 100644 --- a/src/utils/provider.ts +++ b/src/utils/provider.ts @@ -18,8 +18,9 @@ import { ETransactionVersion } from '../types/api'; import { isSierra } from './contract'; import { formatSpaces } from './hash'; import { parse, stringify } from './json'; -import { isBigInt, isHex, isNumber, toHex } from './num'; -import { isDecimalString, isString } from './shortString'; +import { isHex, toHex } from './num'; +import { isDecimalString } from './shortString'; +import { isBigInt, isNumber, isString } from './typed'; import { compressProgram } from './stark'; import type { GetTransactionReceiptResponse } from './transactionReceipt'; @@ -43,7 +44,7 @@ export function wait(delay: number): Promise { * Create Sierra compressed Contract Class from a given Compiled Sierra * * CompiledSierra -> SierraContractClass - * + * * @param {CompiledSierra} contract sierra code from the Cairo compiler * @returns {SierraContractClass} compressed Sierra * @example diff --git a/src/utils/responseParser/rpc.ts b/src/utils/responseParser/rpc.ts index d2f944d81..4b59d488c 100644 --- a/src/utils/responseParser/rpc.ts +++ b/src/utils/responseParser/rpc.ts @@ -17,7 +17,7 @@ import type { TransactionReceipt, } from '../../types/provider'; import { toBigInt } from '../num'; -import { isString } from '../shortString'; +import { isString } from '../typed'; import { estimateFeeToBounds, estimatedFeeToMaxFee } from '../stark'; import { ResponseParser } from './interface'; diff --git a/src/utils/shortString.ts b/src/utils/shortString.ts index 5710711f1..d37e3cf0d 100644 --- a/src/utils/shortString.ts +++ b/src/utils/shortString.ts @@ -1,6 +1,7 @@ import { TEXT_TO_FELT_MAX_LEN } from '../constants'; import { addHexPrefix, removeHexPrefix } from './encode'; import { isHex, isStringWholeNumber } from './num'; +import { isString } from './typed'; /** * Test if string contains only ASCII characters (string can be ascii text) @@ -49,20 +50,6 @@ export function isDecimalString(str: string): boolean { return /^[0-9]*$/i.test(str); } -/** - * Checks if a given value is a string. - * @param {unknown} value the value to be checked. - * @return {boolean} returns true if the value is a string, false otherwise. - * @example - * ```typescript - * const result = shortString.isString("12345"); - * // result = true - * ``` - */ -export function isString(value: unknown): value is string { - return typeof value === 'string'; -} - /** * Test if value is a pure string text, and not a hex string or number string * @param {any} val the value to test @@ -75,7 +62,7 @@ export function isString(value: unknown): value is string { * // result = false * ``` */ -export function isText(val: any) { +export function isText(val: any): boolean { return isString(val) && !isHex(val) && !isStringWholeNumber(val); } @@ -89,7 +76,7 @@ export function isText(val: any) { * // result = true * ``` */ -export const isShortText = (val: any) => isText(val) && isShortString(val); +export const isShortText = (val: any): boolean => isText(val) && isShortString(val); /** * Test if value is long text @@ -101,7 +88,7 @@ export const isShortText = (val: any) => isText(val) && isShortString(val); * // result = true * ``` */ -export const isLongText = (val: any) => isText(val) && !isShortString(val); +export const isLongText = (val: any): boolean => isText(val) && !isShortString(val); /** * Split long text (string greater than 31 characters) into short strings (string lesser or equal 31 characters) diff --git a/src/utils/stark.ts b/src/utils/stark.ts index 4add7ae6a..25f13254d 100644 --- a/src/utils/stark.ts +++ b/src/utils/stark.ts @@ -1,5 +1,3 @@ -// eslint-disable-next-line @typescript-eslint/no-unused-vars -import type { SPEC } from 'starknet-types-07'; import { getPublicKey, getStarkKey, utils } from '@scure/starknet'; import { gzip, ungzip } from 'pako'; @@ -20,10 +18,21 @@ import { addPercent, bigNumberishArrayToDecimalStringArray, bigNumberishArrayToHexadecimalStringArray, - isBigInt, toHex, } from './num'; -import { isString } from './shortString'; +import { isUndefined, isString, isBigInt } from './typed'; + +type V3Details = Required< + Pick< + UniversalDetails, + | 'tip' + | 'paymasterData' + | 'accountDeploymentData' + | 'nonceDataAvailabilityMode' + | 'feeDataAvailabilityMode' + | 'resourceBounds' + > +>; /** * Compress compiled Cairo 0 program @@ -46,8 +55,8 @@ export function compressProgram(jsonProgram: Program | string): CompressedProgra /** * Decompress compressed compiled Cairo 0 program - * @param {CompressedProgram} base64 Compressed Cairo 0 program - * @returns {Object | CompressedProgram} Parsed decompressed compiled Cairo 0 program + * @param {CompressedProgram | CompressedProgram[]} base64 Compressed Cairo 0 program + * @returns Parsed decompressed compiled Cairo 0 program * @example * ```typescript * const contractCairo0 = json.parse(fs.readFileSync("./cairo0contract.json").toString("ascii")); @@ -72,7 +81,7 @@ export function compressProgram(jsonProgram: Program | string): CompressedProgra * // ... * ``` */ -export function decompressProgram(base64: CompressedProgram) { +export function decompressProgram(base64: CompressedProgram | CompressedProgram[]) { if (Array.isArray(base64)) return base64; const decompressed = arrayBufferToString(ungzip(atobUniversal(base64))); return parse(decompressed); @@ -214,7 +223,7 @@ export function estimateFeeToBounds( }; } - if (typeof estimate.gas_consumed === 'undefined' || typeof estimate.gas_price === 'undefined') { + if (isUndefined(estimate.gas_consumed) || isUndefined(estimate.gas_price)) { throw Error('estimateFeeToBounds: estimate is undefined'); } @@ -280,7 +289,7 @@ export function toTransactionVersion( /** * Convert Transaction version to Fee version or throw an error * @param {BigNumberish} [providedVersion] 0..3 number representing the transaction version - * @returns {ETransactionVersion} the fee estimation version corresponding to the transaction version provided + * @returns {ETransactionVersion | undefined} the fee estimation version corresponding to the transaction version provided * @throws {Error} if the transaction version is unknown * @example * ```typescript @@ -288,7 +297,7 @@ export function toTransactionVersion( * // result = "0x100000000000000000000000000000002" * ``` */ -export function toFeeVersion(providedVersion?: BigNumberish) { +export function toFeeVersion(providedVersion?: BigNumberish): ETransactionVersion | undefined { if (!providedVersion) return undefined; const version = toHex(providedVersion); @@ -303,7 +312,7 @@ export function toFeeVersion(providedVersion?: BigNumberish) { /** * Return provided or default v3 tx details * @param {UniversalDetails} details details of the transaction - * @return {} an object including the V3 transaction details. + * @return {V3Details} an object including the V3 transaction details. * @example * ```typescript * const detail: UniversalDetails = { tip: 3456n }; @@ -321,7 +330,8 @@ export function toFeeVersion(providedVersion?: BigNumberish) { * // } * ``` */ -export function v3Details(details: UniversalDetails) { + +export function v3Details(details: UniversalDetails): V3Details { return { tip: details.tip || 0, paymasterData: details.paymasterData || [], diff --git a/src/utils/typed.ts b/src/utils/typed.ts new file mode 100644 index 000000000..4498cdde5 --- /dev/null +++ b/src/utils/typed.ts @@ -0,0 +1,102 @@ +/** + * Check if a value is a undefined. + * + * @param {unknown} value - The value to check. + * @returns {boolean} Returns true if the value is a undefined, otherwise returns false. + * @example + * ```typescript + * const result = isUndefined(undefined); + * // result = true + * + * const result2 = isUndefined('existing value'); + * // result2 = false + * ``` + * @return {boolean} Returns true if the value is undefined, otherwise returns false. + */ +export const isUndefined = (value: unknown): value is undefined => { + return typeof value === 'undefined' || value === undefined; +}; + +/** + * Check if a value is a number. + * + * @param {unknown} value - The value to check. + * @returns {boolean} Returns true if the value is a number, otherwise returns false. + * @example + * ```typescript + * const result = isNumber(123); + * // result = true + * + * const result2 = isNumber("123"); + * // result2 = false + * ``` + * @return {boolean} Returns true if the value is a number, otherwise returns false. + */ +export function isNumber(value: unknown): value is number { + return typeof value === 'number'; +} + +/** + * Checks if a given value is of boolean type. + * + * @param {unknown} value - The value to check. + * @returns {boolean} - True if the value is of boolean type, false otherwise. + * @example + * ```typescript + * const result = isBoolean(true); + * // result = true + * + * const result2 = isBoolean(false); + * // result2 = false + * ``` + * @return {boolean} - True if the value is of boolean type, false otherwise. + */ +export function isBoolean(value: unknown): value is boolean { + return typeof value === 'boolean'; +} + +/** + * Test if value is bigint + * + * @param value value to test + * @returns {boolean} true if value is bigint, false otherwise + * @example + * ```typescript + * isBigInt(10n); // true + * isBigInt(BigInt('10')); // true + * isBigInt(10); // false + * isBigInt('10'); // false + * isBigInt(null); // false + * ``` + */ +export function isBigInt(value: any): value is bigint { + return typeof value === 'bigint'; +} + +/** + * Checks if a given value is a string. + * @param {unknown} value the value to be checked. + * @return {boolean} returns true if the value is a string, false otherwise. + * @example + * ```typescript + * const result = shortString.isString("12345"); + * // result = true + * ``` + */ +export function isString(value: unknown): value is string { + return typeof value === 'string'; +} + +/** + * Checks if a given value is an object (Object or Array) + * @param {unknown} item the tested item + * @returns {boolean} + * @example + * ```typescript + * const result = events.isObject({event: "pending"}); + * // result = true + * ``` + */ +export function isObject(item: unknown | undefined): boolean { + return !!item && typeof item === 'object' && !Array.isArray(item); +} diff --git a/src/utils/typedData.ts b/src/utils/typedData.ts index 07c1e19f8..1d462283a 100644 --- a/src/utils/typedData.ts +++ b/src/utils/typedData.ts @@ -21,7 +21,8 @@ import { } from './hash'; import { MerkleTree } from './merkle'; import { isBigNumberish, isHex, toHex } from './num'; -import { encodeShortString, isString } from './shortString'; +import { encodeShortString } from './shortString'; +import { isBoolean, isString } from './typed'; /** @deprecated prefer importing from 'types' over 'typedData' */ export * from '../types/typedData'; @@ -435,7 +436,7 @@ export function encodeValue( } case 'bool': { if (revision === Revision.ACTIVE) { - assert(typeof data === 'boolean', `Type mismatch for ${type} ${data}`); + assert(isBoolean(data), `Type mismatch for ${type} ${data}`); } // else fall through to default return [type, getHex(data as string)]; } @@ -464,7 +465,7 @@ export function encodeData( type: string, data: T['message'], revision: Revision = Revision.LEGACY -) { +): [string[], string[]] { const targetType = types[type] ?? revisionConfiguration[revision].presetTypes[type]; const [returnTypes, values] = targetType.reduce<[string[], string[]]>( ([ts, vs], field) => { @@ -519,7 +520,7 @@ export function getStructHash( type: string, data: T['message'], revision: Revision = Revision.LEGACY -) { +): string { return revisionConfiguration[revision].hashMethod(encodeData(types, type, data, revision)[1]); } diff --git a/src/utils/url.ts b/src/utils/url.ts deleted file mode 100644 index a5d934e10..000000000 --- a/src/utils/url.ts +++ /dev/null @@ -1,79 +0,0 @@ -import urljoin from 'url-join'; - -/** - * Inspired from https://github.com/segmentio/is-url - */ - -/** - * RegExps. - * A URL must match #1 and then at least one of #2/#3. - * Use two levels of REs to avoid REDOS. - */ -const protocolAndDomainRE = /^(?:\w+:)?\/\/(\S+)$/; - -const localhostDomainRE = /^localhost[:?\d]*(?:[^:?\d]\S*)?$/; -const nonLocalhostDomainRE = /^[^\s.]+\.\S{2,}$/; - -/** - * @deprecated - * - * Loosely validate a URL `string`. - * - * @param {string} s - The URL to check for - * @return {boolean} `true` if url is valid, `false` otherwise - * @example - * ```typescript - * const s = "https://starknetjs.com/docs"; - * const result = isUrl(s); - * // result == true - */ -export function isUrl(s?: string): boolean { - if (!s) { - return false; - } - - if (typeof s !== 'string') { - return false; - } - - const match = s.match(protocolAndDomainRE); - if (!match) { - return false; - } - - const everythingAfterProtocol = match[1]; - if (!everythingAfterProtocol) { - return false; - } - - if ( - localhostDomainRE.test(everythingAfterProtocol) || - nonLocalhostDomainRE.test(everythingAfterProtocol) - ) { - return true; - } - - return false; -} - -/** - * @deprecated - * - * Builds a URL using the provided base URL, default path, and optional URL or path. - * - * @param {string} baseUrl - The base URL of the URL being built. - * @param {string} defaultPath - The default path to use if no URL or path is provided. - * @param {string} [urlOrPath] - The optional URL or path to append to the base URL. - * @return {string} The built URL. - * @example - * ```typescript - * const baseUrl = "https://starknetjs.com"; - * const defaultPath = "/"; - * const urlOrPath = "/docs"; - * const result = buildUrl(baseUrl, defaultPath, urlOrPath); - * - * result = "https://starknetjs.com/docs" - */ -export function buildUrl(baseUrl: string, defaultPath: string, urlOrPath?: string) { - return isUrl(urlOrPath) ? urlOrPath! : urljoin(baseUrl, urlOrPath ?? defaultPath); -} diff --git a/src/wallet/account.ts b/src/wallet/account.ts index 874bab45b..8a1bccffc 100644 --- a/src/wallet/account.ts +++ b/src/wallet/account.ts @@ -1,12 +1,10 @@ -import { - type AccountChangeEventHandler, - type AddStarknetChainParameters, - type NetworkChangeEventHandler, - type WatchAssetParameters, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - type SPEC, +import type { + Signature, + AccountChangeEventHandler, + AddStarknetChainParameters, + NetworkChangeEventHandler, + WatchAssetParameters, } from 'starknet-types-07'; - import { Account, AccountInterface } from '../account'; import { ProviderInterface } from '../provider'; import { @@ -82,11 +80,11 @@ export class WalletAccount extends Account implements AccountInterface { /** * WALLET EVENTS */ - public onAccountChange(callback: AccountChangeEventHandler) { + public onAccountChange(callback: AccountChangeEventHandler): void { onAccountChange(this.walletProvider, callback); } - public onNetworkChanged(callback: NetworkChangeEventHandler) { + public onNetworkChanged(callback: NetworkChangeEventHandler): void { onNetworkChanged(this.walletProvider, callback); } @@ -168,7 +166,7 @@ export class WalletAccount extends Account implements AccountInterface { }; } - override signMessage(typedData: TypedData) { + override signMessage(typedData: TypedData): Promise { return signMessage(this.walletProvider, typedData); } diff --git a/src/wallet/connect.ts b/src/wallet/connect.ts index 4a57d1181..7ec623534 100644 --- a/src/wallet/connect.ts +++ b/src/wallet/connect.ts @@ -8,8 +8,13 @@ import { type ChainId, type StarknetWindowObject, type TypedData, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - type SPEC, + type Permission, + type Address, + AddInvokeTransactionResult, + AddDeclareTransactionResult, + AccountDeploymentData, + Signature, + SpecVersion, } from 'starknet-types-07'; /** @@ -17,12 +22,13 @@ import { * @param {boolean} [silent_mode=false] false: request user interaction allowance. true: return only pre-allowed * @returns allowed accounts addresses */ -export function requestAccounts(swo: StarknetWindowObject, silent_mode = false) { +export function requestAccounts( + swo: StarknetWindowObject, + silent_mode: boolean = false +): Promise { return swo.request({ type: 'wallet_requestAccounts', - params: { - silent_mode, - }, + params: { silent_mode }, }); } @@ -30,7 +36,7 @@ export function requestAccounts(swo: StarknetWindowObject, silent_mode = false) * Request Permission for wallet account * @returns allowed accounts addresses */ -export function getPermissions(swo: StarknetWindowObject) { +export function getPermissions(swo: StarknetWindowObject): Promise { return swo.request({ type: 'wallet_getPermissions' }); } @@ -39,11 +45,11 @@ export function getPermissions(swo: StarknetWindowObject) { * @param asset WatchAssetParameters * @returns boolean */ -export function watchAsset(swo: StarknetWindowObject, asset: WatchAssetParameters) { - return swo.request({ - type: 'wallet_watchAsset', - params: asset, - }); +export function watchAsset( + swo: StarknetWindowObject, + asset: WatchAssetParameters +): Promise { + return swo.request({ type: 'wallet_watchAsset', params: asset }); } /** @@ -51,12 +57,12 @@ export function watchAsset(swo: StarknetWindowObject, asset: WatchAssetParameter * @param chain AddStarknetChainParameters * @returns boolean */ -export function addStarknetChain(swo: StarknetWindowObject, chain: AddStarknetChainParameters) { +export function addStarknetChain( + swo: StarknetWindowObject, + chain: AddStarknetChainParameters +): Promise { // TODO: This should set custom RPC endpoint ? - return swo.request({ - type: 'wallet_addStarknetChain', - params: chain, - }); + return swo.request({ type: 'wallet_addStarknetChain', params: chain }); } /** @@ -64,12 +70,10 @@ export function addStarknetChain(swo: StarknetWindowObject, chain: AddStarknetCh * @param chainId StarknetChainId * @returns boolean */ -export function switchStarknetChain(swo: StarknetWindowObject, chainId: ChainId) { +export function switchStarknetChain(swo: StarknetWindowObject, chainId: ChainId): Promise { return swo.request({ type: 'wallet_switchStarknetChain', - params: { - chainId, - }, + params: { chainId }, }); } @@ -77,7 +81,7 @@ export function switchStarknetChain(swo: StarknetWindowObject, chainId: ChainId) * Request the current chain ID from the wallet. * @returns The current Starknet chain ID. */ -export function requestChainId(swo: StarknetWindowObject) { +export function requestChainId(swo: StarknetWindowObject): Promise { return swo.request({ type: 'wallet_requestChainId' }); } @@ -85,7 +89,7 @@ export function requestChainId(swo: StarknetWindowObject) { * Get deployment data for a contract. * @returns The deployment data result. */ -export function deploymentData(swo: StarknetWindowObject) { +export function deploymentData(swo: StarknetWindowObject): Promise { return swo.request({ type: 'wallet_deploymentData' }); // TODO: test } @@ -97,11 +101,8 @@ export function deploymentData(swo: StarknetWindowObject) { export function addInvokeTransaction( swo: StarknetWindowObject, params: AddInvokeTransactionParameters -) { - return swo.request({ - type: 'wallet_addInvokeTransaction', - params, - }); +): Promise { + return swo.request({ type: 'wallet_addInvokeTransaction', params }); } /** @@ -112,11 +113,8 @@ export function addInvokeTransaction( export function addDeclareTransaction( swo: StarknetWindowObject, params: AddDeclareTransactionParameters -) { - return swo.request({ - type: 'wallet_addDeclareTransaction', - params, - }); +): Promise { + return swo.request({ type: 'wallet_addDeclareTransaction', params }); } /** @@ -125,18 +123,15 @@ export function addDeclareTransaction( * @param typedData The typed data to sign. * @returns An array of signatures as strings. */ -export function signMessage(swo: StarknetWindowObject, typedData: TypedData) { - return swo.request({ - type: 'wallet_signTypedData', - params: typedData, - }); +export function signMessage(swo: StarknetWindowObject, typedData: TypedData): Promise { + return swo.request({ type: 'wallet_signTypedData', params: typedData }); } /** * Get the list of supported specifications. * @returns An array of supported specification strings. */ -export function supportedSpecs(swo: StarknetWindowObject) { +export function supportedSpecs(swo: StarknetWindowObject): Promise { return swo.request({ type: 'wallet_supportedSpecs' }); }