@ouronet/talos-registry 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -0
- package/TOOLTIP-CANON.md +215 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -0
- package/dist/tooltip.d.ts +91 -0
- package/dist/tooltip.js +226 -0
- package/dist/types.d.ts +15 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -167,6 +167,24 @@ four-segment pipe-joined string and an account is a glyph string, not a `k:` add
|
|
|
167
167
|
a placeholder. A preflight-fed parameter is `null` with a note naming the read — the registry
|
|
168
168
|
will not invent a value it has just told you cannot be constructed.
|
|
169
169
|
|
|
170
|
+
## The tooltip canon
|
|
171
|
+
|
|
172
|
+
`TOOLTIP-CANON.md` and `tooltipModel()` are the shared rules for rendering a pre-ZBOM tooltip —
|
|
173
|
+
what to show, in what order, and which of it may be prefilled. They are shipped as an
|
|
174
|
+
implementation rather than a document because a document drifts from the code it describes:
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
import { tooltipModel, signatureRows } from "@ouronet/talos-registry";
|
|
178
|
+
const m = tooltipModel("TS01-C1.DPTF|C_Transfer", { id: '"OURO-8Nh-JO8JO4F5"' });
|
|
179
|
+
// m.slots -> EVERY execution parameter, in order, with type, value and flags
|
|
180
|
+
// m.preview -> the INFO_ call with ITS OWN parameter list
|
|
181
|
+
// m.kind -> "ouronet" | "native" (native renders gold, has no cost section)
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Six rules, each there because it was got wrong first — most recently a six-parameter transfer
|
|
185
|
+
whose tooltip showed five arguments, because it was rendering the preview's list under the
|
|
186
|
+
execution's heading. Read the canon before building one.
|
|
187
|
+
|
|
170
188
|
### Where a ghost may be used — `ghost.use`
|
|
171
189
|
|
|
172
190
|
A ghost is not equally safe on every surface, and a single value cannot say so. Entries whose
|
package/TOOLTIP-CANON.md
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# The pre-ZBOM tooltip canon
|
|
2
|
+
|
|
3
|
+
A pre-ZBOM tooltip answers one question: **what will this button actually execute, and what will
|
|
4
|
+
it cost?** Hovering a launcher should tell you before you commit to opening anything.
|
|
5
|
+
|
|
6
|
+
Two applications render it — OuronetUI and the Codex — and they were converging on it
|
|
7
|
+
independently, each rediscovering the same traps a few weeks apart. **This document is not the
|
|
8
|
+
canon.** `src/tooltip.ts` is. A document drifts from the code it describes and nobody finds out
|
|
9
|
+
until a user sees a wrong number, so the rules are shipped as an implementation:
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { tooltipModel, tooltipModels, signatureRows } from "@ouronet/talos-registry";
|
|
13
|
+
|
|
14
|
+
const m = tooltipModel("TS01-C1.DPTF|C_Transfer", { id: '"OURO-8Nh-JO8JO4F5"' });
|
|
15
|
+
// m.slots -> every execution parameter, in order, with type, value and flags
|
|
16
|
+
// m.preview -> the INFO_ call, with ITS OWN parameter list
|
|
17
|
+
// m.kind -> "ouronet" | "native"
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
This file explains **why** each rule exists. Every one is here because it was got wrong first.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Rule 1 — show every execution parameter
|
|
25
|
+
|
|
26
|
+
All of them, always, in declared order.
|
|
27
|
+
|
|
28
|
+
`DPTF|C_Transfer` takes six arguments and its tooltip displayed five, because it was rendering
|
|
29
|
+
the **preview's** list:
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
execution (patron executor executee id transfer-amount method) 6
|
|
33
|
+
preview (patron id sender receiver transfer-amount) 5
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Both lists were true. Showing one under a heading that named the other was not. A tooltip that
|
|
37
|
+
lists five rows for a six-parameter function is not a summary — it is a wrong answer, and it
|
|
38
|
+
teaches a call shape that will not work.
|
|
39
|
+
|
|
40
|
+
`tooltipModel().slots` is always exactly the entrypoint's own parameters. A test asserts this for
|
|
41
|
+
**all 423**, not for the one that was reported.
|
|
42
|
+
|
|
43
|
+
## Rule 2 — the arguments belong to the execution, the cost belongs to the preview
|
|
44
|
+
|
|
45
|
+
**410 of the 423 entrypoints have a preview whose parameter list differs from their own.** Only 13
|
|
46
|
+
match. Different names, different order, different arity.
|
|
47
|
+
|
|
48
|
+
So the two cannot share one numbered list, and the preview's values cannot be labelled with the
|
|
49
|
+
execution's names. That mislabelling was a real defect: it rendered `executor = <the pool id>`,
|
|
50
|
+
`swpair = true`, `toggle = MISSING` for a call that was entirely correct.
|
|
51
|
+
|
|
52
|
+
Render the execution's arguments as the numbered list. Mention the preview separately — its name,
|
|
53
|
+
its own parameter list, and the cost it returns.
|
|
54
|
+
|
|
55
|
+
**Bind by NAME, never by position.** Positional merging is silent: both values are strings, so an
|
|
56
|
+
executor's account lands in a token-id slot, type-checks, and prices a different question.
|
|
57
|
+
`tooltipModel(key, values)` merges by name and cannot do otherwise.
|
|
58
|
+
|
|
59
|
+
## Rule 3 — types go on their own row
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
(patron executor executee id transfer-amount method)
|
|
63
|
+
(string string string string decimal bool)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
not
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
(patron:string executor:string executee:string id:string transfer-amount:decimal method:bool)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
The inline form roughly doubles the width of the widest line in the panel, and the panel is
|
|
73
|
+
already the widest thing on screen when it opens. A parallel row costs one line.
|
|
74
|
+
|
|
75
|
+
`signatureRows(m)` returns `{ names, types }` as equal-length arrays for exactly this.
|
|
76
|
+
|
|
77
|
+
## Rule 4 — native is not Ouronet, and the difference is narrower than it looks
|
|
78
|
+
|
|
79
|
+
A `coin.*` call is StoaChain's own root-namespace contract. It is **not** in this registry and
|
|
80
|
+
never will be: the registry is generated from `ouronet-ns`.
|
|
81
|
+
|
|
82
|
+
What is actually different:
|
|
83
|
+
|
|
84
|
+
| | Ouronet | native |
|
|
85
|
+
|---|---|---|
|
|
86
|
+
| INFO_ preview | yes | **no** — nothing prices a `coin` call |
|
|
87
|
+
| IGNIS | collected | **none** |
|
|
88
|
+
| accounts | Ouronet glyph strings | **Kadena-shaped** (`k:` `c:` `u:` `w:`) |
|
|
89
|
+
| gas sponsorship | gas station pays | **gas station pays** — the same |
|
|
90
|
+
|
|
91
|
+
**That last row is the one to get right.** An earlier version of this claimed a native call was
|
|
92
|
+
not sponsored and that the signing account paid its own STOA gas. Both false: every native modal
|
|
93
|
+
carries `GAS_PAYER` on the gas-station key beside its own capability, and says so in its own
|
|
94
|
+
header comment. The claim had been *inferred* from "this is not an Ouronet operation", which says
|
|
95
|
+
nothing about who pays the host chain — and it was wrong in the direction that matters, telling a
|
|
96
|
+
user they were about to spend when they were not.
|
|
97
|
+
|
|
98
|
+
**Render native with a gold perimeter.** The gold is semantic, not decorative: it means *nothing
|
|
99
|
+
here prices this call*. And say what is missing rather than leaving the cost panel empty — an
|
|
100
|
+
empty panel reads as "the price failed to load".
|
|
101
|
+
|
|
102
|
+
Suggested footer, which is what OuronetUI ships:
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
no IGNIS — this is not an Ouronet operation
|
|
106
|
+
a StoaChain `coin` call, so nothing prices it. Gas is still sponsored.
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Signatures for the native calls are in `NATIVE_SIGNATURES`, transcribed from the deployed
|
|
110
|
+
contract with line numbers. Use them rather than typing your own.
|
|
111
|
+
|
|
112
|
+
## Rule 5 — a ghost is not data
|
|
113
|
+
|
|
114
|
+
Every parameter has a worked example. The shapes are real — read from mainnet — so you can see
|
|
115
|
+
that a swpair is a four-segment pipe-joined string and an account is a glyph string, not a `k:`
|
|
116
|
+
address. **They are not submittable**, and three cases need distinguishing:
|
|
117
|
+
|
|
118
|
+
- **`isPlaceholder`** — the generic `"example"`. 225 slots across 147 entrypoints, every one an
|
|
119
|
+
entity id for something that **does not exist on chain** (`fvt-id`, `pool-id`, `score-id`,
|
|
120
|
+
`anchor-id`). Mainnet holds zero anchors and zero scores today. A plausible fake id would return
|
|
121
|
+
a confident price for something nobody owns, which is worse than an obvious gap. **Render it so
|
|
122
|
+
it reads as a placeholder** — grey it, italicise it, anything but plain.
|
|
123
|
+
- **`isPreflightFed`** — the argument is the *output of a read*, not user input. The registry
|
|
124
|
+
refuses to invent one and so must you. `execution.preflight` names the read.
|
|
125
|
+
- **`prefillable: false`** — a guard. See rule 6.
|
|
126
|
+
|
|
127
|
+
**Do not fire the preview read when any argument is a placeholder.** The answer is a foregone
|
|
128
|
+
refusal, and a read per hover is latency the user pays for a known-no. `m.shouldRead` is already
|
|
129
|
+
false in that case.
|
|
130
|
+
|
|
131
|
+
## Rule 6 — a guard may be shown and must not be prefilled
|
|
132
|
+
|
|
133
|
+
Six entrypoints take a `guard`, and five are account operations:
|
|
134
|
+
|
|
135
|
+
```
|
|
136
|
+
DALOS|C_DeploySmartAccount · DALOS|C_DeployStandardAccount · DALOS|C_RotateGovernor
|
|
137
|
+
DALOS|C_RotateGuard · CODEX|C_RotateCodexGuard · AQP-DSA|C_SetOracleAuth
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
A ghost carries two use tags, both defaulting true, emitted only where something is restricted:
|
|
141
|
+
|
|
142
|
+
```jsonc
|
|
143
|
+
"use": { "new-guard": { "zbom": false, "display": "(read-keyset \"ks\")" } }
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
- **`zbom: false`** — never prefill it into an input that can reach a signed transaction. You do
|
|
147
|
+
not hand someone a keyset and ask them to sign under it.
|
|
148
|
+
- **`display`** — render *this* instead of the value. The submittable form and the readable form
|
|
149
|
+
differ: `{"readKeyset": "ks"}` is a transaction-data key NAME, and `(read-keyset "ks")` is what
|
|
150
|
+
a caller actually writes.
|
|
151
|
+
|
|
152
|
+
A value that is **correct to show and wrong to submit** is precisely what one tag cannot express,
|
|
153
|
+
which is why there are two. `tooltipModel` applies both for you.
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## Placement and behaviour
|
|
158
|
+
|
|
159
|
+
Not expressible in the model, so they are stated here and they are not optional.
|
|
160
|
+
|
|
161
|
+
**On the LAUNCHER, never on the ZBOM's execute button.** Owner ruling. Inside a ZBOM the
|
|
162
|
+
information is already on the page — the input zone lists every parameter with its declared type,
|
|
163
|
+
the INFO panel shows the cost — and a tooltip there draws *over* the modal it annotates. The
|
|
164
|
+
tooltip exists to tell you what a button will do **before** you commit to opening it.
|
|
165
|
+
|
|
166
|
+
**Anchor the edge NEAREST the button.** Placed above, pin the bottom and grow upward; placed
|
|
167
|
+
below, pin the top and grow down. Either way the edge beside the button never moves. Pinning the
|
|
168
|
+
top in both orientations — from an assumed height — makes every content change move the bottom
|
|
169
|
+
edge, so a cycling panel grows and shrinks *away* from the button, which is the direction nobody
|
|
170
|
+
is looking.
|
|
171
|
+
|
|
172
|
+
**Cycle at 10 seconds** when a button fronts several executions. One window, a depletion bar, a
|
|
173
|
+
dot per variant. The transaction toaster's ~3s is the wrong model to copy: a toast is glanced at,
|
|
174
|
+
this panel is read — an entrypoint, a parameter list, six arguments and a live cost. Restart at
|
|
175
|
+
the first variant on every hover; a panel resuming mid-cycle describes an operation you did not
|
|
176
|
+
just point at. Cycle in the order the ZBOM presents, so the first thing hovered is the first thing
|
|
177
|
+
met.
|
|
178
|
+
|
|
179
|
+
**Use pointer events.** `onMouseLeave` fires when the cursor crosses onto a child in some engines;
|
|
180
|
+
`pointerleave` does not.
|
|
181
|
+
|
|
182
|
+
**A flickering tooltip is usually a REMOUNT.** If a button component is declared *inside* another
|
|
183
|
+
component's body, every render creates a new component type and React unmounts the whole subtree,
|
|
184
|
+
destroying the open state. Hoist to module scope.
|
|
185
|
+
|
|
186
|
+
**Key the preview effect on the rendered CALL STRING**, not on a values object. An inline literal
|
|
187
|
+
gives a new object identity every render; keying on it re-fires the read, which sets state, which
|
|
188
|
+
re-renders — forever.
|
|
189
|
+
|
|
190
|
+
**Fail open.** An unknown key renders the children alone. A tooltip is an affordance; taking a
|
|
191
|
+
page down because a hint has no data is a worse bug than the missing hint.
|
|
192
|
+
|
|
193
|
+
**Elide long values in the middle, keeping head AND tail.** An Ouronet account is 162 glyphs;
|
|
194
|
+
three in one call makes a tooltip taller than the card it annotates. `"OURO-8Nh…JO4F5"` identifies
|
|
195
|
+
a value where a truncated prefix does not. Keep the quotes *outside* the elision — a reader has to
|
|
196
|
+
be able to tell a Pact string from a bare decimal — and put the full value on `title`.
|
|
197
|
+
|
|
198
|
+
## Enforce it, do not remember it
|
|
199
|
+
|
|
200
|
+
The rule is about a **class**: every button that opens a ZBOM. Classes are where hand-checking
|
|
201
|
+
fails. OuronetUI's guard walks every page, finds every ZBOM-opening launcher and fails on a naked
|
|
202
|
+
one — it was written after the Define buttons were missed once, and it had to be widened after
|
|
203
|
+
three whole toolbars shipped bare while it read a single file and claimed the class.
|
|
204
|
+
|
|
205
|
+
Two things such a guard must get right: model **both** coverage forms (a textual wrap, or an
|
|
206
|
+
`entrypoint=` on a component that wraps internally), and exclude `onClose` handlers, which close a
|
|
207
|
+
ZBOM rather than open one.
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## Sources
|
|
212
|
+
|
|
213
|
+
- `src/tooltip.ts` — the canon itself; `tests/tooltip.test.ts` enforces all six rules
|
|
214
|
+
- 410-of-423 preview divergence, 225 placeholder slots: recomputed from the bundled registry
|
|
215
|
+
- The sponsorship correction: owner, 2026-09-27
|
package/dist/index.d.ts
CHANGED
|
@@ -16,4 +16,6 @@ export type { CallPlan } from "./plan.js";
|
|
|
16
16
|
export { parseCapability, parseCapabilities, capabilityRecipe } from "./caps.js";
|
|
17
17
|
export type { ParsedCapability, CapabilityRecipe } from "./caps.js";
|
|
18
18
|
export { formatForType, formatDecimal, formatInteger, formatString, formatBool } from "./format.js";
|
|
19
|
+
export { tooltipModel, tooltipModels, signatureRows, isNativeKey, isPlaceholder, PLACEHOLDER, NATIVE_SIGNATURES } from "./tooltip.js";
|
|
20
|
+
export type { TooltipModel, TooltipSlot, TooltipPreview } from "./tooltip.js";
|
|
19
21
|
export type * from "./types.js";
|
package/dist/index.js
CHANGED
|
@@ -13,3 +13,6 @@ export { buildCall, buildPreviewCall, buildGhostCall } from "./build.js";
|
|
|
13
13
|
export { planCall, explainCall } from "./plan.js";
|
|
14
14
|
export { parseCapability, parseCapabilities, capabilityRecipe } from "./caps.js";
|
|
15
15
|
export { formatForType, formatDecimal, formatInteger, formatString, formatBool } from "./format.js";
|
|
16
|
+
// THE TOOLTIP CANON. Shared implementation rather than a shared document -- see TOOLTIP-CANON.md
|
|
17
|
+
// for why each rule exists, and `tooltip.ts` for the rules themselves.
|
|
18
|
+
export { tooltipModel, tooltipModels, signatureRows, isNativeKey, isPlaceholder, PLACEHOLDER, NATIVE_SIGNATURES } from "./tooltip.js";
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/** One parameter of the execution, with everything a renderer needs about it. */
|
|
2
|
+
export interface TooltipSlot {
|
|
3
|
+
/** 1-based, so a renderer never has to decide whether to add one. */
|
|
4
|
+
index: number;
|
|
5
|
+
name: string;
|
|
6
|
+
/** the Pact type as DECLARED. Rendered on its own row -- rule 3. */
|
|
7
|
+
type: string;
|
|
8
|
+
/** the rendered Pact literal, or the `display` override where one exists. */
|
|
9
|
+
value: string;
|
|
10
|
+
/** the generic "example" ghost: an entity that does not exist on chain. Render it as such. */
|
|
11
|
+
isPlaceholder: boolean;
|
|
12
|
+
/** the output of a preflight read. There is no value and none may be invented. */
|
|
13
|
+
isPreflightFed: boolean;
|
|
14
|
+
/** false = never prefill into an input that can reach a signed transaction (guards). */
|
|
15
|
+
prefillable: boolean;
|
|
16
|
+
/** the registry's own note about this argument, when it has one. */
|
|
17
|
+
note?: string;
|
|
18
|
+
}
|
|
19
|
+
export interface TooltipPreview {
|
|
20
|
+
/** fully qualified INFO_ reader. */
|
|
21
|
+
name: string;
|
|
22
|
+
/** ITS parameter names, which are usually not the execution's. */
|
|
23
|
+
params: string[];
|
|
24
|
+
/** the rendered call, ready to `/local`. */
|
|
25
|
+
call: string;
|
|
26
|
+
}
|
|
27
|
+
export interface TooltipModel {
|
|
28
|
+
/** `native` renders with a gold perimeter and no cost section -- rule 4. */
|
|
29
|
+
kind: "ouronet" | "native";
|
|
30
|
+
/** fully qualified execution, as it would be typed in a transaction. */
|
|
31
|
+
exec: string;
|
|
32
|
+
/** EVERY execution parameter, in declared order -- rule 1. */
|
|
33
|
+
slots: TooltipSlot[];
|
|
34
|
+
/** absent for native calls: nothing on StoaChain's `coin` contract prices a call. */
|
|
35
|
+
preview?: TooltipPreview;
|
|
36
|
+
/**
|
|
37
|
+
* Whether a renderer should fire the preview read.
|
|
38
|
+
*
|
|
39
|
+
* False when any argument is a placeholder -- the read would refuse, and a refusal per hover is
|
|
40
|
+
* latency the user pays for a foregone conclusion. Rule 5.
|
|
41
|
+
*/
|
|
42
|
+
shouldRead: boolean;
|
|
43
|
+
/** things a renderer may want to surface. Never thrown; a tooltip must not take a page down. */
|
|
44
|
+
warnings: string[];
|
|
45
|
+
}
|
|
46
|
+
/** The registry's generic placeholder: an entity id for something that does not exist on chain. */
|
|
47
|
+
export declare const PLACEHOLDER = "\"example\"";
|
|
48
|
+
export declare const isPlaceholder: (rendered: string) => boolean;
|
|
49
|
+
/** A `coin.*` key is StoaChain's root-namespace contract -- outside this registry by design. */
|
|
50
|
+
export declare const isNativeKey: (key: string) => boolean;
|
|
51
|
+
/**
|
|
52
|
+
* STOA-native specs, which the generated registry does not and will not describe.
|
|
53
|
+
*
|
|
54
|
+
* They live HERE rather than in each consumer so that both applications render the same
|
|
55
|
+
* signatures. Transcribed from the deployed contract with its line numbers; `tooltip.test.ts`
|
|
56
|
+
* asserts internal consistency and the Pact repo's own test re-reads the source.
|
|
57
|
+
*
|
|
58
|
+
* source: 0_Stoa/coin-contract/coin-live.pact
|
|
59
|
+
*/
|
|
60
|
+
export declare const NATIVE_SIGNATURES: Readonly<Record<string, ReadonlyArray<[string, string]>>>;
|
|
61
|
+
/**
|
|
62
|
+
* Everything a renderer needs for one entrypoint.
|
|
63
|
+
*
|
|
64
|
+
* `values` overrides ghosts BY PARAMETER NAME. Never positionally: 410 of 423 previews declare a
|
|
65
|
+
* different list from their entrypoint, and positional merging is how an executor's account ends
|
|
66
|
+
* up in a token-id slot -- silently, because both are strings.
|
|
67
|
+
*
|
|
68
|
+
* Throws only on an unknown key, which is a programming error. Everything else degrades: an
|
|
69
|
+
* unresolvable argument becomes a visible placeholder rather than an exception, because a tooltip
|
|
70
|
+
* must never take down the page it annotates.
|
|
71
|
+
*/
|
|
72
|
+
export declare function tooltipModel(key: string, values?: Readonly<Record<string, string>>): TooltipModel;
|
|
73
|
+
/**
|
|
74
|
+
* Models for a button that fronts a CHOICE of executions, in the order the ZBOM presents them.
|
|
75
|
+
*
|
|
76
|
+
* Unknown keys are DROPPED rather than throwing: a button offering three executions should not
|
|
77
|
+
* lose its tooltip because one is stale. The absence is reported in `warnings` of the survivors'
|
|
78
|
+
* caller instead -- and if nothing survives, the caller renders no tooltip, which is the correct
|
|
79
|
+
* outcome and not a crash.
|
|
80
|
+
*/
|
|
81
|
+
export declare function tooltipModels(keys: readonly string[], values?: Readonly<Record<string, string>>): TooltipModel[];
|
|
82
|
+
/**
|
|
83
|
+
* The type row -- rule 3.
|
|
84
|
+
*
|
|
85
|
+
* Returned parallel to the names so a renderer can print two aligned lines rather than
|
|
86
|
+
* `name:type` pairs, which double the width of the widest line in the panel.
|
|
87
|
+
*/
|
|
88
|
+
export declare function signatureRows(m: TooltipModel): {
|
|
89
|
+
names: string[];
|
|
90
|
+
types: string[];
|
|
91
|
+
};
|
package/dist/tooltip.js
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE TOOLTIP CANON, as code.
|
|
3
|
+
*
|
|
4
|
+
* A pre-ZBOM tooltip answers one question: *what will this button actually execute, and what will
|
|
5
|
+
* it cost?* Two applications render it -- OuronetUI and the Codex -- and they had been converging
|
|
6
|
+
* on it independently, each rediscovering the same traps a few weeks apart. This module exists so
|
|
7
|
+
* the rules are a SHARED IMPLEMENTATION rather than a shared document, because a document drifts
|
|
8
|
+
* from the code it describes and nobody finds out until a user sees a wrong number.
|
|
9
|
+
*
|
|
10
|
+
* `TOOLTIP-CANON.md` beside this file explains WHY each rule exists. This file is the rule.
|
|
11
|
+
*
|
|
12
|
+
* ── THE FIVE RULES ────────────────────────────────────────────────────────────────────────────
|
|
13
|
+
*
|
|
14
|
+
* 1. SHOW EVERY EXECUTION PARAMETER. All of them, always, in declared order. A tooltip that lists
|
|
15
|
+
* five rows for a six-parameter function is not a summary, it is a wrong answer -- and it is
|
|
16
|
+
* how `DPTF|C_Transfer` came to display five arguments for a six-argument call.
|
|
17
|
+
*
|
|
18
|
+
* 2. THE ARGUMENTS BELONG TO THE EXECUTION, the cost belongs to the preview. 410 of 423
|
|
19
|
+
* entrypoints have a preview whose parameter list DIFFERS from their own, so the two cannot
|
|
20
|
+
* share one numbered list. Render the execution's; mention the preview's separately.
|
|
21
|
+
*
|
|
22
|
+
* 3. TYPES GO ON THEIR OWN ROW. `patron:string executor:string ...` doubles the width of the
|
|
23
|
+
* widest line in the panel. A parallel row under the names costs one line and reads better.
|
|
24
|
+
*
|
|
25
|
+
* 4. NATIVE IS NOT OURONET, and the difference is narrow. A `coin.*` call has no INFO_ preview
|
|
26
|
+
* and collects no IGNIS. Gas IS still sponsored -- claiming otherwise tells a user they are
|
|
27
|
+
* about to spend when they are not. Render native with a gold perimeter so the two are never
|
|
28
|
+
* confused, and say what is actually different rather than inventing a difference.
|
|
29
|
+
*
|
|
30
|
+
* 5. A GHOST IS NOT DATA. An unresolved argument must LOOK unresolved, and a tooltip whose
|
|
31
|
+
* arguments are placeholders must not fire the preview read at all: the answer is a foregone
|
|
32
|
+
* refusal and the latency is paid by the user for nothing.
|
|
33
|
+
*/
|
|
34
|
+
import { getEntrypoint, getPreview, tryGetEntrypoint, registry } from "./registry.js";
|
|
35
|
+
import { formatForType } from "./format.js";
|
|
36
|
+
/** The registry's generic placeholder: an entity id for something that does not exist on chain. */
|
|
37
|
+
export const PLACEHOLDER = '"example"';
|
|
38
|
+
export const isPlaceholder = (rendered) => rendered === PLACEHOLDER;
|
|
39
|
+
/** A `coin.*` key is StoaChain's root-namespace contract -- outside this registry by design. */
|
|
40
|
+
export const isNativeKey = (key) => key.startsWith("coin.");
|
|
41
|
+
/**
|
|
42
|
+
* STOA-native specs, which the generated registry does not and will not describe.
|
|
43
|
+
*
|
|
44
|
+
* They live HERE rather than in each consumer so that both applications render the same
|
|
45
|
+
* signatures. Transcribed from the deployed contract with its line numbers; `tooltip.test.ts`
|
|
46
|
+
* asserts internal consistency and the Pact repo's own test re-reads the source.
|
|
47
|
+
*
|
|
48
|
+
* source: 0_Stoa/coin-contract/coin-live.pact
|
|
49
|
+
*/
|
|
50
|
+
export const NATIVE_SIGNATURES = {
|
|
51
|
+
// [name, type] pairs, in declared order.
|
|
52
|
+
"coin.C_Transfer": [["sender", "string"], ["receiver", "string"], ["amount", "decimal"]], // :583
|
|
53
|
+
"coin.C_Transmit": [["sender", "string"], ["receiver", "string"], ["amount", "decimal"]], // :593
|
|
54
|
+
"coin.C_TransferAnew": [["sender", "string"], ["receiver", "string"], ["receiver-guard", "guard"], ["amount", "decimal"]], // :586
|
|
55
|
+
"coin.C_TransmitAnew": [["sender", "string"], ["receiver", "string"], ["receiver-guard", "guard"], ["amount", "decimal"]], // :599
|
|
56
|
+
"coin.C_UR|Transfer": [["sender", "string"], ["receiver", "string"], ["amount", "decimal"]], // :1075
|
|
57
|
+
"coin.C_UR|Transmit": [["sender", "string"], ["receiver", "string"], ["amount", "decimal"]], // :1085
|
|
58
|
+
"coin.C_URV|Stake": [["account", "string"], ["urstoa-amount", "decimal"]], // :1407
|
|
59
|
+
"coin.C_URV|Unstake": [["account", "string"], ["urstoa-amount", "decimal"]], // :1439
|
|
60
|
+
"coin.C_URV|Collect": [["account", "string"]], // :1467
|
|
61
|
+
};
|
|
62
|
+
/** Kadena-shaped, NOT an Ouronet glyph account -- a `coin` call takes k:/c:/u:/w:. */
|
|
63
|
+
const NATIVE_GHOST = {
|
|
64
|
+
string: '"k:1ac0d8b0a4f6e2c9d3b5a7e1f4c6089d2b3e5a7c9f1d3b5e7a9c1f3d5b7e9a1c"',
|
|
65
|
+
decimal: "1.0",
|
|
66
|
+
integer: "1",
|
|
67
|
+
bool: "false",
|
|
68
|
+
guard: '(read-keyset "ks")',
|
|
69
|
+
};
|
|
70
|
+
function nativeModel(key, values) {
|
|
71
|
+
// Callers reach this only through `tooltipModel`, which has already rejected an unknown key.
|
|
72
|
+
// Asserted rather than assumed: a non-null assertion here would make a future direct caller
|
|
73
|
+
// fail with a property access on undefined instead of a sentence naming the problem.
|
|
74
|
+
const sig = NATIVE_SIGNATURES[key];
|
|
75
|
+
if (!sig)
|
|
76
|
+
throw new Error(`nativeModel: no signature for "${key}"`);
|
|
77
|
+
return {
|
|
78
|
+
kind: "native",
|
|
79
|
+
exec: key,
|
|
80
|
+
slots: sig.map(([name, type], i) => ({
|
|
81
|
+
index: i + 1,
|
|
82
|
+
name,
|
|
83
|
+
type,
|
|
84
|
+
value: values[name] ?? NATIVE_GHOST[type] ?? PLACEHOLDER,
|
|
85
|
+
isPlaceholder: values[name] === undefined && NATIVE_GHOST[type] === undefined,
|
|
86
|
+
isPreflightFed: false,
|
|
87
|
+
// A guard is never prefillable, native or not. Same rule, same reason.
|
|
88
|
+
prefillable: type !== "guard",
|
|
89
|
+
})),
|
|
90
|
+
// No preview, deliberately. Rule 4: nothing on `coin` prices a call, so there is no cost to
|
|
91
|
+
// show and a renderer must say that rather than leaving an empty panel that reads as a
|
|
92
|
+
// failed load.
|
|
93
|
+
shouldRead: false,
|
|
94
|
+
warnings: [],
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Everything a renderer needs for one entrypoint.
|
|
99
|
+
*
|
|
100
|
+
* `values` overrides ghosts BY PARAMETER NAME. Never positionally: 410 of 423 previews declare a
|
|
101
|
+
* different list from their entrypoint, and positional merging is how an executor's account ends
|
|
102
|
+
* up in a token-id slot -- silently, because both are strings.
|
|
103
|
+
*
|
|
104
|
+
* Throws only on an unknown key, which is a programming error. Everything else degrades: an
|
|
105
|
+
* unresolvable argument becomes a visible placeholder rather than an exception, because a tooltip
|
|
106
|
+
* must never take down the page it annotates.
|
|
107
|
+
*/
|
|
108
|
+
export function tooltipModel(key, values = {}) {
|
|
109
|
+
if (isNativeKey(key)) {
|
|
110
|
+
if (!NATIVE_SIGNATURES[key])
|
|
111
|
+
throw new Error(`tooltipModel: unknown native key "${key}". ` +
|
|
112
|
+
`Known: ${Object.keys(NATIVE_SIGNATURES).join(", ")}`);
|
|
113
|
+
return nativeModel(key, values);
|
|
114
|
+
}
|
|
115
|
+
const ep = getEntrypoint(key); // throws, and names near matches, on a stale key
|
|
116
|
+
const warnings = [];
|
|
117
|
+
const use = ep.ghost.use ?? {};
|
|
118
|
+
// RULE 1: every parameter, in declared order. Not the preview's list, not a subset.
|
|
119
|
+
const slots = ep.params.map((p, i) => {
|
|
120
|
+
const supplied = values[p.name];
|
|
121
|
+
const raw = ep.ghost.args[p.name];
|
|
122
|
+
const u = use[p.name];
|
|
123
|
+
let value;
|
|
124
|
+
let isPlaceholder = false;
|
|
125
|
+
const isPreflightFed = raw === null;
|
|
126
|
+
if (supplied !== undefined) {
|
|
127
|
+
value = supplied;
|
|
128
|
+
}
|
|
129
|
+
else if (u?.display !== undefined) {
|
|
130
|
+
// The submittable form and the readable form differ -- a guard is `(read-keyset "ks")` to
|
|
131
|
+
// a reader and a data-key name to the transaction builder.
|
|
132
|
+
value = u.display;
|
|
133
|
+
}
|
|
134
|
+
else if (isPreflightFed) {
|
|
135
|
+
value = "<from preflight>";
|
|
136
|
+
}
|
|
137
|
+
else if (raw === undefined) {
|
|
138
|
+
value = PLACEHOLDER;
|
|
139
|
+
isPlaceholder = true;
|
|
140
|
+
warnings.push(`no ghost for ${p.name}:${p.type}`);
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
try {
|
|
144
|
+
value = formatForType(raw, p.type);
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
value = PLACEHOLDER;
|
|
148
|
+
isPlaceholder = true;
|
|
149
|
+
warnings.push(`ghost for ${p.name} does not fit ${p.type}`);
|
|
150
|
+
}
|
|
151
|
+
if (isPlaceholder || value === PLACEHOLDER)
|
|
152
|
+
isPlaceholder = true;
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
index: i + 1,
|
|
156
|
+
name: p.name,
|
|
157
|
+
type: p.type,
|
|
158
|
+
value,
|
|
159
|
+
isPlaceholder,
|
|
160
|
+
isPreflightFed,
|
|
161
|
+
prefillable: u?.zbom !== false,
|
|
162
|
+
note: ep.ghost.notes?.[p.name],
|
|
163
|
+
};
|
|
164
|
+
});
|
|
165
|
+
// RULE 2: the preview is priced with ITS OWN parameter list, which is usually not this one.
|
|
166
|
+
let preview;
|
|
167
|
+
const pvKey = ep.preview;
|
|
168
|
+
const pv = pvKey ? getPreview(pvKey) : undefined;
|
|
169
|
+
if (pvKey && pv) {
|
|
170
|
+
const args = pv.params.map((p) => {
|
|
171
|
+
const supplied = values[p.name];
|
|
172
|
+
if (supplied !== undefined)
|
|
173
|
+
return supplied;
|
|
174
|
+
const raw = ep.ghost.args[p.name];
|
|
175
|
+
if (raw === undefined || raw === null)
|
|
176
|
+
return PLACEHOLDER;
|
|
177
|
+
try {
|
|
178
|
+
return formatForType(raw, p.type);
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
return PLACEHOLDER;
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
preview = {
|
|
185
|
+
name: `${registry.namespace}.${pvKey}`,
|
|
186
|
+
params: pv.params.map((p) => p.name),
|
|
187
|
+
call: `(${registry.namespace}.${pvKey} ${args.join(" ")})`,
|
|
188
|
+
};
|
|
189
|
+
if (args.some(isPlaceholder))
|
|
190
|
+
warnings.push("preview has placeholder arguments; the read would refuse");
|
|
191
|
+
}
|
|
192
|
+
else if (pvKey) {
|
|
193
|
+
warnings.push(`preview ${pvKey} is named but not in the registry`);
|
|
194
|
+
}
|
|
195
|
+
return {
|
|
196
|
+
kind: "ouronet",
|
|
197
|
+
exec: `${registry.namespace}.${key}`,
|
|
198
|
+
slots,
|
|
199
|
+
preview,
|
|
200
|
+
// RULE 5: do not fire a read whose answer is a foregone refusal.
|
|
201
|
+
shouldRead: preview !== undefined && !preview.call.includes(PLACEHOLDER),
|
|
202
|
+
warnings,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Models for a button that fronts a CHOICE of executions, in the order the ZBOM presents them.
|
|
207
|
+
*
|
|
208
|
+
* Unknown keys are DROPPED rather than throwing: a button offering three executions should not
|
|
209
|
+
* lose its tooltip because one is stale. The absence is reported in `warnings` of the survivors'
|
|
210
|
+
* caller instead -- and if nothing survives, the caller renders no tooltip, which is the correct
|
|
211
|
+
* outcome and not a crash.
|
|
212
|
+
*/
|
|
213
|
+
export function tooltipModels(keys, values = {}) {
|
|
214
|
+
return keys
|
|
215
|
+
.filter((k) => isNativeKey(k) ? Boolean(NATIVE_SIGNATURES[k]) : Boolean(tryGetEntrypoint(k)))
|
|
216
|
+
.map((k) => tooltipModel(k, values));
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* The type row -- rule 3.
|
|
220
|
+
*
|
|
221
|
+
* Returned parallel to the names so a renderer can print two aligned lines rather than
|
|
222
|
+
* `name:type` pairs, which double the width of the widest line in the panel.
|
|
223
|
+
*/
|
|
224
|
+
export function signatureRows(m) {
|
|
225
|
+
return { names: m.slots.map((s) => s.name), types: m.slots.map((s) => s.type) };
|
|
226
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -104,11 +104,26 @@ export interface ExternalCaps {
|
|
|
104
104
|
attachTo: string;
|
|
105
105
|
note: string;
|
|
106
106
|
}
|
|
107
|
+
/** Where a single ghost value may be used. Absent field = permitted. */
|
|
108
|
+
export interface GhostUse {
|
|
109
|
+
/** false = MUST NOT be prefilled into an input that can reach a signed transaction. */
|
|
110
|
+
zbom?: boolean;
|
|
111
|
+
/** false = must not be rendered at all. */
|
|
112
|
+
tooltip?: boolean;
|
|
113
|
+
/** render this string instead of the value (the submittable and readable forms differ). */
|
|
114
|
+
display?: string;
|
|
115
|
+
}
|
|
107
116
|
export interface Ghost {
|
|
108
117
|
/** example arguments by parameter name. `null` means "obtain from the preflight". */
|
|
109
118
|
args: Record<string, unknown>;
|
|
110
119
|
source: string;
|
|
111
120
|
notes?: Record<string, string>;
|
|
121
|
+
/**
|
|
122
|
+
* Per-parameter surface restrictions. Present ONLY for parameters that have one -- 6 of 423
|
|
123
|
+
* entrypoints today, all guard-taking. An absent parameter is unrestricted.
|
|
124
|
+
*/
|
|
125
|
+
use?: Record<string, GhostUse>;
|
|
126
|
+
useNote?: string;
|
|
112
127
|
unresolved?: string[];
|
|
113
128
|
unresolvedNote?: string;
|
|
114
129
|
warning: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ouronet/talos-registry",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "The Ouronet callable surface, generated from the deployed contracts. Supply values; never type a function name.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
"files": [
|
|
19
19
|
"dist",
|
|
20
20
|
"README.md",
|
|
21
|
-
"CHANGELOG.md"
|
|
21
|
+
"CHANGELOG.md",
|
|
22
|
+
"TOOLTIP-CANON.md"
|
|
22
23
|
],
|
|
23
24
|
"scripts": {
|
|
24
25
|
"sync": "node scripts/sync-registry.mjs",
|