Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion functions/api/og/shard.js
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,9 @@ async function uploadToStorage(env, shard, bytes) {
indexer,
root: root || null,
txSeq: txSeq === null ? null : clampInt(txSeq, 0, Number.MAX_SAFE_INTEGER),
scanUrl: root ? `${GALILEO.storageScan}/file/${root}` : null,
// storagescan 是用提交序號定位的(/submission/<txSeq>),沒有 /file/<root>。
// gateway 有回序號才給得出連結,否則留 null,不要給一個 404 的網址。
scanUrl: txSeq === null ? null : `${GALILEO.storageScan}/submission/${txSeq}`,
Comment on lines 153 to +156
response: body,
};
} catch (err) {
Expand Down
55 changes: 50 additions & 5 deletions functions/api/og/storage.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,47 @@ async function indexerNodes(indexer) {
throw new Error(errors.join(' | ').slice(0, 160));
}

/**
* indexer 把答案包在一層信封裡:
*
* {"code":0,"message":"Success","data":{"tx":{…,"seq":150104},"finalized":true,…}}
*
* 原本這裡直接讀 body.finalized,那永遠是 undefined,於是 found 一直是 false ——
* 「跟 indexer 對一次」這個確認靜靜地失敗了很久,只是被上傳成功的畫面蓋過去。
* 有 data 就剝掉,沒有就當作已經是內容本身(不同版本回過沒包信封的形狀)。
* code 不是 0 代表 indexer 自己說失敗,這時不要把 data 當成有效答案。
*/
function unwrap(body) {
if (!body || typeof body !== 'object') return null;
if (body.data && typeof body.data === 'object') {
return body.code === undefined || body.code === 0 ? body.data : null;
}
return body;
}

/**
* storagescan 的檔案頁是用「提交序號」定位的,不是 root hash:
*
* https://storagescan-galileo.0g.ai/submission/<txSeq>
*
* 這個站沒有 /file/<root> 這種路徑(我們原本這樣組,點下去是 404)。
* 序號來自 indexer 回的檔案資訊 —— 0g-storage-node 把它放在 tx.seq,
* 不同版本也出現過 txSeq / seq 這幾種寫法,所以都認一下。
* 拿不到就回 null,讓前端照實把按鈕留成不可按,而不是給一個會 404 的連結。
*/
function seqOf(body) {
const cands = [body && body.tx && body.tx.seq, body && body.tx && body.tx.txSeq, body && body.txSeq, body && body.seq];
for (const v of cands) {
// 先擋掉 null / undefined / 空字串 —— Number(null) 是 0,會讓「查不到」
// 變成一個看起來合法的 /submission/0 連結。
if (typeof v !== 'number' && typeof v !== 'string') continue;
if (v === '') continue;
const n = Number(v);
if (Number.isInteger(n) && n >= 0) return n;
}
return null;
}

export async function onRequestGet({ request, env }) {
const url = new URL(request.url);
const root = String(url.searchParams.get('root') || '').trim();
Expand All @@ -95,15 +136,19 @@ export async function onRequestGet({ request, env }) {

try {
const info = await getJson(`${indexer.replace(/\/+$/, '')}/file/info/${root}`);
const found = Boolean(info.ok && info.body && info.body.finalized !== undefined);
const file = unwrap(info.body);
const found = Boolean(info.ok && file && file.finalized !== undefined);
const txSeq = found ? seqOf(file) : null;
return json({
indexer,
root,
found,
finalized: found ? Boolean(info.body.finalized) : null,
pruned: found ? Boolean(info.body.pruned) : null,
info: info.body,
scanUrl: `${GALILEO.storageScan}/file/${root}`,
finalized: found ? Boolean(file.finalized) : null,
pruned: found ? Boolean(file.pruned) : null,
size: found && Number.isFinite(Number(file.tx && file.tx.size)) ? Number(file.tx.size) : null,
txSeq,
Comment on lines +146 to +149
info: file,
scanUrl: txSeq === null ? null : `${GALILEO.storageScan}/submission/${txSeq}`,
});
} catch (err) {
return json(
Expand Down
65 changes: 49 additions & 16 deletions public/js/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -781,22 +781,13 @@ async function uploadToStorage() {
note.textContent = `0G Storage root ${shortHash(root)}${tx ? ` submit tx ${shortHash(String(tx))}` : ''}`;
renderTracks({ storage: { uploaded: true, root } });

const link = $('link-storage');
link.href = `https://storagescan-galileo.0g.ai/file/${root}`;
link.classList.remove('is-disabled');
link.removeAttribute('aria-disabled');

// 存完再跟 indexer 對一次,確認節點真的收下並完成同步
try {
const info = await OG.storageInfo(root);
if (info && info.found) {
renderTracks({ storage: { uploaded: true, root, finalized: info.finalized } });
note.textContent = `已存進 0G Storage${info.finalized ? '(已 finalized)' : '(同步中)'} root ${shortHash(root)}`;
if (info.scanUrl) link.href = info.scanUrl;
}
} catch {
// indexer 查詢失敗不影響「已上傳」這件事,畫面維持上一段的結果
}
// 「在 0G Storage 上查看」要等真的查得到才開放。
//
// 之前是上傳完就直接把連結指到 storagescan 的 /file/<root>,結果點下去 404 ——
// 那個站根本沒有這條路徑,檔案頁是用提交序號定位的(/submission/<txSeq>),
// 而序號要跟 indexer 查了才知道。所以改成查到序號才解鎖按鈕;查不到就照實
// 留成不可按,不要給玩家一個會 404 的連結。
await pollStorage(root, note);
} catch (err) {
btn.textContent = '存進 0G Storage';
btn.disabled = false;
Expand All @@ -809,6 +800,48 @@ async function uploadToStorage() {
}
}

/**
* 上傳完之後跟 indexer 要這個檔案的提交序號,拿到才解鎖 storagescan 連結。
*
* 為什麼要輪詢:segment 傳完不等於索引好。節點要先把 log entry 同步進來,
* indexer 才答得出 tx.seq。實測這中間有幾秒到十幾秒,馬上查通常是空的。
*
* 每一輪都照實更新畫面上那句話,不要讓玩家對著沒反應的按鈕猜。
* 輪完還是沒有就維持不可按,並說明檔案已經上傳、只是還沒被索引到。
*/
async function pollStorage(root, note, attempts = 6) {
const link = $('link-storage');

for (let i = 0; i < attempts; i++) {
let info = null;
try {
info = await OG.storageInfo(root);
} catch {
// indexer 查詢失敗不影響「已上傳」這件事,下一輪再試
}

if (info && info.found) {
renderTracks({ storage: { uploaded: true, root, finalized: info.finalized } });
note.textContent = `已存進 0G Storage${info.finalized ? '(已 finalized)' : '(同步中)'} root ${shortHash(root)}`;

if (info.scanUrl) {
link.href = info.scanUrl;
link.classList.remove('is-disabled');
link.removeAttribute('aria-disabled');
return true;
}
}

if (i < attempts - 1) {
note.textContent = `已存進 0G Storage root ${shortHash(root)} 等待索引…(第 ${i + 1} 次查詢)`;
await sleep(3000);
}
}

note.textContent = `已存進 0G Storage root ${shortHash(root)} storagescan 還沒索引到這筆,稍後可用 root 自行查詢`;
return false;
}

/**
* 賽道三的收尾:把剛送出去的交易從 0G Chain 讀回來,重新解析 calldata 並比對摘要。
* 「錢包沒報錯」不算證明,讀得回來、摘要對得上才算。
Expand Down