@cks-systems/manifest-sdk
Version:
TypeScript SDK for Manifest
261 lines (260 loc) • 11.6 kB
JavaScript
;
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;