@cks-systems/manifest-sdk
Version:
TypeScript SDK for Manifest
186 lines (185 loc) • 7.86 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;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.FillFeed = void 0;
const ws_1 = __importDefault(require("ws"));
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;
wss;
shouldEnd = false;
ended = false;
lastUpdateUnix = Date.now();
constructor(connection) {
this.connection = connection;
this.wss = new ws_1.default.Server({ port: 1234 });
this.wss.on('connection', (ws) => {
console.log('New client connected');
ws.on('message', (message) => {
console.log(`Received message: ${message}`);
});
ws.on('close', () => {
console.log('Client disconnected');
});
});
}
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) {
await new Promise((f) => setTimeout(f, 10_000));
const signatures = await this.connection.getSignaturesForAddress(manifest_1.PROGRAM_ID, {
until: lastSignature,
}, 'finalized');
// Flip it so we do oldest first.
signatures.reverse();
// If there is only 1, do not use it because it could get stuck on the same sig.
if (signatures.length <= 1) {
continue;
}
for (const signature of signatures) {
// 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.wss.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;
}
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(fillDiscriminant)) {
continue;
}
const deserializedFillLog = FillLog_1.FillLog.deserialize(buffer.subarray(8))[0];
const resultString = JSON.stringify(toFillLogResult(deserializedFillLog, signature.slot, signature.signature));
console.log('Got a fill', resultString);
fills.inc({
market: deserializedFillLog.market.toString(),
isGlobal: deserializedFillLog.isMakerGlobal.toString(),
takerIsBuy: deserializedFillLog.takerIsBuy.toString(),
});
this.wss.clients.forEach((client) => {
client.send(JSON.stringify(toFillLogResult(deserializedFillLog, signature.slot, signature.signature)));
});
}
}
}
exports.FillFeed = FillFeed;
const fillDiscriminant = (0, discriminator_1.genAccDiscriminator)('manifest::logs::FillLog');
function toFillLogResult(fillLog, slot, signature) {
return {
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,
};
}