UNPKG

@cks-systems/manifest-sdk

Version:
328 lines (327 loc) 14.5 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.fillDiscriminant = exports.ORIGINATING_PROTOCOL_IDS = exports.AGGREGATOR_PROGRAM_IDS = exports.FillFeed = void 0; exports.detectAggregatorFromKeys = detectAggregatorFromKeys; exports.detectOriginatingProtocolFromKeys = detectOriginatingProtocolFromKeys; exports.toFillLogResult = toFillLogResult; const WebSocketManager_1 = require("./utils/WebSocketManager"); const FillLog_1 = require("./manifest/accounts/FillLog"); const manifest_1 = require("./manifest"); const numbers_1 = require("./utils/numbers"); const discriminator_1 = require("./utils/discriminator"); const promClient = __importStar(require("prom-client")); // 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', help: 'Number of fills', labelNames: ['market', 'isGlobal', 'takerIsBuy'], }); /** * FillFeed example implementation. */ class FillFeed { connection; wsManager; shouldEnd = false; ended = false; lastUpdateUnix = Date.now(); constructor(connection) { this.connection = connection; this.wsManager = new WebSocketManager_1.WebSocketManager(1234, 30000); } msSinceLastUpdate() { return Date.now() - this.lastUpdateUnix; } async stopParseLogs() { this.shouldEnd = true; const start = Date.now(); while (!this.ended) { const timeout = 30_000; const pollInterval = 500; if (Date.now() - start > timeout) { return Promise.reject(new Error(`failed to stop parseLogs after ${timeout / 1_000} seconds`)); } await new Promise((resolve) => setTimeout(resolve, pollInterval)); } return Promise.resolve(); } /** * Parse logs in an endless loop. */ async parseLogs(endEarly) { // Start with a hopefully recent signature. const lastSignatureStatus = (await this.connection.getSignaturesForAddress(manifest_1.PROGRAM_ID, { limit: 1 }, 'finalized'))[0]; let lastSignature = lastSignatureStatus.signature; let lastSlot = lastSignatureStatus.slot; // End early is 30 seconds, used for testing. const endTime = endEarly ? new Date(Date.now() + 30_000) : new Date(Date.now() + 1_000_000_000_000); // TODO: remove endTime in favor of stopParseLogs for testing while (!this.shouldEnd && new Date(Date.now()) < endTime) { // This sleep was originally implemented to wait until there was enough // transactions to avoid just spamming the RPC. Reduced to just // enough to avoid RPC spam, but not wait too long since the router // integrations give us steady flow. await new Promise((f) => setTimeout(f, 400)); const signatures = await this.connection.getSignaturesForAddress(manifest_1.PROGRAM_ID, { until: lastSignature, }, 'finalized'); // Flip it so we do oldest first. signatures.reverse(); // Process even single signatures, but handle the edge case differently if (signatures.length === 0) { continue; } // If we only got back the same signature we already processed, skip it if (signatures.length === 1 && signatures[0].signature === lastSignature) { continue; } for (const signature of signatures) { // Skip if we already processed this signature if (signature.signature === lastSignature) { continue; } // Separately track the last slot. This is necessary because sometimes // gsfa ignores the until param and just gives 1_000 signatures. if (signature.slot < lastSlot) { continue; } await this.handleSignature(signature); } console.log('New last signature:', signatures[signatures.length - 1].signature, 'New last signature slot:', signatures[signatures.length - 1].slot, 'num sigs', signatures.length); lastSignature = signatures[signatures.length - 1].signature; lastSlot = signatures[signatures.length - 1].slot; this.lastUpdateUnix = Date.now(); } console.log('ended loop'); this.wsManager.close(); this.ended = true; } /** * Handle a signature by fetching the tx onchain and possibly sending a fill * notification. */ async handleSignature(signature) { console.log('Handling', signature.signature, 'slot', signature.slot); const tx = await this.connection.getTransaction(signature.signature, { maxSupportedTransactionVersion: 0, }); if (!tx?.meta?.logMessages) { console.log('No log messages'); return; } if (tx.meta.err != null) { console.log('Skipping failed tx', signature.signature); return; } // Extract the original signer (fee payer/first signer) and all signers let originalSigner; let signers; try { const message = tx.transaction.message; if ('accountKeys' in message) { // Legacy transaction originalSigner = message.accountKeys[0]?.toBase58(); // 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 for the main accounts originalSigner = message.staticAccountKeys[0]?.toBase58(); // 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); } const aggregator = detectAggregator(tx); const originatingProtocol = detectOriginatingProtocol(tx); 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(exports.fillDiscriminant)) { continue; } const deserializedFillLog = FillLog_1.FillLog.deserialize(buffer.subarray(8))[0]; const fillResult = toFillLogResult(deserializedFillLog, signature.slot, signature.signature, originalSigner, aggregator, originatingProtocol, signers, // ?? undefined because can be null or undefined signature.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(), }); this.wsManager.broadcast(JSON.stringify(fillResult)); } } } exports.FillFeed = FillFeed; // Constants for known aggregators and protocols exports.AGGREGATOR_PROGRAM_IDS = { MEXkeo4BPUCZuEJ4idUUwMPu4qvc9nkqtLn3yAyZLxg: 'Swissborg', T1TANpTeScyeqVzzgNViGDNrkQ6qHz9KrSBS4aNXvGT: 'Titan', '6m2CDdhRgxpH4WjvdzxAYbGxwdGUz5MziiL5jek2kBma': 'OKX', proVF4pMXVaYqmy4NjniPh4pqKNfMmsihgd4wdkCX3u: 'OKX', DF1ow4tspfHX9JwWJsAb9epbkA8hmpSEAtxXy1V27QBH: 'DFlow', JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4: 'Jupiter', SPURp82qAR9nvzy8j1gP31zmzGytrgDBKcpGzeGkka8: 'Spur', s7SunwrPG5SbViEKiViaDThPRJxkkTrNx2iRPN3exNC: 'Bitget', }; exports.ORIGINATING_PROTOCOL_IDS = { LiMoM9rMhrdYrfzUCxQppvxCSG1FcrUK9G8uLq4A1GF: 'kamino', UMnFStVeG1ecZFc2gc5K3vFy3sMpotq8C91mXBQDGwh: 'cabana', BQ72nSv9f3PRyRKCBnHLVrerrv37CYTHm5h3s9VSGQDV: 'jupiter', // JUP 1 '2MFoS3MPtvyQ4Wh4M9pdfPjz6UhVoNbFbGJAskCPCj3h': 'jupiter', // JUP 2 HU23r7UoZbqTUuh3vA7emAGztFtqwTeVips789vqxxBw: 'jupiter', // JUP 3 '6LXutJvKUw8Q5ue2gCgKHQdAN4suWW8awzFVC6XCguFx': 'jupiter', // JUP 5 CapuXNQoDviLvU1PxFiizLgPNQCxrsag1uMeyk6zLVps: 'jupiter', // JUP 6 GGztQqQ6pCPaJQnNpXBgELr5cs3WwDakRbh1iEMzjgSJ: 'jupiter', // JUP 7 '9nnLbotNTcUhvbrsA6Mdkx45Sm82G35zo28AqUvjExn8': 'jupiter', // JUP 8 '6U91aKa8pmMxkJwBCfPTmUEfZi6dHe7DcFq2ALvB2tbB': 'jupiter', // JUP 12 '4xDsmeTWPNjgSVSS1VTfzFq3iHZhp77ffPkAmkZkdu71': 'jupiter', // JUP 14 HFqp6ErWHY6Uzhj8rFyjYuDya2mXUpYEk8VW75K9PSiY: 'jupiter', // JUP 16 '9yj3zvLS3fDMqi1F8zhkaWfq8TZpZWHe6cz1Sgt7djXf': 'phantom', '8psNvWTrdNTiVRNzAgsou9kETXNJm2SXZyaKuJraVRtf': 'phantom', B3111yJCeHBcA1bizdJjUFPALfhAfSRnAbJzGUtnt56A: 'binance', }; // Helper function to detect aggregator from account keys function detectAggregatorFromKeys(accountKeys) { for (const account of accountKeys) { const aggregator = exports.AGGREGATOR_PROGRAM_IDS[account]; if (aggregator) { return aggregator; } } return undefined; } // Helper function to detect originating protocol from account keys function detectOriginatingProtocolFromKeys(accountKeys) { for (const accountKey of accountKeys) { const protocol = exports.ORIGINATING_PROTOCOL_IDS[accountKey]; if (protocol) { return protocol; } } return undefined; } function detectAggregator(tx) { // Look for the aggregator program id from a list of known ids. try { // For versioned transactions, we need to handle both static and resolved account keys const message = tx.transaction.message; // Handle both legacy and versioned transactions if ('accountKeys' in message) { // Legacy transaction const accountKeysStr = message.accountKeys.map((k) => k.toBase58()); return detectAggregatorFromKeys(accountKeysStr); } else { // V0 transaction - use staticAccountKeys directly to avoid lookup resolution issues const accountKeysStr = message.staticAccountKeys.map((k) => k.toBase58()); return detectAggregatorFromKeys(accountKeysStr); } } catch (error) { console.warn('Error detecting aggregator:', error); // Fall back to undefined if we can't detect the aggregator } return undefined; } function detectOriginatingProtocol(tx) { try { const message = tx.transaction.message; // Handle both legacy and versioned transactions if ('accountKeys' in message) { // Legacy transaction const accountKeysStr = message.accountKeys.map((k) => k.toBase58()); return detectOriginatingProtocolFromKeys(accountKeysStr); } else { // V0 transaction - use staticAccountKeys directly to avoid lookup resolution issues const accountKeysStr = message.staticAccountKeys.map((k) => k.toBase58()); return detectOriginatingProtocolFromKeys(accountKeysStr); } } catch (error) { console.warn('Error detecting originating protocol:', error); // Fall back to undefined if we can't detect the originating protocol } return undefined; } exports.fillDiscriminant = (0, discriminator_1.genAccDiscriminator)('manifest::logs::FillLog'); function toFillLogResult(fillLog, slot, signature, originalSigner, aggregator, originatingProtocol, signers, blockTime) { const result = { market: fillLog.market.toBase58(), maker: fillLog.maker.toBase58(), taker: fillLog.taker.toBase58(), baseAtoms: fillLog.baseAtoms.inner.toString(), quoteAtoms: fillLog.quoteAtoms.inner.toString(), priceAtoms: (0, numbers_1.convertU128)(fillLog.price.inner), takerIsBuy: fillLog.takerIsBuy, isMakerGlobal: fillLog.isMakerGlobal, makerSequenceNumber: fillLog.makerSequenceNumber.toString(), takerSequenceNumber: fillLog.takerSequenceNumber.toString(), signature, slot, }; if (originalSigner) { result.originalSigner = originalSigner; } if (aggregator) { result.aggregator = aggregator; } if (originatingProtocol) { result.originatingProtocol = originatingProtocol; } if (signers && signers.length > 0) { result.signers = signers; } if (blockTime !== undefined) { result.blockTime = blockTime; } return result; }