paytaca-cli 0.7.0 → 0.8.1

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.
Files changed (59) hide show
  1. package/README.md +72 -6
  2. package/dist/ai/autoRefill.d.ts +58 -0
  3. package/dist/ai/autoRefill.d.ts.map +1 -1
  4. package/dist/ai/autoRefill.js +193 -2
  5. package/dist/ai/autoRefill.js.map +1 -1
  6. package/dist/ai/config.d.ts +1 -0
  7. package/dist/ai/config.d.ts.map +1 -1
  8. package/dist/ai/config.js +1 -0
  9. package/dist/ai/config.js.map +1 -1
  10. package/dist/ai/credits.d.ts.map +1 -1
  11. package/dist/ai/credits.js +20 -11
  12. package/dist/ai/credits.js.map +1 -1
  13. package/dist/ai/images.d.ts +128 -0
  14. package/dist/ai/images.d.ts.map +1 -0
  15. package/dist/ai/images.js +399 -0
  16. package/dist/ai/images.js.map +1 -0
  17. package/dist/commands/ai.d.ts.map +1 -1
  18. package/dist/commands/ai.js +283 -7
  19. package/dist/commands/ai.js.map +1 -1
  20. package/dist/commands/update.d.ts +32 -0
  21. package/dist/commands/update.d.ts.map +1 -0
  22. package/dist/commands/update.js +200 -0
  23. package/dist/commands/update.js.map +1 -0
  24. package/dist/commands/wallet.js +1 -1
  25. package/dist/commands/wallet.js.map +1 -1
  26. package/dist/commands/web.d.ts +3 -0
  27. package/dist/commands/web.d.ts.map +1 -0
  28. package/dist/commands/web.js +71 -0
  29. package/dist/commands/web.js.map +1 -0
  30. package/dist/index.js +4 -0
  31. package/dist/index.js.map +1 -1
  32. package/dist/mcp/server.d.ts.map +1 -1
  33. package/dist/mcp/server.js +29 -1
  34. package/dist/mcp/server.js.map +1 -1
  35. package/dist/mcp/tools.d.ts +2 -0
  36. package/dist/mcp/tools.d.ts.map +1 -1
  37. package/dist/mcp/tools.js +288 -9
  38. package/dist/mcp/tools.js.map +1 -1
  39. package/dist/wallet/cauldron/swap.d.ts +15 -0
  40. package/dist/wallet/cauldron/swap.d.ts.map +1 -1
  41. package/dist/wallet/cauldron/swap.js +31 -0
  42. package/dist/wallet/cauldron/swap.js.map +1 -1
  43. package/dist/wallet/index.d.ts +16 -1
  44. package/dist/wallet/index.d.ts.map +1 -1
  45. package/dist/wallet/index.js +74 -8
  46. package/dist/wallet/index.js.map +1 -1
  47. package/dist/web/assets.d.ts +3 -0
  48. package/dist/web/assets.d.ts.map +1 -0
  49. package/dist/web/assets.js +3 -0
  50. package/dist/web/assets.js.map +1 -0
  51. package/dist/web/page.d.ts +2 -0
  52. package/dist/web/page.d.ts.map +1 -0
  53. package/dist/web/page.js +638 -0
  54. package/dist/web/page.js.map +1 -0
  55. package/dist/web/server.d.ts +79 -0
  56. package/dist/web/server.d.ts.map +1 -0
  57. package/dist/web/server.js +665 -0
  58. package/dist/web/server.js.map +1 -0
  59. package/package.json +2 -1
@@ -0,0 +1,638 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { createRequire } from 'node:module';
4
+ import { PAYTACA_LOGO, PAYTACA_AI_LOGO } from './assets.js';
5
+ const require = createRequire(new URL('.', import.meta.url).href);
6
+ const ALPINE_JS = fs.readFileSync(path.join(path.dirname(require.resolve('alpinejs/package.json')), 'dist', 'cdn.min.js'), 'utf8');
7
+ export function renderPage() {
8
+ return `<!DOCTYPE html>
9
+ <html lang="en">
10
+ <head>
11
+ <meta charset="UTF-8">
12
+ <meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
13
+ <title>Paytaca</title>
14
+ <link rel="icon" type="image/png" href="${PAYTACA_LOGO}">
15
+ <link rel="preconnect" href="https://fonts.googleapis.com">
16
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
17
+ <link href="https://fonts.googleapis.com/css2?family=Rubik:wght@300;400;500;600&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
18
+ <style>
19
+ *,*::before,*::after{margin:0;padding:0;box-sizing:border-box}
20
+ :root{
21
+ --sans:'Rubik',system-ui,-apple-system,'Segoe UI',sans-serif;
22
+ --mono:'JetBrains Mono',ui-monospace,'SF Mono','Cascadia Code',monospace;
23
+ --bg:#ecf3f3;
24
+ --surface:rgba(255,255,255,0.72);
25
+ --surface-2:#f0f4f5;
26
+ --surface-3:rgba(255,255,255,0.85);
27
+ --ink:#1c2833;
28
+ --ink-2:#4c4f4f;
29
+ --ink-3:#9e9e9e;
30
+ --line:rgba(0,0,0,0.08);
31
+ --line-2:rgba(0,0,0,0.05);
32
+ --accent:#3C64F6;
33
+ --accent-strong:#2b4fd8;
34
+ --accent-soft:rgba(60,100,246,0.12);
35
+ --ok:#28a745;
36
+ --ok-soft:rgba(40,167,69,0.12);
37
+ --warn:#d4a017;
38
+ --warn-soft:rgba(255,193,7,0.12);
39
+ --err:#e5484d;
40
+ --err-soft:rgba(229,72,77,0.1);
41
+ --r:16px;--r-sm:10px;
42
+ --shadow:0 2px 8px rgba(0,0,0,0.05);
43
+ --glass-hi:inset 0 1px 0 rgba(255,255,255,0.6);
44
+ --grad:linear-gradient(to right bottom,rgba(59,123,246,0.9),rgba(54,129,232,0.9),rgba(49,139,218,0.9),rgba(44,149,204,0.9),rgba(39,159,190,0.9));
45
+ --on-ink:#fff;
46
+ }
47
+ [data-theme="dark"]{
48
+ --bg:#273746;
49
+ --surface:rgba(28,40,51,0.7);
50
+ --surface-2:#1c2833;
51
+ --surface-3:rgba(39,55,70,0.85);
52
+ --ink:#eef2f5;
53
+ --ink-2:#c8c8c8;
54
+ --ink-3:#8a949e;
55
+ --line:rgba(255,255,255,0.1);
56
+ --line-2:rgba(255,255,255,0.07);
57
+ --accent:#5b82f8;
58
+ --accent-strong:#7ea0fa;
59
+ --accent-soft:rgba(91,130,248,0.16);
60
+ --ok:#4ade80;
61
+ --ok-soft:rgba(74,222,128,0.15);
62
+ --warn:#fbbf24;
63
+ --warn-soft:rgba(255,193,7,0.15);
64
+ --err:#f05253;
65
+ --err-soft:rgba(240,82,83,0.15);
66
+ --shadow:0 8px 32px rgba(0,0,0,0.35);
67
+ --glass-hi:inset 0 1px 0 rgba(255,255,255,0.1);
68
+ --on-ink:#eef2f5;
69
+ }
70
+ [x-cloak]{display:none!important}
71
+ body{font-family:var(--sans);background:var(--bg);color:var(--ink);font-size:14px;font-weight:300;line-height:1.55;min-height:100dvh;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility}
72
+ ::selection{background:var(--accent);color:#fff}
73
+ .mono-break{font-family:var(--mono);font-size:11.5px;word-break:break-all}
74
+ .shell{display:flex;align-items:stretch;min-height:100dvh}
75
+ .sidebar{position:sticky;top:0;flex:none;width:236px;height:100dvh;display:flex;flex-direction:column;background:var(--surface);backdrop-filter:blur(16px) saturate(180%);-webkit-backdrop-filter:blur(16px) saturate(180%);border-right:1px solid var(--line);z-index:40}
76
+ .brand{display:flex;align-items:center;gap:11px;padding:20px 18px;border-bottom:1px solid var(--line-2)}
77
+ .brand-logo{width:30px;height:auto;flex:none;border-radius:8px;display:block}
78
+ .brand-name{font-size:16px;font-weight:500;letter-spacing:-0.02em;color:var(--ink)}
79
+ .nav{flex:1;display:flex;flex-direction:column;gap:3px;padding:14px 12px}
80
+ .nav-label{font-family:var(--mono);font-size:9.5px;font-weight:500;letter-spacing:0.16em;text-transform:uppercase;color:var(--ink-3);padding:6px 10px 8px}
81
+ .nav-item{display:flex;align-items:center;gap:10px;padding:9px 10px;border-radius:var(--r-sm);font-size:13.5px;font-weight:400;color:var(--ink-2);cursor:pointer;user-select:none;transition:background .12s ease-out,color .12s ease-out}
82
+ .nav-item:hover{background:var(--surface-3);color:var(--ink)}
83
+ .nav-item.active{background:var(--accent);color:#fff;box-shadow:0 4px 20px rgba(31,38,135,0.18)}
84
+ .nav-item .idx{font-family:var(--mono);font-size:10px;color:var(--ink-3)}
85
+ .nav-item.active .idx{color:rgba(255,255,255,0.7)}
86
+ .nav-ico{width:20px;height:20px;flex:none;border-radius:5px;display:block}
87
+ .sidebar-foot{display:flex;flex-direction:column;gap:10px;padding:16px 18px;border-top:1px solid var(--line-2)}
88
+ .net{display:flex;align-items:center;gap:9px;font-family:var(--mono);font-size:10.5px;font-weight:500;letter-spacing:0.1em;color:var(--ink-2)}
89
+ .dot{width:7px;height:7px;flex:none;border-radius:50%;background:var(--accent);box-shadow:0 0 0 3px var(--accent-soft)}
90
+ .dot.chipnet{background:var(--warn);box-shadow:0 0 0 3px var(--warn-soft)}
91
+ .main{flex:1;min-width:0;display:flex;flex-direction:column}
92
+ .topbar{position:sticky;top:0;z-index:30;display:flex;align-items:center;justify-content:space-between;gap:16px;padding:15px 30px;background:var(--surface);backdrop-filter:blur(12px) saturate(1.4);-webkit-backdrop-filter:blur(12px) saturate(1.4);border-bottom:1px solid var(--line)}
93
+ .page-title{font-size:15px;font-weight:500;letter-spacing:-0.01em;color:var(--ink)}
94
+ .topbar-actions{display:flex;align-items:center;gap:10px}
95
+ .live-pill{display:flex;align-items:center;gap:7px;padding:6px 11px;border:1px solid var(--line);border-radius:999px;background:var(--surface-3);font-family:var(--mono);font-size:10px;font-weight:500;letter-spacing:0.12em;text-transform:uppercase;color:var(--ink-3)}
96
+ .live-pill .live-dot{width:6px;height:6px;border-radius:50%;background:var(--ink-3)}
97
+ .live-pill.ws-live{color:var(--ink-2)}
98
+ .live-pill.ws-live .live-dot{background:var(--accent);box-shadow:0 0 0 3px var(--accent-soft)}
99
+ .tx-toast{position:fixed;top:74px;right:30px;z-index:150;display:flex;flex-direction:column;gap:4px;min-width:240px;max-width:320px;padding:14px 16px;background:var(--surface-3);backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);border:1px solid var(--line);color:var(--ink);border-radius:var(--r);box-shadow:0 10px 32px rgba(0,0,0,0.28);animation:modalIn .2s cubic-bezier(.16,1,.3,1)}
100
+ .tx-toast .t{font-family:var(--mono);font-size:9.5px;font-weight:600;letter-spacing:0.14em;text-transform:uppercase;color:var(--ink-3)}
101
+ .tx-toast .a{font-size:17px;font-weight:500;letter-spacing:-0.02em;font-variant-numeric:tabular-nums}
102
+ .tx-toast a{font-family:var(--mono);font-size:10.5px;color:var(--accent);text-decoration:underline;text-underline-offset:2px}
103
+ .tx-toast a:hover{color:var(--accent-strong)}
104
+ .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}
106
+ .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}
108
+ .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
+ .card-head{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:18px}
110
+ .card-title{font-family:var(--mono);font-size:10.5px;font-weight:600;letter-spacing:0.13em;text-transform:uppercase;color:var(--ink-2)}
111
+ .action-bar{display:flex;gap:10px;flex-wrap:wrap;margin:18px 0 4px}
112
+ .action-btn{display:inline-flex;align-items:center;gap:8px;padding:10px 18px;border:1px solid var(--line);border-radius:999px;background:var(--surface);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);color:var(--ink-2);font-family:var(--sans);font-size:13px;font-weight:400;cursor:pointer;user-select:none;transition:all .25s cubic-bezier(0.4,0,0.2,1)}
113
+ .action-btn:hover{color:var(--ink);border-color:var(--ink-3);transform:translateY(-1px)}
114
+ .action-btn.active{background:var(--accent);border-color:var(--accent);color:#fff;box-shadow:0 4px 20px rgba(31,38,135,0.18)}
115
+ .action-btn svg{flex:none}
116
+ .btn-close{margin-left:auto;flex:none;width:26px;height:26px;display:inline-flex;align-items:center;justify-content:center;border:1px solid var(--line);border-radius:50%;background:transparent;color:var(--ink-3);font-size:12px;cursor:pointer;transition:color .12s,border-color .12s}
117
+ .btn-close:hover{color:var(--ink);border-color:var(--ink-3)}
118
+ .ai-head{display:flex;align-items:center;gap:14px;margin:6px 0 20px}
119
+ .ai-logo{width:44px;height:44px;border-radius:12px;flex:none;box-shadow:0 4px 20px rgba(31,38,135,0.12)}
120
+ .ai-title{font-size:18px;font-weight:500;letter-spacing:-0.01em;color:var(--ink)}
121
+ .ai-sub{font-family:var(--mono);font-size:10px;font-weight:500;letter-spacing:0.12em;text-transform:uppercase;color:var(--ink-3);margin-top:3px}
122
+ .balance-card{position:relative;overflow:hidden;background:var(--grad);border:1px solid rgba(255,255,255,0.2);color:#fff;box-shadow:0 8px 32px rgba(31,38,135,0.15);backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px)}
123
+ .balance-card .label{font-family:var(--mono);font-size:10px;font-weight:500;letter-spacing:0.16em;text-transform:uppercase;color:rgba(255,255,255,0.75);margin-bottom:14px}
124
+ .balance-card .value{display:flex;align-items:baseline;gap:9px;font-size:clamp(30px,3.4vw,42px);font-weight:600;letter-spacing:-0.035em;line-height:1;font-variant-numeric:tabular-nums}
125
+ .balance-card .value .unit{font-size:14px;font-weight:400;letter-spacing:0;color:rgba(255,255,255,0.75)}
126
+ .balance-card .sub{font-family:var(--mono);font-size:11.5px;color:rgba(255,255,255,0.75);margin-top:12px;min-height:18px}
127
+ .metric{padding:8px 0 2px}
128
+ .metric-value{font-size:clamp(30px,3vw,38px);font-weight:600;letter-spacing:-0.035em;line-height:1;color:var(--accent);font-variant-numeric:tabular-nums}
129
+ .metric-label{font-family:var(--mono);font-size:10.5px;letter-spacing:0.04em;color:var(--ink-3);margin-top:12px}
130
+ .refill-line{display:flex;align-items:center;gap:9px;margin-top:14px;padding-top:14px;border-top:1px solid var(--line-2);font-size:12px;color:var(--ink-3);flex-wrap:wrap}
131
+ .field{margin-bottom:14px}.field:last-child{margin-bottom:0}
132
+ .field-label{display:block;font-family:var(--mono);font-size:9.5px;font-weight:500;letter-spacing:0.12em;text-transform:uppercase;color:var(--ink-3);margin-bottom:7px}
133
+ .field-input{width:100%;padding:10px 12px;background:var(--surface-2);border:1px solid var(--line);border-radius:var(--r-sm);color:var(--ink);font-size:13.5px;font-family:var(--sans);outline:none;transition:border-color .12s ease-out,box-shadow .12s ease-out,background .12s ease-out}
134
+ .field-input::placeholder{color:var(--ink-3)}
135
+ .field-input:focus{border-color:var(--accent);background:var(--surface);box-shadow:0 0 0 3px var(--accent-soft)}
136
+ textarea.field-input{resize:vertical;min-height:84px;line-height:1.5}
137
+ select.field-input{cursor:pointer}
138
+ input[type="number"]{-moz-appearance:textfield}input::-webkit-outer-spin-button,input::-webkit-inner-spin-button{-webkit-appearance:none}
139
+ .field-hint{font-size:11px;color:var(--ink-3);margin-top:5px}
140
+ .field-error{font-family:var(--mono);font-size:11.5px;color:var(--err);margin-top:8px}
141
+ .field-success{font-family:var(--mono);font-size:11.5px;color:var(--ok);margin-top:8px}
142
+ .segmented{display:flex;gap:2px;padding:2px;background:var(--surface-2);border:1px solid var(--line);border-radius:var(--r-sm)}
143
+ .segmented-opt{flex:1;padding:7px 8px;text-align:center;font-size:12.5px;font-weight:400;color:var(--ink-3);border-radius:8px;cursor:pointer;user-select:none;transition:background .12s ease-out,color .12s ease-out}
144
+ .segmented-opt:hover{color:var(--ink-2)}
145
+ .segmented-opt.active{background:var(--surface-3);color:var(--ink);box-shadow:0 1px 2px rgba(0,0,0,0.08)}
146
+ .inline-row{display:flex;gap:8px}
147
+ .inline-row>*{flex:1;min-width:0}
148
+ .inline-row .shrink{flex:none}
149
+ .btn{display:inline-flex;align-items:center;justify-content:center;gap:7px;padding:10px 16px;border:1px solid var(--line);border-radius:var(--r-sm);background:var(--surface-3);color:var(--ink);font-family:var(--sans);font-size:13px;font-weight:400;cursor:pointer;white-space:nowrap;transition:background .12s ease-out,border-color .12s ease-out,transform .06s ease-out}
150
+ .btn:hover{border-color:var(--ink-3);background:var(--surface-2)}
151
+ .btn:active{transform:translateY(1px)}
152
+ .btn:disabled{opacity:.45;cursor:not-allowed;transform:none}
153
+ .btn-primary{background:var(--grad);border:1px solid rgba(255,255,255,0.2);color:#fff;box-shadow:0 4px 20px rgba(31,38,135,0.12);transition:all .3s cubic-bezier(0.4,0,0.2,1)}
154
+ .btn-primary:hover{transform:translateY(-2px);box-shadow:0 8px 24px rgba(31,38,135,0.18)}
155
+ .btn-primary:active{transform:translateY(1px)}
156
+ .btn-danger{background:transparent;border-color:var(--line);color:var(--err)}
157
+ .btn-danger:hover{border-color:var(--err);background:var(--err-soft)}
158
+ .btn-sm{padding:6px 11px;font-size:11.5px}
159
+ .btn-ghost{background:transparent;border-color:transparent;color:var(--ink-2)}
160
+ .btn-ghost:hover{background:var(--surface-2);border-color:transparent;color:var(--ink)}
161
+ .btn-group{display:flex;gap:8px;margin-top:16px}
162
+ .addr-display{font-family:var(--mono);font-size:12px;line-height:1.6;padding:12px;background:var(--surface-2);border:1px solid var(--line-2);border-radius:var(--r-sm);word-break:break-all}
163
+ .addr-row{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:7px}
164
+ .addr-label{font-family:var(--mono);font-size:9px;font-weight:500;letter-spacing:0.13em;text-transform:uppercase;color:var(--ink-3)}
165
+ .btn-copy{padding:3px 8px;border:1px solid var(--line);border-radius:3px;background:var(--surface);color:var(--ink-3);font-family:var(--mono);font-size:9.5px;font-weight:500;letter-spacing:0.06em;cursor:pointer;transition:border-color .12s,color .12s}
166
+ .btn-copy:hover{border-color:var(--accent);color:var(--accent)}
167
+ .qr-wrap{display:flex;justify-content:center;margin-top:16px}
168
+ .qr-wrap svg{width:100%;max-width:188px;height:auto;padding:9px;background:#fff;border:1px solid var(--line);border-radius:var(--r-sm)}
169
+ .tbl{width:100%;border-collapse:collapse;font-size:13px}
170
+ .tbl th{text-align:left;font-family:var(--mono);font-size:9.5px;font-weight:500;letter-spacing:0.12em;text-transform:uppercase;color:var(--ink-3);padding:0 10px 10px;border-bottom:1px solid var(--line)}
171
+ .tbl td{padding:11px 10px;border-bottom:1px solid var(--line-2);vertical-align:middle}
172
+ .tbl tbody tr:hover{background:var(--surface-2)}
173
+ .tbl tr:last-child td{border-bottom:none}
174
+ .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}
176
+ .hist-date a:hover{color:var(--accent-strong);border-bottom-color:var(--accent)}.hist-addr{max-width:210px}
177
+ .hist-sub{font-family:var(--mono);font-size:10px;color:var(--ink-3);margin-top:2px}
178
+ .hist-amt{font-family:var(--mono);font-size:12px;font-variant-numeric:tabular-nums}
179
+ .hist-in{color:var(--accent-strong)}
180
+ .hist-out{color:var(--ink)}
181
+ .pill{display:inline-flex;align-items:center;padding:3px 9px;border-radius:999px;font-family:var(--mono);font-size:9.5px;font-weight:600;letter-spacing:0.06em}
182
+ .pill-in,.pill-active{background:var(--accent-soft);color:var(--accent-strong)}
183
+ .pill-out{background:var(--err-soft);color:var(--err)}
184
+ .pill-idle{background:var(--surface-2);color:var(--ink-3)}
185
+ .pill-ok{background:rgba(40,167,69,0.12);color:#28a745}
186
+ .pill-fail{background:var(--err-soft);color:var(--err)}
187
+ .pill-wait{background:rgba(255,193,7,0.12);color:#d4a017}
188
+ [data-theme="dark"] .pill-ok{background:rgba(40,167,69,0.18);color:#4ade80}
189
+ [data-theme="dark"] .pill-wait{background:rgba(255,193,7,0.18);color:#fbbf24}
190
+ .pagination{display:flex;align-items:center;justify-content:center;gap:14px;margin-top:18px}
191
+ .pagination .page-info{font-family:var(--mono);font-size:11px;color:var(--ink-3)}
192
+ .empty{padding:30px 12px;text-align:center;font-family:var(--mono);font-size:11.5px;letter-spacing:0.02em;color:var(--ink-3)}
193
+ .empty::before{content:'[ '}
194
+ .empty::after{content:' ]'}
195
+ .token-item{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:11px 0;border-bottom:1px solid var(--line-2)}
196
+ .token-item:last-child{border-bottom:none}
197
+ .token-name{font-size:13.5px;font-weight:600}
198
+ .token-balance{font-family:var(--mono);font-size:12.5px;color:var(--ink-2);font-variant-numeric:tabular-nums}
199
+ .plan-group{padding:16px 0;border-bottom:1px solid var(--line)}
200
+ .plan-group:first-child{padding-top:2px}
201
+ .plan-group:last-child{border-bottom:0;padding-bottom:2px}
202
+ .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}
206
+ .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
+ .plan-price{font-size:13px;font-weight:600}
208
+ .seg{display:inline-flex;border:1px solid var(--line);border-radius:var(--r-sm);overflow:hidden}
209
+ .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
+ .seg button+.seg button{border-left:1px solid var(--line)}
211
+ .seg button.active{background:var(--accent);color:#fff}
212
+ .seg button .off{margin-left:6px;color:var(--accent)}
213
+ .seg button.active .off{color:rgba(255,255,255,0.75)}
214
+ .gallery{display:grid;grid-template-columns:repeat(auto-fill,minmax(170px,1fr));gap:16px}
215
+ .gallery-item{margin:0;cursor:pointer}
216
+ .gallery-item img{display:block;width:100%;aspect-ratio:1;object-fit:cover;border:1px solid var(--line);border-radius:var(--r-sm);transition:border-color .15s ease,opacity .15s ease;background:var(--surface-2)}
217
+ .gallery-item:hover img{border-color:var(--accent)}
218
+ .gallery-item figcaption{display:flex;justify-content:space-between;gap:8px;margin-top:7px;font-family:var(--mono);font-size:10px;color:var(--ink-3)}
219
+ .gallery-item .cap{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
220
+ .lightbox{display:none;position:fixed;inset:0;z-index:120;align-items:center;justify-content:center;padding:24px;background:rgba(15,25,35,0.7);backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px)}
221
+ .lightbox.open{display:flex}
222
+ .lightbox-inner{width:100%;max-width:880px;animation:modalIn .2s cubic-bezier(.16,1,.3,1)}
223
+ .lightbox img{display:block;max-width:100%;max-height:74vh;margin:0 auto;border:1px solid rgba(255,255,255,0.14);border-radius:var(--r);background:var(--surface-3)}
224
+ .lightbox-bar{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-top:12px}
225
+ .lightbox-path{font-family:var(--mono);font-size:10.5px;color:rgba(255,255,255,0.75);opacity:.9;word-break:break-all}
226
+ .lightbox-actions{display:flex;gap:8px;flex-shrink:0}
227
+ .lightbox .btn{background:rgba(255,255,255,0.1);border-color:rgba(255,255,255,0.18);color:#fff}
228
+ .lightbox .btn:hover{background:rgba(255,255,255,0.18)}
229
+ .quote-box{margin-top:14px;padding:14px;background:var(--surface-2);border:1px solid var(--line);border-radius:var(--r-sm);font-family:var(--mono);font-size:12px;line-height:1.9;white-space:pre-line}
230
+ .warning-bar{display:flex;align-items:center;gap:8px;padding:9px 12px;margin-bottom:14px;background:var(--warn-soft);color:var(--warn);border:1px solid color-mix(in oklch,var(--warn) 28%,transparent);border-radius:var(--r-sm);font-size:12px;font-weight:500}
231
+ .kv{display:grid;grid-template-columns:auto 1fr;gap:7px 18px;font-size:12.5px}
232
+ .kv dt{font-family:var(--mono);font-size:9.5px;font-weight:500;letter-spacing:0.1em;text-transform:uppercase;color:var(--ink-3);align-self:center}
233
+ .kv dd{font-weight:500;color:var(--ink)}
234
+ .modal-backdrop{display:none;position:fixed;inset:0;z-index:100;align-items:center;justify-content:center;padding:20px;background:rgba(15,25,35,0.45);backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px)}
235
+ .modal-backdrop.open{display:flex}
236
+ .modal{width:100%;max-width:440px;padding:24px;background:var(--surface-3);backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);border:1px solid var(--line);border-radius:var(--r);box-shadow:0 24px 60px -14px rgba(0,0,0,0.3),var(--glass-hi);animation:modalIn .2s cubic-bezier(.16,1,.3,1)}
237
+ .modal-title{font-family:var(--mono);font-size:12px;font-weight:600;letter-spacing:0.1em;text-transform:uppercase;margin-bottom:16px}
238
+ .modal-body{font-size:13.5px;line-height:1.75;color:var(--ink-2)}
239
+ .modal-body strong{color:var(--ink);font-weight:600}
240
+ .modal-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:22px}
241
+ @keyframes modalIn{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}
242
+ .toast{position:fixed;bottom:24px;left:50%;transform:translateX(-50%);z-index:200;display:flex;align-items:center;padding:10px 16px;border-radius:var(--r-sm);font-family:var(--mono);font-size:12px;font-weight:500;box-shadow:0 10px 30px -10px oklch(20% 0.02 258 / 0.35);animation:toastIn .2s cubic-bezier(.16,1,.3,1)}
243
+ .toast-ok{background:var(--ink);color:var(--bg)}
244
+ .toast-err{background:var(--err);color:#fff}
245
+ @keyframes toastIn{from{opacity:0;transform:translateX(-50%) translateY(10px)}to{opacity:1;transform:translateX(-50%) translateY(0)}}
246
+ :focus-visible{outline:2px solid var(--accent);outline-offset:2px;border-radius:2px}
247
+ @media(max-width:900px){
248
+ .shell{flex-direction:column}
249
+ .sidebar{position:static;width:auto;height:auto;flex-direction:row;align-items:center;gap:12px;padding:11px 14px;border-right:none;border-bottom:1px solid var(--line)}
250
+ .brand{padding:0;border-bottom:none;gap:8px}
251
+ .brand-logo{width:24px}
252
+ .brand-name{display:none}
253
+ .nav{flex-direction:row;gap:3px;padding:0;flex:1;justify-content:center}
254
+ .nav-label{display:none}
255
+ .nav-item{padding:7px 11px;font-size:12.5px}
256
+ .nav-item .idx{display:none}
257
+ .sidebar-foot{padding:0;border-top:none;flex-direction:row;gap:8px}
258
+ .topbar{padding:12px 16px}
259
+ .content{padding:18px 16px 44px}
260
+ .grid{gap:12px}
261
+ .span-4,.span-5,.span-6,.span-7,.span-8{grid-column:span 12}
262
+ .action-btn{padding:9px 14px;font-size:12.5px}
263
+ }
264
+ @media(max-width:560px){
265
+ .inline-row{flex-direction:column}
266
+ .balance-card .value{font-size:30px}
267
+ .action-bar{gap:8px}
268
+ }
269
+ @media(prefers-reduced-motion:reduce){*,*::before,*::after{animation-duration:.01ms!important;transition-duration:.01ms!important}}
270
+ </style>
271
+ </head>
272
+ <body>
273
+ <div class="shell" x-data="app()" x-init="init()" x-cloak>
274
+ <aside class="sidebar">
275
+ <div class="brand">
276
+ <img class="brand-logo" src="${PAYTACA_LOGO}" alt="Paytaca">
277
+ <div class="brand-name">Paytaca</div>
278
+ </div>
279
+ <nav class="nav">
280
+ <div class="nav-label">Navigate</div>
281
+ <div class="nav-item" :class="{active: tab==='wallet'}" @click="tab='wallet'"><svg class="nav-ico" viewBox="0 0 24 24" fill="currentColor"><path d="M21 18v1c0 1.1-.9 2-2 2H5c-1.11 0-2-.9-2-2V5c0-1.1.89-2 2-2h14c1.1 0 2 .9 2 2v1h-9c-1.11 0-2 .9-2 2v8c0 1.1.89 2 2 2h9zm-9-2h10V8H12v8zm4-2.5c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5z"/></svg><span class="idx">01</span>Wallet</div>
282
+ <div class="nav-item" :class="{active: tab==='ai'}" @click="tab='ai'"><img class="nav-ico" src="${PAYTACA_AI_LOGO}" alt=""><span class="idx">02</span>AI</div>
283
+ </nav>
284
+ <div class="sidebar-foot">
285
+ <div class="net"><span class="dot" :class="{chipnet: state?.network==='chipnet'}"></span><span x-text="state?.network==='chipnet' ? 'CHIPNET' : 'MAINNET'">MAINNET</span></div>
286
+ <button class="btn btn-ghost btn-sm" style="width:100%;justify-content:flex-start;font-size:12px" @click="toggleTheme()">
287
+ <span x-text="theme==='dark' ? '☀' : '☾'" style="font-size:14px"></span>
288
+ <span x-text="theme==='dark' ? 'Light Mode' : 'Dark Mode'"></span>
289
+ </button>
290
+ </div>
291
+ </aside>
292
+ <div class="main">
293
+ <header class="topbar">
294
+ <div class="page-title"><span x-text="tab==='wallet' ? 'Wallet' : 'Paytaca AI'"></span></div>
295
+ <div class="topbar-actions">
296
+ <div class="live-pill" :class="wsStatus==='live' ? 'ws-live' : ''" :title="'Watchtower live feed: ' + wsStatus">
297
+ <span class="live-dot"></span>
298
+ <span x-text="wsStatus==='live' ? 'Live' : wsStatus==='connecting' ? 'Sync' : 'Off'"></span>
299
+ </div>
300
+ <button class="btn btn-sm" @click="load()" title="Refresh">
301
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.5 2v6h-6"/><path d="M21.34 15.57a10 10 0 1 1-.57-8.38"/></svg>
302
+ Refresh
303
+ </button>
304
+ </div>
305
+ </header>
306
+ <main class="content">
307
+ <div x-show="tab==='wallet'">
308
+ <div class="grid">
309
+ <div class="span-12">
310
+ <div class="card balance-card">
311
+ <div class="label">Available Balance</div>
312
+ <div class="value"><span x-text="state ? state.balance.spendableBch : '—'"></span><span class="unit">BCH</span></div>
313
+ <div class="sub" x-text="state?.balance?.usd ? '$' + Number(state.balance.usd).toFixed(2) + ' USD' : ''"></div>
314
+ </div>
315
+ </div>
316
+ </div>
317
+ <div class="action-bar">
318
+ <button class="action-btn" :class="{active: walletAction==='send'}" @click="walletAction = walletAction==='send' ? null : 'send'"><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="M7 17 17 7"/><path d="M8 7h9v9"/></svg>Send</button>
319
+ <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
+ <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
+ </div>
322
+ <div class="grid" x-show="walletAction">
323
+ <div class="span-12">
324
+ <div class="card">
325
+ <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>
326
+ <template x-if="walletAction==='receive'"><div>
327
+ <div class="segmented" style="margin-bottom:14px">
328
+ <div class="segmented-opt" :class="{active: rcvType==='bch'}" @click="rcvType='bch'; onRcvChange()">BCH</div>
329
+ <div class="segmented-opt" :class="{active: rcvType==='token'}" @click="rcvType='token'; onRcvChange()">CashToken</div>
330
+ </div>
331
+ <div class="inline-row">
332
+ <div style="flex:1;min-width:0">
333
+ <div x-show="rcvType==='token'" class="field"><label class="field-label">Token Category ID</label><input class="field-input" type="text" x-model="rcvCategory" @input="onRcvChange()" placeholder="64-character hex category ID"></div>
334
+ <div class="field"><label class="field-label">Amount (optional)</label><input class="field-input" type="number" x-model="rcvAmount" @input="onRcvChange()" placeholder="0.0" step="any" min="0"></div>
335
+ <div class="addr-display"><div class="addr-row"><span class="addr-label">Address</span><button class="btn-copy" @click="copyText(rcvView?.address)">COPY</button></div><div x-text="rcvView?.address || 'Loading…'"></div></div>
336
+ <template x-if="rcvView?.paymentUri"><div class="addr-display" style="margin-top:8px"><div class="addr-row"><span class="addr-label">Payment URI</span><button class="btn-copy" @click="copyText(rcvView?.paymentUri)">COPY</button></div><div style="font-size:10.5px;color:var(--ink-3)" x-text="rcvView?.paymentUri"></div></div></template>
337
+ </div>
338
+ <div class="qr-wrap" style="flex:none;width:220px;margin-top:0" x-html="rcvQrSvg"></div>
339
+ </div>
340
+ </div></template>
341
+ <template x-if="walletAction==='send'"><div>
342
+ <div class="segmented" style="margin-bottom:14px">
343
+ <div class="segmented-opt" :class="{active: sendType==='bch'}" @click="sendType='bch'; sendError=''; sendSuccess=''">BCH</div>
344
+ <div class="segmented-opt" :class="{active: sendType==='token'}" @click="sendType='token'; sendError=''; sendSuccess=''">CashToken</div>
345
+ </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>
354
+ <div class="field"><label class="field-label">Recipient Address</label><input class="field-input" type="text" x-model="sendAddress" placeholder="bitcoincash:q..."></div>
355
+ <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
+ <div x-show="sendError" class="field-error" x-text="sendError"></div>
357
+ <div x-show="sendSuccess" class="field-success" x-text="sendSuccess"></div>
358
+ <button class="btn btn-primary" style="margin-top:14px;min-width:180px" @click="doSend()">Send</button>
359
+ </div></template>
360
+ <template x-if="walletAction==='swap'"><div>
361
+ <template x-if="state?.network === 'chipnet'"><div class="warning-bar">Cauldron swaps are only available on mainnet.</div></template>
362
+ <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
+ <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
+ <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
+ <div class="segmented-opt" :class="{active: swapDir==='buy'}" @click="swapDir='buy'; swapQuoteText=''; pendingSwap=null">Buy Token ← BCH</div>
367
+ </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>
369
+ <div x-show="swapError" class="field-error" x-text="swapError"></div>
370
+ <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
+ <template x-if="swapQuoteText"><div style="margin-top:16px;padding-top:16px;border-top:1px solid var(--line-2)">
372
+ <div class="quote-box" style="margin-top:0" x-text="swapQuoteText"></div>
373
+ <div class="btn-group"><button class="btn" @click="swapQuoteText=''; pendingSwap=null">Cancel</button><button class="btn btn-primary" @click="doSwapExecute()" :disabled="swapExecBusy" x-text="swapExecBusy ? 'Executing…' : 'Confirm Swap'"></button></div>
374
+ </div></template>
375
+ </div></template>
376
+ </div>
377
+ </div>
378
+ </div>
379
+ <div class="grid" style="margin-top:20px">
380
+ <div class="span-4">
381
+ <div class="card">
382
+ <div class="card-head"><div class="card-title">Tokens</div></div>
383
+ <template x-if="!state?.tokens?.length"><div class="empty" x-text="state ? 'No tokens yet' : 'Loading…'"></div></template>
384
+ <template x-for="t in state?.tokens || []" :key="t.category"><div class="token-item"><div class="token-name" x-text="t.symbol || t.name || 'Unknown'"></div><div class="token-balance" x-text="t.displayBalance"></div></div></template>
385
+ </div>
386
+ </div>
387
+ <div class="span-8">
388
+ <div class="card">
389
+ <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
+ <template x-if="!histRecords.length && histLoading"><div class="empty">Loading…</div></template>
391
+ <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>
394
+ </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
+ </div>
396
+ </div>
397
+ </div>
398
+ </div>
399
+ <div x-show="tab==='ai'">
400
+ <div class="ai-head">
401
+ <img class="ai-logo" src="${PAYTACA_AI_LOGO}" alt="">
402
+ <div><div class="ai-title">Paytaca AI</div><div class="ai-sub">Chat credits · Image generation</div></div>
403
+ </div>
404
+ <div class="grid">
405
+ <div class="span-4">
406
+ <div class="card">
407
+ <div class="card-head"><div class="card-title">Credits</div></div>
408
+ <template x-if="state?.usage?.length"><div class="metric"><div class="metric-value" x-text="activeCreditsTotal(state) > 0 ? fmtDuration(activeCreditsTotal(state)) : '0m'"></div><div class="metric-label">remaining · <span x-text="activeCreditsCount(state)"></span> active session(s)</div></div></template>
409
+ <template x-if="state && !state?.usage?.length"><div class="empty">No active sessions</div></template>
410
+ <template x-if="!state"><div class="empty">Loading…</div></template>
411
+ <div class="refill-line" x-show="state?.autoRefill"><svg width="12" height="12" 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><span x-text="'Auto-refill ' + (state.autoRefill?.enabled ? 'ON' : 'PAUSED') + ' · ' + state.autoRefill?.minutes + 'm top-up'"></span></div>
412
+ </div>
413
+ </div>
414
+ <div class="span-8">
415
+ <div class="card">
416
+ <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>
418
+ <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>
420
+ <template x-if="state && !state?.usage?.length"><div class="empty">No usage data</div></template>
421
+ <template x-if="!state"><div class="empty">Loading…</div></template>
422
+ </div>
423
+ </div>
424
+ </div>
425
+ <div class="action-bar">
426
+ <button class="action-btn" :class="{active: aiAction==='buy'}" @click="aiAction = aiAction==='buy' ? null : 'buy'"><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="M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4Z"/><path d="M3 6h18"/><path d="M16 10a4 4 0 0 1-8 0"/></svg>Buy Credits</button>
427
+ <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
+ <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
+ </div>
430
+ <div class="grid" x-show="aiAction==='buy' || aiAction==='refill'">
431
+ <div class="span-12">
432
+ <div class="card">
433
+ <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>
434
+ <template x-if="aiAction==='buy'"><div>
435
+ <span class="pill pill-idle" x-show="state?.liftDiscountPercent" x-text="state.liftDiscountPercent + '% OFF WITH LIFT'"></span>
436
+ <template x-if="state?.plans?.length"><div>
437
+ <template x-for="plan in state?.plans || []" :key="plan.modelId">
438
+ <div class="plan-group">
439
+ <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>
448
+ </div>
449
+ </div>
450
+ </template>
451
+ </div></template>
452
+ <template x-if="state && !state?.plans?.length"><div class="empty">No plans available</div></template>
453
+ <template x-if="!state"><div class="empty">Loading…</div></template>
454
+ </div></template>
455
+ <template x-if="aiAction==='refill'"><div>
456
+ <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>
471
+ </div>
472
+ </div></template>
473
+ <template x-if="state?.autoRefill && !refillEditing"><div>
474
+ <div style="display:flex;align-items:flex-start;justify-content:space-between;gap:12px">
475
+ <dl class="kv">
476
+ <dt>Model</dt><dd x-text="state.autoRefill.model"></dd>
477
+ <dt>Every</dt><dd><span x-text="state.autoRefill.minutes"></span> min</dd>
478
+ <dt>Budget</dt><dd x-text="state.autoRefill.maxMinutes ? (state.autoRefill.remainingMinutes || 0) + ' of ' + state.autoRefill.maxMinutes + ' min left' : (state.autoRefill.remainingMinutes || 0) + ' min left (no cap)'"></dd>
479
+ <dt>Refills</dt><dd x-text="state.autoRefill.refillCount || 0"></dd>
480
+ </dl>
481
+ <span class="pill" :class="state.autoRefill.enabled ? 'pill-active' : ''" x-text="state.autoRefill.enabled ? 'ACTIVE' : 'PAUSED'"></span>
482
+ </div>
483
+ <template x-if="state.autoRefill.lastEvent"><div style="margin-top:12px;font-size:12px;color:var(--ink-3)" x-text="'Last: ' + state.autoRefill.lastEvent.action + '/' + state.autoRefill.lastEvent.status + (state.autoRefill.lastEvent.reason ? ' — ' + state.autoRefill.lastEvent.reason : '')"></div></template>
484
+ <template x-if="refillNotice"><div style="margin-top:8px;font-size:12.5px;color:var(--ink-3)" x-text="refillNotice"></div></template>
485
+ <template x-if="!state.autoRefill.enabled && state.autoRefill.maxMinutes && (state.autoRefill.remainingMinutes || 0) < (state.autoRefill.minutes || 0)"><div style="margin-top:8px;font-size:12.5px;color:var(--ink-3)">Budget exhausted — Edit to raise the cap before resuming.</div></template>
486
+ <div style="margin-top:18px;display:flex;gap:8px">
487
+ <button class="btn btn-sm btn-primary" x-show="!state.autoRefill.enabled" @click="resumeRefill()">Resume</button>
488
+ <button class="btn btn-sm btn-danger" x-show="state.autoRefill.enabled" @click="pauseRefill()">Pause</button>
489
+ <button class="btn btn-sm" @click="startRefillEdit()">Edit</button>
490
+ <button class="btn btn-sm" @click="deleteRefill()">Delete</button>
491
+ </div>
492
+ </div></template>
493
+ <template x-if="!state"><div class="empty">Loading…</div></template>
494
+ </div></template>
495
+ </div>
496
+ </div>
497
+ </div>
498
+ <div x-show="aiAction==='images'" class="grid">
499
+ <div class="span-5">
500
+ <div class="card">
501
+ <div class="card-head"><div class="card-title">Image Generation</div></div>
502
+ <div class="field"><label class="field-label">Prompt</label><textarea class="field-input" x-model="imgPrompt" placeholder="Describe the image you want to create..."></textarea></div>
503
+ <template x-if="state?.imageModels?.length"><div class="field"><label class="field-label">Model</label><select class="field-input" x-model="imgModel"><template x-for="m in state?.imageModels || []" :key="m.id"><option :value="m.id" x-text="m.name || m.id"></option></template></select></div></template>
504
+ <div class="inline-row"><div class="field"><label class="field-label">Aspect Ratio</label><select class="field-input" x-model="imgAspect"><option value="1:1">1:1</option><option value="16:9">16:9</option><option value="9:16">9:16</option><option value="4:3">4:3</option><option value="3:4">3:4</option></select></div><div class="field"><label class="field-label">Quality</label><select class="field-input" x-model="imgQuality"><option value="auto">Auto</option><option value="low">Low</option><option value="medium">Medium</option><option value="high">High</option><option value="xhigh">Extra High</option><option value="max">Max</option></select></div></div>
505
+ <button class="btn btn-primary" style="margin-top:14px;width:100%" @click="doImageQuote()" :disabled="imgQuoteBusy" x-text="imgQuoteBusy ? 'Quoting…' : 'Get Quote'"></button>
506
+ </div>
507
+ <div class="card" style="margin-top:14px">
508
+ <div class="card-head"><div class="card-title">Order History</div></div>
509
+ <template x-for="h in (state?.imageHistory || []).slice(0,10)" :key="h.id">
510
+ <div style="display:flex;align-items:center;gap:10px;padding:10px 0;border-bottom:1px solid var(--line-2)">
511
+ <span class="pill" :class="{'pill-ok':h.status==='completed','pill-fail':h.status==='failed','pill-wait':h.status==='processing'||h.status==='pending','pill-idle':!h.status||(!['completed','failed','processing','pending'].includes(h.status))}" x-text="(h.status||'unknown').toUpperCase()"></span>
512
+ <div style="flex:1;min-width:0">
513
+ <div style="font-size:12px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" x-text="h.model_display_name || h.model || 'Unknown'"></div>
514
+ <div style="font-size:11px;color:var(--ink-3);overflow:hidden;text-overflow:ellipsis;white-space:nowrap" x-text="h.prompt || 'No prompt'"></div>
515
+ </div>
516
+ <div style="text-align:right;flex-shrink:0;font-family:var(--mono);font-size:10.5px;color:var(--ink-3)">
517
+ <div x-text="(h.created_at || '').slice(0,10)"></div>
518
+ <div x-show="h.price_usd != null || h.actual_cost_usd != null" x-text="'$' + (h.price_usd != null ? h.price_usd : h.actual_cost_usd).toFixed(2)"></div>
519
+ </div>
520
+ <template x-if="h.status==='completed' && h.filepath"><button class="btn btn-sm btn-ghost" style="flex-shrink:0;padding:4px 6px" @click.stop="openImage(h)" title="View image"><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></button></template>
521
+ </div>
522
+ </template>
523
+ <template x-if="state && !(state?.imageHistory || []).length"><div class="empty">No orders yet</div></template>
524
+ </div>
525
+ </div>
526
+ <div class="span-7">
527
+ <div class="card">
528
+ <div class="card-head"><div class="card-title">Gallery</div><span class="pill pill-idle" x-show="(state?.imageHistory || []).filter(h => h.status === 'completed' && h.filepath).length" x-text="(state?.imageHistory || []).filter(h => h.status === 'completed' && h.filepath).length + ' SAVED'"></span></div>
529
+ <template x-if="(state?.imageHistory || []).filter(h => h.status === 'completed' && h.filepath).length"><div class="gallery">
530
+ <template x-for="h in (state?.imageHistory || []).filter(h => h.status === 'completed' && h.filepath)" :key="h.id">
531
+ <figure class="gallery-item" @click="openImage(h)">
532
+ <img :src="imgUrls[h.id] || ''" :alt="h.prompt || 'Generated image'" loading="lazy" x-show="imgUrls[h.id]">
533
+ <figcaption><span class="cap" x-text="imgCaption(h)"></span><span x-text="(h.completed_at || '').slice(0,10)"></span></figcaption>
534
+ </figure>
535
+ </template>
536
+ </div></template>
537
+ <template x-if="state && !(state?.imageHistory || []).some(h => h.status === 'completed' && h.filepath)"><div class="empty">No images yet — generate your first one</div></template>
538
+ <template x-if="!state"><div class="empty">Loading…</div></template>
539
+ <div style="font-size:10.5px;color:var(--ink-3);font-family:var(--mono);margin-top:16px" x-show="state">files saved in ~/.paytaca/images · click an image to view</div>
540
+ </div>
541
+ </div>
542
+ </div>
543
+ </div>
544
+ </main>
545
+ </div>
546
+ <div class="modal-backdrop" :class="{open: showPurchaseModal}" @click.self="showPurchaseModal=false"><div class="modal" role="dialog" aria-modal="true"><div class="modal-title">Confirm Purchase</div><div class="modal-body"><strong x-text="pendingPurchase?.displayName"></strong><br>Duration: <span x-text="pendingPurchase?.durationDisplay"></span></div><div class="seg" style="margin-top:16px" role="tablist"><button :class="{active: purchaseMethod==='bch'}" @click="purchaseMethod='bch'">BCH</button><button :class="{active: purchaseMethod==='lift'}" @click="purchaseMethod='lift'; loadLiftQuote()">LIFT<span class="off" x-show="state?.liftDiscountPercent" x-text="'-' + state.liftDiscountPercent + '%'"></span></button></div><div class="modal-body" style="margin-top:14px"><template x-if="purchaseMethod==='bch'"><div>Price: <strong x-text="bchPriceLabel(pendingPurchase)"></strong><br><span style="font-size:11.5px;color:var(--ink-3)" x-text="state?.balance ? 'BCH balance: ' + state.balance.spendableBch + ' BCH' : 'BCH balance unavailable'"></span><template x-if="bchInsufficient()"><div style="font-size:11.5px;color:var(--err);margin-top:4px">Insufficient BCH balance — top up or pay with LIFT</div></template></div></template><template x-if="purchaseMethod==='lift'"><div><s style="color:var(--ink-3);font-size:12px" x-text="bchPriceLabel(pendingPurchase)"></s> <strong x-text="liftPriceLabel(pendingPurchase)"></strong> <span class="pill pill-active" x-show="state?.liftDiscountPercent" x-text="state.liftDiscountPercent + '% OFF'"></span><br><span style="font-size:11.5px;color:var(--ink-3)" x-text="state?.lift ? 'LIFT balance: ' + state.lift.displayBalance : 'No LIFT tokens in this wallet'"></span><template x-if="liftQuoteBusy"><div style="font-size:11.5px;color:var(--ink-3);margin-top:6px">Estimating LIFT needed…</div></template><template x-if="liftQuoteError"><div style="font-size:11.5px;color:var(--err);margin-top:6px" x-text="liftQuoteError"></div></template><template x-if="liftQuote && !liftQuoteBusy && !liftQuoteError"><div style="font-size:11.5px;margin-top:6px">LIFT needed: <strong x-text="'≈ ' + liftQuote.display + ' ' + liftQuote.symbol"></strong></div></template><template x-if="liftInsufficient()"><div style="font-size:11.5px;color:var(--err);margin-top:4px">Insufficient LIFT balance — pay with BCH or top up LIFT</div></template></div></template></div><template x-if="purchaseError"><div style="font-size:12px;color:var(--err);margin-top:10px" x-text="purchaseError"></div></template><div class="modal-actions"><button class="btn" @click="showPurchaseModal=false">Cancel</button><button class="btn btn-primary" @click="confirmPurchase()" :disabled="purchaseBusy || (purchaseMethod==='bch' && bchInsufficient()) || (purchaseMethod==='lift' && (!state?.lift || liftQuoteBusy || liftInsufficient()))" x-text="purchaseBusy ? 'Processing…' : (purchaseMethod==='lift' ? 'Pay with LIFT' : 'Pay with BCH')"></button></div></div></div>
547
+ <div class="modal-backdrop" :class="{open: showSendModal}" @click.self="showSendModal=false"><div class="modal" role="dialog" aria-modal="true"><div class="modal-title">Confirm Send</div><template x-if="pendingSend?.type === 'bch'"><div class="modal-body"><strong>Send BCH</strong><br>Address: <span class="mono-break" x-text="pendingSend?.address"></span><br>Amount: <strong><span x-text="pendingSend?.amount"></span> <span x-text="pendingSend?.currency?.toUpperCase()"></span></strong></div></template><template x-if="pendingSend?.type === 'token'"><div class="modal-body"><strong>Send Token</strong><br>Category: <span class="mono-break" x-text="pendingSend?.category?.slice(0,16) + '…'"></span><br>Amount: <strong x-text="pendingSend?.tokenAmount"></strong><br>Address: <span class="mono-break" x-text="pendingSend?.address"></span></div></template><div class="modal-actions"><button class="btn" @click="showSendModal=false">Cancel</button><button class="btn btn-primary" @click="confirmSend()" :disabled="sendConfirmBusy" x-text="sendConfirmBusy ? 'Sending…' : 'Confirm & Send'"></button></div></div></div>
548
+ <div class="modal-backdrop" :class="{open: showImageQuoteModal}" @click.self="showImageQuoteModal=false"><div class="modal" role="dialog" aria-modal="true"><div class="modal-title">Image Generation</div><div class="modal-body">Model: <strong x-text="pendingImageQuote?.model || 'default'"></strong><br>Cost: <span class="tbl-num" x-text="pendingImageQuote?.amountSats ? (pendingImageQuote.amountSats / 1e8).toFixed(8) + ' BCH' : 'calculating...'"></span><span x-show="pendingImageQuote?.amountUsd != null" x-text="pendingImageQuote?.amountUsd != null ? ' · $' + pendingImageQuote.amountUsd.toFixed(2) + ' USD' : ''"></span><br>Order: <span class="mono-break" style="color:var(--ink-3)" x-text="pendingImageQuote?.orderId || 'pending'"></span></div><div class="modal-actions"><button class="btn" @click="showImageQuoteModal=false">Cancel</button><button class="btn btn-primary" @click="confirmImageGen()" :disabled="imgGenBusy" x-text="imgGenBusy ? 'Generating…' : 'Pay & Generate'"></button></div></div></div>
549
+ <div class="lightbox" :class="{open: lightbox}" @click.self="lightbox=null" @keydown.escape.window="lightbox=null" role="dialog" aria-modal="true">
550
+ <div class="lightbox-inner">
551
+ <img :src="lightbox && imgUrls[lightbox.id] ? imgUrls[lightbox.id] : ''" :alt="lightbox?.path || 'Generated image'" x-show="lightbox && imgUrls[lightbox.id]">
552
+ <div class="lightbox-bar">
553
+ <span class="lightbox-path" x-text="lightbox?.path || lightbox?.id || ''"></span>
554
+ <div class="lightbox-actions">
555
+ <button class="btn btn-sm" @click="copyText(lightbox?.path)">Copy Path</button>
556
+ <a class="btn btn-sm" :href="lightbox && imgUrls[lightbox.id] ? imgUrls[lightbox.id] : '#'" target="_blank" rel="noopener" x-show="lightbox && imgUrls[lightbox.id]">Open ↗</a>
557
+ <template x-if="!lightboxConfirmDelete"><button class="btn btn-sm" style="color:var(--err)" @click="lightboxConfirmDelete=true">Delete</button></template>
558
+ <template x-if="lightboxConfirmDelete"><span style="display:flex;gap:6px;align-items:center;font-size:11.5px;color:var(--err)">Delete?<button class="btn btn-sm btn-primary" style="background:var(--err);border-color:var(--err)" @click="deleteImage()">Yes</button><button class="btn btn-sm" @click="lightboxConfirmDelete=false">No</button></span></template>
559
+ <button class="btn btn-sm" @click="lightboxConfirmDelete=false;lightbox=null">Close</button>
560
+ </div>
561
+ </div>
562
+ </div>
563
+ </div>
564
+ <template x-if="toastVisible"><div class="toast" :class="toastOk ? 'toast-ok' : 'toast-err'" x-text="toastMsg"></div></template>
565
+ <template x-if="notify">
566
+ <div class="tx-toast">
567
+ <div class="t" x-text="notify.title"></div>
568
+ <div class="a" x-text="notify.amountText"></div>
569
+ <a :href="notify.explorer" target="_blank" rel="noopener" x-text="notify.txidShort"></a>
570
+ </div>
571
+ </template>
572
+ </div>
573
+ <script>
574
+ var QR=(()=>{var EC_PARAMS=[[],[26,7,1],[44,10,1],[70,15,1],[100,20,1],[134,26,1],[172,18,2],[196,20,2],[242,24,2],[292,30,2],[346,18,4]];var ALIGN_POS=[[],[],[6,18],[6,22],[6,26],[6,30],[6,34],[6,22,38],[6,24,42],[6,26,46],[6,28,50]];var BYTE_CAP=[0,17,32,53,78,106,134,154,192,230,271];var EXP=new Uint8Array(512),LOG=new Uint8Array(256);var x=1,i,j;for(i=0;i<255;i++){EXP[i]=x;LOG[x]=i;x=(x<<1)^(x&128?0x11D:0)}for(i=255;i<512;i++)EXP[i]=EXP[i-255];function gfMul(a,b){return a&&b?EXP[LOG[a]+LOG[b]]:0}function rsEncode(data,ecLen){var gen=[1],next,coef;for(i=0;i<ecLen;i++){next=new Array(gen.length+1).fill(0);for(j=0;j<gen.length;j++){next[j]^=gen[j];next[j+1]^=gfMul(gen[j],EXP[i])}gen=next}var msg=new Array(data.length+ecLen).fill(0);for(i=0;i<data.length;i++)msg[i]=data[i];for(i=0;i<data.length;i++){coef=msg[i];if(coef!==0)for(j=1;j<gen.length;j++)msg[i+j]^=gfMul(gen[j],coef)}return msg.slice(data.length)}function selectVersion(len){for(var v=1;v<=10;v++)if(len<=BYTE_CAP[v])return v;return-1}function toUtf8Bytes(text){var bytes=[],c;for(i=0;i<text.length;i++){c=text.charCodeAt(i);if(c<128)bytes.push(c);else if(c<2048){bytes.push(192|(c>>6));bytes.push(128|(c&63))}else{bytes.push(224|(c>>12));bytes.push(128|((c>>6)&63));bytes.push(128|(c&63))}}return bytes}function encodeData(bytes,version){var params=EC_PARAMS[version],total=params[0],ecPerBlock=params[1],numBlocks=params[2];var dataTotal=total-ecPerBlock*numBlocks,countBits=version<=9?8:16;var bits=[0,1,0,0],totalBits=dataTotal*8;for(i=countBits-1;i>=0;i--)bits.push((bytes.length>>i)&1);for(var k=0;k<bytes.length;k++){c=bytes[k];for(i=7;i>=0;i--)bits.push((c>>i)&1)}for(i=0;i<4&&bits.length<totalBits;i++)bits.push(0);while(bits.length%8)bits.push(0);var padByte=0;while(bits.length<totalBits){var pb=(padByte%2===0)?0xEC:0x11;for(i=7;i>=0&&bits.length<totalBits;i--)bits.push((pb>>i)&1);padByte++}var codewords=[];for(i=0;i<bits.length;i+=8){var v=0;for(j=0;j<8;j++)v=(v<<1)|(bits[i+j]||0);codewords.push(v)}return codewords}function buildAndInterleave(codewords,version){var params=EC_PARAMS[version],total=params[0],ecPerBlock=params[1],numBlocks=params[2];var dataTotal=total-ecPerBlock*numBlocks,dataPerBlock=Math.floor(dataTotal/numBlocks);var longBlocks=dataTotal-dataPerBlock*numBlocks,blocks=[],off=0,result=[],bi,bj;for(bi=0;bi<numBlocks;bi++){var len=dataPerBlock+(bi>=numBlocks-longBlocks?1:0);var data=codewords.slice(off,off+len);off+=len;blocks.push({data:data,ec:rsEncode(data,ecPerBlock)})}var maxData=Math.max.apply(null,blocks.map(function(b){return b.data.length}));for(bi=0;bi<maxData;bi++)for(bj=0;bj<blocks.length;bj++)if(bi<blocks[bj].data.length)result.push(blocks[bj].data[bi]);for(bi=0;bi<ecPerBlock;bi++)for(bj=0;bj<blocks.length;bj++)if(bi<blocks[bj].ec.length)result.push(blocks[bj].ec[bi]);return result}function createMatrix(version){var size=version*4+17;var mod=[],res=[];for(i=0;i<size;i++){mod.push(new Int8Array(size));res.push(new Uint8Array(size))}return{size:size,mod:mod,res:res}}function placeFinder(m,r0,c0){var mr,mc,dark;for(var r=-1;r<=7;r++)for(var c=-1;c<=7;c++){mr=r0+r;mc=c0+c;if(mr<0||mr>=m.size||mc<0||mc>=m.size)continue;dark=(r>=0&&r<=6&&(c===0||c===6))||(c>=0&&c<=6&&(r===0||r===6))||(r>=2&&r<=4&&c>=2&&c<=4);m.mod[mr][mc]=dark?1:-1;m.res[mr][mc]=1}}function placeAlignment(m,cr,cc){var dark;for(var r=-2;r<=2;r++)for(var c=-2;c<=2;c++){dark=Math.abs(r)===2||Math.abs(c)===2||(r===0&&c===0);m.mod[cr+r][cc+c]=dark?1:-1;m.res[cr+r][cc+c]=1}}function placeAll(m,version){placeFinder(m,0,0);placeFinder(m,0,m.size-7);placeFinder(m,m.size-7,0);var v;for(i=8;i<m.size-8;i++){v=(i&1)?-1:1;if(!m.res[6][i]){m.mod[6][i]=v;m.res[6][i]=1}if(!m.res[i][6]){m.mod[i][6]=v;m.res[i][6]=1}}var ap=ALIGN_POS[version];if(ap.length)for(var ai=0;ai<ap.length;ai++)for(var aj=0;aj<ap.length;aj++){if((ai===0&&aj===0)||(ai===0&&aj===ap.length-1)||(ai===ap.length-1&&aj===0))continue;placeAlignment(m,ap[ai],ap[aj])}m.mod[m.size-8][8]=1;m.res[m.size-8][8]=1;for(i=0;i<=8;i++){m.res[8][i]=1;m.res[i][8]=1}for(i=0;i<=7;i++){m.res[8][m.size-1-i]=1;m.res[m.size-1-i][8]=1}if(version>=7){for(i=0;i<18;i++){var vr=Math.floor(i/3),vc=i%3;m.res[vr][m.size-11+vc]=1;m.res[m.size-11+vc][vr]=1}placeVersionInfo(m,version)}}function placeData(m,data){var bits=[],idx=0,up=true,row,c,dc;for(i=0;i<data.length;i++){var b=data[i];for(j=7;j>=0;j--)bits.push((b>>j)&1)}for(var col=m.size-1;col>=0;col-=2){if(col===6)col--;for(i=0;i<m.size;i++){row=up?m.size-1-i:i;for(dc=0;dc<=1;dc++){c=col-dc;if(c>=0&&c<m.size&&!m.res[row][c]){m.mod[row][c]=idx<bits.length?bits[idx]:0;idx++}}}up=!up}}function applyMask(m,mask){var out=[],flip;for(i=0;i<m.size;i++){out.push(new Int8Array(m.mod[i]))}for(var r=0;r<m.size;r++)for(var c=0;c<m.size;c++){if(m.res[r][c])continue;flip=false;if(mask===0)flip=(r+c)%2===0;else if(mask===1)flip=r%2===0;else if(mask===2)flip=c%3===0;else if(mask===3)flip=(r+c)%3===0;else if(mask===4)flip=(Math.floor(r/2)+Math.floor(c/3))%2===0;else if(mask===5)flip=(r*c)%2+(r*c)%3===0;else if(mask===6)flip=((r*c)%2+(r*c)%3)%2===0;else flip=((r+c)%2+(r*c)%3)%2===0;if(flip)out[r][c]=out[r][c]?0:1}return out}function computeFormatBits(mask){var data=(1<<3)|mask,rem=data<<10;for(i=4;i>=0;i--)if(rem&(1<<(i+10)))rem^=0x537<<i;return((data<<10)|rem)^0x5412}function computeVersionBits(version){var data=version,rem=data<<12;for(i=5;i>=0;i--)if(rem&(1<<(i+12)))rem^=0x1F25<<i;return(data<<12)|rem}function placeVersionInfo(m,version){var bits=computeVersionBits(version);for(i=0;i<18;i++){var b=(bits>>i)&1,r=Math.floor(i/3),c=i%3;m.mod[r][m.size-11+c]=b;m.mod[m.size-11+c][r]=b}}function placeFormat(mod,size,mask){var fmt=computeFormatBits(mask);var p1=[[8,0],[8,1],[8,2],[8,3],[8,4],[8,5],[8,7],[8,8],[7,8],[5,8],[4,8],[3,8],[2,8],[1,8],[0,8]];var p2=[[size-1,8],[size-2,8],[size-3,8],[size-4,8],[size-5,8],[size-6,8],[size-7,8],[8,size-8],[8,size-7],[8,size-6],[8,size-5],[8,size-4],[8,size-3],[8,size-2],[8,size-1]];for(i=0;i<15;i++){var b=(fmt>>(14-i))&1;mod[p1[i][0]][p1[i][1]]=b;mod[p2[i][0]][p2[i][1]]=b}}function penalty(mod,size){var score=0,run,v,dark=0;for(var r=0;r<size;r++){run=1;for(var c=1;c<size;c++){if(mod[r][c]===mod[r][c-1])run++;else{if(run>=5)score+=run-2;run=1}}if(run>=5)score+=run-2}for(var c2=0;c2<size;c2++){run=1;for(var r2=1;r2<size;r2++){if(mod[r2][c2]===mod[r2-1][c2])run++;else{if(run>=5)score+=run-2;run=1}}if(run>=5)score+=run-2}for(r=0;r<size-1;r++)for(c2=0;c2<size-1;c2++){v=mod[r][c2];if(v===mod[r][c2+1]&&v===mod[r+1][c2]&&v===mod[r+1][c2+1])score+=3}for(r=0;r<size;r++)for(c2=0;c2<=size-7;c2++){if(mod[r][c2]===1&&mod[r][c2+1]===0&&mod[r][c2+2]===1&&mod[r][c2+3]===1&&mod[r][c2+4]===1&&mod[r][c2+5]===0&&mod[r][c2+6]===1)score+=40}for(c2=0;c2<size;c2++)for(r=0;r<=size-7;r++){if(mod[r][c2]===1&&mod[r+1][c2]===0&&mod[r+2][c2]===1&&mod[r+3][c2]===1&&mod[r+4][c2]===1&&mod[r+5][c2]===0&&mod[r+6][c2]===1)score+=40}for(r=0;r<size;r++)for(c2=0;c2<size;c2++)if(mod[r][c2]===1)dark++;var pct=dark*100/(size*size),p5=Math.floor(pct/5)*5;score+=Math.min(Math.abs(p5-50),Math.abs(p5+5-50))/5*10;return score}function encode(text){var bytes=toUtf8Bytes(text),version=selectVersion(bytes.length);if(version<0)throw new Error('Text too long for QR code');var cw=encodeData(bytes,version),interleaved=buildAndInterleave(cw,version);var m=createMatrix(version);placeAll(m,version);placeData(m,interleaved);var bestMask=0,bestScore=Infinity;for(var mask=0;mask<8;mask++){var masked=applyMask(m,mask);placeFormat(masked,m.size,mask);var s=penalty(masked,m.size);if(s<bestScore){bestScore=s;bestMask=mask}}var final_=applyMask(m,bestMask);placeFormat(final_,m.size,bestMask);return{matrix:final_,size:m.size}}function toSVG(qr,ms){ms=ms||4;var off=ms*4,d='';for(var r=0;r<qr.size;r++)for(var c=0;c<qr.size;c++)if(qr.matrix[r][c]===1)d+='M'+(c*ms+off)+','+(r*ms+off)+'h'+ms+'v'+ms+'h'+(-ms)+'z';var dim=(qr.size+8)*ms;return'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 '+dim+' '+dim+'" width="'+dim+'" height="'+dim+'"><rect width="100%" height="100%" fill="#fff" rx="8"/><path d="'+d+'" fill="#000"/></svg>'}return{encode:encode,toSVG:toSVG}})();
575
+ </script>
576
+ <script>
577
+ document.addEventListener('alpine:init',function(){Alpine.data('app',function(){return{
578
+ token:(function(){var t=new URLSearchParams(window.location.search).get('token')||'';if(t)history.replaceState(null,'',window.location.pathname);return t})(),theme:localStorage.getItem('pt-theme')||'light',tab:'wallet',state:null,
579
+ rcvType:'bch',rcvCategory:'',rcvAmount:'',rcvView:null,rcvQrSvg:'',
580
+ sendType:'bch',sendAmount:'',sendCurrency:'bch',sendAddress:'',sendCategory:'',sendTokenPreset:'__custom__',sendTokenAmount:'',sendError:'',sendSuccess:'',pendingSend:null,
581
+ 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,
584
+ 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:'',
586
+ imgPrompt:'',imgModel:'bytedance-seed/seedream-5-0-pro',imgAspect:'1:1',imgQuality:'auto',
587
+ toastVisible:false,toastMsg:'',toastOk:true,
588
+ 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);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()}})},
590
+ 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)}},
591
+ loadImages(){var s=this;(this.state&&this.state.imageHistory||[]).forEach(function(h){if(h.status==='completed'&&h.filepath)s.loadImage(h.id)})},
592
+ 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){}},
593
+ 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()},
594
+ 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'},
595
+ bchPriceLabel(p){if(!p)return '';return p.priceUsd!=null?'$'+p.priceUsd.toFixed(2)+' USD':((p.priceSats||0)/1e8).toFixed(8)+' BCH'},
596
+ 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'},
597
+
598
+ 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},
599
+
600
+ liftInsufficient(){if(!this.liftQuote||!this.state||!this.state.lift)return false;return Number(this.state.lift.rawBalance)<Number(this.liftQuote.rawAmount)},
601
+ bchInsufficient(){if(!this.pendingPurchase)return false;var have=this.state&&this.state.balance?Number(this.state.balance.spendableSats):0;return have<Number(this.pendingPurchase.priceSats||0)},
602
+ activeCreditsTotal(state){if(!state?.usage)return 0;return state.usage.reduce(function(sum,s){return s.active?sum+(s.remainingSeconds||0):sum},0)},
603
+ activeCreditsCount(state){if(!state?.usage)return 0;return state.usage.filter(function(s){return s.active}).length},
604
+ isTokenAddr(addr){return addr&&/^bitcoincash:z/.test(addr)},
605
+ toast(msg,isErr){this.toastMsg=msg;this.toastOk=!isErr;this.toastVisible=true;var self=this;setTimeout(function(){self.toastVisible=false},3000)},
606
+ toggleTheme(){this.theme=this.theme==='dark'?'light':'dark';localStorage.setItem('pt-theme',this.theme);document.documentElement.setAttribute('data-theme',this.theme)},
607
+ copyText(text){if(text){navigator.clipboard.writeText(text);this.toast('Copied!')}},
608
+ connectWatch(addr){if(!this.state)return;var address=addr||this.rcvView?.address||this.wsAddress;if(!address)return;if(this.ws&&this.wsAddress===address&&(this.ws.readyState===0||this.ws.readyState===1))return;var self=this;var host=this.state.network==='chipnet'?'wss://chipnet.watchtower.cash':'wss://watchtower.cash';this.wsStatus='connecting';this.wsAddress=address;if(this.ws){this.ws.onclose=null;try{this.ws.close()}catch(e){};this.ws=null}try{this.ws=new WebSocket(host+'/ws/watch/bch/'+address+'/')}catch(e){console.debug('[ws] ctor error',e);this.wsStatus='off';return}this.ws.onopen=function(){console.debug('[ws] open');self.wsStatus='live';self.startWsPing()};this.ws.onmessage=function(ev){var m=null;try{m=JSON.parse(ev.data)}catch(e){return}self.handleWsUpdate(m)};this.ws.onclose=function(ev){console.debug('[ws] close',ev.code,ev.reason||'');if(self.wsPingTimer)clearTimeout(self.wsPingTimer);if(self.wsAddress!==address)return;self.wsStatus='off';if(self.wsReconnectTimer)clearTimeout(self.wsReconnectTimer);self.wsReconnectTimer=setTimeout(function(){if(self.state)self.connectWatch()},3000)};this.ws.onerror=function(){console.debug('[ws] error')}},
609
+ startWsPing(){var self=this;if(this.wsPingTimer)clearTimeout(this.wsPingTimer);this.wsPingTimer=setTimeout(function(){if(self.ws&&self.ws.readyState===1){try{self.ws.send('ping')}catch(e){}self.startWsPing()}},30000)},
610
+ 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)},
611
+ 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)},
612
+ 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}}},
613
+ 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}},
614
+ 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},
615
+ 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},
616
+ 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},
617
+ 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},
618
+ 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},
619
+ 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},
620
+ 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)}},
621
+ 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)}},
622
+ 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)}},
623
+ 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},
624
+ cancelRefillEdit(){this.refillEditing=false;this.refillNotice=''},
625
+ 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.'},
626
+ 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)}},
627
+ async doImageQuote(){var prompt=(this.imgPrompt||'').trim();if(!prompt){this.toast('Enter a prompt',true);return}this.imgQuoteBusy=true;try{var quote=await this.api('POST','/api/ai/images/quote',{prompt:prompt,model:this.imgModel||undefined,aspectRatio:this.imgAspect,quality:this.imgQuality});this.pendingImageQuote=quote;this.showImageQuoteModal=true}catch(e){this.toast(e.message,true)}this.imgQuoteBusy=false},
628
+ async confirmImageGen(){if(!this.pendingImageQuote)return;this.imgGenBusy=true;try{var orderId=this.pendingImageQuote.orderId;var result=await this.api('POST','/api/ai/images/fulfill',{orderId:orderId});this.showImageQuoteModal=false;this.aiAction='images';if(result&&result.path){this.lightbox={id:orderId,path:result.path};this.toast('Image saved to ~/.paytaca/images')}else{this.toast((result&&result.error)||(result&&result.paid?'Payment received — image will appear in your gallery':'Image generation failed'),true)}this.load()}catch(e){this.toast(e.message,true)}this.imgGenBusy=false;this.pendingImageQuote=null},
629
+ openImage(h){this.lightbox={id:h.id,path:h.filepath||('~/paytaca/images/'+h.id)};this.lightboxConfirmDelete=false;this.loadImage(h.id)},
630
+ async deleteImage(){if(!this.lightbox)return;var id=this.lightbox.id;try{await this.api('DELETE','/api/ai/images/'+id);this.lightbox=null;this.lightboxConfirmDelete=false;this.toast('Image deleted');this.load()}catch(e){this.toast(e.message,true);this.lightboxConfirmDelete=false}},
631
+ imgCaption(h){var t=h.prompt||h.model_display_name||h.model||h.id||'';return t.length>48?t.slice(0,48)+'…':t}
632
+ }})});
633
+ </script>
634
+ <script defer>${ALPINE_JS}</script>
635
+ </body>
636
+ </html>`;
637
+ }
638
+ //# sourceMappingURL=page.js.map