From d35f154f17c926e43dd688d5b1b358d792b352a2 Mon Sep 17 00:00:00 2001 From: Iris Date: Mon, 22 Apr 2024 16:02:52 +0200 Subject: [PATCH 01/18] refactor: getStarkName & getStarkProfile for latest version of contract --- src/provider/extensions/starknetId.ts | 262 ++++++++++++++++---------- 1 file changed, 161 insertions(+), 101 deletions(-) diff --git a/src/provider/extensions/starknetId.ts b/src/provider/extensions/starknetId.ts index 1c9848ee5..61675adb9 100644 --- a/src/provider/extensions/starknetId.ts +++ b/src/provider/extensions/starknetId.ts @@ -1,4 +1,4 @@ -import { BigNumberish, StarkProfile } from '../../types'; +import { BigNumberish, RawArgsArray, StarkProfile } from '../../types'; import { CallData } from '../../utils/calldata'; import { getSelectorFromName } from '../../utils/hash'; import { decodeShortString, encodeShortString } from '../../utils/shortString'; @@ -67,13 +67,7 @@ export class StarknetId { const contract = StarknetIdContract ?? getStarknetIdContract(chainId); try { - const hexDomain = await provider.callContract({ - contractAddress: contract, - entrypoint: 'address_to_domain', - calldata: CallData.compile({ - address, - }), - }); + const hexDomain = await this.executeStarkName(provider, address as string, contract); const decimalDomain = hexDomain.map((element) => BigInt(element)).slice(1); const stringDomain = useDecoded(decimalDomain); @@ -91,6 +85,29 @@ export class StarknetId { } } + static async executeStarkName(provider: ProviderInterface, address: string, contract: string) { + try { + // Attempt the initial call with the hint parameter + return await provider.callContract({ + contractAddress: contract as string, + entrypoint: 'address_to_domain', + calldata: CallData.compile({ + address, + hint: [], + }), + }); + } catch (initialError) { + // If the initial call fails, try with the fallback calldata without the hint parameter + return await provider.callContract({ + contractAddress: contract as string, + entrypoint: 'address_to_domain', + calldata: CallData.compile({ + address, + }), + }); + } + } + static async getAddressFromStarkName( provider: ProviderInterface, name: string, @@ -136,101 +153,116 @@ export class StarknetId { const multicallAddress = StarknetIdMulticallContract ?? getStarknetIdMulticallContract(chainId); try { - const data = await provider.callContract({ - contractAddress: multicallAddress, - entrypoint: 'aggregate', - calldata: CallData.compile({ - calls: [ - { - execution: execution({}), - to: dynamicFelt(contract), - selector: dynamicFelt(getSelectorFromName('address_to_domain')), - calldata: [dynamicCallData(address)], - }, - { - execution: execution({}), - to: dynamicFelt(contract), - selector: dynamicFelt(getSelectorFromName('domain_to_id')), - calldata: [dynamicCallData(undefined, undefined, [0, 0])], - }, - { - execution: execution({}), - to: dynamicFelt(identityContract), - selector: dynamicFelt(getSelectorFromName('get_verifier_data')), - calldata: [ - dynamicCallData(undefined, [1, 0]), - dynamicCallData(encodeShortString('twitter')), - dynamicCallData(verifierContract), - dynamicCallData('0'), - ], - }, - { - execution: execution({}), - to: dynamicFelt(identityContract), - selector: dynamicFelt(getSelectorFromName('get_verifier_data')), - calldata: [ - dynamicCallData(undefined, [1, 0]), - dynamicCallData(encodeShortString('github')), - dynamicCallData(verifierContract), - dynamicCallData('0'), - ], - }, - { - execution: execution({}), - to: dynamicFelt(identityContract), - selector: dynamicFelt(getSelectorFromName('get_verifier_data')), - calldata: [ - dynamicCallData(undefined, [1, 0]), - dynamicCallData(encodeShortString('discord')), - dynamicCallData(verifierContract), - dynamicCallData('0'), - ], - }, - { - execution: execution({}), - to: dynamicFelt(identityContract), - selector: dynamicFelt(getSelectorFromName('get_verifier_data')), - calldata: [ - dynamicCallData(undefined, [1, 0]), - dynamicCallData(encodeShortString('proof_of_personhood')), - dynamicCallData(popContract), - dynamicCallData('0'), - ], - }, - // PFP - { - execution: execution({}), - to: dynamicFelt(identityContract), - selector: dynamicFelt(getSelectorFromName('get_verifier_data')), - calldata: [ - dynamicCallData(undefined, [1, 0]), - dynamicCallData(encodeShortString('nft_pp_contract')), - dynamicCallData(pfpContract), - dynamicCallData('0'), - ], - }, - { - execution: execution({}), - to: dynamicFelt(identityContract), - selector: dynamicFelt(getSelectorFromName('get_extended_verifier_data')), - calldata: [ - dynamicCallData(undefined, [1, 0]), - dynamicCallData(encodeShortString('nft_pp_id')), - dynamicCallData('2'), - dynamicCallData(pfpContract), - dynamicCallData('0'), - ], - }, - { - execution: execution(undefined, undefined, [6, 0, 0]), - to: dynamicFelt(undefined, [6, 0]), - selector: dynamicFelt(getSelectorFromName('tokenURI')), - calldata: [dynamicCallData(undefined, [7, 1]), dynamicCallData(undefined, [7, 2])], - }, - ], - }), + const initialCalldata: RawArgsArray = []; + const fallbackCalldata: RawArgsArray = []; + + initialCalldata.push({ + execution: execution({}), + to: dynamicCallData(contract), + selector: dynamicCallData(getSelectorFromName('address_to_domain')), + calldata: [dynamicCallData(address), dynamicCallData('0')], + }); + fallbackCalldata.push({ + execution: execution({}), + to: dynamicCallData(contract), + selector: dynamicFelt(getSelectorFromName('address_to_domain')), + calldata: [dynamicCallData(address)], }); + const calls = [ + { + execution: execution({}), + to: dynamicFelt(contract), + selector: dynamicFelt(getSelectorFromName('domain_to_id')), + calldata: [dynamicCallData(undefined, undefined, [0, 0])], + }, + { + execution: execution({}), + to: dynamicFelt(identityContract), + selector: dynamicFelt(getSelectorFromName('get_verifier_data')), + calldata: [ + dynamicCallData(undefined, [1, 0]), + dynamicCallData(encodeShortString('twitter')), + dynamicCallData(verifierContract), + dynamicCallData('0'), + ], + }, + { + execution: execution({}), + to: dynamicFelt(identityContract), + selector: dynamicFelt(getSelectorFromName('get_verifier_data')), + calldata: [ + dynamicCallData(undefined, [1, 0]), + dynamicCallData(encodeShortString('github')), + dynamicCallData(verifierContract), + dynamicCallData('0'), + ], + }, + { + execution: execution({}), + to: dynamicFelt(identityContract), + selector: dynamicFelt(getSelectorFromName('get_verifier_data')), + calldata: [ + dynamicCallData(undefined, [1, 0]), + dynamicCallData(encodeShortString('discord')), + dynamicCallData(verifierContract), + dynamicCallData('0'), + ], + }, + { + execution: execution({}), + to: dynamicFelt(identityContract), + selector: dynamicFelt(getSelectorFromName('get_verifier_data')), + calldata: [ + dynamicCallData(undefined, [1, 0]), + dynamicCallData(encodeShortString('proof_of_personhood')), + dynamicCallData(popContract), + dynamicCallData('0'), + ], + }, + // PFP + { + execution: execution({}), + to: dynamicFelt(identityContract), + selector: dynamicFelt(getSelectorFromName('get_verifier_data')), + calldata: [ + dynamicCallData(undefined, [1, 0]), + dynamicCallData(encodeShortString('nft_pp_contract')), + dynamicCallData(pfpContract), + dynamicCallData('0'), + ], + }, + { + execution: execution({}), + to: dynamicFelt(identityContract), + selector: dynamicFelt(getSelectorFromName('get_extended_verifier_data')), + calldata: [ + dynamicCallData(undefined, [1, 0]), + dynamicCallData(encodeShortString('nft_pp_id')), + dynamicCallData('2'), + dynamicCallData(pfpContract), + dynamicCallData('0'), + ], + }, + { + execution: execution(undefined, undefined, [6, 0, 0]), + to: dynamicFelt(undefined, [6, 0]), + selector: dynamicFelt(getSelectorFromName('tokenURI')), + calldata: [dynamicCallData(undefined, [7, 1]), dynamicCallData(undefined, [7, 2])], + }, + ]; + + initialCalldata.push(...calls); + fallbackCalldata.push(...calls); + + const data = await this.executeStarkProfile( + provider, + multicallAddress, + 'aggregate', + initialCalldata, + fallbackCalldata + ); + if (Array.isArray(data)) { // Format data const size = parseInt(data[0], 16); @@ -288,4 +320,32 @@ export class StarknetId { throw Error('Could not get user stark profile data from address'); } } + + static async executeStarkProfile( + provider: ProviderInterface, + contract: string, + functionName: string, + initialCalldata: RawArgsArray, + fallbackCalldata: RawArgsArray + ) { + try { + // Attempt the initial call with the hint parameter + return await provider.callContract({ + contractAddress: contract as string, + entrypoint: functionName, + calldata: CallData.compile({ + calls: initialCalldata, + }), + }); + } catch (initialError) { + // If the initial call fails, try with the fallback calldata without the hint parameter + return await provider.callContract({ + contractAddress: contract as string, + entrypoint: functionName, + calldata: CallData.compile({ + calls: fallbackCalldata, + }), + }); + } + } } From 23ae319e09b5fdadf3c9302e1a4a7b557666f63b Mon Sep 17 00:00:00 2001 From: Philippe ROSTAN <81040730+PhilippeR26@users.noreply.github.com> Date: Thu, 25 Apr 2024 11:29:41 +0200 Subject: [PATCH 02/18] Snjs enc dec doc (#1101) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore(release): 6.8.0 [skip ci] # [6.8.0](https://github.com/starknet-io/starknet.js/compare/v6.7.0...v6.8.0) (2024-04-23) ### Bug Fixes * starkne types 0.7 ([#1087](https://github.com/starknet-io/starknet.js/issues/1087)) ([b038c76](https://github.com/starknet-io/starknet.js/commit/b038c76fe204746f1d1023c2ad3b46c022f6edbd)) * tslib ([#1068](https://github.com/starknet-io/starknet.js/issues/1068)) ([dd7dc10](https://github.com/starknet-io/starknet.js/commit/dd7dc10c57fc3cc35298c0d584a178666e9cfed1)) * **utils:** fix block identifier ([#1076](https://github.com/starknet-io/starknet.js/issues/1076)) ([0a3499d](https://github.com/starknet-io/starknet.js/commit/0a3499d49751061ceae1a4d6023b34f402376efc)) ### Features * add getGasPrice rpc provider method ([#1056](https://github.com/starknet-io/starknet.js/issues/1056)) ([d396275](https://github.com/starknet-io/starknet.js/commit/d396275348aff9c932d2bb7466b2a55f96214e4e)) * Export function parseCalldataField() ([4d59658](https://github.com/starknet-io/starknet.js/commit/4d596582023f24522c25a1a515ee0246d2eca90a)) * rpc 0.7.1 ([#1071](https://github.com/starknet-io/starknet.js/issues/1071)) ([11dc600](https://github.com/starknet-io/starknet.js/commit/11dc6003c74b6b6d0408b3f5894b5b6739d4bfba)) * docs: add paragrapher for encode decode tool * Update CHANGELOG.md --------- Co-authored-by: Toni Tabak Co-authored-by: semantic-release-bot Co-authored-by: Ivan Pavičić --- CHANGELOG.md | 14 ++++++++++++++ package-lock.json | 4 ++-- package.json | 2 +- www/docs/guides/define_call_message.md | 10 ++++++++++ www/docs/guides/pictures/encodeFn2.png | Bin 0 -> 170975 bytes 5 files changed, 27 insertions(+), 3 deletions(-) create mode 100644 www/docs/guides/pictures/encodeFn2.png diff --git a/CHANGELOG.md b/CHANGELOG.md index fb1b67803..0ab41b55b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,17 @@ +# [6.8.0](https://github.com/starknet-io/starknet.js/compare/v6.7.0...v6.8.0) (2024-04-23) + +### Bug Fixes + +- starknet types 0.7 ([#1087](https://github.com/starknet-io/starknet.js/issues/1087)) ([b038c76](https://github.com/starknet-io/starknet.js/commit/b038c76fe204746f1d1023c2ad3b46c022f6edbd)) +- tslib ([#1068](https://github.com/starknet-io/starknet.js/issues/1068)) ([dd7dc10](https://github.com/starknet-io/starknet.js/commit/dd7dc10c57fc3cc35298c0d584a178666e9cfed1)) +- **utils:** fix block identifier ([#1076](https://github.com/starknet-io/starknet.js/issues/1076)) ([0a3499d](https://github.com/starknet-io/starknet.js/commit/0a3499d49751061ceae1a4d6023b34f402376efc)) + +### Features + +- add getGasPrice rpc provider method ([#1056](https://github.com/starknet-io/starknet.js/issues/1056)) ([d396275](https://github.com/starknet-io/starknet.js/commit/d396275348aff9c932d2bb7466b2a55f96214e4e)) +- Export function parseCalldataField() ([4d59658](https://github.com/starknet-io/starknet.js/commit/4d596582023f24522c25a1a515ee0246d2eca90a)) +- rpc 0.7.1 ([#1071](https://github.com/starknet-io/starknet.js/issues/1071)) ([11dc600](https://github.com/starknet-io/starknet.js/commit/11dc6003c74b6b6d0408b3f5894b5b6739d4bfba)) + # [6.7.0](https://github.com/starknet-io/starknet.js/compare/v6.6.6...v6.7.0) (2024-04-03) ### Features diff --git a/package-lock.json b/package-lock.json index b99f26fad..17aa7a8b3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "starknet", - "version": "6.7.0", + "version": "6.8.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "starknet", - "version": "6.7.0", + "version": "6.8.0", "license": "MIT", "dependencies": { "@noble/curves": "~1.4.0", diff --git a/package.json b/package.json index 4ed042dda..f3f8b697e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "starknet", - "version": "6.7.0", + "version": "6.8.0", "description": "JavaScript library for Starknet", "main": "dist/index.js", "module": "dist/index.mjs", diff --git a/www/docs/guides/define_call_message.md b/www/docs/guides/define_call_message.md index 8aa2965cd..990494c39 100644 --- a/www/docs/guides/define_call_message.md +++ b/www/docs/guides/define_call_message.md @@ -603,3 +603,13 @@ The result will be an object, with 2 strings: ```typescript { name: "Organic", description: "The best way to read a long string!!!" } ``` + +## Tool to learn how to encode/decode + +A DAPP has been created to learn how to encode/decode with Starknet.js : **Startnet-encode-decode**. +It's also a convenient tool for the exploration of any contract ABI. +![](./pictures/encodeFn2.png) + +Follow these links : +DAPP : https://starknet-encode-decode.vercel.app/ +Tuto : https://github.com/PhilippeR26/starknet-encode-decode/blob/main/tuto.md diff --git a/www/docs/guides/pictures/encodeFn2.png b/www/docs/guides/pictures/encodeFn2.png new file mode 100644 index 0000000000000000000000000000000000000000..0135cf1235cc1186166db005e15016a3614b6fce GIT binary patch literal 170975 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
{7Q_I3l8IMi$`KR1V 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∓2|KET2Bx;fB(>Q>^+KwwD1xIh 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 literal 0 HcmV?d00001 From ae5dcf90cba070fed57d5d649088d5435806b3f3 Mon Sep 17 00:00:00 2001 From: Emmanuel A Akalo <124416278+NueloSE@users.noreply.github.com> Date: Fri, 26 Apr 2024 12:22:09 +0100 Subject: [PATCH 03/18] chore: add examples to JsDoc for num.ts file (#1100) * chore: add examples to JsDoc for num.ts file * chores: implement requested update changes examples to JsDoc for num.ts file * chore: change @return to @returns for JsDoc examples in num.ts file --------- Co-authored-by: Toni Tabak Co-authored-by: semantic-release-bot --- CHANGELOG.md | 2 + src/utils/num.ts | 175 ++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 167 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ab41b55b..5ca08d34a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,9 @@ ### Bug Fixes + - starknet types 0.7 ([#1087](https://github.com/starknet-io/starknet.js/issues/1087)) ([b038c76](https://github.com/starknet-io/starknet.js/commit/b038c76fe204746f1d1023c2ad3b46c022f6edbd)) + - tslib ([#1068](https://github.com/starknet-io/starknet.js/issues/1068)) ([dd7dc10](https://github.com/starknet-io/starknet.js/commit/dd7dc10c57fc3cc35298c0d584a178666e9cfed1)) - **utils:** fix block identifier ([#1076](https://github.com/starknet-io/starknet.js/issues/1076)) ([0a3499d](https://github.com/starknet-io/starknet.js/commit/0a3499d49751061ceae1a4d6023b34f402376efc)) diff --git a/src/utils/num.ts b/src/utils/num.ts index 544cb3bab..8e3f471a0 100644 --- a/src/utils/num.ts +++ b/src/utils/num.ts @@ -10,6 +10,17 @@ export type { BigNumberish }; /** * Test if string is hex-string * @param hex hex-string + * @returns {boolean} True if the input string is a hexadecimal string, false otherwise + * @example + * ```typescript + * const hexString1 = "0x2fd23d9182193775423497fc0c472e156c57c69e4089a1967fb288a2d84e914"; + * const result1 = isHex(hexString1); + * // result1 = true + * + * const hexString2 = "2fd23d9182193775423497fc0c472e156c57c69e4089a1967fb288a2d84e914"; + * const result2 = isHex(hexString2); + * // result2 = false + * ``` */ export function isHex(hex: string): boolean { return /^0x[0-9a-f]*$/i.test(hex); @@ -17,6 +28,14 @@ export function isHex(hex: string): boolean { /** * Convert BigNumberish to bigint + * @param value BigNumberish value to convert to bigint + * @returns {bigint} Converted bigint value + * @example + * ```typescript + * const bigNumberishValue1: BigNumberish = 1234567890; + * const result1 = toBigInt(bigNumberishValue1); + * // result1 = 1234567890n + * ``` */ export function toBigInt(value: BigNumberish): bigint { return BigInt(value); @@ -24,6 +43,14 @@ export function toBigInt(value: BigNumberish): bigint { /** * Test if value is bigint + * @param value Value to test + * @returns {boolean} True if the value is a bigint, false otherwise + * @example + * ```typescript + * const bigIntValue1: bigint = 1234567890n; + * const result1 = isBigint(bigIntValue1); + * // result1 = true + * ``` */ export function isBigInt(value: any): value is bigint { return typeof value === 'bigint'; @@ -31,14 +58,27 @@ export function isBigInt(value: any): value is bigint { /** * Convert BigNumberish to hex-string + * @param number BigNumberish value to convert to hex-string * @returns format: hex-string + * @example + * ```typescript + * const bigNumberishValue1: BigNumberish = 1234567890; + * const result1 = toHex(bigNumberishValue1); + * // result = "0x499602d2" + * ``` */ export function toHex(number: BigNumberish): string { return addHexPrefix(toBigInt(number).toString(16)); } /** - * Alias of ToHex + * Alias of toHex + * @returns format: hex-string + * @example + * ```typescript + * const result = toHexString(123); + * // result = "0x7b" + * ``` */ export const toHexString = toHex; @@ -49,7 +89,14 @@ export const toHexString = toHex; * * A storage key is represented as up to 62 hex digits, 3 bits, and 5 leading zeroes: * `0x0 + [0-7] + 62 hex = 0x + 64 hex` + * @param number BigNumberish value to convert to storage-key-string * @returns format: storage-key-string + * @example + * ```typescript + * const bigNumberishValue1: BigNumberish = 1234567890; + * const result = toStorageKey(bigNumberishValue1); + * // result = "0x000000000000000000000000000000000000000000000000000000000499602d2" + * ``` */ export function toStorageKey(number: BigNumberish): string { const res = addHexPrefix(toBigInt(number).toString(16).padStart(64, '0')); @@ -60,23 +107,48 @@ export function toStorageKey(number: BigNumberish): string { * Convert hexadecimal string to decimal string * @param hex hex-string * @returns format: decimal string + * @example + * ```typescript + * const hexString = "0x2fd23d9182193775423497fc0c472e156c57c69e4089a1967fb288a2d84e914"; + * const result = hexToDecimalString(hexString); + * // result = "32507161997631881240494522159444018041090212371621416251492750949915267618964"; + * ``` */ export function hexToDecimalString(hex: string): string { return BigInt(addHexPrefix(hex)).toString(10); } /** - * Remove hex string leading zero and lowercase it - * @example '0x01A...' -> '0x1a..' + * Remove hex string leading zero and lowercase it; * @param hex hex-string * @returns format: hex-string + * @example + * ```typescript + * const hexString = '0x01A2F3' + * const result = cleanHex(hexString); + * // result = '0x1a2f3' + * ``` */ export const cleanHex = (hex: string) => hex.toLowerCase().replace(/^(0x)0+/, '$1'); /** - * Asserts input is equal to or greater then lowerBound and lower then upperBound. + * Asserts input is equal to or greater than lowerBound and lower than upperBound. * * The `inputName` parameter is used in the assertion message. + * @param input Value to check + * @param lowerBound Lower bound value + * @param upperBound Upper bound value + * @param inputName Name of the input for error message + * @Throws Error if input is out of range + * @example + * ```typescript + * const input1:BigNumberish = 10; + * assertInRange(input1, 5, 20, 'value') + * + * const input2: BigNumberish = 25; + * assertInRange(input2, 5, 20, 'value'); + * // Throws Error: Message not signable, invalid value length. + * ``` */ export function assertInRange( input: BigNumberish, @@ -97,7 +169,14 @@ export function assertInRange( /** * Convert BigNumberish array to decimal string array + * @param rawCalldata Array of BigNumberish values * @returns format: decimal string array + * @example + * ```typescript + * const bigNumberishArray: BigNumberish[] = [123, "456", 789n]; + * const result = bigNumberishArrayToDecimalStringArray(bigNumberishArray); + * // result = ["123", "456", "789"] + * ``` */ export function bigNumberishArrayToDecimalStringArray(rawCalldata: BigNumberish[]): string[] { return rawCalldata.map((x) => toBigInt(x).toString(10)); @@ -105,7 +184,14 @@ export function bigNumberishArrayToDecimalStringArray(rawCalldata: BigNumberish[ /** * Convert BigNumberish array to hexadecimal string array + * @param rawCalldata Array of BigNumberish values * @returns format: hex-string array + * @example + * ```typescript + * const bigNumberishArray: BigNumberish[] = [123, "456", 789n]; + * const result = bigNumberishArrayToHexadecimalStringArray(bigNumberishArray); + * // result = ["0x7b", "0x1c8", "0x315"] + * ``` */ export function bigNumberishArrayToHexadecimalStringArray(rawCalldata: BigNumberish[]): string[] { return rawCalldata.map((x) => toHex(x)); @@ -113,12 +199,28 @@ export function bigNumberishArrayToHexadecimalStringArray(rawCalldata: BigNumber /** * Test if string is whole number (0, 1, 2, 3...) + * @param value The string to be tested. + * @returns {boolean} Returns true if the value is a number, otherwise returns false. + * @example + * ```typescript + * const result = isStringWholeNumber("123"); + * // result = true + * ``` */ export const isStringWholeNumber = (value: string) => /^\d+$/.test(value); /** * Convert string to decimal string + * @param value The string to be converted. * @returns format: decimal string + * @example + * ```typescript + * const result = getDecimalString("0x1a"); + * // result = "26" + * + * const result2 = getDecimalString("Hello"); + * // Throws Error: "Hello need to be hex-string or whole-number-string" + * ``` */ export function getDecimalString(value: string) { if (isHex(value)) { @@ -132,7 +234,16 @@ export function getDecimalString(value: string) { /** * Convert string to hexadecimal string + * @param value The string to be converted. * @returns format: hex-string + * @example + * ```typescript + * const result = getHexString("123"); + * // result = "0x7b" + * + * const result2 = getHexString("Hello"); + * // Throws Error: Hello need to be hex-string or whole-number-string + * ``` */ export function getHexString(value: string) { if (isHex(value)) { @@ -146,7 +257,14 @@ export function getHexString(value: string) { /** * Convert string array to hex-string array + * @param value The string array to be converted. * @returns format: hex-string array + * @example + * ```typescript + * const stringArray: string[] = ["123", "456", "789"]; + * const result = getHexStringArray(stringArray); + * // result = ["0x7b", "0x1c8", "0x315"] + * ``` */ export function getHexStringArray(value: Array) { return value.map((el) => getHexString(el)); @@ -154,12 +272,28 @@ export function getHexStringArray(value: Array) { /** * Convert boolean to "0" or "1" + * @param value The boolean value to be converted. + * @returns {boolean} Returns true if the value is a number, otherwise returns false. + * @example + * ```typescript + * const result = toCairoBool(true); + * // result ="1" + * + * const result2 = toCairoBool(false); + * // result2 = "0" + * ``` */ export const toCairoBool = (value: boolean): string => (+value).toString(); /** * Convert hex-string to an array of Bytes (Uint8Array) - * @param value hex-string + * @param value The hex-string to be converted. + * @returns The array of bytes (Uint8Array) corresponding to the hex-string. + * @example + * ```typescript + * const result = hexToBytes("0x123456"); + * // result = Uint8Array [ 18, 52, 86 ] + * ``` */ export function hexToBytes(value: string): Uint8Array { if (!isHex(value)) throw new Error(`${value} need to be a hex-string`); @@ -172,10 +306,15 @@ export function hexToBytes(value: string): Uint8Array { } /** - * - * @param number value to be increased + * Increase a give number by specified percentage + * @param number The value to be increased (BigInt or number). * @param percent integer as percent ex. 50 for 50% - * @returns increased value + * @returns The increased value as a BigInt. + * @example + * ```typescript + * const result = addPercent(100, 50); + * // result = 150n + * ``` */ export function addPercent(number: BigNumberish, percent: number) { const bigIntNum = BigInt(number); @@ -186,7 +325,15 @@ export function addPercent(number: BigNumberish, percent: number) { * Check if a value is a number. * * @param {unknown} value - The value to check. - * @return {boolean} Returns true if the value is a number, otherwise returns false. + * @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 + * ``` */ export function isNumber(value: unknown): value is number { return typeof value === 'number'; @@ -196,7 +343,15 @@ export function isNumber(value: unknown): value is number { * Checks if a given value is of boolean type. * * @param {unknown} value - The value to check. - * @return {boolean} - True if the value is of boolean type, false otherwise. + * @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 + * ``` */ export function isBoolean(value: unknown): value is boolean { return typeof value === 'boolean'; From f77bae58aea45850a1d95247d29d71500eba3b68 Mon Sep 17 00:00:00 2001 From: Emmanuel A Akalo <124416278+NueloSE@users.noreply.github.com> Date: Mon, 29 Apr 2024 11:25:49 +0100 Subject: [PATCH 04/18] chore: Add JsDoc comments and examples for merkle.ts file (#1107) --- src/utils/merkle.ts | 51 +++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 47 insertions(+), 4 deletions(-) diff --git a/src/utils/merkle.ts b/src/utils/merkle.ts index 71c850dee..daa28ae24 100644 --- a/src/utils/merkle.ts +++ b/src/utils/merkle.ts @@ -23,6 +23,17 @@ export class MerkleTree { * Create Merkle tree * @param leaves hex-string array * @returns format: hex-string; Merkle tree root + * @example + * ```typescript + * const leaves: string[] = [ + * "0x2fd23d9182193775423497fc0c472e156c57c69e4089a1967fb288a2d84e914", + * "0x1234567890123456789012345678901234567890123456789012345678901234", + * "0xabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdef" + * ]; + * const merkleTree = new MerkleTree(); + * const root = merkleTree.build(leaves); + * // root = "0x71d6f4b2f7a5aa46daa76f2e01ab44b0e7581a82b40cb1289b89b2353fc1b9e0" + * ``` */ private build(leaves: string[]): string { if (leaves.length === 1) { @@ -44,7 +55,18 @@ export class MerkleTree { /** * Create hash from ordered a and b, Pedersen hash default + * @param a BigNumberish value to be hashed + * @param b BigNumberish value to be hashed + * @param hashMethod Function to compute hash, default is Pedersen hash * @returns format: hex-string + * @example + * ```typescript + * const hash = MerkleTree.hash( + * "0x2fd23d9182193775423497fc0c472e156c57c69e4089a1967fb288a2d84e914", + * "0x1234567890123456789012345678901234567890123456789012345678901234", + * ); + * // hash = "0x71d6f4b2f7a5aa46daa76f2e01ab44b0e7581a82b40cb1289b89b2353fc1b9e0" + * ``` */ static hash( a: BigNumberish, @@ -57,10 +79,19 @@ export class MerkleTree { /** * Return path to leaf - * @param leaf hex-string - * @param branch hex-string array - * @param hashPath hex-string array - * @returns format: hex-string array + * @param leaf hex-string representing the leaf + * @param branch hex-string array representing the branch + * @param hashPath hex-string array representing the hash path + * @returns format: hex-string array representing the path to the leaf + * @example + * ```typescript + * const merkleTree = new MerkleTree(); + * const proof = merkleTree.getProof("0x2fd23d9182193775423497fc0c472e156c57c69e4089a1967fb288a2d84e914"); + * // proof = [ + * // "0x1234567890123456789012345678901234567890123456789012345678901234", + * // "0xabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdef" + * // ] + * ``` */ public getProof(leaf: string, branch = this.leaves, hashPath: string[] = []): string[] { const index = branch.indexOf(leaf); @@ -92,6 +123,18 @@ export class MerkleTree { * @param leaf hex-string * @param path hex-string array * @param hashMethod hash method override, Pedersen default + * @returns {boolean} True if the Merkle tree path is valid, false otherwise + * @example + * ```typescript + * const root = "0x71d6f4b2f7a5aa46daa76f2e01ab44b0e7581a82b40cb1289b89b2353fc1b9e0"; + * const leaf = "0x2fd23d9182193775423497fc0c472e156c57c69e4089a1967fb288a2d84e914"; + * const path = [ + * "0x1234567890123456789012345678901234567890123456789012345678901234", + * "0xabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdef" + * ]; + * const isValid = proofMerklePath(root, leaf, path); + * // isValid = true + * ``` */ export function proofMerklePath( root: string, From 0b25abae10381b2bfb4d784d5d19445a7c6e8511 Mon Sep 17 00:00:00 2001 From: Emmanuel A Akalo <124416278+NueloSE@users.noreply.github.com> Date: Mon, 29 Apr 2024 11:27:10 +0100 Subject: [PATCH 05/18] chore: Add JsDoc comments and examples for selector.ts file (#1106) --- src/utils/selector.ts | 41 +++++++++++++++++++++++++++++++++++------ 1 file changed, 35 insertions(+), 6 deletions(-) diff --git a/src/utils/selector.ts b/src/utils/selector.ts index 1a137a62b..c54029962 100644 --- a/src/utils/selector.ts +++ b/src/utils/selector.ts @@ -7,9 +7,13 @@ import { hexToBytes, isHex, isStringWholeNumber, toHex, toHexString } from './nu /** * Calculate hex-string keccak hash for a given BigNumberish - * - * BigNumberish -> hex-string keccak hash - * @returns format: hex-string + * @param value The value you want to get the keccak hash from. + * @returns format: hex-string keccak hash + * @example + * ```typescript + * const hash: string = keccakBn(123456789); + * // hash = "0x6c1eebcad9e5b7e0f13855f5e4b56e85ad24544b" + * ``` */ export function keccakBn(value: BigNumberish): string { const hexWithoutPrefix = removeHexPrefix(toHex(BigInt(value))); @@ -19,9 +23,14 @@ export function keccakBn(value: BigNumberish): string { /** * Calculate hex-string keccak hash for a given string - * + * @param str The value you want to get the keccak hash from. * String -> hex-string keccak hash * @returns format: hex-string + * @example + * ```typescript + * const hash: string = keccakHex("Hello, world!"); + * // hash = "0x3ad6fcbda8fc87e9fb42f7f0cd36d27da079ffafc6f0dcf36b6a6140e0f67c84" + * ``` */ function keccakHex(str: string): string { return addHexPrefix(keccak(utf8ToArray(str)).toString(16)); @@ -35,6 +44,10 @@ function keccakHex(str: string): string { * [Reference](https://github.com/starkware-libs/cairo-lang/blob/master/src/starkware/starknet/public/abi.py#L17-L22) * @param str the value you want to get the keccak hash from * @returns starknet keccak hash as BigInt + * @example + * ```typescript + * const hash: bigint = starknetKeccak("Hello, world!"); + * // hash = "38418923196344919485056939258679159916n" */ export function starknetKeccak(str: string): bigint { const hash = BigInt(keccakHex(str)); @@ -48,8 +61,13 @@ export function starknetKeccak(str: string): bigint { * Abi-function-name -> hex-string selector * * [Reference](https://github.com/starkware-libs/cairo-lang/blob/master/src/starkware/starknet/public/abi.py#L25-L26) - * @param funcName ascii-string of 'abi function name' - * @returns format: hex-string; selector for 'abi function name' + * @param funcName ascii-string of 'abi function name'. + * @returns format: hex-string; selector for 'abi function name'. + * @example + * ```typescript + * const selector: string = getSelectorFromName("myFunction"); + * // selector = "0x7e44baf0" + * ``` */ export function getSelectorFromName(funcName: string) { // sometimes BigInteger pads the hex string with zeros, which is not allowed in the starknet api @@ -63,6 +81,17 @@ export function getSelectorFromName(funcName: string) { * * @param value hex-string | dec-string | ascii-string * @returns format: hex-string + * @example + * ```typescript + * const selector: string = getSelector("myFunction"); + * // selector = "0x7e44bafo" + * + * const selector1: string = getSelector("0x123abc"); + * // selector1 = "0x123abc" + * + * const selector2: string = getSelector("123456"); + * // selector2 = "0x1e240" + * ``` */ export function getSelector(value: string) { if (isHex(value)) { From 0ab0de70142b07186502c6457933858ccec6e23e Mon Sep 17 00:00:00 2001 From: Peterson_dt <168048103+petersdt@users.noreply.github.com> Date: Mon, 29 Apr 2024 11:29:00 +0100 Subject: [PATCH 06/18] chore: add examples to JsDoc for transaction.ts file (#1105) --- src/utils/transaction.ts | 179 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 173 insertions(+), 6 deletions(-) diff --git a/src/utils/transaction.ts b/src/utils/transaction.ts index 308290223..8ed2ad0fa 100644 --- a/src/utils/transaction.ts +++ b/src/utils/transaction.ts @@ -19,6 +19,33 @@ import { randomAddress } from './stark'; /** * Transforms a list of Calls, each with their own calldata, into * two arrays: one with the entry points, and one with the concatenated calldata + * @param calls - The list of calls to transform. + * @returns An object containin two arrays: callArray and calldata. + * @example + * ```typescript + * const calls: Call[] = [ + * { + * contractAddress: "0x1234567890123456789012345678901234567890", + * entrypoint: "functionName", + * calldata: [1,2,3] + * }, + * { + * contractAddress: "0x0987654321098765432109876543210987654321", + * entrypoint: "anotherFunction", + * calldata: [4,5,6] + * } + * ]; + * const transformedData = transformCallsToMulticallArrays(calls); + * console.log(transformedData); + * // { + * // callArray: [ + * // { to: "0x1234567890123456789012345678901234567890", selector: "1234567890", + * // data_offset: "0", data_len: "3" }, + * // { to: "0x0987654321098765432109876543210987654321", selector: "1234567890", + * // data_offset: "0987654321", data_offset: "3", data_len: "3"} + * // ], calldata: [1, 2, 3, 4, 5, 6] + * // } + * ``` */ export const transformCallsToMulticallArrays = (calls: Call[]) => { const callArray: ParsedStruct[] = []; @@ -41,6 +68,26 @@ export const transformCallsToMulticallArrays = (calls: Call[]) => { /** * Transforms a list of calls into the Cairo 0 `__execute__` calldata. + * @param calls - The list of calls to transform. + * @returns The Cairo 0 `__execute__` calldata. + * @example + * ```typescript + * const calls: Call[] = [ + * { + * contractAddress: "0x1234567890123456789012345678901234567890", + * entrypoint: "functionName", + * calldata: [1, 2, 3] + * }, + * { + * contractAddress: "0x0987654321098765432109876543210987654321", + * entrypoint: "anotherFunction", + * calldata: [4, 5, 6] + * } + * ]; + * const executeCalldata = fromCallsToExecuteCalldata(calls); + * console.log(executeCalldata); + * // [1234567890, 0987654321, 0, 6, 1, 2, 3, 4, 5, 6] + * ``` */ export const fromCallsToExecuteCalldata = (calls: Call[]) => { const { callArray, calldata } = transformCallsToMulticallArrays(calls); @@ -50,8 +97,29 @@ export const fromCallsToExecuteCalldata = (calls: Call[]) => { /** * Transforms a list of calls into the Cairo 0 `__execute__` calldata including nonce. - * + * @param calls - The list of calls to transform. + * @param nonce - The nonce to include in the calldata. + * @returns The Cairo 0 `__execute__` calldata including the nonce. * @deprecated + * @example + * ```typescript + * const calls: Call[] = [ + * { + * contractAddress: "0x1234567890123456789012345678901234567890", + * entrypoint: "functionName", + * calldata: [1, 2, 3] + * }, + * { + * contractAddress: "0x0987654321098765432109876543210987654321", + * entrypoint: "anotherFunction", + * calldata: [4, 5, 6] + * } + * ]; + * const nonce = 123; + * const result = fromCallsToExecuteCalldataWithNonce(calls, nonce); + * console.log(result); + * // [1234567890, 0987654321, 0, 6, 1, 2, 3, 4, 5, 6, "123"] + * ``` */ export const fromCallsToExecuteCalldataWithNonce = (calls: Call[], nonce: BigNumberish) => { return [...fromCallsToExecuteCalldata(calls), toBigInt(nonce).toString()] as Calldata; @@ -59,8 +127,38 @@ export const fromCallsToExecuteCalldataWithNonce = (calls: Call[], nonce: BigNum /** * Format Data inside Calls - * + * @param calls - The list of calls to transform. * @deprecated Not required for getting execute Calldata + * @returns An array of formatted call data. + * @example + * ```typescript + * const calls: Call[] = [ + * { + * contractAddress: "0x1234567890123456789012345678901234567890" + * entrypoint: "functionName", + * calldata: [1, 2, 3] + * }, + * { + * contractAddress: "0x0987654321098765432109876543210987654321", + * entrypoint: "anotherFunction", + * calldata: [4, 5, 6] + * } + * ]; + * const result = transformCallsToMulticallArrays_cairo1(calls); + * console.log(formattedCalls); + * // [ + * // { + * // to: "1234567890123456789012345678901234567890", + * // selector: "1234567890", + * // calldata: [1, 2, 3] + * // }, + * // { + * // to: "0987654321098765432109876543210987654321", + * // selector: "0987654321", + * // calldata: [4, 5, 6] + * // } + * // ] + * ``` */ export const transformCallsToMulticallArrays_cairo1 = (calls: Call[]) => { const callArray = calls.map((call) => ({ @@ -73,6 +171,26 @@ export const transformCallsToMulticallArrays_cairo1 = (calls: Call[]) => { /** * Transforms a list of calls into the Cairo 1 `__execute__` calldata. + * @param calls - The list of calls to transform. + * @returns The Cairo 1 `__execute__` calldata. + * @example + * ```typescript + * const calls: Call[] = [ + * { + * contractAddress: "0x1234567890123456789012345678901234567890", + * entrypoint: "functionName", + * calldata: [1, 2, 3] + * }, + * { + * contractAddress: "0x0987654321098765432109876543210987654321", + * entrypoint: "anotherFunction", + * calldata: [4, 5, 6] + * } + * ]; + * const result = fromCallsToExecuteCalldata_cairo1(calls); + * console.log(result); + * // [1234567890, 0987654321, 0, 6, 1, 2, 3, 4, 5, 6] + * ``` */ export const fromCallsToExecuteCalldata_cairo1 = (calls: Call[]) => { // ensure property order @@ -89,7 +207,28 @@ export const fromCallsToExecuteCalldata_cairo1 = (calls: Call[]) => { }; /** - * Create `__execute__` Calldata from Calls based on Cairo versions + * Create `__execute__` Calldata from Calls based on Cairo versions. + * @param calls - The list of calls to transform. + * @param cairoVersion - The Cairo version. + * @returns The `__execute__` calldata. + * @example + * ```typescript + * const calls: Call[] = [ + * { + * contractAddress: "0x1234567890123456789012345678901234567890", + * entrypoint: "functionName", + * calldata: [1, 2, 3] + * }, + * { + * contractAddress: "0x0987654321098765432109876543210987654321", + * entrypoint: "anotherFunction", + * calldata: [4, 5, 6] + * } + * ]; + * const result = getExecuteCalldata(calls, '1'); + * console.log(result); + * // [1234567890, 0987654321, 0, 6, 1, 2, 3, 4, 5, 6] + * ``` */ export const getExecuteCalldata = (calls: Call[], cairoVersion: CairoVersion = '0') => { if (cairoVersion === '1') { @@ -101,10 +240,30 @@ export const getExecuteCalldata = (calls: Call[], cairoVersion: CairoVersion = ' /** * Builds a UDCCall object. * - * @param {UniversalDeployerContractPayload | UniversalDeployerContractPayload[]} payload - The payload data for the UDCCall. Can be a single payload object or an array of payload objects - *. + * @param {UniversalDeployerContractPayload | UniversalDeployerContractPayload[]} payload - The payload data for the UDCCall. Can be a single payload object or an array of payload objects. * @param {string} address - The address to be used in the UDCCall. * @returns {{ calls: Array, addresses: Array }} - The UDCCall object containing an array of calls and an array of addresses. + * @example + * ```typescript + * const payload: UniversalDeployerContractPayload = { + * classHash: "0x1234567890123456789012345678901234567890", + * salt: "0x0987654321098765432109876543210987654321", + * unique:true, + * constructCalldata: [1, 2, 3] + * }; + * const address = "0xABCDEF1234567890ABCDEF1234567890ABCDEF12", + * const udcall = buildUDCCall(payload, address); + * console.log(udccall); + * // { + * // calls: [ + * { + * // contractAddress: "0xABCDEF1234567890ABCDEF1234567890ABCDEF12", + * // entrypoint: "functionName", + * // calldata: [classHash, salt, true, 3, 1, 2, 3] + * // }], + * // addresses: ["0x6fD084B56a7EDc5C06B3eB40f97Ae5A0C707A865"] + * // } + * ``` */ export function buildUDCCall( payload: UniversalDeployerContractPayload | UniversalDeployerContractPayload[], @@ -149,7 +308,15 @@ export function buildUDCCall( } /** - * Return transaction versions based on version type, default version type is 'transaction' + * Return transaction versions based on version type, default version type is 'transaction'. + * @param versionType - The type of version ("fee" or "transaction"). + * @returns An object containing transaction versions. + * @example + * ```typescript + * const transactionVersions = getVersionsByType('fee'); + * console.log(transactionVersions); + * // { v1: 1, v2: 2, v3: 3 } + * ``` */ export function getVersionsByType(versionType?: 'fee' | 'transaction') { return versionType === 'fee' From 59eb01e451cf64dfdacd6d34b2a709e0a1029f15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Pavi=C4=8Di=C4=87?= Date: Mon, 29 Apr 2024 12:36:30 +0200 Subject: [PATCH 07/18] Revert "chore: add examples to JsDoc for transaction.ts file (#1105)" (#1108) This reverts commit 0ab0de70142b07186502c6457933858ccec6e23e. --- src/utils/transaction.ts | 179 ++------------------------------------- 1 file changed, 6 insertions(+), 173 deletions(-) diff --git a/src/utils/transaction.ts b/src/utils/transaction.ts index 8ed2ad0fa..308290223 100644 --- a/src/utils/transaction.ts +++ b/src/utils/transaction.ts @@ -19,33 +19,6 @@ import { randomAddress } from './stark'; /** * Transforms a list of Calls, each with their own calldata, into * two arrays: one with the entry points, and one with the concatenated calldata - * @param calls - The list of calls to transform. - * @returns An object containin two arrays: callArray and calldata. - * @example - * ```typescript - * const calls: Call[] = [ - * { - * contractAddress: "0x1234567890123456789012345678901234567890", - * entrypoint: "functionName", - * calldata: [1,2,3] - * }, - * { - * contractAddress: "0x0987654321098765432109876543210987654321", - * entrypoint: "anotherFunction", - * calldata: [4,5,6] - * } - * ]; - * const transformedData = transformCallsToMulticallArrays(calls); - * console.log(transformedData); - * // { - * // callArray: [ - * // { to: "0x1234567890123456789012345678901234567890", selector: "1234567890", - * // data_offset: "0", data_len: "3" }, - * // { to: "0x0987654321098765432109876543210987654321", selector: "1234567890", - * // data_offset: "0987654321", data_offset: "3", data_len: "3"} - * // ], calldata: [1, 2, 3, 4, 5, 6] - * // } - * ``` */ export const transformCallsToMulticallArrays = (calls: Call[]) => { const callArray: ParsedStruct[] = []; @@ -68,26 +41,6 @@ export const transformCallsToMulticallArrays = (calls: Call[]) => { /** * Transforms a list of calls into the Cairo 0 `__execute__` calldata. - * @param calls - The list of calls to transform. - * @returns The Cairo 0 `__execute__` calldata. - * @example - * ```typescript - * const calls: Call[] = [ - * { - * contractAddress: "0x1234567890123456789012345678901234567890", - * entrypoint: "functionName", - * calldata: [1, 2, 3] - * }, - * { - * contractAddress: "0x0987654321098765432109876543210987654321", - * entrypoint: "anotherFunction", - * calldata: [4, 5, 6] - * } - * ]; - * const executeCalldata = fromCallsToExecuteCalldata(calls); - * console.log(executeCalldata); - * // [1234567890, 0987654321, 0, 6, 1, 2, 3, 4, 5, 6] - * ``` */ export const fromCallsToExecuteCalldata = (calls: Call[]) => { const { callArray, calldata } = transformCallsToMulticallArrays(calls); @@ -97,29 +50,8 @@ export const fromCallsToExecuteCalldata = (calls: Call[]) => { /** * Transforms a list of calls into the Cairo 0 `__execute__` calldata including nonce. - * @param calls - The list of calls to transform. - * @param nonce - The nonce to include in the calldata. - * @returns The Cairo 0 `__execute__` calldata including the nonce. + * * @deprecated - * @example - * ```typescript - * const calls: Call[] = [ - * { - * contractAddress: "0x1234567890123456789012345678901234567890", - * entrypoint: "functionName", - * calldata: [1, 2, 3] - * }, - * { - * contractAddress: "0x0987654321098765432109876543210987654321", - * entrypoint: "anotherFunction", - * calldata: [4, 5, 6] - * } - * ]; - * const nonce = 123; - * const result = fromCallsToExecuteCalldataWithNonce(calls, nonce); - * console.log(result); - * // [1234567890, 0987654321, 0, 6, 1, 2, 3, 4, 5, 6, "123"] - * ``` */ export const fromCallsToExecuteCalldataWithNonce = (calls: Call[], nonce: BigNumberish) => { return [...fromCallsToExecuteCalldata(calls), toBigInt(nonce).toString()] as Calldata; @@ -127,38 +59,8 @@ export const fromCallsToExecuteCalldataWithNonce = (calls: Call[], nonce: BigNum /** * Format Data inside Calls - * @param calls - The list of calls to transform. + * * @deprecated Not required for getting execute Calldata - * @returns An array of formatted call data. - * @example - * ```typescript - * const calls: Call[] = [ - * { - * contractAddress: "0x1234567890123456789012345678901234567890" - * entrypoint: "functionName", - * calldata: [1, 2, 3] - * }, - * { - * contractAddress: "0x0987654321098765432109876543210987654321", - * entrypoint: "anotherFunction", - * calldata: [4, 5, 6] - * } - * ]; - * const result = transformCallsToMulticallArrays_cairo1(calls); - * console.log(formattedCalls); - * // [ - * // { - * // to: "1234567890123456789012345678901234567890", - * // selector: "1234567890", - * // calldata: [1, 2, 3] - * // }, - * // { - * // to: "0987654321098765432109876543210987654321", - * // selector: "0987654321", - * // calldata: [4, 5, 6] - * // } - * // ] - * ``` */ export const transformCallsToMulticallArrays_cairo1 = (calls: Call[]) => { const callArray = calls.map((call) => ({ @@ -171,26 +73,6 @@ export const transformCallsToMulticallArrays_cairo1 = (calls: Call[]) => { /** * Transforms a list of calls into the Cairo 1 `__execute__` calldata. - * @param calls - The list of calls to transform. - * @returns The Cairo 1 `__execute__` calldata. - * @example - * ```typescript - * const calls: Call[] = [ - * { - * contractAddress: "0x1234567890123456789012345678901234567890", - * entrypoint: "functionName", - * calldata: [1, 2, 3] - * }, - * { - * contractAddress: "0x0987654321098765432109876543210987654321", - * entrypoint: "anotherFunction", - * calldata: [4, 5, 6] - * } - * ]; - * const result = fromCallsToExecuteCalldata_cairo1(calls); - * console.log(result); - * // [1234567890, 0987654321, 0, 6, 1, 2, 3, 4, 5, 6] - * ``` */ export const fromCallsToExecuteCalldata_cairo1 = (calls: Call[]) => { // ensure property order @@ -207,28 +89,7 @@ export const fromCallsToExecuteCalldata_cairo1 = (calls: Call[]) => { }; /** - * Create `__execute__` Calldata from Calls based on Cairo versions. - * @param calls - The list of calls to transform. - * @param cairoVersion - The Cairo version. - * @returns The `__execute__` calldata. - * @example - * ```typescript - * const calls: Call[] = [ - * { - * contractAddress: "0x1234567890123456789012345678901234567890", - * entrypoint: "functionName", - * calldata: [1, 2, 3] - * }, - * { - * contractAddress: "0x0987654321098765432109876543210987654321", - * entrypoint: "anotherFunction", - * calldata: [4, 5, 6] - * } - * ]; - * const result = getExecuteCalldata(calls, '1'); - * console.log(result); - * // [1234567890, 0987654321, 0, 6, 1, 2, 3, 4, 5, 6] - * ``` + * Create `__execute__` Calldata from Calls based on Cairo versions */ export const getExecuteCalldata = (calls: Call[], cairoVersion: CairoVersion = '0') => { if (cairoVersion === '1') { @@ -240,30 +101,10 @@ export const getExecuteCalldata = (calls: Call[], cairoVersion: CairoVersion = ' /** * Builds a UDCCall object. * - * @param {UniversalDeployerContractPayload | UniversalDeployerContractPayload[]} payload - The payload data for the UDCCall. Can be a single payload object or an array of payload objects. + * @param {UniversalDeployerContractPayload | UniversalDeployerContractPayload[]} payload - The payload data for the UDCCall. Can be a single payload object or an array of payload objects + *. * @param {string} address - The address to be used in the UDCCall. * @returns {{ calls: Array, addresses: Array }} - The UDCCall object containing an array of calls and an array of addresses. - * @example - * ```typescript - * const payload: UniversalDeployerContractPayload = { - * classHash: "0x1234567890123456789012345678901234567890", - * salt: "0x0987654321098765432109876543210987654321", - * unique:true, - * constructCalldata: [1, 2, 3] - * }; - * const address = "0xABCDEF1234567890ABCDEF1234567890ABCDEF12", - * const udcall = buildUDCCall(payload, address); - * console.log(udccall); - * // { - * // calls: [ - * { - * // contractAddress: "0xABCDEF1234567890ABCDEF1234567890ABCDEF12", - * // entrypoint: "functionName", - * // calldata: [classHash, salt, true, 3, 1, 2, 3] - * // }], - * // addresses: ["0x6fD084B56a7EDc5C06B3eB40f97Ae5A0C707A865"] - * // } - * ``` */ export function buildUDCCall( payload: UniversalDeployerContractPayload | UniversalDeployerContractPayload[], @@ -308,15 +149,7 @@ export function buildUDCCall( } /** - * Return transaction versions based on version type, default version type is 'transaction'. - * @param versionType - The type of version ("fee" or "transaction"). - * @returns An object containing transaction versions. - * @example - * ```typescript - * const transactionVersions = getVersionsByType('fee'); - * console.log(transactionVersions); - * // { v1: 1, v2: 2, v3: 3 } - * ``` + * Return transaction versions based on version type, default version type is 'transaction' */ export function getVersionsByType(versionType?: 'fee' | 'transaction') { return versionType === 'fee' From 1f3c9340e5090c19d5570f73c006a8acb270b188 Mon Sep 17 00:00:00 2001 From: BlackStarkGoku <165695008+BlackStarkGoku@users.noreply.github.com> Date: Thu, 2 May 2024 15:10:26 +0200 Subject: [PATCH 08/18] chore/add JsDoc for address.ts file (#1096) * chore(release): 6.8.0 [skip ci] # [6.8.0](https://github.com/starknet-io/starknet.js/compare/v6.7.0...v6.8.0) (2024-04-23) ### Bug Fixes * starkne types 0.7 ([#1087](https://github.com/starknet-io/starknet.js/issues/1087)) ([b038c76](https://github.com/starknet-io/starknet.js/commit/b038c76fe204746f1d1023c2ad3b46c022f6edbd)) * tslib ([#1068](https://github.com/starknet-io/starknet.js/issues/1068)) ([dd7dc10](https://github.com/starknet-io/starknet.js/commit/dd7dc10c57fc3cc35298c0d584a178666e9cfed1)) * **utils:** fix block identifier ([#1076](https://github.com/starknet-io/starknet.js/issues/1076)) ([0a3499d](https://github.com/starknet-io/starknet.js/commit/0a3499d49751061ceae1a4d6023b34f402376efc)) ### Features * add getGasPrice rpc provider method ([#1056](https://github.com/starknet-io/starknet.js/issues/1056)) ([d396275](https://github.com/starknet-io/starknet.js/commit/d396275348aff9c932d2bb7466b2a55f96214e4e)) * Export function parseCalldataField() ([4d59658](https://github.com/starknet-io/starknet.js/commit/4d596582023f24522c25a1a515ee0246d2eca90a)) * rpc 0.7.1 ([#1071](https://github.com/starknet-io/starknet.js/issues/1071)) ([11dc600](https://github.com/starknet-io/starknet.js/commit/11dc6003c74b6b6d0408b3f5894b5b6739d4bfba)) * chore/add JsDoc for address.ts file * update/ change address jsDoc - Add quotes to address string - Change address to get more zeros * feat/ Add JSDoc for encode.ts * update/ fix comments of encode.ts * update/ Fix last comment --------- Co-authored-by: Toni Tabak Co-authored-by: semantic-release-bot --- CHANGELOG.md | 1 - src/utils/address.ts | 35 ++++++++-- src/utils/encode.ts | 153 +++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 178 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ca08d34a..0ef332ead 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,6 @@ ### Bug Fixes - - starknet types 0.7 ([#1087](https://github.com/starknet-io/starknet.js/issues/1087)) ([b038c76](https://github.com/starknet-io/starknet.js/commit/b038c76fe204746f1d1023c2ad3b46c022f6edbd)) - tslib ([#1068](https://github.com/starknet-io/starknet.js/issues/1068)) ([dd7dc10](https://github.com/starknet-io/starknet.js/commit/dd7dc10c57fc3cc35298c0d584a178666e9cfed1)) diff --git a/src/utils/address.ts b/src/utils/address.ts index a611d627f..fe5a012e4 100644 --- a/src/utils/address.ts +++ b/src/utils/address.ts @@ -11,6 +11,12 @@ import { assertInRange, toHex } from './num'; * Format a hex number to '0x' and 64 characters, adding leading zeros if necessary. * @param {BigNumberish} address * @returns {string} Hex string : 0x followed by 64 characters. No upper case characters in the response. + * @example + * ```typescript + * const address = "0x90591d9fa3efc87067d95a643f8455e0b8190eb8cb7bfd39e4fb7571fdf"; + * const result = addAddressPadding(address); + * // result = "0x0000090591d9fa3efc87067d95a643f8455e0b8190eb8cb7bfd39e4fb7571fdf" + * ``` */ export function addAddressPadding(address: BigNumberish): string { return addHexPrefix(removeHexPrefix(toHex(address)).padStart(64, '0')); @@ -20,6 +26,12 @@ export function addAddressPadding(address: BigNumberish): string { * Check the validity of a Starknet address, and format it as a hex number : '0x' and 64 characters, adding leading zeros if necessary. * @param {BigNumberish} address * @returns {string} Hex string : 0x followed by 64 characters. No upper case characters in the response. + * @example + * ```typescript + * const address = "0x90591d9fa3efc87067d95a643f8455e0b8190eb8cb7bfd39e4fb7571fdf"; + * const result = validateAndParseAddress(address); + * // result = "0x0000090591d9fa3efc87067d95a643f8455e0b8190eb8cb7bfd39e4fb7571fdf" + * ``` */ export function validateAndParseAddress(address: BigNumberish): string { assertInRange(address, ZERO, ADDR_BOUND - 1n, 'Starknet Address'); @@ -34,13 +46,18 @@ export function validateAndParseAddress(address: BigNumberish): string { } /** - * Computes the checksum address for the given Starknet address. - * - * From https://github.com/ethers-io/ethers.js/blob/fc1e006575d59792fa97b4efb9ea2f8cca1944cf/packages/address/src.ts/index.ts#L12 - * @param {BigNumberish} address - The address to compute the checksum for. - * - * @returns {string} The checksum address. + * Convert an address to her checksum representation which uses a specific pattern of uppercase and lowercase letters within + * a given address to reduce the risk of errors introduced from typing an address or cut and paste issues. + * @param {BigNumberish} address + * @returns {string} Hex string : 0x followed by 64 characters. Mix of uppercase and lowercase + * @example + * ```typescript + * const address = "0x90591d9fa3efc87067d95a643f8455e0b8190eb8cb7bfd39e4fb7571fdf"; + * const result = getChecksumAddress(address); + * // result = "0x0000090591D9fA3EfC87067d95a643f8455E0b8190eb8Cb7bFd39e4fb7571fDF" + * ``` */ +// from https://github.com/ethers-io/ethers.js/blob/fc1e006575d59792fa97b4efb9ea2f8cca1944cf/packages/address/src.ts/index.ts#L12 export function getChecksumAddress(address: BigNumberish): string { const chars = removeHexPrefix(validateAndParseAddress(address)).toLowerCase().split(''); const hex = removeHexPrefix(keccakBn(address)); @@ -65,6 +82,12 @@ export function getChecksumAddress(address: BigNumberish): string { * @param address string * * @returns true if the ChecksumAddress is valid + * @example + * ```typescript + * const address = "0x0000090591D9fA3EfC87067d95a643f8455E0b8190eb8Cb7bFd39e4fb7571fDF"; + * const result = validateChecksumAddress(address); + * // result = true + * ``` */ export function validateChecksumAddress(address: string): boolean { return getChecksumAddress(address) === address; diff --git a/src/utils/encode.ts b/src/utils/encode.ts index 705d45d12..4095e6ba6 100644 --- a/src/utils/encode.ts +++ b/src/utils/encode.ts @@ -14,6 +14,18 @@ const STRING_ZERO = '0'; * Convert array buffer to string * * *[internal usage]* + * + * @param {ArrayBuffer} array The ArrayBuffer to convert to string. + * @returns {string} The converted string. + * + * @example + * ```typescript + * const buffer = new ArrayBuffer(5); + * const view = new Uint8Array(buffer); + * [72, 101, 108, 108, 111].forEach((x, idx) => view[idx] = x); + * const result = encode.arrayBufferToString(buffer); + * // result = "Hello" + * ``` */ export function arrayBufferToString(array: ArrayBuffer): string { return new Uint8Array(array).reduce((data, byte) => data + String.fromCharCode(byte), ''); @@ -23,6 +35,16 @@ export function arrayBufferToString(array: ArrayBuffer): string { * Convert utf8-string to Uint8Array * * *[internal usage]* + * + * @param {string} str The UTF-8 string to convert. + * @returns {Uint8Array} The encoded Uint8Array. + * + * @example + * ```typescript + * const myString = 'Hi'; + * const result = encode.utf8ToArray(myString); + * // result = Uint8Array(2) [ 72, 105 ] + * ``` */ export function utf8ToArray(str: string): Uint8Array { return new TextEncoder().encode(str); @@ -39,6 +61,16 @@ export function stringToArrayBuffer(str: string): Uint8Array { /** * Convert string to array buffer (browser and node compatible) + * + * @param {string} a The Base64 encoded string to convert. + * @returns {Uint8Array} The decoded Uint8Array. + * + * @example + * ```typescript + * const base64String = 'SGVsbG8='; // 'Hello' in Base64 + * const result = encode.atobUniversal(base64String); + * // result = Uint8Array(5) [ 72, 101, 108, 108, 111 ] + * ``` */ export function atobUniversal(a: string): Uint8Array { return base64.decode(a); @@ -46,6 +78,16 @@ export function atobUniversal(a: string): Uint8Array { /** * Convert array buffer to string (browser and node compatible) + * + * @param {ArrayBuffer} b The Array buffer. + * @returns {string} The Base64 encoded string. + * + * @example + * ```typescript + * const buffer = new Uint8Array([72, 101, 108, 108, 111]); // Array with ASCII values for 'Hello' + * const result = encode.btoaUniversal(buffer); + * // result = "SGVsbG8=" + * ``` */ export function btoaUniversal(b: ArrayBuffer): string { return base64.encode(new Uint8Array(b)); @@ -53,7 +95,16 @@ export function btoaUniversal(b: ArrayBuffer): string { /** * Convert array buffer to hex-string - * @returns format: hex-string + * + * @param {Uint8Array} buffer The encoded Uint8Array. + * @returns {string} The hex-string + * + * @example + * ```typescript + * const buffer = new Uint8Array([72, 101, 108, 108, 111]); // Array with ASCII values for 'Hello' + * const result = encode.buf2hex(buffer); + * // result = "48656c6c6f" + * ``` */ export function buf2hex(buffer: Uint8Array) { return buffer.reduce((r, x) => r + x.toString(16).padStart(2, '0'), ''); @@ -62,7 +113,14 @@ export function buf2hex(buffer: Uint8Array) { /** * Remove hex prefix '0x' from hex-string * @param hex hex-string - * @returns format: base16-string + * @returns {string} The hex-string + * + * @example + * ```typescript + * const hexStringWithPrefix = '0x48656c6c6f'; + * const result = encode.removeHexPrefix(hexStringWithPrefix); + * // result: "48656c6c6f" + * ``` */ export function removeHexPrefix(hex: string): string { return hex.replace(/^0x/i, ''); @@ -71,7 +129,14 @@ export function removeHexPrefix(hex: string): string { /** * Add hex prefix '0x' to base16-string * @param hex base16-string - * @returns format: hex-string + * @returns {string} The hex-string + * + * @example + * ```typescript + * const plainHexString = '48656c6c6f'; + * const result = encode.addHexPrefix(plainHexString); + * // result: "0x48656c6c6f" + * ``` */ export function addHexPrefix(hex: string): string { return `0x${removeHexPrefix(hex)}`; @@ -81,6 +146,22 @@ export function addHexPrefix(hex: string): string { * Prepend or append to string * * *[internal usage]* + * + * Pads a string to a certain length with a specific string. + * The padding can be applied either to the left or the right of the input string. + * + * @param {string} str The string to pad. + * @param {number} length The target length for the padded string. + * @param {boolean} left Set to true to add padding to the left, false to add it to the right. + * @param {string} [padding='0'] The string to use for padding. Defaults to '0'. + * @returns {string} The padded string. + * + * @example + * ```typescript + * const myString = 'hello'; + * const result = padString(myString, 10, true); + * // result = '00000hello' + * ``` */ function padString(str: string, length: number, left: boolean, padding = STRING_ZERO): string { const diff = length - str.length; @@ -94,6 +175,21 @@ function padString(str: string, length: number, left: boolean, padding = STRING_ /** * Prepend string (default with '0') + * + * Pads a string to a certain length with a specific string. + * The padding can be applied only to the left of the input string. + * + * @param {string} str The string to pad. + * @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'; + * const result = encode.padLeft(myString, 10); + * // result: '0000001A3F' + * ``` */ export function padLeft(str: string, length: number, padding = STRING_ZERO): string { return padString(str, length, true, padding); @@ -103,6 +199,21 @@ export function padLeft(str: string, length: number, padding = STRING_ZERO): str * Calculate byte length of string * * *[no internal usage]* + * + * Calculates the byte length of a string based on a specified byte size. + * The function rounds up the byte count to the nearest multiple of the specified byte size. + * + * @param {string} str The string whose byte length is to be calculated. + * @param {number} [byteSize='8'] The size of the byte block to round up to. Defaults to 8. + * @returns {number} The calculated byte length, rounded to the nearest multiple of byteSize. + * + * @example + * ```typescript + * const myString = 'Hello'; + * const result = encode.calcByteLength(myString, 4); + * // result = 8 (rounded up to the nearest multiple of 4) + * + * ``` */ export function calcByteLength(str: string, byteSize = 8): number { const { length } = str; @@ -114,17 +225,41 @@ export function calcByteLength(str: string, byteSize = 8): number { * Prepend '0' to string bytes * * *[no internal usage]* + * + * + * * Prepends padding to the left of a string to ensure it matches a specific byte length. + * The function uses a specified padding character and rounds up the string length to the nearest multiple of `byteSize`. + * + * @param {string} str The string to be padded. + * @param {number} [byteSize='8'] The byte block size to which the string length should be rounded up. Defaults to 8. + * @param {string} [padding='0'] The character to use for padding. Defaults to '0'. + * @returns {string} The padded string. + * + * @example + * ```typescript + * const myString = '123'; + * const result = encode.sanitizeBytes(myString); + * // result: '00000123' (padded to 8 characters) + * ``` */ export function sanitizeBytes(str: string, byteSize = 8, padding = STRING_ZERO): string { return padLeft(str, calcByteLength(str, byteSize), padding); } /** - * Prepend '0' to hex-string bytes + * Sanitizes a hex-string by removing any existing '0x' prefix, padding the string with '0' to ensure it has even length, + * and then re-adding the '0x' prefix. * * *[no internal usage]* * @param hex hex-string * @returns format: hex-string + * + * @example + * ```typescript + * const unevenHex = '0x23abc'; + * const result = encode.sanitizeHex(unevenHex); + * // result = '0x023abc' (padded to ensure even length) + * ``` */ export function sanitizeHex(hex: string): string { hex = removeHexPrefix(hex); @@ -139,6 +274,16 @@ export function sanitizeHex(hex: string): string { * String transformation util * * Pascal case to screaming snake case + * + * @param {string} text The PascalCase string to convert. + * @returns {string} The converted snake_case string in uppercase. + * + * @example + * ```typescript + * const pascalString = 'PascalCaseExample'; + * const result = encode.pascalToSnake(pascalString); + * // result: 'PASCAL_CASE_EXAMPLE' + * ``` */ export const pascalToSnake = (text: string) => /[a-z]/.test(text) From 0303b2180f8766953c4710e6ddc21b07ad2330a4 Mon Sep 17 00:00:00 2001 From: Toni Tabak Date: Mon, 6 May 2024 11:02:53 +0200 Subject: [PATCH 09/18] Revert "chore: Add JsDoc comments and examples for merkle.ts file (#1107)" This reverts commit f77bae58aea45850a1d95247d29d71500eba3b68. --- src/utils/merkle.ts | 51 ++++----------------------------------------- 1 file changed, 4 insertions(+), 47 deletions(-) diff --git a/src/utils/merkle.ts b/src/utils/merkle.ts index daa28ae24..71c850dee 100644 --- a/src/utils/merkle.ts +++ b/src/utils/merkle.ts @@ -23,17 +23,6 @@ export class MerkleTree { * Create Merkle tree * @param leaves hex-string array * @returns format: hex-string; Merkle tree root - * @example - * ```typescript - * const leaves: string[] = [ - * "0x2fd23d9182193775423497fc0c472e156c57c69e4089a1967fb288a2d84e914", - * "0x1234567890123456789012345678901234567890123456789012345678901234", - * "0xabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdef" - * ]; - * const merkleTree = new MerkleTree(); - * const root = merkleTree.build(leaves); - * // root = "0x71d6f4b2f7a5aa46daa76f2e01ab44b0e7581a82b40cb1289b89b2353fc1b9e0" - * ``` */ private build(leaves: string[]): string { if (leaves.length === 1) { @@ -55,18 +44,7 @@ export class MerkleTree { /** * Create hash from ordered a and b, Pedersen hash default - * @param a BigNumberish value to be hashed - * @param b BigNumberish value to be hashed - * @param hashMethod Function to compute hash, default is Pedersen hash * @returns format: hex-string - * @example - * ```typescript - * const hash = MerkleTree.hash( - * "0x2fd23d9182193775423497fc0c472e156c57c69e4089a1967fb288a2d84e914", - * "0x1234567890123456789012345678901234567890123456789012345678901234", - * ); - * // hash = "0x71d6f4b2f7a5aa46daa76f2e01ab44b0e7581a82b40cb1289b89b2353fc1b9e0" - * ``` */ static hash( a: BigNumberish, @@ -79,19 +57,10 @@ export class MerkleTree { /** * Return path to leaf - * @param leaf hex-string representing the leaf - * @param branch hex-string array representing the branch - * @param hashPath hex-string array representing the hash path - * @returns format: hex-string array representing the path to the leaf - * @example - * ```typescript - * const merkleTree = new MerkleTree(); - * const proof = merkleTree.getProof("0x2fd23d9182193775423497fc0c472e156c57c69e4089a1967fb288a2d84e914"); - * // proof = [ - * // "0x1234567890123456789012345678901234567890123456789012345678901234", - * // "0xabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdef" - * // ] - * ``` + * @param leaf hex-string + * @param branch hex-string array + * @param hashPath hex-string array + * @returns format: hex-string array */ public getProof(leaf: string, branch = this.leaves, hashPath: string[] = []): string[] { const index = branch.indexOf(leaf); @@ -123,18 +92,6 @@ export class MerkleTree { * @param leaf hex-string * @param path hex-string array * @param hashMethod hash method override, Pedersen default - * @returns {boolean} True if the Merkle tree path is valid, false otherwise - * @example - * ```typescript - * const root = "0x71d6f4b2f7a5aa46daa76f2e01ab44b0e7581a82b40cb1289b89b2353fc1b9e0"; - * const leaf = "0x2fd23d9182193775423497fc0c472e156c57c69e4089a1967fb288a2d84e914"; - * const path = [ - * "0x1234567890123456789012345678901234567890123456789012345678901234", - * "0xabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdef" - * ]; - * const isValid = proofMerklePath(root, leaf, path); - * // isValid = true - * ``` */ export function proofMerklePath( root: string, From cc4b0af5ac95bfb02094996c19992111dd43cc09 Mon Sep 17 00:00:00 2001 From: Toni Tabak Date: Mon, 6 May 2024 11:03:08 +0200 Subject: [PATCH 10/18] Revert "chore: Add JsDoc comments and examples for selector.ts file (#1106)" This reverts commit 0b25abae10381b2bfb4d784d5d19445a7c6e8511. --- src/utils/selector.ts | 41 ++++++----------------------------------- 1 file changed, 6 insertions(+), 35 deletions(-) diff --git a/src/utils/selector.ts b/src/utils/selector.ts index c54029962..1a137a62b 100644 --- a/src/utils/selector.ts +++ b/src/utils/selector.ts @@ -7,13 +7,9 @@ import { hexToBytes, isHex, isStringWholeNumber, toHex, toHexString } from './nu /** * Calculate hex-string keccak hash for a given BigNumberish - * @param value The value you want to get the keccak hash from. - * @returns format: hex-string keccak hash - * @example - * ```typescript - * const hash: string = keccakBn(123456789); - * // hash = "0x6c1eebcad9e5b7e0f13855f5e4b56e85ad24544b" - * ``` + * + * BigNumberish -> hex-string keccak hash + * @returns format: hex-string */ export function keccakBn(value: BigNumberish): string { const hexWithoutPrefix = removeHexPrefix(toHex(BigInt(value))); @@ -23,14 +19,9 @@ export function keccakBn(value: BigNumberish): string { /** * Calculate hex-string keccak hash for a given string - * @param str The value you want to get the keccak hash from. + * * String -> hex-string keccak hash * @returns format: hex-string - * @example - * ```typescript - * const hash: string = keccakHex("Hello, world!"); - * // hash = "0x3ad6fcbda8fc87e9fb42f7f0cd36d27da079ffafc6f0dcf36b6a6140e0f67c84" - * ``` */ function keccakHex(str: string): string { return addHexPrefix(keccak(utf8ToArray(str)).toString(16)); @@ -44,10 +35,6 @@ function keccakHex(str: string): string { * [Reference](https://github.com/starkware-libs/cairo-lang/blob/master/src/starkware/starknet/public/abi.py#L17-L22) * @param str the value you want to get the keccak hash from * @returns starknet keccak hash as BigInt - * @example - * ```typescript - * const hash: bigint = starknetKeccak("Hello, world!"); - * // hash = "38418923196344919485056939258679159916n" */ export function starknetKeccak(str: string): bigint { const hash = BigInt(keccakHex(str)); @@ -61,13 +48,8 @@ export function starknetKeccak(str: string): bigint { * Abi-function-name -> hex-string selector * * [Reference](https://github.com/starkware-libs/cairo-lang/blob/master/src/starkware/starknet/public/abi.py#L25-L26) - * @param funcName ascii-string of 'abi function name'. - * @returns format: hex-string; selector for 'abi function name'. - * @example - * ```typescript - * const selector: string = getSelectorFromName("myFunction"); - * // selector = "0x7e44baf0" - * ``` + * @param funcName ascii-string of 'abi function name' + * @returns format: hex-string; selector for 'abi function name' */ export function getSelectorFromName(funcName: string) { // sometimes BigInteger pads the hex string with zeros, which is not allowed in the starknet api @@ -81,17 +63,6 @@ export function getSelectorFromName(funcName: string) { * * @param value hex-string | dec-string | ascii-string * @returns format: hex-string - * @example - * ```typescript - * const selector: string = getSelector("myFunction"); - * // selector = "0x7e44bafo" - * - * const selector1: string = getSelector("0x123abc"); - * // selector1 = "0x123abc" - * - * const selector2: string = getSelector("123456"); - * // selector2 = "0x1e240" - * ``` */ export function getSelector(value: string) { if (isHex(value)) { From 3a58b844a827de8dfd3dfd8d691baa31986ba1ce Mon Sep 17 00:00:00 2001 From: Toni Tabak Date: Mon, 6 May 2024 11:05:14 +0200 Subject: [PATCH 11/18] Revert "chore: add examples to JsDoc for num.ts file (#1100)" This reverts commit ae5dcf90cba070fed57d5d649088d5435806b3f3. --- CHANGELOG.md | 1 - src/utils/num.ts | 175 +++-------------------------------------------- 2 files changed, 10 insertions(+), 166 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ef332ead..0ab41b55b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,6 @@ ### Bug Fixes - starknet types 0.7 ([#1087](https://github.com/starknet-io/starknet.js/issues/1087)) ([b038c76](https://github.com/starknet-io/starknet.js/commit/b038c76fe204746f1d1023c2ad3b46c022f6edbd)) - - tslib ([#1068](https://github.com/starknet-io/starknet.js/issues/1068)) ([dd7dc10](https://github.com/starknet-io/starknet.js/commit/dd7dc10c57fc3cc35298c0d584a178666e9cfed1)) - **utils:** fix block identifier ([#1076](https://github.com/starknet-io/starknet.js/issues/1076)) ([0a3499d](https://github.com/starknet-io/starknet.js/commit/0a3499d49751061ceae1a4d6023b34f402376efc)) diff --git a/src/utils/num.ts b/src/utils/num.ts index 8e3f471a0..544cb3bab 100644 --- a/src/utils/num.ts +++ b/src/utils/num.ts @@ -10,17 +10,6 @@ export type { BigNumberish }; /** * Test if string is hex-string * @param hex hex-string - * @returns {boolean} True if the input string is a hexadecimal string, false otherwise - * @example - * ```typescript - * const hexString1 = "0x2fd23d9182193775423497fc0c472e156c57c69e4089a1967fb288a2d84e914"; - * const result1 = isHex(hexString1); - * // result1 = true - * - * const hexString2 = "2fd23d9182193775423497fc0c472e156c57c69e4089a1967fb288a2d84e914"; - * const result2 = isHex(hexString2); - * // result2 = false - * ``` */ export function isHex(hex: string): boolean { return /^0x[0-9a-f]*$/i.test(hex); @@ -28,14 +17,6 @@ export function isHex(hex: string): boolean { /** * Convert BigNumberish to bigint - * @param value BigNumberish value to convert to bigint - * @returns {bigint} Converted bigint value - * @example - * ```typescript - * const bigNumberishValue1: BigNumberish = 1234567890; - * const result1 = toBigInt(bigNumberishValue1); - * // result1 = 1234567890n - * ``` */ export function toBigInt(value: BigNumberish): bigint { return BigInt(value); @@ -43,14 +24,6 @@ export function toBigInt(value: BigNumberish): bigint { /** * Test if value is bigint - * @param value Value to test - * @returns {boolean} True if the value is a bigint, false otherwise - * @example - * ```typescript - * const bigIntValue1: bigint = 1234567890n; - * const result1 = isBigint(bigIntValue1); - * // result1 = true - * ``` */ export function isBigInt(value: any): value is bigint { return typeof value === 'bigint'; @@ -58,27 +31,14 @@ export function isBigInt(value: any): value is bigint { /** * Convert BigNumberish to hex-string - * @param number BigNumberish value to convert to hex-string * @returns format: hex-string - * @example - * ```typescript - * const bigNumberishValue1: BigNumberish = 1234567890; - * const result1 = toHex(bigNumberishValue1); - * // result = "0x499602d2" - * ``` */ export function toHex(number: BigNumberish): string { return addHexPrefix(toBigInt(number).toString(16)); } /** - * Alias of toHex - * @returns format: hex-string - * @example - * ```typescript - * const result = toHexString(123); - * // result = "0x7b" - * ``` + * Alias of ToHex */ export const toHexString = toHex; @@ -89,14 +49,7 @@ export const toHexString = toHex; * * A storage key is represented as up to 62 hex digits, 3 bits, and 5 leading zeroes: * `0x0 + [0-7] + 62 hex = 0x + 64 hex` - * @param number BigNumberish value to convert to storage-key-string * @returns format: storage-key-string - * @example - * ```typescript - * const bigNumberishValue1: BigNumberish = 1234567890; - * const result = toStorageKey(bigNumberishValue1); - * // result = "0x000000000000000000000000000000000000000000000000000000000499602d2" - * ``` */ export function toStorageKey(number: BigNumberish): string { const res = addHexPrefix(toBigInt(number).toString(16).padStart(64, '0')); @@ -107,48 +60,23 @@ export function toStorageKey(number: BigNumberish): string { * Convert hexadecimal string to decimal string * @param hex hex-string * @returns format: decimal string - * @example - * ```typescript - * const hexString = "0x2fd23d9182193775423497fc0c472e156c57c69e4089a1967fb288a2d84e914"; - * const result = hexToDecimalString(hexString); - * // result = "32507161997631881240494522159444018041090212371621416251492750949915267618964"; - * ``` */ export function hexToDecimalString(hex: string): string { return BigInt(addHexPrefix(hex)).toString(10); } /** - * Remove hex string leading zero and lowercase it; + * Remove hex string leading zero and lowercase it + * @example '0x01A...' -> '0x1a..' * @param hex hex-string * @returns format: hex-string - * @example - * ```typescript - * const hexString = '0x01A2F3' - * const result = cleanHex(hexString); - * // result = '0x1a2f3' - * ``` */ export const cleanHex = (hex: string) => hex.toLowerCase().replace(/^(0x)0+/, '$1'); /** - * Asserts input is equal to or greater than lowerBound and lower than upperBound. + * Asserts input is equal to or greater then lowerBound and lower then upperBound. * * The `inputName` parameter is used in the assertion message. - * @param input Value to check - * @param lowerBound Lower bound value - * @param upperBound Upper bound value - * @param inputName Name of the input for error message - * @Throws Error if input is out of range - * @example - * ```typescript - * const input1:BigNumberish = 10; - * assertInRange(input1, 5, 20, 'value') - * - * const input2: BigNumberish = 25; - * assertInRange(input2, 5, 20, 'value'); - * // Throws Error: Message not signable, invalid value length. - * ``` */ export function assertInRange( input: BigNumberish, @@ -169,14 +97,7 @@ export function assertInRange( /** * Convert BigNumberish array to decimal string array - * @param rawCalldata Array of BigNumberish values * @returns format: decimal string array - * @example - * ```typescript - * const bigNumberishArray: BigNumberish[] = [123, "456", 789n]; - * const result = bigNumberishArrayToDecimalStringArray(bigNumberishArray); - * // result = ["123", "456", "789"] - * ``` */ export function bigNumberishArrayToDecimalStringArray(rawCalldata: BigNumberish[]): string[] { return rawCalldata.map((x) => toBigInt(x).toString(10)); @@ -184,14 +105,7 @@ export function bigNumberishArrayToDecimalStringArray(rawCalldata: BigNumberish[ /** * Convert BigNumberish array to hexadecimal string array - * @param rawCalldata Array of BigNumberish values * @returns format: hex-string array - * @example - * ```typescript - * const bigNumberishArray: BigNumberish[] = [123, "456", 789n]; - * const result = bigNumberishArrayToHexadecimalStringArray(bigNumberishArray); - * // result = ["0x7b", "0x1c8", "0x315"] - * ``` */ export function bigNumberishArrayToHexadecimalStringArray(rawCalldata: BigNumberish[]): string[] { return rawCalldata.map((x) => toHex(x)); @@ -199,28 +113,12 @@ export function bigNumberishArrayToHexadecimalStringArray(rawCalldata: BigNumber /** * Test if string is whole number (0, 1, 2, 3...) - * @param value The string to be tested. - * @returns {boolean} Returns true if the value is a number, otherwise returns false. - * @example - * ```typescript - * const result = isStringWholeNumber("123"); - * // result = true - * ``` */ export const isStringWholeNumber = (value: string) => /^\d+$/.test(value); /** * Convert string to decimal string - * @param value The string to be converted. * @returns format: decimal string - * @example - * ```typescript - * const result = getDecimalString("0x1a"); - * // result = "26" - * - * const result2 = getDecimalString("Hello"); - * // Throws Error: "Hello need to be hex-string or whole-number-string" - * ``` */ export function getDecimalString(value: string) { if (isHex(value)) { @@ -234,16 +132,7 @@ export function getDecimalString(value: string) { /** * Convert string to hexadecimal string - * @param value The string to be converted. * @returns format: hex-string - * @example - * ```typescript - * const result = getHexString("123"); - * // result = "0x7b" - * - * const result2 = getHexString("Hello"); - * // Throws Error: Hello need to be hex-string or whole-number-string - * ``` */ export function getHexString(value: string) { if (isHex(value)) { @@ -257,14 +146,7 @@ export function getHexString(value: string) { /** * Convert string array to hex-string array - * @param value The string array to be converted. * @returns format: hex-string array - * @example - * ```typescript - * const stringArray: string[] = ["123", "456", "789"]; - * const result = getHexStringArray(stringArray); - * // result = ["0x7b", "0x1c8", "0x315"] - * ``` */ export function getHexStringArray(value: Array) { return value.map((el) => getHexString(el)); @@ -272,28 +154,12 @@ export function getHexStringArray(value: Array) { /** * Convert boolean to "0" or "1" - * @param value The boolean value to be converted. - * @returns {boolean} Returns true if the value is a number, otherwise returns false. - * @example - * ```typescript - * const result = toCairoBool(true); - * // result ="1" - * - * const result2 = toCairoBool(false); - * // result2 = "0" - * ``` */ export const toCairoBool = (value: boolean): string => (+value).toString(); /** * Convert hex-string to an array of Bytes (Uint8Array) - * @param value The hex-string to be converted. - * @returns The array of bytes (Uint8Array) corresponding to the hex-string. - * @example - * ```typescript - * const result = hexToBytes("0x123456"); - * // result = Uint8Array [ 18, 52, 86 ] - * ``` + * @param value hex-string */ export function hexToBytes(value: string): Uint8Array { if (!isHex(value)) throw new Error(`${value} need to be a hex-string`); @@ -306,15 +172,10 @@ export function hexToBytes(value: string): Uint8Array { } /** - * Increase a give number by specified percentage - * @param number The value to be increased (BigInt or number). + * + * @param number value to be increased * @param percent integer as percent ex. 50 for 50% - * @returns The increased value as a BigInt. - * @example - * ```typescript - * const result = addPercent(100, 50); - * // result = 150n - * ``` + * @returns increased value */ export function addPercent(number: BigNumberish, percent: number) { const bigIntNum = BigInt(number); @@ -325,15 +186,7 @@ export function addPercent(number: BigNumberish, percent: number) { * 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'; @@ -343,15 +196,7 @@ export function isNumber(value: unknown): value is 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'; From eceda5dc1c39e472e1105e07797e76aaac3c1531 Mon Sep 17 00:00:00 2001 From: saimeunt Date: Mon, 6 May 2024 15:04:04 +0200 Subject: [PATCH 12/18] feat: add type coverage (#1120) --- .gitignore | 1 + CONTRIBUTING.md | 1 + package-lock.json | 443 +++++++++++++++++++++- package.json | 12 +- src/types/lib/contract/abi.ts | 9 +- src/utils/assert.ts | 2 +- src/utils/calldata/formatter.ts | 93 ++--- src/utils/calldata/index.ts | 2 +- src/utils/calldata/parser/parser-2.0.0.ts | 10 +- src/utils/eth.ts | 2 +- src/utils/responseParser/index.ts | 6 +- 11 files changed, 523 insertions(+), 58 deletions(-) diff --git a/.gitignore b/.gitignore index 632f2b292..b84a92e49 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ .vscode .idea coverage +coverage-ts dist node_modules npm-debug.log diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ec50c5ea9..9f915e625 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -103,6 +103,7 @@ For major changes that markedly transform the existing API or significantly alte - We’re using [Prettier](https://github.com/prettier/prettier) to format code, so don’t worry much about code formatting. - Don’t commit generated files, like minified JavaScript. - Don’t change the version number or changelog. +- Use `npm run ts:coverage` to check the global type coverage rate and `npm run ts:coverage:report` to generate a complete report (summary displayed in the console, full HTML report available in the `coverage-ts` folder by launching `./coverage-ts/index.html` in your browser) and find files having low coverage. ## Need help? diff --git a/package-lock.json b/package-lock.json index 17aa7a8b3..d09dd21f6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -58,8 +58,10 @@ "prettier-plugin-import-sort": "^0.0.7", "semantic-release": "^23.0.5", "tsup": "^8.0.2", + "type-coverage": "^2.28.2", "typedoc": "^0.25.7", - "typescript": "~5.4.0" + "typescript": "~5.4.0", + "typescript-coverage-report": "^1.0.0" } }, "../starknet-types": { @@ -3205,6 +3207,20 @@ "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", "dev": true }, + "node_modules/@hypnosphi/create-react-context": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@hypnosphi/create-react-context/-/create-react-context-0.3.1.tgz", + "integrity": "sha512-V1klUed202XahrWJLLOT3EXNeCpFHCcJntdFGI15ntCwau+jfT386w7OFTMaCqOgXUH1fa0w/I1oZs+i/Rfr0A==", + "dev": true, + "dependencies": { + "gud": "^1.0.0", + "warning": "^4.0.3" + }, + "peerDependencies": { + "prop-types": "^15.0.0", + "react": ">=0.14.0" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -5114,6 +5130,20 @@ "semantic-release": ">=20.1.0" } }, + "node_modules/@semantic-ui-react/event-stack": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@semantic-ui-react/event-stack/-/event-stack-3.1.3.tgz", + "integrity": "sha512-FdTmJyWvJaYinHrKRsMLDrz4tTMGdFfds299Qory53hBugiDvGC0tEJf+cHsi5igDwWb/CLOgOiChInHwq8URQ==", + "dev": true, + "dependencies": { + "exenv": "^1.2.2", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -6589,6 +6619,12 @@ "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==", "dev": true }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", + "dev": true + }, "node_modules/clean-stack": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", @@ -7471,6 +7507,26 @@ } } }, + "node_modules/deep-equal": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.2.tgz", + "integrity": "sha512-5tdhKF6DbU7iIzrIOa1AOUt39ZRm13cmL1cGEh//aqR8x9+tNfbywRf0n5FD/18OKMdo7DNEtrX2t22ZAkI+eg==", + "dev": true, + "dependencies": { + "is-arguments": "^1.1.1", + "is-date-object": "^1.0.5", + "is-regex": "^1.1.4", + "object-is": "^1.1.5", + "object-keys": "^1.1.1", + "regexp.prototype.flags": "^1.5.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", @@ -8579,6 +8635,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/exenv": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/exenv/-/exenv-1.2.2.tgz", + "integrity": "sha512-Z+ktTxTwv9ILfgKCk32OX3n/doe+OcLTRtqK9pcL+JsP3J1/VW8Uvl4ZjLlKqeW4rzK4oesDOGMEMRIZqtP4Iw==", + "dev": true + }, "node_modules/exit": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", @@ -9215,6 +9277,12 @@ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, + "node_modules/gud": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/gud/-/gud-1.0.0.tgz", + "integrity": "sha512-zGEOVKFM5sVPPrYs7J5/hYEw2Pof8KCyOwyhG8sAF26mCAeUFAcYPu1mwB7hhpIP29zOIBaDqwuHdLp0jvZXjw==", + "dev": true + }, "node_modules/handlebars": { "version": "4.7.8", "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", @@ -9788,6 +9856,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-arguments": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", + "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-array-buffer": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", @@ -12290,6 +12374,12 @@ "node": "*" } }, + "node_modules/keyboard-key": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/keyboard-key/-/keyboard-key-1.1.0.tgz", + "integrity": "sha512-qkBzPTi3rlAKvX7k0/ub44sqOfXeLc/jcnGGmj5c7BJpU8eDrEVPyhCvNYAaoubbsLm9uGWwQJO1ytQK1a9/dQ==", + "dev": true + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -12799,6 +12889,18 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, "node_modules/lossless-json": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/lossless-json/-/lossless-json-4.0.1.tgz", @@ -13073,6 +13175,15 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, + "node_modules/ncp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", + "integrity": "sha512-zIdGUrPRFTUELUvr3Gmc7KZ2Sw/h1PiVM0Af/oHB6zgnV1ikqSfRk+TOufi79aHYCW3NiOXmr1BP5nWbzojLaA==", + "dev": true, + "bin": { + "ncp": "bin/ncp" + } + }, "node_modules/neo-async": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", @@ -16056,6 +16167,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/object-is": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", + "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", @@ -16604,6 +16731,17 @@ "node": ">=8" } }, + "node_modules/popper.js": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz", + "integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==", + "deprecated": "You can find the new Popper v2 at @popperjs/core, this package is dedicated to the legacy v1", + "dev": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, "node_modules/possible-typed-array-names": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", @@ -16744,6 +16882,23 @@ "node": ">= 6" } }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true + }, "node_modules/proto-list": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", @@ -16828,6 +16983,33 @@ "node": ">=0.10.0" } }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "dev": true, + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "dev": true, + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, "node_modules/react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", @@ -17286,6 +17468,16 @@ "node": ">=v12.22.7" } }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "dev": true, + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + } + }, "node_modules/semantic-release": { "version": "23.0.6", "resolved": "https://registry.npmjs.org/semantic-release/-/semantic-release-23.0.6.tgz", @@ -17584,6 +17776,82 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, + "node_modules/semantic-ui-react": { + "version": "0.88.2", + "resolved": "https://registry.npmjs.org/semantic-ui-react/-/semantic-ui-react-0.88.2.tgz", + "integrity": "sha512-+02kN2z8PuA/cMdvDUsHhbJmBzxxgOXVHMFr9XK7zGb0wkW9A6OPQMFokWz7ozlVtKjN6r7zsb+Qvjk/qq1OWw==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.1.2", + "@semantic-ui-react/event-stack": "^3.1.0", + "@stardust-ui/react-component-event-listener": "~0.38.0", + "@stardust-ui/react-component-ref": "~0.38.0", + "classnames": "^2.2.6", + "keyboard-key": "^1.0.4", + "lodash": "^4.17.15", + "prop-types": "^15.7.2", + "react-is": "^16.8.6", + "react-popper": "^1.3.4", + "shallowequal": "^1.1.0" + }, + "peerDependencies": { + "react": "^16.8.0", + "react-dom": "^16.8.0" + } + }, + "node_modules/semantic-ui-react/node_modules/@stardust-ui/react-component-event-listener": { + "version": "0.38.0", + "resolved": "https://registry.npmjs.org/@stardust-ui/react-component-event-listener/-/react-component-event-listener-0.38.0.tgz", + "integrity": "sha512-sIP/e0dyOrrlb8K7KWumfMxj/gAifswTBC4o68Aa+C/GA73ccRp/6W1VlHvF/dlOR4KLsA+5SKnhjH36xzPsWg==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.1.2", + "prop-types": "^15.7.2" + }, + "peerDependencies": { + "react": "^16.8.0", + "react-dom": "^16.8.0" + } + }, + "node_modules/semantic-ui-react/node_modules/@stardust-ui/react-component-ref": { + "version": "0.38.0", + "resolved": "https://registry.npmjs.org/@stardust-ui/react-component-ref/-/react-component-ref-0.38.0.tgz", + "integrity": "sha512-xjs6WnvJVueSIXMWw0C3oWIgAPpcD03qw43oGOjUXqFktvpNkB73JoKIhS4sCrtQxBdct75qqr4ZL6JiyPcESw==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.1.2", + "prop-types": "^15.7.2", + "react-is": "^16.6.3" + }, + "peerDependencies": { + "react": "^16.8.0", + "react-dom": "^16.8.0" + } + }, + "node_modules/semantic-ui-react/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true + }, + "node_modules/semantic-ui-react/node_modules/react-popper": { + "version": "1.3.11", + "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-1.3.11.tgz", + "integrity": "sha512-VSA/bS+pSndSF2fiasHK/PTEEAyOpX60+H5EPAjoArr8JGm+oihu4UbrqcEBpQibJxBVCpYyjAX7abJ+7DoYVg==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.1.2", + "@hypnosphi/create-react-context": "^0.3.1", + "deep-equal": "^1.1.1", + "popper.js": "^1.14.4", + "prop-types": "^15.6.1", + "typed-styles": "^0.0.7", + "warning": "^4.0.2" + }, + "peerDependencies": { + "react": "0.14.x || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, "node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -17690,6 +17958,12 @@ "node": ">= 0.4" } }, + "node_modules/shallowequal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==", + "dev": true + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -18641,6 +18915,27 @@ "webidl-conversions": "^4.0.2" } }, + "node_modules/tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "dependencies": { + "tslib": "^1.8.1" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + } + }, + "node_modules/tsutils/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -18653,6 +18948,35 @@ "node": ">= 0.8.0" } }, + "node_modules/type-coverage": { + "version": "2.28.2", + "resolved": "https://registry.npmjs.org/type-coverage/-/type-coverage-2.28.2.tgz", + "integrity": "sha512-mTANnzAeuH7ldrg0fzPnhh+aCCSVAO7V7hlBoVg4XxuUtQyD3ogir/R7Z6Q7W0H1JREtDIY4I91SGHAgEPi6Bw==", + "dev": true, + "dependencies": { + "minimist": "1", + "type-coverage-core": "^2.28.1" + }, + "bin": { + "type-coverage": "bin/type-coverage" + } + }, + "node_modules/type-coverage-core": { + "version": "2.28.1", + "resolved": "https://registry.npmjs.org/type-coverage-core/-/type-coverage-core-2.28.1.tgz", + "integrity": "sha512-NniLJtLiDg0+dhrf/9ACGwi3OAhIfvd20f1CB0yxIUBMECwmJp7e7me3lM8djkrDBJtqSY+uXA6PRs+yv3HTnA==", + "dev": true, + "dependencies": { + "fast-glob": "3", + "minimatch": "6 || 7 || 8 || 9", + "normalize-path": "3", + "tslib": "1 || 2", + "tsutils": "3" + }, + "peerDependencies": { + "typescript": "2 || 3 || 4 || 5" + } + }, "node_modules/type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", @@ -18747,6 +19071,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/typed-styles": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/typed-styles/-/typed-styles-0.0.7.tgz", + "integrity": "sha512-pzP0PWoZUhsECYjABgCGQlRGL1n7tOHsgwYv3oIiEpJwGhFTuty/YNeduxQYzXXa3Ge5BdT6sHYIQYpl4uJ+5Q==", + "dev": true + }, "node_modules/typedoc": { "version": "0.25.12", "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.25.12.tgz", @@ -18793,6 +19123,108 @@ "node": ">=14.17" } }, + "node_modules/typescript-coverage-report": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typescript-coverage-report/-/typescript-coverage-report-1.0.0.tgz", + "integrity": "sha512-ys/DG6eaO0XaHZIPZobJQLj9lUPSOIa1xt5Pz6tvr7QAWRt3OSCuMLiPduWtg7oTeZcOHqG90owA/zOeyZdq3g==", + "dev": true, + "dependencies": { + "chalk": "4.1.2", + "cli-table3": "^0.6.1", + "commander": "^5.0.0", + "ncp": "^2.0.0", + "rimraf": "^3.0.2", + "semantic-ui-react": "^0.88.2", + "type-coverage-core": "^2.23.0" + }, + "bin": { + "typescript-coverage-report": "dist/bin/typescript-coverage-report.js" + }, + "peerDependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0", + "typescript": "2 || 3 || 4 || 5" + } + }, + "node_modules/typescript-coverage-report/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/typescript-coverage-report/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/typescript-coverage-report/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/typescript-coverage-report/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/typescript-coverage-report/node_modules/commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/typescript-coverage-report/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/typescript-coverage-report/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/uglify-js": { "version": "3.17.4", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", @@ -19033,6 +19465,15 @@ "makeerror": "1.0.12" } }, + "node_modules/warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "dev": true, + "dependencies": { + "loose-envify": "^1.0.0" + } + }, "node_modules/webidl-conversions": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", diff --git a/package.json b/package.json index f3f8b697e..0c67828f1 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,9 @@ "info:version": "npm pkg get version | xargs", "format": "prettier --log-level log --write \"**/*.{ts,js,md,yml,json}\"", "lint": "eslint . --cache --fix --ext .ts", - "ts:check": "tsc --noEmit --resolveJsonModule --project tsconfig.eslint.json" + "ts:check": "tsc --noEmit --resolveJsonModule --project tsconfig.eslint.json", + "ts:coverage": "type-coverage --at-least 95", + "ts:coverage:report": "typescript-coverage-report" }, "keywords": [ "starknet", @@ -86,8 +88,10 @@ "prettier-plugin-import-sort": "^0.0.7", "semantic-release": "^23.0.5", "tsup": "^8.0.2", + "type-coverage": "^2.28.2", "typedoc": "^0.25.7", - "typescript": "~5.4.0" + "typescript": "~5.4.0", + "typescript-coverage-report": "^1.0.0" }, "dependencies": { "@noble/curves": "~1.4.0", @@ -98,9 +102,9 @@ "isomorphic-fetch": "^3.0.0", "lossless-json": "^4.0.1", "pako": "^2.0.4", + "starknet-types-07": "npm:starknet-types@^0.7.2", "ts-mixer": "^6.0.3", - "url-join": "^4.0.1", - "starknet-types-07": "npm:starknet-types@^0.7.2" + "url-join": "^4.0.1" }, "lint-staged": { "*.ts": "eslint --cache --fix", diff --git a/src/types/lib/contract/abi.ts b/src/types/lib/contract/abi.ts index 6583165dd..1b23e1b47 100644 --- a/src/types/lib/contract/abi.ts +++ b/src/types/lib/contract/abi.ts @@ -1,5 +1,5 @@ /** ABI */ -export type Abi = ReadonlyArray; +export type Abi = ReadonlyArray; // Basic elements export type AbiEntry = { name: string; type: 'felt' | 'felt*' | string }; @@ -31,6 +31,13 @@ export type StructAbi = { type: 'struct'; }; +export type AbiInterfaces = { [name: string]: InterfaceAbi }; +export type InterfaceAbi = { + items: FunctionAbi[]; + name: string; + type: 'interface'; +}; + export type AbiEnums = { [name: string]: EnumAbi }; export type EnumAbi = { variants: (AbiEntry & { offset: number })[]; diff --git a/src/utils/assert.ts b/src/utils/assert.ts index 4d0830ebb..ef35545a3 100644 --- a/src/utils/assert.ts +++ b/src/utils/assert.ts @@ -4,7 +4,7 @@ * @param {string} [message] - The optional message to include in the error. * @throws {Error} Throws an error if the condition is false. */ -export default function assert(condition: any, message?: string): asserts condition { +export default function assert(condition: boolean, message?: string): asserts condition { if (!condition) { throw new Error(message || 'Assertion failure'); } diff --git a/src/utils/calldata/formatter.ts b/src/utils/calldata/formatter.ts index a0adc3a68..260299d0c 100644 --- a/src/utils/calldata/formatter.ts +++ b/src/utils/calldata/formatter.ts @@ -2,7 +2,7 @@ import { isBigInt } from '../num'; import { decodeShortString } from '../shortString'; const guard = { - isBN: (data: any, type: any, key: any) => { + isBN: (data: Record, type: Record, key: string) => { if (!isBigInt(data[key])) throw new Error( `Data and formatter mismatch on ${key}:${type[key]}, expected response data ${key}:${ @@ -10,7 +10,7 @@ const guard = { } to be BN instead it is ${typeof data[key]}` ); }, - unknown: (data: any, type: any, key: any) => { + unknown: (data: Record, type: Record, key: string) => { throw new Error(`Unhandled formatter type on ${key}:${type[key]} for data ${key}:${data[key]}`); }, }; @@ -23,51 +23,58 @@ const guard = { * @param {any} [sameType] - The same type definition to be used (optional). * @returns - The formatted data. */ -export default function formatter(data: any, type: any, sameType?: any) { +export default function formatter( + data: Record, + type: Record, + sameType?: any +) { // match data element with type element - return Object.entries(data).reduce((acc, [key, value]: [any, any]) => { - const elType = sameType ?? type[key]; + return Object.entries(data).reduce( + (acc, [key, value]: [any, any]) => { + const elType = sameType ?? type[key]; - if (!(key in type) && !sameType) { - // no type definition for element return original element - acc[key] = value; - return acc; - } + if (!(key in type) && !sameType) { + // no type definition for element return original element + acc[key] = value; + return acc; + } - if (elType === 'string') { - if (Array.isArray(data[key])) { - // long string (felt*) - const arrayStr = formatter( - data[key], - data[key].map((_: any) => elType) - ); - acc[key] = Object.values(arrayStr).join(''); + if (elType === 'string') { + if (Array.isArray(data[key])) { + // long string (felt*) + const arrayStr = formatter( + data[key], + data[key].map((_: any) => elType) + ); + acc[key] = Object.values(arrayStr).join(''); + return acc; + } + guard.isBN(data, type, key); + acc[key] = decodeShortString(value); + return acc; + } + if (elType === 'number') { + guard.isBN(data, type, key); + acc[key] = Number(value); + return acc; + } + if (typeof elType === 'function') { + acc[key] = elType(value); + return acc; + } + if (Array.isArray(elType)) { + const arrayObj = formatter(data[key], elType, elType[0]); + acc[key] = Object.values(arrayObj); + return acc; + } + if (typeof elType === 'object') { + acc[key] = formatter(data[key], elType); return acc; } - guard.isBN(data, type, key); - acc[key] = decodeShortString(value); - return acc; - } - if (elType === 'number') { - guard.isBN(data, type, key); - acc[key] = Number(value); - return acc; - } - if (typeof elType === 'function') { - acc[key] = elType(value); - return acc; - } - if (Array.isArray(elType)) { - const arrayObj = formatter(data[key], elType, elType[0]); - acc[key] = Object.values(arrayObj); - return acc; - } - if (typeof elType === 'object') { - acc[key] = formatter(data[key], elType); - return acc; - } - guard.unknown(data, type, key); - return acc; - }, {} as any); + guard.unknown(data, type, key); + return acc; + }, + {} as Record + ); } diff --git a/src/utils/calldata/index.ts b/src/utils/calldata/index.ts index e9765c806..904e99506 100644 --- a/src/utils/calldata/index.ts +++ b/src/utils/calldata/index.ts @@ -271,7 +271,7 @@ export class CallData { */ public format(method: string, response: string[], format: object): Result { const parsed = this.parse(method, response); - return formatter(parsed, format); + return formatter(parsed as Record, format); } /** diff --git a/src/utils/calldata/parser/parser-2.0.0.ts b/src/utils/calldata/parser/parser-2.0.0.ts index 6c9d7b2ba..94e6a83bc 100644 --- a/src/utils/calldata/parser/parser-2.0.0.ts +++ b/src/utils/calldata/parser/parser-2.0.0.ts @@ -1,4 +1,4 @@ -import { Abi, FunctionAbi } from '../../../types'; +import { Abi, FunctionAbi, EventAbi, StructAbi, InterfaceAbi } from '../../../types'; import { AbiParserInterface } from './interface'; export class AbiParser2 implements AbiParserInterface { @@ -23,8 +23,10 @@ export class AbiParser2 implements AbiParserInterface { * @returns FunctionAbi | undefined */ public getMethod(name: string): FunctionAbi | undefined { - const intf = this.abi.find((it) => it.type === 'interface'); - return intf.items.find((it: any) => it.name === name); + const intf = this.abi.find( + (it: FunctionAbi | EventAbi | StructAbi | InterfaceAbi) => it.type === 'interface' + ) as InterfaceAbi; + return intf.items.find((it) => it.name === name); } /** @@ -32,7 +34,7 @@ export class AbiParser2 implements AbiParserInterface { * @returns Abi */ public getLegacyFormat(): Abi { - return this.abi.flatMap((e) => { + return this.abi.flatMap((e: FunctionAbi | EventAbi | StructAbi | InterfaceAbi) => { if (e.type === 'interface') { return e.items; } diff --git a/src/utils/eth.ts b/src/utils/eth.ts index d8c2a7536..21c1f60d2 100644 --- a/src/utils/eth.ts +++ b/src/utils/eth.ts @@ -32,6 +32,6 @@ export function ethRandomPrivateKey(): string { export function validateAndParseEthAddress(address: BigNumberish): string { assertInRange(address, ZERO, 2n ** 160n - 1n, 'Ethereum Address '); const result = addHexPrefix(removeHexPrefix(toHex(address)).padStart(40, '0')); - assert(result.match(/^(0x)?[0-9a-f]{40}$/), 'Invalid Ethereum Address Format'); + assert(Boolean(result.match(/^(0x)?[0-9a-f]{40}$/)), 'Invalid Ethereum Address Format'); return result; } diff --git a/src/utils/responseParser/index.ts b/src/utils/responseParser/index.ts index 9161fc124..9605da691 100644 --- a/src/utils/responseParser/index.ts +++ b/src/utils/responseParser/index.ts @@ -1,4 +1,6 @@ import { + BlockWithTxHashes, + FeeEstimate, CallContractResponse, DeclareContractResponse, DeployContractResponse, @@ -11,13 +13,13 @@ import { import type { GetTransactionReceiptResponse } from '../transactionReceipt'; export abstract class ResponseParser { - abstract parseGetBlockResponse(res: any): GetBlockResponse; + abstract parseGetBlockResponse(res: BlockWithTxHashes): GetBlockResponse; abstract parseGetTransactionResponse(res: any): GetTransactionResponse; abstract parseGetTransactionReceiptResponse(res: any): GetTransactionReceiptResponse; - abstract parseFeeEstimateResponse(res: any): EstimateFeeResponse; + abstract parseFeeEstimateResponse(res: FeeEstimate[]): EstimateFeeResponse; abstract parseCallContractResponse(res: any): CallContractResponse; From f1c3b8e3aeb96f6efb7e512ac3ba689253004c9d Mon Sep 17 00:00:00 2001 From: 0xknwn <145777008+0xknwn@users.noreply.github.com> Date: Fri, 17 May 2024 16:14:09 +0200 Subject: [PATCH 13/18] fix: cannot infer ts2742 types from starknet-types@0.7 (#1098) the externalization of types in starknet-types prevent typescript from inferring a type that are not available in the project. To workaround this issue, this commit imports the SPEC type so that the inference can happen --- src/account/default.ts | 2 ++ src/utils/stark.ts | 2 ++ src/wallet/account.ts | 2 ++ src/wallet/connect.ts | 2 ++ 4 files changed, 8 insertions(+) diff --git a/src/account/default.ts b/src/account/default.ts index 0a43d0a8a..dc4813c67 100644 --- a/src/account/default.ts +++ b/src/account/default.ts @@ -1,3 +1,5 @@ +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import type { SPEC } from 'starknet-types-07'; import { UDC, ZERO } from '../constants'; import { Provider, ProviderInterface } from '../provider'; import { Signer, SignerInterface } from '../signer'; diff --git a/src/utils/stark.ts b/src/utils/stark.ts index 28e295d81..dc9a995a2 100644 --- a/src/utils/stark.ts +++ b/src/utils/stark.ts @@ -1,3 +1,5 @@ +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import type { SPEC } from 'starknet-types-07'; import { getStarkKey, utils } from '@scure/starknet'; import { gzip, ungzip } from 'pako'; diff --git a/src/wallet/account.ts b/src/wallet/account.ts index 82d4e3cf6..874bab45b 100644 --- a/src/wallet/account.ts +++ b/src/wallet/account.ts @@ -3,6 +3,8 @@ import { type AddStarknetChainParameters, type NetworkChangeEventHandler, type WatchAssetParameters, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + type SPEC, } from 'starknet-types-07'; import { Account, AccountInterface } from '../account'; diff --git a/src/wallet/connect.ts b/src/wallet/connect.ts index f519546ea..c9be27f49 100644 --- a/src/wallet/connect.ts +++ b/src/wallet/connect.ts @@ -8,6 +8,8 @@ import { type ChainId, type StarknetWindowObject, type TypedData, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + type SPEC, } from 'starknet-types-07'; /** From cfcf93bfaf6bed271cfdf7ff33db027682af0706 Mon Sep 17 00:00:00 2001 From: Toni Tabak Date: Fri, 17 May 2024 16:14:27 +0200 Subject: [PATCH 14/18] Verify message fix (#1128) * fix: verification signature with new braavos account * refactor: change ContractSpecifies name * fix: change to getContractVersion name * fix: change method name in test * fix: implement most of Penovicq comments * fix: handling when cairoversion is known and signatureveriffunctionname is unknown * fix: verifyMessageBraavos PR polution fix * chore: cleanup * chore: cleanup unrelated to the pr but next-version --------- Co-authored-by: PhilippeR26 --- src/account/default.ts | 108 +++++++++++++++++++++++++++++---------- src/account/interface.ts | 4 ++ src/provider/rpc.ts | 13 ++--- 3 files changed, 91 insertions(+), 34 deletions(-) diff --git a/src/account/default.ts b/src/account/default.ts index dc4813c67..2bd428e95 100644 --- a/src/account/default.ts +++ b/src/account/default.ts @@ -111,7 +111,7 @@ export class Account extends Provider implements AccountInterface { } /** - * Retrieves the Cairo version from the network and sets `cairoVersion` if not already set in the constructor + * Retrieves the Cairo version from the network and sets `cairoVersion` if not already set in the constructor. * @param classHash if provided detects Cairo version from classHash, otherwise from the account address */ public async getCairoVersion(classHash?: string) { @@ -541,38 +541,90 @@ export class Account extends Provider implements AccountInterface { return getMessageHash(typedData, this.address); } - public async verifyMessageHash(hash: BigNumberish, signature: Signature): Promise { - try { - const resp = await this.callContract({ - contractAddress: this.address, - entrypoint: 'isValidSignature', - calldata: CallData.compile({ - hash: toBigInt(hash).toString(), - signature: formatSignature(signature), - }), - }); - if (BigInt(resp[0]) === 0n) { - // OpenZeppelin 0.8.0 invalid signature - return false; - } - // OpenZeppelin 0.8.0, ArgentX 0.3.0 & Braavos Cairo 0 valid signature - return true; - } catch (err) { - if ( - ['argent/invalid-signature', 'is invalid, with respect to the public key'].some( - (errMessage) => (err as Error).message.includes(errMessage) - ) - ) { - // ArgentX 0.3.0 invalid signature, Braavos Cairo 0 invalid signature - return false; + public async verifyMessageHash( + hash: BigNumberish, + signature: Signature, + signatureVerificationFunctionName?: string, + signatureVerificationResponse?: { okResponse: string[]; nokResponse: string[]; error: string[] } + ): Promise { + // HOTFIX: Accounts should conform to SNIP-6 + // (https://github.com/starknet-io/SNIPs/blob/f6998f779ee2157d5e1dea36042b08062093b3c5/SNIPS/snip-6.md?plain=1#L61), + // but they don't always conform. Also, the SNIP doesn't standardize the response if the signature isn't valid. + const knownSigVerificationFName = signatureVerificationFunctionName + ? [signatureVerificationFunctionName] + : ['isValidSignature', 'is_valid_signature']; + const knownSignatureResponse = signatureVerificationResponse || { + okResponse: [ + // any non-nok response is true + ], + nokResponse: [ + '0x0', // Devnet + '0x00', // OpenZeppelin 0.7.0 to 0.9.0 invalid signature + ], + error: [ + 'argent/invalid-signature', // ArgentX 0.3.0 to 0.3.1 + 'is invalid, with respect to the public key', // OpenZeppelin until 0.6.1, Braavos 0.0.11 + 'INVALID_SIG', // Braavos 1.0.0 + ], + }; + let error: any; + + // eslint-disable-next-line no-restricted-syntax + for (const SigVerificationFName of knownSigVerificationFName) { + try { + // eslint-disable-next-line no-await-in-loop + const resp = await this.callContract({ + contractAddress: this.address, + entrypoint: SigVerificationFName, + calldata: CallData.compile({ + hash: toBigInt(hash).toString(), + signature: formatSignature(signature), + }), + }); + // Response NOK Signature + if (knownSignatureResponse.nokResponse.includes(resp[0].toString())) { + return false; + } + // Response OK Signature + // Empty okResponse assume all non-nok responses are valid signatures + // OpenZeppelin 0.7.0 to 0.9.0, ArgentX 0.3.0 to 0.3.1 & Braavos Cairo 0.0.11 to 1.0.0 valid signature + if ( + knownSignatureResponse.okResponse.length === 0 || + knownSignatureResponse.okResponse.includes(resp[0].toString()) + ) { + return true; + } + throw Error('signatureVerificationResponse Error: response is not part of known responses'); + } catch (err) { + // Known NOK Errors + if ( + knownSignatureResponse.error.some((errMessage) => + (err as Error).message.includes(errMessage) + ) + ) { + return false; + } + // Unknown Error + error = err; } - throw Error(`Signature verification request is rejected by the network: ${err}`); } + + throw Error(`Signature verification Error: ${error}`); } - public async verifyMessage(typedData: TypedData, signature: Signature): Promise { + public async verifyMessage( + typedData: TypedData, + signature: Signature, + signatureVerificationFunctionName?: string, + signatureVerificationResponse?: { okResponse: string[]; nokResponse: string[]; error: string[] } + ): Promise { const hash = await this.hashMessage(typedData); - return this.verifyMessageHash(hash, signature); + return this.verifyMessageHash( + hash, + signature, + signatureVerificationFunctionName, + signatureVerificationResponse + ); } /* diff --git a/src/account/interface.ts b/src/account/interface.ts index 6b5d1570c..66b308777 100644 --- a/src/account/interface.ts +++ b/src/account/interface.ts @@ -368,6 +368,8 @@ export abstract class AccountInterface extends ProviderInterface { * * @param typedData - JSON object to be verified * @param signature - signature of the JSON object + * @param signatureVerificationFunctionName - optional account contract verification function name override + * @param signatureVerificationResponse - optional response override { okResponse: string[]; nokResponse: string[]; error: string[] } * @returns true if the signature is valid, false otherwise * @throws {Error} if the JSON object is not a valid JSON or the signature is not a valid signature */ @@ -379,6 +381,8 @@ export abstract class AccountInterface extends ProviderInterface { * * @param hash - hash to be verified * @param signature - signature of the hash + * @param signatureVerificationFunctionName - optional account contract verification function name override + * @param signatureVerificationResponse - optional response override { okResponse: string[]; nokResponse: string[]; error: string[] } * @returns true if the signature is valid, false otherwise * @throws {Error} if the signature is not a valid signature */ diff --git a/src/provider/rpc.ts b/src/provider/rpc.ts index 68b2f5396..66ee958e9 100644 --- a/src/provider/rpc.ts +++ b/src/provider/rpc.ts @@ -1,6 +1,4 @@ -import { ProviderInterface } from './interface'; -import { LibraryError } from './errors'; -import { RpcChannel, RPC06, RPC07 } from '../channel'; +import { RPC06, RPC07, RpcChannel } from '../channel'; import { AccountInvocations, BigNumberish, @@ -8,10 +6,12 @@ import { BlockIdentifier, BlockTag, Call, + ContractClassResponse, ContractVersion, DeclareContractTransaction, DeployAccountContractTransaction, GetBlockResponse, + GetTxReceiptResponseWithoutHelper, Invocation, InvocationsDetailsWithNonce, PendingBlock, @@ -25,12 +25,13 @@ import { getEstimateFeeBulkOptions, getSimulateTransactionOptions, waitForTransactionOptions, - GetTxReceiptResponseWithoutHelper, } from '../types'; import { getAbiContractVersion } from '../utils/calldata/cairo'; import { isSierra } from '../utils/contract'; import { RPCResponseParser } from '../utils/responseParser/rpc'; -import { ReceiptTx, GetTransactionReceiptResponse } from '../utils/transactionReceipt'; +import { GetTransactionReceiptResponse, ReceiptTx } from '../utils/transactionReceipt'; +import { LibraryError } from './errors'; +import { ProviderInterface } from './interface'; export class RpcProvider implements ProviderInterface { private responseParser: RPCResponseParser; @@ -248,7 +249,7 @@ export class RpcProvider implements ProviderInterface { compiler = true, }: getContractVersionOptions = {} ): Promise { - let contractClass; + let contractClass: ContractClassResponse; if (contractAddress) { contractClass = await this.getClassAt(contractAddress, blockIdentifier); } else if (classHash) { From 57e4e17f5e86792b13b4ce291a9b538fdd49184a Mon Sep 17 00:00:00 2001 From: Philippe ROSTAN <81040730+PhilippeR26@users.noreply.github.com> Date: Fri, 17 May 2024 21:05:06 +0200 Subject: [PATCH 15/18] Remove all code for Goerli testnet (#1122) * test: eth signer * test: move secp256k1Point tests in a dedicated test file * feat: helper for transaction receipt * simplify extends for account class * feat: handling of cairo u512 type * refactor: change name of variable : GetTxReceiptResponseWithoutHelper * fix: double lines for same imports * fix: solve an error in validate.ts initiated by pr 1007 * fix: correction of a word in guide * docs: validateChecksumAddress * fix: jsdoc correction * docs: add tsdoc in utils/address.ts * test: add extra fees * fix: estimateFeeBulk include skipValidate in accountInvocationsFactory * feat: add type guard to receipt response status methods * fix: repair i128 typed data encoding and add typed data range checks * chore: update left over StarkNet casing * feat: bundle resolution, module, type import for walletacc * feat: bundle resolution, module, type import for walletaccount * chore: fix connect import * chore: add get-starknet-core next as dependencie * chore: import fix * fix: estimateMessageFee - eth address format (#1040) * fix: estimatemessagefee eth address format * fix: implement requests * docs: small guides cleanup (#1048) * docs: fix nodeUrl code typo (#1046) * docs: small guides cleanup --------- Co-authored-by: Joel Mun * fix(RpcProvider): allow client to provide `specVersion` in 0.7 provider this saves an extra call on RPC for optionally-known information (like the `chainId` case). also fixed speck -> spec typo * fix: remove abis parameter from signer and account execute * feat: configure u512 and Secp256k1Point for abiwan * chore: bump dependencies * chore: expose data gas consumed and data gas price for 0.7 rpc * refactor: remove all code for Goerli testnet * feat: add provider.waitForBlock * test: bump ci tests to devnet-rs v0.0.5 * fix: solve conflict --------- Co-authored-by: gregory <10611760+gregoryguillou@users.noreply.github.com> Co-authored-by: Toni Tabak Co-authored-by: ivpavici Co-authored-by: Petar Penovic Co-authored-by: Joel Mun Co-authored-by: Abraham Makovetsky Co-authored-by: Haroune Mohammedi Co-authored-by: Dhruv Kelawala --- .github/workflows/_test.yml | 2 +- .github/workflows/manual-tests-testnet.yml | 2 +- .github/workflows/pr-push-main.yml | 2 +- CONTRIBUTING.md | 4 +- .../helloCairo2/{hellocairo => hello.cairo} | 0 __tests__/account.starknetId.test.ts | 56 ++++----- __tests__/account.test.ts | 56 ++++++--- __tests__/cairo1.test.ts | 64 ---------- __tests__/cairo1v2.test.ts | 2 +- __tests__/config/fixtures.ts | 20 +++- __tests__/config/helpers/strategyResolver.ts | 21 ++-- __tests__/rpcProvider.test.ts | 69 +++++++++-- __tests__/utils/ellipticalCurve.test.ts | 8 +- __tests__/utils/ethSigner.test.ts | 112 ++++++++++-------- __tests__/utils/starknetId.test.ts | 4 +- __tests__/utils/transactionHash.test.ts | 16 +-- package-lock.json | 6 + package.json | 1 + src/constants.ts | 7 -- src/provider/rpc.ts | 43 +++++++ src/utils/num.ts | 69 +++++++++++ src/utils/selector.ts | 11 ++ src/utils/starknetId.ts | 23 ---- 23 files changed, 367 insertions(+), 231 deletions(-) rename __mocks__/cairo/helloCairo2/{hellocairo => hello.cairo} (100%) diff --git a/.github/workflows/_test.yml b/.github/workflows/_test.yml index 256ecf8b4..ec24c59d6 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.0.4-seed0' || '' }} + image: ${{ (inputs.use-devnet) && 'shardlabs/starknet-devnet-rs:0.0.5-seed0' || '' }} ports: - 5050:5050 diff --git a/.github/workflows/manual-tests-testnet.yml b/.github/workflows/manual-tests-testnet.yml index 65d0c9d2f..234d61943 100644 --- a/.github/workflows/manual-tests-testnet.yml +++ b/.github/workflows/manual-tests-testnet.yml @@ -7,7 +7,7 @@ jobs: strategy: max-parallel: 1 matrix: - name: [rpc-goerli] + name: [rpc-sepolia] uses: ./.github/workflows/_test.yml secrets: diff --git a/.github/workflows/pr-push-main.yml b/.github/workflows/pr-push-main.yml index ba64e23a6..d39d245ea 100644 --- a/.github/workflows/pr-push-main.yml +++ b/.github/workflows/pr-push-main.yml @@ -40,7 +40,7 @@ jobs: strategy: max-parallel: 1 matrix: - name: [rpc-goerli] + name: [rpc-sepolia] uses: ./.github/workflows/_test.yml secrets: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9f915e625..63f20a94f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -44,8 +44,8 @@ By default the tests are executed in your local Devnet and everything should run If you want to use a specific RPC node, you have to set some global variables before executing the tests: ```bash -export TEST_RPC_URL=http://192.168.1.44:9545/rpc/v0.5 # example of a Pathfinder node located in your local network -export TEST_RPC_URL=https://starknet-testnet.public.blastapi.io/rpc/v0.5 # example of a public testnet node +export TEST_RPC_URL=http://192.168.1.44:9545/rpc/v0_7 # example of a Pathfinder node located in your local network +export TEST_RPC_URL=https://starknet-sepolia.public.blastapi.io/rpc/v0_7 # example of a public Sepolia testnet node export TEST_ACCOUNT_ADDRESS=0x065A822f0000000000000000000000000c26641 export TEST_ACCOUNT_PRIVATE_KEY=0x02a80000000000000000000000001754438a ``` diff --git a/__mocks__/cairo/helloCairo2/hellocairo b/__mocks__/cairo/helloCairo2/hello.cairo similarity index 100% rename from __mocks__/cairo/helloCairo2/hellocairo rename to __mocks__/cairo/helloCairo2/hello.cairo diff --git a/__tests__/account.starknetId.test.ts b/__tests__/account.starknetId.test.ts index 15f17fd85..2529c6199 100644 --- a/__tests__/account.starknetId.test.ts +++ b/__tests__/account.starknetId.test.ts @@ -24,46 +24,34 @@ describe('deploy and test Wallet', () => { beforeAll(async () => { // Deploy Starknet id contract - const idResponse = await account.declareAndDeploy( - { - contract: compiledStarknetId, - casm: compiledStarknetIdCasm, - constructorCalldata: [account.address, 0], - }, - { maxFee: 1e18 } - ); + const idResponse = await account.declareAndDeploy({ + contract: compiledStarknetId, + casm: compiledStarknetIdCasm, + constructorCalldata: [account.address, 0], + }); identityAddress = idResponse.deploy.contract_address; // Deploy pricing contract - const pricingResponse = await account.declareAndDeploy( - { - contract: compiledPricing, - casm: compiledPricingCasm, - constructorCalldata: [devnetERC20Address], - }, - { maxFee: 1e18 } - ); + const pricingResponse = await account.declareAndDeploy({ + contract: compiledPricing, + casm: compiledPricingCasm, + constructorCalldata: [devnetERC20Address], + }); const pricingAddress = pricingResponse.deploy.contract_address; // Deploy naming contract - const namingResponse = await account.declareAndDeploy( - { - contract: compiledNaming, - casm: compiledNamingCasm, - constructorCalldata: [identityAddress, pricingAddress, 0, account.address], - }, - { maxFee: 1e18 } - ); + const namingResponse = await account.declareAndDeploy({ + contract: compiledNaming, + casm: compiledNamingCasm, + constructorCalldata: [identityAddress, pricingAddress, 0, account.address], + }); namingAddress = namingResponse.deploy.contract_address; // Deploy multicall contract - const multicallResponse = await account.declareAndDeploy( - { - contract: compiledSidMulticall, - casm: compiledSidMulticallCasm, - }, - { maxFee: 1e18 } - ); + const multicallResponse = await account.declareAndDeploy({ + contract: compiledSidMulticall, + casm: compiledSidMulticallCasm, + }); multicallAddress = multicallResponse.deploy.contract_address; const { transaction_hash } = await account.execute( @@ -97,8 +85,7 @@ describe('deploy and test Wallet', () => { calldata: ['1'], }, ], - undefined, - { maxFee: 1e18 } + undefined ); await provider.waitForTransaction(transaction_hash); @@ -130,8 +117,7 @@ describe('deploy and test Wallet', () => { ], }, ], - undefined, - { maxFee: 1e18 } + undefined ); await provider.waitForTransaction(transaction_hash_verifier); }); diff --git a/__tests__/account.test.ts b/__tests__/account.test.ts index 8ad3849c0..399ac13b0 100644 --- a/__tests__/account.test.ts +++ b/__tests__/account.test.ts @@ -375,8 +375,12 @@ describe('deploy and test Wallet', () => { }; const details = { maxFee: 0n }; - await expect(account.execute(transaction, details)).rejects.toThrow(/zero/); - await expect(account.execute(transaction, undefined, details)).rejects.toThrow(/zero/); + await expect(account.execute(transaction, details)).rejects.toThrow( + /zero|Transaction must commit to pay a positive amount on fee./ + ); + await expect(account.execute(transaction, undefined, details)).rejects.toThrow( + /zero|Transaction must commit to pay a positive amount on fee./ + ); }); test('execute with custom nonce', async () => { @@ -415,28 +419,44 @@ describe('deploy and test Wallet', () => { expect(toBigInt(response.number as string).toString()).toStrictEqual('57'); }); - test('sign and verify EIP712 message fail', async () => { - const signature = await account.signMessage(typedDataExample); - const [r, s] = stark.formatSignature(signature); + describeIfDevnet('EIP712 verification', () => { + // currently only in Devnet-rs, because can fail in Sepolia. + // to test in all cases once PR#989 implemented. + test('sign and verify EIP712 message fail', async () => { + const signature = await account.signMessage(typedDataExample); + const [r, s] = stark.formatSignature(signature); - // change the signature to make it invalid - const r2 = toBigInt(r) + 123n; + // change the signature to make it invalid + const r2 = toBigInt(r) + 123n; - const signature2 = new Signature(toBigInt(r2.toString()), toBigInt(s)); + const signature2 = new Signature(toBigInt(r2.toString()), toBigInt(s)); - if (!signature2) return; + if (!signature2) return; - const verifMessageResponse: boolean = await account.verifyMessage(typedDataExample, signature2); - expect(verifMessageResponse).toBe(false); + const verifMessageResponse: boolean = await account.verifyMessage( + typedDataExample, + signature2 + ); + expect(verifMessageResponse).toBe(false); - const wrongAccount = new Account(provider, '0x037891', '0x026789', undefined, TEST_TX_VERSION); // non existing account - await expect(wrongAccount.verifyMessage(typedDataExample, signature2)).rejects.toThrow(); - }); + const wrongAccount = new Account( + provider, + '0x037891', + '0x026789', + undefined, + TEST_TX_VERSION + ); // non existing account + await expect(wrongAccount.verifyMessage(typedDataExample, signature2)).rejects.toThrow(); + }); - test('sign and verify message', async () => { - const signature = await account.signMessage(typedDataExample); - const verifMessageResponse: boolean = await account.verifyMessage(typedDataExample, signature); - expect(verifMessageResponse).toBe(true); + test('sign and verify message', async () => { + const signature = await account.signMessage(typedDataExample); + const verifMessageResponse: boolean = await account.verifyMessage( + typedDataExample, + signature + ); + expect(verifMessageResponse).toBe(true); + }); }); describe('Contract interaction with Account', () => { diff --git a/__tests__/cairo1.test.ts b/__tests__/cairo1.test.ts index 87680558a..20f636226 100644 --- a/__tests__/cairo1.test.ts +++ b/__tests__/cairo1.test.ts @@ -1,4 +1,3 @@ -import type { Abi } from 'abi-wan-kanabi'; import { type BigNumberish, type Calldata, @@ -26,7 +25,6 @@ import { compiledHelloSierra, compiledHelloSierraCasm, describeIfDevnet, - describeIfTestnet, getTestAccount, getTestProvider, } from './config/fixtures'; @@ -549,65 +547,3 @@ describeIfDevnet('Cairo 1 Devnet', () => { }); }); }); - -describeIfTestnet('Testnet', () => { - describe('TS validation for testnet', () => { - const provider = getTestProvider(); - const account = getTestAccount(provider); - const classHash: any = '0x022332bb9c1e22ae13ae7fd9f3101eced4644533c6bfe51a25cf8dea028e5045'; - const contractAddress: any = - '0x00305ef61e86F4566b8726d8867EF252d4f37F4B6418Cad4288052738ee22A5d'; - let cairo1Contract: Contract; - initializeMatcher(expect); - - beforeAll(async () => { - const cairoClass = await provider.getClassByHash(classHash); - // TODO: Fix typing and responses for abi - cairo1Contract = new Contract(cairoClass.abi as Abi, contractAddress, account); - }); - - test('GetClassByHash', async () => { - const classResponse = await provider.getClassByHash(classHash); - expect(classResponse).toMatchSchemaRef('SierraContractClass'); - }); - - test('GetClassAt', async () => { - const classResponse = await provider.getClassAt(contractAddress); - expect(classResponse).toMatchSchemaRef('SierraContractClass'); - }); - - test('Cairo 1 Contract Interaction - felt252', async () => { - const result = await cairo1Contract.test_felt252(100); - expect(result).toBe(101n); - }); - - test('Cairo 1 Contract Interaction - uint 8, 16, 32, 64, 128', async () => { - let result = await cairo1Contract.test_u8(100n); - expect(result).toBe(107n); - result = await cairo1Contract.test_u16(100n); - expect(result).toBe(106n); - result = await cairo1Contract.test_u32(100n); - expect(result).toBe(104n); - result = await cairo1Contract.test_u64(255n); - expect(result).toBe(258n); - result = await cairo1Contract.test_u128(255n); - expect(result).toBe(257n); - }); - - test('Cairo 1 - uint256 struct', async () => { - const myUint256 = uint256(2n ** 256n - 2n); - const result = await cairo1Contract.test_u256(myUint256); - expect(result).toBe(2n ** 256n - 1n); - }); - - test('Cairo 1 - uint256 by a bignumber', async () => { - const result = await cairo1Contract.test_u256(2n ** 256n - 2n); - expect(result).toBe(2n ** 256n - 1n); - }); - - test('Cairo 1 Contract Interaction - bool', async () => { - const tx = await cairo1Contract.test_bool(true); - expect(tx).toBe(true); - }); - }); -}); diff --git a/__tests__/cairo1v2.test.ts b/__tests__/cairo1v2.test.ts index a83685d0c..a246d4cd6 100644 --- a/__tests__/cairo1v2.test.ts +++ b/__tests__/cairo1v2.test.ts @@ -1174,7 +1174,7 @@ describe('Cairo 1', () => { describe('Cairo2.6.0 Sierra1.5.0', () => { test('declare Sierra 1.5.0', async () => { - const declare260Response = await account.declare({ + const declare260Response = await account.declareIfNot({ contract: compiledC260, casm: compiledC260Casm, }); diff --git a/__tests__/config/fixtures.ts b/__tests__/config/fixtures.ts index bb8132383..8314031b9 100644 --- a/__tests__/config/fixtures.ts +++ b/__tests__/config/fixtures.ts @@ -10,6 +10,7 @@ import { } from '../../src/types'; import { ETransactionVersion } from '../../src/types/api'; import { toHex } from '../../src/utils/num'; +import { wait } from '../../src/utils/provider'; const readContract = (name: string): LegacyCompiledContract => json.parse( @@ -108,11 +109,28 @@ export const createBlockForDevnet = async (): Promise => { await fetch(new URL('/create_block', process.env.TEST_RPC_URL), { method: 'POST' }); }; +export async function waitNextBlock(provider: RpcProvider, delay: number) { + const initBlock = await provider.getBlockNumber(); + createBlockForDevnet(); + let isNewBlock: boolean = false; + while (!isNewBlock) { + // eslint-disable-next-line no-await-in-loop + const currentBlock = await provider.getBlockNumber(); + if (currentBlock !== initBlock) { + isNewBlock = true; + } else { + // eslint-disable-next-line no-await-in-loop + await wait(delay); + } + } +} + const describeIf = (condition: boolean) => (condition ? describe : describe.skip); export const describeIfRpc = describeIf(process.env.IS_RPC === 'true'); export const describeIfNotDevnet = describeIf(process.env.IS_DEVNET === 'false'); export const describeIfDevnet = describeIf(process.env.IS_DEVNET === 'true'); export const describeIfTestnet = describeIf(process.env.IS_TESTNET === 'true'); - export const erc20ClassHash = '0x54328a1075b8820eb43caf0caa233923148c983742402dcfc38541dd843d01a'; export const wrongClassHash = '0x000000000000000000000000000000000000000000000000000000000000000'; +export const devnetETHtokenAddress = + '0x49D36570D4E46F48E99674BD3FCC84644DDD6B96F7C741B1562B82F9E004DC7'; diff --git a/__tests__/config/helpers/strategyResolver.ts b/__tests__/config/helpers/strategyResolver.ts index c30155cb3..d729a130e 100644 --- a/__tests__/config/helpers/strategyResolver.ts +++ b/__tests__/config/helpers/strategyResolver.ts @@ -2,7 +2,7 @@ import accountResolver from './accountResolver'; import { GS_DEFAULT_TEST_PROVIDER_URL, LOCAL_DEVNET_NOT_RUNNING_MESSAGE } from '../constants'; import { setIfNullish } from './env'; -import { BaseUrl } from '../../../src/constants'; +import { RpcProvider } from '../../../src'; class StrategyResolver { private isDevnet = false; @@ -14,7 +14,12 @@ class StrategyResolver { } get isTestnet() { - return process.env.TEST_RPC_URL?.includes(BaseUrl.SN_SEPOLIA); + const provider = new RpcProvider({ nodeUrl: process.env.TEST_RPC_URL }); + const isTestnetSepolia = provider + .getTransactionByHash('0x28dfc05eb4f261b37ddad451ff22f1d08d4e3c24dc646af0ec69fa20e096819') // one random existing Sepolia transaction hash + .then(() => true) + .catch(() => false); + return isTestnetSepolia; } get hasAllAccountEnvs() { @@ -47,7 +52,7 @@ class StrategyResolver { setIfNullish('IS_DEVNET', this.isRpcDevnet); } - resolveRpc(): void { + async resolveRpc(): Promise { const hasRpcUrl = !!process.env.TEST_RPC_URL; this.isRpcNode = hasRpcUrl || this.isDevnet; @@ -57,7 +62,7 @@ class StrategyResolver { } setIfNullish('IS_RPC', this.isRpcNode); - setIfNullish('IS_TESTNET', this.isTestnet); + setIfNullish('IS_TESTNET', await this.isTestnet); console.log('Detected RPC'); } @@ -94,10 +99,10 @@ class StrategyResolver { } } - private useProvidedSetup(): void { + private async useProvidedSetup(): Promise { setIfNullish('IS_DEVNET', false); setIfNullish('IS_RPC', !!process.env.TEST_RPC_URL); - setIfNullish('IS_TESTNET', this.isTestnet); + setIfNullish('IS_TESTNET', await this.isTestnet); this.logConfigInfo(); @@ -110,7 +115,7 @@ class StrategyResolver { this.verifyAccountData(); if (this.hasAllAccountEnvs) { - this.useProvidedSetup(); + await this.useProvidedSetup(); return; } @@ -118,7 +123,7 @@ class StrategyResolver { console.log('Basic test parameters are missing, Auto Setup Started'); await this.detectDevnet(); - this.resolveRpc(); + await this.resolveRpc(); await accountResolver.execute(this.isDevnet); this.verifyAccountData(true); diff --git a/__tests__/rpcProvider.test.ts b/__tests__/rpcProvider.test.ts index 6aa109f3d..18d4f421e 100644 --- a/__tests__/rpcProvider.test.ts +++ b/__tests__/rpcProvider.test.ts @@ -11,6 +11,7 @@ import { ReceiptTx, RpcProvider, TransactionExecutionStatus, + cairo, stark, waitForTransactionOptions, } from '../src'; @@ -29,6 +30,8 @@ import { describeIfDevnet, getTestAccount, getTestProvider, + waitNextBlock, + devnetETHtokenAddress, } from './config/fixtures'; import { initializeMatcher } from './config/schema'; @@ -115,20 +118,15 @@ describeIfRpc('RPCProvider', () => { estimateSpy.mockRestore(); }); - describe('Test Estimate message fee', () => { + describeIfDevnet('Test Estimate message fee Cairo 0', () => { + // declaration of Cairo 0 contract is no more authorized in Sepolia Testnet let l1l2ContractCairo0Address: string; - let l1l2ContractCairo1Address: string; beforeAll(async () => { const { deploy } = await account.declareAndDeploy({ contract: compiledL1L2, }); l1l2ContractCairo0Address = deploy.contract_address; - const { deploy: deploy2 } = await account.declareAndDeploy({ - contract: compiledC1v2, - casm: compiledC1v2Casm, - }); - l1l2ContractCairo1Address = deploy2.contract_address; }); test('estimate message fee Cairo 0', async () => { @@ -147,6 +145,19 @@ describeIfRpc('RPCProvider', () => { }) ); }); + }); + + describe('Test Estimate message fee Cairo 1', () => { + let l1l2ContractCairo1Address: string; + + beforeAll(async () => { + const { deploy: deploy2 } = await account.declareAndDeploy({ + contract: compiledC1v2, + casm: compiledC1v2Casm, + }); + l1l2ContractCairo1Address = deploy2.contract_address; + await waitNextBlock(provider as RpcProvider, 5000); // in Sepolia Testnet, needs pending block validation before interacting + }); test('estimate message fee Cairo 1', async () => { const L1_ADDRESS = '0x8359E4B0152ed5A731162D3c7B0D8D56edB165'; // not coded in 20 bytes @@ -230,6 +241,16 @@ describeIfRpc('RPCProvider', () => { let latestBlock: Block; beforeAll(async () => { + // add a Tx to be sure to have at least one Tx in the last block + const { transaction_hash } = await account.execute({ + contractAddress: devnetETHtokenAddress, + entrypoint: 'transfer', + calldata: { + recipient: account.address, + amount: cairo.uint256(1n * 10n ** 4n), + }, + }); + await account.waitForTransaction(transaction_hash); latestBlock = await provider.getBlock('latest'); }); @@ -408,3 +429,37 @@ describeIfRpc('RPCProvider', () => { }); }); }); + +describeIfNotDevnet('waitForBlock', () => { + // As Devnet-rs isn't generating automatically blocks at a periodic time, it's excluded of this test. + const providerStandard = new RpcProvider({ nodeUrl: process.env.TEST_RPC_URL }); + const providerFastTimeOut = new RpcProvider({ nodeUrl: process.env.TEST_RPC_URL, retries: 1 }); + let block: number; + beforeEach(async () => { + block = await providerStandard.getBlockNumber(); + }); + + test('waitForBlock timeOut', async () => { + await expect(providerFastTimeOut.waitForBlock(10 ** 20, 1)).rejects.toThrow(/timed-out/); + }); + + test('waitForBlock in the past', async () => { + const start = new Date().getTime(); + await providerStandard.waitForBlock(block); + const end = new Date().getTime(); + expect(end - start).toBeLessThan(1000); // quick answer expected + }); + + test('waitForBlock latest', async () => { + const start = new Date().getTime(); + await providerStandard.waitForBlock('latest'); + const end = new Date().getTime(); + expect(end - start).toBeLessThan(100); // nearly immediate answer expected + }); + + // NOTA : this test can have a duration up to block interval. + test('waitForBlock pending', async () => { + await providerStandard.waitForBlock('pending'); + expect(true).toBe(true); // answer without timeout Error (blocks have to be spaced with 16 minutes maximum : 200 retries * 5000ms) + }); +}); diff --git a/__tests__/utils/ellipticalCurve.test.ts b/__tests__/utils/ellipticalCurve.test.ts index 92cb348a0..034ea6439 100644 --- a/__tests__/utils/ellipticalCurve.test.ts +++ b/__tests__/utils/ellipticalCurve.test.ts @@ -53,21 +53,21 @@ test('hashMessage()', () => { BigInt(constants.TRANSACTION_VERSION.V1), calldata, maxFee, - StarknetChainId.SN_GOERLI, + StarknetChainId.SN_SEPOLIA, nonce ); expect(hashMsg).toMatchInlineSnapshot( - `"0x6d1706bd3d1ba7c517be2a2a335996f63d4738e2f182144d078a1dd9997062e"` + `"0xa006ce6da518722c1af8bdb1d8a42cee638102c670bb1a55f063bff10506d4"` ); const { r, s } = ec.starkCurve.sign(hashMsg, privateKey); expect(r.toString()).toMatchInlineSnapshot( - `"1427981024487605678086498726488552139932400435436186597196374630267616399345"` + `"384207128292005766686294801921397180350977625816434242436096267488258549139"` ); expect(s.toString()).toMatchInlineSnapshot( - `"1853664302719670721837677288395394946745467311923401353018029119631574115563"` + `"2521602681140573534692734854765316415611209530542226558354401890884906162365"` ); }); diff --git a/__tests__/utils/ethSigner.test.ts b/__tests__/utils/ethSigner.test.ts index a4b39652d..37d4a684d 100644 --- a/__tests__/utils/ethSigner.test.ts +++ b/__tests__/utils/ethSigner.test.ts @@ -9,9 +9,11 @@ import { cairo, encode, eth, + extractContractHashes, hash, num, stark, + type DeclareContractPayload, } from '../../src'; import { validateAndParseEthAddress } from '../../src/utils/eth'; import { ETransactionVersion } from '../../src/types/api'; @@ -25,6 +27,8 @@ import { compiledEthCasm, compiledEthPubk, compiledEthPubkCasm, + describeIfDevnet, + devnetETHtokenAddress, getTestAccount, getTestProvider, } from '../config/fixtures'; @@ -93,11 +97,10 @@ describe('Ethereum signer', () => { }); }); - describe('ETH account tx V2', () => { + describeIfDevnet('ETH account tx V2', () => { + // devnet only because estimateFee in Sepolia v0.13.1 are producing widely different numbers. const provider = new Provider(getTestProvider()); const account = getTestAccount(provider); - const devnetETHtokenAddress = - '0x49D36570D4E46F48E99674BD3FCC84644DDD6B96F7C741B1562B82F9E004DC7'; let ethAccount: Account; beforeAll(async () => { const { transaction_hash: declTH, class_hash: decClassHash } = await account.declareIfNot({ @@ -107,7 +110,7 @@ describe('Ethereum signer', () => { if (declTH) { await provider.waitForTransaction(declTH); } - const privateKeyETH = '0x45397ee6ca34cb49060f1c303c6cb7ee2d6123e617601ef3e31ccf7bf5bef1f9'; + const privateKeyETH = eth.ethRandomPrivateKey(); const ethSigner = new EthSigner(privateKeyETH); const ethFullPublicKey = await ethSigner.getPubKey(); const pubKeyETHx = cairo.uint256( @@ -125,28 +128,28 @@ describe('Ethereum signer', () => { 0 ); + ethAccount = new Account(provider, contractETHAccountAddress, ethSigner); + const deployPayload = { + classHash: decClassHash, + constructorCalldata: accountETHconstructorCalldata, + addressSalt: salt, + }; + const { suggestedMaxFee: feeDeploy } = + await ethAccount.estimateAccountDeployFee(deployPayload); // fund account with ETH const { transaction_hash } = await account.execute({ contractAddress: devnetETHtokenAddress, entrypoint: 'transfer', calldata: { recipient: contractETHAccountAddress, - amount: cairo.uint256(5 * 10 ** 17), + amount: cairo.uint256(3n * 10n ** 16n), }, }); await account.waitForTransaction(transaction_hash); - ethAccount = new Account(provider, contractETHAccountAddress, ethSigner); - const deployPayload = { - classHash: decClassHash, - constructorCalldata: accountETHconstructorCalldata, - addressSalt: salt, - }; - const { suggestedMaxFee: feeDeploy } = - await ethAccount.estimateAccountDeployFee(deployPayload); const { transaction_hash: txH2, contract_address } = await ethAccount.deployAccount( deployPayload, - { maxFee: stark.estimatedFeeToMaxFee(feeDeploy, 100) } + { maxFee: stark.estimatedFeeToMaxFee(feeDeploy, 300) } ); await provider.waitForTransaction(txH2); expect(contract_address).toBe(contractETHAccountAddress); @@ -156,11 +159,10 @@ describe('Ethereum signer', () => { const ethContract2 = new Contract(compiledErc20.abi, devnetETHtokenAddress, ethAccount); const respTransfer = await ethContract2.transfer( account.address, - cairo.uint256(2 * 10 ** 16), + cairo.uint256(1 * 10 ** 4), { maxFee: 1 * 10 ** 16 } ); const txR = await provider.waitForTransaction(respTransfer.transaction_hash); - if (txR.isSuccess()) { expect(txR.execution_status).toBe('SUCCEEDED'); } else { @@ -185,7 +187,8 @@ describe('Ethereum signer', () => { }); }); - describe('ETH account tx V3', () => { + describeIfDevnet('ETH account tx V3', () => { + // devnet only because estimateFee in Sepolia v0.13.1 are producing widely different numbers. const provider = new Provider(getTestProvider()); const account = getTestAccount(provider); const devnetSTRKtokenAddress = @@ -199,7 +202,7 @@ describe('Ethereum signer', () => { if (declTH) { await provider.waitForTransaction(declTH); } - const privateKeyETH = '0x525bc68475c0955fae83869beec0996114d4bb27b28b781ed2a20ef23121b8de'; + const privateKeyETH = eth.ethRandomPrivateKey(); const ethSigner = new EthSigner(privateKeyETH); const ethFullPublicKey = await ethSigner.getPubKey(); const pubKeyETHx = cairo.uint256( @@ -218,17 +221,6 @@ describe('Ethereum signer', () => { 0 ); - // fund account with STRK - const { transaction_hash } = await account.execute({ - contractAddress: devnetSTRKtokenAddress, - entrypoint: 'transfer', - calldata: { - recipient: contractETHAccountAddress, - amount: cairo.uint256(5 * 10 ** 17), - }, - }); - await account.waitForTransaction(transaction_hash); - ethAccount = new Account( provider, contractETHAccountAddress, @@ -241,6 +233,16 @@ describe('Ethereum signer', () => { addressSalt: salt, constructorCalldata: accountETHconstructorCalldata, }); + // fund account with STRK + const { transaction_hash } = await account.execute({ + contractAddress: devnetSTRKtokenAddress, + entrypoint: 'transfer', + calldata: { + recipient: contractETHAccountAddress, + amount: cairo.uint256(30n * 10n ** 16n), // 0.3 STRK + }, + }); + await account.waitForTransaction(transaction_hash); const { transaction_hash: txH2, contract_address } = await ethAccount.deployAccount( { classHash: decClassHash, @@ -253,7 +255,7 @@ describe('Ethereum signer', () => { l1_gas: { max_amount: num.toHex(BigInt(feeEstimation.resourceBounds.l1_gas.max_amount) * 2n), max_price_per_unit: num.toHex( - BigInt(feeEstimation.resourceBounds.l1_gas.max_price_per_unit) * 2n + BigInt(feeEstimation.resourceBounds.l1_gas.max_price_per_unit) ), }, }, @@ -264,22 +266,18 @@ describe('Ethereum signer', () => { }); test('ETH account transaction V3', async () => { - const ethContract2 = new Contract(compiledErc20.abi, devnetSTRKtokenAddress, ethAccount); - const txCallData = ethContract2.populate('transfer', [ + const strkContract2 = new Contract(compiledErc20.abi, devnetSTRKtokenAddress, ethAccount); + const txCallData = strkContract2.populate('transfer', [ account.address, - cairo.uint256(1 * 10 ** 15), + cairo.uint256(1 * 10 ** 4), ]); const feeTransfer = await ethAccount.estimateInvokeFee(txCallData); const respTransfer = await ethAccount.execute(txCallData, undefined, { resourceBounds: { l2_gas: { max_amount: '0x0', max_price_per_unit: '0x0' }, l1_gas: { - max_amount: num.toHex( - stark.estimatedFeeToMaxFee(feeTransfer.resourceBounds.l1_gas.max_amount, 150) - ), - max_price_per_unit: num.toHex( - stark.estimatedFeeToMaxFee(feeTransfer.resourceBounds.l1_gas.max_price_per_unit, 150) - ), + max_amount: num.toHex(BigInt(feeTransfer.resourceBounds.l1_gas.max_amount) * 3n), + max_price_per_unit: num.toHex(feeTransfer.resourceBounds.l1_gas.max_price_per_unit), }, }, }); @@ -295,30 +293,48 @@ describe('Ethereum signer', () => { test('ETH account declaration V3', async () => { const accountTestSierra = compiledDummy2Eth; const accountTestCasm = compiledDummy2EthCasm; - const feeDeclare = await ethAccount.estimateDeclareFee({ + const payload: DeclareContractPayload = { contract: accountTestSierra, casm: accountTestCasm, - }); - const { transaction_hash: declTH2, class_hash: decClassHash2 } = - await ethAccount.declareIfNot( - { contract: accountTestSierra, casm: accountTestCasm }, + }; + const declareContractPayload = extractContractHashes(payload); + try { + await provider.getClassByHash(declareContractPayload.classHash); + expect(true).toBeTruthy(); // test skipped if class already declared + } catch { + const feeDeclare = await ethAccount.estimateDeclareFee(payload); + const { transaction_hash: declTH2, class_hash: decClassHash2 } = await ethAccount.declare( + payload, { resourceBounds: { l2_gas: { max_amount: '0x0', max_price_per_unit: '0x0' }, l1_gas: { max_amount: num.toHex(BigInt(feeDeclare.resourceBounds.l1_gas.max_amount) * 2n), max_price_per_unit: num.toHex( - BigInt(feeDeclare.resourceBounds.l1_gas.max_price_per_unit) * 2n + BigInt(feeDeclare.resourceBounds.l1_gas.max_price_per_unit) ), }, }, } ); - if (declTH2) { await provider.waitForTransaction(declTH2); + expect(decClassHash2).toBe( + '0x5d574bd1467f1ca5178c118be7cdb3e74718c37bae90ab686a9b8536ca24436' + ); } - expect(decClassHash2).toBe( - '0x5d574bd1467f1ca5178c118be7cdb3e74718c37bae90ab686a9b8536ca24436' + }); + }); + describe('Ethereum address', () => { + test('Eth address format', async () => { + const ethAddr = '0x8359E4B0152ed5A731162D3c7B0D8D56edB165'; // not a valid 20 bytes ETh address + expect(validateAndParseEthAddress(ethAddr)).toBe( + '0x008359e4b0152ed5a731162d3c7b0d8d56edb165' + ); + expect(validateAndParseEthAddress(BigInt(ethAddr))).toBe( + '0x008359e4b0152ed5a731162d3c7b0d8d56edb165' + ); + expect(validateAndParseEthAddress(BigInt(ethAddr).toString(10))).toBe( + '0x008359e4b0152ed5a731162d3c7b0d8d56edb165' ); }); }); diff --git a/__tests__/utils/starknetId.test.ts b/__tests__/utils/starknetId.test.ts index bdd080620..43e8727cd 100644 --- a/__tests__/utils/starknetId.test.ts +++ b/__tests__/utils/starknetId.test.ts @@ -40,8 +40,8 @@ describe('Should tets StarknetId utils', () => { }); test('Should test getStarknetIdContract', () => { - expect(getStarknetIdContract(StarknetChainId.SN_GOERLI)).toBe( - '0x3bab268e932d2cecd1946f100ae67ce3dff9fd234119ea2f6da57d16d29fce' + expect(getStarknetIdContract(StarknetChainId.SN_SEPOLIA)).toBe( + '0x0707f09bc576bd7cfee59694846291047e965f4184fe13dac62c56759b3b6fa7' ); expect(getStarknetIdContract(StarknetChainId.SN_MAIN)).toBe( diff --git a/__tests__/utils/transactionHash.test.ts b/__tests__/utils/transactionHash.test.ts index 9d2cd5c99..52add1758 100644 --- a/__tests__/utils/transactionHash.test.ts +++ b/__tests__/utils/transactionHash.test.ts @@ -11,9 +11,9 @@ describe('TxV2 Hash Tests', () => { '0x64', [], '0x0', - constants.StarknetChainId.SN_GOERLI + constants.StarknetChainId.SN_SEPOLIA ); - expect(result).toBe('0x7d260744de9d8c55e7675a34512d1951a7b262c79e685d26599edd2948de959'); + expect(result).toBe('0x63ba2bc7f3a3912597e221d5fad8eb0783e0684a428b47fa4737faf66f46dfb'); }); }); }); @@ -93,7 +93,7 @@ describe('TxV3 Hash Tests', () => { '0x276faadb842bfcbba834f3af948386a2eb694f7006e118ad6c80305791d3247', '0x613816405e6334ab420e53d4b38a0451cb2ebca2755171315958c87d303cf6', ], - constants.StarknetChainId.SN_GOERLI, + constants.StarknetChainId.SN_SEPOLIA, '0x8a9', [], 0, @@ -106,7 +106,7 @@ describe('TxV3 Hash Tests', () => { [] ); - expect(result).toBe('0x41906f1c314cca5f43170ea75d3b1904196a10101190d2b12a41cc61cfd17c'); + expect(result).toBe('0x6d0e3ff991d62a10189a0ea11685d26b7efdb5baa9fa0d0a4edd1711185f671'); }); test('calculateDeployAccountTransactionHash Demo', () => { @@ -138,7 +138,7 @@ describe('TxV3 Hash Tests', () => { ['0x5cd65f3d7daea6c63939d659b8473ea0c5cd81576035a4d34e52fb06840196c'], '0x0', '0x3', - constants.StarknetChainId.SN_GOERLI, + constants.StarknetChainId.SN_SEPOLIA, '0x0', types.RPC.EDAMode.L1, types.RPC.EDAMode.L1, @@ -150,7 +150,7 @@ describe('TxV3 Hash Tests', () => { [] ); - expect(result).toBe('0x29fd7881f14380842414cdfdd8d6c0b1f2174f8916edcfeb1ede1eb26ac3ef0'); + expect(result).toBe('0x3018236df5779c1f28caba0e64febcb78f5bc69aa3538be54f4e27def9de1b3'); }); test('calculateDeclareTransactionHash Demo', () => { @@ -181,7 +181,7 @@ describe('TxV3 Hash Tests', () => { '0x1add56d64bebf8140f3b8a38bdf102b7874437f0c861ab4ca7526ec33b4d0f8', '0x2fab82e4aef1d8664874e1f194951856d48463c3e6bf9a8c68e234a629a6f50', '0x3', - constants.StarknetChainId.SN_GOERLI, + constants.StarknetChainId.SN_SEPOLIA, '0x1', [], types.RPC.EDAMode.L1, @@ -194,6 +194,6 @@ describe('TxV3 Hash Tests', () => { [] ); - expect(result).toBe('0x41d1f5206ef58a443e7d3d1ca073171ec25fa75313394318fc83a074a6631c3'); + expect(result).toBe('0x61bfaf480ac824971ad1bdc316fa821f58afd6b47e037242ef265d0aaea7c78'); }); }); diff --git a/package-lock.json b/package-lock.json index d09dd21f6..fafabd76c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "@scure/starknet": "~1.0.0", "abi-wan-kanabi": "^2.2.2", "fetch-cookie": "^3.0.0", + "get-starknet-core": "^4.0.0-next.3", "isomorphic-fetch": "^3.0.0", "lossless-json": "^4.0.1", "pako": "^2.0.4", @@ -9053,6 +9054,11 @@ "node": ">=8.0.0" } }, + "node_modules/get-starknet-core": { + "version": "4.0.0-next.3", + "resolved": "https://registry.npmjs.org/get-starknet-core/-/get-starknet-core-4.0.0-next.3.tgz", + "integrity": "sha512-/9q0UpsshYHZyEMcd+XD7CVrnwQ50WfXhy0TNocyHQzn+/qxQIBn25ZYJ+DEzwLPoUtrajMYI4L6DMKu1CJ+ig==" + }, "node_modules/get-stream": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-7.0.1.tgz", diff --git a/package.json b/package.json index 0c67828f1..6ddac578c 100644 --- a/package.json +++ b/package.json @@ -99,6 +99,7 @@ "@scure/starknet": "~1.0.0", "abi-wan-kanabi": "^2.2.2", "fetch-cookie": "^3.0.0", + "get-starknet-core": "^4.0.0-next.3", "isomorphic-fetch": "^3.0.0", "lossless-json": "^4.0.1", "pako": "^2.0.4", diff --git a/src/constants.ts b/src/constants.ts index a13e0c925..0b2d9ef85 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -30,19 +30,16 @@ export const RANGE_U128 = range(ZERO, 2n ** 128n - 1n); export enum BaseUrl { SN_MAIN = 'https://alpha-mainnet.starknet.io', - SN_GOERLI = 'https://alpha4.starknet.io', SN_SEPOLIA = 'https://alpha-sepolia.starknet.io', } export enum NetworkName { SN_MAIN = 'SN_MAIN', - SN_GOERLI = 'SN_GOERLI', SN_SEPOLIA = 'SN_SEPOLIA', } export enum StarknetChainId { SN_MAIN = '0x534e5f4d41494e', // encodeShortString('SN_MAIN'), - SN_GOERLI = '0x534e5f474f45524c49', // encodeShortString('SN_GOERLI') SN_SEPOLIA = '0x534e5f5345504f4c4941', // encodeShortString('SN_SEPOLIA') } @@ -68,10 +65,6 @@ export const UDC = { export const RPC_DEFAULT_VERSION = 'v0_7'; export const RPC_NODES = { - SN_GOERLI: [ - `https://starknet-testnet.public.blastapi.io/rpc/${RPC_DEFAULT_VERSION}`, - `https://free-rpc.nethermind.io/goerli-juno/${RPC_DEFAULT_VERSION}`, - ], SN_MAIN: [ `https://starknet-mainnet.public.blastapi.io/rpc/${RPC_DEFAULT_VERSION}`, `https://free-rpc.nethermind.io/mainnet-juno/${RPC_DEFAULT_VERSION}`, diff --git a/src/provider/rpc.ts b/src/provider/rpc.ts index 66ee958e9..27427ab24 100644 --- a/src/provider/rpc.ts +++ b/src/provider/rpc.ts @@ -30,6 +30,8 @@ import { getAbiContractVersion } from '../utils/calldata/cairo'; import { isSierra } from '../utils/contract'; import { RPCResponseParser } from '../utils/responseParser/rpc'; import { GetTransactionReceiptResponse, ReceiptTx } from '../utils/transactionReceipt'; +import { wait } from '../utils/provider'; +import { toHex } from '../utils/num'; import { LibraryError } from './errors'; import { ProviderInterface } from './interface'; @@ -101,6 +103,47 @@ export class RpcProvider implements ProviderInterface { return this.channel.getBlockWithTxs(blockIdentifier); } + /** + * Pause the execution of the script until a specified block is created. + * @param {BlockIdentifier} blockIdentifier bloc number (BigNumberisk) or 'pending' or 'latest'. + * Use of 'latest" or of a block already created will generate no pause. + * @param {number} [retryInterval] number of milliseconds between 2 requests to the node + * @example + * ```typescript + * await myProvider.waitForBlock(); + * // wait the creation of the pending block + * ``` + */ + public async waitForBlock( + blockIdentifier: BlockIdentifier = 'pending', + retryInterval: number = 5000 + ) { + if (blockIdentifier === BlockTag.latest) return; + const currentBlock = await this.getBlockNumber(); + const targetBlock = + blockIdentifier === BlockTag.pending + ? currentBlock + 1 + : Number(toHex(blockIdentifier as BigNumberish)); + if (targetBlock <= currentBlock) return; + const { retries } = this.channel; + let retriesCount = retries; + let isTargetBlock: boolean = false; + while (!isTargetBlock) { + // eslint-disable-next-line no-await-in-loop + const currBlock = await this.getBlockNumber(); + if (currBlock === targetBlock) { + isTargetBlock = true; + } else { + // eslint-disable-next-line no-await-in-loop + await wait(retryInterval); + } + retriesCount -= 1; + if (retriesCount <= 0) { + throw new Error(`waitForBlock() timed-out after ${retries} tries.`); + } + } + } + public async getL1GasPrice(blockIdentifier?: BlockIdentifier) { return this.channel .getBlockWithTxHashes(blockIdentifier) diff --git a/src/utils/num.ts b/src/utils/num.ts index 544cb3bab..5ada05f38 100644 --- a/src/utils/num.ts +++ b/src/utils/num.ts @@ -10,6 +10,17 @@ export type { BigNumberish }; /** * Test if string is hex-string * @param hex hex-string + * @returns {boolean} True if the input string is a hexadecimal string, false otherwise + * @example + * ```typescript + * const hexString1 = "0x2fd23d9182193775423497fc0c472e156c57c69e4089a1967fb288a2d84e914"; + * const result1 = isHex(hexString1); + * // result1 = true + * + * const hexString2 = "2fd23d9182193775423497fc0c472e156c57c69e4089a1967fb288a2d84e914"; + * const result2 = isHex(hexString2); + * // result2 = false + * ``` */ export function isHex(hex: string): boolean { return /^0x[0-9a-f]*$/i.test(hex); @@ -77,6 +88,20 @@ export const cleanHex = (hex: string) => hex.toLowerCase().replace(/^(0x)0+/, '$ * Asserts input is equal to or greater then lowerBound and lower then upperBound. * * The `inputName` parameter is used in the assertion message. + * @param input Value to check + * @param lowerBound Lower bound value + * @param upperBound Upper bound value + * @param inputName Name of the input for error message + * @Throws Error if input is out of range + * @example + * ```typescript + * const input1:BigNumberish = 10; + * assertInRange(input1, 5, 20, 'value') + * + * const input2: BigNumberish = 25; + * assertInRange(input2, 5, 20, 'value'); + * // Throws Error: Message not signable, invalid value length. + * ``` */ export function assertInRange( input: BigNumberish, @@ -119,6 +144,14 @@ export const isStringWholeNumber = (value: string) => /^\d+$/.test(value); /** * Convert string to decimal string * @returns format: decimal string + * @example + * ```typescript + * const result = getDecimalString("0x1a"); + * // result = "26" + * + * const result2 = getDecimalString("Hello"); + * // Throws Error: "Hello need to be hex-string or whole-number-string" + * ``` */ export function getDecimalString(value: string) { if (isHex(value)) { @@ -133,6 +166,14 @@ export function getDecimalString(value: string) { /** * Convert string to hexadecimal string * @returns format: hex-string + * @example + * ```typescript + * const result = getHexString("123"); + * // result = "0x7b" + * + * const result2 = getHexString("Hello"); + * // Throws Error: Hello need to be hex-string or whole-number-string + * ``` */ export function getHexString(value: string) { if (isHex(value)) { @@ -154,6 +195,16 @@ export function getHexStringArray(value: Array) { /** * Convert boolean to "0" or "1" + * @param value The boolean value to be converted. + * @returns {boolean} Returns true if the value is a number, otherwise returns false. + * @example + * ```typescript + * const result = toCairoBool(true); + * // result ="1" + * + * const result2 = toCairoBool(false); + * // result2 = "0" + * ``` */ export const toCairoBool = (value: boolean): string => (+value).toString(); @@ -186,6 +237,15 @@ export function addPercent(number: BigNumberish, percent: number) { * 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 { @@ -196,6 +256,15 @@ export function isNumber(value: unknown): value is 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 { diff --git a/src/utils/selector.ts b/src/utils/selector.ts index 1a137a62b..43ed3a515 100644 --- a/src/utils/selector.ts +++ b/src/utils/selector.ts @@ -63,6 +63,17 @@ export function getSelectorFromName(funcName: string) { * * @param value hex-string | dec-string | ascii-string * @returns format: hex-string + * @example + * ```typescript + * const selector: string = getSelector("myFunction"); + * // selector = "0x7e44bafo" + * + * const selector1: string = getSelector("0x123abc"); + * // selector1 = "0x123abc" + * + * const selector2: string = getSelector("123456"); + * // selector2 = "0x1e240" + * ``` */ export function getSelector(value: string) { if (isHex(value)) { diff --git a/src/utils/starknetId.ts b/src/utils/starknetId.ts index ad643b486..f9ed7376c 100644 --- a/src/utils/starknetId.ts +++ b/src/utils/starknetId.ts @@ -116,7 +116,6 @@ export function useEncoded(decoded: string): bigint { export const enum StarknetIdContract { MAINNET = '0x6ac597f8116f886fa1c97a23fa4e08299975ecaf6b598873ca6792b9bbfb678', - TESTNET = '0x3bab268e932d2cecd1946f100ae67ce3dff9fd234119ea2f6da57d16d29fce', TESTNET_SEPOLIA = '0x0707f09bc576bd7cfee59694846291047e965f4184fe13dac62c56759b3b6fa7', } @@ -132,9 +131,6 @@ export function getStarknetIdContract(chainId: StarknetChainId): string { case StarknetChainId.SN_MAIN: return StarknetIdContract.MAINNET; - case StarknetChainId.SN_GOERLI: - return StarknetIdContract.TESTNET; - case StarknetChainId.SN_SEPOLIA: return StarknetIdContract.TESTNET_SEPOLIA; @@ -145,7 +141,6 @@ export function getStarknetIdContract(chainId: StarknetChainId): string { export const enum StarknetIdIdentityContract { MAINNET = '0x05dbdedc203e92749e2e746e2d40a768d966bd243df04a6b712e222bc040a9af', - TESTNET = '0x783a9097b26eae0586373b2ce0ed3529ddc44069d1e0fbc4f66d42b69d6850d', TESTNET_SEPOLIA = '0x070DF8B4F5cb2879f8592849fA8f3134da39d25326B8558cc9C8FE8D47EA3A90', } @@ -163,9 +158,6 @@ export function getStarknetIdIdentityContract(chainId: StarknetChainId): string case StarknetChainId.SN_MAIN: return StarknetIdIdentityContract.MAINNET; - case StarknetChainId.SN_GOERLI: - return StarknetIdIdentityContract.TESTNET; - case StarknetChainId.SN_SEPOLIA: return StarknetIdIdentityContract.TESTNET_SEPOLIA; @@ -189,9 +181,6 @@ export function getStarknetIdMulticallContract(chainId: StarknetChainId): string case StarknetChainId.SN_MAIN: return StarknetIdMulticallContract; - case StarknetChainId.SN_GOERLI: - return StarknetIdMulticallContract; - case StarknetChainId.SN_SEPOLIA: return StarknetIdMulticallContract; @@ -202,7 +191,6 @@ export function getStarknetIdMulticallContract(chainId: StarknetChainId): string export const enum StarknetIdVerifierContract { MAINNET = '0x07d14dfd8ee95b41fce179170d88ba1f0d5a512e13aeb232f19cfeec0a88f8bf', - TESTNET = '0x057c942544063c3aea6ea6c37009cc9d1beacd750cb6801549a129c7265f0f11', TESTNET_SEPOLIA = '0x0182EcE8173C216A395f4828e1523541b7e3600bf190CB252E1a1A0cE219d184', } @@ -218,9 +206,6 @@ export function getStarknetIdVerifierContract(chainId: StarknetChainId): string case StarknetChainId.SN_MAIN: return StarknetIdVerifierContract.MAINNET; - case StarknetChainId.SN_GOERLI: - return StarknetIdVerifierContract.TESTNET; - case StarknetChainId.SN_SEPOLIA: return StarknetIdVerifierContract.TESTNET_SEPOLIA; @@ -231,7 +216,6 @@ export function getStarknetIdVerifierContract(chainId: StarknetChainId): string export const enum StarknetIdPfpContract { MAINNET = '0x070aaa20ec4a46da57c932d9fd89ca5e6bb9ca3188d3df361a32306aff7d59c7', - TESTNET = '0x03cac3228b434259734ee0e4ff445f642206ea11adace7e4f45edd2596748698', TESTNET_SEPOLIA = '0x058061bb6bdc501eE215172c9f87d557C1E0f466dC498cA81b18f998Bf1362b2', } @@ -247,9 +231,6 @@ export function getStarknetIdPfpContract(chainId: StarknetChainId): string { case StarknetChainId.SN_MAIN: return StarknetIdPfpContract.MAINNET; - case StarknetChainId.SN_GOERLI: - return StarknetIdPfpContract.TESTNET; - case StarknetChainId.SN_SEPOLIA: return StarknetIdPfpContract.TESTNET_SEPOLIA; @@ -262,7 +243,6 @@ export function getStarknetIdPfpContract(chainId: StarknetChainId): string { export const enum StarknetIdPopContract { MAINNET = '0x0293eb2ba9862f762bd3036586d5755a782bd22e6f5028320f1d0405fd47bff4', - TESTNET = '0x03528caf090179e337931ee669a5b0214041e1bae30d460ff07d2cea2c7a9106', TESTNET_SEPOLIA = '0x0023FE3b845ed5665a9eb3792bbB17347B490EE4090f855C1298d03BB5F49B49', } @@ -278,9 +258,6 @@ export function getStarknetIdPopContract(chainId: StarknetChainId): string { case StarknetChainId.SN_MAIN: return StarknetIdPopContract.MAINNET; - case StarknetChainId.SN_GOERLI: - return StarknetIdPopContract.TESTNET; - case StarknetChainId.SN_SEPOLIA: return StarknetIdPopContract.TESTNET_SEPOLIA; From 1489cf25e7e8598ab161cecc62c82495f64daa33 Mon Sep 17 00:00:00 2001 From: Philippe ROSTAN <81040730+PhilippeR26@users.noreply.github.com> Date: Fri, 17 May 2024 22:14:48 +0200 Subject: [PATCH 16/18] feat: provider.getL1MessageHash (#1123) * feat: provider.getL1MessageHash * docs: add JSDOC example * fix: remaining conflicts --- __tests__/rpcProvider.test.ts | 16 ++++++++++++++++ package-lock.json | 25 +++++++++++++------------ package.json | 1 + src/provider/interface.ts | 13 +++++++++++++ src/provider/rpc.ts | 30 +++++++++++++++++++++++++++++- 5 files changed, 72 insertions(+), 13 deletions(-) diff --git a/__tests__/rpcProvider.test.ts b/__tests__/rpcProvider.test.ts index 18d4f421e..a998e271d 100644 --- a/__tests__/rpcProvider.test.ts +++ b/__tests__/rpcProvider.test.ts @@ -30,6 +30,7 @@ import { describeIfDevnet, getTestAccount, getTestProvider, + describeIfTestnet, waitNextBlock, devnetETHtokenAddress, } from './config/fixtures'; @@ -430,6 +431,21 @@ describeIfRpc('RPCProvider', () => { }); }); +describeIfTestnet('RPCProvider', () => { + const provider = getTestProvider(); + + test('getL1MessageHash', async () => { + const l2TransactionHash = '0x28dfc05eb4f261b37ddad451ff22f1d08d4e3c24dc646af0ec69fa20e096819'; + const l1MessageHash = await provider.getL1MessageHash(l2TransactionHash); + expect(l1MessageHash).toBe( + '0x55b3f8b6e607fffd9b4d843dfe8f9b5c05822cd94fcad8797deb01d77805532a' + ); + await expect( + provider.getL1MessageHash('0x283882a666a418cf88df04cc5f8fc2262af510bba0b637e61b2820a6ab15318') + ).rejects.toThrow(/This L2 transaction is not a L1 message./); + await expect(provider.getL1MessageHash('0x123')).rejects.toThrow(/Transaction hash not found/); + }); +}); describeIfNotDevnet('waitForBlock', () => { // As Devnet-rs isn't generating automatically blocks at a periodic time, it's excluded of this test. const providerStandard = new RpcProvider({ nodeUrl: process.env.TEST_RPC_URL }); diff --git a/package-lock.json b/package-lock.json index fafabd76c..508eb0203 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "MIT", "dependencies": { "@noble/curves": "~1.4.0", + "@noble/hashes": "^1.4.0", "@scure/base": "~1.1.3", "@scure/starknet": "~1.0.0", "abi-wan-kanabi": "^2.2.2", @@ -4104,7 +4105,7 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/@noble/curves/node_modules/@noble/hashes": { + "node_modules/@noble/hashes": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", @@ -4115,17 +4116,6 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/@noble/hashes": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.3.tgz", - "integrity": "sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==", - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -4594,6 +4584,17 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@scure/starknet/node_modules/@noble/hashes": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.3.tgz", + "integrity": "sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@semantic-release/changelog": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/@semantic-release/changelog/-/changelog-6.0.3.tgz", diff --git a/package.json b/package.json index 6ddac578c..475e1931a 100644 --- a/package.json +++ b/package.json @@ -95,6 +95,7 @@ }, "dependencies": { "@noble/curves": "~1.4.0", + "@noble/hashes": "^1.4.0", "@scure/base": "~1.1.3", "@scure/starknet": "~1.0.0", "abi-wan-kanabi": "^2.2.2", diff --git a/src/provider/interface.ts b/src/provider/interface.ts index 8903bbd98..e1144c44e 100644 --- a/src/provider/interface.ts +++ b/src/provider/interface.ts @@ -86,6 +86,19 @@ export abstract class ProviderInterface { */ public abstract getL1GasPrice(blockIdentifier: BlockIdentifier): Promise; + /** + * Get L1 message hash from L2 transaction hash + * @param {BigNumberish} l2TxHash L2 transaction hash + * @returns {string} Hex string of L1 message hash + * @example + * In Sepolia Testnet : + * ```typescript + * const result = provider.getL1MessageHash('0x28dfc05eb4f261b37ddad451ff22f1d08d4e3c24dc646af0ec69fa20e096819'); + * // result = '0x55b3f8b6e607fffd9b4d843dfe8f9b5c05822cd94fcad8797deb01d77805532a' + * ``` + */ + public abstract getL1MessageHash(l2TxHash: BigNumberish): Promise; + /** * Returns the contract class hash in the given block for the contract deployed at the given address * diff --git a/src/provider/rpc.ts b/src/provider/rpc.ts index 27427ab24..2c4f5a0a5 100644 --- a/src/provider/rpc.ts +++ b/src/provider/rpc.ts @@ -1,3 +1,6 @@ +import type { SPEC } from 'starknet-types-07'; +import { bytesToHex } from '@noble/curves/abstract/utils'; +import { keccak_256 } from '@noble/hashes/sha3'; import { RPC06, RPC07, RpcChannel } from '../channel'; import { AccountInvocations, @@ -30,8 +33,11 @@ import { getAbiContractVersion } from '../utils/calldata/cairo'; import { isSierra } from '../utils/contract'; import { RPCResponseParser } from '../utils/responseParser/rpc'; import { GetTransactionReceiptResponse, ReceiptTx } from '../utils/transactionReceipt'; +import type { TransactionWithHash } from '../types/provider/spec'; +import assert from '../utils/assert'; +import { hexToBytes, toHex } from '../utils/num'; +import { addHexPrefix, removeHexPrefix } from '../utils/encode'; import { wait } from '../utils/provider'; -import { toHex } from '../utils/num'; import { LibraryError } from './errors'; import { ProviderInterface } from './interface'; @@ -150,6 +156,28 @@ export class RpcProvider implements ProviderInterface { .then(this.responseParser.parseL1GasPriceResponse); } + public async getL1MessageHash(l2TxHash: BigNumberish) { + const transaction = (await this.channel.getTransactionByHash(l2TxHash)) as TransactionWithHash; + assert(transaction.type === 'L1_HANDLER', 'This L2 transaction is not a L1 message.'); + const { calldata, contract_address, entry_point_selector, nonce } = + transaction as SPEC.L1_HANDLER_TXN; + const params = [ + calldata[0], + contract_address, + nonce, + entry_point_selector, + calldata.length - 1, + ...calldata.slice(1), + ]; + const myEncode = addHexPrefix( + params.reduce( + (res: string, par: BigNumberish) => res + removeHexPrefix(toHex(par)).padStart(64, '0'), + '' + ) + ); + return addHexPrefix(bytesToHex(keccak_256(hexToBytes(myEncode)))); + } + public async getBlockWithReceipts(blockIdentifier?: BlockIdentifier) { if (this.channel instanceof RPC06.RpcChannel) throw new LibraryError('Unsupported method for RPC version'); From 195186fc2974ab0d164b1a48c68f7bf026329df5 Mon Sep 17 00:00:00 2001 From: 0xknwn <145777008+0xknwn@users.noreply.github.com> Date: Fri, 17 May 2024 22:17:03 +0200 Subject: [PATCH 17/18] fix: remove [warning] from typedoc for external usage (#1095) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test: Improve tests performance (#1121) * test: fix transaction retry interval fallback for devnet tests * test: remove test.only * Update _test.yml (#1126) * fix: rename the @param to match the definitions this commit changes the typedoc that do not match the function declaration * fix: add a space before typedoc
so that it catches the name when a @param contains an html tag, it should not be attached to the parameter name otherwise it does consider the tag is part of the name. This commit detaches the name from the tag * fix: improve function declaration so that typedoc matches it this is a specific case when a function has several declarations and the first one does not match the typedoc because the parameter is optional. This commit simplifies the declaration by making the comment optional with a ?. * fix: reintroduce the parameter name change the declaration of a function so that the parameter name appears. To do so it moves the assignement in the function. * fix: remove unused type declaration and export removes a declaration from account that is not used in the code and redundant with the exact same one in the provider file. export the declaration that is the one really used. --------- Co-authored-by: Luka Saric <32763694+lukasaric@users.noreply.github.com> Co-authored-by: Ivan Pavičić --- __tests__/config/fixtures.ts | 25 ++++++++----------------- src/account/interface.ts | 28 ++++++++++++++-------------- src/channel/rpc_0_6.ts | 7 ++++--- src/channel/rpc_0_7.ts | 28 ++++++++++++++++++++++------ src/provider/interface.ts | 7 +++---- src/provider/rpc.ts | 3 ++- src/signer/interface.ts | 4 ++-- src/types/account.ts | 5 ----- src/types/provider/configuration.ts | 1 + src/types/provider/response.ts | 1 + src/utils/calldata/byteArray.ts | 2 +- src/utils/calldata/index.ts | 2 +- src/wallet/connect.ts | 3 ++- 13 files changed, 61 insertions(+), 55 deletions(-) diff --git a/__tests__/config/fixtures.ts b/__tests__/config/fixtures.ts index 8314031b9..c863d8212 100644 --- a/__tests__/config/fixtures.ts +++ b/__tests__/config/fixtures.ts @@ -2,12 +2,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { Account, Provider, ProviderInterface, RpcProvider, json } from '../../src'; -import { - CompiledSierra, - CompiledSierraCasm, - LegacyCompiledContract, - waitForTransactionOptions, -} from '../../src/types'; +import { CompiledSierra, CompiledSierraCasm, LegacyCompiledContract } from '../../src/types'; import { ETransactionVersion } from '../../src/types/api'; import { toHex } from '../../src/utils/num'; import { wait } from '../../src/utils/provider'; @@ -74,22 +69,18 @@ export const compiledTestRejectSierra = readContractSierra('cairo/testReject/tes export const compiledTestRejectCasm = readContractSierraCasm('cairo/testReject/test_reject'); export const compiledSidMulticall = readContractSierra('starknetId/multicall/multicall.sierra'); export const compiledSidMulticallCasm = readContractSierraCasm('starknetId/multicall/multicall'); + export function getTestProvider(isProvider?: true): ProviderInterface; export function getTestProvider(isProvider?: false): RpcProvider; export function getTestProvider(isProvider: boolean = true): ProviderInterface | RpcProvider { - const provider = isProvider - ? new Provider({ nodeUrl: process.env.TEST_RPC_URL }) - : new RpcProvider({ nodeUrl: process.env.TEST_RPC_URL }); + const isDevnet = process.env.IS_DEVNET === 'true'; - if (process.env.IS_DEVNET === 'true') { + const providerOptions = { + nodeUrl: process.env.TEST_RPC_URL, // accelerate the tests when running locally - const originalWaitForTransaction = provider.waitForTransaction.bind(provider); - provider.waitForTransaction = (txHash: string, options: waitForTransactionOptions = {}) => { - return originalWaitForTransaction(txHash, { retryInterval: 1000, ...options }); - }; - } - - return provider; + ...(isDevnet && { transactionRetryIntervalFallback: 1000 }), + }; + return isProvider ? new Provider(providerOptions) : new RpcProvider(providerOptions); } export const TEST_TX_VERSION = process.env.TX_VERSION === 'v3' ? ETransactionVersion.V3 : undefined; diff --git a/src/account/interface.ts b/src/account/interface.ts index 66b308777..eba6e4b4e 100644 --- a/src/account/interface.ts +++ b/src/account/interface.ts @@ -147,11 +147,11 @@ export abstract class AccountInterface extends ProviderInterface { * Estimate Fee for executing a list of transactions on starknet * Contract must be deployed for fee estimation to be possible * - * @param transactions array of transaction object containing : + * @param invocations array of transaction object containing : * - type - the type of transaction : 'DECLARE' | (multi)'DEPLOY' | (multi)'INVOKE_FUNCTION' | 'DEPLOY_ACCOUNT' * - payload - the payload of the transaction * - * @param estimateFeeDetails - + * @param details - * - blockIdentifier? * - nonce? * - skipValidate? - default true @@ -344,34 +344,34 @@ export abstract class AccountInterface extends ProviderInterface { ): Promise; /** - * Signs a JSON object for off-chain usage with the Starknet private key and returns the signature + * Signs a TypedData object for off-chain usage with the Starknet private key and returns the signature * This adds a message prefix so it can't be interchanged with transactions * - * @param json - JSON object to be signed - * @returns the signature of the JSON object - * @throws {Error} if the JSON object is not a valid JSON + * @param typedData - TypedData object to be signed + * @returns the signature of the TypedData object + * @throws {Error} if typedData is not a valid TypedData */ public abstract signMessage(typedData: TypedData): Promise; /** - * Hash a JSON object with Pedersen hash and return the hash + * Hash a TypedData object with Pedersen hash and return the hash * This adds a message prefix so it can't be interchanged with transactions * - * @param json - JSON object to be hashed - * @returns the hash of the JSON object - * @throws {Error} if the JSON object is not a valid JSON + * @param typedData - TypedData object to be hashed + * @returns the hash of the TypedData object + * @throws {Error} if typedData is not a valid TypedData */ public abstract hashMessage(typedData: TypedData): Promise; /** - * Verify a signature of a JSON object + * Verify a signature of a TypedData object * - * @param typedData - JSON object to be verified - * @param signature - signature of the JSON object + * @param typedData - TypedData object to be verified + * @param signature - signature of the TypedData object * @param signatureVerificationFunctionName - optional account contract verification function name override * @param signatureVerificationResponse - optional response override { okResponse: string[]; nokResponse: string[]; error: string[] } * @returns true if the signature is valid, false otherwise - * @throws {Error} if the JSON object is not a valid JSON or the signature is not a valid signature + * @throws {Error} if typedData is not a valid TypedData or the signature is not a valid signature */ public abstract verifyMessage(typedData: TypedData, signature: Signature): Promise; diff --git a/src/channel/rpc_0_6.ts b/src/channel/rpc_0_6.ts index a4c66ca00..3074c82d0 100644 --- a/src/channel/rpc_0_6.ts +++ b/src/channel/rpc_0_6.ts @@ -222,12 +222,13 @@ export class RpcChannel { */ public simulateTransaction( invocations: AccountInvocations, - { + simulateTransactionOptions: getSimulateTransactionOptions = {} + ) { + const { blockIdentifier = this.blockIdentifier, skipValidate = true, skipFeeCharge = true, - }: getSimulateTransactionOptions = {} - ) { + } = simulateTransactionOptions; const block_id = new Block(blockIdentifier).identifier; const simulationFlags: RPC.ESimulationFlag[] = []; if (skipValidate) simulationFlags.push(RPC.ESimulationFlag.SKIP_VALIDATE); diff --git a/src/channel/rpc_0_7.ts b/src/channel/rpc_0_7.ts index 53e51aed2..00e875bd4 100644 --- a/src/channel/rpc_0_7.ts +++ b/src/channel/rpc_0_7.ts @@ -50,11 +50,21 @@ export class RpcChannel { private specVersion?: string; + private transactionRetryIntervalFallback?: number; + readonly waitMode: Boolean; // behave like web2 rpc and return when tx is processed constructor(optionsOrProvider?: RpcProviderOptions) { - const { nodeUrl, retries, headers, blockIdentifier, chainId, specVersion, waitMode } = - optionsOrProvider || {}; + const { + nodeUrl, + retries, + headers, + blockIdentifier, + chainId, + specVersion, + waitMode, + transactionRetryIntervalFallback, + } = optionsOrProvider || {}; if (Object.values(NetworkName).includes(nodeUrl as NetworkName)) { this.nodeUrl = getDefaultNodeUrl(nodeUrl as NetworkName, optionsOrProvider?.default); } else if (nodeUrl) { @@ -69,6 +79,11 @@ export class RpcChannel { this.specVersion = specVersion; this.waitMode = waitMode || false; this.requestId = 0; + this.transactionRetryIntervalFallback = transactionRetryIntervalFallback; + } + + private get transactionRetryIntervalDefault() { + return this.transactionRetryIntervalFallback ?? 5000; } public setChainId(chainId: StarknetChainId) { @@ -227,12 +242,13 @@ export class RpcChannel { */ public simulateTransaction( invocations: AccountInvocations, - { + simulateTransactionOptions: getSimulateTransactionOptions = {} + ) { + const { blockIdentifier = this.blockIdentifier, skipValidate = true, skipFeeCharge = true, - }: getSimulateTransactionOptions = {} - ) { + } = simulateTransactionOptions; const block_id = new Block(blockIdentifier).identifier; const simulationFlags: RPC.ESimulationFlag[] = []; if (skipValidate) simulationFlags.push(RPC.ESimulationFlag.SKIP_VALIDATE); @@ -250,7 +266,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/provider/interface.ts b/src/provider/interface.ts index e1144c44e..7b8067d61 100644 --- a/src/provider/interface.ts +++ b/src/provider/interface.ts @@ -61,8 +61,7 @@ export abstract class ProviderInterface { * @param blockIdentifier block identifier * @returns the block object */ - public abstract getBlock(): Promise; - public abstract getBlock(blockIdentifier: 'pending'): Promise; + public abstract getBlock(blockIdentifier?: 'pending'): Promise; public abstract getBlock(blockIdentifier: 'latest'): Promise; public abstract getBlock(blockIdentifier: BlockIdentifier): Promise; @@ -147,7 +146,7 @@ export abstract class ProviderInterface { /** * Gets the transaction information from a tx id. * - * @param txHash + * @param transactionHash * @returns the transaction object \{ transaction_id, status, transaction, block_number?, block_number?, transaction_index?, transaction_failure_reason? \} */ public abstract getTransaction(transactionHash: BigNumberish): Promise; @@ -155,7 +154,7 @@ export abstract class ProviderInterface { /** * Gets the transaction receipt from a tx hash. * - * @param txHash + * @param transactionHash * @returns the transaction receipt object */ public abstract getTransactionReceipt( diff --git a/src/provider/rpc.ts b/src/provider/rpc.ts index 2c4f5a0a5..9d4ded36c 100644 --- a/src/provider/rpc.ts +++ b/src/provider/rpc.ts @@ -247,7 +247,7 @@ export class RpcProvider implements ProviderInterface { /** * @param invocations AccountInvocations - * @param simulateTransactionOptions blockIdentifier and flags to skip validation and fee charge
+ * @param options blockIdentifier and flags to skip validation and fee charge
* - blockIdentifier
* - skipValidate (default false)
* - skipFeeCharge (default true)
@@ -270,6 +270,7 @@ export class RpcProvider implements ProviderInterface { txHash, options )) as GetTxReceiptResponseWithoutHelper; + return new ReceiptTx(receiptWoHelper) as GetTransactionReceiptResponse; } diff --git a/src/signer/interface.ts b/src/signer/interface.ts index 47c08dd7a..beaea9bd3 100644 --- a/src/signer/interface.ts +++ b/src/signer/interface.ts @@ -47,7 +47,7 @@ export abstract class SignerInterface { /** * Signs a DEPLOY_ACCOUNT transaction with the Starknet private key and returns the signature * - * @param transaction
+ * @param transaction
* - contractAddress
* - chainId
* - classHash
@@ -64,7 +64,7 @@ export abstract class SignerInterface { /** * Signs a DECLARE transaction with the Starknet private key and returns the signature * - * @param transaction
+ * @param transaction
* - classHash
* - compiledClassHash? - used for Cairo1
* - senderAddress
diff --git a/src/types/account.ts b/src/types/account.ts index 9572cfeb8..4da859a92 100644 --- a/src/types/account.ts +++ b/src/types/account.ts @@ -76,11 +76,6 @@ export type SimulateTransactionDetails = { skipExecute?: boolean; } & Partial; -export enum SIMULATION_FLAG { - SKIP_VALIDATE = 'SKIP_VALIDATE', - SKIP_EXECUTE = 'SKIP_EXECUTE', -} - export type EstimateFeeAction = | { type: TransactionType.INVOKE; diff --git a/src/types/provider/configuration.ts b/src/types/provider/configuration.ts index 71eaf534f..db1825f42 100644 --- a/src/types/provider/configuration.ts +++ b/src/types/provider/configuration.ts @@ -6,6 +6,7 @@ export interface ProviderOptions extends RpcProviderOptions {} export type RpcProviderOptions = { nodeUrl?: string | NetworkName; retries?: number; + transactionRetryIntervalFallback?: number; headers?: object; blockIdentifier?: BlockIdentifier; chainId?: StarknetChainId; diff --git a/src/types/provider/response.ts b/src/types/provider/response.ts index e4a4fa361..a91534778 100644 --- a/src/types/provider/response.ts +++ b/src/types/provider/response.ts @@ -137,6 +137,7 @@ export type Storage = FELT; export type Nonce = string; +export type { SIMULATION_FLAG }; export type SimulationFlags = Array; export type SimulatedTransaction = SimulateTransaction & { diff --git a/src/utils/calldata/byteArray.ts b/src/utils/calldata/byteArray.ts index 288848259..28febc303 100644 --- a/src/utils/calldata/byteArray.ts +++ b/src/utils/calldata/byteArray.ts @@ -32,7 +32,7 @@ export function stringFromByteArray(myByteArray: ByteArray): string { /** * convert a JS string to a Cairo ByteArray - * @param myString a JS string + * @param targetString a JS string * @returns Cairo representation of a LongString * @example * ```typescript diff --git a/src/utils/calldata/index.ts b/src/utils/calldata/index.ts index 904e99506..9d01d4f91 100644 --- a/src/utils/calldata/index.ts +++ b/src/utils/calldata/index.ts @@ -104,7 +104,7 @@ export class CallData { * Compile contract callData with abi * Parse the calldata by using input fields from the abi for that method * @param method string - method name - * @param args RawArgs - arguments passed to the method. Can be an array of arguments (in the order of abi definition), or an object constructed in conformity with abi (in this case, the parameter can be in a wrong order). + * @param argsCalldata RawArgs - arguments passed to the method. Can be an array of arguments (in the order of abi definition), or an object constructed in conformity with abi (in this case, the parameter can be in a wrong order). * @return Calldata - parsed arguments in format that contract is expecting * @example * ```typescript diff --git a/src/wallet/connect.ts b/src/wallet/connect.ts index c9be27f49..258c91f80 100644 --- a/src/wallet/connect.ts +++ b/src/wallet/connect.ts @@ -121,7 +121,8 @@ export function addDeclareTransaction( /** * Sign typed data using the wallet. - * @param params The typed data to sign. + * @param swo the starknet (wallet) window object to request the signature. + * @param typedData The typed data to sign. * @returns An array of signatures as strings. */ export function signMessage(swo: StarknetWindowObject, typedData: TypedData) { From 242baff584ab0847371c3dc0eab5294a39332167 Mon Sep 17 00:00:00 2001 From: Philippe ROSTAN <81040730+PhilippeR26@users.noreply.github.com> Date: Mon, 20 May 2024 13:29:20 +0200 Subject: [PATCH 18/18] docs: relocate documentation of eth randomness (#1130) --- www/docs/guides/connect_account.md | 6 ------ www/docs/guides/create_account.md | 10 +++++++++- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/www/docs/guides/connect_account.md b/www/docs/guides/connect_account.md index b5c0edb5b..fa775c329 100644 --- a/www/docs/guides/connect_account.md +++ b/www/docs/guides/connect_account.md @@ -86,9 +86,3 @@ const myEthAccountAddressInStarknet = const myEthSigner = new EthSigner(myEthPrivateKey); const myEthAccount = new Account(provider, myEthAccountAddressInStarknet, myEthSigner); ``` - -And if you need a random Ethereum private key: - -```typescript -const myPrivateKey = eth.ethRandomPrivateKey(); -``` diff --git a/www/docs/guides/create_account.md b/www/docs/guides/create_account.md index 805e3dd57..f601ed991 100644 --- a/www/docs/guides/create_account.md +++ b/www/docs/guides/create_account.md @@ -250,6 +250,14 @@ const contractETHaddress = hash.calculateContractAddressFromHash( console.log('Pre-calculated ETH account address =', contractETHaddress); ``` +> If you need a random Ethereum private key: +> +> ```typescript +> const myPrivateKey = eth.ethRandomPrivateKey(); +> ``` + +```` + Then you have to fund this address. ### Deployment of the new account @@ -271,7 +279,7 @@ const { transaction_hash, contract_address } = await ethAccount.deployAccount( ); await provider.waitForTransaction(transaction_hash); console.log('✅ New Ethereum account final address =', contract_address); -``` +```` ## Create your account abstraction