@bordrless/shared 0.0.0-stage → 0.1.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.
- package/LICENSE +202 -0
- package/README.md +33 -2
- package/dist/api.d.ts +657 -0
- package/dist/api.js +6 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/policy.d.ts +622 -0
- package/dist/policy.js +975 -0
- package/dist/programs.d.ts +112 -0
- package/dist/programs.js +140 -0
- package/package.json +44 -3
package/dist/policy.js
ADDED
|
@@ -0,0 +1,975 @@
|
|
|
1
|
+
import { HOOK_DATA_LEN, TOKEN_HOOK_FLAGS } from "./programs.js";
|
|
2
|
+
export const BPS = 10000n;
|
|
3
|
+
export const WAD = 10n ** 18n;
|
|
4
|
+
/** The largest u64, where the programs' checked arithmetic stops. */
|
|
5
|
+
export const U64_MAX = (1n << 64n) - 1n;
|
|
6
|
+
/** The largest u128 (the kit's `acc_per_share` and `rem`). */
|
|
7
|
+
export const U128_MAX = (1n << 128n) - 1n;
|
|
8
|
+
/** Decimals of a launched token. */
|
|
9
|
+
export const TOKEN_DECIMALS = 6;
|
|
10
|
+
/** Supply of a launched token, base units: one billion whole tokens. */
|
|
11
|
+
export const TOKEN_SUPPLY = 1000000000n * 1000000n;
|
|
12
|
+
/** Share of the supply sold on the curve; the rest is reserved for graduation. */
|
|
13
|
+
export const CURVE_BPS = 7500n;
|
|
14
|
+
/** LP fee of a launch pool, which stays in the pool for ever. */
|
|
15
|
+
export const LP_FEE_BPS = 30;
|
|
16
|
+
/** Protocol fee of an ordinary DEX pool (anyone's pool), in basis points of the quote side: the flat model (§3.1). */
|
|
17
|
+
export const PROTOCOL_FEE_BPS = 100;
|
|
18
|
+
/**
|
|
19
|
+
* Bordrless's take on a launch pool (its curve and the same pool after graduation), the share
|
|
20
|
+
* model of §3.1 (2026-10-07: "we earn 25% of their total fees, not 0.25%"): a quarter of what the
|
|
21
|
+
* launch's rules collect on each swap (the creator fee, holder rewards and any cut a creator's own
|
|
22
|
+
* hook takes), in SOL, rounded up, never more than the cuts. Burns and the pool's LP fee are not
|
|
23
|
+
* collected by anyone and are not shared. A launch whose rules collect nothing pays Bordrless
|
|
24
|
+
* nothing. Every launch-side figure uses this one; the flat rate above is for ordinary pools.
|
|
25
|
+
*/
|
|
26
|
+
export const LAUNCH_PROTOCOL_SHARE_BPS = 2_500;
|
|
27
|
+
/** The largest share the DEX config may set (`MAX_PROTOCOL_SHARE_BPS`): all of the cuts. */
|
|
28
|
+
export const MAX_PROTOCOL_SHARE_BPS = 10_000;
|
|
29
|
+
/** Largest creator fee a launch may choose. */
|
|
30
|
+
export const MAX_CREATOR_FEE_BPS = 200;
|
|
31
|
+
/** The creator fee choices offered on the launch form. */
|
|
32
|
+
export const CREATOR_FEE_CHOICES_BPS = [0, 50, 100, 200];
|
|
33
|
+
/** Seconds after creation during which the LP fee is elevated against snipers. */
|
|
34
|
+
export const SNIPER_WINDOW_SECS = 30;
|
|
35
|
+
/** LP fee at the first second of the sniper window, falling linearly to LP_FEE_BPS. */
|
|
36
|
+
export const SNIPER_START_BPS = 8_000;
|
|
37
|
+
/** Paid by the creator to the treasury at launch. */
|
|
38
|
+
export const LAUNCH_FEE_LAMPORTS = 10000000n;
|
|
39
|
+
/** Bounds of the virtual quote reserve a launch may open with (bridged SOL, 9 decimals). */
|
|
40
|
+
export const MIN_VIRTUAL_QUOTE = 1000000000n;
|
|
41
|
+
export const MAX_VIRTUAL_QUOTE = 10000000000000n;
|
|
42
|
+
/** LP units the pool keeps on the first deposit. */
|
|
43
|
+
export const MINIMUM_LIQUIDITY = 1000n;
|
|
44
|
+
/** Decimals of an LP mint. */
|
|
45
|
+
export const LP_DECIMALS = 9;
|
|
46
|
+
/** Decimals of bridged SOL. */
|
|
47
|
+
export const SOL_DECIMALS = 9;
|
|
48
|
+
/** The dollar market cap a launch opens at; the backend sets the virtual quote to hit it. */
|
|
49
|
+
export const OPENING_MCAP_USD = 5_000;
|
|
50
|
+
/** Candle resolutions the API serves, in seconds. */
|
|
51
|
+
export const CANDLE_RESOLUTIONS = { '1m': 60, '5m': 300, '15m': 900, '1h': 3_600, '4h': 14_400, '1d': 86_400 };
|
|
52
|
+
export function mulDivFloor(a, b, d) {
|
|
53
|
+
if (d === 0n)
|
|
54
|
+
throw new RangeError('division by zero');
|
|
55
|
+
return (a * b) / d;
|
|
56
|
+
}
|
|
57
|
+
export function mulDivCeil(a, b, d) {
|
|
58
|
+
if (d === 0n)
|
|
59
|
+
throw new RangeError('division by zero');
|
|
60
|
+
const p = a * b;
|
|
61
|
+
return p % d === 0n ? p / d : p / d + 1n;
|
|
62
|
+
}
|
|
63
|
+
/** A fee of `bps` on `amount`, rounded up. */
|
|
64
|
+
export function feeAmount(amount, bps) {
|
|
65
|
+
return mulDivCeil(amount, BigInt(bps), BPS);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Bordrless's share of `value`, the quote value of what a launch pool's hooks cut from one side of
|
|
69
|
+
* a swap (`bordrless_core::protocol_share`): `shareBps` of it, rounded up, never more than `value`
|
|
70
|
+
* (a share is at most 10,000). Pure: 0 in gives 0 out, so a launch whose rules collect nothing pays
|
|
71
|
+
* nothing.
|
|
72
|
+
*/
|
|
73
|
+
export function protocolShare(value, shareBps) {
|
|
74
|
+
if (!Number.isInteger(shareBps) || shareBps < 0 || shareBps > MAX_PROTOCOL_SHARE_BPS)
|
|
75
|
+
throw new RangeError('a share is between 0 and 10,000 bps');
|
|
76
|
+
return feeAmount(value, shareBps);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* The quote value of a base-side cut at the swap's own price (`bordrless_core::quote_value`):
|
|
80
|
+
* `ceil(cut * quoteLeg / baseLeg)` over the quote and base the curve exchanged; 0 for no cut.
|
|
81
|
+
* Only a creator's own token hook makes base-side cuts (the kit takes none); the launch quote math
|
|
82
|
+
* here does not model them.
|
|
83
|
+
*/
|
|
84
|
+
export function quoteValue(base, quoteLeg, baseLeg) {
|
|
85
|
+
if (base === 0n)
|
|
86
|
+
return 0n;
|
|
87
|
+
return mulDivCeil(base, quoteLeg, baseLeg);
|
|
88
|
+
}
|
|
89
|
+
export function isqrt(n) {
|
|
90
|
+
if (n < 0n)
|
|
91
|
+
throw new RangeError('negative');
|
|
92
|
+
if (n < 2n)
|
|
93
|
+
return n;
|
|
94
|
+
let x = 1n << BigInt(Math.ceil(n.toString(2).length / 2));
|
|
95
|
+
for (;;) {
|
|
96
|
+
const y = (x + n / x) / 2n;
|
|
97
|
+
if (y >= x)
|
|
98
|
+
return x;
|
|
99
|
+
x = y;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/** Constant-product output of `amountIn` (after fees), or null when nothing comes out or the output would exceed the real reserve. */
|
|
103
|
+
export function swapOut(amountIn, reserveIn, virtualIn, reserveOut, virtualOut) {
|
|
104
|
+
if (amountIn <= 0n)
|
|
105
|
+
return null;
|
|
106
|
+
const x = reserveIn + virtualIn;
|
|
107
|
+
const y = reserveOut + virtualOut;
|
|
108
|
+
const out = mulDivFloor(amountIn, y, x + amountIn);
|
|
109
|
+
if (out <= 0n || out > reserveOut)
|
|
110
|
+
return null;
|
|
111
|
+
return out;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* The input (after fees) that buys exactly `amountOut`, rounded up; null when `amountOut` is above
|
|
115
|
+
* the real reserve, when the curve can never give it (no virtual output reserve left beyond it), or
|
|
116
|
+
* when the input would not fit a u64 (`bordrless_core::swap_in_for_out`).
|
|
117
|
+
*/
|
|
118
|
+
export function swapInForOut(amountOut, reserveIn, virtualIn, reserveOut, virtualOut) {
|
|
119
|
+
if (amountOut <= 0n || amountOut > reserveOut)
|
|
120
|
+
return null;
|
|
121
|
+
const x = reserveIn + virtualIn;
|
|
122
|
+
const y = reserveOut + virtualOut;
|
|
123
|
+
if (y - amountOut <= 0n)
|
|
124
|
+
return null;
|
|
125
|
+
const dx = mulDivCeil(amountOut, x, y - amountOut);
|
|
126
|
+
return dx > U64_MAX ? null : dx;
|
|
127
|
+
}
|
|
128
|
+
/** The largest input (after fees) before the real output reserve is exhausted. */
|
|
129
|
+
export function maxSwapIn(reserveIn, virtualIn, reserveOut, virtualOut) {
|
|
130
|
+
return swapInForOut(reserveOut, reserveIn, virtualIn, reserveOut, virtualOut);
|
|
131
|
+
}
|
|
132
|
+
/** Spot price in quote base units per base base unit, times WAD. */
|
|
133
|
+
export function spotPriceWad(r) {
|
|
134
|
+
return mulDivFloor(r.quoteReserve + r.virtualQuote, WAD, r.baseReserve + r.virtualBase);
|
|
135
|
+
}
|
|
136
|
+
/** Spot price as a number: quote whole units per base whole unit. */
|
|
137
|
+
export function spotPrice(r, baseDecimals, quoteDecimals) {
|
|
138
|
+
const wad = spotPriceWad(r);
|
|
139
|
+
return (Number(wad) / 1e18) * 10 ** (baseDecimals - quoteDecimals);
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* `bordrless_core::swap_amounts`: the amounts of a swap whose input vault received `received`
|
|
143
|
+
* (after any `before_swap` cut), or why the DEX refuses it. A fee that does not fit a u64 is larger
|
|
144
|
+
* than the amount it is taken from.
|
|
145
|
+
*/
|
|
146
|
+
export function swapAmounts(side, received, lpFeeBps, protocolFeeBps, r) {
|
|
147
|
+
const buy = side === 'buy';
|
|
148
|
+
const lpFee = feeAmount(received, lpFeeBps);
|
|
149
|
+
const protocolIn = buy ? feeAmount(received, protocolFeeBps) : 0n;
|
|
150
|
+
if (lpFee > U64_MAX || protocolIn > U64_MAX)
|
|
151
|
+
return { failure: 'fees_exceed_input' };
|
|
152
|
+
const netIn = received - lpFee - protocolIn;
|
|
153
|
+
if (netIn <= 0n)
|
|
154
|
+
return { failure: 'fees_exceed_input' };
|
|
155
|
+
const outGross = buy ? swapOut(netIn, r.quoteReserve, r.virtualQuote, r.baseReserve, r.virtualBase) : swapOut(netIn, r.baseReserve, r.virtualBase, r.quoteReserve, r.virtualQuote);
|
|
156
|
+
if (outGross === null)
|
|
157
|
+
return { failure: 'insufficient_liquidity' };
|
|
158
|
+
if (buy)
|
|
159
|
+
return { lpFee, protocolFee: protocolIn, netIn, outGross, amountOut: outGross, toReserveIn: received - protocolIn, failure: null };
|
|
160
|
+
const protocolFee = feeAmount(outGross, protocolFeeBps);
|
|
161
|
+
if (protocolFee > U64_MAX || outGross - protocolFee <= 0n)
|
|
162
|
+
return { failure: 'fee_exceeds_output' };
|
|
163
|
+
return { lpFee, protocolFee, netIn, outGross, amountOut: outGross - protocolFee, toReserveIn: received, failure: null };
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* A swap on a pool as the v2 DEX runs it (§3.1), `received` being what reached the input vault
|
|
167
|
+
* (the caller applies a hook's `before_swap` cut first, and its `after_swap` cut to `amountOut`).
|
|
168
|
+
* The protocol fee is in the quote token on both sides: a buy's from what reached the vault, a
|
|
169
|
+
* sell's from the curve's output, so a sell's `amountOut` is the curve's output less it. On a
|
|
170
|
+
* failure the fields are those computed up to it and `amountOut` is null.
|
|
171
|
+
*/
|
|
172
|
+
export function quoteSwap(r, direction, amountIn, lpFeeBps, protocolFeeBps) {
|
|
173
|
+
const a = swapAmounts(direction, amountIn, lpFeeBps, protocolFeeBps, r);
|
|
174
|
+
if (a.failure === null)
|
|
175
|
+
return { lpFee: a.lpFee, protocolFee: a.protocolFee, netIn: a.netIn, amountOut: a.amountOut, outGross: a.outGross, toReserveIn: a.toReserveIn, failure: null };
|
|
176
|
+
// What was computed up to the failure.
|
|
177
|
+
const lpFee = feeAmount(amountIn, lpFeeBps);
|
|
178
|
+
const protocolIn = direction === 'buy' ? feeAmount(amountIn, protocolFeeBps) : 0n;
|
|
179
|
+
const netIn = amountIn - lpFee - protocolIn;
|
|
180
|
+
const outGross = a.failure === 'fee_exceeds_output' ? curveOutput(r, direction, netIn) : null;
|
|
181
|
+
const protocolFee = outGross === null ? protocolIn : feeAmount(outGross, protocolFeeBps);
|
|
182
|
+
return { lpFee, protocolFee, netIn, amountOut: null, outGross, toReserveIn: amountIn - protocolIn, failure: a.failure };
|
|
183
|
+
}
|
|
184
|
+
/** First LP: sqrt(base · quote), of which MINIMUM_LIQUIDITY stays with the pool. */
|
|
185
|
+
export function initialLp(base, quote) {
|
|
186
|
+
const total = isqrt(base * quote);
|
|
187
|
+
const toDepositor = total - MINIMUM_LIQUIDITY;
|
|
188
|
+
if (toDepositor <= 0n)
|
|
189
|
+
return null;
|
|
190
|
+
return { toDepositor, total };
|
|
191
|
+
}
|
|
192
|
+
/** A later deposit: what is taken at the pool's ratio and the LP minted. */
|
|
193
|
+
export function lpForDeposit(baseDesired, quoteDesired, baseReserve, quoteReserve, lpSupply) {
|
|
194
|
+
if (baseDesired <= 0n || quoteDesired <= 0n || baseReserve <= 0n || quoteReserve <= 0n || lpSupply <= 0n)
|
|
195
|
+
return null;
|
|
196
|
+
const quoteForBase = mulDivCeil(baseDesired, quoteReserve, baseReserve);
|
|
197
|
+
let baseUsed;
|
|
198
|
+
let quoteUsed;
|
|
199
|
+
if (quoteForBase <= quoteDesired) {
|
|
200
|
+
baseUsed = baseDesired;
|
|
201
|
+
quoteUsed = quoteForBase;
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
baseUsed = mulDivCeil(quoteDesired, baseReserve, quoteReserve);
|
|
205
|
+
quoteUsed = quoteDesired;
|
|
206
|
+
}
|
|
207
|
+
if (baseUsed > baseDesired)
|
|
208
|
+
return null;
|
|
209
|
+
const lp = mulDivFloor(baseUsed, lpSupply, baseReserve) < mulDivFloor(quoteUsed, lpSupply, quoteReserve) ? mulDivFloor(baseUsed, lpSupply, baseReserve) : mulDivFloor(quoteUsed, lpSupply, quoteReserve);
|
|
210
|
+
if (lp <= 0n)
|
|
211
|
+
return null;
|
|
212
|
+
return { baseUsed, quoteUsed, lp };
|
|
213
|
+
}
|
|
214
|
+
/** What `lp` shares withdraw. */
|
|
215
|
+
export function withdrawForLp(lp, baseReserve, quoteReserve, lpSupply) {
|
|
216
|
+
if (lp <= 0n || lp > lpSupply)
|
|
217
|
+
return null;
|
|
218
|
+
return { base: mulDivFloor(lp, baseReserve, lpSupply), quote: mulDivFloor(lp, quoteReserve, lpSupply) };
|
|
219
|
+
}
|
|
220
|
+
/** The curve of a launch from the supply, the curve share and the virtual quote it opens with. */
|
|
221
|
+
export function curveParams(supply, curveBps, virtualQuote) {
|
|
222
|
+
if (curveBps > BPS || virtualQuote <= 0n)
|
|
223
|
+
return null;
|
|
224
|
+
const c = mulDivFloor(supply, curveBps, BPS);
|
|
225
|
+
const r = supply - c;
|
|
226
|
+
if (c <= r || r <= 0n)
|
|
227
|
+
return null;
|
|
228
|
+
const diff = c - r;
|
|
229
|
+
return { curveTokens: c, reserveTokens: r, virtualQuote, virtualBase: mulDivFloor(c, r, diff), graduationQuote: mulDivFloor(virtualQuote, diff, r) };
|
|
230
|
+
}
|
|
231
|
+
/** The market cap a curve opens at, in quote base units. */
|
|
232
|
+
export function openingMcap(supply, p) {
|
|
233
|
+
return mulDivFloor(supply, p.virtualQuote, p.curveTokens + p.virtualBase);
|
|
234
|
+
}
|
|
235
|
+
/** The virtual quote that opens a curve at `mcap` quote base units (the inverse of openingMcap, rounded up). */
|
|
236
|
+
export function virtualQuoteForMcap(supply, curveBps, mcap) {
|
|
237
|
+
const shape = curveParams(supply, curveBps, 1n);
|
|
238
|
+
if (!shape)
|
|
239
|
+
return null;
|
|
240
|
+
return mulDivCeil(mcap, shape.curveTokens + shape.virtualBase, supply);
|
|
241
|
+
}
|
|
242
|
+
/** Base to add at graduation so the price is continuous without the virtual offsets. */
|
|
243
|
+
export function graduationTopup(r) {
|
|
244
|
+
const target = mulDivFloor(r.quoteReserve, r.baseReserve + r.virtualBase, r.quoteReserve + r.virtualQuote);
|
|
245
|
+
return target > r.baseReserve ? target - r.baseReserve : 0n;
|
|
246
|
+
}
|
|
247
|
+
/** The LP fee during the sniper window. */
|
|
248
|
+
export function sniperLpFee(now, createdAt, windowSecs, startBps, baseBps) {
|
|
249
|
+
if (windowSecs <= 0 || now >= createdAt + windowSecs || startBps <= baseBps)
|
|
250
|
+
return baseBps;
|
|
251
|
+
const elapsed = Math.max(0, now - createdAt);
|
|
252
|
+
const span = startBps - baseBps;
|
|
253
|
+
const fallen = Math.floor((span * elapsed) / windowSecs);
|
|
254
|
+
return startBps - Math.min(span, fallen);
|
|
255
|
+
}
|
|
256
|
+
/** The virtual quote for a launch opening at OPENING_MCAP_USD given the quote's dollar price, clamped to the bounds. */
|
|
257
|
+
export function virtualQuoteForUsd(quoteUsd, quoteDecimals = SOL_DECIMALS, mcapUsd = OPENING_MCAP_USD) {
|
|
258
|
+
if (!(quoteUsd > 0))
|
|
259
|
+
throw new RangeError('the quote has no price');
|
|
260
|
+
const mcapQuote = BigInt(Math.round((mcapUsd / quoteUsd) * 10 ** quoteDecimals));
|
|
261
|
+
const vq = virtualQuoteForMcap(TOKEN_SUPPLY, CURVE_BPS, mcapQuote);
|
|
262
|
+
if (vq === null)
|
|
263
|
+
throw new RangeError('no curve');
|
|
264
|
+
return vq < MIN_VIRTUAL_QUOTE ? MIN_VIRTUAL_QUOTE : vq > MAX_VIRTUAL_QUOTE ? MAX_VIRTUAL_QUOTE : vq;
|
|
265
|
+
}
|
|
266
|
+
// ---- v2: token rules (docs/hooks-v2.md §4, §5, §7) ---------------------------------------------------
|
|
267
|
+
//
|
|
268
|
+
// The launch pool hook's fee math and the kit's reward accounting, kept to the same discipline:
|
|
269
|
+
// integer math in bigint, each rounding as the programs round. The Rust tests publish vectors at
|
|
270
|
+
// programs/tests/vectors/launch-fees.json, and vectors.test.ts feeds every case to the functions
|
|
271
|
+
// here (each takes plain numbers so it can).
|
|
272
|
+
const DAY_SECS = 86_400;
|
|
273
|
+
/** The policy every launch config is set to. */
|
|
274
|
+
export const RULE_BOUNDS = {
|
|
275
|
+
maxHolderFeeBps: 200,
|
|
276
|
+
maxBurnBps: 100,
|
|
277
|
+
maxRulesFeeBps: 300,
|
|
278
|
+
minMaxWalletBps: 100,
|
|
279
|
+
maxMaxWalletBps: 500,
|
|
280
|
+
maxCreatorLockSecs: 90 * DAY_SECS,
|
|
281
|
+
maxEarlyWindowSecs: 300,
|
|
282
|
+
maxEarlyLockSecs: 7 * DAY_SECS,
|
|
283
|
+
};
|
|
284
|
+
/** Ceilings `init_config` and `set_config` enforce, which no config can raise (max wallet stays below 10,000; 1 is the kit's own floor). */
|
|
285
|
+
export const RULE_CEILINGS = {
|
|
286
|
+
maxHolderFeeBps: 500,
|
|
287
|
+
maxBurnBps: 500,
|
|
288
|
+
maxRulesFeeBps: 1_000,
|
|
289
|
+
minMaxWalletBps: 1,
|
|
290
|
+
maxMaxWalletBps: 9_999,
|
|
291
|
+
maxCreatorLockSecs: 365 * DAY_SECS,
|
|
292
|
+
maxEarlyWindowSecs: 3_600,
|
|
293
|
+
maxEarlyLockSecs: 30 * DAY_SECS,
|
|
294
|
+
};
|
|
295
|
+
/** The smallest launch supply a config may set (a ceiling of §5.2), so `min_eligible` is at least 1. */
|
|
296
|
+
export const MIN_LAUNCH_SUPPLY = 1000n;
|
|
297
|
+
/** The largest launch supply a config may set: the kit installs on no larger supply (§4.11). */
|
|
298
|
+
export const MAX_LAUNCH_SUPPLY = 10000000000000000n;
|
|
299
|
+
/** Holder rewards offered on the launch form, per side (§7.1). */
|
|
300
|
+
export const HOLDER_FEE_CHOICES_BPS = [0, 50, 100, 200];
|
|
301
|
+
/** Burn offered on the launch form, per side. */
|
|
302
|
+
export const BURN_CHOICES_BPS = [0, 25, 50, 100];
|
|
303
|
+
/** Max wallet offered on the launch form, as a share of supply until graduation. */
|
|
304
|
+
export const MAX_WALLET_CHOICES_BPS = [0, 100, 200, 300, 500];
|
|
305
|
+
/** Creator wallet lock offered on the launch form, in days. */
|
|
306
|
+
export const CREATOR_LOCK_CHOICES_DAYS = [0, 7, 30, 90];
|
|
307
|
+
/** Early-buyer lock: buys this many seconds after the launch or sooner are locked. */
|
|
308
|
+
export const EARLY_WINDOW_CHOICES_SECS = [0, 30, 60, 300];
|
|
309
|
+
/** Early-buyer lock: seconds after the launch when those tokens unlock (15 min, 1 h, 24 h). */
|
|
310
|
+
export const EARLY_LOCK_CHOICES_SECS = [900, 3_600, 86_400];
|
|
311
|
+
/** No token rules. */
|
|
312
|
+
export const NO_RULES = { holderFeeBuyBps: 0, holderFeeSellBps: 0, burnBuyBps: 0, burnSellBps: 0, maxWalletBps: 0, creatorLockDays: 0, earlyWindowSecs: 0, earlyLockSecs: 0 };
|
|
313
|
+
/** The §7.1 per-rule defaults the Custom path starts from when a rule is switched on: holder rewards 1% both sides, max wallet 2%, creator wallet lock 30 days, no burn, no early-buyer lock. (The preset the form starts on is Plain.) */
|
|
314
|
+
export const DEFAULT_RULES = { ...NO_RULES, holderFeeBuyBps: 100, holderFeeSellBps: 100, maxWalletBps: 200, creatorLockDays: 30 };
|
|
315
|
+
/** Holder rewards are on when either side takes a holder fee. */
|
|
316
|
+
export const rewardsOn = (r) => r.holderFeeBuyBps > 0 || r.holderFeeSellBps > 0;
|
|
317
|
+
/** The default creator fee: 0.5% with holder rewards on, else 1%. */
|
|
318
|
+
export const defaultCreatorFeeBps = (r) => (rewardsOn(r) ? 50 : 100);
|
|
319
|
+
/**
|
|
320
|
+
* The presets of the launch form, in order (§7.1), the one list the launch page, the home page and
|
|
321
|
+
* the docs read; `bordrless_core::policy::presets::ALL` is the same list, and the fee vectors pin
|
|
322
|
+
* this one to it. Each stays within the §5.2 bounds and the 3%-per-side cap (the policy test checks
|
|
323
|
+
* every one). Holder rewards set "both sides" take the same rate on buys and sells.
|
|
324
|
+
*/
|
|
325
|
+
export const RULE_PRESETS = [
|
|
326
|
+
{ name: 'Plain', slug: 'plain', description: 'No token rules; the creator fee and the sniper fee every launch has.', creatorFeeBps: 100, rules: NO_RULES },
|
|
327
|
+
{
|
|
328
|
+
name: 'Diamond hands',
|
|
329
|
+
slug: 'diamond-hands',
|
|
330
|
+
description: 'Early-buyer lock (the first 5 min, until 24 h after launch), max wallet 1%, creator wallet lock 90 d, holder rewards 1%, creator fee 0.5%.',
|
|
331
|
+
creatorFeeBps: 50,
|
|
332
|
+
rules: { ...NO_RULES, holderFeeBuyBps: 100, holderFeeSellBps: 100, maxWalletBps: 100, creatorLockDays: 90, earlyWindowSecs: 300, earlyLockSecs: 86_400 },
|
|
333
|
+
},
|
|
334
|
+
{ name: 'Burn', slug: 'burn', description: 'Burn 0.5% on buys and sells, holder rewards 0.5%, creator fee 0.5%.', creatorFeeBps: 50, rules: { ...NO_RULES, holderFeeBuyBps: 50, holderFeeSellBps: 50, burnBuyBps: 50, burnSellBps: 50 } },
|
|
335
|
+
{ name: 'Paid to hold', slug: 'paid-to-hold', description: 'Holder rewards 2% on sells only, creator wallet lock 30 d, creator fee 0.5%.', creatorFeeBps: 50, rules: { ...NO_RULES, holderFeeSellBps: 200, creatorLockDays: 30 } },
|
|
336
|
+
];
|
|
337
|
+
/** The two paths beside the presets, in order, with the words the home console and the launch form print. */
|
|
338
|
+
export const RULE_PATHS = [
|
|
339
|
+
{ name: 'Custom', slug: 'custom', description: 'Mix the rules by hand; holder rewards and burn can differ on buys and sells.' },
|
|
340
|
+
{ name: 'Build your own', slug: 'build-your-own', description: 'Write a hook with the SDK, make a launch config, paste its key.' },
|
|
341
|
+
];
|
|
342
|
+
/** The preset the launch form starts on. */
|
|
343
|
+
export const DEFAULT_RULE_PRESET = 'Plain';
|
|
344
|
+
/** The preset of this name. */
|
|
345
|
+
export function presetNamed(name) {
|
|
346
|
+
return RULE_PRESETS.find((p) => p.name === name) ?? RULE_PRESETS[0];
|
|
347
|
+
}
|
|
348
|
+
/** Kit module bits (§4.2). */
|
|
349
|
+
export const KIT_MODULES = { HOLDER_REWARDS: 1, MAX_WALLET: 2, CREATOR_LOCK: 4, EARLY_LOCK: 8 };
|
|
350
|
+
/** The kit modules a launch with these rules installs (§5.1); 0 means no hook at all (a burn alone runs in the pool hook). */
|
|
351
|
+
export function kitModules(r) {
|
|
352
|
+
return (rewardsOn(r) ? KIT_MODULES.HOLDER_REWARDS : 0) | (r.maxWalletBps > 0 ? KIT_MODULES.MAX_WALLET : 0) | (r.creatorLockDays > 0 ? KIT_MODULES.CREATOR_LOCK : 0) | (r.earlyWindowSecs > 0 ? KIT_MODULES.EARLY_LOCK : 0);
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* The token hook flags a kit mint carries for `modules` (§4.2, `bordrless_kit::mint_flags`):
|
|
356
|
+
* `BEFORE_TRANSFER`, plus `BEFORE_BURN | WRITES_HOOK_DATA` with holder rewards or the early-buyer
|
|
357
|
+
* lock; 0 (no hook at all) without modules. So 1 for modules 2, 4 or 6, and 145 whenever 1 or 8 is
|
|
358
|
+
* among them. A mint naming the kit with other flags is not a kit token (§4.15).
|
|
359
|
+
*/
|
|
360
|
+
export function kitMintFlags(modules) {
|
|
361
|
+
if (modules === 0)
|
|
362
|
+
return 0;
|
|
363
|
+
const data = (modules & (KIT_MODULES.HOLDER_REWARDS | KIT_MODULES.EARLY_LOCK)) !== 0;
|
|
364
|
+
return TOKEN_HOOK_FLAGS.BEFORE_TRANSFER | (data ? TOKEN_HOOK_FLAGS.BEFORE_BURN | TOKEN_HOOK_FLAGS.WRITES_HOOK_DATA : 0);
|
|
365
|
+
}
|
|
366
|
+
/** The rules a launch made at `launchedAt` (unix seconds) carries, as the API shows them. */
|
|
367
|
+
export function launchRulesAt(r, launchedAt) {
|
|
368
|
+
return {
|
|
369
|
+
holderFeeBuyBps: r.holderFeeBuyBps,
|
|
370
|
+
holderFeeSellBps: r.holderFeeSellBps,
|
|
371
|
+
burnBuyBps: r.burnBuyBps,
|
|
372
|
+
burnSellBps: r.burnSellBps,
|
|
373
|
+
maxWalletBps: r.maxWalletBps,
|
|
374
|
+
creatorUnlockAt: r.creatorLockDays > 0 ? launchedAt + r.creatorLockDays * DAY_SECS : null,
|
|
375
|
+
earlyWindowEndsAt: r.earlyWindowSecs > 0 ? launchedAt + r.earlyWindowSecs : null,
|
|
376
|
+
earlyUnlockAt: r.earlyWindowSecs > 0 ? launchedAt + r.earlyLockSecs : null,
|
|
377
|
+
walletsOnly: rewardsOn(r),
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
const percent = (bps) => `${bps / 100}%`;
|
|
381
|
+
function duration(secs) {
|
|
382
|
+
const unit = (n, word) => `${n} ${word}${n === 1 ? '' : 's'}`;
|
|
383
|
+
if (secs % DAY_SECS === 0)
|
|
384
|
+
return unit(secs / DAY_SECS, 'day');
|
|
385
|
+
if (secs % 3_600 === 0)
|
|
386
|
+
return unit(secs / 3_600, 'hour');
|
|
387
|
+
if (secs % 60 === 0)
|
|
388
|
+
return unit(secs / 60, 'minute');
|
|
389
|
+
return unit(secs, 'second');
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Why `create_launch` would refuse these rules with this creator fee under `bounds`, as the sentence
|
|
393
|
+
* the launch form shows; null when it would take them. An early-buyer lock needs its window, and its
|
|
394
|
+
* tokens must unlock after the window ends.
|
|
395
|
+
*/
|
|
396
|
+
export function checkLaunchRules(r, creatorFeeBps, bounds = RULE_BOUNDS) {
|
|
397
|
+
const values = [creatorFeeBps, r.holderFeeBuyBps, r.holderFeeSellBps, r.burnBuyBps, r.burnSellBps, r.maxWalletBps, r.creatorLockDays, r.earlyWindowSecs, r.earlyLockSecs];
|
|
398
|
+
if (!values.every((v) => Number.isSafeInteger(v) && v >= 0))
|
|
399
|
+
return 'Token rules must be whole numbers, 0 or more.';
|
|
400
|
+
if (r.holderFeeBuyBps > bounds.maxHolderFeeBps || r.holderFeeSellBps > bounds.maxHolderFeeBps)
|
|
401
|
+
return `Holder rewards can be at most ${percent(bounds.maxHolderFeeBps)} per side.`;
|
|
402
|
+
if (r.burnBuyBps > bounds.maxBurnBps || r.burnSellBps > bounds.maxBurnBps)
|
|
403
|
+
return `Burn can be at most ${percent(bounds.maxBurnBps)} per side.`;
|
|
404
|
+
for (const [side, holder, burn] of [['buys', r.holderFeeBuyBps, r.burnBuyBps], ['sells', r.holderFeeSellBps, r.burnSellBps]]) {
|
|
405
|
+
const total = creatorFeeBps + holder + burn;
|
|
406
|
+
if (total > bounds.maxRulesFeeBps)
|
|
407
|
+
return `Creator fee, holder rewards and burn add up to ${percent(total)} on ${side}; the most is ${percent(bounds.maxRulesFeeBps)}.`;
|
|
408
|
+
}
|
|
409
|
+
if (r.maxWalletBps !== 0 && (r.maxWalletBps < bounds.minMaxWalletBps || r.maxWalletBps > bounds.maxMaxWalletBps))
|
|
410
|
+
return `Max wallet must be off or between ${percent(bounds.minMaxWalletBps)} and ${percent(bounds.maxMaxWalletBps)}.`;
|
|
411
|
+
if (r.creatorLockDays * DAY_SECS > bounds.maxCreatorLockSecs)
|
|
412
|
+
return `The creator wallet lock can be at most ${duration(bounds.maxCreatorLockSecs)}.`;
|
|
413
|
+
if (r.earlyWindowSecs === 0) {
|
|
414
|
+
if (r.earlyLockSecs !== 0)
|
|
415
|
+
return 'The early-buyer lock needs a window: choose how soon after the launch a buy is locked.';
|
|
416
|
+
}
|
|
417
|
+
else {
|
|
418
|
+
if (r.earlyWindowSecs > bounds.maxEarlyWindowSecs)
|
|
419
|
+
return `The early-buyer window can be at most ${duration(bounds.maxEarlyWindowSecs)}.`;
|
|
420
|
+
if (r.earlyLockSecs <= r.earlyWindowSecs)
|
|
421
|
+
return 'Early buys must unlock after the early-buyer window ends.';
|
|
422
|
+
if (r.earlyLockSecs > bounds.maxEarlyLockSecs)
|
|
423
|
+
return `Early buys must unlock within ${duration(bounds.maxEarlyLockSecs)} of the launch.`;
|
|
424
|
+
}
|
|
425
|
+
return null;
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* §8.1 `preset`: the §7.1 preset whose rules and creator fee a launch matches exactly, 'custom' for
|
|
429
|
+
* other rules (and for a config without a custom hook), 'custom-hook' for a launch made from a
|
|
430
|
+
* config with a creator's own hook, null for a launch without rules and without a hook.
|
|
431
|
+
*/
|
|
432
|
+
export function presetOf(r, creatorFeeBps, customHook = false) {
|
|
433
|
+
if (customHook)
|
|
434
|
+
return 'custom-hook';
|
|
435
|
+
const keys = Object.keys(NO_RULES);
|
|
436
|
+
if (keys.every((key) => r[key] === 0))
|
|
437
|
+
return null;
|
|
438
|
+
const match = RULE_PRESETS.find((p) => p.creatorFeeBps === creatorFeeBps && keys.every((key) => p.rules[key] === r[key]));
|
|
439
|
+
return match ? match.name : 'custom';
|
|
440
|
+
}
|
|
441
|
+
/** §4.9: a buy from the launch pool landing at `now` inside the early window is locked until the early unlock; null when it is not. */
|
|
442
|
+
export function buyLockedUntil(rules, now) {
|
|
443
|
+
return rules.earlyWindowEndsAt !== null && rules.earlyUnlockAt !== null && now < rules.earlyWindowEndsAt && now < rules.earlyUnlockAt ? rules.earlyUnlockAt : null;
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* The creator and holder fees from `amount` of bridged SOL (a buy's input, a sell's output): each
|
|
447
|
+
* rounded up, the holder fee only while `eligible >= minEligible`, and neither when together they are
|
|
448
|
+
* not below the amount.
|
|
449
|
+
*/
|
|
450
|
+
export function creatorAndHolderFees(amount, creatorFeeBps, holderFeeBps, eligible, minEligible) {
|
|
451
|
+
const creatorFee = feeAmount(amount, creatorFeeBps);
|
|
452
|
+
const holderFee = holderFeeBps > 0 && eligible >= minEligible ? feeAmount(amount, holderFeeBps) : 0n;
|
|
453
|
+
if (creatorFee + holderFee >= amount)
|
|
454
|
+
return { creatorFee: 0n, holderFee: 0n };
|
|
455
|
+
return { creatorFee, holderFee };
|
|
456
|
+
}
|
|
457
|
+
/** The burn from `amount` of tokens (a buy's output, a sell's input): rounded down, and none when it is not below the amount. */
|
|
458
|
+
export function tradeBurn(amount, burnBps) {
|
|
459
|
+
const burn = mulDivFloor(amount, BigInt(burnBps), BPS);
|
|
460
|
+
return burn < amount ? burn : 0n;
|
|
461
|
+
}
|
|
462
|
+
/** The hook's `before_swap` cut from the input: a buy's creator and holder fees, a sell's burn. `p.eligible` is the count before the trade. */
|
|
463
|
+
export function launchBeforeSwap(side, amountIn, p) {
|
|
464
|
+
if (side === 'buy')
|
|
465
|
+
return { ...creatorAndHolderFees(amountIn, p.creatorFeeBps, p.holderFeeBuyBps, p.eligible, p.minEligible), burn: 0n };
|
|
466
|
+
return { creatorFee: 0n, holderFee: 0n, burn: tradeBurn(amountIn, p.burnSellBps) };
|
|
467
|
+
}
|
|
468
|
+
/** The hook's `after_swap` cut from the output: a buy's burn, a sell's creator and holder fees. For a sell, `p.eligible` is the count once the input has left the seller. */
|
|
469
|
+
export function launchAfterSwap(side, amountOut, p) {
|
|
470
|
+
if (side === 'buy')
|
|
471
|
+
return { creatorFee: 0n, holderFee: 0n, burn: tradeBurn(amountOut, p.burnBuyBps) };
|
|
472
|
+
return { ...creatorAndHolderFees(amountOut, p.creatorFeeBps, p.holderFeeSellBps, p.eligible, p.minEligible), burn: 0n };
|
|
473
|
+
}
|
|
474
|
+
/** The constant-product output for `netIn` going into the curve, rounded down, before the check against the real reserve. */
|
|
475
|
+
export function curveOutput(r, side, netIn) {
|
|
476
|
+
if (netIn <= 0n)
|
|
477
|
+
return 0n;
|
|
478
|
+
return side === 'buy' ? mulDivFloor(netIn, r.baseReserve + r.virtualBase, r.quoteReserve + r.virtualQuote + netIn) : mulDivFloor(netIn, r.quoteReserve + r.virtualQuote, r.baseReserve + r.virtualBase + netIn);
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* A swap on a launch pool as the v2 DEX runs it with the launch hook under the share model (§3.1,
|
|
482
|
+
* §5.4; the Rust reference is `bordrless_program_tests::launch::quote_launch_swap`): Bordrless
|
|
483
|
+
* takes `protocolShareBps` of the creator and holder fees (the hooks' cuts; the kit takes none of
|
|
484
|
+
* its own, so the cuts are exactly those fees), in bridged SOL, and nothing when they are nothing.
|
|
485
|
+
* A buy: the creator and holder fees from the input, Bordrless's share of them and the LP fee on
|
|
486
|
+
* what reached the vault, the curve, the burn from the output. A sell: the burn from the input, the
|
|
487
|
+
* LP fee on what reached the vault, the curve, then the creator and holder fees from the output and
|
|
488
|
+
* Bordrless's share of them held back from the delivery. A sell takes its input out of the kit's
|
|
489
|
+
* eligible count before `after_swap` reads it (every seller is a holder: the pool and the launch
|
|
490
|
+
* never sell). An ordinary pool's flat rate is `quoteSwap`.
|
|
491
|
+
*/
|
|
492
|
+
export function quoteLaunchSwap(r, side, amountIn, lpFeeBps, protocolShareBps, p) {
|
|
493
|
+
const before = launchBeforeSwap(side, amountIn, p);
|
|
494
|
+
const received = amountIn - before.creatorFee - before.holderFee - before.burn;
|
|
495
|
+
const eligibleAtFees = side === 'sell' ? (p.eligible > amountIn ? p.eligible - amountIn : 0n) : p.eligible;
|
|
496
|
+
const holderFeeOn = (side === 'buy' ? p.holderFeeBuyBps : p.holderFeeSellBps) > 0 && eligibleAtFees >= p.minEligible;
|
|
497
|
+
const lpFee = feeAmount(received, lpFeeBps);
|
|
498
|
+
// A buy's cuts are the hook's quote fees; a sell's input side cuts nothing (the burn is not a cut), so its share is taken from the output.
|
|
499
|
+
const inputProtocolFee = side === 'buy' ? protocolShare(before.creatorFee + before.holderFee, protocolShareBps) : 0n;
|
|
500
|
+
const netIn = received - lpFee - inputProtocolFee;
|
|
501
|
+
const failed = (failure, protocolFee = inputProtocolFee) => ({ ...before, holderFeeOn, received, lpFee, protocolFee, netIn, amountOut: null, delivered: null, failure });
|
|
502
|
+
if (received <= 0n || netIn <= 0n)
|
|
503
|
+
return failed('fees_exceed_input');
|
|
504
|
+
const out = curveOutput(r, side, netIn);
|
|
505
|
+
if (out <= 0n)
|
|
506
|
+
return failed('no_output');
|
|
507
|
+
if (out > (side === 'buy' ? r.baseReserve : r.quoteReserve))
|
|
508
|
+
return failed('insufficient_liquidity');
|
|
509
|
+
if (side === 'buy') {
|
|
510
|
+
const after = launchAfterSwap('buy', out, p);
|
|
511
|
+
return { ...before, burn: after.burn, holderFeeOn, received, lpFee, protocolFee: inputProtocolFee, netIn, amountOut: out, delivered: out - after.burn, failure: null };
|
|
512
|
+
}
|
|
513
|
+
const after = launchAfterSwap('sell', out, { ...p, eligible: eligibleAtFees });
|
|
514
|
+
const protocolFee = protocolShare(after.creatorFee + after.holderFee, protocolShareBps);
|
|
515
|
+
if (after.creatorFee + after.holderFee + protocolFee >= out)
|
|
516
|
+
return failed('no_output', protocolFee);
|
|
517
|
+
return { creatorFee: after.creatorFee, holderFee: after.holderFee, burn: before.burn, holderFeeOn, received, lpFee, protocolFee, netIn, amountOut: out, delivered: out - after.creatorFee - after.holderFee - protocolFee, failure: null };
|
|
518
|
+
}
|
|
519
|
+
/** What a launch-pool swap adds to its input reserve (the Rust reference's `to_reserve_in`): what reached the vault, less a buy's protocol fee. */
|
|
520
|
+
export const launchSwapToReserve = (side, q) => (side === 'buy' ? q.received - q.protocolFee : q.received);
|
|
521
|
+
/**
|
|
522
|
+
* Whether a buy that lands leaves the pool's quote reserve at or above the graduation threshold, so
|
|
523
|
+
* `graduate` can follow it in the same transaction (§8.2: the input after the creator and holder
|
|
524
|
+
* fees, less the protocol fee, joins the reserve; the launch graduates once `quote_reserve >=
|
|
525
|
+
* graduation_quote`).
|
|
526
|
+
*/
|
|
527
|
+
export function buyGraduates(r, q, graduationQuote) {
|
|
528
|
+
return q.failure === null && r.quoteReserve + launchSwapToReserve('buy', q) >= graduationQuote;
|
|
529
|
+
}
|
|
530
|
+
/**
|
|
531
|
+
* §8.2 `maxAmountIn` on a curve pool: the largest buy the curve can still fill with these fees (one
|
|
532
|
+
* lamport more and the DEX refuses it, `InsufficientLiquidity`), found with the quote itself rather
|
|
533
|
+
* than by dividing out the fees, so the rounding is the programs'. Only meaningful on a curve: a pool
|
|
534
|
+
* without virtual reserves can always fill a buy.
|
|
535
|
+
*/
|
|
536
|
+
export function curveMaxBuyIn(r, lpFeeBps, protocolShareBps, p) {
|
|
537
|
+
return maxBuyWithin(r, lpFeeBps, protocolShareBps, p, U64_MAX);
|
|
538
|
+
}
|
|
539
|
+
/** `1 - Π(1 - fee_i)` in basis points, rounded up so the figure never understates. */
|
|
540
|
+
export function compoundFeeBps(feesBps) {
|
|
541
|
+
let kept = 1n;
|
|
542
|
+
let scale = 1n;
|
|
543
|
+
for (const fee of feesBps) {
|
|
544
|
+
if (!Number.isInteger(fee) || fee < 0 || fee > 10_000)
|
|
545
|
+
throw new RangeError('a fee is between 0 and 10,000 bps');
|
|
546
|
+
kept *= BPS - BigInt(fee);
|
|
547
|
+
scale *= BPS;
|
|
548
|
+
}
|
|
549
|
+
return Number(mulDivCeil(scale - kept, BPS, scale));
|
|
550
|
+
}
|
|
551
|
+
/** What Bordrless's share comes to as a rate of the trade, in basis points (may be fractional: 150 bps of cuts at a quarter is 37.5). 0 when the rules collect nothing. */
|
|
552
|
+
export function protocolShareBps(cutsBps, shareBps = LAUNCH_PROTOCOL_SHARE_BPS) {
|
|
553
|
+
return (cutsBps * shareBps) / 10_000;
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* The total fee of one side as one figure (§7.3), in the order the v2 DEX charges it, rounded up to
|
|
557
|
+
* a basis point so the figure never understates. With `s` Bordrless's share of the cuts and `flat`
|
|
558
|
+
* an ordinary pool's rate (one of them 0):
|
|
559
|
+
* a buy `1 - (1 - (creator + holders)(1 + s))(1 - lp - flat)(1 - burn)`,
|
|
560
|
+
* a sell `1 - (1 - burn)(1 - lp)(1 - flat)(1 - (creator + holders)(1 + s))`.
|
|
561
|
+
* Plain on a launch pool (creator 1%, LP 0.3%, s = 0.25): 1 - 0.9875 · 0.997 = 1.5437%, 155 bps;
|
|
562
|
+
* Diamond hands (holders 1%, creator 0.5%): 1 - 0.98125 · 0.997 = 2.1694%, 217 bps a side.
|
|
563
|
+
*/
|
|
564
|
+
export function sideFeeBps(side, f) {
|
|
565
|
+
const share = f.protocolShareBps ?? 0;
|
|
566
|
+
const flat = f.protocolFeeBps ?? 0;
|
|
567
|
+
for (const v of [f.lpFeeBps, f.creatorFeeBps, f.holderFeeBps, f.burnBps, flat])
|
|
568
|
+
if (!Number.isInteger(v) || v < 0 || v > 10_000)
|
|
569
|
+
throw new RangeError('a fee is between 0 and 10,000 bps');
|
|
570
|
+
if (!Number.isInteger(share) || share < 0 || share > MAX_PROTOCOL_SHARE_BPS)
|
|
571
|
+
throw new RangeError('a share is between 0 and 10,000 bps');
|
|
572
|
+
const cuts = BigInt(f.creatorFeeBps + f.holderFeeBps);
|
|
573
|
+
// The cuts with Bordrless's share on top, over BPS²: (creator + holders)(1 + s).
|
|
574
|
+
const cutsScaled = cuts * (BPS + BigInt(share));
|
|
575
|
+
if (cutsScaled > BPS * BPS)
|
|
576
|
+
throw new RangeError('the cuts and their share pass 100%');
|
|
577
|
+
const factors = side === 'buy' ? [[BPS * BPS - cutsScaled, BPS * BPS], [BPS - BigInt(f.lpFeeBps + flat), BPS], [BPS - BigInt(f.burnBps), BPS]] : [[BPS - BigInt(f.burnBps), BPS], [BPS - BigInt(f.lpFeeBps), BPS], [BPS - BigInt(flat), BPS], [BPS * BPS - cutsScaled, BPS * BPS]];
|
|
578
|
+
let kept = 1n;
|
|
579
|
+
let scale = 1n;
|
|
580
|
+
for (const [num, den] of factors) {
|
|
581
|
+
if (num < 0n)
|
|
582
|
+
throw new RangeError('a fee is between 0 and 10,000 bps');
|
|
583
|
+
kept *= num;
|
|
584
|
+
scale *= den;
|
|
585
|
+
}
|
|
586
|
+
return Number(mulDivCeil(scale - kept, BPS, scale));
|
|
587
|
+
}
|
|
588
|
+
/** How far a swap's price on the curve (fees excluded) is from the spot price before it, in basis points, rounded to nearest. */
|
|
589
|
+
export function curveImpactBps(r, side, netIn, amountOut) {
|
|
590
|
+
const quote = r.quoteReserve + r.virtualQuote;
|
|
591
|
+
const base = r.baseReserve + r.virtualBase;
|
|
592
|
+
if (netIn <= 0n || amountOut <= 0n || quote <= 0n || base <= 0n)
|
|
593
|
+
return 0;
|
|
594
|
+
// A buy pays netIn / amountOut and a sell gets amountOut / netIn, against the spot quote / base.
|
|
595
|
+
const den = side === 'buy' ? amountOut * quote : netIn * quote;
|
|
596
|
+
const num = side === 'buy' ? netIn * base - den : den - amountOut * base;
|
|
597
|
+
if (num <= 0n)
|
|
598
|
+
return 0;
|
|
599
|
+
return Number((num * BPS * 2n + den) / (den * 2n));
|
|
600
|
+
}
|
|
601
|
+
// ---- max wallet (§4.3, §4.9) ------------------------------------------------------------------------
|
|
602
|
+
/** The max-wallet cap, fixed at launch (`bordrless_core::max_wallet_cap`): the launch supply times the bps, rounded down; 0 when off. */
|
|
603
|
+
export function maxWalletCap(supplyAtLaunch, maxWalletBps) {
|
|
604
|
+
if (maxWalletBps <= 0)
|
|
605
|
+
return 0n;
|
|
606
|
+
const cap = mulDivFloor(supplyAtLaunch, BigInt(maxWalletBps), BPS);
|
|
607
|
+
return cap > U64_MAX ? U64_MAX : cap;
|
|
608
|
+
}
|
|
609
|
+
/** What a wallet holding `balance` can still receive: the kit refuses a transfer that would take it above `cap`. */
|
|
610
|
+
export function maxWalletLeft(cap, balance) {
|
|
611
|
+
return cap > balance ? cap - balance : 0n;
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* The largest buy the pool can fill whose delivery stays within `allowance` tokens; 0 when no buy
|
|
615
|
+
* does. Fees rounding up make the boundary ragged by a few base units, so this bisects on the exact
|
|
616
|
+
* quote and then looks a little past the boundary.
|
|
617
|
+
*/
|
|
618
|
+
export function maxBuyWithin(r, lpFeeBps, protocolShareBps, p, allowance) {
|
|
619
|
+
if (allowance <= 0n)
|
|
620
|
+
return 0n;
|
|
621
|
+
const quote = (amountIn) => quoteLaunchSwap(r, 'buy', amountIn, lpFeeBps, protocolShareBps, p);
|
|
622
|
+
const fits = (amountIn) => {
|
|
623
|
+
const q = quote(amountIn);
|
|
624
|
+
return q.failure !== 'insufficient_liquidity' && (q.delivered === null || q.delivered <= allowance);
|
|
625
|
+
};
|
|
626
|
+
let lo = 0n;
|
|
627
|
+
let hi = 1n;
|
|
628
|
+
while (fits(hi)) {
|
|
629
|
+
lo = hi;
|
|
630
|
+
if (hi === U64_MAX)
|
|
631
|
+
return U64_MAX;
|
|
632
|
+
hi = hi * 2n > U64_MAX ? U64_MAX : hi * 2n;
|
|
633
|
+
}
|
|
634
|
+
while (hi - lo > 1n) {
|
|
635
|
+
const mid = (lo + hi) / 2n;
|
|
636
|
+
if (fits(mid))
|
|
637
|
+
lo = mid;
|
|
638
|
+
else
|
|
639
|
+
hi = mid;
|
|
640
|
+
}
|
|
641
|
+
const top = lo + 64n < U64_MAX ? lo + 64n : U64_MAX;
|
|
642
|
+
for (let a = lo + 1n; a <= top; a++)
|
|
643
|
+
if (fits(a))
|
|
644
|
+
lo = a;
|
|
645
|
+
// An input too small to buy anything is not a buy.
|
|
646
|
+
return quote(lo).delivered === null ? 0n : lo;
|
|
647
|
+
}
|
|
648
|
+
/**
|
|
649
|
+
* §8.2 `devBuyMaxLamports`: the largest first buy, in lamports, whose tokens after the buy burn stay
|
|
650
|
+
* within max wallet at the opening reserves, with the fees of the creator's own first buy (the normal
|
|
651
|
+
* LP fee, the creator fee and Bordrless's share of it; no holder fee, as nobody is eligible yet).
|
|
652
|
+
* Null without max wallet. A bot that buys first only makes the creator receive less.
|
|
653
|
+
*/
|
|
654
|
+
export function devBuyMaxLamports(curve, supply, rules, creatorFeeBps, lpFeeBps, protocolShareBps) {
|
|
655
|
+
if (rules.maxWalletBps <= 0)
|
|
656
|
+
return null;
|
|
657
|
+
const opening = { baseReserve: curve.curveTokens, quoteReserve: 0n, virtualBase: curve.virtualBase, virtualQuote: curve.virtualQuote };
|
|
658
|
+
const fees = { creatorFeeBps, holderFeeBuyBps: 0, holderFeeSellBps: 0, burnBuyBps: rules.burnBuyBps, burnSellBps: rules.burnSellBps, eligible: 0n, minEligible: 0n };
|
|
659
|
+
return maxBuyWithin(opening, lpFeeBps, protocolShareBps, fees, maxWalletCap(supply, rules.maxWalletBps));
|
|
660
|
+
}
|
|
661
|
+
// ---- holder rewards: the kit's accounting and its off-chain mirror (§4.3, §4.7, §4.8, §4.13) -------
|
|
662
|
+
/** Scale of the reward accounting: `acc_per_share` is lamports per eligible base unit times SCALE. */
|
|
663
|
+
export const SCALE = 10n ** 12n;
|
|
664
|
+
/** `min_eligible` is the supply / 1,000, set at init: holder fees wait until holders hold that much. */
|
|
665
|
+
export const MIN_ELIGIBLE_DIVISOR = 1000n;
|
|
666
|
+
export const minEligibleFor = (supply) => supply / MIN_ELIGIBLE_DIVISOR;
|
|
667
|
+
/** `min_eligible` at the policy supply: 1,000,000 whole tokens. */
|
|
668
|
+
export const MIN_ELIGIBLE = minEligibleFor(TOKEN_SUPPLY);
|
|
669
|
+
/** The least "Share with holders" takes: 0.001 SOL. */
|
|
670
|
+
export const MIN_SHARE_LAMPORTS = 1000000n;
|
|
671
|
+
/** A share streams to holders over this many seconds of eligible holding: from when it is made, or from when the share streaming then ends (§4.10, the review fixes). */
|
|
672
|
+
export const SHARE_STREAM_SECS = 3_600;
|
|
673
|
+
/** Claims below 0.0005 SOL (about 100 times a claim's network fee) are hidden by default (§7.3). */
|
|
674
|
+
export const CLAIM_DUST_LAMPORTS = 500000n;
|
|
675
|
+
/** 64 bytes of hook data: snapshot u128, owed u64, early_locked u64, little-endian, then 32 reserved bytes. */
|
|
676
|
+
export function decodeKitHookData(data) {
|
|
677
|
+
if (data.length !== HOOK_DATA_LEN)
|
|
678
|
+
throw new RangeError(`hook data is ${HOOK_DATA_LEN} bytes, not ${data.length}`);
|
|
679
|
+
const v = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
680
|
+
return { snapshot: v.getBigUint64(0, true) | (v.getBigUint64(8, true) << 64n), owed: v.getBigUint64(16, true), earlyLocked: v.getBigUint64(24, true) };
|
|
681
|
+
}
|
|
682
|
+
/** The inverse of decodeKitHookData, reserved bytes zero. */
|
|
683
|
+
export function encodeKitHookData(d) {
|
|
684
|
+
if (d.snapshot < 0n || d.snapshot >> 128n !== 0n || d.owed < 0n || d.owed > U64_MAX || d.earlyLocked < 0n || d.earlyLocked > U64_MAX)
|
|
685
|
+
throw new RangeError('hook data out of range');
|
|
686
|
+
const out = new Uint8Array(HOOK_DATA_LEN);
|
|
687
|
+
const v = new DataView(out.buffer);
|
|
688
|
+
v.setBigUint64(0, d.snapshot & U64_MAX, true);
|
|
689
|
+
v.setBigUint64(8, d.snapshot >> 64n, true);
|
|
690
|
+
v.setBigUint64(16, d.owed, true);
|
|
691
|
+
v.setBigUint64(24, d.earlyLocked, true);
|
|
692
|
+
return out;
|
|
693
|
+
}
|
|
694
|
+
// The share stream, as the review fixes left it (docs/hooks-v2.md, "Review fixes" note 2 and
|
|
695
|
+
// "Review fixes, second round" note 1). The running stream releases `streamRemaining` linearly over
|
|
696
|
+
// `[streamLast, streamEnd]`. A share made while it runs waits in `streamNext` and streams over the
|
|
697
|
+
// hour after it, `[streamEnd, streamEnd + 3,600]`. While holders hold less than `minEligible` the
|
|
698
|
+
// whole stream pauses: nothing is released and its end (with the waiting shares' hour) moves on
|
|
699
|
+
// with the clock. Every function below is a pure function of the account state; the program side
|
|
700
|
+
// (`kitRelease`, `kitSync`, `kitAddShare`, `kitSettle`, `kitShare`, `kitClaim`) mirrors
|
|
701
|
+
// `bordrless_kit::math` and the `share` and `claim` handlers, the mirror side (`streamAt`,
|
|
702
|
+
// `kitSynced`, `rewardsClaimable`, `rewardTotals`) mirrors `bordrless_kit::mirror`. Arithmetic is
|
|
703
|
+
// checked as the program checks it: what would overflow a u64 (a u128 for `accPerShare`) or go
|
|
704
|
+
// below zero is null, a state the program cannot reach.
|
|
705
|
+
/** Whether a sync divides among holders: `eligible > 0` and `eligible >= min_eligible` (`KitConfig::divides`). */
|
|
706
|
+
export const kitDivides = (k) => k.eligible > 0n && k.eligible >= k.minEligible;
|
|
707
|
+
/** Whether holder rewards are on for this kit state (module 1; a state without `modules` counts as on). */
|
|
708
|
+
const kitRewardsOn = (k) => k.modules === undefined || (k.modules & KIT_MODULES.HOLDER_REWARDS) !== 0;
|
|
709
|
+
/**
|
|
710
|
+
* What a linear stream of `amount` over `[from, to]` releases at `now`, rounded down: all of it at or
|
|
711
|
+
* after `to`, nothing at or before `from` (`bordrless_kit::math::stream_release`, `mirror::linear`).
|
|
712
|
+
*/
|
|
713
|
+
export function linearRelease(amount, from, to, now) {
|
|
714
|
+
if (amount <= 0n)
|
|
715
|
+
return 0n;
|
|
716
|
+
if (now >= to)
|
|
717
|
+
return amount;
|
|
718
|
+
if (now <= from)
|
|
719
|
+
return 0n;
|
|
720
|
+
return (amount * BigInt(now - from)) / BigInt(to - from);
|
|
721
|
+
}
|
|
722
|
+
/**
|
|
723
|
+
* The shared stream at `now` (`bordrless_kit::mirror::stream_at`): the running stream's linear
|
|
724
|
+
* release; once it is out (`now >= streamEnd`), the waiting shares' release over the hour after its
|
|
725
|
+
* end. Nothing while holders hold less than `minEligible`. Null on an overflow.
|
|
726
|
+
*/
|
|
727
|
+
export function streamAt(k, now) {
|
|
728
|
+
const running = k.streamRemaining;
|
|
729
|
+
const waiting = k.streamNext;
|
|
730
|
+
if (!kitDivides(k) || (running === 0n && waiting === 0n))
|
|
731
|
+
return { released: 0n, streamRemaining: running, streamNext: waiting };
|
|
732
|
+
const first = linearRelease(running, k.streamLast, k.streamEnd, now);
|
|
733
|
+
const left = running - first;
|
|
734
|
+
if (left > 0n || waiting === 0n || now < k.streamEnd)
|
|
735
|
+
return { released: first, streamRemaining: left, streamNext: waiting };
|
|
736
|
+
const second = linearRelease(waiting, k.streamEnd, k.streamEnd + SHARE_STREAM_SECS, now);
|
|
737
|
+
const released = first + second;
|
|
738
|
+
if (released > U64_MAX)
|
|
739
|
+
return null;
|
|
740
|
+
return { released, streamRemaining: waiting - second, streamNext: 0n };
|
|
741
|
+
}
|
|
742
|
+
/** What the shared stream releases at `now`, rounded down (`mirror::released_at`): 0 while nobody is eligible. */
|
|
743
|
+
export function streamReleased(k, now) {
|
|
744
|
+
return streamAt(k, now)?.released ?? null;
|
|
745
|
+
}
|
|
746
|
+
/** The mirror's sync at `now` with the reward vault holding `vaultAmount` (`bordrless_kit::mirror::synced`). Null when the state is impossible. */
|
|
747
|
+
export function kitSynced(k, vaultAmount, now) {
|
|
748
|
+
const stream = streamAt(k, now);
|
|
749
|
+
if (!stream)
|
|
750
|
+
return null;
|
|
751
|
+
const total = vaultAmount + k.totalClaimed;
|
|
752
|
+
if (total > U64_MAX || total < k.seen)
|
|
753
|
+
return null;
|
|
754
|
+
const pending = total - k.seen + stream.released;
|
|
755
|
+
const pot = pending + k.held;
|
|
756
|
+
if (pending > U64_MAX || pot > U64_MAX)
|
|
757
|
+
return null;
|
|
758
|
+
const eligible = kitDivides(k);
|
|
759
|
+
const divides = eligible && pot > 0n;
|
|
760
|
+
let accPerShare = k.accPerShare;
|
|
761
|
+
if (divides) {
|
|
762
|
+
const scaled = pot * SCALE + k.rem;
|
|
763
|
+
if (scaled > U128_MAX)
|
|
764
|
+
return null;
|
|
765
|
+
accPerShare += scaled / k.eligible;
|
|
766
|
+
if (accPerShare > U128_MAX)
|
|
767
|
+
return null;
|
|
768
|
+
}
|
|
769
|
+
const distributed = divides ? k.totalDistributed + pot : k.totalDistributed;
|
|
770
|
+
if (distributed > U64_MAX)
|
|
771
|
+
return null;
|
|
772
|
+
const streaming = stream.streamRemaining + stream.streamNext;
|
|
773
|
+
return {
|
|
774
|
+
released: stream.released,
|
|
775
|
+
pending,
|
|
776
|
+
divides,
|
|
777
|
+
accPerShare,
|
|
778
|
+
held: eligible ? 0n : pot,
|
|
779
|
+
streamRemaining: stream.streamRemaining,
|
|
780
|
+
streamNext: stream.streamNext,
|
|
781
|
+
distributed,
|
|
782
|
+
streaming: streaming > U64_MAX ? U64_MAX : streaming,
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
/**
|
|
786
|
+
* The off-chain mirror (§4.13) at `now` (`bordrless_kit::mirror::claimable`): what a holder could
|
|
787
|
+
* claim, from `KitConfig`, the reward vault's balance, the holding's balance and its hook data, and
|
|
788
|
+
* what a claim would pay (no more than the vault holds). 0 for the pool and the launch (`excluded`)
|
|
789
|
+
* and for a token without holder rewards. Null when the kit's state cannot be synced.
|
|
790
|
+
*/
|
|
791
|
+
export function rewardsClaimable(k, vaultAmount, now, balance, data, excluded = false) {
|
|
792
|
+
if (excluded || !kitRewardsOn(k))
|
|
793
|
+
return { claimable: 0n, payable: 0n };
|
|
794
|
+
const after = kitSynced(k, vaultAmount, now);
|
|
795
|
+
if (!after || data.snapshot > after.accPerShare)
|
|
796
|
+
return null;
|
|
797
|
+
const product = balance * (after.accPerShare - data.snapshot);
|
|
798
|
+
if (product > U128_MAX)
|
|
799
|
+
return null;
|
|
800
|
+
const earned = product / SCALE;
|
|
801
|
+
const claimable = data.owed + earned;
|
|
802
|
+
if (earned > U64_MAX || claimable > U64_MAX)
|
|
803
|
+
return null;
|
|
804
|
+
return { claimable, payable: claimable < vaultAmount ? claimable : vaultAmount };
|
|
805
|
+
}
|
|
806
|
+
/**
|
|
807
|
+
* §4.13 totals at `now`, lamports: distributed (with what the next sync would distribute), claimed,
|
|
808
|
+
* shared, and shared but still streaming (`rewardsStreaming`: `stream_remaining + stream_next -
|
|
809
|
+
* released`); also what the stream releases now and what waits in `held`. Null when the state
|
|
810
|
+
* cannot be synced.
|
|
811
|
+
*/
|
|
812
|
+
export function rewardTotals(k, vaultAmount, now) {
|
|
813
|
+
const s = kitSynced(k, vaultAmount, now);
|
|
814
|
+
return s ? { distributed: s.distributed, claimed: k.totalClaimed, shared: k.totalShared, streaming: s.streaming, released: s.released, held: s.held } : null;
|
|
815
|
+
}
|
|
816
|
+
/**
|
|
817
|
+
* `KitConfig::release` at `now`, as the program runs it: the running stream releases linearly; once
|
|
818
|
+
* it is out the waiting shares become the running stream over the hour after its end, and the part
|
|
819
|
+
* of that hour already past is released with it. While nobody is eligible nothing is released and
|
|
820
|
+
* the end moves on by the time that passed. Null on an overflow.
|
|
821
|
+
*/
|
|
822
|
+
export function kitRelease(k, now) {
|
|
823
|
+
if (k.streamRemaining === 0n && k.streamNext === 0n)
|
|
824
|
+
return { state: k, released: 0n };
|
|
825
|
+
if (!kitDivides(k)) {
|
|
826
|
+
if (now > k.streamLast)
|
|
827
|
+
return { state: { ...k, streamEnd: k.streamEnd + (now - k.streamLast), streamLast: now }, released: 0n };
|
|
828
|
+
return { state: k, released: 0n };
|
|
829
|
+
}
|
|
830
|
+
let released = linearRelease(k.streamRemaining, k.streamLast, k.streamEnd, now);
|
|
831
|
+
let state = { ...k, streamRemaining: k.streamRemaining - released, streamLast: Math.max(k.streamLast, now) };
|
|
832
|
+
if (state.streamRemaining === 0n && state.streamNext > 0n && now >= state.streamEnd) {
|
|
833
|
+
// The waiting shares' hour began when the running stream ended.
|
|
834
|
+
const start = state.streamEnd;
|
|
835
|
+
const end = start + SHARE_STREAM_SECS;
|
|
836
|
+
const part = linearRelease(state.streamNext, start, end, now);
|
|
837
|
+
state = { ...state, streamRemaining: state.streamNext - part, streamNext: 0n, streamEnd: end };
|
|
838
|
+
released += part;
|
|
839
|
+
if (released > U64_MAX)
|
|
840
|
+
return null;
|
|
841
|
+
}
|
|
842
|
+
return { state, released };
|
|
843
|
+
}
|
|
844
|
+
/**
|
|
845
|
+
* The kit's sync at `now` as the program runs it (`KitConfig::sync`, §4.7 with the review fixes):
|
|
846
|
+
* the stream releases (`kitRelease`), then what reached the reward vault since the last sync plus
|
|
847
|
+
* what was released is spread over the eligible supply with the exact scaled remainder kept, or held
|
|
848
|
+
* while nobody or too little is eligible. Null when the vault and the claims fall short of what the
|
|
849
|
+
* accounting has seen, or on an overflow, which the program reverts on.
|
|
850
|
+
*/
|
|
851
|
+
export function kitSync(k, vaultAmount, now) {
|
|
852
|
+
const release = kitRelease(k, now);
|
|
853
|
+
if (!release)
|
|
854
|
+
return null;
|
|
855
|
+
const s = release.state;
|
|
856
|
+
const total = vaultAmount + s.totalClaimed;
|
|
857
|
+
if (total > U64_MAX || total < s.seen)
|
|
858
|
+
return null;
|
|
859
|
+
const fresh = total - s.seen + release.released;
|
|
860
|
+
if (fresh > U64_MAX)
|
|
861
|
+
return null;
|
|
862
|
+
if (!kitDivides(s)) {
|
|
863
|
+
const held = s.held + fresh;
|
|
864
|
+
return held > U64_MAX ? null : { ...s, seen: total, held };
|
|
865
|
+
}
|
|
866
|
+
const pot = fresh + s.held;
|
|
867
|
+
if (pot > U64_MAX)
|
|
868
|
+
return null;
|
|
869
|
+
if (pot === 0n)
|
|
870
|
+
return { ...s, seen: total, held: 0n };
|
|
871
|
+
const scaled = pot * SCALE + s.rem;
|
|
872
|
+
const inc = scaled / s.eligible;
|
|
873
|
+
const accPerShare = s.accPerShare + inc;
|
|
874
|
+
const totalDistributed = s.totalDistributed + pot;
|
|
875
|
+
if (scaled > U128_MAX || accPerShare > U128_MAX || totalDistributed > U64_MAX)
|
|
876
|
+
return null;
|
|
877
|
+
return { ...s, seen: total, held: 0n, rem: scaled - inc * s.eligible, accPerShare, totalDistributed };
|
|
878
|
+
}
|
|
879
|
+
/**
|
|
880
|
+
* `KitConfig::add_share`: a share of `received` lamports made at `now`, right after the sync at
|
|
881
|
+
* `now`, counted as seen and streamed over one hour at its own rate: at once when nothing streams or
|
|
882
|
+
* waits; joining the running stream when that stream's whole hour is still ahead; otherwise waiting
|
|
883
|
+
* with any shares already waiting, for the hour after the running stream. Null (the program's
|
|
884
|
+
* `MathOverflow`, a bug) when the stream was not synced at `now`, or on an overflow.
|
|
885
|
+
*/
|
|
886
|
+
export function kitAddShare(k, received, now) {
|
|
887
|
+
const at = Math.max(now, k.streamLast);
|
|
888
|
+
let s;
|
|
889
|
+
if (k.streamRemaining === 0n && k.streamNext === 0n) {
|
|
890
|
+
s = { ...k, streamRemaining: received, streamLast: at, streamEnd: at + SHARE_STREAM_SECS };
|
|
891
|
+
}
|
|
892
|
+
else {
|
|
893
|
+
const left = k.streamEnd - at;
|
|
894
|
+
if (k.streamLast !== at || left <= 0)
|
|
895
|
+
return null;
|
|
896
|
+
s = k.streamRemaining > 0n && left === SHARE_STREAM_SECS ? { ...k, streamRemaining: k.streamRemaining + received } : { ...k, streamNext: k.streamNext + received };
|
|
897
|
+
}
|
|
898
|
+
s = { ...s, seen: s.seen + received, totalShared: s.totalShared + received };
|
|
899
|
+
if (s.streamRemaining > U64_MAX || s.streamNext > U64_MAX || s.seen > U64_MAX || s.totalShared > U64_MAX)
|
|
900
|
+
return null;
|
|
901
|
+
return s;
|
|
902
|
+
}
|
|
903
|
+
/**
|
|
904
|
+
* The kit's settle (§4.8, `bordrless_kit::math::settle`) of a holder at its balance before the
|
|
905
|
+
* operation: `owed += balance * (accPerShare - snapshot) / SCALE` (floor), then `snapshot =
|
|
906
|
+
* accPerShare`, or 0 when the balance after the operation is 0. Null when the snapshot is above the
|
|
907
|
+
* accumulator or on an overflow (the program's `MathOverflow`).
|
|
908
|
+
*/
|
|
909
|
+
export function kitSettle(d, balance, balanceAfter, accPerShare) {
|
|
910
|
+
if (d.snapshot > accPerShare)
|
|
911
|
+
return null;
|
|
912
|
+
const product = balance * (accPerShare - d.snapshot);
|
|
913
|
+
if (product > U128_MAX)
|
|
914
|
+
return null;
|
|
915
|
+
const earned = product / SCALE;
|
|
916
|
+
const owed = d.owed + earned;
|
|
917
|
+
if (earned > U64_MAX || owed > U64_MAX)
|
|
918
|
+
return null;
|
|
919
|
+
return { ...d, owed, snapshot: balanceAfter === 0n ? 0n : accPerShare };
|
|
920
|
+
}
|
|
921
|
+
/**
|
|
922
|
+
* The kit's `share(amount)` at `now` with the reward vault holding `vaultAmount`: refused (null) below
|
|
923
|
+
* MIN_SHARE_LAMPORTS, while nobody is eligible or without holder rewards; else the sync, the
|
|
924
|
+
* transfer into the vault (bridged SOL has no hook, so all of it arrives) and `kitAddShare`.
|
|
925
|
+
*/
|
|
926
|
+
export function kitShare(k, vaultAmount, amount, now) {
|
|
927
|
+
if (amount < MIN_SHARE_LAMPORTS || !kitRewardsOn(k) || !kitDivides(k))
|
|
928
|
+
return null;
|
|
929
|
+
const synced = kitSync(k, vaultAmount, now);
|
|
930
|
+
if (!synced)
|
|
931
|
+
return null;
|
|
932
|
+
const vault = vaultAmount + amount;
|
|
933
|
+
const state = kitAddShare(synced, amount, now);
|
|
934
|
+
return state && vault <= U64_MAX ? { state, vault } : null;
|
|
935
|
+
}
|
|
936
|
+
/**
|
|
937
|
+
* The kit's `claim` at `now` by a holder with `balance` and hook data `data`: the sync, the settle at
|
|
938
|
+
* the balance, then `min(owed, vault)` paid. Null when the kit refuses it: the pool or the launch
|
|
939
|
+
* (`excluded`), no holder rewards, nothing to pay, or a state it cannot sync. The early-buyer bytes
|
|
940
|
+
* of the hook data are kept as they are.
|
|
941
|
+
*/
|
|
942
|
+
export function kitClaim(k, vaultAmount, now, balance, data, excluded = false) {
|
|
943
|
+
if (excluded || !kitRewardsOn(k))
|
|
944
|
+
return null;
|
|
945
|
+
const synced = kitSync(k, vaultAmount, now);
|
|
946
|
+
if (!synced)
|
|
947
|
+
return null;
|
|
948
|
+
const settled = kitSettle(data, balance, balance, synced.accPerShare);
|
|
949
|
+
if (!settled)
|
|
950
|
+
return null;
|
|
951
|
+
const pay = settled.owed < vaultAmount ? settled.owed : vaultAmount;
|
|
952
|
+
if (pay === 0n || synced.totalClaimed + pay > U64_MAX)
|
|
953
|
+
return null;
|
|
954
|
+
return { state: { ...synced, totalClaimed: synced.totalClaimed + pay }, vault: vaultAmount - pay, data: { ...settled, owed: settled.owed - pay }, pay };
|
|
955
|
+
}
|
|
956
|
+
/**
|
|
957
|
+
* When a share made at `now` starts streaming, unix seconds (§7.3's share copy, as the second round
|
|
958
|
+
* of review fixes words it): `now` when nothing streams or waits, or when the running stream's whole
|
|
959
|
+
* hour is still ahead (the share joins it); otherwise when the running stream ends, after the sync
|
|
960
|
+
* the share makes first. Null while nobody is eligible (the kit refuses a share then) or when the
|
|
961
|
+
* state cannot be synced.
|
|
962
|
+
*/
|
|
963
|
+
export function shareStartsAt(k, vaultAmount, now) {
|
|
964
|
+
if (!kitRewardsOn(k) || !kitDivides(k))
|
|
965
|
+
return null;
|
|
966
|
+
const s = kitSync(k, vaultAmount, now);
|
|
967
|
+
if (!s)
|
|
968
|
+
return null;
|
|
969
|
+
const at = Math.max(now, s.streamLast);
|
|
970
|
+
if (s.streamRemaining === 0n && s.streamNext === 0n)
|
|
971
|
+
return at;
|
|
972
|
+
if (s.streamRemaining > 0n && s.streamEnd - at === SHARE_STREAM_SECS)
|
|
973
|
+
return at;
|
|
974
|
+
return s.streamEnd;
|
|
975
|
+
}
|