@dvmkit/sdk 0.4.0-rc.1 → 0.6.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +100 -0
- package/dist/{chunk-TXX4GOUB.js → chunk-7ZUKL4IV.js} +1 -1
- package/dist/chunk-AFV77S4C.js +258 -0
- package/dist/{chunk-2VGWYG4P.js → chunk-B2IROPQF.js} +446 -81
- package/dist/chunk-BQJIFRAZ.js +36 -0
- package/dist/{chunk-PB4FYZET.js → chunk-EQLA4FFY.js} +54 -4
- package/dist/{chunk-74PUEZJ4.js → chunk-GFY5FRX3.js} +1 -1
- package/dist/{chunk-O3QDFPKM.js → chunk-HCLVYZUZ.js} +2 -2
- package/dist/{chunk-K2YX2VRA.js → chunk-HHBDBMEO.js} +170 -43
- package/dist/{chunk-O5AT6S5T.js → chunk-IV6Q7NRW.js} +25 -195
- package/dist/{chunk-W2EQGDF5.js → chunk-LE74UEV7.js} +49 -8
- package/dist/{chunk-CE7XEHYC.js → chunk-N5RZ5HVB.js} +1 -1
- package/dist/chunk-OFVV37XC.js +57 -0
- package/dist/{chunk-5LR6P7WN.js → chunk-OQA7IHI4.js} +133 -213
- package/dist/{chunk-3MHE725Z.js → chunk-PAIM66YA.js} +90 -1
- package/dist/{chunk-6DOGTAHA.js → chunk-QR2EF2NI.js} +9 -43
- package/dist/{chunk-KXZUCCEY.js → chunk-RCM3P2HE.js} +5 -56
- package/dist/{chunk-R4DQZFAL.js → chunk-UOCZXO2Z.js} +3 -3
- package/dist/content-retention-BTGSQRfP.d.ts +78 -0
- package/dist/{credit-menu-B8_p_vUz.d.ts → credit-menu-BAsrRu3z.d.ts} +98 -5
- package/dist/{fx-DuaYHvVn.d.ts → fx-BgDmyTdd.d.ts} +1 -1
- package/dist/index.d.ts +5 -5
- package/dist/index.js +4 -3
- package/dist/internal/caller.d.ts +90 -22
- package/dist/internal/caller.js +40 -40
- package/dist/internal/index.js +1 -1
- package/dist/internal/server.d.ts +8 -99
- package/dist/internal/server.js +15 -11
- package/dist/{job-store-D6Rjj5Oi.d.ts → job-store-BVeF5_Jq.d.ts} +1 -1
- package/dist/{lightning-backend-BaeCPmx7.d.ts → lightning-backend-40fIzz1W.d.ts} +1 -1
- package/dist/{postgres-consumed-credential-store-CUQJKRSL.js → postgres-consumed-credential-store-BUVCAPLS.js} +13 -0
- package/dist/{postgres-job-store-VLXLWXUQ.js → postgres-job-store-VHLQBJZC.js} +3 -1
- package/dist/server/index.d.ts +28 -10
- package/dist/server/index.js +24 -10
- package/dist/{step-cache-Bi43A0JN.d.ts → step-cache-CPdH1hei.d.ts} +3 -3
- package/dist/{tempo-lifecycle-OTHV757Y.js → tempo-lifecycle-ZBBLFBMH.js} +5 -4
- package/dist/{tempo-wallet-32JWQZKQ.js → tempo-wallet-B5XXV3YH.js} +1 -1
- package/dist/testing/index.d.ts +9 -2
- package/dist/testing/index.js +6 -3
- package/dist/{usd-AZPVq4eU.d.ts → usd-AWlrwlM5.d.ts} +1 -1
- package/dist/{x402-CEDHGZDI.js → x402-R6HPBY5D.js} +5 -4
- package/package.json +3 -2
- package/recipes/postgres/financial-export-v1.sql +137 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
|
-
IMPLICIT_CREDIT_TTL_MS
|
|
3
|
-
|
|
2
|
+
IMPLICIT_CREDIT_TTL_MS,
|
|
3
|
+
isDrainReceipt
|
|
4
|
+
} from "./chunk-PAIM66YA.js";
|
|
4
5
|
import {
|
|
5
6
|
CREDITS_FILE,
|
|
6
7
|
ensureConfigDir
|
|
@@ -9,42 +10,6 @@ import {
|
|
|
9
10
|
DvmError
|
|
10
11
|
} from "./chunk-LIRAHP7B.js";
|
|
11
12
|
|
|
12
|
-
// src/lib/output.ts
|
|
13
|
-
import { writeSync } from "fs";
|
|
14
|
-
var _traceId = null;
|
|
15
|
-
function setOutputTraceId(id) {
|
|
16
|
-
_traceId = id;
|
|
17
|
-
}
|
|
18
|
-
function getOutputTraceId() {
|
|
19
|
-
return _traceId;
|
|
20
|
-
}
|
|
21
|
-
function json(data) {
|
|
22
|
-
const out = _traceId !== null && typeof data === "object" && data !== null && !Array.isArray(data) && !("trace_id" in data) ? { trace_id: _traceId, ...data } : data;
|
|
23
|
-
writeAllSync(1, JSON.stringify(out) + "\n");
|
|
24
|
-
}
|
|
25
|
-
function info(msg) {
|
|
26
|
-
writeAllSync(2, msg + "\n");
|
|
27
|
-
}
|
|
28
|
-
function raw(msg) {
|
|
29
|
-
writeAllSync(1, msg + "\n");
|
|
30
|
-
}
|
|
31
|
-
function progress(event) {
|
|
32
|
-
writeAllSync(2, JSON.stringify(event) + "\n");
|
|
33
|
-
}
|
|
34
|
-
function writeAllSync(fd, text) {
|
|
35
|
-
const buf = Buffer.from(text, "utf8");
|
|
36
|
-
let offset = 0;
|
|
37
|
-
while (offset < buf.length) {
|
|
38
|
-
try {
|
|
39
|
-
offset += writeSync(fd, buf, offset, buf.length - offset);
|
|
40
|
-
} catch (err) {
|
|
41
|
-
const code = err.code;
|
|
42
|
-
if (code === "EAGAIN" || code === "EINTR") continue;
|
|
43
|
-
throw err;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
13
|
// src/lib/payment-rails/types.ts
|
|
49
14
|
var RAIL_FLAGS = ["cashu", "tempo", "x402"];
|
|
50
15
|
var ADVERTISED_AMOUNT_TOLERANCE_BPS = 1000n;
|
|
@@ -57,9 +22,9 @@ function withinAdvertisedTolerance(advertisedMicro, localMicro) {
|
|
|
57
22
|
|
|
58
23
|
// src/lib/transport/types.ts
|
|
59
24
|
var SUPPORTED_PROTOCOL_VERSION = 1;
|
|
60
|
-
function parseCreditTerms(
|
|
61
|
-
if (!
|
|
62
|
-
const credit =
|
|
25
|
+
function parseCreditTerms(raw) {
|
|
26
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
|
|
27
|
+
const credit = raw;
|
|
63
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")
|
|
64
29
|
return void 0;
|
|
65
30
|
const tempo = credit.tempo === void 0 ? void 0 : parseCreditTempoTerms(credit.tempo);
|
|
@@ -79,10 +44,10 @@ function parseCreditTerms(raw2) {
|
|
|
79
44
|
}
|
|
80
45
|
};
|
|
81
46
|
}
|
|
82
|
-
function parseCreditMenu(
|
|
83
|
-
const terms = parseCreditTerms(
|
|
47
|
+
function parseCreditMenu(raw) {
|
|
48
|
+
const terms = parseCreditTerms(raw);
|
|
84
49
|
if (!terms) return void 0;
|
|
85
|
-
const credit =
|
|
50
|
+
const credit = raw;
|
|
86
51
|
return {
|
|
87
52
|
...terms,
|
|
88
53
|
...typeof credit.credit_id === "string" && { credit_id: credit.credit_id },
|
|
@@ -93,9 +58,9 @@ function parseCreditMenu(raw2) {
|
|
|
93
58
|
...typeof credit.expiry_ms === "number" && { expiry_ms: credit.expiry_ms }
|
|
94
59
|
};
|
|
95
60
|
}
|
|
96
|
-
function parseCreditTempoTerms(
|
|
97
|
-
if (!
|
|
98
|
-
const block =
|
|
61
|
+
function parseCreditTempoTerms(raw) {
|
|
62
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
|
|
63
|
+
const block = raw;
|
|
99
64
|
if (!Array.isArray(block.methods) || !block.methods.every(isCreditTempoMethod)) return void 0;
|
|
100
65
|
if (block.withheld !== void 0 && (!Array.isArray(block.withheld) || !block.withheld.every(isCreditTempoWithheldMethod)))
|
|
101
66
|
return void 0;
|
|
@@ -113,15 +78,15 @@ function parseCreditTempoTerms(raw2) {
|
|
|
113
78
|
}
|
|
114
79
|
};
|
|
115
80
|
}
|
|
116
|
-
function isCreditTempoMethod(
|
|
117
|
-
return !!
|
|
81
|
+
function isCreditTempoMethod(raw) {
|
|
82
|
+
return !!raw && typeof raw === "object" && typeof raw.method === "string" && (raw.intent === "charge" || raw.intent === "session");
|
|
118
83
|
}
|
|
119
|
-
function isCreditTempoWithheldMethod(
|
|
120
|
-
return isCreditTempoMethod(
|
|
84
|
+
function isCreditTempoWithheldMethod(raw) {
|
|
85
|
+
return isCreditTempoMethod(raw) && typeof raw.reason === "string";
|
|
121
86
|
}
|
|
122
|
-
function parseCreditX402Terms(
|
|
123
|
-
if (!
|
|
124
|
-
const schemes =
|
|
87
|
+
function parseCreditX402Terms(raw) {
|
|
88
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
|
|
89
|
+
const schemes = raw.schemes;
|
|
125
90
|
if (!Array.isArray(schemes) || !schemes.every(
|
|
126
91
|
(entry) => !!entry && typeof entry === "object" && typeof entry.scheme === "string" && typeof entry.network === "string"
|
|
127
92
|
))
|
|
@@ -135,9 +100,348 @@ function parseCreditX402Terms(raw2) {
|
|
|
135
100
|
}
|
|
136
101
|
|
|
137
102
|
// src/lib/credit-store.ts
|
|
138
|
-
import { existsSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "fs";
|
|
103
|
+
import { existsSync, readFileSync as readFileSync2, renameSync, unlinkSync, writeFileSync } from "fs";
|
|
139
104
|
import { dirname } from "path";
|
|
140
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
|
|
141
445
|
function withCreditLock(fn, opts = {}) {
|
|
142
446
|
if (lockDepth > 0) {
|
|
143
447
|
lockDepth += 1;
|
|
@@ -260,6 +564,12 @@ function clearCreditSelection(endpoint, callerPubkey, creditId, identity) {
|
|
|
260
564
|
return structuredClone(record);
|
|
261
565
|
});
|
|
262
566
|
}
|
|
567
|
+
function readCompletedCreditHistory(query = {}) {
|
|
568
|
+
return withCreditLock(() => {
|
|
569
|
+
loadStore(true);
|
|
570
|
+
return readCreditHistoryPage(query);
|
|
571
|
+
});
|
|
572
|
+
}
|
|
263
573
|
function listCredits() {
|
|
264
574
|
return withCreditLock(() => [...loadStore().credits].sort((a, b) => b.updatedAt - a.updatedAt));
|
|
265
575
|
}
|
|
@@ -310,7 +620,13 @@ function markImplicitCreditLost(args) {
|
|
|
310
620
|
where: (candidate) => candidate.callerPubkey === args.callerPubkey && candidate.creditId === args.creditId
|
|
311
621
|
});
|
|
312
622
|
if (!record) return void 0;
|
|
313
|
-
attachDvmLocation(record, key, args.identity);
|
|
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
|
+
}
|
|
314
630
|
record.status = "lost";
|
|
315
631
|
record.lossReason = args.reason;
|
|
316
632
|
record.updatedAt = args.now ?? Date.now();
|
|
@@ -579,7 +895,13 @@ function applyImplicitReceiptCredit(args) {
|
|
|
579
895
|
saveStore(store);
|
|
580
896
|
return store.implicitCredits.includes(record) ? { credit: record } : {};
|
|
581
897
|
}
|
|
582
|
-
attachDvmLocation(record, key, args.identity);
|
|
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
|
+
}
|
|
583
905
|
let mismatch;
|
|
584
906
|
const sequential = args.credit.ledger_seq === record.ledgerSeq + 1;
|
|
585
907
|
if (sequential && args.outcome === "completed" && record.status === "active") {
|
|
@@ -756,6 +1078,9 @@ function forgetCredit(endpoint, callerPubkey, identity) {
|
|
|
756
1078
|
where: (c) => c.callerPubkey === callerPubkey
|
|
757
1079
|
});
|
|
758
1080
|
if (!record) return;
|
|
1081
|
+
archiveCompletedCreditRecords(
|
|
1082
|
+
(record.drainEvidence ?? []).filter((bundle) => bundle.settledAt !== void 0).map((bundle) => completedRefund(record, bundle))
|
|
1083
|
+
);
|
|
759
1084
|
saveStore({ ...store, credits: store.credits.filter((c) => c !== record) });
|
|
760
1085
|
});
|
|
761
1086
|
}
|
|
@@ -798,6 +1123,11 @@ function recordDrainReceipts(args) {
|
|
|
798
1123
|
mutateCredit(mutationAt(endpoint, callerPubkey, identity), (record) => {
|
|
799
1124
|
const evidence = record.drainEvidence ??= [];
|
|
800
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
|
+
}
|
|
801
1131
|
if (!bundle) {
|
|
802
1132
|
bundle = { drainId, creditId, receipts: [], collectedAt: now };
|
|
803
1133
|
evidence.push(bundle);
|
|
@@ -805,9 +1135,15 @@ function recordDrainReceipts(args) {
|
|
|
805
1135
|
bundle.creditId = creditId;
|
|
806
1136
|
bundle.receipts = receipts;
|
|
807
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
|
+
};
|
|
808
1144
|
if (read.anchor) bundle.anchor = read.anchor;
|
|
809
1145
|
else if (read.reachable) delete bundle.anchor;
|
|
810
|
-
record.drainEvidence =
|
|
1146
|
+
record.drainEvidence = evidence;
|
|
811
1147
|
return void 0;
|
|
812
1148
|
});
|
|
813
1149
|
}
|
|
@@ -818,7 +1154,8 @@ function settleDrainEvidence(args) {
|
|
|
818
1154
|
const bundle = evidence.find((e) => e.drainId === drainId);
|
|
819
1155
|
if (!bundle || bundle.settledAt !== void 0) return "skip";
|
|
820
1156
|
bundle.settledAt = now;
|
|
821
|
-
|
|
1157
|
+
if (args.settlement) bundle.settlement = args.settlement;
|
|
1158
|
+
record.drainEvidence = evidence;
|
|
822
1159
|
return void 0;
|
|
823
1160
|
});
|
|
824
1161
|
}
|
|
@@ -884,19 +1221,19 @@ function adoptCreditId(record, creditId) {
|
|
|
884
1221
|
var MAX_SETTLED_DRAIN_EVIDENCE = 8;
|
|
885
1222
|
var MAX_LOST_IMPLICIT_CREDITS = 8;
|
|
886
1223
|
var MAX_DATE_MS = 864e13;
|
|
887
|
-
function loadStore() {
|
|
1224
|
+
function loadStore(requireArchive = false) {
|
|
888
1225
|
if (!existsSync(CREDITS_FILE)) {
|
|
889
1226
|
return { credits: [], implicitCredits: [], pendingFundings: [], fundingMenuSnapshots: [] };
|
|
890
1227
|
}
|
|
891
|
-
let
|
|
1228
|
+
let raw;
|
|
892
1229
|
try {
|
|
893
|
-
|
|
1230
|
+
raw = readFileSync2(CREDITS_FILE, "utf-8");
|
|
894
1231
|
} catch (cause) {
|
|
895
1232
|
throw unreadableStore(cause);
|
|
896
1233
|
}
|
|
897
1234
|
let data;
|
|
898
1235
|
try {
|
|
899
|
-
const parsed = JSON.parse(
|
|
1236
|
+
const parsed = JSON.parse(raw);
|
|
900
1237
|
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
901
1238
|
throw new Error("the file parsed as JSON but is not a credit-store object");
|
|
902
1239
|
}
|
|
@@ -917,7 +1254,8 @@ function loadStore() {
|
|
|
917
1254
|
Array.isArray(data.pendingFundings) ? data.pendingFundings.filter((funding) => isObjectRecord(funding)).map(normalizeRailKey) : []
|
|
918
1255
|
);
|
|
919
1256
|
const implicit = dedupeImplicitCredits(
|
|
920
|
-
Array.isArray(data.implicitCredits) ? data.implicitCredits.filter(isStoredImplicitCredit) : []
|
|
1257
|
+
Array.isArray(data.implicitCredits) ? data.implicitCredits.filter(isStoredImplicitCredit) : [],
|
|
1258
|
+
requireArchive
|
|
921
1259
|
);
|
|
922
1260
|
const store = {
|
|
923
1261
|
credits: merged.credits,
|
|
@@ -925,14 +1263,16 @@ function loadStore() {
|
|
|
925
1263
|
pendingFundings: fundings.fundings,
|
|
926
1264
|
fundingMenuSnapshots: snapshots.snapshots
|
|
927
1265
|
};
|
|
1266
|
+
if (requireArchive) archiveStore(store);
|
|
928
1267
|
if (merged.changed || snapshots.changed || fundings.changed || implicit.changed) saveStore(store);
|
|
1268
|
+
else if (!requireArchive) archiveOrKeep(() => archiveStore(store));
|
|
929
1269
|
return store;
|
|
930
1270
|
}
|
|
931
1271
|
function isStoredImplicitCredit(value) {
|
|
932
1272
|
if (!isObjectRecord(value)) return false;
|
|
933
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);
|
|
934
1274
|
}
|
|
935
|
-
function dedupeImplicitCredits(credits) {
|
|
1275
|
+
function dedupeImplicitCredits(credits, requireArchive = false) {
|
|
936
1276
|
const merged = [];
|
|
937
1277
|
let changed = false;
|
|
938
1278
|
for (const candidate of credits) {
|
|
@@ -947,6 +1287,9 @@ function dedupeImplicitCredits(credits) {
|
|
|
947
1287
|
merged.push(candidate);
|
|
948
1288
|
continue;
|
|
949
1289
|
}
|
|
1290
|
+
if (!archiveOrKeep(() => archiveCompletedCreditRecords(
|
|
1291
|
+
[existing, candidate].filter((record) => record.status === "lost").map(completedLoss)
|
|
1292
|
+
), requireArchive)) return { credits, changed: false };
|
|
950
1293
|
changed = true;
|
|
951
1294
|
for (const origin of dvmLocationOrigins(candidate)) {
|
|
952
1295
|
attachDvmLocation(existing, origin, identity);
|
|
@@ -968,7 +1311,7 @@ function dedupeImplicitCredits(credits) {
|
|
|
968
1311
|
existing.releasedByJobId = candidate.releasedByJobId;
|
|
969
1312
|
}
|
|
970
1313
|
}
|
|
971
|
-
const capped = capLostImplicitCredits(merged);
|
|
1314
|
+
const capped = capLostImplicitCredits(merged, requireArchive);
|
|
972
1315
|
return { credits: capped.credits, changed: changed || capped.changed };
|
|
973
1316
|
}
|
|
974
1317
|
function ageImplicitCredits(credits, now) {
|
|
@@ -983,7 +1326,14 @@ function ageImplicitCredits(credits, now) {
|
|
|
983
1326
|
}
|
|
984
1327
|
return changed;
|
|
985
1328
|
}
|
|
986
|
-
function capLostImplicitCredits(credits) {
|
|
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 };
|
|
987
1337
|
const lostByOwner = /* @__PURE__ */ new Map();
|
|
988
1338
|
for (const credit of credits) {
|
|
989
1339
|
if (credit.status !== "lost") continue;
|
|
@@ -1286,19 +1636,39 @@ function isEvidenceish(value) {
|
|
|
1286
1636
|
function isDrainish(value) {
|
|
1287
1637
|
return typeof value === "object" && value !== null && typeof value.drainId === "string";
|
|
1288
1638
|
}
|
|
1289
|
-
function
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
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;
|
|
1297
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
|
+
);
|
|
1298
1663
|
return evidence.filter((e) => !dropped.has(e));
|
|
1299
1664
|
}
|
|
1300
1665
|
function saveStore(store) {
|
|
1301
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
|
+
}
|
|
1302
1672
|
const tmp = `${CREDITS_FILE}.${String(process.pid)}.tmp`;
|
|
1303
1673
|
writeFileSync(tmp, JSON.stringify(store, null, 2) + "\n", { mode: 384 });
|
|
1304
1674
|
try {
|
|
@@ -1430,12 +1800,6 @@ function sleepSync(ms) {
|
|
|
1430
1800
|
}
|
|
1431
1801
|
|
|
1432
1802
|
export {
|
|
1433
|
-
setOutputTraceId,
|
|
1434
|
-
getOutputTraceId,
|
|
1435
|
-
json,
|
|
1436
|
-
info,
|
|
1437
|
-
raw,
|
|
1438
|
-
progress,
|
|
1439
1803
|
RAIL_FLAGS,
|
|
1440
1804
|
maxAdvertisedMicro,
|
|
1441
1805
|
withinAdvertisedTolerance,
|
|
@@ -1449,6 +1813,7 @@ export {
|
|
|
1449
1813
|
pendingDrainById,
|
|
1450
1814
|
loadCredit,
|
|
1451
1815
|
clearCreditSelection,
|
|
1816
|
+
readCompletedCreditHistory,
|
|
1452
1817
|
listCredits,
|
|
1453
1818
|
listImplicitCredits,
|
|
1454
1819
|
loadImplicitCredits,
|