shine-solana/shine/programs/shine_payments/oracle_check/index.html

256 lines
10 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Проверка оракула Pyth (Devnet/Mainnet/Testnet)</title>
<style>
:root {
color-scheme: dark;
--bg: #0f1218;
--panel: #171b24;
--text: #e8edf6;
--muted: #97a3b8;
--line: #2a3242;
--ok: #55d48a;
--warn: #ffbf5e;
--err: #ff7d7d;
--btn: #273247;
--btn-hover: #32415c;
--code: #1e2633;
}
* { box-sizing: border-box; }
body { font-family: Arial, sans-serif; margin: 20px; background: var(--bg); color: var(--text); }
.wrap { width: 100%; max-width: 1800px; }
.panel { border: 1px solid var(--line); border-radius: 8px; padding: 14px; margin-bottom: 14px; background: var(--panel); }
.row { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; margin: 8px 0; }
input { padding: 9px 10px; min-width: 340px; border: 1px solid var(--line); border-radius: 8px; background: #131824; color: var(--text); }
button { padding: 9px 14px; border: 1px solid var(--line); border-radius: 8px; cursor: pointer; background: var(--btn); color: var(--text); }
button:hover { background: var(--btn-hover); }
.muted { color: var(--muted); }
.ok { color: var(--ok); }
.warn { color: var(--warn); }
.err { color: var(--err); white-space: pre-wrap; }
code { background: var(--code); padding: 2px 4px; border-radius: 4px; }
table { width: 100%; border-collapse: collapse; margin-top: 10px; }
th, td { border: 1px solid var(--line); padding: 6px; text-align: left; vertical-align: top; }
</style>
</head>
<body>
<div class="wrap">
<h1>Диагностика оракула Pyth (SOL/USD)</h1>
<div class="panel">
<div class="muted">
Страница нужна, чтобы проверить, что именно возвращает аккаунт оракула в разных сетях и где ломается парсинг.
</div>
<div class="muted">
Проверяются три сети: <code>devnet</code>, <code>mainnet-beta</code>, <code>testnet</code>.
</div>
</div>
<div class="panel">
<h3>Настройки</h3>
<div class="row">
<label>Feed ID (hex с 0x):<br /><input id="feedId" value="0xef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b56d" /></label>
</div>
<div class="row">
<label>Oracle account (devnet):<br /><input id="oracleDevnet" value="7UVimffxr9ow1uXYxsr4LHAcV58mLzhmwaeKvJ1pjLiE" /></label>
</div>
<div class="row">
<label>Oracle account (mainnet-beta):<br /><input id="oracleMainnet" value="7UVimffxr9ow1uXYxsr4LHAcV58mLzhmwaeKvJ1pjLiE" /></label>
</div>
<div class="row">
<label>Oracle account (testnet):<br /><input id="oracleTestnet" value="7UVimffxr9ow1uXYxsr4LHAcV58mLzhmwaeKvJ1pjLiE" /></label>
</div>
<div class="row">
<button id="runBtn">Проверить все сети</button>
</div>
<div class="muted">
Если для сети аккаунт не существует, это тоже покажется в отчёте.
</div>
</div>
<div class="panel">
<h3>Результаты</h3>
<div id="out" class="muted">Нажмите «Проверить все сети».</div>
</div>
</div>
<script>
const NETWORKS = [
{ name: "devnet", rpc: "https://api.devnet.solana.com", inputId: "oracleDevnet" },
{ name: "mainnet-beta", rpc: "https://api.mainnet-beta.solana.com", inputId: "oracleMainnet" },
{ name: "testnet", rpc: "https://api.testnet.solana.com", inputId: "oracleTestnet" },
];
function readU64(data, offset) {
let x = 0n;
for (let i = 0; i < 8; i++) x |= BigInt(data[offset + i]) << (8n * BigInt(i));
return x;
}
function readI64(data, offset) {
let x = readU64(data, offset);
if (x > 0x7fffffffffffffffn) x -= 0x10000000000000000n;
return x;
}
function readU32(data, offset) {
return data[offset] | (data[offset + 1] << 8) | (data[offset + 2] << 16) | (data[offset + 3] << 24);
}
function readI32(data, offset) {
let x = readU32(data, offset);
if (x > 0x7fffffff) x -= 0x100000000;
return x;
}
function toHex(bytes) {
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
}
function trimZeros(v) {
return v.replace(/(\.\d*?[1-9])0+$/u, "$1").replace(/\.0+$/u, "").replace(/\.$/u, "");
}
function priceToFloatStr(price, exponent) {
const p = Number(price);
if (!Number.isFinite(p)) return "NaN";
return trimZeros((p * Math.pow(10, exponent)).toFixed(12));
}
function fmtErr(e) {
const s = String(e?.message || e || "unknown error");
return s.length > 350 ? s.slice(0, 350) + "..." : s;
}
function parseWithOffsets(data, priceOffset, exponentOffset, publishOffset) {
const price = readI64(data, priceOffset);
const exponent = readI32(data, exponentOffset);
const publishTime = readI64(data, publishOffset);
return { price, exponent, publishTime, valueStr: priceToFloatStr(price, exponent) };
}
function parseFeedIdFromLikelyPosition(data) {
// Для текущего PriceUpdateV2 в аккаунте receiver:
// 0..7 discriminator, 8..39 write_authority, 40..40 verification enum, 41..72 feed_id.
if (data.length < 73) return null;
const feedRaw = data.slice(41, 73);
return "0x" + toHex(feedRaw);
}
async function rpcGetAccountInfo(rpcUrl, pubkey) {
const body = {
jsonrpc: "2.0",
id: 1,
method: "getAccountInfo",
params: [pubkey, { encoding: "base64", commitment: "confirmed" }],
};
const res = await fetch(rpcUrl, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(body),
});
if (!res.ok) throw new Error(`RPC HTTP ${res.status}`);
const json = await res.json();
if (json.error) throw new Error(json.error.message || JSON.stringify(json.error));
return json.result?.value || null;
}
async function fetchHermesPrice(feedIdHex) {
const url = `https://hermes.pyth.network/v2/updates/price/latest?ids[]=${encodeURIComponent(feedIdHex)}`;
const res = await fetch(url);
if (!res.ok) throw new Error(`Hermes HTTP ${res.status}`);
const json = await res.json();
const p = json?.parsed?.[0]?.price;
if (!p) throw new Error("Hermes: price not found");
return {
price: BigInt(p.price),
expo: Number(p.expo),
publishTime: BigInt(p.publish_time),
valueStr: priceToFloatStr(BigInt(p.price), Number(p.expo)),
};
}
async function runCheck() {
const out = document.getElementById("out");
out.textContent = "Проверка...";
const feedId = document.getElementById("feedId").value.trim().toLowerCase();
const rows = [];
let hermes = null;
let hermesErr = null;
try {
hermes = await fetchHermesPrice(feedId);
} catch (e) {
hermesErr = fmtErr(e);
}
for (const n of NETWORKS) {
const oracle = document.getElementById(n.inputId).value.trim();
try {
const ai = await rpcGetAccountInfo(n.rpc, oracle);
if (!ai) {
rows.push({
network: n.name,
status: "account not found",
details: "Аккаунт оракула не найден в этой сети",
});
continue;
}
const data = Uint8Array.from(atob(ai.data[0]), c => c.charCodeAt(0));
const feedFromData = parseFeedIdFromLikelyPosition(data);
const parsedCurrentUi = parseWithOffsets(data, 74, 90, 94); // как сейчас в UI проекта
const parsedShiftMinus1 = parseWithOffsets(data, 73, 89, 93); // диагностический вариант
const currentUiValid = parsedCurrentUi.price > 0n;
const shiftValid = parsedShiftMinus1.price > 0n;
const feedMatch = feedFromData === feedId;
rows.push({
network: n.name,
status: "ok",
details: `
<div>RPC: <code>${n.rpc}</code></div>
<div>Account owner: <code>${ai.owner}</code>, data len: <b>${data.length}</b></div>
<div>Feed ID (из аккаунта): <code>${feedFromData || "n/a"}</code> ${feedMatch ? '<span class="ok">совпадает</span>' : '<span class="warn">НЕ совпадает</span>'}</div>
<div>Парсер UI (offsets 74/90/94): price=<code>${parsedCurrentUi.price.toString()}</code>, exp=<code>${parsedCurrentUi.exponent}</code>, value=<b>${parsedCurrentUi.valueStr}</b> ${currentUiValid ? '<span class="ok">valid</span>' : '<span class="err">invalid</span>'}</div>
<div>Альт. парсер (offsets 73/89/93): price=<code>${parsedShiftMinus1.price.toString()}</code>, exp=<code>${parsedShiftMinus1.exponent}</code>, value=<b>${parsedShiftMinus1.valueStr}</b> ${shiftValid ? '<span class="ok">valid</span>' : '<span class="err">invalid</span>'}</div>
${hermes
? `<div>Hermes (эталон): price=<code>${hermes.price.toString()}</code>, exp=<code>${hermes.expo}</code>, value=<b>${hermes.valueStr}</b></div>`
: `<div class="warn">Hermes недоступен: ${hermesErr}</div>`
}
`,
});
} catch (e) {
rows.push({
network: n.name,
status: "error",
details: `<span class="err">${fmtErr(e)}</span>`,
});
}
}
out.innerHTML = `
<table>
<thead>
<tr>
<th>Сеть</th>
<th>Статус</th>
<th>Детали</th>
</tr>
</thead>
<tbody>
${rows.map((r) => `
<tr>
<td><b>${r.network}</b></td>
<td>${r.status === "ok" ? '<span class="ok">ok</span>' : (r.status === "account not found" ? '<span class="warn">not found</span>' : '<span class="err">error</span>')}</td>
<td>${r.details}</td>
</tr>
`).join("")}
</tbody>
</table>
`;
}
document.getElementById("runBtn").addEventListener("click", runCheck);
</script>
</body>
</html>