@www.hyperlinks.space/program-kit 18.18.18 → 123.123.123
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/.eas/workflows/create-development-builds.yml +21 -0
- package/.eas/workflows/create-draft.yml +15 -0
- package/.eas/workflows/deploy-to-production.yml +68 -0
- package/.gitattributes +48 -0
- package/.gitignore +52 -0
- package/.nvmrc +1 -0
- package/.vercelignore +6 -0
- package/README.md +17 -2
- package/ai/openai.ts +202 -0
- package/ai/transmitter.ts +367 -0
- package/backlogs/medium_term_backlog.md +26 -0
- package/backlogs/short_term_backlog.md +42 -0
- package/eslint.config.cjs +10 -0
- package/npmReadMe.md +17 -2
- package/npmrc.example +1 -0
- package/package.json +3 -28
- package/polyfills/buffer.ts +9 -0
- package/research & docs/ai_and_search_bar_input.md +94 -0
- package/research & docs/ai_bot_messages.md +124 -0
- package/research & docs/auth-and-centralized-encrypted-keys-plan.md +440 -0
- package/research & docs/blue_bar_tackling.md +143 -0
- package/research & docs/bot_async_streaming.md +174 -0
- package/research & docs/build_and_install.md +129 -0
- package/research & docs/database_messages.md +34 -0
- package/research & docs/fonts.md +18 -0
- package/research & docs/github-gitlab-bidirectional-mirroring.md +154 -0
- package/research & docs/keys-retrieval-console-scripts.js +131 -0
- package/research & docs/npm-release.md +46 -0
- package/research & docs/releases.md +201 -0
- package/research & docs/releases_github_actions.md +188 -0
- package/research & docs/scalability.md +34 -0
- package/research & docs/security_plan_raw.md +244 -0
- package/research & docs/security_raw.md +354 -0
- package/research & docs/storage-availability-console-script.js +152 -0
- package/research & docs/storage-lifetime.md +33 -0
- package/research & docs/telegram-raw-keys-cloud-storage-risks.md +31 -0
- package/research & docs/timing_raw.md +63 -0
- package/research & docs/tma_logo_bar_jump_investigation.md +69 -0
- package/research & docs/update.md +205 -0
- package/research & docs/wallet_telegram_standalone_multichain_proposal.md +192 -0
- package/research & docs/wallets_hosting_architecture.md +403 -0
- package/services/wallet/tonWallet.ts +73 -0
- package/ui/components/GlobalBottomBar.tsx +447 -0
- package/ui/components/GlobalBottomBarWeb.tsx +362 -0
- package/ui/components/GlobalLogoBar.tsx +108 -0
- package/ui/components/GlobalLogoBarFallback.tsx +66 -0
- package/ui/components/GlobalLogoBarWithFallback.tsx +24 -0
- package/ui/components/HyperlinksSpaceLogo.tsx +29 -0
- package/ui/components/Telegram.tsx +677 -0
- package/ui/components/telegramWebApp.ts +359 -0
- package/ui/fonts.ts +12 -0
- package/ui/theme.ts +117 -0
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
# HyperlinksSpace — Wallet Architecture Doc
|
|
2
|
+
|
|
3
|
+
> High-level system design, UI flow principles, and implementation thoughts.
|
|
4
|
+
> Target: TON wallet architecture options for HyperlinksSpace app (Flutter/TMA/Web), including non-custodial and custodial modes.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## 1. Guiding Principles
|
|
9
|
+
|
|
10
|
+
- **Non-custodial by default.** The mnemonic (seed phrase) never touches the server. Key generation and signing happen on-device.
|
|
11
|
+
- **Telegram-first identity.** Inside Telegram, the user's Telegram account is the identity anchor. Outside Telegram (Windows desktop, Web), Telegram Login is used as the identity bridge.
|
|
12
|
+
- **Small, working steps.** Each wallet feature ships as an isolated, backward-compatible unit that can be deployed without touching existing bot or app flows.
|
|
13
|
+
- **Stable coin built-in.** The wallet is designed from day one to display and eventually allocate DLLR (or another locked stable) alongside the TON balance.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## 2. Wallet Types and Entry Points
|
|
18
|
+
|
|
19
|
+
The app supports two wallet modes:
|
|
20
|
+
|
|
21
|
+
| Mode | Description | Who uses it |
|
|
22
|
+
|---|---|---|
|
|
23
|
+
| **Unhosted (non-custodial)** | Created inside the app, keys live on device | New users with no existing wallet |
|
|
24
|
+
| **TON Connect** | Connects an existing external wallet (Tonkeeper, MyTonWallet, etc.) | Power users with existing wallets |
|
|
25
|
+
|
|
26
|
+
TON Connect is treated as an **additional/advanced feature**, not the default. The primary flow is always the unhosted wallet.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## 3. Wallet State Machine
|
|
31
|
+
|
|
32
|
+
Every wallet instance moves through these states:
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
[ No Wallet ]
|
|
36
|
+
│
|
|
37
|
+
├──── "Create wallet" ──────► [ Generating ]
|
|
38
|
+
│ │
|
|
39
|
+
└──── "Restore wallet" ────────────►│
|
|
40
|
+
▼
|
|
41
|
+
[ Created ]
|
|
42
|
+
(address shown instantly, QR + copy)
|
|
43
|
+
│
|
|
44
|
+
▼
|
|
45
|
+
[ Deploying ]
|
|
46
|
+
(SMC deploy call in progress)
|
|
47
|
+
│
|
|
48
|
+
┌─────────┴─────────┐
|
|
49
|
+
▼ ▼
|
|
50
|
+
[ Ready ] [ Deploy Failed ]
|
|
51
|
+
(fully operational) (retry available)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
**State storage key:** `awallet_v1` (encrypted blob in SecureStorage)
|
|
55
|
+
**Metadata stored separately (plaintext):**
|
|
56
|
+
```
|
|
57
|
+
wallet_address
|
|
58
|
+
created_at
|
|
59
|
+
last_seen_at
|
|
60
|
+
deploy_status ← "pending" | "deployed" | "failed"
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## 4. First Launch — Unhosted Wallet
|
|
66
|
+
|
|
67
|
+
**Goal:** User has no wallet. They open the app for the first time (any platform).
|
|
68
|
+
|
|
69
|
+
### UI Flow
|
|
70
|
+
|
|
71
|
+
1. **Wallet tab / panel opens** → app checks `awallet_v1` in local SecureStorage.
|
|
72
|
+
2. Nothing found → show two CTAs:
|
|
73
|
+
- **"Create new wallet"** (primary, prominent)
|
|
74
|
+
- **"Restore wallet"** (secondary, text link — visible only if device has an encrypted blob from a previous session or the user chooses to enter a mnemonic)
|
|
75
|
+
3. User taps **"Create new wallet"**:
|
|
76
|
+
- Spinner shown: _"Creating wallet…"_
|
|
77
|
+
- App generates a BIP39 mnemonic + TON keypair **entirely in-memory** (no server call at this step).
|
|
78
|
+
- Address is computed and displayed **instantly**.
|
|
79
|
+
- Encrypted blob is written to SecureStorage.
|
|
80
|
+
4. App shows **"Your wallet is ready"** screen:
|
|
81
|
+
- Address (truncated + copy button)
|
|
82
|
+
- QR code
|
|
83
|
+
- DLLR status: `Allocated / Locked / Available` (placeholders until deploy completes)
|
|
84
|
+
- `"Deploying…"` status badge
|
|
85
|
+
5. SMC deploy call fires in the background (POST `/wallet/deploy`). When complete, status badge updates to ✅ `"Active"`.
|
|
86
|
+
6. A **"Back up your seed phrase"** nudge is shown — non-blocking, dismissible, but persistent until the user acknowledges.
|
|
87
|
+
|
|
88
|
+
### Key decisions
|
|
89
|
+
- Address generation is instant and shown before deploy. This is intentional UX — users can copy the address to receive funds immediately, even before the contract is deployed.
|
|
90
|
+
- Mnemonic is shown **once**, at creation time. After acknowledgement it is never shown again (only recoverable via "Export seed phrase" with auth).
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## 5. Second Launch — Loading Existing Wallet
|
|
95
|
+
|
|
96
|
+
**Goal:** User returns to the app on the same device. Wallet was created previously.
|
|
97
|
+
|
|
98
|
+
### UI Flow
|
|
99
|
+
|
|
100
|
+
1. App reads `awallet_v1` from SecureStorage on startup.
|
|
101
|
+
2. Encrypted blob found → decrypt using device key (Secure Enclave / Android Keystore).
|
|
102
|
+
3. Wallet panel loads directly to **Ready state** — no mnemonic prompt needed.
|
|
103
|
+
4. App polls `/wallet/status?address=...` to refresh balances and deploy status.
|
|
104
|
+
5. If `deploy_status === "pending"` (e.g., deploy was interrupted), app auto-retries the deploy silently.
|
|
105
|
+
|
|
106
|
+
### Key decisions
|
|
107
|
+
- The device key (used to decrypt the blob) **never leaves the device**. It is stored in the OS-level secure keystore.
|
|
108
|
+
- No mnemonic entry is needed on the same device — the encrypted blob handles re-authentication transparently.
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## 6. New Device / Re-connect Scenario (Mnemonic Required)
|
|
113
|
+
|
|
114
|
+
**Goal:** User opens the app on a new phone, new PC, or after clearing app data.
|
|
115
|
+
|
|
116
|
+
### UI Flow
|
|
117
|
+
|
|
118
|
+
1. App finds no `awallet_v1` in SecureStorage (it's a fresh install).
|
|
119
|
+
2. Wallet panel shows:
|
|
120
|
+
- **"Create new wallet"** (primary)
|
|
121
|
+
- **"Restore wallet"** (secondary, prominent here)
|
|
122
|
+
3. User taps **"Restore wallet"** → input field: _"Enter your 24-word seed phrase"_
|
|
123
|
+
4. Words entered → app validates mnemonic → re-derives keypair → re-encrypts blob into the new device's SecureStorage.
|
|
124
|
+
5. App calls `/wallet/restore` to confirm address matches expectation.
|
|
125
|
+
6. Wallet loads at **Ready state** (deploy was already done on the original device).
|
|
126
|
+
|
|
127
|
+
### Key decisions
|
|
128
|
+
- The mnemonic is the **single recovery factor**. There is no server-side recovery.
|
|
129
|
+
- Mnemonic input should use a native secure text field (no clipboard suggestions, no autocorrect logging).
|
|
130
|
+
- CloudStorage (Telegram's `telegram.cloudStorage` API) can optionally store the **ciphertext** (not the key), so restore can be assisted without the user typing all 24 words. The device key is still required to decrypt it — CloudStorage alone is not sufficient to reconstruct the wallet.
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## 7. Telegram Wallet — Inside Telegram (TMA)
|
|
135
|
+
|
|
136
|
+
**Goal:** User accesses the wallet via the Telegram Mini App (TMA).
|
|
137
|
+
|
|
138
|
+
### How identity and storage work inside TMA
|
|
139
|
+
|
|
140
|
+
- **Identity:** `initData.user.id` (Telegram user ID) is the identity anchor. No separate login needed.
|
|
141
|
+
- **Storage:**
|
|
142
|
+
- `telegram.cloudStorage` is used for the encrypted wallet blob — it is synced across Telegram sessions automatically.
|
|
143
|
+
- The device key (to decrypt the blob) is stored in the browser's IndexedDB / WebCrypto key store scoped to the TMA origin.
|
|
144
|
+
- **First launch in TMA:** Same as Section 4, but blob goes to `cloudStorage` instead of native SecureStorage.
|
|
145
|
+
- **Re-open in TMA on same device:** Blob in `cloudStorage` + device key in IndexedDB → loads wallet silently.
|
|
146
|
+
- **TMA on a new device:** CloudStorage has the ciphertext, but the device key is absent → user is prompted to enter mnemonic once. New device key is generated and stored. From then on, that device works silently.
|
|
147
|
+
|
|
148
|
+
### Key decisions
|
|
149
|
+
- `telegram.cloudStorage` is **not end-to-end encrypted by Telegram** — it is accessible to the bot's server-side in theory. This is why we store only the **ciphertext** there, not the plaintext key or mnemonic.
|
|
150
|
+
- The split: _ciphertext in CloudStorage, key in device_ ensures neither half alone can reconstruct the wallet.
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## 8. Outside Telegram — Web / Windows Setup
|
|
155
|
+
|
|
156
|
+
**Goal:** User uses the app via browser or Windows desktop (Electron/Tauri wrapper), outside of Telegram context.
|
|
157
|
+
|
|
158
|
+
### How identity works outside TMA
|
|
159
|
+
|
|
160
|
+
Since there is no `initData` from Telegram, identity is established via **Telegram Login Widget** (OAuth-style flow):
|
|
161
|
+
|
|
162
|
+
1. User is shown a "Log in with Telegram" button.
|
|
163
|
+
2. Telegram sends a signed hash to the app confirming the user's Telegram ID and username.
|
|
164
|
+
3. App uses the Telegram ID as the identity anchor — same as inside TMA.
|
|
165
|
+
|
|
166
|
+
### Storage outside Telegram
|
|
167
|
+
|
|
168
|
+
- **Web browser:** Encrypted blob is stored in `localStorage` (or IndexedDB for larger payloads). Device key in WebCrypto non-extractable key store.
|
|
169
|
+
- **Windows desktop:** Encrypted blob stored in the OS credential manager or app's local data directory. Device key in Windows DPAPI or Keychain equivalent.
|
|
170
|
+
|
|
171
|
+
### First launch on Windows
|
|
172
|
+
|
|
173
|
+
1. App opens → Telegram Login flow → identity confirmed.
|
|
174
|
+
2. App checks local storage for `awallet_v1`.
|
|
175
|
+
3. **No wallet found:** Same "Create / Restore" UI as Section 4.
|
|
176
|
+
4. **Wallet found (transferred or restored):** Loads to Ready state.
|
|
177
|
+
|
|
178
|
+
### Re-launch on Windows
|
|
179
|
+
|
|
180
|
+
Same device → blob in local storage + device key still present → silent load, no mnemonic needed.
|
|
181
|
+
|
|
182
|
+
### Key decisions
|
|
183
|
+
- Windows is primarily a **reading/management surface** — send, receive, view balances. Heavy transaction flows remain on mobile/TMA first.
|
|
184
|
+
- The GitHub auto-updater (workflow-based releases) is already in place for the Windows build. Wallet state must survive app updates — storage paths must not change across versions, or migration logic must be included.
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## 9. API Contract (Wallet Endpoints)
|
|
189
|
+
|
|
190
|
+
```
|
|
191
|
+
POST /wallet/create
|
|
192
|
+
Body: { address, encrypted_blob, public_key }
|
|
193
|
+
Response: { ok: true }
|
|
194
|
+
|
|
195
|
+
POST /wallet/deploy
|
|
196
|
+
Body: { address }
|
|
197
|
+
Response: { status: "pending" | "deployed" | "failed" }
|
|
198
|
+
|
|
199
|
+
GET /wallet/status?address=...
|
|
200
|
+
Response: { deployed: bool, dllr_status: {...}, balances: { ton, dllr } }
|
|
201
|
+
|
|
202
|
+
POST /wallet/restore
|
|
203
|
+
Body: { encrypted_blob }
|
|
204
|
+
Response: { address }
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
The backend is **optional** for the read path — balances can be fetched directly from TON APIs (toncenter/tonapi) on the client side. The backend is primarily needed for:
|
|
208
|
+
- SMC deploy coordination
|
|
209
|
+
- DLLR allocation logic
|
|
210
|
+
- Caching and rate-limit protection for API calls
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
## 10. WalletService Abstraction (Flutter)
|
|
215
|
+
|
|
216
|
+
The Flutter app uses a `WalletService` interface so the UI never depends directly on whether we're in mock, front-only (direct TON API), or backend-connected mode:
|
|
217
|
+
|
|
218
|
+
```dart
|
|
219
|
+
abstract class WalletService {
|
|
220
|
+
Future<WalletState> loadFromStorage();
|
|
221
|
+
Stream<WalletStatus> watchStatus(String address);
|
|
222
|
+
Future<WalletInfo> createWallet();
|
|
223
|
+
Future<WalletInfo> restoreWallet(String mnemonic);
|
|
224
|
+
}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Swap between implementations via a flag (`kUseMockWalletState` for dev, env-driven for prod). No UI refactor needed when switching providers.
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
## 11. Security Summary
|
|
232
|
+
|
|
233
|
+
| Concern | Approach |
|
|
234
|
+
|---|---|
|
|
235
|
+
| Mnemonic exposure | Shown once at creation, never stored in plaintext |
|
|
236
|
+
| Key storage | OS Secure Enclave / Android Keystore / WebCrypto non-extractable |
|
|
237
|
+
| Server-side secrets | None — server never sees mnemonic or private key |
|
|
238
|
+
| Cross-device restore | Mnemonic re-entry required OR ciphertext from CloudStorage + device key |
|
|
239
|
+
| Telegram identity outside TMA | Telegram Login Widget (signed hash verification) |
|
|
240
|
+
| Duplicate deploy | State machine prevents re-deploy if `deploy_status === "deployed"` |
|
|
241
|
+
| App update survivability | Storage keys versioned (`awallet_v1`), migration path required for `v2` |
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
## 12. Phase Roadmap
|
|
246
|
+
|
|
247
|
+
| Phase | Scope |
|
|
248
|
+
|---|---|
|
|
249
|
+
| **Phase 1** | Unhosted wallet: create, display address, deploy SMC, show TON balance |
|
|
250
|
+
| **Phase 2** | DLLR integration: allocated/locked/available display, stable coin status |
|
|
251
|
+
| **Phase 3** | TON Connect (connect existing wallets as secondary option) |
|
|
252
|
+
| **Phase 4** | Send/receive flows, transaction history |
|
|
253
|
+
| **Phase 5** | Cross-device CloudStorage assist, backup/export UX hardening |
|
|
254
|
+
|
|
255
|
+
---
|
|
256
|
+
|
|
257
|
+
## 13. Custodial Model Extension
|
|
258
|
+
|
|
259
|
+
This section adds a custodial architecture option alongside the existing non-custodial design.
|
|
260
|
+
|
|
261
|
+
### 13.1 What "custodial" means in this doc
|
|
262
|
+
|
|
263
|
+
- Wallet secrets are stored centrally as encrypted data in backend storage.
|
|
264
|
+
- Backend trust boundary can participate in decrypt/sign authorization.
|
|
265
|
+
- User identity (Google/Telegram/GitHub/email OTP) becomes a stronger operational gate.
|
|
266
|
+
|
|
267
|
+
This is a product/trust choice, not just an implementation detail.
|
|
268
|
+
|
|
269
|
+
---
|
|
270
|
+
|
|
271
|
+
### 13.2 Custodial key architecture (envelope encryption)
|
|
272
|
+
|
|
273
|
+
Store per-wallet:
|
|
274
|
+
|
|
275
|
+
- `ciphertext` (wallet secret encrypted by a DEK)
|
|
276
|
+
- `wrapped_dek` (DEK encrypted by KEK)
|
|
277
|
+
- metadata (`key_version`, `algo`, `created_at`, `status`)
|
|
278
|
+
|
|
279
|
+
Keep KEK in KMS/HSM, not in DB/app code.
|
|
280
|
+
|
|
281
|
+
Result:
|
|
282
|
+
- DB theft alone is insufficient.
|
|
283
|
+
- Attacker also needs KMS/HSM access path and permissions.
|
|
284
|
+
|
|
285
|
+
---
|
|
286
|
+
|
|
287
|
+
### 13.3 Identity and auth in custodial mode
|
|
288
|
+
|
|
289
|
+
Identity providers:
|
|
290
|
+
- Google OAuth
|
|
291
|
+
- GitHub OAuth
|
|
292
|
+
- Telegram login bridge
|
|
293
|
+
- Email + OTP (protection code)
|
|
294
|
+
|
|
295
|
+
Account linking maps all providers to one internal `user_id`.
|
|
296
|
+
|
|
297
|
+
Auth session is used to authorize protected operations:
|
|
298
|
+
- key unwrap requests
|
|
299
|
+
- signing requests
|
|
300
|
+
- provider linking/unlinking
|
|
301
|
+
|
|
302
|
+
---
|
|
303
|
+
|
|
304
|
+
### 13.4 Custodial state machine
|
|
305
|
+
|
|
306
|
+
```
|
|
307
|
+
[ No Wallet Record ]
|
|
308
|
+
│
|
|
309
|
+
├── Create wallet ─► [ Custodial Encrypted ]
|
|
310
|
+
│ │
|
|
311
|
+
│ ├── Unlock request (auth + policy) ─► [ Session Unlocked ]
|
|
312
|
+
│ │ │
|
|
313
|
+
│ │ └── Sign tx ─► [ Ready ]
|
|
314
|
+
│ │
|
|
315
|
+
└── Restore/import ─────► [ Custodial Encrypted ]
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
`Session Unlocked` should be short-lived and policy-limited (risk checks, cooldowns, limits).
|
|
319
|
+
|
|
320
|
+
---
|
|
321
|
+
|
|
322
|
+
### 13.5 Signing variants
|
|
323
|
+
|
|
324
|
+
## Variant A: Backend signing (fully custodial)
|
|
325
|
+
- Backend unwraps DEK and signs transactions server-side.
|
|
326
|
+
- Client receives signed transaction/hash.
|
|
327
|
+
- Simplest UX, highest custodial responsibility.
|
|
328
|
+
|
|
329
|
+
## Variant B: Backend-assisted client resolve (hybrid custodial)
|
|
330
|
+
- Backend authorizes access and returns short-lived decrypt context.
|
|
331
|
+
- Client resolves and signs locally.
|
|
332
|
+
- Better client-side control, still centralized key lifecycle.
|
|
333
|
+
|
|
334
|
+
Choose one variant explicitly in product docs and legal language.
|
|
335
|
+
|
|
336
|
+
---
|
|
337
|
+
|
|
338
|
+
### 13.6 API extension for custodial mode
|
|
339
|
+
|
|
340
|
+
```
|
|
341
|
+
POST /wallet/custodial/create
|
|
342
|
+
Body: { user_id, wallet_label, encrypted_payload, wrapped_dek, key_version }
|
|
343
|
+
Response: { ok: true, wallet_id }
|
|
344
|
+
|
|
345
|
+
POST /wallet/custodial/unlock
|
|
346
|
+
Body: { wallet_id, auth_context }
|
|
347
|
+
Response: { unlock_token, expires_at } // or server-side unlock only
|
|
348
|
+
|
|
349
|
+
POST /wallet/custodial/sign
|
|
350
|
+
Body: { wallet_id, unlock_token, tx_payload }
|
|
351
|
+
Response: { signed_tx | tx_hash }
|
|
352
|
+
|
|
353
|
+
POST /wallet/custodial/rotate-kek
|
|
354
|
+
Body: { wallet_id? | batch_selector, new_key_version }
|
|
355
|
+
Response: { rewrapped_count }
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
All endpoints must be audited and rate-limited.
|
|
359
|
+
|
|
360
|
+
---
|
|
361
|
+
|
|
362
|
+
### 13.7 Security controls required in custodial mode
|
|
363
|
+
|
|
364
|
+
- KMS/HSM-backed KEK, non-exportable where possible
|
|
365
|
+
- Strict IAM separation (runtime vs admin)
|
|
366
|
+
- Row-level access control by `user_id`
|
|
367
|
+
- Risk checks before unlock/sign (IP/device anomaly, velocity limits)
|
|
368
|
+
- Immutable security event log
|
|
369
|
+
- Emergency freeze and key-rotation runbook
|
|
370
|
+
- Signed release pipeline + dependency controls
|
|
371
|
+
|
|
372
|
+
---
|
|
373
|
+
|
|
374
|
+
### 13.8 UX implications vs non-custodial
|
|
375
|
+
|
|
376
|
+
Benefits:
|
|
377
|
+
- Easier cross-device recovery
|
|
378
|
+
- Less mnemonic friction for mainstream users
|
|
379
|
+
|
|
380
|
+
Tradeoffs:
|
|
381
|
+
- Backend/service compromise has larger blast radius
|
|
382
|
+
- Stronger legal/compliance burden
|
|
383
|
+
- Must clearly disclose custodial trust model
|
|
384
|
+
|
|
385
|
+
Recommended product stance:
|
|
386
|
+
- Keep non-custodial as default for advanced users.
|
|
387
|
+
- Offer custodial mode as explicit opt-in with clear warnings and recovery terms.
|
|
388
|
+
|
|
389
|
+
---
|
|
390
|
+
|
|
391
|
+
### 13.9 Updated roadmap including custodial track
|
|
392
|
+
|
|
393
|
+
| Phase | Non-custodial track | Custodial track |
|
|
394
|
+
|---|---|---|
|
|
395
|
+
| **Phase 1** | Create/restore local wallet, deploy flow | Auth foundation (Google/GitHub/Telegram/email OTP), user linking |
|
|
396
|
+
| **Phase 2** | Device/local storage hardening | Envelope storage (`ciphertext` + `wrapped_dek`) + KMS/HSM integration |
|
|
397
|
+
| **Phase 3** | TMA/Desktop fallback tiers | Unlock/sign endpoints + policy/rate limits |
|
|
398
|
+
| **Phase 4** | Send/receive/history | Custodial signing UX + anomaly protections |
|
|
399
|
+
| **Phase 5** | Backup/export UX hardening | Key rotation, incident runbooks, compliance hardening |
|
|
400
|
+
|
|
401
|
+
---
|
|
402
|
+
|
|
403
|
+
*Last updated: April 2026. Maintained in repo at `docs/wallets_hosting_architecture.md`.*
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { mnemonicNew, mnemonicToPrivateKey } from "@ton/crypto";
|
|
2
|
+
import { WalletContractV4 } from "@ton/ton";
|
|
3
|
+
import { Buffer as BufferPolyfill } from "buffer";
|
|
4
|
+
|
|
5
|
+
if (typeof globalThis !== "undefined" && !(globalThis as { Buffer?: unknown }).Buffer) {
|
|
6
|
+
(globalThis as { Buffer?: unknown }).Buffer = BufferPolyfill;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function bytesToBase64(bytes: Uint8Array): string {
|
|
10
|
+
let binary = "";
|
|
11
|
+
for (let i = 0; i < bytes.length; i += 1) binary += String.fromCharCode(bytes[i]);
|
|
12
|
+
if (typeof btoa === "function") return btoa(binary);
|
|
13
|
+
return BufferPolyfill.from(bytes).toString("base64");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function sha256Bytes(input: string): Promise<Uint8Array> {
|
|
17
|
+
const digest = await crypto.subtle.digest(
|
|
18
|
+
"SHA-256",
|
|
19
|
+
new TextEncoder().encode(input),
|
|
20
|
+
);
|
|
21
|
+
return new Uint8Array(digest);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function generateMnemonic(words = 24): Promise<string[]> {
|
|
25
|
+
return mnemonicNew(words);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function deriveAddressFromMnemonic(opts: {
|
|
29
|
+
mnemonic: string[];
|
|
30
|
+
testnet?: boolean;
|
|
31
|
+
workchain?: number;
|
|
32
|
+
}): Promise<string> {
|
|
33
|
+
const { mnemonic, testnet = false, workchain = 0 } = opts;
|
|
34
|
+
const keyPair = await mnemonicToPrivateKey(mnemonic);
|
|
35
|
+
const wallet = WalletContractV4.create({
|
|
36
|
+
workchain,
|
|
37
|
+
publicKey: keyPair.publicKey,
|
|
38
|
+
});
|
|
39
|
+
return wallet.address.toString({
|
|
40
|
+
bounceable: false,
|
|
41
|
+
urlSafe: true,
|
|
42
|
+
testOnly: testnet,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function deriveMasterKeyFromMnemonic(mnemonic: string[]): Promise<string> {
|
|
47
|
+
const bytes = await sha256Bytes(mnemonic.join(" "));
|
|
48
|
+
return bytesToBase64(bytes);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function createSeedCipher(
|
|
52
|
+
masterKey: string,
|
|
53
|
+
seed: string,
|
|
54
|
+
): Promise<string> {
|
|
55
|
+
const keyMaterial = await crypto.subtle.importKey(
|
|
56
|
+
"raw",
|
|
57
|
+
await sha256Bytes(masterKey),
|
|
58
|
+
{ name: "AES-GCM" },
|
|
59
|
+
false,
|
|
60
|
+
["encrypt"],
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
64
|
+
const encrypted = await crypto.subtle.encrypt(
|
|
65
|
+
{ name: "AES-GCM", iv },
|
|
66
|
+
keyMaterial,
|
|
67
|
+
new TextEncoder().encode(seed),
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const cipherBytes = new Uint8Array(encrypted);
|
|
71
|
+
return `v1.${bytesToBase64(iv)}.${bytesToBase64(cipherBytes)}`;
|
|
72
|
+
}
|
|
73
|
+
|