@dvmkit/sdk 0.1.5-rc.9 → 0.2.1-rc.9
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/README.md +14 -0
- package/dist/{chunk-MLRCSJYX.js → chunk-2D75MNNG.js} +178 -550
- package/dist/{chunk-BIFLRKMO.js → chunk-2KGG5ORW.js} +1 -1
- package/dist/{chunk-27V2ILSR.js → chunk-4A2RAKCW.js} +2 -2
- package/dist/{chunk-5URG56JJ.js → chunk-A4CM2BD3.js} +18 -2
- package/dist/{chunk-L4OYF4DQ.js → chunk-AJNFXR7J.js} +2 -2
- package/dist/{chunk-S3XAHZQY.js → chunk-COYKJRMM.js} +24 -3
- package/dist/{chunk-UP2F5RRT.js → chunk-D7YQYLLM.js} +1 -4
- package/dist/{chunk-CEOAHV2I.js → chunk-FDKRXOZO.js} +0 -5
- package/dist/{chunk-TQWGQCNV.js → chunk-FPOLJRBF.js} +5 -5
- package/dist/{chunk-BTZY7VPH.js → chunk-GAIPXGM3.js} +1 -1
- package/dist/{chunk-SSSZUVWM.js → chunk-JAHRF5X3.js} +89 -39
- package/dist/{chunk-5PBOA25N.js → chunk-JNKQZVZX.js} +472 -617
- package/dist/{chunk-U6M3ATSG.js → chunk-MD3H3BF7.js} +40 -6
- package/dist/{chunk-CGKZDODG.js → chunk-MG67KXU7.js} +0 -5
- package/dist/{chunk-KSQEFVFJ.js → chunk-MMCE7XSR.js} +1 -1
- package/dist/{chunk-JZWELPFH.js → chunk-N5B66UOS.js} +4 -16
- package/dist/{chunk-BIP6G74V.js → chunk-O6D72KYA.js} +10 -10
- package/dist/{chunk-KXCKFIJI.js → chunk-QX52F6IB.js} +1 -1
- package/dist/{chunk-RW5LP57K.js → chunk-SDK6KDJN.js} +0 -1
- package/dist/{chunk-KVEHHC7W.js → chunk-U5VNEALI.js} +24 -13
- package/dist/{chunk-FROTD5XQ.js → chunk-V5ZRATJH.js} +2 -3
- package/dist/{chunk-EVBK675R.js → chunk-VA7GT6C6.js} +31 -37
- package/dist/{chunk-2UUXIIOC.js → chunk-VIVXB4CD.js} +3 -3
- package/dist/{chunk-6BQM7TOW.js → chunk-XMDQOXHO.js} +11 -38
- package/dist/{chunk-DMNLFNTW.js → chunk-XRLKGCAW.js} +2 -2
- package/dist/{chunk-M7LHFJ5K.js → chunk-XZ7LW4S5.js} +2 -3
- package/dist/{chunk-E4EVGPDX.js → chunk-YO56VCTQ.js} +0 -6
- package/dist/{credit-ledger-2DFQHNLB.js → credit-ledger-MHA652TV.js} +2 -2
- package/dist/{credit-menu-enwMbn55.d.ts → credit-menu-kDA9RVzD.d.ts} +524 -1077
- package/dist/{fx-D860pZvP.d.ts → fx-DhyT1Cjh.d.ts} +38 -82
- package/dist/index.d.ts +11 -26
- package/dist/index.js +2 -2
- package/dist/internal/caller.d.ts +644 -2140
- package/dist/internal/caller.js +32 -66
- package/dist/internal/server.d.ts +40 -90
- package/dist/internal/server.js +16 -16
- package/dist/{job-store-BUGqvCfL.d.ts → job-store-D3CcZp_g.d.ts} +73 -66
- package/dist/{lightning-backend-BozcevPZ.d.ts → lightning-backend-BhsyrBLa.d.ts} +19 -41
- package/dist/{memory-credit-ledger-MNUOTQO5.js → memory-credit-ledger-BYBATRN3.js} +3 -3
- package/dist/{mpp-secret-state-WNAQQ6K4.js → mpp-secret-state-B5GCILVH.js} +1 -1
- package/dist/{mpp-setup-4FJD6ZHV.js → mpp-setup-MPP3ST5X.js} +1 -1
- package/dist/{payout-reporter-RG6XNGPI.js → payout-reporter-KEMUXEUO.js} +2 -2
- package/dist/{postgres-consumed-credential-store-VHBT4KEA.js → postgres-consumed-credential-store-6N2V5MJN.js} +2 -2
- package/dist/postgres-job-store-MZZZMS2B.js +7 -0
- package/dist/postgres-kv-store-DXGTKXLU.js +7 -0
- package/dist/{postgres-replay-store-UJXRT6VO.js → postgres-replay-store-ZF4JNZOW.js} +2 -2
- package/dist/{pricing-4CEB34RM.js → pricing-MU5GNUJZ.js} +1 -1
- package/dist/{processed-payment-store-HAA4SFNK.js → processed-payment-store-GKMAWOM3.js} +2 -2
- package/dist/revenue-reporter-O75BGYFK.js +7 -0
- package/dist/server/index.d.ts +47 -159
- package/dist/server/index.js +67 -138
- package/dist/{ssrf-DbFkpDv0.d.ts → ssrf-dMooihtY.d.ts} +1 -2
- package/dist/{step-cache-5dljDqrQ.d.ts → step-cache-v2yhyhSZ.d.ts} +617 -1532
- package/dist/{tempo-charge-store-RIFTALZK.js → tempo-charge-store-SDTUNTKA.js} +2 -2
- package/dist/{tempo-lifecycle-DFIXQ54Q.js → tempo-lifecycle-G5SB4HFC.js} +5 -5
- package/dist/{tempo-session-store-2JNOKJGX.js → tempo-session-store-WF3VZXNF.js} +2 -2
- package/dist/{tempo-wallet-4QKSV65O.js → tempo-wallet-ZFNUDFVO.js} +2 -2
- package/dist/testing/index.d.ts +5 -15
- package/dist/testing/index.js +4 -11
- package/dist/{usd-DoRuAckA.d.ts → usd-Cu9G6xv2.d.ts} +14 -16
- package/dist/{wallet-CJC8lwxx.d.ts → wallet-Dwjs5n_M.d.ts} +1 -1
- package/dist/{x402-5H27DCBE.js → x402-CI6PSHNO.js} +5 -5
- package/package.json +3 -2
- package/dist/postgres-job-store-3RAXMNSY.js +0 -7
- package/dist/postgres-kv-store-JFBDP5IP.js +0 -7
- package/dist/revenue-reporter-ASZ7SHHH.js +0 -7
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
withSdkInitLock
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-COYKJRMM.js";
|
|
4
4
|
import {
|
|
5
5
|
applyRetryToPool
|
|
6
6
|
} from "./chunk-C3MTFLC6.js";
|
|
@@ -108,7 +108,6 @@ var PostgresX402ChannelStorage = class {
|
|
|
108
108
|
fleetLockQueueDepth = 0;
|
|
109
109
|
relayLockTimeoutMs;
|
|
110
110
|
terminalizeUnbackedCredit;
|
|
111
|
-
/** Create the durable channel table under the SDK-wide DDL lock. */
|
|
112
111
|
async init() {
|
|
113
112
|
this.settlementPool ??= createSettlementPool(this.pool);
|
|
114
113
|
await withSdkInitLock(this.pool, async () => {
|
|
@@ -208,7 +207,7 @@ var PostgresX402ChannelStorage = class {
|
|
|
208
207
|
*
|
|
209
208
|
* Both accessors run on the settlement pool: every caller reads or writes the
|
|
210
209
|
* marker while {@link withFleetSettlementLock} holds a host-pool client, which
|
|
211
|
-
* is the nesting the
|
|
210
|
+
* is the nesting the split exists to keep off a non-terminal pool.
|
|
212
211
|
*/
|
|
213
212
|
async isSettlePending(scope) {
|
|
214
213
|
const reader = await this.settlementWriter();
|
|
@@ -235,9 +234,9 @@ var PostgresX402ChannelStorage = class {
|
|
|
235
234
|
*
|
|
236
235
|
* On the dedicated settlement pool like every other marker, and for a sharper
|
|
237
236
|
* reason than they had: this and {@link prepareSettlement} now run *inside*
|
|
238
|
-
* the channel transaction
|
|
237
|
+
* the channel transaction, which holds a host-pool client for its
|
|
239
238
|
* whole duration. Taking a second client from that same pool is the
|
|
240
|
-
* self-deadlock
|
|
239
|
+
* self-deadlock fixed — at pool max every client is a holder and a
|
|
241
240
|
* waiter — and moving these calls under the transaction is exactly what would
|
|
242
241
|
* reintroduce it.
|
|
243
242
|
*/
|
|
@@ -250,7 +249,7 @@ var PostgresX402ChannelStorage = class {
|
|
|
250
249
|
}
|
|
251
250
|
/**
|
|
252
251
|
* The refund on this channel that is neither finished nor abandoned, if there
|
|
253
|
-
* is one — the credit ledger's spend gate
|
|
252
|
+
* is one — the credit ledger's spend gate.
|
|
254
253
|
*
|
|
255
254
|
* `statuses` is the caller's, because the two gates disagree by design: a
|
|
256
255
|
* draw is blocked by {@link X402_SPEND_BLOCKING_SETTLEMENT_STATUSES}, a fresh
|
|
@@ -258,12 +257,12 @@ var PostgresX402ChannelStorage = class {
|
|
|
258
257
|
*
|
|
259
258
|
* On the dedicated settlement pool, and that is what makes the gate callable
|
|
260
259
|
* at all: a draw routinely runs inside an open channel transaction holding a
|
|
261
|
-
* host-pool client, and taking a second one from that pool is the
|
|
260
|
+
* host-pool client, and taking a second one from that pool is the
|
|
262
261
|
* self-deadlock. The oldest blocking row wins, so a channel that somehow
|
|
263
262
|
* accumulated two names the one an operator should reach for first.
|
|
264
263
|
*
|
|
265
264
|
* Projects three columns rather than decoding a whole {@link
|
|
266
|
-
* X402SettlementIntent}: the decoder refuses a
|
|
265
|
+
* X402SettlementIntent}: the decoder refuses a legacy row that carries
|
|
267
266
|
* no reconciliation snapshot, and a gate that throws on one would take every
|
|
268
267
|
* draw on that channel down with it — when the row is exactly the kind that
|
|
269
268
|
* ought to block them.
|
|
@@ -282,7 +281,7 @@ var PostgresX402ChannelStorage = class {
|
|
|
282
281
|
}
|
|
283
282
|
/**
|
|
284
283
|
* Read one settlement by its own identifier — the handle an operator holds
|
|
285
|
-
*
|
|
284
|
+
* Same dedicated pool as {@link getSettlement}, for the same
|
|
286
285
|
* reason: the repair reads this while a channel transaction may be open.
|
|
287
286
|
*/
|
|
288
287
|
async getSettlementById(settlementId) {
|
|
@@ -308,7 +307,7 @@ var PostgresX402ChannelStorage = class {
|
|
|
308
307
|
}
|
|
309
308
|
/**
|
|
310
309
|
* The operator's repair queue: settlements stuck between "chain paid" and
|
|
311
|
-
* "ledger booked", oldest write first
|
|
310
|
+
* "ledger booked", oldest write first.
|
|
312
311
|
*
|
|
313
312
|
* Keyset-paginated on `(created_at, settlement_id)` for the reason the
|
|
314
313
|
* blocked-invoice queue is: a wedged row never self-clears, so a fixed first
|
|
@@ -350,7 +349,7 @@ var PostgresX402ChannelStorage = class {
|
|
|
350
349
|
};
|
|
351
350
|
}
|
|
352
351
|
/**
|
|
353
|
-
* Close a wedged settlement without booking anything
|
|
352
|
+
* Close a wedged settlement without booking anything.
|
|
354
353
|
*
|
|
355
354
|
* A single-statement CAS on the wedged statuses, on the settlement pool —
|
|
356
355
|
* there is no ledger effect to be atomic with, which is the whole point:
|
|
@@ -460,7 +459,7 @@ var PostgresX402ChannelStorage = class {
|
|
|
460
459
|
* Mark that the next operation is the ambiguous external-call boundary.
|
|
461
460
|
*
|
|
462
461
|
* `channelAtSubmission` is the live channel the facilitator is about to size
|
|
463
|
-
* and sign the settlement against
|
|
462
|
+
* and sign the settlement against — bookmarked here, with the
|
|
464
463
|
* block height, because these two are the only description of that instant
|
|
465
464
|
* that survives the call.
|
|
466
465
|
*/
|
|
@@ -521,7 +520,7 @@ var PostgresX402ChannelStorage = class {
|
|
|
521
520
|
/**
|
|
522
521
|
* Mark the external settlement and ledger effect complete in the active transaction.
|
|
523
522
|
*
|
|
524
|
-
* `resolvedAtMs` is set only by the operator repair
|
|
523
|
+
* `resolvedAtMs` is set only by the operator repair, so an audit
|
|
525
524
|
* read can tell a settlement that completed on its own from one a human had
|
|
526
525
|
* to finish.
|
|
527
526
|
*/
|
|
@@ -541,7 +540,6 @@ var PostgresX402ChannelStorage = class {
|
|
|
541
540
|
);
|
|
542
541
|
}
|
|
543
542
|
}
|
|
544
|
-
/** Load one channel snapshot. */
|
|
545
543
|
async get(channelId) {
|
|
546
544
|
const q = this.transaction.getStore() ?? this.pool;
|
|
547
545
|
const { rows } = await q.query(
|
|
@@ -591,7 +589,7 @@ var PostgresX402ChannelStorage = class {
|
|
|
591
589
|
* this is how the settlement layer says "this exact decrease is the one the
|
|
592
590
|
* payer's verified refund voucher authorises". Outside this scope a decrease
|
|
593
591
|
* of `chargedCumulativeAmount` is refused flat, which is what keeps the
|
|
594
|
-
* arbitrary-decrease guard meaningful
|
|
592
|
+
* arbitrary-decrease guard meaningful.
|
|
595
593
|
*/
|
|
596
594
|
withSanctionedRefundBase(base, operation) {
|
|
597
595
|
return this.refundBase.run(base, operation);
|
|
@@ -617,7 +615,7 @@ var PostgresX402ChannelStorage = class {
|
|
|
617
615
|
* The lock is session-scoped, so its client stays checked out for the whole
|
|
618
616
|
* operation — and `operation()` (upstream's claim/settle) re-enters the pool
|
|
619
617
|
* through {@link updateChannel}. Two bounds keep that from wedging the pool
|
|
620
|
-
*
|
|
618
|
+
* local callers are serialized in-process, so this holds at most
|
|
621
619
|
* one client per process no matter how many requests arrive, and a waiting
|
|
622
620
|
* acquire carries a `lock_timeout` so a wedged fleet-mate can't pin that
|
|
623
621
|
* client — and the caller's request — indefinitely. A timeout reports the
|
|
@@ -640,7 +638,7 @@ var PostgresX402ChannelStorage = class {
|
|
|
640
638
|
}
|
|
641
639
|
}
|
|
642
640
|
/**
|
|
643
|
-
* Hold the fleet-wide self-relay submission lock for one chain submission
|
|
641
|
+
* Hold the fleet-wide self-relay submission lock for one chain submission.
|
|
644
642
|
*
|
|
645
643
|
* The session lock keeps one per-DVM gas EOA to one in-flight submission
|
|
646
644
|
* across the whole fleet — two machines submitting at once collide on the
|
|
@@ -997,46 +995,29 @@ var CreditLedger = class {
|
|
|
997
995
|
tempoSessionStore;
|
|
998
996
|
/**
|
|
999
997
|
* Postgres-backed: balances survive a restart and are shared across the
|
|
1000
|
-
* fleet, so this DVM may advertise credit
|
|
998
|
+
* fleet, so this DVM may advertise credit.
|
|
1001
999
|
*/
|
|
1002
1000
|
durable = true;
|
|
1003
1001
|
x402Settlements;
|
|
1004
1002
|
expiryReleaseOutbox;
|
|
1005
1003
|
/**
|
|
1006
|
-
*
|
|
1007
|
-
*
|
|
1008
|
-
*
|
|
1009
|
-
* Late, because the settlement store is built with the batch-settlement
|
|
1010
|
-
* server, which is built *after* the ledger it reads earned draws from. An
|
|
1011
|
-
* unbound ledger simply does not gate — correct for a DVM with no x402
|
|
1012
|
-
* channel storage, where no settlement row can exist to wedge.
|
|
1004
|
+
* Bound after construction because the settlement store shares this ledger and is
|
|
1005
|
+
* initialized separately. Ledger effects and route preflights use the same refund gate.
|
|
1013
1006
|
*/
|
|
1014
1007
|
useX402SettlementGate(gate) {
|
|
1015
1008
|
this.x402Settlements = gate;
|
|
1016
1009
|
}
|
|
1017
1010
|
/**
|
|
1018
|
-
*
|
|
1019
|
-
*
|
|
1020
|
-
*
|
|
1021
|
-
* Held on the ledger rather than threaded through {@link fund} for the same
|
|
1022
|
-
* reason as the gate above: the revival half fires from *every* funding path
|
|
1023
|
-
* — Cashu commit, x402 exact and channel, Tempo, the Lightning invoice
|
|
1024
|
-
* settle, the implicit N=1 per-call payment — and a seam each of those has to
|
|
1025
|
-
* remember to pass is a seam one of them will eventually forget.
|
|
1026
|
-
*
|
|
1027
|
-
* Unbound (a `dvmctl dev` server, a self-hosted builder, a test host) the
|
|
1028
|
-
* sweep and the reversal still run and still write the ledger's own rows;
|
|
1029
|
-
* only the platform report is skipped. Reporting is bookkeeping and must
|
|
1030
|
-
* never gate the ledger.
|
|
1011
|
+
* Expiry release and revival reports join the ledger transaction. A caller-owned funding
|
|
1012
|
+
* transaction must not commit a reversal independently of its outbox row. Without this
|
|
1013
|
+
* wiring, ledger transitions still run.
|
|
1031
1014
|
*/
|
|
1032
1015
|
useCreditExpiryReleaseOutbox(outbox) {
|
|
1033
1016
|
this.expiryReleaseOutbox = outbox;
|
|
1034
1017
|
}
|
|
1035
|
-
/** Create the `credits` / `credit_draws` tables if absent. Call once at SDK boot. */
|
|
1036
1018
|
async init() {
|
|
1037
1019
|
await withSdkInitLock(this.pool, () => this.createTables());
|
|
1038
1020
|
}
|
|
1039
|
-
/** The boot DDL itself — always runs under {@link withSdkInitLock} (internal-review). */
|
|
1040
1021
|
async createTables() {
|
|
1041
1022
|
await this.pool.query(`
|
|
1042
1023
|
CREATE TABLE IF NOT EXISTS credits (
|
|
@@ -1314,25 +1295,11 @@ var CreditLedger = class {
|
|
|
1314
1295
|
);
|
|
1315
1296
|
}
|
|
1316
1297
|
/**
|
|
1317
|
-
*
|
|
1318
|
-
*
|
|
1319
|
-
*
|
|
1320
|
-
*
|
|
1321
|
-
*
|
|
1322
|
-
*
|
|
1323
|
-
* The lot is derived from `credits.msats_remaining`, not replayed out of
|
|
1324
|
-
* `credit_fundings`. That is not a shortcut: `credit_fundings` records only
|
|
1325
|
-
* `{amount_micro, rail}` — no instrument amount at all — and is not written
|
|
1326
|
-
* on every funding path, whereas `msats_remaining` **is** the credit's
|
|
1327
|
-
* current unspent rail value, maintained pro rata on every draw since
|
|
1328
|
-
* internal-review. One synthetic lot pairing it with `balance_micro` therefore
|
|
1329
|
-
* reproduces today's in-kind position exactly, and FIFO over a single lot is
|
|
1330
|
-
* trivially correct. Historic per-funding rates are unrecoverable and would
|
|
1331
|
-
* change nothing: only the remaining position is owed.
|
|
1332
|
-
*
|
|
1333
|
-
* The semantics change is deliberately retroactive (internal-review) — every
|
|
1334
|
-
* current holder is first-party, so there is one regime and no legacy
|
|
1335
|
-
* branch.
|
|
1298
|
+
* Backfill an open non-channel Bitcoin credit only when it has no funding lot. Pair
|
|
1299
|
+
* balance_micro with current msats_remaining; credit_fundings lacks historical instrument
|
|
1300
|
+
* amounts and does not cover every funding path. Historical per-funding rates cannot be
|
|
1301
|
+
* recovered. Existing lots are left untouched, so this does not repair partial coverage
|
|
1302
|
+
* gaps.
|
|
1336
1303
|
*/
|
|
1337
1304
|
async backfillFundingLots() {
|
|
1338
1305
|
const { rowCount } = await this.pool.query(
|
|
@@ -1368,60 +1335,19 @@ var CreditLedger = class {
|
|
|
1368
1335
|
);
|
|
1369
1336
|
}
|
|
1370
1337
|
/**
|
|
1371
|
-
*
|
|
1372
|
-
*
|
|
1373
|
-
*
|
|
1374
|
-
*
|
|
1375
|
-
*
|
|
1376
|
-
*
|
|
1377
|
-
*
|
|
1378
|
-
*
|
|
1379
|
-
*
|
|
1380
|
-
*
|
|
1381
|
-
*
|
|
1382
|
-
*
|
|
1383
|
-
*
|
|
1384
|
-
* single statement: it upserts the credit, records its funding lot
|
|
1385
|
-
* (internal-review), and reverses any standing expiry release (internal-review) — and that
|
|
1386
|
-
* last leg restores balance and queues a report. A crash between the upsert
|
|
1387
|
-
* and the reversal would leave a revived credit whose release still stands,
|
|
1388
|
-
* which the sweep's own exclusion then makes permanent: `balance_micro > 0`
|
|
1389
|
-
* but a standing release means it is neither drainable nor re-releasable.
|
|
1390
|
-
*
|
|
1391
|
-
* `basis` records the rail value behind the fiat (internal-review) so each draw can
|
|
1392
|
-
* be allocated its share of the rail-native amount actually received. A
|
|
1393
|
-
* top-up on a different rail than the credit's is refused with
|
|
1394
|
-
* `rail_mismatch`: sats and USDC microunits aren't summable, so a blended
|
|
1395
|
-
* credit would have no coherent native basis to allocate from.
|
|
1396
|
-
*
|
|
1397
|
-
* It is **required** (internal-review), in the type and again at runtime via
|
|
1398
|
-
* {@link assertFundingBasis}. A basis-less fund wrote `rail = NULL`, and a
|
|
1399
|
-
* draw against such a credit settles — a real debit — while
|
|
1400
|
-
* `railAsFundingMethod` correctly declines to guess a rail, so `bookRevenue`
|
|
1401
|
-
* books nothing and outstanding liability (`deposits − draw revenue`) stays
|
|
1402
|
-
* overstated by the drawn amount forever. `credits.rail` stays nullable
|
|
1403
|
-
* because rows written before this change still exist in live databases:
|
|
1404
|
-
* **write is closed, read is not.** A legacy row adopts a rail from its first
|
|
1405
|
-
* typed top-up via the `COALESCE` below.
|
|
1406
|
-
*
|
|
1407
|
-
* The rail-native **instrument** is pinned at first funding too, and both
|
|
1408
|
-
* channel guards are symmetric for the same reason (internal-review). A reusable
|
|
1409
|
-
* channel — Tempo session or x402 batch-settlement — may only ever top up
|
|
1410
|
-
* the credit it opened, and a credit opened by a one-shot (a Tempo charge, an
|
|
1411
|
-
* x402 exact authorization) may never adopt one. The x402 half used to admit
|
|
1412
|
-
* `credits.x402_channel_id IS NULL`, so a channel deposit bound itself to a
|
|
1413
|
-
* credit already holding exact-funded value: `earnedNativeForX402Channel`
|
|
1414
|
-
* then read that credit's whole settled-draw history as the channel's
|
|
1415
|
-
* earnings, and the first claim tick swept a deposit nobody had consumed,
|
|
1416
|
-
* while the unspent balance lost every exit (the drain's refund base goes
|
|
1417
|
-
* below `totalClaimed`, and a channel-bound credit refuses every non-x402
|
|
1418
|
-
* drain). `credits_x402_channel_uidx` does not cover this: it guards
|
|
1419
|
-
* one-credit-per-channel, not one-source-per-credit.
|
|
1420
|
-
*
|
|
1421
|
-
* An x402 channel deposit is additionally refused `settlement_pending` while
|
|
1422
|
-
* that channel carries an unresolved refund (internal-review) — see the gate read
|
|
1423
|
-
* below. This method is the authority on that rule, as it is on the binding
|
|
1424
|
-
* rules above; every door preflights it where a refusal is still free.
|
|
1338
|
+
* Funding adds balance and overwrites expiry, reviving an expired credit. It is additive,
|
|
1339
|
+
* not idempotent: the caller’s rail marker or funding record must prevent replay. Owner and
|
|
1340
|
+
* currency must match; rail and reusable-channel bindings cannot be mixed with other
|
|
1341
|
+
* instruments. A legacy NULL rail adopts its first typed top-up.
|
|
1342
|
+
*
|
|
1343
|
+
* With tx, all writes join the caller’s transaction without transaction control. Otherwise
|
|
1344
|
+
* the ledger owns a transaction covering balance, funding lot, reversal of standing expiry
|
|
1345
|
+
* releases and their reports. A partial revival would leave restored balance with a release
|
|
1346
|
+
* still counted.
|
|
1347
|
+
*
|
|
1348
|
+
* An incoming x402 channel with an unresolved refund is refused before the upsert. That
|
|
1349
|
+
* preflight is not atomic with a concurrent cooperative drain; see the incoming-channel
|
|
1350
|
+
* gate below.
|
|
1425
1351
|
*/
|
|
1426
1352
|
async fund(args) {
|
|
1427
1353
|
assertAmount(args.amountMicro, { min: 1 });
|
|
@@ -1449,11 +1375,6 @@ var CreditLedger = class {
|
|
|
1449
1375
|
client.release();
|
|
1450
1376
|
}
|
|
1451
1377
|
}
|
|
1452
|
-
/**
|
|
1453
|
-
* The funding itself, on whichever handle {@link fund} chose. Every refusal
|
|
1454
|
-
* it raises is the caller's to see unchanged; a self-opened transaction rolls
|
|
1455
|
-
* back around it.
|
|
1456
|
-
*/
|
|
1457
1378
|
async fundOn(q, args, b, nowMs, creditId) {
|
|
1458
1379
|
const wedged = await this.blockingX402Refund(
|
|
1459
1380
|
b.x402Channel?.channelId,
|
|
@@ -1591,33 +1512,12 @@ var CreditLedger = class {
|
|
|
1591
1512
|
return this.snapshotFromRow(revived ?? rows[0], q, nowMs);
|
|
1592
1513
|
}
|
|
1593
1514
|
/**
|
|
1594
|
-
*
|
|
1595
|
-
*
|
|
1596
|
-
* release
|
|
1515
|
+
* Funding reverses standing expiry releases, restores their released micro and queues
|
|
1516
|
+
* reversal reports on the funding transaction. Do this even when the new expiry is already
|
|
1517
|
+
* past: the next sweep must release the enlarged remainder as a new episode.
|
|
1597
1518
|
*
|
|
1598
|
-
*
|
|
1599
|
-
*
|
|
1600
|
-
* overwrites `expiry_ms` (spec §5 carry-forward: the credit is a rolling
|
|
1601
|
-
* buffer), so the moment money arrives the recorded remainder is no longer
|
|
1602
|
-
* final: the release stops counting and its micro come back.
|
|
1603
|
-
*
|
|
1604
|
-
* Deliberately unconditional on the incoming expiry rather than gated on
|
|
1605
|
-
* `expiryMs > nowMs`. A release exists only for a credit that had already
|
|
1606
|
-
* lapsed, so any funding that reaches one is money the release did not
|
|
1607
|
-
* account for — and a funding that arrives with an already-past expiry leaves
|
|
1608
|
-
* a credit the very next sweep re-releases at its new, larger remainder. One
|
|
1609
|
-
* rule, self-healing in both directions, and it never needs to read the
|
|
1610
|
-
* pre-funding expiry the upsert has already overwritten.
|
|
1611
|
-
*
|
|
1612
|
-
* Runs on the funding's own handle — which {@link fund} now guarantees is a
|
|
1613
|
-
* transaction — so the reversal, the restored balance and the report commit
|
|
1614
|
-
* with the money or not at all. A reversal that committed without its report
|
|
1615
|
-
* would be unrecoverable: `WHERE reversed_at IS NULL` means no later pass
|
|
1616
|
-
* re-derives it.
|
|
1617
|
-
*
|
|
1618
|
-
* @returns the credit row as the restore left it, or undefined when there was
|
|
1619
|
-
* nothing to reverse. The caller reads its snapshot off this rather than off
|
|
1620
|
-
* the funding upsert's `RETURNING`, which predates the restore.
|
|
1519
|
+
* Returns the row after restoration, or undefined when no release stood. The upsert’s
|
|
1520
|
+
* RETURNING row predates the restoration and is not the final balance.
|
|
1621
1521
|
*/
|
|
1622
1522
|
async reverseExpiryReleases(q, creditId, nowMs) {
|
|
1623
1523
|
const { rows } = await q.query(
|
|
@@ -1642,37 +1542,15 @@ var CreditLedger = class {
|
|
|
1642
1542
|
return restored[0];
|
|
1643
1543
|
}
|
|
1644
1544
|
/**
|
|
1645
|
-
* Release
|
|
1646
|
-
*
|
|
1647
|
-
*
|
|
1648
|
-
*
|
|
1649
|
-
*
|
|
1650
|
-
*
|
|
1651
|
-
*
|
|
1652
|
-
*
|
|
1653
|
-
*
|
|
1654
|
-
* `tempo` rail). A Tempo credit is channel-backed and its undrawn value
|
|
1655
|
-
* returns to the *caller* at channel exit, so releasing it would book the
|
|
1656
|
-
* builder money the chain is about to hand back.
|
|
1657
|
-
*
|
|
1658
|
-
* **A credit with a pending hold is skipped, not partially released.** A draw
|
|
1659
|
-
* placed before expiry stays settleable afterwards (`draw`'s replay lookup
|
|
1660
|
-
* runs above the expiry check, so a lost response is still recoverable), so
|
|
1661
|
-
* the remainder is not final while a hold is outstanding. Skipping costs one
|
|
1662
|
-
* sweep interval and keeps the released figure exactly "what nothing bought";
|
|
1663
|
-
* holds do resolve — the orphan-draw watchdog (internal-review) is what guarantees
|
|
1664
|
-
* a stranded one still reaches a terminal state.
|
|
1665
|
-
*
|
|
1666
|
-
* **Idempotent** two ways. `release_id` is derived from the expiry instant,
|
|
1667
|
-
* so a re-sweep after a crash between the insert and its report collides on
|
|
1668
|
-
* the same primary key instead of writing a second row; and the candidate
|
|
1669
|
-
* scan excludes any credit that already has an un-reversed release.
|
|
1670
|
-
*
|
|
1671
|
-
* Each candidate is re-checked under the credit row lock every
|
|
1672
|
-
* `credit_draws` write is taken under, so a draw racing the sweep either
|
|
1673
|
-
* loses the race (its credit is already released and it would have been
|
|
1674
|
-
* refused `credit_expired` anyway) or wins it and leaves a pending hold the
|
|
1675
|
-
* locked re-check sees.
|
|
1545
|
+
* Release expired undrawn balances, retaining readable credit records. Tempo rail or
|
|
1546
|
+
* channel-backed credits are excluded because their collateral returns to the caller
|
|
1547
|
+
* through the channel lifecycle.
|
|
1548
|
+
*
|
|
1549
|
+
* Pending holds prevent release: jobs accepted before expiry may still settle or release.
|
|
1550
|
+
* Candidates are rechecked under the same parent-credit lock used by draw writes. The
|
|
1551
|
+
* balance debit, episode record and report commit together; standing releases exclude
|
|
1552
|
+
* repeat work. A later revival may reuse the expiry instant but receives a new release
|
|
1553
|
+
* sequence.
|
|
1676
1554
|
*/
|
|
1677
1555
|
async sweepExpiredCredits(args) {
|
|
1678
1556
|
const nowMs = args?.nowMs ?? Date.now();
|
|
@@ -1708,30 +1586,12 @@ var CreditLedger = class {
|
|
|
1708
1586
|
return { released };
|
|
1709
1587
|
}
|
|
1710
1588
|
/**
|
|
1711
|
-
*
|
|
1712
|
-
*
|
|
1713
|
-
*
|
|
1714
|
-
* against, or already released between the scan and the lock.
|
|
1715
|
-
*
|
|
1716
|
-
* **Zeroing the balance is what makes the release real**, and it is the whole
|
|
1717
|
-
* reason this runs under the lock rather than as a bare INSERT. The row alone
|
|
1718
|
-
* records that the caller's claim ended; it does not *end* it. `requestDrain`
|
|
1719
|
-
* has no expiry check by design (expiry gates new draws, not reclaims), so a
|
|
1720
|
-
* credit whose remainder had been released and reported was still fully
|
|
1721
|
-
* drainable: the DVM would pay out money the platform had already booked as
|
|
1722
|
-
* the builder's, and `deposited - drawn - drained - released` would go
|
|
1723
|
-
* negative on the same micro. With the balance at zero, `requestDrain`'s
|
|
1724
|
-
* existing `availableMicro <= 0` guard refuses with `nothing_to_drain` and no
|
|
1725
|
-
* new check is needed anywhere. {@link reverseExpiryReleases} puts the
|
|
1726
|
-
* balance back when a funding revives the credit.
|
|
1589
|
+
* Recheck eligibility under the credit lock and zero the released balance in the same
|
|
1590
|
+
* transaction as its episode/report. Otherwise requestDrain, which intentionally ignores
|
|
1591
|
+
* expiry, could reclaim value already released to the builder.
|
|
1727
1592
|
*
|
|
1728
|
-
*
|
|
1729
|
-
*
|
|
1730
|
-
*
|
|
1731
|
-
* The zeroed balance also takes the credit out of `BITCOIN_CREDIT_PREDICATE`
|
|
1732
|
-
* (`balance_micro > 0`), so the in-kind sweep floor stops counting sats the
|
|
1733
|
-
* builder no longer owes back — and counts them again after a revival, with
|
|
1734
|
-
* the credit's funding lots untouched throughout.
|
|
1593
|
+
* Keep status active so future funding can revive it. Funding lots remain intact: balance
|
|
1594
|
+
* zero excludes this credit from Bitcoin liability, and reversal restores that liability.
|
|
1735
1595
|
*/
|
|
1736
1596
|
async releaseExpiredCreditLocked(client, credit, nowMs) {
|
|
1737
1597
|
const expiryMs = toSafeInt(credit.expiry_ms);
|
|
@@ -1779,34 +1639,12 @@ var CreditLedger = class {
|
|
|
1779
1639
|
return row;
|
|
1780
1640
|
}
|
|
1781
1641
|
/**
|
|
1782
|
-
* Record
|
|
1783
|
-
*
|
|
1784
|
-
*
|
|
1785
|
-
* whatever transaction the rail opened — `withCommitTx` for Cashu,
|
|
1786
|
-
* `ProcessedPaymentStore.withTx` for x402/Tempo, `settleInvoice`'s own
|
|
1787
|
-
* `BEGIN` for Lightning. The funding-atomicity invariant therefore extends
|
|
1788
|
-
* to it structurally rather than by every call site remembering: a lot
|
|
1789
|
-
* cannot exist without its ledger credit, or the credit without its lot.
|
|
1790
|
-
*
|
|
1791
|
-
* Only a **channel-backed** credit writes no lot: it is in-kind already —
|
|
1792
|
-
* the escrow itself retains the unused value — so a lot on one would be
|
|
1793
|
-
* counted as Bitcoin deposit liability the hub does not hold.
|
|
1794
|
-
*
|
|
1795
|
-
* Every other non-channel Bitcoin funding writes one, the implicit N=1
|
|
1796
|
-
* per-call payment included. That one funds and draws the same amount in
|
|
1797
|
-
* this transaction, so it looks like a lot born depleted — but the draw is a
|
|
1798
|
-
* *pending hold*, not a debit, and only a `completed` job settles it
|
|
1799
|
-
* (`JobManager.settlesDraw`). A failed, cancelled or stale-swept job
|
|
1800
|
-
* releases the hold and the whole funded amount becomes reclaimable balance,
|
|
1801
|
-
* so suppressing the lot here would leave routine traffic reclaiming off a
|
|
1802
|
-
* live rate and missing from the sweep floor. One small insert on the hot
|
|
1803
|
-
* path buys the invariant the liability read depends on: open lots cover
|
|
1804
|
-
* `balance_micro` for every Bitcoin credit, always.
|
|
1642
|
+
* Record non-channel Bitcoin funding lots on the funding transaction. An implicit payment
|
|
1643
|
+
* also needs a lot: its draw is initially only a hold, and failure releases the whole
|
|
1644
|
+
* balance for reclaim. Channel collateral is excluded from Bitcoin lots.
|
|
1805
1645
|
*
|
|
1806
|
-
* A zero-sats basis still
|
|
1807
|
-
*
|
|
1808
|
-
* distinguishable state from "no lot at all" for the reclaim and the
|
|
1809
|
-
* liability rollup, instead of both reading as a gap.
|
|
1646
|
+
* A zero-sats basis still records a lot, distinguishing covered but basisless credit from a
|
|
1647
|
+
* missing-lot coverage gap.
|
|
1810
1648
|
*/
|
|
1811
1649
|
async recordFundingLot(q, creditId, amountMicro, basis, nowMs) {
|
|
1812
1650
|
if (!isNonChannelBitcoinRail(basis.rail)) return;
|
|
@@ -1827,25 +1665,13 @@ var CreditLedger = class {
|
|
|
1827
1665
|
);
|
|
1828
1666
|
}
|
|
1829
1667
|
/**
|
|
1830
|
-
* Place a
|
|
1831
|
-
*
|
|
1832
|
-
*
|
|
1833
|
-
* `replayed: true` and places no second hold — this is how a caller recovers
|
|
1834
|
-
* a lost response (spec condition 1). A replay whose `amountMicro` or
|
|
1835
|
-
* `jobId` differs from the recorded draw is not a replay but a money bug in
|
|
1836
|
-
* the caller, refused with `draw_conflict`.
|
|
1668
|
+
* Place a pending hold with the next shared per-credit sequence. A matching (creditId,
|
|
1669
|
+
* drawId) replay returns its recorded tuple without another hold; conflicting amount or
|
|
1670
|
+
* jobId is draw_conflict.
|
|
1837
1671
|
*
|
|
1838
|
-
*
|
|
1839
|
-
*
|
|
1840
|
-
*
|
|
1841
|
-
* expired credit are refused with `credit_expired`; the balance stays
|
|
1842
|
-
* intact and readable (spec §5: expiry ends spending, never ownership).
|
|
1843
|
-
*
|
|
1844
|
-
* Pass `tx` (a client inside a caller-owned `BEGIN`) to place the hold in
|
|
1845
|
-
* the same transaction as the rail commit and the `fund` upsert (spec
|
|
1846
|
-
* condition 3 — the internal-review implicit N=1 path). The ledger issues no
|
|
1847
|
-
* transaction control on `tx`; the `SELECT … FOR UPDATE` row lock is still
|
|
1848
|
-
* taken on the caller's transaction, so the locking invariant holds.
|
|
1672
|
+
* Replay precedes expiry and settlement gates so a lost response stays recoverable. Fresh
|
|
1673
|
+
* draws require spendable credit. With tx, take the parent-credit lock on the caller’s
|
|
1674
|
+
* transaction without issuing transaction control.
|
|
1849
1675
|
*/
|
|
1850
1676
|
async draw(args) {
|
|
1851
1677
|
assertAmount(args.amountMicro, { min: 0 });
|
|
@@ -1961,66 +1787,26 @@ var CreditLedger = class {
|
|
|
1961
1787
|
return this.withCreditLock(args.creditId, (client, credit) => run(client, credit));
|
|
1962
1788
|
}
|
|
1963
1789
|
/**
|
|
1964
|
-
*
|
|
1965
|
-
*
|
|
1966
|
-
*
|
|
1967
|
-
* `undefined` whenever there is nothing to ask — an unbound credit, or a DVM
|
|
1968
|
-
* with no durable settlement store, where a settlement row cannot exist.
|
|
1969
|
-
*
|
|
1970
|
-
* Public since internal-review, so `/v1/credit`'s pre-payment preflight can ask the
|
|
1971
|
-
* same question `fund` will ask from inside the rail transaction — one read,
|
|
1972
|
-
* one answer, rather than a second copy of the rule in the routes.
|
|
1790
|
+
* Return the unresolved refund that blocks an effect on this channel, or undefined. Route
|
|
1791
|
+
* preflights use the same rule before moving money; draw and drain also read it under the
|
|
1792
|
+
* credit lock.
|
|
1973
1793
|
*/
|
|
1974
1794
|
async blockingX402Refund(channelId, statuses) {
|
|
1975
1795
|
if (!channelId || !this.x402Settlements) return void 0;
|
|
1976
1796
|
return this.x402Settlements.pendingRefundSettlement(channelId, statuses);
|
|
1977
1797
|
}
|
|
1978
1798
|
/**
|
|
1979
|
-
* Grow
|
|
1980
|
-
*
|
|
1981
|
-
*
|
|
1982
|
-
* so the receipt's `ReceiptCredit` block countersigns the job's full cost
|
|
1983
|
-
* and `revenue_events` still books exactly one row per job.
|
|
1984
|
-
*
|
|
1985
|
-
* `draw()` cannot express this: a same-`draw_id` call with a different
|
|
1986
|
-
* amount is `draw_conflict` by design (a replayed draw must never re-price).
|
|
1987
|
-
*
|
|
1988
|
-
* The arithmetic is `draw()`'s, unchanged: `addAmountMicro` is checked
|
|
1989
|
-
* against — and `balance_after_micro` recomputed from — the balance net of
|
|
1990
|
-
* **every** pending hold, this draw's own included. That is deliberately the
|
|
1991
|
-
* conservative reading (a fresh draw of `addAmountMicro` would see exactly
|
|
1992
|
-
* the same figure), and it is what keeps the total held after growth inside
|
|
1993
|
-
* the credit's balance.
|
|
1994
|
-
*
|
|
1995
|
-
* The increment's rail value comes from {@link growthRailValue}: earmarked
|
|
1996
|
-
* to the funding that backs it when the caller names one (the mid-job case),
|
|
1997
|
-
* pro-rata otherwise. Either way the draws of a credit keep summing to
|
|
1998
|
-
* precisely what the rails paid (internal-review).
|
|
1999
|
-
*
|
|
2000
|
-
* **`ledger_seq` is not re-taken.** The per-credit sequence is gap-free
|
|
2001
|
-
* (`credit_draws_seq_uidx`), so moving this draw forward would strand its
|
|
2002
|
-
* original number. The consequence is deliberate and worth knowing: the
|
|
2003
|
-
* recorded `balance_after_micro` is the post-growth figure stamped at the
|
|
2004
|
-
* *original* seq, so a credit with interleaved sibling activity has a
|
|
2005
|
-
* trajectory that is truthful per draw rather than monotonic across seqs.
|
|
2006
|
-
*
|
|
2007
|
-
* **No expiry check**, matching `settle`/`release`: the job was accepted
|
|
2008
|
-
* pre-expiry and is still running. Refusing here would strand the handler
|
|
2009
|
-
* mid-flight over a clock the caller can't influence.
|
|
1799
|
+
* Grow an accepted job’s pending hold while preserving its original ledger_seq. The balance
|
|
1800
|
+
* tuple is updated at that sequence, so interleaved draws need not form a monotonic balance
|
|
1801
|
+
* trajectory. No expiry check applies to an already accepted job.
|
|
2010
1802
|
*
|
|
2011
|
-
*
|
|
2012
|
-
*
|
|
2013
|
-
* the whole transaction back on replay; x402/mpp collide on the
|
|
2014
|
-
* `processed_payments` marker). Never call it outside a rail commit.
|
|
1803
|
+
* Available balance excludes every pending hold, including this one. An earmark allocates
|
|
1804
|
+
* the incoming funding’s rail value; absent one, growth uses the pool.
|
|
2015
1805
|
*
|
|
2016
|
-
*
|
|
2017
|
-
*
|
|
2018
|
-
*
|
|
2019
|
-
*
|
|
2020
|
-
* drawn. Excess is **granted partially or not at all rather than thrown** —
|
|
2021
|
-
* this runs inside the rail commit, so a throw would roll back cashu proofs
|
|
2022
|
-
* the caller already sent. The refused remainder stays funded balance the
|
|
2023
|
-
* caller owns and can drain.
|
|
1806
|
+
* Growth has no independent replay key: compose it with the rail’s replay protection in one
|
|
1807
|
+
* transaction. capMicro bounds cumulative growth at the job’s cumulative ask. Excess
|
|
1808
|
+
* remains caller-owned funded balance; partial or zero growth returns that excess rather
|
|
1809
|
+
* than throwing and rolling back accepted proofs.
|
|
2024
1810
|
*/
|
|
2025
1811
|
async growDraw(args) {
|
|
2026
1812
|
assertAmount(args.addAmountMicro, { min: 1 });
|
|
@@ -2204,7 +1990,7 @@ var CreditLedger = class {
|
|
|
2204
1990
|
}
|
|
2205
1991
|
/**
|
|
2206
1992
|
* Release a pending draw: the hold evaporates, the balance is untouched —
|
|
2207
|
-
* this is how "no debit on job failure" is mechanically real (
|
|
1993
|
+
* this is how "no debit on job failure" is mechanically real (the credit lifecycle contract).
|
|
2208
1994
|
* Idempotent: releasing a released draw returns the original tuple. A
|
|
2209
1995
|
* settled draw cannot be released (`invalid_draw_state`) — un-settling
|
|
2210
1996
|
* booked revenue is a reconciliation problem, not a ledger verb.
|
|
@@ -2213,18 +1999,9 @@ var CreditLedger = class {
|
|
|
2213
1999
|
return this.resolveDraw({ ...args, to: "released" });
|
|
2214
2000
|
}
|
|
2215
2001
|
/**
|
|
2216
|
-
* Record
|
|
2217
|
-
*
|
|
2218
|
-
*
|
|
2219
|
-
* loses with a typed `funding_replayed` rather than crediting twice.
|
|
2220
|
-
*
|
|
2221
|
-
* Call it inside the same `tx` as {@link fund} — the rail commit, the
|
|
2222
|
-
* funding record, and the balance increment must land together or not at
|
|
2223
|
-
* all (spec §2 condition 3). Like `fund`, this issues no transaction
|
|
2224
|
-
* control on `tx`; it is a single statement.
|
|
2225
|
-
*
|
|
2226
|
-
* This does **not** move money on its own. `fund` still does the crediting;
|
|
2227
|
-
* this row is what lets a retry be answered with the original outcome.
|
|
2002
|
+
* Record the fund-only replay key without moving money. A duplicate (credit_id, fund_id)
|
|
2003
|
+
* raises funding_replayed. Compose this insert, fund and rail acceptance on the same
|
|
2004
|
+
* caller-owned transaction; a retry reads the recorded funding outcome.
|
|
2228
2005
|
*/
|
|
2229
2006
|
async recordFunding(args) {
|
|
2230
2007
|
assertAmount(args.amountMicro, { min: 1 });
|
|
@@ -2295,7 +2072,7 @@ var CreditLedger = class {
|
|
|
2295
2072
|
return rows[0].receipt;
|
|
2296
2073
|
}
|
|
2297
2074
|
/**
|
|
2298
|
-
* Record the bolt11 issued for a `(creditId, fundId)` top-up
|
|
2075
|
+
* Record the bolt11 issued for a `(creditId, fundId)` top-up, or
|
|
2299
2076
|
* return the one already issued for it.
|
|
2300
2077
|
*
|
|
2301
2078
|
* **Returning the existing row is the point.** A caller re-polling an unpaid
|
|
@@ -2349,7 +2126,6 @@ var CreditLedger = class {
|
|
|
2349
2126
|
}
|
|
2350
2127
|
return existing;
|
|
2351
2128
|
}
|
|
2352
|
-
/** Read one invoice (no lock). */
|
|
2353
2129
|
async getInvoice(args) {
|
|
2354
2130
|
const { rows } = await this.pool.query(
|
|
2355
2131
|
`SELECT * FROM credit_invoices WHERE credit_id = $1 AND fund_id = $2`,
|
|
@@ -2374,22 +2150,10 @@ var CreditLedger = class {
|
|
|
2374
2150
|
return rows.map(invoiceRecordFromRow);
|
|
2375
2151
|
}
|
|
2376
2152
|
/**
|
|
2377
|
-
*
|
|
2378
|
-
*
|
|
2379
|
-
*
|
|
2380
|
-
*
|
|
2381
|
-
* One transaction covers the funding record, the balance, and the invoice's
|
|
2382
|
-
* own status flip, so the three can never disagree. A crash anywhere inside
|
|
2383
|
-
* leaves the invoice `pending` and the balance untouched: the next
|
|
2384
|
-
* `lookup_invoice` observes the same settled payment and credits it, once.
|
|
2385
|
-
* A replayed check after the commit sees `settled` and moves nothing.
|
|
2386
|
-
*
|
|
2387
|
-
* The lock is on the **invoice** row, not the credit row: the first
|
|
2388
|
-
* Lightning top-up is what opens the credit, so there is frequently no
|
|
2389
|
-
* `credits` row to take `FOR UPDATE` yet. That does not weaken the ledger's
|
|
2390
|
-
* concurrency contract — no `credit_draws` write happens here, and `fund`'s
|
|
2391
|
-
* upsert is a single statement — while still serialising two settlement
|
|
2392
|
-
* checks racing on the same invoice.
|
|
2153
|
+
* Serialize settlement on the invoice row, which exists before the first credit does.
|
|
2154
|
+
* Funding record, balance, invoice status and optional deposit outbox commit together. An
|
|
2155
|
+
* interrupted transaction leaves the prior invoice status intact for retry; a settled replay
|
|
2156
|
+
* moves no money.
|
|
2393
2157
|
*/
|
|
2394
2158
|
async settleInvoice(args) {
|
|
2395
2159
|
assertFundingBasis(args.basis);
|
|
@@ -2483,19 +2247,9 @@ var CreditLedger = class {
|
|
|
2483
2247
|
return this.getInvoice(args);
|
|
2484
2248
|
}
|
|
2485
2249
|
/**
|
|
2486
|
-
*
|
|
2487
|
-
*
|
|
2488
|
-
*
|
|
2489
|
-
* expired invoice was never paid and owes nobody anything, whereas a blocked
|
|
2490
|
-
* one has sats sitting in the builder's wallet with no ledger row to put them
|
|
2491
|
-
* on. Only an operator can resolve that, so the row records `reason` and
|
|
2492
|
-
* stops being swept — leaving it `pending` would spend one of the sweep's few
|
|
2493
|
-
* slots re-deriving the same verdict on every request the caller makes, and
|
|
2494
|
-
* three of them would wedge that caller's sweep so a genuinely payable
|
|
2495
|
-
* invoice behind them never credits.
|
|
2496
|
-
*
|
|
2497
|
-
* A CAS on `pending`, like the expiry retirement: a settlement observed
|
|
2498
|
-
* concurrently must win.
|
|
2250
|
+
* Mark a paid invoice blocked when its payment cannot be credited. This removes it from the
|
|
2251
|
+
* bounded pending sweep and leaves its reason for operator repair. The pending-only CAS
|
|
2252
|
+
* cannot overwrite a committed settlement.
|
|
2499
2253
|
*/
|
|
2500
2254
|
async markInvoiceBlocked(args) {
|
|
2501
2255
|
await this.pool.query(
|
|
@@ -2505,7 +2259,6 @@ var CreditLedger = class {
|
|
|
2505
2259
|
);
|
|
2506
2260
|
return this.getInvoice(args);
|
|
2507
2261
|
}
|
|
2508
|
-
/** Read one invoice by the wallet's own identifier for the payment (no lock). */
|
|
2509
2262
|
async getInvoiceByPaymentHash(paymentHash) {
|
|
2510
2263
|
const { rows } = await this.pool.query(
|
|
2511
2264
|
`SELECT * FROM credit_invoices WHERE payment_hash = $1`,
|
|
@@ -2514,17 +2267,9 @@ var CreditLedger = class {
|
|
|
2514
2267
|
return rows.length > 0 ? invoiceRecordFromRow(rows[0]) : void 0;
|
|
2515
2268
|
}
|
|
2516
2269
|
/**
|
|
2517
|
-
*
|
|
2518
|
-
*
|
|
2519
|
-
*
|
|
2520
|
-
* self-clear, so a row the operator declines to act on sits at the head of
|
|
2521
|
-
* the age ordering forever; a fixed first page would starve everything
|
|
2522
|
-
* behind it on every run, which is the internal-review shape one page up.
|
|
2523
|
-
*
|
|
2524
|
-
* `includeResolved` widens to the operator-resolved statuses so a run can be
|
|
2525
|
-
* audited after the fact — the acceptance criterion this verb exists for.
|
|
2526
|
-
* Host-wide, like `listPendingDrains`: the ledger has no `dvm_id`, so on a
|
|
2527
|
-
* multi-mount host one builder's admin credential reads every mount's rows.
|
|
2270
|
+
* Host-wide operator queue, keyset-paginated so an untouched blocked row does not starve
|
|
2271
|
+
* later rows. includeResolved also exposes reconciled and written-off history; the ledger
|
|
2272
|
+
* has no per-mount dvm_id.
|
|
2528
2273
|
*/
|
|
2529
2274
|
async listBlockedInvoices(args) {
|
|
2530
2275
|
const statuses = args.includeResolved ? ["blocked", "reconciled", "written_off"] : ["blocked"];
|
|
@@ -2546,37 +2291,15 @@ var CreditLedger = class {
|
|
|
2546
2291
|
return rows.map(invoiceRecordFromRow);
|
|
2547
2292
|
}
|
|
2548
2293
|
/**
|
|
2549
|
-
* Apply a blocked invoice
|
|
2550
|
-
*
|
|
2551
|
-
*
|
|
2552
|
-
*
|
|
2553
|
-
*
|
|
2554
|
-
*
|
|
2555
|
-
*
|
|
2556
|
-
*
|
|
2557
|
-
*
|
|
2558
|
-
* identifier the operator actually holds (it is what the
|
|
2559
|
-
* `lightning_settlement_blocked` log carries and what names the payment in
|
|
2560
|
-
* their wallet). Invoice row first, credit row second via `fund`'s upsert —
|
|
2561
|
-
* the same order `settleInvoice` takes, which is what keeps a reconcile and
|
|
2562
|
-
* a concurrent settlement check from deadlocking against each other.
|
|
2563
|
-
* - **The funding lands at `(targetCreditId, paymentHash)`, not the invoice's
|
|
2564
|
-
* own `(credit_id, fund_id)`.** That key is frequently the reason the row
|
|
2565
|
-
* is blocked at all — `funding_replayed_on_cashu` means something else
|
|
2566
|
-
* already holds it — and the operator's most natural target is that very
|
|
2567
|
-
* credit. The payment hash cannot collide with it, and it makes all three
|
|
2568
|
-
* references to this payment agree: `basis.fundingRef`, the deposit's
|
|
2569
|
-
* `funding_id`, and the funding row's `fund_id`.
|
|
2570
|
-
* - **`written_off` is an accepted input status.** A write-off unwound
|
|
2571
|
-
* nothing, so an operator who closed a row by mistake must not need raw SQL
|
|
2572
|
-
* against a money table to reopen it.
|
|
2573
|
-
*
|
|
2574
|
-
* The invoice row's status — never the funding row — is the idempotency
|
|
2575
|
-
* source of truth. `fund_id` is caller-chosen and the payment hash is
|
|
2576
|
-
* disclosed to them, so a caller *can* squat `(theirCredit, thatHash)` with a
|
|
2577
|
-
* top-up on another rail; when they have, `recordFunding` throws
|
|
2578
|
-
* `funding_replayed` and that surfaces as its own loud failure rather than
|
|
2579
|
-
* being absorbed as "already reconciled".
|
|
2294
|
+
* Apply a blocked or written-off invoice to an operator-named credit. Lock the invoice by
|
|
2295
|
+
* its unique payment_hash before funding the credit, matching settlement’s
|
|
2296
|
+
* invoice-to-credit lock order. Funding, balance, resolution and optional outbox share one
|
|
2297
|
+
* transaction.
|
|
2298
|
+
*
|
|
2299
|
+
* The repair funding key is (targetCreditId, paymentHash), distinct from the original
|
|
2300
|
+
* caller-chosen fundId. Invoice status determines replay. A caller can still occupy the
|
|
2301
|
+
* repair key with another payment; funding_replayed must remain a refusal, not be absorbed
|
|
2302
|
+
* as already reconciled. Preserve blocked_reason and the wallet’s settlement instant.
|
|
2580
2303
|
*/
|
|
2581
2304
|
async reconcileBlockedInvoice(args) {
|
|
2582
2305
|
assertFundingBasis(args.basis);
|
|
@@ -2676,17 +2399,9 @@ var CreditLedger = class {
|
|
|
2676
2399
|
}
|
|
2677
2400
|
}
|
|
2678
2401
|
/**
|
|
2679
|
-
*
|
|
2680
|
-
*
|
|
2681
|
-
*
|
|
2682
|
-
* A CAS on `blocked`, and idempotent: re-running returns the recorded row
|
|
2683
|
-
* rather than overwriting the first note. It cannot capture a `reconciled`
|
|
2684
|
-
* row, because that one moved money.
|
|
2685
|
-
*
|
|
2686
|
-
* `written` reports whether *this* call was the transition. The caller has
|
|
2687
|
-
* no other way to tell — the row reads identically either way — and the
|
|
2688
|
-
* write-off's audit log is the only record of the amount, so a re-emitted
|
|
2689
|
-
* one reads as a second decision about the same sats.
|
|
2402
|
+
* Write off an invoice without crediting or moving money. Only the first transition records
|
|
2403
|
+
* the note; its written bit controls the audit event. An operator can later reconcile a
|
|
2404
|
+
* written-off invoice, because write-off did not return its payment.
|
|
2690
2405
|
*/
|
|
2691
2406
|
async writeOffBlockedInvoice(args) {
|
|
2692
2407
|
const { rowCount } = await this.pool.query(
|
|
@@ -2720,7 +2435,6 @@ var CreditLedger = class {
|
|
|
2720
2435
|
}
|
|
2721
2436
|
return out;
|
|
2722
2437
|
}
|
|
2723
|
-
/** Read one draw (no lock), with its credit's rail basis joined in. */
|
|
2724
2438
|
async getDraw(args) {
|
|
2725
2439
|
const { rows } = await this.pool.query(
|
|
2726
2440
|
`${DRAW_WITH_BASIS_SELECT} WHERE d.credit_id = $1 AND d.draw_id = $2`,
|
|
@@ -2736,7 +2450,6 @@ var CreditLedger = class {
|
|
|
2736
2450
|
);
|
|
2737
2451
|
return rows.length === 1 ? drawRecordFromRow(rows[0], rows[0]) : void 0;
|
|
2738
2452
|
}
|
|
2739
|
-
/** Read every draw linked to a job, including already-resolved rows. */
|
|
2740
2453
|
async listDrawsByJobId(jobId) {
|
|
2741
2454
|
const { rows } = await this.pool.query(
|
|
2742
2455
|
`${DRAW_WITH_BASIS_SELECT} WHERE d.job_id = $1 ORDER BY d.created_at, d.credit_id, d.ledger_seq`,
|
|
@@ -2746,7 +2459,7 @@ var CreditLedger = class {
|
|
|
2746
2459
|
}
|
|
2747
2460
|
/**
|
|
2748
2461
|
* All pending holds on a credit, in `ledger_seq` order (no lock). Read
|
|
2749
|
-
* surface for the
|
|
2462
|
+
* surface for the sweeper wiring — a worker that dies mid-job
|
|
2750
2463
|
* leaves its hold `pending` until something releases it.
|
|
2751
2464
|
*/
|
|
2752
2465
|
async listPendingDraws(creditId) {
|
|
@@ -2757,22 +2470,10 @@ var CreditLedger = class {
|
|
|
2757
2470
|
return rows.map((row) => drawRecordFromRow(row, row));
|
|
2758
2471
|
}
|
|
2759
2472
|
/**
|
|
2760
|
-
*
|
|
2761
|
-
*
|
|
2762
|
-
*
|
|
2763
|
-
*
|
|
2764
|
-
* resolve — the only release path keys off the `credit_id`/`draw_id` written
|
|
2765
|
-
* on the job row.
|
|
2766
|
-
*
|
|
2767
|
-
* Host-wide like {@link listPendingDrains}, not per-credit: the sweeper has
|
|
2768
|
-
* no candidate credit to start from. Age-bounded so it never sees a draw
|
|
2769
|
-
* whose job row is merely still in flight.
|
|
2770
|
-
*
|
|
2771
|
-
* `limit` is required and `after` keyset-paginates: an aged hold whose job
|
|
2772
|
-
* row *does* exist is legitimately un-sweepable and stays `pending`
|
|
2773
|
-
* indefinitely, so a caller that re-issues the same first page would never
|
|
2774
|
-
* see past a `limit`-sized wall of them. Pass the last row's
|
|
2775
|
-
* {@link StalePendingDrawCursor} to advance.
|
|
2473
|
+
* Page host-wide pending holds older than createdBeforeMs by (created_at, credit_id,
|
|
2474
|
+
* ledger_seq). Use the same compound key for ordering and continuation, preserving
|
|
2475
|
+
* same-millisecond rows. Terminal resolution still rechecks state under the parent-credit
|
|
2476
|
+
* lock.
|
|
2776
2477
|
*/
|
|
2777
2478
|
async listStalePendingDraws(args) {
|
|
2778
2479
|
const after = args.after;
|
|
@@ -2794,21 +2495,9 @@ var CreditLedger = class {
|
|
|
2794
2495
|
return rows.map((row) => drawRecordFromRow(row, row));
|
|
2795
2496
|
}
|
|
2796
2497
|
/**
|
|
2797
|
-
*
|
|
2798
|
-
*
|
|
2799
|
-
*
|
|
2800
|
-
* ceiling (internal-review): a channel is claimable up to what its credit's draws
|
|
2801
|
-
* have earned, never up to the deposit that funded it.
|
|
2802
|
-
*
|
|
2803
|
-
* `undefined` when no credit is bound to the channel, which the claim job
|
|
2804
|
-
* treats as "cannot be resolved" and claims nothing for — distinct from a
|
|
2805
|
-
* resolved credit that has earned `0`.
|
|
2806
|
-
*
|
|
2807
|
-
* Deliberately a sum over settled draws rather than `funded − remaining`:
|
|
2808
|
-
* a drain lowers `native_remaining` without earning anything, so the
|
|
2809
|
-
* subtraction would read the caller's own reclaim as revenue. Pending holds
|
|
2810
|
-
* are excluded for the mirror reason — a hold is work in flight, and
|
|
2811
|
-
* claiming against it would take money a release still owes back.
|
|
2498
|
+
* Sum settled draws’ native value for the credit bound to this x402 channel.
|
|
2499
|
+
* Pending/released holds and drains earn nothing. Return undefined for an unbound channel,
|
|
2500
|
+
* distinct from earned zero, so settlement cannot guess a claim limit.
|
|
2812
2501
|
*/
|
|
2813
2502
|
async earnedNativeForX402Channel(channelId) {
|
|
2814
2503
|
const { rows } = await this.pool.query(
|
|
@@ -2828,14 +2517,8 @@ var CreditLedger = class {
|
|
|
2828
2517
|
return toSafeInt(rows[0].earned);
|
|
2829
2518
|
}
|
|
2830
2519
|
/**
|
|
2831
|
-
*
|
|
2832
|
-
*
|
|
2833
|
-
*
|
|
2834
|
-
* The operator repair's entry point: a wedged settlement row carries the
|
|
2835
|
-
* channel and an `effect_id`, and this is what turns them back into the
|
|
2836
|
-
* credit id, the caller pubkey, and the drain id the effect encodes. Deriving
|
|
2837
|
-
* the credit id by splitting `effect_id` instead would be wrong — credit ids
|
|
2838
|
-
* carry their own colons (`imp:`, `fnd:` and the rail segment inside them).
|
|
2520
|
+
* Resolve the unique credit bound to the channel. The ID is opaque and can contain colons;
|
|
2521
|
+
* do not reconstruct it by splitting settlement effect IDs.
|
|
2839
2522
|
*/
|
|
2840
2523
|
async getCreditByX402Channel(channelId) {
|
|
2841
2524
|
const { rows } = await this.pool.query(
|
|
@@ -2844,7 +2527,6 @@ var CreditLedger = class {
|
|
|
2844
2527
|
);
|
|
2845
2528
|
return rows[0] ? this.snapshotFromRow(rows[0], this.pool, Date.now()) : void 0;
|
|
2846
2529
|
}
|
|
2847
|
-
/** Retire every active credit backed by a chain-proven lost Tempo channel. */
|
|
2848
2530
|
async terminalizeTempoCredits(evidence, tx) {
|
|
2849
2531
|
const channelId = evidence.channelId.toLowerCase();
|
|
2850
2532
|
const observedAt = evidence.observedAt ?? Date.now();
|
|
@@ -2910,7 +2592,6 @@ var CreditLedger = class {
|
|
|
2910
2592
|
}
|
|
2911
2593
|
return this.readTempoCreditLosses(q, { channelId });
|
|
2912
2594
|
}
|
|
2913
|
-
/** List terminal Tempo credit losses, newest observation first. */
|
|
2914
2595
|
async listTempoCreditLosses(args) {
|
|
2915
2596
|
return this.readTempoCreditLosses(this.pool, { limit: args?.limit ?? 50 });
|
|
2916
2597
|
}
|
|
@@ -3004,33 +2685,18 @@ var CreditLedger = class {
|
|
|
3004
2685
|
}
|
|
3005
2686
|
return this.readX402CreditLosses(q, { channelId });
|
|
3006
2687
|
}
|
|
3007
|
-
/** List terminal x402 credit losses, newest observation first. */
|
|
3008
2688
|
async listX402CreditLosses(args) {
|
|
3009
2689
|
return this.readX402CreditLosses(this.pool, { limit: args?.limit ?? 50 });
|
|
3010
2690
|
}
|
|
3011
|
-
// ── Drains (internal-review) ─────────────────────────────────────────────────
|
|
3012
2691
|
/**
|
|
3013
|
-
*
|
|
3014
|
-
*
|
|
3015
|
-
*
|
|
3016
|
-
*
|
|
3017
|
-
*
|
|
3018
|
-
*
|
|
3019
|
-
*
|
|
3020
|
-
*
|
|
3021
|
-
* Idempotent on `(creditId, drainId)`: a replay returns the recorded drain
|
|
3022
|
-
* (this is also the poll/pickup read). A replay whose `method` or `payout`
|
|
3023
|
-
* differs from the recorded drain is refused with `drain_conflict` —
|
|
3024
|
-
* silently honouring a changed payout would ship the caller's money to a
|
|
3025
|
-
* destination they never signed alongside this `drain_id`.
|
|
3026
|
-
*
|
|
3027
|
-
* Pending holds are NOT drained — in-flight jobs keep their money until
|
|
3028
|
-
* they settle or release. A release after a drain restores available
|
|
3029
|
-
* balance the caller reclaims with a **new** `drain_id`; drains are ledger
|
|
3030
|
-
* arithmetic, not a terminal credit state, so `status` stays `active`.
|
|
3031
|
-
*
|
|
3032
|
-
* Deliberately **no expiry check**: draining works before and after
|
|
3033
|
-
* expiry. The one thing expiry gates is new draws.
|
|
2692
|
+
* Move all available balance to a drain liability under the credit lock; pending holds stay
|
|
2693
|
+
* reserved. Expiry alone does not prevent reclaim before a sweep releases the balance.
|
|
2694
|
+
* Repeating (creditId, drainId) returns the recorded liability; owner, method and payout
|
|
2695
|
+
* conflicts refuse.
|
|
2696
|
+
*
|
|
2697
|
+
* An unresolved channel refund blocks fresh drain IDs but admits its own effect ID for
|
|
2698
|
+
* repair. Caller-supplied payout validation belongs to the route. With tx, join the channel
|
|
2699
|
+
* transaction without controlling it.
|
|
3034
2700
|
*/
|
|
3035
2701
|
async requestDrain(args) {
|
|
3036
2702
|
const nowMs = args.nowMs ?? Date.now();
|
|
@@ -3140,7 +2806,6 @@ var CreditLedger = class {
|
|
|
3140
2806
|
}
|
|
3141
2807
|
return this.withCreditLock(args.creditId, run);
|
|
3142
2808
|
}
|
|
3143
|
-
/** Read one drain (no lock), joined with its credit for currency + owner. */
|
|
3144
2809
|
async getDrain(args) {
|
|
3145
2810
|
const { rows } = await this.pool.query(
|
|
3146
2811
|
`SELECT d.*, ${DRAIN_CREDIT_COLUMNS} FROM credit_drains d
|
|
@@ -3151,7 +2816,7 @@ var CreditLedger = class {
|
|
|
3151
2816
|
return rows.length > 0 ? drainRecordFromJoinRow(rows[0]) : void 0;
|
|
3152
2817
|
}
|
|
3153
2818
|
/**
|
|
3154
|
-
* One credit's undepleted funding lots, oldest first
|
|
2819
|
+
* One credit's undepleted funding lots, oldest first — the
|
|
3155
2820
|
* public read behind the reclaim's own arithmetic, for anything that needs
|
|
3156
2821
|
* to show its work.
|
|
3157
2822
|
*/
|
|
@@ -3159,25 +2824,11 @@ var CreditLedger = class {
|
|
|
3159
2824
|
return this.openLots(this.pool, creditId);
|
|
3160
2825
|
}
|
|
3161
2826
|
/**
|
|
3162
|
-
*
|
|
3163
|
-
*
|
|
3164
|
-
*
|
|
3165
|
-
*
|
|
3166
|
-
*
|
|
3167
|
-
* exchange-rate lookup anywhere: the obligation is in kind, so a rate has
|
|
3168
|
-
* nothing to say about it. Pending job holds are deliberately **not**
|
|
3169
|
-
* subtracted — a hold either settles into revenue or releases back to the
|
|
3170
|
-
* caller, and until it does, treating it as already earned would understate
|
|
3171
|
-
* the floor. The figure is conservative by exactly that amount.
|
|
3172
|
-
*
|
|
3173
|
-
* The two coverage holes ride along rather than being folded in silently, so
|
|
3174
|
-
* a gap reads as a gap: `uncovered` is a credit whose lots fall short of its
|
|
3175
|
-
* balance, `unbacked` is one holding any open micro with no sats basis. A
|
|
3176
|
-
* reclaim on either prices the **whole** balance off a live rate — the
|
|
3177
|
-
* depletion refuses a mixture rather than blending two bases — so both are
|
|
3178
|
-
* reported as the whole balance, and neither contributes to `depositSats`.
|
|
3179
|
-
* A figure that counted the backed half of a mixed credit would be a floor
|
|
3180
|
-
* the payout never matches.
|
|
2827
|
+
* Host-wide in-kind Bitcoin deposit liability, excluding channel collateral. Credits with
|
|
2828
|
+
* missing coverage or any basisless lot are reported as gaps and use the live-rate fallback
|
|
2829
|
+
* rather than a guessed sats obligation. Group fiat micro by currency; never sum
|
|
2830
|
+
* denominations.
|
|
2831
|
+
* Pending job holds remain liabilities until settled because they may release to the caller.
|
|
3181
2832
|
*/
|
|
3182
2833
|
async bitcoinDepositLiability() {
|
|
3183
2834
|
const { rows } = await this.pool.query(
|
|
@@ -3205,21 +2856,43 @@ var CreditLedger = class {
|
|
|
3205
2856
|
byRail: [],
|
|
3206
2857
|
byCurrency: [],
|
|
3207
2858
|
uncoveredCredits: 0,
|
|
3208
|
-
|
|
2859
|
+
uncoveredByCurrency: [],
|
|
3209
2860
|
unbackedCredits: 0,
|
|
3210
|
-
|
|
2861
|
+
unbackedByCurrency: []
|
|
3211
2862
|
};
|
|
3212
2863
|
const byRail = /* @__PURE__ */ new Map();
|
|
3213
2864
|
const byCurrency = /* @__PURE__ */ new Map();
|
|
2865
|
+
const uncoveredByCurrency = /* @__PURE__ */ new Map();
|
|
2866
|
+
const unbackedByCurrency = /* @__PURE__ */ new Map();
|
|
3214
2867
|
for (const row of rows) {
|
|
3215
2868
|
const sats = toSafeInt(row.owed_sats);
|
|
3216
2869
|
const credits = toSafeInt(row.credits);
|
|
2870
|
+
const uncoveredCredits = toSafeInt(row.uncovered_credits);
|
|
2871
|
+
const unbackedCredits = toSafeInt(row.unbacked_credits);
|
|
3217
2872
|
liability.depositSats += sats;
|
|
3218
2873
|
liability.credits += credits;
|
|
3219
|
-
liability.uncoveredCredits +=
|
|
3220
|
-
liability.
|
|
3221
|
-
|
|
3222
|
-
|
|
2874
|
+
liability.uncoveredCredits += uncoveredCredits;
|
|
2875
|
+
liability.unbackedCredits += unbackedCredits;
|
|
2876
|
+
if (uncoveredCredits > 0) {
|
|
2877
|
+
const currency2 = uncoveredByCurrency.get(row.currency) ?? {
|
|
2878
|
+
currency: row.currency,
|
|
2879
|
+
micro: 0,
|
|
2880
|
+
credits: 0
|
|
2881
|
+
};
|
|
2882
|
+
currency2.micro += toSafeInt(row.uncovered_micro);
|
|
2883
|
+
currency2.credits += uncoveredCredits;
|
|
2884
|
+
uncoveredByCurrency.set(row.currency, currency2);
|
|
2885
|
+
}
|
|
2886
|
+
if (unbackedCredits > 0) {
|
|
2887
|
+
const currency2 = unbackedByCurrency.get(row.currency) ?? {
|
|
2888
|
+
currency: row.currency,
|
|
2889
|
+
micro: 0,
|
|
2890
|
+
credits: 0
|
|
2891
|
+
};
|
|
2892
|
+
currency2.micro += toSafeInt(row.unbacked_micro);
|
|
2893
|
+
currency2.credits += unbackedCredits;
|
|
2894
|
+
unbackedByCurrency.set(row.currency, currency2);
|
|
2895
|
+
}
|
|
3223
2896
|
const rail = byRail.get(row.rail) ?? { rail: row.rail, sats: 0, credits: 0 };
|
|
3224
2897
|
rail.sats += sats;
|
|
3225
2898
|
rail.credits += credits;
|
|
@@ -3235,6 +2908,8 @@ var CreditLedger = class {
|
|
|
3235
2908
|
}
|
|
3236
2909
|
liability.byRail = [...byRail.values()];
|
|
3237
2910
|
liability.byCurrency = [...byCurrency.values()];
|
|
2911
|
+
liability.uncoveredByCurrency = [...uncoveredByCurrency.values()];
|
|
2912
|
+
liability.unbackedByCurrency = [...unbackedByCurrency.values()];
|
|
3238
2913
|
return liability;
|
|
3239
2914
|
}
|
|
3240
2915
|
/**
|
|
@@ -3254,34 +2929,10 @@ var CreditLedger = class {
|
|
|
3254
2929
|
return rows.map(drainRecordFromJoinRow);
|
|
3255
2930
|
}
|
|
3256
2931
|
/**
|
|
3257
|
-
*
|
|
3258
|
-
*
|
|
3259
|
-
*
|
|
3260
|
-
*
|
|
3261
|
-
* never self-clears, so a caller that re-issues the same first page would
|
|
3262
|
-
* never see past a `limit`-sized wall of them. The keyset is
|
|
3263
|
-
* `(created_at, credit_id, drain_id)` — `created_at` is immutable, so no
|
|
3264
|
-
* concurrent write can push an unexamined row past the cursor. It is a
|
|
3265
|
-
* BIGINT of epoch milliseconds rather than a `TIMESTAMPTZ`, so the
|
|
3266
|
-
* microsecond-vs-millisecond cursor trap that bit `listSettlements` cannot
|
|
3267
|
-
* arise here: the value round-trips as the integer it already is.
|
|
3268
|
-
*
|
|
3269
|
-
* `createdBeforeMs` is the staleness floor. A healthy cooperative close
|
|
3270
|
-
* moves the drain to `sent` inside the request that opened it, so anything
|
|
3271
|
-
* younger is in flight rather than stuck.
|
|
3272
|
-
*
|
|
3273
|
-
* The method filter is `'tempo'`, and it is the one line here worth a second
|
|
3274
|
-
* look: `init()` migrates the pre-internal-review `'mpp'` spelling away and
|
|
3275
|
-
* `DrainMethod` no longer carries it, so a query naming it matches nothing a
|
|
3276
|
-
* DVM has ever written. It named it anyway until internal-review's rename sweep and
|
|
3277
|
-
* internal-review's live rung caught it independently — the whole operator queue
|
|
3278
|
-
* read empty in production while every route test stayed green, because
|
|
3279
|
-
* those run on `MemoryCreditLedger`, which filters on the typed value. The
|
|
3280
|
-
* two implementations of this one predicate must be read together.
|
|
3281
|
-
*
|
|
3282
|
-
* A written-off row is excluded unless `includeResolved`: the operator has
|
|
3283
|
-
* already decided about it, and leaving it on the live queue would re-serve
|
|
3284
|
-
* a closed decision on every walk.
|
|
2932
|
+
* Page pending Tempo-channel drains by (created_at, credit_id, drain_id), optionally
|
|
2933
|
+
* age-bounded, excluding written-off rows. Accept the legacy mpp stored method as well as
|
|
2934
|
+
* tempo so pre-migration drains remain repairable. Use the compound key for both ordering
|
|
2935
|
+
* and continuation.
|
|
3285
2936
|
*/
|
|
3286
2937
|
async listChannelDrains(args) {
|
|
3287
2938
|
const params = [args.createdBeforeMs, args.limit];
|
|
@@ -3307,7 +2958,7 @@ var CreditLedger = class {
|
|
|
3307
2958
|
}
|
|
3308
2959
|
/**
|
|
3309
2960
|
* Record that an operator reviewed a floor-refused channel drain and is not
|
|
3310
|
-
* booking it
|
|
2961
|
+
* booking it. Books nothing anywhere — no status change, no
|
|
3311
2962
|
* balance movement, no platform report.
|
|
3312
2963
|
*
|
|
3313
2964
|
* A single-statement CAS on `written_off_at IS NULL`, so the first note on
|
|
@@ -3450,7 +3101,7 @@ var CreditLedger = class {
|
|
|
3450
3101
|
* loss, so the caller's re-poll keeps returning the same token either way.
|
|
3451
3102
|
*
|
|
3452
3103
|
* Returns `replayed` because this is a **terminal** transition and the money
|
|
3453
|
-
* has left at exactly one of these calls
|
|
3104
|
+
* has left at exactly one of these calls: the platform drain
|
|
3454
3105
|
* report must be emitted by that caller and no other. Pass `tx` to write the
|
|
3455
3106
|
* report through the same transaction as the CAS.
|
|
3456
3107
|
*/
|
|
@@ -3497,11 +3148,9 @@ var CreditLedger = class {
|
|
|
3497
3148
|
});
|
|
3498
3149
|
}
|
|
3499
3150
|
/**
|
|
3500
|
-
* Append
|
|
3501
|
-
*
|
|
3502
|
-
*
|
|
3503
|
-
* transition is the source of truth; the receipt is the proof the caller
|
|
3504
|
-
* can carry away).
|
|
3151
|
+
* Append an opaque supplied receipt observation to the drain’s JSONB evidence array.
|
|
3152
|
+
* Issuance and authentication belong to callers/consumers; this store does not verify
|
|
3153
|
+
* signatures. Preserve append order on replay/poll reads.
|
|
3505
3154
|
*/
|
|
3506
3155
|
async appendDrainReceipt(args) {
|
|
3507
3156
|
const q = args.tx ?? this.pool;
|
|
@@ -3511,7 +3160,6 @@ var CreditLedger = class {
|
|
|
3511
3160
|
[args.creditId, args.drainId, JSON.stringify([args.receipt])]
|
|
3512
3161
|
);
|
|
3513
3162
|
}
|
|
3514
|
-
// ── Internals ─────────────────────────────────────────────────────────
|
|
3515
3163
|
async readDrain(q, creditId, drainId) {
|
|
3516
3164
|
const { rows } = await q.query(
|
|
3517
3165
|
`SELECT * FROM credit_drains WHERE credit_id = $1 AND drain_id = $2`,
|
|
@@ -3519,7 +3167,6 @@ var CreditLedger = class {
|
|
|
3519
3167
|
);
|
|
3520
3168
|
return rows[0];
|
|
3521
3169
|
}
|
|
3522
|
-
/** Re-read the credit join fields for a freshly-updated drain row. */
|
|
3523
3170
|
async rejoin(q, row) {
|
|
3524
3171
|
const { rows } = await q.query(
|
|
3525
3172
|
`SELECT currency, caller_pubkey, tempo_channel_id, x402_channel_id
|
|
@@ -3528,7 +3175,6 @@ var CreditLedger = class {
|
|
|
3528
3175
|
);
|
|
3529
3176
|
return drainRecordFromRow(row, rows[0]);
|
|
3530
3177
|
}
|
|
3531
|
-
/** Shared refusal tail for the CAS transitions: idempotent replay or typed error. */
|
|
3532
3178
|
async refuseTransition(q, creditId, drainId, opts) {
|
|
3533
3179
|
const row = await this.readDrain(q, creditId, drainId);
|
|
3534
3180
|
if (!row) {
|
|
@@ -3544,26 +3190,10 @@ var CreditLedger = class {
|
|
|
3544
3190
|
);
|
|
3545
3191
|
}
|
|
3546
3192
|
/**
|
|
3547
|
-
*
|
|
3548
|
-
*
|
|
3549
|
-
*
|
|
3550
|
-
*
|
|
3551
|
-
* lots have to shrink with it or the deposit-liability rollup would keep
|
|
3552
|
-
* counting a balance that is gone.
|
|
3553
|
-
*
|
|
3554
|
-
* `owedSats` is `null` where {@link isInKindDepletion} refuses — the lots
|
|
3555
|
-
* are short of the balance, or the covering lots carry no sats basis (a
|
|
3556
|
-
* credit funded before internal-review). That row is priced off a live rate by the
|
|
3557
|
-
* admin surface instead, which is what *every* row did before this change,
|
|
3558
|
-
* so the fallback is the old behaviour rather than a new failure mode.
|
|
3559
|
-
*
|
|
3560
|
-
* What it publishes is **net of the delivery reserve** (internal-review): handing a
|
|
3561
|
-
* refund over costs a mint fee and, when the notes have to be minted just in
|
|
3562
|
-
* time, a Lightning hop. The caller carries that cost per internal-review, and a
|
|
3563
|
-
* flat reserve is how they carry it without the figure moving — an
|
|
3564
|
-
* actual-fee true-up could only be applied after the promise was made. The
|
|
3565
|
-
* debits stay gross because they are denominated in micro and are what
|
|
3566
|
-
* `releaseDrain` restores; only the sats figure is netted.
|
|
3193
|
+
* Deplete FIFO lots even when they cannot price the reclaim; stale open lots must not
|
|
3194
|
+
* remain behind a debited balance. Publish owedSats only with complete positive sats
|
|
3195
|
+
* coverage, net of one delivery reserve. Otherwise preserve null for a live-rate fallback.
|
|
3196
|
+
* Returned debits allow an unpaid drain release to restore the exact lots.
|
|
3567
3197
|
*/
|
|
3568
3198
|
async reclaimInKind(q, creditId, amountMicro) {
|
|
3569
3199
|
const depletion = depleteLots(await this.openLots(q, creditId), amountMicro);
|
|
@@ -3575,7 +3205,7 @@ var CreditLedger = class {
|
|
|
3575
3205
|
};
|
|
3576
3206
|
}
|
|
3577
3207
|
/**
|
|
3578
|
-
* This credit's undepleted funding lots, oldest first
|
|
3208
|
+
* This credit's undepleted funding lots, oldest first.
|
|
3579
3209
|
*
|
|
3580
3210
|
* Ordered in SQL on the same `(created_at, lot_id)` key the partial index
|
|
3581
3211
|
* carries, so FIFO is the index's own order rather than something a reader
|
|
@@ -3608,7 +3238,6 @@ var CreditLedger = class {
|
|
|
3608
3238
|
);
|
|
3609
3239
|
}
|
|
3610
3240
|
}
|
|
3611
|
-
/** Shared settle/release state machine — see the public JSDoc on each verb. */
|
|
3612
3241
|
async resolveDraw(args) {
|
|
3613
3242
|
if (args.tx) {
|
|
3614
3243
|
const credit = await this.lockCreditRow(args.tx, args.creditId);
|
|
@@ -3718,7 +3347,6 @@ var CreditLedger = class {
|
|
|
3718
3347
|
client.release();
|
|
3719
3348
|
}
|
|
3720
3349
|
}
|
|
3721
|
-
/** `SELECT … FOR UPDATE` the credit row on `q` (which must be inside a transaction). */
|
|
3722
3350
|
async lockCreditRow(q, creditId) {
|
|
3723
3351
|
const { rows } = await q.query(
|
|
3724
3352
|
`SELECT * FROM credits WHERE credit_id = $1 FOR UPDATE`,
|
|
@@ -3739,7 +3367,7 @@ var CreditLedger = class {
|
|
|
3739
3367
|
/**
|
|
3740
3368
|
* Held-but-unresolved totals for a credit, in all three units at once — the
|
|
3741
3369
|
* fiat micro that defines available balance plus the rail-value remainders
|
|
3742
|
-
* a new draw allocates against
|
|
3370
|
+
* a new draw allocates against. One round-trip, since every
|
|
3743
3371
|
* caller needs the micro sum anyway.
|
|
3744
3372
|
*/
|
|
3745
3373
|
async pendingSums(q, creditId) {
|
|
@@ -3814,7 +3442,7 @@ function isFundingRail(rail) {
|
|
|
3814
3442
|
}
|
|
3815
3443
|
function assertFundingBasis(basis) {
|
|
3816
3444
|
if (!basis) {
|
|
3817
|
-
throw new CreditLedgerError("invalid_basis", "basis is required
|
|
3445
|
+
throw new CreditLedgerError("invalid_basis", "basis is required");
|
|
3818
3446
|
}
|
|
3819
3447
|
if (!isFundingRail(basis.rail)) {
|
|
3820
3448
|
throw new CreditLedgerError(
|