@dvmkit/sdk 0.6.0-rc.1 → 0.7.0-rc.10
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/README.md +39 -2
- package/dist/{chunk-PLG4XER6.js → chunk-2NUFE2PG.js} +47 -5
- package/dist/{chunk-7ZUKL4IV.js → chunk-3E4C6W7W.js} +16 -5
- package/dist/{chunk-HHBDBMEO.js → chunk-53SNRGQQ.js} +375 -158
- package/dist/{chunk-IWQHUSMO.js → chunk-62FI3NJF.js} +78 -2
- package/dist/chunk-6HJEVCVI.js +17 -0
- package/dist/chunk-A262WVYX.js +6 -0
- package/dist/{chunk-RLTSCKRI.js → chunk-BPYMEMQX.js} +50 -3
- package/dist/{chunk-IV6Q7NRW.js → chunk-C5RIQWG4.js} +1 -1
- package/dist/{chunk-GAOAYHZA.js → chunk-FBQVOOYF.js} +37 -6
- package/dist/{chunk-Z4ZT5LNT.js → chunk-GEI4MAXE.js} +3 -3
- package/dist/{chunk-FYRR2UEZ.js → chunk-GLIAR7RN.js} +1 -1
- package/dist/{chunk-X7EZ5CLI.js → chunk-HCATFJDD.js} +3 -3
- package/dist/{chunk-BYCEKO65.js → chunk-HPCXF3DV.js} +220 -32
- package/dist/{chunk-JH3SX3LY.js → chunk-IRXIH775.js} +6 -6
- package/dist/{chunk-QR2EF2NI.js → chunk-KCQDXIJU.js} +1 -42
- package/dist/chunk-MDW7ZR7V.js +47 -0
- package/dist/{chunk-KA6VN5FK.js → chunk-MQYM4T66.js} +3 -3
- package/dist/{chunk-P6QON7ER.js → chunk-NV7Z7377.js} +1 -1
- package/dist/{chunk-OQA7IHI4.js → chunk-OBBVDKLE.js} +484 -234
- package/dist/{chunk-EQLA4FFY.js → chunk-OHUXCAOM.js} +17 -6
- package/dist/{chunk-FI4CST2D.js → chunk-PZ5IAWFD.js} +5 -7
- package/dist/{chunk-IXR5RFK7.js → chunk-R3TQSXWT.js} +191 -5
- package/dist/{chunk-VHNPVGSA.js → chunk-T5DIXOLR.js} +3 -3
- package/dist/{chunk-RCM3P2HE.js → chunk-UKD3R2KU.js} +55 -4
- package/dist/chunk-WMGXJQI3.js +120 -0
- package/dist/{chunk-N5RZ5HVB.js → chunk-X6S2CHFM.js} +2 -2
- package/dist/{chunk-ETXDTUJO.js → chunk-ZCNPIILZ.js} +112 -1
- package/dist/{content-retention-BTGSQRfP.d.ts → content-retention-D2K4MKNO.d.ts} +1 -1
- package/dist/{credit-ledger-MI2H7NEC.js → credit-ledger-MFIIOSZH.js} +4 -5
- package/dist/{credit-menu-BAsrRu3z.d.ts → credit-menu-D2t1-Tf6.d.ts} +13 -6
- package/dist/{fx-BgDmyTdd.d.ts → fx-Ql4nvdbh.d.ts} +10 -3
- package/dist/index.d.ts +5 -5
- package/dist/index.js +248 -16
- package/dist/internal/caller.d.ts +4586 -7318
- package/dist/internal/caller.js +4431 -8127
- package/dist/internal/index.js +1 -1
- package/dist/internal/server.d.ts +7 -7
- package/dist/internal/server.js +30 -33
- package/dist/{job-store-BVeF5_Jq.d.ts → job-store-DCvJQUX0.d.ts} +1 -1
- package/dist/{lightning-backend-40fIzz1W.d.ts → lightning-backend-CY5ML3km.d.ts} +4 -2
- package/dist/memory-credit-ledger-44NE4PZC.js +11 -0
- package/dist/{mpp-setup-RF5FXGGV.js → mpp-setup-HRWUBULX.js} +4 -2
- package/dist/{payout-reporter-3Y2U55ZX.js → payout-reporter-7P66T77I.js} +6 -4
- package/dist/{postgres-consumed-credential-store-BUVCAPLS.js → postgres-consumed-credential-store-IDGPFKIL.js} +3 -3
- package/dist/{postgres-job-store-VHLQBJZC.js → postgres-job-store-UKNJ2F6Z.js} +2 -2
- package/dist/{postgres-replay-store-ARZSQ45X.js → postgres-replay-store-PMA4ADXN.js} +2 -2
- package/dist/{pricing-XT2IAPNE.js → pricing-52XJHJ6K.js} +12 -12
- package/dist/{revenue-reporter-MAERPYGK.js → revenue-reporter-HV7RMS54.js} +2 -2
- package/dist/server/index.d.ts +12 -11
- package/dist/server/index.js +49 -53
- package/dist/{step-cache-CPdH1hei.d.ts → step-cache-BOWAn_BH.d.ts} +84 -7
- package/dist/{tempo-charge-store-Q3D7KYMH.js → tempo-charge-store-P2Z4RMU7.js} +2 -2
- package/dist/{tempo-session-store-ZRNCOX7P.js → tempo-session-store-VSP7KLBS.js} +3 -3
- package/dist/testing/index.d.ts +3 -3
- package/dist/testing/index.js +7 -6
- package/dist/{usd-AWlrwlM5.d.ts → usd-MAtMp3DB.d.ts} +1 -1
- package/dist/{x402-R6HPBY5D.js → x402-3IQ3HXSG.js} +9 -29
- package/package.json +3 -2
- package/dist/chunk-AT6V3SY7.js +0 -102
- package/dist/chunk-B2IROPQF.js +0 -1844
- package/dist/chunk-CZULM42S.js +0 -143
- package/dist/chunk-GFY5FRX3.js +0 -218
- package/dist/chunk-HCLVYZUZ.js +0 -244
- package/dist/chunk-LE74UEV7.js +0 -1071
- package/dist/chunk-LIRAHP7B.js +0 -53
- package/dist/chunk-MZRJR2CW.js +0 -776
- package/dist/chunk-OFVV37XC.js +0 -57
- package/dist/chunk-PAIM66YA.js +0 -347
- package/dist/chunk-UOCZXO2Z.js +0 -1663
- package/dist/memory-credit-ledger-W2BAUYJP.js +0 -12
- package/dist/tempo-lifecycle-ZBBLFBMH.js +0 -15
- package/dist/tempo-wallet-B5XXV3YH.js +0 -40
package/dist/chunk-B2IROPQF.js
DELETED
|
@@ -1,1844 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
IMPLICIT_CREDIT_TTL_MS,
|
|
3
|
-
isDrainReceipt
|
|
4
|
-
} from "./chunk-PAIM66YA.js";
|
|
5
|
-
import {
|
|
6
|
-
CREDITS_FILE,
|
|
7
|
-
ensureConfigDir
|
|
8
|
-
} from "./chunk-CZULM42S.js";
|
|
9
|
-
import {
|
|
10
|
-
DvmError
|
|
11
|
-
} from "./chunk-LIRAHP7B.js";
|
|
12
|
-
|
|
13
|
-
// src/lib/payment-rails/types.ts
|
|
14
|
-
var RAIL_FLAGS = ["cashu", "tempo", "x402"];
|
|
15
|
-
var ADVERTISED_AMOUNT_TOLERANCE_BPS = 1000n;
|
|
16
|
-
function maxAdvertisedMicro(localMicro) {
|
|
17
|
-
return localMicro * (10000n + ADVERTISED_AMOUNT_TOLERANCE_BPS) / 10000n;
|
|
18
|
-
}
|
|
19
|
-
function withinAdvertisedTolerance(advertisedMicro, localMicro) {
|
|
20
|
-
return advertisedMicro * 10000n <= localMicro * (10000n + ADVERTISED_AMOUNT_TOLERANCE_BPS);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// src/lib/transport/types.ts
|
|
24
|
-
var SUPPORTED_PROTOCOL_VERSION = 1;
|
|
25
|
-
function parseCreditTerms(raw) {
|
|
26
|
-
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
|
|
27
|
-
const credit = raw;
|
|
28
|
-
if (typeof credit.min_micro !== "number" || typeof credit.max_micro !== "number" || typeof credit.ttl_ms !== "number" || typeof credit.currency !== "string" || !Array.isArray(credit.funding) || !credit.funding.every((method) => typeof method === "string") || credit.lightning_min_micro !== void 0 && typeof credit.lightning_min_micro !== "number")
|
|
29
|
-
return void 0;
|
|
30
|
-
const tempo = credit.tempo === void 0 ? void 0 : parseCreditTempoTerms(credit.tempo);
|
|
31
|
-
if (credit.tempo !== void 0 && !tempo) return void 0;
|
|
32
|
-
const x402 = credit.x402 === void 0 ? void 0 : parseCreditX402Terms(credit.x402);
|
|
33
|
-
if (credit.x402 !== void 0 && !x402) return void 0;
|
|
34
|
-
return {
|
|
35
|
-
min_micro: credit.min_micro,
|
|
36
|
-
max_micro: credit.max_micro,
|
|
37
|
-
ttl_ms: credit.ttl_ms,
|
|
38
|
-
currency: credit.currency,
|
|
39
|
-
funding: [...credit.funding],
|
|
40
|
-
...tempo && { tempo },
|
|
41
|
-
...x402 && { x402 },
|
|
42
|
-
...typeof credit.lightning_min_micro === "number" && {
|
|
43
|
-
lightning_min_micro: credit.lightning_min_micro
|
|
44
|
-
}
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
function parseCreditMenu(raw) {
|
|
48
|
-
const terms = parseCreditTerms(raw);
|
|
49
|
-
if (!terms) return void 0;
|
|
50
|
-
const credit = raw;
|
|
51
|
-
return {
|
|
52
|
-
...terms,
|
|
53
|
-
...typeof credit.credit_id === "string" && { credit_id: credit.credit_id },
|
|
54
|
-
...typeof credit.balance_micro === "number" && { balance_micro: credit.balance_micro },
|
|
55
|
-
...typeof credit.remaining_micro === "number" && {
|
|
56
|
-
remaining_micro: credit.remaining_micro
|
|
57
|
-
},
|
|
58
|
-
...typeof credit.expiry_ms === "number" && { expiry_ms: credit.expiry_ms }
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
function parseCreditTempoTerms(raw) {
|
|
62
|
-
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
|
|
63
|
-
const block = raw;
|
|
64
|
-
if (!Array.isArray(block.methods) || !block.methods.every(isCreditTempoMethod)) return void 0;
|
|
65
|
-
if (block.withheld !== void 0 && (!Array.isArray(block.withheld) || !block.withheld.every(isCreditTempoWithheldMethod)))
|
|
66
|
-
return void 0;
|
|
67
|
-
return {
|
|
68
|
-
methods: block.methods.map((method) => ({
|
|
69
|
-
method: method.method,
|
|
70
|
-
intent: method.intent
|
|
71
|
-
})),
|
|
72
|
-
...Array.isArray(block.withheld) && {
|
|
73
|
-
withheld: block.withheld.map((method) => ({
|
|
74
|
-
method: method.method,
|
|
75
|
-
intent: method.intent,
|
|
76
|
-
reason: method.reason
|
|
77
|
-
}))
|
|
78
|
-
}
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
function isCreditTempoMethod(raw) {
|
|
82
|
-
return !!raw && typeof raw === "object" && typeof raw.method === "string" && (raw.intent === "charge" || raw.intent === "session");
|
|
83
|
-
}
|
|
84
|
-
function isCreditTempoWithheldMethod(raw) {
|
|
85
|
-
return isCreditTempoMethod(raw) && typeof raw.reason === "string";
|
|
86
|
-
}
|
|
87
|
-
function parseCreditX402Terms(raw) {
|
|
88
|
-
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
|
|
89
|
-
const schemes = raw.schemes;
|
|
90
|
-
if (!Array.isArray(schemes) || !schemes.every(
|
|
91
|
-
(entry) => !!entry && typeof entry === "object" && typeof entry.scheme === "string" && typeof entry.network === "string"
|
|
92
|
-
))
|
|
93
|
-
return void 0;
|
|
94
|
-
return {
|
|
95
|
-
schemes: schemes.map((entry) => ({
|
|
96
|
-
scheme: entry.scheme,
|
|
97
|
-
network: entry.network
|
|
98
|
-
}))
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// src/lib/credit-store.ts
|
|
103
|
-
import { existsSync, readFileSync as readFileSync2, renameSync, unlinkSync, writeFileSync } from "fs";
|
|
104
|
-
import { dirname } from "path";
|
|
105
|
-
import lockfile from "proper-lockfile";
|
|
106
|
-
|
|
107
|
-
// src/lib/credit-history.ts
|
|
108
|
-
import { createHash } from "crypto";
|
|
109
|
-
import {
|
|
110
|
-
closeSync,
|
|
111
|
-
fsyncSync,
|
|
112
|
-
openSync,
|
|
113
|
-
readFileSync,
|
|
114
|
-
writeSync,
|
|
115
|
-
ftruncateSync
|
|
116
|
-
} from "fs";
|
|
117
|
-
function historyPath() {
|
|
118
|
-
return `${CREDITS_FILE.slice(0, -5)}-history.jsonl`;
|
|
119
|
-
}
|
|
120
|
-
function hash(value) {
|
|
121
|
-
return createHash("sha256").update(value).digest("hex");
|
|
122
|
-
}
|
|
123
|
-
function fail(detail) {
|
|
124
|
-
throw new DvmError(
|
|
125
|
-
"credit_history_unavailable",
|
|
126
|
-
`Completed credit history could not be preserved: ${detail}`,
|
|
127
|
-
"Keep the credit store and history file intact, fix the file access or damage, and retry. Working evidence was not evicted."
|
|
128
|
-
);
|
|
129
|
-
}
|
|
130
|
-
var receiptKeys = /* @__PURE__ */ new Set([
|
|
131
|
-
"v",
|
|
132
|
-
"kind",
|
|
133
|
-
"credit_id",
|
|
134
|
-
"drain_id",
|
|
135
|
-
"event",
|
|
136
|
-
"method",
|
|
137
|
-
"amount_micro",
|
|
138
|
-
"balance_after_micro",
|
|
139
|
-
"ledger_seq",
|
|
140
|
-
"issued_at",
|
|
141
|
-
"caller_pubkey",
|
|
142
|
-
"dvm_id",
|
|
143
|
-
"dvm",
|
|
144
|
-
"receipt_pubkey",
|
|
145
|
-
"signature"
|
|
146
|
-
]);
|
|
147
|
-
var attestationKeys = /* @__PURE__ */ new Set([
|
|
148
|
-
"dvm_id",
|
|
149
|
-
"slug",
|
|
150
|
-
"capabilities_hash",
|
|
151
|
-
"builder_pubkey",
|
|
152
|
-
"receipt_pubkey",
|
|
153
|
-
"deployed_at"
|
|
154
|
-
]);
|
|
155
|
-
function safeReceipts(bundle) {
|
|
156
|
-
const receipts = [];
|
|
157
|
-
for (const receipt of bundle.receipts) {
|
|
158
|
-
if (!isDrainReceipt(receipt))
|
|
159
|
-
fail("a completed bundle contains an unrecognized receipt");
|
|
160
|
-
if (Object.keys(receipt).some((key) => !receiptKeys.has(key)))
|
|
161
|
-
fail("a receipt contains unsupported fields");
|
|
162
|
-
if (Object.values(receipt).some(
|
|
163
|
-
(value) => typeof value !== "string" && typeof value !== "number" || typeof value === "number" && (!Number.isSafeInteger(value) || value < 0)
|
|
164
|
-
))
|
|
165
|
-
fail("a receipt contains unsupported values");
|
|
166
|
-
receipts.push(receipt);
|
|
167
|
-
}
|
|
168
|
-
return receipts;
|
|
169
|
-
}
|
|
170
|
-
function safeAnchor(anchor) {
|
|
171
|
-
if (!anchor) return void 0;
|
|
172
|
-
if (typeof anchor.pubkey !== "string" || typeof anchor.signature !== "string" || !anchor.attestation || Object.keys(anchor.attestation).some((key) => !attestationKeys.has(key)) || Object.values(anchor.attestation).some(
|
|
173
|
-
(value) => typeof value !== "string" && typeof value !== "number"
|
|
174
|
-
))
|
|
175
|
-
fail("an advertisement contains unsupported fields");
|
|
176
|
-
return {
|
|
177
|
-
pubkey: anchor.pubkey,
|
|
178
|
-
signature: anchor.signature,
|
|
179
|
-
attestation: anchor.attestation
|
|
180
|
-
};
|
|
181
|
-
}
|
|
182
|
-
function hasAttestedIdentity(record) {
|
|
183
|
-
return Boolean(record.dvmId && record.attestedBuilderPubkey);
|
|
184
|
-
}
|
|
185
|
-
function compatibleLocations(left, right) {
|
|
186
|
-
if (hasAttestedIdentity(left) && hasAttestedIdentity(right))
|
|
187
|
-
return left.dvmId === right.dvmId && left.attestedBuilderPubkey === right.attestedBuilderPubkey;
|
|
188
|
-
return [left.endpoint, ...left.endpointAliases].some(
|
|
189
|
-
(origin) => [right.endpoint, ...right.endpointAliases].includes(origin)
|
|
190
|
-
);
|
|
191
|
-
}
|
|
192
|
-
function base(record, identity, occurredAt) {
|
|
193
|
-
return {
|
|
194
|
-
v: 1,
|
|
195
|
-
id: hash(
|
|
196
|
-
JSON.stringify([
|
|
197
|
-
record.callerPubkey,
|
|
198
|
-
hasAttestedIdentity(record) ? record.dvmId : record.endpoint,
|
|
199
|
-
hasAttestedIdentity(record) ? record.attestedBuilderPubkey : null,
|
|
200
|
-
record.creditId,
|
|
201
|
-
identity
|
|
202
|
-
])
|
|
203
|
-
),
|
|
204
|
-
endpoint: record.endpoint,
|
|
205
|
-
endpointAliases: record.endpointAliases ?? [],
|
|
206
|
-
dvmId: record.dvmId ?? null,
|
|
207
|
-
attestedBuilderPubkey: record.attestedBuilderPubkey ?? null,
|
|
208
|
-
callerPubkey: record.callerPubkey,
|
|
209
|
-
creditId: record.creditId,
|
|
210
|
-
occurredAt,
|
|
211
|
-
rail: null,
|
|
212
|
-
currency: null,
|
|
213
|
-
amountMicro: null,
|
|
214
|
-
feeMicro: null
|
|
215
|
-
};
|
|
216
|
-
}
|
|
217
|
-
function completedRefund(record, bundle) {
|
|
218
|
-
const receipts = safeReceipts(bundle);
|
|
219
|
-
const anchor = safeAnchor(bundle.anchor);
|
|
220
|
-
const receipt = receipts.find(
|
|
221
|
-
(r) => r.drain_id === bundle.drainId && r.credit_id === bundle.creditId
|
|
222
|
-
);
|
|
223
|
-
const metadata = bundle.settlement;
|
|
224
|
-
if (metadata && (metadata.rail !== null && typeof metadata.rail !== "string" || metadata.currency !== null && typeof metadata.currency !== "string" || [metadata.amountMicro, metadata.feeMicro].some(
|
|
225
|
-
(value) => value !== null && (!Number.isSafeInteger(value) || value < 0)
|
|
226
|
-
)))
|
|
227
|
-
fail("invalid settlement metadata");
|
|
228
|
-
return {
|
|
229
|
-
...base(
|
|
230
|
-
{ ...record, creditId: bundle.creditId },
|
|
231
|
-
`refund:${bundle.drainId}`,
|
|
232
|
-
bundle.settledAt
|
|
233
|
-
),
|
|
234
|
-
kind: "completed_refund",
|
|
235
|
-
outcome: "refunded",
|
|
236
|
-
drainId: bundle.drainId,
|
|
237
|
-
settledAt: bundle.settledAt,
|
|
238
|
-
collectedAt: bundle.collectedAt === 0 ? null : bundle.collectedAt,
|
|
239
|
-
rail: metadata?.rail ?? receipt?.method ?? null,
|
|
240
|
-
currency: metadata?.currency ?? null,
|
|
241
|
-
amountMicro: metadata?.amountMicro ?? receipt?.amount_micro ?? null,
|
|
242
|
-
feeMicro: metadata?.feeMicro ?? null,
|
|
243
|
-
receipts,
|
|
244
|
-
...anchor ? { anchor } : {},
|
|
245
|
-
provenance: {
|
|
246
|
-
settlement: "local_observation",
|
|
247
|
-
receipts: "unverified_observations",
|
|
248
|
-
advertisement: anchor ? "present" : "absent"
|
|
249
|
-
}
|
|
250
|
-
};
|
|
251
|
-
}
|
|
252
|
-
function completedLoss(record) {
|
|
253
|
-
return {
|
|
254
|
-
...base(record, "loss", record.updatedAt),
|
|
255
|
-
kind: "expired_credit_loss",
|
|
256
|
-
outcome: "lost",
|
|
257
|
-
rail: record.creditId.startsWith("imp:") ? record.creditId.split(":")[1] || null : null,
|
|
258
|
-
currency: record.currency,
|
|
259
|
-
amountMicro: record.remainingMicro,
|
|
260
|
-
reason: record.lossReason ?? "expired",
|
|
261
|
-
releasedAt: record.releasedAt,
|
|
262
|
-
expiryMs: record.expiryMs,
|
|
263
|
-
releasedByJobId: record.releasedByJobId
|
|
264
|
-
};
|
|
265
|
-
}
|
|
266
|
-
function validateHistoryRecord(row) {
|
|
267
|
-
if (!row || row.v !== 1 || typeof row.id !== "string" || !Number.isSafeInteger(row.occurredAt) || typeof row.endpoint !== "string" || typeof row.callerPubkey !== "string" || typeof row.creditId !== "string" || !Array.isArray(row.endpointAliases) || row.endpointAliases.some((value) => typeof value !== "string") || [row.dvmId, row.attestedBuilderPubkey, row.currency, row.rail].some(
|
|
268
|
-
(value) => value !== null && typeof value !== "string"
|
|
269
|
-
) || [row.amountMicro, row.feeMicro].some(
|
|
270
|
-
(value) => value !== null && (!Number.isSafeInteger(value) || value < 0)
|
|
271
|
-
))
|
|
272
|
-
fail("invalid complete journal entry");
|
|
273
|
-
if (row.kind === "completed_refund") {
|
|
274
|
-
if (row.outcome !== "refunded" || typeof row.drainId !== "string" || !Number.isSafeInteger(row.settledAt) || row.collectedAt !== null && !Number.isSafeInteger(row.collectedAt) || !Array.isArray(row.receipts) || row.provenance?.settlement !== "local_observation" || row.provenance.receipts !== "unverified_observations" || !["present", "absent"].includes(row.provenance.advertisement))
|
|
275
|
-
fail("invalid refund entry");
|
|
276
|
-
safeReceipts({
|
|
277
|
-
drainId: row.drainId,
|
|
278
|
-
creditId: row.creditId,
|
|
279
|
-
receipts: row.receipts,
|
|
280
|
-
collectedAt: row.collectedAt ?? 0
|
|
281
|
-
});
|
|
282
|
-
safeAnchor(row.anchor);
|
|
283
|
-
} else if (row.kind === "expired_credit_loss") {
|
|
284
|
-
if (row.outcome !== "lost" || !["expired", "not_found"].includes(row.reason) || !Number.isSafeInteger(row.releasedAt) || !Number.isSafeInteger(row.expiryMs) || typeof row.releasedByJobId !== "string")
|
|
285
|
-
fail("invalid loss entry");
|
|
286
|
-
} else fail("invalid complete journal entry");
|
|
287
|
-
}
|
|
288
|
-
function journal() {
|
|
289
|
-
let bytes;
|
|
290
|
-
try {
|
|
291
|
-
bytes = readFileSync(historyPath());
|
|
292
|
-
} catch (error) {
|
|
293
|
-
if (error.code === "ENOENT")
|
|
294
|
-
return { bytes: Buffer.alloc(0), complete: 0, rows: [] };
|
|
295
|
-
fail(error instanceof Error ? error.message : String(error));
|
|
296
|
-
}
|
|
297
|
-
const complete = bytes.lastIndexOf(10) + 1;
|
|
298
|
-
const rows = bytes.subarray(0, complete).toString("utf8").split("\n").filter(Boolean).map((line) => {
|
|
299
|
-
let row;
|
|
300
|
-
try {
|
|
301
|
-
row = JSON.parse(line);
|
|
302
|
-
} catch {
|
|
303
|
-
fail("damaged complete journal line");
|
|
304
|
-
}
|
|
305
|
-
validateHistoryRecord(row);
|
|
306
|
-
return row;
|
|
307
|
-
});
|
|
308
|
-
return { bytes, complete, rows };
|
|
309
|
-
}
|
|
310
|
-
function archiveCompletedCreditRecords(records) {
|
|
311
|
-
if (!records.length) return;
|
|
312
|
-
try {
|
|
313
|
-
const current = journal();
|
|
314
|
-
const latest = new Map(
|
|
315
|
-
current.rows.map((record) => [record.id, JSON.stringify(record)])
|
|
316
|
-
);
|
|
317
|
-
const observed = new Map(current.rows.map((record) => [record.id, record]));
|
|
318
|
-
const additions = [];
|
|
319
|
-
for (const incoming of records) {
|
|
320
|
-
const prior = [...observed.values()].reverse().find(
|
|
321
|
-
(row) => row.kind === incoming.kind && row.callerPubkey === incoming.callerPubkey && row.creditId === incoming.creditId && (row.kind !== "completed_refund" || incoming.kind === "completed_refund" && row.drainId === incoming.drainId) && compatibleLocations(row, incoming)
|
|
322
|
-
);
|
|
323
|
-
const newerPrior = prior && (incoming.kind === "completed_refund" && prior.kind === "completed_refund" ? (prior.collectedAt ?? 0) > (incoming.collectedAt ?? 0) : prior.occurredAt > incoming.occurredAt);
|
|
324
|
-
const facts = newerPrior ? prior : incoming;
|
|
325
|
-
const identity = prior && hasAttestedIdentity(prior) ? prior : hasAttestedIdentity(incoming) ? incoming : newerPrior || !incoming.dvmId && !incoming.attestedBuilderPubkey ? prior ?? incoming : incoming;
|
|
326
|
-
const record = prior ? {
|
|
327
|
-
...facts,
|
|
328
|
-
id: prior.id,
|
|
329
|
-
dvmId: identity.dvmId,
|
|
330
|
-
attestedBuilderPubkey: identity.attestedBuilderPubkey,
|
|
331
|
-
endpointAliases: [
|
|
332
|
-
.../* @__PURE__ */ new Set([
|
|
333
|
-
prior.endpoint,
|
|
334
|
-
...prior.endpointAliases,
|
|
335
|
-
incoming.endpoint,
|
|
336
|
-
...incoming.endpointAliases
|
|
337
|
-
])
|
|
338
|
-
].filter((origin) => origin !== facts.endpoint)
|
|
339
|
-
} : incoming;
|
|
340
|
-
validateHistoryRecord(record);
|
|
341
|
-
const encoded = JSON.stringify(record);
|
|
342
|
-
if (latest.get(record.id) === encoded) continue;
|
|
343
|
-
additions.push(encoded + "\n");
|
|
344
|
-
latest.set(record.id, encoded);
|
|
345
|
-
observed.set(record.id, record);
|
|
346
|
-
}
|
|
347
|
-
const fd = openSync(historyPath(), "a", 384);
|
|
348
|
-
try {
|
|
349
|
-
if (current.complete !== current.bytes.length)
|
|
350
|
-
ftruncateSync(fd, current.complete);
|
|
351
|
-
const bytes = Buffer.from(additions.join(""));
|
|
352
|
-
let written = 0;
|
|
353
|
-
while (written < bytes.length) {
|
|
354
|
-
const count = writeSync(fd, bytes, written, bytes.length - written);
|
|
355
|
-
if (!count) fail("zero-length append");
|
|
356
|
-
written += count;
|
|
357
|
-
}
|
|
358
|
-
fsyncSync(fd);
|
|
359
|
-
} finally {
|
|
360
|
-
closeSync(fd);
|
|
361
|
-
}
|
|
362
|
-
{
|
|
363
|
-
const directory = openSync(
|
|
364
|
-
historyPath().slice(0, historyPath().lastIndexOf("/")),
|
|
365
|
-
"r"
|
|
366
|
-
);
|
|
367
|
-
try {
|
|
368
|
-
fsyncSync(directory);
|
|
369
|
-
} finally {
|
|
370
|
-
closeSync(directory);
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
} catch (error) {
|
|
374
|
-
fail(error instanceof Error ? error.message : String(error));
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
function readCreditHistoryPage(query) {
|
|
378
|
-
const limit = query.limit ?? 50;
|
|
379
|
-
if (!Number.isInteger(limit) || limit < 1 || limit > 100)
|
|
380
|
-
throw new DvmError(
|
|
381
|
-
"invalid_credit_history_query",
|
|
382
|
-
"History page limit must be an integer from 1 through 100."
|
|
383
|
-
);
|
|
384
|
-
const order = query.order ?? "asc";
|
|
385
|
-
if (order !== "asc" && order !== "desc")
|
|
386
|
-
throw new DvmError(
|
|
387
|
-
"invalid_credit_history_query",
|
|
388
|
-
"History order must be asc or desc."
|
|
389
|
-
);
|
|
390
|
-
const direction = order === "asc" ? 1 : -1;
|
|
391
|
-
const filter = JSON.stringify([
|
|
392
|
-
order,
|
|
393
|
-
query.endpoint ? new URL(query.endpoint).origin : null,
|
|
394
|
-
query.dvmId ?? null,
|
|
395
|
-
query.callerPubkey ?? null
|
|
396
|
-
]);
|
|
397
|
-
const current = journal();
|
|
398
|
-
let end = current.complete;
|
|
399
|
-
let after;
|
|
400
|
-
if (query.cursor) {
|
|
401
|
-
try {
|
|
402
|
-
const cursor = JSON.parse(
|
|
403
|
-
Buffer.from(query.cursor, "base64url").toString("utf8")
|
|
404
|
-
);
|
|
405
|
-
if (cursor.v !== 1 || !Number.isSafeInteger(cursor.end) || cursor.end < 0 || cursor.end > current.complete || cursor.end > 0 && current.bytes[cursor.end - 1] !== 10 || cursor.filter !== filter || cursor.prefix !== hash(current.bytes.subarray(0, cursor.end).toString("utf8")) || !Array.isArray(cursor.after) || !Number.isFinite(cursor.after[0]) || typeof cursor.after[1] !== "string")
|
|
406
|
-
throw new Error("cursor mismatch");
|
|
407
|
-
end = cursor.end;
|
|
408
|
-
after = cursor.after;
|
|
409
|
-
} catch {
|
|
410
|
-
throw new DvmError(
|
|
411
|
-
"invalid_credit_history_cursor",
|
|
412
|
-
"History cursor is invalid, stale, or belongs to different filters."
|
|
413
|
-
);
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
const rows = current.bytes.subarray(0, end).toString("utf8").split("\n").filter(Boolean).map((line) => JSON.parse(line));
|
|
417
|
-
const endpoint = query.endpoint ? new URL(query.endpoint).origin : void 0;
|
|
418
|
-
const ordered = [...new Map(rows.map((row) => [row.id, row])).values()].filter(
|
|
419
|
-
(row) => (!endpoint || row.endpoint === endpoint || row.endpointAliases.includes(endpoint)) && (!query.dvmId || row.dvmId === query.dvmId) && (!query.callerPubkey || row.callerPubkey === query.callerPubkey)
|
|
420
|
-
).sort(
|
|
421
|
-
(a, b) => direction * (a.occurredAt - b.occurredAt || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
|
|
422
|
-
);
|
|
423
|
-
const remaining = ordered.filter(
|
|
424
|
-
(row) => !after || direction * (row.occurredAt - after[0]) > 0 || row.occurredAt === after[0] && (direction === 1 ? row.id > after[1] : row.id < after[1])
|
|
425
|
-
);
|
|
426
|
-
const records = remaining.slice(0, limit);
|
|
427
|
-
const last = records.at(-1);
|
|
428
|
-
return {
|
|
429
|
-
records,
|
|
430
|
-
...remaining.length > limit && last ? {
|
|
431
|
-
nextCursor: Buffer.from(
|
|
432
|
-
JSON.stringify({
|
|
433
|
-
v: 1,
|
|
434
|
-
end,
|
|
435
|
-
prefix: hash(current.bytes.subarray(0, end).toString("utf8")),
|
|
436
|
-
filter,
|
|
437
|
-
after: [last.occurredAt, last.id]
|
|
438
|
-
})
|
|
439
|
-
).toString("base64url")
|
|
440
|
-
} : {}
|
|
441
|
-
};
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
// src/lib/credit-store.ts
|
|
445
|
-
function withCreditLock(fn, opts = {}) {
|
|
446
|
-
if (lockDepth > 0) {
|
|
447
|
-
lockDepth += 1;
|
|
448
|
-
try {
|
|
449
|
-
return rejectAsyncBody(fn());
|
|
450
|
-
} finally {
|
|
451
|
-
lockDepth -= 1;
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
const release = acquireCreditLock(opts);
|
|
455
|
-
lockDepth = 1;
|
|
456
|
-
try {
|
|
457
|
-
return rejectAsyncBody(fn());
|
|
458
|
-
} finally {
|
|
459
|
-
lockDepth = 0;
|
|
460
|
-
try {
|
|
461
|
-
release();
|
|
462
|
-
} catch {
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
function normalizeCreditEndpoint(endpoint) {
|
|
467
|
-
try {
|
|
468
|
-
return new URL(endpoint).origin;
|
|
469
|
-
} catch {
|
|
470
|
-
return endpoint.replace(/\/+$/, "");
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
function dvmLocationIdentity(record) {
|
|
474
|
-
if (typeof record.dvmId !== "string" || record.dvmId.length === 0) return void 0;
|
|
475
|
-
const pubkey = typeof record.attestedBuilderPubkey === "string" ? record.attestedBuilderPubkey.trim().toLowerCase() : "";
|
|
476
|
-
if (!/^[0-9a-f]{64}$/.test(pubkey)) return void 0;
|
|
477
|
-
return { dvmId: record.dvmId, builderPubkey: pubkey };
|
|
478
|
-
}
|
|
479
|
-
function findDvmLocated(records, args) {
|
|
480
|
-
const where = args.where ?? (() => true);
|
|
481
|
-
const identity = args.identity;
|
|
482
|
-
if (identity) {
|
|
483
|
-
const attested = records.find(
|
|
484
|
-
(record) => where(record) && matchesDvmIdentity(record, identity)
|
|
485
|
-
);
|
|
486
|
-
if (attested) return attested;
|
|
487
|
-
}
|
|
488
|
-
return records.find(
|
|
489
|
-
(record) => where(record) && matchesDvmOrigin(record, args.endpoint, identity)
|
|
490
|
-
);
|
|
491
|
-
}
|
|
492
|
-
function attachDvmLocation(record, endpoint, identity) {
|
|
493
|
-
let changed = false;
|
|
494
|
-
if (identity) {
|
|
495
|
-
if (record.dvmId !== identity.dvmId) {
|
|
496
|
-
record.dvmId = identity.dvmId;
|
|
497
|
-
changed = true;
|
|
498
|
-
}
|
|
499
|
-
if (record.attestedBuilderPubkey !== identity.builderPubkey) {
|
|
500
|
-
record.attestedBuilderPubkey = identity.builderPubkey;
|
|
501
|
-
changed = true;
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
if (endpoint !== record.endpoint && !dvmLocationOrigins(record).includes(endpoint)) {
|
|
505
|
-
record.endpointAliases = [...record.endpointAliases ?? [], endpoint];
|
|
506
|
-
changed = true;
|
|
507
|
-
}
|
|
508
|
-
return changed;
|
|
509
|
-
}
|
|
510
|
-
function dvmLocationOrigins(record) {
|
|
511
|
-
const aliases = Array.isArray(record.endpointAliases) ? record.endpointAliases.filter((endpoint) => typeof endpoint === "string") : [];
|
|
512
|
-
const primary = typeof record.endpoint === "string" ? [record.endpoint] : [];
|
|
513
|
-
return [...primary, ...aliases].map(normalizeCreditEndpoint);
|
|
514
|
-
}
|
|
515
|
-
function fundingReachableAt(funding, endpoint, credits) {
|
|
516
|
-
const at = normalizeCreditEndpoint(funding.endpoint);
|
|
517
|
-
const named = normalizeCreditEndpoint(endpoint);
|
|
518
|
-
if (at === named) return true;
|
|
519
|
-
const record = credits.find(
|
|
520
|
-
(c) => c.callerPubkey === funding.callerPubkey && c.creditId === funding.creditId
|
|
521
|
-
);
|
|
522
|
-
if (!record) return false;
|
|
523
|
-
const origins = dvmLocationOrigins(record);
|
|
524
|
-
return origins.includes(at) && origins.includes(named);
|
|
525
|
-
}
|
|
526
|
-
function pendingDrainFor(record, creditId) {
|
|
527
|
-
return record?.pendingDrains?.find((d) => d.creditId === creditId);
|
|
528
|
-
}
|
|
529
|
-
function pendingDrainById(record, drainId) {
|
|
530
|
-
return record?.pendingDrains?.find((d) => d.drainId === drainId);
|
|
531
|
-
}
|
|
532
|
-
function loadCredit(endpoint, callerPubkey, identity) {
|
|
533
|
-
return withCreditLock(() => {
|
|
534
|
-
const key = normalizeCreditEndpoint(endpoint);
|
|
535
|
-
const store = loadStore();
|
|
536
|
-
const record = findDvmLocated(store.credits, {
|
|
537
|
-
endpoint: key,
|
|
538
|
-
...identity ? { identity } : {},
|
|
539
|
-
where: (c) => c.callerPubkey === callerPubkey
|
|
540
|
-
});
|
|
541
|
-
if (record && attachDvmLocation(record, key, identity)) saveStore(store);
|
|
542
|
-
return record;
|
|
543
|
-
});
|
|
544
|
-
}
|
|
545
|
-
function clearCreditSelection(endpoint, callerPubkey, creditId, identity) {
|
|
546
|
-
return withCreditLock(() => {
|
|
547
|
-
const key = normalizeCreditEndpoint(endpoint);
|
|
548
|
-
const store = loadStore();
|
|
549
|
-
const record = findDvmLocated(store.credits, {
|
|
550
|
-
endpoint: key,
|
|
551
|
-
...identity ? { identity } : {},
|
|
552
|
-
where: (candidate) => candidate.callerPubkey === callerPubkey && candidate.creditId === creditId
|
|
553
|
-
});
|
|
554
|
-
if (!record) {
|
|
555
|
-
throw new DvmError(
|
|
556
|
-
"credit_not_found",
|
|
557
|
-
`Credit ${creditId} is not the selected local credit at ${key}.`
|
|
558
|
-
);
|
|
559
|
-
}
|
|
560
|
-
attachDvmLocation(record, key, identity);
|
|
561
|
-
record.selectionCleared = true;
|
|
562
|
-
record.updatedAt = Date.now();
|
|
563
|
-
saveStore(store);
|
|
564
|
-
return structuredClone(record);
|
|
565
|
-
});
|
|
566
|
-
}
|
|
567
|
-
function readCompletedCreditHistory(query = {}) {
|
|
568
|
-
return withCreditLock(() => {
|
|
569
|
-
loadStore(true);
|
|
570
|
-
return readCreditHistoryPage(query);
|
|
571
|
-
});
|
|
572
|
-
}
|
|
573
|
-
function listCredits() {
|
|
574
|
-
return withCreditLock(() => [...loadStore().credits].sort((a, b) => b.updatedAt - a.updatedAt));
|
|
575
|
-
}
|
|
576
|
-
function listImplicitCredits(now = Date.now()) {
|
|
577
|
-
return withCreditLock(() => {
|
|
578
|
-
const store = loadStore();
|
|
579
|
-
const aged = ageImplicitCredits(store.implicitCredits, now);
|
|
580
|
-
const capped = capLostImplicitCredits(store.implicitCredits);
|
|
581
|
-
if (aged || capped.changed) {
|
|
582
|
-
store.implicitCredits = capped.credits;
|
|
583
|
-
saveStore(store);
|
|
584
|
-
}
|
|
585
|
-
return [...store.implicitCredits].sort(compareImplicitCredits);
|
|
586
|
-
});
|
|
587
|
-
}
|
|
588
|
-
function loadImplicitCredits(endpoint, callerPubkey, identity, now = Date.now()) {
|
|
589
|
-
return withCreditLock(() => {
|
|
590
|
-
const store = loadStore();
|
|
591
|
-
const key = normalizeCreditEndpoint(endpoint);
|
|
592
|
-
let changed = ageImplicitCredits(store.implicitCredits, now);
|
|
593
|
-
const matches = store.implicitCredits.filter(
|
|
594
|
-
(record) => Boolean(
|
|
595
|
-
findDvmLocated([record], {
|
|
596
|
-
endpoint: key,
|
|
597
|
-
...identity ? { identity } : {},
|
|
598
|
-
where: (candidate) => candidate.callerPubkey === callerPubkey
|
|
599
|
-
})
|
|
600
|
-
)
|
|
601
|
-
);
|
|
602
|
-
for (const record of matches) {
|
|
603
|
-
changed = attachDvmLocation(record, key, identity) || changed;
|
|
604
|
-
}
|
|
605
|
-
const capped = capLostImplicitCredits(store.implicitCredits);
|
|
606
|
-
if (changed || capped.changed) {
|
|
607
|
-
store.implicitCredits = capped.credits;
|
|
608
|
-
saveStore(store);
|
|
609
|
-
}
|
|
610
|
-
return matches.filter((record) => store.implicitCredits.includes(record)).sort(compareImplicitCredits);
|
|
611
|
-
});
|
|
612
|
-
}
|
|
613
|
-
function markImplicitCreditLost(args) {
|
|
614
|
-
return withCreditLock(() => {
|
|
615
|
-
const store = loadStore();
|
|
616
|
-
const key = normalizeCreditEndpoint(args.endpoint);
|
|
617
|
-
const record = findDvmLocated(store.implicitCredits, {
|
|
618
|
-
endpoint: key,
|
|
619
|
-
...args.identity ? { identity: args.identity } : {},
|
|
620
|
-
where: (candidate) => candidate.callerPubkey === args.callerPubkey && candidate.creditId === args.creditId
|
|
621
|
-
});
|
|
622
|
-
if (!record) return void 0;
|
|
623
|
-
const locationChanged = attachDvmLocation(record, key, args.identity);
|
|
624
|
-
if (record.status === "lost" && !archiveOrKeep(
|
|
625
|
-
() => archiveCompletedCreditRecords([completedLoss(record)])
|
|
626
|
-
)) {
|
|
627
|
-
if (locationChanged) saveStore(store);
|
|
628
|
-
return record;
|
|
629
|
-
}
|
|
630
|
-
record.status = "lost";
|
|
631
|
-
record.lossReason = args.reason;
|
|
632
|
-
record.updatedAt = args.now ?? Date.now();
|
|
633
|
-
const capped = capLostImplicitCredits(store.implicitCredits);
|
|
634
|
-
store.implicitCredits = capped.credits;
|
|
635
|
-
saveStore(store);
|
|
636
|
-
return store.implicitCredits.includes(record) ? record : void 0;
|
|
637
|
-
});
|
|
638
|
-
}
|
|
639
|
-
function listFundingMenuSnapshots() {
|
|
640
|
-
return withCreditLock(
|
|
641
|
-
() => [...loadStore().fundingMenuSnapshots].sort((a, b) => b.observedAt - a.observedAt)
|
|
642
|
-
);
|
|
643
|
-
}
|
|
644
|
-
function recordFundingMenuSnapshot(args) {
|
|
645
|
-
const menu = parseCreditMenu(args.menu);
|
|
646
|
-
if (!menu) return;
|
|
647
|
-
withCreditLock(() => {
|
|
648
|
-
const store = loadStore();
|
|
649
|
-
const key = normalizeCreditEndpoint(args.endpoint);
|
|
650
|
-
const previous = findDvmLocated(store.fundingMenuSnapshots, {
|
|
651
|
-
endpoint: key,
|
|
652
|
-
...args.identity ? { identity: args.identity } : {},
|
|
653
|
-
where: (entry) => entry.callerPubkey === args.callerPubkey
|
|
654
|
-
});
|
|
655
|
-
const index = previous ? store.fundingMenuSnapshots.indexOf(previous) : -1;
|
|
656
|
-
const builderPubkey = args.builderPubkey ?? previous?.builderPubkey;
|
|
657
|
-
const snapshot = {
|
|
658
|
-
// The row keeps the origin it was opened at and learns the new spelling
|
|
659
|
-
// as an alias, so a caller who alternates hostnames still reads one menu.
|
|
660
|
-
endpoint: previous?.endpoint ?? key,
|
|
661
|
-
...previous?.endpointAliases ? { endpointAliases: [...previous.endpointAliases] } : {},
|
|
662
|
-
...previous?.dvmId ? { dvmId: previous.dvmId } : {},
|
|
663
|
-
...previous?.attestedBuilderPubkey ? { attestedBuilderPubkey: previous.attestedBuilderPubkey } : {},
|
|
664
|
-
callerPubkey: args.callerPubkey,
|
|
665
|
-
menu,
|
|
666
|
-
observedAt: args.now ?? Date.now(),
|
|
667
|
-
...builderPubkey ? { builderPubkey } : {}
|
|
668
|
-
};
|
|
669
|
-
attachDvmLocation(snapshot, key, args.identity);
|
|
670
|
-
if (index === -1) store.fundingMenuSnapshots.push(snapshot);
|
|
671
|
-
else store.fundingMenuSnapshots[index] = snapshot;
|
|
672
|
-
saveStore(store);
|
|
673
|
-
});
|
|
674
|
-
}
|
|
675
|
-
function listPendingFundings(args = {}) {
|
|
676
|
-
const endpoint = args.endpoint ? normalizeCreditEndpoint(args.endpoint) : void 0;
|
|
677
|
-
return withCreditLock(() => {
|
|
678
|
-
const store = loadStore();
|
|
679
|
-
return store.pendingFundings.filter(
|
|
680
|
-
(funding) => (!endpoint || fundingReachableAt(funding, endpoint, store.credits)) && (!args.callerPubkey || funding.callerPubkey === args.callerPubkey) && (!args.creditId || funding.creditId === args.creditId)
|
|
681
|
-
).sort((a, b) => a.requestedAt - b.requestedAt);
|
|
682
|
-
});
|
|
683
|
-
}
|
|
684
|
-
function loadPendingFunding(endpoint, callerPubkey, creditId) {
|
|
685
|
-
return listPendingFundings({ endpoint, callerPubkey, ...creditId ? { creditId } : {} })[0];
|
|
686
|
-
}
|
|
687
|
-
function recordPendingFunding(funding) {
|
|
688
|
-
withCreditLock(() => {
|
|
689
|
-
const store = loadStore();
|
|
690
|
-
const normalized = { ...funding, endpoint: normalizeCreditEndpoint(funding.endpoint) };
|
|
691
|
-
const index = store.pendingFundings.findIndex(
|
|
692
|
-
(entry) => entry.callerPubkey === normalized.callerPubkey && entry.creditId === normalized.creditId && entry.fundId === normalized.fundId && fundingReachableAt(entry, normalized.endpoint, store.credits)
|
|
693
|
-
);
|
|
694
|
-
if (index === -1) store.pendingFundings.push(normalized);
|
|
695
|
-
else
|
|
696
|
-
store.pendingFundings[index] = {
|
|
697
|
-
...normalized,
|
|
698
|
-
endpoint: store.pendingFundings[index].endpoint
|
|
699
|
-
};
|
|
700
|
-
saveStore(store);
|
|
701
|
-
});
|
|
702
|
-
}
|
|
703
|
-
function clearPendingFunding(endpoint, callerPubkey, creditId, fundId) {
|
|
704
|
-
withCreditLock(() => {
|
|
705
|
-
const store = loadStore();
|
|
706
|
-
const normalized = normalizeCreditEndpoint(endpoint);
|
|
707
|
-
const remaining = store.pendingFundings.filter(
|
|
708
|
-
(entry) => !(entry.callerPubkey === callerPubkey && entry.creditId === creditId && entry.fundId === fundId && fundingReachableAt(entry, normalized, store.credits))
|
|
709
|
-
);
|
|
710
|
-
if (remaining.length === store.pendingFundings.length) return;
|
|
711
|
-
saveStore({ ...store, pendingFundings: remaining });
|
|
712
|
-
});
|
|
713
|
-
}
|
|
714
|
-
function recordFundResponse(args) {
|
|
715
|
-
const now = args.now ?? Date.now();
|
|
716
|
-
return withCreditLock(() => {
|
|
717
|
-
const store = loadStore();
|
|
718
|
-
const key = normalizeCreditEndpoint(args.endpoint);
|
|
719
|
-
let record = findDvmLocated(store.credits, {
|
|
720
|
-
endpoint: key,
|
|
721
|
-
...args.identity ? { identity: args.identity } : {},
|
|
722
|
-
where: (c) => c.callerPubkey === args.callerPubkey
|
|
723
|
-
});
|
|
724
|
-
if (!record) {
|
|
725
|
-
record = {
|
|
726
|
-
endpoint: key,
|
|
727
|
-
...identityFields(args.identity),
|
|
728
|
-
callerPubkey: args.callerPubkey,
|
|
729
|
-
creditId: args.credit.credit_id,
|
|
730
|
-
currency: args.credit.currency,
|
|
731
|
-
balanceMicro: 0,
|
|
732
|
-
remainingMicro: 0,
|
|
733
|
-
ledgerSeq: 0,
|
|
734
|
-
expiryMs: 0,
|
|
735
|
-
targetMicro: args.targetMicro,
|
|
736
|
-
status: "active",
|
|
737
|
-
openedAt: now,
|
|
738
|
-
updatedAt: now
|
|
739
|
-
};
|
|
740
|
-
store.credits.push(record);
|
|
741
|
-
}
|
|
742
|
-
attachDvmLocation(record, key, args.identity);
|
|
743
|
-
adoptCreditId(record, args.credit.credit_id);
|
|
744
|
-
record.currency = args.credit.currency;
|
|
745
|
-
record.balanceMicro = args.credit.balance_micro;
|
|
746
|
-
record.remainingMicro = args.credit.remaining_micro;
|
|
747
|
-
record.ledgerSeq = Math.max(record.ledgerSeq, args.credit.ledger_seq);
|
|
748
|
-
record.expiryMs = args.credit.expiry_ms;
|
|
749
|
-
record.targetMicro = args.targetMicro;
|
|
750
|
-
record.status = args.credit.status === "unbacked" ? "unbacked" : args.credit.expired ? "expired" : "active";
|
|
751
|
-
record.reconciliationPending = args.credit.reconciliation_pending;
|
|
752
|
-
record.updatedAt = now;
|
|
753
|
-
delete record.provisional;
|
|
754
|
-
if (args.sequenced === false) record.unsequencedBalance = true;
|
|
755
|
-
else delete record.unsequencedBalance;
|
|
756
|
-
if (args.menu) record.menu = args.menu;
|
|
757
|
-
if (args.rail) {
|
|
758
|
-
record.rail = args.rail;
|
|
759
|
-
delete record.refillUnavailableRail;
|
|
760
|
-
}
|
|
761
|
-
if (args.x402Instrument) record.x402Instrument = args.x402Instrument;
|
|
762
|
-
if (args.x402ChannelNetwork) record.x402ChannelNetwork = args.x402ChannelNetwork;
|
|
763
|
-
if (args.builderPubkey) record.builderPubkey = args.builderPubkey;
|
|
764
|
-
if (args.select) delete record.selectionCleared;
|
|
765
|
-
delete record.discrepancy;
|
|
766
|
-
saveStore(store);
|
|
767
|
-
return record;
|
|
768
|
-
});
|
|
769
|
-
}
|
|
770
|
-
function recordBalanceResponse(args) {
|
|
771
|
-
const now = args.now ?? Date.now();
|
|
772
|
-
return withCreditLock(() => {
|
|
773
|
-
const existing = loadCredit(args.endpoint, args.callerPubkey, args.identity);
|
|
774
|
-
const prepaidCredits = args.credits.filter((credit) => !credit.credit_id.startsWith("imp:"));
|
|
775
|
-
const selectedPrimary = args.primary && !args.primary.credit_id.startsWith("imp:") ? args.primary : void 0;
|
|
776
|
-
const primary = selectedPrimary ?? prepaidCredits.find((credit) => credit.credit_id === existing?.creditId);
|
|
777
|
-
if (!primary) {
|
|
778
|
-
if (existing) {
|
|
779
|
-
mutateCredit(
|
|
780
|
-
{
|
|
781
|
-
endpoint: args.endpoint,
|
|
782
|
-
callerPubkey: args.callerPubkey,
|
|
783
|
-
...args.identity ? { identity: args.identity } : {}
|
|
784
|
-
},
|
|
785
|
-
(record2) => {
|
|
786
|
-
record2.siblingCredits = [];
|
|
787
|
-
return void 0;
|
|
788
|
-
}
|
|
789
|
-
);
|
|
790
|
-
}
|
|
791
|
-
return existing;
|
|
792
|
-
}
|
|
793
|
-
const record = recordFundResponse({
|
|
794
|
-
endpoint: args.endpoint,
|
|
795
|
-
...args.identity ? { identity: args.identity } : {},
|
|
796
|
-
callerPubkey: args.callerPubkey,
|
|
797
|
-
credit: primary,
|
|
798
|
-
targetMicro: args.targetMicro ?? existing?.targetMicro ?? primary.remaining_micro,
|
|
799
|
-
...args.menu ? { menu: args.menu } : {},
|
|
800
|
-
...args.builderPubkey ? { builderPubkey: args.builderPubkey } : {},
|
|
801
|
-
...args.selectPrimary ? { select: true } : {},
|
|
802
|
-
...args.sequenced === false ? { sequenced: false } : {},
|
|
803
|
-
now
|
|
804
|
-
});
|
|
805
|
-
mutateCredit(
|
|
806
|
-
{
|
|
807
|
-
endpoint: args.endpoint,
|
|
808
|
-
callerPubkey: args.callerPubkey,
|
|
809
|
-
...args.identity ? { identity: args.identity } : {}
|
|
810
|
-
},
|
|
811
|
-
(current) => {
|
|
812
|
-
current.siblingCredits = prepaidCredits.filter((credit) => credit.credit_id !== current.creditId).map((credit) => siblingFromWire(credit, now));
|
|
813
|
-
return void 0;
|
|
814
|
-
}
|
|
815
|
-
);
|
|
816
|
-
return loadCredit(args.endpoint, args.callerPubkey, args.identity) ?? record;
|
|
817
|
-
});
|
|
818
|
-
}
|
|
819
|
-
function applyReceiptCredit(args) {
|
|
820
|
-
const now = args.now ?? Date.now();
|
|
821
|
-
return withCreditLock(() => {
|
|
822
|
-
const store = loadStore();
|
|
823
|
-
const key = normalizeCreditEndpoint(args.endpoint);
|
|
824
|
-
const record = findDvmLocated(store.credits, {
|
|
825
|
-
endpoint: key,
|
|
826
|
-
...args.identity ? { identity: args.identity } : {},
|
|
827
|
-
where: (c) => c.callerPubkey === args.callerPubkey && c.creditId === args.credit.credit_id
|
|
828
|
-
});
|
|
829
|
-
if (!record) return {};
|
|
830
|
-
attachDvmLocation(record, key, args.identity);
|
|
831
|
-
let mismatch;
|
|
832
|
-
const sequential = args.credit.ledger_seq === record.ledgerSeq + 1;
|
|
833
|
-
if (sequential && args.outcome === "completed" && record.status === "active" && !record.provisional && !record.unsequencedBalance) {
|
|
834
|
-
const expected = record.remainingMicro - args.credit.amount;
|
|
835
|
-
if (args.credit.balance_after !== expected) {
|
|
836
|
-
mismatch = {
|
|
837
|
-
atMs: now,
|
|
838
|
-
expectedMicro: expected,
|
|
839
|
-
reportedMicro: args.credit.balance_after,
|
|
840
|
-
drawId: args.credit.draw_id,
|
|
841
|
-
ledgerSeq: args.credit.ledger_seq,
|
|
842
|
-
...record.lastReceiptJobId ? { priorJobId: record.lastReceiptJobId } : {},
|
|
843
|
-
jobId: args.jobId
|
|
844
|
-
};
|
|
845
|
-
record.discrepancy = mismatch;
|
|
846
|
-
}
|
|
847
|
-
}
|
|
848
|
-
if (args.credit.ledger_seq >= record.ledgerSeq) {
|
|
849
|
-
record.remainingMicro = args.credit.balance_after;
|
|
850
|
-
record.ledgerSeq = args.credit.ledger_seq;
|
|
851
|
-
delete record.unsequencedBalance;
|
|
852
|
-
}
|
|
853
|
-
delete record.provisional;
|
|
854
|
-
if (args.outcome === "completed") record.lastPriceMicro = args.credit.amount;
|
|
855
|
-
if (!mismatch && record.discrepancy && args.credit.ledger_seq > record.discrepancy.ledgerSeq) {
|
|
856
|
-
delete record.discrepancy;
|
|
857
|
-
}
|
|
858
|
-
record.lastReceiptJobId = args.jobId;
|
|
859
|
-
record.updatedAt = now;
|
|
860
|
-
saveStore(store);
|
|
861
|
-
return { credit: record, ...mismatch ? { mismatch } : {} };
|
|
862
|
-
});
|
|
863
|
-
}
|
|
864
|
-
function applyImplicitReceiptCredit(args) {
|
|
865
|
-
if (!args.credit.credit_id.startsWith("imp:")) return {};
|
|
866
|
-
const now = args.now ?? Date.now();
|
|
867
|
-
return withCreditLock(() => {
|
|
868
|
-
const store = loadStore();
|
|
869
|
-
const key = normalizeCreditEndpoint(args.endpoint);
|
|
870
|
-
let record = findDvmLocated(store.implicitCredits, {
|
|
871
|
-
endpoint: key,
|
|
872
|
-
...args.identity ? { identity: args.identity } : {},
|
|
873
|
-
where: (candidate) => candidate.callerPubkey === args.callerPubkey && candidate.creditId === args.credit.credit_id
|
|
874
|
-
});
|
|
875
|
-
if (!record && (args.outcome === "completed" || args.credit.balance_after <= 0)) return {};
|
|
876
|
-
if (!record) {
|
|
877
|
-
record = {
|
|
878
|
-
endpoint: key,
|
|
879
|
-
...identityFields(args.identity),
|
|
880
|
-
callerPubkey: args.callerPubkey,
|
|
881
|
-
creditId: args.credit.credit_id,
|
|
882
|
-
currency: args.currency,
|
|
883
|
-
remainingMicro: args.credit.balance_after,
|
|
884
|
-
ledgerSeq: args.credit.ledger_seq,
|
|
885
|
-
expiryMs: args.issuedAtMs + IMPLICIT_CREDIT_TTL_MS,
|
|
886
|
-
status: args.issuedAtMs + IMPLICIT_CREDIT_TTL_MS <= now ? "lost" : "active",
|
|
887
|
-
releasedByJobId: args.jobId,
|
|
888
|
-
releasedAt: args.issuedAtMs,
|
|
889
|
-
updatedAt: now,
|
|
890
|
-
lastReceiptJobId: args.jobId
|
|
891
|
-
};
|
|
892
|
-
store.implicitCredits.push(record);
|
|
893
|
-
const capped2 = capLostImplicitCredits(store.implicitCredits);
|
|
894
|
-
store.implicitCredits = capped2.credits;
|
|
895
|
-
saveStore(store);
|
|
896
|
-
return store.implicitCredits.includes(record) ? { credit: record } : {};
|
|
897
|
-
}
|
|
898
|
-
const locationChanged = attachDvmLocation(record, key, args.identity);
|
|
899
|
-
if (record.status === "lost" && !archiveOrKeep(
|
|
900
|
-
() => archiveCompletedCreditRecords([completedLoss(record)])
|
|
901
|
-
)) {
|
|
902
|
-
if (locationChanged) saveStore(store);
|
|
903
|
-
return { credit: record };
|
|
904
|
-
}
|
|
905
|
-
let mismatch;
|
|
906
|
-
const sequential = args.credit.ledger_seq === record.ledgerSeq + 1;
|
|
907
|
-
if (sequential && args.outcome === "completed" && record.status === "active") {
|
|
908
|
-
const expected = record.remainingMicro - args.credit.amount;
|
|
909
|
-
if (args.credit.balance_after !== expected) {
|
|
910
|
-
mismatch = {
|
|
911
|
-
atMs: now,
|
|
912
|
-
expectedMicro: expected,
|
|
913
|
-
reportedMicro: args.credit.balance_after,
|
|
914
|
-
drawId: args.credit.draw_id,
|
|
915
|
-
ledgerSeq: args.credit.ledger_seq,
|
|
916
|
-
...record.lastReceiptJobId ? { priorJobId: record.lastReceiptJobId } : {},
|
|
917
|
-
jobId: args.jobId
|
|
918
|
-
};
|
|
919
|
-
record.discrepancy = mismatch;
|
|
920
|
-
}
|
|
921
|
-
}
|
|
922
|
-
if (args.credit.ledger_seq >= record.ledgerSeq) {
|
|
923
|
-
record.remainingMicro = args.credit.balance_after;
|
|
924
|
-
record.ledgerSeq = args.credit.ledger_seq;
|
|
925
|
-
}
|
|
926
|
-
if (!mismatch && record.discrepancy && args.credit.ledger_seq > record.discrepancy.ledgerSeq) {
|
|
927
|
-
delete record.discrepancy;
|
|
928
|
-
}
|
|
929
|
-
record.currency = args.currency;
|
|
930
|
-
record.lastReceiptJobId = args.jobId;
|
|
931
|
-
record.updatedAt = now;
|
|
932
|
-
if (record.remainingMicro <= 0) {
|
|
933
|
-
store.implicitCredits = store.implicitCredits.filter((candidate) => candidate !== record);
|
|
934
|
-
saveStore(store);
|
|
935
|
-
return { ...mismatch ? { mismatch } : {}, pruned: true };
|
|
936
|
-
}
|
|
937
|
-
if (record.expiryMs <= now) record.status = "lost";
|
|
938
|
-
const capped = capLostImplicitCredits(store.implicitCredits);
|
|
939
|
-
store.implicitCredits = capped.credits;
|
|
940
|
-
saveStore(store);
|
|
941
|
-
return {
|
|
942
|
-
...store.implicitCredits.includes(record) ? { credit: record } : {},
|
|
943
|
-
...mismatch ? { mismatch } : {}
|
|
944
|
-
};
|
|
945
|
-
});
|
|
946
|
-
}
|
|
947
|
-
function ensureProvisionalCredit(args) {
|
|
948
|
-
const now = args.now ?? Date.now();
|
|
949
|
-
withCreditLock(() => {
|
|
950
|
-
const store = loadStore();
|
|
951
|
-
const key = normalizeCreditEndpoint(args.endpoint);
|
|
952
|
-
let record = findDvmLocated(store.credits, {
|
|
953
|
-
endpoint: key,
|
|
954
|
-
...args.identity ? { identity: args.identity } : {},
|
|
955
|
-
where: (c) => c.callerPubkey === args.callerPubkey
|
|
956
|
-
});
|
|
957
|
-
if (!record) {
|
|
958
|
-
record = {
|
|
959
|
-
endpoint: key,
|
|
960
|
-
...identityFields(args.identity),
|
|
961
|
-
callerPubkey: args.callerPubkey,
|
|
962
|
-
creditId: args.creditId,
|
|
963
|
-
currency: args.currency,
|
|
964
|
-
balanceMicro: 0,
|
|
965
|
-
remainingMicro: 0,
|
|
966
|
-
ledgerSeq: 0,
|
|
967
|
-
expiryMs: 0,
|
|
968
|
-
targetMicro: args.targetMicro,
|
|
969
|
-
status: "active",
|
|
970
|
-
openedAt: now,
|
|
971
|
-
updatedAt: now
|
|
972
|
-
};
|
|
973
|
-
store.credits.push(record);
|
|
974
|
-
}
|
|
975
|
-
attachDvmLocation(record, key, args.identity);
|
|
976
|
-
adoptCreditId(record, args.creditId);
|
|
977
|
-
record.currency = args.currency;
|
|
978
|
-
record.targetMicro = args.targetMicro;
|
|
979
|
-
record.status = "active";
|
|
980
|
-
record.provisional = true;
|
|
981
|
-
record.updatedAt = now;
|
|
982
|
-
if (args.menu) record.menu = args.menu;
|
|
983
|
-
saveStore(store);
|
|
984
|
-
});
|
|
985
|
-
}
|
|
986
|
-
function markCredit(endpoint, callerPubkey, status, opts = {}) {
|
|
987
|
-
mutateCredit(
|
|
988
|
-
{ endpoint, callerPubkey, ...opts.identity ? { identity: opts.identity } : {} },
|
|
989
|
-
(record) => {
|
|
990
|
-
if (opts.ifCreditId !== void 0 && record.creditId !== opts.ifCreditId) return "skip";
|
|
991
|
-
record.status = status;
|
|
992
|
-
if (status !== "active") {
|
|
993
|
-
delete record.refillUnavailableRail;
|
|
994
|
-
delete record.refillDrainNoticed;
|
|
995
|
-
}
|
|
996
|
-
if (status === "drained") record.remainingMicro = 0;
|
|
997
|
-
return void 0;
|
|
998
|
-
}
|
|
999
|
-
);
|
|
1000
|
-
}
|
|
1001
|
-
function loadCreditX402Binding(endpoint, callerPubkey, creditId, identity) {
|
|
1002
|
-
const record = loadCredit(endpoint, callerPubkey, identity);
|
|
1003
|
-
if (record?.creditId !== creditId) return {};
|
|
1004
|
-
if (record.status !== "active" && record.status !== "expired") return {};
|
|
1005
|
-
return {
|
|
1006
|
-
...record.x402Instrument ? { instrument: record.x402Instrument } : {},
|
|
1007
|
-
...record.x402ChannelNetwork ? { channelNetwork: record.x402ChannelNetwork } : {}
|
|
1008
|
-
};
|
|
1009
|
-
}
|
|
1010
|
-
function recordCreditX402Instrument(endpoint, callerPubkey, creditId, instrument, identity, channelNetwork) {
|
|
1011
|
-
mutateCredit(mutationAt(endpoint, callerPubkey, identity), (record) => {
|
|
1012
|
-
if (record.creditId !== creditId) return "skip";
|
|
1013
|
-
const instrumentChanged = record.x402Instrument !== instrument;
|
|
1014
|
-
const channelNetworkChanged = instrument === "x402_channel" && channelNetwork !== void 0 && record.x402ChannelNetwork !== channelNetwork;
|
|
1015
|
-
if (!instrumentChanged && !channelNetworkChanged) return "skip";
|
|
1016
|
-
if (instrumentChanged) {
|
|
1017
|
-
record.x402Instrument = instrument;
|
|
1018
|
-
delete record.x402ChannelNetwork;
|
|
1019
|
-
}
|
|
1020
|
-
if (instrument === "x402_channel" && channelNetwork !== void 0) {
|
|
1021
|
-
record.x402ChannelNetwork = channelNetwork;
|
|
1022
|
-
}
|
|
1023
|
-
return void 0;
|
|
1024
|
-
});
|
|
1025
|
-
}
|
|
1026
|
-
function recordRefusedDrawBalance(args) {
|
|
1027
|
-
const remaining = safeMicro(args.remainingMicro);
|
|
1028
|
-
if (remaining === void 0) return;
|
|
1029
|
-
const balance = safeMicro(args.balanceMicro);
|
|
1030
|
-
mutateCredit(mutationAt(args.endpoint, args.callerPubkey, args.identity), (record) => {
|
|
1031
|
-
if (record.creditId !== args.creditId) return "skip";
|
|
1032
|
-
const unchanged = record.remainingMicro === remaining && (balance === void 0 || record.balanceMicro === balance) && record.unsequencedBalance === true && !record.provisional;
|
|
1033
|
-
if (unchanged) return "skip";
|
|
1034
|
-
record.remainingMicro = remaining;
|
|
1035
|
-
if (balance !== void 0) record.balanceMicro = balance;
|
|
1036
|
-
record.unsequencedBalance = true;
|
|
1037
|
-
delete record.provisional;
|
|
1038
|
-
return void 0;
|
|
1039
|
-
});
|
|
1040
|
-
}
|
|
1041
|
-
function markRefillRailUnavailable(endpoint, callerPubkey, creditId, rail, identity) {
|
|
1042
|
-
let firstObservation = false;
|
|
1043
|
-
mutateCredit(mutationAt(endpoint, callerPubkey, identity), (record) => {
|
|
1044
|
-
if (record.creditId !== creditId) return "skip";
|
|
1045
|
-
if (record.refillUnavailableRail === rail) return "skip";
|
|
1046
|
-
record.refillUnavailableRail = rail;
|
|
1047
|
-
firstObservation = true;
|
|
1048
|
-
return void 0;
|
|
1049
|
-
});
|
|
1050
|
-
return firstObservation;
|
|
1051
|
-
}
|
|
1052
|
-
function markRefillDrainNoticed(endpoint, callerPubkey, creditId, drainId, identity) {
|
|
1053
|
-
let firstObservation = false;
|
|
1054
|
-
mutateCredit(mutationAt(endpoint, callerPubkey, identity), (record) => {
|
|
1055
|
-
if (record.creditId !== creditId) return "skip";
|
|
1056
|
-
if (record.refillDrainNoticed === drainId) return "skip";
|
|
1057
|
-
record.refillDrainNoticed = drainId;
|
|
1058
|
-
firstObservation = true;
|
|
1059
|
-
return void 0;
|
|
1060
|
-
});
|
|
1061
|
-
return firstObservation;
|
|
1062
|
-
}
|
|
1063
|
-
function clearRefillRailUnavailable(endpoint, callerPubkey, creditId, identity) {
|
|
1064
|
-
mutateCredit(mutationAt(endpoint, callerPubkey, identity), (record) => {
|
|
1065
|
-
if (record.creditId !== creditId) return "skip";
|
|
1066
|
-
if (record.refillUnavailableRail === void 0) return "skip";
|
|
1067
|
-
delete record.refillUnavailableRail;
|
|
1068
|
-
return void 0;
|
|
1069
|
-
});
|
|
1070
|
-
}
|
|
1071
|
-
function forgetCredit(endpoint, callerPubkey, identity) {
|
|
1072
|
-
withCreditLock(() => {
|
|
1073
|
-
const store = loadStore();
|
|
1074
|
-
const key = normalizeCreditEndpoint(endpoint);
|
|
1075
|
-
const record = findDvmLocated(store.credits, {
|
|
1076
|
-
endpoint: key,
|
|
1077
|
-
...identity ? { identity } : {},
|
|
1078
|
-
where: (c) => c.callerPubkey === callerPubkey
|
|
1079
|
-
});
|
|
1080
|
-
if (!record) return;
|
|
1081
|
-
archiveCompletedCreditRecords(
|
|
1082
|
-
(record.drainEvidence ?? []).filter((bundle) => bundle.settledAt !== void 0).map((bundle) => completedRefund(record, bundle))
|
|
1083
|
-
);
|
|
1084
|
-
saveStore({ ...store, credits: store.credits.filter((c) => c !== record) });
|
|
1085
|
-
});
|
|
1086
|
-
}
|
|
1087
|
-
function recordPendingDrain(endpoint, callerPubkey, drain, identity) {
|
|
1088
|
-
mutateCredit(mutationAt(endpoint, callerPubkey, identity), (record) => {
|
|
1089
|
-
const drains = record.pendingDrains ?? [];
|
|
1090
|
-
const index = drains.findIndex(
|
|
1091
|
-
(d) => d.drainId === drain.drainId && d.creditId === drain.creditId
|
|
1092
|
-
);
|
|
1093
|
-
if (index === -1) drains.push(drain);
|
|
1094
|
-
else drains[index] = drain;
|
|
1095
|
-
record.pendingDrains = drains;
|
|
1096
|
-
return void 0;
|
|
1097
|
-
});
|
|
1098
|
-
}
|
|
1099
|
-
function clearPendingDrain(endpoint, callerPubkey, drain, identity) {
|
|
1100
|
-
mutateCredit(mutationAt(endpoint, callerPubkey, identity), (record) => {
|
|
1101
|
-
const before = record.pendingDrains ?? [];
|
|
1102
|
-
const remaining = before.filter(
|
|
1103
|
-
(d) => !(d.drainId === drain.drainId && d.creditId === drain.creditId)
|
|
1104
|
-
);
|
|
1105
|
-
if (remaining.length === before.length) return "skip";
|
|
1106
|
-
if (remaining.length === 0) delete record.pendingDrains;
|
|
1107
|
-
else record.pendingDrains = remaining;
|
|
1108
|
-
return void 0;
|
|
1109
|
-
});
|
|
1110
|
-
}
|
|
1111
|
-
function recordDrainReceipts(args) {
|
|
1112
|
-
const {
|
|
1113
|
-
endpoint,
|
|
1114
|
-
identity,
|
|
1115
|
-
callerPubkey,
|
|
1116
|
-
drainId,
|
|
1117
|
-
creditId,
|
|
1118
|
-
receipts,
|
|
1119
|
-
read,
|
|
1120
|
-
now = Date.now()
|
|
1121
|
-
} = args;
|
|
1122
|
-
if (receipts.length === 0) return;
|
|
1123
|
-
mutateCredit(mutationAt(endpoint, callerPubkey, identity), (record) => {
|
|
1124
|
-
const evidence = record.drainEvidence ??= [];
|
|
1125
|
-
let bundle = evidence.find((e) => e.drainId === drainId);
|
|
1126
|
-
if (bundle?.settledAt !== void 0 && !archiveOrKeep(
|
|
1127
|
-
() => archiveCompletedCreditRecords([completedRefund(record, bundle)])
|
|
1128
|
-
)) {
|
|
1129
|
-
return "skip";
|
|
1130
|
-
}
|
|
1131
|
-
if (!bundle) {
|
|
1132
|
-
bundle = { drainId, creditId, receipts: [], collectedAt: now };
|
|
1133
|
-
evidence.push(bundle);
|
|
1134
|
-
}
|
|
1135
|
-
bundle.creditId = creditId;
|
|
1136
|
-
bundle.receipts = receipts;
|
|
1137
|
-
bundle.collectedAt = now;
|
|
1138
|
-
bundle.settlement ??= {
|
|
1139
|
-
rail: null,
|
|
1140
|
-
currency: record.creditId === creditId ? record.currency : null,
|
|
1141
|
-
amountMicro: null,
|
|
1142
|
-
feeMicro: null
|
|
1143
|
-
};
|
|
1144
|
-
if (read.anchor) bundle.anchor = read.anchor;
|
|
1145
|
-
else if (read.reachable) delete bundle.anchor;
|
|
1146
|
-
record.drainEvidence = evidence;
|
|
1147
|
-
return void 0;
|
|
1148
|
-
});
|
|
1149
|
-
}
|
|
1150
|
-
function settleDrainEvidence(args) {
|
|
1151
|
-
const { endpoint, identity, callerPubkey, drainId, now = Date.now() } = args;
|
|
1152
|
-
mutateCredit(mutationAt(endpoint, callerPubkey, identity), (record) => {
|
|
1153
|
-
const evidence = record.drainEvidence ?? [];
|
|
1154
|
-
const bundle = evidence.find((e) => e.drainId === drainId);
|
|
1155
|
-
if (!bundle || bundle.settledAt !== void 0) return "skip";
|
|
1156
|
-
bundle.settledAt = now;
|
|
1157
|
-
if (args.settlement) bundle.settlement = args.settlement;
|
|
1158
|
-
record.drainEvidence = evidence;
|
|
1159
|
-
return void 0;
|
|
1160
|
-
});
|
|
1161
|
-
}
|
|
1162
|
-
function identityFields(identity) {
|
|
1163
|
-
if (!identity) return {};
|
|
1164
|
-
return { dvmId: identity.dvmId, attestedBuilderPubkey: identity.builderPubkey };
|
|
1165
|
-
}
|
|
1166
|
-
function mutationAt(endpoint, callerPubkey, identity) {
|
|
1167
|
-
return { endpoint, callerPubkey, ...identity ? { identity } : {} };
|
|
1168
|
-
}
|
|
1169
|
-
function matchesDvmIdentity(record, identity) {
|
|
1170
|
-
const stored = dvmLocationIdentity(record);
|
|
1171
|
-
return stored?.dvmId === identity.dvmId && stored.builderPubkey === identity.builderPubkey;
|
|
1172
|
-
}
|
|
1173
|
-
function matchesDvmOrigin(record, endpoint, identity) {
|
|
1174
|
-
if (identity) {
|
|
1175
|
-
if (dvmLocationIdentity(record) !== void 0) return false;
|
|
1176
|
-
if (record.dvmId !== void 0 && record.dvmId !== identity.dvmId) return false;
|
|
1177
|
-
}
|
|
1178
|
-
return dvmLocationOrigins(record).includes(endpoint);
|
|
1179
|
-
}
|
|
1180
|
-
function adoptCreditId(record, creditId) {
|
|
1181
|
-
if (record.creditId === creditId) return;
|
|
1182
|
-
if (record.status === "active" || record.status === "expired" || record.status === "unbacked") {
|
|
1183
|
-
const prior = {
|
|
1184
|
-
creditId: record.creditId,
|
|
1185
|
-
status: record.status === "unbacked" ? "unbacked" : "active",
|
|
1186
|
-
currency: record.currency,
|
|
1187
|
-
balanceMicro: record.balanceMicro,
|
|
1188
|
-
remainingMicro: record.remainingMicro,
|
|
1189
|
-
ledgerSeq: record.ledgerSeq,
|
|
1190
|
-
expiryMs: record.expiryMs,
|
|
1191
|
-
expired: record.status === "expired",
|
|
1192
|
-
drainable: record.status !== "unbacked" && record.balanceMicro > 0,
|
|
1193
|
-
...record.reconciliationPending ? { reconciliationPending: true } : {},
|
|
1194
|
-
observedAt: record.updatedAt
|
|
1195
|
-
};
|
|
1196
|
-
record.siblingCredits = [
|
|
1197
|
-
...(record.siblingCredits ?? []).filter(
|
|
1198
|
-
(sibling) => sibling.creditId !== prior.creditId && sibling.creditId !== creditId
|
|
1199
|
-
),
|
|
1200
|
-
prior
|
|
1201
|
-
];
|
|
1202
|
-
} else if (record.siblingCredits) {
|
|
1203
|
-
record.siblingCredits = record.siblingCredits.filter(
|
|
1204
|
-
(sibling) => sibling.creditId !== creditId
|
|
1205
|
-
);
|
|
1206
|
-
}
|
|
1207
|
-
record.creditId = creditId;
|
|
1208
|
-
record.ledgerSeq = 0;
|
|
1209
|
-
record.balanceMicro = 0;
|
|
1210
|
-
record.remainingMicro = 0;
|
|
1211
|
-
record.expiryMs = 0;
|
|
1212
|
-
delete record.unsequencedBalance;
|
|
1213
|
-
delete record.rail;
|
|
1214
|
-
delete record.x402Instrument;
|
|
1215
|
-
delete record.x402ChannelNetwork;
|
|
1216
|
-
delete record.refillUnavailableRail;
|
|
1217
|
-
delete record.refillDrainNoticed;
|
|
1218
|
-
delete record.discrepancy;
|
|
1219
|
-
delete record.lastReceiptJobId;
|
|
1220
|
-
}
|
|
1221
|
-
var MAX_SETTLED_DRAIN_EVIDENCE = 8;
|
|
1222
|
-
var MAX_LOST_IMPLICIT_CREDITS = 8;
|
|
1223
|
-
var MAX_DATE_MS = 864e13;
|
|
1224
|
-
function loadStore(requireArchive = false) {
|
|
1225
|
-
if (!existsSync(CREDITS_FILE)) {
|
|
1226
|
-
return { credits: [], implicitCredits: [], pendingFundings: [], fundingMenuSnapshots: [] };
|
|
1227
|
-
}
|
|
1228
|
-
let raw;
|
|
1229
|
-
try {
|
|
1230
|
-
raw = readFileSync2(CREDITS_FILE, "utf-8");
|
|
1231
|
-
} catch (cause) {
|
|
1232
|
-
throw unreadableStore(cause);
|
|
1233
|
-
}
|
|
1234
|
-
let data;
|
|
1235
|
-
try {
|
|
1236
|
-
const parsed = JSON.parse(raw);
|
|
1237
|
-
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1238
|
-
throw new Error("the file parsed as JSON but is not a credit-store object");
|
|
1239
|
-
}
|
|
1240
|
-
data = parsed;
|
|
1241
|
-
} catch (cause) {
|
|
1242
|
-
throw quarantineCorruptStore(cause);
|
|
1243
|
-
}
|
|
1244
|
-
const merged = mergeDuplicateCredits(
|
|
1245
|
-
Array.isArray(data.credits) ? data.credits.filter((record) => isObjectRecord(record)).map(
|
|
1246
|
-
(record) => normalizeRailKey(normalizeEvidence(normalizeDrains(normalizeSiblingCredits(record))))
|
|
1247
|
-
) : []
|
|
1248
|
-
);
|
|
1249
|
-
const snapshots = mergeDuplicateSnapshots(
|
|
1250
|
-
Array.isArray(data.fundingMenuSnapshots) ? data.fundingMenuSnapshots.filter(isFundingMenuSnapshot) : [],
|
|
1251
|
-
merged.credits
|
|
1252
|
-
);
|
|
1253
|
-
const fundings = dedupePendingFundings(
|
|
1254
|
-
Array.isArray(data.pendingFundings) ? data.pendingFundings.filter((funding) => isObjectRecord(funding)).map(normalizeRailKey) : []
|
|
1255
|
-
);
|
|
1256
|
-
const implicit = dedupeImplicitCredits(
|
|
1257
|
-
Array.isArray(data.implicitCredits) ? data.implicitCredits.filter(isStoredImplicitCredit) : [],
|
|
1258
|
-
requireArchive
|
|
1259
|
-
);
|
|
1260
|
-
const store = {
|
|
1261
|
-
credits: merged.credits,
|
|
1262
|
-
implicitCredits: implicit.credits,
|
|
1263
|
-
pendingFundings: fundings.fundings,
|
|
1264
|
-
fundingMenuSnapshots: snapshots.snapshots
|
|
1265
|
-
};
|
|
1266
|
-
if (requireArchive) archiveStore(store);
|
|
1267
|
-
if (merged.changed || snapshots.changed || fundings.changed || implicit.changed) saveStore(store);
|
|
1268
|
-
else if (!requireArchive) archiveOrKeep(() => archiveStore(store));
|
|
1269
|
-
return store;
|
|
1270
|
-
}
|
|
1271
|
-
function isStoredImplicitCredit(value) {
|
|
1272
|
-
if (!isObjectRecord(value)) return false;
|
|
1273
|
-
return typeof value.endpoint === "string" && typeof value.callerPubkey === "string" && typeof value.creditId === "string" && value.creditId.startsWith("imp:") && typeof value.currency === "string" && typeof value.remainingMicro === "number" && Number.isFinite(value.remainingMicro) && typeof value.ledgerSeq === "number" && Number.isInteger(value.ledgerSeq) && typeof value.expiryMs === "number" && Number.isFinite(value.expiryMs) && (value.status === "active" || value.status === "lost") && typeof value.releasedByJobId === "string" && typeof value.releasedAt === "number" && Number.isFinite(value.releasedAt) && typeof value.updatedAt === "number" && Number.isFinite(value.updatedAt);
|
|
1274
|
-
}
|
|
1275
|
-
function dedupeImplicitCredits(credits, requireArchive = false) {
|
|
1276
|
-
const merged = [];
|
|
1277
|
-
let changed = false;
|
|
1278
|
-
for (const candidate of credits) {
|
|
1279
|
-
candidate.endpoint = normalizeCreditEndpoint(candidate.endpoint);
|
|
1280
|
-
const identity = dvmLocationIdentity(candidate);
|
|
1281
|
-
const existing = findDvmLocated(merged, {
|
|
1282
|
-
endpoint: candidate.endpoint,
|
|
1283
|
-
...identity ? { identity } : {},
|
|
1284
|
-
where: (record) => record.callerPubkey === candidate.callerPubkey && record.creditId === candidate.creditId
|
|
1285
|
-
});
|
|
1286
|
-
if (!existing) {
|
|
1287
|
-
merged.push(candidate);
|
|
1288
|
-
continue;
|
|
1289
|
-
}
|
|
1290
|
-
if (!archiveOrKeep(() => archiveCompletedCreditRecords(
|
|
1291
|
-
[existing, candidate].filter((record) => record.status === "lost").map(completedLoss)
|
|
1292
|
-
), requireArchive)) return { credits, changed: false };
|
|
1293
|
-
changed = true;
|
|
1294
|
-
for (const origin of dvmLocationOrigins(candidate)) {
|
|
1295
|
-
attachDvmLocation(existing, origin, identity);
|
|
1296
|
-
}
|
|
1297
|
-
if (candidate.ledgerSeq > existing.ledgerSeq || candidate.ledgerSeq === existing.ledgerSeq && candidate.updatedAt > existing.updatedAt) {
|
|
1298
|
-
existing.currency = candidate.currency;
|
|
1299
|
-
existing.remainingMicro = candidate.remainingMicro;
|
|
1300
|
-
existing.ledgerSeq = candidate.ledgerSeq;
|
|
1301
|
-
existing.status = candidate.status;
|
|
1302
|
-
if (candidate.lossReason) existing.lossReason = candidate.lossReason;
|
|
1303
|
-
else delete existing.lossReason;
|
|
1304
|
-
existing.updatedAt = candidate.updatedAt;
|
|
1305
|
-
existing.lastReceiptJobId = candidate.lastReceiptJobId;
|
|
1306
|
-
existing.discrepancy = candidate.discrepancy;
|
|
1307
|
-
}
|
|
1308
|
-
existing.expiryMs = Math.min(existing.expiryMs, candidate.expiryMs);
|
|
1309
|
-
if (candidate.releasedAt < existing.releasedAt) {
|
|
1310
|
-
existing.releasedAt = candidate.releasedAt;
|
|
1311
|
-
existing.releasedByJobId = candidate.releasedByJobId;
|
|
1312
|
-
}
|
|
1313
|
-
}
|
|
1314
|
-
const capped = capLostImplicitCredits(merged, requireArchive);
|
|
1315
|
-
return { credits: capped.credits, changed: changed || capped.changed };
|
|
1316
|
-
}
|
|
1317
|
-
function ageImplicitCredits(credits, now) {
|
|
1318
|
-
let changed = false;
|
|
1319
|
-
for (const credit of credits) {
|
|
1320
|
-
if (credit.status === "active" && credit.expiryMs <= now) {
|
|
1321
|
-
credit.status = "lost";
|
|
1322
|
-
credit.lossReason = "expired";
|
|
1323
|
-
credit.updatedAt = now;
|
|
1324
|
-
changed = true;
|
|
1325
|
-
}
|
|
1326
|
-
}
|
|
1327
|
-
return changed;
|
|
1328
|
-
}
|
|
1329
|
-
function capLostImplicitCredits(credits, requireArchive = false) {
|
|
1330
|
-
const archived = archiveOrKeep(
|
|
1331
|
-
() => archiveCompletedCreditRecords(
|
|
1332
|
-
credits.filter((credit) => credit.status === "lost").map(completedLoss)
|
|
1333
|
-
),
|
|
1334
|
-
requireArchive
|
|
1335
|
-
);
|
|
1336
|
-
if (!archived) return { credits, changed: false };
|
|
1337
|
-
const lostByOwner = /* @__PURE__ */ new Map();
|
|
1338
|
-
for (const credit of credits) {
|
|
1339
|
-
if (credit.status !== "lost") continue;
|
|
1340
|
-
const identity = dvmLocationIdentity(credit);
|
|
1341
|
-
const dvmKey = identity ? `${identity.dvmId}\0${identity.builderPubkey}` : normalizeCreditEndpoint(credit.endpoint);
|
|
1342
|
-
const key = `${credit.callerPubkey}\0${dvmKey}`;
|
|
1343
|
-
const group = lostByOwner.get(key) ?? [];
|
|
1344
|
-
group.push(credit);
|
|
1345
|
-
lostByOwner.set(key, group);
|
|
1346
|
-
}
|
|
1347
|
-
const dropped = /* @__PURE__ */ new Set();
|
|
1348
|
-
for (const group of lostByOwner.values()) {
|
|
1349
|
-
group.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
1350
|
-
for (const credit of group.slice(MAX_LOST_IMPLICIT_CREDITS)) dropped.add(credit);
|
|
1351
|
-
}
|
|
1352
|
-
return dropped.size === 0 ? { credits, changed: false } : { credits: credits.filter((credit) => !dropped.has(credit)), changed: true };
|
|
1353
|
-
}
|
|
1354
|
-
function compareImplicitCredits(a, b) {
|
|
1355
|
-
if (a.status !== b.status) return a.status === "active" ? -1 : 1;
|
|
1356
|
-
return a.status === "active" ? a.expiryMs - b.expiryMs : b.updatedAt - a.updatedAt;
|
|
1357
|
-
}
|
|
1358
|
-
function mergeDuplicateSnapshots(snapshots, credits) {
|
|
1359
|
-
const identities = originIdentityMap(credits);
|
|
1360
|
-
const merged = [];
|
|
1361
|
-
const byIdentity = /* @__PURE__ */ new Map();
|
|
1362
|
-
let changed = false;
|
|
1363
|
-
for (const snapshot of snapshots) {
|
|
1364
|
-
const stamped = identities.get(`${snapshot.callerPubkey}\0${snapshot.endpoint}`);
|
|
1365
|
-
if (stamped !== void 0 && snapshot.dvmId === void 0) {
|
|
1366
|
-
snapshot.dvmId = stamped.dvmId;
|
|
1367
|
-
if (stamped.attestedBuilderPubkey !== void 0) {
|
|
1368
|
-
snapshot.attestedBuilderPubkey = stamped.attestedBuilderPubkey;
|
|
1369
|
-
}
|
|
1370
|
-
changed = true;
|
|
1371
|
-
}
|
|
1372
|
-
if (snapshot.dvmId === void 0) {
|
|
1373
|
-
merged.push(snapshot);
|
|
1374
|
-
continue;
|
|
1375
|
-
}
|
|
1376
|
-
const key = `${snapshot.callerPubkey}\0${snapshot.dvmId}\0${snapshot.attestedBuilderPubkey ?? ""}`;
|
|
1377
|
-
const index = byIdentity.get(key);
|
|
1378
|
-
if (index === void 0) {
|
|
1379
|
-
byIdentity.set(key, merged.length);
|
|
1380
|
-
merged.push(snapshot);
|
|
1381
|
-
continue;
|
|
1382
|
-
}
|
|
1383
|
-
merged[index] = mergeSnapshots(merged[index], snapshot);
|
|
1384
|
-
changed = true;
|
|
1385
|
-
}
|
|
1386
|
-
return { snapshots: merged, changed };
|
|
1387
|
-
}
|
|
1388
|
-
function mergeSnapshots(left, right) {
|
|
1389
|
-
const leftSeen = Number.isFinite(left.observedAt) ? left.observedAt : 0;
|
|
1390
|
-
const rightSeen = Number.isFinite(right.observedAt) ? right.observedAt : 0;
|
|
1391
|
-
const [newer, older] = rightSeen >= leftSeen ? [right, left] : [left, right];
|
|
1392
|
-
const snapshot = { ...newer };
|
|
1393
|
-
if (snapshot.builderPubkey === void 0 && older.builderPubkey !== void 0) {
|
|
1394
|
-
snapshot.builderPubkey = older.builderPubkey;
|
|
1395
|
-
}
|
|
1396
|
-
const origins = dvmLocationOrigins(newer).concat(dvmLocationOrigins(older));
|
|
1397
|
-
const aliases = [...new Set(origins)].filter((origin) => origin !== snapshot.endpoint);
|
|
1398
|
-
if (aliases.length > 0) snapshot.endpointAliases = aliases;
|
|
1399
|
-
else delete snapshot.endpointAliases;
|
|
1400
|
-
return snapshot;
|
|
1401
|
-
}
|
|
1402
|
-
function originIdentityMap(credits) {
|
|
1403
|
-
const identities = /* @__PURE__ */ new Map();
|
|
1404
|
-
const ambiguous = /* @__PURE__ */ new Set();
|
|
1405
|
-
for (const record of credits) {
|
|
1406
|
-
if (typeof record.dvmId !== "string" || typeof record.callerPubkey !== "string") continue;
|
|
1407
|
-
const identity = {
|
|
1408
|
-
dvmId: record.dvmId,
|
|
1409
|
-
...record.attestedBuilderPubkey !== void 0 ? { attestedBuilderPubkey: record.attestedBuilderPubkey } : {}
|
|
1410
|
-
};
|
|
1411
|
-
for (const origin of dvmLocationOrigins(record)) {
|
|
1412
|
-
const key = `${record.callerPubkey}\0${origin}`;
|
|
1413
|
-
const seen = identities.get(key);
|
|
1414
|
-
if (seen !== void 0 && (seen.dvmId !== identity.dvmId || seen.attestedBuilderPubkey !== identity.attestedBuilderPubkey)) {
|
|
1415
|
-
ambiguous.add(key);
|
|
1416
|
-
continue;
|
|
1417
|
-
}
|
|
1418
|
-
identities.set(key, identity);
|
|
1419
|
-
}
|
|
1420
|
-
}
|
|
1421
|
-
for (const key of ambiguous) identities.delete(key);
|
|
1422
|
-
return identities;
|
|
1423
|
-
}
|
|
1424
|
-
function dedupePendingFundings(fundings) {
|
|
1425
|
-
const kept = [];
|
|
1426
|
-
const byIntent = /* @__PURE__ */ new Map();
|
|
1427
|
-
let changed = false;
|
|
1428
|
-
for (const funding of fundings) {
|
|
1429
|
-
if (typeof funding.callerPubkey !== "string" || typeof funding.creditId !== "string" || typeof funding.fundId !== "string") {
|
|
1430
|
-
kept.push(funding);
|
|
1431
|
-
continue;
|
|
1432
|
-
}
|
|
1433
|
-
const key = `${funding.callerPubkey}\0${funding.creditId}\0${funding.fundId}`;
|
|
1434
|
-
const index = byIntent.get(key);
|
|
1435
|
-
if (index === void 0) {
|
|
1436
|
-
byIntent.set(key, kept.length);
|
|
1437
|
-
kept.push(funding);
|
|
1438
|
-
continue;
|
|
1439
|
-
}
|
|
1440
|
-
const seen = kept[index];
|
|
1441
|
-
const seenAt = Number.isFinite(seen.requestedAt) ? seen.requestedAt : 0;
|
|
1442
|
-
const at = Number.isFinite(funding.requestedAt) ? funding.requestedAt : 0;
|
|
1443
|
-
if (at > seenAt) kept[index] = funding;
|
|
1444
|
-
changed = true;
|
|
1445
|
-
}
|
|
1446
|
-
return { fundings: kept, changed };
|
|
1447
|
-
}
|
|
1448
|
-
function isObjectRecord(value) {
|
|
1449
|
-
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1450
|
-
}
|
|
1451
|
-
function mergeDuplicateCredits(credits) {
|
|
1452
|
-
const merged = [];
|
|
1453
|
-
const byCredit = /* @__PURE__ */ new Map();
|
|
1454
|
-
let changed = false;
|
|
1455
|
-
for (const record of credits) {
|
|
1456
|
-
if (typeof record.creditId !== "string" || typeof record.callerPubkey !== "string") {
|
|
1457
|
-
merged.push(record);
|
|
1458
|
-
continue;
|
|
1459
|
-
}
|
|
1460
|
-
const key = `${record.callerPubkey}\0${record.creditId}`;
|
|
1461
|
-
const index = byCredit.get(key);
|
|
1462
|
-
if (index === void 0) {
|
|
1463
|
-
byCredit.set(key, merged.length);
|
|
1464
|
-
merged.push(record);
|
|
1465
|
-
continue;
|
|
1466
|
-
}
|
|
1467
|
-
merged[index] = mergeCreditRecords(merged[index], record);
|
|
1468
|
-
changed = true;
|
|
1469
|
-
}
|
|
1470
|
-
return { credits: merged, changed };
|
|
1471
|
-
}
|
|
1472
|
-
function mergeCreditRecords(left, right) {
|
|
1473
|
-
const leftUpdated = Number.isFinite(left.updatedAt) ? left.updatedAt : 0;
|
|
1474
|
-
const rightUpdated = Number.isFinite(right.updatedAt) ? right.updatedAt : 0;
|
|
1475
|
-
const [newer, older] = rightUpdated >= leftUpdated ? [right, left] : [left, right];
|
|
1476
|
-
const record = { ...newer };
|
|
1477
|
-
if (record.dvmId === void 0 && older.dvmId !== void 0) record.dvmId = older.dvmId;
|
|
1478
|
-
if (record.attestedBuilderPubkey === void 0 && older.attestedBuilderPubkey !== void 0 && record.dvmId === older.dvmId) {
|
|
1479
|
-
record.attestedBuilderPubkey = older.attestedBuilderPubkey;
|
|
1480
|
-
}
|
|
1481
|
-
if (record.rail === void 0 && older.rail !== void 0) record.rail = older.rail;
|
|
1482
|
-
if (record.x402Instrument === void 0 && older.x402Instrument !== void 0) {
|
|
1483
|
-
record.x402Instrument = older.x402Instrument;
|
|
1484
|
-
}
|
|
1485
|
-
if (record.x402ChannelNetwork === void 0 && older.x402ChannelNetwork !== void 0) {
|
|
1486
|
-
record.x402ChannelNetwork = older.x402ChannelNetwork;
|
|
1487
|
-
}
|
|
1488
|
-
if (record.menu === void 0 && older.menu !== void 0) record.menu = older.menu;
|
|
1489
|
-
if (record.builderPubkey === void 0 && older.builderPubkey !== void 0) {
|
|
1490
|
-
record.builderPubkey = older.builderPubkey;
|
|
1491
|
-
}
|
|
1492
|
-
const endpoints = dvmLocationOrigins(newer).concat(dvmLocationOrigins(older));
|
|
1493
|
-
const aliases = [...new Set(endpoints)].filter((endpoint) => endpoint !== record.endpoint);
|
|
1494
|
-
if (aliases.length > 0) record.endpointAliases = aliases;
|
|
1495
|
-
else delete record.endpointAliases;
|
|
1496
|
-
const pendingDrains = unionPendingDrains(newer.pendingDrains, older.pendingDrains);
|
|
1497
|
-
if (pendingDrains.length > 0) record.pendingDrains = pendingDrains;
|
|
1498
|
-
else delete record.pendingDrains;
|
|
1499
|
-
const drainEvidence = unionDrainEvidence(newer.drainEvidence, older.drainEvidence);
|
|
1500
|
-
if (drainEvidence.length > 0) record.drainEvidence = drainEvidence;
|
|
1501
|
-
else delete record.drainEvidence;
|
|
1502
|
-
const siblings = Array.isArray(newer.siblingCredits) ? newer.siblingCredits : older.siblingCredits;
|
|
1503
|
-
if (siblings) {
|
|
1504
|
-
record.siblingCredits = siblings.filter((sibling) => sibling.creditId !== record.creditId);
|
|
1505
|
-
} else {
|
|
1506
|
-
delete record.siblingCredits;
|
|
1507
|
-
}
|
|
1508
|
-
return record;
|
|
1509
|
-
}
|
|
1510
|
-
function unionPendingDrains(preferred, fallback) {
|
|
1511
|
-
const byDrain = /* @__PURE__ */ new Map();
|
|
1512
|
-
for (const drain of [...fallback ?? [], ...preferred ?? []]) {
|
|
1513
|
-
const key = `${drain.drainId}\0${drain.creditId ?? ""}`;
|
|
1514
|
-
byDrain.set(key, { ...byDrain.get(key) ?? {}, ...drain });
|
|
1515
|
-
}
|
|
1516
|
-
return [...byDrain.values()];
|
|
1517
|
-
}
|
|
1518
|
-
function unionDrainEvidence(preferred, fallback) {
|
|
1519
|
-
const byDrain = /* @__PURE__ */ new Map();
|
|
1520
|
-
for (const evidence of [...fallback ?? [], ...preferred ?? []]) {
|
|
1521
|
-
const key = `${evidence.drainId}\0${evidence.creditId}`;
|
|
1522
|
-
const existing = byDrain.get(key);
|
|
1523
|
-
if (!existing) {
|
|
1524
|
-
byDrain.set(key, evidence);
|
|
1525
|
-
continue;
|
|
1526
|
-
}
|
|
1527
|
-
const [newer, older] = evidence.collectedAt >= existing.collectedAt ? [evidence, existing] : [existing, evidence];
|
|
1528
|
-
const receipts = unionUnknown(newer.receipts, older.receipts);
|
|
1529
|
-
byDrain.set(key, {
|
|
1530
|
-
...older,
|
|
1531
|
-
...newer,
|
|
1532
|
-
receipts,
|
|
1533
|
-
collectedAt: Math.max(existing.collectedAt, evidence.collectedAt),
|
|
1534
|
-
...existing.settledAt !== void 0 || evidence.settledAt !== void 0 ? { settledAt: Math.max(existing.settledAt ?? 0, evidence.settledAt ?? 0) } : {}
|
|
1535
|
-
});
|
|
1536
|
-
}
|
|
1537
|
-
return [...byDrain.values()];
|
|
1538
|
-
}
|
|
1539
|
-
function unionUnknown(preferred, fallback) {
|
|
1540
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1541
|
-
const values = [];
|
|
1542
|
-
for (const value of [...preferred, ...fallback]) {
|
|
1543
|
-
let key;
|
|
1544
|
-
try {
|
|
1545
|
-
const serialized = JSON.stringify(value);
|
|
1546
|
-
key = typeof serialized === "string" ? serialized : String(value);
|
|
1547
|
-
} catch {
|
|
1548
|
-
key = String(value);
|
|
1549
|
-
}
|
|
1550
|
-
if (seen.has(key)) continue;
|
|
1551
|
-
seen.add(key);
|
|
1552
|
-
values.push(value);
|
|
1553
|
-
}
|
|
1554
|
-
return values;
|
|
1555
|
-
}
|
|
1556
|
-
function isFundingMenuSnapshot(value) {
|
|
1557
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
1558
|
-
const snapshot = value;
|
|
1559
|
-
return typeof snapshot.endpoint === "string" && typeof snapshot.callerPubkey === "string" && typeof snapshot.observedAt === "number" && Number.isFinite(snapshot.observedAt) && Math.abs(snapshot.observedAt) <= MAX_DATE_MS && parseCreditMenu(snapshot.menu) !== void 0 && (snapshot.builderPubkey === void 0 || typeof snapshot.builderPubkey === "string");
|
|
1560
|
-
}
|
|
1561
|
-
function siblingFromWire(credit, observedAt) {
|
|
1562
|
-
return {
|
|
1563
|
-
creditId: credit.credit_id,
|
|
1564
|
-
status: credit.status ?? "active",
|
|
1565
|
-
currency: credit.currency,
|
|
1566
|
-
balanceMicro: credit.balance_micro,
|
|
1567
|
-
remainingMicro: credit.remaining_micro,
|
|
1568
|
-
ledgerSeq: credit.ledger_seq,
|
|
1569
|
-
expiryMs: credit.expiry_ms,
|
|
1570
|
-
expired: credit.expired,
|
|
1571
|
-
drainable: credit.drainable,
|
|
1572
|
-
...credit.reconciliation_pending ? { reconciliationPending: true } : {},
|
|
1573
|
-
observedAt
|
|
1574
|
-
};
|
|
1575
|
-
}
|
|
1576
|
-
function normalizeRailKey(record) {
|
|
1577
|
-
const loose = record;
|
|
1578
|
-
if (loose.rail === LEGACY_TEMPO_RAIL_KEY) loose.rail = "tempo";
|
|
1579
|
-
if (loose.refillUnavailableRail === LEGACY_TEMPO_RAIL_KEY) loose.refillUnavailableRail = "tempo";
|
|
1580
|
-
if (Array.isArray(loose.pendingDrains)) {
|
|
1581
|
-
for (const drain of loose.pendingDrains) {
|
|
1582
|
-
if (drain?.method === LEGACY_TEMPO_RAIL_KEY) drain.method = "tempo";
|
|
1583
|
-
}
|
|
1584
|
-
}
|
|
1585
|
-
return record;
|
|
1586
|
-
}
|
|
1587
|
-
var LEGACY_TEMPO_RAIL_KEY = "mpp";
|
|
1588
|
-
function normalizeSiblingCredits(record) {
|
|
1589
|
-
if (!Array.isArray(record.siblingCredits)) {
|
|
1590
|
-
delete record.siblingCredits;
|
|
1591
|
-
return record;
|
|
1592
|
-
}
|
|
1593
|
-
record.siblingCredits = record.siblingCredits.filter(
|
|
1594
|
-
(sibling) => isSiblingCredit(sibling) && sibling.creditId !== record.creditId
|
|
1595
|
-
);
|
|
1596
|
-
return record;
|
|
1597
|
-
}
|
|
1598
|
-
function isSiblingCredit(value) {
|
|
1599
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
1600
|
-
const sibling = value;
|
|
1601
|
-
const status = value.status;
|
|
1602
|
-
return typeof sibling.creditId === "string" && typeof sibling.currency === "string" && Number.isSafeInteger(sibling.balanceMicro) && Number.isSafeInteger(sibling.remainingMicro) && Number.isSafeInteger(sibling.ledgerSeq) && Number.isFinite(sibling.expiryMs) && Math.abs(sibling.expiryMs) <= MAX_DATE_MS && typeof sibling.expired === "boolean" && typeof sibling.drainable === "boolean" && (status === void 0 || status === "active" || status === "unbacked") && (sibling.reconciliationPending === void 0 || typeof sibling.reconciliationPending === "boolean") && Number.isFinite(sibling.observedAt) && Math.abs(sibling.observedAt) <= MAX_DATE_MS;
|
|
1603
|
-
}
|
|
1604
|
-
function normalizeDrains(record) {
|
|
1605
|
-
const legacy = record;
|
|
1606
|
-
const drains = (Array.isArray(record.pendingDrains) ? record.pendingDrains : []).filter(
|
|
1607
|
-
isDrainish
|
|
1608
|
-
);
|
|
1609
|
-
if (isDrainish(legacy.pendingDrain)) drains.unshift(legacy.pendingDrain);
|
|
1610
|
-
delete legacy.pendingDrain;
|
|
1611
|
-
if (drains.length === 0) delete record.pendingDrains;
|
|
1612
|
-
else record.pendingDrains = drains;
|
|
1613
|
-
return record;
|
|
1614
|
-
}
|
|
1615
|
-
function normalizeEvidence(record) {
|
|
1616
|
-
if (!Array.isArray(record.drainEvidence)) {
|
|
1617
|
-
delete record.drainEvidence;
|
|
1618
|
-
return record;
|
|
1619
|
-
}
|
|
1620
|
-
const evidence = record.drainEvidence.filter(isEvidenceish).map((e) => {
|
|
1621
|
-
const bundle = {
|
|
1622
|
-
...e,
|
|
1623
|
-
creditId: typeof e.creditId === "string" ? e.creditId : "",
|
|
1624
|
-
collectedAt: Number.isFinite(e.collectedAt) ? e.collectedAt : 0
|
|
1625
|
-
};
|
|
1626
|
-
if (!Number.isFinite(bundle.settledAt)) delete bundle.settledAt;
|
|
1627
|
-
return bundle;
|
|
1628
|
-
});
|
|
1629
|
-
if (evidence.length === 0) delete record.drainEvidence;
|
|
1630
|
-
else record.drainEvidence = evidence;
|
|
1631
|
-
return record;
|
|
1632
|
-
}
|
|
1633
|
-
function isEvidenceish(value) {
|
|
1634
|
-
return typeof value === "object" && value !== null && typeof value.drainId === "string" && Array.isArray(value.receipts);
|
|
1635
|
-
}
|
|
1636
|
-
function isDrainish(value) {
|
|
1637
|
-
return typeof value === "object" && value !== null && typeof value.drainId === "string";
|
|
1638
|
-
}
|
|
1639
|
-
function archiveOrKeep(action, requireArchive = false) {
|
|
1640
|
-
try {
|
|
1641
|
-
action();
|
|
1642
|
-
return true;
|
|
1643
|
-
} catch (error) {
|
|
1644
|
-
if (requireArchive || !(error instanceof DvmError) || error.code !== "credit_history_unavailable") {
|
|
1645
|
-
throw error;
|
|
1646
|
-
}
|
|
1647
|
-
return false;
|
|
1648
|
-
}
|
|
1649
|
-
}
|
|
1650
|
-
function archiveStore(store) {
|
|
1651
|
-
archiveCompletedCreditRecords([
|
|
1652
|
-
...store.credits.flatMap(
|
|
1653
|
-
(credit) => (credit.drainEvidence ?? []).filter((bundle) => bundle.settledAt !== void 0).map((bundle) => completedRefund(credit, bundle))
|
|
1654
|
-
),
|
|
1655
|
-
...store.implicitCredits.filter((credit) => credit.status === "lost").map(completedLoss)
|
|
1656
|
-
]);
|
|
1657
|
-
}
|
|
1658
|
-
function capDrainEvidence(evidence) {
|
|
1659
|
-
const settled = evidence.filter((e) => e.settledAt !== void 0).sort((a, b) => a.settledAt - b.settledAt);
|
|
1660
|
-
const dropped = new Set(
|
|
1661
|
-
settled.slice(0, Math.max(0, settled.length - MAX_SETTLED_DRAIN_EVIDENCE))
|
|
1662
|
-
);
|
|
1663
|
-
return evidence.filter((e) => !dropped.has(e));
|
|
1664
|
-
}
|
|
1665
|
-
function saveStore(store) {
|
|
1666
|
-
ensureConfigDir();
|
|
1667
|
-
if (archiveOrKeep(() => archiveStore(store))) {
|
|
1668
|
-
for (const credit of store.credits) {
|
|
1669
|
-
if (credit.drainEvidence) credit.drainEvidence = capDrainEvidence(credit.drainEvidence);
|
|
1670
|
-
}
|
|
1671
|
-
}
|
|
1672
|
-
const tmp = `${CREDITS_FILE}.${String(process.pid)}.tmp`;
|
|
1673
|
-
writeFileSync(tmp, JSON.stringify(store, null, 2) + "\n", { mode: 384 });
|
|
1674
|
-
try {
|
|
1675
|
-
renameSync(tmp, CREDITS_FILE);
|
|
1676
|
-
} catch (err) {
|
|
1677
|
-
try {
|
|
1678
|
-
unlinkSync(tmp);
|
|
1679
|
-
} catch {
|
|
1680
|
-
}
|
|
1681
|
-
throw err;
|
|
1682
|
-
}
|
|
1683
|
-
}
|
|
1684
|
-
function unreadableStore(cause) {
|
|
1685
|
-
const detail = cause instanceof Error ? cause.message : String(cause);
|
|
1686
|
-
return new DvmError(
|
|
1687
|
-
"credit_store_unreadable",
|
|
1688
|
-
`The prepaid-credit record at ${CREDITS_FILE} exists but could not be opened: ${detail}`,
|
|
1689
|
-
`The file was left untouched. Check that you own it and can read it ('ls -l ${CREDITS_FILE}'), then retry \u2014 'chmod 600 ${CREDITS_FILE}' fixes the common case. Do not delete it: it may hold DVM-signed drain receipts for a refund that is still owed.`,
|
|
1690
|
-
{ credits_file: CREDITS_FILE, cause: detail },
|
|
1691
|
-
`Your local record of prepaid credits could not be opened \u2014 this looks like a file-permissions problem on this machine, not damage to the record. Nothing was changed or deleted, so any signed proof of a refund you are owed is still there.`
|
|
1692
|
-
);
|
|
1693
|
-
}
|
|
1694
|
-
function quarantineCorruptStore(cause) {
|
|
1695
|
-
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
1696
|
-
const quarantined = `${CREDITS_FILE}.corrupt-${stamp}-${String(process.pid)}`;
|
|
1697
|
-
let moved = false;
|
|
1698
|
-
try {
|
|
1699
|
-
renameSync(CREDITS_FILE, quarantined);
|
|
1700
|
-
moved = true;
|
|
1701
|
-
} catch {
|
|
1702
|
-
}
|
|
1703
|
-
const kept = moved ? quarantined : CREDITS_FILE;
|
|
1704
|
-
return new DvmError(
|
|
1705
|
-
"credit_store_corrupt",
|
|
1706
|
-
moved ? `The prepaid-credit record at ${CREDITS_FILE} is damaged and could not be parsed; it was moved aside to ${quarantined}.` : `The prepaid-credit record at ${CREDITS_FILE} is damaged and could not be parsed, and could not be moved aside.`,
|
|
1707
|
-
`Do not delete ${kept} \u2014 it may hold DVM-signed drain receipts for a refund that is still owed. Balances themselves live on the DVM, so 'dvm credit balance <dvm>' re-reads them.`,
|
|
1708
|
-
{
|
|
1709
|
-
credits_file: CREDITS_FILE,
|
|
1710
|
-
...moved ? { quarantined_path: quarantined } : {},
|
|
1711
|
-
cause: cause instanceof Error ? cause.message : String(cause)
|
|
1712
|
-
},
|
|
1713
|
-
`The local record of your prepaid credits is damaged. It was set aside at ${kept} rather than overwritten, so any signed proof of a refund you are owed is still in that file. Your credits are held by each DVM, not by this file, so re-reading a balance will restore what the CLI shows.`
|
|
1714
|
-
);
|
|
1715
|
-
}
|
|
1716
|
-
function safeMicro(value) {
|
|
1717
|
-
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : void 0;
|
|
1718
|
-
}
|
|
1719
|
-
function mutateCredit(location, mutate) {
|
|
1720
|
-
withCreditLock(() => {
|
|
1721
|
-
const store = loadStore();
|
|
1722
|
-
const key = normalizeCreditEndpoint(location.endpoint);
|
|
1723
|
-
const record = findDvmLocated(store.credits, {
|
|
1724
|
-
endpoint: key,
|
|
1725
|
-
...location.identity ? { identity: location.identity } : {},
|
|
1726
|
-
where: (c) => c.callerPubkey === location.callerPubkey
|
|
1727
|
-
});
|
|
1728
|
-
if (!record) return;
|
|
1729
|
-
const locationChanged = attachDvmLocation(record, key, location.identity);
|
|
1730
|
-
if (mutate(record) === "skip") {
|
|
1731
|
-
if (locationChanged) saveStore(store);
|
|
1732
|
-
return;
|
|
1733
|
-
}
|
|
1734
|
-
record.updatedAt = Date.now();
|
|
1735
|
-
saveStore(store);
|
|
1736
|
-
});
|
|
1737
|
-
}
|
|
1738
|
-
var DEFAULT_LOCK_STALE_MS = 1e4;
|
|
1739
|
-
var DEFAULT_LOCK_RETRIES = 600;
|
|
1740
|
-
var DEFAULT_LOCK_RETRY_INTERVAL_MS = 25;
|
|
1741
|
-
var lockDepth = 0;
|
|
1742
|
-
function acquireCreditLock(opts) {
|
|
1743
|
-
ensureConfigDir();
|
|
1744
|
-
const lockPath = `${CREDITS_FILE}.lock`;
|
|
1745
|
-
const stale = opts.stale ?? DEFAULT_LOCK_STALE_MS;
|
|
1746
|
-
const retries = opts.retries ?? DEFAULT_LOCK_RETRIES;
|
|
1747
|
-
const interval = opts.retryIntervalMs ?? DEFAULT_LOCK_RETRY_INTERVAL_MS;
|
|
1748
|
-
let lastError;
|
|
1749
|
-
for (let attempt = 0; attempt <= retries; attempt += 1) {
|
|
1750
|
-
try {
|
|
1751
|
-
return lockfile.lockSync(CREDITS_FILE, {
|
|
1752
|
-
lockfilePath: lockPath,
|
|
1753
|
-
stale,
|
|
1754
|
-
realpath: false
|
|
1755
|
-
});
|
|
1756
|
-
} catch (err) {
|
|
1757
|
-
lastError = err;
|
|
1758
|
-
if (err.code !== "ELOCKED") {
|
|
1759
|
-
const detail = err instanceof Error ? err.message : String(err);
|
|
1760
|
-
const configDir = dirname(lockPath);
|
|
1761
|
-
throw new DvmError(
|
|
1762
|
-
"credit_store_lock_failed",
|
|
1763
|
-
`Could not create the prepaid-credit lock at ${lockPath}: ${detail}`,
|
|
1764
|
-
`This is not contention \u2014 nothing is holding the lock. Check that ${configDir} exists and is writable by you ('ls -ld ${configDir}'), then retry.`,
|
|
1765
|
-
{
|
|
1766
|
-
lock_file: lockPath,
|
|
1767
|
-
config_dir: configDir,
|
|
1768
|
-
underlying: detail,
|
|
1769
|
-
errno_code: err.code
|
|
1770
|
-
},
|
|
1771
|
-
`The dvm CLI could not create the small lock file it uses to keep two commands from writing your credit record at once. Its folder looks unwritable \u2014 nothing about your credits is wrong, and nothing was changed.`
|
|
1772
|
-
);
|
|
1773
|
-
}
|
|
1774
|
-
if (attempt === retries) break;
|
|
1775
|
-
sleepSync(interval);
|
|
1776
|
-
}
|
|
1777
|
-
}
|
|
1778
|
-
throw new DvmError(
|
|
1779
|
-
"credit_store_locked",
|
|
1780
|
-
`Could not acquire the prepaid-credit lock at ${lockPath} within the retry budget.`,
|
|
1781
|
-
`Another dvm CLI may be writing to ${CREDITS_FILE}. Wait a moment and retry. If no other dvm CLI is running, remove ${lockPath} and try again.`,
|
|
1782
|
-
{
|
|
1783
|
-
lock_file: lockPath,
|
|
1784
|
-
underlying: lastError instanceof Error ? lastError.message : String(lastError)
|
|
1785
|
-
}
|
|
1786
|
-
);
|
|
1787
|
-
}
|
|
1788
|
-
function rejectAsyncBody(result) {
|
|
1789
|
-
if (typeof result?.then !== "function") {
|
|
1790
|
-
return result;
|
|
1791
|
-
}
|
|
1792
|
-
throw new DvmError(
|
|
1793
|
-
"credit_store_lock_misuse",
|
|
1794
|
-
"withCreditLock was called with an asynchronous body. The lock is released when the body returns, so a promise's writes would run unprotected.",
|
|
1795
|
-
"Make the locked body synchronous \u2014 the credit store's whole surface is sync \u2014 and do any awaiting outside the lock."
|
|
1796
|
-
);
|
|
1797
|
-
}
|
|
1798
|
-
function sleepSync(ms) {
|
|
1799
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
1800
|
-
}
|
|
1801
|
-
|
|
1802
|
-
export {
|
|
1803
|
-
RAIL_FLAGS,
|
|
1804
|
-
maxAdvertisedMicro,
|
|
1805
|
-
withinAdvertisedTolerance,
|
|
1806
|
-
SUPPORTED_PROTOCOL_VERSION,
|
|
1807
|
-
parseCreditTerms,
|
|
1808
|
-
parseCreditMenu,
|
|
1809
|
-
normalizeCreditEndpoint,
|
|
1810
|
-
dvmLocationIdentity,
|
|
1811
|
-
fundingReachableAt,
|
|
1812
|
-
pendingDrainFor,
|
|
1813
|
-
pendingDrainById,
|
|
1814
|
-
loadCredit,
|
|
1815
|
-
clearCreditSelection,
|
|
1816
|
-
readCompletedCreditHistory,
|
|
1817
|
-
listCredits,
|
|
1818
|
-
listImplicitCredits,
|
|
1819
|
-
loadImplicitCredits,
|
|
1820
|
-
markImplicitCreditLost,
|
|
1821
|
-
listFundingMenuSnapshots,
|
|
1822
|
-
recordFundingMenuSnapshot,
|
|
1823
|
-
listPendingFundings,
|
|
1824
|
-
loadPendingFunding,
|
|
1825
|
-
recordPendingFunding,
|
|
1826
|
-
clearPendingFunding,
|
|
1827
|
-
recordFundResponse,
|
|
1828
|
-
recordBalanceResponse,
|
|
1829
|
-
applyReceiptCredit,
|
|
1830
|
-
applyImplicitReceiptCredit,
|
|
1831
|
-
ensureProvisionalCredit,
|
|
1832
|
-
markCredit,
|
|
1833
|
-
loadCreditX402Binding,
|
|
1834
|
-
recordCreditX402Instrument,
|
|
1835
|
-
recordRefusedDrawBalance,
|
|
1836
|
-
markRefillRailUnavailable,
|
|
1837
|
-
markRefillDrainNoticed,
|
|
1838
|
-
clearRefillRailUnavailable,
|
|
1839
|
-
forgetCredit,
|
|
1840
|
-
recordPendingDrain,
|
|
1841
|
-
clearPendingDrain,
|
|
1842
|
-
recordDrainReceipts,
|
|
1843
|
-
settleDrainEvidence
|
|
1844
|
-
};
|