/* This file is part of web3.js. web3.js is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. web3.js is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with web3.js. If not, see . */ import { keccak256 } from '@theqrl/qrl-cryptography/keccak.js'; import { utf8ToBytes } from '@theqrl/qrl-cryptography/utils.js'; import { InvalidAddressError, InvalidBooleanError, InvalidBytesError, InvalidLargeValueError, InvalidSizeError, InvalidStringError, InvalidUnsignedIntegerError, } from '@theqrl/web3-errors'; import { Bytes, EncodingTypes, Numbers, Sha3Input, TypedObject, TypedObjectAbbreviated, } from '@theqrl/web3-types'; import { isAddressString, isNullish, isHexStrict } from '@theqrl/web3-validator'; import { bytesToUint8Array, bytesToHex, hexToBytes, toBigInt, toHex, toNumber, utf8ToHex, addressToHex, } from './converters.js'; import { leftPad, rightPad, toTwosComplement } from './string_manipulation.js'; const SHA3_EMPTY_BYTES = '0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470'; /** * computes the Keccak-256 hash of the input and returns a hexstring * @param data - the input to hash * @returns - the Keccak-256 hash of the input * * @example * ```ts * console.log(web3.utils.sha3('web3.js')); * > 0x63667efb1961039c9bb0d6ea7a5abdd223a3aca7daa5044ad894226e1f83919a * * console.log(web3.utils.sha3('')); * > undefined * ``` */ export const sha3 = (data: Bytes): string | undefined => { let updatedData: Uint8Array; if (typeof data === 'string') { if (data.startsWith('0x') && isHexStrict(data)) { updatedData = hexToBytes(data); } else { updatedData = utf8ToBytes(data); } } else { updatedData = data; } const hash = bytesToHex(keccak256(updatedData)); // EIP-1052 if hash is equal to c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470, keccak was given empty data return hash === SHA3_EMPTY_BYTES ? undefined : hash; }; /** * Will calculate the sha3 of the input but does return the hash value instead of null if for example a empty string is passed. * @param data - the input to hash * @returns - the Keccak-256 hash of the input * * @example * ```ts * conosle.log(web3.utils.sha3Raw('web3.js')); * > 0x63667efb1961039c9bb0d6ea7a5abdd223a3aca7daa5044ad894226e1f83919a * * console.log(web3.utils.sha3Raw('')); * > 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470 * ``` */ export const sha3Raw = (data: Bytes): string => { const hash = sha3(data); if (isNullish(hash)) { return SHA3_EMPTY_BYTES; } return hash; }; /** * A wrapper for qrl-cryptography/keccak256 to allow hashing a `string` and a `bigint` in addition to `UInt8Array` * @param data - the input to hash * @returns - the Keccak-256 hash of the input * * @example * ```ts * console.log(web3.utils.keccak256Wrapper('web3.js')); * > 0x63667efb1961039c9bb0d6ea7a5abdd223a3aca7daa5044ad894226e1f83919a * * console.log(web3.utils.keccak256Wrapper(1)); * > 0xc89efdaa54c0f20c7adf612882df0950f5a951637e0307cdcb4c672f298b8bc6 * * console.log(web3.utils.keccak256Wrapper(0xaf12fd)); * > 0x358640fd4719fa923525d74ab5ae80a594301aba5543e3492b052bf4598b794c * ``` */ export const keccak256Wrapper = ( data: Bytes | Numbers | string | ReadonlyArray, ): string => { let processedData; if (typeof data === 'bigint' || typeof data === 'number') { processedData = utf8ToBytes(data.toString()); } else if (Array.isArray(data)) { processedData = new Uint8Array(data); } else if (typeof data === 'string' && !isHexStrict(data)) { processedData = utf8ToBytes(data); } else { processedData = bytesToUint8Array(data as Bytes); } return bytesToHex(keccak256(processedData)); }; export { keccak256Wrapper as keccak256 }; /** * returns type and value * @param arg - the input to return the type and value * @returns - the type and value of the input */ const getType = (arg: Sha3Input): [string, EncodingTypes] => { if (Array.isArray(arg)) { throw new Error('Autodetection of array types is not supported.'); } let type; let value; // if type is given if ( typeof arg === 'object' && ('t' in arg || 'type' in arg) && ('v' in arg || 'value' in arg) ) { type = 't' in arg ? arg.t : arg.type; value = 'v' in arg ? arg.v : arg.value; type = type.toLowerCase() === 'bigint' ? 'int' : type; } else if (typeof arg === 'bigint') { return ['int', arg]; } // otherwise try to guess the type else { type = toHex(arg, true); value = toHex(arg); if (!type.startsWith('int') && !type.startsWith('uint')) { type = 'bytes'; } } if ( (type.startsWith('int') || type.startsWith('uint')) && typeof value === 'string' && !/^(-)?0x/i.test(value) ) { value = toBigInt(value); } return [type, value]; }; /** * returns the type with size if uint or int * @param name - the input to return the type with size * @returns - the type with size of the input */ const elementaryName = (name: string): string => { if (name.startsWith('int[')) { return `int256${name.slice(3)}`; } if (name === 'int') { return 'int256'; } if (name.startsWith('uint[')) { return `uint256'${name.slice(4)}`; } if (name === 'uint') { return 'uint256'; } return name; }; /** * returns the size of the value of type 'byte' */ const parseTypeN = (value: string, typeLength: number): number => { const typesize = /^(\d+).*$/.exec(value.slice(typeLength)); return typesize ? parseInt(typesize[1], 10) : 0; }; /** * returns the bit length of the value * @param value - the input to return the bit length * @returns - the bit length of the input */ const bitLength = (value: bigint | number): number => { const updatedVal = value.toString(2); return updatedVal.length; }; /** * Pads the value based on size and type * returns a string of the padded value * @param type - the input to pad * @returns = the padded value */ const hyperionPack = (type: string, val: EncodingTypes): string => { const value = val.toString(); if (type === 'string') { if (typeof val === 'string') return utf8ToHex(val); throw new InvalidStringError(val); } if (type === 'bool' || type === 'boolean') { if (typeof val === 'boolean') return val ? '01' : '00'; throw new InvalidBooleanError(val); } if (type === 'address') { if (!isAddressString(value)) { throw new InvalidAddressError(value); } return addressToHex(value); } const name = elementaryName(type); if (type.startsWith('uint')) { const size = parseTypeN(name, 'uint'.length); if (size % 8 || size < 8 || size > 256) { throw new InvalidSizeError(value); } const num = toNumber(value); if (bitLength(num) > size) { throw new InvalidLargeValueError(value); } if (num < BigInt(0)) { throw new InvalidUnsignedIntegerError(value); } return size ? leftPad(num.toString(16), (size / 8) * 2) : num.toString(16); } if (type.startsWith('int')) { const size = parseTypeN(name, 'int'.length); if (size % 8 || size < 8 || size > 256) { throw new InvalidSizeError(type); } const num = toNumber(value); if (bitLength(num) > size) { throw new InvalidLargeValueError(value); } if (num < BigInt(0)) { return toTwosComplement(num.toString(), (size / 8) * 2); } return size ? leftPad(num.toString(16), size / 4) : num.toString(16); } if (name === 'bytes') { if (value.replace(/^0x/i, '').length % 2 !== 0) { throw new InvalidBytesError(value); } return value; } if (type.startsWith('bytes')) { if (value.replace(/^0x/i, '').length % 2 !== 0) { throw new InvalidBytesError(value); } const size = parseTypeN(type, 'bytes'.length); if (!size || size < 1 || size > 64 || size < value.replace(/^0x/i, '').length / 2) { throw new InvalidBytesError(value); } return rightPad(value, size * 2); } return ''; }; /** * returns a string of the tightly packed value given based on the type * @param arg - the input to return the tightly packed value * @returns - the tightly packed value */ export const processHyperionEncodePackedArgs = (arg: Sha3Input): string => { const [type, val] = getType(arg); // array case if (Array.isArray(val)) { // go through each element of the array and use map function to create new hexarg list const hexArg = val.map((v: Numbers | boolean) => hyperionPack(type, v).replace('0x', '')); return hexArg.join(''); } const hexArg = hyperionPack(type, val); return hexArg.replace('0x', ''); }; /** * Encode packed arguments to a hexstring */ export const encodePacked = (...values: Sha3Input[]): string => { const args = Array.prototype.slice.call(values); const hexArgs = args.map(processHyperionEncodePackedArgs); return `0x${hexArgs.join('').toLowerCase()}`; }; /** * Will tightly pack values given in the same way hyperion would then hash. * returns a hash string, or null if input is empty * @param values - the input to return the tightly packed values * @returns - the keccack246 of the tightly packed values * * @example * ```ts * console.log([{ type: 'string', value: '31323334' }]); * console.log(web3.utils.hyperionSha3({ type: "string", value: "31323334" })); * > 0xf15f8da2ad27e486d632dc37d24912f634398918d6f9913a0a0ff84e388be62b * ``` */ export const hyperionSha3 = (...values: Sha3Input[]): string | undefined => sha3(encodePacked(...values)); /** * Will tightly pack values given in the same way hyperion would then hash. * returns a hash string, if input is empty will return `0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470` * @param values - the input to return the tightly packed values * @returns - the keccack246 of the tightly packed values * * @example * ```ts * console.log(web3.utils.hyperionSha3Raw({ type: "string", value: "helloworld" })) * > 0xfa26db7ca85ead399216e7c6316bc50ed24393c3122b582735e7f3b0f91b93f0 * ``` */ export const hyperionSha3Raw = (...values: TypedObject[] | TypedObjectAbbreviated[]): string => sha3Raw(encodePacked(...values)); /** * Get slot number for storage long string in contract. Basically for getStorage method * returns slotNumber where will data placed * @param mainSlotNumber - the slot number where will be stored hash of long string * @returns - the slot number where will be stored long string */ export const getStorageSlotNumForLongString = (mainSlotNumber: number | string) => sha3( `0x${(typeof mainSlotNumber === 'number' ? mainSlotNumber.toString() : mainSlotNumber ).padStart(64, '0')}`, );