paytaca-cli 0.8.0 → 0.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,7 +4,7 @@ A command-line interface for the Paytaca [Bitcoin Cash](https://bitcoincash.org)
4
4
 
5
5
  Bitcoin Cash is peer-to-peer electronic cash, enabling fast, low-fee transactions for everyday use. Paytaca CLI brings the full capabilities of the Paytaca wallet to the terminal — create wallets, derive addresses, send and receive BCH, manage CashTokens (fungible tokens and NFTs), swap tokens on the Cauldron DEX, pay x402 HTTP APIs, chat over Nostr, and view transaction history, all from the command line.
6
6
 
7
- Designed to be AI agent-friendly and useful for automation by power users. It ships an MCP server and a one-step installer for the Paytaca AI provider.
7
+ Designed to be AI agent-friendly and useful for automation by power users. It ships an MCP server, a one-step installer for the Paytaca AI provider, and a local web UI (`paytaca web`).
8
8
 
9
9
  ## Requirements
10
10
 
@@ -36,11 +36,15 @@ Most commands accept `--json` for machine-readable output and `--chipnet` to tar
36
36
  ```bash
37
37
  paytaca wallet create # Generate a new 12-word seed phrase
38
38
  paytaca wallet create --chipnet # Create on chipnet (testnet)
39
- paytaca wallet import # Import an existing seed phrase
39
+ paytaca wallet import # Import a seed phrase (12/15/18/21/24 words)
40
40
  paytaca wallet info # Show wallet hash, address, and balance
41
41
  paytaca wallet export # Display the stored seed phrase (interactive terminal + biometrics)
42
42
  ```
43
43
 
44
+ > `wallet import` accepts 12/15/18/21/24-word phrases separated by spaces or
45
+ > commas. Unknown words are rejected with the closest BIP39 suggestion, and the
46
+ > checksum is validated before the wallet is stored.
47
+
44
48
  > `wallet export` requires an interactive terminal and prompts for biometric
45
49
  > authentication (Touch ID, fingerprint, or Windows Hello) when available. It
46
50
  > cannot be used from scripts, pipes, or other non-interactive processes.
@@ -185,7 +189,7 @@ paytaca ai auto-refill --status # Inspect; also --disable, --delete
185
189
 
186
190
  `ai configure` accepts `--backend`, `--path`, `--api-key`, `--chipnet`, and `-y/--yes`. `ai purchase` and `ai auto-refill` accept `--lift` to pay with LIFT tokens at a discount. Add `--json` to any `ai` subcommand for machine-readable output.
187
191
 
188
- Armed auto-refill state lives at `~/.paytaca/auto-refill.json`. It runs inline — arming performs one immediate check (buying right away if the armed model has no credits), the `get_credits` MCP tool refills on demand, and the `paytaca mcp` server keeps a background backstop while a session is running. Each execution is appended to `~/.paytaca/auto-refill-events.jsonl` and the latest is reported as `lastEvent`. It disarms when the budget is spent, funds are short, or the plan is no longer offered. Use `--disable` to pause and `--delete` to remove the configuration (history is kept).
192
+ Armed auto-refill state lives at `~/.paytaca/auto-refill.json`. Arming performs one immediate check (buying right away if the armed model has no credits), and the `paytaca mcp` server keeps a background backstop (every 60s) while a session is running. The `get_credits` MCP tool is read-only and never triggers a purchase. Each execution is appended to `~/.paytaca/auto-refill-events.jsonl` and the latest is reported as `lastEvent`. It disarms when the budget is spent, funds are short, or the plan is no longer offered. Use `--disable` to pause and `--delete` to remove the configuration (history is kept).
189
193
 
190
194
  ### Image Generation
191
195
 
@@ -232,7 +236,7 @@ paytaca ai configure opencode --api-key sk-pytc-… # read-only wallet
232
236
 
233
237
  Read-only wallets report credits from the shared wallet hash but can't buy plans — fund credits from the full wallet.
234
238
 
235
- **MCP tools:** wallet reads (`get_balance`, `get_transactions`, `get_receiving_address`, `get_tokens`, `send`) plus Paytaca AI (`get_models`, `get_plans`, `get_credits`, `buy_plan`, `auto_refill`, `get_help`, `generate_image`, `get_image_status`, `get_image_models`, `get_image_history`).
239
+ **MCP tools:** wallet (`get_balance`, `get_transactions`, `get_receiving_address`, `get_tokens`, `send`) plus Paytaca AI (`get_models`, `get_plans`, `get_credits`, `buy_plan`, `auto_refill`, `get_help`, `generate_image`, `get_image_status`, `get_image_models`, `get_image_history`).
236
240
 
237
241
  Spending tools (`send`, `buy_plan`, `auto_refill`, `generate_image`) require host-level approval. By default MCP operates on your **main wallet** and can spend real funds.
238
242
 
@@ -271,6 +275,27 @@ paytaca chat remove-display-name # Remove published name
271
275
  paytaca chat remove-bch-address # Remove published address
272
276
  ```
273
277
 
278
+ ### Web UI
279
+
280
+ Launch a local browser UI for the wallet, Cauldron swaps, and Paytaca AI:
281
+
282
+ ```bash
283
+ paytaca web # Serve on http://127.0.0.1:7474 and open the browser
284
+ paytaca web --port 8080 # Use a custom port
285
+ paytaca web --chipnet # Mainnet by default; --chipnet targets testnet
286
+ paytaca web --backend <url> # Override the Paytaca AI backend URL
287
+ paytaca web --no-open # Do not auto-open the browser
288
+ ```
289
+
290
+ The server binds to `127.0.0.1` only. Access is gated by a random per-session token sent in the `X-Paytaca-Token` header; the browser is auto-opened with a session link that carries the token, and the page strips it from the address bar after loading. With `--no-open`, use the printed URL alongside the session link to open it manually.
291
+
292
+ The UI is organized into two tabs:
293
+
294
+ - **Wallet** — send and receive BCH, send CashTokens, swap on the Cauldron DEX, and review transaction history (date, type, counterparty, amount, USD value, fee, and explorer links), with token-category selection in the send and swap forms.
295
+ - **AI** — buy plans (BCH or a live LIFT quote), manage auto-refill (enable / pause / delete, with budget-cap validation), and browse generated images (gallery with lightbox, order history, and delete).
296
+
297
+ Incoming transactions are streamed live over a Watchtower WebSocket and surface as in-page notifications; a light/dark theme toggle is persisted in the browser.
298
+
274
299
  ## Network
275
300
 
276
301
  All commands default to **mainnet**. Pass `--chipnet` for testnet:
@@ -293,6 +318,8 @@ Powered by [@napi-rs/keyring](https://github.com/Brooooooklyn/keyring-node) (pre
293
318
 
294
319
  Nostr chat keys are **not** persisted — they are re-derived from the wallet mnemonic at runtime via HD path `m/44'/1237'/0'/0/0` and held only in memory for the duration of a session. Non-secret chat metadata (contacts, rooms, messages) is written to `~/.paytaca/chat-state.json` with mode `0600`; no key material is ever written there.
295
320
 
321
+ The `paytaca web` server listens on `127.0.0.1` only and requires a random per-session token in the `X-Paytaca-Token` header; requests whose `Host` header is not `127.0.0.1`/`localhost` are rejected, request bodies are capped at 1 MB, and internal error details are logged to stderr rather than returned to the client.
322
+
296
323
  ## Architecture
297
324
 
298
325
  ```
@@ -310,6 +337,7 @@ src/
310
337
  check.ts Check if URL requires x402 payment
311
338
  ai.ts Paytaca AI (configure, api-key, models, plans, credits, purchase, auto-refill, image)
312
339
  chat.ts Nostr chat (contacts, conversations, identity, listen)
340
+ web.ts `paytaca web` — local browser UI launcher
313
341
  mcp.ts `paytaca mcp` stdio entry + per-harness config templates
314
342
  update.ts Self-update from npm registry
315
343
  core/
@@ -318,6 +346,10 @@ src/
318
346
  mcp/
319
347
  server.ts MCP server construction and stdio lifecycle
320
348
  tools.ts MCP tool registration (wallet reads, send, Paytaca AI)
349
+ web/
350
+ server.ts Local HTTP server (token auth, JSON API routes)
351
+ page.ts Self-contained Alpine.js UI (wallet, swap, AI tabs)
352
+ assets.ts Inline asset loading (vendored Alpine.js, styles)
321
353
  wallet/
322
354
  index.ts Wallet class, mnemonic gen/import/load
323
355
  bch.ts BchWallet (balance, send, history, CashTokens)
@@ -364,6 +396,7 @@ src/
364
396
  | `@cashlab/cauldron` | Cauldron DEX swap building |
365
397
  | `@napi-rs/keyring` | OS-native keychain storage |
366
398
  | `@modelcontextprotocol/sdk` | MCP server (stdio) |
399
+ | `alpinejs` | Reactive web UI (`paytaca web`), vendored inline |
367
400
  | `bip39` | Mnemonic generation and validation |
368
401
  | `nostr-tools` | Nostr chat events, keys, and relay transport |
369
402
  | `js-sha256` | Hashing for key derivation and payment headers |
@@ -1 +1 @@
1
- {"version":3,"file":"page.d.ts","sourceRoot":"","sources":["../../src/web/page.ts"],"names":[],"mappings":"AAWA,wBAAgB,UAAU,IAAI,MAAM,CAqnBnC"}
1
+ {"version":3,"file":"page.d.ts","sourceRoot":"","sources":["../../src/web/page.ts"],"names":[],"mappings":"AAWA,wBAAgB,UAAU,IAAI,MAAM,CAwpBnC"}
package/dist/web/page.js CHANGED
@@ -18,6 +18,7 @@ export function renderPage() {
18
18
  <style>
19
19
  *,*::before,*::after{margin:0;padding:0;box-sizing:border-box}
20
20
  :root{
21
+ color-scheme:light;
21
22
  --sans:'Rubik',system-ui,-apple-system,'Segoe UI',sans-serif;
22
23
  --mono:'JetBrains Mono',ui-monospace,'SF Mono','Cascadia Code',monospace;
23
24
  --bg:#ecf3f3;
@@ -45,6 +46,7 @@ export function renderPage() {
45
46
  --on-ink:#fff;
46
47
  }
47
48
  [data-theme="dark"]{
49
+ color-scheme:dark;
48
50
  --bg:#273746;
49
51
  --surface:rgba(28,40,51,0.7);
50
52
  --surface-2:#1c2833;
@@ -102,9 +104,10 @@ body{font-family:var(--sans);background:var(--bg);color:var(--ink);font-size:14p
102
104
  .tx-toast a{font-family:var(--mono);font-size:10.5px;color:var(--accent);text-decoration:underline;text-underline-offset:2px}
103
105
  .tx-toast a:hover{color:var(--accent-strong)}
104
106
  .content{width:100%;max-width:1400px;margin:0 auto;padding:26px 30px 56px}
105
- .grid{display:grid;grid-template-columns:repeat(12,1fr);gap:16px}
107
+ .grid{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));gap:16px}
106
108
  .span-4{grid-column:span 4}.span-5{grid-column:span 5}.span-6{grid-column:span 6}.span-7{grid-column:span 7}.span-8{grid-column:span 8}.span-12{grid-column:span 12}
107
- .grid>div{display:flex;flex-direction:column}.grid>div>.card{flex:1}
109
+ .grid>div{display:flex;flex-direction:column;min-width:0}.grid>div>.card{flex:1;min-width:0}
110
+ .tbl-scroll{overflow-x:auto}
108
111
  .card{background:var(--surface);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);border:1px solid var(--line);border-radius:var(--r);padding:20px;box-shadow:var(--shadow),var(--glass-hi)}
109
112
  .card-head{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:18px}
110
113
  .card-title{font-family:var(--mono);font-size:10.5px;font-weight:600;letter-spacing:0.13em;text-transform:uppercase;color:var(--ink-2)}
@@ -172,7 +175,8 @@ input[type="number"]{-moz-appearance:textfield}input::-webkit-outer-spin-button,
172
175
  .tbl tbody tr:hover{background:var(--surface-2)}
173
176
  .tbl tr:last-child td{border-bottom:none}
174
177
  .tbl-num{font-family:var(--mono);font-variant-numeric:tabular-nums;font-size:12px}
175
- .hist-date a{color:var(--ink-2);text-decoration:none;border-bottom:1px dotted var(--line-2);font-family:var(--mono);font-size:11px}
178
+ .hist-date-text{font-family:var(--mono);font-size:11px;color:var(--ink-2)}
179
+ .hist-date a{color:var(--ink-3);text-decoration:none;border-bottom:1px dotted var(--line-2);font-family:var(--mono);font-size:10px}
176
180
  .hist-date a:hover{color:var(--accent-strong);border-bottom-color:var(--accent)}.hist-addr{max-width:210px}
177
181
  .hist-sub{font-family:var(--mono);font-size:10px;color:var(--ink-3);margin-top:2px}
178
182
  .hist-amt{font-family:var(--mono);font-size:12px;font-variant-numeric:tabular-nums}
@@ -187,6 +191,11 @@ input[type="number"]{-moz-appearance:textfield}input::-webkit-outer-spin-button,
187
191
  .pill-wait{background:rgba(255,193,7,0.12);color:#d4a017}
188
192
  [data-theme="dark"] .pill-ok{background:rgba(40,167,69,0.18);color:#4ade80}
189
193
  [data-theme="dark"] .pill-wait{background:rgba(255,193,7,0.18);color:#fbbf24}
194
+ [data-theme="dark"] .field-input{background:rgba(255,255,255,0.06);border-color:rgba(255,255,255,0.2)}
195
+ [data-theme="dark"] .field-input:focus{background:rgba(255,255,255,0.09)}
196
+ [data-theme="dark"] .segmented,[data-theme="dark"] .seg{border-color:rgba(255,255,255,0.2)}
197
+ [data-theme="dark"] .segmented-opt.active{background:rgba(255,255,255,0.12)}
198
+ [data-theme="dark"] .seg button{background:rgba(255,255,255,0.08)}
190
199
  .pagination{display:flex;align-items:center;justify-content:center;gap:14px;margin-top:18px}
191
200
  .pagination .page-info{font-family:var(--mono);font-size:11px;color:var(--ink-3)}
192
201
  .empty{padding:30px 12px;text-align:center;font-family:var(--mono);font-size:11.5px;letter-spacing:0.02em;color:var(--ink-3)}
@@ -200,11 +209,23 @@ input[type="number"]{-moz-appearance:textfield}input::-webkit-outer-spin-button,
200
209
  .plan-group:first-child{padding-top:2px}
201
210
  .plan-group:last-child{border-bottom:0;padding-bottom:2px}
202
211
  .plan-model{font-size:13.5px;font-weight:600;margin-bottom:12px}
203
- .plan-tiers{display:flex;gap:10px;flex-wrap:wrap}
204
- .plan-tier{display:flex;align-items:center;gap:14px;border:1px solid var(--line);border-radius:var(--r-sm);padding:9px 14px;background:var(--surface-3)}
205
- .plan-tier:hover{border-color:var(--accent);transform:translateY(-1px);transition:all .2s ease}
212
+ .plan-slider{margin:2px 0 0}
213
+ .plan-slider input[type="range"]{-webkit-appearance:none;appearance:none;display:block;width:100%;height:4px;border-radius:2px;background:var(--line);outline:none;cursor:pointer;margin:0}
214
+ .plan-slider input[type="range"]::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;width:16px;height:16px;border-radius:50%;background:var(--accent);border:2px solid var(--surface);box-shadow:0 1px 4px rgba(0,0,0,0.25);cursor:pointer}
215
+ .plan-slider input[type="range"]::-moz-range-thumb{width:12px;height:12px;border-radius:50%;background:var(--accent);border:2px solid var(--surface);box-shadow:0 1px 4px rgba(0,0,0,0.25);cursor:pointer}
216
+ .plan-slider input[type="range"]::-moz-range-track{background:transparent}
217
+ .plan-ticks{display:flex;justify-content:space-between;padding:7px 8px 0}
218
+ .plan-ticks span{width:0;display:flex;flex-direction:column;align-items:center;white-space:nowrap;font-family:var(--mono);font-size:9.5px;letter-spacing:0.04em;color:var(--ink-3)}
219
+ .plan-ticks span::before{content:'';width:1px;height:4px;background:var(--line);margin-bottom:3px}
220
+ .plan-selection{display:flex;align-items:center;gap:12px;margin-top:12px;min-height:29px}
221
+ .plan-selection .grow{flex:1}
222
+ .plan-hint{font-family:var(--mono);font-size:10.5px;letter-spacing:0.04em;color:var(--ink-3)}
223
+ .plan-insufficient{font-family:var(--mono);font-size:10px;letter-spacing:0.04em;color:var(--err)}
206
224
  .plan-duration{font-family:var(--mono);font-size:10.5px;font-weight:600;letter-spacing:0.08em;text-transform:uppercase;color:var(--ink-2)}
207
225
  .plan-price{font-size:13px;font-weight:600}
226
+ .refill-model{margin-bottom:14px}
227
+ .refill-pay{display:flex;align-items:center;gap:12px;margin-top:14px}
228
+ .refill-pay .field-label{margin:0}
208
229
  .seg{display:inline-flex;border:1px solid var(--line);border-radius:var(--r-sm);overflow:hidden}
209
230
  .seg button{appearance:none;background:var(--surface-3);border:0;padding:8px 20px;font-family:var(--mono);font-size:11px;font-weight:600;letter-spacing:0.08em;color:var(--ink-3);cursor:pointer}
210
231
  .seg button+.seg button{border-left:1px solid var(--line)}
@@ -319,7 +340,7 @@ input[type="number"]{-moz-appearance:textfield}input::-webkit-outer-spin-button,
319
340
  <button class="action-btn" :class="{active: walletAction==='receive'}" @click="walletAction = walletAction==='receive' ? null : 'receive'"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 7 7 17"/><path d="M16 17H7V8"/></svg>Receive</button>
320
341
  <button class="action-btn" :class="{active: walletAction==='swap'}" @click="walletAction = walletAction==='swap' ? null : 'swap'"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m16 3 4 4-4 4"/><path d="M20 7H4"/><path d="m8 21-4-4 4-4"/><path d="M4 17h16"/></svg>Swap</button>
321
342
  </div>
322
- <div class="grid" x-show="walletAction">
343
+ <div class="grid" x-show="walletAction" style="margin-top:16px">
323
344
  <div class="span-12">
324
345
  <div class="card">
325
346
  <div class="card-head"><div class="card-title" x-text="walletAction==='send' ? 'Send' : walletAction==='receive' ? 'Receive' : 'Cauldron Swap'"></div><button class="btn-close" @click="walletAction=null" title="Close">✕</button></div>
@@ -343,14 +364,10 @@ input[type="number"]{-moz-appearance:textfield}input::-webkit-outer-spin-button,
343
364
  <div class="segmented-opt" :class="{active: sendType==='bch'}" @click="sendType='bch'; sendError=''; sendSuccess=''">BCH</div>
344
365
  <div class="segmented-opt" :class="{active: sendType==='token'}" @click="sendType='token'; sendError=''; sendSuccess=''">CashToken</div>
345
366
  </div>
346
- <div x-show="sendType==='token'">
347
- <div class="field"><label class="field-label">Token Category ID</label><select class="field-input" x-model="sendTokenPreset" @change="if(sendTokenPreset==='__custom__')sendCategory='';else sendCategory=sendTokenPreset"><option value="__custom__">Enter token category</option><option value="5932b2fd4915d6a75d3ec53282cd49118149a2176ee67ed68b1111ff0786f7fc">LIFT</option><option value="2469acc5afa4b10cb5b5c04afb89c3a3ffd61c5da9c01e26d00951cae2a02544">PUSD</option></select></div>
348
- <div class="field" x-show="sendTokenPreset === '__custom__'"><input class="field-input" type="text" x-model="sendCategory" placeholder="Paste 64-char hex category ID"></div>
349
- <div class="field"><label class="field-label">Token Amount (base units)</label><input class="field-input" type="text" x-model="sendTokenAmount" placeholder="e.g. 1000"></div>
350
- </div>
351
- <div x-show="sendType==='bch'">
352
- <div class="field"><label class="field-label">Amount</label><div class="inline-row"><input class="field-input" type="number" x-model="sendAmount" placeholder="0.0" step="any" min="0"><select class="field-input shrink" x-model="sendCurrency" style="width:104px"><option value="bch">BCH</option><option value="sats">sats</option><option value="usd">USD</option></select></div></div>
353
- </div>
367
+ <div class="field" x-show="sendType==='token'"><label class="field-label">Token Category ID</label><select class="field-input" x-model="sendTokenPreset" @change="if(sendTokenPreset==='__custom__')sendCategory='';else sendCategory=sendTokenPreset"><option value="__custom__">Enter token category</option><option value="5932b2fd4915d6a75d3ec53282cd49118149a2176ee67ed68b1111ff0786f7fc">LIFT</option><option value="2469acc5afa4b10cb5b5c04afb89c3a3ffd61c5da9c01e26d00951cae2a02544">PUSD</option></select></div>
368
+ <div class="field" x-show="sendType==='token' && sendTokenPreset === '__custom__'"><input class="field-input" type="text" x-model="sendCategory" placeholder="Paste 64-char hex category ID"></div>
369
+ <div class="field" x-show="sendType==='token'"><label class="field-label">Token Amount (base units)</label><input class="field-input" type="text" x-model="sendTokenAmount" placeholder="e.g. 1000"></div>
370
+ <div class="field" x-show="sendType==='bch'"><div class="field-label" style="display:flex;justify-content:space-between;align-items:baseline">Amount<a href="#" @click.prevent="fillMaxSend()" style="font-family:var(--mono);font-size:9.5px;font-weight:500;letter-spacing:0.12em;text-transform:uppercase;color:var(--accent);text-decoration:underline;text-underline-offset:2px;cursor:pointer">Max</a></div><div class="inline-row"><input class="field-input" type="number" x-model="sendAmount" placeholder="0.0" step="any" min="0"><select class="field-input shrink" x-model="sendCurrency" style="width:104px"><option value="bch">BCH</option><option value="sats">sats</option><option value="usd">USD</option></select></div></div>
354
371
  <div class="field"><label class="field-label">Recipient Address</label><input class="field-input" type="text" x-model="sendAddress" placeholder="bitcoincash:q..."></div>
355
372
  <template x-if="sendType==='token' && sendAddress && !isTokenAddr(sendAddress)"><div class="warning-bar">Address is not token-aware (z-prefix). Tokens may be lost.</div></template>
356
373
  <div x-show="sendError" class="field-error" x-text="sendError"></div>
@@ -362,10 +379,10 @@ input[type="number"]{-moz-appearance:textfield}input::-webkit-outer-spin-button,
362
379
  <div class="field"><label class="field-label">Token</label><select class="field-input" x-model="swapToken"><option value="__custom__">Enter token category</option><template x-for="t in swapTokens" :key="t.category"><option :value="t.category" x-text="t.symbol + ' · ' + t.category.slice(0,10) + '…' + t.category.slice(-6)"></option></template></select></div>
363
380
  <div class="field" x-show="swapToken === '__custom__'"><input class="field-input" type="text" x-model="swapCustomCategory" placeholder="Paste 64-char hex category ID"></div>
364
381
  <div class="segmented" style="margin-bottom:14px">
365
- <div class="segmented-opt" :class="{active: swapDir==='sell'}" @click="swapDir='sell'; swapQuoteText=''; pendingSwap=null">Sell Token → BCH</div>
366
382
  <div class="segmented-opt" :class="{active: swapDir==='buy'}" @click="swapDir='buy'; swapQuoteText=''; pendingSwap=null">Buy Token ← BCH</div>
383
+ <div class="segmented-opt" :class="{active: swapDir==='sell'}" @click="swapDir='sell'; swapQuoteText=''; pendingSwap=null">Sell Token → BCH</div>
367
384
  </div>
368
- <div class="field"><label class="field-label" x-text="swapDir==='sell' ? 'Token Amount' : 'BCH Amount'"></label><input class="field-input" type="number" x-model="swapAmount" placeholder="0.0" step="any" min="0"></div>
385
+ <div class="field"><label class="field-label">Token Amount (human units)</label><input class="field-input" type="number" x-model="swapAmount" placeholder="0.0" step="any" min="0"></div>
369
386
  <div x-show="swapError" class="field-error" x-text="swapError"></div>
370
387
  <button class="btn btn-primary" style="margin-top:14px;min-width:180px" @click="doSwapQuote()" :disabled="swapBusy" x-text="swapBusy ? 'Fetching quote…' : 'Get Quote'"></button>
371
388
  <template x-if="swapQuoteText"><div style="margin-top:16px;padding-top:16px;border-top:1px solid var(--line-2)">
@@ -389,8 +406,8 @@ input[type="number"]{-moz-appearance:textfield}input::-webkit-outer-spin-button,
389
406
  <div class="card-head"><div class="card-title">History</div><select class="field-input" x-model="histType" @change="loadHistory(1)" style="width:auto;padding:5px 9px;font-size:11.5px"><option value="all">All</option><option value="incoming">Incoming</option><option value="outgoing">Outgoing</option></select></div>
390
407
  <template x-if="!histRecords.length && histLoading"><div class="empty">Loading…</div></template>
391
408
  <template x-if="!histRecords.length && !histLoading"><div class="empty">No transactions yet</div></template>
392
- <template x-if="histRecords.length"><div><table class="tbl"><thead><tr><th>Date</th><th>Type</th><th>Counterparty</th><th style="text-align:right">Amount</th></tr></thead><tbody>
393
- <template x-for="(r, ri) in histRecords" :key="ri"><tr><td class="hist-date" style="white-space:nowrap"><a :href="r.explorer" target="_blank" rel="noopener" :title="'View '+r.txid+' on explorer'" x-text="r.dateText"></a></td><td><span class="pill" :class="r.type==='incoming'?'pill-in':'pill-out'" x-text="r.type==='incoming'?'IN':'OUT'"></span></td><td class="hist-addr"><span class="mono-break" :title="r.counterparty" x-text="r.partyShort"></span><div class="hist-sub" x-show="r.type==='outgoing' && r.fee" x-text="'fee '+r.fee+' BCH'"></div></td><td class="tbl-num" style="text-align:right"><div class="hist-amt" :class="r.type==='incoming'?'hist-in':'hist-out'" x-text="(r.type==='incoming'?'+':'-')+r.amountText+' BCH'"></div><div class="hist-sub" x-show="r.usd!=null" x-text="'≈ $'+r.usd+' USD'"></div></td></tr></template>
409
+ <template x-if="histRecords.length"><div class="tbl-scroll"><table class="tbl"><thead><tr><th>Date</th><th>Type</th><th>Counterparty</th><th style="text-align:right">Amount</th></tr></thead><tbody>
410
+ <template x-for="(r, ri) in histRecords" :key="ri"><tr><td class="hist-date" style="white-space:nowrap"><div class="hist-date-text" x-text="r.dateText"></div><a x-show="r.txidShort" :href="r.explorer" target="_blank" rel="noopener" :title="'View '+r.txid+' on explorer'" x-text="r.txidShort"></a></td><td><span class="pill" :class="r.type==='incoming'?'pill-in':'pill-out'" x-text="r.type==='incoming'?'IN':'OUT'"></span></td><td class="hist-addr"><span class="mono-break" :title="r.counterparty" x-text="r.partyShort"></span><div class="hist-sub" x-show="r.type==='outgoing' && r.fee" x-text="'fee '+r.fee+' BCH'"></div></td><td class="tbl-num" style="text-align:right"><div class="hist-amt" :class="r.type==='incoming'?'hist-in':'hist-out'" x-text="(r.type==='incoming'?'+':'-')+r.amountText+' BCH'"></div><div class="hist-sub" x-show="r.usd!=null" x-text="'≈ $'+r.usd+' USD'"></div></td></tr></template>
394
411
  </tbody></table><div class="pagination"><button class="btn btn-sm btn-ghost" @click="loadHistory(histPage - 1)" x-show="histPage > 1">← Prev</button><span class="page-info">Page <span x-text="histPage"></span> / <span x-text="histNumPages"></span></span><button class="btn btn-sm btn-ghost" @click="loadHistory(histPage + 1)" x-show="histHasNext">Next →</button></div></div></template>
395
412
  </div>
396
413
  </div>
@@ -414,9 +431,9 @@ input[type="number"]{-moz-appearance:textfield}input::-webkit-outer-spin-button,
414
431
  <div class="span-8">
415
432
  <div class="card">
416
433
  <div class="card-head"><div class="card-title">Usage</div></div>
417
- <template x-if="state?.usage?.length"><table class="tbl"><thead><tr><th>Model</th><th>Status</th><th>Remaining</th><th>Used</th></tr></thead><tbody>
434
+ <template x-if="state?.usage?.length"><div class="tbl-scroll"><table class="tbl"><thead><tr><th>Model</th><th>Status</th><th>Remaining</th><th>Used</th></tr></thead><tbody>
418
435
  <template x-for="(s, i) in state?.usage || []" :key="i"><tr><td style="font-size:12.5px" x-text="s.displayName || s.model || 'Unknown'"></td><td><span class="pill pill-active" x-show="s.active">ACTIVE</span><span class="pill pill-idle" x-show="!s.active">IDLE</span></td><td class="tbl-num" x-text="fmtDuration(s.remainingSeconds)"></td><td class="tbl-num" x-text="fmtDuration(s.usedSeconds)"></td></tr></template>
419
- </tbody></table></template>
436
+ </tbody></table></div></template>
420
437
  <template x-if="state && !state?.usage?.length"><div class="empty">No usage data</div></template>
421
438
  <template x-if="!state"><div class="empty">Loading…</div></template>
422
439
  </div>
@@ -427,7 +444,7 @@ input[type="number"]{-moz-appearance:textfield}input::-webkit-outer-spin-button,
427
444
  <button class="action-btn" :class="{active: aiAction==='refill'}" @click="aiAction = aiAction==='refill' ? null : 'refill'"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12a9 9 0 0 1 15-6.7L21 8"/><path d="M21 3v5h-5"/><path d="M21 12a9 9 0 0 1-15 6.7L3 16"/><path d="M3 21v-5h5"/></svg>Auto-Refill</button>
428
445
  <button class="action-btn" :class="{active: aiAction==='images'}" @click="aiAction = aiAction==='images' ? null : 'images'"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><path d="m21 15-5-5L5 21"/></svg>Generate Images</button>
429
446
  </div>
430
- <div class="grid" x-show="aiAction==='buy' || aiAction==='refill'">
447
+ <div class="grid" x-show="aiAction==='buy' || aiAction==='refill'" style="margin-top:16px">
431
448
  <div class="span-12">
432
449
  <div class="card">
433
450
  <div class="card-head"><div class="card-title" x-text="aiAction==='buy' ? 'Buy Credits' : 'Auto-Refill'"></div><button class="btn-close" @click="aiAction=null" title="Close">✕</button></div>
@@ -437,14 +454,15 @@ input[type="number"]{-moz-appearance:textfield}input::-webkit-outer-spin-button,
437
454
  <template x-for="plan in state?.plans || []" :key="plan.modelId">
438
455
  <div class="plan-group">
439
456
  <div class="plan-model" x-text="plan.displayName || plan.modelId"></div>
440
- <div class="plan-tiers">
441
- <template x-for="tier in plan.tiers || []" :key="tier.minutes">
442
- <div class="plan-tier">
443
- <span class="plan-duration" x-text="tier.durationDisplay"></span>
444
- <span class="plan-price" x-text="tier.priceUsd != null ? '$' + tier.priceUsd.toFixed(2) : (tier.priceSats/1e8).toFixed(8) + ' BCH'"></span>
445
- <button class="btn btn-sm btn-primary" @click="startPurchase(plan.modelId, tier.minutes, tier.priceUsd, tier.priceSats, tier.durationDisplay, plan.displayName || plan.modelId)">Buy</button>
446
- </div>
447
- </template>
457
+ <div class="plan-slider">
458
+ <input type="range" min="0" max="3" step="1" :value="planSliderIdx[plan.modelId] || 0" @input="setPlanSlider(plan.modelId, parseInt($event.target.value))" :aria-label="'Duration for ' + (plan.displayName || plan.modelId)" :style="'background:linear-gradient(to right,var(--accent) 0%,var(--accent) ' + ((planSliderIdx[plan.modelId] || 0) / 3 * 100) + '%,var(--line) ' + ((planSliderIdx[plan.modelId] || 0) / 3 * 100) + '%,var(--line) 100%)'">
459
+ <div class="plan-ticks"><span>0</span><span>15</span><span>30</span><span>60</span></div>
460
+ </div>
461
+ <div class="plan-selection">
462
+ <template x-if="planMinutes(plan.modelId) > 0"><span style="display:flex;align-items:center;gap:12px"><span class="plan-duration" x-text="planTier(plan)?.durationDisplay || ''"></span><span class="plan-price" x-text="tierPriceLabel(planTier(plan))"></span><template x-if="tierInsufficient(planTier(plan))"><span class="plan-insufficient">insufficient balance</span></template></span></template>
463
+ <template x-if="!planMinutes(plan.modelId)"><span class="plan-hint">Slide to select duration</span></template>
464
+ <span class="grow"></span>
465
+ <button class="btn btn-sm btn-primary" :disabled="!planMinutes(plan.modelId) || tierInsufficient(planTier(plan))" @click="buyPlan(plan)">Buy</button>
448
466
  </div>
449
467
  </div>
450
468
  </template>
@@ -454,20 +472,26 @@ input[type="number"]{-moz-appearance:textfield}input::-webkit-outer-spin-button,
454
472
  </div></template>
455
473
  <template x-if="aiAction==='refill'"><div>
456
474
  <template x-if="state && (!state.autoRefill || refillEditing)"><div>
457
- <div style="font-size:12.5px;color:var(--ink-3);margin-bottom:12px">Auto-refill buys a new plan when credits run out. It tops up immediately if this model has no active credits.</div>
458
- <div class="inline-row">
459
- <div class="field"><label class="field-label">Model</label><select class="field-input" x-model="refillModel"><option value="">Select model</option><template x-for="p in state?.plans || []" :key="p.modelId"><option :value="p.modelId" x-text="p.displayName || p.modelId"></option></template></select></div>
460
- <div class="field"><label class="field-label">Top up (minutes)</label><select class="field-input" x-model="refillMinutes"><template x-for="m in [15,30,60]" :key="m"><option :value="String(m)" x-text="m"></option></template><template x-if="refillMinutes && ![15,30,60].includes(parseInt(refillMinutes))"><option :value="refillMinutes" x-text="refillMinutes"></option></template></select></div>
461
- </div>
462
- <div class="inline-row" style="margin-top:12px">
463
- <div class="field"><label class="field-label">Budget cap (minutes, optional)</label><input class="field-input" type="number" min="1" x-model="refillMax" placeholder="Unlimited"></div>
464
- <div class="field"><label class="field-label">Pay with</label><select class="field-input" x-model="refillPayMethod"><option value="bch">BCH</option><option value="lift">LIFT</option></select></div>
465
- </div>
466
- <div style="margin-top:6px;font-size:11.5px;color:var(--ink-3)">Budget cap must be at least twice the top-up and a whole-number multiple of it<span x-text="' (min ' + (parseInt(refillMinutes||'30')*2) + ' min, steps of ' + parseInt(refillMinutes||'30') + ')'"></span>.</div>
467
- <template x-if="refillNotice"><div style="margin-top:12px;font-size:12.5px;color:var(--ink-3)" x-text="refillNotice"></div></template>
468
- <div style="margin-top:16px;display:flex;gap:8px">
469
- <button class="btn btn-sm btn-primary" @click="enableRefill()" x-text="refillEditing ? 'Save & Resume' : 'Enable Auto-Refill'"></button>
470
- <button class="btn btn-sm" x-show="refillEditing" @click="cancelRefillEdit()">Cancel</button>
475
+ <div style="font-size:12.5px;color:var(--ink-3);margin-bottom:14px">Auto-refill buys a new plan when credits run out. It tops up immediately if this model has no active credits.</div>
476
+ <div class="plan-group" style="padding-top:0">
477
+ <div class="refill-model"><label class="field-label">Model</label><select class="field-input" x-model="refillModel"><option value="">Select model</option><template x-for="p in state?.plans || []" :key="p.modelId"><option :value="p.modelId" x-text="p.displayName || p.modelId"></option></template></select></div>
478
+ <div class="plan-slider">
479
+ <input type="range" min="0" max="2" step="1" :value="refillMinutesIdx" @input="refillMinutesIdx = parseInt($event.target.value)" aria-label="Top-up duration" :style="'background:linear-gradient(to right,var(--accent) 0%,var(--accent) ' + (refillMinutesIdx / 2 * 100) + '%,var(--line) ' + (refillMinutesIdx / 2 * 100) + '%,var(--line) 100%)'">
480
+ <div class="plan-ticks"><span>15m</span><span>30m</span><span>1h</span></div>
481
+ </div>
482
+ <div class="plan-slider" style="margin-top:10px">
483
+ <input type="range" min="0" max="3" step="1" :value="refillCapIdx" @input="refillCapIdx = parseInt($event.target.value)" aria-label="Budget cap multiplier" :style="'background:linear-gradient(to right,var(--accent) 0%,var(--accent) ' + (refillCapIdx / 3 * 100) + '%,var(--line) ' + (refillCapIdx / 3 * 100) + '%,var(--line) 100%)'">
484
+ <div class="plan-ticks"><span>2×</span><span>4×</span><span>8×</span><span>∞</span></div>
485
+ </div>
486
+ <div class="refill-pay"><span class="field-label">Pay with</span><div class="seg"><button :class="{active: refillPayMethod==='bch'}" @click="refillPayMethod='bch'">BCH</button><button :class="{active: refillPayMethod==='lift'}" @click="refillPayMethod='lift'">LIFT</button></div></div>
487
+ <div class="plan-selection">
488
+ <template x-if="refillModel"><span style="display:flex;align-items:center;gap:12px"><span class="plan-duration" x-text="refillTopUpMinutes() + 'm top-up'"></span><span class="plan-price" style="font-size:11.5px;color:var(--ink-3)" x-text="refillCapLabel()"></span></span></template>
489
+ <template x-if="!refillModel"><span class="plan-hint">Select a model to configure auto-refill</span></template>
490
+ <span class="grow"></span>
491
+ <button class="btn btn-sm btn-primary" :disabled="!refillModel" @click="enableRefill()" x-text="refillEditing ? 'Save & Resume' : 'Enable'"></button>
492
+ <template x-if="refillEditing"><button class="btn btn-sm" @click="cancelRefillEdit()">Cancel</button></template>
493
+ </div>
494
+ <template x-if="refillNotice"><div style="margin-top:10px;font-size:12px;color:var(--ink-3)" x-text="refillNotice"></div></template>
471
495
  </div>
472
496
  </div></template>
473
497
  <template x-if="state?.autoRefill && !refillEditing"><div>
@@ -495,7 +519,7 @@ input[type="number"]{-moz-appearance:textfield}input::-webkit-outer-spin-button,
495
519
  </div>
496
520
  </div>
497
521
  </div>
498
- <div x-show="aiAction==='images'" class="grid">
522
+ <div x-show="aiAction==='images'" class="grid" style="margin-top:16px">
499
523
  <div class="span-5">
500
524
  <div class="card">
501
525
  <div class="card-head"><div class="card-title">Image Generation</div></div>
@@ -579,20 +603,30 @@ token:(function(){var t=new URLSearchParams(window.location.search).get('token')
579
603
  rcvType:'bch',rcvCategory:'',rcvAmount:'',rcvView:null,rcvQrSvg:'',
580
604
  sendType:'bch',sendAmount:'',sendCurrency:'bch',sendAddress:'',sendCategory:'',sendTokenPreset:'__custom__',sendTokenAmount:'',sendError:'',sendSuccess:'',pendingSend:null,
581
605
  histType:'all',histRecords:[],histPage:1,histNumPages:1,histHasNext:false,histLoading:false,histNetwork:'mainnet',
582
- swapTokens:[{symbol:'LIFT',category:'5932b2fd4915d6a75d3ec53282cd49118149a2176ee67ed68b1111ff0786f7fc'},{symbol:'PUSD',category:'2469acc5afa4b10cb5b5c04afb89c3a3ffd61c5da9c01e26d00951cae2a02544'}],swapToken:'5932b2fd4915d6a75d3ec53282cd49118149a2176ee67ed68b1111ff0786f7fc',swapCustomCategory:'',swapDir:'sell',swapAmount:'',swapError:'',swapQuoteText:'',swapBusy:false,swapExecBusy:false,pendingSwap:null,
583
- pendingPurchase:null,purchaseBusy:false,purchaseMethod:'bch',purchaseError:'',liftQuote:null,liftQuoteBusy:false,liftQuoteError:'',showPurchaseModal:false,showSendModal:false,sendConfirmBusy:false,
606
+ swapTokens:[{symbol:'LIFT',category:'5932b2fd4915d6a75d3ec53282cd49118149a2176ee67ed68b1111ff0786f7fc'},{symbol:'PUSD',category:'2469acc5afa4b10cb5b5c04afb89c3a3ffd61c5da9c01e26d00951cae2a02544'}],swapToken:'5932b2fd4915d6a75d3ec53282cd49118149a2176ee67ed68b1111ff0786f7fc',swapCustomCategory:'',swapDir:'buy',swapAmount:'',swapError:'',swapQuoteText:'',swapBusy:false,swapExecBusy:false,pendingSwap:null,
607
+ pendingPurchase:null,purchaseBusy:false,purchaseMethod:'bch',purchaseError:'',liftQuote:null,liftQuoteBusy:false,liftQuoteError:'',showPurchaseModal:false,showSendModal:false,sendConfirmBusy:false,planSliderIdx:{},PLAN_STEPS:[0,15,30,60],
584
608
  pendingImageQuote:null,showImageQuoteModal:false,imgGenBusy:false,imgQuoteBusy:false,walletAction:null,aiAction:null,lightbox:null,lightboxConfirmDelete:false,imgUrls:{},
585
- refillModel:'',refillMinutes:'30',refillMax:'',refillPayMethod:'bch',refillEditing:false,refillNotice:'',
609
+ refillModel:'',refillMinutesIdx:1,refillCapIdx:0,REFILL_MINUTES:[15,30,60],REFILL_CAPS:[2,4,8,-1],refillPayMethod:'bch',refillEditing:false,refillNotice:'',
586
610
  imgPrompt:'',imgModel:'bytedance-seed/seedream-5-0-pro',imgAspect:'1:1',imgQuality:'auto',
587
611
  toastVisible:false,toastMsg:'',toastOk:true,
588
612
  ws:null,wsStatus:'connecting',wsAddress:null,wsSeen:{},wsReconnectTimer:null,wsPingTimer:null,wsRefreshTimer:null,wsLateTimer:null,notify:null,_ntid:0,
589
- async init(){document.documentElement.setAttribute('data-theme',this.theme);try{this.state=await this.api('GET','/api/wallet/state');this.loadHistory(1);await this.onRcvChange();this.connectWatch()}catch(e){this.toast(e.message,true)}var self=this;document.addEventListener('visibilitychange',function(){if(document.visibilityState==='visible'&&self.state){if(!self.ws||self.ws.readyState!==1){if(self.wsReconnectTimer)clearTimeout(self.wsReconnectTimer);self.connectWatch()}}});window.addEventListener('online',function(){if(self.state){if(self.wsReconnectTimer)clearTimeout(self.wsReconnectTimer);self.connectWatch()}})},
590
- async load(){try{this.state=await this.api('GET','/api/wallet/state');this.onRcvChange();this.loadHistory(this.histPage);(this.state.imageHistory||[]).forEach(h=>{if(h.status==='completed'&&h.filepath)this.loadImage(h.id)})}catch(e){this.toast(e.message,true)}},
613
+ async init(){document.documentElement.setAttribute('data-theme',this.theme);try{this.state=await this.api('GET','/api/wallet/state');this.loadHistory(1);this.loadImages();await this.onRcvChange();this.connectWatch()}catch(e){this.toast(e.message,true)}var self=this;document.addEventListener('visibilitychange',function(){if(document.visibilityState==='visible'&&self.state){if(!self.ws||self.ws.readyState!==1){if(self.wsReconnectTimer)clearTimeout(self.wsReconnectTimer);self.connectWatch()}}});window.addEventListener('online',function(){if(self.state){if(self.wsReconnectTimer)clearTimeout(self.wsReconnectTimer);self.connectWatch()}})},
614
+ async load(){try{this.state=await this.api('GET','/api/wallet/state');this.onRcvChange();this.loadHistory(this.histPage);this.loadImages()}catch(e){this.toast(e.message,true)}},
615
+ loadImages(){var s=this;(this.state&&this.state.imageHistory||[]).forEach(function(h){if(h.status==='completed'&&h.filepath)s.loadImage(h.id)})},
591
616
  async loadImage(id){if(!id||this.imgUrls[id])return;try{var res=await fetch('/api/ai/images/'+id+'/file',{headers:{'X-Paytaca-Token':this.token}});if(!res.ok)return;var blob=await res.blob();this.imgUrls[id]=URL.createObjectURL(blob)}catch(e){}},
592
617
  async api(method,apipath,body){var opts={method:method,headers:{'X-Paytaca-Token':this.token,'Accept':'application/json'}};if(body!==undefined){opts.headers['Content-Type']='application/json';opts.body=JSON.stringify(body)}var res=await fetch(apipath,opts);if(!res.ok){var msg='Request failed ('+res.status+')';try{var j=await res.json();if(j.error)msg=j.error}catch(ex){}throw new Error(msg)}return res.json()},
593
618
  fmtDuration(s){if(!s||s<=0)return '0m';var h=Math.floor(s/3600),m=Math.floor((s%3600)/60);return h?h+'h '+m+'m':m+'m'},
594
619
  bchPriceLabel(p){if(!p)return '';return p.priceUsd!=null?'$'+p.priceUsd.toFixed(2)+' USD':((p.priceSats||0)/1e8).toFixed(8)+' BCH'},
595
620
  liftPriceLabel(p){if(!p)return '';var pct=(this.state&&this.state.liftDiscountPercent)||0;var f=1-pct/100;return p.priceUsd!=null?'$'+(p.priceUsd*f).toFixed(2)+' USD':(((p.priceSats||0)*f)/1e8).toFixed(8)+' BCH'},
621
+ planMinutes(modelId){return this.PLAN_STEPS[this.planSliderIdx[modelId]||0]||0},
622
+ setPlanSlider(modelId,val){var next={};var keys=Object.keys(this.planSliderIdx);for(var i=0;i<keys.length;i++){next[keys[i]]=keys[i]===modelId?val:0}next[modelId]=val;this.planSliderIdx=next},
623
+ planTier(plan){var m=this.planMinutes(plan.modelId);if(!m)return null;var tiers=(plan&&plan.tiers)||[],i;for(i=0;i<tiers.length;i++)if(tiers[i].minutes===m)return tiers[i];var sorted=tiers.slice().sort(function(a,b){return a.minutes-b.minutes});for(i=0;i<sorted.length;i++)if(sorted[i].minutes>=m)return sorted[i];return sorted[sorted.length-1]||null},
624
+ tierPriceLabel(tier){if(!tier)return '';return tier.priceUsd!=null?'$'+tier.priceUsd.toFixed(2):(tier.priceSats/1e8).toFixed(8)+' BCH'},
625
+ tierInsufficient(tier){if(!tier)return false;var have=this.state&&this.state.balance?Number(this.state.balance.spendableSats):0;return have<Number(tier.priceSats||0)},
626
+ buyPlan(plan){var t=this.planTier(plan);if(!t)return;this.startPurchase(plan.modelId,t.minutes,t.priceUsd,t.priceSats,t.durationDisplay,plan.displayName||plan.modelId)},
627
+ refillTopUpMinutes(){return this.REFILL_MINUTES[this.refillMinutesIdx]||30},
628
+ refillCapMinutes(){var cap=this.REFILL_CAPS[this.refillCapIdx];return cap>0?cap*this.refillTopUpMinutes():null},
629
+ refillCapLabel(){var cap=this.refillCapMinutes();if(!cap)return 'No cap';return 'Max '+cap+' min ('+this.REFILL_CAPS[this.refillCapIdx]+'×)'},
596
630
 
597
631
  async loadLiftQuote(){var p=this.pendingPurchase;if(!p||!p.priceSats){this.liftQuote=null;this.liftQuoteError='';return}var pct=(this.state&&this.state.liftDiscountPercent)||0;var sats=Math.round(p.priceSats*(1-pct/100));if(sats<1)sats=1;this.liftQuoteBusy=true;this.liftQuoteError='';try{this.liftQuote=await this.api('GET','/api/ai/lift-quote?sats='+sats)}catch(e){this.liftQuote=null;this.liftQuoteError=e.message||'Failed to estimate LIFT needed'}this.liftQuoteBusy=false},
598
632
 
@@ -609,17 +643,18 @@ startWsPing(){var self=this;if(this.wsPingTimer)clearTimeout(this.wsPingTimer);t
609
643
  handleWsUpdate(m){if(!m||!m.txid)return;var key=m.txid+'|'+(m.address||'')+'|'+(m.index==null?'':m.index);if(this.wsSeen[key])return;this.wsSeen[key]=1;if(Object.keys(this.wsSeen).length>400)this.wsSeen={};this.showTxToast(m,Array.isArray(m.senders)&&m.senders.length>0)},
610
644
  showTxToast(m,incoming){var self=this;var sym=(m.token_symbol||'').toLowerCase();var tname=(m.token_name||'').toLowerCase();var isBch=!m.token_symbol||sym==='bch'||tname==='bch'||tname==='bitcoin cash'||m.token==='bch';var amountText;if(isBch){var sats=m.value!=null?Number(m.value):null;var bch=sats!=null&&isFinite(sats)?sats/1e8:null;amountText=bch!=null?bch.toFixed(8).replace(/\.?0+$/,'')+' BCH':'New activity';if(bch!=null&&this.state&&this.state.bchPriceUsd){var usd=bch*this.state.bchPriceUsd;if(usd>0.0001){var usdStr=usd.toFixed(2);if(usdStr.slice(-3)==='.00')usdStr=usdStr.slice(0,-3);amountText+=' ('+usdStr+' USD)'}}}else{amountText=m.amount!=null?(m.amount+' '+m.token_symbol):(m.token_symbol||'Token')+' transfer'}var base=this.state&&this.state.network==='chipnet'?'https://chipnet.bchexplorer.info/tx/':'https://bchexplorer.info/tx/';var id=m.txid||'';var tid=++self._ntid;var n={title:incoming?'Incoming transaction':'Wallet activity',amountText:amountText,txidShort:id.slice(0,10)+'…'+id.slice(-6),explorer:base+id};this.notify=n;if(this.wsRefreshTimer)clearTimeout(this.wsRefreshTimer);this.wsRefreshTimer=setTimeout(function(){self.load()},1200);if(this.wsLateTimer)clearTimeout(this.wsLateTimer);this.wsLateTimer=setTimeout(function(){self.load()},5000);setTimeout(function(){if(self._ntid===tid)self.notify=null},6000)},
611
645
  async onRcvChange(){this.rcvView=null;this.rcvQrSvg='';var params=new URLSearchParams();if(this.rcvType==='token'&&this.rcvCategory){params.set('category',this.rcvCategory);params.set('token','1')}if(this.rcvAmount)params.set('amount',this.rcvAmount);try{var view=await this.api('GET','/api/wallet/receive?'+params.toString());this.rcvView=view;if(view.address){try{this.rcvQrSvg=QR.toSVG(QR.encode(view.address))}catch(ex){}}if(view&&view.address&&this.wsAddress!==view.address)this.connectWatch(view.address)}catch(e){this.rcvView={address:'Error: '+e.message}}},
646
+ fillMaxSend(){var bal=this.state&&this.state.balance;if(!bal)return;if(this.sendCurrency==='bch'){this.sendAmount=String(bal.spendableBch||'')}else if(this.sendCurrency==='sats'){this.sendAmount=String(bal.spendableSats||0)}else{var bch=parseFloat(bal.spendableBch)||0;var price=this.state.bchPriceUsd||0;this.sendAmount=price?(bch*price).toFixed(2):''}},
612
647
  doSend(){this.sendError='';this.sendSuccess='';var address=(this.sendAddress||'').trim();if(!address){this.sendError='Enter a recipient address';return}if(this.sendType==='bch'){var amount=parseFloat(this.sendAmount);var currency=this.sendCurrency;if(!amount||amount<=0){this.sendError='Enter a valid amount';return}this.pendingSend={type:'bch',address:address,amount:amount,currency:currency};this.showSendModal=true}else{var category=(this.sendCategory||'').trim();var tokenAmount=(this.sendTokenAmount||'').trim();if(!category||!/^[a-fA-F0-9]{64}$/.test(category)){this.sendError='Enter a valid 64-char hex category ID';return}if(!tokenAmount){this.sendError='Enter a token amount';return}this.pendingSend={type:'token',category:category,tokenAmount:tokenAmount,address:address};this.showSendModal=true}},
613
648
  async confirmSend(){if(!this.pendingSend)return;this.sendConfirmBusy=true;try{var result;if(this.pendingSend.type==='bch'){result=await this.api('POST','/api/wallet/send',this.pendingSend)}else{result=await this.api('POST','/api/wallet/send-token',{category:this.pendingSend.category,amount:this.pendingSend.tokenAmount,address:this.pendingSend.address})}this.showSendModal=false;if(result.success){this.sendSuccess='Sent! TX: '+(result.txid||'').slice(0,16)+'...';this.toast('Transaction sent');this.load()}else{this.sendError=result.error||'Send failed'}}catch(e){this.sendError=e.message}this.sendConfirmBusy=false;this.pendingSend=null},
614
- async loadHistory(page){this.histPage=page||1;this.histLoading=true;this.histRecords=[];try{var data=await this.api('GET','/api/wallet/history?page='+this.histPage+'&type='+this.histType);var net=data.network||'mainnet';this.histNetwork=net;var base=net==='chipnet'?'https://chipnet.bchexplorer.info/tx/':'https://bchexplorer.info/tx/';this.histRecords=(data.records||[]).map(function(r){var isIn=r.record_type==='incoming';var party=isIn?((r.senders&&r.senders[0]&&r.senders[0][0])||''):((r.recipients&&r.recipients[0]&&r.recipients[0][0])||'');var amt=Number(r.amount)||0;var when=r.tx_timestamp||r.date_created||'';var dt=when?new Date(when):null;return{txid:r.txid,type:isIn?'incoming':'outgoing',dateText:dt?dt.toLocaleString(undefined,{year:'numeric',month:'short',day:'numeric',hour:'2-digit',minute:'2-digit'}):'',counterparty:party,partyShort:party?(party.slice(0,16)+'…'+party.slice(-6)):'—',amount:amt,amountText:amt.toFixed(8),usd:r.usd_price!=null?(amt*Number(r.usd_price)).toFixed(2):null,fee:r.tx_fee!=null?(Number(r.tx_fee)/1e8).toFixed(8):null,explorer:r.txid?base+r.txid:''}});this.histNumPages=data.numPages||1;this.histHasNext=!!data.hasNext}catch(e){this.histRecords=[]}this.histLoading=false},
649
+ async loadHistory(page){this.histPage=page||1;this.histLoading=true;this.histRecords=[];try{var data=await this.api('GET','/api/wallet/history?page='+this.histPage+'&type='+this.histType);var net=data.network||'mainnet';this.histNetwork=net;var base=net==='chipnet'?'https://chipnet.bchexplorer.info/tx/':'https://bchexplorer.info/tx/';this.histRecords=(data.records||[]).map(function(r){var isIn=r.record_type==='incoming';var party=isIn?((r.senders&&r.senders[0]&&r.senders[0][0])||''):((r.recipients&&r.recipients[0]&&r.recipients[0][0])||'');var amt=Number(r.amount)||0;var when=r.tx_timestamp||r.date_created||'';var dt=when?new Date(when):null;return{txid:r.txid,type:isIn?'incoming':'outgoing',dateText:dt?dt.toLocaleString(undefined,{year:'numeric',month:'short',day:'numeric',hour:'2-digit',minute:'2-digit'}):'',txidShort:r.txid?(r.txid.slice(0,10)+'…'+r.txid.slice(-6)):'',counterparty:party,partyShort:party?(party.slice(0,16)+'…'+party.slice(-6)):'—',amount:amt,amountText:amt.toFixed(8),usd:r.usd_price!=null?(amt*Number(r.usd_price)).toFixed(2):null,fee:r.tx_fee!=null?(Number(r.tx_fee)/1e8).toFixed(8):null,explorer:r.txid?base+r.txid:''}});this.histNumPages=data.numPages||1;this.histHasNext=!!data.hasNext}catch(e){this.histRecords=[]}this.histLoading=false},
615
650
  async doSwapQuote(){var tokenId=this.swapToken==='__custom__'?(this.swapCustomCategory||'').trim():(this.swapToken||'').trim();var amount=(this.swapAmount||'').trim();this.swapError='';if(!tokenId||!/^[a-fA-F0-9]{64}$/.test(tokenId)){this.swapError='Enter a valid 64-char hex token ID';return}if(!amount||isNaN(parseFloat(amount))||parseFloat(amount)<=0){this.swapError='Enter a valid amount';return}this.swapBusy=true;try{var q=await this.api('POST','/api/swap/quote',{tokenId:tokenId,direction:this.swapDir,amount:amount});this.pendingSwap={tokenId:tokenId,direction:this.swapDir,amount:amount};var text=q.formatted||'';if(!text){text='Rate: '+(q.rate||'?')+'\\nToken: '+q.tokenAmount+' '+(q.symbol||'')+'\\nBCH: '+(Number(q.bchAmountSats)/1e8).toFixed(8)+' BCH\\nTrade fee: '+(Number(q.tradeFeeSats)/1e8).toFixed(8)+' BCH';if(q.platformFeeSats)text+='\\nPlatform fee: '+(Number(q.platformFeeSats)/1e8).toFixed(8)+' BCH'}this.swapQuoteText=text}catch(e){this.swapError=e.message}this.swapBusy=false},
616
651
  async doSwapExecute(){if(!this.pendingSwap)return;this.swapExecBusy=true;try{var result=await this.api('POST','/api/swap/execute',this.pendingSwap);if(result.success){this.toast('Swap executed! TX: '+(result.txid||'').slice(0,16)+'...');this.swapQuoteText='';this.pendingSwap=null;this.load()}else{this.swapError=result.error||'Swap failed'}}catch(e){this.swapError=e.message}this.swapExecBusy=false},
617
652
  startPurchase(model,minutes,priceUsd,priceSats,durationDisplay,displayName){this.pendingPurchase={model:model,minutes:minutes,priceUsd:priceUsd,priceSats:priceSats,durationDisplay:durationDisplay,displayName:displayName};this.purchaseMethod='bch';this.purchaseError='';this.liftQuote=null;this.liftQuoteError='';this.showPurchaseModal=true},
618
- async confirmPurchase(){if(!this.pendingPurchase)return;this.purchaseBusy=true;this.purchaseError='';try{var r=await this.api('POST','/api/ai/purchase',{model:this.pendingPurchase.model,minutes:this.pendingPurchase.minutes,method:this.purchaseMethod});if(r&&r.success&&r.paid){this.showPurchaseModal=false;this.pendingPurchase=null;this.toast('Plan purchased!');this.load()}else if(r&&r.success&&!r.paid){this.showPurchaseModal=false;this.pendingPurchase=null;this.toast(r.error||'Payment submitted — credits will update shortly',true);this.load()}else{this.purchaseError=(r&&r.error)||'Purchase failed';this.toast(this.purchaseError,true)}}catch(e){this.purchaseError=e.message||'Purchase failed';this.toast(e.message,true)}this.purchaseBusy=false},
619
- async enableRefill(){var model=this.refillModel;var minutes=parseInt(this.refillMinutes)||30;var paymentMethod=this.refillPayMethod||'bch';var maxMinutes;if(!model){this.toast('Select a model',true);return}if(this.refillMax!==''&&this.refillMax!=null){var raw=Number(this.refillMax);if(!Number.isInteger(raw)||raw<=0){this.toast('Budget cap must be a whole number',true);return}if(raw<minutes*2){this.toast('Budget cap must be at least twice the top-up ('+minutes*2+' min)',true);return}if(raw%minutes!==0){this.toast('Budget cap must be a multiple of the top-up ('+minutes+' min)',true);return}maxMinutes=raw}try{var r=await this.api('POST','/api/ai/auto-refill',{enabled:true,model:model,minutes:minutes,maxMinutes:maxMinutes,paymentMethod:paymentMethod});this.refillNotice=this.refillNoticeText(r&&r.initialTick);this.refillEditing=false;this.toast('Auto-refill enabled');this.load()}catch(e){this.toast(e.message,true)}},
653
+ async confirmPurchase(){if(!this.pendingPurchase)return;this.purchaseBusy=true;this.purchaseError='';try{var r=await this.api('POST','/api/ai/purchase',{model:this.pendingPurchase.model,minutes:this.pendingPurchase.minutes,method:this.purchaseMethod});if(r&&r.success&&r.paid){this.showPurchaseModal=false;this.pendingPurchase=null;this.planSliderIdx={};this.toast('Plan purchased!');this.load()}else if(r&&r.success&&!r.paid){this.showPurchaseModal=false;this.pendingPurchase=null;this.toast(r.error||'Payment submitted — credits will update shortly',true);this.load()}else{this.purchaseError=(r&&r.error)||'Purchase failed';this.toast(this.purchaseError,true)}}catch(e){this.purchaseError=e.message||'Purchase failed';this.toast(e.message,true)}this.purchaseBusy=false},
654
+ async enableRefill(){var model=this.refillModel;var minutes=this.refillTopUpMinutes();var maxMinutes=this.refillCapMinutes();var paymentMethod=this.refillPayMethod||'bch';if(!model){this.toast('Select a model',true);return}try{var r=await this.api('POST','/api/ai/auto-refill',{enabled:true,model:model,minutes:minutes,maxMinutes:maxMinutes,paymentMethod:paymentMethod});this.refillNotice=this.refillNoticeText(r&&r.initialTick);this.refillEditing=false;this.toast('Auto-refill enabled');this.load()}catch(e){this.toast(e.message,true)}},
620
655
  async resumeRefill(){try{var r=await this.api('POST','/api/ai/auto-refill',{enabled:true});this.refillNotice=this.refillNoticeText(r&&r.initialTick);this.toast('Auto-refill resumed');this.load()}catch(e){this.toast(e.message,true)}},
621
656
  async pauseRefill(){try{await this.api('POST','/api/ai/auto-refill',{enabled:false});this.refillNotice='';this.toast('Auto-refill paused');this.load()}catch(e){this.toast(e.message,true)}},
622
- startRefillEdit(){if(!this.state||!this.state.autoRefill)return;this.refillModel=this.state.autoRefill.model||'';this.refillMinutes=String(this.state.autoRefill.minutes||30);this.refillMax=this.state.autoRefill.maxMinutes?String(this.state.autoRefill.maxMinutes):'';this.refillPayMethod=this.state.autoRefill.paymentMethod||'bch';this.refillNotice='';this.refillEditing=true},
657
+ startRefillEdit(){if(!this.state||!this.state.autoRefill)return;var ar=this.state.autoRefill;this.refillModel=ar.model||'';var mins=ar.minutes||30;var mi=this.REFILL_MINUTES.indexOf(mins);this.refillMinutesIdx=mi>=0?mi:1;this.refillCapIdx=ar.maxMinutes?Math.round(ar.maxMinutes/mins)<=2?0:Math.round(ar.maxMinutes/mins)<=4?1:2:3;this.refillPayMethod=ar.paymentMethod||'bch';this.refillNotice='';this.refillEditing=true},
623
658
  cancelRefillEdit(){this.refillEditing=false;this.refillNotice=''},
624
659
  refillNoticeText(tick){if(!tick)return '';if(tick.action==='refilled'){var m=(tick.state&&tick.state.minutes)||'';var tx=tick.state&&tick.state.lastRefillTxid;return 'Topped up '+m+' min now'+(tx?' (tx '+String(tx).slice(0,10)+'…)':'')+'.'}if(tick.action==='skipped')return 'Refill skipped: '+tick.reason+'.';if(tick.action==='disarmed')return 'Auto-refill stopped: '+tick.reason+'.';return 'Credits still active — no refill needed.'},
625
660
  async deleteRefill(){if(!confirm('Delete auto-refill configuration?'))return;try{await this.api('POST','/api/ai/auto-refill',{delete:true});this.refillEditing=false;this.refillNotice='';this.toast('Auto-refill deleted');this.load()}catch(e){this.toast(e.message,true)}},
@@ -1 +1 @@
1
- {"version":3,"file":"page.js","sourceRoot":"","sources":["../../src/web/page.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAA;AACxB,OAAO,IAAI,MAAM,WAAW,CAAA;AAC5B,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAC3C,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAE3D,MAAM,OAAO,GAAG,aAAa,CAAC,IAAI,GAAG,CAAC,GAAG,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,CAAA;AACjE,MAAM,SAAS,GAAG,EAAE,CAAC,YAAY,CAC/B,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,uBAAuB,CAAC,CAAC,EAAE,MAAM,EAAE,YAAY,CAAC,EACvF,MAAM,CACP,CAAA;AAED,MAAM,UAAU,UAAU;IACxB,OAAO;;;;;;0CAMiC,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qCAsQjB,YAAY;;;;;;wGAMuD,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;sCAuHjF,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;gBAwOrC,SAAS;;QAEjB,CAAC;AACT,CAAC"}
1
+ {"version":3,"file":"page.js","sourceRoot":"","sources":["../../src/web/page.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAA;AACxB,OAAO,IAAI,MAAM,WAAW,CAAA;AAC5B,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAC3C,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAE3D,MAAM,OAAO,GAAG,aAAa,CAAC,IAAI,GAAG,CAAC,GAAG,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,CAAA;AACjE,MAAM,SAAS,GAAG,EAAE,CAAC,YAAY,CAC/B,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,uBAAuB,CAAC,CAAC,EAAE,MAAM,EAAE,YAAY,CAAC,EACvF,MAAM,CACP,CAAA;AAED,MAAM,UAAU,UAAU;IACxB,OAAO;;;;;;0CAMiC,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qCA2RjB,YAAY;;;;;;wGAMuD,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;sCAmHjF,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;gBA0PrC,SAAS;;QAEjB,CAAC;AACT,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "paytaca-cli",
3
- "version": "0.8.0",
3
+ "version": "0.8.2",
4
4
  "description": "Command-line interface for the Paytaca Bitcoin Cash wallet",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",