@cardano-slips/flow 0.2.0 → 0.3.1
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/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/complete-error.d.ts +8 -1
- package/dist/complete-error.d.ts.map +1 -1
- package/dist/complete-error.js +6 -1
- package/dist/complete-error.js.map +1 -1
- package/dist/complete.d.ts +29 -10
- package/dist/complete.d.ts.map +1 -1
- package/dist/complete.js +83 -9
- package/dist/complete.js.map +1 -1
- 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 +19 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -1
- 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/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 +57 -32
- package/package.json +11 -7
- package/src/address.ts +53 -0
- package/src/card.css +492 -0
- package/src/card.ts +86 -0
- package/src/complete-error.ts +14 -2
- package/src/complete.ts +128 -19
- 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 +39 -3
- package/src/ledger.ts +503 -0
- package/src/outcome-panel.tsx +263 -0
- package/src/parameter-form.tsx +138 -0
- package/src/slip-card.tsx +244 -0
- package/src/tokens.css +57 -32
package/src/exchange.ts
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two HTTP exchanges a client makes: `GET` for the Slip and `POST` for the
|
|
3
|
+
* partial intent. Every way either can fail arrives as one error in the spec's
|
|
4
|
+
* vocabulary, classified by the rules in "Failure responses".
|
|
5
|
+
*/
|
|
6
|
+
import {
|
|
7
|
+
type BoundedAnswer,
|
|
8
|
+
boundedRequest,
|
|
9
|
+
type BoundedRequestOptions,
|
|
10
|
+
checkTemplates,
|
|
11
|
+
classifyErrorCode,
|
|
12
|
+
classifyStatus,
|
|
13
|
+
decodePartialIntent,
|
|
14
|
+
decodeSlip,
|
|
15
|
+
decodeSlipError,
|
|
16
|
+
fetchDomainMapping,
|
|
17
|
+
type InsecureSlipUrl,
|
|
18
|
+
type Network,
|
|
19
|
+
type PartialIntent,
|
|
20
|
+
PROTOCOL_VERSION,
|
|
21
|
+
resolveSlipUrl,
|
|
22
|
+
type Slip,
|
|
23
|
+
type SlipErrorClass
|
|
24
|
+
} from "@cardano-slips/core"
|
|
25
|
+
import { Data, Effect, Either } from "effect"
|
|
26
|
+
|
|
27
|
+
export type ExchangeOptions = BoundedRequestOptions
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* `code` is a spec code, or one an endpoint sent that the spec does not define.
|
|
31
|
+
* It is absent only where the body could not be read, and then `status` is all
|
|
32
|
+
* there is to go on.
|
|
33
|
+
*/
|
|
34
|
+
export class ExchangeError extends Data.TaggedError("ExchangeError")<{
|
|
35
|
+
readonly code?: string
|
|
36
|
+
readonly errorClass: SlipErrorClass
|
|
37
|
+
/** The endpoint's own words, present only when its failure body was readable. */
|
|
38
|
+
readonly endpointMessage?: string
|
|
39
|
+
readonly field?: string
|
|
40
|
+
readonly status?: number
|
|
41
|
+
/** Seconds, from `Retry-After`. */
|
|
42
|
+
readonly retryAfter?: number
|
|
43
|
+
/** For whoever integrates this; never put in front of a person. */
|
|
44
|
+
readonly detail: string
|
|
45
|
+
}> {
|
|
46
|
+
override get message(): string {
|
|
47
|
+
return this.detail
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const clientFailure = (code: string, detail: string, status?: number): ExchangeError =>
|
|
52
|
+
new ExchangeError({
|
|
53
|
+
code,
|
|
54
|
+
errorClass: classifyErrorCode(code),
|
|
55
|
+
detail,
|
|
56
|
+
...(status === undefined ? {} : { status })
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
/** Whole seconds or an HTTP date, per RFC 9110; anything else says nothing. */
|
|
60
|
+
const readRetryAfter = (header: string | null, now: number): number | undefined => {
|
|
61
|
+
if (header === null) return undefined
|
|
62
|
+
const trimmed = header.trim()
|
|
63
|
+
if (/^\d+$/.test(trimmed)) return Number(trimmed)
|
|
64
|
+
const at = Date.parse(trimmed)
|
|
65
|
+
return Number.isNaN(at) ? undefined : Math.max(0, Math.ceil((at - now) / 1000))
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const parseJson = (text: string): unknown => {
|
|
69
|
+
try {
|
|
70
|
+
return JSON.parse(text) as unknown
|
|
71
|
+
} catch {
|
|
72
|
+
return undefined
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** A major version is checked before the shape, so a newer protocol is not reported as a broken one. */
|
|
77
|
+
const newerVersion = (body: unknown): string | undefined => {
|
|
78
|
+
if (typeof body !== "object" || body === null) return undefined
|
|
79
|
+
const version = (body as Record<string, unknown>).version
|
|
80
|
+
return typeof version === "string" && /^[1-9][0-9]*$/.test(version) && version !== PROTOCOL_VERSION
|
|
81
|
+
? version
|
|
82
|
+
: undefined
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const failureFrom = (response: Response, text: string): ExchangeError => {
|
|
86
|
+
const status = response.status
|
|
87
|
+
const retryAfter = readRetryAfter(response.headers.get("retry-after"), Date.now())
|
|
88
|
+
const retry = retryAfter === undefined ? {} : { retryAfter }
|
|
89
|
+
const body = parseJson(text)
|
|
90
|
+
|
|
91
|
+
const version = newerVersion(body)
|
|
92
|
+
if (version !== undefined) {
|
|
93
|
+
return clientFailure("UNSUPPORTED_VERSION", `the failure body speaks version ${version}`, status)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const read = decodeSlipError(body)
|
|
97
|
+
// Unreadable, so classified by status and never rendered: it is as likely an
|
|
98
|
+
// intermediary's error page as the publisher's words.
|
|
99
|
+
if (Either.isLeft(read)) {
|
|
100
|
+
return new ExchangeError({
|
|
101
|
+
errorClass: classifyStatus(status),
|
|
102
|
+
status,
|
|
103
|
+
detail: `answered ${status} with a body that is not a failure response`,
|
|
104
|
+
...retry
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return new ExchangeError({
|
|
109
|
+
code: read.right.code,
|
|
110
|
+
errorClass: classifyErrorCode(read.right.code),
|
|
111
|
+
endpointMessage: read.right.message,
|
|
112
|
+
status,
|
|
113
|
+
detail: `answered ${status} ${read.right.code}`,
|
|
114
|
+
...(read.right.field === undefined ? {} : { field: read.right.field }),
|
|
115
|
+
...retry
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
type Answer = BoundedAnswer
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* One request, through core's bounded request. Anything that stops a usable
|
|
123
|
+
* answer arriving — DNS, TLS, a refused CORS read, the clock, the size — is `UNREACHABLE`.
|
|
124
|
+
*/
|
|
125
|
+
const exchange = (url: string, init: RequestInit, options: ExchangeOptions): Effect.Effect<Answer, ExchangeError> =>
|
|
126
|
+
boundedRequest(url, init, options).pipe(Effect.mapError((failure) => clientFailure(failure.code, failure.detail)))
|
|
127
|
+
|
|
128
|
+
const successBody = ({ response, text }: Answer): Effect.Effect<unknown, ExchangeError> => {
|
|
129
|
+
if (response.status !== 200) return Effect.fail(failureFrom(response, text))
|
|
130
|
+
const body = parseJson(text)
|
|
131
|
+
if (body === undefined) {
|
|
132
|
+
return Effect.fail(clientFailure("MALFORMED_RESPONSE", `${response.url} answered 200 with something not JSON`))
|
|
133
|
+
}
|
|
134
|
+
const version = newerVersion(body)
|
|
135
|
+
if (version !== undefined) {
|
|
136
|
+
return Effect.fail(clientFailure("UNSUPPORTED_VERSION", `the response speaks version ${version}`))
|
|
137
|
+
}
|
|
138
|
+
return Effect.succeed(body)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export type FetchedSlip = {
|
|
142
|
+
readonly slip: Slip
|
|
143
|
+
/** The endpoint the link resolved to. Every `href` resolves against it. */
|
|
144
|
+
readonly discoveryUrl: string
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* The link to a Slip a person can be shown, or why not. `slips.json` is read
|
|
149
|
+
* first, because the link a person shares is not always the endpoint. A link
|
|
150
|
+
* that is not `https:` fails as `InsecureSlipUrl`, before anything is fetched.
|
|
151
|
+
*/
|
|
152
|
+
export const fetchSlip = (
|
|
153
|
+
link: string,
|
|
154
|
+
options: ExchangeOptions = {}
|
|
155
|
+
): Effect.Effect<FetchedSlip, ExchangeError | InsecureSlipUrl> =>
|
|
156
|
+
Effect.gen(function* () {
|
|
157
|
+
const mapping = yield* fetchDomainMapping(link, options).pipe(
|
|
158
|
+
Effect.catchTag("DomainMappingFailure", (failure) => Effect.fail(clientFailure(failure.code, failure.detail)))
|
|
159
|
+
)
|
|
160
|
+
const discoveryUrl = yield* resolveSlipUrl(link, mapping)
|
|
161
|
+
|
|
162
|
+
const answer = yield* exchange(discoveryUrl, { method: "GET", headers: { accept: "application/json" } }, options)
|
|
163
|
+
const body = yield* successBody(answer)
|
|
164
|
+
|
|
165
|
+
const slip = decodeSlip(body)
|
|
166
|
+
if (Either.isLeft(slip)) {
|
|
167
|
+
return yield* Effect.fail(clientFailure("MALFORMED_RESPONSE", `${discoveryUrl} did not answer with a Slip`))
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const defects = checkTemplates(slip.right, discoveryUrl)
|
|
171
|
+
if (defects.length > 0) {
|
|
172
|
+
return yield* Effect.fail(
|
|
173
|
+
clientFailure("MALFORMED_RESPONSE", `${discoveryUrl} answered a Slip with ${defects[0]?._tag ?? "a defect"}`)
|
|
174
|
+
)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return { slip: slip.right, discoveryUrl }
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
export type IntentRequest = {
|
|
181
|
+
/** Absolute, as `SlipCard` hands it over. */
|
|
182
|
+
readonly href: string
|
|
183
|
+
readonly changeAddress: string
|
|
184
|
+
readonly network: Network
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* The partial intent for one action. The body is the change address and the
|
|
189
|
+
* network and nothing else: Mode A never tells an endpoint what a wallet holds.
|
|
190
|
+
*/
|
|
191
|
+
export const requestIntent = (
|
|
192
|
+
{ changeAddress, href, network }: IntentRequest,
|
|
193
|
+
options: ExchangeOptions = {}
|
|
194
|
+
): Effect.Effect<PartialIntent, ExchangeError> =>
|
|
195
|
+
Effect.gen(function* () {
|
|
196
|
+
const answer = yield* exchange(
|
|
197
|
+
href,
|
|
198
|
+
{
|
|
199
|
+
method: "POST",
|
|
200
|
+
headers: { accept: "application/json", "content-type": "application/json" },
|
|
201
|
+
body: JSON.stringify({ changeAddress, network })
|
|
202
|
+
},
|
|
203
|
+
options
|
|
204
|
+
)
|
|
205
|
+
const body = yield* successBody(answer)
|
|
206
|
+
|
|
207
|
+
const intent = decodePartialIntent(body)
|
|
208
|
+
if (Either.isLeft(intent)) {
|
|
209
|
+
return yield* Effect.fail(clientFailure("MALFORMED_RESPONSE", `${href} did not answer with a partial intent`))
|
|
210
|
+
}
|
|
211
|
+
return intent.right
|
|
212
|
+
})
|
package/src/explain.ts
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The words the effects panel puts in front of a person: exact figures, and a
|
|
3
|
+
* plain account of each mismatch. Nothing here rounds, and nothing guesses a
|
|
4
|
+
* token's decimals — the CIP requires base units where they are not known.
|
|
5
|
+
*/
|
|
6
|
+
import type { Reason, UnsupportedMember } from "@cardano-slips/verifier"
|
|
7
|
+
|
|
8
|
+
const lovelacePerAda = 1_000_000n
|
|
9
|
+
|
|
10
|
+
const grouped = (digits: string): string => digits.replace(/\B(?=(\d{3})+(?!\d))/g, ",")
|
|
11
|
+
|
|
12
|
+
const magnitude = (value: bigint): bigint => (value < 0n ? -value : value)
|
|
13
|
+
|
|
14
|
+
/** Lovelace as ADA, to the lovelace. The sign is dropped: "you pay" and "you receive" carry the direction. */
|
|
15
|
+
export const formatAda = (lovelace: bigint): string => {
|
|
16
|
+
const amount = magnitude(lovelace)
|
|
17
|
+
const whole = grouped((amount / lovelacePerAda).toString())
|
|
18
|
+
const fraction = (amount % lovelacePerAda).toString().padStart(6, "0").replace(/0+$/, "")
|
|
19
|
+
return `${fraction === "" ? whole : `${whole}.${fraction}`} ADA`
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** A token quantity in base units, unsigned for the same reason as `formatAda`. */
|
|
23
|
+
export const formatQuantity = (quantity: bigint): string => grouped(magnitude(quantity).toString())
|
|
24
|
+
|
|
25
|
+
const bytesOf = (hex: string): Uint8Array =>
|
|
26
|
+
Uint8Array.from({ length: hex.length / 2 }, (_, index) => Number.parseInt(hex.slice(index * 2, index * 2 + 2), 16))
|
|
27
|
+
|
|
28
|
+
/** The asset name as text, or null where it is not text a person could read — which is then no name at all. */
|
|
29
|
+
export const assetLabel = (assetName: string): string | null => {
|
|
30
|
+
if (assetName === "") return null
|
|
31
|
+
try {
|
|
32
|
+
const text = new TextDecoder("utf-8", { fatal: true }).decode(bytesOf(assetName))
|
|
33
|
+
return /\p{C}/u.test(text) ? null : text
|
|
34
|
+
} catch {
|
|
35
|
+
return null
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const tokenAmount = (quantity: bigint, policyId: string, assetName: string): string => {
|
|
40
|
+
const label = assetLabel(assetName)
|
|
41
|
+
return label === null
|
|
42
|
+
? `${formatQuantity(quantity)} of token ${assetName || "with no name"} under policy ${policyId}`
|
|
43
|
+
: `${formatQuantity(quantity)} ${label}`
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
|
|
47
|
+
|
|
48
|
+
/** Unix milliseconds as a UTC instant. UTC so that the publisher and the person read the same moment. */
|
|
49
|
+
export const formatInstant = (time: bigint): string => {
|
|
50
|
+
const date = new Date(Number(time))
|
|
51
|
+
const two = (value: number): string => value.toString().padStart(2, "0")
|
|
52
|
+
return `${date.getUTCDate()} ${months[date.getUTCMonth()]} ${date.getUTCFullYear()}, ${two(date.getUTCHours())}:${two(date.getUTCMinutes())} UTC`
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Whole seconds until `until`, never below zero. */
|
|
56
|
+
export const secondsLeft = (until: bigint, now: bigint): number =>
|
|
57
|
+
until <= now ? 0 : Math.floor(Number(until - now) / 1000)
|
|
58
|
+
|
|
59
|
+
/** The sheet's clock: `4m 12s`, and hours in front once there are any. */
|
|
60
|
+
export const formatCountdown = (seconds: number): string => {
|
|
61
|
+
const hours = Math.floor(seconds / 3600)
|
|
62
|
+
const minutes = Math.floor((seconds % 3600) / 60)
|
|
63
|
+
const rest = `${(seconds % 60).toString().padStart(2, "0")}s`
|
|
64
|
+
return hours > 0 ? `${hours}h ${minutes.toString().padStart(2, "0")}m ${rest}` : `${minutes}m ${rest}`
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* A bech32 string cut to what a column can hold: the prefix, three characters,
|
|
69
|
+
* and the last four. Only where the full string is a click away.
|
|
70
|
+
*/
|
|
71
|
+
export const shortened = (text: string): string => {
|
|
72
|
+
const separator = text.lastIndexOf("1")
|
|
73
|
+
if (separator < 1 || text.length - separator <= 12) return text
|
|
74
|
+
return `${text.slice(0, separator + 4)}…${text.slice(-4)}`
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const unsupportedWords: { readonly [member in UnsupportedMember]: string } = {
|
|
78
|
+
"reference-input": "reads another output without spending it",
|
|
79
|
+
collateral: "puts up collateral",
|
|
80
|
+
"required-signer": "asks for an extra signature",
|
|
81
|
+
vote: "casts a governance vote",
|
|
82
|
+
proposal: "submits a governance proposal",
|
|
83
|
+
donation: "donates to the treasury",
|
|
84
|
+
"treasury-value": "states the treasury's value",
|
|
85
|
+
script: "runs a script",
|
|
86
|
+
datum: "attaches data to an output"
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const payments = (count: number): string => (count === 1 ? "1 payment" : `${count} payments`)
|
|
90
|
+
|
|
91
|
+
const certificates = (count: number): string => (count === 1 ? "1 certificate" : `${count} certificates`)
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* What the block shows for one reason, in the sheet's voice: the transaction
|
|
95
|
+
* is held to "the link", and a sentence never carries an address — `where` does,
|
|
96
|
+
* in full, beside it. The CIP asks for the difference, not the rule: `declared`
|
|
97
|
+
* is what the link asked for and `actual` is what the transaction does, where
|
|
98
|
+
* the reason has a figure for each.
|
|
99
|
+
*/
|
|
100
|
+
export type Explanation = {
|
|
101
|
+
readonly summary: string
|
|
102
|
+
readonly declared?: string
|
|
103
|
+
readonly actual?: string
|
|
104
|
+
/** The address or reward account the reason is about. */
|
|
105
|
+
readonly where?: string
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export const explainReason = (reason: Reason): Explanation => {
|
|
109
|
+
switch (reason.code) {
|
|
110
|
+
case "output.missing":
|
|
111
|
+
return {
|
|
112
|
+
summary: "The transaction leaves out a payment the link asks for.",
|
|
113
|
+
declared: payments(reason.declared),
|
|
114
|
+
actual: payments(reason.paid),
|
|
115
|
+
where: reason.address
|
|
116
|
+
}
|
|
117
|
+
case "output.undeclared":
|
|
118
|
+
return reason.declared === 0
|
|
119
|
+
? { summary: "The transaction pays an address the link never mentions.", where: reason.address }
|
|
120
|
+
: {
|
|
121
|
+
summary: "The transaction pays one address more times than the link asks for.",
|
|
122
|
+
declared: payments(reason.declared),
|
|
123
|
+
actual: payments(reason.paid),
|
|
124
|
+
where: reason.address
|
|
125
|
+
}
|
|
126
|
+
case "output.lovelace":
|
|
127
|
+
return {
|
|
128
|
+
summary: "The transaction pays a different amount of ADA from the one the link asks for.",
|
|
129
|
+
declared: formatAda(reason.declared),
|
|
130
|
+
actual: formatAda(reason.paid),
|
|
131
|
+
where: reason.address
|
|
132
|
+
}
|
|
133
|
+
case "output.assets":
|
|
134
|
+
return {
|
|
135
|
+
summary: "The transaction pays a different amount of a token from the one the link asks for.",
|
|
136
|
+
declared: tokenAmount(reason.declared, reason.policyId, reason.assetName),
|
|
137
|
+
actual: tokenAmount(reason.paid, reason.policyId, reason.assetName),
|
|
138
|
+
where: reason.address
|
|
139
|
+
}
|
|
140
|
+
case "certificate.missing":
|
|
141
|
+
return {
|
|
142
|
+
summary: "A certificate the link asks for is missing from the transaction.",
|
|
143
|
+
declared: certificates(reason.declared),
|
|
144
|
+
actual: certificates(reason.carried)
|
|
145
|
+
}
|
|
146
|
+
case "certificate.undeclared":
|
|
147
|
+
return {
|
|
148
|
+
summary: "The transaction carries a certificate the link never mentions.",
|
|
149
|
+
declared: certificates(reason.declared),
|
|
150
|
+
actual: certificates(reason.carried)
|
|
151
|
+
}
|
|
152
|
+
case "certificate.order":
|
|
153
|
+
return { summary: "The transaction's certificates are in a different order from the link's." }
|
|
154
|
+
case "certificate.target":
|
|
155
|
+
return {
|
|
156
|
+
summary: `Certificate ${reason.index + 1} names a different pool or DRep from the one the link asks for.`,
|
|
157
|
+
declared: reason.declared,
|
|
158
|
+
actual: reason.carried
|
|
159
|
+
}
|
|
160
|
+
case "certificate.credential":
|
|
161
|
+
return { summary: `Certificate ${reason.index + 1} acts on a stake key this wallet does not hold.` }
|
|
162
|
+
case "certificate.deposit":
|
|
163
|
+
return {
|
|
164
|
+
summary: `Certificate ${reason.index + 1} takes a deposit other than the one the network sets.`,
|
|
165
|
+
declared: formatAda(reason.parameter),
|
|
166
|
+
actual: formatAda(reason.stated)
|
|
167
|
+
}
|
|
168
|
+
case "withdrawal.missing":
|
|
169
|
+
return { summary: "The link asks to withdraw your rewards, and the transaction does not." }
|
|
170
|
+
case "withdrawal.undeclared":
|
|
171
|
+
return { summary: "The transaction withdraws rewards the link never mentions." }
|
|
172
|
+
case "withdrawal.account":
|
|
173
|
+
return {
|
|
174
|
+
summary: "The transaction withdraws from a reward account that is not this wallet's.",
|
|
175
|
+
where: reason.account
|
|
176
|
+
}
|
|
177
|
+
case "mint.undeclared":
|
|
178
|
+
return {
|
|
179
|
+
summary: "The transaction creates or destroys tokens, which no link can ask for.",
|
|
180
|
+
actual: reason.assets
|
|
181
|
+
.map(
|
|
182
|
+
(asset) =>
|
|
183
|
+
`${asset.quantity < 0n ? "Destroys" : "Creates"} ${tokenAmount(asset.quantity, asset.policyId, asset.assetName)}`
|
|
184
|
+
)
|
|
185
|
+
.join(", ")
|
|
186
|
+
}
|
|
187
|
+
case "body.unsupported":
|
|
188
|
+
return {
|
|
189
|
+
summary: `The transaction does things a link cannot describe: it ${reason.members
|
|
190
|
+
.map((member) => unsupportedWords[member])
|
|
191
|
+
.join(", ")}.`
|
|
192
|
+
}
|
|
193
|
+
case "fee.excessive":
|
|
194
|
+
return {
|
|
195
|
+
summary: "The network fee is higher than this transaction needs.",
|
|
196
|
+
declared: `At most ${formatAda(reason.ceiling)}`,
|
|
197
|
+
actual: formatAda(reason.fee)
|
|
198
|
+
}
|
|
199
|
+
case "interval.beyond-declared":
|
|
200
|
+
return {
|
|
201
|
+
summary: "The transaction stays valid for longer than the link says.",
|
|
202
|
+
declared: `Until ${formatInstant(reason.declared)}`,
|
|
203
|
+
actual: reason.validUntil === null ? "Never expires" : `Until ${formatInstant(reason.validUntil)}`
|
|
204
|
+
}
|
|
205
|
+
case "interval.not-yet-valid":
|
|
206
|
+
return {
|
|
207
|
+
summary: "The transaction cannot be submitted yet.",
|
|
208
|
+
actual: `Valid from ${formatInstant(reason.validFrom)}`
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
* The public entry point of `@cardano-slips/flow`. The `exports` map exposes
|
|
3
3
|
* this module and nothing else, so moving a file is never a breaking change.
|
|
4
4
|
*/
|
|
5
|
-
export type { NetworkId, WalletAddress } from "./address.js"
|
|
6
|
-
export { readWalletAddress } from "./address.js"
|
|
5
|
+
export type { NetworkId, OwnAddress, WalletAddress } from "./address.js"
|
|
6
|
+
export { readOwnAddress, readWalletAddress } from "./address.js"
|
|
7
7
|
export type { BalancedTransaction, BalanceInputs, BalancingParameters } from "./balance.js"
|
|
8
8
|
export { balanceIntent } from "./balance.js"
|
|
9
9
|
export type { BalanceRefusal } from "./balance-error.js"
|
|
10
10
|
export { BalanceError, balanceRefusals, slipErrorCodeFor as balanceErrorCodeFor } from "./balance-error.js"
|
|
11
|
+
export { actionLabel, actionsOf, blockedLabel, closureOf, monogramOf, originOf } from "./card.js"
|
|
11
12
|
export type { CborHex, Cip30Api, Cip30ApiError, Cip30Provider, Paginate, SignedData } from "./cip30.js"
|
|
12
13
|
export {
|
|
13
14
|
apiErrorNames,
|
|
@@ -20,14 +21,47 @@ export {
|
|
|
20
21
|
txSignErrorNames,
|
|
21
22
|
USER_DECLINED
|
|
22
23
|
} from "./cip30.js"
|
|
23
|
-
export type { Attempt, CompletionRequest, Receipt } from "./complete.js"
|
|
24
|
+
export type { Attempt, CompletionRequest, Progress, Receipt } from "./complete.js"
|
|
24
25
|
export { completeIntent } from "./complete.js"
|
|
25
26
|
export type { CompletionRefusal } from "./complete-error.js"
|
|
26
27
|
export { CompletionError, completionRefusals, slipErrorCodeFor as completionErrorCodeFor } from "./complete-error.js"
|
|
27
28
|
export type { ConnectedWallet, ConnectOptions } from "./connect.js"
|
|
28
29
|
export { connectWallet, networkIdFor } from "./connect.js"
|
|
30
|
+
export type { Derived } from "./derived.js"
|
|
29
31
|
export type { DiscoveredWallet, WalletHost } from "./discovery.js"
|
|
30
32
|
export { discoverWallets, findWallet, injectedUnder } from "./discovery.js"
|
|
33
|
+
export type { EffectsPanelProps } from "./effects-panel.js"
|
|
34
|
+
export { EffectsPanel } from "./effects-panel.js"
|
|
35
|
+
export type { ExchangeOptions, FetchedSlip, IntentRequest } from "./exchange.js"
|
|
36
|
+
export { ExchangeError, fetchSlip, requestIntent } from "./exchange.js"
|
|
37
|
+
export type { Explanation } from "./explain.js"
|
|
38
|
+
export {
|
|
39
|
+
assetLabel,
|
|
40
|
+
explainReason,
|
|
41
|
+
formatAda,
|
|
42
|
+
formatCountdown,
|
|
43
|
+
formatInstant,
|
|
44
|
+
formatQuantity,
|
|
45
|
+
secondsLeft,
|
|
46
|
+
shortened
|
|
47
|
+
} from "./explain.js"
|
|
48
|
+
export type { Amount, Evidence, Group, Headline, Mark, Row, Tone } from "./ledger.js"
|
|
49
|
+
export {
|
|
50
|
+
blockedLine,
|
|
51
|
+
describeCertificate,
|
|
52
|
+
evidenceOf,
|
|
53
|
+
groupsOf,
|
|
54
|
+
headlineOf,
|
|
55
|
+
isCollapsed,
|
|
56
|
+
isGrouped,
|
|
57
|
+
ledgerOf,
|
|
58
|
+
matchLine,
|
|
59
|
+
rawLines
|
|
60
|
+
} from "./ledger.js"
|
|
61
|
+
export type { Outcome, OutcomePanelProps, SlipReceiptProps } from "./outcome-panel.js"
|
|
62
|
+
export { networkNames, OutcomePanel, SlipReceipt } from "./outcome-panel.js"
|
|
63
|
+
export type { FieldError, ParameterFormProps } from "./parameter-form.js"
|
|
64
|
+
export { boundsOf, ParameterForm } from "./parameter-form.js"
|
|
31
65
|
export type { KnownWallet } from "./registry.js"
|
|
32
66
|
export { knownWallet, knownWallets } from "./registry.js"
|
|
33
67
|
export { asResolvedInput, asResolvedInputs } from "./resolve.js"
|
|
@@ -35,6 +69,8 @@ export type { SignedTransaction, SignRequest } from "./sign.js"
|
|
|
35
69
|
export { signTransaction, submitTransaction } from "./sign.js"
|
|
36
70
|
export type { SignRefusal } from "./sign-error.js"
|
|
37
71
|
export { signRefusals, SigningError, slipErrorCodeFor as signErrorCodeFor } from "./sign-error.js"
|
|
72
|
+
export type { SlipCardErrorProps, SlipCardProps, SlipSubmission } from "./slip-card.js"
|
|
73
|
+
export { SlipCard, SlipCardError, SlipCardSkeleton } from "./slip-card.js"
|
|
38
74
|
export { readWalletUtxos } from "./utxo.js"
|
|
39
75
|
export type { ConnectRefusal } from "./wallet-error.js"
|
|
40
76
|
export { connectRefusals, slipErrorCodeFor, WalletConnectError } from "./wallet-error.js"
|