Solana Geyser gRPC - ตัวกรอง (Filter)

Solana Stream SDK
Solana Stream SDK เป็นซอฟต์แวร์โอเพนซอร์ส ดูรายละเอียดเพิ่มเติมได้ที่ GitHub repository ด้านล่าง

ภาพรวมของ gRPC filter

Solana Geyser gRPC ใช้ filter เพื่อดึงเฉพาะข้อมูลที่คุณสนใจได้อย่างมีประสิทธิภาพ เช่น account, โปรแกรม, transaction, slot และ block ที่เจาะจง
ด้านล่างนี้เป็นตัวอย่าง TypeScript ที่ใช้ Solana Stream SDK พร้อมอธิบายบทบาทของแต่ละ filter อย่างชัดเจน โครงสร้างและความหมายของ filter จะเหมือนกันเมื่อใช้ Rust

กฎ filter ปัจจุบันของ Shared

ในแพลน Shared ปัจจุบัน blocks map ที่ไม่ว่างทุกแบบใช้ได้เฉพาะกับ Dedicated แม้จะเป็น request ที่จำกัดขอบเขตด้วย accountInclude หรือโดยการไม่รวม transaction ก็ตาม ให้ใช้ transaction subscription หรือ blocksMeta เมื่อตอบโจทย์ได้ แต่ทั้งสองแบบไม่ได้เทียบเท่ากับ full block หากต้องการ full block ต้องใช้ stream ของ Dedicated Geyser gRPC
แต่ละ entry ของ account filter ต้องมี account, owner หรือ cuckoo filter ที่รองรับ เงื่อนไข memcmp หรือ dataSize เพียงอย่างเดียวไม่ถือเป็นการเลือก account shape ที่รองรับ ให้จำกัด request ด้วย selector อย่างใดอย่างหนึ่งข้างต้น หรือใช้ Dedicated Geyser gRPC เมื่อต้องการ stream ของ account ทั้งหมด

บทบาทและตัวอย่างของแต่ละ filter

Subscribe account

Subscribe เพื่อรับ update แบบเรียลไทม์ของ account ที่ระบุ ตัวอย่างต่อไปนี้ subscribe account OpenBook ของ SOL-USDC ที่ระดับ commitment Confirmed:
typescript
import { CommitmentLevel } from '@validators-dao/solana-stream-sdk'

const request = {
  slots: { slots: {} },
  accounts: {
    'wsol/usdc': {
      account: ['8BnEgHoWFysVcuFFX7QztDmzuH8r5ZFvyP3sYwn1XTh6'],
    },
  },
  transactions: {},
  blocks: {},
  blocksMeta: {},
  accountsDataSlice: [],
  commitment: CommitmentLevel.CONFIRMED,
}
  • "wsol/usdc" คือ label ที่ client กำหนดเอง
  • สามารถรวม filter หลายรายการสำหรับ account, โปรแกรม, block และ slot ไว้ใน JSON request เดียวได้

Subscribe account ด้วย account_data_slice

ตัวอย่างนี้แสดงการดึงข้อมูลของ account มาเพียงบางส่วน แทนที่จะดึงข้อมูลทั้งหมด (165 ไบต์) ของ USDC token account ตัวอย่างนี้ดึง 40 ไบต์โดยเริ่มที่ offset 32 ช่วงไบต์นี้มีข้อมูล เช่น owner และยอด lamports
typescript
import { CommitmentLevel } from '@validators-dao/solana-stream-sdk'

const request = {
  slots: {},
  accounts: {
    usdc: {
      owner: ['TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'],
      filters: [
        {
          tokenAccountState: true,
        },
        {
          memcmp: {
            offset: 0,
            data: {
              base58: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
            },
          },
        },
      ],
    },
  },
  transactions: {},
  blocks: {},
  blocksMeta: {},
  entry: {},
  commitment: CommitmentLevel.CONFIRMED,
  accountsDataSlice: [{ offset: 32, length: 40 }],
}

Subscribe โปรแกรม

ตัวอย่างนี้แสดงการ subscribe รับ update ของ account ที่เกี่ยวข้องกับโปรแกรมที่ระบุ
ด้านล่างนี้เรา subscribe รับ update ของ account ที่โปรแกรม Solend เป็นเจ้าของ ที่ระดับ commitment Processed
typescript
import { CommitmentLevel } from '@validators-dao/solana-stream-sdk'

const request = {
  slots: {
    slots: {},
  },
  accounts: {
    solend: {
      owner: ['So1endDq2YkqhipRh3WViPa8hdiSpxWy6z3Z6tMCpAo'],
    },
  },
  transactions: {},
  blocks: {},
  blocksMeta: {},
  accountsDataSlice: [],
  commitment: CommitmentLevel.PROCESSED,
}
  • "solend" คือ label ที่ client ตั้งได้อย่างอิสระ
  • หากต้องการ subscribe หลายโปรแกรม โปรดดูหัวข้อถัดไป "Subscribe หลายโปรแกรม"

Subscribe หลายโปรแกรม

ตัวอย่างนี้แสดงวิธี subscribe รับ update ของ account ที่เกี่ยวข้องกับหลายโปรแกรมในคราวเดียว
ตัวอย่างด้านล่าง subscribe รับ update ของ account ที่โปรแกรม Solend และ Serum เป็นเจ้าของ
typescript
import { CommitmentLevel } from '@validators-dao/solana-stream-sdk'

const request = {
  slots: {
    slots: {},
  },
  accounts: {
    programs: {
      owner: [
        'So1endDq2YkqhipRh3WViPa8hdiSpxWy6z3Z6tMCpAo',
        '9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin',
      ],
    },
  },
  transactions: {},
  blocks: {},
  blocksMeta: {},
  accountsDataSlice: [],
  commitment: CommitmentLevel.PROCESSED,
}
หากต้องการกำหนด label แยกให้แต่ละโปรแกรม ให้ใช้วิธีต่อไปนี้:
typescript
import { CommitmentLevel } from '@validators-dao/solana-stream-sdk'

const request = {
  slots: {
    slots: {},
  },
  accounts: {
    solend: {
      owner: ['So1endDq2YkqhipRh3WViPa8hdiSpxWy6z3Z6tMCpAo'],
    },
    serum: {
      owner: ['9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin'],
    },
  },
  transactions: {},
  blocks: {},
  blocksMeta: {},
  accountsDataSlice: [],
  commitment: CommitmentLevel.PROCESSED,
}

Subscribe transaction ทั้งหมดที่ finalized แล้วซึ่งไม่ใช่ vote และไม่ล้มเหลว

ตัวอย่างนี้แสดงการ subscribe transaction ทั้งหมดที่ระดับ commitment Finalized โดยไม่รวม vote transaction และ transaction ที่ล้มเหลว
typescript
import { CommitmentLevel } from '@validators-dao/solana-stream-sdk'

const request = {
  slots: {
    slots: {},
  },
  accounts: {},
  transactions: {
    alltxs: {
      vote: false,
      failed: false,
    },
  },
  blocks: {},
  blocksMeta: {},
  accountsDataSlice: [],
  commitment: CommitmentLevel.FINALIZED,
}
  • vote: false ไม่รวม vote transaction
  • failed: false ไม่รวม transaction ที่ล้มเหลว
  • หากเว้นฟิลด์ว่างไว้ จะได้รับ transaction ทั้งหมด
  • เมื่อระบุหลายฟิลด์ เงื่อนไขจะทำงานแบบ AND

Subscribe transaction ที่ไม่ใช่ vote ซึ่งอ้างถึง account

ตัวอย่างนี้แสดงการ subscribe transaction ที่เกี่ยวข้องกับ account ที่ระบุ โดยไม่รวม vote transaction
ตัวอย่างด้านล่าง subscribe transaction ที่ไม่ใช่ vote ซึ่งอ้างถึง account ที่เกี่ยวข้องกับโปรแกรม Serum
typescript
import { CommitmentLevel } from '@validators-dao/solana-stream-sdk'

const request = {
  slots: {
    slots: {},
  },
  accounts: {},
  transactions: {
    serum: {
      vote: false,
      accountInclude: ['9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin'],
    },
  },
  blocks: {},
  blocksMeta: {},
  accountsDataSlice: [],
  commitment: CommitmentLevel.PROCESSED,
}

Subscribe transaction โดยไม่รวม account ที่ระบุ

ตัวอย่างนี้แสดงการ subscribe transaction โดยไม่รวม transaction ที่เกี่ยวข้องกับ account ที่ระบุ
ตัวอย่างด้านล่างดึง transaction โดยไม่รวม account ใด ๆ ที่โปรแกรม Serum และ Tokenkeg เป็นเจ้าของ
typescript
import { CommitmentLevel } from '@validators-dao/solana-stream-sdk'

const request = {
  slots: {
    slots: {},
  },
  accounts: {},
  transactions: {
    serum: {
      accountExclude: [
        '9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin',
        'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA',
      ],
    },
  },
  blocks: {},
  blocksMeta: {},
  accountsDataSlice: [],
  commitment: CommitmentLevel.PROCESSED,
}

Subscribe transaction ที่อ้างถึง account และไม่รวม account บางรายการ

ตัวอย่างนี้แสดงการ subscribe transaction ที่เกี่ยวข้องกับ account บางรายการ พร้อมกับกำหนดอย่างชัดเจนให้ไม่รวม account อื่นที่ระบุไว้
ตัวอย่างด้านล่าง subscribe transaction ที่อ้างถึง account ของ Serum แต่ไม่รวม account ที่ระบุ:
typescript
import { CommitmentLevel } from '@validators-dao/solana-stream-sdk'

const request = {
  slots: {
    slots: {},
  },
  accounts: {},
  transactions: {
    serum: {
      accountInclude: ['9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin'],
      accountExclude: ['9wFFyRfZBsuAha4YcuxcXLKwMxJR43S7fPfQLusDBzvT'],
    },
  },
  blocks: {},
  blocksMeta: {},
  accountsDataSlice: [],
  commitment: CommitmentLevel.PROCESSED,
}

Subscribe transaction signature

ตัวอย่างนี้แสดงการ subscribe รับ update แบบเรียลไทม์ของ transaction signature ที่ระบุ จนกว่าจะถึงสถานะ Confirmed หรือ Finalized
typescript
import { CommitmentLevel } from '@validators-dao/solana-stream-sdk'

const request = {
  slots: {},
  accounts: {},
  transactions: {
    sign: {
      signature:
        '5rp2hL9b6kexex11Mugfs3vfU9GhieKruj4CkFFSnu52WLxiGn4VcLLwsB62XURhMmT1j4CZiXT6FFtYbXsLq2Zs',
    },
  },
  blocks: {},
  blocksMeta: {},
  accountsDataSlice: [],
  commitment: CommitmentLevel.PROCESSED,
}

Subscribe slot

ตัวอย่างนี้แสดงการ subscribe รับการแจ้งเตือน slot ที่เข้ามา ไม่ต้องระบุรายละเอียดอื่นนอกจากการตั้งชื่อ tag เอง:
typescript
import { CommitmentLevel } from '@validators-dao/solana-stream-sdk'

const request = {
  slots: {
    incoming_slots: {},
  },
  accounts: {},
  transactions: {},
  blocks: {},
  blocksMeta: {},
  accountsDataSlice: [],
  commitment: CommitmentLevel.PROCESSED,
}

Subscribe block (ในแพลน Shared ปัจจุบันใช้ได้เฉพาะกับ Dedicated)

Subscribe รับ update แบบเรียลไทม์ของ block ทั้งหมดที่ถูกสร้างขึ้น โดยค่าเริ่มต้นจะได้รับ transaction ทั้งหมดภายใน block:
typescript
import { CommitmentLevel } from '@validators-dao/solana-stream-sdk'

const request = {
  slots: {},
  accounts: {},
  transactions: {},
  blocks: {
    blocks: {},
  },
  blocksMeta: {},
  accountsDataSlice: [],
  commitment: CommitmentLevel.PROCESSED,
}

หากต้องการไม่รวม transaction และรับเฉพาะข้อมูล account ที่อัปเดต (ในแพลน Shared ปัจจุบันใช้ได้เฉพาะกับ Dedicated):

typescript
import { CommitmentLevel } from '@validators-dao/solana-stream-sdk'

const request = {
  slots: {},
  accounts: {},
  transactions: {},
  blocks: {
    blocks: {
      includeTransactions: false,
      includeAccounts: true,
    },
  },
  blocksMeta: {},
  accountsDataSlice: [],
  commitment: CommitmentLevel.PROCESSED,
}

หากต้องการรับเฉพาะ transaction/account ที่เกี่ยวข้องกับ account ที่ระบุ (ในแพลน Shared ปัจจุบันใช้ได้เฉพาะกับ Dedicated):

typescript
import { CommitmentLevel } from '@validators-dao/solana-stream-sdk'

const request = {
  slots: {},
  accounts: {},
  transactions: {},
  blocks: {
    blocks: {
      accountInclude: ['So1endDq2YkqhipRh3WViPa8hdiSpxWy6z3Z6tMCpAo'],
    },
  },
  blocksMeta: {},
  accountsDataSlice: [],
  commitment: CommitmentLevel.PROCESSED,
}

Subscribe block metadata

Subscribe รับเฉพาะการแจ้งเตือน block metadata เมื่อ block ถูกประมวลผล โดยไม่มีข้อมูล transaction โดยละเอียด
typescript
import { CommitmentLevel } from '@validators-dao/solana-stream-sdk'

const request = {
  slots: {},
  accounts: {},
  transactions: {},
  blocks: {},
  blocksMeta: {
    blockmetadata: {},
  },
  accountsDataSlice: [],
  commitment: CommitmentLevel.PROCESSED,
}

การจัดการระดับ commitment

Stream ของ Solana Geyser gRPC ใช้ระดับ commitment Processed เป็นค่าเริ่มต้น
คุณสามารถระบุระดับ commitment ที่สูงขึ้น เช่น Confirmed หรือ Finalized ได้ ในกรณีนี้ Geyser จะ buffer ข้อมูลไว้และส่งการแจ้งเตือนเมื่อถึงระดับ commitment ที่ระบุ
เพื่อประสิทธิภาพสูงสุด แนะนำให้จัดการระดับ commitment ที่ฝั่ง client
วิธีระบุระดับ commitment มีดังนี้:
typescript
import { CommitmentLevel } from '@validators-dao/solana-stream-sdk'

enum CommitmentLevel {
  PROCESSED = 0,
  CONFIRMED = 1,
  FINALIZED = 2,
}
  • PROCESSED: ข้อมูลที่ได้ทันทีหลังการประมวลผล ดึงได้เร็ว แต่ยังไม่ได้รับการยืนยัน
  • CONFIRMED: ข้อมูลที่ cluster ยืนยันแล้ว จึงมีความแน่นอนมากขึ้น
  • FINALIZED: ข้อมูลที่ finalized อย่างสมบูรณ์ ไม่มีความเสี่ยงที่จะถูก reorganize

ข้อดีของการทำงานที่ระดับ Processed

ข้อดีหลักของระดับ commitment Processed คือได้รับ transaction ทันที ทำให้ client ประมวลผลได้อย่างรวดเร็ว จากนั้น client สามารถตรวจจับการเปลี่ยนเป็น Confirmed หรือ Finalized ได้ จึงตอบสนองได้รวดเร็ว

วิธีจัดการ Confirmed และ Finalized

เมื่อใช้ระดับ Confirmed หรือ Finalized ควร buffer event (transaction หรือ update ของ account) แยกตาม slot
ให้ buffer event แยกทีละ slot พร้อม subscribe การแจ้งเตือนของ slot แล้วปล่อย event ออกจาก buffer เมื่อ slot นั้นถึงระดับ commitment ที่ต้องการ (Confirmed หรือ Finalized)
ในตอนแรก event จะมาถึงก่อนที่ slot จะถึง Confirmed หรือ Finalized

ลักษณะเฉพาะของ Finalized

ตามข้อกำหนดของ Solana Geyser ไม่ใช่ทุก slot จะได้รับการแจ้งเตือน finalized อย่างชัดเจน ดังนั้นเมื่อได้รับการแจ้งเตือน finalized ของ slot ใด คุณต้องถือว่า slot บรรพบุรุษ (ancestor) ทั้งหมดของ slot นั้น finalized แล้ว แม้จะไม่ได้รับการแจ้งเตือนอย่างชัดเจนก็ตาม
กล่าวคือ เมื่อได้รับการแจ้งเตือน finalized ให้ถือย้อนหลังว่า slot บรรพบุรุษทั้งหมด finalized แล้ว