@render-foundation/utils 0.0.248 → 0.0.250

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,59 +76,97 @@ 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) => {
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();
102
87
  const sorted = [...events].sort((a, b) => a.t - b.t || (a.amt > b.amt ? -1 : 1)); // credits first on ties
103
- let run = BigInt(0);
104
- let minRun = BigInt(0);
88
+ const buckets = [];
89
+ const remaining = (b) => b.amount - b.reversed - b.drawnEmissions - b.drawnMisrouted;
90
+ let beyond = BigInt(0);
105
91
  for (const e of sorted) {
106
- run += e.amt;
107
- if (run < minRun)
108
- minRun = run;
92
+ if (e.src === 'grant') {
93
+ if (e.amt > BigInt(0)) {
94
+ buckets.push({
95
+ grantId: e.grantId,
96
+ t: e.t,
97
+ amount: e.amt,
98
+ reversed: BigInt(0),
99
+ drawnEmissions: BigInt(0),
100
+ drawnMisrouted: BigInt(0),
101
+ expired: BigInt(0),
102
+ left: BigInt(0),
103
+ });
104
+ }
105
+ else {
106
+ let claw = -e.amt; // clawback hits remaining capacity oldest-first; excess is dropped
107
+ for (const b of buckets) {
108
+ if (claw <= BigInt(0))
109
+ break;
110
+ const take = remaining(b) < claw ? remaining(b) : claw;
111
+ if (take > BigInt(0)) {
112
+ b.reversed += take;
113
+ claw -= take;
114
+ }
115
+ }
116
+ }
117
+ }
118
+ else {
119
+ let need = -e.amt;
120
+ let emissionsLeft = (_c = e.emissions) !== null && _c !== void 0 ? _c : need; // no split provided -> count it all as emissions
121
+ for (const b of buckets) {
122
+ if (need <= BigInt(0))
123
+ break;
124
+ if (b.t + lookback < e.t)
125
+ continue; // grant expired before this job ran
126
+ const take = remaining(b) < need ? remaining(b) : need;
127
+ if (take <= BigInt(0))
128
+ continue;
129
+ const em = emissionsLeft < take ? emissionsLeft : take;
130
+ b.drawnEmissions += em;
131
+ b.drawnMisrouted += take - em;
132
+ emissionsLeft -= em;
133
+ need -= take;
134
+ }
135
+ beyond += need;
136
+ }
109
137
  }
110
- const overshoot = minRun < BigInt(0) ? -minRun : BigInt(0);
111
- const avail = run + overshoot; // ≥ 0 by construction
112
- return { avail, beyondGrants: overshoot };
113
- };
114
- exports.foldTimeAware = foldTimeAware;
115
- const foldGrantAllotment = (grants, attributedSpend, correctionSettled) => {
116
- const granted = grants.reduce((s, g) => s + g.amountCredits, BigInt(0));
117
- const consumed = (attributedSpend > BigInt(0) ? attributedSpend : BigInt(0)) +
118
- (correctionSettled > BigInt(0) ? correctionSettled : BigInt(0));
119
- const avail = granted > consumed ? granted - consumed : BigInt(0);
120
- let drawn = consumed;
138
+ let avail = BigInt(0);
139
+ let expired = BigInt(0);
140
+ let usedEmissions = BigInt(0);
141
+ let usedMisrouted = BigInt(0);
121
142
  let fifoGrantId;
122
- for (const g of grants) {
123
- if (drawn < g.amountCredits) {
124
- fifoGrantId = g.id;
125
- break;
143
+ for (const b of buckets) {
144
+ usedEmissions += b.drawnEmissions;
145
+ usedMisrouted += b.drawnMisrouted;
146
+ const rem = remaining(b);
147
+ if (b.t + lookback < now) {
148
+ b.expired = rem;
126
149
  }
127
- drawn -= g.amountCredits;
150
+ else {
151
+ b.left = rem;
152
+ avail += rem;
153
+ if (fifoGrantId === undefined && rem > BigInt(0) && b.grantId !== undefined)
154
+ fifoGrantId = b.grantId;
155
+ }
156
+ expired += b.expired;
128
157
  }
129
- return { granted, consumed, avail, fifoGrantId };
158
+ return {
159
+ avail,
160
+ beyondGrants: beyond,
161
+ jobCharged: usedEmissions + usedMisrouted,
162
+ usedEmissions,
163
+ usedMisrouted,
164
+ expired,
165
+ buckets,
166
+ fifoGrantId,
167
+ };
130
168
  };
131
- exports.foldGrantAllotment = foldGrantAllotment;
169
+ exports.foldGrantWindow = foldGrantWindow;
132
170
  const base_2 = require("./base");
133
171
  const moment_1 = __importDefault(require("moment"));
134
172
  pg.types.setTypeParser(1114, (str) => moment_1.default.utc(str).toDate());
@@ -256,91 +294,54 @@ const pgClient = (config) => {
256
294
  // Spendable grant per user, straight off otoy_grant (FIFO), net of what's already been spent or
257
295
  // already settled by a burn correction. See GrantAllotment for the units + the double-spend guard.
258
296
  const getGrantAllotments = (db, p, log = logger_1.consoleLogger) => __awaiter(void 0, void 0, void 0, function* () {
259
- var _b;
297
+ var _b, _c;
260
298
  const out = {};
261
299
  if (!p.userIds.length)
262
300
  return out;
263
- const grants = yield db
264
- .selectFrom(exports.OTOY_GRANT_TABLE)
265
- .select(['id', 'user_id', 'amount_credits', 'otoy_created_at'])
266
- .where('user_id', 'in', p.userIds)
267
- .orderBy('otoy_created_at', 'asc')
268
- .orderBy('id', 'asc')
269
- .execute();
270
- if (!grants.length)
271
- return out;
272
- // EVERY grant-ledger row retires pool capacity, in either direction:
273
- // - negative delta = an emissions burn. ALL of them count, not just ones stamped with an
274
- // otoy_grant_id. Historical grants are now backfilled to cover the Sep/Oct-2025 burns, so those
275
- // burns must net against them; counting only stamped rows would re-issue ~1.06M credits of
276
- // already-spent grant.
277
- // - positive delta = the grant was CREDITED into current_user_grant_spend (the one-off canary
278
- // route). That balance reaches the split separately, so leaving it in the pool double-issues it.
279
- // Hence abs() over the whole ledger.
280
- // NOT abs() over the whole ledger — that would charge the Sep/Oct-2025 CSV *credits* as if they
281
- // were spend, double-penalising those users (464e8e33: 376,749 burned + 79,304 credited = 456,053).
282
- // A positive only retires capacity when it is tied to a pool grant (the canary route, where the
283
- // grant was moved into current_user_grant_spend and reaches the split from there).
284
- const spent = yield db
285
- .selectFrom(exports.USER_GRANT_SPEND_TABLE)
286
- .select((eb) => [
287
- 'user_id',
288
- eb.fn
289
- .sum((0, kysely_1.sql) `case when render_spent_delta < 0 then -render_spent_delta
290
- when otoy_grant_id is not null then render_spent_delta
291
- else 0 end`)
292
- .as('net'),
293
- ])
294
- .where('user_id', 'in', p.userIds)
295
- .groupBy('user_id')
296
- .execute();
297
- // Time-aware fold (see foldTimeAware): one pass over the user's event stream — grant issuances
298
- // (+, at issue time; the legacy CSV credit at window start; pool-stamped credits as capacity moved
299
- // out at their created_at) and job spend (−, at completion). A job can only draw entitlement that
300
- // existed when it ran; overshoot is purchase-paid forever.
301
- const fold = yield (0, kysely_1.sql) `
302
- with ev as (
303
- select user_id::text as u, coalesce(otoy_created_at, ${exports.GRANT_WINDOW_START}::timestamp) as t,
304
- (amount_credits * 4 * 1e8)::numeric as amt
305
- from otoy_grant where user_id = any(${p.userIds}::uuid[])
306
- union all
307
- select user_id::text, ${exports.GRANT_WINDOW_START}::timestamp, sum(render_spent_delta)::numeric
308
- from user_grant_spend
309
- where user_id = any(${p.userIds}::uuid[]) and render_spent_delta > 0 and otoy_grant_id is null
310
- group by user_id
311
- union all
312
- select user_id::text, created_at, -render_spent_delta::numeric
313
- from user_grant_spend
314
- where user_id = any(${p.userIds}::uuid[]) and render_spent_delta > 0 and otoy_grant_id is not null
315
- union all
316
- select user_id, completed_at, -render_amt::numeric
317
- from job
318
- where user_id = any(${p.userIds}::text[]) and completed_at >= ${exports.GRANT_WINDOW_START}::timestamp
319
- ),
320
- r as (
321
- select u, amt,
322
- sum(amt) over (partition by u order by t asc, amt desc rows unbounded preceding) as run
323
- from ev
324
- )
325
- select u as user_id,
326
- sum(case when amt > 0 then amt else 0 end)::text as granted_total,
327
- sum(amt)::text as net,
328
- greatest(-min(run), 0)::text as overshoot
329
- from r group by u
330
- `.execute(db); // db is always a Kysely trx at runtime; QueryCreator's type just lacks getExecutor
331
- const toB = (v) => BigInt(Math.round(Number(v !== null && v !== void 0 ? v : 0)));
332
- for (const r of fold.rows) {
333
- const u = r.user_id;
334
- const cur = (_b = out[u]) !== null && _b !== void 0 ? _b : (out[u] = { userId: u, granted: BigInt(0), consumed: BigInt(0), avail: BigInt(0), grants: [] });
335
- const granted = toB(r.granted_total);
336
- const net = toB(r.net);
337
- const overshoot = toB(r.overshoot);
338
- const avail = net + overshoot > BigInt(0) ? net + overshoot : BigInt(0);
339
- cur.granted = granted;
340
- cur.consumed = granted - avail; // grant capacity actually charged
341
- cur.avail = avail;
342
- if (overshoot > BigInt(0)) {
343
- log.info(`grant allotment ${u}: time-aware fold — overshoot ${overshoot} is purchase-paid; granted ${granted} => avail ${avail}`);
301
+ // Raw event stream, folded in code (foldGrantWindow — per-grant buckets, 60-day consumable window,
302
+ // reversals clamp capacity, only jobs create beyond-grants). SQL can't express the bucketed clamp.
303
+ const evRes = yield (0, kysely_1.sql) `
304
+ select user_id::text as u, coalesce(otoy_created_at, ${exports.GRANT_WINDOW_START}::timestamp) as t,
305
+ (amount_credits * 4 * 1e8)::numeric::text as amt, 'grant' as src, id as gid
306
+ from otoy_grant where user_id = any(${p.userIds}::uuid[])
307
+ union all
308
+ select user_id::text, ${exports.GRANT_WINDOW_START}::timestamp, sum(render_spent_delta)::text, 'grant', null
309
+ from user_grant_spend
310
+ where user_id = any(${p.userIds}::uuid[]) and render_spent_delta > 0 and otoy_grant_id is null
311
+ group by user_id
312
+ union all
313
+ select user_id::text, created_at, (-render_spent_delta)::text, 'grant', null
314
+ from user_grant_spend
315
+ where user_id = any(${p.userIds}::uuid[]) and render_spent_delta > 0 and otoy_grant_id is not null
316
+ union all
317
+ select user_id, completed_at, (-render_amt)::text, 'job', null
318
+ from job
319
+ where user_id = any(${p.userIds}::text[]) and completed_at >= ${exports.GRANT_WINDOW_START}::timestamp
320
+ `.execute(db); // db is always a Kysely trx at runtime
321
+ const byUser = new Map();
322
+ for (const r of evRes.rows) {
323
+ const arr = (_b = byUser.get(r.u)) !== null && _b !== void 0 ? _b : [];
324
+ arr.push({
325
+ t: new Date(r.t).getTime(),
326
+ amt: BigInt(Math.round(Number(r.amt))),
327
+ src: r.src,
328
+ grantId: (_c = r.gid) !== null && _c !== void 0 ? _c : undefined,
329
+ });
330
+ byUser.set(r.u, arr);
331
+ }
332
+ for (const [u, events] of byUser) {
333
+ const f = (0, exports.foldGrantWindow)(events);
334
+ out[u] = {
335
+ userId: u,
336
+ granted: events.reduce((a, e) => (e.src === 'grant' && e.amt > BigInt(0) ? a + e.amt : a), BigInt(0)),
337
+ consumed: f.jobCharged,
338
+ avail: f.avail,
339
+ expired: f.expired,
340
+ fifoGrantId: f.fifoGrantId,
341
+ };
342
+ if (f.beyondGrants > BigInt(0)) {
343
+ log.info(`grant allotment ${u}: ${f.beyondGrants} beyond grants (purchase-paid at the time), ` +
344
+ `${f.expired} expired past the ${exports.GRANT_LOOKBACK_DAYS}d window — avail ${f.avail}`);
344
345
  }
345
346
  }
346
347
  return out;
@@ -1209,7 +1210,7 @@ const pgClient = (config) => {
1209
1210
  });
1210
1211
  },
1211
1212
  insertGrantSpend(db, p, log = logger_1.consoleLogger) {
1212
- var _a, _b, _c;
1213
+ var _a, _b, _c, _d;
1213
1214
  return __awaiter(this, void 0, void 0, function* () {
1214
1215
  const existing = yield getCurrentUserGrantSpend(db.trx, { userId: p.userId }, log);
1215
1216
  const diff = {
@@ -1236,16 +1237,13 @@ const pgClient = (config) => {
1236
1237
  return;
1237
1238
  }
1238
1239
  }
1239
- // FIFO-attribute this spend to the user's oldest OTOY grant that still has room. Stamping
1240
- // otoy_grant_id is what makes getGrantAllotments count it as consumed an unattributed row
1241
- // would leave the grant looking unspent and let it fund a second burn.
1240
+ // FIFO-attribute this spend to the user's oldest LIVE OTOY grant that still has room (the fold
1241
+ // already applies the lookback window). Stamping otoy_grant_id is what shows the draw in the
1242
+ // admin drill-down — an unattributed row would leave the grant looking unspent there.
1242
1243
  let otoyGrantId = p.otoyGrantId;
1243
1244
  if (otoyGrantId === undefined && p.renderSpentDelta < BigInt(0)) {
1244
1245
  const allots = yield getGrantAllotments(db.trx, { userIds: [p.userId] }, log);
1245
- const a = allots[p.userId];
1246
- if (a) {
1247
- otoyGrantId = (0, exports.foldGrantAllotment)(a.grants, a.consumed, BigInt(0)).fifoGrantId;
1248
- }
1246
+ otoyGrantId = (_c = allots[p.userId]) === null || _c === void 0 ? void 0 : _c.fifoGrantId;
1249
1247
  }
1250
1248
  const v = {
1251
1249
  user_id: p.userId,
@@ -1271,7 +1269,7 @@ const pgClient = (config) => {
1271
1269
  user_id: p.userId,
1272
1270
  render_spent: p.renderSpentDelta,
1273
1271
  description: p.description,
1274
- perpetual: (_c = p.perpetual) !== null && _c !== void 0 ? _c : false,
1272
+ perpetual: (_d = p.perpetual) !== null && _d !== void 0 ? _d : false,
1275
1273
  })
1276
1274
  .onConflict((oc) => oc.column('user_id').doUpdateSet(diff))
1277
1275
  .returning((eb) => [