@render-foundation/utils 0.0.246 → 0.0.248

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.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.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;
39
39
  const kysely_1 = require("kysely");
40
40
  const logger_1 = require("../../logger");
41
41
  const pg_1 = require("pg");
@@ -88,6 +88,30 @@ exports.CREDIT_DECIMALS = BigInt(100000000); // 1e8
88
88
  *
89
89
  * fifoGrantId = the oldest grant still holding room, i.e. the one the next spend draws from.
90
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
+ 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);
105
+ for (const e of sorted) {
106
+ run += e.amt;
107
+ if (run < minRun)
108
+ minRun = run;
109
+ }
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;
91
115
  const foldGrantAllotment = (grants, attributedSpend, correctionSettled) => {
92
116
  const granted = grants.reduce((s, g) => s + g.amountCredits, BigInt(0));
93
117
  const consumed = (attributedSpend > BigInt(0) ? attributedSpend : BigInt(0)) +
@@ -232,7 +256,7 @@ const pgClient = (config) => {
232
256
  // Spendable grant per user, straight off otoy_grant (FIFO), net of what's already been spent or
233
257
  // already settled by a burn correction. See GrantAllotment for the units + the double-spend guard.
234
258
  const getGrantAllotments = (db, p, log = logger_1.consoleLogger) => __awaiter(void 0, void 0, void 0, function* () {
235
- var _b, _c, _d, _e;
259
+ var _b;
236
260
  const out = {};
237
261
  if (!p.userIds.length)
238
262
  return out;
@@ -270,70 +294,53 @@ const pgClient = (config) => {
270
294
  .where('user_id', 'in', p.userIds)
271
295
  .groupBy('user_id')
272
296
  .execute();
273
- // ALL in-window job spend the true deduction ledger. Every render job flows through us, and every
274
- // job consumed the user's OTOY credits regardless of HOW we funded the burn (emissions or USDC
275
- // buy-and-burn). This supersedes the old correction-settled term (those jobs are in-window) and
276
- // closes the pre-go-live gap where buy-and-burn jobs didn't decrement grants.
277
- const jobspend = yield db
278
- .selectFrom(`${exports.JOB_TABLE} as j`)
279
- .select((eb) => ['j.user_id as user_id', eb.fn.sum('j.render_amt').as('amt')])
280
- .where('j.user_id', 'in', p.userIds)
281
- .where('j.completed_at', '>=', new Date(exports.GRANT_WINDOW_START))
282
- .groupBy('j.user_id')
283
- .execute();
284
- // legacy CSV CREDITED (pre-cutoff OTOY grants redeemed against in-window jobs): the FULL credited
285
- // amount offsets job spend those jobs were paid by pre-cutoff grants per OTOY's own redemption
286
- // report. Using the floored remaining BALANCE here under-grants users whose emissions burns exceeded
287
- // their credited amount (f5adcc66: balance 0 but credited 50,213 -> avail 6,390 vs correct 56,603).
288
- const legacy = yield db
289
- .selectFrom(exports.USER_GRANT_SPEND_TABLE)
290
- .select((eb) => ['user_id', eb.fn.sum('render_spent_delta').as('v')])
291
- .where('user_id', 'in', p.userIds)
292
- .where('render_spent_delta', '>', '0')
293
- .where('otoy_grant_id', 'is', null)
294
- .groupBy('user_id')
295
- .execute();
296
- const spentBy = new Map(spent.map((r) => { var _a; return [r.user_id, BigInt((_a = r.net) !== null && _a !== void 0 ? _a : 0)]; }));
297
- const jobBy = new Map(jobspend.map((r) => { var _a; return [r.user_id, BigInt((_a = r.amt) !== null && _a !== void 0 ? _a : 0)]; }));
298
- // clamp: legacy exists ONLY to offset the jobs it was minted from it can never exceed the user's
299
- // in-window job spend, so it can never fund forward (closes the 32-user CSV snapshot edge).
300
- const legByRaw = new Map(legacy.map((r) => { var _a; return [r.user_id, BigInt((_a = r.v) !== null && _a !== void 0 ? _a : 0)]; }));
301
- const legBy = new Map([...legByRaw].map(([u, v]) => {
302
- var _a;
303
- const js = (_a = jobBy.get(u)) !== null && _a !== void 0 ? _a : BigInt(0);
304
- return [u, v < js ? v : js];
305
- }));
306
- for (const g of grants) {
307
- const u = g.user_id;
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;
308
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: [] });
309
- // amount_credits is numeric EUR — truncate to whole base units after the x4 conversion
310
- const credits = (BigInt(Math.round(Number(g.amount_credits) * 1e8)) * exports.EUR_TO_CREDITS);
311
- cur.granted += credits;
312
- cur.grants.push({
313
- id: Number(g.id),
314
- amountCredits: credits,
315
- createdAt: g.otoy_created_at ? new Date(g.otoy_created_at) : null,
316
- });
317
- }
318
- // legacy-only users (no mirrored grant) still get an entry — their CSV balance is a granted source
319
- for (const [u, v] of legBy) {
320
- if (v > BigInt(0) && !out[u])
321
- out[u] = { userId: u, granted: BigInt(0), consumed: BigInt(0), avail: BigInt(0), grants: [] };
322
- }
323
- for (const u of Object.keys(out)) {
324
- const attributed = (_c = spentBy.get(u)) !== null && _c !== void 0 ? _c : BigInt(0); // ledger-counted (burns + pool-tied credits)
325
- const spend = (_d = jobBy.get(u)) !== null && _d !== void 0 ? _d : BigInt(0); // ALL in-window job spend
326
- const leg = (_e = legBy.get(u)) !== null && _e !== void 0 ? _e : BigInt(0);
327
- // consumed = the larger of the two accountings — the ledger can exceed job spend (pool-tied
328
- // credits retiring capacity), job spend can exceed the ledger (pre-go-live buy-and-burn jobs).
329
- const consumed = attributed > spend ? attributed : spend;
330
- const f = (0, exports.foldGrantAllotment)(out[u].grants, consumed, BigInt(0));
331
- out[u].granted = f.granted + leg;
332
- out[u].consumed = consumed;
333
- out[u].avail =
334
- f.granted + leg > consumed ? f.granted + leg - consumed : BigInt(0);
335
- if (spend > attributed) {
336
- log.info(`grant allotment ${u}: in-window job spend ${spend} > ledger-counted ${attributed} — strict consumption applied (granted ${f.granted} + legacy ${leg} => avail ${out[u].avail})`);
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}`);
337
344
  }
338
345
  }
339
346
  return out;
@@ -2009,7 +2016,7 @@ const pgClient = (config) => {
2009
2016
  // took the room), the whole txn rolls back and we return {inserted:false} — the caller must NOT
2010
2017
  // broadcast (no row, no counter change, no burn).
2011
2018
  return yield db.transaction().execute((trx) => __awaiter(this, void 0, void 0, function* () {
2012
- var _a, _b, _c, _d, _e, _f, _g;
2019
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j;
2013
2020
  const res = yield trx
2014
2021
  .insertInto(exports.DISPERSED_SETTLEMENT_TABLE)
2015
2022
  .values({
@@ -2024,10 +2031,12 @@ const pgClient = (config) => {
2024
2031
  usdc_spent: (_c = p.usdcSpent) !== null && _c !== void 0 ? _c : null,
2025
2032
  correction_id: (_d = p.correctionId) !== null && _d !== void 0 ? _d : null,
2026
2033
  correction_render: (_e = p.correctionRender) !== null && _e !== void 0 ? _e : null,
2034
+ usage_from: (_f = p.usageFrom) !== null && _f !== void 0 ? _f : null,
2035
+ usage_to: (_g = p.usageTo) !== null && _g !== void 0 ? _g : null,
2027
2036
  })
2028
2037
  .onConflict((oc) => oc.column('tx_signature').doNothing())
2029
2038
  .executeTakeFirst();
2030
- const inserted = ((_f = res.numInsertedOrUpdatedRows) !== null && _f !== void 0 ? _f : BigInt(0)) > BigInt(0);
2039
+ const inserted = ((_h = res.numInsertedOrUpdatedRows) !== null && _h !== void 0 ? _h : BigInt(0)) > BigInt(0);
2031
2040
  // Only reserve when the row was actually inserted (a tx_signature conflict is a no-op → no double
2032
2041
  // count) and it drew a correction.
2033
2042
  if (inserted &&
@@ -2042,7 +2051,7 @@ const pgClient = (config) => {
2042
2051
  .where('id', '=', String(p.correctionId))
2043
2052
  .where((eb) => eb((0, kysely_1.sql) `consumed_render + ${p.correctionRender}::numeric`, '<=', eb.ref('amount_render')))
2044
2053
  .executeTakeFirst();
2045
- if (((_g = upd.numUpdatedRows) !== null && _g !== void 0 ? _g : BigInt(0)) === BigInt(0)) {
2054
+ if (((_j = upd.numUpdatedRows) !== null && _j !== void 0 ? _j : BigInt(0)) === BigInt(0)) {
2046
2055
  // Would exceed amount_render (concurrent draw won the room) — roll back the whole txn.
2047
2056
  log.warn(`dispersed correction ${p.correctionId}: draw ${p.correctionRender} would over-consume; rejecting outbox insert for tx ${p.txSignature}`);
2048
2057
  throw new DispersedCorrectionRejected();