@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.
- package/lib/cjs/client/pg/v2Client.js +230 -3
- package/lib/cjs/client/pg/v2Client.js.map +1 -1
- package/lib/cjs/index.js +3 -1
- package/lib/cjs/index.js.map +1 -1
- package/lib/esm/src/client/pg/v2Client.js +213 -2
- package/lib/esm/src/client/pg/v2Client.js.map +1 -1
- package/lib/esm/src/index.js +1 -0
- package/lib/esm/src/index.js.map +1 -1
- package/lib/esm/tsconfig.esm.tsbuildinfo +1 -1
- package/lib/types/src/client/pg/v2Client.d.ts +60 -1
- package/lib/types/src/client/pg/v2Client.d.ts.map +1 -1
- package/lib/types/src/index.d.ts +2 -1
- package/lib/types/src/index.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -21,6 +21,7 @@ export const ENTITY_EPOCH_INFO_TABLE = 'entity_epoch_info';
|
|
|
21
21
|
export const POLYGON_UPGRADE_TABLE = 'polygon_upgrade';
|
|
22
22
|
export const CURRENT_USER_GRANT_SPEND_TABLE = 'current_user_grant_spend';
|
|
23
23
|
export const USER_GRANT_SPEND_TABLE = 'user_grant_spend';
|
|
24
|
+
export const OTOY_GRANT_TABLE = 'otoy_grant';
|
|
24
25
|
export const GRANT_BURN_ID_TABLE = 'grant_burn_id';
|
|
25
26
|
export const GRANT_BURN_TABLE = 'grant_burn';
|
|
26
27
|
export const DISPERSED_SETTLEMENT_TABLE = 'dispersed_settlement';
|
|
@@ -28,6 +29,39 @@ export const DISPERSED_BURN_CORRECTION_TABLE = 'dispersed_burn_correction';
|
|
|
28
29
|
BigInt.prototype.toJSON = function () {
|
|
29
30
|
return this.toString();
|
|
30
31
|
};
|
|
32
|
+
// EUR -> render credits. OTOY issues grants in EUR (otoy_grant.amount_credits holds the raw OTOY
|
|
33
|
+
// confirmed_amount = EUR, despite the column name); 1 render credit = EUR 0.25, so credits = EUR x 4.
|
|
34
|
+
// job.render_amt / user_grant_spend.render_spent_delta are credits x 1e8.
|
|
35
|
+
export const EUR_TO_CREDITS = BigInt(4);
|
|
36
|
+
export const CREDIT_DECIMALS = BigInt(100000000); // 1e8
|
|
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;
|
|
55
|
+
let fifoGrantId;
|
|
56
|
+
for (const g of grants) {
|
|
57
|
+
if (drawn < g.amountCredits) {
|
|
58
|
+
fifoGrantId = g.id;
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
drawn -= g.amountCredits;
|
|
62
|
+
}
|
|
63
|
+
return { granted, consumed, avail, fifoGrantId };
|
|
64
|
+
};
|
|
31
65
|
import { begin } from './base';
|
|
32
66
|
import moment from 'moment';
|
|
33
67
|
pg.types.setTypeParser(1114, (str) => moment.utc(str).toDate());
|
|
@@ -150,6 +184,73 @@ export const pgClient = (config) => {
|
|
|
150
184
|
}
|
|
151
185
|
: undefined;
|
|
152
186
|
};
|
|
187
|
+
// Spendable grant per user, straight off otoy_grant (FIFO), net of what's already been spent or
|
|
188
|
+
// already settled by a burn correction. See GrantAllotment for the units + the double-spend guard.
|
|
189
|
+
const getGrantAllotments = async (db, p, log = consoleLogger) => {
|
|
190
|
+
const out = {};
|
|
191
|
+
if (!p.userIds.length)
|
|
192
|
+
return out;
|
|
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,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
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}`);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return out;
|
|
253
|
+
};
|
|
153
254
|
const updateTotalRndr = async (db, p, totalRndr, savedAt, log) => {
|
|
154
255
|
const res = (await db
|
|
155
256
|
.updateTable(EPOCH_TABLE)
|
|
@@ -999,11 +1100,23 @@ export const pgClient = (config) => {
|
|
|
999
1100
|
return;
|
|
1000
1101
|
}
|
|
1001
1102
|
}
|
|
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.
|
|
1106
|
+
let otoyGrantId = p.otoyGrantId;
|
|
1107
|
+
if (otoyGrantId === undefined && p.renderSpentDelta < BigInt(0)) {
|
|
1108
|
+
const allots = await getGrantAllotments(db.trx, { userIds: [p.userId] }, log);
|
|
1109
|
+
const a = allots[p.userId];
|
|
1110
|
+
if (a) {
|
|
1111
|
+
otoyGrantId = foldGrantAllotment(a.grants, a.consumed, BigInt(0)).fifoGrantId;
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1002
1114
|
const v = {
|
|
1003
1115
|
user_id: p.userId,
|
|
1004
1116
|
render_spent_delta: p.renderSpentDelta,
|
|
1005
1117
|
grant_burn_id: p.grantBurnId,
|
|
1006
1118
|
job_id: p.jobId,
|
|
1119
|
+
otoy_grant_id: otoyGrantId,
|
|
1007
1120
|
};
|
|
1008
1121
|
const res = await db.trx
|
|
1009
1122
|
.insertInto(USER_GRANT_SPEND_TABLE)
|
|
@@ -1348,6 +1461,103 @@ export const pgClient = (config) => {
|
|
|
1348
1461
|
}
|
|
1349
1462
|
return burns;
|
|
1350
1463
|
},
|
|
1464
|
+
async fetchBurnsPage(db, f, log = consoleLogger) {
|
|
1465
|
+
const lastId = f.cursor ? atob(f.cursor) : undefined;
|
|
1466
|
+
let q = db.trx
|
|
1467
|
+
.selectFrom('burn')
|
|
1468
|
+
.selectAll('burn')
|
|
1469
|
+
.innerJoin('sol_tx', 'sol_tx.id', 'burn.sol_tx_id')
|
|
1470
|
+
.select(['sol_tx.executed_at', 'sol_tx.sig']);
|
|
1471
|
+
if (!f.inclManual) {
|
|
1472
|
+
q = q.where('burn.usdc_spent', '>', String(0));
|
|
1473
|
+
}
|
|
1474
|
+
if (f.executedAfter) {
|
|
1475
|
+
q = q.where('sol_tx.executed_at', '>=', f.executedAfter);
|
|
1476
|
+
}
|
|
1477
|
+
if (f.executedBefore) {
|
|
1478
|
+
q = q.where('sol_tx.executed_at', '<=', f.executedBefore);
|
|
1479
|
+
}
|
|
1480
|
+
if (lastId) {
|
|
1481
|
+
q = q.where('burn.id', '>', lastId);
|
|
1482
|
+
}
|
|
1483
|
+
const limit = f.limit ?? 1000;
|
|
1484
|
+
q = q.limit(limit).orderBy('burn.id');
|
|
1485
|
+
const comp = q.compile();
|
|
1486
|
+
const { rows } = await timedQuery(pgPool, comp.sql, comp.parameters, 'fetchBurnsPage', log);
|
|
1487
|
+
const burns = rows.map((r) => ({
|
|
1488
|
+
jobs: [],
|
|
1489
|
+
id: Number(r.id),
|
|
1490
|
+
renderToUsdc: r.render_to_usdc,
|
|
1491
|
+
solTx: r.sig,
|
|
1492
|
+
executedAt: r.executed_at,
|
|
1493
|
+
pricedAt: r.priced_at,
|
|
1494
|
+
usdcSpent: BigInt(r.usdc_spent ?? 0),
|
|
1495
|
+
quoteAmt: BigInt(r.quote_amt ?? 0),
|
|
1496
|
+
burned: BigInt(r.burned),
|
|
1497
|
+
eurToUsdc: r.eur_to_usdc,
|
|
1498
|
+
markedBurnedAt: r.marked_burned_at,
|
|
1499
|
+
tags: r.tags ? r.tags.split(',') : [],
|
|
1500
|
+
}));
|
|
1501
|
+
return {
|
|
1502
|
+
burns,
|
|
1503
|
+
// Only hand back a cursor on a full page; a short page means we're done.
|
|
1504
|
+
cursor: burns.length === limit
|
|
1505
|
+
? btoa(String(burns[burns.length - 1].id))
|
|
1506
|
+
: undefined,
|
|
1507
|
+
};
|
|
1508
|
+
},
|
|
1509
|
+
async fetchBurnJobs(db, burnId, log = consoleLogger) {
|
|
1510
|
+
const rows = await db.trx
|
|
1511
|
+
.selectFrom('job')
|
|
1512
|
+
.selectAll('job')
|
|
1513
|
+
.where('burn_id', '=', String(burnId))
|
|
1514
|
+
.execute();
|
|
1515
|
+
log.debug(`fetchBurnJobs burn ${burnId} -> ${rows.length} jobs`);
|
|
1516
|
+
return rows.map((r) => ({
|
|
1517
|
+
id: Number(r.id),
|
|
1518
|
+
completedAt: r.completed_at,
|
|
1519
|
+
rndrUsed: BigInt(r.render_amt),
|
|
1520
|
+
obhUsed: r.obh_used ? Number(r.obh_used) : undefined,
|
|
1521
|
+
}));
|
|
1522
|
+
},
|
|
1523
|
+
async fetchDailyBurnStats(db, f, log = consoleLogger) {
|
|
1524
|
+
// sol_tx.executed_at is `timestamp WITHOUT time zone` already holding UTC wall-clock, so truncate it
|
|
1525
|
+
// DIRECTLY. Do not write `executed_at at time zone 'UTC'`: that promotes it to timestamptz, after which
|
|
1526
|
+
// date_trunc truncates in the Postgres session's TimeZone — measured against prod, a Tokyo session moved
|
|
1527
|
+
// the bucket boundary to 15:00Z and a Los_Angeles session changed the bucket COUNT (783 vs 786).
|
|
1528
|
+
// Truncating the naive column is pure arithmetic and identical under every session timezone, and
|
|
1529
|
+
// to_char hands back a plain 'YYYY-MM-DD' so node-postgres has no Date to decode in local time.
|
|
1530
|
+
const dayExpr = sql `to_char(date_trunc('day', sol_tx.executed_at), 'YYYY-MM-DD')`;
|
|
1531
|
+
let q = db.trx
|
|
1532
|
+
.selectFrom('burn')
|
|
1533
|
+
.innerJoin('sol_tx', 'sol_tx.id', 'burn.sol_tx_id')
|
|
1534
|
+
.select(({ fn }) => [
|
|
1535
|
+
dayExpr.as('day'),
|
|
1536
|
+
fn.sum('burn.burned').as('render_burned'),
|
|
1537
|
+
fn.sum('burn.usdc_spent').as('usdc_spent'),
|
|
1538
|
+
fn.count('burn.id').as('burn_count'),
|
|
1539
|
+
])
|
|
1540
|
+
.groupBy(sql `date_trunc('day', sol_tx.executed_at)`)
|
|
1541
|
+
.orderBy(sql `date_trunc('day', sol_tx.executed_at)`);
|
|
1542
|
+
if (!f.inclManual) {
|
|
1543
|
+
q = q.where('burn.usdc_spent', '>', String(0));
|
|
1544
|
+
}
|
|
1545
|
+
if (f.executedAfter) {
|
|
1546
|
+
q = q.where('sol_tx.executed_at', '>=', f.executedAfter);
|
|
1547
|
+
}
|
|
1548
|
+
if (f.executedBefore) {
|
|
1549
|
+
q = q.where('sol_tx.executed_at', '<=', f.executedBefore);
|
|
1550
|
+
}
|
|
1551
|
+
const comp = q.compile();
|
|
1552
|
+
const { rows } = await timedQuery(pgPool, comp.sql, comp.parameters, 'fetchDailyBurnStats', log);
|
|
1553
|
+
log.info(`fetchDailyBurnStats len ${rows.length}`);
|
|
1554
|
+
return rows.map((r) => ({
|
|
1555
|
+
day: r.day,
|
|
1556
|
+
renderBurned: BigInt(r.render_burned ?? 0),
|
|
1557
|
+
usdcSpent: BigInt(r.usdc_spent ?? 0),
|
|
1558
|
+
burnCount: Number(r.burn_count ?? 0),
|
|
1559
|
+
}));
|
|
1560
|
+
},
|
|
1351
1561
|
async fetchBurnsAndGrants(db, f, log = consoleLogger) {
|
|
1352
1562
|
// Build a unified query to get all transactions with both buy burns and grant burns
|
|
1353
1563
|
let baseQuery = db.trx
|
|
@@ -1751,7 +1961,6 @@ export const pgClient = (config) => {
|
|
|
1751
1961
|
.select([
|
|
1752
1962
|
'tx_signature',
|
|
1753
1963
|
'claim_uuid',
|
|
1754
|
-
'obligation_uuids',
|
|
1755
1964
|
'settled_amount',
|
|
1756
1965
|
'burned_amount',
|
|
1757
1966
|
'usdc_spent',
|
|
@@ -1765,7 +1974,6 @@ export const pgClient = (config) => {
|
|
|
1765
1974
|
return rows.map((r) => ({
|
|
1766
1975
|
txSignature: r.tx_signature,
|
|
1767
1976
|
claimUuid: r.claim_uuid,
|
|
1768
|
-
obligationUuids: r.obligation_uuids,
|
|
1769
1977
|
settledAmount: r.settled_amount,
|
|
1770
1978
|
burnedAmount: r.burned_amount,
|
|
1771
1979
|
usdcSpent: r.usdc_spent ?? null,
|
|
@@ -1979,6 +2187,9 @@ export const pgClient = (config) => {
|
|
|
1979
2187
|
async getCurrentUserGrantSpend(db, f, log = consoleLogger) {
|
|
1980
2188
|
return await getCurrentUserGrantSpend(db.trx, f, log);
|
|
1981
2189
|
},
|
|
2190
|
+
async getGrantAllotments(db, f, log = consoleLogger) {
|
|
2191
|
+
return await getGrantAllotments(db.trx, f, log);
|
|
2192
|
+
},
|
|
1982
2193
|
async deleteAll(log = consoleLogger) {
|
|
1983
2194
|
if (config.database.includes('render')) {
|
|
1984
2195
|
throw new Error('cannot nuke from deployed database. call from tests only');
|