UNPKG

necjs

Version:
1,438 lines (1,426 loc) 2.26 MB
import axios, { AxiosError } from 'axios'; import fs from 'fs'; import 'path'; import { webcrypto } from 'node:crypto'; import require$$0$3 from 'events'; import require$$1$1 from 'https'; import require$$2 from 'http'; import require$$3 from 'net'; import require$$4 from 'tls'; import require$$1 from 'crypto'; import require$$0$2 from 'stream'; import require$$7 from 'url'; import require$$0 from 'zlib'; import require$$0$1 from 'buffer'; // src/utils.ts // Generic base for decimal handling const TEN = BigInt(10); // Default number of decimals (e.g., for Ether or NEC token) const DEFAULT_DECIMALS = 18; // NEC token decimals (replace if different) const NEC_DECIMALS = 18; // BigInt factor for converting whole units ↔ base units const WEI_FACTOR = TEN ** BigInt(DEFAULT_DECIMALS); /** * Convert a hex string to a decimal (string or number). * Assumes input is already in base units. */ function hexToDecimalString(hex) { if (typeof hex !== 'string') { throw new TypeError(`hexToDecimalString: expected a string, got ${typeof hex}`); } const raw = hex.trim().toLowerCase(); if (raw === '0x') return 0; const normalized = raw.startsWith('0x') ? raw : `0x${raw}`; if (!/^0x[0-9a-f]+$/.test(normalized)) { throw new Error(`hexToDecimalString: invalid hex string "${hex}"`); } const asDec = BigInt(normalized).toString(10); return isNaN(Number(asDec)) ? asDec : Number(asDec); } /** * Convert a hex string to a decimal-string (no extra multiplication). * Use for normalizing RPC response fields already in base units. */ function normalizeHexField(key, hex) { if (hex === '0x') return '0'; return BigInt(hex).toString(10); } /** * Serialize a decimal (number, numeric-string, or bigint) to hex-with-0x. * Assumes input is already in base units. */ function decimalToHex(value) { return '0x' + BigInt(value).toString(16); } /** * Generic: parse whole- or fractional-unit amount into base-unit hex. * Accepts number|string|bigint, handles fractional up to `decimals`. */ function parseUnits(value, decimals = DEFAULT_DECIMALS) { let str; if (typeof value === 'number') { str = value.toFixed(decimals); } else { str = value.toString(); } const [wholePart, fracPart = ''] = str.split('.'); if (!/^\d+$/.test(wholePart) || !/^\d*$/.test(fracPart)) { throw new Error(`parseUnits: invalid numeric value "${value}"`); } if (fracPart.length > decimals) { throw new Error(`parseUnits: too many decimal places (max ${decimals}) in "${value}"`); } const factor = decimals === DEFAULT_DECIMALS ? WEI_FACTOR : TEN ** BigInt(decimals); const whole = BigInt(wholePart) * factor; const frac = BigInt(fracPart.padEnd(decimals, '0')); const combined = whole + frac; return '0x' + combined.toString(16); } /** * Convert an Ether value (number|string|bigint), including fractional, * → Wei → hex-with-0x. */ function etherToWeiHex(value) { return parseUnits(value, DEFAULT_DECIMALS); } /** * Convert a Wei-hex (or bigint or numeric string) into an Ether decimal string. */ function hexToEther(value) { return formatUnits(value, DEFAULT_DECIMALS); } /** * Generic: format a base-unit amount (hex, number, or bigint) * into a human-readable decimal string. */ function formatUnits(value, decimals = DEFAULT_DECIMALS) { const big = typeof value === 'string' && value.startsWith('0x') ? BigInt(value) : BigInt(value); const factor = decimals === DEFAULT_DECIMALS ? WEI_FACTOR : TEN ** BigInt(decimals); const integer = big / factor; const fraction = big % factor; let fracStr = fraction .toString() .padStart(decimals, '0') .replace(/0+$/, ''); return fracStr ? `${integer.toString()}.${fracStr}` : integer.toString(); } /** * Convert a NEC base-unit amount (hex, number, or bigint) into a NEC decimal string. */ function hexToNec(value) { return formatUnits(value, NEC_DECIMALS); } /** * Convert a whole-NEC amount (number|string|bigint) into base-unit hex. */ function necToHex(value) { return parseUnits(value, NEC_DECIMALS); } /** * Convert a Wei (number, bigint, or hex string) directly into a NEC decimal string. * Useful when NEC is pegged 1:1 with Ether base units. */ function weiToNec(value) { return formatUnits(value, NEC_DECIMALS); } /** * Walk and serialize all fields in TxParams for JSON-RPC. */ function serializeForRpc(payload) { const out = {}; for (const [key, val] of Object.entries(payload)) { if (typeof val === 'number' || (/^([0-9]*\.?[0-9]+)$/.test(val))) { if (key === 'value') { out[key] = etherToWeiHex(val); } else if (key.toLowerCase().includes('amount') || key.toLowerCase().includes('balance')) { out[key] = parseUnits(val); } else { out[key] = decimalToHex(val); } } else { out[key] = val; } } return out; } /** * Walk and normalize JSON-RPC response (hex → decimal string or number). */ function normalizeResponse(resp) { if (resp === null) return {}; if (typeof resp === 'boolean') return resp; if (typeof resp === 'string') { if (/^0x[a-fA-F0-9]{40,}$/.test(resp)) return resp; return hexToDecimalString(resp); } if (Array.isArray(resp)) { return resp.map((v) => typeof v === 'object' ? normalizeResponse(v) : v); } const out = {}; for (const [key, val] of Object.entries(resp)) { if (typeof val === 'string' && val.startsWith('0x')) { if (['address', 'hash', 'from', 'to', 'transactionHash', 'blockHash', 'contractAddress'] .includes(key)) { out[key] = val; } else { out[key] = normalizeHexField(key, val); } } else if (Array.isArray(val)) { out[key] = val.map((v) => typeof v === 'object' ? normalizeResponse(v) : v); } else if (val && typeof val === 'object') { out[key] = normalizeResponse(val); } else { out[key] = val; } } return out; } /** * Checks if a string is a valid Ethereum/EVM address. */ function isValidAddress(address) { return (typeof address === 'string' && address.startsWith('0x') && address.length === 42 && /^[0-9a-fA-F]{40}$/.test(address.slice(2))); } /** * Convert a decimal Ether value (number|string|bigint) to a Wei value as a string (base 10, not hex). * E.g., 1.23 -> '1230000000000000000' */ function decimalToWei(value, decimals = DEFAULT_DECIMALS) { // Use parseUnits to get the hex, then convert to BigInt and return as string if (typeof value === 'number' || typeof value === 'string' || typeof value === 'bigint') { const str = value.toString(); const hex = parseUnits(str, decimals); // returns hex string return BigInt(hex).toString(10); } throw new Error('decimalToWei: invalid value'); } /** * Represents a structured error returned from a JSON-RPC call. */ class RpcError extends Error { constructor(message, code, data) { super(`RPC Error: ${message} (code: ${code})`); this.name = 'RpcError'; this.code = code; this.data = data; } } /** * The Provider class is a low-level wrapper for making JSON-RPC requests to an NCOG chain node. * It handles request creation, error parsing, and provides convenience methods for all standard RPC calls. */ class Provider { /** * Register a request middleware function. Called before sending each request. */ useRequest(middleware) { this.requestMiddleware.push(middleware); } /** * Register a response middleware function. Called after receiving each response. */ useResponse(middleware) { this.responseMiddleware.push(middleware); } /** * @param url The URL of the JSON-RPC endpoint (e.g., "http://localhost:8545"). */ constructor(url) { this.idCounter = 1; this.requestMiddleware = []; this.responseMiddleware = []; this.url = url; } /** * Performs a raw JSON-RPC request. This is the core private method used by all others. * @param method The RPC method name. * @param params An array of parameters for the RPC method. * @returns The result from the RPC call. * @throws {RpcError} if the RPC call returns a JSON-RPC error object. * @throws {Error} for network or other request-level errors. */ async rpc(method, params = []) { var _a, _b, _c; if (!this.url) { throw new Error('Provider URL is not set'); } let payload = { jsonrpc: '2.0', id: this.idCounter++, method, params }; // Apply request middleware for (const mw of this.requestMiddleware) { payload = await mw(payload); } try { const { data } = await axios.post(this.url, payload); let response = data; // Apply response middleware for (const mw of this.responseMiddleware) { response = await mw(response, payload); } if (response === null || response === void 0 ? void 0 : response.error) { throw new RpcError((_a = response === null || response === void 0 ? void 0 : response.error) === null || _a === void 0 ? void 0 : _a.message, (_b = response === null || response === void 0 ? void 0 : response.error) === null || _b === void 0 ? void 0 : _b.code, (_c = response === null || response === void 0 ? void 0 : response.error) === null || _c === void 0 ? void 0 : _c.data); } return normalizeResponse((response === null || response === void 0 ? void 0 : response.result) || response); } catch (error) { if (error instanceof AxiosError) { throw new Error(`RPC request failed for method "${method}": ${error.message}`); } throw error; } } /** * Performs a batch of JSON-RPC requests. Returns an array of results/errors in the same order. * @param calls Array of { method, params } objects. * @returns Array of results or errors (in order). */ async batchRpc(calls) { if (!this.url) { throw new Error('Provider URL is not set'); } let payloads = calls.map((call, i) => ({ jsonrpc: '2.0', id: this.idCounter + i, method: call.method, params: call.params || [] })); // Apply request middleware to each payload for (const mw of this.requestMiddleware) { payloads = await Promise.all(payloads.map(p => mw(p))); } try { const { data } = await axios.post(this.url, payloads); let results = Array.isArray(data) ? data : [data]; // Apply response middleware to each result for (const mw of this.responseMiddleware) { results = await Promise.all(results.map((r, i) => mw(r, payloads[i]))); } results.sort((a, b) => a.id - b.id); return results.map(res => { if (res.error) { return { error: res.error }; } return normalizeResponse(res.result || res); }); } catch (error) { return calls.map(() => ({ error: error.message || error })); } } /** * Provides a public way to make any RPC call, for methods not explicitly wrapped. * @param method The RPC method name. * @param params An array of parameters for the RPC method. */ async callRpc(method, params = []) { // Serialize all params for RPC const serializedParams = params.map(p => typeof p === 'object' && p !== null ? serializeForRpc(p) : p); return this.rpc(method, serializedParams); } // --- web3 --- /** * Returns the client version of the node. */ async clientVersion() { return this.rpc('web3_clientVersion'); } // --- net --- /** * Returns the current network ID. */ async netVersion() { return this.rpc('net_version'); } /** * Returns true if the client is actively listening for network connections. */ async listening() { return this.rpc('net_listening'); } /** * Returns the number of peers currently connected to the client. */ async peerCount() { return this.rpc('net_peerCount'); } // --- eth --- /** * Returns the current protocol version. */ async protocolVersion() { return this.rpc('eth_protocolVersion'); } /** * Returns an object with data about the sync status or `false` if not syncing. */ async syncing() { return this.rpc('eth_syncing'); } /** * Returns the coinbase address of the client. */ async coinbase() { return this.rpc('eth_coinbase'); } /** * Returns the number of hashes per second that the node is mining with. */ async hashrate() { return this.rpc('eth_hashrate'); } /** * Returns the current chain ID. */ async getChainId() { return await this.rpc('eth_chainId'); } /** * Returns the current price per gas in wei. */ async getGasPrice() { return this.rpc('eth_gasPrice'); } /** * Returns a list of accounts owned by the client. */ async accounts() { return this.rpc('eth_accounts'); } /** * Returns the number of the most recent block. */ async getBlockNumber() { return await this.rpc('eth_blockNumber'); } /** * Returns the balance of an account in wei. * @param address The address to get the balance of. * @param tag The block tag (e.g., "latest", "earliest", "pending", or a block number). Defaults to "latest". */ async getBalance(address, tag = 'latest') { const balance = await this.rpc('eth_getBalance', [address, tag]); const convertedBalance = weiToNec(balance); return isNaN(Number(convertedBalance)) ? 0 : Number(convertedBalance); } /** * Returns the value from a storage position at a given address. * @param address Address of the storage. * @param position Hex of the position in storage. * @param tag Block tag. Defaults to "latest". */ async getStorageAt(address, position, tag = 'latest') { return this.rpc('eth_getStorageAt', [address, position, tag]); } /** * Returns the number of transactions sent from an address. * @param address The address. * @param tag The block tag. Defaults to "latest". */ async getTransactionCount(address, tag = 'latest') { return await this.rpc('eth_getTransactionCount', [address, tag]); } /** * Returns the number of transactions in a block from a block matching the given block number. * @param tag The block tag. */ async getBlockTransactionCountByNumber(tag) { return await this.rpc('eth_getBlockTransactionCountByNumber', [tag]); } /** * Returns the code at a given address. * @param address The address. * @param tag The block tag. Defaults to "latest". */ async getCode(address, tag = 'latest') { return this.rpc('eth_getCode', [address, tag]); } /** * Returns a block matching the given block number. * @param tag The block tag or number. * @param full If true, returns full transaction objects; otherwise, only transaction hashes. */ async getBlockByNumber(tag, full = false) { return this.rpc('eth_getBlockByNumber', [tag, full]); } /** * Returns a block matching the given block hash. * @param hash The hash of the block. * @param full If true, returns full transaction objects; otherwise, only transaction hashes. */ async getBlockByHash(hash, full = false) { return this.rpc('eth_getBlockByHash', [hash, full]); } /** * Calculates a signature for data, using a specific account. * The account must be unlocked on the node. * @param address The address to sign with. * @param data The data to sign. */ async sign(address, data) { return this.rpc('eth_sign', [address, data]); } /** * Asks the remote node to sign a transaction with an unlocked account. * @param txObj The transaction object to sign. * @returns An object containing the raw signed transaction and the decoded transaction fields. */ async signTransaction(txObj) { const rpcParams = serializeForRpc(txObj); return this.rpc('eth_signTransaction', [rpcParams]); } /** * Submits a transaction to be signed and broadcasted by the remote node. * The `from` account must be unlocked. * @param obj The transaction object. */ async sendTransaction(obj) { const rpcParams = serializeForRpc(obj); return this.rpc('eth_sendTransaction', [rpcParams]); } /** * Submits a pre-signed transaction to the network. * @param signedTx The hex-encoded signed transaction. * @returns The transaction hash. */ async sendRawTransaction(signedTx) { return this.rpc('eth_sendRawTransaction', [signedTx]); } /** * Executes a message call immediately without creating a transaction on the block-chain (read-only). * @param tx The transaction call object. * @param tag The block tag. Defaults to "latest". */ async call(tx, tag = 'latest') { const rpcTx = serializeForRpc(tx); return this.rpc('eth_call', [rpcTx, tag]); } /** * Estimates the gas necessary to execute a specific transaction. * @param obj The transaction object. */ async estimateGas(obj) { const rpcObj = serializeForRpc(obj); return await this.rpc('eth_estimateGas', [rpcObj]); } /** * Returns a transaction by its hash. * @param hash The hash of the transaction. */ async getTransactionByHash(hash) { return this.rpc('eth_getTransactionByHash', [hash]); } /** * Returns the receipt of a transaction by its hash. * @param hash The hash of the transaction. */ async getTransactionReceipt(hash) { return this.rpc('eth_getTransactionReceipt', [hash]); } /** * Returns an array of all logs matching a given filter object. * @param filter The filter object. */ async getLogs(filter) { return this.rpc('eth_getLogs', [filter]); } // --- Mining --- /** * Used for submitting a proof-of-work solution. */ async submitWork(nonce, powHash, mixDigest) { return this.rpc('eth_submitWork', [nonce, powHash, mixDigest]); } /** * Used for obtaining a proof-of-work problem. */ async getWork() { return this.rpc('eth_getWork'); } // --- personal --- /** * Creates a new account in the node's keystore. * @param password The password to protect the account with. */ async newAccount(password) { return this.rpc('personal_newAccount', [password]); } /** * Imports an unencrypted private key into the node's keystore. * @param privateKey The raw private key. * @param password The password to encrypt the key with. */ async importRawKey(privateKey, password) { return this.rpc('personal_importRawKey', [privateKey, password]); } /** * Signs data with a specific account. * The account must be unlocked on the node. * @param data The data to sign. * @param address The address to sign with. * @param password The password for the account. */ async personalSign(data, address, password) { return this.rpc('personal_sign', [data, address, password]); } /** * Recovers the address that signed a piece of data. * @param data The original data. * @param signature The signature. */ async ecRecover(data, signature) { return this.rpc('personal_ecRecover', [data, signature]); } /** * Unlocks a specified account for a given duration. * @param address The address to unlock. * @param password The account's password. * @param duration The duration in seconds to keep the account unlocked. Defaults to 300. */ async unlockAccount(address, password, duration) { return this.rpc('personal_unlockAccount', [address, password, duration]); } /** * Locks a specified account. * @param address The address to lock. */ async lockAccount(address) { return this.rpc('personal_lockAccount', [address]); } /** * Sends a transaction from an account in the node's keystore. * @param tx The transaction object. * @param password The password for the `from` account. */ async sendPersonalTransaction(tx, password) { return this.rpc('personal_sendTransaction', [tx, password]); } /** * Resolves an ENS name to an Ethereum address using the ENS registry contract. * @param ensName The ENS name to resolve (e.g., 'vitalik.eth'). * @param registryAddress The ENS registry contract address (optional, defaults to mainnet address). * @returns The resolved Ethereum address, or null if not found. */ async resolveEnsName(ensName, registryAddress = '0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e') { try { const { namehash } = require('ethers'); const node = namehash(ensName); // ENS registry ABI: function resolver(bytes32 node) external view returns (address) const data = '0x0178b8bf' + node.replace(/^0x/, ''); // resolver(bytes32) selector + node const callObj = { to: registryAddress, data }; const resolverAddr = await this.call(callObj); if (!resolverAddr || resolverAddr === '0x' || /^0x0+$/.test(resolverAddr)) return null; // ENS resolver ABI: function addr(bytes32 node) external view returns (address) const addrSelector = '0x3b3b57de'; const data2 = addrSelector + node.replace(/^0x/, ''); const callObj2 = { to: resolverAddr, data: data2 }; const address = await this.call(callObj2); if (!address || address === '0x' || /^0x0+$/.test(address)) return null; return address; } catch (err) { return null; } } } var provider = /*#__PURE__*/Object.freeze({ __proto__: null, Provider: Provider, RpcError: RpcError }); // Unified MLKEM interface for both Node.js and browser environments class WasmError extends Error { constructor(message, context) { super(`WASM Error: ${message}`); this.name = 'WasmError'; this.context = context; } } // Environment detection const isNode$1 = typeof process !== 'undefined' && process.versions && process.versions.node; const isBrowser$1 = typeof window !== 'undefined' && typeof document !== 'undefined'; const isReactNative$1 = typeof navigator !== 'undefined' && navigator.product === 'ReactNative'; /** * Load and initialize the MLKEM Go WebAssembly module. * Automatically detects environment and uses appropriate loader. */ async function loadWasm$2() { if (isNode$1) { // Use Node.js loader const { loadWasm: loadNodeWasm } = await Promise.resolve().then(function () { return mlkemNode; }); return await loadNodeWasm(); } else if (isBrowser$1 || isReactNative$1) { // Use browser loader for browser and React Native const { loadWasm: loadBrowserWasm } = await Promise.resolve().then(function () { return mlkemBrowser; }); return await loadBrowserWasm(); } else { throw new WasmError('Unsupported environment. This package requires Node.js, browser, or React Native.', { env: { isNode: isNode$1, isBrowser: isBrowser$1, isReactNative: isReactNative$1 } }); } } /** * Load WASM from buffer (useful for bundlers that inline WASM) */ async function loadWasmFromBuffer$1(wasmBuffer) { if (isNode$1) { throw new WasmError('loadWasmFromBuffer is not supported in Node.js environment. Use loadWasm() instead.', { env: 'node' }); } else if (isBrowser$1 || isReactNative$1) { const { loadWasmFromBuffer: loadBrowserWasmFromBuffer } = await Promise.resolve().then(function () { return mlkemBrowser; }); return await loadBrowserWasmFromBuffer(wasmBuffer); } else { throw new WasmError('Unsupported environment. This package requires Node.js, browser, or React Native.', { env: { isNode: isNode$1, isBrowser: isBrowser$1, isReactNative: isReactNative$1 } }); } } // src/wallet.ts class Wallet { constructor(mlkem, privateKey) { this.mlkem = mlkem; this.privateKey = privateKey; this.address = this.mlkem.privateKeyToAddress(privateKey); } static async create(hexPrivateKey) { const mlkem = await loadWasm$2(); return new Wallet(mlkem, hexPrivateKey); } connect(provider) { return new Signer(provider, this); } /** * Unified connect: creates a Wallet, Provider, and Signer in one call. * @param hexPrivateKey The private key as a hex string. * @param providerUrl The RPC URL (optional, defaults to http://localhost:8545) * @returns { signer, provider, address } */ static async connect(hexPrivateKey, providerUrl) { const wallet = await Wallet.create(hexPrivateKey); const provider$1 = new (await Promise.resolve().then(function () { return provider; })).Provider(providerUrl || 'http://localhost:8545'); const signer = wallet.connect(provider$1); return { signer, provider: provider$1, address: wallet.address }; } } class Signer { constructor(provider, wallet) { this.provider = provider; this.wallet = wallet; } get address() { return this.wallet.address; } async getAddress() { return this.wallet.address; } async sendTransaction(txParams) { // const rpcParams = serializeForRpc(txParams); // console.log("TxParams", txParams) if (!(txParams === null || txParams === void 0 ? void 0 : txParams.chainId)) { txParams.chainId = await this.provider.getChainId(); } if ((!txParams.gas && !txParams.gasLimit) || !txParams.gasPrice || (txParams.nonce !== undefined && txParams.nonce < 0)) { throw new Error('Missing required transaction parameters: gasLimit, gasPrice, nonce'); } if (txParams.value && txParams.value != '0x') { txParams.value = etherToWeiHex(txParams.value); } const rawSignedObj = this.wallet.mlkem.signTransactionMLDSA87(txParams, this.wallet.privateKey); if (!rawSignedObj || (!rawSignedObj.raw && !rawSignedObj.rawTransaction)) { throw new Error('signTransactionMLDSA87 failed: ' + JSON.stringify(rawSignedObj)); } const rawSigned = rawSignedObj.raw || rawSignedObj.rawTransaction; const sendResponse = await this.provider.callRpc('eth_sendRawTransaction', [rawSigned]); if (sendResponse.error) { throw new Error('eth_sendRawTransaction failed: ' + JSON.stringify(sendResponse.error)); } return normalizeResponse((sendResponse === null || sendResponse === void 0 ? void 0 : sendResponse.result) || sendResponse); // returns tx hash } async decode(rawSigned) { const response = this.wallet.mlkem.decodeRLPTransaction(rawSigned); if (response.error) { throw new Error('eth_decodeRawTransaction failed: ' + JSON.stringify(response.error)); } return response; } } /* Do NOT modify this file; see /src.ts/_admin/update-version.ts */ /** * The current version of Ethers. */ const version = "6.15.0"; /** * Property helper functions. * * @_subsection api/utils:Properties [about-properties] */ function checkType(value, type, name) { const types = type.split("|").map(t => t.trim()); for (let i = 0; i < types.length; i++) { switch (type) { case "any": return; case "bigint": case "boolean": case "number": case "string": if (typeof (value) === type) { return; } } } const error = new Error(`invalid value for type ${type}`); error.code = "INVALID_ARGUMENT"; error.argument = `value.${name}`; error.value = value; throw error; } /** * Assigns the %%values%% to %%target%% as read-only values. * * It %%types%% is specified, the values are checked. */ function defineProperties(target, values, types) { for (let key in values) { let value = values[key]; const type = (types ? types[key] : null); if (type) { checkType(value, type, key); } Object.defineProperty(target, key, { enumerable: true, value, writable: false }); } } /** * All errors in ethers include properties to ensure they are both * human-readable (i.e. ``.message``) and machine-readable (i.e. ``.code``). * * The [[isError]] function can be used to check the error ``code`` and * provide a type guard for the properties present on that error interface. * * @_section: api/utils/errors:Errors [about-errors] */ function stringify(value, seen) { if (value == null) { return "null"; } if (seen == null) { seen = new Set(); } if (typeof (value) === "object") { if (seen.has(value)) { return "[Circular]"; } seen.add(value); } if (Array.isArray(value)) { return "[ " + (value.map((v) => stringify(v, seen))).join(", ") + " ]"; } if (value instanceof Uint8Array) { const HEX = "0123456789abcdef"; let result = "0x"; for (let i = 0; i < value.length; i++) { result += HEX[value[i] >> 4]; result += HEX[value[i] & 0xf]; } return result; } if (typeof (value) === "object" && typeof (value.toJSON) === "function") { return stringify(value.toJSON(), seen); } switch (typeof (value)) { case "boolean": case "number": case "symbol": return value.toString(); case "bigint": return BigInt(value).toString(); case "string": return JSON.stringify(value); case "object": { const keys = Object.keys(value); keys.sort(); return "{ " + keys.map((k) => `${stringify(k, seen)}: ${stringify(value[k], seen)}`).join(", ") + " }"; } } return `[ COULD NOT SERIALIZE ]`; } /** * Returns true if the %%error%% matches an error thrown by ethers * that matches the error %%code%%. * * In TypeScript environments, this can be used to check that %%error%% * matches an EthersError type, which means the expected properties will * be set. * * @See [ErrorCodes](api:ErrorCode) * @example * try { * // code.... * } catch (e) { * if (isError(e, "CALL_EXCEPTION")) { * // The Type Guard has validated this object * console.log(e.data); * } * } */ function isError(error, code) { return (error && error.code === code); } /** * Returns a new Error configured to the format ethers emits errors, with * the %%message%%, [[api:ErrorCode]] %%code%% and additional properties * for the corresponding EthersError. * * Each error in ethers includes the version of ethers, a * machine-readable [[ErrorCode]], and depending on %%code%%, additional * required properties. The error message will also include the %%message%%, * ethers version, %%code%% and all additional properties, serialized. */ function makeError(message, code, info) { let shortMessage = message; { const details = []; if (info) { if ("message" in info || "code" in info || "name" in info) { throw new Error(`value will overwrite populated values: ${stringify(info)}`); } for (const key in info) { if (key === "shortMessage") { continue; } const value = (info[key]); // try { details.push(key + "=" + stringify(value)); // } catch (error: any) { // console.log("MMM", error.message); // details.push(key + "=[could not serialize object]"); // } } } details.push(`code=${code}`); details.push(`version=${version}`); if (details.length) { message += " (" + details.join(", ") + ")"; } } let error; switch (code) { case "INVALID_ARGUMENT": error = new TypeError(message); break; case "NUMERIC_FAULT": case "BUFFER_OVERRUN": error = new RangeError(message); break; default: error = new Error(message); } defineProperties(error, { code }); if (info) { Object.assign(error, info); } if (error.shortMessage == null) { defineProperties(error, { shortMessage }); } return error; } /** * Throws an EthersError with %%message%%, %%code%% and additional error * %%info%% when %%check%% is falsish.. * * @see [[api:makeError]] */ function assert(check, message, code, info) { if (!check) { throw makeError(message, code, info); } } /** * A simple helper to simply ensuring provided arguments match expected * constraints, throwing if not. * * In TypeScript environments, the %%check%% has been asserted true, so * any further code does not need additional compile-time checks. */ function assertArgument(check, message, name, value) { assert(check, message, "INVALID_ARGUMENT", { argument: name, value: value }); } function assertArgumentCount(count, expectedCount, message) { if (message == null) { message = ""; } if (message) { message = ": " + message; } assert(count >= expectedCount, "missing argument" + message, "MISSING_ARGUMENT", { count: count, expectedCount: expectedCount }); assert(count <= expectedCount, "too many arguments" + message, "UNEXPECTED_ARGUMENT", { count: count, expectedCount: expectedCount }); } ["NFD", "NFC", "NFKD", "NFKC"].reduce((accum, form) => { try { // General test for normalize /* c8 ignore start */ if ("test".normalize(form) !== "test") { throw new Error("bad"); } ; /* c8 ignore stop */ if (form === "NFD") { const check = String.fromCharCode(0xe9).normalize("NFD"); const expected = String.fromCharCode(0x65, 0x0301); /* c8 ignore start */ if (check !== expected) { throw new Error("broken"); } /* c8 ignore stop */ } accum.push(form); } catch (error) { } return accum; }, []); /** * Many classes use file-scoped values to guard the constructor, * making it effectively private. This facilitates that pattern * by ensuring the %%givenGaurd%% matches the file-scoped %%guard%%, * throwing if not, indicating the %%className%% if provided. */ function assertPrivate(givenGuard, guard, className) { if (className == null) { className = ""; } if (givenGuard !== guard) { let method = className, operation = "new"; if (className) { method += "."; operation += " " + className; } assert(false, `private constructor; use ${method}from* methods`, "UNSUPPORTED_OPERATION", { operation }); } } /** * Some data helpers. * * * @_subsection api/utils:Data Helpers [about-data] */ function _getBytes(value, name, copy) { if (value instanceof Uint8Array) { if (copy) { return new Uint8Array(value); } return value; } if (typeof (value) === "string" && value.match(/^0x(?:[0-9a-f][0-9a-f])*$/i)) { const result = new Uint8Array((value.length - 2) / 2); let offset = 2; for (let i = 0; i < result.length; i++) { result[i] = parseInt(value.substring(offset, offset + 2), 16); offset += 2; } return result; } assertArgument(false, "invalid BytesLike value", name || "value", value); } /** * Get a typed Uint8Array for %%value%%. If already a Uint8Array * the original %%value%% is returned; if a copy is required use * [[getBytesCopy]]. * * @see: getBytesCopy */ function getBytes(value, name) { return _getBytes(value, name, false); } /** * Get a typed Uint8Array for %%value%%, creating a copy if necessary * to prevent any modifications of the returned value from being * reflected elsewhere. * * @see: getBytes */ function getBytesCopy(value, name) { return _getBytes(value, name, true); } /** * Returns true if %%value%% is a valid [[HexString]]. * * If %%length%% is ``true`` or a //number//, it also checks that * %%value%% is a valid [[DataHexString]] of %%length%% (if a //number//) * bytes of data (e.g. ``0x1234`` is 2 bytes). */ function isHexString(value, length) { if (typeof (value) !== "string" || !value.match(/^0x[0-9A-Fa-f]*$/)) { return false; } if (typeof (length) === "number" && value.length !== 2 + 2 * length) { return false; } if (length === true && (value.length % 2) !== 0) { return false; } return true; } const HexCharacters = "0123456789abcdef"; /** * Returns a [[DataHexString]] representation of %%data%%. */ function hexlify(data) { const bytes = getBytes(data); let result = "0x"; for (let i = 0; i < bytes.length; i++) { const v = bytes[i]; result += HexCharacters[(v & 0xf0) >> 4] + HexCharacters[v & 0x0f]; } return result; } /** * Returns a [[DataHexString]] by concatenating all values * within %%data%%. */ function concat(datas) { return "0x" + datas.map((d) => hexlify(d).substring(2)).join(""); } /** * Returns a [[DataHexString]] by slicing %%data%% from the %%start%% * offset to the %%end%% offset. * * By default %%start%% is 0 and %%end%% is the length of %%data%%. */ function dataSlice(data, start, end) { const bytes = getBytes(data); if (end != null && end > bytes.length) { assert(false, "cannot slice beyond data bounds", "BUFFER_OVERRUN", { buffer: bytes, length: bytes.length, offset: end }); } return hexlify(bytes.slice((start == null) ? 0 : start, (end == null) ? bytes.length : end)); } function zeroPad(data, length, left) { const bytes = getBytes(data); assert(length >= bytes.length, "padding exceeds data length", "BUFFER_OVERRUN", { buffer: new Uint8Array(bytes), length: length, offset: length + 1 }); const result = new Uint8Array(length); result.fill(0); if (left) { result.set(bytes, length - bytes.length); } else { result.set(bytes, 0); } return hexlify(result); } /** * Return the [[DataHexString]] of %%data%% padded on the **left** * to %%length%% bytes. * * If %%data%% already exceeds %%length%%, a [[BufferOverrunError]] is * thrown. * * This pads data the same as **values** are in Solidity * (e.g. ``uint128``). */ function zeroPadValue(data, length) { return zeroPad(data, length, true); } /** * Return the [[DataHexString]] of %%data%% padded on the **right** * to %%length%% bytes. * * If %%data%% already exceeds %%length%%, a [[BufferOverrunError]] is * thrown. * * This pads data the same as **bytes** are in Solidity * (e.g. ``bytes16``). */ function zeroPadBytes(data, length) { return zeroPad(data, length, false); } /** * Some mathematic operations. * * @_subsection: api/utils:Math Helpers [about-maths] */ const BN_0$2 = BigInt(0); const BN_1$1 = BigInt(1); //const BN_Max256 = (BN_1 << BigInt(256)) - BN_1; // IEEE 754 support 53-bits of mantissa const maxValue = 0x1fffffffffffff; /** * Convert %%value%% from a twos-compliment representation of %%width%% * bits to its value. * * If the highest bit is ``1``, the result will be negative. */ function fromTwos(_value, _width) { const value = getUint(_value, "value"); const width = BigInt(getNumber(_width, "width")); assert((value >> width) === BN_0$2, "overflow", "NUMERIC_FAULT", { operation: "fromTwos", fault: "overflow", value: _value }); // Top bit set; treat as a negative value if (value >> (width - BN_1$1)) { const mask = (BN_1$1 << width) - BN_1$1; return -(((~value) & mask) + BN_1$1); } return value; } /** * Convert %%value%% to a twos-compliment representation of * %%width%% bits. * * The result will always be positive. */ function toTwos(_value, _width) { let value = getBigInt(_value, "value"); const width = BigInt(getNumber(_width, "width")); const limit = (BN_1$1 << (width - BN_1$1)); if (value < BN_0$2) { value = -value; assert(value <= limit, "too low", "NUMERIC_FAULT", { operation: "toTwos", fault: "overflow", value: _value }); const mask = (BN_1$1 << width) - BN_1$1; return ((~value) & mask) + BN_1$1; } else { assert(value < limit, "too high", "NUMERIC_FAULT", { operation: "toTwos", fault: "overflow", value: _value }); } return value; } /** * Mask %%value%% with a bitmask of %%bits%% ones. */ function mask(_value, _bits) { const value = getUint(_value, "value"); const bits = BigInt(getNumber(_bits, "bits")); return value & ((BN_1$1 << bits) - BN_1$1); } /** * Gets a BigInt from %%value%%. If it is an invalid value for * a BigInt, then an ArgumentError will be thrown for %%name%%. */ function getBigInt(value, name) { switch (typeof (value)) { case "bigint": return value; case "number": assertArgument(Number.isInteger(value), "underflow", name || "value", value); assertArgument(value >= -maxValue && value <= maxValue, "overflow", name || "value", value); return BigInt(value); case "string": try { if (value === "") { throw new Error("empty string"); } if (value[0] === "-" && value[1] !== "-") { return -BigInt(value.substring(1)); } return BigInt(value); } catch (e) { assertArgument(false, `invalid BigNumberish string: ${e.message}`, name || "value", value); } } assertArgument(false, "invalid BigNumberish value", name || "value", value); } /** * Returns %%value%% as a bigint, validating it is valid as a bigint * value and that it is positive. */ function getUint(value, name) { const result = getBigInt(value, name); assert(result >= BN_0$2, "unsigned value cannot be negative", "NUMERIC_FAULT", { fault: "overflow", operation: "getUint", value }); return result; } const Nibbles = "0123456789abcdef"; /* * Converts %%value%% to a BigInt. If %%value%% is a Uint8Array, it * is treated as Big Endian data. */ function toBigInt(value) { if (value instanceof Uint8Array) { let result = "0x0"; for (const v of value) { result += Nibbles[v >> 4]; result += Nibbles[v & 0x0f]; } return BigInt(result); } return getBigInt(value); } /** * Gets a //number// from %%value%%. If it is an invalid value for * a //number//, then an ArgumentError will be thrown for %%name%%. */ function getNumber(value, name) { switch (typeof (value)) { case "bigint": assertArgument(value >= -maxValue && value <= maxValue, "overflow", name || "value", value); return Number(value); case "number": assertArgument(Number.isInteger(value), "underflow", name || "value", value); assertArgument(value >= -maxValue && value <= maxValue, "overflow", name || "value", value); return value; case "string": try { if (value === "") { throw new Error("empty string"); } return getNumber(BigInt(value), name); } catch (e) { assertArgument(false, `invalid numeric string: ${e.message}`, name || "value", value); } } assertArgument(false, "invalid numeric value", name || "value", value); } /** * Converts %%value%% to a number. If %%value%% is a Uint8Array, it * is treated as Big Endian data. Throws if the value is not safe. */ function toNumber(value) { return getNumber(toBigInt(value)); } /** * Converts %%value%% to a Big Endian hexstring, optionally padded to * %%width%% bytes. */ function toBeHex(_value, _width) { const value = getUint(_value, "value"); let result = value.toString(16); if (_width == null) { // Ensure the value is of even length if (result.length % 2) { result = "0" + result; } } else { const width = getNumber(_width, "width"); assert(width * 2 >= result.length, `value exceeds width (${width} bytes)`, "NUMERIC_FAULT", { operation: "toBeHex", fault: "overflow", value: _value }); // Pad the value to the required width while (result.length < (width * 2)) { result = "0" + result; } } return "0x" + result; } /** * Converts %%value%% to a Big Endian Uint8Array. */ function toBeArray(_value) { const value = getUint(_value, "value"); if (value === BN_0$2) { return new Uint8Array([]); } let hex = value.toString(16); if (hex.length % 2) { hex = "0" + hex; } const result = new Uint8Array(hex.length / 2); for (let i = 0; i < result.length; i++) { const offset = i * 2; result[i] = parseInt(hex.substring(offset, offset + 2), 16); } return result; } /** * Using strings in Ethereum (or any security-basd system) requires * additional care. These utilities attempt to mitigate some of the * safety issues as well as provide the ability to recover and analyse * strings. * * @_subsection api/utils:Strings and UTF-8 [about-strings] */ function errorFunc(reason, offset, bytes, output, badCodepoint) { assertArgument(false, `invalid codepoint at offset ${offset}; ${reason}`, "bytes", bytes); } function ignoreFunc(reason, offset, bytes, output, badCodepoint) { // If there is an invalid prefix (including stray continuation), skip any additional continuation bytes if (reason === "BAD_PREFIX" || reason === "UNEXPECTED_CONTINUE") { let i = 0; for (let o = offset + 1; o < bytes.length; o++) { if (bytes[o] >> 6 !== 0x02) { break; } i++; } return i; } // This byte runs us past the end of the string, so just jump to the end // (but the first byte was read already read and therefore skipped) if (reason === "OVERRUN") { return bytes.length - offset - 1; } // Nothing to skip return 0; } function replaceFunc(reason, offset, bytes, output, badCodepoint) { // Overlong representations are otherwise "valid" code points; just non-deistingtished if (reason === "OVERLONG") { assertArgument(typeof (badCodepoint) === "number", "invalid bad code point for replacement", "badCodepoint", badCodepoint); output.push(badCodepoint); return 0; } // Put the replacement character into the output output.push(0xfffd); // Otherwise, process as if ignoring errors return ignoreFunc(reason, offset, bytes); } /** * A handful of popular, built-in UTF-8 error handling strategies. * * **``"error"``** - throws on ANY illegal UTF-8 sequence or * non-canonical (overlong) codepoints (this is the default) * * **``"ignore"``** - silently drops any illegal UTF-8 sequence * and accepts non-canonical (overlong) codepoints * * **``"replace"``** - replace any illegal UTF-8 sequence with the * UTF-8 replacement character (i.e. ``"\\ufffd"``) and accepts * non-canonical (overlong) codepoints * * @returns: Record<"error" | "ignore" | "replace", Utf8ErrorFunc> */ const Utf8ErrorFuncs = Object.freeze({ error: errorFunc, ignore: ignoreFunc, replace: replaceFunc }); // http://stackoverflow.com/questions/13356493/decode-utf-8-with-javascript#13691499 function getUtf8CodePoints(_bytes, onError) { if (onError == null) { onError = Utf8ErrorFuncs.error; } const bytes = getBytes(_bytes, "bytes"); const result = []; let i = 0; // Invalid bytes are ignored while (i < bytes.length) { const c = bytes[i++]; // 0xxx xxxx if (c >> 7 === 0) { result.push(c); continue; } // Multibyte; how many bytes left for this character? let extraLength = null; let overlongMask = null;