@render-foundation/utils 0.0.237 → 0.0.239

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.DISPERSED_BURN_CORRECTION_TABLE = exports.DISPERSED_SETTLEMENT_TABLE = exports.GRANT_BURN_TABLE = exports.GRANT_BURN_ID_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.CREDIT_DECIMALS = 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");
@@ -59,6 +59,7 @@ exports.ENTITY_EPOCH_INFO_TABLE = 'entity_epoch_info';
59
59
  exports.POLYGON_UPGRADE_TABLE = 'polygon_upgrade';
60
60
  exports.CURRENT_USER_GRANT_SPEND_TABLE = 'current_user_grant_spend';
61
61
  exports.USER_GRANT_SPEND_TABLE = 'user_grant_spend';
62
+ exports.OTOY_GRANT_TABLE = 'otoy_grant';
62
63
  exports.GRANT_BURN_ID_TABLE = 'grant_burn_id';
63
64
  exports.GRANT_BURN_TABLE = 'grant_burn';
64
65
  exports.DISPERSED_SETTLEMENT_TABLE = 'dispersed_settlement';
@@ -66,6 +67,40 @@ exports.DISPERSED_BURN_CORRECTION_TABLE = 'dispersed_burn_correction';
66
67
  BigInt.prototype.toJSON = function () {
67
68
  return this.toString();
68
69
  };
70
+ // EUR -> render credits. OTOY issues grants in EUR (otoy_grant.amount_credits holds the raw OTOY
71
+ // confirmed_amount = EUR, despite the column name); 1 render credit = EUR 0.25, so credits = EUR x 4.
72
+ // job.render_amt / user_grant_spend.render_spent_delta are credits x 1e8.
73
+ exports.EUR_TO_CREDITS = BigInt(4);
74
+ exports.CREDIT_DECIMALS = BigInt(100000000); // 1e8
75
+ /**
76
+ * Pure core of the grant allotment: fold a user's OTOY grants against what's already been spent.
77
+ *
78
+ * consumed = attributedSpend (spend already stamped with an otoy_grant_id)
79
+ * + correctionSettled (jobs whose grant is already being settled by a burn_correction)
80
+ *
81
+ * The correctionSettled term is the DOUBLE-SPEND GUARD: an escrow_credit correction means we're
82
+ * recovering that job's dollars from escrow, so its grant is already committed. Counting it here stops
83
+ * the same grant from also funding a future emissions burn.
84
+ *
85
+ * fifoGrantId = the oldest grant still holding room, i.e. the one the next spend draws from.
86
+ */
87
+ const foldGrantAllotment = (grants, attributedSpend, correctionSettled) => {
88
+ const granted = grants.reduce((s, g) => s + g.amountCredits, BigInt(0));
89
+ const consumed = (attributedSpend > BigInt(0) ? attributedSpend : BigInt(0)) +
90
+ (correctionSettled > BigInt(0) ? correctionSettled : BigInt(0));
91
+ const avail = granted > consumed ? granted - consumed : BigInt(0);
92
+ let drawn = consumed;
93
+ let fifoGrantId;
94
+ for (const g of grants) {
95
+ if (drawn < g.amountCredits) {
96
+ fifoGrantId = g.id;
97
+ break;
98
+ }
99
+ drawn -= g.amountCredits;
100
+ }
101
+ return { granted, consumed, avail, fifoGrantId };
102
+ };
103
+ exports.foldGrantAllotment = foldGrantAllotment;
69
104
  const base_2 = require("./base");
70
105
  const moment_1 = __importDefault(require("moment"));
71
106
  pg.types.setTypeParser(1114, (str) => moment_1.default.utc(str).toDate());
@@ -190,6 +225,73 @@ const pgClient = (config) => {
190
225
  }
191
226
  : undefined;
192
227
  });
228
+ // Spendable grant per user, straight off otoy_grant (FIFO), net of what's already been spent or
229
+ // already settled by a burn correction. See GrantAllotment for the units + the double-spend guard.
230
+ const getGrantAllotments = (db, p, log = logger_1.consoleLogger) => __awaiter(void 0, void 0, void 0, function* () {
231
+ var _b, _c, _d;
232
+ const out = {};
233
+ if (!p.userIds.length)
234
+ return out;
235
+ const grants = yield db
236
+ .selectFrom(exports.OTOY_GRANT_TABLE)
237
+ .select(['id', 'user_id', 'amount_credits', 'otoy_created_at'])
238
+ .where('user_id', 'in', p.userIds)
239
+ .orderBy('otoy_created_at', 'asc')
240
+ .orderBy('id', 'asc')
241
+ .execute();
242
+ if (!grants.length)
243
+ return out;
244
+ // Anything already booked against a pool grant, in EITHER direction:
245
+ // - negative delta = spend drawn from the grant (this mechanism)
246
+ // - positive delta = the grant was CREDITED into current_user_grant_spend (the one-off canary
247
+ // route). That balance is already handed to the caller separately, so leaving it in the pool too
248
+ // would hand out the same grant twice.
249
+ // Hence abs(): both directions retire pool capacity.
250
+ const spent = yield db
251
+ .selectFrom(exports.USER_GRANT_SPEND_TABLE)
252
+ .select((eb) => [
253
+ 'user_id',
254
+ eb.fn.sum((0, kysely_1.sql) `abs(render_spent_delta)`).as('net'),
255
+ ])
256
+ .where('user_id', 'in', p.userIds)
257
+ .where('otoy_grant_id', 'is not', null)
258
+ .groupBy('user_id')
259
+ .execute();
260
+ // jobs already settled by a burn correction — their grant is spoken for (double-spend guard)
261
+ const corrected = yield db
262
+ .selectFrom(`${exports.JOB_TABLE} as j`)
263
+ .innerJoin(`${exports.BURN_CORRECTION_TABLE} as bc`, 'bc.job_id', 'j.id')
264
+ .select((eb) => ['j.user_id as user_id', eb.fn.sum('j.render_amt').as('amt')])
265
+ .where('j.user_id', 'in', p.userIds)
266
+ .groupBy('j.user_id')
267
+ .execute();
268
+ const spentBy = new Map(spent.map((r) => { var _a; return [r.user_id, BigInt((_a = r.net) !== null && _a !== void 0 ? _a : 0)]; }));
269
+ const corrBy = new Map(corrected.map((r) => { var _a; return [r.user_id, BigInt((_a = r.amt) !== null && _a !== void 0 ? _a : 0)]; }));
270
+ for (const g of grants) {
271
+ const u = g.user_id;
272
+ const cur = (_b = out[u]) !== null && _b !== void 0 ? _b : (out[u] = { userId: u, granted: BigInt(0), consumed: BigInt(0), avail: BigInt(0), grants: [] });
273
+ // amount_credits is numeric EUR — truncate to whole base units after the x4 conversion
274
+ const credits = (BigInt(Math.round(Number(g.amount_credits) * 1e8)) * exports.EUR_TO_CREDITS);
275
+ cur.granted += credits;
276
+ cur.grants.push({
277
+ id: Number(g.id),
278
+ amountCredits: credits,
279
+ createdAt: g.otoy_created_at ? new Date(g.otoy_created_at) : null,
280
+ });
281
+ }
282
+ for (const u of Object.keys(out)) {
283
+ const attributed = (_c = spentBy.get(u)) !== null && _c !== void 0 ? _c : BigInt(0); // abs() — spend drawn or grant already credited
284
+ const viaCorrection = (_d = corrBy.get(u)) !== null && _d !== void 0 ? _d : BigInt(0);
285
+ const f = (0, exports.foldGrantAllotment)(out[u].grants, attributed, viaCorrection);
286
+ out[u].granted = f.granted;
287
+ out[u].consumed = f.consumed;
288
+ out[u].avail = f.avail;
289
+ if (viaCorrection > BigInt(0)) {
290
+ log.info(`grant allotment ${u}: granted ${f.granted} - attributed ${attributed} - correction-settled ${viaCorrection} = ${f.avail}`);
291
+ }
292
+ }
293
+ return out;
294
+ });
193
295
  const updateTotalRndr = (db, p, totalRndr, savedAt, log) => __awaiter(void 0, void 0, void 0, function* () {
194
296
  const res = (yield db
195
297
  .updateTable(exports.EPOCH_TABLE)
@@ -1081,11 +1183,23 @@ const pgClient = (config) => {
1081
1183
  return;
1082
1184
  }
1083
1185
  }
1186
+ // FIFO-attribute this spend to the user's oldest OTOY grant that still has room. Stamping
1187
+ // otoy_grant_id is what makes getGrantAllotments count it as consumed — an unattributed row
1188
+ // would leave the grant looking unspent and let it fund a second burn.
1189
+ let otoyGrantId = p.otoyGrantId;
1190
+ if (otoyGrantId === undefined && p.renderSpentDelta < BigInt(0)) {
1191
+ const allots = yield getGrantAllotments(db.trx, { userIds: [p.userId] }, log);
1192
+ const a = allots[p.userId];
1193
+ if (a) {
1194
+ otoyGrantId = (0, exports.foldGrantAllotment)(a.grants, a.consumed, BigInt(0)).fifoGrantId;
1195
+ }
1196
+ }
1084
1197
  const v = {
1085
1198
  user_id: p.userId,
1086
1199
  render_spent_delta: p.renderSpentDelta,
1087
1200
  grant_burn_id: p.grantBurnId,
1088
1201
  job_id: p.jobId,
1202
+ otoy_grant_id: otoyGrantId,
1089
1203
  };
1090
1204
  const res = yield db.trx
1091
1205
  .insertInto(exports.USER_GRANT_SPEND_TABLE)
@@ -1446,6 +1560,116 @@ const pgClient = (config) => {
1446
1560
  return burns;
1447
1561
  });
1448
1562
  },
1563
+ fetchBurnsPage(db, f, log = logger_1.consoleLogger) {
1564
+ var _a;
1565
+ return __awaiter(this, void 0, void 0, function* () {
1566
+ const lastId = f.cursor ? atob(f.cursor) : undefined;
1567
+ let q = db.trx
1568
+ .selectFrom('burn')
1569
+ .selectAll('burn')
1570
+ .innerJoin('sol_tx', 'sol_tx.id', 'burn.sol_tx_id')
1571
+ .select(['sol_tx.executed_at', 'sol_tx.sig']);
1572
+ if (!f.inclManual) {
1573
+ q = q.where('burn.usdc_spent', '>', String(0));
1574
+ }
1575
+ if (f.executedAfter) {
1576
+ q = q.where('sol_tx.executed_at', '>=', f.executedAfter);
1577
+ }
1578
+ if (f.executedBefore) {
1579
+ q = q.where('sol_tx.executed_at', '<=', f.executedBefore);
1580
+ }
1581
+ if (lastId) {
1582
+ q = q.where('burn.id', '>', lastId);
1583
+ }
1584
+ const limit = (_a = f.limit) !== null && _a !== void 0 ? _a : 1000;
1585
+ q = q.limit(limit).orderBy('burn.id');
1586
+ const comp = q.compile();
1587
+ const { rows } = yield (0, base_1.timedQuery)(pgPool, comp.sql, comp.parameters, 'fetchBurnsPage', log);
1588
+ const burns = rows.map((r) => {
1589
+ var _a, _b;
1590
+ return ({
1591
+ jobs: [],
1592
+ id: Number(r.id),
1593
+ renderToUsdc: r.render_to_usdc,
1594
+ solTx: r.sig,
1595
+ executedAt: r.executed_at,
1596
+ pricedAt: r.priced_at,
1597
+ usdcSpent: BigInt((_a = r.usdc_spent) !== null && _a !== void 0 ? _a : 0),
1598
+ quoteAmt: BigInt((_b = r.quote_amt) !== null && _b !== void 0 ? _b : 0),
1599
+ burned: BigInt(r.burned),
1600
+ eurToUsdc: r.eur_to_usdc,
1601
+ markedBurnedAt: r.marked_burned_at,
1602
+ tags: r.tags ? r.tags.split(',') : [],
1603
+ });
1604
+ });
1605
+ return {
1606
+ burns,
1607
+ // Only hand back a cursor on a full page; a short page means we're done.
1608
+ cursor: burns.length === limit
1609
+ ? btoa(String(burns[burns.length - 1].id))
1610
+ : undefined,
1611
+ };
1612
+ });
1613
+ },
1614
+ fetchBurnJobs(db, burnId, log = logger_1.consoleLogger) {
1615
+ return __awaiter(this, void 0, void 0, function* () {
1616
+ const rows = yield db.trx
1617
+ .selectFrom('job')
1618
+ .selectAll('job')
1619
+ .where('burn_id', '=', String(burnId))
1620
+ .execute();
1621
+ log.debug(`fetchBurnJobs burn ${burnId} -> ${rows.length} jobs`);
1622
+ return rows.map((r) => ({
1623
+ id: Number(r.id),
1624
+ completedAt: r.completed_at,
1625
+ rndrUsed: BigInt(r.render_amt),
1626
+ obhUsed: r.obh_used ? Number(r.obh_used) : undefined,
1627
+ }));
1628
+ });
1629
+ },
1630
+ fetchDailyBurnStats(db, f, log = logger_1.consoleLogger) {
1631
+ return __awaiter(this, void 0, void 0, function* () {
1632
+ // sol_tx.executed_at is `timestamp WITHOUT time zone` already holding UTC wall-clock, so truncate it
1633
+ // DIRECTLY. Do not write `executed_at at time zone 'UTC'`: that promotes it to timestamptz, after which
1634
+ // date_trunc truncates in the Postgres session's TimeZone — measured against prod, a Tokyo session moved
1635
+ // the bucket boundary to 15:00Z and a Los_Angeles session changed the bucket COUNT (783 vs 786).
1636
+ // Truncating the naive column is pure arithmetic and identical under every session timezone, and
1637
+ // to_char hands back a plain 'YYYY-MM-DD' so node-postgres has no Date to decode in local time.
1638
+ const dayExpr = (0, kysely_1.sql) `to_char(date_trunc('day', sol_tx.executed_at), 'YYYY-MM-DD')`;
1639
+ let q = db.trx
1640
+ .selectFrom('burn')
1641
+ .innerJoin('sol_tx', 'sol_tx.id', 'burn.sol_tx_id')
1642
+ .select(({ fn }) => [
1643
+ dayExpr.as('day'),
1644
+ fn.sum('burn.burned').as('render_burned'),
1645
+ fn.sum('burn.usdc_spent').as('usdc_spent'),
1646
+ fn.count('burn.id').as('burn_count'),
1647
+ ])
1648
+ .groupBy((0, kysely_1.sql) `date_trunc('day', sol_tx.executed_at)`)
1649
+ .orderBy((0, kysely_1.sql) `date_trunc('day', sol_tx.executed_at)`);
1650
+ if (!f.inclManual) {
1651
+ q = q.where('burn.usdc_spent', '>', String(0));
1652
+ }
1653
+ if (f.executedAfter) {
1654
+ q = q.where('sol_tx.executed_at', '>=', f.executedAfter);
1655
+ }
1656
+ if (f.executedBefore) {
1657
+ q = q.where('sol_tx.executed_at', '<=', f.executedBefore);
1658
+ }
1659
+ const comp = q.compile();
1660
+ const { rows } = yield (0, base_1.timedQuery)(pgPool, comp.sql, comp.parameters, 'fetchDailyBurnStats', log);
1661
+ log.info(`fetchDailyBurnStats len ${rows.length}`);
1662
+ return rows.map((r) => {
1663
+ var _a, _b, _c;
1664
+ return ({
1665
+ day: r.day,
1666
+ renderBurned: BigInt((_a = r.render_burned) !== null && _a !== void 0 ? _a : 0),
1667
+ usdcSpent: BigInt((_b = r.usdc_spent) !== null && _b !== void 0 ? _b : 0),
1668
+ burnCount: Number((_c = r.burn_count) !== null && _c !== void 0 ? _c : 0),
1669
+ });
1670
+ });
1671
+ });
1672
+ },
1449
1673
  fetchBurnsAndGrants(db, f, log = logger_1.consoleLogger) {
1450
1674
  var _a, _b;
1451
1675
  return __awaiter(this, void 0, void 0, function* () {
@@ -1881,7 +2105,6 @@ const pgClient = (config) => {
1881
2105
  .select([
1882
2106
  'tx_signature',
1883
2107
  'claim_uuid',
1884
- 'obligation_uuids',
1885
2108
  'settled_amount',
1886
2109
  'burned_amount',
1887
2110
  'usdc_spent',
@@ -1897,7 +2120,6 @@ const pgClient = (config) => {
1897
2120
  return ({
1898
2121
  txSignature: r.tx_signature,
1899
2122
  claimUuid: r.claim_uuid,
1900
- obligationUuids: r.obligation_uuids,
1901
2123
  settledAmount: r.settled_amount,
1902
2124
  burnedAmount: r.burned_amount,
1903
2125
  usdcSpent: (_a = r.usdc_spent) !== null && _a !== void 0 ? _a : null,
@@ -2134,6 +2356,11 @@ const pgClient = (config) => {
2134
2356
  return yield getCurrentUserGrantSpend(db.trx, f, log);
2135
2357
  });
2136
2358
  },
2359
+ getGrantAllotments(db, f, log = logger_1.consoleLogger) {
2360
+ return __awaiter(this, void 0, void 0, function* () {
2361
+ return yield getGrantAllotments(db.trx, f, log);
2362
+ });
2363
+ },
2137
2364
  deleteAll(log = logger_1.consoleLogger) {
2138
2365
  return __awaiter(this, void 0, void 0, function* () {
2139
2366
  if (config.database.includes('render')) {