UNPKG

@cks-systems/manifest-sdk

Version:
261 lines (260 loc) 11.6 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.FillFeedBlockSub = void 0; const FillLog_1 = require("./manifest/accounts/FillLog"); const manifest_1 = require("./manifest"); const promClient = __importStar(require("prom-client")); const fillFeed_1 = require("./fillFeed"); const WebSocketManager_1 = require("./utils/WebSocketManager"); // For live monitoring of the fill feed. For a more complete look at fill // history stats, need to index all trades. const fills = new promClient.Counter({ name: 'fills_block', help: 'Number of fills from block processing', labelNames: ['market', 'isGlobal', 'takerIsBuy'], }); /** * FillFeedBlockSub - Processes blocks sequentially using getBlock to find Manifest program transactions */ class FillFeedBlockSub { connection; wsManager; shouldEnd = false; ended = false; lastUpdateUnix = Date.now(); currentSlot = 0; blockProcessingDelay = 100; // 100ms delay between iterations constructor(connection, wsPort = 1234) { this.connection = connection; this.wsManager = new WebSocketManager_1.WebSocketManager(wsPort, 30000); } msSinceLastUpdate() { return Date.now() - this.lastUpdateUnix; } async stop() { this.shouldEnd = true; // Wait for processing to finish gracefully const start = Date.now(); while (!this.ended) { const timeout = 10_000; const pollInterval = 500; if (Date.now() - start > timeout) { console.warn('Force stopping block processing after timeout'); break; } await new Promise((resolve) => setTimeout(resolve, pollInterval)); } // Close WebSocket server this.wsManager.close(); this.ended = true; } /** * Start processing blocks sequentially */ async start() { try { // Get the current slot to start processing from this.currentSlot = await this.connection.getSlot('finalized'); console.log(`Starting block processing from slot ${this.currentSlot}`); while (!this.shouldEnd) { try { // TODO: If the last one had too many, dont waste time with getSlot and just get a bunch. // Get the latest finalized slot const latestSlot = await this.connection.getSlot('finalized'); // Determine which slots need to be processed const slotsToProcess = []; for (let slot = this.currentSlot; slot <= latestSlot; slot++) { slotsToProcess.push(slot); } if (slotsToProcess.length === 0) { // No new slots to process, continue immediately continue; } console.log(`Fetching ${slotsToProcess.length} blocks in parallel (${this.currentSlot} to ${latestSlot})`); // Fetch all blocks in parallel const blockPromises = slotsToProcess.map((slot) => this.connection.getBlock(slot, { maxSupportedTransactionVersion: 0, transactionDetails: 'full', commitment: 'finalized', })); const blocks = await Promise.all(blockPromises); // Process blocks in order for (let i = 0; i < blocks.length; i++) { const slot = slotsToProcess[i]; const block = blocks[i]; if (!block) { // Block doesn't exist or is not finalized yet continue; } console.log(`Processing block ${slot} with ${block.transactions.length} transactions`); for (const tx of block.transactions) { if (tx.meta?.err !== null) { // Skip failed transactions continue; } // Check if this transaction involves the Manifest program const hasManifestProgram = this.transactionInvolvesManifestProgram(tx); if (!hasManifestProgram) { continue; } await this.processTransaction(tx, slot, block.blockTime); } this.lastUpdateUnix = Date.now(); } // Update current slot to continue from the next unprocessed slot this.currentSlot = latestSlot + 1; } catch (error) { console.error(`Error processing blocks from ${this.currentSlot}:`, error); // On error, move forward one slot and add a longer delay this.currentSlot++; await new Promise((resolve) => setTimeout(resolve, this.blockProcessingDelay * 3)); } } } catch (error) { console.error('Fatal error in block processing:', error); } finally { console.log('FillFeedBlockSub ended'); this.ended = true; } } /** * Check if a transaction involves the Manifest program * This checks account keys and addresses loaded from lookup tables */ transactionInvolvesManifestProgram(tx) { if (!tx.transaction?.message) { return false; } const message = tx.transaction.message; const programId = manifest_1.PROGRAM_ID.toBase58(); // Check legacy transaction format if ('accountKeys' in message) { const inAccountKeys = message.accountKeys.some((key) => key.toBase58() === programId); if (inAccountKeys) { return true; } } // Check versioned transaction format if ('staticAccountKeys' in message) { const inAccountKeys = message.staticAccountKeys.some((key) => key.toBase58() === programId); if (inAccountKeys) { return true; } } // Check addresses loaded from address lookup tables (ALTs) if (tx.meta?.loadedAddresses) { const loadedAddresses = tx.meta.loadedAddresses; if (loadedAddresses.writable) { const inWritable = loadedAddresses.writable.some((key) => (typeof key === 'string' ? key : key.toBase58()) === programId); if (inWritable) { return true; } } if (loadedAddresses.readonly) { const inReadonly = loadedAddresses.readonly.some((key) => (typeof key === 'string' ? key : key.toBase58()) === programId); if (inReadonly) { return true; } } } return false; } /** * Process a single transaction from a block */ async processTransaction(tx, slot, blockTime) { const signature = tx.transaction.signatures[0]; console.log('Handling transaction', signature, 'slot', slot); if (!tx.meta?.logMessages) { console.log('No log messages'); return; } // Extract signers from the transaction let originalSigner; let signers = []; let accountKeysStr = []; try { const message = tx.transaction.message; if ('accountKeys' in message) { // Legacy transaction accountKeysStr = message.accountKeys.map((key) => key.toBase58()); originalSigner = accountKeysStr[0]; // Extract all signers using isAccountSigner method signers = message.accountKeys .map((key, index) => ({ key, index })) .filter(({ index }) => message.isAccountSigner(index)) .map(({ key }) => key.toBase58()); } else { // Versioned transaction (v0) - use staticAccountKeys accountKeysStr = message.staticAccountKeys.map((key) => key.toBase58()); originalSigner = accountKeysStr[0]; // Extract all signers using isAccountSigner method signers = message.staticAccountKeys .map((key, index) => ({ key, index })) .filter(({ index }) => message.isAccountSigner(index)) .map(({ key }) => key.toBase58()); } } catch (error) { console.error('Error extracting signers:', error); return; } const aggregator = (0, fillFeed_1.detectAggregatorFromKeys)(accountKeysStr); const originatingProtocol = (0, fillFeed_1.detectOriginatingProtocolFromKeys)(accountKeysStr); const messages = tx.meta.logMessages; const programDatas = messages.filter((message) => { return message.includes('Program data:'); }); if (programDatas.length === 0) { console.log('No program datas'); return; } for (const programDataEntry of programDatas) { const programData = programDataEntry.split(' ')[2]; const byteArray = Uint8Array.from(atob(programData), (c) => c.charCodeAt(0)); const buffer = Buffer.from(byteArray); if (!buffer.subarray(0, 8).equals(fillFeed_1.fillDiscriminant)) { continue; } const deserializedFillLog = FillLog_1.FillLog.deserialize(buffer.subarray(8))[0]; const fillResult = (0, fillFeed_1.toFillLogResult)(deserializedFillLog, slot, signature, originalSigner, aggregator, originatingProtocol, signers, blockTime ?? undefined); const resultString = JSON.stringify(fillResult); console.log('Got a fill', resultString); fills.inc({ market: deserializedFillLog.market.toString(), isGlobal: deserializedFillLog.isMakerGlobal.toString(), takerIsBuy: deserializedFillLog.takerIsBuy.toString(), }); // Send to all connected clients this.wsManager.broadcast(JSON.stringify(fillResult)); } } } exports.FillFeedBlockSub = FillFeedBlockSub;