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.
- package/README.md +242 -0
- package/lib/components/icons/copy-icon.component.ts +14 -0
- package/lib/components/web3-signer/web3-signer.component.module.css +106 -0
- package/lib/components/web3-signer/web3-signer.component.ts +320 -0
- package/lib/components/web3-signing/web3-signing.component.module.css +36 -0
- package/lib/components/web3-signing/web3-signing.component.ts +239 -0
- package/lib/components/web3-wallet-picker/web3-wallet-picker.bucket.ts +22 -0
- package/lib/components/web3-wallet-picker/web3-wallet-picker.component.module.css +62 -0
- package/lib/components/web3-wallet-picker/web3-wallet-picker.component.ts +171 -0
- package/lib/crypto/account-signature.ts +175 -0
- package/lib/crypto/decode-bytes.ts +93 -0
- package/lib/crypto/ecdsa-signature.ts +45 -0
- package/lib/crypto/keccak256.ts +117 -0
- package/lib/crypto/secp256k1-verify.ts +232 -0
- package/lib/crypto/sha256.ts +8 -0
- package/lib/crypto/verify-signature.ts +54 -0
- package/lib/env.d.ts +4 -0
- package/lib/errors/web3-error.ts +49 -0
- package/lib/index.ts +20 -0
- package/lib/providers/eip1193.provider.ts +152 -0
- package/lib/providers/injected.ts +71 -0
- package/lib/providers/message-bytes.ts +13 -0
- package/lib/providers/solana.provider.ts +116 -0
- package/lib/providers/tonconnect.provider.ts +161 -0
- package/lib/providers/tron.provider.ts +142 -0
- package/lib/providers/wallet-transport.ts +1 -0
- package/lib/providers/wallet.ts +92 -0
- package/lib/providers/walletconnect-ethereum.provider.ts +49 -0
- package/lib/pub-key.ts +144 -0
- package/lib/signing-url.ts +334 -0
- package/lib/types/signer-state.ts +10 -0
- package/lib/types/web3-types.ts +27 -0
- package/lib/utils/format-pub-key.ts +18 -0
- package/lib/web3-wallet.ts +31 -0
- package/lib/web3.service.ts +419 -0
- 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);
|