@cardano-slips/flow 0.1.0 → 0.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.
Files changed (113) hide show
  1. package/README.md +53 -9
  2. package/dist/address.d.ts +12 -0
  3. package/dist/address.d.ts.map +1 -1
  4. package/dist/address.js +43 -0
  5. package/dist/address.js.map +1 -1
  6. package/dist/balance-error.d.ts +48 -0
  7. package/dist/balance-error.d.ts.map +1 -0
  8. package/dist/balance-error.js +45 -0
  9. package/dist/balance-error.js.map +1 -0
  10. package/dist/balance.d.ts +54 -0
  11. package/dist/balance.d.ts.map +1 -0
  12. package/dist/balance.js +175 -0
  13. package/dist/balance.js.map +1 -0
  14. package/dist/card.css +492 -0
  15. package/dist/card.d.ts +41 -0
  16. package/dist/card.d.ts.map +1 -0
  17. package/dist/card.js +69 -0
  18. package/dist/card.js.map +1 -0
  19. package/dist/cip30.d.ts +10 -0
  20. package/dist/cip30.d.ts.map +1 -1
  21. package/dist/cip30.js +16 -0
  22. package/dist/cip30.js.map +1 -1
  23. package/dist/complete-error.d.ts +57 -0
  24. package/dist/complete-error.d.ts.map +1 -0
  25. package/dist/complete-error.js +52 -0
  26. package/dist/complete-error.js.map +1 -0
  27. package/dist/complete.d.ts +77 -0
  28. package/dist/complete.d.ts.map +1 -0
  29. package/dist/complete.js +182 -0
  30. package/dist/complete.js.map +1 -0
  31. package/dist/derived.d.ts +8 -0
  32. package/dist/derived.d.ts.map +1 -0
  33. package/dist/derived.js +2 -0
  34. package/dist/derived.js.map +1 -0
  35. package/dist/effects-panel.d.ts +37 -0
  36. package/dist/effects-panel.d.ts.map +1 -0
  37. package/dist/effects-panel.js +64 -0
  38. package/dist/effects-panel.js.map +1 -0
  39. package/dist/effects.css +629 -0
  40. package/dist/exchange.d.ts +54 -0
  41. package/dist/exchange.d.ts.map +1 -0
  42. package/dist/exchange.js +136 -0
  43. package/dist/exchange.js.map +1 -0
  44. package/dist/explain.d.ts +39 -0
  45. package/dist/explain.d.ts.map +1 -0
  46. package/dist/explain.js +172 -0
  47. package/dist/explain.js.map +1 -0
  48. package/dist/index.d.ts +34 -3
  49. package/dist/index.d.ts.map +1 -1
  50. package/dist/index.js +19 -2
  51. package/dist/index.js.map +1 -1
  52. package/dist/ledger.d.ts +74 -0
  53. package/dist/ledger.d.ts.map +1 -0
  54. package/dist/ledger.js +402 -0
  55. package/dist/ledger.js.map +1 -0
  56. package/dist/outcome-panel.d.ts +63 -0
  57. package/dist/outcome-panel.d.ts.map +1 -0
  58. package/dist/outcome-panel.js +40 -0
  59. package/dist/outcome-panel.js.map +1 -0
  60. package/dist/parameter-form.d.ts +26 -0
  61. package/dist/parameter-form.d.ts.map +1 -0
  62. package/dist/parameter-form.js +42 -0
  63. package/dist/parameter-form.js.map +1 -0
  64. package/dist/resolve.d.ts +11 -0
  65. package/dist/resolve.d.ts.map +1 -0
  66. package/dist/resolve.js +21 -0
  67. package/dist/resolve.js.map +1 -0
  68. package/dist/sign-error.d.ts +62 -0
  69. package/dist/sign-error.d.ts.map +1 -0
  70. package/dist/sign-error.js +42 -0
  71. package/dist/sign-error.js.map +1 -0
  72. package/dist/sign.d.ts +38 -0
  73. package/dist/sign.d.ts.map +1 -0
  74. package/dist/sign.js +79 -0
  75. package/dist/sign.js.map +1 -0
  76. package/dist/slip-card.d.ts +55 -0
  77. package/dist/slip-card.d.ts.map +1 -0
  78. package/dist/slip-card.js +89 -0
  79. package/dist/slip-card.js.map +1 -0
  80. package/dist/tokens.css +97 -123
  81. package/dist/utxo.d.ts +16 -0
  82. package/dist/utxo.d.ts.map +1 -0
  83. package/dist/utxo.js +51 -0
  84. package/dist/utxo.js.map +1 -0
  85. package/dist/witness.d.ts +23 -0
  86. package/dist/witness.d.ts.map +1 -0
  87. package/dist/witness.js +101 -0
  88. package/dist/witness.js.map +1 -0
  89. package/package.json +13 -6
  90. package/src/address.ts +53 -0
  91. package/src/balance-error.ts +77 -0
  92. package/src/balance.ts +285 -0
  93. package/src/card.css +492 -0
  94. package/src/card.ts +86 -0
  95. package/src/cip30.ts +19 -0
  96. package/src/complete-error.ts +95 -0
  97. package/src/complete.ts +307 -0
  98. package/src/derived.ts +8 -0
  99. package/src/effects-panel.tsx +386 -0
  100. package/src/effects.css +629 -0
  101. package/src/exchange.ts +212 -0
  102. package/src/explain.ts +211 -0
  103. package/src/index.ts +64 -3
  104. package/src/ledger.ts +503 -0
  105. package/src/outcome-panel.tsx +263 -0
  106. package/src/parameter-form.tsx +138 -0
  107. package/src/resolve.ts +40 -0
  108. package/src/sign-error.ts +89 -0
  109. package/src/sign.ts +118 -0
  110. package/src/slip-card.tsx +244 -0
  111. package/src/tokens.css +97 -123
  112. package/src/utxo.ts +70 -0
  113. package/src/witness.ts +132 -0
@@ -0,0 +1,244 @@
1
+ "use client"
2
+
3
+ /**
4
+ * The rendered face of a Slip link — `2 · Action card` on the design sheet.
5
+ * It renders what the endpoint declared and collects what the endpoint asked
6
+ * for; it makes no claim about the transaction, which is the effects panel's
7
+ * job and is checked against the bytes rather than against this card.
8
+ *
9
+ * The Slip is assumed to have been decoded and passed `checkTemplates`: a
10
+ * response with a placeholder naming no parameter is one a client never renders.
11
+ */
12
+ import { checkValues, fillHref, type LinkedAction, type ParameterValues, type Slip } from "@cardano-slips/core"
13
+ import { Either } from "effect"
14
+ import { useState } from "react"
15
+
16
+ import { actionLabel, actionsOf, blockedLabel, closureOf, monogramOf, originOf, valuesFor } from "./card.js"
17
+ import { type FieldError, ParameterForm } from "./parameter-form.js"
18
+
19
+ /** What the person chose, ready to `POST`. */
20
+ export type SlipSubmission = {
21
+ readonly action: LinkedAction
22
+ /** Only what this action asked for. A caller is handed no answer it did not request. */
23
+ readonly values: ParameterValues
24
+ /** Absolute, with every placeholder percent-encoded, so no typed value can move the request. */
25
+ readonly href: string
26
+ }
27
+
28
+ export type SlipCardProps = {
29
+ readonly slip: Slip
30
+ /**
31
+ * Where the metadata came from. Every `href` resolves against it and its host
32
+ * is the publisher the card names, so the card cannot be rendered without it.
33
+ */
34
+ readonly discoveryUrl: string
35
+ readonly onSubmit?: (submission: SlipSubmission) => void
36
+ /**
37
+ * What the endpoint refused, by parameter name. The only failure that lands
38
+ * on a field instead of replacing the card, because there is something to fix.
39
+ * It is the caller's to clear, since only the caller knows which answer the
40
+ * endpoint was refusing.
41
+ */
42
+ readonly rejected?: Readonly<Record<string, FieldError>>
43
+ /** True while a request is in flight: every button stops taking presses. */
44
+ readonly busy?: boolean
45
+ /** Prefixes generated ids, so two cards on one page do not collide. */
46
+ readonly idPrefix?: string
47
+ }
48
+
49
+ /** The line that is the whole point of the card, and the reason it is worth pressing. */
50
+ const promise = "You'll see the exact effects, checked against this card, before you sign."
51
+
52
+ /** Nothing to choose between means the one action is already the open one. */
53
+ const opened = (count: number): number | undefined => (count === 1 ? 0 : undefined)
54
+
55
+ export const SlipCard = ({
56
+ slip,
57
+ discoveryUrl,
58
+ onSubmit,
59
+ rejected,
60
+ busy = false,
61
+ idPrefix = "slip"
62
+ }: SlipCardProps): React.JSX.Element => {
63
+ const actions = actionsOf(slip, discoveryUrl)
64
+
65
+ const [values, setValues] = useState<ParameterValues>({})
66
+ // Errors stay quiet until a send is attempted: a card that opens red is
67
+ // scolding someone for not having typed yet.
68
+ const [attempted, setAttempted] = useState(false)
69
+ const [open, setOpen] = useState<number | undefined>(opened(actions.length))
70
+ const [failedIcon, setFailedIcon] = useState<string | undefined>(undefined)
71
+
72
+ // Resetting during render is React's own answer to a prop moving out from
73
+ // under the state derived from it. A second Slip must not open pre-filled
74
+ // with the first one's answers, and remembering to pass a `key` is not a
75
+ // thing to leave to whoever embeds this.
76
+ const [shown, setShown] = useState(discoveryUrl)
77
+ if (shown !== discoveryUrl) {
78
+ setShown(discoveryUrl)
79
+ setValues({})
80
+ setAttempted(false)
81
+ setOpen(opened(actions.length))
82
+ setFailedIcon(undefined)
83
+ }
84
+
85
+ const closed = slip.disabled === true
86
+ const parameters = (open === undefined ? undefined : actions[open]?.parameters) ?? []
87
+ const issues = checkValues(parameters, values)
88
+ const blocked = attempted && issues.length > 0
89
+
90
+ // `??=` would be wrong here: a parameter may be called `constructor`, and a
91
+ // plain object already answers to that with something that is not nullish.
92
+ const errors: Record<string, FieldError> = {}
93
+ const note = (name: string, error: FieldError): void => {
94
+ if (!Object.hasOwn(errors, name)) errors[name] = error
95
+ }
96
+ if (attempted) for (const issue of issues) note(issue.name, { message: issue.message })
97
+ for (const [name, error] of Object.entries(rejected ?? {})) note(name, error)
98
+
99
+ const press = (index: number): void => {
100
+ const action = actions[index]
101
+
102
+ // The spec: an action without parameters is submitted as soon as it is
103
+ // chosen. One that has them opens its form instead of sending blind.
104
+ if (action.parameters !== undefined && index !== open) {
105
+ setOpen(index)
106
+ setAttempted(false)
107
+ return
108
+ }
109
+
110
+ const target = fillHref(action, values, discoveryUrl)
111
+ if (Either.isLeft(target)) {
112
+ setAttempted(true)
113
+ return
114
+ }
115
+ onSubmit?.({ action, values: valuesFor(action, values), href: target.right })
116
+ }
117
+
118
+ const monogram = monogramOf(discoveryUrl)
119
+ const origin = originOf(discoveryUrl)
120
+
121
+ return (
122
+ <div className="slip-root slip-card" data-closed={closed ? "" : undefined} data-busy={busy ? "" : undefined}>
123
+ <div className="slip-card__head">
124
+ <div className="slip-card__icon">
125
+ {failedIcon !== slip.icon ? (
126
+ <img className="slip-card__image" src={slip.icon} alt="" onError={() => setFailedIcon(slip.icon)} />
127
+ ) : monogram === undefined ? (
128
+ <span className="slip-card__pattern" />
129
+ ) : (
130
+ <span className="slip-card__monogram">{monogram}</span>
131
+ )}
132
+ </div>
133
+ <div className="slip-card__identity">
134
+ <h2 className="slip-card__title">{slip.title}</h2>
135
+ {origin === undefined ? undefined : <span className="slip-card__origin">{origin}</span>}
136
+ </div>
137
+ </div>
138
+
139
+ {/* Plain text by the spec: a description carries no markup, so none is parsed. */}
140
+ <p className="slip-card__description">{slip.description}</p>
141
+
142
+ {parameters.length === 0 ? undefined : (
143
+ <ParameterForm
144
+ parameters={parameters}
145
+ values={values}
146
+ errors={errors}
147
+ idPrefix={idPrefix}
148
+ onChange={(name, value) => setValues((held) => ({ ...held, [name]: value }))}
149
+ />
150
+ )}
151
+
152
+ <div className="slip-card__actions">
153
+ {actions.map((action, index) => {
154
+ const closure = closureOf(slip, action)
155
+ const stopped = index === (open ?? 0) && blocked
156
+ return (
157
+ <button
158
+ key={index}
159
+ type="button"
160
+ className="slip-card__action"
161
+ data-primary={index === (open ?? 0) ? "" : undefined}
162
+ disabled={busy || closure !== undefined || stopped}
163
+ onClick={() => press(index)}
164
+ >
165
+ {stopped ? blockedLabel(issues) : actionLabel(slip, action, values)}
166
+ </button>
167
+ )
168
+ })}
169
+ </div>
170
+
171
+ {/*
172
+ A closed Slip states its reason once, however many actions it carries;
173
+ an open one states the reason of each option that is closed on its own.
174
+ Neither hides the action: a shared link is seen by many people at once.
175
+ */}
176
+ {closed ? (
177
+ <p className="slip-card__reason">{slip.reason?.message}</p>
178
+ ) : (
179
+ actions.map((action, index) =>
180
+ action.disabled === true && action.reason !== undefined ? (
181
+ <p className="slip-card__reason" key={index}>
182
+ {action.reason.message}
183
+ </p>
184
+ ) : undefined
185
+ )
186
+ )}
187
+
188
+ {closed ? undefined : <p className="slip-card__note">{promise}</p>}
189
+ </div>
190
+ )
191
+ }
192
+
193
+ /** The same box and the same rhythm while the metadata is in flight, so nothing shifts when it lands. */
194
+ export const SlipCardSkeleton = (): React.JSX.Element => (
195
+ <div className="slip-root slip-card slip-card--loading" role="status" aria-busy="true">
196
+ <span className="slip-hidden">Loading this Slip</span>
197
+ <div className="slip-card__head">
198
+ <span className="slip-skeleton slip-skeleton--icon" />
199
+ <div className="slip-card__identity">
200
+ <span className="slip-skeleton slip-skeleton--title" />
201
+ <span className="slip-skeleton slip-skeleton--origin" />
202
+ </div>
203
+ </div>
204
+ <div className="slip-skeleton__lines">
205
+ <span className="slip-skeleton slip-skeleton--line" />
206
+ <span className="slip-skeleton slip-skeleton--line slip-skeleton--short" />
207
+ </div>
208
+ <span className="slip-skeleton slip-skeleton--action" />
209
+ </div>
210
+ )
211
+
212
+ export type SlipCardErrorProps = {
213
+ /** The endpoint that did not answer, in full: the person is being asked to judge it. */
214
+ readonly url: string
215
+ readonly message: string
216
+ readonly onRetry?: () => void
217
+ }
218
+
219
+ /**
220
+ * The endpoint did not answer, or answered with something that is not a Slip.
221
+ * This replaces the card rather than landing on a field, because there is
222
+ * nothing here for the person to fix.
223
+ */
224
+ export const SlipCardError = ({ url, message, onRetry }: SlipCardErrorProps): React.JSX.Element => (
225
+ <div className="slip-root slip-card slip-card--failed" role="alert">
226
+ <div className="slip-card__head">
227
+ <span className="slip-card__alarm" aria-hidden="true">
228
+ !
229
+ </span>
230
+ <div className="slip-card__identity">
231
+ <h2 className="slip-card__title">This Slip didn't load</h2>
232
+ <span className="slip-card__origin">{url}</span>
233
+ </div>
234
+ </div>
235
+ <p className="slip-card__description">{message}</p>
236
+ {onRetry === undefined ? undefined : (
237
+ <div className="slip-card__actions">
238
+ <button type="button" className="slip-card__action" onClick={onRetry}>
239
+ Try again
240
+ </button>
241
+ </div>
242
+ )}
243
+ </div>
244
+ )
package/src/tokens.css CHANGED
@@ -2,114 +2,145 @@
2
2
  * Every colour, type role, step of spacing and radius the client uses, taken
3
3
  * from `1 · Tokens` on the design sheet. This is the only file in the package
4
4
  * that writes a colour — `test/no-hard-coded-colours.test.ts` fails if another
5
- * one does, and `test/tokens.test.ts` re-runs the sheet's contrast audit
6
- * against these values.
5
+ * one does, and `test/tokens.test.ts` re-runs the contrast audit against these
6
+ * values rather than trusting the numbers printed on the sheet.
7
7
  *
8
8
  * Scoped to a class, never `:root`: this ships into pages we do not own, and a
9
9
  * package that writes tokens onto the document redefines whatever the host
10
10
  * already called `--ink`. Put `slip-root` on the element that wraps the Slip.
11
11
  *
12
12
  * There is no reset here and no rule that selects an element. Nothing sets a
13
- * font-family on anything — a component reads `var(--font-text)` — so a host
13
+ * font-family on anything — a component reads a `--type-*` role — so a host
14
14
  * page's own stack stays where it is until something of ours asks for a token.
15
15
  *
16
- * Three families of surface, and the difference is the point:
16
+ * One theme. The sheet collapsed an earlier light/dark pair into a single
17
+ * ground that leans light, so there is no `data-theme` and no role that means
18
+ * two things. What carries the weight instead is that the three surface
19
+ * families are genuinely different surfaces:
20
+ *
17
21
  * page / card the ordinary ground, and what lifts off it
18
- * vault-* the transaction preview, which separates itself by surface
19
- * and border rather than by being dark
20
- * dark-* code blocks and the Open Graph card, dark in both themes
22
+ * vault-* the transaction preview, on slate — the one grave surface,
23
+ * and the reason the rest can stay bright
24
+ * dark-* code blocks and the Open Graph card, fixed wherever they
25
+ * appear because neither sits on a ground we control
21
26
  */
22
27
 
23
28
  .slip-root {
24
29
  color-scheme: light;
25
30
 
26
31
  /* Surfaces. Depth is a border, never a shadow: there is no shadow token. */
27
- --page: #f6f8fe; /* never pure white */
28
- --card: #ffffff;
29
- --subtle: rgba(114, 142, 243, 0.08); /* hover, chips */
30
- --skeleton: rgba(114, 142, 243, 0.14);
31
- --border: rgba(18, 23, 33, 0.12);
32
- --divider: rgba(18, 23, 33, 0.1);
32
+ --page: #ebeef8; /* never pure white */
33
+ --card: #fcfdff;
34
+ --subtle: rgba(90, 110, 205, 0.09); /* hover, chips */
35
+ --skeleton: rgba(90, 110, 205, 0.14);
36
+ --border: rgba(20, 26, 40, 0.13);
37
+ --divider: rgba(20, 26, 40, 0.1);
33
38
 
34
39
  /* Ink */
35
- --ink: #121721;
36
- --muted: #5c6a9c;
40
+ --ink: #141a26;
41
+ --muted: #56628a;
37
42
 
38
43
  /*
39
- * The split rule: --accent-fill reaches 3.05:1 on the card and is a fill —
40
- * it never carries text below 24px. An accent-coloured word uses
41
- * --accent-text. It stays a fill in the dark theme too, where it would
42
- * otherwise clear AA, because the rule is about what the colour is for.
44
+ * The split rule: --accent-fill is a fill and never carries text below 24px.
45
+ * An accent-coloured word uses --accent-text. It clears AA on the vault and
46
+ * is still held to fills there, because the rule is about what the colour is
47
+ * for and not about the ratio it happens to reach.
43
48
  */
44
49
  --accent-fill: #728ef3;
45
- --accent-text: #2f4bc4; /* links */
46
- --accent-action: #4361e8; /* button background */
47
- --accent-hover: #2f4bc4;
50
+ --accent-text: #123cd3; /* links */
51
+ --accent-action: #123cd3; /* the outline and the label of a button */
52
+ --accent-hover: #0e2fa6;
48
53
  --accent-deep: #0c288d; /* headings */
49
- --on-accent: #ffffff; /* anything drawn on --accent-action */
50
- --focus: #4361e8;
54
+ --on-accent: #ffffff; /* anything drawn on a filled accent */
55
+ --focus: #123cd3;
56
+
57
+ /* The accent lightened for slate, where #123cd3 disappears into the ground. */
58
+ --accent-on-slate: #8fa9ff;
51
59
 
52
60
  /* Semantic, and rare. A page that colours everything says nothing. */
53
- --pos: #1f7a5c; /* positive, refundable */
54
- --warn: #8c5a12;
55
- --bad: #b3261e;
56
- --bad-tint: rgba(179, 38, 30, 0.06);
57
- --bad-line: rgba(179, 38, 30, 0.24);
58
- --warn-tint: rgba(233, 180, 76, 0.16);
59
- --warn-line: rgba(233, 180, 76, 0.5);
61
+ --pos: #065708; /* value arriving */
62
+ --warn: #7a5200; /* held, taken, caution */
63
+ --bad: #d20a19; /* problem, blocked */
64
+ --bad-tint: rgba(210, 10, 25, 0.06);
65
+ --bad-line: rgba(210, 10, 25, 0.24);
66
+ --warn-tint: rgba(254, 206, 82, 0.22);
67
+ --warn-line: rgba(254, 206, 82, 0.85);
68
+ --warn-fill: #fece52; /* the warning as a fill, never as a word on a light ground */
69
+ --on-warn: #3a2600; /* words on the warning fill: the testnet bar */
60
70
 
61
71
  /* The alternating rows of the effects list, on and off. */
62
- --stripe-a: rgba(114, 142, 243, 0.16);
63
- --stripe-b: rgba(114, 142, 243, 0.05);
64
- --stripe-off-a: rgba(92, 106, 156, 0.12);
65
- --stripe-off-b: rgba(92, 106, 156, 0.04);
72
+ --stripe-a: rgba(90, 110, 205, 0.16);
73
+ --stripe-b: rgba(90, 110, 205, 0.05);
74
+ --stripe-off-a: rgba(86, 98, 138, 0.12);
75
+ --stripe-off-b: rgba(86, 98, 138, 0.04);
66
76
 
67
77
  /*
68
78
  * The transaction preview. Its own family because it is the surface that
69
- * judges the publisher, and `10 · Chrome rules` fixes it whoever hosts the
70
- * page. It follows the theme like everything else.
79
+ * judges the publisher, and `13 · Chrome rules` fixes it whoever hosts the
80
+ * page. Slate rather than black: it has to read as a distinct surface, not
81
+ * as a hole in the page.
71
82
  */
72
- --vault: #ffffff;
73
- --vault-ink: #121721;
74
- --vault-muted: #5c6a9c;
75
- --vault-surface: rgba(114, 142, 243, 0.08);
76
- --vault-border: rgba(18, 23, 33, 0.12);
77
- --vault-chip: rgba(114, 142, 243, 0.16);
78
- --vault-rule: rgba(114, 142, 243, 0.8);
79
- --vault-pos: #1f7a5c;
80
- --vault-warn: #8c5a12;
81
- --vault-bad: #b3261e;
82
- --vault-bad-tint: rgba(179, 38, 30, 0.06);
83
- --vault-bad-line: rgba(179, 38, 30, 0.32);
84
- --vault-bad-chip: rgba(179, 38, 30, 0.1);
85
- --vault-warn-tint: rgba(233, 180, 76, 0.14);
86
- --vault-warn-line: rgba(233, 180, 76, 0.55);
83
+ --vault: #222b3d;
84
+ --vault-ink: #f5f7ff;
85
+ --vault-muted: #c3cdec;
86
+ --vault-surface: rgba(255, 255, 255, 0.09);
87
+ --vault-border: rgba(255, 255, 255, 0.16);
88
+ --vault-chip: rgba(168, 188, 255, 0.22);
89
+ --vault-rule: rgba(168, 188, 255, 0.7);
90
+ --vault-pos: #6fcb72;
91
+ --vault-warn: #fece52;
92
+ /*
93
+ * #ff5247 on the sheet, lightened here: that value measures 4.42:1 on the
94
+ * vault and the sheet's own row calls it a pass. This is the colour the
95
+ * mismatch block is written in, so it clears AA or it does not ship.
96
+ */
97
+ --vault-bad: #ff5a50;
98
+ --vault-bad-tint: rgba(210, 10, 25, 0.16);
99
+ --vault-bad-line: rgba(255, 82, 71, 0.55);
100
+ --vault-bad-chip: rgba(210, 10, 25, 0.28);
101
+ --vault-warn-tint: rgba(254, 206, 82, 0.14);
102
+ --vault-warn-line: rgba(254, 206, 82, 0.55);
87
103
 
88
104
  /*
89
- * Dark in both themes, and nowhere else: code blocks in the docs and the
90
- * Open Graph card. Deliberately absent from the dark theme below — a value
91
- * that is fixed cannot also follow.
105
+ * Code blocks in the docs and the Open Graph card. Both land on a ground we
106
+ * do not control — a reader's docs theme, someone else's timeline — so they
107
+ * carry their own and do not follow anything.
92
108
  */
93
109
  --dark-bg: #121721;
94
110
  --dark-surface: rgba(255, 255, 255, 0.08);
95
111
  --dark-text: #ffffff;
96
112
  --dark-muted: #ccd6ff;
97
113
 
98
- /* Type. Each stack ends in a generic family, so a missing webfont degrades. */
99
- --font-display: Poppins, system-ui, sans-serif;
100
- --font-text: Figtree, system-ui, sans-serif;
101
- --font-mono: "IBM Plex Mono", ui-monospace, monospace;
114
+ /*
115
+ * One family, for everything. The sheet's earlier display/text/mono trio is
116
+ * gone: weight and size separate a heading from a label perfectly well, and
117
+ * three names for one face is three chances to drift apart. The stack ends in
118
+ * a generic family, so a missing webfont degrades rather than disappears.
119
+ */
120
+ --font: Poppins, system-ui, sans-serif;
102
121
 
103
122
  /* Whole roles, so half a role cannot be applied without the other half. */
104
- --type-h1: 700 44px/48px var(--font-display);
123
+ --type-h1: 700 44px/48px var(--font);
105
124
  --type-h1-tracking: -0.01em;
106
- --type-card-title: 700 18px/24px var(--font-display);
107
- --type-numeral: 700 26px/32px var(--font-display); /* a figure is always Poppins 700 */
108
- --type-body: 500 16px/24px var(--font-text);
109
- --type-button: 500 14px/20px var(--font-text);
125
+ --type-card-title: 700 18px/24px var(--font);
126
+ --type-numeral: 700 26px/32px var(--font); /* a figure is always 700 */
127
+ --type-body: 400 16px/24px var(--font);
128
+ --type-button: 500 14px/20px var(--font);
110
129
  --type-button-tracking: 0.1px;
111
- --type-label: 500 13px var(--font-text);
112
- --type-mono: 400 12px/20px var(--font-mono); /* addresses, hashes, the tx block */
130
+ --type-label: 500 13px var(--font);
131
+ /* Addresses, hashes, error codes, the tx block — text meant for a machine. */
132
+ --type-technical: 400 12px/20px var(--font);
133
+
134
+ /* The transaction preview's own scale, from `4 · Anatomy` and `6 · Preview states`. */
135
+ --type-headline: 700 32px/38px var(--font); /* the one figure the person is deciding on */
136
+ --type-preview-title: 700 20px/26px var(--font);
137
+ --type-amount: 700 20px/26px var(--font); /* a row's figure, one size so the column scans */
138
+ --type-countdown: 700 16px/20px var(--font);
139
+ --type-row: 400 14px/20px var(--font);
140
+ --type-note: 400 13px/19px var(--font);
141
+ --type-kicker: 400 11px/16px var(--font);
142
+ --type-kicker-tracking: 0.12em;
143
+ --type-chip: 600 11px/20px var(--font);
113
144
 
114
145
  /* Spacing, on a 4px base. */
115
146
  --space-4: 4px;
@@ -125,60 +156,3 @@
125
156
  --radius-control: 8px;
126
157
  --radius-card: 12px;
127
158
  }
128
-
129
- /*
130
- * The dark theme rebinds the roles rather than adding a second vocabulary. A
131
- * component writes var(--ink) once and is correct on both grounds; a component
132
- * choosing between a light token and a dark one is a component deciding the
133
- * rule, and two of them will decide it differently.
134
- *
135
- * Whether dark follows the operating system is not settled by the sheet, so it
136
- * is not decided here: the attribute is the only way in.
137
- */
138
- .slip-root[data-theme="dark"] {
139
- color-scheme: dark;
140
-
141
- --page: #121721; /* the brand dark */
142
- --card: #1b2231; /* lifted off the page */
143
- --subtle: rgba(114, 142, 243, 0.16);
144
- --skeleton: rgba(114, 142, 243, 0.18);
145
- --border: rgba(255, 255, 255, 0.14);
146
- --divider: rgba(255, 255, 255, 0.1);
147
-
148
- --ink: #ffffff;
149
- --muted: #ccd6ff;
150
-
151
- --accent-text: #a8bcff;
152
- --accent-hover: #5c79ee;
153
- --accent-deep: #a8bcff;
154
- --focus: #a8bcff;
155
-
156
- --pos: #5fd1a6;
157
- --warn: #e9b44c;
158
- --bad: #ffa39b;
159
- --bad-tint: rgba(255, 163, 155, 0.1);
160
- --bad-line: rgba(255, 163, 155, 0.4);
161
- --warn-tint: rgba(233, 180, 76, 0.14);
162
- --warn-line: rgba(233, 180, 76, 0.45);
163
-
164
- --stripe-a: rgba(114, 142, 243, 0.26);
165
- --stripe-b: rgba(114, 142, 243, 0.1);
166
- --stripe-off-a: rgba(204, 214, 255, 0.16);
167
- --stripe-off-b: rgba(204, 214, 255, 0.05);
168
-
169
- --vault: #080b12; /* still the gravest surface */
170
- --vault-ink: #ffffff;
171
- --vault-muted: #ccd6ff;
172
- --vault-surface: rgba(255, 255, 255, 0.08);
173
- --vault-border: rgba(255, 255, 255, 0.14);
174
- --vault-chip: rgba(114, 142, 243, 0.24);
175
- --vault-rule: rgba(114, 142, 243, 0.5);
176
- --vault-pos: #5fd1a6;
177
- --vault-warn: #e9b44c;
178
- --vault-bad: #ffa39b;
179
- --vault-bad-tint: rgba(255, 163, 155, 0.08);
180
- --vault-bad-line: rgba(255, 163, 155, 0.45);
181
- --vault-bad-chip: rgba(255, 163, 155, 0.2);
182
- --vault-warn-tint: rgba(233, 180, 76, 0.08);
183
- --vault-warn-line: rgba(233, 180, 76, 0.45);
184
- }
package/src/utxo.ts ADDED
@@ -0,0 +1,70 @@
1
+ /**
2
+ * CIP-30 hands unspent outputs back as CBOR hex; evolution-sdk's builder takes
3
+ * them typed. The SDK has this conversion — `cip30UtxoFromCBORHex` — but only
4
+ * behind an export path its manifest blocks, so it is the sixth gap of ADR-0004
5
+ * and belongs upstream rather than here.
6
+ */
7
+ import {
8
+ Address,
9
+ AddressEras,
10
+ Assets,
11
+ CBOR,
12
+ Schema,
13
+ Script,
14
+ TransactionInput,
15
+ TransactionOutput,
16
+ UTxO
17
+ } from "@evolution-sdk/evolution"
18
+ import { Either } from "effect"
19
+
20
+ import type { CborHex } from "./cip30.js"
21
+
22
+ /** The output's address is era-tagged and a UTxO's is not; the bytes are the same. */
23
+ const toAddressStructure = (tagged: AddressEras.AddressEras): Address.Address =>
24
+ Schema.decodeSync(Address.FromBytes)(Schema.encodeSync(AddressEras.FromBytes)(tagged))
25
+
26
+ const readOne = (hex: CborHex): UTxO.UTxO => {
27
+ const decoded = CBOR.fromCBORHex(hex)
28
+ if (!Array.isArray(decoded) || decoded.length !== 2) {
29
+ throw new Error("a CIP-30 unspent output is an input and an output, and this is neither")
30
+ }
31
+ const input = TransactionInput.fromCBORBytes(CBOR.toCBORBytes(decoded[0]))
32
+ const output = TransactionOutput.fromCBORBytes(CBOR.toCBORBytes(decoded[1]))
33
+ const amount = output.amount
34
+
35
+ return new UTxO.UTxO({
36
+ transactionId: input.transactionId,
37
+ index: input.index,
38
+ address: toAddressStructure(output.address),
39
+ assets:
40
+ amount._tag === "WithAssets"
41
+ ? Assets.withMultiAsset(amount.coin, amount.assets)
42
+ : Assets.fromLovelace(amount.coin),
43
+ // A Babbage output carries a datum option and a script; a Shelley one carries
44
+ // a datum hash, which is itself a valid datum option.
45
+ datumOption: output._tag === "BabbageTransactionOutput" ? output.datumOption : output.datumHash,
46
+ scriptRef:
47
+ output._tag === "BabbageTransactionOutput" && output.scriptRef
48
+ ? Script.fromCBOR(output.scriptRef.bytes)
49
+ : undefined
50
+ })
51
+ }
52
+
53
+ /**
54
+ * Reads what `getUtxos` returned. `Either` and not a throw: this runs on
55
+ * whatever a browser extension chose to send, and one unreadable entry must
56
+ * not take the page down.
57
+ */
58
+ export const readWalletUtxos = (hexes: ReadonlyArray<CborHex>): Either.Either<ReadonlyArray<UTxO.UTxO>, string> => {
59
+ const utxos: Array<UTxO.UTxO> = []
60
+ for (const [index, hex] of hexes.entries()) {
61
+ try {
62
+ utxos.push(readOne(hex))
63
+ } catch (cause) {
64
+ return Either.left(
65
+ `unspent output ${index} could not be read: ${cause instanceof Error ? cause.message : String(cause)}`
66
+ )
67
+ }
68
+ }
69
+ return Either.right(utxos)
70
+ }