Skip to content

Instantly share code, notes, and snippets.

@rubpy
Created November 5, 2024 21:58
Show Gist options
  • Save rubpy/8bdfa5baaa2c461fe5d83a5b9e42deb9 to your computer and use it in GitHub Desktop.
Save rubpy/8bdfa5baaa2c461fe5d83a5b9e42deb9 to your computer and use it in GitHub Desktop.
import web3 from "@solana/web3.js";
import * as BufferLayout from "@solana/buffer-layout";
import QuickLRU from "quick-lru";
////////////////////////////////////////////////////////////////////////////////
export class BorshBoolean extends BufferLayout.Layout<boolean> {
constructor(property?: string) {
super(1, property);
}
decode(b: Buffer, offset = 0): boolean {
return !!Buffer.from(b.buffer, b.byteOffset, b.length)[offset];
}
encode(src: boolean, b: Buffer, offset = 0): number {
Buffer.from(b.buffer, b.byteOffset, b.length).writeUInt8(src ? 1 : 0, offset);
return this.span;
}
}
export class BorshUInt64LE extends BufferLayout.Layout<bigint> {
constructor(property?: string) {
super(8, property);
}
decode(b: Buffer, offset = 0): bigint {
return Buffer.from(b.buffer, b.byteOffset, b.length).readBigUInt64LE(offset);
}
encode(src: bigint, b: Buffer, offset = 0): number {
Buffer.from(b.buffer, b.byteOffset, b.length).writeBigUInt64LE(BigInt(src), offset);
return this.span;
}
}
export class BorshInt64LE extends BufferLayout.Layout<bigint> {
constructor(property?: string) {
super(8, property);
}
decode(b: Buffer, offset = 0): bigint {
return Buffer.from(b.buffer, b.byteOffset, b.length).readBigInt64LE(offset);
}
encode(src: bigint, b: Buffer, offset = 0): number {
Buffer.from(b.buffer, b.byteOffset, b.length).writeBigInt64LE(BigInt(src), offset);
return this.span;
}
}
class BorshPublicKey extends BufferLayout.Layout<web3.PublicKey> {
constructor(property?: string) {
super(32, property);
}
decode(b: Uint8Array, offset?: number): web3.PublicKey {
offset = offset || 0;
const span = this.getSpan(b, offset);
return new web3.PublicKey(
Buffer.from(b.buffer, b.byteOffset, b.length).subarray(offset, offset + span),
);
}
encode(src: web3.PublicKey, b: Uint8Array, offset?: number): number {
offset = offset || 0;
const dstBuf = Buffer.from(b.buffer, b.byteOffset, b.length);
const srcBuf = src.toBuffer();
return srcBuf.copy(dstBuf, offset);
}
}
////////////////////////////////////////////////////////////////////////////////
export type PfTradeEventLayout = {
mint: web3.PublicKey;
solAmount: bigint;
tokenAmount: bigint;
isBuy: boolean;
user: web3.PublicKey;
timestamp: bigint;
virtualSolReserves: bigint;
virtualTokenReserves: bigint;
realSolReserves: bigint;
realTokenReserves: bigint;
};
export const PfTradeEventLayout = BufferLayout.struct<PfTradeEventLayout>([
new BorshPublicKey("mint"),
new BorshUInt64LE("solAmount"),
new BorshUInt64LE("tokenAmount"),
new BorshBoolean("isBuy"),
new BorshPublicKey("user"),
new BorshInt64LE("timestamp"),
new BorshUInt64LE("virtualSolReserves"),
new BorshUInt64LE("virtualTokenReserves"),
new BorshUInt64LE("realSolReserves"),
new BorshUInt64LE("realTokenReserves"),
]);
export const PF_PROGRAM_ID = new web3.PublicKey("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P");
export const PF_LOG_PREFIX = `Program ${PF_PROGRAM_ID.toBase58()}`;
export const PF_LOG_SUCCESS = `${PF_LOG_PREFIX} success`;
export const PF_LOG_DATA_PREFIX = "Program data: ";
export const PF_LOG_TRADE_IDL_DISCRIMINATOR = Uint8Array.from([0xbd, 0xdb, 0x7f, 0xd3, 0x4e, 0xe6, 0x61, 0xee]);
export const PF_LOG_TRADE_DATA_DECODED_LENGTH = PF_LOG_TRADE_IDL_DISCRIMINATOR.byteLength + PfTradeEventLayout.span; // 129
export const PF_LOG_TRADE_DATA_ENCODED_LENGTH = ((4 * (PF_LOG_TRADE_DATA_DECODED_LENGTH / 3)) + 3) & ~3; // 172
export const PF_LOG_TRADE_TOTAL_LENGTH = PF_LOG_DATA_PREFIX.length + PF_LOG_TRADE_DATA_ENCODED_LENGTH; // 186
export const PF_LOG_TRADE_DATA_ENCODED_PREFIX = Buffer.from(PF_LOG_TRADE_IDL_DISCRIMINATOR).toString('base64').slice(0, Math.floor(4 * (PF_LOG_TRADE_IDL_DISCRIMINATOR.byteLength / 3))); // "vdt/007mYe"
export const PF_LOG_TRADE_DATA_ENCODED_PREFIX_OFFSET = PF_LOG_DATA_PREFIX.length; // 14
export function findPfTradeInLogs(logs: string[]): PfTradeEventLayout | null {
const logsLength = logs.length;
if (!logs || logsLength < 3) {
return null;
}
for (let idx = 1; idx < logsLength; ++idx) {
if (logs[idx].length !== PF_LOG_TRADE_TOTAL_LENGTH
|| !logs[idx].startsWith(PF_LOG_TRADE_DATA_ENCODED_PREFIX, PF_LOG_TRADE_DATA_ENCODED_PREFIX_OFFSET)) {
continue;
}
if (logs[idx - 1] !== PF_LOG_SUCCESS
&& (idx !== (logsLength - 1) && !logs[idx + 1].startsWith(PF_LOG_PREFIX))) {
continue;
}
try {
const rawData = Buffer.from(logs[idx].slice(PF_LOG_TRADE_DATA_ENCODED_PREFIX_OFFSET), "base64");
return PfTradeEventLayout.decode(Buffer.from(rawData.buffer, rawData.byteOffset + 8, rawData.byteLength - 8));
} catch (e) {
return null;
}
}
return null;
}
export const PF_BONDING_CURVE_SEED = Buffer.from("bonding-curve");
export function findPfBondingCurveAddress(tokenMint: web3.PublicKey): web3.PublicKey {
return web3.PublicKey.findProgramAddressSync([
PF_BONDING_CURVE_SEED,
tokenMint.toBuffer(),
], PF_PROGRAM_ID)[0];
}
////////////////////////////////////////////////////////////////////////////////
export const OPAQUE_SIGNATURE = "1111111111111111111111111111111111111111111111111111111111111111";
(async (rpcUrl: string) => {
const conn = new web3.Connection(rpcUrl, "confirmed");
const seenSignatures = new QuickLRU({ maxSize: 256 });
conn.onLogs(PF_PROGRAM_ID, (txLogs: web3.Logs, ctx: web3.Context) => {
if ((!ctx || !txLogs || txLogs.err !== null || !txLogs.logs || txLogs.logs.length < 3)
|| (!txLogs.signature || txLogs.signature === OPAQUE_SIGNATURE)) {
return;
}
if (seenSignatures.has(txLogs.signature)) return;
seenSignatures.set(txLogs.signature, true);
const trade = findPfTradeInLogs(txLogs.logs);
if (!trade) {
return;
}
// NOTE: this is optional; showing how the address can be derived (even
// though it is not present in the logged event itself).
(trade as any).bondingCurve = findPfBondingCurveAddress(trade.mint);
console.log(ctx.slot, txLogs.signature);
console.log(JSON.stringify(
Object.fromEntries(Object.entries(trade).map(([k, v]) =>
[k, v !== null && (typeof v === "object" || typeof v === "bigint") ? String(v) : v])),
null, 2,
));
console.log();
});
})(process.env.SOL_RPC_URL || "https://mainnet.helius-rpc.com/?api-key=00000000-0000-0000-0000-000000000000");
////////////////////////////////////////////////////////////////////////////////
/* ---------- Output (example) ----------
299711471 nyxck8KdQG3tADVg5aVjL27x5jirN8PbQnADkq1uUgRajrzacofAyLiaYzWH79VHumVQbgtTTJ81bnFDNxaem78
{
"mint": "vCWfJEK7usVaLbvrVwkzYAcEfs2ME7UneAtEfkVpump",
"solAmount": "7337100",
"tokenAmount": "261347508837",
"isBuy": false,
"user": "DNngDZZ46FMreCGvdvCywopa54QaE3qA5rQzzbRhxvDM",
"timestamp": "1730842893",
"virtualSolReserves": "30058031026",
"virtualTokenReserves": "1070928431798389",
"realSolReserves": "58031026",
"realTokenReserves": "791028431798389",
"bondingCurve": "D7rTBjaBqPmXvnhfq24uk39Poyt67XfU1df9Yj4MUopi"
}
299711471 2MTdJKZboSM4ab3AHvTvqXdFirDBvoTn8wDKCM9dbjPJ3u33WNBhuCMKi8s44xXbRS4hvp2PCQsN22Aifh6bxeNt
{
"mint": "2py6SsiJgonGmfaCoVEBPbLLEs2YSpyvNmM1XUjWpump",
"solAmount": "4872906",
"tokenAmount": "79937904407",
"isBuy": false,
"user": "qzdBcZ4kAnEfTJKUvnhwVJuMWnx9A8F5m57w9yoTF67",
"timestamp": "1730842893",
"virtualSolReserves": "44294953943",
"virtualTokenReserves": "726719347731142",
"realSolReserves": "14294953943",
"realTokenReserves": "446819347731142",
"bondingCurve": "5StpJ5SAS19Y9vrWPupgpSfHwJxSL3rUbZHbQCv6A84E"
}
...
299711472 2Hq2YVJuLgonaLoLcCzLi9VLhAzEFhBzDU8uyyTpjKunxB1Cs3ZGZqdJ2GXN1XTefgqBRwsro66GmCD8FTWzwLtm
{
"mint": "9Ri9NfFMcnMgFUXqNQMudnRMZtY4nTcKSHNUUMM84Ztu",
"solAmount": "20000000",
"tokenAmount": "221005693641",
"isBuy": true,
"user": "59V6rEqH9xVSpeUtfttQ4uVVJhcdvMfpaKYsWTsyDEL9",
"timestamp": "1730842894",
"virtualSolReserves": "53222793380",
"virtualTokenReserves": "587906012730295",
"realSolReserves": "23222793380",
"realTokenReserves": "376106012730295",
"bondingCurve": "3rZVMm7vSCBPDvy33ggFTAnCwYXrdGRtJmN9scWN4TKn"
}
299711472 5Rz7Dbv2mKvfxakgLxuwgzcoo2jiWqtELaVD2UEs1NbHWiQ7AQw5CZ44asQZYzki2CR4wgomrK5bDSFHMdmYaBv1
{
"mint": "3byxQJYoFdCRX1qRk3EW8hKWvin5tEgik3KozLXepump",
"solAmount": "1868084",
"tokenAmount": "11308008764",
"isBuy": false,
"user": "feJ8dgSw4ag9x74kQ4GffrfwSmor9oQ5FAJzaWQbQMC",
"timestamp": "1730842894",
"virtualSolReserves": "72922262219",
"virtualTokenReserves": "441428983109251",
"realSolReserves": "42922262219",
"realTokenReserves": "161528983109251",
"bondingCurve": "AmEQFZnkGByjmeMxaTsqCWfDr3QZ2oFLB2jkMkg8PkF1"
}
299711473 31vbPUxM1g1WMfMWZ2g3etZEVMhADztDT2etNBrnG91XuvPsELv9Ly9iHhz1d3ny5emW9ZBQqpE4btCh3sTtL6LZ
{
"mint": "B7T6NYtb1VvzCbFCPkEGepFCxPnJNb8V9tQiJSu9pump",
"solAmount": "10088406",
"tokenAmount": "61349693251",
"isBuy": true,
"user": "9CDWeVFvNkho6r9jTPDAutTfDMavd24hGfQwoNpVL7xc",
"timestamp": "1730842894",
"virtualSolReserves": "72760497627",
"virtualTokenReserves": "442410388536965",
"realSolReserves": "42760497627",
"realTokenReserves": "162510388536965",
"bondingCurve": "DcAWVrFjXsH2oyifcnBCmt8eGN4dsG2AWrPyv45d83Ay"
}
299711473 sb5e3Eq5p9Yq42u27LtqCLPEfu3DpP5jfJ6X64T66T8JhzgfcUh6HYM3mRPkL8v4AeD1WNzUMgA2LFAPypJcasc
{
"mint": "Aj95ceHTnNEHBNmSbrUNhksuAcrWjPhR8pAtdMCypump",
"solAmount": "244284670",
"tokenAmount": "3296440108891",
"isBuy": true,
"user": "9sQmz1ufJcSJbr45R1pu3XNWEjEfLuvjuTTdWCV9sp2h",
"timestamp": "1730842894",
"virtualSolReserves": "48963458479",
"virtualTokenReserves": "657429050641188",
"realSolReserves": "18963458479",
"realTokenReserves": "377529050641188",
"bondingCurve": "CQTyh7ML1QExjrZCv9XPDFMUn3gGrUyJGJwG1RvW8c9Z"
}
*/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment