@render-foundation/utils 0.0.232-beta.0 → 0.0.233-beta.0

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.
@@ -9,12 +9,15 @@ export const MANUAL_BURN_TABLE = 'manual_burn';
9
9
  export const ENTITY_TABLE = 'entity';
10
10
  export const SOL_TRANSFER_TABLE = 'sol_transfer1';
11
11
  export const LIABILITY_TABLE = 'liability';
12
+ export const LIABILITY_ADJUSTMENT_TABLE = 'liability_adjustment';
13
+ export const LIABILITY_ADJUSTMENT_BATCH_TABLE = 'liability_adjustment_batch';
12
14
  export const JOB_ID_TABLE = 'job_id';
13
15
  export const JOB_TABLE = 'job';
14
16
  export const BURN_TABLE = 'burn';
15
17
  export const BRIDGE_TRANSFER_TABLE = 'bridge_transfer';
16
18
  export const NETWORK_REVENUE_TABLE = 'network_revenue';
17
- export const BURN_ADJUSTMENT_TABLE = 'burn_adjustment';
19
+ export const JOB_BURN_ADJUSTMENT_TABLE = 'job_burn_adjustment';
20
+ export const TX_BURN_ADJUSTMENT_TABLE = 'tx_burn_adjustment';
18
21
  export const ENTITY_EPOCH_INFO_TABLE = 'entity_epoch_info';
19
22
  export const POLYGON_UPGRADE_TABLE = 'polygon_upgrade';
20
23
  export const CURRENT_USER_GRANT_SPEND_TABLE = 'current_user_grant_spend';
@@ -313,7 +316,11 @@ export const pgClient = (config) => {
313
316
  sig: solTx,
314
317
  executedAt,
315
318
  }, log);
316
- const { id } = await db
319
+ // sol_transfer1 already has UNIQUE(sol_tx_id, from_entity_id,
320
+ // to_entity_id, symbol). Without ON CONFLICT a retry of the record step
321
+ // throws a unique violation and wedges the BullMQ job; make it an
322
+ // idempotent no-op and re-fetch the existing id instead.
323
+ let transferRow = await db
317
324
  .insertInto(SOL_TRANSFER_TABLE)
318
325
  .values({
319
326
  id: dbId,
@@ -324,9 +331,23 @@ export const pgClient = (config) => {
324
331
  symbol,
325
332
  amount_payed: amountPayed,
326
333
  })
334
+ .onConflict((oc) => oc
335
+ .columns(['sol_tx_id', 'from_entity_id', 'to_entity_id', 'symbol'])
336
+ .doNothing())
327
337
  .returning((eb) => ['id as id'])
328
- .executeTakeFirstOrThrow();
329
- transferId = BigInt(id);
338
+ .executeTakeFirst();
339
+ if (!transferRow) {
340
+ log.warn(`sol_transfer for sol_tx_id ${solTxId} ${fromEntityId}->${toEntityId} ${symbol} already exists; reusing existing id (idempotent retry)`);
341
+ transferRow = await db
342
+ .selectFrom(SOL_TRANSFER_TABLE)
343
+ .select((eb) => ['id as id'])
344
+ .where('sol_tx_id', '=', solTxId)
345
+ .where('from_entity_id', '=', fromEntityId)
346
+ .where('to_entity_id', '=', toEntityId)
347
+ .where('symbol', '=', symbol)
348
+ .executeTakeFirstOrThrow();
349
+ }
350
+ transferId = BigInt(transferRow.id);
330
351
  }
331
352
  ids.push({ transferId, liabilityId });
332
353
  }
@@ -633,9 +654,36 @@ export const pgClient = (config) => {
633
654
  }
634
655
  return b.as('x');
635
656
  }, (join) => join.onRef('y.sol_key', '=', 'x.sol_key'))
657
+ // Standing per-wallet/channel corrections (migration 36). NO epoch filter — an adjustment is
658
+ // a permanent delta on outstanding, not tied to a payout window; payments net it via `payed`.
659
+ .fullJoin((eb) => {
660
+ let b = eb
661
+ .selectFrom(LIABILITY_ADJUSTMENT_TABLE)
662
+ .select(({ fn }) => fn.sum(`amount_adjustment`).as('adj'))
663
+ .innerJoin(ENTITY_TABLE, `${LIABILITY_ADJUSTMENT_TABLE}.entity_id`, 'entity.id')
664
+ .select(['entity.sol_key'])
665
+ .groupBy('entity.sol_key');
666
+ if (f.banned) {
667
+ b = b.where('entity.banned', '=', true);
668
+ }
669
+ else {
670
+ b = b.where('entity.banned', '=', false);
671
+ }
672
+ if (f.channels && f.channels.length > 0) {
673
+ b = b.where((eb) => eb.or(f.channels.map((c) => eb('liability_adjustment.channel', '=', c))));
674
+ }
675
+ else {
676
+ b = b.where((eb) => eb.or([
677
+ eb('liability_adjustment.channel', '=', 'node_operator'),
678
+ eb('liability_adjustment.channel', '=', 'availability'),
679
+ ]));
680
+ }
681
+ return b.as('a');
682
+ }, (join) => join.on(sql `"a"."sol_key" = coalesce("y"."sol_key", "x"."sol_key")`))
636
683
  .select(({ fn }) => [
637
- fn.coalesce('y.sol_key', 'x.sol_key').as('sol_key'),
638
- sql `${fn.coalesce(`x.due`, sql `0`)} -
684
+ fn.coalesce('y.sol_key', 'x.sol_key', 'a.sol_key').as('sol_key'),
685
+ sql `${fn.coalesce(`x.due`, sql `0`)} +
686
+ ${fn.coalesce(`a.adj`, sql `0`)} -
639
687
  ${fn.coalesce(`y.payed`, sql `0`)}`.as('out'),
640
688
  ])
641
689
  .as('z'))
@@ -658,6 +706,56 @@ export const pgClient = (config) => {
658
706
  }
659
707
  return out;
660
708
  },
709
+ async addLiabilityAdjustments(batch, rows, log = consoleLogger) {
710
+ return await db.transaction().execute(async (trx) => {
711
+ // Upsert the batch by its label (idempotent re-runs; label is the run identity).
712
+ const b = await trx
713
+ .insertInto(LIABILITY_ADJUSTMENT_BATCH_TABLE)
714
+ .values({
715
+ label: batch.label,
716
+ description: batch.description,
717
+ created_by: batch.createdBy,
718
+ })
719
+ .onConflict((oc) => oc.column('label').doUpdateSet({
720
+ description: batch.description,
721
+ created_by: batch.createdBy,
722
+ }))
723
+ .returning('id')
724
+ .executeTakeFirstOrThrow();
725
+ const batchId = BigInt(b.id);
726
+ let inserted = 0;
727
+ let skipped = 0;
728
+ let net = BigInt(0);
729
+ let positive = BigInt(0);
730
+ let negative = BigInt(0);
731
+ for (const r of rows) {
732
+ const entityId = await getOrInsertEntity(trx, { solKey: r.solKey }, log);
733
+ const res = await trx
734
+ .insertInto(LIABILITY_ADJUSTMENT_TABLE)
735
+ .values({
736
+ entity_id: entityId,
737
+ channel: r.channel,
738
+ amount_adjustment: r.amountAdjustment,
739
+ batch_id: batchId,
740
+ })
741
+ // unique (entity_id, channel, batch_id) → a re-run of the same batch is a no-op
742
+ .onConflict((oc) => oc.columns(['entity_id', 'channel', 'batch_id']).doNothing())
743
+ .executeTakeFirst();
744
+ if ((res.numInsertedOrUpdatedRows ?? BigInt(0)) > BigInt(0))
745
+ inserted++;
746
+ else
747
+ skipped++;
748
+ net += r.amountAdjustment;
749
+ if (r.amountAdjustment > BigInt(0))
750
+ positive += r.amountAdjustment;
751
+ else
752
+ negative += r.amountAdjustment;
753
+ }
754
+ log.info(`liability_adjustment batch '${batch.label}' (id ${batchId}): ` +
755
+ `inserted ${inserted}, skipped ${skipped}, net ${net} (+${positive} / ${negative})`);
756
+ return { batchId, inserted, skipped, net, positive, negative };
757
+ });
758
+ },
661
759
  async fetchNodeOperatorEpoch(epochId, log = consoleLogger) {
662
760
  const q = db
663
761
  .selectFrom(JOB_ID_TABLE)
@@ -942,7 +1040,7 @@ export const pgClient = (config) => {
942
1040
  let buyBurnId;
943
1041
  let grantBurnId;
944
1042
  if (p.buyBurn) {
945
- const { burned, usdcSpent, eurToUsdc, renderToUsdc, quoteAmt, tags, burnAdjustmentId, toBurn, } = p.buyBurn;
1043
+ const { burned, usdcSpent, eurToUsdc, renderToUsdc, quoteAmt, tags, jobBurnAdjustmentId, txBurnAdjustmentId, toBurn, fromEscrowUsdc, } = p.buyBurn;
946
1044
  let pricedAt = p.buyBurn.pricedAt;
947
1045
  if (!pricedAt) {
948
1046
  const jobs = await fetchJobs1(trx.trx, { ids: p.jobs.map((j) => String(j.id)) }, log);
@@ -961,17 +1059,30 @@ export const pgClient = (config) => {
961
1059
  quote_amt: quoteAmt,
962
1060
  burned: burned,
963
1061
  tags: tags.join(','),
964
- burn_adjustment_id: burnAdjustmentId,
1062
+ job_burn_adjustment_id: jobBurnAdjustmentId,
1063
+ tx_burn_adjustment_id: txBurnAdjustmentId,
965
1064
  to_burn: toBurn,
1065
+ from_escrow_usdc: fromEscrowUsdc ?? 0,
966
1066
  };
967
1067
  log.debug(`inserting burn ${JSON.stringify(v)}`);
968
- const { id } = await trx.trx
1068
+ // Idempotent on sol_tx_id (uniq_burn_sol_tx_id, migration 27): a retry of
1069
+ // the record step for the same on-chain burn is a safe no-op. On conflict
1070
+ // executeTakeFirst() returns undefined, so re-fetch the existing id.
1071
+ let burnRow = await trx.trx
969
1072
  .insertInto(BURN_TABLE)
970
1073
  .values(v)
971
- //.onConflict((oc) => oc.column('sol_tx').doNothing())
1074
+ .onConflict((oc) => oc.column('sol_tx_id').doNothing())
972
1075
  .returning((eb) => ['id as id'])
973
- .executeTakeFirstOrThrow();
974
- buyBurnId = Number(id);
1076
+ .executeTakeFirst();
1077
+ if (!burnRow) {
1078
+ log.warn(`burn for sol_tx_id ${solTxId} already exists; reusing existing id (idempotent retry)`);
1079
+ burnRow = await trx.trx
1080
+ .selectFrom(BURN_TABLE)
1081
+ .select((eb) => ['id as id'])
1082
+ .where('sol_tx_id', '=', solTxId)
1083
+ .executeTakeFirstOrThrow();
1084
+ }
1085
+ buyBurnId = Number(burnRow.id);
975
1086
  }
976
1087
  if (p.grantBurn) {
977
1088
  const { burned, tags } = p.grantBurn;
@@ -981,12 +1092,22 @@ export const pgClient = (config) => {
981
1092
  tags: tags?.join(','),
982
1093
  };
983
1094
  log.debug(`inserting grant burn ${JSON.stringify(v)}`);
984
- const { id } = await trx.trx
1095
+ // Idempotent on sol_tx_id (uniq_grant_burn_sol_tx_id, migration 27).
1096
+ let grantBurnRow = await trx.trx
985
1097
  .insertInto(GRANT_BURN_TABLE)
986
1098
  .values(v)
1099
+ .onConflict((oc) => oc.column('sol_tx_id').doNothing())
987
1100
  .returning((eb) => ['id as id'])
988
- .executeTakeFirstOrThrow();
989
- grantBurnId = Number(id);
1101
+ .executeTakeFirst();
1102
+ if (!grantBurnRow) {
1103
+ log.warn(`grant_burn for sol_tx_id ${solTxId} already exists; reusing existing id (idempotent retry)`);
1104
+ grantBurnRow = await trx.trx
1105
+ .selectFrom(GRANT_BURN_TABLE)
1106
+ .select((eb) => ['id as id'])
1107
+ .where('sol_tx_id', '=', solTxId)
1108
+ .executeTakeFirstOrThrow();
1109
+ }
1110
+ grantBurnId = Number(grantBurnRow.id);
990
1111
  }
991
1112
  const burnId = buyBurnId;
992
1113
  const grantId = grantBurnId ?? buyBurnId;
@@ -1595,45 +1716,85 @@ export const pgClient = (config) => {
1595
1716
  : {},
1596
1717
  }));
1597
1718
  },
1598
- async getBurnAdjustments(trx, ps, log = consoleLogger) {
1599
- if (ps.toFill) {
1600
- const q = trx.trx
1601
- .selectFrom(BURN_ADJUSTMENT_TABLE)
1602
- .selectAll(BURN_ADJUSTMENT_TABLE)
1603
- .innerJoin(JOB_TABLE, `${JOB_TABLE}.id`, `${BURN_ADJUSTMENT_TABLE}.job_id`)
1604
- .select([`${JOB_TABLE}.completed_at`, `${JOB_TABLE}.render_amt`])
1605
- .leftJoin(BURN_TABLE, `${BURN_TABLE}.burn_adjustment_id`, `${BURN_ADJUSTMENT_TABLE}.id`)
1606
- .select(({ fn, lit }) => [
1607
- fn
1608
- .sum(fn.coalesce(`${BURN_TABLE}.to_burn`, lit(0)))
1609
- .as('tot_to_burn'),
1610
- fn
1611
- .sum(fn.coalesce(`${BURN_TABLE}.burned`, lit(0)))
1612
- .as('tot_burned'),
1613
- ])
1614
- .groupBy([
1615
- `${BURN_ADJUSTMENT_TABLE}.id`,
1616
- `${JOB_TABLE}.completed_at`,
1617
- `${JOB_TABLE}.render_amt`,
1618
- ])
1619
- .havingRef(sql `SUM(COALESCE("burn"."to_burn", 0)) - SUM(COALESCE("burn"."burned", 0))`, '<', `${BURN_ADJUSTMENT_TABLE}.down_adj_to_burn`);
1620
- log.info(`getBurnAdjustments ${q.compile().sql}`);
1621
- const res = await q.execute();
1622
- return res.map((r) => ({
1623
- id: Number(r.id),
1624
- createdAt: r.created_at,
1625
- jobId: Number(r.job_id),
1626
- downAdjRndrUsed: Number(r.down_adj_rndr_used),
1627
- downAdjToBurn: Number(r.down_adj_to_burn),
1628
- adjusted: Number(r.tot_to_burn) - Number(r.tot_burned),
1629
- job: {
1630
- id: Number(r.job_id),
1631
- completedAt: r.completed_at,
1632
- rndrUsed: BigInt(r.render_amt),
1633
- },
1634
- }));
1719
+ async createTxBurnAdjustment(trx, p, log = consoleLogger) {
1720
+ // Idempotent per sol_tx_id (unique) — a re-run for the same bad tx is a no-op.
1721
+ const row = await trx.trx
1722
+ .insertInto(TX_BURN_ADJUSTMENT_TABLE)
1723
+ .values({
1724
+ sol_tx_id: p.solTxId,
1725
+ surplus_usdc: p.surplusUsdc,
1726
+ description: p.description,
1727
+ })
1728
+ .onConflict((oc) => oc.column('sol_tx_id').doNothing())
1729
+ .returning('id')
1730
+ .executeTakeFirst();
1731
+ if (!row) {
1732
+ log.info(`tx_burn_adjustment for sol_tx ${p.solTxId} already exists — skipped`);
1733
+ return undefined;
1635
1734
  }
1636
- return [];
1735
+ log.info(`created tx_burn_adjustment ${row.id}: ${p.surplusUsdc} USDC over-spent (sol_tx ${p.solTxId})`);
1736
+ return Number(row.id);
1737
+ },
1738
+ async getJobBurnAdjustments(trx, ps, log = consoleLogger) {
1739
+ if (!ps.toFill)
1740
+ return [];
1741
+ const q = trx.trx
1742
+ .selectFrom(JOB_BURN_ADJUSTMENT_TABLE)
1743
+ .selectAll(JOB_BURN_ADJUSTMENT_TABLE)
1744
+ .innerJoin(JOB_TABLE, `${JOB_TABLE}.id`, `${JOB_BURN_ADJUSTMENT_TABLE}.job_id`)
1745
+ .select([`${JOB_TABLE}.completed_at`, `${JOB_TABLE}.render_amt`])
1746
+ .leftJoin(BURN_TABLE, `${BURN_TABLE}.job_burn_adjustment_id`, `${JOB_BURN_ADJUSTMENT_TABLE}.id`)
1747
+ .select(({ fn, lit }) => [
1748
+ fn.sum(fn.coalesce(`${BURN_TABLE}.to_burn`, lit(0))).as('tot_to_burn'),
1749
+ fn.sum(fn.coalesce(`${BURN_TABLE}.burned`, lit(0))).as('tot_burned'),
1750
+ ])
1751
+ .groupBy([
1752
+ `${JOB_BURN_ADJUSTMENT_TABLE}.id`,
1753
+ `${JOB_TABLE}.completed_at`,
1754
+ `${JOB_TABLE}.render_amt`,
1755
+ ]);
1756
+ log.info(`getJobBurnAdjustments ${q.compile().sql}`);
1757
+ const res = await q.execute();
1758
+ return res
1759
+ .map((r) => ({
1760
+ id: Number(r.id),
1761
+ createdAt: r.created_at,
1762
+ jobId: Number(r.job_id),
1763
+ downAdjRndrUsed: Number(r.down_adj_rndr_used),
1764
+ downAdjToBurn: Number(r.down_adj_to_burn),
1765
+ adjusted: Number(r.tot_to_burn) - Number(r.tot_burned),
1766
+ job: {
1767
+ id: Number(r.job_id),
1768
+ completedAt: r.completed_at,
1769
+ rndrUsed: BigInt(r.render_amt),
1770
+ },
1771
+ }))
1772
+ .filter((a) => a.adjusted < a.downAdjToBurn);
1773
+ },
1774
+ async getTxBurnAdjustments(trx, ps, log = consoleLogger) {
1775
+ if (!ps.toFill)
1776
+ return [];
1777
+ const q = trx.trx
1778
+ .selectFrom(TX_BURN_ADJUSTMENT_TABLE)
1779
+ .selectAll(TX_BURN_ADJUSTMENT_TABLE)
1780
+ .leftJoin(BURN_TABLE, `${BURN_TABLE}.tx_burn_adjustment_id`, `${TX_BURN_ADJUSTMENT_TABLE}.id`)
1781
+ .select(({ fn, lit }) => [
1782
+ fn
1783
+ .sum(fn.coalesce(`${BURN_TABLE}.from_escrow_usdc`, lit(0)))
1784
+ .as('tot_from_escrow'),
1785
+ ])
1786
+ .groupBy(`${TX_BURN_ADJUSTMENT_TABLE}.id`);
1787
+ log.info(`getTxBurnAdjustments ${q.compile().sql}`);
1788
+ const res = await q.execute();
1789
+ return res
1790
+ .map((r) => ({
1791
+ id: Number(r.id),
1792
+ createdAt: r.created_at,
1793
+ solTxId: Number(r.sol_tx_id),
1794
+ surplusUsdc: Number(r.surplus_usdc),
1795
+ consumed: Number(r.tot_from_escrow),
1796
+ }))
1797
+ .filter((a) => a.consumed < a.surplusUsdc);
1637
1798
  },
1638
1799
  async insertPolygonUpgrade(db, p, log = consoleLogger) {
1639
1800
  const solTxId = await getOrInsertTx(db.trx, {