Compare commits
12 Commits
fetch_all_
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 54831c71bc | |||
| 5d1882e6a7 | |||
| b121b1aec7 | |||
| 67309e9273 | |||
| 212fa5047a | |||
|
|
0e03af78ef | ||
| 1a956912ab | |||
| 8d0f9db29b | |||
| ae8e519f73 | |||
| bae54354fc | |||
| 5a30567825 | |||
| e2f6e1dcab |
2
.github/workflows/push-dappbundle.yml
vendored
2
.github/workflows/push-dappbundle.yml
vendored
@ -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 && 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/${{ 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"
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
147
css/main.css
147
css/main.css
@ -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,101 @@ 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 */
|
||||||
|
}
|
||||||
|
|
||||||
#error_section {
|
#error_section {
|
||||||
display: grid;
|
display: grid;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@ -1171,4 +1274,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;
|
||||||
|
}
|
||||||
|
|||||||
132
css/main.scss
132
css/main.scss
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
1206
index.html
1206
index.html
File diff suppressed because it is too large
Load Diff
@ -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,269 @@
|
|||||||
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 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 = {});
|
})('object' === typeof module ? module.exports : window.ethOperator = {});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user