diff --git a/api1.js b/api1.js index b0d8f14..e4790c4 100644 --- a/api1.js +++ b/api1.js @@ -464,6 +464,60 @@ function registerTakerHandlers() { } }); + ipcMain.handle('taker:checkSwapLiquidity', async () => { + try { + if (!api1State.takerInstance) { + return { success: false, error: 'Taker not initialized' }; + } + + const balance = api1State.takerInstance.getBalances(); + const regular = Number(balance?.regular || 0); + const swap = Number(balance?.swap || 0); + const spendable = Number(balance?.spendable || 0); + + let maxSwappable = Math.max(regular, swap) - 3000; + + if ( + typeof api1State.takerInstance.checkSwapLiquidity === 'function' + ) { + try { + const nativeResult = api1State.takerInstance.checkSwapLiquidity(); + if (typeof nativeResult === 'number') { + maxSwappable = nativeResult; + } else if ( + nativeResult && + typeof nativeResult.maxSwappable === 'number' + ) { + maxSwappable = nativeResult.maxSwappable; + } else if ( + nativeResult && + typeof nativeResult.max_swappable === 'number' + ) { + maxSwappable = nativeResult.max_swappable; + } + } catch (nativeError) { + console.warn( + '⚠️ checkSwapLiquidity native call failed, using balance fallback:', + nativeError.message + ); + } + } + + return { + success: true, + liquidity: { + spendable, + regular, + swap, + maxSwappable: Math.max(0, Math.floor(maxSwappable)), + }, + }; + } catch (error) { + console.error('❌ Failed to check swap liquidity:', error); + return { success: false, error: error.message }; + } + }); + // Start periodic wallet sync (every 5 minutes) function startPeriodicWalletSync() { if (api1State.walletSyncInterval) { @@ -1344,6 +1398,64 @@ function registerDialogHandlers() { function registerTorHandlers() { const net = require('net'); + ipcMain.handle('network:testTcpPort', async (event, config) => { + const host = config?.host || '127.0.0.1'; + const port = config?.port; + const timeout = config?.timeout || 3000; + + return new Promise((resolve) => { + if (!port) { + resolve({ + success: false, + host, + port, + error: 'Port is required', + }); + return; + } + + const socket = new net.Socket(); + let settled = false; + + const finish = (result) => { + if (settled) return; + settled = true; + socket.destroy(); + resolve(result); + }; + + socket.setTimeout(timeout); + + socket.on('connect', () => { + finish({ + success: true, + host, + port, + }); + }); + + socket.on('error', () => { + finish({ + success: false, + host, + port, + error: `Cannot connect to ${host}:${port}`, + }); + }); + + socket.on('timeout', () => { + finish({ + success: false, + host, + port, + error: `Connection timeout to ${host}:${port}`, + }); + }); + + socket.connect(port, host); + }); + }); + ipcMain.handle('tor:testConnection', async (event, config) => { const socksPort = config?.socksPort || 9050; const controlPort = config?.controlPort || 9051; diff --git a/execution.md b/execution.md new file mode 100644 index 0000000..ffbc5f0 --- /dev/null +++ b/execution.md @@ -0,0 +1,229 @@ +# Execution Plan + +--- + +## 1. General + +| Task Name | Task Description | Code Site | Suggested Code Change | +|-----------|-----------------|-----------|----------------------| +| Update Font System | Replace current awkward fonts with standard system fonts used in Android/iOS apps (e.g., Inter, SF Pro, Roboto) | `app.js`, global CSS / Tailwind config | Add `font-family: 'Inter', system-ui, -apple-system, sans-serif` to the root stylesheet. Import Inter from Google Fonts or bundle it locally. | +| Update Icon System | Replace emoji-based icons with a proper icon library (e.g., Lucide, Heroicons, or Material Icons) used in modern mobile apps | All component files — `Nav.js`, `FirstTimeSetup.js`, `Recovery.js`, `Settings.js`, `Market.js`, etc. | Install `lucide` or `heroicons`. Replace all emoji/unicode icon usages (`ðŸ"§`, `âš ï¸`, `ðŸ"`, etc.) with SVG icon components from the chosen library. | +| Display All Amounts in Sats with ₿ Symbol | Change all BTC value displays to show amounts in satoshis using comma-separated formatting and ₿ as the unit symbol | `UtxoList.js` (`satsToBtc`), `TransactionsList.js` (`formatAmount`), `SwapReport.js` (`satsToBtc`), `SwapHistory.js` (`satsToBtc`), `Wallet.js`, `Send.js`, `Receive.js`, `Market.js` | Remove `satsToBtc()` conversion calls in display output. Replace with `formatSats(sats)` that returns `sats.toLocaleString() + ' ₿'`. E.g., `1,000,000 ₿` instead of `0.01000000 BTC`. Apply globally across all components. | + +--- + +## 2. Setup Flow — General + +| Task Name | Task Description | Code Site | Suggested Code Change | +|-----------|-----------------|-----------|----------------------| +| Update Screen Title and Subtitle | Change the setup modal header title to "Coinswap Client GUI" and subtitle to "Wallet and Other Setups." | `FirstTimeSetup.js` lines 16–18 | Change `

` text from `"Welcome to Coinswap Taker!"` to `"Coinswap Client GUI"`. Change `

` subtitle to `"Wallet and Other Setups."` | +| High-Contrast Step Indicator | Render the "Step N of 4" indicator in a high-contrast color to make it visually prominent | `FirstTimeSetup.js` line 23 (`step-indicator` span) | Style `#step-indicator` with a bright background e.g. `bg-white text-[#FF6B35] px-3 py-1 rounded-full font-bold`. | +| Upgrade Notice/Warning/Info Icons | Replace the small inline warning/info icons with larger, clearer icons from the updated icon library | `FirstTimeSetup.js` — all info/warning boxes using emoji (`âš ï¸`, `ℹï¸`, `🧅`, `ðŸ"‹`) | Replace emoji icons with `` icons from Lucide/Heroicons at `w-5 h-5` or `w-6 h-6`. Apply consistently across all info boxes. | +| Persist Step Data on Back Navigation | Prevent field values from resetting to defaults when navigating back to a previous step | `FirstTimeSetup.js` — `showStep()` function and the Back button handler (lines 1314–1329) | Store each step's field values in the existing `walletData` object whenever the user advances. In `showStep()`, re-populate fields from `walletData` when rendering a step that was previously completed. | + +--- + +## 3. Setup Flow — Step 2 (Bitcoin Endpoints) + +| Task Name | Task Description | Code Site | Suggested Code Change | +|-----------|-----------------|-----------|----------------------| +| Update Step 2 Title and Subtitle | Change title to "Bitcoin Endpoints" and subtitle to "Connect to a running bitcoind RPC+REST & ZMQ Ports. This is needed to sync the wallet and market data." | `FirstTimeSetup.js` lines 109–112 (`step-2` section) | Update `

` to `"Bitcoin Endpoints"` and `

` subtitle to the new copy. | +| Replace Dual ZMQ Fields with Single ZMQ Port Field | Remove the two ZMQ full-URL inputs (`setup-zmq-rawblock`, `setup-zmq-rawtx`) and replace with a single ZMQ port number field. Prepend `tcp://127.0.0.1:` to the port value internally. | `FirstTimeSetup.js` lines 179–201 (ZMQ Notifications section), `buildConfiguration()` function | Remove both ZMQ URL inputs. Add single ``. In `buildConfiguration()`, construct `zmqRawBlock` and `zmqRawTx` as `` `tcp://127.0.0.1:${zmqPort}` ``. | +| Rename Test Button and Test All Three Connections | Change "Test RPC Connection" button label to "Test Node Connection" and test RPC, REST, and ZMQ endpoints in the test handler | `FirstTimeSetup.js` line 203 (`test-rpc-setup` button), `testRPCConnection()` function | Change button label to `"Test Node Connection"`. In the click handler, fire three checks: RPC via existing method, REST endpoint (e.g. `http://host:restport/`), and ZMQ port availability. Display pass/fail for each. | +| Update Notice Message | Change the info/notice text to: "Info: Don't have a running Bitcoin Node? Follow these instructions to setup your own node." with a link | `FirstTimeSetup.js` lines 209–221 (yellow info box) | Replace the existing notice text with the new copy and a hyperlink to node setup instructions. | + +--- + +## 4. Setup Flow — Step 3 (Wallet) + +| Task Name | Task Description | Code Site | Suggested Code Change | +|-----------|-----------------|-----------|----------------------| +| Update Step 3 Title and Remove Subtitle | Change title to "Choose A Wallet. Or Create a New One." Remove any subtitle text. | `FirstTimeSetup.js` lines 226–229 (`step-3a` section) | Update `

` text. Remove or empty the `

` subtitle element. | +| Remove Skip Encryption Checkbox | Remove the "Skip encryption" checkbox from the Create Wallet view. Empty password should implicitly mean unencrypted. | `FirstTimeSetup.js` lines 341–350 (`skip-encryption` checkbox), lines 1169–1183 (event listener) | Delete the `skip-encryption` checkbox HTML block and its `change` event listener. | +| Add Subtext Under Wallet Password Label | Add subtext under the "Wallet Password" header in Create Wallet: "Leaving it empty will create unencrypted wallet file" | `FirstTimeSetup.js` lines 290–314 (create password block) | After the ``, add `

Leaving it empty will create unencrypted wallet file

`. | +| Remove No-Password Checkbox from Restore | Remove the "Backup has no password" checkbox from the Restore from Backup view. | `FirstTimeSetup.js` lines 479–488 (`restore-no-password` checkbox), lines 1227–1238 (event listener) | Delete the `restore-no-password` checkbox HTML and its `change` event listener. | +| Update Restore Note Text | Change the restore note to: "Note: Restoring will re-sync the wallet from wallet-birthday. This can take some time." | `FirstTimeSetup.js` lines 495–499 (purple info box in restore section) | Replace existing note text with the updated copy. | + +--- + +## 5. Setup Flow — Step 4 (Tor — reorder to Step 3) + +| Task Name | Task Description | Code Site | Suggested Code Change | +|-----------|-----------------|-----------|----------------------| +| Move Tor Setup to Step 3 | Reorder setup steps so Tor configuration is Step 3 and Wallet setup becomes Step 4 | `FirstTimeSetup.js` — `showStep()` function, step numbering logic, `currentStep` navigation, and `validateStep3()` / `buildConfiguration()` | Swap the rendering order in `showStep()`: render the Tor step (`step-4`) when `currentStep === 3` and wallet steps (`step-3a/b`) when `currentStep === 4`. Update all validation and config-building references accordingly. Update `totalSteps` if needed. | +| Update Tor Step Subtitle | Change subtitle to: "Connect with the Tor Proxy. This is needed for all network communications." | `FirstTimeSetup.js` lines 505–508 (`step-4` header section) | Update `

` subtitle text inside `step-4` header div. | +| Remove Subtext from Port Fields | Remove the helper subtext paragraphs under Control Port and SOCKS Port fields | `FirstTimeSetup.js` lines 524–525 and 536–537 (port subtext `

` tags) | Delete the `

` elements under both port inputs. | +| Remove Subtext from Password Field | Remove the helper subtext under the Tor Auth Password field | `FirstTimeSetup.js` line 565 (`

`) | Delete that subtext `

` element. | +| Update Tor Test to Check Both Ports | Ensure the Tor test button checks both SOCKS port and control port availability | `FirstTimeSetup.js` `testTorConnection()` function | Extend the test handler to validate both `setup-tor-socks-port` and `setup-tor-control-port` connections and show per-port pass/fail status in `#tor-test-result`. | +| Remove Privacy Notice | Remove the purple privacy notice box ("All maker connections go through Tor…") | `FirstTimeSetup.js` lines 574–583 (purple info box with bullet list) | Delete the entire purple privacy notice `

` block. | +| Add New Tor Setup Info | Add a new info message: "Info: Don't have a running Tor instance? Use these instructions to set up." with a setup link | `FirstTimeSetup.js` — Tor step section, after the test result div | Insert a new blue info box after `#tor-test-result` with the new copy and a hyperlink to Tor setup instructions. | + +--- + +## 6. Wallet Page + +| Task Name | Task Description | Code Site | Suggested Code Change | +|-----------|-----------------|-----------|----------------------| +| Rename Refresh Button | Change the top-right refresh button label to "Refresh" or replace it with just a circular arrow icon | `Wallet.js` — refresh/reload button | Update the button's `textContent` or `innerHTML` to `"Refresh"` or an SVG circular icon. Apply the same pattern to all other page refresh buttons (Market, Send, Receive, etc.). | + +--- + +## 7. Wallet Page — All UTXOs + +| Task Name | Task Description | Code Site | Suggested Code Change | +|-----------|-----------------|-----------|----------------------| +| Remove P2WPKH/P2WSH/P2TR Filter Buttons | Remove the script-type filter buttons and their associated filter logic. Show script type only as a column in the table. | `UtxoList.js` lines 175–189 (`updateFilterButtons()`), lines 155–173 (`applyFilter()`), HTML filter button block | Delete the script-type filter button HTML. Remove `activeTypeFilter` state and `applyFilter()` by script type. Keep the script type column in the table rows. | +| Add New Spend-Type Filters | Add new filter buttons: Regular UTXOs, Contract UTXOs, Swap UTXOs, Spendable UTXOs | `UtxoList.js` — `applyFilter()` function and filter button HTML | Replace old script-type filters with new spend-type filters. In `applyFilter()`, filter by `spendInfo.spendType`: Regular, Contract, Swap. "Spendable" filters for UTXOs that are not locked in contracts. | +| Remove "Filter by Script Type" Label | Remove any label text before the filter buttons | `UtxoList.js` — filter section HTML | Delete any `` or `

` element containing "Filter by Script Type" text. | +| Rename Spend Type Column to "Type" | Rename the "Spend Type" column header to just "Type" with values Regular, Swap, or Contract | `UtxoList.js` — table header HTML and `getUtxoTypeColor()` display logic | Update column header text. In row rendering, map spendType values to simple labels: `Regular`, `Swap`, `Contract`. | +| Add Sorting: Newest and Amount | Add sort controls to sort UTXOs by newest first or by amount | `UtxoList.js` — add sort state and sort logic in `filteredUtxos` pipeline | Add `sortBy` state variable. Add two sort buttons (or a dropdown). In the rendering pipeline, sort `filteredUtxos` by `utxo.confirmations` (newest = lowest/0 first) or by `utxo.amount` descending before rendering. | + +--- + +## 8. Wallet Page — All Transactions + +| Task Name | Task Description | Code Site | Suggested Code Change | +|-----------|-----------------|-----------|----------------------| +| Update Summary Items to Received, Sent, Swaps | Change the top summary stat cards to show only: Received, Sent, Swaps — remove Total Transactions and Net Balance cards | `TransactionsList.js` lines 380–397 (stats grid) and `updateStats()` (lines 337–359) | Remove the "Total Transactions" and "Net Balance" cards from the grid. Keep or rename "Total Received", "Total Sent", and add "Total Swaps" stats card. Update `updateStats()` to populate these. | +| Remove Debug Tip Text | Remove the bottom dev/debug tip box: "Tip: Check browser console for transaction type breakdown…" | `TransactionsList.js` lines 433–436 | Delete the entire `

` tip block. | +| Add Sorting: Newest and Amount | Add sort controls for transactions — newest first and by amount | `TransactionsList.js` — `getFilteredTransactions()` and HTML header area | Transactions are already sorted newest-first via `sortTransactionsByTime()`. Add an "Amount" sort option. Add a sort toggle button in the UI. In `getFilteredTransactions()`, conditionally sort by `tx.detail.amount.sats` descending when amount sort is active. | + +--- + +## 9. Market Page + +| Task Name | Task Description | Code Site | Suggested Code Change | +|-----------|-----------------|-----------|----------------------| +| Remove Top Warning Message | Remove the warning/info message currently shown at the top of the market page (if present) | `Market.js` — top of `content.innerHTML` template (~line 819) | Check for and delete any warning box HTML at the top of the market content. (Currently no explicit warning div is present; confirm and skip if not found.) | +| Remove Maker Data Row | Remove the "Maker Data" informational row if present | `Market.js` — `content.innerHTML` | Locate and remove any "Maker Data" row or info section in the market HTML template. | +| Rename "Refund Lock Time" to "Refund Locktime" | Fix the label in the Fee Calculation section | `Market.js` lines 843–851 (fee calculation box) | Change `"Refund Lock Time"` to `"Refund Locktime"` in the fee formula text. | +| Add Refund Locktime Explanation | Add text below the fee calculation box: "Refund Locktime depends on the position of a maker in swap circuit. Calculated as 20*(n+1), where n = index position of the maker in the swap circuit." | `Market.js` — after the fee calculation `` block (~line 848) | Insert a new `

` with the explanation text after the formula block. | +| Remove "Lower Fee means..." Text | Remove the explanatory text that starts with "Lower fees mean cheaper swaps…" | `Market.js` lines 847–850 | Delete the `

Lower fees mean...` paragraph. | +| Remove Average Fee Summary Card | Remove the "Average Fee" stat card from the summary tab area | `Market.js` `updateUI()` function lines 703–718 and `calculateStats()` | Remove the Average Fee card from `statsContainer.innerHTML`. Remove `avgFee` from `calculateStats()` return value. Remove the grid column. | +| Add Total Fidelity Locked Amount Card | Add a new summary card showing total fidelity bond amount locked across all makers | `Market.js` `calculateStats()` and `updateUI()` | In `calculateStats()`, compute `totalFidelity = displayedMakers.reduce((sum, m) => sum + m.bond, 0)`. Add a new card in `statsContainer.innerHTML` displaying this value. | +| Add Nostr Relays Card | Add a summary card showing the number of Nostr relays used for market sync | `Market.js` `updateUI()` summary section | Add a "Nostr Relays" stat card. Source relay count from API (if available via `window.api.taker.getOffers()` response or a new endpoint). Display statically if API doesn't expose it yet. | +| Remove Online Makers Count Card | Remove the "Online Makers" stat card from the summary section | `Market.js` `updateUI()` lines 713–716 | Delete the "Online Makers" card from `statsContainer.innerHTML`. Remove `onlineMakers` from `calculateStats()`. | +| Rename "Maker Address" Column to "Tor Address" | Update the column header in the maker table | `Market.js` line 898 (`

Maker Address
`) | Change text to `"Tor Address"`. | +| Reduce Table Header Font Size | Make all table column header text smaller so it fits in a single line | `Market.js` lines 892–905 (header grid row) | Add `text-xs` class to the header grid row or each `
` cell. Adjust column widths if needed. | + +--- + +## 10. Market Page — Fidelity Bond Details + +| Task Name | Task Description | Code Site | Suggested Code Change | +|-----------|-----------------|-----------|----------------------| +| Rename "Maker Address" to "Tor Address" in Detail Modal | Update label in the fidelity bond detail modal | `Market.js` line 450 (`

Maker Address

`) | Change label text to `"Tor Address"`. | +| Simplify Bond Summary to Three Fields | Show only: Bond Amount, Bond Status, Expires In (days estimate). Remove all other fields. | `Market.js` `viewFidelityBond()` function lines 441–548 | Remove grids for Bond Outpoint, Confirmation Height, Bond Public Key, Required Confirmations, Minimum Locktime, Certificate Expiry detail rows. Keep Bond Amount, Bond Status, and compute `expiresInDays` from `bondLocktime` (blocks ÷ 144) to show "~X days". | +| Make Bond Txid Clickable | Make the Bond Txid open mempool.space in an external browser | `Market.js` — bond modal | In the simplified bond summary, add the `bondTxid` as a clickable element: `onclick="window.open('https://mempool.space/.../tx/${maker.bondTxid}', '_blank')"` with an underline style. | +| Remove "View on Block Explorer" Button | Remove the dedicated explorer button from the bond detail modal | `Market.js` lines 529–536 (explorer button block) | Delete the `
` containing the "View on Block Explorer" button. | + +--- + +## 11. Send Page + +| Task Name | Task Description | Code Site | Suggested Code Change | +|-----------|-----------------|-----------|----------------------| +| Update Page Title to "Send" | Ensure the page title is simply "Send" | `Send.js` — page header | Confirm `

` text is `"Send"`. Update if it says something else. | +| Bug Fix: Manual UTXO Selection — Change Amount and Remaining Balance | Fix bug where Change Amount and Remaining Balance are not displayed when UTXOs are manually selected | `Send.js` — `updateSummary()` function and manual UTXO selection flow | In `updateSummary()`, ensure the `selectionMode === 'manual'` branch correctly computes and renders `changeAmount = selectedUtxosTotal - totalToSend - fee` and `remainingBalance = availableBalance - selectedUtxosTotal`. Check that summary DOM elements are not hidden when in manual mode. | +| Calculate and Display Estimated Time | Replace "Est. Time" with a real estimate: for mainnet/testnet use fee-based block estimation; for signet hardcode 30 secs; for regtest show "N/A" | `Send.js` — summary section, `updateSummary()` | Detect the network from `bitcoinNetwork` config. For mainnet/testnet: compute `estimatedBlocks = 1` (at selected fee rate) and display `~10 min`. For signet: display `"~30 sec"`. For regtest: display `"N/A"`. | +| Remove RBF Toggle | Remove the RBF (Replace-By-Fee) option since it is disabled for all transactions | `Send.js` — RBF checkbox/toggle in HTML and its logic | Delete the RBF checkbox HTML element and any associated state or logic. | +| Remove "Sign first… then broadcast" Info | Remove the info text "Sign first… then broadcast" from the UI | `Send.js` — info box HTML | Locate and delete the info box containing this text. | + +--- + +## 12. Receive Page — All Addresses + +| Task Name | Task Description | Code Site | Suggested Code Change | +|-----------|-----------------|-----------|----------------------| +| Remove "Total Addresses" Stat Card | Remove the Total Addresses stat card since it always equals Used Addresses | `AddressList.js` lines 211–215 (Total Addresses card in stats grid) | Delete that stat card `
` block. Update grid from `grid-cols-4` to `grid-cols-3`. | +| Replace Filter by Address Type with Spend-Type Filters | Remove address-type filters (P2WPKH, P2TR etc.) and replace with: Regular, Contract, Swap | `AddressList.js` lines 230–260 (filter section), `getFilteredAddresses()`, `detectAddressType()` | Update `currentFilter` values to `regular`, `contract`, `swap`. Update `getFilteredAddresses()` to filter by address spend type (sourced from the API response or address metadata) instead of script type. Update filter button labels. | +| Remove Privacy Tip | Remove the privacy tip text from the All Addresses view | `AddressList.js` — any privacy tip element in the address list HTML | Locate and delete any privacy tip info box in the `render()` HTML output. | +| Bug Fix: Unknown Address Types | Fix the bug where some address types display as "Unknown" — address type should always be determinable | `AddressList.js` lines 50–58 (`detectAddressType()`), `Receive.js` lines 145–153 | Extend `detectAddressType()` to handle regtest/signet prefixes (`bcrt1p` for P2TR, `tb1p` for testnet P2TR, `bcrt1q` for regtest P2WPKH). Add fallback from UTXO spend type data if address prefix matching fails. | +| Remove Status Column | Remove the Status column from the address table since unused addresses are not shown | `AddressList.js` — table header and row rendering with `getStatusInfo()` | Delete the Status column from table headers. Remove the status badge from each address row. Remove `getStatusInfo()` call from render. | + +--- + +## 13. Swap Page + +| Task Name | Task Description | Code Site | Suggested Code Change | +|-----------|-----------------|-----------|----------------------| +| Remove "You Can Only Swap With..." Notice | Remove the obsolete notice about swap restrictions | `Swap.js` — top of content HTML template | Locate and delete the notice box with this text. | +| Add Minimum Hop Warning | Add: "Warning: If swap with only one maker, the maker can deanonymize you. Recommended minimum hop = 2." | `Swap.js` — top of content HTML, after the removed notice | Insert a yellow warning box `
` with the new warning text. | +| Rename "Selection Mode" to "Select UTXOs" | Rename the UTXO selection mode label | `Swap.js` — UTXO selection section HTML | Change any label/header text reading "Selection Mode" to "Select UTXOs". | +| Bug Fix: Max Swap Amount Calculation | Fix the max swap amount bug — it should be `max(regular_balance, swap_balance) - 3000 sats` using the same logic as `taker::api::check_swap_liquidity` | `Swap.js` — `fetchUtxos()` and swap amount max calculation. `api1.js` — add a new API call | Add a new IPC-exposed function in `api1.js` that calls `taker::api::check_swap_liquidity` (or equivalent). In `Swap.js`, call this to get the correct max swappable amount. Display it as the upper bound for the amount input. | +| High-Contrast Available Makers Display | Make the "Available Makers" count bigger and brighter/high-contrast | `Swap.js` — available makers display element | Increase font size to `text-2xl` or `text-3xl`, use `text-[#FF6B35]` or `text-white font-bold` styling. | +| Rename "Available Balance" to "Swappable Balance" | Update the label in Swap Summary | `Swap.js` — swap summary section HTML | Change label text from `"Available Balance"` to `"Swappable Balance"`. | +| Add Selected Makers' Addresses to Summary | Display the Tor addresses of selected makers in the Swap Summary | `Swap.js` — swap summary section | Add a "Selected Makers" row in the summary. Populate with `.address` from each selected maker object in `availableMakers`. | +| Update Estimated Time Calculation | Compute estimated swap time as `BlockInterval * nHops` reflecting current network block time | `Swap.js` — swap summary `estimatedTime` calculation | Determine block interval from network config (signet=30s, mainnet=600s, regtest=0). Compute `estimatedTime = blockInterval * numberOfHops` and display in minutes/seconds. | +| Improve Maker Fee Estimation | Calculate exact maker fees per hop position using the deterministic formula instead of a rough estimate | `Swap.js` — maker fee display in summary | For each maker at hop position `i`, compute fee as `baseFee + amount * volumeFeePct + refundLocktime(i) * amount * timeFeePct` where `refundLocktime(i) = 20 * (i + 1)`. Sum across all hops. Display exact value in sats. | +| Add Number of Funding Transactions | Display the number of funding transactions in the Swap Summary | `Swap.js` — swap summary | Add a "Funding Txs" row. Value = `numberOfHops` (one funding tx per hop). | +| Add Funding Tx Average Size | Display funding tx average size in the Swap Summary (assume 2 inputs, rest is deterministic) | `Swap.js` — swap summary | Compute `avgFundingTxSize` based on 2 inputs + deterministic output structure (approx 250–400 vB). Display in the summary. | +| Calculate and Display Exact Network Fee | Show exact network fee as `nFundingTx * avgTxSize * feeRate` | `Swap.js` — swap summary | Add "Network Fee" row. Compute as `numberOfHops * avgFundingTxSize * networkFeeRate`. Display in sats. | +| Remove "Privacy Benefits" Info Box | Remove the info box about privacy benefits from the swap initiation screen | `Swap.js` — swap summary or initiation section | Locate and delete the info box with "Privacy Benefits" text. | + +--- + +## 14. Swap Page — Recent Swaps / Swap History + +| Task Name | Task Description | Code Site | Suggested Code Change | +|-----------|-----------------|-----------|----------------------| +| Expand Recent Swaps to Full Table Layout on Main Swap Page | Replace the compact Recent Swaps section with a full table layout directly on the Swap page. Remove the separate Swap History page navigation. | `Swap.js` — recent swaps section at bottom, `SwapHistory.js` | In `Swap.js`, replace the "Recent Swaps" collapsed section with the full table layout from `SwapHistoryComponent`. Inline the history display rather than navigating to `SwapHistory.js`. | +| Move Swap History Data to Main Swap Page | All history data (stats + list) should live on the main swap page | `Swap.js`, `SwapHistory.js` | Call `loadSwapHistory()` within `SwapComponent` and render summary stats + swap rows inline on the main swap page. | +| Rename "Total Volume" to "Total Amount" | Update the stat label in swap history summary | `SwapHistory.js` line 226 | Change `"Total Volume"` text to `"Total Amount"`. | +| Replace "Avg Hops" with "Avg Fee Paid" | Remove the avg hops metric and replace with avg fee paid | `SwapHistory.js` lines 181–186 (`avgHops` calculation) and lines 232–235 (display card) | Remove `avgHops` calculation. Compute `avgFeePaid = totalFees / totalSwaps`. Update the stat card label to `"Avg Fee Paid"` and display in sats. | +| Remove "Clear History" Button | Remove the Clear History button — swap history must always be preserved | `SwapHistory.js` lines 204–211 (conditional Clear History button), lines 265–276 (event listener) | Delete the Clear History button HTML and its click event listener. | +| Include Failed Swaps in History | Show failed swap histories in addition to completed ones | `SwapHistory.js` line 14 (`.filter(report => report.status === 'completed')`) | Remove the `.filter()` that excludes non-completed swaps. Add a status badge (e.g. "Failed" in red) in the row rendering to visually distinguish failed swaps. Confirm failed swap data is available from `window.api.swapReports.getAll()`. | +| Categorise Swaps by Protocol | Group or label swaps by their protocol (Legacy P2WSH vs Taproot) | `SwapHistory.js` — row rendering in `buildSwapHistoryList()` | Add a protocol badge (e.g. "Legacy" / "Taproot") to each swap row derived from `report.protocol` or equivalent field. Consider grouping rows by protocol with a section header if the field is available. | + +--- + +## 15. Coinswap Report + +| Task Name | Task Description | Code Site | Suggested Code Change | +|-----------|-----------------|-----------|----------------------| +| Update Subtitle | Change subtitle to: "View Detailed Swap Data." | `SwapReport.js` — page header subtitle | Add or update `

` subtitle in the report header. | +| Change "Back To Wallet" to "Back to Swaps" | Update the back navigation button label | `SwapReport.js` — back button HTML and `app.js` navigation target (line 19 references `wallet`) | Change button text to `"Back to Swaps"`. Update the `onclick` handler to navigate to the Swap page instead of the Wallet page. | +| Rename "Transaction Flow" to "Swap Circuit" | Change the section heading | `SwapReport.js` — swap flow/circuit section heading | Change `"Transaction Flow"` text to `"Swap Circuit"`. | +| Replace Link Icon Before "Transaction Flow" | Replace the link/chain icon with a circuit-style icon | `SwapReport.js` — icon before the Transaction Flow heading | Replace the link icon SVG with a circuit SVG icon (e.g. from Lucide `Circuit` or a custom SVG). | +| Fix Swap Graphics Alignment | Fix overlapping graphics and misaligned arrows in the swap circuit diagram | `SwapReport.js` — swap diagram/graphics HTML and CSS | Audit the diagram container. Use `flexbox` or `grid` layout with explicit widths. Center arrows using `margin: auto` or `absolute` positioning within their containers. | +| Update Taproot Privacy Info Box | Update the info box just below the swap graphics with new content | `SwapReport.js` — Taproot info box HTML | Replace content with: "Save Money: Lesser Fees than V1 swaps." / "Efficient: Combined tapscript with Musig2 + HTLC leaves." / "Anonymity Set — Legacy: All P2WSH UTXOs." / "Anonymity Set — Taproot: All Taproot Single Sig UTXOs." | +| Bug Fix: On-Chain Txs Count | Fix the bug showing wrong on-chain tx count (showing 2 for a 3-hop swap) | `SwapReport.js` — `report.totalFundingTxs` rendering | Verify `totalFundingTxs` is being read from the correct field in the report object (check both camelCase and snake_case variants in the normalization block lines 25–52). Ensure the display uses `report.totalFundingTxs` correctly. | +| Remove Funding Tx Info Box | Remove the info box from the Funding Transactions section | `SwapReport.js` — Funding Transactions section | Locate and delete the info box HTML within the Funding Transactions area. | +| Remove Privacy Details from Funding Tx Section | Remove "Privacy Details" subsection from Funding Transactions | `SwapReport.js` — Funding Transactions section | Delete the Privacy Details element from the funding transactions section. | +| Simplify UTXO Summary to Two Items | Show only: Outgoing Regular/Swap UTXOs and Incoming Swap UTXOs. Remove everything else. | `SwapReport.js` — UTXO Summary section | Remove all other UTXO detail rows. Keep only two rows: "Outgoing Regular/Swap UTXOs" (determine type from wallet data) and "Incoming Swap UTXOs". Populate from `report.inputUtxos` and `report.outputSwapUtxos`. | +| Rename "Done" Button to "Back to Swaps" | Update the final/done button label and navigation | `SwapReport.js` — done/close button | Change button text from any "Done..." variant to `"Back to Swaps"` and update click handler to navigate to the Swap page. | + +--- + +## 16. Recovery Page + +| Task Name | Task Description | Code Site | Suggested Code Change | +|-----------|-----------------|-----------|----------------------| +| Reorder Layout: How Recovery Works as Main Text | Move "How Recovery Works" to be the primary/first content on the page | `Recovery.js` lines 35–61 (How Recovery Works block) | Restructure layout to place the How Recovery Works section at the top of the page, not in a side column. | +| Move Recovery Stats to Main Summary View | Elevate Recovery Stats to be the primary summary section visible at the top | `Recovery.js` lines 64–80 (Recovery Stats block) | Move Recovery Stats block to be prominently displayed below the How Recovery Works text, as a horizontal summary bar. | +| Move Trigger Recovery to Top-Right Button | Replace the current centered/embedded layout of the manual recovery button with a top-right corner button | `Recovery.js` lines 25–28 (`manual-recovery-btn`) | Move the recovery button HTML to a top-right position using `absolute top-4 right-4` or a header flexbox row. Update styling to a standard top-action button. | +| Update "How Recovery Works" Copy | Replace the current step-list text with the new copy | `Recovery.js` lines 37–54 (numbered steps) | Replace step text with: (1) "The Recovery routine detects failed swaps, waits for HTLC timelock expiry then creates and broadcasts a refund transaction back to wallet." (2) "It might take several hours for timelock to expire." (3) "Recovery is automatically triggered for any unspent swap contract transactions at wallet startup. If you still see pending recoveries here, use the Trigger Recovery button to manually trigger a recovery." (4) "While waiting for recovery the app can be safely closed. Recovery will resume in next restart." (5) "Always ensure to not have very old pending recoveries. That can put your funds at risk." | + +--- + +## 17. Log Page + +| Task Name | Task Description | Code Site | Suggested Code Change | +|-----------|-----------------|-----------|----------------------| +| Remove Bottom-Right Info Text | Remove the performance warning info box at the bottom right of the Log page | `Log.js` lines 216–221 (yellow performance warning box) | Delete the `

` warning block. | + +--- + +## 18. Settings Page + +| Task Name | Task Description | Code Site | Suggested Code Change | +|-----------|-----------------|-----------|----------------------| +| Replace Wallet Backup Description with Single Text Block | Replace bullet-point backup tips with a single consolidated description paragraph | `Settings.js` lines 26–37 (blue tips info box) | Replace the tips `
    ` block with a single `

    ` containing: "Wallet Backup is an encrypted json file that restores your coinswap wallet in any client app. The backup file contains all data related to swaps to restore swap histories. The backup file can also be used to migrate your coinswap wallet from one client app to another. Always use a strong password for the backup file, or else your seed phrase can be compromised. Use the same password while restoring wallet from backup." | +| Consolidate Taker Config into Single Compact Section | Merge all Taker configuration options (Tor, Bitcoin RPC, ZMQ) into one compact section | `Settings.js` lines 40–285 (separate Taker Config, Bitcoin Core RPC, ZMQ sections) | Combine Tor config, Bitcoin RPC config, and ZMQ config into a single "Node & Network Configuration" section. Use a compact grid layout instead of large spaced-out blocks. | +| Add Test Tor Button | Add a "Test Tor" button that checks both control and SOCKS ports | `Settings.js` — Tor Configuration section | Add a ``. In its click handler, test both `tor-control-port-input` and `tor-socks-port-input` connections and display per-port pass/fail results. | +| Rename Bitcoin Test Button to "Test Bitcoind" and Test RPC + REST + ZMQ | Rename the test button and expand its test to cover RPC, REST, and ZMQ | `Settings.js` lines 199–201 (`test-connection-btn`) | Change button label to `"Test Bitcoind"`. Extend the test handler to check RPC, REST endpoint, and ZMQ port. Display individual pass/fail for each. | +| Remove Connect and Disconnect Buttons | Remove the Connect and Disconnect buttons from the Bitcoin settings | `Settings.js` lines 203–210 (`connect-btn`, `disconnect-btn`) | Delete both button HTML elements and their event listeners. | +| Remove Refresh Status Button | Remove the "Refresh Status" button | `Settings.js` lines 212–214 (`refresh-status-btn`) | Delete the Refresh Status button HTML and its event listener. | +| Replace Dual ZMQ Fields with Single ZMQ Port Field | Replace the two full ZMQ URL inputs with a single ZMQ port number field | `Settings.js` lines 228–254 (ZMQ Endpoints section) | Remove `zmq-rawblock-input` and `zmq-rawtx-input` full URL fields. Add single ``. Construct full URLs internally when saving settings. | +| Add `rest=1` to Bitcoin.conf Reference | Add the `rest=1` setting to the complete bitcoin.conf reference block | `Settings.js` lines 292–332 (`full-config-preview`) | Insert `rest=1` line in the `[signet]` and `[regtest]` sections of the conf reference block. | +| Remove All Other Info Boxes from Settings | Remove all remaining info/tip boxes from the Settings page (ZMQ warning, blue tips, etc.) | `Settings.js` — all `

    ` info boxes outside the retained ones | Audit and delete all info/warning boxes in Settings except the retained backup description text and any error state boxes needed for functionality. | diff --git a/preload.js b/preload.js index 869c472..7f1e396 100644 --- a/preload.js +++ b/preload.js @@ -17,6 +17,7 @@ contextBridge.exposeInMainWorld('api', { getSyncStatus: (syncId) => ipcRenderer.invoke('taker:getSyncStatus', syncId), getOffers: () => ipcRenderer.invoke('taker:getOffers'), + checkSwapLiquidity: () => ipcRenderer.invoke('taker:checkSwapLiquidity'), getGoodMakers: () => ipcRenderer.invoke('taker:getGoodMakers'), getTransactions: (count, skip) => ipcRenderer.invoke('taker:getTransactions', { count, skip }), @@ -72,4 +73,5 @@ contextBridge.exposeInMainWorld('api', { saveFile: (options) => ipcRenderer.invoke('dialog:saveFile', options), restoreWallet: (data) => ipcRenderer.invoke('taker:restore', data), backupWallet: (data) => ipcRenderer.invoke('taker:backup', data), + testTcpPort: (config) => ipcRenderer.invoke('network:testTcpPort', config), }); diff --git a/setup-coinswap.js b/setup-coinswap.js index 7968b36..5b03e1b 100755 --- a/setup-coinswap.js +++ b/setup-coinswap.js @@ -18,16 +18,19 @@ function runCommand(cmd, options = {}) { } // STEP 1 — Clone coinswap-ffi if missing +const BRANCH = 'offerbook-fix'; +const REPO_URL = 'https://github.com/citadel-tech/coinswap-ffi.git'; + if (!fs.existsSync(FFI_DIR)) { - console.log('➡️ Cloning coinswap-ffi...'); - runCommand('git clone https://github.com/citadel-tech/coinswap-ffi.git'); - console.log('✓ Cloned coinswap-ffi\n'); + console.log(`➡️ Cloning coinswap-ffi (branch ${BRANCH})...`); + runCommand(`git clone -b ${BRANCH} ${REPO_URL}`); } else { - console.log('➡️ Updating coinswap-ffi...'); + console.log(`➡️ Updating coinswap-ffi to branch ${BRANCH}...`); + // Fetch all branches and checkout the desired one + runCommand('git fetch --all', { cwd: FFI_DIR }); + runCommand(`git checkout ${BRANCH}`, { cwd: FFI_DIR }); runCommand('git pull', { cwd: FFI_DIR }); - console.log('✓ coinswap-ffi updated\n'); } - // STEP 2 — Install deps & build coinswap-js console.log('➡️ Installing dependencies for coinswap-js...'); runCommand('npm install', { cwd: NAPI_SOURCE }); diff --git a/src/components/log/Log.js b/src/components/log/Log.js index 11158ea..5534ac4 100644 --- a/src/components/log/Log.js +++ b/src/components/log/Log.js @@ -243,11 +243,6 @@ export function LogComponent(container) {
-
-

- 💡 Tip: The logs shown here are real-time. For detailed debugging or historical logs, open the complete log file. -

-
`; diff --git a/src/components/market/Market.js b/src/components/market/Market.js index 588b7d5..0d5cfdb 100644 --- a/src/components/market/Market.js +++ b/src/components/market/Market.js @@ -9,6 +9,7 @@ export function Market(container) { let currentMakerStatus = 'good'; // 'good', 'bad', or 'unresponsive' let syncCheckInterval = null; let periodicRefreshInterval = null; + let relayCount = null; // Check sync state every second function startSyncStateMonitor() { @@ -25,7 +26,7 @@ export function Market(container) { if (refreshBtn) { refreshBtn.disabled = true; refreshBtn.innerHTML = - '⏳ Syncing...'; + 'Refreshing...'; } // Monitor the current sync @@ -36,7 +37,7 @@ export function Market(container) { // No sync running - enable button if (refreshBtn && refreshBtn.disabled) { refreshBtn.disabled = false; - refreshBtn.innerHTML = '🔄 Sync Market Data'; + refreshBtn.innerHTML = 'Refresh'; } } } @@ -116,6 +117,11 @@ export function Market(container) { const goodMakers = data.offerbook.goodMakers || []; const badMakers = data.offerbook.badMakers || []; const unresponsiveMakers = data.offerbook.unresponsiveMakers || []; + relayCount = Array.isArray(data.relays) + ? data.relays.length + : typeof data.relayCount === 'number' + ? data.relayCount + : relayCount; makers = [ ...goodMakers.map((item, index) => ({ @@ -264,7 +270,7 @@ export function Market(container) { const originalText = refreshBtn.innerHTML; refreshBtn.disabled = true; - refreshBtn.innerHTML = 'Syncing...'; + refreshBtn.innerHTML = 'Refreshing...'; syncProgress = { percent: 50, status: 'syncing', message: 'Syncing market data...' }; updateUI(); @@ -290,7 +296,7 @@ export function Market(container) { syncProgress = null; await fetchMakers(); - refreshBtn.innerHTML = '✅ Synced!'; + refreshBtn.innerHTML = 'Refreshed!'; setTimeout(() => { refreshBtn.disabled = false; refreshBtn.innerHTML = originalText; @@ -298,7 +304,7 @@ export function Market(container) { } catch (error) { syncProgress = null; updateUI(); - refreshBtn.innerHTML = '❌ Failed'; + refreshBtn.innerHTML = 'Refresh Failed'; showError(error.message); setTimeout(() => { refreshBtn.disabled = false; @@ -308,26 +314,8 @@ export function Market(container) { } async function initialize() { - // Get protocol version - const protocolResult = await window.api.taker.getProtocol(); - const protocol = protocolResult.protocol; - const protocolName = protocolResult.protocolName; - - // Show protocol warning banner - const banner = content.querySelector('#protocol-banner'); - const title = content.querySelector('#protocol-warning-title'); - const text = content.querySelector('#protocol-warning-text'); - - if (protocol === 'v2') { - title.textContent = '⚡ You Can Only Swap With Taproot Makers'; - text.textContent = - 'Your wallet is configured for Taproot swaps. Legacy makers will be filtered out.'; - } else { - title.textContent = '🔒 You Can Only Swap With Legacy Makers'; - text.textContent = - 'Your wallet is configured for Legacy swaps. Taproot makers will be filtered out.'; - } - banner.classList.remove('hidden'); + // Keep protocol fetch to preserve existing initialization flow. + await window.api.taker.getProtocol(); // Check if app-level sync is currently running. try { @@ -358,7 +346,7 @@ export function Market(container) { if (refreshBtn) { refreshBtn.disabled = true; refreshBtn.innerHTML = - '⏳ Syncing...'; + 'Refreshing...'; } isLoading = true; @@ -459,16 +447,12 @@ export function Market(container) { (sum, m) => sum + m.maxSize, 0 ); - const avgFee = - displayedMakers.length > 0 - ? displayedMakers.reduce((sum, m) => sum + parseFloat(m.volumeFee), 0) / - displayedMakers.length - : 0; + const totalFidelity = displayedMakers.reduce((sum, m) => sum + m.bond, 0); return { totalLiquidity: (totalLiquidity / 100000000).toFixed(2), - avgFee: avgFee.toFixed(1), - onlineMakers: displayedMakers.length, + totalFidelity: totalFidelity.toLocaleString(), + nostrRelays: relayCount ?? 0, }; } @@ -490,12 +474,8 @@ export function Market(container) { ? Math.floor(maker.bondLocktime / 144) : 0; - const certExpiryDays = maker.bondCertExpiry - ? Math.floor((maker.bondCertExpiry * 2016) / 144) - : null; - modal.innerHTML = ` -
+

Fidelity Bond Details

@@ -503,93 +483,48 @@ export function Market(container) {
-

Maker Address

+

Tor Address

${maker.address}

-
-
-

Bond Amount

-

${maker.bond.toLocaleString()} sats

-

${(maker.bond / 100000000).toFixed(8)} BTC

-
- -
-

Bond Status

-

- ${maker.bondIsSpent ? '❌ Spent' : '✅ Active'} -

-
-
-
-

Bond Outpoint (UTXO)

-

${maker.bondOutpoint}

-
- Txid: ${maker.bondTxid} - Vout: ${maker.bondVout} -
+

Bond Amount

+

${maker.bond.toLocaleString()} sats

+

${(maker.bond / 100000000).toFixed(8)} BTC

-
-
-

Bond Locktime

-

${maker.bondLocktime.toLocaleString()} blocks

-

~${locktimeDays} days

-
- -
-

Confirmation Height

-

- ${maker.bondConfHeight !== null ? maker.bondConfHeight.toLocaleString() : 'N/A'} -

-
+
+

Bond Status

+

+ ${maker.bondIsSpent ? 'Spent' : 'Active'} +

- ${ - maker.bondCertExpiry !== null - ? `
-

Certificate Expiry

-

${maker.bondCertExpiry} difficulty periods

-

${maker.bondCertExpiry * 2016} blocks (~${certExpiryDays} days)

+

Expires In

+

~${locktimeDays} days

+ ${ + maker.bondLocktime + ? `

${maker.bondLocktime.toLocaleString()} blocks

` + : '' + }
- ` - : '' - } ${ - maker.bondPubkey + maker.bondTxid ? `
-

Bond Public Key

-

${maker.bondPubkey}

+

Bond Txid

+
` : '' } - -
-
-

Required Confirmations

-

${maker.requiredConfirms}

-
- -
-

Minimum Locktime

-

${maker.minimumLocktime} blocks

-

~${Math.floor(maker.minimumLocktime / 144)} days

-
-
- -
-

Transaction Details

- -
@@ -697,13 +632,13 @@ export function Market(container) {

${stats.totalLiquidity} BTC

-

Average Fee

-

${stats.avgFee}%

-
-
-

Online Makers

-

${stats.onlineMakers}

+

Total Fidelity Locked

+

${stats.totalFidelity} sats

+ `; } @@ -725,7 +660,7 @@ export function Market(container) { if (isLoading) { // ✅ SHOW BIG LOADING SPINNER IN TABLE tableBody.innerHTML = ` -
+
@@ -738,10 +673,10 @@ export function Market(container) { `; } else if (makers.length === 0) { tableBody.innerHTML = ` -
+

No makers found

`; @@ -752,7 +687,7 @@ export function Market(container) { if (displayedMakers.length === 0) { tableBody.innerHTML = ` -
+

No ${currentMakerStatus} makers found

`; @@ -760,7 +695,7 @@ export function Market(container) { tableBody.innerHTML = displayedMakers .map( (maker) => ` -
+
Live view of the current coinswap market

- - -
@@ -840,27 +764,27 @@ export function Market(container) {

Fee Calculation

Total fee for a swap is calculated as:

- Total Fee = Base Fee + (Swap Amount × % Fee Rate) + (Refund Lock Time × Swap Amount × % Time Rate) + Total Fee = Base Fee + (Swap Amount × % Fee Rate) + (Refund Locktime × Swap Amount × % Time Rate) -

- Lower fees mean cheaper swaps, but may indicate lower liquidity or reputation. +

+ Refund Locktime depends on the position of a maker in swap circuit. Calculated as 20*(n+1), where n = index position of the maker in the swap circuit.

-
+

Total Liquidity

0.00 BTC

-

Average Fee

-

0.0%

+

Total Fidelity Locked

+

0 sats

-

Online Makers

-

0

+

Nostr Relays

+

0

@@ -880,19 +804,19 @@ export function Market(container) { -
-
Protocol
-
Maker Address
-
Base Fee
-
% Fee Rate
-
% Time Rate
-
Min Swap Size
-
Max Swap Size
-
Fidelity Bond
+
+
Protocol
+
Tor Address
+
Base Fee
+
% Fee Rate
+
% Time Rate
+
Min Swap Size
+
Max Swap Size
+
Fidelity Bond
-
+

Loading makers...

diff --git a/src/components/receive/AddressList.js b/src/components/receive/AddressList.js index 5266bc5..f8aa4ff 100644 --- a/src/components/receive/AddressList.js +++ b/src/components/receive/AddressList.js @@ -3,6 +3,27 @@ export function AddressListComponent(container) { let sortBy = 'newest'; let allAddresses = []; + function getSpendTypeDisplay(spendType = '') { + const normalized = String(spendType || '').toLowerCase(); + if (normalized.includes('swap')) return 'Swap'; + if (normalized.includes('contract')) return 'Contract'; + if (normalized.includes('seed') || normalized.includes('regular')) return 'Regular'; + return 'Regular'; + } + + function extractSpendType(tx) { + const candidates = [ + tx?.detail?.address?.spendType, + tx?.detail?.address?.spendInfo?.spendType, + tx?.detail?.spendType, + tx?.detail?.spendInfo?.spendType, + tx?.detail?.label, + ]; + + const match = candidates.find((value) => typeof value === 'string' && value.trim()); + return getSpendTypeDisplay(match); + } + // Fetch addresses from transactions async function fetchAddresses() { try { @@ -27,7 +48,8 @@ const result = await window.api.taker.getTransactions(200, 0); used: 0, received: 0, createdAt: (tx.info.blocktime || tx.info.time) * 1000, - type: detectAddressType(addr), + type: detectAddressType(addr, extractSpendType(tx)), + spendType: extractSpendType(tx), lastUsed: (tx.info.blocktime || tx.info.time) * 1000 }); } @@ -47,22 +69,29 @@ const result = await window.api.taker.getTransactions(200, 0); } } - function detectAddressType(address) { - if (address.startsWith('bc1q')) return 'P2WPKH'; - if (address.startsWith('bc1p')) return 'P2TR'; + function detectAddressType(address, fallbackSpendType = '') { + if (address.startsWith('bc1q') || address.startsWith('tb1q') || address.startsWith('bcrt1q')) { + return address.length > 50 ? 'P2WSH' : 'P2WPKH'; + } + if (address.startsWith('bc1p') || address.startsWith('tb1p') || address.startsWith('bcrt1p')) return 'P2TR'; if (address.startsWith('3')) return 'P2SH'; if (address.startsWith('1')) return 'P2PKH'; - if (address.startsWith('tb1q')) return 'P2WPKH'; - if (address.startsWith('bcrt1q')) return 'P2WPKH'; - return 'Unknown'; + if (address.startsWith('2')) return 'P2SH'; + if (address.startsWith('m') || address.startsWith('n')) return 'P2PKH'; + + const spendType = getSpendTypeDisplay(fallbackSpendType); + if (spendType === 'Contract' || spendType === 'Swap') return 'P2WSH'; + return 'P2WPKH'; } function getFilteredAddresses() { let addresses = [...allAddresses]; - // Apply type filter + // Apply spend-type filter if (currentFilter !== 'all') { - addresses = addresses.filter((addr) => addr.type === currentFilter); + addresses = addresses.filter( + (addr) => addr.spendType.toLowerCase() === currentFilter + ); } // Apply sorting @@ -90,16 +119,6 @@ const result = await window.api.taker.getTransactions(200, 0); return colors[type] || 'gray'; } - function getStatusInfo(addr) { - if (addr.used === 0) { - return { text: 'Unused', color: 'yellow' }; - } else if (addr.used === 1) { - return { text: 'Used', color: 'green' }; - } else { - return { text: `Reused (${addr.used}x)`, color: 'blue' }; - } - } - function formatLastUsed(timestamp) { if (!timestamp) return 'Never'; const date = new Date(timestamp); @@ -120,7 +139,6 @@ const result = await window.api.taker.getTransactions(200, 0); const totalReceived = allAddresses.reduce((sum, a) => sum + a.received, 0); return { - total: allAddresses.length, used, reused, totalReceived @@ -162,10 +180,9 @@ const result = await window.api.taker.getTransactions(200, 0); } const csv = [ - 'Address,Type,Status,Times Used,Received (BTC),Created At,Last Used', + 'Address,Type,Spend Type,Times Used,Received (BTC),Created At,Last Used', ...allAddresses.map((addr) => { - const statusInfo = getStatusInfo(addr); - return `"${addr.address}","${addr.type}","${statusInfo.text}",${addr.used},${(addr.received / 100000000).toFixed(8)},"${new Date(addr.createdAt).toLocaleString()}","${formatLastUsed(addr.lastUsed)}"`; + return `"${addr.address}","${addr.type}","${addr.spendType}",${addr.used},${(addr.received / 100000000).toFixed(8)},"${new Date(addr.createdAt).toLocaleString()}","${formatLastUsed(addr.lastUsed)}"`; }), ].join('\n'); @@ -183,9 +200,11 @@ const result = await window.api.taker.getTransactions(200, 0); function render() { const filteredAddresses = getFilteredAddresses(); const stats = getStats(); - - // Get unique address types for filter buttons - const addressTypes = [...new Set(allAddresses.map((a) => a.type))]; + const spendTypeFilters = [ + { value: 'regular', label: 'Regular' }, + { value: 'contract', label: 'Contract' }, + { value: 'swap', label: 'Swap' }, + ]; container.innerHTML = `
@@ -202,17 +221,13 @@ const result = await window.api.taker.getTransactions(200, 0); 📥 Export CSV
-
-
-

Total Addresses

-

${stats.total}

-
+

Used Addresses

${stats.used}

@@ -236,15 +251,20 @@ const result = await window.api.taker.getTransactions(200, 0); - ${addressTypes + ${spendTypeFilters .map((type) => { const count = allAddresses.filter( - (a) => a.type === type + (a) => a.spendType === type.label ).length; - const color = getTypeColor(type); + const color = + type.value === 'regular' + ? 'green' + : type.value === 'contract' + ? 'yellow' + : 'blue'; return ` - `; }) @@ -273,7 +293,7 @@ const result = await window.api.taker.getTransactions(200, 0); ? `
📭
-

No addresses ${currentFilter !== 'all' ? `of type ${currentFilter}` : 'found in transaction history'}

+

No addresses ${currentFilter !== 'all' ? `for ${currentFilter} spend type` : 'found in transaction history'}

@@ -286,7 +306,7 @@ const result = await window.api.taker.getTransactions(200, 0); Address Type - Status + Spend Type Times Used Received Created @@ -297,7 +317,12 @@ const result = await window.api.taker.getTransactions(200, 0); ${filteredAddresses .map((addr) => { const typeColor = getTypeColor(addr.type); - const statusInfo = getStatusInfo(addr); + const spendTypeColor = + addr.spendType === 'Regular' + ? 'green' + : addr.spendType === 'Contract' + ? 'yellow' + : 'blue'; const createdDate = new Date(addr.createdAt); return ` @@ -321,8 +346,8 @@ const result = await window.api.taker.getTransactions(200, 0); - - ${statusInfo.text} + + ${addr.spendType} ${addr.used} @@ -349,15 +374,6 @@ const result = await window.api.taker.getTransactions(200, 0); ` }
- - -
-

💡 Privacy Tip

-

- For best privacy, generate a new address for each transaction. Address reuse can link your transactions together and reduce your anonymity. - Addresses marked as "Reused" have received multiple transactions and may have reduced privacy. -

-
`; @@ -429,7 +445,7 @@ const result = await window.api.taker.getTransactions(200, 0); const refreshButton = container.querySelector('#refresh-addresses'); if (refreshButton) { refreshButton.addEventListener('click', async () => { - refreshButton.innerHTML = '⏳ Loading...'; + refreshButton.innerHTML = 'Refreshing...'; refreshButton.disabled = true; allAddresses = await fetchAddresses(); render(); @@ -452,4 +468,4 @@ const result = await window.api.taker.getTransactions(200, 0); allAddresses = await fetchAddresses(); render(); })(); -} \ No newline at end of file +} diff --git a/src/components/receive/Receive.js b/src/components/receive/Receive.js index 84389a0..b733539 100644 --- a/src/components/receive/Receive.js +++ b/src/components/receive/Receive.js @@ -142,17 +142,28 @@ export function ReceiveComponent(container) { } // Detect address type from address string - function detectAddressType(address) { - if (address.startsWith('bc1q')) return 'P2WPKH'; - if (address.startsWith('bc1p')) return 'P2TR'; + function detectAddressType(address, fallbackSpendType = '') { + if ( + address.startsWith('bc1q') || + address.startsWith('tb1q') || + address.startsWith('bcrt1q') + ) { + return address.length > 50 ? 'P2WSH' : 'P2WPKH'; + } + if ( + address.startsWith('bc1p') || + address.startsWith('bcrt1p') || + address.startsWith('tb1p') + ) + return 'P2TR'; if (address.startsWith('3')) return 'P2SH'; if (address.startsWith('1')) return 'P2PKH'; - if (address.startsWith('tb1q')) return 'P2WPKH'; // testnet - if (address.startsWith('bcrt1q')) return 'P2WPKH'; // regtest - if (address.startsWith('bcrt1p')) return 'P2TR'; - if (address.startsWith('tb1q')) return 'P2WPKH'; - if (address.startsWith('tb1p')) return 'P2TR'; - return 'Unknown'; + if (address.startsWith('2')) return 'P2SH'; + if (address.startsWith('m') || address.startsWith('n')) return 'P2PKH'; + + const spendType = String(fallbackSpendType || '').toLowerCase(); + if (spendType.includes('contract') || spendType.includes('swap')) return 'P2WSH'; + return 'P2WPKH'; } // Get addresses from transaction history diff --git a/src/components/recovery/Recovery.js b/src/components/recovery/Recovery.js index 2a6e05e..d1bd456 100644 --- a/src/components/recovery/Recovery.js +++ b/src/components/recovery/Recovery.js @@ -14,73 +14,68 @@ export function RecoveryComponent(container) { } content.innerHTML = ` -

Recovery

-

Recover funds from failed or stuck coinswaps

- -
- -
-

Manual Recovery

-

If automatic recovery fails, you can manually recover funds

- - -
+
+
+

Recovery

+

Recover funds from failed or stuck coinswaps

+
+ +
- -
- -
-

How Recovery Works

-
-
- 1. -

System detects failed swap or timeout

-
-
- 2. -

Broadcast contract transactions to blockchain

-
-
- 3. -

Wait for timelock or claim via hashlock

-
-
- 4. -

Funds returned to your wallet

-
+
+
+

How Recovery Works

+
+
+ 1. +

The Recovery routine detects failed swaps, waits for HTLC timelock expiry then creates and broadcasts a refund transaction back to wallet.

- -
-

- ℹ Recovery is automatic but may take several hours due to timelock periods -

+
+ 2. +

It might take several hours for timelock to expire.

+
+
+ 3. +

Recovery is automatically triggered for any unspent swap contract transactions at wallet startup. If you still see pending recoveries here, use the Trigger Recovery button to manually trigger a recovery.

+
+
+ 4. +

While waiting for recovery the app can be safely closed. Recovery will resume in next restart.

+
+
+ 5. +

Always ensure to not have very old pending recoveries. That can put your funds at risk.

+
- -
-

Recovery Stats

-
-
-

Total Recovered

-

0.00000000 BTC

-
-
-

Recovery Rate

-

100%

-
-
-

Pending

-

0

-
+
+

Recovery Stats

+
+
+

Total Recovered

+

0.00000000 BTC

+
+
+

Recovery Rate

+

100%

+
+
+

Pending

+

0

+
- - +
+

Manual Recovery

+

If automatic recovery fails, you can manually recover funds.

+
`; @@ -115,9 +110,9 @@ export function RecoveryComponent(container) { } setTimeout(() => { - btn.textContent = '🔧 Manually Trigger Recovery'; + btn.textContent = 'Trigger Recovery'; btn.disabled = false; btn.classList.remove('opacity-50', 'cursor-not-allowed'); }, 3000); }); -} \ No newline at end of file +} diff --git a/src/components/settings/FirstTimeSetup.js b/src/components/settings/FirstTimeSetup.js index 3d7a880..99903ed 100644 --- a/src/components/settings/FirstTimeSetup.js +++ b/src/components/settings/FirstTimeSetup.js @@ -1,4 +1,21 @@ export function FirstTimeSetupModal(container, onComplete) { + const defaultWalletName = `taker-wallet-${Math.floor(100000 + Math.random() * 900000)}`; + const iconClass = 'w-5 h-5 flex-shrink-0'; + const iconWarning = ` + + `; + const iconInfo = ` + + `; + const iconShield = ` + + `; const modal = document.createElement('div'); modal.id = 'setup-modal'; modal.className = @@ -14,13 +31,13 @@ export function FirstTimeSetupModal(container, onComplete) {
-

Welcome to Coinswap Taker!

-

Let's set up your wallet for private Bitcoin swaps

-
+

Coinswap Client GUI

+

Wallet and Other Setups.

+
- Step 1 of 4 + Step 1 of 4
@@ -82,18 +99,21 @@ export function FirstTimeSetupModal(container, onComplete) {
-

- ⚠️ Important:You can only perform one type of swap with a taker (Taproot OR Legacy swaps). You cannot do both. However, your wallet can handle both Taproot and Legacy transactions for regular operations of send and receive. -

+
+ ${iconWarning} +

+ Important: You can only perform one type of swap with a taker (Taproot OR Legacy swaps). You cannot do both. However, your wallet can handle both Taproot and Legacy transactions for regular operations of send and receive. +

+
- + - +
@@ -547,7 +541,6 @@ export function FirstTimeSetupModal(container, onComplete) {
-

Authentication password for Tor control interface

@@ -604,15 +601,134 @@ export function FirstTimeSetupModal(container, onComplete) { stepIndicator.textContent = `Step ${currentStep} of ${totalSteps}`; } + function getRpcUrl(host, port) { + return `http://${host}:${port}`; + } + + function getRestUrl(host, port) { + return `${getRpcUrl(host, port)}/rest/chaininfo.json`; + } + + function getZmqAddress(port) { + return `tcp://127.0.0.1:${port}`; + } + + function renderConnectionResults(resultDiv, results) { + const hasFailure = results.some((result) => !result.ok); + resultDiv.className = hasFailure + ? 'bg-red-500/10 border border-red-500/30 rounded-lg p-3' + : 'bg-green-500/10 border border-green-500/30 rounded-lg p-3'; + resultDiv.innerHTML = ` +
+ ${results + .map( + (result) => ` +
+ + ${result.ok ? '✅' : '❌'} ${result.label} + + ${result.message} +
+ ` + ) + .join('')} +
+ `; + resultDiv.classList.remove('hidden'); + } + + function syncFormData() { + walletData.rpc = { + host: modal.querySelector('#setup-rpc-host')?.value || '127.0.0.1', + port: modal.querySelector('#setup-rpc-port')?.value || '38332', + username: modal.querySelector('#setup-rpc-username')?.value || 'user', + password: modal.querySelector('#setup-rpc-password')?.value || 'password', + zmqPort: modal.querySelector('#setup-zmq-port')?.value || '28332', + }; + + walletData.create = { + walletName: + modal.querySelector('#create-wallet-name')?.value || defaultWalletName, + password: modal.querySelector('#create-password')?.value || '', + confirmPassword: + modal.querySelector('#create-password-confirm')?.value || '', + }; + + walletData.load = { + walletPath: modal.querySelector('#load-wallet-path')?.value || '', + password: modal.querySelector('#load-password')?.value || '', + }; + + walletData.restore = { + walletName: modal.querySelector('#restore-wallet-name')?.value || '', + backupPath: modal.querySelector('#restore-backup-path')?.value || '', + password: modal.querySelector('#restore-password')?.value || '', + }; + + walletData.tor = { + controlPort: modal.querySelector('#setup-tor-control-port')?.value || '9051', + socksPort: modal.querySelector('#setup-tor-socks-port')?.value || '9050', + authPassword: + modal.querySelector('#setup-tor-auth-password')?.value || '', + }; + } + + function restoreFormData() { + const rpcData = walletData.rpc || {}; + const createData = walletData.create || {}; + const loadData = walletData.load || {}; + const restoreData = walletData.restore || {}; + const torData = walletData.tor || {}; + + const setValue = (selector, value) => { + const input = modal.querySelector(selector); + if (input && value !== undefined) { + input.value = value; + } + }; + + const setChecked = (selector, checked) => { + const input = modal.querySelector(selector); + if (input) { + input.checked = Boolean(checked); + } + }; + + setValue('#setup-rpc-host', rpcData.host); + setValue('#setup-rpc-port', rpcData.port); + setValue('#setup-rpc-username', rpcData.username); + setValue('#setup-rpc-password', rpcData.password); + setValue('#setup-zmq-port', rpcData.zmqPort); + + setValue('#create-wallet-name', createData.walletName || defaultWalletName); + setValue('#create-password', createData.password); + setValue('#create-password-confirm', createData.confirmPassword); + + setValue('#load-wallet-path', loadData.walletPath); + setValue('#load-password', loadData.password); + + setValue('#restore-wallet-name', restoreData.walletName); + setValue('#restore-backup-path', restoreData.backupPath); + setValue('#restore-password', restoreData.password); + + setValue('#setup-tor-control-port', torData.controlPort); + setValue('#setup-tor-socks-port', torData.socksPort); + setValue('#setup-tor-auth-password', torData.authPassword); + } + function showStep(step) { + syncFormData(); + // Hide all steps modal .querySelectorAll('.setup-step') .forEach((el) => el.classList.add('hidden')); - // Determine which substep to show for step 3 + // Determine which screen to show for each wizard step. let stepToShow = `step-${step}`; if (step === 3) { + stepToShow = 'step-4'; + } else if (step === 4) { if (!walletAction) { stepToShow = 'step-3a'; // Show choice screen } else if (walletAction === 'create') { @@ -630,6 +746,8 @@ export function FirstTimeSetupModal(container, onComplete) { stepElement.classList.remove('hidden'); } + restoreFormData(); + // Update buttons const backBtn = modal.querySelector('#setup-back-btn'); const nextBtn = modal.querySelector('#setup-next-btn'); @@ -648,7 +766,9 @@ export function FirstTimeSetupModal(container, onComplete) { updateProgress(); } - async function validateStep3() { + async function validateWalletStep() { + syncFormData(); + // If no wallet action selected, show message if (!walletAction) { showMessage('choice-message'); @@ -662,8 +782,6 @@ export function FirstTimeSetupModal(container, onComplete) { const password = modal.querySelector('#create-password')?.value || ''; const confirmPassword = modal.querySelector('#create-password-confirm')?.value || ''; - const skipEncryption = - modal.querySelector('#skip-encryption')?.checked || false; // Validate wallet name if (!walletName || walletName.trim() === '') { @@ -671,28 +789,24 @@ export function FirstTimeSetupModal(container, onComplete) { return false; } - // Validate password - if (!skipEncryption && !password) { - showError( - 'password-error', - 'Please enter a password or check "Skip encryption"' - ); - return false; - } - - if (!skipEncryption && password !== confirmPassword) { + if (password !== confirmPassword) { showError('password-error', 'Passwords do not match'); return false; } - if (!skipEncryption && password.length < 8) { + if (password && password.length < 8) { showError('password-error', 'Password must be at least 8 characters'); return false; } // Save wallet data (including wallet name) walletData.walletName = walletName.trim(); - walletData.password = skipEncryption ? '' : password; + walletData.password = password; + walletData.create = { + walletName: walletName.trim(), + password, + confirmPassword, + }; return true; } @@ -710,6 +824,10 @@ export function FirstTimeSetupModal(container, onComplete) { const walletFileName = walletPath.split('/').pop(); walletData.walletFileName = walletFileName; walletData.password = password || undefined; + walletData.load = { + walletPath, + password, + }; return true; } @@ -718,8 +836,6 @@ export function FirstTimeSetupModal(container, onComplete) { const backupPath = modal.querySelector('#restore-backup-path')?.value || ''; const password = modal.querySelector('#restore-password')?.value || ''; - const noPassword = - modal.querySelector('#restore-no-password')?.checked || false; const walletName = modal.querySelector('#restore-wallet-name')?.value?.trim() || ''; @@ -737,8 +853,13 @@ export function FirstTimeSetupModal(container, onComplete) { } walletData.backupPath = backupPath; - walletData.password = noPassword ? '' : password || ''; - walletData.walletName = walletName; // ✅ Save wallet name + walletData.password = password || ''; + walletData.walletName = walletName; + walletData.restore = { + walletName, + backupPath, + password, + }; return true; } @@ -773,6 +894,9 @@ export function FirstTimeSetupModal(container, onComplete) { } function buildConfiguration() { + const zmqPort = modal.querySelector('#setup-zmq-port').value; + const zmqAddress = getZmqAddress(zmqPort); + const config = { protocol: protocolVersion, // 'v1' (P2WSH) or 'v2' (Taproot) rpc: { @@ -782,9 +906,9 @@ export function FirstTimeSetupModal(container, onComplete) { password: modal.querySelector('#setup-rpc-password').value, }, zmq: { - rawblock: modal.querySelector('#setup-zmq-rawblock').value, - rawtx: modal.querySelector('#setup-zmq-rawtx').value, - address: modal.querySelector('#setup-zmq-rawblock').value, + rawblock: zmqAddress, + rawtx: zmqAddress, + address: zmqAddress, }, taker: { control_port: parseInt( @@ -813,126 +937,135 @@ export function FirstTimeSetupModal(container, onComplete) { return config; } - // Real RPC connection test - async function testRPCConnection() { - const btn = modal.querySelector('#test-rpc-setup'); - const resultDiv = modal.querySelector('#rpc-test-result'); - - const originalText = btn.textContent; - btn.textContent = 'Testing...'; - btn.disabled = true; - + async function makeRpcCall(method, params = []) { const host = modal.querySelector('#setup-rpc-host').value; const port = modal.querySelector('#setup-rpc-port').value; const username = modal.querySelector('#setup-rpc-username').value; const password = modal.querySelector('#setup-rpc-password').value; if (!username || !password) { - resultDiv.className = - 'bg-red-500/10 border border-red-500/30 rounded-lg p-3'; - resultDiv.innerHTML = ` -
- ❌ RPC username and password are required -
- `; - resultDiv.classList.remove('hidden'); - btn.textContent = originalText; - btn.disabled = false; - return; + throw new Error('RPC username and password are required'); } - try { - const url = `http://${host}:${port}`; - const auth = btoa(`${username}:${password}`); - - const response = await fetch(url, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: `Basic ${auth}`, - }, - body: JSON.stringify({ - jsonrpc: '1.0', - id: Date.now(), - method: 'getblockchaininfo', - params: [], - }), - }); + const response = await fetch(getRpcUrl(host, port), { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Basic ${btoa(`${username}:${password}`)}`, + }, + body: JSON.stringify({ + jsonrpc: '1.0', + id: Date.now(), + method, + params, + }), + }); - if (!response.ok) { - if (response.status === 401) { - throw new Error( - 'Authentication failed - check RPC username/password' - ); - } else if (response.status === 403) { - throw new Error( - 'Access forbidden - check rpcallowip in bitcoin.conf' - ); - } else { - throw new Error(`HTTP ${response.status}: ${response.statusText}`); - } + if (!response.ok) { + if (response.status === 401) { + throw new Error('Authentication failed - check RPC username/password'); + } + if (response.status === 403) { + throw new Error('Access forbidden - check rpcallowip in bitcoin.conf'); } + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } - const data = await response.json(); + const data = await response.json(); + if (data.error) { + throw new Error(`RPC Error: ${data.error.message}`); + } - if (data.error) { - throw new Error(`RPC Error: ${data.error.message}`); - } + return data.result; + } - // Success - get network info too - const networkResponse = await fetch(url, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: `Basic ${auth}`, - }, - body: JSON.stringify({ - jsonrpc: '1.0', - id: Date.now(), - method: 'getnetworkinfo', - params: [], - }), - }); + // Real node connection test + async function testRPCConnection() { + const btn = modal.querySelector('#test-rpc-setup'); + const resultDiv = modal.querySelector('#rpc-test-result'); - const networkData = await networkResponse.json(); - const version = networkData.result?.subversion || 'Unknown'; - const chain = data.result?.chain || 'unknown'; - const blocks = data.result?.blocks || 0; + const originalText = btn.textContent; + btn.textContent = 'Testing...'; + btn.disabled = true; - resultDiv.className = - 'bg-green-500/10 border border-green-500/30 rounded-lg p-3'; - resultDiv.innerHTML = ` -
-
- ✅ Connection successful! -
-
- Version: ${version} • - Network: ${chain} • - Blocks: ${blocks.toLocaleString()} -
-
- `; - resultDiv.classList.remove('hidden'); + const host = modal.querySelector('#setup-rpc-host').value; + const port = modal.querySelector('#setup-rpc-port').value; + const zmqPort = parseInt(modal.querySelector('#setup-zmq-port').value, 10); + + try { + const [blockchainInfo, networkInfo, restResponse, zmqResult] = + await Promise.allSettled([ + makeRpcCall('getblockchaininfo'), + makeRpcCall('getnetworkinfo'), + fetch(getRestUrl(host, port)), + window.api.testTcpPort({ host: '127.0.0.1', port: zmqPort }), + ]); + + const rpcOk = + blockchainInfo.status === 'fulfilled' && + networkInfo.status === 'fulfilled'; + const chain = + blockchainInfo.status === 'fulfilled' + ? blockchainInfo.value?.chain || 'unknown' + : null; + const blocks = + blockchainInfo.status === 'fulfilled' + ? blockchainInfo.value?.blocks || 0 + : null; + const version = + networkInfo.status === 'fulfilled' + ? networkInfo.value?.subversion || 'Unknown' + : null; + + const restOk = + restResponse.status === 'fulfilled' && restResponse.value.ok; + + const results = [ + { + label: 'RPC', + ok: rpcOk, + message: rpcOk + ? `${version} • ${chain} • ${blocks.toLocaleString()} blocks` + : blockchainInfo.reason?.message || networkInfo.reason?.message, + }, + { + label: 'REST', + ok: restOk, + message: restOk + ? `${getRestUrl(host, port)} reachable` + : restResponse.status === 'fulfilled' + ? `HTTP ${restResponse.value.status}: ${restResponse.value.statusText}` + : restResponse.reason?.message, + }, + { + label: 'ZMQ', + ok: + zmqResult.status === 'fulfilled' && + Boolean(zmqResult.value?.success), + message: + zmqResult.status === 'fulfilled' && zmqResult.value?.success + ? `Port ${zmqPort} reachable` + : zmqResult.status === 'fulfilled' + ? zmqResult.value?.error + : zmqResult.reason?.message, + }, + ]; + + renderConnectionResults(resultDiv, results); } catch (error) { console.error('RPC test failed:', error); - let errorMessage = error.message; - if ( - error.message.includes('Failed to fetch') || - error.message.includes('NetworkError') - ) { - errorMessage = 'Cannot connect to Bitcoin Core. Is bitcoind running?'; - } - - resultDiv.className = - 'bg-red-500/10 border border-red-500/30 rounded-lg p-3'; - resultDiv.innerHTML = ` -
- ❌ ${errorMessage} -
- `; - resultDiv.classList.remove('hidden'); + renderConnectionResults(resultDiv, [ + { + label: 'Node Test', + ok: false, + message: + error.message.includes('Failed to fetch') || + error.message.includes('NetworkError') + ? 'Cannot connect to Bitcoin Core. Is bitcoind running?' + : error.message, + }, + ]); } btn.textContent = originalText; @@ -1018,45 +1151,37 @@ export function FirstTimeSetupModal(container, onComplete) { ); try { - const result = await window.api.taker.testTorConnection({ - socksPort, - controlPort, - }); - - if (result.success) { - resultDiv.className = - 'bg-green-500/10 border border-green-500/30 rounded-lg p-3'; - resultDiv.innerHTML = ` -
-
- ✅ ${result.message} -
-
- Tor SOCKS proxy is accessible on port ${socksPort} -
-
- `; - } else { - throw new Error(result.error); - } - - resultDiv.classList.remove('hidden'); + const [socksResult, controlResult] = await Promise.all([ + window.api.testTcpPort({ host: '127.0.0.1', port: socksPort }), + window.api.testTcpPort({ host: '127.0.0.1', port: controlPort }), + ]); + + renderConnectionResults(resultDiv, [ + { + label: 'SOCKS Port', + ok: Boolean(socksResult?.success), + message: socksResult?.success + ? `Port ${socksPort} reachable` + : socksResult?.error, + }, + { + label: 'Control Port', + ok: Boolean(controlResult?.success), + message: controlResult?.success + ? `Port ${controlPort} reachable` + : controlResult?.error, + }, + ]); } catch (error) { console.error('Tor test failed:', error); - resultDiv.className = - 'bg-red-500/10 border border-red-500/30 rounded-lg p-3'; - resultDiv.innerHTML = ` -
-
- ❌ ${error.message || error} -
-
- Make sure Tor is running with SOCKS proxy on port ${socksPort} -
-
- `; - resultDiv.classList.remove('hidden'); + renderConnectionResults(resultDiv, [ + { + label: 'Tor Connection', + ok: false, + message: error.message || String(error), + }, + ]); } btn.textContent = originalText; @@ -1151,22 +1276,6 @@ export function FirstTimeSetupModal(container, onComplete) { }); } - // Skip encryption checkbox - const skipEncryption = modal.querySelector('#skip-encryption'); - if (skipEncryption) { - skipEncryption.addEventListener('change', (e) => { - const passwordInputs = modal.querySelectorAll( - '#create-password, #create-password-confirm' - ); - passwordInputs.forEach((input) => { - input.disabled = e.target.checked; - if (e.target.checked) { - input.value = ''; - } - }); - }); - } - // Browse wallet file const browseWalletBtn = modal.querySelector('#browse-wallet-file'); if (browseWalletBtn) { @@ -1209,19 +1318,6 @@ export function FirstTimeSetupModal(container, onComplete) { }); } - const restoreNoPassword = modal.querySelector('#restore-no-password'); - if (restoreNoPassword) { - restoreNoPassword.addEventListener('change', (e) => { - const passwordInput = modal.querySelector('#restore-password'); - if (passwordInput) { - passwordInput.disabled = e.target.checked; - if (e.target.checked) { - passwordInput.value = ''; - } - } - }); - } - // Reusable function to toggle password visibility function setupPasswordToggle(toggleButtonId, passwordInputId) { const toggleButton = modal.querySelector(toggleButtonId); @@ -1265,8 +1361,8 @@ export function FirstTimeSetupModal(container, onComplete) { walletAction ); - if (currentStep === 3) { - const valid = await validateStep3(); + if (currentStep === 4) { + const valid = await validateWalletStep(); if (!valid) { console.log('Validation failed'); return; @@ -1297,10 +1393,9 @@ export function FirstTimeSetupModal(container, onComplete) { // Back button modal.querySelector('#setup-back-btn').addEventListener('click', () => { - if (currentStep === 3 && walletAction) { + if (currentStep === 4 && walletAction) { // If in step 3 substep, go back to step 3a (choice) walletAction = null; - walletData = {}; showStep(currentStep); // Reset choice borders modal.querySelectorAll('.wallet-choice').forEach((el) => { diff --git a/src/components/settings/Settings.js b/src/components/settings/Settings.js index 3fb4b66..0b5110c 100644 --- a/src/components/settings/Settings.js +++ b/src/components/settings/Settings.js @@ -23,155 +23,152 @@ export function SettingsComponent(container) {
-
-

- 💡 Backup Tips: -

-
    -
  • • Store your backup file in a safe location (external drive, cloud storage)
  • -
  • • Remember your backup password - you'll need it to restore
  • -
  • • Create regular backups after significant transactions
  • -
  • • Keep multiple backup copies in different locations
  • -
-
-
-
- - -
-

Taker Configuration

- -
-

Tor Configuration

- -
-
- - -

Control port for Tor interface

-
- -
- - -

SOCKS port for Tor proxy

+
+
+

• Wallet Backup is an encrypted json file that restores your coinswap wallet in any client app.

+

• The backup file contains all data related to swaps to restore swap histories.

+

• The backup file can also be used to migrate your coinswap wallet from one client app to another.

+

• Always use a strong password for the backup file, or else your seed phrase can be compromised.

+

• Use the same password while restoring wallet from backup.

- -
- -
- - -
-

Authentication password for Tor interface

-
-
- -
-

- 🧅 Tor Network: Coinswap uses Tor for private maker discovery and communication. Make sure Tor is running on your system. -

- +
-

Bitcoin Core RPC Configuration

+

Node & Network Configuration

-
- -
-

Connection Settings

- -
- - -

Bitcoin Core RPC host address

-
- -
- - -

Bitcoin Core RPC port (8332 for mainnet, 18332 for testnet, 38332 for regtest)

-
- -
- - -

RPC username from bitcoin.conf

+
+
+
+

Tor

+
+
+ + +
+
+ + +
+
+ +
+ + +
+
+
+
+ + +
- -
- - -

RPC password from bitcoin.conf

+ +
+

Bitcoin Core

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
- - +
-

Connection Status

- -
+
+

Connection Status

- Connection Status + RPC Status
Not Connected
@@ -194,91 +191,18 @@ export function SettingsComponent(container) {
- -
- - -
- - -
- - -
-
-
-
- -
-

ZMQ Configuration

- -
-
-

ZMQ Endpoints

- -
- - -

ZMQ endpoint for raw block notifications

-
- -
- - -

ZMQ endpoint for raw transaction notifications

-
- -
-

- ⚠️ Note: Both ZMQ ports should be set to 28332 for proper operation. -

-
-
- -
-

Bitcoin.conf Setup

- -
-

- ⚠️ ZMQ Required: Bitcoin Core must have ZMQ enabled for real-time notifications. -

-

Add these lines to your bitcoin.conf:

-
- -
-
- zmqpubrawblock=tcp://127.0.0.1:28332
- zmqpubrawtx=tcp://127.0.0.1:28332 +
+

Bitcoin.conf Setup

+
+
+ zmqpubrawblock=tcp://127.0.0.1:28332
+ zmqpubrawtx=tcp://127.0.0.1:28332 +
-
- - - -
-

- 💡 After adding ZMQ config, restart Bitcoin Core for changes to take effect. -

+
@@ -304,6 +228,7 @@ rpcpassword=password
rpcport=38332
rpcbind=127.0.0.1
rpcallowip=127.0.0.1
+rest=1
# ZMQ Configurations for real-time transaction and block notifications
# Needed for the Watchers.
zmqpubrawblock=tcp://127.0.0.1:28332
@@ -322,6 +247,7 @@ rpcpassword=password
rpcport=18442
rpcbind=127.0.0.1
rpcallowip=127.0.0.1
+rest=1
# ZMQ Configurations for real-time transaction and block notifications
# Needed for the Watchers.
zmqpubrawblock=tcp://127.0.0.1:28332
@@ -409,6 +335,53 @@ blockfilterindex=1 // FUNCTIONS + function getRpcUrl(host, port) { + return `http://${host}:${port}`; + } + + function getRestUrl(host, port) { + return `${getRpcUrl(host, port)}/rest/chaininfo.json`; + } + + function getZmqAddress(port) { + return `tcp://127.0.0.1:${port}`; + } + + function extractPortFromAddress(address, fallback = '28332') { + if (!address) return fallback; + const match = String(address).match(/:(\d+)$/); + return match ? match[1] : fallback; + } + + function renderConnectionResults(resultDiv, results) { + const hasFailure = results.some((result) => !result.ok); + resultDiv.className = hasFailure + ? 'bg-red-500/10 border border-red-500/30 rounded-lg p-3' + : 'bg-green-500/10 border border-green-500/30 rounded-lg p-3'; + + const resultsContainer = document.createElement('div'); + resultsContainer.className = 'space-y-2'; + + results.forEach((result) => { + const row = document.createElement('div'); + row.className = 'flex items-start justify-between gap-3'; + + const label = document.createElement('span'); + label.className = `text-sm ${result.ok ? 'text-green-400' : 'text-red-400'}`; + label.textContent = `${result.ok ? '✅' : '❌'} ${result.label}`; + + const message = document.createElement('span'); + message.className = 'text-xs text-gray-400 text-right'; + message.textContent = result.message ?? ''; + + row.append(label, message); + resultsContainer.appendChild(row); + }); + + resultDiv.replaceChildren(resultsContainer); + resultDiv.classList.remove('hidden'); + } + // Load existing configuration and populate form fields function loadExistingConfig() { try { @@ -446,11 +419,10 @@ blockfilterindex=1 // Populate ZMQ fields if (config.zmq) { - if (config.zmq.rawblock) - content.querySelector('#zmq-rawblock-input').value = - config.zmq.rawblock; - if (config.zmq.rawtx) - content.querySelector('#zmq-rawtx-input').value = config.zmq.rawtx; + const derivedPort = + config.zmq.port || + extractPortFromAddress(config.zmq.rawblock || config.zmq.address); + content.querySelector('#zmq-port-input').value = derivedPort; } // Update config previews @@ -463,13 +435,14 @@ blockfilterindex=1 // Update the config preview sections function updateConfigPreviews() { - const rawblock = content.querySelector('#zmq-rawblock-input').value; - const rawtx = content.querySelector('#zmq-rawtx-input').value; + const zmqPort = content.querySelector('#zmq-port-input').value || '28332'; + const rawblock = getZmqAddress(zmqPort); + const rawtx = getZmqAddress(zmqPort); const rpcUser = content.querySelector('#rpc-username-input').value || 'user'; const rpcPass = content.querySelector('#rpc-password-input').value || 'password'; - const rpcPort = content.querySelector('#rpc-port-input').value || '38332'; + const rpcPort = content.querySelector('#rpc-port-input').value || '18442'; // Update ZMQ config preview const zmqPreview = content.querySelector('#zmq-config-preview'); @@ -494,6 +467,7 @@ rpcpassword=${rpcPass}
rpcport=38332
rpcbind=127.0.0.1
rpcallowip=127.0.0.1
+rest=1
# ZMQ Configurations for real-time transaction and block notifications
# Needed for the Watchers.
zmqpubrawblock=${rawblock}
@@ -512,6 +486,7 @@ rpcpassword=${rpcPass}
rpcport=${rpcPort}
rpcbind=127.0.0.1
rpcallowip=127.0.0.1
+rest=1
# ZMQ Configurations for real-time transaction and block notifications
# Needed for the Watchers.
zmqpubrawblock=${rawblock}
@@ -641,13 +616,8 @@ blockfilterindex=1`; await performBackup(skipEncryption ? '' : password); }); - // ZMQ input changes - update previews - content - .querySelector('#zmq-rawblock-input') - .addEventListener('input', updateConfigPreviews); - content - .querySelector('#zmq-rawtx-input') - .addEventListener('input', updateConfigPreviews); + // Config input changes - update previews + content.querySelector('#zmq-port-input').addEventListener('input', updateConfigPreviews); content .querySelector('#rpc-username-input') .addEventListener('input', updateConfigPreviews); @@ -662,8 +632,9 @@ blockfilterindex=1`; content .querySelector('#copy-zmq-config-btn') .addEventListener('click', async () => { - const rawblock = content.querySelector('#zmq-rawblock-input').value; - const rawtx = content.querySelector('#zmq-rawtx-input').value; + const zmqPort = content.querySelector('#zmq-port-input').value || '28332'; + const rawblock = getZmqAddress(zmqPort); + const rawtx = getZmqAddress(zmqPort); const configText = `zmqpubrawblock=${rawblock}\nzmqpubrawtx=${rawtx}`; try { @@ -683,8 +654,9 @@ blockfilterindex=1`; content .querySelector('#copy-full-config-btn') .addEventListener('click', async () => { - const rawblock = content.querySelector('#zmq-rawblock-input').value; - const rawtx = content.querySelector('#zmq-rawtx-input').value; + const zmqPort = content.querySelector('#zmq-port-input').value || '28332'; + const rawblock = getZmqAddress(zmqPort); + const rawtx = getZmqAddress(zmqPort); const rpcUser = content.querySelector('#rpc-username-input').value || 'user'; const rpcPass = @@ -707,6 +679,7 @@ blockfilterindex=1 rpcport=38332 rpcbind=127.0.0.1 rpcallowip=127.0.0.1 +rest=1 zmqpubrawblock=${rawblock} zmqpubrawtx=${rawtx} @@ -723,6 +696,7 @@ blockfilterindex=1 rpcport=18442 rpcbind=127.0.0.1 rpcallowip=127.0.0.1 +rest=1 zmqpubrawblock=${rawblock} zmqpubrawtx=${rawtx}`; try { @@ -738,23 +712,14 @@ zmqpubrawtx=${rawtx}`; } }); - // Enhanced connection status management - let connectionTimer = null; - let isConnected = false; - function updateConnectionStatus(connected, info = {}) { const indicator = content.querySelector('#connection-indicator'); const status = content.querySelector('#rpc-status'); - const connectBtn = content.querySelector('#connect-btn'); - const disconnectBtn = content.querySelector('#disconnect-btn'); if (connected) { indicator.className = 'w-3 h-3 bg-green-500 rounded-full mr-2'; status.textContent = 'Connected'; status.className = 'text-sm font-semibold text-lg text-green-400'; - connectBtn.disabled = true; - disconnectBtn.disabled = false; - isConnected = true; if (info.version) content.querySelector('#bitcoin-version').textContent = info.version; @@ -771,9 +736,6 @@ zmqpubrawtx=${rawtx}`; indicator.className = 'w-3 h-3 bg-red-500 rounded-full mr-2'; status.textContent = 'Not Connected'; status.className = 'text-sm font-semibold text-lg text-red-400'; - connectBtn.disabled = false; - disconnectBtn.disabled = true; - isConnected = false; content.querySelector('#bitcoin-version').textContent = '--'; content.querySelector('#bitcoin-network').textContent = '--'; @@ -792,7 +754,7 @@ zmqpubrawtx=${rawtx}`; throw new Error('RPC username and password are required'); } - const url = `http://${host}:${port}`; + const url = getRpcUrl(host, port); const auth = btoa(`${username}:${password}`); const body = { @@ -830,112 +792,152 @@ zmqpubrawtx=${rawtx}`; return data.result; } - async function testZMQConnection() { - const rawblock = content.querySelector('#zmq-rawblock-input').value; - const rawtx = content.querySelector('#zmq-rawtx-input').value; + async function testTorConnection() { + const btn = content.querySelector('#test-tor-btn'); + const resultDiv = content.querySelector('#tor-test-result'); + const originalText = btn.textContent; + btn.textContent = 'Testing...'; + btn.disabled = true; - // Extract port from ZMQ address - const portMatch = rawblock.match(/:(\d+)$/); - if (!portMatch) { - throw new Error('Invalid ZMQ address format'); - } + const socksPort = parseInt( + content.querySelector('#tor-socks-port-input').value, + 10 + ); + const controlPort = parseInt( + content.querySelector('#tor-control-port-input').value, + 10 + ); - // Just validate format for now - actual ZMQ test would need socket connection - if (rawblock !== rawtx) { - throw new Error('ZMQ ports must match (both should be 28332)'); + try { + const [socksResult, controlResult] = await Promise.all([ + window.api.testTcpPort({ host: '127.0.0.1', port: socksPort }), + window.api.testTcpPort({ host: '127.0.0.1', port: controlPort }), + ]); + + renderConnectionResults(resultDiv, [ + { + label: 'SOCKS Port', + ok: Boolean(socksResult?.success), + message: socksResult?.success + ? `Port ${socksPort} reachable` + : socksResult?.error, + }, + { + label: 'Control Port', + ok: Boolean(controlResult?.success), + message: controlResult?.success + ? `Port ${controlPort} reachable` + : controlResult?.error, + }, + ]); + } catch (error) { + console.error('Tor test failed:', error); + renderConnectionResults(resultDiv, [ + { + label: 'Tor Connection', + ok: false, + message: error.message || String(error), + }, + ]); } - return { rawblock, rawtx }; + btn.textContent = originalText; + btn.disabled = false; } - // Test RPC connection - content - .querySelector('#test-connection-btn') - .addEventListener('click', async () => { - const btn = content.querySelector('#test-connection-btn'); - const originalText = btn.textContent; - btn.textContent = 'Testing...'; - btn.disabled = true; + async function testBitcoindConnection() { + const btn = content.querySelector('#test-connection-btn'); + const resultDiv = content.querySelector('#bitcoind-test-result'); + const originalText = btn.textContent; + btn.textContent = 'Testing...'; + btn.disabled = true; - try { - const info = await makeRPCCall('getblockchaininfo'); - const networkInfo = await makeRPCCall('getnetworkinfo'); + const host = content.querySelector('#rpc-host-input').value; + const port = content.querySelector('#rpc-port-input').value; + const zmqPort = parseInt(content.querySelector('#zmq-port-input').value, 10); + try { + const [blockchainInfo, networkInfo, restResponse, zmqResult] = + await Promise.allSettled([ + makeRPCCall('getblockchaininfo'), + makeRPCCall('getnetworkinfo'), + fetch(getRestUrl(host, port)), + window.api.testTcpPort({ host: '127.0.0.1', port: zmqPort }), + ]); + + const rpcOk = + blockchainInfo.status === 'fulfilled' && + networkInfo.status === 'fulfilled'; + const chain = + blockchainInfo.status === 'fulfilled' + ? blockchainInfo.value?.chain || 'unknown' + : null; + const blocks = + blockchainInfo.status === 'fulfilled' + ? blockchainInfo.value?.blocks || 0 + : null; + const version = + networkInfo.status === 'fulfilled' + ? networkInfo.value?.subversion || 'Unknown' + : null; + const restOk = + restResponse.status === 'fulfilled' && restResponse.value.ok; + const zmqOk = + zmqResult.status === 'fulfilled' && Boolean(zmqResult.value?.success); + + renderConnectionResults(resultDiv, [ + { + label: 'RPC', + ok: rpcOk, + message: rpcOk + ? `${version} • ${chain} • ${blocks.toLocaleString()} blocks` + : blockchainInfo.reason?.message || networkInfo.reason?.message, + }, + { + label: 'REST', + ok: restOk, + message: restOk + ? `${getRestUrl(host, port)} reachable` + : restResponse.status === 'fulfilled' + ? `HTTP ${restResponse.value.status}: ${restResponse.value.statusText}` + : restResponse.reason?.message, + }, + { + label: 'ZMQ', + ok: zmqOk, + message: zmqOk + ? `Port ${zmqPort} reachable` + : zmqResult.status === 'fulfilled' + ? zmqResult.value?.error + : zmqResult.reason?.message, + }, + ]); + + if (rpcOk) { updateConnectionStatus(true, { - version: networkInfo.subversion || 'Unknown', - network: info.chain, - blocks: info.blocks, - verificationprogress: info.verificationprogress, + version, + network: chain, + blocks, + verificationprogress: blockchainInfo.value?.verificationprogress, }); - - console.log('✅ RPC connection successful:', info); - } catch (error) { - console.error('❌ RPC connection failed:', error); + } else { updateConnectionStatus(false); - alert( - `RPC Connection failed: ${error.message}\n\nPlease check:\n- Bitcoin Core is running\n- RPC credentials are correct\n- RPC port matches your bitcoin.conf` - ); } - - btn.textContent = originalText; - btn.disabled = false; - }); - - // Connect button - content.querySelector('#connect-btn').addEventListener('click', async () => { - const btn = content.querySelector('#connect-btn'); - btn.textContent = 'Connecting...'; - btn.disabled = true; - - try { - // First save the configuration - const updatedConfig = buildConfig(); - localStorage.setItem('coinswap_config', JSON.stringify(updatedConfig)); - console.log('💾 Config saved:', updatedConfig); - - // Test the connection - const info = await makeRPCCall('getblockchaininfo'); - const networkInfo = await makeRPCCall('getnetworkinfo'); - - updateConnectionStatus(true, { - version: networkInfo.subversion || 'Unknown', - network: info.chain, - blocks: info.blocks, - verificationprogress: info.verificationprogress, - }); - - // Start status refresh timer - if (connectionTimer) clearInterval(connectionTimer); - connectionTimer = setInterval(async () => { - if (isConnected) { - try { - const info = await makeRPCCall('getblockchaininfo'); - content.querySelector('#block-height').textContent = - info.blocks.toLocaleString(); - const progress = (info.verificationprogress * 100).toFixed(1); - content.querySelector('#sync-progress').textContent = - `${progress}%`; - } catch (error) { - console.log('Status refresh failed'); - updateConnectionStatus(false); - if (connectionTimer) { - clearInterval(connectionTimer); - connectionTimer = null; - } - } - } - }, 5000); - - console.log('✅ Connected and monitoring status'); } catch (error) { - console.error('❌ Connection failed:', error); + console.error('Bitcoind test failed:', error); updateConnectionStatus(false); - alert(`Connection failed: ${error.message}`); + renderConnectionResults(resultDiv, [ + { + label: 'Bitcoind Test', + ok: false, + message: error.message || String(error), + }, + ]); } - btn.textContent = 'Connect'; + btn.textContent = originalText; btn.disabled = false; - }); + } // Toggle password visibility content @@ -956,42 +958,10 @@ zmqpubrawtx=${rawtx}`; } }); - // Disconnect button - content.querySelector('#disconnect-btn').addEventListener('click', () => { - if (connectionTimer) { - clearInterval(connectionTimer); - connectionTimer = null; - } - updateConnectionStatus(false); - console.log('🔌 Disconnected from Bitcoin Core'); - }); - - // Refresh status button content - .querySelector('#refresh-status-btn') - .addEventListener('click', async () => { - const btn = content.querySelector('#refresh-status-btn'); - btn.textContent = 'Refreshing...'; - btn.disabled = true; - - try { - const info = await makeRPCCall('getblockchaininfo'); - const networkInfo = await makeRPCCall('getnetworkinfo'); - - updateConnectionStatus(true, { - version: networkInfo.subversion || 'Unknown', - network: info.chain, - blocks: info.blocks, - verificationprogress: info.verificationprogress, - }); - } catch (error) { - console.log('Refresh failed:', error.message); - updateConnectionStatus(false); - } - - btn.textContent = 'Refresh Status'; - btn.disabled = false; - }); + .querySelector('#test-connection-btn') + .addEventListener('click', testBitcoindConnection); + content.querySelector('#test-tor-btn').addEventListener('click', testTorConnection); // Build config object from form values function buildConfig() { @@ -1006,6 +976,10 @@ zmqpubrawtx=${rawtx}`; console.error('Error loading existing config:', e); } + const zmqPortInput = content.querySelector('#zmq-port-input').value.trim(); + const zmqPort = parseInt(zmqPortInput, 10); + const hasValidZmqPort = Number.isInteger(zmqPort) && zmqPort > 0; + return { ...existingConfig, // Preserve wallet config rpc: { @@ -1024,11 +998,14 @@ zmqpubrawtx=${rawtx}`; tor_auth_password: content.querySelector('#tor-auth-password-input').value || undefined, }, - zmq: { - rawblock: content.querySelector('#zmq-rawblock-input').value, - rawtx: content.querySelector('#zmq-rawtx-input').value, - address: content.querySelector('#zmq-rawblock-input').value, - }, + zmq: hasValidZmqPort + ? { + port: zmqPort, + rawblock: getZmqAddress(zmqPort), + rawtx: getZmqAddress(zmqPort), + address: getZmqAddress(zmqPort), + } + : {}, setupComplete: true, setupDate: existingConfig.setupDate || new Date().toISOString(), lastModified: new Date().toISOString(), @@ -1071,13 +1048,12 @@ zmqpubrawtx=${rawtx}`; content.querySelector('#rpc-username-input').value = 'user'; content.querySelector('#rpc-password-input').value = ''; - // Reset ZMQ fields (both to 28332) - content.querySelector('#zmq-rawblock-input').value = - 'tcp://127.0.0.1:28332'; - content.querySelector('#zmq-rawtx-input').value = 'tcp://127.0.0.1:28332'; + // Reset ZMQ field + content.querySelector('#zmq-port-input').value = '28332'; // Update previews updateConfigPreviews(); + updateConnectionStatus(false); alert('Settings reset to defaults'); } diff --git a/src/components/swap/Swap.js b/src/components/swap/Swap.js index e8284ea..4f725e0 100644 --- a/src/components/swap/Swap.js +++ b/src/components/swap/Swap.js @@ -1,4 +1,9 @@ import { SwapStateManager, formatRelativeTime } from './SwapStateManager.js'; +import { + buildSwapHistoryMarkup, + loadSwapHistory, + summarizeSwapHistory, +} from './SwapHistory.js'; // ✅ ADD CACHE CONSTANTS const SWAP_DATA_CACHE_KEY = 'swap_data_cache'; @@ -94,6 +99,25 @@ export async function SwapComponent(container) { let useCustomHops = false; let customHopCount = 6; let networkFeeRate = 2; + let currentProtocol = 'v1'; + let currentNetwork = 'signet'; + let walletBalances = { + spendable: 0, + regular: 0, + swap: 0, + contract: 0, + fidelity: 0, + }; + let maxSwappableAmount = 0; + let balanceLoadPromise = null; + let balancesLoaded = false; + + try { + const config = JSON.parse(localStorage.getItem('coinswap_config') || '{}'); + currentNetwork = config.network || currentNetwork; + } catch (error) { + console.error('Failed to load swap config context:', error); + } // Restore user selections from saved state if available const savedSelections = await SwapStateManager.getUserSelections(); @@ -118,18 +142,37 @@ export async function SwapComponent(container) { let totalBalance = 0; const btcPrice = 50000; + function filterMakersByProtocol(makers) { + return makers.filter((maker) => + currentProtocol === 'v2' + ? maker.protocol === 'Taproot' + : maker.protocol !== 'Taproot' + ); + } + + function updateAvailableMakersCount() { + const makersCountEl = content.querySelector('#available-makers-count'); + if (makersCountEl) { + makersCountEl.textContent = availableMakers.length; + } + } + + try { + const protocolResult = await window.api.taker.getProtocol(); + currentProtocol = protocolResult.protocol || currentProtocol; + } catch (error) { + console.error('Failed to get authoritative protocol:', error); + } + // ✅ LOAD FROM CACHE IMMEDIATELY IF AVAILABLE if (cached && !cached.isStale) { console.log('⚡ Using cached swap data (still fresh)'); availableUtxos = cached.utxos || []; - availableMakers = cached.makers || []; + availableMakers = filterMakersByProtocol(cached.makers || []); totalBalance = cached.balance || 0; // Update makers count immediately since we have the data - const makersCountEl = content.querySelector('#available-makers-count'); - if (makersCountEl) { - makersCountEl.textContent = availableMakers.length; - } + updateAvailableMakersCount(); } // Fetch real UTXOs from API @@ -172,8 +215,9 @@ export async function SwapComponent(container) { async function fetchMakers(useCache = false) { // ✅ USE CACHE IF REQUESTED if (useCache && cached && cached.makers) { - availableMakers = cached.makers; + availableMakers = filterMakersByProtocol(cached.makers); console.log('✅ Loaded', availableMakers.length, 'makers from cache'); + updateAvailableMakersCount(); return availableMakers; } @@ -183,17 +227,27 @@ export async function SwapComponent(container) { if (data.success && data.offerbook) { const goodMakers = data.offerbook.goodMakers || []; - availableMakers = goodMakers - .filter((item) => item.offer !== null) - .map((item, index) => { - const offer = item.offer; - return { - minSize: offer.minSize || 0, - maxSize: offer.maxSize || 0, - fee: (offer.amountRelativeFeePct || 0).toFixed(1), - index: index, - }; - }); + availableMakers = filterMakersByProtocol( + goodMakers + .filter((item) => item.offer !== null) + .map((item, index) => { + const offer = item.offer; + const addressObj = item.address || {}; + const onionAddr = addressObj.onion_addr || ''; + const port = addressObj.port || '6102'; + const makerAddress = `${onionAddr}:${port}`; + return { + address: makerAddress, + minSize: offer.minSize || 0, + maxSize: offer.maxSize || 0, + baseFee: offer.baseFee || 0, + volumeFeePct: offer.amountRelativeFeePct || 0, + timeFeePct: offer.timeRelativeFeePct || 0, + protocol: item.protocol || 'Legacy', + index: index, + }; + }) + ); console.log( '✅ Loaded', @@ -202,10 +256,7 @@ export async function SwapComponent(container) { ); // Update UI count - const makersCountEl = content.querySelector('#available-makers-count'); - if (makersCountEl) { - makersCountEl.textContent = availableMakers.length; - } + updateAvailableMakersCount(); return availableMakers; } } catch (error) { @@ -220,6 +271,7 @@ export async function SwapComponent(container) { // ✅ USE CACHE IF REQUESTED if (useCache && cached && cached.balance) { totalBalance = cached.balance; + balancesLoaded = true; console.log('✅ Loaded balance from cache:', totalBalance); // Update UI if function exists @@ -233,7 +285,15 @@ export async function SwapComponent(container) { const data = await window.api.taker.getBalance(); if (data.success) { - totalBalance = data.balance.spendable; + walletBalances = { + spendable: data.balance.spendable || 0, + regular: data.balance.regular || 0, + swap: data.balance.swap || 0, + contract: data.balance.contract || 0, + fidelity: data.balance.fidelity || 0, + }; + totalBalance = walletBalances.spendable; + balancesLoaded = true; console.log('✅ Loaded balance from API:', totalBalance); // Update UI if function exists @@ -244,10 +304,48 @@ export async function SwapComponent(container) { } } catch (error) { console.error('Failed to fetch balance:', error); + balancesLoaded = true; } return 0; } + async function ensureBalancesLoaded() { + if (!balanceLoadPromise) { + balanceLoadPromise = fetchBalance(false).finally(() => { + balanceLoadPromise = null; + }); + } + + await balanceLoadPromise; + return walletBalances; + } + + async function fetchSwapLiquidity() { + try { + const data = await window.api.taker.checkSwapLiquidity(); + if (data.success && data.liquidity) { + maxSwappableAmount = data.liquidity.maxSwappable ?? 0; + walletBalances = { + ...walletBalances, + spendable: data.liquidity.spendable ?? walletBalances.spendable, + regular: data.liquidity.regular ?? walletBalances.regular, + swap: data.liquidity.swap ?? walletBalances.swap, + }; + return maxSwappableAmount; + } + } catch (error) { + console.error('Failed to fetch swap liquidity:', error); + } + + await ensureBalancesLoaded(); + + maxSwappableAmount = Math.max( + 0, + Math.max(walletBalances.regular || 0, walletBalances.swap || 0) - 3000 + ); + return maxSwappableAmount; + } + // Render UTXO list dynamically function renderUtxoList() { const utxoListContainer = content.querySelector('#utxo-list'); @@ -304,75 +402,59 @@ export async function SwapComponent(container) { }); } - // Render Recent Swaps section - // Render Recent Swaps section - async function renderRecentSwaps() { - const recentSwapsContainer = content.querySelector( - '#recent-swaps-container' - ); - if (!recentSwapsContainer) return; + async function renderSwapHistorySection() { + const swapHistoryContainer = content.querySelector('#swap-history-container'); + const swapHistoryStats = content.querySelector('#swap-history-stats'); + if (!swapHistoryContainer || !swapHistoryStats) return; - let recentSwaps = []; + let swapHistory = []; try { - const result = await window.api.swapReports.getAll(); - if (result.success && result.reports) { - recentSwaps = result.reports - .filter((report) => report.status === 'completed') - .slice(0, 5) - .map((report) => ({ - id: report.swapId || `swap_${Date.now()}`, - completedAt: report.completedAt || Date.now(), - amount: report.amount || 0, - hops: (report.report?.makersCount || 0) + 1, - })); - } + swapHistory = await loadSwapHistory(); } catch (error) { - console.error('Failed to load recent swaps:', error); - } - - if (recentSwaps.length === 0) { - recentSwapsContainer.innerHTML = ` -
-

🔄

-

No swaps yet

-

Your completed swaps will appear here

-
- `; - return; - } - - recentSwapsContainer.innerHTML = recentSwaps - .map((swap) => { - const btcAmount = (swap.amount / 100000000).toFixed(8); - const timeAgo = formatRelativeTime(swap.completedAt); - - return ` -
-
- -
-
-
- Coinswap - ${btcAmount} BTC -
-
- ${timeAgo} - ${swap.hops} hops -
-
+ swapHistoryStats.innerHTML = ''; + swapHistoryContainer.innerHTML = ` +
+

Unable to load swap history

+

${error.message || 'Please try again.'}

`; - }) - .join(''); - - // Add click handlers for swap history items - content.querySelectorAll('.swap-history-item').forEach((item) => { - item.addEventListener('click', () => { - const swapId = item.dataset.swapId; - viewSwapReport(swapId); + return; + } + const stats = summarizeSwapHistory(swapHistory); + + swapHistoryStats.innerHTML = + stats.totalSwaps > 0 + ? ` +
+

Total Swaps

+

${stats.totalSwaps}

+
+
+

Total Amount

+

${( + stats.totalAmount / 100000000 + ).toFixed(8)} BTC

+
+
+

Total Fees Paid

+

${stats.totalFees.toLocaleString()} sats

+
+
+

Avg Fee Paid

+

${stats.avgFeePaid.toLocaleString()} sats

+
+ ` + : ''; + + swapHistoryContainer.innerHTML = buildSwapHistoryMarkup(swapHistory); + swapHistoryContainer + .querySelectorAll('.swap-history-row') + .forEach((row) => { + row.addEventListener('click', () => { + const swapId = row.dataset.swapId; + viewSwapReport(swapId); + }); }); - }); } // View swap report from history @@ -382,7 +464,13 @@ export async function SwapComponent(container) { if (result.success && result.report) { import('./SwapReport.js').then((module) => { container.innerHTML = ''; - module.SwapReportComponent(container, result.report.report); + module.SwapReportComponent(container, { + ...result.report, + ...result.report.report, + protocol: result.report.protocol ?? 'v1', + isTaproot: result.report.isTaproot ?? false, + protocolVersion: result.report.protocolVersion ?? 1, + }); }); } else { console.error('Swap report not found for ID:', swapId); @@ -426,53 +514,68 @@ export async function SwapComponent(container) { }, 0); } - // Calculate fees based on hops/makers - function calculateFees(amount) { - const hops = getNumberOfHops(); - const makers = getNumberOfMakers(); + function getTopCandidateMakers() { + return availableMakers.slice(0, getNumberOfMakers()); + } - const baseFeePercent = 0.1; + function getBlockIntervalSeconds() { + if (currentNetwork === 'mainnet' || currentNetwork === 'bitcoin') return 600; + if (currentNetwork === 'regtest') return 0; + return 30; + } - // Network fees: estimate ~250 vBytes per hop - const txSize = 250; - const networkFee = networkFeeRate * txSize * hops; + function formatEstimatedTime(seconds) { + if (seconds <= 0) return 'Instant'; + const mins = Math.floor(seconds / 60); + const secs = seconds % 60; + if (mins === 0) return `${secs}s`; + if (secs === 0) return `${mins}m`; + return `${mins}m ${secs}s`; + } - const makerFeePercent = baseFeePercent * makers; - const makerFee = (amount * makerFeePercent) / 100; + // Estimate fees from the top candidate makers shown in the UI. + function calculateFees(amount) { + const hops = getNumberOfHops(); + const topCandidateMakers = getTopCandidateMakers(); + const avgFundingTxSize = 300; + const fundingTxs = hops; + const networkFee = fundingTxs * avgFundingTxSize * networkFeeRate; + const makerFee = topCandidateMakers.reduce((sum, maker, index) => { + const refundLocktime = 20 * (index + 1); + const volumeFee = amount * ((maker.volumeFeePct || 0) / 100); + const timeFee = + refundLocktime * amount * ((maker.timeFeePct || 0) / 100); + return sum + (maker.baseFee || 0) + volumeFee + timeFee; + }, 0); const totalFee = makerFee + networkFee; return { makerFeeSats: Math.floor(makerFee), networkFeeSats: Math.floor(networkFee), totalFeeSats: Math.floor(totalFee), - makerFeePercent: makerFeePercent.toFixed(2), + makerFeePercent: + amount > 0 ? ((makerFee / amount) * 100).toFixed(2) : '0.00', + fundingTxs, + avgFundingTxSize, }; } function calculateSwapDetails() { const hops = getNumberOfHops(); const makers = getNumberOfMakers(); - - const baseTime = 10; - const baseFeePercent = 0.1; - - // Network fees: estimate ~250 vBytes per hop - const txSize = 250; - const networkFee = networkFeeRate * txSize * hops; - - const estimatedTime = hops * baseTime; - const makerFeePercent = baseFeePercent * makers; - const makerFee = (swapAmount * makerFeePercent) / 100; - const totalFee = makerFee + networkFee; + const feeDetails = calculateFees(swapAmount); + const estimatedTime = getBlockIntervalSeconds() * hops; return { - hops: hops, - makers: makers, - time: estimatedTime, - makerFeePercent: makerFeePercent.toFixed(2), - makerFeeSats: Math.floor(makerFee), - networkFeeSats: Math.floor(networkFee), - totalFeeSats: Math.floor(totalFee), + hops, + makers, + timeSeconds: estimatedTime, + makerFeePercent: feeDetails.makerFeePercent, + makerFeeSats: feeDetails.makerFeeSats, + networkFeeSats: feeDetails.networkFeeSats, + totalFeeSats: feeDetails.totalFeeSats, + fundingTxs: feeDetails.fundingTxs, + avgFundingTxSize: feeDetails.avgFundingTxSize, }; } @@ -542,17 +645,30 @@ export async function SwapComponent(container) { content.querySelector('#num-hops-display').textContent = details.hops + ' hop' + (details.hops !== 1 ? 's' : ''); content.querySelector('#estimated-time').textContent = - details.time + ' min'; + formatEstimatedTime(details.timeSeconds); + content.querySelector('#selected-makers-display').textContent = + getTopCandidateMakers() + .map((maker) => maker.address) + .join(', ') || 'None selected'; content.querySelector('#maker-fee-percent').textContent = details.makerFeePercent + '%'; content.querySelector('#maker-fee-sats').textContent = - '~' + details.makerFeeSats.toLocaleString() + ' sats'; + details.makerFeeSats.toLocaleString() + ' sats'; content.querySelector('#network-fee-sats').textContent = - '~' + details.networkFeeSats.toLocaleString() + ' sats'; + details.networkFeeSats.toLocaleString() + ' sats'; content.querySelector('#network-fee-rate').textContent = networkFeeRate + ' sat/vB'; + content.querySelector('#funding-txs-count').textContent = + details.fundingTxs.toString(); + content.querySelector('#avg-funding-tx-size').textContent = + `${details.avgFundingTxSize} vB`; content.querySelector('#total-fee-sats').textContent = - '~' + details.totalFeeSats.toLocaleString() + ' sats'; + details.totalFeeSats.toLocaleString() + ' sats'; + + const maxSwapEl = content.querySelector('#max-swappable-amount'); + if (maxSwapEl) { + maxSwapEl.textContent = `${maxSwappableAmount.toLocaleString()} sats`; + } // Total = Amount - Fees (what user receives) content.querySelector('#total-amount').textContent = @@ -594,13 +710,17 @@ export async function SwapComponent(container) { } // Check if amount exceeds balance - if (swapAmount > totalBalance && totalBalance > 0) { + if (balancesLoaded && swapAmount > maxSwappableAmount) { warnings.push( - `Swap amount (${swapAmount.toLocaleString()} sats) exceeds available balance` + `Swap amount (${swapAmount.toLocaleString()} sats) exceeds swappable balance` ); } } + if (!balancesLoaded) { + warnings.push('Loading wallet balances...'); + } + // Check if enough makers available if (availableMakers.length > 0 && makersNeeded > availableMakers.length) { warnings.push( @@ -764,10 +884,7 @@ export async function SwapComponent(container) { return; // In manual mode, amount is from UTXOs } - const details = calculateSwapDetails(); - const estimatedFees = details.totalFeeSats || 1500; - - swapAmount = Math.max(0, totalBalance - estimatedFees - 500); + swapAmount = Math.max(0, maxSwappableAmount); // Also check maker max size if (availableMakers.length > 0) { @@ -808,15 +925,14 @@ export async function SwapComponent(container) {

Coinswap

Perform private Bitcoin swaps through multiple makers

- +
+ +
+

Swap History

+
+
+

Loading swap history...

`; @@ -1046,10 +1153,14 @@ export async function SwapComponent(container) { const balanceEl = content.querySelector('#available-balance-sats'); const balanceBtcEl = content.querySelector('#available-balance-btc'); if (balanceEl) { - balanceEl.textContent = totalBalance.toLocaleString() + ' sats'; + balanceEl.textContent = maxSwappableAmount.toLocaleString() + ' sats'; } if (balanceBtcEl) { - balanceBtcEl.textContent = (totalBalance / 100000000).toFixed(8) + ' BTC'; + balanceBtcEl.textContent = (maxSwappableAmount / 100000000).toFixed(8) + ' BTC'; + } + const input = content.querySelector('#swap-amount-input'); + if (input) { + input.max = String(maxSwappableAmount); } } @@ -1118,14 +1229,6 @@ export async function SwapComponent(container) { saveCurrentSelections(); }); - // View All Swaps button - content.querySelector('#view-all-swaps').addEventListener('click', () => { - import('./SwapHistory.js').then((module) => { - container.innerHTML = ''; - module.SwapHistoryComponent(container); - }); - }); - content .querySelector('#start-coinswap-btn') .addEventListener('click', async () => { @@ -1290,17 +1393,16 @@ export async function SwapComponent(container) { fetchMakers(false), fetchBalance(false), ]).then(async ([utxos, makers, balance]) => { + await fetchSwapLiquidity(); + // Save to cache saveSwapDataToCache(utxos, makers, balance); updateBalanceUI(); - const makersCountEl = content.querySelector('#available-makers-count'); - if (makersCountEl) { - makersCountEl.textContent = makers.length; - } + updateAvailableMakersCount(); renderUtxoList(); - await renderRecentSwaps(); + await renderSwapHistorySection(); // Restore UTXO selections if (selectedUtxos.length > 0) { @@ -1333,7 +1435,11 @@ export async function SwapComponent(container) { console.log('⚡ Using cached swap data (still fresh)'); // Just use cache - render immediately renderUtxoList(); - renderRecentSwaps(); + ensureBalancesLoaded().then(() => fetchSwapLiquidity()).then(() => { + updateBalanceUI(); + updateSummary(); + }); + renderSwapHistorySection(); // Restore UTXO selections if (selectedUtxos.length > 0) { @@ -1367,26 +1473,22 @@ export async function SwapComponent(container) { (async () => { try { const protocolResult = await window.api.taker.getProtocol(); - const protocol = protocolResult.protocol; - - const banner = content.querySelector('#protocol-banner'); - const title = content.querySelector('#protocol-warning-title'); - const text = content.querySelector('#protocol-warning-text'); - - if (banner && title && text) { - if (protocol === 'v2') { - title.textContent = '⚡ You Can Only Swap With Taproot Makers'; - text.textContent = - 'Your wallet is configured for Taproot swaps. Selecting Legacy makers will cause the swap to fail.'; - } else { - title.textContent = '🔒 You Can Only Swap With Legacy Makers'; - text.textContent = - 'Your wallet is configured for Legacy swaps. Selecting Taproot makers will cause the swap to fail.'; - } - banner.classList.remove('hidden'); + const nextProtocol = protocolResult.protocol || 'v1'; + if (nextProtocol !== currentProtocol) { + currentProtocol = nextProtocol; + await fetchMakers(false); + updateSummary(); } } catch (error) { console.error('Failed to get protocol:', error); } })(); + + (async () => { + try { + await ensureBalancesLoaded(); + } catch (error) { + console.error('Failed to prime wallet balances:', error); + } + })(); } diff --git a/src/components/swap/SwapHistory.js b/src/components/swap/SwapHistory.js index b52aa36..787c34e 100644 --- a/src/components/swap/SwapHistory.js +++ b/src/components/swap/SwapHistory.js @@ -1,202 +1,184 @@ -import { - SwapStateManager, - formatRelativeTime, - formatElapsedTime, -} from './SwapStateManager.js'; +import { formatRelativeTime } from './SwapStateManager.js'; -let swapHistory = []; - -async function loadSwapHistory() { - try { - const result = await window.api.swapReports.getAll(); - if (result.success && result.reports) { - swapHistory = result.reports - .filter((report) => report.status === 'completed') // ✅ Only show completed - .map((report) => ({ - id: report.swapId || `swap_${Date.now()}`, - completedAt: report.completedAt || Date.now(), - amount: report.amount || 0, - totalOutputAmount: report.report?.totalOutputAmount || 0, - makersCount: report.report?.makersCount || 0, - hops: (report.report?.makersCount || 0) + 1, - totalFee: report.report?.totalFee || 0, - feePercentage: report.report?.feePercentage || 0, - durationSeconds: - Math.floor((report.completedAt - report.startedAt) / 1000) || 0, - status: report.status, - report: report.report, - })); - } - } catch (error) { - console.error('Failed to load swap history:', error); - } +function satsToBtc(sats) { + if (typeof sats !== 'number' || isNaN(sats)) return '0.00000000'; + return (sats / 100000000).toFixed(8); } -export async function SwapHistoryComponent(container) { - if (container.querySelector('#swap-history-content')) { - console.log('⚠️ SwapHistory component already rendered, skipping'); - return; - } - - console.log('📜 SwapHistoryComponent loading...'); - await loadSwapHistory(); +function formatDuration(seconds) { + if (typeof seconds !== 'number' || isNaN(seconds)) return '0m 0s'; + const mins = Math.floor(seconds / 60); + const secs = Math.floor(seconds % 60); + return `${mins}m ${secs}s`; +} - const content = document.createElement('div'); - content.id = 'swap-history-content'; +function formatDate(timestamp) { + const date = new Date(timestamp); + return date.toLocaleDateString('en-US', { + year: 'numeric', + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + }); +} - function satsToBtc(sats) { - if (typeof sats !== 'number' || isNaN(sats)) return '0.00000000'; - return (sats / 100000000).toFixed(8); - } +function getProtocolLabel(report) { + const protocol = report.protocol || report.report?.protocol || 'v1'; + return protocol === 'v2' ? 'Taproot' : 'Legacy P2WSH'; +} - function formatDuration(seconds) { - if (typeof seconds !== 'number' || isNaN(seconds)) return '0m 0s'; - const mins = Math.floor(seconds / 60); - const secs = Math.floor(seconds % 60); - return `${mins}m ${secs}s`; - } +function getProtocolBadgeClasses(protocolLabel) { + return protocolLabel === 'Taproot' + ? 'bg-purple-500/20 text-purple-400' + : 'bg-blue-500/20 text-blue-400'; +} - function formatDate(timestamp) { - const date = new Date(timestamp); - return date.toLocaleDateString('en-US', { - year: 'numeric', - month: 'short', - day: 'numeric', - hour: '2-digit', - minute: '2-digit', - }); - } +function normalizeSwapReport(report) { + const nested = report.report || {}; + const startedAt = report.startedAt || nested.startedAt || Date.now(); + const completedAt = + report.completedAt || report.failedAt || nested.completedAt || Date.now(); + const totalFee = + nested.totalFee || + nested.total_fee || + report.totalFee || + report.total_fee || + 0; + const makersCount = + nested.makersCount || nested.makers_count || report.makerCount || 0; - async function viewSwapReport(swapId) { - try { - const result = await window.api.swapReports.get(swapId); - console.log('📋 Raw result from API:', result); // ← ADD THIS - console.log('📋 Report from result:', result.report.report); // ← ADD THIS + return { + id: report.swapId || nested.swapId || `swap_${Date.now()}`, + completedAt, + amount: report.amount || nested.amount || nested.targetAmount || 0, + totalOutputAmount: + nested.totalOutputAmount || nested.total_output_amount || 0, + makersCount, + hops: makersCount + 1, + totalFee, + feePercentage: + nested.feePercentage || nested.fee_percentage || report.feePercentage || 0, + durationSeconds: Math.max( + 0, + Math.floor((completedAt - startedAt) / 1000) || 0 + ), + status: report.status || 'completed', + protocol: report.protocol || nested.protocol || 'v1', + report: nested, + }; +} - if (result.success && result.report) { - import('./SwapReport.js').then((module) => { - container.innerHTML = ''; - const fullReport = { - ...result.report.report, - protocol: result.report.protocol ?? 'v1', - isTaproot: result.report.isTaproot ?? false, - protocolVersion: result.report.protocolVersion ?? 1, - }; - module.SwapReportComponent(container, fullReport); - }); - } else { - console.error('Swap report not found for ID:', swapId); - alert('Swap report not found'); - } - } catch (error) { - console.error('Failed to load swap report:', error); - alert('Failed to load swap report: ' + error.message); +export async function loadSwapHistory() { + try { + const result = await window.api.swapReports.getAll(); + if (!result.success) { + throw new Error(result.error || 'Failed to load swap history'); } + return (result.reports || []).map(normalizeSwapReport); + } catch (error) { + console.error('Failed to load swap history:', error); + throw error; } +} - function buildSwapHistoryList() { - if (swapHistory.length === 0) { - return ` -
-
🔄
-

No Swap History

-

You haven't completed any coinswaps yet.

- -
- `; - } +export function summarizeSwapHistory(swapHistory) { + const totalSwaps = swapHistory.length; + const totalAmount = swapHistory.reduce((sum, s) => sum + (s.amount || 0), 0); + const totalFees = swapHistory.reduce((sum, s) => sum + (s.totalFee || 0), 0); + const avgFeePaid = + totalSwaps > 0 ? Math.round(totalFees / totalSwaps) : 0; + return { + totalSwaps, + totalAmount, + totalFees, + avgFeePaid, + }; +} + +export function buildSwapHistoryMarkup(swapHistory) { + if (swapHistory.length === 0) { return ` -
+
+
🔄
+

No Swap History

+

Your completed and failed swaps will appear here.

+
+ `; + } + + return ` +
+
+
Swap
+
Status
+
Protocol
+
Amount
+
Fee Paid
+
When
+
+
${swapHistory - .map((swap, index) => { - const btcAmount = satsToBtc(swap.amount); - const outputBtc = satsToBtc(swap.totalOutputAmount); - const timeAgo = formatRelativeTime(swap.completedAt); - const dateStr = formatDate(swap.completedAt); - const duration = formatDuration(swap.durationSeconds); + .map((swap) => { + const protocolLabel = getProtocolLabel(swap); + const isFailed = swap.status === 'failed'; return ` -
-
- -
- -
- - -
-
- Coinswap - Completed - ${swap.hops} hops -
-
- ${timeAgo} - - ${dateStr} - - ${duration} -
-
- - -
-
${btcAmount} BTC
-
${swap.amount.toLocaleString()} sats
+
+
+

Coinswap

+

${swap.hops} hops • ${swap.makersCount} makers

- - -
- - - +
+ + ${isFailed ? 'Failed' : 'Completed'} +
-
- - -
- Makers -

${swap.makersCount}

+ + ${protocolLabel} +
- Fee -

${swap.feePercentage?.toFixed(2) || '0.00'}%

+

${satsToBtc(swap.amount)} BTC

+

${swap.amount.toLocaleString()} sats

- Total Fee -

${(swap.totalFee || 0).toLocaleString()} sats

+

${(swap.totalFee || 0).toLocaleString()} sats

+

${(swap.feePercentage || 0).toFixed(2)}%

- Output -

${outputBtc} BTC

+

${formatRelativeTime(swap.completedAt)}

+

${formatDate(swap.completedAt)} • ${formatDuration(swap.durationSeconds)}

-
- `; + `; }) .join('')}
- `; - } +
+ `; +} - // Calculate stats - const totalSwaps = swapHistory.length; - const totalVolume = swapHistory.reduce((sum, s) => sum + (s.amount || 0), 0); - const totalFees = swapHistory.reduce((sum, s) => sum + (s.totalFee || 0), 0); - const avgHops = - totalSwaps > 0 - ? ( - swapHistory.reduce((sum, s) => sum + (s.hops || 0), 0) / totalSwaps - ).toFixed(1) - : 0; +export async function SwapHistoryComponent(container) { + if (container.querySelector('#swap-history-content')) return; + let swapHistory = []; + let stats = summarizeSwapHistory([]); + let loadError = null; + + try { + swapHistory = await loadSwapHistory(); + stats = summarizeSwapHistory(swapHistory); + } catch (error) { + loadError = error; + } + + const content = document.createElement('div'); + content.id = 'swap-history-content'; content.innerHTML = ` -
- +
-
-
-

Swap History

-

View all your completed coinswap transactions

-
- ${ - totalSwaps > 0 - ? ` - - ` - : '' - } -
+

Swap History

+

View all your completed and failed coinswaps

- ${ - totalSwaps > 0 + loadError + ? ` +
+

Unable to load swap history

+

${loadError.message || 'Please try again.'}

+
+ ` + : '' + } + + ${ + stats.totalSwaps > 0 ? `

Total Swaps

-

${totalSwaps}

+

${stats.totalSwaps}

-

Total Volume

-

${satsToBtc(totalVolume)} BTC

+

Total Amount

+

${satsToBtc(stats.totalAmount)} BTC

Total Fees Paid

-

${totalFees.toLocaleString()} sats

+

${stats.totalFees.toLocaleString()} sats

-

Avg. Hops

-

${avgHops}

+

Avg Fee Paid

+

${stats.avgFeePaid.toLocaleString()} sats

` : '' } -
- ${buildSwapHistoryList()} + ${loadError ? '' : buildSwapHistoryMarkup(swapHistory)}
`; container.appendChild(content); - // Event Listeners content.querySelector('#back-to-swap')?.addEventListener('click', () => { import('./Swap.js').then((module) => { container.innerHTML = ''; @@ -264,31 +241,38 @@ export async function SwapHistoryComponent(container) { }); }); - content.querySelector('#start-first-swap')?.addEventListener('click', () => { - import('./Swap.js').then((module) => { - container.innerHTML = ''; - module.SwapComponent(container); - }); - }); + content.querySelectorAll('.swap-history-row').forEach((row) => { + row.addEventListener('click', async () => { + try { + const swapId = row.dataset.swapId; + const result = await window.api.swapReports.get(swapId); - content.querySelector('#clear-history')?.addEventListener('click', () => { - if ( - confirm( - 'Are you sure you want to clear all swap history? This cannot be undone.' - ) - ) { - SwapStateManager.clearSwapHistory(); - // Re-render the component - container.innerHTML = ''; - SwapHistoryComponent(container); - } - }); + if (!result.success || !result.report) { + throw new Error(`Swap report not found for ${swapId}`); + } - // Click handlers for swap rows - content.querySelectorAll('.swap-history-row').forEach((row) => { - row.addEventListener('click', () => { - const swapId = row.dataset.swapId; - viewSwapReport(swapId); + const module = await import('./SwapReport.js'); + + container.innerHTML = ''; + const fullReport = { + ...result.report, + ...result.report.report, + protocol: result.report.protocol ?? 'v1', + isTaproot: result.report.isTaproot ?? false, + protocolVersion: result.report.protocolVersion ?? 1, + }; + module.SwapReportComponent(container, fullReport); + } catch (error) { + console.error('Failed to load swap report:', error); + container.innerHTML = ` +
+
+

Unable to open swap report

+

${error.message || 'Please try again.'}

+
+
+ `; + } }); }); } diff --git a/src/components/swap/SwapReport.js b/src/components/swap/SwapReport.js index f7cf8f3..df9f063 100644 --- a/src/components/swap/SwapReport.js +++ b/src/components/swap/SwapReport.js @@ -11,12 +11,12 @@ export function SwapReportComponent(container, swapReport) { content.innerHTML = `

Error: No swap report data available

- +
`; container.appendChild(content); content.querySelector('#back-btn')?.addEventListener('click', () => { - if (window.appManager) window.appManager.renderComponent('wallet'); + if (window.appManager) window.appManager.renderComponent('swap'); }); return; } @@ -252,16 +252,6 @@ export function SwapReportComponent(container, swapReport) { const takerIncoming = allTxids[allTxids.length - 1]; // [3] return ` -
-

- Taproot Coinswap (V2 Protocol) -

-

- Uses MuSig2 for cooperative signatures. Only 2 on-chain transactions visible: - your outgoing contract and the final sweep. All ${report.makersCount} makers coordinate off-chain. -

-
-

@@ -328,7 +318,7 @@ export function SwapReportComponent(container, swapReport) { return `
-

+

Maker ${idx + 1}

@@ -434,155 +424,69 @@ export function SwapReportComponent(container, swapReport) { .join(''); } - // Build circular flow visualization - true circle layout - function buildCircularFlowHtml() { - const totalNodes = report.makersCount + 1; // You + makers (You appears once, at start/end position) - const size = 350; - const centerX = size / 2; - const centerY = size / 2; - const radius = 160; - - // Calculate positions around a circle - // Start at top (You), go clockwise through makers, back to You - const allNodes = []; - - // "You" at top (both start and end point) - allNodes.push({ - type: 'you', - label: 'You', - angle: -Math.PI / 2, // Top - color: '#FF6B35', - }); - - // Makers distributed around the circle clockwise - for (let i = 0; i < report.makersCount; i++) { - // Distribute makers evenly around the circle (excluding the "You" position) - const angleStep = (2 * Math.PI) / (report.makersCount + 1); - const angle = -Math.PI / 2 + angleStep * (i + 1); - - allNodes.push({ - type: 'maker', - index: i, - label: `M${i + 1}`, - angle: angle, - color: makerColors[i % makerColors.length], - }); - } - - // Calculate x, y positions - const nodePositions = allNodes.map((node) => ({ - ...node, - x: centerX + radius * Math.cos(node.angle), - y: centerY + radius * Math.sin(node.angle), - })); - - // Build SVG arrows (curved paths around the circle) - let arrowsHtml = ''; - - for (let i = 0; i < nodePositions.length; i++) { - const from = nodePositions[i]; - const toIndex = (i + 1) % nodePositions.length; - const to = nodePositions[toIndex]; - - const midAngle = (from.angle + to.angle) / 2; - let adjustedMidAngle = midAngle; - if (Math.abs(from.angle - to.angle) > Math.PI) { - adjustedMidAngle = midAngle + Math.PI; - } + function getProtocolInfoLines() { + return ` +
+

Save Money: Lesser Fees than V1 swaps.

+

Efficient: Combined tapscript with Musig2 + HTLC leaves.

+
+
+

Anonymity Set — Legacy: All P2WSH UTXOs.

+

Anonymity Set — Taproot: All Taproot Single Sig UTXOs.

+
+ `; + } - const arcRadius = radius + 20; - const midX = centerX + arcRadius * Math.cos(adjustedMidAngle); - const midY = centerY + arcRadius * Math.sin(adjustedMidAngle); - - const circleRadius = 40; - - // Calculate start point offset along the arc from 'from' node - const startAngle = Math.atan2(midY - from.y, midX - from.x); - const startX = from.x + circleRadius * Math.cos(startAngle); - const startY = from.y + circleRadius * Math.sin(startAngle); - - // Calculate end point offset along the arc to 'to' node - const arrowheadLength = -2; // Negative to pull it back - const endAngle = Math.atan2(midY - to.y, midX - to.x); - const endX = to.x + (circleRadius + arrowheadLength) * Math.cos(endAngle); - const endY = to.y + (circleRadius + arrowheadLength) * Math.sin(endAngle); - - const color = from.color; - - arrowsHtml += ` - - - - - - - - - - - - - - - - - + // Build swap circuit visualization + function buildCircularFlowHtml() { + const nodes = [ + { label: 'You', sublabel: 'Outgoing', color: '#FF6B35' }, + ...report.makerAddresses.map((addr, index) => ({ + label: `Maker ${index + 1}`, + sublabel: truncateAddress(addr, 10, 6), + color: makerColors[index % makerColors.length], + makerIndex: index, + })), + { label: 'You', sublabel: 'Incoming', color: '#10B981' }, + ]; + const columns = Math.max(nodes.length * 2 - 1, 1); + const flowItems = nodes.flatMap((node, index) => { + const nodeHtml = ` +
+

${node.label}

+

${node.sublabel}

+
`; - } - // Build node elements - let nodesHtml = nodePositions - .map((node, idx) => { - const isYou = node.type === 'you'; + if (index === nodes.length - 1) { + return [nodeHtml]; + } - return ` -
-
-
- ${ - isYou - ? '👤' - : `${node.label}` - } -
-
-

${isYou ? 'You' : `Maker ${node.index + 1}`}

- ${!isYou ? `

${truncateAddress(report.makerAddresses[node.index] || '', 8, 4)}

` : '

Start/End

'} + return [ + nodeHtml, + ` +
+ + +
-
-
- `; - }) - .join(''); + `, + ]; + }); return ` -
- - ${arrowsHtml} - - ${nodesHtml} - - -
-

${report.makersCount + 1}

-

Total Hops

+
+
+ ${flowItems.join('')} +
-
- `; + `; } // UI @@ -674,11 +578,11 @@ export function SwapReportComponent(container, swapReport) {

Coinswap Report

-

Privacy-Enhanced Bitcoin Transaction

+

View Detailed Swap Data.

@@ -697,10 +601,16 @@ export function SwapReportComponent(container, swapReport) {

- 🔗 Transaction Flow + + + + + + + Swap Circuit (Click on makers for details)

-

Your coins flow through multiple makers and return to you with broken transaction links

+

Your coins move across the swap circuit and come back with broken transaction links.

@@ -710,34 +620,12 @@ export function SwapReportComponent(container, swapReport) { -
-

- ℹ️ ${isV2Swap ? 'Taproot Protocol (V2)' : 'P2WSH Protocol (V1)'} +
+

+ ℹ️ Protocol Details

-
- ${ - isV2Swap - ? ` -
-

⚡ MuSig2: Cooperative signatures between makers

-

🔗 One TX: Only 1 on-chain funding transaction

-
-
-

🔓 Privacy: Same link-breaking as V1

-

💰 Efficient: Lower on-chain footprint

-
- ` - : ` -
-

🔄 Circular Path: Coins flow You → Makers → You

-

⚛️ Atomic Swaps: HTLCs ensure safe exchanges

-
-
-

🔓 Link Breaking: Each hop uses different UTXOs

-

👁️ Result: Observers cannot trace the path

-
- ` - } +
+ ${getProtocolInfoLines()}
@@ -769,10 +657,10 @@ export function SwapReportComponent(container, swapReport) { 🔗

- ${isV2Swap ? '2' : report.makersCount + 1} + ${report.totalFundingTxs}

- ${isV2Swap ? 'On-chain TXs (V2)' : `${report.makersCount} makers used`} + ${isV2Swap ? 'Funding transactions observed' : `${report.makersCount} makers used`}

@@ -842,68 +730,18 @@ export function SwapReportComponent(container, swapReport) {
- -
-

- 🔒 Privacy Achieved -

-
    -
  • - - ${report.makersCount + 1} transaction hops completed -
  • -
  • - - Links broken at each hop -
  • -
  • - - No common input ownership -
  • -
  • - - Enhanced anonymity set -
  • -
-
-

📦 UTXO Summary - - - - UTXOs (Unspent Transaction Outputs) used in the swap process - -

-
- - Inputs - - Your UTXOs that were spent to initiate the swap - - +
+ Outgoing Regular/Swap UTXOs ${report.inputUtxos.length}
-
- - Regular Outputs - - Standard outputs returned to your wallet - - - ${report.outputRegularUtxos.length} -
-
- - Swap Coins - - Privacy-enhanced coins from the swap (different history) - - +
+ Incoming Swap UTXOs ${report.outputSwapUtxos.length}
@@ -918,7 +756,7 @@ export function SwapReportComponent(container, swapReport) { 📥 Export Report
@@ -975,14 +813,14 @@ export function SwapReportComponent(container, swapReport) { // Back to wallet content.querySelector('#back-to-wallet').addEventListener('click', () => { if (window.appManager) { - window.appManager.renderComponent('wallet'); + window.appManager.renderComponent('swap'); } }); // Done button content.querySelector('#done-btn').addEventListener('click', () => { if (window.appManager) { - window.appManager.renderComponent('wallet'); + window.appManager.renderComponent('swap'); } }); } diff --git a/src/components/wallet/TransactionsList.js b/src/components/wallet/TransactionsList.js index 35c4c6e..9e26a9e 100644 --- a/src/components/wallet/TransactionsList.js +++ b/src/components/wallet/TransactionsList.js @@ -1,6 +1,7 @@ export function TransactionsListComponent(container) { // State let currentFilter = 'all'; + let currentSort = 'newest'; let allTransactions = []; let currentPage = 0; const transactionsPerPage = 20; @@ -93,19 +94,24 @@ export function TransactionsListComponent(container) { } function getFilteredTransactions() { - // First sort all transactions - const sorted = sortTransactionsByTime(allTransactions); + let filtered = [...allTransactions]; - if (currentFilter === 'all') { - return sorted; - } else if (currentFilter === 'received') { - return sorted.filter((tx) => getTransactionType(tx) === 'received'); + if (currentFilter === 'received') { + filtered = filtered.filter((tx) => getTransactionType(tx) === 'received'); } else if (currentFilter === 'sent') { - return sorted.filter((tx) => getTransactionType(tx) === 'sent'); + filtered = filtered.filter((tx) => getTransactionType(tx) === 'sent'); } else if (currentFilter === 'swaps') { - return sorted.filter((tx) => getTransactionType(tx) === 'swap'); + filtered = filtered.filter((tx) => getTransactionType(tx) === 'swap'); } - return sorted; + + if (currentSort === 'amount') { + return filtered.sort( + (a, b) => + Math.abs(b.detail.amount.sats || 0) - Math.abs(a.detail.amount.sats || 0) + ); + } + + return sortTransactionsByTime(filtered); } function updateFilterButtons() { @@ -128,6 +134,26 @@ export function TransactionsListComponent(container) { renderTransactions(); } + function updateSortButtons() { + const buttons = content.querySelectorAll('.sort-btn'); + buttons.forEach((btn) => { + const sort = btn.dataset.sort; + if (sort === currentSort) { + btn.className = + 'sort-btn bg-[#FF6B35] text-white px-4 py-2 rounded-lg text-sm font-semibold text-lg transition-colors'; + } else { + btn.className = + 'sort-btn bg-[#0f1419] hover:bg-[#242d3d] border border-gray-700 text-gray-400 px-4 py-2 rounded-lg text-sm font-semibold text-lg transition-colors'; + } + }); + } + + function setSort(sort) { + currentSort = sort; + updateSortButtons(); + renderTransactions(); + } + function getTransactionIcon(type) { switch (type) { case 'received': @@ -252,21 +278,22 @@ export function TransactionsListComponent(container) { function calculateTotals() { const totalReceived = allTransactions - .filter((tx) => tx.detail.amount.sats > 0) + .filter((tx) => getTransactionType(tx) === 'received') .reduce((sum, tx) => sum + tx.detail.amount.sats, 0); const totalSent = Math.abs( allTransactions - .filter((tx) => tx.detail.amount.sats < 0) + .filter((tx) => getTransactionType(tx) === 'sent') .reduce((sum, tx) => sum + tx.detail.amount.sats, 0) ); - - const netBalance = totalReceived - totalSent; + const totalSwaps = allTransactions.filter( + (tx) => getTransactionType(tx) === 'swap' + ).length; return { totalReceived: satsToBtc(totalReceived), totalSent: satsToBtc(totalSent), - netBalance: satsToBtc(netBalance), + totalSwaps, }; } @@ -292,20 +319,6 @@ export function TransactionsListComponent(container) { try { allTransactions = await fetchTransactions(100); // Load more transactions - // Debug: log transaction types to help identify swap detection - console.log('📊 Transaction breakdown:'); - allTransactions.forEach((tx, i) => { - const type = getTransactionType(tx); - const label = tx.detail.label || 'no label'; - const category = tx.detail.category; - if (i < 10) { - // Log first 10 for debugging - console.log( - ` ${i}: type=${type}, category=${category}, label=${label}` - ); - } - }); - updateStats(); renderTransactions(); console.log('✅ Transactions loaded:', allTransactions.length); @@ -346,16 +359,11 @@ export function TransactionsListComponent(container) { content.querySelector('#filter-swaps-count').textContent = stats.swaps; // Update stats cards - content.querySelector('#total-transactions').textContent = stats.all; content.querySelector('#total-received').textContent = totals.totalReceived + ' BTC'; content.querySelector('#total-sent').textContent = totals.totalSent + ' BTC'; - - const netEl = content.querySelector('#net-balance'); - const netValue = parseFloat(totals.netBalance); - netEl.textContent = (netValue >= 0 ? '+' : '') + totals.netBalance + ' BTC'; - netEl.className = `text-2xl font-mono ${netValue >= 0 ? 'text-green-400' : 'text-red-400'}`; + content.querySelector('#total-swaps').textContent = totals.totalSwaps; } // Create content @@ -369,7 +377,7 @@ export function TransactionsListComponent(container) { Back to Wallet

All Transactions

-

Complete transaction history (newest first)

+

Complete transaction history with filtering and sorting

-
-
-

Total Transactions

-

--

-
+

Total Received

-- BTC

@@ -391,8 +395,8 @@ export function TransactionsListComponent(container) {

-- BTC

-

Net Balance

-

-- BTC

+

Total Swaps

+

--

@@ -400,8 +404,7 @@ export function TransactionsListComponent(container) {

Transaction History

- -
+
@@ -414,6 +417,12 @@ export function TransactionsListComponent(container) { + +
@@ -429,11 +438,6 @@ export function TransactionsListComponent(container) {
- - -
-

💡 Tip: Check browser console for transaction type breakdown to debug swap detection

-
`; container.appendChild(content); @@ -494,6 +498,14 @@ export function TransactionsListComponent(container) { }); }); + const sortButtons = content.querySelectorAll('.sort-btn'); + sortButtons.forEach((button) => { + button.addEventListener('click', () => { + const sort = button.dataset.sort; + setSort(sort); + }); + }); + // Add back button handler const backButton = content.querySelector('#back-to-wallet'); backButton.addEventListener('click', () => { @@ -504,5 +516,6 @@ export function TransactionsListComponent(container) { }); // Initialize data + updateSortButtons(); loadTransactions(); } diff --git a/src/components/wallet/UtxoList.js b/src/components/wallet/UtxoList.js index a408e8b..cf0b8bc 100644 --- a/src/components/wallet/UtxoList.js +++ b/src/components/wallet/UtxoList.js @@ -3,7 +3,8 @@ export function UtxoListComponent(container) { let selectedUtxos = []; let allUtxos = []; let filteredUtxos = []; - let activeTypeFilter = 'all'; // 'all', 'p2wpkh', 'p2wsh', 'p2tr' + let activeTypeFilter = 'all'; // 'all', 'regular', 'contract', 'swap', 'spendable' + let currentSort = 'newest'; // 'newest', 'amount' // API Functions async function fetchUtxos() { @@ -34,7 +35,7 @@ export function UtxoListComponent(container) { return `${txid.substring(0, 12)}...${txid.substring(txid.length - 4)}`; } - function getUtxoTypeColor(spendType) { + function getUtxoTypeColor(spendType = '') { const type = spendType.toLowerCase(); if (type.includes('seed') || type.includes('regular')) return 'green'; if (type.includes('swap')) return 'blue'; @@ -44,6 +45,14 @@ export function UtxoListComponent(container) { return 'gray'; } + function getSpendTypeDisplay(spendType = '') { + const type = spendType.toLowerCase(); + if (type.includes('seed') || type.includes('regular')) return 'Regular'; + if (type.includes('swap')) return 'Swap'; + if (type.includes('contract')) return 'Contract'; + return spendType || 'Unknown'; + } + // Determine script type from UTXO data function getScriptType(utxoData) { const utxo = utxoData.utxo; @@ -130,15 +139,17 @@ export function UtxoListComponent(container) { ).length; const unconfirmed = totalUtxos - confirmed; - // Count by script type (from all UTXOs, not filtered) - const p2wpkhCount = allUtxos.filter( - (u) => getScriptType(u) === 'p2wpkh' + const regularCount = allUtxos.filter( + (u) => getSpendTypeDisplay(u.spendInfo?.spendType) === 'Regular' + ).length; + const contractCount = allUtxos.filter( + (u) => getSpendTypeDisplay(u.spendInfo?.spendType) === 'Contract' ).length; - const p2wshCount = allUtxos.filter( - (u) => getScriptType(u) === 'p2wsh' + const swapCount = allUtxos.filter( + (u) => getSpendTypeDisplay(u.spendInfo?.spendType) === 'Swap' ).length; - const p2trCount = allUtxos.filter( - (u) => getScriptType(u) === 'p2tr' + const spendableCount = allUtxos.filter((u) => + ['Regular', 'Swap'].includes(getSpendTypeDisplay(u.spendInfo?.spendType)) ).length; return { @@ -146,9 +157,10 @@ export function UtxoListComponent(container) { totalValue, confirmed, unconfirmed, - p2wpkhCount, - p2wshCount, - p2trCount, + regularCount, + contractCount, + swapCount, + spendableCount, }; } @@ -157,12 +169,27 @@ export function UtxoListComponent(container) { if (filterType === 'all') { filteredUtxos = [...allUtxos]; + } else if (filterType === 'spendable') { + filteredUtxos = allUtxos.filter((utxo) => + ['Regular', 'Swap'].includes( + getSpendTypeDisplay(utxo.spendInfo?.spendType) + ) + ); } else { filteredUtxos = allUtxos.filter( - (utxo) => getScriptType(utxo) === filterType + (utxo) => + getSpendTypeDisplay(utxo.spendInfo?.spendType).toLowerCase() === + filterType ); } + filteredUtxos.sort((a, b) => { + if (currentSort === 'amount') { + return b.utxo.amount - a.utxo.amount; + } + return (a.utxo.confirmations || 0) - (b.utxo.confirmations || 0); + }); + // Clear selections when filter changes selectedUtxos = []; @@ -173,7 +200,7 @@ export function UtxoListComponent(container) { } function updateFilterButtons() { - const filters = ['all', 'p2wpkh', 'p2wsh', 'p2tr']; + const filters = ['all', 'regular', 'contract', 'swap', 'spendable']; filters.forEach((filter) => { const btn = content.querySelector(`#filter-${filter}`); if (btn) { @@ -188,6 +215,28 @@ export function UtxoListComponent(container) { }); } + function updateSortButtons() { + const sorts = ['newest', 'amount']; + sorts.forEach((sort) => { + const btn = content.querySelector(`#sort-${sort}`); + if (btn) { + if (sort === currentSort) { + btn.className = + 'sort-btn bg-[#FF6B35] text-white px-4 py-2 rounded-lg text-sm font-semibold text-lg transition-colors'; + } else { + btn.className = + 'sort-btn bg-[#0f1419] hover:bg-[#242d3d] border border-gray-700 text-gray-400 px-4 py-2 rounded-lg text-sm font-semibold text-lg transition-colors'; + } + } + }); + } + + function setSort(sortType) { + currentSort = sortType; + applyFilter(activeTypeFilter); + updateSortButtons(); + } + function toggleUtxoSelection(index) { const utxoIndex = selectedUtxos.indexOf(index); if (utxoIndex > -1) { @@ -318,21 +367,26 @@ export function UtxoListComponent(container) { content.querySelector('#unconfirmed-count').textContent = stats.unconfirmed; // Update filter button counts - const p2wpkhBtn = content.querySelector('#filter-p2wpkh'); - const p2wshBtn = content.querySelector('#filter-p2wsh'); - const p2trBtn = content.querySelector('#filter-p2tr'); - - if (p2wpkhBtn) { - p2wpkhBtn.querySelector('.filter-count').textContent = - `(${stats.p2wpkhCount})`; + const regularBtn = content.querySelector('#filter-regular'); + const contractBtn = content.querySelector('#filter-contract'); + const swapBtn = content.querySelector('#filter-swap'); + const spendableBtn = content.querySelector('#filter-spendable'); + + if (regularBtn) { + regularBtn.querySelector('.filter-count').textContent = + `(${stats.regularCount})`; + } + if (contractBtn) { + contractBtn.querySelector('.filter-count').textContent = + `(${stats.contractCount})`; } - if (p2wshBtn) { - p2wshBtn.querySelector('.filter-count').textContent = - `(${stats.p2wshCount})`; + if (swapBtn) { + swapBtn.querySelector('.filter-count').textContent = + `(${stats.swapCount})`; } - if (p2trBtn) { - p2trBtn.querySelector('.filter-count').textContent = - `(${stats.p2trCount})`; + if (spendableBtn) { + spendableBtn.querySelector('.filter-count').textContent = + `(${stats.spendableCount})`; } } @@ -343,7 +397,7 @@ export function UtxoListComponent(container) { const message = activeTypeFilter === 'all' ? 'No UTXOs found' - : `No ${getScriptTypeDisplay(activeTypeFilter)} UTXOs found`; + : `No ${activeTypeFilter} UTXOs found`; tableBody.innerHTML = `${message}`; return; } @@ -357,6 +411,7 @@ export function UtxoListComponent(container) { const txid = typeof utxo.txid === 'object' ? utxo.txid.value : utxo.txid; const scriptType = getScriptType(utxoData); const scriptColor = getScriptTypeColor(scriptType); + const spendTypeDisplay = getSpendTypeDisplay(spendInfo.spendType); return ` @@ -372,7 +427,7 @@ export function UtxoListComponent(container) { ${getScriptTypeDisplay(scriptType)} - ${spendInfo.spendType} + ${spendTypeDisplay} ${utxo.address ? utxo.address.substring(0, 8) + '...' + utxo.address.substring(utxo.address.length - 4) : '--'} `; @@ -399,7 +454,7 @@ export function UtxoListComponent(container) { ← Back to Wallet

All UTXOs

-

Complete list of unspent transaction outputs

+

Complete list of unspent transaction outputs with filtering and sorting

- +
-
-
- Filter by Script Type: +
+
- - - +
-
- P2WPKH = SegWit Pubkey | - P2WSH = SegWit Script | - P2TR = Taproot +
+ +
@@ -481,7 +541,7 @@ export function UtxoListComponent(container) { Amount Confirmations Script Type - Spend Type + Type Address @@ -520,14 +580,23 @@ export function UtxoListComponent(container) { .querySelector('#filter-all') .addEventListener('click', () => applyFilter('all')); content - .querySelector('#filter-p2wpkh') - .addEventListener('click', () => applyFilter('p2wpkh')); + .querySelector('#filter-regular') + .addEventListener('click', () => applyFilter('regular')); + content + .querySelector('#filter-contract') + .addEventListener('click', () => applyFilter('contract')); + content + .querySelector('#filter-swap') + .addEventListener('click', () => applyFilter('swap')); + content + .querySelector('#filter-spendable') + .addEventListener('click', () => applyFilter('spendable')); content - .querySelector('#filter-p2wsh') - .addEventListener('click', () => applyFilter('p2wsh')); + .querySelector('#sort-newest') + .addEventListener('click', () => setSort('newest')); content - .querySelector('#filter-p2tr') - .addEventListener('click', () => applyFilter('p2tr')); + .querySelector('#sort-amount') + .addEventListener('click', () => setSort('amount')); // Add select all handler const selectAllCheckbox = content.querySelector('#select-all-utxos'); @@ -557,5 +626,6 @@ export function UtxoListComponent(container) { }); // Initialize data + updateSortButtons(); loadUtxos(); } diff --git a/src/components/wallet/Wallet.js b/src/components/wallet/Wallet.js index d5c24de..ad7fa31 100644 --- a/src/components/wallet/Wallet.js +++ b/src/components/wallet/Wallet.js @@ -397,7 +397,7 @@ export async function WalletComponent(container) {

${walletInfo.walletPath}

diff --git a/src/styles/output.css b/src/styles/output.css index 9e7d8cb..9437533 100644 --- a/src/styles/output.css +++ b/src/styles/output.css @@ -14,12 +14,14 @@ --color-orange-200: oklch(90.1% 0.076 70.697); --color-orange-400: oklch(75% 0.183 55.934); --color-orange-500: oklch(70.5% 0.213 47.604); + --color-yellow-300: oklch(90.5% 0.182 98.111); --color-yellow-400: oklch(85.2% 0.199 91.936); --color-yellow-500: oklch(79.5% 0.184 86.047); --color-green-400: oklch(79.2% 0.209 151.711); --color-green-500: oklch(72.3% 0.219 149.579); --color-green-600: oklch(62.7% 0.194 149.214); --color-green-700: oklch(52.7% 0.154 150.069); + --color-cyan-300: oklch(86.5% 0.127 207.078); --color-cyan-400: oklch(78.9% 0.154 211.53); --color-cyan-500: oklch(71.5% 0.143 215.221); --color-blue-300: oklch(80.9% 0.105 251.813); @@ -44,6 +46,7 @@ --container-2xl: 42rem; --container-3xl: 48rem; --container-5xl: 64rem; + --container-6xl: 72rem; --container-7xl: 80rem; --text-xs: 0.75rem; --text-xs--line-height: calc(1 / 0.75); @@ -66,6 +69,7 @@ --font-weight-semibold: 600; --font-weight-bold: 700; --font-weight-black: 900; + --tracking-wide: 0.025em; --tracking-wider: 0.05em; --leading-relaxed: 1.625; --radius-lg: 0.5rem; @@ -293,8 +297,8 @@ .col-span-3 { grid-column: span 3 / span 3; } - .col-span-9 { - grid-column: span 9 / span 9; + .col-span-8 { + grid-column: span 8 / span 8; } .container { width: 100%; @@ -392,9 +396,15 @@ .hidden { display: none; } + .inline { + display: inline; + } .inline-block { display: inline-block; } + .inline-flex { + display: inline-flex; + } .table { display: table; } @@ -413,6 +423,9 @@ .h-6 { height: calc(var(--spacing) * 6); } + .h-7 { + height: calc(var(--spacing) * 7); + } .h-8 { height: calc(var(--spacing) * 8); } @@ -479,6 +492,9 @@ .w-6 { width: calc(var(--spacing) * 6); } + .w-7 { + width: calc(var(--spacing) * 7); + } .w-8 { width: calc(var(--spacing) * 8); } @@ -509,12 +525,12 @@ .max-w-2xl { max-width: var(--container-2xl); } - .max-w-3xl { - max-width: var(--container-3xl); - } .max-w-5xl { max-width: var(--container-5xl); } + .max-w-6xl { + max-width: var(--container-6xl); + } .max-w-7xl { max-width: var(--container-7xl); } @@ -536,6 +552,9 @@ .flex-shrink-0 { flex-shrink: 0; } + .shrink-0 { + flex-shrink: 0; + } .-translate-x-1\/2 { --tw-translate-x: calc(calc(1/2 * 100%) * -1); translate: var(--tw-translate-x) var(--tw-translate-y); @@ -577,8 +596,14 @@ .grid-cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); } - .grid-cols-9 { - grid-template-columns: repeat(9, minmax(0, 1fr)); + .grid-cols-6 { + grid-template-columns: repeat(6, minmax(0, 1fr)); + } + .grid-cols-8 { + grid-template-columns: repeat(8, minmax(0, 1fr)); + } + .grid-cols-\[1\.7fr_0\.9fr_1fr_1fr_1fr_1\.25fr\] { + grid-template-columns: 1.7fr 0.9fr 1fr 1fr 1fr 1.25fr; } .flex-col { flex-direction: column; @@ -706,6 +731,14 @@ border-color: var(--color-gray-700); } } + .divide-gray-800 { + :where(& > :not(:last-child)) { + border-color: var(--color-gray-800); + } + } + .self-center { + align-self: center; + } .truncate { overflow: hidden; text-overflow: ellipsis; @@ -880,6 +913,12 @@ .bg-\[\#242d3d\] { background-color: #242d3d; } + .bg-\[\#111827\] { + background-color: #111827; + } + .bg-\[\#111827\]\/40 { + background-color: color-mix(in oklab, #111827 40%, transparent); + } .bg-\[\#FF6B35\] { background-color: #FF6B35; } @@ -1113,6 +1152,9 @@ .px-4 { padding-inline: calc(var(--spacing) * 4); } + .px-5 { + padding-inline: calc(var(--spacing) * 5); + } .px-6 { padding-inline: calc(var(--spacing) * 6); } @@ -1128,6 +1170,9 @@ .py-2 { padding-block: calc(var(--spacing) * 2); } + .py-2\.5 { + padding-block: calc(var(--spacing) * 2.5); + } .py-3 { padding-block: calc(var(--spacing) * 3); } @@ -1215,6 +1260,10 @@ --tw-leading: calc(var(--spacing) * 6); line-height: calc(var(--spacing) * 6); } + .leading-none { + --tw-leading: 1; + line-height: 1; + } .leading-relaxed { --tw-leading: var(--leading-relaxed); line-height: var(--leading-relaxed); @@ -1239,6 +1288,10 @@ --tw-font-weight: var(--font-weight-semibold); font-weight: var(--font-weight-semibold); } + .tracking-wide { + --tw-tracking: var(--tracking-wide); + letter-spacing: var(--tracking-wide); + } .tracking-wider { --tw-tracking: var(--tracking-wider); letter-spacing: var(--tracking-wider); @@ -1333,6 +1386,9 @@ color: color-mix(in oklab, var(--color-white) 90%, transparent); } } + .text-yellow-300 { + color: var(--color-yellow-300); + } .text-yellow-400 { color: var(--color-yellow-400); } @@ -1624,6 +1680,13 @@ } } } + .hover\:text-cyan-300 { + &:hover { + @media (hover: hover) { + color: var(--color-cyan-300); + } + } + } .hover\:text-purple-300 { &:hover { @media (hover: hover) { @@ -1706,6 +1769,16 @@ opacity: 50%; } } + .sm\:grid-cols-3 { + @media (width >= 40rem) { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } + } + .md\:flex { + @media (width >= 48rem) { + display: flex; + } + } .md\:grid-cols-2 { @media (width >= 48rem) { grid-template-columns: repeat(2, minmax(0, 1fr)); @@ -1721,6 +1794,42 @@ grid-template-columns: repeat(4, minmax(0, 1fr)); } } + .md\:flex-row { + @media (width >= 48rem) { + flex-direction: row; + } + } + .md\:items-start { + @media (width >= 48rem) { + align-items: flex-start; + } + } + .md\:justify-between { + @media (width >= 48rem) { + justify-content: space-between; + } + } + .lg\:grid-cols-5 { + @media (width >= 64rem) { + grid-template-columns: repeat(5, minmax(0, 1fr)); + } + } + .xl\:grid-cols-4 { + @media (width >= 80rem) { + grid-template-columns: repeat(4, minmax(0, 1fr)); + } + } + .xl\:grid-cols-\[1\.5fr_1fr\] { + @media (width >= 80rem) { + grid-template-columns: 1.5fr 1fr; + } + } + .xl\:text-2xl { + @media (width >= 80rem) { + font-size: var(--text-2xl); + line-height: var(--tw-leading, var(--text-2xl--line-height)); + } + } } @font-face { font-family: 'JetBrains Mono';