Проект

Общее

Профиль

Файлы » orgonlink-dapp-test_1.html

Alexandr Kolokhmatov, 25.06.2026 09:32

 
1
<!DOCTYPE html>
2
<html lang="ru">
3
<head>
4
<meta charset="UTF-8">
5
<meta name="viewport" content="width=device-width, initial-scale=1">
6
<title>OrgonLink · dApp test bench</title>
7
<style>
8
  :root{
9
    --bg:#0a0d18; --panel:#121728; --panel2:#0e1322; --line:#232b45;
10
    --text:#e7eaf3; --muted:#8a93ad; --faint:#5b6480;
11
    --accent:#ff2d8f; --accent2:#7c5cff;
12
    --ok:#2ecc8f; --err:#ff5a6e; --warn:#ffb02e; --pending:#5aa9ff;
13
    --mono:ui-monospace,"SF Mono","JetBrains Mono",Menlo,Consolas,monospace;
14
    --sans:"Inter",system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;
15
    --r:10px;
16
  }
17
  *{box-sizing:border-box}
18
  html,body{margin:0;background:var(--bg);color:var(--text);font-family:var(--sans);font-size:14px;line-height:1.5}
19
  a{color:var(--accent)}
20
  code,.mono{font-family:var(--mono)}
21
  ::selection{background:var(--accent);color:#fff}
22

    
23
  /* ── Top bar ───────────────────────────── */
24
  header{position:sticky;top:0;z-index:20;display:flex;align-items:center;gap:16px;
25
    padding:14px 22px;background:rgba(10,13,24,.92);backdrop-filter:blur(8px);
26
    border-bottom:1px solid var(--line)}
27
  .brand{display:flex;align-items:center;gap:11px;font-weight:600;letter-spacing:.2px}
28
  .glyph{width:30px;height:30px;border-radius:8px;display:grid;place-items:center;
29
    background:linear-gradient(135deg,var(--accent),var(--accent2));color:#fff;font-weight:700;
30
    font-family:var(--mono);font-size:18px}
31
  .brand small{display:block;color:var(--muted);font-weight:400;font-size:11px;letter-spacing:.3px}
32
  .wire{flex:1;display:flex;align-items:center;gap:10px;min-width:0}
33
  .dot{width:9px;height:9px;border-radius:50%;background:var(--err);flex:none;
34
    box-shadow:0 0 0 0 currentColor;color:var(--err)}
35
  .dot.on{background:var(--ok);color:var(--ok);animation:pulse 2.2s infinite}
36
  @keyframes pulse{0%{box-shadow:0 0 0 0 currentColor}70%{box-shadow:0 0 0 7px transparent}100%{box-shadow:0 0 0 0 transparent}}
37
  .wire .stat{color:var(--muted);font-family:var(--mono);font-size:12px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
38
  .wire .stat b{color:var(--text)}
39
  header button{font-family:var(--sans)}
40

    
41
  /* ── Layout ────────────────────────────── */
42
  .wrap{display:grid;grid-template-columns:330px 1fr;gap:20px;max-width:1280px;margin:0 auto;padding:20px 22px 80px}
43
  @media(max-width:900px){.wrap{grid-template-columns:1fr}}
44
  aside{position:sticky;top:74px;align-self:start;display:flex;flex-direction:column;gap:14px;max-height:calc(100vh - 94px)}
45
  @media(max-width:900px){aside{position:static;max-height:none}}
46

    
47
  .card{background:var(--panel);border:1px solid var(--line);border-radius:var(--r)}
48
  .card>.hd{padding:11px 15px;border-bottom:1px solid var(--line);display:flex;align-items:center;justify-content:space-between;gap:8px}
49
  .hd h2,.hd h3{margin:0;font-size:13px;font-weight:600;letter-spacing:.2px}
50
  .hd .sub{color:var(--muted);font-size:11px;font-family:var(--mono)}
51
  .bd{padding:14px 15px}
52

    
53
  /* status panel */
54
  .kv{display:flex;justify-content:space-between;gap:10px;padding:5px 0;border-bottom:1px dashed #1c2440;font-family:var(--mono);font-size:12px}
55
  .kv:last-child{border-bottom:none}
56
  .kv .k{color:var(--muted)}
57
  .kv .v{color:var(--text);text-align:right;word-break:break-all;max-width:200px}
58
  .v.good{color:var(--ok)} .v.bad{color:var(--err)} .v.warnc{color:var(--warn)}
59

    
60
  /* event log */
61
  .log{font-family:var(--mono);font-size:11.5px;background:var(--panel2);border-radius:8px;
62
    padding:10px;height:230px;overflow:auto;display:flex;flex-direction:column;gap:3px}
63
  .log .row{display:flex;gap:8px;align-items:baseline}
64
  .log .t{color:var(--faint);flex:none}
65
  .log .ev{font-weight:600}
66
  .log .connect,.log .unlocked{color:var(--ok)}
67
  .log .disconnect,.log .locked{color:var(--err)}
68
  .log .accountsChanged{color:var(--warn)}
69
  .log .networkChanged{color:var(--accent2)}
70
  .log .sys{color:var(--muted)}
71
  .log .msg{color:var(--muted);word-break:break-all}
72

    
73
  /* method groups */
74
  main{display:flex;flex-direction:column;gap:18px;min-width:0}
75
  .group>.ghd{display:flex;align-items:center;gap:10px;margin:4px 2px 10px}
76
  .ghd .bar{width:3px;height:16px;border-radius:2px;background:var(--accent)}
77
  .ghd h2{margin:0;font-size:14px;font-weight:600}
78
  .ghd .count{color:var(--muted);font-size:11px;font-family:var(--mono)}
79
  .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(330px,1fr));gap:12px}
80

    
81
  .m{background:var(--panel);border:1px solid var(--line);border-radius:var(--r);display:flex;flex-direction:column;overflow:hidden}
82
  .m .top{padding:11px 13px 9px;display:flex;align-items:flex-start;justify-content:space-between;gap:8px}
83
  .m .name{font-family:var(--mono);font-size:12.5px;color:var(--text);font-weight:600}
84
  .m .desc{color:var(--muted);font-size:11.5px;margin-top:2px}
85
  .pill{font-family:var(--mono);font-size:10px;padding:2px 7px;border-radius:20px;border:1px solid var(--line);color:var(--muted);flex:none;white-space:nowrap}
86
  .pill.popup{color:var(--warn);border-color:#3a2f12}
87
  .pill.read{color:var(--pending);border-color:#1c3050}
88
  .pill.local{color:var(--accent2);border-color:#2a2354}
89

    
90
  .fields{padding:0 13px 11px;display:flex;flex-direction:column;gap:7px}
91
  .field label{display:block;color:var(--muted);font-size:10.5px;font-family:var(--mono);margin-bottom:3px}
92
  .field input,.field textarea{width:100%;background:var(--panel2);border:1px solid var(--line);color:var(--text);
93
    border-radius:7px;padding:7px 9px;font-family:var(--mono);font-size:12px;resize:vertical}
94
  .field input:focus,.field textarea:focus{outline:none;border-color:var(--accent2)}
95
  .field textarea{min-height:54px}
96

    
97
  .actions{padding:9px 13px;border-top:1px solid var(--line);display:flex;align-items:center;gap:8px;background:var(--panel2)}
98
  button{cursor:pointer;border:none;border-radius:7px;padding:7px 13px;font-size:12px;font-weight:600;
99
    background:var(--accent);color:#fff;transition:filter .15s,opacity .15s}
100
  button:hover{filter:brightness(1.1)} button:active{filter:brightness(.95)}
101
  button:disabled{opacity:.45;cursor:not-allowed;filter:none}
102
  button.ghost{background:transparent;color:var(--muted);border:1px solid var(--line)}
103
  button.ghost:hover{color:var(--text);filter:none;border-color:var(--accent2)}
104
  button.small{padding:4px 9px;font-size:11px}
105
  .status{font-family:var(--mono);font-size:11px;margin-left:auto;display:flex;align-items:center;gap:6px;color:var(--muted)}
106
  .led{width:7px;height:7px;border-radius:50%;background:var(--line)}
107
  .led.ok{background:var(--ok)} .led.err{background:var(--err)} .led.pending{background:var(--pending);animation:blink .8s infinite}
108
  @keyframes blink{50%{opacity:.3}}
109

    
110
  .out{margin:0;font-family:var(--mono);font-size:11.5px;background:#080b14;border-top:1px solid var(--line);
111
    padding:11px 13px;max-height:280px;overflow:auto;white-space:pre-wrap;word-break:break-word;color:#cdd4e6;display:none}
112
  .out.show{display:block}
113
  .out .ok{color:var(--ok)} .out .err{color:var(--err)}
114

    
115
  .banner{margin:0 0 18px;padding:13px 16px;border-radius:var(--r);font-size:13px;display:none}
116
  .banner.show{display:block}
117
  .banner.warn{background:#241a08;border:1px solid #4a3712;color:#ffcd78}
118
  .banner code{background:#0006;padding:1px 5px;border-radius:4px}
119

    
120
  .toolbar{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin-bottom:4px}
121
  .toolbar .sep{flex:1}
122
</style>
123
</head>
124
<body>
125

    
126
<header>
127
  <div class="brand">
128
    <div class="glyph">o</div>
129
    <div>OrgonLink <small>dApp test bench · provider API</small></div>
130
  </div>
131
  <div class="wire">
132
    <span class="dot" id="dot"></span>
133
    <span class="stat" id="wireStat">Поиск провайдера…</span>
134
  </div>
135
  <button class="ghost small" id="refreshBtn">Обновить статус</button>
136
</header>
137

    
138
<div class="wrap">
139
  <!-- LEFT RAIL -->
140
  <aside>
141
    <div class="card">
142
      <div class="hd"><h2>Состояние провайдера</h2><span class="sub" id="verTag"></span></div>
143
      <div class="bd" id="statusKV">
144
        <div class="kv"><span class="k">window.tron</span><span class="v" id="kv-tron"></span></div>
145
        <div class="kv"><span class="k">window.orgonWeb</span><span class="v" id="kv-ow"></span></div>
146
        <div class="kv"><span class="k">isOrgonLink</span><span class="v" id="kv-is"></span></div>
147
        <div class="kv"><span class="k">ready</span><span class="v" id="kv-ready"></span></div>
148
        <div class="kv"><span class="k">defaultAddress</span><span class="v" id="kv-addr"></span></div>
149
      </div>
150
    </div>
151

    
152
    <div class="card" style="flex:1;min-height:0;display:flex;flex-direction:column">
153
      <div class="hd">
154
        <h3>Лог событий</h3>
155
        <button class="ghost small" id="clearLog">Очистить</button>
156
      </div>
157
      <div class="bd" style="flex:1;min-height:0;display:flex">
158
        <div class="log" id="log"></div>
159
      </div>
160
    </div>
161
  </aside>
162

    
163
  <!-- RIGHT -->
164
  <main>
165
    <div class="banner warn" id="noProvider">
166
      <b>Провайдер OrgonLink не обнаружен.</b><br>
167
      Проверь: расширение загружено в <code>chrome://extensions</code>; эта страница открыта по
168
      <code>https://</code>, <code>http://localhost</code> или <code>file://</code> (под манифест).
169
      Для <code>file://</code> включи у OrgonLink тумблер «Разрешить доступ к URL-адресам файлов».
170
    </div>
171

    
172
    <div class="toolbar">
173
      <button id="sweepBtn">▶ Прогнать read-only пакет</button>
174
      <button class="ghost" id="connectBtn">Подключить кошелёк</button>
175
      <span class="sep"></span>
176
      <span class="status mono" id="sweepStatus"></span>
177
    </div>
178

    
179
    <div id="groups"></div>
180
  </main>
181
</div>
182

    
183
<script>
184
(function(){
185
  "use strict";
186

    
187
  // ── helpers ───────────────────────────────────────────────
188
  const $ = s => document.querySelector(s);
189
  const ce = (t,c)=>{const e=document.createElement(t); if(c)e.className=c; return e;};
190
  const now = ()=>new Date().toLocaleTimeString('ru-RU',{hour12:false})+"."+String(Date.now()%1000).padStart(3,'0');
191
  const ow = ()=>window.orgonWeb;
192
  const curAddr = ()=>{ const d=ow()&&ow().defaultAddress; return d? (d.base58||d) : null; };
193
  function jstr(v){ try{return JSON.stringify(v,(k,val)=>typeof val==='bigint'?val.toString():val,2);}catch{ return String(v);} }
194

    
195
  // ── event log ─────────────────────────────────────────────
196
  const logEl = $('#log');
197
  function log(ev, msg, cls){
198
    const row=ce('div','row');
199
    row.innerHTML=`<span class="t">${now()}</span><span class="ev ${cls||ev}">${ev}</span>`;
200
    if(msg!==undefined){const m=ce('span','msg'); m.textContent=typeof msg==='string'?msg:jstr(msg); row.appendChild(m);}
201
    logEl.appendChild(row); logEl.scrollTop=logEl.scrollHeight;
202
    while(logEl.children.length>300) logEl.removeChild(logEl.firstChild);
203
  }
204
  $('#clearLog').onclick=()=>logEl.innerHTML='';
205

    
206
  // ── status panel ──────────────────────────────────────────
207
  function setKV(id,val,kind){const e=$('#'+id); e.textContent=val; e.className='v'+(kind?(' '+kind):'');}
208
  function refreshStatus(){
209
    const hasTron = typeof window.tron!=='undefined';
210
    const o = ow();
211
    setKV('kv-tron', hasTron?'present':'absent', hasTron?'good':'bad');
212
    setKV('kv-ow', o?'present':'absent', o?'good':'bad');
213
    setKV('kv-is', o?String(!!o.isOrgonLink):'', o&&o.isOrgonLink?'good':'');
214
    setKV('kv-ready', o?String(!!o.ready):'', o?(o.ready?'good':'warnc'):'');
215
    const a=curAddr(); setKV('kv-addr', a||'(не подключён)', a?'good':'warnc');
216
    $('#verTag').textContent = o&&o.version ? 'v'+o.version : '';
217
    const dot=$('#dot'), stat=$('#wireStat');
218
    if(o){ dot.classList.add('on');
219
      stat.innerHTML = o.ready
220
        ? `Подключён · <b>${a||'?'}</b>`
221
        : `Провайдер найден · <b>кошелёк заблокирован / не подключён</b>`;
222
    } else { dot.classList.remove('on'); stat.textContent='Провайдер не обнаружен'; }
223
    $('#noProvider').classList.toggle('show', !o);
224
  }
225
  $('#refreshBtn').onclick=refreshStatus;
226

    
227
  // ── shared tx buffer (для chain: build → sign → broadcast) ─
228
  const buf = { unsigned:null, signed:null };
229

    
230
  // ── registry of every provider method ─────────────────────
231
  // kind: 'read' (no popup), 'popup' (подтверждение в расширении), 'local' (без расширения)
232
  const F = (id,group,name,desc,kind,fields,run)=>({id,group,name,desc,kind,fields:fields||[],run});
233

    
234
  const REG = [
235
    // ── Provider / connection ──
236
    F('detect','Провайдер','snapshot()','Срез состояния провайдера (локально)','local',[],
237
      async()=>({hasWindowTron:typeof window.tron!=='undefined', isOrgonLink: ow()?.isOrgonLink,
238
                 version: ow()?.version, ready: ow()?.ready, defaultAddress: ow()?.defaultAddress})),
239
    F('reqAccounts','Провайдер','tron.request(tron_requestAccounts)','EIP-1193-стиль запрос подключения','popup',[],
240
      async()=> await window.tron.request({method:'tron_requestAccounts'})),
241
    F('requestAccess','Провайдер','orgonWeb.requestAccess()','Прямой запрос доступа к кошельку','popup',[],
242
      async()=> await ow().requestAccess()),
243

    
244
    // ── Chain reads ──
245
    F('getCurrentBlock','Чтение сети','trx.getCurrentBlock()','Текущий блок','read',[],
246
      async()=> await ow().trx.getCurrentBlock()),
247
    F('getBlockByNumber','Чтение сети','trx.getBlockByNumber(num)','Блок по номеру','read',
248
      [{n:'num',l:'num',ph:'1',v:'1'}],
249
      async p=> await ow().trx.getBlockByNumber(Number(p.num))),
250
    F('getBalance','Чтение сети','trx.getBalance(address)','Баланс ORGON (SUN)','read',
251
      [{n:'address',l:'address (пусто = текущий)',ph:'o...'}],
252
      async p=> await ow().trx.getBalance(p.address||curAddr())),
253
    F('getAccount','Чтение сети','trx.getAccount(address)','Данные аккаунта','read',
254
      [{n:'address',l:'address (пусто = текущий)',ph:'o...'}],
255
      async p=> await ow().trx.getAccount(p.address||curAddr())),
256
    F('getTransactions','Чтение сети','trx.getTransactions(address,limit)','История транзакций','read',
257
      [{n:'address',l:'address (пусто = текущий)',ph:'o...'},{n:'limit',l:'limit',ph:'20',v:'20'}],
258
      async p=> await ow().trx.getTransactions(p.address||curAddr(), Number(p.limit)||20)),
259
    F('getTransaction','Чтение сети','trx.getTransaction(txid)','Транзакция по id','read',
260
      [{n:'txid',l:'txid',ph:'hex txid'}],
261
      async p=> await ow().trx.getTransaction(p.txid)),
262
    F('getTxInfo','Чтение сети','trx.getTransactionInfo(txid)','Receipt / результат исполнения','read',
263
      [{n:'txid',l:'txid',ph:'hex txid'}],
264
      async p=> await ow().trx.getTransactionInfo(p.txid)),
265
    F('rpcCall','Чтение сети','fullNode(method,params)','Прямой RPC к Full Node (rpcCall). Путь без ведущего слэша','read',
266
      [{n:'method',l:'method (без ведущего «/»)',ph:'wallet/getnowblock',v:'wallet/getnowblock'},
267
       {n:'params',l:'params (JSON)',ta:1,ph:'{}',v:'{}'}],
268
      async p=> await ow().fullNode(p.method.replace(/^\/+/,''), JSON.parse(p.params||'{}'))),
269

    
270
    // ── transactionBuilder ──
271
    F('sendTrx','Сборка tx','transactionBuilder.sendTrx(to,amount)','Собрать перевод ORGON → unsigned tx','read',
272
      [{n:'to',l:'to (base58)',ph:'o...'},{n:'amount',l:'amount (SUN, 1 ORGON = 1 000 000)',ph:'1000000',v:'1000000'}],
273
      async p=>{ const tx=await ow().transactionBuilder.sendTrx(p.to, Number(p.amount)); buf.unsigned=tx; return tx;}),
274
    F('sendToken','Сборка tx','transactionBuilder.sendToken(to,amount,tokenId)','Перевод oRC-10 токена → unsigned tx','read',
275
      [{n:'to',l:'to (base58)',ph:'o...'},{n:'amount',l:'amount',ph:'1'},{n:'tokenId',l:'tokenId',ph:'1000001'}],
276
      async p=>{ const tx=await ow().transactionBuilder.sendToken(p.to, Number(p.amount), p.tokenId); buf.unsigned=tx; return tx;}),
277
    F('triggerConst','Сборка tx','triggerConstantContract(...)','Read-only вызов контракта (view/pure)','read',
278
      [{n:'addr',l:'contractAddress',ph:'o...'},{n:'sel',l:'functionSelector',ph:'balanceOf(address)'},
279
       {n:'params',l:'parameters (JSON)',ta:1,ph:'[]',v:'[]'}],
280
      async p=> await ow().transactionBuilder.triggerConstantContract(p.addr,p.sel,{},JSON.parse(p.params||'[]'))),
281
    F('triggerSmart','Сборка tx','triggerSmartContract(...)','Write-вызов → unsigned tx','read',
282
      [{n:'addr',l:'contractAddress',ph:'o...'},{n:'sel',l:'functionSelector',ph:'transfer(address,uint256)'},
283
       {n:'params',l:'parameters (JSON)',ta:1,ph:'[]',v:'[]'},{n:'feeLimit',l:'feeLimit',ph:'150000000',v:'150000000'}],
284
      async p=>{ const r=await ow().transactionBuilder.triggerSmartContract(p.addr,p.sel,{feeLimit:Number(p.feeLimit)},JSON.parse(p.params||'[]'));
285
                 if(r&&r.transaction) buf.unsigned=r.transaction; return r;}),
286
    F('estimateEnergy','Сборка tx','estimateEnergy(...)','Оценка energy вызова','read',
287
      [{n:'addr',l:'contractAddress',ph:'o...'},{n:'sel',l:'functionSelector',ph:'transfer(address,uint256)'},
288
       {n:'params',l:'parameters (JSON)',ta:1,ph:'[]',v:'[]'}],
289
      async p=> await ow().transactionBuilder.estimateEnergy(p.addr,p.sel,{},JSON.parse(p.params||'[]'))),
290
    F('createContract','Сборка tx','createSmartContract(options)','Деплой контракта → unsigned tx','read',
291
      [{n:'options',l:'options (JSON)',ta:1,ph:'{"abi":[],"bytecode":"..."}',v:'{}'}],
292
      async p=>{ const r=await ow().transactionBuilder.createSmartContract(JSON.parse(p.options||'{}')); buf.unsigned=r; return r;}),
293

    
294
    // ── Signing & broadcast ──
295
    F('signTx','Подпись','trx.sign(tx)','Подписать tx (попап). Пусто = из буфера','popup',
296
      [{n:'tx',l:'transaction JSON (пусто = последняя собранная)',ta:1,ph:'{ ... } или пусто'}],
297
      async p=>{ const tx=p.tx?JSON.parse(p.tx):buf.unsigned;
298
                 if(!tx) throw new Error('Нет tx: сначала собери (sendTrx/triggerSmartContract) или вставь JSON');
299
                 const s=await ow().trx.sign(tx); buf.signed=s; return s;}),
300
    F('signMsg','Подпись','signMessageV2(message)','Подпись сообщения TIP-191 (попап)','popup',
301
      [{n:'message',l:'message',ph:'Hello Orgon',v:'Hello Orgon'}],
302
      async p=> await ow().signMessageV2(p.message)),
303
    F('verifyMsg','Подпись','verifyMessageV2(message,signature)','Восстановить адрес подписанта','read',
304
      [{n:'message',l:'message',ph:'Hello Orgon'},{n:'signature',l:'signature (hex)',ph:'0x...'}],
305
      async p=> await ow().verifyMessageV2(p.message,p.signature)),
306
    F('verifyMsgAddr','Подпись','verifyMessage(message,signature,address)','Проверка подписи против адреса','read',
307
      [{n:'message',l:'message',ph:'Hello Orgon'},{n:'signature',l:'signature (hex)',ph:'0x...'},{n:'address',l:'address',ph:'o...'}],
308
      async p=> await ow().verifyMessage(p.message,p.signature,p.address)),
309
    F('broadcast','Подпись','trx.sendRawTransaction(signed)','Отправить подписанную tx. Пусто = из буфера','popup',
310
      [{n:'signed',l:'signed tx JSON (пусто = последняя подписанная)',ta:1,ph:'{ ... } или пусто'}],
311
      async p=>{ const s=p.signed?JSON.parse(p.signed):buf.signed;
312
                 if(!s) throw new Error('Нет подписанной tx: сначала подпиши или вставь JSON');
313
                 return await ow().trx.sendRawTransaction(s);}),
314

    
315
    // ── Contract factory ──
316
    F('contractCall','Контракт','contract(abi,addr).m(...).call()','Read-метод контракта через фабрику','read',
317
      [{n:'abi',l:'ABI (JSON)',ta:1,ph:'[{"name":"balanceOf","type":"function","inputs":[{"type":"address"}]}]',v:'[]'},
318
       {n:'address',l:'address',ph:'o...'},{n:'method',l:'method',ph:'balanceOf'},
319
       {n:'args',l:'args (JSON array)',ta:1,ph:'["o..."]',v:'[]'}],
320
      async p=>{ const c=ow().contract(JSON.parse(p.abi||'[]'),p.address);
321
                 if(typeof c[p.method]!=='function') throw new Error('Метод '+p.method+' не найден в ABI');
322
                 return await c[p.method](...JSON.parse(p.args||'[]')).call();}),
323
    F('contractSend','Контракт','contract(abi,addr).m(...).send()','Write-метод контракта (попап)','popup',
324
      [{n:'abi',l:'ABI (JSON)',ta:1,ph:'[...]',v:'[]'},{n:'address',l:'address',ph:'o...'},
325
       {n:'method',l:'method',ph:'transfer'},{n:'args',l:'args (JSON array)',ta:1,ph:'["o...",100]',v:'[]'},
326
       {n:'feeLimit',l:'feeLimit',ph:'150000000',v:'150000000'}],
327
      async p=>{ const c=ow().contract(JSON.parse(p.abi||'[]'),p.address);
328
                 if(typeof c[p.method]!=='function') throw new Error('Метод '+p.method+' не найден в ABI');
329
                 return await c[p.method](...JSON.parse(p.args||'[]')).send({feeLimit:Number(p.feeLimit)});}),
330

    
331
    // ── End-to-end ──
332
    F('e2e','Сценарий','build → sign → broadcast','Полный путь перевода ORGON одной кнопкой','popup',
333
      [{n:'to',l:'to (base58)',ph:'o...'},{n:'amount',l:'amount (SUN)',ph:'1000000',v:'1000000'}],
334
      async (p,emit)=>{
335
        emit('1/3 transactionBuilder.sendTrx …');
336
        const tx=await ow().transactionBuilder.sendTrx(p.to,Number(p.amount)); buf.unsigned=tx;
337
        emit('2/3 trx.sign (подтверди в попапе) …');
338
        const signed=await ow().trx.sign(tx); buf.signed=signed;
339
        emit('3/3 trx.sendRawTransaction …');
340
        const res=await ow().trx.sendRawTransaction(signed);
341
        return {step1_unsigned:tx, step2_signed:signed, step3_broadcast:res};
342
      }),
343

    
344
    // ── Utils (без расширения) ──
345
    F('fromSun','Утилиты','utils.fromSun(sun)','SUN → ORGON','local',
346
      [{n:'sun',l:'sun',ph:'1000000',v:'1000000'}],
347
      async p=> (ow()?.utils?.fromSun? ow().utils.fromSun(p.sun) : Number(p.sun)/1e6)),
348
    F('toSun','Утилиты','utils.toSun(orgon)','ORGON → SUN','local',
349
      [{n:'orgon',l:'orgon',ph:'1',v:'1'}],
350
      async p=> (ow()?.utils?.toSun? ow().utils.toSun(p.orgon) : Math.round(Number(p.orgon)*1e6))),
351
    F('isAddress','Утилиты','utils.isAddress(addr)','Проверка формата адреса','local',
352
      [{n:'addr',l:'addr',ph:'o...'}],
353
      async p=> ow()?.utils?.isAddress(p.addr)),
354
    F('hexToBytes','Утилиты','utils.hexToBytes(hex)','hex → Uint8Array','local',
355
      [{n:'hex',l:'hex',ph:'0x48656c6c6f',v:'0x48656c6c6f'}],
356
      async p=> Array.from(ow().utils.hexToBytes(p.hex))),
357
    F('bytesToHex','Утилиты','utils.bytesToHex(bytes)','bytes(JSON массив) → hex','local',
358
      [{n:'bytes',l:'bytes (JSON array)',ph:'[72,101,108,108,111]',v:'[72,101,108,108,111]'}],
359
      async p=> ow().utils.bytesToHex(new Uint8Array(JSON.parse(p.bytes||'[]')))),
360
  ];
361

    
362
  // ── render ────────────────────────────────────────────────
363
  const KIND_LABEL = {read:'read', popup:'попап', local:'локально'};
364
  const groupsEl = $('#groups');
365
  const order = [];
366
  REG.forEach(m=>{ if(!order.includes(m.group)) order.push(m.group); });
367

    
368
  order.forEach(g=>{
369
    const items = REG.filter(m=>m.group===g);
370
    const gel=ce('div','group');
371
    const ghd=ce('div','ghd');
372
    ghd.innerHTML=`<span class="bar"></span><h2>${g}</h2><span class="count">${items.length}</span>`;
373
    gel.appendChild(ghd);
374
    const grid=ce('div','grid');
375
    items.forEach(m=>grid.appendChild(renderCard(m)));
376
    gel.appendChild(grid);
377
    groupsEl.appendChild(gel);
378
  });
379

    
380
  function renderCard(m){
381
    const el=ce('div','m'); el.dataset.id=m.id;
382
    const top=ce('div','top');
383
    top.innerHTML=`<div><div class="name">${m.name}</div><div class="desc">${m.desc}</div></div>
384
      <span class="pill ${m.kind}">${KIND_LABEL[m.kind]}</span>`;
385
    el.appendChild(top);
386

    
387
    const fwrap=ce('div','fields');
388
    m.fields.forEach(f=>{
389
      const fl=ce('div','field');
390
      const inp = f.ta ? `<textarea data-f="${f.n}" placeholder="${(f.ph||'').replace(/"/g,'&quot;')}">${f.v||''}</textarea>`
391
                       : `<input data-f="${f.n}" value="${f.v||''}" placeholder="${(f.ph||'').replace(/"/g,'&quot;')}">`;
392
      fl.innerHTML=`<label>${f.l}</label>${inp}`;
393
      fwrap.appendChild(fl);
394
    });
395
    if(m.fields.length) el.appendChild(fwrap);
396

    
397
    const act=ce('div','actions');
398
    const btn=ce('button'); btn.textContent='Run'; btn.className='small';
399
    const st=ce('span','status'); st.innerHTML=`<span class="led"></span><span class="txt">—</span>`;
400
    act.appendChild(btn); act.appendChild(st); el.appendChild(act);
401

    
402
    const out=ce('pre','out'); el.appendChild(out);
403

    
404
    btn.onclick=()=>execCard(m,el,btn,st,out);
405
    return el;
406
  }
407

    
408
  function readFields(el){
409
    const p={};
410
    el.querySelectorAll('[data-f]').forEach(i=>p[i.dataset.f]=i.value.trim());
411
    return p;
412
  }
413
  function setLed(st,cls,txt){
414
    st.querySelector('.led').className='led '+(cls||'');
415
    st.querySelector('.txt').textContent=txt;
416
  }
417

    
418
  async function execCard(m,el,btn,st,out){
419
    if(m.kind!=='local' && !ow()){ refreshStatus(); flashOut(out,true,'Провайдер не обнаружен — расширение не активно на этой странице'); return; }
420
    btn.disabled=true; setLed(st,'pending','');
421
    out.classList.add('show'); out.innerHTML='<span style="color:#5aa9ff">running…</span>';
422
    const t0=performance.now();
423
    const emit = msg=>{ out.innerHTML=`<span style="color:#5aa9ff">${msg}</span>`; log(m.id, msg, 'sys'); };
424
    try{
425
      const res = await m.run(readFields(el), emit);
426
      const dt=(performance.now()-t0).toFixed(0);
427
      setLed(st,'ok',dt+' ms');
428
      flashOut(out,false,jstr(res));
429
      log(m.id, 'ok '+dt+'ms', 'sys');
430
    }catch(e){
431
      const dt=(performance.now()-t0).toFixed(0);
432
      setLed(st,'err',dt+' ms');
433
      const detail = e&&e.code!==undefined ? `[${e.code}] ${e.message}` : (e&&e.message?e.message:String(e));
434
      flashOut(out,true,detail);
435
      log(m.id, 'err '+detail, 'sys');
436
    }finally{
437
      btn.disabled=false;
438
      refreshStatus();
439
    }
440
  }
441
  function flashOut(out,isErr,text){
442
    out.classList.add('show');
443
    out.innerHTML=`<span class="${isErr?'err':'ok'}">${isErr?'✗ ERROR':'✓ OK'}</span>\n`+escapeHtml(text);
444
  }
445
  function escapeHtml(s){return String(s).replace(/[&<>]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;'}[c]));}
446

    
447
  // ── top toolbar actions ───────────────────────────────────
448
  $('#connectBtn').onclick=async()=>{
449
    if(!ow()){refreshStatus();return;}
450
    try{ const r=await window.tron.request({method:'tron_requestAccounts'}); log('connect', r,'sys'); }
451
    catch(e){ log('connect', 'err '+(e.message||e),'sys'); }
452
    refreshStatus();
453
  };
454

    
455
  $('#sweepBtn').onclick=async()=>{
456
    if(!ow()){refreshStatus();return;}
457
    const ss=$('#sweepStatus'); const ids=['detect','getCurrentBlock','getBlockByNumber','getBalance','getAccount','getTransactions','rpcCall'];
458
    let ok=0,err=0;
459
    ss.textContent='прогон…';
460
    for(const id of ids){
461
      const m=REG.find(x=>x.id===id);
462
      const el=document.querySelector(`.m[data-id="${id}"]`);
463
      const btn=el.querySelector('button'), st=el.querySelector('.status'), out=el.querySelector('.out');
464
      await execCard(m,el,btn,st,out);
465
      st.querySelector('.led').classList.contains('ok') ? ok++ : err++;
466
      ss.textContent=`пройдено ${ok} · ошибок ${err}`;
467
      await new Promise(r=>setTimeout(r,120));
468
    }
469
    ss.innerHTML=`<span style="color:${err?'var(--warn)':'var(--ok)'}">готово · ${ok} ok · ${err} err</span>`;
470
    log('sweep', `read-only пакет: ${ok} ok, ${err} err`,'sys');
471
  };
472

    
473
  // ── wire provider events ──────────────────────────────────
474
  function bindEvents(){
475
    const o=ow(); if(!o||!o.on) return false;
476
    ['connect','disconnect','accountsChanged','networkChanged'].forEach(ev=>{
477
      o.on(ev,(d)=>{ log(ev, d===undefined?'':d, ev); refreshStatus(); });
478
    });
479
    return true;
480
  }
481
  // raw extension push events (на случай если on() ещё не готов)
482
  window.addEventListener('OrgonLinkEvent', e=>{
483
    const d=e.detail||{}; log(d.type||'event', d, d.type);
484
  });
485

    
486
  // ── boot ──────────────────────────────────────────────────
487
  function boot(){
488
    refreshStatus(); bindEvents();
489
    if(ow()) log('provider', 'detected v'+(ow().version||'?'),'sys');
490
  }
491
  if(window.orgonWeb){ boot(); }
492
  else {
493
    log('provider','ожидание инъекции…','sys');
494
    window.addEventListener('orgonWeb#initialized', boot, {once:true});
495
    window.addEventListener('tron#initialized', boot, {once:true});
496
    // fallback-поллинг 3с
497
    let n=0; const iv=setInterval(()=>{ if(window.orgonWeb){clearInterval(iv);boot();} else if(++n>30){clearInterval(iv);refreshStatus();} },100);
498
  }
499
})();
500
</script>
501
</body>
502
</html>
(9-9/10)