@pushchain/core
Version:
Push Chain is a true universal L1 that is 100% EVM compatible. It allows developers to deploy once and make their apps instantly compatible with users from all other L1s (Ethereum, Solana, etc) with zero on-chain code change.
611 lines • 24.4 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.Utils = void 0;
const account_1 = require("./universal/account");
const signer_1 = require("./universal/signer");
const enums_1 = require("./constants/enums");
const tokens_1 = require("./constants/tokens");
const chain_1 = require("./constants/chain");
const ethers_1 = require("ethers");
/**
* @dev - THESE UTILS ARE EXPORTED TO SDK CONSUMER
* @dev - Make sure each exported fn has good comments to help out sdk consumer
*/
/**
* Utility class for handling CAIP-10 chain-agnostic address formatting
* and universal account conversions.
*/
class Utils {
/**
* Internal: resolves a CHAIN enum from either a CHAIN value or a PushChain client instance.
*/
static resolveChainFromInput(chainOrClient) {
var _a;
if (!chainOrClient)
return undefined;
if (typeof chainOrClient === 'string')
return chainOrClient;
// PushChain client → get origin chain from signer account
try {
const originAccount = (_a = chainOrClient.universal) === null || _a === void 0 ? void 0 : _a.origin;
if (originAccount && originAccount.chain)
return originAccount.chain;
}
catch (_b) {
// ignore
}
return undefined;
}
}
exports.Utils = Utils;
Utils.account = {
/*
* Converts a UniversalAccount into a CAIP-10 style address string.
*
* Format: `namespace:chainId:address`
* Namespace is derived from the chain's VM type using VM_NAMESPACE.
*
* @param {UniversalAccount} account - The account to convert.
* @returns {string} A CAIP-10 formatted string.
*
* @example
* Utils.account.toChainAgnostic({
* chain: CHAIN.ETHEREUM_SEPOLIA,
* address: '0xabc'
* })
* // → 'eip155:11155111:0xabc'
*/
toChainAgnostic: account_1.toChainAgnostic,
toUniversal: account_1.toUniversal,
/**
* Converts a CAIP-10 formatted string into a UniversalAccount.
*
* @param {string} caip - A CAIP-10 address string (e.g., 'eip155:1:0xabc...').
* @returns {UniversalAccount} The resolved account.
* @throws {Error} If the CAIP string is invalid or unsupported.
*
* @example
* Utils.account.fromChainAgnostic('eip155:11155111:0xabc...')
* // → { chain: CHAIN.ETHEREUM_SEPOLIA, address: '0xabc...' }
*/
fromChainAgnostic: account_1.fromChainAgnostic,
convertOriginToExecutor: account_1.convertOriginToExecutor,
convertExecutorToOriginAccount: account_1.convertExecutorToOriginAccount,
};
Utils.signer = {
/**
* Converts various signer types (viem, ethers v6, Solana) into a UniversalSigner.
*/
toUniversalFromKeypair: signer_1.toUniversalFromKeypair,
/**
* Constructs a UniversalSignerSkeleton from raw signing functions.
*/
construct: signer_1.construct,
/**
* Converts a UniversalSignerSkeleton to a UniversalSigner.
*/
toUniversal: signer_1.toUniversal,
};
Utils.chains = {
/**
* Returns the list of supported chains for a given Push network.
* Future-proofed to return an object with a `chains` array.
*
* @param {PUSH_NETWORK} network - The Push network environment.
* @returns {{ chains: CHAIN[] }} Object containing supported chains.
*
* @example
* Utils.chains.getSupportedChains(PushChain.CONSTANTS.PUSH_NETWORK.TESTNET)
* // => { chains: [CHAIN.ETHEREUM_SEPOLIA, CHAIN.SOLANA_DEVNET] }
*
* @example
* Utils.chains.getSupportedChains(PushChain.CONSTANTS.PUSH_NETWORK.MAINNET)
* // => { chains: [] }
*/
getSupportedChains: (network) => {
var _a;
// Current support: expose test/dev chains; mainnet returns empty until GA
const mapping = {
[enums_1.PUSH_NETWORK.MAINNET]: [],
[enums_1.PUSH_NETWORK.TESTNET]: [
enums_1.CHAIN.ETHEREUM_SEPOLIA,
enums_1.CHAIN.ARBITRUM_SEPOLIA,
enums_1.CHAIN.BASE_SEPOLIA,
enums_1.CHAIN.SOLANA_DEVNET,
],
[enums_1.PUSH_NETWORK.TESTNET_DONUT]: [
enums_1.CHAIN.ETHEREUM_SEPOLIA,
enums_1.CHAIN.ARBITRUM_SEPOLIA,
enums_1.CHAIN.BASE_SEPOLIA,
enums_1.CHAIN.SOLANA_DEVNET,
],
[enums_1.PUSH_NETWORK.LOCALNET]: [
enums_1.CHAIN.ETHEREUM_SEPOLIA,
enums_1.CHAIN.ARBITRUM_SEPOLIA,
enums_1.CHAIN.BASE_SEPOLIA,
enums_1.CHAIN.SOLANA_DEVNET,
],
};
return { chains: (_a = mapping[network]) !== null && _a !== void 0 ? _a : [] };
},
getChainName: (chainNamespace) => {
// Special case: prefer PUSH_TESTNET_DONUT over PUSH_TESTNET for 'eip155:42101'
if (chainNamespace === 'eip155:42101') {
return 'PUSH_TESTNET_DONUT';
}
const chainEntries = Object.entries(enums_1.CHAIN);
const foundEntry = chainEntries.find((entry) => entry[1] === chainNamespace);
if (!foundEntry) {
return undefined;
}
return foundEntry[0];
},
/**
* Returns the chain namespace (e.g., 'eip155:11155111') for a given chain name.
* Reverse of getChainName. If input is already a namespace, it is returned.
*
* @param {string} chainName - The CHAIN key name (e.g., 'ETHEREUM_SEPOLIA' or 'PUSH_TESTNET_DONUT')
* or an existing namespace (e.g., 'eip155:11155111').
* @returns {string | undefined} The chain namespace, or undefined if unsupported.
*/
getChainNamespace: (chainName) => {
// If already a valid namespace value, return as-is
const namespaceValues = Object.values(enums_1.CHAIN);
if (namespaceValues.includes(chainName)) {
return chainName;
}
// Map enum key -> value
const namespace = enums_1.CHAIN[chainName];
if (typeof namespace === 'string') {
return namespace;
}
return undefined;
},
};
Utils.helpers = {
/**
* @deprecated Use PushChain.utils.chains.getChainNamespace(chainName) instead.
* Alias maintained for backwards compatibility. Logs a deprecation warning
* and delegates to Utils.chains.getChainNamespace.
*/
getChainName: (chainName) => {
// Emit deprecation warning on every call to surface migration need
// Note: Keeping message explicit for SDK consumers
console.warn('[DEPRECATED] PushChain.utils.helper.getChainName is deprecated. ' +
'Use PushChain.utils.chains.getChainNamespace(chainName) instead.');
return Utils.chains.getChainName(chainName);
},
encodeTxData({ abi, functionName, args = [], }) {
// Validate inputs
if (!Array.isArray(abi)) {
throw new Error('ABI must be an array');
}
if (!Array.isArray(args)) {
throw new Error('Arguments must be an array');
}
// Find the function in the ABI
const functionAbi = abi.find((f) => f.name === functionName);
if (!functionAbi) {
throw new Error(`Function '${functionName}' not found in ABI`);
}
try {
// Create ethers Interface and encode the function data
const abiInterface = new ethers_1.ethers.Interface(abi);
const data = abiInterface.encodeFunctionData(functionName, args);
return data;
}
catch (error) {
throw new Error(`Failed to encode function '${functionName}': ${error instanceof Error ? error.message : 'Unknown error'}`);
}
},
/**
* Multiplies a string representation of a number by a given exponent of base 10 (10^exponent).
*
* This is commonly used for converting human-readable token amounts to their on-chain representation.
* For example, converting "1.5" ETH to wei (18 decimals) would be parseUnits("1.5", 18).
*
* @param {string} value - The string representation of the number to multiply.
* @param {number | {decimals: number}} exponent - The exponent (number of decimal places) or an object with decimals property.
* @returns {bigint} The result as a bigint.
*
* @example
* Utils.helpers.parseUnits('420', 9)
* // → 420000000000n
*
* @example
* Utils.helpers.parseUnits('1.5', 18)
* // → 1500000000000000000n
*
* @example
* Utils.helpers.parseUnits('1.5', {decimals: 18})
* // → 1500000000000000000n
*/
parseUnits(value, exponent) {
// Validate inputs
if (typeof value !== 'string') {
throw new Error('Value must be a string');
}
// Extract the actual exponent value from either number or object
let actualExponent;
if (typeof exponent === 'number') {
actualExponent = exponent;
}
else if (typeof exponent === 'object' &&
exponent !== null &&
'decimals' in exponent) {
actualExponent = exponent.decimals;
}
else {
throw new Error('Exponent must be a number or an object with decimals property');
}
if (typeof actualExponent !== 'number') {
throw new Error('Exponent must be a number');
}
if (!Number.isInteger(actualExponent)) {
throw new Error('Exponent must be an integer');
}
if (actualExponent < 0) {
throw new Error('Exponent must be non-negative');
}
// Handle empty string
if (value.trim() === '') {
throw new Error('Value cannot be empty');
}
// Remove any whitespace
const trimmedValue = value.trim();
// Check for valid number format
if (!/^-?\d*\.?\d*$/.test(trimmedValue)) {
throw new Error('Value must be a valid number string');
}
// Handle case where value is just a decimal point
if (trimmedValue === '.' ||
trimmedValue === '-.' ||
trimmedValue === '') {
throw new Error('Value must be a valid number string');
}
try {
// Split on decimal point to handle fractional values
const parts = trimmedValue.split('.');
const integerPart = parts[0] || '0';
const fractionalPart = parts[1] || '';
// Check if fractional part has more digits than the exponent allows
if (fractionalPart.length > actualExponent) {
throw new Error(`Value has more decimal places (${fractionalPart.length}) than exponent allows (${actualExponent})`);
}
// Pad fractional part with zeros to match exponent
const paddedFractionalPart = fractionalPart.padEnd(actualExponent, '0');
// Combine integer and fractional parts
const combinedValue = integerPart + paddedFractionalPart;
// Convert to bigint
return BigInt(combinedValue);
}
catch (error) {
if (error instanceof Error &&
error.message.includes('decimal places')) {
throw error;
}
throw new Error(`Failed to parse value '${value}': ${error instanceof Error ? error.message : 'Invalid number format'}`);
}
},
/**
* Formats a value from smallest units to human-readable string.
*
* Supports both EVM-style (like ethers/viem) and Push-style (options object) usage patterns.
* Always returns a string for UI safety.
*
* @param {bigint | string} value - The value in smallest units (e.g., "1500000" or 1500000000000000000n).
* @param {number | {decimals: number; precision?: number}} decimalsOrOptions - Token decimals or options object.
* @returns {string} Human-readable string (e.g., "1.5").
*
* @example
* // EVM-style usage
* Utils.helpers.formatUnits(1500000000000000000n, 18)
* // → "1.5"
*
* @example
* // Push-style usage
* Utils.helpers.formatUnits("1500000", { decimals: 6 })
* // → "1.5"
*
* @example
* // With precision (truncate after 2 decimals)
* Utils.helpers.formatUnits("1234567", { decimals: 6, precision: 2 })
* // → "1.23"
*/
formatUnits(value, decimalsOrOptions) {
// Validate inputs
if (typeof value !== 'bigint' && typeof value !== 'string') {
throw new Error('Value must be a bigint or string');
}
// Extract decimals and precision from the second parameter
let decimals;
let precision;
if (typeof decimalsOrOptions === 'number') {
// EVM-style: formatUnits(value, decimals)
decimals = decimalsOrOptions;
}
else if (typeof decimalsOrOptions === 'object' &&
decimalsOrOptions !== null &&
'decimals' in decimalsOrOptions) {
// Push-style: formatUnits(value, { decimals, precision? })
decimals = decimalsOrOptions.decimals;
precision = decimalsOrOptions.precision;
}
else {
throw new Error('Second parameter must be a number (decimals) or an object with decimals property');
}
// Validate decimals
if (typeof decimals !== 'number') {
throw new Error('Decimals must be a number');
}
if (!Number.isInteger(decimals)) {
throw new Error('Decimals must be an integer');
}
if (decimals < 0) {
throw new Error('Decimals must be non-negative');
}
// Validate precision if provided
if (precision !== undefined) {
if (typeof precision !== 'number') {
throw new Error('Precision must be a number');
}
if (!Number.isInteger(precision)) {
throw new Error('Precision must be an integer');
}
if (precision < 0) {
throw new Error('Precision must be non-negative');
}
}
try {
// Convert string to bigint if needed
const bigintValue = typeof value === 'string' ? BigInt(value) : value;
// Use ethers to format the units
const formatted = ethers_1.ethers.formatUnits(bigintValue, decimals);
// Apply precision if specified
if (precision !== undefined) {
const num = parseFloat(formatted);
const factor = Math.pow(10, precision);
const truncated = Math.floor(num * factor) / factor;
return truncated.toString();
}
return formatted;
}
catch (error) {
throw new Error(`Failed to format units: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
},
};
Utils.conversion = {
/**
* Calculates the minimum amount out after applying slippage.
*
* Given an input amount and slippage in basis points, returns the minimum amount
* that should be received after accounting for slippage.
*
* @param {string} amount - The input amount in smallest units (e.g., "100000000" for 100 USDC with 6 decimals)
* @param {object} options - Configuration options
* @param {number} options.slippageBps - Slippage in basis points (100 = 1%, 50 = 0.5%)
* @returns {string} The minimum amount out in smallest units
*
* @example
* // Calculate minimum amount for 100 USDC with 1% slippage
* const amount = PushChain.utils.helpers.parseUnits("100", 6); // "100000000"
* const minOut = PushChain.utils.conversion.slippageToMinAmount(amount, {
* slippageBps: 100, // 1%
* });
* // => "99000000" (99 USDC in smallest units)
*
* @example
* // Simple case with whole numbers
* const minOut = PushChain.utils.conversion.slippageToMinAmount("100", {
* slippageBps: 100, // 1%
* });
* // => "99"
*/
slippageToMinAmount(amount, options) {
// Validate inputs
if (typeof amount !== 'string') {
throw new Error('Amount must be a string');
}
if (typeof options.slippageBps !== 'number') {
throw new Error('slippageBps must be a number');
}
if (!Number.isInteger(options.slippageBps)) {
throw new Error('slippageBps must be an integer');
}
if (options.slippageBps < 0) {
throw new Error('slippageBps must be non-negative');
}
if (options.slippageBps > 10000) {
throw new Error('slippageBps cannot exceed 10000 (100%)');
}
// Handle empty string
if (amount.trim() === '') {
throw new Error('Amount cannot be empty');
}
try {
// Convert amount to BigInt for precise calculation
const amountBigInt = BigInt(amount);
// Calculate slippage factor: (10000 - slippageBps) / 10000
// For 1% slippage (100 bps): (10000 - 100) / 10000 = 0.99
const slippageFactor = BigInt(10000 - options.slippageBps);
// Calculate minimum amount: amount * slippageFactor / 10000
const minAmountBigInt = (amountBigInt * slippageFactor) / BigInt(10000);
return minAmountBigInt.toString();
}
catch (error) {
throw new Error(`Failed to calculate slippage: ${error instanceof Error ? error.message : 'Invalid amount format'}`);
}
},
};
Utils.tokens = {
/**
* Returns supported moveable tokens as a flat list with chain info.
* - If a specific chain or a PushChain client is passed, returns only that chain's tokens
* - Otherwise returns tokens across all chains
*/
getMoveableTokens(chainOrClient) {
var _a;
const chain = Utils.resolveChainFromInput(chainOrClient);
if (chain) {
const list = (_a = tokens_1.MOVEABLE_TOKENS[chain]) !== null && _a !== void 0 ? _a : [];
return {
tokens: list.map((t) => ({
chain,
symbol: t.symbol,
decimals: t.decimals,
address: t.address,
mechanism: t.mechanism,
})),
};
}
const tokens = [];
for (const [key, list] of Object.entries(tokens_1.MOVEABLE_TOKENS)) {
const k = key;
for (const t of list !== null && list !== void 0 ? list : []) {
tokens.push({
chain: k,
symbol: t.symbol,
decimals: t.decimals,
address: t.address,
mechanism: t.mechanism,
});
}
}
return { tokens };
},
/**
* Returns supported payable tokens as a flat list with chain info.
* - If a specific chain or a PushChain client is passed, returns only that chain's tokens
* - Otherwise returns tokens across all chains
*/
getPayableTokens(chainOrClient) {
var _a;
const chain = Utils.resolveChainFromInput(chainOrClient);
if (chain) {
const list = (_a = tokens_1.PAYABLE_TOKENS[chain]) !== null && _a !== void 0 ? _a : [];
return {
tokens: list.map((t) => ({
chain,
symbol: t.symbol,
decimals: t.decimals,
address: t.address,
mechanism: t.mechanism,
})),
};
}
const tokens = [];
for (const [key, list] of Object.entries(tokens_1.PAYABLE_TOKENS)) {
const k = key;
for (const t of list !== null && list !== void 0 ? list : []) {
tokens.push({
chain: k,
symbol: t.symbol,
decimals: t.decimals,
address: t.address,
mechanism: t.mechanism,
});
}
}
return { tokens };
},
/**
* Convert any supported origin-chain token into its mapped PRC20 token address on Push Chain.
*
* @param token - Either a MoveableToken from `pushChainClient.moveable.token.*`
* or an object with the origin chain and token address.
* @returns {`0x${string}`} The synthetic asset address on Push Chain.
*
* @example
* ```jsx
* PushChain.utils.tokens.getPRC20Address(
* token: MoveableToken | {
* chain: CONSTANTS.CHAIN.ETHEREUM_SEPOLIA;
* address: `0x${string}`;
* }
* );
* // → `0x...`
* ```
*/
getPRC20Address(token) {
var _a;
// Infer origin chain and symbol by matching against the MOVEABLE_TOKENS registry
let originChain;
let tokenSymbol;
if ('symbol' in token) {
// MoveableToken path: infer chain by symbol + address
for (const [key, list] of Object.entries(tokens_1.MOVEABLE_TOKENS)) {
const k = key;
const found = (list !== null && list !== void 0 ? list : []).some((t) => t.symbol === token.symbol && t.address === token.address);
if (found) {
originChain = k;
tokenSymbol = token.symbol;
break;
}
}
}
else {
// { chain, address } path: trust the provided chain and resolve symbol via registry
originChain = token.chain;
const list = (_a = tokens_1.MOVEABLE_TOKENS[originChain]) !== null && _a !== void 0 ? _a : [];
const match = (list !== null && list !== void 0 ? list : []).find((t) => t.address === token.address);
if (match) {
tokenSymbol = match.symbol;
}
}
if (!originChain || !tokenSymbol) {
throw new Error('Unable to infer origin chain or token symbol for token');
}
// Select Push network mapping (tests/use-cases use TESTNET_DONUT; identical to TESTNET here)
const network = enums_1.PUSH_NETWORK.TESTNET_DONUT;
const map = chain_1.SYNTHETIC_PUSH_ERC20[network];
// Map token → synthetic key by origin chain family
const isEthFamily = originChain === enums_1.CHAIN.ETHEREUM_MAINNET ||
originChain === enums_1.CHAIN.ETHEREUM_SEPOLIA;
const isArbFamily = originChain === enums_1.CHAIN.ARBITRUM_SEPOLIA;
const isBaseFamily = originChain === enums_1.CHAIN.BASE_SEPOLIA;
const isBnbFamily = originChain === enums_1.CHAIN.BNB_TESTNET;
const isSolFamily = originChain === enums_1.CHAIN.SOLANA_DEVNET;
let key;
switch (tokenSymbol) {
case 'ETH': {
if (isEthFamily)
key = 'pETH';
else if (isArbFamily)
key = 'pETH_ARB';
else if (isBaseFamily)
key = 'pETH_BASE';
else if (isBnbFamily)
key = 'pETH_BNB';
else
throw new Error('Unsupported ETH origin chain for synthetic mapping');
break;
}
case 'SOL': {
if (!isSolFamily)
throw new Error('SOL token provided but origin is not Solana');
key = 'pSOL';
break;
}
case 'USDT': {
if (isEthFamily)
key = 'USDT_ETH';
else if (isArbFamily)
key = 'USDT_ARB';
else if (isBaseFamily)
key = 'USDT_BASE';
else if (isBnbFamily)
key = 'USDT_BNB';
else if (isSolFamily)
key = 'USDT_SOL';
else
throw new Error('Unsupported USDT origin chain for synthetic mapping');
break;
}
default:
throw new Error(`Unsupported token symbol: ${tokenSymbol}`);
}
return map[key];
},
};
//# sourceMappingURL=utils.js.map