import { RLP } from '@ethereumjs/rlp'; import { keccak256 } from 'ethereum-cryptography/keccak.js'; import { secp256k1 } from 'ethereum-cryptography/secp256k1.js'; import { bigIntToUnpaddedBytes, bytesToBigInt, bytesToHex, bytesToInt, concatBytes, equalsBytes, hexToBytes, intToUnpaddedBytes, toBytes, utf8ToBytes, zeros, } from './bytes.js'; import { BIGINT_0, KECCAK256_NULL, KECCAK256_RLP } from './constants.js'; import { assertIsBytes, assertIsHexString, assertIsString } from './helpers.js'; import { stripHexPrefix } from './internal.js'; /** * Account class to load and maintain the basic account objects. * Supports partial loading and access required for verkle with null * as the placeholder. * * Note: passing undefined in constructor is different from null * While undefined leads to default assignment, null is retained * to track the information not available/loaded because of partial * witness access */ export class Account { /** * This constructor assigns and validates the values. * Use the static factory methods to assist in creating an Account from varying data types. * undefined get assigned with the defaults present, but null args are retained as is */ constructor(nonce = BIGINT_0, balance = BIGINT_0, storageRoot = KECCAK256_RLP, codeHash = KECCAK256_NULL, codeSize = null, version = 0) { this._nonce = null; this._balance = null; this._storageRoot = null; this._codeHash = null; // codeSize and version is separately stored in VKT this._codeSize = null; this._version = null; this._nonce = nonce; this._balance = balance; this._storageRoot = storageRoot; this._codeHash = codeHash; if (codeSize === null && codeHash !== null && !this.isContract()) { codeSize = 0; } this._codeSize = codeSize; this._version = version; this._validate(); } get version() { if (this._version !== null) { return this._version; } else { throw Error(`version=${this._version} not loaded`); } } set version(_version) { this._version = _version; } get nonce() { if (this._nonce !== null) { return this._nonce; } else { throw Error(`nonce=${this._nonce} not loaded`); } } set nonce(_nonce) { this._nonce = _nonce; } get balance() { if (this._balance !== null) { return this._balance; } else { throw Error(`balance=${this._balance} not loaded`); } } set balance(_balance) { this._balance = _balance; } get storageRoot() { if (this._storageRoot !== null) { return this._storageRoot; } else { throw Error(`storageRoot=${this._storageRoot} not loaded`); } } set storageRoot(_storageRoot) { this._storageRoot = _storageRoot; } get codeHash() { if (this._codeHash !== null) { return this._codeHash; } else { throw Error(`codeHash=${this._codeHash} not loaded`); } } set codeHash(_codeHash) { this._codeHash = _codeHash; } get codeSize() { if (this._codeSize !== null) { return this._codeSize; } else { throw Error(`codeHash=${this._codeSize} not loaded`); } } set codeSize(_codeSize) { this._codeSize = _codeSize; } static fromAccountData(accountData) { const { nonce, balance, storageRoot, codeHash } = accountData; if (nonce === null || balance === null || storageRoot === null || codeHash === null) { throw Error(`Partial fields not supported in fromAccountData`); } return new Account(nonce !== undefined ? bytesToBigInt(toBytes(nonce)) : undefined, balance !== undefined ? bytesToBigInt(toBytes(balance)) : undefined, storageRoot !== undefined ? toBytes(storageRoot) : undefined, codeHash !== undefined ? toBytes(codeHash) : undefined); } static fromPartialAccountData(partialAccountData) { const { nonce, balance, storageRoot, codeHash, codeSize, version } = partialAccountData; if (nonce === null && balance === null && storageRoot === null && codeHash === null && codeSize === null && version === null) { throw Error(`All partial fields null`); } return new Account(nonce !== undefined && nonce !== null ? bytesToBigInt(toBytes(nonce)) : nonce, balance !== undefined && balance !== null ? bytesToBigInt(toBytes(balance)) : balance, storageRoot !== undefined && storageRoot !== null ? toBytes(storageRoot) : storageRoot, codeHash !== undefined && codeHash !== null ? toBytes(codeHash) : codeHash, codeSize !== undefined && codeSize !== null ? bytesToInt(toBytes(codeSize)) : codeSize, version !== undefined && version !== null ? bytesToInt(toBytes(version)) : version); } static fromRlpSerializedAccount(serialized) { const values = RLP.decode(serialized); if (!Array.isArray(values)) { throw new Error('Invalid serialized account input. Must be array'); } return this.fromValuesArray(values); } static fromRlpSerializedPartialAccount(serialized) { const values = RLP.decode(serialized); if (!Array.isArray(values)) { throw new Error('Invalid serialized account input. Must be array'); } let nonce = null; if (!Array.isArray(values[0])) { throw new Error('Invalid partial nonce encoding. Must be array'); } else { const isNotNullIndicator = bytesToInt(values[0][0]); if (isNotNullIndicator !== 0 && isNotNullIndicator !== 1) { throw new Error(`Invalid isNullIndicator=${isNotNullIndicator} for nonce`); } if (isNotNullIndicator === 1) { nonce = bytesToBigInt(values[0][1]); } } let balance = null; if (!Array.isArray(values[1])) { throw new Error('Invalid partial balance encoding. Must be array'); } else { const isNotNullIndicator = bytesToInt(values[1][0]); if (isNotNullIndicator !== 0 && isNotNullIndicator !== 1) { throw new Error(`Invalid isNullIndicator=${isNotNullIndicator} for balance`); } if (isNotNullIndicator === 1) { balance = bytesToBigInt(values[1][1]); } } let storageRoot = null; if (!Array.isArray(values[2])) { throw new Error('Invalid partial storageRoot encoding. Must be array'); } else { const isNotNullIndicator = bytesToInt(values[2][0]); if (isNotNullIndicator !== 0 && isNotNullIndicator !== 1) { throw new Error(`Invalid isNullIndicator=${isNotNullIndicator} for storageRoot`); } if (isNotNullIndicator === 1) { storageRoot = values[2][1]; } } let codeHash = null; if (!Array.isArray(values[3])) { throw new Error('Invalid partial codeHash encoding. Must be array'); } else { const isNotNullIndicator = bytesToInt(values[3][0]); if (isNotNullIndicator !== 0 && isNotNullIndicator !== 1) { throw new Error(`Invalid isNullIndicator=${isNotNullIndicator} for codeHash`); } if (isNotNullIndicator === 1) { codeHash = values[3][1]; } } let codeSize = null; if (!Array.isArray(values[4])) { throw new Error('Invalid partial codeSize encoding. Must be array'); } else { const isNotNullIndicator = bytesToInt(values[4][0]); if (isNotNullIndicator !== 0 && isNotNullIndicator !== 1) { throw new Error(`Invalid isNullIndicator=${isNotNullIndicator} for codeSize`); } if (isNotNullIndicator === 1) { codeSize = bytesToInt(values[4][1]); } } let version = null; if (!Array.isArray(values[5])) { throw new Error('Invalid partial version encoding. Must be array'); } else { const isNotNullIndicator = bytesToInt(values[5][0]); if (isNotNullIndicator !== 0 && isNotNullIndicator !== 1) { throw new Error(`Invalid isNullIndicator=${isNotNullIndicator} for version`); } if (isNotNullIndicator === 1) { version = bytesToInt(values[5][1]); } } return this.fromPartialAccountData({ balance, nonce, storageRoot, codeHash, codeSize, version }); } static fromValuesArray(values) { const [nonce, balance, storageRoot, codeHash] = values; return new Account(bytesToBigInt(nonce), bytesToBigInt(balance), storageRoot, codeHash); } _validate() { if (this._nonce !== null && this._nonce < BIGINT_0) { throw new Error('nonce must be greater than zero'); } if (this._balance !== null && this._balance < BIGINT_0) { throw new Error('balance must be greater than zero'); } if (this._storageRoot !== null && this._storageRoot.length !== 32) { throw new Error('storageRoot must have a length of 32'); } if (this._codeHash !== null && this._codeHash.length !== 32) { throw new Error('codeHash must have a length of 32'); } if (this._codeSize !== null && this._codeSize < BIGINT_0) { throw new Error('codeSize must be greater than zero'); } } /** * Returns an array of Uint8Arrays of the raw bytes for the account, in order. */ raw() { return [ bigIntToUnpaddedBytes(this.nonce), bigIntToUnpaddedBytes(this.balance), this.storageRoot, this.codeHash, ]; } /** * Returns the RLP serialization of the account as a `Uint8Array`. */ serialize() { return RLP.encode(this.raw()); } serializeWithPartialInfo() { const partialData = []; const zeroEncoded = intToUnpaddedBytes(0); const oneEncoded = intToUnpaddedBytes(1); if (this._nonce !== null) { partialData.push([oneEncoded, bigIntToUnpaddedBytes(this._nonce)]); } else { partialData.push([zeroEncoded]); } if (this._balance !== null) { partialData.push([oneEncoded, bigIntToUnpaddedBytes(this._balance)]); } else { partialData.push([zeroEncoded]); } if (this._storageRoot !== null) { partialData.push([oneEncoded, this._storageRoot]); } else { partialData.push([zeroEncoded]); } if (this._codeHash !== null) { partialData.push([oneEncoded, this._codeHash]); } else { partialData.push([zeroEncoded]); } if (this._codeSize !== null) { partialData.push([oneEncoded, intToUnpaddedBytes(this._codeSize)]); } else { partialData.push([zeroEncoded]); } if (this._version !== null) { partialData.push([oneEncoded, intToUnpaddedBytes(this._version)]); } else { partialData.push([zeroEncoded]); } return RLP.encode(partialData); } /** * Returns a `Boolean` determining if the account is a contract. */ isContract() { if (this._codeHash === null && this._codeSize === null) { throw Error(`Insufficient data as codeHash=null and codeSize=null`); } return ((this._codeHash !== null && !equalsBytes(this._codeHash, KECCAK256_NULL)) || (this._codeSize !== null && this._codeSize !== 0)); } /** * Returns a `Boolean` determining if the account is empty complying to the definition of * account emptiness in [EIP-161](https://eips.ethereum.org/EIPS/eip-161): * "An account is considered empty when it has no code and zero nonce and zero balance." */ isEmpty() { // helpful for determination in partial accounts if ((this._balance !== null && this.balance !== BIGINT_0) || (this._nonce === null && this.nonce !== BIGINT_0) || (this._codeHash !== null && !equalsBytes(this.codeHash, KECCAK256_NULL))) { return false; } return (this.balance === BIGINT_0 && this.nonce === BIGINT_0 && equalsBytes(this.codeHash, KECCAK256_NULL)); } } /** * Checks if the address is a valid. Accepts checksummed addresses too. */ export const isValidAddress = function (hexAddress) { try { assertIsString(hexAddress); } catch (e) { return false; } return /^0x[0-9a-fA-F]{40}$/.test(hexAddress); }; /** * Returns a checksummed address. * * If an eip1191ChainId is provided, the chainId will be included in the checksum calculation. This * has the effect of checksummed addresses for one chain having invalid checksums for others. * For more details see [EIP-1191](https://eips.ethereum.org/EIPS/eip-1191). * * WARNING: Checksums with and without the chainId will differ and the EIP-1191 checksum is not * backwards compatible to the original widely adopted checksum format standard introduced in * [EIP-55](https://eips.ethereum.org/EIPS/eip-55), so this will break in existing applications. * Usage of this EIP is therefore discouraged unless you have a very targeted use case. */ export const toChecksumAddress = function (hexAddress, eip1191ChainId) { assertIsHexString(hexAddress); const address = stripHexPrefix(hexAddress).toLowerCase(); let prefix = ''; if (eip1191ChainId !== undefined) { const chainId = bytesToBigInt(toBytes(eip1191ChainId)); prefix = chainId.toString() + '0x'; } const bytes = utf8ToBytes(prefix + address); const hash = bytesToHex(keccak256(bytes)).slice(2); let ret = ''; for (let i = 0; i < address.length; i++) { if (parseInt(hash[i], 16) >= 8) { ret += address[i].toUpperCase(); } else { ret += address[i]; } } return `0x${ret}`; }; /** * Checks if the address is a valid checksummed address. * * See toChecksumAddress' documentation for details about the eip1191ChainId parameter. */ export const isValidChecksumAddress = function (hexAddress, eip1191ChainId) { return isValidAddress(hexAddress) && toChecksumAddress(hexAddress, eip1191ChainId) === hexAddress; }; /** * Generates an address of a newly created contract. * @param from The address which is creating this new address * @param nonce The nonce of the from account */ export const generateAddress = function (from, nonce) { assertIsBytes(from); assertIsBytes(nonce); if (bytesToBigInt(nonce) === BIGINT_0) { // in RLP we want to encode null in the case of zero nonce // read the RLP documentation for an answer if you dare return keccak256(RLP.encode([from, Uint8Array.from([])])).subarray(-20); } // Only take the lower 160bits of the hash return keccak256(RLP.encode([from, nonce])).subarray(-20); }; /** * Generates an address for a contract created using CREATE2. * @param from The address which is creating this new address * @param salt A salt * @param initCode The init code of the contract being created */ export const generateAddress2 = function (from, salt, initCode) { assertIsBytes(from); assertIsBytes(salt); assertIsBytes(initCode); if (from.length !== 20) { throw new Error('Expected from to be of length 20'); } if (salt.length !== 32) { throw new Error('Expected salt to be of length 32'); } const address = keccak256(concatBytes(hexToBytes('0xff'), from, salt, keccak256(initCode))); return address.subarray(-20); }; /** * Checks if the private key satisfies the rules of the curve secp256k1. */ export const isValidPrivate = function (privateKey) { return secp256k1.utils.isValidPrivateKey(privateKey); }; /** * Checks if the public key satisfies the rules of the curve secp256k1 * and the requirements of Ethereum. * @param publicKey The two points of an uncompressed key, unless sanitize is enabled * @param sanitize Accept public keys in other formats */ export const isValidPublic = function (publicKey, sanitize = false) { assertIsBytes(publicKey); if (publicKey.length === 64) { // Convert to SEC1 for secp256k1 // Automatically checks whether point is on curve try { secp256k1.ProjectivePoint.fromHex(concatBytes(Uint8Array.from([4]), publicKey)); return true; } catch (e) { return false; } } if (!sanitize) { return false; } try { secp256k1.ProjectivePoint.fromHex(publicKey); return true; } catch (e) { return false; } }; /** * Returns the ethereum address of a given public key. * Accepts "Ethereum public keys" and SEC1 encoded keys. * @param pubKey The two points of an uncompressed key, unless sanitize is enabled * @param sanitize Accept public keys in other formats */ export const pubToAddress = function (pubKey, sanitize = false) { assertIsBytes(pubKey); if (sanitize && pubKey.length !== 64) { pubKey = secp256k1.ProjectivePoint.fromHex(pubKey).toRawBytes(false).slice(1); } if (pubKey.length !== 64) { throw new Error('Expected pubKey to be of length 64'); } // Only take the lower 160bits of the hash return keccak256(pubKey).subarray(-20); }; export const publicToAddress = pubToAddress; /** * Returns the ethereum public key of a given private key. * @param privateKey A private key must be 256 bits wide */ export const privateToPublic = function (privateKey) { assertIsBytes(privateKey); // skip the type flag and use the X, Y points return secp256k1.ProjectivePoint.fromPrivateKey(privateKey).toRawBytes(false).slice(1); }; /** * Returns the ethereum address of a given private key. * @param privateKey A private key must be 256 bits wide */ export const privateToAddress = function (privateKey) { return publicToAddress(privateToPublic(privateKey)); }; /** * Converts a public key to the Ethereum format. */ export const importPublic = function (publicKey) { assertIsBytes(publicKey); if (publicKey.length !== 64) { publicKey = secp256k1.ProjectivePoint.fromHex(publicKey).toRawBytes(false).slice(1); } return publicKey; }; /** * Returns the zero address. */ export const zeroAddress = function () { const addressLength = 20; const addr = zeros(addressLength); return bytesToHex(addr); }; /** * Checks if a given address is the zero address. */ export const isZeroAddress = function (hexAddress) { try { assertIsString(hexAddress); } catch (e) { return false; } const zeroAddr = zeroAddress(); return zeroAddr === hexAddress; }; export function accountBodyFromSlim(body) { const [nonce, balance, storageRoot, codeHash] = body; return [ nonce, balance, storageRoot.length === 0 ? KECCAK256_RLP : storageRoot, codeHash.length === 0 ? KECCAK256_NULL : codeHash, ]; } const emptyUint8Arr = new Uint8Array(0); export function accountBodyToSlim(body) { const [nonce, balance, storageRoot, codeHash] = body; return [ nonce, balance, equalsBytes(storageRoot, KECCAK256_RLP) ? emptyUint8Arr : storageRoot, equalsBytes(codeHash, KECCAK256_NULL) ? emptyUint8Arr : codeHash, ]; } /** * Converts a slim account (per snap protocol spec) to the RLP encoded version of the account * @param body Array of 4 Uint8Array-like items to represent the account * @returns RLP encoded version of the account */ export function accountBodyToRLP(body, couldBeSlim = true) { const accountBody = couldBeSlim ? accountBodyFromSlim(body) : body; return RLP.encode(accountBody); } //# sourceMappingURL=account.js.map