@render-foundation/utils 0.0.254 → 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.
- package/lib/cjs/burn/burnCalculations.js +140 -0
- package/lib/cjs/burn/burnCalculations.js.map +1 -0
- package/lib/cjs/client/dispersed.js.map +1 -1
- package/lib/cjs/client/pg/base.js.map +1 -1
- package/lib/cjs/client/pg/v2Client.js +245 -206
- package/lib/cjs/client/pg/v2Client.js.map +1 -1
- package/lib/cjs/index.js +4 -2
- package/lib/cjs/index.js.map +1 -1
- package/lib/esm/src/burn/burnCalculations.js +124 -0
- package/lib/esm/src/burn/burnCalculations.js.map +1 -0
- package/lib/esm/src/client/dispersed.js.map +1 -1
- package/lib/esm/src/client/pg/base.js.map +1 -1
- package/lib/esm/src/client/pg/v2Client.js +227 -191
- package/lib/esm/src/client/pg/v2Client.js.map +1 -1
- package/lib/esm/src/index.js +1 -1
- package/lib/esm/src/index.js.map +1 -1
- package/lib/esm/tsconfig.esm.tsbuildinfo +1 -1
- package/lib/types/src/burn/burnCalculations.d.ts +52 -0
- package/lib/types/src/burn/burnCalculations.d.ts.map +1 -0
- package/lib/types/src/client/dispersed.d.ts +3 -0
- package/lib/types/src/client/dispersed.d.ts.map +1 -1
- package/lib/types/src/client/pg/base.d.ts +1 -5
- package/lib/types/src/client/pg/base.d.ts.map +1 -1
- package/lib/types/src/client/pg/v2Client.d.ts +70 -12
- package/lib/types/src/client/pg/v2Client.d.ts.map +1 -1
- package/lib/types/src/dbTypesV2.d.ts +12 -0
- package/lib/types/src/dbTypesV2.d.ts.map +1 -1
- package/lib/types/src/index.d.ts +2 -2
- package/lib/types/src/index.d.ts.map +1 -1
- package/package.json +2 -2
|
@@ -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.
|
|
38
|
+
exports.pgClient = exports.batchFinishedAtMedian = exports.foldGrantWindow = exports.GRANT_LOOKBACK_MS = exports.GRANT_LOOKBACK_DAYS = exports.CREDIT_DECIMALS = exports.GRANT_WINDOW_START = 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");
|
|
@@ -71,36 +71,117 @@ BigInt.prototype.toJSON = function () {
|
|
|
71
71
|
// confirmed_amount = EUR, despite the column name); 1 render credit = EUR 0.25, so credits = EUR x 4.
|
|
72
72
|
// job.render_amt / user_grant_spend.render_spent_delta are credits x 1e8.
|
|
73
73
|
exports.EUR_TO_CREDITS = BigInt(4);
|
|
74
|
+
// Grant accounting window: otoy_grant mirrors OTOY RFG issuances from this date. Consumption against the
|
|
75
|
+
// window counts EVERY job the user ran in it — buy-and-burn jobs consumed OTOY credits exactly like
|
|
76
|
+
// emissions-funded ones (counting only emissions-attributed spend over-issued ~2.58M credits, 2026-08-04).
|
|
77
|
+
exports.GRANT_WINDOW_START = '2025-05-01';
|
|
74
78
|
exports.CREDIT_DECIMALS = BigInt(100000000); // 1e8
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
79
|
+
// A grant is consumable for this long after issuance; whatever is left after that EXPIRES (2026-08-05
|
|
80
|
+
// model change). A job can only draw grants issued within the lookback window before it ran.
|
|
81
|
+
exports.GRANT_LOOKBACK_DAYS = 60;
|
|
82
|
+
exports.GRANT_LOOKBACK_MS = exports.GRANT_LOOKBACK_DAYS * 24 * 3600 * 1000;
|
|
83
|
+
const foldGrantWindow = (events, opts = {}) => {
|
|
84
|
+
var _a, _b, _c;
|
|
85
|
+
const lookback = (_a = opts.lookbackMs) !== null && _a !== void 0 ? _a : exports.GRANT_LOOKBACK_MS;
|
|
86
|
+
const now = (_b = opts.now) !== null && _b !== void 0 ? _b : Date.now();
|
|
87
|
+
const sorted = [...events].sort((a, b) => a.t - b.t || (a.amt > b.amt ? -1 : 1)); // credits first on ties
|
|
88
|
+
const buckets = [];
|
|
89
|
+
const jobs = [];
|
|
90
|
+
const remaining = (b) => b.amount - b.reversed - b.drawnEmissions - b.drawnMisrouted;
|
|
91
|
+
let beyond = BigInt(0);
|
|
92
|
+
for (const e of sorted) {
|
|
93
|
+
if (e.src === 'grant') {
|
|
94
|
+
if (e.amt > BigInt(0)) {
|
|
95
|
+
buckets.push({
|
|
96
|
+
grantId: e.grantId,
|
|
97
|
+
t: e.t,
|
|
98
|
+
amount: e.amt,
|
|
99
|
+
reversed: BigInt(0),
|
|
100
|
+
drawnEmissions: BigInt(0),
|
|
101
|
+
drawnMisrouted: BigInt(0),
|
|
102
|
+
expired: BigInt(0),
|
|
103
|
+
left: BigInt(0),
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
let claw = -e.amt; // clawback hits remaining capacity oldest-first; excess is dropped
|
|
108
|
+
for (const b of buckets) {
|
|
109
|
+
if (claw <= BigInt(0))
|
|
110
|
+
break;
|
|
111
|
+
const take = remaining(b) < claw ? remaining(b) : claw;
|
|
112
|
+
if (take > BigInt(0)) {
|
|
113
|
+
b.reversed += take;
|
|
114
|
+
claw -= take;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
let need = -e.amt;
|
|
121
|
+
const total = need;
|
|
122
|
+
let emissionsLeft = (_c = e.emissions) !== null && _c !== void 0 ? _c : need; // no split provided -> count it all as emissions
|
|
123
|
+
let jobEm = BigInt(0);
|
|
124
|
+
let jobMis = BigInt(0);
|
|
125
|
+
for (const b of buckets) {
|
|
126
|
+
if (need <= BigInt(0))
|
|
127
|
+
break;
|
|
128
|
+
if (b.t + lookback < e.t)
|
|
129
|
+
continue; // grant expired before this job ran
|
|
130
|
+
const take = remaining(b) < need ? remaining(b) : need;
|
|
131
|
+
if (take <= BigInt(0))
|
|
132
|
+
continue;
|
|
133
|
+
const em = emissionsLeft < take ? emissionsLeft : take;
|
|
134
|
+
b.drawnEmissions += em;
|
|
135
|
+
b.drawnMisrouted += take - em;
|
|
136
|
+
jobEm += em;
|
|
137
|
+
jobMis += take - em;
|
|
138
|
+
emissionsLeft -= em;
|
|
139
|
+
need -= take;
|
|
140
|
+
}
|
|
141
|
+
beyond += need;
|
|
142
|
+
jobs.push({
|
|
143
|
+
id: e.id,
|
|
144
|
+
t: e.t,
|
|
145
|
+
need: total,
|
|
146
|
+
drawnEmissions: jobEm,
|
|
147
|
+
drawnMisrouted: jobMis,
|
|
148
|
+
beyond: need,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
let avail = BigInt(0);
|
|
153
|
+
let expired = BigInt(0);
|
|
154
|
+
let usedEmissions = BigInt(0);
|
|
155
|
+
let usedMisrouted = BigInt(0);
|
|
93
156
|
let fifoGrantId;
|
|
94
|
-
for (const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
157
|
+
for (const b of buckets) {
|
|
158
|
+
usedEmissions += b.drawnEmissions;
|
|
159
|
+
usedMisrouted += b.drawnMisrouted;
|
|
160
|
+
const rem = remaining(b);
|
|
161
|
+
if (b.t + lookback < now) {
|
|
162
|
+
b.expired = rem;
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
b.left = rem;
|
|
166
|
+
avail += rem;
|
|
167
|
+
if (fifoGrantId === undefined && rem > BigInt(0) && b.grantId !== undefined)
|
|
168
|
+
fifoGrantId = b.grantId;
|
|
98
169
|
}
|
|
99
|
-
|
|
170
|
+
expired += b.expired;
|
|
100
171
|
}
|
|
101
|
-
return {
|
|
172
|
+
return {
|
|
173
|
+
avail,
|
|
174
|
+
beyondGrants: beyond,
|
|
175
|
+
jobCharged: usedEmissions + usedMisrouted,
|
|
176
|
+
usedEmissions,
|
|
177
|
+
usedMisrouted,
|
|
178
|
+
expired,
|
|
179
|
+
buckets,
|
|
180
|
+
jobs,
|
|
181
|
+
fifoGrantId,
|
|
182
|
+
};
|
|
102
183
|
};
|
|
103
|
-
exports.
|
|
184
|
+
exports.foldGrantWindow = foldGrantWindow;
|
|
104
185
|
const base_2 = require("./base");
|
|
105
186
|
const moment_1 = __importDefault(require("moment"));
|
|
106
187
|
pg.types.setTypeParser(1114, (str) => moment_1.default.utc(str).toDate());
|
|
@@ -228,66 +309,54 @@ const pgClient = (config) => {
|
|
|
228
309
|
// Spendable grant per user, straight off otoy_grant (FIFO), net of what's already been spent or
|
|
229
310
|
// already settled by a burn correction. See GrantAllotment for the units + the double-spend guard.
|
|
230
311
|
const getGrantAllotments = (db, p, log = logger_1.consoleLogger) => __awaiter(void 0, void 0, void 0, function* () {
|
|
231
|
-
var _b, _c
|
|
312
|
+
var _b, _c;
|
|
232
313
|
const out = {};
|
|
233
314
|
if (!p.userIds.length)
|
|
234
315
|
return out;
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
.
|
|
258
|
-
.
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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,
|
|
316
|
+
// Raw event stream, folded in code (foldGrantWindow — per-grant buckets, 60-day consumable window,
|
|
317
|
+
// reversals clamp capacity, only jobs create beyond-grants). SQL can't express the bucketed clamp.
|
|
318
|
+
const evRes = yield (0, kysely_1.sql) `
|
|
319
|
+
select user_id::text as u, coalesce(otoy_created_at, ${exports.GRANT_WINDOW_START}::timestamp) as t,
|
|
320
|
+
(amount_credits * 4 * 1e8)::numeric::text as amt, 'grant' as src, id as gid
|
|
321
|
+
from otoy_grant where user_id = any(${p.userIds}::uuid[])
|
|
322
|
+
union all
|
|
323
|
+
select user_id::text, ${exports.GRANT_WINDOW_START}::timestamp, sum(render_spent_delta)::text, 'grant', null
|
|
324
|
+
from user_grant_spend
|
|
325
|
+
where user_id = any(${p.userIds}::uuid[]) and render_spent_delta > 0 and otoy_grant_id is null
|
|
326
|
+
group by user_id
|
|
327
|
+
union all
|
|
328
|
+
select user_id::text, created_at, (-render_spent_delta)::text, 'grant', null
|
|
329
|
+
from user_grant_spend
|
|
330
|
+
where user_id = any(${p.userIds}::uuid[]) and render_spent_delta > 0 and otoy_grant_id is not null
|
|
331
|
+
union all
|
|
332
|
+
select user_id, completed_at, (-render_amt)::text, 'job', null
|
|
333
|
+
from job
|
|
334
|
+
where user_id = any(${p.userIds}::text[]) and completed_at >= ${exports.GRANT_WINDOW_START}::timestamp
|
|
335
|
+
`.execute(db); // db is always a Kysely trx at runtime
|
|
336
|
+
const byUser = new Map();
|
|
337
|
+
for (const r of evRes.rows) {
|
|
338
|
+
const arr = (_b = byUser.get(r.u)) !== null && _b !== void 0 ? _b : [];
|
|
339
|
+
arr.push({
|
|
340
|
+
t: new Date(r.t).getTime(),
|
|
341
|
+
amt: BigInt(Math.round(Number(r.amt))),
|
|
342
|
+
src: r.src,
|
|
343
|
+
grantId: (_c = r.gid) !== null && _c !== void 0 ? _c : undefined,
|
|
280
344
|
});
|
|
345
|
+
byUser.set(r.u, arr);
|
|
281
346
|
}
|
|
282
|
-
for (const u of
|
|
283
|
-
const
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
347
|
+
for (const [u, events] of byUser) {
|
|
348
|
+
const f = (0, exports.foldGrantWindow)(events);
|
|
349
|
+
out[u] = {
|
|
350
|
+
userId: u,
|
|
351
|
+
granted: events.reduce((a, e) => (e.src === 'grant' && e.amt > BigInt(0) ? a + e.amt : a), BigInt(0)),
|
|
352
|
+
consumed: f.jobCharged,
|
|
353
|
+
avail: f.avail,
|
|
354
|
+
expired: f.expired,
|
|
355
|
+
fifoGrantId: f.fifoGrantId,
|
|
356
|
+
};
|
|
357
|
+
if (f.beyondGrants > BigInt(0)) {
|
|
358
|
+
log.info(`grant allotment ${u}: ${f.beyondGrants} beyond grants (purchase-paid at the time), ` +
|
|
359
|
+
`${f.expired} expired past the ${exports.GRANT_LOOKBACK_DAYS}d window — avail ${f.avail}`);
|
|
291
360
|
}
|
|
292
361
|
}
|
|
293
362
|
return out;
|
|
@@ -372,7 +441,7 @@ const pgClient = (config) => {
|
|
|
372
441
|
const sequenceNumbers = [];
|
|
373
442
|
const dbEpochIds = [];
|
|
374
443
|
for (const bt of bts) {
|
|
375
|
-
let { solKey: toSolKey, seq, createdAt, amount, upgradeBracket, priorSupply, points, epochId, } = bt;
|
|
444
|
+
let { solKey: toSolKey, seq, createdAt, amount, upgradeBracket, priorSupply, points, epochId, sender, } = bt;
|
|
376
445
|
let entityId;
|
|
377
446
|
if (toSolKey && toSolKey != '') {
|
|
378
447
|
entityId = yield getOrInsertEntity(db, { solKey: toSolKey });
|
|
@@ -393,6 +462,7 @@ const pgClient = (config) => {
|
|
|
393
462
|
prior_supply: priorSupply,
|
|
394
463
|
points: points,
|
|
395
464
|
epoch_id: dbEpochId,
|
|
465
|
+
sender: sender,
|
|
396
466
|
})
|
|
397
467
|
.onConflict((oc) => oc.column('sequence').doUpdateSet({
|
|
398
468
|
created_at: createdAt,
|
|
@@ -402,6 +472,7 @@ const pgClient = (config) => {
|
|
|
402
472
|
points: points,
|
|
403
473
|
epoch_id: dbEpochId,
|
|
404
474
|
entity_id: entityId,
|
|
475
|
+
sender: sender,
|
|
405
476
|
}))
|
|
406
477
|
.returning('sequence');
|
|
407
478
|
const c = q.compile();
|
|
@@ -1156,7 +1227,7 @@ const pgClient = (config) => {
|
|
|
1156
1227
|
});
|
|
1157
1228
|
},
|
|
1158
1229
|
insertGrantSpend(db, p, log = logger_1.consoleLogger) {
|
|
1159
|
-
var _a, _b, _c;
|
|
1230
|
+
var _a, _b, _c, _d;
|
|
1160
1231
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1161
1232
|
const existing = yield getCurrentUserGrantSpend(db.trx, { userId: p.userId }, log);
|
|
1162
1233
|
const diff = {
|
|
@@ -1183,16 +1254,13 @@ const pgClient = (config) => {
|
|
|
1183
1254
|
return;
|
|
1184
1255
|
}
|
|
1185
1256
|
}
|
|
1186
|
-
// FIFO-attribute this spend to the user's oldest OTOY grant that still has room
|
|
1187
|
-
//
|
|
1188
|
-
// would leave the grant looking unspent
|
|
1257
|
+
// FIFO-attribute this spend to the user's oldest LIVE OTOY grant that still has room (the fold
|
|
1258
|
+
// already applies the lookback window). Stamping otoy_grant_id is what shows the draw in the
|
|
1259
|
+
// admin drill-down — an unattributed row would leave the grant looking unspent there.
|
|
1189
1260
|
let otoyGrantId = p.otoyGrantId;
|
|
1190
1261
|
if (otoyGrantId === undefined && p.renderSpentDelta < BigInt(0)) {
|
|
1191
1262
|
const allots = yield getGrantAllotments(db.trx, { userIds: [p.userId] }, log);
|
|
1192
|
-
|
|
1193
|
-
if (a) {
|
|
1194
|
-
otoyGrantId = (0, exports.foldGrantAllotment)(a.grants, a.consumed, BigInt(0)).fifoGrantId;
|
|
1195
|
-
}
|
|
1263
|
+
otoyGrantId = (_c = allots[p.userId]) === null || _c === void 0 ? void 0 : _c.fifoGrantId;
|
|
1196
1264
|
}
|
|
1197
1265
|
const v = {
|
|
1198
1266
|
user_id: p.userId,
|
|
@@ -1218,7 +1286,7 @@ const pgClient = (config) => {
|
|
|
1218
1286
|
user_id: p.userId,
|
|
1219
1287
|
render_spent: p.renderSpentDelta,
|
|
1220
1288
|
description: p.description,
|
|
1221
|
-
perpetual: (
|
|
1289
|
+
perpetual: (_d = p.perpetual) !== null && _d !== void 0 ? _d : false,
|
|
1222
1290
|
})
|
|
1223
1291
|
.onConflict((oc) => oc.column('user_id').doUpdateSet(diff))
|
|
1224
1292
|
.returning((eb) => [
|
|
@@ -1563,138 +1631,75 @@ const pgClient = (config) => {
|
|
|
1563
1631
|
fetchBurnsPage(db, f, log = logger_1.consoleLogger) {
|
|
1564
1632
|
var _a;
|
|
1565
1633
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1566
|
-
const
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
if (f.
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
// Legacy cursor: bare burn.id — treat as buy-burn cursor.
|
|
1585
|
-
cursorId = Number(raw);
|
|
1586
|
-
cursorType = 'buy';
|
|
1587
|
-
}
|
|
1634
|
+
const lastId = f.cursor ? atob(f.cursor) : undefined;
|
|
1635
|
+
let q = db.trx
|
|
1636
|
+
.selectFrom('burn')
|
|
1637
|
+
.selectAll('burn')
|
|
1638
|
+
.innerJoin('sol_tx', 'sol_tx.id', 'burn.sol_tx_id')
|
|
1639
|
+
.select(['sol_tx.executed_at', 'sol_tx.sig']);
|
|
1640
|
+
if (!f.inclManual) {
|
|
1641
|
+
q = q.where('burn.usdc_spent', '>', String(0));
|
|
1642
|
+
}
|
|
1643
|
+
if (f.executedAfter) {
|
|
1644
|
+
q = q.where('sol_tx.executed_at', '>=', f.executedAfter);
|
|
1645
|
+
}
|
|
1646
|
+
if (f.executedBefore) {
|
|
1647
|
+
q = q.where('sol_tx.executed_at', '<=', f.executedBefore);
|
|
1648
|
+
}
|
|
1649
|
+
if (f.sig) {
|
|
1650
|
+
// Exact match, not a prefix: sol_tx.sig is unique, so this is an index hit returning at most one row.
|
|
1651
|
+
q = q.where('sol_tx.sig', '=', f.sig);
|
|
1588
1652
|
}
|
|
1589
|
-
// Shared WHERE fragments pushed into each union leg for index use.
|
|
1590
|
-
const dateFilters = [];
|
|
1591
|
-
if (f.executedAfter)
|
|
1592
|
-
dateFilters.push(`st.executed_at >= ${p(f.executedAfter)}`);
|
|
1593
|
-
if (f.executedBefore)
|
|
1594
|
-
dateFilters.push(`st.executed_at <= ${p(f.executedBefore)}`);
|
|
1595
|
-
const sigFilter = f.sig ? `st.sig = ${p(f.sig)}` : '';
|
|
1596
|
-
// Buy-burn leg
|
|
1597
|
-
const buyWhere = [...dateFilters];
|
|
1598
|
-
if (!f.inclManual)
|
|
1599
|
-
buyWhere.push('b.usdc_spent > 0');
|
|
1600
|
-
if (sigFilter)
|
|
1601
|
-
buyWhere.push(sigFilter);
|
|
1602
|
-
if (f.jobId !== undefined) {
|
|
1603
|
-
buyWhere.push(`b.id IN (SELECT burn_id FROM job WHERE id = ${p(f.jobId)})`);
|
|
1604
|
-
}
|
|
1605
|
-
const buyWhereClause = buyWhere.length ? `WHERE ${buyWhere.join(' AND ')}` : '';
|
|
1606
|
-
const buySql = `
|
|
1607
|
-
SELECT 'buy'::text AS type, b.id::bigint AS sortid, b.id AS id,
|
|
1608
|
-
st.executed_at, b.priced_at, b.burned::text AS burned,
|
|
1609
|
-
b.usdc_spent::text AS usdc_spent, b.quote_amt::text AS quote_amt,
|
|
1610
|
-
b.tags, st.sig, b.render_to_usdc, b.eur_to_usdc,
|
|
1611
|
-
coalesce(b.from_escrow_usdc, 0)::text AS from_escrow_usdc,
|
|
1612
|
-
b.down_correction_id, b.escrow_correction_id,
|
|
1613
|
-
b.marked_burned_at
|
|
1614
|
-
FROM burn b
|
|
1615
|
-
INNER JOIN sol_tx st ON st.id = b.sol_tx_id
|
|
1616
|
-
${buyWhereClause}`;
|
|
1617
|
-
// Grant-burn leg
|
|
1618
|
-
const grantWhere = [...dateFilters];
|
|
1619
|
-
if (sigFilter)
|
|
1620
|
-
grantWhere.push(sigFilter);
|
|
1621
1653
|
if (f.jobId !== undefined) {
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
gb.marked_burned_at
|
|
1633
|
-
FROM grant_burn gb
|
|
1634
|
-
INNER JOIN sol_tx st ON st.id = gb.sol_tx_id
|
|
1635
|
-
${grantWhereClause}`;
|
|
1636
|
-
// Cursor filter on the outer query — row comparison for deterministic paging.
|
|
1637
|
-
let cursorFilter = '';
|
|
1638
|
-
if (cursorEa && cursorType && cursorId !== undefined) {
|
|
1639
|
-
cursorFilter = `WHERE (u.executed_at, u.type, u.sortid) > (${p(cursorEa)}, ${p(cursorType)}, ${p(cursorId)})`;
|
|
1640
|
-
}
|
|
1641
|
-
else if (cursorId !== undefined) {
|
|
1642
|
-
// Legacy cursor fallback: only buy burns, ordered by id.
|
|
1643
|
-
cursorFilter = `WHERE u.type = 'buy' AND u.sortid > ${p(cursorId)}`;
|
|
1654
|
+
// "Which burn consumed this job?" — job.burn_id is the FK, so resolve it as a subquery rather than
|
|
1655
|
+
// joining `job` into the page query, which would multiply rows by job count and break both the limit
|
|
1656
|
+
// and the cursor.
|
|
1657
|
+
q = q.where('burn.id', 'in', (eb) => eb
|
|
1658
|
+
.selectFrom('job')
|
|
1659
|
+
.select('job.burn_id')
|
|
1660
|
+
.where('job.id', '=', String(f.jobId)));
|
|
1661
|
+
}
|
|
1662
|
+
if (lastId) {
|
|
1663
|
+
q = q.where('burn.id', '>', lastId);
|
|
1644
1664
|
}
|
|
1645
1665
|
const limit = (_a = f.limit) !== null && _a !== void 0 ? _a : 1000;
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
UNION ALL
|
|
1650
|
-
${grantSql}
|
|
1651
|
-
) u
|
|
1652
|
-
${cursorFilter}
|
|
1653
|
-
ORDER BY u.executed_at ASC, u.type, u.sortid
|
|
1654
|
-
LIMIT ${p(limit)}`;
|
|
1655
|
-
const { rows } = yield (0, base_1.timedQuery)(pgPool, sql, params, 'fetchBurnsPage', log);
|
|
1666
|
+
q = q.limit(limit).orderBy('burn.id');
|
|
1667
|
+
const comp = q.compile();
|
|
1668
|
+
const { rows } = yield (0, base_1.timedQuery)(pgPool, comp.sql, comp.parameters, 'fetchBurnsPage', log);
|
|
1656
1669
|
const burns = rows.map((r) => {
|
|
1657
|
-
var _a, _b
|
|
1670
|
+
var _a, _b;
|
|
1658
1671
|
return ({
|
|
1659
1672
|
jobs: [],
|
|
1660
1673
|
id: Number(r.id),
|
|
1661
|
-
|
|
1662
|
-
renderToUsdc: (_a = r.render_to_usdc) !== null && _a !== void 0 ? _a : undefined,
|
|
1674
|
+
renderToUsdc: r.render_to_usdc,
|
|
1663
1675
|
solTx: r.sig,
|
|
1664
1676
|
executedAt: r.executed_at,
|
|
1665
|
-
pricedAt:
|
|
1666
|
-
usdcSpent: BigInt((
|
|
1667
|
-
quoteAmt: BigInt((
|
|
1677
|
+
pricedAt: r.priced_at,
|
|
1678
|
+
usdcSpent: BigInt((_a = r.usdc_spent) !== null && _a !== void 0 ? _a : 0),
|
|
1679
|
+
quoteAmt: BigInt((_b = r.quote_amt) !== null && _b !== void 0 ? _b : 0),
|
|
1668
1680
|
burned: BigInt(r.burned),
|
|
1669
|
-
eurToUsdc:
|
|
1681
|
+
eurToUsdc: r.eur_to_usdc,
|
|
1670
1682
|
markedBurnedAt: r.marked_burned_at,
|
|
1671
1683
|
tags: r.tags ? r.tags.split(',') : [],
|
|
1672
|
-
fromEscrowUsdc: BigInt((_f = r.from_escrow_usdc) !== null && _f !== void 0 ? _f : 0),
|
|
1673
|
-
downCorrectionId: r.down_correction_id ? Number(r.down_correction_id) : undefined,
|
|
1674
|
-
escrowCorrectionId: r.escrow_correction_id ? Number(r.escrow_correction_id) : undefined,
|
|
1675
1684
|
});
|
|
1676
1685
|
});
|
|
1677
1686
|
return {
|
|
1678
1687
|
burns,
|
|
1688
|
+
// Only hand back a cursor on a full page; a short page means we're done.
|
|
1679
1689
|
cursor: burns.length === limit
|
|
1680
|
-
? btoa(
|
|
1681
|
-
ea: rows[burns.length - 1].executed_at,
|
|
1682
|
-
t: rows[burns.length - 1].type,
|
|
1683
|
-
id: rows[burns.length - 1].sortid,
|
|
1684
|
-
}))
|
|
1690
|
+
? btoa(String(burns[burns.length - 1].id))
|
|
1685
1691
|
: undefined,
|
|
1686
1692
|
};
|
|
1687
1693
|
});
|
|
1688
1694
|
},
|
|
1689
|
-
fetchBurnJobs(db, burnId,
|
|
1695
|
+
fetchBurnJobs(db, burnId, log = logger_1.consoleLogger) {
|
|
1690
1696
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1691
|
-
const fk = burnType === 'grant' ? 'grant_burn_id' : 'burn_id';
|
|
1692
1697
|
const rows = yield db.trx
|
|
1693
1698
|
.selectFrom('job')
|
|
1694
1699
|
.selectAll('job')
|
|
1695
|
-
.where(
|
|
1700
|
+
.where('burn_id', '=', String(burnId))
|
|
1696
1701
|
.execute();
|
|
1697
|
-
log.debug(`fetchBurnJobs
|
|
1702
|
+
log.debug(`fetchBurnJobs burn ${burnId} -> ${rows.length} jobs`);
|
|
1698
1703
|
return rows.map((r) => ({
|
|
1699
1704
|
id: Number(r.id),
|
|
1700
1705
|
completedAt: r.completed_at,
|
|
@@ -2017,7 +2022,23 @@ const pgClient = (config) => {
|
|
|
2017
2022
|
});
|
|
2018
2023
|
},
|
|
2019
2024
|
insertDispersedOutbox(p, log = logger_1.consoleLogger) {
|
|
2025
|
+
var _a;
|
|
2020
2026
|
return __awaiter(this, void 0, void 0, function* () {
|
|
2027
|
+
// Check if dispersed_obligation table exists BEFORE the transaction so a missing table never
|
|
2028
|
+
// poisons the txn (PG aborts the entire transaction on any error, even a caught one).
|
|
2029
|
+
let hasObligationTable = false;
|
|
2030
|
+
if ((_a = p.obligations) === null || _a === void 0 ? void 0 : _a.length) {
|
|
2031
|
+
try {
|
|
2032
|
+
const { rows } = yield (0, base_1.timedQuery)(pgPool, `SELECT 1 FROM information_schema.tables WHERE table_name = 'dispersed_obligation' LIMIT 1`, [], 'checkObligationTable', log);
|
|
2033
|
+
hasObligationTable = rows.length > 0;
|
|
2034
|
+
}
|
|
2035
|
+
catch (_b) {
|
|
2036
|
+
hasObligationTable = false;
|
|
2037
|
+
}
|
|
2038
|
+
if (!hasObligationTable) {
|
|
2039
|
+
log.warn(`dispersed obligations: table not found (migration 44 pending); skipping for tx ${p.txSignature}`);
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2021
2042
|
// The outbox row is the durable, crash-safe anchor persisted BEFORE broadcast. When it carries a
|
|
2022
2043
|
// burn-correction draw, we increment consumed_render in the SAME transaction as the insert, bounded
|
|
2023
2044
|
// so it can never exceed amount_render. Row + counter are therefore atomic and inseparable: no
|
|
@@ -2025,8 +2046,8 @@ const pgClient = (config) => {
|
|
|
2025
2046
|
// (decremented again by voidDispersedOutbox). If the bounded increment can't fit (a concurrent draw
|
|
2026
2047
|
// took the room), the whole txn rolls back and we return {inserted:false} — the caller must NOT
|
|
2027
2048
|
// broadcast (no row, no counter change, no burn).
|
|
2028
|
-
|
|
2029
|
-
var
|
|
2049
|
+
const result = yield db.transaction().execute((trx) => __awaiter(this, void 0, void 0, function* () {
|
|
2050
|
+
var _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
|
|
2030
2051
|
const res = yield trx
|
|
2031
2052
|
.insertInto(exports.DISPERSED_SETTLEMENT_TABLE)
|
|
2032
2053
|
.values({
|
|
@@ -2036,15 +2057,17 @@ const pgClient = (config) => {
|
|
|
2036
2057
|
settled_amount: p.settledAmount,
|
|
2037
2058
|
burned_amount: p.burnedAmount,
|
|
2038
2059
|
executed_at: p.executedAt,
|
|
2039
|
-
eur_render: (
|
|
2040
|
-
last_valid_block_height: (
|
|
2041
|
-
usdc_spent: (
|
|
2042
|
-
correction_id: (
|
|
2043
|
-
correction_render: (
|
|
2060
|
+
eur_render: (_c = p.eurRender) !== null && _c !== void 0 ? _c : null,
|
|
2061
|
+
last_valid_block_height: (_d = p.lastValidBlockHeight) !== null && _d !== void 0 ? _d : null,
|
|
2062
|
+
usdc_spent: (_e = p.usdcSpent) !== null && _e !== void 0 ? _e : null,
|
|
2063
|
+
correction_id: (_f = p.correctionId) !== null && _f !== void 0 ? _f : null,
|
|
2064
|
+
correction_render: (_g = p.correctionRender) !== null && _g !== void 0 ? _g : null,
|
|
2065
|
+
usage_from: (_h = p.usageFrom) !== null && _h !== void 0 ? _h : null,
|
|
2066
|
+
usage_to: (_j = p.usageTo) !== null && _j !== void 0 ? _j : null,
|
|
2044
2067
|
})
|
|
2045
2068
|
.onConflict((oc) => oc.column('tx_signature').doNothing())
|
|
2046
2069
|
.executeTakeFirst();
|
|
2047
|
-
const inserted = ((
|
|
2070
|
+
const inserted = ((_k = res.numInsertedOrUpdatedRows) !== null && _k !== void 0 ? _k : BigInt(0)) > BigInt(0);
|
|
2048
2071
|
// Only reserve when the row was actually inserted (a tx_signature conflict is a no-op → no double
|
|
2049
2072
|
// count) and it drew a correction.
|
|
2050
2073
|
if (inserted &&
|
|
@@ -2059,19 +2082,35 @@ const pgClient = (config) => {
|
|
|
2059
2082
|
.where('id', '=', String(p.correctionId))
|
|
2060
2083
|
.where((eb) => eb((0, kysely_1.sql) `consumed_render + ${p.correctionRender}::numeric`, '<=', eb.ref('amount_render')))
|
|
2061
2084
|
.executeTakeFirst();
|
|
2062
|
-
if (((
|
|
2085
|
+
if (((_l = upd.numUpdatedRows) !== null && _l !== void 0 ? _l : BigInt(0)) === BigInt(0)) {
|
|
2063
2086
|
// Would exceed amount_render (concurrent draw won the room) — roll back the whole txn.
|
|
2064
2087
|
log.warn(`dispersed correction ${p.correctionId}: draw ${p.correctionRender} would over-consume; rejecting outbox insert for tx ${p.txSignature}`);
|
|
2065
2088
|
throw new DispersedCorrectionRejected();
|
|
2066
2089
|
}
|
|
2067
2090
|
}
|
|
2068
|
-
|
|
2091
|
+
if (inserted && hasObligationTable && ((_m = p.obligations) === null || _m === void 0 ? void 0 : _m.length)) {
|
|
2092
|
+
for (const o of p.obligations) {
|
|
2093
|
+
yield trx
|
|
2094
|
+
.insertInto('dispersed_obligation')
|
|
2095
|
+
.values({
|
|
2096
|
+
settlement_tx_sig: p.txSignature,
|
|
2097
|
+
uuid: o.uuid,
|
|
2098
|
+
amount_eur: o.amountEur,
|
|
2099
|
+
opened_at: o.openedAt,
|
|
2100
|
+
authorization_uuid: (_o = o.authorizationUuid) !== null && _o !== void 0 ? _o : null,
|
|
2101
|
+
})
|
|
2102
|
+
.onConflict((oc) => oc.column('uuid').doNothing())
|
|
2103
|
+
.execute();
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
2106
|
+
log.debug(`dispersed outbox tx ${p.txSignature}: ${res.numInsertedOrUpdatedRows} row(s), ${hasObligationTable ? ((_q = (_p = p.obligations) === null || _p === void 0 ? void 0 : _p.length) !== null && _q !== void 0 ? _q : 0) : 0} obligations`);
|
|
2069
2107
|
return { inserted };
|
|
2070
2108
|
})).catch((e) => {
|
|
2071
2109
|
if (e instanceof DispersedCorrectionRejected)
|
|
2072
2110
|
return { inserted: false };
|
|
2073
2111
|
throw e;
|
|
2074
2112
|
});
|
|
2113
|
+
return result;
|
|
2075
2114
|
});
|
|
2076
2115
|
},
|
|
2077
2116
|
getOutstandingDispersedCorrection(log = logger_1.consoleLogger) {
|