@dvmkit/sdk 0.3.4-rc.10 → 0.5.0-rc.1

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.
Files changed (56) hide show
  1. package/dist/{chunk-2KGG5ORW.js → chunk-7ZUKL4IV.js} +3 -3
  2. package/dist/{chunk-KXZUCCEY.js → chunk-A4YFWZ7R.js} +4 -55
  3. package/dist/{chunk-C2AGARLM.js → chunk-B2IROPQF.js} +450 -83
  4. package/dist/{chunk-MKI6OVW4.js → chunk-CZULM42S.js} +0 -51
  5. package/dist/{chunk-UZC4A4DM.js → chunk-EYTHFFFU.js} +2489 -213
  6. package/dist/{chunk-M4QDXD2A.js → chunk-FYRR2UEZ.js} +1 -1
  7. package/dist/{chunk-4UZN2GP5.js → chunk-GAOAYHZA.js} +209 -33
  8. package/dist/{chunk-YYQG4R7T.js → chunk-GFY5FRX3.js} +6 -4
  9. package/dist/{chunk-Z4BNLUZF.js → chunk-IWQHUSMO.js} +3 -105
  10. package/dist/{chunk-4J32DTLT.js → chunk-IXR5RFK7.js} +82 -4
  11. package/dist/{chunk-2SD2W2BG.js → chunk-JH3SX3LY.js} +252 -26
  12. package/dist/chunk-KA6VN5FK.js +245 -0
  13. package/dist/{chunk-YRDA7EWN.js → chunk-LE74UEV7.js} +61 -16
  14. package/dist/chunk-LIRAHP7B.js +53 -0
  15. package/dist/{chunk-MG67KXU7.js → chunk-MZRJR2CW.js} +4 -2
  16. package/dist/chunk-OFVV37XC.js +57 -0
  17. package/dist/{chunk-A4CM2BD3.js → chunk-PAIM66YA.js} +91 -2
  18. package/dist/{chunk-JDFLRNAZ.js → chunk-PB4FYZET.js} +67 -7
  19. package/dist/{chunk-BQ2NMWKE.js → chunk-PLG4XER6.js} +1 -1
  20. package/dist/chunk-RLTSCKRI.js +109 -0
  21. package/dist/{chunk-VAMSJRJT.js → chunk-TUA7PYH7.js} +27 -105
  22. package/dist/{chunk-O3QDFPKM.js → chunk-TZ2WKCDD.js} +1 -1
  23. package/dist/{chunk-Z3EHBW2G.js → chunk-UOCZXO2Z.js} +7 -5
  24. package/dist/{chunk-EAMZZ2B4.js → chunk-X7EZ5CLI.js} +59 -0
  25. package/dist/{chunk-CE7XEHYC.js → chunk-Z4KLECAC.js} +1 -1
  26. package/dist/{chunk-7X6UDVCQ.js → chunk-Z4ZT5LNT.js} +66 -2
  27. package/dist/{credit-ledger-A35WH2TI.js → credit-ledger-MI2H7NEC.js} +3 -1
  28. package/dist/{credit-menu-0-UtJ3hh.d.ts → credit-menu-CAfqe8IY.d.ts} +133 -7
  29. package/dist/{fx-DhyT1Cjh.d.ts → fx-D9nsN3n4.d.ts} +1 -1
  30. package/dist/index.d.ts +6 -6
  31. package/dist/index.js +4 -3
  32. package/dist/internal/caller.d.ts +80 -13
  33. package/dist/internal/caller.js +56 -54
  34. package/dist/internal/index.js +1 -1
  35. package/dist/internal/server.d.ts +11 -85
  36. package/dist/internal/server.js +21 -18
  37. package/dist/{job-store-D3CcZp_g.d.ts → job-store-Bx-VFRjg.d.ts} +1 -1
  38. package/dist/{lightning-backend-BhsyrBLa.d.ts → lightning-backend-BAx2Y8pP.d.ts} +1 -1
  39. package/dist/{memory-credit-ledger-2LBZVMKJ.js → memory-credit-ledger-W2BAUYJP.js} +4 -2
  40. package/dist/{mpp-setup-7CGBKNUP.js → mpp-setup-RF5FXGGV.js} +5 -1
  41. package/dist/{payout-reporter-ZMOA37GN.js → payout-reporter-3Y2U55ZX.js} +1 -1
  42. package/dist/{postgres-job-store-MSD2HZOI.js → postgres-job-store-VLXLWXUQ.js} +1 -1
  43. package/dist/{revenue-reporter-EKJOGQYR.js → revenue-reporter-MAERPYGK.js} +1 -1
  44. package/dist/server/index.d.ts +14 -10
  45. package/dist/server/index.js +57 -19
  46. package/dist/{step-cache-v2yhyhSZ.d.ts → step-cache-QG_h5-3G.d.ts} +1769 -1549
  47. package/dist/{tempo-charge-store-4WX7FPOV.js → tempo-charge-store-Q3D7KYMH.js} +1 -1
  48. package/dist/tempo-lifecycle-ZBBLFBMH.js +15 -0
  49. package/dist/{tempo-session-store-LCSDHB7G.js → tempo-session-store-ZRNCOX7P.js} +1 -1
  50. package/dist/{tempo-wallet-RCGBBAJH.js → tempo-wallet-B5XXV3YH.js} +4 -3
  51. package/dist/testing/index.d.ts +3 -3
  52. package/dist/{usd-Cu9G6xv2.d.ts → usd-EwRqCPF-.d.ts} +1 -1
  53. package/dist/{x402-S47Q77UE.js → x402-R6HPBY5D.js} +18 -14
  54. package/package.json +1 -1
  55. package/dist/chunk-YHN3WMMF.js +0 -122
  56. package/dist/tempo-lifecycle-TJ62A7UX.js +0 -13
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  MemoryProcessedPaymentStore,
3
+ PostgresProcessedPaymentStore,
3
4
  ProcessedPaymentReplayError
4
5
  } from "./chunk-XRLKGCAW.js";
5
6
  import {
@@ -7,13 +8,16 @@ import {
7
8
  createX402FacilitatorClient,
8
9
  settleWithFacilitator,
9
10
  verifyWithFacilitator
10
- } from "./chunk-BQ2NMWKE.js";
11
+ } from "./chunk-PLG4XER6.js";
11
12
  import {
12
13
  MemoryCreditLedger
13
- } from "./chunk-M4QDXD2A.js";
14
+ } from "./chunk-FYRR2UEZ.js";
14
15
  import {
15
16
  PostgresJobStore
16
- } from "./chunk-JDFLRNAZ.js";
17
+ } from "./chunk-PB4FYZET.js";
18
+ import {
19
+ parseStoreValue
20
+ } from "./chunk-YG7G4DPZ.js";
17
21
  import {
18
22
  SIGNED_ENVELOPE_FIELDS,
19
23
  SignedRequestError,
@@ -21,7 +25,7 @@ import {
21
25
  humanizeZodIssues,
22
26
  signedRequestInput,
23
27
  zodIssuesOf
24
- } from "./chunk-CE7XEHYC.js";
28
+ } from "./chunk-Z4KLECAC.js";
25
29
  import {
26
30
  JobCancelledError,
27
31
  MemoryJobStore,
@@ -48,7 +52,6 @@ import {
48
52
  attestedDvmIdentity,
49
53
  buildAdminChallenge,
50
54
  checkMintHealth,
51
- computeResultHash,
52
55
  createMonotonicClock,
53
56
  getWallet,
54
57
  isPublicRequestId,
@@ -57,13 +60,10 @@ import {
57
60
  normaliseMintUrl,
58
61
  parseTraceparent,
59
62
  receiptPubkeyFromSecret,
60
- signDrainReceipt,
61
- signFundingReceipt,
62
- signReceipt,
63
63
  toLockPubkey,
64
64
  verifyCashuToken,
65
65
  verifyChallenge
66
- } from "./chunk-VAMSJRJT.js";
66
+ } from "./chunk-TUA7PYH7.js";
67
67
  import {
68
68
  X402_BATCH_SETTLEMENT_SCHEME,
69
69
  X402_DEFAULT_FACILITATOR,
@@ -77,25 +77,30 @@ import {
77
77
  encodeSettleResponseHeader,
78
78
  exactEvmAuthorization,
79
79
  paymentRequiredV2FromV1,
80
- x402NetworkByCaip2,
81
80
  x402NetworkToCaip2
82
- } from "./chunk-Z4BNLUZF.js";
81
+ } from "./chunk-IWQHUSMO.js";
83
82
  import {
84
83
  IMPLICIT_CREDIT_TTL_MS,
84
+ computeResultHash,
85
85
  formatStaleness,
86
86
  msatsToUsd,
87
87
  msatsToUsdc,
88
+ signDrainReceipt,
89
+ signFundingReceipt,
90
+ signReceipt,
88
91
  usdcToMsats
89
- } from "./chunk-A4CM2BD3.js";
92
+ } from "./chunk-PAIM66YA.js";
90
93
  import {
91
94
  resolveRpcOverride
92
- } from "./chunk-MKI6OVW4.js";
95
+ } from "./chunk-CZULM42S.js";
93
96
  import {
94
97
  RAIL_REFUNDABLE,
95
98
  canonicalRequestPath,
96
- canonicalize,
97
99
  isZodSchema
98
- } from "./chunk-KXZUCCEY.js";
100
+ } from "./chunk-A4YFWZ7R.js";
101
+ import {
102
+ canonicalize
103
+ } from "./chunk-OFVV37XC.js";
99
104
  import {
100
105
  StepCache,
101
106
  createConsoleLogger,
@@ -120,6 +125,7 @@ import {
120
125
  satsToFiat
121
126
  } from "./chunk-AT6V3SY7.js";
122
127
  import {
128
+ CreditLedger,
123
129
  CreditLedgerError,
124
130
  DRAIN_METHODS,
125
131
  PostgresX402ChannelStorage,
@@ -133,7 +139,7 @@ import {
133
139
  isNonChannelBitcoinRail,
134
140
  netOwedSats,
135
141
  x402SettlementPending
136
- } from "./chunk-2SD2W2BG.js";
142
+ } from "./chunk-JH3SX3LY.js";
137
143
  import {
138
144
  withSdkInitLock
139
145
  } from "./chunk-COYKJRMM.js";
@@ -141,10 +147,18 @@ import {
141
147
  withPostgresRecoveryContext
142
148
  } from "./chunk-P6QON7ER.js";
143
149
  import {
150
+ x402NetworkByCaip2
151
+ } from "./chunk-RLTSCKRI.js";
152
+ import {
153
+ TEMPO_MODERATO_CHAIN_ID,
154
+ TEMPO_MODERATO_RPC,
144
155
  advertisedMethods,
145
156
  challengeMeta,
146
- isTempoInsufficientFundsError
147
- } from "./chunk-4J32DTLT.js";
157
+ installHostedTempoChargeGuard,
158
+ installHostedTempoCloseGuard,
159
+ isTempoInsufficientFundsError,
160
+ resolveTempoNetwork
161
+ } from "./chunk-IXR5RFK7.js";
148
162
  import {
149
163
  createDiagnosticCapabilityAuthority,
150
164
  diagnosticConsole
@@ -205,7 +219,7 @@ async function seedLockPubkeyRows(db, dvmId, pubkey, now) {
205
219
  }
206
220
  }
207
221
  async function loadLockPubkeyState(db, dvmId, graceMs, now = Date.now()) {
208
- const { rows } = await db.query(
222
+ const { rows: rows2 } = await db.query(
209
223
  `SELECT pubkey, retired_at_ms
210
224
  FROM dvm_lock_pubkeys
211
225
  WHERE dvm_id = $1
@@ -215,7 +229,7 @@ async function loadLockPubkeyState(db, dvmId, graceMs, now = Date.now()) {
215
229
  );
216
230
  let current = null;
217
231
  const retired = [];
218
- for (const r of rows) {
232
+ for (const r of rows2) {
219
233
  if (r.retired_at_ms == null) {
220
234
  current = toLockPubkey(r.pubkey);
221
235
  } else {
@@ -234,19 +248,19 @@ async function rotateLockPubkey(db, dvmId, newPubkey, graceMs, now = Date.now())
234
248
  const client = await db.connect();
235
249
  try {
236
250
  await client.query("BEGIN");
237
- const { rows } = await client.query(
251
+ const { rows: rows2 } = await client.query(
238
252
  `SELECT pubkey FROM dvm_lock_pubkeys
239
253
  WHERE dvm_id = $1 AND retired_at_ms IS NULL
240
254
  FOR UPDATE`,
241
255
  [dvmId]
242
256
  );
243
- if (rows.length === 0) {
257
+ if (rows2.length === 0) {
244
258
  await client.query("ROLLBACK");
245
259
  throw new Error(
246
260
  `dvm_lock_pubkeys: cannot rotate dvm_id=${dvmId} \u2014 no current pubkey exists.`
247
261
  );
248
262
  }
249
- if (rows[0].pubkey === normalised) {
263
+ if (rows2[0].pubkey === normalised) {
250
264
  await client.query("ROLLBACK");
251
265
  return await loadLockPubkeyState(db, dvmId, graceMs, now);
252
266
  }
@@ -356,10 +370,10 @@ async function createWalletAccumulatorTable(db) {
356
370
  await db.query(`ALTER TABLE wallet_accumulator DROP COLUMN IF EXISTS spend_authorization;`);
357
371
  }
358
372
  async function backfillProofSecretHash(db) {
359
- const { rows } = await db.query(
373
+ const { rows: rows2 } = await db.query(
360
374
  `SELECT id, proof_secret FROM wallet_accumulator WHERE proof_secret_hash IS NULL`
361
375
  );
362
- for (const r of rows) {
376
+ for (const r of rows2) {
363
377
  await db.query(`UPDATE wallet_accumulator SET proof_secret_hash = $2 WHERE id = $1`, [
364
378
  r.id,
365
379
  hashProofSecret(r.proof_secret)
@@ -393,24 +407,24 @@ async function insertAccumulatorRows(db, args) {
393
407
  }
394
408
  }
395
409
  async function listPendingBatches(db, dvmId) {
396
- const { rows } = await db.query(
410
+ const { rows: rows2 } = await db.query(
397
411
  `SELECT id, lock_pubkey, mint_url, proof_keyset_id, proof_secret, proof_c, proof_amount, proof_dleq, received_at, melt_quote_id, t_expire
398
412
  FROM wallet_accumulator
399
413
  WHERE dvm_id = $1 AND melted_at IS NULL AND melt_status IS NULL
400
414
  ORDER BY t_expire ASC NULLS LAST, lock_pubkey, mint_url, received_at ASC`,
401
415
  [dvmId]
402
416
  );
403
- return groupRowsIntoBatches(rows);
417
+ return groupRowsIntoBatches(rows2);
404
418
  }
405
419
  async function fetchRowsForMelt(db, dvmId, rowIds) {
406
420
  if (rowIds.length === 0) return [];
407
- const { rows } = await db.query(
421
+ const { rows: rows2 } = await db.query(
408
422
  `SELECT id, mint_url, proof_keyset_id, proof_secret, proof_c, proof_amount, melt_status
409
423
  FROM wallet_accumulator
410
424
  WHERE dvm_id = $1 AND id = ANY($2::text[])`,
411
425
  [dvmId, rowIds]
412
426
  );
413
- return rows.map((r) => ({
427
+ return rows2.map((r) => ({
414
428
  id: r.id,
415
429
  mintUrl: r.mint_url,
416
430
  proofKeysetId: r.proof_keyset_id,
@@ -541,9 +555,9 @@ async function insertRows(q, args) {
541
555
  }
542
556
  return inserted;
543
557
  }
544
- function groupRowsIntoBatches(rows) {
558
+ function groupRowsIntoBatches(rows2) {
545
559
  const groups = /* @__PURE__ */ new Map();
546
- for (const r of rows) {
560
+ for (const r of rows2) {
547
561
  const key = JSON.stringify([r.lock_pubkey, r.mint_url, r.melt_quote_id ?? null]);
548
562
  const rowTExpire = r.t_expire == null ? null : Number(r.t_expire);
549
563
  let batch = groups.get(key);
@@ -1258,6 +1272,7 @@ import {
1258
1272
  BatchSettlementEvmScheme as BatchSettlementEvmScheme2,
1259
1273
  InMemoryChannelStorage
1260
1274
  } from "@x402/evm/batch-settlement/server";
1275
+ import { computeChannelId } from "@x402/evm/batch-settlement/client";
1261
1276
  import { createPublicClient as createPublicClient3, http as http3 } from "viem";
1262
1277
  import { privateKeyToAccount as privateKeyToAccount2 } from "viem/accounts";
1263
1278
 
@@ -1590,12 +1605,12 @@ async function findExactPayment(client, intent) {
1590
1605
  );
1591
1606
  for (const log of logs) {
1592
1607
  if (!log.transactionHash) continue;
1593
- const transaction = await client.getTransaction({ hash: log.transactionHash });
1594
- if (!transaction.to || !isAddressEqual(transaction.to, getAddress(intent.asset))) continue;
1608
+ const transaction2 = await client.getTransaction({ hash: log.transactionHash });
1609
+ if (!transaction2.to || !isAddressEqual(transaction2.to, getAddress(intent.asset))) continue;
1595
1610
  try {
1596
1611
  const decoded = decodeFunctionData({
1597
1612
  abi: EXACT_SETTLEMENT_CALL_ABI,
1598
- data: transaction.input
1613
+ data: transaction2.input
1599
1614
  });
1600
1615
  const [from, to, value, validAfter, validBefore, nonce] = decoded.args;
1601
1616
  if (isAddressEqual(from, getAddress(intent.from)) && isAddressEqual(to, getAddress(intent.to)) && value === BigInt(intent.value) && validAfter === BigInt(intent.validAfter) && validBefore === BigInt(intent.validBefore) && nonce.toLowerCase() === intent.nonce.toLowerCase()) {
@@ -1650,12 +1665,12 @@ async function findAppliedSettlement(client, intent) {
1650
1665
  authorizationLogs.flatMap((log) => log.transactionHash ? [log.transactionHash] : [])
1651
1666
  );
1652
1667
  for (const log of depositLogs) {
1653
- const transaction = log.transactionHash;
1654
- if (!transaction || !authorizationTransactions.has(transaction)) continue;
1668
+ const transaction2 = log.transactionHash;
1669
+ if (!transaction2 || !authorizationTransactions.has(transaction2)) continue;
1655
1670
  const amount = logAmount(log);
1656
1671
  if (amount !== BigInt(raw.deposit.amount)) continue;
1657
- if (await transactionMatchesDeposit(client, transaction, intent)) {
1658
- return { transaction, amount: amount.toString() };
1672
+ if (await transactionMatchesDeposit(client, transaction2, intent)) {
1673
+ return { transaction: transaction2, amount: amount.toString() };
1659
1674
  }
1660
1675
  }
1661
1676
  return void 0;
@@ -1672,11 +1687,11 @@ async function findAppliedSettlement(client, intent) {
1672
1687
  toBlock
1673
1688
  );
1674
1689
  for (const log of refundLogs) {
1675
- const transaction = log.transactionHash;
1690
+ const transaction2 = log.transactionHash;
1676
1691
  const amount = logAmount(log);
1677
- if (!transaction || amount === void 0) continue;
1678
- if (await transactionMatchesRefund(client, transaction, intent)) {
1679
- return { transaction, amount: amount.toString() };
1692
+ if (!transaction2 || amount === void 0) continue;
1693
+ if (await transactionMatchesRefund(client, transaction2, intent)) {
1694
+ return { transaction: transaction2, amount: amount.toString() };
1680
1695
  }
1681
1696
  }
1682
1697
  }
@@ -1691,10 +1706,10 @@ async function getLogsInChunks(client, query, fromBlock, toBlock) {
1691
1706
  return logs;
1692
1707
  }
1693
1708
  async function transactionMatchesDeposit(client, transactionHash, intent) {
1694
- const transaction = await client.getTransaction({ hash: transactionHash });
1695
- if (!transaction.to || !isAddressEqual(transaction.to, BATCH_SETTLEMENT_ADDRESS)) return false;
1709
+ const transaction2 = await client.getTransaction({ hash: transactionHash });
1710
+ if (!transaction2.to || !isAddressEqual(transaction2.to, BATCH_SETTLEMENT_ADDRESS)) return false;
1696
1711
  try {
1697
- const decoded = decodeFunctionData({ abi: BATCH_SETTLEMENT_CALL_ABI, data: transaction.input });
1712
+ const decoded = decodeFunctionData({ abi: BATCH_SETTLEMENT_CALL_ABI, data: transaction2.input });
1698
1713
  if (decoded.functionName !== "deposit") return false;
1699
1714
  const [config, amount, collector] = decoded.args;
1700
1715
  const raw = intent.payload.payload;
@@ -1704,9 +1719,9 @@ async function transactionMatchesDeposit(client, transactionHash, intent) {
1704
1719
  }
1705
1720
  }
1706
1721
  async function transactionMatchesRefund(client, transactionHash, intent) {
1707
- const transaction = await client.getTransaction({ hash: transactionHash });
1708
- if (!transaction.to || !isAddressEqual(transaction.to, BATCH_SETTLEMENT_ADDRESS)) return false;
1709
- return calldataMatchesRefund(transaction.input, intent);
1722
+ const transaction2 = await client.getTransaction({ hash: transactionHash });
1723
+ if (!transaction2.to || !isAddressEqual(transaction2.to, BATCH_SETTLEMENT_ADDRESS)) return false;
1724
+ return calldataMatchesRefund(transaction2.input, intent);
1710
1725
  }
1711
1726
  function calldataMatchesRefund(data, intent) {
1712
1727
  try {
@@ -1850,6 +1865,77 @@ var X402SettlementSubmissionError = class extends Error {
1850
1865
  code = "x402_settlement_submission_failed";
1851
1866
  retryable = true;
1852
1867
  };
1868
+ function hostedSettlementFacilitator(facilitator, obligations, network) {
1869
+ return new Proxy(facilitator, {
1870
+ get(target, property, receiver) {
1871
+ if (property !== "settle") {
1872
+ const value = Reflect.get(target, property, receiver);
1873
+ return typeof value === "function" ? value.bind(target) : value;
1874
+ }
1875
+ return async (payment, requirements) => {
1876
+ const payload = payment.payload;
1877
+ const reservations = [];
1878
+ if (payload.type === "claim") {
1879
+ if (!Array.isArray(payload.claims) || !payload.claims.length) {
1880
+ throw new Error("Hosted x402 claim has no selected vouchers");
1881
+ }
1882
+ for (const raw of payload.claims) {
1883
+ const claim = raw;
1884
+ if (!claim.voucher?.channel || !claim.signature || !claim.totalClaimed) {
1885
+ throw new Error("Hosted x402 claim lacks original voucher identity");
1886
+ }
1887
+ const channelId = computeChannelId(claim.voucher.channel, network);
1888
+ const reservation = await obligations.reserveX402(channelId, "claim", {
1889
+ charged: claim.totalClaimed,
1890
+ signature: claim.signature,
1891
+ signedMax: String(claim.voucher.maxClaimableAmount),
1892
+ authorizationPayload: payload,
1893
+ authorizationRequirements: requirements
1894
+ });
1895
+ if (!reservation) throw new Error("Hosted x402 selected voucher is not claimable");
1896
+ if (reservation.replayed) throw new Error("Hosted x402 claim already has an original effect attempt");
1897
+ reservations.push(reservation);
1898
+ }
1899
+ } else if (payload.type === "refund") {
1900
+ const config = payload.channelConfig;
1901
+ if (!config || typeof payload.refundNonce !== "string" || typeof payload.amount !== "string") {
1902
+ throw new Error("Hosted x402 refund lacks original channel identity");
1903
+ }
1904
+ const channelId = computeChannelId(config, network);
1905
+ const claims = payload.claims;
1906
+ if (!Array.isArray(claims) || claims.length > 1 || claims.some((raw) => {
1907
+ const claim = raw;
1908
+ return !claim.voucher?.channel || computeChannelId(claim.voucher.channel, network).toLowerCase() !== channelId.toLowerCase();
1909
+ })) {
1910
+ throw new Error("Hosted x402 refund contains an unbound claim");
1911
+ }
1912
+ const reservation = await obligations.reserveX402(channelId, "refund", {
1913
+ refundNonce: payload.refundNonce,
1914
+ refundAmount: payload.amount,
1915
+ voucherSignature: String(payload.voucher?.signature),
1916
+ signedMax: String(payload.voucher?.maxClaimableAmount),
1917
+ authorizationPayload: payload,
1918
+ authorizationRequirements: requirements
1919
+ });
1920
+ if (!reservation) throw new Error("Hosted x402 selected channel is not refundable");
1921
+ if (reservation.replayed) throw new Error("Hosted x402 refund already has an original effect attempt");
1922
+ reservations.push(reservation);
1923
+ } else if (payload.type === "settle") {
1924
+ const reservation = await obligations.reserveOpenX402Settlement();
1925
+ if (reservation.replayed) throw new Error("Hosted x402 settle already has an original effect attempt");
1926
+ reservations.push(reservation);
1927
+ }
1928
+ const response = await target.settle(payment, requirements);
1929
+ if (response.success && reservations.length) {
1930
+ for (const reservation of reservations) {
1931
+ await obligations.recordSubmitted(reservation.id, response.transaction);
1932
+ }
1933
+ }
1934
+ return response;
1935
+ };
1936
+ }
1937
+ });
1938
+ }
1853
1939
  async function createX402BatchSettlementServer(opts) {
1854
1940
  const selfRelayEnabled = !!opts.config.selfRelay;
1855
1941
  if (selfRelayEnabled && opts.network !== X402_BATCH_SETTLEMENT_MAINNET_NETWORK) {
@@ -1897,6 +1983,9 @@ async function createX402BatchSettlementServer(opts) {
1897
1983
  }
1898
1984
  storage = new InMemoryChannelStorage();
1899
1985
  }
1986
+ if (postgres && opts.hostedObligations) {
1987
+ opts.hostedObligations.attachX402SettlementStore(postgres);
1988
+ }
1900
1989
  const selfRelayActive = selfRelayEnabled && !opts.config.facilitatorClient;
1901
1990
  const relayLockOwner = selfRelayActive ? postgres ?? (opts.db ? new PostgresX402ChannelStorage(opts.db) : void 0) : void 0;
1902
1991
  if (selfRelayActive && !relayLockOwner && !opts.allowInMemory) {
@@ -1919,6 +2008,7 @@ async function createX402BatchSettlementServer(opts) {
1919
2008
  ...opts.facilitatorAuth ? { facilitatorAuth: opts.facilitatorAuth } : {}
1920
2009
  });
1921
2010
  const facilitator = opts.supportedResponse && !selfRelayEnabled ? withCachedSupportedResponse(rawFacilitator, opts.supportedResponse) : rawFacilitator;
2011
+ const settlementFacilitator = opts.hostedObligations ? hostedSettlementFacilitator(facilitator, opts.hostedObligations, opts.network) : facilitator;
1922
2012
  const rpcUrl = opts.rpcUrl ?? opts.config.rpcUrl;
1923
2013
  const settlementEvidence = opts.settlementEvidenceReader ?? await createX402SettlementEvidenceReader({
1924
2014
  network: opts.network,
@@ -1934,7 +2024,7 @@ async function createX402BatchSettlementServer(opts) {
1934
2024
  withdrawDelay,
1935
2025
  ...opts.config.onchainStateTtlMs !== void 0 ? { onchainStateTtlMs: opts.config.onchainStateTtlMs } : {}
1936
2026
  });
1937
- const resource = new x402ResourceServer(facilitator).register(
2027
+ const resource = new x402ResourceServer(settlementFacilitator).register(
1938
2028
  opts.network,
1939
2029
  scheme
1940
2030
  );
@@ -1943,7 +2033,7 @@ async function createX402BatchSettlementServer(opts) {
1943
2033
  if (!network) throw new Error(`No USDC contract configured for ${opts.network}`);
1944
2034
  const token = network.usdc;
1945
2035
  const manager = scheme.createChannelManager(
1946
- facilitator,
2036
+ settlementFacilitator,
1947
2037
  opts.network,
1948
2038
  token
1949
2039
  );
@@ -2044,6 +2134,15 @@ async function createX402BatchSettlementServer(opts) {
2044
2134
  requirements
2045
2135
  };
2046
2136
  }
2137
+ if (args.operation === "fund" && postgres) {
2138
+ if (args.hostingFunding && args.effectId !== `${args.hostingFunding.creditId}:${args.hostingFunding.fundId}`) {
2139
+ throw new Error("x402 funding effect conflicts with its hosted admission");
2140
+ }
2141
+ if (args.hostingFunding && createHash3("sha256").update(args.paymentHeader).digest("hex") !== args.hostingFunding.paymentArtifactHash) {
2142
+ throw new Error("x402 payment differs from its hosted admission artifact");
2143
+ }
2144
+ await postgres.assertHostingFundingReservation(args.hostingFunding);
2145
+ }
2047
2146
  if (isRefund && raw.amount !== void 0) {
2048
2147
  return {
2049
2148
  ok: false,
@@ -2258,7 +2357,7 @@ async function createX402BatchSettlementServer(opts) {
2258
2357
  requirements: matched,
2259
2358
  channelBefore: channelBeforeVerify ?? reserved,
2260
2359
  ...pendingId ? { pendingId } : {}
2261
- });
2360
+ }, args.hostingFunding);
2262
2361
  if (intent.status === "settled" && intent.response) {
2263
2362
  const value = await recoverSettledPayment({
2264
2363
  postgres,
@@ -2315,7 +2414,7 @@ async function createX402BatchSettlementServer(opts) {
2315
2414
  throw error;
2316
2415
  }
2317
2416
  };
2318
- const result = postgres ? await postgres.withChannelTransaction(funding.channelId, (tx) => guarded(tx)) : await withLocalChannelLock(inMemoryChannelTails, funding.channelId, () => guarded());
2417
+ const result = postgres ? await postgres.withChannelTransaction(funding.channelId, (tx) => guarded(tx), args.hostingFunding) : await withLocalChannelLock(inMemoryChannelTails, funding.channelId, () => guarded());
2319
2418
  if ("withdrawalPending" in result) {
2320
2419
  try {
2321
2420
  const claim = async () => {
@@ -2339,6 +2438,18 @@ async function createX402BatchSettlementServer(opts) {
2339
2438
  }
2340
2439
  return correctiveRefusal(resource, requirements, payload, "withdrawal_pending");
2341
2440
  }
2441
+ if (isRefund && opts.hostedObligations && result.ok) {
2442
+ const delta = await opts.hostedObligations.refundClaimDelta(
2443
+ result.value.settlement.transaction,
2444
+ funding.channelId
2445
+ );
2446
+ if (delta > 0n) {
2447
+ if (!opts.payout?.recordRefundClaim) {
2448
+ throw new Error("Hosted x402 refund claim has no durable payout tracker");
2449
+ }
2450
+ await opts.payout.recordRefundClaim(result.value.settlement.transaction, delta);
2451
+ }
2452
+ }
2342
2453
  return result;
2343
2454
  },
2344
2455
  getChannel: (channelId) => storage.get(channelId),
@@ -2514,11 +2625,11 @@ function createSettlementRepair(postgres, storage, facilitator, evidenceReader)
2514
2625
  if (args.evidence === false) {
2515
2626
  return { settlements: page.settlements.map((intent) => ({ intent })), ...nextAfter };
2516
2627
  }
2517
- const rows = [];
2628
+ const rows2 = [];
2518
2629
  for (const intent of page.settlements) {
2519
- rows.push({ intent, evidence: await classifySettlementEvidence(evidenceReader, intent) });
2630
+ rows2.push({ intent, evidence: await classifySettlementEvidence(evidenceReader, intent) });
2520
2631
  }
2521
- return { settlements: rows, ...nextAfter };
2632
+ return { settlements: rows2, ...nextAfter };
2522
2633
  },
2523
2634
  async reconcile(args) {
2524
2635
  const nowMs = args.nowMs ?? Date.now();
@@ -3084,13 +3195,13 @@ function methodNativeAmountFromDecimal(amountStr, methodName) {
3084
3195
  return Math.round(parseFloat(amountStr) * 10 ** decimalsFor(methodName));
3085
3196
  }
3086
3197
  function methodNativeAmountToDecimal(amount, methodName) {
3087
- const nativeAmount = BigInt(amount);
3088
- if (nativeAmount < 0n) {
3198
+ const nativeAmount2 = BigInt(amount);
3199
+ if (nativeAmount2 < 0n) {
3089
3200
  throw new Error("mpp-units: amount must be non-negative");
3090
3201
  }
3091
3202
  const decimals = decimalsFor(methodName);
3092
3203
  const scale = 10n ** BigInt(decimals);
3093
- return `${nativeAmount / scale}.${(nativeAmount % scale).toString().padStart(decimals, "0")}`;
3204
+ return `${nativeAmount2 / scale}.${(nativeAmount2 % scale).toString().padStart(decimals, "0")}`;
3094
3205
  }
3095
3206
 
3096
3207
  // src/sdk/server/wallet-accumulator-receive.ts
@@ -3535,6 +3646,9 @@ async function verifyUpfrontPayment(opts) {
3535
3646
  if (!ledgerCtx?.fundOnly?.creditId) {
3536
3647
  throw new Error("x402 batch settlement requires a named credit funding effect");
3537
3648
  }
3649
+ if (ledgerCtx.hostingAdmissionId && !ledgerCtx.hostingPaymentArtifactHash) {
3650
+ throw new Error("Hosted x402 funding lacks its original payment artifact");
3651
+ }
3538
3652
  const requiredUsdcMicro = x402RequiredUsdcMicro(x402PriceFiat);
3539
3653
  try {
3540
3654
  const accepted = await opts.x402BatchSettlement.accept({
@@ -3542,6 +3656,16 @@ async function verifyUpfrontPayment(opts) {
3542
3656
  amount: String(requiredUsdcMicro),
3543
3657
  operation: "fund",
3544
3658
  effectId: `${ledgerCtx.fundOnly.creditId}:${ledgerCtx.fundOnly.fundId}`,
3659
+ ...ledgerCtx.hostingAdmissionId && {
3660
+ hostingFunding: {
3661
+ admissionId: ledgerCtx.hostingAdmissionId,
3662
+ creditId: ledgerCtx.fundOnly.creditId,
3663
+ fundId: ledgerCtx.fundOnly.fundId,
3664
+ callerPubkey: ledgerCtx.callerId,
3665
+ commitment: ledgerCtx.fundOnly.commitment,
3666
+ paymentArtifactHash: ledgerCtx.hostingPaymentArtifactHash ?? ""
3667
+ }
3668
+ },
3545
3669
  commit: async (funding, tx) => {
3546
3670
  return commitMarkedFunding(
3547
3671
  ledgerCtx,
@@ -3655,7 +3779,7 @@ async function verifyUpfrontPayment(opts) {
3655
3779
  x402Requirements
3656
3780
  });
3657
3781
  }
3658
- const { verifyX402Payment } = await import("./x402-S47Q77UE.js");
3782
+ const { verifyX402Payment } = await import("./x402-R6HPBY5D.js");
3659
3783
  const receipt = await verifyX402Payment(
3660
3784
  x402Payment,
3661
3785
  x402Config,
@@ -3824,8 +3948,21 @@ async function verifyUpfrontPayment(opts) {
3824
3948
  challenge_id: cred.challenge.id,
3825
3949
  amount: expectedAmount
3826
3950
  },
3827
- () => mpp.broadcastCredential(cred, {
3951
+ () => mpp.broadcastCredential(mppCredential, {
3828
3952
  request: { amount: expectedAmount },
3953
+ ...opts.hostingAdmissionId && {
3954
+ hostedPaymentIntent: {
3955
+ admissionId: opts.hostingAdmissionId,
3956
+ jobId: opts.jobId,
3957
+ paymentArtifactHash: opts.hostingPaymentArtifactHash,
3958
+ ...ledgerCtx?.fundOnly && {
3959
+ creditId: ledgerCtx.fundOnly.creditId,
3960
+ fundId: ledgerCtx.fundOnly.fundId,
3961
+ callerPubkey: ledgerCtx.callerId,
3962
+ fundingCommitment: ledgerCtx.fundOnly.commitment
3963
+ }
3964
+ }
3965
+ },
3829
3966
  meta: {
3830
3967
  path: resourcePath ?? "/v1/job",
3831
3968
  nonce: challengeMeta(cred.challenge)?.nonce ?? ""
@@ -3846,11 +3983,25 @@ async function verifyUpfrontPayment(opts) {
3846
3983
  nativeAsset: methodNativeAsset(cred.challenge.method)
3847
3984
  };
3848
3985
  try {
3986
+ if (ledgerCtx.hostingAdmissionId && ledgerCtx.fundOnly && !ledgerCtx.hostingPaymentArtifactHash) {
3987
+ throw new Error("Hosted Tempo funding lacks its original payment artifact");
3988
+ }
3989
+ const voucherAcceptance = {
3990
+ channelId: sessionBasis.channelId,
3991
+ cumulativeAmount: BigInt(sessionBasis.cumulativeAmount),
3992
+ ...ledgerCtx.hostingAdmissionId && ledgerCtx.fundOnly && {
3993
+ hostingFunding: {
3994
+ admissionId: ledgerCtx.hostingAdmissionId,
3995
+ creditId: ledgerCtx.fundOnly.creditId,
3996
+ fundId: ledgerCtx.fundOnly.fundId,
3997
+ callerPubkey: ledgerCtx.callerId,
3998
+ commitment: ledgerCtx.fundOnly.commitment,
3999
+ paymentArtifactHash: ledgerCtx.hostingPaymentArtifactHash ?? ""
4000
+ }
4001
+ }
4002
+ };
3849
4003
  const accepted = await mpp.withTempoSessionAcceptance(
3850
- {
3851
- channelId: sessionBasis.channelId,
3852
- cumulativeAmount: BigInt(sessionBasis.cumulativeAmount)
3853
- },
4004
+ voucherAcceptance,
3854
4005
  async (tx, voucher) => {
3855
4006
  const binding = await mpp.getTempoSessionBinding?.(sessionBasis.channelId);
3856
4007
  if (binding) {
@@ -3954,6 +4105,11 @@ async function verifyUpfrontPayment(opts) {
3954
4105
  CONSUMED_CREDENTIAL_TTL_MS / 1e3
3955
4106
  );
3956
4107
  }
4108
+ if (credited) {
4109
+ await mpp.completeHostedTempoCharge?.(cred.challenge.id).catch((error) => {
4110
+ console.warn("Hosted Tempo charge completion remains unresolved", error);
4111
+ });
4112
+ }
3957
4113
  if (credited?.drawRefusal) {
3958
4114
  return creditLedgerErrorResult(credited.drawRefusal, x402Requirements, credited.deposit);
3959
4115
  }
@@ -4001,7 +4157,9 @@ async function verifyUpfrontPayment(opts) {
4001
4157
  creditId: creditEnv.creditId,
4002
4158
  drawId: creditEnv.drawId,
4003
4159
  amountMicro: ledgerCtx.priceFiat.amountMicro,
4004
- jobId: ledgerCtx.jobId
4160
+ jobId: ledgerCtx.jobId,
4161
+ hostingAdmissionId: ledgerCtx.hostingAdmissionId,
4162
+ hostingPaymentSeq: ledgerCtx.hostingPaymentSeq
4005
4163
  });
4006
4164
  if (draw.replayed) {
4007
4165
  const rec = await ledgerCtx.ledger.getDraw({
@@ -4140,8 +4298,7 @@ async function verifyTempoSessionManagementCredential(args) {
4140
4298
  if (typeof challengeAmount !== "string" && typeof challengeAmount !== "bigint") {
4141
4299
  throw new Error("Tempo session challenge has an invalid amount");
4142
4300
  }
4143
- await args.onBound?.();
4144
- const receipt = await args.mpp.broadcastCredential(credential, {
4301
+ const verifyOptions = {
4145
4302
  request: {
4146
4303
  amount: methodNativeAmountToDecimal(challengeAmount, credential.challenge.method)
4147
4304
  },
@@ -4149,7 +4306,15 @@ async function verifyTempoSessionManagementCredential(args) {
4149
4306
  path: args.resourcePath,
4150
4307
  nonce: challengeMeta(credential.challenge)?.nonce ?? ""
4151
4308
  }
4152
- });
4309
+ };
4310
+ if (args.preflightBeforeBound) {
4311
+ if (!args.mpp.validateCredential) {
4312
+ throw new Error("Hosted Tempo close requires non-mutating credential validation");
4313
+ }
4314
+ await args.mpp.validateCredential(credential, verifyOptions);
4315
+ }
4316
+ await args.onBound?.();
4317
+ const receipt = await args.mpp.broadcastCredential(credential, verifyOptions);
4153
4318
  const sessionReceipt = receipt;
4154
4319
  return {
4155
4320
  reference: typeof sessionReceipt.txHash === "string" ? sessionReceipt.txHash : receipt.reference,
@@ -4589,7 +4754,7 @@ async function verifyIncomingPayment(body, opts, snapshot) {
4589
4754
  snapshot
4590
4755
  });
4591
4756
  }
4592
- const { verifyX402Payment } = await import("./x402-S47Q77UE.js");
4757
+ const { verifyX402Payment } = await import("./x402-R6HPBY5D.js");
4593
4758
  const receipt = await verifyX402Payment(
4594
4759
  body.content.x402_payment,
4595
4760
  opts.x402Config,
@@ -4755,6 +4920,9 @@ async function verifyIncomingPayment(body, opts, snapshot) {
4755
4920
  },
4756
4921
  () => mppServer.broadcastCredential(credential, {
4757
4922
  request: { amount: expectedAmount },
4923
+ ...opts.hostingPaymentSeq !== void 0 && {
4924
+ hostedPaymentIntent: { jobId: opts.jobId, paymentSeq: opts.hostingPaymentSeq }
4925
+ },
4758
4926
  ...expectedMeta && { meta: expectedMeta }
4759
4927
  })
4760
4928
  );
@@ -4770,6 +4938,11 @@ async function verifyIncomingPayment(body, opts, snapshot) {
4770
4938
  throw err;
4771
4939
  }
4772
4940
  }
4941
+ if (credited) {
4942
+ await mppServer.completeHostedTempoCharge?.(credential.challenge.id).catch((error) => {
4943
+ console.warn("Hosted Tempo charge completion remains unresolved", error);
4944
+ });
4945
+ }
4773
4946
  const info = withDrawBasis(
4774
4947
  {
4775
4948
  paidMsats: expectedMsats,
@@ -5006,7 +5179,7 @@ async function processIncomingPayment(job, body, opts) {
5006
5179
  try {
5007
5180
  const requiredMsats = job.pendingPaymentMsats ?? 0;
5008
5181
  const requiredUsdcMicro = job.pendingX402AmountUsdcMicro !== void 0 ? BigInt(job.pendingX402AmountUsdcMicro) : BigInt(msatsToUsdc(requiredMsats, rate));
5009
- const { verifyX402Payment } = await import("./x402-S47Q77UE.js");
5182
+ const { verifyX402Payment } = await import("./x402-R6HPBY5D.js");
5010
5183
  const receipt = await verifyX402Payment(
5011
5184
  body.content.x402_payment,
5012
5185
  opts.x402Config,
@@ -5083,6 +5256,9 @@ async function processIncomingPayment(job, body, opts) {
5083
5256
  },
5084
5257
  () => mppHandle.broadcastCredential(credential, {
5085
5258
  request: { amount: expectedAmount },
5259
+ ...opts.hostingPaymentSeq !== void 0 && {
5260
+ hostedPaymentIntent: { jobId: opts.jobId, paymentSeq: opts.hostingPaymentSeq }
5261
+ },
5086
5262
  ...expectedMeta && { meta: expectedMeta }
5087
5263
  })
5088
5264
  );
@@ -5106,6 +5282,9 @@ async function processIncomingPayment(job, body, opts) {
5106
5282
  job.paymentRail = "tempo";
5107
5283
  job.pendingMppChallengeIds = void 0;
5108
5284
  creditPendingPayment(job);
5285
+ await mppHandle.completeHostedTempoCharge?.(credential.challenge.id).catch((error) => {
5286
+ console.warn("Hosted Tempo charge completion remains unresolved", error);
5287
+ });
5109
5288
  } catch (err) {
5110
5289
  logCredentialRejected("tempo", "mid-job", err);
5111
5290
  return {
@@ -5357,6 +5536,8 @@ function resolveLedgerContext(opts) {
5357
5536
  requiredMsats: opts.requiredMsats,
5358
5537
  callerId: opts.callerPubkey ?? opts.requesterId ?? "anonymous",
5359
5538
  jobId: opts.jobId,
5539
+ hostingAdmissionId: opts.hostingAdmissionId,
5540
+ hostingPaymentArtifactHash: opts.hostingPaymentArtifactHash,
5360
5541
  jobAdmission: opts.jobId && opts.requestFingerprint ? {
5361
5542
  requestFingerprint: opts.requestFingerprint,
5362
5543
  authFingerprint: opts.requestAuthFingerprint
@@ -5477,6 +5658,7 @@ async function resolveTopUpLedgerContext(opts, snapshot, binding) {
5477
5658
  requiredMsats: priceBasisMsats,
5478
5659
  callerId,
5479
5660
  jobId: opts.jobId,
5661
+ hostingPaymentSeq: opts.hostingPaymentSeq,
5480
5662
  dvmId,
5481
5663
  topUp,
5482
5664
  topUpCapMicro,
@@ -5650,11 +5832,15 @@ async function fundAndDraw(ctx, funding, tx) {
5650
5832
  fundId: ctx.fundOnly.fundId,
5651
5833
  amountMicro: fundedMicro,
5652
5834
  rail: funding.rail,
5835
+ hostingAdmissionId: ctx.hostingAdmissionId,
5836
+ hostingCallerPubkey: ctx.callerId,
5653
5837
  tx
5654
5838
  });
5655
5839
  const credit = await ctx.ledger.fund({
5656
5840
  creditId: creditId2,
5657
5841
  callerPubkey: ctx.callerId,
5842
+ hostingAdmissionId: ctx.hostingAdmissionId,
5843
+ hostingFundId: ctx.fundOnly.fundId,
5658
5844
  currency: ctx.priceFiat.currency,
5659
5845
  amountMicro: fundedMicro,
5660
5846
  expiryMs,
@@ -5676,6 +5862,9 @@ async function fundAndDraw(ctx, funding, tx) {
5676
5862
  const credit = await ctx.ledger.fund({
5677
5863
  creditId: creditId2,
5678
5864
  callerPubkey: ctx.callerId,
5865
+ hostingJobId: ctx.jobId,
5866
+ hostingAdmissionId: ctx.hostingAdmissionId,
5867
+ hostingPaymentSeq: ctx.hostingPaymentSeq,
5679
5868
  currency: ctx.priceFiat.currency,
5680
5869
  amountMicro: fundedMicro,
5681
5870
  expiryMs,
@@ -5727,12 +5916,18 @@ async function fundAndDraw(ctx, funding, tx) {
5727
5916
  fundId: funding.paymentId,
5728
5917
  amountMicro: fundedMicro,
5729
5918
  rail: funding.rail,
5919
+ hostingAdmissionId: ctx.hostingAdmissionId,
5920
+ hostingJobId: ctx.jobId,
5921
+ hostingPaymentSeq: ctx.hostingPaymentSeq,
5730
5922
  tx
5731
5923
  });
5732
5924
  }
5733
5925
  const fundedCredit = await ctx.ledger.fund({
5734
5926
  creditId,
5735
5927
  callerPubkey: ctx.callerId,
5928
+ hostingJobId: ctx.jobId,
5929
+ hostingAdmissionId: ctx.hostingAdmissionId,
5930
+ hostingPaymentSeq: ctx.hostingPaymentSeq,
5736
5931
  currency: ctx.priceFiat.currency,
5737
5932
  amountMicro: fundedMicro,
5738
5933
  expiryMs,
@@ -5747,6 +5942,8 @@ async function fundAndDraw(ctx, funding, tx) {
5747
5942
  drawId,
5748
5943
  amountMicro: drawMicro,
5749
5944
  jobId: ctx.jobId,
5945
+ hostingAdmissionId: ctx.hostingAdmissionId,
5946
+ hostingPaymentSeq: ctx.hostingPaymentSeq,
5750
5947
  tx
5751
5948
  });
5752
5949
  } catch (err) {
@@ -5837,6 +6034,8 @@ async function repairX402ExactSettlementEffect(args) {
5837
6034
  callerId: effect.callerId,
5838
6035
  dvmId: effect.dvmId,
5839
6036
  jobId: effect.jobId,
6037
+ hostingAdmissionId: effect.hostingAdmissionId,
6038
+ hostingPaymentSeq: effect.hostingPaymentSeq,
5840
6039
  jobAdmission: effect.jobAdmission,
5841
6040
  creditEnvelope: effect.creditEnvelope,
5842
6041
  fundOnly: effect.fundOnly,
@@ -5984,6 +6183,8 @@ function exactSettlementEffect(ctx, funding) {
5984
6183
  callerId: ctx.callerId,
5985
6184
  dvmId: ctx.dvmId,
5986
6185
  jobId: ctx.jobId,
6186
+ hostingAdmissionId: ctx.hostingAdmissionId,
6187
+ hostingPaymentSeq: ctx.hostingPaymentSeq,
5987
6188
  jobAdmission: ctx.jobAdmission,
5988
6189
  creditEnvelope: ctx.creditEnvelope,
5989
6190
  fundOnly: ctx.fundOnly,
@@ -7111,6 +7312,7 @@ async function completeTempoDrain(c, deps, record, expectedChannelId, credit) {
7111
7312
  }
7112
7313
  const beginClose = rail.mpp.beginTempoSessionClose.bind(rail.mpp);
7113
7314
  let recoveryVersion;
7315
+ let hostedObligationId;
7114
7316
  try {
7115
7317
  const closed = await verifyTempoSessionManagementCredential({
7116
7318
  credential: rail.mppCredential,
@@ -7118,13 +7320,30 @@ async function completeTempoDrain(c, deps, record, expectedChannelId, credit) {
7118
7320
  resourcePath: c.req.path,
7119
7321
  action: "close",
7120
7322
  expectedChannelId,
7323
+ preflightBeforeBound: !!deps.hostedSettlementReports,
7121
7324
  // Reassert the intent only after the verifier binds the credential to this channel, and
7122
7325
  // before broadcast. A wrong-channel signature cannot create or cancel its close intent.
7123
7326
  onBound: async () => {
7124
7327
  await beginClose(expectedChannelId, record.drainId);
7125
7328
  recoveryVersion = await deps.tempoSettlementReadiness?.captureRecoveryVersion();
7329
+ if (deps.hostedSettlementReports) {
7330
+ const reserved = await deps.hostedSettlementReports.reserveTempo(
7331
+ expectedChannelId,
7332
+ "close",
7333
+ record.drainId
7334
+ );
7335
+ if (reserved.replayed) throw new Error("Hosted Tempo close already has an original effect attempt");
7336
+ hostedObligationId = reserved.id;
7337
+ if (!rail.mpp?.permitHostedTempoClose) {
7338
+ throw new Error("Hosted Tempo close credential guard is unavailable");
7339
+ }
7340
+ rail.mpp.permitHostedTempoClose(rail.mppCredential, reserved.id);
7341
+ }
7126
7342
  }
7127
7343
  });
7344
+ if (hostedObligationId) {
7345
+ await deps.hostedSettlementReports?.recordSubmitted(hostedObligationId, closed.reference);
7346
+ }
7128
7347
  const reference = await rail.mpp.confirmTempoSessionClose(
7129
7348
  expectedChannelId,
7130
7349
  record.drainId,
@@ -8572,9 +8791,9 @@ async function handleMarkMelted(c, opts) {
8572
8791
  400
8573
8792
  );
8574
8793
  }
8575
- const rows = await fetchRowsForMelt(opts.db, opts.dvmId, rowIds);
8576
- if (rows.length !== rowIds.length) {
8577
- const found = new Set(rows.map((r) => r.id));
8794
+ const rows2 = await fetchRowsForMelt(opts.db, opts.dvmId, rowIds);
8795
+ if (rows2.length !== rowIds.length) {
8796
+ const found = new Set(rows2.map((r) => r.id));
8578
8797
  return c.json(
8579
8798
  {
8580
8799
  error: "rows_not_found",
@@ -8584,7 +8803,7 @@ async function handleMarkMelted(c, opts) {
8584
8803
  404
8585
8804
  );
8586
8805
  }
8587
- const alreadyMelted = rows.filter((r) => r.meltStatus === "melted").map((r) => r.id);
8806
+ const alreadyMelted = rows2.filter((r) => r.meltStatus === "melted").map((r) => r.id);
8588
8807
  if (alreadyMelted.length > 0) {
8589
8808
  return c.json(
8590
8809
  {
@@ -8596,7 +8815,7 @@ async function handleMarkMelted(c, opts) {
8596
8815
  );
8597
8816
  }
8598
8817
  const byMint = /* @__PURE__ */ new Map();
8599
- for (const row of rows) {
8818
+ for (const row of rows2) {
8600
8819
  const list = byMint.get(row.mintUrl);
8601
8820
  if (list) list.push(row);
8602
8821
  else byMint.set(row.mintUrl, [row]);
@@ -8660,7 +8879,7 @@ async function handleMarkMelted(c, opts) {
8660
8879
  if (opts.onMelted) {
8661
8880
  await opts.onMelted.enqueue(
8662
8881
  {
8663
- rows: rows.map((r) => ({ id: r.id, mintUrl: r.mintUrl, proofAmount: r.proofAmount })),
8882
+ rows: rows2.map((r) => ({ id: r.id, mintUrl: r.mintUrl, proofAmount: r.proofAmount })),
8664
8883
  meltQuoteId,
8665
8884
  paymentPreimage
8666
8885
  },
@@ -9064,34 +9283,1552 @@ function decodeAndDescribe(args) {
9064
9283
  intentId: hash([facts.network, facts.asset, facts.from, facts.nonce])
9065
9284
  };
9066
9285
  }
9067
- function receiptFromIntent(intent, btcUsdRate) {
9068
- const amountUsdcMicro = Number.parseInt(intent.value, 10);
9069
- const response = intent.settleResponse ?? {
9070
- success: true,
9071
- transaction: intent.transaction
9286
+ function receiptFromIntent(intent, btcUsdRate) {
9287
+ const amountUsdcMicro = Number.parseInt(intent.value, 10);
9288
+ const response = intent.settleResponse ?? {
9289
+ success: true,
9290
+ transaction: intent.transaction
9291
+ };
9292
+ return {
9293
+ verified: true,
9294
+ amountMsats: btcUsdRate > 0 ? usdcToMsats(amountUsdcMicro / 1e6, btcUsdRate) : 0,
9295
+ amountUsdcMicro,
9296
+ txHash: intent.transaction,
9297
+ settleResponseHeader: encodeSettleResponseHeader(response)
9298
+ };
9299
+ }
9300
+ function settlementMayHaveMoved(response) {
9301
+ const reason = response.errorReason?.toLowerCase() ?? "";
9302
+ return reason.includes("authorization_already_used") || reason.includes("authorization already used") || /facilitator_status_5\d\d/.test(reason);
9303
+ }
9304
+ function hash(parts) {
9305
+ return createHash4("sha256").update(JSON.stringify(parts)).digest("hex");
9306
+ }
9307
+ function safeError(err) {
9308
+ if (err instanceof Error) return err.name;
9309
+ return "settlement_transport_error";
9310
+ }
9311
+
9312
+ // src/sdk/server/hosting-control.ts
9313
+ import { createHmac, randomUUID as randomUUID4, timingSafeEqual } from "crypto";
9314
+
9315
+ // src/sdk/server/hosting-cleanup-fence.ts
9316
+ var HostingCleanupFence = class {
9317
+ constructor(pool, dvmId, scope) {
9318
+ this.pool = pool;
9319
+ this.dvmId = dvmId;
9320
+ this.scope = scope;
9321
+ }
9322
+ pool;
9323
+ dvmId;
9324
+ scope;
9325
+ async assertNewIntent(tx, intent) {
9326
+ const continuation = intent?.admissionId || intent?.paymentSeq !== void 0 || intent?.invoicePaymentHash || intent?.invoiceRepairPaymentHash || intent?.creditId && intent.fundId && intent.callerPubkey;
9327
+ const { rows: rows2 } = await tx.query(
9328
+ `SELECT state, closed_at_ms, closed_at_us FROM sdk_hosting_control
9329
+ WHERE dvm_id=$1 AND scope=$2 ${continuation ? "" : "FOR UPDATE"}`,
9330
+ [this.dvmId, this.scope]
9331
+ );
9332
+ const control = rows2[0];
9333
+ if (!control) throw new Error("Hosted obligation fence is unavailable");
9334
+ if (control.state === "closed" && (control.closed_at_ms === null || control.closed_at_us === null)) {
9335
+ throw new Error("Hosted closure cutoff is unavailable");
9336
+ }
9337
+ if (control.state !== "closed" && !continuation) return;
9338
+ if (intent?.invoicePaymentHash || intent?.invoiceRepairPaymentHash) {
9339
+ const repair = !!intent.invoiceRepairPaymentHash;
9340
+ const invoice = await tx.query(
9341
+ `SELECT i.payment_hash FROM credit_invoices i
9342
+ LEFT JOIN sdk_hosting_admissions a ON a.id=i.hosting_admission_id
9343
+ WHERE i.payment_hash=$1 AND i.caller_pubkey=$4 AND i.amount_micro=$5
9344
+ AND (($10::boolean AND $3=i.payment_hash AND i.status IN ('blocked','written_off'))
9345
+ OR (NOT $10::boolean AND i.credit_id=$2 AND i.fund_id=$3 AND i.status='pending'))
9346
+ AND ((i.hosting_admission_id IS NULL AND
9347
+ ($9::bigint IS NULL OR i.created_at_db_us <= $9)) OR
9348
+ (a.dvm_id=$6 AND a.scope=$7 AND a.credit_id=i.credit_id
9349
+ AND a.fund_id=i.fund_id AND a.caller_pubkey=i.caller_pubkey
9350
+ AND a.outcome IN ('pending','accepted','unresolved')
9351
+ AND ($8::bigint IS NULL OR a.admitted_at_ms <= $8)))`,
9352
+ [
9353
+ intent.invoicePaymentHash ?? intent.invoiceRepairPaymentHash,
9354
+ intent.creditId,
9355
+ intent.fundId,
9356
+ intent.callerPubkey,
9357
+ intent.amountMicro,
9358
+ this.dvmId,
9359
+ this.scope,
9360
+ control.closed_at_ms,
9361
+ control.closed_at_us,
9362
+ repair
9363
+ ]
9364
+ );
9365
+ if (invoice.rows[0]) return;
9366
+ throw new Error("Hosted invoice has no original payable admission");
9367
+ }
9368
+ if (intent?.paymentSeq !== void 0 && intent.jobId) {
9369
+ const payment = await tx.query(
9370
+ `SELECT m.job_id FROM job_messages m JOIN jobs j ON j.id=m.job_id
9371
+ WHERE m.job_id=$1 AND m.seq=$2 AND m.status='pending-verification'
9372
+ AND m.created_at_ms IS NOT NULL
9373
+ AND ($3::bigint IS NULL OR m.created_at_ms <= $3) AND j.dvm_id=$4
9374
+ AND ($6::text IS NULL OR m.data->'content'->'tempo_credential'->'challenge'->>'id'=$6)
9375
+ AND EXISTS (SELECT 1 FROM sdk_hosting_admissions a
9376
+ WHERE a.dvm_id=$4 AND a.scope=$5 AND a.job_id=m.job_id AND a.outcome <> 'failed')`,
9377
+ [
9378
+ intent.jobId,
9379
+ intent.paymentSeq,
9380
+ control.closed_at_ms,
9381
+ this.dvmId,
9382
+ this.scope,
9383
+ intent.challengeId ?? null
9384
+ ]
9385
+ );
9386
+ if (payment.rows[0]) return;
9387
+ throw new Error("Hosted payment has no pre-closure inbound intent");
9388
+ }
9389
+ if (!intent?.admissionId && !intent?.jobId && !(intent?.creditId && intent.fundId && intent.callerPubkey) || !intent?.jobId && !(intent?.creditId && intent.fundId)) {
9390
+ throw new Error("Hosted obligations are closed to new intents");
9391
+ }
9392
+ const accepted = await tx.query(
9393
+ `SELECT id FROM sdk_hosting_admissions
9394
+ WHERE ($1::text IS NULL OR id=$1) AND dvm_id=$2 AND scope=$3
9395
+ AND ($4::bigint IS NULL OR admitted_at_ms <= $4)
9396
+ AND (outcome IN ('pending', 'unresolved') OR ($11::boolean AND outcome='accepted'))
9397
+ AND ($5::text IS NULL OR job_id=$5)
9398
+ AND ($6::text IS NULL OR credit_id=$6)
9399
+ AND ($7::text IS NULL OR fund_id=$7)
9400
+ AND ($8::text IS NULL OR caller_pubkey=$8)
9401
+ AND ($9::text IS NULL OR funding_commitment=$9)
9402
+ AND ($10::text IS NULL OR payment_artifact_hash=$10)`,
9403
+ [
9404
+ intent.admissionId ?? null,
9405
+ this.dvmId,
9406
+ this.scope,
9407
+ control.closed_at_ms,
9408
+ intent.jobId ?? null,
9409
+ intent.creditId ?? null,
9410
+ intent.fundId ?? null,
9411
+ intent.callerPubkey ?? null,
9412
+ intent.fundingCommitment ?? null,
9413
+ intent.paymentArtifactHash ?? null,
9414
+ control.state !== "closed"
9415
+ ]
9416
+ );
9417
+ if (!accepted.rows[0]) throw new Error("Hosted obligation has no pre-closure admission");
9418
+ }
9419
+ async withNewIntent(intent, operation) {
9420
+ return this.withNewIntentOn(this.pool, intent, operation);
9421
+ }
9422
+ async assertFundingReservation(tx, reservation) {
9423
+ await this.assertNewIntent(tx, {
9424
+ admissionId: reservation.admissionId,
9425
+ creditId: reservation.creditId,
9426
+ fundId: reservation.fundId,
9427
+ callerPubkey: reservation.callerPubkey,
9428
+ fundingCommitment: reservation.commitment,
9429
+ paymentArtifactHash: reservation.paymentArtifactHash
9430
+ });
9431
+ const verified = await tx.query(
9432
+ `SELECT id FROM sdk_hosting_admissions
9433
+ WHERE id=$1 AND dvm_id=$2 AND scope=$3 AND credit_id=$4 AND fund_id=$5
9434
+ AND caller_pubkey=$6 AND funding_commitment=$7 AND payment_artifact_hash=$8
9435
+ AND outcome IN ('pending','accepted','unresolved')`,
9436
+ [
9437
+ reservation.admissionId,
9438
+ this.dvmId,
9439
+ this.scope,
9440
+ reservation.creditId,
9441
+ reservation.fundId,
9442
+ reservation.callerPubkey,
9443
+ reservation.commitment,
9444
+ reservation.paymentArtifactHash
9445
+ ]
9446
+ );
9447
+ if (!verified.rows[0]) throw new Error("Hosted funding has no authentic admission");
9448
+ }
9449
+ async fundingAdmission(tx, creditId, fundId, callerPubkey) {
9450
+ await this.assertNewIntent(tx, { creditId, fundId, callerPubkey });
9451
+ const { rows: rows2 } = await tx.query(
9452
+ `SELECT id FROM sdk_hosting_admissions
9453
+ WHERE dvm_id=$1 AND scope=$2 AND credit_id=$3 AND fund_id=$4
9454
+ AND caller_pubkey=$5 AND outcome IN ('pending','unresolved')
9455
+ ORDER BY admitted_at_ms,id LIMIT 2`,
9456
+ [this.dvmId, this.scope, creditId, fundId, callerPubkey]
9457
+ );
9458
+ if (rows2.length !== 1) throw new Error("Hosted invoice funding admission is missing or ambiguous");
9459
+ return rows2[0].id;
9460
+ }
9461
+ async withNewIntentOn(db, intent, operation) {
9462
+ const tx = await db.connect();
9463
+ try {
9464
+ await tx.query("BEGIN");
9465
+ await this.assertNewIntent(tx, intent);
9466
+ const result = await operation(tx);
9467
+ await tx.query("COMMIT");
9468
+ return result;
9469
+ } catch (error) {
9470
+ await tx.query("ROLLBACK").catch(() => void 0);
9471
+ throw error;
9472
+ } finally {
9473
+ tx.release();
9474
+ }
9475
+ }
9476
+ };
9477
+
9478
+ // src/sdk/server/hosting-settlement-report-obligations.ts
9479
+ import { createHash as createHash5, randomUUID as randomUUID3 } from "crypto";
9480
+ import { isDeepStrictEqual as isDeepStrictEqual2 } from "util";
9481
+ import { BATCH_SETTLEMENT_ADDRESS as BATCH_SETTLEMENT_ADDRESS3 } from "@x402/evm";
9482
+ import { computeChannelId as computeChannelId2 } from "@x402/evm/batch-settlement/client";
9483
+ import { Session as TempoSession } from "mppx/tempo";
9484
+ import { createPublicClient as createPublicClient4, decodeEventLog, decodeFunctionData as decodeFunctionData2, encodeFunctionData, http as http4, parseAbiItem as parseAbiItem2, toFunctionSelector } from "viem";
9485
+ var HOSTED_SETTLEMENT_UNRESOLVED_SQL = `state NOT IN ('delivered','no_report')
9486
+ OR (rail='x402' AND operation IN ('claim','refund')
9487
+ AND COALESCE(original->>'authorizationCallData','') !~* '^0x([0-9a-f]{2})+$')`;
9488
+ var X402_SETTLED_EVENT = parseAbiItem2(
9489
+ "event Settled(address indexed receiver,address indexed token,address indexed sender,uint128 amount)"
9490
+ );
9491
+ var X402_REFUNDED_EVENT = parseAbiItem2(
9492
+ "event Refunded(bytes32 indexed channelId,address indexed sender,uint128 amount)"
9493
+ );
9494
+ var X402_CLAIM_ABI = [{
9495
+ type: "function",
9496
+ name: "claimWithSignature",
9497
+ stateMutability: "nonpayable",
9498
+ inputs: [{ name: "voucherClaims", type: "tuple[]", components: [
9499
+ { name: "voucher", type: "tuple", components: [
9500
+ { name: "channel", type: "tuple", components: [
9501
+ { name: "payer", type: "address" },
9502
+ { name: "payerAuthorizer", type: "address" },
9503
+ { name: "receiver", type: "address" },
9504
+ { name: "receiverAuthorizer", type: "address" },
9505
+ { name: "token", type: "address" },
9506
+ { name: "withdrawDelay", type: "uint40" },
9507
+ { name: "salt", type: "bytes32" }
9508
+ ] },
9509
+ { name: "maxClaimableAmount", type: "uint128" }
9510
+ ] },
9511
+ { name: "signature", type: "bytes" },
9512
+ { name: "totalClaimed", type: "uint128" }
9513
+ ] }, { name: "authorizerSignature", type: "bytes" }],
9514
+ outputs: []
9515
+ }];
9516
+ var X402_REFUND_ABI = [{
9517
+ type: "function",
9518
+ name: "refundWithSignature",
9519
+ stateMutability: "nonpayable",
9520
+ inputs: [
9521
+ { name: "config", type: "tuple", components: X402_CLAIM_ABI[0].inputs[0].components[0].components[0].components },
9522
+ { name: "amount", type: "uint128" },
9523
+ { name: "nonce", type: "uint256" },
9524
+ { name: "receiverAuthorizerSignature", type: "bytes" }
9525
+ ],
9526
+ outputs: []
9527
+ }];
9528
+ var X402_MULTICALL_ABI = [{
9529
+ type: "function",
9530
+ name: "multicall",
9531
+ stateMutability: "nonpayable",
9532
+ inputs: [{ name: "data", type: "bytes[]" }],
9533
+ outputs: [{ name: "results", type: "bytes[]" }]
9534
+ }];
9535
+ var TEMPO_SETTLED_EVENT = TempoSession.Precompile.escrowAbi.find(
9536
+ (item) => item.type === "event" && item.name === "Settled"
9537
+ );
9538
+ var TEMPO_CLOSED_EVENT = TempoSession.Precompile.escrowAbi.find(
9539
+ (item) => item.type === "event" && item.name === "ChannelClosed"
9540
+ );
9541
+ function hash2(value) {
9542
+ return createHash5("sha256").update(JSON.stringify(value)).digest("hex");
9543
+ }
9544
+ function hex(value, bytes) {
9545
+ return typeof value === "string" && new RegExp(`^0x[0-9a-fA-F]{${bytes * 2}}$`).test(value);
9546
+ }
9547
+ function signedBytes(value) {
9548
+ if (typeof value !== "string" || !/^0x(?:[0-9a-fA-F]{2})+$/.test(value)) {
9549
+ throw new Error("Hosted x402 authorization signature is unavailable before settlement");
9550
+ }
9551
+ return value;
9552
+ }
9553
+ function uint(value) {
9554
+ if ((typeof value !== "string" || !/^\d+$/.test(value)) && (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) && (typeof value !== "bigint" || value < 0n)) {
9555
+ throw new Error("Hosted x402 authorization has an invalid unsigned amount");
9556
+ }
9557
+ return BigInt(value);
9558
+ }
9559
+ function contractChannel(value) {
9560
+ const channel = value;
9561
+ if (!channel || !hex(channel.payer, 20) || !hex(channel.payerAuthorizer, 20) || !hex(channel.receiver, 20) || !hex(channel.receiverAuthorizer, 20) || !hex(channel.token, 20) || !hex(channel.salt, 32)) {
9562
+ throw new Error("Hosted x402 authorization has no original channel config");
9563
+ }
9564
+ const withdrawDelay = uint(channel.withdrawDelay);
9565
+ if (withdrawDelay > (1n << 40n) - 1n) {
9566
+ throw new Error("Hosted x402 authorization has an invalid withdrawal delay");
9567
+ }
9568
+ return {
9569
+ payer: channel.payer,
9570
+ payerAuthorizer: channel.payerAuthorizer,
9571
+ receiver: channel.receiver,
9572
+ receiverAuthorizer: channel.receiverAuthorizer,
9573
+ token: channel.token,
9574
+ withdrawDelay: Number(withdrawDelay),
9575
+ salt: channel.salt
9576
+ };
9577
+ }
9578
+ function authorizationCallData(operation, payload) {
9579
+ if (payload.type !== operation) throw new Error("Hosted x402 authorization operation changed");
9580
+ const rawClaims = payload.claims;
9581
+ if (!Array.isArray(rawClaims) || operation === "claim" && !rawClaims.length || operation === "refund" && rawClaims.length > 1) {
9582
+ throw new Error("Hosted x402 authorization has no exact original claim set");
9583
+ }
9584
+ const claims = rawClaims.map((raw) => {
9585
+ const claim = raw;
9586
+ const voucher = claim?.voucher;
9587
+ if (!voucher) throw new Error("Hosted x402 authorization has no original voucher");
9588
+ return {
9589
+ voucher: {
9590
+ channel: contractChannel(voucher.channel),
9591
+ maxClaimableAmount: uint(voucher.maxClaimableAmount)
9592
+ },
9593
+ signature: signedBytes(claim.signature),
9594
+ totalClaimed: uint(claim.totalClaimed)
9595
+ };
9596
+ });
9597
+ const claimData = claims.length ? encodeFunctionData({
9598
+ abi: X402_CLAIM_ABI,
9599
+ functionName: "claimWithSignature",
9600
+ args: [
9601
+ claims,
9602
+ signedBytes(payload.claimAuthorizerSignature)
9603
+ ]
9604
+ }) : void 0;
9605
+ if (operation === "claim") return claimData;
9606
+ const refundData = encodeFunctionData({
9607
+ abi: X402_REFUND_ABI,
9608
+ functionName: "refundWithSignature",
9609
+ args: [
9610
+ contractChannel(payload.channelConfig),
9611
+ uint(payload.amount),
9612
+ uint(payload.refundNonce),
9613
+ signedBytes(payload.refundAuthorizerSignature)
9614
+ ]
9615
+ });
9616
+ return claimData ? encodeFunctionData({
9617
+ abi: X402_MULTICALL_ABI,
9618
+ functionName: "multicall",
9619
+ args: [[claimData, refundData]]
9620
+ }) : refundData;
9621
+ }
9622
+ function nativeAmount(value) {
9623
+ if (typeof value !== "bigint" || value < 0n) {
9624
+ throw new Error("Hosted settlement source has no exact nonnegative amount");
9625
+ }
9626
+ return value.toString();
9627
+ }
9628
+ async function transaction(pool, operation) {
9629
+ const tx = await pool.connect();
9630
+ try {
9631
+ await tx.query("BEGIN");
9632
+ const result = await operation(tx);
9633
+ await tx.query("COMMIT");
9634
+ return result;
9635
+ } catch (error) {
9636
+ await tx.query("ROLLBACK").catch(() => void 0);
9637
+ throw error;
9638
+ } finally {
9639
+ tx.release();
9640
+ }
9641
+ }
9642
+ var HostedSettlementReportObligations = class {
9643
+ constructor(pool, dvmId, scope) {
9644
+ this.pool = pool;
9645
+ this.dvmId = dvmId;
9646
+ this.scope = scope;
9647
+ }
9648
+ pool;
9649
+ dvmId;
9650
+ scope;
9651
+ x402;
9652
+ tempo;
9653
+ x402SettlementStore;
9654
+ configureX402(config) {
9655
+ if (!x402NetworkByCaip2(config.network) || !hex(config.receiver, 20) || !hex(config.token, 20)) {
9656
+ throw new Error("Hosted x402 settlement identity is unsupported");
9657
+ }
9658
+ if (this.x402 && hash2(this.x402) !== hash2(config)) throw new Error("Hosted x402 settlement identity changed");
9659
+ this.x402 = config;
9660
+ }
9661
+ attachX402SettlementStore(store) {
9662
+ if (this.x402SettlementStore && this.x402SettlementStore !== store) {
9663
+ throw new Error("Hosted x402 settlement writer changed");
9664
+ }
9665
+ this.x402SettlementStore = store;
9666
+ }
9667
+ x402Transaction(operation) {
9668
+ const store = this.x402SettlementStore;
9669
+ if (!store) throw new Error("Hosted x402 durable settlement writer is unavailable");
9670
+ return store.withSettlementTransaction(operation);
9671
+ }
9672
+ configureTempo(config) {
9673
+ if (!/^eip155:[0-9]+$/.test(config.network) || !hex(config.escrow, 20) || !hex(config.payee, 20) || !hex(config.token, 20)) {
9674
+ throw new Error("Hosted Tempo settlement identity is unsupported");
9675
+ }
9676
+ if (this.tempo && hash2(this.tempo) !== hash2(config)) throw new Error("Hosted Tempo settlement identity changed");
9677
+ this.tempo = config;
9678
+ }
9679
+ async init() {
9680
+ await withSdkInitLock(this.pool, async () => {
9681
+ const earlier = await this.pool.query(
9682
+ `SELECT to_regclass('x402_batch_channels') IS NOT NULL AS x402,
9683
+ to_regclass('x402_batch_settlements') IS NOT NULL AS "x402Settlement",
9684
+ to_regclass('payout_x402_batches') IS NOT NULL AS "x402Batch",
9685
+ to_regclass('mpp_tempo_channels') IS NOT NULL AS tempo,
9686
+ to_regclass('mpp_tempo_close_intents') IS NOT NULL AS "tempoClose",
9687
+ to_regclass('pending_dvm_reports') IS NOT NULL AS reports,
9688
+ to_regclass('sdk_hosted_settlement_report_obligations') IS NOT NULL AS obligations`
9689
+ );
9690
+ await this.pool.query(`
9691
+ CREATE TABLE IF NOT EXISTS sdk_hosted_settlement_report_history (
9692
+ singleton BOOLEAN PRIMARY KEY CHECK (singleton), complete BOOLEAN NOT NULL
9693
+ );
9694
+ CREATE TABLE IF NOT EXISTS sdk_hosted_settlement_report_obligations (
9695
+ id TEXT PRIMARY KEY, source_key TEXT NOT NULL UNIQUE,
9696
+ dvm_id TEXT NOT NULL, scope TEXT NOT NULL, instance_id TEXT NOT NULL,
9697
+ boot_epoch BIGINT NOT NULL, rail TEXT NOT NULL CHECK (rail IN ('x402','tempo')),
9698
+ operation TEXT NOT NULL CHECK (operation IN ('claim','refund','settle','close')),
9699
+ network TEXT NOT NULL, contract TEXT NOT NULL, channel_id TEXT NOT NULL,
9700
+ original JSONB NOT NULL,
9701
+ state TEXT NOT NULL CHECK (state IN ('reserved','observed','report_queued','delivered','no_report')),
9702
+ tx_hash TEXT, block_number BIGINT, block_hash TEXT, delta_native NUMERIC(78,0),
9703
+ payout_id TEXT, report_id TEXT, report_payload JSONB, delivered_at_ms BIGINT,
9704
+ created_at_ms BIGINT NOT NULL DEFAULT FLOOR(EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint,
9705
+ updated_at_ms BIGINT NOT NULL DEFAULT FLOOR(EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint
9706
+ );
9707
+ CREATE INDEX IF NOT EXISTS sdk_hosted_settlement_report_pending
9708
+ ON sdk_hosted_settlement_report_obligations (dvm_id,scope,state)
9709
+ WHERE state NOT IN ('delivered','no_report');
9710
+ CREATE INDEX IF NOT EXISTS sdk_hosted_settlement_report_delivery
9711
+ ON sdk_hosted_settlement_report_obligations (report_id)
9712
+ WHERE report_id IS NOT NULL;
9713
+ `);
9714
+ await this.pool.query(
9715
+ `INSERT INTO sdk_hosted_settlement_report_history (singleton,complete)
9716
+ VALUES (TRUE,$1) ON CONFLICT (singleton) DO NOTHING`,
9717
+ [Object.values(earlier.rows[0]).every((exists) => !exists)]
9718
+ );
9719
+ });
9720
+ }
9721
+ async reserveX402(channelId, operation, expected) {
9722
+ const config = this.x402;
9723
+ if (!config || !hex(channelId, 32)) throw new Error("Hosted x402 settlement configuration is unavailable");
9724
+ const requirements = expected.authorizationRequirements;
9725
+ if (requirements.scheme !== "batch-settlement" || requirements.network !== config.network || requirements.asset.toLowerCase() !== config.token.toLowerCase() || requirements.payTo.toLowerCase() !== config.receiver.toLowerCase() || requirements.amount !== "0") {
9726
+ throw new Error("Hosted x402 authorization has another selected rail or recipient");
9727
+ }
9728
+ const originalCallData = authorizationCallData(operation, expected.authorizationPayload);
9729
+ return this.x402Transaction(async (tx) => {
9730
+ let row;
9731
+ let refundSettlementId;
9732
+ if (operation === "refund") {
9733
+ const result = await tx.query(`SELECT settlement_id,channel_at_submission,payment_payload,payment_requirements
9734
+ FROM x402_batch_settlements
9735
+ WHERE channel_id=$1 AND operation='refund' AND status='submitting'
9736
+ ORDER BY created_at DESC LIMIT 2`, [channelId.toLowerCase()]);
9737
+ if (result.rows.length !== 1 || !result.rows[0].channel_at_submission) {
9738
+ throw new Error("Hosted x402 refund has no unique durable pre-submission source");
9739
+ }
9740
+ const original2 = result.rows[0];
9741
+ const snapshot = original2.channel_at_submission;
9742
+ const payment = original2.payment_payload.payload;
9743
+ const voucher = payment?.voucher;
9744
+ const requirements2 = original2.payment_requirements;
9745
+ if (payment?.type !== "refund" || !payment.channelConfig || computeChannelId2(payment.channelConfig, config.network).toLowerCase() !== channelId.toLowerCase() || String(voucher?.signature).toLowerCase() !== expected.voucherSignature?.toLowerCase() || String(voucher?.channelId).toLowerCase() !== channelId.toLowerCase() || String(voucher?.maxClaimableAmount) !== expected.signedMax || String(snapshot.signature).toLowerCase() !== expected.voucherSignature?.toLowerCase() || String(snapshot.signedMaxClaimable) !== expected.signedMax || requirements2.scheme !== "batch-settlement" || requirements2.network !== config.network || String(requirements2.payTo).toLowerCase() !== config.receiver.toLowerCase() || String(requirements2.asset).toLowerCase() !== config.token.toLowerCase()) {
9746
+ throw new Error("Hosted x402 refund differs from its original signed credit effect");
9747
+ }
9748
+ row = {
9749
+ channel_config: snapshot.channelConfig,
9750
+ charged_cumulative_amount: String(snapshot.chargedCumulativeAmount),
9751
+ signed_max_claimable: String(snapshot.signedMaxClaimable),
9752
+ signature: String(snapshot.signature),
9753
+ total_claimed: String(snapshot.totalClaimed),
9754
+ balance: String(snapshot.balance),
9755
+ refund_nonce: String(snapshot.refundNonce)
9756
+ };
9757
+ refundSettlementId = original2.settlement_id;
9758
+ } else {
9759
+ const result = await tx.query(`SELECT channel_config,charged_cumulative_amount,signed_max_claimable,
9760
+ signature,total_claimed,balance,refund_nonce
9761
+ FROM x402_batch_channels WHERE channel_id=$1`, [channelId.toLowerCase()]);
9762
+ row = result.rows[0];
9763
+ }
9764
+ if (!row) return null;
9765
+ const identity = await this.lockIdentity(tx);
9766
+ if (operation === "claim" && BigInt(row.charged_cumulative_amount) <= BigInt(row.total_claimed)) return null;
9767
+ if (operation === "refund" && BigInt(row.balance) <= BigInt(row.total_claimed)) return null;
9768
+ if (String(row.channel_config.receiver).toLowerCase() !== config.receiver.toLowerCase() || String(row.channel_config.token).toLowerCase() !== config.token.toLowerCase() || computeChannelId2(row.channel_config, config.network).toLowerCase() !== channelId.toLowerCase()) {
9769
+ throw new Error("Hosted x402 channel has another recipient or token");
9770
+ }
9771
+ if (operation === "claim" && (expected.charged !== row.charged_cumulative_amount || expected.signature?.toLowerCase() !== row.signature.toLowerCase() || expected.signedMax !== row.signed_max_claimable)) {
9772
+ throw new Error("Hosted x402 claim changed after selection");
9773
+ }
9774
+ if (operation === "refund" && (expected.refundNonce !== row.refund_nonce || expected.refundAmount !== (BigInt(row.balance) - BigInt(row.charged_cumulative_amount)).toString())) {
9775
+ throw new Error("Hosted x402 refund changed after selection");
9776
+ }
9777
+ const batchId = operation === "claim" || BigInt(row.charged_cumulative_amount) > BigInt(row.total_claimed) ? await this.ensureOpenX402Batch(tx, config) : void 0;
9778
+ const original = operation === "claim" ? {
9779
+ charged: row.charged_cumulative_amount,
9780
+ claimedBefore: row.total_claimed,
9781
+ signed: row.signed_max_claimable,
9782
+ signature: row.signature,
9783
+ batchId,
9784
+ authorizationRequirements: requirements,
9785
+ authorizationCallData: originalCallData
9786
+ } : {
9787
+ charged: row.charged_cumulative_amount,
9788
+ claimedBefore: row.total_claimed,
9789
+ balance: row.balance,
9790
+ refundNonce: row.refund_nonce,
9791
+ refundAmount: (BigInt(row.balance) - BigInt(row.charged_cumulative_amount)).toString(),
9792
+ signed: row.signed_max_claimable,
9793
+ signature: row.signature,
9794
+ settlementId: refundSettlementId,
9795
+ authorizationRequirements: requirements,
9796
+ authorizationCallData: originalCallData,
9797
+ ...batchId ? { batchId } : {}
9798
+ };
9799
+ return this.insert(
9800
+ tx,
9801
+ identity,
9802
+ "x402",
9803
+ operation,
9804
+ config.network,
9805
+ BATCH_SETTLEMENT_ADDRESS3,
9806
+ channelId.toLowerCase(),
9807
+ original,
9808
+ operation === "claim" ? [row.charged_cumulative_amount, row.signature.toLowerCase()] : [refundSettlementId, row.refund_nonce]
9809
+ );
9810
+ });
9811
+ }
9812
+ async reserveTempo(channelId, operation, drainId) {
9813
+ const config = this.tempo;
9814
+ if (!config || !hex(channelId, 32)) throw new Error("Hosted Tempo settlement configuration is unavailable");
9815
+ return transaction(this.pool, async (tx) => {
9816
+ const state = await tx.query(
9817
+ "SELECT state_json FROM mpp_tempo_channels WHERE channel_key=$1",
9818
+ [channelId.toLowerCase()]
9819
+ );
9820
+ if (!state.rows[0]) throw new Error("Hosted Tempo channel is not durable");
9821
+ const identity = await this.lockIdentity(tx);
9822
+ const parsed = parseStoreValue(state.rows[0].state_json);
9823
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("Hosted Tempo state is invalid");
9824
+ const row = parsed;
9825
+ if (row.chainId !== Number(config.network.split(":")[1]) || String(row.escrowContract).toLowerCase() !== config.escrow.toLowerCase() || String(row.payee).toLowerCase() !== config.payee.toLowerCase() || String(row.token).toLowerCase() !== config.token.toLowerCase()) {
9826
+ throw new Error("Hosted Tempo channel has another chain or recipient");
9827
+ }
9828
+ if (operation === "close") {
9829
+ if (!drainId) throw new Error("Hosted Tempo close requires the original drain");
9830
+ const close = await tx.query(
9831
+ "SELECT drain_id FROM mpp_tempo_close_intents WHERE channel_id=$1",
9832
+ [channelId.toLowerCase()]
9833
+ );
9834
+ if (close.rows[0]?.drain_id !== drainId) throw new Error("Hosted Tempo close has no matching drain");
9835
+ }
9836
+ const highestVoucherAmount = nativeAmount(row.highestVoucherAmount);
9837
+ const settledOnChain = nativeAmount(row.settledOnChain);
9838
+ const spent = nativeAmount(row.spent);
9839
+ if (BigInt(settledOnChain) > BigInt(highestVoucherAmount) || BigInt(spent) > BigInt(highestVoucherAmount)) {
9840
+ throw new Error("Hosted Tempo settlement source has inconsistent amounts");
9841
+ }
9842
+ const original = operation === "settle" ? {
9843
+ highestVoucherAmount,
9844
+ settledOnChain,
9845
+ spent
9846
+ } : { drainId, highestVoucherAmount, settledOnChain, spent };
9847
+ return this.insert(
9848
+ tx,
9849
+ identity,
9850
+ "tempo",
9851
+ operation,
9852
+ config.network,
9853
+ config.escrow,
9854
+ channelId.toLowerCase(),
9855
+ original,
9856
+ operation === "close" ? [drainId] : [highestVoucherAmount]
9857
+ );
9858
+ });
9859
+ }
9860
+ async findPendingTempo(channelId, operation) {
9861
+ const result = await this.pool.query(
9862
+ `SELECT * FROM sdk_hosted_settlement_report_obligations
9863
+ WHERE dvm_id=$1 AND scope=$2 AND rail='tempo' AND operation=$3 AND channel_id=$4
9864
+ AND state NOT IN ('delivered','no_report') ORDER BY created_at_ms DESC LIMIT 1`,
9865
+ [this.dvmId, this.scope, operation, channelId.toLowerCase()]
9866
+ );
9867
+ return result.rows[0];
9868
+ }
9869
+ async submittedIds(rail, operation, txHash, batchId) {
9870
+ if (!hex(txHash, 32)) throw new Error("Hosted effect has no exact transaction hash");
9871
+ const result = await this.pool.query(
9872
+ `SELECT id,original FROM sdk_hosted_settlement_report_obligations
9873
+ WHERE dvm_id=$1 AND scope=$2 AND rail=$3 AND operation=$4 AND tx_hash=$5
9874
+ ORDER BY source_key`,
9875
+ [this.dvmId, this.scope, rail, operation, txHash.toLowerCase()]
9876
+ );
9877
+ if (batchId && result.rows.some((row) => row.original.batchId !== batchId)) {
9878
+ throw new Error("Hosted x402 submitted claim belongs to another payout batch");
9879
+ }
9880
+ return result.rows.map((row) => row.id);
9881
+ }
9882
+ async refundClaimDelta(txHash, channelId) {
9883
+ const rows2 = await this.pool.query(
9884
+ `SELECT original FROM sdk_hosted_settlement_report_obligations
9885
+ WHERE dvm_id=$1 AND scope=$2 AND rail='x402' AND operation='refund'
9886
+ AND channel_id=$3 AND tx_hash=$4`,
9887
+ [this.dvmId, this.scope, channelId.toLowerCase(), txHash.toLowerCase()]
9888
+ );
9889
+ if (rows2.rows.length !== 1) throw new Error("Hosted refund has no exact submitted source");
9890
+ const original = rows2.rows[0].original;
9891
+ const delta = BigInt(String(original.charged)) - BigInt(String(original.claimedBefore));
9892
+ if (delta < 0n) throw new Error("Hosted refund original claim amount is invalid");
9893
+ return delta;
9894
+ }
9895
+ async assertReservedTempoClose(id, channelId) {
9896
+ const result = await this.pool.query(
9897
+ "SELECT * FROM sdk_hosted_settlement_report_obligations WHERE id=$1",
9898
+ [id]
9899
+ );
9900
+ const row = result.rows[0];
9901
+ if (!row || row.dvm_id !== this.dvmId || row.scope !== this.scope || row.rail !== "tempo" || row.operation !== "close" || row.channel_id !== channelId.toLowerCase() || row.state !== "reserved" || row.tx_hash) {
9902
+ throw new Error("Hosted Tempo close permit has no original reserved drain");
9903
+ }
9904
+ }
9905
+ async originalTempoCloseSettled(id, channelId, txHash) {
9906
+ const result = await this.pool.query(
9907
+ "SELECT * FROM sdk_hosted_settlement_report_obligations WHERE id=$1",
9908
+ [id]
9909
+ );
9910
+ const row = result.rows[0];
9911
+ if (!row || row.dvm_id !== this.dvmId || row.scope !== this.scope || row.rail !== "tempo" || row.operation !== "close" || row.channel_id !== channelId.toLowerCase() || row.tx_hash?.toLowerCase() !== txHash.toLowerCase()) {
9912
+ throw new Error("Hosted Tempo repair has no matching original close");
9913
+ }
9914
+ const amount = row.original.settledOnChain;
9915
+ if (typeof amount !== "string" || !/^\d+$/.test(amount)) {
9916
+ throw new Error("Hosted Tempo repair has no original settled amount");
9917
+ }
9918
+ return BigInt(amount);
9919
+ }
9920
+ async settlementId(batchId, txHash) {
9921
+ const result = await this.pool.query(
9922
+ `SELECT id,original FROM sdk_hosted_settlement_report_obligations
9923
+ WHERE dvm_id=$1 AND scope=$2 AND rail='x402' AND operation='settle' AND tx_hash=$3`,
9924
+ [this.dvmId, this.scope, txHash.toLowerCase()]
9925
+ );
9926
+ const matching = result.rows.filter((row) => row.original.batchId === batchId);
9927
+ if (matching.length !== 1) throw new Error("Hosted x402 settlement is not bound to the original batch");
9928
+ return matching[0].id;
9929
+ }
9930
+ async reserveOpenX402Settlement() {
9931
+ const config = this.x402;
9932
+ if (!config) throw new Error("Hosted x402 payout batch is unavailable");
9933
+ return this.x402Transaction(async (tx) => {
9934
+ const expectedScope = `${config.network}|${config.receiver.toLowerCase()}|${config.token.toLowerCase()}`;
9935
+ const batch = await tx.query(
9936
+ "SELECT batch_id,claimed_native FROM payout_x402_batches WHERE scope=$1",
9937
+ [expectedScope]
9938
+ );
9939
+ const row = batch.rows[0];
9940
+ if (!row || BigInt(row.claimed_native) <= 0n) {
9941
+ throw new Error("Hosted x402 settlement has no original claimed batch");
9942
+ }
9943
+ const identity = await this.lockIdentity(tx);
9944
+ return this.insert(
9945
+ tx,
9946
+ identity,
9947
+ "x402",
9948
+ "settle",
9949
+ config.network,
9950
+ BATCH_SETTLEMENT_ADDRESS3,
9951
+ expectedScope,
9952
+ { batchId: row.batch_id, claimedNative: row.claimed_native },
9953
+ [row.batch_id]
9954
+ );
9955
+ });
9956
+ }
9957
+ async ensureOpenX402Batch(tx, config) {
9958
+ const scope = `${config.network}|${config.receiver.toLowerCase()}|${config.token.toLowerCase()}`;
9959
+ const result = await tx.query(
9960
+ `INSERT INTO payout_x402_batches
9961
+ (scope,batch_id,claimed_native,voucher_count,claims,opened_at,updated_at)
9962
+ VALUES ($1,$2,0,0,0,
9963
+ FLOOR(EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint,
9964
+ FLOOR(EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint)
9965
+ ON CONFLICT (scope) DO UPDATE SET scope=EXCLUDED.scope
9966
+ RETURNING batch_id`,
9967
+ [scope, randomUUID3()]
9968
+ );
9969
+ return result.rows[0].batch_id;
9970
+ }
9971
+ async recordSubmitted(id, txHash) {
9972
+ if (!hex(txHash, 32)) throw new Error("Hosted settlement has no transaction hash");
9973
+ const write = this.x402SettlementStore ? this.x402Transaction.bind(this) : (operation) => transaction(this.pool, operation);
9974
+ await write(async (tx) => {
9975
+ const row = await this.lockRow(tx, id);
9976
+ if (row.tx_hash && row.tx_hash.toLowerCase() !== txHash.toLowerCase()) {
9977
+ throw new Error("Hosted settlement transaction replay conflicts");
9978
+ }
9979
+ await tx.query(
9980
+ `UPDATE sdk_hosted_settlement_report_obligations SET tx_hash=COALESCE(tx_hash,$2),
9981
+ updated_at_ms=FLOOR(EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint WHERE id=$1`,
9982
+ [id, txHash.toLowerCase()]
9983
+ );
9984
+ });
9985
+ }
9986
+ async recoveryCandidates() {
9987
+ const result = await this.pool.query(
9988
+ `SELECT * FROM sdk_hosted_settlement_report_obligations
9989
+ WHERE dvm_id=$1 AND scope=$2 AND tx_hash IS NOT NULL
9990
+ AND state NOT IN ('delivered','no_report')
9991
+ AND (block_hash IS NULL OR report_id IS NULL)
9992
+ ORDER BY created_at_ms,id`,
9993
+ [this.dvmId, this.scope]
9994
+ );
9995
+ return result.rows.map((row) => ({
9996
+ id: row.id,
9997
+ rail: row.rail,
9998
+ operation: row.operation,
9999
+ channelId: row.channel_id,
10000
+ txHash: row.tx_hash,
10001
+ ...typeof row.original.batchId === "string" ? { batchId: row.original.batchId } : {},
10002
+ reportId: row.report_id,
10003
+ blockHash: row.block_hash,
10004
+ deltaNative: row.delta_native
10005
+ }));
10006
+ }
10007
+ async recoverOriginalX402Refunds() {
10008
+ const rows2 = await this.pool.query(
10009
+ `SELECT obligation.id,settlement.settle_response AS response
10010
+ FROM sdk_hosted_settlement_report_obligations obligation
10011
+ JOIN x402_batch_settlements settlement
10012
+ ON settlement.settlement_id=obligation.original->>'settlementId'
10013
+ WHERE obligation.dvm_id=$1 AND obligation.scope=$2 AND obligation.rail='x402'
10014
+ AND obligation.operation='refund' AND obligation.tx_hash IS NULL
10015
+ AND settlement.operation='refund' AND settlement.channel_id=obligation.channel_id
10016
+ AND settlement.status IN ('settled','complete')
10017
+ AND settlement.settle_response->>'success'='true'`,
10018
+ [this.dvmId, this.scope]
10019
+ );
10020
+ const failures = [];
10021
+ for (const row of rows2.rows) {
10022
+ if (!hex(row.response?.transaction, 32)) continue;
10023
+ try {
10024
+ await this.recordSubmitted(row.id, row.response.transaction);
10025
+ } catch (error) {
10026
+ failures.push({ id: row.id, error });
10027
+ }
10028
+ }
10029
+ return failures;
10030
+ }
10031
+ async reconcileX402Batches() {
10032
+ const batches = await this.pool.query(
10033
+ `SELECT DISTINCT obligation.original->>'batchId' AS batch_id
10034
+ FROM sdk_hosted_settlement_report_obligations obligation
10035
+ JOIN payout_x402_batches batch ON batch.batch_id=obligation.original->>'batchId'
10036
+ WHERE obligation.dvm_id=$1 AND obligation.scope=$2 AND obligation.rail='x402'
10037
+ AND obligation.operation IN ('claim','refund') AND obligation.original ? 'batchId'
10038
+ ORDER BY batch_id`,
10039
+ [this.dvmId, this.scope]
10040
+ );
10041
+ const failures = [];
10042
+ for (const batch of batches.rows) {
10043
+ try {
10044
+ await transaction(this.pool, async (tx) => {
10045
+ const open = await tx.query(
10046
+ "SELECT scope FROM payout_x402_batches WHERE batch_id=$1 FOR UPDATE",
10047
+ [batch.batch_id]
10048
+ );
10049
+ if (!open.rows[0]) return;
10050
+ const sources = await tx.query(
10051
+ `SELECT * FROM sdk_hosted_settlement_report_obligations
10052
+ WHERE dvm_id=$1 AND scope=$2 AND rail='x402'
10053
+ AND operation IN ('claim','refund') AND original->>'batchId'=$3
10054
+ ORDER BY source_key FOR UPDATE`,
10055
+ [this.dvmId, this.scope, batch.batch_id]
10056
+ );
10057
+ if (!sources.rows.length || sources.rows.some((row) => !row.tx_hash || !row.block_hash || row.delta_native === null)) return;
10058
+ const claimed = sources.rows.reduce((sum, row) => sum + BigInt(row.delta_native), 0n);
10059
+ const hashes = new Set(sources.rows.map((row) => row.tx_hash));
10060
+ await tx.query(
10061
+ `UPDATE payout_x402_batches
10062
+ SET claimed_native=$2,voucher_count=$3,claims=$4,obligation_ids=$5,
10063
+ updated_at=FLOOR(EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint
10064
+ WHERE batch_id=$1`,
10065
+ [
10066
+ batch.batch_id,
10067
+ claimed.toString(),
10068
+ sources.rows.length,
10069
+ hashes.size,
10070
+ JSON.stringify(sources.rows.map((row) => row.id))
10071
+ ]
10072
+ );
10073
+ });
10074
+ } catch (error) {
10075
+ failures.push({ id: batch.batch_id, error });
10076
+ }
10077
+ }
10078
+ return failures;
10079
+ }
10080
+ async queueReport(ids, payoutId, reportId, payload, tx) {
10081
+ if (!ids.length || !payoutId || !reportId || new Set(ids).size !== ids.length) {
10082
+ throw new Error("Hosted payout report has no exact obligation set");
10083
+ }
10084
+ if (payoutId.startsWith("x402:batch:")) {
10085
+ const batchId = payoutId.slice("x402:batch:".length);
10086
+ const sources = await tx.query(
10087
+ `SELECT id,tx_hash,operation FROM sdk_hosted_settlement_report_obligations
10088
+ WHERE dvm_id=$1 AND scope=$2 AND rail='x402'
10089
+ AND operation IN ('claim','refund','settle')
10090
+ AND original->>'batchId'=$3 FOR UPDATE`,
10091
+ [this.dvmId, this.scope, batchId]
10092
+ );
10093
+ if (sources.rows.filter((row) => row.operation === "settle").length !== 1 || !sources.rows.some((row) => row.operation === "claim" || row.operation === "refund") || sources.rows.some((row) => !row.tx_hash || !ids.includes(row.id)) || sources.rows.length !== ids.length) {
10094
+ throw new Error("Hosted x402 payout excludes an original effect or its submission is unknown");
10095
+ }
10096
+ } else if (payoutId.startsWith("tempo:")) {
10097
+ if (ids.length !== 1) throw new Error("Hosted Tempo payout requires one original effect");
10098
+ const row = await this.lockRow(tx, ids[0]);
10099
+ if (row.rail !== "tempo" || !row.tx_hash || payoutId !== `tempo:${row.tx_hash}` || row.operation !== "settle" && row.operation !== "close") {
10100
+ throw new Error("Hosted Tempo payout differs from its original effect");
10101
+ }
10102
+ } else {
10103
+ throw new Error("Hosted payout has an unsupported original rail identity");
10104
+ }
10105
+ for (const id of [...ids].sort()) {
10106
+ const row = await this.lockRow(tx, id);
10107
+ if (!row.tx_hash || row.payout_id && row.payout_id !== payoutId || row.report_id && row.report_id !== reportId || row.report_payload && !isDeepStrictEqual2(row.report_payload, payload)) {
10108
+ throw new Error("Hosted payout report has no original transaction or conflicts");
10109
+ }
10110
+ await tx.query(
10111
+ `UPDATE sdk_hosted_settlement_report_obligations
10112
+ SET payout_id=$2, report_id=$3, report_payload=$4,
10113
+ state=CASE WHEN state='delivered' THEN 'delivered'
10114
+ WHEN block_hash IS NULL THEN 'report_queued' ELSE 'observed' END,
10115
+ updated_at_ms=FLOOR(EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint
10116
+ WHERE id=$1`,
10117
+ [id, payoutId, reportId, JSON.stringify(payload)]
10118
+ );
10119
+ }
10120
+ }
10121
+ async hasOriginalReport(ids, payoutId, payload, tx) {
10122
+ if (!ids.length || new Set(ids).size !== ids.length) {
10123
+ throw new Error("Hosted payout replay has no exact original set");
10124
+ }
10125
+ const rows2 = [];
10126
+ for (const id of [...ids].sort()) rows2.push(await this.lockRow(tx, id));
10127
+ const recorded = rows2.filter((row) => row.report_id !== null);
10128
+ if (!recorded.length) return false;
10129
+ const first = recorded[0];
10130
+ if (recorded.length !== ids.length || rows2.some((row) => row.payout_id !== payoutId || row.report_id !== first.report_id || row.report_payload?.dvmId !== payload.dvmId || row.report_payload?.rail !== payload.rail || row.report_payload?.kind !== payload.kind || row.report_payload?.nativeAmount !== payload.nativeAmount || row.report_payload?.nativeAsset !== payload.nativeAsset || row.report_payload?.recipient !== payload.recipient || !isDeepStrictEqual2(row.report_payload?.refs, payload.refs))) {
10131
+ throw new Error("Hosted payout replay conflicts with its original report");
10132
+ }
10133
+ return true;
10134
+ }
10135
+ async assertReportReady(reportId, payload, query = this.pool) {
10136
+ const rows2 = await query.query(
10137
+ `SELECT * FROM sdk_hosted_settlement_report_obligations
10138
+ WHERE report_id=$1 ORDER BY source_key`,
10139
+ [reportId]
10140
+ );
10141
+ if (!rows2.rows.length) {
10142
+ if (await this.hasCompleteHistory(query)) {
10143
+ throw new Error("Hosted payout has no original effect report");
10144
+ }
10145
+ return;
10146
+ }
10147
+ const amount = payload.nativeAmount;
10148
+ if (typeof amount !== "number" || !Number.isSafeInteger(amount) || amount < 0 || rows2.rows.some((row) => row.dvm_id !== this.dvmId || row.scope !== this.scope || row.report_id !== reportId || row.payout_id !== payload.payoutId || !isDeepStrictEqual2(row.report_payload, payload) || row.rail === "x402" && (row.operation === "claim" || row.operation === "refund") && (typeof row.original.authorizationCallData !== "string" || !/^0x(?:[0-9a-f]{2})+$/i.test(row.original.authorizationCallData)) || !row.block_hash || row.delta_native === null || row.state === "no_report")) {
10149
+ throw new Error("Hosted payout has no finalized exact original effect set");
10150
+ }
10151
+ if (payload.rail === "tempo") {
10152
+ if (rows2.rows.length !== 1 || rows2.rows[0].rail !== "tempo" || BigInt(rows2.rows[0].delta_native) !== BigInt(amount)) {
10153
+ throw new Error("Hosted Tempo payout amount differs from its original finalized effect");
10154
+ }
10155
+ } else if (payload.rail === "x402" && typeof payload.payoutId === "string" && payload.payoutId.startsWith("x402:batch:")) {
10156
+ const batchId = payload.payoutId.slice("x402:batch:".length);
10157
+ const settle = rows2.rows.filter((row) => row.operation === "settle" && row.rail === "x402");
10158
+ const claims = rows2.rows.filter((row) => row.operation === "claim" || row.operation === "refund");
10159
+ 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((sum, row) => sum + BigInt(row.delta_native), 0n) !== BigInt(amount)) {
10160
+ throw new Error("Hosted x402 payout differs from its original finalized batch");
10161
+ }
10162
+ } else {
10163
+ throw new Error("Hosted payout has no supported original rail report");
10164
+ }
10165
+ }
10166
+ async acknowledgeDelivered(reportId, tx, requireLinked = true) {
10167
+ const pending = await tx.query(
10168
+ "SELECT payload FROM pending_dvm_reports WHERE id=$1 AND kind='payout' FOR UPDATE",
10169
+ [reportId]
10170
+ );
10171
+ if (!pending.rows[0]) throw new Error("Hosted payout acknowledgement has no durable outbox row");
10172
+ if (pending.rows[0].payload.rail === "x402" || pending.rows[0].payload.rail === "tempo") {
10173
+ await this.assertReportReady(reportId, pending.rows[0].payload, tx);
10174
+ }
10175
+ const rows2 = await tx.query(
10176
+ `UPDATE sdk_hosted_settlement_report_obligations
10177
+ SET delivered_at_ms=COALESCE(delivered_at_ms,
10178
+ FLOOR(EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint),
10179
+ state=CASE WHEN block_hash IS NULL THEN 'report_queued' ELSE 'delivered' END,
10180
+ updated_at_ms=FLOOR(EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint
10181
+ WHERE report_id=$1 AND state <> 'no_report' RETURNING id`,
10182
+ [reportId]
10183
+ );
10184
+ if (!rows2.rows.length && requireLinked) throw new Error("Hosted payout report lacks its original obligation");
10185
+ }
10186
+ async observeCanonical(id) {
10187
+ const result = await this.pool.query(
10188
+ "SELECT * FROM sdk_hosted_settlement_report_obligations WHERE id=$1",
10189
+ [id]
10190
+ );
10191
+ const row = result.rows[0];
10192
+ if (!row || !row.tx_hash || !hex(row.tx_hash, 32)) throw new Error("Hosted effect has no durable transaction");
10193
+ if (row.rail === "x402" && (row.operation === "claim" || row.operation === "refund") && (typeof row.original.authorizationCallData !== "string" || !/^0x(?:[0-9a-f]{2})+$/i.test(row.original.authorizationCallData))) {
10194
+ throw new Error("Hosted x402 original signed authorization is unknown");
10195
+ }
10196
+ if (row.block_hash && row.block_number !== null && row.delta_native !== null) {
10197
+ return {
10198
+ id,
10199
+ txHash: row.tx_hash,
10200
+ blockNumber: row.block_number,
10201
+ blockHash: row.block_hash,
10202
+ deltaNative: row.delta_native
10203
+ };
10204
+ }
10205
+ const config = row.rail === "x402" ? this.x402 : this.tempo;
10206
+ const expectedContract = row.rail === "x402" ? BATCH_SETTLEMENT_ADDRESS3 : this.tempo?.escrow;
10207
+ if (!config || !expectedContract || row.network !== config.network || row.contract !== expectedContract.toLowerCase()) {
10208
+ throw new Error("Hosted effect chain configuration changed");
10209
+ }
10210
+ const { tempo, tempoModerato } = await import("viem/chains");
10211
+ const spec = row.rail === "x402" ? x402NetworkByCaip2(config.network) : void 0;
10212
+ const chains = await import("viem/chains");
10213
+ const chain = row.rail === "x402" ? spec && chains[spec.viemChain] : Number(config.network.split(":")[1]) === tempoModerato.id ? tempoModerato : tempo;
10214
+ if (!chain) throw new Error("Hosted effect chain is unsupported");
10215
+ const rpcUrl = resolveRpcOverride(config.rpcUrl, chain.rpcUrls.default.http[0]).url;
10216
+ const client = createPublicClient4({ chain, transport: http4(rpcUrl) });
10217
+ const [chainId, finalBlock, receipt, transactionRecord] = await Promise.all([
10218
+ client.getChainId(),
10219
+ client.getBlock({ blockTag: "finalized" }),
10220
+ client.getTransactionReceipt({ hash: row.tx_hash }),
10221
+ client.getTransaction({ hash: row.tx_hash })
10222
+ ]);
10223
+ if (chainId !== chain.id || finalBlock.number === null || receipt.status !== "success" || receipt.blockNumber > finalBlock.number || receipt.transactionHash.toLowerCase() !== row.tx_hash.toLowerCase() || transactionRecord.to?.toLowerCase() !== row.contract || !hex(receipt.blockHash, 32)) {
10224
+ throw new Error("Hosted effect is not finalized on its original contract");
10225
+ }
10226
+ const receiptBlock = await client.getBlock({ blockNumber: receipt.blockNumber });
10227
+ if (receiptBlock.hash.toLowerCase() !== receipt.blockHash.toLowerCase()) {
10228
+ throw new Error("Hosted effect receipt is not canonical");
10229
+ }
10230
+ if (row.rail === "x402" && (row.operation === "claim" || row.operation === "refund")) {
10231
+ const committed = row.original.authorizationCallData;
10232
+ if (typeof committed !== "string" || !/^0x[0-9a-f]+$/i.test(committed) || transactionRecord.input?.toLowerCase() !== committed.toLowerCase()) {
10233
+ throw new Error("Hosted x402 transaction differs from its pre-effect signed authorization");
10234
+ }
10235
+ }
10236
+ let delta;
10237
+ if (row.rail === "x402" && row.operation === "claim") {
10238
+ if (!transactionRecord.input) throw new Error("Hosted claim has no transaction input");
10239
+ const decoded = decodeFunctionData2({ abi: X402_CLAIM_ABI, data: transactionRecord.input });
10240
+ if (decoded.functionName !== "claimWithSignature") throw new Error("Hosted claim transaction has another method");
10241
+ const claims = decoded.args[0];
10242
+ const submitted = await this.pool.query(
10243
+ `SELECT * FROM sdk_hosted_settlement_report_obligations
10244
+ WHERE tx_hash=$1 AND rail='x402' AND operation='claim'`,
10245
+ [row.tx_hash]
10246
+ );
10247
+ if (submitted.rows.length !== claims.length || submitted.rows.some((original) => original.dvm_id !== this.dvmId || original.scope !== this.scope || claims.filter(
10248
+ (claim) => computeChannelId2(claim.voucher.channel, config.network).toLowerCase() === original.channel_id && claim.voucher.maxClaimableAmount === BigInt(String(original.original.signed)) && claim.signature.toLowerCase() === String(original.original.signature).toLowerCase() && claim.totalClaimed === BigInt(String(original.original.charged))
10249
+ ).length !== 1)) {
10250
+ throw new Error("Hosted claim transaction contains an unbound voucher");
10251
+ }
10252
+ const source = claims.find(
10253
+ (claim) => computeChannelId2(claim.voucher.channel, config.network).toLowerCase() === row.channel_id && claim.voucher.maxClaimableAmount === BigInt(String(row.original.signed)) && claim.signature.toLowerCase() === String(row.original.signature).toLowerCase() && claim.totalClaimed === BigInt(String(row.original.charged))
10254
+ );
10255
+ if (!source) throw new Error("Hosted claim transaction did not consume the original voucher");
10256
+ const [balance, claimed] = await client.readContract({
10257
+ address: BATCH_SETTLEMENT_ADDRESS3,
10258
+ abi: X402_BATCH_CHANNEL_ABI,
10259
+ functionName: "channels",
10260
+ args: [row.channel_id],
10261
+ blockNumber: receipt.blockNumber
10262
+ });
10263
+ if (claimed < source.totalClaimed || balance < claimed) throw new Error("Hosted claim was not applied");
10264
+ delta = source.totalClaimed - BigInt(String(row.original.claimedBefore));
10265
+ } else {
10266
+ if (row.rail === "x402" && row.operation === "refund") {
10267
+ if (!transactionRecord.input) throw new Error("Hosted refund has no transaction input");
10268
+ let refundData = transactionRecord.input;
10269
+ let claimData;
10270
+ if (refundData.slice(0, 10).toLowerCase() === toFunctionSelector(X402_MULTICALL_ABI[0]).toLowerCase()) {
10271
+ const multicall = decodeFunctionData2({ abi: X402_MULTICALL_ABI, data: refundData });
10272
+ if (multicall.functionName !== "multicall" || multicall.args[0].length !== 2) {
10273
+ throw new Error("Hosted refund has unexpected bundled calls");
10274
+ }
10275
+ [claimData, refundData] = multicall.args[0];
10276
+ }
10277
+ const refund = decodeFunctionData2({ abi: X402_REFUND_ABI, data: refundData });
10278
+ if (refund.functionName !== "refundWithSignature" || computeChannelId2(refund.args[0], config.network).toLowerCase() !== row.channel_id || refund.args[1] !== BigInt(String(row.original.refundAmount)) || refund.args[2] !== BigInt(String(row.original.refundNonce))) {
10279
+ throw new Error("Hosted refund transaction differs from its original request");
10280
+ }
10281
+ if (claimData) {
10282
+ const claim = decodeFunctionData2({ abi: X402_CLAIM_ABI, data: claimData });
10283
+ if (claim.functionName !== "claimWithSignature" || claim.args[0].length !== 1 || !claim.args[0].some((source) => computeChannelId2(source.voucher.channel, config.network).toLowerCase() === row.channel_id && source.voucher.maxClaimableAmount === BigInt(String(row.original.signed)) && source.signature.toLowerCase() === String(row.original.signature).toLowerCase() && source.totalClaimed === BigInt(String(row.original.charged)))) {
10284
+ throw new Error("Hosted refund did not consume its original claim voucher");
10285
+ }
10286
+ } else if (BigInt(String(row.original.charged)) > BigInt(String(row.original.claimedBefore))) {
10287
+ throw new Error("Hosted refund did not include its original claim");
10288
+ }
10289
+ }
10290
+ const event = row.rail === "x402" ? row.operation === "refund" ? X402_REFUNDED_EVENT : X402_SETTLED_EVENT : row.operation === "close" ? TEMPO_CLOSED_EVENT : TEMPO_SETTLED_EVENT;
10291
+ if (!event) throw new Error("Hosted effect event is unsupported");
10292
+ for (const log of receipt.logs) {
10293
+ if (log.address.toLowerCase() !== row.contract) continue;
10294
+ try {
10295
+ const decoded = decodeEventLog({ abi: [event], data: log.data, topics: log.topics });
10296
+ const args = decoded.args;
10297
+ if (row.rail === "x402" && row.operation === "settle" && String(args.receiver).toLowerCase() === this.x402?.receiver.toLowerCase() && String(args.token).toLowerCase() === this.x402?.token.toLowerCase()) {
10298
+ delta = BigInt(String(args.amount));
10299
+ } else if (row.rail === "x402" && row.operation === "refund" && String(args.channelId).toLowerCase() === row.channel_id && BigInt(String(args.amount)) === BigInt(String(row.original.refundAmount))) {
10300
+ delta = BigInt(String(row.original.charged)) - BigInt(String(row.original.claimedBefore));
10301
+ } else if (row.rail === "tempo" && String(args.channelId).toLowerCase() === row.channel_id && String(args.payee).toLowerCase() === this.tempo?.payee.toLowerCase()) {
10302
+ delta = row.operation === "close" ? BigInt(String(args.settledToPayee)) - BigInt(String(row.original.settledOnChain)) : BigInt(String(args.deltaPaid));
10303
+ }
10304
+ } catch {
10305
+ }
10306
+ }
10307
+ }
10308
+ if (delta === void 0 || delta < 0n) throw new Error("Hosted effect has no matching positive or zero result");
10309
+ if (row.report_payload && (row.rail === "tempo" || row.operation === "settle")) {
10310
+ const reported = row.report_payload.nativeAmount;
10311
+ if (typeof reported !== "number" || !Number.isSafeInteger(reported) || reported < 0 || BigInt(reported) !== delta) {
10312
+ throw new Error("Hosted payout amount differs from finalized original effect");
10313
+ }
10314
+ }
10315
+ const canonical = await client.getBlock({ blockNumber: receipt.blockNumber });
10316
+ const currentFinal = await client.getBlock({ blockTag: "finalized" });
10317
+ if (canonical.hash.toLowerCase() !== receipt.blockHash.toLowerCase() || currentFinal.number === null || currentFinal.number < receipt.blockNumber) {
10318
+ throw new Error("Hosted effect finality changed");
10319
+ }
10320
+ await transaction(this.pool, async (tx) => {
10321
+ const current = await this.lockRow(tx, id);
10322
+ if (current.tx_hash?.toLowerCase() !== row.tx_hash.toLowerCase() || current.block_hash && current.block_hash.toLowerCase() !== receipt.blockHash.toLowerCase() || current.delta_native && BigInt(current.delta_native) !== delta) {
10323
+ throw new Error("Hosted effect observation conflicts with durable original");
10324
+ }
10325
+ const noReport = delta === 0n && (row.operation === "refund" || row.operation === "close");
10326
+ await tx.query(
10327
+ `UPDATE sdk_hosted_settlement_report_obligations
10328
+ SET block_number=$2,block_hash=$3,delta_native=$4,
10329
+ state=CASE WHEN $5 THEN 'no_report'
10330
+ WHEN delivered_at_ms IS NOT NULL THEN 'delivered' ELSE 'observed' END,
10331
+ updated_at_ms=FLOOR(EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint
10332
+ WHERE id=$1`,
10333
+ [
10334
+ id,
10335
+ receipt.blockNumber.toString(),
10336
+ receipt.blockHash.toLowerCase(),
10337
+ delta.toString(),
10338
+ noReport
10339
+ ]
10340
+ );
10341
+ });
10342
+ return {
10343
+ id,
10344
+ txHash: row.tx_hash,
10345
+ blockNumber: receipt.blockNumber.toString(),
10346
+ blockHash: receipt.blockHash,
10347
+ deltaNative: delta.toString()
10348
+ };
10349
+ }
10350
+ async lockRow(tx, id) {
10351
+ const result = await tx.query(
10352
+ "SELECT * FROM sdk_hosted_settlement_report_obligations WHERE id=$1 FOR UPDATE",
10353
+ [id]
10354
+ );
10355
+ const row = result.rows[0];
10356
+ if (!row || row.dvm_id !== this.dvmId || row.scope !== this.scope) {
10357
+ throw new Error("Hosted settlement obligation is not owned by this mount");
10358
+ }
10359
+ return row;
10360
+ }
10361
+ async unresolvedCount(query = this.pool) {
10362
+ const result = await query.query(
10363
+ `SELECT COUNT(*)::text AS n FROM sdk_hosted_settlement_report_obligations
10364
+ WHERE ${HOSTED_SETTLEMENT_UNRESOLVED_SQL}`
10365
+ );
10366
+ return Number(result.rows[0].n);
10367
+ }
10368
+ async hasCompleteHistory(query = this.pool) {
10369
+ const result = await query.query(
10370
+ "SELECT complete FROM sdk_hosted_settlement_report_history WHERE singleton=TRUE"
10371
+ );
10372
+ return result.rows[0]?.complete === true;
10373
+ }
10374
+ async lockIdentity(tx) {
10375
+ const result = await tx.query(
10376
+ "SELECT instance_id,boot_epoch FROM sdk_hosting_control WHERE dvm_id=$1 AND scope=$2 FOR UPDATE",
10377
+ [this.dvmId, this.scope]
10378
+ );
10379
+ if (!result.rows[0]?.instance_id || !BigInt(result.rows[0].boot_epoch)) {
10380
+ throw new Error("Hosted settlement identity is unavailable");
10381
+ }
10382
+ return { instanceId: result.rows[0].instance_id, bootEpoch: result.rows[0].boot_epoch };
10383
+ }
10384
+ async insert(tx, identity, rail, operation, network, contract, channelId, original, operationIdentity) {
10385
+ const priorNoEffect = await tx.query(
10386
+ `SELECT COUNT(*)::text AS n FROM sdk_hosted_settlement_report_obligations
10387
+ WHERE dvm_id=$1 AND scope=$2 AND rail=$3 AND operation=$4 AND channel_id=$5
10388
+ AND state='no_report'`,
10389
+ [this.dvmId, this.scope, rail, operation, channelId]
10390
+ );
10391
+ const sourceKey = hash2([
10392
+ this.dvmId,
10393
+ this.scope,
10394
+ rail,
10395
+ operation,
10396
+ network,
10397
+ contract.toLowerCase(),
10398
+ channelId,
10399
+ operationIdentity,
10400
+ priorNoEffect.rows[0].n
10401
+ ]);
10402
+ const competing = await tx.query(
10403
+ `SELECT source_key FROM sdk_hosted_settlement_report_obligations
10404
+ WHERE dvm_id=$1 AND scope=$2 AND rail=$3 AND operation=$4 AND channel_id=$5
10405
+ AND state NOT IN ('delivered','no_report') FOR UPDATE`,
10406
+ [this.dvmId, this.scope, rail, operation, channelId]
10407
+ );
10408
+ if (competing.rows.some((row2) => row2.source_key !== sourceKey)) {
10409
+ throw new Error("Hosted settlement has another unresolved original effect");
10410
+ }
10411
+ const id = randomUUID3();
10412
+ const result = await tx.query(
10413
+ `INSERT INTO sdk_hosted_settlement_report_obligations
10414
+ (id,source_key,dvm_id,scope,instance_id,boot_epoch,rail,operation,network,
10415
+ contract,channel_id,original,state)
10416
+ VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,'reserved')
10417
+ ON CONFLICT (source_key) DO NOTHING RETURNING id`,
10418
+ [
10419
+ id,
10420
+ sourceKey,
10421
+ this.dvmId,
10422
+ this.scope,
10423
+ identity.instanceId,
10424
+ identity.bootEpoch,
10425
+ rail,
10426
+ operation,
10427
+ network,
10428
+ contract.toLowerCase(),
10429
+ channelId,
10430
+ JSON.stringify(original)
10431
+ ]
10432
+ );
10433
+ if (result.rows[0]) return { id, replayed: false };
10434
+ const prior = await tx.query(
10435
+ "SELECT * FROM sdk_hosted_settlement_report_obligations WHERE source_key=$1",
10436
+ [sourceKey]
10437
+ );
10438
+ const row = prior.rows[0];
10439
+ if (!row || row.dvm_id !== this.dvmId || row.scope !== this.scope || row.instance_id !== identity.instanceId || !isDeepStrictEqual2(row.original, original)) throw new Error("Hosted settlement source replay conflicts");
10440
+ return { id: row.id, replayed: true };
10441
+ }
10442
+ };
10443
+
10444
+ // src/sdk/server/hosting-channel-evidence.ts
10445
+ import { createHash as createHash6 } from "crypto";
10446
+ import { Session as TempoSession2 } from "mppx/tempo";
10447
+ import { createPublicClient as createPublicClient5, http as http5 } from "viem";
10448
+ import { BATCH_SETTLEMENT_ADDRESS as BATCH_SETTLEMENT_ADDRESS4 } from "@x402/evm";
10449
+ function unsigned(value) {
10450
+ if (typeof value !== "string" && typeof value !== "bigint") return void 0;
10451
+ try {
10452
+ const number = BigInt(value);
10453
+ return number >= 0n ? number : void 0;
10454
+ } catch {
10455
+ return void 0;
10456
+ }
10457
+ }
10458
+ function hex2(value, bytes) {
10459
+ return typeof value === "string" && new RegExp(`^0x[0-9a-fA-F]{${bytes * 2}}$`).test(value);
10460
+ }
10461
+ async function rows(query, table, order) {
10462
+ const result = await query.query(`SELECT * FROM ${table} ORDER BY ${order}`);
10463
+ return result.rows;
10464
+ }
10465
+ async function readHostedChannelSnapshot(query) {
10466
+ const names = [
10467
+ "x402_batch_channels",
10468
+ "x402_batch_settlements",
10469
+ "mpp_tempo_channels",
10470
+ "mpp_tempo_close_intents",
10471
+ "x402_batch_channel_tombstones",
10472
+ "mpp_tempo_channel_tombstones",
10473
+ "x402_batch_channel_history",
10474
+ "mpp_tempo_channel_history",
10475
+ "sdk_hosted_settlement_report_obligations"
10476
+ ];
10477
+ const present = await query.query(
10478
+ "SELECT name, to_regclass(name) IS NOT NULL AS exists FROM unnest($1::text[]) AS name",
10479
+ [names]
10480
+ );
10481
+ const exists = new Set(present.rows.filter((row) => row.exists).map((row) => row.name));
10482
+ const x402 = exists.has(names[0]) ? await rows(query, names[0], "channel_id") : [];
10483
+ const x402Settlements = exists.has(names[1]) ? await rows(query, names[1], "settlement_id") : [];
10484
+ const tempo = exists.has(names[2]) ? await rows(query, names[2], "channel_key") : [];
10485
+ const tempoCloses = exists.has(names[3]) ? await rows(query, names[3], "channel_id") : [];
10486
+ const x402Tombstones = exists.has(names[4]) ? await rows(query, names[4], "channel_id") : [];
10487
+ const tempoTombstones = exists.has(names[5]) ? await rows(query, names[5], "channel_key") : [];
10488
+ const effects = exists.has(names[8]) ? await rows(query, names[8], "source_key") : [];
10489
+ const history = async (table) => {
10490
+ if (!exists.has(table)) return null;
10491
+ const result = await query.query(`SELECT complete FROM ${table} WHERE singleton=TRUE`);
10492
+ return result.rows.length === 1 ? result.rows[0].complete === true : false;
9072
10493
  };
10494
+ const x402HistoryComplete = await history(names[6]);
10495
+ const tempoHistoryComplete = await history(names[7]);
10496
+ const digest = createHash6("sha256").update(JSON.stringify({
10497
+ present: [...exists].sort(),
10498
+ x402,
10499
+ x402Tombstones,
10500
+ x402Settlements,
10501
+ tempo,
10502
+ tempoTombstones,
10503
+ tempoCloses,
10504
+ effects,
10505
+ x402HistoryComplete,
10506
+ tempoHistoryComplete
10507
+ })).digest("hex");
9073
10508
  return {
9074
- verified: true,
9075
- amountMsats: btcUsdRate > 0 ? usdcToMsats(amountUsdcMicro / 1e6, btcUsdRate) : 0,
9076
- amountUsdcMicro,
9077
- txHash: intent.transaction,
9078
- settleResponseHeader: encodeSettleResponseHeader(response)
10509
+ digest,
10510
+ present: [...exists],
10511
+ x402,
10512
+ x402Tombstones,
10513
+ x402Settlements,
10514
+ tempo,
10515
+ tempoTombstones,
10516
+ tempoCloses,
10517
+ effects,
10518
+ x402HistoryComplete,
10519
+ tempoHistoryComplete
9079
10520
  };
9080
10521
  }
9081
- function settlementMayHaveMoved(response) {
9082
- const reason = response.errorReason?.toLowerCase() ?? "";
9083
- return reason.includes("authorization_already_used") || reason.includes("authorization already used") || /facilitator_status_5\d\d/.test(reason);
9084
- }
9085
- function hash(parts) {
9086
- return createHash4("sha256").update(JSON.stringify(parts)).digest("hex");
10522
+ async function finalized(client, expectedChainId) {
10523
+ const chainId = await client.getChainId();
10524
+ if (chainId !== expectedChainId) throw new Error("configured RPC chain mismatch");
10525
+ const block = await client.getBlock({ blockTag: "finalized" });
10526
+ if (block.number === null || !hex2(block.hash, 32)) throw new Error("finalized block unavailable");
10527
+ return { chainId, number: block.number, hash: block.hash };
10528
+ }
10529
+ async function stillFinalized(client, selected) {
10530
+ const [canonical, current] = await Promise.all([
10531
+ client.getBlock({ blockNumber: selected.number }),
10532
+ client.getBlock({ blockTag: "finalized" })
10533
+ ]);
10534
+ if (canonical.hash.toLowerCase() !== selected.hash.toLowerCase() || current.number === null || current.number < selected.number) {
10535
+ throw new Error("finalized block changed");
10536
+ }
9087
10537
  }
9088
- function safeError(err) {
9089
- if (err instanceof Error) return err.name;
9090
- return "settlement_transport_error";
10538
+ async function verifyOriginalEffect(client, effect, selected, contract, requireSignedCall) {
10539
+ const number = unsigned(effect.block_number);
10540
+ const delta = unsigned(effect.delta_native);
10541
+ if (!hex2(effect.tx_hash, 32) || !hex2(effect.block_hash, 32) || number === void 0 || number > selected || delta === void 0 || effect.contract !== contract || effect.state !== "delivered" && effect.state !== "no_report" || effect.state === "no_report" && (delta !== 0n || effect.operation !== "refund" && effect.operation !== "close")) {
10542
+ throw new Error("original settlement or report remains unresolved");
10543
+ }
10544
+ const [receipt, canonical] = await Promise.all([
10545
+ client.getTransactionReceipt({ hash: effect.tx_hash }),
10546
+ client.getBlock({ blockNumber: number })
10547
+ ]);
10548
+ if (receipt.status !== "success" || receipt.blockNumber !== number || receipt.transactionHash.toLowerCase() !== effect.tx_hash.toLowerCase() || receipt.blockHash.toLowerCase() !== effect.block_hash.toLowerCase() || canonical.hash.toLowerCase() !== effect.block_hash.toLowerCase()) {
10549
+ throw new Error("original effect receipt is not canonical at the selected block");
10550
+ }
10551
+ if (requireSignedCall) {
10552
+ const original = effect.original;
10553
+ const call = original?.authorizationCallData;
10554
+ if (typeof call !== "string" || !/^0x(?:[0-9a-f]{2})+$/i.test(call)) {
10555
+ throw new Error("original signed call is unknown");
10556
+ }
10557
+ const transaction2 = await client.getTransaction({ hash: effect.tx_hash });
10558
+ if (transaction2.to?.toLowerCase() !== contract || transaction2.input.toLowerCase() !== call.toLowerCase()) {
10559
+ throw new Error("original signed call differs from the finalized transaction");
10560
+ }
10561
+ }
10562
+ return delta;
9091
10563
  }
10564
+ var HostedChannelEvidenceReader = class _HostedChannelEvidenceReader {
10565
+ constructor(pool, x402, tempo) {
10566
+ this.pool = pool;
10567
+ this.x402 = x402;
10568
+ this.tempo = tempo;
10569
+ }
10570
+ pool;
10571
+ x402;
10572
+ tempo;
10573
+ static async fromServerConfig(pool, config) {
10574
+ let x402;
10575
+ if (config.x402) {
10576
+ const spec = x402NetworkByCaip2(config.x402.network);
10577
+ if (spec) {
10578
+ const chains = await import("viem/chains");
10579
+ const chain = chains[spec.viemChain];
10580
+ if (chain) {
10581
+ const rpcUrl = resolveRpcOverride(config.x402.rpcUrl, chain.rpcUrls.default.http[0]).url;
10582
+ x402 = {
10583
+ client: createPublicClient5({ chain, transport: http5(rpcUrl) }),
10584
+ chainId: chain.id
10585
+ };
10586
+ }
10587
+ }
10588
+ }
10589
+ let tempo;
10590
+ if (config.tempoEnv) {
10591
+ const network = resolveTempoNetwork(config.tempoEnv);
10592
+ const { tempo: mainnet, tempoModerato: moderato } = await import("viem/chains");
10593
+ const chain = network.chainId === TEMPO_MODERATO_CHAIN_ID ? moderato : mainnet;
10594
+ const rpcUrl = resolveRpcOverride(
10595
+ config.tempoEnv.DVMKIT_TEMPO_RPC_URL,
10596
+ network.chainId === TEMPO_MODERATO_CHAIN_ID ? TEMPO_MODERATO_RPC : chain.rpcUrls.default.http[0]
10597
+ ).url;
10598
+ tempo = {
10599
+ client: createPublicClient5({ chain, transport: http5(rpcUrl) }),
10600
+ chainId: network.chainId,
10601
+ currency: network.currency.toLowerCase()
10602
+ };
10603
+ }
10604
+ return new _HostedChannelEvidenceReader(pool, x402, tempo);
10605
+ }
10606
+ async observe(identity) {
10607
+ const snapshot = await readHostedChannelSnapshot(this.pool);
10608
+ const observation = { digest: snapshot.digest, x402: null, tempo: null, reasons: [] };
10609
+ if ((snapshot.present.includes("x402_batch_channels") || snapshot.present.includes("x402_batch_channel_tombstones")) && snapshot.x402HistoryComplete !== true) {
10610
+ observation.reasons.push("x402_channel_history_incomplete");
10611
+ }
10612
+ if ((snapshot.present.includes("mpp_tempo_channels") || snapshot.present.includes("mpp_tempo_channel_tombstones")) && snapshot.tempoHistoryComplete !== true) {
10613
+ observation.reasons.push("tempo_channel_history_incomplete");
10614
+ }
10615
+ if (this.x402 && !snapshot.present.includes("x402_batch_channel_tombstones")) {
10616
+ observation.reasons.push("x402_channel_history_missing");
10617
+ }
10618
+ if (this.tempo && (!snapshot.present.includes("mpp_tempo_channels") || !snapshot.present.includes("mpp_tempo_close_intents") || !snapshot.present.includes("mpp_tempo_channel_tombstones"))) {
10619
+ observation.reasons.push("tempo_channel_history_missing");
10620
+ }
10621
+ if (snapshot.x402.length || snapshot.x402Tombstones.length || snapshot.effects.some((effect) => effect.rail === "x402")) {
10622
+ try {
10623
+ observation.x402 = await this.observeX402(snapshot, identity);
10624
+ } catch {
10625
+ observation.reasons.push("x402_channel_evidence_unresolved");
10626
+ }
10627
+ }
10628
+ if (snapshot.tempo.length || snapshot.tempoTombstones.length || snapshot.tempoCloses.length || snapshot.effects.some((effect) => effect.rail === "tempo")) {
10629
+ try {
10630
+ observation.tempo = await this.observeTempo(snapshot, identity);
10631
+ } catch {
10632
+ observation.reasons.push("tempo_channel_evidence_unresolved");
10633
+ }
10634
+ }
10635
+ return observation;
10636
+ }
10637
+ async observeX402(snapshot, identity) {
10638
+ if (!this.x402 || !identity || snapshot.x402Settlements.some((row) => row.status !== "complete")) {
10639
+ throw new Error("x402 settlement unresolved");
10640
+ }
10641
+ const { client, chainId } = this.x402;
10642
+ const block = await finalized(client, chainId);
10643
+ const channels = [];
10644
+ const channelIds = new Set([...snapshot.x402, ...snapshot.x402Tombstones].map((row) => row.channel_id));
10645
+ if (snapshot.effects.some((effect) => effect.rail === "x402" && (effect.operation === "claim" || effect.operation === "refund") && !channelIds.has(effect.channel_id))) throw new Error("original x402 channel disappeared");
10646
+ for (const row of [...snapshot.x402, ...snapshot.x402Tombstones]) {
10647
+ if (!hex2(row.channel_id, 32)) throw new Error("invalid channel");
10648
+ if (row.pending_request !== null) {
10649
+ const pending2 = row.pending_request;
10650
+ const pendingId = pending2 && typeof pending2 === "object" && !Array.isArray(pending2) ? pending2.pendingId : void 0;
10651
+ if (!snapshot.x402Tombstones.includes(row) || typeof pendingId !== "string" || !snapshot.x402Settlements.some((settlement) => settlement.channel_id === row.channel_id && settlement.pending_id === pendingId && settlement.status === "complete")) throw new Error("x402 request remains pending");
10652
+ }
10653
+ const binding = row.hosting_binding;
10654
+ if (!binding || typeof binding !== "object" || Array.isArray(binding)) {
10655
+ throw new Error("historical x402 channel has no original network");
10656
+ }
10657
+ const original = binding;
10658
+ if (original.network !== `eip155:${chainId}` || original.chainId !== chainId || original.contract !== BATCH_SETTLEMENT_ADDRESS4.toLowerCase() || original.version !== "x402-batch-settlement-v1" || original.channelId !== row.channel_id) {
10659
+ throw new Error("x402 channel original contract differs from configured chain");
10660
+ }
10661
+ const charged = unsigned(row.charged_cumulative_amount);
10662
+ const signed = unsigned(row.signed_max_claimable);
10663
+ const localNonce = unsigned(row.refund_nonce);
10664
+ if (charged === void 0 || signed === void 0 || localNonce === void 0) throw new Error("unpriced channel");
10665
+ const originalEffects = snapshot.effects.filter((effect) => effect.rail === "x402" && (effect.operation === "claim" || effect.operation === "refund") && effect.channel_id === row.channel_id);
10666
+ const intervals = [];
10667
+ const refundNonces = [];
10668
+ for (const effect of originalEffects) {
10669
+ if (effect.dvm_id !== identity.dvmId || effect.scope !== identity.scope || effect.instance_id !== identity.instanceId || effect.network !== original.network) {
10670
+ throw new Error("original x402 effect has another owner or chain");
10671
+ }
10672
+ const delta = await verifyOriginalEffect(
10673
+ client,
10674
+ effect,
10675
+ block.number,
10676
+ BATCH_SETTLEMENT_ADDRESS4.toLowerCase(),
10677
+ true
10678
+ );
10679
+ const source = effect.original;
10680
+ const start = unsigned(source?.claimedBefore);
10681
+ const end = unsigned(source?.charged);
10682
+ if (start === void 0 || end === void 0 || end < start || end - start !== delta) {
10683
+ throw new Error("original x402 claim interval is incomplete");
10684
+ }
10685
+ if (end > start) intervals.push({ start, end });
10686
+ if (effect.operation === "refund") {
10687
+ const preRefundNonce = unsigned(source?.refundNonce);
10688
+ if (preRefundNonce === void 0) throw new Error("original x402 refund nonce is unknown");
10689
+ refundNonces.push(preRefundNonce);
10690
+ }
10691
+ }
10692
+ intervals.sort((a, b) => a.start < b.start ? -1 : a.start > b.start ? 1 : 0);
10693
+ let covered = 0n;
10694
+ for (const interval of intervals) {
10695
+ if (interval.start !== covered) throw new Error("original x402 claims have a gap or duplicate");
10696
+ covered = interval.end;
10697
+ }
10698
+ if (covered < charged || covered < signed) {
10699
+ throw new Error("original x402 effects do not cover accepted voucher authority");
10700
+ }
10701
+ refundNonces.sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
10702
+ for (let index = 0; index < refundNonces.length; index++) {
10703
+ if (refundNonces[index] !== BigInt(index)) {
10704
+ throw new Error("original x402 refunds have a gap or duplicate nonce");
10705
+ }
10706
+ }
10707
+ const [state, withdrawal, nonce] = await Promise.all([
10708
+ client.readContract({
10709
+ address: BATCH_SETTLEMENT_ADDRESS4,
10710
+ abi: X402_BATCH_CHANNEL_ABI,
10711
+ functionName: "channels",
10712
+ args: [row.channel_id],
10713
+ blockNumber: block.number
10714
+ }),
10715
+ client.readContract({
10716
+ address: BATCH_SETTLEMENT_ADDRESS4,
10717
+ abi: X402_BATCH_CHANNEL_ABI,
10718
+ functionName: "pendingWithdrawals",
10719
+ args: [row.channel_id],
10720
+ blockNumber: block.number
10721
+ }),
10722
+ client.readContract({
10723
+ address: BATCH_SETTLEMENT_ADDRESS4,
10724
+ abi: X402_BATCH_CHANNEL_ABI,
10725
+ functionName: "refundNonce",
10726
+ args: [row.channel_id],
10727
+ blockNumber: block.number
10728
+ })
10729
+ ]);
10730
+ const [balance, claimed] = state;
10731
+ const [pending, initiatedAt] = withdrawal;
10732
+ if (balance !== claimed || BigInt(pending) !== 0n || initiatedAt !== 0 || claimed !== covered || localNonce > BigInt(refundNonces.length) || nonce !== BigInt(refundNonces.length)) {
10733
+ throw new Error("x402 balance, original claims, or refunds remain unresolved");
10734
+ }
10735
+ channels.push(row.channel_id);
10736
+ }
10737
+ for (const effect of snapshot.effects.filter((row) => row.rail === "x402" && row.operation === "settle")) {
10738
+ if (effect.dvm_id !== identity.dvmId || effect.scope !== identity.scope || effect.instance_id !== identity.instanceId || effect.network !== `eip155:${chainId}`) {
10739
+ throw new Error("original x402 payout has another owner or chain");
10740
+ }
10741
+ await verifyOriginalEffect(
10742
+ client,
10743
+ effect,
10744
+ block.number,
10745
+ BATCH_SETTLEMENT_ADDRESS4.toLowerCase(),
10746
+ false
10747
+ );
10748
+ }
10749
+ await stillFinalized(client, block);
10750
+ return { chainId, blockNumber: block.number.toString(), blockHash: block.hash, channels };
10751
+ }
10752
+ async observeTempo(snapshot, identity) {
10753
+ const tempoRows = [...snapshot.tempo, ...snapshot.tempoTombstones.filter((row) => !snapshot.tempo.some((live) => live.channel_key === row.channel_key))];
10754
+ if (!this.tempo || !identity || snapshot.tempoCloses.length !== tempoRows.length || snapshot.tempoCloses.some((close) => !tempoRows.some((row) => row.channel_key === close.channel_id))) {
10755
+ throw new Error("Tempo close facts incomplete");
10756
+ }
10757
+ const { client, chainId, currency } = this.tempo;
10758
+ const block = await finalized(client, chainId);
10759
+ const channels = [];
10760
+ if (snapshot.effects.some((effect) => effect.rail === "tempo" && !tempoRows.some((row) => row.channel_key === effect.channel_id))) {
10761
+ throw new Error("original Tempo channel disappeared");
10762
+ }
10763
+ for (const row of tempoRows) {
10764
+ const state = typeof row.state_json === "string" ? parseStoreValue(row.state_json) : void 0;
10765
+ if (!state || typeof state !== "object" || Array.isArray(state)) throw new Error("Tempo state invalid");
10766
+ const local = state;
10767
+ const channelId = local.channelId;
10768
+ const escrow = local.escrowContract;
10769
+ const payer = local.payer;
10770
+ const payee = local.payee;
10771
+ const close = snapshot.tempoCloses.find((item) => item.channel_id === channelId);
10772
+ const spent = unsigned(local.spent);
10773
+ const highest = unsigned(local.highestVoucherAmount);
10774
+ const settled = unsigned(local.settledOnChain);
10775
+ const deposit = unsigned(local.deposit);
10776
+ const requested = unsigned(local.closeRequestedAt);
10777
+ if (!hex2(channelId, 32) || row.channel_key !== channelId.toLowerCase() || !hex2(escrow, 20) || escrow.toLowerCase() !== TempoSession2.Precompile.Constants.tip20ChannelEscrow.toLowerCase() || !hex2(payer, 20) || !hex2(payee, 20) || local.chainId !== chainId || String(local.token).toLowerCase() !== currency || local.finalized !== true || spent === void 0 || highest === void 0 || settled === void 0 || deposit === void 0 || requested === void 0 || !close || close.forecast_pending !== false || !hex2(close.reference, 32)) {
10778
+ throw new Error("Tempo local close evidence incomplete");
10779
+ }
10780
+ const [chain, receipt] = await Promise.all([
10781
+ TempoSession2.Precompile.Chain.getChannelState(client, channelId, escrow, block.number),
10782
+ client.getTransactionReceipt({ hash: close.reference })
10783
+ ]);
10784
+ if (chain.deposit !== 0n || chain.settled < spent || chain.settled < settled || receipt.status !== "success" || receipt.blockNumber > block.number || receipt.transactionHash.toLowerCase() !== close.reference.toLowerCase() || !hex2(receipt.blockHash, 32)) throw new Error("Tempo chain close not complete");
10785
+ const receiptBlock = await client.getBlock({ blockNumber: receipt.blockNumber });
10786
+ if (receiptBlock.hash.toLowerCase() !== receipt.blockHash.toLowerCase()) {
10787
+ throw new Error("Tempo close receipt is not canonical");
10788
+ }
10789
+ const event = TempoSession2.Precompile.Chain.getChannelEvent(
10790
+ {
10791
+ ...receipt,
10792
+ logs: receipt.logs.filter((log) => log.address.toLowerCase() === escrow.toLowerCase())
10793
+ },
10794
+ "ChannelClosed",
10795
+ channelId
10796
+ );
10797
+ const amounts = TempoSession2.Precompile.Chain.readChannelClosedReceiptFields(event);
10798
+ if (chain.settled + amounts.refundedToPayer < highest) {
10799
+ throw new Error("Tempo close did not cover the accepted voucher");
10800
+ }
10801
+ const effects = snapshot.effects.filter((effect) => effect.rail === "tempo" && effect.channel_id === channelId);
10802
+ const closes = effects.filter((effect) => effect.operation === "close" && effect.tx_hash === close.reference);
10803
+ if (closes.length !== 1) throw new Error("Tempo close has no original effect");
10804
+ for (const effect of effects) {
10805
+ if (effect.dvm_id !== identity.dvmId || effect.scope !== identity.scope || effect.instance_id !== identity.instanceId || effect.network !== `eip155:${chainId}`) {
10806
+ throw new Error("original Tempo effect has another owner or chain");
10807
+ }
10808
+ const delta = await verifyOriginalEffect(
10809
+ client,
10810
+ effect,
10811
+ block.number,
10812
+ escrow.toLowerCase(),
10813
+ false
10814
+ );
10815
+ if (effect.operation === "close") {
10816
+ const original = effect.original;
10817
+ const before = unsigned(original?.settledOnChain);
10818
+ const accepted = unsigned(original?.highestVoucherAmount);
10819
+ if (before === void 0 || accepted === void 0 || before + delta !== chain.settled || accepted < highest || effect.block_hash !== receipt.blockHash.toLowerCase()) {
10820
+ throw new Error("Tempo original close does not discharge the accepted spend");
10821
+ }
10822
+ }
10823
+ }
10824
+ channels.push(channelId);
10825
+ }
10826
+ await stillFinalized(client, block);
10827
+ return { chainId, blockNumber: block.number.toString(), blockHash: block.hash, channels };
10828
+ }
10829
+ };
9092
10830
 
9093
10831
  // src/sdk/server/hosting-control.ts
9094
- import { createHmac, timingSafeEqual } from "crypto";
9095
10832
  function signHostingControl(secret, command) {
9096
10833
  const signed = command.version === 2 ? [
9097
10834
  "dvmkit-hosting-v2",
@@ -9115,18 +10852,35 @@ function signHostingControl(secret, command) {
9115
10852
  ];
9116
10853
  return createHmac("sha256", secret).update(JSON.stringify(signed)).digest("hex");
9117
10854
  }
10855
+ function signHostingClosure(secret, command) {
10856
+ return createHmac("sha256", secret).update(JSON.stringify([
10857
+ "dvmkit-hosting-closure-v3",
10858
+ command.action,
10859
+ command.scope,
10860
+ command.dvmId,
10861
+ command.generation,
10862
+ command.operationId,
10863
+ command.timestampMs,
10864
+ command.nonce
10865
+ ])).digest("hex");
10866
+ }
9118
10867
  var HostingControl = class {
9119
- constructor(pool, dvmId, config) {
10868
+ constructor(pool, dvmId, config, channelReader) {
9120
10869
  this.pool = pool;
9121
10870
  this.dvmId = dvmId;
9122
10871
  this.config = config;
10872
+ this.channelReader = channelReader;
9123
10873
  if (Buffer.byteLength(config.secret) < 32 || !config.scope) {
9124
10874
  throw new Error("Hosting control requires a 32-byte secret and a non-empty scope");
9125
10875
  }
10876
+ this.cleanupFence = new HostingCleanupFence(pool, dvmId, config.scope);
9126
10877
  }
9127
10878
  pool;
9128
10879
  dvmId;
9129
10880
  config;
10881
+ channelReader;
10882
+ cleanupFence;
10883
+ bootEpoch;
9130
10884
  async init() {
9131
10885
  await withSdkInitLock(this.pool, async () => {
9132
10886
  await this.pool.query(`
@@ -9140,17 +10894,27 @@ var HostingControl = class {
9140
10894
  created_at_ms BIGINT NOT NULL,
9141
10895
  PRIMARY KEY (dvm_id, scope, nonce)
9142
10896
  );
10897
+ ALTER TABLE sdk_hosting_control ADD COLUMN IF NOT EXISTS lease_expires_at_ms BIGINT;
10898
+ ALTER TABLE sdk_hosting_control ADD COLUMN IF NOT EXISTS instance_id TEXT;
10899
+ ALTER TABLE sdk_hosting_control ADD COLUMN IF NOT EXISTS boot_epoch BIGINT NOT NULL DEFAULT 0;
10900
+ ALTER TABLE sdk_hosting_control ADD COLUMN IF NOT EXISTS closed_operation_id TEXT;
10901
+ ALTER TABLE sdk_hosting_control ADD COLUMN IF NOT EXISTS closed_at_ms BIGINT;
10902
+ ALTER TABLE sdk_hosting_control ADD COLUMN IF NOT EXISTS closed_at_us BIGINT;
10903
+ ALTER TABLE sdk_hosting_control ADD COLUMN IF NOT EXISTS cutoff_txid BIGINT;
10904
+ ALTER TABLE sdk_hosting_control ADD COLUMN IF NOT EXISTS channel_evidence JSONB;
9143
10905
  CREATE TABLE IF NOT EXISTS sdk_hosting_admissions (
9144
10906
  id TEXT PRIMARY KEY, dvm_id TEXT NOT NULL, scope TEXT NOT NULL,
9145
10907
  job_id TEXT NOT NULL, admitted_at_ms BIGINT NOT NULL,
9146
10908
  outcome TEXT NOT NULL DEFAULT 'pending', drain_deadline_ms BIGINT,
9147
- credit_id TEXT, fund_id TEXT, payment_artifact_hash TEXT
10909
+ credit_id TEXT, fund_id TEXT, caller_pubkey TEXT, funding_commitment TEXT,
10910
+ payment_artifact_hash TEXT
9148
10911
  );
9149
10912
  ALTER TABLE sdk_hosting_admissions ADD COLUMN IF NOT EXISTS drain_deadline_ms BIGINT;
9150
10913
  ALTER TABLE sdk_hosting_admissions ADD COLUMN IF NOT EXISTS credit_id TEXT;
9151
10914
  ALTER TABLE sdk_hosting_admissions ADD COLUMN IF NOT EXISTS fund_id TEXT;
10915
+ ALTER TABLE sdk_hosting_admissions ADD COLUMN IF NOT EXISTS caller_pubkey TEXT;
10916
+ ALTER TABLE sdk_hosting_admissions ADD COLUMN IF NOT EXISTS funding_commitment TEXT;
9152
10917
  ALTER TABLE sdk_hosting_admissions ADD COLUMN IF NOT EXISTS payment_artifact_hash TEXT;
9153
- ALTER TABLE sdk_hosting_control ADD COLUMN IF NOT EXISTS lease_expires_at_ms BIGINT;
9154
10918
  CREATE INDEX IF NOT EXISTS sdk_hosting_admissions_pending
9155
10919
  ON sdk_hosting_admissions (dvm_id, scope) WHERE outcome = 'pending';
9156
10920
  CREATE INDEX IF NOT EXISTS sdk_hosting_admissions_unresolved
@@ -9170,6 +10934,14 @@ var HostingControl = class {
9170
10934
  const client = await this.pool.connect();
9171
10935
  try {
9172
10936
  await client.query("BEGIN");
10937
+ const boot = await client.query(
10938
+ `UPDATE sdk_hosting_control
10939
+ SET instance_id=COALESCE(instance_id,$3), boot_epoch=boot_epoch+1
10940
+ WHERE dvm_id=$1 AND scope=$2 RETURNING boot_epoch`,
10941
+ [this.dvmId, this.config.scope, randomUUID4()]
10942
+ );
10943
+ if (!boot.rows[0]) throw new Error("Hosting control identity was not initialized");
10944
+ this.bootEpoch = Number(boot.rows[0].boot_epoch);
9173
10945
  const migrated = await client.query(
9174
10946
  `UPDATE sdk_hosting_control SET state='draining',
9175
10947
  deadline_ms=FLOOR(EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint + 3600000
@@ -9200,16 +10972,342 @@ var HostingControl = class {
9200
10972
  await this.pool.query("DELETE FROM sdk_hosting_nonces WHERE created_at_ms < $1", [Date.now() - 6e5]);
9201
10973
  return result.rowCount === 1;
9202
10974
  }
10975
+ async authorizeClosure(command, signature) {
10976
+ if (command?.version !== 3 || command.action !== "close" && command.action !== "certificate" || command.scope !== this.config.scope || command.dvmId !== this.dvmId || !Number.isSafeInteger(command.generation) || command.generation < 1 || typeof command.operationId !== "string" || !/^[A-Za-z0-9_-]{16,128}$/.test(command.operationId) || !Number.isSafeInteger(command.timestampMs) || Math.abs(Date.now() - command.timestampMs) > 3e5 || typeof command.nonce !== "string" || !/^[A-Za-z0-9_-]{16,128}$/.test(command.nonce) || !signature || !/^[0-9a-f]{64}$/i.test(signature)) return false;
10977
+ if (!timingSafeEqual(
10978
+ Buffer.from(signature, "hex"),
10979
+ Buffer.from(signHostingClosure(this.config.secret, command), "hex")
10980
+ )) return false;
10981
+ const inserted = await this.pool.query(
10982
+ `INSERT INTO sdk_hosting_nonces (dvm_id, scope, nonce, created_at_ms)
10983
+ VALUES ($1,$2,$3,$4) ON CONFLICT DO NOTHING`,
10984
+ [this.dvmId, this.config.scope, command.nonce, Date.now()]
10985
+ );
10986
+ return inserted.rowCount === 1;
10987
+ }
10988
+ async close(command, support) {
10989
+ if (command.action !== "close") return "conflict";
10990
+ if (!Object.values(support).every(Boolean)) return "conflict";
10991
+ const client = await this.pool.connect();
10992
+ try {
10993
+ await client.query("BEGIN");
10994
+ const locked = await client.query(
10995
+ `SELECT state, generation, deadline_ms, closed_operation_id FROM sdk_hosting_control
10996
+ WHERE dvm_id=$1 AND scope=$2 FOR UPDATE`,
10997
+ [this.dvmId, this.config.scope]
10998
+ );
10999
+ const prior = locked.rows[0];
11000
+ if (!prior) throw new Error("Hosting control identity is unavailable");
11001
+ if (prior.state === "closed") {
11002
+ await client.query("COMMIT");
11003
+ return Number(prior.generation) === command.generation && prior.closed_operation_id === command.operationId ? "ok" : "conflict";
11004
+ }
11005
+ if (command.generation <= Number(prior.generation)) {
11006
+ await client.query("ROLLBACK");
11007
+ return "conflict";
11008
+ }
11009
+ const now = await this.databaseNow(client);
11010
+ const deadline = prior.deadline_ms === null ? now + 36e5 : Math.min(Number(prior.deadline_ms), now + 36e5);
11011
+ await client.query(
11012
+ `UPDATE sdk_hosting_control
11013
+ SET state='closed', generation=$3, closed_operation_id=$4,
11014
+ closed_at_ms=FLOOR(EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint,
11015
+ closed_at_us=FLOOR(EXTRACT(EPOCH FROM clock_timestamp()) * 1000000)::bigint,
11016
+ cutoff_txid=txid_current(), channel_evidence=NULL,
11017
+ lease_expires_at_ms=NULL, deadline_ms=$5
11018
+ WHERE dvm_id=$1 AND scope=$2`,
11019
+ [this.dvmId, this.config.scope, command.generation, command.operationId, deadline]
11020
+ );
11021
+ await this.pinAdmissionDeadlines(client, deadline);
11022
+ await client.query("COMMIT");
11023
+ return "ok";
11024
+ } catch (error) {
11025
+ await client.query("ROLLBACK").catch(() => void 0);
11026
+ throw error;
11027
+ } finally {
11028
+ client.release();
11029
+ }
11030
+ }
11031
+ async certificate(command, support) {
11032
+ if (command.action !== "certificate" || this.bootEpoch === void 0) return void 0;
11033
+ const observedClosure = await this.pool.query(`SELECT state,generation,closed_operation_id,boot_epoch,cutoff_txid,instance_id
11034
+ FROM sdk_hosting_control WHERE dvm_id=$1 AND scope=$2`, [this.dvmId, this.config.scope]);
11035
+ const observed = observedClosure.rows[0];
11036
+ if (observed?.state !== "closed" || observed.closed_operation_id !== command.operationId || Number(observed.generation) !== command.generation || Number(observed.boot_epoch) !== this.bootEpoch || observed.cutoff_txid === null || observed.instance_id === null) return void 0;
11037
+ const channelObservation = this.channelReader ? await this.channelReader.observe({
11038
+ dvmId: this.dvmId,
11039
+ scope: this.config.scope,
11040
+ instanceId: observed.instance_id
11041
+ }).catch(() => void 0) : void 0;
11042
+ const client = await this.pool.connect();
11043
+ try {
11044
+ await client.query("BEGIN");
11045
+ const locked = await client.query(
11046
+ `SELECT state,generation,instance_id,boot_epoch,closed_operation_id,closed_at_ms,
11047
+ closed_at_us,cutoff_txid
11048
+ FROM sdk_hosting_control WHERE dvm_id=$1 AND scope=$2 FOR UPDATE`,
11049
+ [this.dvmId, this.config.scope]
11050
+ );
11051
+ const row = locked.rows[0];
11052
+ if (!row || row.state !== "closed" || row.closed_operation_id !== command.operationId || Number(row.generation) !== command.generation || Number(row.boot_epoch) !== this.bootEpoch || row.instance_id === null || row.instance_id !== observed.instance_id || row.closed_at_ms === null || row.closed_at_us === null || row.cutoff_txid === null || row.cutoff_txid !== observed.cutoff_txid) {
11053
+ await client.query("ROLLBACK");
11054
+ return void 0;
11055
+ }
11056
+ const reasons = [];
11057
+ if (!support.singleMount) reasons.push("multi_mount_ledger_unowned");
11058
+ if (!support.durableLedger) reasons.push("credit_ledger_unavailable");
11059
+ if (!support.durableJobStore) reasons.push("job_store_unavailable");
11060
+ if (!support.durableReporter) reasons.push("reporter_unavailable");
11061
+ if (!support.durablePaymentReplay) reasons.push("payment_replay_unavailable");
11062
+ if (!support.knownRailStores) reasons.push("rail_store_unsupported");
11063
+ const tables = [
11064
+ "jobs",
11065
+ "job_messages",
11066
+ "request_id_claims",
11067
+ "sdk_hosting_admissions",
11068
+ "credits",
11069
+ "credit_draws",
11070
+ "credit_drains",
11071
+ "credit_invoices",
11072
+ "credit_fundings",
11073
+ "pending_dvm_reports",
11074
+ "payout_x402_batches",
11075
+ "x402_exact_settlement_intents",
11076
+ "x402_batch_settlements",
11077
+ "x402_batch_settlement_state",
11078
+ "x402_batch_channels",
11079
+ "mpp_tempo_channels",
11080
+ "mpp_tempo_close_intents",
11081
+ "tempo_settlement_readiness",
11082
+ "mpp_tempo_charge_replays",
11083
+ "sdk_hosted_tempo_charge_intents",
11084
+ "sdk_hosted_settlement_report_history",
11085
+ "sdk_hosted_settlement_report_obligations"
11086
+ ];
11087
+ const present = await client.query(
11088
+ `SELECT name, to_regclass(name) IS NOT NULL AS exists FROM unnest($1::text[]) AS name`,
11089
+ [tables]
11090
+ );
11091
+ const exists = new Set(present.rows.filter((table) => table.exists).map((table) => table.name));
11092
+ for (const required of [
11093
+ "jobs",
11094
+ "job_messages",
11095
+ "request_id_claims",
11096
+ "sdk_hosting_admissions",
11097
+ "credits",
11098
+ "credit_draws",
11099
+ "credit_drains",
11100
+ "credit_invoices",
11101
+ "credit_fundings",
11102
+ "pending_dvm_reports",
11103
+ "payout_x402_batches",
11104
+ "sdk_hosted_settlement_report_history",
11105
+ "sdk_hosted_settlement_report_obligations"
11106
+ ]) {
11107
+ if (!exists.has(required)) reasons.push(`missing_${required}`);
11108
+ }
11109
+ const count = async (table, predicate, params = []) => {
11110
+ if (!exists.has(table)) return 0;
11111
+ const result = await client.query(
11112
+ `SELECT COUNT(*)::text AS n FROM ${table} WHERE ${predicate}`,
11113
+ params
11114
+ );
11115
+ return Number(result.rows[0].n);
11116
+ };
11117
+ if (await count(
11118
+ "sdk_hosting_admissions",
11119
+ "dvm_id=$1 AND scope=$2 AND outcome IN ('pending','unresolved')",
11120
+ [this.dvmId, this.config.scope]
11121
+ )) reasons.push("admissions_unresolved");
11122
+ if (await count(
11123
+ "sdk_hosting_admissions",
11124
+ "dvm_id<>$1 OR scope<>$2",
11125
+ [this.dvmId, this.config.scope]
11126
+ )) reasons.push("historical_admission_owner_unknown");
11127
+ if (exists.has("sdk_hosting_admissions") && exists.has("credit_fundings")) {
11128
+ const unbooked = await client.query(
11129
+ `SELECT COUNT(*)::text AS n FROM sdk_hosting_admissions a
11130
+ WHERE a.dvm_id=$1 AND a.scope=$2 AND a.outcome='accepted'
11131
+ AND a.credit_id IS NOT NULL AND a.fund_id IS NOT NULL
11132
+ AND NOT EXISTS (SELECT 1 FROM credit_fundings f
11133
+ WHERE f.credit_id=a.credit_id AND f.fund_id=a.fund_id)`,
11134
+ [this.dvmId, this.config.scope]
11135
+ );
11136
+ if (Number(unbooked.rows[0].n)) reasons.push("funding_intents_unbooked");
11137
+ }
11138
+ if (await count(
11139
+ "jobs",
11140
+ "(dvm_id=$1 OR dvm_id IS NULL) AND status NOT IN ('completed','failed','cancelled')",
11141
+ [this.dvmId]
11142
+ )) reasons.push("jobs_in_flight_or_unowned");
11143
+ if (await count("jobs", "dvm_id IS DISTINCT FROM $1", [this.dvmId])) {
11144
+ reasons.push("historical_job_owner_unknown");
11145
+ }
11146
+ if (await count(
11147
+ "jobs",
11148
+ "dvm_id=$1 AND status IN ('completed','failed','cancelled') AND terminal_report_enqueued IS DISTINCT FROM TRUE",
11149
+ [this.dvmId]
11150
+ )) reasons.push("terminal_reports_unenqueued");
11151
+ if (await count("job_messages", "status IN ('pending-verification','failed-verify') AND data->>'type'='payment'")) {
11152
+ reasons.push("inbound_payments_unresolved");
11153
+ }
11154
+ if (exists.has("request_id_claims") && exists.has("jobs")) {
11155
+ const claims = await client.query(
11156
+ `SELECT COUNT(*)::text AS n FROM request_id_claims c
11157
+ LEFT JOIN jobs j ON j.id=c.job_id WHERE j.id IS NULL`
11158
+ );
11159
+ if (Number(claims.rows[0].n)) reasons.push("request_claims_unowned");
11160
+ }
11161
+ if (await count("credit_draws", "status='pending'")) reasons.push("draws_pending");
11162
+ if (await count("credit_invoices", "status NOT IN ('settled','reconciled')")) {
11163
+ reasons.push("invoices_unresolved");
11164
+ }
11165
+ if (await count("credit_invoices", "hosting_admission_id IS NULL")) {
11166
+ reasons.push("historical_invoice_owner_unknown");
11167
+ }
11168
+ if (await count(
11169
+ "credit_fundings",
11170
+ "caller_pubkey IS NULL OR balance_after_micro IS NULL OR rail IS NULL"
11171
+ )) {
11172
+ reasons.push("funding_facts_incomplete");
11173
+ }
11174
+ if (await count("pending_dvm_reports", "TRUE")) reasons.push("reports_pending_or_unowned");
11175
+ if (exists.has("sdk_hosted_settlement_report_history")) {
11176
+ const history = await client.query(
11177
+ "SELECT complete FROM sdk_hosted_settlement_report_history WHERE singleton=TRUE"
11178
+ );
11179
+ if (history.rows[0]?.complete !== true) reasons.push("historical_settlement_report_provenance_unknown");
11180
+ }
11181
+ if (await count(
11182
+ "sdk_hosted_settlement_report_obligations",
11183
+ HOSTED_SETTLEMENT_UNRESOLVED_SQL
11184
+ )) {
11185
+ reasons.push("settlement_reports_unresolved");
11186
+ }
11187
+ if (await count(
11188
+ "sdk_hosted_settlement_report_obligations",
11189
+ "dvm_id<>$1 OR scope<>$2 OR instance_id<>$3",
11190
+ [this.dvmId, this.config.scope, row.instance_id]
11191
+ )) {
11192
+ reasons.push("historical_settlement_owner_unknown");
11193
+ }
11194
+ if (await count("payout_x402_batches", "TRUE")) reasons.push("x402_payout_batches_pending");
11195
+ if (await count("x402_exact_settlement_intents", "status NOT IN ('complete','rejected')")) {
11196
+ reasons.push("x402_exact_unresolved");
11197
+ }
11198
+ if (await count("x402_batch_settlements", "status <> 'complete'")) {
11199
+ reasons.push("x402_batch_unresolved");
11200
+ }
11201
+ if (exists.has("x402_batch_channels") && !exists.has("x402_batch_settlement_state")) {
11202
+ reasons.push("x402_payout_transfer_state_missing");
11203
+ }
11204
+ if (await count("x402_batch_settlement_state", "pending_settle=TRUE")) {
11205
+ reasons.push("x402_payout_transfer_pending");
11206
+ }
11207
+ const channelSnapshot = await readHostedChannelSnapshot(client);
11208
+ if (this.channelReader && !channelObservation) reasons.push("channel_evidence_unavailable");
11209
+ if (channelSnapshot.present.includes("x402_batch_channels") && channelSnapshot.x402HistoryComplete !== true) reasons.push("x402_channel_history_incomplete");
11210
+ if (channelSnapshot.present.includes("mpp_tempo_channels") && channelSnapshot.tempoHistoryComplete !== true) reasons.push("tempo_channel_history_incomplete");
11211
+ const observationCurrent = channelObservation?.digest === channelSnapshot.digest;
11212
+ const x402FactCount = channelSnapshot.x402.length + channelSnapshot.x402Tombstones.length;
11213
+ if (x402FactCount && (!observationCurrent || !channelObservation?.x402 || channelObservation.x402.channels.length !== x402FactCount)) {
11214
+ reasons.push("x402_channel_evidence_unresolved");
11215
+ }
11216
+ const tempoFactCount = new Set([...channelSnapshot.tempo, ...channelSnapshot.tempoTombstones].map((channel) => channel.channel_key)).size;
11217
+ if ((tempoFactCount || channelSnapshot.tempoCloses.length) && (!observationCurrent || !channelObservation?.tempo || channelObservation.tempo.channels.length !== tempoFactCount)) {
11218
+ reasons.push("tempo_channel_evidence_unresolved");
11219
+ }
11220
+ if (channelObservation && !observationCurrent) reasons.push("channel_evidence_stale");
11221
+ if (channelObservation?.reasons.length) reasons.push(...channelObservation.reasons);
11222
+ if (await count("tempo_settlement_readiness", "failure_latched=TRUE")) {
11223
+ reasons.push("tempo_settlement_unresolved");
11224
+ }
11225
+ if (support.knownRailStores && !exists.has("sdk_hosted_tempo_charge_intents") && exists.has("mpp_tempo_charge_replays")) reasons.push("tempo_charge_intents_missing");
11226
+ if (await count("sdk_hosted_tempo_charge_intents", "status <> 'complete'")) {
11227
+ reasons.push("tempo_charge_unresolved");
11228
+ }
11229
+ if (await count("mpp_tempo_charge_replays", "TRUE")) {
11230
+ reasons.push("tempo_charge_store_not_quiescent");
11231
+ }
11232
+ const liabilities = [];
11233
+ if (exists.has("credits") && exists.has("credit_drains")) {
11234
+ const balances = await client.query(
11235
+ `SELECT currency, SUM(amount_micro)::text AS amount_micro FROM (
11236
+ SELECT currency, balance_micro AS amount_micro FROM credits WHERE balance_micro > 0
11237
+ UNION ALL
11238
+ SELECT c.currency, d.amount_micro FROM credit_drains d
11239
+ JOIN credits c ON c.credit_id=d.credit_id WHERE d.status <> 'sent' AND d.status <> 'released'
11240
+ ) owed GROUP BY currency ORDER BY currency`
11241
+ );
11242
+ liabilities.push(...balances.rows.map((balance) => ({
11243
+ currency: balance.currency,
11244
+ amountMicro: balance.amount_micro
11245
+ })));
11246
+ if (await count("credit_drains", "status <> 'sent' AND status <> 'released' AND owed_sats IS NULL")) {
11247
+ reasons.push("drain_value_unpriced");
11248
+ }
11249
+ const orphan = await client.query(
11250
+ `SELECT COUNT(*)::text AS n FROM credit_drains d
11251
+ LEFT JOIN credits c ON c.credit_id=d.credit_id WHERE c.credit_id IS NULL`
11252
+ );
11253
+ if (Number(orphan.rows[0].n)) reasons.push("drain_owner_unknown");
11254
+ }
11255
+ if (liabilities.length) reasons.push("caller_liability_positive");
11256
+ if (reasons.length === 0 && observationCurrent && channelObservation) {
11257
+ const persisted = {
11258
+ dvmId: this.dvmId,
11259
+ scope: this.config.scope,
11260
+ instanceId: row.instance_id,
11261
+ bootEpoch: this.bootEpoch,
11262
+ generation: command.generation,
11263
+ operationId: command.operationId,
11264
+ cutoffTxid: row.cutoff_txid,
11265
+ digest: channelSnapshot.digest,
11266
+ x402: channelObservation.x402,
11267
+ tempo: channelObservation.tempo
11268
+ };
11269
+ const saved = await client.query(
11270
+ `UPDATE sdk_hosting_control SET channel_evidence=$3::jsonb
11271
+ WHERE dvm_id=$1 AND scope=$2 RETURNING channel_evidence`,
11272
+ [this.dvmId, this.config.scope, JSON.stringify(persisted)]
11273
+ );
11274
+ if (saved.rows[0]?.channel_evidence?.digest !== channelSnapshot.digest || saved.rows[0]?.channel_evidence?.operationId !== command.operationId || saved.rows[0]?.channel_evidence?.bootEpoch !== this.bootEpoch) {
11275
+ throw new Error("Hosting channel evidence could not be persisted for this closure");
11276
+ }
11277
+ }
11278
+ await client.query("COMMIT");
11279
+ const complete = reasons.length === 0;
11280
+ return {
11281
+ dvmId: this.dvmId,
11282
+ scope: this.config.scope,
11283
+ instanceId: row.instance_id,
11284
+ bootEpoch: this.bootEpoch,
11285
+ generation: command.generation,
11286
+ operationId: command.operationId,
11287
+ cutoffTxid: row.cutoff_txid,
11288
+ closedAtMs: Number(row.closed_at_ms),
11289
+ complete,
11290
+ eligible: complete,
11291
+ reasons,
11292
+ liabilities
11293
+ };
11294
+ } catch (error) {
11295
+ await client.query("ROLLBACK").catch(() => void 0);
11296
+ throw error;
11297
+ } finally {
11298
+ client.release();
11299
+ }
11300
+ }
9203
11301
  async admit(id, jobId, details) {
9204
11302
  const client = await this.pool.connect();
9205
11303
  try {
9206
11304
  await client.query("BEGIN");
9207
- const { rows } = await client.query(
11305
+ const { rows: rows2 } = await client.query(
9208
11306
  `SELECT state, lease_expires_at_ms, deadline_ms FROM sdk_hosting_control WHERE dvm_id=$1 AND scope=$2 FOR UPDATE`,
9209
11307
  [this.dvmId, this.config.scope]
9210
11308
  );
9211
11309
  const now = await this.databaseNow(client);
9212
- const current = rows[0];
11310
+ const current = rows2[0];
9213
11311
  if (current) await this.expireIfNeeded(client, current, now);
9214
11312
  if (current?.state !== "accepting" || current.lease_expires_at_ms === null || Number(current.lease_expires_at_ms) <= now) {
9215
11313
  await client.query("COMMIT");
@@ -9217,8 +11315,9 @@ var HostingControl = class {
9217
11315
  }
9218
11316
  await client.query(
9219
11317
  `INSERT INTO sdk_hosting_admissions
9220
- (id, dvm_id, scope, job_id, admitted_at_ms, credit_id, fund_id, payment_artifact_hash)
9221
- VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,
11318
+ (id, dvm_id, scope, job_id, admitted_at_ms, credit_id, fund_id,
11319
+ caller_pubkey, funding_commitment, payment_artifact_hash)
11320
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)`,
9222
11321
  [
9223
11322
  id,
9224
11323
  this.dvmId,
@@ -9227,6 +11326,8 @@ var HostingControl = class {
9227
11326
  now,
9228
11327
  details?.creditId ?? null,
9229
11328
  details?.fundId ?? null,
11329
+ details?.callerPubkey ?? null,
11330
+ details?.fundingCommitment ?? null,
9230
11331
  details?.paymentArtifactHash ?? null
9231
11332
  ]
9232
11333
  );
@@ -9239,16 +11340,36 @@ var HostingControl = class {
9239
11340
  client.release();
9240
11341
  }
9241
11342
  }
9242
- async acceptedJobByPaymentArtifactHash(hash2) {
9243
- const { rows } = await this.pool.query(
11343
+ async acceptedJobByPaymentArtifactHash(hash3) {
11344
+ const { rows: rows2 } = await this.pool.query(
9244
11345
  `SELECT a.job_id FROM sdk_hosting_admissions a
9245
11346
  JOIN jobs j ON j.id=a.job_id
9246
11347
  WHERE a.dvm_id=$1 AND a.scope=$2 AND a.payment_artifact_hash=$3
9247
11348
  AND a.outcome IN ('accepted', 'unresolved', 'pending')
9248
11349
  ORDER BY a.admitted_at_ms DESC LIMIT 1`,
9249
- [this.dvmId, this.config.scope, hash2]
11350
+ [this.dvmId, this.config.scope, hash3]
11351
+ );
11352
+ return rows2[0]?.job_id;
11353
+ }
11354
+ async acceptedFundingAdmission(creditId, fundId, callerPubkey, commitment, paymentArtifactHash) {
11355
+ const { rows: rows2 } = await this.pool.query(
11356
+ `SELECT id FROM sdk_hosting_admissions
11357
+ WHERE dvm_id=$1 AND scope=$2 AND credit_id=$3 AND fund_id=$4
11358
+ AND caller_pubkey=$5 AND funding_commitment=$6
11359
+ AND payment_artifact_hash IS NOT DISTINCT FROM $7
11360
+ AND outcome IN ('pending','accepted','unresolved')
11361
+ ORDER BY admitted_at_ms, id LIMIT 1`,
11362
+ [
11363
+ this.dvmId,
11364
+ this.config.scope,
11365
+ creditId,
11366
+ fundId,
11367
+ callerPubkey,
11368
+ commitment,
11369
+ paymentArtifactHash ?? null
11370
+ ]
9250
11371
  );
9251
- return rows[0]?.job_id;
11372
+ return rows2[0]?.id;
9252
11373
  }
9253
11374
  async rebindAdmissionJob(id, jobId) {
9254
11375
  await this.pool.query(
@@ -9264,33 +11385,33 @@ var HostingControl = class {
9264
11385
  );
9265
11386
  }
9266
11387
  async jobDeadline(jobId) {
9267
- const { rows } = await this.pool.query(
11388
+ const { rows: rows2 } = await this.pool.query(
9268
11389
  `SELECT MIN(drain_deadline_ms)::text AS drain_deadline_ms
9269
11390
  FROM sdk_hosting_admissions WHERE dvm_id=$1 AND scope=$2 AND job_id=$3`,
9270
11391
  [this.dvmId, this.config.scope, jobId]
9271
11392
  );
9272
- return rows[0]?.drain_deadline_ms === null || !rows[0] ? null : Number(rows[0].drain_deadline_ms);
11393
+ return rows2[0]?.drain_deadline_ms === null || !rows2[0] ? null : Number(rows2[0].drain_deadline_ms);
9273
11394
  }
9274
11395
  async dispatchDeadlineReached(jobId) {
9275
- const { rows } = await this.pool.query(
11396
+ const { rows: rows2 } = await this.pool.query(
9276
11397
  `SELECT EXISTS (
9277
11398
  SELECT 1 FROM sdk_hosting_admissions
9278
11399
  WHERE dvm_id=$1 AND scope=$2 AND job_id=$3
9279
11400
  AND drain_deadline_ms <= FLOOR(EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint
9280
11401
  ) OR EXISTS (
9281
11402
  SELECT 1 FROM sdk_hosting_control
9282
- WHERE dvm_id=$1 AND scope=$2 AND state='draining'
11403
+ WHERE dvm_id=$1 AND scope=$2 AND state IN ('draining','closed')
9283
11404
  AND deadline_ms <= FLOOR(EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint
9284
11405
  ) AS reached`,
9285
11406
  [this.dvmId, this.config.scope, jobId]
9286
11407
  );
9287
- return rows[0]?.reached ?? false;
11408
+ return rows2[0]?.reached ?? false;
9288
11409
  }
9289
11410
  async databaseNow(client) {
9290
- const { rows } = await client.query(
11411
+ const { rows: rows2 } = await client.query(
9291
11412
  `SELECT FLOOR(EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint AS now_ms`
9292
11413
  );
9293
- return Number(rows[0].now_ms);
11414
+ return Number(rows2[0].now_ms);
9294
11415
  }
9295
11416
  async pinAdmissionDeadlines(client, deadlineMs) {
9296
11417
  await client.query(
@@ -9317,13 +11438,17 @@ var HostingControl = class {
9317
11438
  const client = await this.pool.connect();
9318
11439
  try {
9319
11440
  await client.query("BEGIN");
9320
- const { rows } = await client.query(
11441
+ const { rows: rows2 } = await client.query(
9321
11442
  `SELECT state, generation, deadline_ms, lease_expires_at_ms FROM sdk_hosting_control
9322
11443
  WHERE dvm_id=$1 AND scope=$2 FOR UPDATE`,
9323
11444
  [this.dvmId, this.config.scope]
9324
11445
  );
9325
- const prior = rows[0];
11446
+ const prior = rows2[0];
9326
11447
  if (!prior) throw new Error("Hosting control was not initialized");
11448
+ if (prior.state === "closed") {
11449
+ await client.query("ROLLBACK");
11450
+ return "conflict";
11451
+ }
9327
11452
  const now = await this.databaseNow(client);
9328
11453
  await this.expireIfNeeded(client, prior, now);
9329
11454
  const generation = Number(prior.generation);
@@ -9404,7 +11529,7 @@ var HostingControl = class {
9404
11529
  ))` : ""})`,
9405
11530
  [this.dvmId, this.config.scope]
9406
11531
  );
9407
- const { rows } = await this.pool.query(
11532
+ const { rows: rows2 } = await this.pool.query(
9408
11533
  `SELECT state, generation, deadline_ms, lease_expires_at_ms FROM sdk_hosting_control WHERE dvm_id=$1 AND scope=$2`,
9409
11534
  [this.dvmId, this.config.scope]
9410
11535
  );
@@ -9441,10 +11566,10 @@ var HostingControl = class {
9441
11566
  [this.dvmId, this.config.scope]
9442
11567
  );
9443
11568
  return {
9444
- state: rows[0].state,
9445
- generation: Number(rows[0].generation),
9446
- deadlineMs: rows[0].deadline_ms === null ? null : Number(rows[0].deadline_ms),
9447
- leaseExpiresAtMs: rows[0].lease_expires_at_ms === null ? null : Number(rows[0].lease_expires_at_ms),
11569
+ state: rows2[0].state,
11570
+ generation: Number(rows2[0].generation),
11571
+ deadlineMs: rows2[0].deadline_ms === null ? null : Number(rows2[0].deadline_ms),
11572
+ leaseExpiresAtMs: rows2[0].lease_expires_at_ms === null ? null : Number(rows2[0].lease_expires_at_ms),
9448
11573
  activeJobIds: active.rows.map((row) => row.id),
9449
11574
  unownedActiveJobIds: unowned.rows.map((row) => row.id),
9450
11575
  pendingAdmissionCount: Number(pending.rows[0].pending),
@@ -9517,9 +11642,9 @@ import { AsyncLocalStorage } from "async_hooks";
9517
11642
  import { randomBytes as randomBytes3 } from "crypto";
9518
11643
 
9519
11644
  // src/sdk/server/instrumented-kv.ts
9520
- import { createHash as createHash5 } from "crypto";
11645
+ import { createHash as createHash7 } from "crypto";
9521
11646
  function hashKey(key) {
9522
- return createHash5("sha256").update(key).digest("hex").slice(0, 16);
11647
+ return createHash7("sha256").update(key).digest("hex").slice(0, 16);
9523
11648
  }
9524
11649
  function instrumentKVStore(inner) {
9525
11650
  return {
@@ -10090,7 +12215,7 @@ var ReceiptIssuer = class {
10090
12215
  * only for a resolved ledger draw.
10091
12216
  */
10092
12217
  issue(record, seq, issuedAt, credit) {
10093
- const unsigned = {
12218
+ const unsigned2 = {
10094
12219
  v: 1,
10095
12220
  job_id: record.id,
10096
12221
  dvm_id: this.dvmId,
@@ -10121,7 +12246,7 @@ var ReceiptIssuer = class {
10121
12246
  receipt_pubkey: this.pubkey,
10122
12247
  ...credit && { credit }
10123
12248
  };
10124
- return signReceipt(unsigned, this.secret);
12249
+ return signReceipt(unsigned2, this.secret);
10125
12250
  }
10126
12251
  /**
10127
12252
  * Countersign one reclaim event. Unlike job receipts there is
@@ -10256,11 +12381,11 @@ function startAccumulatorMonitor(opts) {
10256
12381
  async function runTick(opts) {
10257
12382
  const client = await opts.pool.connect();
10258
12383
  try {
10259
- const { rows } = await client.query(
12384
+ const { rows: rows2 } = await client.query(
10260
12385
  `SELECT pg_try_advisory_lock($1) AS acquired`,
10261
12386
  [opts.lockKey.toString()]
10262
12387
  );
10263
- if (!rows[0]?.acquired) {
12388
+ if (!rows2[0]?.acquired) {
10264
12389
  return;
10265
12390
  }
10266
12391
  try {
@@ -10370,15 +12495,15 @@ function emitExpiringSoon(dvmId, batch, timeToExpireMs) {
10370
12495
  );
10371
12496
  }
10372
12497
  async function reapExpiredKeysetRows(opts, db) {
10373
- const { rows } = await db.query(
12498
+ const { rows: rows2 } = await db.query(
10374
12499
  `SELECT DISTINCT mint_url, proof_keyset_id
10375
12500
  FROM wallet_accumulator
10376
12501
  WHERE dvm_id = $1`,
10377
12502
  [opts.dvmId]
10378
12503
  );
10379
- if (rows.length === 0) return;
12504
+ if (rows2.length === 0) return;
10380
12505
  const nowSec = Math.floor(opts.now() / 1e3);
10381
- for (const { mint_url, proof_keyset_id } of rows) {
12506
+ for (const { mint_url, proof_keyset_id } of rows2) {
10382
12507
  const finalExpiry = await getKeysetFinalExpiry(mint_url, proof_keyset_id);
10383
12508
  if (finalExpiry === void 0 || finalExpiry > nowSec) continue;
10384
12509
  const { rowCount } = await db.query(
@@ -12160,6 +14285,7 @@ var JobManager = class {
12160
14285
  processedPayments: this.opts.processedPayments,
12161
14286
  enqueueCreditDeposit: this.opts.enqueueCreditDeposit,
12162
14287
  jobId: job.id,
14288
+ hostingPaymentSeq: inboundSeq ?? void 0,
12163
14289
  creditTtlMs: this.opts.creditTtlMs
12164
14290
  },
12165
14291
  {
@@ -13545,12 +15671,16 @@ function tempoCloseEventStatus(result) {
13545
15671
  }
13546
15672
 
13547
15673
  // src/sdk/server/x402-exact-settlement-store.ts
13548
- import { randomUUID as randomUUID3 } from "crypto";
15674
+ import { randomUUID as randomUUID5 } from "crypto";
13549
15675
  var PostgresX402ExactSettlementStore = class {
13550
15676
  constructor(pool) {
13551
15677
  this.pool = pool;
13552
15678
  }
13553
15679
  pool;
15680
+ hostingCleanupFence;
15681
+ useHostingCleanupFence(fence) {
15682
+ this.hostingCleanupFence = fence;
15683
+ }
13554
15684
  async init() {
13555
15685
  await withSdkInitLock(this.pool, async () => {
13556
15686
  await this.pool.query(`
@@ -13610,49 +15740,65 @@ var PostgresX402ExactSettlementStore = class {
13610
15740
  if (existing && !sameIntentBinding(existing, intent)) {
13611
15741
  throw new X402ExactIntentConflictError(intent.id);
13612
15742
  }
13613
- const authFingerprint = intent.effect?.jobAdmission?.authFingerprint;
13614
- if (authFingerprint) {
13615
- await this.pool.query(
13616
- `INSERT INTO x402_exact_job_auth_bindings (dvm_id, auth_fingerprint, intent_id)
15743
+ const write = async (q) => {
15744
+ const authFingerprint = intent.effect?.jobAdmission?.authFingerprint;
15745
+ if (authFingerprint) {
15746
+ await q.query(
15747
+ `INSERT INTO x402_exact_job_auth_bindings (dvm_id, auth_fingerprint, intent_id)
13617
15748
  VALUES ($1,$2,$3) ON CONFLICT DO NOTHING`,
13618
- [intent.dvmId, authFingerprint, intent.id]
13619
- );
13620
- const { rows } = await this.pool.query(
13621
- `SELECT intent_id FROM x402_exact_job_auth_bindings
15749
+ [intent.dvmId, authFingerprint, intent.id]
15750
+ );
15751
+ const { rows: rows2 } = await q.query(
15752
+ `SELECT intent_id FROM x402_exact_job_auth_bindings
13622
15753
  WHERE dvm_id = $1 AND auth_fingerprint = $2`,
13623
- [intent.dvmId, authFingerprint]
13624
- );
13625
- if (rows[0]?.intent_id !== intent.id) throw new X402ExactIntentConflictError(intent.id);
13626
- }
13627
- if (existing) return existing;
13628
- await this.pool.query(
13629
- `INSERT INTO x402_exact_settlement_intents
15754
+ [intent.dvmId, authFingerprint]
15755
+ );
15756
+ if (rows2[0]?.intent_id !== intent.id) throw new X402ExactIntentConflictError(intent.id);
15757
+ }
15758
+ if (existing) return;
15759
+ await q.query(
15760
+ `INSERT INTO x402_exact_settlement_intents
13630
15761
  (dvm_id, intent_id, resource, network, asset, authorizer, recipient, value,
13631
15762
  valid_after, valid_before, nonce, fingerprint, status, effect, submission_block,
13632
15763
  transaction, settle_response, last_outcome, created_at, updated_at)
13633
15764
  VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,'prepared',$13,$14,$15,$16,$17,$18,$18)
13634
15765
  ON CONFLICT (dvm_id, intent_id) DO NOTHING`,
13635
- [
13636
- intent.dvmId,
13637
- intent.id,
13638
- intent.resource,
13639
- intent.network,
13640
- intent.asset,
13641
- intent.from,
13642
- intent.to,
13643
- intent.value,
13644
- intent.validAfter,
13645
- intent.validBefore,
13646
- intent.nonce,
13647
- intent.fingerprint,
13648
- intent.effect ? JSON.stringify(intent.effect) : null,
13649
- intent.submissionBlock ?? null,
13650
- intent.transaction ?? null,
13651
- intent.settleResponse ? JSON.stringify(intent.settleResponse) : null,
13652
- intent.lastOutcome ?? null,
13653
- now
13654
- ]
13655
- );
15766
+ [
15767
+ intent.dvmId,
15768
+ intent.id,
15769
+ intent.resource,
15770
+ intent.network,
15771
+ intent.asset,
15772
+ intent.from,
15773
+ intent.to,
15774
+ intent.value,
15775
+ intent.validAfter,
15776
+ intent.validBefore,
15777
+ intent.nonce,
15778
+ intent.fingerprint,
15779
+ intent.effect ? JSON.stringify(intent.effect) : null,
15780
+ intent.submissionBlock ?? null,
15781
+ intent.transaction ?? null,
15782
+ intent.settleResponse ? JSON.stringify(intent.settleResponse) : null,
15783
+ intent.lastOutcome ?? null,
15784
+ now
15785
+ ]
15786
+ );
15787
+ };
15788
+ if (this.hostingCleanupFence && !existing) {
15789
+ const effect = intent.effect;
15790
+ await this.hostingCleanupFence.withNewIntent({
15791
+ admissionId: effect?.hostingAdmissionId,
15792
+ jobId: effect?.jobId,
15793
+ paymentSeq: effect?.hostingPaymentSeq,
15794
+ creditId: effect?.jobId ? void 0 : effect?.fundOnly?.creditId,
15795
+ fundId: effect?.fundOnly?.fundId,
15796
+ callerPubkey: effect?.jobId ? void 0 : effect?.callerId
15797
+ }, write);
15798
+ } else {
15799
+ await write(this.pool);
15800
+ }
15801
+ if (existing) return existing;
13656
15802
  const stored = await this.get(intent.dvmId, intent.id);
13657
15803
  if (!stored) throw new Error(`x402 exact intent ${intent.id} was not persisted`);
13658
15804
  if (!sameIntentBinding(stored, intent)) {
@@ -13661,11 +15807,11 @@ var PostgresX402ExactSettlementStore = class {
13661
15807
  return stored;
13662
15808
  }
13663
15809
  async get(dvmId, intentId) {
13664
- const { rows } = await this.pool.query(
15810
+ const { rows: rows2 } = await this.pool.query(
13665
15811
  `SELECT * FROM x402_exact_settlement_intents WHERE dvm_id = $1 AND intent_id = $2`,
13666
15812
  [dvmId, intentId]
13667
15813
  );
13668
- return rows[0] ? rowToIntent(rows[0]) : void 0;
15814
+ return rows2[0] ? rowToIntent(rows2[0]) : void 0;
13669
15815
  }
13670
15816
  async markSubmitting(args) {
13671
15817
  const now = Date.now();
@@ -13705,7 +15851,7 @@ var PostgresX402ExactSettlementStore = class {
13705
15851
  const client = await this.pool.connect();
13706
15852
  try {
13707
15853
  await client.query("BEGIN");
13708
- const { rows } = await client.query(
15854
+ const { rows: rows2 } = await client.query(
13709
15855
  `UPDATE x402_exact_settlement_intents
13710
15856
  SET status = $3, last_outcome = $4,
13711
15857
  transaction = COALESCE($5, transaction),
@@ -13724,7 +15870,7 @@ var PostgresX402ExactSettlementStore = class {
13724
15870
  now
13725
15871
  ]
13726
15872
  );
13727
- const current = rows.at(0) ?? (await client.query(
15873
+ const current = rows2.at(0) ?? (await client.query(
13728
15874
  `SELECT * FROM x402_exact_settlement_intents
13729
15875
  WHERE dvm_id = $1 AND intent_id = $2 FOR UPDATE`,
13730
15876
  [args.dvmId, args.intentId]
@@ -13752,13 +15898,13 @@ var PostgresX402ExactSettlementStore = class {
13752
15898
  const client = await this.pool.connect();
13753
15899
  try {
13754
15900
  await client.query("BEGIN");
13755
- const { rows } = await client.query(
15901
+ const { rows: rows2 } = await client.query(
13756
15902
  `SELECT * FROM x402_exact_settlement_intents
13757
15903
  WHERE dvm_id = $1 AND intent_id = $2 FOR UPDATE`,
13758
15904
  [dvmId, intentId]
13759
15905
  );
13760
- if (!rows[0]) throw new Error(`x402 exact intent ${intentId} was not found`);
13761
- const intent = rowToIntent(rows[0]);
15906
+ if (!rows2[0]) throw new Error(`x402 exact intent ${intentId} was not found`);
15907
+ const intent = rowToIntent(rows2[0]);
13762
15908
  if (intent.status === "complete" || intent.status === "local_committed") {
13763
15909
  await client.query("COMMIT");
13764
15910
  return { intent, replayed: true };
@@ -13797,14 +15943,14 @@ var PostgresX402ExactSettlementStore = class {
13797
15943
  }
13798
15944
  async markAdmissionComplete(dvmId, intentId) {
13799
15945
  const now = Date.now();
13800
- const { rows } = await this.pool.query(
15946
+ const { rows: rows2 } = await this.pool.query(
13801
15947
  `UPDATE x402_exact_settlement_intents
13802
15948
  SET status = 'complete', resolved_at = $3, updated_at = $3
13803
15949
  WHERE dvm_id = $1 AND intent_id = $2 AND status = 'local_committed'
13804
15950
  RETURNING *`,
13805
15951
  [dvmId, intentId, now]
13806
15952
  );
13807
- const intent = rows[0] ? rowToIntent(rows[0]) : await this.get(dvmId, intentId);
15953
+ const intent = rows2[0] ? rowToIntent(rows2[0]) : await this.get(dvmId, intentId);
13808
15954
  if (!intent) throw new Error(`x402 exact intent ${intentId} was not found`);
13809
15955
  if (intent.status !== "complete") {
13810
15956
  throw new X402ExactSettlementNotReadyError(intentId, intent.status);
@@ -13812,28 +15958,28 @@ var PostgresX402ExactSettlementStore = class {
13812
15958
  return intent;
13813
15959
  }
13814
15960
  async list(args) {
13815
- const { rows } = await this.pool.query(
15961
+ const { rows: rows2 } = await this.pool.query(
13816
15962
  `SELECT * FROM x402_exact_settlement_intents
13817
15963
  WHERE dvm_id = $1 ${args.includeResolved ? "" : "AND status <> 'complete' AND status <> 'rejected'"}
13818
15964
  ORDER BY updated_at ASC LIMIT $2`,
13819
15965
  [args.dvmId, args.limit]
13820
15966
  );
13821
- return rows.map(rowToIntent);
15967
+ return rows2.map(rowToIntent);
13822
15968
  }
13823
15969
  async attempts(dvmId, intentId) {
13824
- const { rows } = await this.pool.query(
15970
+ const { rows: rows2 } = await this.pool.query(
13825
15971
  `SELECT * FROM x402_exact_settlement_attempts
13826
15972
  WHERE dvm_id = $1 AND intent_id = $2 ORDER BY created_at, id`,
13827
15973
  [dvmId, intentId]
13828
15974
  );
13829
- return rows.map(rowToAttempt);
15975
+ return rows2.map(rowToAttempt);
13830
15976
  }
13831
- async insertAttempt(q, dvmId, intentId, outcome, detail, transaction, createdAt) {
15977
+ async insertAttempt(q, dvmId, intentId, outcome, detail, transaction2, createdAt) {
13832
15978
  await q.query(
13833
15979
  `INSERT INTO x402_exact_settlement_attempts
13834
15980
  (id, dvm_id, intent_id, outcome, detail, transaction, created_at)
13835
15981
  VALUES ($1,$2,$3,$4,$5,$6,$7)`,
13836
- [randomUUID3(), dvmId, intentId, outcome, detail ?? null, transaction ?? null, createdAt]
15982
+ [randomUUID5(), dvmId, intentId, outcome, detail ?? null, transaction2 ?? null, createdAt]
13837
15983
  );
13838
15984
  }
13839
15985
  };
@@ -13957,15 +16103,15 @@ var MemoryX402ExactSettlementStore = class {
13957
16103
  attempts(dvmId, intentId) {
13958
16104
  return Promise.resolve(this.evidence.get(intentKey(dvmId, intentId)) ?? []);
13959
16105
  }
13960
- addAttempt(dvmId, intentId, outcome, detail, transaction) {
16106
+ addAttempt(dvmId, intentId, outcome, detail, transaction2) {
13961
16107
  const key = intentKey(dvmId, intentId);
13962
16108
  const attempts = this.evidence.get(key) ?? [];
13963
16109
  attempts.push({
13964
- id: randomUUID3(),
16110
+ id: randomUUID5(),
13965
16111
  intentId,
13966
16112
  outcome,
13967
16113
  detail,
13968
- transaction,
16114
+ transaction: transaction2,
13969
16115
  createdAt: Date.now()
13970
16116
  });
13971
16117
  this.evidence.set(key, attempts);
@@ -14274,7 +16420,7 @@ function withWallClockDeadline(promise, timeoutMs) {
14274
16420
  }
14275
16421
 
14276
16422
  // src/sdk/server/app.ts
14277
- import { createHash as createHash7, randomBytes as randomBytes5, randomUUID as randomUUID4 } from "crypto";
16423
+ import { createHash as createHash9, randomBytes as randomBytes5, randomUUID as randomUUID6 } from "crypto";
14278
16424
  import { Hono } from "hono";
14279
16425
  import { getCookie } from "hono/cookie";
14280
16426
  import { cors } from "hono/cors";
@@ -15138,13 +17284,13 @@ async function handleX402Settlements(c, opts) {
15138
17284
  includeResolved: c.req.query("include_resolved") === "true",
15139
17285
  evidence: c.req.query("evidence") !== "false"
15140
17286
  });
15141
- const rows = await Promise.all(
17287
+ const rows2 = await Promise.all(
15142
17288
  page.settlements.map(
15143
17289
  (row) => settlementBody(row, opts.ledger.getCreditByX402Channel.bind(opts.ledger))
15144
17290
  )
15145
17291
  );
15146
17292
  return c.json({
15147
- settlements: rows,
17293
+ settlements: rows2,
15148
17294
  ...page.nextAfter ? { next_after: encodeX402Cursor(page.nextAfter) } : {}
15149
17295
  });
15150
17296
  }
@@ -15331,14 +17477,14 @@ async function handleTempoDrains(c, opts) {
15331
17477
  includeResolved: c.req.query("include_resolved") === "true"
15332
17478
  });
15333
17479
  const wantsEvidence = c.req.query("evidence") !== "false";
15334
- const rows = [];
17480
+ const rows2 = [];
15335
17481
  for (const drain of drains) {
15336
17482
  const evidence = wantsEvidence && drain.channelId ? await classifyTempoCloseEvidence(rail.rail, drain.channelId) : void 0;
15337
- rows.push(tempoDrainBody(drain, evidence));
17483
+ rows2.push(tempoDrainBody(drain, evidence));
15338
17484
  }
15339
17485
  const last = drains.length === limit ? drains[drains.length - 1] : void 0;
15340
17486
  return c.json({
15341
- drains: rows,
17487
+ drains: rows2,
15342
17488
  ...last ? {
15343
17489
  next_after: encodeTempoCursor({
15344
17490
  createdAt: last.createdAt,
@@ -17063,7 +19209,7 @@ initSchemaForm();
17063
19209
  }
17064
19210
 
17065
19211
  // src/sdk/server/request-fingerprint.ts
17066
- import { createHash as createHash6 } from "crypto";
19212
+ import { createHash as createHash8 } from "crypto";
17067
19213
  function requestFingerprint(req, opts) {
17068
19214
  const canonical = canonicalize({
17069
19215
  capability: req.capability,
@@ -17071,7 +19217,7 @@ function requestFingerprint(req, opts) {
17071
19217
  params: req.params ?? {},
17072
19218
  data: opts.stripAuthEnvelope ? stripEnvelope(req.data) : req.data
17073
19219
  });
17074
- return createHash6("sha256").update(canonical).digest("hex");
19220
+ return createHash8("sha256").update(canonical).digest("hex");
17075
19221
  }
17076
19222
  var ENVELOPE_KEYS = /* @__PURE__ */ new Set(["pubkey", "signature", "timestamp", "nonce", "auth_statement"]);
17077
19223
  function stripEnvelope(data) {
@@ -17113,6 +19259,7 @@ async function createDVMServer(descriptor, opts) {
17113
19259
  let x402BatchSettlement;
17114
19260
  let server;
17115
19261
  let hostingControl;
19262
+ let hostedSettlementReports;
17116
19263
  let shutdownPromise;
17117
19264
  const shutdown = () => shutdownPromise ??= (async () => {
17118
19265
  const errors = [];
@@ -17163,17 +19310,52 @@ async function createDVMServer(descriptor, opts) {
17163
19310
  const platformToken = opts.platformToken ?? envMaybe.DVMKIT_PLATFORM_TOKEN;
17164
19311
  const platformUrl = opts.platformUrl ?? envMaybe.DVMKIT_PLATFORM_URL;
17165
19312
  const dvmId = opts.dvmId;
19313
+ if (opts.hostingControl && opts.db && dvmId && typeof opts.db.connect === "function") {
19314
+ hostedSettlementReports = new HostedSettlementReportObligations(
19315
+ opts.db,
19316
+ dvmId,
19317
+ opts.hostingControl.scope
19318
+ );
19319
+ await hostedSettlementReports.init();
19320
+ if (opts.x402?.batchSettlement) {
19321
+ const network = x402NetworkByCaip2(opts.x402.network);
19322
+ if (!network) throw new Error("Hosted x402 batch network is unsupported");
19323
+ hostedSettlementReports.configureX402({
19324
+ network: opts.x402.network,
19325
+ receiver: opts.x402.payTo,
19326
+ token: network.usdc,
19327
+ ...opts.x402.batchSettlement.rpcUrl ? { rpcUrl: opts.x402.batchSettlement.rpcUrl } : {}
19328
+ });
19329
+ }
19330
+ if (opts.hostingTempoSessionStore && opts.mpp?.tempoConfiguration) {
19331
+ const { Session: TempoSession3 } = await import("mppx/tempo");
19332
+ const tempo = opts.mpp.tempoConfiguration;
19333
+ hostedSettlementReports.configureTempo({
19334
+ network: `eip155:${tempo.chainId}`,
19335
+ escrow: TempoSession3.Precompile.Constants.tip20ChannelEscrow,
19336
+ payee: tempo.recipient,
19337
+ token: tempo.paymentToken,
19338
+ ...envMaybe.DVMKIT_TEMPO_RPC_URL ? { rpcUrl: envMaybe.DVMKIT_TEMPO_RPC_URL } : {}
19339
+ });
19340
+ }
19341
+ }
17166
19342
  if (!opts.onJobCompleted && platformToken && platformUrl && dvmId && opts.db) {
17167
- const { RevenueReporter } = await import("./revenue-reporter-EKJOGQYR.js");
17168
- const reporter = new RevenueReporter(opts.db, platformUrl, platformToken);
19343
+ const { RevenueReporter } = await import("./revenue-reporter-MAERPYGK.js");
19344
+ const reporter = new RevenueReporter(
19345
+ opts.db,
19346
+ platformUrl,
19347
+ platformToken,
19348
+ hostedSettlementReports
19349
+ );
17169
19350
  revenueReporter = reporter;
17170
19351
  await reporter.init();
17171
19352
  reporter.startRetryLoop();
17172
- const { MemoryX402BatchStore, PayoutReporter } = await import("./payout-reporter-ZMOA37GN.js");
19353
+ const { MemoryX402BatchStore, PayoutReporter } = await import("./payout-reporter-3Y2U55ZX.js");
17173
19354
  payoutReporter = new PayoutReporter({
17174
19355
  dvmId,
17175
19356
  db: opts.db,
17176
19357
  transport: reporter,
19358
+ ...hostedSettlementReports ? { hostedObligations: hostedSettlementReports } : {},
17177
19359
  // The same condition the admin routes and the accumulator monitor mount on.
17178
19360
  cashu: opts.cashuMode === "p2pk-accumulator" && !!opts.lockPubkey,
17179
19361
  ...typeof opts.db.connect === "function" ? {} : { batchStore: new MemoryX402BatchStore() }
@@ -17265,6 +19447,7 @@ async function createDVMServer(descriptor, opts) {
17265
19447
  }
17266
19448
  } : x402BatchConfig;
17267
19449
  x402ChannelStorage = opts.db && x402Config && effectiveX402BatchConfig && !effectiveX402BatchConfig.storage ? new PostgresX402ChannelStorage(opts.db, {
19450
+ ...opts.hostingControl ? { hostedNetwork: x402Config.network } : {},
17268
19451
  terminalizeUnbackedCredit: (evidence, tx) => creditLedger.terminalizeX402Credit(evidence, tx)
17269
19452
  }) : void 0;
17270
19453
  if (x402ChannelStorage) {
@@ -17282,7 +19465,8 @@ async function createDVMServer(descriptor, opts) {
17282
19465
  allowInMemory: opts.devMode,
17283
19466
  earnedNative: (channelId) => creditLedger.earnedNativeForX402Channel(channelId),
17284
19467
  supportedResponse: selfRelayEnabled ? void 0 : opts.x402FacilitatorHealth?.supportedResponse?.(),
17285
- payout: payoutReporter?.x402Observer()
19468
+ payout: payoutReporter?.x402Observer(),
19469
+ hostedObligations: hostedSettlementReports
17286
19470
  }) : void 0;
17287
19471
  x402BatchSettlement = x402BatchSettlementFactory && (bootX402Support?.batchSettlement || selfRelayEnabled) ? await x402BatchSettlementFactory() : void 0;
17288
19472
  const x402ExactStore = x402Config && opts.db && typeof opts.db.connect === "function" ? new PostgresX402ExactSettlementStore(opts.db) : void 0;
@@ -17313,12 +19497,45 @@ async function createDVMServer(descriptor, opts) {
17313
19497
  if (descriptor.credit && !opts.creditLedger?.durable) {
17314
19498
  throw new Error("Hosting control with credit requires a durable credit ledger");
17315
19499
  }
17316
- hostingControl = new HostingControl(opts.db, opts.dvmId, opts.hostingControl);
19500
+ const channelReader = await HostedChannelEvidenceReader.fromServerConfig(opts.db, {
19501
+ ...x402BatchConfig && x402ChannelStorage && x402Config ? { x402: {
19502
+ network: x402Config.network,
19503
+ ...x402BatchConfig.rpcUrl ? { rpcUrl: x402BatchConfig.rpcUrl } : {}
19504
+ } } : {},
19505
+ ...opts.hostingTempoSessionStore && opts.mpp ? { tempoEnv: opts.env } : {}
19506
+ });
19507
+ hostingControl = new HostingControl(opts.db, opts.dvmId, opts.hostingControl, channelReader);
17317
19508
  await hostingControl.init();
19509
+ if (opts.hostingCleanupSingleMount ?? true) {
19510
+ if (opts.creditLedger instanceof CreditLedger) {
19511
+ opts.creditLedger.useHostingCleanupFence(hostingControl.cleanupFence);
19512
+ }
19513
+ opts.jobStore.useHostingCleanupFence(hostingControl.cleanupFence);
19514
+ x402ExactStore?.useHostingCleanupFence(hostingControl.cleanupFence);
19515
+ x402ChannelStorage?.useHostingCleanupFence(hostingControl.cleanupFence);
19516
+ opts.hostingTempoSessionStore?.useHostingCleanupFence(hostingControl.cleanupFence);
19517
+ opts.hostingTempoChargeStore?.useHostingCleanupFence(hostingControl.cleanupFence);
19518
+ if (opts.mpp && opts.hostingTempoChargeStore) {
19519
+ installHostedTempoChargeGuard(opts.mpp, opts.hostingTempoChargeStore);
19520
+ }
19521
+ if (opts.mpp && opts.hostingTempoSessionStore && hostedSettlementReports) {
19522
+ installHostedTempoCloseGuard(opts.mpp, hostedSettlementReports);
19523
+ }
19524
+ }
17318
19525
  }
19526
+ const hostingClosureSupport = hostingControl ? {
19527
+ singleMount: opts.hostingCleanupSingleMount ?? true,
19528
+ durableLedger: opts.creditLedger instanceof CreditLedger,
19529
+ durableJobStore: opts.jobStore instanceof PostgresJobStore,
19530
+ durableReporter: !!activeRevenueReporter,
19531
+ durablePaymentReplay: processedPayments instanceof PostgresProcessedPaymentStore,
19532
+ knownRailStores: (opts.hostingCleanupKnownRailStores ?? !opts.mpp) && (!x402Config || !!x402ExactStore && (!x402BatchConfig || !!x402ChannelStorage))
19533
+ } : void 0;
17319
19534
  server = new DVMServer(descriptor, {
17320
19535
  ...opts,
17321
19536
  hostingControlInstance: hostingControl,
19537
+ hostedSettlementReports,
19538
+ hostingClosureSupport,
17322
19539
  creditLedger,
17323
19540
  processedPayments,
17324
19541
  onJobCompleted: effectiveOnJobCompleted,
@@ -17570,13 +19787,36 @@ var DVMServer = class {
17570
19787
  if (!statusOnly) await this.sweepHostingDeadline();
17571
19788
  return c.json(await control.status());
17572
19789
  }
19790
+ async handleHostingClosure(c, certificateOnly) {
19791
+ const control = this.opts.hostingControlInstance;
19792
+ const support = this.opts.hostingClosureSupport;
19793
+ if (!control || !support) return c.json({ error: "not_found" }, 404);
19794
+ let command;
19795
+ try {
19796
+ command = await c.req.json();
19797
+ } catch {
19798
+ return c.json({ error: "invalid_hosting_closure" }, 400);
19799
+ }
19800
+ if (!command || command.action !== (certificateOnly ? "certificate" : "close")) {
19801
+ return c.json({ error: "invalid_hosting_closure" }, 400);
19802
+ }
19803
+ if (!await control.authorizeClosure(command, c.req.header("X-DVM-Hosting-Signature"))) {
19804
+ return c.json({ error: "hosting_closure_unauthorized" }, 401);
19805
+ }
19806
+ if (!certificateOnly && await control.close(command, support) === "conflict") {
19807
+ return c.json({ error: "hosting_closure_conflict" }, 409);
19808
+ }
19809
+ const evidence = await control.certificate({ ...command, action: "certificate" }, support);
19810
+ if (!evidence) return c.json({ error: "hosting_closure_stale" }, 409);
19811
+ return c.json(evidence);
19812
+ }
17573
19813
  registerRoutes() {
17574
19814
  const dvmSlug = this.descriptor.name;
17575
19815
  this.app.use("*", async (c, next) => {
17576
19816
  const raw = c.req.header("traceparent");
17577
19817
  const parsed = raw ? parseTraceparent(raw) : null;
17578
19818
  const traceCtx = {
17579
- traceId: parsed ? parsed.traceId : randomUUID4().replace(/-/g, ""),
19819
+ traceId: parsed ? parsed.traceId : randomUUID6().replace(/-/g, ""),
17580
19820
  spanId: parsed ? parsed.parentSpanId : null,
17581
19821
  dimensions: {},
17582
19822
  service: `isolate-${dvmSlug}`,
@@ -17679,6 +19919,8 @@ var DVMServer = class {
17679
19919
  if (this.opts.hostingControlInstance) {
17680
19920
  this.app.post("/_hosting/control", (c) => this.handleHostingCommand(c, false));
17681
19921
  this.app.post("/_hosting/status", (c) => this.handleHostingCommand(c, true));
19922
+ this.app.post("/_hosting/closure", (c) => this.handleHostingClosure(c, false));
19923
+ this.app.post("/_hosting/closure-certificate", (c) => this.handleHostingClosure(c, true));
17682
19924
  }
17683
19925
  this.app.post("/v1/request-resume/:requestId", (c) => this.handleCreateJob(c, true));
17684
19926
  this.app.post("/v1/request-result/:requestId", (c) => this.handleRequestResult(c));
@@ -17966,7 +20208,7 @@ var DVMServer = class {
17966
20208
  const dvmId = this.opts.dvmId ?? `dev:${this.descriptor.name}`;
17967
20209
  return {
17968
20210
  dvm_id: dvmId,
17969
- builder_pubkey: createHash7("sha256").update(`dvmkit-dev:${dvmId}`).digest("hex")
20211
+ builder_pubkey: createHash9("sha256").update(`dvmkit-dev:${dvmId}`).digest("hex")
17970
20212
  };
17971
20213
  }
17972
20214
  throw new AuthAudienceError();
@@ -18278,15 +20520,22 @@ var DVMServer = class {
18278
20520
  });
18279
20521
  }
18280
20522
  async handleCredit(c) {
20523
+ let hostingFundingAdmissionId;
20524
+ let hostingFundingArtifactHash;
18281
20525
  this.maybeSweepTempoSessions();
18282
20526
  return handleCreditRequest(c, {
20527
+ hostedSettlementReports: this.opts.hostedSettlementReports,
18283
20528
  menu: () => this.creditMenu(this.creditCurrency()),
18284
20529
  creditConfig: this.descriptor.credit,
18285
20530
  auth: this.descriptor.auth,
18286
20531
  authDomain: this.authAudience ? this.signedRequestDomain(c, null) : void 0,
18287
20532
  ledger: this.creditLedger,
18288
20533
  fxFetcher: this.fxFetcher,
18289
- railOpts: (ctx) => this.railVerifyOpts(ctx),
20534
+ railOpts: async (ctx) => ({
20535
+ ...await this.railVerifyOpts(ctx),
20536
+ hostingAdmissionId: hostingFundingAdmissionId,
20537
+ hostingPaymentArtifactHash: hostingFundingArtifactHash
20538
+ }),
18290
20539
  onDeposit: (funding) => {
18291
20540
  this.reportCreditDeposit(funding);
18292
20541
  },
@@ -18300,17 +20549,39 @@ var DVMServer = class {
18300
20549
  settleLightning: (callerPubkey, creditId) => this.settleLightningFunding(callerPubkey, creditId),
18301
20550
  withFundingAdmission: this.opts.hostingControlInstance ? async (ctx, body, run) => {
18302
20551
  const control = this.opts.hostingControlInstance;
18303
- const admissionId = randomUUID4();
20552
+ let admissionId = randomUUID6();
18304
20553
  const fundId = body.fund?.fund_id ?? admissionId;
18305
20554
  const creditId = body.credit_id ?? derivedFundCreditId(body.pubkey, fundId);
18306
20555
  const workId = `credit-fund:${creditId}:${fundId}`;
18307
- if (!await control.admit(admissionId, workId, { creditId, fundId })) {
18308
- return ctx.json({
18309
- error: "hosting_draining",
18310
- message: "This service is draining and is not accepting new credit funding.",
18311
- hint: "Balance, reclaim, and already-issued funding recovery remain available."
18312
- }, 503);
20556
+ const paymentAuthorization = ctx.req.header("Authorization");
20557
+ const mppArtifact = paymentAuthorization ? (await import("mppx")).Credential.extractPaymentScheme(paymentAuthorization) : void 0;
20558
+ const attachedPayment = ctx.req.header("X-Cashu") ?? ctx.req.header("PAYMENT-SIGNATURE") ?? ctx.req.header("X-PAYMENT") ?? mppArtifact;
20559
+ const paymentArtifactHash = attachedPayment ? createHash9("sha256").update(attachedPayment).digest("hex") : void 0;
20560
+ hostingFundingArtifactHash = paymentArtifactHash;
20561
+ if (!await control.admit(admissionId, workId, {
20562
+ creditId,
20563
+ fundId,
20564
+ callerPubkey: body.pubkey,
20565
+ fundingCommitment: body.fund?.commitment,
20566
+ paymentArtifactHash
20567
+ })) {
20568
+ const prior = body.fund?.commitment ? await control.acceptedFundingAdmission(
20569
+ creditId,
20570
+ fundId,
20571
+ body.pubkey,
20572
+ body.fund.commitment,
20573
+ paymentArtifactHash
20574
+ ) : void 0;
20575
+ if (!prior) {
20576
+ return ctx.json({
20577
+ error: "hosting_draining",
20578
+ message: "This service is draining and is not accepting new credit funding.",
20579
+ hint: "Balance, reclaim, and already-issued funding recovery remain available."
20580
+ }, 503);
20581
+ }
20582
+ admissionId = prior;
18313
20583
  }
20584
+ hostingFundingAdmissionId = admissionId;
18314
20585
  const authorization = ctx.req.header("Authorization");
18315
20586
  const paymentArtifact = Boolean(
18316
20587
  ctx.req.header("X-Cashu") || ctx.req.header("PAYMENT-SIGNATURE") || ctx.req.header("X-PAYMENT") || authorization && /^Payment\s/i.test(authorization)
@@ -18989,8 +21260,9 @@ var DVMServer = class {
18989
21260
  { stripAuthEnvelope: false }
18990
21261
  ) : void 0;
18991
21262
  const authorization = c.req.header("Authorization");
18992
- const attachedPayment = c.req.header("X-Cashu") ?? c.req.header("PAYMENT-SIGNATURE") ?? c.req.header("X-PAYMENT") ?? (authorization && /^Payment\s/i.test(authorization) ? authorization : void 0);
18993
- const hostingPaymentArtifactHash = attachedPayment ? createHash7("sha256").update(attachedPayment).digest("hex") : void 0;
21263
+ const mppArtifact = authorization ? (await import("mppx")).Credential.extractPaymentScheme(authorization) : void 0;
21264
+ const attachedPayment = c.req.header("X-Cashu") ?? c.req.header("PAYMENT-SIGNATURE") ?? c.req.header("X-PAYMENT") ?? mppArtifact;
21265
+ const hostingPaymentArtifactHash = attachedPayment ? createHash9("sha256").update(attachedPayment).digest("hex") : void 0;
18994
21266
  if (!resumeOnly && this.opts.hostingControlInstance && hostingPaymentArtifactHash) {
18995
21267
  const priorId = await this.opts.hostingControlInstance.acceptedJobByPaymentArtifactHash(
18996
21268
  hostingPaymentArtifactHash
@@ -19045,7 +21317,7 @@ var DVMServer = class {
19045
21317
  }
19046
21318
  let jobId = publicRequestClaimResult?.jobId ?? this.jobManager.allocateJobId();
19047
21319
  if (!resumeOnly && this.opts.hostingControlInstance) {
19048
- hostingAdmissionId = randomUUID4();
21320
+ hostingAdmissionId = randomUUID6();
19049
21321
  if (!await this.opts.hostingControlInstance.admit(hostingAdmissionId, jobId, {
19050
21322
  paymentArtifactHash: hostingPaymentArtifactHash
19051
21323
  })) {
@@ -19132,6 +21404,8 @@ var DVMServer = class {
19132
21404
  async () => {
19133
21405
  const result = await verifyUpfrontPayment({
19134
21406
  ...railOpts,
21407
+ hostingAdmissionId,
21408
+ hostingPaymentArtifactHash,
19135
21409
  requiredMsats: priceMsats,
19136
21410
  priceFiat,
19137
21411
  jobId,
@@ -19286,7 +21560,7 @@ var DVMServer = class {
19286
21560
  }
19287
21561
  const isAnonymous = requesterId === "anonymous" || requesterId === "dev-anonymous";
19288
21562
  const jobToken = isAnonymous ? randomBytes5(32).toString("hex") : void 0;
19289
- const requesterTokenHash = jobToken ? createHash7("sha256").update(jobToken).digest("hex") : void 0;
21563
+ const requesterTokenHash = jobToken ? createHash9("sha256").update(jobToken).digest("hex") : void 0;
19290
21564
  const job = jm.createJob(
19291
21565
  normalisedBody,
19292
21566
  requesterId,
@@ -19975,7 +22249,7 @@ function isJobOwner(c, job) {
19975
22249
  if (job.requesterTokenHash) {
19976
22250
  const token = c.req.header("X-Job-Token");
19977
22251
  if (!token) return false;
19978
- const provided = createHash7("sha256").update(token).digest("hex");
22252
+ const provided = createHash9("sha256").update(token).digest("hex");
19979
22253
  return timingSafeEqualHex(provided, job.requesterTokenHash);
19980
22254
  }
19981
22255
  return c.get("requesterId") === job.requesterId;
@@ -20478,7 +22752,9 @@ export {
20478
22752
  createAdminCashuNonceStore,
20479
22753
  X402ExactSettlementServer,
20480
22754
  X402ExactSettlementEvidenceMissingError,
22755
+ HostedSettlementReportObligations,
20481
22756
  signHostingControl,
22757
+ signHostingClosure,
20482
22758
  jobCredentialGate,
20483
22759
  REQUEST_ID_FIELD,
20484
22760
  protocolEnvelopeIgnoreFields,