@dvmkit/sdk 0.2.0-rc.9 → 0.3.0-rc.10
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 +2 -0
- package/dist/{chunk-V7EVFLAK.js → chunk-2D75MNNG.js} +154 -523
- package/dist/{chunk-BIFLRKMO.js → chunk-2KGG5ORW.js} +1 -1
- package/dist/{chunk-PCUQZDZA.js → chunk-4VHTKNOR.js} +635 -321
- package/dist/{chunk-5URG56JJ.js → chunk-A4CM2BD3.js} +18 -2
- package/dist/{chunk-FT6HTUM4.js → chunk-AJNFXR7J.js} +1 -1
- package/dist/{chunk-S3XAHZQY.js → chunk-COYKJRMM.js} +24 -3
- package/dist/{chunk-UP2F5RRT.js → chunk-D7YQYLLM.js} +1 -4
- package/dist/{chunk-OMIQMMME.js → chunk-FPOLJRBF.js} +3 -3
- package/dist/{chunk-QP53RWAD.js → chunk-HHTL3ZFT.js} +9 -6
- package/dist/{chunk-JDT5LCJC.js → chunk-MD3H3BF7.js} +1 -1
- package/dist/{chunk-KSQEFVFJ.js → chunk-MMCE7XSR.js} +1 -1
- package/dist/{chunk-MRAGS5VP.js → chunk-N5B66UOS.js} +4 -16
- package/dist/{chunk-2ATUAUAO.js → chunk-O6D72KYA.js} +2 -2
- package/dist/{chunk-KXCKFIJI.js → chunk-QX52F6IB.js} +1 -1
- package/dist/{chunk-PHHAYRQV.js → chunk-U5VNEALI.js} +23 -10
- package/dist/{chunk-JLXYOV4Y.js → chunk-V5ZRATJH.js} +1 -1
- package/dist/{chunk-6GRIKOFB.js → chunk-VA7GT6C6.js} +5 -16
- package/dist/{chunk-O2X2CCKH.js → chunk-VIVXB4CD.js} +1 -1
- package/dist/{chunk-L67WTZX2.js → chunk-XMDQOXHO.js} +10 -33
- package/dist/{chunk-QT4ONTST.js → chunk-XRLKGCAW.js} +1 -1
- package/dist/{chunk-KMZXTBLA.js → chunk-XZ7LW4S5.js} +0 -1
- package/dist/{chunk-XQXJKJ3P.js → chunk-YO56VCTQ.js} +0 -4
- package/dist/{credit-ledger-5ZEJRI46.js → credit-ledger-MHA652TV.js} +2 -2
- package/dist/{credit-menu-D4Gcdgc4.d.ts → credit-menu-0-UtJ3hh.d.ts} +179 -533
- package/dist/{fx-B0SLBe5x.d.ts → fx-DhyT1Cjh.d.ts} +1 -1
- package/dist/index.d.ts +5 -17
- package/dist/internal/caller.d.ts +109 -698
- package/dist/internal/caller.js +10 -12
- package/dist/internal/server.d.ts +17 -42
- package/dist/internal/server.js +16 -15
- package/dist/{job-store-B2uZvga4.d.ts → job-store-D3CcZp_g.d.ts} +6 -10
- package/dist/{lightning-backend-CQBnQgsT.d.ts → lightning-backend-BhsyrBLa.d.ts} +1 -15
- package/dist/{memory-credit-ledger-ZOH6C3N4.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-IOJBF7DB.js → mpp-setup-MPP3ST5X.js} +1 -1
- package/dist/{payout-reporter-5PIRYFVQ.js → payout-reporter-KEMUXEUO.js} +2 -2
- package/dist/{postgres-consumed-credential-store-ISRHBMOU.js → postgres-consumed-credential-store-6N2V5MJN.js} +1 -1
- package/dist/postgres-job-store-VE2JRUEC.js +7 -0
- package/dist/postgres-kv-store-DXGTKXLU.js +7 -0
- package/dist/{postgres-replay-store-IZFLTTAC.js → postgres-replay-store-ZF4JNZOW.js} +2 -2
- package/dist/{processed-payment-store-FIDI3RNH.js → processed-payment-store-GKMAWOM3.js} +2 -2
- package/dist/revenue-reporter-O75BGYFK.js +7 -0
- package/dist/server/index.d.ts +37 -140
- package/dist/server/index.js +64 -127
- package/dist/{step-cache-CXg7ziML.d.ts → step-cache-v2yhyhSZ.d.ts} +359 -1112
- package/dist/{tempo-charge-store-76TDAF34.js → tempo-charge-store-SDTUNTKA.js} +2 -2
- package/dist/{tempo-lifecycle-DXM7QXJQ.js → tempo-lifecycle-G5SB4HFC.js} +4 -4
- package/dist/{tempo-session-store-2JNOKJGX.js → tempo-session-store-WF3VZXNF.js} +2 -2
- package/dist/{tempo-wallet-O67H5M4N.js → tempo-wallet-ZFNUDFVO.js} +1 -1
- package/dist/testing/index.d.ts +2 -2
- package/dist/testing/index.js +2 -2
- package/dist/{usd-BNDg1715.d.ts → usd-Cu9G6xv2.d.ts} +1 -1
- package/dist/{x402-7S2EFINY.js → x402-CI6PSHNO.js} +4 -4
- package/package.json +2 -2
- package/dist/postgres-job-store-OGQ6IT4U.js +0 -7
- package/dist/postgres-kv-store-D5E2EZ24.js +0 -7
- package/dist/revenue-reporter-NNCNRY4C.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 () => {
|
|
@@ -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(
|
|
@@ -1003,39 +1001,23 @@ var CreditLedger = class {
|
|
|
1003
1001
|
x402Settlements;
|
|
1004
1002
|
expiryReleaseOutbox;
|
|
1005
1003
|
/**
|
|
1006
|
-
*
|
|
1007
|
-
*
|
|
1008
|
-
* Late, because the settlement store is built with the batch-settlement
|
|
1009
|
-
* server, which is built *after* the ledger it reads earned draws from. An
|
|
1010
|
-
* unbound ledger simply does not gate — correct for a DVM with no x402
|
|
1011
|
-
* 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.
|
|
1012
1006
|
*/
|
|
1013
1007
|
useX402SettlementGate(gate) {
|
|
1014
1008
|
this.x402Settlements = gate;
|
|
1015
1009
|
}
|
|
1016
1010
|
/**
|
|
1017
|
-
*
|
|
1018
|
-
*
|
|
1019
|
-
*
|
|
1020
|
-
* Held on the ledger rather than threaded through {@link fund} for the same
|
|
1021
|
-
* reason as the gate above: the revival half fires from *every* funding path
|
|
1022
|
-
* Cashu commit, x402 exact and channel, Tempo, the Lightning invoice
|
|
1023
|
-
* settle, the implicit N=1 per-call payment — and a seam each of those has to
|
|
1024
|
-
* remember to pass is a seam one of them will eventually forget.
|
|
1025
|
-
*
|
|
1026
|
-
* Unbound (a `dvmctl dev` server, a self-hosted builder, a test host) the
|
|
1027
|
-
* sweep and the reversal still run and still write the ledger's own rows;
|
|
1028
|
-
* only the platform report is skipped. Reporting is bookkeeping and must
|
|
1029
|
-
* 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.
|
|
1030
1014
|
*/
|
|
1031
1015
|
useCreditExpiryReleaseOutbox(outbox) {
|
|
1032
1016
|
this.expiryReleaseOutbox = outbox;
|
|
1033
1017
|
}
|
|
1034
|
-
/** Create the `credits` / `credit_draws` tables if absent. Call once at SDK boot. */
|
|
1035
1018
|
async init() {
|
|
1036
1019
|
await withSdkInitLock(this.pool, () => this.createTables());
|
|
1037
1020
|
}
|
|
1038
|
-
/** The boot DDL itself — always runs under {@link withSdkInitLock}. */
|
|
1039
1021
|
async createTables() {
|
|
1040
1022
|
await this.pool.query(`
|
|
1041
1023
|
CREATE TABLE IF NOT EXISTS credits (
|
|
@@ -1313,25 +1295,11 @@ var CreditLedger = class {
|
|
|
1313
1295
|
);
|
|
1314
1296
|
}
|
|
1315
1297
|
/**
|
|
1316
|
-
*
|
|
1317
|
-
*
|
|
1318
|
-
*
|
|
1319
|
-
*
|
|
1320
|
-
*
|
|
1321
|
-
*
|
|
1322
|
-
* The lot is derived from `credits.msats_remaining`, not replayed out of
|
|
1323
|
-
* `credit_fundings`. That is not a shortcut: `credit_fundings` records only
|
|
1324
|
-
* `{amount_micro, rail}` — no instrument amount at all — and is not written
|
|
1325
|
-
* on every funding path, whereas `msats_remaining` **is** the credit's
|
|
1326
|
-
* current unspent rail value, maintained pro rata on every draw since
|
|
1327
|
-
* One synthetic lot pairing it with `balance_micro` therefore
|
|
1328
|
-
* reproduces today's in-kind position exactly, and FIFO over a single lot is
|
|
1329
|
-
* trivially correct. Historic per-funding rates are unrecoverable and would
|
|
1330
|
-
* change nothing: only the remaining position is owed.
|
|
1331
|
-
*
|
|
1332
|
-
* The semantics change is deliberately retroactive — every
|
|
1333
|
-
* current holder is first-party, so there is one regime and no legacy
|
|
1334
|
-
* 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.
|
|
1335
1303
|
*/
|
|
1336
1304
|
async backfillFundingLots() {
|
|
1337
1305
|
const { rowCount } = await this.pool.query(
|
|
@@ -1367,60 +1335,19 @@ var CreditLedger = class {
|
|
|
1367
1335
|
);
|
|
1368
1336
|
}
|
|
1369
1337
|
/**
|
|
1370
|
-
*
|
|
1371
|
-
*
|
|
1372
|
-
*
|
|
1373
|
-
*
|
|
1374
|
-
*
|
|
1375
|
-
*
|
|
1376
|
-
*
|
|
1377
|
-
*
|
|
1378
|
-
*
|
|
1379
|
-
*
|
|
1380
|
-
*
|
|
1381
|
-
*
|
|
1382
|
-
*
|
|
1383
|
-
* single statement: it upserts the credit, records its funding lot
|
|
1384
|
-
* and reverses any standing expiry release — and that
|
|
1385
|
-
* last leg restores balance and queues a report. A crash between the upsert
|
|
1386
|
-
* and the reversal would leave a revived credit whose release still stands,
|
|
1387
|
-
* which the sweep's own exclusion then makes permanent: `balance_micro > 0`
|
|
1388
|
-
* but a standing release means it is neither drainable nor re-releasable.
|
|
1389
|
-
*
|
|
1390
|
-
* `basis` records the rail value behind the fiat so each draw can
|
|
1391
|
-
* be allocated its share of the rail-native amount actually received. A
|
|
1392
|
-
* top-up on a different rail than the credit's is refused with
|
|
1393
|
-
* `rail_mismatch`: sats and USDC microunits aren't summable, so a blended
|
|
1394
|
-
* credit would have no coherent native basis to allocate from.
|
|
1395
|
-
*
|
|
1396
|
-
* It is **required**, in the type and again at runtime via
|
|
1397
|
-
* {@link assertFundingBasis}. A basis-less fund wrote `rail = NULL`, and a
|
|
1398
|
-
* draw against such a credit settles — a real debit — while
|
|
1399
|
-
* `railAsFundingMethod` correctly declines to guess a rail, so `bookRevenue`
|
|
1400
|
-
* books nothing and outstanding liability (`deposits − draw revenue`) stays
|
|
1401
|
-
* overstated by the drawn amount forever. `credits.rail` stays nullable
|
|
1402
|
-
* because rows written before this change still exist in live databases:
|
|
1403
|
-
* **write is closed, read is not.** A legacy row adopts a rail from its first
|
|
1404
|
-
* typed top-up via the `COALESCE` below.
|
|
1405
|
-
*
|
|
1406
|
-
* The rail-native **instrument** is pinned at first funding too, and both
|
|
1407
|
-
* channel guards are symmetric for the same reason. A reusable
|
|
1408
|
-
* channel — Tempo session or x402 batch-settlement — may only ever top up
|
|
1409
|
-
* the credit it opened, and a credit opened by a one-shot (a Tempo charge, an
|
|
1410
|
-
* x402 exact authorization) may never adopt one. The x402 half used to admit
|
|
1411
|
-
* `credits.x402_channel_id IS NULL`, so a channel deposit bound itself to a
|
|
1412
|
-
* credit already holding exact-funded value: `earnedNativeForX402Channel`
|
|
1413
|
-
* then read that credit's whole settled-draw history as the channel's
|
|
1414
|
-
* earnings, and the first claim tick swept a deposit nobody had consumed,
|
|
1415
|
-
* while the unspent balance lost every exit (the drain's refund base goes
|
|
1416
|
-
* below `totalClaimed`, and a channel-bound credit refuses every non-x402
|
|
1417
|
-
* drain). `credits_x402_channel_uidx` does not cover this: it guards
|
|
1418
|
-
* one-credit-per-channel, not one-source-per-credit.
|
|
1419
|
-
*
|
|
1420
|
-
* An x402 channel deposit is additionally refused `settlement_pending` while
|
|
1421
|
-
* that channel carries an unresolved refund — see the gate read
|
|
1422
|
-
* below. This method is the authority on that rule, as it is on the binding
|
|
1423
|
-
* 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.
|
|
1424
1351
|
*/
|
|
1425
1352
|
async fund(args) {
|
|
1426
1353
|
assertAmount(args.amountMicro, { min: 1 });
|
|
@@ -1448,11 +1375,6 @@ var CreditLedger = class {
|
|
|
1448
1375
|
client.release();
|
|
1449
1376
|
}
|
|
1450
1377
|
}
|
|
1451
|
-
/**
|
|
1452
|
-
* The funding itself, on whichever handle {@link fund} chose. Every refusal
|
|
1453
|
-
* it raises is the caller's to see unchanged; a self-opened transaction rolls
|
|
1454
|
-
* back around it.
|
|
1455
|
-
*/
|
|
1456
1378
|
async fundOn(q, args, b, nowMs, creditId) {
|
|
1457
1379
|
const wedged = await this.blockingX402Refund(
|
|
1458
1380
|
b.x402Channel?.channelId,
|
|
@@ -1590,33 +1512,12 @@ var CreditLedger = class {
|
|
|
1590
1512
|
return this.snapshotFromRow(revived ?? rows[0], q, nowMs);
|
|
1591
1513
|
}
|
|
1592
1514
|
/**
|
|
1593
|
-
*
|
|
1594
|
-
*
|
|
1595
|
-
* 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.
|
|
1596
1518
|
*
|
|
1597
|
-
*
|
|
1598
|
-
*
|
|
1599
|
-
* overwrites `expiry_ms` (the caller-ownership contract carry-forward: the credit is a rolling
|
|
1600
|
-
* buffer), so the moment money arrives the recorded remainder is no longer
|
|
1601
|
-
* final: the release stops counting and its micro come back.
|
|
1602
|
-
*
|
|
1603
|
-
* Deliberately unconditional on the incoming expiry rather than gated on
|
|
1604
|
-
* `expiryMs > nowMs`. A release exists only for a credit that had already
|
|
1605
|
-
* lapsed, so any funding that reaches one is money the release did not
|
|
1606
|
-
* account for — and a funding that arrives with an already-past expiry leaves
|
|
1607
|
-
* a credit the very next sweep re-releases at its new, larger remainder. One
|
|
1608
|
-
* rule, self-healing in both directions, and it never needs to read the
|
|
1609
|
-
* pre-funding expiry the upsert has already overwritten.
|
|
1610
|
-
*
|
|
1611
|
-
* Runs on the funding's own handle — which {@link fund} now guarantees is a
|
|
1612
|
-
* transaction — so the reversal, the restored balance and the report commit
|
|
1613
|
-
* with the money or not at all. A reversal that committed without its report
|
|
1614
|
-
* would be unrecoverable: `WHERE reversed_at IS NULL` means no later pass
|
|
1615
|
-
* re-derives it.
|
|
1616
|
-
*
|
|
1617
|
-
* @returns the credit row as the restore left it, or undefined when there was
|
|
1618
|
-
* nothing to reverse. The caller reads its snapshot off this rather than off
|
|
1619
|
-
* 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.
|
|
1620
1521
|
*/
|
|
1621
1522
|
async reverseExpiryReleases(q, creditId, nowMs) {
|
|
1622
1523
|
const { rows } = await q.query(
|
|
@@ -1641,37 +1542,15 @@ var CreditLedger = class {
|
|
|
1641
1542
|
return restored[0];
|
|
1642
1543
|
}
|
|
1643
1544
|
/**
|
|
1644
|
-
* Release
|
|
1645
|
-
*
|
|
1646
|
-
*
|
|
1647
|
-
*
|
|
1648
|
-
*
|
|
1649
|
-
*
|
|
1650
|
-
*
|
|
1651
|
-
*
|
|
1652
|
-
*
|
|
1653
|
-
* `tempo` rail). A Tempo credit is channel-backed and its undrawn value
|
|
1654
|
-
* returns to the *caller* at channel exit, so releasing it would book the
|
|
1655
|
-
* builder money the chain is about to hand back.
|
|
1656
|
-
*
|
|
1657
|
-
* **A credit with a pending hold is skipped, not partially released.** A draw
|
|
1658
|
-
* placed before expiry stays settleable afterwards (`draw`'s replay lookup
|
|
1659
|
-
* runs above the expiry check, so a lost response is still recoverable), so
|
|
1660
|
-
* the remainder is not final while a hold is outstanding. Skipping costs one
|
|
1661
|
-
* sweep interval and keeps the released figure exactly "what nothing bought";
|
|
1662
|
-
* holds do resolve — the orphan-draw watchdog is what guarantees
|
|
1663
|
-
* a stranded one still reaches a terminal state.
|
|
1664
|
-
*
|
|
1665
|
-
* **Idempotent** two ways. `release_id` is derived from the expiry instant,
|
|
1666
|
-
* so a re-sweep after a crash between the insert and its report collides on
|
|
1667
|
-
* the same primary key instead of writing a second row; and the candidate
|
|
1668
|
-
* scan excludes any credit that already has an un-reversed release.
|
|
1669
|
-
*
|
|
1670
|
-
* Each candidate is re-checked under the credit row lock every
|
|
1671
|
-
* `credit_draws` write is taken under, so a draw racing the sweep either
|
|
1672
|
-
* loses the race (its credit is already released and it would have been
|
|
1673
|
-
* refused `credit_expired` anyway) or wins it and leaves a pending hold the
|
|
1674
|
-
* 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.
|
|
1675
1554
|
*/
|
|
1676
1555
|
async sweepExpiredCredits(args) {
|
|
1677
1556
|
const nowMs = args?.nowMs ?? Date.now();
|
|
@@ -1707,30 +1586,12 @@ var CreditLedger = class {
|
|
|
1707
1586
|
return { released };
|
|
1708
1587
|
}
|
|
1709
1588
|
/**
|
|
1710
|
-
*
|
|
1711
|
-
*
|
|
1712
|
-
*
|
|
1713
|
-
* against, or already released between the scan and the lock.
|
|
1714
|
-
*
|
|
1715
|
-
* **Zeroing the balance is what makes the release real**, and it is the whole
|
|
1716
|
-
* reason this runs under the lock rather than as a bare INSERT. The row alone
|
|
1717
|
-
* records that the caller's claim ended; it does not *end* it. `requestDrain`
|
|
1718
|
-
* has no expiry check by design (expiry gates new draws, not reclaims), so a
|
|
1719
|
-
* credit whose remainder had been released and reported was still fully
|
|
1720
|
-
* drainable: the DVM would pay out money the platform had already booked as
|
|
1721
|
-
* the builder's, and `deposited - drawn - drained - released` would go
|
|
1722
|
-
* negative on the same micro. With the balance at zero, `requestDrain`'s
|
|
1723
|
-
* existing `availableMicro <= 0` guard refuses with `nothing_to_drain` and no
|
|
1724
|
-
* new check is needed anywhere. {@link reverseExpiryReleases} puts the
|
|
1725
|
-
* 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.
|
|
1726
1592
|
*
|
|
1727
|
-
*
|
|
1728
|
-
*
|
|
1729
|
-
*
|
|
1730
|
-
* The zeroed balance also takes the credit out of `BITCOIN_CREDIT_PREDICATE`
|
|
1731
|
-
* (`balance_micro > 0`), so the in-kind sweep floor stops counting sats the
|
|
1732
|
-
* builder no longer owes back — and counts them again after a revival, with
|
|
1733
|
-
* 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.
|
|
1734
1595
|
*/
|
|
1735
1596
|
async releaseExpiredCreditLocked(client, credit, nowMs) {
|
|
1736
1597
|
const expiryMs = toSafeInt(credit.expiry_ms);
|
|
@@ -1778,34 +1639,12 @@ var CreditLedger = class {
|
|
|
1778
1639
|
return row;
|
|
1779
1640
|
}
|
|
1780
1641
|
/**
|
|
1781
|
-
* Record
|
|
1782
|
-
*
|
|
1783
|
-
*
|
|
1784
|
-
* whatever transaction the rail opened — `withCommitTx` for Cashu,
|
|
1785
|
-
* `ProcessedPaymentStore.withTx` for x402/Tempo, `settleInvoice`'s own
|
|
1786
|
-
* `BEGIN` for Lightning. The funding-atomicity invariant therefore extends
|
|
1787
|
-
* to it structurally rather than by every call site remembering: a lot
|
|
1788
|
-
* cannot exist without its ledger credit, or the credit without its lot.
|
|
1789
|
-
*
|
|
1790
|
-
* Only a **channel-backed** credit writes no lot: it is in-kind already —
|
|
1791
|
-
* the escrow itself retains the unused value — so a lot on one would be
|
|
1792
|
-
* counted as Bitcoin deposit liability the hub does not hold.
|
|
1793
|
-
*
|
|
1794
|
-
* Every other non-channel Bitcoin funding writes one, the implicit N=1
|
|
1795
|
-
* per-call payment included. That one funds and draws the same amount in
|
|
1796
|
-
* this transaction, so it looks like a lot born depleted — but the draw is a
|
|
1797
|
-
* *pending hold*, not a debit, and only a `completed` job settles it
|
|
1798
|
-
* (`JobManager.settlesDraw`). A failed, cancelled or stale-swept job
|
|
1799
|
-
* releases the hold and the whole funded amount becomes reclaimable balance,
|
|
1800
|
-
* so suppressing the lot here would leave routine traffic reclaiming off a
|
|
1801
|
-
* live rate and missing from the sweep floor. One small insert on the hot
|
|
1802
|
-
* path buys the invariant the liability read depends on: open lots cover
|
|
1803
|
-
* `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.
|
|
1804
1645
|
*
|
|
1805
|
-
* A zero-sats basis still
|
|
1806
|
-
*
|
|
1807
|
-
* distinguishable state from "no lot at all" for the reclaim and the
|
|
1808
|
-
* 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.
|
|
1809
1648
|
*/
|
|
1810
1649
|
async recordFundingLot(q, creditId, amountMicro, basis, nowMs) {
|
|
1811
1650
|
if (!isNonChannelBitcoinRail(basis.rail)) return;
|
|
@@ -1826,25 +1665,13 @@ var CreditLedger = class {
|
|
|
1826
1665
|
);
|
|
1827
1666
|
}
|
|
1828
1667
|
/**
|
|
1829
|
-
* Place a
|
|
1830
|
-
*
|
|
1831
|
-
*
|
|
1832
|
-
* `replayed: true` and places no second hold — this is how a caller recovers
|
|
1833
|
-
* a lost response (spec condition 1). A replay whose `amountMicro` or
|
|
1834
|
-
* `jobId` differs from the recorded draw is not a replay but a money bug in
|
|
1835
|
-
* 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.
|
|
1836
1671
|
*
|
|
1837
|
-
*
|
|
1838
|
-
*
|
|
1839
|
-
*
|
|
1840
|
-
* expired credit are refused with `credit_expired`; the balance stays
|
|
1841
|
-
* intact and readable (the caller-ownership contract: expiry ends spending, never ownership).
|
|
1842
|
-
*
|
|
1843
|
-
* Pass `tx` (a client inside a caller-owned `BEGIN`) to place the hold in
|
|
1844
|
-
* the same transaction as the rail commit and the `fund` upsert (spec
|
|
1845
|
-
* condition 3 — the implicit N=1 path). The ledger issues no
|
|
1846
|
-
* transaction control on `tx`; the `SELECT … FOR UPDATE` row lock is still
|
|
1847
|
-
* 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.
|
|
1848
1675
|
*/
|
|
1849
1676
|
async draw(args) {
|
|
1850
1677
|
assertAmount(args.amountMicro, { min: 0 });
|
|
@@ -1960,66 +1787,26 @@ var CreditLedger = class {
|
|
|
1960
1787
|
return this.withCreditLock(args.creditId, (client, credit) => run(client, credit));
|
|
1961
1788
|
}
|
|
1962
1789
|
/**
|
|
1963
|
-
*
|
|
1964
|
-
*
|
|
1965
|
-
*
|
|
1966
|
-
* `undefined` whenever there is nothing to ask — an unbound credit, or a DVM
|
|
1967
|
-
* with no durable settlement store, where a settlement row cannot exist.
|
|
1968
|
-
*
|
|
1969
|
-
* Public since the behavior was introduced, so `/v1/credit`'s pre-payment preflight can ask the
|
|
1970
|
-
* same question `fund` will ask from inside the rail transaction — one read,
|
|
1971
|
-
* 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.
|
|
1972
1793
|
*/
|
|
1973
1794
|
async blockingX402Refund(channelId, statuses) {
|
|
1974
1795
|
if (!channelId || !this.x402Settlements) return void 0;
|
|
1975
1796
|
return this.x402Settlements.pendingRefundSettlement(channelId, statuses);
|
|
1976
1797
|
}
|
|
1977
1798
|
/**
|
|
1978
|
-
* Grow
|
|
1979
|
-
*
|
|
1980
|
-
*
|
|
1981
|
-
* so the receipt's `ReceiptCredit` block countersigns the job's full cost
|
|
1982
|
-
* and `revenue_events` still books exactly one row per job.
|
|
1983
|
-
*
|
|
1984
|
-
* `draw()` cannot express this: a same-`draw_id` call with a different
|
|
1985
|
-
* amount is `draw_conflict` by design (a replayed draw must never re-price).
|
|
1986
|
-
*
|
|
1987
|
-
* The arithmetic is `draw()`'s, unchanged: `addAmountMicro` is checked
|
|
1988
|
-
* against — and `balance_after_micro` recomputed from — the balance net of
|
|
1989
|
-
* **every** pending hold, this draw's own included. That is deliberately the
|
|
1990
|
-
* conservative reading (a fresh draw of `addAmountMicro` would see exactly
|
|
1991
|
-
* the same figure), and it is what keeps the total held after growth inside
|
|
1992
|
-
* the credit's balance.
|
|
1993
|
-
*
|
|
1994
|
-
* The increment's rail value comes from {@link growthRailValue}: earmarked
|
|
1995
|
-
* to the funding that backs it when the caller names one (the mid-job case),
|
|
1996
|
-
* pro-rata otherwise. Either way the draws of a credit keep summing to
|
|
1997
|
-
* precisely what the rails paid.
|
|
1998
|
-
*
|
|
1999
|
-
* **`ledger_seq` is not re-taken.** The per-credit sequence is gap-free
|
|
2000
|
-
* (`credit_draws_seq_uidx`), so moving this draw forward would strand its
|
|
2001
|
-
* original number. The consequence is deliberate and worth knowing: the
|
|
2002
|
-
* recorded `balance_after_micro` is the post-growth figure stamped at the
|
|
2003
|
-
* *original* seq, so a credit with interleaved sibling activity has a
|
|
2004
|
-
* trajectory that is truthful per draw rather than monotonic across seqs.
|
|
2005
|
-
*
|
|
2006
|
-
* **No expiry check**, matching `settle`/`release`: the job was accepted
|
|
2007
|
-
* pre-expiry and is still running. Refusing here would strand the handler
|
|
2008
|
-
* 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.
|
|
2009
1802
|
*
|
|
2010
|
-
*
|
|
2011
|
-
*
|
|
2012
|
-
* the whole transaction back on replay; x402/mpp collide on the
|
|
2013
|
-
* `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.
|
|
2014
1805
|
*
|
|
2015
|
-
*
|
|
2016
|
-
*
|
|
2017
|
-
*
|
|
2018
|
-
*
|
|
2019
|
-
* drawn. Excess is **granted partially or not at all rather than thrown** —
|
|
2020
|
-
* this runs inside the rail commit, so a throw would roll back cashu proofs
|
|
2021
|
-
* the caller already sent. The refused remainder stays funded balance the
|
|
2022
|
-
* 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.
|
|
2023
1810
|
*/
|
|
2024
1811
|
async growDraw(args) {
|
|
2025
1812
|
assertAmount(args.addAmountMicro, { min: 1 });
|
|
@@ -2212,18 +1999,9 @@ var CreditLedger = class {
|
|
|
2212
1999
|
return this.resolveDraw({ ...args, to: "released" });
|
|
2213
2000
|
}
|
|
2214
2001
|
/**
|
|
2215
|
-
* Record
|
|
2216
|
-
*
|
|
2217
|
-
*
|
|
2218
|
-
* loses with a typed `funding_replayed` rather than crediting twice.
|
|
2219
|
-
*
|
|
2220
|
-
* Call it inside the same `tx` as {@link fund} — the rail commit, the
|
|
2221
|
-
* funding record, and the balance increment must land together or not at
|
|
2222
|
-
* all (the funding-commitment rule). Like `fund`, this issues no transaction
|
|
2223
|
-
* control on `tx`; it is a single statement.
|
|
2224
|
-
*
|
|
2225
|
-
* This does **not** move money on its own. `fund` still does the crediting;
|
|
2226
|
-
* 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.
|
|
2227
2005
|
*/
|
|
2228
2006
|
async recordFunding(args) {
|
|
2229
2007
|
assertAmount(args.amountMicro, { min: 1 });
|
|
@@ -2348,7 +2126,6 @@ var CreditLedger = class {
|
|
|
2348
2126
|
}
|
|
2349
2127
|
return existing;
|
|
2350
2128
|
}
|
|
2351
|
-
/** Read one invoice (no lock). */
|
|
2352
2129
|
async getInvoice(args) {
|
|
2353
2130
|
const { rows } = await this.pool.query(
|
|
2354
2131
|
`SELECT * FROM credit_invoices WHERE credit_id = $1 AND fund_id = $2`,
|
|
@@ -2373,22 +2150,10 @@ var CreditLedger = class {
|
|
|
2373
2150
|
return rows.map(invoiceRecordFromRow);
|
|
2374
2151
|
}
|
|
2375
2152
|
/**
|
|
2376
|
-
*
|
|
2377
|
-
*
|
|
2378
|
-
*
|
|
2379
|
-
*
|
|
2380
|
-
* One transaction covers the funding record, the balance, and the invoice's
|
|
2381
|
-
* own status flip, so the three can never disagree. A crash anywhere inside
|
|
2382
|
-
* leaves the invoice `pending` and the balance untouched: the next
|
|
2383
|
-
* `lookup_invoice` observes the same settled payment and credits it, once.
|
|
2384
|
-
* A replayed check after the commit sees `settled` and moves nothing.
|
|
2385
|
-
*
|
|
2386
|
-
* The lock is on the **invoice** row, not the credit row: the first
|
|
2387
|
-
* Lightning top-up is what opens the credit, so there is frequently no
|
|
2388
|
-
* `credits` row to take `FOR UPDATE` yet. That does not weaken the ledger's
|
|
2389
|
-
* concurrency contract — no `credit_draws` write happens here, and `fund`'s
|
|
2390
|
-
* upsert is a single statement — while still serialising two settlement
|
|
2391
|
-
* 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.
|
|
2392
2157
|
*/
|
|
2393
2158
|
async settleInvoice(args) {
|
|
2394
2159
|
assertFundingBasis(args.basis);
|
|
@@ -2482,19 +2247,9 @@ var CreditLedger = class {
|
|
|
2482
2247
|
return this.getInvoice(args);
|
|
2483
2248
|
}
|
|
2484
2249
|
/**
|
|
2485
|
-
*
|
|
2486
|
-
*
|
|
2487
|
-
*
|
|
2488
|
-
* expired invoice was never paid and owes nobody anything, whereas a blocked
|
|
2489
|
-
* one has sats sitting in the builder's wallet with no ledger row to put them
|
|
2490
|
-
* on. Only an operator can resolve that, so the row records `reason` and
|
|
2491
|
-
* stops being swept — leaving it `pending` would spend one of the sweep's few
|
|
2492
|
-
* slots re-deriving the same verdict on every request the caller makes, and
|
|
2493
|
-
* three of them would wedge that caller's sweep so a genuinely payable
|
|
2494
|
-
* invoice behind them never credits.
|
|
2495
|
-
*
|
|
2496
|
-
* A CAS on `pending`, like the expiry retirement: a settlement observed
|
|
2497
|
-
* 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.
|
|
2498
2253
|
*/
|
|
2499
2254
|
async markInvoiceBlocked(args) {
|
|
2500
2255
|
await this.pool.query(
|
|
@@ -2504,7 +2259,6 @@ var CreditLedger = class {
|
|
|
2504
2259
|
);
|
|
2505
2260
|
return this.getInvoice(args);
|
|
2506
2261
|
}
|
|
2507
|
-
/** Read one invoice by the wallet's own identifier for the payment (no lock). */
|
|
2508
2262
|
async getInvoiceByPaymentHash(paymentHash) {
|
|
2509
2263
|
const { rows } = await this.pool.query(
|
|
2510
2264
|
`SELECT * FROM credit_invoices WHERE payment_hash = $1`,
|
|
@@ -2513,17 +2267,9 @@ var CreditLedger = class {
|
|
|
2513
2267
|
return rows.length > 0 ? invoiceRecordFromRow(rows[0]) : void 0;
|
|
2514
2268
|
}
|
|
2515
2269
|
/**
|
|
2516
|
-
*
|
|
2517
|
-
*
|
|
2518
|
-
*
|
|
2519
|
-
* self-clear, so a row the operator declines to act on sits at the head of
|
|
2520
|
-
* the age ordering forever; a fixed first page would starve everything
|
|
2521
|
-
* behind it on every run, which is the shape one page up.
|
|
2522
|
-
*
|
|
2523
|
-
* `includeResolved` widens to the operator-resolved statuses so a run can be
|
|
2524
|
-
* audited after the fact.
|
|
2525
|
-
* Host-wide, like `listPendingDrains`: the ledger has no `dvm_id`, so on a
|
|
2526
|
-
* 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.
|
|
2527
2273
|
*/
|
|
2528
2274
|
async listBlockedInvoices(args) {
|
|
2529
2275
|
const statuses = args.includeResolved ? ["blocked", "reconciled", "written_off"] : ["blocked"];
|
|
@@ -2545,37 +2291,15 @@ var CreditLedger = class {
|
|
|
2545
2291
|
return rows.map(invoiceRecordFromRow);
|
|
2546
2292
|
}
|
|
2547
2293
|
/**
|
|
2548
|
-
* Apply a blocked invoice
|
|
2549
|
-
*
|
|
2550
|
-
*
|
|
2551
|
-
*
|
|
2552
|
-
*
|
|
2553
|
-
*
|
|
2554
|
-
*
|
|
2555
|
-
*
|
|
2556
|
-
*
|
|
2557
|
-
* identifier the operator actually holds (it is what the
|
|
2558
|
-
* `lightning_settlement_blocked` log carries and what names the payment in
|
|
2559
|
-
* their wallet). Invoice row first, credit row second via `fund`'s upsert —
|
|
2560
|
-
* the same order `settleInvoice` takes, which is what keeps a reconcile and
|
|
2561
|
-
* a concurrent settlement check from deadlocking against each other.
|
|
2562
|
-
* - **The funding lands at `(targetCreditId, paymentHash)`, not the invoice's
|
|
2563
|
-
* own `(credit_id, fund_id)`.** That key is frequently the reason the row
|
|
2564
|
-
* is blocked at all — `funding_replayed_on_cashu` means something else
|
|
2565
|
-
* already holds it — and the operator's most natural target is that very
|
|
2566
|
-
* credit. The payment hash cannot collide with it, and it makes all three
|
|
2567
|
-
* references to this payment agree: `basis.fundingRef`, the deposit's
|
|
2568
|
-
* `funding_id`, and the funding row's `fund_id`.
|
|
2569
|
-
* - **`written_off` is an accepted input status.** A write-off unwound
|
|
2570
|
-
* nothing, so an operator who closed a row by mistake must not need raw SQL
|
|
2571
|
-
* against a money table to reopen it.
|
|
2572
|
-
*
|
|
2573
|
-
* The invoice row's status — never the funding row — is the idempotency
|
|
2574
|
-
* source of truth. `fund_id` is caller-chosen and the payment hash is
|
|
2575
|
-
* disclosed to them, so a caller *can* squat `(theirCredit, thatHash)` with a
|
|
2576
|
-
* top-up on another rail; when they have, `recordFunding` throws
|
|
2577
|
-
* `funding_replayed` and that surfaces as its own loud failure rather than
|
|
2578
|
-
* 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.
|
|
2579
2303
|
*/
|
|
2580
2304
|
async reconcileBlockedInvoice(args) {
|
|
2581
2305
|
assertFundingBasis(args.basis);
|
|
@@ -2675,17 +2399,9 @@ var CreditLedger = class {
|
|
|
2675
2399
|
}
|
|
2676
2400
|
}
|
|
2677
2401
|
/**
|
|
2678
|
-
*
|
|
2679
|
-
*
|
|
2680
|
-
*
|
|
2681
|
-
* A CAS on `blocked`, and idempotent: re-running returns the recorded row
|
|
2682
|
-
* rather than overwriting the first note. It cannot capture a `reconciled`
|
|
2683
|
-
* row, because that one moved money.
|
|
2684
|
-
*
|
|
2685
|
-
* `written` reports whether *this* call was the transition. The caller has
|
|
2686
|
-
* no other way to tell — the row reads identically either way — and the
|
|
2687
|
-
* write-off's audit log is the only record of the amount, so a re-emitted
|
|
2688
|
-
* 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.
|
|
2689
2405
|
*/
|
|
2690
2406
|
async writeOffBlockedInvoice(args) {
|
|
2691
2407
|
const { rowCount } = await this.pool.query(
|
|
@@ -2719,7 +2435,6 @@ var CreditLedger = class {
|
|
|
2719
2435
|
}
|
|
2720
2436
|
return out;
|
|
2721
2437
|
}
|
|
2722
|
-
/** Read one draw (no lock), with its credit's rail basis joined in. */
|
|
2723
2438
|
async getDraw(args) {
|
|
2724
2439
|
const { rows } = await this.pool.query(
|
|
2725
2440
|
`${DRAW_WITH_BASIS_SELECT} WHERE d.credit_id = $1 AND d.draw_id = $2`,
|
|
@@ -2735,7 +2450,6 @@ var CreditLedger = class {
|
|
|
2735
2450
|
);
|
|
2736
2451
|
return rows.length === 1 ? drawRecordFromRow(rows[0], rows[0]) : void 0;
|
|
2737
2452
|
}
|
|
2738
|
-
/** Read every draw linked to a job, including already-resolved rows. */
|
|
2739
2453
|
async listDrawsByJobId(jobId) {
|
|
2740
2454
|
const { rows } = await this.pool.query(
|
|
2741
2455
|
`${DRAW_WITH_BASIS_SELECT} WHERE d.job_id = $1 ORDER BY d.created_at, d.credit_id, d.ledger_seq`,
|
|
@@ -2756,22 +2470,10 @@ var CreditLedger = class {
|
|
|
2756
2470
|
return rows.map((row) => drawRecordFromRow(row, row));
|
|
2757
2471
|
}
|
|
2758
2472
|
/**
|
|
2759
|
-
*
|
|
2760
|
-
*
|
|
2761
|
-
*
|
|
2762
|
-
*
|
|
2763
|
-
* resolve — the only release path keys off the `credit_id`/`draw_id` written
|
|
2764
|
-
* on the job row.
|
|
2765
|
-
*
|
|
2766
|
-
* Host-wide like {@link listPendingDrains}, not per-credit: the sweeper has
|
|
2767
|
-
* no candidate credit to start from. Age-bounded so it never sees a draw
|
|
2768
|
-
* whose job row is merely still in flight.
|
|
2769
|
-
*
|
|
2770
|
-
* `limit` is required and `after` keyset-paginates: an aged hold whose job
|
|
2771
|
-
* row *does* exist is legitimately un-sweepable and stays `pending`
|
|
2772
|
-
* indefinitely, so a caller that re-issues the same first page would never
|
|
2773
|
-
* see past a `limit`-sized wall of them. Pass the last row's
|
|
2774
|
-
* {@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.
|
|
2775
2477
|
*/
|
|
2776
2478
|
async listStalePendingDraws(args) {
|
|
2777
2479
|
const after = args.after;
|
|
@@ -2793,21 +2495,9 @@ var CreditLedger = class {
|
|
|
2793
2495
|
return rows.map((row) => drawRecordFromRow(row, row));
|
|
2794
2496
|
}
|
|
2795
2497
|
/**
|
|
2796
|
-
*
|
|
2797
|
-
*
|
|
2798
|
-
*
|
|
2799
|
-
* ceiling: a channel is claimable up to what its credit's draws
|
|
2800
|
-
* have earned, never up to the deposit that funded it.
|
|
2801
|
-
*
|
|
2802
|
-
* `undefined` when no credit is bound to the channel, which the claim job
|
|
2803
|
-
* treats as "cannot be resolved" and claims nothing for — distinct from a
|
|
2804
|
-
* resolved credit that has earned `0`.
|
|
2805
|
-
*
|
|
2806
|
-
* Deliberately a sum over settled draws rather than `funded − remaining`:
|
|
2807
|
-
* a drain lowers `native_remaining` without earning anything, so the
|
|
2808
|
-
* subtraction would read the caller's own reclaim as revenue. Pending holds
|
|
2809
|
-
* are excluded for the mirror reason — a hold is work in flight, and
|
|
2810
|
-
* 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.
|
|
2811
2501
|
*/
|
|
2812
2502
|
async earnedNativeForX402Channel(channelId) {
|
|
2813
2503
|
const { rows } = await this.pool.query(
|
|
@@ -2827,14 +2517,8 @@ var CreditLedger = class {
|
|
|
2827
2517
|
return toSafeInt(rows[0].earned);
|
|
2828
2518
|
}
|
|
2829
2519
|
/**
|
|
2830
|
-
*
|
|
2831
|
-
*
|
|
2832
|
-
*
|
|
2833
|
-
* The operator repair's entry point: a wedged settlement row carries the
|
|
2834
|
-
* channel and an `effect_id`, and this is what turns them back into the
|
|
2835
|
-
* credit id, the caller pubkey, and the drain id the effect encodes. Deriving
|
|
2836
|
-
* the credit id by splitting `effect_id` instead would be wrong — credit ids
|
|
2837
|
-
* 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.
|
|
2838
2522
|
*/
|
|
2839
2523
|
async getCreditByX402Channel(channelId) {
|
|
2840
2524
|
const { rows } = await this.pool.query(
|
|
@@ -2843,7 +2527,6 @@ var CreditLedger = class {
|
|
|
2843
2527
|
);
|
|
2844
2528
|
return rows[0] ? this.snapshotFromRow(rows[0], this.pool, Date.now()) : void 0;
|
|
2845
2529
|
}
|
|
2846
|
-
/** Retire every active credit backed by a chain-proven lost Tempo channel. */
|
|
2847
2530
|
async terminalizeTempoCredits(evidence, tx) {
|
|
2848
2531
|
const channelId = evidence.channelId.toLowerCase();
|
|
2849
2532
|
const observedAt = evidence.observedAt ?? Date.now();
|
|
@@ -2909,7 +2592,6 @@ var CreditLedger = class {
|
|
|
2909
2592
|
}
|
|
2910
2593
|
return this.readTempoCreditLosses(q, { channelId });
|
|
2911
2594
|
}
|
|
2912
|
-
/** List terminal Tempo credit losses, newest observation first. */
|
|
2913
2595
|
async listTempoCreditLosses(args) {
|
|
2914
2596
|
return this.readTempoCreditLosses(this.pool, { limit: args?.limit ?? 50 });
|
|
2915
2597
|
}
|
|
@@ -3003,32 +2685,18 @@ var CreditLedger = class {
|
|
|
3003
2685
|
}
|
|
3004
2686
|
return this.readX402CreditLosses(q, { channelId });
|
|
3005
2687
|
}
|
|
3006
|
-
/** List terminal x402 credit losses, newest observation first. */
|
|
3007
2688
|
async listX402CreditLosses(args) {
|
|
3008
2689
|
return this.readX402CreditLosses(this.pool, { limit: args?.limit ?? 50 });
|
|
3009
2690
|
}
|
|
3010
2691
|
/**
|
|
3011
|
-
*
|
|
3012
|
-
*
|
|
3013
|
-
*
|
|
3014
|
-
*
|
|
3015
|
-
*
|
|
3016
|
-
*
|
|
3017
|
-
*
|
|
3018
|
-
*
|
|
3019
|
-
* Idempotent on `(creditId, drainId)`: a replay returns the recorded drain
|
|
3020
|
-
* (this is also the poll/pickup read). A replay whose `method` or `payout`
|
|
3021
|
-
* differs from the recorded drain is refused with `drain_conflict` —
|
|
3022
|
-
* silently honouring a changed payout would ship the caller's money to a
|
|
3023
|
-
* destination they never signed alongside this `drain_id`.
|
|
3024
|
-
*
|
|
3025
|
-
* Pending holds are NOT drained — in-flight jobs keep their money until
|
|
3026
|
-
* they settle or release. A release after a drain restores available
|
|
3027
|
-
* balance the caller reclaims with a **new** `drain_id`; drains are ledger
|
|
3028
|
-
* arithmetic, not a terminal credit state, so `status` stays `active`.
|
|
3029
|
-
*
|
|
3030
|
-
* Deliberately **no expiry check**: draining works before and after
|
|
3031
|
-
* 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.
|
|
3032
2700
|
*/
|
|
3033
2701
|
async requestDrain(args) {
|
|
3034
2702
|
const nowMs = args.nowMs ?? Date.now();
|
|
@@ -3138,7 +2806,6 @@ var CreditLedger = class {
|
|
|
3138
2806
|
}
|
|
3139
2807
|
return this.withCreditLock(args.creditId, run);
|
|
3140
2808
|
}
|
|
3141
|
-
/** Read one drain (no lock), joined with its credit for currency + owner. */
|
|
3142
2809
|
async getDrain(args) {
|
|
3143
2810
|
const { rows } = await this.pool.query(
|
|
3144
2811
|
`SELECT d.*, ${DRAIN_CREDIT_COLUMNS} FROM credit_drains d
|
|
@@ -3157,25 +2824,11 @@ var CreditLedger = class {
|
|
|
3157
2824
|
return this.openLots(this.pool, creditId);
|
|
3158
2825
|
}
|
|
3159
2826
|
/**
|
|
3160
|
-
*
|
|
3161
|
-
*
|
|
3162
|
-
*
|
|
3163
|
-
*
|
|
3164
|
-
*
|
|
3165
|
-
* exchange-rate lookup anywhere: the obligation is in kind, so a rate has
|
|
3166
|
-
* nothing to say about it. Pending job holds are deliberately **not**
|
|
3167
|
-
* subtracted — a hold either settles into revenue or releases back to the
|
|
3168
|
-
* caller, and until it does, treating it as already earned would understate
|
|
3169
|
-
* the floor. The figure is conservative by exactly that amount.
|
|
3170
|
-
*
|
|
3171
|
-
* The two coverage holes ride along rather than being folded in silently, so
|
|
3172
|
-
* a gap reads as a gap: `uncovered` is a credit whose lots fall short of its
|
|
3173
|
-
* balance, `unbacked` is one holding any open micro with no sats basis. A
|
|
3174
|
-
* reclaim on either prices the **whole** balance off a live rate — the
|
|
3175
|
-
* depletion refuses a mixture rather than blending two bases — so both are
|
|
3176
|
-
* reported as the whole balance, and neither contributes to `depositSats`.
|
|
3177
|
-
* A figure that counted the backed half of a mixed credit would be a floor
|
|
3178
|
-
* 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.
|
|
3179
2832
|
*/
|
|
3180
2833
|
async bitcoinDepositLiability() {
|
|
3181
2834
|
const { rows } = await this.pool.query(
|
|
@@ -3203,21 +2856,43 @@ var CreditLedger = class {
|
|
|
3203
2856
|
byRail: [],
|
|
3204
2857
|
byCurrency: [],
|
|
3205
2858
|
uncoveredCredits: 0,
|
|
3206
|
-
|
|
2859
|
+
uncoveredByCurrency: [],
|
|
3207
2860
|
unbackedCredits: 0,
|
|
3208
|
-
|
|
2861
|
+
unbackedByCurrency: []
|
|
3209
2862
|
};
|
|
3210
2863
|
const byRail = /* @__PURE__ */ new Map();
|
|
3211
2864
|
const byCurrency = /* @__PURE__ */ new Map();
|
|
2865
|
+
const uncoveredByCurrency = /* @__PURE__ */ new Map();
|
|
2866
|
+
const unbackedByCurrency = /* @__PURE__ */ new Map();
|
|
3212
2867
|
for (const row of rows) {
|
|
3213
2868
|
const sats = toSafeInt(row.owed_sats);
|
|
3214
2869
|
const credits = toSafeInt(row.credits);
|
|
2870
|
+
const uncoveredCredits = toSafeInt(row.uncovered_credits);
|
|
2871
|
+
const unbackedCredits = toSafeInt(row.unbacked_credits);
|
|
3215
2872
|
liability.depositSats += sats;
|
|
3216
2873
|
liability.credits += credits;
|
|
3217
|
-
liability.uncoveredCredits +=
|
|
3218
|
-
liability.
|
|
3219
|
-
|
|
3220
|
-
|
|
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
|
+
}
|
|
3221
2896
|
const rail = byRail.get(row.rail) ?? { rail: row.rail, sats: 0, credits: 0 };
|
|
3222
2897
|
rail.sats += sats;
|
|
3223
2898
|
rail.credits += credits;
|
|
@@ -3233,6 +2908,8 @@ var CreditLedger = class {
|
|
|
3233
2908
|
}
|
|
3234
2909
|
liability.byRail = [...byRail.values()];
|
|
3235
2910
|
liability.byCurrency = [...byCurrency.values()];
|
|
2911
|
+
liability.uncoveredByCurrency = [...uncoveredByCurrency.values()];
|
|
2912
|
+
liability.unbackedByCurrency = [...unbackedByCurrency.values()];
|
|
3236
2913
|
return liability;
|
|
3237
2914
|
}
|
|
3238
2915
|
/**
|
|
@@ -3252,34 +2929,10 @@ var CreditLedger = class {
|
|
|
3252
2929
|
return rows.map(drainRecordFromJoinRow);
|
|
3253
2930
|
}
|
|
3254
2931
|
/**
|
|
3255
|
-
*
|
|
3256
|
-
*
|
|
3257
|
-
*
|
|
3258
|
-
*
|
|
3259
|
-
* never self-clears, so a caller that re-issues the same first page would
|
|
3260
|
-
* never see past a `limit`-sized wall of them. The keyset is
|
|
3261
|
-
* `(created_at, credit_id, drain_id)` — `created_at` is immutable, so no
|
|
3262
|
-
* concurrent write can push an unexamined row past the cursor. It is a
|
|
3263
|
-
* BIGINT of epoch milliseconds rather than a `TIMESTAMPTZ`, so the
|
|
3264
|
-
* microsecond-vs-millisecond cursor trap that bit `listSettlements` cannot
|
|
3265
|
-
* arise here: the value round-trips as the integer it already is.
|
|
3266
|
-
*
|
|
3267
|
-
* `createdBeforeMs` is the staleness floor. A healthy cooperative close
|
|
3268
|
-
* moves the drain to `sent` inside the request that opened it, so anything
|
|
3269
|
-
* younger is in flight rather than stuck.
|
|
3270
|
-
*
|
|
3271
|
-
* The method filter is `'tempo'`, and it is the one line here worth a second
|
|
3272
|
-
* look: `init()` migrates the legacy `'mpp'` spelling away and
|
|
3273
|
-
* `DrainMethod` no longer carries it, so a query naming it matches nothing a
|
|
3274
|
-
* DVM has ever written. It named it anyway until rename sweep and
|
|
3275
|
-
* live rung caught it independently — the whole operator queue
|
|
3276
|
-
* read empty in production while every route test stayed green, because
|
|
3277
|
-
* those run on `MemoryCreditLedger`, which filters on the typed value. The
|
|
3278
|
-
* two implementations of this one predicate must be read together.
|
|
3279
|
-
*
|
|
3280
|
-
* A written-off row is excluded unless `includeResolved`: the operator has
|
|
3281
|
-
* already decided about it, and leaving it on the live queue would re-serve
|
|
3282
|
-
* 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.
|
|
3283
2936
|
*/
|
|
3284
2937
|
async listChannelDrains(args) {
|
|
3285
2938
|
const params = [args.createdBeforeMs, args.limit];
|
|
@@ -3495,11 +3148,9 @@ var CreditLedger = class {
|
|
|
3495
3148
|
});
|
|
3496
3149
|
}
|
|
3497
3150
|
/**
|
|
3498
|
-
* Append
|
|
3499
|
-
*
|
|
3500
|
-
*
|
|
3501
|
-
* transition is the source of truth; the receipt is the proof the caller
|
|
3502
|
-
* 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.
|
|
3503
3154
|
*/
|
|
3504
3155
|
async appendDrainReceipt(args) {
|
|
3505
3156
|
const q = args.tx ?? this.pool;
|
|
@@ -3516,7 +3167,6 @@ var CreditLedger = class {
|
|
|
3516
3167
|
);
|
|
3517
3168
|
return rows[0];
|
|
3518
3169
|
}
|
|
3519
|
-
/** Re-read the credit join fields for a freshly-updated drain row. */
|
|
3520
3170
|
async rejoin(q, row) {
|
|
3521
3171
|
const { rows } = await q.query(
|
|
3522
3172
|
`SELECT currency, caller_pubkey, tempo_channel_id, x402_channel_id
|
|
@@ -3525,7 +3175,6 @@ var CreditLedger = class {
|
|
|
3525
3175
|
);
|
|
3526
3176
|
return drainRecordFromRow(row, rows[0]);
|
|
3527
3177
|
}
|
|
3528
|
-
/** Shared refusal tail for the CAS transitions: idempotent replay or typed error. */
|
|
3529
3178
|
async refuseTransition(q, creditId, drainId, opts) {
|
|
3530
3179
|
const row = await this.readDrain(q, creditId, drainId);
|
|
3531
3180
|
if (!row) {
|
|
@@ -3541,26 +3190,10 @@ var CreditLedger = class {
|
|
|
3541
3190
|
);
|
|
3542
3191
|
}
|
|
3543
3192
|
/**
|
|
3544
|
-
*
|
|
3545
|
-
*
|
|
3546
|
-
*
|
|
3547
|
-
*
|
|
3548
|
-
* lots have to shrink with it or the deposit-liability rollup would keep
|
|
3549
|
-
* counting a balance that is gone.
|
|
3550
|
-
*
|
|
3551
|
-
* `owedSats` is `null` where {@link isInKindDepletion} refuses — the lots
|
|
3552
|
-
* are short of the balance, or the covering lots carry no sats basis (a
|
|
3553
|
-
* legacy credit whose funding recorded no sats basis. That row is priced off a live rate by the
|
|
3554
|
-
* admin surface instead, which is what *every* row did before this change,
|
|
3555
|
-
* so the fallback is the old behaviour rather than a new failure mode.
|
|
3556
|
-
*
|
|
3557
|
-
* What it publishes is **net of the delivery reserve**: handing a
|
|
3558
|
-
* refund over costs a mint fee and, when the notes have to be minted just in
|
|
3559
|
-
* time, a Lightning hop. The caller carries that cost per and a
|
|
3560
|
-
* flat reserve is how they carry it without the figure moving — an
|
|
3561
|
-
* actual-fee true-up could only be applied after the promise was made. The
|
|
3562
|
-
* debits stay gross because they are denominated in micro and are what
|
|
3563
|
-
* `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.
|
|
3564
3197
|
*/
|
|
3565
3198
|
async reclaimInKind(q, creditId, amountMicro) {
|
|
3566
3199
|
const depletion = depleteLots(await this.openLots(q, creditId), amountMicro);
|
|
@@ -3605,7 +3238,6 @@ var CreditLedger = class {
|
|
|
3605
3238
|
);
|
|
3606
3239
|
}
|
|
3607
3240
|
}
|
|
3608
|
-
/** Shared settle/release state machine — see the public JSDoc on each verb. */
|
|
3609
3241
|
async resolveDraw(args) {
|
|
3610
3242
|
if (args.tx) {
|
|
3611
3243
|
const credit = await this.lockCreditRow(args.tx, args.creditId);
|
|
@@ -3715,7 +3347,6 @@ var CreditLedger = class {
|
|
|
3715
3347
|
client.release();
|
|
3716
3348
|
}
|
|
3717
3349
|
}
|
|
3718
|
-
/** `SELECT … FOR UPDATE` the credit row on `q` (which must be inside a transaction). */
|
|
3719
3350
|
async lockCreditRow(q, creditId) {
|
|
3720
3351
|
const { rows } = await q.query(
|
|
3721
3352
|
`SELECT * FROM credits WHERE credit_id = $1 FOR UPDATE`,
|