@dvmkit/sdk 0.3.4-rc.10 → 0.5.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-2KGG5ORW.js → chunk-7ZUKL4IV.js} +3 -3
- package/dist/{chunk-KXZUCCEY.js → chunk-A4YFWZ7R.js} +4 -55
- package/dist/{chunk-C2AGARLM.js → chunk-B2IROPQF.js} +450 -83
- package/dist/{chunk-MKI6OVW4.js → chunk-CZULM42S.js} +0 -51
- package/dist/{chunk-UZC4A4DM.js → chunk-EYTHFFFU.js} +2489 -213
- package/dist/{chunk-M4QDXD2A.js → chunk-FYRR2UEZ.js} +1 -1
- package/dist/{chunk-4UZN2GP5.js → chunk-GAOAYHZA.js} +209 -33
- package/dist/{chunk-YYQG4R7T.js → chunk-GFY5FRX3.js} +6 -4
- package/dist/{chunk-Z4BNLUZF.js → chunk-IWQHUSMO.js} +3 -105
- package/dist/{chunk-4J32DTLT.js → chunk-IXR5RFK7.js} +82 -4
- package/dist/{chunk-2SD2W2BG.js → chunk-JH3SX3LY.js} +252 -26
- package/dist/chunk-KA6VN5FK.js +245 -0
- package/dist/{chunk-YRDA7EWN.js → chunk-LE74UEV7.js} +61 -16
- package/dist/chunk-LIRAHP7B.js +53 -0
- package/dist/{chunk-MG67KXU7.js → chunk-MZRJR2CW.js} +4 -2
- package/dist/chunk-OFVV37XC.js +57 -0
- package/dist/{chunk-A4CM2BD3.js → chunk-PAIM66YA.js} +91 -2
- package/dist/{chunk-JDFLRNAZ.js → chunk-PB4FYZET.js} +67 -7
- package/dist/{chunk-BQ2NMWKE.js → chunk-PLG4XER6.js} +1 -1
- package/dist/chunk-RLTSCKRI.js +109 -0
- package/dist/{chunk-VAMSJRJT.js → chunk-TUA7PYH7.js} +27 -105
- package/dist/{chunk-O3QDFPKM.js → chunk-TZ2WKCDD.js} +1 -1
- package/dist/{chunk-Z3EHBW2G.js → chunk-UOCZXO2Z.js} +7 -5
- package/dist/{chunk-EAMZZ2B4.js → chunk-X7EZ5CLI.js} +59 -0
- package/dist/{chunk-CE7XEHYC.js → chunk-Z4KLECAC.js} +1 -1
- package/dist/{chunk-7X6UDVCQ.js → chunk-Z4ZT5LNT.js} +66 -2
- package/dist/{credit-ledger-A35WH2TI.js → credit-ledger-MI2H7NEC.js} +3 -1
- package/dist/{credit-menu-0-UtJ3hh.d.ts → credit-menu-CAfqe8IY.d.ts} +133 -7
- package/dist/{fx-DhyT1Cjh.d.ts → fx-D9nsN3n4.d.ts} +1 -1
- package/dist/index.d.ts +6 -6
- package/dist/index.js +4 -3
- package/dist/internal/caller.d.ts +80 -13
- package/dist/internal/caller.js +56 -54
- package/dist/internal/index.js +1 -1
- package/dist/internal/server.d.ts +11 -85
- package/dist/internal/server.js +21 -18
- package/dist/{job-store-D3CcZp_g.d.ts → job-store-Bx-VFRjg.d.ts} +1 -1
- package/dist/{lightning-backend-BhsyrBLa.d.ts → lightning-backend-BAx2Y8pP.d.ts} +1 -1
- package/dist/{memory-credit-ledger-2LBZVMKJ.js → memory-credit-ledger-W2BAUYJP.js} +4 -2
- package/dist/{mpp-setup-7CGBKNUP.js → mpp-setup-RF5FXGGV.js} +5 -1
- package/dist/{payout-reporter-ZMOA37GN.js → payout-reporter-3Y2U55ZX.js} +1 -1
- package/dist/{postgres-job-store-MSD2HZOI.js → postgres-job-store-VLXLWXUQ.js} +1 -1
- package/dist/{revenue-reporter-EKJOGQYR.js → revenue-reporter-MAERPYGK.js} +1 -1
- package/dist/server/index.d.ts +14 -10
- package/dist/server/index.js +57 -19
- package/dist/{step-cache-v2yhyhSZ.d.ts → step-cache-QG_h5-3G.d.ts} +1769 -1549
- package/dist/{tempo-charge-store-4WX7FPOV.js → tempo-charge-store-Q3D7KYMH.js} +1 -1
- package/dist/tempo-lifecycle-ZBBLFBMH.js +15 -0
- package/dist/{tempo-session-store-LCSDHB7G.js → tempo-session-store-ZRNCOX7P.js} +1 -1
- package/dist/{tempo-wallet-RCGBBAJH.js → tempo-wallet-B5XXV3YH.js} +4 -3
- package/dist/testing/index.d.ts +3 -3
- package/dist/{usd-Cu9G6xv2.d.ts → usd-EwRqCPF-.d.ts} +1 -1
- package/dist/{x402-S47Q77UE.js → x402-R6HPBY5D.js} +18 -14
- package/package.json +1 -1
- package/dist/chunk-YHN3WMMF.js +0 -122
- package/dist/tempo-lifecycle-TJ62A7UX.js +0 -13
|
@@ -1,47 +1,14 @@
|
|
|
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
|
-
DvmError,
|
|
7
7
|
ensureConfigDir
|
|
8
|
-
} from "./chunk-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
var _traceId = null;
|
|
13
|
-
function setOutputTraceId(id) {
|
|
14
|
-
_traceId = id;
|
|
15
|
-
}
|
|
16
|
-
function getOutputTraceId() {
|
|
17
|
-
return _traceId;
|
|
18
|
-
}
|
|
19
|
-
function json(data) {
|
|
20
|
-
const out = _traceId !== null && typeof data === "object" && data !== null && !Array.isArray(data) && !("trace_id" in data) ? { trace_id: _traceId, ...data } : data;
|
|
21
|
-
writeAllSync(1, JSON.stringify(out) + "\n");
|
|
22
|
-
}
|
|
23
|
-
function info(msg) {
|
|
24
|
-
writeAllSync(2, msg + "\n");
|
|
25
|
-
}
|
|
26
|
-
function raw(msg) {
|
|
27
|
-
writeAllSync(1, msg + "\n");
|
|
28
|
-
}
|
|
29
|
-
function progress(event) {
|
|
30
|
-
writeAllSync(2, JSON.stringify(event) + "\n");
|
|
31
|
-
}
|
|
32
|
-
function writeAllSync(fd, text) {
|
|
33
|
-
const buf = Buffer.from(text, "utf8");
|
|
34
|
-
let offset = 0;
|
|
35
|
-
while (offset < buf.length) {
|
|
36
|
-
try {
|
|
37
|
-
offset += writeSync(fd, buf, offset, buf.length - offset);
|
|
38
|
-
} catch (err) {
|
|
39
|
-
const code = err.code;
|
|
40
|
-
if (code === "EAGAIN" || code === "EINTR") continue;
|
|
41
|
-
throw err;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
}
|
|
8
|
+
} from "./chunk-CZULM42S.js";
|
|
9
|
+
import {
|
|
10
|
+
DvmError
|
|
11
|
+
} from "./chunk-LIRAHP7B.js";
|
|
45
12
|
|
|
46
13
|
// src/lib/payment-rails/types.ts
|
|
47
14
|
var RAIL_FLAGS = ["cashu", "tempo", "x402"];
|
|
@@ -55,9 +22,9 @@ function withinAdvertisedTolerance(advertisedMicro, localMicro) {
|
|
|
55
22
|
|
|
56
23
|
// src/lib/transport/types.ts
|
|
57
24
|
var SUPPORTED_PROTOCOL_VERSION = 1;
|
|
58
|
-
function parseCreditTerms(
|
|
59
|
-
if (!
|
|
60
|
-
const credit =
|
|
25
|
+
function parseCreditTerms(raw) {
|
|
26
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
|
|
27
|
+
const credit = raw;
|
|
61
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")
|
|
62
29
|
return void 0;
|
|
63
30
|
const tempo = credit.tempo === void 0 ? void 0 : parseCreditTempoTerms(credit.tempo);
|
|
@@ -77,10 +44,10 @@ function parseCreditTerms(raw2) {
|
|
|
77
44
|
}
|
|
78
45
|
};
|
|
79
46
|
}
|
|
80
|
-
function parseCreditMenu(
|
|
81
|
-
const terms = parseCreditTerms(
|
|
47
|
+
function parseCreditMenu(raw) {
|
|
48
|
+
const terms = parseCreditTerms(raw);
|
|
82
49
|
if (!terms) return void 0;
|
|
83
|
-
const credit =
|
|
50
|
+
const credit = raw;
|
|
84
51
|
return {
|
|
85
52
|
...terms,
|
|
86
53
|
...typeof credit.credit_id === "string" && { credit_id: credit.credit_id },
|
|
@@ -91,9 +58,9 @@ function parseCreditMenu(raw2) {
|
|
|
91
58
|
...typeof credit.expiry_ms === "number" && { expiry_ms: credit.expiry_ms }
|
|
92
59
|
};
|
|
93
60
|
}
|
|
94
|
-
function parseCreditTempoTerms(
|
|
95
|
-
if (!
|
|
96
|
-
const block =
|
|
61
|
+
function parseCreditTempoTerms(raw) {
|
|
62
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
|
|
63
|
+
const block = raw;
|
|
97
64
|
if (!Array.isArray(block.methods) || !block.methods.every(isCreditTempoMethod)) return void 0;
|
|
98
65
|
if (block.withheld !== void 0 && (!Array.isArray(block.withheld) || !block.withheld.every(isCreditTempoWithheldMethod)))
|
|
99
66
|
return void 0;
|
|
@@ -111,15 +78,15 @@ function parseCreditTempoTerms(raw2) {
|
|
|
111
78
|
}
|
|
112
79
|
};
|
|
113
80
|
}
|
|
114
|
-
function isCreditTempoMethod(
|
|
115
|
-
return !!
|
|
81
|
+
function isCreditTempoMethod(raw) {
|
|
82
|
+
return !!raw && typeof raw === "object" && typeof raw.method === "string" && (raw.intent === "charge" || raw.intent === "session");
|
|
116
83
|
}
|
|
117
|
-
function isCreditTempoWithheldMethod(
|
|
118
|
-
return isCreditTempoMethod(
|
|
84
|
+
function isCreditTempoWithheldMethod(raw) {
|
|
85
|
+
return isCreditTempoMethod(raw) && typeof raw.reason === "string";
|
|
119
86
|
}
|
|
120
|
-
function parseCreditX402Terms(
|
|
121
|
-
if (!
|
|
122
|
-
const schemes =
|
|
87
|
+
function parseCreditX402Terms(raw) {
|
|
88
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
|
|
89
|
+
const schemes = raw.schemes;
|
|
123
90
|
if (!Array.isArray(schemes) || !schemes.every(
|
|
124
91
|
(entry) => !!entry && typeof entry === "object" && typeof entry.scheme === "string" && typeof entry.network === "string"
|
|
125
92
|
))
|
|
@@ -133,9 +100,348 @@ function parseCreditX402Terms(raw2) {
|
|
|
133
100
|
}
|
|
134
101
|
|
|
135
102
|
// src/lib/credit-store.ts
|
|
136
|
-
import { existsSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "fs";
|
|
103
|
+
import { existsSync, readFileSync as readFileSync2, renameSync, unlinkSync, writeFileSync } from "fs";
|
|
137
104
|
import { dirname } from "path";
|
|
138
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
|
|
139
445
|
function withCreditLock(fn, opts = {}) {
|
|
140
446
|
if (lockDepth > 0) {
|
|
141
447
|
lockDepth += 1;
|
|
@@ -258,6 +564,12 @@ function clearCreditSelection(endpoint, callerPubkey, creditId, identity) {
|
|
|
258
564
|
return structuredClone(record);
|
|
259
565
|
});
|
|
260
566
|
}
|
|
567
|
+
function readCompletedCreditHistory(query = {}) {
|
|
568
|
+
return withCreditLock(() => {
|
|
569
|
+
loadStore(true);
|
|
570
|
+
return readCreditHistoryPage(query);
|
|
571
|
+
});
|
|
572
|
+
}
|
|
261
573
|
function listCredits() {
|
|
262
574
|
return withCreditLock(() => [...loadStore().credits].sort((a, b) => b.updatedAt - a.updatedAt));
|
|
263
575
|
}
|
|
@@ -308,7 +620,13 @@ function markImplicitCreditLost(args) {
|
|
|
308
620
|
where: (candidate) => candidate.callerPubkey === args.callerPubkey && candidate.creditId === args.creditId
|
|
309
621
|
});
|
|
310
622
|
if (!record) return void 0;
|
|
311
|
-
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
|
+
}
|
|
312
630
|
record.status = "lost";
|
|
313
631
|
record.lossReason = args.reason;
|
|
314
632
|
record.updatedAt = args.now ?? Date.now();
|
|
@@ -577,7 +895,13 @@ function applyImplicitReceiptCredit(args) {
|
|
|
577
895
|
saveStore(store);
|
|
578
896
|
return store.implicitCredits.includes(record) ? { credit: record } : {};
|
|
579
897
|
}
|
|
580
|
-
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
|
+
}
|
|
581
905
|
let mismatch;
|
|
582
906
|
const sequential = args.credit.ledger_seq === record.ledgerSeq + 1;
|
|
583
907
|
if (sequential && args.outcome === "completed" && record.status === "active") {
|
|
@@ -754,6 +1078,9 @@ function forgetCredit(endpoint, callerPubkey, identity) {
|
|
|
754
1078
|
where: (c) => c.callerPubkey === callerPubkey
|
|
755
1079
|
});
|
|
756
1080
|
if (!record) return;
|
|
1081
|
+
archiveCompletedCreditRecords(
|
|
1082
|
+
(record.drainEvidence ?? []).filter((bundle) => bundle.settledAt !== void 0).map((bundle) => completedRefund(record, bundle))
|
|
1083
|
+
);
|
|
757
1084
|
saveStore({ ...store, credits: store.credits.filter((c) => c !== record) });
|
|
758
1085
|
});
|
|
759
1086
|
}
|
|
@@ -796,6 +1123,11 @@ function recordDrainReceipts(args) {
|
|
|
796
1123
|
mutateCredit(mutationAt(endpoint, callerPubkey, identity), (record) => {
|
|
797
1124
|
const evidence = record.drainEvidence ??= [];
|
|
798
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
|
+
}
|
|
799
1131
|
if (!bundle) {
|
|
800
1132
|
bundle = { drainId, creditId, receipts: [], collectedAt: now };
|
|
801
1133
|
evidence.push(bundle);
|
|
@@ -803,9 +1135,15 @@ function recordDrainReceipts(args) {
|
|
|
803
1135
|
bundle.creditId = creditId;
|
|
804
1136
|
bundle.receipts = receipts;
|
|
805
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
|
+
};
|
|
806
1144
|
if (read.anchor) bundle.anchor = read.anchor;
|
|
807
1145
|
else if (read.reachable) delete bundle.anchor;
|
|
808
|
-
record.drainEvidence =
|
|
1146
|
+
record.drainEvidence = evidence;
|
|
809
1147
|
return void 0;
|
|
810
1148
|
});
|
|
811
1149
|
}
|
|
@@ -816,7 +1154,8 @@ function settleDrainEvidence(args) {
|
|
|
816
1154
|
const bundle = evidence.find((e) => e.drainId === drainId);
|
|
817
1155
|
if (!bundle || bundle.settledAt !== void 0) return "skip";
|
|
818
1156
|
bundle.settledAt = now;
|
|
819
|
-
|
|
1157
|
+
if (args.settlement) bundle.settlement = args.settlement;
|
|
1158
|
+
record.drainEvidence = evidence;
|
|
820
1159
|
return void 0;
|
|
821
1160
|
});
|
|
822
1161
|
}
|
|
@@ -882,19 +1221,19 @@ function adoptCreditId(record, creditId) {
|
|
|
882
1221
|
var MAX_SETTLED_DRAIN_EVIDENCE = 8;
|
|
883
1222
|
var MAX_LOST_IMPLICIT_CREDITS = 8;
|
|
884
1223
|
var MAX_DATE_MS = 864e13;
|
|
885
|
-
function loadStore() {
|
|
1224
|
+
function loadStore(requireArchive = false) {
|
|
886
1225
|
if (!existsSync(CREDITS_FILE)) {
|
|
887
1226
|
return { credits: [], implicitCredits: [], pendingFundings: [], fundingMenuSnapshots: [] };
|
|
888
1227
|
}
|
|
889
|
-
let
|
|
1228
|
+
let raw;
|
|
890
1229
|
try {
|
|
891
|
-
|
|
1230
|
+
raw = readFileSync2(CREDITS_FILE, "utf-8");
|
|
892
1231
|
} catch (cause) {
|
|
893
1232
|
throw unreadableStore(cause);
|
|
894
1233
|
}
|
|
895
1234
|
let data;
|
|
896
1235
|
try {
|
|
897
|
-
const parsed = JSON.parse(
|
|
1236
|
+
const parsed = JSON.parse(raw);
|
|
898
1237
|
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
899
1238
|
throw new Error("the file parsed as JSON but is not a credit-store object");
|
|
900
1239
|
}
|
|
@@ -915,7 +1254,8 @@ function loadStore() {
|
|
|
915
1254
|
Array.isArray(data.pendingFundings) ? data.pendingFundings.filter((funding) => isObjectRecord(funding)).map(normalizeRailKey) : []
|
|
916
1255
|
);
|
|
917
1256
|
const implicit = dedupeImplicitCredits(
|
|
918
|
-
Array.isArray(data.implicitCredits) ? data.implicitCredits.filter(isStoredImplicitCredit) : []
|
|
1257
|
+
Array.isArray(data.implicitCredits) ? data.implicitCredits.filter(isStoredImplicitCredit) : [],
|
|
1258
|
+
requireArchive
|
|
919
1259
|
);
|
|
920
1260
|
const store = {
|
|
921
1261
|
credits: merged.credits,
|
|
@@ -923,14 +1263,16 @@ function loadStore() {
|
|
|
923
1263
|
pendingFundings: fundings.fundings,
|
|
924
1264
|
fundingMenuSnapshots: snapshots.snapshots
|
|
925
1265
|
};
|
|
1266
|
+
if (requireArchive) archiveStore(store);
|
|
926
1267
|
if (merged.changed || snapshots.changed || fundings.changed || implicit.changed) saveStore(store);
|
|
1268
|
+
else if (!requireArchive) archiveOrKeep(() => archiveStore(store));
|
|
927
1269
|
return store;
|
|
928
1270
|
}
|
|
929
1271
|
function isStoredImplicitCredit(value) {
|
|
930
1272
|
if (!isObjectRecord(value)) return false;
|
|
931
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);
|
|
932
1274
|
}
|
|
933
|
-
function dedupeImplicitCredits(credits) {
|
|
1275
|
+
function dedupeImplicitCredits(credits, requireArchive = false) {
|
|
934
1276
|
const merged = [];
|
|
935
1277
|
let changed = false;
|
|
936
1278
|
for (const candidate of credits) {
|
|
@@ -945,6 +1287,9 @@ function dedupeImplicitCredits(credits) {
|
|
|
945
1287
|
merged.push(candidate);
|
|
946
1288
|
continue;
|
|
947
1289
|
}
|
|
1290
|
+
if (!archiveOrKeep(() => archiveCompletedCreditRecords(
|
|
1291
|
+
[existing, candidate].filter((record) => record.status === "lost").map(completedLoss)
|
|
1292
|
+
), requireArchive)) return { credits, changed: false };
|
|
948
1293
|
changed = true;
|
|
949
1294
|
for (const origin of dvmLocationOrigins(candidate)) {
|
|
950
1295
|
attachDvmLocation(existing, origin, identity);
|
|
@@ -966,7 +1311,7 @@ function dedupeImplicitCredits(credits) {
|
|
|
966
1311
|
existing.releasedByJobId = candidate.releasedByJobId;
|
|
967
1312
|
}
|
|
968
1313
|
}
|
|
969
|
-
const capped = capLostImplicitCredits(merged);
|
|
1314
|
+
const capped = capLostImplicitCredits(merged, requireArchive);
|
|
970
1315
|
return { credits: capped.credits, changed: changed || capped.changed };
|
|
971
1316
|
}
|
|
972
1317
|
function ageImplicitCredits(credits, now) {
|
|
@@ -981,7 +1326,14 @@ function ageImplicitCredits(credits, now) {
|
|
|
981
1326
|
}
|
|
982
1327
|
return changed;
|
|
983
1328
|
}
|
|
984
|
-
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 };
|
|
985
1337
|
const lostByOwner = /* @__PURE__ */ new Map();
|
|
986
1338
|
for (const credit of credits) {
|
|
987
1339
|
if (credit.status !== "lost") continue;
|
|
@@ -1284,19 +1636,39 @@ function isEvidenceish(value) {
|
|
|
1284
1636
|
function isDrainish(value) {
|
|
1285
1637
|
return typeof value === "object" && value !== null && typeof value.drainId === "string";
|
|
1286
1638
|
}
|
|
1287
|
-
function
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
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;
|
|
1295
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
|
+
);
|
|
1296
1663
|
return evidence.filter((e) => !dropped.has(e));
|
|
1297
1664
|
}
|
|
1298
1665
|
function saveStore(store) {
|
|
1299
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
|
+
}
|
|
1300
1672
|
const tmp = `${CREDITS_FILE}.${String(process.pid)}.tmp`;
|
|
1301
1673
|
writeFileSync(tmp, JSON.stringify(store, null, 2) + "\n", { mode: 384 });
|
|
1302
1674
|
try {
|
|
@@ -1428,12 +1800,6 @@ function sleepSync(ms) {
|
|
|
1428
1800
|
}
|
|
1429
1801
|
|
|
1430
1802
|
export {
|
|
1431
|
-
setOutputTraceId,
|
|
1432
|
-
getOutputTraceId,
|
|
1433
|
-
json,
|
|
1434
|
-
info,
|
|
1435
|
-
raw,
|
|
1436
|
-
progress,
|
|
1437
1803
|
RAIL_FLAGS,
|
|
1438
1804
|
maxAdvertisedMicro,
|
|
1439
1805
|
withinAdvertisedTolerance,
|
|
@@ -1447,6 +1813,7 @@ export {
|
|
|
1447
1813
|
pendingDrainById,
|
|
1448
1814
|
loadCredit,
|
|
1449
1815
|
clearCreditSelection,
|
|
1816
|
+
readCompletedCreditHistory,
|
|
1450
1817
|
listCredits,
|
|
1451
1818
|
listImplicitCredits,
|
|
1452
1819
|
loadImplicitCredits,
|