Compare commits

..

No commits in common. "main" and "fetch_all_transactions" have entirely different histories.

6 changed files with 260 additions and 1637 deletions

View File

@ -1,17 +1,11 @@
name: Deploy to Dappbundle name: Workflow push to Dappbundle
on: [push]
on:
push:
branches:
- main
jobs: jobs:
build: build:
name: Deploy name: Build
runs-on: ubuntu-latest runs-on: self-hosted
steps: steps:
- name: Deploy via SSH - name: Executing remote command
uses: appleboy/ssh-action@v1.0.0 uses: appleboy/ssh-action@v1.0.0
with: with:
host: ${{ secrets.R_HOST }} host: ${{ secrets.R_HOST }}
@ -19,31 +13,20 @@ jobs:
password: ${{ secrets.P_PASSWORD }} password: ${{ secrets.P_PASSWORD }}
port: ${{ secrets.SSH_PORT }} port: ${{ secrets.SSH_PORT }}
script: | script: |
set -e if [ -d "${{ secrets.DEPLOYMENT_LOCATION}}/dappbundle" ]; then
echo "Folder exists. Skipping Git clone."
BASE="${{ secrets.DEPLOYMENT_LOCATION }}"
APP="${{ github.event.repository.name }}"
echo "== Ensuring dappbundle repo exists =="
if [ ! -d "$BASE/dappbundle/.git" ]; then
echo "Cloning dappbundle..."
git clone git@gitea.ranchimall.net:RanchiMall/dappbundle.git "$BASE/dappbundle"
else else
echo "Updating dappbundle..." echo "Folder does not exist. Cloning repository..."
cd "$BASE/dappbundle" cd ${{ secrets.DEPLOYMENT_LOCATION}}/ && git clone https://github.com/ranchimall/dappbundle.git
git pull
fi fi
echo "== Refreshing app bundle ==" if [ -d "${{ secrets.DEPLOYMENT_LOCATION}}/dappbundle/${{ github.event.repository.name }}" ]; then
echo "Repository exists. Remove folder "
rm -r "${{ secrets.DEPLOYMENT_LOCATION}}/dappbundle/${{ github.event.repository.name }}"
fi
rm -rf "$BASE/dappbundle/$APP" echo "Cloning repository..."
git clone git@gitea.ranchimall.net:RanchiMall/$APP.git "$BASE/dappbundle/$APP" cd ${{ secrets.DEPLOYMENT_LOCATION}}/dappbundle && git clone https://github.com/ranchimall/${{ github.event.repository.name }}
cd "$BASE/dappbundle" cd "${{ secrets.DEPLOYMENT_LOCATION}}/dappbundle/${{ github.event.repository.name }}" && rm -rf .gitattributes .git .github .gitignore
git config user.email "ranchimallfze@gmail.com" cd ${{ secrets.DEPLOYMENT_LOCATION}}/dappbundle/ && git add . && git commit -m "Workflow updating files of ${{ github.event.repository.name }}" && git push "https://ranchimalldev:${{ secrets.RM_ACCESS_TOKEN }}@github.com/ranchimall/dappbundle.git"
git config user.name "ranchimall"
git add .
git commit -m "Auto-update $APP" || echo "No changes to commit"
git push

View File

@ -1 +1 @@
# Check balance, transaction history and send ERC-20 USDT and USDC on Ethereum mainnet with your FLO/BTC/ETH private key (WIF) # Check your USDC and USDT balance on Ethereum mainnet with you FLO/BTC private key (WIF)

View File

@ -788,20 +788,20 @@ theme-toggle {
align-self: center; align-self: center;
} }
.label { .label {
text-transform: capitalize; text-transform: capitalize;
font-size: 0.8rem; font-size: 0.8rem;
margin-bottom: 0.3rem; margin-bottom: 0.3rem;
color: rgba(var(--text-color), 0.8); color: rgba(var(--text-color), 0.8);
margin-top: 1.5rem; margin-top: 1.5rem;
font-weight: 500; font-weight: 500;
} }
.label:first-of-type { .label:first-of-type {
margin-top: 0; margin-top: 0;
} }
.label + :is(h1, h2, h3, h4, h5, h6, p, span, sm-copy, a) { .label + :is(h1, h2, h3, h4, h5, h6, p, span, sm-copy, a) {
font-weight: 700; font-weight: 700;
} }
main { main {
display: grid; display: grid;
@ -887,14 +887,6 @@ main {
flex-direction: column; flex-direction: column;
width: min(100%, 42rem); width: min(100%, 42rem);
} }
#page_container[data-page=retrieve] {
align-items: flex-start;
}
#page_container[data-page=retrieve] > * {
padding: 1rem;
margin: 0 auto;
}
aside { aside {
view-transition-name: search-history; view-transition-name: search-history;
@ -959,106 +951,6 @@ aside h4 {
padding-bottom: 0.5rem; padding-bottom: 0.5rem;
} }
/* Transaction list styling matching BTC wallet */
.transaction {
display: grid;
grid-template-columns: auto 1fr;
gap: 0.8rem;
padding: 1rem;
border-bottom: solid thin rgba(var(--text-color), 0.1);
}
.transaction:last-child {
border-bottom: none;
}
.transaction__icon {
display: flex;
align-items: flex-start;
padding-top: 0.2rem;
}
.transaction__icon .icon {
width: 1.5rem;
height: 1.5rem;
fill: rgba(var(--text-color), 0.7);
}
.transaction.in .icon {
fill: var(--green);
}
.transaction.out .icon.sent {
fill: var(--danger-color);
}
.transaction__time {
font-size: 0.85rem;
color: rgba(var(--text-color), 0.7);
}
.transaction__amount {
font-weight: 600;
font-size: 0.95rem;
}
.transaction.in .transaction__amount {
color: var(--green);
}
.transaction.out .transaction__amount {
color: var(--danger-color);
}
.transaction__receiver {
font-size: 0.9rem;
color: rgba(var(--text-color), 0.9);
word-break: break-all;
}
.tx-participant {
color: var(--accent-color);
text-decoration: none;
font-family: monospace;
font-size: 0.85rem;
}
.tx-participant:hover {
text-decoration: underline;
}
.wrap-around {
word-break: break-all;
}
.transaction__id {
display: inline-flex;
align-items: center;
}
.transaction__id .icon {
width: 1rem;
height: 1rem;
fill: currentColor;
}
#transaction_list {
max-height: 500px;
overflow-y: auto;
/* Hide scrollbar for Chrome, Safari and Opera */
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE and Edge */
}
#transaction_list::-webkit-scrollbar {
display: none; /* Chrome, Safari and Opera */
}
#tx_filter_chips sm-chip {
font-size: 0.75rem;
--padding: 0.3rem 0.6rem;
}
#error_section { #error_section {
display: grid; display: grid;
height: 100%; height: 100%;
@ -1279,18 +1171,4 @@ aside h4 {
-webkit-animation: none !important; -webkit-animation: none !important;
animation: none !important; animation: none !important;
} }
} }
.transaction__time {
font-size: 0.75rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex-shrink: 1;
min-width: 0;
}
.transaction__amount {
font-size: 0.75rem;
margin-left: auto;
white-space: nowrap;
flex-shrink: 0;
}

View File

@ -1113,101 +1113,6 @@ aside {
} }
} }
/* Transaction list styling matching BTC wallet */
.transaction {
display: grid;
grid-template-columns: auto 1fr;
gap: 0.8rem;
padding: 1rem;
border-bottom: solid thin rgba(var(--text-color), 0.1);
&:last-child {
border-bottom: none;
}
}
.transaction__icon {
display: flex;
align-items: flex-start;
padding-top: 0.2rem;
.icon {
width: 1.5rem;
height: 1.5rem;
fill: rgba(var(--text-color), 0.7);
}
}
.transaction.in .icon {
fill: var(--green);
}
.transaction.out .icon.sent {
fill: var(--danger-color);
}
.transaction__time {
font-size: 0.85rem;
color: rgba(var(--text-color), 0.7);
}
.transaction__amount {
font-weight: 600;
font-size: 0.95rem;
}
.transaction.in .transaction__amount {
color: var(--green);
}
.transaction.out .transaction__amount {
color: var(--danger-color);
}
.transaction__receiver {
font-size: 0.9rem;
color: rgba(var(--text-color), 0.9);
word-break: break-all;
}
.tx-participant {
color: var(--accent-color);
text-decoration: none;
font-family: monospace;
font-size: 0.85rem;
&:hover {
text-decoration: underline;
}
}
.wrap-around {
word-break: break-all;
}
.transaction__id {
display: inline-flex;
align-items: center;
.icon {
width: 1rem;
height: 1rem;
fill: currentColor;
}
}
#transaction_list {
max-height: 500px;
overflow-y: auto;
/* Hide scrollbar for Chrome, Safari and Opera */
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE and Edge */
&::-webkit-scrollbar {
display: none; /* Chrome, Safari and Opera */
}
}
@media only screen and (max-width: 640px) { @media only screen and (max-width: 640px) {
.hide-on-small { .hide-on-small {
display: none; display: none;
@ -1267,9 +1172,7 @@ aside {
} }
} }
aside { aside {
min-width: 18rem;
border-right: solid thin rgba(var(--text-color), 0.3); border-right: solid thin rgba(var(--text-color), 0.3);
overflow-y: auto; overflow-y: auto;
@ -1335,37 +1238,4 @@ aside {
::view-transition-new(*) { ::view-transition-new(*) {
animation: none !important; animation: none !important;
} }
} }
// Transaction display styles
.transaction__time {
font-size: 0.75rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex-shrink: 1;
min-width: 0;
}
.transaction__amount {
font-size: 0.75rem;
margin-left: auto;
white-space: nowrap;
flex-shrink: 0;
}
.transaction {
grid-template-columns: auto 1fr;
gap: 0.5rem;
}
.transaction__icon {
display: flex;
align-items: flex-start;
padding-top: 0.2rem;
.icon {
width: 1rem;
height: 1rem;
}
}

1215
index.html

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,4 @@
(function (EXPORTS) { // ethOperator v1.0.2 (function (EXPORTS) { //ethOperator v1.0.2
/* ETH Crypto and API Operator */ /* ETH Crypto and API Operator */
if (!window.ethers) if (!window.ethers)
return console.error('ethers.js not found') return console.error('ethers.js not found')
@ -240,51 +240,61 @@
usdc: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", usdc: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
usdt: "0xdac17f958d2ee523a2206206994597c13d831ec7" usdt: "0xdac17f958d2ee523a2206206994597c13d831ec7"
} }
/** function getProvider() {
* Get Ethereum provider (MetaMask or public RPC) // switches provider based on whether the user is using MetaMask or not
* @param {boolean} readOnly - If true, use public RPC; if false, use MetaMask when available if (window.ethereum) {
* @returns {ethers.providers.Provider} Ethereum provider instance
*/
const getProvider = ethOperator.getProvider = (readOnly = false) => {
if (!readOnly && window.ethereum) {
return new ethers.providers.Web3Provider(window.ethereum); return new ethers.providers.Web3Provider(window.ethereum);
} else { } else {
return new ethers.providers.JsonRpcProvider(`https://mainnet.infura.io/v3/6e12fee52bdd48208f0d82fb345bcb3c`) return new ethers.providers.JsonRpcProvider(`https://mainnet.infura.io/v3/6e12fee52bdd48208f0d82fb345bcb3c`)
} }
} }
// Note: MetaMask connection is handled in the UI layer, not here function connectToMetaMask() {
return new Promise((resolve, reject) => {
// if (typeof window.ethereum === "undefined")
// return reject("MetaMask not installed");
return resolve(true)
ethereum
.request({ method: 'eth_requestAccounts' })
.then((accounts) => {
console.log('Connected to MetaMask')
return resolve(accounts)
})
.catch((err) => {
console.log(err)
return reject(err)
})
})
}
// connectToMetaMask();
const getBalance = ethOperator.getBalance = async (address) => { const getBalance = ethOperator.getBalance = async (address) => {
try { try {
if (!address || !isValidAddress(address)) if (!address || !isValidAddress(address))
return new Error('Invalid address'); return new Error('Invalid address');
// Get the balance
// Use read-only provider (public RPC) for balance checks const provider = getProvider();
const provider = getProvider(true);
const balanceWei = await provider.getBalance(address); const balanceWei = await provider.getBalance(address);
const balanceEth = parseFloat(ethers.utils.formatEther(balanceWei)); const balanceEth = parseFloat(ethers.utils.formatEther(balanceWei));
return balanceEth; return balanceEth;
} catch (error) { } catch (error) {
console.error('Balance error:', error.message); console.error('Error:', error.message);
return 0; return error;
} }
} }
const getTokenBalance = ethOperator.getTokenBalance = async (address, token, { contractAddress } = {}) => { const getTokenBalance = ethOperator.getTokenBalance = async (address, token, { contractAddress } = {}) => {
try { try {
// if (!window.ethereum.isConnected()) {
// await connectToMetaMask();
// }
if (!token) if (!token)
return new Error("Token not specified"); return new Error("Token not specified");
if (!CONTRACT_ADDRESSES[token] && contractAddress) if (!CONTRACT_ADDRESSES[token] && contractAddress)
return new Error('Contract address of token not available') return new Error('Contract address of token not available')
const usdcContract = new ethers.Contract(CONTRACT_ADDRESSES[token] || contractAddress, ERC20ABI, getProvider());
// Use read-only provider (public RPC) for token balance checks let balance = await usdcContract.balanceOf(address);
const provider = getProvider(true); balance = parseFloat(ethers.utils.formatUnits(balance, 6)); // Assuming 6 decimals
const tokenAddress = CONTRACT_ADDRESSES[token] || contractAddress;
const tokenContract = new ethers.Contract(tokenAddress, ERC20ABI, provider);
let balance = await tokenContract.balanceOf(address);
balance = parseFloat(ethers.utils.formatUnits(balance, 6)); // USDC and USDT use 6 decimals
return balance; return balance;
} catch (e) { } catch (e) {
console.error('Token balance error:', e); console.error(e);
return 0;
} }
} }
@ -307,301 +317,28 @@
const provider = getProvider(); const provider = getProvider();
const signer = new ethers.Wallet(privateKey, provider); const signer = new ethers.Wallet(privateKey, provider);
const limit = await estimateGas({ privateKey, receiver, amount }) const limit = await estimateGas({ privateKey, receiver, amount })
// Get current fee data from the network
const feeData = await provider.getFeeData();
// Calculate priority fee (tip to miners) - use 1.5 gwei or the network's suggested priority fee, whichever is higher
const priorityFee = feeData.maxPriorityFeePerGas || ethers.utils.parseUnits("1.5", "gwei");
// Calculate max fee per gas (base fee + priority fee)
// Use the network's suggested maxFeePerGas or calculate it manually
let maxFee = feeData.maxFeePerGas;
// If maxFeePerGas is not available or is less than priority fee, calculate it
if (!maxFee || maxFee.lt(priorityFee)) {
// Get the base fee from the latest block and add our priority fee
const block = await provider.getBlock("latest");
const baseFee = block.baseFeePerGas || ethers.utils.parseUnits("1", "gwei");
// maxFee = (baseFee * 2) + priorityFee to account for potential base fee increases
maxFee = baseFee.mul(2).add(priorityFee);
}
// Ensure maxFee is at least 1.5x the priority fee for safety
const minMaxFee = priorityFee.mul(15).div(10); // 1.5x priority fee
if (maxFee.lt(minMaxFee)) {
maxFee = minMaxFee;
}
// Creating and sending the transaction object // Creating and sending the transaction object
return signer.sendTransaction({ return signer.sendTransaction({
to: receiver, to: receiver,
value: ethers.utils.parseUnits(amount, "ether"), value: ethers.utils.parseUnits(amount, "ether"),
gasLimit: limit, gasLimit: limit,
nonce: await signer.getTransactionCount(), nonce: signer.getTransactionCount(),
maxPriorityFeePerGas: priorityFee, maxPriorityFeePerGas: ethers.utils.parseUnits("2", "gwei"),
maxFeePerGas: maxFee,
}) })
} catch (e) { } catch (e) {
throw new Error(e) throw new Error(e)
} }
} }
/**
* Send ERC20 tokens (USDC or USDT)
* @param {object} params - Transaction parameters
* @param {string} params.token - Token symbol ('usdc' or 'usdt')
* @param {string} params.privateKey - Sender's private key
* @param {string} params.amount - Amount to send
* @param {string} params.receiver - Recipient's Ethereum address
* @param {string} params.contractAddress - Optional custom contract address
* @returns {Promise} Transaction promise
*/
const sendToken = ethOperator.sendToken = async ({ token, privateKey, amount, receiver, contractAddress }) => { const sendToken = ethOperator.sendToken = async ({ token, privateKey, amount, receiver, contractAddress }) => {
// Create a wallet using the private key
const wallet = new ethers.Wallet(privateKey, getProvider()); const wallet = new ethers.Wallet(privateKey, getProvider());
// Contract interface
const tokenContract = new ethers.Contract(CONTRACT_ADDRESSES[token] || contractAddress, ERC20ABI, wallet); const tokenContract = new ethers.Contract(CONTRACT_ADDRESSES[token] || contractAddress, ERC20ABI, wallet);
// Convert amount to smallest unit (both USDC and USDT use 6 decimals) // Convert the amount to the smallest unit of USDC (wei)
const amountWei = ethers.utils.parseUnits(amount.toString(), 6); const amountWei = ethers.utils.parseUnits(amount.toString(), 6); // Assuming 6 decimals for USDC
// Call the transfer function on the USDC contract
return tokenContract.transfer(receiver, amountWei) return tokenContract.transfer(receiver, amountWei)
} }
const ETHERSCAN_API_KEY = 'M3YBAHI21FVE7VS2FEKU6ZFGRA128WUVQK';
/**
* Get transaction history for an Ethereum address
* @param {string} address - Ethereum address
* @param {object} options - Optional parameters
* @returns {Promise<Array>} Array of transactions
*/
const getTransactionHistory = ethOperator.getTransactionHistory = async (address, options = {}) => {
try {
if (!address || !isValidAddress(address)) {
throw new Error('Invalid Ethereum address');
}
const {
startBlock = 0,
endBlock = 99999999,
page = 1,
offset = 100,
sort = 'desc'
} = options;
// Fetch normal transactions using V2 API
const normalTxUrl = `https://api.etherscan.io/v2/api?chainid=1&module=account&action=txlist&address=${address}&startblock=${startBlock}&endblock=${endBlock}&page=${page}&offset=${offset}&sort=${sort}&apikey=${ETHERSCAN_API_KEY}`;
const normalTxResponse = await fetch(normalTxUrl);
const normalTxData = await normalTxResponse.json();
if (normalTxData.status !== '1') {
if (normalTxData.message === 'No transactions found') {
return [];
}
// Provide more detailed error messages
if (normalTxData.result && normalTxData.result.includes('Invalid API Key')) {
throw new Error('Invalid Etherscan API Key. Please check your API key.');
}
if (normalTxData.result && normalTxData.result.includes('Max rate limit reached')) {
throw new Error('Etherscan API rate limit reached. Please try again later.');
}
throw new Error(`Etherscan API Error: ${normalTxData.message || normalTxData.result || 'Failed to fetch transactions'}`);
}
// Fetch ERC20 token transfers using V2 API
const tokenTxUrl = `https://api.etherscan.io/v2/api?chainid=1&module=account&action=tokentx&address=${address}&startblock=${startBlock}&endblock=${endBlock}&page=${page}&offset=${offset}&sort=${sort}&apikey=${ETHERSCAN_API_KEY}`;
const tokenTxResponse = await fetch(tokenTxUrl);
const tokenTxData = await tokenTxResponse.json();
const allowedTokenAddresses = Object.values(CONTRACT_ADDRESSES).map(addr => addr.toLowerCase());
const rawTokenTransfers = tokenTxData.status === '1' ? tokenTxData.result : [];
const tokenTransfers = rawTokenTransfers.filter(tx =>
allowedTokenAddresses.includes(tx.contractAddress.toLowerCase()) && tx.value !== '0'
);
// Combine and sort transactions
// Filter out normal transactions that are already present in token transfers (duplicate hash) AND have 0 value
// This prevents showing "0 ETH to Contract" alongside the actual "Token Transfer"
const tokenTxHashes = new Set(tokenTransfers.map(tx => tx.hash));
const uniqueNormalTxs = normalTxData.result.filter(tx => {
if (tokenTxHashes.has(tx.hash) && tx.value === '0') {
return false;
}
return true;
});
const allTransactions = [...uniqueNormalTxs, ...tokenTransfers];
// Sort by timestamp (descending)
allTransactions.sort((a, b) => parseInt(b.timeStamp) - parseInt(a.timeStamp));
// Parse and format transactions
return allTransactions.map(tx => {
const isTokenTransfer = tx.tokenSymbol !== undefined;
const isReceived = tx.to.toLowerCase() === address.toLowerCase();
let value, symbol, decimals;
if (isTokenTransfer) {
decimals = parseInt(tx.tokenDecimal) || 18;
value = parseFloat(ethers.utils.formatUnits(tx.value, decimals));
symbol = tx.tokenSymbol || 'TOKEN';
} else {
value = parseFloat(ethers.utils.formatEther(tx.value));
symbol = 'ETH';
}
return {
hash: tx.hash,
from: tx.from,
to: tx.to,
value: value,
symbol: symbol,
timestamp: parseInt(tx.timeStamp),
blockNumber: parseInt(tx.blockNumber),
isReceived: isReceived,
isSent: !isReceived,
gasUsed: tx.gasUsed ? parseInt(tx.gasUsed) : 0,
gasPrice: tx.gasPrice ? parseFloat(ethers.utils.formatUnits(tx.gasPrice, 'gwei')) : 0,
isError: tx.isError === '1' || tx.txreceipt_status === '0',
contractAddress: tx.contractAddress || null,
tokenName: tx.tokenName || null,
confirmations: tx.confirmations ? parseInt(tx.confirmations) : 0,
nonce: tx.nonce ? parseInt(tx.nonce) : 0,
input: tx.input || '0x',
isTokenTransfer: isTokenTransfer
};
});
} catch (error) {
console.error('Error fetching transaction history:', error);
throw error;
}
};
/**
* Get detailed information about a specific transaction
* @param {string} txHash - Transaction hash
* @returns {Promise<Object>} Transaction details
*/
const getTransactionDetails = ethOperator.getTransactionDetails = async (txHash) => {
try {
if (!txHash || !/^0x([A-Fa-f0-9]{64})$/.test(txHash)) {
throw new Error('Invalid transaction hash');
}
// Use read-only provider for fetching transaction details
const provider = getProvider(true);
// Get transaction details
const tx = await provider.getTransaction(txHash);
if (!tx) {
throw new Error('Transaction not found');
}
// Get transaction receipt for status and gas used
const receipt = await provider.getTransactionReceipt(txHash);
// Get current block number for confirmations
const currentBlock = await provider.getBlockNumber();
// Get block details for timestamp
const block = await provider.getBlock(tx.blockNumber);
// Calculate gas fee
const gasUsed = receipt ? receipt.gasUsed : null;
const effectiveGasPrice = receipt ? receipt.effectiveGasPrice : tx.gasPrice;
const gasFee = gasUsed && effectiveGasPrice ?
parseFloat(ethers.utils.formatEther(gasUsed.mul(effectiveGasPrice))) : null;
// Check if it's a token transfer by examining logs
let tokenTransfers = [];
// Simple in-memory cache for token metadata to avoid rate limiting
const TOKEN_METADATA_CACHE = ethOperator.TOKEN_METADATA_CACHE || {};
ethOperator.TOKEN_METADATA_CACHE = TOKEN_METADATA_CACHE;
if (receipt && receipt.logs.length > 0) {
// Try to decode ERC20 Transfer event
const transferEventSignature = ethers.utils.id('Transfer(address,address,uint256)');
const transferLogs = receipt.logs.filter(log => log.topics[0] === transferEventSignature);
if (transferLogs.length > 0) {
try {
// Process all transfer logs
tokenTransfers = await Promise.all(transferLogs.map(async (transferLog) => {
const contractAddress = transferLog.address;
let symbol, decimals;
// Check cache first
if (TOKEN_METADATA_CACHE[contractAddress]) {
({ symbol, decimals } = TOKEN_METADATA_CACHE[contractAddress]);
} else {
// Fetch from network if not cached
const tokenContract = new ethers.Contract(contractAddress, ERC20ABI, provider);
[symbol, decimals] = await Promise.all([
tokenContract.symbol().catch(() => 'TOKEN'),
tokenContract.decimals().catch(() => 18)
]);
// Store in cache
TOKEN_METADATA_CACHE[contractAddress] = { symbol, decimals };
}
const from = ethers.utils.getAddress('0x' + transferLog.topics[1].slice(26));
const to = ethers.utils.getAddress('0x' + transferLog.topics[2].slice(26));
const value = parseFloat(ethers.utils.formatUnits(transferLog.data, decimals));
return {
from,
to,
value,
symbol,
contractAddress
};
}));
} catch (e) {
console.warn('Could not decode token transfers:', e);
}
}
}
return {
hash: tx.hash,
from: tx.from,
to: tx.to,
value: parseFloat(ethers.utils.formatEther(tx.value)),
symbol: 'ETH',
blockNumber: tx.blockNumber,
timestamp: block ? block.timestamp : null,
confirmations: currentBlock - tx.blockNumber,
gasLimit: tx.gasLimit.toString(),
gasUsed: gasUsed ? gasUsed.toString() : null,
gasPrice: parseFloat(ethers.utils.formatUnits(tx.gasPrice, 'gwei')),
gasFee: gasFee,
nonce: tx.nonce,
input: tx.data,
status: receipt ? (receipt.status === 1 ? 'success' : 'failed') : 'pending',
isError: receipt ? receipt.status !== 1 : false,
tokenTransfers: tokenTransfers,
logs: receipt ? receipt.logs : [],
type: tx.type
};
} catch (error) {
console.error('Error fetching transaction details:', error);
throw error;
}
};
/**
* Check if a string is a valid transaction hash
* @param {string} hash - Potential transaction hash
* @returns {boolean}
*/
const isValidTxHash = ethOperator.isValidTxHash = (hash) => {
return /^0x([A-Fa-f0-9]{64})$/.test(hash);
};
})('object' === typeof module ? module.exports : window.ethOperator = {}); })('object' === typeof module ? module.exports : window.ethOperator = {});