Created
February 19, 2025 23:31
-
-
Save rubpy/b1cce7d14d8dc22e9ab9f46d49fa7513 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import web3 from "@solana/web3.js"; | |
| import * as BufferLayout from "@solana/buffer-layout"; | |
| import * as spl from "@solana/spl-token"; | |
| import QuickLRU from "quick-lru"; | |
| //////////////////////////////////////////////////////////////////////////////// | |
| function onPfTransaction( | |
| signature: string, | |
| slot: number, | |
| rawEvents: PfEvent[], | |
| _txLogs: web3.Logs, | |
| _conn: web3.Connection, | |
| ) { | |
| const events = enhancePfEvents(rawEvents); | |
| // Pretty printing... | |
| console.log( | |
| slot, signature, | |
| mapInPlaceObjectContainingBigInts(events, 15), | |
| "\n", | |
| ); | |
| } | |
| /* | |
| ╔═════════════════════════════ Example output ═════════════════════════════╗ | |
| ║ | |
| ║ 321786059 2wLFDwGuDyKDz13Qxfr6zxq5kyyDJiN1gv2TnHn1Me9NQ217GmpXeXCvLpLyvYUcaEuH6wykTY9uFzw3jreeckkF [ | |
| ║ { | |
| ║ mint: 'ABKC9TX19KVN2zvK6Qvu38hp8EhF6LxmAWhwmSLLpump', | |
| ║ solAmount: '125426874', | |
| ║ tokenAmount: '3698978768369', | |
| ║ isBuy: true, | |
| ║ user: '9a51T5oXWpqa59XDERetS5ELaG8sHPwVuWYQq47BusHB', | |
| ║ timestamp: '1740007135', | |
| ║ virtualSolReserves: '33100858036', | |
| ║ virtualTokenReserves: '972482343699938', | |
| ║ realSolReserves: '3100858036', | |
| ║ realTokenReserves: '692582343699938', | |
| ║ type: 'pf:event:TradeEvent', | |
| ║ bondingCurve: '5yHhVoiRhHxeMtHRU8NzachEy6WdSYWzqsFPZaZwcZF6', | |
| ║ associatedBondingCurve: '3nGeayALRL9KgTWu4XTLCoaoAzf8gg3TUXLtDicru3ku', | |
| ║ bondingProgressBefore: '0.122131347656250', | |
| ║ bondingProgressAfter: '0.126770019531250', | |
| ║ kothProgressBefore: '0.144104003906250', | |
| ║ kothProgressAfter: '0.149597167968750', | |
| ║ priceBefore: '0.000000033780027', | |
| ║ priceAfter: '0.000000034037490', | |
| ║ priceAverage: '0.000000033908514', | |
| ║ pricePaid: '0.000000033908514', | |
| ║ solAmountParsed: '0.125426874000000', | |
| ║ tokenAmountParsed: '3698978.768368999939412' | |
| ║ } | |
| ║ ] | |
| ║ | |
| ║ . . . | |
| ║ | |
| ║ 321786060 2aT86SkygKpCWCQGGBB6JNEotqVPr8CZoCWsGE58RNJ7rwXRuRgw9udrdVFaA2Cv4gJwJcQoZpFzUUJ3sM3acx7Z [ | |
| ║ { | |
| ║ mint: '9oxe4qCxAQPtC44kR1Rr6AsSh54j8PAbvkaNwoqepump', | |
| ║ solAmount: '10000001', | |
| ║ tokenAmount: '38591468667', | |
| ║ isBuy: true, | |
| ║ user: '4nppHvT9meLhNGAcYW27BVEPybNL7deEfr2xT1xKPi95', | |
| ║ timestamp: '1740007135', | |
| ║ virtualSolReserves: '91335291845', | |
| ║ virtualTokenReserves: '352437713868769', | |
| ║ realSolReserves: '61335291845', | |
| ║ realTokenReserves: '72537713868769', | |
| ║ type: 'pf:event:TradeEvent', | |
| ║ bondingCurve: 'ArxiKYrHPGFVjbQVKuuMzPoxMvL9DMndKkSXHs1EZznV', | |
| ║ associatedBondingCurve: 'H5DCDjR4WdFJYcGvkyvk7cWoeE44Y5C6Ungq6YVsq4Gd', | |
| ║ bondingProgressBefore: '0.908508300781250', | |
| ║ bondingProgressAfter: '0.908569335937500', | |
| ║ kothProgressBefore: '1.000000000000000', | |
| ║ kothProgressAfter: '1.000000000000000', | |
| ║ priceBefore: '0.000000259096258', | |
| ║ priceAfter: '0.000000259153003', | |
| ║ priceAverage: '0.000000259124629', | |
| ║ pricePaid: '0.000000259124655', | |
| ║ solAmountParsed: '0.010000001000000', | |
| ║ tokenAmountParsed: '38591.468667000001005' | |
| ║ }, | |
| ║ { | |
| ║ mint: '9oxe4qCxAQPtC44kR1Rr6AsSh54j8PAbvkaNwoqepump', | |
| ║ solAmount: '10000000', | |
| ║ tokenAmount: '38591468667', | |
| ║ isBuy: false, | |
| ║ user: '4nppHvT9meLhNGAcYW27BVEPybNL7deEfr2xT1xKPi95', | |
| ║ timestamp: '1740007135', | |
| ║ virtualSolReserves: '91325291845', | |
| ║ virtualTokenReserves: '352476305337436', | |
| ║ realSolReserves: '61325291845', | |
| ║ realTokenReserves: '72576305337436', | |
| ║ type: 'pf:event:TradeEvent', | |
| ║ bondingCurve: 'ArxiKYrHPGFVjbQVKuuMzPoxMvL9DMndKkSXHs1EZznV', | |
| ║ associatedBondingCurve: 'H5DCDjR4WdFJYcGvkyvk7cWoeE44Y5C6Ungq6YVsq4Gd', | |
| ║ bondingProgressBefore: '0.908569335937500', | |
| ║ bondingProgressAfter: '0.908508300781250', | |
| ║ kothProgressBefore: '1.000000000000000', | |
| ║ kothProgressAfter: '1.000000000000000', | |
| ║ priceBefore: '0.000000259153003', | |
| ║ priceAfter: '0.000000259096258', | |
| ║ priceAverage: '0.000000259124629', | |
| ║ pricePaid: '0.000000259124629', | |
| ║ solAmountParsed: '0.010000000000000', | |
| ║ tokenAmountParsed: '38591.468667000001005' | |
| ║ } | |
| ║ ] | |
| ║ | |
| ║ . . . | |
| ║ | |
| ║ 321786065 3VR7yKT84PB9Vnm9ABbmHL9nJsFamUtsD6V7B4pZpwNPjErKRJ4PXVjw2snEskk5KVJv3jjf2BLekYukts9XoSZq [ | |
| ║ { | |
| ║ name: 'shiba', | |
| ║ symbol: 'SHIBA', | |
| ║ uri: 'https://metadata.pumplify.eu/data/QmTYacvCKbhDtA76cG9CfpGYQ5bNhBXSwPPXEJc3Q2sMYQ', | |
| ║ mint: '91QgGcNGFURc99eEAmXk7zKUJZ4fpMead5SGajuZbes2', | |
| ║ bondingCurve: 'JXbnDavBeV5s1RkW5rMW3bLVAR2ZWVXssLnXMFPD3t2', | |
| ║ user: 'C88rn1cz1PcRJFh44TuynaWbnRis4xuBRi65sD1Nwxog', | |
| ║ type: 'pf:event:CreateEvent' | |
| ║ }, | |
| ║ { | |
| ║ mint: '91QgGcNGFURc99eEAmXk7zKUJZ4fpMead5SGajuZbes2', | |
| ║ solAmount: '3000000000', | |
| ║ tokenAmount: '97545454545454', | |
| ║ isBuy: true, | |
| ║ user: 'C88rn1cz1PcRJFh44TuynaWbnRis4xuBRi65sD1Nwxog', | |
| ║ timestamp: '1740007137', | |
| ║ virtualSolReserves: '33000000000', | |
| ║ virtualTokenReserves: '975454545454546', | |
| ║ realSolReserves: '3000000000', | |
| ║ realTokenReserves: '695554545454546', | |
| ║ type: 'pf:event:TradeEvent', | |
| ║ bondingCurve: 'JXbnDavBeV5s1RkW5rMW3bLVAR2ZWVXssLnXMFPD3t2', | |
| ║ associatedBondingCurve: 'BgYzNTsTYhgPkmGK1chZju8shTaPKKMzzeQWxbbTcKWK', | |
| ║ bondingProgressBefore: '0.000000000000000', | |
| ║ bondingProgressAfter: '0.123046875000000', | |
| ║ kothProgressBefore: '0.000000000000000', | |
| ║ kothProgressAfter: '0.145141601562500', | |
| ║ priceBefore: '0.000000027958993', | |
| ║ priceAfter: '0.000000033830382', | |
| ║ priceAverage: '0.000000030754893', | |
| ║ pricePaid: '0.000000030754893', | |
| ║ solAmountParsed: '3.000000000000000', | |
| ║ tokenAmountParsed: '97545454.545453995466232' | |
| ║ } | |
| ║ ] | |
| ║ | |
| ║ 321786065 3p8G6Ksx8tjxcyLTUvyfro1eMmxzWovSRpVYhK7rUxeNE7JMNGhoHTBCEwYSPDJY9ykvSjdGo6NJ1Ke5eMFzCG2i [ | |
| ║ { | |
| ║ user: 'FjSNewStC9txkREbMMrkyRRMivnewips45reKoxtXd76', | |
| ║ mint: 'MjmMdCDWPYZQndpyNGY7GN6HwYCtjUPRjCDWvSDpump', | |
| ║ bondingCurve: 'A3co8MXGjQgtgrtJxV4iJenSFttdNtsEzm2Dz3pcAWgG', | |
| ║ timestamp: '1740007137', | |
| ║ type: 'pf:event:CompleteEvent' | |
| ║ }, | |
| ║ { | |
| ║ mint: 'MjmMdCDWPYZQndpyNGY7GN6HwYCtjUPRjCDWvSDpump', | |
| ║ solAmount: '248293644', | |
| ║ tokenAmount: '605604460987', | |
| ║ isBuy: true, | |
| ║ user: 'FjSNewStC9txkREbMMrkyRRMivnewips45reKoxtXd76', | |
| ║ timestamp: '1740007137', | |
| ║ virtualSolReserves: '115005359189', | |
| ║ virtualTokenReserves: '279900000000000', | |
| ║ realSolReserves: '85005359189', | |
| ║ realTokenReserves: '0', | |
| ║ type: 'pf:event:TradeEvent', | |
| ║ bondingCurve: 'A3co8MXGjQgtgrtJxV4iJenSFttdNtsEzm2Dz3pcAWgG', | |
| ║ associatedBondingCurve: 'EzaF55dKqT5CEueF3icP5CBtHQv11rHS4kpFbXiSywB', | |
| ║ bondingProgressBefore: '0.999267578125000', | |
| ║ bondingProgressAfter: '1.000000000000000', | |
| ║ kothProgressBefore: '1.000000000000000', | |
| ║ kothProgressAfter: '1.000000000000000', | |
| ║ priceBefore: '0.000000409107924', | |
| ║ priceAfter: '0.000000410880169', | |
| ║ priceAverage: '0.000000409993089', | |
| ║ pricePaid: '0.000000409993090', | |
| ║ solAmountParsed: '0.248293644000000', | |
| ║ tokenAmountParsed: '605604.460987000027671' | |
| ║ } | |
| ║ ] | |
| ║ | |
| ╚══════════════════════════════════════════════════════════════════════════════╝ | |
| */ | |
| //////////////////////////////////////////////////////////////////////////////// | |
| //////////////////////////////////////////////////////////////////////////////// | |
| //////////////////////////////////////////////////////////////////////////////// | |
| async function main(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 events = findPfEventsInLogs(txLogs.logs); | |
| if (!events || !events.length) { | |
| return; | |
| } | |
| onPfTransaction(txLogs.signature, ctx.slot, events, txLogs, conn); | |
| }); | |
| } | |
| //////////////////////////////////////////////////////////////////////////////// | |
| //////////////////////////////////////////////////////////////////////////////// | |
| //////////////////////////////////////////////////////////////////////////////// | |
| 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; | |
| } | |
| }; | |
| export 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 class BorshString extends BufferLayout.Layout<string> { | |
| constructor(property?: string) { | |
| super(-1, property); | |
| } | |
| get minSpan() { return 4; } | |
| getSpan(b?: Uint8Array, offset?: number): number { | |
| offset = offset || 0; | |
| if (!b || b.byteLength < offset + 4) { | |
| throw new RangeError("invalid string encoding"); | |
| } | |
| const len = Buffer.from(b.buffer, b.byteOffset, b.length).readUInt32LE(offset); | |
| if ((offset + len) > b.byteLength) { | |
| throw new RangeError("encoding overruns Buffer"); | |
| } | |
| return len + 4; | |
| } | |
| decode(b: Uint8Array, offset?: number): string { | |
| offset = offset || 0; | |
| const span = this.getSpan(b, offset); | |
| return span <= 4 ? "" : Buffer.from(b.buffer, b.byteOffset, b.length).subarray(offset + 4, offset + span).toString("utf-8"); | |
| } | |
| encode(src: string, b: Uint8Array, offset?: number): number { | |
| if (typeof src !== "string") { | |
| src = String(src); | |
| } | |
| offset = offset || 0; | |
| const srcBuf = Buffer.from(src, "utf-8"); | |
| const span = srcBuf.length; | |
| if (span > 0xffffffff) { | |
| throw new RangeError("unsupported data size"); | |
| } | |
| if ((offset + span + 4) > b.length) { | |
| throw new RangeError("encoding overruns Buffer"); | |
| } | |
| const dstBuf = Buffer.from(b.buffer, b.byteOffset, b.length); | |
| dstBuf.writeUInt32LE(span, offset); | |
| srcBuf.copy(dstBuf, offset + 4); | |
| return span + 4; | |
| } | |
| }; | |
| export type BufferLayoutStructureWithMinSpan<T> | |
| = (BufferLayout.Structure<T> & { get minSpan(): number }); | |
| export function bufferLayoutStructureWithMinSpan<T>(structure: BufferLayout.Structure<T>): BufferLayoutStructureWithMinSpan<T> { | |
| return Object.defineProperty(structure, "minSpan", { | |
| get: function () { | |
| if (!this || !Array.isArray(this.fields)) { | |
| return -1; | |
| } | |
| let minSpan = 0; | |
| for (const field of this.fields) { | |
| if (!field || typeof field.span !== "number") { | |
| return -1; | |
| } | |
| if (field.span >= 0) { | |
| minSpan += field.span; | |
| } else if (typeof field.minSpan === "number" && field.minSpan >= 0) { | |
| minSpan += field.minSpan; | |
| } else { | |
| return -1; | |
| } | |
| } | |
| return minSpan; | |
| }, | |
| }) as BufferLayoutStructureWithMinSpan<T>; | |
| }; | |
| export function decodeStructure<T, U extends any = T, F extends ((value: T) => U) = ((value: T) => U)>( | |
| layout: BufferLayout.Structure<T>, | |
| buffer: Buffer, | |
| discriminator?: Uint8Array | null, | |
| offset?: number, | |
| mapFn?: F, | |
| ): U | null { | |
| if ((layout === null || typeof layout !== "object" || !layout.span) | |
| || (buffer === null || typeof buffer !== "object")) { | |
| return null; | |
| } | |
| offset = typeof offset === "number" && offset >= 0 ? offset : 0; | |
| if (offset >= buffer.byteLength | |
| || (buffer.byteLength < (offset + (discriminator ? discriminator.byteLength : 0) + layout.span)) | |
| || ((discriminator && discriminator.byteLength > 0) && !buffer.subarray(offset, (offset += discriminator.byteLength)).equals(discriminator))) { | |
| return null; | |
| } | |
| const value = layout.decode(buffer, offset); | |
| return value && mapFn ? mapFn(value) : (value as any as U); | |
| }; | |
| //////////////////////////////////////////////////////////////////////////////// | |
| export type PfType = ((<T>(instance: T) => T) & { | |
| readonly symbol: symbol; | |
| readonly [Symbol.hasInstance]: ((instance: any) => boolean); | |
| }); | |
| export type PfTypeConstructor = (((symbol: symbol) => PfType) & { | |
| readonly symbol: symbol; | |
| readonly unknown: symbol; | |
| readonly of: (instance: any) => symbol; | |
| readonly is: (type: PfType | symbol, instance: any) => boolean; | |
| readonly as: <T>(type: PfType | symbol, instance: any) => T | null; | |
| readonly tag: <T>(type: PfType | symbol, instance: T, extraSymbol?: symbol) => T; | |
| }); | |
| export const PfType = Object.freeze(Object.defineProperties(function (symbol: symbol) { | |
| return Object.freeze(Object.defineProperties(function <T>(instance: T) { | |
| return PfType.tag(symbol, instance); | |
| }, { | |
| symbol: { | |
| get() { | |
| return this[PfType.symbol]; | |
| }, | |
| }, | |
| [PfType.symbol]: { | |
| value: typeof symbol === "symbol" ? symbol : PfType.unknown, | |
| }, | |
| toString: { | |
| value: function () { | |
| return Symbol.prototype.toString.call( | |
| this && typeof this[PfType.symbol] === "symbol" | |
| ? this[PfType.symbol] : PfType.unknown, | |
| ); | |
| }, | |
| }, | |
| [Symbol.hasInstance]: { | |
| value(instance: any) { | |
| return this && instance !== null && typeof instance === "object" | |
| && instance[PfType.symbol] === this[PfType.symbol]; | |
| } | |
| }, | |
| [Symbol.toStringTag]: { | |
| get() { | |
| return Symbol.prototype.toString.call( | |
| this && typeof this[PfType.symbol] === "symbol" | |
| ? this[PfType.symbol] : PfType.unknown, | |
| ); | |
| }, | |
| } | |
| })); | |
| }, { | |
| symbol: { value: Symbol("type") }, | |
| unknown: { value: Symbol("unknown") }, | |
| of: { | |
| value(instance: any): symbol { | |
| return instance !== null && typeof instance === "object" | |
| && typeof instance[PfType.symbol] === "symbol" | |
| ? instance[PfType.symbol] : PfType.unknown; | |
| } | |
| }, | |
| is: { | |
| value(type: PfType | symbol, instance: any): boolean { | |
| const instanceType | |
| = instance !== null && typeof instance === "object" | |
| && typeof instance[PfType.symbol] === "symbol" | |
| ? instance[PfType.symbol] : PfType.unknown; | |
| return instanceType === ((typeof type === "symbol" ? type : type[PfType.symbol]) || PfType.unknown); | |
| } | |
| }, | |
| as: { | |
| value<T>(type: PfType | symbol, instance: any): T | null { | |
| return PfType.is(type, instance) ? (instance as T) : null; | |
| } | |
| }, | |
| tag: { | |
| value<T>(type: typeof PfType | symbol, instance: T, extraSymbol?: symbol): T { | |
| Object.defineProperty(instance, PfType.symbol, { | |
| value: (typeof type === "symbol" ? type : type[PfType.symbol]) || PfType.unknown, | |
| // enumerable: true, | |
| }); | |
| if (typeof extraSymbol === "symbol") { | |
| Object.defineProperty(instance, extraSymbol, { | |
| value: (typeof type === "symbol" ? type : type[PfType.symbol]) || PfType.unknown, | |
| }); | |
| } | |
| return instance; | |
| } | |
| }, | |
| })) as any as PfTypeConstructor; | |
| export function createPfTypeWrapper< | |
| E extends { [key: string]: (PfType | symbol) }, | |
| S extends symbol = symbol, | |
| >( | |
| symbol: S, | |
| types: E, | |
| ): ((<T>(type: PfType, data: T) => T) & { readonly type: { [K in keyof E]: PfType }; readonly symbol: symbol; }) { | |
| return Object.freeze(Object.defineProperties(function <T>(type: PfType, data: T) { | |
| return PfType.tag(type, data, PfEvent.symbol); | |
| }, { | |
| symbol: { value: symbol }, | |
| type: { | |
| value: Object.fromEntries(Object.keys(types).map(k => { | |
| const v = types[k]; | |
| return [k, (typeof v === "symbol" ? PfType(v) : v)]; | |
| })) | |
| }, | |
| [Symbol.hasInstance]: { | |
| value(instance: any) { | |
| return this && instance !== null && typeof instance === "object" | |
| && instance[symbol]; | |
| } | |
| }, | |
| })) as any; | |
| }; | |
| //////////////////////////////////////////////////////////////////////////////// | |
| //////////////////////////////////////////////////////////////////////////////// | |
| //////////////////////////////////////////////////////////////////////////////// | |
| export interface PfAccountBondingCurve { | |
| virtualTokenReserves: bigint; | |
| virtualSolReserves: bigint; | |
| realTokenReserves: bigint; | |
| realSolReserves: bigint; | |
| tokenTotalSupply: bigint; | |
| complete: boolean; | |
| }; | |
| export const PfAccountBondingCurveLayout = BufferLayout.struct<PfAccountBondingCurve>([ | |
| new BorshUInt64LE("virtualTokenReserves"), | |
| new BorshUInt64LE("virtualSolReserves"), | |
| new BorshUInt64LE("realTokenReserves"), | |
| new BorshUInt64LE("realSolReserves"), | |
| new BorshUInt64LE("tokenTotalSupply"), | |
| new BorshBoolean("complete"), | |
| ]); | |
| export const PF_ACCOUNT_BONDING_CURVE_DISCRIMINATOR = Uint8Array.from([0x17, 0xb7, 0xf8, 0x37, 0x60, 0xd8, 0xac, 0x60]); // `sha256("account:BondingCurve")[0:8]` | |
| export const PF_ACCOUNT_BONDING_CURVE_TOTAL_SIZE = PF_ACCOUNT_BONDING_CURVE_DISCRIMINATOR.byteLength + PfAccountBondingCurveLayout.span; | |
| export const PF_ACCOUNT_BONDING_CURVE_SEED = Buffer.from("bonding-curve"); | |
| //////////////////////////////////////////////////////////////////////////////// | |
| export interface PfEventCreate { | |
| name: string; | |
| symbol: string; | |
| uri: string; | |
| mint: web3.PublicKey; | |
| bondingCurve: web3.PublicKey; | |
| user: web3.PublicKey; | |
| }; | |
| export const PfEventCreateLayout = bufferLayoutStructureWithMinSpan(BufferLayout.struct<PfEventCreate>([ | |
| new BorshString("name"), | |
| new BorshString("symbol"), | |
| new BorshString("uri"), | |
| new BorshPublicKey("mint"), | |
| new BorshPublicKey("bondingCurve"), | |
| new BorshPublicKey("user"), | |
| ])); | |
| export const PF_EVENT_CREATE_DISCRIMINATOR = Uint8Array.from([0x1b, 0x72, 0xa9, 0x4d, 0xde, 0xeb, 0x63, 0x76]); // `sha256("event:CreateEvent")[0:8]` | |
| //////////////////////////////////////////////////////////////////////////////// | |
| export interface PfEventTrade { | |
| mint: web3.PublicKey; | |
| solAmount: bigint; | |
| tokenAmount: bigint; | |
| isBuy: boolean; | |
| user: web3.PublicKey; | |
| timestamp: bigint; | |
| virtualSolReserves: bigint; | |
| virtualTokenReserves: bigint; | |
| realSolReserves: bigint; | |
| realTokenReserves: bigint; | |
| }; | |
| export const PfEventTradeLayout = BufferLayout.struct<PfEventTrade>([ | |
| 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_EVENT_TRADE_DISCRIMINATOR = Uint8Array.from([0xbd, 0xdb, 0x7f, 0xd3, 0x4e, 0xe6, 0x61, 0xee]); // `sha256("event:TradeEvent")[0:8]` | |
| //////////////////////////////////////////////////////////////////////////////// | |
| export interface PfEventComplete { | |
| user: web3.PublicKey; | |
| mint: web3.PublicKey; | |
| bondingCurve: web3.PublicKey; | |
| timestamp: bigint; | |
| }; | |
| export const PfEventCompleteLayout = BufferLayout.struct<PfEventComplete>([ | |
| new BorshPublicKey("user"), | |
| new BorshPublicKey("mint"), | |
| new BorshPublicKey("bondingCurve"), | |
| new BorshInt64LE("timestamp"), | |
| ]); | |
| export const PF_EVENT_COMPLETE_DISCRIMINATOR = Uint8Array.from([0x5f, 0x72, 0x61, 0x9c, 0xd4, 0x2e, 0x98, 0x08]); // `sha256("event:CompleteEvent")[0:8]` | |
| //////////////////////////////////////////////////////////////////////////////// | |
| export const PfAccount = createPfTypeWrapper( | |
| Symbol("pf:account"), | |
| { | |
| BONDING_CURVE: Symbol("pf:account:BondingCurve"), | |
| } | |
| ); | |
| export type PfAccount = PfAccountBondingCurve; | |
| export const PfEvent = createPfTypeWrapper( | |
| Symbol("pf:event"), | |
| { | |
| CREATE: Symbol("pf:event:CreateEvent"), | |
| TRADE: Symbol("pf:event:TradeEvent"), | |
| COMPLETE: Symbol("pf:event:CompleteEvent"), | |
| } | |
| ); | |
| export type PfEvent = PfEventCreate | PfEventTrade | PfEventComplete; | |
| //////////////////////////////////////////////////////////////////////////////// | |
| //////////////////////////////////////////////////////////////////////////////// | |
| //////////////////////////////////////////////////////////////////////////////// | |
| export const PF_PROGRAM_ID = new web3.PublicKey("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P"); | |
| export const PF_CONFIG_DECIMALS_TOKEN = 6n; | |
| export const PF_CONFIG_DECIMALS_TOKEN_PRODUCT = 10n ** PF_CONFIG_DECIMALS_TOKEN; | |
| export const PF_CONFIG_DECIMALS_SOL = 9n; | |
| export const PF_CONFIG_DECIMALS_SOL_PRODUCT = 10n ** PF_CONFIG_DECIMALS_SOL; | |
| export const PF_CONFIG_DECIMALS_TOTAL_PRODUCT = PF_CONFIG_DECIMALS_TOKEN_PRODUCT * PF_CONFIG_DECIMALS_SOL_PRODUCT; | |
| export const PF_CONFIG_DECIMALS_TOTAL_SHIFT_PRODUCT = PF_CONFIG_DECIMALS_TOTAL_PRODUCT * (PF_CONFIG_DECIMALS_SOL_PRODUCT / PF_CONFIG_DECIMALS_TOKEN_PRODUCT); | |
| export const PF_CONFIG_INITIAL_REAL_TOKEN_RESERVES = 793100000000000n; | |
| export const PF_CONFIG_INITIAL_VIRTUAL_TOKEN_RESERVES = 1073000000000000n; | |
| export const PF_CONFIG_INITIAL_VIRTUAL_SOL_RESERVES = 30000000000n; | |
| export const PF_CONFIG_TOKEN_TOTAL_SUPPLY = 1000000000000000n; | |
| export const PF_CONFIG_KOTH_TARGET_FACTOR = 200000000000n; | |
| export const PF_CONFIG_KOTH_TARGET = ( | |
| PF_CONFIG_INITIAL_VIRTUAL_TOKEN_RESERVES - BigInt(Math.trunc( | |
| Math.sqrt(Number( | |
| (PF_CONFIG_INITIAL_VIRTUAL_TOKEN_RESERVES | |
| * PF_CONFIG_INITIAL_VIRTUAL_SOL_RESERVES | |
| * PF_CONFIG_TOKEN_TOTAL_SUPPLY) | |
| / PF_CONFIG_KOTH_TARGET_FACTOR | |
| )) | |
| )) | |
| ); // 671814257481650n | |
| 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_EVENT_CREATE_DATA_DECODED_MIN_LENGTH = PF_EVENT_CREATE_DISCRIMINATOR.byteLength + (PfEventCreateLayout.minSpan || 0); // 116 | |
| export const PF_LOG_EVENT_CREATE_DATA_ENCODED_MIN_LENGTH = ((4 * (PF_LOG_EVENT_CREATE_DATA_DECODED_MIN_LENGTH / 3)) + 3) & ~3; // 156 | |
| export const PF_LOG_EVENT_CREATE_TOTAL_MIN_LENGTH = PF_LOG_DATA_PREFIX.length + PF_LOG_EVENT_CREATE_DATA_ENCODED_MIN_LENGTH; // 170 | |
| export const PF_LOG_EVENT_CREATE_DATA_ENCODED_PREFIX = Buffer.from(PF_EVENT_CREATE_DISCRIMINATOR).toString('base64').slice(0, Math.floor(4 * (PF_EVENT_CREATE_DISCRIMINATOR.byteLength / 3))); // "G3KpTd7rY3" | |
| export const PF_LOG_EVENT_CREATE_DATA_ENCODED_PREFIX_OFFSET = PF_LOG_DATA_PREFIX.length; // 14 | |
| export const PF_LOG_EVENT_TRADE_DATA_DECODED_LENGTH = PF_EVENT_TRADE_DISCRIMINATOR.byteLength + PfEventTradeLayout.span; // 129 | |
| export const PF_LOG_EVENT_TRADE_DATA_ENCODED_LENGTH = ((4 * (PF_LOG_EVENT_TRADE_DATA_DECODED_LENGTH / 3)) + 3) & ~3; // 172 | |
| export const PF_LOG_EVENT_TRADE_TOTAL_LENGTH = PF_LOG_DATA_PREFIX.length + PF_LOG_EVENT_TRADE_DATA_ENCODED_LENGTH; // 186 | |
| export const PF_LOG_EVENT_TRADE_DATA_ENCODED_PREFIX = Buffer.from(PF_EVENT_TRADE_DISCRIMINATOR).toString('base64').slice(0, Math.floor(4 * (PF_EVENT_TRADE_DISCRIMINATOR.byteLength / 3))); // "vdt/007mYe" | |
| export const PF_LOG_EVENT_TRADE_DATA_ENCODED_PREFIX_OFFSET = PF_LOG_DATA_PREFIX.length; // 14 | |
| export const PF_LOG_EVENT_COMPLETE_DATA_DECODED_LENGTH = PF_EVENT_COMPLETE_DISCRIMINATOR.byteLength + PfEventCompleteLayout.span; // 112 | |
| export const PF_LOG_EVENT_COMPLETE_DATA_ENCODED_LENGTH = ((4 * (PF_LOG_EVENT_COMPLETE_DATA_DECODED_LENGTH / 3)) + 3) & ~3; // 152 | |
| export const PF_LOG_EVENT_COMPLETE_TOTAL_LENGTH = PF_LOG_DATA_PREFIX.length + PF_LOG_EVENT_COMPLETE_DATA_ENCODED_LENGTH; // 166 | |
| export const PF_LOG_EVENT_COMPLETE_DATA_ENCODED_PREFIX = Buffer.from(PF_EVENT_COMPLETE_DISCRIMINATOR).toString('base64').slice(0, Math.floor(4 * (PF_EVENT_COMPLETE_DISCRIMINATOR.byteLength / 3))); // "X3JhnNQumA" | |
| export const PF_LOG_EVENT_COMPLETE_DATA_ENCODED_PREFIX_OFFSET = PF_LOG_DATA_PREFIX.length; // 14 | |
| export function findPfEventsInLogs( | |
| logs: string[], | |
| parseWhilePredicate?: (events: PfEvent[], currentEvent: PfEvent, previousEvent: PfEvent | null, logs: string[]) => boolean, | |
| ): PfEvent[] { | |
| const logsLength = logs.length; | |
| if (!logs || logsLength < 3) { | |
| return []; | |
| } | |
| const events = <PfEvent[]>[]; | |
| let previousEvent: PfEvent | null = null; | |
| let currentEvent: PfEvent | null = null; | |
| for (let idx = 1; idx < logsLength; ++idx) { | |
| if (logs[idx].length >= PF_LOG_EVENT_CREATE_TOTAL_MIN_LENGTH && logs[idx].startsWith(PF_LOG_EVENT_CREATE_DATA_ENCODED_PREFIX, PF_LOG_EVENT_CREATE_DATA_ENCODED_PREFIX_OFFSET)) { | |
| if (!(logs[idx - 1] === PF_LOG_SUCCESS || (idx !== (logsLength - 1) && logs[idx + 1].startsWith(PF_LOG_PREFIX)))) { | |
| continue; | |
| } | |
| if ((currentEvent = decodeStructure( | |
| PfEventCreateLayout, | |
| Buffer.from(logs[idx].slice(PF_LOG_EVENT_CREATE_DATA_ENCODED_PREFIX_OFFSET), "base64"), | |
| PF_EVENT_CREATE_DISCRIMINATOR, | |
| 0, | |
| e => PfEvent.type.CREATE(e), | |
| ))) { | |
| events.push(currentEvent); | |
| if (parseWhilePredicate && !parseWhilePredicate(events, currentEvent, previousEvent, logs)) { | |
| break; | |
| } | |
| previousEvent = currentEvent; | |
| } | |
| continue; | |
| } | |
| if (logs[idx].length >= PF_LOG_EVENT_TRADE_TOTAL_LENGTH && logs[idx].startsWith(PF_LOG_EVENT_TRADE_DATA_ENCODED_PREFIX, PF_LOG_EVENT_TRADE_DATA_ENCODED_PREFIX_OFFSET)) { | |
| if (!(logs[idx - 1] === PF_LOG_SUCCESS || (idx !== (logsLength - 1) && logs[idx + 1].startsWith(PF_LOG_PREFIX)))) { | |
| continue; | |
| } | |
| if ((currentEvent = decodeStructure( | |
| PfEventTradeLayout, | |
| Buffer.from(logs[idx].slice(PF_LOG_EVENT_TRADE_DATA_ENCODED_PREFIX_OFFSET), "base64"), | |
| PF_EVENT_TRADE_DISCRIMINATOR, | |
| 0, | |
| e => PfEvent.type.TRADE(e), | |
| ))) { | |
| events.push(currentEvent); | |
| if (parseWhilePredicate && !parseWhilePredicate(events, currentEvent, previousEvent, logs)) { | |
| break; | |
| } | |
| previousEvent = currentEvent; | |
| } | |
| continue; | |
| } | |
| if (logs[idx].length >= PF_LOG_EVENT_COMPLETE_TOTAL_LENGTH && logs[idx].startsWith(PF_LOG_EVENT_COMPLETE_DATA_ENCODED_PREFIX, PF_LOG_EVENT_COMPLETE_DATA_ENCODED_PREFIX_OFFSET)) { | |
| if (!(logs[idx - 1] === PF_LOG_SUCCESS || (idx !== (logsLength - 1) && logs[idx + 1].startsWith(PF_LOG_PREFIX)))) { | |
| continue; | |
| } | |
| if ((currentEvent = decodeStructure( | |
| PfEventCompleteLayout, | |
| Buffer.from(logs[idx].slice(PF_LOG_EVENT_COMPLETE_DATA_ENCODED_PREFIX_OFFSET), "base64"), | |
| PF_EVENT_COMPLETE_DISCRIMINATOR, | |
| 0, | |
| e => PfEvent.type.COMPLETE(e), | |
| ))) { | |
| events.push(currentEvent); | |
| if (parseWhilePredicate && !parseWhilePredicate(events, currentEvent, previousEvent, logs)) { | |
| break; | |
| } | |
| previousEvent = currentEvent; | |
| } | |
| continue; | |
| } | |
| } | |
| return events; | |
| }; | |
| export function findPfBondingCurveAddress(tokenMint: web3.PublicKey): web3.PublicKey { | |
| return web3.PublicKey.findProgramAddressSync([ | |
| PF_ACCOUNT_BONDING_CURVE_SEED, | |
| tokenMint.toBuffer(), | |
| ], PF_PROGRAM_ID)[0]; | |
| }; | |
| export function findPfAssociatedBondingCurveAddress(tokenMint: web3.PublicKey, bondingCurve?: web3.PublicKey): web3.PublicKey { | |
| if (!bondingCurve) { | |
| bondingCurve = findPfBondingCurveAddress(tokenMint); | |
| } | |
| return spl.getAssociatedTokenAddressSync(tokenMint, bondingCurve, true); | |
| }; | |
| //////////////////////////////////////////////////////////////////////////////// | |
| function mapInPlaceObjectContainingBigInts(v: any, fixedDecimalPrecision?: number): any { | |
| if (v === null || typeof v !== "object") { | |
| return v; | |
| } else if (Array.isArray(v)) { | |
| return v.map(e => mapInPlaceObjectContainingBigInts(e, fixedDecimalPrecision)); | |
| } | |
| for (const k in v) { | |
| if (!v.hasOwnProperty(k) || v[k] === null) { | |
| continue; | |
| } | |
| switch (typeof v[k]) { | |
| case "object": | |
| if (v[k].hasOwnProperty("_bn") || (v[k].constructor && v[k].constructor.hasOwnProperty("isBN"))) | |
| v[k] = String(v[k]); | |
| break; | |
| case "bigint": | |
| v[k] = String(v[k]); | |
| break; | |
| case "number": | |
| if (fixedDecimalPrecision) | |
| v[k] = (v[k] as number).toFixed(fixedDecimalPrecision); | |
| break; | |
| } | |
| } | |
| return v; | |
| } | |
| //////////////////////////////////////////////////////////////////////////////// | |
| export const OPAQUE_SIGNATURE = "1111111111111111111111111111111111111111111111111111111111111111"; | |
| export function calculatePfTokenPriceInShiftedLamports( | |
| virtualTokenReserves: bigint, | |
| virtualSolReserves: bigint, | |
| decimalsTotalProduct: bigint = PF_CONFIG_DECIMALS_TOTAL_PRODUCT, | |
| ): bigint { | |
| if (virtualSolReserves <= 0 || virtualTokenReserves <= 0 || decimalsTotalProduct <= 0n) { | |
| return 0n; | |
| } | |
| return (virtualSolReserves * decimalsTotalProduct) / virtualTokenReserves; | |
| }; | |
| export function shiftedLamportsToSol( | |
| lamports: bigint | number, | |
| decimalsTotalShiftProduct: bigint | number = PF_CONFIG_DECIMALS_TOTAL_SHIFT_PRODUCT, | |
| ): number { | |
| return Number(lamports) / Number(decimalsTotalShiftProduct); | |
| }; | |
| export function lamportsToSol( | |
| lamports: bigint | number, | |
| decimalsProduct: bigint | number = PF_CONFIG_DECIMALS_SOL_PRODUCT, | |
| ): number { | |
| return Number(lamports) / Number(decimalsProduct); | |
| }; | |
| export type PfEventEnhanced = (PfEventCreateEnhanced | PfEventTradeEnhanced | PfEventCompleteEnhanced) & { | |
| readonly type: string; | |
| }; | |
| export type PfEventCreateEnhanced = PfEventCreate & {}; | |
| export type PfEventTradeEnhanced = PfEventTrade & { | |
| bondingCurve: web3.PublicKey; | |
| associatedBondingCurve: web3.PublicKey; | |
| bondingProgressBefore: number; | |
| bondingProgressAfter: number; | |
| kothProgressBefore: number; | |
| kothProgressAfter: number; | |
| priceBefore: number; | |
| priceAfter: number; | |
| priceAverage: number; | |
| pricePaid: number; | |
| solAmountParsed: number; | |
| tokenAmountParsed: number; | |
| }; | |
| export type PfEventCompleteEnhanced = PfEventComplete & {}; | |
| export function enhancePfEventTrade(event: PfEventTrade): PfEventTradeEnhanced | null { | |
| if (event === null || typeof event !== "object" || !(event instanceof PfEvent.type.TRADE)) { | |
| return null; | |
| } | |
| const e = event as PfEventTradeEnhanced; | |
| const curvePublicKey = findPfBondingCurveAddress(e.mint); | |
| const curveAssociatedPublicKey = findPfAssociatedBondingCurveAddress(e.mint, curvePublicKey); | |
| const realTokenReservesBefore = e.realTokenReserves + (e.isBuy ? e.tokenAmount : -e.tokenAmount); | |
| const realTokenReservesBeforeDeltaInitial = (PF_CONFIG_INITIAL_REAL_TOKEN_RESERVES - realTokenReservesBefore); | |
| const realTokenReservesDeltaInitial = (PF_CONFIG_INITIAL_REAL_TOKEN_RESERVES - e.realTokenReserves); | |
| const virtualTokenReservesBefore = e.virtualTokenReserves + (e.isBuy ? e.tokenAmount : -e.tokenAmount); | |
| const virtualSolReservesAfter = e.virtualSolReserves + (e.isBuy ? -e.solAmount : e.solAmount); | |
| const bondingProgressBefore = realTokenReservesBeforeDeltaInitial <= 0n ? 0.0 | |
| : (1.0 - (Number(realTokenReservesBefore * 16384n / PF_CONFIG_INITIAL_REAL_TOKEN_RESERVES) / 16384.0)); | |
| const bondingProgressAfter = realTokenReservesDeltaInitial <= 0n ? 0.0 | |
| : (1.0 - (Number(e.realTokenReserves * 16384n / PF_CONFIG_INITIAL_REAL_TOKEN_RESERVES) / 16384.0)); | |
| const kothProgressBefore = realTokenReservesBeforeDeltaInitial <= 0n ? 0.0 | |
| : realTokenReservesBeforeDeltaInitial >= PF_CONFIG_KOTH_TARGET ? 1.0 | |
| : Math.min(1, (Number((realTokenReservesBeforeDeltaInitial * 16384n) / PF_CONFIG_KOTH_TARGET) / 16384.0)); | |
| const kothProgressAfter = realTokenReservesDeltaInitial <= 0n ? 0.0 | |
| : realTokenReservesDeltaInitial >= PF_CONFIG_KOTH_TARGET ? 1.0 | |
| : Math.min(1, (Number((realTokenReservesDeltaInitial * 16384n) / PF_CONFIG_KOTH_TARGET) / 16384.0)); | |
| const priceBefore = calculatePfTokenPriceInShiftedLamports(virtualTokenReservesBefore, virtualSolReservesAfter); | |
| const priceAfter = calculatePfTokenPriceInShiftedLamports(e.virtualTokenReserves, e.virtualSolReserves); | |
| const priceAverage = Math.floor(Math.sqrt(Number(priceBefore * priceAfter))); | |
| const pricePaid = calculatePfTokenPriceInShiftedLamports(e.tokenAmount, e.solAmount); | |
| e.bondingCurve = curvePublicKey; | |
| e.associatedBondingCurve = curveAssociatedPublicKey; | |
| e.bondingProgressBefore = bondingProgressBefore; | |
| e.bondingProgressAfter = bondingProgressAfter; | |
| e.kothProgressBefore = kothProgressBefore; | |
| e.kothProgressAfter = kothProgressAfter; | |
| e.priceBefore = shiftedLamportsToSol(priceBefore); | |
| e.priceAfter = shiftedLamportsToSol(priceAfter); | |
| e.priceAverage = shiftedLamportsToSol(priceAverage); | |
| e.pricePaid = shiftedLamportsToSol(pricePaid); | |
| e.solAmountParsed = lamportsToSol(e.solAmount); | |
| e.tokenAmountParsed = lamportsToSol(e.tokenAmount, PF_CONFIG_DECIMALS_TOKEN_PRODUCT); | |
| return e; | |
| } | |
| export function enhancePfEvents(rawEvents: PfEvent[]): PfEventEnhanced[] { | |
| if (!Array.isArray(rawEvents) || !rawEvents.length) { | |
| return []; | |
| } | |
| const events = rawEvents as PfEventEnhanced[]; | |
| for (const event of events) { | |
| const type = PfType.of(event); | |
| Object.defineProperty(event, "type", { | |
| value: type.description, | |
| enumerable: true, | |
| }); | |
| switch (type) { | |
| case PfEvent.type.TRADE.symbol: | |
| enhancePfEventTrade(event as PfEventTrade); | |
| break; | |
| // case PfEvent.type.CREATE: /* ... */ break; | |
| // case PfEvent.type.COMPLETE: /* ... */ break; | |
| } | |
| } | |
| return events; | |
| } | |
| (async function () { | |
| await main( | |
| process.env.SOL_RPC_URL || "https://mainnet.helius-rpc.com/?api-key=00000000-0000-0000-0000-000000000000", | |
| ); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment