Compare commits

..

21 Commits

Author SHA1 Message Date
9ccbc02b42 Update README.md
Some checks failed
Deploy to Dappbundle / Deploy (push) Failing after 16s
2026-03-01 22:56:19 +00:00
7de196645b Update .github/workflows/push-dappbundle.yml
Some checks failed
Deploy to Dappbundle / Deploy (push) Has been cancelled
2026-03-01 22:55:46 +00:00
a9fa4f3c78 Update .github/workflows/push-dappbundle.yml
Some checks failed
Workflow push to Dappbundle / Build (push) Failing after 2m3s
2026-02-25 21:38:41 +00:00
f0c422b183 Update README.md
Some checks are pending
Workflow push to Dappbundle / Build (push) Waiting to run
2026-02-25 21:29:16 +00:00
a2105fc970 Fix mobile UI truncation in transaction list and filter chips
Some checks are pending
Workflow push to Dappbundle / Build (push) Waiting to run
2026-02-01 14:31:26 +05:30
d106eb516d Updated [ethOperator.js] to filter out transactions with 0 value to prevent spam. 2026-02-01 03:16:25 +05:30
8c768f0081 Refactored transaction filtering and pagination to apply filters to the entire dataset before pagination. 2026-02-01 02:35:00 +05:30
1125e65ed4 feat: Filtered transaction history to only show verified tokens (USDC, USDT) in ethOperator.js to prevent spam 2026-02-01 01:58:14 +05:30
1aa07f35e5 Fix Transaction History, Details, and Sorting Logic
- **Transaction History**:
  - Deduplicated entries: Filtered out redundant "0 ETH" contract calls when a corresponding Token Transfer exists.
  - Fixed Sorting: Implemented batch fetching (500 items) and local pagination to ensure transactions are sorted correctly by date across all pages.

- **Transaction Details**:
  - Enhanced Display: Now shows ALL token transfers within a transaction
  - Clarified "To" Address: Explicitly labeled the contract address as "To (Contract)" and added the actual token receiver in the transfer list.
  - RPC Optimization: Implemented in-memory caching for token metadata (decimals/symbols) to prevent Infura rate-limiting errors (429).

- **UI Improvements**:
  - Transaction Details: Updated Token Transfer list layout to stack addresses vertically on new lines for better readability on mobile.
2026-01-31 18:05:48 +05:30
54831c71bc Merge branch 'main' of https://github.com/ranchimall/floethereum
Some checks failed
Workflow push to Dappbundle / Build (push) Has been cancelled
2026-01-24 00:27:09 +05:30
5d1882e6a7 feat: Add Bitcoin address support and improve UX
- Add Bitcoin bech32 address generation with proper BTC WIF private keys (L/K prefix)
- Reject FLO/BTC addresses in balance search (ETH addresses and private keys only)
- Update search placeholder for clarity
- Move notifications to top-right on desktop to avoid Searched addresses overlap
2026-01-24 00:27:03 +05:30
b121b1aec7
Update README 2026-01-12 18:16:45 +05:30
67309e9273 Merge branch 'main' of https://github.com/ranchimall/floethereum 2026-01-12 17:04:21 +05:30
212fa5047a fix: update git push credentials in workflow 2026-01-12 17:04:05 +05:30
SaketAnand
0e03af78ef
Update README.md 2026-01-08 00:43:36 +05:30
1a956912ab
Merge pull request #4 from void-57/main
feat: Add comprehensive wallet features
2025-12-30 00:11:49 +05:30
8d0f9db29b feat: Add comprehensive wallet features
- Add balance checking for ETH, USDC, and USDT tokens
- Implement transaction history with filtering (All/Received/Sent)
- Add pagination support for transaction history
- Implement transaction hash search functionality
- Fix ETH transaction sending issues
- Add gas fee calculation popup before sending transactions

This update significantly enhances the wallet functionality by providing
users with complete visibility into their token balances and transaction
history, along with improved transaction management and gas fee transparency.
2025-12-29 00:23:03 +05:30
ae8e519f73
Merge pull request #3 from void-57/main
fix: improve sidebar UI layout
2025-12-11 00:01:11 +05:30
bae54354fc fix: improve sidebar UI layout 2025-12-10 22:07:12 +05:30
5a30567825
Adding retrieve functionality 2025-08-17 19:52:43 +05:30
e2f6e1dcab
Adding Retrieve Address Functionality 2025-08-17 19:52:05 +05:30
6 changed files with 1638 additions and 262 deletions

View File

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

View File

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

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,6 +887,14 @@ 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;
@ -951,6 +959,106 @@ 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%;
@ -1171,4 +1279,18 @@ 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,6 +1113,101 @@ 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;
@ -1172,7 +1267,9 @@ 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;
@ -1238,4 +1335,37 @@ 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;
}
}

1219
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,61 +240,51 @@
usdc: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", usdc: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
usdt: "0xdac17f958d2ee523a2206206994597c13d831ec7" usdt: "0xdac17f958d2ee523a2206206994597c13d831ec7"
} }
function getProvider() { /**
// switches provider based on whether the user is using MetaMask or not * Get Ethereum provider (MetaMask or public RPC)
if (window.ethereum) { * @param {boolean} readOnly - If true, use public RPC; if false, use MetaMask when available
* @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`)
} }
} }
function connectToMetaMask() { // Note: MetaMask connection is handled in the UI layer, not here
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
const provider = getProvider(); // Use read-only provider (public RPC) for balance checks
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('Error:', error.message); console.error('Balance error:', error.message);
return error; return 0;
} }
} }
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());
let balance = await usdcContract.balanceOf(address); // Use read-only provider (public RPC) for token balance checks
balance = parseFloat(ethers.utils.formatUnits(balance, 6)); // Assuming 6 decimals const provider = getProvider(true);
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(e); console.error('Token balance error:', e);
return 0;
} }
} }
@ -317,28 +307,301 @@
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: signer.getTransactionCount(), nonce: await signer.getTransactionCount(),
maxPriorityFeePerGas: ethers.utils.parseUnits("2", "gwei"), maxPriorityFeePerGas: priorityFee,
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 the amount to the smallest unit of USDC (wei) // Convert amount to smallest unit (both USDC and USDT use 6 decimals)
const amountWei = ethers.utils.parseUnits(amount.toString(), 6); // Assuming 6 decimals for USDC const amountWei = ethers.utils.parseUnits(amount.toString(), 6);
// 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 = {});