@forgedevv/core 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,202 @@
1
+ /**
2
+ * Priority fees of the transactions built by @forge/core (compute unit price).
3
+ *
4
+ * Under congestion a transaction without a compute unit price is dropped by the leaders long
5
+ * before its blockhash expires; a late payment then becomes a manual anomaly. Every builder that
6
+ * returns an unsigned transaction accepts an optional `priority` setting (`PriorityFeeSetting`):
7
+ * the price is read from the cluster, bounded, and two compute budget instructions are put first
8
+ * (`setComputeUnitLimit`, then `setComputeUnitPrice`; after the nonce advance of a durable nonce
9
+ * transaction).
10
+ *
11
+ * The price, in micro-lamports per compute unit (pure: `computePriorityFee`):
12
+ *
13
+ * p = the `percentile` (nearest rank) of the `prioritizationFee` values returned by
14
+ * `getRecentPrioritizationFees` for the transaction's writable accounts (up to 150
15
+ * recent slots, zeros included);
16
+ * price = min(max(p, floorMicroLamports), maxMicroLamports, floor(maxLamports x 10^6 / limit))
17
+ *
18
+ * so the priority fee of the whole transaction, `ceil(limit x price / 10^6)` lamports, is never
19
+ * above `maxLamports`, whatever the cluster answers. An empty answer, or an RPC error (the relay
20
+ * refusing the method, a timeout), falls back to the floor: the quote says so (`source`), nothing
21
+ * is hidden, and the transaction is still built.
22
+ *
23
+ * The compute unit limit is a per-transaction-kind constant measured on devnet with a margin
24
+ * (e.g. `DEFAULT_SWAP_COMPUTE_UNIT_LIMIT`, `LAUNCH_COIN_COMPUTE_UNITS`, `PAYMENT_COMPUTE_UNITS`):
25
+ * the price applies to the requested limit, not to what the transaction uses.
26
+ *
27
+ * Defaults (`DEFAULT_PRIORITY_FEE_POLICY`), sized for mainnet-beta and harmless on devnet: 75th
28
+ * percentile, floor 10,000 micro-lamports per CU, at most 1,000,000 micro-lamports per CU (1
29
+ * lamport) and at most 500,000 lamports (0.0005 SOL) per transaction. A setting can tighten or
30
+ * relax them within the hard ceilings `MAX_PRIORITY_FEE_MICRO_LAMPORTS` (10 lamports per CU) and
31
+ * `MAX_PRIORITY_FEE_LAMPORTS` (0.01 SOL), the bounds every FORGE verifier accepts.
32
+ *
33
+ * `checkComputeBudgetInstructions` is the matching check for transaction policies: compute
34
+ * budget instructions in their exact form only (no accounts, `SetComputeUnitLimit` or
35
+ * `SetComputeUnitPrice`, at most one of each), with bounded values.
36
+ *
37
+ * Browser-safe: only @solana/web3.js.
38
+ */
39
+ import { PublicKey, type Connection, type TransactionInstruction } from '@solana/web3.js';
40
+ /** Solana's per-transaction compute ceiling (also the limit assumed when none is set). */
41
+ export declare const MAX_TRANSACTION_COMPUTE_UNITS = 1400000;
42
+ /** Hard ceiling of a compute unit price: 10 lamports per CU (`MAX_SWAP_PRIORITY_FEE_MICRO_LAMPORTS`). */
43
+ export declare const MAX_PRIORITY_FEE_MICRO_LAMPORTS = 10000000;
44
+ /** Hard ceiling of the priority fee of one transaction: 0.01 SOL. */
45
+ export declare const MAX_PRIORITY_FEE_LAMPORTS = 10000000n;
46
+ /**
47
+ * Most writable accounts passed to `getRecentPrioritizationFees` (the RPC accepts 128; the FORGE
48
+ * relay accepts this many).
49
+ */
50
+ export declare const MAX_PRIORITY_FEE_ACCOUNTS = 32;
51
+ /** How the price is chosen and bounded. */
52
+ export interface PriorityFeePolicy {
53
+ /** Percentile of the recent fees, an integer from 0 (lowest) to 100 (highest). */
54
+ percentile: number;
55
+ /** Lowest price, micro-lamports per CU (also the price when the cluster gives none). */
56
+ floorMicroLamports: number;
57
+ /** Highest price, micro-lamports per CU. */
58
+ maxMicroLamports: number;
59
+ /** Highest priority fee of the whole transaction, lamports (the cost guard shown to users). */
60
+ maxLamports: bigint;
61
+ }
62
+ /** What a caller passes: any subset of the policy, the rest from `DEFAULT_PRIORITY_FEE_POLICY`. */
63
+ export type PriorityFeeSetting = Partial<PriorityFeePolicy>;
64
+ /** Mainnet-sized defaults (see the top of this file). */
65
+ export declare const DEFAULT_PRIORITY_FEE_POLICY: Readonly<PriorityFeePolicy>;
66
+ /** One entry of `getRecentPrioritizationFees`. */
67
+ export interface RecentPrioritizationFee {
68
+ slot: number;
69
+ /** Micro-lamports per CU paid by the cheapest transaction of the slot writing those accounts. */
70
+ prioritizationFee: number;
71
+ }
72
+ /**
73
+ * Where the price comes from: the recent fees, the floor because the cluster returned no fee, or
74
+ * the floor because the fees could not be read (`rpc_unavailable`).
75
+ */
76
+ export type PriorityFeeSource = 'recent_fees' | 'no_recent_fees' | 'rpc_unavailable' | 'fixed';
77
+ /** The priority fee of one transaction, as the builders return it. */
78
+ export interface PriorityFeeQuote {
79
+ /** Compute unit limit the price applies to (the one set by the transaction). */
80
+ computeUnitLimit: number;
81
+ /** Compute unit price, micro-lamports per CU (0: no `setComputeUnitPrice` instruction). */
82
+ microLamports: number;
83
+ /** Priority fee the transaction pays at most: `ceil(limit x price / 10^6)` lamports. */
84
+ priorityFeeLamports: bigint;
85
+ /** The cost guard: no priority fee of this setting is ever above it (lamports). */
86
+ maxLamports: bigint;
87
+ source: PriorityFeeSource;
88
+ }
89
+ /**
90
+ * The full policy of a setting: defaults filled in, every value checked against the hard
91
+ * ceilings. @throws RangeError on an invalid value.
92
+ */
93
+ export declare function resolvePriorityFeePolicy(setting?: PriorityFeeSetting): PriorityFeePolicy;
94
+ /**
95
+ * Nearest-rank percentile of `values` (0 = lowest, 100 = highest); 0 for an empty list.
96
+ * @throws RangeError on a percentile outside 0..100.
97
+ */
98
+ export declare function feePercentile(values: readonly number[], percentile: number): number;
99
+ /** Highest price whose fee at `computeUnitLimit` stays within `maxLamports`. */
100
+ export declare function maxPriceForBudget(computeUnitLimit: number, maxLamports: bigint): number;
101
+ /**
102
+ * Pure: the price of a transaction of `computeUnitLimit` CU from the recent fees (see the top of
103
+ * this file). `recentFees: null` means the fees could not be read (`rpc_unavailable`); entries
104
+ * that are not non-negative safe integers are ignored.
105
+ *
106
+ * @throws RangeError on an invalid limit or policy.
107
+ */
108
+ export declare function computePriorityFee(input: {
109
+ recentFees: readonly RecentPrioritizationFee[] | null;
110
+ computeUnitLimit: number;
111
+ policy?: PriorityFeeSetting;
112
+ }): PriorityFeeQuote;
113
+ /**
114
+ * The most a transaction of `computeUnitLimit` CU can pay as priority fee under `setting`, in
115
+ * lamports: its fee at the highest price the setting allows. The cost guard to show before the
116
+ * transaction is built (e.g. next to a swap quote).
117
+ */
118
+ export declare function maxPriorityFeeLamports(computeUnitLimit: number, setting?: PriorityFeeSetting): bigint;
119
+ /**
120
+ * A quote for a fixed price (the builders' legacy `computeUnitPriceMicroLamports` /
121
+ * `priorityFeeMicroLamports` parameters): no RPC read, the given price as is.
122
+ */
123
+ export declare function fixedPriorityFee(computeUnitLimit: number, microLamports: number): PriorityFeeQuote;
124
+ /**
125
+ * The compute budget instructions of a quote, to put first: `setComputeUnitLimit`, then
126
+ * `setComputeUnitPrice` when the price is not 0. `priceOnly`: the price alone (no limit), for a
127
+ * transaction too tight for both; its quote must then be computed for
128
+ * `MAX_TRANSACTION_COMPUTE_UNITS`, the limit the runtime may apply.
129
+ */
130
+ export declare function priorityFeeInstructions(quote: Pick<PriorityFeeQuote, 'computeUnitLimit' | 'microLamports'>, options?: {
131
+ priceOnly?: boolean;
132
+ }): TransactionInstruction[];
133
+ /**
134
+ * The writable accounts of `instructions`, without duplicates, in order of appearance (at most
135
+ * `MAX_PRIORITY_FEE_ACCOUNTS`): what the leaders lock, so what the recent fees are read for.
136
+ */
137
+ export declare function writableAccountsOf(instructions: readonly TransactionInstruction[]): PublicKey[];
138
+ /** The connection method the estimate uses (a `Connection` satisfies it). */
139
+ export type PriorityFeeReader = Pick<Connection, 'getRecentPrioritizationFees'>;
140
+ /**
141
+ * Reads the recent prioritization fees for `writableAccounts` (deduplicated, at most
142
+ * `MAX_PRIORITY_FEE_ACCOUNTS`). Throws what the connection throws.
143
+ */
144
+ export declare function readRecentPriorityFees(connection: PriorityFeeReader, writableAccounts: readonly PublicKey[]): Promise<RecentPrioritizationFee[]>;
145
+ export interface EstimatePriorityFeeParams {
146
+ connection: PriorityFeeReader;
147
+ /** The transaction's writable accounts (`writableAccountsOf`). */
148
+ writableAccounts: readonly PublicKey[];
149
+ computeUnitLimit: number;
150
+ setting?: PriorityFeeSetting;
151
+ }
152
+ /**
153
+ * Reads the recent fees and computes the price (`computePriorityFee`). A failed read is not an
154
+ * error: the price is the floor and the quote says `rpc_unavailable`. Invalid settings throw
155
+ * (RangeError) before any network call.
156
+ */
157
+ export declare function estimatePriorityFee(params: EstimatePriorityFeeParams): Promise<PriorityFeeQuote>;
158
+ export type ComputeBudgetCheckCode =
159
+ /** A compute budget instruction with accounts, or of another kind (heap frame, data size...). */
160
+ 'unexpected_instruction'
161
+ /** Two unit limits or two unit prices. */
162
+ | 'duplicate_instruction' | 'limit_out_of_range' | 'price_too_high' | 'fee_too_high';
163
+ /** A compute budget instruction is not in the accepted form or above the bounds. */
164
+ export declare class ComputeBudgetCheckError extends Error {
165
+ readonly code: ComputeBudgetCheckCode;
166
+ constructor(code: ComputeBudgetCheckCode, message: string);
167
+ }
168
+ export interface ComputeBudgetBounds {
169
+ /** Highest unit limit (default `MAX_TRANSACTION_COMPUTE_UNITS`). */
170
+ maxComputeUnits?: number;
171
+ /** Highest unit price, micro-lamports per CU (default `MAX_PRIORITY_FEE_MICRO_LAMPORTS`). */
172
+ maxMicroLamports?: number | bigint;
173
+ /** Highest priority fee of the transaction, lamports (default `MAX_PRIORITY_FEE_LAMPORTS`). */
174
+ maxPriorityFeeLamports?: bigint;
175
+ }
176
+ export interface ComputeBudgetSummary {
177
+ /** The unit limit set, or null when there is none. */
178
+ computeUnitLimit: number | null;
179
+ /** The unit price set, micro-lamports per CU (0 when there is none). */
180
+ microLamports: bigint;
181
+ /**
182
+ * The highest priority fee the transaction can pay, lamports. Without a unit limit it is
183
+ * computed for `MAX_TRANSACTION_COMPUTE_UNITS` (the runtime's default depends on the
184
+ * instructions; this bound holds for any of them).
185
+ */
186
+ priorityFeeLamports: bigint;
187
+ }
188
+ /** What `checkComputeBudgetInstructions` reads of an instruction (a web3.js instruction fits). */
189
+ export interface InspectedInstruction {
190
+ programId: PublicKey;
191
+ keys: readonly unknown[];
192
+ data: Uint8Array;
193
+ }
194
+ /**
195
+ * Checks every Compute Budget instruction of `instructions` (other programs are left to the
196
+ * caller's policy): no account, `SetComputeUnitLimit` (tag 2, u32) or `SetComputeUnitPrice`
197
+ * (tag 3, u64) only, at most one of each, limit within 1..`maxComputeUnits`, price within
198
+ * `maxMicroLamports`, priority fee within `maxPriorityFeeLamports`.
199
+ *
200
+ * @throws ComputeBudgetCheckError with a stable `code`.
201
+ */
202
+ export declare function checkComputeBudgetInstructions(instructions: readonly InspectedInstruction[], bounds?: ComputeBudgetBounds): ComputeBudgetSummary;
@@ -0,0 +1,322 @@
1
+ /**
2
+ * Priority fees of the transactions built by @forge/core (compute unit price).
3
+ *
4
+ * Under congestion a transaction without a compute unit price is dropped by the leaders long
5
+ * before its blockhash expires; a late payment then becomes a manual anomaly. Every builder that
6
+ * returns an unsigned transaction accepts an optional `priority` setting (`PriorityFeeSetting`):
7
+ * the price is read from the cluster, bounded, and two compute budget instructions are put first
8
+ * (`setComputeUnitLimit`, then `setComputeUnitPrice`; after the nonce advance of a durable nonce
9
+ * transaction).
10
+ *
11
+ * The price, in micro-lamports per compute unit (pure: `computePriorityFee`):
12
+ *
13
+ * p = the `percentile` (nearest rank) of the `prioritizationFee` values returned by
14
+ * `getRecentPrioritizationFees` for the transaction's writable accounts (up to 150
15
+ * recent slots, zeros included);
16
+ * price = min(max(p, floorMicroLamports), maxMicroLamports, floor(maxLamports x 10^6 / limit))
17
+ *
18
+ * so the priority fee of the whole transaction, `ceil(limit x price / 10^6)` lamports, is never
19
+ * above `maxLamports`, whatever the cluster answers. An empty answer, or an RPC error (the relay
20
+ * refusing the method, a timeout), falls back to the floor: the quote says so (`source`), nothing
21
+ * is hidden, and the transaction is still built.
22
+ *
23
+ * The compute unit limit is a per-transaction-kind constant measured on devnet with a margin
24
+ * (e.g. `DEFAULT_SWAP_COMPUTE_UNIT_LIMIT`, `LAUNCH_COIN_COMPUTE_UNITS`, `PAYMENT_COMPUTE_UNITS`):
25
+ * the price applies to the requested limit, not to what the transaction uses.
26
+ *
27
+ * Defaults (`DEFAULT_PRIORITY_FEE_POLICY`), sized for mainnet-beta and harmless on devnet: 75th
28
+ * percentile, floor 10,000 micro-lamports per CU, at most 1,000,000 micro-lamports per CU (1
29
+ * lamport) and at most 500,000 lamports (0.0005 SOL) per transaction. A setting can tighten or
30
+ * relax them within the hard ceilings `MAX_PRIORITY_FEE_MICRO_LAMPORTS` (10 lamports per CU) and
31
+ * `MAX_PRIORITY_FEE_LAMPORTS` (0.01 SOL), the bounds every FORGE verifier accepts.
32
+ *
33
+ * `checkComputeBudgetInstructions` is the matching check for transaction policies: compute
34
+ * budget instructions in their exact form only (no accounts, `SetComputeUnitLimit` or
35
+ * `SetComputeUnitPrice`, at most one of each), with bounded values.
36
+ *
37
+ * Browser-safe: only @solana/web3.js.
38
+ */
39
+ import { ComputeBudgetProgram, PublicKey, } from '@solana/web3.js';
40
+ import { computeBudgetInstructions, priorityFeeLamports } from './tx.js';
41
+ // ---------------------------------------------------------------------------------------------
42
+ // Constants
43
+ // ---------------------------------------------------------------------------------------------
44
+ /** Solana's per-transaction compute ceiling (also the limit assumed when none is set). */
45
+ export const MAX_TRANSACTION_COMPUTE_UNITS = 1_400_000;
46
+ /** Hard ceiling of a compute unit price: 10 lamports per CU (`MAX_SWAP_PRIORITY_FEE_MICRO_LAMPORTS`). */
47
+ export const MAX_PRIORITY_FEE_MICRO_LAMPORTS = 10_000_000;
48
+ /** Hard ceiling of the priority fee of one transaction: 0.01 SOL. */
49
+ export const MAX_PRIORITY_FEE_LAMPORTS = 10000000n;
50
+ /**
51
+ * Most writable accounts passed to `getRecentPrioritizationFees` (the RPC accepts 128; the FORGE
52
+ * relay accepts this many).
53
+ */
54
+ export const MAX_PRIORITY_FEE_ACCOUNTS = 32;
55
+ /** Mainnet-sized defaults (see the top of this file). */
56
+ export const DEFAULT_PRIORITY_FEE_POLICY = Object.freeze({
57
+ percentile: 75,
58
+ floorMicroLamports: 10_000,
59
+ maxMicroLamports: 1_000_000,
60
+ maxLamports: 500000n,
61
+ });
62
+ // ---------------------------------------------------------------------------------------------
63
+ // Pure computation
64
+ // ---------------------------------------------------------------------------------------------
65
+ function assertInteger(name, value, min, max) {
66
+ if (typeof value !== 'number' || !Number.isInteger(value) || value < min || value > max) {
67
+ throw new RangeError(`${name} must be an integer between ${min} and ${max}`);
68
+ }
69
+ return value;
70
+ }
71
+ /**
72
+ * The full policy of a setting: defaults filled in, every value checked against the hard
73
+ * ceilings. @throws RangeError on an invalid value.
74
+ */
75
+ export function resolvePriorityFeePolicy(setting = {}) {
76
+ if (setting === null || typeof setting !== 'object') {
77
+ throw new RangeError('the priority fee setting must be an object');
78
+ }
79
+ const percentile = assertInteger('percentile', setting.percentile ?? DEFAULT_PRIORITY_FEE_POLICY.percentile, 0, 100);
80
+ const maxMicroLamports = assertInteger('maxMicroLamports', setting.maxMicroLamports ?? DEFAULT_PRIORITY_FEE_POLICY.maxMicroLamports, 0, MAX_PRIORITY_FEE_MICRO_LAMPORTS);
81
+ const floorMicroLamports = assertInteger('floorMicroLamports', setting.floorMicroLamports ??
82
+ Math.min(DEFAULT_PRIORITY_FEE_POLICY.floorMicroLamports, maxMicroLamports), 0, maxMicroLamports);
83
+ const maxLamports = setting.maxLamports ?? DEFAULT_PRIORITY_FEE_POLICY.maxLamports;
84
+ if (typeof maxLamports !== 'bigint' ||
85
+ maxLamports < 0n ||
86
+ maxLamports > MAX_PRIORITY_FEE_LAMPORTS) {
87
+ throw new RangeError(`maxLamports must be a bigint between 0 and ${MAX_PRIORITY_FEE_LAMPORTS} lamports`);
88
+ }
89
+ return { percentile, floorMicroLamports, maxMicroLamports, maxLamports };
90
+ }
91
+ /**
92
+ * Nearest-rank percentile of `values` (0 = lowest, 100 = highest); 0 for an empty list.
93
+ * @throws RangeError on a percentile outside 0..100.
94
+ */
95
+ export function feePercentile(values, percentile) {
96
+ assertInteger('percentile', percentile, 0, 100);
97
+ if (values.length === 0)
98
+ return 0;
99
+ const sorted = [...values].sort((a, b) => a - b);
100
+ const rank = Math.ceil((percentile / 100) * sorted.length);
101
+ return sorted[Math.min(Math.max(rank, 1), sorted.length) - 1];
102
+ }
103
+ /** Highest price whose fee at `computeUnitLimit` stays within `maxLamports`. */
104
+ export function maxPriceForBudget(computeUnitLimit, maxLamports) {
105
+ assertInteger('computeUnitLimit', computeUnitLimit, 1, MAX_TRANSACTION_COMPUTE_UNITS);
106
+ const price = (maxLamports * 1000000n) / BigInt(computeUnitLimit);
107
+ return price > BigInt(MAX_PRIORITY_FEE_MICRO_LAMPORTS)
108
+ ? MAX_PRIORITY_FEE_MICRO_LAMPORTS
109
+ : Number(price);
110
+ }
111
+ /**
112
+ * Pure: the price of a transaction of `computeUnitLimit` CU from the recent fees (see the top of
113
+ * this file). `recentFees: null` means the fees could not be read (`rpc_unavailable`); entries
114
+ * that are not non-negative safe integers are ignored.
115
+ *
116
+ * @throws RangeError on an invalid limit or policy.
117
+ */
118
+ export function computePriorityFee(input) {
119
+ const policy = resolvePriorityFeePolicy(input.policy);
120
+ const units = assertInteger('computeUnitLimit', input.computeUnitLimit, 1, MAX_TRANSACTION_COMPUTE_UNITS);
121
+ const values = input.recentFees === null
122
+ ? []
123
+ : input.recentFees
124
+ .map((entry) => entry?.prioritizationFee)
125
+ .filter((fee) => Number.isSafeInteger(fee) && fee >= 0);
126
+ const source = input.recentFees === null
127
+ ? 'rpc_unavailable'
128
+ : values.length > 0
129
+ ? 'recent_fees'
130
+ : 'no_recent_fees';
131
+ let price = Math.max(feePercentile(values, policy.percentile), policy.floorMicroLamports);
132
+ price = Math.min(price, policy.maxMicroLamports, maxPriceForBudget(units, policy.maxLamports));
133
+ return {
134
+ computeUnitLimit: units,
135
+ microLamports: price,
136
+ priorityFeeLamports: priorityFeeLamports({ units, microLamports: price }),
137
+ maxLamports: policy.maxLamports,
138
+ source,
139
+ };
140
+ }
141
+ /**
142
+ * The most a transaction of `computeUnitLimit` CU can pay as priority fee under `setting`, in
143
+ * lamports: its fee at the highest price the setting allows. The cost guard to show before the
144
+ * transaction is built (e.g. next to a swap quote).
145
+ */
146
+ export function maxPriorityFeeLamports(computeUnitLimit, setting) {
147
+ return computePriorityFee({
148
+ recentFees: [{ slot: 0, prioritizationFee: MAX_PRIORITY_FEE_MICRO_LAMPORTS }],
149
+ computeUnitLimit,
150
+ ...(setting ? { policy: setting } : {}),
151
+ }).priorityFeeLamports;
152
+ }
153
+ /**
154
+ * A quote for a fixed price (the builders' legacy `computeUnitPriceMicroLamports` /
155
+ * `priorityFeeMicroLamports` parameters): no RPC read, the given price as is.
156
+ */
157
+ export function fixedPriorityFee(computeUnitLimit, microLamports) {
158
+ assertInteger('computeUnitLimit', computeUnitLimit, 1, MAX_TRANSACTION_COMPUTE_UNITS);
159
+ assertInteger('microLamports', microLamports, 0, Number.MAX_SAFE_INTEGER);
160
+ const fee = priorityFeeLamports({ units: computeUnitLimit, microLamports });
161
+ return {
162
+ computeUnitLimit,
163
+ microLamports,
164
+ priorityFeeLamports: fee,
165
+ maxLamports: fee,
166
+ source: 'fixed',
167
+ };
168
+ }
169
+ /**
170
+ * The compute budget instructions of a quote, to put first: `setComputeUnitLimit`, then
171
+ * `setComputeUnitPrice` when the price is not 0. `priceOnly`: the price alone (no limit), for a
172
+ * transaction too tight for both; its quote must then be computed for
173
+ * `MAX_TRANSACTION_COMPUTE_UNITS`, the limit the runtime may apply.
174
+ */
175
+ export function priorityFeeInstructions(quote, options = {}) {
176
+ if (!options.priceOnly) {
177
+ return computeBudgetInstructions({
178
+ units: quote.computeUnitLimit,
179
+ microLamports: quote.microLamports,
180
+ });
181
+ }
182
+ assertInteger('microLamports', quote.microLamports, 0, MAX_PRIORITY_FEE_MICRO_LAMPORTS);
183
+ return quote.microLamports > 0
184
+ ? [ComputeBudgetProgram.setComputeUnitPrice({ microLamports: quote.microLamports })]
185
+ : [];
186
+ }
187
+ /**
188
+ * The writable accounts of `instructions`, without duplicates, in order of appearance (at most
189
+ * `MAX_PRIORITY_FEE_ACCOUNTS`): what the leaders lock, so what the recent fees are read for.
190
+ */
191
+ export function writableAccountsOf(instructions) {
192
+ const seen = new Set();
193
+ const out = [];
194
+ for (const ix of instructions) {
195
+ for (const meta of ix.keys) {
196
+ if (!meta.isWritable)
197
+ continue;
198
+ const id = meta.pubkey.toBase58();
199
+ if (seen.has(id))
200
+ continue;
201
+ seen.add(id);
202
+ out.push(meta.pubkey);
203
+ if (out.length === MAX_PRIORITY_FEE_ACCOUNTS)
204
+ return out;
205
+ }
206
+ }
207
+ return out;
208
+ }
209
+ /**
210
+ * Reads the recent prioritization fees for `writableAccounts` (deduplicated, at most
211
+ * `MAX_PRIORITY_FEE_ACCOUNTS`). Throws what the connection throws.
212
+ */
213
+ export async function readRecentPriorityFees(connection, writableAccounts) {
214
+ const seen = new Set();
215
+ const accounts = [];
216
+ for (const account of writableAccounts) {
217
+ if (!(account instanceof PublicKey))
218
+ throw new TypeError('writable accounts must be PublicKeys');
219
+ const id = account.toBase58();
220
+ if (seen.has(id))
221
+ continue;
222
+ seen.add(id);
223
+ accounts.push(account);
224
+ if (accounts.length === MAX_PRIORITY_FEE_ACCOUNTS)
225
+ break;
226
+ }
227
+ const fees = await connection.getRecentPrioritizationFees(accounts.length > 0 ? { lockedWritableAccounts: accounts } : undefined);
228
+ if (!Array.isArray(fees))
229
+ throw new TypeError('getRecentPrioritizationFees did not return a list');
230
+ return fees;
231
+ }
232
+ /**
233
+ * Reads the recent fees and computes the price (`computePriorityFee`). A failed read is not an
234
+ * error: the price is the floor and the quote says `rpc_unavailable`. Invalid settings throw
235
+ * (RangeError) before any network call.
236
+ */
237
+ export async function estimatePriorityFee(params) {
238
+ // Validate first: a bad setting never reaches the network.
239
+ computePriorityFee({
240
+ recentFees: [],
241
+ computeUnitLimit: params.computeUnitLimit,
242
+ ...(params.setting ? { policy: params.setting } : {}),
243
+ });
244
+ let recentFees;
245
+ try {
246
+ recentFees = await readRecentPriorityFees(params.connection, params.writableAccounts);
247
+ }
248
+ catch {
249
+ // Degraded, reported in the quote (`source`): the floor still gives the transaction a price.
250
+ recentFees = null;
251
+ }
252
+ return computePriorityFee({
253
+ recentFees,
254
+ computeUnitLimit: params.computeUnitLimit,
255
+ ...(params.setting ? { policy: params.setting } : {}),
256
+ });
257
+ }
258
+ /** A compute budget instruction is not in the accepted form or above the bounds. */
259
+ export class ComputeBudgetCheckError extends Error {
260
+ code;
261
+ constructor(code, message) {
262
+ super(message);
263
+ this.name = 'ComputeBudgetCheckError';
264
+ this.code = code;
265
+ }
266
+ }
267
+ function readU32(data, offset) {
268
+ return new DataView(data.buffer, data.byteOffset, data.byteLength).getUint32(offset, true);
269
+ }
270
+ function readU64(data, offset) {
271
+ return new DataView(data.buffer, data.byteOffset, data.byteLength).getBigUint64(offset, true);
272
+ }
273
+ /**
274
+ * Checks every Compute Budget instruction of `instructions` (other programs are left to the
275
+ * caller's policy): no account, `SetComputeUnitLimit` (tag 2, u32) or `SetComputeUnitPrice`
276
+ * (tag 3, u64) only, at most one of each, limit within 1..`maxComputeUnits`, price within
277
+ * `maxMicroLamports`, priority fee within `maxPriorityFeeLamports`.
278
+ *
279
+ * @throws ComputeBudgetCheckError with a stable `code`.
280
+ */
281
+ export function checkComputeBudgetInstructions(instructions, bounds = {}) {
282
+ const maxUnits = bounds.maxComputeUnits ?? MAX_TRANSACTION_COMPUTE_UNITS;
283
+ const maxPrice = BigInt(bounds.maxMicroLamports ?? MAX_PRIORITY_FEE_MICRO_LAMPORTS);
284
+ const maxFee = bounds.maxPriorityFeeLamports ?? MAX_PRIORITY_FEE_LAMPORTS;
285
+ let units = null;
286
+ let price = null;
287
+ for (const ix of instructions) {
288
+ if (!ix.programId.equals(ComputeBudgetProgram.programId))
289
+ continue;
290
+ const data = ix.data;
291
+ if (ix.keys.length !== 0) {
292
+ throw new ComputeBudgetCheckError('unexpected_instruction', 'compute budget instruction with accounts');
293
+ }
294
+ if (data[0] === 2 && data.length === 5) {
295
+ if (units !== null) {
296
+ throw new ComputeBudgetCheckError('duplicate_instruction', 'duplicate compute unit limit');
297
+ }
298
+ units = readU32(data, 1);
299
+ if (units < 1 || units > maxUnits) {
300
+ throw new ComputeBudgetCheckError('limit_out_of_range', 'compute unit limit out of range');
301
+ }
302
+ }
303
+ else if (data[0] === 3 && data.length === 9) {
304
+ if (price !== null) {
305
+ throw new ComputeBudgetCheckError('duplicate_instruction', 'duplicate compute unit price');
306
+ }
307
+ price = readU64(data, 1);
308
+ if (price > maxPrice) {
309
+ throw new ComputeBudgetCheckError('price_too_high', 'compute unit price too high');
310
+ }
311
+ }
312
+ else {
313
+ throw new ComputeBudgetCheckError('unexpected_instruction', 'compute budget instruction other than a unit limit or a unit price');
314
+ }
315
+ }
316
+ const microLamports = price ?? 0n;
317
+ const budget = BigInt(units ?? MAX_TRANSACTION_COMPUTE_UNITS);
318
+ const fee = (budget * microLamports + 999999n) / 1000000n;
319
+ if (fee > maxFee)
320
+ throw new ComputeBudgetCheckError('fee_too_high', 'priority fee too high');
321
+ return { computeUnitLimit: units, microLamports, priorityFeeLamports: fee };
322
+ }
package/dist/reads.d.ts CHANGED
@@ -160,6 +160,26 @@ export declare function readCoin(connection: Connection, pool: PublicKey, option
160
160
  * @throws DbcReadError when the config does not exist.
161
161
  */
162
162
  export declare function listCoinsOfConfig(connection: Connection, config: PublicKey, options?: ReadOptions): Promise<CoinOfConfig[]>;
163
+ /**
164
+ * The decoded pools of a config with the config itself (what `listCoinsOfConfig` summarizes):
165
+ * one `getPoolsByConfig` scan (one `getProgramAccounts` per pool account variant, filtered on the
166
+ * config at offset 72) and one config read. A pool whose decoded `config` is not `config` is
167
+ * dropped (a wrong RPC answer is never shown). Sorted by pool address.
168
+ *
169
+ * The scan returns every pool of the config: callers should cache it (the FORGE RPC relay serves
170
+ * it to server-token callers only, with a response size cap).
171
+ * @throws DbcReadError when the config does not exist.
172
+ */
173
+ export declare function listPoolStatesOfConfig(connection: Connection, config: PublicKey, options?: ReadOptions): Promise<DbcPoolState[]>;
174
+ /**
175
+ * Pure: spot price of one whole base token (the coin) in whole quote tokens (SOL for FORGE pools),
176
+ * from the pool's `sqrtPrice`: `sqrtPrice^2 / 2^128` quote units per base unit (the price the
177
+ * curve is at, before fees), times `10^(baseDecimals - quoteDecimals)`. A float, for display only:
178
+ * quotes and transactions use the exact bigint math of `quoteSwap`.
179
+ */
180
+ export declare function poolSpotPrice(sqrtPrice: {
181
+ toString(): string;
182
+ } | bigint, baseDecimals: number, quoteDecimals?: number): number;
163
183
  /**
164
184
  * Byte offset of `creator` in a DBC pool account (`VirtualPool` and `TransferHookPool`: 8-byte
165
185
  * account discriminator, then `PoolState` with `config` at 72, `creator` at 104, `baseMint` at 136).
package/dist/reads.js CHANGED
@@ -165,6 +165,19 @@ export async function readCoin(connection, pool, options = {}) {
165
165
  * @throws DbcReadError when the config does not exist.
166
166
  */
167
167
  export async function listCoinsOfConfig(connection, config, options = {}) {
168
+ return (await listPoolStatesOfConfig(connection, config, options)).map(summarizeCoin);
169
+ }
170
+ /**
171
+ * The decoded pools of a config with the config itself (what `listCoinsOfConfig` summarizes):
172
+ * one `getPoolsByConfig` scan (one `getProgramAccounts` per pool account variant, filtered on the
173
+ * config at offset 72) and one config read. A pool whose decoded `config` is not `config` is
174
+ * dropped (a wrong RPC answer is never shown). Sorted by pool address.
175
+ *
176
+ * The scan returns every pool of the config: callers should cache it (the FORGE RPC relay serves
177
+ * it to server-token callers only, with a response size cap).
178
+ * @throws DbcReadError when the config does not exist.
179
+ */
180
+ export async function listPoolStatesOfConfig(connection, config, options = {}) {
168
181
  const state = stateService(connection, options.commitment ?? READ_COMMITMENT);
169
182
  const configState = await retryRpc(() => state.getPoolConfig(config), options.retry);
170
183
  if (!configState) {
@@ -173,8 +186,32 @@ export async function listCoinsOfConfig(connection, config, options = {}) {
173
186
  const pools = await retryRpc(() => state.getPoolsByConfig(config), options.retry);
174
187
  return pools
175
188
  .filter((p) => p.account.poolState.config.equals(config))
176
- .map((p) => summarizeCoin({ address: p.publicKey, virtualPool: p.account, config: configState }))
177
- .sort((a, b) => compareStrings(a.pool.toBase58(), b.pool.toBase58()));
189
+ .map((p) => ({ address: p.publicKey, virtualPool: p.account, config: configState }))
190
+ .sort((a, b) => compareStrings(a.address.toBase58(), b.address.toBase58()));
191
+ }
192
+ /** 2^128: a pool's `sqrtPrice` is a Q64.64 number, so its square is a Q128.128 price. */
193
+ const Q128 = 1n << 128n;
194
+ /** Fixed-point scale used to turn the bigint price into a float without losing small prices. */
195
+ const SPOT_PRICE_SCALE = 10n ** 18n;
196
+ /**
197
+ * Pure: spot price of one whole base token (the coin) in whole quote tokens (SOL for FORGE pools),
198
+ * from the pool's `sqrtPrice`: `sqrtPrice^2 / 2^128` quote units per base unit (the price the
199
+ * curve is at, before fees), times `10^(baseDecimals - quoteDecimals)`. A float, for display only:
200
+ * quotes and transactions use the exact bigint math of `quoteSwap`.
201
+ */
202
+ export function poolSpotPrice(sqrtPrice, baseDecimals, quoteDecimals = 9) {
203
+ const sqrt = toBigint(sqrtPrice);
204
+ if (sqrt <= 0n)
205
+ return 0;
206
+ const shift = baseDecimals - quoteDecimals;
207
+ let numerator = sqrt * sqrt * SPOT_PRICE_SCALE;
208
+ let denominator = Q128;
209
+ if (shift >= 0)
210
+ numerator *= 10n ** BigInt(shift);
211
+ else
212
+ denominator *= 10n ** BigInt(-shift);
213
+ const scaled = numerator / denominator;
214
+ return Number(scaled) / Number(SPOT_PRICE_SCALE);
178
215
  }
179
216
  /**
180
217
  * Byte offset of `creator` in a DBC pool account (`VirtualPool` and `TransferHookPool`: 8-byte