@hazbase/simplicity 0.0.5 → 0.2.0
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 +140 -1038
- package/dist/cli.js +3341 -269
- package/dist/client/SimplicityClient.d.ts +91 -290
- package/dist/client/SimplicityClient.js +93 -23
- package/dist/core/executor.js +67 -92
- package/dist/core/lineage.d.ts +18 -0
- package/dist/core/lineage.js +30 -0
- package/dist/core/outputBinding.d.ts +61 -0
- package/dist/core/outputBinding.js +552 -0
- package/dist/core/reporting.d.ts +22 -0
- package/dist/core/reporting.js +40 -0
- package/dist/core/schnorr.d.ts +5 -0
- package/dist/core/schnorr.js +46 -0
- package/dist/core/types.d.ts +799 -1
- package/dist/docs/definitions/bond-anchor.simf +19 -0
- package/dist/docs/definitions/bond-definition.json +10 -0
- package/dist/docs/definitions/bond-descriptor-bound-settlement-machine.simf +144 -0
- package/dist/docs/definitions/bond-issuance-anchor.simf +26 -0
- package/dist/docs/definitions/bond-issuance-state-partial-redemption.json +18 -0
- package/dist/docs/definitions/bond-issuance-state-redeemed.json +18 -0
- package/dist/docs/definitions/bond-issuance-state.json +12 -0
- package/dist/docs/definitions/bond-redemption-state-machine.simf +118 -0
- package/dist/docs/definitions/bond-redemption-transition.simf +41 -0
- package/dist/docs/definitions/bond-script-bound-settlement-machine.simf +142 -0
- package/dist/docs/definitions/fund-capital-call-open.simf +82 -0
- package/dist/docs/definitions/fund-capital-call-refund-only.simf +67 -0
- package/dist/docs/definitions/fund-capital-call-state.json +11 -0
- package/dist/docs/definitions/fund-definition.json +8 -0
- package/dist/docs/definitions/fund-distribution-claim.simf +57 -0
- package/dist/docs/definitions/receivable-definition.json +9 -0
- package/dist/docs/definitions/receivable-funding-claim.json +13 -0
- package/dist/docs/definitions/receivable-funding-claim.simf +58 -0
- package/dist/docs/definitions/receivable-repayment-claim.json +13 -0
- package/dist/docs/definitions/receivable-repayment-claim.simf +59 -0
- package/dist/docs/definitions/receivable-state-funded.json +20 -0
- package/dist/docs/definitions/receivable-state-originated.json +19 -0
- package/dist/docs/definitions/receivable-state-repaid.json +20 -0
- package/dist/docs/definitions/recursive-delay-direct-next.simf +60 -0
- package/dist/docs/definitions/recursive-delay-optional.simf +88 -0
- package/dist/docs/definitions/recursive-delay-required.simf +72 -0
- package/dist/docs/definitions/recursive-delay.simf +83 -0
- package/dist/docs/definitions/recursive-policy-transfer-machine.simf +65 -0
- package/dist/domain/bond.d.ts +8649 -720
- package/dist/domain/bond.js +1398 -8
- package/dist/domain/bondSettlementValidation.d.ts +2 -0
- package/dist/domain/bondSettlementValidation.js +28 -0
- package/dist/domain/bondValidation.d.ts +37 -1
- package/dist/domain/bondValidation.js +137 -11
- package/dist/domain/fund.d.ts +2452 -0
- package/dist/domain/fund.js +1756 -0
- package/dist/domain/fundValidation.d.ts +152 -0
- package/dist/domain/fundValidation.js +767 -0
- package/dist/domain/policies.d.ts +1064 -0
- package/dist/domain/policies.js +1625 -0
- package/dist/domain/receivable.d.ts +824 -0
- package/dist/domain/receivable.js +1375 -0
- package/dist/domain/receivableValidation.d.ts +147 -0
- package/dist/domain/receivableValidation.js +831 -0
- package/dist/index.d.ts +8 -2
- package/dist/index.js +137 -21
- package/package.json +20 -2
|
@@ -0,0 +1,767 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.summarizeFundDefinition = summarizeFundDefinition;
|
|
4
|
+
exports.summarizeCapitalCallState = summarizeCapitalCallState;
|
|
5
|
+
exports.summarizeLPPositionReceipt = summarizeLPPositionReceipt;
|
|
6
|
+
exports.summarizeDistributionDescriptor = summarizeDistributionDescriptor;
|
|
7
|
+
exports.summarizeFundClosingDescriptor = summarizeFundClosingDescriptor;
|
|
8
|
+
exports.summarizeFundFinalityPayload = summarizeFundFinalityPayload;
|
|
9
|
+
exports.buildPositionReceiptAttestationMessageHash = buildPositionReceiptAttestationMessageHash;
|
|
10
|
+
exports.summarizeLPPositionReceiptEnvelope = summarizeLPPositionReceiptEnvelope;
|
|
11
|
+
exports.validateFundDefinition = validateFundDefinition;
|
|
12
|
+
exports.validateCapitalCallState = validateCapitalCallState;
|
|
13
|
+
exports.validateLPPositionReceipt = validateLPPositionReceipt;
|
|
14
|
+
exports.validateLPPositionReceiptEnvelope = validateLPPositionReceiptEnvelope;
|
|
15
|
+
exports.validateDistributionDescriptor = validateDistributionDescriptor;
|
|
16
|
+
exports.validateFundClosingDescriptor = validateFundClosingDescriptor;
|
|
17
|
+
exports.validateFundCrossChecks = validateFundCrossChecks;
|
|
18
|
+
exports.verifyLPPositionReceiptEnvelope = verifyLPPositionReceiptEnvelope;
|
|
19
|
+
exports.verifyLPPositionReceiptEnvelopeChain = verifyLPPositionReceiptEnvelopeChain;
|
|
20
|
+
exports.signLPPositionReceipt = signLPPositionReceipt;
|
|
21
|
+
exports.validateDistributionAgainstReceipt = validateDistributionAgainstReceipt;
|
|
22
|
+
exports.validateClosingAgainstReceipt = validateClosingAgainstReceipt;
|
|
23
|
+
exports.buildClaimedCapitalCallState = buildClaimedCapitalCallState;
|
|
24
|
+
exports.buildRefundOnlyCapitalCallState = buildRefundOnlyCapitalCallState;
|
|
25
|
+
exports.buildRefundedCapitalCallState = buildRefundedCapitalCallState;
|
|
26
|
+
exports.buildLPPositionReceipt = buildLPPositionReceipt;
|
|
27
|
+
exports.applyDistributionToReceipt = applyDistributionToReceipt;
|
|
28
|
+
exports.applyDistributionsToReceipt = applyDistributionsToReceipt;
|
|
29
|
+
exports.reconcileLPPositionReceipt = reconcileLPPositionReceipt;
|
|
30
|
+
exports.buildDistributionDescriptor = buildDistributionDescriptor;
|
|
31
|
+
exports.buildFundClosingDescriptor = buildFundClosingDescriptor;
|
|
32
|
+
const errors_1 = require("../core/errors");
|
|
33
|
+
const lineage_1 = require("../core/lineage");
|
|
34
|
+
const summary_1 = require("../core/summary");
|
|
35
|
+
const schnorr_1 = require("../core/schnorr");
|
|
36
|
+
function assertNonEmptyString(value, code, message) {
|
|
37
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
38
|
+
throw new errors_1.ValidationError(message, { code });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function assertFinitePositiveNumber(value, code, message) {
|
|
42
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
|
|
43
|
+
throw new errors_1.ValidationError(message, { code });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function assertFiniteNonNegativeNumber(value, code, message) {
|
|
47
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
|
|
48
|
+
throw new errors_1.ValidationError(message, { code });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function assertArrayOfHashes(value, code, message) {
|
|
52
|
+
if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string" || !/^[0-9a-f]{64}$/i.test(entry))) {
|
|
53
|
+
throw new errors_1.ValidationError(message, { code });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function assertIsoDateLike(value, code, message) {
|
|
57
|
+
assertNonEmptyString(value, code, message);
|
|
58
|
+
if (Number.isNaN(Date.parse(value))) {
|
|
59
|
+
throw new errors_1.ValidationError(message, { code });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function assertCapitalCallStatus(status) {
|
|
63
|
+
if (status !== "OPEN" && status !== "CLAIMED" && status !== "REFUND_ONLY" && status !== "REFUNDED") {
|
|
64
|
+
throw new errors_1.ValidationError("capital call status must be OPEN, CLAIMED, REFUND_ONLY, or REFUNDED", {
|
|
65
|
+
code: "FUND_CAPITAL_CALL_STATUS_INVALID",
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function assertReceiptStatus(status) {
|
|
70
|
+
if (status !== "ACTIVE"
|
|
71
|
+
&& status !== "PARTIALLY_DISTRIBUTED"
|
|
72
|
+
&& status !== "FULLY_DISTRIBUTED"
|
|
73
|
+
&& status !== "CLOSED") {
|
|
74
|
+
throw new errors_1.ValidationError("LP position receipt status must be ACTIVE, PARTIALLY_DISTRIBUTED, FULLY_DISTRIBUTED, or CLOSED", { code: "FUND_POSITION_STATUS_INVALID" });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function assertClosingReason(reason) {
|
|
78
|
+
if (reason !== "LIQUIDATED" && reason !== "CANCELLED" && reason !== "WRITTEN_OFF") {
|
|
79
|
+
throw new errors_1.ValidationError("closingReason must be LIQUIDATED, CANCELLED, or WRITTEN_OFF", {
|
|
80
|
+
code: "FUND_CLOSING_REASON_INVALID",
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function assertHashHex(value, code, message) {
|
|
85
|
+
if (typeof value !== "string" || !/^[0-9a-f]{64}$/i.test(value)) {
|
|
86
|
+
throw new errors_1.ValidationError(message, { code });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function assertXonlyPubkey(value, code, message) {
|
|
90
|
+
if (typeof value !== "string" || !/^[0-9a-f]{64}$/i.test(value)) {
|
|
91
|
+
throw new errors_1.ValidationError(message, { code });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function summarizeFundDefinition(definition) {
|
|
95
|
+
const canonicalJson = (0, summary_1.stableStringify)(definition);
|
|
96
|
+
return { canonicalJson, hash: (0, summary_1.sha256HexUtf8)(canonicalJson) };
|
|
97
|
+
}
|
|
98
|
+
function summarizeCapitalCallState(state) {
|
|
99
|
+
const canonicalJson = (0, summary_1.stableStringify)(state);
|
|
100
|
+
return { canonicalJson, hash: (0, summary_1.sha256HexUtf8)(canonicalJson) };
|
|
101
|
+
}
|
|
102
|
+
function summarizeLPPositionReceipt(receipt) {
|
|
103
|
+
const canonicalJson = (0, summary_1.stableStringify)(receipt);
|
|
104
|
+
return { canonicalJson, hash: (0, summary_1.sha256HexUtf8)(canonicalJson) };
|
|
105
|
+
}
|
|
106
|
+
function summarizeDistributionDescriptor(descriptor) {
|
|
107
|
+
const canonicalJson = (0, summary_1.stableStringify)(descriptor);
|
|
108
|
+
return { canonicalJson, hash: (0, summary_1.sha256HexUtf8)(canonicalJson) };
|
|
109
|
+
}
|
|
110
|
+
function summarizeFundClosingDescriptor(descriptor) {
|
|
111
|
+
const canonicalJson = (0, summary_1.stableStringify)(descriptor);
|
|
112
|
+
return { canonicalJson, hash: (0, summary_1.sha256HexUtf8)(canonicalJson) };
|
|
113
|
+
}
|
|
114
|
+
function summarizeFundFinalityPayload(payload) {
|
|
115
|
+
const canonicalJson = (0, summary_1.stableStringify)(payload);
|
|
116
|
+
return { canonicalJson, hash: (0, summary_1.sha256HexUtf8)(canonicalJson) };
|
|
117
|
+
}
|
|
118
|
+
function buildPositionReceiptAttestationMessageHash(input) {
|
|
119
|
+
assertNonEmptyString(input.fundId, "FUND_POSITION_ATTESTATION_FUND_ID_INVALID", "fundId must be a non-empty string");
|
|
120
|
+
assertNonEmptyString(input.positionId, "FUND_POSITION_ATTESTATION_POSITION_ID_INVALID", "positionId must be a non-empty string");
|
|
121
|
+
assertHashHex(input.positionReceiptHash, "FUND_POSITION_ATTESTATION_RECEIPT_HASH_INVALID", "positionReceiptHash must be a 64-character hex string");
|
|
122
|
+
if (!Number.isInteger(input.sequence) || input.sequence < 0) {
|
|
123
|
+
throw new errors_1.ValidationError("sequence must be a non-negative integer", {
|
|
124
|
+
code: "FUND_POSITION_ATTESTATION_SEQUENCE_INVALID",
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
return (0, summary_1.sha256HexUtf8)(`fund-position-attestation/v1:${input.fundId}:${input.positionId}:${input.positionReceiptHash}:${input.sequence}`);
|
|
128
|
+
}
|
|
129
|
+
function summarizeLPPositionReceiptEnvelope(envelope) {
|
|
130
|
+
const canonicalJson = (0, summary_1.stableStringify)(envelope);
|
|
131
|
+
return { canonicalJson, hash: (0, summary_1.sha256HexUtf8)(canonicalJson) };
|
|
132
|
+
}
|
|
133
|
+
function validateFundDefinition(value) {
|
|
134
|
+
const definition = value;
|
|
135
|
+
assertNonEmptyString(definition?.fundId, "FUND_ID_INVALID", "fundId must be a non-empty string");
|
|
136
|
+
assertNonEmptyString(definition?.managerEntityId, "FUND_MANAGER_ENTITY_INVALID", "managerEntityId must be a non-empty string");
|
|
137
|
+
assertXonlyPubkey(definition?.managerXonly, "FUND_MANAGER_XONLY_INVALID", "managerXonly must be a 32-byte xonly public key");
|
|
138
|
+
assertNonEmptyString(definition?.currencyAssetId, "FUND_CURRENCY_ASSET_INVALID", "currencyAssetId must be a non-empty string");
|
|
139
|
+
if (definition?.jurisdiction !== undefined) {
|
|
140
|
+
assertNonEmptyString(definition.jurisdiction, "FUND_JURISDICTION_INVALID", "jurisdiction must be a non-empty string");
|
|
141
|
+
}
|
|
142
|
+
if (definition?.vintage !== undefined) {
|
|
143
|
+
assertNonEmptyString(definition.vintage, "FUND_VINTAGE_INVALID", "vintage must be a non-empty string");
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
fundId: definition.fundId,
|
|
147
|
+
managerEntityId: definition.managerEntityId,
|
|
148
|
+
managerXonly: definition.managerXonly,
|
|
149
|
+
currencyAssetId: definition.currencyAssetId,
|
|
150
|
+
...(definition.jurisdiction ? { jurisdiction: definition.jurisdiction } : {}),
|
|
151
|
+
...(definition.vintage ? { vintage: definition.vintage } : {}),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
function validateCapitalCallState(value) {
|
|
155
|
+
const state = value;
|
|
156
|
+
assertNonEmptyString(state?.callId, "FUND_CALL_ID_INVALID", "callId must be a non-empty string");
|
|
157
|
+
assertNonEmptyString(state?.fundId, "FUND_CALL_FUND_ID_INVALID", "fundId must be a non-empty string");
|
|
158
|
+
assertNonEmptyString(state?.lpId, "FUND_CALL_LP_ID_INVALID", "lpId must be a non-empty string");
|
|
159
|
+
assertNonEmptyString(state?.currencyAssetId, "FUND_CALL_CURRENCY_INVALID", "currencyAssetId must be a non-empty string");
|
|
160
|
+
assertFinitePositiveNumber(state?.amount, "FUND_CALL_AMOUNT_INVALID", "amount must be a positive finite number");
|
|
161
|
+
assertXonlyPubkey(state?.lpXonly, "FUND_CALL_LP_XONLY_INVALID", "lpXonly must be a 32-byte xonly public key");
|
|
162
|
+
assertXonlyPubkey(state?.managerXonly, "FUND_CALL_MANAGER_XONLY_INVALID", "managerXonly must be a 32-byte xonly public key");
|
|
163
|
+
assertCapitalCallStatus(state?.status);
|
|
164
|
+
assertFinitePositiveNumber(state?.claimCutoffHeight, "FUND_CALL_CUTOFF_INVALID", "claimCutoffHeight must be a positive finite number");
|
|
165
|
+
if (state.fundedAt !== undefined) {
|
|
166
|
+
assertIsoDateLike(state.fundedAt, "FUND_CALL_FUNDED_AT_INVALID", "fundedAt must be an ISO timestamp");
|
|
167
|
+
}
|
|
168
|
+
if (state.status === "CLAIMED") {
|
|
169
|
+
assertIsoDateLike(state.claimedAt, "FUND_CALL_CLAIMED_AT_INVALID", "claimedAt must be an ISO timestamp");
|
|
170
|
+
assertHashHex(state.previousStateHash, "FUND_CALL_PREVIOUS_HASH_REQUIRED", "CLAIMED capital call state must set previousStateHash");
|
|
171
|
+
}
|
|
172
|
+
if (state.status === "REFUND_ONLY") {
|
|
173
|
+
assertHashHex(state.previousStateHash, "FUND_CALL_PREVIOUS_HASH_REQUIRED", "REFUND_ONLY capital call state must set previousStateHash");
|
|
174
|
+
}
|
|
175
|
+
if (state.status === "REFUNDED") {
|
|
176
|
+
assertIsoDateLike(state.refundedAt, "FUND_CALL_REFUNDED_AT_INVALID", "refundedAt must be an ISO timestamp");
|
|
177
|
+
assertHashHex(state.previousStateHash, "FUND_CALL_PREVIOUS_HASH_REQUIRED", "REFUNDED capital call state must set previousStateHash");
|
|
178
|
+
}
|
|
179
|
+
return {
|
|
180
|
+
callId: state.callId,
|
|
181
|
+
fundId: state.fundId,
|
|
182
|
+
lpId: state.lpId,
|
|
183
|
+
currencyAssetId: state.currencyAssetId,
|
|
184
|
+
amount: state.amount,
|
|
185
|
+
lpXonly: state.lpXonly,
|
|
186
|
+
managerXonly: state.managerXonly,
|
|
187
|
+
status: state.status,
|
|
188
|
+
claimCutoffHeight: state.claimCutoffHeight,
|
|
189
|
+
...(state.fundedAt ? { fundedAt: state.fundedAt } : {}),
|
|
190
|
+
...(state.claimedAt ? { claimedAt: state.claimedAt } : {}),
|
|
191
|
+
...(state.refundedAt ? { refundedAt: state.refundedAt } : {}),
|
|
192
|
+
...(state.previousStateHash !== undefined ? { previousStateHash: state.previousStateHash } : {}),
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
function validateLPPositionReceipt(value) {
|
|
196
|
+
const receipt = value;
|
|
197
|
+
if (receipt?.schemaVersion !== "lp-position-receipt/v2") {
|
|
198
|
+
throw new errors_1.ValidationError("LP position receipt schemaVersion must be lp-position-receipt/v2", {
|
|
199
|
+
code: "FUND_POSITION_SCHEMA_VERSION_INVALID",
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
assertNonEmptyString(receipt?.positionId, "FUND_POSITION_ID_INVALID", "positionId must be a non-empty string");
|
|
203
|
+
assertNonEmptyString(receipt?.fundId, "FUND_POSITION_FUND_ID_INVALID", "fundId must be a non-empty string");
|
|
204
|
+
assertNonEmptyString(receipt?.lpId, "FUND_POSITION_LP_ID_INVALID", "lpId must be a non-empty string");
|
|
205
|
+
assertNonEmptyString(receipt?.callId, "FUND_POSITION_CALL_ID_INVALID", "callId must be a non-empty string");
|
|
206
|
+
assertNonEmptyString(receipt?.currencyAssetId, "FUND_POSITION_CURRENCY_INVALID", "currencyAssetId must be a non-empty string");
|
|
207
|
+
assertXonlyPubkey(receipt?.lpXonly, "FUND_POSITION_LP_XONLY_INVALID", "lpXonly must be a 32-byte xonly public key");
|
|
208
|
+
if (!Number.isInteger(receipt?.sequence) || (receipt?.sequence ?? -1) < 0) {
|
|
209
|
+
throw new errors_1.ValidationError("sequence must be a non-negative integer", {
|
|
210
|
+
code: "FUND_POSITION_SEQUENCE_INVALID",
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
assertFinitePositiveNumber(receipt?.committedAmount, "FUND_POSITION_COMMITTED_INVALID", "committedAmount must be a positive finite number");
|
|
214
|
+
assertFinitePositiveNumber(receipt?.fundedAmount, "FUND_POSITION_FUNDED_INVALID", "fundedAmount must be a positive finite number");
|
|
215
|
+
assertFiniteNonNegativeNumber(receipt?.distributedAmount, "FUND_POSITION_DISTRIBUTED_INVALID", "distributedAmount must be a non-negative finite number");
|
|
216
|
+
if (!Number.isInteger(receipt?.distributionCount) || (receipt?.distributionCount ?? -1) < 0) {
|
|
217
|
+
throw new errors_1.ValidationError("distributionCount must be a non-negative integer", {
|
|
218
|
+
code: "FUND_POSITION_DISTRIBUTION_COUNT_INVALID",
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
assertIsoDateLike(receipt?.effectiveAt, "FUND_POSITION_EFFECTIVE_AT_INVALID", "effectiveAt must be an ISO timestamp");
|
|
222
|
+
if (receipt?.lastDistributedAt !== undefined) {
|
|
223
|
+
assertIsoDateLike(receipt.lastDistributedAt, "FUND_POSITION_LAST_DISTRIBUTED_AT_INVALID", "lastDistributedAt must be an ISO timestamp");
|
|
224
|
+
}
|
|
225
|
+
assertReceiptStatus(receipt?.status);
|
|
226
|
+
if (receipt.previousReceiptHash !== undefined && receipt.previousReceiptHash !== null) {
|
|
227
|
+
assertHashHex(receipt.previousReceiptHash, "FUND_POSITION_PREVIOUS_HASH_INVALID", "previousReceiptHash must be a 64-character hex string when provided");
|
|
228
|
+
}
|
|
229
|
+
if (receipt.sequence === 0 && receipt.previousReceiptHash) {
|
|
230
|
+
throw new errors_1.ValidationError("sequence=0 receipts cannot set previousReceiptHash", {
|
|
231
|
+
code: "FUND_POSITION_SEQUENCE_ZERO_PREVIOUS_HASH",
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
if ((receipt.sequence ?? 0) > 0 && !receipt.previousReceiptHash) {
|
|
235
|
+
throw new errors_1.ValidationError("sequence>0 receipts must set previousReceiptHash", {
|
|
236
|
+
code: "FUND_POSITION_PREVIOUS_HASH_REQUIRED",
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
if ((receipt.distributedAmount ?? 0) > (receipt.fundedAmount ?? 0)) {
|
|
240
|
+
throw new errors_1.ValidationError("distributedAmount cannot exceed fundedAmount", {
|
|
241
|
+
code: "FUND_POSITION_DISTRIBUTED_EXCEEDS_FUNDED",
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
return {
|
|
245
|
+
schemaVersion: receipt.schemaVersion,
|
|
246
|
+
positionId: receipt.positionId,
|
|
247
|
+
fundId: receipt.fundId,
|
|
248
|
+
lpId: receipt.lpId,
|
|
249
|
+
callId: receipt.callId,
|
|
250
|
+
currencyAssetId: receipt.currencyAssetId,
|
|
251
|
+
lpXonly: receipt.lpXonly,
|
|
252
|
+
sequence: receipt.sequence,
|
|
253
|
+
committedAmount: receipt.committedAmount,
|
|
254
|
+
fundedAmount: receipt.fundedAmount,
|
|
255
|
+
distributedAmount: receipt.distributedAmount,
|
|
256
|
+
distributionCount: receipt.distributionCount,
|
|
257
|
+
effectiveAt: receipt.effectiveAt,
|
|
258
|
+
...(receipt.lastDistributedAt !== undefined ? { lastDistributedAt: receipt.lastDistributedAt } : {}),
|
|
259
|
+
status: receipt.status,
|
|
260
|
+
...(receipt.previousReceiptHash !== undefined ? { previousReceiptHash: receipt.previousReceiptHash } : {}),
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
function validateLPPositionReceiptEnvelope(value) {
|
|
264
|
+
const envelope = value;
|
|
265
|
+
const receipt = validateLPPositionReceipt(envelope?.receipt);
|
|
266
|
+
const attestation = envelope?.attestation;
|
|
267
|
+
assertHashHex(attestation?.positionReceiptHash, "FUND_POSITION_ATTESTATION_RECEIPT_HASH_INVALID", "positionReceiptHash must be a 64-character hex string");
|
|
268
|
+
if (!Number.isInteger(attestation?.sequence) || (attestation?.sequence ?? -1) < 0) {
|
|
269
|
+
throw new errors_1.ValidationError("attestation sequence must be a non-negative integer", {
|
|
270
|
+
code: "FUND_POSITION_ATTESTATION_SEQUENCE_INVALID",
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
assertXonlyPubkey(attestation?.managerXonly, "FUND_POSITION_ATTESTATION_MANAGER_XONLY_INVALID", "managerXonly must be a 32-byte xonly public key");
|
|
274
|
+
assertIsoDateLike(attestation?.signedAt, "FUND_POSITION_ATTESTATION_SIGNED_AT_INVALID", "signedAt must be an ISO timestamp");
|
|
275
|
+
if (attestation?.scheme !== "bip340-sha256") {
|
|
276
|
+
throw new errors_1.ValidationError("attestation scheme must be bip340-sha256", {
|
|
277
|
+
code: "FUND_POSITION_ATTESTATION_SCHEME_INVALID",
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
if (typeof attestation?.signature !== "string" || !/^[0-9a-f]{128}$/i.test(attestation.signature)) {
|
|
281
|
+
throw new errors_1.ValidationError("signature must be a 64-byte hex string", {
|
|
282
|
+
code: "FUND_POSITION_ATTESTATION_SIGNATURE_INVALID",
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
return {
|
|
286
|
+
receipt,
|
|
287
|
+
attestation: {
|
|
288
|
+
positionReceiptHash: attestation.positionReceiptHash,
|
|
289
|
+
sequence: attestation.sequence,
|
|
290
|
+
managerXonly: attestation.managerXonly,
|
|
291
|
+
signedAt: attestation.signedAt,
|
|
292
|
+
signature: attestation.signature.toLowerCase(),
|
|
293
|
+
scheme: attestation.scheme,
|
|
294
|
+
},
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
function validateDistributionDescriptor(value) {
|
|
298
|
+
const descriptor = value;
|
|
299
|
+
assertNonEmptyString(descriptor?.distributionId, "FUND_DISTRIBUTION_ID_INVALID", "distributionId must be a non-empty string");
|
|
300
|
+
assertNonEmptyString(descriptor?.positionId, "FUND_DISTRIBUTION_POSITION_ID_INVALID", "positionId must be a non-empty string");
|
|
301
|
+
assertNonEmptyString(descriptor?.fundId, "FUND_DISTRIBUTION_FUND_ID_INVALID", "fundId must be a non-empty string");
|
|
302
|
+
assertNonEmptyString(descriptor?.lpId, "FUND_DISTRIBUTION_LP_ID_INVALID", "lpId must be a non-empty string");
|
|
303
|
+
assertNonEmptyString(descriptor?.assetId, "FUND_DISTRIBUTION_ASSET_INVALID", "assetId must be a non-empty string");
|
|
304
|
+
assertFinitePositiveNumber(descriptor?.amountSat, "FUND_DISTRIBUTION_AMOUNT_INVALID", "amountSat must be a positive finite number");
|
|
305
|
+
assertIsoDateLike(descriptor?.approvedAt, "FUND_DISTRIBUTION_APPROVED_AT_INVALID", "approvedAt must be an ISO timestamp");
|
|
306
|
+
assertHashHex(descriptor?.positionReceiptHash, "FUND_DISTRIBUTION_RECEIPT_HASH_INVALID", "positionReceiptHash must be a 64-character hex string");
|
|
307
|
+
return {
|
|
308
|
+
distributionId: descriptor.distributionId,
|
|
309
|
+
positionId: descriptor.positionId,
|
|
310
|
+
fundId: descriptor.fundId,
|
|
311
|
+
lpId: descriptor.lpId,
|
|
312
|
+
assetId: descriptor.assetId,
|
|
313
|
+
amountSat: descriptor.amountSat,
|
|
314
|
+
approvedAt: descriptor.approvedAt,
|
|
315
|
+
positionReceiptHash: descriptor.positionReceiptHash,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
function validateFundClosingDescriptor(value) {
|
|
319
|
+
const descriptor = value;
|
|
320
|
+
assertNonEmptyString(descriptor?.closingId, "FUND_CLOSING_ID_INVALID", "closingId must be a non-empty string");
|
|
321
|
+
assertNonEmptyString(descriptor?.fundId, "FUND_CLOSING_FUND_ID_INVALID", "fundId must be a non-empty string");
|
|
322
|
+
assertNonEmptyString(descriptor?.lpId, "FUND_CLOSING_LP_ID_INVALID", "lpId must be a non-empty string");
|
|
323
|
+
assertNonEmptyString(descriptor?.positionId, "FUND_CLOSING_POSITION_ID_INVALID", "positionId must be a non-empty string");
|
|
324
|
+
assertHashHex(descriptor?.positionReceiptHash, "FUND_CLOSING_RECEIPT_HASH_INVALID", "positionReceiptHash must be a 64-character hex string");
|
|
325
|
+
assertArrayOfHashes(descriptor?.finalDistributionHashes, "FUND_CLOSING_DISTRIBUTION_HASHES_INVALID", "finalDistributionHashes must be an array of 64-character hex strings");
|
|
326
|
+
assertIsoDateLike(descriptor?.closedAt, "FUND_CLOSING_CLOSED_AT_INVALID", "closedAt must be an ISO timestamp");
|
|
327
|
+
assertClosingReason(descriptor?.closingReason);
|
|
328
|
+
return {
|
|
329
|
+
closingId: descriptor.closingId,
|
|
330
|
+
fundId: descriptor.fundId,
|
|
331
|
+
lpId: descriptor.lpId,
|
|
332
|
+
positionId: descriptor.positionId,
|
|
333
|
+
positionReceiptHash: descriptor.positionReceiptHash,
|
|
334
|
+
finalDistributionHashes: descriptor.finalDistributionHashes,
|
|
335
|
+
closedAt: descriptor.closedAt,
|
|
336
|
+
closingReason: descriptor.closingReason,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
function validateFundCrossChecks(definition, capitalCall) {
|
|
340
|
+
const result = {
|
|
341
|
+
fundIdMatch: definition.fundId === capitalCall.fundId,
|
|
342
|
+
currencyMatch: definition.currencyAssetId === capitalCall.currencyAssetId,
|
|
343
|
+
managerMatch: definition.managerXonly.toLowerCase() === capitalCall.managerXonly.toLowerCase(),
|
|
344
|
+
};
|
|
345
|
+
if (!result.fundIdMatch) {
|
|
346
|
+
throw new errors_1.ValidationError("Fund definition and capital call fundId do not match", { code: "FUND_ID_MISMATCH" });
|
|
347
|
+
}
|
|
348
|
+
if (!result.currencyMatch) {
|
|
349
|
+
throw new errors_1.ValidationError("Fund definition and capital call currencyAssetId do not match", {
|
|
350
|
+
code: "FUND_CURRENCY_MISMATCH",
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
if (!result.managerMatch) {
|
|
354
|
+
throw new errors_1.ValidationError("Fund definition and capital call managerXonly do not match", {
|
|
355
|
+
code: "FUND_MANAGER_MISMATCH",
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
return result;
|
|
359
|
+
}
|
|
360
|
+
async function verifyLPPositionReceiptEnvelope(input) {
|
|
361
|
+
const envelope = validateLPPositionReceiptEnvelope(input.envelope);
|
|
362
|
+
const receiptSummary = summarizeLPPositionReceipt(envelope.receipt);
|
|
363
|
+
const digestHex = buildPositionReceiptAttestationMessageHash({
|
|
364
|
+
fundId: envelope.receipt.fundId,
|
|
365
|
+
positionId: envelope.receipt.positionId,
|
|
366
|
+
positionReceiptHash: receiptSummary.hash,
|
|
367
|
+
sequence: envelope.receipt.sequence,
|
|
368
|
+
});
|
|
369
|
+
const positionReceiptHashMatch = envelope.attestation.positionReceiptHash === receiptSummary.hash;
|
|
370
|
+
const sequenceMatch = envelope.attestation.sequence === envelope.receipt.sequence;
|
|
371
|
+
const sequenceMonotonic = envelope.receipt.sequence === 0
|
|
372
|
+
? !envelope.receipt.previousReceiptHash
|
|
373
|
+
: Boolean(envelope.receipt.previousReceiptHash);
|
|
374
|
+
const attestingSignerMatch = input.expectedManagerXonly
|
|
375
|
+
? envelope.attestation.managerXonly.toLowerCase() === input.expectedManagerXonly.toLowerCase()
|
|
376
|
+
: true;
|
|
377
|
+
const attestationVerified = positionReceiptHashMatch
|
|
378
|
+
&& sequenceMatch
|
|
379
|
+
&& await (0, schnorr_1.schnorrVerifyHex)(envelope.attestation.signature, digestHex, envelope.attestation.managerXonly);
|
|
380
|
+
const previousEnvelopeProvided = Boolean(input.previousEnvelope);
|
|
381
|
+
const previousEnvelope = input.previousEnvelope ? validateLPPositionReceiptEnvelope(input.previousEnvelope) : undefined;
|
|
382
|
+
const previousSummary = previousEnvelope ? summarizeLPPositionReceipt(previousEnvelope.receipt) : undefined;
|
|
383
|
+
const previousReceiptHashMatch = envelope.receipt.sequence === 0
|
|
384
|
+
? !previousEnvelope
|
|
385
|
+
: previousSummary
|
|
386
|
+
? envelope.receipt.previousReceiptHash === previousSummary.hash
|
|
387
|
+
: false;
|
|
388
|
+
const previousSequenceMatch = envelope.receipt.sequence === 0
|
|
389
|
+
? !previousEnvelope
|
|
390
|
+
: previousEnvelope
|
|
391
|
+
? previousEnvelope.receipt.sequence + 1 === envelope.receipt.sequence
|
|
392
|
+
: false;
|
|
393
|
+
const previousAttestingSignerMatch = previousEnvelope && input.expectedManagerXonly
|
|
394
|
+
? previousEnvelope.attestation.managerXonly.toLowerCase() === input.expectedManagerXonly.toLowerCase()
|
|
395
|
+
: true;
|
|
396
|
+
const previousAttestationVerified = previousEnvelope
|
|
397
|
+
? previousEnvelope.attestation.positionReceiptHash === previousSummary?.hash
|
|
398
|
+
&& previousEnvelope.attestation.sequence === previousEnvelope.receipt.sequence
|
|
399
|
+
&& await (0, schnorr_1.schnorrVerifyHex)(previousEnvelope.attestation.signature, buildPositionReceiptAttestationMessageHash({
|
|
400
|
+
fundId: previousEnvelope.receipt.fundId,
|
|
401
|
+
positionId: previousEnvelope.receipt.positionId,
|
|
402
|
+
positionReceiptHash: previousSummary.hash,
|
|
403
|
+
sequence: previousEnvelope.receipt.sequence,
|
|
404
|
+
}), previousEnvelope.attestation.managerXonly)
|
|
405
|
+
: envelope.receipt.sequence === 0;
|
|
406
|
+
const previousSequenceMonotonic = previousEnvelope
|
|
407
|
+
? previousEnvelope.receipt.sequence === 0
|
|
408
|
+
? !previousEnvelope.receipt.previousReceiptHash
|
|
409
|
+
: Boolean(previousEnvelope.receipt.previousReceiptHash)
|
|
410
|
+
: envelope.receipt.sequence === 0;
|
|
411
|
+
const continuityVerified = envelope.receipt.sequence === 0
|
|
412
|
+
? !previousEnvelope
|
|
413
|
+
: previousEnvelopeProvided
|
|
414
|
+
&& previousReceiptHashMatch
|
|
415
|
+
&& previousSequenceMatch
|
|
416
|
+
&& previousAttestingSignerMatch
|
|
417
|
+
&& previousAttestationVerified
|
|
418
|
+
&& previousSequenceMonotonic;
|
|
419
|
+
return {
|
|
420
|
+
positionReceiptHashMatch,
|
|
421
|
+
sequenceMatch,
|
|
422
|
+
sequenceMonotonic,
|
|
423
|
+
attestingSignerMatch,
|
|
424
|
+
attestationVerified,
|
|
425
|
+
previousEnvelopeProvided,
|
|
426
|
+
previousReceiptHashMatch,
|
|
427
|
+
previousSequenceMatch,
|
|
428
|
+
continuityVerified,
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
async function verifyLPPositionReceiptEnvelopeChain(input) {
|
|
432
|
+
if (input.envelopes.length === 0) {
|
|
433
|
+
throw new errors_1.ValidationError("position receipt chain must include at least one envelope", {
|
|
434
|
+
code: "FUND_POSITION_CHAIN_REQUIRED",
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
const envelopes = input.envelopes.map((envelope) => validateLPPositionReceiptEnvelope(envelope));
|
|
438
|
+
const first = envelopes[0];
|
|
439
|
+
const firstSigner = first.attestation.managerXonly.toLowerCase();
|
|
440
|
+
const stepChecks = [];
|
|
441
|
+
for (let index = 0; index < envelopes.length; index += 1) {
|
|
442
|
+
stepChecks.push(await verifyLPPositionReceiptEnvelope({
|
|
443
|
+
envelope: envelopes[index],
|
|
444
|
+
expectedManagerXonly: input.expectedManagerXonly,
|
|
445
|
+
previousEnvelope: index > 0 ? envelopes[index - 1] : undefined,
|
|
446
|
+
}));
|
|
447
|
+
}
|
|
448
|
+
const lineage = (0, lineage_1.verifyHashLinkedLineage)({
|
|
449
|
+
entries: envelopes,
|
|
450
|
+
summarize: (envelope) => summarizeLPPositionReceipt(envelope.receipt),
|
|
451
|
+
getPreviousHash: (envelope) => envelope.receipt.previousReceiptHash,
|
|
452
|
+
isGenesis: (envelope) => envelope.receipt.sequence === 0 && !envelope.receipt.previousReceiptHash,
|
|
453
|
+
consistencyChecks: {
|
|
454
|
+
fundConsistent: (envelope, base) => envelope.receipt.fundId === base.receipt.fundId,
|
|
455
|
+
positionConsistent: (envelope, base) => envelope.receipt.positionId === base.receipt.positionId,
|
|
456
|
+
lpConsistent: (envelope, base) => envelope.receipt.lpId === base.receipt.lpId,
|
|
457
|
+
callConsistent: (envelope, base) => envelope.receipt.callId === base.receipt.callId,
|
|
458
|
+
currencyConsistent: (envelope, base) => envelope.receipt.currencyAssetId === base.receipt.currencyAssetId,
|
|
459
|
+
lpXonlyConsistent: (envelope, base) => envelope.receipt.lpXonly === base.receipt.lpXonly,
|
|
460
|
+
},
|
|
461
|
+
});
|
|
462
|
+
const startsAtGenesis = lineage.startsAtGenesis;
|
|
463
|
+
const latestSequence = envelopes.at(-1)?.receipt.sequence;
|
|
464
|
+
const latestSequenceCovered = latestSequence === envelopes.length - 1;
|
|
465
|
+
const sequenceContiguous = envelopes.every((envelope, index) => envelope.receipt.sequence === first.receipt.sequence + index);
|
|
466
|
+
const fundConsistent = lineage.consistency.fundConsistent;
|
|
467
|
+
const positionConsistent = lineage.consistency.positionConsistent;
|
|
468
|
+
const lpConsistent = lineage.consistency.lpConsistent;
|
|
469
|
+
const callConsistent = lineage.consistency.callConsistent;
|
|
470
|
+
const currencyConsistent = lineage.consistency.currencyConsistent;
|
|
471
|
+
const lpXonlyConsistent = lineage.consistency.lpXonlyConsistent;
|
|
472
|
+
const attestingSignerConsistent = envelopes.every((envelope) => envelope.attestation.managerXonly.toLowerCase() === firstSigner);
|
|
473
|
+
const allPositionReceiptHashMatch = stepChecks.every((check) => check.positionReceiptHashMatch);
|
|
474
|
+
const allSequenceMatch = stepChecks.every((check) => check.sequenceMatch);
|
|
475
|
+
const allSequenceMonotonic = stepChecks.every((check) => check.sequenceMonotonic);
|
|
476
|
+
const allAttestingSignerMatch = stepChecks.every((check) => check.attestingSignerMatch);
|
|
477
|
+
const allAttestationVerified = stepChecks.every((check) => check.attestationVerified);
|
|
478
|
+
const allContinuityVerified = stepChecks.every((check, index) => (index === 0 ? true : check.continuityVerified));
|
|
479
|
+
const fullChainVerified = startsAtGenesis
|
|
480
|
+
&& latestSequenceCovered
|
|
481
|
+
&& sequenceContiguous
|
|
482
|
+
&& fundConsistent
|
|
483
|
+
&& positionConsistent
|
|
484
|
+
&& lpConsistent
|
|
485
|
+
&& callConsistent
|
|
486
|
+
&& currencyConsistent
|
|
487
|
+
&& lpXonlyConsistent
|
|
488
|
+
&& attestingSignerConsistent
|
|
489
|
+
&& allPositionReceiptHashMatch
|
|
490
|
+
&& allSequenceMatch
|
|
491
|
+
&& allSequenceMonotonic
|
|
492
|
+
&& allAttestingSignerMatch
|
|
493
|
+
&& allAttestationVerified
|
|
494
|
+
&& allContinuityVerified;
|
|
495
|
+
return {
|
|
496
|
+
chainLength: envelopes.length,
|
|
497
|
+
latestSequence,
|
|
498
|
+
startsAtGenesis,
|
|
499
|
+
latestSequenceCovered,
|
|
500
|
+
sequenceContiguous,
|
|
501
|
+
fundConsistent,
|
|
502
|
+
positionConsistent,
|
|
503
|
+
lpConsistent,
|
|
504
|
+
callConsistent,
|
|
505
|
+
currencyConsistent,
|
|
506
|
+
lpXonlyConsistent,
|
|
507
|
+
attestingSignerConsistent,
|
|
508
|
+
allPositionReceiptHashMatch,
|
|
509
|
+
allSequenceMatch,
|
|
510
|
+
allSequenceMonotonic,
|
|
511
|
+
allAttestingSignerMatch,
|
|
512
|
+
allAttestationVerified,
|
|
513
|
+
allContinuityVerified,
|
|
514
|
+
fullChainVerified,
|
|
515
|
+
stepChecks,
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
async function signLPPositionReceipt(input) {
|
|
519
|
+
const receipt = validateLPPositionReceipt(input.receipt);
|
|
520
|
+
if (input.signer.type !== "schnorrPrivkeyHex") {
|
|
521
|
+
throw new errors_1.ValidationError("position receipt attestation signer must be schnorrPrivkeyHex", {
|
|
522
|
+
code: "FUND_POSITION_ATTESTATION_SIGNER_TYPE_INVALID",
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
const derivedManagerXonly = await (0, schnorr_1.schnorrPublicKeyFromPrivkeyHex)(input.signer.privkeyHex);
|
|
526
|
+
if (derivedManagerXonly !== input.managerXonly.toLowerCase()) {
|
|
527
|
+
throw new errors_1.ValidationError("position receipt attestation signer does not match managerXonly", {
|
|
528
|
+
code: "FUND_POSITION_ATTESTATION_SIGNER_MISMATCH",
|
|
529
|
+
managerXonly: input.managerXonly,
|
|
530
|
+
derivedManagerXonly,
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
const receiptSummary = summarizeLPPositionReceipt(receipt);
|
|
534
|
+
const digestHex = buildPositionReceiptAttestationMessageHash({
|
|
535
|
+
fundId: receipt.fundId,
|
|
536
|
+
positionId: receipt.positionId,
|
|
537
|
+
positionReceiptHash: receiptSummary.hash,
|
|
538
|
+
sequence: receipt.sequence,
|
|
539
|
+
});
|
|
540
|
+
const signedAt = input.signedAt ?? new Date().toISOString();
|
|
541
|
+
assertIsoDateLike(signedAt, "FUND_POSITION_ATTESTATION_SIGNED_AT_INVALID", "signedAt must be an ISO timestamp");
|
|
542
|
+
return validateLPPositionReceiptEnvelope({
|
|
543
|
+
receipt,
|
|
544
|
+
attestation: {
|
|
545
|
+
positionReceiptHash: receiptSummary.hash,
|
|
546
|
+
sequence: receipt.sequence,
|
|
547
|
+
managerXonly: derivedManagerXonly,
|
|
548
|
+
signedAt,
|
|
549
|
+
signature: await (0, schnorr_1.schnorrSignHex)(digestHex, input.signer.privkeyHex),
|
|
550
|
+
scheme: "bip340-sha256",
|
|
551
|
+
},
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
function validateDistributionAgainstReceipt(receipt, distribution) {
|
|
555
|
+
const receiptHash = summarizeLPPositionReceipt(receipt).hash;
|
|
556
|
+
const result = {
|
|
557
|
+
fundIdMatch: receipt.fundId === distribution.fundId,
|
|
558
|
+
lpIdMatch: receipt.lpId === distribution.lpId,
|
|
559
|
+
positionIdMatch: receipt.positionId === distribution.positionId,
|
|
560
|
+
positionReceiptHashMatch: receiptHash === distribution.positionReceiptHash,
|
|
561
|
+
positionStatusEligible: receipt.status !== "CLOSED",
|
|
562
|
+
};
|
|
563
|
+
if (!result.fundIdMatch) {
|
|
564
|
+
throw new errors_1.ValidationError("Distribution fundId does not match LP position receipt", {
|
|
565
|
+
code: "FUND_DISTRIBUTION_FUND_ID_MISMATCH",
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
if (!result.lpIdMatch) {
|
|
569
|
+
throw new errors_1.ValidationError("Distribution lpId does not match LP position receipt", {
|
|
570
|
+
code: "FUND_DISTRIBUTION_LP_ID_MISMATCH",
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
if (!result.positionIdMatch) {
|
|
574
|
+
throw new errors_1.ValidationError("Distribution positionId does not match LP position receipt", {
|
|
575
|
+
code: "FUND_DISTRIBUTION_POSITION_ID_MISMATCH",
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
if (!result.positionReceiptHashMatch) {
|
|
579
|
+
throw new errors_1.ValidationError("Distribution positionReceiptHash does not match LP position receipt", {
|
|
580
|
+
code: "FUND_DISTRIBUTION_RECEIPT_HASH_MISMATCH",
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
if (!result.positionStatusEligible) {
|
|
584
|
+
throw new errors_1.ValidationError("CLOSED LP position receipt cannot produce a new distribution descriptor", {
|
|
585
|
+
code: "FUND_DISTRIBUTION_POSITION_CLOSED",
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
if (receipt.distributedAmount + distribution.amountSat > receipt.fundedAmount) {
|
|
589
|
+
throw new errors_1.ValidationError("Distribution amount would exceed fundedAmount for the LP position receipt", {
|
|
590
|
+
code: "FUND_DISTRIBUTION_EXCEEDS_POSITION",
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
return result;
|
|
594
|
+
}
|
|
595
|
+
function validateClosingAgainstReceipt(receipt, closing) {
|
|
596
|
+
const receiptHash = summarizeLPPositionReceipt(receipt).hash;
|
|
597
|
+
const result = {
|
|
598
|
+
positionReceiptHashMatch: receiptHash === closing.positionReceiptHash,
|
|
599
|
+
finalDistributionHashesPresent: closing.finalDistributionHashes.length > 0,
|
|
600
|
+
positionStatusEligible: (receipt.status === "FULLY_DISTRIBUTED" || receipt.status === "CLOSED")
|
|
601
|
+
&& receipt.distributedAmount === receipt.fundedAmount,
|
|
602
|
+
};
|
|
603
|
+
if (!result.positionReceiptHashMatch) {
|
|
604
|
+
throw new errors_1.ValidationError("Fund closing positionReceiptHash does not match LP position receipt", {
|
|
605
|
+
code: "FUND_CLOSING_RECEIPT_HASH_MISMATCH",
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
if (!result.finalDistributionHashesPresent) {
|
|
609
|
+
throw new errors_1.ValidationError("Fund closing must include at least one finalDistributionHash", {
|
|
610
|
+
code: "FUND_CLOSING_DISTRIBUTION_HASHES_REQUIRED",
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
if (!result.positionStatusEligible) {
|
|
614
|
+
throw new errors_1.ValidationError("Fund closing requires a FULLY_DISTRIBUTED or CLOSED LP position receipt", {
|
|
615
|
+
code: "FUND_CLOSING_POSITION_STATUS_INVALID",
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
return result;
|
|
619
|
+
}
|
|
620
|
+
function buildClaimedCapitalCallState(input) {
|
|
621
|
+
const previous = validateCapitalCallState(input.previous);
|
|
622
|
+
if (previous.status !== "OPEN") {
|
|
623
|
+
throw new errors_1.ValidationError("Only OPEN capital call states can be claimed", {
|
|
624
|
+
code: "FUND_CAPITAL_CALL_CLAIM_STAGE_INVALID",
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
assertIsoDateLike(input.claimedAt, "FUND_CALL_CLAIMED_AT_INVALID", "claimedAt must be an ISO timestamp");
|
|
628
|
+
return validateCapitalCallState({
|
|
629
|
+
...previous,
|
|
630
|
+
status: "CLAIMED",
|
|
631
|
+
claimedAt: input.claimedAt,
|
|
632
|
+
previousStateHash: summarizeCapitalCallState(previous).hash,
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
function buildRefundOnlyCapitalCallState(input) {
|
|
636
|
+
const previous = validateCapitalCallState(input.previous);
|
|
637
|
+
if (previous.status !== "OPEN") {
|
|
638
|
+
throw new errors_1.ValidationError("Only OPEN capital call states can roll over to REFUND_ONLY", {
|
|
639
|
+
code: "FUND_CAPITAL_CALL_ROLLOVER_STAGE_INVALID",
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
return validateCapitalCallState({
|
|
643
|
+
...previous,
|
|
644
|
+
status: "REFUND_ONLY",
|
|
645
|
+
previousStateHash: summarizeCapitalCallState(previous).hash,
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
function buildRefundedCapitalCallState(input) {
|
|
649
|
+
const previous = validateCapitalCallState(input.previous);
|
|
650
|
+
if (previous.status !== "REFUND_ONLY") {
|
|
651
|
+
throw new errors_1.ValidationError("Only REFUND_ONLY capital call states can be refunded", {
|
|
652
|
+
code: "FUND_CAPITAL_CALL_REFUND_STAGE_INVALID",
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
assertIsoDateLike(input.refundedAt, "FUND_CALL_REFUNDED_AT_INVALID", "refundedAt must be an ISO timestamp");
|
|
656
|
+
return validateCapitalCallState({
|
|
657
|
+
...previous,
|
|
658
|
+
status: "REFUNDED",
|
|
659
|
+
refundedAt: input.refundedAt,
|
|
660
|
+
previousStateHash: summarizeCapitalCallState(previous).hash,
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
function buildLPPositionReceipt(input) {
|
|
664
|
+
const capitalCall = validateCapitalCallState(input.capitalCall);
|
|
665
|
+
assertIsoDateLike(input.effectiveAt, "FUND_POSITION_EFFECTIVE_AT_INVALID", "effectiveAt must be an ISO timestamp");
|
|
666
|
+
assertNonEmptyString(input.positionId, "FUND_POSITION_ID_INVALID", "positionId must be a non-empty string");
|
|
667
|
+
const status = input.status ?? "ACTIVE";
|
|
668
|
+
assertReceiptStatus(status);
|
|
669
|
+
return validateLPPositionReceipt({
|
|
670
|
+
schemaVersion: "lp-position-receipt/v2",
|
|
671
|
+
positionId: input.positionId,
|
|
672
|
+
fundId: capitalCall.fundId,
|
|
673
|
+
lpId: capitalCall.lpId,
|
|
674
|
+
callId: capitalCall.callId,
|
|
675
|
+
currencyAssetId: capitalCall.currencyAssetId,
|
|
676
|
+
lpXonly: capitalCall.lpXonly,
|
|
677
|
+
sequence: 0,
|
|
678
|
+
committedAmount: capitalCall.amount,
|
|
679
|
+
fundedAmount: capitalCall.amount,
|
|
680
|
+
distributedAmount: 0,
|
|
681
|
+
distributionCount: 0,
|
|
682
|
+
effectiveAt: input.effectiveAt,
|
|
683
|
+
status,
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
function applyDistributionToReceipt(input) {
|
|
687
|
+
const receipt = validateLPPositionReceipt(input.receipt);
|
|
688
|
+
const distribution = validateDistributionDescriptor(input.distribution);
|
|
689
|
+
validateDistributionAgainstReceipt(receipt, distribution);
|
|
690
|
+
const nextDistributedAmount = receipt.distributedAmount + distribution.amountSat;
|
|
691
|
+
const effectiveAt = input.effectiveAt ?? distribution.approvedAt;
|
|
692
|
+
assertIsoDateLike(effectiveAt, "FUND_POSITION_EFFECTIVE_AT_INVALID", "effectiveAt must be an ISO timestamp");
|
|
693
|
+
const status = input.status
|
|
694
|
+
?? (nextDistributedAmount === receipt.fundedAmount ? "FULLY_DISTRIBUTED" : "PARTIALLY_DISTRIBUTED");
|
|
695
|
+
assertReceiptStatus(status);
|
|
696
|
+
return validateLPPositionReceipt({
|
|
697
|
+
...receipt,
|
|
698
|
+
distributedAmount: nextDistributedAmount,
|
|
699
|
+
distributionCount: receipt.distributionCount + 1,
|
|
700
|
+
effectiveAt,
|
|
701
|
+
lastDistributedAt: effectiveAt,
|
|
702
|
+
status,
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
function applyDistributionsToReceipt(input) {
|
|
706
|
+
if (!Array.isArray(input.distributions) || input.distributions.length === 0) {
|
|
707
|
+
throw new errors_1.ValidationError("distributions must be a non-empty array", {
|
|
708
|
+
code: "FUND_DISTRIBUTIONS_REQUIRED",
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
return input.distributions.reduce((current, distribution) => applyDistributionToReceipt({ receipt: current, distribution }), validateLPPositionReceipt(input.receipt));
|
|
712
|
+
}
|
|
713
|
+
function reconcileLPPositionReceipt(input) {
|
|
714
|
+
const previousEnvelope = validateLPPositionReceiptEnvelope(input.previousEnvelope);
|
|
715
|
+
if (!Array.isArray(input.distributions) || input.distributions.length === 0) {
|
|
716
|
+
throw new errors_1.ValidationError("distributions must be a non-empty array", {
|
|
717
|
+
code: "FUND_DISTRIBUTIONS_REQUIRED",
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
let workingReceipt = previousEnvelope.receipt;
|
|
721
|
+
for (const distribution of input.distributions) {
|
|
722
|
+
workingReceipt = applyDistributionToReceipt({
|
|
723
|
+
receipt: workingReceipt,
|
|
724
|
+
distribution: validateDistributionDescriptor(distribution),
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
return validateLPPositionReceipt({
|
|
728
|
+
...workingReceipt,
|
|
729
|
+
sequence: previousEnvelope.receipt.sequence + 1,
|
|
730
|
+
previousReceiptHash: summarizeLPPositionReceipt(previousEnvelope.receipt).hash,
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
function buildDistributionDescriptor(input) {
|
|
734
|
+
const receipt = validateLPPositionReceipt(input.receipt);
|
|
735
|
+
assertNonEmptyString(input.distributionId, "FUND_DISTRIBUTION_ID_INVALID", "distributionId must be a non-empty string");
|
|
736
|
+
assertNonEmptyString(input.assetId, "FUND_DISTRIBUTION_ASSET_INVALID", "assetId must be a non-empty string");
|
|
737
|
+
assertFinitePositiveNumber(input.amountSat, "FUND_DISTRIBUTION_AMOUNT_INVALID", "amountSat must be a positive finite number");
|
|
738
|
+
assertIsoDateLike(input.approvedAt, "FUND_DISTRIBUTION_APPROVED_AT_INVALID", "approvedAt must be an ISO timestamp");
|
|
739
|
+
return validateDistributionDescriptor({
|
|
740
|
+
distributionId: input.distributionId,
|
|
741
|
+
positionId: receipt.positionId,
|
|
742
|
+
fundId: receipt.fundId,
|
|
743
|
+
lpId: receipt.lpId,
|
|
744
|
+
assetId: input.assetId,
|
|
745
|
+
amountSat: input.amountSat,
|
|
746
|
+
approvedAt: input.approvedAt,
|
|
747
|
+
positionReceiptHash: summarizeLPPositionReceipt(receipt).hash,
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
function buildFundClosingDescriptor(input) {
|
|
751
|
+
const receipt = validateLPPositionReceipt(input.receipt);
|
|
752
|
+
assertNonEmptyString(input.closingId, "FUND_CLOSING_ID_INVALID", "closingId must be a non-empty string");
|
|
753
|
+
assertArrayOfHashes(input.finalDistributionHashes, "FUND_CLOSING_DISTRIBUTION_HASHES_INVALID", "finalDistributionHashes must be an array of 64-character hex strings");
|
|
754
|
+
assertIsoDateLike(input.closedAt, "FUND_CLOSING_CLOSED_AT_INVALID", "closedAt must be an ISO timestamp");
|
|
755
|
+
const closingReason = input.closingReason ?? "LIQUIDATED";
|
|
756
|
+
assertClosingReason(closingReason);
|
|
757
|
+
return validateFundClosingDescriptor({
|
|
758
|
+
closingId: input.closingId,
|
|
759
|
+
fundId: receipt.fundId,
|
|
760
|
+
lpId: receipt.lpId,
|
|
761
|
+
positionId: receipt.positionId,
|
|
762
|
+
positionReceiptHash: summarizeLPPositionReceipt(receipt).hash,
|
|
763
|
+
finalDistributionHashes: input.finalDistributionHashes,
|
|
764
|
+
closedAt: input.closedAt,
|
|
765
|
+
closingReason,
|
|
766
|
+
});
|
|
767
|
+
}
|