@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
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
} from "./chunk-XRLKGCAW.js";
|
|
6
6
|
import {
|
|
7
7
|
x402EarnedCeiling
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-DY5QIHJA.js";
|
|
9
9
|
import {
|
|
10
10
|
createX402FacilitatorAuthHeaders,
|
|
11
11
|
createX402FacilitatorClient,
|
|
@@ -14,13 +14,13 @@ import {
|
|
|
14
14
|
verifyWithFacilitator,
|
|
15
15
|
withPaymentDiagnostics,
|
|
16
16
|
withPaymentStage
|
|
17
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-3DGIRHP2.js";
|
|
18
18
|
import {
|
|
19
19
|
MemoryCreditLedger
|
|
20
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-HVZIITWW.js";
|
|
21
21
|
import {
|
|
22
22
|
PostgresJobStore
|
|
23
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-SQOVLAJ2.js";
|
|
24
24
|
import {
|
|
25
25
|
CASHU_MIN_LOCKTIME_SECONDS,
|
|
26
26
|
DEFAULT_CASHU_LOCK_WINDOW_SECONDS,
|
|
@@ -34,7 +34,7 @@ import {
|
|
|
34
34
|
signedRequestInput,
|
|
35
35
|
unsignedCashuLockWindowSeconds,
|
|
36
36
|
zodIssuesOf
|
|
37
|
-
} from "./chunk-
|
|
37
|
+
} from "./chunk-DLRPKEAM.js";
|
|
38
38
|
import {
|
|
39
39
|
MemoryJobStore,
|
|
40
40
|
UNCAPPABLE_ASK_TOTAL,
|
|
@@ -43,7 +43,7 @@ import {
|
|
|
43
43
|
isJobRetentionStore,
|
|
44
44
|
isReceiptIssuingStore,
|
|
45
45
|
isStreamableJobStore
|
|
46
|
-
} from "./chunk-
|
|
46
|
+
} from "./chunk-5NMHZIIU.js";
|
|
47
47
|
import {
|
|
48
48
|
ContentHoldError,
|
|
49
49
|
JobCancelledError,
|
|
@@ -84,7 +84,7 @@ import {
|
|
|
84
84
|
receiptPubkeyFromSecret,
|
|
85
85
|
toLockPubkey,
|
|
86
86
|
verifyChallenge
|
|
87
|
-
} from "./chunk-
|
|
87
|
+
} from "./chunk-EQIP6TFD.js";
|
|
88
88
|
import {
|
|
89
89
|
X402_BATCH_SETTLEMENT_SCHEME,
|
|
90
90
|
X402_DEFAULT_FACILITATOR,
|
|
@@ -103,7 +103,7 @@ import {
|
|
|
103
103
|
resolveRpcOverride,
|
|
104
104
|
usdcToMsats,
|
|
105
105
|
x402NetworkToCaip2
|
|
106
|
-
} from "./chunk-
|
|
106
|
+
} from "./chunk-LABJSMSJ.js";
|
|
107
107
|
import {
|
|
108
108
|
RAIL_REFUNDABLE,
|
|
109
109
|
canonicalRequestPath,
|
|
@@ -111,7 +111,7 @@ import {
|
|
|
111
111
|
} from "./chunk-SXL44PL5.js";
|
|
112
112
|
import {
|
|
113
113
|
createConsoleLogger
|
|
114
|
-
} from "./chunk-
|
|
114
|
+
} from "./chunk-JMTLK2SZ.js";
|
|
115
115
|
import {
|
|
116
116
|
StepCache
|
|
117
117
|
} from "./chunk-BQJIFRAZ.js";
|
|
@@ -128,8 +128,9 @@ import {
|
|
|
128
128
|
roundUsd,
|
|
129
129
|
satsToFiat,
|
|
130
130
|
validateCurrency
|
|
131
|
-
} from "./chunk-
|
|
131
|
+
} from "./chunk-UVRBD5GR.js";
|
|
132
132
|
import {
|
|
133
|
+
CARD_EXPIRY_DRAIN_PREFIX,
|
|
133
134
|
CreditLedger,
|
|
134
135
|
CreditLedgerError,
|
|
135
136
|
DRAIN_METHODS,
|
|
@@ -139,12 +140,14 @@ import {
|
|
|
139
140
|
X402_RESOLVED_SETTLEMENT_STATUSES,
|
|
140
141
|
X402_SPEND_BLOCKING_SETTLEMENT_STATUSES,
|
|
141
142
|
X402_WEDGED_SETTLEMENT_STATUSES,
|
|
143
|
+
cardExpiryDrainId,
|
|
144
|
+
isCardFundedRail,
|
|
142
145
|
isDrainMethod,
|
|
143
146
|
isFundingRail,
|
|
144
147
|
isNonChannelBitcoinRail,
|
|
145
148
|
netOwedSats,
|
|
146
149
|
x402SettlementPending
|
|
147
|
-
} from "./chunk-
|
|
150
|
+
} from "./chunk-SEIMPHIP.js";
|
|
148
151
|
import {
|
|
149
152
|
parseStoreValue
|
|
150
153
|
} from "./chunk-YG7G4DPZ.js";
|
|
@@ -163,7 +166,7 @@ import {
|
|
|
163
166
|
import {
|
|
164
167
|
isTransportError,
|
|
165
168
|
withPostgresRecoveryContext
|
|
166
|
-
} from "./chunk-
|
|
169
|
+
} from "./chunk-L35WYOH2.js";
|
|
167
170
|
import {
|
|
168
171
|
DvmError,
|
|
169
172
|
x402NetworkByCaip2
|
|
@@ -181,17 +184,17 @@ import {
|
|
|
181
184
|
installHostedTempoCloseGuard,
|
|
182
185
|
isTempoInsufficientFundsError,
|
|
183
186
|
resolveTempoNetwork
|
|
184
|
-
} from "./chunk-
|
|
187
|
+
} from "./chunk-LCNRFKP7.js";
|
|
185
188
|
import {
|
|
186
189
|
loggers,
|
|
187
190
|
pinDimension,
|
|
188
191
|
withSpan,
|
|
189
192
|
withTraceContext
|
|
190
|
-
} from "./chunk-
|
|
193
|
+
} from "./chunk-ZH5CLGO6.js";
|
|
191
194
|
import {
|
|
192
195
|
createDiagnosticCapabilityAuthority,
|
|
193
196
|
diagnosticConsole
|
|
194
|
-
} from "./chunk-
|
|
197
|
+
} from "./chunk-2WWVI32F.js";
|
|
195
198
|
|
|
196
199
|
// src/sdk/server/lock-pubkey-store.ts
|
|
197
200
|
async function initLockPubkeyTable(db) {
|
|
@@ -904,6 +907,7 @@ function stablecoinCreditInstrumentAllowed(allowOneShotStablecoin, instrument) {
|
|
|
904
907
|
}
|
|
905
908
|
|
|
906
909
|
// src/sdk/server/credit-menu.ts
|
|
910
|
+
var STRIPE_CREDIT_MIN_MICRO = 5e5;
|
|
907
911
|
function creditMenuConfigured(args) {
|
|
908
912
|
if (args.runtime === "isolate") return false;
|
|
909
913
|
if (!args.config) return false;
|
|
@@ -952,6 +956,9 @@ async function buildCreditMenu(args) {
|
|
|
952
956
|
const funding = args.funding.filter((rail) => {
|
|
953
957
|
if (rail === "tempo") return tempoMethods.length > 0;
|
|
954
958
|
if (rail === "x402") return x402Schemes.length > 0;
|
|
959
|
+
if (rail === "stripe") {
|
|
960
|
+
return args.stripeNetworkId !== void 0 && currency === "usd" && bounds.maxMicro >= STRIPE_CREDIT_MIN_MICRO;
|
|
961
|
+
}
|
|
955
962
|
if (isNonChannelBitcoinRail(rail)) return args.hasAcceptedMint === true;
|
|
956
963
|
return true;
|
|
957
964
|
});
|
|
@@ -971,7 +978,14 @@ async function buildCreditMenu(args) {
|
|
|
971
978
|
// a builder who overrides `paymentMethods` can drop x402 from `funding`
|
|
972
979
|
// while the rail stays wired, and terms for a rail the menu doesn't offer
|
|
973
980
|
// are the same misadvertisement in the other direction.
|
|
974
|
-
...funding.includes("x402") && x402Schemes.length > 0 && { x402: { schemes: x402Schemes } }
|
|
981
|
+
...funding.includes("x402") && x402Schemes.length > 0 && { x402: { schemes: x402Schemes } },
|
|
982
|
+
...funding.includes("stripe") && args.stripeNetworkId !== void 0 && {
|
|
983
|
+
stripe: {
|
|
984
|
+
network_id: args.stripeNetworkId,
|
|
985
|
+
min_micro: Math.max(bounds.minMicro, STRIPE_CREDIT_MIN_MICRO),
|
|
986
|
+
currency: "usd"
|
|
987
|
+
}
|
|
988
|
+
}
|
|
975
989
|
};
|
|
976
990
|
if (funding.includes("lightning") && args.lightningFundingMinSats !== void 0) {
|
|
977
991
|
try {
|
|
@@ -1051,7 +1065,8 @@ function toCreditTerms(menu) {
|
|
|
1051
1065
|
},
|
|
1052
1066
|
...menu.lightning_min_micro !== void 0 && {
|
|
1053
1067
|
lightning_min_micro: menu.lightning_min_micro
|
|
1054
|
-
}
|
|
1068
|
+
},
|
|
1069
|
+
...menu.stripe && { stripe: { ...menu.stripe } }
|
|
1055
1070
|
};
|
|
1056
1071
|
}
|
|
1057
1072
|
var LIGHTNING_FLOOR_FX_MARGIN = 1.15;
|
|
@@ -4257,7 +4272,7 @@ function upfrontPaymentFromDraw(draw) {
|
|
|
4257
4272
|
amount_micro: draw.amountMicro,
|
|
4258
4273
|
currency: value.creditCurrency
|
|
4259
4274
|
},
|
|
4260
|
-
...rail
|
|
4275
|
+
...rail !== null && isFundingRail(rail) ? { rail } : {},
|
|
4261
4276
|
...value.drawNative !== null && value.nativeAsset !== null && {
|
|
4262
4277
|
native_amount: value.drawNative,
|
|
4263
4278
|
native_asset: value.nativeAsset
|
|
@@ -4332,8 +4347,10 @@ function decimalsFor(methodName) {
|
|
|
4332
4347
|
switch (methodName) {
|
|
4333
4348
|
case "tempo":
|
|
4334
4349
|
return 6;
|
|
4350
|
+
case "stripe":
|
|
4351
|
+
return 2;
|
|
4335
4352
|
default:
|
|
4336
|
-
throw new Error(`mpp-units: unknown method '${methodName}' (expected 'tempo')`);
|
|
4353
|
+
throw new Error(`mpp-units: unknown method '${methodName}' (expected 'tempo' or 'stripe')`);
|
|
4337
4354
|
}
|
|
4338
4355
|
}
|
|
4339
4356
|
function msatsToMethodAmount(msats, methodName, btcUsd) {
|
|
@@ -4364,8 +4381,10 @@ function methodNativeAsset(methodName) {
|
|
|
4364
4381
|
switch (methodName) {
|
|
4365
4382
|
case "tempo":
|
|
4366
4383
|
return "usdc";
|
|
4384
|
+
case "stripe":
|
|
4385
|
+
return "usd-cents";
|
|
4367
4386
|
default:
|
|
4368
|
-
throw new Error(`mpp-units: unknown method '${methodName}' (expected 'tempo')`);
|
|
4387
|
+
throw new Error(`mpp-units: unknown method '${methodName}' (expected 'tempo' or 'stripe')`);
|
|
4369
4388
|
}
|
|
4370
4389
|
}
|
|
4371
4390
|
function methodNativeAmountFromDecimal(amountStr, methodName) {
|
|
@@ -4825,6 +4844,235 @@ async function verifyUpfrontPayment(opts) {
|
|
|
4825
4844
|
}
|
|
4826
4845
|
return result;
|
|
4827
4846
|
}
|
|
4847
|
+
async function verifyCardFunding(opts) {
|
|
4848
|
+
return withPaymentDiagnostics(() => verifyCardFundingInScope(opts));
|
|
4849
|
+
}
|
|
4850
|
+
async function verifyCardFundingInScope(opts) {
|
|
4851
|
+
const rail = opts.stripeCredit;
|
|
4852
|
+
const { fundOnly, amountMicro } = opts;
|
|
4853
|
+
const resourcePath = opts.resourcePath ?? "/v1/credit";
|
|
4854
|
+
if (!rail || !opts.creditLedger) {
|
|
4855
|
+
return creditRefusal("payment_invalid", "This DVM does not accept card funding.", {
|
|
4856
|
+
status: 409,
|
|
4857
|
+
hint: "Fund the credit over one of the rails listed in credit.funding on /v1/info."
|
|
4858
|
+
});
|
|
4859
|
+
}
|
|
4860
|
+
if (amountMicro % 1e4 !== 0) {
|
|
4861
|
+
return creditRefusal(
|
|
4862
|
+
"payment_invalid",
|
|
4863
|
+
`A card top-up must be a whole number of cents; ${amountMicro} micro-USD is not.`,
|
|
4864
|
+
{ status: 400, hint: "Round fund.amount_micro to a multiple of 10000 (one cent) and sign again." }
|
|
4865
|
+
);
|
|
4866
|
+
}
|
|
4867
|
+
const challenge = async () => [
|
|
4868
|
+
await rail.issueChallenge({
|
|
4869
|
+
amountMicro,
|
|
4870
|
+
resourcePath,
|
|
4871
|
+
expires: new Date(Date.now() + UPFRONT_CHALLENGE_TTL_MS).toISOString()
|
|
4872
|
+
})
|
|
4873
|
+
];
|
|
4874
|
+
const credential = typeof opts.mppCredential === "string" ? opts.mppCredential : void 0;
|
|
4875
|
+
if (!credential) {
|
|
4876
|
+
return {
|
|
4877
|
+
paidMsats: 0,
|
|
4878
|
+
receivedProofs: [],
|
|
4879
|
+
error: {
|
|
4880
|
+
body: {
|
|
4881
|
+
error: "payment_required",
|
|
4882
|
+
amount_micro: amountMicro,
|
|
4883
|
+
currency: "usd",
|
|
4884
|
+
display: "This top-up needs a card payment approved in Link.",
|
|
4885
|
+
hint: 'Build an MPP credential for the attached method="stripe" challenge from the shared payment token (spt_\u2026) your human approved, set fund.commitment to its SHA-256, and resend this fund request, freshly signed, with the credential in the Authorization header.'
|
|
4886
|
+
},
|
|
4887
|
+
status: 402,
|
|
4888
|
+
mppChallenges: await challenge()
|
|
4889
|
+
}
|
|
4890
|
+
};
|
|
4891
|
+
}
|
|
4892
|
+
if (fundingCommitment(credential) !== fundOnly.commitment) {
|
|
4893
|
+
logCreditRejected("fund_commitment_mismatch", fundOnly.creditId);
|
|
4894
|
+
return creditRefusal(
|
|
4895
|
+
"funding_commitment_mismatch",
|
|
4896
|
+
"fund.commitment does not match the SHA-256 of the attached funding artifact."
|
|
4897
|
+
);
|
|
4898
|
+
}
|
|
4899
|
+
const refusal = crossRailRefusal(await opts.creditLedger.get(fundOnly.creditId), {
|
|
4900
|
+
creditId: fundOnly.creditId,
|
|
4901
|
+
binding: { rail: "stripe" }
|
|
4902
|
+
});
|
|
4903
|
+
if (refusal) return refusal;
|
|
4904
|
+
const charged = await withPaymentStage(
|
|
4905
|
+
"verifier_broadcast",
|
|
4906
|
+
() => rail.charge(credential, { amountMicro, resourcePath })
|
|
4907
|
+
);
|
|
4908
|
+
if (!charged.ok) {
|
|
4909
|
+
logCreditRejected(`stripe_${charged.reason}`, fundOnly.creditId, cardLogFields(charged));
|
|
4910
|
+
return cardChargeRefusal(charged, await challenge());
|
|
4911
|
+
}
|
|
4912
|
+
const ledgerCtx = {
|
|
4913
|
+
ledger: opts.creditLedger,
|
|
4914
|
+
processedPayments: opts.processedPayments,
|
|
4915
|
+
enqueueCreditDeposit: opts.enqueueCreditDeposit,
|
|
4916
|
+
priceFiat: { currency: "usd", amountMicro },
|
|
4917
|
+
// A card charge is valued in cents, never through a sats quote.
|
|
4918
|
+
requiredMsats: 0,
|
|
4919
|
+
callerId: opts.callerPubkey ?? opts.requesterId ?? "anonymous",
|
|
4920
|
+
hostingAdmissionId: opts.hostingAdmissionId,
|
|
4921
|
+
hostingPaymentArtifactHash: opts.hostingPaymentArtifactHash,
|
|
4922
|
+
dvmId: opts.dvmId ?? "",
|
|
4923
|
+
fundOnly,
|
|
4924
|
+
drawBasis: false,
|
|
4925
|
+
ttlMs: opts.creditTtlMs ?? IMPLICIT_CREDIT_TTL_MS
|
|
4926
|
+
};
|
|
4927
|
+
const cents = amountMicro / 1e4;
|
|
4928
|
+
let credited;
|
|
4929
|
+
try {
|
|
4930
|
+
credited = await commitMarkedFunding(ledgerCtx, {
|
|
4931
|
+
rail: "stripe",
|
|
4932
|
+
paymentId: charged.paymentIntentId,
|
|
4933
|
+
paidMsats: 0,
|
|
4934
|
+
fundedMicro: amountMicro,
|
|
4935
|
+
nativeAmount: cents,
|
|
4936
|
+
nativeAsset: "usd-cents"
|
|
4937
|
+
});
|
|
4938
|
+
} catch (err) {
|
|
4939
|
+
const mapped = mapLedgerCommitError(err, void 0, true);
|
|
4940
|
+
if (mapped) {
|
|
4941
|
+
logCredentialRejected("stripe", "upfront", err);
|
|
4942
|
+
return mapped.error?.body.error === "replay_rejected" ? cardReplayRefusal(mapped) : mapped;
|
|
4943
|
+
}
|
|
4944
|
+
logCreditRejected("stripe_booking_failed", fundOnly.creditId, {
|
|
4945
|
+
payment_intent: charged.paymentIntentId
|
|
4946
|
+
});
|
|
4947
|
+
return {
|
|
4948
|
+
paidMsats: 0,
|
|
4949
|
+
receivedProofs: [],
|
|
4950
|
+
error: {
|
|
4951
|
+
body: paymentErrorBody("payment_invalid", {
|
|
4952
|
+
message: "The card was charged but the credit could not be recorded.",
|
|
4953
|
+
display: "Your card was charged, but the credit wasn't recorded yet. Sending the same request again will record it without charging again.",
|
|
4954
|
+
hint: "Resend this exact fund request with the same credential and fund_id (re-sign it if the signature has expired). Do not build a new credential: that would be a second charge.",
|
|
4955
|
+
retryable: true,
|
|
4956
|
+
extra: { rail: "stripe", reason: "booking_failed" }
|
|
4957
|
+
}),
|
|
4958
|
+
status: 503
|
|
4959
|
+
}
|
|
4960
|
+
};
|
|
4961
|
+
}
|
|
4962
|
+
return withDrawBasis(
|
|
4963
|
+
{
|
|
4964
|
+
paidMsats: 0,
|
|
4965
|
+
paymentRail: "stripe",
|
|
4966
|
+
receivedProofs: [],
|
|
4967
|
+
paymentTxHash: charged.paymentIntentId,
|
|
4968
|
+
nativeAmount: cents,
|
|
4969
|
+
nativeAsset: "usd-cents"
|
|
4970
|
+
},
|
|
4971
|
+
ledgerCtx,
|
|
4972
|
+
credited
|
|
4973
|
+
);
|
|
4974
|
+
}
|
|
4975
|
+
function cardLogFields(outcome) {
|
|
4976
|
+
return {
|
|
4977
|
+
message: outcome.message,
|
|
4978
|
+
...outcome.stripeError?.type && { stripe_error_type: outcome.stripeError.type },
|
|
4979
|
+
...outcome.stripeError?.code && { stripe_error_code: outcome.stripeError.code },
|
|
4980
|
+
...outcome.stripeError?.declineCode && { decline_code: outcome.stripeError.declineCode },
|
|
4981
|
+
...outcome.paymentIntentId && { payment_intent: outcome.paymentIntentId }
|
|
4982
|
+
};
|
|
4983
|
+
}
|
|
4984
|
+
function cardChargeRefusal(outcome, challenges) {
|
|
4985
|
+
const newApproval = "Ask your human to approve a new top-up in Link for at least this amount, then build a credential from the new shared payment token for the challenge attached to this response, and resend the fund request, freshly signed.";
|
|
4986
|
+
const copy = {
|
|
4987
|
+
invalid_credential: {
|
|
4988
|
+
status: 402,
|
|
4989
|
+
display: "The card payment couldn't be verified, so nothing was charged.",
|
|
4990
|
+
hint: "Build a new credential for the challenge attached to this response, from a shared payment token that hasn't been charged, and resend the fund request, freshly signed.",
|
|
4991
|
+
retryable: false,
|
|
4992
|
+
challenges
|
|
4993
|
+
},
|
|
4994
|
+
declined: {
|
|
4995
|
+
status: 402,
|
|
4996
|
+
display: "The card was declined, so nothing was charged and no credit was added.",
|
|
4997
|
+
hint: `${newApproval} A different card is more likely to work.`,
|
|
4998
|
+
retryable: false,
|
|
4999
|
+
challenges
|
|
5000
|
+
},
|
|
5001
|
+
requires_action: {
|
|
5002
|
+
status: 402,
|
|
5003
|
+
display: "The card needs an extra verification step that can't be completed here, so no credit was added.",
|
|
5004
|
+
hint: "Ask your human to approve a new top-up in Link with a card that doesn't require an extra authentication step, then fund again with the new shared payment token.",
|
|
5005
|
+
retryable: false,
|
|
5006
|
+
challenges
|
|
5007
|
+
},
|
|
5008
|
+
token_unusable: {
|
|
5009
|
+
status: 402,
|
|
5010
|
+
display: "The card approval can't be used for this top-up (it was already used, is for a smaller amount, or has lapsed), so nothing was charged.",
|
|
5011
|
+
hint: `If you already sent this shared payment token in an earlier fund request that got no answer, resend that same request (same credential and fund_id) first: it may have been charged, and the resend records it without charging again. Only if no earlier request used this token: ${newApproval.charAt(0).toLowerCase()}${newApproval.slice(1)} An approval can be charged only once, up to the amount it was approved for.`,
|
|
5012
|
+
retryable: false,
|
|
5013
|
+
challenges
|
|
5014
|
+
},
|
|
5015
|
+
// Charged, but not as this top-up: never booked, and a resend would not change it.
|
|
5016
|
+
mismatch: {
|
|
5017
|
+
status: 502,
|
|
5018
|
+
display: "The card processor reported a charge that doesn't match this top-up, so no credit was added.",
|
|
5019
|
+
hint: "Don't retry with this approval. Tell your human the top-up failed and give the provider the payment_intent reference so they can check the charge.",
|
|
5020
|
+
retryable: false,
|
|
5021
|
+
challenges: []
|
|
5022
|
+
},
|
|
5023
|
+
// Nothing reached Stripe and the approval is unused, so the same credential still works.
|
|
5024
|
+
unavailable: {
|
|
5025
|
+
status: 503,
|
|
5026
|
+
display: "The service couldn't record the payment attempt, so this request did not charge the card and no credit was added.",
|
|
5027
|
+
hint: "Resend this exact fund request with the same credential and fund_id (re-sign it if the signature has expired), within five minutes of the original request. This request charged nothing. If an earlier request with this credential got no answer, it may have charged the card; resending records that charge instead of charging again. If resends keep failing, stop and tell your human.",
|
|
5028
|
+
retryable: true,
|
|
5029
|
+
challenges: []
|
|
5030
|
+
},
|
|
5031
|
+
// No fresh challenge: a new challenge would mean a second PaymentIntent.
|
|
5032
|
+
indeterminate: {
|
|
5033
|
+
status: 503,
|
|
5034
|
+
display: "The card processor didn't confirm the payment. The card may or may not have been charged; no credit was added yet.",
|
|
5035
|
+
hint: "Resend this exact fund request with the same credential and fund_id (re-sign it if the signature has expired), within five minutes of the original request. If the earlier attempt was charged and its answer was lost, the resend records that charge instead of charging again. Do not build a new credential from the same approval. If resends keep failing, stop and tell your human; the provider can trace any charge from its records.",
|
|
5036
|
+
retryable: true,
|
|
5037
|
+
challenges: []
|
|
5038
|
+
}
|
|
5039
|
+
}[outcome.reason];
|
|
5040
|
+
return {
|
|
5041
|
+
paidMsats: 0,
|
|
5042
|
+
receivedProofs: [],
|
|
5043
|
+
error: {
|
|
5044
|
+
body: paymentErrorBody("payment_invalid", {
|
|
5045
|
+
message: `Card funding refused (${outcome.reason}): ${outcome.message}.`,
|
|
5046
|
+
display: copy.display,
|
|
5047
|
+
hint: copy.hint,
|
|
5048
|
+
retryable: copy.retryable,
|
|
5049
|
+
extra: {
|
|
5050
|
+
rail: "stripe",
|
|
5051
|
+
reason: outcome.reason,
|
|
5052
|
+
...outcome.stripeError?.code && { stripe_error_code: outcome.stripeError.code },
|
|
5053
|
+
...outcome.stripeError?.declineCode && { decline_code: outcome.stripeError.declineCode },
|
|
5054
|
+
...outcome.reason === "mismatch" && outcome.paymentIntentId && { payment_intent: outcome.paymentIntentId }
|
|
5055
|
+
}
|
|
5056
|
+
}),
|
|
5057
|
+
status: copy.status,
|
|
5058
|
+
...copy.challenges.length > 0 && { mppChallenges: copy.challenges }
|
|
5059
|
+
}
|
|
5060
|
+
};
|
|
5061
|
+
}
|
|
5062
|
+
function cardReplayRefusal(mapped) {
|
|
5063
|
+
if (!mapped.error) return mapped;
|
|
5064
|
+
return {
|
|
5065
|
+
...mapped,
|
|
5066
|
+
error: {
|
|
5067
|
+
...mapped.error,
|
|
5068
|
+
body: {
|
|
5069
|
+
...mapped.error.body,
|
|
5070
|
+
display: "This card payment was already used to add credit, so it wasn't charged or added again.",
|
|
5071
|
+
hint: "Don't resend this credential. To add more credit, ask your human to approve a new top-up in Link and fund with the new shared payment token under a new fund_id."
|
|
5072
|
+
}
|
|
5073
|
+
}
|
|
5074
|
+
};
|
|
5075
|
+
}
|
|
4828
5076
|
async function verifyUpfrontPaymentInScope(opts) {
|
|
4829
5077
|
const {
|
|
4830
5078
|
cashuToken,
|
|
@@ -5181,7 +5429,7 @@ async function verifyUpfrontPaymentInScope(opts) {
|
|
|
5181
5429
|
x402Requirements
|
|
5182
5430
|
});
|
|
5183
5431
|
}
|
|
5184
|
-
const { verifyX402Payment } = await import("./x402-
|
|
5432
|
+
const { verifyX402Payment } = await import("./x402-MFBRORHG.js");
|
|
5185
5433
|
const receipt = await withPaymentStage("verifier_broadcast", () => verifyX402Payment(
|
|
5186
5434
|
x402Payment,
|
|
5187
5435
|
x402Config,
|
|
@@ -5873,6 +6121,11 @@ var PAYMENT_ERROR_COPY = {
|
|
|
5873
6121
|
hint: "Do not retry, top up, or drain this credit. Pay per call or open a separate prepaid balance.",
|
|
5874
6122
|
retryable: false
|
|
5875
6123
|
},
|
|
6124
|
+
credit_closed: {
|
|
6125
|
+
display: "This card-funded balance is closed: either its expired balance was refunded to the card, or a dispute on the card payment froze it. It can't pay for jobs, take a top-up, or be drained.",
|
|
6126
|
+
hint: "Do not retry with this credit_id. Pay per call, or open a new prepaid balance by funding without a credit_id.",
|
|
6127
|
+
retryable: false
|
|
6128
|
+
},
|
|
5876
6129
|
credit_not_found: {
|
|
5877
6130
|
display: "No prepaid balance exists at this service under the referenced credit. Nothing was charged.",
|
|
5878
6131
|
hint: 'Check the credit_id, or open one at POST /v1/credit (op "fund") before drawing on it.',
|
|
@@ -6237,7 +6490,7 @@ async function verifyIncomingPaymentInScope(body, opts, snapshot) {
|
|
|
6237
6490
|
snapshot
|
|
6238
6491
|
});
|
|
6239
6492
|
}
|
|
6240
|
-
const { verifyX402Payment } = await import("./x402-
|
|
6493
|
+
const { verifyX402Payment } = await import("./x402-MFBRORHG.js");
|
|
6241
6494
|
const receipt = await withPaymentStage("verifier_broadcast", () => verifyX402Payment(
|
|
6242
6495
|
body.content.x402_payment,
|
|
6243
6496
|
opts.x402Config,
|
|
@@ -6689,7 +6942,7 @@ async function processIncomingPaymentInScope(job, body, opts) {
|
|
|
6689
6942
|
try {
|
|
6690
6943
|
const requiredMsats = job.pendingPaymentMsats ?? 0;
|
|
6691
6944
|
const requiredUsdcMicro = job.pendingX402AmountUsdcMicro !== void 0 ? BigInt(job.pendingX402AmountUsdcMicro) : BigInt(msatsToUsdc(requiredMsats, rate));
|
|
6692
|
-
const { verifyX402Payment } = await import("./x402-
|
|
6945
|
+
const { verifyX402Payment } = await import("./x402-MFBRORHG.js");
|
|
6693
6946
|
const receipt = await withPaymentStage("verifier_broadcast", () => verifyX402Payment(
|
|
6694
6947
|
body.content.x402_payment,
|
|
6695
6948
|
opts.x402Config,
|
|
@@ -7975,7 +8228,7 @@ function mapLedgerCommitError(err, x402Requirements, paymentCommitted = false) {
|
|
|
7975
8228
|
return void 0;
|
|
7976
8229
|
}
|
|
7977
8230
|
function creditLedgerErrorResult(err, x402Requirements, funding, paymentCommitted = false) {
|
|
7978
|
-
const code = err.code === "insufficient_credit" || err.code === "credit_expired" || err.code === "credit_unbacked" || err.code === "credit_not_found" || err.code === "tempo_channel_closing" || err.code === "settlement_pending" || err.code === "draw_conflict" ? err.code : "payment_invalid";
|
|
8231
|
+
const code = err.code === "insufficient_credit" || err.code === "credit_expired" || err.code === "credit_unbacked" || err.code === "credit_closed" || err.code === "credit_not_found" || err.code === "tempo_channel_closing" || err.code === "settlement_pending" || err.code === "draw_conflict" ? err.code : "payment_invalid";
|
|
7979
8232
|
if (code === "payment_invalid") logCreditRejected(err.code, void 0);
|
|
7980
8233
|
const extra = {};
|
|
7981
8234
|
if (err.details.availableMicro !== void 0) {
|
|
@@ -8485,6 +8738,401 @@ async function drawFundingReceiptFor(ledger, issuer, funding, currency) {
|
|
|
8485
8738
|
return isDrawFundingReceipt(saved) ? saved : void 0;
|
|
8486
8739
|
}
|
|
8487
8740
|
|
|
8741
|
+
// src/sdk/server/stripe-drain-leg.ts
|
|
8742
|
+
import { createHash as createHash7 } from "crypto";
|
|
8743
|
+
|
|
8744
|
+
// src/sdk/server/stripe-refund-api.ts
|
|
8745
|
+
var StripeApiError = class extends Error {
|
|
8746
|
+
status;
|
|
8747
|
+
type;
|
|
8748
|
+
code;
|
|
8749
|
+
constructor(status, codes) {
|
|
8750
|
+
super(status === 0 ? "Stripe could not be reached" : `Stripe answered HTTP ${status}`);
|
|
8751
|
+
this.name = "StripeApiError";
|
|
8752
|
+
this.status = status;
|
|
8753
|
+
if (codes.type) this.type = codes.type;
|
|
8754
|
+
if (codes.code) this.code = codes.code;
|
|
8755
|
+
}
|
|
8756
|
+
/** Stripe looked at the request and refused it; repeating it unchanged will not help. */
|
|
8757
|
+
get rejected() {
|
|
8758
|
+
return this.status >= 400 && this.status < 500 && this.status !== 409 && this.status !== 429;
|
|
8759
|
+
}
|
|
8760
|
+
};
|
|
8761
|
+
function createStripeRefundApi(opts) {
|
|
8762
|
+
const apiBase = opts.apiBase ?? "https://api.stripe.com";
|
|
8763
|
+
const fetchImpl = opts.fetch ?? fetch;
|
|
8764
|
+
const auth = `Basic ${Buffer.from(`${opts.secretKey}:`).toString("base64")}`;
|
|
8765
|
+
const call = async (method, path, args = {}) => {
|
|
8766
|
+
const url = `${apiBase}${path}${args.query ? `?${args.query.toString()}` : ""}`;
|
|
8767
|
+
let response;
|
|
8768
|
+
try {
|
|
8769
|
+
response = await fetchImpl(url, {
|
|
8770
|
+
method,
|
|
8771
|
+
headers: {
|
|
8772
|
+
Authorization: auth,
|
|
8773
|
+
...args.form && { "Content-Type": "application/x-www-form-urlencoded" },
|
|
8774
|
+
...args.idempotencyKey && { "Idempotency-Key": args.idempotencyKey }
|
|
8775
|
+
},
|
|
8776
|
+
...args.form && { body: args.form }
|
|
8777
|
+
});
|
|
8778
|
+
} catch {
|
|
8779
|
+
throw new StripeApiError(0, {});
|
|
8780
|
+
}
|
|
8781
|
+
const body = await response.json().catch(() => ({}));
|
|
8782
|
+
if (!response.ok) {
|
|
8783
|
+
const error = body.error ?? {};
|
|
8784
|
+
throw new StripeApiError(response.status, {
|
|
8785
|
+
type: enumField(error.type),
|
|
8786
|
+
code: enumField(error.code)
|
|
8787
|
+
});
|
|
8788
|
+
}
|
|
8789
|
+
return body;
|
|
8790
|
+
};
|
|
8791
|
+
return {
|
|
8792
|
+
async retrieveCharge(paymentIntentId) {
|
|
8793
|
+
const query = new URLSearchParams([["expand[]", "latest_charge.balance_transaction"]]);
|
|
8794
|
+
const pi = await call("GET", `/v1/payment_intents/${encodeURIComponent(paymentIntentId)}`, {
|
|
8795
|
+
query
|
|
8796
|
+
});
|
|
8797
|
+
const charge = isRecord2(pi.latest_charge) ? pi.latest_charge : void 0;
|
|
8798
|
+
const txn = charge && isRecord2(charge.balance_transaction) ? charge.balance_transaction : void 0;
|
|
8799
|
+
return {
|
|
8800
|
+
paymentIntentId,
|
|
8801
|
+
status: stringOr(pi.status, "unknown"),
|
|
8802
|
+
amount: integerOr(pi.amount, 0),
|
|
8803
|
+
currency: stringOr(pi.currency, ""),
|
|
8804
|
+
amountRefunded: integerOr(charge?.amount_refunded, 0),
|
|
8805
|
+
disputed: charge?.disputed === true,
|
|
8806
|
+
fee: txn && Number.isSafeInteger(txn.fee) && Number.isSafeInteger(txn.amount) && txn.amount > 0 ? { fee: txn.fee, settledAmount: txn.amount } : null
|
|
8807
|
+
};
|
|
8808
|
+
},
|
|
8809
|
+
async listRefunds(paymentIntentId) {
|
|
8810
|
+
const query = new URLSearchParams({ payment_intent: paymentIntentId, limit: "100" });
|
|
8811
|
+
const page = await call("GET", "/v1/refunds", { query });
|
|
8812
|
+
return arrayOf(page.data).map(refundFrom);
|
|
8813
|
+
},
|
|
8814
|
+
async createRefund(args) {
|
|
8815
|
+
const form = new URLSearchParams({
|
|
8816
|
+
payment_intent: args.paymentIntentId,
|
|
8817
|
+
amount: String(args.amount)
|
|
8818
|
+
});
|
|
8819
|
+
for (const [key, value] of Object.entries(args.metadata)) form.set(`metadata[${key}]`, value);
|
|
8820
|
+
return refundFrom(
|
|
8821
|
+
await call("POST", "/v1/refunds", { form, idempotencyKey: args.idempotencyKey })
|
|
8822
|
+
);
|
|
8823
|
+
},
|
|
8824
|
+
async retrieveRefund(refundId) {
|
|
8825
|
+
return refundFrom(await call("GET", `/v1/refunds/${encodeURIComponent(refundId)}`));
|
|
8826
|
+
},
|
|
8827
|
+
async listDisputes(args) {
|
|
8828
|
+
const query = new URLSearchParams({ "created[gte]": String(args.createdGte), limit: "100" });
|
|
8829
|
+
if (args.startingAfter) query.set("starting_after", args.startingAfter);
|
|
8830
|
+
const page = await call("GET", "/v1/disputes", { query });
|
|
8831
|
+
return {
|
|
8832
|
+
disputes: arrayOf(page.data).map((d) => ({
|
|
8833
|
+
id: stringOr(d.id, ""),
|
|
8834
|
+
paymentIntentId: typeof d.payment_intent === "string" ? d.payment_intent : null,
|
|
8835
|
+
status: stringOr(d.status, "unknown"),
|
|
8836
|
+
created: integerOr(d.created, 0)
|
|
8837
|
+
})),
|
|
8838
|
+
hasMore: page.has_more === true
|
|
8839
|
+
};
|
|
8840
|
+
},
|
|
8841
|
+
async listDisputesForPaymentIntent(paymentIntentId) {
|
|
8842
|
+
const query = new URLSearchParams({ payment_intent: paymentIntentId, limit: "100" });
|
|
8843
|
+
const page = await call("GET", "/v1/disputes", { query });
|
|
8844
|
+
return {
|
|
8845
|
+
disputes: arrayOf(page.data).map((d) => ({
|
|
8846
|
+
id: stringOr(d.id, ""),
|
|
8847
|
+
paymentIntentId: typeof d.payment_intent === "string" ? d.payment_intent : null,
|
|
8848
|
+
status: stringOr(d.status, "unknown"),
|
|
8849
|
+
created: integerOr(d.created, 0)
|
|
8850
|
+
})),
|
|
8851
|
+
hasMore: page.has_more === true
|
|
8852
|
+
};
|
|
8853
|
+
},
|
|
8854
|
+
async searchPaymentIntentsByMetadata(key, value) {
|
|
8855
|
+
if (!/^[A-Za-z0-9_-]+$/.test(key) || !/^[A-Za-z0-9_-]+$/.test(value)) return [];
|
|
8856
|
+
const query = new URLSearchParams({ query: `metadata['${key}']:'${value}'`, limit: "20" });
|
|
8857
|
+
const page = await call("GET", "/v1/payment_intents/search", { query });
|
|
8858
|
+
return arrayOf(page.data).map((pi) => ({
|
|
8859
|
+
id: stringOr(pi.id, ""),
|
|
8860
|
+
status: stringOr(pi.status, "unknown"),
|
|
8861
|
+
amount: integerOr(pi.amount, 0),
|
|
8862
|
+
currency: stringOr(pi.currency, ""),
|
|
8863
|
+
created: integerOr(pi.created, 0)
|
|
8864
|
+
}));
|
|
8865
|
+
}
|
|
8866
|
+
};
|
|
8867
|
+
}
|
|
8868
|
+
function refundFrom(body) {
|
|
8869
|
+
const metadata = {};
|
|
8870
|
+
if (isRecord2(body.metadata)) {
|
|
8871
|
+
for (const [key, value] of Object.entries(body.metadata)) {
|
|
8872
|
+
if (typeof value === "string") metadata[key] = value;
|
|
8873
|
+
}
|
|
8874
|
+
}
|
|
8875
|
+
return {
|
|
8876
|
+
id: stringOr(body.id, ""),
|
|
8877
|
+
status: stringOr(body.status, "unknown"),
|
|
8878
|
+
amount: integerOr(body.amount, 0),
|
|
8879
|
+
paymentIntentId: typeof body.payment_intent === "string" ? body.payment_intent : null,
|
|
8880
|
+
metadata
|
|
8881
|
+
};
|
|
8882
|
+
}
|
|
8883
|
+
function enumField(value) {
|
|
8884
|
+
return typeof value === "string" && /^[a-z0-9_]{1,64}$/.test(value) ? value : void 0;
|
|
8885
|
+
}
|
|
8886
|
+
function isRecord2(value) {
|
|
8887
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
8888
|
+
}
|
|
8889
|
+
function arrayOf(value) {
|
|
8890
|
+
return Array.isArray(value) ? value.filter(isRecord2) : [];
|
|
8891
|
+
}
|
|
8892
|
+
function stringOr(value, fallback) {
|
|
8893
|
+
return typeof value === "string" ? value : fallback;
|
|
8894
|
+
}
|
|
8895
|
+
function integerOr(value, fallback) {
|
|
8896
|
+
return Number.isSafeInteger(value) ? value : fallback;
|
|
8897
|
+
}
|
|
8898
|
+
|
|
8899
|
+
// src/sdk/server/stripe-drain-leg.ts
|
|
8900
|
+
var MICRO_PER_CENT = 1e4;
|
|
8901
|
+
async function runStripeDrain(deps, drain) {
|
|
8902
|
+
if (drain.method !== "stripe" || drain.status !== "pending") {
|
|
8903
|
+
return { drain, completed: false };
|
|
8904
|
+
}
|
|
8905
|
+
const save = deps.ledger.saveCardRefund?.bind(deps.ledger);
|
|
8906
|
+
if (!save) throw new Error("this credit ledger cannot record card refunds");
|
|
8907
|
+
let plan = drain.cardRefund ?? null;
|
|
8908
|
+
if (!plan) {
|
|
8909
|
+
try {
|
|
8910
|
+
const fresh = await planCardRefund(deps, drain);
|
|
8911
|
+
plan = await save({
|
|
8912
|
+
creditId: drain.creditId,
|
|
8913
|
+
drainId: drain.drainId,
|
|
8914
|
+
plan: fresh,
|
|
8915
|
+
onlyIfUnset: true
|
|
8916
|
+
});
|
|
8917
|
+
} catch (err) {
|
|
8918
|
+
logPending(drain, { stage: "plan", error_code: failureCode(err) });
|
|
8919
|
+
return { drain, completed: false };
|
|
8920
|
+
}
|
|
8921
|
+
if (!plan) return { drain: await reread(deps, drain), completed: false };
|
|
8922
|
+
}
|
|
8923
|
+
plan = structuredClone(plan);
|
|
8924
|
+
for (const entry of plan.entries) {
|
|
8925
|
+
if (entry.amountCents <= 0 || entry.status === "succeeded") continue;
|
|
8926
|
+
const status = await advanceRefund(deps.stripe, drain, entry);
|
|
8927
|
+
if (status !== "succeeded") {
|
|
8928
|
+
logPending(drain, {
|
|
8929
|
+
stage: "refund",
|
|
8930
|
+
payment_intent: entry.paymentIntentId,
|
|
8931
|
+
refund_status: entry.status,
|
|
8932
|
+
error_code: entry.lastError ?? void 0
|
|
8933
|
+
});
|
|
8934
|
+
}
|
|
8935
|
+
}
|
|
8936
|
+
const stored = await save({ creditId: drain.creditId, drainId: drain.drainId, plan });
|
|
8937
|
+
if (stored) plan = stored;
|
|
8938
|
+
if (!refundsFinished(plan)) {
|
|
8939
|
+
return { drain: { ...await reread(deps, drain), cardRefund: plan }, completed: false };
|
|
8940
|
+
}
|
|
8941
|
+
const result = await commitTerminalDrain(
|
|
8942
|
+
deps.drainReport,
|
|
8943
|
+
(tx) => deps.ledger.markDrainSent({
|
|
8944
|
+
creditId: drain.creditId,
|
|
8945
|
+
drainId: drain.drainId,
|
|
8946
|
+
sentRef: cardRefundSentRef(plan),
|
|
8947
|
+
tx
|
|
8948
|
+
})
|
|
8949
|
+
);
|
|
8950
|
+
let record = result.drain;
|
|
8951
|
+
if (!result.replayed && deps.receiptIssuer) {
|
|
8952
|
+
const receipt = deps.receiptIssuer.issueDrain({
|
|
8953
|
+
creditId: record.creditId,
|
|
8954
|
+
drainId: record.drainId,
|
|
8955
|
+
event: "sent",
|
|
8956
|
+
method: record.method,
|
|
8957
|
+
amountMicro: record.amountMicro,
|
|
8958
|
+
balanceAfterMicro: record.balanceAfterMicro,
|
|
8959
|
+
ledgerSeq: record.ledgerSeq,
|
|
8960
|
+
callerPubkey: record.callerPubkey
|
|
8961
|
+
});
|
|
8962
|
+
await deps.ledger.appendDrainReceipt({
|
|
8963
|
+
creditId: record.creditId,
|
|
8964
|
+
drainId: record.drainId,
|
|
8965
|
+
receipt
|
|
8966
|
+
});
|
|
8967
|
+
record = { ...record, receipts: [...record.receipts, receipt] };
|
|
8968
|
+
}
|
|
8969
|
+
return { drain: record, completed: !result.replayed };
|
|
8970
|
+
}
|
|
8971
|
+
function proportionalFeeShare(grossCents, fee) {
|
|
8972
|
+
const share = Math.floor(
|
|
8973
|
+
(2 * grossCents * fee.fee + fee.settledAmount) / (2 * fee.settledAmount)
|
|
8974
|
+
);
|
|
8975
|
+
return Math.min(grossCents, Math.max(0, share));
|
|
8976
|
+
}
|
|
8977
|
+
function cardRefundSummary(plan) {
|
|
8978
|
+
return {
|
|
8979
|
+
grossCents: plan.grossCents,
|
|
8980
|
+
refundedCents: sum(plan.entries.map((entry) => entry.amountCents)),
|
|
8981
|
+
feeShareCents: sum(plan.entries.map((entry) => entry.feeShareCents)),
|
|
8982
|
+
subCentMicro: plan.subCentMicro,
|
|
8983
|
+
refundIds: plan.entries.flatMap((entry) => entry.refundId ? [entry.refundId] : [])
|
|
8984
|
+
};
|
|
8985
|
+
}
|
|
8986
|
+
function usdFromCents(cents) {
|
|
8987
|
+
return `$${(cents / 100).toFixed(2)}`;
|
|
8988
|
+
}
|
|
8989
|
+
async function planCardRefund(deps, drain) {
|
|
8990
|
+
const lots = (await deps.ledger.listFundingLots(drain.creditId)).filter(
|
|
8991
|
+
(lot) => isCardFundedRail(lot.rail) && lot.fundingRef?.startsWith("pi_")
|
|
8992
|
+
);
|
|
8993
|
+
const grossCents = Math.floor(drain.amountMicro / MICRO_PER_CENT);
|
|
8994
|
+
const entries = [];
|
|
8995
|
+
let left = grossCents;
|
|
8996
|
+
for (const lot of lots) {
|
|
8997
|
+
if (left === 0) break;
|
|
8998
|
+
const charge = await deps.stripe.retrieveCharge(lot.fundingRef);
|
|
8999
|
+
assertRefundable(charge);
|
|
9000
|
+
const gross = Math.min(left, charge.amount - charge.amountRefunded);
|
|
9001
|
+
if (gross <= 0) continue;
|
|
9002
|
+
const feeShare = proportionalFeeShare(gross, charge.fee);
|
|
9003
|
+
entries.push({
|
|
9004
|
+
lotId: lot.lotId,
|
|
9005
|
+
paymentIntentId: charge.paymentIntentId,
|
|
9006
|
+
grossCents: gross,
|
|
9007
|
+
feeShareCents: feeShare,
|
|
9008
|
+
amountCents: gross - feeShare,
|
|
9009
|
+
attempt: 1,
|
|
9010
|
+
refundId: null,
|
|
9011
|
+
status: "planned",
|
|
9012
|
+
lastError: null
|
|
9013
|
+
});
|
|
9014
|
+
left -= gross;
|
|
9015
|
+
}
|
|
9016
|
+
if (left > 0) throw new CardRefundPlanError("charges_exhausted");
|
|
9017
|
+
return { grossCents, subCentMicro: drain.amountMicro - grossCents * MICRO_PER_CENT, entries };
|
|
9018
|
+
}
|
|
9019
|
+
var CardRefundPlanError = class extends Error {
|
|
9020
|
+
constructor(code) {
|
|
9021
|
+
super(`card refund cannot be planned: ${code}`);
|
|
9022
|
+
this.code = code;
|
|
9023
|
+
this.name = "CardRefundPlanError";
|
|
9024
|
+
}
|
|
9025
|
+
code;
|
|
9026
|
+
};
|
|
9027
|
+
function assertRefundable(charge) {
|
|
9028
|
+
if (charge.status !== "succeeded" || charge.currency !== "usd") {
|
|
9029
|
+
throw new CardRefundPlanError("charge_not_refundable");
|
|
9030
|
+
}
|
|
9031
|
+
if (!charge.fee) throw new CardRefundPlanError("fee_not_settled");
|
|
9032
|
+
}
|
|
9033
|
+
async function advanceRefund(stripe, drain, entry) {
|
|
9034
|
+
await advanceRefundOnce(stripe, drain, entry);
|
|
9035
|
+
return entry.status;
|
|
9036
|
+
}
|
|
9037
|
+
async function advanceRefundOnce(stripe, drain, entry) {
|
|
9038
|
+
const tag = refundDrainTag(drain);
|
|
9039
|
+
try {
|
|
9040
|
+
if (entry.refundId && isOpenStatus(entry.status)) {
|
|
9041
|
+
const refund2 = await stripe.retrieveRefund(entry.refundId);
|
|
9042
|
+
setStatus(entry, refund2.status);
|
|
9043
|
+
return;
|
|
9044
|
+
}
|
|
9045
|
+
const existing = (await stripe.listRefunds(entry.paymentIntentId)).find(
|
|
9046
|
+
(refund2) => refund2.metadata.dvmkit_drain === tag && refund2.metadata.dvmkit_lot === entry.lotId && refund2.status !== "failed" && refund2.status !== "canceled"
|
|
9047
|
+
);
|
|
9048
|
+
if (existing) {
|
|
9049
|
+
entry.refundId = existing.id;
|
|
9050
|
+
setStatus(entry, existing.status);
|
|
9051
|
+
return;
|
|
9052
|
+
}
|
|
9053
|
+
if (entry.status === "rejected" || entry.status === "failed" || entry.status === "canceled") {
|
|
9054
|
+
entry.attempt += 1;
|
|
9055
|
+
}
|
|
9056
|
+
const refund = await stripe.createRefund({
|
|
9057
|
+
paymentIntentId: entry.paymentIntentId,
|
|
9058
|
+
amount: entry.amountCents,
|
|
9059
|
+
idempotencyKey: refundIdempotencyKey(drain, entry),
|
|
9060
|
+
metadata: { dvmkit_drain: tag, dvmkit_lot: entry.lotId, dvmkit_attempt: String(entry.attempt) }
|
|
9061
|
+
});
|
|
9062
|
+
entry.refundId = refund.id;
|
|
9063
|
+
setStatus(entry, refund.status);
|
|
9064
|
+
} catch (err) {
|
|
9065
|
+
if (err instanceof StripeApiError && err.rejected) entry.status = "rejected";
|
|
9066
|
+
entry.lastError = failureCode(err);
|
|
9067
|
+
}
|
|
9068
|
+
}
|
|
9069
|
+
function setStatus(entry, status) {
|
|
9070
|
+
switch (status) {
|
|
9071
|
+
case "succeeded":
|
|
9072
|
+
case "pending":
|
|
9073
|
+
case "requires_action":
|
|
9074
|
+
case "failed":
|
|
9075
|
+
case "canceled":
|
|
9076
|
+
entry.status = status;
|
|
9077
|
+
entry.lastError = status === "failed" || status === "canceled" ? status : null;
|
|
9078
|
+
return;
|
|
9079
|
+
default:
|
|
9080
|
+
entry.status = "pending";
|
|
9081
|
+
}
|
|
9082
|
+
}
|
|
9083
|
+
function isOpenStatus(status) {
|
|
9084
|
+
return status === "pending" || status === "requires_action" || status === "planned";
|
|
9085
|
+
}
|
|
9086
|
+
function refundsFinished(plan) {
|
|
9087
|
+
return plan.entries.every((entry) => entry.amountCents <= 0 || entry.status === "succeeded");
|
|
9088
|
+
}
|
|
9089
|
+
function refundIdempotencyKey(drain, entry) {
|
|
9090
|
+
const digest = createHash7("sha256").update([drain.creditId, drain.drainId, entry.lotId, String(entry.attempt)].join("\0")).digest("hex");
|
|
9091
|
+
return `dvmkit-card-refund-${digest}`;
|
|
9092
|
+
}
|
|
9093
|
+
function cardRefundSentRef(plan) {
|
|
9094
|
+
const summary = cardRefundSummary(plan);
|
|
9095
|
+
return {
|
|
9096
|
+
refund_ids: summary.refundIds,
|
|
9097
|
+
refunded_cents: summary.refundedCents,
|
|
9098
|
+
fee_share_cents: summary.feeShareCents,
|
|
9099
|
+
sub_cent_micro: summary.subCentMicro,
|
|
9100
|
+
refunds: plan.entries.map((entry) => ({
|
|
9101
|
+
payment_intent: entry.paymentIntentId,
|
|
9102
|
+
refund_id: entry.refundId,
|
|
9103
|
+
gross_cents: entry.grossCents,
|
|
9104
|
+
fee_share_cents: entry.feeShareCents,
|
|
9105
|
+
amount_cents: entry.amountCents
|
|
9106
|
+
}))
|
|
9107
|
+
};
|
|
9108
|
+
}
|
|
9109
|
+
async function reread(deps, drain) {
|
|
9110
|
+
return await deps.ledger.getDrain({ creditId: drain.creditId, drainId: drain.drainId }) ?? drain;
|
|
9111
|
+
}
|
|
9112
|
+
function failureCode(err) {
|
|
9113
|
+
if (err instanceof CardRefundPlanError) return err.code;
|
|
9114
|
+
if (err instanceof StripeApiError) {
|
|
9115
|
+
return err.code ?? err.type ?? (err.status === 0 ? "stripe_unreachable" : `http_${err.status}`);
|
|
9116
|
+
}
|
|
9117
|
+
return "internal_error";
|
|
9118
|
+
}
|
|
9119
|
+
function refundDrainTag(drain) {
|
|
9120
|
+
const tag = `${drain.creditId}:${drain.drainId}`;
|
|
9121
|
+
if (tag.length <= 200) return tag;
|
|
9122
|
+
return `sha256:${createHash7("sha256").update(tag).digest("hex")}`;
|
|
9123
|
+
}
|
|
9124
|
+
function logPending(drain, fields) {
|
|
9125
|
+
diagnosticConsole.warn({
|
|
9126
|
+
event: "card_refund_pending",
|
|
9127
|
+
credit_id: drain.creditId,
|
|
9128
|
+
drain_id: drain.drainId,
|
|
9129
|
+
...fields
|
|
9130
|
+
});
|
|
9131
|
+
}
|
|
9132
|
+
function sum(values) {
|
|
9133
|
+
return values.reduce((total, value) => total + value, 0);
|
|
9134
|
+
}
|
|
9135
|
+
|
|
8488
9136
|
// src/sdk/server/tempo-drain-leg.ts
|
|
8489
9137
|
async function completeTempoChannelDrain(args) {
|
|
8490
9138
|
if (!args.tx && args.refundedNative !== void 0 && /^\d{1,78}$/.test(String(args.refundedNative))) {
|
|
@@ -8630,9 +9278,10 @@ var CREDIT_REQUEST_SCHEMA = z.object({
|
|
|
8630
9278
|
/**
|
|
8631
9279
|
* Lightning names an invoice request; MPP names the charge/session
|
|
8632
9280
|
* intent before a credential exists, so a bound credit can refuse the
|
|
8633
|
-
* wrong instrument before issuing or broadcasting anything.
|
|
9281
|
+
* wrong instrument before issuing or broadcasting anything. Stripe names
|
|
9282
|
+
* a card top-up: its MPP `method="stripe"` challenge is issued only here.
|
|
8634
9283
|
*/
|
|
8635
|
-
method: z.enum(["lightning", "tempo"]).optional(),
|
|
9284
|
+
method: z.enum(["lightning", "tempo", "stripe"]).optional(),
|
|
8636
9285
|
/** Required with `method: "tempo"`; invalid on every other funding rail. */
|
|
8637
9286
|
intent: z.enum(["charge", "session"]).optional()
|
|
8638
9287
|
}).optional(),
|
|
@@ -8861,6 +9510,9 @@ async function handleDrain(c, deps, args) {
|
|
|
8861
9510
|
if (isHttpResponse(completed)) return completed;
|
|
8862
9511
|
existing = completed;
|
|
8863
9512
|
}
|
|
9513
|
+
if (existing.method === "stripe" && existing.status === "pending" && deps.stripeCredit) {
|
|
9514
|
+
existing = (await runStripeDrain(stripeLegDeps(deps, deps.stripeCredit), existing)).drain;
|
|
9515
|
+
}
|
|
8864
9516
|
return respondDrain(c, deps, { record: await deliverIfParked(deps, existing), credit });
|
|
8865
9517
|
}
|
|
8866
9518
|
if (credit.tempoChannelId && drain.method !== "tempo") {
|
|
@@ -8886,8 +9538,17 @@ async function handleDrain(c, deps, args) {
|
|
|
8886
9538
|
}
|
|
8887
9539
|
const rejected = await burnNonce(c, deps, args.envelope);
|
|
8888
9540
|
if (rejected) return rejected;
|
|
9541
|
+
if (isCardFundedRail(credit.rail) && drain.method !== "stripe") {
|
|
9542
|
+
return c.json(cardCreditNotDrainable(credit), 409);
|
|
9543
|
+
}
|
|
9544
|
+
if (drain.method === "stripe" && !isCardFundedRail(credit.rail)) {
|
|
9545
|
+
return c.json(cardRefundOfNonCardCredit(credit), 409);
|
|
9546
|
+
}
|
|
8889
9547
|
const target = validateDrainTarget(drain);
|
|
8890
9548
|
if ("error" in target) return c.json(target.error, 400);
|
|
9549
|
+
if (target.method === "stripe") {
|
|
9550
|
+
return handleCardRefundDrain(c, deps, { creditId, drainId: drain.drain_id, callerPubkey: args.callerPubkey });
|
|
9551
|
+
}
|
|
8891
9552
|
const railMismatch = railMismatchRefusal(credit, target.method);
|
|
8892
9553
|
if (railMismatch) return c.json(railMismatch, 400);
|
|
8893
9554
|
const tempoChargeDrain = target.method === "tempo" && !credit.tempoChannelId;
|
|
@@ -9644,13 +10305,14 @@ async function respondDrain(c, deps, args) {
|
|
|
9644
10305
|
...drainCopy(args.record)
|
|
9645
10306
|
});
|
|
9646
10307
|
}
|
|
10308
|
+
var MAX_CARD_REFUND_DRAIN_ID = 128;
|
|
9647
10309
|
function validateDrainTarget(drain) {
|
|
9648
10310
|
const refuse = (message, hint) => ({
|
|
9649
10311
|
error: { error: "invalid_request", message, hint }
|
|
9650
10312
|
});
|
|
9651
10313
|
const missingFields = () => refuse(
|
|
9652
10314
|
"A new drain requires drain.method and drain.payout.",
|
|
9653
|
-
'Send drain: { drain_id, method: "cashu" | "x402" | "tempo", payout: { \u2026 } }. Only a poll of an existing drain_id may omit them.'
|
|
10315
|
+
'Send drain: { drain_id, method: "cashu" | "x402" | "tempo", payout: { \u2026 } }, or { drain_id, method: "stripe" } for card-funded credit. Only a poll of an existing drain_id may omit them.'
|
|
9654
10316
|
);
|
|
9655
10317
|
const method = drain.method;
|
|
9656
10318
|
if (!method) return missingFields();
|
|
@@ -9658,10 +10320,33 @@ function validateDrainTarget(drain) {
|
|
|
9658
10320
|
return {
|
|
9659
10321
|
error: paymentErrorBody("drain_method_unsupported", {
|
|
9660
10322
|
message: `"${method}" is not a payout rail this service reclaims over.`,
|
|
9661
|
-
|
|
10323
|
+
// Payout rails only: a card refund is not one, and card credit is pointed at it by its own refusal.
|
|
10324
|
+
extra: { supported_methods: DRAIN_METHODS.filter((m) => m !== "stripe"), drain_registered: false }
|
|
9662
10325
|
})
|
|
9663
10326
|
};
|
|
9664
10327
|
}
|
|
10328
|
+
if (method === "stripe") {
|
|
10329
|
+
const named = Object.values(drain.payout ?? {}).some((value) => value !== void 0 && value !== "");
|
|
10330
|
+
if (named) {
|
|
10331
|
+
return refuse(
|
|
10332
|
+
"stripe drains carry no payout destination: the refund goes back to the card that paid for the credit.",
|
|
10333
|
+
'Send drain: { drain_id, method: "stripe" } with no payout.'
|
|
10334
|
+
);
|
|
10335
|
+
}
|
|
10336
|
+
if (drain.drain_id.length > MAX_CARD_REFUND_DRAIN_ID) {
|
|
10337
|
+
return refuse(
|
|
10338
|
+
`stripe drain ids are at most ${MAX_CARD_REFUND_DRAIN_ID} characters.`,
|
|
10339
|
+
"Choose a shorter drain_id, such as a UUID."
|
|
10340
|
+
);
|
|
10341
|
+
}
|
|
10342
|
+
if (drain.drain_id.startsWith(CARD_EXPIRY_DRAIN_PREFIX)) {
|
|
10343
|
+
return refuse(
|
|
10344
|
+
`drain ids starting "${CARD_EXPIRY_DRAIN_PREFIX}" are reserved for the refund this service makes when a card balance expires.`,
|
|
10345
|
+
"Choose a drain_id that does not start with that prefix, such as a UUID."
|
|
10346
|
+
);
|
|
10347
|
+
}
|
|
10348
|
+
return { method: "stripe", payout: {} };
|
|
10349
|
+
}
|
|
9665
10350
|
if (!drain.payout && method !== "tempo") return missingFields();
|
|
9666
10351
|
switch (method) {
|
|
9667
10352
|
case "cashu": {
|
|
@@ -9690,9 +10375,70 @@ function validateDrainTarget(drain) {
|
|
|
9690
10375
|
}
|
|
9691
10376
|
}
|
|
9692
10377
|
}
|
|
10378
|
+
function cardCreditNotDrainable(credit) {
|
|
10379
|
+
return {
|
|
10380
|
+
error: "card_credit_not_drainable",
|
|
10381
|
+
message: "This credit was funded by card. Card credit is refunded to the card that paid for it, never paid out on another rail.",
|
|
10382
|
+
display: credit.expired ? "This balance was paid by card and has expired, so it can only go back to that card. It will be refunded there automatically, or you can ask for the refund now." : "This balance was paid by card, so it can only go back to that card.",
|
|
10383
|
+
hint: `Drain it with method "stripe" and no payout ('dvm credit drain <dvm> --method stripe'); no other method is accepted for card-funded credit. The refund goes to the card, less the refund's share of Stripe's card processing fee.`,
|
|
10384
|
+
retryable: false
|
|
10385
|
+
};
|
|
10386
|
+
}
|
|
10387
|
+
function cardRefundOfNonCardCredit(credit) {
|
|
10388
|
+
return {
|
|
10389
|
+
error: "invalid_request",
|
|
10390
|
+
message: `This credit was funded over ${credit.rail ?? "another rail"}, not by card, so it cannot be refunded to a card.`,
|
|
10391
|
+
hint: 'Reclaim it on the rail that funded it; method "stripe" only refunds credit that a card paid for.',
|
|
10392
|
+
retryable: false,
|
|
10393
|
+
...credit.rail ? { credit_rail: credit.rail } : {}
|
|
10394
|
+
};
|
|
10395
|
+
}
|
|
10396
|
+
async function handleCardRefundDrain(c, deps, args) {
|
|
10397
|
+
const stripeCredit = deps.stripeCredit;
|
|
10398
|
+
if (!stripeCredit) {
|
|
10399
|
+
return c.json(
|
|
10400
|
+
{
|
|
10401
|
+
error: "card_refund_unavailable",
|
|
10402
|
+
message: "This service is not configured for card refunds right now, so nothing was drained.",
|
|
10403
|
+
hint: "Retry later with the same drain_id. Your balance is unchanged and stays yours.",
|
|
10404
|
+
retryable: true
|
|
10405
|
+
},
|
|
10406
|
+
503
|
|
10407
|
+
);
|
|
10408
|
+
}
|
|
10409
|
+
try {
|
|
10410
|
+
const result = await deps.ledger.requestDrain({
|
|
10411
|
+
creditId: args.creditId,
|
|
10412
|
+
drainId: args.drainId,
|
|
10413
|
+
callerPubkey: args.callerPubkey,
|
|
10414
|
+
method: "stripe",
|
|
10415
|
+
payout: {}
|
|
10416
|
+
});
|
|
10417
|
+
let record = result.drain;
|
|
10418
|
+
if (!result.replayed && deps.receiptIssuer) {
|
|
10419
|
+
record = await countersignDrain(deps, record, "requested");
|
|
10420
|
+
}
|
|
10421
|
+
record = (await runStripeDrain(stripeLegDeps(deps, stripeCredit), record)).drain;
|
|
10422
|
+
return await respondDrain(c, deps, { record, credit: result.credit });
|
|
10423
|
+
} catch (err) {
|
|
10424
|
+
if (err instanceof CreditLedgerError) {
|
|
10425
|
+
return drainLedgerErrorResponse(err, args.creditId, args.drainId);
|
|
10426
|
+
}
|
|
10427
|
+
throw err;
|
|
10428
|
+
}
|
|
10429
|
+
}
|
|
10430
|
+
function stripeLegDeps(deps, stripeCredit) {
|
|
10431
|
+
return {
|
|
10432
|
+
ledger: deps.ledger,
|
|
10433
|
+
drainReport: deps.drainReport,
|
|
10434
|
+
stripe: stripeCredit.refunds,
|
|
10435
|
+
...deps.receiptIssuer && { receiptIssuer: deps.receiptIssuer }
|
|
10436
|
+
};
|
|
10437
|
+
}
|
|
9693
10438
|
function railMismatchRefusal(credit, method) {
|
|
9694
10439
|
if (method !== "x402" && method !== "tempo") return void 0;
|
|
9695
10440
|
if (!isFundingRail(credit.rail) || credit.rail === method) return void 0;
|
|
10441
|
+
if (isCardFundedRail(credit.rail)) return cardCreditNotDrainable(credit);
|
|
9696
10442
|
const target = credit.rail === "cashu" || credit.rail === "lightning" ? "--method cashu" : `--method ${credit.rail} --address 0x\u2026`;
|
|
9697
10443
|
return {
|
|
9698
10444
|
error: "invalid_request",
|
|
@@ -9730,10 +10476,57 @@ function drainBody(record) {
|
|
|
9730
10476
|
created_at: record.createdAt,
|
|
9731
10477
|
...record.token !== null ? { token: record.token } : {},
|
|
9732
10478
|
...record.sentRef !== null ? { sent: record.sentRef } : {},
|
|
10479
|
+
...record.method === "stripe" ? cardRefundBody(record) : {},
|
|
9733
10480
|
receipts: record.receipts
|
|
9734
10481
|
};
|
|
9735
10482
|
}
|
|
10483
|
+
function cardRefundBody(record) {
|
|
10484
|
+
if (!record.cardRefund) return { refund_ids: [] };
|
|
10485
|
+
const summary = cardRefundSummary(record.cardRefund);
|
|
10486
|
+
return {
|
|
10487
|
+
refund_ids: summary.refundIds,
|
|
10488
|
+
card_refund: {
|
|
10489
|
+
refunded_usd: usdFromCents(summary.refundedCents),
|
|
10490
|
+
stripe_fee_share_usd: usdFromCents(summary.feeShareCents),
|
|
10491
|
+
refunded_cents: summary.refundedCents,
|
|
10492
|
+
stripe_fee_share_cents: summary.feeShareCents,
|
|
10493
|
+
sub_cent_micro: summary.subCentMicro,
|
|
10494
|
+
refunds: record.cardRefund.entries.map((entry) => ({
|
|
10495
|
+
refund_id: entry.refundId,
|
|
10496
|
+
amount_cents: entry.amountCents,
|
|
10497
|
+
fee_share_cents: entry.feeShareCents,
|
|
10498
|
+
status: entry.status
|
|
10499
|
+
}))
|
|
10500
|
+
}
|
|
10501
|
+
};
|
|
10502
|
+
}
|
|
10503
|
+
function cardRefundCopy(record) {
|
|
10504
|
+
const sent = record.status === "sent";
|
|
10505
|
+
if (!record.cardRefund) {
|
|
10506
|
+
return {
|
|
10507
|
+
display: "The refund to your card is approved and the balance is set aside for it, but the card processor hasn't been reached yet.",
|
|
10508
|
+
hint: "Poll this op with the same drain_id; each poll retries the refund. The balance stays owed to the card until it goes through."
|
|
10509
|
+
};
|
|
10510
|
+
}
|
|
10511
|
+
const summary = cardRefundSummary(record.cardRefund);
|
|
10512
|
+
const gross = usdFromCents(summary.grossCents);
|
|
10513
|
+
const fee = summary.feeShareCents > 0 ? ` ${usdFromCents(summary.feeShareCents)} of the ${gross} balance was kept to cover this refund's share of Stripe's card processing fee: Stripe charged that fee when the card paid and does not return it on a refund.` : "";
|
|
10514
|
+
const dust = summary.subCentMicro > 0 ? " Less than one cent of the balance could not be refunded, because cards are refunded in whole cents." : "";
|
|
10515
|
+
if (sent) {
|
|
10516
|
+
return {
|
|
10517
|
+
display: `${usdFromCents(summary.refundedCents)} went back to the card that paid for this credit.${fee}${dust} Card refunds usually show on the statement within 5 to 10 business days.`,
|
|
10518
|
+
hint: "See refund_ids for the Stripe refund references. Nothing else to do."
|
|
10519
|
+
};
|
|
10520
|
+
}
|
|
10521
|
+
return {
|
|
10522
|
+
display: `A refund of ${usdFromCents(summary.refundedCents)} to the card that paid for this credit is under way and the balance is set aside for it.${fee}${dust}`,
|
|
10523
|
+
hint: "Poll this op with the same drain_id; each poll retries any refund the card processor hasn't finished, and it flips to sent once every refund has gone through. No refund is made twice."
|
|
10524
|
+
};
|
|
10525
|
+
}
|
|
9736
10526
|
function drainCopy(record) {
|
|
10527
|
+
if (record.method === "stripe" && (record.status === "pending" || record.status === "sent")) {
|
|
10528
|
+
return cardRefundCopy(record);
|
|
10529
|
+
}
|
|
9737
10530
|
switch (record.status) {
|
|
9738
10531
|
case "pending":
|
|
9739
10532
|
if (record.method === "cashu") {
|
|
@@ -9873,26 +10666,36 @@ async function handleFund(c, deps, args) {
|
|
|
9873
10666
|
tempoIntent: fund.method === "tempo" ? fund.intent : void 0
|
|
9874
10667
|
});
|
|
9875
10668
|
if (sizing) return sizing;
|
|
10669
|
+
if (fund.method === "stripe" && (!menu.stripe || !deps.stripeCredit)) {
|
|
10670
|
+
return jsonPaymentError("payment_invalid", 409, {
|
|
10671
|
+
message: "This DVM does not offer card funding for prepaid credit.",
|
|
10672
|
+
display: "This service doesn't take card payments for prepaid credit. Nothing was charged.",
|
|
10673
|
+
hint: "Fund the credit over one of the rails listed in credit.funding on /v1/info.",
|
|
10674
|
+
extra: { alternatives: menu.funding.filter((rail) => rail !== "stripe") }
|
|
10675
|
+
});
|
|
10676
|
+
}
|
|
9876
10677
|
if (fund.method === "tempo" && fund.intent === "charge" && !stablecoinCreditInstrumentAllowed(deps.creditConfig?.allowOneShotStablecoin, "tempo_charge")) {
|
|
9877
10678
|
return jsonPaymentError("one_shot_credit_not_supported", 409, {
|
|
9878
10679
|
message: "Tempo charge remains available for per-call payment, but this DVM accepts only reusable Tempo sessions for prepaid credit."
|
|
9879
10680
|
});
|
|
9880
10681
|
}
|
|
9881
|
-
let requiredMsats;
|
|
9882
|
-
|
|
9883
|
-
|
|
9884
|
-
|
|
9885
|
-
|
|
9886
|
-
|
|
9887
|
-
|
|
9888
|
-
|
|
9889
|
-
|
|
9890
|
-
|
|
9891
|
-
|
|
9892
|
-
|
|
9893
|
-
|
|
9894
|
-
|
|
9895
|
-
|
|
10682
|
+
let requiredMsats = 0;
|
|
10683
|
+
if (fund.method !== "stripe") {
|
|
10684
|
+
try {
|
|
10685
|
+
const fx = await deps.fxFetcher.fetch();
|
|
10686
|
+
requiredMsats = fiatToSatsCeil(fund.amount_micro / 1e6, fxRateFor(fx, menu.currency)) * 1e3;
|
|
10687
|
+
} catch {
|
|
10688
|
+
return c.json(
|
|
10689
|
+
{
|
|
10690
|
+
error: "fx_rate_unavailable",
|
|
10691
|
+
message: `Could not price a ${menu.currency} top-up in sats right now.`,
|
|
10692
|
+
display: "The service can't work out the exchange rate right now. Nothing was charged.",
|
|
10693
|
+
hint: "Retry in a few moments.",
|
|
10694
|
+
retryable: true
|
|
10695
|
+
},
|
|
10696
|
+
503
|
|
10697
|
+
);
|
|
10698
|
+
}
|
|
9896
10699
|
}
|
|
9897
10700
|
if (fund.method === "lightning") {
|
|
9898
10701
|
return lightningIssue(c, deps, {
|
|
@@ -9913,7 +10716,13 @@ async function handleFund(c, deps, args) {
|
|
|
9913
10716
|
const result = await loggers.isolate.span(
|
|
9914
10717
|
"dvm.credit.fund",
|
|
9915
10718
|
{ required_msats: requiredMsats },
|
|
9916
|
-
() =>
|
|
10719
|
+
() => fund.method === "stripe" ? verifyCardFunding({
|
|
10720
|
+
...railOpts,
|
|
10721
|
+
stripeCredit: deps.stripeCredit,
|
|
10722
|
+
amountMicro: fund.amount_micro,
|
|
10723
|
+
callerPubkey: args.callerPubkey,
|
|
10724
|
+
fundOnly: { creditId, fundId: fund.fund_id, commitment }
|
|
10725
|
+
}) : verifyUpfrontPayment({
|
|
9917
10726
|
...railOpts,
|
|
9918
10727
|
requiredMsats,
|
|
9919
10728
|
priceFiat: { currency: menu.currency, amountMicro: fund.amount_micro },
|
|
@@ -10197,6 +11006,19 @@ async function checkSizing(deps, args) {
|
|
|
10197
11006
|
extra: { min_micro: menu.min_micro, max_micro: menu.max_micro, currency: menu.currency }
|
|
10198
11007
|
});
|
|
10199
11008
|
}
|
|
11009
|
+
if (args.rail === "stripe" && menu.stripe && args.amountMicro < menu.stripe.min_micro) {
|
|
11010
|
+
return jsonPaymentError("below_rail_minimum", 400, {
|
|
11011
|
+
message: `Top-up of ${args.amountMicro} micro-${menu.currency.toUpperCase()} is below the ${menu.stripe.min_micro} micro-${menu.currency.toUpperCase()} card minimum.`,
|
|
11012
|
+
display: "Card top-ups start at $0.50. Nothing was charged.",
|
|
11013
|
+
hint: `Ask for a top-up of at least ${menu.stripe.min_micro} micro-USD ($0.50) and sign the fund request again with that amount.`,
|
|
11014
|
+
extra: {
|
|
11015
|
+
rail: "stripe",
|
|
11016
|
+
min_micro: menu.stripe.min_micro,
|
|
11017
|
+
alternatives: menu.funding.filter((rail) => rail !== "stripe"),
|
|
11018
|
+
currency: menu.currency
|
|
11019
|
+
}
|
|
11020
|
+
});
|
|
11021
|
+
}
|
|
10200
11022
|
const floorSats = deps.lightning?.receive.fundingMinSats;
|
|
10201
11023
|
if (args.rail === "lightning" && floorSats !== void 0) {
|
|
10202
11024
|
let fx;
|
|
@@ -10224,7 +11046,7 @@ async function checkSizing(deps, args) {
|
|
|
10224
11046
|
const held = await deps.ledger.getForCaller(args.callerPubkey);
|
|
10225
11047
|
const target = await checkTargetCredit(deps, { ...args, held });
|
|
10226
11048
|
if (target) return target;
|
|
10227
|
-
const heldMicro = held.filter((credit) => spendable(credit, menu.currency)).reduce((
|
|
11049
|
+
const heldMicro = held.filter((credit) => spendable(credit, menu.currency)).reduce((sum2, credit) => sum2 + credit.availableMicro, 0);
|
|
10228
11050
|
const fundableMicro = menu.max_micro - heldMicro;
|
|
10229
11051
|
if (args.amountMicro > fundableMicro) {
|
|
10230
11052
|
return jsonPaymentError("credit_over_max", 400, {
|
|
@@ -10247,6 +11069,17 @@ async function checkTargetCredit(deps, args) {
|
|
|
10247
11069
|
message: `Credit ${args.creditId} is terminal because its Tempo backing finalized before the DVM was protected.`
|
|
10248
11070
|
});
|
|
10249
11071
|
}
|
|
11072
|
+
if (mine.status === "refunded" || mine.status === "disputed") {
|
|
11073
|
+
return jsonPaymentError("credit_closed", 409, {
|
|
11074
|
+
message: mine.status === "refunded" ? `Credit ${args.creditId} is closed: its expired card balance was refunded to the card.` : `Credit ${args.creditId} is frozen: a card payment that funded it is disputed.`
|
|
11075
|
+
});
|
|
11076
|
+
}
|
|
11077
|
+
if (isCardFundedRail(mine.rail) && mine.expiryMs <= Date.now()) {
|
|
11078
|
+
return jsonPaymentError("credit_closed", 409, {
|
|
11079
|
+
message: `Credit ${args.creditId} expired; its card balance is being refunded to the card.`,
|
|
11080
|
+
hint: "Omit credit_id (or use a new fund_id) to open a new prepaid balance. The expired balance comes back to the card on its own."
|
|
11081
|
+
});
|
|
11082
|
+
}
|
|
10250
11083
|
if (mine.currency !== args.menu.currency) {
|
|
10251
11084
|
return jsonPaymentError("payment_invalid", 400, {
|
|
10252
11085
|
message: `Credit ${args.creditId} is denominated in ${mine.currency}, not ${args.menu.currency}.`
|
|
@@ -10391,6 +11224,12 @@ var DRAIN_CONFLICT_COPY = {
|
|
|
10391
11224
|
retryable: true,
|
|
10392
11225
|
drainRegistered: false
|
|
10393
11226
|
},
|
|
11227
|
+
card_refund_pending: {
|
|
11228
|
+
display: "A refund of this prepaid balance to your card is already in progress, so a second one was not started. Nothing changed.",
|
|
11229
|
+
hint: ({ blockingDrainId }) => `Poll drain ${blockingDrainId ?? "the earlier card refund"} (the same drain request again) until it is sent; it returns the balance it covers. Start a new card refund only after that, if any balance is left.`,
|
|
11230
|
+
retryable: true,
|
|
11231
|
+
drainRegistered: false
|
|
11232
|
+
},
|
|
10394
11233
|
refund_floor: {
|
|
10395
11234
|
display: "The refund reached the chain, but it paid less than the balance this service had recorded, so nothing was booked here. Your on-chain refund arrived; the difference has to be reconciled by the service operator.",
|
|
10396
11235
|
hint: ({ creditId, drainId }) => `Report drain ${drainId} on credit ${creditId} to the service operator \u2014 only they can reconcile the difference. Retrying this drain, or requesting a new drain_id, lands here again.`,
|
|
@@ -10442,9 +11281,12 @@ function drainLedgerErrorResponse(error, creditId, drainId) {
|
|
|
10442
11281
|
return jsonPaymentError("drain_conflict", 409, {
|
|
10443
11282
|
message: error.message,
|
|
10444
11283
|
display: copy?.display,
|
|
10445
|
-
hint: copy?.hint({ creditId, drainId }),
|
|
11284
|
+
hint: copy?.hint({ creditId, drainId, blockingDrainId: error.details.blockingDrainId }),
|
|
10446
11285
|
retryable: copy?.retryable,
|
|
10447
|
-
extra: {
|
|
11286
|
+
extra: {
|
|
11287
|
+
drain_registered: copy?.drainRegistered ?? false,
|
|
11288
|
+
...error.details.blockingDrainId && { blocking_drain_id: error.details.blockingDrainId }
|
|
11289
|
+
}
|
|
10448
11290
|
});
|
|
10449
11291
|
}
|
|
10450
11292
|
if (error.code === "settlement_pending") {
|
|
@@ -10457,6 +11299,9 @@ function drainLedgerErrorResponse(error, creditId, drainId) {
|
|
|
10457
11299
|
...error.details.blockingDrainId ? { blockingDrainId: error.details.blockingDrainId } : {}
|
|
10458
11300
|
});
|
|
10459
11301
|
}
|
|
11302
|
+
if (error.code === "credit_closed") {
|
|
11303
|
+
return jsonPaymentError("credit_closed", 409, { message: error.message });
|
|
11304
|
+
}
|
|
10460
11305
|
if (error.code === "caller_mismatch" || error.code === "credit_not_found") {
|
|
10461
11306
|
return jsonPaymentError("credit_not_found", 404, {
|
|
10462
11307
|
message: `No credit ${creditId} for this caller.`
|
|
@@ -10472,6 +11317,7 @@ function mapLedgerCode(err) {
|
|
|
10472
11317
|
case "insufficient_credit":
|
|
10473
11318
|
case "credit_expired":
|
|
10474
11319
|
case "credit_unbacked":
|
|
11320
|
+
case "credit_closed":
|
|
10475
11321
|
case "credit_not_found":
|
|
10476
11322
|
case "tempo_channel_closing":
|
|
10477
11323
|
case "settlement_pending":
|
|
@@ -10496,6 +11342,8 @@ function creditBody(credit, reconciliationPending = false) {
|
|
|
10496
11342
|
// Nominal expiry does not itself reclaim ownership; a committed sweep can remove undrawn
|
|
10497
11343
|
// value.
|
|
10498
11344
|
drainable: credit.status === "active" && credit.availableMicro > 0,
|
|
11345
|
+
// The drain an expired card balance went back to the card under; poll it like any drain.
|
|
11346
|
+
...credit.status === "refunded" && { refund_drain_id: cardExpiryDrainId(credit.expiryMs) },
|
|
10499
11347
|
...reconciliationPending && { reconciliation_pending: true },
|
|
10500
11348
|
ledger_seq: credit.lastLedgerSeq
|
|
10501
11349
|
};
|
|
@@ -10912,7 +11760,7 @@ async function handle(c, opts, action) {
|
|
|
10912
11760
|
}
|
|
10913
11761
|
|
|
10914
11762
|
// src/sdk/server/hosting-settlement-report-obligations.ts
|
|
10915
|
-
import { createHash as
|
|
11763
|
+
import { createHash as createHash8, randomUUID as randomUUID4 } from "crypto";
|
|
10916
11764
|
import { isDeepStrictEqual as isDeepStrictEqual2 } from "util";
|
|
10917
11765
|
import { BATCH_SETTLEMENT_ADDRESS as BATCH_SETTLEMENT_ADDRESS3 } from "@x402/evm";
|
|
10918
11766
|
import { computeChannelId as computeChannelId2 } from "@x402/evm/batch-settlement/client";
|
|
@@ -10985,7 +11833,7 @@ var TEMPO_CLOSED_EVENT = TempoSession.Precompile.escrowAbi.find(
|
|
|
10985
11833
|
(item) => item.type === "event" && item.name === "ChannelClosed"
|
|
10986
11834
|
);
|
|
10987
11835
|
function hash2(value) {
|
|
10988
|
-
return
|
|
11836
|
+
return createHash8("sha256").update(JSON.stringify(value)).digest("hex");
|
|
10989
11837
|
}
|
|
10990
11838
|
function hex(value, bytes) {
|
|
10991
11839
|
return typeof value === "string" && new RegExp(`^0x[0-9a-fA-F]{${bytes * 2}}$`).test(value);
|
|
@@ -11803,7 +12651,7 @@ var HostedSettlementReportObligations = class {
|
|
|
11803
12651
|
[this.dvmId, this.scope, batch.batch_id]
|
|
11804
12652
|
);
|
|
11805
12653
|
if (!sources.rows.length || sources.rows.some((row) => !row.tx_hash || !row.block_hash || row.delta_native === null)) return;
|
|
11806
|
-
const claimed = sources.rows.reduce((
|
|
12654
|
+
const claimed = sources.rows.reduce((sum2, row) => sum2 + BigInt(row.delta_native), 0n);
|
|
11807
12655
|
const hashes = new Set(sources.rows.map((row) => row.tx_hash));
|
|
11808
12656
|
await tx.query(
|
|
11809
12657
|
`UPDATE payout_x402_batches
|
|
@@ -11904,7 +12752,7 @@ var HostedSettlementReportObligations = class {
|
|
|
11904
12752
|
const batchId = payload.payoutId.slice("x402:batch:".length);
|
|
11905
12753
|
const settle = rows2.rows.filter((row) => row.operation === "settle" && row.rail === "x402");
|
|
11906
12754
|
const claims = rows2.rows.filter((row) => row.operation === "claim" || row.operation === "refund");
|
|
11907
|
-
if (settle.length !== 1 || !claims.length || claims.length + 1 !== rows2.rows.length || rows2.rows.some((row) => row.original.batchId !== batchId) || BigInt(settle[0].delta_native) !== BigInt(amount) || claims.reduce((
|
|
12755
|
+
if (settle.length !== 1 || !claims.length || claims.length + 1 !== rows2.rows.length || rows2.rows.some((row) => row.original.batchId !== batchId) || BigInt(settle[0].delta_native) !== BigInt(amount) || claims.reduce((sum2, row) => sum2 + BigInt(row.delta_native), 0n) !== BigInt(amount)) {
|
|
11908
12756
|
throw new Error("Hosted x402 payout differs from its original finalized batch");
|
|
11909
12757
|
}
|
|
11910
12758
|
} else {
|
|
@@ -12373,7 +13221,7 @@ var HostingCleanupFence = class {
|
|
|
12373
13221
|
};
|
|
12374
13222
|
|
|
12375
13223
|
// src/sdk/server/hosting-channel-evidence.ts
|
|
12376
|
-
import { createHash as
|
|
13224
|
+
import { createHash as createHash9 } from "crypto";
|
|
12377
13225
|
import { Session as TempoSession2 } from "mppx/tempo";
|
|
12378
13226
|
import { createPublicClient as createPublicClient5, http as http5 } from "viem";
|
|
12379
13227
|
import { BATCH_SETTLEMENT_ADDRESS as BATCH_SETTLEMENT_ADDRESS4 } from "@x402/evm";
|
|
@@ -12424,7 +13272,7 @@ async function readHostedChannelSnapshot(query) {
|
|
|
12424
13272
|
};
|
|
12425
13273
|
const x402HistoryComplete = await history(names[6]);
|
|
12426
13274
|
const tempoHistoryComplete = await history(names[7]);
|
|
12427
|
-
const digest =
|
|
13275
|
+
const digest = createHash9("sha256").update(JSON.stringify({
|
|
12428
13276
|
present: [...exists].sort(),
|
|
12429
13277
|
x402,
|
|
12430
13278
|
x402Tombstones,
|
|
@@ -13547,7 +14395,7 @@ function protocolEnvelopeIgnoreFields(schema) {
|
|
|
13547
14395
|
}
|
|
13548
14396
|
function stripProtocolEnvelope(schema, data) {
|
|
13549
14397
|
const withoutCredit = stripCreditEnvelope(schema, data);
|
|
13550
|
-
if (!
|
|
14398
|
+
if (!isRecord3(withoutCredit)) return withoutCredit;
|
|
13551
14399
|
const {
|
|
13552
14400
|
[REQUEST_ID_FIELD]: _requestId,
|
|
13553
14401
|
[AUTH_STATEMENT_FIELD]: _authStatement,
|
|
@@ -13556,13 +14404,13 @@ function stripProtocolEnvelope(schema, data) {
|
|
|
13556
14404
|
return builderInput;
|
|
13557
14405
|
}
|
|
13558
14406
|
function requestIdFromEnvelope(data) {
|
|
13559
|
-
if (!
|
|
14407
|
+
if (!isRecord3(data) || !(REQUEST_ID_FIELD in data)) return void 0;
|
|
13560
14408
|
return isPublicRequestId(data[REQUEST_ID_FIELD]) ? data[REQUEST_ID_FIELD] : void 0;
|
|
13561
14409
|
}
|
|
13562
14410
|
function hasInvalidRequestId(data) {
|
|
13563
|
-
return
|
|
14411
|
+
return isRecord3(data) && REQUEST_ID_FIELD in data && !isPublicRequestId(data[REQUEST_ID_FIELD]);
|
|
13564
14412
|
}
|
|
13565
|
-
function
|
|
14413
|
+
function isRecord3(value) {
|
|
13566
14414
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
13567
14415
|
}
|
|
13568
14416
|
|
|
@@ -13574,9 +14422,9 @@ import { AsyncLocalStorage } from "async_hooks";
|
|
|
13574
14422
|
import { randomBytes as randomBytes3 } from "crypto";
|
|
13575
14423
|
|
|
13576
14424
|
// src/sdk/server/instrumented-kv.ts
|
|
13577
|
-
import { createHash as
|
|
14425
|
+
import { createHash as createHash10 } from "crypto";
|
|
13578
14426
|
function hashKey(key) {
|
|
13579
|
-
return
|
|
14427
|
+
return createHash10("sha256").update(key).digest("hex").slice(0, 16);
|
|
13580
14428
|
}
|
|
13581
14429
|
function instrumentKVStore(inner) {
|
|
13582
14430
|
return {
|
|
@@ -17190,7 +18038,7 @@ var JobManager = class {
|
|
|
17190
18038
|
const rail = draw?.railValue.rail;
|
|
17191
18039
|
const currency = draw?.railValue.creditCurrency;
|
|
17192
18040
|
if (draw?.status !== "settled" || draw.amountMicro <= 0 || !currency || !rail || !isFundingRail(rail)) return;
|
|
17193
|
-
const testFunds = rail === "lightning" ? null : this.opts.paymentMode?.rails[rail].testFunds ?? null;
|
|
18041
|
+
const testFunds = rail === "lightning" || rail === "stripe" ? null : this.opts.paymentMode?.rails[rail].testFunds ?? null;
|
|
17194
18042
|
try {
|
|
17195
18043
|
await callback({ jobId: record.id, rail, amountMicro: draw.amountMicro, currency, testFunds });
|
|
17196
18044
|
} catch (error) {
|
|
@@ -18530,7 +19378,7 @@ function isReadUnavailable(error) {
|
|
|
18530
19378
|
}
|
|
18531
19379
|
|
|
18532
19380
|
// src/sdk/server/balance-payment.ts
|
|
18533
|
-
import { createHash as
|
|
19381
|
+
import { createHash as createHash11 } from "crypto";
|
|
18534
19382
|
import { z as z2 } from "zod";
|
|
18535
19383
|
var BALANCE_PAYMENT_SCHEMA = z2.object({
|
|
18536
19384
|
credit_id: z2.string().min(1).max(256),
|
|
@@ -18560,7 +19408,7 @@ async function commitBalancePayment(args) {
|
|
|
18560
19408
|
args.auth.recheckSignature(BALANCE_PAYMENT_SCHEMA, args.raw, args.domain);
|
|
18561
19409
|
const auth = BALANCE_PAYMENT_SCHEMA.parse(args.raw);
|
|
18562
19410
|
const { pubkey, credit_id, draw_id, payment_id, request_seq, amount_micro, currency } = auth;
|
|
18563
|
-
const fingerprint =
|
|
19411
|
+
const fingerprint = createHash11("sha256").update(JSON.stringify([
|
|
18564
19412
|
args.domain,
|
|
18565
19413
|
args.jobId,
|
|
18566
19414
|
pubkey,
|
|
@@ -18684,7 +19532,7 @@ async function commitBalancePayment(args) {
|
|
|
18684
19532
|
}
|
|
18685
19533
|
|
|
18686
19534
|
// src/sdk/server/app.ts
|
|
18687
|
-
import { createHash as
|
|
19535
|
+
import { createHash as createHash13, randomBytes as randomBytes5, randomUUID as randomUUID6 } from "crypto";
|
|
18688
19536
|
import { Hono } from "hono";
|
|
18689
19537
|
import { getCookie } from "hono/cookie";
|
|
18690
19538
|
import { cors } from "hono/cors";
|
|
@@ -18964,7 +19812,7 @@ async function handlePendingDrains(c, opts) {
|
|
|
18964
19812
|
const drains = pending.map((d) => {
|
|
18965
19813
|
const automatic = d.method === "cashu";
|
|
18966
19814
|
const amountSats = !automatic ? null : d.owedSats ?? netOwedSats(fiatToSatsCeil(d.amountMicro / 1e6, rateFor(d.currency)));
|
|
18967
|
-
const settlement = automatic ? "automatic" : d.channelId === null ? "manual" : "channel";
|
|
19815
|
+
const settlement = automatic ? "automatic" : d.method === "stripe" ? "card" : d.channelId === null ? "manual" : "channel";
|
|
18968
19816
|
return {
|
|
18969
19817
|
credit_id: d.creditId,
|
|
18970
19818
|
drain_id: d.drainId,
|
|
@@ -18980,17 +19828,19 @@ async function handlePendingDrains(c, opts) {
|
|
|
18980
19828
|
amount_native: d.drainedNative,
|
|
18981
19829
|
settlement,
|
|
18982
19830
|
channel_id: d.channelId,
|
|
18983
|
-
created_at: d.createdAt
|
|
19831
|
+
created_at: d.createdAt,
|
|
19832
|
+
...d.method === "stripe" && { card_refund: cardRefundStatus(d) }
|
|
18984
19833
|
};
|
|
18985
19834
|
});
|
|
18986
19835
|
const liability = {
|
|
18987
|
-
cashu_sats: drains.filter((d) => d.method === "cashu").reduce((
|
|
19836
|
+
cashu_sats: drains.filter((d) => d.method === "cashu").reduce((sum2, d) => sum2 + (d.amount_sats ?? 0), 0),
|
|
18988
19837
|
by_method_micro: DRAIN_METHODS.reduce((acc, method) => {
|
|
18989
|
-
acc[method] = pending.filter((d) => d.method === method).reduce((
|
|
19838
|
+
acc[method] = pending.filter((d) => d.method === method).reduce((sum2, d) => sum2 + d.amountMicro, 0);
|
|
18990
19839
|
return acc;
|
|
18991
19840
|
}, {}),
|
|
18992
|
-
manual_micro: drains.filter((d) => d.settlement === "manual").reduce((
|
|
18993
|
-
channel_micro: drains.filter((d) => d.settlement === "channel").reduce((
|
|
19841
|
+
manual_micro: drains.filter((d) => d.settlement === "manual").reduce((sum2, d) => sum2 + d.amount_micro, 0),
|
|
19842
|
+
channel_micro: drains.filter((d) => d.settlement === "channel").reduce((sum2, d) => sum2 + d.amount_micro, 0),
|
|
19843
|
+
card_micro: drains.filter((d) => d.settlement === "card").reduce((sum2, d) => sum2 + d.amount_micro, 0)
|
|
18994
19844
|
};
|
|
18995
19845
|
return c.json({ drains, liability, mints: opts.mints?.() ?? [] });
|
|
18996
19846
|
}
|
|
@@ -19000,7 +19850,7 @@ async function handleBitcoinLiability(c, opts) {
|
|
|
19000
19850
|
const deposit = await opts.ledger.bitcoinDepositLiability();
|
|
19001
19851
|
const pending = await opts.ledger.listPendingDrains();
|
|
19002
19852
|
const promised = pending.filter((d) => d.method === "cashu" && d.status === "pending");
|
|
19003
|
-
const drainSats = promised.reduce((
|
|
19853
|
+
const drainSats = promised.reduce((sum2, d) => sum2 + (d.owedSats ?? 0), 0);
|
|
19004
19854
|
const drainsUnpriced = promised.filter((d) => d.owedSats === null).length;
|
|
19005
19855
|
return c.json({
|
|
19006
19856
|
deposit_liability_sats: deposit.depositSats,
|
|
@@ -19337,6 +20187,7 @@ async function handleMarkDrainSent(c, opts) {
|
|
|
19337
20187
|
409
|
|
19338
20188
|
);
|
|
19339
20189
|
}
|
|
20190
|
+
if (existing?.method === "stripe") return retryCardRefund(c, opts, existing);
|
|
19340
20191
|
let record;
|
|
19341
20192
|
let replayed;
|
|
19342
20193
|
try {
|
|
@@ -20749,6 +21600,60 @@ function parseSerializedDleq(raw) {
|
|
|
20749
21600
|
if (dleq.r !== void 0 && typeof dleq.r !== "string") return void 0;
|
|
20750
21601
|
return { e: dleq.e, s: dleq.s, ...dleq.r !== void 0 ? { r: dleq.r } : {} };
|
|
20751
21602
|
}
|
|
21603
|
+
function cardRefundStatus(drain) {
|
|
21604
|
+
const plan = drain.cardRefund;
|
|
21605
|
+
if (!plan) return { planned: false };
|
|
21606
|
+
const summary = cardRefundSummary(plan);
|
|
21607
|
+
return {
|
|
21608
|
+
planned: true,
|
|
21609
|
+
refunded_cents: summary.refundedCents,
|
|
21610
|
+
fee_share_cents: summary.feeShareCents,
|
|
21611
|
+
sub_cent_micro: summary.subCentMicro,
|
|
21612
|
+
refund_ids: summary.refundIds,
|
|
21613
|
+
refunds: plan.entries.map((entry) => ({
|
|
21614
|
+
payment_intent: entry.paymentIntentId,
|
|
21615
|
+
refund_id: entry.refundId,
|
|
21616
|
+
amount_cents: entry.amountCents,
|
|
21617
|
+
fee_share_cents: entry.feeShareCents,
|
|
21618
|
+
attempt: entry.attempt,
|
|
21619
|
+
status: entry.status,
|
|
21620
|
+
last_error: entry.lastError
|
|
21621
|
+
}))
|
|
21622
|
+
};
|
|
21623
|
+
}
|
|
21624
|
+
async function retryCardRefund(c, opts, drain) {
|
|
21625
|
+
if (drain.status === "sent") {
|
|
21626
|
+
return c.json({ status: "sent", credit_id: drain.creditId, drain_id: drain.drainId, replayed: true });
|
|
21627
|
+
}
|
|
21628
|
+
if (!opts.stripeRefunds) {
|
|
21629
|
+
return c.json(
|
|
21630
|
+
{
|
|
21631
|
+
error: "card_refund_unavailable",
|
|
21632
|
+
message: "This DVM has no Stripe secret key configured, so it cannot refund to the card."
|
|
21633
|
+
},
|
|
21634
|
+
503
|
|
21635
|
+
);
|
|
21636
|
+
}
|
|
21637
|
+
const { drain: after, completed } = await runStripeDrain(
|
|
21638
|
+
{
|
|
21639
|
+
ledger: opts.ledger,
|
|
21640
|
+
drainReport: { db: opts.db, dvmId: opts.dvmId, enqueueCreditDrain: opts.enqueueCreditDrain },
|
|
21641
|
+
stripe: opts.stripeRefunds,
|
|
21642
|
+
...opts.receiptIssuer && { receiptIssuer: opts.receiptIssuer }
|
|
21643
|
+
},
|
|
21644
|
+
drain
|
|
21645
|
+
);
|
|
21646
|
+
return c.json(
|
|
21647
|
+
{
|
|
21648
|
+
status: after.status,
|
|
21649
|
+
credit_id: after.creditId,
|
|
21650
|
+
drain_id: after.drainId,
|
|
21651
|
+
replayed: after.status === "sent" && !completed,
|
|
21652
|
+
card_refund: cardRefundStatus(after)
|
|
21653
|
+
},
|
|
21654
|
+
after.status === "sent" ? 200 : 202
|
|
21655
|
+
);
|
|
21656
|
+
}
|
|
20752
21657
|
|
|
20753
21658
|
// src/sdk/server/caller-facing-url.ts
|
|
20754
21659
|
function resolvePublicOrigin(value) {
|
|
@@ -20767,6 +21672,220 @@ function callerFacingRequestUrl(requestUrl, publicOrigin) {
|
|
|
20767
21672
|
return `${publicOrigin}${request.pathname}${request.search}`;
|
|
20768
21673
|
}
|
|
20769
21674
|
|
|
21675
|
+
// src/sdk/server/card-refund-sweep.ts
|
|
21676
|
+
var DISPUTE_LOOKBACK_MS = 180 * 24 * 60 * 60 * 1e3;
|
|
21677
|
+
var MAX_DISPUTE_PAGES = 10;
|
|
21678
|
+
var DISPUTE_SETTLED_FOR_MERCHANT = /* @__PURE__ */ new Set(["won", "warning_closed"]);
|
|
21679
|
+
var STRANDED_AFTER_MS = 30 * 60 * 1e3;
|
|
21680
|
+
var STRANDED_SEARCH_WINDOW_MS = 24 * 60 * 60 * 1e3;
|
|
21681
|
+
var STRANDED_ABANDON_AFTER_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
21682
|
+
async function runCardRefundSweep(deps, opts = {}) {
|
|
21683
|
+
const nowMs = opts.nowMs ?? Date.now();
|
|
21684
|
+
const limit = opts.limit ?? 200;
|
|
21685
|
+
const result = {
|
|
21686
|
+
frozen: [],
|
|
21687
|
+
unfrozen: [],
|
|
21688
|
+
expiryRefunds: [],
|
|
21689
|
+
completed: [],
|
|
21690
|
+
pending: [],
|
|
21691
|
+
strandedRefunds: [],
|
|
21692
|
+
failedSteps: []
|
|
21693
|
+
};
|
|
21694
|
+
const legDeps = {
|
|
21695
|
+
ledger: deps.ledger,
|
|
21696
|
+
stripe: deps.stripe,
|
|
21697
|
+
drainReport: deps.drainReport,
|
|
21698
|
+
...deps.receiptIssuer && { receiptIssuer: deps.receiptIssuer }
|
|
21699
|
+
};
|
|
21700
|
+
const step = async (name, run) => {
|
|
21701
|
+
try {
|
|
21702
|
+
await run();
|
|
21703
|
+
} catch {
|
|
21704
|
+
result.failedSteps.push(name);
|
|
21705
|
+
diagnosticConsole.error({ event: "card_refund_sweep_step_failed", stage: name });
|
|
21706
|
+
}
|
|
21707
|
+
};
|
|
21708
|
+
await step("disputes", () => applyDisputes(deps, nowMs, result));
|
|
21709
|
+
const { listExpiredCardCredits, openCardExpiryRefund } = deps.ledger;
|
|
21710
|
+
if (listExpiredCardCredits && openCardExpiryRefund) {
|
|
21711
|
+
await step("expiry", async () => {
|
|
21712
|
+
for (const creditId of await listExpiredCardCredits.call(deps.ledger, { nowMs, limit })) {
|
|
21713
|
+
try {
|
|
21714
|
+
const drain = await openCardExpiryRefund.call(deps.ledger, { creditId, nowMs });
|
|
21715
|
+
if (drain) result.expiryRefunds.push(creditId);
|
|
21716
|
+
} catch {
|
|
21717
|
+
diagnosticConsole.error({ event: "card_refund_expiry_failed", credit_id: creditId });
|
|
21718
|
+
}
|
|
21719
|
+
}
|
|
21720
|
+
});
|
|
21721
|
+
}
|
|
21722
|
+
await step("retry", async () => {
|
|
21723
|
+
const pending = (await deps.ledger.listPendingDrains()).filter(
|
|
21724
|
+
(drain) => drain.method === "stripe" && drain.status === "pending"
|
|
21725
|
+
);
|
|
21726
|
+
for (const drain of pending.slice(0, limit)) {
|
|
21727
|
+
try {
|
|
21728
|
+
const { drain: after } = await runStripeDrain(legDeps, drain);
|
|
21729
|
+
(after.status === "sent" ? result.completed : result.pending).push(
|
|
21730
|
+
`${drain.creditId}:${drain.drainId}`
|
|
21731
|
+
);
|
|
21732
|
+
} catch {
|
|
21733
|
+
result.pending.push(`${drain.creditId}:${drain.drainId}`);
|
|
21734
|
+
}
|
|
21735
|
+
}
|
|
21736
|
+
});
|
|
21737
|
+
const journal = deps.journal;
|
|
21738
|
+
if (journal && deps.ledger.cardFundingBooked) {
|
|
21739
|
+
await step("stranded", async () => {
|
|
21740
|
+
result.strandedRefunds = await refundStranded(deps, journal, nowMs, limit);
|
|
21741
|
+
});
|
|
21742
|
+
}
|
|
21743
|
+
return result;
|
|
21744
|
+
}
|
|
21745
|
+
function startCardRefundSweep(deps, opts = {}) {
|
|
21746
|
+
if (!deps.ledger.saveCardRefund) return void 0;
|
|
21747
|
+
const intervalMs = opts.intervalMs ?? 60 * 60 * 1e3;
|
|
21748
|
+
const bootDelayMs = opts.bootDelayMs ?? 3e4;
|
|
21749
|
+
let stopped = false;
|
|
21750
|
+
const runOnce = () => runCardRefundSweep(deps, { limit: opts.limit });
|
|
21751
|
+
const pass = () => {
|
|
21752
|
+
if (stopped) return;
|
|
21753
|
+
void runOnce().then((summary) => {
|
|
21754
|
+
const moved = summary.frozen.length + summary.unfrozen.length + summary.expiryRefunds.length + summary.completed.length + summary.pending.length + summary.strandedRefunds.length;
|
|
21755
|
+
if (moved === 0) return;
|
|
21756
|
+
diagnosticConsole.log({
|
|
21757
|
+
event: "card_refund_sweep",
|
|
21758
|
+
frozen: summary.frozen.length,
|
|
21759
|
+
unfrozen: summary.unfrozen.length,
|
|
21760
|
+
expiry_refunds: summary.expiryRefunds.length,
|
|
21761
|
+
completed: summary.completed.length,
|
|
21762
|
+
pending: summary.pending.length,
|
|
21763
|
+
stranded_refunds: summary.strandedRefunds.length
|
|
21764
|
+
});
|
|
21765
|
+
}).catch(() => {
|
|
21766
|
+
diagnosticConsole.error({ event: "card_refund_sweep_failed" });
|
|
21767
|
+
});
|
|
21768
|
+
};
|
|
21769
|
+
const bootTimer = bootDelayMs > 0 ? setTimeout(pass, bootDelayMs) : void 0;
|
|
21770
|
+
bootTimer?.unref();
|
|
21771
|
+
const intervalTimer = intervalMs > 0 ? setInterval(pass, intervalMs) : void 0;
|
|
21772
|
+
intervalTimer?.unref();
|
|
21773
|
+
return {
|
|
21774
|
+
stop() {
|
|
21775
|
+
stopped = true;
|
|
21776
|
+
if (bootTimer) clearTimeout(bootTimer);
|
|
21777
|
+
if (intervalTimer) clearInterval(intervalTimer);
|
|
21778
|
+
},
|
|
21779
|
+
runOnce
|
|
21780
|
+
};
|
|
21781
|
+
}
|
|
21782
|
+
async function applyDisputes(deps, nowMs, result) {
|
|
21783
|
+
const freeze = deps.ledger.freezeCardCreditsForPaymentIntent?.bind(deps.ledger);
|
|
21784
|
+
if (!freeze) return;
|
|
21785
|
+
const statusesByIntent = /* @__PURE__ */ new Map();
|
|
21786
|
+
const createdGte = Math.floor((nowMs - DISPUTE_LOOKBACK_MS) / 1e3);
|
|
21787
|
+
let startingAfter;
|
|
21788
|
+
try {
|
|
21789
|
+
for (let page = 0; page < MAX_DISPUTE_PAGES; page++) {
|
|
21790
|
+
const { disputes, hasMore } = await deps.stripe.listDisputes({
|
|
21791
|
+
createdGte,
|
|
21792
|
+
...startingAfter && { startingAfter }
|
|
21793
|
+
});
|
|
21794
|
+
for (const dispute of disputes) {
|
|
21795
|
+
if (!dispute.paymentIntentId) continue;
|
|
21796
|
+
const statuses = statusesByIntent.get(dispute.paymentIntentId) ?? [];
|
|
21797
|
+
statuses.push(dispute.status);
|
|
21798
|
+
statusesByIntent.set(dispute.paymentIntentId, statuses);
|
|
21799
|
+
}
|
|
21800
|
+
if (!hasMore || disputes.length === 0) break;
|
|
21801
|
+
startingAfter = disputes[disputes.length - 1].id;
|
|
21802
|
+
}
|
|
21803
|
+
} finally {
|
|
21804
|
+
const blocking = new Set(
|
|
21805
|
+
[...statusesByIntent].filter(([, statuses]) => !allSettledForMerchant(statuses)).map(([paymentIntentId]) => paymentIntentId)
|
|
21806
|
+
);
|
|
21807
|
+
for (const paymentIntentId of blocking) {
|
|
21808
|
+
result.frozen.push(...await freeze(paymentIntentId));
|
|
21809
|
+
}
|
|
21810
|
+
for (const paymentIntentId of statusesByIntent.keys()) {
|
|
21811
|
+
if (!blocking.has(paymentIntentId)) result.unfrozen.push(...await unfreezeSettled(deps, paymentIntentId));
|
|
21812
|
+
}
|
|
21813
|
+
}
|
|
21814
|
+
}
|
|
21815
|
+
async function unfreezeSettled(deps, paymentIntentId) {
|
|
21816
|
+
const candidates = deps.ledger.disputedCardCreditsFundedBy?.bind(deps.ledger);
|
|
21817
|
+
const unfreeze = deps.ledger.unfreezeCardCredit?.bind(deps.ledger);
|
|
21818
|
+
if (!candidates || !unfreeze) return [];
|
|
21819
|
+
const unfrozen = [];
|
|
21820
|
+
for (const credit of await candidates(paymentIntentId)) {
|
|
21821
|
+
let settled = true;
|
|
21822
|
+
for (const fundingIntent of credit.paymentIntentIds) {
|
|
21823
|
+
const { disputes, hasMore } = await deps.stripe.listDisputesForPaymentIntent(fundingIntent);
|
|
21824
|
+
if (hasMore || !allSettledForMerchant(disputes.map((dispute) => dispute.status))) {
|
|
21825
|
+
settled = false;
|
|
21826
|
+
break;
|
|
21827
|
+
}
|
|
21828
|
+
}
|
|
21829
|
+
if (settled && await unfreeze(credit.creditId)) unfrozen.push(credit.creditId);
|
|
21830
|
+
}
|
|
21831
|
+
return unfrozen;
|
|
21832
|
+
}
|
|
21833
|
+
function allSettledForMerchant(statuses) {
|
|
21834
|
+
return statuses.every((status) => DISPUTE_SETTLED_FOR_MERCHANT.has(status));
|
|
21835
|
+
}
|
|
21836
|
+
async function refundStranded(deps, journal, nowMs, limit) {
|
|
21837
|
+
const booked = deps.ledger.cardFundingBooked?.bind(deps.ledger);
|
|
21838
|
+
if (!booked) return [];
|
|
21839
|
+
const refunded = [];
|
|
21840
|
+
const attempts = await journal.listUnresolved({ olderThanMs: nowMs - STRANDED_AFTER_MS, limit });
|
|
21841
|
+
for (const attempt of attempts) {
|
|
21842
|
+
try {
|
|
21843
|
+
const intents = await deps.stripe.searchPaymentIntentsByMetadata(
|
|
21844
|
+
"mpp_challenge_id",
|
|
21845
|
+
attempt.challengeId
|
|
21846
|
+
);
|
|
21847
|
+
if (intents.length === 0) {
|
|
21848
|
+
if (nowMs - attempt.createdAt > STRANDED_SEARCH_WINDOW_MS) {
|
|
21849
|
+
await journal.resolve(attempt.challengeId, "not_found");
|
|
21850
|
+
}
|
|
21851
|
+
continue;
|
|
21852
|
+
}
|
|
21853
|
+
let unsettled = false;
|
|
21854
|
+
let refundedHere = 0;
|
|
21855
|
+
for (const intent of intents) {
|
|
21856
|
+
if (intent.status !== "succeeded") {
|
|
21857
|
+
if (intent.status !== "canceled" && intent.status !== "requires_payment_method") {
|
|
21858
|
+
unsettled = true;
|
|
21859
|
+
}
|
|
21860
|
+
continue;
|
|
21861
|
+
}
|
|
21862
|
+
if (await booked(intent.id)) continue;
|
|
21863
|
+
const charge = await deps.stripe.retrieveCharge(intent.id);
|
|
21864
|
+
const owed = charge.amount - charge.amountRefunded;
|
|
21865
|
+
if (owed > 0 && !charge.disputed) {
|
|
21866
|
+
await deps.stripe.createRefund({
|
|
21867
|
+
paymentIntentId: intent.id,
|
|
21868
|
+
amount: owed,
|
|
21869
|
+
idempotencyKey: `dvmkit-stranded-${intent.id}`,
|
|
21870
|
+
metadata: { dvmkit_stranded: "true" }
|
|
21871
|
+
});
|
|
21872
|
+
refunded.push(intent.id);
|
|
21873
|
+
refundedHere += 1;
|
|
21874
|
+
diagnosticConsole.warn({ event: "card_charge_stranded_refunded", payment_intent: intent.id });
|
|
21875
|
+
}
|
|
21876
|
+
}
|
|
21877
|
+
if (!unsettled) {
|
|
21878
|
+
await journal.resolve(attempt.challengeId, refundedHere > 0 ? "refunded" : "settled");
|
|
21879
|
+
} else if (nowMs - attempt.createdAt > STRANDED_ABANDON_AFTER_MS) {
|
|
21880
|
+
diagnosticConsole.error({ event: "card_charge_unsettled", challenge_id: attempt.challengeId });
|
|
21881
|
+
await journal.resolve(attempt.challengeId, "unsettled");
|
|
21882
|
+
}
|
|
21883
|
+
} catch {
|
|
21884
|
+
}
|
|
21885
|
+
}
|
|
21886
|
+
return refunded;
|
|
21887
|
+
}
|
|
21888
|
+
|
|
20770
21889
|
// src/sdk/server/credit-expiry-sweep.ts
|
|
20771
21890
|
function startCreditExpirySweep(ledger, opts = {}) {
|
|
20772
21891
|
const sweepExpiredCredits = ledger.sweepExpiredCredits?.bind(ledger);
|
|
@@ -21740,7 +22859,7 @@ initSchemaForm();
|
|
|
21740
22859
|
}
|
|
21741
22860
|
|
|
21742
22861
|
// src/sdk/server/request-fingerprint.ts
|
|
21743
|
-
import { createHash as
|
|
22862
|
+
import { createHash as createHash12 } from "crypto";
|
|
21744
22863
|
function requestFingerprint(req, opts) {
|
|
21745
22864
|
const canonical = canonicalize({
|
|
21746
22865
|
capability: req.capability,
|
|
@@ -21748,7 +22867,7 @@ function requestFingerprint(req, opts) {
|
|
|
21748
22867
|
params: req.params ?? {},
|
|
21749
22868
|
data: opts.stripAuthEnvelope ? stripEnvelope(req.data) : req.data
|
|
21750
22869
|
});
|
|
21751
|
-
return
|
|
22870
|
+
return createHash12("sha256").update(canonical).digest("hex");
|
|
21752
22871
|
}
|
|
21753
22872
|
var ENVELOPE_KEYS = /* @__PURE__ */ new Set(["pubkey", "signature", "timestamp", "nonce", "auth_statement"]);
|
|
21754
22873
|
function stripEnvelope(data) {
|
|
@@ -21786,6 +22905,7 @@ async function createDVMServer(descriptor, opts) {
|
|
|
21786
22905
|
let payoutReporter;
|
|
21787
22906
|
let unsubscribeTempoSettlementReadiness;
|
|
21788
22907
|
let creditExpirySweep;
|
|
22908
|
+
let cardRefundSweep;
|
|
21789
22909
|
let x402ChannelStorage;
|
|
21790
22910
|
let x402BatchSettlement;
|
|
21791
22911
|
let server;
|
|
@@ -21818,6 +22938,7 @@ async function createDVMServer(descriptor, opts) {
|
|
|
21818
22938
|
await capture(() => payoutReporter?.stop());
|
|
21819
22939
|
await capture(() => revenueReporter?.stop());
|
|
21820
22940
|
await capture(() => creditExpirySweep?.stop());
|
|
22941
|
+
await capture(() => cardRefundSweep?.stop());
|
|
21821
22942
|
await capture(() => unsubscribeTempoSettlementReadiness?.());
|
|
21822
22943
|
if (settlementStopped) await capture(() => settlementStopped);
|
|
21823
22944
|
if (x402ChannelStorage) {
|
|
@@ -21871,7 +22992,7 @@ async function createDVMServer(descriptor, opts) {
|
|
|
21871
22992
|
}
|
|
21872
22993
|
}
|
|
21873
22994
|
if (!opts.onJobCompleted && platformToken && platformUrl && dvmId && opts.db) {
|
|
21874
|
-
const { RevenueReporter } = await import("./revenue-reporter-
|
|
22995
|
+
const { RevenueReporter } = await import("./revenue-reporter-FD3GDQZS.js");
|
|
21875
22996
|
const reporter = new RevenueReporter(
|
|
21876
22997
|
opts.db,
|
|
21877
22998
|
platformUrl,
|
|
@@ -21881,7 +23002,7 @@ async function createDVMServer(descriptor, opts) {
|
|
|
21881
23002
|
revenueReporter = reporter;
|
|
21882
23003
|
await reporter.init();
|
|
21883
23004
|
reporter.startRetryLoop();
|
|
21884
|
-
const { MemoryX402BatchStore, PayoutReporter } = await import("./payout-reporter-
|
|
23005
|
+
const { MemoryX402BatchStore, PayoutReporter } = await import("./payout-reporter-KTLBNJJ7.js");
|
|
21885
23006
|
payoutReporter = new PayoutReporter({
|
|
21886
23007
|
dvmId,
|
|
21887
23008
|
db: opts.db,
|
|
@@ -22087,6 +23208,18 @@ async function createDVMServer(descriptor, opts) {
|
|
|
22087
23208
|
payoutReporter
|
|
22088
23209
|
});
|
|
22089
23210
|
await server.jobManager.recoverTerminalCosts();
|
|
23211
|
+
if (opts.stripeCredit) {
|
|
23212
|
+
cardRefundSweep = startCardRefundSweep(
|
|
23213
|
+
{
|
|
23214
|
+
ledger: creditLedger,
|
|
23215
|
+
stripe: opts.stripeCredit.refunds,
|
|
23216
|
+
drainReport: { db: opts.db, dvmId, enqueueCreditDrain },
|
|
23217
|
+
...server.drainReceiptIssuer && { receiptIssuer: server.drainReceiptIssuer },
|
|
23218
|
+
...opts.stripeCredit.chargeJournal && { journal: opts.stripeCredit.chargeJournal }
|
|
23219
|
+
},
|
|
23220
|
+
opts.creditExpirySweep
|
|
23221
|
+
);
|
|
23222
|
+
}
|
|
22090
23223
|
return {
|
|
22091
23224
|
app: server.app,
|
|
22092
23225
|
authAudience: server.authAudience,
|
|
@@ -22137,6 +23270,10 @@ var DVMServer = class {
|
|
|
22137
23270
|
tempoSweepLastAttemptMs = 0;
|
|
22138
23271
|
receiptIssuer;
|
|
22139
23272
|
creditLedger;
|
|
23273
|
+
/** Countersigns reclaim events outside a request, such as a card refund the sweep finishes. */
|
|
23274
|
+
get drainReceiptIssuer() {
|
|
23275
|
+
return this.receiptIssuer;
|
|
23276
|
+
}
|
|
22140
23277
|
processedPayments;
|
|
22141
23278
|
authAudience;
|
|
22142
23279
|
/** Lazily constructed when a refreshed facilitator probe gains batch support. */
|
|
@@ -22582,6 +23719,7 @@ var DVMServer = class {
|
|
|
22582
23719
|
// DVM wired with an in-memory session store resolves to a rail with no
|
|
22583
23720
|
// close-intent read and the verbs answer 503.
|
|
22584
23721
|
tempoDrainRail: () => Promise.resolve(this.opts.mpp),
|
|
23722
|
+
...this.opts.stripeCredit && { stripeRefunds: this.opts.stripeCredit.refunds },
|
|
22585
23723
|
...this.opts.hostedSettlementReports ? {
|
|
22586
23724
|
hostedSettlementReports: { store: this.opts.hostedSettlementReports }
|
|
22587
23725
|
} : {},
|
|
@@ -22823,7 +23961,7 @@ var DVMServer = class {
|
|
|
22823
23961
|
const dvmId = this.opts.dvmId ?? `dev:${this.descriptor.name}`;
|
|
22824
23962
|
return {
|
|
22825
23963
|
dvm_id: dvmId,
|
|
22826
|
-
builder_pubkey:
|
|
23964
|
+
builder_pubkey: createHash13("sha256").update(`dvmkit-dev:${dvmId}`).digest("hex")
|
|
22827
23965
|
};
|
|
22828
23966
|
}
|
|
22829
23967
|
throw new AuthAudienceError();
|
|
@@ -22925,6 +24063,7 @@ var DVMServer = class {
|
|
|
22925
24063
|
methods.push("x402");
|
|
22926
24064
|
}
|
|
22927
24065
|
if (this.opts.lightningReceive?.available()) methods.push("lightning");
|
|
24066
|
+
if (this.opts.stripeCredit) methods.push("stripe");
|
|
22928
24067
|
return methods;
|
|
22929
24068
|
}
|
|
22930
24069
|
creditMppMethods() {
|
|
@@ -23133,6 +24272,7 @@ var DVMServer = class {
|
|
|
23133
24272
|
tempoWithheld: mppSplit.withheld,
|
|
23134
24273
|
x402Schemes: this.creditX402Schemes(),
|
|
23135
24274
|
lightningFundingMinSats: this.opts.lightningReceive?.fundingMinSats,
|
|
24275
|
+
stripeNetworkId: this.opts.stripeCredit?.networkId,
|
|
23136
24276
|
// Configured, not advertised: `advertisedMints()` is health
|
|
23137
24277
|
// filtered, and a rail that comes and goes with a mint's uptime is worse
|
|
23138
24278
|
// product than one that is honestly absent.
|
|
@@ -23184,7 +24324,7 @@ var DVMServer = class {
|
|
|
23184
24324
|
const paymentAuthorization = ctx.req.header("Authorization");
|
|
23185
24325
|
const mppArtifact = paymentAuthorization ? (await import("mppx")).Credential.extractPaymentScheme(paymentAuthorization) : void 0;
|
|
23186
24326
|
const attachedPayment = ctx.req.header("X-Cashu") ?? ctx.req.header("PAYMENT-SIGNATURE") ?? ctx.req.header("X-PAYMENT") ?? mppArtifact;
|
|
23187
|
-
const paymentArtifactHash = attachedPayment ?
|
|
24327
|
+
const paymentArtifactHash = attachedPayment ? createHash13("sha256").update(attachedPayment).digest("hex") : void 0;
|
|
23188
24328
|
hostingFundingArtifactHash = paymentArtifactHash;
|
|
23189
24329
|
if (!await control.admit(admissionId, workId, {
|
|
23190
24330
|
creditId,
|
|
@@ -23223,7 +24363,8 @@ var DVMServer = class {
|
|
|
23223
24363
|
await control.finishAdmission(admissionId, outcome);
|
|
23224
24364
|
}
|
|
23225
24365
|
} : void 0,
|
|
23226
|
-
tempoSettlementReadiness: this.opts.tempoSettlementReadiness
|
|
24366
|
+
tempoSettlementReadiness: this.opts.tempoSettlementReadiness,
|
|
24367
|
+
stripeCredit: this.opts.stripeCredit
|
|
23227
24368
|
});
|
|
23228
24369
|
}
|
|
23229
24370
|
/**
|
|
@@ -24044,7 +25185,7 @@ var DVMServer = class {
|
|
|
24044
25185
|
const authorization = c.req.header("Authorization");
|
|
24045
25186
|
const mppArtifact = authorization ? (await import("mppx")).Credential.extractPaymentScheme(authorization) : void 0;
|
|
24046
25187
|
const attachedPayment = c.req.header("X-Cashu") ?? c.req.header("PAYMENT-SIGNATURE") ?? c.req.header("X-PAYMENT") ?? mppArtifact;
|
|
24047
|
-
const hostingPaymentArtifactHash = attachedPayment ?
|
|
25188
|
+
const hostingPaymentArtifactHash = attachedPayment ? createHash13("sha256").update(attachedPayment).digest("hex") : void 0;
|
|
24048
25189
|
if (!resumeOnly && this.opts.hostingControlInstance && hostingPaymentArtifactHash) {
|
|
24049
25190
|
const priorId = await this.opts.hostingControlInstance.acceptedJobByPaymentArtifactHash(
|
|
24050
25191
|
hostingPaymentArtifactHash
|
|
@@ -24368,7 +25509,7 @@ var DVMServer = class {
|
|
|
24368
25509
|
}
|
|
24369
25510
|
const isAnonymous = requesterId === "anonymous" || requesterId === "dev-anonymous";
|
|
24370
25511
|
const jobToken = isAnonymous ? randomBytes5(32).toString("hex") : void 0;
|
|
24371
|
-
const requesterTokenHash = jobToken ?
|
|
25512
|
+
const requesterTokenHash = jobToken ? createHash13("sha256").update(jobToken).digest("hex") : void 0;
|
|
24372
25513
|
const job = jm.createJob(
|
|
24373
25514
|
normalisedBody,
|
|
24374
25515
|
requesterId,
|
|
@@ -25226,7 +26367,7 @@ function isJobOwner(c, job) {
|
|
|
25226
26367
|
if (job.requesterTokenHash) {
|
|
25227
26368
|
const token = c.req.header("X-Job-Token");
|
|
25228
26369
|
if (!token) return false;
|
|
25229
|
-
const provided =
|
|
26370
|
+
const provided = createHash13("sha256").update(token).digest("hex");
|
|
25230
26371
|
return timingSafeEqualHex(provided, job.requesterTokenHash);
|
|
25231
26372
|
}
|
|
25232
26373
|
return c.get("requesterId") === job.requesterId;
|
|
@@ -25780,11 +26921,14 @@ export {
|
|
|
25780
26921
|
extractCreditEnvelope,
|
|
25781
26922
|
creditEnvelopeIgnoreFields,
|
|
25782
26923
|
stripCreditEnvelope,
|
|
26924
|
+
STRIPE_CREDIT_MIN_MICRO,
|
|
25783
26925
|
buildCreditMenu,
|
|
25784
26926
|
selectPrimaryCredit,
|
|
25785
26927
|
toCreditView,
|
|
25786
26928
|
attachCreditMenu,
|
|
25787
26929
|
toCreditTerms,
|
|
26930
|
+
fiatMicroToMethodAmount,
|
|
26931
|
+
createStripeRefundApi,
|
|
25788
26932
|
callerInputIssues,
|
|
25789
26933
|
resolvePriceFiat,
|
|
25790
26934
|
priceFiatMicro,
|