From 55bbae91655653397cc82ce3ef2689601b6333df Mon Sep 17 00:00:00 2001 From: Hank Date: Wed, 3 Jun 2026 18:25:00 -0400 Subject: [PATCH 01/14] feat(quote): add USDC_MINT + QUOTE_MINTS registry --- src/__tests__/usdc.test.ts | 21 +++++++++++++++++++++ src/index.ts | 1 + src/quoteMints.ts | 24 ++++++++++++++++++++++++ 3 files changed, 46 insertions(+) create mode 100644 src/__tests__/usdc.test.ts create mode 100644 src/quoteMints.ts diff --git a/src/__tests__/usdc.test.ts b/src/__tests__/usdc.test.ts new file mode 100644 index 00000000..4c589b46 --- /dev/null +++ b/src/__tests__/usdc.test.ts @@ -0,0 +1,21 @@ +import { NATIVE_MINT, TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID } from "@solana/spl-token"; +import { USDC_MINT, QUOTE_MINTS, isNativeQuote } from "../quoteMints"; + +describe("quoteMints", () => { + it("exposes the canonical mainnet USDC mint (6 decimals, SPL Token program)", () => { + expect(USDC_MINT.toBase58()).toBe("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"); + expect(QUOTE_MINTS.USDC.mint.equals(USDC_MINT)).toBe(true); + expect(QUOTE_MINTS.USDC.decimals).toBe(6); + expect(QUOTE_MINTS.USDC.tokenProgram.equals(TOKEN_PROGRAM_ID)).toBe(true); + expect(QUOTE_MINTS.USDC.tokenProgram.equals(TOKEN_2022_PROGRAM_ID)).toBe(false); + }); + it("models wSOL with 9 decimals under the SPL Token program", () => { + expect(QUOTE_MINTS.wSOL.mint.equals(NATIVE_MINT)).toBe(true); + expect(QUOTE_MINTS.wSOL.decimals).toBe(9); + expect(QUOTE_MINTS.wSOL.tokenProgram.equals(TOKEN_PROGRAM_ID)).toBe(true); + }); + it("isNativeQuote distinguishes wSOL from USDC", () => { + expect(isNativeQuote(NATIVE_MINT)).toBe(true); + expect(isNativeQuote(USDC_MINT)).toBe(false); + }); +}); diff --git a/src/index.ts b/src/index.ts index 843eb095..d7622d52 100644 --- a/src/index.ts +++ b/src/index.ts @@ -145,6 +145,7 @@ export { export type { Fees, FeeTier } from "./state"; export { totalUnclaimedTokens, currentDayTokens } from "./tokenIncentives"; export * from "./errors"; +export * from "./quoteMints"; export { calculateBuyPriceImpact, calculateSellPriceImpact, diff --git a/src/quoteMints.ts b/src/quoteMints.ts new file mode 100644 index 00000000..458c2c3e --- /dev/null +++ b/src/quoteMints.ts @@ -0,0 +1,24 @@ +import { NATIVE_MINT, TOKEN_PROGRAM_ID } from "@solana/spl-token"; +import { PublicKey } from "@solana/web3.js"; + +/** Canonical mainnet USDC mint (legacy SPL Token, 6 decimals). */ +export const USDC_MINT = new PublicKey("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"); + +export interface QuoteMintInfo { + mint: PublicKey; + decimals: number; + /** Token program that owns the quote mint. Both wSOL and USDC use the legacy SPL Token program. */ + tokenProgram: PublicKey; + ticker: string; +} + +/** Supported quote mints. The SDK's `quoteMint`/`quoteTokenProgram` params default to wSOL. */ +export const QUOTE_MINTS: Record<"wSOL" | "USDC", QuoteMintInfo> = { + wSOL: { mint: NATIVE_MINT, decimals: 9, tokenProgram: TOKEN_PROGRAM_ID, ticker: "SOL" }, + USDC: { mint: USDC_MINT, decimals: 6, tokenProgram: TOKEN_PROGRAM_ID, ticker: "USDC" }, +}; + +/** True when a quote mint is native SOL (wrapped SOL), i.e. the legacy/default path. */ +export function isNativeQuote(quoteMint: PublicKey): boolean { + return quoteMint.equals(NATIVE_MINT); +} From 3e2d943837269fac892c05f5609714490187bb89 Mon Sep 17 00:00:00 2001 From: Hank Date: Wed, 3 Jun 2026 18:32:31 -0400 Subject: [PATCH 02/14] chore(idl): add buy_v2 to bundled pump IDL (json + ts) --- src/__tests__/usdc.test.ts | 19 + src/idl/pump.json | 788 +++++++++++++++++++++++++++++++++++++ src/idl/pump.ts | 762 +++++++++++++++++++++++++++++++++++ 3 files changed, 1569 insertions(+) diff --git a/src/__tests__/usdc.test.ts b/src/__tests__/usdc.test.ts index 4c589b46..5ca00f98 100644 --- a/src/__tests__/usdc.test.ts +++ b/src/__tests__/usdc.test.ts @@ -1,5 +1,8 @@ import { NATIVE_MINT, TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID } from "@solana/spl-token"; +import { Connection } from "@solana/web3.js"; import { USDC_MINT, QUOTE_MINTS, isNativeQuote } from "../quoteMints"; +import { getPumpProgram } from "../sdk"; +import pumpIdl from "../idl/pump.json"; describe("quoteMints", () => { it("exposes the canonical mainnet USDC mint (6 decimals, SPL Token program)", () => { @@ -19,3 +22,19 @@ describe("quoteMints", () => { expect(isNativeQuote(USDC_MINT)).toBe(false); }); }); + +describe("bundled IDL: buy_v2", () => { + // Assert against the raw bundled IDL JSON: Anchor's Program constructor + // (v0.31) normalizes instruction/field names to camelCase, so program.idl + // would expose "buyV2"/"maxSolCost" instead of the on-disk snake_case names. + // The program is still constructed to prove the bundled IDL loads cleanly. + const program = getPumpProgram(new Connection("http://localhost:8899")); + it("includes buy_v2 with the official discriminator and exactly 2 args", () => { + expect(program.idl.instructions.some((i) => i.name === "buyV2")).toBe(true); + const ix = (pumpIdl.instructions as any[]).find((i) => i.name === "buy_v2"); + expect(ix).toBeDefined(); + expect(ix.discriminator).toEqual([184, 23, 238, 97, 103, 197, 211, 61]); + expect(ix.args.map((a: any) => a.name)).toEqual(["amount", "max_sol_cost"]); + expect(ix.accounts).toHaveLength(27); + }); +}); diff --git a/src/idl/pump.json b/src/idl/pump.json index 11385017..74f1394e 100644 --- a/src/idl/pump.json +++ b/src/idl/pump.json @@ -790,6 +790,794 @@ ] }, { + "name": "buy_v2", + "discriminator": [ + 184, + 23, + 238, + 97, + 103, + 197, + 211, + 61 + ], + "accounts": [ + { + "name": "global", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] + } + ] + } + }, + { + "name": "base_mint" + }, + { + "name": "quote_mint" + }, + { + "name": "base_token_program" + }, + { + "name": "quote_token_program" + }, + { + "name": "associated_token_program", + "address": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" + }, + { + "name": "fee_recipient", + "writable": true + }, + { + "name": "associated_quote_fee_recipient", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "fee_recipient" + }, + { + "kind": "account", + "path": "quote_token_program" + }, + { + "kind": "account", + "path": "quote_mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, + 151, + 37, + 143, + 78, + 36, + 137, + 241, + 187, + 61, + 16, + 41, + 20, + 142, + 13, + 131, + 11, + 90, + 19, + 153, + 218, + 255, + 16, + 132, + 4, + 142, + 123, + 216, + 219, + 233, + 248, + 89 + ] + } + } + }, + { + "name": "buyback_fee_recipient", + "writable": true + }, + { + "name": "associated_quote_buyback_fee_recipient", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "buyback_fee_recipient" + }, + { + "kind": "account", + "path": "quote_token_program" + }, + { + "kind": "account", + "path": "quote_mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, + 151, + 37, + 143, + 78, + 36, + 137, + 241, + 187, + 61, + 16, + 41, + 20, + 142, + 13, + 131, + 11, + 90, + 19, + 153, + 218, + 255, + 16, + 132, + 4, + 142, + 123, + 216, + 219, + 233, + 248, + 89 + ] + } + } + }, + { + "name": "bonding_curve", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 98, + 111, + 110, + 100, + 105, + 110, + 103, + 45, + 99, + 117, + 114, + 118, + 101 + ] + }, + { + "kind": "account", + "path": "base_mint" + } + ] + } + }, + { + "name": "associated_base_bonding_curve", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "bonding_curve" + }, + { + "kind": "account", + "path": "base_token_program" + }, + { + "kind": "account", + "path": "base_mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, + 151, + 37, + 143, + 78, + 36, + 137, + 241, + 187, + 61, + 16, + 41, + 20, + 142, + 13, + 131, + 11, + 90, + 19, + 153, + 218, + 255, + 16, + 132, + 4, + 142, + 123, + 216, + 219, + 233, + 248, + 89 + ] + } + } + }, + { + "name": "associated_quote_bonding_curve", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "bonding_curve" + }, + { + "kind": "account", + "path": "quote_token_program" + }, + { + "kind": "account", + "path": "quote_mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, + 151, + 37, + 143, + 78, + 36, + 137, + 241, + 187, + 61, + 16, + 41, + 20, + 142, + 13, + 131, + 11, + 90, + 19, + 153, + 218, + 255, + 16, + 132, + 4, + 142, + 123, + 216, + 219, + 233, + 248, + 89 + ] + } + } + }, + { + "name": "user", + "writable": true, + "signer": true + }, + { + "name": "associated_base_user", + "writable": true + }, + { + "name": "associated_quote_user", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "user" + }, + { + "kind": "account", + "path": "quote_token_program" + }, + { + "kind": "account", + "path": "quote_mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, + 151, + 37, + 143, + 78, + 36, + 137, + 241, + 187, + 61, + 16, + 41, + 20, + 142, + 13, + 131, + 11, + 90, + 19, + 153, + 218, + 255, + 16, + 132, + 4, + 142, + 123, + 216, + 219, + 233, + 248, + 89 + ] + } + } + }, + { + "name": "creator_vault", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 99, + 114, + 101, + 97, + 116, + 111, + 114, + 45, + 118, + 97, + 117, + 108, + 116 + ] + }, + { + "kind": "account", + "path": "bonding_curve.creator", + "account": "BondingCurve" + } + ] + } + }, + { + "name": "associated_creator_vault", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "creator_vault" + }, + { + "kind": "account", + "path": "quote_token_program" + }, + { + "kind": "account", + "path": "quote_mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, + 151, + 37, + 143, + 78, + 36, + 137, + 241, + 187, + 61, + 16, + 41, + 20, + 142, + 13, + 131, + 11, + 90, + 19, + 153, + 218, + 255, + 16, + 132, + 4, + 142, + 123, + 216, + 219, + 233, + 248, + 89 + ] + } + } + }, + { + "name": "sharing_config", + "docs": [ + "seeds; the account is intentionally not deserialized here because it may be uninitialized", + "for mints that have not created a fee sharing config. Handlers must check", + "`data_is_empty()` / owner before reading." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 115, + 104, + 97, + 114, + 105, + 110, + 103, + 45, + 99, + 111, + 110, + 102, + 105, + 103 + ] + }, + { + "kind": "account", + "path": "base_mint" + } + ], + "program": { + "kind": "const", + "value": [ + 12, + 53, + 255, + 169, + 5, + 90, + 142, + 86, + 141, + 168, + 247, + 188, + 7, + 86, + 21, + 39, + 76, + 241, + 201, + 44, + 164, + 31, + 64, + 0, + 156, + 81, + 106, + 164, + 20, + 194, + 124, + 112 + ] + } + } + }, + { + "name": "global_volume_accumulator", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108, + 95, + 118, + 111, + 108, + 117, + 109, + 101, + 95, + 97, + 99, + 99, + 117, + 109, + 117, + 108, + 97, + 116, + 111, + 114 + ] + } + ] + } + }, + { + "name": "user_volume_accumulator", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 117, + 115, + 101, + 114, + 95, + 118, + 111, + 108, + 117, + 109, + 101, + 95, + 97, + 99, + 99, + 117, + 109, + 117, + 108, + 97, + 116, + 111, + 114 + ] + }, + { + "kind": "account", + "path": "user" + } + ] + } + }, + { + "name": "associated_user_volume_accumulator", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "user_volume_accumulator" + }, + { + "kind": "account", + "path": "quote_token_program" + }, + { + "kind": "account", + "path": "quote_mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, + 151, + 37, + 143, + 78, + 36, + 137, + 241, + 187, + 61, + 16, + 41, + 20, + 142, + 13, + 131, + 11, + 90, + 19, + 153, + 218, + 255, + 16, + 132, + 4, + 142, + 123, + 216, + 219, + 233, + 248, + 89 + ] + } + } + }, + { + "name": "fee_config", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 102, + 101, + 101, + 95, + 99, + 111, + 110, + 102, + 105, + 103 + ] + }, + { + "kind": "const", + "value": [ + 1, + 86, + 224, + 246, + 147, + 102, + 90, + 207, + 68, + 219, + 21, + 104, + 191, + 23, + 91, + 170, + 81, + 137, + 203, + 151, + 245, + 210, + 255, + 59, + 101, + 93, + 43, + 182, + 253, + 109, + 24, + 176 + ] + } + ], + "program": { + "kind": "account", + "path": "fee_program" + } + } + }, + { + "name": "fee_program", + "address": "pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ" + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, + 95, + 101, + 118, + 101, + 110, + 116, + 95, + 97, + 117, + 116, + 104, + 111, + 114, + 105, + 116, + 121 + ] + } + ] + } + }, + { + "name": "program", + "address": "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P" + } + ], + "args": [ + { + "name": "amount", + "type": "u64" + }, + { + "name": "max_sol_cost", + "type": "u64" + } + ] + }, { "name": "buy_exact_sol_in", "docs": [ "Given a budget of spendable SOL, buy at least min_tokens_out tokens.", diff --git a/src/idl/pump.ts b/src/idl/pump.ts index c6772361..83341231 100644 --- a/src/idl/pump.ts +++ b/src/idl/pump.ts @@ -712,6 +712,768 @@ export interface Pump { }, ]; }, + { + name: "buyV2"; + discriminator: [184, 23, 238, 97, 103, 197, 211, 61]; + accounts: [ + { + name: "global"; + pda: { + seeds: [ + { + kind: "const"; + value: [103, 108, 111, 98, 97, 108]; + }, + ]; + }; + }, + { + name: "baseMint"; + }, + { + name: "quoteMint"; + }, + { + name: "baseTokenProgram"; + }, + { + name: "quoteTokenProgram"; + }, + { + name: "associatedTokenProgram"; + address: "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"; + }, + { + name: "feeRecipient"; + writable: true; + }, + { + name: "associatedQuoteFeeRecipient"; + writable: true; + pda: { + seeds: [ + { + kind: "account"; + path: "feeRecipient"; + }, + { + kind: "account"; + path: "quoteTokenProgram"; + }, + { + kind: "account"; + path: "quoteMint"; + }, + ]; + program: { + kind: "const"; + value: [ + 140, + 151, + 37, + 143, + 78, + 36, + 137, + 241, + 187, + 61, + 16, + 41, + 20, + 142, + 13, + 131, + 11, + 90, + 19, + 153, + 218, + 255, + 16, + 132, + 4, + 142, + 123, + 216, + 219, + 233, + 248, + 89, + ]; + }; + }; + }, + { + name: "buybackFeeRecipient"; + writable: true; + }, + { + name: "associatedQuoteBuybackFeeRecipient"; + writable: true; + pda: { + seeds: [ + { + kind: "account"; + path: "buybackFeeRecipient"; + }, + { + kind: "account"; + path: "quoteTokenProgram"; + }, + { + kind: "account"; + path: "quoteMint"; + }, + ]; + program: { + kind: "const"; + value: [ + 140, + 151, + 37, + 143, + 78, + 36, + 137, + 241, + 187, + 61, + 16, + 41, + 20, + 142, + 13, + 131, + 11, + 90, + 19, + 153, + 218, + 255, + 16, + 132, + 4, + 142, + 123, + 216, + 219, + 233, + 248, + 89, + ]; + }; + }; + }, + { + name: "bondingCurve"; + writable: true; + pda: { + seeds: [ + { + kind: "const"; + value: [ + 98, + 111, + 110, + 100, + 105, + 110, + 103, + 45, + 99, + 117, + 114, + 118, + 101, + ]; + }, + { + kind: "account"; + path: "baseMint"; + }, + ]; + }; + }, + { + name: "associatedBaseBondingCurve"; + writable: true; + pda: { + seeds: [ + { + kind: "account"; + path: "bondingCurve"; + }, + { + kind: "account"; + path: "baseTokenProgram"; + }, + { + kind: "account"; + path: "baseMint"; + }, + ]; + program: { + kind: "const"; + value: [ + 140, + 151, + 37, + 143, + 78, + 36, + 137, + 241, + 187, + 61, + 16, + 41, + 20, + 142, + 13, + 131, + 11, + 90, + 19, + 153, + 218, + 255, + 16, + 132, + 4, + 142, + 123, + 216, + 219, + 233, + 248, + 89, + ]; + }; + }; + }, + { + name: "associatedQuoteBondingCurve"; + writable: true; + pda: { + seeds: [ + { + kind: "account"; + path: "bondingCurve"; + }, + { + kind: "account"; + path: "quoteTokenProgram"; + }, + { + kind: "account"; + path: "quoteMint"; + }, + ]; + program: { + kind: "const"; + value: [ + 140, + 151, + 37, + 143, + 78, + 36, + 137, + 241, + 187, + 61, + 16, + 41, + 20, + 142, + 13, + 131, + 11, + 90, + 19, + 153, + 218, + 255, + 16, + 132, + 4, + 142, + 123, + 216, + 219, + 233, + 248, + 89, + ]; + }; + }; + }, + { + name: "user"; + writable: true; + signer: true; + }, + { + name: "associatedBaseUser"; + writable: true; + }, + { + name: "associatedQuoteUser"; + writable: true; + pda: { + seeds: [ + { + kind: "account"; + path: "user"; + }, + { + kind: "account"; + path: "quoteTokenProgram"; + }, + { + kind: "account"; + path: "quoteMint"; + }, + ]; + program: { + kind: "const"; + value: [ + 140, + 151, + 37, + 143, + 78, + 36, + 137, + 241, + 187, + 61, + 16, + 41, + 20, + 142, + 13, + 131, + 11, + 90, + 19, + 153, + 218, + 255, + 16, + 132, + 4, + 142, + 123, + 216, + 219, + 233, + 248, + 89, + ]; + }; + }; + }, + { + name: "creatorVault"; + writable: true; + pda: { + seeds: [ + { + kind: "const"; + value: [ + 99, + 114, + 101, + 97, + 116, + 111, + 114, + 45, + 118, + 97, + 117, + 108, + 116, + ]; + }, + { + kind: "account"; + path: "bonding_curve.creator"; + account: "bondingCurve"; + }, + ]; + }; + }, + { + name: "associatedCreatorVault"; + writable: true; + pda: { + seeds: [ + { + kind: "account"; + path: "creatorVault"; + }, + { + kind: "account"; + path: "quoteTokenProgram"; + }, + { + kind: "account"; + path: "quoteMint"; + }, + ]; + program: { + kind: "const"; + value: [ + 140, + 151, + 37, + 143, + 78, + 36, + 137, + 241, + 187, + 61, + 16, + 41, + 20, + 142, + 13, + 131, + 11, + 90, + 19, + 153, + 218, + 255, + 16, + 132, + 4, + 142, + 123, + 216, + 219, + 233, + 248, + 89, + ]; + }; + }; + }, + { + name: "sharingConfig"; + docs: [ + "seeds; the account is intentionally not deserialized here because it may be uninitialized", + "for mints that have not created a fee sharing config. Handlers must check", + "`data_is_empty()` / owner before reading.", + ]; + pda: { + seeds: [ + { + kind: "const"; + value: [ + 115, + 104, + 97, + 114, + 105, + 110, + 103, + 45, + 99, + 111, + 110, + 102, + 105, + 103, + ]; + }, + { + kind: "account"; + path: "baseMint"; + }, + ]; + program: { + kind: "const"; + value: [ + 12, + 53, + 255, + 169, + 5, + 90, + 142, + 86, + 141, + 168, + 247, + 188, + 7, + 86, + 21, + 39, + 76, + 241, + 201, + 44, + 164, + 31, + 64, + 0, + 156, + 81, + 106, + 164, + 20, + 194, + 124, + 112, + ]; + }; + }; + }, + { + name: "globalVolumeAccumulator"; + pda: { + seeds: [ + { + kind: "const"; + value: [ + 103, + 108, + 111, + 98, + 97, + 108, + 95, + 118, + 111, + 108, + 117, + 109, + 101, + 95, + 97, + 99, + 99, + 117, + 109, + 117, + 108, + 97, + 116, + 111, + 114, + ]; + }, + ]; + }; + }, + { + name: "userVolumeAccumulator"; + writable: true; + pda: { + seeds: [ + { + kind: "const"; + value: [ + 117, + 115, + 101, + 114, + 95, + 118, + 111, + 108, + 117, + 109, + 101, + 95, + 97, + 99, + 99, + 117, + 109, + 117, + 108, + 97, + 116, + 111, + 114, + ]; + }, + { + kind: "account"; + path: "user"; + }, + ]; + }; + }, + { + name: "associatedUserVolumeAccumulator"; + writable: true; + pda: { + seeds: [ + { + kind: "account"; + path: "userVolumeAccumulator"; + }, + { + kind: "account"; + path: "quoteTokenProgram"; + }, + { + kind: "account"; + path: "quoteMint"; + }, + ]; + program: { + kind: "const"; + value: [ + 140, + 151, + 37, + 143, + 78, + 36, + 137, + 241, + 187, + 61, + 16, + 41, + 20, + 142, + 13, + 131, + 11, + 90, + 19, + 153, + 218, + 255, + 16, + 132, + 4, + 142, + 123, + 216, + 219, + 233, + 248, + 89, + ]; + }; + }; + }, + { + name: "feeConfig"; + pda: { + seeds: [ + { + kind: "const"; + value: [102, 101, 101, 95, 99, 111, 110, 102, 105, 103]; + }, + { + kind: "const"; + value: [ + 1, + 86, + 224, + 246, + 147, + 102, + 90, + 207, + 68, + 219, + 21, + 104, + 191, + 23, + 91, + 170, + 81, + 137, + 203, + 151, + 245, + 210, + 255, + 59, + 101, + 93, + 43, + 182, + 253, + 109, + 24, + 176, + ]; + }, + ]; + program: { + kind: "account"; + path: "feeProgram"; + }; + }; + }, + { + name: "feeProgram"; + address: "pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ"; + }, + { + name: "systemProgram"; + address: "11111111111111111111111111111111"; + }, + { + name: "eventAuthority"; + pda: { + seeds: [ + { + kind: "const"; + value: [ + 95, + 95, + 101, + 118, + 101, + 110, + 116, + 95, + 97, + 117, + 116, + 104, + 111, + 114, + 105, + 116, + 121, + ]; + }, + ]; + }; + }, + { + name: "program"; + address: "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P"; + }, + ]; + args: [ + { + name: "amount"; + type: "u64"; + }, + { + name: "maxSolCost"; + type: "u64"; + }, + ]; + }, { name: "buyExactSolIn"; docs: [ From bb67234067af7537d90a402e86dee0c6a18cdcf3 Mon Sep 17 00:00:00 2001 From: Hank Date: Wed, 3 Jun 2026 18:38:59 -0400 Subject: [PATCH 03/14] feat(fees): add pickBuybackFeeRecipient alias for buy_v2/sell_v2 --- src/__tests__/usdc.test.ts | 23 +++++++++++++++++++++++ src/fees.ts | 12 ++++++++++++ src/index.ts | 2 ++ 3 files changed, 37 insertions(+) diff --git a/src/__tests__/usdc.test.ts b/src/__tests__/usdc.test.ts index 5ca00f98..96d85735 100644 --- a/src/__tests__/usdc.test.ts +++ b/src/__tests__/usdc.test.ts @@ -3,6 +3,11 @@ import { Connection } from "@solana/web3.js"; import { USDC_MINT, QUOTE_MINTS, isNativeQuote } from "../quoteMints"; import { getPumpProgram } from "../sdk"; import pumpIdl from "../idl/pump.json"; +import { + BUYBACK_FEE_RECIPIENTS, + pickBuybackFeeRecipient, + BREAKING_FEE_RECIPIENTS, +} from "../fees"; describe("quoteMints", () => { it("exposes the canonical mainnet USDC mint (6 decimals, SPL Token program)", () => { @@ -38,3 +43,21 @@ describe("bundled IDL: buy_v2", () => { expect(ix.accounts).toHaveLength(27); }); }); + +describe("buyback fee recipients", () => { + it("are the 8 official buyback recipients (same set as BREAKING_FEE_RECIPIENTS)", () => { + expect(BUYBACK_FEE_RECIPIENTS.map((p) => p.toBase58())).toEqual( + BREAKING_FEE_RECIPIENTS.map((p) => p.toBase58()), + ); + expect(BUYBACK_FEE_RECIPIENTS).toHaveLength(8); + expect(BUYBACK_FEE_RECIPIENTS[0]!.toBase58()).toBe( + "5YxQFdt3Tr9zJLvkFccqXVUwhdTWJQc1fFg2YPbxvxeD", + ); + }); + it("pickBuybackFeeRecipient returns one of the set", () => { + for (let i = 0; i < 100; i++) { + const picked = pickBuybackFeeRecipient(); + expect(BUYBACK_FEE_RECIPIENTS.some((p) => p.equals(picked))).toBe(true); + } + }); +}); diff --git a/src/fees.ts b/src/fees.ts index e13854b7..4e799a2f 100644 --- a/src/fees.ts +++ b/src/fees.ts @@ -176,6 +176,18 @@ export function pickBreakingFeeRecipient(): PublicKey { ]!; } +/** + * The 8 buyback fee recipients (official `FEE_RECIPIENTS.md`). `buy_v2`/`sell_v2` + * take a `buybackFeeRecipient` from this set. These are the same 8 addresses that + * the 2026-04-28 upgrade appends to legacy buy/sell (a.k.a. {@link BREAKING_FEE_RECIPIENTS}). + */ +export const BUYBACK_FEE_RECIPIENTS = BREAKING_FEE_RECIPIENTS; + +/** Pick one of the 8 buyback fee recipients at random (for `buy_v2`/`sell_v2`). */ +export function pickBuybackFeeRecipient(): PublicKey { + return pickBreakingFeeRecipient(); +} + /** * Pre-computed WSOL ATAs for each of the 8 breaking fee recipients, keyed by * recipient base58 address. Use this in high-throughput paths (e.g. AMM trading diff --git a/src/index.ts b/src/index.ts index d7622d52..b120dffc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -45,6 +45,8 @@ export { BREAKING_FEE_RECIPIENT_WSOL_ATAS, isBreakingFeeRecipient, pickBreakingFeeRecipient, + BUYBACK_FEE_RECIPIENTS, + pickBuybackFeeRecipient, buildAmmBreakingFeeRecipientAccounts, validateBcInstruction, validateAmmInstruction, From 5db01dfbf7f63a7ac9868af9165c372259dabb75 Mon Sep 17 00:00:00 2001 From: Hank Date: Wed, 3 Jun 2026 18:41:51 -0400 Subject: [PATCH 04/14] feat(create): optional quoteMint on createV2Instruction (USDC remaining accounts) --- src/__tests__/usdc.test.ts | 39 +++++++++++++++++++++++++++++++++++--- src/sdk.ts | 16 ++++++++++++++++ 2 files changed, 52 insertions(+), 3 deletions(-) diff --git a/src/__tests__/usdc.test.ts b/src/__tests__/usdc.test.ts index 96d85735..ab8adad1 100644 --- a/src/__tests__/usdc.test.ts +++ b/src/__tests__/usdc.test.ts @@ -1,7 +1,13 @@ -import { NATIVE_MINT, TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID } from "@solana/spl-token"; -import { Connection } from "@solana/web3.js"; +import { + NATIVE_MINT, + TOKEN_PROGRAM_ID, + TOKEN_2022_PROGRAM_ID, + getAssociatedTokenAddressSync, +} from "@solana/spl-token"; +import { Connection, Keypair } from "@solana/web3.js"; import { USDC_MINT, QUOTE_MINTS, isNativeQuote } from "../quoteMints"; -import { getPumpProgram } from "../sdk"; +import { getPumpProgram, PUMP_SDK } from "../sdk"; +import { bondingCurvePda } from "../pda"; import pumpIdl from "../idl/pump.json"; import { BUYBACK_FEE_RECIPIENTS, @@ -61,3 +67,30 @@ describe("buyback fee recipients", () => { } }); }); + +describe("createV2Instruction quote mint", () => { + const mint = new Keypair().publicKey; + const creator = new Keypair().publicKey; + const user = new Keypair().publicKey; + const baseArgs = { mint, name: "n", symbol: "n", uri: "u", creator, user, mayhemMode: false }; + + it("SOL (default): exactly 16 accounts, no remaining accounts appended", async () => { + const ix = await PUMP_SDK.createV2Instruction(baseArgs); + expect(ix.keys).toHaveLength(16); + }); + it("explicit NATIVE_MINT behaves like SOL (16 accounts)", async () => { + const ix = await PUMP_SDK.createV2Instruction({ ...baseArgs, quoteMint: NATIVE_MINT }); + expect(ix.keys).toHaveLength(16); + }); + it("USDC: appends exactly the 3 quote remaining accounts in order", async () => { + const ix = await PUMP_SDK.createV2Instruction({ ...baseArgs, quoteMint: USDC_MINT }); + expect(ix.keys).toHaveLength(19); + const [r0, r1, r2] = ix.keys.slice(16); + expect(r0!.pubkey.equals(USDC_MINT)).toBe(true); + expect(r0!.isWritable).toBe(false); + expect(r1!.pubkey.equals(getAssociatedTokenAddressSync(USDC_MINT, bondingCurvePda(mint), true, TOKEN_PROGRAM_ID))).toBe(true); + expect(r1!.isWritable).toBe(true); + expect(r2!.pubkey.equals(TOKEN_PROGRAM_ID)).toBe(true); + expect(r2!.isWritable).toBe(false); + }); +}); diff --git a/src/sdk.ts b/src/sdk.ts index 5ef48088..21199c84 100644 --- a/src/sdk.ts +++ b/src/sdk.ts @@ -344,6 +344,8 @@ export class PumpSdk { user, mayhemMode, cashback = false, + quoteMint = NATIVE_MINT, + quoteTokenProgram = TOKEN_PROGRAM_ID, }: { mint: PublicKey; name: string; @@ -353,7 +355,20 @@ export class PumpSdk { user: PublicKey; mayhemMode: boolean; cashback?: boolean; + quoteMint?: PublicKey; + quoteTokenProgram?: PublicKey; }): Promise { + const remaining = quoteMint.equals(NATIVE_MINT) + ? [] + : [ + { pubkey: quoteMint, isWritable: false, isSigner: false }, + { + pubkey: getAssociatedTokenAddressSync(quoteMint, bondingCurvePda(mint), true, quoteTokenProgram), + isWritable: true, + isSigner: false, + }, + { pubkey: quoteTokenProgram, isWritable: false, isSigner: false }, + ]; return await this.offlinePumpProgram.methods .createV2(name, symbol, uri, creator, mayhemMode, [cashback ?? false]) .accountsPartial({ @@ -366,6 +381,7 @@ export class PumpSdk { mayhemState: getMayhemStatePda(mint), mayhemTokenVault: getTokenVaultPda(mint), }) + .remainingAccounts(remaining) .instruction(); } From 5b81d2358b577d3ac2590bd62f8024a5c1dfcf97 Mon Sep 17 00:00:00 2001 From: Hank Date: Wed, 3 Jun 2026 18:46:57 -0400 Subject: [PATCH 05/14] feat(buy): add buyV2 builder for non-native (USDC) quote mints --- src/__tests__/usdc.test.ts | 32 +++++++++++++++++++- src/sdk.ts | 62 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+), 1 deletion(-) diff --git a/src/__tests__/usdc.test.ts b/src/__tests__/usdc.test.ts index ab8adad1..6c561506 100644 --- a/src/__tests__/usdc.test.ts +++ b/src/__tests__/usdc.test.ts @@ -5,15 +5,18 @@ import { getAssociatedTokenAddressSync, } from "@solana/spl-token"; import { Connection, Keypair } from "@solana/web3.js"; +import BN from "bn.js"; import { USDC_MINT, QUOTE_MINTS, isNativeQuote } from "../quoteMints"; import { getPumpProgram, PUMP_SDK } from "../sdk"; -import { bondingCurvePda } from "../pda"; +import { bondingCurvePda, creatorVaultPda } from "../pda"; import pumpIdl from "../idl/pump.json"; import { BUYBACK_FEE_RECIPIENTS, pickBuybackFeeRecipient, BREAKING_FEE_RECIPIENTS, + getFeeRecipient, } from "../fees"; +import { makeGlobal } from "./fixtures"; describe("quoteMints", () => { it("exposes the canonical mainnet USDC mint (6 decimals, SPL Token program)", () => { @@ -94,3 +97,30 @@ describe("createV2Instruction quote mint", () => { expect(r2!.isWritable).toBe(false); }); }); + +describe("buyV2 builder (USDC)", () => { + const mint = new Keypair().publicKey; + const creator = new Keypair().publicKey; + const user = new Keypair().publicKey; + + it("builds buy_v2 with 27 accounts, 2-arg data, USDC quote wiring", async () => { + const ix = await PUMP_SDK.buyV2({ + user, mint, creator, + amount: new BN("15000000000000"), + quoteAmount: new BN("15000000"), + quoteMint: USDC_MINT, + quoteTokenProgram: TOKEN_PROGRAM_ID, + feeRecipient: getFeeRecipient(makeGlobal(), false), + buybackFeeRecipient: pickBuybackFeeRecipient(), + }); + expect(ix.keys).toHaveLength(27); + expect(ix.keys[2]!.pubkey.equals(USDC_MINT)).toBe(true); + expect(ix.keys[3]!.pubkey.equals(TOKEN_2022_PROGRAM_ID)).toBe(true); + expect(ix.keys[4]!.pubkey.equals(TOKEN_PROGRAM_ID)).toBe(true); + expect(ix.keys[13]!.isSigner).toBe(true); + expect(ix.keys[12]!.pubkey.equals(getAssociatedTokenAddressSync(USDC_MINT, bondingCurvePda(mint), true, TOKEN_PROGRAM_ID))).toBe(true); + expect(ix.keys[16]!.pubkey.equals(creatorVaultPda(creator))).toBe(true); + expect(ix.data).toHaveLength(24); + expect([...ix.data.slice(0, 8)]).toEqual([184, 23, 238, 97, 103, 197, 211, 61]); + }); +}); diff --git a/src/sdk.ts b/src/sdk.ts index 21199c84..655e59cf 100644 --- a/src/sdk.ts +++ b/src/sdk.ts @@ -32,6 +32,7 @@ import { buildAmmBreakingFeeRecipientAccounts, getFeeRecipient, pickBreakingFeeRecipient, + pickBuybackFeeRecipient, } from "./fees"; import { Pump } from "./idl/pump"; import pumpIdl from "./idl/pump.json"; @@ -892,6 +893,67 @@ export class PumpSdk { }); } + /** + * Build a `buy_v2` instruction (V2 buy supporting non-native quote mints, e.g. USDC). + * For SOL-paired coins prefer the legacy `buy` path. `quoteAmount` is the max QUOTE + * cost in the quote mint's base units (USDC = 6dp), NOT lamports. + */ + async buyV2({ + user, + mint, + creator, + amount, + quoteAmount, + quoteMint = NATIVE_MINT, + quoteTokenProgram = TOKEN_PROGRAM_ID, + baseTokenProgram = TOKEN_2022_PROGRAM_ID, + feeRecipient, + buybackFeeRecipient = pickBuybackFeeRecipient(), + }: { + user: PublicKey; + mint: PublicKey; + creator: PublicKey; + amount: BN; + quoteAmount: BN; + quoteMint?: PublicKey; + quoteTokenProgram?: PublicKey; + baseTokenProgram?: PublicKey; + feeRecipient: PublicKey; + buybackFeeRecipient?: PublicKey; + }): Promise { + const creatorVault = creatorVaultPda(creator); + return await this.offlinePumpProgram.methods + .buyV2(amount, quoteAmount) + .accountsPartial({ + baseMint: mint, + quoteMint, + baseTokenProgram, + quoteTokenProgram, + feeRecipient, + buybackFeeRecipient, + user, + // associated_base_user has no PDA metadata in the IDL, so Anchor cannot + // offline-resolve it: pass the user's base-mint ATA explicitly. + associatedBaseUser: getAssociatedTokenAddressSync( + mint, + user, + true, + baseTokenProgram, + ), + // creator_vault is seeded by bonding_curve.creator (an on-chain field + // Anchor cannot read offline), so we derive it from the passed creator. + creatorVault, + // associated_creator_vault depends on creator_vault; derive it too. + associatedCreatorVault: getAssociatedTokenAddressSync( + quoteMint, + creatorVault, + true, + quoteTokenProgram, + ), + }) + .instruction(); + } + private async getBuyInstructionInternal({ user, associatedUser, From c1416e9d424c6a658f84db18e2115b127d7d7913 Mon Sep 17 00:00:00 2001 From: Hank Date: Wed, 3 Jun 2026 18:53:31 -0400 Subject: [PATCH 06/14] feat(buy): route buy builders to buy_v2 for non-native quote mints --- src/__tests__/usdc.test.ts | 24 ++++++++++++++++ src/sdk.ts | 59 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+) diff --git a/src/__tests__/usdc.test.ts b/src/__tests__/usdc.test.ts index 6c561506..ca831237 100644 --- a/src/__tests__/usdc.test.ts +++ b/src/__tests__/usdc.test.ts @@ -124,3 +124,27 @@ describe("buyV2 builder (USDC)", () => { expect([...ix.data.slice(0, 8)]).toEqual([184, 23, 238, 97, 103, 197, 211, 61]); }); }); + +describe("getBuyInstructionRaw routing", () => { + const mint = new Keypair().publicKey; + const creator = new Keypair().publicKey; + const user = new Keypair().publicKey; + + it("SOL (default): emits legacy buy (disc 0x66063d12), unchanged", async () => { + const ix = await PUMP_SDK.getBuyInstructionRaw({ + user, mint, creator, amount: new BN(1), solAmount: new BN(1), + feeRecipient: getFeeRecipient(makeGlobal(), false), + }); + expect([...ix.data.slice(0, 8)]).toEqual([102, 6, 61, 18, 1, 218, 235, 234]); + expect(ix.keys.at(-1)!.isWritable).toBe(true); // breaking fee recipient appended last + }); + it("USDC: routes to buy_v2 (disc 0xb817ee…)", async () => { + const ix = await PUMP_SDK.getBuyInstructionRaw({ + user, mint, creator, amount: new BN("15000000000000"), solAmount: new BN("15000000"), + feeRecipient: getFeeRecipient(makeGlobal(), false), + quoteMint: USDC_MINT, quoteTokenProgram: TOKEN_PROGRAM_ID, + }); + expect([...ix.data.slice(0, 8)]).toEqual([184, 23, 238, 97, 103, 197, 211, 61]); + expect(ix.keys).toHaveLength(27); + }); +}); diff --git a/src/sdk.ts b/src/sdk.ts index 655e59cf..51f2369b 100644 --- a/src/sdk.ts +++ b/src/sdk.ts @@ -397,6 +397,8 @@ export class PumpSdk { solAmount, slippage, tokenProgram = TOKEN_PROGRAM_ID, + quoteMint = NATIVE_MINT, + quoteTokenProgram = TOKEN_PROGRAM_ID, }: { global: Global; bondingCurveAccountInfo: AccountInfo; @@ -408,7 +410,27 @@ export class PumpSdk { solAmount: BN; slippage: number; tokenProgram: PublicKey; + quoteMint?: PublicKey; + quoteTokenProgram?: PublicKey; }): Promise { + // Non-native quote (e.g. USDC) routes to buy_v2, which builds its own + // accounts (and uses TOKEN_2022 for the base mint). It does not need the + // legacy extendAccount / base-ATA-creation setup, so branch before it. + if (!quoteMint.equals(NATIVE_MINT)) { + return [ + await this.buyV2({ + user, + mint, + creator: bondingCurve.creator, + amount, + quoteAmount: solAmount, + quoteMint, + quoteTokenProgram, + feeRecipient: getFeeRecipient(global, bondingCurve.isMayhemMode), + }), + ]; + } + const instructions: TransactionInstruction[] = []; if (bondingCurveAccountInfo.data.length < BONDING_CURVE_NEW_SIZE) { @@ -588,6 +610,8 @@ export class PumpSdk { slippage, tokenProgram = TOKEN_PROGRAM_ID, mayhemMode = false, + quoteMint = NATIVE_MINT, + quoteTokenProgram = TOKEN_PROGRAM_ID, }: { global: Global; mint: PublicKey; @@ -599,7 +623,24 @@ export class PumpSdk { slippage: number; tokenProgram: PublicKey; mayhemMode: boolean; + quoteMint?: PublicKey; + quoteTokenProgram?: PublicKey; }) { + // Non-native quote (e.g. USDC) routes to buy_v2. solAmount is the max quote + // cost the caller already capped; pass it straight as quoteAmount (no legacy + // slippage math). + if (!quoteMint.equals(NATIVE_MINT)) { + return await this.buyV2({ + user, + mint, + creator, + amount, + quoteAmount: solAmount, + quoteMint, + quoteTokenProgram, + feeRecipient: getFeeRecipient(global, mayhemMode), + }); + } return await this.getBuyInstructionInternal({ user, associatedUser, @@ -868,6 +909,8 @@ export class PumpSdk { solAmount, feeRecipient = getStaticRandomFeeRecipient(), tokenProgram = TOKEN_PROGRAM_ID, + quoteMint = NATIVE_MINT, + quoteTokenProgram = TOKEN_PROGRAM_ID, }: { user: PublicKey; mint: PublicKey; @@ -876,7 +919,23 @@ export class PumpSdk { solAmount: BN; feeRecipient: PublicKey; tokenProgram?: PublicKey; + quoteMint?: PublicKey; + quoteTokenProgram?: PublicKey; }): Promise { + // Non-native quote (e.g. USDC) routes to buy_v2; solAmount is the max quote + // cost (caller controls the cap), passed straight through as quoteAmount. + if (!quoteMint.equals(NATIVE_MINT)) { + return await this.buyV2({ + user, + mint, + creator, + amount, + quoteAmount: solAmount, + quoteMint, + quoteTokenProgram, + feeRecipient, + }); + } return await this.getBuyInstructionInternal({ user, associatedUser: getAssociatedTokenAddressSync( From 1d086b26d06a23503f367cd5214385813b659737 Mon Sep 17 00:00:00 2001 From: Hank Date: Wed, 3 Jun 2026 18:58:59 -0400 Subject: [PATCH 07/14] fix(buy): create user base+quote ATAs in USDC buyInstructions path --- src/__tests__/usdc.test.ts | 98 ++++++++++++++++++++++++++++++++++++-- src/sdk.ts | 35 ++++++++++++-- 2 files changed, 127 insertions(+), 6 deletions(-) diff --git a/src/__tests__/usdc.test.ts b/src/__tests__/usdc.test.ts index ca831237..b05da6a5 100644 --- a/src/__tests__/usdc.test.ts +++ b/src/__tests__/usdc.test.ts @@ -1,13 +1,14 @@ import { + ASSOCIATED_TOKEN_PROGRAM_ID, NATIVE_MINT, TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID, getAssociatedTokenAddressSync, } from "@solana/spl-token"; -import { Connection, Keypair } from "@solana/web3.js"; +import { AccountInfo, Connection, Keypair, PublicKey } from "@solana/web3.js"; import BN from "bn.js"; import { USDC_MINT, QUOTE_MINTS, isNativeQuote } from "../quoteMints"; -import { getPumpProgram, PUMP_SDK } from "../sdk"; +import { getPumpProgram, PUMP_SDK, BONDING_CURVE_NEW_SIZE } from "../sdk"; import { bondingCurvePda, creatorVaultPda } from "../pda"; import pumpIdl from "../idl/pump.json"; import { @@ -16,7 +17,7 @@ import { BREAKING_FEE_RECIPIENTS, getFeeRecipient, } from "../fees"; -import { makeGlobal } from "./fixtures"; +import { makeGlobal, makeBondingCurve } from "./fixtures"; describe("quoteMints", () => { it("exposes the canonical mainnet USDC mint (6 decimals, SPL Token program)", () => { @@ -148,3 +149,94 @@ describe("getBuyInstructionRaw routing", () => { expect(ix.keys).toHaveLength(27); }); }); + +describe("buyInstructions routing (USDC)", () => { + const mint = new Keypair().publicKey; + const creator = new Keypair().publicKey; + const user = new Keypair().publicKey; + + // A "new" bonding curve account (data >= BONDING_CURVE_NEW_SIZE) so the legacy + // path would not prepend extendAccount — though the USDC branch returns before + // that check anyway. Shape mirrors makeBcAccountInfo in onlineSdk.test.ts. + function makeBcAccountInfo(): AccountInfo { + return { + data: Buffer.alloc(BONDING_CURVE_NEW_SIZE), + executable: false, + lamports: 1_000_000, + owner: new PublicKey("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P"), + rentEpoch: 0, + }; + } + + it("prepends user base + quote ATA creates and ends with buy_v2", async () => { + const ixs = await PUMP_SDK.buyInstructions({ + global: makeGlobal(), + bondingCurveAccountInfo: makeBcAccountInfo(), + bondingCurve: makeBondingCurve({ creator, isMayhemMode: false }), + associatedUserAccountInfo: null, + mint, + user, + amount: new BN("15000000000000"), + solAmount: new BN("15000000"), + slippage: 1, + tokenProgram: TOKEN_PROGRAM_ID, + quoteMint: USDC_MINT, + quoteTokenProgram: TOKEN_PROGRAM_ID, + }); + + // Last instruction is buy_v2 (27 accounts, official discriminator). + const last = ixs.at(-1)!; + expect([...last.data.slice(0, 8)]).toEqual([184, 23, 238, 97, 103, 197, 211, 61]); + expect(last.keys).toHaveLength(27); + + // At least 2 ATA-program create instructions precede it: the user's base + // Token-2022 ATA and the user's USDC quote ATA. Without these, a standalone + // USDC buy for a fresh wallet would fail. + const ataCreates = ixs.filter((ix) => + ix.programId.equals(ASSOCIATED_TOKEN_PROGRAM_ID), + ); + expect(ataCreates.length).toBeGreaterThanOrEqual(2); + + // The base ATA create targets the user's Token-2022 ATA for the base mint. + const baseAta = getAssociatedTokenAddressSync(mint, user, true, TOKEN_2022_PROGRAM_ID); + expect( + ataCreates.some((ix) => ix.keys.some((k) => k.pubkey.equals(baseAta))), + ).toBe(true); + + // The quote ATA create targets the user's USDC (SPL Token) ATA. + const quoteAta = getAssociatedTokenAddressSync(USDC_MINT, user, true, TOKEN_PROGRAM_ID); + expect( + ataCreates.some((ix) => ix.keys.some((k) => k.pubkey.equals(quoteAta))), + ).toBe(true); + }); + + it("still creates the quote ATA when the base ATA already exists", async () => { + const ixs = await PUMP_SDK.buyInstructions({ + global: makeGlobal(), + bondingCurveAccountInfo: makeBcAccountInfo(), + bondingCurve: makeBondingCurve({ creator, isMayhemMode: false }), + // base ATA present -> skip base create, but quote create must remain. + associatedUserAccountInfo: makeBcAccountInfo(), + mint, + user, + amount: new BN("15000000000000"), + solAmount: new BN("15000000"), + slippage: 1, + tokenProgram: TOKEN_PROGRAM_ID, + quoteMint: USDC_MINT, + quoteTokenProgram: TOKEN_PROGRAM_ID, + }); + + const last = ixs.at(-1)!; + expect([...last.data.slice(0, 8)]).toEqual([184, 23, 238, 97, 103, 197, 211, 61]); + + const quoteAta = getAssociatedTokenAddressSync(USDC_MINT, user, true, TOKEN_PROGRAM_ID); + const ataCreates = ixs.filter((ix) => + ix.programId.equals(ASSOCIATED_TOKEN_PROGRAM_ID), + ); + expect(ataCreates.length).toBeGreaterThanOrEqual(1); + expect( + ataCreates.some((ix) => ix.keys.some((k) => k.pubkey.equals(quoteAta))), + ).toBe(true); + }); +}); diff --git a/src/sdk.ts b/src/sdk.ts index 51f2369b..61990b7a 100644 --- a/src/sdk.ts +++ b/src/sdk.ts @@ -413,11 +413,40 @@ export class PumpSdk { quoteMint?: PublicKey; quoteTokenProgram?: PublicKey; }): Promise { - // Non-native quote (e.g. USDC) routes to buy_v2, which builds its own - // accounts (and uses TOKEN_2022 for the base mint). It does not need the - // legacy extendAccount / base-ATA-creation setup, so branch before it. + // Non-native quote (e.g. USDC) routes to buy_v2. buy_v2 does NOT init the + // user's base/quote ATAs (associated_base_user / associated_quote_user have + // no init in the IDL), so we must prepend idempotent ATA creates ourselves: + // - base: Token-2022 ATA (create_v2 coins use TOKEN_2022 for the base + // mint), gated on a missing account like the legacy native path. + // - quote: the quote mint's ATA, always (the USDC ATA may not exist); + // idempotent so it's a no-op when it already does. if (!quoteMint.equals(NATIVE_MINT)) { + const setupIxs: TransactionInstruction[] = []; + + if (!associatedUserAccountInfo) { + setupIxs.push( + createAssociatedTokenAccountIdempotentInstruction( + user, + getAssociatedTokenAddressSync(mint, user, true, TOKEN_2022_PROGRAM_ID), + user, + mint, + TOKEN_2022_PROGRAM_ID, + ), + ); + } + + setupIxs.push( + createAssociatedTokenAccountIdempotentInstruction( + user, + getAssociatedTokenAddressSync(quoteMint, user, true, quoteTokenProgram), + user, + quoteMint, + quoteTokenProgram, + ), + ); + return [ + ...setupIxs, await this.buyV2({ user, mint, From 0135fe282dbd5fbc586d86e1e1c1bd2be8ee7239 Mon Sep 17 00:00:00 2001 From: Hank Date: Wed, 3 Jun 2026 19:01:36 -0400 Subject: [PATCH 08/14] feat(create): USDC support in createV2AndBuyInstructions --- src/__tests__/usdc.test.ts | 29 ++++++++++++++++++++++++++ src/sdk.ts | 42 ++++++++++++++++++++++++++++++++++++-- 2 files changed, 69 insertions(+), 2 deletions(-) diff --git a/src/__tests__/usdc.test.ts b/src/__tests__/usdc.test.ts index b05da6a5..9ea247ab 100644 --- a/src/__tests__/usdc.test.ts +++ b/src/__tests__/usdc.test.ts @@ -240,3 +240,32 @@ describe("buyInstructions routing (USDC)", () => { ).toBe(true); }); }); + +describe("createV2AndBuyInstructions", () => { + const mint = new Keypair().publicKey; + const creator = new Keypair().publicKey; + const user = new Keypair().publicKey; + const common = { + global: makeGlobal(), mint, name: "n", symbol: "n", uri: "u", + creator, user, amount: new BN("15000000000000"), mayhemMode: false, + }; + it("SOL: 4 instructions, create_v2 has 16 keys, buy leg is legacy buy", async () => { + const ixs = await PUMP_SDK.createV2AndBuyInstructions({ ...common, solAmount: new BN("430000000") }); + expect(ixs).toHaveLength(4); + expect(ixs[0]!.keys).toHaveLength(16); + expect([...ixs[3]!.data.slice(0, 8)]).toEqual([102, 6, 61, 18, 1, 218, 235, 234]); + }); + it("USDC: create_v2 has 19 keys; user USDC ATA created; buy leg is buy_v2 capped by quoteAmount", async () => { + const ixs = await PUMP_SDK.createV2AndBuyInstructions({ + ...common, solAmount: new BN(0), + quoteMint: USDC_MINT, quoteTokenProgram: TOKEN_PROGRAM_ID, quoteAmount: new BN("15000000"), + }); + expect(ixs[0]!.keys).toHaveLength(19); + const buyLeg = ixs.at(-1)!; + expect([...buyLeg.data.slice(0, 8)]).toEqual([184, 23, 238, 97, 103, 197, 211, 61]); + expect(buyLeg.keys).toHaveLength(27); + // the user's USDC ATA is created (associated-token-program ix targeting the user's USDC ATA) + const userUsdcAta = getAssociatedTokenAddressSync(USDC_MINT, user, true, TOKEN_PROGRAM_ID); + expect(ixs.some((ix) => ix.programId.equals(ASSOCIATED_TOKEN_PROGRAM_ID) && ix.keys.some((k) => k.pubkey.equals(userUsdcAta)))).toBe(true); + }); +}); diff --git a/src/sdk.ts b/src/sdk.ts index 61990b7a..62d2143d 100644 --- a/src/sdk.ts +++ b/src/sdk.ts @@ -520,6 +520,9 @@ export class PumpSdk { solAmount, mayhemMode, cashback = false, + quoteMint = NATIVE_MINT, + quoteTokenProgram = TOKEN_PROGRAM_ID, + quoteAmount, }: { global: Global; mint: PublicKey; @@ -532,6 +535,9 @@ export class PumpSdk { solAmount: BN; mayhemMode: boolean; cashback?: boolean; + quoteMint?: PublicKey; + quoteTokenProgram?: PublicKey; + quoteAmount?: BN; }): Promise { const associatedUser = getAssociatedTokenAddressSync( mint, @@ -539,7 +545,8 @@ export class PumpSdk { true, TOKEN_2022_PROGRAM_ID, ); - return [ + + const instructions: TransactionInstruction[] = [ await this.createV2Instruction({ mint, name, @@ -549,6 +556,8 @@ export class PumpSdk { user, mayhemMode, cashback, + quoteMint, + quoteTokenProgram, }), await this.extendAccountInstruction({ account: bondingCurvePda(mint), @@ -561,6 +570,34 @@ export class PumpSdk { mint, TOKEN_2022_PROGRAM_ID, ), + ]; + + if (!quoteMint.equals(NATIVE_MINT)) { + // buy_v2 does not init the user's quote ATA, so the dev-buy would fail + // without it. The base Token-2022 ATA is already created above. + instructions.push( + createAssociatedTokenAccountIdempotentInstruction( + user, + getAssociatedTokenAddressSync(quoteMint, user, true, quoteTokenProgram), + user, + quoteMint, + quoteTokenProgram, + ), + await this.buyV2({ + user, + mint, + creator, + amount, + quoteAmount: quoteAmount ?? solAmount, + quoteMint, + quoteTokenProgram, + feeRecipient: getFeeRecipient(global, mayhemMode), + }), + ); + return instructions; + } + + instructions.push( await this.buyInstruction({ global, mint, @@ -573,7 +610,8 @@ export class PumpSdk { tokenProgram: TOKEN_2022_PROGRAM_ID, mayhemMode, }), - ]; + ); + return instructions; } /** From 9cf70ee31c09023cdedfbb639d2ee086a2f35cac Mon Sep 17 00:00:00 2001 From: Hank Date: Wed, 3 Jun 2026 19:09:45 -0400 Subject: [PATCH 09/14] docs(create): document USDC quoteAmount semantics; use isNativeQuote helper --- src/sdk.ts | 33 ++++++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/src/sdk.ts b/src/sdk.ts index 62d2143d..ad2ee834 100644 --- a/src/sdk.ts +++ b/src/sdk.ts @@ -131,6 +131,7 @@ import { SUPPORTED_SOCIAL_PLATFORMS, platformToString, } from "./state"; +import { isNativeQuote } from "./quoteMints"; /** Create an Anchor Program instance for the Pump bonding curve program. */ export function getPumpProgram(connection: Connection): Program { @@ -359,7 +360,7 @@ export class PumpSdk { quoteMint?: PublicKey; quoteTokenProgram?: PublicKey; }): Promise { - const remaining = quoteMint.equals(NATIVE_MINT) + const remaining = isNativeQuote(quoteMint) ? [] : [ { pubkey: quoteMint, isWritable: false, isSigner: false }, @@ -386,6 +387,17 @@ export class PumpSdk { .instruction(); } + /** + * Build the instructions to buy a coin on the bonding curve. + * + * When `quoteMint` is a non-native mint (e.g. USDC), the coin is bought against + * that quote mint; the buy leg routes to `buy_v2`. In that mode the maximum + * quote cost is expressed in the quote mint's base units (USDC = 6 decimals) — + * NOT lamports — and is passed straight through as `buy_v2`'s `quoteAmount` + * (the caller is responsible for the cap; no legacy slippage math is applied). + * The default (`quoteMint = NATIVE_MINT`) preserves the existing SOL behavior + * unchanged. + */ async buyInstructions({ global, bondingCurveAccountInfo, @@ -420,7 +432,7 @@ export class PumpSdk { // mint), gated on a missing account like the legacy native path. // - quote: the quote mint's ATA, always (the USDC ATA may not exist); // idempotent so it's a no-op when it already does. - if (!quoteMint.equals(NATIVE_MINT)) { + if (!isNativeQuote(quoteMint)) { const setupIxs: TransactionInstruction[] = []; if (!associatedUserAccountInfo) { @@ -508,6 +520,17 @@ export class PumpSdk { return instructions; } + /** + * Build the create + dev-buy instructions for a new coin. + * + * When `quoteMint` is a non-native mint (e.g. USDC), the coin is created and + * bought against that quote mint; the buy leg routes to `buy_v2`. In that mode, + * `quoteAmount` is the maximum quote cost in the quote mint's base units + * (USDC = 6 decimals) — NOT lamports. When `quoteAmount` is omitted it falls + * back to `solAmount` interpreted as quote base units, so prefer passing + * `quoteAmount` explicitly in USDC mode. The default (`quoteMint = NATIVE_MINT`) + * preserves the existing SOL behavior unchanged. + */ async createV2AndBuyInstructions({ global, mint, @@ -572,7 +595,7 @@ export class PumpSdk { ), ]; - if (!quoteMint.equals(NATIVE_MINT)) { + if (!isNativeQuote(quoteMint)) { // buy_v2 does not init the user's quote ATA, so the dev-buy would fail // without it. The base Token-2022 ATA is already created above. instructions.push( @@ -696,7 +719,7 @@ export class PumpSdk { // Non-native quote (e.g. USDC) routes to buy_v2. solAmount is the max quote // cost the caller already capped; pass it straight as quoteAmount (no legacy // slippage math). - if (!quoteMint.equals(NATIVE_MINT)) { + if (!isNativeQuote(quoteMint)) { return await this.buyV2({ user, mint, @@ -991,7 +1014,7 @@ export class PumpSdk { }): Promise { // Non-native quote (e.g. USDC) routes to buy_v2; solAmount is the max quote // cost (caller controls the cap), passed straight through as quoteAmount. - if (!quoteMint.equals(NATIVE_MINT)) { + if (!isNativeQuote(quoteMint)) { return await this.buyV2({ user, mint, From a26c0dca8c6e683acad292c1aef3a6d78aaae800 Mon Sep 17 00:00:00 2001 From: Hank Date: Wed, 3 Jun 2026 19:16:38 -0400 Subject: [PATCH 10/14] feat(online): forward USDC quote params through OnlinePumpSdk wrappers --- src/__tests__/onlineSdk.test.ts | 75 ++++++++++++++++++++++++++++++++- src/onlineSdk.ts | 21 +++++++++ 2 files changed, 94 insertions(+), 2 deletions(-) diff --git a/src/__tests__/onlineSdk.test.ts b/src/__tests__/onlineSdk.test.ts index 92af1557..2fc4e418 100644 --- a/src/__tests__/onlineSdk.test.ts +++ b/src/__tests__/onlineSdk.test.ts @@ -6,11 +6,13 @@ * These tests mock the RPC connection and underlying SDK calls so no network * access is required. */ -import { AccountInfo, PublicKey } from "@solana/web3.js"; +import { AccountInfo, Keypair, PublicKey } from "@solana/web3.js"; +import { TOKEN_PROGRAM_ID } from "@solana/spl-token"; import BN from "bn.js"; -import { OnlinePumpSdk, BuyQuote } from "../onlineSdk"; +import { OnlinePumpSdk, BuyQuote, OFFLINE_PUMP_PROGRAM } from "../onlineSdk"; import { PUMP_SDK } from "../sdk"; +import { USDC_MINT } from "../quoteMints"; import { bondingCurvePda } from "../pda"; import { makeGlobal, @@ -754,3 +756,72 @@ describe("OnlinePumpSdk.getTokenBalance", () => { expect(bal.toString()).toBe("5000000000"); }); }); + +// ─── USDC quote forwarding (Task 7b) ────────────────────────────────────────── +// Proves the OnlinePumpSdk wrappers forward the new quote params +// (quoteMint / quoteTokenProgram / quoteAmount) through to PUMP_SDK.*. +describe("OnlinePumpSdk USDC quote forwarding", () => { + // The wrappers delegate to the offline PUMP_SDK instruction builders, which + // need PUMP_SDK's offline anchor program. sdk.ts <-> onlineSdk.ts form a + // circular import; because this suite imports "../onlineSdk" first, the + // PUMP_SDK singleton constructed mid-cycle captured an undefined offline + // program. Heal it from the now-initialized OFFLINE_PUMP_PROGRAM so the + // offline builders the wrappers call work. (Existing suites import order is + // unchanged; the AMM-event decode tests still rely on it.) + beforeAll(() => { + if (!(PUMP_SDK as any).offlinePumpProgram) { + (PUMP_SDK as any).offlinePumpProgram = OFFLINE_PUMP_PROGRAM; + } + }); + + it("createV2Instruction forwards quoteMint (USDC → create_v2 has 19 keys)", async () => { + // createV2Instruction needs no on-chain fetch, so a bare sdk suffices. + const sdk = makeSdk(); + const mint = new Keypair().publicKey; + + const ix = await sdk.createV2Instruction({ + mint, + name: "n", + symbol: "n", + uri: "u", + creator: TEST_CREATOR, + user: USER, + quoteMint: USDC_MINT, + quoteTokenProgram: TOKEN_PROGRAM_ID, + }); + + // USDC appends the 3 quote remaining accounts (16 → 19) vs the SOL default. + expect(ix.keys).toHaveLength(19); + expect(ix.keys.some((k) => k.pubkey.equals(USDC_MINT))).toBe(true); + }); + + it("createV2AndBuyInstructions forwards quoteMint/quoteTokenProgram/quoteAmount (buy leg is buy_v2)", async () => { + const sdk = makeSdk(); + // The wrapper fetches global + feeConfig; both are spy-able (no RPC needed). + jest.spyOn(sdk, "fetchGlobal").mockResolvedValue(makeGlobal()); + jest.spyOn(sdk, "fetchFeeConfig").mockResolvedValue(makeFeeConfig()); + const mint = new Keypair().publicKey; + + const ixs = await sdk.createV2AndBuyInstructions({ + mint, + name: "n", + symbol: "n", + uri: "u", + creator: TEST_CREATOR, + user: USER, + solAmount: new BN(0), + quoteMint: USDC_MINT, + quoteTokenProgram: TOKEN_PROGRAM_ID, + quoteAmount: new BN("15000000"), + }); + + // create leg gains the 3 USDC quote accounts (19 keys). + expect(ixs[0]!.keys).toHaveLength(19); + // buy leg routes to buy_v2: official discriminator + 27 accounts. + const buyLeg = ixs.at(-1)!; + expect([...buyLeg.data.slice(0, 8)]).toEqual([ + 184, 23, 238, 97, 103, 197, 211, 61, + ]); + expect(buyLeg.keys).toHaveLength(27); + }); +}); diff --git a/src/onlineSdk.ts b/src/onlineSdk.ts index 359e48f7..7aeb9838 100644 --- a/src/onlineSdk.ts +++ b/src/onlineSdk.ts @@ -292,6 +292,8 @@ export class OnlinePumpSdk { solAmount, slippage, tokenProgram = TOKEN_PROGRAM_ID, + quoteMint = NATIVE_MINT, + quoteTokenProgram = TOKEN_PROGRAM_ID, }: { bondingCurveAccountInfo: AccountInfo; bondingCurve: BondingCurve; @@ -302,6 +304,8 @@ export class OnlinePumpSdk { solAmount: BN; slippage: number; tokenProgram?: PublicKey; + quoteMint?: PublicKey; + quoteTokenProgram?: PublicKey; }): Promise { const global = await this.fetchGlobal(); return PUMP_SDK.buyInstructions({ @@ -315,6 +319,8 @@ export class OnlinePumpSdk { solAmount, slippage, tokenProgram, + quoteMint, + quoteTokenProgram, }); } @@ -2379,6 +2385,8 @@ export class OnlinePumpSdk { user, mayhemMode = false, cashback = false, + quoteMint = NATIVE_MINT, + quoteTokenProgram = TOKEN_PROGRAM_ID, }: { mint: PublicKey; name: string; @@ -2388,6 +2396,8 @@ export class OnlinePumpSdk { user: PublicKey; mayhemMode?: boolean; cashback?: boolean; + quoteMint?: PublicKey; + quoteTokenProgram?: PublicKey; }): Promise { return PUMP_SDK.createV2Instruction({ mint, @@ -2398,6 +2408,8 @@ export class OnlinePumpSdk { user, mayhemMode, cashback, + quoteMint, + quoteTokenProgram, }); } @@ -2425,6 +2437,9 @@ export class OnlinePumpSdk { solAmount, mayhemMode = false, cashback = false, + quoteMint = NATIVE_MINT, + quoteTokenProgram = TOKEN_PROGRAM_ID, + quoteAmount, }: { mint: PublicKey; name: string; @@ -2435,6 +2450,9 @@ export class OnlinePumpSdk { solAmount: BN; mayhemMode?: boolean; cashback?: boolean; + quoteMint?: PublicKey; + quoteTokenProgram?: PublicKey; + quoteAmount?: BN; }): Promise { const [global, feeConfig] = await Promise.all([ this.fetchGlobal(), @@ -2473,6 +2491,9 @@ export class OnlinePumpSdk { solAmount, mayhemMode, cashback, + quoteMint, + quoteTokenProgram, + quoteAmount, }); } From 248da80593d6ff64cbc64b10248c5745972e9de5 Mon Sep 17 00:00:00 2001 From: Hank Date: Wed, 3 Jun 2026 19:17:18 -0400 Subject: [PATCH 11/14] test(usdc): add simulate + devnet smoke harness for the USDC launch path --- scripts/devnet-usdc-smoke.ts | 100 +++++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 scripts/devnet-usdc-smoke.ts diff --git a/scripts/devnet-usdc-smoke.ts b/scripts/devnet-usdc-smoke.ts new file mode 100644 index 00000000..fe174bde --- /dev/null +++ b/scripts/devnet-usdc-smoke.ts @@ -0,0 +1,100 @@ +/** + * USDC launch-path verification harness for `createV2AndBuyInstructions`. + * + * Builds an atomic create_v2 + buy_v2 (USDC quote mint) transaction and either + * SIMULATES it (default, no broadcast) or broadcasts it once on devnet. + * + * This is NOT part of the unit suite — it requires an RPC and a funded keypair. + * + * Usage: + * # Simulate only (no broadcast). Needs an RPC and a keypair (payer/mint signer). + * RPC_URL= KEYPAIR=~/.config/solana/id.json npx tsx scripts/devnet-usdc-smoke.ts + * + * # Broadcast once on devnet (free SOL via airdrop). Set the devnet-whitelisted + * # quote mint if USDC itself isn't whitelisted on devnet. + * RPC_URL=https://api.devnet.solana.com DEVNET_BROADCAST=1 \ + * DEVNET_QUOTE_MINT= KEYPAIR= \ + * npx tsx scripts/devnet-usdc-smoke.ts + */ +import { readFileSync } from "node:fs"; +import { + ComputeBudgetProgram, + Connection, + Keypair, + PublicKey, + TransactionMessage, + VersionedTransaction, +} from "@solana/web3.js"; +import { TOKEN_PROGRAM_ID } from "@solana/spl-token"; +import BN from "bn.js"; + +import { OnlinePumpSdk } from "../src/onlineSdk"; +import { USDC_MINT } from "../src/quoteMints"; + +function loadKeypair(path: string): Keypair { + return Keypair.fromSecretKey( + Uint8Array.from(JSON.parse(readFileSync(path, "utf8"))), + ); +} + +async function main(): Promise { + const rpcUrl = process.env.RPC_URL; + const keypairPath = process.env.KEYPAIR; + if (!rpcUrl || !keypairPath) { + throw new Error("Set RPC_URL and KEYPAIR env vars."); + } + + const connection = new Connection(rpcUrl, "confirmed"); + const sdk = new OnlinePumpSdk(connection); + const payer = loadKeypair(keypairPath); + const mint = Keypair.generate(); + const quoteMint = process.env.DEVNET_QUOTE_MINT + ? new PublicKey(process.env.DEVNET_QUOTE_MINT) + : USDC_MINT; + + const global = await sdk.fetchGlobal(); + const ixs = await sdk.createV2AndBuyInstructions({ + global, + mint: mint.publicKey, + name: "SMOKE", + symbol: "SMOKE", + uri: "https://example.com/smoke.json", + creator: payer.publicKey, + user: payer.publicKey, + amount: new BN("15000000000000"), // 1.5% of supply (token base units) + solAmount: new BN(0), + mayhemMode: false, + quoteMint, + quoteTokenProgram: TOKEN_PROGRAM_ID, + quoteAmount: new BN(process.env.QUOTE_AMOUNT ?? "15000000"), // max quote cost (6dp) + }); + + const { blockhash } = await connection.getLatestBlockhash(); + const message = new TransactionMessage({ + payerKey: payer.publicKey, + recentBlockhash: blockhash, + instructions: [ + ComputeBudgetProgram.setComputeUnitLimit({ units: 400_000 }), + ...ixs, + ], + }).compileToV0Message(); + const tx = new VersionedTransaction(message); + tx.sign([payer, mint]); + + const sim = await connection.simulateTransaction(tx, { sigVerify: false }); + console.log("simulate err:", sim.value.err); + console.log((sim.value.logs ?? []).join("\n")); + if (sim.value.err) { + throw new Error("simulation failed — do NOT broadcast"); + } + + if (process.env.DEVNET_BROADCAST === "1") { + const sig = await connection.sendTransaction(tx); + console.log("devnet broadcast sig:", sig); + } +} + +main().catch((e) => { + console.error(e); + process.exit(1); +}); From fc23c05a14d23d03531e8a4c8482eac1ae32f209 Mon Sep 17 00:00:00 2001 From: Hank Wyatt <51133067+HankWyatt@users.noreply.github.com> Date: Wed, 3 Jun 2026 23:17:20 -0400 Subject: [PATCH 12/14] test(usdc): cover quoteAmount cap, ALT tx-size constraint, explicit quote token program --- src/__tests__/usdc.test.ts | 108 ++++++++++++++++++++++++++++++++++++- 1 file changed, 107 insertions(+), 1 deletion(-) diff --git a/src/__tests__/usdc.test.ts b/src/__tests__/usdc.test.ts index 9ea247ab..5ca1e020 100644 --- a/src/__tests__/usdc.test.ts +++ b/src/__tests__/usdc.test.ts @@ -5,7 +5,14 @@ import { TOKEN_2022_PROGRAM_ID, getAssociatedTokenAddressSync, } from "@solana/spl-token"; -import { AccountInfo, Connection, Keypair, PublicKey } from "@solana/web3.js"; +import { + AccountInfo, + Connection, + Keypair, + PublicKey, + TransactionMessage, + VersionedTransaction, +} from "@solana/web3.js"; import BN from "bn.js"; import { USDC_MINT, QUOTE_MINTS, isNativeQuote } from "../quoteMints"; import { getPumpProgram, PUMP_SDK, BONDING_CURVE_NEW_SIZE } from "../sdk"; @@ -97,6 +104,105 @@ describe("createV2Instruction quote mint", () => { expect(r2!.pubkey.equals(TOKEN_PROGRAM_ID)).toBe(true); expect(r2!.isWritable).toBe(false); }); + it("threads an explicit non-default quoteTokenProgram into createV2Instruction", async () => { + // Stand-in: drive the quote program off USDC's default (TOKEN_PROGRAM_ID) so + // the threading is observable. R2 (the quote_token_program remaining account) + // must equal it, and R1 (associated_quote_bonding_curve) must be the ATA + // derived *with* that program (a different ATA than the default would yield). + const ix = await PUMP_SDK.createV2Instruction({ + mint, name: "n", symbol: "n", uri: "u", creator, user, mayhemMode: false, + quoteMint: USDC_MINT, quoteTokenProgram: TOKEN_2022_PROGRAM_ID, + }); + expect(ix.keys).toHaveLength(19); + const [, r1, r2] = ix.keys.slice(16); + expect(r2!.pubkey.equals(TOKEN_2022_PROGRAM_ID)).toBe(true); + expect( + r1!.pubkey.equals( + getAssociatedTokenAddressSync(USDC_MINT, bondingCurvePda(mint), true, TOKEN_2022_PROGRAM_ID), + ), + ).toBe(true); + }); +}); + +describe("createV2AndBuyInstructions USDC quoteAmount", () => { + const mint = new Keypair().publicKey; + const creator = new Keypair().publicKey; + const user = new Keypair().publicKey; + + it("encodes quoteAmount (not solAmount) as the buy_v2 cap", async () => { + const quoteAmount = new BN("12345678"); + const ixs = await PUMP_SDK.createV2AndBuyInstructions({ + global: makeGlobal(), mint, name: "n", symbol: "n", uri: "u", + creator, user, amount: new BN("15000000000000"), mayhemMode: false, + solAmount: new BN("999"), // deliberately different from quoteAmount + quoteMint: USDC_MINT, quoteTokenProgram: TOKEN_PROGRAM_ID, quoteAmount, + }); + const buyLeg = ixs.at(-1)!; + // buy_v2 data = disc[8] + amount u64 (LE) [8,16) + max_sol_cost u64 (LE) [16,24). + // max_sol_cost is the quote cap; assert it is the quoteAmount we passed, not solAmount. + expect(buyLeg.data).toHaveLength(24); + expect([...buyLeg.data.slice(0, 8)]).toEqual([184, 23, 238, 97, 103, 197, 211, 61]); + const capLE = buyLeg.data.subarray(16, 24); + expect(new BN(capLE, "le").toString()).toBe(quoteAmount.toString()); + }); +}); + +describe("createV2AndBuyInstructions tx-size / ALT constraint", () => { + // OPERATIONAL FINDING: the atomic USDC `create_v2 + buy_v2` instruction set is + // too large for a single legacy/v0 transaction without an Address Lookup Table — + // its combined unique account set is larger, and the compiled v0 message + // serializes to MORE than the 1232-byte packet limit. The SOL path fits without + // one. Senders launching a USDC coin with a dev-buy MUST attach an ALT. + // + // NOTE: compileToV0Message()/serialize() do NOT throw at this size (the RangeError + // only fires above ~256 account keys), so we assert the measured byte size and the + // unique-account count directly rather than relying on a throw. + const PACKET_DATA_SIZE = 1232; // Solana max serialized transaction size. + const mint = new Keypair().publicKey; + const creator = new Keypair().publicKey; + const user = new Keypair().publicKey; + const common = { + global: makeGlobal(), mint, name: "n", symbol: "n", uri: "u", + creator, user, amount: new BN("15000000000000"), mayhemMode: false, + }; + + function uniqueAccounts(ixs: { programId: PublicKey; keys: { pubkey: PublicKey }[] }[]): number { + const seen = new Set(); + for (const ix of ixs) { + seen.add(ix.programId.toBase58()); + for (const k of ix.keys) seen.add(k.pubkey.toBase58()); + } + return seen.size; + } + + function serializedV0Size(ixs: any[]): number { + const message = new TransactionMessage({ + payerKey: user, + recentBlockhash: "11111111111111111111111111111111", // dummy blockhash + instructions: ixs, + }).compileToV0Message(); + return new VersionedTransaction(message).serialize().length; + } + + it("SOL fits in one legacy tx but USDC needs an Address Lookup Table", async () => { + const solIxs = await PUMP_SDK.createV2AndBuyInstructions({ ...common, solAmount: new BN("430000000") }); + const usdcIxs = await PUMP_SDK.createV2AndBuyInstructions({ + ...common, solAmount: new BN(0), + quoteMint: USDC_MINT, quoteTokenProgram: TOKEN_PROGRAM_ID, quoteAmount: new BN("15000000"), + }); + + const solUnique = uniqueAccounts(solIxs); + const usdcUnique = uniqueAccounts(usdcIxs); + // Measured (deterministic given fixed keypairs): SOL=24, USDC=32 unique accounts. + expect(usdcUnique).toBeGreaterThan(solUnique); + expect(usdcUnique).toBeGreaterThanOrEqual(32); + + const solSize = serializedV0Size(solIxs); + const usdcSize = serializedV0Size(usdcIxs); + // Measured (deterministic): SOL=1084 bytes (fits), USDC=1361 bytes (exceeds 1232). + expect(solSize).toBeLessThanOrEqual(PACKET_DATA_SIZE); + expect(usdcSize).toBeGreaterThan(PACKET_DATA_SIZE); + }); }); describe("buyV2 builder (USDC)", () => { From cc8f460014717bebbd3da3bc668d7340fef99075 Mon Sep 17 00:00:00 2001 From: Hank Wyatt <51133067+HankWyatt@users.noreply.github.com> Date: Wed, 3 Jun 2026 23:25:12 -0400 Subject: [PATCH 13/14] docs(usdc): document ALT requirement; make devnet smoke harness ALT-aware --- CHANGELOG.md | 9 +++ scripts/devnet-usdc-smoke.ts | 121 ++++++++++++++++++++++++++++++++++- src/sdk.ts | 8 +++ 3 files changed, 135 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d104bccb..761591c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,15 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Added +- USDC (non-native quote-mint) support for the launch path: optional `quoteMint`/`quoteTokenProgram` on `createV2Instruction`, `buyInstructions`, `getBuyInstructionRaw`, and `createV2AndBuyInstructions` (plus an explicit `quoteAmount` cap), a new `buyV2` builder, `USDC_MINT`/`QUOTE_MINTS`/`isNativeQuote`, `BUYBACK_FEE_RECIPIENTS`/`pickBuybackFeeRecipient`, `OnlinePumpSdk` quote forwarding, and the `buy_v2` instruction in the bundled IDL. Existing SOL behavior is unchanged (all new params default to wrapped SOL). +- `scripts/devnet-usdc-smoke.ts`: a devnet/simulate verification harness for the USDC launch path. + +### Notes +- A USDC `createV2AndBuyInstructions` transaction (~1361 bytes) exceeds the 1232-byte single-transaction limit and must be sent as a v0 transaction with an Address Lookup Table. The SOL path fits without one. + ## [1.32.0] - 2026-04-23 Prepares the SDK for the **2026-04-28, 16:00 UTC** breaking on-chain program upgrade to the Pump bonding curve and PumpSwap AMM programs. See [docs/pump-public-docs/BREAKING_FEE_RECIPIENT.md](docs/pump-public-docs/BREAKING_FEE_RECIPIENT.md) for the protocol spec and [docs/MIGRATION.md](docs/MIGRATION.md#upgrading-to-v1320-latest) for call-site guidance. diff --git a/scripts/devnet-usdc-smoke.ts b/scripts/devnet-usdc-smoke.ts index fe174bde..d215379e 100644 --- a/scripts/devnet-usdc-smoke.ts +++ b/scripts/devnet-usdc-smoke.ts @@ -4,10 +4,19 @@ * Builds an atomic create_v2 + buy_v2 (USDC quote mint) transaction and either * SIMULATES it (default, no broadcast) or broadcasts it once on devnet. * + * The USDC create_v2 + buy_v2 set serializes to ~1361 bytes across 32 unique + * accounts, which exceeds the 1232-byte single-transaction limit. It therefore + * MUST be sent as a v0 transaction with an Address Lookup Table (ALT). This + * harness builds an ephemeral ALT over the static (non-signer) accounts, waits + * for it to activate, then compiles the create+buy into a v0 message that + * references it. (The SOL path fits without an ALT.) + * * This is NOT part of the unit suite — it requires an RPC and a funded keypair. * * Usage: * # Simulate only (no broadcast). Needs an RPC and a keypair (payer/mint signer). + * # Still creates + confirms a real ephemeral ALT on-chain (a cheap tx), so the + * # keypair must be funded even in simulate mode. * RPC_URL= KEYPAIR=~/.config/solana/id.json npx tsx scripts/devnet-usdc-smoke.ts * * # Broadcast once on devnet (free SOL via airdrop). Set the devnet-whitelisted @@ -18,10 +27,13 @@ */ import { readFileSync } from "node:fs"; import { + AddressLookupTableAccount, + AddressLookupTableProgram, ComputeBudgetProgram, Connection, Keypair, PublicKey, + TransactionInstruction, TransactionMessage, VersionedTransaction, } from "@solana/web3.js"; @@ -29,6 +41,7 @@ import { TOKEN_PROGRAM_ID } from "@solana/spl-token"; import BN from "bn.js"; import { OnlinePumpSdk } from "../src/onlineSdk"; +import { PUMP_SDK } from "../src/sdk"; import { USDC_MINT } from "../src/quoteMints"; function loadKeypair(path: string): Keypair { @@ -37,6 +50,95 @@ function loadKeypair(path: string): Keypair { ); } +const sleep = (ms: number): Promise => + new Promise((resolve) => setTimeout(resolve, ms)); + +/** + * Collect the unique account keys referenced across `ixs` (including each + * instruction's programId), EXCLUDING the static signer keys (`exclude`). The + * payer and the mint keypair must remain static signer keys in the compiled + * message — they cannot be sourced from an ALT — so we never put them in the + * lookup table. + */ +function altAddresses( + ixs: TransactionInstruction[], + exclude: PublicKey[], +): PublicKey[] { + const excluded = new Set(exclude.map((k) => k.toBase58())); + const seen = new Map(); + for (const ix of ixs) { + const candidates = [ix.programId, ...ix.keys.map((k) => k.pubkey)]; + for (const key of candidates) { + const b58 = key.toBase58(); + if (excluded.has(b58)) continue; + if (!seen.has(b58)) seen.set(b58, key); + } + } + return [...seen.values()]; +} + +/** + * Create an ephemeral ALT over `addresses`, then poll until it is fetchable and + * populated. A newly created table can only be referenced once the slot it was + * created in is confirmed, so we bound-poll `getAddressLookupTable` (re-fetching + * the slot too) with a short delay between tries. + * + * PRODUCTION NOTE: launching many coins should reuse ONE long-lived ALT of the + * static pump accounts (global, programs, fee recipients, etc.) rather than + * creating + paying for a fresh ephemeral table per launch. + */ +async function buildEphemeralAlt( + connection: Connection, + payer: Keypair, + addresses: PublicKey[], +): Promise { + const recentSlot = await connection.getSlot("finalized"); + const [createIx, altAddress] = AddressLookupTableProgram.createLookupTable({ + authority: payer.publicKey, + payer: payer.publicKey, + recentSlot, + }); + const extendIx = AddressLookupTableProgram.extendLookupTable({ + lookupTable: altAddress, + authority: payer.publicKey, + payer: payer.publicKey, + addresses, + }); + + const { blockhash, lastValidBlockHeight } = + await connection.getLatestBlockhash(); + const message = new TransactionMessage({ + payerKey: payer.publicKey, + recentBlockhash: blockhash, + instructions: [createIx, extendIx], + }).compileToV0Message(); + const tx = new VersionedTransaction(message); + tx.sign([payer]); + + const sig = await connection.sendTransaction(tx); + await connection.confirmTransaction( + { signature: sig, blockhash, lastValidBlockHeight }, + "confirmed", + ); + console.log("created ephemeral ALT:", altAddress.toBase58(), "tx:", sig); + + // Bound-poll until the ALT resolves and is populated/usable. + for (let attempt = 0; attempt < 30; attempt++) { + const fetched = await connection.getAddressLookupTable(altAddress); + const account = fetched.value; + if (account && account.state.addresses.length >= addresses.length) { + console.log( + "ALT active with", + account.state.addresses.length, + "addresses", + ); + return account; + } + await sleep(800); + } + throw new Error(`ALT ${altAddress.toBase58()} did not activate in time`); +} + async function main(): Promise { const rpcUrl = process.env.RPC_URL; const keypairPath = process.env.KEYPAIR; @@ -52,8 +154,11 @@ async function main(): Promise { ? new PublicKey(process.env.DEVNET_QUOTE_MINT) : USDC_MINT; + // Fetch global once and call the lower-level builder directly so we can pass an + // explicit `amount` (1.5% of supply). The OnlinePumpSdk wrapper instead derives + // `amount` from `solAmount` and does not take `global`/`amount`. const global = await sdk.fetchGlobal(); - const ixs = await sdk.createV2AndBuyInstructions({ + const ixs = await PUMP_SDK.createV2AndBuyInstructions({ global, mint: mint.publicKey, name: "SMOKE", @@ -69,6 +174,14 @@ async function main(): Promise { quoteAmount: new BN(process.env.QUOTE_AMOUNT ?? "15000000"), // max quote cost (6dp) }); + // The USDC create+buy is too large for a single tx; build an ALT over the + // static (non-signer) accounts. The payer and mint stay as static signer keys. + const altAccount = await buildEphemeralAlt( + connection, + payer, + altAddresses(ixs, [payer.publicKey, mint.publicKey]), + ); + const { blockhash } = await connection.getLatestBlockhash(); const message = new TransactionMessage({ payerKey: payer.publicKey, @@ -77,13 +190,15 @@ async function main(): Promise { ComputeBudgetProgram.setComputeUnitLimit({ units: 400_000 }), ...ixs, ], - }).compileToV0Message(); + }).compileToV0Message([altAccount]); const tx = new VersionedTransaction(message); tx.sign([payer, mint]); + console.log("create+buy v0 tx size (bytes):", tx.serialize().length); + const sim = await connection.simulateTransaction(tx, { sigVerify: false }); console.log("simulate err:", sim.value.err); - console.log((sim.value.logs ?? []).join("\n")); + console.log((sim.value.logs ?? []).slice(-15).join("\n")); if (sim.value.err) { throw new Error("simulation failed — do NOT broadcast"); } diff --git a/src/sdk.ts b/src/sdk.ts index ad2ee834..3f107341 100644 --- a/src/sdk.ts +++ b/src/sdk.ts @@ -530,6 +530,14 @@ export class PumpSdk { * back to `solAmount` interpreted as quote base units, so prefer passing * `quoteAmount` explicitly in USDC mode. The default (`quoteMint = NATIVE_MINT`) * preserves the existing SOL behavior unchanged. + * + * Transaction-size note: for a non-native (USDC) quote mint, the returned + * `create_v2 + buy_v2` instructions serialize to ~1361 bytes across 32 unique + * accounts, which exceeds the 1232-byte single-legacy-transaction limit. They + * MUST be sent as a v0 `VersionedTransaction` with an Address Lookup Table + * covering the static (non-signer) accounts. The SOL path (~1084 bytes) fits in + * one legacy transaction without an ALT. See `scripts/devnet-usdc-smoke.ts` for + * a worked example of building the ALT and compiling the v0 message. */ async createV2AndBuyInstructions({ global, From 9c314856e058b883fb01f11376fac6442747f232 Mon Sep 17 00:00:00 2001 From: Hank Wyatt <51133067+HankWyatt@users.noreply.github.com> Date: Wed, 3 Jun 2026 23:29:39 -0400 Subject: [PATCH 14/14] fix(smoke): import via package index (circular-init order), batch ALT extends, wait for ALT warmup --- scripts/devnet-usdc-smoke.ts | 72 ++++++++++++++++++++---------------- 1 file changed, 40 insertions(+), 32 deletions(-) diff --git a/scripts/devnet-usdc-smoke.ts b/scripts/devnet-usdc-smoke.ts index d215379e..b75c63bd 100644 --- a/scripts/devnet-usdc-smoke.ts +++ b/scripts/devnet-usdc-smoke.ts @@ -40,9 +40,10 @@ import { import { TOKEN_PROGRAM_ID } from "@solana/spl-token"; import BN from "bn.js"; -import { OnlinePumpSdk } from "../src/onlineSdk"; -import { PUMP_SDK } from "../src/sdk"; -import { USDC_MINT } from "../src/quoteMints"; +// Import from the package index (not deep paths): the index evaluates `./sdk` +// before `./onlineSdk`, which is the order that avoids the sdk<->onlineSdk +// circular-init ordering issue. This is also how real consumers import. +import { OnlinePumpSdk, PUMP_SDK, USDC_MINT } from "../src"; function loadKeypair(path: string): Keypair { return Keypair.fromSecretKey( @@ -98,41 +99,48 @@ async function buildEphemeralAlt( payer: payer.publicKey, recentSlot, }); - const extendIx = AddressLookupTableProgram.extendLookupTable({ - lookupTable: altAddress, - authority: payer.publicKey, - payer: payer.publicKey, - addresses, - }); - const { blockhash, lastValidBlockHeight } = - await connection.getLatestBlockhash(); - const message = new TransactionMessage({ - payerKey: payer.publicKey, - recentBlockhash: blockhash, - instructions: [createIx, extendIx], - }).compileToV0Message(); - const tx = new VersionedTransaction(message); - tx.sign([payer]); - - const sig = await connection.sendTransaction(tx); - await connection.confirmTransaction( - { signature: sig, blockhash, lastValidBlockHeight }, - "confirmed", - ); - console.log("created ephemeral ALT:", altAddress.toBase58(), "tx:", sig); + const sendIxs = async (instructions: TransactionInstruction[]): Promise => { + const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash(); + const message = new TransactionMessage({ + payerKey: payer.publicKey, + recentBlockhash: blockhash, + instructions, + }).compileToV0Message(); + const tx = new VersionedTransaction(message); + tx.sign([payer]); + const sig = await connection.sendTransaction(tx); + await connection.confirmTransaction({ signature: sig, blockhash, lastValidBlockHeight }, "confirmed"); + }; + + // `extendLookupTable` with many addresses overflows a single tx, so extend in + // batches (~20 addresses/tx). The create + first batch share one tx. + const BATCH = 20; + const extendFor = (addrs: PublicKey[]) => + AddressLookupTableProgram.extendLookupTable({ + lookupTable: altAddress, + authority: payer.publicKey, + payer: payer.publicKey, + addresses: addrs, + }); + await sendIxs([createIx, extendFor(addresses.slice(0, BATCH))]); + for (let i = BATCH; i < addresses.length; i += BATCH) { + await sendIxs([extendFor(addresses.slice(i, i + BATCH))]); + } + console.log("created ephemeral ALT:", altAddress.toBase58()); - // Bound-poll until the ALT resolves and is populated/usable. + // Bound-poll until the ALT resolves and is populated. A freshly-extended table + // can only be referenced starting the slot AFTER its last extension, so once the + // addresses are present we also wait for the slot to advance before returning. for (let attempt = 0; attempt < 30; attempt++) { const fetched = await connection.getAddressLookupTable(altAddress); const account = fetched.value; if (account && account.state.addresses.length >= addresses.length) { - console.log( - "ALT active with", - account.state.addresses.length, - "addresses", - ); - return account; + const warmAt = await connection.getSlot(); + while ((await connection.getSlot()) <= warmAt + 1) await sleep(400); + const ready = (await connection.getAddressLookupTable(altAddress)).value!; + console.log("ALT active with", ready.state.addresses.length, "addresses"); + return ready; } await sleep(800); }