necjs
Version:
NECJS SDK for NCOG Earth Chain RPC
1,438 lines (1,426 loc) • 2.26 MB
JavaScript
'use strict';
var axios = require('axios');
var fs = require('fs');
require('path');
var node_crypto = require('node:crypto');
var require$$0$3 = require('events');
var require$$1$1 = require('https');
var require$$2 = require('http');
var require$$3 = require('net');
var require$$4 = require('tls');
var require$$1 = require('crypto');
var require$$0$2 = require('stream');
var require$$7 = require('url');
var require$$0 = require('zlib');
var require$$0$1 = require('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 axios.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