cruzo-web3 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.
Files changed (36) hide show
  1. package/README.md +242 -0
  2. package/lib/components/icons/copy-icon.component.ts +14 -0
  3. package/lib/components/web3-signer/web3-signer.component.module.css +106 -0
  4. package/lib/components/web3-signer/web3-signer.component.ts +320 -0
  5. package/lib/components/web3-signing/web3-signing.component.module.css +36 -0
  6. package/lib/components/web3-signing/web3-signing.component.ts +239 -0
  7. package/lib/components/web3-wallet-picker/web3-wallet-picker.bucket.ts +22 -0
  8. package/lib/components/web3-wallet-picker/web3-wallet-picker.component.module.css +62 -0
  9. package/lib/components/web3-wallet-picker/web3-wallet-picker.component.ts +171 -0
  10. package/lib/crypto/account-signature.ts +175 -0
  11. package/lib/crypto/decode-bytes.ts +93 -0
  12. package/lib/crypto/ecdsa-signature.ts +45 -0
  13. package/lib/crypto/keccak256.ts +117 -0
  14. package/lib/crypto/secp256k1-verify.ts +232 -0
  15. package/lib/crypto/sha256.ts +8 -0
  16. package/lib/crypto/verify-signature.ts +54 -0
  17. package/lib/env.d.ts +4 -0
  18. package/lib/errors/web3-error.ts +49 -0
  19. package/lib/index.ts +20 -0
  20. package/lib/providers/eip1193.provider.ts +152 -0
  21. package/lib/providers/injected.ts +71 -0
  22. package/lib/providers/message-bytes.ts +13 -0
  23. package/lib/providers/solana.provider.ts +116 -0
  24. package/lib/providers/tonconnect.provider.ts +161 -0
  25. package/lib/providers/tron.provider.ts +142 -0
  26. package/lib/providers/wallet-transport.ts +1 -0
  27. package/lib/providers/wallet.ts +92 -0
  28. package/lib/providers/walletconnect-ethereum.provider.ts +49 -0
  29. package/lib/pub-key.ts +144 -0
  30. package/lib/signing-url.ts +334 -0
  31. package/lib/types/signer-state.ts +10 -0
  32. package/lib/types/web3-types.ts +27 -0
  33. package/lib/utils/format-pub-key.ts +18 -0
  34. package/lib/web3-wallet.ts +31 -0
  35. package/lib/web3.service.ts +419 -0
  36. package/package.json +58 -0
@@ -0,0 +1,334 @@
1
+ import { isPubKey } from "./pub-key";
2
+ import type { WalletKind } from "./providers/wallet";
3
+ import type { WalletTransport } from "./providers/wallet-transport";
4
+ import type { PubKey, PubKeyAlgorithm, PubKeyEncoding } from "./types/web3-types";
5
+ import type { SignerState, SignerWallet } from "./types/signer-state";
6
+ import { isCustomWallet } from "./web3-wallet";
7
+
8
+ export const SIGNING_QUERY_KEY = "s";
9
+ export const PAYLOAD_QUERY_KEY = "p";
10
+ export const MAX_SIGNING_PAYLOAD_LENGTH = 512;
11
+
12
+ export type SigningUrlSnapshot = Record<string, SignerState>;
13
+
14
+ type CompactPubKey = [string, string, string];
15
+ type CompactSigner = [string, string, 0 | 1, CompactPubKey | null];
16
+ type CompactSnapshot = Record<string, CompactSigner>;
17
+
18
+ const SIGNER_ID_TO_KEY: Record<string, string> = {
19
+ signer1: "1",
20
+ signer2: "2",
21
+ };
22
+
23
+ const SIGNER_KEY_TO_ID: Record<string, string> = {
24
+ "1": "signer1",
25
+ "2": "signer2",
26
+ };
27
+
28
+ const WALLET_KIND_TO_KEY: Record<WalletKind, string> = {
29
+ ethereum: "e",
30
+ ton: "t",
31
+ solana: "s",
32
+ tron: "r",
33
+ };
34
+
35
+ const WALLET_KEY_TO_KIND = Object.fromEntries(
36
+ Object.entries(WALLET_KIND_TO_KEY).map(([kind, key]) => [key, kind]),
37
+ ) as Record<string, WalletKind>;
38
+
39
+ const TRANSPORT_TO_KEY: Record<WalletTransport, string> = {
40
+ extension: "x",
41
+ app: "a",
42
+ auto: "u",
43
+ };
44
+
45
+ const TRANSPORT_KEY_TO_MODE = Object.fromEntries(
46
+ Object.entries(TRANSPORT_TO_KEY).map(([mode, key]) => [key, mode]),
47
+ ) as Record<string, WalletTransport>;
48
+
49
+ const CUSTOM_WALLET_TRANSPORT_KEY = "c";
50
+
51
+ const ALGO_TO_KEY: Record<PubKeyAlgorithm, string> = {
52
+ secp256k1: "k",
53
+ Ed25519: "e",
54
+ sr25519: "r",
55
+ };
56
+
57
+ const ALGO_KEY_TO_TYPE = Object.fromEntries(
58
+ Object.entries(ALGO_TO_KEY).map(([type, key]) => [key, type]),
59
+ ) as Record<string, PubKeyAlgorithm>;
60
+
61
+ const ENC_TO_KEY: Record<PubKeyEncoding, string> = {
62
+ hex: "h",
63
+ base64: "b",
64
+ base58: "8",
65
+ };
66
+
67
+ const ENC_KEY_TO_ENCODING = Object.fromEntries(
68
+ Object.entries(ENC_TO_KEY).map(([enc, key]) => [key, enc]),
69
+ ) as Record<string, PubKeyEncoding>;
70
+
71
+ function hasSignerData(state: SignerState) {
72
+ return !!(state.pubKey || state.signed || state.wallet);
73
+ }
74
+
75
+ function isSignerState(value: unknown): value is SignerState {
76
+ if (!value || typeof value !== "object") return false;
77
+
78
+ const state = value as SignerState;
79
+
80
+ if (typeof state.signed !== "boolean") return false;
81
+ if (state.pubKey !== null && !isPubKey(state.pubKey)) return false;
82
+
83
+ if (state.wallet != null) {
84
+ if (typeof state.wallet !== "object") return false;
85
+
86
+ if (isCustomWallet(state.wallet)) {
87
+ if (!state.wallet.providerId) return false;
88
+ } else if (
89
+ !WALLET_KIND_TO_KEY[state.wallet.kind] ||
90
+ !TRANSPORT_TO_KEY[state.wallet.transport]
91
+ ) {
92
+ return false;
93
+ }
94
+ }
95
+
96
+ return true;
97
+ }
98
+
99
+ function toBase64Url(text: string) {
100
+ const bytes = new TextEncoder().encode(text);
101
+ let binary = "";
102
+
103
+ for (const byte of bytes) binary += String.fromCharCode(byte);
104
+
105
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
106
+ }
107
+
108
+ function fromBase64Url(value: string) {
109
+ const padded = value.replace(/-/g, "+").replace(/_/g, "/");
110
+ const pad = padded.length % 4 === 0 ? "" : "=".repeat(4 - (padded.length % 4));
111
+ const binary = atob(padded + pad);
112
+ const bytes = new Uint8Array(binary.length);
113
+
114
+ for (let index = 0; index < binary.length; index++) {
115
+ bytes[index] = binary.charCodeAt(index);
116
+ }
117
+
118
+ return new TextDecoder().decode(bytes);
119
+ }
120
+
121
+ function compactPubKey(pubKey: PubKey): CompactPubKey {
122
+ return [ALGO_TO_KEY[pubKey.type], pubKey.value, ENC_TO_KEY[pubKey.encoding]];
123
+ }
124
+
125
+ function expandPubKey(value: CompactPubKey | null): PubKey | null {
126
+ if (!value) return null;
127
+
128
+ const [typeKey, keyValue, encKey] = value;
129
+ const type = ALGO_KEY_TO_TYPE[typeKey];
130
+ const encoding = ENC_KEY_TO_ENCODING[encKey];
131
+
132
+ if (!type || !encoding) return null;
133
+
134
+ return { type, value: keyValue, encoding };
135
+ }
136
+
137
+ function compactWallet(wallet: SignerWallet | null | undefined): [string, string] {
138
+ if (!wallet) return ["", ""];
139
+
140
+ if (isCustomWallet(wallet)) {
141
+ return [wallet.providerId, CUSTOM_WALLET_TRANSPORT_KEY];
142
+ }
143
+
144
+ return [WALLET_KIND_TO_KEY[wallet.kind], TRANSPORT_TO_KEY[wallet.transport]];
145
+ }
146
+
147
+ function expandWallet(kindKey: string, transportKey: string): SignerWallet | null {
148
+ if (transportKey === CUSTOM_WALLET_TRANSPORT_KEY) {
149
+ if (!kindKey) return null;
150
+
151
+ return { type: "custom", providerId: kindKey };
152
+ }
153
+
154
+ const kind = WALLET_KEY_TO_KIND[kindKey];
155
+ const transport = TRANSPORT_KEY_TO_MODE[transportKey];
156
+
157
+ if (!kind || !transport) return null;
158
+
159
+ return { kind, transport };
160
+ }
161
+
162
+ function compactSnapshot(snapshot: SigningUrlSnapshot): CompactSnapshot {
163
+ const compact: CompactSnapshot = {};
164
+
165
+ for (const [id, state] of Object.entries(snapshot)) {
166
+ if (!hasSignerData(state)) continue;
167
+
168
+ const signerKey = SIGNER_ID_TO_KEY[id] ?? id;
169
+ const [kindKey, transportKey] = compactWallet(state.wallet);
170
+ const pubKey = state.pubKey ? compactPubKey(state.pubKey) : null;
171
+
172
+ compact[signerKey] = [kindKey, transportKey, state.signed ? 1 : 0, pubKey];
173
+ }
174
+
175
+ return compact;
176
+ }
177
+
178
+ function expandSnapshot(compact: CompactSnapshot): SigningUrlSnapshot {
179
+ const snapshot: SigningUrlSnapshot = {};
180
+
181
+ for (const [signerKey, entry] of Object.entries(compact)) {
182
+ if (!Array.isArray(entry) || entry.length !== 4) continue;
183
+
184
+ const [kindKey, transportKey, signedFlag, pubKeyValue] = entry;
185
+
186
+ if (signedFlag !== 0 && signedFlag !== 1) continue;
187
+
188
+ const id = SIGNER_KEY_TO_ID[signerKey] ?? signerKey;
189
+ const wallet = expandWallet(String(kindKey), String(transportKey));
190
+ const pubKey = expandPubKey(pubKeyValue);
191
+
192
+ snapshot[id] = {
193
+ pubKey,
194
+ signed: signedFlag === 1,
195
+ wallet,
196
+ };
197
+ }
198
+
199
+ return snapshot;
200
+ }
201
+
202
+ function encodeSigningParam(snapshot: SigningUrlSnapshot): string {
203
+ const compact = compactSnapshot(snapshot);
204
+
205
+ if (!Object.keys(compact).length) return "";
206
+
207
+ return toBase64Url(JSON.stringify(compact));
208
+ }
209
+
210
+ function decodeSigningParam(raw: string): SigningUrlSnapshot | null {
211
+ if (!raw) return null;
212
+
213
+ if (raw.startsWith("{")) {
214
+ return decodeLegacySigningParam(raw);
215
+ }
216
+
217
+ try {
218
+ const json = fromBase64Url(raw);
219
+ const parsed = JSON.parse(json) as CompactSnapshot;
220
+
221
+ if (!parsed || typeof parsed !== "object") return null;
222
+
223
+ const snapshot = expandSnapshot(parsed);
224
+
225
+ return Object.keys(snapshot).length ? snapshot : null;
226
+ } catch {
227
+ return decodeLegacySigningParam(raw);
228
+ }
229
+ }
230
+
231
+ function decodeLegacySigningParam(raw: string): SigningUrlSnapshot | null {
232
+ try {
233
+ const parsed = JSON.parse(raw) as unknown;
234
+
235
+ if (!parsed || typeof parsed !== "object") return null;
236
+
237
+ const snapshot: SigningUrlSnapshot = {};
238
+
239
+ for (const [id, state] of Object.entries(parsed)) {
240
+ if (isSignerState(state)) snapshot[id] = state;
241
+ }
242
+
243
+ return Object.keys(snapshot).length ? snapshot : null;
244
+ } catch {
245
+ return null;
246
+ }
247
+ }
248
+
249
+ export function clampSigningPayload(payload: string): string {
250
+ return payload.slice(0, MAX_SIGNING_PAYLOAD_LENGTH);
251
+ }
252
+
253
+ function encodePayloadParam(payload: string | null | undefined): string {
254
+ if (!payload) return "";
255
+
256
+ const clamped = clampSigningPayload(payload);
257
+
258
+ if (!clamped) return "";
259
+
260
+ return toBase64Url(clamped);
261
+ }
262
+
263
+ export function readPayloadFromUrl(search: string): string | null {
264
+ const qs = search.startsWith("?") ? search.slice(1) : search;
265
+
266
+ if (!qs) return null;
267
+
268
+ const raw = new URLSearchParams(qs).get(PAYLOAD_QUERY_KEY);
269
+
270
+ if (!raw) return null;
271
+
272
+ try {
273
+ return clampSigningPayload(fromBase64Url(raw));
274
+ } catch {
275
+ return null;
276
+ }
277
+ }
278
+
279
+ export function readSigningUrl(search: string): SigningUrlSnapshot | null {
280
+ const qs = search.startsWith("?") ? search.slice(1) : search;
281
+
282
+ if (!qs) return null;
283
+
284
+ const raw = new URLSearchParams(qs).get(SIGNING_QUERY_KEY);
285
+
286
+ if (!raw) return null;
287
+
288
+ return decodeSigningParam(raw);
289
+ }
290
+
291
+ export function buildSearchWithSigning(
292
+ search: string,
293
+ snapshot: SigningUrlSnapshot,
294
+ payload?: string | null,
295
+ ): string {
296
+ const params = new URLSearchParams(search.startsWith("?") ? search.slice(1) : search);
297
+ const encoded = encodeSigningParam(snapshot);
298
+
299
+ if (encoded) {
300
+ params.set(SIGNING_QUERY_KEY, encoded);
301
+ } else {
302
+ params.delete(SIGNING_QUERY_KEY);
303
+ }
304
+
305
+ const payloadEncoded = encodePayloadParam(payload);
306
+
307
+ if (payloadEncoded) {
308
+ params.set(PAYLOAD_QUERY_KEY, payloadEncoded);
309
+ } else {
310
+ params.delete(PAYLOAD_QUERY_KEY);
311
+ }
312
+
313
+ const query = params.toString();
314
+
315
+ return query ? `?${query}` : "";
316
+ }
317
+
318
+ export function buildSigningPageUrl(
319
+ pathname: string,
320
+ search: string,
321
+ snapshot: SigningUrlSnapshot,
322
+ hashMode = false,
323
+ payload?: string | null,
324
+ ): string {
325
+ const nextSearch = buildSearchWithSigning(search, snapshot, payload);
326
+ const origin = window.location.origin;
327
+
328
+ if (hashMode) {
329
+ const hashRoute = `#/${pathname.replace(/^\//, "")}${nextSearch}`;
330
+ return `${origin}${window.location.pathname}${window.location.search}${hashRoute}`;
331
+ }
332
+
333
+ return `${origin}${pathname}${nextSearch}`;
334
+ }
@@ -0,0 +1,10 @@
1
+ import type { PubKey } from "./web3-types";
2
+ import type { Web3WalletTarget } from "../web3-wallet";
3
+
4
+ export type SignerWallet = Web3WalletTarget;
5
+
6
+ export type SignerState = {
7
+ pubKey: PubKey | null;
8
+ signed: boolean;
9
+ wallet?: SignerWallet | null;
10
+ };
@@ -0,0 +1,27 @@
1
+ export type PubKeyAlgorithm = "secp256k1" | "Ed25519" | "sr25519";
2
+
3
+ export type PubKeyEncoding = "hex" | "base64" | "base58";
4
+
5
+ export type SignableMessage = string | Uint8Array;
6
+
7
+ export type Web3ProviderId = "eip1193" | "solana" | "tron" | "tonconnect" | (string & {});
8
+
9
+ export interface PubKey {
10
+ type: PubKeyAlgorithm;
11
+ value: string;
12
+ encoding: PubKeyEncoding;
13
+ }
14
+
15
+ export interface VerifySignedContentOptions {
16
+ signatureEncoding?: PubKeyEncoding;
17
+ /** EIP-191 prefix + keccak256 before secp256k1 verify. Default: true for secp256k1. */
18
+ evmPersonalSign?: boolean;
19
+ }
20
+
21
+ export interface Web3Provider {
22
+ readonly id: Web3ProviderId;
23
+
24
+ connect(): Promise<PubKey>;
25
+ disconnect(): Promise<void>;
26
+ signMessage(message: SignableMessage): Promise<string>;
27
+ }
@@ -0,0 +1,18 @@
1
+ import type { PubKey } from "../types/web3-types";
2
+
3
+ export function pubKeyToText(pubKey: PubKey | null): string {
4
+ if (!pubKey?.value) return "—";
5
+
6
+ const value = pubKey.value;
7
+
8
+ if (pubKey.type === "secp256k1" && value.length === 40 && !value.startsWith("0x")) {
9
+ return `0x${value}`;
10
+ }
11
+
12
+ return value;
13
+ }
14
+
15
+ /** @deprecated Use pubKeyToText for full key display */
16
+ export function formatPubKey(pubKey: PubKey | null): string {
17
+ return pubKeyToText(pubKey);
18
+ }
@@ -0,0 +1,31 @@
1
+ import type { WalletKind } from "./providers/wallet";
2
+ import type { WalletTransport } from "./providers/wallet-transport";
3
+
4
+ export type Web3WalletSlot = {
5
+ kind: WalletKind;
6
+ transport: WalletTransport;
7
+ };
8
+
9
+ export type Web3BuiltinWallet = {
10
+ kind: WalletKind;
11
+ transport: WalletTransport;
12
+ };
13
+
14
+ export type Web3CustomWallet = {
15
+ type: "custom";
16
+ providerId: string;
17
+ };
18
+
19
+ export type Web3WalletTarget = Web3BuiltinWallet | Web3CustomWallet;
20
+
21
+ export function isCustomWallet(
22
+ wallet: Web3WalletTarget | null | undefined,
23
+ ): wallet is Web3CustomWallet {
24
+ return !!wallet && "type" in wallet && wallet.type === "custom";
25
+ }
26
+
27
+ export function isBuiltinWallet(
28
+ wallet: Web3WalletTarget | null | undefined,
29
+ ): wallet is Web3BuiltinWallet {
30
+ return !!wallet && !isCustomWallet(wallet);
31
+ }