import { Program } from "@coral-xyz/anchor"; import { coinCreatorVaultAtaPda, coinCreatorVaultAuthorityPda, OnlinePumpAmmSdk, PUMP_AMM_SDK, PumpAmmAdminSdk, } from "@pump-fun/pump-swap-sdk"; import { createAssociatedTokenAccountIdempotentInstruction, getAssociatedTokenAddressSync, NATIVE_MINT, TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID, unpackAccount, unpackMint, } from "@solana/spl-token"; import { AccountInfo, ComputeBudgetProgram, Connection, PublicKey, PublicKeyInitData, TransactionInstruction, TransactionMessage, VersionedTransaction, } from "@solana/web3.js"; import BN from "bn.js"; import { CreatorFeeBpsOutOfRangeError, CreatorFeeNotAllowedForCashbackCoinError, CreatorFeeNotConfigurableError, CreatorFeeNotConfigurableForQuoteError, CtoNotAllowedForMayhemCoinError, HolderRewardCreatorImmutableError, HolderRewardDisabledError, UnsupportedQuoteMintError, } from "./errors"; import { Pump } from "./idl/pump"; import { PumpAmm } from "./idl/pump_amm"; import { bondingCurvePda, canonicalPumpPoolPdaWithQuote, creatorVaultPda, feeSharingConfigPda, GLOBAL_PDA, GLOBAL_VOLUME_ACCUMULATOR_PDA, holderRewardsPda, isLegacyQuoteMint, normalizeQuoteMint, PUMP_FEE_CONFIG_PDA, QUOTE_CONTROL_PDA, quoteAta, userVolumeAccumulatorPda, } from "./pda"; import { ADMIN_CTO_COMPUTE_UNIT_LIMIT, getPumpAmmProgram, getPumpProgram, PUMP_SDK, PUMP_TOKEN_MINT, } from "./sdk"; import { BondingCurve, FeeConfig, Fees, Global, GlobalVolumeAccumulator, MinimumDistributableFeeEvent, QuoteControl, QuoteMintSource, ResolvedQuoteMint, SupportedQuoteMint, UserVolumeAccumulator, UserVolumeAccumulatorTotalStats, } from "./state"; import { currentDayTokens, totalUnclaimedTokens } from "./tokenIncentives"; export const OFFLINE_PUMP_PROGRAM = getPumpProgram(null as any as Connection); // Standard Solana RPCs reject `getMultipleAccounts` with more than this many // keys ("failed to get info for accounts"); some providers accept more. const GET_MULTIPLE_ACCOUNTS_MAX_KEYS = 100; /** * `connection.getMultipleAccountsInfo` over any number of keys, in slices of * `GET_MULTIPLE_ACCOUNTS_MAX_KEYS` fetched concurrently (web3.js does not * slice by itself). No RPC call for an empty list. */ async function getMultipleAccountsInfoChunked( connection: Connection, keys: readonly PublicKey[], ): Promise<(AccountInfo | null)[]> { const chunks = Array.from( { length: Math.ceil(keys.length / GET_MULTIPLE_ACCOUNTS_MAX_KEYS) }, (_, index) => keys.slice( index * GET_MULTIPLE_ACCOUNTS_MAX_KEYS, (index + 1) * GET_MULTIPLE_ACCOUNTS_MAX_KEYS, ), ); const results = await Promise.all( chunks.map((chunk) => connection.getMultipleAccountsInfo(chunk)), ); return results.flat(); } /** Whether `programId` is a program a quote mint may be owned by. */ function isQuoteTokenProgram(programId: PublicKey): boolean { return ( programId.equals(TOKEN_PROGRAM_ID) || programId.equals(TOKEN_2022_PROGRAM_ID) ); } /** * The token program a fetched quote mint belongs to, which every quote-side * ATA must be derived with. A mint owned by anything else cannot be a quote. */ function quoteTokenProgramOf( mint: PublicKey, mintAccountInfo: AccountInfo, ): PublicKey { const { owner } = mintAccountInfo; if (!isQuoteTokenProgram(owner)) { throw new Error( `Quote mint ${mint.toBase58()} is owned by ${owner.toBase58()}, not by SPL Token or Token-2022`, ); } return owner; } /** * Whether a fetched account is a token account of `tokenProgram`, which is * what both fee programs deserialize a vault ATA as (`TokenAccount`). Any * other account at an ATA address (anyone can fund a system account there; the * address is public) counts as absent, so it is never put in a collect * instruction the program would then fail on, and never unpacked. */ function isTokenAccount( accountInfo: AccountInfo | null, tokenProgram: PublicKey, ): accountInfo is AccountInfo { return accountInfo !== null && accountInfo.owner.equals(tokenProgram); } /** A quote mint with the program that owns it. */ interface QuoteMintProgram { mint: PublicKey; quoteTokenProgram: PublicKey; } /** * The quote mints `create_v2` accepts, from the fetched `Global` and * `QuoteControl` accounts: SOL first, then the non-zero `Global` whitelist * slots, then the quote-control entries `Global` does not already list. A * mint in both lists is reported once, as `Global`'s, because that is the * seeding `create_v2` applies to it. A missing `QuoteControl` PDA is an empty * list, as the program treats it. */ function supportedQuoteMints( globalAccountInfo: AccountInfo | null, quoteControlAccountInfo: AccountInfo | null, ): SupportedQuoteMint[] { if (!globalAccountInfo) { throw new Error(`Global account not found: ${GLOBAL_PDA.toBase58()}`); } const global = PUMP_SDK.decodeGlobal(globalAccountInfo); // Strict on purpose: this list decides which vaults get swept, so an account // at the PDA that is not a well-formed `QuoteControl` is an error, not an // empty list (`decodeQuoteControlNullable` would swallow it). const quoteControl = quoteControlAccountInfo ? PUMP_SDK.decodeQuoteControl(quoteControlAccountInfo) : null; const supported: SupportedQuoteMint[] = [ { mint: NATIVE_MINT, source: "sol", initialVirtualQuoteReserves: global.initialVirtualSolReserves, }, ...global.whitelistedQuoteMints .filter((mint) => !isLegacyQuoteMint(mint)) .map((mint) => ({ mint, source: "global" as const, initialVirtualQuoteReserves: global.initialVirtualQuoteReserves, })), ]; for (const entry of quoteControl?.mints ?? []) { if (!supported.some(({ mint }) => mint.equals(entry.mint))) { supported.push({ mint: entry.mint, source: "quoteControl", initialVirtualQuoteReserves: entry.initialVirtualQuoteReserves, }); } } return supported; } /** * Token amount held by a fetched token account, or zero when it is missing or * is not a token account of `tokenProgram` (see `isTokenAccount`). */ function tokenAccountAmount( address: PublicKey, accountInfo: AccountInfo | null, tokenProgram: PublicKey, ): BN { if (!isTokenAccount(accountInfo, tokenProgram)) { return new BN(0); } const { amount } = unpackAccount(address, accountInfo, tokenProgram); return new BN(amount.toString()); } export class OnlinePumpSdk { private readonly connection: Connection; private readonly pumpProgram: Program; private readonly offlinePumpProgram: Program; private readonly pumpAmmProgram: Program; private readonly pumpAmmSdk: OnlinePumpAmmSdk; private readonly pumpAmmAdminSdk: PumpAmmAdminSdk; constructor(connection: Connection) { this.connection = connection; this.pumpProgram = getPumpProgram(connection); this.offlinePumpProgram = OFFLINE_PUMP_PROGRAM; this.pumpAmmProgram = getPumpAmmProgram(connection); this.pumpAmmSdk = new OnlinePumpAmmSdk(connection); this.pumpAmmAdminSdk = new PumpAmmAdminSdk(connection); } /** * The pump `Global` account, decoded by `PumpSdk.decodeGlobal` so the live * account still decodes while it is shorter than `GLOBAL_SIZE` (before * `extend_account` adds the newest fields). */ async fetchGlobal(): Promise { const accountInfo = await this.connection.getAccountInfo(GLOBAL_PDA); if (!accountInfo) { throw new Error(`Global account not found: ${GLOBAL_PDA.toBase58()}`); } return PUMP_SDK.decodeGlobal(accountInfo); } async fetchFeeConfig(): Promise { const accountInfo = await this.connection.getAccountInfo(PUMP_FEE_CONFIG_PDA); if (!accountInfo) { throw new Error( `Fee config account not found: ${PUMP_FEE_CONFIG_PDA.toBase58()}`, ); } return PUMP_SDK.decodeFeeConfig(accountInfo); } /** * The pump `QuoteControl` PDA, or `null` while it is uninitialized (the * program treats a missing account as an empty mint list). Only a missing * account yields `null`: an account at the PDA that does not decode as a * `QuoteControl` throws (Anchor `fetchNullable`), unlike * `PumpSdk.decodeQuoteControlNullable`, which maps decode failures to * `null`. */ async fetchQuoteControl(): Promise { return await this.pumpProgram.account.quoteControl.fetchNullable( QUOTE_CONTROL_PDA, ); } /** * The program that owns `quoteMint`, which every quote-side ATA and every * `quoteTokenProgram` parameter in this SDK must use. WSOL and the zero key * resolve to `TOKEN_PROGRAM_ID` without an RPC call; any other mint is * fetched and must be owned by SPL Token or Token-2022 (a missing account or * another owner throws). That includes the Token-2022 native mint, which * resolves to its owner, `TOKEN_2022_PROGRAM_ID`: it is SOL-like for fee * selection only, and `create_v2` rejects it as a quote. */ async fetchQuoteTokenProgram(quoteMint: PublicKey): Promise { if (isLegacyQuoteMint(quoteMint)) { return TOKEN_PROGRAM_ID; } const mintAccountInfo = await this.connection.getAccountInfo(quoteMint); if (!mintAccountInfo) { throw new Error(`Quote mint account not found: ${quoteMint.toBase58()}`); } return quoteTokenProgramOf(quoteMint, mintAccountInfo); } /** * Every quote mint `create_v2` accepts right now, in one RPC round-trip: SOL * (as `NATIVE_MINT`, seeded from `Global.initialVirtualSolReserves`), the * non-zero `Global.whitelistedQuoteMints` slots, then the `QuoteControl` * entries `Global` does not already list. The PDA is the only source of * quote-control mints; while it is uninitialized the list is SOL plus the * `Global` whitelist. */ async fetchSupportedQuoteMints(): Promise { const [globalAccountInfo, quoteControlAccountInfo] = await this.connection.getMultipleAccountsInfo([ GLOBAL_PDA, QUOTE_CONTROL_PDA, ]); return supportedQuoteMints(globalAccountInfo, quoteControlAccountInfo); } /** * `fetchSupportedQuoteMints` narrowed to one mint, plus what a builder needs * for it: the owning token program and the mint's decimals. SOL-like inputs * (`NATIVE_MINT`, the zero key, `undefined`) resolve to the `sol` entry. One * RPC round-trip. * * @throws {UnsupportedQuoteMintError} when the mint is in neither list, * `create_v2`'s first admission check. A listed mint can still be rejected * on chain: a `quoteTokenProgram` that is not its owner or a Token-2022 * extension outside the xStock set (6063), or `mayhemMode` with a mint * admitted only through `QuoteControl` (6071). */ async resolveQuoteMint( quoteMint: PublicKey = NATIVE_MINT, ): Promise { const mint = normalizeQuoteMint(quoteMint); const [globalAccountInfo, quoteControlAccountInfo, mintAccountInfo] = await this.connection.getMultipleAccountsInfo([ GLOBAL_PDA, QUOTE_CONTROL_PDA, mint, ]); const supported = supportedQuoteMints( globalAccountInfo, quoteControlAccountInfo, ).find((entry) => entry.mint.equals(mint)); if (!supported) { throw new UnsupportedQuoteMintError(mint); } if (!mintAccountInfo) { throw new Error(`Quote mint account not found: ${mint.toBase58()}`); } const quoteTokenProgram = quoteTokenProgramOf(mint, mintAccountInfo); const { decimals } = unpackMint(mint, mintAccountInfo, quoteTokenProgram); return { ...supported, quoteTokenProgram, decimals }; } /** * @deprecated use PumpSdk.decodeBondingCurveNullable instead. */ async fetchBondingCurve(mint: PublicKeyInitData): Promise { const bondingCurve = bondingCurvePda(mint); const accountInfo = await this.connection.getAccountInfo(bondingCurve); if (!accountInfo) { throw new Error( `Bonding curve account not found: ${bondingCurve.toBase58()}`, ); } // Through the padding decoder: a 115-byte curve does not decode under the // vendored IDL otherwise. return PUMP_SDK.decodeBondingCurve(accountInfo); } /** * The state `buyV2Instructions` needs, plus the curve's normalized * `quoteMint` (`NATIVE_MINT` for SOL curves) and `quoteTokenProgram` (the * mint's owner) to forward to it. * * @param quoteMint - Optional hint: the curve's quote mint when the caller * already knows it (from an earlier fetch, `resolveQuoteMint`, or an * indexer). A curve stores its quote mint, so for a non-SOL curve the * token program is only known after decoding the curve: without the hint * that is a second RPC round-trip; with a correct hint the mint account * joins the first batch. A hint that does not match the curve is ignored * (the second round-trip happens). SOL curves never need one. */ async fetchBuyState( mint: PublicKey, user: PublicKey, tokenProgram: PublicKey = TOKEN_PROGRAM_ID, quoteMint?: PublicKey, ) { const quoteMintHint = this.quoteMintHint(quoteMint); const [ bondingCurveAccountInfo, associatedUserAccountInfo, quoteMintHintAccountInfo, ] = await this.connection.getMultipleAccountsInfo([ bondingCurvePda(mint), getAssociatedTokenAddressSync(mint, user, true, tokenProgram), ...(quoteMintHint ? [quoteMintHint] : []), ]); if (!bondingCurveAccountInfo) { throw new Error( `Bonding curve account not found for mint: ${mint.toBase58()}`, ); } const bondingCurve = PUMP_SDK.decodeBondingCurve(bondingCurveAccountInfo); const quote = await this.curveQuote( bondingCurve, quoteMintHint, quoteMintHintAccountInfo, ); return { bondingCurveAccountInfo, bondingCurve, associatedUserAccountInfo, ...quote, }; } /** * The state `sellV2Instructions` needs, plus the curve's normalized * `quoteMint` and `quoteTokenProgram`. `quoteMint` is the same optional * hint as on `fetchBuyState`. */ async fetchSellState( mint: PublicKey, user: PublicKey, tokenProgram: PublicKey = TOKEN_PROGRAM_ID, quoteMint?: PublicKey, ) { const quoteMintHint = this.quoteMintHint(quoteMint); const [ bondingCurveAccountInfo, associatedUserAccountInfo, quoteMintHintAccountInfo, ] = await this.connection.getMultipleAccountsInfo([ bondingCurvePda(mint), getAssociatedTokenAddressSync(mint, user, true, tokenProgram), ...(quoteMintHint ? [quoteMintHint] : []), ]); if (!bondingCurveAccountInfo) { throw new Error( `Bonding curve account not found for mint: ${mint.toBase58()}`, ); } if (!associatedUserAccountInfo) { throw new Error( `Associated token account not found for mint: ${mint.toBase58()} and user: ${user.toBase58()}`, ); } const bondingCurve = PUMP_SDK.decodeBondingCurve(bondingCurveAccountInfo); const quote = await this.curveQuote( bondingCurve, quoteMintHint, quoteMintHintAccountInfo, ); return { bondingCurveAccountInfo, bondingCurve, ...quote }; } // A quote mint worth fetching alongside the curve: SOL needs no lookup. private quoteMintHint(quoteMint: PublicKey | undefined): PublicKey | null { return quoteMint && !isLegacyQuoteMint(quoteMint) ? quoteMint : null; } /** * The quote mint a decoded curve trades in (normalized) and the program * that owns it. SOL curves need no RPC; another curve uses the hinted mint * account when the hint is the curve's own quote, else one more fetch. */ private async curveQuote( bondingCurve: BondingCurve, quoteMintHint: PublicKey | null, quoteMintHintAccountInfo: AccountInfo | null | undefined, ): Promise<{ quoteMint: PublicKey; quoteTokenProgram: PublicKey }> { const quoteMint = normalizeQuoteMint(bondingCurve.quoteMint); if (quoteMint.equals(NATIVE_MINT)) { return { quoteMint, quoteTokenProgram: TOKEN_PROGRAM_ID }; } if (quoteMintHint?.equals(quoteMint) && quoteMintHintAccountInfo) { return { quoteMint, quoteTokenProgram: quoteTokenProgramOf( quoteMint, quoteMintHintAccountInfo, ), }; } return { quoteMint, quoteTokenProgram: await this.fetchQuoteTokenProgram(quoteMint), }; } async fetchGlobalVolumeAccumulator(): Promise { return await this.pumpProgram.account.globalVolumeAccumulator.fetch( GLOBAL_VOLUME_ACCUMULATOR_PDA, ); } async fetchUserVolumeAccumulator( user: PublicKey, ): Promise { return await this.pumpProgram.account.userVolumeAccumulator.fetchNullable( userVolumeAccumulatorPda(user), ); } async fetchUserVolumeAccumulatorTotalStats( user: PublicKey, ): Promise { const userVolumeAccumulator = (await this.fetchUserVolumeAccumulator( user, )) ?? { totalUnclaimedTokens: new BN(0), totalClaimedTokens: new BN(0), currentSolVolume: new BN(0), }; const userVolumeAccumulatorAmm = (await this.pumpAmmSdk.fetchUserVolumeAccumulator(user)) ?? { totalUnclaimedTokens: new BN(0), totalClaimedTokens: new BN(0), currentSolVolume: new BN(0), }; return { totalUnclaimedTokens: userVolumeAccumulator.totalUnclaimedTokens.add( userVolumeAccumulatorAmm.totalUnclaimedTokens, ), totalClaimedTokens: userVolumeAccumulator.totalClaimedTokens.add( userVolumeAccumulatorAmm.totalClaimedTokens, ), currentSolVolume: userVolumeAccumulator.currentSolVolume.add( userVolumeAccumulatorAmm.currentSolVolume, ), }; } async collectCoinCreatorFeeInstructions( coinCreator: PublicKey, feePayer?: PublicKey, ): Promise { const quoteMint = NATIVE_MINT; const quoteTokenProgram = TOKEN_PROGRAM_ID; const coinCreatorVaultAuthority = coinCreatorVaultAuthorityPda(coinCreator); const coinCreatorVaultAta = coinCreatorVaultAtaPda( coinCreatorVaultAuthority, quoteMint, quoteTokenProgram, ); const coinCreatorTokenAccount = getAssociatedTokenAddressSync( quoteMint, coinCreator, true, quoteTokenProgram, ); const [coinCreatorVaultAtaAccountInfo, coinCreatorTokenAccountInfo] = await this.connection.getMultipleAccountsInfo([ coinCreatorVaultAta, coinCreatorTokenAccount, ]); return [ await this.offlinePumpProgram.methods .collectCreatorFee() .accountsPartial({ creator: coinCreator, }) .instruction(), ...(await PUMP_AMM_SDK.collectCoinCreatorFee( { coinCreator, quoteMint, quoteTokenProgram, coinCreatorVaultAuthority, coinCreatorVaultAta, coinCreatorTokenAccount, coinCreatorVaultAtaAccountInfo, coinCreatorTokenAccountInfo, }, feePayer, )), ]; } /** * Collects a creator's fees in one quote from the pump creator vault and * the pump-amm coin creator vault. `quoteTokenProgram` must be the program * that owns `quoteMint` (`fetchQuoteTokenProgram`). Assumes the creator's * quote ATA exists for a non-SOL quote: neither program creates it (see * `collectCoinCreatorFeeAllQuotesInstructions`, which does). */ async collectCoinCreatorFeeV2Instructions( coinCreator: PublicKey, quoteMint: PublicKey, quoteTokenProgram: PublicKey, feePayer?: PublicKey, ): Promise { const coinCreatorVaultAuthority = coinCreatorVaultAuthorityPda(coinCreator); const coinCreatorVaultAta = coinCreatorVaultAtaPda( coinCreatorVaultAuthority, quoteMint, quoteTokenProgram, ); const coinCreatorTokenAccount = getAssociatedTokenAddressSync( quoteMint, coinCreator, true, quoteTokenProgram, ); const [coinCreatorVaultAtaAccountInfo, coinCreatorTokenAccountInfo] = await this.connection.getMultipleAccountsInfo([ coinCreatorVaultAta, coinCreatorTokenAccount, ]); const pumpAmmInstructions = coinCreatorVaultAtaAccountInfo ? await PUMP_AMM_SDK.collectCoinCreatorFee( { coinCreator, quoteMint, quoteTokenProgram, coinCreatorVaultAuthority, coinCreatorVaultAta, coinCreatorTokenAccount, coinCreatorVaultAtaAccountInfo, coinCreatorTokenAccountInfo, }, feePayer, ) : []; return [ await this.offlinePumpProgram.methods .collectCreatorFeeV2() .accountsPartial({ creator: coinCreator, quoteMint, quoteTokenProgram, creatorVault: creatorVaultPda(coinCreator), }) .instruction(), ...pumpAmmInstructions, ]; } /** * Collects a creator's fees in every quote mint currently listed on `Global` * or in the `QuoteControl` PDA (`fetchSupportedQuoteMints`), plus * `extraQuoteMints`, from both the pump creator vault and the pump-amm coin * creator vault, into the creator's wallet and quote ATAs. For SOL: the * lamport vault (`collect_creator_fee`) and, when the AMM WSOL vault ATA * exists, the AMM leg (which wraps and unwraps by itself). For every other * quote, in this order: an idempotent create of the creator's quote ATA when * it is missing (neither program creates it), the pump * `collect_creator_fee_v2` leg only when the pump vault's quote ATA exists, * and the AMM leg only when its vault ATA exists; a quote with neither vault * ATA contributes nothing. "Exists" means a token account of the quote's * program: any other account at a vault ATA address is treated as absent * (the programs could not deserialize it), and one at the creator's ATA * address is created over. Three RPC round-trips: the supported mints, their * owners, then every vault and destination ATA (in batches of 100 keys). * * De-listing a mint from `QuoteControl` stops new creates only: curves * already quoted in it keep trading and accruing fees, and this method does * not know about them. Pass such mints (from an indexer, or the creator's * own coins' `bondingCurve.quoteMint`) as `extraQuoteMints`. A listed or * extra mint whose account is missing or is not a token mint is skipped * (no ATA can exist for it, so nothing is lost). * * Each quote's instructions are contiguous, so the result can be split per * quote when it does not fit one transaction (up to four instructions for * SOL, three per token quote). * * @param feePayer - Signs and pays ATA rent; defaults to the creator. * @param extraQuoteMints - Quote mints to sweep in addition to the listed * ones, e.g. de-listed mints the creator's coins are quoted in. SOL-like * keys and duplicates are ignored. */ async collectCoinCreatorFeeAllQuotesInstructions( coinCreator: PublicKey, feePayer?: PublicKey, extraQuoteMints: readonly PublicKey[] = [], ): Promise { const payer = feePayer ?? coinCreator; const supported = await this.fetchSupportedQuoteMints(); const mints = extraQuoteMints .map((mint) => normalizeQuoteMint(mint)) .reduce( (all, mint) => all.some((known) => known.equals(mint)) ? all : [...all, mint], supported.map(({ mint }) => mint), ); const quotes = await this.fetchQuoteMintPrograms(mints); const coinCreatorVaultAuthority = coinCreatorVaultAuthorityPda(coinCreator); const creatorVault = creatorVaultPda(coinCreator); // Per quote: pump vault ATA (unused for SOL), AMM vault ATA, destination. const vaultAccounts = quotes.map(({ mint, quoteTokenProgram }) => ({ pumpVaultAta: quoteAta(creatorVault, mint, quoteTokenProgram), ammVaultAta: coinCreatorVaultAtaPda( coinCreatorVaultAuthority, mint, quoteTokenProgram, ), creatorAta: getAssociatedTokenAddressSync( mint, coinCreator, true, quoteTokenProgram, ), })); const accountInfos = await getMultipleAccountsInfoChunked( this.connection, vaultAccounts.flatMap(({ pumpVaultAta, ammVaultAta, creatorAta }) => [ pumpVaultAta, ammVaultAta, creatorAta, ]), ); const instructions: TransactionInstruction[] = []; for (const [index, { mint, quoteTokenProgram }] of quotes.entries()) { const { ammVaultAta, creatorAta } = vaultAccounts[index]; const [ pumpVaultAtaAccountInfo, ammVaultAtaAccountInfo, creatorAtaAccountInfo, ] = accountInfos.slice(index * 3, index * 3 + 3); const isSol = mint.equals(NATIVE_MINT); const collectPump = isSol || isTokenAccount(pumpVaultAtaAccountInfo, quoteTokenProgram); const collectAmm = isTokenAccount( ammVaultAtaAccountInfo, quoteTokenProgram, ); if (!collectPump && !collectAmm) { continue; } // The AMM SDK wraps/unwraps WSOL itself; for a token quote both programs // expect the creator's ATA to exist already. if (!isSol && !isTokenAccount(creatorAtaAccountInfo, quoteTokenProgram)) { instructions.push( createAssociatedTokenAccountIdempotentInstruction( payer, creatorAta, coinCreator, mint, quoteTokenProgram, ), ); } if (collectPump) { instructions.push( isSol ? await this.offlinePumpProgram.methods .collectCreatorFee() .accountsPartial({ creator: coinCreator }) .instruction() : await this.offlinePumpProgram.methods .collectCreatorFeeV2() .accountsPartial({ creator: coinCreator, quoteMint: mint, quoteTokenProgram, creatorVault, }) .instruction(), ); } if (collectAmm) { instructions.push( ...(await PUMP_AMM_SDK.collectCoinCreatorFee( { coinCreator, quoteMint: mint, quoteTokenProgram, coinCreatorVaultAuthority, coinCreatorVaultAta: ammVaultAta, coinCreatorTokenAccount: creatorAta, coinCreatorVaultAtaAccountInfo: ammVaultAtaAccountInfo, coinCreatorTokenAccountInfo: creatorAtaAccountInfo, }, feePayer, )), ); } } return instructions; } /** * What a creator has waiting in each quote mint currently listed on `Global` * or in the `QuoteControl` PDA (`fetchSupportedQuoteMints`): the pump * creator vault (lamports above rent for SOL, the vault ATA's token amount * otherwise) and the pump-amm coin creator vault ATA. An account at a vault * ATA address that is not a token account of the quote's program counts as * zero. Up to four RPC round-trips (the vault batch in slices of 100 keys). * * Like `collectCoinCreatorFeeAllQuotesInstructions`, this does not see fees * in a mint that has since been de-listed: read those vault ATAs * (`quoteAta(creatorVaultPda(creator), mint, program)` and the pump-amm * `coinCreatorVaultAtaPda`) directly. A listed mint whose account is missing * or is not a token mint is left out. */ async getCreatorVaultQuoteBalances( creator: PublicKey, ): Promise { const quotes = await this.fetchSupportedQuoteMintPrograms(); const coinCreatorVaultAuthority = coinCreatorVaultAuthorityPda(creator); const creatorVault = creatorVaultPda(creator); const vaultAccounts = quotes.map(({ mint, quoteTokenProgram }) => ({ pumpVaultAta: quoteAta(creatorVault, mint, quoteTokenProgram), ammVaultAta: coinCreatorVaultAtaPda( coinCreatorVaultAuthority, mint, quoteTokenProgram, ), })); const [creatorVaultAccountInfo, ...accountInfos] = await getMultipleAccountsInfoChunked(this.connection, [ creatorVault, ...vaultAccounts.flatMap(({ pumpVaultAta, ammVaultAta }) => [ pumpVaultAta, ammVaultAta, ]), ]); const solVaultBalance = await this.creatorVaultLamportsAboveRent( creatorVaultAccountInfo, ); return quotes.map(({ mint, source, quoteTokenProgram }, index) => { const { pumpVaultAta, ammVaultAta } = vaultAccounts[index]; const [pumpVaultAtaAccountInfo, ammVaultAtaAccountInfo] = accountInfos.slice(index * 2, index * 2 + 2); const pumpVault = mint.equals(NATIVE_MINT) ? solVaultBalance : tokenAccountAmount( pumpVaultAta, pumpVaultAtaAccountInfo, quoteTokenProgram, ); const ammVault = tokenAccountAmount( ammVaultAta, ammVaultAtaAccountInfo, quoteTokenProgram, ); return { mint, source, quoteTokenProgram, pumpVault, ammVault, total: pumpVault.add(ammVault), }; }); } // Every supported quote mint with the program that owns it, dropping the // entries `fetchQuoteMintPrograms` cannot resolve. private async fetchSupportedQuoteMintPrograms(): Promise< (SupportedQuoteMint & QuoteMintProgram)[] > { const supported = await this.fetchSupportedQuoteMints(); const quotes = await this.fetchQuoteMintPrograms( supported.map(({ mint }) => mint), ); return quotes.map(({ mint, quoteTokenProgram }) => ({ // Present by construction: `fetchQuoteMintPrograms` only drops entries. ...supported.find((entry) => entry.mint.equals(mint))!, quoteTokenProgram, })); } // `mints` (normalized; SOL as `NATIVE_MINT`) with the program that owns each, // in order: SOL without a lookup, the others from one chunked fetch of their // mint accounts. A mint whose account is missing or is owned by neither token // program is dropped rather than thrown on: `add_quote_control_mint` does not // check the mint exists, and no ATA can exist for such a mint, so a sweep or // balance over the remaining mints is complete. private async fetchQuoteMintPrograms( mints: readonly PublicKey[], ): Promise { const tokenMints = mints.filter((mint) => !mint.equals(NATIVE_MINT)); const mintAccountInfos = await getMultipleAccountsInfoChunked( this.connection, tokenMints, ); const programs = new Map( tokenMints.flatMap((mint, index) => { const owner = mintAccountInfos[index]?.owner; return owner && isQuoteTokenProgram(owner) ? [[mint.toBase58(), owner] as const] : []; }), ); return mints.flatMap((mint) => { if (mint.equals(NATIVE_MINT)) { return [{ mint, quoteTokenProgram: TOKEN_PROGRAM_ID }]; } const quoteTokenProgram = programs.get(mint.toBase58()); return quoteTokenProgram ? [{ mint, quoteTokenProgram }] : []; }); } // `getCreatorVaultBalance` over an already fetched vault account. private async creatorVaultLamportsAboveRent( accountInfo: AccountInfo | null, ): Promise { if (accountInfo === null) { return new BN(0); } const rentExemptionLamports = await this.connection.getMinimumBalanceForRentExemption( accountInfo.data.length, ); if (accountInfo.lamports < rentExemptionLamports) { return new BN(0); } return new BN(accountInfo.lamports - rentExemptionLamports); } /** * `PumpSdk.adminCtoInstruction` for `mint`, with the signer defaulting to * `Global.adminSetCreatorAuthority` and every other account resolved from * chain: `Global` and the curve in one round-trip, plus the quote mint's * owner program for a token quote. Returned as * `[setComputeUnitLimit(ADMIN_CTO_COMPUTE_UNIT_LIMIT), admin_cto]`. * * Rejects up front, with typed errors, what the program would reject * (`admin_cto.rs`): a mayhem coin (`CtoNotAllowedForMayhemCoinError`, 6088), * changing a holder-reward coin's creator (`HolderRewardCreatorImmutableError`, * 6083; only `isHolderReward: true` again is allowed), converting while * `Global.isHolderRewardEnabled` is off (`HolderRewardDisabledError`, 6084), * a missing `newCreator` on the new-creator path or a present one on the * holder path (6089 / 6090), and for a `creatorFeeBps`: a SOL or * `Global`-whitelisted quote (`CreatorFeeNotConfigurableForQuoteError`, * 6091), a coin that stays cashback * (`CreatorFeeNotAllowedForCashbackCoinError`, 6080), the gate off * (`CreatorFeeNotConfigurableError`, 6077) and a rate outside * `1..=Global.maxConfigurableCreatorFeeBps` (`CreatorFeeBpsOutOfRangeError`, * 6078). The program alone judges the rest (a `newCreator` equal to a PDA, * a frozen shared vault, ...). */ async adminCtoInstructions( mint: PublicKey, { isHolderReward, creatorFeeBps, newCreator, adminSetCreatorAuthority, }: { isHolderReward?: boolean; creatorFeeBps?: BN; newCreator?: PublicKey; adminSetCreatorAuthority?: PublicKey; }, ): Promise { const { global, bondingCurve, quoteMint, quoteTokenProgram } = await this.fetchCurveWithQuote(mint); if (bondingCurve.isMayhemMode) { throw new CtoNotAllowedForMayhemCoinError(mint); } const toHolder = isHolderReward === true; if (bondingCurve.isHolderReward && !toHolder) { throw new HolderRewardCreatorImmutableError(mint); } if (toHolder) { if (!bondingCurve.isHolderReward && !global.isHolderRewardEnabled) { throw new HolderRewardDisabledError(); } if (newCreator) { throw new Error( "newCreator must be omitted when converting to holder rewards", ); } } else if (!newCreator) { throw new Error( "newCreator is required unless converting to holder rewards", ); } if (creatorFeeBps) { const quoteIsSolOrWhitelisted = isLegacyQuoteMint(bondingCurve.quoteMint) || global.whitelistedQuoteMints.some((whitelisted) => whitelisted.equals(bondingCurve.quoteMint), ); if (quoteIsSolOrWhitelisted) { throw new CreatorFeeNotConfigurableForQuoteError(quoteMint); } // The holder path clears cashback; the new-creator path keeps it. if (bondingCurve.isCashbackCoin && !toHolder) { throw new CreatorFeeNotAllowedForCashbackCoinError(mint); } if (!global.creatorFeeConfigurable) { throw new CreatorFeeNotConfigurableError(); } if ( creatorFeeBps.ltn(1) || creatorFeeBps.gt(global.maxConfigurableCreatorFeeBps) ) { throw new CreatorFeeBpsOutOfRangeError( creatorFeeBps, global.maxConfigurableCreatorFeeBps, ); } } return [ ComputeBudgetProgram.setComputeUnitLimit({ units: ADMIN_CTO_COMPUTE_UNIT_LIMIT, }), await PUMP_SDK.adminCtoInstruction({ adminSetCreatorAuthority: adminSetCreatorAuthority ?? global.adminSetCreatorAuthority, mint, currentCreator: bondingCurve.creator, quoteMint, quoteTokenProgram, isHolderReward, creatorFeeBps, newCreator, }), ]; } /** * `PumpSdk.updateHolderRewardConfigInstruction` with `authority` defaulting * to the current `Global.authority`, the only key the program accepts. */ async adminUpdateHolderRewardConfigInstruction({ isHolderRewardEnabled, holderRewardClaimAuthority, authority, }: { isHolderRewardEnabled: boolean; holderRewardClaimAuthority: PublicKey; authority?: PublicKey; }): Promise { return await PUMP_SDK.updateHolderRewardConfigInstruction({ authority: authority ?? (await this.fetchGlobal()).authority, isHolderRewardEnabled, holderRewardClaimAuthority, }); } /** * `PumpSdk.distributeFeeToHoldersInstruction` for `mint`, with the signer * defaulting to `Global.holderRewardClaimAuthority`, the quote read from * the curve, and the PDA's quote ATA passed as `holderRewardsTokenAccount` * when it exists (on a SOL quote it is a parked WSOL account the program * sweeps first; on a token quote it is the payout source and this throws * when it is missing, since there is nothing to pay from). Throws when the * coin is not a holder-reward coin. */ async distributeFeeToHoldersInstructions( mint: PublicKey, recipients: readonly { owner: PublicKey; amount: BN }[], holderRewardClaimAuthority?: PublicKey, ): Promise { const { global, bondingCurve, quoteMint, quoteTokenProgram } = await this.fetchCurveWithQuote(mint); if (!bondingCurve.isHolderReward) { throw new Error( `Mint ${mint.toBase58()} is not a holder-reward coin; nothing to distribute`, ); } const holderRewardsAta = quoteAta( holderRewardsPda(mint), quoteMint, quoteTokenProgram, ); const holderRewardsAtaAccountInfo = await this.connection.getAccountInfo(holderRewardsAta); if (!holderRewardsAtaAccountInfo && !quoteMint.equals(NATIVE_MINT)) { throw new Error( `Holder-rewards token account ${holderRewardsAta.toBase58()} does not exist; collect the coin's creator fees onto the PDA first`, ); } return [ await PUMP_SDK.distributeFeeToHoldersInstruction({ holderRewardClaimAuthority: holderRewardClaimAuthority ?? global.holderRewardClaimAuthority, mint, quoteMint, quoteTokenProgram, recipients, holderRewardsTokenAccount: holderRewardsAtaAccountInfo ? holderRewardsAta : undefined, }), ]; } /** * `Global` and `mint`'s curve in one round-trip, with the curve's quote * normalized (`NATIVE_MINT` for SOL curves) and the program that owns it * (a second round-trip for a token quote, none for SOL). */ private async fetchCurveWithQuote(mint: PublicKey): Promise<{ global: Global; bondingCurve: BondingCurve; quoteMint: PublicKey; quoteTokenProgram: PublicKey; }> { const [globalAccountInfo, bondingCurveAccountInfo] = await this.connection.getMultipleAccountsInfo([ GLOBAL_PDA, bondingCurvePda(mint), ]); if (!globalAccountInfo) { throw new Error(`Global account not found: ${GLOBAL_PDA.toBase58()}`); } if (!bondingCurveAccountInfo) { throw new Error( `Bonding curve account not found for mint: ${mint.toBase58()}`, ); } const global = PUMP_SDK.decodeGlobal(globalAccountInfo); const bondingCurve = PUMP_SDK.decodeBondingCurve(bondingCurveAccountInfo); const quoteMint = normalizeQuoteMint(bondingCurve.quoteMint); return { global, bondingCurve, quoteMint, quoteTokenProgram: await this.fetchQuoteTokenProgram(quoteMint), }; } /** * `PumpSdk.setQuoteControlAdminInstruction` with `authority` defaulting to * the current `Global.authority`, the only key the program accepts. */ async adminSetQuoteControlAdminInstruction({ newAdmin, authority, }: { newAdmin: PublicKey; authority?: PublicKey; }): Promise { return await PUMP_SDK.setQuoteControlAdminInstruction({ authority: authority ?? (await this.fetchGlobal()).authority, newAdmin, }); } /** * `PumpSdk.addQuoteControlMintInstruction` with `authority` defaulting to * the current `Global.authority` (pass `QuoteControl.admin` to sign as the * delegated admin instead). */ async adminAddQuoteControlMintInstruction({ quoteMint, initialVirtualQuoteReserves, authority, }: { quoteMint: PublicKey; initialVirtualQuoteReserves: BN; authority?: PublicKey; }): Promise { return await PUMP_SDK.addQuoteControlMintInstruction({ authority: authority ?? (await this.fetchGlobal()).authority, quoteMint, initialVirtualQuoteReserves, }); } /** * `PumpSdk.removeQuoteControlMintInstruction` with `authority` defaulting * to the current `Global.authority`. */ async adminRemoveQuoteControlMintInstruction({ quoteMint, authority, }: { quoteMint: PublicKey; authority?: PublicKey; }): Promise { return await PUMP_SDK.removeQuoteControlMintInstruction({ authority: authority ?? (await this.fetchGlobal()).authority, quoteMint, }); } /** * `PumpSdk.updateCreatorFeeConfigInstruction` with `authority` defaulting * to the current `Global.authority`, the only key the program accepts. */ async adminUpdateCreatorFeeConfigInstruction({ creatorFeeConfigurable, maxConfigurableCreatorFeeBps, authority, }: { creatorFeeConfigurable: boolean; maxConfigurableCreatorFeeBps: BN; authority?: PublicKey; }): Promise { return await PUMP_SDK.updateCreatorFeeConfigInstruction({ authority: authority ?? (await this.fetchGlobal()).authority, creatorFeeConfigurable, maxConfigurableCreatorFeeBps, }); } /** * `PumpSdk.setExoticFlatFeesInstruction` with `admin` defaulting to the * current `FeeConfig.admin`. */ async adminSetExoticFlatFeesInstruction({ exoticFlatFees, admin, }: { exoticFlatFees: Fees; admin?: PublicKey; }): Promise { return await PUMP_SDK.setExoticFlatFeesInstruction({ admin: admin ?? (await this.fetchFeeConfig()).admin, exoticFlatFees, }); } async getCreatorVaultBalance(creator: PublicKey): Promise { const creatorVault = creatorVaultPda(creator); const accountInfo = await this.connection.getAccountInfo(creatorVault); return await this.creatorVaultLamportsAboveRent(accountInfo); } async getCreatorVaultBalanceBothPrograms(creator: PublicKey): Promise { const balance = await this.getCreatorVaultBalance(creator); const ammBalance = await this.pumpAmmSdk.getCoinCreatorVaultBalance(creator); return balance.add(ammBalance); } async adminUpdateTokenIncentives( startTime: BN, endTime: BN, dayNumber: BN, tokenSupplyPerDay: BN, secondsInADay: BN = new BN(86_400), mint: PublicKey = PUMP_TOKEN_MINT, tokenProgram: PublicKey = TOKEN_2022_PROGRAM_ID, ): Promise { const { authority } = await this.fetchGlobal(); return await this.offlinePumpProgram.methods .adminUpdateTokenIncentives( startTime, endTime, secondsInADay, dayNumber, tokenSupplyPerDay, ) .accountsPartial({ authority, mint, tokenProgram, }) .instruction(); } async adminUpdateTokenIncentivesBothPrograms( startTime: BN, endTime: BN, dayNumber: BN, tokenSupplyPerDay: BN, secondsInADay: BN = new BN(86_400), mint: PublicKey = PUMP_TOKEN_MINT, tokenProgram: PublicKey = TOKEN_2022_PROGRAM_ID, ): Promise { return [ await this.adminUpdateTokenIncentives( startTime, endTime, dayNumber, tokenSupplyPerDay, secondsInADay, mint, tokenProgram, ), await this.pumpAmmAdminSdk.adminUpdateTokenIncentives( startTime, endTime, dayNumber, tokenSupplyPerDay, secondsInADay, mint, tokenProgram, ), ]; } async claimTokenIncentives( user: PublicKey, payer: PublicKey, ): Promise { const { mint } = await this.fetchGlobalVolumeAccumulator(); if (mint.equals(PublicKey.default)) { return []; } const [mintAccountInfo, userAccumulatorAccountInfo] = await this.connection.getMultipleAccountsInfo([ mint, userVolumeAccumulatorPda(user), ]); if (!mintAccountInfo) { return []; } if (!userAccumulatorAccountInfo) { return []; } return [ await this.offlinePumpProgram.methods .claimTokenIncentives() .accountsPartial({ user, payer, mint, tokenProgram: mintAccountInfo.owner, }) .instruction(), ]; } async claimTokenIncentivesBothPrograms( user: PublicKey, payer: PublicKey, ): Promise { return [ ...(await this.claimTokenIncentives(user, payer)), ...(await this.pumpAmmSdk.claimTokenIncentives(user, payer)), ]; } async getTotalUnclaimedTokens(user: PublicKey): Promise { const [ globalVolumeAccumulatorAccountInfo, userVolumeAccumulatorAccountInfo, ] = await this.connection.getMultipleAccountsInfo([ GLOBAL_VOLUME_ACCUMULATOR_PDA, userVolumeAccumulatorPda(user), ]); if ( !globalVolumeAccumulatorAccountInfo || !userVolumeAccumulatorAccountInfo ) { return new BN(0); } const globalVolumeAccumulator = PUMP_SDK.decodeGlobalVolumeAccumulator( globalVolumeAccumulatorAccountInfo, ); const userVolumeAccumulator = PUMP_SDK.decodeUserVolumeAccumulator( userVolumeAccumulatorAccountInfo, ); return totalUnclaimedTokens(globalVolumeAccumulator, userVolumeAccumulator); } async getTotalUnclaimedTokensBothPrograms(user: PublicKey): Promise { return (await this.getTotalUnclaimedTokens(user)).add( await this.pumpAmmSdk.getTotalUnclaimedTokens(user), ); } async getCurrentDayTokens(user: PublicKey): Promise { const [ globalVolumeAccumulatorAccountInfo, userVolumeAccumulatorAccountInfo, ] = await this.connection.getMultipleAccountsInfo([ GLOBAL_VOLUME_ACCUMULATOR_PDA, userVolumeAccumulatorPda(user), ]); if ( !globalVolumeAccumulatorAccountInfo || !userVolumeAccumulatorAccountInfo ) { return new BN(0); } const globalVolumeAccumulator = PUMP_SDK.decodeGlobalVolumeAccumulator( globalVolumeAccumulatorAccountInfo, ); const userVolumeAccumulator = PUMP_SDK.decodeUserVolumeAccumulator( userVolumeAccumulatorAccountInfo, ); return currentDayTokens(globalVolumeAccumulator, userVolumeAccumulator); } async getCurrentDayTokensBothPrograms(user: PublicKey): Promise { return (await this.getCurrentDayTokens(user)).add( await this.pumpAmmSdk.getCurrentDayTokens(user), ); } async syncUserVolumeAccumulatorBothPrograms( user: PublicKey, ): Promise { return [ await PUMP_SDK.syncUserVolumeAccumulator(user), await PUMP_AMM_SDK.syncUserVolumeAccumulator(user), ]; } /** * Gets the minimum distributable fee for a token's fee sharing configuration. * * This method handles both graduated (AMM) and non-graduated (bonding curve) tokens. * For graduated tokens, it automatically consolidates fees from the AMM vault before * calculating the minimum distributable fee. * * @param mint - The mint address of the token * @param simulationSigner - Optional signer address for transaction simulation. * Must have a non-zero SOL balance. Defaults to a known funded address. * @param options - Quote-mint-specific parameters. Without them the coin is * treated as SOL-quoted, exactly as before these options existed. * @param options.quoteMint - The coin's quote mint (`bondingCurve.quoteMint` * can be passed as is). Selects the canonical pool, so `isGraduated` is * right for a non-SOL coin, and the consolidation instruction * (`transferCreatorFeesToPumpV2`). Note that the on-chain * `get_minimum_distributable_fee` view reads the lamport creator vault * only, so for a non-SOL quote `minimumRequired`, `distributableFees` and * `canDistribute` still describe SOL fees, not the quote-token fees (the * consolidation moves tokens the view does not look at); the result's * `feesQuoteMint` says which mint the figures are in. Use * `getCreatorVaultQuoteBalances` for token-quote amounts. * @param options.quoteTokenProgram - The program that owns `quoteMint`; * fetched from the mint when omitted for a non-SOL quote (one extra * round-trip). * @param options.payer - Signer for the V2 consolidation instruction, which * may initialize the pump vault ATA; defaults to `simulationSigner`. * @returns The minimum distributable fee information including whether distribution is possible */ async getMinimumDistributableFee( mint: PublicKey, simulationSigner: PublicKey = new PublicKey( "UqN2p5bAzBqYdHXcgB6WLtuVrdvmy9JSAtgqZb3CMKw", ), options: { quoteMint?: PublicKey; quoteTokenProgram?: PublicKey; payer?: PublicKey; } = {}, ): Promise { const quoteMint = normalizeQuoteMint(options.quoteMint); const isNativeQuote = quoteMint.equals(NATIVE_MINT); const quoteTokenProgram = options.quoteTokenProgram ?? (isNativeQuote ? TOKEN_PROGRAM_ID : await this.fetchQuoteTokenProgram(quoteMint)); const payer = options.payer ?? simulationSigner; const sharingConfigPubkey = feeSharingConfigPda(mint); const poolAddress = canonicalPumpPoolPdaWithQuote(mint, quoteMint); const coinCreatorVaultAuthority = coinCreatorVaultAuthorityPda(sharingConfigPubkey); const ammVaultAta = coinCreatorVaultAtaPda( coinCreatorVaultAuthority, quoteMint, quoteTokenProgram, ); const [sharingConfigAccountInfo, poolAccountInfo, ammVaultAtaInfo] = await this.connection.getMultipleAccountsInfo([ sharingConfigPubkey, poolAddress, ammVaultAta, ]); if (!sharingConfigAccountInfo) { throw new Error(`Sharing config not found for mint: ${mint.toBase58()}`); } const sharingConfig = PUMP_SDK.decodeSharingConfig( sharingConfigAccountInfo, ); const instructions: TransactionInstruction[] = []; const isGraduated = poolAccountInfo !== null; if (isGraduated && ammVaultAtaInfo) { // Consolidate fees from AMM to bonding curve program for distribution const transferCreatorFeesToPumpIx = isNativeQuote ? await this.pumpAmmProgram.methods .transferCreatorFeesToPump() .accountsPartial({ wsolMint: NATIVE_MINT, tokenProgram: TOKEN_PROGRAM_ID, coinCreator: sharingConfigPubkey, }) .instruction() : await PUMP_SDK.transferCreatorFeesToPumpV2({ payer, mint, quoteMint, quoteTokenProgram, }); instructions.push(transferCreatorFeesToPumpIx); } const getMinFeeIx = await PUMP_SDK.getMinimumDistributableFee({ mint, sharingConfig, sharingConfigAddress: sharingConfigPubkey, }); instructions.push(getMinFeeIx); const { blockhash } = await this.connection.getLatestBlockhash(); const tx = new VersionedTransaction( new TransactionMessage({ payerKey: simulationSigner, recentBlockhash: blockhash, instructions, }).compileToV0Message(), ); const result = await this.connection.simulateTransaction(tx); let minimumDistributableFee: MinimumDistributableFeeEvent = { minimumRequired: new BN(0), distributableFees: new BN(0), canDistribute: false, }; if (!result.value.err) { const [data, encoding] = result.value.returnData?.data ?? []; if (data) { const buffer = Buffer.from(data, encoding as BufferEncoding); minimumDistributableFee = PUMP_SDK.decodeMinimumDistributableFee(buffer); } } return { ...minimumDistributableFee, isGraduated, // The only view the program has; a quote-aware one is a program change. feesQuoteMint: NATIVE_MINT, }; } /** * Gets the instructions to distribute creator fees for a token's fee sharing configuration. * * This method handles both graduated (AMM) and non-graduated (bonding curve) tokens. * For graduated tokens, it automatically includes an instruction to consolidate fees * from the AMM vault before distributing. * * @param mint - The mint address of the token * @param options - Optional quote-mint-specific parameters * @param options.quoteTokenProgram - The program that owns `quoteMint`. * Fetched from the mint when omitted for a non-SOL quote (one extra * round-trip before the account batch, which derives every ATA from it); * `TOKEN_PROGRAM_ID` for SOL. * @param options.payer - Transaction signer. Required when `quoteMint` is not * `NATIVE_MINT`: pays rent for the vault/shareholder ATAs the V2 * consolidation and distribution instructions may initialize. * @param options.quoteMint - The coin's quote mint * @returns The instructions to distribute creator fees and whether the token is graduated */ async buildDistributeCreatorFeesInstructions( mint: PublicKey, options: { quoteMint?: PublicKey; quoteTokenProgram?: PublicKey; payer?: PublicKey; } = {}, ): Promise { const { payer } = options; const quoteMint = normalizeQuoteMint(options.quoteMint ?? NATIVE_MINT); const isNativeQuote = quoteMint.equals(NATIVE_MINT); if (!isNativeQuote && !payer) { throw new Error( "payer is required when quoteMint is not NATIVE_MINT (V2 instructions may initialize ATAs)", ); } const quoteTokenProgram = options.quoteTokenProgram ?? (isNativeQuote ? TOKEN_PROGRAM_ID : await this.fetchQuoteTokenProgram(quoteMint)); const sharingConfigPubkey = feeSharingConfigPda(mint); const poolAddress = canonicalPumpPoolPdaWithQuote(mint, quoteMint); const ammVaultAta = coinCreatorVaultAtaPda( coinCreatorVaultAuthorityPda(sharingConfigPubkey), quoteMint, quoteTokenProgram, ); const [sharingConfigAccountInfo, poolAccountInfo, ammVaultAtaInfo] = await this.connection.getMultipleAccountsInfo([ sharingConfigPubkey, poolAddress, ammVaultAta, ]); if (!sharingConfigAccountInfo) { throw new Error(`Sharing config not found for mint: ${mint.toBase58()}`); } const sharingConfig = PUMP_SDK.decodeSharingConfig( sharingConfigAccountInfo, ); const instructions: TransactionInstruction[] = []; const isGraduated = poolAccountInfo !== null; if (isGraduated && ammVaultAtaInfo) { // Consolidate fees from AMM to bonding curve program for distribution const transferCreatorFeesToPumpIx = isNativeQuote ? await this.pumpAmmProgram.methods .transferCreatorFeesToPump() .accountsPartial({ wsolMint: NATIVE_MINT, tokenProgram: TOKEN_PROGRAM_ID, coinCreator: sharingConfigPubkey, }) .instruction() : await PUMP_SDK.transferCreatorFeesToPumpV2({ payer: payer!, mint, quoteMint, quoteTokenProgram, }); instructions.push(transferCreatorFeesToPumpIx); } const distributeCreatorFeesIx = isNativeQuote ? await PUMP_SDK.distributeCreatorFees({ mint, sharingConfig, sharingConfigAddress: sharingConfigPubkey, }) : await PUMP_SDK.distributeCreatorFeesV2({ mint, sharingConfig, sharingConfigAddress: sharingConfigPubkey, quoteMint, payer: payer!, shouldInitializeAta: true, quoteTokenProgram, }); instructions.push(distributeCreatorFeesIx); return { instructions, isGraduated, }; } } export interface MinimumDistributableFeeResult extends MinimumDistributableFeeEvent { isGraduated: boolean; /** * The mint `minimumRequired` / `distributableFees` / `canDistribute` are * denominated in. Always `NATIVE_MINT` today: the on-chain view reads the * lamport creator vault only. Compare it with the coin's quote mint before * gating a token-quoted coin's distribution on `canDistribute`. */ feesQuoteMint: PublicKey; } export interface DistributeCreatorFeeResult { instructions: TransactionInstruction[]; isGraduated: boolean; } /** * A creator's uncollected fees in one supported quote mint, as returned by * `OnlinePumpSdk.getCreatorVaultQuoteBalances`. Amounts are in the mint's * base units (lamports for SOL, reported as `NATIVE_MINT`). */ export interface CreatorVaultQuoteBalance { mint: PublicKey; source: QuoteMintSource; quoteTokenProgram: PublicKey; /** * Pump creator vault: lamports above rent for SOL, the vault ATA's amount * otherwise. */ pumpVault: BN; /** Pump-amm coin creator vault ATA amount. */ ammVault: BN; total: BN; }