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