@hyperscale0/udl 2.6.1 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -328
- package/LICENSING.md +1 -2
- package/README.md +3 -114
- package/TRADEMARKS.md +2 -2
- package/dist/diagnostics.d.ts +21 -201
- package/dist/diagnostics.d.ts.map +1 -1
- package/dist/diagnostics.js +29 -193
- package/dist/diagnostics.js.map +1 -1
- package/dist/evolution.d.ts +3 -132
- package/dist/evolution.d.ts.map +1 -1
- package/dist/evolution.js +29 -633
- package/dist/evolution.js.map +1 -1
- package/dist/finance.d.ts +5 -72
- package/dist/finance.d.ts.map +1 -1
- package/dist/finance.js +233 -737
- package/dist/finance.js.map +1 -1
- package/dist/index.d.ts +7 -18
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -12
- package/dist/index.js.map +1 -1
- package/dist/instrument-references.d.ts.map +1 -1
- package/dist/instrument-references.js +3 -2
- package/dist/instrument-references.js.map +1 -1
- package/dist/schema.d.ts +2079 -4564
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +270 -1609
- package/dist/schema.js.map +1 -1
- package/dist/validation.d.ts +8 -104
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +653 -3271
- package/dist/validation.js.map +1 -1
- package/docs/README.md +148 -14
- package/package.json +7 -11
- package/spec/README.md +152 -164
- package/spec/darb.udl.json +249 -0
- package/spec/udl.schema.json +4104 -3202
- package/src/diagnostics.ts +51 -246
- package/src/evolution.ts +39 -1045
- package/src/finance.ts +287 -1127
- package/src/index.ts +18 -142
- package/src/instrument-references.ts +3 -2
- package/src/schema.ts +285 -1843
- package/src/validation.ts +870 -5611
- 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/call-binds-results.expected.json +0 -10
- package/conformance/invalid/call-binds-results.udl +0 -314
- package/conformance/invalid/call-unknown-action.expected.json +0 -10
- package/conformance/invalid/call-unknown-action.udl +0 -314
- 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-journeys.expected.json +0 -10
- package/conformance/invalid/invalid-journeys.udl +0 -77
- 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/leaf-effect-mismatch.expected.json +0 -10
- package/conformance/invalid/leaf-effect-mismatch.udl +0 -314
- 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/piece-plan-without-partition.expected.json +0 -10
- package/conformance/invalid/piece-plan-without-partition.udl +0 -305
- package/conformance/invalid/private-action-independent-approval.expected.json +0 -10
- package/conformance/invalid/private-action-independent-approval.udl +0 -314
- 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/unfund-order-not-reversed.expected.json +0 -10
- package/conformance/invalid/unfund-order-not-reversed.udl +0 -314
- 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/attested.expected.json +0 -6
- package/conformance/valid/attested.udl +0 -251
- 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 -62
- package/conformance/valid/piece-plan-calls.expected.json +0 -6
- package/conformance/valid/piece-plan-calls.udl +0 -314
- 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/conformance/valid/vocabulary.expected.json +0 -6
- package/conformance/valid/vocabulary.udl +0 -2012
- package/dist/allocation.d.ts +0 -60
- package/dist/allocation.d.ts.map +0 -1
- package/dist/allocation.js +0 -177
- package/dist/allocation.js.map +0 -1
- 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/distribution.d.ts +0 -15
- package/dist/distribution.d.ts.map +0 -1
- package/dist/distribution.js +0 -49
- package/dist/distribution.js.map +0 -1
- package/dist/effects.d.ts +0 -58
- package/dist/effects.d.ts.map +0 -1
- package/dist/effects.js +0 -1126
- package/dist/effects.js.map +0 -1
- package/dist/reference.d.ts +0 -3
- package/dist/reference.d.ts.map +0 -1
- package/dist/reference.js +0 -28
- package/dist/reference.js.map +0 -1
- package/dist/vocabulary.d.ts +0 -23
- package/dist/vocabulary.d.ts.map +0 -1
- package/dist/vocabulary.js +0 -1052
- package/dist/vocabulary.js.map +0 -1
- package/docs/funding-custody.md +0 -165
- 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 -36
- 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/guide/09-schedules-and-allocation.md +0 -132
- package/docs/llms-full.txt +0 -2008
- package/docs/llms.txt +0 -14
- package/docs/piece-plans.md +0 -148
- package/docs/reference/canonical.md +0 -16
- package/docs/reference/clauses.md +0 -1585
- package/docs/reference/cli.md +0 -24
- package/docs/reference/diagnostics.md +0 -38
- package/skills/udl/SKILL.md +0 -28
- package/src/allocation.ts +0 -259
- package/src/check-profiles.ts +0 -80
- package/src/distribution.ts +0 -61
- package/src/effects.ts +0 -1920
- package/src/reference.ts +0 -31
- package/src/vocabulary.ts +0 -1635
package/dist/finance.js
CHANGED
|
@@ -1,755 +1,251 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
if (trackedAccounts.size === 0)
|
|
54
|
-
return issues;
|
|
55
|
-
if (trackedAccounts.size > UDL_LIMITS.financeAccounts) {
|
|
56
|
-
add(["actions"], `financial analysis exceeds ${UDL_LIMITS.financeAccounts} tracked accounts`, "UDL1004");
|
|
57
|
-
return issues;
|
|
58
|
-
}
|
|
59
|
-
const reservations = reservationsByKey(instrument);
|
|
60
|
-
let pathVariants = 0;
|
|
61
|
-
let work = 0;
|
|
62
|
-
for (const account of trackedAccounts) {
|
|
63
|
-
const effects = effectsByAction(instrument, account, reservations, quotes);
|
|
64
|
-
const createEffects = effects.get("create") ?? [];
|
|
65
|
-
work += 1 + createEffects.length;
|
|
66
|
-
if (work > UDL_LIMITS.financeWork) {
|
|
67
|
-
add(["lifecycle"], `financial analysis exceeds ${UDL_LIMITS.financeWork} deterministic work units`, "UDL1004");
|
|
68
|
-
return issues;
|
|
69
|
-
}
|
|
70
|
-
const initial = applyEffects(instrument, 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;
|
|
93
|
-
}
|
|
94
|
-
const targetState = applyEffects(instrument, 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
|
-
if (account.startsWith("ref:")) {
|
|
112
|
-
for (const [state, variants] of states) {
|
|
113
|
-
if (nonterminalStates.has(state))
|
|
114
|
-
continue;
|
|
115
|
-
for (const variant of variants.values()) {
|
|
116
|
-
if (variant.balance.kind === "empty" &&
|
|
117
|
-
Object.keys(variant.holds).length === 0) {
|
|
118
|
-
continue;
|
|
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);
|
|
11
|
+
}
|
|
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)
|
|
28
|
+
continue;
|
|
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]}`);
|
|
119
53
|
}
|
|
120
|
-
add(["lifecycle", "states", instrument.lifecycle.states.indexOf(state)], `terminal state ${state} can strand value in ${formatAccount(account)}`);
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
return issues;
|
|
126
|
-
}
|
|
127
|
-
export function financeAdmissionProblem(instrument) {
|
|
128
|
-
if (instrument.lifecycle.states.length > UDL_LIMITS.financeStates) {
|
|
129
|
-
return `financial analysis exceeds ${UDL_LIMITS.financeStates} lifecycle states`;
|
|
130
|
-
}
|
|
131
|
-
let transitionCount = 0;
|
|
132
|
-
let transitionEdges = 0;
|
|
133
|
-
for (const name in instrument.lifecycle.transitions) {
|
|
134
|
-
if (!Object.hasOwn(instrument.lifecycle.transitions, name))
|
|
135
|
-
continue;
|
|
136
|
-
transitionCount += 1;
|
|
137
|
-
if (transitionCount > UDL_LIMITS.financeTransitions) {
|
|
138
|
-
return `financial analysis exceeds ${UDL_LIMITS.financeTransitions} lifecycle transitions`;
|
|
139
|
-
}
|
|
140
|
-
transitionEdges += instrument.lifecycle.transitions[name]?.from.length ?? 0;
|
|
141
|
-
if (transitionEdges > UDL_LIMITS.financeTransitionEdges) {
|
|
142
|
-
return `financial analysis exceeds ${UDL_LIMITS.financeTransitionEdges} lifecycle transition edges`;
|
|
143
|
-
}
|
|
144
54
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
return undefined;
|
|
161
|
-
}
|
|
162
|
-
function productEscrowAccounts(instrument) {
|
|
163
|
-
const accounts = new Set();
|
|
164
|
-
for (const action of Object.values(instrument.actions)) {
|
|
165
|
-
for (const step of action.steps) {
|
|
166
|
-
// This operation fixes the escrow role; an omitted redundant binding
|
|
167
|
-
// must not exempt its captured account from the conservation proof.
|
|
168
|
-
if (step.operation !== "account.escrow.provision")
|
|
169
|
-
continue;
|
|
170
|
-
for (const [key, result] of Object.entries(step.capture ?? {})) {
|
|
171
|
-
if (result === "accountId")
|
|
172
|
-
accounts.add(`ref:${key}`);
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
// `product_escrow` also backs open-ended balance products such as wallets.
|
|
177
|
-
// Only instance-bound amounts claim static conservation; caller-sized flows
|
|
178
|
-
// remain runtime balance checks. Commit sources are added separately above.
|
|
179
|
-
return new Set([...accounts].filter((account) => Object.values(instrument.actions).some((action) => {
|
|
180
|
-
if (action.contributionStage &&
|
|
181
|
-
canonicalAccountPath(instrument, action.contributionStage.accountPath) === account) {
|
|
182
|
-
return true;
|
|
183
|
-
}
|
|
184
|
-
return (action.moves ?? []).some((step) => {
|
|
185
|
-
if ((step.operation !== "internal_transfer.create" &&
|
|
186
|
-
step.operation !== "internal_transfer.reserve") ||
|
|
187
|
-
step.bind.amount?.from !== "instance") {
|
|
188
|
-
return false;
|
|
189
|
-
}
|
|
190
|
-
return (canonicalAccount(instrument, step.bind.sourceAccountId) ===
|
|
191
|
-
account ||
|
|
192
|
-
canonicalAccount(instrument, step.bind.destinationAccountId) ===
|
|
193
|
-
account);
|
|
194
|
-
});
|
|
195
|
-
})));
|
|
196
|
-
}
|
|
197
|
-
function reservationsByKey(instrument) {
|
|
198
|
-
const reservations = new Map();
|
|
199
|
-
for (const action of Object.values(instrument.actions)) {
|
|
200
|
-
for (const step of action.moves ?? []) {
|
|
201
|
-
if (step.operation !== "internal_transfer.reserve")
|
|
202
|
-
continue;
|
|
203
|
-
const source = canonicalAccount(instrument, step.bind.sourceAccountId);
|
|
204
|
-
const destination = canonicalAccount(instrument, step.bind.destinationAccountId);
|
|
205
|
-
const amount = amountIdentity(step.bind.amount);
|
|
206
|
-
for (const [key, result] of Object.entries(step.capture ?? {})) {
|
|
207
|
-
if (result === "transferId") {
|
|
208
|
-
reservations.set(key, {
|
|
209
|
-
amount,
|
|
210
|
-
...(destination ? { destination } : {}),
|
|
211
|
-
...(source ? { source } : {}),
|
|
212
|
-
});
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
return reservations;
|
|
218
|
-
}
|
|
219
|
-
function effectsByAction(instrument, account, reservations, quotes) {
|
|
220
|
-
const quoteByCommit = new Map(quotes.map((quote) => [quote.commit, quote]));
|
|
221
|
-
const chargePaths = new Map(quotes.map((quote) => [`refs.${quote.chargeRef}`, quote]));
|
|
222
|
-
return new Map(Object.entries(instrument.actions).map(([actionName, action]) => {
|
|
223
|
-
const stageEffects = [];
|
|
224
|
-
if (action.contributionStage && instrument.contributions) {
|
|
225
|
-
const stage = action.contributionStage;
|
|
226
|
-
const stageAccount = canonicalAccountPath(instrument, stage.accountPath);
|
|
227
|
-
if (stageAccount &&
|
|
228
|
-
accountsMayAlias(instrument, stageAccount, account)) {
|
|
229
|
-
const path = ["actions", actionName, "contributionStage"];
|
|
230
|
-
const totalAmount = `fields.${instrument.contributions.totalField}`;
|
|
231
|
-
if (stage.stage === "fund") {
|
|
232
|
-
stageEffects.push({
|
|
233
|
-
amount: totalAmount,
|
|
234
|
-
kind: "credit",
|
|
235
|
-
path,
|
|
236
|
-
});
|
|
237
|
-
}
|
|
238
|
-
else if (stage.stage === "refund") {
|
|
239
|
-
stageEffects.push({
|
|
240
|
-
amount: totalAmount,
|
|
241
|
-
kind: "debit",
|
|
242
|
-
path,
|
|
243
|
-
});
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
const moveEffects = (action.moves ?? []).flatMap((step, stepIndex) => {
|
|
248
|
-
const path = [
|
|
249
|
-
"actions",
|
|
250
|
-
actionName,
|
|
251
|
-
"moves",
|
|
252
|
-
stepIndex,
|
|
253
|
-
"bind",
|
|
254
|
-
];
|
|
255
|
-
if (step.operation === "internal_transfer.create") {
|
|
256
|
-
const effects = [];
|
|
257
|
-
const amount = amountIdentity(step.bind.amount);
|
|
258
|
-
if (canonicalAccount(instrument, step.bind.destinationAccountId) ===
|
|
259
|
-
account) {
|
|
260
|
-
effects.push({ amount, kind: "credit", path });
|
|
261
|
-
}
|
|
262
|
-
const source = canonicalAccount(instrument, step.bind.sourceAccountId);
|
|
263
|
-
if (source && accountsMayAlias(instrument, source, account)) {
|
|
264
|
-
const committed = quoteByCommit.get(actionName);
|
|
265
|
-
const amountPath = step.bind.amount?.from === "instance"
|
|
266
|
-
? step.bind.amount.path
|
|
267
|
-
: undefined;
|
|
268
|
-
const refunded = committed && amountPath === `refs.${committed.netRef}`
|
|
269
|
-
? committed
|
|
270
|
-
: undefined;
|
|
271
|
-
const charged = amountPath
|
|
272
|
-
? chargePaths.get(amountPath)
|
|
273
|
-
: undefined;
|
|
274
|
-
const kind = refunded
|
|
275
|
-
? "refund"
|
|
276
|
-
: charged
|
|
277
|
-
? "penalty_debit"
|
|
278
|
-
: amountPath !== undefined
|
|
279
|
-
? "debit"
|
|
280
|
-
: "dynamic_debit";
|
|
281
|
-
effects.push({
|
|
282
|
-
amount: refunded && !refunded.chargeCanBeNonzero
|
|
283
|
-
? "refund_without_penalty"
|
|
284
|
-
: amount,
|
|
285
|
-
kind,
|
|
286
|
-
path,
|
|
287
|
-
...(refunded ? { refundable: refunded.baseField } : {}),
|
|
288
|
-
});
|
|
289
|
-
}
|
|
290
|
-
return effects;
|
|
291
|
-
}
|
|
292
|
-
if (step.operation === "internal_transfer.reserve") {
|
|
293
|
-
const reservation = transferRefKey(step) ?? `${actionName}:${stepIndex}`;
|
|
294
|
-
const effects = [];
|
|
295
|
-
const source = canonicalAccount(instrument, step.bind.sourceAccountId);
|
|
296
|
-
if (source && accountsMayAlias(instrument, source, account)) {
|
|
297
|
-
effects.push({
|
|
298
|
-
amount: amountIdentity(step.bind.amount),
|
|
299
|
-
kind: "outgoing_reserve",
|
|
300
|
-
path,
|
|
301
|
-
reservation,
|
|
302
|
-
});
|
|
303
|
-
}
|
|
304
|
-
if (canonicalAccount(instrument, step.bind.destinationAccountId) ===
|
|
305
|
-
account) {
|
|
306
|
-
effects.push({
|
|
307
|
-
amount: amountIdentity(step.bind.amount),
|
|
308
|
-
kind: "incoming_reserve",
|
|
309
|
-
path,
|
|
310
|
-
reservation,
|
|
311
|
-
});
|
|
312
|
-
}
|
|
313
|
-
return effects;
|
|
314
|
-
}
|
|
315
|
-
if (step.operation === "internal_transfer.post" ||
|
|
316
|
-
step.operation === "internal_transfer.void") {
|
|
317
|
-
const reservation = boundTransferRefKey(step);
|
|
318
|
-
if (!reservation)
|
|
319
|
-
return [];
|
|
320
|
-
const reserved = reservations.get(reservation);
|
|
321
|
-
if (!reserved ||
|
|
322
|
-
(!(reserved.source &&
|
|
323
|
-
accountsMayAlias(instrument, reserved.source, account)) &&
|
|
324
|
-
reserved.destination !== account)) {
|
|
325
|
-
return [];
|
|
326
|
-
}
|
|
327
|
-
return [
|
|
328
|
-
{
|
|
329
|
-
amount: reserved.amount,
|
|
330
|
-
kind: step.operation === "internal_transfer.post" ? "post" : "void",
|
|
331
|
-
path,
|
|
332
|
-
reservation,
|
|
333
|
-
},
|
|
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]),
|
|
334
68
|
];
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
}
|
|
341
|
-
function
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
}
|
|
387
|
-
for (const
|
|
388
|
-
if (
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
continue;
|
|
398
|
-
}
|
|
399
|
-
if (balance.amounts.length === candidateInPieces.length &&
|
|
400
|
-
candidateInPieces.every((piece) => balance.amounts.includes(piece))) {
|
|
401
|
-
return funded(candidateOutPieces);
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
return null;
|
|
406
|
-
}
|
|
407
|
-
function applyEffects(instrument, account, action, input, effects, reservations, exactBalance, add) {
|
|
408
|
-
let balance = input.balance;
|
|
409
|
-
const holds = { ...input.holds };
|
|
410
|
-
let exchanged = input.exchanged ?? false;
|
|
411
|
-
const accountLabel = formatAccount(account);
|
|
412
|
-
for (const effect of effects) {
|
|
413
|
-
const fail = (message) => add([...effect.path, "sourceAccountId"], `action ${action} ${message}`);
|
|
414
|
-
if (effect.kind === "credit") {
|
|
415
|
-
if (exactBalance &&
|
|
416
|
-
(balance.kind !== "empty" || Object.keys(holds).length > 0)) {
|
|
417
|
-
add([...effect.path, "destinationAccountId"], `action ${action} funds ${accountLabel} while earlier value may remain; quoted funding must establish exactly one refundable balance`);
|
|
418
|
-
balance = UNKNOWN;
|
|
419
|
-
continue;
|
|
420
|
-
}
|
|
421
|
-
const pieces = balance.kind === "funded" ? balance.amounts : [];
|
|
422
|
-
const stacks = (balance.kind === "empty" || balance.kind === "funded") &&
|
|
423
|
-
!pieces.includes(effect.amount);
|
|
424
|
-
balance = stacks ? funded([...pieces, effect.amount]) : UNKNOWN;
|
|
425
|
-
continue;
|
|
426
|
-
}
|
|
427
|
-
if (effect.kind === "incoming_reserve")
|
|
428
|
-
continue;
|
|
429
|
-
if (effect.kind === "outgoing_reserve") {
|
|
430
|
-
let held = balance.kind === "funded" && balance.amounts.includes(effect.amount);
|
|
431
|
-
if (!held && balance.kind === "funded") {
|
|
432
|
-
const substituted = tryPartitionSubstitution(instrument, balance, holds, exchanged, effect.amount);
|
|
433
|
-
if (substituted !== null) {
|
|
434
|
-
exchanged = true;
|
|
435
|
-
balance = substituted;
|
|
436
|
-
held =
|
|
437
|
-
balance.kind === "funded" &&
|
|
438
|
-
balance.amounts.includes(effect.amount);
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
if (!held) {
|
|
442
|
-
fail(`cannot reserve ${effect.amount} from ${accountLabel}; that exact balance is not guaranteed`);
|
|
443
|
-
}
|
|
444
|
-
holds[effect.reservation] = held
|
|
445
|
-
? { kind: "funded", amounts: [effect.amount] }
|
|
446
|
-
: UNKNOWN;
|
|
447
|
-
balance =
|
|
448
|
-
held && balance.kind === "funded"
|
|
449
|
-
? funded(withoutPiece(balance.amounts, effect.amount))
|
|
450
|
-
: EMPTY;
|
|
451
|
-
continue;
|
|
452
|
-
}
|
|
453
|
-
if (effect.kind === "post" || effect.kind === "void") {
|
|
454
|
-
const key = effect.reservation;
|
|
455
|
-
const reservation = reservations.get(key);
|
|
456
|
-
if (reservation?.source &&
|
|
457
|
-
accountsMayAlias(instrument, reservation.source, account)) {
|
|
458
|
-
const held = holds[key];
|
|
459
|
-
delete holds[key];
|
|
460
|
-
if (effect.kind === "void" && held) {
|
|
461
|
-
const pieces = balance.kind === "funded" ? balance.amounts : [];
|
|
462
|
-
const restorable = held.kind === "funded" &&
|
|
463
|
-
(balance.kind === "empty" ||
|
|
464
|
-
(balance.kind === "funded" &&
|
|
465
|
-
held.amounts.every((piece) => !pieces.includes(piece))));
|
|
466
|
-
if (restorable && held.kind === "funded") {
|
|
467
|
-
balance = funded([...pieces, ...held.amounts]);
|
|
468
|
-
}
|
|
469
|
-
else if (balance.kind === "empty") {
|
|
470
|
-
balance = held;
|
|
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);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
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))]]);
|
|
86
|
+
}
|
|
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);
|
|
94
|
+
}
|
|
95
|
+
return result;
|
|
96
|
+
}
|
|
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]]);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
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));
|
|
471
131
|
}
|
|
472
|
-
else {
|
|
473
|
-
add([...effect.path, "transferId"], `action ${action} restores a held ${accountLabel} balance on top of existing value`);
|
|
474
|
-
balance = UNKNOWN;
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
if (effect.kind === "post" && reservation?.destination === account) {
|
|
479
|
-
if (balance.kind !== "empty" || Object.keys(holds).length > 0) {
|
|
480
|
-
add([...effect.path, "transferId"], `action ${action} posts funding into ${accountLabel} while earlier value may remain`);
|
|
481
|
-
balance = UNKNOWN;
|
|
482
|
-
}
|
|
483
|
-
else {
|
|
484
|
-
balance = { kind: "funded", amounts: [effect.amount] };
|
|
485
132
|
}
|
|
486
133
|
}
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
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 });
|
|
157
|
+
}
|
|
158
|
+
else
|
|
159
|
+
credit(move.to, value);
|
|
496
160
|
}
|
|
497
161
|
else {
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
: { kind: "penalty" };
|
|
502
|
-
}
|
|
503
|
-
continue;
|
|
504
|
-
}
|
|
505
|
-
if (effect.kind === "penalty_debit") {
|
|
506
|
-
if (balance.kind !== "penalty") {
|
|
507
|
-
fail(`cannot pay the quoted charge from ${accountLabel} before the refund leaves that exact remainder`);
|
|
508
|
-
}
|
|
509
|
-
balance = EMPTY;
|
|
510
|
-
continue;
|
|
511
|
-
}
|
|
512
|
-
if (effect.kind === "dynamic_debit") {
|
|
513
|
-
if (balance.kind === "empty") {
|
|
514
|
-
fail(`can debit unfunded ${accountLabel}`);
|
|
515
|
-
}
|
|
516
|
-
else if (balance.kind === "penalty") {
|
|
517
|
-
fail(`can debit ${accountLabel} after its refund left only the quoted charge`);
|
|
518
|
-
}
|
|
519
|
-
balance = balance.kind === "empty" ? EMPTY : { kind: "unknown" };
|
|
520
|
-
continue;
|
|
521
|
-
}
|
|
522
|
-
if (balance.kind === "funded") {
|
|
523
|
-
if (balance.amounts.includes(effect.amount)) {
|
|
524
|
-
balance = funded(withoutPiece(balance.amounts, effect.amount));
|
|
525
|
-
continue;
|
|
526
|
-
}
|
|
527
|
-
const substituted = tryPartitionSubstitution(instrument, balance, holds, exchanged, effect.amount);
|
|
528
|
-
if (substituted !== null) {
|
|
529
|
-
exchanged = true;
|
|
530
|
-
balance = substituted;
|
|
531
|
-
if (balance.kind === "funded" &&
|
|
532
|
-
balance.amounts.includes(effect.amount)) {
|
|
533
|
-
balance = funded(withoutPiece(balance.amounts, effect.amount));
|
|
162
|
+
const hold = state.holds.get(move.transfer);
|
|
163
|
+
if (!hold) {
|
|
164
|
+
report(`${move.key} consumes a missing reservation ${move.transfer}`);
|
|
534
165
|
continue;
|
|
535
166
|
}
|
|
167
|
+
credit(move.operation === "internal_transfer.post" ? hold.to : hold.from, hold.amount);
|
|
168
|
+
state.holds.delete(move.transfer);
|
|
536
169
|
}
|
|
537
170
|
}
|
|
538
|
-
if (
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
: `can debit unfunded ${accountLabel}`);
|
|
542
|
-
}
|
|
543
|
-
else if (balance.kind === "penalty") {
|
|
544
|
-
fail(`can debit ${accountLabel} after its refund left only the quoted charge`);
|
|
545
|
-
}
|
|
546
|
-
else if (balance.kind === "funded" || exactBalance) {
|
|
547
|
-
fail(`cannot prove ${effect.amount} is the exact available balance of ${accountLabel}`);
|
|
548
|
-
}
|
|
549
|
-
balance = EMPTY;
|
|
550
|
-
}
|
|
551
|
-
// A held account permits at most one equal-partition substitution per funded
|
|
552
|
-
// balance, preventing cycles, duplicate minting, and double spending. When an
|
|
553
|
-
// account is completely drained to empty with no active holds, the exchange
|
|
554
|
-
// record resets for any distinct subsequent funding.
|
|
555
|
-
if (balance.kind === "empty" && Object.keys(holds).length === 0) {
|
|
556
|
-
exchanged = false;
|
|
557
|
-
}
|
|
558
|
-
return { balance, holds, exchanged };
|
|
559
|
-
}
|
|
560
|
-
/**
|
|
561
|
-
* Flattens every quote-commit pair the instrument declares. A quoting action
|
|
562
|
-
* with no committing action, or a commit naming a non-quoting action, is a
|
|
563
|
-
* validator failure rather than a typestate one, so it is simply skipped here.
|
|
564
|
-
*/
|
|
565
|
-
function quoteFacts(instrument, options) {
|
|
566
|
-
const facts = [];
|
|
567
|
-
for (const [quoting, action] of Object.entries(instrument.actions)) {
|
|
568
|
-
const quote = action.quote;
|
|
569
|
-
if (!quote)
|
|
570
|
-
continue;
|
|
571
|
-
const commit = Object.entries(instrument.actions).find(([, candidate]) => candidate.commit === quoting)?.[0];
|
|
572
|
-
if (commit === undefined)
|
|
573
|
-
continue;
|
|
574
|
-
const chargePath = `refs.${quote.chargeRef}`;
|
|
575
|
-
const declaresChargePayout = Object.values(instrument.actions).some((candidate) => (candidate.moves ?? []).some((move) => Object.values(move.bind).some((binding) => binding.from === "instance" && binding.path === chargePath)));
|
|
576
|
-
facts.push({
|
|
577
|
-
baseField: quote.baseField,
|
|
578
|
-
chargeCanBeNonzero: options.penaltyMayBeNonzero === true ||
|
|
579
|
-
declaresChargePayout ||
|
|
580
|
-
quote.charges.some((tier) => tier.bps > 0),
|
|
581
|
-
chargeRef: quote.chargeRef,
|
|
582
|
-
chargeRetainedBy: quote.chargeRetainedBy,
|
|
583
|
-
commit,
|
|
584
|
-
netRef: quote.netRef,
|
|
585
|
-
quoting,
|
|
586
|
-
});
|
|
587
|
-
}
|
|
588
|
-
return facts;
|
|
589
|
-
}
|
|
590
|
-
/**
|
|
591
|
-
* The quoted charge is the exact remainder the commit leaves behind, so it may
|
|
592
|
-
* fund one transfer, out of the same account the refund left, in its own
|
|
593
|
-
* one-way action directly after the commit.
|
|
594
|
-
*/
|
|
595
|
-
function validateChargePayout(instrument, quote, refundSource, add) {
|
|
596
|
-
const chargePath = `refs.${quote.chargeRef}`;
|
|
597
|
-
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
|
|
598
|
-
? [{ binding, step, stepIndex, target, action, actionName }]
|
|
599
|
-
: [])));
|
|
600
|
-
if (quote.chargeRetainedBy !== undefined) {
|
|
601
|
-
const role = quote.chargeRetainedBy;
|
|
602
|
-
const partyField = Object.hasOwn(instrument.parties ?? {}, role)
|
|
603
|
-
? instrument.parties?.[role]
|
|
604
|
-
: undefined;
|
|
605
|
-
if (!partyField) {
|
|
606
|
-
add(["actions", quote.quoting, "quote", "chargeRetainedBy"], `quoting action ${quote.quoting} retains charge by undeclared party role ${role}`);
|
|
607
|
-
}
|
|
608
|
-
else {
|
|
609
|
-
const expectedSource = `field:${partyField}`;
|
|
610
|
-
if (refundSource === undefined) {
|
|
611
|
-
add(["actions", quote.commit, "moves"], `commit action ${quote.commit} refund source cannot be resolved to charge-retaining party field ${partyField}`);
|
|
612
|
-
}
|
|
613
|
-
else if (refundSource !== expectedSource) {
|
|
614
|
-
if (refundSource.startsWith("ref:")) {
|
|
615
|
-
add(["actions", quote.commit, "moves"], `commit action ${quote.commit} refund source cannot be a product escrow ref for a retained quote`);
|
|
616
|
-
}
|
|
617
|
-
else {
|
|
618
|
-
add(["actions", quote.commit, "moves"], `commit action ${quote.commit} refund source ${refundSource} does not match charge-retaining party field ${partyField}`);
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
if (uses.length > 0) {
|
|
623
|
-
add(["actions"], `${chargePath} is retained by ${role} and cannot be consumed by an action`);
|
|
624
|
-
for (const use of uses) {
|
|
625
|
-
add([
|
|
626
|
-
"actions",
|
|
627
|
-
use.actionName,
|
|
628
|
-
"moves",
|
|
629
|
-
use.stepIndex,
|
|
630
|
-
"bind",
|
|
631
|
-
use.target,
|
|
632
|
-
], `${chargePath} is retained by ${role} and cannot be consumed by an action`);
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
return;
|
|
636
|
-
}
|
|
637
|
-
if (quote.chargeCanBeNonzero && uses.length !== 1) {
|
|
638
|
-
add(["actions"], `${chargePath} is consumed ${uses.length} times; a nonzero charge schedule requires exactly one payout`);
|
|
639
|
-
}
|
|
640
|
-
if (uses.length === 0)
|
|
641
|
-
return;
|
|
642
|
-
if (uses.length !== 1) {
|
|
643
|
-
if (!quote.chargeCanBeNonzero) {
|
|
644
|
-
add(["actions"], `${chargePath} is consumed ${uses.length} times; it may fund at most one payout`);
|
|
645
|
-
}
|
|
646
|
-
return;
|
|
647
|
-
}
|
|
648
|
-
const use = uses[0];
|
|
649
|
-
const payoutTransition = instrument.lifecycle.transitions[use.actionName];
|
|
650
|
-
const commitTarget = instrument.lifecycle.transitions[quote.commit]?.to;
|
|
651
|
-
const source = canonicalAccount(instrument, use.step.bind.sourceAccountId);
|
|
652
|
-
const destination = canonicalAccount(instrument, use.step.bind.destinationAccountId);
|
|
653
|
-
const directAfterCommit = commitTarget !== undefined &&
|
|
654
|
-
payoutTransition?.from.length === 1 &&
|
|
655
|
-
payoutTransition.from[0] === commitTarget;
|
|
656
|
-
const oneWay = payoutTransition !== undefined &&
|
|
657
|
-
payoutTransition.to !== commitTarget &&
|
|
658
|
-
!stateCanReach(instrument, payoutTransition.to, commitTarget);
|
|
659
|
-
const valid = use.target === "amount" &&
|
|
660
|
-
use.step.operation === "internal_transfer.create" &&
|
|
661
|
-
(use.action.moves?.length ?? 0) === 1 &&
|
|
662
|
-
directAfterCommit &&
|
|
663
|
-
oneWay &&
|
|
664
|
-
source !== undefined &&
|
|
665
|
-
source === refundSource &&
|
|
666
|
-
destination !== undefined &&
|
|
667
|
-
!accountsMayAlias(instrument, source, destination);
|
|
668
|
-
if (!valid) {
|
|
669
|
-
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}`);
|
|
670
|
-
}
|
|
671
|
-
}
|
|
672
|
-
function canonicalAccount(instrument, binding) {
|
|
673
|
-
if (binding?.from !== "instance")
|
|
674
|
-
return undefined;
|
|
675
|
-
return canonicalAccountPath(instrument, binding.path);
|
|
676
|
-
}
|
|
677
|
-
function canonicalAccountPath(instrument, path) {
|
|
678
|
-
const [root, key, extra] = path.split(".");
|
|
679
|
-
if (!key || extra !== undefined)
|
|
680
|
-
return undefined;
|
|
681
|
-
if (root === "fields")
|
|
682
|
-
return `field:${key}`;
|
|
683
|
-
if (root === "refs")
|
|
684
|
-
return `ref:${key}`;
|
|
685
|
-
if (root === "party") {
|
|
686
|
-
const field = instrument.parties?.[key];
|
|
687
|
-
return field ? `field:${field}` : undefined;
|
|
688
|
-
}
|
|
689
|
-
return undefined;
|
|
690
|
-
}
|
|
691
|
-
function accountsMayAlias(instrument, left, right) {
|
|
692
|
-
if (left === right)
|
|
693
|
-
return true;
|
|
694
|
-
if (left.startsWith("field:") && right.startsWith("field:")) {
|
|
695
|
-
const leftField = left.slice("field:".length);
|
|
696
|
-
const rightField = right.slice("field:".length);
|
|
697
|
-
if (leftField === rightField)
|
|
698
|
-
return true;
|
|
699
|
-
if (instrument.parties) {
|
|
700
|
-
const leftParty = Object.entries(instrument.parties).find(([, field]) => field === leftField)?.[0];
|
|
701
|
-
const rightParty = Object.entries(instrument.parties).find(([, field]) => field === rightField)?.[0];
|
|
702
|
-
if (leftParty !== undefined &&
|
|
703
|
-
rightParty !== undefined &&
|
|
704
|
-
leftParty !== rightParty) {
|
|
705
|
-
return false;
|
|
706
|
-
}
|
|
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);
|
|
707
174
|
}
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
175
|
+
};
|
|
176
|
+
const initial = {
|
|
177
|
+
status: instrument.lifecycle.initial,
|
|
178
|
+
balances: new Map([...owned].map((key) => [key, new Map()])),
|
|
179
|
+
holds: new Map(),
|
|
180
|
+
};
|
|
181
|
+
if (instrument.actions.create)
|
|
182
|
+
apply(initial, instrument.actions.create, "create");
|
|
183
|
+
const pending = [initial];
|
|
184
|
+
const reached = new Map();
|
|
185
|
+
const copy = (state) => ({
|
|
186
|
+
status: state.status,
|
|
187
|
+
balances: new Map(state.balances),
|
|
188
|
+
holds: new Map([...state.holds].map(([key, hold]) => [key, { ...hold }])),
|
|
189
|
+
});
|
|
190
|
+
let work = 0;
|
|
191
|
+
while (pending.length && !problems.length) {
|
|
192
|
+
if (++work > 4096) {
|
|
193
|
+
report("owned proof exceeds its finite work budget");
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
let state = pending.pop();
|
|
197
|
+
const key = JSON.stringify([state.status, [...state.holds.keys()].sort()]);
|
|
198
|
+
const previous = reached.get(key);
|
|
199
|
+
if (previous) {
|
|
200
|
+
const joined = copy(previous);
|
|
201
|
+
let changed = false;
|
|
202
|
+
for (const account of owned) {
|
|
203
|
+
const before = previous.balances.has(account)
|
|
204
|
+
? previous.balances.get(account)
|
|
205
|
+
: new Map();
|
|
206
|
+
const after = state.balances.has(account)
|
|
207
|
+
? state.balances.get(account)
|
|
208
|
+
: new Map();
|
|
209
|
+
if (before !== null && !same(before, after)) {
|
|
210
|
+
joined.balances.set(account, null);
|
|
211
|
+
changed = true;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
for (const [name, hold] of joined.holds) {
|
|
215
|
+
const next = state.holds.get(name);
|
|
216
|
+
if (hold.from !== next.from || hold.to !== next.to) {
|
|
217
|
+
report(`${name} changes reservation endpoints across paths`);
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
if (hold.amount !== null && !same(hold.amount, next.amount)) {
|
|
221
|
+
hold.amount = null;
|
|
222
|
+
changed = true;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (!changed)
|
|
737
226
|
continue;
|
|
738
|
-
|
|
739
|
-
|
|
227
|
+
state = joined;
|
|
228
|
+
}
|
|
229
|
+
if (reached.size >= 256 && !previous) {
|
|
230
|
+
report("owned proof exceeds 256 state and reservation combinations");
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
reached.set(key, copy(state));
|
|
234
|
+
const edges = Object.entries(instrument.lifecycle.transitions).filter(([, edge]) => edge.from.includes(state.status));
|
|
235
|
+
if (!edges.length &&
|
|
236
|
+
([...state.balances].some(([account, balance]) => !shared.has(account) && (balance === null || balance.size > 0)) ||
|
|
237
|
+
state.holds.size))
|
|
238
|
+
report(`terminal state ${state.status} may strand owned money or an open reservation`);
|
|
239
|
+
for (const [name, edge] of edges) {
|
|
240
|
+
const action = instrument.actions[name];
|
|
241
|
+
if (!action)
|
|
242
|
+
continue;
|
|
243
|
+
const next = copy(state);
|
|
244
|
+
next.status = edge.to;
|
|
245
|
+
apply(next, action, name);
|
|
246
|
+
pending.push(next);
|
|
740
247
|
}
|
|
741
248
|
}
|
|
742
|
-
return
|
|
743
|
-
}
|
|
744
|
-
function stateKey(state) {
|
|
745
|
-
return JSON.stringify({
|
|
746
|
-
balance: state.balance,
|
|
747
|
-
holds: Object.fromEntries(Object.entries(state.holds).sort(([a], [b]) => a.localeCompare(b))),
|
|
748
|
-
exchanged: state.exchanged ?? false,
|
|
749
|
-
});
|
|
750
|
-
}
|
|
751
|
-
function formatAccount(account) {
|
|
752
|
-
const [kind, key] = account.split(":");
|
|
753
|
-
return `${kind === "field" ? "fields" : "refs"}.${key}`;
|
|
249
|
+
return problems;
|
|
754
250
|
}
|
|
755
251
|
//# sourceMappingURL=finance.js.map
|