@meshconnect/uwc-bridge-child 0.2.2 → 0.2.3-snapshot.35caafb
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/dist/BridgeChild.d.ts +11 -3
- package/dist/BridgeChild.d.ts.map +1 -1
- package/dist/BridgeChild.js +269 -150
- package/dist/BridgeChild.js.map +1 -1
- package/dist/BridgeChild.test.js +348 -7
- package/dist/BridgeChild.test.js.map +1 -1
- package/dist/bridge-timing.d.ts +22 -0
- package/dist/bridge-timing.d.ts.map +1 -0
- package/dist/bridge-timing.js +41 -0
- package/dist/bridge-timing.js.map +1 -0
- package/dist/bridge-timing.test.d.ts +2 -0
- package/dist/bridge-timing.test.d.ts.map +1 -0
- package/dist/bridge-timing.test.js +27 -0
- package/dist/bridge-timing.test.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/BridgeChild.test.ts +393 -7
- package/src/BridgeChild.ts +320 -196
- package/src/bridge-timing.test.ts +33 -0
- package/src/bridge-timing.ts +52 -0
- package/src/index.ts +3 -0
package/src/BridgeChild.ts
CHANGED
|
@@ -3,6 +3,12 @@
|
|
|
3
3
|
|
|
4
4
|
import * as Comlink from 'comlink'
|
|
5
5
|
import { hasInFrameTonBridge } from './tonInFrameBridge'
|
|
6
|
+
import {
|
|
7
|
+
MAX_POLL_MS,
|
|
8
|
+
POLL_INTERVAL_MS,
|
|
9
|
+
raceParentRead,
|
|
10
|
+
BridgeReadTimeout
|
|
11
|
+
} from './bridge-timing'
|
|
6
12
|
|
|
7
13
|
// Define the expected parent API interface locally
|
|
8
14
|
interface ParentAPI {
|
|
@@ -23,8 +29,12 @@ export class BridgeChild {
|
|
|
23
29
|
private parentAPI?: Comlink.Remote<ParentAPI>
|
|
24
30
|
private tronInjectedIds: string[]
|
|
25
31
|
private tonJsBridgeKeys: string[]
|
|
26
|
-
|
|
27
|
-
|
|
32
|
+
|
|
33
|
+
// Per-namespace completion flags so we only marshal each set once.
|
|
34
|
+
private eip6963Done = false
|
|
35
|
+
private solanaDone = false
|
|
36
|
+
private tronDone = false
|
|
37
|
+
private tonDone = false
|
|
28
38
|
|
|
29
39
|
constructor(options?: {
|
|
30
40
|
tronInjectedIds?: string[]
|
|
@@ -41,241 +51,355 @@ export class BridgeChild {
|
|
|
41
51
|
}
|
|
42
52
|
window.UWCBridgeChildInitialized = true
|
|
43
53
|
|
|
54
|
+
// If another bootstrap already published at least one non-empty wallet
|
|
55
|
+
// array, skip everything. Matches the old BridgeChild semantics for
|
|
56
|
+
// embedded flows. Note: a previously-set empty array (`[]`, distinct from
|
|
57
|
+
// `undefined`) does NOT short-circuit us — another module saying "I
|
|
58
|
+
// couldn't find any EVM wallets" shouldn't prevent us from still
|
|
59
|
+
// discovering TON/Tron via the bridge.
|
|
60
|
+
if (this.anyWindowWalletsAlreadySet()) {
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
|
|
44
64
|
this.parentAPI = Comlink.wrap<ParentAPI>(
|
|
45
65
|
Comlink.windowEndpoint(window.parent, self, '*')
|
|
46
66
|
)
|
|
47
67
|
|
|
68
|
+
// Tron/TON discovery is driven from the poll loop (tryMarshalTron/Ton),
|
|
69
|
+
// which re-issues discoverTron/TonWallets every tick until the parent
|
|
70
|
+
// answers. A single eager trigger here would be silently lost if it raced
|
|
71
|
+
// the parent's Comlink.expose (Comlink calls hang, never reject).
|
|
72
|
+
|
|
73
|
+
// Fetch the parent origin concurrently for TON Connect manifest URL.
|
|
74
|
+
this.syncParentOrigin()
|
|
75
|
+
|
|
48
76
|
this.pollForWallets()
|
|
49
77
|
}
|
|
50
78
|
|
|
51
|
-
private
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
79
|
+
private anyWindowWalletsAlreadySet(): boolean {
|
|
80
|
+
return Boolean(
|
|
81
|
+
(window.eip6963Wallets && window.eip6963Wallets.length > 0) ||
|
|
82
|
+
(window.walletStandardWallets &&
|
|
83
|
+
window.walletStandardWallets.length > 0) ||
|
|
84
|
+
(window.tronWallets && window.tronWallets.length > 0) ||
|
|
85
|
+
(window.tonWallets && window.tonWallets.length > 0)
|
|
86
|
+
)
|
|
87
|
+
}
|
|
55
88
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
89
|
+
private async syncParentOrigin(): Promise<void> {
|
|
90
|
+
// Same late-expose hazard as the wallet poll: a parentOrigin read posted
|
|
91
|
+
// before the parent's Comlink.expose hangs forever (the request message is
|
|
92
|
+
// dropped, so it never resolves even once the parent comes up). Retry with a
|
|
93
|
+
// per-read deadline until the parent answers or the poll budget elapses.
|
|
94
|
+
// A *resolved* read is final, not retried: the current BridgeParent sets
|
|
95
|
+
// parentOrigin to window.location.origin synchronously before expose, so a
|
|
96
|
+
// resolved value is always a real origin; a falsy resolve means an older
|
|
97
|
+
// parent without parentOrigin, where spinning the full budget is pointless.
|
|
98
|
+
const startTime = Date.now()
|
|
99
|
+
while (Date.now() - startTime <= MAX_POLL_MS) {
|
|
100
|
+
try {
|
|
101
|
+
const origin = await raceParentRead(this.parentAPI?.parentOrigin)
|
|
102
|
+
if (origin) window.uwcParentOrigin = origin
|
|
103
|
+
return
|
|
104
|
+
} catch (err) {
|
|
105
|
+
if (err instanceof BridgeReadTimeout) {
|
|
106
|
+
await new Promise(resolve => setTimeout(resolve, POLL_INTERVAL_MS))
|
|
107
|
+
continue
|
|
108
|
+
}
|
|
109
|
+
// Older BridgeParent that throws on parentOrigin — ignore.
|
|
59
110
|
return
|
|
60
111
|
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
61
114
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
(
|
|
69
|
-
) {
|
|
115
|
+
private pollForWallets(): void {
|
|
116
|
+
const startTime = Date.now()
|
|
117
|
+
|
|
118
|
+
const poll = async () => {
|
|
119
|
+
if (!this.parentAPI) return
|
|
120
|
+
if (Date.now() - startTime > MAX_POLL_MS) {
|
|
121
|
+
this.fillEmptyDefaults()
|
|
70
122
|
return
|
|
71
123
|
}
|
|
72
124
|
|
|
73
|
-
|
|
74
|
-
|
|
125
|
+
// Run all four namespace marshals concurrently. Each short-circuits if
|
|
126
|
+
// already done or not yet ready, so the cost is a single
|
|
127
|
+
// `await ready` round-trip per pending namespace per tick.
|
|
128
|
+
await Promise.all([
|
|
129
|
+
this.tryMarshalEip6963(),
|
|
130
|
+
this.tryMarshalSolana(),
|
|
131
|
+
this.tryMarshalTron(),
|
|
132
|
+
this.tryMarshalTon()
|
|
133
|
+
])
|
|
134
|
+
|
|
135
|
+
// Tron/TON have no early default: tryMarshal{Tron,Ton} writes the real
|
|
136
|
+
// wallets (or [] on a ready-with-empty / unsupported parent), and the
|
|
137
|
+
// MAX_POLL_MS fillEmptyDefaults backstop covers a parent that never
|
|
138
|
+
// reports ready. Writing [] earlier would race a still-pending discovery
|
|
139
|
+
// and make the consumer's one-shot poll miss a wallet that lands a tick
|
|
140
|
+
// later.
|
|
141
|
+
|
|
142
|
+
if (
|
|
143
|
+
this.eip6963Done &&
|
|
144
|
+
this.solanaDone &&
|
|
145
|
+
this.tronDone &&
|
|
146
|
+
this.tonDone
|
|
147
|
+
) {
|
|
75
148
|
return
|
|
76
149
|
}
|
|
77
|
-
|
|
78
|
-
// Create a timeout promise to prevent hanging
|
|
79
|
-
const timeoutPromise = new Promise((_, reject) => {
|
|
80
|
-
setTimeout(() => reject(new Error('Timeout accessing parent API')), 100)
|
|
81
|
-
})
|
|
82
|
-
|
|
83
|
-
// Try to check wallets with a timeout
|
|
84
|
-
Promise.race([this.checkWallets(), timeoutPromise])
|
|
85
|
-
.then(result => {
|
|
86
|
-
if (result === 'continue') {
|
|
87
|
-
// Continue polling
|
|
88
|
-
setTimeout(poll, pollInterval)
|
|
89
|
-
}
|
|
90
|
-
// If result is 'stop', polling will stop
|
|
91
|
-
})
|
|
92
|
-
.catch(_error => {
|
|
93
|
-
// Continue polling on error
|
|
94
|
-
setTimeout(poll, pollInterval)
|
|
95
|
-
})
|
|
150
|
+
setTimeout(poll, POLL_INTERVAL_MS)
|
|
96
151
|
}
|
|
97
152
|
|
|
98
|
-
//
|
|
153
|
+
// Kick off synchronously — no first-tick delay.
|
|
99
154
|
poll()
|
|
100
155
|
}
|
|
101
156
|
|
|
102
|
-
private
|
|
157
|
+
private fillEmptyDefaults(): void {
|
|
158
|
+
if (window.eip6963Wallets === undefined) window.eip6963Wallets = []
|
|
159
|
+
if (window.walletStandardWallets === undefined) {
|
|
160
|
+
window.walletStandardWallets = []
|
|
161
|
+
}
|
|
162
|
+
if (window.tronWallets === undefined) window.tronWallets = []
|
|
163
|
+
if (window.tonWallets === undefined) window.tonWallets = []
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
private async tryMarshalEip6963(): Promise<void> {
|
|
167
|
+
if (this.eip6963Done || !this.parentAPI) return
|
|
103
168
|
try {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
169
|
+
const ready = await raceParentRead(this.parentAPI.eip6963WalletsReady)
|
|
170
|
+
if (!ready) return
|
|
171
|
+
|
|
172
|
+
const len = await this.parentAPI.eip6963Wallets.length
|
|
173
|
+
const wallets: unknown[] = []
|
|
174
|
+
if (len > 0) {
|
|
175
|
+
const indices = Array.from({ length: len }, (_, i) => i)
|
|
176
|
+
const marshalled = await Promise.all(
|
|
177
|
+
indices.map(async i => {
|
|
178
|
+
const entry = this.parentAPI!.eip6963Wallets[i]
|
|
179
|
+
const [uuid, name, icon, rdns] = await Promise.all([
|
|
180
|
+
entry.uuid,
|
|
181
|
+
entry.name,
|
|
182
|
+
entry.icon,
|
|
183
|
+
entry.rdns
|
|
184
|
+
])
|
|
185
|
+
return { uuid, name, icon, rdns, provider: entry.provider }
|
|
186
|
+
})
|
|
187
|
+
)
|
|
188
|
+
wallets.push(...marshalled)
|
|
189
|
+
}
|
|
190
|
+
window.eip6963Wallets = wallets
|
|
191
|
+
this.eip6963Done = true
|
|
192
|
+
} catch {
|
|
193
|
+
// retry on next tick
|
|
194
|
+
}
|
|
195
|
+
}
|
|
108
196
|
|
|
109
|
-
|
|
110
|
-
|
|
197
|
+
private async tryMarshalSolana(): Promise<void> {
|
|
198
|
+
if (this.solanaDone || !this.parentAPI) return
|
|
199
|
+
try {
|
|
200
|
+
const ready = await raceParentRead(
|
|
201
|
+
this.parentAPI.walletStandardWalletsReady
|
|
202
|
+
)
|
|
203
|
+
if (!ready) return
|
|
204
|
+
|
|
205
|
+
const len = await this.parentAPI.walletStandardWallets.length
|
|
206
|
+
const wallets: unknown[] = []
|
|
207
|
+
if (len > 0) {
|
|
208
|
+
const indices = Array.from({ length: len }, (_, i) => i)
|
|
209
|
+
const marshalled = await Promise.all(
|
|
210
|
+
indices.map(async i => {
|
|
211
|
+
const entry = this.parentAPI!.walletStandardWallets[i]
|
|
212
|
+
const [uuid, name, chains, features] = await Promise.all([
|
|
213
|
+
entry.uuid,
|
|
214
|
+
entry.name,
|
|
215
|
+
entry.chains,
|
|
216
|
+
entry.features
|
|
217
|
+
])
|
|
218
|
+
return { uuid, name, chains, features, adapter: entry.adapter }
|
|
219
|
+
})
|
|
220
|
+
)
|
|
221
|
+
wallets.push(...marshalled)
|
|
111
222
|
}
|
|
223
|
+
window.walletStandardWallets = wallets
|
|
224
|
+
this.solanaDone = true
|
|
225
|
+
} catch {
|
|
226
|
+
// retry on next tick
|
|
227
|
+
}
|
|
228
|
+
}
|
|
112
229
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
230
|
+
private async tryMarshalTron(): Promise<void> {
|
|
231
|
+
if (this.tronDone || !this.parentAPI) return
|
|
232
|
+
|
|
233
|
+
// (Re-)ask the parent to run Tron discovery on every tick. A one-shot
|
|
234
|
+
// trigger is silently lost if posted before the parent's Comlink.expose (the
|
|
235
|
+
// request hangs, never rejects), so re-issuing mirrors EIP-6963's per-tick
|
|
236
|
+
// resilience. Comlink calls return a Promise and never throw synchronously,
|
|
237
|
+
// so an unsupported parent is handled by the tronWalletsReady read below (or
|
|
238
|
+
// the MAX_POLL_MS backstop) — not here. The try only guards a non-Comlink
|
|
239
|
+
// stub whose call throws synchronously.
|
|
240
|
+
// Each pre-expose reissue leaves one lightweight Comlink pending-listener
|
|
241
|
+
// Map entry (requestResponseMessage — not a DOM listener or MessageChannel
|
|
242
|
+
// port); bounded by the expose delay and GC'd. An in-flight guard is
|
|
243
|
+
// intentionally avoided: it would deadlock recovery, since a never-settling
|
|
244
|
+
// pre-expose call would block all subsequent re-issues.
|
|
245
|
+
//
|
|
246
|
+
// Fired in a detached async IIFE so that the await — needed to make the
|
|
247
|
+
// try/catch absorb both sync throws (test stubs) and async rejections —
|
|
248
|
+
// does not block this tick. A pre-expose hang sits inside the IIFE
|
|
249
|
+
// harmlessly until the parent comes up or GC reaps it.
|
|
250
|
+
void (async () => {
|
|
251
|
+
try {
|
|
252
|
+
await this.parentAPI.discoverTronWallets?.(this.tronInjectedIds)
|
|
253
|
+
} catch {
|
|
254
|
+
// absorb — the readiness read below governs the outcome
|
|
129
255
|
}
|
|
256
|
+
})()
|
|
130
257
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
uuid: await this.parentAPI.walletStandardWallets[index].uuid,
|
|
140
|
-
name: await this.parentAPI.walletStandardWallets[index].name,
|
|
141
|
-
chains: await this.parentAPI.walletStandardWallets[index].chains,
|
|
142
|
-
features:
|
|
143
|
-
await this.parentAPI.walletStandardWallets[index].features,
|
|
144
|
-
adapter: this.parentAPI.walletStandardWallets[index].adapter
|
|
145
|
-
}
|
|
146
|
-
walletStandardWallets.push(wallet)
|
|
147
|
-
}
|
|
258
|
+
let ready
|
|
259
|
+
try {
|
|
260
|
+
ready = await raceParentRead(this.parentAPI.tronWalletsReady)
|
|
261
|
+
} catch (err) {
|
|
262
|
+
// Read timed out → parent not live yet, retry next tick. Any other error
|
|
263
|
+
// (older parent throwing on the property) → no Tron support.
|
|
264
|
+
if (err instanceof BridgeReadTimeout) {
|
|
265
|
+
return
|
|
148
266
|
}
|
|
267
|
+
window.tronWallets = []
|
|
268
|
+
this.tronDone = true
|
|
269
|
+
return
|
|
270
|
+
}
|
|
149
271
|
|
|
150
|
-
|
|
151
|
-
// with older BridgeParent versions that don't have Tron support
|
|
152
|
-
let tronWallets: unknown[] = []
|
|
153
|
-
try {
|
|
154
|
-
if (!this.tronDiscoveryTriggered) {
|
|
155
|
-
await this.parentAPI?.discoverTronWallets?.(this.tronInjectedIds)
|
|
156
|
-
this.tronDiscoveryTriggered = true
|
|
157
|
-
}
|
|
272
|
+
if (!ready) return
|
|
158
273
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
// Parent doesn't support Tron - use empty array
|
|
177
|
-
tronWallets = []
|
|
274
|
+
try {
|
|
275
|
+
const len = await this.parentAPI.tronWallets.length
|
|
276
|
+
const wallets: unknown[] = []
|
|
277
|
+
if (len > 0) {
|
|
278
|
+
const indices = Array.from({ length: len }, (_, i) => i)
|
|
279
|
+
const marshalled = await Promise.all(
|
|
280
|
+
indices.map(async i => {
|
|
281
|
+
const entry = this.parentAPI!.tronWallets[i]
|
|
282
|
+
const [uuid, name, injectedId] = await Promise.all([
|
|
283
|
+
entry.uuid,
|
|
284
|
+
entry.name,
|
|
285
|
+
entry.injectedId
|
|
286
|
+
])
|
|
287
|
+
return { uuid, name, injectedId, provider: entry.provider }
|
|
288
|
+
})
|
|
289
|
+
)
|
|
290
|
+
wallets.push(...marshalled)
|
|
178
291
|
}
|
|
292
|
+
window.tronWallets = wallets
|
|
293
|
+
this.tronDone = true
|
|
294
|
+
} catch {
|
|
295
|
+
window.tronWallets = []
|
|
296
|
+
this.tronDone = true
|
|
297
|
+
}
|
|
298
|
+
}
|
|
179
299
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
let tonWallets: unknown[] = []
|
|
183
|
-
try {
|
|
184
|
-
if (!this.tonDiscoveryTriggered) {
|
|
185
|
-
await this.parentAPI?.discoverTonWallets?.(this.tonJsBridgeKeys)
|
|
186
|
-
this.tonDiscoveryTriggered = true
|
|
187
|
-
}
|
|
300
|
+
private async tryMarshalTon(): Promise<void> {
|
|
301
|
+
if (this.tonDone || !this.parentAPI) return
|
|
188
302
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
for (let index = 0; index < availableTonWallets; index++) {
|
|
195
|
-
const wallet = {
|
|
196
|
-
uuid: await this.parentAPI.tonWallets[index].uuid,
|
|
197
|
-
name: await this.parentAPI.tonWallets[index].name,
|
|
198
|
-
icon: await this.parentAPI.tonWallets[index].icon,
|
|
199
|
-
jsBridgeKey: await this.parentAPI.tonWallets[index].jsBridgeKey,
|
|
200
|
-
provider: this.parentAPI.tonWallets[index].provider
|
|
201
|
-
}
|
|
202
|
-
tonWallets.push(wallet)
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
}
|
|
303
|
+
// Re-issue discovery each tick — same expose-race resilience as Tron.
|
|
304
|
+
// See tryMarshalTron for the detached-IIFE rationale.
|
|
305
|
+
void (async () => {
|
|
306
|
+
try {
|
|
307
|
+
await this.parentAPI.discoverTonWallets?.(this.tonJsBridgeKeys)
|
|
206
308
|
} catch {
|
|
207
|
-
//
|
|
208
|
-
tonWallets = []
|
|
309
|
+
// absorb — the readiness read below governs the outcome
|
|
209
310
|
}
|
|
311
|
+
})()
|
|
210
312
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
313
|
+
let ready
|
|
314
|
+
try {
|
|
315
|
+
ready = await raceParentRead(this.parentAPI.tonWalletsReady)
|
|
316
|
+
} catch (err) {
|
|
317
|
+
if (err instanceof BridgeReadTimeout) return
|
|
318
|
+
window.tonWallets = []
|
|
319
|
+
this.tonDone = true
|
|
320
|
+
return
|
|
321
|
+
}
|
|
322
|
+
if (!ready) return
|
|
323
|
+
|
|
324
|
+
try {
|
|
325
|
+
const len = await this.parentAPI.tonWallets.length
|
|
326
|
+
const wallets: Array<{
|
|
327
|
+
uuid: string
|
|
328
|
+
name: string
|
|
329
|
+
icon: string
|
|
330
|
+
jsBridgeKey: string
|
|
331
|
+
provider: any
|
|
332
|
+
}> = []
|
|
333
|
+
|
|
334
|
+
if (len > 0) {
|
|
335
|
+
const indices = Array.from({ length: len }, (_, i) => i)
|
|
336
|
+
const marshalled = await Promise.all(
|
|
337
|
+
indices.map(async i => {
|
|
338
|
+
const entry = this.parentAPI!.tonWallets[i]
|
|
339
|
+
const [uuid, name, icon, jsBridgeKey] = await Promise.all([
|
|
340
|
+
entry.uuid,
|
|
341
|
+
entry.name,
|
|
342
|
+
entry.icon,
|
|
343
|
+
entry.jsBridgeKey
|
|
344
|
+
])
|
|
345
|
+
return { uuid, name, icon, jsBridgeKey, provider: entry.provider }
|
|
346
|
+
})
|
|
347
|
+
)
|
|
348
|
+
wallets.push(...marshalled)
|
|
219
349
|
}
|
|
220
350
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
window.
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
Promise.resolve(result).then((unsub: unknown) => {
|
|
265
|
-
unsubscribe = typeof unsub === 'function' ? unsub : () => {}
|
|
266
|
-
})
|
|
267
|
-
return () => {
|
|
268
|
-
if (unsubscribe) unsubscribe()
|
|
269
|
-
}
|
|
270
|
-
},
|
|
271
|
-
disconnect: () => proxy.disconnect?.()
|
|
351
|
+
window.tonWallets = wallets
|
|
352
|
+
|
|
353
|
+
// Shim window[jsBridgeKey].tonconnect so @tonconnect/sdk finds the
|
|
354
|
+
// Comlink proxy. Static props must be sync, but Comlink returns
|
|
355
|
+
// Promises — so we prefetch them here in parallel.
|
|
356
|
+
await Promise.all(
|
|
357
|
+
wallets.map(async wallet => {
|
|
358
|
+
if (!wallet.jsBridgeKey || !wallet.provider) return
|
|
359
|
+
// Keep the real in-frame provider so connect() attributes to the
|
|
360
|
+
// iframe origin; shimming would attribute to the parent, which the
|
|
361
|
+
// wallet's manifest origin check rejects.
|
|
362
|
+
if (hasInFrameTonBridge(wallet.jsBridgeKey)) return
|
|
363
|
+
const proxy = wallet.provider
|
|
364
|
+
const [deviceInfo, walletInfo, protocolVersion, isWalletBrowser] =
|
|
365
|
+
await Promise.all([
|
|
366
|
+
proxy.deviceInfo,
|
|
367
|
+
proxy.walletInfo,
|
|
368
|
+
proxy.protocolVersion,
|
|
369
|
+
proxy.isWalletBrowser
|
|
370
|
+
])
|
|
371
|
+
|
|
372
|
+
;(window as any)[wallet.jsBridgeKey] = {
|
|
373
|
+
tonconnect: {
|
|
374
|
+
deviceInfo,
|
|
375
|
+
walletInfo,
|
|
376
|
+
protocolVersion: protocolVersion ?? 2,
|
|
377
|
+
isWalletBrowser: isWalletBrowser ?? false,
|
|
378
|
+
connect: (ver: number, msg: unknown) => proxy.connect(ver, msg),
|
|
379
|
+
restoreConnection: () => proxy.restoreConnection(),
|
|
380
|
+
send: (msg: unknown) => proxy.send(msg),
|
|
381
|
+
listen: (callback: (event: unknown) => void) => {
|
|
382
|
+
const proxiedCallback = Comlink.proxy(callback)
|
|
383
|
+
let unsubscribe: (() => void) | null = null
|
|
384
|
+
const result = proxy.listen(proxiedCallback)
|
|
385
|
+
Promise.resolve(result).then((unsub: unknown) => {
|
|
386
|
+
unsubscribe = typeof unsub === 'function' ? unsub : () => {}
|
|
387
|
+
})
|
|
388
|
+
return () => {
|
|
389
|
+
if (unsubscribe) unsubscribe()
|
|
390
|
+
}
|
|
391
|
+
},
|
|
392
|
+
disconnect: () => proxy.disconnect?.()
|
|
393
|
+
}
|
|
272
394
|
}
|
|
273
|
-
}
|
|
274
|
-
|
|
395
|
+
})
|
|
396
|
+
)
|
|
275
397
|
|
|
276
|
-
|
|
398
|
+
this.tonDone = true
|
|
277
399
|
} catch {
|
|
278
|
-
|
|
400
|
+
// Parent doesn't support TON — mark done with empty list.
|
|
401
|
+
window.tonWallets = []
|
|
402
|
+
this.tonDone = true
|
|
279
403
|
}
|
|
280
404
|
}
|
|
281
405
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
2
|
+
import { raceParentRead, BridgeReadTimeout } from './bridge-timing'
|
|
3
|
+
|
|
4
|
+
describe('raceParentRead', () => {
|
|
5
|
+
beforeEach(() => vi.useFakeTimers())
|
|
6
|
+
afterEach(() => vi.useRealTimers())
|
|
7
|
+
|
|
8
|
+
it('resolves with the value when the read settles before the deadline', async () => {
|
|
9
|
+
const result = await raceParentRead(Promise.resolve('origin'))
|
|
10
|
+
expect(result).toBe('origin')
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it('resolves a non-promise value', async () => {
|
|
14
|
+
const result = await raceParentRead(true)
|
|
15
|
+
expect(result).toBe(true)
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('rejects with BridgeReadTimeout when the read never settles', async () => {
|
|
19
|
+
const neverSettles = new Promise<string>(() => {})
|
|
20
|
+
const assertion = expect(
|
|
21
|
+
raceParentRead(neverSettles)
|
|
22
|
+
).rejects.toBeInstanceOf(BridgeReadTimeout)
|
|
23
|
+
await vi.advanceTimersByTimeAsync(200)
|
|
24
|
+
await assertion
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('clears the deadline timer on the winning-read path (no leaked timer)', async () => {
|
|
28
|
+
const clearSpy = vi.spyOn(globalThis, 'clearTimeout')
|
|
29
|
+
await raceParentRead(Promise.resolve('x'))
|
|
30
|
+
expect(clearSpy).toHaveBeenCalled()
|
|
31
|
+
clearSpy.mockRestore()
|
|
32
|
+
})
|
|
33
|
+
})
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Timing primitives for the bridge child's wallet poll.
|
|
3
|
+
*
|
|
4
|
+
* Extracted into a typed module (BridgeChild.ts is `@ts-nocheck`) so this
|
|
5
|
+
* infrastructure is type-checked.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Child's give-up deadline for the wallet poll: writes [] via fillEmptyDefaults
|
|
9
|
+
// if the parent never reports ready. The injected-connector consumer derives its
|
|
10
|
+
// own poll budget (BRIDGE_WALLET_POLL_MS) from this and MUST exceed it, so it can
|
|
11
|
+
// observe a wallet the child marshals on its final tick.
|
|
12
|
+
export const MAX_POLL_MS = 5000
|
|
13
|
+
|
|
14
|
+
export const POLL_INTERVAL_MS = 50
|
|
15
|
+
|
|
16
|
+
// A parent read is a postMessage round-trip — typically <5ms, so 150ms is
|
|
17
|
+
// generous. A spurious timeout (parent briefly busy) just retries on the next
|
|
18
|
+
// tick; it never drops a result.
|
|
19
|
+
const PARENT_READ_TIMEOUT_MS = 150
|
|
20
|
+
|
|
21
|
+
export class BridgeReadTimeout extends Error {
|
|
22
|
+
constructor() {
|
|
23
|
+
super('bridge parent read timed out')
|
|
24
|
+
this.name = 'BridgeReadTimeout'
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* A Comlink read to a parent that hasn't `Comlink.expose`d yet never settles
|
|
30
|
+
* (comlink's `requestResponseMessage` has no timeout, and the request message is
|
|
31
|
+
* dropped — so it won't resolve even once the parent comes up; a *new* read must
|
|
32
|
+
* be posted). The parent is typically created on the iframe's `load` event,
|
|
33
|
+
* which can fire after the child starts polling, so the first reads would hang
|
|
34
|
+
* the poll loop forever. Race every read against a short deadline so a pre-expose
|
|
35
|
+
* tick rejects (and the caller retries) instead of stalling.
|
|
36
|
+
*/
|
|
37
|
+
export function raceParentRead<T>(
|
|
38
|
+
read: PromiseLike<T> | T
|
|
39
|
+
): Promise<Awaited<T>> {
|
|
40
|
+
let timer: ReturnType<typeof setTimeout> | undefined
|
|
41
|
+
const timeout = new Promise<never>((_, reject) => {
|
|
42
|
+
timer = setTimeout(
|
|
43
|
+
() => reject(new BridgeReadTimeout()),
|
|
44
|
+
PARENT_READ_TIMEOUT_MS
|
|
45
|
+
)
|
|
46
|
+
})
|
|
47
|
+
// clearTimeout on settle so a winning read doesn't leak a pending timer. (The
|
|
48
|
+
// losing Comlink read can't be cancelled, but the timer can.)
|
|
49
|
+
return Promise.race([Promise.resolve(read), timeout]).finally(() =>
|
|
50
|
+
clearTimeout(timer)
|
|
51
|
+
)
|
|
52
|
+
}
|