Compare commits

...

10 Commits

4 changed files with 491 additions and 26 deletions

32
.github/workflows/push-dappbundle.yml vendored Normal file
View File

@ -0,0 +1,32 @@
name: Workflow push to Dappbundle
on: [push]
jobs:
build:
name: Build
runs-on: self-hosted
steps:
- name: Executing remote command
uses: appleboy/ssh-action@v1.0.0
with:
host: ${{ secrets.R_HOST }}
username: ${{ secrets.P_USERNAME }}
password: ${{ secrets.P_PASSWORD }}
port: ${{ secrets.SSH_PORT }}
script: |
if [ -d "${{ secrets.DEPLOYMENT_LOCATION}}/dappbundle" ]; then
echo "Folder exists. Skipping Git clone."
else
echo "Folder does not exist. Cloning repository..."
cd ${{ secrets.DEPLOYMENT_LOCATION}}/ && git clone https://github.com/ranchimall/dappbundle.git
fi
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
echo "Cloning repository..."
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://saketongit:${{ secrets.RM_ACCESS_TOKEN }}@github.com/ranchimall/dappbundle.git"

249
README.md Normal file
View File

@ -0,0 +1,249 @@
# Avax Web Wallet - Technical Documentation
## Overview
The Avalanche Multi-Chain Wallet is a web-based cryptocurrency wallet that supports multiple blockchain networks including Avalanche (AVAX), FLO, and Bitcoin (BTC). The wallet provides comprehensive functionality for address generation, transaction management, balance checking, and transaction history viewing.
### Key Features
- **Multi-Chain Support**: AVAX, FLO, and BTC address generation from a single private key
- **Transaction History**: Paginated transaction viewing with smart caching
- **Address Search**: Persistent search history with IndexedDB storage
- **URL Sharing**: Direct link sharing for addresses and transaction hashes
- **Real-Time Data**: Live balance updates and transaction status checking
- **Responsive Design**: Mobile-first responsive interface
## Architecture
### System Architecture
```
┌────────────────────────────────────────────────────────────┐
│ Frontend Layer │
├────────────────────────────────────────────────────────────┤
│ index.html │ style.css │ JavaScript Modules │
├──────────────┼─────────────┼───────────────────────────────┤
│ │ │ • avaxCrypto.js │
│ │ │ • avaxBlockchainAPI.js │
│ │ │ • avaxSearchDB.js │
│ │ │ • lib.avax.js │
└──────────────┴─────────────┴───────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Storage Layer │
├─────────────────────────────────────────────────────────────┤
│ IndexedDB │ LocalStorage │ Session Storage │
│ • Address History │ • Theme Prefs │ • Temp Data │
│ • Search Cache │ • User Settings │ • Form State │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Blockchain Layer │
├─────────────────────────────────────────────────────────────┤
│ AVAX C-Chain │ FLO Network │ Bitcoin Network │
│ • RPC Endpoints │ • Address Gen │ • Address Gen │
│ • Transaction │ • Key Derivation│ • Key Derivation │
│ • Balance Query │ │ │
└─────────────────────────────────────────────────────────────┘
```
## Core Components
### 1. Cryptographic Engine (`avaxCrypto.js`)
The cryptographic engine handles multi-chain address generation and key management.
#### Key Functions
```javascript
// Generate multi-chain addresses from private key
async generateMultiChain(privateKey = null)
// Create new random wallet
generateNewID()
// Hash generation utilities
hashID(str)
tmpID()
```
#### Supported Private Key Formats
- **AVAX**: 64-character hexadecimal
- **FLO**: Base58 format starting with 'R'
- **BTC**: WIF format starting with 'K', or 'L'
### 2. Blockchain API Layer (`avaxBlockchainAPI.js`)
Handles all blockchain interactions and RPC communications.
#### Core Functions
```javascript
// Balance retrieval
async getBalanceRPC(address)
// Transaction history with pagination
async fetchAvalancheTxHistory(address, page, limit)
// Transaction preparation for sending
async prepareAvalancheTransaction(privateKey, to, amount)
// Utility functions
weiToAvax(weiAmount)
formatTransactionTime(timestamp)
```
#### RPC Configuration
```javascript
const RPC_ENDPOINTS = "https://go.getblock.io/.../ext/bc/C/rpc";
```
### 3. Data Persistence (`avaxSearchDB.js`)
IndexedDB wrapper for persistent storage of searched addresses and metadata.
#### Database Schema
```sql
-- Object Store: searchedAddresses
{
address: string (Primary Key),
balance: number,
timestamp: number (Indexed),
sourceInfo: {
originalPrivateKey: string,
originalAddress: string,
blockchain: string,
derivedAvaxAddress: string
} | null
}
```
#### API Methods
```javascript
class SearchedAddressDB {
async init()
async saveSearchedAddress(address, balance, timestamp, sourceInfo)
async getSearchedAddresses()
async deleteSearchedAddress(address)
async clearAllSearchedAddresses()
}
```
## API Reference
### Wallet Generation
#### `generateWallet()`
Generates a new multi-chain wallet with random private keys.
**Returns:** Promise resolving to wallet object
```javascript
{
AVAX: { address: string, privateKey: string },
FLO: { address: string, privateKey: string },
BTC: { address: string, privateKey: string }
}
```
### Address Recovery
#### `recoverWallet()`
Recovers wallet addresses from an existing private key.
**Parameters:**
- `privateKey` (string): Valid AVAX/FLO/BTC private key
**Returns:** Promise resolving to wallet object (same structure as generateWallet)
### Transaction Management
#### `loadTransactions()`
Loads transaction history for a given address with smart pagination.
**Process Flow:**
1. Input validation (address/private key)
2. Address derivation (if private key provided)
3. Balance retrieval
4. Transaction history fetching (50 transactions initially)
5. Pagination setup (3-5 pages based on transaction count)
6. UI updates and data persistence
#### `sendTransaction()`
Prepares and broadcasts a transaction to the Avalanche network.
**Parameters:**
- `privateKey` (string): Sender's private key
- `recipientAddress` (string): Recipient's AVAX address
- `amount` (string): Amount in AVAX
**Process:**
```
Input Validation → Gas Estimation → User Confirmation → Transaction Signing → Broadcast
```
### Search Functionality
#### `handleSearch()`
Unified search handler supporting both address and transaction hash lookup.
**Search Types:**
- `address`: Loads balance and transaction history
- `hash`: Retrieves transaction details from blockchain
#### URL Parameter Support
- `?address=0x...` - Direct address loading
- `?hash=0x...` - Direct transaction hash loading
## Security Features
### Private Key Handling
- **No Storage**: Private keys are never stored in any form
- **Memory Clearing**: Variables containing keys are nullified after use
- **Input Validation**: Strict format validation before processing
- **Error Handling**: Secure error messages without key exposure
### URL Security
- **Address-Only URLs**: Only public addresses included in shareable URLs
- **No Private Data**: Private keys never included in URL parameters
- **State Management**: Secure browser history handling
## Performance Optimizations
### Smart Pagination
```javascript
// Initial load: Fetch 50 transactions
// Analyze count to determine pages (3-5)
// Cache data for instant navigation
// Lazy load additional pages on demand
const isFirstLoad = currentPage === 1 && allTransactions.length === 0;
const fetchCount = isFirstLoad ? 50 : 10;
```
### Caching Strategy
- **Transaction Cache**: Store 50+ transactions for fast pagination
- **Balance Cache**: Cache balance data in IndexedDB
- **Address History**: Persistent search history with timestamps
### UI Optimizations
- **Lazy Loading**: Progressive content loading
- **Debounced Inputs**: Prevent excessive API calls
- **Responsive Images**: Optimized for mobile devices
- **CSS Grid/Flexbox**: Efficient layout rendering
### File Structure
```
avalanche-wallet/
├── index.html # Main application
├── style.css # Stylesheet
├── avaxCrypto.js # Cryptographic functions
├── avaxBlockchainAPI.js # Blockchain integration
├── avaxSearchDB.js # Data persistence
├── lib.avax.js # External libraries
```

View File

@ -149,18 +149,33 @@ async function prepareAvalancheTransaction(
// Check sender balance
const balance = await getBalanceRPC(senderAddress);
if (parseFloat(balance.avax) < parseFloat(amountInAvax)) {
const balanceAvax = parseFloat(balance.avax);
const amount = parseFloat(amountInAvax);
// Get current gas price and calculate fee
const gasPrice = await getGasPrice();
const gasLimit = 21000; // Standard gas limit for a simple AVAX transfer
const gasPriceBN = ethers.BigNumber.from(gasPrice);
const gasLimitBN = ethers.BigNumber.from(gasLimit);
const gasFee = parseFloat(
ethers.utils.formatEther(gasPriceBN.mul(gasLimitBN))
);
// Check if balance is sufficient for amount + gas fee
if (balanceAvax < amount + gasFee) {
throw new Error(
`Insufficient balance. You have ${balance.avax} AVAX but trying to send ${amountInAvax} AVAX`
`Insufficient balance for transaction. You have ${
balance.avax
} AVAX but need ${
amount + gasFee
} AVAX (amount + gas fee). Please lower the amount to proceed.`
);
}
// Get transaction count (nonce)
const nonce = await getTransactionCount(senderAddress);
// Get current gas price
const gasPrice = await getGasPrice();
// Return prepared transaction data
return {
senderAddress: senderAddress,
@ -168,7 +183,7 @@ async function prepareAvalancheTransaction(
amount: amountInAvax,
nonce: nonce,
gasPrice: gasPrice,
gasLimit: 21000,
gasLimit: gasLimit,
chainId: 43114,
balance: balance.avax,
cleanPrivateKey: privateKey,

View File

@ -266,8 +266,10 @@
id="sendAmount"
class="form-input"
placeholder="Enter the amount to send"
min="0.0001"
step="0.000001"
min="0.000000001"
step="0.0000000001"
oninvalid="this.setCustomValidity('Value must be greater than or equal to 0.000000001')"
oninput="this.setCustomValidity('')"
required
/>
<button
@ -1022,7 +1024,14 @@
button.innerHTML = originalHTML;
}
}
// Clear transaction output when user starts typing a new transaction
const sendFormFields = ["privateKey", "recipientAddress", "sendAmount"];
sendFormFields.forEach(fieldId => {
document.getElementById(fieldId).addEventListener("input", () => {
document.getElementById("sendOutput").innerHTML = "";
});
});
// Send Transaction functionality
document
.getElementById("sendForm")
@ -1374,6 +1383,7 @@
updateURLWithAddress(input);
} else if (searchType === "hash") {
await lookupTransactionHash(input);
updateURLWithHash(input);
}
}
@ -1382,20 +1392,65 @@
if (!address) return;
const url = new URL(window.location);
url.searchParams.delete("hash");
url.searchParams.set("address", address);
window.history.pushState({ address: address }, "", url);
}
// Update URL with hash
function updateURLWithHash(hash) {
if (!hash) return;
const url = new URL(window.location);
url.searchParams.delete("address");
url.searchParams.set("hash", hash);
window.history.pushState({ hash: hash }, "", url);
}
// Get address from URL parameters
function getAddressFromURL() {
const urlParams = new URLSearchParams(window.location.search);
return urlParams.get("address");
}
// Get hash from URL parameters
function getHashFromURL() {
const urlParams = new URLSearchParams(window.location.search);
return urlParams.get("hash");
}
// Load address from URL on page load
async function loadAddressFromURL() {
const address = getAddressFromURL();
if (address) {
const hash = getHashFromURL();
if (hash) {
// If hash parameter exists, load transaction hash
showPage("transactions");
await new Promise((resolve) => setTimeout(resolve, 100));
const hashRadio = document.querySelector(
'input[name="searchType"][value="hash"]'
);
if (hashRadio) {
hashRadio.checked = true;
document
.querySelectorAll(".radio-button-container")
.forEach((c) => c.classList.remove("active"));
hashRadio
.closest(".radio-button-container")
.classList.add("active");
updateSearchInterface("hash");
}
document.getElementById("transactionInput").value = hash;
await handleSearch();
} else if (address) {
// If address parameter exists, load address
showPage("transactions");
await new Promise((resolve) => setTimeout(resolve, 100));
@ -1405,6 +1460,14 @@
);
if (addressRadio) {
addressRadio.checked = true;
document
.querySelectorAll(".radio-button-container")
.forEach((c) => c.classList.remove("active"));
addressRadio
.closest(".radio-button-container")
.classList.add("active");
updateSearchInterface("address");
}
@ -1415,7 +1478,33 @@
}
window.addEventListener("popstate", async function (event) {
if (event.state && event.state.address) {
if (event.state && event.state.hash) {
// Handle transaction hash navigation
showPage("transactions");
await new Promise((resolve) => setTimeout(resolve, 100));
const hashRadio = document.querySelector(
'input[name="searchType"][value="hash"]'
);
if (hashRadio) {
hashRadio.checked = true;
document
.querySelectorAll(".radio-button-container")
.forEach((c) => c.classList.remove("active"));
hashRadio
.closest(".radio-button-container")
.classList.add("active");
updateSearchInterface("hash");
}
document.getElementById("transactionInput").value = event.state.hash;
await lookupTransactionHash(event.state.hash);
} else if (event.state && event.state.address) {
// Handle address navigation
showPage("transactions");
await new Promise((resolve) => setTimeout(resolve, 100));
@ -1425,6 +1514,14 @@
);
if (addressRadio) {
addressRadio.checked = true;
document
.querySelectorAll(".radio-button-container")
.forEach((c) => c.classList.remove("active"));
addressRadio
.closest(".radio-button-container")
.classList.add("active");
updateSearchInterface("address");
}
@ -1458,19 +1555,36 @@
"Invalid transaction hash format. Hash should be 0x followed by 64 hexadecimal characters."
);
}
const AVAX_RPC_URL = "https://api.avax.network/ext/bc/C/rpc";
// Fetch transaction details from AVAX RPC
const provider = new ethers.providers.JsonRpcProvider(AVAX_RPC_URL);
const tx = await provider.getTransaction(hash);
const receipt = await provider.getTransactionReceipt(hash);
if (!tx) {
throw new Error("Transaction not found on the blockchain.");
}
// Generate SnowTrace explorer URL
const explorerUrl = `https://snowtrace.io/tx/${hash}`;
// Display hash lookup result
const value = ethers.utils.formatEther(tx.value || "0");
const gasUsed = receipt ? receipt.gasUsed.toString() : "Pending";
const status = receipt
? receipt.status === 1
? '<span class="tx-status confirmed">Confirmed</span>'
: '<span class="tx-status failed">Failed</span>'
: '<span class="tx-status" style="background-color: rgba(255, 193, 7, 0.1); color: #ffc107;">Pending</span>';
const blockNumber = tx.blockNumber || "Pending";
output.innerHTML = `
<div class="wallet-generated-success">
<div class="success-icon">
<i class="fas fa-fingerprint"></i>
<i class="fas fa-check-circle"></i>
</div>
<div class="success-message">
<h3>Transaction Hash Found</h3>
<p>View the transaction details on SnowTrace explorer.</p>
<h3>Transaction Found</h3>
<p>Transaction details retrieved successfully.</p>
</div>
</div>
@ -1483,32 +1597,81 @@
<div class="detail-row">
<label><i class="fas fa-fingerprint"></i> Transaction Hash</label>
<div class="value-container">
<code>${hash}</code>
<code style="font-size: 0.85rem; word-break: break-all;">${hash}</code>
<button class="btn-icon" onclick="copyToClipboard('${hash}')" title="Copy hash">
<i class="fas fa-copy"></i>
</button>
</div>
</div>
<div class="detail-row">
<label><i class="fas fa-info-circle"></i> Status</label>
<div class="value-container">
${status}
</div>
</div>
<div class="detail-row">
<label><i class="fas fa-cube"></i> Block Number</label>
<div class="value-container">
<code>${blockNumber}</code>
</div>
</div>
<div class="detail-row">
<label><i class="fas fa-paper-plane"></i> From</label>
<div class="value-container">
<code style="font-size: 0.85rem; word-break: break-all;">${
tx.from
}</code>
<button class="btn-icon" onclick="copyToClipboard('${
tx.from
}')" title="Copy address">
<i class="fas fa-copy"></i>
</button>
</div>
</div>
<div class="detail-row">
<label><i class="fas fa-external-link-alt"></i> Explorer</label>
<label><i class="fas fa-inbox"></i> To</label>
<div class="value-container">
<a href="${explorerUrl}" target="_blank" style="color: var(--primary-color); text-decoration: underline;">
View on SnowTrace Explorer
</a>
<code style="font-size: 0.85rem; word-break: break-all;">${
tx.to || "Contract Creation"
}</code>
${
tx.to
? `<button class="btn-icon" onclick="copyToClipboard('${tx.to}')" title="Copy address">
<i class="fas fa-copy"></i>
</button>`
: ""
}
</div>
</div>
<div style="margin-top: 1rem;">
<div class="detail-row">
<label><i class="fas fa-coins"></i> Value</label>
<div class="value-container">
<span style="font-weight: 600;">${value} AVAX</span>
</div>
</div>
<div class="detail-row">
<label><i class="fas fa-fire"></i> Gas Used</label>
<div class="value-container">
<code>${gasUsed}</code>
</div>
</div>
<div style="margin-top: 1.5rem;">
<button class="btn btn-primary btn-block" onclick="window.open('${explorerUrl}', '_blank')">
<i class="fas fa-external-link-alt"></i>
Open in SnowTrace
View on SnowTrace Explorer
</button>
</div>
</div>
`;
showNotification("Transaction hash found!", "success");
showNotification("Transaction details loaded!", "success");
} catch (error) {
output.innerHTML = `
<div class="error-state">
@ -1516,12 +1679,12 @@
<i class="fas fa-exclamation-triangle"></i>
</div>
<div class="error-message">
<h3>Invalid Transaction Hash</h3>
<h3>Error Loading Transaction</h3>
<p>${error.message}</p>
</div>
</div>
`;
showNotification("Invalid transaction hash", "error");
showNotification("Failed to load transaction", "error");
} finally {
button.disabled = false;
button.innerHTML = originalHTML;
@ -2029,7 +2192,13 @@
const list = document.getElementById("searchedAddressesList");
if (!container || !list) return;
const searchType = document.querySelector(
'input[name="searchType"]:checked'
)?.value;
if (searchType === "hash") {
container.style.display = "none";
return;
}
if (addresses.length === 0) {
container.style.display = "none";
return;