@hyperscale0/hsx 1.0.265 → 1.0.284
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 +36 -5
- package/README.md +2 -1
- package/SECURITY.md +10 -21
- package/dist/src/compile.d.ts.map +1 -1
- package/dist/src/compile.js +108 -14
- package/dist/src/compile.js.map +1 -1
- package/dist/src/examples-bundle.js +3 -3
- package/dist/src/examples-bundle.js.map +1 -1
- package/dist/src/std-bundle.d.ts.map +1 -1
- package/dist/src/std-bundle.js +7 -6
- package/dist/src/std-bundle.js.map +1 -1
- package/docs/README.md +40 -31
- package/examples/library.hsx +7 -1
- package/examples/rental-deposit.hsx +5 -12
- package/examples/serviced.hsx +7 -1
- package/package.json +4 -4
- package/src/compile.ts +138 -19
- package/src/examples-bundle.ts +3 -3
- package/src/std-bundle.ts +7 -6
- package/std/booking.hsx +8 -3
- package/std/collections.hsx +1 -1
- package/std/escrow.hsx +16 -29
- package/std/financing.hsx +21 -48
- package/std/money.hsx +6 -8
- package/std/purchase.hsx +89 -0
- package/std/reporting.hsx +1 -11
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"std-bundle.js","sourceRoot":"","sources":["../../src/std-bundle.ts"],"names":[],"mappings":"AAAA,oEAAoE;AAEpE,MAAM,CAAC,MAAM,iBAAiB,GAAgC,IAAI,GAAG,CAAiB;IACpF,CAAC,aAAa,EAAE,
|
|
1
|
+
{"version":3,"file":"std-bundle.js","sourceRoot":"","sources":["../../src/std-bundle.ts"],"names":[],"mappings":"AAAA,oEAAoE;AAEpE,MAAM,CAAC,MAAM,iBAAiB,GAAgC,IAAI,GAAG,CAAiB;IACpF,CAAC,aAAa,EAAE,m7LAAm7L,CAAC;IACp8L,CAAC,WAAW,EAAE,6wMAA6wM,CAAC;IAC5xM,CAAC,iBAAiB,EAAE,qrNAAqrN,CAAC;IAC1sN,CAAC,YAAY,EAAE,mkOAAmkO,CAAC;IACnlO,CAAC,eAAe,EAAE,kinFAAkinF,CAAC;IACrjnF,CAAC,eAAe,EAAE,41LAA41L,CAAC;IAC/2L,CAAC,aAAa,EAAE,03UAA03U,CAAC;IAC34U,CAAC,iBAAiB,EAAE,syTAAsyT,CAAC;IAC3zT,CAAC,WAAW,EAAE,ogYAAogY,CAAC;IACnhY,CAAC,cAAc,EAAE,svJAAsvJ,CAAC;IACxwJ,CAAC,eAAe,EAAE,4o1BAA4o1B,CAAC;IAC/p1B,CAAC,aAAa,EAAE,4hKAA4hK,CAAC;IAC7iK,CAAC,YAAY,EAAE,2zIAA2zI,CAAC;CAC50I,CAAC,CAAC"}
|
package/docs/README.md
CHANGED
|
@@ -42,10 +42,10 @@ hsx cost lesson.hsx
|
|
|
42
42
|
`check` exits zero without output on success. `build` writes canonical UDL;
|
|
43
43
|
without `--out` it prints to stdout. `cost` prints instruction counts, described
|
|
44
44
|
below. `format` prints formatted source or writes it with `--out`. `headers --json`
|
|
45
|
-
prints the header manifest.
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
45
|
+
prints the header manifest. Exit 1 means invalid source; exit 2 means a command
|
|
46
|
+
or file error. Diagnostics name the source, line, column, code, problem and
|
|
47
|
+
suggested fix. UDL validation keeps its UDL diagnostic codes; binding errors can
|
|
48
|
+
have names such as
|
|
49
49
|
`subject_party_unbound`. Read the message and fix, not just the code.
|
|
50
50
|
|
|
51
51
|
At runtime, create a lesson object first, then call `book_lesson` to create its
|
|
@@ -57,38 +57,37 @@ and party IDs are not metadata supplied by the caller.
|
|
|
57
57
|
## A rental deposit in one hour
|
|
58
58
|
|
|
59
59
|
Open [rental-deposit.hsx](../examples/rental-deposit.hsx). It is a complete source
|
|
60
|
-
file for a
|
|
60
|
+
file for a customer renting equipment from the program operator. Copy it to
|
|
61
61
|
`rental.hsx` and run the same check, build and cost commands against that file.
|
|
62
62
|
No header import is needed because it declares its own instrument.
|
|
63
63
|
|
|
64
|
-
The terms are a 1,000 SAR deposit and
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
64
|
+
The terms are a 1,000 SAR deposit and one late charge of 50 SAR plus 15% VAT,
|
|
65
|
+
which is 7.50 SAR. The record's `startsAt` and `dueAt` timestamps reach the
|
|
66
|
+
agreement through `rename`, so `agree_rental` reads them from the object. A
|
|
67
|
+
return before `dueAt` gets the whole deposit back; a return at or after it gets
|
|
68
|
+
942.50 SAR back. The operator receives 50 SAR and `programTax` receives 7.50 SAR.
|
|
69
|
+
The late charge is not a daily charge and is not `financing.late_charge`, which
|
|
68
70
|
requires a financing plan and an overdue installment.
|
|
69
71
|
|
|
70
|
-
| Step | Public action | Actor and required values
|
|
71
|
-
| ---------------------------- | ----------------------- |
|
|
72
|
-
| Create the object | Host object creation | Optional `equipment` metadata
|
|
73
|
-
| Agree terms | `agree_rental` | Bound renter; `dueAt`, for example `2027-01-10T12:00:00+03:00`,
|
|
74
|
-
| Fund before
|
|
75
|
-
| Record an on-time return | `return_equipment` | Operator
|
|
76
|
-
| Or record a late return | `return_equipment_late` | Operator
|
|
77
|
-
| Cancel
|
|
72
|
+
| Step | Public action | Actor and required values | Money effect |
|
|
73
|
+
| ---------------------------- | ----------------------- | --------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
|
|
74
|
+
| Create the object | Host object creation | Optional `equipment`, `serialNumber`, `renterName`, `startsAt` and `dueAt` metadata | None |
|
|
75
|
+
| Agree terms | `agree_rental` | Bound renter; `startsAt` and `dueAt`, for example `2027-01-10T12:00:00+03:00`, both future, `dueAt` later | Creates the agreement's held account |
|
|
76
|
+
| Fund before pickup | `pay_deposit` | Bound renter with 1,000 SAR available, before `startsAt` | Renter pays 1,000 SAR into held |
|
|
77
|
+
| Record an on-time return | `return_equipment` | Operator, before `dueAt`; `returnReference` metadata | Held pays 1,000 SAR to renter |
|
|
78
|
+
| Or record a late return | `return_equipment_late` | Operator, at or after `dueAt`; `returnReference` metadata | Held pays 50 SAR to operator, 7.50 SAR to tax and 942.50 SAR to renter |
|
|
79
|
+
| Cancel before pickup | `cancel_before_pickup` | Bound renter on a funded agreement, before `startsAt` | Held pays 1,000 SAR back to renter |
|
|
80
|
+
| Cancel an unfunded agreement | `cancel_unpaid_rental` | Bound renter, while pending | None |
|
|
78
81
|
|
|
79
82
|
Choose one return action. Both finish the agreement, so the fee and refund cannot
|
|
80
|
-
repeat. The late action calculates the refund from the held balance before
|
|
83
|
+
repeat. The late action calculates the refund from the held balance before any
|
|
81
84
|
move runs. Both paths empty the held account. `returnReference` records the
|
|
82
85
|
operator's return evidence; it does not verify the physical return. Time means
|
|
83
86
|
the executor's current time when the action runs, not a caller-supplied return date.
|
|
84
87
|
A late `due` permits the operator action; it does not schedule that action.
|
|
85
88
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
parameters. Changing the renter binding is a Build configuration decision, not a
|
|
89
|
-
different party ID on each request. For per-object participants, give a program
|
|
90
|
-
instrument parameters as a header would, such as `instrument purse(holder: party)`,
|
|
91
|
-
and bind `holder: owner` or `holder: actor` in each `attach` block. A
|
|
89
|
+
The renter is a party parameter bound to the rental object's `owner`. Each rental
|
|
90
|
+
can belong to a different customer without a fixed business binding. A
|
|
92
91
|
parameterized program instrument exists only through its attachments. A `fee` on
|
|
93
92
|
a move of an action input, such as `moves input.amount from holder to self.held
|
|
94
93
|
fee { seller: 1% }`, is calculated when that action runs. Reusable custom headers
|
|
@@ -97,8 +96,7 @@ this CLI.
|
|
|
97
96
|
|
|
98
97
|
Inspect `objects` in the compiled UDL for the attachment and public names, then
|
|
99
98
|
its instrument for fields, transitions, actors, requirements and ordered moves.
|
|
100
|
-
|
|
101
|
-
the attachment exposes its own copy. `money.hold` and `escrow.hold` both refund
|
|
99
|
+
`money.hold` and `escrow.hold` both refund
|
|
102
100
|
whole amounts, so neither implements this partial-refund policy by itself.
|
|
103
101
|
|
|
104
102
|
A local compile gives you a contract. Executing it needs a host with authenticated
|
|
@@ -189,7 +187,7 @@ and basis points. Fee and tax calculations round down. Split percentages sum to
|
|
|
189
187
|
schedules use positions 1 through n and put the division remainder in position 1.
|
|
190
188
|
Amortizing financing is the exception: it rounds half up and puts the remainder in
|
|
191
189
|
the last instalment, as a standard amortization table does.
|
|
192
|
-
Tax binds `programTax`. Fees
|
|
190
|
+
Tax binds `programTax`. Fees go to `programOperator`. Fine, recovery and
|
|
193
191
|
residual destinations are party tunables.
|
|
194
192
|
|
|
195
193
|
Attached actions are private unless the attachment exposes them with
|
|
@@ -284,8 +282,9 @@ maxAge 1d` requires a recent completed provider check.
|
|
|
284
282
|
only to reservations. Fees settle with create moves, never reservations. A fee move
|
|
285
283
|
also derives `<action>_<move>_debit`, the payer's whole debit: amount plus fee and
|
|
286
284
|
tax when the buyer pays, the amount alone when the seller pays. Repeated clauses keep
|
|
287
|
-
their declaration order.
|
|
288
|
-
|
|
285
|
+
their declaration order. A clause list after a colon, such as `calculate: [{ ... }]`
|
|
286
|
+
or `invoke: [{ ... }]`, writes [UDL clauses](https://github.com/hyperscale0/hyperscale-udl/blob/main/spec/README.md)
|
|
287
|
+
directly; the std headers use it for calculations, `set` and invocations.
|
|
289
288
|
Use `economics { purpose: earning, sourceParty: merchant }` on a move to
|
|
290
289
|
classify posted money. Other purposes are `principal`, `participant_payout`,
|
|
291
290
|
`internal`, `prepaid_credit`, and `pass_through`. `reversalOf: self.originalTransfer` links a
|
|
@@ -369,7 +368,8 @@ the selection limit, so a count is an upper bound rather than observed usage.
|
|
|
369
368
|
Zero-valued moves can cost fewer actual transfers than their static count.
|
|
370
369
|
|
|
371
370
|
The rental attachment's create declares one owned account, fund declares one
|
|
372
|
-
transfer, on-time return one, and late return
|
|
371
|
+
transfer, on-time return one, and late return three: the charge, its VAT and
|
|
372
|
+
the refund. These are counts, not SAR
|
|
373
373
|
prices. The executor's tariff supplies commercial prices and actual usage.
|
|
374
374
|
|
|
375
375
|
The compiler API is `compile(source)`. On success, `result.verdict` is `valid`
|
|
@@ -395,7 +395,16 @@ customer: `financing.limits` and `financing.installments` (borrower),
|
|
|
395
395
|
(member). Several customers can each start one on the same record. A plan,
|
|
396
396
|
commitment or other reference resolves the sibling that binds the same
|
|
397
397
|
accounts to the parties both share, so each customer's plan finds its own
|
|
398
|
-
|
|
398
|
+
checkout and each investor's commitment finds their own wallet.
|
|
399
|
+
|
|
400
|
+
A financed purchase connects `financing.installments` to `escrow.hold` through
|
|
401
|
+
`purchase.checkout`. Bind the plan's `funds` to checkout, checkout's `funds` to
|
|
402
|
+
the hold, and checkout's `plans` to the plan. Give the hold the funding policy
|
|
403
|
+
`{ controllers: [checkout], reference: "funds", blocking_states: [active] }`.
|
|
404
|
+
If a marketplace reservation contributes a deposit, its `converters` also names
|
|
405
|
+
checkout. Create the hold, then checkout, then the plan. The coordinator collects
|
|
406
|
+
the missing down payment and returns outstanding lender capital before a buyer
|
|
407
|
+
refund. See [library.hsx](../examples/library.hsx) for the complete wiring.
|
|
399
408
|
|
|
400
409
|
`financing.limits` and `financing.portfolio_limit` declare `scope: product`.
|
|
401
410
|
Create and approve the portfolio limit once per Product and the borrower limit
|
package/examples/library.hsx
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
program cars "Riyadh used van finance"
|
|
4
4
|
use escrow
|
|
5
5
|
use financing
|
|
6
|
+
use purchase
|
|
6
7
|
party dealer: business
|
|
7
8
|
|
|
8
9
|
// Bind dealer to the vehicle seller. Set price to its gross 34,500 SAR invoice;
|
|
@@ -13,6 +14,7 @@ object car "Financed van purchase" {
|
|
|
13
14
|
columns: [make, model, year, vin, price]
|
|
14
15
|
attach sale = escrow.hold {
|
|
15
16
|
payer: actor, payee: dealer
|
|
17
|
+
funding: { controllers: [checkout], reference: "funds", blocking_states: [active] }
|
|
16
18
|
fee: { seller: 2%, tax: 15% }
|
|
17
19
|
deliver_within: 7d, accept_within: 3d
|
|
18
20
|
expose create as agree_van_purchase
|
|
@@ -42,10 +44,14 @@ object car "Financed van purchase" {
|
|
|
42
44
|
// At 34,500 SAR, 2.5% flat profit is 862.50 SAR for the whole term.
|
|
43
45
|
// Financing fee: 0%, tax: 0% on principal; no separate financing service fee.
|
|
44
46
|
// Supply three future monthly dates and a unique settlementIdentity.
|
|
47
|
+
attach checkout = purchase.checkout {
|
|
48
|
+
plans: plan, funds: sale, borrower: actor, capital: operator
|
|
49
|
+
expose create as prepare_financed_purchase
|
|
50
|
+
}
|
|
45
51
|
attach plan = financing.installments {
|
|
46
52
|
borrower: actor, capital: operator, share: 0%
|
|
47
53
|
months: 3, pricing: flat_total, profit_rate: 2.5%
|
|
48
|
-
funds:
|
|
54
|
+
funds: checkout, limits: limits, portfolio: ceiling
|
|
49
55
|
returned_goods: refund_all, refund_within: 30d, write_off_after: 90d
|
|
50
56
|
expose create as finance
|
|
51
57
|
expose sign as sign_finance_agreement
|
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Equipment rental holds a 1,000 SAR deposit for the customer who owns each rental.
|
|
2
2
|
// Return the deposit in full, or deduct a single 50 SAR late charge plus 7.50 SAR VAT.
|
|
3
|
-
program equipment_rental "
|
|
4
|
-
party renter: business
|
|
3
|
+
program equipment_rental "Equipment rental deposits"
|
|
5
4
|
|
|
6
|
-
// Bind renter to the
|
|
5
|
+
// Bind renter to the customer who owns this rental. The deposit has fee 0%
|
|
7
6
|
// and tax 0% because it stays refundable principal. The late service has 15% VAT.
|
|
8
|
-
instrument deposit {
|
|
7
|
+
instrument deposit(renter: party) {
|
|
9
8
|
summary: "Hold security until return, with one disclosed late charge."
|
|
10
9
|
fields {
|
|
11
10
|
renter: account of renter
|
|
@@ -56,6 +55,7 @@ object rental "Equipment deposit" {
|
|
|
56
55
|
fields { equipment: text, serialNumber: text, renterName: text }
|
|
57
56
|
columns: [equipment, serialNumber, renterName, startsAt, dueAt]
|
|
58
57
|
attach security = deposit {
|
|
58
|
+
renter: owner
|
|
59
59
|
rename { startsAt: startsAt, dueAt: dueAt }
|
|
60
60
|
expose create as agree_rental
|
|
61
61
|
expose fund as pay_deposit
|
|
@@ -65,10 +65,3 @@ object rental "Equipment deposit" {
|
|
|
65
65
|
expose cancel as cancel_unpaid_rental
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
|
-
|
|
69
|
-
hide deposit.create
|
|
70
|
-
hide deposit.fund
|
|
71
|
-
hide deposit.return_on_time
|
|
72
|
-
hide deposit.return_late
|
|
73
|
-
hide deposit.refund_before_pickup
|
|
74
|
-
hide deposit.cancel
|
package/examples/serviced.hsx
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
program car_financing "Dammam delivery van finance"
|
|
4
4
|
use escrow
|
|
5
5
|
use financing
|
|
6
|
+
use purchase
|
|
6
7
|
use collections
|
|
7
8
|
party dealer: business
|
|
8
9
|
|
|
@@ -13,6 +14,7 @@ object car "Delivery van finance" {
|
|
|
13
14
|
columns: [make, model, year, vin, price]
|
|
14
15
|
attach sale = escrow.hold {
|
|
15
16
|
payer: actor, payee: dealer
|
|
17
|
+
funding: { controllers: [checkout], reference: "funds", blocking_states: [active] }
|
|
16
18
|
fee: { seller: 1%, tax: 15% }, deliver_within: 14d, accept_within: 3d
|
|
17
19
|
expose create as agree_van_purchase
|
|
18
20
|
expose fund as pay_cash_instead
|
|
@@ -40,13 +42,17 @@ object car "Delivery van finance" {
|
|
|
40
42
|
// Down payment is 6,900 SAR. Capital is 62,100 SAR; total flat profit is
|
|
41
43
|
// 5,278.50 SAR. Financing service fee: 0%, tax: 0% on principal repayments.
|
|
42
44
|
// Supply 36 future monthly dates. Approve limits before collecting and funding.
|
|
45
|
+
attach checkout = purchase.checkout {
|
|
46
|
+
plans: plan, funds: sale, borrower: actor, capital: operator
|
|
47
|
+
expose create as prepare_financed_purchase
|
|
48
|
+
}
|
|
43
49
|
attach plan = financing.installments {
|
|
44
50
|
borrower: actor, capital: operator, share: 0%
|
|
45
51
|
months: 36, pricing: flat_total, profit_rate: 8.5%, down_payment: 10%
|
|
46
52
|
apply: fines_profit_principal, payoff_rebate: 100%, write_off_after: 90d
|
|
47
53
|
refund_within: 30d, payoff_quote_expiry: 1h, returned_goods: refund_all
|
|
48
54
|
max_extension: 60d, max_amendments: 2, amendment_expiry: 7d
|
|
49
|
-
funds:
|
|
55
|
+
funds: checkout, limits: limits, portfolio: ceiling
|
|
50
56
|
expose create as finance
|
|
51
57
|
expose sign as sign_contract
|
|
52
58
|
expose collect_down_payment as pay_down_payment
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperscale0/hsx",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.284",
|
|
4
4
|
"description": "The strictly typed HSX language: compile total financial programs into canonical UDL.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"hsx",
|
|
@@ -90,8 +90,8 @@
|
|
|
90
90
|
"bases:bundle": "bun scripts/bundle-bases.ts"
|
|
91
91
|
},
|
|
92
92
|
"dependencies": {
|
|
93
|
-
"@hyperscale0/adl": "1.0.
|
|
94
|
-
"@hyperscale0/udl": "1.0.
|
|
93
|
+
"@hyperscale0/adl": "1.0.284",
|
|
94
|
+
"@hyperscale0/udl": "1.0.284",
|
|
95
95
|
"fast-sha256": "1.3.0"
|
|
96
96
|
},
|
|
97
97
|
"devDependencies": {
|
|
@@ -103,5 +103,5 @@
|
|
|
103
103
|
"Amir Ayub",
|
|
104
104
|
"Sara AlBakaawi"
|
|
105
105
|
],
|
|
106
|
-
"gitHead": "
|
|
106
|
+
"gitHead": "f4101f194e0e7c24997b0625ab1d48bec1255508"
|
|
107
107
|
}
|
package/src/compile.ts
CHANGED
|
@@ -241,6 +241,24 @@ const automatic = (actor: UdlAction["actor"]) =>
|
|
|
241
241
|
(actor !== null && typeof actor === "object" && "parent" in actor);
|
|
242
242
|
const title = (name: string) =>
|
|
243
243
|
name[0]!.toUpperCase() + name.slice(1).replaceAll("_", " ");
|
|
244
|
+
/** Parties an instrument names itself: account owners, actors, move endpoints. */
|
|
245
|
+
function namedParties(instrument: UdlInstrument): string[] {
|
|
246
|
+
const names = instrument.fields.flatMap((field) =>
|
|
247
|
+
field.type === "account" && typeof field.owner === "string"
|
|
248
|
+
? [field.owner]
|
|
249
|
+
: [],
|
|
250
|
+
);
|
|
251
|
+
for (const action of Object.values(instrument.actions)) {
|
|
252
|
+
if (typeof action.actor === "object" && "party" in action.actor)
|
|
253
|
+
names.push(action.actor.party);
|
|
254
|
+
for (const move of action.moves) {
|
|
255
|
+
if (move.economics) names.push(move.economics.sourceParty);
|
|
256
|
+
for (const endpoint of "from" in move ? [move.from, move.to] : [])
|
|
257
|
+
if (endpoint.startsWith("party.")) names.push(endpoint.slice(6));
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return names;
|
|
261
|
+
}
|
|
244
262
|
function entries(block: BlockExpr): Map<string, Expr> {
|
|
245
263
|
const map = new Map<string, Expr>();
|
|
246
264
|
for (const row of block.entries) {
|
|
@@ -283,6 +301,12 @@ function text(expr: Expr): string {
|
|
|
283
301
|
);
|
|
284
302
|
}
|
|
285
303
|
function decimal(raw: string, scale: number, expr: Expr): string {
|
|
304
|
+
if (raw.startsWith("-"))
|
|
305
|
+
fail(
|
|
306
|
+
expr,
|
|
307
|
+
"literal cannot be negative",
|
|
308
|
+
"write a positive value; a move's from and to set its direction",
|
|
309
|
+
);
|
|
286
310
|
const [whole = "", fraction = ""] = raw.split(".");
|
|
287
311
|
if (!/^\d+$/.test(whole) || fraction.length > scale)
|
|
288
312
|
fail(
|
|
@@ -389,6 +413,12 @@ function literal(expr: Expr, expectedType?: string): string | number | boolean {
|
|
|
389
413
|
if (expr.kind === "text") return expr.value;
|
|
390
414
|
if (expr.kind === "name" && ["true", "false"].includes(expr.value))
|
|
391
415
|
return expr.value === "true";
|
|
416
|
+
if (expr.kind === "name" && expr.value === "runtime")
|
|
417
|
+
fail(
|
|
418
|
+
expr,
|
|
419
|
+
"this value is fixed when the program compiles, so it cannot be runtime",
|
|
420
|
+
"write a literal value",
|
|
421
|
+
);
|
|
392
422
|
return fail(expr, "expected a literal", "write a typed constant");
|
|
393
423
|
}
|
|
394
424
|
|
|
@@ -1453,7 +1483,7 @@ export function compile(
|
|
|
1453
1483
|
)
|
|
1454
1484
|
fail(
|
|
1455
1485
|
v,
|
|
1456
|
-
|
|
1486
|
+
`value is outside ${bounds.minimum}..${bounds.maximum}`,
|
|
1457
1487
|
"choose a value inside the tunable's bounds",
|
|
1458
1488
|
);
|
|
1459
1489
|
} catch (error) {
|
|
@@ -1524,7 +1554,7 @@ export function compile(
|
|
|
1524
1554
|
"name the compared tunables",
|
|
1525
1555
|
);
|
|
1526
1556
|
const numericKind = (expr: Expr) =>
|
|
1527
|
-
expr.kind === "name" && /^P/.test(expr.value)
|
|
1557
|
+
expr.kind === "name" && /^P(?:\d|T)/.test(expr.value)
|
|
1528
1558
|
? "duration"
|
|
1529
1559
|
: expr.kind;
|
|
1530
1560
|
if (
|
|
@@ -1627,6 +1657,7 @@ export function compile(
|
|
|
1627
1657
|
return lowered;
|
|
1628
1658
|
};
|
|
1629
1659
|
const fields: UdlField[] = [];
|
|
1660
|
+
const boundaryReceipts = new Map<string, string>();
|
|
1630
1661
|
const calculations: UdlCalculation[] = [];
|
|
1631
1662
|
let currentAction: UdlAction | undefined;
|
|
1632
1663
|
let currentActionName: string | undefined;
|
|
@@ -2070,6 +2101,38 @@ export function compile(
|
|
|
2070
2101
|
actions: Object.create(null) as UdlInstrument["actions"],
|
|
2071
2102
|
actionOrder: [],
|
|
2072
2103
|
};
|
|
2104
|
+
const actionTargets = new Set<string>();
|
|
2105
|
+
const collectTargets = (block: BlockExpr): void => {
|
|
2106
|
+
for (const entry of block.entries)
|
|
2107
|
+
if (entry.key.startsWith("when "))
|
|
2108
|
+
collectTargets(asBlock(entry.value));
|
|
2109
|
+
else if (entry.key === "calculate")
|
|
2110
|
+
for (const item of entry.value.kind === "list"
|
|
2111
|
+
? entry.value.items
|
|
2112
|
+
: [entry.value]) {
|
|
2113
|
+
const target =
|
|
2114
|
+
item.kind === "block" ? entries(item).get("target") : undefined;
|
|
2115
|
+
if (target) actionTargets.add(String(data(target)));
|
|
2116
|
+
}
|
|
2117
|
+
};
|
|
2118
|
+
for (const row of decl.body.entries)
|
|
2119
|
+
if (row.key.startsWith("action ")) collectTargets(asBlock(row.value));
|
|
2120
|
+
// Instrument calculations run before every action, create included, so
|
|
2121
|
+
// a fee or split calculates there only on an amount held from create.
|
|
2122
|
+
const heldFromCreate = (amount: UdlValue): boolean => {
|
|
2123
|
+
if (!("field" in amount)) return true;
|
|
2124
|
+
const [root, name, ...rest] = amount.field.split(".");
|
|
2125
|
+
const field = fields.find((item) => item.name === name);
|
|
2126
|
+
return (
|
|
2127
|
+
root === "self" &&
|
|
2128
|
+
!rest.length &&
|
|
2129
|
+
!!field &&
|
|
2130
|
+
!actionTargets.has(name!) &&
|
|
2131
|
+
(!field.optional ||
|
|
2132
|
+
"value" in field ||
|
|
2133
|
+
calculations.some((node) => node.target === name))
|
|
2134
|
+
);
|
|
2135
|
+
};
|
|
2073
2136
|
for (const row of decl.body.entries.filter((e) =>
|
|
2074
2137
|
e.key.startsWith("action "),
|
|
2075
2138
|
)) {
|
|
@@ -2407,7 +2470,7 @@ export function compile(
|
|
|
2407
2470
|
...(actionSubject ? { subject: actionSubject } : {}),
|
|
2408
2471
|
};
|
|
2409
2472
|
authored.set(a, row.value);
|
|
2410
|
-
const
|
|
2473
|
+
const moveCalculations: UdlCalculation[] = [];
|
|
2411
2474
|
for (const binding of boundaryBindings)
|
|
2412
2475
|
if (
|
|
2413
2476
|
!a.subject?.adapters.find((entry) => entry.binding === binding)
|
|
@@ -2447,12 +2510,28 @@ export function compile(
|
|
|
2447
2510
|
const op = parts.has("operation")
|
|
2448
2511
|
? String(data(parts.get("operation")!))
|
|
2449
2512
|
: "internal_transfer.create";
|
|
2450
|
-
if (op === "internal_transfer.reserve" && !parts.has("capture"))
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2513
|
+
if (op === "internal_transfer.reserve" && !parts.has("capture")) {
|
|
2514
|
+
if (!parts.has("boundary"))
|
|
2515
|
+
fail(
|
|
2516
|
+
move,
|
|
2517
|
+
"reserve needs a receipt field",
|
|
2518
|
+
"add capture: receipt_name",
|
|
2519
|
+
);
|
|
2520
|
+
const capture = `${name}Receipt`;
|
|
2521
|
+
if (fields.some((field) => field.name === capture))
|
|
2522
|
+
fail(
|
|
2523
|
+
move,
|
|
2524
|
+
"boundary receipt name is already used",
|
|
2525
|
+
"name each reservation with an explicit capture",
|
|
2526
|
+
);
|
|
2527
|
+
fields.push({ name: capture, type: "text", optional: true });
|
|
2528
|
+
boundaryReceipts.set(name, capture);
|
|
2529
|
+
parts.set("capture", {
|
|
2530
|
+
kind: "text",
|
|
2531
|
+
value: capture,
|
|
2532
|
+
span: move.span,
|
|
2533
|
+
});
|
|
2534
|
+
}
|
|
2456
2535
|
if (
|
|
2457
2536
|
(op === "internal_transfer.post" ||
|
|
2458
2537
|
op === "internal_transfer.void") &&
|
|
@@ -2643,6 +2722,7 @@ export function compile(
|
|
|
2643
2722
|
);
|
|
2644
2723
|
let total = 0;
|
|
2645
2724
|
const pieces: UdlValue[] = [];
|
|
2725
|
+
const perAction = !heldFromCreate(val(amount));
|
|
2646
2726
|
for (const [index, [party, rate]] of shares.entries()) {
|
|
2647
2727
|
const recipient = resolve({
|
|
2648
2728
|
kind: "name",
|
|
@@ -2694,8 +2774,12 @@ export function compile(
|
|
|
2694
2774
|
"split field name conflicts",
|
|
2695
2775
|
"rename the move key",
|
|
2696
2776
|
);
|
|
2697
|
-
fields.push({
|
|
2698
|
-
|
|
2777
|
+
fields.push({
|
|
2778
|
+
name: target,
|
|
2779
|
+
type: "money",
|
|
2780
|
+
...(perAction ? { optional: true } : {}),
|
|
2781
|
+
});
|
|
2782
|
+
(perAction ? moveCalculations : calculations).push(
|
|
2699
2783
|
index === shares.length - 1 && pieces.length
|
|
2700
2784
|
? {
|
|
2701
2785
|
target,
|
|
@@ -2797,6 +2881,22 @@ export function compile(
|
|
|
2797
2881
|
"write seller: 1% or buyer: 1%",
|
|
2798
2882
|
);
|
|
2799
2883
|
const quoted = terms.get(paidBy)!;
|
|
2884
|
+
// An account the agreement owns holds only what was moved
|
|
2885
|
+
// into it, so a charge on top has nothing to come from.
|
|
2886
|
+
if (
|
|
2887
|
+
paidBy === "buyer" &&
|
|
2888
|
+
fields.some(
|
|
2889
|
+
(field) =>
|
|
2890
|
+
`self.${field.name}` === transfer.from &&
|
|
2891
|
+
field.type === "account" &&
|
|
2892
|
+
field.owner === "self",
|
|
2893
|
+
)
|
|
2894
|
+
)
|
|
2895
|
+
fail(
|
|
2896
|
+
quoted,
|
|
2897
|
+
`a buyer fee cannot come from \`${transfer.from.slice("self.".length)}\`, which holds only the moved amount`,
|
|
2898
|
+
"use a seller fee, which comes out of the held amount",
|
|
2899
|
+
);
|
|
2800
2900
|
const rate = quoted.kind === "capped" ? quoted.rate : quoted;
|
|
2801
2901
|
if (rate.kind !== "percent")
|
|
2802
2902
|
fail(
|
|
@@ -2805,11 +2905,7 @@ export function compile(
|
|
|
2805
2905
|
"write a percentage such as 1%",
|
|
2806
2906
|
);
|
|
2807
2907
|
const prefix = `${name}_${key}`;
|
|
2808
|
-
|
|
2809
|
-
// input exists, so a fee on an input amount runs with its action.
|
|
2810
|
-
const perAction =
|
|
2811
|
-
"field" in transfer.amount &&
|
|
2812
|
-
transfer.amount.field.startsWith("input.");
|
|
2908
|
+
const perAction = !heldFromCreate(transfer.amount);
|
|
2813
2909
|
const derived = (
|
|
2814
2910
|
suffix: string,
|
|
2815
2911
|
label: string,
|
|
@@ -2828,7 +2924,7 @@ export function compile(
|
|
|
2828
2924
|
label,
|
|
2829
2925
|
...(perAction ? { optional: true } : {}),
|
|
2830
2926
|
});
|
|
2831
|
-
(perAction ?
|
|
2927
|
+
(perAction ? moveCalculations : calculations).push({
|
|
2832
2928
|
...calculation,
|
|
2833
2929
|
target,
|
|
2834
2930
|
} as UdlCalculation);
|
|
@@ -2960,8 +3056,8 @@ export function compile(
|
|
|
2960
3056
|
}
|
|
2961
3057
|
} else (a as unknown as Record<string, unknown>)[key] = data(expr);
|
|
2962
3058
|
}
|
|
2963
|
-
if (
|
|
2964
|
-
a.calculate = [...(a.calculate ?? []), ...
|
|
3059
|
+
if (moveCalculations.length)
|
|
3060
|
+
a.calculate = [...(a.calculate ?? []), ...moveCalculations];
|
|
2965
3061
|
if (attachmentInfo) {
|
|
2966
3062
|
if (attachmentInfo.exposed.has(name)) {
|
|
2967
3063
|
if (automatic(a.actor))
|
|
@@ -3060,6 +3156,14 @@ export function compile(
|
|
|
3060
3156
|
inst.actions[name] = a;
|
|
3061
3157
|
inst.actionOrder.push(name);
|
|
3062
3158
|
}
|
|
3159
|
+
for (const action of Object.values(inst.actions))
|
|
3160
|
+
for (const move of action.moves)
|
|
3161
|
+
if ("transfer" in move) {
|
|
3162
|
+
const receipt = boundaryReceipts.get(
|
|
3163
|
+
move.transfer.replace(/^self\./, ""),
|
|
3164
|
+
);
|
|
3165
|
+
if (receipt) move.transfer = `self.${receipt}`;
|
|
3166
|
+
}
|
|
3063
3167
|
if (attachmentInfo && !attachmentInfo.child)
|
|
3064
3168
|
applyAttachmentEconomics(
|
|
3065
3169
|
document,
|
|
@@ -3339,6 +3443,21 @@ export function compile(
|
|
|
3339
3443
|
bindingDiagnostics.push(error.diagnostic);
|
|
3340
3444
|
continue;
|
|
3341
3445
|
}
|
|
3446
|
+
// A business the instrument names itself binds like a parameter, so
|
|
3447
|
+
// Build asks for it and the engine resolves its account and principal.
|
|
3448
|
+
// programOperator and programTax are the company's own accounts.
|
|
3449
|
+
for (const party of document.instruments
|
|
3450
|
+
.slice(firstAttachedInstrument)
|
|
3451
|
+
.flatMap(namedParties))
|
|
3452
|
+
if (
|
|
3453
|
+
document.parties[party]?.kind === "business" &&
|
|
3454
|
+
party !== "programOperator" &&
|
|
3455
|
+
party !== "programTax" &&
|
|
3456
|
+
!Object.values(parties).some(
|
|
3457
|
+
(binding) => "party" in binding && binding.party === party,
|
|
3458
|
+
)
|
|
3459
|
+
)
|
|
3460
|
+
parties[party] ??= { party };
|
|
3342
3461
|
const attachedInst = document.instruments.find((i) => i.id === instId);
|
|
3343
3462
|
for (const createdInst of document.instruments
|
|
3344
3463
|
.slice(firstAttachedInstrument)
|