@dvmkit/sdk 0.10.1 → 0.11.0-rc.1

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.
Files changed (57) hide show
  1. package/dist/{chunk-3Q6FTZDB.js → chunk-3YFFZLW7.js} +2 -2
  2. package/dist/{chunk-YOSZIWC3.js → chunk-55DVLBCD.js} +135 -8
  3. package/dist/{chunk-VMHYGKWT.js → chunk-566CFXGU.js} +9 -1
  4. package/dist/chunk-5H7P3ORL.js +92 -0
  5. package/dist/{chunk-3R7NQA3Z.js → chunk-7KQLPVV7.js} +1 -1
  6. package/dist/{chunk-SYBFMHXQ.js → chunk-AIQGF52I.js} +242 -38
  7. package/dist/{chunk-JNECYURT.js → chunk-D4FFS3IS.js} +1 -1
  8. package/dist/{chunk-VTHYQCDB.js → chunk-E7ZBCT5U.js} +1 -1
  9. package/dist/{chunk-MMGSYMIC.js → chunk-EP7OVWI5.js} +1 -1
  10. package/dist/{chunk-LDIPVPKI.js → chunk-FFRTAQUW.js} +1 -1
  11. package/dist/chunk-H3GGFEE5.js +57 -0
  12. package/dist/{chunk-M3T5JYDJ.js → chunk-HNXMWHLU.js} +1 -1
  13. package/dist/{chunk-2OGBZPPJ.js → chunk-HQPQ4PFI.js} +1 -1
  14. package/dist/{chunk-UKD3R2KU.js → chunk-HW2UCOOC.js} +4 -55
  15. package/dist/{chunk-FFXXGCVG.js → chunk-ITRSITFP.js} +1 -1
  16. package/dist/{chunk-MU3IE434.js → chunk-JLT2YIBS.js} +267 -52
  17. package/dist/{chunk-STJBBVNL.js → chunk-K2ZNIQZP.js} +458 -69
  18. package/dist/{chunk-KFCEXCZM.js → chunk-KJZVTWMX.js} +1 -1
  19. package/dist/{chunk-W3VGCNG6.js → chunk-LDKV4UHI.js} +1 -1
  20. package/dist/{chunk-AJ6UHXUA.js → chunk-M4NFYEEY.js} +1 -1
  21. package/dist/{chunk-MB6R4XPZ.js → chunk-NS2OMIU7.js} +15 -3
  22. package/dist/{chunk-VUGEEMLT.js → chunk-RPUFWIT4.js} +11 -89
  23. package/dist/{chunk-EWZFANHJ.js → chunk-VWIMQ5H5.js} +1 -1
  24. package/dist/{chunk-2XQLL6N6.js → chunk-WFCVS6QB.js} +2 -2
  25. package/dist/{chunk-QGNXYVBO.js → chunk-Z4FOPROB.js} +17 -0
  26. package/dist/{content-retention-CrILCNxk.d.ts → content-retention-ySTD1wwZ.d.ts} +1 -1
  27. package/dist/{credit-ledger-4PZNAMDT.js → credit-ledger-4CBYLSLY.js} +6 -3
  28. package/dist/{credit-menu-BWsVqMh-.d.ts → credit-menu-BYySnZBk.d.ts} +4 -4
  29. package/dist/{fx-BM5h4o5S.d.ts → fx-DVTNs9q1.d.ts} +1 -1
  30. package/dist/index.d.ts +5 -5
  31. package/dist/index.js +6 -5
  32. package/dist/internal/caller.d.ts +6 -6
  33. package/dist/internal/caller.js +26 -22
  34. package/dist/internal/server.d.ts +7 -7
  35. package/dist/internal/server.js +24 -22
  36. package/dist/{job-store-92v8e1LQ.d.ts → job-store-DRwXCJDI.d.ts} +1 -1
  37. package/dist/{lightning-backend-9oky_wJi.d.ts → lightning-backend-BX5YvjnM.d.ts} +1 -1
  38. package/dist/memory-credit-ledger-DKCN7FY2.js +14 -0
  39. package/dist/{mpp-setup-FURD45LI.js → mpp-setup-JU3MT7QX.js} +3 -3
  40. package/dist/{payout-reporter-HVJ6WUF3.js → payout-reporter-NXWQVKXW.js} +2 -2
  41. package/dist/{postgres-consumed-credential-store-WO6OCFXZ.js → postgres-consumed-credential-store-WXJLM6J7.js} +1 -1
  42. package/dist/{postgres-job-store-H45PRBLA.js → postgres-job-store-WIIVRZPD.js} +2 -2
  43. package/dist/{postgres-replay-store-XXJSA457.js → postgres-replay-store-QJ4FG6AR.js} +2 -2
  44. package/dist/{pricing-X4XU5HXY.js → pricing-KCLENT2C.js} +3 -3
  45. package/dist/{revenue-reporter-EV3K5L6B.js → revenue-reporter-HD4GMXGD.js} +2 -2
  46. package/dist/server/index.d.ts +28 -12
  47. package/dist/server/index.js +31 -29
  48. package/dist/{step-cache--7yA5Ihr.d.ts → step-cache-d-VKen4K.d.ts} +147 -13
  49. package/dist/{tempo-charge-store-5JOIYUIH.js → tempo-charge-store-7LTUAQHQ.js} +2 -2
  50. package/dist/{tempo-session-store-S6QMLQUA.js → tempo-session-store-LYAIDR2B.js} +3 -3
  51. package/dist/testing/index.d.ts +3 -3
  52. package/dist/testing/index.js +6 -6
  53. package/dist/{usd-C7lvLvLk.d.ts → usd-DSroMJ--.d.ts} +1 -1
  54. package/dist/{x402-EN5LSOVM.js → x402-RUJ4LUKF.js} +3 -3
  55. package/package.json +1 -1
  56. package/recipes/postgres/financial-export-v1.sql +19 -5
  57. package/dist/memory-credit-ledger-RP7DT5IY.js +0 -11
@@ -1,15 +1,22 @@
1
+ import {
2
+ parseStoreValue
3
+ } from "./chunk-YG7G4DPZ.js";
1
4
  import {
2
5
  withSdkInitLock
3
6
  } from "./chunk-COYKJRMM.js";
7
+ import {
8
+ isSignedJobReceipt,
9
+ verifyReceipt
10
+ } from "./chunk-5H7P3ORL.js";
4
11
  import {
5
12
  applyRetryToPool
6
- } from "./chunk-MMGSYMIC.js";
13
+ } from "./chunk-EP7OVWI5.js";
7
14
  import {
8
15
  x402NetworkByCaip2
9
16
  } from "./chunk-BPYMEMQX.js";
10
17
  import {
11
18
  diagnosticConsole
12
- } from "./chunk-QGNXYVBO.js";
19
+ } from "./chunk-Z4FOPROB.js";
13
20
 
14
21
  // src/sdk/server/credit-ledger.ts
15
22
  import { randomUUID } from "crypto";
@@ -35,7 +42,7 @@ function depleteLots(lots, amountMicro) {
35
42
  const sats = lotSats(lot, available) - lotSats(lot, available - micro);
36
43
  debits.push({ lotId: lot.lotId, micro, sats });
37
44
  satsOwed += sats;
38
- if (lot.satsFunded > 0) backedMicro += micro;
45
+ if (lot.satsFunded > 0 || lot.satsBasisKnown) backedMicro += micro;
39
46
  left -= micro;
40
47
  }
41
48
  return { debits, satsOwed, uncoveredMicro: left, backedMicro };
@@ -59,7 +66,28 @@ function isInKindDepletion(depletion, amountMicro) {
59
66
  function lotSats(lot, micro) {
60
67
  if (lot.satsFunded <= 0 || lot.creditedMicro <= 0 || micro <= 0) return 0;
61
68
  const taken = Math.min(micro, lot.creditedMicro);
62
- return Math.floor(lot.satsFunded * taken / lot.creditedMicro);
69
+ return Number(BigInt(lot.satsFunded) * BigInt(taken) / BigInt(lot.creditedMicro));
70
+ }
71
+
72
+ // src/sdk/server/credit-draw-refund.ts
73
+ function refundRequest(args) {
74
+ const id = (value) => typeof value === "string" && value.length > 0;
75
+ if (!id(args.refundId) || !(id(args.jobId) && args.creditId === void 0 && args.drawId === void 0 || args.jobId === void 0 && id(args.creditId) && id(args.drawId)) || args.amountMicro !== void 0 && (!Number.isSafeInteger(args.amountMicro) || args.amountMicro <= 0) || args.reason !== void 0 && (typeof args.reason !== "string" || args.reason.length > 280)) {
76
+ throw new CreditLedgerError("invalid_refund", "Provide one draw selector, a refundId, a positive integer amountMicro if specified, and a reason of at most 280 characters.");
77
+ }
78
+ return JSON.stringify({
79
+ dvmId: args.outbox.dvmId,
80
+ jobId: args.jobId ?? null,
81
+ creditId: args.creditId ?? null,
82
+ drawId: args.drawId ?? null,
83
+ amountMicro: args.amountMicro ?? null,
84
+ reason: args.reason ?? null
85
+ });
86
+ }
87
+ function refundAssetDelta(original, amountMicro, before, after) {
88
+ const value = BigInt(original);
89
+ const denominator = BigInt(amountMicro);
90
+ return Number(value * BigInt(after) / denominator - value * BigInt(before) / denominator);
63
91
  }
64
92
 
65
93
  // src/sdk/server/x402-channel-store.ts
@@ -1140,6 +1168,7 @@ var CreditLedger = class {
1140
1168
  * fleet, so this DVM may advertise credit.
1141
1169
  */
1142
1170
  durable = true;
1171
+ tempoCreditLossEvidenceVersion = 1;
1143
1172
  x402Settlements;
1144
1173
  expiryReleaseOutbox;
1145
1174
  hostingCleanupFence;
@@ -1200,6 +1229,10 @@ var CreditLedger = class {
1200
1229
  observed_at BIGINT NOT NULL
1201
1230
  );
1202
1231
  `);
1232
+ await this.pool.query(`ALTER TABLE tempo_credit_losses
1233
+ ADD COLUMN IF NOT EXISTS refunded_to_payer_native BIGINT,
1234
+ ADD COLUMN IF NOT EXISTS loss_native BIGINT,
1235
+ ADD COLUMN IF NOT EXISTS loss_basis TEXT`);
1203
1236
  await this.pool.query(
1204
1237
  `CREATE INDEX IF NOT EXISTS idx_tempo_credit_losses_observed
1205
1238
  ON tempo_credit_losses(observed_at, credit_id);`
@@ -1229,7 +1262,8 @@ var CreditLedger = class {
1229
1262
  "tempo_channel_id TEXT",
1230
1263
  "msats_remaining BIGINT NOT NULL DEFAULT 0",
1231
1264
  "native_remaining BIGINT",
1232
- "x402_channel_id TEXT"
1265
+ "x402_channel_id TEXT",
1266
+ "last_funding_ledger_seq BIGINT"
1233
1267
  ]) {
1234
1268
  await this.pool.query(`ALTER TABLE credits ADD COLUMN IF NOT EXISTS ${column}`);
1235
1269
  }
@@ -1261,10 +1295,25 @@ var CreditLedger = class {
1261
1295
  "draw_msats BIGINT NOT NULL DEFAULT 0",
1262
1296
  "draw_native BIGINT",
1263
1297
  "grown_micro BIGINT NOT NULL DEFAULT 0",
1264
- "allocated_msats BIGINT"
1298
+ "allocated_msats BIGINT",
1299
+ "settled_sats BIGINT",
1300
+ "refund_dvm_id TEXT"
1265
1301
  ]) {
1266
1302
  await this.pool.query(`ALTER TABLE credit_draws ADD COLUMN IF NOT EXISTS ${column}`);
1267
1303
  }
1304
+ await this.pool.query(`
1305
+ CREATE TABLE IF NOT EXISTS credit_draw_refunds (
1306
+ refund_id TEXT PRIMARY KEY,
1307
+ credit_id TEXT NOT NULL,
1308
+ draw_id TEXT NOT NULL,
1309
+ request TEXT NOT NULL,
1310
+ amount_micro BIGINT NOT NULL CHECK (amount_micro > 0),
1311
+ restored_sats BIGINT,
1312
+ result JSONB NOT NULL
1313
+ );
1314
+ `);
1315
+ await this.pool.query(`CREATE INDEX IF NOT EXISTS credit_draw_refunds_draw_idx
1316
+ ON credit_draw_refunds(credit_id, draw_id)`);
1268
1317
  await this.pool.query(
1269
1318
  `CREATE UNIQUE INDEX IF NOT EXISTS credit_draws_seq_uidx
1270
1319
  ON credit_draws(credit_id, ledger_seq);`
@@ -1312,6 +1361,8 @@ var CreditLedger = class {
1312
1361
  created_at BIGINT NOT NULL
1313
1362
  );
1314
1363
  `);
1364
+ await this.pool.query(`ALTER TABLE credit_funding_lots
1365
+ ADD COLUMN IF NOT EXISTS sats_basis_known BOOLEAN NOT NULL DEFAULT false`);
1315
1366
  await this.pool.query(
1316
1367
  `CREATE INDEX IF NOT EXISTS idx_credit_funding_lots_open
1317
1368
  ON credit_funding_lots(credit_id, created_at, lot_id) WHERE remaining_micro > 0;`
@@ -1354,6 +1405,10 @@ var CreditLedger = class {
1354
1405
  await this.pool.query(`ALTER TABLE credit_drains ADD COLUMN IF NOT EXISTS owed_sats BIGINT`);
1355
1406
  await this.pool.query(`ALTER TABLE credit_drains ADD COLUMN IF NOT EXISTS lot_debits JSONB`);
1356
1407
  await this.pool.query(`ALTER TABLE credit_drains ADD COLUMN IF NOT EXISTS released_at BIGINT`);
1408
+ await this.pool.query(`ALTER TABLE credit_drains ADD COLUMN IF NOT EXISTS operator_decision JSONB`);
1409
+ await this.pool.query(`ALTER TABLE credit_drains ADD COLUMN IF NOT EXISTS hold_reason TEXT`);
1410
+ await this.pool.query(`ALTER TABLE credit_drains ADD COLUMN IF NOT EXISTS proven_refund_native NUMERIC(78,0)`);
1411
+ await this.pool.query(`ALTER TABLE credit_drains ADD COLUMN IF NOT EXISTS refund_payment_proven BOOLEAN NOT NULL DEFAULT FALSE`);
1357
1412
  await this.pool.query(`ALTER TABLE credit_drains ADD COLUMN IF NOT EXISTS fulfilment JSONB`);
1358
1413
  await this.pool.query(`ALTER TABLE credit_drains ADD COLUMN IF NOT EXISTS write_off_note TEXT`);
1359
1414
  await this.pool.query(
@@ -1556,10 +1611,11 @@ var CreditLedger = class {
1556
1611
  ({ rows } = await q.query(
1557
1612
  `INSERT INTO credits (credit_id, caller_pubkey, currency, balance_micro, expiry_ms, status, created_at,
1558
1613
  rail, native_asset, mint, funding_ref, msats_remaining, native_remaining,
1559
- tempo_channel_id, x402_channel_id)
1560
- VALUES ($1, $2, $3, $4, $5, 'active', $6, $7, $8, $9, $10, $11, $12, $13, $14)
1614
+ tempo_channel_id, x402_channel_id, last_funding_ledger_seq)
1615
+ VALUES ($1, $2, $3, $4, $5, 'active', $6, $7, $8, $9, $10, $11, $12, $13, $14, 0)
1561
1616
  ON CONFLICT (credit_id) DO UPDATE
1562
1617
  SET balance_micro = credits.balance_micro + EXCLUDED.balance_micro,
1618
+ last_funding_ledger_seq = credits.last_ledger_seq,
1563
1619
  expiry_ms = EXCLUDED.expiry_ms,
1564
1620
  rail = COALESCE(credits.rail, EXCLUDED.rail),
1565
1621
  native_asset = COALESCE(EXCLUDED.native_asset, credits.native_asset),
@@ -2091,6 +2147,180 @@ var CreditLedger = class {
2091
2147
  }
2092
2148
  return this.withCreditLock(args.creditId, run);
2093
2149
  }
2150
+ /** Restore a completed draw and its report under one durable transaction. */
2151
+ async refundDraw(args) {
2152
+ const request = refundRequest(args);
2153
+ const client = await this.pool.connect();
2154
+ try {
2155
+ await client.query("BEGIN");
2156
+ await client.query("SELECT pg_advisory_xact_lock(hashtextextended($1, 0))", [args.refundId]);
2157
+ const prior = await client.query(
2158
+ "SELECT request, result FROM credit_draw_refunds WHERE refund_id = $1",
2159
+ [args.refundId]
2160
+ );
2161
+ if (prior.rows[0]) {
2162
+ if (prior.rows[0].request !== request) {
2163
+ throw new CreditLedgerError("refund_conflict", "This refundId was recorded with different arguments.");
2164
+ }
2165
+ await client.query("COMMIT");
2166
+ return { ...prior.rows[0].result, replayed: true };
2167
+ }
2168
+ let creditId = args.creditId;
2169
+ let drawId = args.drawId;
2170
+ if (args.jobId !== void 0) {
2171
+ const matches = await client.query(
2172
+ "SELECT credit_id, draw_id FROM credit_draws WHERE job_id = $1 LIMIT 2",
2173
+ [args.jobId]
2174
+ );
2175
+ if (matches.rows.length === 1) {
2176
+ creditId = matches.rows[0].credit_id;
2177
+ drawId = matches.rows[0].draw_id;
2178
+ }
2179
+ }
2180
+ if (creditId === void 0 || drawId === void 0) {
2181
+ throw new CreditLedgerError("draw_not_found", "No unique credit draw matches this job.");
2182
+ }
2183
+ let credit;
2184
+ try {
2185
+ credit = await this.lockCreditRow(client, creditId);
2186
+ } catch (error) {
2187
+ if (error instanceof CreditLedgerError && error.code === "credit_not_found") {
2188
+ throw new CreditLedgerError("draw_not_found", "No draw matches this credit.");
2189
+ }
2190
+ throw error;
2191
+ }
2192
+ const draw = await this.readDraw(client, creditId, drawId);
2193
+ if (!credit || !draw || !draw.job_id || args.jobId !== void 0 && draw.job_id !== args.jobId) {
2194
+ throw new CreditLedgerError("draw_not_found", "No job-bound draw matches this selector.");
2195
+ }
2196
+ const owners = new Set(draw.refund_dvm_id ? [draw.refund_dvm_id] : []);
2197
+ const sources = await client.query(
2198
+ "SELECT to_regclass('jobs')::text AS jobs, to_regclass('reported_job_terminals')::text AS terminals"
2199
+ );
2200
+ if (sources.rows[0].jobs) {
2201
+ const jobs = await client.query(
2202
+ "SELECT dvm_id, receipt FROM jobs WHERE id = $1",
2203
+ [draw.job_id]
2204
+ );
2205
+ const job = jobs.rows[0];
2206
+ if (job?.dvm_id) owners.add(job.dvm_id);
2207
+ const receipt = job?.receipt;
2208
+ if (args.trustedReceiptPubkey !== void 0 && isSignedJobReceipt(receipt) && receipt.receipt_pubkey === args.trustedReceiptPubkey && receipt.job_id === draw.job_id && verifyReceipt(receipt)) {
2209
+ owners.add(receipt.dvm_id);
2210
+ }
2211
+ }
2212
+ if (sources.rows[0].terminals) {
2213
+ const terminals = await client.query(
2214
+ "SELECT dvm_id FROM reported_job_terminals WHERE job_id = $1",
2215
+ [draw.job_id]
2216
+ );
2217
+ for (const terminal of terminals.rows) owners.add(terminal.dvm_id);
2218
+ }
2219
+ if (owners.size !== 1 || !owners.has(args.outbox.dvmId)) {
2220
+ throw new CreditLedgerError("draw_not_found", "No draw with proven ownership matches this DVM.");
2221
+ }
2222
+ if (draw.status !== "settled") {
2223
+ throw new CreditLedgerError("draw_not_completed", "Only a settled completed draw can be refunded.");
2224
+ }
2225
+ const nowMs = args.nowMs ?? Date.now();
2226
+ const drained = await client.query(`SELECT 1 FROM credit_drains d
2227
+ WHERE d.credit_id = $1 AND d.status <> 'released' AND d.balance_after_micro = 0
2228
+ AND (SELECT MAX(ledger_seq) FROM credit_drains WHERE credit_id = $1 AND status <> 'released') = d.ledger_seq
2229
+ AND ($2::bigint IS NULL OR $2::bigint < d.ledger_seq)
2230
+ AND NOT EXISTS (SELECT 1 FROM credit_draws later WHERE later.credit_id = $1
2231
+ AND later.ledger_seq > d.ledger_seq AND later.amount_micro > 0)
2232
+ AND NOT EXISTS (SELECT 1 FROM credit_fundings f WHERE f.credit_id = $1
2233
+ AND f.ledger_seq >= d.ledger_seq AND f.amount_micro > 0)
2234
+ LIMIT 1`, [creditId, credit.last_funding_ledger_seq]);
2235
+ if (credit.status !== "active" || toSafeInt(credit.expiry_ms) <= nowMs) {
2236
+ throw new CreditLedgerError("credit_not_open", "The original credit is no longer open for refunds.");
2237
+ }
2238
+ if (credit.tempo_channel_id || credit.x402_channel_id) {
2239
+ throw new CreditLedgerError("credit_refund_channel_unsupported", "Channel collateral cannot back a refund of settled spending.");
2240
+ }
2241
+ if (toSafeInt(credit.balance_micro) === 0 && drained.rows.length > 0) {
2242
+ if (credit.last_funding_ledger_seq === null) {
2243
+ throw new CreditLedgerError("credit_refund_state_unavailable", "The original credit's historical funding and drain order cannot be established.");
2244
+ }
2245
+ throw new CreditLedgerError("credit_not_open", "The original credit is no longer open for refunds.");
2246
+ }
2247
+ const totals = await client.query(`SELECT COALESCE(SUM(amount_micro), 0) AS refunded
2248
+ FROM credit_draw_refunds WHERE credit_id = $1 AND draw_id = $2`, [creditId, drawId]);
2249
+ const before = toSafeInt(totals.rows[0].refunded);
2250
+ const originalMicro = toSafeInt(draw.amount_micro);
2251
+ const amountMicro = args.amountMicro ?? originalMicro - before;
2252
+ if (amountMicro <= 0 || amountMicro > originalMicro - before) {
2253
+ throw new CreditLedgerError("refund_exceeds_draw", "The refund exceeds the draw's remaining unrefunded charge.");
2254
+ }
2255
+ const after = before + amountMicro;
2256
+ if (!isFundingRail(credit.rail)) {
2257
+ throw new CreditLedgerError("credit_refund_basis_unavailable", "The original funding rail is not recorded on this older credit.");
2258
+ }
2259
+ const bitcoin = isNonChannelBitcoinRail(credit.rail);
2260
+ const originalSats = draw.settled_sats === null ? null : toSafeInt(draw.settled_sats);
2261
+ if (bitcoin && originalSats === null) {
2262
+ throw new CreditLedgerError("credit_refund_basis_unavailable", "The original Bitcoin value is not provable from this older draw.");
2263
+ }
2264
+ const restoredSats = bitcoin ? refundAssetDelta(originalSats, originalMicro, before, after) : null;
2265
+ const restoredMsats = restoredSats === null ? refundAssetDelta(toSafeInt(draw.draw_msats), originalMicro, before, after) : restoredSats * 1e3;
2266
+ const restoredNative = draw.draw_native === null ? null : refundAssetDelta(toSafeInt(draw.draw_native), originalMicro, before, after);
2267
+ const balanceAfterMicro = toSafeInt(toSafeInt(credit.balance_micro) + amountMicro);
2268
+ const ledgerSeq = toSafeInt(toSafeInt(credit.last_ledger_seq) + 1);
2269
+ toSafeInt(toSafeInt(credit.msats_remaining) + restoredMsats);
2270
+ if (credit.native_remaining !== null) toSafeInt(toSafeInt(credit.native_remaining) + (restoredNative ?? 0));
2271
+ await client.query(`UPDATE credit_draws SET refund_dvm_id = $3
2272
+ WHERE credit_id = $1 AND draw_id = $2`, [creditId, drawId, args.outbox.dvmId]);
2273
+ await client.query(`UPDATE credits SET balance_micro = $2, last_ledger_seq = $3,
2274
+ msats_remaining = msats_remaining + $4,
2275
+ native_remaining = CASE WHEN native_remaining IS NULL THEN NULL ELSE native_remaining + $5 END
2276
+ WHERE credit_id = $1`, [creditId, balanceAfterMicro, ledgerSeq, restoredMsats, restoredNative ?? 0]);
2277
+ if (bitcoin) {
2278
+ await client.query(
2279
+ `INSERT INTO credit_funding_lots
2280
+ (lot_id, credit_id, rail, sats_funded, credited_micro, remaining_micro, funding_ref, created_at, sats_basis_known)
2281
+ VALUES ($1, $2, $3, $4, $5, $5, $6, $7, true)`,
2282
+ [`lot:refund:${randomUUID()}`, creditId, credit.rail, restoredSats, amountMicro, args.refundId, nowMs]
2283
+ );
2284
+ }
2285
+ const result = {
2286
+ refundId: args.refundId,
2287
+ creditId,
2288
+ drawId,
2289
+ jobId: draw.job_id,
2290
+ amountMicro,
2291
+ currency: credit.currency,
2292
+ ledgerSeq,
2293
+ refundedAt: nowMs,
2294
+ balanceAfterMicro,
2295
+ replayed: false
2296
+ };
2297
+ await client.query(
2298
+ `INSERT INTO credit_draw_refunds
2299
+ (refund_id, credit_id, draw_id, request, amount_micro, restored_sats, result)
2300
+ VALUES ($1, $2, $3, $4, $5, $6, $7)`,
2301
+ [args.refundId, creditId, drawId, request, amountMicro, restoredSats, JSON.stringify(result)]
2302
+ );
2303
+ await args.outbox.enqueue(client, {
2304
+ dvmId: args.outbox.dvmId,
2305
+ creditId,
2306
+ drawId,
2307
+ jobId: draw.job_id,
2308
+ refundId: args.refundId,
2309
+ amountMicro,
2310
+ creditCurrency: credit.currency,
2311
+ ledgerSeq,
2312
+ refundedAt: nowMs,
2313
+ ...args.reason === void 0 ? {} : { reason: args.reason }
2314
+ });
2315
+ await client.query("COMMIT");
2316
+ return result;
2317
+ } catch (error) {
2318
+ await client.query("ROLLBACK").catch(() => void 0);
2319
+ throw error;
2320
+ } finally {
2321
+ client.release();
2322
+ }
2323
+ }
2094
2324
  /**
2095
2325
  * Finalize a pending draw: the hold becomes a real debit
2096
2326
  * (`balance_micro −= amount`). Available balance is unchanged — the amount
@@ -2779,6 +3009,15 @@ var CreditLedger = class {
2779
3009
  }
2780
3010
  }
2781
3011
  async terminalizeTempoCreditsLocked(q, evidence, channelId, observedAt) {
3012
+ const refunded = evidence.refundedToPayerNative ?? 0n;
3013
+ if (refunded < 0n) throw new Error("Tempo close refund must be nonnegative");
3014
+ const payerExit = evidence.closeKind === "payer_exit";
3015
+ if (payerExit && evidence.refundedToPayerNative !== void 0 || evidence.closeKind === "cooperative_close" && evidence.refundedToPayerNative === void 0) {
3016
+ throw new Error("Tempo close kind requires matching exact cooperative refund evidence");
3017
+ }
3018
+ const lossNative = (payerExit ? evidence.consumedNative : evidence.highestVoucherNative) - evidence.settledOnChainNative - (payerExit ? 0n : refunded);
3019
+ const lossBasis = payerExit ? "payer_exit" : evidence.refundedToPayerNative === void 0 ? "caller_asserted" : "cooperative_close";
3020
+ if (lossNative <= 0n) return this.readTempoCreditLosses(q, { channelId });
2782
3021
  const { rows: credits } = await q.query(
2783
3022
  `SELECT * FROM credits
2784
3023
  WHERE tempo_channel_id = $1
@@ -2799,8 +3038,8 @@ var CreditLedger = class {
2799
3038
  `INSERT INTO tempo_credit_losses
2800
3039
  (credit_id, channel_id, caller_pubkey, currency, former_balance_micro,
2801
3040
  settled_on_chain_native, highest_voucher_native, consumed_service_micro,
2802
- consumed_native, observed_at)
2803
- VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10)
3041
+ consumed_native, observed_at, refunded_to_payer_native, loss_native, loss_basis)
3042
+ VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13)
2804
3043
  ON CONFLICT (credit_id) DO NOTHING`,
2805
3044
  [
2806
3045
  credit.credit_id,
@@ -2812,7 +3051,10 @@ var CreditLedger = class {
2812
3051
  evidence.highestVoucherNative.toString(),
2813
3052
  consumed[0]?.amount ?? "0",
2814
3053
  evidence.consumedNative.toString(),
2815
- observedAt
3054
+ observedAt,
3055
+ evidence.refundedToPayerNative?.toString() ?? null,
3056
+ lossNative.toString(),
3057
+ lossBasis
2816
3058
  ]
2817
3059
  );
2818
3060
  await q.query(`UPDATE credits SET status = 'unbacked' WHERE credit_id = $1`, [
@@ -2821,8 +3063,8 @@ var CreditLedger = class {
2821
3063
  }
2822
3064
  return this.readTempoCreditLosses(q, { channelId });
2823
3065
  }
2824
- async listTempoCreditLosses(args) {
2825
- return this.readTempoCreditLosses(this.pool, { limit: args?.limit ?? 50 });
3066
+ async listTempoCreditLosses(args, tx) {
3067
+ return this.readTempoCreditLosses(tx ?? this.pool, { limit: args?.limit ?? 50, channelId: args?.channelId?.toLowerCase() });
2826
3068
  }
2827
3069
  /**
2828
3070
  * Reconcile the credit bound to a chain-proven empty x402 channel.
@@ -3158,26 +3400,27 @@ var CreditLedger = class {
3158
3400
  return rows.map(drainRecordFromJoinRow);
3159
3401
  }
3160
3402
  /**
3161
- * Page pending Tempo-channel drains by (created_at, credit_id, drain_id), optionally
3162
- * age-bounded, excluding written-off rows. Accept the legacy mpp stored method as well as
3403
+ * Page held Tempo-channel drains by (created_at, credit_id, drain_id), optionally
3404
+ * age-bounded. Audit reads also include terminal operator decisions. Accept the legacy mpp stored method as well as
3163
3405
  * tempo so pre-migration drains remain repairable. Use the compound key for both ordering
3164
3406
  * and continuation.
3165
3407
  */
3166
3408
  async listChannelDrains(args) {
3167
- const params = [args.createdBeforeMs, args.limit];
3409
+ const params = [args.createdBeforeMs, args.limit, args.includeResolved ?? false, args.includeWrittenOff || args.includeResolved || false];
3168
3410
  let keyset = "";
3169
3411
  if (args.after) {
3170
3412
  params.push(args.after.createdAt, args.after.creditId, args.after.drainId);
3171
- keyset = ` AND (d.created_at, d.credit_id, d.drain_id) > ($3, $4, $5)`;
3413
+ keyset = ` AND (d.created_at, d.credit_id, d.drain_id) > ($5, $6, $7)`;
3172
3414
  }
3173
3415
  const { rows } = await this.pool.query(
3174
3416
  `SELECT d.*, ${DRAIN_CREDIT_COLUMNS} FROM credit_drains d
3175
3417
  JOIN credits c ON c.credit_id = d.credit_id
3176
- WHERE d.status IN ('pending','exit_required')
3418
+ WHERE (d.status IN ('pending','exit_required')
3419
+ OR ($3::boolean AND d.operator_decision IS NOT NULL))
3177
3420
  AND d.method = 'tempo'
3178
3421
  AND c.tempo_channel_id IS NOT NULL
3179
3422
  AND d.created_at <= $1
3180
- ${args.includeResolved ? "" : "AND d.written_off_at IS NULL"}
3423
+ AND ($4::boolean OR d.written_off_at IS NULL)
3181
3424
  ${keyset}
3182
3425
  ORDER BY d.created_at, d.credit_id, d.drain_id
3183
3426
  LIMIT $2`,
@@ -3241,49 +3484,182 @@ var CreditLedger = class {
3241
3484
  verb: "require timed exit"
3242
3485
  });
3243
3486
  }
3487
+ /** Retain an exact receipt-derived refund; a later missing/zero read cannot erase payment. */
3488
+ async recordTempoDrainRefund(args) {
3489
+ const amount = String(args.refundedNative);
3490
+ if (!/^\d{1,78}$/.test(amount)) throw new CreditLedgerError("invalid_basis", "Tempo refund evidence must be a bounded non-negative integer.");
3491
+ await this.recordTempoDrainRefundEvidence(args, amount);
3492
+ }
3493
+ /** Retain trusted proof that some refund was paid, without claiming an exact amount. */
3494
+ async recordTempoDrainPayment(args) {
3495
+ await this.recordTempoDrainRefundEvidence(args, null);
3496
+ }
3497
+ async recordTempoDrainRefundEvidence(args, amount) {
3498
+ const { rows } = await this.pool.query(
3499
+ "SELECT tempo_channel_id FROM credits WHERE credit_id=$1",
3500
+ [args.creditId]
3501
+ );
3502
+ const record = async (tx) => {
3503
+ await tx.query(
3504
+ `UPDATE credit_drains SET proven_refund_native=
3505
+ GREATEST(proven_refund_native, $3::numeric),
3506
+ refund_payment_proven=refund_payment_proven OR COALESCE($3::numeric > 0, TRUE)
3507
+ WHERE credit_id=$1 AND drain_id=$2 AND method='tempo'
3508
+ AND status IN ('pending','exit_required') AND operator_decision IS NULL`,
3509
+ [args.creditId, args.drainId, amount]
3510
+ );
3511
+ };
3512
+ const channelId = rows[0]?.tempo_channel_id;
3513
+ if (channelId && this.tempoSessionStore) {
3514
+ await this.tempoSessionStore.withDrawPlacement(channelId, record);
3515
+ } else {
3516
+ await record(this.pool);
3517
+ }
3518
+ }
3519
+ /** Record the latest established proof refusal without changing the held debit. */
3520
+ async recordTempoDrainHold(args) {
3521
+ await this.pool.query(
3522
+ `UPDATE credit_drains SET hold_reason=
3523
+ CASE WHEN hold_reason='refund_short' AND proven_refund_native IS NULL THEN hold_reason ELSE $3 END
3524
+ WHERE credit_id=$1 AND drain_id=$2 AND method='tempo'
3525
+ AND status IN ('pending','exit_required') AND operator_decision IS NULL`,
3526
+ [args.creditId, args.drainId, args.reason]
3527
+ );
3528
+ }
3244
3529
  /**
3245
- * Cancel an unfulfilled Tempo cooperative drain and restore the exact
3246
- * balance plus rail basis it removed (`pending -> released`). This is only
3247
- * for a close the channel reconciliation proved cannot pay; ordinary payout
3248
- * rails never re-credit a liability after it has been handed to a worker.
3530
+ * An operator attests a full off-path refund or restores the exact unpaid debit. The
3531
+ * channel lock precedes the credit and drain locks; automatic booking shares the drain
3532
+ * CAS, so only one terminal outcome can commit. No channel lifecycle state is reopened.
3533
+ * Direct callers own reporting: hosted booking must join the caller's drain outbox transaction.
3249
3534
  */
3250
- async releaseDrain(args) {
3251
- const nowMs = args.nowMs ?? Date.now();
3252
- const run = async (client, credit) => {
3253
- const row = await this.readDrain(client, args.creditId, args.drainId);
3254
- if (!row) {
3255
- throw new CreditLedgerError(
3256
- "drain_not_found",
3257
- `drain ${args.drainId} on credit ${args.creditId} not found`
3258
- );
3259
- }
3260
- if (row.status === "released") {
3261
- return {
3535
+ async decideTempoDrain(args) {
3536
+ if (!["book_refunded", "release"].includes(args.decision) || typeof args.reason !== "string" || !args.reason.trim() || args.reason.trim().length > 2e3 || args.evidence !== void 0 && (typeof args.evidence !== "string" || !args.evidence.trim() || args.evidence.trim().length > 4e3)) {
3537
+ throw new CreditLedgerError("invalid_drain_decision", "A decision requires a reason of 1\u20132000 characters and optional evidence of 1\u20134000 characters.");
3538
+ }
3539
+ const query = args.tx ?? this.pool;
3540
+ const { rows } = await query.query(
3541
+ `SELECT tempo_channel_id FROM credits WHERE credit_id=$1`,
3542
+ [args.creditId]
3543
+ );
3544
+ if (!rows[0]) throw new CreditLedgerError("credit_not_found", "The credit was not found.");
3545
+ const channelId = rows[0].tempo_channel_id;
3546
+ if (!channelId || !this.tempoSessionStore) {
3547
+ throw new CreditLedgerError("drain_not_repairable", "A durable Tempo session channel is required for an operator decision.");
3548
+ }
3549
+ return this.tempoSessionStore.withDrawPlacement(channelId, async (tx, lifecycle) => {
3550
+ const intent = await tx.query(
3551
+ `SELECT reference, settled_before_close, forecast_pending FROM mpp_tempo_close_intents
3552
+ WHERE channel_id=$1 AND drain_id=$2 FOR UPDATE`,
3553
+ [channelId, args.drainId]
3554
+ );
3555
+ const credit = await this.lockCreditRow(tx, args.creditId);
3556
+ const { rows: rows2 } = await tx.query(
3557
+ `SELECT * FROM credit_drains WHERE credit_id=$1 AND drain_id=$2 FOR UPDATE`,
3558
+ [args.creditId, args.drainId]
3559
+ );
3560
+ const row = rows2[0];
3561
+ if (!row) throw new CreditLedgerError("drain_not_found", "The drain was not found.");
3562
+ if (row.method !== "tempo") throw new CreditLedgerError("drain_not_repairable", "Only a held Tempo session drain accepts an operator decision.");
3563
+ const reason = args.reason.trim();
3564
+ const evidence = args.evidence?.trim();
3565
+ let result;
3566
+ if (row.operator_decision) {
3567
+ const prior = row.operator_decision;
3568
+ if (prior.decision !== args.decision || prior.reason !== reason || prior.evidence !== evidence) {
3569
+ throw new CreditLedgerError("drain_decision_conflict", "This drain already has a different immutable operator decision.");
3570
+ }
3571
+ result = {
3262
3572
  drain: drainRecordFromRow(row, credit),
3263
- credit: await this.snapshotFromRow(credit, client, nowMs),
3573
+ credit: await this.snapshotFromRow(credit, tx, args.nowMs ?? Date.now()),
3264
3574
  replayed: true
3265
3575
  };
3576
+ } else {
3577
+ if (row.status !== "pending" && row.status !== "exit_required") {
3578
+ throw new CreditLedgerError("drain_decision_conflict", "This drain has already reached another terminal outcome.");
3579
+ }
3580
+ if (args.decision === "release") {
3581
+ const closeIntent = intent.rows[0];
3582
+ const reference = closeIntent?.reference;
3583
+ if (!lifecycle.finalized && closeIntent && (closeIntent.forecast_pending !== true || closeIntent.settled_before_close != null || reference && /^0x[0-9a-fA-F]{64}$/.test(reference))) {
3584
+ throw new CreditLedgerError("tempo_drain_close_pending", "The close may still pay. Reconcile its final outcome before releasing this drain.");
3585
+ }
3586
+ const channel = await tx.query(
3587
+ "SELECT state_json FROM mpp_tempo_channels WHERE channel_key=$1",
3588
+ [channelId]
3589
+ );
3590
+ const state = channel.rows[0] && parseStoreValue(channel.rows[0].state_json);
3591
+ const observation = state?.tempoDrainPaymentObservation;
3592
+ const paymentObserved = observation?.creditId === args.creditId && observation.drainId === args.drainId;
3593
+ if (paymentObserved || row.refund_payment_proven || row.proven_refund_native !== null && BigInt(row.proven_refund_native) > 0n) {
3594
+ throw new CreditLedgerError("tempo_drain_refund_paid", "A positive refund is already proven. Verify return of the full held amount, including any remainder, then book it as refunded; never release it as unpaid.");
3595
+ }
3596
+ if (row.hold_reason === "refund_short" && row.proven_refund_native === null) {
3597
+ throw new CreditLedgerError("tempo_drain_refund_unknown", "This short-refund history has no retained exact amount. Recover its refund evidence before releasing it as unpaid.");
3598
+ }
3599
+ }
3600
+ const decision = {
3601
+ source: "operator",
3602
+ decision: args.decision,
3603
+ reason,
3604
+ ...evidence === void 0 ? {} : { evidence },
3605
+ decidedAt: args.nowMs ?? Date.now()
3606
+ };
3607
+ await tx.query(`UPDATE credit_drains SET operator_decision=$3
3608
+ WHERE credit_id=$1 AND drain_id=$2`, [args.creditId, args.drainId, JSON.stringify(decision)]);
3609
+ row.operator_decision = decision;
3610
+ if (args.decision === "release") {
3611
+ result = await this.releaseDrainLocked(tx, row, args, decision.decidedAt);
3612
+ } else {
3613
+ await this.clearDrainWriteOff({ creditId: args.creditId, drainId: args.drainId, tx });
3614
+ const booked = await this.markDrainSent({
3615
+ creditId: args.creditId,
3616
+ drainId: args.drainId,
3617
+ sentRef: { source: "operator", channel_id: channelId },
3618
+ tx,
3619
+ nowMs: decision.decidedAt
3620
+ });
3621
+ result = { ...booked, credit: await this.snapshotFromRow(credit, tx, decision.decidedAt) };
3622
+ }
3266
3623
  }
3267
- if (row.status !== "pending") {
3268
- throw new CreditLedgerError(
3269
- "invalid_drain_state",
3270
- `cannot release ${row.method} drain ${args.drainId} from status ${row.status}`
3271
- );
3272
- }
3273
- const { rows } = await client.query(
3274
- `UPDATE credit_drains SET status = 'released', released_at = $3
3275
- WHERE credit_id = $1 AND drain_id = $2 AND status = 'pending'
3624
+ const spendableMicro = lifecycle.finalized || lifecycle.closeRequestedAt !== 0n || result.credit.expired ? 0 : result.credit.availableMicro;
3625
+ return { ...result, spendableMicro };
3626
+ }, args.tx);
3627
+ }
3628
+ /** Restore an unpaid pending drain; operator decisions also admit exit_required. */
3629
+ async releaseDrain(args) {
3630
+ const nowMs = args.nowMs ?? Date.now();
3631
+ const run = async (client, credit) => {
3632
+ const row = await this.readDrain(client, args.creditId, args.drainId);
3633
+ if (!row) throw new CreditLedgerError("drain_not_found", `drain ${args.drainId} on credit ${args.creditId} not found`);
3634
+ if (row.status === "released") return {
3635
+ drain: drainRecordFromRow(row, credit),
3636
+ credit: await this.snapshotFromRow(credit, client, nowMs),
3637
+ replayed: true
3638
+ };
3639
+ if (row.status !== "pending") throw new CreditLedgerError(
3640
+ "invalid_drain_state",
3641
+ `cannot release ${row.method} drain ${args.drainId} from status ${row.status}`
3642
+ );
3643
+ return this.releaseDrainLocked(client, row, args, nowMs);
3644
+ };
3645
+ if (args.tx) return run(args.tx, await this.lockCreditRow(args.tx, args.creditId));
3646
+ return this.withCreditLock(args.creditId, run);
3647
+ }
3648
+ async releaseDrainLocked(client, row, args, nowMs) {
3649
+ const { rows } = await client.query(
3650
+ `UPDATE credit_drains SET status = 'released', released_at = $3
3651
+ WHERE credit_id = $1 AND drain_id = $2 AND status = $4
3276
3652
  RETURNING *`,
3277
- [args.creditId, args.drainId, nowMs]
3653
+ [args.creditId, args.drainId, nowMs, row.status]
3654
+ );
3655
+ if (!rows[0]) {
3656
+ throw new CreditLedgerError(
3657
+ "invalid_drain_state",
3658
+ `drain ${args.drainId} changed while it was being released`
3278
3659
  );
3279
- if (!rows[0]) {
3280
- throw new CreditLedgerError(
3281
- "invalid_drain_state",
3282
- `drain ${args.drainId} changed while it was being released`
3283
- );
3284
- }
3285
- await client.query(
3286
- `UPDATE credits
3660
+ }
3661
+ await client.query(
3662
+ `UPDATE credits
3287
3663
  SET balance_micro = balance_micro + $2,
3288
3664
  msats_remaining = msats_remaining + $3,
3289
3665
  native_remaining = CASE
@@ -3291,18 +3667,15 @@ var CreditLedger = class {
3291
3667
  ELSE native_remaining + $4
3292
3668
  END
3293
3669
  WHERE credit_id = $1`,
3294
- [args.creditId, row.amount_micro, row.drained_msats, row.drained_native]
3295
- );
3296
- await this.applyLotDebits(client, row.lot_debits ?? [], 1);
3297
- const restored = await this.lockCreditRow(client, args.creditId);
3298
- return {
3299
- drain: drainRecordFromRow(rows[0], restored),
3300
- credit: await this.snapshotFromRow(restored, client, nowMs),
3301
- replayed: false
3302
- };
3670
+ [args.creditId, row.amount_micro, row.drained_msats, row.drained_native]
3671
+ );
3672
+ await this.applyLotDebits(client, row.lot_debits ?? [], 1);
3673
+ const restored = await this.lockCreditRow(client, args.creditId);
3674
+ return {
3675
+ drain: drainRecordFromRow(rows[0], restored),
3676
+ credit: await this.snapshotFromRow(restored, client, nowMs),
3677
+ replayed: false
3303
3678
  };
3304
- if (args.tx) return run(args.tx, await this.lockCreditRow(args.tx, args.creditId));
3305
- return this.withCreditLock(args.creditId, run);
3306
3679
  }
3307
3680
  /**
3308
3681
  * Attach the parked Cashu token to a pending drain (`pending → parked`).
@@ -3525,7 +3898,13 @@ var CreditLedger = class {
3525
3898
  let settledMsats = forecastMsats;
3526
3899
  if (isNonChannelBitcoinRail(credit.rail)) {
3527
3900
  const depletion = depleteLots(await this.openLots(client, args.creditId), amountMicro);
3528
- settledMsats = inKindDrawMsats(depletion, amountMicro) ?? forecastMsats;
3901
+ const inKindMsats = inKindDrawMsats(depletion, amountMicro);
3902
+ settledMsats = inKindMsats ?? forecastMsats;
3903
+ await client.query(
3904
+ `UPDATE credit_draws SET settled_sats = $3
3905
+ WHERE credit_id = $1 AND draw_id = $2`,
3906
+ [args.creditId, args.drawId, inKindMsats === null ? null : inKindMsats / 1e3]
3907
+ );
3529
3908
  await this.applyLotDebits(client, depletion.debits, -1);
3530
3909
  }
3531
3910
  if (settledMsats !== forecastMsats) {
@@ -3771,8 +4150,8 @@ var BITCOIN_CREDIT_PREDICATE = `c.rail IN ('cashu', 'lightning')
3771
4150
  AND c.balance_micro > 0`;
3772
4151
  var LOT_COVERAGE_LATERAL = `SELECT
3773
4152
  COALESCE(SUM(l.remaining_micro), 0) AS covered_micro,
3774
- COALESCE(SUM(l.remaining_micro) FILTER (WHERE l.sats_funded = 0), 0) AS basisless_micro,
3775
- COALESCE(SUM(l.sats_funded * l.remaining_micro / NULLIF(l.credited_micro, 0)), 0) AS owed_sats
4153
+ COALESCE(SUM(l.remaining_micro) FILTER (WHERE l.sats_funded = 0 AND NOT l.sats_basis_known), 0) AS basisless_micro,
4154
+ COALESCE(SUM(FLOOR(l.sats_funded::numeric * l.remaining_micro / NULLIF(l.credited_micro, 0))), 0) AS owed_sats
3776
4155
  FROM credit_funding_lots l
3777
4156
  WHERE l.credit_id = c.credit_id AND l.remaining_micro > 0`;
3778
4157
  var IN_KIND_PREDICATE = `cov.covered_micro >= c.balance_micro AND cov.basisless_micro = 0`;
@@ -3840,6 +4219,9 @@ function tempoCreditLossFromRow(row) {
3840
4219
  highestVoucherNative: BigInt(row.highest_voucher_native),
3841
4220
  consumedServiceMicro: toSafeInt(row.consumed_service_micro),
3842
4221
  consumedNative: BigInt(row.consumed_native),
4222
+ refundedToPayerNative: row.refunded_to_payer_native === null ? null : BigInt(row.refunded_to_payer_native),
4223
+ lossNative: row.loss_native === null ? null : BigInt(row.loss_native),
4224
+ lossBasis: row.loss_basis === "cooperative_close" || row.loss_basis === "payer_exit" || row.loss_basis === "caller_asserted" ? row.loss_basis : null,
3843
4225
  observedAt: toSafeInt(row.observed_at)
3844
4226
  };
3845
4227
  }
@@ -3883,7 +4265,11 @@ function drainRecordFromRow(row, credit) {
3883
4265
  sentAt: row.sent_at === null ? null : toSafeInt(row.sent_at),
3884
4266
  releasedAt: row.released_at === null ? null : toSafeInt(row.released_at),
3885
4267
  writeOffNote: row.write_off_note,
3886
- writtenOffAt: row.written_off_at === null ? null : toSafeInt(row.written_off_at)
4268
+ writtenOffAt: row.written_off_at === null ? null : toSafeInt(row.written_off_at),
4269
+ operatorDecision: row.operator_decision ?? null,
4270
+ holdReason: row.hold_reason ?? null,
4271
+ provenRefundNative: row.proven_refund_native ?? null,
4272
+ refundPaymentProven: row.refund_payment_proven ?? false
3887
4273
  };
3888
4274
  }
3889
4275
  function drainRecordFromJoinRow(row) {
@@ -3900,6 +4286,7 @@ function fundingLotFromRow(row) {
3900
4286
  creditId: row.credit_id,
3901
4287
  rail: row.rail,
3902
4288
  satsFunded: toSafeInt(row.sats_funded),
4289
+ satsBasisKnown: row.sats_basis_known,
3903
4290
  creditedMicro: toSafeInt(row.credited_micro),
3904
4291
  remainingMicro: toSafeInt(row.remaining_micro),
3905
4292
  fundingRef: row.funding_ref,
@@ -3983,6 +4370,8 @@ export {
3983
4370
  netOwedSats,
3984
4371
  fifoOrder,
3985
4372
  isInKindDepletion,
4373
+ refundRequest,
4374
+ refundAssetDelta,
3986
4375
  X402SettlementRaceError,
3987
4376
  X402RelaySubmissionLockError,
3988
4377
  X402_WEDGED_SETTLEMENT_STATUSES,