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