cruzo-web3 0.1.1 → 0.1.3

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 CHANGED
@@ -8,11 +8,6 @@ Web3 addon for [cruzo](https://github.com/MaratBektemirov/cruzo): wallet connect
8
8
  npm install cruzo cruzo-web3
9
9
  ```
10
10
 
11
- For mobile wallets, also install optional peer dependencies:
12
-
13
- ```bash
14
- npm install @tonconnect/sdk @tonconnect/ui @walletconnect/ethereum-provider
15
- ```
16
11
 
17
12
  ## Public API
18
13
 
@@ -83,12 +78,14 @@ Without a Project ID, the **Mobile wallet (WalletConnect)** option stays disable
83
78
  TON wallets (extension and mobile app) require a public `tonconnect-manifest.json`.
84
79
 
85
80
  1. Add the manifest file to your site root (must be reachable over HTTPS in production).
86
- 2. Point `web3Service` to it:
81
+ 2. Point `web3Service` to the absolute manifest URL at app bootstrap:
87
82
 
88
83
  ```ts
89
- web3Service.setTonManifestUrl(
90
- new URL("/tonconnect-manifest.json", window.location.href).href
91
- );
84
+ import { web3Service } from "cruzo-web3";
85
+
86
+ const tonManifestUrl = new URL("/tonconnect-manifest.json", window.location.href).href;
87
+
88
+ web3Service.setTonManifestUrl(tonManifestUrl);
92
89
  ```
93
90
 
94
91
  Example manifest:
@@ -16,6 +16,7 @@ import {
16
16
  import type { SignerState, SignerWallet } from "../../types/signer-state";
17
17
  import { buildSigningPageUrl } from "../../signing-url";
18
18
  import { pubKeyToText } from "../../utils/format-pub-key";
19
+ import { formatWalletError, isWalletUserCancellation } from "../../utils/wallet-error";
19
20
  import { web3Service } from "../../web3.service";
20
21
  import { isCustomWallet } from "../../web3-wallet";
21
22
 
@@ -309,7 +310,9 @@ export class Web3SignerComponent extends AbstractComponent<SignerConfig, any, Si
309
310
 
310
311
  action()
311
312
  .catch((error: unknown) => {
312
- this.error$.update(error instanceof Error ? error.message : String(error));
313
+ if (isWalletUserCancellation(error)) return;
314
+
315
+ this.error$.update(formatWalletError(error));
313
316
  })
314
317
  .finally(() => {
315
318
  this.busy$.update(false);
@@ -85,7 +85,6 @@ export class Web3SigningComponent extends AbstractComponent {
85
85
  connectedCallback() {
86
86
  componentsRegistryService.connectBucket(this.innerBucket);
87
87
  super.connectedCallback();
88
- this.ensureTonManifest();
89
88
  this.updateWalletHint();
90
89
  this.setupUrlSync();
91
90
  }
@@ -218,12 +217,6 @@ export class Web3SigningComponent extends AbstractComponent {
218
217
  return { pubKey: null, signed: false, wallet: null };
219
218
  }
220
219
 
221
- private ensureTonManifest() {
222
- if (web3Service.getTonManifestUrl()) return;
223
-
224
- web3Service.setTonManifestUrl(new URL("/tonconnect-manifest.json", window.location.href).href);
225
- }
226
-
227
220
  private updateWalletHint() {
228
221
  const extensions = detectInjectedWallets();
229
222
  const hints = ["Click Connect wallet to choose a provider (extension or mobile app)."];
@@ -70,9 +70,12 @@ export class TonConnectProvider implements Web3Provider {
70
70
  });
71
71
  }
72
72
 
73
- return this.waitForAccount(() => {
74
- void this.ui.openModal();
75
- });
73
+ return this.waitForAccount(
74
+ () => {
75
+ void this.ui.openModal();
76
+ },
77
+ { watchModalClose: true },
78
+ );
76
79
  }
77
80
 
78
81
  async disconnect() {
@@ -80,6 +83,7 @@ export class TonConnectProvider implements Web3Provider {
80
83
  }
81
84
 
82
85
  async signMessage(message: string | Uint8Array) {
86
+ await this.ui.connectionRestored;
83
87
  const account = this.requireAccount();
84
88
  const text = new TextDecoder().decode(toMessageBytes(message));
85
89
 
@@ -114,28 +118,46 @@ export class TonConnectProvider implements Web3Provider {
114
118
  return account;
115
119
  }
116
120
 
117
- private waitForAccount(start: () => void) {
121
+ private waitForAccount(start: () => void, options?: { watchModalClose?: boolean }) {
118
122
  return new Promise<PubKey>((resolve, reject) => {
119
- const unsubscribe = this.ui.onStatusChange(
123
+ let settled = false;
124
+
125
+ const finish = (action: () => void) => {
126
+ if (settled) return;
127
+
128
+ settled = true;
129
+ unsubscribeStatus();
130
+ unsubscribeModal?.();
131
+ action();
132
+ };
133
+
134
+ const unsubscribeStatus = this.ui.onStatusChange(
120
135
  (wallet) => {
121
136
  if (!wallet?.account) return;
122
137
 
123
- unsubscribe();
124
138
  const pubKey = this.toPubKey(wallet.account);
125
139
  this.onAccountChange?.(pubKey);
126
- resolve(pubKey);
140
+ finish(() => resolve(pubKey));
127
141
  },
128
142
  (error) => {
129
- unsubscribe();
130
- reject(error);
143
+ finish(() => reject(error));
131
144
  },
132
145
  );
133
146
 
147
+ const unsubscribeModal = options?.watchModalClose
148
+ ? this.ui.onModalStateChange((state) => {
149
+ if (state.status === "opened" || this.ui.account) return;
150
+
151
+ if (state.closeReason === "action-cancelled") {
152
+ finish(() => reject(new Error("Connection cancelled")));
153
+ }
154
+ })
155
+ : null;
156
+
134
157
  try {
135
158
  start();
136
159
  } catch (error) {
137
- unsubscribe();
138
- reject(error);
160
+ finish(() => reject(error));
139
161
  }
140
162
  });
141
163
  }
@@ -0,0 +1,67 @@
1
+ function readMessage(error: unknown): string {
2
+ if (typeof error === "string") return error;
3
+
4
+ if (error instanceof Error) {
5
+ return error.message || error.name;
6
+ }
7
+
8
+ if (error && typeof error === "object") {
9
+ const record = error as Record<string, unknown>;
10
+ const message = record.message;
11
+
12
+ if (typeof message === "string" && message.length) return message;
13
+ if (message && typeof message === "object") {
14
+ const nested = message as Record<string, unknown>;
15
+
16
+ if (typeof nested.message === "string" && nested.message.length) {
17
+ return nested.message;
18
+ }
19
+ }
20
+
21
+ if (typeof record.reason === "string" && record.reason.length) {
22
+ return record.reason;
23
+ }
24
+ }
25
+
26
+ return "";
27
+ }
28
+
29
+ function isUserCancellationMessage(message: string) {
30
+ const text = message.toLowerCase();
31
+
32
+ return (
33
+ text.includes("user rejected") ||
34
+ text.includes("rejected the request") ||
35
+ text.includes("request rejected") ||
36
+ text.includes("action rejected") ||
37
+ text.includes("connection cancelled") ||
38
+ text.includes("connection canceled") ||
39
+ text.includes("wallet was not connected") ||
40
+ text.includes("was not sent") ||
41
+ text.includes("sign data canceled") ||
42
+ text.includes("sign data cancelled") ||
43
+ text.includes("sign message canceled") ||
44
+ text.includes("sign message cancelled") ||
45
+ text.includes("transaction canceled") ||
46
+ text.includes("transaction cancelled")
47
+ );
48
+ }
49
+
50
+ export function isWalletUserCancellation(error: unknown) {
51
+ if (error && typeof error === "object") {
52
+ const record = error as Record<string, unknown>;
53
+
54
+ if (record.code === 4001 || record.code === "4001") return true;
55
+ if (record.name === "UserRejectsError") return true;
56
+ }
57
+
58
+ const message = readMessage(error);
59
+
60
+ return message.length > 0 && isUserCancellationMessage(message);
61
+ }
62
+
63
+ export function formatWalletError(error: unknown) {
64
+ const message = readMessage(error);
65
+
66
+ return message || "Wallet request failed";
67
+ }
@@ -118,6 +118,7 @@ export class Web3Service extends AbstractService {
118
118
  readonly setup$ = this.newRx(0);
119
119
 
120
120
  private provider: Web3Provider | null = null;
121
+ private activeProviderKey: string | null = null;
121
122
  private tonManifestUrl: string | null = null;
122
123
  private walletConnectProjectId: string | null = null;
123
124
  private builtinProviders: Web3WalletSlot[] | null = null;
@@ -170,11 +171,20 @@ export class Web3Service extends AbstractService {
170
171
  return getWalletModeLabel(wallet.kind, wallet.transport);
171
172
  }
172
173
 
173
- useProvider(provider: Web3Provider) {
174
+ useProvider(provider: Web3Provider, key: string | null = null) {
174
175
  this.provider = provider;
176
+ this.activeProviderKey = key;
175
177
  return this;
176
178
  }
177
179
 
180
+ private walletProviderKey(kind: WalletKind, transport: WalletTransport) {
181
+ return `${kind}:${transport}`;
182
+ }
183
+
184
+ private customProviderKey(providerId: string) {
185
+ return `custom:${providerId}`;
186
+ }
187
+
178
188
  getProvider() {
179
189
  return this.provider;
180
190
  }
@@ -218,6 +228,12 @@ export class Web3Service extends AbstractService {
218
228
  kind: InjectedWalletKind = "ethereum",
219
229
  options: InjectedProviderOptions = {},
220
230
  ) {
231
+ const key = this.walletProviderKey(kind, "extension");
232
+
233
+ if (this.provider && this.activeProviderKey === key) {
234
+ return this;
235
+ }
236
+
221
237
  const provider = createInjectedProvider(
222
238
  kind,
223
239
  (pubKey) => {
@@ -228,7 +244,7 @@ export class Web3Service extends AbstractService {
228
244
  },
229
245
  );
230
246
 
231
- return this.useProvider(provider);
247
+ return this.useProvider(provider, key);
232
248
  }
233
249
 
234
250
  async useWalletProvider(
@@ -236,6 +252,12 @@ export class Web3Service extends AbstractService {
236
252
  transport: WalletTransport = "auto",
237
253
  options: WalletProviderOptions = {},
238
254
  ) {
255
+ const key = this.walletProviderKey(kind, transport);
256
+
257
+ if (this.provider && this.activeProviderKey === key) {
258
+ return this;
259
+ }
260
+
239
261
  const provider = await createWalletProvider(
240
262
  kind,
241
263
  transport,
@@ -245,7 +267,7 @@ export class Web3Service extends AbstractService {
245
267
  this.walletOptions(options),
246
268
  );
247
269
 
248
- return this.useProvider(provider);
270
+ return this.useProvider(provider, key);
249
271
  }
250
272
 
251
273
  ensureInjectedProvider(
@@ -286,6 +308,8 @@ export class Web3Service extends AbstractService {
286
308
  }
287
309
 
288
310
  await this.provider.disconnect();
311
+ this.provider = null;
312
+ this.activeProviderKey = null;
289
313
  this.userPubKey$.update(null);
290
314
  }
291
315
 
@@ -314,8 +338,14 @@ export class Web3Service extends AbstractService {
314
338
  }
315
339
 
316
340
  async useCustomProvider(providerId: string) {
341
+ const key = this.customProviderKey(providerId);
342
+
343
+ if (this.provider && this.activeProviderKey === key) {
344
+ return this;
345
+ }
346
+
317
347
  const provider = await this.resolveCustomProvider(providerId);
318
- return this.useProvider(provider);
348
+ return this.useProvider(provider, key);
319
349
  }
320
350
 
321
351
  async connectCustom(providerId: string) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cruzo-web3",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Web3 addon for cruzo: wallet providers, sign, verify",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -31,28 +31,16 @@
31
31
  "typecheck": "tsc -p tsconfig.json",
32
32
  "prepublishOnly": "npm run typecheck"
33
33
  },
34
+ "dependencies": {
35
+ "@tonconnect/sdk": "^3.4.1",
36
+ "@tonconnect/ui": "^3.0.0",
37
+ "@walletconnect/ethereum-provider": "^2.23.9"
38
+ },
34
39
  "peerDependencies": {
35
- "@tonconnect/sdk": ">=3.0.0",
36
- "@tonconnect/ui": ">=3.0.0",
37
- "@walletconnect/ethereum-provider": ">=2.0.0",
38
40
  "cruzo": "0.9.889"
39
41
  },
40
- "peerDependenciesMeta": {
41
- "@tonconnect/sdk": {
42
- "optional": true
43
- },
44
- "@tonconnect/ui": {
45
- "optional": true
46
- },
47
- "@walletconnect/ethereum-provider": {
48
- "optional": true
49
- }
50
- },
51
42
  "devDependencies": {
52
- "@tonconnect/sdk": "^3.4.1",
53
- "@tonconnect/ui": "^3.0.0",
54
43
  "@types/node": "20.0.0",
55
- "@walletconnect/ethereum-provider": "^2.23.9",
56
44
  "cruzo": "0.9.889",
57
45
  "typescript": "5.9.3"
58
46
  }