วิธีตรวจสอบการชำระเงิน 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

งานที่คู่มือนี้จะช่วยคุณดำเนินการ

เลือกระหว่าง 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 (บล็อกสูงสุดที่ประมวลผลและคอมมิตแล้ว) ไว้ในฐานข้อมูลของคุณ:

  1. สำหรับแต่ละรอบการโพลล์ ให้ตั้งค่า fromBlock = last_polled_block + 1
  2. สอบถามส่วนหัวของเชนปัจจุบันผ่าน eth_blockNumber และคำนวณความสูงเป้าหมายที่ปลอดภัย safe_head ตาม confirmation depth ของคุณ
  3. หาก 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 อย่างเข้มงวด:

  1. การโอนหลายรายการในธุรกรรมเดียว: ธุรกรรมเดียวสามารถมี event Transfer หลายรายการไปยังแอดเดรสรับเงินฝากเดียวกัน (เช่น ตัวเราเตอร์โทเค็นที่แบ่งการสวอป หรือสัญญาจ่ายเงินหลายรายการ) ข้อสำคัญ: transactionHash เพียงอย่างเดียวไม่ถือเป็นค่าเฉพาะสำหรับการชำระเงินแต่ละรายการ
  2. การโพลล์ซ้ำซ้อนและการลองใหม่: เมื่อบริการโพลล์เริ่มต้นใหม่ กู้คืนจากข้อผิดพลาดของเครือข่ายชั่วคราว หรือกรอย้อนกลับหลายบล็อกเพื่อจัดการกับ reorg แบบตื้น log จากช่วงบล็อกเดียวกันจะถูกสอบถามหลายครั้ง
  3. ความเป็นเอกลักษณ์ของ 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

ขั้นตอนถัดไป

อัปเดตล่าสุด:

ในหน้านี้