@dvmkit/sdk 0.1.0-rc.1 → 0.1.0-rc.2
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/dist/{chunk-RPXHKMYE.js → chunk-365P52XQ.js} +346 -33
- package/dist/{chunk-NTK5DJ6R.js → chunk-OJ5WFIB2.js} +11 -1
- package/dist/{credit-ledger-EDMEZSA2.js → credit-ledger-RO4FGSHG.js} +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/{job-store-C5n6bhap.d.ts → job-store-6gR4pZRP.d.ts} +279 -19
- package/dist/{memory-credit-ledger-7TTZDSRS.js → memory-credit-ledger-I2G64DDK.js} +2 -2
- package/dist/payout-reporter-4TNWRS5F.js +753 -0
- package/dist/{revenue-reporter-M35KP6V7.js → revenue-reporter-GB4WKLDC.js} +77 -2
- package/dist/server/index.d.ts +64 -4
- package/dist/server/index.js +203 -25
- package/dist/{ssrf-BdHsrrIb.d.ts → ssrf-DZi-xJyn.d.ts} +1 -1
- package/dist/testing/index.d.ts +1 -1
- package/package.json +1 -1
|
@@ -1001,6 +1001,7 @@ var CreditLedger = class {
|
|
|
1001
1001
|
*/
|
|
1002
1002
|
durable = true;
|
|
1003
1003
|
x402Settlements;
|
|
1004
|
+
expiryReleaseOutbox;
|
|
1004
1005
|
/**
|
|
1005
1006
|
* Bind the durable x402 settlement state this ledger gates spending on
|
|
1006
1007
|
* (internal-review).
|
|
@@ -1013,6 +1014,24 @@ var CreditLedger = class {
|
|
|
1013
1014
|
useX402SettlementGate(gate) {
|
|
1014
1015
|
this.x402Settlements = gate;
|
|
1015
1016
|
}
|
|
1017
|
+
/**
|
|
1018
|
+
* Bind the durable outbox that reports credit-expiry releases and their
|
|
1019
|
+
* revivals to the platform (internal-review).
|
|
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.
|
|
1031
|
+
*/
|
|
1032
|
+
useCreditExpiryReleaseOutbox(outbox) {
|
|
1033
|
+
this.expiryReleaseOutbox = outbox;
|
|
1034
|
+
}
|
|
1016
1035
|
/** Create the `credits` / `credit_draws` tables if absent. Call once at SDK boot. */
|
|
1017
1036
|
async init() {
|
|
1018
1037
|
await withSdkInitLock(this.pool, () => this.createTables());
|
|
@@ -1275,6 +1294,24 @@ var CreditLedger = class {
|
|
|
1275
1294
|
`CREATE INDEX IF NOT EXISTS idx_credit_invoices_blocked
|
|
1276
1295
|
ON credit_invoices(created_at, credit_id, fund_id) WHERE status = 'blocked';`
|
|
1277
1296
|
);
|
|
1297
|
+
await this.pool.query(`
|
|
1298
|
+
CREATE TABLE IF NOT EXISTS credit_expiry_releases (
|
|
1299
|
+
credit_id TEXT NOT NULL,
|
|
1300
|
+
release_id TEXT NOT NULL,
|
|
1301
|
+
caller_pubkey TEXT NOT NULL,
|
|
1302
|
+
currency TEXT NOT NULL,
|
|
1303
|
+
rail TEXT,
|
|
1304
|
+
amount_micro BIGINT NOT NULL,
|
|
1305
|
+
expiry_ms BIGINT NOT NULL,
|
|
1306
|
+
released_at BIGINT NOT NULL,
|
|
1307
|
+
reversed_at BIGINT,
|
|
1308
|
+
PRIMARY KEY (credit_id, release_id)
|
|
1309
|
+
);
|
|
1310
|
+
`);
|
|
1311
|
+
await this.pool.query(
|
|
1312
|
+
`CREATE INDEX IF NOT EXISTS idx_credit_expiry_releases_open
|
|
1313
|
+
ON credit_expiry_releases(credit_id) WHERE reversed_at IS NULL;`
|
|
1314
|
+
);
|
|
1278
1315
|
}
|
|
1279
1316
|
/**
|
|
1280
1317
|
* Give every open non-channel Bitcoin credit a funding lot (internal-review).
|
|
@@ -1341,9 +1378,15 @@ var CreditLedger = class {
|
|
|
1341
1378
|
*
|
|
1342
1379
|
* Pass `tx` (a client inside a caller-owned `BEGIN`) to commit the rail
|
|
1343
1380
|
* receive and the ledger credit atomically (spec condition 3 — the internal-review
|
|
1344
|
-
* verifier does this). The ledger issues **no** transaction control on `tx
|
|
1345
|
-
*
|
|
1346
|
-
*
|
|
1381
|
+
* verifier does this). The ledger issues **no** transaction control on `tx`.
|
|
1382
|
+
*
|
|
1383
|
+
* Without `tx` it opens one of its own, because a funding is no longer a
|
|
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.
|
|
1347
1390
|
*
|
|
1348
1391
|
* `basis` records the rail value behind the fiat (internal-review) so each draw can
|
|
1349
1392
|
* be allocated its share of the rail-native amount actually received. A
|
|
@@ -1389,9 +1432,29 @@ var CreditLedger = class {
|
|
|
1389
1432
|
);
|
|
1390
1433
|
}
|
|
1391
1434
|
const b = assertFundingBasis(args.basis);
|
|
1392
|
-
const q = args.tx ?? this.pool;
|
|
1393
1435
|
const nowMs = args.nowMs ?? Date.now();
|
|
1394
1436
|
const creditId = args.creditId ?? randomUUID();
|
|
1437
|
+
if (args.tx) return this.fundOn(args.tx, args, b, nowMs, creditId);
|
|
1438
|
+
const client = await this.pool.connect();
|
|
1439
|
+
try {
|
|
1440
|
+
await client.query("BEGIN");
|
|
1441
|
+
const snapshot = await this.fundOn(client, args, b, nowMs, creditId);
|
|
1442
|
+
await client.query("COMMIT");
|
|
1443
|
+
return snapshot;
|
|
1444
|
+
} catch (err) {
|
|
1445
|
+
await client.query("ROLLBACK").catch(() => {
|
|
1446
|
+
});
|
|
1447
|
+
throw err;
|
|
1448
|
+
} finally {
|
|
1449
|
+
client.release();
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
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
|
+
async fundOn(q, args, b, nowMs, creditId) {
|
|
1395
1458
|
const wedged = await this.blockingX402Refund(
|
|
1396
1459
|
b.x402Channel?.channelId,
|
|
1397
1460
|
X402_SPEND_BLOCKING_SETTLEMENT_STATUSES
|
|
@@ -1524,7 +1587,196 @@ var CreditLedger = class {
|
|
|
1524
1587
|
);
|
|
1525
1588
|
}
|
|
1526
1589
|
await this.recordFundingLot(q, creditId, args.amountMicro, b, nowMs);
|
|
1527
|
-
|
|
1590
|
+
const revived = await this.reverseExpiryReleases(q, creditId, nowMs);
|
|
1591
|
+
return this.snapshotFromRow(revived ?? rows[0], q, nowMs);
|
|
1592
|
+
}
|
|
1593
|
+
/**
|
|
1594
|
+
* Undo any expiry release this credit still carries, because a funding just
|
|
1595
|
+
* landed on it (internal-review — the revival rule), restoring the balance the
|
|
1596
|
+
* release took.
|
|
1597
|
+
*
|
|
1598
|
+
* A release records the credit's **final** undrawn remainder and zeroes it
|
|
1599
|
+
* (see {@link releaseExpiredCreditLocked}). Funding adds to the balance and
|
|
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.
|
|
1621
|
+
*/
|
|
1622
|
+
async reverseExpiryReleases(q, creditId, nowMs) {
|
|
1623
|
+
const { rows } = await q.query(
|
|
1624
|
+
`UPDATE credit_expiry_releases
|
|
1625
|
+
SET reversed_at = $2
|
|
1626
|
+
WHERE credit_id = $1 AND reversed_at IS NULL
|
|
1627
|
+
RETURNING *`,
|
|
1628
|
+
[creditId, nowMs]
|
|
1629
|
+
);
|
|
1630
|
+
if (rows.length === 0) return void 0;
|
|
1631
|
+
const restoredMicro = rows.reduce((sum, row) => sum + toSafeInt(row.amount_micro), 0);
|
|
1632
|
+
const { rows: restored } = await q.query(
|
|
1633
|
+
`UPDATE credits SET balance_micro = balance_micro + $2 WHERE credit_id = $1 RETURNING *`,
|
|
1634
|
+
[creditId, restoredMicro]
|
|
1635
|
+
);
|
|
1636
|
+
const outbox = this.expiryReleaseOutbox;
|
|
1637
|
+
if (outbox) {
|
|
1638
|
+
for (const row of rows) {
|
|
1639
|
+
await outbox.enqueue(q, expiryReleaseReport(outbox.dvmId, row));
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
return restored[0];
|
|
1643
|
+
}
|
|
1644
|
+
/**
|
|
1645
|
+
* Release the undrawn remainder of every credit whose TTL has run out
|
|
1646
|
+
* (internal-review). Returns the releases this pass recorded.
|
|
1647
|
+
*
|
|
1648
|
+
* **What is released.** The credit's whole `balance_micro`. Expiry ends
|
|
1649
|
+
* spending but never ownership of the record, so the balance stays readable;
|
|
1650
|
+
* what ends is the caller's claim on it, and on the prepaid rails the value
|
|
1651
|
+
* behind it is already in builder custody.
|
|
1652
|
+
*
|
|
1653
|
+
* **Tempo is excluded**, at both of its markers (`tempo_channel_id` and a
|
|
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.
|
|
1676
|
+
*/
|
|
1677
|
+
async sweepExpiredCredits(args) {
|
|
1678
|
+
const nowMs = args?.nowMs ?? Date.now();
|
|
1679
|
+
const limit = args?.limit ?? DEFAULT_EXPIRY_SWEEP_LIMIT;
|
|
1680
|
+
const { rows: candidates } = await this.pool.query(
|
|
1681
|
+
`SELECT c.credit_id
|
|
1682
|
+
FROM credits c
|
|
1683
|
+
WHERE c.expiry_ms <= $1
|
|
1684
|
+
AND c.balance_micro > 0
|
|
1685
|
+
AND c.status = 'active'
|
|
1686
|
+
AND c.tempo_channel_id IS NULL
|
|
1687
|
+
AND COALESCE(c.rail, '') <> 'tempo'
|
|
1688
|
+
AND NOT EXISTS (
|
|
1689
|
+
SELECT 1 FROM credit_expiry_releases r
|
|
1690
|
+
WHERE r.credit_id = c.credit_id AND r.reversed_at IS NULL
|
|
1691
|
+
)
|
|
1692
|
+
AND NOT EXISTS (
|
|
1693
|
+
SELECT 1 FROM credit_draws d
|
|
1694
|
+
WHERE d.credit_id = c.credit_id AND d.status = 'pending'
|
|
1695
|
+
)
|
|
1696
|
+
ORDER BY c.expiry_ms
|
|
1697
|
+
LIMIT $2`,
|
|
1698
|
+
[nowMs, limit]
|
|
1699
|
+
);
|
|
1700
|
+
const released = [];
|
|
1701
|
+
for (const candidate of candidates) {
|
|
1702
|
+
const row = await this.withCreditLock(
|
|
1703
|
+
candidate.credit_id,
|
|
1704
|
+
(client, credit) => this.releaseExpiredCreditLocked(client, credit, nowMs)
|
|
1705
|
+
);
|
|
1706
|
+
if (row) released.push(expiryReleaseFromRow(row));
|
|
1707
|
+
}
|
|
1708
|
+
return { released };
|
|
1709
|
+
}
|
|
1710
|
+
/**
|
|
1711
|
+
* Write one credit's expiry release under its row lock, zero the balance it
|
|
1712
|
+
* released, and re-assert every condition the unlocked candidate scan tested.
|
|
1713
|
+
* Returns undefined when the credit no longer qualifies — funded, drawn
|
|
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.
|
|
1727
|
+
*
|
|
1728
|
+
* `status` deliberately stays `active`: a released credit must remain
|
|
1729
|
+
* fundable, or the revival rule has nothing to revive.
|
|
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.
|
|
1735
|
+
*/
|
|
1736
|
+
async releaseExpiredCreditLocked(client, credit, nowMs) {
|
|
1737
|
+
const expiryMs = toSafeInt(credit.expiry_ms);
|
|
1738
|
+
const balanceMicro = toSafeInt(credit.balance_micro);
|
|
1739
|
+
if (expiryMs > nowMs || balanceMicro <= 0 || credit.status !== "active") return void 0;
|
|
1740
|
+
if (credit.tempo_channel_id !== null || credit.rail === "tempo") return void 0;
|
|
1741
|
+
const pending = await this.pendingSums(client, credit.credit_id);
|
|
1742
|
+
if (pending.micro > 0) return void 0;
|
|
1743
|
+
const { rows: open } = await client.query(
|
|
1744
|
+
`SELECT 1 AS one FROM credit_expiry_releases
|
|
1745
|
+
WHERE credit_id = $1 AND reversed_at IS NULL`,
|
|
1746
|
+
[credit.credit_id]
|
|
1747
|
+
);
|
|
1748
|
+
if (open.length > 0) return void 0;
|
|
1749
|
+
const { rows: episodes } = await client.query(
|
|
1750
|
+
`SELECT COUNT(*)::text AS n FROM credit_expiry_releases WHERE credit_id = $1`,
|
|
1751
|
+
[credit.credit_id]
|
|
1752
|
+
);
|
|
1753
|
+
const seq = toSafeInt(episodes[0].n) + 1;
|
|
1754
|
+
const { rows } = await client.query(
|
|
1755
|
+
`INSERT INTO credit_expiry_releases
|
|
1756
|
+
(credit_id, release_id, caller_pubkey, currency, rail, amount_micro, expiry_ms, released_at)
|
|
1757
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
|
1758
|
+
ON CONFLICT (credit_id, release_id) DO NOTHING
|
|
1759
|
+
RETURNING *`,
|
|
1760
|
+
[
|
|
1761
|
+
credit.credit_id,
|
|
1762
|
+
expiryReleaseId(expiryMs, seq),
|
|
1763
|
+
credit.caller_pubkey,
|
|
1764
|
+
credit.currency,
|
|
1765
|
+
credit.rail,
|
|
1766
|
+
balanceMicro,
|
|
1767
|
+
expiryMs,
|
|
1768
|
+
nowMs
|
|
1769
|
+
]
|
|
1770
|
+
);
|
|
1771
|
+
if (rows.length === 0) return void 0;
|
|
1772
|
+
const row = rows[0];
|
|
1773
|
+
await client.query(
|
|
1774
|
+
`UPDATE credits SET balance_micro = balance_micro - $2 WHERE credit_id = $1`,
|
|
1775
|
+
[credit.credit_id, balanceMicro]
|
|
1776
|
+
);
|
|
1777
|
+
const outbox = this.expiryReleaseOutbox;
|
|
1778
|
+
if (outbox) await outbox.enqueue(client, expiryReleaseReport(outbox.dvmId, row));
|
|
1779
|
+
return row;
|
|
1528
1780
|
}
|
|
1529
1781
|
/**
|
|
1530
1782
|
* Record this funding's in-kind basis as a lot (internal-review).
|
|
@@ -2654,7 +2906,15 @@ var CreditLedger = class {
|
|
|
2654
2906
|
async listTempoCreditLosses(args) {
|
|
2655
2907
|
return this.readTempoCreditLosses(this.pool, { limit: args?.limit ?? 50 });
|
|
2656
2908
|
}
|
|
2657
|
-
/**
|
|
2909
|
+
/**
|
|
2910
|
+
* Reconcile the credit bound to a chain-proven empty x402 channel.
|
|
2911
|
+
*
|
|
2912
|
+
* A channel with no unclaimed backing makes every remaining customer
|
|
2913
|
+
* liability unsafe, so those credits become terminal. A fully settled credit
|
|
2914
|
+
* has neither a balance nor an unfinished drain and is safe to retain for a
|
|
2915
|
+
* later deposit. This also repairs a zero-liability row an older build
|
|
2916
|
+
* terminalized from the same observation.
|
|
2917
|
+
*/
|
|
2658
2918
|
async terminalizeX402Credit(evidence, tx) {
|
|
2659
2919
|
const channelId = evidence.channelId.toLowerCase();
|
|
2660
2920
|
const observedAt = evidence.observedAt ?? Date.now();
|
|
@@ -2682,36 +2942,57 @@ var CreditLedger = class {
|
|
|
2682
2942
|
`SELECT * FROM credits WHERE x402_channel_id = $1 FOR UPDATE`,
|
|
2683
2943
|
[channelId]
|
|
2684
2944
|
);
|
|
2685
|
-
if (credits.length > 0
|
|
2945
|
+
if (credits.length > 0) {
|
|
2686
2946
|
const credit = credits[0];
|
|
2687
|
-
if (credit.status !== "active"
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2947
|
+
if (credit.status !== "active" && credit.status !== "unbacked") {
|
|
2948
|
+
throw terminalCreditError(credit.credit_id, credit.status);
|
|
2949
|
+
}
|
|
2950
|
+
const { rows: drains } = await q.query(
|
|
2951
|
+
`SELECT EXISTS(
|
|
2952
|
+
SELECT 1 FROM credit_drains
|
|
2953
|
+
WHERE credit_id = $1 AND status IN ('pending', 'parked')
|
|
2954
|
+
) AS outstanding`,
|
|
2692
2955
|
[credit.credit_id]
|
|
2693
2956
|
);
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2957
|
+
const hasLiability = toSafeInt(credit.balance_micro) > 0 || drains[0]?.outstanding;
|
|
2958
|
+
if (!hasLiability) {
|
|
2959
|
+
if (credit.status === "unbacked") {
|
|
2960
|
+
await q.query(`UPDATE credits SET status = 'active' WHERE credit_id = $1`, [
|
|
2961
|
+
credit.credit_id
|
|
2962
|
+
]);
|
|
2963
|
+
await q.query(`DELETE FROM x402_credit_losses WHERE credit_id = $1`, [credit.credit_id]);
|
|
2964
|
+
}
|
|
2965
|
+
return this.readX402CreditLosses(q, { channelId });
|
|
2966
|
+
}
|
|
2967
|
+
if (credit.status !== "unbacked") {
|
|
2968
|
+
const { rows: consumed } = await q.query(
|
|
2969
|
+
`SELECT COALESCE(SUM(amount_micro), 0)::text AS amount
|
|
2970
|
+
FROM credit_draws
|
|
2971
|
+
WHERE credit_id = $1 AND status = 'settled'`,
|
|
2972
|
+
[credit.credit_id]
|
|
2973
|
+
);
|
|
2974
|
+
await q.query(
|
|
2975
|
+
`INSERT INTO x402_credit_losses
|
|
2976
|
+
(credit_id, channel_id, caller_pubkey, currency, former_balance_micro,
|
|
2977
|
+
channel_balance_native, total_claimed_native, consumed_service_micro, observed_at)
|
|
2978
|
+
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9)
|
|
2979
|
+
ON CONFLICT (credit_id) DO NOTHING`,
|
|
2980
|
+
[
|
|
2981
|
+
credit.credit_id,
|
|
2982
|
+
channelId,
|
|
2983
|
+
credit.caller_pubkey,
|
|
2984
|
+
credit.currency,
|
|
2985
|
+
credit.balance_micro,
|
|
2986
|
+
evidence.channelBalanceNative.toString(),
|
|
2987
|
+
evidence.totalClaimedNative.toString(),
|
|
2988
|
+
consumed[0]?.amount ?? "0",
|
|
2989
|
+
observedAt
|
|
2990
|
+
]
|
|
2991
|
+
);
|
|
2992
|
+
await q.query(`UPDATE credits SET status = 'unbacked' WHERE credit_id = $1`, [
|
|
2993
|
+
credit.credit_id
|
|
2994
|
+
]);
|
|
2995
|
+
}
|
|
2715
2996
|
}
|
|
2716
2997
|
return this.readX402CreditLosses(q, { channelId });
|
|
2717
2998
|
}
|
|
@@ -3778,6 +4059,38 @@ function invoiceRecordFromRow(row) {
|
|
|
3778
4059
|
settledAt: row.settled_at === null ? null : toSafeInt(row.settled_at)
|
|
3779
4060
|
};
|
|
3780
4061
|
}
|
|
4062
|
+
var DEFAULT_EXPIRY_SWEEP_LIMIT = 200;
|
|
4063
|
+
function expiryReleaseId(expiryMs, seq) {
|
|
4064
|
+
return `xrel:${expiryMs}:${seq}`;
|
|
4065
|
+
}
|
|
4066
|
+
function expiryReleaseFromRow(row) {
|
|
4067
|
+
return {
|
|
4068
|
+
creditId: row.credit_id,
|
|
4069
|
+
releaseId: row.release_id,
|
|
4070
|
+
callerPubkey: row.caller_pubkey,
|
|
4071
|
+
currency: row.currency,
|
|
4072
|
+
rail: row.rail,
|
|
4073
|
+
amountMicro: toSafeInt(row.amount_micro),
|
|
4074
|
+
expiryMs: toSafeInt(row.expiry_ms),
|
|
4075
|
+
releasedAt: toSafeInt(row.released_at),
|
|
4076
|
+
reversedAt: row.reversed_at === null ? null : toSafeInt(row.reversed_at)
|
|
4077
|
+
};
|
|
4078
|
+
}
|
|
4079
|
+
function expiryReleaseReport(dvmId, row) {
|
|
4080
|
+
const release = expiryReleaseFromRow(row);
|
|
4081
|
+
return {
|
|
4082
|
+
dvmId,
|
|
4083
|
+
creditId: release.creditId,
|
|
4084
|
+
releaseId: release.releaseId,
|
|
4085
|
+
callerPubkey: release.callerPubkey,
|
|
4086
|
+
rail: release.rail,
|
|
4087
|
+
amountMicro: release.amountMicro,
|
|
4088
|
+
creditCurrency: release.currency,
|
|
4089
|
+
expiryMs: release.expiryMs,
|
|
4090
|
+
releasedAt: release.releasedAt,
|
|
4091
|
+
reversedAt: release.reversedAt
|
|
4092
|
+
};
|
|
4093
|
+
}
|
|
3781
4094
|
|
|
3782
4095
|
export {
|
|
3783
4096
|
isNonChannelBitcoinRail,
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
lotOwedSats,
|
|
16
16
|
netOwedSats,
|
|
17
17
|
x402SettlementPending
|
|
18
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-365P52XQ.js";
|
|
19
19
|
|
|
20
20
|
// src/sdk/server/memory-credit-ledger.ts
|
|
21
21
|
import { randomUUID } from "crypto";
|
|
@@ -702,6 +702,16 @@ var MemoryCreditLedger = class {
|
|
|
702
702
|
const observedAt = evidence.observedAt ?? Date.now();
|
|
703
703
|
for (const credit of this.credits.values()) {
|
|
704
704
|
if (credit.x402ChannelId !== channelId) continue;
|
|
705
|
+
const hasLiability = credit.balanceMicro > 0 || [...credit.drains.values()].some(
|
|
706
|
+
(drain) => drain.status === "pending" || drain.status === "parked"
|
|
707
|
+
);
|
|
708
|
+
if (!hasLiability) {
|
|
709
|
+
if (credit.status === "unbacked") {
|
|
710
|
+
credit.status = "active";
|
|
711
|
+
this.x402Losses.delete(credit.creditId);
|
|
712
|
+
}
|
|
713
|
+
continue;
|
|
714
|
+
}
|
|
705
715
|
if (credit.status === "unbacked") continue;
|
|
706
716
|
const consumedServiceMicro = [...credit.draws.values()].filter((draw) => draw.status === "settled").reduce((sum, draw) => sum + draw.amountMicro, 0);
|
|
707
717
|
this.x402Losses.set(credit.creditId, {
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { C as Currency, Z as ZodLike, D as DVMConfig, a as DVMDescriptor } from './job-store-
|
|
2
|
-
export { A as ApprovalContent, b as ArtifactContent, c as CancelContent, d as CanonicalEnvelope, e as CreateSignedRequestVerifierOpts, f as CreditConfig, g as CreditView, h as DEFAULT_CREDIT_MAX, i as DEFAULT_CREDIT_MIN, j as DEFAULT_CREDIT_TTL_SECONDS, k as DVMRouteContext, I as IncomingMessage, l as InputType, m as InvalidCurrencyError, J as JobRecord, n as JobStatus, o as JobStore, K as KVStore, L as Logger, P as PaymentContent, p as PaymentMethod, q as PriceValue, r as ProgressContent, s as PromptOpts, Q as QuoteConfig, t as QuoteContext, u as QuoteResult, R as ResolvedCreditConfig, v as ResponseContent, S as SDKJobContext, w as SDKPaymentRequestOpts, x as SIGNED_REQUEST_AUTH_ID, y as SIGNED_REQUEST_STATEMENT_VERSION, z as SignedRequestAudience, B as SignedRequestDomain, E as SignedRequestError, F as SignedRequestFailure, G as SignedRequestReplayStore, H as SignedRequestSignOpts, M as SignedRequestStatementHeader, N as SignedRequestVerifier, U as UnsupportedCurrencyError, O as createSignedRequestVerifier, T as isZodSchema, V as signedRequestStatementHeader, W as validateCurrency } from './job-store-
|
|
3
|
-
export { C as CreateFxFetcherOpts, D as DEFAULT_FX_CURRENCIES, a as DEFAULT_FX_RATE_SOURCE, F as FxFetcher, b as FxRateSnapshot, c as FxRateUnavailableError, P as PinnedFetch, d as PlatformFxSource, S as SSRFError, e as SSRFGuardOpts, f as SSRFReason, g as SSRFResolver, h as assertSafeUrl, i as createFxFetcher, j as createPinnedFetch, k as fxRateFor, r as resolveFxSourceFromEnv } from './ssrf-
|
|
1
|
+
import { C as Currency, Z as ZodLike, D as DVMConfig, a as DVMDescriptor } from './job-store-6gR4pZRP.js';
|
|
2
|
+
export { A as ApprovalContent, b as ArtifactContent, c as CancelContent, d as CanonicalEnvelope, e as CreateSignedRequestVerifierOpts, f as CreditConfig, g as CreditView, h as DEFAULT_CREDIT_MAX, i as DEFAULT_CREDIT_MIN, j as DEFAULT_CREDIT_TTL_SECONDS, k as DVMRouteContext, I as IncomingMessage, l as InputType, m as InvalidCurrencyError, J as JobRecord, n as JobStatus, o as JobStore, K as KVStore, L as Logger, P as PaymentContent, p as PaymentMethod, q as PriceValue, r as ProgressContent, s as PromptOpts, Q as QuoteConfig, t as QuoteContext, u as QuoteResult, R as ResolvedCreditConfig, v as ResponseContent, S as SDKJobContext, w as SDKPaymentRequestOpts, x as SIGNED_REQUEST_AUTH_ID, y as SIGNED_REQUEST_STATEMENT_VERSION, z as SignedRequestAudience, B as SignedRequestDomain, E as SignedRequestError, F as SignedRequestFailure, G as SignedRequestReplayStore, H as SignedRequestSignOpts, M as SignedRequestStatementHeader, N as SignedRequestVerifier, U as UnsupportedCurrencyError, O as createSignedRequestVerifier, T as isZodSchema, V as signedRequestStatementHeader, W as validateCurrency } from './job-store-6gR4pZRP.js';
|
|
3
|
+
export { C as CreateFxFetcherOpts, D as DEFAULT_FX_CURRENCIES, a as DEFAULT_FX_RATE_SOURCE, F as FxFetcher, b as FxRateSnapshot, c as FxRateUnavailableError, P as PinnedFetch, d as PlatformFxSource, S as SSRFError, e as SSRFGuardOpts, f as SSRFReason, g as SSRFResolver, h as assertSafeUrl, i as createFxFetcher, j as createPinnedFetch, k as fxRateFor, r as resolveFxSourceFromEnv } from './ssrf-DZi-xJyn.js';
|
|
4
4
|
export { z } from 'zod';
|
|
5
5
|
import '@cashu/cashu-ts';
|
|
6
6
|
import 'mppx';
|