Skip to content

Instantly share code, notes, and snippets.

@rubpy
Created June 20, 2024 18:39
Show Gist options
  • Save rubpy/6c57e9d12acd4b6ed84e9f205372631d to your computer and use it in GitHub Desktop.
Save rubpy/6c57e9d12acd4b6ed84e9f205372631d to your computer and use it in GitHub Desktop.
Fetching Pump.fun bonding curve state and calculating price of token/SOL.
import * as web3 from "@solana/web3.js";
//////////////////////////////////////////////////
function readBytes(buf: Buffer, offset: number, length: number): Buffer {
const end = offset + length;
if (buf.byteLength < end) throw new RangeError("range out of bounds");
return buf.subarray(offset, end);
}
function readBigUintLE(buf: Buffer, offset: number, length: number): bigint {
switch (length) {
case 1: return BigInt(buf.readUint8(offset));
case 2: return BigInt(buf.readUint16LE(offset));
case 4: return BigInt(buf.readUint32LE(offset));
case 8: return buf.readBigUint64LE(offset);
}
throw new Error(`unsupported data size (${length} bytes)`);
}
function readBoolean(buf: Buffer, offset: number, length: number): boolean {
const data = readBytes(buf, offset, length);
for (const b of data) {
if (b) return true;
}
return false;
}
//////////////////////////////////////////////////
const PUMP_CURVE_TOKEN_DECIMALS = 6;
// Calculated as the first 8 bytes of: `sha256("account:BondingCurve")`.
const PUMP_CURVE_STATE_SIGNATURE = Uint8Array.from([0x17, 0xb7, 0xf8, 0x37, 0x60, 0xd8, 0xac, 0x60]);
const PUMP_CURVE_STATE_SIZE = 0x29;
const PUMP_CURVE_STATE_OFFSETS = {
VIRTUAL_TOKEN_RESERVES: 0x08,
VIRTUAL_SOL_RESERVES: 0x10,
REAL_TOKEN_RESERVES: 0x18,
REAL_SOL_RESERVES: 0x20,
TOKEN_TOTAL_SUPPLY: 0x28,
COMPLETE: 0x30,
};
interface PumpCurveState {
virtualTokenReserves: bigint
virtualSolReserves: bigint
realTokenReserves: bigint
realSolReserves: bigint
tokenTotalSupply: bigint
complete: boolean
}
// Fetches account data of a Pump.fun bonding curve, and deserializes it
// according to `accounts.BondingCurve` (see: Pump.fun program's Anchor IDL).
async function getPumpCurveState(conn: web3.Connection, curveAddress: web3.PublicKey): Promise<PumpCurveState> {
const response = await conn.getAccountInfo(curveAddress);
if (!response || !response.data || response.data.byteLength < PUMP_CURVE_STATE_SIGNATURE.byteLength + PUMP_CURVE_STATE_SIZE) {
throw new Error("unexpected curve state");
}
const idlSignature = readBytes(response.data, 0, PUMP_CURVE_STATE_SIGNATURE.byteLength);
if (idlSignature.compare(PUMP_CURVE_STATE_SIGNATURE) !== 0) {
throw new Error("unexpected curve state IDL signature");
}
return {
virtualTokenReserves: readBigUintLE(response.data, PUMP_CURVE_STATE_OFFSETS.VIRTUAL_TOKEN_RESERVES, 8),
virtualSolReserves: readBigUintLE(response.data, PUMP_CURVE_STATE_OFFSETS.VIRTUAL_SOL_RESERVES, 8),
realTokenReserves: readBigUintLE(response.data, PUMP_CURVE_STATE_OFFSETS.REAL_TOKEN_RESERVES, 8),
realSolReserves: readBigUintLE(response.data, PUMP_CURVE_STATE_OFFSETS.REAL_SOL_RESERVES, 8),
tokenTotalSupply: readBigUintLE(response.data, PUMP_CURVE_STATE_OFFSETS.TOKEN_TOTAL_SUPPLY, 8),
complete: readBoolean(response.data, PUMP_CURVE_STATE_OFFSETS.COMPLETE, 1),
};
}
// Calculates token price (in SOL) of a Pump.fun bonding curve.
function calculatePumpCurvePrice(curveState: PumpCurveState): number {
if (curveState === null || typeof curveState !== "object"
|| !(typeof curveState.virtualTokenReserves === "bigint" && typeof curveState.virtualSolReserves === "bigint")) {
throw new TypeError("curveState must be a PumpCurveState");
}
if (curveState.virtualTokenReserves <= 0n || curveState.virtualSolReserves <= 0n) {
throw new RangeError("curve state contains invalid reserve data");
}
return (Number(curveState.virtualSolReserves) / web3.LAMPORTS_PER_SOL) / (Number(curveState.virtualTokenReserves) / 10 ** PUMP_CURVE_TOKEN_DECIMALS);
}
//////////////////////////////////////////////////
(async (rpcUrl: string) => {
const conn = new web3.Connection(rpcUrl, "confirmed");
const curveAddress = new web3.PublicKey("5BwXbPNGbfd2UuE8rkvASmJYXWXSiqmrhqJ1FX6rQnKd");
const curveState = await getPumpCurveState(conn, curveAddress);
if (!curveState) return;
const tokenPriceSol = calculatePumpCurvePrice(curveState);
//////////////////////////////////////////////////
console.log("Token price:");
console.log(` ${(tokenPriceSol.toFixed(10))} SOL`);
})(process.env.SOL_RPC_URL || "https://mainnet.helius-rpc.com/?api-key=00000000-0000-0000-0000-000000000000");
@syahrul12345
Copy link

the key is to stop making shit up

@rubpy
Copy link
Author

rubpy commented Jan 9, 2025

the key is to stop making shit up

@syahrul12345 I don't know who you're accusing of "making shit up", because the formula used in this script is exactly what Pump.fun frontend uses.

Pump.fun frontend source code

@cuddeford
Copy link

Thanks for the gist. Do you have a calculation for the bonding curve progress too?

@rubpy
Copy link
Author

rubpy commented Feb 6, 2025

@cuddeford you can do something like...

// NOTE: this is not a constant pre-defined within the Pump.fun program executable;
//       it resides in the mutable (on-chain) account `4wTV1YmiEkRvAtNtsSGPtUrqRYQMe5SKy2uB4Jjaxnjf`,
//       and as such the value may change in the future.
const INITIAL_REAL_TOKEN_RESERVES = 793100000000000n;

// ...

const bondingCurveState = {
  virtualSolReserves: 70750300176n,
  virtualTokenReserves: 454980409814361n,
  realSolReserves: 40750300176n,
  realTokenReserves: 175080409814361n,   //   <——————
};

const bondingProgress = bondingCurveState.realTokenReserves >= INITIAL_REAL_TOKEN_RESERVES ? 0
  : (1 - (Number(bondingCurveState.realTokenReserves * 10000n / INITIAL_REAL_TOKEN_RESERVES) / 10000));

console.log(`${Math.round(bondingProgress * 10000) / 100}%`);
// Output:   77.93%

@kristijorgji
Copy link

this isn't working

@rubpy
Copy link
Author

rubpy commented Feb 6, 2025

@kristijorgji what isn't working?

Although it's a pretty old gist, it definitely still works. I think what many have struggled with is attempting to use token mint address (it's those addresses that end with the pump suffix, in most cases) as curveAddress.

If that's the issue that you are currently facing, you can derive one address from the other, as such:

// ...

// ┌──── ➕ ➕ ➕ ➕ ➕ ➕ ➕ ➕ ➕ ➕ ➕ ➕ ➕ ➕ ➕ ────┐
const PUMP_PROGRAM_ID = new web3.PublicKey("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P");
const PUMP_CURVE_SEED = Buffer.from("bonding-curve");

function findPumpCurveAddress(tokenMint: web3.PublicKey): web3.PublicKey {
  return web3.PublicKey.findProgramAddressSync([
    PUMP_CURVE_SEED,
    tokenMint.toBuffer(),
  ], PUMP_PROGRAM_ID)[0];
}
// └──── ➕ ➕ ➕ ➕ ➕ ➕ ➕ ➕ ➕ ➕ ➕ ➕ ➕ ➕ ➕ ────┘

(async (rpcUrl: string) => {

  const conn = new web3.Connection(rpcUrl, "confirmed");

  // ┌──── ➕ ➕ ➕ ➕ ➕ ➕ ➕ ➕ ➕ ➕ ➕ ➕ ➕ ➕ ➕ ────┐
  const tokenMint = new web3.PublicKey("GjSn1XHncttWZtx9u6JB9BNM3QYqiumXfGbtkm4ypump");
  const curveAddress = findPumpCurveAddress(tokenMint);
  // └──── ➕ ➕ ➕ ➕ ➕ ➕ ➕ ➕ ➕ ➕ ➕ ➕ ➕ ➕ ➕ ────┘

  const curveState = await getPumpCurveState(conn, curveAddress);
  if (!curveState) return;

  const tokenPriceSol = calculatePumpCurvePrice(curveState);

  //////////////////////////////////////////////////

  console.log("Token price:");
  console.log(`  ${(tokenPriceSol.toFixed(10))} SOL`);

})(process.env.SOL_RPC_URL || "https://mainnet.helius-rpc.com/?api-key=00000000-0000-0000-0000-000000000000");

@elmgo
Copy link

elmgo commented Apr 2, 2025

This works great! I was searching everywhere for this. thanks!

@DominicOrga
Copy link

Thanks for this. Just tested and I can guarantee that the SOL price is correct. Just make sure that you are using the token's bonding curve address and not the token address itself.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment