@hazbase/simplicity 0.1.1 → 0.2.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.
Files changed (39) hide show
  1. package/README.md +143 -1531
  2. package/dist/cli.js +1128 -1
  3. package/dist/client/SimplicityClient.d.ts +30 -3
  4. package/dist/client/SimplicityClient.js +28 -0
  5. package/dist/core/lineage.d.ts +18 -0
  6. package/dist/core/lineage.js +30 -0
  7. package/dist/core/reporting.d.ts +22 -0
  8. package/dist/core/reporting.js +40 -0
  9. package/dist/core/schnorr.d.ts +3 -3
  10. package/dist/core/schnorr.js +19 -7
  11. package/dist/core/types.d.ts +256 -5
  12. package/dist/docs/definitions/fund-capital-call-refund-only.simf +36 -2
  13. package/dist/docs/definitions/receivable-definition.json +10 -0
  14. package/dist/docs/definitions/receivable-funding-claim.json +13 -0
  15. package/dist/docs/definitions/receivable-funding-claim.simf +58 -0
  16. package/dist/docs/definitions/receivable-repayment-claim-partial.json +13 -0
  17. package/dist/docs/definitions/receivable-repayment-claim.json +13 -0
  18. package/dist/docs/definitions/receivable-repayment-claim.simf +59 -0
  19. package/dist/docs/definitions/receivable-state-funded.json +21 -0
  20. package/dist/docs/definitions/receivable-state-originated.json +20 -0
  21. package/dist/docs/definitions/receivable-state-partially-repaid.json +21 -0
  22. package/dist/docs/definitions/receivable-state-repaid.json +21 -0
  23. package/dist/domain/bond.d.ts +82 -15
  24. package/dist/domain/bond.js +159 -10
  25. package/dist/domain/bondValidation.d.ts +31 -1
  26. package/dist/domain/bondValidation.js +73 -9
  27. package/dist/domain/fund.d.ts +451 -68
  28. package/dist/domain/fund.js +460 -88
  29. package/dist/domain/fundValidation.d.ts +33 -3
  30. package/dist/domain/fundValidation.js +137 -5
  31. package/dist/domain/policies.d.ts +13 -0
  32. package/dist/domain/policies.js +50 -30
  33. package/dist/domain/receivable.d.ts +825 -0
  34. package/dist/domain/receivable.js +1385 -0
  35. package/dist/domain/receivableValidation.d.ts +150 -0
  36. package/dist/domain/receivableValidation.js +874 -0
  37. package/dist/index.d.ts +5 -3
  38. package/dist/index.js +55 -3
  39. package/package.json +6 -1
@@ -0,0 +1,874 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.summarizeReceivableDefinition = summarizeReceivableDefinition;
4
+ exports.summarizeReceivableState = summarizeReceivableState;
5
+ exports.summarizeReceivableClosingDescriptor = summarizeReceivableClosingDescriptor;
6
+ exports.summarizeReceivableFundingClaimDescriptor = summarizeReceivableFundingClaimDescriptor;
7
+ exports.summarizeReceivableRepaymentClaimDescriptor = summarizeReceivableRepaymentClaimDescriptor;
8
+ exports.validateReceivableDefinition = validateReceivableDefinition;
9
+ exports.validateReceivableState = validateReceivableState;
10
+ exports.validateReceivableClosingDescriptor = validateReceivableClosingDescriptor;
11
+ exports.validateReceivableFundingClaimDescriptor = validateReceivableFundingClaimDescriptor;
12
+ exports.validateReceivableRepaymentClaimDescriptor = validateReceivableRepaymentClaimDescriptor;
13
+ exports.validateReceivableCrossChecks = validateReceivableCrossChecks;
14
+ exports.buildFundedReceivableState = buildFundedReceivableState;
15
+ exports.applyReceivableRepayment = applyReceivableRepayment;
16
+ exports.buildDefaultedReceivableState = buildDefaultedReceivableState;
17
+ exports.buildReceivableClosingDescriptor = buildReceivableClosingDescriptor;
18
+ exports.buildReceivableFundingClaimDescriptor = buildReceivableFundingClaimDescriptor;
19
+ exports.buildReceivableRepaymentClaimDescriptor = buildReceivableRepaymentClaimDescriptor;
20
+ exports.validateReceivableFundingClaimAgainstState = validateReceivableFundingClaimAgainstState;
21
+ exports.validateReceivableRepaymentClaimAgainstState = validateReceivableRepaymentClaimAgainstState;
22
+ exports.validateReceivableFundingTransition = validateReceivableFundingTransition;
23
+ exports.validateReceivableRepaymentTransition = validateReceivableRepaymentTransition;
24
+ exports.validateReceivableWriteOffTransition = validateReceivableWriteOffTransition;
25
+ exports.validateReceivableClosingAgainstState = validateReceivableClosingAgainstState;
26
+ exports.verifyReceivableStateHistory = verifyReceivableStateHistory;
27
+ const errors_1 = require("../core/errors");
28
+ const lineage_1 = require("../core/lineage");
29
+ const summary_1 = require("../core/summary");
30
+ function assertNonEmptyString(value, code, message) {
31
+ if (typeof value !== "string" || value.trim().length === 0) {
32
+ throw new errors_1.ValidationError(message, { code });
33
+ }
34
+ }
35
+ function assertFinitePositiveNumber(value, code, message) {
36
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
37
+ throw new errors_1.ValidationError(message, { code });
38
+ }
39
+ }
40
+ function assertPositiveInteger(value, code, message) {
41
+ if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) {
42
+ throw new errors_1.ValidationError(message, { code });
43
+ }
44
+ }
45
+ function assertFiniteNonNegativeNumber(value, code, message) {
46
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
47
+ throw new errors_1.ValidationError(message, { code });
48
+ }
49
+ }
50
+ function assertIsoDateLike(value, code, message) {
51
+ assertNonEmptyString(value, code, message);
52
+ if (Number.isNaN(Date.parse(value))) {
53
+ throw new errors_1.ValidationError(message, { code });
54
+ }
55
+ }
56
+ function assertHashHex(value, code, message) {
57
+ if (typeof value !== "string" || !/^[0-9a-f]{64}$/i.test(value)) {
58
+ throw new errors_1.ValidationError(message, { code });
59
+ }
60
+ }
61
+ function assertXonlyPubkey(value, code, message) {
62
+ if (typeof value !== "string" || !/^[0-9a-f]{64}$/i.test(value)) {
63
+ throw new errors_1.ValidationError(message, { code });
64
+ }
65
+ }
66
+ function assertReceivableStatus(value) {
67
+ if (value !== "ORIGINATED"
68
+ && value !== "FUNDED"
69
+ && value !== "PARTIALLY_REPAID"
70
+ && value !== "REPAID"
71
+ && value !== "DEFAULTED") {
72
+ throw new errors_1.ValidationError("receivable status must be ORIGINATED, FUNDED, PARTIALLY_REPAID, REPAID, or DEFAULTED", { code: "RECEIVABLE_STATUS_INVALID" });
73
+ }
74
+ }
75
+ function validateReceivableTransition(value) {
76
+ const transition = value;
77
+ if (transition?.type !== "ORIGINATE"
78
+ && transition?.type !== "FUND"
79
+ && transition?.type !== "REPAY"
80
+ && transition?.type !== "WRITE_OFF") {
81
+ throw new errors_1.ValidationError("lastTransition.type must be ORIGINATE, FUND, REPAY, or WRITE_OFF", {
82
+ code: "RECEIVABLE_TRANSITION_TYPE_INVALID",
83
+ });
84
+ }
85
+ assertFinitePositiveNumber(transition.amount, "RECEIVABLE_TRANSITION_AMOUNT_INVALID", "lastTransition.amount must be a positive finite number");
86
+ assertIsoDateLike(transition.at, "RECEIVABLE_TRANSITION_AT_INVALID", "lastTransition.at must be an ISO8601 string");
87
+ return {
88
+ type: transition.type,
89
+ amount: transition.amount,
90
+ at: transition.at,
91
+ };
92
+ }
93
+ function summarizeReceivableDefinition(definition) {
94
+ const canonicalJson = (0, summary_1.stableStringify)(definition);
95
+ return { canonicalJson, hash: (0, summary_1.sha256HexUtf8)(canonicalJson) };
96
+ }
97
+ function summarizeReceivableState(state) {
98
+ const canonicalJson = (0, summary_1.stableStringify)(state);
99
+ return { canonicalJson, hash: (0, summary_1.sha256HexUtf8)(canonicalJson) };
100
+ }
101
+ function summarizeReceivableClosingDescriptor(closing) {
102
+ const canonicalJson = (0, summary_1.stableStringify)(closing);
103
+ return { canonicalJson, hash: (0, summary_1.sha256HexUtf8)(canonicalJson) };
104
+ }
105
+ function summarizeReceivableFundingClaimDescriptor(claim) {
106
+ const canonicalJson = (0, summary_1.stableStringify)(claim);
107
+ return { canonicalJson, hash: (0, summary_1.sha256HexUtf8)(canonicalJson) };
108
+ }
109
+ function summarizeReceivableRepaymentClaimDescriptor(claim) {
110
+ const canonicalJson = (0, summary_1.stableStringify)(claim);
111
+ return { canonicalJson, hash: (0, summary_1.sha256HexUtf8)(canonicalJson) };
112
+ }
113
+ function validateReceivableDefinition(value) {
114
+ const definition = value;
115
+ assertNonEmptyString(definition?.receivableId, "RECEIVABLE_ID_INVALID", "receivableId must be a non-empty string");
116
+ assertNonEmptyString(definition?.originatorEntityId, "RECEIVABLE_ORIGINATOR_INVALID", "originatorEntityId must be a non-empty string");
117
+ assertNonEmptyString(definition?.debtorEntityId, "RECEIVABLE_DEBTOR_INVALID", "debtorEntityId must be a non-empty string");
118
+ assertNonEmptyString(definition?.currencyAssetId, "RECEIVABLE_CURRENCY_INVALID", "currencyAssetId must be a non-empty string");
119
+ assertFinitePositiveNumber(definition?.faceValue, "RECEIVABLE_FACE_VALUE_INVALID", "faceValue must be a positive finite number");
120
+ assertIsoDateLike(definition?.dueDate, "RECEIVABLE_DUE_DATE_INVALID", "dueDate must be an ISO8601 string");
121
+ assertXonlyPubkey(definition?.controllerXonly, "RECEIVABLE_CONTROLLER_INVALID", "controllerXonly must be a 32-byte xonly public key");
122
+ if (definition.originatorClaimantXonly !== undefined) {
123
+ assertXonlyPubkey(definition.originatorClaimantXonly, "RECEIVABLE_ORIGINATOR_CLAIMANT_INVALID", "originatorClaimantXonly must be a 32-byte xonly public key");
124
+ }
125
+ return {
126
+ receivableId: definition.receivableId,
127
+ originatorEntityId: definition.originatorEntityId,
128
+ debtorEntityId: definition.debtorEntityId,
129
+ currencyAssetId: definition.currencyAssetId,
130
+ faceValue: definition.faceValue,
131
+ dueDate: definition.dueDate,
132
+ controllerXonly: definition.controllerXonly,
133
+ ...(definition.originatorClaimantXonly ? { originatorClaimantXonly: definition.originatorClaimantXonly } : {}),
134
+ };
135
+ }
136
+ function validateReceivableState(value) {
137
+ const state = value;
138
+ assertNonEmptyString(state?.stateId, "RECEIVABLE_STATE_ID_INVALID", "stateId must be a non-empty string");
139
+ assertNonEmptyString(state?.receivableId, "RECEIVABLE_ID_INVALID", "receivableId must be a non-empty string");
140
+ assertNonEmptyString(state?.originatorEntityId, "RECEIVABLE_ORIGINATOR_INVALID", "originatorEntityId must be a non-empty string");
141
+ assertNonEmptyString(state?.debtorEntityId, "RECEIVABLE_DEBTOR_INVALID", "debtorEntityId must be a non-empty string");
142
+ assertNonEmptyString(state?.holderEntityId, "RECEIVABLE_HOLDER_INVALID", "holderEntityId must be a non-empty string");
143
+ assertNonEmptyString(state?.currencyAssetId, "RECEIVABLE_CURRENCY_INVALID", "currencyAssetId must be a non-empty string");
144
+ assertXonlyPubkey(state?.controllerXonly, "RECEIVABLE_CONTROLLER_INVALID", "controllerXonly must be a 32-byte xonly public key");
145
+ if (state.holderClaimantXonly !== undefined) {
146
+ assertXonlyPubkey(state.holderClaimantXonly, "RECEIVABLE_HOLDER_CLAIMANT_INVALID", "holderClaimantXonly must be a 32-byte xonly public key");
147
+ }
148
+ assertFinitePositiveNumber(state?.faceValue, "RECEIVABLE_FACE_VALUE_INVALID", "faceValue must be a positive finite number");
149
+ assertFiniteNonNegativeNumber(state?.outstandingAmount, "RECEIVABLE_OUTSTANDING_INVALID", "outstandingAmount must be a non-negative finite number");
150
+ assertFiniteNonNegativeNumber(state?.repaidAmount, "RECEIVABLE_REPAID_INVALID", "repaidAmount must be a non-negative finite number");
151
+ assertReceivableStatus(state?.status);
152
+ assertIsoDateLike(state?.createdAt, "RECEIVABLE_CREATED_AT_INVALID", "createdAt must be an ISO8601 string");
153
+ if (state.previousStateHash !== undefined && state.previousStateHash !== null) {
154
+ assertHashHex(state.previousStateHash, "RECEIVABLE_PREVIOUS_STATE_HASH_INVALID", "previousStateHash must be a 64-character hex string when provided");
155
+ }
156
+ const transition = state.lastTransition ? validateReceivableTransition(state.lastTransition) : undefined;
157
+ if (state.faceValue !== state.outstandingAmount + state.repaidAmount) {
158
+ throw new errors_1.ValidationError("faceValue must equal outstandingAmount + repaidAmount", {
159
+ code: "RECEIVABLE_ARITHMETIC_INVALID",
160
+ });
161
+ }
162
+ if (state.status === "ORIGINATED") {
163
+ if (state.repaidAmount !== 0 || state.outstandingAmount !== state.faceValue) {
164
+ throw new errors_1.ValidationError("ORIGINATED state must have zero repaidAmount and full outstandingAmount", {
165
+ code: "RECEIVABLE_STATUS_INVALID",
166
+ });
167
+ }
168
+ if (state.previousStateHash) {
169
+ throw new errors_1.ValidationError("ORIGINATED state must not set previousStateHash", {
170
+ code: "RECEIVABLE_PREVIOUS_STATE_HASH_INVALID",
171
+ });
172
+ }
173
+ if (transition && transition.type !== "ORIGINATE") {
174
+ throw new errors_1.ValidationError("ORIGINATED state may only carry an ORIGINATE transition", {
175
+ code: "RECEIVABLE_TRANSITION_TYPE_INVALID",
176
+ });
177
+ }
178
+ }
179
+ if (state.status === "FUNDED") {
180
+ if (!state.previousStateHash) {
181
+ throw new errors_1.ValidationError("FUNDED state must set previousStateHash", {
182
+ code: "RECEIVABLE_PREVIOUS_STATE_HASH_INVALID",
183
+ });
184
+ }
185
+ if (!transition || transition.type !== "FUND") {
186
+ throw new errors_1.ValidationError("FUNDED state must carry a FUND transition", {
187
+ code: "RECEIVABLE_TRANSITION_TYPE_INVALID",
188
+ });
189
+ }
190
+ }
191
+ if (state.status === "PARTIALLY_REPAID") {
192
+ if (state.repaidAmount <= 0 || state.outstandingAmount <= 0) {
193
+ throw new errors_1.ValidationError("PARTIALLY_REPAID state must have both positive repaid and outstanding amounts", {
194
+ code: "RECEIVABLE_STATUS_INVALID",
195
+ });
196
+ }
197
+ if (!state.previousStateHash) {
198
+ throw new errors_1.ValidationError("PARTIALLY_REPAID state must set previousStateHash", {
199
+ code: "RECEIVABLE_PREVIOUS_STATE_HASH_INVALID",
200
+ });
201
+ }
202
+ if (!transition || transition.type !== "REPAY") {
203
+ throw new errors_1.ValidationError("PARTIALLY_REPAID state must carry a REPAY transition", {
204
+ code: "RECEIVABLE_TRANSITION_TYPE_INVALID",
205
+ });
206
+ }
207
+ }
208
+ if (state.status === "REPAID") {
209
+ if (state.outstandingAmount !== 0 || state.repaidAmount !== state.faceValue) {
210
+ throw new errors_1.ValidationError("REPAID state must have zero outstandingAmount and full repaidAmount", {
211
+ code: "RECEIVABLE_STATUS_INVALID",
212
+ });
213
+ }
214
+ if (!state.previousStateHash) {
215
+ throw new errors_1.ValidationError("REPAID state must set previousStateHash", {
216
+ code: "RECEIVABLE_PREVIOUS_STATE_HASH_INVALID",
217
+ });
218
+ }
219
+ if (!transition || transition.type !== "REPAY") {
220
+ throw new errors_1.ValidationError("REPAID state must carry a REPAY transition", {
221
+ code: "RECEIVABLE_TRANSITION_TYPE_INVALID",
222
+ });
223
+ }
224
+ }
225
+ if (state.status === "DEFAULTED") {
226
+ if (state.outstandingAmount <= 0 || state.repaidAmount >= state.faceValue) {
227
+ throw new errors_1.ValidationError("DEFAULTED state must keep a positive outstandingAmount below faceValue", {
228
+ code: "RECEIVABLE_STATUS_INVALID",
229
+ });
230
+ }
231
+ if (!state.previousStateHash) {
232
+ throw new errors_1.ValidationError("DEFAULTED state must set previousStateHash", {
233
+ code: "RECEIVABLE_PREVIOUS_STATE_HASH_INVALID",
234
+ });
235
+ }
236
+ if (!transition || transition.type !== "WRITE_OFF") {
237
+ throw new errors_1.ValidationError("DEFAULTED state must carry a WRITE_OFF transition", {
238
+ code: "RECEIVABLE_TRANSITION_TYPE_INVALID",
239
+ });
240
+ }
241
+ }
242
+ return {
243
+ stateId: state.stateId,
244
+ receivableId: state.receivableId,
245
+ originatorEntityId: state.originatorEntityId,
246
+ debtorEntityId: state.debtorEntityId,
247
+ holderEntityId: state.holderEntityId,
248
+ currencyAssetId: state.currencyAssetId,
249
+ controllerXonly: state.controllerXonly,
250
+ ...(state.holderClaimantXonly ? { holderClaimantXonly: state.holderClaimantXonly } : {}),
251
+ faceValue: state.faceValue,
252
+ outstandingAmount: state.outstandingAmount,
253
+ repaidAmount: state.repaidAmount,
254
+ status: state.status,
255
+ createdAt: state.createdAt,
256
+ ...(state.previousStateHash !== undefined ? { previousStateHash: state.previousStateHash } : {}),
257
+ ...(transition ? { lastTransition: transition } : {}),
258
+ };
259
+ }
260
+ function validateReceivableClosingDescriptor(value) {
261
+ const closing = value;
262
+ assertNonEmptyString(closing?.closingId, "RECEIVABLE_CLOSING_ID_INVALID", "closingId must be a non-empty string");
263
+ assertNonEmptyString(closing?.receivableId, "RECEIVABLE_ID_INVALID", "receivableId must be a non-empty string");
264
+ assertHashHex(closing?.latestStateHash, "RECEIVABLE_LATEST_STATE_HASH_INVALID", "latestStateHash must be a 64-character hex string");
265
+ assertReceivableStatus(closing?.latestStatus);
266
+ assertNonEmptyString(closing?.holderEntityId, "RECEIVABLE_HOLDER_INVALID", "holderEntityId must be a non-empty string");
267
+ assertIsoDateLike(closing?.closedAt, "RECEIVABLE_CLOSED_AT_INVALID", "closedAt must be an ISO8601 string");
268
+ if (closing?.closingReason !== "REPAID"
269
+ && closing?.closingReason !== "DEFAULTED"
270
+ && closing?.closingReason !== "CANCELLED") {
271
+ throw new errors_1.ValidationError("closingReason must be REPAID, DEFAULTED, or CANCELLED", {
272
+ code: "RECEIVABLE_CLOSING_REASON_INVALID",
273
+ });
274
+ }
275
+ return {
276
+ closingId: closing.closingId,
277
+ receivableId: closing.receivableId,
278
+ latestStateHash: closing.latestStateHash,
279
+ latestStatus: closing.latestStatus,
280
+ holderEntityId: closing.holderEntityId,
281
+ closedAt: closing.closedAt,
282
+ closingReason: closing.closingReason,
283
+ };
284
+ }
285
+ function validateReceivableClaimKind(value) {
286
+ if (value !== "FUNDING" && value !== "REPAYMENT") {
287
+ throw new errors_1.ValidationError("claimKind must be FUNDING or REPAYMENT", {
288
+ code: "RECEIVABLE_CLAIM_KIND_INVALID",
289
+ });
290
+ }
291
+ return value;
292
+ }
293
+ function validateReceivableClaimDescriptorBase(value) {
294
+ const claim = value;
295
+ assertNonEmptyString(claim?.claimId, "RECEIVABLE_CLAIM_ID_INVALID", "claimId must be a non-empty string");
296
+ const claimKind = validateReceivableClaimKind(claim?.claimKind);
297
+ assertNonEmptyString(claim?.receivableId, "RECEIVABLE_ID_INVALID", "receivableId must be a non-empty string");
298
+ assertHashHex(claim?.currentStateHash, "RECEIVABLE_CURRENT_STATE_HASH_INVALID", "currentStateHash must be a 64-character hex string");
299
+ assertReceivableStatus(claim?.currentStatus);
300
+ assertNonEmptyString(claim?.payerEntityId, "RECEIVABLE_PAYER_INVALID", "payerEntityId must be a non-empty string");
301
+ assertNonEmptyString(claim?.payeeEntityId, "RECEIVABLE_PAYEE_INVALID", "payeeEntityId must be a non-empty string");
302
+ assertXonlyPubkey(claim?.claimantXonly, "RECEIVABLE_CLAIMANT_INVALID", "claimantXonly must be a 32-byte xonly public key");
303
+ assertNonEmptyString(claim?.currencyAssetId, "RECEIVABLE_CURRENCY_INVALID", "currencyAssetId must be a non-empty string");
304
+ assertPositiveInteger(claim?.amountSat, "RECEIVABLE_CLAIM_AMOUNT_INVALID", "amountSat must be a positive integer");
305
+ assertIsoDateLike(claim?.eventTimestamp, "RECEIVABLE_EVENT_TIMESTAMP_INVALID", "eventTimestamp must be an ISO8601 string");
306
+ return {
307
+ claimId: claim.claimId,
308
+ claimKind,
309
+ receivableId: claim.receivableId,
310
+ currentStateHash: claim.currentStateHash,
311
+ currentStatus: claim.currentStatus,
312
+ payerEntityId: claim.payerEntityId,
313
+ payeeEntityId: claim.payeeEntityId,
314
+ claimantXonly: claim.claimantXonly,
315
+ currencyAssetId: claim.currencyAssetId,
316
+ amountSat: claim.amountSat,
317
+ eventTimestamp: claim.eventTimestamp,
318
+ };
319
+ }
320
+ function validateReceivableFundingClaimDescriptor(value) {
321
+ const claim = validateReceivableClaimDescriptorBase(value);
322
+ if (claim.claimKind !== "FUNDING") {
323
+ throw new errors_1.ValidationError("funding claim descriptor must have claimKind=FUNDING", {
324
+ code: "RECEIVABLE_CLAIM_KIND_INVALID",
325
+ });
326
+ }
327
+ return {
328
+ ...claim,
329
+ claimKind: "FUNDING",
330
+ };
331
+ }
332
+ function validateReceivableRepaymentClaimDescriptor(value) {
333
+ const claim = validateReceivableClaimDescriptorBase(value);
334
+ if (claim.claimKind !== "REPAYMENT") {
335
+ throw new errors_1.ValidationError("repayment claim descriptor must have claimKind=REPAYMENT", {
336
+ code: "RECEIVABLE_CLAIM_KIND_INVALID",
337
+ });
338
+ }
339
+ return {
340
+ ...claim,
341
+ claimKind: "REPAYMENT",
342
+ };
343
+ }
344
+ function validateReceivableCrossChecks(definition, state) {
345
+ const result = {
346
+ receivableIdMatch: definition.receivableId === state.receivableId,
347
+ originatorMatch: definition.originatorEntityId === state.originatorEntityId,
348
+ debtorMatch: definition.debtorEntityId === state.debtorEntityId,
349
+ currencyMatch: definition.currencyAssetId === state.currencyAssetId,
350
+ controllerMatch: definition.controllerXonly === state.controllerXonly,
351
+ faceValueMatch: definition.faceValue === state.faceValue,
352
+ arithmeticValid: state.faceValue === state.outstandingAmount + state.repaidAmount,
353
+ };
354
+ if (!result.receivableIdMatch) {
355
+ throw new errors_1.ValidationError("Receivable definition and state receivableId do not match", {
356
+ code: "RECEIVABLE_ID_MISMATCH",
357
+ });
358
+ }
359
+ if (!result.originatorMatch) {
360
+ throw new errors_1.ValidationError("Receivable definition and state originatorEntityId do not match", {
361
+ code: "RECEIVABLE_ORIGINATOR_MISMATCH",
362
+ });
363
+ }
364
+ if (!result.debtorMatch) {
365
+ throw new errors_1.ValidationError("Receivable definition and state debtorEntityId do not match", {
366
+ code: "RECEIVABLE_DEBTOR_MISMATCH",
367
+ });
368
+ }
369
+ if (!result.currencyMatch) {
370
+ throw new errors_1.ValidationError("Receivable definition and state currencyAssetId do not match", {
371
+ code: "RECEIVABLE_CURRENCY_MISMATCH",
372
+ });
373
+ }
374
+ if (!result.controllerMatch) {
375
+ throw new errors_1.ValidationError("Receivable definition and state controllerXonly do not match", {
376
+ code: "RECEIVABLE_CONTROLLER_MISMATCH",
377
+ });
378
+ }
379
+ if (!result.faceValueMatch) {
380
+ throw new errors_1.ValidationError("Receivable definition and state faceValue do not match", {
381
+ code: "RECEIVABLE_FACE_VALUE_MISMATCH",
382
+ });
383
+ }
384
+ if (!result.arithmeticValid) {
385
+ throw new errors_1.ValidationError("Receivable state faceValue must equal outstandingAmount + repaidAmount", {
386
+ code: "RECEIVABLE_ARITHMETIC_INVALID",
387
+ });
388
+ }
389
+ return result;
390
+ }
391
+ function assertSameReceivableIdentity(previous, next) {
392
+ const participantConsistencyValid = previous.receivableId === next.receivableId
393
+ && previous.originatorEntityId === next.originatorEntityId
394
+ && previous.debtorEntityId === next.debtorEntityId
395
+ && previous.currencyAssetId === next.currencyAssetId
396
+ && previous.controllerXonly === next.controllerXonly
397
+ && previous.faceValue === next.faceValue;
398
+ if (!participantConsistencyValid) {
399
+ throw new errors_1.ValidationError("Receivable transition must preserve ids, currency, controller, and faceValue", {
400
+ code: "RECEIVABLE_TRANSITION_IDENTITY_INVALID",
401
+ });
402
+ }
403
+ return participantConsistencyValid;
404
+ }
405
+ function assertCreatedAtMonotonic(previous, next) {
406
+ const createdAtMonotonic = Date.parse(next.createdAt) >= Date.parse(previous.createdAt);
407
+ if (!createdAtMonotonic) {
408
+ throw new errors_1.ValidationError("Receivable transition must not move createdAt backwards", {
409
+ code: "RECEIVABLE_TRANSITION_CREATED_AT_INVALID",
410
+ });
411
+ }
412
+ return createdAtMonotonic;
413
+ }
414
+ function assertPreviousStateHashMatch(previous, next) {
415
+ const previousStateHashMatch = next.previousStateHash === summarizeReceivableState(previous).hash;
416
+ if (!previousStateHashMatch) {
417
+ throw new errors_1.ValidationError("Receivable transition previousStateHash does not match the previous state hash", {
418
+ code: "RECEIVABLE_PREVIOUS_STATE_HASH_INVALID",
419
+ });
420
+ }
421
+ return previousStateHashMatch;
422
+ }
423
+ function deriveClosingReasonFromStatus(status) {
424
+ if (status === "REPAID")
425
+ return "REPAID";
426
+ if (status === "DEFAULTED")
427
+ return "DEFAULTED";
428
+ throw new errors_1.ValidationError("Only REPAID or DEFAULTED receivables can derive a closing reason automatically", {
429
+ code: "RECEIVABLE_CLOSING_REASON_INVALID",
430
+ });
431
+ }
432
+ function buildFundedReceivableState(input) {
433
+ const previous = validateReceivableState(input.previous);
434
+ if (previous.status !== "ORIGINATED") {
435
+ throw new errors_1.ValidationError("Only ORIGINATED receivables can move into FUNDED", {
436
+ code: "RECEIVABLE_STATUS_INVALID",
437
+ });
438
+ }
439
+ return validateReceivableState({
440
+ ...previous,
441
+ stateId: input.stateId,
442
+ holderEntityId: input.holderEntityId,
443
+ holderClaimantXonly: input.holderEntityId === previous.holderEntityId
444
+ ? previous.holderClaimantXonly ?? previous.controllerXonly
445
+ : input.holderClaimantXonly ?? previous.controllerXonly,
446
+ status: "FUNDED",
447
+ createdAt: input.fundedAt,
448
+ previousStateHash: summarizeReceivableState(previous).hash,
449
+ lastTransition: {
450
+ type: "FUND",
451
+ amount: previous.outstandingAmount,
452
+ at: input.fundedAt,
453
+ },
454
+ });
455
+ }
456
+ function applyReceivableRepayment(input) {
457
+ const previous = validateReceivableState(input.previous);
458
+ if (previous.status !== "FUNDED" && previous.status !== "PARTIALLY_REPAID") {
459
+ throw new errors_1.ValidationError("Only FUNDED or PARTIALLY_REPAID receivables can be repaid", {
460
+ code: "RECEIVABLE_STATUS_INVALID",
461
+ });
462
+ }
463
+ assertFinitePositiveNumber(input.amount, "RECEIVABLE_TRANSITION_AMOUNT_INVALID", "repayment amount must be a positive finite number");
464
+ if (input.amount > previous.outstandingAmount) {
465
+ throw new errors_1.ValidationError("repayment amount must not exceed outstandingAmount", {
466
+ code: "RECEIVABLE_TRANSITION_AMOUNT_INVALID",
467
+ });
468
+ }
469
+ const outstandingAmount = previous.outstandingAmount - input.amount;
470
+ const repaidAmount = previous.repaidAmount + input.amount;
471
+ return validateReceivableState({
472
+ ...previous,
473
+ stateId: input.stateId,
474
+ outstandingAmount,
475
+ repaidAmount,
476
+ status: outstandingAmount === 0 ? "REPAID" : "PARTIALLY_REPAID",
477
+ createdAt: input.repaidAt,
478
+ previousStateHash: summarizeReceivableState(previous).hash,
479
+ lastTransition: {
480
+ type: "REPAY",
481
+ amount: input.amount,
482
+ at: input.repaidAt,
483
+ },
484
+ });
485
+ }
486
+ function buildDefaultedReceivableState(input) {
487
+ const previous = validateReceivableState(input.previous);
488
+ if (previous.status !== "FUNDED" && previous.status !== "PARTIALLY_REPAID") {
489
+ throw new errors_1.ValidationError("Only FUNDED or PARTIALLY_REPAID receivables can be written off", {
490
+ code: "RECEIVABLE_STATUS_INVALID",
491
+ });
492
+ }
493
+ const writeOffAmount = input.writeOffAmount ?? previous.outstandingAmount;
494
+ assertFinitePositiveNumber(writeOffAmount, "RECEIVABLE_TRANSITION_AMOUNT_INVALID", "writeOffAmount must be a positive finite number");
495
+ return validateReceivableState({
496
+ ...previous,
497
+ stateId: input.stateId,
498
+ status: "DEFAULTED",
499
+ createdAt: input.defaultedAt,
500
+ previousStateHash: summarizeReceivableState(previous).hash,
501
+ lastTransition: {
502
+ type: "WRITE_OFF",
503
+ amount: writeOffAmount,
504
+ at: input.defaultedAt,
505
+ },
506
+ });
507
+ }
508
+ function buildReceivableClosingDescriptor(input) {
509
+ const latestState = validateReceivableState(input.latestState);
510
+ const latestStateHash = summarizeReceivableState(latestState).hash;
511
+ return validateReceivableClosingDescriptor({
512
+ closingId: input.closingId,
513
+ receivableId: latestState.receivableId,
514
+ latestStateHash,
515
+ latestStatus: latestState.status,
516
+ holderEntityId: latestState.holderEntityId,
517
+ closedAt: input.closedAt,
518
+ closingReason: input.closingReason ?? deriveClosingReasonFromStatus(latestState.status),
519
+ });
520
+ }
521
+ function buildReceivableFundingClaimDescriptor(input) {
522
+ const definition = input.definition ? validateReceivableDefinition(input.definition) : undefined;
523
+ const currentState = validateReceivableState(input.currentState);
524
+ const currentStateHash = summarizeReceivableState(currentState).hash;
525
+ return validateReceivableFundingClaimDescriptor({
526
+ claimId: input.claimId,
527
+ claimKind: "FUNDING",
528
+ receivableId: currentState.receivableId,
529
+ currentStateHash,
530
+ currentStatus: currentState.status,
531
+ payerEntityId: input.payerEntityId ?? currentState.holderEntityId,
532
+ payeeEntityId: input.payeeEntityId ?? currentState.originatorEntityId,
533
+ claimantXonly: input.claimantXonly ?? definition?.originatorClaimantXonly ?? currentState.controllerXonly,
534
+ currencyAssetId: currentState.currencyAssetId,
535
+ amountSat: input.amountSat ?? currentState.lastTransition?.amount ?? currentState.outstandingAmount,
536
+ eventTimestamp: input.eventTimestamp ?? currentState.lastTransition?.at ?? currentState.createdAt,
537
+ });
538
+ }
539
+ function buildReceivableRepaymentClaimDescriptor(input) {
540
+ const currentState = validateReceivableState(input.currentState);
541
+ const currentStateHash = summarizeReceivableState(currentState).hash;
542
+ return validateReceivableRepaymentClaimDescriptor({
543
+ claimId: input.claimId,
544
+ claimKind: "REPAYMENT",
545
+ receivableId: currentState.receivableId,
546
+ currentStateHash,
547
+ currentStatus: currentState.status,
548
+ payerEntityId: input.payerEntityId ?? currentState.debtorEntityId,
549
+ payeeEntityId: input.payeeEntityId ?? currentState.holderEntityId,
550
+ claimantXonly: input.claimantXonly ?? currentState.holderClaimantXonly ?? currentState.controllerXonly,
551
+ currencyAssetId: currentState.currencyAssetId,
552
+ amountSat: input.amountSat ?? currentState.lastTransition?.amount ?? currentState.repaidAmount,
553
+ eventTimestamp: input.eventTimestamp ?? currentState.lastTransition?.at ?? currentState.createdAt,
554
+ });
555
+ }
556
+ function resolveFundingClaimAuthority(definition, currentState) {
557
+ if (definition.originatorClaimantXonly) {
558
+ return {
559
+ claimantXonly: definition.originatorClaimantXonly,
560
+ authoritySource: "originator-claimant",
561
+ };
562
+ }
563
+ return {
564
+ claimantXonly: currentState.controllerXonly,
565
+ authoritySource: "controller-fallback",
566
+ };
567
+ }
568
+ function resolveRepaymentClaimAuthority(currentState) {
569
+ if (currentState.holderClaimantXonly) {
570
+ return {
571
+ claimantXonly: currentState.holderClaimantXonly,
572
+ authoritySource: "holder-claimant",
573
+ };
574
+ }
575
+ return {
576
+ claimantXonly: currentState.controllerXonly,
577
+ authoritySource: "controller-fallback",
578
+ };
579
+ }
580
+ function validateReceivableFundingClaimAgainstState(input) {
581
+ const definition = validateReceivableDefinition(input.definition);
582
+ const currentState = validateReceivableState(input.currentState);
583
+ const claim = validateReceivableFundingClaimDescriptor(input.claim);
584
+ const authority = resolveFundingClaimAuthority(definition, currentState);
585
+ const currentStateHash = summarizeReceivableState(currentState).hash;
586
+ const stateStatusEligible = currentState.status === "FUNDED";
587
+ const receivableIdMatch = claim.receivableId === currentState.receivableId;
588
+ const currentStateHashMatch = claim.currentStateHash === currentStateHash;
589
+ const currentStatusMatch = claim.currentStatus === currentState.status;
590
+ const payerEntityMatch = claim.payerEntityId === currentState.holderEntityId;
591
+ const payeeEntityMatch = claim.payeeEntityId === currentState.originatorEntityId;
592
+ const claimantXonlyMatch = claim.claimantXonly === authority.claimantXonly;
593
+ const currencyAssetMatch = claim.currencyAssetId === currentState.currencyAssetId;
594
+ const amountMatch = currentState.lastTransition?.type === "FUND" && claim.amountSat === currentState.lastTransition.amount;
595
+ const eventTimestampMatch = currentState.lastTransition?.type === "FUND" && claim.eventTimestamp === currentState.lastTransition.at;
596
+ return {
597
+ claimKind: "FUNDING",
598
+ claimantAuthoritySource: authority.authoritySource,
599
+ roleSeparatedAuthorization: authority.authoritySource !== "controller-fallback",
600
+ stateStatusEligible,
601
+ receivableIdMatch,
602
+ currentStateHashMatch,
603
+ currentStatusMatch,
604
+ payerEntityMatch,
605
+ payeeEntityMatch,
606
+ claimantXonlyMatch,
607
+ currencyAssetMatch,
608
+ amountMatch,
609
+ eventTimestampMatch,
610
+ fullClaimVerified: stateStatusEligible
611
+ && receivableIdMatch
612
+ && currentStateHashMatch
613
+ && currentStatusMatch
614
+ && payerEntityMatch
615
+ && payeeEntityMatch
616
+ && claimantXonlyMatch
617
+ && currencyAssetMatch
618
+ && amountMatch
619
+ && eventTimestampMatch,
620
+ };
621
+ }
622
+ function validateReceivableRepaymentClaimAgainstState(input) {
623
+ const currentState = validateReceivableState(input.currentState);
624
+ const claim = validateReceivableRepaymentClaimDescriptor(input.claim);
625
+ const authority = resolveRepaymentClaimAuthority(currentState);
626
+ const currentStateHash = summarizeReceivableState(currentState).hash;
627
+ const stateStatusEligible = currentState.status === "PARTIALLY_REPAID" || currentState.status === "REPAID";
628
+ const receivableIdMatch = claim.receivableId === currentState.receivableId;
629
+ const currentStateHashMatch = claim.currentStateHash === currentStateHash;
630
+ const currentStatusMatch = claim.currentStatus === currentState.status;
631
+ const payerEntityMatch = claim.payerEntityId === currentState.debtorEntityId;
632
+ const payeeEntityMatch = claim.payeeEntityId === currentState.holderEntityId;
633
+ const claimantXonlyMatch = claim.claimantXonly === authority.claimantXonly;
634
+ const currencyAssetMatch = claim.currencyAssetId === currentState.currencyAssetId;
635
+ const amountMatch = currentState.lastTransition?.type === "REPAY" && claim.amountSat === currentState.lastTransition.amount;
636
+ const eventTimestampMatch = currentState.lastTransition?.type === "REPAY" && claim.eventTimestamp === currentState.lastTransition.at;
637
+ return {
638
+ claimKind: "REPAYMENT",
639
+ claimantAuthoritySource: authority.authoritySource,
640
+ roleSeparatedAuthorization: authority.authoritySource !== "controller-fallback",
641
+ stateStatusEligible,
642
+ receivableIdMatch,
643
+ currentStateHashMatch,
644
+ currentStatusMatch,
645
+ payerEntityMatch,
646
+ payeeEntityMatch,
647
+ claimantXonlyMatch,
648
+ currencyAssetMatch,
649
+ amountMatch,
650
+ eventTimestampMatch,
651
+ fullClaimVerified: stateStatusEligible
652
+ && receivableIdMatch
653
+ && currentStateHashMatch
654
+ && currentStatusMatch
655
+ && payerEntityMatch
656
+ && payeeEntityMatch
657
+ && claimantXonlyMatch
658
+ && currencyAssetMatch
659
+ && amountMatch
660
+ && eventTimestampMatch,
661
+ };
662
+ }
663
+ function validateReceivableFundingTransition(previousValue, nextValue) {
664
+ const previous = validateReceivableState(previousValue);
665
+ const next = validateReceivableState(nextValue);
666
+ const previousStateHashMatch = assertPreviousStateHashMatch(previous, next);
667
+ const participantConsistencyValid = assertSameReceivableIdentity(previous, next);
668
+ const statusProgressionValid = previous.status === "ORIGINATED" && next.status === "FUNDED";
669
+ if (!statusProgressionValid) {
670
+ throw new errors_1.ValidationError("Funding transition must move ORIGINATED to FUNDED", {
671
+ code: "RECEIVABLE_STATUS_INVALID",
672
+ });
673
+ }
674
+ const arithmeticDeltaValid = previous.outstandingAmount === next.outstandingAmount
675
+ && previous.repaidAmount === next.repaidAmount;
676
+ if (!arithmeticDeltaValid) {
677
+ throw new errors_1.ValidationError("Funding transition must preserve outstandingAmount and repaidAmount", {
678
+ code: "RECEIVABLE_ARITHMETIC_INVALID",
679
+ });
680
+ }
681
+ const createdAtMonotonic = assertCreatedAtMonotonic(previous, next);
682
+ const transitionAmountValid = next.lastTransition?.type === "FUND"
683
+ && next.lastTransition.amount === previous.outstandingAmount;
684
+ if (!transitionAmountValid) {
685
+ throw new errors_1.ValidationError("Funding transition must carry a FUND transition for the previous outstandingAmount", {
686
+ code: "RECEIVABLE_TRANSITION_AMOUNT_INVALID",
687
+ });
688
+ }
689
+ return {
690
+ transitionType: "FUND",
691
+ previousStateHashMatch,
692
+ participantConsistencyValid,
693
+ statusProgressionValid,
694
+ arithmeticDeltaValid,
695
+ createdAtMonotonic,
696
+ transitionAmountValid,
697
+ fullTransitionVerified: true,
698
+ };
699
+ }
700
+ function validateReceivableRepaymentTransition(previousValue, nextValue) {
701
+ const previous = validateReceivableState(previousValue);
702
+ const next = validateReceivableState(nextValue);
703
+ const previousStateHashMatch = assertPreviousStateHashMatch(previous, next);
704
+ const participantConsistencyValid = assertSameReceivableIdentity(previous, next) && previous.holderEntityId === next.holderEntityId;
705
+ if (!participantConsistencyValid) {
706
+ throw new errors_1.ValidationError("Repayment transition must preserve the current holder and receivable identity", {
707
+ code: "RECEIVABLE_TRANSITION_IDENTITY_INVALID",
708
+ });
709
+ }
710
+ const statusProgressionValid = (previous.status === "FUNDED" || previous.status === "PARTIALLY_REPAID")
711
+ && (next.status === "PARTIALLY_REPAID" || next.status === "REPAID");
712
+ if (!statusProgressionValid) {
713
+ throw new errors_1.ValidationError("Repayment transition must move FUNDED/PARTIALLY_REPAID to PARTIALLY_REPAID or REPAID", {
714
+ code: "RECEIVABLE_STATUS_INVALID",
715
+ });
716
+ }
717
+ const delta = next.repaidAmount - previous.repaidAmount;
718
+ const arithmeticDeltaValid = delta > 0
719
+ && previous.outstandingAmount - next.outstandingAmount === delta
720
+ && next.faceValue === next.outstandingAmount + next.repaidAmount;
721
+ if (!arithmeticDeltaValid) {
722
+ throw new errors_1.ValidationError("Repayment transition must increase repaidAmount and reduce outstandingAmount by the same amount", {
723
+ code: "RECEIVABLE_ARITHMETIC_INVALID",
724
+ });
725
+ }
726
+ const createdAtMonotonic = assertCreatedAtMonotonic(previous, next);
727
+ const transitionAmountValid = next.lastTransition?.type === "REPAY" && next.lastTransition.amount === delta;
728
+ if (!transitionAmountValid) {
729
+ throw new errors_1.ValidationError("Repayment transition must carry a REPAY transition that matches the repayment delta", {
730
+ code: "RECEIVABLE_TRANSITION_AMOUNT_INVALID",
731
+ });
732
+ }
733
+ return {
734
+ transitionType: "REPAY",
735
+ previousStateHashMatch,
736
+ participantConsistencyValid,
737
+ statusProgressionValid,
738
+ arithmeticDeltaValid,
739
+ createdAtMonotonic,
740
+ transitionAmountValid,
741
+ fullTransitionVerified: true,
742
+ };
743
+ }
744
+ function validateReceivableWriteOffTransition(previousValue, nextValue) {
745
+ const previous = validateReceivableState(previousValue);
746
+ const next = validateReceivableState(nextValue);
747
+ const previousStateHashMatch = assertPreviousStateHashMatch(previous, next);
748
+ const participantConsistencyValid = assertSameReceivableIdentity(previous, next) && previous.holderEntityId === next.holderEntityId;
749
+ if (!participantConsistencyValid) {
750
+ throw new errors_1.ValidationError("Write-off transition must preserve the current holder and receivable identity", {
751
+ code: "RECEIVABLE_TRANSITION_IDENTITY_INVALID",
752
+ });
753
+ }
754
+ const statusProgressionValid = (previous.status === "FUNDED" || previous.status === "PARTIALLY_REPAID")
755
+ && next.status === "DEFAULTED";
756
+ if (!statusProgressionValid) {
757
+ throw new errors_1.ValidationError("Write-off transition must move FUNDED/PARTIALLY_REPAID to DEFAULTED", {
758
+ code: "RECEIVABLE_STATUS_INVALID",
759
+ });
760
+ }
761
+ const arithmeticDeltaValid = previous.outstandingAmount === next.outstandingAmount
762
+ && previous.repaidAmount === next.repaidAmount;
763
+ if (!arithmeticDeltaValid) {
764
+ throw new errors_1.ValidationError("Write-off transition must preserve outstandingAmount and repaidAmount", {
765
+ code: "RECEIVABLE_ARITHMETIC_INVALID",
766
+ });
767
+ }
768
+ const createdAtMonotonic = assertCreatedAtMonotonic(previous, next);
769
+ const transitionAmountValid = next.lastTransition?.type === "WRITE_OFF" && next.lastTransition.amount > 0 && next.lastTransition.amount <= next.outstandingAmount;
770
+ if (!transitionAmountValid) {
771
+ throw new errors_1.ValidationError("Write-off transition must carry a positive WRITE_OFF transition amount within the outstanding amount", {
772
+ code: "RECEIVABLE_TRANSITION_AMOUNT_INVALID",
773
+ });
774
+ }
775
+ return {
776
+ transitionType: "WRITE_OFF",
777
+ previousStateHashMatch,
778
+ participantConsistencyValid,
779
+ statusProgressionValid,
780
+ arithmeticDeltaValid,
781
+ createdAtMonotonic,
782
+ transitionAmountValid,
783
+ fullTransitionVerified: true,
784
+ };
785
+ }
786
+ function validateReceivableClosingAgainstState(input) {
787
+ const latestState = validateReceivableState(input.latestState);
788
+ const closing = validateReceivableClosingDescriptor(input.closing);
789
+ const latestStateHash = summarizeReceivableState(latestState).hash;
790
+ const terminalStatusEligible = latestState.status === "REPAID" || latestState.status === "DEFAULTED";
791
+ if (!terminalStatusEligible) {
792
+ throw new errors_1.ValidationError("Only REPAID or DEFAULTED receivables can be closed", {
793
+ code: "RECEIVABLE_STATUS_INVALID",
794
+ });
795
+ }
796
+ const latestStateHashMatch = closing.latestStateHash === latestStateHash;
797
+ if (!latestStateHashMatch) {
798
+ throw new errors_1.ValidationError("Receivable closing latestStateHash does not match the latest state hash", {
799
+ code: "RECEIVABLE_LATEST_STATE_HASH_INVALID",
800
+ });
801
+ }
802
+ const closingReasonMatchesStatus = (latestState.status === "REPAID" && closing.closingReason === "REPAID")
803
+ || (latestState.status === "DEFAULTED" && closing.closingReason === "DEFAULTED")
804
+ || closing.closingReason === "CANCELLED";
805
+ if (!closingReasonMatchesStatus) {
806
+ throw new errors_1.ValidationError("Receivable closingReason must match the terminal state status", {
807
+ code: "RECEIVABLE_CLOSING_REASON_INVALID",
808
+ });
809
+ }
810
+ const historyTipMatches = !input.history || input.history.length === 0
811
+ ? true
812
+ : summarizeReceivableState(validateReceivableState(input.history.at(-1))).hash === latestStateHash;
813
+ if (!historyTipMatches) {
814
+ throw new errors_1.ValidationError("Receivable closing latest state must match the tip of the provided state history", {
815
+ code: "RECEIVABLE_HISTORY_TIP_MISMATCH",
816
+ });
817
+ }
818
+ return {
819
+ terminalStatusEligible,
820
+ latestStateHashMatch,
821
+ closingReasonMatchesStatus,
822
+ historyTipMatches,
823
+ lineageProvided: Boolean(input.history && input.history.length > 0),
824
+ fullClosingVerified: true,
825
+ };
826
+ }
827
+ function isStatusProgressionValid(previous, current) {
828
+ if (previous.status === "ORIGINATED") {
829
+ return current.status === "FUNDED" || current.status === "PARTIALLY_REPAID" || current.status === "REPAID" || current.status === "DEFAULTED";
830
+ }
831
+ if (previous.status === "FUNDED") {
832
+ return current.status === "PARTIALLY_REPAID" || current.status === "REPAID" || current.status === "DEFAULTED";
833
+ }
834
+ if (previous.status === "PARTIALLY_REPAID") {
835
+ return current.status === "PARTIALLY_REPAID" || current.status === "REPAID" || current.status === "DEFAULTED";
836
+ }
837
+ return false;
838
+ }
839
+ function verifyReceivableStateHistory(input) {
840
+ const history = input.history.map((entry) => validateReceivableState(entry));
841
+ const lineage = (0, lineage_1.verifyHashLinkedLineage)({
842
+ entries: history,
843
+ summarize: summarizeReceivableState,
844
+ getPreviousHash: (entry) => entry.previousStateHash,
845
+ isGenesis: (entry) => !entry.previousStateHash,
846
+ consistencyChecks: {
847
+ receivableIdConsistent: (entry, first) => entry.receivableId === first.receivableId,
848
+ originatorConsistent: (entry, first) => entry.originatorEntityId === first.originatorEntityId,
849
+ debtorConsistent: (entry, first) => entry.debtorEntityId === first.debtorEntityId,
850
+ currencyConsistent: (entry, first) => entry.currencyAssetId === first.currencyAssetId,
851
+ controllerConsistent: (entry, first) => entry.controllerXonly === first.controllerXonly,
852
+ faceValueConsistent: (entry, first) => entry.faceValue === first.faceValue,
853
+ },
854
+ });
855
+ const allArithmeticValid = history.every((entry) => entry.faceValue === entry.outstandingAmount + entry.repaidAmount);
856
+ const allStatusProgressionValid = history.every((entry, index) => (index === 0 ? entry.status === "ORIGINATED" : isStatusProgressionValid(history[index - 1], entry)));
857
+ const fullHistoryVerified = lineage.fullLineageVerified && allArithmeticValid && allStatusProgressionValid;
858
+ return {
859
+ chainLength: lineage.chainLength,
860
+ startsAtGenesis: lineage.startsAtGenesis,
861
+ latestStatus: history.at(-1).status,
862
+ allPreviousStateHashMatch: lineage.previousHashLinked,
863
+ receivableIdConsistent: lineage.consistency.receivableIdConsistent,
864
+ originatorConsistent: lineage.consistency.originatorConsistent,
865
+ debtorConsistent: lineage.consistency.debtorConsistent,
866
+ currencyConsistent: lineage.consistency.currencyConsistent,
867
+ controllerConsistent: lineage.consistency.controllerConsistent,
868
+ faceValueConsistent: lineage.consistency.faceValueConsistent,
869
+ allArithmeticValid,
870
+ allStatusProgressionValid,
871
+ fullHistoryVerified,
872
+ summaries: lineage.summaries,
873
+ };
874
+ }