OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-wallet-desktop commits / 4e844a4

USDT refactor: unified Ethereum view, Bitcoin sync fix, Alchemy token support
Deployment: testing (hyper-wallet-desktop-test) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
4e844a4b3564152592f06d4a0921c0cb54bc44c5
by plankatron <fleeky@gmail.com> · 2026-01-30T01:47:40.000Z

Changed files (6) +1422 -117

desktop/wallet.css +113 -0
……@@ -2015,3 +2015,116 @@ footer a:hover {
20152015 border-color: var(--accent-blue);
20162016 box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
20172017}
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}
desktop/wallet.js +182 -34
……@@ -1276,16 +1276,26 @@ async function sendToContactAddress(contactId, addressId) {
12761276 // Close contacts modal
12771277 closeContacts()
12781278
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
12831293
12841294 // Update wallet view
12851295 updateWalletView()
12861296
12871297 // Show send form
1288 await showSend(chain)
1298 await showSend(selectedCurrency)
12891299
12901300 // Pre-fill address
12911301 setTimeout(() => {
……@@ -1456,8 +1466,10 @@ const SUPPORTED_CHAINS = {
14561466 testnet: /^(tb1|[mn])[a-zA-HJ-NP-Z0-9]{25,62}$/,
14571467 mainnet: /^(bc1|[13])[a-zA-HJ-NP-Z0-9]{25,62}$/
14581468 },
1459 feeReserve: 500
1469 feeReserve: 500,
1470 tokens: null
14601471 },
1472
14611473 ethereum: {
14621474 name: 'Ethereum',
14631475 icon: 'Ξ',
……@@ -1465,7 +1477,7 @@ const SUPPORTED_CHAINS = {
14651477 mainnetName: 'Mainnet',
14661478 formatBalance: (balance) => {
14671479 const eth = Number(balance) / 1e18
1468 return `${eth.toFixed(18).replace(/\.?0+$/, '')} ETH`
1480 return `${eth.toFixed(6)} ETH`
14691481 },
14701482 formatSmallUnit: (balance) => {
14711483 return `${Number(balance).toLocaleString()} wei`
……@@ -1475,8 +1487,28 @@ const SUPPORTED_CHAINS = {
14751487 testnet: /^0x[a-fA-F0-9]{40}$/,
14761488 mainnet: /^0x[a-fA-F0-9]{40}$/
14771489 },
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 }
14791509 },
1510
1511 // Backwards compatibility: Keep flat entries for existing code paths
14801512 'ethereum-usdt': {
14811513 name: 'Ethereum - USDT',
14821514 icon: '₮',
……@@ -1498,22 +1530,22 @@ const SUPPORTED_CHAINS = {
14981530 isToken: true,
14991531 baseChain: 'ethereum'
15001532 }
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.
15131533}
15141534
1515// Currency state management
1516let selectedCurrency = localStorage.getItem('currency') || 'bitcoin'
1535// Chain and asset state management
1536let selectedChain = localStorage.getItem('chain') || 'bitcoin'
1537let selectedAsset = localStorage.getItem('asset') || null // null = native, 'usdt' = token
1538
1539// Backwards compatibility: selectedCurrency derived from selectedChain + selectedAsset
1540function getSelectedCurrency() {
1541 if (selectedAsset && selectedChain === 'ethereum') {
1542 return `ethereum-${selectedAsset}`
1543 }
1544 return selectedChain
1545}
1546
1547// Legacy support: set selectedCurrency getter
1548let selectedCurrency = getSelectedCurrency()
15171549
15181550// Price cache (keyed by CoinGecko ID)
15191551let prices = {}
……@@ -1554,21 +1586,33 @@ function updateNetworkLabels() {
15541586 updateWalletView()
15551587}
15561588
1557// Switch currency
1589// Switch currency (chain selector)
15581590function switchCurrency() {
15591591 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()
15621599
15631600 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
15651609 loadCurrentQRCode()
15661610 loadTransactions()
15671611}
15681612
1569// Update wallet view for selected currency
1613// Update wallet view for selected chain
15701614function updateWalletView() {
1571 const chain = SUPPORTED_CHAINS[selectedCurrency]
1615 const chain = SUPPORTED_CHAINS[selectedChain]
15721616 if (chain) {
15731617 const networkName = selectedNetwork === 'testnet' ? chain.testnetName : chain.mainnetName
15741618 document.getElementById('wallet-icon').textContent = chain.icon
……@@ -1699,6 +1743,97 @@ async function loadCurrentBalance() {
16991743 }
17001744}
17011745
1746// Load unified Ethereum balances (ETH + USDT + USDC)
1747async 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)
1818function 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
17021837// Load QR code for current currency
17031838async function loadCurrentQRCode() {
17041839 const qrContainer = document.getElementById('wallet-qr-container')
……@@ -3416,16 +3551,19 @@ async function init() {
34163551 // Clear existing options
34173552 currencySelector.innerHTML = ''
34183553
3419 // Add option for each supported chain
3554 // Add option for each base chain (not tokens)
34203555 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
34213559 const option = document.createElement('option')
34223560 option.value = chainId
34233561 option.textContent = `${chain.icon} ${chain.name}`
34243562 currencySelector.appendChild(option)
34253563 }
34263564
3427 // Set selected currency
3428 currencySelector.value = selectedCurrency
3565 // Set selected chain
3566 currencySelector.value = selectedChain
34293567 }
34303568
34313569 // Fetch prices first (don't wait)
……@@ -3433,11 +3571,16 @@ async function init() {
34333571
34343572 // Only load wallet data if initialized AND authenticated
34353573 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)
34373575 updateWalletView()
34383576
3577 // Use unified balance display for Ethereum
3578 const loadBalancePromise = selectedChain === 'ethereum'
3579 ? loadEthereumBalances()
3580 : loadCurrentBalance()
3581
34393582 await Promise.all([
3440 loadCurrentBalance(),
3583 loadBalancePromise,
34413584 loadCurrentQRCode(),
34423585 loadTransactions()
34433586 ])
……@@ -3448,7 +3591,12 @@ async function init() {
34483591 // Auto-refresh every 30 seconds
34493592 setInterval(() => {
34503593 if (document.visibilityState === 'visible') {
3451 loadCurrentBalance()
3594 // Use unified balance display for Ethereum
3595 if (selectedChain === 'ethereum') {
3596 loadEthereumBalances()
3597 } else {
3598 loadCurrentBalance()
3599 }
34523600 loadCurrentQRCode()
34533601 loadTransactions()
34543602 }
docs/DEPLOYMENT.md +135 -0
……@@ -0,0 +1,135 @@
1# Hyper Wallet Desktop - Deployment Process
2
3## Overview
4
5This 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
14Both channels share the same tmux window since only one is seeded at a time.
15
16## Pre-Deployment Checklist
17
18Before deploying, the script automatically verifies:
19
201. **Tests Pass** - All unit tests must pass (`bare test/all.cjs`)
212. **node_modules Exists** - Critical for Pear module resolution
223. **Critical Files Present** - index.cjs, wallet-server.cjs, desktop files
234. **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
33This will:
341. Run tests (abort if failing)
352. Commit changes (if any)
363. Stage the app (`pear stage hyper-wallet-desktop .`)
374. Release the app (`pear release hyper-wallet-desktop`)
385. Restart the seeder in tmux
396. Print summary with version info
40
41### Manual Steps (If Needed)
42
43```bash
44# 1. Run tests
45bare test/all.cjs
46
47# 2. Stage
48pear stage hyper-wallet-desktop .
49
50# 3. Release
51pear release hyper-wallet-desktop
52
53# 4. Restart seeder
54tmux send-keys -t pear-seed:hyper-wallet-desktop C-c
55sleep 2
56tmux send-keys -t pear-seed:hyper-wallet-desktop "pear seed hyper-wallet-desktop ." Enter
57```
58
59## Testing on Remote Device (ashatron.local)
60
61After deployment, test on ashatron.local:
62
63```bash
64# On ashatron.local
65pear run --checkout=latest pear://[KEY]
66```
67
68The `--checkout=latest` flag ensures the latest version is fetched.
69
70## Rollback
71
72If issues are found:
73
74```bash
75# Check release history
76pear 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
86The seeder runs in `pear-seed:hyper-wallet-desktop`:
87
88```bash
89# Check seeder status
90tmux list-windows -t pear-seed
91
92# Attach to seeder
93tmux 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
114tmux send-keys -t pear-seed:hyper-wallet-desktop "pear seed hyper-wallet-desktop ." Enter
115```
116
117## Version Tracking
118
119After 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
1311. **ALWAYS run tests before deploying**
1322. **ALWAYS use channel name** (`pear stage hyper-wallet-desktop .`, not `pear stage .`)
1333. **ALWAYS restart seeder** after release
1344. **NEVER ignore node_modules** in pear.json
1355. **NEVER tell users to clear cache** - fix deployment if updates don't work
docs/USDT_REFACTOR_PLAN.md +614 -0
……@@ -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
9This 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
131. **WDK Bitcoin** has public `getTransfers()` API - but code uses private `_electrumClient`
142. **WDK Ethereum** does NOT have transaction history API - external providers (Blockscout) are correct approach
153. **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
29if (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
43if (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
85From `@tetherto/wdk-wallet-btc` docs:
86
87```javascript
88const 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
178const 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
236let selectedChain = localStorage.getItem('chain') || 'bitcoin'
237let 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
246async 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
316function 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
333function 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
358async 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
482const currencySelector = document.getElementById('currency-selector')
483if (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
500function 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
526Add `'erc20'` to category array and implement token methods for Alchemy provider.
527
528```javascript
529alchemy: {
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")
scripts/pear-deploy.sh +319 -83
……@@ -1,124 +1,360 @@
11#!/bin/bash
22# 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
417
518set -e # Exit on error
619
7COMMIT_MSG="${1:-Update desktop app}"
20# ============================================================================
21# CONFIGURATION
22# ============================================================================
23
824PROJECT_DIR="/storage/projects/code/hypercore-kb/hyper-fleeky/hyper-wallet-desktop"
25TMUX_SESSION="pear-seed"
26TMUX_WINDOW="5" # hyper-wallet-desktop window
27
28# Channel configuration
29TESTING_CHANNEL="hyper-wallet-desktop-test"
30TESTING_BRANCH="testing"
31
32PRODUCTION_CHANNEL="hyper-wallet-desktop"
33PRODUCTION_BRANCH="stable"
34
35# Colors
36RED='\033[0;31m'
37GREEN='\033[0;32m'
38YELLOW='\033[1;33m'
39BLUE='\033[0;34m'
40NC='\033[0m'
41
42# ============================================================================
43# PARSE ARGUMENTS
44# ============================================================================
45
46MODE=""
47COMMIT_MSG=""
48SKIP_TESTS=false
49
50show_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
68while [[ $# -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
91done
92
93if [ -z "$MODE" ]; then
94 echo -e "${RED}Error: Must specify --testing or --production${NC}"
95 echo ""
96 show_usage
97fi
98
99if [ -z "$COMMIT_MSG" ]; then
100 echo -e "${RED}Error: Commit message required${NC}"
101 echo ""
102 show_usage
103fi
104
105# Set channel and branch based on mode
106if [ "$MODE" == "testing" ]; then
107 CHANNEL="$TESTING_CHANNEL"
108 EXPECTED_BRANCH="$TESTING_BRANCH"
109else
110 CHANNEL="$PRODUCTION_CHANNEL"
111 EXPECTED_BRANCH="$PRODUCTION_BRANCH"
112fi
9113
10114cd "$PROJECT_DIR"
11115
12echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
13echo "🚀 Hyper Wallet Desktop - Pear Deployment"
14echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
116# ============================================================================
117# HEADER
118# ============================================================================
119
120echo ""
121echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
122echo -e "${BLUE} Hyper Wallet Desktop - Pear Deployment${NC}"
123echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
124echo ""
125echo " Mode: $MODE"
126echo " Channel: $CHANNEL"
127echo " Branch: $EXPECTED_BRANCH"
15128echo ""
16129
17# 1. Verify node_modules exists (CRITICAL for Pear)
18if [ ! -d "node_modules" ]; then
19 echo "❌ ERROR: node_modules not found!"
20 echo " Run: npm install"
21 exit 1
22fi
130# ============================================================================
131# STEP 1: PRE-FLIGHT CHECKS
132# ============================================================================
23133
24echo "✓ node_modules verified ($(du -sh node_modules | cut -f1))"
134echo -e "${YELLOW}Step 1: Pre-flight Checks${NC}"
25135
26# 2. Verify critical files
27CRITICAL_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
137CURRENT_BRANCH=$(git branch --show-current)
138if [ "$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
143fi
144echo -e " ${GREEN}✓ On correct branch: $CURRENT_BRANCH${NC}"
36145
37for file in "${CRITICAL_FILES[@]}"; do
38 if [ ! -f "$file" ]; then
39 echo "❌ ERROR: Missing critical file: $file"
146# Check node_modules
147if [ ! -d "node_modules" ]; then
148 echo -e "${RED} ✗ node_modules not found!${NC}"
149 echo " Run: npm install"
40150 exit 1
41 fi
151fi
152echo -e " ${GREEN}✓ node_modules exists ($(du -sh node_modules 2>/dev/null | cut -f1))${NC}"
153
154# Check critical files
155CRITICAL_FILES=("index.cjs" "wallet-server.cjs" "pear.json" "desktop/index.html" "desktop/wallet.js")
156for file in "${CRITICAL_FILES[@]}"; do
157 if [ ! -f "$file" ]; then
158 echo -e "${RED} ✗ Missing: $file${NC}"
159 exit 1
160 fi
42161done
162echo -e " ${GREEN}✓ All critical files present${NC}"
43163
44echo "✓ All critical files present"
164# ============================================================================
165# STEP 2: RUN TESTS
166# ============================================================================
45167
46# 3. Stage the app
47168echo ""
48echo "📦 Staging app..."
49pear stage hyper-wallet-desktop .
169if [ "$SKIP_TESTS" = true ]; then
170 echo -e "${YELLOW}Step 2: Tests SKIPPED (--skip-tests)${NC}"
171else
172 echo -e "${YELLOW}Step 2: Running Tests${NC}"
173 echo " Running: bare test/all.cjs"
50174
51if [ $? -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
54182fi
55183
56# Get the new app key
57APP_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
188echo ""
189echo -e "${YELLOW}Step 3: Git Commit + Push${NC}"
190
191if [ -n "$(git status --porcelain)" ]; then
192 echo " Changes to commit:"
193 git status --short | sed 's/^/ /'
58194
59if [ -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
202Deployment: $MODE ($CHANNEL)
203
204Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
205EOF
206)"
207 echo -e " ${GREEN}✓ Committed${NC}"
208else
209 echo -e " ${GREEN}✓ No uncommitted changes${NC}"
210fi
211
212# Push to remote
213echo " Pushing to origin/$CURRENT_BRANCH..."
214if git push origin "$CURRENT_BRANCH" 2>&1; then
215 echo -e " ${GREEN}✓ Pushed to remote${NC}"
216else
217 echo -e "${YELLOW} ⚠ Push failed (continuing anyway)${NC}"
62218fi
63219
64echo "✓ Staged successfully"
65echo " Key: $APP_KEY"
220# ============================================================================
221# STEP 4: PEAR STAGE
222# ============================================================================
66223
67# 4. Release (make this version the default)
68224echo ""
69echo "🎯 Releasing app (making it the default version)..."
70pear release hyper-wallet-desktop
225echo -e "${YELLOW}Step 4: Pear Stage${NC}"
71226
72if [ $? -ne 0 ]; then
73 echo "❌ Release failed!"
74 exit 1
75fi
227echo " Running: pear stage $CHANNEL ."
228STAGE_OUTPUT=$(pear stage "$CHANNEL" . 2>&1)
229echo "$STAGE_OUTPUT" | sed 's/^/ /'
76230
77echo "✓ Released successfully"
231# Extract version
232LATEST=$(echo "$STAGE_OUTPUT" | grep -oP 'Latest:\s+\K\d+' || echo "unknown")
233echo -e " ${GREEN}✓ Staged version: $LATEST${NC}"
234
235# ============================================================================
236# STEP 5: PEAR RELEASE
237# ============================================================================
78238
79# 5. Optional: Seed in background
80239echo ""
81read -p "🌱 Start seeding in tmux? (y/N) " -n 1 -r
82echo
83if [[ $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
240echo -e "${YELLOW}Step 5: Pear Release${NC}"
97241
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
242echo " Running: pear release $CHANNEL"
243pear release "$CHANNEL" 2>&1 | sed 's/^/ /'
244echo -e " ${GREEN}✓ Released as default${NC}"
101245
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
250echo ""
251echo -e "${YELLOW}Step 6: Restart Seeder in Tmux${NC}"
252
253# Check tmux session exists
254if ! 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
104258fi
259echo -e " ${GREEN}✓ Tmux session exists: $TMUX_SESSION${NC}"
260
261# Check window exists
262if ! 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"
265fi
266echo -e " ${GREEN}✓ Tmux window exists: $TMUX_SESSION:$TMUX_WINDOW${NC}"
267
268# Stop existing seeder
269echo " Stopping existing seeder..."
270tmux send-keys -t "$TMUX_SESSION:$TMUX_WINDOW" C-c 2>/dev/null || true
271sleep 2
272
273# Start new seeder
274echo " Starting seeder: pear seed $CHANNEL ."
275tmux send-keys -t "$TMUX_SESSION:$TMUX_WINDOW" "cd $PROJECT_DIR && pear seed $CHANNEL ." Enter
276
277echo -e " ${GREEN}✓ Seeder command sent to $TMUX_SESSION:$TMUX_WINDOW${NC}"
278
279# ============================================================================
280# STEP 7: VERIFY SEEDER
281# ============================================================================
105282
106# 6. Summary
107283echo ""
108echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
109echo "✅ Deployment Complete!"
110echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
284echo -e "${YELLOW}Step 7: Verify Seeder${NC}"
285
286sleep 3
287PANE_CONTENT=$(tmux capture-pane -t "$TMUX_SESSION:$TMUX_WINDOW" -p 2>/dev/null | tail -10)
288
289if echo "$PANE_CONTENT" | grep -qi "seed\|swarm\|listen"; then
290 echo -e " ${GREEN}✓ Seeder appears to be running${NC}"
291else
292 echo -e "${YELLOW} ⚠ Could not verify seeder started${NC}"
293 echo " Check manually: tmux attach -t $TMUX_SESSION"
294fi
295
296# Get pear key
297PEAR_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
111303echo ""
112echo "📋 App Details:"
113echo " Name: Hyper Wallet Desktop"
114echo " Key: $APP_KEY"
304echo -e "${YELLOW}Step 8: Update Deployment History${NC}"
305
306HISTORY_FILE="$PROJECT_DIR/.claude/deployment-history.json"
307mkdir -p "$(dirname "$HISTORY_FILE")"
308
309cat > "$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}
320EOF
321
322echo -e " ${GREEN}✓ History updated: .claude/deployment-history.json${NC}"
323
324# ============================================================================
325# SUMMARY
326# ============================================================================
327
115328echo ""
116echo "🚀 Run the app:"
117echo " pear run $APP_KEY"
329echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
330echo -e "${GREEN} Deployment Complete!${NC}"
331echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
118332echo ""
119echo "🌱 Seed the app (if not already seeding):"
120echo " pear seed $APP_KEY"
333echo " Mode: $MODE"
334echo " Channel: $CHANNEL"
335echo " Branch: $CURRENT_BRANCH"
336echo " Version: $LATEST"
337if [ "$PEAR_KEY" != "unknown" ]; then
338 echo " Key: pear://$PEAR_KEY"
339fi
340echo " Seeder: $TMUX_SESSION:$TMUX_WINDOW"
121341echo ""
122echo "📊 Check seeding status:"
123echo " tmux attach -t pear-seed"
342
343if [ "$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\""
352else
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"
359fi
124360echo ""
wallet-server.cjs +59 -0
……@@ -440,6 +440,65 @@ const routes = {
440440 }
441441 }),
442442
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
443502 // GET /api/address/:chain - Get current receiving address
444503 // For tokens (e.g., ethereum-usdt), returns the base chain address (ethereum)
445504 'GET /api/address/': requireAuth(async (req, res, chain) => {