@cardano-slips/flow 0.0.2 → 0.1.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 +27 -0
- package/dist/address.d.ts +13 -0
- package/dist/address.d.ts.map +1 -0
- package/dist/address.js +54 -0
- package/dist/address.js.map +1 -0
- package/dist/cip30.d.ts +66 -0
- package/dist/cip30.d.ts.map +1 -0
- package/dist/cip30.js +53 -0
- package/dist/cip30.js.map +1 -0
- package/dist/connect.d.ts +45 -0
- package/dist/connect.d.ts.map +1 -0
- package/dist/connect.js +73 -0
- package/dist/connect.js.map +1 -0
- package/dist/discovery.d.ts +37 -0
- package/dist/discovery.d.ts.map +1 -0
- package/dist/discovery.js +71 -0
- package/dist/discovery.js.map +1 -0
- package/dist/index.d.ts +12 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -1
- package/dist/index.js.map +1 -1
- package/dist/registry.d.ts +18 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +15 -0
- package/dist/registry.js.map +1 -0
- package/dist/tokens.css +184 -0
- package/dist/wallet-error.d.ts +48 -0
- package/dist/wallet-error.d.ts.map +1 -0
- package/dist/wallet-error.js +31 -0
- package/dist/wallet-error.js.map +1 -0
- package/package.json +6 -4
- package/src/address.ts +69 -0
- package/src/cip30.ts +94 -0
- package/src/connect.ts +136 -0
- package/src/discovery.ts +92 -0
- package/src/index.ts +12 -7
- package/src/registry.ts +32 -0
- package/src/tokens.css +184 -0
- package/src/wallet-error.ts +68 -0
package/dist/tokens.css
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Every colour, type role, step of spacing and radius the client uses, taken
|
|
3
|
+
* from `1 · Tokens` on the design sheet. This is the only file in the package
|
|
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.
|
|
7
|
+
*
|
|
8
|
+
* Scoped to a class, never `:root`: this ships into pages we do not own, and a
|
|
9
|
+
* package that writes tokens onto the document redefines whatever the host
|
|
10
|
+
* already called `--ink`. Put `slip-root` on the element that wraps the Slip.
|
|
11
|
+
*
|
|
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
|
|
14
|
+
* page's own stack stays where it is until something of ours asks for a token.
|
|
15
|
+
*
|
|
16
|
+
* Three families of surface, and the difference is the point:
|
|
17
|
+
* 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
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
.slip-root {
|
|
24
|
+
color-scheme: light;
|
|
25
|
+
|
|
26
|
+
/* 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);
|
|
33
|
+
|
|
34
|
+
/* Ink */
|
|
35
|
+
--ink: #121721;
|
|
36
|
+
--muted: #5c6a9c;
|
|
37
|
+
|
|
38
|
+
/*
|
|
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.
|
|
43
|
+
*/
|
|
44
|
+
--accent-fill: #728ef3;
|
|
45
|
+
--accent-text: #2f4bc4; /* links */
|
|
46
|
+
--accent-action: #4361e8; /* button background */
|
|
47
|
+
--accent-hover: #2f4bc4;
|
|
48
|
+
--accent-deep: #0c288d; /* headings */
|
|
49
|
+
--on-accent: #ffffff; /* anything drawn on --accent-action */
|
|
50
|
+
--focus: #4361e8;
|
|
51
|
+
|
|
52
|
+
/* 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);
|
|
60
|
+
|
|
61
|
+
/* 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);
|
|
66
|
+
|
|
67
|
+
/*
|
|
68
|
+
* 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.
|
|
71
|
+
*/
|
|
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);
|
|
87
|
+
|
|
88
|
+
/*
|
|
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.
|
|
92
|
+
*/
|
|
93
|
+
--dark-bg: #121721;
|
|
94
|
+
--dark-surface: rgba(255, 255, 255, 0.08);
|
|
95
|
+
--dark-text: #ffffff;
|
|
96
|
+
--dark-muted: #ccd6ff;
|
|
97
|
+
|
|
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;
|
|
102
|
+
|
|
103
|
+
/* Whole roles, so half a role cannot be applied without the other half. */
|
|
104
|
+
--type-h1: 700 44px/48px var(--font-display);
|
|
105
|
+
--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);
|
|
110
|
+
--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 */
|
|
113
|
+
|
|
114
|
+
/* Spacing, on a 4px base. */
|
|
115
|
+
--space-4: 4px;
|
|
116
|
+
--space-8: 8px;
|
|
117
|
+
--space-12: 12px;
|
|
118
|
+
--space-16: 16px;
|
|
119
|
+
--space-20: 20px;
|
|
120
|
+
--space-24: 24px;
|
|
121
|
+
--space-32: 32px;
|
|
122
|
+
--space-48: 48px;
|
|
123
|
+
|
|
124
|
+
--radius-chip: 6px;
|
|
125
|
+
--radius-control: 8px;
|
|
126
|
+
--radius-card: 12px;
|
|
127
|
+
}
|
|
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
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Why a wallet did not reach a usable connection. One error, a closed set of
|
|
3
|
+
* refusals, because the client renders a state per refusal and a state nothing
|
|
4
|
+
* can produce is a screen nobody sees.
|
|
5
|
+
*/
|
|
6
|
+
import { type ClientErrorCode } from "@cardano-slips/core";
|
|
7
|
+
import { type Cip30ApiError } from "./cip30.js";
|
|
8
|
+
export type ConnectRefusal =
|
|
9
|
+
/** Nothing is injected under this key. The wallet was uninstalled, or never there. */
|
|
10
|
+
"NotInjected"
|
|
11
|
+
/** Something is under the key that does not implement the connector. */
|
|
12
|
+
| "NotCip30"
|
|
13
|
+
/** The person declined the connection — CIP-30 `APIError` `-3`. Not a fault. */
|
|
14
|
+
| "Refused"
|
|
15
|
+
/** `enable()` rejected for another reason, or returned something that is not the API. */
|
|
16
|
+
| "EnableFailed"
|
|
17
|
+
/** A question the wallet did not answer, or answered with something that is not what it claims. */
|
|
18
|
+
| "Unreadable"
|
|
19
|
+
/** The wallet is on a different network from the one the Slip declares. */
|
|
20
|
+
| "WrongNetwork";
|
|
21
|
+
/**
|
|
22
|
+
* Exhaustive by type: adding a refusal above without listing it here fails to
|
|
23
|
+
* compile, and `test/connect.test.ts` fails if any of them stops being reachable.
|
|
24
|
+
*/
|
|
25
|
+
export declare const connectRefusals: Readonly<Record<ConnectRefusal, true>>;
|
|
26
|
+
/**
|
|
27
|
+
* Only the network disagreement has a spec code, because only it is a failure
|
|
28
|
+
* of the exchange the spec describes. The rest are states of a browser
|
|
29
|
+
* extension, and dressing them in a protocol code would say the endpoint
|
|
30
|
+
* answered when nothing was ever sent.
|
|
31
|
+
*/
|
|
32
|
+
export declare const slipErrorCodeFor: (refusal: ConnectRefusal) => ClientErrorCode | undefined;
|
|
33
|
+
declare const WalletConnectError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
|
|
34
|
+
readonly _tag: "WalletConnectError";
|
|
35
|
+
} & Readonly<A>;
|
|
36
|
+
/** `cip30` is the wallet's own `{ code, info }`, kept so a `-3` stays distinguishable from a crash. */
|
|
37
|
+
export declare class WalletConnectError extends WalletConnectError_base<{
|
|
38
|
+
readonly refusal: ConnectRefusal;
|
|
39
|
+
readonly key: string;
|
|
40
|
+
readonly detail: string;
|
|
41
|
+
readonly cip30?: Cip30ApiError;
|
|
42
|
+
}> {
|
|
43
|
+
get message(): string;
|
|
44
|
+
get code(): ClientErrorCode | undefined;
|
|
45
|
+
}
|
|
46
|
+
export declare const refuse: (refusal: ConnectRefusal, key: string, detail: string, cip30?: Cip30ApiError) => WalletConnectError;
|
|
47
|
+
export {};
|
|
48
|
+
//# sourceMappingURL=wallet-error.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wallet-error.d.ts","sourceRoot":"","sources":["../src/wallet-error.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EAAE,KAAK,eAAe,EAAE,MAAM,qBAAqB,CAAA;AAG1D,OAAO,EAAE,KAAK,aAAa,EAAE,MAAM,YAAY,CAAA;AAE/C,MAAM,MAAM,cAAc;AACxB,sFAAsF;AACpF,aAAa;AACf,wEAAwE;GACtE,UAAU;AACZ,gFAAgF;GAC9E,SAAS;AACX,yFAAyF;GACvF,cAAc;AAChB,mGAAmG;GACjG,YAAY;AACd,2EAA2E;GACzE,cAAc,CAAA;AAElB;;;GAGG;AACH,eAAO,MAAM,eAAe,EAAE,QAAQ,CAAC,MAAM,CAAC,cAAc,EAAE,IAAI,CAAC,CAOlE,CAAA;AAED;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,GAAI,SAAS,cAAc,KAAG,eAAe,GAAG,SACnB,CAAA;;;;AAE1D,uGAAuG;AACvG,qBAAa,kBAAmB,SAAQ,wBAAuC;IAC7E,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAA;IAChC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,KAAK,CAAC,EAAE,aAAa,CAAA;CAC/B,CAAC;IACA,IAAa,OAAO,IAAI,MAAM,CAE7B;IAED,IAAI,IAAI,IAAI,eAAe,GAAG,SAAS,CAEtC;CACF;AAED,eAAO,MAAM,MAAM,GACjB,SAAS,cAAc,EACvB,KAAK,MAAM,EACX,QAAQ,MAAM,EACd,QAAQ,aAAa,KACpB,kBAAiH,CAAA"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Data } from "effect";
|
|
2
|
+
/**
|
|
3
|
+
* Exhaustive by type: adding a refusal above without listing it here fails to
|
|
4
|
+
* compile, and `test/connect.test.ts` fails if any of them stops being reachable.
|
|
5
|
+
*/
|
|
6
|
+
export const connectRefusals = {
|
|
7
|
+
NotInjected: true,
|
|
8
|
+
NotCip30: true,
|
|
9
|
+
Refused: true,
|
|
10
|
+
EnableFailed: true,
|
|
11
|
+
Unreadable: true,
|
|
12
|
+
WrongNetwork: true
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Only the network disagreement has a spec code, because only it is a failure
|
|
16
|
+
* of the exchange the spec describes. The rest are states of a browser
|
|
17
|
+
* extension, and dressing them in a protocol code would say the endpoint
|
|
18
|
+
* answered when nothing was ever sent.
|
|
19
|
+
*/
|
|
20
|
+
export const slipErrorCodeFor = (refusal) => refusal === "WrongNetwork" ? "WRONG_NETWORK" : undefined;
|
|
21
|
+
/** `cip30` is the wallet's own `{ code, info }`, kept so a `-3` stays distinguishable from a crash. */
|
|
22
|
+
export class WalletConnectError extends Data.TaggedError("WalletConnectError") {
|
|
23
|
+
get message() {
|
|
24
|
+
return `${this.key}: ${this.detail}`;
|
|
25
|
+
}
|
|
26
|
+
get code() {
|
|
27
|
+
return slipErrorCodeFor(this.refusal);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export const refuse = (refusal, key, detail, cip30) => new WalletConnectError({ refusal, key, detail, ...(cip30 === undefined ? {} : { cip30 }) });
|
|
31
|
+
//# sourceMappingURL=wallet-error.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wallet-error.js","sourceRoot":"","sources":["../src/wallet-error.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA;AAkB7B;;;GAGG;AACH,MAAM,CAAC,MAAM,eAAe,GAA2C;IACrE,WAAW,EAAE,IAAI;IACjB,QAAQ,EAAE,IAAI;IACd,OAAO,EAAE,IAAI;IACb,YAAY,EAAE,IAAI;IAClB,UAAU,EAAE,IAAI;IAChB,YAAY,EAAE,IAAI;CACnB,CAAA;AAED;;;;;GAKG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,OAAuB,EAA+B,EAAE,CACvF,OAAO,KAAK,cAAc,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,SAAS,CAAA;AAE1D,uGAAuG;AACvG,MAAM,OAAO,kBAAmB,SAAQ,IAAI,CAAC,WAAW,CAAC,oBAAoB,CAK3E;IACA,IAAa,OAAO;QAClB,OAAO,GAAG,IAAI,CAAC,GAAG,KAAK,IAAI,CAAC,MAAM,EAAE,CAAA;IACtC,CAAC;IAED,IAAI,IAAI;QACN,OAAO,gBAAgB,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;IACvC,CAAC;CACF;AAED,MAAM,CAAC,MAAM,MAAM,GAAG,CACpB,OAAuB,EACvB,GAAW,EACX,MAAc,EACd,KAAqB,EACD,EAAE,CAAC,IAAI,kBAAkB,CAAC,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CAAA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cardano-slips/flow",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "The client side of Cardano Slips: CIP-30 orchestration, local balancing, and the React components that render a Slip and block a signature when its effects disagree",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cardano",
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
"types": "./dist/index.d.ts",
|
|
25
25
|
"default": "./dist/index.js"
|
|
26
26
|
},
|
|
27
|
+
"./tokens.css": "./dist/tokens.css",
|
|
27
28
|
"./package.json": "./package.json"
|
|
28
29
|
},
|
|
29
30
|
"types": "./dist/index.d.ts",
|
|
@@ -41,8 +42,9 @@
|
|
|
41
42
|
"react": "^19.0.0"
|
|
42
43
|
},
|
|
43
44
|
"dependencies": {
|
|
44
|
-
"
|
|
45
|
-
"@cardano-slips/
|
|
45
|
+
"effect": "^3.22.1",
|
|
46
|
+
"@cardano-slips/core": "^0.2.0",
|
|
47
|
+
"@cardano-slips/verifier": "^0.2.0"
|
|
46
48
|
},
|
|
47
49
|
"devDependencies": {
|
|
48
50
|
"@testing-library/react": "^16.3.2",
|
|
@@ -53,7 +55,7 @@
|
|
|
53
55
|
"typescript": "^6.0.3"
|
|
54
56
|
},
|
|
55
57
|
"scripts": {
|
|
56
|
-
"build": "tsc -b tsconfig.build.json",
|
|
58
|
+
"build": "tsc -b tsconfig.build.json && mkdir -p dist && cp src/tokens.css dist/tokens.css",
|
|
57
59
|
"typecheck": "tsc -b tsconfig.src.json && tsc -p tsconfig.test.json",
|
|
58
60
|
"test": "vitest run",
|
|
59
61
|
"lint": "eslint .",
|
package/src/address.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CIP-30 hands addresses back as hex; the endpoint is sent bech32, because that
|
|
3
|
+
* is what a `BuildRequest` carries and what a person can recognise. The
|
|
4
|
+
* conversion is also the second statement of network — the header byte says
|
|
5
|
+
* which chain the address is for, and a wallet whose `getNetworkId` disagrees
|
|
6
|
+
* with its own address has answered two ways.
|
|
7
|
+
*/
|
|
8
|
+
import { PaymentAddress } from "@cardano-slips/core"
|
|
9
|
+
import { encodeBech32 } from "@cardano-slips/verifier"
|
|
10
|
+
import { Either, Schema } from "effect"
|
|
11
|
+
|
|
12
|
+
/** CIP-30's `getNetworkId`: 1 is mainnet, 0 is every test network. */
|
|
13
|
+
export type NetworkId = 0 | 1
|
|
14
|
+
|
|
15
|
+
export type WalletAddress = {
|
|
16
|
+
readonly bech32: string
|
|
17
|
+
readonly networkId: NetworkId
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const decodePaymentAddress = Schema.decodeUnknownEither(PaymentAddress)
|
|
21
|
+
|
|
22
|
+
const refuse = (detail: string): Either.Either<never, string> => Either.left(detail)
|
|
23
|
+
|
|
24
|
+
const fromHex = (hex: string): Uint8Array | undefined => {
|
|
25
|
+
if (hex.length === 0 || hex.length % 2 !== 0 || !/^[0-9a-fA-F]+$/.test(hex)) return undefined
|
|
26
|
+
const bytes = new Uint8Array(hex.length / 2)
|
|
27
|
+
for (let index = 0; index < bytes.length; index++)
|
|
28
|
+
bytes[index] = Number.parseInt(hex.slice(index * 2, index * 2 + 2), 16)
|
|
29
|
+
return bytes
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* CIP-30 calls the value "the CBOR of the address" and wallets ship the address
|
|
34
|
+
* bytes bare. Both are accepted, and they cannot be confused: `0x58` as a first
|
|
35
|
+
* byte is a CBOR byte string of up to 255 bytes, and as an address header it
|
|
36
|
+
* would claim network 8, which does not exist.
|
|
37
|
+
*/
|
|
38
|
+
const unwrapCbor = (bytes: Uint8Array): Uint8Array =>
|
|
39
|
+
bytes.length > 2 && bytes[0] === 0x58 && bytes[1] === bytes.length - 2 ? bytes.subarray(2) : bytes
|
|
40
|
+
|
|
41
|
+
/** CIP-19: the header's high nibble is the address type, the low nibble the network. Types 0-7 can hold a payment. */
|
|
42
|
+
const LAST_PAYMENT_TYPE = 7
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Reads what CIP-30 returned. `Either` and not a throw: this runs on whatever a
|
|
46
|
+
* browser extension chose to send, and every refusal reaches a person as words.
|
|
47
|
+
*/
|
|
48
|
+
export const readWalletAddress = (hex: string): Either.Either<WalletAddress, string> => {
|
|
49
|
+
const raw = fromHex(hex.trim())
|
|
50
|
+
if (raw === undefined) return refuse(`${hex.slice(0, 16)}… is not hex`)
|
|
51
|
+
|
|
52
|
+
const bytes = unwrapCbor(raw)
|
|
53
|
+
const header = bytes[0]
|
|
54
|
+
|
|
55
|
+
const networkId = header & 0x0f
|
|
56
|
+
if (networkId !== 0 && networkId !== 1) return refuse(`the address header names network ${networkId}`)
|
|
57
|
+
|
|
58
|
+
const type = header >> 4
|
|
59
|
+
if (type > LAST_PAYMENT_TYPE) {
|
|
60
|
+
// A reward address (14, 15) or a Byron address (8) cannot receive change.
|
|
61
|
+
return refuse(`address type ${type} cannot hold a payment`)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const bech32 = encodeBech32(networkId === 1 ? "addr" : "addr_test", bytes)
|
|
65
|
+
return Either.match(decodePaymentAddress(bech32), {
|
|
66
|
+
onLeft: () => refuse(`${bech32.slice(0, 16)}… is not a payment address`),
|
|
67
|
+
onRight: () => Either.right({ bech32, networkId: networkId as NetworkId })
|
|
68
|
+
})
|
|
69
|
+
}
|
package/src/cip30.ts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The CIP-30 surface this client calls, declared here because evolution-sdk's
|
|
3
|
+
* `WalletApi` omits `getChangeAddress`, `getCollateral` and `getNetworkId`
|
|
4
|
+
* while the object a wallet actually hands back carries all three (ADR-0004).
|
|
5
|
+
* A typing we assert rather than one the SDK guarantees, so it is proved
|
|
6
|
+
* against a stubbed provider rather than trusted.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** Hex-encoded CBOR: the only form CIP-30 speaks, and the form `verifier` decodes. */
|
|
10
|
+
export type CborHex = string
|
|
11
|
+
|
|
12
|
+
export type Paginate = { readonly page: number; readonly limit: number }
|
|
13
|
+
|
|
14
|
+
export type SignedData = { readonly signature: CborHex; readonly key: CborHex }
|
|
15
|
+
|
|
16
|
+
export interface Cip30Api {
|
|
17
|
+
readonly getNetworkId: () => Promise<number>
|
|
18
|
+
readonly getChangeAddress: () => Promise<CborHex>
|
|
19
|
+
readonly getUsedAddresses: (paginate?: Paginate) => Promise<ReadonlyArray<CborHex>>
|
|
20
|
+
readonly getUnusedAddresses: () => Promise<ReadonlyArray<CborHex>>
|
|
21
|
+
readonly getRewardAddresses: () => Promise<ReadonlyArray<CborHex>>
|
|
22
|
+
readonly getBalance: () => Promise<CborHex>
|
|
23
|
+
/** `null` where the wallet has nothing to give, per CIP-30. */
|
|
24
|
+
readonly getUtxos: (amount?: CborHex, paginate?: Paginate) => Promise<ReadonlyArray<CborHex> | null>
|
|
25
|
+
readonly getCollateral: (params?: { readonly amount: CborHex }) => Promise<ReadonlyArray<CborHex> | null>
|
|
26
|
+
/** Returns a **witness set**, never a signed transaction — invariant 4 is CIP-30's own shape. */
|
|
27
|
+
readonly signTx: (tx: CborHex, partialSign?: boolean) => Promise<CborHex>
|
|
28
|
+
readonly signData: (address: CborHex, payload: CborHex) => Promise<SignedData>
|
|
29
|
+
readonly submitTx: (tx: CborHex) => Promise<string>
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** What an extension injects at `window.cardano[key]`, before anyone calls `enable()`. */
|
|
33
|
+
export interface Cip30Provider {
|
|
34
|
+
readonly apiVersion: string
|
|
35
|
+
readonly name: string
|
|
36
|
+
readonly icon: string
|
|
37
|
+
readonly supportedExtensions?: ReadonlyArray<{ readonly cip: number }>
|
|
38
|
+
readonly enable: (extensions?: ReadonlyArray<{ readonly cip: number }>) => Promise<Cip30Api>
|
|
39
|
+
readonly isEnabled: () => Promise<boolean>
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const isFunction = (value: unknown): boolean => typeof value === "function"
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Shape, not name: `window.cardano` is a namespace anything may write to, and a
|
|
46
|
+
* key that does not answer these four is not a connector whatever it is called.
|
|
47
|
+
*/
|
|
48
|
+
export const isCip30Provider = (value: unknown): value is Cip30Provider => {
|
|
49
|
+
if (typeof value !== "object" || value === null) return false
|
|
50
|
+
const candidate = value as Record<string, unknown>
|
|
51
|
+
return (
|
|
52
|
+
isFunction(candidate.enable) &&
|
|
53
|
+
isFunction(candidate.isEnabled) &&
|
|
54
|
+
typeof candidate.name === "string" &&
|
|
55
|
+
typeof candidate.apiVersion === "string"
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** The methods we call. A provider may enable and still not answer them. */
|
|
60
|
+
const apiMethods = ["getNetworkId", "getChangeAddress", "getUsedAddresses", "getUtxos", "signTx", "submitTx"] as const
|
|
61
|
+
|
|
62
|
+
export const isCip30Api = (value: unknown): value is Cip30Api => {
|
|
63
|
+
if (typeof value !== "object" || value === null) return false
|
|
64
|
+
const candidate = value as Record<string, unknown>
|
|
65
|
+
return apiMethods.every((method) => isFunction(candidate[method]))
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** CIP-30's `APIError`, kept whole: the numeric code is what says a person declined. */
|
|
69
|
+
export type Cip30ApiError = { readonly code: number; readonly info: string }
|
|
70
|
+
|
|
71
|
+
/** The four `APIError` codes CIP-30 defines. Anything else is passed through as its number. */
|
|
72
|
+
export const apiErrorNames: Readonly<Record<number, string>> = {
|
|
73
|
+
[-1]: "InvalidRequest",
|
|
74
|
+
[-2]: "InternalError",
|
|
75
|
+
[-3]: "Refused",
|
|
76
|
+
[-4]: "AccountChange"
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** `-3` is the person saying no, which is not a fault and must not be shown as one. */
|
|
80
|
+
export const REFUSED = -3
|
|
81
|
+
|
|
82
|
+
/** A wallet rejects with a plain `{ code, info }` object, not an `Error`. */
|
|
83
|
+
export const readApiError = (cause: unknown): Cip30ApiError | undefined => {
|
|
84
|
+
if (typeof cause !== "object" || cause === null) return undefined
|
|
85
|
+
const candidate = cause as Record<string, unknown>
|
|
86
|
+
if (typeof candidate.code !== "number") return undefined
|
|
87
|
+
return { code: candidate.code, info: typeof candidate.info === "string" ? candidate.info : "" }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export const describeApiError = (error: Cip30ApiError): string => {
|
|
91
|
+
const name = apiErrorNames[error.code]
|
|
92
|
+
const label = name === undefined ? `code ${error.code}` : name
|
|
93
|
+
return error.info === "" ? label : `${label}: ${error.info}`
|
|
94
|
+
}
|
package/src/connect.ts
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `enable()` and the three answers a connection is not usable without: the API
|
|
3
|
+
* object, the network, and the change address. Everything downstream of here —
|
|
4
|
+
* UTxOs, balancing, signing — goes through evolution-sdk with this API object
|
|
5
|
+
* (ADR-0004).
|
|
6
|
+
*/
|
|
7
|
+
import { type Network } from "@cardano-slips/core"
|
|
8
|
+
import { Effect } from "effect"
|
|
9
|
+
|
|
10
|
+
import { type NetworkId, readWalletAddress } from "./address.js"
|
|
11
|
+
import { type Cip30Api, describeApiError, isCip30Api, readApiError, REFUSED } from "./cip30.js"
|
|
12
|
+
import { type DiscoveredWallet, findWallet, injectedUnder } from "./discovery.js"
|
|
13
|
+
import { refuse, type WalletConnectError } from "./wallet-error.js"
|
|
14
|
+
|
|
15
|
+
export type ConnectOptions = {
|
|
16
|
+
/** The network the Slip declares. The wallet is held to it, not asked for it. */
|
|
17
|
+
readonly network: Network
|
|
18
|
+
/** Where `cardano` lives. Defaults to the global object, so a test never needs one. */
|
|
19
|
+
readonly host?: unknown
|
|
20
|
+
/** CIP extensions to ask for. A wallet may grant fewer, and none of ours are required. */
|
|
21
|
+
readonly extensions?: ReadonlyArray<{ readonly cip: number }>
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type ConnectedWallet = {
|
|
25
|
+
readonly wallet: DiscoveredWallet
|
|
26
|
+
readonly api: Cip30Api
|
|
27
|
+
/** The Slip's network, now known to be the wallet's as well. */
|
|
28
|
+
readonly network: Network
|
|
29
|
+
readonly networkId: NetworkId
|
|
30
|
+
/** Bech32, ready for a `BuildRequest`. */
|
|
31
|
+
readonly changeAddress: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* A CIP-19 address separates mainnet from testnet and no further, so this is
|
|
36
|
+
* every network id there is — preprod and preview are one value here. Telling
|
|
37
|
+
* them apart is exactly why a Slip states its network by name (`core/types.ts`).
|
|
38
|
+
*/
|
|
39
|
+
export const networkIdFor = (network: Network): NetworkId => (network === "mainnet" ? 1 : 0)
|
|
40
|
+
|
|
41
|
+
const networkNames: Readonly<Record<NetworkId, string>> = { 0: "a test network", 1: "mainnet" }
|
|
42
|
+
|
|
43
|
+
const call = <A>(key: string, method: string, run: () => Promise<A>): Effect.Effect<A, WalletConnectError> =>
|
|
44
|
+
Effect.tryPromise({
|
|
45
|
+
try: run,
|
|
46
|
+
catch: (cause) => {
|
|
47
|
+
const cip30 = readApiError(cause)
|
|
48
|
+
return refuse(
|
|
49
|
+
"Unreadable",
|
|
50
|
+
key,
|
|
51
|
+
`${method} failed: ${cip30 === undefined ? String(cause) : describeApiError(cip30)}`,
|
|
52
|
+
cip30
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Connects, or says why not. The network check happens here rather than in a
|
|
59
|
+
* component because it decides whether there is a connection at all: a wallet
|
|
60
|
+
* on another chain cannot sign this Slip's transaction, and letting the flow
|
|
61
|
+
* continue only moves the failure to where it costs a person more.
|
|
62
|
+
*/
|
|
63
|
+
export const connectWallet = (
|
|
64
|
+
key: string,
|
|
65
|
+
options: ConnectOptions
|
|
66
|
+
): Effect.Effect<ConnectedWallet, WalletConnectError> =>
|
|
67
|
+
Effect.gen(function* () {
|
|
68
|
+
const host = options.host ?? globalThis
|
|
69
|
+
const wallet = findWallet(key, host)
|
|
70
|
+
if (wallet === undefined) {
|
|
71
|
+
return yield* Effect.fail(
|
|
72
|
+
injectedUnder(key, host) === undefined
|
|
73
|
+
? refuse("NotInjected", key, "no wallet is installed under this name")
|
|
74
|
+
: refuse("NotCip30", key, "what is installed under this name does not implement CIP-30")
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const api = yield* Effect.tryPromise({
|
|
79
|
+
try: () => wallet.provider.enable(options.extensions),
|
|
80
|
+
catch: (cause) => {
|
|
81
|
+
const cip30 = readApiError(cause)
|
|
82
|
+
if (cip30?.code === REFUSED) {
|
|
83
|
+
return refuse("Refused", key, "the connection was declined in the wallet", cip30)
|
|
84
|
+
}
|
|
85
|
+
const detail = cip30 === undefined ? String(cause) : describeApiError(cip30)
|
|
86
|
+
return refuse("EnableFailed", key, `the wallet could not be connected: ${detail}`, cip30)
|
|
87
|
+
}
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
// `enable()` is typed to return the API and is under no obligation to.
|
|
91
|
+
if (!isCip30Api(api)) {
|
|
92
|
+
return yield* Effect.fail(refuse("NotCip30", key, "the wallet connected without returning a CIP-30 API"))
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const reported = yield* call(key, "getNetworkId", () => api.getNetworkId())
|
|
96
|
+
if (reported !== 0 && reported !== 1) {
|
|
97
|
+
return yield* Effect.fail(refuse("Unreadable", key, `getNetworkId answered ${String(reported)}`))
|
|
98
|
+
}
|
|
99
|
+
const networkId: NetworkId = reported === 1 ? 1 : 0
|
|
100
|
+
|
|
101
|
+
const hex = yield* call(key, "getChangeAddress", () => api.getChangeAddress())
|
|
102
|
+
const address = yield* readWalletAddress(hex).pipe(
|
|
103
|
+
Effect.mapError((detail) => refuse("Unreadable", key, `getChangeAddress answered ${detail}`))
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
// Two answers about one wallet. A wallet that gives both cannot be taken at
|
|
107
|
+
// either word, and the disagreement is not the person's to resolve.
|
|
108
|
+
if (address.networkId !== networkId) {
|
|
109
|
+
return yield* Effect.fail(
|
|
110
|
+
refuse(
|
|
111
|
+
"Unreadable",
|
|
112
|
+
key,
|
|
113
|
+
`the wallet reports ${networkNames[networkId]} and its change address is on ${networkNames[address.networkId]}`
|
|
114
|
+
)
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const expected = networkIdFor(options.network)
|
|
119
|
+
if (networkId !== expected) {
|
|
120
|
+
return yield* Effect.fail(
|
|
121
|
+
refuse(
|
|
122
|
+
"WrongNetwork",
|
|
123
|
+
key,
|
|
124
|
+
`this Slip is on ${options.network} and the wallet is on ${networkNames[networkId]}`
|
|
125
|
+
)
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
wallet,
|
|
131
|
+
api,
|
|
132
|
+
network: options.network,
|
|
133
|
+
networkId,
|
|
134
|
+
changeAddress: address.bech32
|
|
135
|
+
}
|
|
136
|
+
})
|