@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,386 @@
1
+ "use client"
2
+
3
+ /**
4
+ * The transaction preview, drawn from `4 · Anatomy` and `6 · Preview states` on
5
+ * the design sheet. Every figure is derived from the transaction; the claim is
6
+ * only the title it is held against. A mismatch removes the sign button rather
7
+ * than disabling it, and no prop brings it back.
8
+ */
9
+ import type { Verdict } from "@cardano-slips/verifier"
10
+ import { useEffect, useId, useState } from "react"
11
+
12
+ import type { Derived } from "./derived.js"
13
+ import { formatCountdown, secondsLeft } from "./explain.js"
14
+ import {
15
+ blockedLine,
16
+ evidenceOf,
17
+ groupsOf,
18
+ headlineOf,
19
+ isCollapsed,
20
+ isGrouped,
21
+ ledgerOf,
22
+ matchLine,
23
+ rawLines,
24
+ type Group,
25
+ type Row
26
+ } from "./ledger.js"
27
+
28
+ export type EffectsPanelProps = {
29
+ /** What the card promised, restated so it can be compared with what follows. */
30
+ readonly claim: string
31
+ /** The card's description, set beside the transaction when the two disagree. */
32
+ readonly description?: string
33
+ /** The host that served the link, as a host only. Whether it is verified is the identity chip's to say. */
34
+ readonly origin?: string
35
+ readonly derived: Derived
36
+ /** The verifier's own verdict. Required, so no caller renders a match by leaving the reasons out. */
37
+ readonly verdict: Verdict
38
+ /** Read once a second for the countdown. */
39
+ readonly clock?: () => number
40
+ readonly onSign?: () => void
41
+ readonly onCancel?: () => void
42
+ readonly onRebuild?: () => void
43
+ /** "Report this link" is offered only where there is somewhere to send the report. */
44
+ readonly onReport?: () => void
45
+ }
46
+
47
+ /** Under this, the countdown leaves the meta row and takes the headline's place (state f). */
48
+ const expiringUnder = 60
49
+
50
+ const groupNames: { readonly [group in Group]: string } = {
51
+ arriving: "Arriving",
52
+ leaving: "Leaving",
53
+ unmoved: "No value moved"
54
+ }
55
+
56
+ const markWords = { "not-in-the-link": "Not in the link", "not-as-the-link-says": "Not as the link says" } as const
57
+
58
+ const chipWords = { refundable: "Refundable", "as-stated": "As stated" } as const
59
+
60
+ const useNow = (clock: () => number): number => {
61
+ const [now, setNow] = useState(clock)
62
+ useEffect(() => {
63
+ const timer = setInterval(() => setNow(clock()), 1000)
64
+ return () => clearInterval(timer)
65
+ }, [clock])
66
+ return now
67
+ }
68
+
69
+ const LedgerRow = ({ row }: { readonly row: Row }): React.JSX.Element => (
70
+ <div className="slip-panel__row" data-mark={row.mark}>
71
+ <div className="slip-panel__row-name">
72
+ <div className="slip-panel__row-label">
73
+ <span className="slip-panel__label">{row.label}</span>
74
+ {row.chip === undefined ? undefined : (
75
+ <span className="slip-panel__chip" data-chip={row.chip}>
76
+ {chipWords[row.chip]}
77
+ </span>
78
+ )}
79
+ {row.mark === undefined ? undefined : (
80
+ <span className="slip-panel__chip" data-chip="mark">
81
+ {markWords[row.mark]}
82
+ </span>
83
+ )}
84
+ </div>
85
+ {row.detail === undefined ? undefined : <span className="slip-panel__detail">{row.detail}</span>}
86
+ </div>
87
+ <div className="slip-panel__amounts">
88
+ {row.identifier === undefined ? undefined : <span className="slip-panel__identifier">{row.identifier}</span>}
89
+ {row.amounts.map((amount, index) => (
90
+ <span key={index} className="slip-panel__amount" data-tone={amount.tone}>
91
+ {amount.text}
92
+ </span>
93
+ ))}
94
+ </div>
95
+ {row.note === undefined ? undefined : <p className="slip-panel__note">{row.note}</p>}
96
+ </div>
97
+ )
98
+
99
+ const Rows = ({ rows }: { readonly rows: ReadonlyArray<Row> }): React.JSX.Element => (
100
+ <div className="slip-panel__ledger">
101
+ {rows.map((row) => (
102
+ <LedgerRow key={row.key} row={row} />
103
+ ))}
104
+ </div>
105
+ )
106
+
107
+ /**
108
+ * Past eight rows each group folds to its header and a count. A group holding
109
+ * a marked row never starts folded: the one thing the block points at is not
110
+ * something a person should have to open.
111
+ */
112
+ export const Ledger = ({ rows }: { readonly rows: ReadonlyArray<Row> }): React.JSX.Element => {
113
+ if (!isGrouped(rows)) return <Rows rows={rows} />
114
+ const collapsed = isCollapsed(rows)
115
+ return (
116
+ <div className="slip-panel__groups">
117
+ {groupsOf(rows).map(({ group, rows: inGroup }) =>
118
+ collapsed ? (
119
+ <details
120
+ key={group}
121
+ className="slip-panel__group"
122
+ open={inGroup.some((row) => row.mark !== undefined) || undefined}
123
+ >
124
+ <summary className="slip-panel__group-name">
125
+ {groupNames[group]} · {inGroup.length}
126
+ </summary>
127
+ <Rows rows={inGroup} />
128
+ </details>
129
+ ) : (
130
+ <div key={group} className="slip-panel__group">
131
+ <p className="slip-panel__group-name">{groupNames[group]}</p>
132
+ <Rows rows={inGroup} />
133
+ </div>
134
+ )
135
+ )}
136
+ </div>
137
+ )
138
+ }
139
+
140
+ const Raw = ({ derived }: { readonly derived: Derived }): React.JSX.Element => (
141
+ <pre className="slip-panel__raw-lines">{rawLines(derived).join("\n")}</pre>
142
+ )
143
+
144
+ const Countdown = ({ seconds }: { readonly seconds: number }): React.JSX.Element => (
145
+ // The server and the browser read the clock a moment apart.
146
+ <span suppressHydrationWarning>{formatCountdown(seconds)}</span>
147
+ )
148
+
149
+ const Blocked = ({
150
+ claim,
151
+ description,
152
+ derived,
153
+ reasons,
154
+ onCancel,
155
+ onReport
156
+ }: Pick<EffectsPanelProps, "claim" | "description" | "derived" | "onCancel" | "onReport"> & {
157
+ readonly reasons: Extract<Verdict, { _tag: "mismatch" }>["reasons"]
158
+ }): React.JSX.Element => {
159
+ const [rawOpen, setRawOpen] = useState(false)
160
+ const title = useId()
161
+ const rows = ledgerOf(derived, reasons)
162
+ return (
163
+ <section className="slip-root slip-panel" data-blocked="" aria-labelledby={title}>
164
+ <div className="slip-panel__claim" role="alert">
165
+ <p className="slip-panel__kicker" data-tone="bad">
166
+ Signing blocked
167
+ </p>
168
+ <h2 className="slip-panel__title" id={title}>
169
+ This transaction doesn't do what the link says
170
+ </h2>
171
+ <p className="slip-panel__lead">{blockedLine(reasons)}</p>
172
+ </div>
173
+
174
+ <div className="slip-panel__compare">
175
+ <div className="slip-panel__side">
176
+ <p className="slip-panel__kicker">The link claims</p>
177
+ <p className="slip-panel__claimed">{claim}</p>
178
+ {description === undefined ? undefined : <p className="slip-panel__note">{description}</p>}
179
+ </div>
180
+ <div className="slip-panel__side" data-tone="bad">
181
+ <p className="slip-panel__kicker" data-tone="bad">
182
+ The transaction does
183
+ </p>
184
+ {reasons.map((reason, index) => {
185
+ const { figure, lines } = evidenceOf(reason, derived)
186
+ return (
187
+ <div key={index} className="slip-panel__evidence">
188
+ {figure === undefined ? undefined : <p className="slip-panel__evidence-figure">{figure}</p>}
189
+ {lines.map((line) => (
190
+ <p key={line} className="slip-panel__detail">
191
+ {line}
192
+ </p>
193
+ ))}
194
+ </div>
195
+ )
196
+ })}
197
+ </div>
198
+ </div>
199
+
200
+ <Ledger rows={rows} />
201
+
202
+ {rawOpen ? <Raw derived={derived} /> : undefined}
203
+
204
+ <div className="slip-panel__exits">
205
+ <button type="button" className="slip-panel__button" data-kind="close" onClick={onCancel}>
206
+ Close without signing
207
+ </button>
208
+ <div className="slip-panel__quiet">
209
+ <button
210
+ type="button"
211
+ className="slip-panel__button"
212
+ data-kind="quiet"
213
+ aria-expanded={rawOpen}
214
+ onClick={() => setRawOpen((open) => !open)}
215
+ >
216
+ {rawOpen ? "Hide raw transaction" : "Show raw transaction"}
217
+ </button>
218
+ {onReport === undefined ? undefined : (
219
+ <button type="button" className="slip-panel__button" data-kind="quiet" onClick={onReport}>
220
+ Report this link
221
+ </button>
222
+ )}
223
+ </div>
224
+ </div>
225
+ </section>
226
+ )
227
+ }
228
+
229
+ export const EffectsPanel = ({
230
+ claim,
231
+ description,
232
+ origin,
233
+ derived,
234
+ verdict,
235
+ clock = Date.now,
236
+ onSign,
237
+ onCancel,
238
+ onRebuild,
239
+ onReport
240
+ }: EffectsPanelProps): React.JSX.Element => {
241
+ const now = useNow(clock)
242
+ const [rawOpen, setRawOpen] = useState(false)
243
+ const title = useId()
244
+
245
+ if (verdict._tag === "mismatch") {
246
+ return (
247
+ <Blocked
248
+ claim={claim}
249
+ derived={derived}
250
+ reasons={verdict.reasons}
251
+ {...(description === undefined ? {} : { description })}
252
+ {...(onCancel === undefined ? {} : { onCancel })}
253
+ {...(onReport === undefined ? {} : { onReport })}
254
+ />
255
+ )
256
+ }
257
+
258
+ const rows = ledgerOf(derived)
259
+ const until = derived.effects.validity.validUntil
260
+ const seconds = until === null ? undefined : secondsLeft(until.time, BigInt(now))
261
+
262
+ if (seconds === 0) {
263
+ return (
264
+ <section className="slip-root slip-panel" data-expired="" aria-labelledby={title}>
265
+ <div className="slip-panel__claim">
266
+ <p className="slip-panel__kicker" data-tone="warn">
267
+ Expired
268
+ </p>
269
+ <h2 className="slip-panel__title" id={title}>
270
+ This transaction ran out of time
271
+ </h2>
272
+ <p className="slip-panel__lead">
273
+ Nothing was signed and nothing left your wallet. Building a new one takes a second, and the amounts will be
274
+ checked again.
275
+ </p>
276
+ </div>
277
+ <div className="slip-panel__stale">
278
+ <Ledger rows={rows} />
279
+ </div>
280
+ <div className="slip-panel__buttons">
281
+ <button type="button" className="slip-panel__button" data-kind="cancel" onClick={onCancel}>
282
+ Cancel
283
+ </button>
284
+ <button type="button" className="slip-panel__button" data-kind="primary" onClick={onRebuild}>
285
+ Build a new transaction
286
+ </button>
287
+ </div>
288
+ </section>
289
+ )
290
+ }
291
+
292
+ const headline = headlineOf(derived)
293
+ const expiring = seconds !== undefined && seconds < expiringUnder
294
+
295
+ return (
296
+ <section className="slip-root slip-panel" aria-labelledby={title}>
297
+ <div className="slip-panel__claim">
298
+ <p className="slip-panel__kicker">What you're signing</p>
299
+ <h2 className="slip-panel__title" id={title}>
300
+ {claim}
301
+ </h2>
302
+ {origin === undefined ? undefined : <span className="slip-panel__origin">{origin}</span>}
303
+ </div>
304
+
305
+ {expiring ? (
306
+ <div className="slip-panel__expiring" role="timer">
307
+ <div className="slip-panel__expiring-head">
308
+ <span>This transaction expires in</span>
309
+ <span className="slip-panel__expiring-clock">
310
+ <Countdown seconds={seconds} />
311
+ </span>
312
+ </div>
313
+ <div className="slip-panel__bar" aria-hidden="true">
314
+ <span
315
+ className="slip-panel__bar-fill"
316
+ suppressHydrationWarning
317
+ style={{ width: `${(seconds / expiringUnder) * 100}%` }}
318
+ />
319
+ </div>
320
+ <p className="slip-panel__note">After that the amounts could be stale, so we'll build a fresh one.</p>
321
+ </div>
322
+ ) : (
323
+ <div className="slip-panel__headline">
324
+ <div className="slip-panel__figure">
325
+ <span className="slip-panel__note">{headline.label}</span>
326
+ <span className="slip-panel__figure-amount" data-tone={headline.tone}>
327
+ {headline.amount}
328
+ </span>
329
+ {headline.fee === undefined ? undefined : <span className="slip-panel__note">{headline.fee}</span>}
330
+ </div>
331
+ {headline.held === undefined ? undefined : (
332
+ <>
333
+ <span className="slip-panel__rule" aria-hidden="true" />
334
+ <div className="slip-panel__figure">
335
+ <span className="slip-panel__note">{headline.held.label}</span>
336
+ <span className="slip-panel__figure-amount" data-tone={headline.held.tone}>
337
+ {headline.held.amount}
338
+ </span>
339
+ </div>
340
+ </>
341
+ )}
342
+ </div>
343
+ )}
344
+
345
+ <Ledger rows={rows} />
346
+
347
+ {expiring || seconds === undefined ? undefined : (
348
+ <div className="slip-panel__expiry">
349
+ <span className="slip-panel__note">Expires in</span>
350
+ <span className="slip-panel__clock">
351
+ <Countdown seconds={seconds} />
352
+ </span>
353
+ </div>
354
+ )}
355
+
356
+ <p className="slip-panel__verdict">
357
+ <span className="slip-panel__tick" aria-hidden="true">
358
+ ✓
359
+ </span>
360
+ <span>{matchLine(derived, rows)}</span>
361
+ </p>
362
+
363
+ <div className="slip-panel__buttons">
364
+ <button type="button" className="slip-panel__button" data-kind="cancel" onClick={onCancel}>
365
+ Cancel
366
+ </button>
367
+ <button type="button" className="slip-panel__button" data-kind="primary" onClick={onSign}>
368
+ Sign transaction
369
+ </button>
370
+ </div>
371
+
372
+ <div className="slip-panel__raw">
373
+ <button
374
+ type="button"
375
+ className="slip-panel__raw-toggle"
376
+ aria-expanded={rawOpen}
377
+ onClick={() => setRawOpen((open) => !open)}
378
+ >
379
+ <span aria-hidden="true">{rawOpen ? "▾" : "▸"}</span>
380
+ <span>Raw transaction</span>
381
+ </button>
382
+ {rawOpen ? <Raw derived={derived} /> : undefined}
383
+ </div>
384
+ </section>
385
+ )
386
+ }