Compare commits

..

12 Commits

Author SHA1 Message Date
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 1546 additions and 256 deletions

View File

@ -29,4 +29,4 @@ jobs:
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
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 ${{ secrets.DEPLOYMENT_LOCATION}}/dappbundle/ && git add . && git commit -m "Workflow updating files of ${{ github.event.repository.name }}" && git push "https://saketongit:${{ secrets.RM_ACCESS_TOKEN }}@github.com/ranchimall/dappbundle.git"

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 USDC and USDT on Ethereum mainnet with your FLO/BTC/ETH private key (WIF)

View File

@ -788,20 +788,20 @@ theme-toggle {
align-self: center;
}
.label {
text-transform: capitalize;
font-size: 0.8rem;
margin-bottom: 0.3rem;
color: rgba(var(--text-color), 0.8);
margin-top: 1.5rem;
font-weight: 500;
}
.label:first-of-type {
margin-top: 0;
}
.label + :is(h1, h2, h3, h4, h5, h6, p, span, sm-copy, a) {
font-weight: 700;
}
.label {
text-transform: capitalize;
font-size: 0.8rem;
margin-bottom: 0.3rem;
color: rgba(var(--text-color), 0.8);
margin-top: 1.5rem;
font-weight: 500;
}
.label:first-of-type {
margin-top: 0;
}
.label + :is(h1, h2, h3, h4, h5, h6, p, span, sm-copy, a) {
font-weight: 700;
}
main {
display: grid;
@ -887,6 +887,14 @@ main {
flex-direction: column;
width: min(100%, 42rem);
}
#page_container[data-page=retrieve] {
align-items: flex-start;
}
#page_container[data-page=retrieve] > * {
padding: 1rem;
margin: 0 auto;
}
aside {
view-transition-name: search-history;
@ -951,6 +959,101 @@ aside h4 {
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 */
}
#error_section {
display: grid;
height: 100%;
@ -1171,4 +1274,18 @@ aside h4 {
-webkit-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) {
.hide-on-small {
display: none;
@ -1172,7 +1267,9 @@ aside {
}
}
aside {
min-width: 18rem;
border-right: solid thin rgba(var(--text-color), 0.3);
overflow-y: auto;
@ -1238,4 +1335,37 @@ aside {
::view-transition-new(*) {
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;
}
}

1206
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 */
if (!window.ethers)
return console.error('ethers.js not found')
@ -240,61 +240,51 @@
usdc: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
usdt: "0xdac17f958d2ee523a2206206994597c13d831ec7"
}
function getProvider() {
// switches provider based on whether the user is using MetaMask or not
if (window.ethereum) {
/**
* Get Ethereum provider (MetaMask or public RPC)
* @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);
} else {
return new ethers.providers.JsonRpcProvider(`https://mainnet.infura.io/v3/6e12fee52bdd48208f0d82fb345bcb3c`)
}
}
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();
// Note: MetaMask connection is handled in the UI layer, not here
const getBalance = ethOperator.getBalance = async (address) => {
try {
if (!address || !isValidAddress(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 balanceEth = parseFloat(ethers.utils.formatEther(balanceWei));
return balanceEth;
} catch (error) {
console.error('Error:', error.message);
return error;
console.error('Balance error:', error.message);
return 0;
}
}
const getTokenBalance = ethOperator.getTokenBalance = async (address, token, { contractAddress } = {}) => {
try {
// if (!window.ethereum.isConnected()) {
// await connectToMetaMask();
// }
if (!token)
return new Error("Token not specified");
if (!CONTRACT_ADDRESSES[token] && contractAddress)
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);
balance = parseFloat(ethers.utils.formatUnits(balance, 6)); // Assuming 6 decimals
// Use read-only provider (public RPC) for token balance checks
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;
} catch (e) {
console.error(e);
console.error('Token balance error:', e);
return 0;
}
}
@ -317,28 +307,269 @@
const provider = getProvider();
const signer = new ethers.Wallet(privateKey, provider);
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
return signer.sendTransaction({
to: receiver,
value: ethers.utils.parseUnits(amount, "ether"),
gasLimit: limit,
nonce: signer.getTransactionCount(),
maxPriorityFeePerGas: ethers.utils.parseUnits("2", "gwei"),
nonce: await signer.getTransactionCount(),
maxPriorityFeePerGas: priorityFee,
maxFeePerGas: maxFee,
})
} catch (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 }) => {
// Create a wallet using the private key
const wallet = new ethers.Wallet(privateKey, getProvider());
// Contract interface
const tokenContract = new ethers.Contract(CONTRACT_ADDRESSES[token] || contractAddress, ERC20ABI, wallet);
// Convert the amount to the smallest unit of USDC (wei)
const amountWei = ethers.utils.parseUnits(amount.toString(), 6); // Assuming 6 decimals for USDC
// Call the transfer function on the USDC contract
// Convert amount to smallest unit (both USDC and USDT use 6 decimals)
const amountWei = ethers.utils.parseUnits(amount.toString(), 6);
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 tokenTransfers = tokenTxData.status === '1' ? tokenTxData.result : [];
// Combine and sort transactions
const allTransactions = [...normalTxData.result, ...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 tokenTransfer = null;
if (receipt && receipt.logs.length > 0) {
// Try to decode ERC20 Transfer event
const transferEventSignature = ethers.utils.id('Transfer(address,address,uint256)');
const transferLog = receipt.logs.find(log => log.topics[0] === transferEventSignature);
if (transferLog) {
try {
const tokenContract = new ethers.Contract(transferLog.address, ERC20ABI, provider);
const [symbol, decimals] = await Promise.all([
tokenContract.symbol().catch(() => 'TOKEN'),
tokenContract.decimals().catch(() => 18)
]);
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));
tokenTransfer = {
from,
to,
value,
symbol,
contractAddress: transferLog.address
};
} catch (e) {
console.warn('Could not decode token transfer:', 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,
tokenTransfer: tokenTransfer,
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 = {});