@sly-rentals/core 1.1.2 → 2.0.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.
Files changed (164) hide show
  1. package/dist/api.json +12242 -9231
  2. package/dist/cjs/calculator/ContestTimeline.js +396 -0
  3. package/dist/cjs/calculator/ContestTimeline.js.map +1 -0
  4. package/dist/cjs/calculator/MinBidChart.js +419 -0
  5. package/dist/cjs/calculator/MinBidChart.js.map +1 -0
  6. package/dist/cjs/calculator/dom.js +39 -0
  7. package/dist/cjs/calculator/dom.js.map +1 -0
  8. package/dist/cjs/calculator/emitter.js +36 -0
  9. package/dist/cjs/calculator/emitter.js.map +1 -0
  10. package/dist/cjs/calculator/index.js +20 -0
  11. package/dist/cjs/calculator/index.js.map +1 -0
  12. package/dist/cjs/cost/constants.js +54 -5
  13. package/dist/cjs/cost/constants.js.map +1 -1
  14. package/dist/cjs/cost/estimate.js +7 -4
  15. package/dist/cjs/cost/estimate.js.map +1 -1
  16. package/dist/cjs/cost/estimate.test.js +16 -4
  17. package/dist/cjs/cost/estimate.test.js.map +1 -1
  18. package/dist/cjs/cost/index.js +32 -17
  19. package/dist/cjs/cost/index.js.map +1 -1
  20. package/dist/cjs/cost/sizes.js +20 -3
  21. package/dist/cjs/cost/sizes.js.map +1 -1
  22. package/dist/cjs/instructions/acceptRental.js +39 -53
  23. package/dist/cjs/instructions/acceptRental.js.map +1 -1
  24. package/dist/cjs/instructions/closeContract.js +18 -50
  25. package/dist/cjs/instructions/closeContract.js.map +1 -1
  26. package/dist/cjs/instructions/closeRental.js +19 -15
  27. package/dist/cjs/instructions/closeRental.js.map +1 -1
  28. package/dist/cjs/instructions/createContract.js +2 -4
  29. package/dist/cjs/instructions/createContract.js.map +1 -1
  30. package/dist/cjs/instructions/createContractThread.js +1 -2
  31. package/dist/cjs/instructions/createContractThread.js.map +1 -1
  32. package/dist/cjs/instructions/index.js +65 -42
  33. package/dist/cjs/instructions/index.js.map +1 -1
  34. package/dist/cjs/instructions/reserveRental.js +34 -46
  35. package/dist/cjs/instructions/reserveRental.js.map +1 -1
  36. package/dist/cjs/kit/index.js +5 -27
  37. package/dist/cjs/kit/index.js.map +1 -1
  38. package/dist/cjs/legacy/index.js +6 -3
  39. package/dist/cjs/legacy/index.js.map +1 -1
  40. package/dist/cjs/package.json +1 -1
  41. package/dist/cjs/react/ContestTimeline.js +59 -0
  42. package/dist/cjs/react/ContestTimeline.js.map +1 -0
  43. package/dist/cjs/react/MinBidChart.js +34 -0
  44. package/dist/cjs/react/MinBidChart.js.map +1 -0
  45. package/dist/cjs/react/index.js +18 -0
  46. package/dist/cjs/react/index.js.map +1 -0
  47. package/dist/cjs/utils/bidMath.js +6 -115
  48. package/dist/cjs/utils/bidMath.js.map +1 -1
  49. package/dist/cjs/utils/contestChain.js +199 -0
  50. package/dist/cjs/utils/contestChain.js.map +1 -0
  51. package/dist/cjs/utils/contractSnapshot.js +207 -0
  52. package/dist/cjs/utils/contractSnapshot.js.map +1 -0
  53. package/dist/cjs/utils/contractSnapshot.test.js +247 -0
  54. package/dist/cjs/utils/contractSnapshot.test.js.map +1 -0
  55. package/dist/cjs/utils/index.js +8 -3
  56. package/dist/cjs/utils/index.js.map +1 -1
  57. package/dist/cjs/version.js +1 -1
  58. package/dist/esm/calculator/ContestTimeline.js +392 -0
  59. package/dist/esm/calculator/ContestTimeline.js.map +1 -0
  60. package/dist/esm/calculator/MinBidChart.js +412 -0
  61. package/dist/esm/calculator/MinBidChart.js.map +1 -0
  62. package/dist/esm/calculator/dom.js +35 -0
  63. package/dist/esm/calculator/dom.js.map +1 -0
  64. package/dist/esm/calculator/emitter.js +32 -0
  65. package/dist/esm/calculator/emitter.js.map +1 -0
  66. package/dist/esm/calculator/index.js +13 -0
  67. package/dist/esm/calculator/index.js.map +1 -0
  68. package/dist/esm/cost/constants.js +52 -4
  69. package/dist/esm/cost/constants.js.map +1 -1
  70. package/dist/esm/cost/estimate.js +9 -6
  71. package/dist/esm/cost/estimate.js.map +1 -1
  72. package/dist/esm/cost/estimate.test.js +18 -6
  73. package/dist/esm/cost/estimate.test.js.map +1 -1
  74. package/dist/esm/cost/index.js +13 -3
  75. package/dist/esm/cost/index.js.map +1 -1
  76. package/dist/esm/cost/sizes.js +19 -2
  77. package/dist/esm/cost/sizes.js.map +1 -1
  78. package/dist/esm/instructions/acceptRental.js +40 -54
  79. package/dist/esm/instructions/acceptRental.js.map +1 -1
  80. package/dist/esm/instructions/closeContract.js +16 -15
  81. package/dist/esm/instructions/closeContract.js.map +1 -1
  82. package/dist/esm/instructions/closeRental.js +20 -16
  83. package/dist/esm/instructions/closeRental.js.map +1 -1
  84. package/dist/esm/instructions/createContract.js +2 -4
  85. package/dist/esm/instructions/createContract.js.map +1 -1
  86. package/dist/esm/instructions/createContractThread.js +2 -3
  87. package/dist/esm/instructions/createContractThread.js.map +1 -1
  88. package/dist/esm/instructions/index.js +36 -28
  89. package/dist/esm/instructions/index.js.map +1 -1
  90. package/dist/esm/instructions/reserveRental.js +34 -46
  91. package/dist/esm/instructions/reserveRental.js.map +1 -1
  92. package/dist/esm/kit/index.js +4 -4
  93. package/dist/esm/kit/index.js.map +1 -1
  94. package/dist/esm/legacy/index.js +5 -2
  95. package/dist/esm/legacy/index.js.map +1 -1
  96. package/dist/esm/package.json +1 -1
  97. package/dist/esm/react/ContestTimeline.js +56 -0
  98. package/dist/esm/react/ContestTimeline.js.map +1 -0
  99. package/dist/esm/react/MinBidChart.js +30 -0
  100. package/dist/esm/react/MinBidChart.js.map +1 -0
  101. package/dist/esm/react/index.js +12 -0
  102. package/dist/esm/react/index.js.map +1 -0
  103. package/dist/esm/utils/bidMath.js +6 -114
  104. package/dist/esm/utils/bidMath.js.map +1 -1
  105. package/dist/esm/utils/contestChain.js +191 -0
  106. package/dist/esm/utils/contestChain.js.map +1 -0
  107. package/dist/esm/utils/contractSnapshot.js +201 -0
  108. package/dist/esm/utils/contractSnapshot.js.map +1 -0
  109. package/dist/esm/utils/contractSnapshot.test.js +245 -0
  110. package/dist/esm/utils/contractSnapshot.test.js.map +1 -0
  111. package/dist/esm/utils/index.js +4 -2
  112. package/dist/esm/utils/index.js.map +1 -1
  113. package/dist/esm/version.js +1 -1
  114. package/dist/types/calculator/ContestTimeline.d.ts +69 -0
  115. package/dist/types/calculator/ContestTimeline.d.ts.map +1 -0
  116. package/dist/types/calculator/MinBidChart.d.ts +73 -0
  117. package/dist/types/calculator/MinBidChart.d.ts.map +1 -0
  118. package/dist/types/calculator/dom.d.ts +16 -0
  119. package/dist/types/calculator/dom.d.ts.map +1 -0
  120. package/dist/types/calculator/emitter.d.ts +13 -0
  121. package/dist/types/calculator/emitter.d.ts.map +1 -0
  122. package/dist/types/calculator/index.d.ts +13 -0
  123. package/dist/types/calculator/index.d.ts.map +1 -0
  124. package/dist/types/cost/constants.d.ts +49 -4
  125. package/dist/types/cost/constants.d.ts.map +1 -1
  126. package/dist/types/cost/estimate.d.ts +12 -1
  127. package/dist/types/cost/estimate.d.ts.map +1 -1
  128. package/dist/types/cost/index.d.ts +13 -3
  129. package/dist/types/cost/index.d.ts.map +1 -1
  130. package/dist/types/cost/sizes.d.ts +19 -2
  131. package/dist/types/cost/sizes.d.ts.map +1 -1
  132. package/dist/types/instructions/acceptRental.d.ts +8 -0
  133. package/dist/types/instructions/acceptRental.d.ts.map +1 -1
  134. package/dist/types/instructions/closeContract.d.ts +9 -0
  135. package/dist/types/instructions/closeContract.d.ts.map +1 -1
  136. package/dist/types/instructions/closeRental.d.ts +8 -0
  137. package/dist/types/instructions/closeRental.d.ts.map +1 -1
  138. package/dist/types/instructions/createContract.d.ts.map +1 -1
  139. package/dist/types/instructions/index.d.ts +36 -28
  140. package/dist/types/instructions/index.d.ts.map +1 -1
  141. package/dist/types/instructions/reserveRental.d.ts +8 -0
  142. package/dist/types/instructions/reserveRental.d.ts.map +1 -1
  143. package/dist/types/kit/index.d.ts +0 -2
  144. package/dist/types/kit/index.d.ts.map +1 -1
  145. package/dist/types/legacy/index.d.ts +1 -2
  146. package/dist/types/legacy/index.d.ts.map +1 -1
  147. package/dist/types/react/ContestTimeline.d.ts +17 -0
  148. package/dist/types/react/ContestTimeline.d.ts.map +1 -0
  149. package/dist/types/react/MinBidChart.d.ts +12 -0
  150. package/dist/types/react/MinBidChart.d.ts.map +1 -0
  151. package/dist/types/react/index.d.ts +12 -0
  152. package/dist/types/react/index.d.ts.map +1 -0
  153. package/dist/types/utils/bidMath.d.ts +6 -44
  154. package/dist/types/utils/bidMath.d.ts.map +1 -1
  155. package/dist/types/utils/contestChain.d.ts +118 -0
  156. package/dist/types/utils/contestChain.d.ts.map +1 -0
  157. package/dist/types/utils/contractSnapshot.d.ts +123 -0
  158. package/dist/types/utils/contractSnapshot.d.ts.map +1 -0
  159. package/dist/types/utils/contractSnapshot.test.d.ts +2 -0
  160. package/dist/types/utils/contractSnapshot.test.d.ts.map +1 -0
  161. package/dist/types/utils/index.d.ts +2 -1
  162. package/dist/types/utils/index.d.ts.map +1 -1
  163. package/dist/types/version.d.ts +1 -1
  164. package/package.json +15 -1
@@ -0,0 +1,191 @@
1
+ /**
2
+ * @purpose Pure-math chain-of-contests simulator.
3
+ *
4
+ * Walks a sequence of reservation events (calc defender + user-added
5
+ * takeovers) and produces per-row floor / bonus / owner-take values
6
+ * plus the segment breakdown UIs use to render piecewise curves.
7
+ *
8
+ * No React, no IO, no RPC. Feeds a calculator/visualization layer
9
+ * (`@sly-rentals/core/react/calculator`) but is callable standalone
10
+ * from any environment.
11
+ */
12
+ import { computeMinimumBid } from './bidMath';
13
+ function clamp(n, lo, hi) {
14
+ return Math.max(lo, Math.min(hi, n));
15
+ }
16
+ /**
17
+ * Convert a stardust value into the caller's display currency.
18
+ *
19
+ * ATLAS: stardust / stardustPerAtlas.
20
+ * POINTS: stardust / (stardustPerAtlas × atlasPerPoint), with a
21
+ * min-capture floor since the on-chain threshold rejects sub-floor bids.
22
+ */
23
+ export function stardustToDisplay(stardust, cur, stardustPerAtlas, atlasPerPoint, minCaptureThresholdPoints) {
24
+ const atlas = Number(stardust) / stardustPerAtlas;
25
+ if (cur === 'ATLAS')
26
+ return atlas;
27
+ return Math.max(atlas / atlasPerPoint, minCaptureThresholdPoints);
28
+ }
29
+ /**
30
+ * Walk a chain of contest events and produce per-contest math + segment
31
+ * breakdown.
32
+ *
33
+ * The chain begins with an optional calc defender row (the simulator's
34
+ * "your bid" entry). Subsequent takeover knobs are computed via
35
+ * `computeMinimumBid` against the previous row's bid.
36
+ *
37
+ * v1 assumes same-currency ATLAS knockoffs; tier-upgrade compensation
38
+ * is not modeled.
39
+ */
40
+ export function computeContestChain(args) {
41
+ const { cfg, rateStardust, activeDays, userDefender, takeoverKnobs, displayCur, stardustPerAtlas, atlasPerPoint, minCaptureThresholdPoints, contestedThreshold, contestedCountStart, } = args;
42
+ const safeSpan = Math.max(0.0001, activeDays);
43
+ const sorted = [...takeoverKnobs].sort((a, b) => a.atDay - b.atDay);
44
+ const rows = [];
45
+ const segments = [];
46
+ const toDisplay = (s) => stardustToDisplay(s, displayCur, stardustPerAtlas, atlasPerPoint, minCaptureThresholdPoints);
47
+ const callMinBid = (closeness, defenderBidStardust) => computeMinimumBid({
48
+ rateStardust,
49
+ defenderBidStardust,
50
+ contestedMultiplierMaxBps: cfg.contestedMultiplierMaxBps,
51
+ captureRateBps: cfg.captureRateBps,
52
+ closenessFraction: closeness,
53
+ });
54
+ const tmp = [];
55
+ if (userDefender) {
56
+ tmp.push({
57
+ id: '__calc',
58
+ atDay: userDefender.atDay,
59
+ bidStardust: userDefender.bidStardust,
60
+ bidDisplay: toDisplay(userDefender.bidStardust),
61
+ defenderBidStardust: 0n,
62
+ defenderAtDay: 0,
63
+ source: 'calc',
64
+ });
65
+ }
66
+ const firstEventAt = tmp[0]?.atDay ?? 0;
67
+ if (firstEventAt > 0) {
68
+ segments.push({
69
+ startDay: 0,
70
+ endDay: firstEventAt,
71
+ defenderBidStardust: 0n,
72
+ hasDefender: false,
73
+ });
74
+ }
75
+ let cursorBid = userDefender?.bidStardust ?? 0n;
76
+ let cursorAt = userDefender?.atDay ?? 0;
77
+ for (const k of sorted) {
78
+ segments.push({
79
+ startDay: cursorAt,
80
+ endDay: k.atDay,
81
+ defenderBidStardust: cursorBid,
82
+ hasDefender: true,
83
+ });
84
+ const closeness = clamp(k.atDay / safeSpan, 0, 1);
85
+ const floor = callMinBid(closeness, cursorBid);
86
+ tmp.push({
87
+ id: k.id,
88
+ atDay: k.atDay,
89
+ bidStardust: floor,
90
+ bidDisplay: toDisplay(floor),
91
+ defenderBidStardust: cursorBid,
92
+ defenderAtDay: cursorAt,
93
+ source: 'knob',
94
+ });
95
+ cursorBid = floor;
96
+ cursorAt = k.atDay;
97
+ }
98
+ if (tmp.length > 0) {
99
+ segments.push({
100
+ startDay: cursorAt,
101
+ endDay: activeDays,
102
+ defenderBidStardust: cursorBid,
103
+ hasDefender: true,
104
+ });
105
+ }
106
+ // Bonus multiplier — config knob, scales the same-cur knockoff bonus.
107
+ // 100 bps = 1× = no-op (current on-chain formula).
108
+ // TODO(bonus_multiplier_bps): drop the `?? 100` fallback once the
109
+ // on-chain field is guaranteed present.
110
+ const bonusMult = (cfg.bonusMultiplierBps ?? 100) / 100;
111
+ const bonusByIdx = tmp.map((_, i) => {
112
+ const next = tmp[i + 1];
113
+ if (!next)
114
+ return 0;
115
+ const closeness = clamp((next.atDay - tmp[i].atDay) / safeSpan, 0, 1);
116
+ const raw = Math.max(0, (next.bidDisplay - tmp[i].bidDisplay) * closeness * bonusMult);
117
+ // Match on-chain `b.min(bid_atlas)` — bonus capped at challenger's bid.
118
+ return Math.min(next.bidDisplay, raw);
119
+ });
120
+ // Owner-take is realized only by the LAST contester in the chain — every
121
+ // earlier contester gets their bid back as refund when outbid, plus a
122
+ // bonus from the next challenger's bid. Net contract escrow at end of
123
+ // chain = bid_last − Σ bonuses paid across the chain (the deficit on
124
+ // each refund is covered by the next challenger's bid). Intermediate
125
+ // rows therefore contribute 0 to owner take.
126
+ const totalBonusForOwner = bonusByIdx.reduce((acc, b) => acc + b, 0);
127
+ const lastIdx = tmp.length - 1;
128
+ let contestedCount = contestedCountStart;
129
+ let bumpFlagged = false;
130
+ for (let i = 0; i < tmp.length; i++) {
131
+ const t = tmp[i];
132
+ const bonusEarned = bonusByIdx[i];
133
+ const ownerTake = i === lastIdx ? Math.max(0, t.bidDisplay - totalBonusForOwner) : 0;
134
+ contestedCount = contestedCount + 1;
135
+ const isInitial = i === 0;
136
+ const triggersRateBump = !bumpFlagged && contestedThreshold > 0 && contestedCount >= contestedThreshold && !isInitial;
137
+ if (triggersRateBump)
138
+ bumpFlagged = true;
139
+ rows.push({
140
+ id: t.id,
141
+ idx: i + 1,
142
+ atDay: t.atDay,
143
+ floorStardust: t.bidStardust,
144
+ floorDisplay: t.bidDisplay,
145
+ bonusDisplay: bonusEarned,
146
+ ownerTakeDisplay: ownerTake,
147
+ defenderBidStardust: t.defenderBidStardust,
148
+ defenderAtDay: t.defenderAtDay,
149
+ source: t.source,
150
+ isInitial,
151
+ contestedCountAfter: contestedCount,
152
+ triggersRateBump,
153
+ });
154
+ }
155
+ const baselineAtlas = (Number(rateStardust) / stardustPerAtlas) * activeDays;
156
+ const baselineDisplay = displayCur === 'ATLAS'
157
+ ? baselineAtlas
158
+ : Math.max(baselineAtlas / atlasPerPoint, minCaptureThresholdPoints);
159
+ const totalOwnerDisplay = rows.reduce((acc, r) => acc + r.ownerTakeDisplay, 0);
160
+ const totalBonusDisplay = rows.reduce((acc, r) => acc + r.bonusDisplay, 0);
161
+ const multiplier = baselineDisplay > 0 ? totalOwnerDisplay / baselineDisplay : 0;
162
+ return {
163
+ segments,
164
+ rows,
165
+ totalOwnerDisplay,
166
+ totalBonusDisplay,
167
+ baselineDisplay,
168
+ multiplier,
169
+ };
170
+ }
171
+ /** Even spacing across [from, to] excluding endpoints. */
172
+ export function distributeEven(count, from, to) {
173
+ if (count <= 0 || to <= from)
174
+ return [];
175
+ const step = (to - from) / (count + 1);
176
+ return Array.from({ length: count }, (_, i) => from + step * (i + 1));
177
+ }
178
+ /** Quadratic bias toward `to` — clusters knobs near the snipe window. */
179
+ export function distributeSnipeHeavy(count, from, to) {
180
+ if (count <= 0 || to <= from)
181
+ return [];
182
+ const span = to - from;
183
+ return Array.from({ length: count }, (_, i) => {
184
+ const t = (i + 1) / (count + 1);
185
+ const skewed = 1 - Math.pow(1 - t, 2);
186
+ return from + span * skewed;
187
+ });
188
+ }
189
+ let knobCounter = 0;
190
+ export const nextKnobId = () => `k${++knobCounter}`;
191
+ //# sourceMappingURL=contestChain.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contestChain.js","sourceRoot":"","sources":["../../../src/utils/contestChain.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,201 @@
1
+ /**
2
+ * @purpose Aggregated point-in-time view of a contract and its rentals
3
+ *
4
+ * One-shot fetch that pulls every on-chain account relevant to a single
5
+ * contract — `ConfigState`, `ContractState`, and the active / queued
6
+ * `RentalState`s (when they exist) — and also computes the contest
7
+ * minimum-bid floor evaluated at "now". The raw account states are
8
+ * exposed verbatim so callers decide what matters; `minimumBid` is the
9
+ * one piece of derived data that nearly every consumer of this snapshot
10
+ * needs.
11
+ *
12
+ * Granular `fetchConfig` / `fetchContract` / `fetchRental` remain
13
+ * available for narrow lookups that don't need the whole picture.
14
+ */
15
+ import { address } from '@solana/kit';
16
+ import { fetchConfig } from '../accounts/config';
17
+ import { fetchContract } from '../accounts/contract';
18
+ import { fetchRental } from '../accounts/rental';
19
+ import { deriveActiveRental, deriveQueuedRental } from '../pda/srsly';
20
+ import { getRpcUrl } from './config';
21
+ import { computeMinimumBid } from './bidMath';
22
+ const SYSTEM_PROGRAM_ID = '11111111111111111111111111111111';
23
+ /**
24
+ * Fetch a contract and every account directly attached to it in one shot,
25
+ * then derive the contest minimum-bid floor at "now".
26
+ *
27
+ * Strategy: fetch `ConfigState` and `ContractState` in parallel, then read
28
+ * the contract's `activeRental` / `queuedRental` pointers and fetch
29
+ * whichever ones exist (in parallel). Pointers that equal SystemProgram
30
+ * are skipped without an RPC call. The bid floor is computed locally
31
+ * once all states are in.
32
+ *
33
+ * @example
34
+ * ```ts
35
+ * const snap = await getContractSnapshot({ contractAddress: 'Abc...' });
36
+ * const rate = snap.contract.data.rate;
37
+ * const defenderBid = snap.queuedRental?.data.bidAtlas ?? 0n;
38
+ * const floorNow = snap.minimumBid.minBidStardust;
39
+ *
40
+ * // Project the floor 4 hours forward without re-fetching:
41
+ * const floorLater = computeMinimumBidFromSnapshot(snap, {
42
+ * delaySeconds: 4 * 60 * 60,
43
+ * });
44
+ * ```
45
+ */
46
+ export async function getContractSnapshot(args) {
47
+ const rpcUrl = args.rpcUrl ?? getRpcUrl();
48
+ const [config, contract] = await Promise.all([
49
+ fetchConfig(rpcUrl),
50
+ fetchContract(args.contractAddress, rpcUrl),
51
+ ]);
52
+ const contractData = contract.data;
53
+ const activePromise = contractData.activeRental && contractData.activeRental.toString() !== SYSTEM_PROGRAM_ID
54
+ ? deriveActiveRental(contract.address)
55
+ .then(addr => fetchRental(addr, rpcUrl))
56
+ .catch(() => null)
57
+ : Promise.resolve(null);
58
+ const queuedPromise = contractData.queuedRental && contractData.queuedRental.toString() !== SYSTEM_PROGRAM_ID
59
+ ? deriveQueuedRental(contract.address)
60
+ .then(addr => fetchRental(addr, rpcUrl))
61
+ .catch(() => null)
62
+ : Promise.resolve(null);
63
+ const [activeRental, queuedRental] = await Promise.all([activePromise, queuedPromise]);
64
+ const partialSnap = { config, contract, activeRental, queuedRental };
65
+ const minimumBid = computeMinimumBidFromSnapshot(partialSnap);
66
+ return { ...partialSnap, minimumBid };
67
+ }
68
+ /**
69
+ * Resolve a `ContractSnapshot` from caller-supplied data or fetch fresh.
70
+ *
71
+ * Designed for callers (e.g. UIs) that already have a snapshot in hand and
72
+ * want to skip the RPC roundtrip when building an instruction. Behavior:
73
+ *
74
+ * 1. If no snapshot is supplied → fetch a fresh one.
75
+ * 2. If snapshot's contract address doesn't match `contractAddress` →
76
+ * throw immediately (programmer error).
77
+ * 3. If snapshot's rental fields are stale relative to the contract's
78
+ * pointers (contract says a rental exists but snapshot has `null` —
79
+ * typical when the snapshot was fetched mid-state-change) → lazy
80
+ * refetch the snapshot once.
81
+ * 4. If still stale after the refetch → throw. The on-chain reality
82
+ * conflicts with itself; let the caller decide how to recover.
83
+ */
84
+ export async function resolveContractSnapshot(args) {
85
+ if (!args.snapshot) {
86
+ return getContractSnapshot({
87
+ contractAddress: args.contractAddress,
88
+ rpcUrl: args.rpcUrl,
89
+ });
90
+ }
91
+ const requested = address(args.contractAddress);
92
+ if (args.snapshot.contract.address !== requested) {
93
+ throw new Error(`Provided snapshot is for contract ${args.snapshot.contract.address}, expected ${requested}`);
94
+ }
95
+ if (!isSnapshotStale(args.snapshot)) {
96
+ return args.snapshot;
97
+ }
98
+ const fresh = await getContractSnapshot({
99
+ contractAddress: args.contractAddress,
100
+ rpcUrl: args.rpcUrl,
101
+ });
102
+ if (isSnapshotStale(fresh)) {
103
+ throw new Error(`Contract ${requested} has a rental pointer set but the rental account could not be loaded after lazy refetch`);
104
+ }
105
+ return fresh;
106
+ }
107
+ function isSnapshotStale(snap) {
108
+ const activeStale = snap.contract.data.activeRental.toString() !== SYSTEM_PROGRAM_ID && !snap.activeRental;
109
+ const queuedStale = snap.contract.data.queuedRental.toString() !== SYSTEM_PROGRAM_ID && !snap.queuedRental;
110
+ return activeStale || queuedStale;
111
+ }
112
+ /**
113
+ * Pure derivation of `MinimumBid` from an already-fetched snapshot. No RPC.
114
+ *
115
+ * Use when the caller has a snapshot and wants the floor at a custom time
116
+ * projection (`delaySeconds` / `nowSeconds`) or with an overridden
117
+ * `atlasPerPoint` ratio. For the at-fetch-time floor, read
118
+ * `snap.minimumBid` directly.
119
+ */
120
+ export function computeMinimumBidFromSnapshot(snap, opts = {}) {
121
+ const cfgData = snap.config.data;
122
+ const contractData = snap.contract.data;
123
+ const rateStardust = BigInt(contractData.rate);
124
+ const atlasPerPoint = opts.atlasPerPoint ?? BigInt(cfgData.atlasPerPoint);
125
+ const stardustPerAtlas = BigInt(cfgData.stardustToAtlas);
126
+ const activeStart = snap.activeRental ? Number(snap.activeRental.data.startTime) : null;
127
+ const activeEnd = snap.activeRental ? Number(snap.activeRental.data.endTime) : null;
128
+ let defenderBidStardust = 0n;
129
+ if (snap.queuedRental) {
130
+ const q = snap.queuedRental.data;
131
+ const bidAtlas = BigInt(q.bidAtlas);
132
+ const bidPoints = BigInt(q.bidPoints);
133
+ // Points are 8-decimal dust; converting to stardust requires the same
134
+ // 8-decimal precision: stardust = points_dust * atlasPerPoint *
135
+ // stardustPerAtlas / 1e8. With both at 1e8 precision this collapses
136
+ // to points_dust * atlasPerPoint.
137
+ const pointsAsStardust = bidPoints > 0n ? (bidPoints * atlasPerPoint * stardustPerAtlas) / 100000000n : 0n;
138
+ defenderBidStardust = bidAtlas + pointsAsStardust;
139
+ }
140
+ const evaluatedAtSeconds = opts.nowSeconds !== undefined
141
+ ? Math.floor(opts.nowSeconds)
142
+ : Math.floor(Date.now() / 1000) + Math.max(0, opts.delaySeconds ?? 0);
143
+ let closeness = 0;
144
+ if (activeStart !== null && activeEnd !== null && activeEnd > activeStart) {
145
+ const span = activeEnd - activeStart;
146
+ const elapsed = evaluatedAtSeconds - activeStart;
147
+ closeness = Math.max(0, Math.min(1, elapsed / span));
148
+ }
149
+ const minBidStardust = computeMinimumBid({
150
+ rateStardust,
151
+ defenderBidStardust,
152
+ contestedMultiplierMaxBps: cfgData.contestedMultiplierMaxBps,
153
+ captureRateBps: BigInt(cfgData.captureRateBps),
154
+ closenessFraction: closeness,
155
+ });
156
+ const baseStardust = rateStardust > defenderBidStardust ? rateStardust : defenderBidStardust;
157
+ const rampMultiplier = baseStardust > 0n ? Number(minBidStardust) / Number(baseStardust) : 1;
158
+ return {
159
+ minBidStardust,
160
+ defenderBidStardust,
161
+ rateStardust,
162
+ closenessFraction: closeness,
163
+ rampMultiplier,
164
+ evaluatedAtSeconds,
165
+ activeEndSeconds: activeEnd,
166
+ activeStartSeconds: activeStart,
167
+ };
168
+ }
169
+ /**
170
+ * Convenience wrapper: fetch a snapshot and return only its `minimumBid`.
171
+ *
172
+ * Same fetches as `getContractSnapshot` — if you need the contract / rental
173
+ * states too, call `getContractSnapshot` directly so you don't fetch twice.
174
+ *
175
+ * For time-projected floors, pass `delaySeconds` or `nowSeconds`; the
176
+ * underlying fetch is identical.
177
+ */
178
+ export async function getMinimumBid(args) {
179
+ if (args.snapshot) {
180
+ return computeMinimumBidFromSnapshot(args.snapshot, {
181
+ delaySeconds: args.delaySeconds,
182
+ nowSeconds: args.nowSeconds,
183
+ atlasPerPoint: args.atlasPerPoint,
184
+ });
185
+ }
186
+ const snap = await getContractSnapshot({
187
+ contractAddress: args.contractAddress,
188
+ rpcUrl: args.rpcUrl,
189
+ });
190
+ if (args.delaySeconds === undefined &&
191
+ args.nowSeconds === undefined &&
192
+ args.atlasPerPoint === undefined) {
193
+ return snap.minimumBid;
194
+ }
195
+ return computeMinimumBidFromSnapshot(snap, {
196
+ delaySeconds: args.delaySeconds,
197
+ nowSeconds: args.nowSeconds,
198
+ atlasPerPoint: args.atlasPerPoint,
199
+ });
200
+ }
201
+ //# sourceMappingURL=contractSnapshot.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contractSnapshot.js","sourceRoot":"","sources":["../../../src/utils/contractSnapshot.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,245 @@
1
+ import { describe, expect, it, vi, beforeEach } from 'vitest';
2
+ vi.mock('../accounts/config', () => ({
3
+ fetchConfig: vi.fn(),
4
+ }));
5
+ vi.mock('../accounts/contract', () => ({
6
+ fetchContract: vi.fn(),
7
+ }));
8
+ vi.mock('../accounts/rental', () => ({
9
+ fetchRental: vi.fn(),
10
+ }));
11
+ vi.mock('../pda/srsly', () => ({
12
+ deriveActiveRental: vi.fn(),
13
+ deriveQueuedRental: vi.fn(),
14
+ }));
15
+ vi.mock('./config', () => ({
16
+ getRpcUrl: () => 'http://localhost:8899',
17
+ }));
18
+ import { fetchConfig } from '../accounts/config';
19
+ import { fetchContract } from '../accounts/contract';
20
+ import { fetchRental } from '../accounts/rental';
21
+ import { deriveActiveRental, deriveQueuedRental } from '../pda/srsly';
22
+ import { getContractSnapshot, resolveContractSnapshot, computeMinimumBidFromSnapshot, } from './contractSnapshot';
23
+ const SYSTEM = '11111111111111111111111111111111';
24
+ // Real validated 32-byte base58 addresses for test fixtures.
25
+ const CONTRACT_ADDR = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v';
26
+ const ACTIVE_ADDR = 'So11111111111111111111111111111111111111112';
27
+ const QUEUED_ADDR = 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA';
28
+ const OTHER_ADDR = 'BPFLoader2111111111111111111111111111111111';
29
+ const VAULT_ADDR = 'ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL';
30
+ const makeConfig = (overrides = {}) => ({
31
+ stardustToAtlas: 100000000n,
32
+ atlasPerPoint: 100n,
33
+ captureRateBps: 10500n,
34
+ contestedMultiplierMaxBps: 465,
35
+ slyvault: VAULT_ADDR,
36
+ atlasMint: VAULT_ADDR,
37
+ ...overrides,
38
+ });
39
+ const makeContract = (overrides = {}) => ({
40
+ rate: 100000000n,
41
+ activeRental: SYSTEM,
42
+ queuedRental: SYSTEM,
43
+ fleet: VAULT_ADDR,
44
+ gameId: VAULT_ADDR,
45
+ thread: SYSTEM,
46
+ ...overrides,
47
+ });
48
+ const makeRental = (overrides = {}) => ({
49
+ bidAtlas: 0n,
50
+ bidPoints: 0n,
51
+ startTime: 0n,
52
+ endTime: 0n,
53
+ ...overrides,
54
+ });
55
+ beforeEach(() => {
56
+ vi.clearAllMocks();
57
+ vi.mocked(deriveActiveRental).mockResolvedValue(ACTIVE_ADDR);
58
+ vi.mocked(deriveQueuedRental).mockResolvedValue(QUEUED_ADDR);
59
+ });
60
+ describe('getContractSnapshot', () => {
61
+ it('returns null rentals when contract pointers are SystemProgram', async () => {
62
+ vi.mocked(fetchConfig).mockResolvedValue({ data: makeConfig(), address: 'CFG' });
63
+ vi.mocked(fetchContract).mockResolvedValue({
64
+ data: makeContract(),
65
+ address: CONTRACT_ADDR,
66
+ });
67
+ const snap = await getContractSnapshot({ contractAddress: CONTRACT_ADDR });
68
+ expect(snap.activeRental).toBeNull();
69
+ expect(snap.queuedRental).toBeNull();
70
+ expect(fetchRental).not.toHaveBeenCalled();
71
+ expect(snap.minimumBid).toBeDefined();
72
+ });
73
+ it('fetches active + queued when contract pointers are set', async () => {
74
+ vi.mocked(fetchConfig).mockResolvedValue({ data: makeConfig(), address: 'CFG' });
75
+ vi.mocked(fetchContract).mockResolvedValue({
76
+ data: makeContract({ activeRental: ACTIVE_ADDR, queuedRental: QUEUED_ADDR }),
77
+ address: CONTRACT_ADDR,
78
+ });
79
+ vi.mocked(fetchRental).mockImplementation(async (addr) => ({
80
+ data: makeRental(),
81
+ address: addr,
82
+ }));
83
+ const snap = await getContractSnapshot({ contractAddress: CONTRACT_ADDR });
84
+ expect(snap.activeRental?.address).toBe(ACTIVE_ADDR);
85
+ expect(snap.queuedRental?.address).toBe(QUEUED_ADDR);
86
+ expect(fetchRental).toHaveBeenCalledTimes(2);
87
+ });
88
+ it('returns null when pointer is set but fetchRental throws (stale pointer)', async () => {
89
+ vi.mocked(fetchConfig).mockResolvedValue({ data: makeConfig(), address: 'CFG' });
90
+ vi.mocked(fetchContract).mockResolvedValue({
91
+ data: makeContract({ activeRental: ACTIVE_ADDR }),
92
+ address: CONTRACT_ADDR,
93
+ });
94
+ vi.mocked(fetchRental).mockRejectedValue(new Error('not found'));
95
+ const snap = await getContractSnapshot({ contractAddress: CONTRACT_ADDR });
96
+ expect(snap.activeRental).toBeNull();
97
+ expect(snap.queuedRental).toBeNull();
98
+ });
99
+ });
100
+ describe('computeMinimumBidFromSnapshot', () => {
101
+ const buildSnap = (parts) => ({
102
+ config: { data: makeConfig(), address: 'CFG' },
103
+ contract: {
104
+ data: makeContract({
105
+ rate: parts.rate ?? 100000000n,
106
+ activeRental: parts.activeStart !== undefined ? ACTIVE_ADDR : SYSTEM,
107
+ queuedRental: parts.queuedBidAtlas !== undefined || parts.queuedBidPoints !== undefined
108
+ ? QUEUED_ADDR
109
+ : SYSTEM,
110
+ }),
111
+ address: CONTRACT_ADDR,
112
+ },
113
+ activeRental: parts.activeStart !== undefined
114
+ ? {
115
+ data: makeRental({
116
+ startTime: parts.activeStart,
117
+ endTime: parts.activeEnd ?? parts.activeStart + 86400n,
118
+ }),
119
+ address: ACTIVE_ADDR,
120
+ }
121
+ : null,
122
+ queuedRental: parts.queuedBidAtlas !== undefined || parts.queuedBidPoints !== undefined
123
+ ? {
124
+ data: makeRental({
125
+ bidAtlas: parts.queuedBidAtlas ?? 0n,
126
+ bidPoints: parts.queuedBidPoints ?? 0n,
127
+ }),
128
+ address: QUEUED_ADDR,
129
+ }
130
+ : null,
131
+ });
132
+ it('no defender → floor = rate × captureRate cap', async () => {
133
+ const snap = buildSnap({ rate: 100000000n });
134
+ const bid = computeMinimumBidFromSnapshot(snap);
135
+ expect(bid.defenderBidStardust).toBe(0n);
136
+ // rate * captureRateBps / baseBps = 100M * 10500 / 10000 = 105M
137
+ expect(bid.minBidStardust).toBe(105000000n);
138
+ expect(bid.closenessFraction).toBe(0);
139
+ });
140
+ it('defender ATLAS bid raises floor above rate when bid > rate', async () => {
141
+ const snap = buildSnap({ rate: 100000000n, queuedBidAtlas: 200000000n });
142
+ const bid = computeMinimumBidFromSnapshot(snap);
143
+ expect(bid.defenderBidStardust).toBe(200000000n);
144
+ // base = max(rate, defender) = 200M, ramp at closeness=0 capped to 10500/10000 = 1.05x
145
+ expect(bid.minBidStardust).toBe(210000000n);
146
+ });
147
+ it('closeness ramps from 1× to maxMult as active rental nears end', async () => {
148
+ const now = 1_000_000;
149
+ const start = BigInt(now - 50);
150
+ const end = BigInt(now + 50);
151
+ const snap = buildSnap({
152
+ rate: 100000000n,
153
+ queuedBidAtlas: 100000000n,
154
+ activeStart: start,
155
+ activeEnd: end,
156
+ });
157
+ const bid = computeMinimumBidFromSnapshot(snap, { nowSeconds: now });
158
+ // closeness = 50/100 = 0.5
159
+ expect(bid.closenessFraction).toBeCloseTo(0.5, 5);
160
+ // ramp_bps = 10000 + (465 - 100) * 5000 / 100 = 10000 + 18250 = 28250 → 2.825×
161
+ // floor = 100M * 28250 / 10000 = 282_500_000
162
+ expect(bid.minBidStardust).toBe(282500000n);
163
+ });
164
+ it('delaySeconds projects closeness forward without RPC', async () => {
165
+ const start = 1000000n;
166
+ const end = 1000100n;
167
+ const snap = buildSnap({
168
+ rate: 100000000n,
169
+ queuedBidAtlas: 100000000n,
170
+ activeStart: start,
171
+ activeEnd: end,
172
+ });
173
+ const now = computeMinimumBidFromSnapshot(snap, { nowSeconds: 1_000_000 });
174
+ const later = computeMinimumBidFromSnapshot(snap, { nowSeconds: 1_000_050 });
175
+ expect(now.closenessFraction).toBe(0);
176
+ expect(later.closenessFraction).toBeCloseTo(0.5, 5);
177
+ expect(later.minBidStardust).toBeGreaterThan(now.minBidStardust);
178
+ });
179
+ });
180
+ describe('resolveContractSnapshot', () => {
181
+ it('fetches fresh when no snapshot provided', async () => {
182
+ vi.mocked(fetchConfig).mockResolvedValue({ data: makeConfig(), address: 'CFG' });
183
+ vi.mocked(fetchContract).mockResolvedValue({
184
+ data: makeContract(),
185
+ address: CONTRACT_ADDR,
186
+ });
187
+ await resolveContractSnapshot({ contractAddress: CONTRACT_ADDR });
188
+ expect(fetchContract).toHaveBeenCalledTimes(1);
189
+ });
190
+ it('returns provided snapshot as-is when address matches and no staleness', async () => {
191
+ vi.mocked(fetchConfig).mockResolvedValue({ data: makeConfig(), address: 'CFG' });
192
+ vi.mocked(fetchContract).mockResolvedValue({
193
+ data: makeContract(),
194
+ address: CONTRACT_ADDR,
195
+ });
196
+ const snap = await getContractSnapshot({ contractAddress: CONTRACT_ADDR });
197
+ vi.clearAllMocks();
198
+ const resolved = await resolveContractSnapshot({
199
+ contractAddress: CONTRACT_ADDR,
200
+ snapshot: snap,
201
+ });
202
+ expect(resolved).toBe(snap);
203
+ expect(fetchContract).not.toHaveBeenCalled();
204
+ });
205
+ it('throws when snapshot contract address does not match', async () => {
206
+ vi.mocked(fetchConfig).mockResolvedValue({ data: makeConfig(), address: VAULT_ADDR });
207
+ vi.mocked(fetchContract).mockResolvedValue({ data: makeContract(), address: OTHER_ADDR });
208
+ const snap = await getContractSnapshot({ contractAddress: OTHER_ADDR });
209
+ await expect(resolveContractSnapshot({ contractAddress: CONTRACT_ADDR, snapshot: snap })).rejects.toThrow(/snapshot is for contract/);
210
+ });
211
+ it('lazy-refetches when snapshot is stale (pointer set, rental null)', async () => {
212
+ // First fetch: pointer set but rental fetch fails → snap.activeRental = null (stale)
213
+ vi.mocked(fetchConfig).mockResolvedValue({ data: makeConfig(), address: 'CFG' });
214
+ vi.mocked(fetchContract).mockResolvedValue({
215
+ data: makeContract({ activeRental: ACTIVE_ADDR }),
216
+ address: CONTRACT_ADDR,
217
+ });
218
+ vi.mocked(fetchRental).mockRejectedValueOnce(new Error('not found'));
219
+ const staleSnap = await getContractSnapshot({ contractAddress: CONTRACT_ADDR });
220
+ expect(staleSnap.activeRental).toBeNull();
221
+ // Refetch succeeds — rental now loads
222
+ vi.mocked(fetchRental).mockResolvedValueOnce({
223
+ data: makeRental(),
224
+ address: ACTIVE_ADDR,
225
+ });
226
+ const resolved = await resolveContractSnapshot({
227
+ contractAddress: CONTRACT_ADDR,
228
+ snapshot: staleSnap,
229
+ });
230
+ expect(resolved.activeRental).not.toBeNull();
231
+ // resolve called fetchContract once more for refetch
232
+ expect(fetchContract).toHaveBeenCalledTimes(2);
233
+ });
234
+ it('throws when still stale after lazy refetch', async () => {
235
+ vi.mocked(fetchConfig).mockResolvedValue({ data: makeConfig(), address: 'CFG' });
236
+ vi.mocked(fetchContract).mockResolvedValue({
237
+ data: makeContract({ activeRental: ACTIVE_ADDR }),
238
+ address: CONTRACT_ADDR,
239
+ });
240
+ vi.mocked(fetchRental).mockRejectedValue(new Error('not found'));
241
+ const staleSnap = await getContractSnapshot({ contractAddress: CONTRACT_ADDR });
242
+ await expect(resolveContractSnapshot({ contractAddress: CONTRACT_ADDR, snapshot: staleSnap })).rejects.toThrow(/could not be loaded after lazy refetch/);
243
+ });
244
+ });
245
+ //# sourceMappingURL=contractSnapshot.test.js.map