@dvmkit/sdk 0.21.2 → 0.22.0
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/card-charge-journal-SKMBEPRM.js +44 -0
- package/dist/{chunk-MA55LUNR.js → chunk-2WWVI32F.js} +55 -1
- package/dist/{chunk-3V6CSTWB.js → chunk-3DGIRHP2.js} +2 -2
- package/dist/{chunk-WWKFG6TM.js → chunk-5NMHZIIU.js} +1 -1
- package/dist/{chunk-FAT57DAP.js → chunk-5YJWAJ4N.js} +1 -1
- package/dist/{chunk-D2MGOQIS.js → chunk-CUWSJQGT.js} +1 -1
- package/dist/{chunk-6GZF7VDO.js → chunk-DLRPKEAM.js} +1 -1
- package/dist/{chunk-62JG4AF7.js → chunk-DY5QIHJA.js} +1 -1
- package/dist/{chunk-LWF4ABYF.js → chunk-EHBUAJ5M.js} +1 -1
- package/dist/{chunk-IUFVIIPF.js → chunk-EQIP6TFD.js} +1 -1
- package/dist/{chunk-ZBOCNF56.js → chunk-GDZXGB43.js} +3 -3
- package/dist/{chunk-2HSWWHXK.js → chunk-HEH7YUGE.js} +1 -1
- package/dist/{chunk-B27E7JIR.js → chunk-HVZIITWW.js} +27 -4
- package/dist/{chunk-VIAMJ6GS.js → chunk-JINZMDNK.js} +1224 -80
- package/dist/{chunk-NRE4NOCY.js → chunk-JMTLK2SZ.js} +1 -1
- package/dist/{chunk-LSANCGLP.js → chunk-L35WYOH2.js} +1 -1
- package/dist/{chunk-27QB2D3Z.js → chunk-LCNRFKP7.js} +2 -2
- package/dist/{chunk-2AGADOX3.js → chunk-QMKGYIBM.js} +1 -1
- package/dist/{chunk-JOSNSNS7.js → chunk-SEIMPHIP.js} +225 -11
- package/dist/{chunk-Y746QPX7.js → chunk-SQOVLAJ2.js} +1 -1
- package/dist/{chunk-XWG54M6T.js → chunk-UVRBD5GR.js} +1 -1
- package/dist/{chunk-5RGZN6WQ.js → chunk-XB644VN3.js} +1 -1
- package/dist/{chunk-2RZRYMCR.js → chunk-YNHFJE6U.js} +1 -1
- package/dist/{chunk-7KNFEDFW.js → chunk-ZH5CLGO6.js} +1 -1
- package/dist/{content-retention-efjireLM.d.ts → content-retention-DEvY5vjM.d.ts} +1 -1
- package/dist/{credit-ledger-M3EBVMOP.js → credit-ledger-S76KU53A.js} +11 -3
- package/dist/{credit-menu-UOOxuxVL.d.ts → credit-menu-BA2my6hE.d.ts} +229 -8
- package/dist/{fx-BtxHVj6w.d.ts → fx-J5oXbv9A.d.ts} +1 -1
- package/dist/index.d.ts +5 -5
- package/dist/index.js +4 -4
- package/dist/internal/caller.d.ts +10 -9
- package/dist/internal/caller.js +11 -11
- package/dist/internal/server.d.ts +7 -7
- package/dist/internal/server.js +21 -21
- package/dist/{job-store-DWVkleH3.d.ts → job-store-CjPnf0dd.d.ts} +1 -1
- package/dist/{lightning-backend-CTijN35u.d.ts → lightning-backend-D9Q2KDS-.d.ts} +1 -1
- package/dist/{memory-credit-ledger-WL2VKLE3.js → memory-credit-ledger-5KXHOWB2.js} +4 -4
- package/dist/{mpp-setup-DBYDXISN.js → mpp-setup-3YZMURZL.js} +3 -3
- package/dist/{payout-reporter-D5TCIBQA.js → payout-reporter-KTLBNJJ7.js} +2 -2
- package/dist/{postgres-consumed-credential-store-IPWPJ6V7.js → postgres-consumed-credential-store-Y336KNIB.js} +1 -1
- package/dist/{postgres-job-store-POZJNFJU.js → postgres-job-store-ESEAN7JL.js} +2 -2
- package/dist/{postgres-replay-store-JQYAKG47.js → postgres-replay-store-6SVCZ475.js} +2 -2
- package/dist/{pricing-FGPVRNTL.js → pricing-RP5CGW6I.js} +3 -3
- package/dist/{revenue-reporter-QKCBYFWT.js → revenue-reporter-FD3GDQZS.js} +2 -2
- package/dist/server/index.d.ts +20 -11
- package/dist/server/index.js +292 -28
- package/dist/{step-cache-Bx_ZybWF.d.ts → step-cache-DXFftnAc.d.ts} +154 -12
- package/dist/{tempo-charge-store-P7ZBR3GG.js → tempo-charge-store-SOIRMEFJ.js} +2 -2
- package/dist/{tempo-session-store-ZVRBAMLG.js → tempo-session-store-UBDFFHDG.js} +3 -3
- package/dist/testing/index.d.ts +3 -3
- package/dist/testing/index.js +6 -6
- package/dist/{usd-eOAMEO12.d.ts → usd-Bc_rva_K.d.ts} +1 -1
- package/dist/{x402-44JZJQ5F.js → x402-MFBRORHG.js} +5 -5
- package/package.json +1 -1
- package/dist/{chunk-NUDHO4HV.js → chunk-LABJSMSJ.js} +48 -48
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
loggers
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-ZH5CLGO6.js";
|
|
4
4
|
import {
|
|
5
5
|
diagnosticConsole
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-2WWVI32F.js";
|
|
7
7
|
|
|
8
8
|
// src/sdk/server/mpp-setup.ts
|
|
9
9
|
import { createHash, randomUUID } from "crypto";
|
|
@@ -10,13 +10,13 @@ import {
|
|
|
10
10
|
} from "./chunk-5UC7TOQC.js";
|
|
11
11
|
import {
|
|
12
12
|
applyRetryToPool
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-L35WYOH2.js";
|
|
14
14
|
import {
|
|
15
15
|
x402NetworkByCaip2
|
|
16
16
|
} from "./chunk-BPYMEMQX.js";
|
|
17
17
|
import {
|
|
18
18
|
diagnosticConsole
|
|
19
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-2WWVI32F.js";
|
|
20
20
|
|
|
21
21
|
// src/sdk/server/credit-ledger.ts
|
|
22
22
|
import { randomUUID } from "crypto";
|
|
@@ -26,6 +26,10 @@ var NON_CHANNEL_BITCOIN_RAILS = [
|
|
|
26
26
|
"cashu",
|
|
27
27
|
"lightning"
|
|
28
28
|
];
|
|
29
|
+
var CARD_FUNDED_RAILS = ["stripe"];
|
|
30
|
+
function isCardFundedRail(rail) {
|
|
31
|
+
return typeof rail === "string" && CARD_FUNDED_RAILS.includes(rail);
|
|
32
|
+
}
|
|
29
33
|
function isNonChannelBitcoinRail(rail) {
|
|
30
34
|
return typeof rail === "string" && NON_CHANNEL_BITCOIN_RAILS.includes(rail);
|
|
31
35
|
}
|
|
@@ -1414,6 +1418,7 @@ var CreditLedger = class {
|
|
|
1414
1418
|
await this.pool.query(`ALTER TABLE credit_drains ADD COLUMN IF NOT EXISTS proven_refund_native NUMERIC(78,0)`);
|
|
1415
1419
|
await this.pool.query(`ALTER TABLE credit_drains ADD COLUMN IF NOT EXISTS refund_payment_proven BOOLEAN NOT NULL DEFAULT FALSE`);
|
|
1416
1420
|
await this.pool.query(`ALTER TABLE credit_drains ADD COLUMN IF NOT EXISTS fulfilment JSONB`);
|
|
1421
|
+
await this.pool.query(`ALTER TABLE credit_drains ADD COLUMN IF NOT EXISTS card_refund JSONB`);
|
|
1417
1422
|
await this.pool.query(`ALTER TABLE credit_drains ADD COLUMN IF NOT EXISTS write_off_note TEXT`);
|
|
1418
1423
|
await this.pool.query(
|
|
1419
1424
|
`ALTER TABLE credit_drains ADD COLUMN IF NOT EXISTS written_off_at BIGINT`
|
|
@@ -1774,7 +1779,9 @@ var CreditLedger = class {
|
|
|
1774
1779
|
/**
|
|
1775
1780
|
* Release expired undrawn balances, retaining readable credit records. Tempo rail or
|
|
1776
1781
|
* channel-backed credits are excluded because their collateral returns to the caller
|
|
1777
|
-
* through the channel lifecycle.
|
|
1782
|
+
* through the channel lifecycle. Card-funded credits are excluded because an expired card
|
|
1783
|
+
* balance is owed back to the card, never released to the builder: it stays on the credit,
|
|
1784
|
+
* where expiry already refuses new draws, until a card refund returns it.
|
|
1778
1785
|
*
|
|
1779
1786
|
* Pending holds prevent release: jobs accepted before expiry may still settle or release.
|
|
1780
1787
|
* Candidates are rechecked under the same parent-credit lock used by draw writes. The
|
|
@@ -1793,6 +1800,7 @@ var CreditLedger = class {
|
|
|
1793
1800
|
AND c.status = 'active'
|
|
1794
1801
|
AND c.tempo_channel_id IS NULL
|
|
1795
1802
|
AND COALESCE(c.rail, '') <> 'tempo'
|
|
1803
|
+
AND NOT (COALESCE(c.rail, '') = ANY($3::text[]))
|
|
1796
1804
|
AND NOT EXISTS (
|
|
1797
1805
|
SELECT 1 FROM credit_expiry_releases r
|
|
1798
1806
|
WHERE r.credit_id = c.credit_id AND r.reversed_at IS NULL
|
|
@@ -1803,7 +1811,7 @@ var CreditLedger = class {
|
|
|
1803
1811
|
)
|
|
1804
1812
|
ORDER BY c.expiry_ms
|
|
1805
1813
|
LIMIT $2`,
|
|
1806
|
-
[nowMs, limit]
|
|
1814
|
+
[nowMs, limit, [...CARD_FUNDED_RAILS]]
|
|
1807
1815
|
);
|
|
1808
1816
|
const released = [];
|
|
1809
1817
|
for (const candidate of candidates) {
|
|
@@ -1828,6 +1836,7 @@ var CreditLedger = class {
|
|
|
1828
1836
|
const balanceMicro = toSafeInt(credit.balance_micro);
|
|
1829
1837
|
if (expiryMs > nowMs || balanceMicro <= 0 || credit.status !== "active") return void 0;
|
|
1830
1838
|
if (credit.tempo_channel_id !== null || credit.rail === "tempo") return void 0;
|
|
1839
|
+
if (isCardFundedRail(credit.rail)) return void 0;
|
|
1831
1840
|
const pending = await this.pendingSums(client, credit.credit_id);
|
|
1832
1841
|
if (pending.micro > 0) return void 0;
|
|
1833
1842
|
const { rows: open } = await client.query(
|
|
@@ -1871,13 +1880,14 @@ var CreditLedger = class {
|
|
|
1871
1880
|
/**
|
|
1872
1881
|
* Record non-channel Bitcoin funding lots on the funding transaction. An implicit payment
|
|
1873
1882
|
* also needs a lot: its draw is initially only a hold, and failure releases the whole
|
|
1874
|
-
* balance for reclaim. Channel collateral is excluded from Bitcoin lots.
|
|
1883
|
+
* balance for reclaim. Channel collateral is excluded from Bitcoin lots. A card funding
|
|
1884
|
+
* records a lot per charge so a later refund can address the charge that bought it.
|
|
1875
1885
|
*
|
|
1876
1886
|
* A zero-sats basis still records a lot, distinguishing covered but basisless credit from a
|
|
1877
1887
|
* missing-lot coverage gap.
|
|
1878
1888
|
*/
|
|
1879
1889
|
async recordFundingLot(q, creditId, amountMicro, basis, nowMs) {
|
|
1880
|
-
if (!isNonChannelBitcoinRail(basis.rail)) return;
|
|
1890
|
+
if (!isNonChannelBitcoinRail(basis.rail) && !isCardFundedRail(basis.rail)) return;
|
|
1881
1891
|
if (basis.tempoSession || basis.x402Channel) return;
|
|
1882
1892
|
await q.query(
|
|
1883
1893
|
`INSERT INTO credit_funding_lots
|
|
@@ -3238,6 +3248,7 @@ var CreditLedger = class {
|
|
|
3238
3248
|
`credit ${args.creditId} belongs to a different caller pubkey`
|
|
3239
3249
|
);
|
|
3240
3250
|
}
|
|
3251
|
+
assertCardDrainMatchesRail(args.creditId, credit.rail, args.method);
|
|
3241
3252
|
const existing = await this.readDrain(client, args.creditId, args.drainId);
|
|
3242
3253
|
if (existing) {
|
|
3243
3254
|
if (existing.method !== args.method || JSON.stringify(existing.payout) !== JSON.stringify(args.payout)) {
|
|
@@ -3253,6 +3264,10 @@ var CreditLedger = class {
|
|
|
3253
3264
|
};
|
|
3254
3265
|
}
|
|
3255
3266
|
if (credit.status !== "active") throw terminalCreditError(args.creditId, credit.status);
|
|
3267
|
+
if (args.method === "stripe") {
|
|
3268
|
+
const blockingDrainId = await this.pendingCardRefundDrainId(client, args.creditId);
|
|
3269
|
+
if (blockingDrainId) throw cardRefundPendingConflict(args.creditId, blockingDrainId);
|
|
3270
|
+
}
|
|
3256
3271
|
const blocking = await this.blockingX402Refund(
|
|
3257
3272
|
credit.x402_channel_id,
|
|
3258
3273
|
X402_WEDGED_SETTLEMENT_STATUSES
|
|
@@ -3346,6 +3361,154 @@ var CreditLedger = class {
|
|
|
3346
3361
|
);
|
|
3347
3362
|
return rows.length > 0 ? drainRecordFromJoinRow(rows[0]) : void 0;
|
|
3348
3363
|
}
|
|
3364
|
+
/**
|
|
3365
|
+
* Store a card refund's plan or progress on its pending `stripe` drain. With `onlyIfUnset`
|
|
3366
|
+
* the first plan wins and every later caller gets that stored plan back, so concurrent
|
|
3367
|
+
* retries send one set of amounts under one set of idempotency keys. Returns what is
|
|
3368
|
+
* stored after the write; null when the drain is not a pending stripe drain with a plan.
|
|
3369
|
+
*/
|
|
3370
|
+
async saveCardRefund(args) {
|
|
3371
|
+
const { rows } = await this.pool.query(
|
|
3372
|
+
`UPDATE credit_drains SET card_refund = $3
|
|
3373
|
+
WHERE credit_id = $1 AND drain_id = $2 AND method = 'stripe' AND status = 'pending'
|
|
3374
|
+
AND ($4::boolean IS FALSE OR card_refund IS NULL)
|
|
3375
|
+
RETURNING card_refund`,
|
|
3376
|
+
[args.creditId, args.drainId, JSON.stringify(args.plan), args.onlyIfUnset === true]
|
|
3377
|
+
);
|
|
3378
|
+
if (rows.length > 0) return rows[0].card_refund;
|
|
3379
|
+
return (await this.readDrain(this.pool, args.creditId, args.drainId))?.card_refund ?? null;
|
|
3380
|
+
}
|
|
3381
|
+
/**
|
|
3382
|
+
* Expired card credits still holding balance, with no pending holds and no card refund in
|
|
3383
|
+
* progress, oldest expiry first. A credit waiting on its refund drops out of the page, so
|
|
3384
|
+
* one that can never finish does not crowd out the rest.
|
|
3385
|
+
*/
|
|
3386
|
+
async listExpiredCardCredits(args = {}) {
|
|
3387
|
+
const { rows } = await this.pool.query(
|
|
3388
|
+
`SELECT c.credit_id FROM credits c
|
|
3389
|
+
WHERE c.expiry_ms <= $1 AND c.balance_micro > 0 AND c.status = 'active'
|
|
3390
|
+
AND c.rail = ANY($3::text[])
|
|
3391
|
+
AND NOT EXISTS (
|
|
3392
|
+
SELECT 1 FROM credit_draws d WHERE d.credit_id = c.credit_id AND d.status = 'pending'
|
|
3393
|
+
)
|
|
3394
|
+
AND NOT EXISTS (
|
|
3395
|
+
SELECT 1 FROM credit_drains r
|
|
3396
|
+
WHERE r.credit_id = c.credit_id AND r.method = 'stripe' AND r.status = 'pending'
|
|
3397
|
+
)
|
|
3398
|
+
ORDER BY c.expiry_ms
|
|
3399
|
+
LIMIT $2`,
|
|
3400
|
+
[args.nowMs ?? Date.now(), args.limit ?? DEFAULT_EXPIRY_SWEEP_LIMIT, [...CARD_FUNDED_RAILS]]
|
|
3401
|
+
);
|
|
3402
|
+
return rows.map((row) => row.credit_id);
|
|
3403
|
+
}
|
|
3404
|
+
/**
|
|
3405
|
+
* Turn an expired card credit's whole balance into a pending `stripe` drain owed back to
|
|
3406
|
+
* the card, and close the credit (`refunded`) in the same transaction. Closing is what
|
|
3407
|
+
* makes this terminal: a later top-up cannot revive a balance that went back to the card,
|
|
3408
|
+
* and the builder never receives it. Returns undefined when the credit no longer
|
|
3409
|
+
* qualifies under its lock (not card, not expired, empty, holds pending, or closed).
|
|
3410
|
+
*/
|
|
3411
|
+
async openCardExpiryRefund(args) {
|
|
3412
|
+
const nowMs = args.nowMs ?? Date.now();
|
|
3413
|
+
return this.withCreditLock(args.creditId, async (client, credit) => {
|
|
3414
|
+
const balanceMicro = toSafeInt(credit.balance_micro);
|
|
3415
|
+
const expiryMs = toSafeInt(credit.expiry_ms);
|
|
3416
|
+
if (!isCardFundedRail(credit.rail) || credit.status !== "active") return void 0;
|
|
3417
|
+
if (expiryMs > nowMs || balanceMicro <= 0) return void 0;
|
|
3418
|
+
if ((await this.pendingSums(client, credit.credit_id)).micro > 0) return void 0;
|
|
3419
|
+
if (await this.pendingCardRefundDrainId(client, credit.credit_id)) return void 0;
|
|
3420
|
+
const ledgerSeq = toSafeInt(credit.last_ledger_seq) + 1;
|
|
3421
|
+
const { rows } = await client.query(
|
|
3422
|
+
`INSERT INTO credit_drains
|
|
3423
|
+
(credit_id, drain_id, method, payout, amount_micro, balance_after_micro,
|
|
3424
|
+
drained_msats, drained_native, owed_sats, lot_debits, ledger_seq, status, created_at)
|
|
3425
|
+
VALUES ($1, $2, 'stripe', '{}'::jsonb, $3, 0, $4, $5, NULL, NULL, $6, 'pending', $7)
|
|
3426
|
+
ON CONFLICT (credit_id, drain_id) DO NOTHING
|
|
3427
|
+
RETURNING *`,
|
|
3428
|
+
[
|
|
3429
|
+
credit.credit_id,
|
|
3430
|
+
cardExpiryDrainId(expiryMs),
|
|
3431
|
+
balanceMicro,
|
|
3432
|
+
toSafeInt(credit.msats_remaining),
|
|
3433
|
+
credit.native_remaining === null ? null : toSafeInt(credit.native_remaining),
|
|
3434
|
+
ledgerSeq,
|
|
3435
|
+
nowMs
|
|
3436
|
+
]
|
|
3437
|
+
);
|
|
3438
|
+
if (rows.length === 0) return void 0;
|
|
3439
|
+
await client.query(
|
|
3440
|
+
`UPDATE credits
|
|
3441
|
+
SET balance_micro = 0, last_ledger_seq = $2, msats_remaining = 0,
|
|
3442
|
+
native_remaining = CASE WHEN native_remaining IS NULL THEN NULL ELSE 0 END,
|
|
3443
|
+
status = 'refunded'
|
|
3444
|
+
WHERE credit_id = $1`,
|
|
3445
|
+
[credit.credit_id, ledgerSeq]
|
|
3446
|
+
);
|
|
3447
|
+
return drainRecordFromRow(rows[0], credit);
|
|
3448
|
+
});
|
|
3449
|
+
}
|
|
3450
|
+
/**
|
|
3451
|
+
* Freeze every active credit a disputed card payment funded (`disputed`): no draw, top-up
|
|
3452
|
+
* or drain moves its balance while the card network decides. Returns the credits this
|
|
3453
|
+
* call froze; already-frozen or closed credits are left as they are.
|
|
3454
|
+
*/
|
|
3455
|
+
async freezeCardCreditsForPaymentIntent(paymentIntentId) {
|
|
3456
|
+
const { rows } = await this.pool.query(
|
|
3457
|
+
`SELECT DISTINCT credit_id FROM credit_funding_lots
|
|
3458
|
+
WHERE funding_ref = $1 AND rail = ANY($2::text[])`,
|
|
3459
|
+
[paymentIntentId, [...CARD_FUNDED_RAILS]]
|
|
3460
|
+
);
|
|
3461
|
+
const frozen = [];
|
|
3462
|
+
for (const { credit_id } of rows) {
|
|
3463
|
+
const changed = await this.withCreditLock(credit_id, async (client, credit) => {
|
|
3464
|
+
if (credit.status !== "active") return false;
|
|
3465
|
+
await client.query(`UPDATE credits SET status = 'disputed' WHERE credit_id = $1`, [
|
|
3466
|
+
credit_id
|
|
3467
|
+
]);
|
|
3468
|
+
return true;
|
|
3469
|
+
});
|
|
3470
|
+
if (changed) frozen.push(credit_id);
|
|
3471
|
+
}
|
|
3472
|
+
return frozen;
|
|
3473
|
+
}
|
|
3474
|
+
/**
|
|
3475
|
+
* Frozen credits this PaymentIntent funded, each with every card PaymentIntent that funded
|
|
3476
|
+
* it: the sweep checks all of them with Stripe before lifting a freeze.
|
|
3477
|
+
*/
|
|
3478
|
+
async disputedCardCreditsFundedBy(paymentIntentId) {
|
|
3479
|
+
const { rows } = await this.pool.query(
|
|
3480
|
+
`SELECT l.credit_id, array_agg(DISTINCT l.funding_ref) AS refs
|
|
3481
|
+
FROM credit_funding_lots l JOIN credits c ON c.credit_id = l.credit_id
|
|
3482
|
+
WHERE c.status = 'disputed' AND l.rail = ANY($2::text[]) AND l.funding_ref IS NOT NULL
|
|
3483
|
+
AND l.credit_id IN (
|
|
3484
|
+
SELECT credit_id FROM credit_funding_lots WHERE funding_ref = $1 AND rail = ANY($2::text[])
|
|
3485
|
+
)
|
|
3486
|
+
GROUP BY l.credit_id`,
|
|
3487
|
+
[paymentIntentId, [...CARD_FUNDED_RAILS]]
|
|
3488
|
+
);
|
|
3489
|
+
return rows.map((row) => ({ creditId: row.credit_id, paymentIntentIds: row.refs }));
|
|
3490
|
+
}
|
|
3491
|
+
/**
|
|
3492
|
+
* Lift a dispute freeze: the credit is `active` and refundable again, including at expiry.
|
|
3493
|
+
* The caller decides that no top-up of the credit has an open or lost dispute. Returns
|
|
3494
|
+
* false when the credit was not frozen.
|
|
3495
|
+
*/
|
|
3496
|
+
async unfreezeCardCredit(creditId) {
|
|
3497
|
+
return this.withCreditLock(creditId, async (client, credit) => {
|
|
3498
|
+
if (credit.status !== "disputed") return false;
|
|
3499
|
+
await client.query(`UPDATE credits SET status = 'active' WHERE credit_id = $1`, [creditId]);
|
|
3500
|
+
return true;
|
|
3501
|
+
});
|
|
3502
|
+
}
|
|
3503
|
+
/** True when a funding lot books this PaymentIntent: the charge reached a credit. */
|
|
3504
|
+
async cardFundingBooked(paymentIntentId) {
|
|
3505
|
+
const { rows } = await this.pool.query(
|
|
3506
|
+
`SELECT 1 FROM credit_funding_lots
|
|
3507
|
+
WHERE funding_ref = $1 AND rail = ANY($2::text[]) LIMIT 1`,
|
|
3508
|
+
[paymentIntentId, [...CARD_FUNDED_RAILS]]
|
|
3509
|
+
);
|
|
3510
|
+
return rows.length > 0;
|
|
3511
|
+
}
|
|
3349
3512
|
/**
|
|
3350
3513
|
* One credit's undepleted funding lots, oldest first — the
|
|
3351
3514
|
* public read behind the reclaim's own arithmetic, for anything that needs
|
|
@@ -4054,6 +4217,15 @@ var CreditLedger = class {
|
|
|
4054
4217
|
* a new draw allocates against. One round-trip, since every
|
|
4055
4218
|
* caller needs the micro sum anyway.
|
|
4056
4219
|
*/
|
|
4220
|
+
async pendingCardRefundDrainId(q, creditId) {
|
|
4221
|
+
const { rows } = await q.query(
|
|
4222
|
+
`SELECT drain_id FROM credit_drains
|
|
4223
|
+
WHERE credit_id = $1 AND method = 'stripe' AND status = 'pending'
|
|
4224
|
+
ORDER BY created_at LIMIT 1`,
|
|
4225
|
+
[creditId]
|
|
4226
|
+
);
|
|
4227
|
+
return rows[0]?.drain_id;
|
|
4228
|
+
}
|
|
4057
4229
|
async pendingSums(q, creditId) {
|
|
4058
4230
|
const { rows } = await q.query(
|
|
4059
4231
|
`SELECT COALESCE(SUM(amount_micro), 0) AS micro,
|
|
@@ -4082,7 +4254,7 @@ var CreditLedger = class {
|
|
|
4082
4254
|
nativeAvailable: row.native_remaining === null ? null : live ? Math.max(0, toSafeInt(row.native_remaining) - pending.native) : 0,
|
|
4083
4255
|
expiryMs,
|
|
4084
4256
|
expired: expiryMs <= nowMs,
|
|
4085
|
-
status: row.status
|
|
4257
|
+
status: creditStatusOf(row.status),
|
|
4086
4258
|
lastLedgerSeq: toSafeInt(row.last_ledger_seq),
|
|
4087
4259
|
createdAt: toSafeInt(row.created_at),
|
|
4088
4260
|
rail: row.rail,
|
|
@@ -4111,7 +4283,12 @@ var CreditLedger = class {
|
|
|
4111
4283
|
return rows.map(x402CreditLossFromRow);
|
|
4112
4284
|
}
|
|
4113
4285
|
};
|
|
4114
|
-
var DRAIN_METHODS = [
|
|
4286
|
+
var DRAIN_METHODS = [
|
|
4287
|
+
"cashu",
|
|
4288
|
+
"x402",
|
|
4289
|
+
"tempo",
|
|
4290
|
+
"stripe"
|
|
4291
|
+
];
|
|
4115
4292
|
function isDrainMethod(value) {
|
|
4116
4293
|
return DRAIN_METHODS.includes(value);
|
|
4117
4294
|
}
|
|
@@ -4119,7 +4296,8 @@ var FUNDING_RAILS = {
|
|
|
4119
4296
|
cashu: true,
|
|
4120
4297
|
x402: true,
|
|
4121
4298
|
tempo: true,
|
|
4122
|
-
lightning: true
|
|
4299
|
+
lightning: true,
|
|
4300
|
+
stripe: true
|
|
4123
4301
|
};
|
|
4124
4302
|
function isFundingRail(rail) {
|
|
4125
4303
|
return typeof rail === "string" && Object.hasOwn(FUNDING_RAILS, rail);
|
|
@@ -4191,11 +4369,41 @@ function x402SettlementPending(creditId, refund) {
|
|
|
4191
4369
|
);
|
|
4192
4370
|
}
|
|
4193
4371
|
function terminalCreditError(creditId, status) {
|
|
4372
|
+
if (status === "refunded" || status === "disputed") {
|
|
4373
|
+
return new CreditLedgerError(
|
|
4374
|
+
"credit_closed",
|
|
4375
|
+
status === "refunded" ? `credit ${creditId} is closed: its expired card balance was refunded to the card` : `credit ${creditId} is frozen: a card payment that funded it is disputed`,
|
|
4376
|
+
{ creditStatus: status }
|
|
4377
|
+
);
|
|
4378
|
+
}
|
|
4194
4379
|
return new CreditLedgerError(
|
|
4195
4380
|
"credit_unbacked",
|
|
4196
4381
|
`credit ${creditId} is terminal (${status}); its Tempo backing finalized before the DVM was protected`
|
|
4197
4382
|
);
|
|
4198
4383
|
}
|
|
4384
|
+
function assertCardDrainMatchesRail(creditId, rail, method) {
|
|
4385
|
+
const card = isCardFundedRail(rail);
|
|
4386
|
+
if (card === (method === "stripe")) return;
|
|
4387
|
+
throw new CreditLedgerError(
|
|
4388
|
+
"rail_mismatch",
|
|
4389
|
+
card ? `credit ${creditId} was funded by card and is refunded only to that card (method stripe)` : `credit ${creditId} was not funded by card, so it cannot be refunded to one`,
|
|
4390
|
+
{ expectedRail: rail }
|
|
4391
|
+
);
|
|
4392
|
+
}
|
|
4393
|
+
var CARD_EXPIRY_DRAIN_PREFIX = "expiry-";
|
|
4394
|
+
function cardExpiryDrainId(expiryMs) {
|
|
4395
|
+
return `${CARD_EXPIRY_DRAIN_PREFIX}${expiryMs}`;
|
|
4396
|
+
}
|
|
4397
|
+
function cardRefundPendingConflict(creditId, blockingDrainId) {
|
|
4398
|
+
return new CreditLedgerError(
|
|
4399
|
+
"drain_conflict",
|
|
4400
|
+
`credit ${creditId} already has card refund ${blockingDrainId} in progress`,
|
|
4401
|
+
{ drainConflict: "card_refund_pending", blockingDrainId }
|
|
4402
|
+
);
|
|
4403
|
+
}
|
|
4404
|
+
function creditStatusOf(status) {
|
|
4405
|
+
return status === "unbacked" || status === "refunded" || status === "disputed" ? status : "active";
|
|
4406
|
+
}
|
|
4199
4407
|
function isUniqueViolation(err) {
|
|
4200
4408
|
return typeof err === "object" && err !== null && err.code === "23505";
|
|
4201
4409
|
}
|
|
@@ -4329,7 +4537,8 @@ function drainRecordFromRow(row, credit) {
|
|
|
4329
4537
|
operatorDecision: row.operator_decision ?? null,
|
|
4330
4538
|
holdReason: row.hold_reason ?? null,
|
|
4331
4539
|
provenRefundNative: row.proven_refund_native ?? null,
|
|
4332
|
-
refundPaymentProven: row.refund_payment_proven ?? false
|
|
4540
|
+
refundPaymentProven: row.refund_payment_proven ?? false,
|
|
4541
|
+
cardRefund: row.card_refund ?? null
|
|
4333
4542
|
};
|
|
4334
4543
|
}
|
|
4335
4544
|
function drainRecordFromJoinRow(row) {
|
|
@@ -4425,6 +4634,7 @@ function expiryReleaseReport(dvmId, row) {
|
|
|
4425
4634
|
|
|
4426
4635
|
export {
|
|
4427
4636
|
NON_CHANNEL_BITCOIN_RAILS,
|
|
4637
|
+
isCardFundedRail,
|
|
4428
4638
|
isNonChannelBitcoinRail,
|
|
4429
4639
|
depleteLots,
|
|
4430
4640
|
inKindDrawMsats,
|
|
@@ -4451,5 +4661,9 @@ export {
|
|
|
4451
4661
|
growthRailValue,
|
|
4452
4662
|
clipEarmark,
|
|
4453
4663
|
CreditLedgerError,
|
|
4454
|
-
x402SettlementPending
|
|
4664
|
+
x402SettlementPending,
|
|
4665
|
+
assertCardDrainMatchesRail,
|
|
4666
|
+
CARD_EXPIRY_DRAIN_PREFIX,
|
|
4667
|
+
cardExpiryDrainId,
|
|
4668
|
+
cardRefundPendingConflict
|
|
4455
4669
|
};
|
|
@@ -1,30 +1,38 @@
|
|
|
1
1
|
import {
|
|
2
|
+
CARD_EXPIRY_DRAIN_PREFIX,
|
|
2
3
|
CreditLedger,
|
|
3
4
|
CreditLedgerError,
|
|
4
5
|
DRAIN_METHODS,
|
|
5
6
|
FUNDING_RAILS,
|
|
6
7
|
allocateDrawValue,
|
|
8
|
+
assertCardDrainMatchesRail,
|
|
7
9
|
assertFundingBasis,
|
|
10
|
+
cardExpiryDrainId,
|
|
11
|
+
cardRefundPendingConflict,
|
|
8
12
|
clipEarmark,
|
|
9
13
|
growthRailValue,
|
|
10
14
|
isDrainMethod,
|
|
11
15
|
isFundingRail,
|
|
12
16
|
x402SettlementPending
|
|
13
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-SEIMPHIP.js";
|
|
14
18
|
import "./chunk-YG7G4DPZ.js";
|
|
15
19
|
import "./chunk-COYKJRMM.js";
|
|
16
20
|
import "./chunk-5UC7TOQC.js";
|
|
17
|
-
import "./chunk-
|
|
21
|
+
import "./chunk-L35WYOH2.js";
|
|
18
22
|
import "./chunk-BPYMEMQX.js";
|
|
19
23
|
import "./chunk-H3GGFEE5.js";
|
|
20
|
-
import "./chunk-
|
|
24
|
+
import "./chunk-2WWVI32F.js";
|
|
21
25
|
export {
|
|
26
|
+
CARD_EXPIRY_DRAIN_PREFIX,
|
|
22
27
|
CreditLedger,
|
|
23
28
|
CreditLedgerError,
|
|
24
29
|
DRAIN_METHODS,
|
|
25
30
|
FUNDING_RAILS,
|
|
26
31
|
allocateDrawValue,
|
|
32
|
+
assertCardDrainMatchesRail,
|
|
27
33
|
assertFundingBasis,
|
|
34
|
+
cardExpiryDrainId,
|
|
35
|
+
cardRefundPendingConflict,
|
|
28
36
|
clipEarmark,
|
|
29
37
|
growthRailValue,
|
|
30
38
|
isDrainMethod,
|