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
package/README.md ADDED
@@ -0,0 +1,242 @@
1
+ # cruzo-web3
2
+
3
+ Web3 addon for [cruzo](https://github.com/MaratBektemirov/cruzo): wallet connect, sign, verify, and ready-made UI components.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install cruzo cruzo-web3
9
+ ```
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
+
17
+ ## Public API
18
+
19
+ Main entry (`cruzo-web3`):
20
+
21
+ - `web3Service` — wallet connect, sign, verify, wallet picker config
22
+ - `Web3SigningComponent`, `Web3SignerComponent` — ready-made UI
23
+ - `ALL_BUILTIN_WALLET_SLOTS`, `isBuiltinWallet`, `isCustomWallet`
24
+ - types: `Web3Config`, `Web3CustomProviderConfig`, `Web3WalletSlot`, `Web3WalletTarget`, …
25
+
26
+ Subpath entries (side-effect imports that register components):
27
+
28
+ - `cruzo-web3/components/web3-signing`
29
+ - `cruzo-web3/components/web3-signer`
30
+
31
+ ```ts
32
+ import { web3Service } from "cruzo-web3";
33
+ import { Web3SigningComponent } from "cruzo-web3/components/web3-signing";
34
+ import { Web3SignerComponent } from "cruzo-web3/components/web3-signer";
35
+ ```
36
+
37
+ Package exports point to TypeScript sources in `lib/`. The app bundler (e.g. Vite) compiles them in dev and production — no separate build step in `cruzo-web3` is required.
38
+
39
+ ### App setup
40
+
41
+ Import cruzo UI **styles** and register **toast** in your app entry (`main.ts`). `modal` registers automatically when you import web3 components (`Web3SignerComponent` imports `cruzo/ui-components/modal`).
42
+
43
+ Use a single `cruzo` instance in the bundle — in Vite: `resolve.dedupe: ['cruzo']`.
44
+
45
+ ## Setup
46
+
47
+ Configure `web3Service` once at app startup, before components mount.
48
+
49
+ `web3Service.setup$` updates when WalletConnect Project ID, TON manifest URL, or wallet config changes — the wallet picker reacts automatically.
50
+
51
+ ### WalletConnect (Ethereum mobile)
52
+
53
+ WalletConnect is required for **Ethereum → Mobile wallet** in the connect modal.
54
+
55
+ 1. Create a project at [cloud.walletconnect.com](https://cloud.walletconnect.com).
56
+ 2. Pass the Project ID to `web3Service`:
57
+
58
+ ```ts
59
+ import { web3Service } from "cruzo-web3";
60
+
61
+ web3Service.setWalletConnectProjectId("your_project_id");
62
+ ```
63
+
64
+ With Vite:
65
+
66
+ ```env
67
+ # .env
68
+ VITE_WALLETCONNECT_PROJECT_ID=your_project_id
69
+ ```
70
+
71
+ ```ts
72
+ const projectId = import.meta.env.VITE_WALLETCONNECT_PROJECT_ID;
73
+
74
+ if (projectId) {
75
+ web3Service.setWalletConnectProjectId(projectId);
76
+ }
77
+ ```
78
+
79
+ Without a Project ID, the **Mobile wallet (WalletConnect)** option stays disabled in the picker.
80
+
81
+ ### TON Connect manifest
82
+
83
+ TON wallets (extension and mobile app) require a public `tonconnect-manifest.json`.
84
+
85
+ 1. Add the manifest file to your site root (must be reachable over HTTPS in production).
86
+ 2. Point `web3Service` to it:
87
+
88
+ ```ts
89
+ web3Service.setTonManifestUrl(
90
+ new URL("/tonconnect-manifest.json", window.location.href).href
91
+ );
92
+ ```
93
+
94
+ Example manifest:
95
+
96
+ ```json
97
+ {
98
+ "url": "https://cruzo.org",
99
+ "name": "cruzo",
100
+ "iconUrl": "https://cruzo.org/favicon.ico"
101
+ }
102
+ ```
103
+
104
+ - `url` — canonical app URL (production domain)
105
+ - `name` — app name shown in the wallet
106
+ - `iconUrl` — square icon, HTTPS
107
+
108
+ Spec: [Ton Connect manifest](https://github.com/ton-blockchain/ton-connect/blob/main/spec/manifest.md)
109
+
110
+ Without a manifest URL, TON wallet options stay disabled.
111
+
112
+ ### Custom providers (optional)
113
+
114
+ ```ts
115
+ import { web3Service } from "cruzo-web3";
116
+
117
+ web3Service.configure({
118
+ // hide built-in slots, keep only what you need
119
+ providers: [
120
+ { kind: "ethereum", transport: "extension" },
121
+ { kind: "ton", transport: "app" },
122
+ ],
123
+ customProviders: [
124
+ {
125
+ id: "my-wallet",
126
+ label: "My wallet",
127
+ hint: "Custom EIP-1193 bridge",
128
+ provider: () => myProvider,
129
+ },
130
+ ],
131
+ });
132
+ ```
133
+
134
+ ### Full startup example
135
+
136
+ ```ts
137
+ // web3-setup.ts
138
+ import { web3Service } from "cruzo-web3";
139
+
140
+ const walletConnectProjectId = import.meta.env.VITE_WALLETCONNECT_PROJECT_ID;
141
+
142
+ if (walletConnectProjectId) {
143
+ web3Service.setWalletConnectProjectId(walletConnectProjectId);
144
+ }
145
+
146
+ web3Service.setTonManifestUrl(
147
+ new URL("/tonconnect-manifest.json", window.location.href).href
148
+ );
149
+ ```
150
+
151
+ ```ts
152
+ // vite.config.ts
153
+ export default defineConfig({
154
+ resolve: { dedupe: ["cruzo"] },
155
+ });
156
+ ```
157
+
158
+ ```ts
159
+ // main.ts
160
+ import "cruzo/ui-components/vars.css";
161
+ import "cruzo/ui-components/button.css";
162
+ import "cruzo/ui-components/modal.css";
163
+ import "cruzo/ui-components/textarea.css";
164
+ import "cruzo/ui-components/toast.css";
165
+
166
+ import "cruzo/ui-components/toast";
167
+ import "site/web3-setup";
168
+ import "cruzo-web3/components/web3-signing";
169
+ ```
170
+
171
+ ```html
172
+ <web3-signing-component></web3-signing-component>
173
+ ```
174
+
175
+ See [cruzo-starter](https://github.com/MaratBektemirov/cruzo-starter) for a working local setup.
176
+
177
+ ## Components
178
+
179
+ `Web3SigningComponent` — demo page with payload textarea and two signers. Syncs payload and signer state to the URL.
180
+
181
+ `Web3SignerComponent` — single signer card with **Connect wallet** and **Sign payload**.
182
+
183
+ On **Connect wallet**, a modal opens with available providers:
184
+
185
+ | Network | Browser extension | Mobile app |
186
+ |----------|--------------------------------|-------------------|
187
+ | Ethereum | MetaMask, Rabby, EIP-1193 | WalletConnect |
188
+ | TON | Tonkeeper, other TON wallets | Ton Connect app |
189
+ | Solana | Phantom, other Solana wallets | — |
190
+ | Tron | TronLink | — |
191
+
192
+ Unavailable options are disabled (no extension installed, missing WalletConnect Project ID, or missing TON manifest).
193
+
194
+ Signer state is stored in the parent bucket:
195
+
196
+ ```ts
197
+ { pubKey: PubKey | null; signed: boolean; wallet?: SignerWallet | null }
198
+ ```
199
+
200
+ ## `web3Service` API
201
+
202
+ ```ts
203
+ // Connect / sign with a built-in wallet
204
+ await web3Service.connectWallet("ethereum", "extension");
205
+ await web3Service.signWallet(payload, "ethereum", "extension");
206
+
207
+ // transport: "extension" | "app" | "auto"
208
+ // "auto" picks extension if available, otherwise mobile app (Ethereum, TON)
209
+ await web3Service.connectWallet("ton", "auto");
210
+
211
+ // Injected-only shortcuts
212
+ await web3Service.connectInjected("ethereum");
213
+ await web3Service.signInjected(payload, "solana");
214
+
215
+ // Custom provider (from web3Service.configure)
216
+ await web3Service.connectCustom("my-wallet");
217
+ await web3Service.signCustom(payload, "my-wallet");
218
+
219
+ await web3Service.disconnect();
220
+
221
+ // Verify signature (async)
222
+ const ok = await web3Service.verifySignedContent(content, signature, pubKey);
223
+ ```
224
+
225
+ Supported `wallet` / `kind` values: `"ethereum"` | `"ton"` | `"solana"` | `"tron"`.
226
+
227
+ `verifySignedContent` supports:
228
+
229
+ - **TON / Solana** — Ed25519 public keys
230
+ - **Ethereum** — EVM address in `pubKey` + `personal_sign` signature (ecrecover)
231
+ - **Tron** — base58 address in `PubKey` + `signMessageV2` signature
232
+ - **sr25519** — not supported yet (throws `UNSUPPORTED_ALGORITHM`)
233
+
234
+ `userPubKey$` — reactive pub key of the last connected wallet on `web3Service`.
235
+
236
+ Also available: `detectInjectedWallets()`, `hasInjectedWallet()`, `parsePubKey()`, `isPubKey()`, `isValidPubKey()`, `useProvider()`, `useWalletProvider()`.
237
+
238
+ ## Typecheck
239
+
240
+ ```bash
241
+ npm run typecheck
242
+ ```
@@ -0,0 +1,14 @@
1
+ import { AbstractComponent, componentsRegistryService } from "cruzo";
2
+
3
+ export class CopyIconComponent extends AbstractComponent {
4
+ static selector = "copy-icon";
5
+
6
+ getHTML() {
7
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
8
+ <path d="M28,10V28H10V10H28m0-2H10a2,2,0,0,0-2,2V28a2,2,0,0,0,2,2H28a2,2,0,0,0,2-2V10a2,2,0,0,0-2-2Z"/>
9
+ <path d="M4,18H2V4A2,2,0,0,1,4,2H18V4H4Z"/>
10
+ </svg>`;
11
+ }
12
+ }
13
+
14
+ componentsRegistryService.define(CopyIconComponent);
@@ -0,0 +1,106 @@
1
+ .signer {
2
+ display: flex;
3
+ flex-direction: column;
4
+ gap: var(--s);
5
+ padding: var(--m);
6
+ border: 1px solid var(--border-dark);
7
+ border-radius: var(--m);
8
+ background: var(--surface);
9
+ }
10
+
11
+ .head {
12
+ display: flex;
13
+ align-items: center;
14
+ justify-content: space-between;
15
+ gap: var(--s);
16
+ }
17
+
18
+ .title {
19
+ font-size: 16px;
20
+ font-weight: 600;
21
+ }
22
+
23
+ .wallet {
24
+ margin-top: 2px;
25
+ font-size: 12px;
26
+ color: rgba(0, 0, 0, 0.55);
27
+ }
28
+
29
+ .status {
30
+ display: inline-flex;
31
+ align-items: center;
32
+ gap: 6px;
33
+ font-size: 13px;
34
+ color: rgba(0, 0, 0, 0.45);
35
+ }
36
+
37
+ .check {
38
+ display: inline-flex;
39
+ align-items: center;
40
+ justify-content: center;
41
+ width: 18px;
42
+ height: 18px;
43
+ border-radius: 999px;
44
+ border: 1px solid rgba(0, 0, 0, 0.12);
45
+ color: transparent;
46
+ font-size: 12px;
47
+ font-weight: 700;
48
+ line-height: 1;
49
+ }
50
+
51
+ .statusSigned {
52
+ color: #15803d;
53
+ }
54
+
55
+ .statusSigned .check {
56
+ border-color: #16a34a;
57
+ background: #dcfce7;
58
+ color: #15803d;
59
+ }
60
+
61
+ .pubkey {
62
+ display: flex;
63
+ flex-direction: column;
64
+ gap: 4px;
65
+ }
66
+
67
+ .label {
68
+ font-size: 12px;
69
+ color: rgba(0, 0, 0, 0.55);
70
+ }
71
+
72
+ .pubkeyRow {
73
+ display: flex;
74
+ align-items: center;
75
+ }
76
+
77
+ .value {
78
+ flex: 1;
79
+ min-width: 0;
80
+ font-family: var(--mono);
81
+ font-size: 13px;
82
+ line-height: 1.5;
83
+ word-break: break-all;
84
+ }
85
+
86
+ .copyBtn {
87
+ width: 16px;
88
+ height: 16px;
89
+ cursor: pointer;
90
+ }
91
+
92
+ .copyBtn:hover {
93
+ opacity: 0.5;
94
+ }
95
+
96
+ .actions {
97
+ display: flex;
98
+ flex-wrap: wrap;
99
+ gap: var(--xs);
100
+ }
101
+
102
+ .error {
103
+ margin: 0;
104
+ font-size: 13px;
105
+ color: #b91c1c;
106
+ }
@@ -0,0 +1,320 @@
1
+ import styles from "./web3-signer.component.module.css";
2
+
3
+ import { AbstractComponent, componentsRegistryService, routerService, toastService } from "cruzo";
4
+ import { UI_KIT } from "cruzo/ui-components/const";
5
+ import { ModalComponent } from "cruzo/ui-components/modal";
6
+ import "cruzo/ui-components/toast";
7
+
8
+ import { CopyIconComponent } from "../icons/copy-icon.component";
9
+ import { Web3WalletPickerComponent } from "../web3-wallet-picker/web3-wallet-picker.component";
10
+ import {
11
+ WALLET_ACTIVE_SIGNER_ID,
12
+ WALLET_MODAL_ID,
13
+ WALLET_PICKER_ID,
14
+ web3WalletPickerBucket,
15
+ } from "../web3-wallet-picker/web3-wallet-picker.bucket";
16
+ import type { SignerState, SignerWallet } from "../../types/signer-state";
17
+ import { buildSigningPageUrl } from "../../signing-url";
18
+ import { pubKeyToText } from "../../utils/format-pub-key";
19
+ import { web3Service } from "../../web3.service";
20
+ import { isCustomWallet } from "../../web3-wallet";
21
+
22
+ export interface SignerConfig {
23
+ payload: string;
24
+ title: string;
25
+ }
26
+
27
+ export class Web3SignerComponent extends AbstractComponent<SignerConfig, any, SignerState> {
28
+ static selector = "web3-signer-component";
29
+
30
+ hasOuterBucket = true;
31
+ hasConfig = true;
32
+
33
+ dependencies = new Set([
34
+ "modal-component",
35
+ Web3WalletPickerComponent.selector,
36
+ CopyIconComponent.selector,
37
+ ]);
38
+
39
+ title$ = this.newRx("");
40
+ walletLabel$ = this.newRx("No wallet selected");
41
+ busy$ = this.newRx(false);
42
+ error$ = this.newRx("");
43
+ pubKeyLabel$ = this.newRx("—");
44
+
45
+ private selectedWallet: SignerWallet | null = null;
46
+ private walletPickEvents$ = this.newRxEventFromBucketByIndex(
47
+ web3WalletPickerBucket,
48
+ WALLET_PICKER_ID,
49
+ "web3WalletSelected",
50
+ );
51
+
52
+ constructor() {
53
+ super();
54
+
55
+ this.newRxFunc((events) => {
56
+ const event = events?.["0"];
57
+
58
+ if (!event) return;
59
+
60
+ if (web3WalletPickerBucket.getValue(WALLET_ACTIVE_SIGNER_ID) !== this.id) return;
61
+
62
+ events["0"] = null;
63
+ this.onWalletSelected(event.data);
64
+ }, this.walletPickEvents$);
65
+ }
66
+
67
+ getHTML() {
68
+ return `<div class="${styles.signer}">
69
+ <div class="${styles.head}">
70
+ <div>
71
+ <h3 class="${styles.title}">{{ root.title$::rx }}</h3>
72
+ <div class="${styles.wallet}">{{ root.walletLabel$::rx }}</div>
73
+ </div>
74
+ <span class="${styles.status} ${styles.statusSigned}"
75
+ attached="{{ root.state$::rx?.signed }}">
76
+ <span class="${styles.check}" aria-hidden="true">✓</span>
77
+ <span>Signed</span>
78
+ </span>
79
+ <span class="${styles.status}"
80
+ attached="{{ !root.state$::rx?.signed }}">
81
+ <span class="${styles.check}" aria-hidden="true">✓</span>
82
+ <span>Not signed</span>
83
+ </span>
84
+ </div>
85
+
86
+ <div class="${styles.pubkey}">
87
+ <span class="${styles.label}">PubKey</span>
88
+ <div class="${styles.pubkeyRow}">
89
+ <code class="${styles.value}">{{ root.pubKeyLabel$::rx }}</code>
90
+ <div
91
+ class="${styles.copyBtn}"
92
+ title="Copy PubKey"
93
+ attached="{{ root.state$::rx?.pubKey }}"
94
+ onclick="{{ root.copyPubKey(event.currentTarget) }}">
95
+ <copy-icon></copy-icon>
96
+ </div>
97
+ </div>
98
+ </div>
99
+
100
+ <div class="${styles.actions}">
101
+ <button type="button"
102
+ class="${UI_KIT}_button ${UI_KIT}_button-s ${UI_KIT}_button-secondary"
103
+ disabled="{{ root.busy$::rx }}"
104
+ onclick="{{ root.connect() }}">Connect wallet</button>
105
+ <button type="button"
106
+ class="${UI_KIT}_button ${UI_KIT}_button-s ${UI_KIT}_button-primary"
107
+ disabled="{{ root.busy$::rx || !root.state$::rx?.pubKey || root.state$::rx?.signed }}"
108
+ onclick="{{ root.sign(event.currentTarget) }}">Sign payload</button>
109
+ </div>
110
+
111
+ <p class="${styles.error}" attached="{{ root.error$::rx }}">{{ root.error$::rx }}</p>
112
+ </div>`;
113
+ }
114
+
115
+ connect() {
116
+ this.error$.update("");
117
+ web3WalletPickerBucket.setValue(WALLET_ACTIVE_SIGNER_ID, this.id);
118
+ ModalComponent.attach(WALLET_MODAL_ID, web3WalletPickerBucket.id);
119
+ }
120
+
121
+ sign(el?: Element) {
122
+ const payload = this.config$.actual?.payload;
123
+
124
+ if (!payload) {
125
+ this.error$.update("Payload is not configured");
126
+ return;
127
+ }
128
+
129
+ if (!this.state$.actual?.pubKey) {
130
+ this.error$.update("Connect wallet first");
131
+ return;
132
+ }
133
+
134
+ if (!this.selectedWallet) {
135
+ const wallet = this.state$.actual?.wallet ?? null;
136
+
137
+ if (wallet) {
138
+ this.selectedWallet = wallet;
139
+ this.walletLabel$.update(web3Service.getWalletLabel(wallet));
140
+ } else {
141
+ this.error$.update("Reconnect wallet to sign");
142
+ this.connect();
143
+ return;
144
+ }
145
+ }
146
+
147
+ const pubKey = this.state$.actual.pubKey;
148
+ const wallet = this.selectedWallet;
149
+
150
+ this.runWalletAction(() => {
151
+ const signAction = isCustomWallet(wallet)
152
+ ? web3Service.signCustom(payload, wallet.providerId)
153
+ : web3Service.signWallet(payload, wallet.kind, wallet.transport, this.getWalletOptions());
154
+
155
+ return signAction.then(() => {
156
+ this.outerBucket.setState(this.id, { pubKey, signed: true, wallet }, this.index);
157
+
158
+ const url = buildSigningPageUrl(
159
+ routerService.pathname$.actual,
160
+ routerService.search$.actual,
161
+ this.buildSigningSnapshot(),
162
+ routerService.isHashMode(),
163
+ this.config$.actual?.payload,
164
+ );
165
+
166
+ void this.copyText(url)
167
+ .then(() => {
168
+ toastService.show({
169
+ kind: "success",
170
+ title: "Signing successful",
171
+ message: "URL copied — you can send it to someone else.",
172
+ alignX: "center",
173
+ alignY: "top",
174
+ timeoutMs: 0,
175
+ });
176
+ })
177
+ .catch(() => {
178
+ toastService.show({
179
+ kind: "success",
180
+ title: "Signing successful",
181
+ message: "You can copy the page URL and send it to someone else.",
182
+ alignX: "center",
183
+ alignY: "top",
184
+ timeoutMs: 0,
185
+ });
186
+ });
187
+ });
188
+ });
189
+ }
190
+
191
+ connectedCallback() {
192
+ super.connectedCallback();
193
+
194
+ this.title$.update(this.config$.actual?.title ?? this.id);
195
+ this.applyWalletFromState(this.state$.actual);
196
+
197
+ this.newRxFunc((state) => {
198
+ this.pubKeyLabel$.update(pubKeyToText(state?.pubKey ?? null));
199
+ this.applyWalletFromState(state);
200
+ }, this.state$);
201
+ }
202
+
203
+ copyPubKey(el?: Element) {
204
+ const pubKey = this.state$.actual?.pubKey;
205
+
206
+ if (!pubKey?.value) return;
207
+
208
+ void navigator.clipboard
209
+ .writeText(pubKeyToText(pubKey))
210
+ .then(() => {
211
+ toastService.show({
212
+ kind: "success",
213
+ message: "Public key copied",
214
+ element: el ?? null,
215
+ alignX: "right",
216
+ alignY: "top",
217
+ timeoutMs: 1600,
218
+ });
219
+ })
220
+ .catch(() => {
221
+ toastService.show({
222
+ kind: "error",
223
+ message: "Copy failed",
224
+ element: el ?? null,
225
+ alignX: "right",
226
+ alignY: "top",
227
+ timeoutMs: 2200,
228
+ });
229
+ });
230
+ }
231
+
232
+ private buildSigningSnapshot(): Record<string, SignerState> {
233
+ const snapshot: Record<string, SignerState> = {};
234
+
235
+ for (const id of Object.keys(this.outerBucket.descriptors)) {
236
+ snapshot[id] =
237
+ this.outerBucket.getState(id) ?? { pubKey: null, signed: false, wallet: null };
238
+ }
239
+
240
+ return snapshot;
241
+ }
242
+
243
+ private copyText(text: string): Promise<void> {
244
+ if (navigator.clipboard?.writeText) {
245
+ return navigator.clipboard.writeText(text).catch(() => this.copyTextFallback(text));
246
+ }
247
+
248
+ return this.copyTextFallback(text);
249
+ }
250
+
251
+ private copyTextFallback(text: string): Promise<void> {
252
+ const textarea = document.createElement("textarea");
253
+
254
+ textarea.value = text;
255
+ textarea.setAttribute("readonly", "");
256
+ textarea.style.position = "fixed";
257
+ textarea.style.left = "-9999px";
258
+
259
+ document.body.appendChild(textarea);
260
+ textarea.select();
261
+
262
+ try {
263
+ if (!document.execCommand("copy")) {
264
+ return Promise.reject(new Error("Copy failed"));
265
+ }
266
+
267
+ return Promise.resolve();
268
+ } finally {
269
+ document.body.removeChild(textarea);
270
+ }
271
+ }
272
+
273
+ private applyWalletFromState(state: SignerState | null | undefined) {
274
+ if (!state?.wallet) return;
275
+
276
+ this.selectedWallet = state.wallet;
277
+ this.walletLabel$.update(web3Service.getWalletLabel(state.wallet));
278
+ }
279
+
280
+ private onWalletSelected(wallet: SignerWallet) {
281
+ this.selectedWallet = wallet;
282
+ this.walletLabel$.update(web3Service.getWalletLabel(wallet));
283
+
284
+ this.runWalletAction(() => {
285
+ const connectAction = isCustomWallet(wallet)
286
+ ? web3Service.connectCustom(wallet.providerId)
287
+ : web3Service.connectWallet(wallet.kind, wallet.transport, this.getWalletOptions());
288
+
289
+ return connectAction.then((pubKey) => {
290
+ this.outerBucket.setState(this.id, { pubKey, signed: false, wallet }, this.index);
291
+ });
292
+ });
293
+ }
294
+
295
+ private getWalletOptions() {
296
+ const options: { tonManifestUrl?: string; walletConnectProjectId?: string } = {};
297
+ const tonManifestUrl = web3Service.getTonManifestUrl();
298
+ const walletConnectProjectId = web3Service.getWalletConnectProjectId();
299
+
300
+ if (tonManifestUrl) options.tonManifestUrl = tonManifestUrl;
301
+ if (walletConnectProjectId) options.walletConnectProjectId = walletConnectProjectId;
302
+
303
+ return options;
304
+ }
305
+
306
+ private runWalletAction(action: () => Promise<void>) {
307
+ this.error$.update("");
308
+ this.busy$.update(true);
309
+
310
+ action()
311
+ .catch((error: unknown) => {
312
+ this.error$.update(error instanceof Error ? error.message : String(error));
313
+ })
314
+ .finally(() => {
315
+ this.busy$.update(false);
316
+ });
317
+ }
318
+ }
319
+
320
+ componentsRegistryService.define(Web3SignerComponent);