@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.
- package/README.md +53 -9
- package/dist/address.d.ts +12 -0
- package/dist/address.d.ts.map +1 -1
- package/dist/address.js +43 -0
- package/dist/address.js.map +1 -1
- package/dist/balance-error.d.ts +48 -0
- package/dist/balance-error.d.ts.map +1 -0
- package/dist/balance-error.js +45 -0
- package/dist/balance-error.js.map +1 -0
- package/dist/balance.d.ts +54 -0
- package/dist/balance.d.ts.map +1 -0
- package/dist/balance.js +175 -0
- package/dist/balance.js.map +1 -0
- package/dist/card.css +492 -0
- package/dist/card.d.ts +41 -0
- package/dist/card.d.ts.map +1 -0
- package/dist/card.js +69 -0
- package/dist/card.js.map +1 -0
- package/dist/cip30.d.ts +10 -0
- package/dist/cip30.d.ts.map +1 -1
- package/dist/cip30.js +16 -0
- package/dist/cip30.js.map +1 -1
- package/dist/complete-error.d.ts +57 -0
- package/dist/complete-error.d.ts.map +1 -0
- package/dist/complete-error.js +52 -0
- package/dist/complete-error.js.map +1 -0
- package/dist/complete.d.ts +77 -0
- package/dist/complete.d.ts.map +1 -0
- package/dist/complete.js +182 -0
- package/dist/complete.js.map +1 -0
- package/dist/derived.d.ts +8 -0
- package/dist/derived.d.ts.map +1 -0
- package/dist/derived.js +2 -0
- package/dist/derived.js.map +1 -0
- package/dist/effects-panel.d.ts +37 -0
- package/dist/effects-panel.d.ts.map +1 -0
- package/dist/effects-panel.js +64 -0
- package/dist/effects-panel.js.map +1 -0
- package/dist/effects.css +629 -0
- package/dist/exchange.d.ts +54 -0
- package/dist/exchange.d.ts.map +1 -0
- package/dist/exchange.js +136 -0
- package/dist/exchange.js.map +1 -0
- package/dist/explain.d.ts +39 -0
- package/dist/explain.d.ts.map +1 -0
- package/dist/explain.js +172 -0
- package/dist/explain.js.map +1 -0
- package/dist/index.d.ts +34 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +19 -2
- package/dist/index.js.map +1 -1
- package/dist/ledger.d.ts +74 -0
- package/dist/ledger.d.ts.map +1 -0
- package/dist/ledger.js +402 -0
- package/dist/ledger.js.map +1 -0
- package/dist/outcome-panel.d.ts +63 -0
- package/dist/outcome-panel.d.ts.map +1 -0
- package/dist/outcome-panel.js +40 -0
- package/dist/outcome-panel.js.map +1 -0
- package/dist/parameter-form.d.ts +26 -0
- package/dist/parameter-form.d.ts.map +1 -0
- package/dist/parameter-form.js +42 -0
- package/dist/parameter-form.js.map +1 -0
- package/dist/resolve.d.ts +11 -0
- package/dist/resolve.d.ts.map +1 -0
- package/dist/resolve.js +21 -0
- package/dist/resolve.js.map +1 -0
- package/dist/sign-error.d.ts +62 -0
- package/dist/sign-error.d.ts.map +1 -0
- package/dist/sign-error.js +42 -0
- package/dist/sign-error.js.map +1 -0
- package/dist/sign.d.ts +38 -0
- package/dist/sign.d.ts.map +1 -0
- package/dist/sign.js +79 -0
- package/dist/sign.js.map +1 -0
- package/dist/slip-card.d.ts +55 -0
- package/dist/slip-card.d.ts.map +1 -0
- package/dist/slip-card.js +89 -0
- package/dist/slip-card.js.map +1 -0
- package/dist/tokens.css +97 -123
- package/dist/utxo.d.ts +16 -0
- package/dist/utxo.d.ts.map +1 -0
- package/dist/utxo.js +51 -0
- package/dist/utxo.js.map +1 -0
- package/dist/witness.d.ts +23 -0
- package/dist/witness.d.ts.map +1 -0
- package/dist/witness.js +101 -0
- package/dist/witness.js.map +1 -0
- package/package.json +13 -6
- package/src/address.ts +53 -0
- package/src/balance-error.ts +77 -0
- package/src/balance.ts +285 -0
- package/src/card.css +492 -0
- package/src/card.ts +86 -0
- package/src/cip30.ts +19 -0
- package/src/complete-error.ts +95 -0
- package/src/complete.ts +307 -0
- package/src/derived.ts +8 -0
- package/src/effects-panel.tsx +386 -0
- package/src/effects.css +629 -0
- package/src/exchange.ts +212 -0
- package/src/explain.ts +211 -0
- package/src/index.ts +64 -3
- package/src/ledger.ts +503 -0
- package/src/outcome-panel.tsx +263 -0
- package/src/parameter-form.tsx +138 -0
- package/src/resolve.ts +40 -0
- package/src/sign-error.ts +89 -0
- package/src/sign.ts +118 -0
- package/src/slip-card.tsx +244 -0
- package/src/tokens.css +97 -123
- package/src/utxo.ts +70 -0
- package/src/witness.ts +132 -0
package/src/ledger.ts
ADDED
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the effects panel draws, worked out without React: the headline figure,
|
|
3
|
+
* one row per effect, which rows a mismatch marks, and the line that states
|
|
4
|
+
* the verdict. Every figure comes from the derived effects, never the claim.
|
|
5
|
+
*/
|
|
6
|
+
import {
|
|
7
|
+
addressText,
|
|
8
|
+
type CertificateEffect,
|
|
9
|
+
type Deposit,
|
|
10
|
+
type DRep,
|
|
11
|
+
encodeBech32,
|
|
12
|
+
type Reason
|
|
13
|
+
} from "@cardano-slips/verifier"
|
|
14
|
+
|
|
15
|
+
import type { Derived } from "./derived.js"
|
|
16
|
+
import { assetLabel, explainReason, formatAda, formatQuantity, shortened } from "./explain.js"
|
|
17
|
+
|
|
18
|
+
/** How an amount reads: gone for good, arriving, taken by the network, held to be given back, or only claimed. */
|
|
19
|
+
export type Tone = "spent" | "gained" | "fee" | "held" | "stated"
|
|
20
|
+
|
|
21
|
+
export type Amount = { readonly text: string; readonly tone: Tone }
|
|
22
|
+
|
|
23
|
+
export type Group = "arriving" | "leaving" | "unmoved"
|
|
24
|
+
|
|
25
|
+
/** A row the link did not describe, or described otherwise. Only these rows take colour. */
|
|
26
|
+
export type Mark = "not-in-the-link" | "not-as-the-link-says"
|
|
27
|
+
|
|
28
|
+
export type Row = {
|
|
29
|
+
readonly key: string
|
|
30
|
+
readonly label: string
|
|
31
|
+
readonly detail?: string
|
|
32
|
+
readonly amounts: ReadonlyArray<Amount>
|
|
33
|
+
/** A pool or DRep, where the row moves no value and the amount column carries a name instead. */
|
|
34
|
+
readonly identifier?: string
|
|
35
|
+
/** `refundable` where the ledger guarantees the figure, `as-stated` where only the transaction claims it. */
|
|
36
|
+
readonly chip?: "refundable" | "as-stated"
|
|
37
|
+
/** The line under a deposit or refund saying what brings it back, or who claims it. */
|
|
38
|
+
readonly note?: string
|
|
39
|
+
readonly group: Group
|
|
40
|
+
readonly mark?: Mark
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type Headline = {
|
|
44
|
+
readonly label: string
|
|
45
|
+
readonly amount: string
|
|
46
|
+
readonly tone: "spent" | "gained"
|
|
47
|
+
/** Only beside what leaves for someone else, where the fee is the smaller figure and is said once. */
|
|
48
|
+
readonly fee?: string
|
|
49
|
+
/** What is locked or handed back, beside the headline and never counted in it. */
|
|
50
|
+
readonly held?: { readonly label: string; readonly amount: string; readonly tone: "held" | "stated" }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** What the "The transaction does" box says for one reason. */
|
|
54
|
+
export type Evidence = { readonly figure?: string; readonly lines: ReadonlyArray<string> }
|
|
55
|
+
|
|
56
|
+
/** Rows past this group into what arrives, what leaves and what moves nothing (sheet state h). */
|
|
57
|
+
const groupedFrom = 5
|
|
58
|
+
|
|
59
|
+
/** Rows past this collapse each group to its header and a count. */
|
|
60
|
+
const collapsedFrom = 9
|
|
61
|
+
|
|
62
|
+
const minus = "−"
|
|
63
|
+
|
|
64
|
+
const hexOf = (bytes: Uint8Array): string => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("")
|
|
65
|
+
|
|
66
|
+
const spent = (text: string): Amount => ({ text: `${minus}${text}`, tone: "spent" })
|
|
67
|
+
const gained = (text: string): Amount => ({ text: `+${text}`, tone: "gained" })
|
|
68
|
+
|
|
69
|
+
/** A token by its name where the name is text, and by its bytes where it is not. The quantity stays in base units. */
|
|
70
|
+
const tokenText = (quantity: bigint, name: Uint8Array): string => {
|
|
71
|
+
const label = assetLabel(hexOf(name))
|
|
72
|
+
if (label !== null) return `${formatQuantity(quantity)} ${label}`
|
|
73
|
+
return name.length === 0
|
|
74
|
+
? `${formatQuantity(quantity)} of an unnamed token`
|
|
75
|
+
: `${formatQuantity(quantity)} of token ${hexOf(name)}`
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const policyLine = (policyId: Uint8Array): string => `policy ${shortenedHex(hexOf(policyId))}`
|
|
79
|
+
|
|
80
|
+
const shortenedHex = (hex: string): string => (hex.length <= 12 ? hex : `${hex.slice(0, 4)}…${hex.slice(-4)}`)
|
|
81
|
+
|
|
82
|
+
/** A DRep id per CIP-129: a header byte naming key or script, then the hash. */
|
|
83
|
+
const drepText = (drep: DRep): string => {
|
|
84
|
+
switch (drep._tag) {
|
|
85
|
+
case "Abstain":
|
|
86
|
+
return "abstain"
|
|
87
|
+
case "NoConfidence":
|
|
88
|
+
return "no confidence"
|
|
89
|
+
case "KeyHash":
|
|
90
|
+
return shortened(encodeBech32("drep", Uint8Array.of(0x22, ...drep.hash)))
|
|
91
|
+
case "ScriptHash":
|
|
92
|
+
return shortened(encodeBech32("drep", Uint8Array.of(0x23, ...drep.hash)))
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const identifierOf = (certificate: CertificateEffect): string | undefined => {
|
|
97
|
+
if (certificate.pool !== null) return shortened(encodeBech32("pool", certificate.pool))
|
|
98
|
+
if (certificate.drep !== null) return drepText(certificate.drep)
|
|
99
|
+
return undefined
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const voteWords = (drep: DRep | null, whose: string): string => {
|
|
103
|
+
if (drep?._tag === "Abstain") return `set ${whose} vote to always abstain`
|
|
104
|
+
if (drep?._tag === "NoConfidence") return `set ${whose} vote to always no confidence`
|
|
105
|
+
return `delegate ${whose} vote to a DRep`
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** What a certificate does, in words. "Your" only where the wallet holds the key it acts on. */
|
|
109
|
+
export const describeCertificate = (certificate: CertificateEffect): string => {
|
|
110
|
+
const whose = certificate.ours || certificate.credential === null ? "your" : "another wallet's"
|
|
111
|
+
const capital = (text: string): string => `${text.charAt(0).toUpperCase()}${text.slice(1)}`
|
|
112
|
+
switch (certificate.kind) {
|
|
113
|
+
case "StakeRegistration":
|
|
114
|
+
case "Registration":
|
|
115
|
+
return capital(`register ${whose} stake key`)
|
|
116
|
+
case "StakeDeregistration":
|
|
117
|
+
case "Deregistration":
|
|
118
|
+
return capital(`close ${whose} stake key and stop delegating`)
|
|
119
|
+
case "StakeDelegation":
|
|
120
|
+
return capital(`delegate ${whose} stake to a stake pool`)
|
|
121
|
+
case "VoteDelegation":
|
|
122
|
+
return capital(voteWords(certificate.drep, whose))
|
|
123
|
+
case "StakeVoteDelegation":
|
|
124
|
+
return capital(`delegate ${whose} stake to a stake pool and ${voteWords(certificate.drep, whose)}`)
|
|
125
|
+
case "StakeRegistrationDelegation":
|
|
126
|
+
return capital(`register ${whose} stake key and delegate it to a stake pool`)
|
|
127
|
+
case "VoteRegistrationDelegation":
|
|
128
|
+
return capital(`register ${whose} stake key and ${voteWords(certificate.drep, whose)}`)
|
|
129
|
+
case "StakeVoteRegistrationDelegation":
|
|
130
|
+
return capital(
|
|
131
|
+
`register ${whose} stake key, delegate it to a stake pool and ${voteWords(certificate.drep, whose)}`
|
|
132
|
+
)
|
|
133
|
+
case "PoolRegistration":
|
|
134
|
+
return "Register a stake pool"
|
|
135
|
+
case "PoolRetirement":
|
|
136
|
+
return "Retire a stake pool"
|
|
137
|
+
case "AuthorizeCommitteeHot":
|
|
138
|
+
return "Authorise a hot key for the constitutional committee"
|
|
139
|
+
case "ResignCommitteeCold":
|
|
140
|
+
return "Resign from the constitutional committee"
|
|
141
|
+
case "RegisterDrep":
|
|
142
|
+
return `Register ${whose === "your" ? "yourself" : "another wallet"} as a DRep`
|
|
143
|
+
case "UnregisterDrep":
|
|
144
|
+
return `Retire ${whose === "your" ? "yourself" : "another wallet"} as a DRep`
|
|
145
|
+
case "UpdateDrep":
|
|
146
|
+
return capital(`update ${whose} DRep details`)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** The parameter and a stated deposit are both what the ledger takes and gives back. Only an assumed one is a claim. */
|
|
151
|
+
const depositChip = (deposit: Deposit): "refundable" | "as-stated" =>
|
|
152
|
+
deposit.basis === "assumed" ? "as-stated" : "refundable"
|
|
153
|
+
|
|
154
|
+
const depositNote = (deposit: Deposit): string => {
|
|
155
|
+
const all = formatAda(deposit.amount)
|
|
156
|
+
if (depositChip(deposit) === "as-stated") {
|
|
157
|
+
return deposit.kind === "pool"
|
|
158
|
+
? "Today's pool deposit. A pool that is already registered pays nothing to register again."
|
|
159
|
+
: "Today's deposit, standing in for a figure the transaction does not settle."
|
|
160
|
+
}
|
|
161
|
+
switch (deposit.kind) {
|
|
162
|
+
case "stake":
|
|
163
|
+
return `Held by the network while you delegate. You get all ${all} back when you stop.`
|
|
164
|
+
case "drep":
|
|
165
|
+
return `Held by the network while you are a DRep. You get all ${all} back when you retire.`
|
|
166
|
+
case "pool":
|
|
167
|
+
return `Held by the network while the pool is registered. It comes back when the pool retires.`
|
|
168
|
+
case "governance-action":
|
|
169
|
+
return "Held with the proposal. It comes back once the proposal is decided."
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const refundNote = (refund: Deposit): string => {
|
|
174
|
+
const what = refund.kind === "drep" ? "the DRep" : "this key"
|
|
175
|
+
return refund.basis === "assumed"
|
|
176
|
+
? `The network returns what ${what} was registered under. The transaction doesn't say what that was, so this is today's deposit standing in for it.`
|
|
177
|
+
: `The transaction states this figure. The network returns what ${what} was registered under, so this is the transaction's claim.`
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const depositKey = (side: "deposit" | "refund", deposit: Deposit): string =>
|
|
181
|
+
`${side}-${deposit.source}-${deposit.index}`
|
|
182
|
+
|
|
183
|
+
/** Every effect as a row, in the sheet's order: what is paid, what arrives, what moves nothing, what the network takes. */
|
|
184
|
+
const rowsOf = (derived: Derived): Array<Row> => {
|
|
185
|
+
const { assets, effects, lovelace } = derived
|
|
186
|
+
const minted = new Set(effects.mint.map((asset) => `${hexOf(asset.policyId)}.${hexOf(asset.name)}`))
|
|
187
|
+
|
|
188
|
+
const payments = effects.outputs
|
|
189
|
+
.filter((output) => !output.mine)
|
|
190
|
+
.map((output): Row => ({
|
|
191
|
+
key: `output-${output.index}`,
|
|
192
|
+
label: "You pay",
|
|
193
|
+
detail: shortened(addressText(output.address)),
|
|
194
|
+
amounts: [
|
|
195
|
+
...output.value.assets.flatMap((policy) =>
|
|
196
|
+
policy.assets.map((asset) => spent(tokenText(asset.quantity, asset.name)))
|
|
197
|
+
),
|
|
198
|
+
...(output.value.coin > 0n ? [spent(formatAda(output.value.coin))] : [])
|
|
199
|
+
],
|
|
200
|
+
group: "leaving"
|
|
201
|
+
}))
|
|
202
|
+
|
|
203
|
+
const arrivals = assets.user
|
|
204
|
+
.filter((asset) => asset.delta < 0n && !minted.has(`${hexOf(asset.policyId)}.${hexOf(asset.name)}`))
|
|
205
|
+
.map((asset): Row => ({
|
|
206
|
+
key: `receive-${hexOf(asset.policyId)}.${hexOf(asset.name)}`,
|
|
207
|
+
label: "You receive",
|
|
208
|
+
detail: policyLine(asset.policyId),
|
|
209
|
+
amounts: [gained(tokenText(asset.delta, asset.name))],
|
|
210
|
+
group: "arriving"
|
|
211
|
+
}))
|
|
212
|
+
|
|
213
|
+
const mint = effects.mint.map((asset, index): Row =>
|
|
214
|
+
asset.quantity > 0n
|
|
215
|
+
? {
|
|
216
|
+
key: `mint-${index}`,
|
|
217
|
+
label: "Tokens created",
|
|
218
|
+
detail: policyLine(asset.policyId),
|
|
219
|
+
amounts: [gained(tokenText(asset.quantity, asset.name))],
|
|
220
|
+
group: "arriving"
|
|
221
|
+
}
|
|
222
|
+
: {
|
|
223
|
+
key: `mint-${index}`,
|
|
224
|
+
label: "Tokens destroyed",
|
|
225
|
+
detail: policyLine(asset.policyId),
|
|
226
|
+
amounts: [spent(tokenText(asset.quantity, asset.name))],
|
|
227
|
+
group: "leaving"
|
|
228
|
+
}
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
const withdrawals = effects.withdrawals.map((withdrawal, index): Row => ({
|
|
232
|
+
key: `withdrawal-${index}`,
|
|
233
|
+
label: "Withdrawal",
|
|
234
|
+
detail: withdrawal.ours ? "Staking rewards to date" : shortened(addressText(withdrawal.rewardAccount)),
|
|
235
|
+
amounts: [gained(formatAda(withdrawal.amount))],
|
|
236
|
+
group: "arriving"
|
|
237
|
+
}))
|
|
238
|
+
|
|
239
|
+
const certificates = effects.certificates.map((certificate): Row => {
|
|
240
|
+
const identifier = identifierOf(certificate)
|
|
241
|
+
return {
|
|
242
|
+
key: `certificate-${certificate.index}`,
|
|
243
|
+
label: "Certificate",
|
|
244
|
+
detail: describeCertificate(certificate),
|
|
245
|
+
amounts: [],
|
|
246
|
+
...(identifier === undefined ? {} : { identifier }),
|
|
247
|
+
group: "unmoved"
|
|
248
|
+
}
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
const fee: Row = {
|
|
252
|
+
key: "fee",
|
|
253
|
+
label: "Network fee",
|
|
254
|
+
amounts: [{ text: `${minus}${formatAda(lovelace.fee)}`, tone: "fee" }],
|
|
255
|
+
group: "leaving"
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const deposits = lovelace.deposits.map((deposit): Row => ({
|
|
259
|
+
key: depositKey("deposit", deposit),
|
|
260
|
+
label: "Deposit",
|
|
261
|
+
amounts: [
|
|
262
|
+
{ text: `${minus}${formatAda(deposit.amount)}`, tone: depositChip(deposit) === "refundable" ? "held" : "stated" }
|
|
263
|
+
],
|
|
264
|
+
chip: depositChip(deposit),
|
|
265
|
+
note: depositNote(deposit),
|
|
266
|
+
group: "leaving"
|
|
267
|
+
}))
|
|
268
|
+
|
|
269
|
+
const refunds = lovelace.refunds.map((refund): Row => ({
|
|
270
|
+
key: depositKey("refund", refund),
|
|
271
|
+
label: "Refund",
|
|
272
|
+
amounts: [{ text: `+${formatAda(refund.amount)}`, tone: "stated" }],
|
|
273
|
+
chip: "as-stated",
|
|
274
|
+
note: refundNote(refund),
|
|
275
|
+
group: "arriving"
|
|
276
|
+
}))
|
|
277
|
+
|
|
278
|
+
return [...payments, ...arrivals, ...mint, ...withdrawals, ...certificates, fee, ...deposits, ...refunds]
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* The rows each reason is about. A reason with nothing in the transaction to
|
|
283
|
+
* point at — a payment left out, certificates out of order — marks no row and
|
|
284
|
+
* is carried by the sentence alone.
|
|
285
|
+
*/
|
|
286
|
+
const marksOf = (derived: Derived, reasons: ReadonlyArray<Reason>): Map<string, Mark> => {
|
|
287
|
+
const marks = new Map<string, Mark>()
|
|
288
|
+
const mark = (key: string, how: Mark): void => {
|
|
289
|
+
if (!marks.has(key)) marks.set(key, how)
|
|
290
|
+
}
|
|
291
|
+
const { effects } = derived
|
|
292
|
+
const paidTo = (address: string) =>
|
|
293
|
+
effects.outputs.filter((output) => !output.mine && addressText(output.address) === address)
|
|
294
|
+
|
|
295
|
+
for (const reason of reasons) {
|
|
296
|
+
switch (reason.code) {
|
|
297
|
+
case "output.undeclared":
|
|
298
|
+
// Those past the count the link asked for are the extra ones.
|
|
299
|
+
for (const output of paidTo(reason.address).slice(reason.declared))
|
|
300
|
+
mark(`output-${output.index}`, "not-in-the-link")
|
|
301
|
+
break
|
|
302
|
+
case "output.lovelace":
|
|
303
|
+
case "output.assets":
|
|
304
|
+
for (const output of paidTo(reason.address)) mark(`output-${output.index}`, "not-as-the-link-says")
|
|
305
|
+
break
|
|
306
|
+
case "certificate.undeclared":
|
|
307
|
+
for (const certificate of effects.certificates.slice(reason.declared)) {
|
|
308
|
+
mark(`certificate-${certificate.index}`, "not-in-the-link")
|
|
309
|
+
}
|
|
310
|
+
break
|
|
311
|
+
case "certificate.target":
|
|
312
|
+
case "certificate.credential":
|
|
313
|
+
mark(`certificate-${reason.index}`, "not-as-the-link-says")
|
|
314
|
+
break
|
|
315
|
+
case "certificate.deposit":
|
|
316
|
+
mark(`certificate-${reason.index}`, "not-as-the-link-says")
|
|
317
|
+
mark(`deposit-certificate-${reason.index}`, "not-as-the-link-says")
|
|
318
|
+
break
|
|
319
|
+
case "withdrawal.undeclared":
|
|
320
|
+
effects.withdrawals.forEach((_, index) => mark(`withdrawal-${index}`, "not-in-the-link"))
|
|
321
|
+
break
|
|
322
|
+
case "withdrawal.account":
|
|
323
|
+
effects.withdrawals.forEach((withdrawal, index) => {
|
|
324
|
+
if (addressText(withdrawal.rewardAccount) === reason.account) mark(`withdrawal-${index}`, "not-in-the-link")
|
|
325
|
+
})
|
|
326
|
+
break
|
|
327
|
+
case "mint.undeclared":
|
|
328
|
+
effects.mint.forEach((_, index) => mark(`mint-${index}`, "not-in-the-link"))
|
|
329
|
+
break
|
|
330
|
+
case "fee.excessive":
|
|
331
|
+
mark("fee", "not-as-the-link-says")
|
|
332
|
+
break
|
|
333
|
+
case "output.missing":
|
|
334
|
+
case "certificate.missing":
|
|
335
|
+
case "certificate.order":
|
|
336
|
+
case "withdrawal.missing":
|
|
337
|
+
case "body.unsupported":
|
|
338
|
+
case "interval.beyond-declared":
|
|
339
|
+
case "interval.not-yet-valid":
|
|
340
|
+
break
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
return marks
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/** The rows, with each mismatch marked on the row it is about. */
|
|
347
|
+
export const ledgerOf = (derived: Derived, reasons: ReadonlyArray<Reason> = []): ReadonlyArray<Row> => {
|
|
348
|
+
const marks = marksOf(derived, reasons)
|
|
349
|
+
return rowsOf(derived).map((row) => {
|
|
350
|
+
const mark = marks.get(row.key)
|
|
351
|
+
return mark === undefined ? row : { ...row, mark }
|
|
352
|
+
})
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
export const isGrouped = (rows: ReadonlyArray<Row>): boolean => rows.length >= groupedFrom
|
|
356
|
+
|
|
357
|
+
export const isCollapsed = (rows: ReadonlyArray<Row>): boolean => rows.length >= collapsedFrom
|
|
358
|
+
|
|
359
|
+
/** The sheet's grouped order: what arrives, what leaves, what moves nothing. Empty groups are left out. */
|
|
360
|
+
export const groupsOf = (
|
|
361
|
+
rows: ReadonlyArray<Row>
|
|
362
|
+
): ReadonlyArray<{ readonly group: Group; readonly rows: ReadonlyArray<Row> }> =>
|
|
363
|
+
(["arriving", "leaving", "unmoved"] as const)
|
|
364
|
+
.map((group) => ({ group, rows: rows.filter((row) => row.group === group) }))
|
|
365
|
+
.filter(({ rows: inGroup }) => inGroup.length > 0)
|
|
366
|
+
|
|
367
|
+
const sum = (values: ReadonlyArray<bigint>): bigint => values.reduce((total, value) => total + value, 0n)
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* The one large figure. What goes to someone else, where anything does;
|
|
371
|
+
* otherwise what the person ends up with or spends. A deposit and a refund are
|
|
372
|
+
* never counted in it — one comes back and the other is only claimed.
|
|
373
|
+
*/
|
|
374
|
+
export const headlineOf = (derived: Derived): Headline => {
|
|
375
|
+
const { effects, lovelace } = derived
|
|
376
|
+
const paid = effects.outputs.filter((output) => !output.mine)
|
|
377
|
+
|
|
378
|
+
const tokens = new Map<string, { readonly name: Uint8Array; quantity: bigint }>()
|
|
379
|
+
for (const output of paid) {
|
|
380
|
+
for (const policy of output.value.assets) {
|
|
381
|
+
for (const asset of policy.assets) {
|
|
382
|
+
const key = `${hexOf(policy.policyId)}.${hexOf(asset.name)}`
|
|
383
|
+
const held = tokens.get(key)
|
|
384
|
+
if (held === undefined) tokens.set(key, { name: asset.name, quantity: asset.quantity })
|
|
385
|
+
else held.quantity += asset.quantity
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
const coin = sum(paid.map((output) => output.value.coin))
|
|
390
|
+
|
|
391
|
+
const deposited = sum(lovelace.deposits.map((deposit) => deposit.amount))
|
|
392
|
+
const refunded = sum(lovelace.refunds.map((refund) => refund.amount))
|
|
393
|
+
const withdrawn = sum(effects.withdrawals.filter((withdrawal) => withdrawal.ours).map((w) => w.amount))
|
|
394
|
+
const gain = withdrawn - lovelace.fee
|
|
395
|
+
|
|
396
|
+
const base =
|
|
397
|
+
tokens.size > 0 || coin > 0n
|
|
398
|
+
? {
|
|
399
|
+
label: "Leaves your wallet",
|
|
400
|
+
amount: [
|
|
401
|
+
...[...tokens.values()].map((token) => tokenText(token.quantity, token.name)),
|
|
402
|
+
...(coin > 0n ? [formatAda(coin)] : [])
|
|
403
|
+
].join(" and "),
|
|
404
|
+
tone: "spent" as const,
|
|
405
|
+
fee: `plus ${formatAda(lovelace.fee)} in network fees`
|
|
406
|
+
}
|
|
407
|
+
: gain > 0n
|
|
408
|
+
? { label: "You end up with", amount: `+${formatAda(gain)}`, tone: "gained" as const }
|
|
409
|
+
: { label: "You spend", amount: formatAda(-gain), tone: "spent" as const }
|
|
410
|
+
|
|
411
|
+
if (deposited > 0n) {
|
|
412
|
+
const promised = lovelace.deposits.every((deposit) => depositChip(deposit) === "refundable")
|
|
413
|
+
const verb = base.tone === "gained" ? "Locked" : "You lock"
|
|
414
|
+
return {
|
|
415
|
+
...base,
|
|
416
|
+
held: {
|
|
417
|
+
label: promised ? `${verb}, refundable` : `${verb}, as stated`,
|
|
418
|
+
amount: formatAda(deposited),
|
|
419
|
+
tone: promised ? "held" : "stated"
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
if (refunded > 0n) {
|
|
424
|
+
return { ...base, held: { label: "Returned, as stated", amount: formatAda(refunded), tone: "stated" } }
|
|
425
|
+
}
|
|
426
|
+
return base
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const spelled = [
|
|
430
|
+
"zero",
|
|
431
|
+
"one",
|
|
432
|
+
"two",
|
|
433
|
+
"three",
|
|
434
|
+
"four",
|
|
435
|
+
"five",
|
|
436
|
+
"six",
|
|
437
|
+
"seven",
|
|
438
|
+
"eight",
|
|
439
|
+
"nine",
|
|
440
|
+
"ten",
|
|
441
|
+
"eleven",
|
|
442
|
+
"twelve"
|
|
443
|
+
]
|
|
444
|
+
|
|
445
|
+
const moves = (kind: CertificateEffect["kind"]): boolean => kind.includes("Delegation")
|
|
446
|
+
|
|
447
|
+
/** The one sentence a match gets, above the buttons. */
|
|
448
|
+
export const matchLine = (derived: Derived, rows: ReadonlyArray<Row>): string => {
|
|
449
|
+
const { effects } = derived
|
|
450
|
+
if (isGrouped(rows)) {
|
|
451
|
+
return `Checked against the link. All ${spelled[rows.length] ?? String(rows.length)} effects are described by it, and the transaction does nothing else.`
|
|
452
|
+
}
|
|
453
|
+
const paysNobody = effects.outputs.every((output) => output.mine) && effects.withdrawals.length === 0
|
|
454
|
+
if (paysNobody && effects.certificates.some((certificate) => moves(certificate.kind))) {
|
|
455
|
+
return "Checked against the link. Your ADA stays in your wallet — delegation moves no funds."
|
|
456
|
+
}
|
|
457
|
+
return "Checked against the link. Every effect above is in the transaction, and the transaction does nothing else."
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/** The opening of the block: what differs, then the reassurance, before any detail. */
|
|
461
|
+
export const blockedLine = (reasons: ReadonlyArray<Reason>): string => {
|
|
462
|
+
const said = reasons.map((reason) => explainReason(reason).summary)
|
|
463
|
+
const what = said.length === 0 ? "The transaction could not be matched to the link." : said.join(" ")
|
|
464
|
+
return `${what} Nothing has been signed.`
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/** For one reason: the figure the transaction carries, and where it goes or what the link asked instead. */
|
|
468
|
+
export const evidenceOf = (reason: Reason, derived: Derived): Evidence => {
|
|
469
|
+
const { actual, declared, summary, where } = explainReason(reason)
|
|
470
|
+
const marked = ledgerOf(derived, [reason])
|
|
471
|
+
.filter((row) => row.mark !== undefined)
|
|
472
|
+
.flatMap((row) => row.amounts.map((amount) => amount.text))
|
|
473
|
+
const figure = marked.length > 0 ? marked.join(", ") : actual
|
|
474
|
+
|
|
475
|
+
const lines = [
|
|
476
|
+
...(where === undefined ? [] : [reason.code === "withdrawal.account" ? `from ${where}` : `to ${where}`]),
|
|
477
|
+
...(declared === undefined ? [] : [`The link asks for ${declared}`])
|
|
478
|
+
]
|
|
479
|
+
return figure === undefined && lines.length === 0
|
|
480
|
+
? { lines: [summary] }
|
|
481
|
+
: { ...(figure === undefined ? {} : { figure }), lines }
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/** The transaction as its outputs, fee and expiry, for the collapsed raw detail. Addresses in full. */
|
|
485
|
+
export const rawLines = (derived: Derived): ReadonlyArray<string> => {
|
|
486
|
+
const { effects } = derived
|
|
487
|
+
const valueText = (coin: bigint, assets: Derived["effects"]["outputs"][number]["value"]["assets"]): string =>
|
|
488
|
+
[
|
|
489
|
+
formatAda(coin),
|
|
490
|
+
...assets.flatMap((policy) =>
|
|
491
|
+
policy.assets.map((asset) => `${tokenText(asset.quantity, asset.name)} (${policyLine(policy.policyId)})`)
|
|
492
|
+
)
|
|
493
|
+
].join(" + ")
|
|
494
|
+
|
|
495
|
+
const until = effects.validity.validUntil
|
|
496
|
+
return [
|
|
497
|
+
...effects.outputs.map(
|
|
498
|
+
(output) =>
|
|
499
|
+
`#${output.index} → ${addressText(output.address)} : ${valueText(output.value.coin, output.value.assets)}${output.mine ? " (change)" : ""}`
|
|
500
|
+
),
|
|
501
|
+
`fee ${formatAda(effects.fee)}${until === null ? "" : ` · invalid from slot ${until.slot}`}`
|
|
502
|
+
]
|
|
503
|
+
}
|