| … | … | @@ -2015,3 +2015,116 @@ footer a:hover { |
| 2015 | 2015 | border-color: var(--accent-blue); |
| 2016 | 2016 | box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); |
| 2017 | 2017 | } |
| 2018 | ||
| 2019 | /* Unified Balance Display (Ethereum with tokens) */ | |
| 2020 | .unified-balance { | |
| 2021 | padding: 1rem; | |
| 2022 | } | |
| 2023 | ||
| 2024 | .total-value { | |
| 2025 | text-align: center; | |
| 2026 | margin-bottom: 1.5rem; | |
| 2027 | } | |
| 2028 | ||
| 2029 | .total-usd { | |
| 2030 | font-size: 2rem; | |
| 2031 | font-weight: bold; | |
| 2032 | display: block; | |
| 2033 | } | |
| 2034 | ||
| 2035 | .total-label { | |
| 2036 | color: var(--text-secondary); | |
| 2037 | font-size: 0.875rem; | |
| 2038 | } | |
| 2039 | ||
| 2040 | .asset-list { | |
| 2041 | display: flex; | |
| 2042 | flex-direction: column; | |
| 2043 | gap: 0.5rem; | |
| 2044 | } | |
| 2045 | ||
| 2046 | .asset-row { | |
| 2047 | display: grid; | |
| 2048 | grid-template-columns: 2rem 4rem 1fr auto; | |
| 2049 | align-items: center; | |
| 2050 | padding: 0.75rem; | |
| 2051 | border-radius: 8px; | |
| 2052 | cursor: pointer; | |
| 2053 | transition: background 0.2s; | |
| 2054 | } | |
| 2055 | ||
| 2056 | .asset-row:hover { | |
| 2057 | background: var(--bg-hover); | |
| 2058 | } | |
| 2059 | ||
| 2060 | .asset-row.selected { | |
| 2061 | background: var(--bg-selected, var(--bg-hover)); | |
| 2062 | border: 1px solid var(--accent-green); | |
| 2063 | } | |
| 2064 | ||
| 2065 | .asset-icon { | |
| 2066 | font-size: 1.25rem; | |
| 2067 | } | |
| 2068 | ||
| 2069 | .asset-name { | |
| 2070 | font-weight: 500; | |
| 2071 | } | |
| 2072 | ||
| 2073 | .asset-balance { | |
| 2074 | text-align: right; | |
| 2075 | font-family: monospace; | |
| 2076 | } | |
| 2077 | ||
| 2078 | .asset-usd { | |
| 2079 | text-align: right; | |
| 2080 | color: var(--text-secondary); | |
| 2081 | min-width: 80px; | |
| 2082 | } | |
| 2083 | ||
| 2084 | .gas-warning { | |
| 2085 | margin-top: 1rem; | |
| 2086 | padding: 0.75rem; | |
| 2087 | background: rgba(255, 193, 7, 0.1); | |
| 2088 | border: 1px solid #ffc107; | |
| 2089 | border-radius: 8px; | |
| 2090 | color: #ffc107; | |
| 2091 | text-align: center; | |
| 2092 | } | |
| 2093 | ||
| 2094 | .gas-info { | |
| 2095 | display: flex; | |
| 2096 | justify-content: space-between; | |
| 2097 | padding: 0.5rem; | |
| 2098 | background: var(--bg-secondary); | |
| 2099 | border-radius: 4px; | |
| 2100 | font-size: 0.875rem; | |
| 2101 | } | |
| 2102 | ||
| 2103 | .gas-info .warning { | |
| 2104 | color: #ffc107; | |
| 2105 | } | |
| 2106 | ||
| 2107 | #send-asset-selector { | |
| 2108 | margin-bottom: 1rem; | |
| 2109 | } | |
| 2110 | ||
| 2111 | #send-asset-selector label { | |
| 2112 | display: block; | |
| 2113 | margin-bottom: 0.5rem; | |
| 2114 | font-weight: 600; | |
| 2115 | color: var(--text-secondary); | |
| 2116 | } | |
| 2117 | ||
| 2118 | #send-asset-selector select { | |
| 2119 | width: 100%; | |
| 2120 | padding: 0.75rem; | |
| 2121 | background: var(--bg-secondary); | |
| 2122 | border: 1px solid var(--border-color); | |
| 2123 | border-radius: 6px; | |
| 2124 | color: var(--text-primary); | |
| 2125 | font-size: 1rem; | |
| 2126 | } | |
| 2127 | ||
| 2128 | #send-gas-info { | |
| 2129 | margin-top: 0.5rem; | |
| 2130 | } |
| … | … | @@ -1276,16 +1276,26 @@ async function sendToContactAddress(contactId, addressId) { |
| 1276 | 1276 | // Close contacts modal |
| 1277 | 1277 | closeContacts() |
| 1278 | 1278 | |
| 1279 | // Set currency to match chain | |
| 1280 | selectedCurrency = chain | |
| 1281 | localStorage.setItem('currency', chain) | |
| 1282 | document.getElementById('currency-selector').value = chain | |
| 1279 | // Set chain and asset to match contact address | |
| 1280 | // Handle token chains like 'ethereum-usdt' -> chain='ethereum', asset='usdt' | |
| 1281 | if (chain.includes('-')) { | |
| 1282 | const parts = chain.split('-') | |
| 1283 | selectedChain = parts[0] | |
| 1284 | selectedAsset = parts[1] | |
| 1285 | } else { | |
| 1286 | selectedChain = chain | |
| 1287 | selectedAsset = null | |
| 1288 | } | |
| 1289 | localStorage.setItem('chain', selectedChain) | |
| 1290 | localStorage.setItem('asset', selectedAsset || '') | |
| 1291 | selectedCurrency = getSelectedCurrency() | |
| 1292 | document.getElementById('currency-selector').value = selectedChain | |
| 1283 | 1293 | |
| 1284 | 1294 | // Update wallet view |
| 1285 | 1295 | updateWalletView() |
| 1286 | 1296 | |
| 1287 | 1297 | // Show send form |
| 1288 | await showSend(chain) | |
| 1298 | await showSend(selectedCurrency) | |
| 1289 | 1299 | |
| 1290 | 1300 | // Pre-fill address |
| 1291 | 1301 | setTimeout(() => { |
| … | … | @@ -1456,8 +1466,10 @@ const SUPPORTED_CHAINS = { |
| 1456 | 1466 | testnet: /^(tb1|[mn])[a-zA-HJ-NP-Z0-9]{25,62}$/, |
| 1457 | 1467 | mainnet: /^(bc1|[13])[a-zA-HJ-NP-Z0-9]{25,62}$/ |
| 1458 | 1468 | }, |
| 1459 | feeReserve: 500 | |
| 1469 | feeReserve: 500, | |
| 1470 | tokens: null | |
| 1460 | 1471 | }, |
| 1472 | ||
| 1461 | 1473 | ethereum: { |
| 1462 | 1474 | name: 'Ethereum', |
| 1463 | 1475 | icon: 'Ξ', |
| … | … | @@ -1465,7 +1477,7 @@ const SUPPORTED_CHAINS = { |
| 1465 | 1477 | mainnetName: 'Mainnet', |
| 1466 | 1478 | formatBalance: (balance) => { |
| 1467 | 1479 | const eth = Number(balance) / 1e18 |
| 1468 | return `${eth.toFixed(18).replace(/\.?0+$/, '')} ETH` | |
| 1480 | return `${eth.toFixed(6)} ETH` | |
| 1469 | 1481 | }, |
| 1470 | 1482 | formatSmallUnit: (balance) => { |
| 1471 | 1483 | return `${Number(balance).toLocaleString()} wei` |
| … | … | @@ -1475,8 +1487,28 @@ const SUPPORTED_CHAINS = { |
| 1475 | 1487 | testnet: /^0x[a-fA-F0-9]{40}$/, |
| 1476 | 1488 | mainnet: /^0x[a-fA-F0-9]{40}$/ |
| 1477 | 1489 | }, |
| 1478 | feeReserve: 100000 | |
| 1490 | feeReserve: 100000, | |
| 1491 | tokens: { | |
| 1492 | usdt: { | |
| 1493 | symbol: 'USDT', | |
| 1494 | name: 'Tether USD', | |
| 1495 | icon: '₮', | |
| 1496 | decimals: 6, | |
| 1497 | priceId: 'tether', | |
| 1498 | formatBalance: (balance) => `${(Number(balance) / 1e6).toFixed(2)} USDT` | |
| 1499 | }, | |
| 1500 | usdc: { | |
| 1501 | symbol: 'USDC', | |
| 1502 | name: 'USD Coin', | |
| 1503 | icon: '$', | |
| 1504 | decimals: 6, | |
| 1505 | priceId: 'usd-coin', | |
| 1506 | formatBalance: (balance) => `${(Number(balance) / 1e6).toFixed(2)} USDC` | |
| 1507 | } | |
| 1508 | } | |
| 1479 | 1509 | }, |
| 1510 | ||
| 1511 | // Backwards compatibility: Keep flat entries for existing code paths | |
| 1480 | 1512 | 'ethereum-usdt': { |
| 1481 | 1513 | name: 'Ethereum - USDT', |
| 1482 | 1514 | icon: '₮', |
| … | … | @@ -1498,22 +1530,22 @@ const SUPPORTED_CHAINS = { |
| 1498 | 1530 | isToken: true, |
| 1499 | 1531 | baseChain: 'ethereum' |
| 1500 | 1532 | } |
| 1501 | // TODO: Add more chains when WDK support is available | |
| 1502 | // These EVM chains can be added by registering WalletManagerEvm with different RPC endpoints: | |
| 1503 | // - polygon (Polygon/MATIC) | |
| 1504 | // - bsc (BNB Chain) | |
| 1505 | // - arbitrum (Arbitrum One) | |
| 1506 | // - optimism (Optimism) | |
| 1507 | // - avalanche (Avalanche C-Chain) | |
| 1508 | // | |
| 1509 | // Future non-EVM chains require new WDK wallet implementations: | |
| 1510 | // - solana (wdk-wallet-solana) | |
| 1511 | // - cardano (wdk-wallet-cardano) | |
| 1512 | // - etc. | |
| 1513 | 1533 | } |
| 1514 | 1534 | |
| 1515 | // Currency state management | |
| 1516 | let selectedCurrency = localStorage.getItem('currency') || 'bitcoin' | |
| 1535 | // Chain and asset state management | |
| 1536 | let selectedChain = localStorage.getItem('chain') || 'bitcoin' | |
| 1537 | let selectedAsset = localStorage.getItem('asset') || null // null = native, 'usdt' = token | |
| 1538 | ||
| 1539 | // Backwards compatibility: selectedCurrency derived from selectedChain + selectedAsset | |
| 1540 | function getSelectedCurrency() { | |
| 1541 | if (selectedAsset && selectedChain === 'ethereum') { | |
| 1542 | return `ethereum-${selectedAsset}` | |
| 1543 | } | |
| 1544 | return selectedChain | |
| 1545 | } | |
| 1546 | ||
| 1547 | // Legacy support: set selectedCurrency getter | |
| 1548 | let selectedCurrency = getSelectedCurrency() | |
| 1517 | 1549 | |
| 1518 | 1550 | // Price cache (keyed by CoinGecko ID) |
| 1519 | 1551 | let prices = {} |
| … | … | @@ -1554,21 +1586,33 @@ function updateNetworkLabels() { |
| 1554 | 1586 | updateWalletView() |
| 1555 | 1587 | } |
| 1556 | 1588 | |
| 1557 | // Switch currency | |
| 1589 | // Switch currency (chain selector) | |
| 1558 | 1590 | function switchCurrency() { |
| 1559 | 1591 | const selector = document.getElementById('currency-selector') |
| 1560 | selectedCurrency = selector.value | |
| 1561 | localStorage.setItem('currency', selectedCurrency) | |
| 1592 | selectedChain = selector.value | |
| 1593 | selectedAsset = null // Reset asset when switching chains | |
| 1594 | localStorage.setItem('chain', selectedChain) | |
| 1595 | localStorage.setItem('asset', '') | |
| 1596 | ||
| 1597 | // Update selectedCurrency for backwards compatibility | |
| 1598 | selectedCurrency = getSelectedCurrency() | |
| 1562 | 1599 | |
| 1563 | 1600 | updateWalletView() |
| 1564 | loadCurrentBalance() | |
| 1601 | ||
| 1602 | // Use unified balance display for Ethereum, standard for others | |
| 1603 | if (selectedChain === 'ethereum') { | |
| 1604 | loadEthereumBalances() | |
| 1605 | } else { | |
| 1606 | loadCurrentBalance() | |
| 1607 | } | |
| 1608 | ||
| 1565 | 1609 | loadCurrentQRCode() |
| 1566 | 1610 | loadTransactions() |
| 1567 | 1611 | } |
| 1568 | 1612 | |
| 1569 | // Update wallet view for selected currency | |
| 1613 | // Update wallet view for selected chain | |
| 1570 | 1614 | function updateWalletView() { |
| 1571 | const chain = SUPPORTED_CHAINS[selectedCurrency] | |
| 1615 | const chain = SUPPORTED_CHAINS[selectedChain] | |
| 1572 | 1616 | if (chain) { |
| 1573 | 1617 | const networkName = selectedNetwork === 'testnet' ? chain.testnetName : chain.mainnetName |
| 1574 | 1618 | document.getElementById('wallet-icon').textContent = chain.icon |
| … | … | @@ -1699,6 +1743,97 @@ async function loadCurrentBalance() { |
| 1699 | 1743 | } |
| 1700 | 1744 | } |
| 1701 | 1745 | |
| 1746 | // Load unified Ethereum balances (ETH + USDT + USDC) | |
| 1747 | async function loadEthereumBalances() { | |
| 1748 | const container = document.getElementById('wallet-balance-display') | |
| 1749 | container.innerHTML = '<div class="spinner"></div>' | |
| 1750 | ||
| 1751 | try { | |
| 1752 | const res = await authFetch(`/api/balances/ethereum`) | |
| 1753 | const data = await res.json() | |
| 1754 | ||
| 1755 | if (data.error) { | |
| 1756 | container.innerHTML = `<span class="error">${data.error}</span>` | |
| 1757 | return | |
| 1758 | } | |
| 1759 | ||
| 1760 | const { balances } = data | |
| 1761 | ||
| 1762 | const ethAmount = Number(balances.eth.balance) / 1e18 | |
| 1763 | const usdtAmount = Number(balances.usdt.balance) / 1e6 | |
| 1764 | const usdcAmount = Number(balances.usdc.balance) / 1e6 | |
| 1765 | ||
| 1766 | const ethUsd = ethAmount * (prices.ethereum || 0) | |
| 1767 | const usdtUsd = usdtAmount * (prices.tether || 1) | |
| 1768 | const usdcUsd = usdcAmount * (prices['usd-coin'] || 1) | |
| 1769 | const totalUsd = ethUsd + usdtUsd + usdcUsd | |
| 1770 | ||
| 1771 | container.innerHTML = ` | |
| 1772 | <div class="unified-balance"> | |
| 1773 | <div class="total-value"> | |
| 1774 | <span class="total-usd">${formatUSD(totalUsd)}</span> | |
| 1775 | <span class="total-label">Total Balance</span> | |
| 1776 | </div> | |
| 1777 | ||
| 1778 | <div class="asset-list"> | |
| 1779 | <div class="asset-row ${selectedAsset === null ? 'selected' : ''}" | |
| 1780 | onclick="selectAsset(null)"> | |
| 1781 | <span class="asset-icon">Ξ</span> | |
| 1782 | <span class="asset-name">ETH</span> | |
| 1783 | <span class="asset-balance">${ethAmount.toFixed(6)}</span> | |
| 1784 | <span class="asset-usd">${formatUSD(ethUsd)}</span> | |
| 1785 | </div> | |
| 1786 | ||
| 1787 | <div class="asset-row ${selectedAsset === 'usdt' ? 'selected' : ''}" | |
| 1788 | onclick="selectAsset('usdt')"> | |
| 1789 | <span class="asset-icon">₮</span> | |
| 1790 | <span class="asset-name">USDT</span> | |
| 1791 | <span class="asset-balance">${usdtAmount.toFixed(2)}</span> | |
| 1792 | <span class="asset-usd">${formatUSD(usdtUsd)}</span> | |
| 1793 | </div> | |
| 1794 | ||
| 1795 | <div class="asset-row ${selectedAsset === 'usdc' ? 'selected' : ''}" | |
| 1796 | onclick="selectAsset('usdc')"> | |
| 1797 | <span class="asset-icon">$</span> | |
| 1798 | <span class="asset-name">USDC</span> | |
| 1799 | <span class="asset-balance">${usdcAmount.toFixed(2)}</span> | |
| 1800 | <span class="asset-usd">${formatUSD(usdcUsd)}</span> | |
| 1801 | </div> | |
| 1802 | </div> | |
| 1803 | ||
| 1804 | ${ethAmount < 0.001 ? ` | |
| 1805 | <div class="gas-warning"> | |
| 1806 | ⚠️ Low ETH balance - needed for gas fees | |
| 1807 | </div> | |
| 1808 | ` : ''} | |
| 1809 | </div> | |
| 1810 | ` | |
| 1811 | } catch (err) { | |
| 1812 | console.error('Load balances error:', err) | |
| 1813 | container.innerHTML = '<span class="error">Failed to load balances</span>' | |
| 1814 | } | |
| 1815 | } | |
| 1816 | ||
| 1817 | // Select asset for Ethereum chain (null = ETH, 'usdt' = USDT, 'usdc' = USDC) | |
| 1818 | function selectAsset(asset) { | |
| 1819 | selectedAsset = asset | |
| 1820 | localStorage.setItem('asset', asset || '') | |
| 1821 | ||
| 1822 | // Update selectedCurrency for backwards compatibility | |
| 1823 | selectedCurrency = getSelectedCurrency() | |
| 1824 | ||
| 1825 | document.querySelectorAll('.asset-row').forEach(row => { | |
| 1826 | row.classList.remove('selected') | |
| 1827 | }) | |
| 1828 | if (event && event.currentTarget) { | |
| 1829 | event.currentTarget.classList.add('selected') | |
| 1830 | } | |
| 1831 | ||
| 1832 | // Update QR code and transactions for selected asset | |
| 1833 | loadCurrentQRCode() | |
| 1834 | loadTransactions() | |
| 1835 | } | |
| 1836 | ||
| 1702 | 1837 | // Load QR code for current currency |
| 1703 | 1838 | async function loadCurrentQRCode() { |
| 1704 | 1839 | const qrContainer = document.getElementById('wallet-qr-container') |
| … | … | @@ -3416,16 +3551,19 @@ async function init() { |
| 3416 | 3551 | // Clear existing options |
| 3417 | 3552 | currencySelector.innerHTML = '' |
| 3418 | 3553 | |
| 3419 | // Add option for each supported chain | |
| 3554 | // Add option for each base chain (not tokens) | |
| 3420 | 3555 | for (const [chainId, chain] of Object.entries(SUPPORTED_CHAINS)) { |
| 3556 | // Skip token entries - they're accessed via the unified Ethereum view | |
| 3557 | if (chain.isToken) continue | |
| 3558 | ||
| 3421 | 3559 | const option = document.createElement('option') |
| 3422 | 3560 | option.value = chainId |
| 3423 | 3561 | option.textContent = `${chain.icon} ${chain.name}` |
| 3424 | 3562 | currencySelector.appendChild(option) |
| 3425 | 3563 | } |
| 3426 | 3564 | |
| 3427 | // Set selected currency | |
| 3428 | currencySelector.value = selectedCurrency | |
| 3565 | // Set selected chain | |
| 3566 | currencySelector.value = selectedChain | |
| 3429 | 3567 | } |
| 3430 | 3568 | |
| 3431 | 3569 | // Fetch prices first (don't wait) |
| … | … | @@ -3433,11 +3571,16 @@ async function init() { |
| 3433 | 3571 | |
| 3434 | 3572 | // Only load wallet data if initialized AND authenticated |
| 3435 | 3573 | if (status.initialized && getToken()) { |
| 3436 | // Update wallet view for selected currency (icon, name, network, address selector) | |
| 3574 | // Update wallet view for selected chain (icon, name, network, address selector) | |
| 3437 | 3575 | updateWalletView() |
| 3438 | 3576 | |
| 3577 | // Use unified balance display for Ethereum | |
| 3578 | const loadBalancePromise = selectedChain === 'ethereum' | |
| 3579 | ? loadEthereumBalances() | |
| 3580 | : loadCurrentBalance() | |
| 3581 | ||
| 3439 | 3582 | await Promise.all([ |
| 3440 | loadCurrentBalance(), | |
| 3583 | loadBalancePromise, | |
| 3441 | 3584 | loadCurrentQRCode(), |
| 3442 | 3585 | loadTransactions() |
| 3443 | 3586 | ]) |
| … | … | @@ -3448,7 +3591,12 @@ async function init() { |
| 3448 | 3591 | // Auto-refresh every 30 seconds |
| 3449 | 3592 | setInterval(() => { |
| 3450 | 3593 | if (document.visibilityState === 'visible') { |
| 3451 | loadCurrentBalance() | |
| 3594 | // Use unified balance display for Ethereum | |
| 3595 | if (selectedChain === 'ethereum') { | |
| 3596 | loadEthereumBalances() | |
| 3597 | } else { | |
| 3598 | loadCurrentBalance() | |
| 3599 | } | |
| 3452 | 3600 | loadCurrentQRCode() |
| 3453 | 3601 | loadTransactions() |
| 3454 | 3602 | } |
| … | … | @@ -0,0 +1,135 @@ |
| 1 | # Hyper Wallet Desktop - Deployment Process | |
| 2 | ||
| 3 | ## Overview | |
| 4 | ||
| 5 | This document defines the deployment workflow for hyper-wallet-desktop. The process is designed to be fault-tolerant, non-interactive, and support both testing and production deployments. | |
| 6 | ||
| 7 | ## Deployment Channels | |
| 8 | ||
| 9 | | Channel | Branch | Tmux Window | Pear Key | | |
| 10 | |---------|--------|-------------|----------| | |
| 11 | | `hyper-wallet-desktop-test` | `testing` | `pear-seed:5` | `pear://hddktsuzwnnawn3rohicf9hfdgaaetqsmgbuwd64nf6nc8nw51do` | | |
| 12 | | `hyper-wallet-desktop` | `stable` | `pear-seed:5` | TBD (use after testing verified) | | |
| 13 | ||
| 14 | Both channels share the same tmux window since only one is seeded at a time. | |
| 15 | ||
| 16 | ## Pre-Deployment Checklist | |
| 17 | ||
| 18 | Before deploying, the script automatically verifies: | |
| 19 | ||
| 20 | 1. **Tests Pass** - All unit tests must pass (`bare test/all.cjs`) | |
| 21 | 2. **node_modules Exists** - Critical for Pear module resolution | |
| 22 | 3. **Critical Files Present** - index.cjs, wallet-server.cjs, desktop files | |
| 23 | 4. **No Uncommitted Changes** (optional) - Can auto-commit or warn | |
| 24 | ||
| 25 | ## Deployment Steps | |
| 26 | ||
| 27 | ### Quick Deploy (Automated) | |
| 28 | ||
| 29 | ```bash | |
| 30 | ./scripts/pear-deploy.sh "Your commit message" | |
| 31 | ``` | |
| 32 | ||
| 33 | This will: | |
| 34 | 1. Run tests (abort if failing) | |
| 35 | 2. Commit changes (if any) | |
| 36 | 3. Stage the app (`pear stage hyper-wallet-desktop .`) | |
| 37 | 4. Release the app (`pear release hyper-wallet-desktop`) | |
| 38 | 5. Restart the seeder in tmux | |
| 39 | 6. Print summary with version info | |
| 40 | ||
| 41 | ### Manual Steps (If Needed) | |
| 42 | ||
| 43 | ```bash | |
| 44 | # 1. Run tests | |
| 45 | bare test/all.cjs | |
| 46 | ||
| 47 | # 2. Stage | |
| 48 | pear stage hyper-wallet-desktop . | |
| 49 | ||
| 50 | # 3. Release | |
| 51 | pear release hyper-wallet-desktop | |
| 52 | ||
| 53 | # 4. Restart seeder | |
| 54 | tmux send-keys -t pear-seed:hyper-wallet-desktop C-c | |
| 55 | sleep 2 | |
| 56 | tmux send-keys -t pear-seed:hyper-wallet-desktop "pear seed hyper-wallet-desktop ." Enter | |
| 57 | ``` | |
| 58 | ||
| 59 | ## Testing on Remote Device (ashatron.local) | |
| 60 | ||
| 61 | After deployment, test on ashatron.local: | |
| 62 | ||
| 63 | ```bash | |
| 64 | # On ashatron.local | |
| 65 | pear run --checkout=latest pear://[KEY] | |
| 66 | ``` | |
| 67 | ||
| 68 | The `--checkout=latest` flag ensures the latest version is fetched. | |
| 69 | ||
| 70 | ## Rollback | |
| 71 | ||
| 72 | If issues are found: | |
| 73 | ||
| 74 | ```bash | |
| 75 | # Check release history | |
| 76 | pear releases hyper-wallet-desktop | |
| 77 | ||
| 78 | # Rollback to previous version (if needed) | |
| 79 | # Note: Pear doesn't have native rollback, but you can: | |
| 80 | # 1. Git checkout previous version | |
| 81 | # 2. Re-deploy | |
| 82 | ``` | |
| 83 | ||
| 84 | ## Tmux Session Management | |
| 85 | ||
| 86 | The seeder runs in `pear-seed:hyper-wallet-desktop`: | |
| 87 | ||
| 88 | ```bash | |
| 89 | # Check seeder status | |
| 90 | tmux list-windows -t pear-seed | |
| 91 | ||
| 92 | # Attach to seeder | |
| 93 | tmux attach -t pear-seed | |
| 94 | ||
| 95 | # Switch to hyper-wallet-desktop window | |
| 96 | # Press Ctrl+B then 5 (or the window number) | |
| 97 | ``` | |
| 98 | ||
| 99 | ## Troubleshooting | |
| 100 | ||
| 101 | ### "Module not found" errors | |
| 102 | - Ensure `node_modules` is included in pear.json staging | |
| 103 | - Run `npm install` if dependencies missing | |
| 104 | - Run `./scripts/setup-pear-compat.sh` for buffer shim | |
| 105 | ||
| 106 | ### Updates not appearing on remote device | |
| 107 | - Verify seeder restarted after release | |
| 108 | - Remote device should use `--checkout=latest` | |
| 109 | - Check DHT connectivity | |
| 110 | ||
| 111 | ### Seeder not running | |
| 112 | ```bash | |
| 113 | # Start seeder manually | |
| 114 | tmux send-keys -t pear-seed:hyper-wallet-desktop "pear seed hyper-wallet-desktop ." Enter | |
| 115 | ``` | |
| 116 | ||
| 117 | ## Version Tracking | |
| 118 | ||
| 119 | After each deployment, update `.claude/deployment-history.json`: | |
| 120 | ||
| 121 | ```json | |
| 122 | { | |
| 123 | "latest_version": "XXX", | |
| 124 | "last_deployed": "YYYY-MM-DD", | |
| 125 | "features": "Description of changes" | |
| 126 | } | |
| 127 | ``` | |
| 128 | ||
| 129 | ## Critical Rules | |
| 130 | ||
| 131 | 1. **ALWAYS run tests before deploying** | |
| 132 | 2. **ALWAYS use channel name** (`pear stage hyper-wallet-desktop .`, not `pear stage .`) | |
| 133 | 3. **ALWAYS restart seeder** after release | |
| 134 | 4. **NEVER ignore node_modules** in pear.json | |
| 135 | 5. **NEVER tell users to clear cache** - fix deployment if updates don't work |
| … | … | @@ -0,0 +1,614 @@ |
| 1 | # USDT/Token Handling Refactor Plan | |
| 2 | ||
| 3 | **Created**: 2026-01-29 | |
| 4 | **Status**: Planned | |
| 5 | **Priority**: High | |
| 6 | ||
| 7 | ## Overview | |
| 8 | ||
| 9 | This plan addresses issues with how tokens (USDT, USDC) are handled compared to native coins (BTC, ETH) in hyper-wallet-desktop. | |
| 10 | ||
| 11 | ### Key Findings from Code Review | |
| 12 | ||
| 13 | 1. **WDK Bitcoin** has public `getTransfers()` API - but code uses private `_electrumClient` | |
| 14 | 2. **WDK Ethereum** does NOT have transaction history API - external providers (Blockscout) are correct approach | |
| 15 | 3. **Token UX** treats tokens as separate "chains" - should be unified under Ethereum view | |
| 16 | ||
| 17 | --- | |
| 18 | ||
| 19 | ## Change 1: Fix Bitcoin Sync (High Priority) | |
| 20 | ||
| 21 | **Problem**: Code uses private WDK API (`_electrumClient`, `_getScriptHash`) which can break on library updates. | |
| 22 | ||
| 23 | **File**: `lib/wallet-storage.cjs` | |
| 24 | **Lines**: ~883-929 | |
| 25 | ||
| 26 | ### Current Code (Fragile) | |
| 27 | ||
| 28 | ```javascript | |
| 29 | if (chain === 'bitcoin') { | |
| 30 | if (!account._electrumClient || typeof account._electrumClient.blockchainScripthash_getHistory !== 'function') { | |
| 31 | results.errors.push(`${chain}: Electrum client unavailable`) | |
| 32 | continue | |
| 33 | } | |
| 34 | const scriptHash = await account._getScriptHash() | |
| 35 | const history = await account._electrumClient.blockchainScripthash_getHistory(scriptHash) | |
| 36 | // ... parsing logic | |
| 37 | } | |
| 38 | ``` | |
| 39 | ||
| 40 | ### Replace With (Public API) | |
| 41 | ||
| 42 | ```javascript | |
| 43 | if (chain === 'bitcoin') { | |
| 44 | try { | |
| 45 | // Use public WDK API (documented in wdk-wallet-btc) | |
| 46 | const transfers = await account.getTransfers({ | |
| 47 | direction: 'all', | |
| 48 | limit: 100 | |
| 49 | }) | |
| 50 | ||
| 51 | for (const transfer of transfers) { | |
| 52 | const existing = await getTransaction(chain, transfer.txid) | |
| 53 | if (existing) { | |
| 54 | chainSkipped++ | |
| 55 | continue | |
| 56 | } | |
| 57 | ||
| 58 | const tx = { | |
| 59 | hash: transfer.txid, | |
| 60 | chain, | |
| 61 | network: config.network || 'testnet', | |
| 62 | direction: transfer.direction, // 'incoming' or 'outgoing' | |
| 63 | from: transfer.direction === 'incoming' ? '(external)' : currentAddress, | |
| 64 | to: transfer.direction === 'incoming' ? currentAddress : (transfer.recipient || '(unknown)'), | |
| 65 | amount: String(transfer.value), | |
| 66 | fee: transfer.fee ? String(transfer.fee) : '0', | |
| 67 | status: transfer.height > 0 ? 'confirmed' : 'pending', | |
| 68 | timestamp: Date.now(), | |
| 69 | blockHeight: transfer.height || null | |
| 70 | } | |
| 71 | ||
| 72 | await storeTransaction(tx) | |
| 73 | chainSynced++ | |
| 74 | results.synced++ | |
| 75 | } | |
| 76 | } catch (err) { | |
| 77 | console.error(`Bitcoin sync error: ${err.message}`) | |
| 78 | results.errors.push(`${chain}: ${err.message}`) | |
| 79 | } | |
| 80 | } | |
| 81 | ``` | |
| 82 | ||
| 83 | ### WDK Public API Reference | |
| 84 | ||
| 85 | From `@tetherto/wdk-wallet-btc` docs: | |
| 86 | ||
| 87 | ```javascript | |
| 88 | const transfers = await account.getTransfers({ | |
| 89 | direction: 'all', // 'incoming', 'outgoing', or 'all' | |
| 90 | limit: 10, | |
| 91 | skip: 0 | |
| 92 | }) | |
| 93 | ||
| 94 | // Returns BtcTransfer[]: | |
| 95 | // - txid: string | |
| 96 | // - address: string (user's own address) | |
| 97 | // - vout: number | |
| 98 | // - height: number (0 if unconfirmed) | |
| 99 | // - value: number (satoshis) | |
| 100 | // - direction: 'incoming' | 'outgoing' | |
| 101 | // - fee?: number | |
| 102 | // - recipient?: string | |
| 103 | ``` | |
| 104 | ||
| 105 | --- | |
| 106 | ||
| 107 | ## Change 2: Unified Ethereum View (Medium Priority) | |
| 108 | ||
| 109 | **Problem**: USDT appears as separate "chain" in dropdown. Users don't see ETH balance (needed for gas) when viewing USDT. | |
| 110 | ||
| 111 | ### 2A. Backend - New Endpoint | |
| 112 | ||
| 113 | **File**: `wallet-server.cjs` | |
| 114 | **Add**: `GET /api/balances/ethereum` | |
| 115 | ||
| 116 | ```javascript | |
| 117 | 'GET /api/balances/ethereum': requireAuth(async (req, res) => { | |
| 118 | try { | |
| 119 | const walletMnemonic = loadWalletMnemonicCached() | |
| 120 | if (!walletMnemonic) { | |
| 121 | return sendError(res, 'Wallet not initialized', 400) | |
| 122 | } | |
| 123 | ||
| 124 | const { initializeWDK, TOKEN_CONTRACTS } = require('./lib/wdk-integration.cjs') | |
| 125 | const { wdk } = await initializeWDK(walletMnemonic, { testnet: true }) | |
| 126 | ||
| 127 | const currentAddr = await getCurrentAddress('ethereum') | |
| 128 | const accountIndex = currentAddr ? currentAddr.index : 0 | |
| 129 | const account = await wdk.getAccount('ethereum', accountIndex) | |
| 130 | const address = await account.getAddress() | |
| 131 | ||
| 132 | // Fetch all balances in parallel | |
| 133 | const [ethBalance, usdtBalance, usdcBalance] = await Promise.all([ | |
| 134 | account.getBalance(), | |
| 135 | account.getTokenBalance(TOKEN_CONTRACTS['ethereum-usdt'].testnet), | |
| 136 | account.getTokenBalance(TOKEN_CONTRACTS['ethereum-usdc'].testnet) | |
| 137 | ]) | |
| 138 | ||
| 139 | sendJSON(res, { | |
| 140 | address, | |
| 141 | network: 'testnet', | |
| 142 | balances: { | |
| 143 | eth: { | |
| 144 | balance: ethBalance.toString(), | |
| 145 | symbol: 'ETH', | |
| 146 | decimals: 18, | |
| 147 | name: 'Ethereum' | |
| 148 | }, | |
| 149 | usdt: { | |
| 150 | balance: usdtBalance.toString(), | |
| 151 | symbol: 'USDT', | |
| 152 | decimals: 6, | |
| 153 | name: 'Tether USD', | |
| 154 | contract: TOKEN_CONTRACTS['ethereum-usdt'].testnet | |
| 155 | }, | |
| 156 | usdc: { | |
| 157 | balance: usdcBalance.toString(), | |
| 158 | symbol: 'USDC', | |
| 159 | decimals: 6, | |
| 160 | name: 'USD Coin', | |
| 161 | contract: TOKEN_CONTRACTS['ethereum-usdc'].testnet | |
| 162 | } | |
| 163 | } | |
| 164 | }) | |
| 165 | } catch (err) { | |
| 166 | console.error('Get Ethereum balances error:', err) | |
| 167 | sendError(res, err.message) | |
| 168 | } | |
| 169 | }), | |
| 170 | ``` | |
| 171 | ||
| 172 | ### 2B. Frontend - Restructure SUPPORTED_CHAINS | |
| 173 | ||
| 174 | **File**: `desktop/wallet.js` | |
| 175 | **Replace** lines ~1441-1513: | |
| 176 | ||
| 177 | ```javascript | |
| 178 | const SUPPORTED_CHAINS = { | |
| 179 | bitcoin: { | |
| 180 | name: 'Bitcoin', | |
| 181 | icon: '₿', | |
| 182 | testnetName: 'Testnet', | |
| 183 | mainnetName: 'Mainnet', | |
| 184 | formatBalance: (balance) => { | |
| 185 | const btc = Number(balance) / 100000000 | |
| 186 | return `${btc.toFixed(8)} BTC` | |
| 187 | }, | |
| 188 | formatSmallUnit: (balance) => `${Number(balance).toLocaleString()} sats`, | |
| 189 | priceId: 'bitcoin', | |
| 190 | addressPattern: { | |
| 191 | testnet: /^(tb1|[mn])[a-zA-HJ-NP-Z0-9]{25,62}$/, | |
| 192 | mainnet: /^(bc1|[13])[a-zA-HJ-NP-Z0-9]{25,62}$/ | |
| 193 | }, | |
| 194 | feeReserve: 500, | |
| 195 | tokens: null | |
| 196 | }, | |
| 197 | ||
| 198 | ethereum: { | |
| 199 | name: 'Ethereum', | |
| 200 | icon: 'Ξ', | |
| 201 | testnetName: 'Sepolia', | |
| 202 | mainnetName: 'Mainnet', | |
| 203 | formatBalance: (balance) => { | |
| 204 | const eth = Number(balance) / 1e18 | |
| 205 | return `${eth.toFixed(6)} ETH` | |
| 206 | }, | |
| 207 | formatSmallUnit: (balance) => `${Number(balance).toLocaleString()} wei`, | |
| 208 | priceId: 'ethereum', | |
| 209 | addressPattern: { | |
| 210 | testnet: /^0x[a-fA-F0-9]{40}$/, | |
| 211 | mainnet: /^0x[a-fA-F0-9]{40}$/ | |
| 212 | }, | |
| 213 | feeReserve: 100000, | |
| 214 | tokens: { | |
| 215 | usdt: { | |
| 216 | symbol: 'USDT', | |
| 217 | name: 'Tether USD', | |
| 218 | icon: '₮', | |
| 219 | decimals: 6, | |
| 220 | priceId: 'tether', | |
| 221 | formatBalance: (balance) => `${(Number(balance) / 1e6).toFixed(2)} USDT` | |
| 222 | }, | |
| 223 | usdc: { | |
| 224 | symbol: 'USDC', | |
| 225 | name: 'USD Coin', | |
| 226 | icon: '$', | |
| 227 | decimals: 6, | |
| 228 | priceId: 'usd-coin', | |
| 229 | formatBalance: (balance) => `${(Number(balance) / 1e6).toFixed(2)} USDC` | |
| 230 | } | |
| 231 | } | |
| 232 | } | |
| 233 | } | |
| 234 | ||
| 235 | // Track selected chain AND selected asset | |
| 236 | let selectedChain = localStorage.getItem('chain') || 'bitcoin' | |
| 237 | let selectedAsset = localStorage.getItem('asset') || null // null = native, 'usdt' = token | |
| 238 | ``` | |
| 239 | ||
| 240 | ### 2C. Frontend - Unified Balance Display | |
| 241 | ||
| 242 | **File**: `desktop/wallet.js` | |
| 243 | **Add** new function: | |
| 244 | ||
| 245 | ```javascript | |
| 246 | async function loadEthereumBalances() { | |
| 247 | const container = document.getElementById('balance-display') | |
| 248 | container.innerHTML = '<div class="spinner"></div>' | |
| 249 | ||
| 250 | try { | |
| 251 | const res = await authFetch(`/api/balances/ethereum`) | |
| 252 | const data = await res.json() | |
| 253 | ||
| 254 | if (data.error) { | |
| 255 | container.innerHTML = `<span class="error">${data.error}</span>` | |
| 256 | return | |
| 257 | } | |
| 258 | ||
| 259 | const { balances } = data | |
| 260 | ||
| 261 | const ethAmount = Number(balances.eth.balance) / 1e18 | |
| 262 | const usdtAmount = Number(balances.usdt.balance) / 1e6 | |
| 263 | const usdcAmount = Number(balances.usdc.balance) / 1e6 | |
| 264 | ||
| 265 | const ethUsd = ethAmount * (prices.ethereum || 0) | |
| 266 | const usdtUsd = usdtAmount * (prices.tether || 1) | |
| 267 | const usdcUsd = usdcAmount * (prices['usd-coin'] || 1) | |
| 268 | const totalUsd = ethUsd + usdtUsd + usdcUsd | |
| 269 | ||
| 270 | container.innerHTML = ` | |
| 271 | <div class="unified-balance"> | |
| 272 | <div class="total-value"> | |
| 273 | <span class="total-usd">${formatUSD(totalUsd)}</span> | |
| 274 | <span class="total-label">Total Balance</span> | |
| 275 | </div> | |
| 276 | ||
| 277 | <div class="asset-list"> | |
| 278 | <div class="asset-row ${selectedAsset === null ? 'selected' : ''}" | |
| 279 | onclick="selectAsset(null)"> | |
| 280 | <span class="asset-icon">Ξ</span> | |
| 281 | <span class="asset-name">ETH</span> | |
| 282 | <span class="asset-balance">${ethAmount.toFixed(6)}</span> | |
| 283 | <span class="asset-usd">${formatUSD(ethUsd)}</span> | |
| 284 | </div> | |
| 285 | ||
| 286 | <div class="asset-row ${selectedAsset === 'usdt' ? 'selected' : ''}" | |
| 287 | onclick="selectAsset('usdt')"> | |
| 288 | <span class="asset-icon">₮</span> | |
| 289 | <span class="asset-name">USDT</span> | |
| 290 | <span class="asset-balance">${usdtAmount.toFixed(2)}</span> | |
| 291 | <span class="asset-usd">${formatUSD(usdtUsd)}</span> | |
| 292 | </div> | |
| 293 | ||
| 294 | <div class="asset-row ${selectedAsset === 'usdc' ? 'selected' : ''}" | |
| 295 | onclick="selectAsset('usdc')"> | |
| 296 | <span class="asset-icon">$</span> | |
| 297 | <span class="asset-name">USDC</span> | |
| 298 | <span class="asset-balance">${usdcAmount.toFixed(2)}</span> | |
| 299 | <span class="asset-usd">${formatUSD(usdcUsd)}</span> | |
| 300 | </div> | |
| 301 | </div> | |
| 302 | ||
| 303 | ${ethAmount < 0.001 ? ` | |
| 304 | <div class="gas-warning"> | |
| 305 | ⚠️ Low ETH balance - needed for gas fees | |
| 306 | </div> | |
| 307 | ` : ''} | |
| 308 | </div> | |
| 309 | ` | |
| 310 | } catch (err) { | |
| 311 | console.error('Load balances error:', err) | |
| 312 | container.innerHTML = '<span class="error">Failed to load balances</span>' | |
| 313 | } | |
| 314 | } | |
| 315 | ||
| 316 | function selectAsset(asset) { | |
| 317 | selectedAsset = asset | |
| 318 | localStorage.setItem('asset', asset || '') | |
| 319 | ||
| 320 | document.querySelectorAll('.asset-row').forEach(row => { | |
| 321 | row.classList.remove('selected') | |
| 322 | }) | |
| 323 | event.currentTarget.classList.add('selected') | |
| 324 | } | |
| 325 | ``` | |
| 326 | ||
| 327 | ### 2D. Frontend - Updated Send Flow | |
| 328 | ||
| 329 | **File**: `desktop/wallet.js` | |
| 330 | **Modify** `showSend()`: | |
| 331 | ||
| 332 | ```javascript | |
| 333 | function showSend(chain) { | |
| 334 | currentSendChain = chain | |
| 335 | const modal = document.getElementById('send-modal') | |
| 336 | const chainInfo = SUPPORTED_CHAINS[chain] | |
| 337 | ||
| 338 | // For Ethereum, show asset selector | |
| 339 | if (chain === 'ethereum' && chainInfo.tokens) { | |
| 340 | document.getElementById('send-asset-selector').innerHTML = ` | |
| 341 | <label>Asset to Send:</label> | |
| 342 | <select id="send-asset" onchange="updateSendAsset()"> | |
| 343 | <option value="">ETH (Native)</option> | |
| 344 | ${Object.entries(chainInfo.tokens).map(([key, token]) => | |
| 345 | `<option value="${key}">${token.icon} ${token.symbol}</option>` | |
| 346 | ).join('')} | |
| 347 | </select> | |
| 348 | ` | |
| 349 | document.getElementById('send-asset-selector').style.display = 'block' | |
| 350 | } else { | |
| 351 | document.getElementById('send-asset-selector').style.display = 'none' | |
| 352 | } | |
| 353 | ||
| 354 | updateGasInfo() | |
| 355 | modal.showModal() | |
| 356 | } | |
| 357 | ||
| 358 | async function updateGasInfo() { | |
| 359 | const gasInfo = document.getElementById('send-gas-info') | |
| 360 | const sendAsset = document.getElementById('send-asset')?.value | |
| 361 | ||
| 362 | if (currentSendChain === 'ethereum' && sendAsset) { | |
| 363 | const ethRes = await authFetch(`/api/balance/${selectedNetwork}-ethereum`) | |
| 364 | const ethData = await ethRes.json() | |
| 365 | const ethBalance = Number(ethData.balance || 0) / 1e18 | |
| 366 | ||
| 367 | gasInfo.innerHTML = ` | |
| 368 | <div class="gas-info"> | |
| 369 | <span>Gas paid in ETH</span> | |
| 370 | <span>Your ETH: ${ethBalance.toFixed(6)} ETH</span> | |
| 371 | ${ethBalance < 0.001 ? '<span class="warning">⚠️ May be insufficient for gas</span>' : ''} | |
| 372 | </div> | |
| 373 | ` | |
| 374 | gasInfo.style.display = 'block' | |
| 375 | } else { | |
| 376 | gasInfo.style.display = 'none' | |
| 377 | } | |
| 378 | } | |
| 379 | ``` | |
| 380 | ||
| 381 | ### 2E. CSS for Unified View | |
| 382 | ||
| 383 | **File**: `desktop/wallet.css` | |
| 384 | **Add**: | |
| 385 | ||
| 386 | ```css | |
| 387 | /* Unified Balance Display */ | |
| 388 | .unified-balance { | |
| 389 | padding: 1rem; | |
| 390 | } | |
| 391 | ||
| 392 | .total-value { | |
| 393 | text-align: center; | |
| 394 | margin-bottom: 1.5rem; | |
| 395 | } | |
| 396 | ||
| 397 | .total-usd { | |
| 398 | font-size: 2rem; | |
| 399 | font-weight: bold; | |
| 400 | display: block; | |
| 401 | } | |
| 402 | ||
| 403 | .total-label { | |
| 404 | color: var(--text-secondary); | |
| 405 | font-size: 0.875rem; | |
| 406 | } | |
| 407 | ||
| 408 | .asset-list { | |
| 409 | display: flex; | |
| 410 | flex-direction: column; | |
| 411 | gap: 0.5rem; | |
| 412 | } | |
| 413 | ||
| 414 | .asset-row { | |
| 415 | display: grid; | |
| 416 | grid-template-columns: 2rem 4rem 1fr auto; | |
| 417 | align-items: center; | |
| 418 | padding: 0.75rem; | |
| 419 | border-radius: 8px; | |
| 420 | cursor: pointer; | |
| 421 | transition: background 0.2s; | |
| 422 | } | |
| 423 | ||
| 424 | .asset-row:hover { | |
| 425 | background: var(--bg-hover); | |
| 426 | } | |
| 427 | ||
| 428 | .asset-row.selected { | |
| 429 | background: var(--bg-selected); | |
| 430 | border: 1px solid var(--accent); | |
| 431 | } | |
| 432 | ||
| 433 | .asset-icon { | |
| 434 | font-size: 1.25rem; | |
| 435 | } | |
| 436 | ||
| 437 | .asset-name { | |
| 438 | font-weight: 500; | |
| 439 | } | |
| 440 | ||
| 441 | .asset-balance { | |
| 442 | text-align: right; | |
| 443 | font-family: monospace; | |
| 444 | } | |
| 445 | ||
| 446 | .asset-usd { | |
| 447 | text-align: right; | |
| 448 | color: var(--text-secondary); | |
| 449 | min-width: 80px; | |
| 450 | } | |
| 451 | ||
| 452 | .gas-warning { | |
| 453 | margin-top: 1rem; | |
| 454 | padding: 0.75rem; | |
| 455 | background: var(--warning-bg); | |
| 456 | border-radius: 8px; | |
| 457 | color: var(--warning-text); | |
| 458 | text-align: center; | |
| 459 | } | |
| 460 | ||
| 461 | .gas-info { | |
| 462 | display: flex; | |
| 463 | justify-content: space-between; | |
| 464 | padding: 0.5rem; | |
| 465 | background: var(--bg-secondary); | |
| 466 | border-radius: 4px; | |
| 467 | font-size: 0.875rem; | |
| 468 | } | |
| 469 | ||
| 470 | .gas-info .warning { | |
| 471 | color: var(--warning-text); | |
| 472 | } | |
| 473 | ``` | |
| 474 | ||
| 475 | ### 2F. Update Currency Selector | |
| 476 | ||
| 477 | **File**: `desktop/wallet.js` | |
| 478 | **Modify** in `initApp()`: | |
| 479 | ||
| 480 | ```javascript | |
| 481 | // Populate currency selector - only base chains, not tokens | |
| 482 | const currencySelector = document.getElementById('currency-selector') | |
| 483 | if (currencySelector) { | |
| 484 | currencySelector.innerHTML = '' | |
| 485 | ||
| 486 | for (const [chainId, chain] of Object.entries(SUPPORTED_CHAINS)) { | |
| 487 | const option = document.createElement('option') | |
| 488 | option.value = chainId | |
| 489 | option.textContent = `${chain.icon} ${chain.name}` | |
| 490 | currencySelector.appendChild(option) | |
| 491 | } | |
| 492 | ||
| 493 | currencySelector.value = selectedChain | |
| 494 | } | |
| 495 | ``` | |
| 496 | ||
| 497 | **Modify** `switchCurrency()`: | |
| 498 | ||
| 499 | ```javascript | |
| 500 | function switchCurrency() { | |
| 501 | const selector = document.getElementById('currency-selector') | |
| 502 | selectedChain = selector.value | |
| 503 | selectedAsset = null | |
| 504 | localStorage.setItem('chain', selectedChain) | |
| 505 | localStorage.setItem('asset', '') | |
| 506 | ||
| 507 | updateWalletView() | |
| 508 | ||
| 509 | if (selectedChain === 'ethereum') { | |
| 510 | loadEthereumBalances() | |
| 511 | } else { | |
| 512 | loadCurrentBalance() | |
| 513 | } | |
| 514 | ||
| 515 | loadCurrentAddress() | |
| 516 | loadTransactions() | |
| 517 | } | |
| 518 | ``` | |
| 519 | ||
| 520 | --- | |
| 521 | ||
| 522 | ## Change 3: Add Alchemy Token Support (Low Priority) | |
| 523 | ||
| 524 | **File**: `lib/blockchain-providers.cjs` | |
| 525 | ||
| 526 | Add `'erc20'` to category array and implement token methods for Alchemy provider. | |
| 527 | ||
| 528 | ```javascript | |
| 529 | alchemy: { | |
| 530 | // ... existing config ... | |
| 531 | ||
| 532 | async fetchTransactions (address, network, apiKey) { | |
| 533 | // ... existing code but add 'erc20' to category: | |
| 534 | category: ['external', 'internal', 'erc20'], | |
| 535 | }, | |
| 536 | ||
| 537 | getTokenTxListUrl (address, network, contractAddress = null) { | |
| 538 | return null // Signal to use fetchTokenTransactions | |
| 539 | }, | |
| 540 | ||
| 541 | async fetchTokenTransactions (address, network, apiKey, contractAddress = null) { | |
| 542 | const baseUrl = network === 'testnet' ? this.testnetUrl : this.mainnetUrl | |
| 543 | const url = `${baseUrl}/${apiKey}` | |
| 544 | ||
| 545 | const params = { | |
| 546 | fromBlock: '0x0', | |
| 547 | toBlock: 'latest', | |
| 548 | toAddress: address, | |
| 549 | category: ['erc20'], | |
| 550 | maxCount: '0x64' | |
| 551 | } | |
| 552 | ||
| 553 | if (contractAddress) { | |
| 554 | params.contractAddresses = [contractAddress] | |
| 555 | } | |
| 556 | ||
| 557 | const response = await fetch(url, { | |
| 558 | method: 'POST', | |
| 559 | headers: { 'Content-Type': 'application/json' }, | |
| 560 | body: JSON.stringify({ | |
| 561 | jsonrpc: '2.0', | |
| 562 | method: 'alchemy_getAssetTransfers', | |
| 563 | params: [params], | |
| 564 | id: 1 | |
| 565 | }) | |
| 566 | }) | |
| 567 | ||
| 568 | return response.json() | |
| 569 | }, | |
| 570 | ||
| 571 | parseTokenResponse (data) { | |
| 572 | if (data.result && Array.isArray(data.result.transfers)) { | |
| 573 | return data.result.transfers.map(tx => ({ | |
| 574 | hash: tx.hash, | |
| 575 | from: tx.from, | |
| 576 | to: tx.to, | |
| 577 | value: tx.rawContract?.value || '0', | |
| 578 | tokenSymbol: tx.asset || 'TOKEN', | |
| 579 | contractAddress: tx.rawContract?.address, | |
| 580 | fee: '0', | |
| 581 | blockNumber: tx.blockNum ? parseInt(tx.blockNum, 16) : 0, | |
| 582 | timestamp: 0, | |
| 583 | status: 'confirmed' | |
| 584 | })) | |
| 585 | } | |
| 586 | throw new Error('Invalid Alchemy response format') | |
| 587 | } | |
| 588 | } | |
| 589 | ``` | |
| 590 | ||
| 591 | --- | |
| 592 | ||
| 593 | ## Summary | |
| 594 | ||
| 595 | | # | Change | Priority | Files | Status | | |
| 596 | |---|--------|----------|-------|--------| | |
| 597 | | 1 | Bitcoin sync - use public API | **High** | `wallet-storage.cjs` | Planned | | |
| 598 | | 2 | Unified Ethereum view | **Medium** | `wallet-server.cjs`, `wallet.js`, `wallet.css` | Planned | | |
| 599 | | 3 | Alchemy token support | Low | `blockchain-providers.cjs` | Planned | | |
| 600 | ||
| 601 | --- | |
| 602 | ||
| 603 | ## References | |
| 604 | ||
| 605 | - WDK Bitcoin docs: `tetherto-kb/repos/wdk-docs/sdk/wallet-modules/wallet-btc/api-reference.md` | |
| 606 | - WDK Ethereum docs: `tetherto-kb/repos/wdk-docs/sdk/wallet-modules/wallet-evm/api-reference.md` | |
| 607 | - Current token contracts: `lib/wdk-integration.cjs` lines 146-155 | |
| 608 | - Current providers: `lib/blockchain-providers.cjs` | |
| 609 | ||
| 610 | ## Notes | |
| 611 | ||
| 612 | - WDK Ethereum does NOT have `getTransfers()` - external providers (Blockscout) are correct | |
| 613 | - Blockscout token API verified working on Sepolia testnet | |
| 614 | - USDT Sepolia contract verified: `0x7169D38820dfd117C3FA1f22a697dBA58d90BA06` ("Test Tether USD") |
| … | … | @@ -1,124 +1,360 @@ |
| 1 | 1 | #!/bin/bash |
| 2 | 2 | # Pear Deployment Script for Hyper Wallet Desktop |
| 3 | # Stages, releases, and optionally seeds the desktop app | |
| 3 | # | |
| 4 | # Usage: | |
| 5 | # ./scripts/pear-deploy.sh --testing "commit message" | |
| 6 | # ./scripts/pear-deploy.sh --production "commit message" | |
| 7 | # | |
| 8 | # Workflow: | |
| 9 | # 1. Pre-flight checks (node_modules, critical files) | |
| 10 | # 2. Run tests (abort if failing) | |
| 11 | # 3. Git commit + push | |
| 12 | # 4. Pear stage | |
| 13 | # 5. Pear release | |
| 14 | # 6. Restart seeder in tmux (NOT background) | |
| 15 | # 7. Verify seeder started | |
| 16 | # 8. Update deployment history | |
| 4 | 17 | |
| 5 | 18 | set -e # Exit on error |
| 6 | 19 | |
| 7 | COMMIT_MSG="${1:-Update desktop app}" | |
| 20 | # ============================================================================ | |
| 21 | # CONFIGURATION | |
| 22 | # ============================================================================ | |
| 23 | ||
| 8 | 24 | PROJECT_DIR="/storage/projects/code/hypercore-kb/hyper-fleeky/hyper-wallet-desktop" |
| 25 | TMUX_SESSION="pear-seed" | |
| 26 | TMUX_WINDOW="5" # hyper-wallet-desktop window | |
| 27 | ||
| 28 | # Channel configuration | |
| 29 | TESTING_CHANNEL="hyper-wallet-desktop-test" | |
| 30 | TESTING_BRANCH="testing" | |
| 31 | ||
| 32 | PRODUCTION_CHANNEL="hyper-wallet-desktop" | |
| 33 | PRODUCTION_BRANCH="stable" | |
| 34 | ||
| 35 | # Colors | |
| 36 | RED='\033[0;31m' | |
| 37 | GREEN='\033[0;32m' | |
| 38 | YELLOW='\033[1;33m' | |
| 39 | BLUE='\033[0;34m' | |
| 40 | NC='\033[0m' | |
| 41 | ||
| 42 | # ============================================================================ | |
| 43 | # PARSE ARGUMENTS | |
| 44 | # ============================================================================ | |
| 45 | ||
| 46 | MODE="" | |
| 47 | COMMIT_MSG="" | |
| 48 | SKIP_TESTS=false | |
| 49 | ||
| 50 | show_usage() { | |
| 51 | echo "Usage:" | |
| 52 | echo " ./scripts/pear-deploy.sh --testing \"commit message\"" | |
| 53 | echo " ./scripts/pear-deploy.sh --production \"commit message\"" | |
| 54 | echo "" | |
| 55 | echo "Options:" | |
| 56 | echo " --testing, -t Deploy to testing channel (branch: $TESTING_BRANCH)" | |
| 57 | echo " --production, -p Deploy to production channel (branch: $PRODUCTION_BRANCH)" | |
| 58 | echo " --skip-tests Skip running tests (not recommended)" | |
| 59 | echo "" | |
| 60 | echo "Channels:" | |
| 61 | echo " Testing: $TESTING_CHANNEL" | |
| 62 | echo " Production: $PRODUCTION_CHANNEL" | |
| 63 | echo "" | |
| 64 | exit 1 | |
| 65 | } | |
| 66 | ||
| 67 | # Parse flags | |
| 68 | while [[ $# -gt 0 ]]; do | |
| 69 | case $1 in | |
| 70 | --testing|-t) | |
| 71 | MODE="testing" | |
| 72 | shift | |
| 73 | ;; | |
| 74 | --production|-p) | |
| 75 | MODE="production" | |
| 76 | shift | |
| 77 | ;; | |
| 78 | --skip-tests) | |
| 79 | SKIP_TESTS=true | |
| 80 | shift | |
| 81 | ;; | |
| 82 | -*) | |
| 83 | echo -e "${RED}Unknown option: $1${NC}" | |
| 84 | show_usage | |
| 85 | ;; | |
| 86 | *) | |
| 87 | COMMIT_MSG="$1" | |
| 88 | shift | |
| 89 | ;; | |
| 90 | esac | |
| 91 | done | |
| 92 | ||
| 93 | if [ -z "$MODE" ]; then | |
| 94 | echo -e "${RED}Error: Must specify --testing or --production${NC}" | |
| 95 | echo "" | |
| 96 | show_usage | |
| 97 | fi | |
| 98 | ||
| 99 | if [ -z "$COMMIT_MSG" ]; then | |
| 100 | echo -e "${RED}Error: Commit message required${NC}" | |
| 101 | echo "" | |
| 102 | show_usage | |
| 103 | fi | |
| 104 | ||
| 105 | # Set channel and branch based on mode | |
| 106 | if [ "$MODE" == "testing" ]; then | |
| 107 | CHANNEL="$TESTING_CHANNEL" | |
| 108 | EXPECTED_BRANCH="$TESTING_BRANCH" | |
| 109 | else | |
| 110 | CHANNEL="$PRODUCTION_CHANNEL" | |
| 111 | EXPECTED_BRANCH="$PRODUCTION_BRANCH" | |
| 112 | fi | |
| 9 | 113 | |
| 10 | 114 | cd "$PROJECT_DIR" |
| 11 | 115 | |
| 12 | echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" | |
| 13 | echo "🚀 Hyper Wallet Desktop - Pear Deployment" | |
| 14 | echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" | |
| 116 | # ============================================================================ | |
| 117 | # HEADER | |
| 118 | # ============================================================================ | |
| 119 | ||
| 120 | echo "" | |
| 121 | echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" | |
| 122 | echo -e "${BLUE} Hyper Wallet Desktop - Pear Deployment${NC}" | |
| 123 | echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" | |
| 124 | echo "" | |
| 125 | echo " Mode: $MODE" | |
| 126 | echo " Channel: $CHANNEL" | |
| 127 | echo " Branch: $EXPECTED_BRANCH" | |
| 15 | 128 | echo "" |
| 16 | 129 | |
| 17 | # 1. Verify node_modules exists (CRITICAL for Pear) | |
| 18 | if [ ! -d "node_modules" ]; then | |
| 19 | echo "❌ ERROR: node_modules not found!" | |
| 20 | echo " Run: npm install" | |
| 21 | exit 1 | |
| 22 | fi | |
| 130 | # ============================================================================ | |
| 131 | # STEP 1: PRE-FLIGHT CHECKS | |
| 132 | # ============================================================================ | |
| 23 | 133 | |
| 24 | echo "✓ node_modules verified ($(du -sh node_modules | cut -f1))" | |
| 134 | echo -e "${YELLOW}Step 1: Pre-flight Checks${NC}" | |
| 25 | 135 | |
| 26 | # 2. Verify critical files | |
| 27 | CRITICAL_FILES=( | |
| 28 | "index.cjs" | |
| 29 | "wallet-server.cjs" | |
| 30 | "pear.json" | |
| 31 | "package.json" | |
| 32 | "desktop/index.html" | |
| 33 | "desktop/wallet.js" | |
| 34 | "desktop/wallet.css" | |
| 35 | ) | |
| 136 | # Check we're on the correct branch | |
| 137 | CURRENT_BRANCH=$(git branch --show-current) | |
| 138 | if [ "$CURRENT_BRANCH" != "$EXPECTED_BRANCH" ]; then | |
| 139 | echo -e "${RED} Error: Must be on '$EXPECTED_BRANCH' branch for $MODE deployment${NC}" | |
| 140 | echo " Current branch: $CURRENT_BRANCH" | |
| 141 | echo " Run: git checkout $EXPECTED_BRANCH" | |
| 142 | exit 1 | |
| 143 | fi | |
| 144 | echo -e " ${GREEN}✓ On correct branch: $CURRENT_BRANCH${NC}" | |
| 36 | 145 | |
| 37 | for file in "${CRITICAL_FILES[@]}"; do | |
| 38 | if [ ! -f "$file" ]; then | |
| 39 | echo "❌ ERROR: Missing critical file: $file" | |
| 146 | # Check node_modules | |
| 147 | if [ ! -d "node_modules" ]; then | |
| 148 | echo -e "${RED} ✗ node_modules not found!${NC}" | |
| 149 | echo " Run: npm install" | |
| 40 | 150 | exit 1 |
| 41 | fi | |
| 151 | fi | |
| 152 | echo -e " ${GREEN}✓ node_modules exists ($(du -sh node_modules 2>/dev/null | cut -f1))${NC}" | |
| 153 | ||
| 154 | # Check critical files | |
| 155 | CRITICAL_FILES=("index.cjs" "wallet-server.cjs" "pear.json" "desktop/index.html" "desktop/wallet.js") | |
| 156 | for file in "${CRITICAL_FILES[@]}"; do | |
| 157 | if [ ! -f "$file" ]; then | |
| 158 | echo -e "${RED} ✗ Missing: $file${NC}" | |
| 159 | exit 1 | |
| 160 | fi | |
| 42 | 161 | done |
| 162 | echo -e " ${GREEN}✓ All critical files present${NC}" | |
| 43 | 163 | |
| 44 | echo "✓ All critical files present" | |
| 164 | # ============================================================================ | |
| 165 | # STEP 2: RUN TESTS | |
| 166 | # ============================================================================ | |
| 45 | 167 | |
| 46 | # 3. Stage the app | |
| 47 | 168 | echo "" |
| 48 | echo "📦 Staging app..." | |
| 49 | pear stage hyper-wallet-desktop . | |
| 169 | if [ "$SKIP_TESTS" = true ]; then | |
| 170 | echo -e "${YELLOW}Step 2: Tests SKIPPED (--skip-tests)${NC}" | |
| 171 | else | |
| 172 | echo -e "${YELLOW}Step 2: Running Tests${NC}" | |
| 173 | echo " Running: bare test/all.cjs" | |
| 50 | 174 | |
| 51 | if [ $? -ne 0 ]; then | |
| 52 | echo "❌ Staging failed!" | |
| 53 | exit 1 | |
| 175 | # Run tests directly (output streams to terminal) | |
| 176 | if bare test/all.cjs; then | |
| 177 | echo -e " ${GREEN}✓ All tests passed${NC}" | |
| 178 | else | |
| 179 | echo -e "${RED} ✗ Tests FAILED!${NC}" | |
| 180 | exit 1 | |
| 181 | fi | |
| 54 | 182 | fi |
| 55 | 183 | |
| 56 | # Get the new app key | |
| 57 | APP_KEY=$(pear stage hyper-wallet-desktop . 2>&1 | grep -oP 'pear://[a-z0-9]+' | tail -1) | |
| 184 | # ============================================================================ | |
| 185 | # STEP 3: GIT COMMIT + PUSH | |
| 186 | # ============================================================================ | |
| 187 | ||
| 188 | echo "" | |
| 189 | echo -e "${YELLOW}Step 3: Git Commit + Push${NC}" | |
| 190 | ||
| 191 | if [ -n "$(git status --porcelain)" ]; then | |
| 192 | echo " Changes to commit:" | |
| 193 | git status --short | sed 's/^/ /' | |
| 58 | 194 | |
| 59 | if [ -z "$APP_KEY" ]; then | |
| 60 | echo "❌ Failed to get app key from staging output" | |
| 61 | exit 1 | |
| 195 | # Stage all changes | |
| 196 | git add -A | |
| 197 | ||
| 198 | # Commit with co-authorship | |
| 199 | git commit -m "$(cat <<EOF | |
| 200 | $COMMIT_MSG | |
| 201 | ||
| 202 | Deployment: $MODE ($CHANNEL) | |
| 203 | ||
| 204 | Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> | |
| 205 | EOF | |
| 206 | )" | |
| 207 | echo -e " ${GREEN}✓ Committed${NC}" | |
| 208 | else | |
| 209 | echo -e " ${GREEN}✓ No uncommitted changes${NC}" | |
| 210 | fi | |
| 211 | ||
| 212 | # Push to remote | |
| 213 | echo " Pushing to origin/$CURRENT_BRANCH..." | |
| 214 | if git push origin "$CURRENT_BRANCH" 2>&1; then | |
| 215 | echo -e " ${GREEN}✓ Pushed to remote${NC}" | |
| 216 | else | |
| 217 | echo -e "${YELLOW} ⚠ Push failed (continuing anyway)${NC}" | |
| 62 | 218 | fi |
| 63 | 219 | |
| 64 | echo "✓ Staged successfully" | |
| 65 | echo " Key: $APP_KEY" | |
| 220 | # ============================================================================ | |
| 221 | # STEP 4: PEAR STAGE | |
| 222 | # ============================================================================ | |
| 66 | 223 | |
| 67 | # 4. Release (make this version the default) | |
| 68 | 224 | echo "" |
| 69 | echo "🎯 Releasing app (making it the default version)..." | |
| 70 | pear release hyper-wallet-desktop | |
| 225 | echo -e "${YELLOW}Step 4: Pear Stage${NC}" | |
| 71 | 226 | |
| 72 | if [ $? -ne 0 ]; then | |
| 73 | echo "❌ Release failed!" | |
| 74 | exit 1 | |
| 75 | fi | |
| 227 | echo " Running: pear stage $CHANNEL ." | |
| 228 | STAGE_OUTPUT=$(pear stage "$CHANNEL" . 2>&1) | |
| 229 | echo "$STAGE_OUTPUT" | sed 's/^/ /' | |
| 76 | 230 | |
| 77 | echo "✓ Released successfully" | |
| 231 | # Extract version | |
| 232 | LATEST=$(echo "$STAGE_OUTPUT" | grep -oP 'Latest:\s+\K\d+' || echo "unknown") | |
| 233 | echo -e " ${GREEN}✓ Staged version: $LATEST${NC}" | |
| 234 | ||
| 235 | # ============================================================================ | |
| 236 | # STEP 5: PEAR RELEASE | |
| 237 | # ============================================================================ | |
| 78 | 238 | |
| 79 | # 5. Optional: Seed in background | |
| 80 | 239 | echo "" |
| 81 | read -p "🌱 Start seeding in tmux? (y/N) " -n 1 -r | |
| 82 | echo | |
| 83 | if [[ $REPLY =~ ^[Yy]$ ]]; then | |
| 84 | # Check if pear-seed session exists | |
| 85 | if ! tmux has-session -t pear-seed 2>/dev/null; then | |
| 86 | echo "Creating tmux session: pear-seed" | |
| 87 | tmux new-session -d -s pear-seed -n hyper-wallet-desktop | |
| 88 | else | |
| 89 | # Check if window already exists | |
| 90 | if tmux list-windows -t pear-seed | grep -q "hyper-wallet-desktop"; then | |
| 91 | echo "Window already exists, killing old seeder..." | |
| 92 | tmux kill-window -t pear-seed:hyper-wallet-desktop | |
| 93 | fi | |
| 94 | # Create new window | |
| 95 | tmux new-window -t pear-seed -n hyper-wallet-desktop | |
| 96 | fi | |
| 240 | echo -e "${YELLOW}Step 5: Pear Release${NC}" | |
| 97 | 241 | |
| 98 | # Start seeding in the window | |
| 99 | tmux send-keys -t pear-seed:hyper-wallet-desktop "cd $PROJECT_DIR" C-m | |
| 100 | tmux send-keys -t pear-seed:hyper-wallet-desktop "pear seed $APP_KEY" C-m | |
| 242 | echo " Running: pear release $CHANNEL" | |
| 243 | pear release "$CHANNEL" 2>&1 | sed 's/^/ /' | |
| 244 | echo -e " ${GREEN}✓ Released as default${NC}" | |
| 101 | 245 | |
| 102 | echo "✓ Seeding started in tmux session: pear-seed:hyper-wallet-desktop" | |
| 103 | echo " Attach: tmux attach -t pear-seed" | |
| 246 | # ============================================================================ | |
| 247 | # STEP 6: RESTART SEEDER IN TMUX | |
| 248 | # ============================================================================ | |
| 249 | ||
| 250 | echo "" | |
| 251 | echo -e "${YELLOW}Step 6: Restart Seeder in Tmux${NC}" | |
| 252 | ||
| 253 | # Check tmux session exists | |
| 254 | if ! tmux has-session -t "$TMUX_SESSION" 2>/dev/null; then | |
| 255 | echo -e "${RED} ✗ Tmux session '$TMUX_SESSION' not found!${NC}" | |
| 256 | echo " Create it with: tmux new-session -d -s $TMUX_SESSION" | |
| 257 | exit 1 | |
| 104 | 258 | fi |
| 259 | echo -e " ${GREEN}✓ Tmux session exists: $TMUX_SESSION${NC}" | |
| 260 | ||
| 261 | # Check window exists | |
| 262 | if ! tmux list-windows -t "$TMUX_SESSION" -F "#{window_index}" | grep -q "^${TMUX_WINDOW}$"; then | |
| 263 | echo -e "${YELLOW} ⚠ Window $TMUX_WINDOW not found, creating...${NC}" | |
| 264 | tmux new-window -t "$TMUX_SESSION:$TMUX_WINDOW" -n "hyper-wallet-desktop" | |
| 265 | fi | |
| 266 | echo -e " ${GREEN}✓ Tmux window exists: $TMUX_SESSION:$TMUX_WINDOW${NC}" | |
| 267 | ||
| 268 | # Stop existing seeder | |
| 269 | echo " Stopping existing seeder..." | |
| 270 | tmux send-keys -t "$TMUX_SESSION:$TMUX_WINDOW" C-c 2>/dev/null || true | |
| 271 | sleep 2 | |
| 272 | ||
| 273 | # Start new seeder | |
| 274 | echo " Starting seeder: pear seed $CHANNEL ." | |
| 275 | tmux send-keys -t "$TMUX_SESSION:$TMUX_WINDOW" "cd $PROJECT_DIR && pear seed $CHANNEL ." Enter | |
| 276 | ||
| 277 | echo -e " ${GREEN}✓ Seeder command sent to $TMUX_SESSION:$TMUX_WINDOW${NC}" | |
| 278 | ||
| 279 | # ============================================================================ | |
| 280 | # STEP 7: VERIFY SEEDER | |
| 281 | # ============================================================================ | |
| 105 | 282 | |
| 106 | # 6. Summary | |
| 107 | 283 | echo "" |
| 108 | echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" | |
| 109 | echo "✅ Deployment Complete!" | |
| 110 | echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" | |
| 284 | echo -e "${YELLOW}Step 7: Verify Seeder${NC}" | |
| 285 | ||
| 286 | sleep 3 | |
| 287 | PANE_CONTENT=$(tmux capture-pane -t "$TMUX_SESSION:$TMUX_WINDOW" -p 2>/dev/null | tail -10) | |
| 288 | ||
| 289 | if echo "$PANE_CONTENT" | grep -qi "seed\|swarm\|listen"; then | |
| 290 | echo -e " ${GREEN}✓ Seeder appears to be running${NC}" | |
| 291 | else | |
| 292 | echo -e "${YELLOW} ⚠ Could not verify seeder started${NC}" | |
| 293 | echo " Check manually: tmux attach -t $TMUX_SESSION" | |
| 294 | fi | |
| 295 | ||
| 296 | # Get pear key | |
| 297 | PEAR_KEY=$(pear info "$CHANNEL" --json 2>/dev/null | grep -oP '"key":"pear://\K[^"]+' || echo "unknown") | |
| 298 | ||
| 299 | # ============================================================================ | |
| 300 | # STEP 8: UPDATE DEPLOYMENT HISTORY | |
| 301 | # ============================================================================ | |
| 302 | ||
| 111 | 303 | echo "" |
| 112 | echo "📋 App Details:" | |
| 113 | echo " Name: Hyper Wallet Desktop" | |
| 114 | echo " Key: $APP_KEY" | |
| 304 | echo -e "${YELLOW}Step 8: Update Deployment History${NC}" | |
| 305 | ||
| 306 | HISTORY_FILE="$PROJECT_DIR/.claude/deployment-history.json" | |
| 307 | mkdir -p "$(dirname "$HISTORY_FILE")" | |
| 308 | ||
| 309 | cat > "$HISTORY_FILE" << EOF | |
| 310 | { | |
| 311 | "mode": "$MODE", | |
| 312 | "channel": "$CHANNEL", | |
| 313 | "branch": "$CURRENT_BRANCH", | |
| 314 | "version": "$LATEST", | |
| 315 | "pear_key": "pear://$PEAR_KEY", | |
| 316 | "deployed_at": "$(date -Iseconds)", | |
| 317 | "commit_message": "$COMMIT_MSG", | |
| 318 | "tmux": "$TMUX_SESSION:$TMUX_WINDOW" | |
| 319 | } | |
| 320 | EOF | |
| 321 | ||
| 322 | echo -e " ${GREEN}✓ History updated: .claude/deployment-history.json${NC}" | |
| 323 | ||
| 324 | # ============================================================================ | |
| 325 | # SUMMARY | |
| 326 | # ============================================================================ | |
| 327 | ||
| 115 | 328 | echo "" |
| 116 | echo "🚀 Run the app:" | |
| 117 | echo " pear run $APP_KEY" | |
| 329 | echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" | |
| 330 | echo -e "${GREEN} Deployment Complete!${NC}" | |
| 331 | echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" | |
| 118 | 332 | echo "" |
| 119 | echo "🌱 Seed the app (if not already seeding):" | |
| 120 | echo " pear seed $APP_KEY" | |
| 333 | echo " Mode: $MODE" | |
| 334 | echo " Channel: $CHANNEL" | |
| 335 | echo " Branch: $CURRENT_BRANCH" | |
| 336 | echo " Version: $LATEST" | |
| 337 | if [ "$PEAR_KEY" != "unknown" ]; then | |
| 338 | echo " Key: pear://$PEAR_KEY" | |
| 339 | fi | |
| 340 | echo " Seeder: $TMUX_SESSION:$TMUX_WINDOW" | |
| 121 | 341 | echo "" |
| 122 | echo "📊 Check seeding status:" | |
| 123 | echo " tmux attach -t pear-seed" | |
| 342 | ||
| 343 | if [ "$MODE" == "testing" ]; then | |
| 344 | echo -e "${YELLOW}Next Steps (Testing):${NC}" | |
| 345 | echo " 1. Test on ashatron.local:" | |
| 346 | echo " pear run --checkout=latest pear://$PEAR_KEY" | |
| 347 | echo "" | |
| 348 | echo " 2. After testing verified, deploy to production:" | |
| 349 | echo " git checkout stable" | |
| 350 | echo " git merge testing" | |
| 351 | echo " ./scripts/pear-deploy.sh --production \"$COMMIT_MSG\"" | |
| 352 | else | |
| 353 | echo -e "${YELLOW}Next Steps (Production):${NC}" | |
| 354 | echo " 1. Verify on ashatron.local:" | |
| 355 | echo " pear run --checkout=latest pear://$PEAR_KEY" | |
| 356 | echo "" | |
| 357 | echo " 2. Monitor seeder:" | |
| 358 | echo " tmux attach -t $TMUX_SESSION" | |
| 359 | fi | |
| 124 | 360 | echo "" |
| … | … | @@ -440,6 +440,65 @@ const routes = { |
| 440 | 440 | } |
| 441 | 441 | }), |
| 442 | 442 | |
| 443 | // GET /api/balances/ethereum (protected) - Unified Ethereum view with all token balances | |
| 444 | 'GET /api/balances/ethereum': requireAuth(async (req, res) => { | |
| 445 | try { | |
| 446 | const walletMnemonic = loadWalletMnemonicCached() | |
| 447 | if (!walletMnemonic) { | |
| 448 | return sendError(res, 'Wallet not initialized', 400) | |
| 449 | } | |
| 450 | ||
| 451 | // Get current address index from storage | |
| 452 | const identity = await loadIdentity() | |
| 453 | await initializeStorage(identity) | |
| 454 | ||
| 455 | const currentAddr = await getCurrentAddress('ethereum') | |
| 456 | const accountIndex = currentAddr ? currentAddr.index : 0 | |
| 457 | ||
| 458 | // Initialize WDK for testnet (can be made configurable later) | |
| 459 | const { wdk } = await initializeWDK(walletMnemonic, { testnet: true }) | |
| 460 | const account = await wdk.getAccount('ethereum', accountIndex) | |
| 461 | const address = await account.getAddress() | |
| 462 | ||
| 463 | // Fetch all balances in parallel | |
| 464 | const [ethBalance, usdtBalance, usdcBalance] = await Promise.all([ | |
| 465 | account.getBalance(), | |
| 466 | account.getTokenBalance(TOKEN_CONTRACTS['ethereum-usdt'].testnet), | |
| 467 | account.getTokenBalance(TOKEN_CONTRACTS['ethereum-usdc'].testnet) | |
| 468 | ]) | |
| 469 | ||
| 470 | sendJSON(res, { | |
| 471 | address, | |
| 472 | network: 'testnet', | |
| 473 | balances: { | |
| 474 | eth: { | |
| 475 | balance: ethBalance.toString(), | |
| 476 | symbol: 'ETH', | |
| 477 | decimals: 18, | |
| 478 | name: 'Ethereum' | |
| 479 | }, | |
| 480 | usdt: { | |
| 481 | balance: usdtBalance.toString(), | |
| 482 | symbol: 'USDT', | |
| 483 | decimals: 6, | |
| 484 | name: 'Tether USD', | |
| 485 | contract: TOKEN_CONTRACTS['ethereum-usdt'].testnet | |
| 486 | }, | |
| 487 | usdc: { | |
| 488 | balance: usdcBalance.toString(), | |
| 489 | symbol: 'USDC', | |
| 490 | decimals: 6, | |
| 491 | name: 'USD Coin', | |
| 492 | contract: TOKEN_CONTRACTS['ethereum-usdc'].testnet | |
| 493 | } | |
| 494 | } | |
| 495 | }) | |
| 496 | } catch (err) { | |
| 497 | console.error('Get Ethereum balances error:', err) | |
| 498 | sendError(res, err.message) | |
| 499 | } | |
| 500 | }), | |
| 501 | ||
| 443 | 502 | // GET /api/address/:chain - Get current receiving address |
| 444 | 503 | // For tokens (e.g., ethereum-usdt), returns the base chain address (ethereum) |
| 445 | 504 | 'GET /api/address/': requireAuth(async (req, res, chain) => { |