Skip to content

Commit 17acd89

Browse files
committed
chore: autosave checkpoint
1 parent bd29960 commit 17acd89

5 files changed

Lines changed: 919 additions & 18 deletions

File tree

examples/24-decode-fee-config.ts

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
/**
2+
* Example 24: Decoding the Fee Config
3+
*
4+
* Category: Accounts & Events
5+
*
6+
* Fetches the fee program's config account, decodes it with
7+
* decodeFeeConfig, and walks its FeeTier list into the ladder a trade is
8+
* actually priced against. Also shows what the flat fees are for, and what
9+
* happens to a quote when the tier list has a single entry.
10+
*
11+
* Run: npm run example 24
12+
*/
13+
import { PUMP_FEE_CONFIG_PDA, PUMP_SDK } from "@nirholas/pump-sdk";
14+
import BN from "bn.js";
15+
16+
import type { FeeConfig, Fees } from "@nirholas/pump-sdk";
17+
18+
import { getConnection } from "./_lib/connection";
19+
import { formatSol, heading, row } from "./_lib/format";
20+
21+
export interface TierBand {
22+
index: number;
23+
/** Lowest market cap, in lamports, that selects this tier. */
24+
fromMarketCap: BN;
25+
/** Highest market cap this tier covers, or null when it is the top band. */
26+
toMarketCap: BN | null;
27+
protocolFeeBps: BN;
28+
creatorFeeBps: BN;
29+
lpFeeBps: BN;
30+
totalBps: BN;
31+
}
32+
33+
export interface FeeConfigReport {
34+
admin: string;
35+
tierCount: number;
36+
/** Contiguous bands, derived by pairing each threshold with the next. */
37+
bands: TierBand[];
38+
/** Rate the lowest band charges, which also covers caps below its floor. */
39+
entryFees: Fees;
40+
/** Rate the highest band charges. */
41+
topFees: Fees;
42+
/** True when every tier charges the same, so cap does not change the rate. */
43+
flatInPractice: boolean;
44+
/** The config's own flatFees field. */
45+
flatFees: Fees;
46+
/** True when the tier list is ordered by ascending threshold, as required. */
47+
thresholdsAscending: boolean;
48+
}
49+
50+
function totalBps(fees: Fees): BN {
51+
return fees.protocolFeeBps.add(fees.creatorFeeBps).add(fees.lpFeeBps);
52+
}
53+
54+
/**
55+
* Turn a fee config into the ladder a caller can reason about.
56+
*
57+
* Tiers are stored as (threshold, fees) pairs with no upper bound, so the
58+
* band a tier covers is implicit: it runs to the next tier's threshold. The
59+
* lowest tier is special, and it is where integrations go wrong: a market
60+
* cap below its threshold does not escape fees, it falls back to that same
61+
* lowest tier. Ordering matters too, because the tier search walks the list
62+
* from the top, so an unsorted list would return the wrong rate.
63+
*/
64+
export function interpretFeeConfig(feeConfig: FeeConfig): FeeConfigReport {
65+
const tiers = feeConfig.feeTiers;
66+
const bands: TierBand[] = tiers.map((tier, index) => {
67+
const next = tiers[index + 1];
68+
return {
69+
index,
70+
fromMarketCap: tier.marketCapLamportsThreshold,
71+
toMarketCap: next ? next.marketCapLamportsThreshold : null,
72+
protocolFeeBps: tier.fees.protocolFeeBps,
73+
creatorFeeBps: tier.fees.creatorFeeBps,
74+
lpFeeBps: tier.fees.lpFeeBps,
75+
totalBps: totalBps(tier.fees),
76+
};
77+
});
78+
79+
const first = tiers[0];
80+
const last = tiers[tiers.length - 1];
81+
if (!first || !last) {
82+
throw new Error("Fee config has no tiers; every quote would fail");
83+
}
84+
85+
let thresholdsAscending = true;
86+
for (let i = 1; i < tiers.length; i += 1) {
87+
if (
88+
tiers[i]!.marketCapLamportsThreshold.lt(
89+
tiers[i - 1]!.marketCapLamportsThreshold,
90+
)
91+
) {
92+
thresholdsAscending = false;
93+
}
94+
}
95+
96+
return {
97+
admin: feeConfig.admin.toBase58(),
98+
tierCount: tiers.length,
99+
bands,
100+
entryFees: first.fees,
101+
topFees: last.fees,
102+
flatInPractice: bands.every((band) =>
103+
band.totalBps.eq(bands[0]!.totalBps),
104+
),
105+
flatFees: feeConfig.flatFees,
106+
thresholdsAscending,
107+
};
108+
}
109+
110+
function bandLabel(band: TierBand): string {
111+
const from = formatSol(band.fromMarketCap, 0);
112+
return band.toMarketCap === null
113+
? `${from} and above`
114+
: `${from} to ${formatSol(band.toMarketCap, 0)}`;
115+
}
116+
117+
export async function main(): Promise<void> {
118+
const connection = getConnection();
119+
120+
heading("Fetching the fee config");
121+
row("Address", PUMP_FEE_CONFIG_PDA.toBase58());
122+
const accountInfo = await connection.getAccountInfo(PUMP_FEE_CONFIG_PDA);
123+
if (!accountInfo) {
124+
throw new Error(
125+
`No account at ${PUMP_FEE_CONFIG_PDA.toBase58()}. Check the RPC endpoint (PUMP_RPC_URL) is mainnet.`,
126+
);
127+
}
128+
row("Owner", accountInfo.owner.toBase58());
129+
row("Data size", `${accountInfo.data.length} bytes`);
130+
console.log("\nThe account is allocated for a long tier list. The vector length is");
131+
console.log("stored with the data, so the decoder returns only the live tiers and");
132+
console.log("the slack costs nothing to read.");
133+
134+
const feeConfig = PUMP_SDK.decodeFeeConfig(accountInfo);
135+
const report = interpretFeeConfig(feeConfig);
136+
137+
heading("Config");
138+
row("Admin", report.admin);
139+
row("Tiers", report.tierCount);
140+
row("Thresholds ascending", report.thresholdsAscending);
141+
142+
heading("Tier ladder");
143+
for (const band of report.bands) {
144+
row(`[${band.index}] ${bandLabel(band)}`, `${band.totalBps.toString()} bps all-in`);
145+
row(
146+
" protocol / creator / lp",
147+
`${band.protocolFeeBps.toString()} / ${band.creatorFeeBps.toString()} / ${band.lpFeeBps.toString()} bps`,
148+
);
149+
}
150+
if (report.flatInPractice) {
151+
console.log("\nEvery live tier charges the same all-in rate, so on-chain pricing");
152+
console.log("is currently cap-independent. The tier machinery is still active;");
153+
console.log("adding a tier changes rates with no client update.");
154+
} else {
155+
console.log("\nRates step down as cap grows, so the same trade costs less on a");
156+
console.log("larger token. A quote must therefore be computed against the curve's");
157+
console.log("current cap, never cached across price moves.");
158+
}
159+
160+
heading("Below the lowest threshold");
161+
const entryTotal = totalBps(report.entryFees);
162+
row("Lowest threshold", formatSol(report.bands[0]!.fromMarketCap, 0));
163+
row("Rate applied below it", `${entryTotal.toString()} bps`);
164+
console.log("\ncalculateFeeTier returns the first tier for any cap under its");
165+
console.log("threshold. There is no zero-fee region, and no error.");
166+
167+
heading("Flat fees");
168+
row("Protocol", `${feeConfig.flatFees.protocolFeeBps.toString()} bps`);
169+
row("Creator", `${feeConfig.flatFees.creatorFeeBps.toString()} bps`);
170+
row("LP", `${feeConfig.flatFees.lpFeeBps.toString()} bps`);
171+
console.log("\nflatFees is a separate field the fee program uses for flows that");
172+
console.log("are not priced off a bonding curve market cap. Bonding curve quotes");
173+
console.log("go through the tier list; passing feeConfig: null instead falls back");
174+
console.log("to the Global account's own rates, not to this field.");
175+
176+
heading("Using it");
177+
console.log("Fetch this account once per quote cycle and pass it into");
178+
console.log("getBuyTokenAmountFromSolAmount and getSellSolAmountFromTokenAmount.");
179+
console.log("Example 17 walks the tier selection itself; example 22 covers the");
180+
console.log("Global fallback rates.");
181+
}
182+
183+
if (require.main === module) {
184+
main().catch((err) => {
185+
console.error(err);
186+
process.exit(1);
187+
});
188+
}

examples/__tests__/09-10-42-50.test.ts

Lines changed: 55 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -39,44 +39,82 @@ import {
3939
buildMayhemPair,
4040
diffInstructions,
4141
mayhemAccounts,
42+
main as example09,
4243
} from "../09-mayhem-mode";
4344
import {
4445
cashbackAccumulators,
4546
changedDataOffsets,
4647
encodeClaimCashbackEvent,
4748
readCashbackPosition,
49+
main as example10,
4850
} from "../10-cashback-token";
49-
import { interpretAmmSellQuote, roundTripLossBps } from "../42-amm-sell";
50-
import { depositRatio, slippageHeadroomBps } from "../43-amm-deposit";
51-
import { slippageFloorBps, withdrawShare } from "../44-amm-withdraw";
52-
import { compareVenuePrices, spotPriceLamports } from "../45-canonical-pool";
51+
import {
52+
interpretAmmSellQuote,
53+
main as example42,
54+
roundTripLossBps,
55+
} from "../42-amm-sell";
56+
import {
57+
depositRatio,
58+
main as example43,
59+
slippageHeadroomBps,
60+
} from "../43-amm-deposit";
61+
import {
62+
main as example44,
63+
slippageFloorBps,
64+
withdrawShare,
65+
} from "../44-amm-withdraw";
66+
import {
67+
compareVenuePrices,
68+
main as example45,
69+
spotPriceLamports,
70+
} from "../45-canonical-pool";
5371
import {
5472
evenSplit,
5573
evenSplitUnchecked,
5674
invalidSplits,
5775
splitTotalBps,
76+
main as example46,
5877
} from "../46-fee-sharing-create";
5978
import {
6079
encodeDistributeCreatorFeesEvent,
6180
encodeMinimumDistributableFeeEvent,
6281
payoutSplit,
82+
main as example47,
6383
} from "../47-fee-sharing-distribute";
6484
import {
6585
CLAIM_WORTH_IT_LAMPORTS,
6686
splitVaultBalances,
6787
worthClaiming,
88+
main as example48,
6889
} from "../48-creator-fees";
6990
import {
7091
incentiveWindow,
7192
projectedDayShare,
7293
remainingProgramSupply,
94+
main as example49,
7395
} from "../49-token-incentives";
7496
import {
7597
estimateSeconds,
7698
matchesVanityPattern,
7799
unmatchableCharacters,
100+
main as example50,
78101
} from "../50-vanity-mint";
79102

103+
/** Every example's entry point, gathered so the walkthroughs stay runnable. */
104+
const exampleMains = {
105+
example09,
106+
example10,
107+
example42,
108+
example43,
109+
example44,
110+
example45,
111+
example46,
112+
example47,
113+
example48,
114+
example49,
115+
example50,
116+
};
117+
80118
const SOL = (n: number) => new BN(n).mul(new BN(1_000_000_000));
81119
const TOKENS = (n: number) => new BN(n).mul(new BN(1_000_000));
82120

@@ -865,18 +903,18 @@ describe("example 50: vanity mints", () => {
865903

866904
describe("every example exports a runnable main", () => {
867905
it.each([
868-
["09", require("../09-mayhem-mode")],
869-
["10", require("../10-cashback-token")],
870-
["42", require("../42-amm-sell")],
871-
["43", require("../43-amm-deposit")],
872-
["44", require("../44-amm-withdraw")],
873-
["45", require("../45-canonical-pool")],
874-
["46", require("../46-fee-sharing-create")],
875-
["47", require("../47-fee-sharing-distribute")],
876-
["48", require("../48-creator-fees")],
877-
["49", require("../49-token-incentives")],
878-
["50", require("../50-vanity-mint")],
879-
])("example %s", (_n, mod: { main?: unknown }) => {
880-
expect(typeof mod.main).toBe("function");
906+
["09", exampleMains.example09],
907+
["10", exampleMains.example10],
908+
["42", exampleMains.example42],
909+
["43", exampleMains.example43],
910+
["44", exampleMains.example44],
911+
["45", exampleMains.example45],
912+
["46", exampleMains.example46],
913+
["47", exampleMains.example47],
914+
["48", exampleMains.example48],
915+
["49", exampleMains.example49],
916+
["50", exampleMains.example50],
917+
])("example %s", (_n, main: unknown) => {
918+
expect(typeof main).toBe("function");
881919
});
882920
});

0 commit comments

Comments
 (0)