|
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,'"')}">${f.v||''}</textarea>`
|
|
391
|
: `<input data-f="${f.n}" value="${f.v||''}" placeholder="${(f.ph||'').replace(/"/g,'"')}">`;
|
|
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=>({'&':'&','<':'<','>':'>'}[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>
|