ethersService.ts•106 kB
/**
* @file EthersService
* @version 1.0.0
* @status STABLE - DO NOT MODIFY WITHOUT TESTS
* @lastModified 2024-03-11
*
* Service for interacting with Ethereum via ethers.js
*
* IMPORTANT:
* - Any changes must be thoroughly tested
* - Maintain backward compatibility with existing contracts
*
* Functionality:
* - Ethereum account & network management
* - Contract interaction
* - Transaction processing
* - ERC token standards support
*/
import { ethers } from "ethers";
import { z } from "zod";
import { ConfigurationError, EthersServerError, NetworkError, ProviderError, TransactionError, WalletError, handleUnknownError } from "../utils/errors.js";
import { DefaultProvider, DEFAULT_PROVIDERS } from "../config/networks.js";
import { networkList, NetworkName, NetworkInfo } from "../config/networkList.js";
import * as erc20 from "./erc/erc20.js";
import * as erc721 from "./erc/erc721.js";
import * as erc1155 from "./erc/erc1155.js";
import { ERC20_ABI } from "./erc/constants.js";
import { ERC20Info, ERC721Info, NFTMetadata, ERC721TokenInfo, ERC1155TokenInfo, TokenOperationOptions } from "./erc/types.js";
import { TokenError } from "./erc/errors.js";
import { logger } from "../utils/logger.js";
import { silentLogger } from "../utils/silentLogger.js";
// Move addressSchema to class level to avoid duplication
const addressSchema = z.string().refine(
(address) => {
// First check if it's a valid format
if (!address.startsWith('0x')) {
throw new Error('Address must start with 0x');
}
const addressLength = address.length;
if (addressLength !== 42) {
if (addressLength === 43) {
throw new Error(`Address is too long (${addressLength} characters). Expected exactly 42 characters. Your address has an extra character: "${address}". Try: "${address.substring(0, 42)}"`);
} else if (addressLength === 41) {
throw new Error(`Address is too short (${addressLength} characters). Expected exactly 42 characters. Your address is missing ${42 - addressLength} character(s).`);
} else if (addressLength < 42) {
throw new Error(`Address is too short (${addressLength} characters). Expected exactly 42 characters. Your address is missing ${42 - addressLength} character(s).`);
} else {
throw new Error(`Address is too long (${addressLength} characters). Expected exactly 42 characters. Your address has ${addressLength - 42} extra character(s).`);
}
}
// Check if all characters after 0x are valid hex
const hexPart = address.substring(2);
if (!/^[a-fA-F0-9]+$/.test(hexPart)) {
throw new Error('Address contains invalid characters. Only hexadecimal characters (0-9, a-f, A-F) are allowed after 0x.');
}
// Finally validate with ethers
return ethers.isAddress(address);
},
{
message: "Invalid Ethereum address format. Expected format: 0x followed by exactly 40 hexadecimal characters (e.g., 0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb7)"
}
);
const networkToEthersMap: Record<string, string> = {
"Ethereum": "mainnet",
"Polygon PoS": "matic",
"Arbitrum": "arbitrum",
"Arbitrum Nova": "arbitrum-nova",
"Optimism": "optimism",
"Avalanche C-Chain": "avalanche",
"Base": "base",
"BNB Smart Chain": "bnb",
"Linea": "linea",
"Polygon zkEVM": "polygon-zkevm",
// New networks will use their custom RPC URLs directly, no mapping needed
// as they don't have standard ethers.js names
};
// Add a mapping from common network names to official names
const NETWORK_ALIASES: Record<string, string> = {
"mainnet": "Ethereum",
"ethereum": "Ethereum",
"eth": "Ethereum",
"polygon": "Polygon PoS",
"matic": "Polygon PoS",
"bsc": "BNB Smart Chain",
"binance": "BNB Smart Chain",
"avalanche": "Avalanche C-Chain",
"avax": "Avalanche C-Chain",
"arbitrum": "Arbitrum",
"arb": "Arbitrum",
"optimism": "Optimism",
"op": "Optimism",
"base": "Base",
"zksync": "ZKsync",
"linea": "Linea",
"scroll": "Scroll",
"zkEVM": "Polygon zkEVM",
"polygonZkEVM": "Polygon zkEVM",
"monad": "Monad Testnet",
"monadtestnet": "Monad Testnet",
"mega": "MEGA Testnet",
"megatestnet": "MEGA Testnet",
"rari": "Rari Chain Mainnet",
"rarichain": "Rari Chain Mainnet",
"bera": "Berachain",
"sonic": "Sonic Mainnet"
};
export class EthersService {
private _provider: ethers.Provider;
private _signer?: ethers.Signer;
constructor(provider?: ethers.Provider, signer?: ethers.Signer) {
// Find a suitable default network if provider is not provided
let defaultNetwork: DefaultProvider = DEFAULT_PROVIDERS.includes("Ethereum") ?
"Ethereum" :
DEFAULT_PROVIDERS.length > 0 ? DEFAULT_PROVIDERS[0] : "Ethereum";
this._provider = provider || this.createAlchemyProvider(defaultNetwork);
this._signer = signer;
}
get provider() {
return this._provider;
}
setProvider(provider: ethers.Provider): void {
this._provider = provider;
}
setSigner(signer: ethers.Signer): void {
this._signer = signer;
}
private getAlchemyApiKey(): string {
const apiKey = process.env.ALCHEMY_API_KEY;
if (!apiKey) {
throw new ConfigurationError("Alchemy API key is not set in environment variables", {
variableName: "ALCHEMY_API_KEY"
});
}
return apiKey;
}
private createAlchemyProvider(network: DefaultProvider): ethers.Provider {
try {
const apiKey = this.getAlchemyApiKey();
// Map DefaultProvider names to Alchemy network names
let alchemyNetwork: string;
switch (network) {
case "Ethereum":
alchemyNetwork = "mainnet";
break;
case "Polygon PoS":
alchemyNetwork = "polygon";
break;
case "Arbitrum":
alchemyNetwork = "arbitrum";
break;
case "Arbitrum Nova":
alchemyNetwork = "arbitrum-nova";
break;
case "Optimism":
alchemyNetwork = "optimism";
break;
case "Avalanche C-Chain":
alchemyNetwork = "avalanche";
break;
case "Base":
alchemyNetwork = "base";
break;
default:
// For other networks, convert to lowercase and replace spaces with hyphens
alchemyNetwork = network.toLowerCase().replace(/ /g, "-");
}
try {
// First try with standard AlchemyProvider
return new ethers.AlchemyProvider(alchemyNetwork, apiKey);
} catch (error) {
logger.warn(`Failed to create Alchemy provider, falling back to default provider`, { error });
// Fall back to ethers default provider
const ethersNetwork = networkToEthersMap[network] || alchemyNetwork;
return ethers.getDefaultProvider(ethersNetwork);
}
} catch (error) {
if (error instanceof ConfigurationError) {
logger.info("No Alchemy API key found, using default provider");
const ethersNetwork = networkToEthersMap[network] || network.toLowerCase().replace(/ /g, "-");
return ethers.getDefaultProvider(ethersNetwork);
}
throw new NetworkError(`Failed to create provider for network ${network}`, {
network, error
});
}
}
// Check if a string has a valid RPC URL format without throwing an error
private isValidRpcUrlFormat(url: string): boolean {
return url.startsWith('http://') || url.startsWith('https://') ||
url.startsWith('ws://') || url.startsWith('wss://');
}
// Validate that a URL has a proper RPC format and throw if not
private validateRpcUrl(url: string): void {
if (!this.isValidRpcUrlFormat(url)) {
throw new NetworkError(`Invalid RPC URL format: ${url}`, {
url
});
}
}
private handleProviderError(error: unknown, context: string, details?: Record<string, any>): never {
if (error instanceof EthersServerError) {
throw error;
}
if (error instanceof Error) {
const errorMessage = error.message.toLowerCase();
if (errorMessage.includes('network') || errorMessage.includes('connection') || errorMessage.includes('timeout')) {
throw new NetworkError(`Network error while trying to ${context}: ${error.message}`, {
...details,
originalError: error
});
}
if (errorMessage.includes('contract') || errorMessage.includes('abi')) {
throw new ProviderError(`Contract error while trying to ${context}: ${error.message}`, {
...details,
originalError: error
});
}
if (errorMessage.includes('transaction') || errorMessage.includes('gas') || errorMessage.includes('fee')) {
throw new TransactionError(`Transaction error while trying to ${context}: ${error.message}`, {
...details,
originalError: error
});
}
}
// Generic error fallback
throw new EthersServerError(
`Error while trying to ${context}: ${error instanceof Error ? error.message : String(error)}`,
'UNKNOWN_ERROR',
{
...details,
originalError: error
}
);
}
private serializeValue(value: any): string {
if (value === undefined || value === null) {
return 'null';
}
if (typeof value === 'bigint') {
return value.toString();
}
if (Array.isArray(value)) {
return `[${value.map(v => this.serializeValue(v)).join(', ')}]`;
}
if (typeof value === 'object') {
// Skip internal properties of ethers
if (value._isSigner || value._isProvider || value._isFragment) {
return '[Object]';
}
try {
return JSON.stringify(value, (_, v) =>
typeof v === 'bigint' ? v.toString() : v
);
} catch (e) {
return '[Object]';
}
}
return String(value);
}
private getEthersNetworkName(network: string): string {
return network in networkList ? network : 'mainnet';
}
private getProvider(provider?: string, chainId?: number): ethers.Provider {
// If no provider specified, return the default provider
if (!provider) {
return this._provider;
}
let selectedProvider: ethers.Provider;
// Check if the provider is an alias and convert it to the official name
const normalizedProvider = NETWORK_ALIASES[provider.toLowerCase()] || provider;
// Check if provider is a named network in our list
if (normalizedProvider in networkList) {
const network = normalizedProvider as NetworkName;
const networkInfo = networkList[network];
// If chainId is provided, verify it matches the network
if (chainId !== undefined && networkInfo.chainId !== chainId) {
throw new NetworkError(`Chain ID mismatch: requested ${chainId}, but network ${network} has chain ID ${networkInfo.chainId}`, {
requestedChainId: chainId,
networkChainId: networkInfo.chainId,
network
});
}
// For Ethereum mainnet and common networks, use Alchemy
if (DEFAULT_PROVIDERS.includes(network as DefaultProvider)) {
try {
selectedProvider = this.createAlchemyProvider(network as DefaultProvider);
} catch (error) {
// Fall back to custom RPC if Alchemy fails
// Check if this is an Alchemy URL that needs the API key appended
if (networkInfo.RPC.includes('alchemy.com/v2/')) {
try {
const apiKey = this.getAlchemyApiKey();
const rpcWithApiKey = networkInfo.RPC + apiKey;
this.validateRpcUrl(rpcWithApiKey);
selectedProvider = new ethers.JsonRpcProvider(rpcWithApiKey);
} catch (apiError) {
throw new NetworkError(`Failed to create provider for network ${network}: API key error`, {
network, error: apiError
});
}
} else {
this.validateRpcUrl(networkInfo.RPC);
selectedProvider = new ethers.JsonRpcProvider(networkInfo.RPC);
}
}
} else {
// For other networks, use the custom RPC
// Check if this is an Alchemy URL that needs the API key appended
if (networkInfo.RPC.includes('alchemy.com/v2/')) {
try {
const apiKey = this.getAlchemyApiKey();
const rpcWithApiKey = networkInfo.RPC + apiKey;
this.validateRpcUrl(rpcWithApiKey);
selectedProvider = new ethers.JsonRpcProvider(rpcWithApiKey);
} catch (apiError) {
throw new NetworkError(`Failed to create provider for network ${network}: API key error`, {
network, error: apiError
});
}
} else {
this.validateRpcUrl(networkInfo.RPC);
selectedProvider = new ethers.JsonRpcProvider(networkInfo.RPC);
}
}
} else {
// Check if the provider value looks like a valid RPC URL
if (this.isValidRpcUrlFormat(normalizedProvider)) {
// Assume provider is a custom RPC URL
selectedProvider = new ethers.JsonRpcProvider(normalizedProvider);
// If chainId is provided, check if it matches the network
if (chainId !== undefined) {
// This will be checked when connecting to the network
// and throw an error if mismatched
}
} else {
// Not a valid network name or URL format
throw new NetworkError(`Invalid provider: "${provider}" is not a recognized network name or valid RPC URL`, {
provider
});
}
}
return selectedProvider;
}
async getBalance(address: string, provider?: string, chainId?: number): Promise<string> {
try {
addressSchema.parse(address);
const selectedProvider = this.getProvider(provider, chainId);
const balance = await selectedProvider.getBalance(address);
return ethers.formatEther(balance);
} catch (error) {
this.handleProviderError(error, "fetch balance", { address });
}
}
// Note: This method signature is kept for backward compatibility
// but internally delegates to the erc20 module
async getERC20Balance(address: string, tokenAddress: string, provider?: string, chainId?: number): Promise<string> {
try {
addressSchema.parse(address);
addressSchema.parse(tokenAddress);
return await erc20.getBalance(this, tokenAddress, address, provider, chainId);
} catch (error) {
if (error instanceof TokenError) {
throw error;
}
this.handleProviderError(error, "fetch ERC20 balance", { address, tokenAddress });
}
}
async getTransactionCount(address: string, provider?: string, chainId?: number): Promise<number> {
try {
addressSchema.parse(address);
const selectedProvider = this.getProvider(provider, chainId);
const count = await selectedProvider.getTransactionCount(address);
return count;
} catch (error) {
this.handleProviderError(error, "fetch transaction count", { address });
}
}
async getBlockNumber(provider?: string, chainId?: number): Promise<number> {
try {
const selectedProvider = this.getProvider(provider, chainId);
return await selectedProvider.getBlockNumber();
} catch (error) {
this.handleProviderError(error, "fetch latest block number");
}
}
async getBlockDetails(blockTag: string | number, provider?: string, chainId?: number): Promise<ethers.Block | null> {
try {
const selectedProvider = this.getProvider(provider, chainId);
const block = await selectedProvider.getBlock(blockTag);
return block;
} catch (error) {
this.handleProviderError(error, "fetch block details", { blockTag: String(blockTag) });
}
}
async getTransactionDetails(txHash: string, provider?: string, chainId?: number): Promise<ethers.TransactionResponse | null> {
try {
const txSchema = z.string().regex(/^0x[a-fA-F0-9]{64}$/);
txSchema.parse(txHash);
let selectedProvider = this.getProvider(provider, chainId);
if (!provider && !chainId) {
try {
const derivedChainId = await this.getChainIdFromTransaction(txHash);
selectedProvider = this.getProvider(provider, derivedChainId);
} catch (error) {
// If we can't get the chainId, continue with the default provider
logger.warn("Could not derive chainId from transaction, using default provider");
}
}
return await selectedProvider.getTransaction(txHash);
} catch (error) {
this.handleProviderError(error, "fetch transaction details", { txHash });
}
}
async getGasPrice(provider?: string, chainId?: number): Promise<bigint> {
try {
const selectedProvider = this.getProvider(provider, chainId);
const feeData = await selectedProvider.getFeeData();
return feeData.gasPrice || 0n;
} catch (error) {
this.handleProviderError(error, "get gas price");
}
}
async getFeeData(provider?: string, chainId?: number): Promise<ethers.FeeData> {
try {
const selectedProvider = this.getProvider(provider, chainId);
return await selectedProvider.getFeeData();
} catch (error) {
this.handleProviderError(error, "get fee data");
}
}
async getContractCode(address: string, provider?: string, chainId?: number): Promise<string | null> {
try {
addressSchema.parse(address);
const selectedProvider = this.getProvider(provider, chainId);
return await selectedProvider.getCode(address);
} catch (error) {
this.handleProviderError(error, "get contract bytecode", { address });
}
}
async lookupAddress(address: string, provider?: string, chainId?: number): Promise<string | null> {
try {
addressSchema.parse(address);
const selectedProvider = this.getProvider(provider, chainId);
return await selectedProvider.lookupAddress(address);
} catch (error) {
this.handleProviderError(error, "look up ENS name for address", { address });
}
}
async resolveName(name: string, provider?: string, chainId?: number): Promise<string | null> {
try {
const selectedProvider = this.getProvider(provider, chainId);
return await selectedProvider.resolveName(name);
} catch (error) {
this.handleProviderError(error, "resolve ENS name", { name });
}
}
formatEther(wei: string | number | bigint): string {
try {
return ethers.formatEther(wei);
} catch (error) {
this.handleProviderError(error, "format Ether value", { wei: String(wei) });
}
}
parseEther(ether: string): bigint {
try {
return ethers.parseEther(ether);
} catch (error) {
this.handleProviderError(error, "parse Ether string", { ether });
}
}
formatUnits(value: string | number | bigint, unit: string | number): string {
try {
return ethers.formatUnits(value, unit);
} catch (error) {
this.handleProviderError(error, "format units", { value: String(value), unit: String(unit) });
}
}
parseUnits(value: string, unit: string | number): bigint {
try {
return ethers.parseUnits(value, unit);
} catch (error) {
this.handleProviderError(error, "parse units", { value, unit: String(unit) });
}
}
private getSigner(provider?: string, chainId?: number, signerOverride?: ethers.Signer): ethers.Signer {
if (signerOverride) {
return signerOverride;
}
if (this._signer) {
return this._signer;
}
const privateKey = process.env.PRIVATE_KEY;
if (!privateKey) {
throw new Error("Missing PRIVATE_KEY in environment variables. Either provide a signer in the constructor or set PRIVATE_KEY in environment variables.");
}
const selectedProvider = this.getProvider(provider, chainId);
return new ethers.Wallet(privateKey, selectedProvider);
}
async createTransaction(to: string, value: string, data?: string, provider?: string): Promise<ethers.TransactionRequest> {
try {
addressSchema.parse(to);
const parsedValue = ethers.parseEther(value);
const transaction: ethers.TransactionRequest = {
to,
value: parsedValue,
data: data || "0x",
};
const signer = this.getSigner(provider);
const populatedTx = await signer.populateTransaction(transaction);
return populatedTx;
} catch (error) {
this.handleProviderError(error, "create transaction", { to, value });
}
}
async estimateGas(tx: ethers.TransactionRequest, provider?: string): Promise<bigint> {
try {
const signer = this.getSigner(provider);
const result = await signer.estimateGas(tx);
return result;
} catch (error) {
this.handleProviderError(error, "estimate gas", { tx: JSON.stringify(tx) });
}
}
async sendTransaction(
toOrTx: string | ethers.TransactionRequest,
value?: string,
data?: string,
provider?: string
): Promise<ethers.TransactionResponse> {
try {
let tx: ethers.TransactionRequest;
if (typeof toOrTx === 'string') {
// Handle old-style parameter based call
addressSchema.parse(toOrTx);
tx = {
to: toOrTx,
value: value ? ethers.parseEther(value) : undefined,
data: data || "0x"
};
} else {
// Handle object-style call
if (toOrTx.to) {
addressSchema.parse(toOrTx.to);
}
tx = toOrTx;
}
const signer = this.getSigner(provider);
return await signer.sendTransaction(tx);
} catch (error) {
this.handleProviderError(error, "send transaction", { tx: toOrTx });
}
}
async signMessage(message: string, provider?: string): Promise<string> {
try {
const signer = this.getSigner(provider);
return await signer.signMessage(message);
} catch (error) {
this.handleProviderError(error, "sign message", { message });
}
}
/**
* Signs data using the Ethereum eth_sign method (legacy)
* Note: This method is less secure than signMessage (personal_sign) as it can sign transaction-like data
*
* @param data The data to sign (as a hex string)
* @param provider Optional provider name or URL
* @returns The signature as a hexadecimal string
*/
async ethSign(data: string, provider?: string): Promise<string> {
try {
// Ensure data is properly formatted as hex
if (!data.startsWith('0x')) {
data = '0x' + Buffer.from(data).toString('hex');
}
const signer = this.getSigner(provider);
// In ethers v6, we can use signMessage for both personal_sign and eth_sign
// The difference is in how the message is formatted
// For eth_sign, we use the raw data without the Ethereum prefix
// This is a lower-level operation and should be used with caution
// Convert hex to bytes
const dataBytes = ethers.getBytes(data);
// Sign the raw bytes
// Note: This is equivalent to eth_sign in most cases
const signature = await signer.signMessage(dataBytes);
return signature;
} catch (error) {
this.handleProviderError(error, "eth_sign", { data });
}
}
async contractCall(
contractAddress: string,
abi: string | Array<string>,
method: string,
args: any[] = [],
provider?: string,
chainId?: number
): Promise<any> {
try {
addressSchema.parse(contractAddress);
const selectedProvider = this.getProvider(provider, chainId);
// Parse ABI if it's a string
let parsedAbi: any = abi;
if (typeof abi === 'string') {
try {
parsedAbi = JSON.parse(abi);
} catch (e) {
throw new Error(`Invalid ABI: ${abi}. The ABI must be a valid JSON string or array of strings`);
}
}
// Create contract instance with provider
const contract = new ethers.Contract(
contractAddress,
parsedAbi,
selectedProvider
);
// Get function fragment to check if it's view/pure
const fragment = contract.interface.getFunction(method);
if (!fragment) {
throw new Error(`Method ${method} not found in contract ABI`);
}
// For view/pure functions, use provider directly
if (fragment.constant || fragment.stateMutability === 'view' || fragment.stateMutability === 'pure') {
const result = await contract.getFunction(method).staticCall(...args);
return this.serializeEventArgs(result); // Use our serializer for the result
}
throw new Error(`Use contractSendTransaction for state-changing function: ${method}`);
} catch (error) {
this.handleProviderError(error, `call contract method: ${method}`, {
contractAddress,
abi: typeof abi === 'string' ? abi : JSON.stringify(abi),
args: this.serializeValue(args),
});
}
}
async contractCallView(
contractAddress: string,
abi: string | Array<string>,
method: string,
args: any[] = [],
provider?: string,
chainId?: number
): Promise<any> {
try {
addressSchema.parse(contractAddress);
const selectedProvider = this.getProvider(provider, chainId);
// Parse ABI if it's a string
let parsedAbi: any = abi;
if (typeof abi === 'string') {
try {
parsedAbi = JSON.parse(abi);
} catch (e) {
throw new Error(`Invalid ABI: ${abi}. The ABI must be a valid JSON string or array of strings`);
}
}
// Create contract instance with provider
const contract = new ethers.Contract(
contractAddress,
parsedAbi,
selectedProvider
);
// Get function fragment to check if it's view/pure
const fragment = contract.interface.getFunction(method);
if (!fragment) {
throw new Error(`Method ${method} not found in contract ABI`);
}
// For view/pure functions, use provider directly
if (!fragment.constant && fragment.stateMutability !== 'view' && fragment.stateMutability !== 'pure') {
throw new Error(`Use contractSendTransaction for state-changing function: ${method}`);
}
const result = await contract.getFunction(method).staticCall(...args);
return this.serializeEventArgs(result); // Use our serializer for the result
} catch (error) {
this.handleProviderError(error, `call contract view method: ${method}`, {
contractAddress,
abi: typeof abi === 'string' ? abi : JSON.stringify(abi),
args: this.serializeValue(args),
});
}
}
async contractCallWithEstimate(
contractAddress: string,
abi: string,
method: string,
args: any[] = [],
value: string = "0",
provider?: string
): Promise<any> {
try {
addressSchema.parse(contractAddress);
const signer = this.getSigner(provider);
const contract = new ethers.Contract(
contractAddress,
abi,
signer
);
const parsedValue = ethers.parseEther(value);
// Get the function fragment for the method
const fragment = contract.interface.getFunction(method);
if (!fragment) {
throw new Error(`Method ${method} not found in contract ABI`);
}
// Encode the function data
const data = contract.interface.encodeFunctionData(fragment, args);
// Create the transaction request
const tx = {
to: contractAddress,
data,
value: parsedValue
};
// Estimate the gas
const estimatedGas = await signer.estimateGas(tx);
// Add the estimated gas and send the transaction
return await this.contractSendTransaction(
contractAddress,
abi,
method,
args,
value,
provider,
{ gasLimit: estimatedGas }
);
} catch (error) {
this.handleProviderError(error, `call contract method with estimate: ${method}`, {
contractAddress,
abi: JSON.stringify(abi),
args: JSON.stringify(args),
value
});
}
}
async contractCallWithOverrides(
contractAddress: string,
abi: string,
method: string,
args: any[] = [],
value: string = "0",
provider?: string,
overrides?: ethers.Overrides
): Promise<any> {
try {
addressSchema.parse(contractAddress);
const signer = this.getSigner(provider);
const contract = new ethers.Contract(
contractAddress,
abi,
signer
);
const parsedValue = ethers.parseEther(value);
// Get the function fragment for the method
const fragment = contract.interface.getFunction(method);
if (!fragment) {
throw new Error(`Method ${method} not found in contract ABI`);
}
// Merge value with other overrides
const txOverrides = {
...overrides,
value: parsedValue
};
// Call the contract method with overrides
const tx = await contract[method](...args, txOverrides);
return tx;
} catch (error) {
this.handleProviderError(error, `call contract method with overrides: ${method}`, {
contractAddress,
abi: JSON.stringify(abi),
args: this.serializeValue(args),
value,
overrides: this.serializeValue(overrides)
});
}
}
async contractSendTransaction(
contractAddress: string,
abi: string,
method: string,
args: any[] = [],
value: string = "0",
provider?: string,
overrides?: ethers.Overrides
): Promise<ethers.TransactionResponse> {
try {
addressSchema.parse(contractAddress);
const signer = this.getSigner(provider);
const contract = new ethers.Contract(
contractAddress,
abi,
signer
);
const parsedValue = ethers.parseEther(value);
// Get the function fragment for the method
const fragment = contract.interface.getFunction(method);
if (!fragment) {
throw new Error(`Method ${method} not found in contract ABI`);
}
// Encode the function data
const data = contract.interface.encodeFunctionData(fragment, args);
// Create the transaction request with overrides
const tx = {
to: contractAddress,
data,
value: parsedValue,
...overrides
};
// Send the transaction
return await signer.sendTransaction(tx);
} catch (error) {
this.handleProviderError(error, `send transaction to contract method: ${method}`, {
contractAddress,
abi: JSON.stringify(abi),
args: JSON.stringify(args),
value
});
}
}
async contractSendTransactionWithEstimate(
contractAddress: string,
abi: string,
method: string,
args: any[],
value: string = "0",
provider?: string
): Promise<ethers.TransactionResponse> {
try {
const parsedAddress = addressSchema.parse(contractAddress);
const contract = new ethers.Contract(parsedAddress, abi, await this.getSigner(provider));
const parsedValue = ethers.parseEther(value);
// Get the function fragment for the method
const fragment = contract.interface.getFunction(method);
if (!fragment) {
throw new Error(`Method ${method} not found in contract ABI`);
}
// Encode the function data with value
const data = contract.interface.encodeFunctionData(fragment, args);
const tx = {
to: parsedAddress,
data,
value: parsedValue
};
// Estimate gas
const gasEstimate = await contract.getFunction(method).estimateGas(...args, { value: parsedValue });
// Send transaction with estimated gas
return await contract.getFunction(method)(...args, {
value: parsedValue,
gasLimit: gasEstimate
});
} catch (error) {
throw this.handleProviderError(error, `send transaction to contract method with estimate: ${method}`, {
contractAddress,
abi: JSON.stringify(abi),
args: JSON.stringify(args),
value
});
}
}
async contractSendTransactionWithOverrides(
contractAddress: string,
abi: string,
method: string,
args: any[],
value: string = "0",
provider?: string,
overrides: ethers.Overrides = {}
): Promise<ethers.TransactionResponse> {
try {
const parsedAddress = addressSchema.parse(contractAddress);
const contract = new ethers.Contract(parsedAddress, abi, await this.getSigner(provider));
const parsedValue = ethers.parseEther(value);
// Get the function fragment for the method
const fragment = contract.interface.getFunction(method);
if (!fragment) {
throw new Error(`Method ${method} not found in contract ABI`);
}
// Merge value with other overrides
const txOverrides = {
...overrides,
value: parsedValue
};
// Encode the function data
const data = contract.interface.encodeFunctionData(fragment, args);
// Send transaction with overrides
return await contract.getFunction(method)(...args, txOverrides);
} catch (error) {
throw this.handleProviderError(error, `send transaction to contract method with overrides: ${method}`, {
contractAddress,
abi: JSON.stringify(abi),
args: this.serializeValue(args),
value,
overrides: this.serializeValue(overrides)
});
}
}
async sendRawTransaction(
signedTransaction: string,
provider?: string
): Promise<ethers.TransactionResponse> {
try {
const selectedProvider = this.getProvider(provider);
return await selectedProvider.broadcastTransaction(signedTransaction);
} catch (error) {
this.handleProviderError(error, "send raw transaction", { signedTransaction });
}
}
private formatEvent(log: ethers.EventLog | ethers.Log): any {
const formattedEvent = {
address: log.address,
blockNumber: log.blockNumber?.toString(),
transactionHash: log.transactionHash,
logIndex: log.index,
name: 'eventName' in log ? log.eventName : undefined,
args: 'args' in log ? this.serializeEventArgs(log.args) : undefined,
data: log.data,
topics: log.topics
};
return formattedEvent;
}
private serializeEventArgs(args: any): any {
if (args === null || args === undefined) return args;
if (typeof args === 'bigint') return args.toString();
if (Array.isArray(args)) {
return args.map(arg => this.serializeEventArgs(arg));
}
if (typeof args === 'object') {
const serialized: any = {};
for (const [key, value] of Object.entries(args)) {
if (key === 'length' && Array.isArray(args)) continue;
if (key === '_isBigNumber' || key === 'type' || key === 'hash') continue; // Skip internal ethers properties
serialized[key] = this.serializeEventArgs(value);
}
return serialized;
}
return args;
}
async queryLogs(
address?: string,
topics?: Array<string | null | Array<string>>,
fromBlock?: string | number,
toBlock?: string | number,
provider?: string,
chainId?: number
): Promise<any> {
try {
let checksummedAddress: string | undefined;
if (address) {
checksummedAddress = ethers.getAddress(address);
}
const selectedProvider = this.getProvider(provider, chainId);
const filter: ethers.Filter = {
address: checksummedAddress,
topics: topics
};
const logs = await selectedProvider.getLogs({
...filter,
fromBlock: fromBlock,
toBlock: toBlock
});
return logs.map((log) => this.formatEvent(log));
} catch (error) {
this.handleProviderError(error, "query logs", {
address: address || "any",
topics: topics ? JSON.stringify(topics) : "any",
fromBlock: String(fromBlock || "any"),
toBlock: String(toBlock || "any")
});
}
}
async contractEvents(
contractAddress: string,
abi: string | Array<string>,
eventName?: string,
topics?: Array<string | null | Array<string>>,
fromBlock?: string | number,
toBlock?: string | number,
provider?: string,
chainId?: number
): Promise<any> {
try {
// Use queryLogs under the hood as it's more reliable
const checksummedAddress = ethers.getAddress(contractAddress);
const selectedProvider = this.getProvider(provider, chainId);
const contract = new ethers.Contract(checksummedAddress, abi, selectedProvider);
// If no event name specified, get all events
if (!eventName) {
return this.queryLogs(
checksummedAddress,
topics,
fromBlock,
toBlock,
provider,
chainId
);
}
// Get the event fragment to encode topics
const fragment = contract.interface.getEvent(eventName);
if (!fragment) {
throw new Error(`Event ${eventName} not found in contract ABI`);
}
// Get the topic hash for this event
const topicHash = fragment.topicHash;
const eventTopics: (string | null | Array<string>)[] = [topicHash];
if (topics && topics.length > 0) {
eventTopics.push(...topics);
}
// Use queryLogs with the event-specific topic
const logs = await this.queryLogs(
checksummedAddress,
eventTopics,
fromBlock,
toBlock,
provider,
chainId
);
// Parse the logs with the contract interface
return logs.map((log: ethers.Log) => {
try {
const parsedLog = contract.interface.parseLog({
topics: log.topics,
data: log.data
});
return {
...log,
name: parsedLog?.name,
args: this.serializeEventArgs(parsedLog?.args)
};
} catch (e) {
// If parsing fails, return the raw log
return log;
}
});
} catch (error) {
this.handleProviderError(error, "query contract events", {
contractAddress,
abi: typeof abi === 'string' ? abi : JSON.stringify(abi),
eventName: eventName || "any",
topics: topics ? this.serializeValue(topics) : "any",
fromBlock: String(fromBlock || "any"),
toBlock: String(toBlock || "any")
});
}
}
async sendTransactionWithOptions(
toOrTx: string | ethers.TransactionRequest,
value?: string,
data?: string,
gasLimit?: string,
gasPrice?: string,
nonce?: number,
provider?: string,
chainId?: number,
signerOverride?: ethers.Signer
): Promise<ethers.TransactionResponse> {
try {
let tx: ethers.TransactionRequest;
if (typeof toOrTx === 'string') {
addressSchema.parse(toOrTx);
tx = {
to: toOrTx,
value: value ? ethers.parseEther(value) : undefined,
data: data || "0x",
gasLimit: gasLimit ? ethers.getBigInt(gasLimit) : undefined,
gasPrice: gasPrice ? ethers.parseUnits(gasPrice, "gwei") : undefined,
nonce,
};
} else {
if(toOrTx.to) {
addressSchema.parse(toOrTx.to);
}
tx = {
...toOrTx,
gasLimit: gasLimit ? ethers.getBigInt(gasLimit) : undefined,
gasPrice: gasPrice ? ethers.parseUnits(gasPrice, "gwei") : undefined,
nonce,
}
}
const signer = this.getSigner(provider, chainId, signerOverride);
return await signer.sendTransaction(tx);
} catch (error) {
this.handleProviderError(error, "send transaction with options", {
tx: toOrTx, value, data, gasLimit, gasPrice, nonce
});
}
}
getSupportedNetworks(): Array<{
name: string;
chainId?: number;
isTestnet?: boolean;
nativeCurrency?: {
name: string;
symbol: string;
decimals: number;
};
isDefault?: boolean;
}> {
try {
const defaultNetwork = process.env.DEFAULT_NETWORK || "mainnet";
// Filter out networks that don't have a corresponding entry in networkList or don't have a chainId
return DEFAULT_PROVIDERS
.filter((network) => {
const networkInfo = networkList[network as NetworkName];
return networkInfo && typeof networkInfo.chainId === 'number';
})
.map((network) => {
const networkInfo = networkList[network as NetworkName];
return {
name: network,
chainId: networkInfo.chainId,
isTestnet: network.toLowerCase().includes('testnet') ||
network.toLowerCase().includes('goerli') ||
network.toLowerCase().includes('sepolia'),
nativeCurrency: {
name: networkInfo.currency || 'Native Token',
symbol: networkInfo.currency || 'NATIVE',
decimals: 18
},
isDefault: network === defaultNetwork
};
});
} catch (error) {
throw this.handleProviderError(error, "get supported networks");
}
}
async getWalletInfo(provider?: string): Promise<{ address: string } | null> {
try {
if (!this._signer) {
return null;
}
const selectedProvider = provider ? this.getProvider(provider) : this._provider;
const signer = this._signer.connect(selectedProvider);
const address = await signer.getAddress();
return { address };
} catch (error) {
this.handleProviderError(error, "get wallet info");
}
}
/**
* Get the current wallet signer
*
* @param provider Optional provider to connect the signer to
* @returns The ethers wallet signer or null if no wallet is set
*/
async getWallet(provider?: string): Promise<ethers.Signer | null> {
try {
if (!this._signer) {
return null;
}
const selectedProvider = provider ? this.getProvider(provider) : this._provider;
return this._signer.connect(selectedProvider);
} catch (error) {
this.handleProviderError(error, "get wallet");
}
}
async getChainIdFromTransaction(txHash: string, provider?: string): Promise<number> {
try {
const txSchema = z.string().regex(/^0x[a-fA-F0-9]{64}$/);
txSchema.parse(txHash);
const selectedProvider = this.getProvider(provider);
const tx = await selectedProvider.getTransaction(txHash);
if (!tx) {
throw new Error("Transaction not found");
}
return Number(tx.chainId);
} catch (error) {
this.handleProviderError(error, "fetch transaction details", { txHash });
}
}
async getTransactionsByBlock(blockTag: string | number, provider?: string, chainId?: number): Promise<ethers.TransactionResponse[]> {
try {
const selectedProvider = this.getProvider(provider, chainId);
const block = await selectedProvider.getBlock(blockTag, true);
if (!block || !block.transactions) {
return [];
}
const transactionRequests = await Promise.all(block.transactions.map(tx => selectedProvider.getTransaction(tx)));
return transactionRequests.filter((tx): tx is ethers.TransactionResponse => tx != null);
} catch (error) {
this.handleProviderError(error, "get transactions by block", { blockTag: String(blockTag) });
}
}
// ERC20 Token Methods
/**
* Get basic information about an ERC20 token
*
* @param tokenAddress Token contract address
* @param provider Optional provider name or instance
* @param chainId Optional chain ID
* @returns Promise with token information
*/
async getERC20TokenInfo(
tokenAddress: string,
provider?: string,
chainId?: number
): Promise<ERC20Info> {
try {
addressSchema.parse(tokenAddress);
return await erc20.getTokenInfo(this, tokenAddress, provider, chainId);
} catch (error) {
if (error instanceof TokenError) {
throw error;
}
this.handleProviderError(error, "get ERC20 token info", { tokenAddress });
}
}
/**
* Get the allowance amount approved for a spender
*
* @param tokenAddress ERC20 token contract address
* @param ownerAddress Token owner address
* @param spenderAddress Spender address
* @param provider Optional provider name or instance
* @param chainId Optional chain ID
* @returns Promise with formatted allowance as string
*/
async getERC20Allowance(
tokenAddress: string,
ownerAddress: string,
spenderAddress: string,
provider?: string,
chainId?: number
): Promise<string> {
try {
addressSchema.parse(tokenAddress);
addressSchema.parse(ownerAddress);
addressSchema.parse(spenderAddress);
return await erc20.getAllowance(this, tokenAddress, ownerAddress, spenderAddress, provider, chainId);
} catch (error) {
if (error instanceof TokenError) {
throw error;
}
this.handleProviderError(error, "get ERC20 allowance", { tokenAddress, ownerAddress, spenderAddress });
}
}
/**
* Transfer ERC20 tokens to a recipient
*
* @param tokenAddress ERC20 token contract address
* @param recipientAddress Recipient address
* @param amount Amount to transfer in token units (e.g., "1.5" not wei)
* @param provider Optional provider name or instance
* @param chainId Optional chain ID
* @param options Optional transaction options
* @returns Promise with transaction response
*/
async transferERC20(
tokenAddress: string,
recipientAddress: string,
amount: string,
provider?: string,
chainId?: number,
options: TokenOperationOptions = {}
): Promise<ethers.TransactionResponse> {
try {
addressSchema.parse(tokenAddress);
addressSchema.parse(recipientAddress);
return await erc20.transfer(this, tokenAddress, recipientAddress, amount, provider, chainId, options);
} catch (error) {
if (error instanceof TokenError) {
throw error;
}
this.handleProviderError(error, "transfer ERC20 tokens", { tokenAddress, recipientAddress, amount });
}
}
/**
* Approve a spender to use ERC20 tokens
*
* @param tokenAddress ERC20 token contract address
* @param spenderAddress Spender address to approve
* @param amount Amount to approve in token units (e.g., "1.5" not wei)
* @param provider Optional provider name or instance
* @param chainId Optional chain ID
* @param options Optional transaction options
* @returns Promise with transaction response
*/
async approveERC20(
tokenAddress: string,
spenderAddress: string,
amount: string,
provider?: string,
chainId?: number,
options: TokenOperationOptions = {}
): Promise<ethers.TransactionResponse> {
try {
addressSchema.parse(tokenAddress);
addressSchema.parse(spenderAddress);
return await erc20.approve(this, tokenAddress, spenderAddress, amount, provider, chainId, options);
} catch (error) {
if (error instanceof TokenError) {
throw error;
}
this.handleProviderError(error, "approve ERC20 tokens", { tokenAddress, spenderAddress, amount });
}
}
/**
* Prepare ERC20 transfer transaction for signing
*
* @param tokenAddress ERC20 token contract address
* @param recipientAddress Recipient address
* @param amount Amount to transfer in token units (e.g., "1.5" not wei)
* @param fromAddress Sender address
* @param provider Optional provider name or instance
* @param chainId Optional chain ID
* @param options Optional transaction options
* @returns Promise with prepared transaction object
*/
async prepareERC20Transfer(
tokenAddress: string,
recipientAddress: string,
amount: string,
fromAddress: string,
provider?: string,
chainId?: number,
options: TokenOperationOptions = {}
): Promise<ethers.TransactionRequest> {
try {
addressSchema.parse(tokenAddress);
addressSchema.parse(recipientAddress);
addressSchema.parse(fromAddress);
// Get provider
const ethersProvider = this.getProvider(provider, chainId);
// Get token info to parse amount correctly
const tokenInfo = await this.getERC20TokenInfo(tokenAddress, provider, chainId);
// Create contract interface
const contract = new ethers.Contract(tokenAddress, ERC20_ABI, ethersProvider);
// Parse amount to wei equivalent (considering token decimals)
const amountWei = ethers.parseUnits(amount, tokenInfo.decimals);
// Prepare transaction data
const data = contract.interface.encodeFunctionData("transfer", [recipientAddress, amountWei]);
// Get network info
const network = await ethersProvider.getNetwork();
// Prepare transaction request
const txRequest: ethers.TransactionRequest = {
to: tokenAddress,
data: data,
from: fromAddress,
chainId: chainId || Number(network.chainId)
};
// Add gas options if provided
if (options.gasLimit) {
txRequest.gasLimit = typeof options.gasLimit === 'string' ?
ethers.getBigInt(options.gasLimit) :
ethers.getBigInt(options.gasLimit.toString());
}
if (options.gasPrice) {
txRequest.gasPrice = typeof options.gasPrice === 'string' ?
ethers.parseUnits(options.gasPrice, 'gwei') :
ethers.parseUnits(options.gasPrice.toString(), 'gwei');
}
if (options.maxFeePerGas) {
txRequest.maxFeePerGas = typeof options.maxFeePerGas === 'string' ?
ethers.parseUnits(options.maxFeePerGas, 'gwei') :
ethers.parseUnits(options.maxFeePerGas.toString(), 'gwei');
}
if (options.maxPriorityFeePerGas) {
txRequest.maxPriorityFeePerGas = typeof options.maxPriorityFeePerGas === 'string' ?
ethers.parseUnits(options.maxPriorityFeePerGas, 'gwei') :
ethers.parseUnits(options.maxPriorityFeePerGas.toString(), 'gwei');
}
return txRequest;
} catch (error) {
this.handleProviderError(error, "prepare ERC20 transfer", { tokenAddress, recipientAddress, amount });
}
}
/**
* Prepare ERC20 approval transaction for signing
*
* @param tokenAddress ERC20 token contract address
* @param spenderAddress Address to approve for spending
* @param amount Amount to approve in token units (e.g., "1.5" not wei)
* @param fromAddress Owner address
* @param provider Optional provider name or instance
* @param chainId Optional chain ID
* @param options Optional transaction options
* @returns Promise with prepared transaction object
*/
async prepareERC20Approval(
tokenAddress: string,
spenderAddress: string,
amount: string,
fromAddress: string,
provider?: string,
chainId?: number,
options: TokenOperationOptions = {}
): Promise<ethers.TransactionRequest> {
try {
addressSchema.parse(tokenAddress);
addressSchema.parse(spenderAddress);
addressSchema.parse(fromAddress);
// Get provider
const ethersProvider = this.getProvider(provider, chainId);
// Get token info to parse amount correctly
const tokenInfo = await this.getERC20TokenInfo(tokenAddress, provider, chainId);
// Create contract interface
const contract = new ethers.Contract(tokenAddress, ERC20_ABI, ethersProvider);
// Parse amount to wei equivalent (considering token decimals)
const amountWei = ethers.parseUnits(amount, tokenInfo.decimals);
// Prepare transaction data
const data = contract.interface.encodeFunctionData("approve", [spenderAddress, amountWei]);
// Get network info
const network = await ethersProvider.getNetwork();
// Prepare transaction request
const txRequest: ethers.TransactionRequest = {
to: tokenAddress,
data: data,
from: fromAddress,
chainId: chainId || Number(network.chainId)
};
// Add gas options if provided
if (options.gasLimit) {
txRequest.gasLimit = typeof options.gasLimit === 'string' ?
ethers.getBigInt(options.gasLimit) :
ethers.getBigInt(options.gasLimit.toString());
}
if (options.gasPrice) {
txRequest.gasPrice = typeof options.gasPrice === 'string' ?
ethers.parseUnits(options.gasPrice, 'gwei') :
ethers.parseUnits(options.gasPrice.toString(), 'gwei');
}
if (options.maxFeePerGas) {
txRequest.maxFeePerGas = typeof options.maxFeePerGas === 'string' ?
ethers.parseUnits(options.maxFeePerGas, 'gwei') :
ethers.parseUnits(options.maxFeePerGas.toString(), 'gwei');
}
if (options.maxPriorityFeePerGas) {
txRequest.maxPriorityFeePerGas = typeof options.maxPriorityFeePerGas === 'string' ?
ethers.parseUnits(options.maxPriorityFeePerGas, 'gwei') :
ethers.parseUnits(options.maxPriorityFeePerGas.toString(), 'gwei');
}
return txRequest;
} catch (error) {
this.handleProviderError(error, "prepare ERC20 approval", { tokenAddress, spenderAddress, amount });
}
}
/**
* Transfer ERC20 tokens from one address to another (requires approval)
*
* @param tokenAddress ERC20 token contract address
* @param senderAddress Address to transfer from
* @param recipientAddress Recipient address
* @param amount Amount to transfer in token units (e.g., "1.5" not wei)
* @param provider Optional provider name or instance
* @param chainId Optional chain ID
* @param options Optional transaction options
* @returns Promise with transaction response
*/
async transferFromERC20(
tokenAddress: string,
senderAddress: string,
recipientAddress: string,
amount: string,
provider?: string,
chainId?: number,
options: TokenOperationOptions = {}
): Promise<ethers.TransactionResponse> {
try {
addressSchema.parse(tokenAddress);
addressSchema.parse(senderAddress);
addressSchema.parse(recipientAddress);
return await erc20.transferFrom(this, tokenAddress, senderAddress, recipientAddress, amount, provider, chainId, options);
} catch (error) {
if (error instanceof TokenError) {
throw error;
}
this.handleProviderError(error, "transfer ERC20 tokens from sender", { tokenAddress, senderAddress, recipientAddress, amount });
}
}
// ERC721 NFT Methods
/**
* Get basic information about an ERC721 NFT collection
*
* @param contractAddress NFT contract address
* @param provider Optional provider name or instance
* @param chainId Optional chain ID
* @returns Promise with NFT collection information
*/
async getERC721CollectionInfo(
contractAddress: string,
provider?: string,
chainId?: number
): Promise<ERC721Info> {
try {
addressSchema.parse(contractAddress);
return await erc721.getNFTInfo(this, contractAddress, provider, chainId);
} catch (error) {
if (error instanceof TokenError) {
throw error;
}
this.handleProviderError(error, "get ERC721 collection info", { contractAddress });
}
}
/**
* Get the owner of a specific NFT
*
* @param contractAddress NFT contract address
* @param tokenId Token ID to check ownership
* @param provider Optional provider name or instance
* @param chainId Optional chain ID
* @returns Promise with owner address
*/
async getERC721Owner(
contractAddress: string,
tokenId: string | number,
provider?: string,
chainId?: number
): Promise<string> {
try {
addressSchema.parse(contractAddress);
return await erc721.ownerOf(this, contractAddress, tokenId, provider, chainId);
} catch (error) {
if (error instanceof TokenError) {
throw error;
}
this.handleProviderError(error, "get ERC721 owner", { contractAddress, tokenId });
}
}
/**
* Get and parse metadata for a specific NFT
*
* @param contractAddress NFT contract address
* @param tokenId Token ID
* @param provider Optional provider name or instance
* @param chainId Optional chain ID
* @returns Promise with parsed metadata
*/
async getERC721Metadata(
contractAddress: string,
tokenId: string | number,
provider?: string,
chainId?: number
): Promise<NFTMetadata> {
try {
addressSchema.parse(contractAddress);
return await erc721.getMetadata(this, contractAddress, tokenId, provider, chainId);
} catch (error) {
if (error instanceof TokenError) {
throw error;
}
this.handleProviderError(error, "get ERC721 metadata", { contractAddress, tokenId });
}
}
/**
* Get all NFTs owned by an address
*
* @param contractAddress NFT contract address
* @param ownerAddress Owner address to check
* @param includeMetadata Whether to include metadata
* @param provider Optional provider name or instance
* @param chainId Optional chain ID
* @returns Promise with array of owned NFTs
*/
async getERC721TokensOfOwner(
contractAddress: string,
ownerAddress: string,
includeMetadata: boolean = false,
provider?: string,
chainId?: number
): Promise<ERC721TokenInfo[]> {
try {
addressSchema.parse(contractAddress);
addressSchema.parse(ownerAddress);
return await erc721.getUserNFTs(this, contractAddress, ownerAddress, includeMetadata, provider, chainId);
} catch (error) {
if (error instanceof TokenError) {
throw error;
}
this.handleProviderError(error, "get ERC721 tokens of owner", { contractAddress, ownerAddress });
}
}
/**
* Transfer an NFT to a new owner
*
* @param contractAddress NFT contract address
* @param toAddress Recipient address
* @param tokenId Token ID to transfer
* @param provider Optional provider name or instance
* @param chainId Optional chain ID
* @param options Optional transaction options
* @returns Promise with transaction response
*/
async transferERC721(
contractAddress: string,
toAddress: string,
tokenId: string | number,
provider?: string,
chainId?: number,
options: TokenOperationOptions = {}
): Promise<ethers.TransactionResponse> {
try {
addressSchema.parse(contractAddress);
addressSchema.parse(toAddress);
return await erc721.transferNFT(this, contractAddress, toAddress, tokenId, provider, chainId, options);
} catch (error) {
if (error instanceof TokenError) {
throw error;
}
this.handleProviderError(error, "transfer ERC721 NFT", { contractAddress, toAddress, tokenId });
}
}
/**
* Safely transfer an NFT to a new owner
*
* @param contractAddress NFT contract address
* @param toAddress Recipient address
* @param tokenId Token ID to transfer
* @param data Optional data to include
* @param provider Optional provider name or instance
* @param chainId Optional chain ID
* @param options Optional transaction options
* @returns Promise with transaction response
*/
async safeTransferERC721(
contractAddress: string,
toAddress: string,
tokenId: string | number,
data: string = '0x',
provider?: string,
chainId?: number,
options: TokenOperationOptions = {}
): Promise<ethers.TransactionResponse> {
try {
addressSchema.parse(contractAddress);
addressSchema.parse(toAddress);
return await erc721.safeTransferNFT(this, contractAddress, toAddress, tokenId, data, provider, chainId, options);
} catch (error) {
if (error instanceof TokenError) {
throw error;
}
this.handleProviderError(error, "safe transfer ERC721 NFT", { contractAddress, toAddress, tokenId });
}
}
/**
* Prepare ERC721 NFT transfer transaction for signing
*
* @param contractAddress ERC721 contract address
* @param toAddress Recipient address
* @param tokenId Token ID to transfer
* @param fromAddress Sender address
* @param provider Optional provider name or instance
* @param chainId Optional chain ID
* @param options Optional transaction options
* @returns Promise with prepared transaction object
*/
async prepareERC721Transfer(
contractAddress: string,
toAddress: string,
tokenId: string | number,
fromAddress: string,
provider?: string,
chainId?: number,
options: TokenOperationOptions = {}
): Promise<ethers.TransactionRequest> {
try {
addressSchema.parse(contractAddress);
addressSchema.parse(toAddress);
addressSchema.parse(fromAddress);
// Get provider
const ethersProvider = this.getProvider(provider, chainId);
// Import ERC721_ABI
const { ERC721_ABI } = await import("./erc/constants.js");
// Create contract interface
const contract = new ethers.Contract(contractAddress, ERC721_ABI, ethersProvider);
// Prepare transaction data for transferFrom
const data = contract.interface.encodeFunctionData("transferFrom", [fromAddress, toAddress, tokenId.toString()]);
// Get network info
const network = await ethersProvider.getNetwork();
// Prepare transaction request
const txRequest: ethers.TransactionRequest = {
to: contractAddress,
data: data,
from: fromAddress,
chainId: chainId || Number(network.chainId)
};
// Add gas options if provided
if (options.gasLimit) {
txRequest.gasLimit = typeof options.gasLimit === 'string' ?
ethers.getBigInt(options.gasLimit) :
ethers.getBigInt(options.gasLimit.toString());
}
if (options.gasPrice) {
txRequest.gasPrice = typeof options.gasPrice === 'string' ?
ethers.parseUnits(options.gasPrice, 'gwei') :
ethers.parseUnits(options.gasPrice.toString(), 'gwei');
}
if (options.maxFeePerGas) {
txRequest.maxFeePerGas = typeof options.maxFeePerGas === 'string' ?
ethers.parseUnits(options.maxFeePerGas, 'gwei') :
ethers.parseUnits(options.maxFeePerGas.toString(), 'gwei');
}
if (options.maxPriorityFeePerGas) {
txRequest.maxPriorityFeePerGas = typeof options.maxPriorityFeePerGas === 'string' ?
ethers.parseUnits(options.maxPriorityFeePerGas, 'gwei') :
ethers.parseUnits(options.maxPriorityFeePerGas.toString(), 'gwei');
}
return txRequest;
} catch (error) {
this.handleProviderError(error, "prepare ERC721 transfer", { contractAddress, toAddress, tokenId });
}
}
/**
* Prepare ERC721 NFT approval transaction for signing
*
* @param contractAddress ERC721 contract address
* @param toAddress Address to approve for the token
* @param tokenId Token ID to approve
* @param fromAddress Owner address
* @param provider Optional provider name or instance
* @param chainId Optional chain ID
* @param options Optional transaction options
* @returns Promise with prepared transaction object
*/
async prepareERC721Approval(
contractAddress: string,
toAddress: string,
tokenId: string | number,
fromAddress: string,
provider?: string,
chainId?: number,
options: TokenOperationOptions = {}
): Promise<ethers.TransactionRequest> {
try {
addressSchema.parse(contractAddress);
addressSchema.parse(toAddress);
addressSchema.parse(fromAddress);
// Get provider
const ethersProvider = this.getProvider(provider, chainId);
// Import ERC721_ABI
const { ERC721_ABI } = await import("./erc/constants.js");
// Create contract interface
const contract = new ethers.Contract(contractAddress, ERC721_ABI, ethersProvider);
// Prepare transaction data for approve
const data = contract.interface.encodeFunctionData("approve", [toAddress, tokenId.toString()]);
// Get network info
const network = await ethersProvider.getNetwork();
// Prepare transaction request
const txRequest: ethers.TransactionRequest = {
to: contractAddress,
data: data,
from: fromAddress,
chainId: chainId || Number(network.chainId)
};
// Add gas options if provided
if (options.gasLimit) {
txRequest.gasLimit = typeof options.gasLimit === 'string' ?
ethers.getBigInt(options.gasLimit) :
ethers.getBigInt(options.gasLimit.toString());
}
if (options.gasPrice) {
txRequest.gasPrice = typeof options.gasPrice === 'string' ?
ethers.parseUnits(options.gasPrice, 'gwei') :
ethers.parseUnits(options.gasPrice.toString(), 'gwei');
}
if (options.maxFeePerGas) {
txRequest.maxFeePerGas = typeof options.maxFeePerGas === 'string' ?
ethers.parseUnits(options.maxFeePerGas, 'gwei') :
ethers.parseUnits(options.maxFeePerGas.toString(), 'gwei');
}
if (options.maxPriorityFeePerGas) {
txRequest.maxPriorityFeePerGas = typeof options.maxPriorityFeePerGas === 'string' ?
ethers.parseUnits(options.maxPriorityFeePerGas, 'gwei') :
ethers.parseUnits(options.maxPriorityFeePerGas.toString(), 'gwei');
}
return txRequest;
} catch (error) {
this.handleProviderError(error, "prepare ERC721 approval", { contractAddress, toAddress, tokenId });
}
}
/**
* Prepare ERC721 NFT setApprovalForAll transaction for signing
*
* @param contractAddress ERC721 contract address
* @param operator Address to approve/revoke for all tokens
* @param approved Whether to approve or revoke
* @param fromAddress Owner address
* @param provider Optional provider name or instance
* @param chainId Optional chain ID
* @param options Optional transaction options
* @returns Promise with prepared transaction object
*/
async prepareERC721SetApprovalForAll(
contractAddress: string,
operator: string,
approved: boolean,
fromAddress: string,
provider?: string,
chainId?: number,
options: TokenOperationOptions = {}
): Promise<ethers.TransactionRequest> {
try {
addressSchema.parse(contractAddress);
addressSchema.parse(operator);
addressSchema.parse(fromAddress);
// Get provider
const ethersProvider = this.getProvider(provider, chainId);
// Import ERC721_ABI
const { ERC721_ABI } = await import("./erc/constants.js");
// Create contract interface
const contract = new ethers.Contract(contractAddress, ERC721_ABI, ethersProvider);
// Prepare transaction data for setApprovalForAll
const data = contract.interface.encodeFunctionData("setApprovalForAll", [operator, approved]);
// Get network info
const network = await ethersProvider.getNetwork();
// Prepare transaction request
const txRequest: ethers.TransactionRequest = {
to: contractAddress,
data: data,
from: fromAddress,
chainId: chainId || Number(network.chainId)
};
// Add gas options if provided
if (options.gasLimit) {
txRequest.gasLimit = typeof options.gasLimit === 'string' ?
ethers.getBigInt(options.gasLimit) :
ethers.getBigInt(options.gasLimit.toString());
}
if (options.gasPrice) {
txRequest.gasPrice = typeof options.gasPrice === 'string' ?
ethers.parseUnits(options.gasPrice, 'gwei') :
ethers.parseUnits(options.gasPrice.toString(), 'gwei');
}
if (options.maxFeePerGas) {
txRequest.maxFeePerGas = typeof options.maxFeePerGas === 'string' ?
ethers.parseUnits(options.maxFeePerGas, 'gwei') :
ethers.parseUnits(options.maxFeePerGas.toString(), 'gwei');
}
if (options.maxPriorityFeePerGas) {
txRequest.maxPriorityFeePerGas = typeof options.maxPriorityFeePerGas === 'string' ?
ethers.parseUnits(options.maxPriorityFeePerGas, 'gwei') :
ethers.parseUnits(options.maxPriorityFeePerGas.toString(), 'gwei');
}
return txRequest;
} catch (error) {
this.handleProviderError(error, "prepare ERC721 setApprovalForAll", { contractAddress, operator, approved });
}
}
// ERC1155 Multi-Token Methods
/**
* Get token balance for a specific ERC1155 token ID
*
* @param contractAddress ERC1155 contract address
* @param ownerAddress Owner address to check
* @param tokenId Token ID to check balance
* @param provider Optional provider name or instance
* @param chainId Optional chain ID
* @returns Promise with token balance as string
*/
async getERC1155Balance(
contractAddress: string,
ownerAddress: string,
tokenId: string | number,
provider?: string,
chainId?: number
): Promise<string> {
try {
addressSchema.parse(contractAddress);
addressSchema.parse(ownerAddress);
return await erc1155.balanceOf(this, contractAddress, ownerAddress, tokenId, provider, chainId);
} catch (error) {
if (error instanceof TokenError) {
throw error;
}
this.handleProviderError(error, "get ERC1155 balance", { contractAddress, ownerAddress, tokenId });
}
}
/**
* Get token balances for multiple ERC1155 token IDs at once
*
* @param contractAddress ERC1155 contract address
* @param ownerAddresses Array of owner addresses
* @param tokenIds Array of token IDs
* @param provider Optional provider name or instance
* @param chainId Optional chain ID
* @returns Promise with array of token balances
*/
async getERC1155BatchBalances(
contractAddress: string,
ownerAddresses: string[],
tokenIds: (string | number)[],
provider?: string,
chainId?: number
): Promise<string[]> {
try {
addressSchema.parse(contractAddress);
ownerAddresses.forEach(address => addressSchema.parse(address));
return await erc1155.balanceOfBatch(this, contractAddress, ownerAddresses, tokenIds, provider, chainId);
} catch (error) {
if (error instanceof TokenError) {
throw error;
}
this.handleProviderError(error, "get ERC1155 batch balances", { contractAddress, ownerAddresses, tokenIds });
}
}
/**
* Get and parse metadata for a specific ERC1155 token
*
* @param contractAddress ERC1155 contract address
* @param tokenId Token ID to get metadata for
* @param provider Optional provider name or instance
* @param chainId Optional chain ID
* @returns Promise with token metadata
*/
async getERC1155Metadata(
contractAddress: string,
tokenId: string | number,
provider?: string,
chainId?: number
): Promise<NFTMetadata> {
try {
addressSchema.parse(contractAddress);
return await erc1155.getMetadata(this, contractAddress, tokenId, provider, chainId);
} catch (error) {
if (error instanceof TokenError) {
throw error;
}
this.handleProviderError(error, "get ERC1155 metadata", { contractAddress, tokenId });
}
}
/**
* Get all ERC1155 tokens owned by an address
*
* @param contractAddress ERC1155 contract address
* @param ownerAddress Owner address to check
* @param tokenIds Optional array of specific token IDs to check
* @param includeMetadata Whether to include metadata
* @param provider Optional provider name or instance
* @param chainId Optional chain ID
* @returns Promise with array of token info
*/
async getERC1155TokensOfOwner(
contractAddress: string,
ownerAddress: string,
tokenIds?: (string | number)[],
includeMetadata: boolean = false,
provider?: string,
chainId?: number
): Promise<ERC1155TokenInfo[]> {
try {
addressSchema.parse(contractAddress);
addressSchema.parse(ownerAddress);
return await erc1155.getUserTokens(this, contractAddress, ownerAddress, tokenIds, includeMetadata, provider, chainId);
} catch (error) {
if (error instanceof TokenError) {
throw error;
}
this.handleProviderError(error, "get ERC1155 tokens of owner", { contractAddress, ownerAddress });
}
}
/**
* Safely transfer ERC1155 tokens to another address
*
* @param contractAddress ERC1155 contract address
* @param fromAddress Sender address
* @param toAddress Recipient address
* @param tokenId Token ID to transfer
* @param amount Amount to transfer
* @param data Additional data to include with the transfer
* @param provider Optional provider name or instance
* @param chainId Optional chain ID
* @param options Optional transaction options
* @returns Promise with transaction response
*/
async safeTransferERC1155(
contractAddress: string,
fromAddress: string,
toAddress: string,
tokenId: string | number,
amount: string,
data: string = '0x',
provider?: string,
chainId?: number,
options: TokenOperationOptions = {}
): Promise<ethers.TransactionResponse> {
try {
addressSchema.parse(contractAddress);
addressSchema.parse(fromAddress);
addressSchema.parse(toAddress);
return await erc1155.safeTransferFrom(this, contractAddress, fromAddress, toAddress, tokenId, amount, data, provider, chainId, options);
} catch (error) {
if (error instanceof TokenError) {
throw error;
}
this.handleProviderError(error, "safe transfer ERC1155 tokens", { contractAddress, fromAddress, toAddress, tokenId, amount });
}
}
/**
* Safely transfer multiple ERC1155 tokens in a batch
*
* @param contractAddress ERC1155 contract address
* @param fromAddress Sender address
* @param toAddress Recipient address
* @param tokenIds Array of token IDs to transfer
* @param amounts Array of amounts to transfer
* @param data Additional data to include with the transfer
* @param provider Optional provider name or instance
* @param chainId Optional chain ID
* @param options Optional transaction options
* @returns Promise with transaction response
*/
async safeBatchTransferERC1155(
contractAddress: string,
fromAddress: string,
toAddress: string,
tokenIds: (string | number)[],
amounts: string[],
data: string = '0x',
provider?: string,
chainId?: number,
options: TokenOperationOptions = {}
): Promise<ethers.TransactionResponse> {
try {
addressSchema.parse(contractAddress);
addressSchema.parse(fromAddress);
addressSchema.parse(toAddress);
return await erc1155.safeBatchTransferFrom(this, contractAddress, fromAddress, toAddress, tokenIds, amounts, data, provider, chainId, options);
} catch (error) {
if (error instanceof TokenError) {
throw error;
}
this.handleProviderError(error, "safe batch transfer ERC1155 tokens", { contractAddress, fromAddress, toAddress, tokenIds, amounts });
}
}
/**
* Prepare ERC1155 NFT transfer transaction for signing
*
* @param contractAddress ERC1155 contract address
* @param fromAddress Sender address
* @param toAddress Recipient address
* @param tokenId Token ID to transfer
* @param amount Amount to transfer
* @param data Additional data (default: '0x')
* @param provider Optional provider name or instance
* @param chainId Optional chain ID
* @param options Optional transaction options
* @returns Promise with prepared transaction object
*/
async prepareERC1155Transfer(
contractAddress: string,
fromAddress: string,
toAddress: string,
tokenId: string | number,
amount: string,
data: string = '0x',
provider?: string,
chainId?: number,
options: TokenOperationOptions = {}
): Promise<ethers.TransactionRequest> {
try {
addressSchema.parse(contractAddress);
addressSchema.parse(fromAddress);
addressSchema.parse(toAddress);
// Get provider
const ethersProvider = this.getProvider(provider, chainId);
// Import ERC1155_ABI
const { ERC1155_ABI } = await import("./erc/constants.js");
// Create contract interface
const contract = new ethers.Contract(contractAddress, ERC1155_ABI, ethersProvider);
// Prepare transaction data for safeTransferFrom
const txData = contract.interface.encodeFunctionData("safeTransferFrom", [
fromAddress,
toAddress,
tokenId.toString(),
amount,
data
]);
// Get network info
const network = await ethersProvider.getNetwork();
// Prepare transaction request
const txRequest: ethers.TransactionRequest = {
to: contractAddress,
data: txData,
from: fromAddress,
chainId: chainId || Number(network.chainId)
};
// Add gas options if provided
if (options.gasLimit) {
txRequest.gasLimit = typeof options.gasLimit === 'string' ?
ethers.getBigInt(options.gasLimit) :
ethers.getBigInt(options.gasLimit.toString());
}
if (options.gasPrice) {
txRequest.gasPrice = typeof options.gasPrice === 'string' ?
ethers.parseUnits(options.gasPrice, 'gwei') :
ethers.parseUnits(options.gasPrice.toString(), 'gwei');
}
if (options.maxFeePerGas) {
txRequest.maxFeePerGas = typeof options.maxFeePerGas === 'string' ?
ethers.parseUnits(options.maxFeePerGas, 'gwei') :
ethers.parseUnits(options.maxFeePerGas.toString(), 'gwei');
}
if (options.maxPriorityFeePerGas) {
txRequest.maxPriorityFeePerGas = typeof options.maxPriorityFeePerGas === 'string' ?
ethers.parseUnits(options.maxPriorityFeePerGas, 'gwei') :
ethers.parseUnits(options.maxPriorityFeePerGas.toString(), 'gwei');
}
return txRequest;
} catch (error) {
this.handleProviderError(error, "prepare ERC1155 transfer", { contractAddress, fromAddress, toAddress, tokenId, amount });
}
}
/**
* Prepare ERC1155 NFT batch transfer transaction for signing
*
* @param contractAddress ERC1155 contract address
* @param fromAddress Sender address
* @param toAddress Recipient address
* @param tokenIds Array of token IDs to transfer
* @param amounts Array of amounts to transfer
* @param data Additional data (default: '0x')
* @param provider Optional provider name or instance
* @param chainId Optional chain ID
* @param options Optional transaction options
* @returns Promise with prepared transaction object
*/
async prepareERC1155BatchTransfer(
contractAddress: string,
fromAddress: string,
toAddress: string,
tokenIds: (string | number)[],
amounts: string[],
data: string = '0x',
provider?: string,
chainId?: number,
options: TokenOperationOptions = {}
): Promise<ethers.TransactionRequest> {
try {
addressSchema.parse(contractAddress);
addressSchema.parse(fromAddress);
addressSchema.parse(toAddress);
// Get provider
const ethersProvider = this.getProvider(provider, chainId);
// Import ERC1155_ABI
const { ERC1155_ABI } = await import("./erc/constants.js");
// Create contract interface
const contract = new ethers.Contract(contractAddress, ERC1155_ABI, ethersProvider);
// Convert tokenIds to strings
const tokenIdStrings = tokenIds.map(id => id.toString());
// Prepare transaction data for safeBatchTransferFrom
const txData = contract.interface.encodeFunctionData("safeBatchTransferFrom", [
fromAddress,
toAddress,
tokenIdStrings,
amounts,
data
]);
// Get network info
const network = await ethersProvider.getNetwork();
// Prepare transaction request
const txRequest: ethers.TransactionRequest = {
to: contractAddress,
data: txData,
from: fromAddress,
chainId: chainId || Number(network.chainId)
};
// Add gas options if provided
if (options.gasLimit) {
txRequest.gasLimit = typeof options.gasLimit === 'string' ?
ethers.getBigInt(options.gasLimit) :
ethers.getBigInt(options.gasLimit.toString());
}
if (options.gasPrice) {
txRequest.gasPrice = typeof options.gasPrice === 'string' ?
ethers.parseUnits(options.gasPrice, 'gwei') :
ethers.parseUnits(options.gasPrice.toString(), 'gwei');
}
if (options.maxFeePerGas) {
txRequest.maxFeePerGas = typeof options.maxFeePerGas === 'string' ?
ethers.parseUnits(options.maxFeePerGas, 'gwei') :
ethers.parseUnits(options.maxFeePerGas.toString(), 'gwei');
}
if (options.maxPriorityFeePerGas) {
txRequest.maxPriorityFeePerGas = typeof options.maxPriorityFeePerGas === 'string' ?
ethers.parseUnits(options.maxPriorityFeePerGas, 'gwei') :
ethers.parseUnits(options.maxPriorityFeePerGas.toString(), 'gwei');
}
return txRequest;
} catch (error) {
this.handleProviderError(error, "prepare ERC1155 batch transfer", { contractAddress, fromAddress, toAddress, tokenIds, amounts });
}
}
/**
* Prepare ERC1155 NFT setApprovalForAll transaction for signing
*
* @param contractAddress ERC1155 contract address
* @param operator Address to approve/revoke for all tokens
* @param approved Whether to approve or revoke
* @param fromAddress Owner address
* @param provider Optional provider name or instance
* @param chainId Optional chain ID
* @param options Optional transaction options
* @returns Promise with prepared transaction object
*/
async prepareERC1155SetApprovalForAll(
contractAddress: string,
operator: string,
approved: boolean,
fromAddress: string,
provider?: string,
chainId?: number,
options: TokenOperationOptions = {}
): Promise<ethers.TransactionRequest> {
try {
addressSchema.parse(contractAddress);
addressSchema.parse(operator);
addressSchema.parse(fromAddress);
// Get provider
const ethersProvider = this.getProvider(provider, chainId);
// Import ERC1155_ABI
const { ERC1155_ABI } = await import("./erc/constants.js");
// Create contract interface
const contract = new ethers.Contract(contractAddress, ERC1155_ABI, ethersProvider);
// Prepare transaction data for setApprovalForAll
const txData = contract.interface.encodeFunctionData("setApprovalForAll", [operator, approved]);
// Get network info
const network = await ethersProvider.getNetwork();
// Prepare transaction request
const txRequest: ethers.TransactionRequest = {
to: contractAddress,
data: txData,
from: fromAddress,
chainId: chainId || Number(network.chainId)
};
// Add gas options if provided
if (options.gasLimit) {
txRequest.gasLimit = typeof options.gasLimit === 'string' ?
ethers.getBigInt(options.gasLimit) :
ethers.getBigInt(options.gasLimit.toString());
}
if (options.gasPrice) {
txRequest.gasPrice = typeof options.gasPrice === 'string' ?
ethers.parseUnits(options.gasPrice, 'gwei') :
ethers.parseUnits(options.gasPrice.toString(), 'gwei');
}
if (options.maxFeePerGas) {
txRequest.maxFeePerGas = typeof options.maxFeePerGas === 'string' ?
ethers.parseUnits(options.maxFeePerGas, 'gwei') :
ethers.parseUnits(options.maxFeePerGas.toString(), 'gwei');
}
if (options.maxPriorityFeePerGas) {
txRequest.maxPriorityFeePerGas = typeof options.maxPriorityFeePerGas === 'string' ?
ethers.parseUnits(options.maxPriorityFeePerGas, 'gwei') :
ethers.parseUnits(options.maxPriorityFeePerGas.toString(), 'gwei');
}
return txRequest;
} catch (error) {
this.handleProviderError(error, "prepare ERC1155 setApprovalForAll", { contractAddress, operator, approved });
}
}
/**
* Get wallet balance in ETH
*
* @param address Address to check balance for
* @param provider Optional provider or network name
* @param chainId Optional chain ID
* @returns Formatted balance string in ETH
*/
async getWalletBalance(address: string, provider?: string, chainId?: number): Promise<string> {
try {
const selectedProvider = this.getProvider(provider, chainId);
const balance = await selectedProvider.getBalance(address);
return ethers.formatEther(balance);
} catch (error) {
this.handleProviderError(error, "get wallet balance", { address });
}
}
/**
* Prepare a basic ETH transfer transaction for signing
*
* @param toAddress Recipient address
* @param value Amount in ETH to send
* @param fromAddress Sender address
* @param provider Optional provider name or instance
* @param chainId Optional chain ID
* @param options Optional transaction options
* @returns Promise with prepared transaction object
*/
async prepareTransaction(
toAddress: string,
value: string,
fromAddress: string,
provider?: string,
chainId?: number,
options: TokenOperationOptions = {}
): Promise<ethers.TransactionRequest> {
try {
addressSchema.parse(toAddress);
addressSchema.parse(fromAddress);
// Get provider
const ethersProvider = this.getProvider(provider, chainId);
// Get network info
const network = await ethersProvider.getNetwork();
// Convert ETH amount to wei
const valueInWei = ethers.parseEther(value);
// Prepare transaction request
const txRequest: ethers.TransactionRequest = {
to: toAddress,
value: valueInWei,
from: fromAddress,
chainId: chainId || Number(network.chainId)
};
// Add gas options if provided
if (options.gasLimit) {
txRequest.gasLimit = typeof options.gasLimit === 'string' ?
ethers.getBigInt(options.gasLimit) :
ethers.getBigInt(options.gasLimit.toString());
}
if (options.gasPrice) {
txRequest.gasPrice = typeof options.gasPrice === 'string' ?
ethers.parseUnits(options.gasPrice, 'gwei') :
ethers.parseUnits(options.gasPrice.toString(), 'gwei');
}
if (options.maxFeePerGas) {
txRequest.maxFeePerGas = typeof options.maxFeePerGas === 'string' ?
ethers.parseUnits(options.maxFeePerGas, 'gwei') :
ethers.parseUnits(options.maxFeePerGas.toString(), 'gwei');
}
if (options.maxPriorityFeePerGas) {
txRequest.maxPriorityFeePerGas = typeof options.maxPriorityFeePerGas === 'string' ?
ethers.parseUnits(options.maxPriorityFeePerGas, 'gwei') :
ethers.parseUnits(options.maxPriorityFeePerGas.toString(), 'gwei');
}
return txRequest;
} catch (error) {
this.handleProviderError(error, "prepare transaction", { toAddress, value, fromAddress });
}
}
/**
* Prepare a smart contract transaction for signing
*
* @param contractAddress Smart contract address
* @param data Contract interaction data (encoded function call)
* @param value Amount in ETH to send (default: "0")
* @param fromAddress Sender address
* @param provider Optional provider name or instance
* @param chainId Optional chain ID
* @param options Optional transaction options
* @returns Promise with prepared transaction object
*/
async prepareContractTransaction(
contractAddress: string,
data: string,
value: string = "0",
fromAddress: string,
provider?: string,
chainId?: number,
options: TokenOperationOptions = {}
): Promise<ethers.TransactionRequest> {
try {
addressSchema.parse(contractAddress);
addressSchema.parse(fromAddress);
// Validate data is hex
if (!data.startsWith('0x')) {
throw new Error('Contract data must be hex string starting with 0x');
}
// Get provider
const ethersProvider = this.getProvider(provider, chainId);
// Get network info
const network = await ethersProvider.getNetwork();
// Convert ETH amount to wei
const valueInWei = ethers.parseEther(value);
// Prepare transaction request
const txRequest: ethers.TransactionRequest = {
to: contractAddress,
data: data,
value: valueInWei,
from: fromAddress,
chainId: chainId || Number(network.chainId)
};
// Add gas options if provided
if (options.gasLimit) {
txRequest.gasLimit = typeof options.gasLimit === 'string' ?
ethers.getBigInt(options.gasLimit) :
ethers.getBigInt(options.gasLimit.toString());
}
if (options.gasPrice) {
txRequest.gasPrice = typeof options.gasPrice === 'string' ?
ethers.parseUnits(options.gasPrice, 'gwei') :
ethers.parseUnits(options.gasPrice.toString(), 'gwei');
}
if (options.maxFeePerGas) {
txRequest.maxFeePerGas = typeof options.maxFeePerGas === 'string' ?
ethers.parseUnits(options.maxFeePerGas, 'gwei') :
ethers.parseUnits(options.maxFeePerGas.toString(), 'gwei');
}
if (options.maxPriorityFeePerGas) {
txRequest.maxPriorityFeePerGas = typeof options.maxPriorityFeePerGas === 'string' ?
ethers.parseUnits(options.maxPriorityFeePerGas, 'gwei') :
ethers.parseUnits(options.maxPriorityFeePerGas.toString(), 'gwei');
}
return txRequest;
} catch (error) {
this.handleProviderError(error, "prepare contract transaction", { contractAddress, data, value, fromAddress });
}
}
/**
* Send a signed transaction to the network
*
* @param signedTransaction The signed transaction data (hex string)
* @param provider Optional provider name or instance
* @param chainId Optional chain ID
* @returns Promise with transaction response and receipt
*/
async sendSignedTransaction(
signedTransaction: string,
provider?: string,
chainId?: number
): Promise<{ hash: string; receipt: ethers.TransactionReceipt | null }> {
try {
// Validate signed transaction is hex
if (!signedTransaction.startsWith('0x')) {
throw new Error('Signed transaction must be hex string starting with 0x');
}
// Get provider
const ethersProvider = this.getProvider(provider, chainId);
// Send the signed transaction
const txResponse = await ethersProvider.broadcastTransaction(signedTransaction);
// Wait for transaction to be mined (with timeout)
let receipt: ethers.TransactionReceipt | null = null;
try {
receipt = await txResponse.wait(1); // Wait for 1 confirmation
} catch (error) {
// Transaction might still be pending, that's ok
silentLogger.debug('Transaction receipt not available yet:', { error: error instanceof Error ? error.message : String(error) });
}
return {
hash: txResponse.hash,
receipt: receipt
};
} catch (error) {
this.handleProviderError(error, "send signed transaction", { signedTransaction: signedTransaction.substring(0, 20) + '...' });
}
}
}