@hyperscale0/udl 1.0.0 → 1.0.123
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/CHANGELOG.md +5 -216
- package/LICENSE.md +356 -0
- package/NOTICE.md +8 -0
- package/README.md +14 -106
- package/dist/canonical.d.ts.map +1 -1
- package/dist/canonical.js +1 -29
- package/dist/canonical.js.map +1 -1
- package/dist/diagnostics.d.ts +13 -165
- package/dist/diagnostics.d.ts.map +1 -1
- package/dist/diagnostics.js +33 -157
- package/dist/diagnostics.js.map +1 -1
- package/dist/field-value.d.ts +5 -0
- package/dist/field-value.d.ts.map +1 -0
- package/dist/field-value.js +85 -0
- package/dist/field-value.js.map +1 -0
- package/dist/finance.d.ts +10 -53
- package/dist/finance.d.ts.map +1 -1
- package/dist/finance.js +252 -536
- package/dist/finance.js.map +1 -1
- package/dist/index.d.ts +9 -13
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -9
- package/dist/index.js.map +1 -1
- package/dist/json.d.ts +2 -0
- package/dist/json.d.ts.map +1 -0
- package/dist/json.js +30 -0
- package/dist/json.js.map +1 -0
- package/dist/limits.d.ts +3 -28
- package/dist/limits.d.ts.map +1 -1
- package/dist/limits.js +5 -31
- package/dist/limits.js.map +1 -1
- package/dist/object-contract.d.ts +140 -0
- package/dist/object-contract.d.ts.map +1 -0
- package/dist/object-contract.js +314 -0
- package/dist/object-contract.js.map +1 -0
- package/dist/object-semantics-schema.d.ts +5710 -0
- package/dist/object-semantics-schema.d.ts.map +1 -0
- package/dist/object-semantics-schema.js +119 -0
- package/dist/object-semantics-schema.js.map +1 -0
- package/dist/object-semantics.d.ts +6 -0
- package/dist/object-semantics.d.ts.map +1 -0
- package/dist/object-semantics.js +279 -0
- package/dist/object-semantics.js.map +1 -0
- package/dist/object-structures.d.ts +4 -0
- package/dist/object-structures.d.ts.map +1 -0
- package/dist/object-structures.js +134 -0
- package/dist/object-structures.js.map +1 -0
- package/dist/reporting-validation.d.ts +8 -0
- package/dist/reporting-validation.d.ts.map +1 -0
- package/dist/reporting-validation.js +252 -0
- package/dist/reporting-validation.js.map +1 -0
- package/dist/reporting.d.ts +477 -0
- package/dist/reporting.d.ts.map +1 -0
- package/dist/reporting.js +169 -0
- package/dist/reporting.js.map +1 -0
- package/dist/schema.d.ts +5572 -2710
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +444 -1121
- package/dist/schema.js.map +1 -1
- package/dist/validation.d.ts +10 -73
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +1109 -2569
- package/dist/validation.js.map +1 -1
- package/docs/assets/brand/manifest.json +30 -0
- package/docs/assets/brand/udl-horizontal-white.svg +1 -0
- package/docs/assets/brand/udl-horizontal.svg +1 -0
- package/docs/assets/brand/udl-stacked-white.svg +1 -0
- package/docs/assets/brand/udl-stacked.svg +1 -0
- package/docs/assets/udl.svg +11 -0
- package/package.json +13 -22
- package/spec/README.md +300 -172
- package/spec/cars.udl.json +249 -0
- package/spec/udl.schema.json +3066 -2242
- package/src/canonical.ts +1 -35
- package/src/diagnostics.ts +76 -209
- package/src/field-value.ts +87 -0
- package/src/finance.ts +327 -832
- package/src/index.ts +18 -95
- package/src/json.ts +34 -0
- package/src/limits.ts +5 -31
- package/src/object-contract.ts +559 -0
- package/src/object-semantics-schema.ts +148 -0
- package/src/object-semantics.ts +378 -0
- package/src/object-structures.ts +255 -0
- package/src/reporting-validation.ts +383 -0
- package/src/reporting.ts +178 -0
- package/src/schema.ts +510 -1277
- package/src/validation.ts +1630 -4545
- package/LICENSE +0 -661
- package/LICENSING.md +0 -51
- package/TRADEMARKS.md +0 -35
- package/conformance/README.md +0 -82
- package/conformance/evolution/action-contract.expected.json +0 -10
- package/conformance/evolution/action-contract.live.udl +0 -44
- package/conformance/evolution/action-contract.next.udl +0 -45
- package/conformance/evolution/product-identity.expected.json +0 -10
- package/conformance/evolution/product-identity.live.udl +0 -55
- package/conformance/evolution/product-identity.next.udl +0 -55
- package/conformance/evolution/version-required.expected.json +0 -10
- package/conformance/evolution/version-required.live.udl +0 -55
- package/conformance/evolution/version-required.next.udl +0 -58
- package/conformance/invalid/action-without-transition.expected.json +0 -10
- package/conformance/invalid/action-without-transition.udl +0 -60
- package/conformance/invalid/agent-description-too-long.expected.json +0 -10
- package/conformance/invalid/agent-description-too-long.udl +0 -57
- package/conformance/invalid/blank-title.expected.json +0 -10
- package/conformance/invalid/blank-title.udl +0 -54
- package/conformance/invalid/composition-dial-duplicate-key.expected.json +0 -10
- package/conformance/invalid/composition-dial-duplicate-key.udl +0 -73
- package/conformance/invalid/depth-budget.expected.json +0 -10
- package/conformance/invalid/depth-budget.udl +0 -49
- package/conformance/invalid/duplicate-subject.expected.json +0 -10
- package/conformance/invalid/duplicate-subject.udl +0 -230
- package/conformance/invalid/forged-effects.expected.json +0 -10
- package/conformance/invalid/forged-effects.udl +0 -76
- package/conformance/invalid/format-version.expected.json +0 -10
- package/conformance/invalid/format-version.udl +0 -54
- package/conformance/invalid/instrument-id-not-snake-case.expected.json +0 -10
- package/conformance/invalid/instrument-id-not-snake-case.udl +0 -54
- package/conformance/invalid/invalid-aggregate-gate-shape.expected.json +0 -10
- package/conformance/invalid/invalid-aggregate-gate-shape.udl +0 -1819
- package/conformance/invalid/invalid-check-duration.expected.json +0 -10
- package/conformance/invalid/invalid-check-duration.udl +0 -219
- package/conformance/invalid/invalid-dial-anchor.expected.json +0 -10
- package/conformance/invalid/invalid-dial-anchor.udl +0 -219
- package/conformance/invalid/invalid-exception-parent-ref.expected.json +0 -10
- package/conformance/invalid/invalid-exception-parent-ref.udl +0 -2487
- package/conformance/invalid/invalid-exposure-shape.expected.json +0 -10
- package/conformance/invalid/invalid-exposure-shape.udl +0 -1819
- package/conformance/invalid/invalid-remainder.expected.json +0 -10
- package/conformance/invalid/invalid-remainder.udl +0 -220
- package/conformance/invalid/invalid-schema-keyword.expected.json +0 -10
- package/conformance/invalid/invalid-schema-keyword.udl +0 -220
- package/conformance/invalid/invalid-utf8.expected.json +0 -10
- package/conformance/invalid/invalid-utf8.udl +0 -1
- package/conformance/invalid/malformed-json.expected.json +0 -10
- package/conformance/invalid/malformed-json.udl +0 -1
- package/conformance/invalid/missing-create-action.expected.json +0 -10
- package/conformance/invalid/missing-create-action.udl +0 -48
- package/conformance/invalid/missing-exception-amount-field.expected.json +0 -10
- package/conformance/invalid/missing-exception-amount-field.udl +0 -2487
- package/conformance/invalid/missing-exception-contract.expected.json +0 -14
- package/conformance/invalid/missing-exception-contract.udl +0 -2523
- package/conformance/invalid/missing-exception-reason-field.expected.json +0 -10
- package/conformance/invalid/missing-exception-reason-field.udl +0 -2487
- package/conformance/invalid/not-an-object.expected.json +0 -10
- package/conformance/invalid/not-an-object.udl +0 -1
- package/conformance/invalid/payout-reconcile-not-a-bank-debit.expected.json +0 -10
- package/conformance/invalid/payout-reconcile-not-a-bank-debit.udl +0 -259
- package/conformance/invalid/quote-freeze-set-incomplete.expected.json +0 -10
- package/conformance/invalid/quote-freeze-set-incomplete.udl +0 -261
- package/conformance/invalid/quote-named-reference-gate.expected.json +0 -10
- package/conformance/invalid/quote-named-reference-gate.udl +0 -50
- package/conformance/invalid/reconcile-named-reference-gate.expected.json +0 -10
- package/conformance/invalid/reconcile-named-reference-gate.udl +0 -50
- package/conformance/invalid/unknown-key.expected.json +0 -10
- package/conformance/invalid/unknown-key.udl +0 -55
- package/conformance/invalid/unknown-reference-gate-field.expected.json +0 -10
- package/conformance/invalid/unknown-reference-gate-field.udl +0 -1819
- package/conformance/invalid/unknown-required-field.expected.json +0 -10
- package/conformance/invalid/unknown-required-field.udl +0 -220
- package/conformance/invalid/unreachable-state.expected.json +0 -10
- package/conformance/invalid/unreachable-state.udl +0 -55
- package/conformance/invalid/wrong-exception-amount-field.expected.json +0 -10
- package/conformance/invalid/wrong-exception-amount-field.udl +0 -2487
- package/conformance/invalid/wrong-exception-reason-field.expected.json +0 -10
- package/conformance/invalid/wrong-exception-reason-field.udl +0 -2487
- package/conformance/valid/agent-description.expected.json +0 -6
- package/conformance/valid/agent-description.udl +0 -66
- package/conformance/valid/cards.expected.json +0 -6
- package/conformance/valid/cards.udl +0 -1579
- package/conformance/valid/commerce-escrow.expected.json +0 -6
- package/conformance/valid/commerce-escrow.udl +0 -1512
- package/conformance/valid/compiled-crowdfunding.expected.json +0 -6
- package/conformance/valid/compiled-crowdfunding.udl +0 -1843
- package/conformance/valid/compiled-watch-club.expected.json +0 -6
- package/conformance/valid/compiled-watch-club.udl +0 -2486
- package/conformance/valid/complete-contract.expected.json +0 -6
- package/conformance/valid/complete-contract.udl +0 -218
- package/conformance/valid/effect-signatures.expected.json +0 -6
- package/conformance/valid/effect-signatures.udl +0 -75
- package/conformance/valid/hand-edited.expected.json +0 -6
- package/conformance/valid/hand-edited.udl +0 -1
- package/conformance/valid/insured-car-marketplace.expected.json +0 -6
- package/conformance/valid/insured-car-marketplace.udl +0 -1050
- package/conformance/valid/insured-travel.expected.json +0 -6
- package/conformance/valid/insured-travel.udl +0 -3469
- package/conformance/valid/minimal.expected.json +0 -6
- package/conformance/valid/minimal.udl +0 -54
- package/conformance/valid/protection.expected.json +0 -6
- package/conformance/valid/protection.udl +0 -1551
- package/conformance/valid/string-escaping.expected.json +0 -6
- package/conformance/valid/string-escaping.udl +0 -54
- package/dist/check-profiles.d.ts +0 -57
- package/dist/check-profiles.d.ts.map +0 -1
- package/dist/check-profiles.js +0 -62
- package/dist/check-profiles.js.map +0 -1
- package/dist/cli.d.ts +0 -3
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js +0 -184
- package/dist/cli.js.map +0 -1
- package/dist/effects.d.ts +0 -41
- package/dist/effects.d.ts.map +0 -1
- package/dist/effects.js +0 -110
- package/dist/effects.js.map +0 -1
- package/dist/evolution.d.ts +0 -120
- package/dist/evolution.d.ts.map +0 -1
- package/dist/evolution.js +0 -567
- package/dist/evolution.js.map +0 -1
- package/dist/parser.d.ts +0 -4
- package/dist/parser.d.ts.map +0 -1
- package/dist/parser.js +0 -44
- package/dist/parser.js.map +0 -1
- package/docs/README.md +0 -14
- package/docs/guide/01-a-document.md +0 -37
- package/docs/guide/02-money-steps.md +0 -23
- package/docs/guide/03-laws.md +0 -18
- package/docs/guide/04-fees-and-remainder.md +0 -15
- package/docs/guide/05-checks-updates-dials.md +0 -7
- package/docs/guide/06-effects.md +0 -11
- package/docs/guide/07-evolution.md +0 -11
- package/docs/guide/08-implementing.md +0 -13
- package/docs/llms-full.txt +0 -1241
- package/docs/llms.txt +0 -14
- package/docs/reference/canonical.md +0 -16
- package/docs/reference/clauses.md +0 -986
- package/docs/reference/cli.md +0 -22
- package/docs/reference/diagnostics.md +0 -32
- package/skills/udl/SKILL.md +0 -28
- package/src/check-profiles.ts +0 -80
- package/src/cli.ts +0 -196
- package/src/effects.ts +0 -163
- package/src/evolution.ts +0 -965
- package/src/parser.ts +0 -58
package/dist/finance.js
CHANGED
|
@@ -1,567 +1,283 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
}
|
|
12
|
-
function withoutPiece(amounts, amount) {
|
|
13
|
-
const index = amounts.indexOf(amount);
|
|
14
|
-
return amounts.filter((_, position) => position !== index);
|
|
15
|
-
}
|
|
16
|
-
/**
|
|
17
|
-
* One financial typestate owner for canonical UDL and static instrument contracts.
|
|
18
|
-
* It proves a tracked escrow account holds an exact set of funded amounts,
|
|
19
|
-
* models held transfers through reserve/post/void, consumes every direct
|
|
20
|
-
* debit piece by piece, and makes the quoted refund's sole remainder an
|
|
21
|
-
* exactly-once penalty payout.
|
|
22
|
-
*/
|
|
23
|
-
export function analyzeInstrumentFinance(instrument, options = {}) {
|
|
24
|
-
const issues = [];
|
|
25
|
-
const seenIssues = new Set();
|
|
26
|
-
const add = (path, message, code = "UDL4001") => {
|
|
27
|
-
const key = `${path.join(".")}\0${message}`;
|
|
28
|
-
if (seenIssues.has(key))
|
|
29
|
-
return;
|
|
30
|
-
seenIssues.add(key);
|
|
31
|
-
issues.push({ code, message, path });
|
|
32
|
-
};
|
|
33
|
-
const admissionProblem = financeAdmissionProblem(instrument);
|
|
34
|
-
if (admissionProblem) {
|
|
35
|
-
add(["lifecycle"], admissionProblem, "UDL1004");
|
|
36
|
-
return issues;
|
|
1
|
+
const add = (a, b, sign = 1n) => {
|
|
2
|
+
if (a === null || b === null)
|
|
3
|
+
return null;
|
|
4
|
+
const result = new Map(a);
|
|
5
|
+
for (const [key, value] of b) {
|
|
6
|
+
const next = (result.get(key) ?? 0n) + value * sign;
|
|
7
|
+
if (next)
|
|
8
|
+
result.set(key, next);
|
|
9
|
+
else
|
|
10
|
+
result.delete(key);
|
|
37
11
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
12
|
+
return result;
|
|
13
|
+
};
|
|
14
|
+
const show = (sum) => sum === null
|
|
15
|
+
? "unknown"
|
|
16
|
+
: JSON.stringify([...sum]
|
|
17
|
+
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
|
|
18
|
+
.map(([key, value]) => [key, String(value)]));
|
|
19
|
+
const same = (a, b) => show(a) === show(b);
|
|
20
|
+
/** Join loop balances conservatively; a balance equality proves an unknown balance empty. */
|
|
21
|
+
export function analyzeInstrumentFinance(instrument, document) {
|
|
22
|
+
const owned = new Set(instrument.fields
|
|
23
|
+
.filter((f) => f.type === "account" && f.owner === "self")
|
|
24
|
+
.map((f) => `self.${f.name}`));
|
|
25
|
+
const shared = new Set();
|
|
26
|
+
for (const owner of document?.instruments ?? []) {
|
|
27
|
+
if (owner.id === instrument.id)
|
|
45
28
|
continue;
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const initial = applyEffects(account, "create", { balance: EMPTY, holds: {} }, createEffects, reservations, refundSourceAccounts.has(account), add);
|
|
71
|
-
const states = new Map([
|
|
72
|
-
[instrument.lifecycle.initial, new Map([[stateKey(initial), initial]])],
|
|
73
|
-
]);
|
|
74
|
-
pathVariants += 1;
|
|
75
|
-
const pending = [instrument.lifecycle.initial];
|
|
76
|
-
while (pending.length > 0) {
|
|
77
|
-
const from = pending.shift();
|
|
78
|
-
const sourceStates = states.get(from);
|
|
79
|
-
if (!sourceStates)
|
|
80
|
-
continue;
|
|
81
|
-
for (const [action, transition] of Object.entries(instrument.lifecycle.transitions)) {
|
|
82
|
-
if (!transition.from.includes(from))
|
|
83
|
-
continue;
|
|
84
|
-
const targetStates = states.get(transition.to) ?? new Map();
|
|
85
|
-
states.set(transition.to, targetStates);
|
|
86
|
-
let grew = false;
|
|
87
|
-
for (const sourceState of sourceStates.values()) {
|
|
88
|
-
const transitionEffects = effects.get(action) ?? [];
|
|
89
|
-
work += 1 + transitionEffects.length;
|
|
90
|
-
if (work > UDL_LIMITS.financeWork) {
|
|
91
|
-
add(["lifecycle"], `financial analysis exceeds ${UDL_LIMITS.financeWork} deterministic work units`, "UDL1004");
|
|
92
|
-
return issues;
|
|
29
|
+
for (const action of Object.values(owner.actions))
|
|
30
|
+
for (const move of action.moves)
|
|
31
|
+
if ("amount" in move)
|
|
32
|
+
for (const endpoint of [move.from, move.to]) {
|
|
33
|
+
const parts = endpoint.split(".");
|
|
34
|
+
if (parts.shift() !== "self")
|
|
35
|
+
continue;
|
|
36
|
+
let targets = [owner];
|
|
37
|
+
while (parts.length > 1) {
|
|
38
|
+
const key = parts.shift();
|
|
39
|
+
targets = targets.flatMap((target) => {
|
|
40
|
+
const reference = target.fields.find((field) => field.name === key);
|
|
41
|
+
if (reference?.type !== "ref")
|
|
42
|
+
return [];
|
|
43
|
+
const ids = typeof reference.target === "string"
|
|
44
|
+
? [reference.target]
|
|
45
|
+
: reference.target;
|
|
46
|
+
return (document?.instruments.filter((candidate) => ids.includes(candidate.id)) ?? []);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
if (parts.length === 1 &&
|
|
50
|
+
targets.some((target) => target.id === instrument.id) &&
|
|
51
|
+
owned.has(`self.${parts[0]}`))
|
|
52
|
+
shared.add(`self.${parts[0]}`);
|
|
93
53
|
}
|
|
94
|
-
const targetState = applyEffects(account, action, sourceState, transitionEffects, reservations, refundSourceAccounts.has(account), add);
|
|
95
|
-
const key = stateKey(targetState);
|
|
96
|
-
if (targetStates.has(key))
|
|
97
|
-
continue;
|
|
98
|
-
targetStates.set(key, targetState);
|
|
99
|
-
pathVariants += 1;
|
|
100
|
-
if (pathVariants > UDL_LIMITS.financePathVariants) {
|
|
101
|
-
add(["lifecycle"], `financial analysis exceeds ${UDL_LIMITS.financePathVariants} distinct path variants`, "UDL1004");
|
|
102
|
-
return issues;
|
|
103
|
-
}
|
|
104
|
-
grew = true;
|
|
105
|
-
}
|
|
106
|
-
if (grew && !pending.includes(transition.to))
|
|
107
|
-
pending.push(transition.to);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
const nonterminalStates = new Set(Object.values(instrument.lifecycle.transitions).flatMap((transition) => transition.from));
|
|
111
|
-
for (const [state, variants] of states) {
|
|
112
|
-
if (nonterminalStates.has(state))
|
|
113
|
-
continue;
|
|
114
|
-
for (const variant of variants.values()) {
|
|
115
|
-
if (variant.balance.kind === "empty" &&
|
|
116
|
-
Object.keys(variant.holds).length === 0) {
|
|
117
|
-
continue;
|
|
118
|
-
}
|
|
119
|
-
add(["lifecycle", "states", instrument.lifecycle.states.indexOf(state)], `terminal state ${state} can strand value in ${formatAccount(account)}`);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
54
|
}
|
|
123
|
-
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
55
|
+
const problems = [];
|
|
56
|
+
const report = (message) => problems.push({ path: ".actions", message });
|
|
57
|
+
const equations = new Map();
|
|
58
|
+
for (const field of instrument.fields)
|
|
59
|
+
if (field.type === "money" && field.value !== undefined)
|
|
60
|
+
equations.set(`self.${field.name}`, new Map([["#", BigInt(field.value)]]));
|
|
61
|
+
for (const calc of instrument.calculate) {
|
|
62
|
+
if (calc.op === "sum" || calc.op === "subtract") {
|
|
63
|
+
const terms = calc.op === "sum"
|
|
64
|
+
? calc.values.map((v) => [v, 1n])
|
|
65
|
+
: [
|
|
66
|
+
[calc.base, 1n],
|
|
67
|
+
...calc.subtract.map((v) => [v, -1n]),
|
|
68
|
+
];
|
|
69
|
+
let result = new Map();
|
|
70
|
+
for (const [value, sign] of terms)
|
|
71
|
+
result = add(result, raw(value, "create"), sign);
|
|
72
|
+
equations.set(`self.${calc.target}`, result);
|
|
141
73
|
}
|
|
142
74
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
if (effectCount > UDL_LIMITS.financeEffects) {
|
|
155
|
-
return `financial analysis exceeds ${UDL_LIMITS.financeEffects} kernel effects`;
|
|
156
|
-
}
|
|
75
|
+
function raw(value, invocation) {
|
|
76
|
+
return "field" in value
|
|
77
|
+
? new Map([
|
|
78
|
+
[
|
|
79
|
+
value.field.startsWith("input.")
|
|
80
|
+
? `${invocation}:${value.field}`
|
|
81
|
+
: value.field,
|
|
82
|
+
1n,
|
|
83
|
+
],
|
|
84
|
+
])
|
|
85
|
+
: new Map([["#", BigInt(String(value.literal))]]);
|
|
157
86
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
step.bind.role?.from !== "const" ||
|
|
166
|
-
step.bind.role.value !== "product_escrow") {
|
|
167
|
-
continue;
|
|
168
|
-
}
|
|
169
|
-
for (const [key, result] of Object.entries(step.capture ?? {})) {
|
|
170
|
-
if (result === "accountId")
|
|
171
|
-
accounts.add(`ref:${key}`);
|
|
172
|
-
}
|
|
87
|
+
function expand(sum, seen = new Set()) {
|
|
88
|
+
let result = new Map();
|
|
89
|
+
for (const [key, coefficient] of sum) {
|
|
90
|
+
const equation = equations.get(key);
|
|
91
|
+
result = add(result, equation && !seen.has(key)
|
|
92
|
+
? expand(equation, new Set([...seen, key]))
|
|
93
|
+
: new Map([[key, 1n]]), coefficient);
|
|
173
94
|
}
|
|
95
|
+
return result;
|
|
174
96
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
continue;
|
|
196
|
-
const source = canonicalAccount(instrument, step.bind.sourceAccountId);
|
|
197
|
-
const destination = canonicalAccount(instrument, step.bind.destinationAccountId);
|
|
198
|
-
const amount = amountIdentity(step.bind.amount);
|
|
199
|
-
for (const [key, result] of Object.entries(step.capture ?? {})) {
|
|
200
|
-
if (result === "transferId") {
|
|
201
|
-
reservations.set(key, {
|
|
202
|
-
amount,
|
|
203
|
-
...(destination ? { destination } : {}),
|
|
204
|
-
...(source ? { source } : {}),
|
|
205
|
-
});
|
|
97
|
+
let invocation = 0;
|
|
98
|
+
const apply = (state, action, name) => {
|
|
99
|
+
const scope = `${name}#${invocation++}`;
|
|
100
|
+
const calculated = new Map();
|
|
101
|
+
const local = new Map((action.calculate ?? []).map((c) => [`self.${c.target}`, c]));
|
|
102
|
+
const amount = (value, seen = new Set()) => {
|
|
103
|
+
if ("field" in value) {
|
|
104
|
+
if (calculated.has(value.field))
|
|
105
|
+
return calculated.get(value.field);
|
|
106
|
+
if (value.field.endsWith(".balance") &&
|
|
107
|
+
owned.has(value.field.slice(0, -8)))
|
|
108
|
+
return state.balances.get(value.field.slice(0, -8)) ?? null;
|
|
109
|
+
const calculation = local.get(value.field);
|
|
110
|
+
if (calculation && !seen.has(value.field)) {
|
|
111
|
+
const next = new Set([...seen, value.field]);
|
|
112
|
+
if (calculation.op === "sum")
|
|
113
|
+
return calculation.values.reduce((sum, value) => add(sum, amount(value, next)), new Map());
|
|
114
|
+
if (calculation.op === "subtract")
|
|
115
|
+
return calculation.subtract.reduce((sum, value) => add(sum, amount(value, next), -1n), amount(calculation.base, next));
|
|
116
|
+
return new Map([[`${scope}:${value.field}`, 1n]]);
|
|
206
117
|
}
|
|
207
118
|
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
"moves",
|
|
222
|
-
stepIndex,
|
|
223
|
-
"bind",
|
|
224
|
-
];
|
|
225
|
-
if (step.operation === "internal_transfer.create") {
|
|
226
|
-
const effects = [];
|
|
227
|
-
const amount = amountIdentity(step.bind.amount);
|
|
228
|
-
if (canonicalAccount(instrument, step.bind.destinationAccountId) ===
|
|
229
|
-
account) {
|
|
230
|
-
effects.push({ amount, kind: "credit", path });
|
|
231
|
-
}
|
|
232
|
-
const source = canonicalAccount(instrument, step.bind.sourceAccountId);
|
|
233
|
-
if (source && accountsMayAlias(source, account)) {
|
|
234
|
-
const committed = quoteByCommit.get(actionName);
|
|
235
|
-
const amountPath = step.bind.amount?.from === "instance"
|
|
236
|
-
? step.bind.amount.path
|
|
237
|
-
: undefined;
|
|
238
|
-
const refunded = committed && amountPath === `refs.${committed.netRef}`
|
|
239
|
-
? committed
|
|
240
|
-
: undefined;
|
|
241
|
-
const charged = amountPath
|
|
242
|
-
? chargePaths.get(amountPath)
|
|
243
|
-
: undefined;
|
|
244
|
-
const kind = refunded
|
|
245
|
-
? "refund"
|
|
246
|
-
: charged
|
|
247
|
-
? "penalty_debit"
|
|
248
|
-
: amountPath !== undefined
|
|
249
|
-
? "debit"
|
|
250
|
-
: "dynamic_debit";
|
|
251
|
-
effects.push({
|
|
252
|
-
amount: refunded && !refunded.chargeCanBeNonzero
|
|
253
|
-
? "refund_without_penalty"
|
|
254
|
-
: amount,
|
|
255
|
-
kind,
|
|
256
|
-
path,
|
|
257
|
-
...(refunded ? { refundable: refunded.baseField } : {}),
|
|
258
|
-
});
|
|
119
|
+
return expand(raw(value, scope));
|
|
120
|
+
};
|
|
121
|
+
for (const requirement of action.requires)
|
|
122
|
+
if (requirement.kind === "compare" && requirement.operator === "==") {
|
|
123
|
+
for (const [left, right] of [
|
|
124
|
+
[requirement.left, requirement.right],
|
|
125
|
+
[requirement.right, requirement.left],
|
|
126
|
+
]) {
|
|
127
|
+
if ("field" in left && left.field.endsWith(".balance")) {
|
|
128
|
+
const account = left.field.slice(0, -8);
|
|
129
|
+
if (owned.has(account))
|
|
130
|
+
state.balances.set(account, amount(right));
|
|
131
|
+
}
|
|
259
132
|
}
|
|
260
|
-
return effects;
|
|
261
133
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
}
|
|
134
|
+
for (const key of local.keys())
|
|
135
|
+
calculated.set(key, amount({ field: key }));
|
|
136
|
+
const debit = (account, value) => {
|
|
137
|
+
if (owned.has(account))
|
|
138
|
+
state.balances.set(account, add(state.balances.has(account)
|
|
139
|
+
? state.balances.get(account)
|
|
140
|
+
: new Map(), value, -1n));
|
|
141
|
+
};
|
|
142
|
+
const credit = (account, value) => {
|
|
143
|
+
if (owned.has(account))
|
|
144
|
+
state.balances.set(account, add(state.balances.has(account)
|
|
145
|
+
? state.balances.get(account)
|
|
146
|
+
: new Map(), value));
|
|
147
|
+
};
|
|
148
|
+
for (const move of action.moves) {
|
|
149
|
+
if ("amount" in move) {
|
|
150
|
+
const value = amount(move.amount);
|
|
151
|
+
debit(move.from, value);
|
|
152
|
+
if (move.operation === "internal_transfer.reserve") {
|
|
153
|
+
const key = `self.${move.capture}`;
|
|
154
|
+
if (state.holds.has(key))
|
|
155
|
+
report(`${move.capture} overwrites a live reservation`);
|
|
156
|
+
state.holds.set(key, { from: move.from, to: move.to, amount: value });
|
|
282
157
|
}
|
|
283
|
-
|
|
158
|
+
else
|
|
159
|
+
credit(move.to, value);
|
|
284
160
|
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
const reserved = reservations.get(reservation);
|
|
291
|
-
if (!reserved ||
|
|
292
|
-
(!(reserved.source && accountsMayAlias(reserved.source, account)) &&
|
|
293
|
-
reserved.destination !== account)) {
|
|
294
|
-
return [];
|
|
161
|
+
else {
|
|
162
|
+
const hold = state.holds.get(move.transfer);
|
|
163
|
+
if (!hold) {
|
|
164
|
+
report(`${move.key} consumes a missing reservation ${move.transfer}`);
|
|
165
|
+
continue;
|
|
295
166
|
}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
amount: reserved.amount,
|
|
299
|
-
kind: step.operation === "internal_transfer.post" ? "post" : "void",
|
|
300
|
-
path,
|
|
301
|
-
reservation,
|
|
302
|
-
},
|
|
303
|
-
];
|
|
167
|
+
credit(move.operation === "internal_transfer.post" ? hold.to : hold.from, hold.amount);
|
|
168
|
+
state.holds.delete(move.transfer);
|
|
304
169
|
}
|
|
305
|
-
return [];
|
|
306
|
-
}),
|
|
307
|
-
]));
|
|
308
|
-
}
|
|
309
|
-
function applyEffects(account, action, input, effects, reservations, exactBalance, add) {
|
|
310
|
-
let balance = input.balance;
|
|
311
|
-
const holds = { ...input.holds };
|
|
312
|
-
const accountLabel = formatAccount(account);
|
|
313
|
-
for (const effect of effects) {
|
|
314
|
-
const fail = (message) => add([...effect.path, "sourceAccountId"], `action ${action} ${message}`);
|
|
315
|
-
if (effect.kind === "credit") {
|
|
316
|
-
if (exactBalance &&
|
|
317
|
-
(balance.kind !== "empty" || Object.keys(holds).length > 0)) {
|
|
318
|
-
add([...effect.path, "destinationAccountId"], `action ${action} funds ${accountLabel} while earlier value may remain; quoted funding must establish exactly one refundable balance`);
|
|
319
|
-
balance = UNKNOWN;
|
|
320
|
-
continue;
|
|
321
|
-
}
|
|
322
|
-
const pieces = balance.kind === "funded" ? balance.amounts : [];
|
|
323
|
-
const stacks = (balance.kind === "empty" || balance.kind === "funded") &&
|
|
324
|
-
!pieces.includes(effect.amount);
|
|
325
|
-
balance = stacks ? funded([...pieces, effect.amount]) : UNKNOWN;
|
|
326
|
-
continue;
|
|
327
170
|
}
|
|
328
|
-
if (
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
const held = balance.kind === "funded" && balance.amounts.includes(effect.amount);
|
|
332
|
-
if (!held) {
|
|
333
|
-
fail(`cannot reserve ${effect.amount} from ${accountLabel}; that exact balance is not guaranteed`);
|
|
334
|
-
}
|
|
335
|
-
holds[effect.reservation] = held
|
|
336
|
-
? { kind: "funded", amounts: [effect.amount] }
|
|
337
|
-
: UNKNOWN;
|
|
338
|
-
balance =
|
|
339
|
-
held && balance.kind === "funded"
|
|
340
|
-
? funded(withoutPiece(balance.amounts, effect.amount))
|
|
341
|
-
: EMPTY;
|
|
342
|
-
continue;
|
|
171
|
+
if (Object.keys(action.set ?? {}).some((key) => instrument.fields.some((f) => f.name === key && f.type === "money"))) {
|
|
172
|
+
for (const account of owned)
|
|
173
|
+
state.balances.set(account, null);
|
|
343
174
|
}
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
175
|
+
};
|
|
176
|
+
const initial = {
|
|
177
|
+
status: instrument.lifecycle.initial,
|
|
178
|
+
actions: [],
|
|
179
|
+
paths: new Map(),
|
|
180
|
+
balances: new Map([...owned].map((key) => [key, new Map()])),
|
|
181
|
+
holds: new Map(),
|
|
182
|
+
};
|
|
183
|
+
if (instrument.actions.create) {
|
|
184
|
+
apply(initial, instrument.actions.create, "create");
|
|
185
|
+
for (const [account, balance] of initial.balances)
|
|
186
|
+
if (balance === null || balance.size)
|
|
187
|
+
initial.paths.set(account, ["create"]);
|
|
188
|
+
}
|
|
189
|
+
const pending = [initial];
|
|
190
|
+
const reached = new Map();
|
|
191
|
+
const copy = (state) => ({
|
|
192
|
+
status: state.status,
|
|
193
|
+
actions: [...state.actions],
|
|
194
|
+
paths: new Map(state.paths),
|
|
195
|
+
balances: new Map(state.balances),
|
|
196
|
+
holds: new Map([...state.holds].map(([key, hold]) => [key, { ...hold }])),
|
|
197
|
+
});
|
|
198
|
+
let work = 0;
|
|
199
|
+
while (pending.length && !problems.length) {
|
|
200
|
+
if (++work > 4096) {
|
|
201
|
+
report("owned proof exceeds its finite work budget");
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
let state = pending.pop();
|
|
205
|
+
const key = JSON.stringify([state.status, [...state.holds.keys()].sort()]);
|
|
206
|
+
const previous = reached.get(key);
|
|
207
|
+
if (previous) {
|
|
208
|
+
const joined = copy(previous);
|
|
209
|
+
let changed = false;
|
|
210
|
+
for (const account of owned) {
|
|
211
|
+
const before = previous.balances.has(account)
|
|
212
|
+
? previous.balances.get(account)
|
|
213
|
+
: new Map();
|
|
214
|
+
const after = state.balances.has(account)
|
|
215
|
+
? state.balances.get(account)
|
|
216
|
+
: new Map();
|
|
217
|
+
if (before !== null && !same(before, after)) {
|
|
218
|
+
joined.balances.set(account, null);
|
|
219
|
+
if (!before.size && state.paths.has(account))
|
|
220
|
+
joined.paths.set(account, state.paths.get(account));
|
|
221
|
+
changed = true;
|
|
367
222
|
}
|
|
368
223
|
}
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
224
|
+
for (const [name, hold] of joined.holds) {
|
|
225
|
+
const next = state.holds.get(name);
|
|
226
|
+
if (hold.from !== next.from || hold.to !== next.to) {
|
|
227
|
+
report(`${name} changes reservation endpoints across paths`);
|
|
228
|
+
break;
|
|
373
229
|
}
|
|
374
|
-
|
|
375
|
-
|
|
230
|
+
if (hold.amount !== null && !same(hold.amount, next.amount)) {
|
|
231
|
+
hold.amount = null;
|
|
232
|
+
changed = true;
|
|
376
233
|
}
|
|
377
234
|
}
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
const refundable = `fields.${effect.refundable ?? ""}`;
|
|
382
|
-
if (balance.kind !== "funded" ||
|
|
383
|
-
balance.amounts.length !== 1 ||
|
|
384
|
-
balance.amounts[0] !== refundable) {
|
|
385
|
-
fail(`cannot refund ${refundable} from ${accountLabel}; that exact balance is not guaranteed`);
|
|
386
|
-
balance = UNKNOWN;
|
|
387
|
-
}
|
|
388
|
-
else {
|
|
389
|
-
balance =
|
|
390
|
-
effect.amount === "refund_without_penalty"
|
|
391
|
-
? EMPTY
|
|
392
|
-
: { kind: "penalty" };
|
|
393
|
-
}
|
|
394
|
-
continue;
|
|
395
|
-
}
|
|
396
|
-
if (effect.kind === "penalty_debit") {
|
|
397
|
-
if (balance.kind !== "penalty") {
|
|
398
|
-
fail(`cannot pay the quoted charge from ${accountLabel} before the refund leaves that exact remainder`);
|
|
399
|
-
}
|
|
400
|
-
balance = EMPTY;
|
|
401
|
-
continue;
|
|
402
|
-
}
|
|
403
|
-
if (effect.kind === "dynamic_debit") {
|
|
404
|
-
if (balance.kind === "empty") {
|
|
405
|
-
fail(`can debit unfunded ${accountLabel}`);
|
|
406
|
-
}
|
|
407
|
-
else if (balance.kind === "penalty") {
|
|
408
|
-
fail(`can debit ${accountLabel} after its refund left only the quoted charge`);
|
|
409
|
-
}
|
|
410
|
-
balance = balance.kind === "empty" ? EMPTY : { kind: "unknown" };
|
|
411
|
-
continue;
|
|
412
|
-
}
|
|
413
|
-
if (balance.kind === "funded" && balance.amounts.includes(effect.amount)) {
|
|
414
|
-
balance = funded(withoutPiece(balance.amounts, effect.amount));
|
|
415
|
-
continue;
|
|
416
|
-
}
|
|
417
|
-
if (balance.kind === "empty") {
|
|
418
|
-
fail(`can debit unfunded ${accountLabel}`);
|
|
419
|
-
}
|
|
420
|
-
else if (balance.kind === "penalty") {
|
|
421
|
-
fail(`can debit ${accountLabel} after its refund left only the quoted charge`);
|
|
422
|
-
}
|
|
423
|
-
else if (balance.kind === "funded" || exactBalance) {
|
|
424
|
-
fail(`cannot prove ${effect.amount} is the exact available balance of ${accountLabel}`);
|
|
235
|
+
if (!changed)
|
|
236
|
+
continue;
|
|
237
|
+
state = joined;
|
|
425
238
|
}
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
}
|
|
430
|
-
/**
|
|
431
|
-
* Flattens every quote-commit pair the instrument declares. A quoting action
|
|
432
|
-
* with no committing action, or a commit naming a non-quoting action, is a
|
|
433
|
-
* validator failure rather than a typestate one, so it is simply skipped here.
|
|
434
|
-
*/
|
|
435
|
-
function quoteFacts(instrument, options) {
|
|
436
|
-
const facts = [];
|
|
437
|
-
for (const [quoting, action] of Object.entries(instrument.actions)) {
|
|
438
|
-
const quote = action.quote;
|
|
439
|
-
if (!quote)
|
|
440
|
-
continue;
|
|
441
|
-
const commit = Object.entries(instrument.actions).find(([, candidate]) => candidate.commit === quoting)?.[0];
|
|
442
|
-
if (commit === undefined)
|
|
443
|
-
continue;
|
|
444
|
-
const chargePath = `refs.${quote.chargeRef}`;
|
|
445
|
-
const declaresChargePayout = Object.values(instrument.actions).some((candidate) => (candidate.moves ?? []).some((move) => Object.values(move.bind).some((binding) => binding.from === "instance" && binding.path === chargePath)));
|
|
446
|
-
facts.push({
|
|
447
|
-
baseField: quote.baseField,
|
|
448
|
-
chargeCanBeNonzero: options.penaltyMayBeNonzero === true ||
|
|
449
|
-
declaresChargePayout ||
|
|
450
|
-
quote.charges.some((tier) => tier.bps > 0),
|
|
451
|
-
chargeRef: quote.chargeRef,
|
|
452
|
-
commit,
|
|
453
|
-
netRef: quote.netRef,
|
|
454
|
-
quoting,
|
|
455
|
-
});
|
|
456
|
-
}
|
|
457
|
-
return facts;
|
|
458
|
-
}
|
|
459
|
-
/**
|
|
460
|
-
* The quoted charge is the exact remainder the commit leaves behind, so it may
|
|
461
|
-
* fund one transfer, out of the same account the refund left, in its own
|
|
462
|
-
* one-way action directly after the commit.
|
|
463
|
-
*/
|
|
464
|
-
function validateChargePayout(instrument, quote, refundSource, add) {
|
|
465
|
-
const chargePath = `refs.${quote.chargeRef}`;
|
|
466
|
-
const uses = Object.entries(instrument.actions).flatMap(([actionName, action]) => (action.moves ?? []).flatMap((step, stepIndex) => Object.entries(step.bind).flatMap(([target, binding]) => binding.from === "instance" && binding.path === chargePath
|
|
467
|
-
? [{ binding, step, stepIndex, target, action, actionName }]
|
|
468
|
-
: [])));
|
|
469
|
-
if (quote.chargeCanBeNonzero && uses.length !== 1) {
|
|
470
|
-
add(["actions"], `${chargePath} is consumed ${uses.length} times; a nonzero charge schedule requires exactly one payout`);
|
|
471
|
-
}
|
|
472
|
-
if (uses.length === 0)
|
|
473
|
-
return;
|
|
474
|
-
if (uses.length !== 1) {
|
|
475
|
-
if (!quote.chargeCanBeNonzero) {
|
|
476
|
-
add(["actions"], `${chargePath} is consumed ${uses.length} times; it may fund at most one payout`);
|
|
239
|
+
if (reached.size >= 256 && !previous) {
|
|
240
|
+
report("owned proof exceeds 256 state and reservation combinations");
|
|
241
|
+
break;
|
|
477
242
|
}
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
destination !== undefined &&
|
|
499
|
-
!accountsMayAlias(source, destination);
|
|
500
|
-
if (!valid) {
|
|
501
|
-
add(["actions", use.actionName, "moves", use.stepIndex, "bind", use.target], `${chargePath} must be the amount of one internal transfer from the quoted refund source to a different account, in its own one-way action directly after ${quote.commit}`);
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
function canonicalAccount(instrument, binding) {
|
|
505
|
-
if (binding?.from !== "instance")
|
|
506
|
-
return undefined;
|
|
507
|
-
const [root, key, extra] = binding.path.split(".");
|
|
508
|
-
if (!key || extra !== undefined)
|
|
509
|
-
return undefined;
|
|
510
|
-
if (root === "fields")
|
|
511
|
-
return `field:${key}`;
|
|
512
|
-
if (root === "refs")
|
|
513
|
-
return `ref:${key}`;
|
|
514
|
-
if (root === "party") {
|
|
515
|
-
const field = instrument.parties?.[key];
|
|
516
|
-
return field ? `field:${field}` : undefined;
|
|
517
|
-
}
|
|
518
|
-
return undefined;
|
|
519
|
-
}
|
|
520
|
-
function accountsMayAlias(left, right) {
|
|
521
|
-
if (left.startsWith("field:") && right.startsWith("field:"))
|
|
522
|
-
return true;
|
|
523
|
-
return left === right;
|
|
524
|
-
}
|
|
525
|
-
function amountIdentity(binding) {
|
|
526
|
-
if (binding?.from === "instance")
|
|
527
|
-
return binding.path;
|
|
528
|
-
return binding?.from === "const" ? `const:${binding.value}` : "dynamic";
|
|
529
|
-
}
|
|
530
|
-
function transferRefKey(step) {
|
|
531
|
-
return Object.entries(step.capture ?? {}).find(([, result]) => result === "transferId")?.[0];
|
|
532
|
-
}
|
|
533
|
-
function boundTransferRefKey(step) {
|
|
534
|
-
const binding = step.bind.transferId;
|
|
535
|
-
return binding?.from === "instance" && binding.path.startsWith("refs.")
|
|
536
|
-
? binding.path.slice("refs.".length)
|
|
537
|
-
: undefined;
|
|
538
|
-
}
|
|
539
|
-
function stateCanReach(instrument, start, target) {
|
|
540
|
-
if (!target)
|
|
541
|
-
return false;
|
|
542
|
-
const seen = new Set([start]);
|
|
543
|
-
const pending = [start];
|
|
544
|
-
while (pending.length > 0) {
|
|
545
|
-
const state = pending.shift();
|
|
546
|
-
if (state === target)
|
|
547
|
-
return true;
|
|
548
|
-
for (const transition of Object.values(instrument.lifecycle.transitions)) {
|
|
549
|
-
if (!transition.from.includes(state) || seen.has(transition.to))
|
|
243
|
+
reached.set(key, copy(state));
|
|
244
|
+
const edges = Object.entries(instrument.lifecycle.transitions).filter(([, edge]) => edge.from.includes(state.status));
|
|
245
|
+
if (!edges.length &&
|
|
246
|
+
([...state.balances].some(([account, balance]) => !shared.has(account) && (balance === null || balance.size > 0)) ||
|
|
247
|
+
state.holds.size))
|
|
248
|
+
problems.push({
|
|
249
|
+
path: ".actions",
|
|
250
|
+
message: `terminal state ${state.status} may strand owned money or an open reservation`,
|
|
251
|
+
stranded: {
|
|
252
|
+
state: state.status,
|
|
253
|
+
accounts: [...state.balances]
|
|
254
|
+
.filter(([account, balance]) => !shared.has(account) && (balance === null || balance.size > 0))
|
|
255
|
+
.map(([account]) => account.slice(5)),
|
|
256
|
+
actions: state.actions,
|
|
257
|
+
paths: Object.fromEntries([...state.paths].map(([account, path]) => [account.slice(5), path])),
|
|
258
|
+
},
|
|
259
|
+
});
|
|
260
|
+
for (const [name, edge] of edges) {
|
|
261
|
+
const action = instrument.actions[name];
|
|
262
|
+
if (!action)
|
|
550
263
|
continue;
|
|
551
|
-
|
|
552
|
-
|
|
264
|
+
const next = copy(state);
|
|
265
|
+
if (edge.to !== "preserve")
|
|
266
|
+
next.status = edge.to;
|
|
267
|
+
apply(next, action, name);
|
|
268
|
+
next.actions.push(name);
|
|
269
|
+
for (const [account, balance] of next.balances) {
|
|
270
|
+
if (balance !== null && !balance.size)
|
|
271
|
+
next.paths.delete(account);
|
|
272
|
+
else
|
|
273
|
+
next.paths.set(account, [
|
|
274
|
+
...(state.paths.get(account) ?? state.actions),
|
|
275
|
+
name,
|
|
276
|
+
]);
|
|
277
|
+
}
|
|
278
|
+
pending.push(next);
|
|
553
279
|
}
|
|
554
280
|
}
|
|
555
|
-
return
|
|
556
|
-
}
|
|
557
|
-
function stateKey(state) {
|
|
558
|
-
return JSON.stringify({
|
|
559
|
-
balance: state.balance,
|
|
560
|
-
holds: Object.fromEntries(Object.entries(state.holds).sort(([a], [b]) => a.localeCompare(b))),
|
|
561
|
-
});
|
|
562
|
-
}
|
|
563
|
-
function formatAccount(account) {
|
|
564
|
-
const [kind, key] = account.split(":");
|
|
565
|
-
return `${kind === "field" ? "fields" : "refs"}.${key}`;
|
|
281
|
+
return problems;
|
|
566
282
|
}
|
|
567
283
|
//# sourceMappingURL=finance.js.map
|