יישום חוקר בלוקצ'יין סוהר (Turnkey)
תארו לעצמכם: אתם משיקים בלוקצ'יין חדש או sidechain. משתמשים רוצים לעקוב אחרי העסקאות שלהם, אבל בלי חוקר (explorer) זה בלתי אפשרי. בניית שירות כזה מאפס היא לא טריוויאלית: צריך לסנכרן צומת ארכיון, לאינדקס כל בלוק, וליישם חיפוש מהיר לפי hashes וכתובות. עשינו את זה עבור 8 פרויקטים ואנחנו מכירים את כל המלכודות: מ-reorgs ועד לירידה בביצועים ככל שהשרשרת גדלה. פעם סטארטאפ פנה אלינו—החוקר שלהם המבוסס על The Graph קרס תחת 50 בקשות בשנייה; נאלצנו לשכתב אותו לחלוטין.
במאמר זה, אני אראה כיצד הגישה שלנו עובדת: אינדקסר על PostgreSQL, API על Next.js, ופענוח חוזים חכמים. תלמדו כיצד להימנע מטעויות נפוצות ולקצר את זמן הפיתוח. תבינו גם למה השקעה בפתרון מותאם אישית משתלמת תוך חצי שנה של פעילות פעילה. יישמנו 8 חוקרי בלוקצ'יין עבור רשתות EVM שונות, כולל כמה sidechains עם תכונות מותאמות אישית.
רכיבי המערכת
Блокчейн-нода (archive)
├── RPC/WebSocket (eth_getBlock, eth_getTransaction...)
│
Индексатор (собственный или The Graph)
│ Читает блоки → парсит транзакции → сохраняет
│
PostgreSQL + Redis (кеш)
│
REST API / GraphQL
│
Frontend (Next.js)
├── Поиск (tx hash, address, block)
├── Список блоков
├── Детали транзакции
├── Профиль адреса (баланс + история)
└── Декодирование смарт-контракт вызововלפעולה תקינה, נדרש צומת ארכיון. למידע נוסף ראו תיעוד הפיתוח של Ethereum.
כיצד להבטיח זמן תגובה מינימלי לחיפוש Hash?
המפתח הוא אינדוקס מסד נתונים נכון וקאשינג. אנו משתמשים ב-PostgreSQL עם אינדקסים על Блокчейн-нода (archive) | ├── RPC/WebSocket (eth_getBlock, eth_getTransaction...) | Индексатор (собственный или The Graph) | Читает блоки → парсит транзакции → сохраняет | PostgreSQL + Redis (кеш) | REST API / GraphQL | Frontend (Next.js) ├── Поиск (tx hash, address, block) ├── Список блоков ├── Детали транзакции ├── Профиль адреса (баланс + история) └── Декодирование смарт-контракт вызовов ו-hash, וב-Redis לנתונים חמים (בלוקים אחרונים, כתובות פופולריות). טעות אופיינית היא לפנות לצומת בכל חיפוש. אנו תמיד מכניסים שכבת קאשינג; אחרת, TTFB יכול לעלות על 2 שניות.
תחת עומס של 100+ בקשות בשנייה, הפתרון שלנו שומר על זמן תגובה ממוצע מתחת ל-200 אלפיות השנייה. זה מושג באמצעות חימום מוקדם של הקאש ופאג'ינג של שאילתות בלוקים.
למה אינדקסר מותאם אישית עדיף על פתרונות מוכנים?
אינדקסרים מוכנים (The Graph, Moralis) נוחים להתחלה, אבל מטילים מגבלות: תלות בשרתים חיצוניים, חוסר יכולת לכוונון עדין. השוואה:
| מאפיין | אינדקסר מוכן | אינדקסר מותאם אישית |
|---|---|---|
| שליטה | נמוכה – רק subgraph | מלאה – לוגיקה משלכם |
| מהירות | תלויה ברשת האינדקסר | מותאמת לנתונים שלכם |
| עלות | חינם (self-host) או בתשלום | פיתוח חד-פעמי + אירוח |
| גמישות | מוגבלת (סכמת GraphQL) | כל שדות ויחסים |
| עקומת למידה | קיימת | נדרש מפתח |
לייצור עם עומס גבוה (מעל 100 בקשות/שנייה), אנו תמיד ממליצים על אינדקסר מותאם אישית. הוא מספק ביצועים צפויים וחופש להרחבה. לפי הנתונים שלנו, מעבר מפתרון מוכן לפתרון מותאם אישית מפחית את עלויות התשתית ב-30–40%.
כיצד לאינדקס בלוקים: שלושה שלבים
- הגדרת צומת וחיבור. פרסו צומת ארכיון (Geth/Nethermind) או השתמשו בספק WebSocket. ודאו שהצומת מוכן להחזיר בלוקים מלאים עם עסקאות.
-
יישום האינדקסר. באמצעות ספריית
block_number, הירשמו לבלוקים חדשים ופרסו אותם ל-PostgreSQL. חשוב לטפל ב-reorgs—שמרו את ה-hash של הבלוק הקודם ובדקו שוב על fork. - עיבוד לאחר. לאחר הכנסת עסקאות, עדכנו סטטיסטיקות כתובות (יתרות, מספר עסקאות) והכניסו נתונים חמים ל-Redis.
import { createPublicClient, webSocket } from 'viem';
import { mainnet } from 'viem/chains';
const client = createPublicClient({
chain: mainnet,
transport: webSocket(process.env.ETH_WS_URL)
});
class BlockIndexer {
async indexBlock(blockNumber: bigint): Promise<void> {
const block = await client.getBlock({ blockNumber, includeTransactions: true });
await db.transaction(async (trx) => {
await trx('blocks').insert({
number: Number(block.number),
hash: block.hash,
parent_hash: block.parentHash,
timestamp: new Date(Number(block.timestamp) * 1000),
miner: block.miner,
gas_used: block.gasUsed.toString(),
gas_limit: block.gasLimit.toString(),
transaction_count: block.transactions.length,
base_fee_per_gas: block.baseFeePerGas?.toString() ?? null
});
for (const tx of block.transactions) {
await trx('transactions').insert({
hash: tx.hash,
block_number: Number(tx.blockNumber),
from_address: tx.from.toLowerCase(),
to_address: tx.to?.toLowerCase() ?? null,
value: tx.value.toString(),
gas: tx.gas.toString(),
gas_price: tx.gasPrice?.toString() ?? null,
max_fee_per_gas: tx.maxFeePerGas?.toString() ?? null,
input: tx.input,
nonce: tx.nonce,
transaction_index: tx.transactionIndex
});
await this.updateAddressStats(trx, tx.from.toLowerCase());
if (tx.to) await this.updateAddressStats(trx, tx.to.toLowerCase());
}
});
}
async watchNewBlocks(): Promise<void> {
const unwatch = client.watchBlocks({
onBlock: async (block) => {
await this.indexBlock(block.number);
},
onError: (error) => {
logger.error('Block watch error', error);
}
});
const latestIndexed = await this.getLatestIndexedBlock();
const currentBlock = await client.getBlockNumber();
for (let i = latestIndexed + 1n; i <= currentBlock; i++) {
await this.indexBlock(i);
}
}
} API: חיפוש
app.get('/api/search', async (req, res) => {
const query = req.query.q as string;
if (!query) return res.status(400).json({ error: 'Query required' });
if (/^0x[0-9a-f]{64}$/i.test(query)) {
const tx = await db('transactions').where('hash', query.toLowerCase()).first();
if (tx) return res.json({ type: 'transaction', data: tx });
const block = await db('blocks').where('hash', query.toLowerCase()).first();
if (block) return res.json({ type: 'block', data: block });
} else if (/^0x[0-9a-f]{40}$/i.test(query)) {
return res.json({ type: 'address', address: query.toLowerCase() });
} else if (/^\d+$/.test(query)) {
const block = await db('blocks').where('number', parseInt(query)).first();
if (block) return res.json({ type: 'block', data: block });
}
res.json({ type: 'not_found' });
});
Frontend: פענוח נתוני קלט
import { decodeFunctionData } from 'viem';
async function decodeTransactionInput(
input: string,
contractAddress: string
): Promise<DecodedInput | null> {
if (input === '0x') return null;
const abi = await getContractAbi(contractAddress);
if (!abi) return { raw: input };
try {
const decoded = decodeFunctionData({ abi, data: input as `0x${string}` });
return {
functionName: decoded.functionName,
args: decoded.args,
raw: input,
};
} catch {
return { raw: input };
}
} מה כלול בפיתוח חוקר
| רכיב | תוצאה |
|---|---|
| תיעוד ארכיטקטורה | דיאגרמת ER, מפרט API, תיאור זרימת נתונים |
| אינדקסר | קוד מקור ב-TypeScript (או Python), קונטיינר Docker לפריסה |
| Backend | REST/GraphQL API עם קאשינג (Redis), תיעוד OpenAPI |
| Frontend | יישום Next.js עם חיפוש, רשימת בלוקים, פרופיל כתובת ופענוח |
| בדיקות | בדיקות עומס (k6) ודוח ביצועים |
| תיעוד צוות | הוראות להפעלה, הגדרה וניטור |
| תמיכה טכנית | שבועיים לאחר ההשקה: תיקוני באגים וסיוע בהתאמה |
תהליך העבודה
אנו מחלקים את הפרויקט לחמישה שלבים:
| שלב | משך | תוצאה |
|---|---|---|
| אנליטיקה | 2-3 ימים | מסמך ארכיטקטורה |
| עיצוב | 3-5 ימים | סכמת DB, API, אבטיפוס |
| יישום | 2-4 שבועות | אינדקסר עובד + API |
| בדיקות | 5-7 ימים | דוח בדיקת עומס |
| פריסה וניטור | 2-3 ימים | השקת ייצור |
במהלך שלב האנליטיקה, אנו מבהירים אילו נתונים אתם צריכים: רק בלוקים ועסקאות, או תמונה מלאה עם קריאות פנימיות ויומני אירועים. העיצוב כולל הכנת סכמת מסד נתונים (בדרך כלל כ-15–20 טבלאות) ומפרט API בפורמט OpenAPI.
לוחות זמנים משוערים
- חוקר MVP (עסקאות, בלוקים, כתובות) ללא אינדקסר (דרך RPC) – 2–3 שבועות.
- אינדקסר מלא עם PostgreSQL + API + Frontend – 6–10 שבועות.
- חוקר לרשת EVM מותאמת אישית – החל מחודשיים.
עלויות התשתית תלויות בגודל הרשת ונקבעות בנפרד. צרו קשר כדי לקבל הערכה ראשונית לפרויקט שלכם.
טעויות אופייניות בבניית חוקר בלוקצ'יין
- חוסר backfill לאחר כשל באינדקסר – נתונים אובדים. פתרון: שמרו את הבלוק האחרון שעובד ב-DB והמשיכו משם בהפעלה מחדש.
-
התעלמות מ-reorgs – בלוקים יכולים להשתנות. הירשמו לאירועי
viemדרך WebSocket ובדקו hashes. - סריקת כל הבלוקים דרך RPC ללא פאג'ינג – עומס יתר על הצומת. השתמשו בטעינת אצווה עם השהיות.
אם נתקלתם באחת מהבעיות הללו או רוצים להימנע מהן מההתחלה, בקשו ייעוץ—נעזור לכם לתכנן חוקר יציב.
מה הלאה?
אינדקסר מותאם אישית הוא השקעה בביצועים ואמינות. אין תלות בשירותים חיצוניים: הנתונים שלכם תמיד בשליטה. הזמינו פיתוח—נציע ארכיטקטורה לרשת שלכם תוך יום עסקים אחד.







