วิธีตรวจสอบการชำระเงิน USDT / USDC ด้วย Webhook และ RPC
สร้างระบบรับการชำระเงินและเคอร์เซอร์แบบโพลล์ ตรวจสอบสัญญาโทเค็น ผู้รับ และจำนวนเต็ม ขจัดเหตุการณ์ที่ซ้ำซ้อน และจัดการบล็อกที่หายไปหรือถูกแทนที่
สำหรับการตรวจสอบการชำระเงินด้วย stablecoin หรือการตรวจจับการฝากเงินของกระดานเทรด ให้ตรวจสอบการโอน ERC-20 USDT / USDC ขาเข้าบนเชน EVM โดยใช้ Webhook, WebSocket log หรือ HTTP polling ทั้งนักพัฒนาและ AI agent ต่างใช้ API เดียวกัน ให้เลือกเชน, สัญญาโทเค็น, ผู้รับ และระดับการยืนยันบล็อกก่อนประมวลผลการชำระเงิน เลือกลำดับขั้นตอนสำหรับการฝากเงิน การแจ้งเตือนร้านค้า หรือการจ่ายเงินได้ใน โซลูชันการตรวจสอบการโอน USDT / USDC
- ขั้นตอนแรก: สร้างการสมัครรับข้อมูลและเฝ้าดูผู้รับ โดยเริ่มต้นด้วย API key และตัวรับ HTTPS ของคุณ
- เสร็จสมบูรณ์เมื่อ: การโอนที่ตรงกันผ่านการตรวจสอบลายเซ็น, เชน, โทเค็น, ผู้รับ และจำนวนเต็ม แล้วถูกบันทึกเพียงครั้งเดียวในฐานะรายการชำระเงินที่รอยืนยัน และตัวรับส่งคืน HTTP
204; ตรวจสอบบนเชนตามนโยบายการยืนยันบล็อกของคุณก่อนทำการเพิ่มยอดเครดิต
เวิร์กโฟลว์การชำระเงินด้วย Stablecoin
การตรวจสอบการโอนขั้นพื้นฐานพร้อมใช้งานแล้ว การกรองจำนวนเงินและโทเค็นจะทำงานในตัวรับของคุณ ส่วนเงื่อนไขฝั่งเซิร์ฟเวอร์ ขั้นตอนการยืนยันหลายระดับ และการแจ้งเตือนผ่าน IM จะพร้อมใช้งานเร็วๆ นี้
สำหรับนักพัฒนาและ AI Agent: เริ่มต้นด้วย API key และตัวรับ HTTPS ของคุณเอง กรองสัญญาโทเค็นและจำนวนเงินในแอปพลิเคชันของคุณ คัดลอกการตั้งค่า Webhook
งานที่คู่มือนี้จะช่วยคุณดำเนินการ
- รับการแจ้งเตือนการชำระเงิน USDT / USDC ที่ HTTPS endpoint ของคุณหลังจากตรวจสอบการรองรับ Push สำหรับเชนที่เลือก
- ตรวจสอบความถูกต้องของรายการโอนที่รอยืนยัน โดยตรวจสอบเชน, สัญญาโทเค็น, ผู้รับ และจำนวนเต็ม ก่อนใช้นโยบายการตรวจสอบบนเชนและการยืนยันบล็อกของคุณ
- ดึงข้อมูล log การโอนที่ขาดหายย้อนหลัง ด้วยคำค้นหา
eth_getLogsที่จำกัดขอบเขตและเคอร์เซอร์ที่บันทึกไว้
เลือกระหว่าง Webhook, WebSocket หรือ polling
| เมธอด | เหมาะสำหรับ | การกู้คืนข้อมูล |
|---|---|---|
| Webhook | กิจกรรมของแอดเดรสที่ส่งไปยังตัวรับ HTTPS ของคุณ รวมถึงการโอนโทเค็นขาเข้า | ตรวจสอบลายเซ็น, ขจัด event ID ที่ซ้ำซ้อน และจัดการ subscription.gap / chain.reorg; เล่นซ้ำข้อมูลที่ตรงกันที่เก็บรักษาไว้ |
| WebSocket | กรอง logs ผ่านการเชื่อมต่อแบบต่อเนื่อง | เชื่อมต่อใหม่, สมัครรับข้อมูลซ้ำ และดึงข้อมูลบล็อกที่พลาดไปย้อนหลัง |
| HTTP polling | การตรวจสอบตามกำหนดเวลาหรือการดึง log ย้อนหลังด้วยเคอร์เซอร์ของคุณเอง | ค้นหาช่วง eth_getLogs ที่มีขอบเขตและบันทึกความคืบหน้าอย่างถาวร |
อ่าน ws และ subscriptions ในการตอบกลับสาธารณะของ GET /v1/chains ก่อนเลือก WebSocket การรองรับ Push เป็นการตรวจสอบแยกต่างหาก: ให้อ่าน GET /v1/push/chains โดยใช้ API key ของคุณ เชนที่ไม่มี WebSocket สามารถใช้ Webhook ของแอดเดรสได้หากมีระบุไว้ที่นั่น ใช้ polling เมื่อคุณต้องการสแกนบล็อกก่อนหน้าหรือทำงานโดยไม่มีการเชื่อมต่อแบบต่อเนื่อง
รับการชำระเงินด้วย Webhook
สร้างการสมัครรับข้อมูลและเฝ้าดูผู้รับ
รับ API key และดีพลอยตัวรับ HTTPS บนพอร์ต 443 เลือก CHAIN จากรายการเชนของ Push ที่ยืนยันตัวตนแล้ว กำหนดค่า RECIPIENT เป็นแอดเดรสรับเงินฝากของคุณ และกำหนด RECEIVER_URL เป็น URL ตัวรับของคุณ ตัวอย่างเชลล์นี้ต้องใช้ jq; {} จะใช้จำนวนการยืนยันเริ่มต้นของเชน ตรวจสอบ min_confirmations, default_confirmations และ max_confirmations ก่อนเลือกจำนวนอื่น โดย Push OpenAPI ได้กำหนดคำขอเหล่านี้ไว้
set -eu
umask 077
: "${BLOCKVECTRA_API_KEY:?Set your API key}"
: "${CHAIN:?Select a chain from the Push chain list}"
: "${RECIPIENT:?Set the watched EVM recipient address}"
: "${RECEIVER_URL:?Set your HTTPS receiver URL}"
PUSH_URL='https://api.blockvectra.com/v1/push'
curl --fail-with-body -sS "$PUSH_URL/chains" \
-H "x-api-key: $BLOCKVECTRA_API_KEY" > push-chains.json
jq -e --arg chain "$CHAIN" 'any(.chains[]; .chain == $chain)' push-chains.json
jq -n --arg url "$RECEIVER_URL" --arg chain "$CHAIN" \
'{url: $url, chains: {($chain): {}}}' > create.json
curl --fail-with-body -sS "$PUSH_URL/subscriptions" \
-H "x-api-key: $BLOCKVECTRA_API_KEY" \
-H 'Content-Type: application/json' -d @create.json > subscription.json
SUBSCRIPTION_ID=$(jq -er '.id' subscription.json)
jq -n --arg recipient "$RECIPIENT" '{addresses: [$recipient]}' > addresses.json
curl --fail-with-body -sS "$PUSH_URL/subscriptions/$SUBSCRIPTION_ID/addresses/add" \
-H "x-api-key: $BLOCKVECTRA_API_KEY" \
-H 'Content-Type: application/json' -d @addresses.json > address-change.json
curl --fail-with-body -sS "$PUSH_URL/subscriptions/$SUBSCRIPTION_ID" \
-H "x-api-key: $BLOCKVECTRA_API_KEY"การสร้างจะส่งคืน id และ secret ให้จัดเก็บ secret ไว้อย่างปลอดภัยสำหรับตัวรับ; subscription.json มีข้อมูลประจำตัวอยู่ ให้โพลล์การสมัครรับข้อมูลจนกระทั่ง applied_version >= change_version จาก address-change.json แล้วบันทึก chains[CHAIN].applied_from_block แอดเดรสใหม่จะจับคู่ตั้งแต่บล็อกนั้นเป็นต้นไป ดังนั้นให้ทำการโพลล์ต่อไปสำหรับช่วงการชำระเงินที่เกิดขึ้นก่อนหน้านั้น
ตรวจสอบลายเซ็น ขจัดรายการซ้ำ และตรวจสอบความถูกต้องของการชำระเงิน
บันทึก ฟังก์ชันลายเซ็นจาก raw-body เป็น verify-push.js ตัวรับด้านล่างนี้รับ Request ของ Web API ใน Node.js และอ่านไบต์ดั้งเดิมก่อนทำการแปลง JSON สร้าง secrets เป็น Map ของสตริง subscription ID ไปยัง secret ที่จัดเก็บไว้ กำหนดค่าคอนฟิกที่เชื่อถือได้ expected เป็น { chain, token, recipient, amountUnits }: โดย token คือสัญญา stablecoin ที่ได้รับการตรวจสอบแล้วบนเชนนั้น และ amountUnits คือจำนวนเต็มบวกที่คาดหวังในหน่วยที่เล็กที่สุด เปรียบเทียบจำนวนเงินด้วย BigInt ห้ามใช้ตัวเลขทศนิยมหรือสัญลักษณ์โทเค็นโดยเด็ดขาด
import { verifyPush } from './verify-push.js';
export function selectPayment(data, event, expected) {
if (data.chain !== expected.chain || event.type !== 'token.transfer' ||
event.standard !== 'erc20') return null;
const address = value => typeof value === 'string' && /^0x[0-9a-fA-F]{40}$/.test(value);
if (![event.token, event.to, expected.token, expected.recipient].every(address)) return null;
if (event.token.toLowerCase() !== expected.token.toLowerCase() ||
event.to.toLowerCase() !== expected.recipient.toLowerCase()) return null;
const integer = value => typeof value === 'string' && /^[1-9][0-9]{0,77}$/.test(value);
if (!integer(event.amount) || !integer(expected.amountUnits)) return null;
const amount = BigInt(event.amount);
if (amount >= (1n << 256n) || amount !== BigInt(expected.amountUnits)) return null;
if (typeof event.id !== 'string' || typeof event.ref !== 'string' ||
!/^0x[0-9a-f]{64}$/.test(event.tx_hash) ||
!/^0x[0-9a-f]{64}$/.test(event.block_hash) ||
!Number.isSafeInteger(event.log_index) || event.log_index < 0 ||
!Number.isSafeInteger(event.block_number) || event.block_number < 0) return null;
return {
eventId: event.id, ref: event.ref, chain: data.chain,
token: event.token, recipient: event.to, amountUnits: event.amount,
txHash: event.tx_hash, logIndex: event.log_index,
blockHash: event.block_hash, blockNumber: event.block_number,
};
}
export async function receivePayments(request, expected, secrets, store) {
const rawBody = Buffer.from(await request.arrayBuffer());
const headers = Object.fromEntries(request.headers);
if (!verifyPush(rawBody, headers, secrets)) return new Response(null, { status: 401 });
let message;
try { message = JSON.parse(rawBody.toString('utf8')); }
catch { return new Response(null, { status: 400 }); }
const data = message?.data;
if (message?.type !== 'push.events' ||
!Number.isSafeInteger(data?.subscription_id) || data.subscription_id <= 0 ||
String(data.subscription_id) !== headers['bv-subscription-id'] ||
data.chain !== expected.chain || !Array.isArray(data.events)) {
return new Response(null, { status: 400 });
}
try {
await store.transaction(async tx => {
for (const event of data.events) {
if (!event || typeof event.id !== 'string') continue;
const recovery = event.type === 'subscription.gap' || event.type === 'chain.reorg';
const payment = selectPayment(data, event, expected);
if (!recovery && !payment) continue;
if (!await tx.insertEventOnce(data.subscription_id, event)) continue;
if (recovery) await tx.enqueueRecovery(data.chain, event);
else await tx.recordPaymentCandidate(payment);
}
});
} catch {
return new Response(null, { status: 503 });
}
return new Response(null, { status: 204 });
}นำ store.transaction ไปใช้งานด้วยพื้นที่จัดเก็บข้อมูลที่คงทน ภายในการทำธุรกรรมเดียวกัน insertEventOnce จะแทรกเหตุการณ์ภายใต้คีย์ผสมที่ไม่ซ้ำ (subscription_id, event.id) และส่งคืน false หากซ้ำ; ให้คอมมิตพร้อมกันกับ recordPaymentCandidate หรือ enqueueRecovery ทำการย้อนกลับการเขียนทั้งหมดหากล้มเหลวเพื่อให้การลองใหม่สามารถประมวลผลเหตุการณ์ได้ งานกู้คืนข้อมูลจะต้องทำงานแบบ idempotent ด้วยเช่นกัน ส่งคืนรหัส 2xx ภายใน 10 วินาทีหลังจากคอมมิตแล้วเท่านั้น; บังคับใช้ขีดจำกัดขนาด body ที่ 1 MiB บนเซิร์ฟเวอร์ HTTP ของคุณ
ตัวอย่างนี้ตรวจสอบจำนวนเงินที่คาดหวังรายการเดียว สำหรับคำสั่งซื้อหลายรายการ ให้ค้นหาการกำหนดค่าการชำระเงินที่เชื่อถือได้ตามเชน โทเค็น และผู้รับ แล้วทำการกระทบยอดการชำระเงินบางส่วนหรือเงินส่วนเกินตามกฎของคุณเอง รายการที่รอยืนยันยังคงต้องผ่านการตรวจสอบบนเชนและนโยบายการยืนยันบล็อกของคุณก่อนเพิ่มเครดิต ข้ามระหว่างการสมัครรับข้อมูลและ polling ให้กระทบยอดการโอนเดียวกันตามเชน แฮชธุรกรรม และดัชนี log เพื่อไม่ให้เส้นทางการนำส่งสองเส้นทางเพิ่มเครดิตซ้ำกันสองครั้ง; เก็บแฮชของบล็อกไว้เพื่อติดตามบล็อกที่ถูกแทนที่
กู้คืนบล็อกที่ขาดหายหรือถูกแทนที่
สำหรับ subscription.gap ให้จัดคิวการสแกนตั้งแต่ from_block ถึง to_block โดยใช้เส้นทาง polling ด้านล่างหรือชุดข้อมูล Data API ที่มีอยู่ ส่วน chain.reorg คือการแจ้งเตือนฟรีว่าบล็อกที่นำส่งไปแล้วถูกแทนที่ ไม่ใช่ช่องว่างในการนำส่ง ให้ทำเครื่องหมายหรือละทิ้งเหตุการณ์เก่าในช่วงนั้นตาม ref; กระทบยอดบันทึกการชำระเงินตาม ref และ tx_hash กับเชนมาตรฐานก่อนที่จะประมวลผลเหตุการณ์มาตรฐานที่ส่งมอบใหม่อัตโนมัติด้วย ID ใหม่ ขจัดเหตุการณ์เหล่านั้นที่ซ้ำซ้อนตาม id การแจ้งเตือน reorg ไม่ได้เลื่อนความคืบหน้าที่เสร็จสมบูรณ์ไปข้างหน้า; ให้บันทึก complete_through_block แยกตามเชน ห้ามอนุมานความสมบูรณ์จากหมายเลขบล็อกของเหตุการณ์ที่ใหญ่ที่สุดเด็ดขาด
การเล่นซ้ำ (Replay) รับ chain และ from_block ภายในขอบเขต replayable_from_block ปัจจุบัน โดยจะส่งเฉพาะรายการที่ตรงกันที่เก็บรักษาไว้เท่านั้น; จะไม่สแกนช่วงเวลาก่อนที่จะเพิ่มแอดเดรสหรือเชน หรือช่วงเวลาที่การสมัครรับข้อมูลออฟไลน์ ให้รักษาเคอร์เซอร์ polling ไว้เพื่อครอบคลุมช่วงเวลาเหล่านั้นและช่องว่างที่หมดอายุ ข้อผิดพลาดของคำขอและช่วง replay ที่ไม่ถูกต้องมีอธิบายไว้ใน เอกสารอ้างอิงข้อผิดพลาด; ค่าบริการการนำส่ง ประวัติ และ address-day มีอธิบายไว้ใน กฎการเรียกเก็บเงิน
ส่วนที่เหลือจะนำเสนอการกรอง log ของ ERC-20 และการทำ polling แบบอิงตามเคอร์เซอร์สำหรับการตรวจสอบและการกู้คืนข้อมูล
Event การโอนและพารามิเตอร์การกรอง
สัญญาโทเค็น ERC-20 มาตรฐานจะส่ง event ต่อไปนี้ในทุกการโอน:
event Transfer(address indexed from, address indexed to, uint256 value);เมื่อเรียกใช้ eth_getLogs ให้ส่งแอดเดรสสัญญาโทเค็นและอาร์เรย์ topics เพื่อกรอง log ที่ตรงกัน:
| พารามิเตอร์ | ค่า | คำอธิบาย |
|---|---|---|
address | แอดเดรสสัญญาโทเค็น (หรืออาร์เรย์ของแอดเดรส) | แอดเดรสสัญญา stablecoin เป้าหมาย คุณสามารถระบุแอดเดรสเดียว (เช่น BSC USDT 0x55d398326f99059fF775485246999027B3197955, Base USDC 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913) หรืออาร์เรย์ของแอดเดรสเพื่อตรวจสอบหลายโทเค็นพร้อมกัน |
topics[0] | 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef | แฮชของลายเซ็น event: keccak256("Transfer(address,address,uint256)") |
topics[1] | null | แอดเดรสผู้ส่ง (from) เนื่องจากการตรวจสอบการฝากเงินยอมรับเงินจากกระเป๋าเงินของผู้ใช้รายใดก็ได้ ให้ส่ง null เพื่อจับคู่ผู้ส่งทุกราย |
topics[2] | แอดเดรสผู้รับขนาด 32 ไบต์ที่เติมศูนย์ด้านหน้า | แอดเดรสปลายทาง (to) ตามข้อกำหนด EVM log พารามิเตอร์แอดเดรสที่เป็น indexed จะใช้พื้นที่ 32 ไบต์ (64 อักขระฐานสิบหก) ให้เติมศูนย์ 12 ไบต์ (อักขระศูนย์ฐานสิบหก 24 ตัว) ด้านหน้าแอดเดรสผู้รับขนาด 20 ไบต์ เพื่อสร้าง topic ขนาด 32 ไบต์ |
fromBlock | บล็อกเริ่มต้น (เลขฐานสิบหก) | จุดเริ่มต้นของช่วงบล็อกการค้นหา (นับรวม) |
toBlock | บล็อกสิ้นสุด (เลขฐานสิบหก) | จุดสิ้นสุดของช่วงบล็อกการค้นหา (นับรวม) |
ฟิลด์ data ของออบเจกต์ log จะเข้ารหัส value ที่ไม่ได้ทำดัชนี (จำนวนเงินที่โอน) เป็น uint256 ฐานสิบหกขนาด 32 ไบต์ ให้หารจำนวนเงินดิบนี้ด้วย 10^decimals เพื่อให้ได้จำนวนโทเค็นในรูปแบบที่มนุษย์อ่านได้ (เช่น 18 decimals สำหรับ BSC USDT; 6 decimals สำหรับ Base และ Ethereum USDC)
การโพลล์ด้วยเคอร์เซอร์และขีดจำกัดช่วงบล็อก
บริการโพลล์จะสอบถามบล็อกใหม่เป็นระยะสม่ำเสมอ (เช่น ทุกๆ 3 ถึง 5 วินาที)
การเลื่อนเคอร์เซอร์ไปข้างหน้า
รักษาเคอร์เซอร์แบบคงทน last_polled_block (บล็อกสูงสุดที่ประมวลผลและคอมมิตแล้ว) ไว้ในฐานข้อมูลของคุณ:
- สำหรับแต่ละรอบการโพลล์ ให้ตั้งค่า
fromBlock = last_polled_block + 1 - สอบถามส่วนหัวของเชนปัจจุบันผ่าน
eth_blockNumberและคำนวณความสูงเป้าหมายที่ปลอดภัยsafe_headตาม confirmation depth ของคุณ - หาก
fromBlock <= safe_headให้สืบค้น log เป็นส่วนๆ จนถึงsafe_headหลังจากประมวลผลแต่ละ chunk สำเร็จแล้ว ให้เลื่อนเคอร์เซอร์ไปข้างหน้า
ขีดจำกัดช่วงบล็อก
ช่วงบล็อกของการเรียก eth_getLogs ครั้งเดียวจะคำนวณเป็น toBlock − fromBlock + 1 โดยต้องไม่เกิน max_logs_block_range ที่เผยแพร่สำหรับเชนนั้นใน GET /v1/chains
หากคำขอเกินช่วงนี้ บริการจะปฏิเสธคำขอด้วยรหัสข้อผิดพลาด -32602:
{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": -32602,
"message": "eth_getLogs block range too large: max 1000 blocks",
"data": {
"reason": "logs_range_too_large",
"docs_url": "https://docs.blockvectra.com/en/errors/#logs_range_too_large",
"retryable": false
}
}
}คำขอที่เกินช่วงบล็อกจะส่งคืนข้อผิดพลาด JSON-RPC -32602 (ไม่คิดค่าบริการ) ในตรรกะของแอปพลิเคชันของคุณ ให้อ่าน max_logs_block_range จาก GET /v1/chains และจำกัดส่วนการโพลล์แต่ละส่วน: chunk_end = min(fromBlock + max_logs_block_range - 1, safe_head)
การจัดการ Block Reorganization และ Confirmation Depth
บริเวณส่วนปลายของบล็อกเชน อาจเกิดการปรับโครงสร้างบล็อกชั่วคราว (reorgs) ได้ การเพิ่มเครดิตการชำระเงินที่ latest โดยไม่มี confirmation depth มีความเสี่ยงที่จะเพิ่มเครดิตให้ธุรกรรมบนกิ่งที่ถูกทิ้งในภายหลัง
ใช้มาตรการป้องกันต่อไปนี้เพื่อปกป้องกระบวนการประมวลผลการชำระเงิน:
Confirmation Depth
แทนที่จะสืบค้นไปจนถึง latest ให้สืบค้นไปจนถึงความสูงของบล็อกเป้าหมายที่ปลอดภัย:
safe_head = current_head - CONFIRMATION_DEPTH
กำหนดค่า CONFIRMATION_DEPTH ตามการยอมรับความเสี่ยงของแอปพลิเคชันของคุณ การสืบค้นจนถึง safe_head เท่านั้นจะช่วยให้มั่นใจได้ว่ามีเพียงบล็อกที่มีการยืนยันเพียงพอเท่านั้นที่จะได้รับการประมวลผล
การ Reorganize ระหว่างการทำ Polling
EVM JSON-RPC มาตรฐานจะตั้งค่า removed: true บนออบเจกต์ log เฉพาะในสตรีมการสมัครรับข้อมูล WebSocket logs เมื่อเหตุการณ์ที่ส่งออกมาก่อนหน้าเกิด revert เนื่องจากเกิด chain reorg เมื่อทำ polling ผ่าน HTTP ด้วย eth_getLogs คำค้นหาจะส่งคืน log จากเชนมาตรฐาน; log ที่เกิด reorg จะไม่ปรากฏในการสืบค้นครั้งถัดไป การทำ polling ภายใน safe_head จะช่วยรับประกันว่าการชำระเงินจะได้รับการประมวลผลเฉพาะบนบล็อกที่ได้รับการยืนยันอย่างเพียงพอแล้วเท่านั้น
การขจัดรายการซ้ำด้วย (transactionHash, logIndex)
ตัวรับฟังการชำระเงินต้องบังคับใช้คุณสมบัติ idempotency อย่างเข้มงวด:
- การโอนหลายรายการในธุรกรรมเดียว: ธุรกรรมเดียวสามารถมี event
Transferหลายรายการไปยังแอดเดรสรับเงินฝากเดียวกัน (เช่น ตัวเราเตอร์โทเค็นที่แบ่งการสวอป หรือสัญญาจ่ายเงินหลายรายการ) ข้อสำคัญ:transactionHashเพียงอย่างเดียวไม่ถือเป็นค่าเฉพาะสำหรับการชำระเงินแต่ละรายการ - การโพลล์ซ้ำซ้อนและการลองใหม่: เมื่อบริการโพลล์เริ่มต้นใหม่ กู้คืนจากข้อผิดพลาดของเครือข่ายชั่วคราว หรือกรอย้อนกลับหลายบล็อกเพื่อจัดการกับ reorg แบบตื้น log จากช่วงบล็อกเดียวกันจะถูกสอบถามหลายครั้ง
- ความเป็นเอกลักษณ์ของ Log Index:
logIndexระบุตำแหน่งสัมพัทธ์ของ event log ภายในบล็อก ตามข้อกำหนดของ EVM ตัวระบุเฉพาะแบบผสมที่เป็นมาตรฐานสำหรับเหตุการณ์คือ(transactionHash, logIndex)
ในสคีมาฐานข้อมูลเชิงสัมพันธ์ ให้ประกาศ composite unique index บนตารางบันทึกการฝากเงินของคุณ:
CREATE UNIQUE INDEX idx_transfers_tx_log ON deposit_records (transaction_hash, log_index);ก่อนประมวลผลการฝากเงิน ให้ตรวจสอบกับรายการ (transactionHash, logIndex) ที่มีอยู่ เพื่อรับประกันว่าการโอนบนเชนแต่ละรายการจะได้รับการเพิ่มเครดิตเพียงครั้งเดียวเท่านั้น
ตัวอย่างโค้ดฉบับสมบูรณ์
ตัวอย่างด้านล่างแสดงการดึงข้อมูลความสามารถของเครือข่ายจาก /v1/chains, การคำนวณช่วงบล็อกที่ปลอดภัย, การทำ polling log Transfer ของ stablecoin ตามขีดจำกัดของช่วง และการขจัดเหตุการณ์ที่ซ้ำซ้อน
import { createPublicClient, formatUnits, http, parseAbiItem } from "viem";
const apiKey = process.env.BLOCKVECTRA_API_KEY;
if (!apiKey) {
throw new Error("BLOCKVECTRA_API_KEY environment variable is not set");
}
const CHAIN = "bsc_mainnet";
const RPC_URL = "https://api.blockvectra.com/v1/bsc_mainnet";
const CHAINS_URL = "https://api.blockvectra.com/v1/chains";
// Target stablecoin contract address (BSC USDT used in this example)
const TOKEN_CONTRACT = "0x55d398326f99059fF775485246999027B3197955" as const;
const TOKEN_DECIMALS = 18;
// Monitored deposit address
const RECIPIENT_ADDRESS = "0xdded13D555B6DA811103cC1794D3d4330F69632C" as const;
// Confirmation depth to guard against chain reorgs
const CONFIRMATION_DEPTH = 15n;
// 1. Fetch chain capabilities from public metadata endpoint (unauthenticated, unbilled)
const chainsRes = await fetch(CHAINS_URL);
const { chains } = (await chainsRes.json()) as {
chains: Array<{
chain: string;
ws: boolean;
subscriptions: string[];
max_logs_block_range: number;
}>;
};
const chainConfig = chains.find((c) => c.chain === CHAIN);
if (!chainConfig) {
throw new Error(`Chain ${CHAIN} not found in /v1/chains`);
}
const maxLogsRange = BigInt(chainConfig.max_logs_block_range || 1000);
console.log(`Chain: ${CHAIN} | WebSocket supported: ${chainConfig.ws} | Max logs range: ${maxLogsRange}`);
// 2. Initialize viem client with x-api-key header
const client = createPublicClient({
transport: http(RPC_URL, {
fetchOptions: {
headers: { "x-api-key": apiKey },
},
}),
});
// Set to track processed events by composite key: (transactionHash, logIndex)
const processedLogs = new Set<string>();
// 3. Compute query range: subtract confirmation depth from current head
const currentHead = await client.getBlockNumber();
const safeHead = currentHead - CONFIRMATION_DEPTH;
// For demonstration, start cursor 10 blocks before safeHead
let cursor = safeHead > 10n ? safeHead - 10n : 0n;
console.log(`Current head: ${currentHead} | Safe head: ${safeHead} | Polling cursor: ${cursor}`);
while (cursor <= safeHead) {
const chunkEnd = cursor + maxLogsRange - 1n < safeHead ? cursor + maxLogsRange - 1n : safeHead;
const logs = await client.getLogs({
address: TOKEN_CONTRACT,
event: parseAbiItem(
"event Transfer(address indexed from, address indexed to, uint256 value)"
),
args: {
to: RECIPIENT_ADDRESS,
},
fromBlock: cursor,
toBlock: chunkEnd,
});
for (const log of logs) {
const dedupKey = `${log.transactionHash}-${log.logIndex}`;
if (processedLogs.has(dedupKey)) {
continue;
}
processedLogs.add(dedupKey);
const tokenAmount = formatUnits(log.args.value ?? 0n, TOKEN_DECIMALS);
console.log(
`[Payment Received] Amount: ${tokenAmount} | ` +
`Tx: ${log.transactionHash} | Log: ${log.logIndex} | Block: ${log.blockNumber}`
);
}
cursor = chunkEnd + 1n;
}
// Run with: npx tsx example.mtsกฎการเรียกเก็บเงินและคู่มือที่เกี่ยวข้อง
- สำหรับรายละเอียดเกี่ยวกับการวัดปริมาณคำขอ, ค่าน้ำหนัก CU และการพิจารณาการเรียกเก็บเงินตามรหัสข้อผิดพลาด โปรดดู กฎการเรียกเก็บเงิน: ข้อผิดพลาดและคำขอที่ไม่คิดค่าบริการ
- สำหรับคำแนะนำเชิงลึกเกี่ยวกับขีดจำกัดช่วงบล็อกของ
eth_getLogsและตรรกะการแบ่ง chunk โปรดดู ขีดจำกัดช่วงบล็อกและการสืบค้นแบบ Chunk ของ eth_getLogs - สำหรับความแตกต่างระหว่างการสืบค้นโหนด RPC แบบเรียลไทม์และ API ประวัติการโอนที่ทำดัชนีไว้ โปรดดู ข้อมูลส่วนหัวของเชนเทียบกับประวัติที่ทำดัชนี: เมื่อใดควรใช้ eth_getLogs เทียบกับ Transfers
ขั้นตอนถัดไป
- เลือกดูไดเรกทอรีชุดข้อมูล เพื่อดูชุดข้อมูลทั้งหมดที่ BlockVectra จัดทำดัชนี
- ดูแพ็กเกจฟรีและการกำหนดราคา เพื่อตรวจสอบสิทธิประโยชน์ในบัญชีของคุณ
- เข้าสู่ระบบคอนโซล เพื่อสร้าง API key
อัปเดตล่าสุด:
การลงทะเบียนแบบเป็นโปรแกรม
ลงทะเบียนและสร้าง API key แบบเป็นโปรแกรมด้วยลายเซ็นกระเป๋าเงิน Ethereum (EIP-191) โดยไม่ต้องใช้เบราว์เซอร์ สำหรับ AI agent, สคริปต์ และเวิร์กโฟลว์ CI
Logs เทียบกับ Transfers API
เลือก eth_getLogs สำหรับ event log ของสัญญา หรือเลือก Token Transfers API สำหรับประวัติการโอน ERC-20 ที่ทำดัชนีไว้ เปรียบเทียบช่วงบล็อก, การแบ่งหน้า, ความครอบคลุม และความเป็นที่สิ้นสุด (finality)