@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.
- package/dist/{chunk-C2AGARLM.js → chunk-2VGWYG4P.js} +5 -3
- package/dist/{chunk-A4CM2BD3.js → chunk-3MHE725Z.js} +1 -1
- package/dist/{chunk-VAMSJRJT.js → chunk-5LR6P7WN.js} +5 -3
- package/dist/{chunk-YYQG4R7T.js → chunk-74PUEZJ4.js} +6 -4
- package/dist/{chunk-MKI6OVW4.js → chunk-CZULM42S.js} +0 -51
- package/dist/{chunk-M4QDXD2A.js → chunk-FYRR2UEZ.js} +1 -1
- package/dist/{chunk-4UZN2GP5.js → chunk-GAOAYHZA.js} +209 -33
- package/dist/{chunk-Z4BNLUZF.js → chunk-IWQHUSMO.js} +3 -105
- package/dist/{chunk-4J32DTLT.js → chunk-IXR5RFK7.js} +82 -4
- package/dist/{chunk-2SD2W2BG.js → chunk-JH3SX3LY.js} +252 -26
- package/dist/{chunk-UZC4A4DM.js → chunk-K2YX2VRA.js} +2480 -206
- package/dist/chunk-KA6VN5FK.js +245 -0
- package/dist/chunk-LIRAHP7B.js +53 -0
- package/dist/{chunk-MG67KXU7.js → chunk-MZRJR2CW.js} +4 -2
- package/dist/{chunk-JDFLRNAZ.js → chunk-PB4FYZET.js} +67 -7
- package/dist/{chunk-BQ2NMWKE.js → chunk-PLG4XER6.js} +1 -1
- package/dist/{chunk-Z3EHBW2G.js → chunk-R4DQZFAL.js} +7 -5
- package/dist/chunk-RLTSCKRI.js +109 -0
- package/dist/{chunk-2KGG5ORW.js → chunk-TXX4GOUB.js} +3 -3
- package/dist/{chunk-YRDA7EWN.js → chunk-W2EQGDF5.js} +14 -10
- package/dist/{chunk-EAMZZ2B4.js → chunk-X7EZ5CLI.js} +59 -0
- package/dist/{chunk-7X6UDVCQ.js → chunk-Z4ZT5LNT.js} +66 -2
- package/dist/{credit-ledger-A35WH2TI.js → credit-ledger-MI2H7NEC.js} +3 -1
- package/dist/{credit-menu-0-UtJ3hh.d.ts → credit-menu-B8_p_vUz.d.ts} +133 -7
- package/dist/{fx-DhyT1Cjh.d.ts → fx-DuaYHvVn.d.ts} +1 -1
- package/dist/index.d.ts +6 -6
- package/dist/internal/caller.d.ts +6 -6
- package/dist/internal/caller.js +28 -24
- package/dist/internal/server.d.ts +11 -85
- package/dist/internal/server.js +18 -16
- package/dist/{job-store-D3CcZp_g.d.ts → job-store-D6Rjj5Oi.d.ts} +1 -1
- package/dist/{lightning-backend-BhsyrBLa.d.ts → lightning-backend-BaeCPmx7.d.ts} +1 -1
- package/dist/{memory-credit-ledger-2LBZVMKJ.js → memory-credit-ledger-W2BAUYJP.js} +4 -2
- package/dist/{mpp-setup-7CGBKNUP.js → mpp-setup-RF5FXGGV.js} +5 -1
- package/dist/{payout-reporter-ZMOA37GN.js → payout-reporter-3Y2U55ZX.js} +1 -1
- package/dist/{postgres-job-store-MSD2HZOI.js → postgres-job-store-VLXLWXUQ.js} +1 -1
- package/dist/{revenue-reporter-EKJOGQYR.js → revenue-reporter-MAERPYGK.js} +1 -1
- package/dist/server/index.d.ts +14 -10
- package/dist/server/index.js +54 -17
- package/dist/{step-cache-v2yhyhSZ.d.ts → step-cache-Bi43A0JN.d.ts} +1769 -1549
- package/dist/{tempo-charge-store-4WX7FPOV.js → tempo-charge-store-Q3D7KYMH.js} +1 -1
- package/dist/tempo-lifecycle-OTHV757Y.js +14 -0
- package/dist/{tempo-session-store-LCSDHB7G.js → tempo-session-store-ZRNCOX7P.js} +1 -1
- package/dist/{tempo-wallet-RCGBBAJH.js → tempo-wallet-32JWQZKQ.js} +4 -3
- package/dist/testing/index.d.ts +3 -3
- package/dist/{usd-Cu9G6xv2.d.ts → usd-AZPVq4eU.d.ts} +1 -1
- package/dist/{x402-S47Q77UE.js → x402-CEDHGZDI.js} +17 -14
- package/package.json +1 -1
- package/dist/chunk-YHN3WMMF.js +0 -122
- 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-
|
|
11
|
+
} from "./chunk-PLG4XER6.js";
|
|
11
12
|
import {
|
|
12
13
|
MemoryCreditLedger
|
|
13
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-FYRR2UEZ.js";
|
|
14
15
|
import {
|
|
15
16
|
PostgresJobStore
|
|
16
|
-
} from "./chunk-
|
|
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-
|
|
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-
|
|
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-
|
|
92
|
+
} from "./chunk-3MHE725Z.js";
|
|
90
93
|
import {
|
|
91
94
|
resolveRpcOverride
|
|
92
|
-
} from "./chunk-
|
|
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-
|
|
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
|
-
|
|
147
|
-
|
|
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
|
|
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 (
|
|
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 (
|
|
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
|
|
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(
|
|
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
|
|
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(
|
|
556
|
+
function groupRowsIntoBatches(rows2) {
|
|
545
557
|
const groups = /* @__PURE__ */ new Map();
|
|
546
|
-
for (const r of
|
|
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
|
|
1594
|
-
if (!
|
|
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:
|
|
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
|
|
1654
|
-
if (!
|
|
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,
|
|
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
|
|
1688
|
+
const transaction2 = log.transactionHash;
|
|
1676
1689
|
const amount = logAmount(log);
|
|
1677
|
-
if (!
|
|
1678
|
-
if (await transactionMatchesRefund(client,
|
|
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
|
|
1695
|
-
if (!
|
|
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:
|
|
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
|
|
1708
|
-
if (!
|
|
1709
|
-
return calldataMatchesRefund(
|
|
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(
|
|
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
|
-
|
|
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
|
|
2626
|
+
const rows2 = [];
|
|
2518
2627
|
for (const intent of page.settlements) {
|
|
2519
|
-
|
|
2628
|
+
rows2.push({ intent, evidence: await classifySettlementEvidence(evidenceReader, intent) });
|
|
2520
2629
|
}
|
|
2521
|
-
return { settlements:
|
|
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
|
|
3088
|
-
if (
|
|
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 `${
|
|
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-
|
|
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(
|
|
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
|
-
|
|
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-
|
|
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-
|
|
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
|
|
8576
|
-
if (
|
|
8577
|
-
const found = new Set(
|
|
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 =
|
|
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
|
|
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:
|
|
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
|
-
|
|
9075
|
-
|
|
9076
|
-
|
|
9077
|
-
|
|
9078
|
-
|
|
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
|
|
9082
|
-
const
|
|
9083
|
-
|
|
9084
|
-
}
|
|
9085
|
-
|
|
9086
|
-
return
|
|
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
|
|
9089
|
-
|
|
9090
|
-
|
|
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,
|
|
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 =
|
|
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,
|
|
9221
|
-
|
|
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(
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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 =
|
|
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:
|
|
9445
|
-
generation: Number(
|
|
9446
|
-
deadlineMs:
|
|
9447
|
-
leaseExpiresAtMs:
|
|
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
|
|
11643
|
+
import { createHash as createHash7 } from "crypto";
|
|
9521
11644
|
function hashKey(key) {
|
|
9522
|
-
return
|
|
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
|
|
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(
|
|
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 (!
|
|
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 (
|
|
12502
|
+
if (rows2.length === 0) return;
|
|
10380
12503
|
const nowSec = Math.floor(opts.now() / 1e3);
|
|
10381
|
-
for (const { mint_url, proof_keyset_id } of
|
|
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
|
|
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
|
|
13614
|
-
|
|
13615
|
-
|
|
13616
|
-
|
|
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
|
-
|
|
13619
|
-
|
|
13620
|
-
|
|
13621
|
-
|
|
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
|
-
|
|
13624
|
-
|
|
13625
|
-
|
|
13626
|
-
|
|
13627
|
-
|
|
13628
|
-
|
|
13629
|
-
|
|
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
|
-
|
|
13637
|
-
|
|
13638
|
-
|
|
13639
|
-
|
|
13640
|
-
|
|
13641
|
-
|
|
13642
|
-
|
|
13643
|
-
|
|
13644
|
-
|
|
13645
|
-
|
|
13646
|
-
|
|
13647
|
-
|
|
13648
|
-
|
|
13649
|
-
|
|
13650
|
-
|
|
13651
|
-
|
|
13652
|
-
|
|
13653
|
-
|
|
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
|
|
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 =
|
|
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 (!
|
|
13761
|
-
const intent = rowToIntent(
|
|
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 =
|
|
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
|
|
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
|
|
15973
|
+
return rows2.map(rowToAttempt);
|
|
13830
15974
|
}
|
|
13831
|
-
async insertAttempt(q, dvmId, intentId, outcome, detail,
|
|
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
|
-
[
|
|
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,
|
|
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:
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
|
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-
|
|
17168
|
-
const reporter = new RevenueReporter(
|
|
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-
|
|
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
|
-
|
|
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 :
|
|
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:
|
|
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) =>
|
|
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
|
-
|
|
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
|
-
|
|
18308
|
-
|
|
18309
|
-
|
|
18310
|
-
|
|
18311
|
-
|
|
18312
|
-
|
|
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
|
|
18993
|
-
const
|
|
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 =
|
|
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 ?
|
|
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 =
|
|
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,
|