@render-foundation/utils 0.0.249 → 0.0.251

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.
@@ -35,7 +35,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
35
35
  return (mod && mod.__esModule) ? mod : { "default": mod };
36
36
  };
37
37
  Object.defineProperty(exports, "__esModule", { value: true });
38
- exports.pgClient = exports.batchFinishedAtMedian = exports.foldGrantAllotment = exports.foldTimeAware = exports.CREDIT_DECIMALS = exports.GRANT_WINDOW_START = exports.EUR_TO_CREDITS = exports.DISPERSED_BURN_CORRECTION_TABLE = exports.DISPERSED_SETTLEMENT_TABLE = exports.GRANT_BURN_TABLE = exports.GRANT_BURN_ID_TABLE = exports.OTOY_GRANT_TABLE = exports.USER_GRANT_SPEND_TABLE = exports.CURRENT_USER_GRANT_SPEND_TABLE = exports.POLYGON_UPGRADE_TABLE = exports.ENTITY_EPOCH_INFO_TABLE = exports.BURN_CORRECTION_TABLE = exports.NETWORK_REVENUE_TABLE = exports.BRIDGE_TRANSFER_TABLE = exports.BURN_TABLE = exports.JOB_TABLE = exports.JOB_ID_TABLE = exports.LIABILITY_ADJUSTMENT_BATCH_TABLE = exports.LIABILITY_ADJUSTMENT_TABLE = exports.LIABILITY_TABLE = exports.SOL_TRANSFER_TABLE = exports.ENTITY_TABLE = exports.MANUAL_BURN_TABLE = exports.EPOCH_TABLE = exports.SOL_TX_TABLE = void 0;
38
+ exports.pgClient = exports.batchFinishedAtMedian = exports.foldGrantWindow = exports.GRANT_LOOKBACK_MS = exports.GRANT_LOOKBACK_DAYS = exports.CREDIT_DECIMALS = exports.GRANT_WINDOW_START = exports.EUR_TO_CREDITS = exports.DISPERSED_BURN_CORRECTION_TABLE = exports.DISPERSED_SETTLEMENT_TABLE = exports.GRANT_BURN_TABLE = exports.GRANT_BURN_ID_TABLE = exports.OTOY_GRANT_TABLE = exports.USER_GRANT_SPEND_TABLE = exports.CURRENT_USER_GRANT_SPEND_TABLE = exports.POLYGON_UPGRADE_TABLE = exports.ENTITY_EPOCH_INFO_TABLE = exports.BURN_CORRECTION_TABLE = exports.NETWORK_REVENUE_TABLE = exports.BRIDGE_TRANSFER_TABLE = exports.BURN_TABLE = exports.JOB_TABLE = exports.JOB_ID_TABLE = exports.LIABILITY_ADJUSTMENT_BATCH_TABLE = exports.LIABILITY_ADJUSTMENT_TABLE = exports.LIABILITY_TABLE = exports.SOL_TRANSFER_TABLE = exports.ENTITY_TABLE = exports.MANUAL_BURN_TABLE = exports.EPOCH_TABLE = exports.SOL_TX_TABLE = void 0;
39
39
  const kysely_1 = require("kysely");
40
40
  const logger_1 = require("../../logger");
41
41
  const pg_1 = require("pg");
@@ -76,73 +76,112 @@ exports.EUR_TO_CREDITS = BigInt(4);
76
76
  // emissions-funded ones (counting only emissions-attributed spend over-issued ~2.58M credits, 2026-08-04).
77
77
  exports.GRANT_WINDOW_START = '2025-05-01';
78
78
  exports.CREDIT_DECIMALS = BigInt(100000000); // 1e8
79
- /**
80
- * Pure core of the grant allotment: fold a user's OTOY grants against what's already been spent.
81
- *
82
- * consumed = attributedSpend (spend already stamped with an otoy_grant_id)
83
- * + correctionSettled (jobs whose grant is already being settled by a burn_correction)
84
- *
85
- * The correctionSettled term is the DOUBLE-SPEND GUARD: an escrow_credit correction means we're
86
- * recovering that job's dollars from escrow, so its grant is already committed. Counting it here stops
87
- * the same grant from also funding a future emissions burn.
88
- *
89
- * fifoGrantId = the oldest grant still holding room, i.e. the one the next spend draws from.
90
- */
91
- /**
92
- * Time-aware reservoir fold. Events = grant issuances (+, at issue time) and job spend (−, at job time),
93
- * processed chronologically: a job may only draw entitlement that existed WHEN IT RAN. Spend beyond the
94
- * balance at that moment is purchase-paid forever — a later grant must not retroactively absorb it
95
- * (the aggregate `granted − allSpend` formula did exactly that, under-issuing 256 users / 44,243 credits;
96
- * it also let the UI attribute jobs to grants issued days after they ran).
97
- *
98
- * Identity used everywhere (SQL + client): with net = Σamt and overshoot = max prefix deficit,
99
- * avail = max(net + overshoot, 0); beyondGrants = overshoot.
100
- */
101
- const foldTimeAware = (events) => {
102
- // True reservoir semantics, chronological:
103
- // grant + -> capacity += amt
104
- // grant − -> capacity = max(capacity + amt, 0) (a REVERSAL claws back capacity; it is not spend
105
- // and can never create "beyond grants" — the linear min-run identity got this wrong:
106
- // a reversal ordered before its covering positives produced phantom overshoot)
107
- // job -> draw min(need, capacity); the un-drawable remainder is purchase-paid forever
79
+ // A grant is consumable for this long after issuance; whatever is left after that EXPIRES (2026-08-05
80
+ // model change). A job can only draw grants issued within the lookback window before it ran.
81
+ exports.GRANT_LOOKBACK_DAYS = 60;
82
+ exports.GRANT_LOOKBACK_MS = exports.GRANT_LOOKBACK_DAYS * 24 * 3600 * 1000;
83
+ const foldGrantWindow = (events, opts = {}) => {
84
+ var _a, _b, _c;
85
+ const lookback = (_a = opts.lookbackMs) !== null && _a !== void 0 ? _a : exports.GRANT_LOOKBACK_MS;
86
+ const now = (_b = opts.now) !== null && _b !== void 0 ? _b : Date.now();
108
87
  const sorted = [...events].sort((a, b) => a.t - b.t || (a.amt > b.amt ? -1 : 1)); // credits first on ties
109
- let cap = BigInt(0);
88
+ const buckets = [];
89
+ const jobs = [];
90
+ const remaining = (b) => b.amount - b.reversed - b.drawnEmissions - b.drawnMisrouted;
110
91
  let beyond = BigInt(0);
111
- let charged = BigInt(0);
112
92
  for (const e of sorted) {
113
93
  if (e.src === 'grant') {
114
- cap += e.amt;
115
- if (cap < BigInt(0))
116
- cap = BigInt(0);
94
+ if (e.amt > BigInt(0)) {
95
+ buckets.push({
96
+ grantId: e.grantId,
97
+ t: e.t,
98
+ amount: e.amt,
99
+ reversed: BigInt(0),
100
+ drawnEmissions: BigInt(0),
101
+ drawnMisrouted: BigInt(0),
102
+ expired: BigInt(0),
103
+ left: BigInt(0),
104
+ });
105
+ }
106
+ else {
107
+ let claw = -e.amt; // clawback hits remaining capacity oldest-first; excess is dropped
108
+ for (const b of buckets) {
109
+ if (claw <= BigInt(0))
110
+ break;
111
+ const take = remaining(b) < claw ? remaining(b) : claw;
112
+ if (take > BigInt(0)) {
113
+ b.reversed += take;
114
+ claw -= take;
115
+ }
116
+ }
117
+ }
117
118
  }
118
119
  else {
119
- const need = -e.amt; // job events are negative
120
- const take = need < cap ? need : cap;
121
- cap -= take;
122
- charged += take;
123
- beyond += need - take;
120
+ let need = -e.amt;
121
+ const total = need;
122
+ let emissionsLeft = (_c = e.emissions) !== null && _c !== void 0 ? _c : need; // no split provided -> count it all as emissions
123
+ let jobEm = BigInt(0);
124
+ let jobMis = BigInt(0);
125
+ for (const b of buckets) {
126
+ if (need <= BigInt(0))
127
+ break;
128
+ if (b.t + lookback < e.t)
129
+ continue; // grant expired before this job ran
130
+ const take = remaining(b) < need ? remaining(b) : need;
131
+ if (take <= BigInt(0))
132
+ continue;
133
+ const em = emissionsLeft < take ? emissionsLeft : take;
134
+ b.drawnEmissions += em;
135
+ b.drawnMisrouted += take - em;
136
+ jobEm += em;
137
+ jobMis += take - em;
138
+ emissionsLeft -= em;
139
+ need -= take;
140
+ }
141
+ beyond += need;
142
+ jobs.push({
143
+ id: e.id,
144
+ t: e.t,
145
+ need: total,
146
+ drawnEmissions: jobEm,
147
+ drawnMisrouted: jobMis,
148
+ beyond: need,
149
+ });
124
150
  }
125
151
  }
126
- return { avail: cap, beyondGrants: beyond, jobCharged: charged };
127
- };
128
- exports.foldTimeAware = foldTimeAware;
129
- const foldGrantAllotment = (grants, attributedSpend, correctionSettled) => {
130
- const granted = grants.reduce((s, g) => s + g.amountCredits, BigInt(0));
131
- const consumed = (attributedSpend > BigInt(0) ? attributedSpend : BigInt(0)) +
132
- (correctionSettled > BigInt(0) ? correctionSettled : BigInt(0));
133
- const avail = granted > consumed ? granted - consumed : BigInt(0);
134
- let drawn = consumed;
152
+ let avail = BigInt(0);
153
+ let expired = BigInt(0);
154
+ let usedEmissions = BigInt(0);
155
+ let usedMisrouted = BigInt(0);
135
156
  let fifoGrantId;
136
- for (const g of grants) {
137
- if (drawn < g.amountCredits) {
138
- fifoGrantId = g.id;
139
- break;
157
+ for (const b of buckets) {
158
+ usedEmissions += b.drawnEmissions;
159
+ usedMisrouted += b.drawnMisrouted;
160
+ const rem = remaining(b);
161
+ if (b.t + lookback < now) {
162
+ b.expired = rem;
140
163
  }
141
- drawn -= g.amountCredits;
164
+ else {
165
+ b.left = rem;
166
+ avail += rem;
167
+ if (fifoGrantId === undefined && rem > BigInt(0) && b.grantId !== undefined)
168
+ fifoGrantId = b.grantId;
169
+ }
170
+ expired += b.expired;
142
171
  }
143
- return { granted, consumed, avail, fifoGrantId };
172
+ return {
173
+ avail,
174
+ beyondGrants: beyond,
175
+ jobCharged: usedEmissions + usedMisrouted,
176
+ usedEmissions,
177
+ usedMisrouted,
178
+ expired,
179
+ buckets,
180
+ jobs,
181
+ fifoGrantId,
182
+ };
144
183
  };
145
- exports.foldGrantAllotment = foldGrantAllotment;
184
+ exports.foldGrantWindow = foldGrantWindow;
146
185
  const base_2 = require("./base");
147
186
  const moment_1 = __importDefault(require("moment"));
148
187
  pg.types.setTypeParser(1114, (str) => moment_1.default.utc(str).toDate());
@@ -274,74 +313,50 @@ const pgClient = (config) => {
274
313
  const out = {};
275
314
  if (!p.userIds.length)
276
315
  return out;
277
- const grants = yield db
278
- .selectFrom(exports.OTOY_GRANT_TABLE)
279
- .select(['id', 'user_id', 'amount_credits', 'otoy_created_at'])
280
- .where('user_id', 'in', p.userIds)
281
- .orderBy('otoy_created_at', 'asc')
282
- .orderBy('id', 'asc')
283
- .execute();
284
- if (!grants.length)
285
- return out;
286
- // EVERY grant-ledger row retires pool capacity, in either direction:
287
- // - negative delta = an emissions burn. ALL of them count, not just ones stamped with an
288
- // otoy_grant_id. Historical grants are now backfilled to cover the Sep/Oct-2025 burns, so those
289
- // burns must net against them; counting only stamped rows would re-issue ~1.06M credits of
290
- // already-spent grant.
291
- // - positive delta = the grant was CREDITED into current_user_grant_spend (the one-off canary
292
- // route). That balance reaches the split separately, so leaving it in the pool double-issues it.
293
- // Hence abs() over the whole ledger.
294
- // NOT abs() over the whole ledger — that would charge the Sep/Oct-2025 CSV *credits* as if they
295
- // were spend, double-penalising those users (464e8e33: 376,749 burned + 79,304 credited = 456,053).
296
- // A positive only retires capacity when it is tied to a pool grant (the canary route, where the
297
- // grant was moved into current_user_grant_spend and reaches the split from there).
298
- const spent = yield db
299
- .selectFrom(exports.USER_GRANT_SPEND_TABLE)
300
- .select((eb) => [
301
- 'user_id',
302
- eb.fn
303
- .sum((0, kysely_1.sql) `case when render_spent_delta < 0 then -render_spent_delta
304
- when otoy_grant_id is not null then render_spent_delta
305
- else 0 end`)
306
- .as('net'),
307
- ])
308
- .where('user_id', 'in', p.userIds)
309
- .groupBy('user_id')
310
- .execute();
311
- // Fetch the raw event stream and fold in code (foldTimeAware — reservoir semantics; reversals
312
- // clamp capacity, only jobs create beyond-grants). SQL window identities can't express the clamp.
316
+ // Raw event stream, folded in code (foldGrantWindow — per-grant buckets, 60-day consumable window,
317
+ // reversals clamp capacity, only jobs create beyond-grants). SQL can't express the bucketed clamp.
313
318
  const evRes = yield (0, kysely_1.sql) `
314
319
  select user_id::text as u, coalesce(otoy_created_at, ${exports.GRANT_WINDOW_START}::timestamp) as t,
315
- (amount_credits * 4 * 1e8)::numeric::text as amt, 'grant' as src
320
+ (amount_credits * 4 * 1e8)::numeric::text as amt, 'grant' as src, id as gid
316
321
  from otoy_grant where user_id = any(${p.userIds}::uuid[])
317
322
  union all
318
- select user_id::text, ${exports.GRANT_WINDOW_START}::timestamp, sum(render_spent_delta)::text, 'grant'
323
+ select user_id::text, ${exports.GRANT_WINDOW_START}::timestamp, sum(render_spent_delta)::text, 'grant', null
319
324
  from user_grant_spend
320
325
  where user_id = any(${p.userIds}::uuid[]) and render_spent_delta > 0 and otoy_grant_id is null
321
326
  group by user_id
322
327
  union all
323
- select user_id::text, created_at, (-render_spent_delta)::text, 'grant'
328
+ select user_id::text, created_at, (-render_spent_delta)::text, 'grant', null
324
329
  from user_grant_spend
325
330
  where user_id = any(${p.userIds}::uuid[]) and render_spent_delta > 0 and otoy_grant_id is not null
326
331
  union all
327
- select user_id, completed_at, (-render_amt)::text, 'job'
332
+ select user_id, completed_at, (-render_amt)::text, 'job', null
328
333
  from job
329
334
  where user_id = any(${p.userIds}::text[]) and completed_at >= ${exports.GRANT_WINDOW_START}::timestamp
330
335
  `.execute(db); // db is always a Kysely trx at runtime
331
336
  const byUser = new Map();
332
337
  for (const r of evRes.rows) {
333
338
  const arr = (_b = byUser.get(r.u)) !== null && _b !== void 0 ? _b : [];
334
- arr.push({ t: new Date(r.t).getTime(), amt: BigInt(Math.round(Number(r.amt))), src: r.src });
339
+ arr.push({
340
+ t: new Date(r.t).getTime(),
341
+ amt: BigInt(Math.round(Number(r.amt))),
342
+ src: r.src,
343
+ grantId: (_c = r.gid) !== null && _c !== void 0 ? _c : undefined,
344
+ });
335
345
  byUser.set(r.u, arr);
336
346
  }
337
347
  for (const [u, events] of byUser) {
338
- const cur = (_c = out[u]) !== null && _c !== void 0 ? _c : (out[u] = { userId: u, granted: BigInt(0), consumed: BigInt(0), avail: BigInt(0), grants: [] });
339
- const f = (0, exports.foldTimeAware)(events);
340
- cur.granted = events.reduce((a, e) => (e.src === 'grant' && e.amt > BigInt(0) ? a + e.amt : a), BigInt(0));
341
- cur.consumed = f.jobCharged;
342
- cur.avail = f.avail;
348
+ const f = (0, exports.foldGrantWindow)(events);
349
+ out[u] = {
350
+ userId: u,
351
+ granted: events.reduce((a, e) => (e.src === 'grant' && e.amt > BigInt(0) ? a + e.amt : a), BigInt(0)),
352
+ consumed: f.jobCharged,
353
+ avail: f.avail,
354
+ expired: f.expired,
355
+ fifoGrantId: f.fifoGrantId,
356
+ };
343
357
  if (f.beyondGrants > BigInt(0)) {
344
- log.info(`grant allotment ${u}: ${f.beyondGrants} beyond grants (purchase-paid at the time) avail ${f.avail}`);
358
+ log.info(`grant allotment ${u}: ${f.beyondGrants} beyond grants (purchase-paid at the time), ` +
359
+ `${f.expired} expired past the ${exports.GRANT_LOOKBACK_DAYS}d window — avail ${f.avail}`);
345
360
  }
346
361
  }
347
362
  return out;
@@ -1210,7 +1225,7 @@ const pgClient = (config) => {
1210
1225
  });
1211
1226
  },
1212
1227
  insertGrantSpend(db, p, log = logger_1.consoleLogger) {
1213
- var _a, _b, _c;
1228
+ var _a, _b, _c, _d;
1214
1229
  return __awaiter(this, void 0, void 0, function* () {
1215
1230
  const existing = yield getCurrentUserGrantSpend(db.trx, { userId: p.userId }, log);
1216
1231
  const diff = {
@@ -1237,16 +1252,13 @@ const pgClient = (config) => {
1237
1252
  return;
1238
1253
  }
1239
1254
  }
1240
- // FIFO-attribute this spend to the user's oldest OTOY grant that still has room. Stamping
1241
- // otoy_grant_id is what makes getGrantAllotments count it as consumed an unattributed row
1242
- // would leave the grant looking unspent and let it fund a second burn.
1255
+ // FIFO-attribute this spend to the user's oldest LIVE OTOY grant that still has room (the fold
1256
+ // already applies the lookback window). Stamping otoy_grant_id is what shows the draw in the
1257
+ // admin drill-down — an unattributed row would leave the grant looking unspent there.
1243
1258
  let otoyGrantId = p.otoyGrantId;
1244
1259
  if (otoyGrantId === undefined && p.renderSpentDelta < BigInt(0)) {
1245
1260
  const allots = yield getGrantAllotments(db.trx, { userIds: [p.userId] }, log);
1246
- const a = allots[p.userId];
1247
- if (a) {
1248
- otoyGrantId = (0, exports.foldGrantAllotment)(a.grants, a.consumed, BigInt(0)).fifoGrantId;
1249
- }
1261
+ otoyGrantId = (_c = allots[p.userId]) === null || _c === void 0 ? void 0 : _c.fifoGrantId;
1250
1262
  }
1251
1263
  const v = {
1252
1264
  user_id: p.userId,
@@ -1272,7 +1284,7 @@ const pgClient = (config) => {
1272
1284
  user_id: p.userId,
1273
1285
  render_spent: p.renderSpentDelta,
1274
1286
  description: p.description,
1275
- perpetual: (_c = p.perpetual) !== null && _c !== void 0 ? _c : false,
1287
+ perpetual: (_d = p.perpetual) !== null && _d !== void 0 ? _d : false,
1276
1288
  })
1277
1289
  .onConflict((oc) => oc.column('user_id').doUpdateSet(diff))
1278
1290
  .returning((eb) => [