@render-foundation/utils 0.0.255 → 0.0.256

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.
@@ -1506,7 +1506,6 @@ export const pgClient = (config) => {
1506
1506
  eurToUsdc: r.eur_to_usdc,
1507
1507
  markedBurnedAt: r.marked_burned_at,
1508
1508
  tags: r.tags ? r.tags.split(',') : [],
1509
- fromEscrowUsdc: BigInt(r.from_escrow_usdc ?? 0),
1510
1509
  });
1511
1510
  burnIdToIdx[r.id] = burns.length - 1;
1512
1511
  }
@@ -1529,133 +1528,70 @@ export const pgClient = (config) => {
1529
1528
  return burns;
1530
1529
  },
1531
1530
  async fetchBurnsPage(db, f, log = consoleLogger) {
1532
- const params = [];
1533
- const p = (v) => {
1534
- params.push(v);
1535
- return `$${params.length}`;
1536
- };
1537
- // Decode cursor — supports new {t,id,ea} format and legacy bare-id format.
1538
- let cursorEa;
1539
- let cursorType;
1540
- let cursorId;
1541
- if (f.cursor) {
1542
- const raw = atob(f.cursor);
1543
- try {
1544
- const c = JSON.parse(raw);
1545
- cursorEa = new Date(c.ea);
1546
- cursorType = c.t;
1547
- cursorId = c.id;
1548
- }
1549
- catch {
1550
- // Legacy cursor: bare burn.id — treat as buy-burn cursor.
1551
- cursorId = Number(raw);
1552
- cursorType = 'buy';
1553
- }
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);
1554
1549
  }
1555
- // Shared WHERE fragments pushed into each union leg for index use.
1556
- const dateFilters = [];
1557
- if (f.executedAfter)
1558
- dateFilters.push(`st.executed_at >= ${p(f.executedAfter)}`);
1559
- if (f.executedBefore)
1560
- dateFilters.push(`st.executed_at <= ${p(f.executedBefore)}`);
1561
- const sigFilter = f.sig ? `st.sig = ${p(f.sig)}` : '';
1562
- // Buy-burn leg
1563
- const buyWhere = [...dateFilters];
1564
- if (!f.inclManual)
1565
- buyWhere.push('b.usdc_spent > 0');
1566
- if (sigFilter)
1567
- buyWhere.push(sigFilter);
1568
- if (f.jobId !== undefined) {
1569
- buyWhere.push(`b.id IN (SELECT burn_id FROM job WHERE id = ${p(f.jobId)})`);
1570
- }
1571
- const buyWhereClause = buyWhere.length ? `WHERE ${buyWhere.join(' AND ')}` : '';
1572
- const buySql = `
1573
- SELECT 'buy'::text AS type, b.id::bigint AS sortid, b.id AS id,
1574
- st.executed_at, b.priced_at, b.burned::text AS burned,
1575
- b.usdc_spent::text AS usdc_spent, b.quote_amt::text AS quote_amt,
1576
- b.tags, st.sig, b.render_to_usdc, b.eur_to_usdc,
1577
- coalesce(b.from_escrow_usdc, 0)::text AS from_escrow_usdc,
1578
- b.down_correction_id, b.escrow_correction_id,
1579
- b.marked_burned_at
1580
- FROM burn b
1581
- INNER JOIN sol_tx st ON st.id = b.sol_tx_id
1582
- ${buyWhereClause}`;
1583
- // Grant-burn leg
1584
- const grantWhere = [...dateFilters];
1585
- if (sigFilter)
1586
- grantWhere.push(sigFilter);
1587
1550
  if (f.jobId !== undefined) {
1588
- grantWhere.push(`gb.id IN (SELECT grant_burn_id FROM job WHERE id = ${p(f.jobId)})`);
1589
- }
1590
- const grantWhereClause = grantWhere.length ? `WHERE ${grantWhere.join(' AND ')}` : '';
1591
- const grantSql = `
1592
- SELECT 'grant'::text AS type, gb.id::bigint AS sortid, gb.id AS id,
1593
- st.executed_at, null::timestamp AS priced_at, gb.burned::text AS burned,
1594
- '0' AS usdc_spent, '0' AS quote_amt,
1595
- gb.tags, st.sig, null::float8 AS render_to_usdc, null::float8 AS eur_to_usdc,
1596
- '0' AS from_escrow_usdc,
1597
- null::bigint AS down_correction_id, null::bigint AS escrow_correction_id,
1598
- gb.marked_burned_at
1599
- FROM grant_burn gb
1600
- INNER JOIN sol_tx st ON st.id = gb.sol_tx_id
1601
- ${grantWhereClause}`;
1602
- // Cursor filter on the outer query — row comparison for deterministic paging.
1603
- let cursorFilter = '';
1604
- if (cursorEa && cursorType && cursorId !== undefined) {
1605
- cursorFilter = `WHERE (u.executed_at, u.type, u.sortid) > (${p(cursorEa)}, ${p(cursorType)}, ${p(cursorId)})`;
1606
- }
1607
- else if (cursorId !== undefined) {
1608
- // Legacy cursor fallback: only buy burns, ordered by id.
1609
- cursorFilter = `WHERE u.type = 'buy' AND u.sortid > ${p(cursorId)}`;
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);
1610
1561
  }
1611
1562
  const limit = f.limit ?? 1000;
1612
- const sql = `
1613
- SELECT u.* FROM (
1614
- ${buySql}
1615
- UNION ALL
1616
- ${grantSql}
1617
- ) u
1618
- ${cursorFilter}
1619
- ORDER BY u.executed_at ASC, u.type, u.sortid
1620
- LIMIT ${p(limit)}`;
1621
- const { rows } = await timedQuery(pgPool, sql, params, 'fetchBurnsPage', log);
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);
1622
1566
  const burns = rows.map((r) => ({
1623
1567
  jobs: [],
1624
1568
  id: Number(r.id),
1625
- type: r.type,
1626
- renderToUsdc: r.render_to_usdc ?? undefined,
1569
+ renderToUsdc: r.render_to_usdc,
1627
1570
  solTx: r.sig,
1628
1571
  executedAt: r.executed_at,
1629
- pricedAt: r.priced_at ?? undefined,
1572
+ pricedAt: r.priced_at,
1630
1573
  usdcSpent: BigInt(r.usdc_spent ?? 0),
1631
1574
  quoteAmt: BigInt(r.quote_amt ?? 0),
1632
1575
  burned: BigInt(r.burned),
1633
- eurToUsdc: r.eur_to_usdc ?? undefined,
1576
+ eurToUsdc: r.eur_to_usdc,
1634
1577
  markedBurnedAt: r.marked_burned_at,
1635
1578
  tags: r.tags ? r.tags.split(',') : [],
1636
- fromEscrowUsdc: BigInt(r.from_escrow_usdc ?? 0),
1637
- downCorrectionId: r.down_correction_id ? Number(r.down_correction_id) : undefined,
1638
- escrowCorrectionId: r.escrow_correction_id ? Number(r.escrow_correction_id) : undefined,
1639
1579
  }));
1640
1580
  return {
1641
1581
  burns,
1582
+ // Only hand back a cursor on a full page; a short page means we're done.
1642
1583
  cursor: burns.length === limit
1643
- ? btoa(JSON.stringify({
1644
- ea: rows[burns.length - 1].executed_at,
1645
- t: rows[burns.length - 1].type,
1646
- id: rows[burns.length - 1].sortid,
1647
- }))
1584
+ ? btoa(String(burns[burns.length - 1].id))
1648
1585
  : undefined,
1649
1586
  };
1650
1587
  },
1651
- async fetchBurnJobs(db, burnId, burnType = 'buy', log = consoleLogger) {
1652
- const fk = burnType === 'grant' ? 'grant_burn_id' : 'burn_id';
1588
+ async fetchBurnJobs(db, burnId, log = consoleLogger) {
1653
1589
  const rows = await db.trx
1654
1590
  .selectFrom('job')
1655
1591
  .selectAll('job')
1656
- .where(fk, '=', String(burnId))
1592
+ .where('burn_id', '=', String(burnId))
1657
1593
  .execute();
1658
- log.debug(`fetchBurnJobs ${burnType} ${burnId} -> ${rows.length} jobs`);
1594
+ log.debug(`fetchBurnJobs burn ${burnId} -> ${rows.length} jobs`);
1659
1595
  return rows.map((r) => ({
1660
1596
  id: Number(r.id),
1661
1597
  completedAt: r.completed_at,
@@ -1676,8 +1612,8 @@ export const pgClient = (config) => {
1676
1612
  .innerJoin('sol_tx', 'sol_tx.id', 'burn.sol_tx_id')
1677
1613
  .select(({ fn }) => [
1678
1614
  dayExpr.as('day'),
1679
- sql `SUM(CASE WHEN burn.burned > 0 THEN burn.burned WHEN COALESCE(burn.from_escrow_usdc, 0) > 0 AND burn.render_to_usdc > 0 THEN (burn.from_escrow_usdc::numeric / burn.render_to_usdc * 100)::bigint ELSE 0 END)`.as('render_burned'),
1680
- sql `SUM(CASE WHEN burn.usdc_spent > 0 THEN burn.usdc_spent ELSE COALESCE(burn.from_escrow_usdc, 0) END)`.as('usdc_spent'),
1615
+ fn.sum('burn.burned').as('render_burned'),
1616
+ fn.sum('burn.usdc_spent').as('usdc_spent'),
1681
1617
  fn.count('burn.id').as('burn_count'),
1682
1618
  ])
1683
1619
  .groupBy(sql `date_trunc('day', sol_tx.executed_at)`)
@@ -1957,6 +1893,21 @@ export const pgClient = (config) => {
1957
1893
  });
1958
1894
  },
1959
1895
  async insertDispersedOutbox(p, log = consoleLogger) {
1896
+ // Check if dispersed_obligation table exists BEFORE the transaction so a missing table never
1897
+ // poisons the txn (PG aborts the entire transaction on any error, even a caught one).
1898
+ let hasObligationTable = false;
1899
+ if (p.obligations?.length) {
1900
+ try {
1901
+ const { rows } = await timedQuery(pgPool, `SELECT 1 FROM information_schema.tables WHERE table_name = 'dispersed_obligation' LIMIT 1`, [], 'checkObligationTable', log);
1902
+ hasObligationTable = rows.length > 0;
1903
+ }
1904
+ catch {
1905
+ hasObligationTable = false;
1906
+ }
1907
+ if (!hasObligationTable) {
1908
+ log.warn(`dispersed obligations: table not found (migration 44 pending); skipping for tx ${p.txSignature}`);
1909
+ }
1910
+ }
1960
1911
  // The outbox row is the durable, crash-safe anchor persisted BEFORE broadcast. When it carries a
1961
1912
  // burn-correction draw, we increment consumed_render in the SAME transaction as the insert, bounded
1962
1913
  // so it can never exceed amount_render. Row + counter are therefore atomic and inseparable: no
@@ -1964,7 +1915,7 @@ export const pgClient = (config) => {
1964
1915
  // (decremented again by voidDispersedOutbox). If the bounded increment can't fit (a concurrent draw
1965
1916
  // took the room), the whole txn rolls back and we return {inserted:false} — the caller must NOT
1966
1917
  // broadcast (no row, no counter change, no burn).
1967
- return await db.transaction().execute(async (trx) => {
1918
+ const result = await db.transaction().execute(async (trx) => {
1968
1919
  const res = await trx
1969
1920
  .insertInto(DISPERSED_SETTLEMENT_TABLE)
1970
1921
  .values({
@@ -2005,13 +1956,29 @@ export const pgClient = (config) => {
2005
1956
  throw new DispersedCorrectionRejected();
2006
1957
  }
2007
1958
  }
2008
- log.debug(`dispersed outbox tx ${p.txSignature}: ${res.numInsertedOrUpdatedRows} row(s)`);
1959
+ if (inserted && hasObligationTable && p.obligations?.length) {
1960
+ for (const o of p.obligations) {
1961
+ await trx
1962
+ .insertInto('dispersed_obligation')
1963
+ .values({
1964
+ settlement_tx_sig: p.txSignature,
1965
+ uuid: o.uuid,
1966
+ amount_eur: o.amountEur,
1967
+ opened_at: o.openedAt,
1968
+ authorization_uuid: o.authorizationUuid ?? null,
1969
+ })
1970
+ .onConflict((oc) => oc.column('uuid').doNothing())
1971
+ .execute();
1972
+ }
1973
+ }
1974
+ log.debug(`dispersed outbox tx ${p.txSignature}: ${res.numInsertedOrUpdatedRows} row(s), ${hasObligationTable ? (p.obligations?.length ?? 0) : 0} obligations`);
2009
1975
  return { inserted };
2010
1976
  }).catch((e) => {
2011
1977
  if (e instanceof DispersedCorrectionRejected)
2012
1978
  return { inserted: false };
2013
1979
  throw e;
2014
1980
  });
1981
+ return result;
2015
1982
  },
2016
1983
  async getOutstandingDispersedCorrection(log = consoleLogger) {
2017
1984
  // consumed_render is the atomic source of truth; outstanding = amount_render - consumed_render.