@cks-systems/manifest-sdk
Version:
TypeScript SDK for Manifest
328 lines (327 loc) • 14.5 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.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;
}