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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/dist/{chunk-2KGG5ORW.js → chunk-7ZUKL4IV.js} +3 -3
  2. package/dist/{chunk-KXZUCCEY.js → chunk-A4YFWZ7R.js} +4 -55
  3. package/dist/{chunk-C2AGARLM.js → chunk-B2IROPQF.js} +450 -83
  4. package/dist/{chunk-MKI6OVW4.js → chunk-CZULM42S.js} +0 -51
  5. package/dist/{chunk-UZC4A4DM.js → chunk-EYTHFFFU.js} +2489 -213
  6. package/dist/{chunk-M4QDXD2A.js → chunk-FYRR2UEZ.js} +1 -1
  7. package/dist/{chunk-4UZN2GP5.js → chunk-GAOAYHZA.js} +209 -33
  8. package/dist/{chunk-YYQG4R7T.js → chunk-GFY5FRX3.js} +6 -4
  9. package/dist/{chunk-Z4BNLUZF.js → chunk-IWQHUSMO.js} +3 -105
  10. package/dist/{chunk-4J32DTLT.js → chunk-IXR5RFK7.js} +82 -4
  11. package/dist/{chunk-2SD2W2BG.js → chunk-JH3SX3LY.js} +252 -26
  12. package/dist/chunk-KA6VN5FK.js +245 -0
  13. package/dist/{chunk-YRDA7EWN.js → chunk-LE74UEV7.js} +61 -16
  14. package/dist/chunk-LIRAHP7B.js +53 -0
  15. package/dist/{chunk-MG67KXU7.js → chunk-MZRJR2CW.js} +4 -2
  16. package/dist/chunk-OFVV37XC.js +57 -0
  17. package/dist/{chunk-A4CM2BD3.js → chunk-PAIM66YA.js} +91 -2
  18. package/dist/{chunk-JDFLRNAZ.js → chunk-PB4FYZET.js} +67 -7
  19. package/dist/{chunk-BQ2NMWKE.js → chunk-PLG4XER6.js} +1 -1
  20. package/dist/chunk-RLTSCKRI.js +109 -0
  21. package/dist/{chunk-VAMSJRJT.js → chunk-TUA7PYH7.js} +27 -105
  22. package/dist/{chunk-O3QDFPKM.js → chunk-TZ2WKCDD.js} +1 -1
  23. package/dist/{chunk-Z3EHBW2G.js → chunk-UOCZXO2Z.js} +7 -5
  24. package/dist/{chunk-EAMZZ2B4.js → chunk-X7EZ5CLI.js} +59 -0
  25. package/dist/{chunk-CE7XEHYC.js → chunk-Z4KLECAC.js} +1 -1
  26. package/dist/{chunk-7X6UDVCQ.js → chunk-Z4ZT5LNT.js} +66 -2
  27. package/dist/{credit-ledger-A35WH2TI.js → credit-ledger-MI2H7NEC.js} +3 -1
  28. package/dist/{credit-menu-0-UtJ3hh.d.ts → credit-menu-CAfqe8IY.d.ts} +133 -7
  29. package/dist/{fx-DhyT1Cjh.d.ts → fx-D9nsN3n4.d.ts} +1 -1
  30. package/dist/index.d.ts +6 -6
  31. package/dist/index.js +4 -3
  32. package/dist/internal/caller.d.ts +80 -13
  33. package/dist/internal/caller.js +56 -54
  34. package/dist/internal/index.js +1 -1
  35. package/dist/internal/server.d.ts +11 -85
  36. package/dist/internal/server.js +21 -18
  37. package/dist/{job-store-D3CcZp_g.d.ts → job-store-Bx-VFRjg.d.ts} +1 -1
  38. package/dist/{lightning-backend-BhsyrBLa.d.ts → lightning-backend-BAx2Y8pP.d.ts} +1 -1
  39. package/dist/{memory-credit-ledger-2LBZVMKJ.js → memory-credit-ledger-W2BAUYJP.js} +4 -2
  40. package/dist/{mpp-setup-7CGBKNUP.js → mpp-setup-RF5FXGGV.js} +5 -1
  41. package/dist/{payout-reporter-ZMOA37GN.js → payout-reporter-3Y2U55ZX.js} +1 -1
  42. package/dist/{postgres-job-store-MSD2HZOI.js → postgres-job-store-VLXLWXUQ.js} +1 -1
  43. package/dist/{revenue-reporter-EKJOGQYR.js → revenue-reporter-MAERPYGK.js} +1 -1
  44. package/dist/server/index.d.ts +14 -10
  45. package/dist/server/index.js +57 -19
  46. package/dist/{step-cache-v2yhyhSZ.d.ts → step-cache-QG_h5-3G.d.ts} +1769 -1549
  47. package/dist/{tempo-charge-store-4WX7FPOV.js → tempo-charge-store-Q3D7KYMH.js} +1 -1
  48. package/dist/tempo-lifecycle-ZBBLFBMH.js +15 -0
  49. package/dist/{tempo-session-store-LCSDHB7G.js → tempo-session-store-ZRNCOX7P.js} +1 -1
  50. package/dist/{tempo-wallet-RCGBBAJH.js → tempo-wallet-B5XXV3YH.js} +4 -3
  51. package/dist/testing/index.d.ts +3 -3
  52. package/dist/{usd-Cu9G6xv2.d.ts → usd-EwRqCPF-.d.ts} +1 -1
  53. package/dist/{x402-S47Q77UE.js → x402-R6HPBY5D.js} +18 -14
  54. package/package.json +1 -1
  55. package/dist/chunk-YHN3WMMF.js +0 -122
  56. package/dist/tempo-lifecycle-TJ62A7UX.js +0 -13
@@ -4,6 +4,9 @@ import {
4
4
  import {
5
5
  applyRetryToPool
6
6
  } from "./chunk-P6QON7ER.js";
7
+ import {
8
+ x402NetworkByCaip2
9
+ } from "./chunk-RLTSCKRI.js";
7
10
  import {
8
11
  diagnosticConsole
9
12
  } from "./chunk-ETXDTUJO.js";
@@ -62,6 +65,7 @@ function lotSats(lot, micro) {
62
65
  // src/sdk/server/x402-channel-store.ts
63
66
  import { AsyncLocalStorage } from "async_hooks";
64
67
  import { isDeepStrictEqual } from "util";
68
+ import { BATCH_SETTLEMENT_ADDRESS } from "@x402/evm";
65
69
  var X402SettlementRaceError = class extends Error {
66
70
  };
67
71
  var X402RelaySubmissionLockError = class extends Error {
@@ -99,21 +103,45 @@ var PostgresX402ChannelStorage = class {
99
103
  this.pool = pool;
100
104
  this.relayLockTimeoutMs = opts?.relaySubmissionLockTimeoutMs ?? RELAY_SUBMISSION_LOCK_TIMEOUT_MS;
101
105
  this.terminalizeUnbackedCredit = opts?.terminalizeUnbackedCredit;
106
+ this.hostedNetwork = opts?.hostedNetwork;
107
+ if (this.hostedNetwork && !x402NetworkByCaip2(this.hostedNetwork)) {
108
+ throw new Error("Hosted x402 batch channel network is unsupported");
109
+ }
102
110
  if (!Number.isInteger(this.relayLockTimeoutMs) || this.relayLockTimeoutMs <= 0) {
103
111
  throw new Error("relaySubmissionLockTimeoutMs must be a positive integer number of ms");
104
112
  }
105
113
  }
106
114
  pool;
115
+ hostingCleanupFence;
116
+ useHostingCleanupFence(fence) {
117
+ this.hostingCleanupFence = fence;
118
+ }
119
+ async assertHostingFundingReservation(reservation) {
120
+ if (!this.hostingCleanupFence) return;
121
+ if (!reservation) throw new Error("Hosted x402 funding requires an original admission");
122
+ await this.hostingCleanupFence.withNewIntent({
123
+ admissionId: reservation.admissionId,
124
+ creditId: reservation.creditId,
125
+ fundId: reservation.fundId,
126
+ callerPubkey: reservation.callerPubkey,
127
+ fundingCommitment: reservation.commitment
128
+ }, (tx) => this.hostingCleanupFence.assertFundingReservation(tx, reservation));
129
+ }
107
130
  transaction = new AsyncLocalStorage();
131
+ hostedFunding = /* @__PURE__ */ new WeakMap();
108
132
  refundBase = new AsyncLocalStorage();
109
133
  settlementPool;
110
134
  fleetLock = Promise.resolve();
111
135
  fleetLockQueueDepth = 0;
112
136
  relayLockTimeoutMs;
113
137
  terminalizeUnbackedCredit;
138
+ hostedNetwork;
114
139
  async init() {
115
140
  this.settlementPool ??= createSettlementPool(this.pool);
116
141
  await withSdkInitLock(this.pool, async () => {
142
+ const before = await this.pool.query(
143
+ "SELECT to_regclass('x402_batch_channels') IS NOT NULL AS existed"
144
+ );
117
145
  await this.pool.query(`
118
146
  CREATE TABLE IF NOT EXISTS x402_batch_channels (
119
147
  channel_id TEXT PRIMARY KEY,
@@ -127,9 +155,35 @@ var PostgresX402ChannelStorage = class {
127
155
  refund_nonce BIGINT NOT NULL,
128
156
  onchain_synced_at BIGINT,
129
157
  last_request_timestamp BIGINT NOT NULL,
130
- pending_request JSONB
158
+ pending_request JSONB,
159
+ hosting_binding JSONB
160
+ )
161
+ `);
162
+ await this.pool.query(
163
+ `ALTER TABLE x402_batch_channels ADD COLUMN IF NOT EXISTS hosting_binding JSONB`
164
+ );
165
+ await this.pool.query(`
166
+ CREATE TABLE IF NOT EXISTS x402_batch_channel_tombstones (
167
+ channel_id TEXT PRIMARY KEY,
168
+ hosting_binding JSONB,
169
+ charged_cumulative_amount NUMERIC(78,0) NOT NULL,
170
+ signed_max_claimable NUMERIC(78,0) NOT NULL,
171
+ refund_nonce BIGINT NOT NULL,
172
+ pending_request JSONB,
173
+ deleted_at_ms BIGINT NOT NULL
131
174
  )
132
175
  `);
176
+ await this.pool.query(`
177
+ CREATE TABLE IF NOT EXISTS x402_batch_channel_history (
178
+ singleton BOOLEAN PRIMARY KEY CHECK (singleton),
179
+ complete BOOLEAN NOT NULL
180
+ )
181
+ `);
182
+ await this.pool.query(
183
+ `INSERT INTO x402_batch_channel_history (singleton,complete) VALUES (TRUE,$1)
184
+ ON CONFLICT (singleton) DO NOTHING`,
185
+ [!before.rows[0].existed]
186
+ );
133
187
  await this.pool.query(
134
188
  `CREATE INDEX IF NOT EXISTS idx_x402_batch_channels_withdrawal
135
189
  ON x402_batch_channels(withdraw_requested_at)
@@ -379,9 +433,9 @@ var PostgresX402ChannelStorage = class {
379
433
  return existing ? { intent: existing, written: false } : void 0;
380
434
  }
381
435
  /** Persist the reconciliation identity, refreshing a prepared retry's transient reservation. */
382
- async prepareSettlement(intent) {
436
+ async prepareSettlement(intent, hostingFunding) {
383
437
  const settlements = await this.settlementWriter();
384
- await settlements.query(
438
+ const insert = (q) => q.query(
385
439
  `INSERT INTO x402_batch_settlements (
386
440
  settlement_id, channel_id, operation, effect_id, payment_id,
387
441
  payment_payload, payment_requirements, channel_before, pending_id, status
@@ -399,6 +453,27 @@ var PostgresX402ChannelStorage = class {
399
453
  intent.pendingId ?? null
400
454
  ]
401
455
  );
456
+ if (this.hostingCleanupFence && intent.operation === "fund") {
457
+ if (!hostingFunding || intent.effectId !== `${hostingFunding.creditId}:${hostingFunding.fundId}`) {
458
+ throw new Error("Hosted x402 settlement has no matching original funding admission");
459
+ }
460
+ await this.hostingCleanupFence.withNewIntentOn(
461
+ await this.settlementPoolRef(),
462
+ {
463
+ admissionId: hostingFunding.admissionId,
464
+ creditId: hostingFunding.creditId,
465
+ fundId: hostingFunding.fundId,
466
+ callerPubkey: hostingFunding.callerPubkey,
467
+ fundingCommitment: hostingFunding.commitment
468
+ },
469
+ async (tx) => {
470
+ await this.hostingCleanupFence.assertFundingReservation(tx, hostingFunding);
471
+ await insert(tx);
472
+ }
473
+ );
474
+ } else {
475
+ await insert(settlements);
476
+ }
402
477
  const { rows } = await settlements.query(
403
478
  `SELECT * FROM x402_batch_settlements
404
479
  WHERE settlement_id = $1
@@ -597,10 +672,14 @@ var PostgresX402ChannelStorage = class {
597
672
  withSanctionedRefundBase(base, operation) {
598
673
  return this.refundBase.run(base, operation);
599
674
  }
600
- async withChannelTransaction(channelId, operation) {
675
+ async withChannelTransaction(channelId, operation, hostingFunding) {
601
676
  const client = await this.pool.connect();
602
677
  try {
603
678
  await client.query("BEGIN");
679
+ if (this.hostingCleanupFence && hostingFunding) {
680
+ await this.hostingCleanupFence.assertFundingReservation(client, hostingFunding);
681
+ this.hostedFunding.set(client, hostingFunding);
682
+ }
604
683
  await lockChannel(client, channelId);
605
684
  const result = await this.transaction.run(client, () => operation(client));
606
685
  await client.query("COMMIT");
@@ -608,6 +687,22 @@ var PostgresX402ChannelStorage = class {
608
687
  } catch (error) {
609
688
  await client.query("ROLLBACK").catch(() => void 0);
610
689
  throw error;
690
+ } finally {
691
+ this.hostedFunding.delete(client);
692
+ client.release();
693
+ }
694
+ }
695
+ /** Durable effect bookkeeping reached while a caller already holds a host-pool client. */
696
+ async withSettlementTransaction(operation) {
697
+ const client = await (await this.settlementPoolRef()).connect();
698
+ try {
699
+ await client.query("BEGIN");
700
+ const result = await operation(client);
701
+ await client.query("COMMIT");
702
+ return result;
703
+ } catch (error) {
704
+ await client.query("ROLLBACK").catch(() => void 0);
705
+ throw error;
611
706
  } finally {
612
707
  client.release();
613
708
  }
@@ -758,10 +853,36 @@ var PostgresX402ChannelStorage = class {
758
853
  const next = update(current);
759
854
  if (next === void 0) {
760
855
  if (!current) return { channel: void 0, status: "unchanged" };
856
+ await client.query(
857
+ `INSERT INTO x402_batch_channel_tombstones
858
+ (channel_id,hosting_binding,charged_cumulative_amount,signed_max_claimable,refund_nonce,pending_request,deleted_at_ms)
859
+ VALUES ($1,$2,$3,$4,$5,$6,$7)
860
+ ON CONFLICT (channel_id) DO UPDATE SET
861
+ charged_cumulative_amount=GREATEST(x402_batch_channel_tombstones.charged_cumulative_amount,
862
+ EXCLUDED.charged_cumulative_amount),
863
+ signed_max_claimable=GREATEST(x402_batch_channel_tombstones.signed_max_claimable,
864
+ EXCLUDED.signed_max_claimable),
865
+ refund_nonce=GREATEST(x402_batch_channel_tombstones.refund_nonce,EXCLUDED.refund_nonce),
866
+ pending_request=COALESCE(x402_batch_channel_tombstones.pending_request,EXCLUDED.pending_request)`,
867
+ [
868
+ normalized,
869
+ rows[0].hosting_binding,
870
+ rows[0].charged_cumulative_amount,
871
+ rows[0].signed_max_claimable,
872
+ rows[0].refund_nonce,
873
+ rows[0].pending_request,
874
+ Date.now()
875
+ ]
876
+ );
761
877
  await client.query(`DELETE FROM x402_batch_channels WHERE channel_id = $1`, [normalized]);
762
878
  return { channel: void 0, status: "deleted" };
763
879
  }
764
880
  assertChannelTransition(current, next, normalized, this.refundBase.getStore());
881
+ if (this.hostingCleanupFence && (!current || BigInt(next.chargedCumulativeAmount) > BigInt(current.chargedCumulativeAmount) || BigInt(next.signedMaxClaimable) > BigInt(current.signedMaxClaimable))) {
882
+ const funding = this.hostedFunding.get(client);
883
+ if (funding) await this.hostingCleanupFence.assertFundingReservation(client, funding);
884
+ else await this.hostingCleanupFence.assertNewIntent(client);
885
+ }
765
886
  if (this.terminalizeUnbackedCredit && next.onchainSyncedAt !== void 0 && next.onchainSyncedAt !== current?.onchainSyncedAt && next.withdrawRequestedAt === 0 && BigInt(next.balance) === BigInt(next.totalClaimed)) {
766
887
  await this.terminalizeUnbackedCredit(
767
888
  {
@@ -776,7 +897,17 @@ var PostgresX402ChannelStorage = class {
776
897
  if (current && sameChannel(current, next)) {
777
898
  return { channel: current, status: "unchanged" };
778
899
  }
779
- await writeChannel(client, normalized, next);
900
+ const prior = !current && this.hostedNetwork ? await client.query(
901
+ "SELECT hosting_binding FROM x402_batch_channel_tombstones WHERE channel_id=$1",
902
+ [normalized]
903
+ ) : void 0;
904
+ await writeChannel(
905
+ client,
906
+ normalized,
907
+ next,
908
+ this.hostedNetwork,
909
+ prior?.rows[0] ? prior.rows[0].hosting_binding : void 0
910
+ );
780
911
  return { channel: cloneChannel(next), status: "updated" };
781
912
  }
782
913
  };
@@ -893,13 +1024,20 @@ async function lockChannel(client, channelId) {
893
1024
  normalizeChannelId(channelId)
894
1025
  ]);
895
1026
  }
896
- async function writeChannel(client, channelId, channel) {
1027
+ async function writeChannel(client, channelId, channel, hostedNetwork, priorBinding) {
1028
+ const binding = priorBinding !== void 0 ? priorBinding : hostedNetwork ? {
1029
+ network: hostedNetwork,
1030
+ chainId: Number(x402NetworkByCaip2(hostedNetwork)?.caip2.split(":")[1]),
1031
+ contract: BATCH_SETTLEMENT_ADDRESS.toLowerCase(),
1032
+ version: "x402-batch-settlement-v1",
1033
+ channelId
1034
+ } : null;
897
1035
  await client.query(
898
1036
  `INSERT INTO x402_batch_channels (
899
1037
  channel_id, channel_config, charged_cumulative_amount, signed_max_claimable,
900
1038
  signature, balance, total_claimed, withdraw_requested_at, refund_nonce,
901
- onchain_synced_at, last_request_timestamp, pending_request
902
- ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
1039
+ onchain_synced_at, last_request_timestamp, pending_request, hosting_binding
1040
+ ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
903
1041
  ON CONFLICT (channel_id) DO UPDATE SET
904
1042
  channel_config = EXCLUDED.channel_config,
905
1043
  charged_cumulative_amount = EXCLUDED.charged_cumulative_amount,
@@ -924,7 +1062,8 @@ async function writeChannel(client, channelId, channel) {
924
1062
  channel.refundNonce,
925
1063
  channel.onchainSyncedAt ?? null,
926
1064
  channel.lastRequestTimestamp,
927
- channel.pendingRequest ?? null
1065
+ channel.pendingRequest ?? null,
1066
+ binding
928
1067
  ]
929
1068
  );
930
1069
  }
@@ -1003,6 +1142,8 @@ var CreditLedger = class {
1003
1142
  durable = true;
1004
1143
  x402Settlements;
1005
1144
  expiryReleaseOutbox;
1145
+ hostingCleanupFence;
1146
+ invoiceSettlementClients = /* @__PURE__ */ new WeakSet();
1006
1147
  /**
1007
1148
  * Bound after construction because the settlement store shares this ledger and is
1008
1149
  * initialized separately. Ledger effects and route preflights use the same refund gate.
@@ -1018,6 +1159,9 @@ var CreditLedger = class {
1018
1159
  useCreditExpiryReleaseOutbox(outbox) {
1019
1160
  this.expiryReleaseOutbox = outbox;
1020
1161
  }
1162
+ useHostingCleanupFence(fence) {
1163
+ this.hostingCleanupFence = fence;
1164
+ }
1021
1165
  async init() {
1022
1166
  await withSdkInitLock(this.pool, () => this.createTables());
1023
1167
  }
@@ -1248,6 +1392,8 @@ var CreditLedger = class {
1248
1392
  expires_at BIGINT NOT NULL,
1249
1393
  created_at BIGINT NOT NULL,
1250
1394
  settled_at BIGINT,
1395
+ hosting_admission_id TEXT,
1396
+ created_at_db_us BIGINT NOT NULL DEFAULT FLOOR(EXTRACT(EPOCH FROM clock_timestamp()) * 1000000)::bigint,
1251
1397
  PRIMARY KEY (credit_id, fund_id)
1252
1398
  );
1253
1399
  `);
@@ -1266,6 +1412,13 @@ var CreditLedger = class {
1266
1412
  await this.pool.query(
1267
1413
  `ALTER TABLE credit_invoices ADD COLUMN IF NOT EXISTS write_off_note TEXT`
1268
1414
  );
1415
+ await this.pool.query(
1416
+ `ALTER TABLE credit_invoices ADD COLUMN IF NOT EXISTS hosting_admission_id TEXT`
1417
+ );
1418
+ await this.pool.query(
1419
+ `ALTER TABLE credit_invoices ADD COLUMN IF NOT EXISTS created_at_db_us BIGINT NOT NULL
1420
+ DEFAULT FLOOR(EXTRACT(EPOCH FROM clock_timestamp()) * 1000000)::bigint`
1421
+ );
1269
1422
  await this.pool.query(
1270
1423
  `CREATE UNIQUE INDEX IF NOT EXISTS credit_invoices_hash_uidx
1271
1424
  ON credit_invoices(payment_hash);`
@@ -1379,6 +1532,20 @@ var CreditLedger = class {
1379
1532
  }
1380
1533
  }
1381
1534
  async fundOn(q, args, b, nowMs, creditId) {
1535
+ if (this.hostingCleanupFence && (args.hostingInvoicePaymentHash || args.hostingInvoiceRepairPaymentHash) && !this.invoiceSettlementClients.has(q)) {
1536
+ throw new Error("Hosted invoice settlement must use its original locked transaction");
1537
+ }
1538
+ await this.hostingCleanupFence?.assertNewIntent(q, {
1539
+ admissionId: args.hostingAdmissionId,
1540
+ jobId: args.hostingJobId,
1541
+ paymentSeq: args.hostingPaymentSeq,
1542
+ creditId: args.hostingJobId ? void 0 : creditId,
1543
+ fundId: args.hostingFundId,
1544
+ callerPubkey: args.hostingJobId ? void 0 : args.callerPubkey,
1545
+ invoicePaymentHash: args.hostingInvoicePaymentHash,
1546
+ invoiceRepairPaymentHash: args.hostingInvoiceRepairPaymentHash,
1547
+ amountMicro: args.amountMicro
1548
+ });
1382
1549
  const wedged = await this.blockingX402Refund(
1383
1550
  b.x402Channel?.channelId,
1384
1551
  X402_SPEND_BLOCKING_SETTLEMENT_STATUSES
@@ -1734,6 +1901,11 @@ var CreditLedger = class {
1734
1901
  });
1735
1902
  const ledgerSeq = toSafeInt(credit.last_ledger_seq) + 1;
1736
1903
  const balanceAfterMicro = available - args.amountMicro;
1904
+ await this.hostingCleanupFence?.assertNewIntent(client, {
1905
+ admissionId: args.hostingAdmissionId,
1906
+ jobId: args.jobId,
1907
+ paymentSeq: args.hostingPaymentSeq
1908
+ });
1737
1909
  await client.query(`UPDATE credits SET last_ledger_seq = $2 WHERE credit_id = $1`, [
1738
1910
  args.creditId,
1739
1911
  ledgerSeq
@@ -2008,7 +2180,34 @@ var CreditLedger = class {
2008
2180
  */
2009
2181
  async recordFunding(args) {
2010
2182
  assertAmount(args.amountMicro, { min: 1 });
2183
+ if (this.hostingCleanupFence && (args.hostingInvoicePaymentHash || args.hostingInvoiceRepairPaymentHash) && (!args.tx || !this.invoiceSettlementClients.has(args.tx))) {
2184
+ throw new Error("Hosted invoice settlement must use its original locked transaction");
2185
+ }
2186
+ if (this.hostingCleanupFence && !args.tx) {
2187
+ return this.hostingCleanupFence.withNewIntent({
2188
+ admissionId: args.hostingAdmissionId,
2189
+ jobId: args.hostingJobId,
2190
+ paymentSeq: args.hostingPaymentSeq,
2191
+ creditId: args.hostingJobId ? void 0 : args.creditId,
2192
+ fundId: args.hostingJobId ? void 0 : args.fundId,
2193
+ callerPubkey: args.hostingJobId ? void 0 : args.hostingCallerPubkey,
2194
+ invoicePaymentHash: args.hostingInvoicePaymentHash,
2195
+ amountMicro: args.amountMicro,
2196
+ invoiceRepairPaymentHash: args.hostingInvoiceRepairPaymentHash
2197
+ }, (tx) => this.recordFunding({ ...args, tx }));
2198
+ }
2011
2199
  const q = args.tx ?? this.pool;
2200
+ if (args.tx) await this.hostingCleanupFence?.assertNewIntent(args.tx, {
2201
+ admissionId: args.hostingAdmissionId,
2202
+ jobId: args.hostingJobId,
2203
+ paymentSeq: args.hostingPaymentSeq,
2204
+ creditId: args.hostingJobId ? void 0 : args.creditId,
2205
+ fundId: args.hostingJobId ? void 0 : args.fundId,
2206
+ callerPubkey: args.hostingJobId ? void 0 : args.hostingCallerPubkey,
2207
+ invoicePaymentHash: args.hostingInvoicePaymentHash,
2208
+ amountMicro: args.amountMicro,
2209
+ invoiceRepairPaymentHash: args.hostingInvoiceRepairPaymentHash
2210
+ });
2012
2211
  const nowMs = args.nowMs ?? Date.now();
2013
2212
  const { rows } = await q.query(
2014
2213
  `INSERT INTO credit_fundings (credit_id, fund_id, amount_micro, rail, created_at)
@@ -2089,25 +2288,40 @@ var CreditLedger = class {
2089
2288
  const nowMs = args.nowMs ?? Date.now();
2090
2289
  let rows;
2091
2290
  try {
2092
- ({ rows } = await this.pool.query(
2093
- `INSERT INTO credit_invoices (credit_id, fund_id, caller_pubkey, currency, amount_micro,
2094
- amount_msats, payment_hash, bolt11, status, expires_at, created_at)
2095
- VALUES ($1, $2, $3, $4, $5, $6, $7, $8, 'pending', $9, $10)
2096
- ON CONFLICT (credit_id, fund_id) DO NOTHING
2097
- RETURNING *`,
2098
- [
2291
+ const insert = async (q) => {
2292
+ const hostingAdmissionId = this.hostingCleanupFence ? await this.hostingCleanupFence.fundingAdmission(
2293
+ q,
2099
2294
  args.creditId,
2100
2295
  args.fundId,
2101
- args.callerPubkey,
2102
- args.currency,
2103
- args.amountMicro,
2104
- args.amountMsats,
2105
- args.paymentHash,
2106
- args.bolt11,
2107
- args.expiresAt,
2108
- nowMs
2109
- ]
2110
- ));
2296
+ args.callerPubkey
2297
+ ) : null;
2298
+ return q.query(
2299
+ `INSERT INTO credit_invoices (credit_id, fund_id, caller_pubkey, currency, amount_micro,
2300
+ amount_msats, payment_hash, bolt11, status, expires_at, created_at,
2301
+ hosting_admission_id)
2302
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, 'pending', $9, $10, $11)
2303
+ ON CONFLICT (credit_id, fund_id) DO NOTHING
2304
+ RETURNING *`,
2305
+ [
2306
+ args.creditId,
2307
+ args.fundId,
2308
+ args.callerPubkey,
2309
+ args.currency,
2310
+ args.amountMicro,
2311
+ args.amountMsats,
2312
+ args.paymentHash,
2313
+ args.bolt11,
2314
+ args.expiresAt,
2315
+ nowMs,
2316
+ hostingAdmissionId
2317
+ ]
2318
+ );
2319
+ };
2320
+ ({ rows } = this.hostingCleanupFence ? await this.hostingCleanupFence.withNewIntent({
2321
+ creditId: args.creditId,
2322
+ fundId: args.fundId,
2323
+ callerPubkey: args.callerPubkey
2324
+ }, insert) : await insert(this.pool));
2111
2325
  } catch (err) {
2112
2326
  if (isUniqueViolation(err)) {
2113
2327
  throw new CreditLedgerError(
@@ -2184,11 +2398,14 @@ var CreditLedger = class {
2184
2398
  ]);
2185
2399
  return { invoice, funding: funding2, credit: credit2, replayed: true };
2186
2400
  }
2401
+ this.invoiceSettlementClients.add(client);
2187
2402
  const funding = await this.recordFunding({
2188
2403
  creditId: invoice.creditId,
2189
2404
  fundId: invoice.fundId,
2190
2405
  amountMicro: invoice.amountMicro,
2191
2406
  rail: "lightning",
2407
+ hostingInvoicePaymentHash: invoice.paymentHash,
2408
+ hostingCallerPubkey: invoice.callerPubkey,
2192
2409
  tx: client,
2193
2410
  nowMs
2194
2411
  });
@@ -2199,6 +2416,8 @@ var CreditLedger = class {
2199
2416
  amountMicro: invoice.amountMicro,
2200
2417
  expiryMs: args.expiryMs,
2201
2418
  basis: args.basis,
2419
+ hostingInvoicePaymentHash: invoice.paymentHash,
2420
+ hostingFundId: invoice.fundId,
2202
2421
  tx: client,
2203
2422
  nowMs
2204
2423
  });
@@ -2233,6 +2452,7 @@ var CreditLedger = class {
2233
2452
  }
2234
2453
  throw err;
2235
2454
  } finally {
2455
+ this.invoiceSettlementClients.delete(client);
2236
2456
  client.release();
2237
2457
  }
2238
2458
  }
@@ -2338,11 +2558,14 @@ var CreditLedger = class {
2338
2558
  `invoice ${args.paymentHash} is ${invoice.status}; only a blocked or written-off invoice can be reconciled`
2339
2559
  );
2340
2560
  }
2561
+ this.invoiceSettlementClients.add(client);
2341
2562
  const funding = await this.recordFunding({
2342
2563
  creditId: args.targetCreditId,
2343
2564
  fundId: invoice.paymentHash,
2344
2565
  amountMicro: invoice.amountMicro,
2345
2566
  rail: "lightning",
2567
+ hostingInvoiceRepairPaymentHash: invoice.paymentHash,
2568
+ hostingCallerPubkey: invoice.callerPubkey,
2346
2569
  tx: client,
2347
2570
  nowMs
2348
2571
  });
@@ -2353,6 +2576,8 @@ var CreditLedger = class {
2353
2576
  amountMicro: invoice.amountMicro,
2354
2577
  expiryMs: args.expiryMs,
2355
2578
  basis: args.basis,
2579
+ hostingInvoiceRepairPaymentHash: invoice.paymentHash,
2580
+ hostingFundId: invoice.paymentHash,
2356
2581
  tx: client,
2357
2582
  nowMs
2358
2583
  });
@@ -2398,6 +2623,7 @@ var CreditLedger = class {
2398
2623
  }
2399
2624
  throw err;
2400
2625
  } finally {
2626
+ this.invoiceSettlementClients.delete(client);
2401
2627
  client.release();
2402
2628
  }
2403
2629
  }