@render-foundation/utils 0.0.253 → 0.0.254

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.
@@ -33,114 +33,34 @@ BigInt.prototype.toJSON = function () {
33
33
  // confirmed_amount = EUR, despite the column name); 1 render credit = EUR 0.25, so credits = EUR x 4.
34
34
  // job.render_amt / user_grant_spend.render_spent_delta are credits x 1e8.
35
35
  export const EUR_TO_CREDITS = BigInt(4);
36
- // Grant accounting window: otoy_grant mirrors OTOY RFG issuances from this date. Consumption against the
37
- // window counts EVERY job the user ran in it — buy-and-burn jobs consumed OTOY credits exactly like
38
- // emissions-funded ones (counting only emissions-attributed spend over-issued ~2.58M credits, 2026-08-04).
39
- export const GRANT_WINDOW_START = '2025-05-01';
40
36
  export const CREDIT_DECIMALS = BigInt(100000000); // 1e8
41
- // A grant is consumable for this long after issuance; whatever is left after that EXPIRES (2026-08-05
42
- // model change). A job can only draw grants issued within the lookback window before it ran.
43
- export const GRANT_LOOKBACK_DAYS = 60;
44
- export const GRANT_LOOKBACK_MS = GRANT_LOOKBACK_DAYS * 24 * 3600 * 1000;
45
- export const foldGrantWindow = (events, opts = {}) => {
46
- const lookback = opts.lookbackMs ?? GRANT_LOOKBACK_MS;
47
- const now = opts.now ?? Date.now();
48
- const sorted = [...events].sort((a, b) => a.t - b.t || (a.amt > b.amt ? -1 : 1)); // credits first on ties
49
- const buckets = [];
50
- const jobs = [];
51
- const remaining = (b) => b.amount - b.reversed - b.drawnEmissions - b.drawnMisrouted;
52
- let beyond = BigInt(0);
53
- for (const e of sorted) {
54
- if (e.src === 'grant') {
55
- if (e.amt > BigInt(0)) {
56
- buckets.push({
57
- grantId: e.grantId,
58
- t: e.t,
59
- amount: e.amt,
60
- reversed: BigInt(0),
61
- drawnEmissions: BigInt(0),
62
- drawnMisrouted: BigInt(0),
63
- expired: BigInt(0),
64
- left: BigInt(0),
65
- });
66
- }
67
- else {
68
- let claw = -e.amt; // clawback hits remaining capacity oldest-first; excess is dropped
69
- for (const b of buckets) {
70
- if (claw <= BigInt(0))
71
- break;
72
- const take = remaining(b) < claw ? remaining(b) : claw;
73
- if (take > BigInt(0)) {
74
- b.reversed += take;
75
- claw -= take;
76
- }
77
- }
78
- }
79
- }
80
- else {
81
- let need = -e.amt;
82
- const total = need;
83
- let emissionsLeft = e.emissions ?? need; // no split provided -> count it all as emissions
84
- let jobEm = BigInt(0);
85
- let jobMis = BigInt(0);
86
- for (const b of buckets) {
87
- if (need <= BigInt(0))
88
- break;
89
- if (b.t + lookback < e.t)
90
- continue; // grant expired before this job ran
91
- const take = remaining(b) < need ? remaining(b) : need;
92
- if (take <= BigInt(0))
93
- continue;
94
- const em = emissionsLeft < take ? emissionsLeft : take;
95
- b.drawnEmissions += em;
96
- b.drawnMisrouted += take - em;
97
- jobEm += em;
98
- jobMis += take - em;
99
- emissionsLeft -= em;
100
- need -= take;
101
- }
102
- beyond += need;
103
- jobs.push({
104
- id: e.id,
105
- t: e.t,
106
- need: total,
107
- drawnEmissions: jobEm,
108
- drawnMisrouted: jobMis,
109
- beyond: need,
110
- });
111
- }
112
- }
113
- let avail = BigInt(0);
114
- let expired = BigInt(0);
115
- let usedEmissions = BigInt(0);
116
- let usedMisrouted = BigInt(0);
37
+ /**
38
+ * Pure core of the grant allotment: fold a user's OTOY grants against what's already been spent.
39
+ *
40
+ * consumed = attributedSpend (spend already stamped with an otoy_grant_id)
41
+ * + correctionSettled (jobs whose grant is already being settled by a burn_correction)
42
+ *
43
+ * The correctionSettled term is the DOUBLE-SPEND GUARD: an escrow_credit correction means we're
44
+ * recovering that job's dollars from escrow, so its grant is already committed. Counting it here stops
45
+ * the same grant from also funding a future emissions burn.
46
+ *
47
+ * fifoGrantId = the oldest grant still holding room, i.e. the one the next spend draws from.
48
+ */
49
+ export const foldGrantAllotment = (grants, attributedSpend, correctionSettled) => {
50
+ const granted = grants.reduce((s, g) => s + g.amountCredits, BigInt(0));
51
+ const consumed = (attributedSpend > BigInt(0) ? attributedSpend : BigInt(0)) +
52
+ (correctionSettled > BigInt(0) ? correctionSettled : BigInt(0));
53
+ const avail = granted > consumed ? granted - consumed : BigInt(0);
54
+ let drawn = consumed;
117
55
  let fifoGrantId;
118
- for (const b of buckets) {
119
- usedEmissions += b.drawnEmissions;
120
- usedMisrouted += b.drawnMisrouted;
121
- const rem = remaining(b);
122
- if (b.t + lookback < now) {
123
- b.expired = rem;
124
- }
125
- else {
126
- b.left = rem;
127
- avail += rem;
128
- if (fifoGrantId === undefined && rem > BigInt(0) && b.grantId !== undefined)
129
- fifoGrantId = b.grantId;
56
+ for (const g of grants) {
57
+ if (drawn < g.amountCredits) {
58
+ fifoGrantId = g.id;
59
+ break;
130
60
  }
131
- expired += b.expired;
61
+ drawn -= g.amountCredits;
132
62
  }
133
- return {
134
- avail,
135
- beyondGrants: beyond,
136
- jobCharged: usedEmissions + usedMisrouted,
137
- usedEmissions,
138
- usedMisrouted,
139
- expired,
140
- buckets,
141
- jobs,
142
- fifoGrantId,
143
- };
63
+ return { granted, consumed, avail, fifoGrantId };
144
64
  };
145
65
  import { begin } from './base';
146
66
  import moment from 'moment';
@@ -270,50 +190,63 @@ export const pgClient = (config) => {
270
190
  const out = {};
271
191
  if (!p.userIds.length)
272
192
  return out;
273
- // Raw event stream, folded in code (foldGrantWindow — per-grant buckets, 60-day consumable window,
274
- // reversals clamp capacity, only jobs create beyond-grants). SQL can't express the bucketed clamp.
275
- const evRes = await sql `
276
- select user_id::text as u, coalesce(otoy_created_at, ${GRANT_WINDOW_START}::timestamp) as t,
277
- (amount_credits * 4 * 1e8)::numeric::text as amt, 'grant' as src, id as gid
278
- from otoy_grant where user_id = any(${p.userIds}::uuid[])
279
- union all
280
- select user_id::text, ${GRANT_WINDOW_START}::timestamp, sum(render_spent_delta)::text, 'grant', null
281
- from user_grant_spend
282
- where user_id = any(${p.userIds}::uuid[]) and render_spent_delta > 0 and otoy_grant_id is null
283
- group by user_id
284
- union all
285
- select user_id::text, created_at, (-render_spent_delta)::text, 'grant', null
286
- from user_grant_spend
287
- where user_id = any(${p.userIds}::uuid[]) and render_spent_delta > 0 and otoy_grant_id is not null
288
- union all
289
- select user_id, completed_at, (-render_amt)::text, 'job', null
290
- from job
291
- where user_id = any(${p.userIds}::text[]) and completed_at >= ${GRANT_WINDOW_START}::timestamp
292
- `.execute(db); // db is always a Kysely trx at runtime
293
- const byUser = new Map();
294
- for (const r of evRes.rows) {
295
- const arr = byUser.get(r.u) ?? [];
296
- arr.push({
297
- t: new Date(r.t).getTime(),
298
- amt: BigInt(Math.round(Number(r.amt))),
299
- src: r.src,
300
- grantId: r.gid ?? undefined,
193
+ const grants = await db
194
+ .selectFrom(OTOY_GRANT_TABLE)
195
+ .select(['id', 'user_id', 'amount_credits', 'otoy_created_at'])
196
+ .where('user_id', 'in', p.userIds)
197
+ .orderBy('otoy_created_at', 'asc')
198
+ .orderBy('id', 'asc')
199
+ .execute();
200
+ if (!grants.length)
201
+ return out;
202
+ // Anything already booked against a pool grant, in EITHER direction:
203
+ // - negative delta = spend drawn from the grant (this mechanism)
204
+ // - positive delta = the grant was CREDITED into current_user_grant_spend (the one-off canary
205
+ // route). That balance is already handed to the caller separately, so leaving it in the pool too
206
+ // would hand out the same grant twice.
207
+ // Hence abs(): both directions retire pool capacity.
208
+ const spent = await db
209
+ .selectFrom(USER_GRANT_SPEND_TABLE)
210
+ .select((eb) => [
211
+ 'user_id',
212
+ eb.fn.sum(sql `abs(render_spent_delta)`).as('net'),
213
+ ])
214
+ .where('user_id', 'in', p.userIds)
215
+ .where('otoy_grant_id', 'is not', null)
216
+ .groupBy('user_id')
217
+ .execute();
218
+ // jobs already settled by a burn correction — their grant is spoken for (double-spend guard)
219
+ const corrected = await db
220
+ .selectFrom(`${JOB_TABLE} as j`)
221
+ .innerJoin(`${BURN_CORRECTION_TABLE} as bc`, 'bc.job_id', 'j.id')
222
+ .select((eb) => ['j.user_id as user_id', eb.fn.sum('j.render_amt').as('amt')])
223
+ .where('j.user_id', 'in', p.userIds)
224
+ .groupBy('j.user_id')
225
+ .execute();
226
+ const spentBy = new Map(spent.map((r) => [r.user_id, BigInt(r.net ?? 0)]));
227
+ const corrBy = new Map(corrected.map((r) => [r.user_id, BigInt(r.amt ?? 0)]));
228
+ for (const g of grants) {
229
+ const u = g.user_id;
230
+ const cur = out[u] ??
231
+ (out[u] = { userId: u, granted: BigInt(0), consumed: BigInt(0), avail: BigInt(0), grants: [] });
232
+ // amount_credits is numeric EUR — truncate to whole base units after the x4 conversion
233
+ const credits = (BigInt(Math.round(Number(g.amount_credits) * 1e8)) * EUR_TO_CREDITS);
234
+ cur.granted += credits;
235
+ cur.grants.push({
236
+ id: Number(g.id),
237
+ amountCredits: credits,
238
+ createdAt: g.otoy_created_at ? new Date(g.otoy_created_at) : null,
301
239
  });
302
- byUser.set(r.u, arr);
303
240
  }
304
- for (const [u, events] of byUser) {
305
- const f = foldGrantWindow(events);
306
- out[u] = {
307
- userId: u,
308
- granted: events.reduce((a, e) => (e.src === 'grant' && e.amt > BigInt(0) ? a + e.amt : a), BigInt(0)),
309
- consumed: f.jobCharged,
310
- avail: f.avail,
311
- expired: f.expired,
312
- fifoGrantId: f.fifoGrantId,
313
- };
314
- if (f.beyondGrants > BigInt(0)) {
315
- log.info(`grant allotment ${u}: ${f.beyondGrants} beyond grants (purchase-paid at the time), ` +
316
- `${f.expired} expired past the ${GRANT_LOOKBACK_DAYS}d window — avail ${f.avail}`);
241
+ for (const u of Object.keys(out)) {
242
+ const attributed = spentBy.get(u) ?? BigInt(0); // abs() — spend drawn or grant already credited
243
+ const viaCorrection = corrBy.get(u) ?? BigInt(0);
244
+ const f = foldGrantAllotment(out[u].grants, attributed, viaCorrection);
245
+ out[u].granted = f.granted;
246
+ out[u].consumed = f.consumed;
247
+ out[u].avail = f.avail;
248
+ if (viaCorrection > BigInt(0)) {
249
+ log.info(`grant allotment ${u}: granted ${f.granted} - attributed ${attributed} - correction-settled ${viaCorrection} = ${f.avail}`);
317
250
  }
318
251
  }
319
252
  return out;
@@ -395,7 +328,7 @@ export const pgClient = (config) => {
395
328
  const sequenceNumbers = [];
396
329
  const dbEpochIds = [];
397
330
  for (const bt of bts) {
398
- let { solKey: toSolKey, seq, createdAt, amount, upgradeBracket, priorSupply, points, epochId, sender, } = bt;
331
+ let { solKey: toSolKey, seq, createdAt, amount, upgradeBracket, priorSupply, points, epochId, } = bt;
399
332
  let entityId;
400
333
  if (toSolKey && toSolKey != '') {
401
334
  entityId = await getOrInsertEntity(db, { solKey: toSolKey });
@@ -416,7 +349,6 @@ export const pgClient = (config) => {
416
349
  prior_supply: priorSupply,
417
350
  points: points,
418
351
  epoch_id: dbEpochId,
419
- sender: sender,
420
352
  })
421
353
  .onConflict((oc) => oc.column('sequence').doUpdateSet({
422
354
  created_at: createdAt,
@@ -426,7 +358,6 @@ export const pgClient = (config) => {
426
358
  points: points,
427
359
  epoch_id: dbEpochId,
428
360
  entity_id: entityId,
429
- sender: sender,
430
361
  }))
431
362
  .returning('sequence');
432
363
  const c = q.compile();
@@ -1169,13 +1100,16 @@ export const pgClient = (config) => {
1169
1100
  return;
1170
1101
  }
1171
1102
  }
1172
- // FIFO-attribute this spend to the user's oldest LIVE OTOY grant that still has room (the fold
1173
- // already applies the lookback window). Stamping otoy_grant_id is what shows the draw in the
1174
- // admin drill-down — an unattributed row would leave the grant looking unspent there.
1103
+ // FIFO-attribute this spend to the user's oldest OTOY grant that still has room. Stamping
1104
+ // otoy_grant_id is what makes getGrantAllotments count it as consumed an unattributed row
1105
+ // would leave the grant looking unspent and let it fund a second burn.
1175
1106
  let otoyGrantId = p.otoyGrantId;
1176
1107
  if (otoyGrantId === undefined && p.renderSpentDelta < BigInt(0)) {
1177
1108
  const allots = await getGrantAllotments(db.trx, { userIds: [p.userId] }, log);
1178
- otoyGrantId = allots[p.userId]?.fifoGrantId;
1109
+ const a = allots[p.userId];
1110
+ if (a) {
1111
+ otoyGrantId = foldGrantAllotment(a.grants, a.consumed, BigInt(0)).fifoGrantId;
1112
+ }
1179
1113
  }
1180
1114
  const v = {
1181
1115
  user_id: p.userId,
@@ -1528,70 +1462,133 @@ export const pgClient = (config) => {
1528
1462
  return burns;
1529
1463
  },
1530
1464
  async fetchBurnsPage(db, f, log = consoleLogger) {
1531
- const lastId = f.cursor ? atob(f.cursor) : undefined;
1532
- let q = db.trx
1533
- .selectFrom('burn')
1534
- .selectAll('burn')
1535
- .innerJoin('sol_tx', 'sol_tx.id', 'burn.sol_tx_id')
1536
- .select(['sol_tx.executed_at', 'sol_tx.sig']);
1537
- if (!f.inclManual) {
1538
- q = q.where('burn.usdc_spent', '>', String(0));
1539
- }
1540
- if (f.executedAfter) {
1541
- q = q.where('sol_tx.executed_at', '>=', f.executedAfter);
1542
- }
1543
- if (f.executedBefore) {
1544
- q = q.where('sol_tx.executed_at', '<=', f.executedBefore);
1545
- }
1546
- if (f.sig) {
1547
- // Exact match, not a prefix: sol_tx.sig is unique, so this is an index hit returning at most one row.
1548
- q = q.where('sol_tx.sig', '=', f.sig);
1465
+ const params = [];
1466
+ const p = (v) => {
1467
+ params.push(v);
1468
+ return `$${params.length}`;
1469
+ };
1470
+ // Decode cursor — supports new {t,id,ea} format and legacy bare-id format.
1471
+ let cursorEa;
1472
+ let cursorType;
1473
+ let cursorId;
1474
+ if (f.cursor) {
1475
+ const raw = atob(f.cursor);
1476
+ try {
1477
+ const c = JSON.parse(raw);
1478
+ cursorEa = new Date(c.ea);
1479
+ cursorType = c.t;
1480
+ cursorId = c.id;
1481
+ }
1482
+ catch {
1483
+ // Legacy cursor: bare burn.id — treat as buy-burn cursor.
1484
+ cursorId = Number(raw);
1485
+ cursorType = 'buy';
1486
+ }
1549
1487
  }
1488
+ // Shared WHERE fragments pushed into each union leg for index use.
1489
+ const dateFilters = [];
1490
+ if (f.executedAfter)
1491
+ dateFilters.push(`st.executed_at >= ${p(f.executedAfter)}`);
1492
+ if (f.executedBefore)
1493
+ dateFilters.push(`st.executed_at <= ${p(f.executedBefore)}`);
1494
+ const sigFilter = f.sig ? `st.sig = ${p(f.sig)}` : '';
1495
+ // Buy-burn leg
1496
+ const buyWhere = [...dateFilters];
1497
+ if (!f.inclManual)
1498
+ buyWhere.push('b.usdc_spent > 0');
1499
+ if (sigFilter)
1500
+ buyWhere.push(sigFilter);
1550
1501
  if (f.jobId !== undefined) {
1551
- // "Which burn consumed this job?" job.burn_id is the FK, so resolve it as a subquery rather than
1552
- // joining `job` into the page query, which would multiply rows by job count and break both the limit
1553
- // and the cursor.
1554
- q = q.where('burn.id', 'in', (eb) => eb
1555
- .selectFrom('job')
1556
- .select('job.burn_id')
1557
- .where('job.id', '=', String(f.jobId)));
1558
- }
1559
- if (lastId) {
1560
- q = q.where('burn.id', '>', lastId);
1502
+ buyWhere.push(`b.id IN (SELECT burn_id FROM job WHERE id = ${p(f.jobId)})`);
1503
+ }
1504
+ const buyWhereClause = buyWhere.length ? `WHERE ${buyWhere.join(' AND ')}` : '';
1505
+ const buySql = `
1506
+ SELECT 'buy'::text AS type, b.id::bigint AS sortid, b.id AS id,
1507
+ st.executed_at, b.priced_at, b.burned::text AS burned,
1508
+ b.usdc_spent::text AS usdc_spent, b.quote_amt::text AS quote_amt,
1509
+ b.tags, st.sig, b.render_to_usdc, b.eur_to_usdc,
1510
+ coalesce(b.from_escrow_usdc, 0)::text AS from_escrow_usdc,
1511
+ b.down_correction_id, b.escrow_correction_id,
1512
+ b.marked_burned_at
1513
+ FROM burn b
1514
+ INNER JOIN sol_tx st ON st.id = b.sol_tx_id
1515
+ ${buyWhereClause}`;
1516
+ // Grant-burn leg
1517
+ const grantWhere = [...dateFilters];
1518
+ if (sigFilter)
1519
+ grantWhere.push(sigFilter);
1520
+ if (f.jobId !== undefined) {
1521
+ grantWhere.push(`gb.id IN (SELECT grant_burn_id FROM job WHERE id = ${p(f.jobId)})`);
1522
+ }
1523
+ const grantWhereClause = grantWhere.length ? `WHERE ${grantWhere.join(' AND ')}` : '';
1524
+ const grantSql = `
1525
+ SELECT 'grant'::text AS type, gb.id::bigint AS sortid, gb.id AS id,
1526
+ st.executed_at, null::timestamp AS priced_at, gb.burned::text AS burned,
1527
+ '0' AS usdc_spent, '0' AS quote_amt,
1528
+ gb.tags, st.sig, null::float8 AS render_to_usdc, null::float8 AS eur_to_usdc,
1529
+ '0' AS from_escrow_usdc,
1530
+ null::bigint AS down_correction_id, null::bigint AS escrow_correction_id,
1531
+ gb.marked_burned_at
1532
+ FROM grant_burn gb
1533
+ INNER JOIN sol_tx st ON st.id = gb.sol_tx_id
1534
+ ${grantWhereClause}`;
1535
+ // Cursor filter on the outer query — row comparison for deterministic paging.
1536
+ let cursorFilter = '';
1537
+ if (cursorEa && cursorType && cursorId !== undefined) {
1538
+ cursorFilter = `WHERE (u.executed_at, u.type, u.sortid) > (${p(cursorEa)}, ${p(cursorType)}, ${p(cursorId)})`;
1539
+ }
1540
+ else if (cursorId !== undefined) {
1541
+ // Legacy cursor fallback: only buy burns, ordered by id.
1542
+ cursorFilter = `WHERE u.type = 'buy' AND u.sortid > ${p(cursorId)}`;
1561
1543
  }
1562
1544
  const limit = f.limit ?? 1000;
1563
- q = q.limit(limit).orderBy('burn.id');
1564
- const comp = q.compile();
1565
- const { rows } = await timedQuery(pgPool, comp.sql, comp.parameters, 'fetchBurnsPage', log);
1545
+ const sql = `
1546
+ SELECT u.* FROM (
1547
+ ${buySql}
1548
+ UNION ALL
1549
+ ${grantSql}
1550
+ ) u
1551
+ ${cursorFilter}
1552
+ ORDER BY u.executed_at ASC, u.type, u.sortid
1553
+ LIMIT ${p(limit)}`;
1554
+ const { rows } = await timedQuery(pgPool, sql, params, 'fetchBurnsPage', log);
1566
1555
  const burns = rows.map((r) => ({
1567
1556
  jobs: [],
1568
1557
  id: Number(r.id),
1569
- renderToUsdc: r.render_to_usdc,
1558
+ type: r.type,
1559
+ renderToUsdc: r.render_to_usdc ?? undefined,
1570
1560
  solTx: r.sig,
1571
1561
  executedAt: r.executed_at,
1572
- pricedAt: r.priced_at,
1562
+ pricedAt: r.priced_at ?? undefined,
1573
1563
  usdcSpent: BigInt(r.usdc_spent ?? 0),
1574
1564
  quoteAmt: BigInt(r.quote_amt ?? 0),
1575
1565
  burned: BigInt(r.burned),
1576
- eurToUsdc: r.eur_to_usdc,
1566
+ eurToUsdc: r.eur_to_usdc ?? undefined,
1577
1567
  markedBurnedAt: r.marked_burned_at,
1578
1568
  tags: r.tags ? r.tags.split(',') : [],
1569
+ fromEscrowUsdc: BigInt(r.from_escrow_usdc ?? 0),
1570
+ downCorrectionId: r.down_correction_id ? Number(r.down_correction_id) : undefined,
1571
+ escrowCorrectionId: r.escrow_correction_id ? Number(r.escrow_correction_id) : undefined,
1579
1572
  }));
1580
1573
  return {
1581
1574
  burns,
1582
- // Only hand back a cursor on a full page; a short page means we're done.
1583
1575
  cursor: burns.length === limit
1584
- ? btoa(String(burns[burns.length - 1].id))
1576
+ ? btoa(JSON.stringify({
1577
+ ea: rows[burns.length - 1].executed_at,
1578
+ t: rows[burns.length - 1].type,
1579
+ id: rows[burns.length - 1].sortid,
1580
+ }))
1585
1581
  : undefined,
1586
1582
  };
1587
1583
  },
1588
- async fetchBurnJobs(db, burnId, log = consoleLogger) {
1584
+ async fetchBurnJobs(db, burnId, burnType = 'buy', log = consoleLogger) {
1585
+ const fk = burnType === 'grant' ? 'grant_burn_id' : 'burn_id';
1589
1586
  const rows = await db.trx
1590
1587
  .selectFrom('job')
1591
1588
  .selectAll('job')
1592
- .where('burn_id', '=', String(burnId))
1589
+ .where(fk, '=', String(burnId))
1593
1590
  .execute();
1594
- log.debug(`fetchBurnJobs burn ${burnId} -> ${rows.length} jobs`);
1591
+ log.debug(`fetchBurnJobs ${burnType} ${burnId} -> ${rows.length} jobs`);
1595
1592
  return rows.map((r) => ({
1596
1593
  id: Number(r.id),
1597
1594
  completedAt: r.completed_at,
@@ -1915,8 +1912,6 @@ export const pgClient = (config) => {
1915
1912
  usdc_spent: p.usdcSpent ?? null,
1916
1913
  correction_id: p.correctionId ?? null,
1917
1914
  correction_render: p.correctionRender ?? null,
1918
- usage_from: p.usageFrom ?? null,
1919
- usage_to: p.usageTo ?? null,
1920
1915
  })
1921
1916
  .onConflict((oc) => oc.column('tx_signature').doNothing())
1922
1917
  .executeTakeFirst();