@thru/wallet 0.2.22
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 +67 -0
- package/android/build.gradle +37 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/java/org/thru/walletnative/ThruWebViewBridgeModule.kt +77 -0
- package/app.plugin.cjs +101 -0
- package/dist/BrowserSDK-CpRFiJsW.d.ts +409 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +941 -0
- package/dist/index.js.map +1 -0
- package/dist/native/react.d.ts +109 -0
- package/dist/native/react.js +2381 -0
- package/dist/native/react.js.map +1 -0
- package/dist/native.d.ts +329 -0
- package/dist/native.js +1126 -0
- package/dist/native.js.map +1 -0
- package/dist/react-ui.d.ts +5 -0
- package/dist/react-ui.js +266 -0
- package/dist/react-ui.js.map +1 -0
- package/dist/react.d.ts +66 -0
- package/dist/react.js +1151 -0
- package/dist/react.js.map +1 -0
- package/expo-module.config.json +6 -0
- package/package.json +114 -0
- package/src/BrowserSDK.ts +315 -0
- package/src/index.ts +27 -0
- package/src/interfaces/IThruChain.ts +37 -0
- package/src/interfaces/accounts.ts +61 -0
- package/src/interfaces/index.ts +9 -0
- package/src/interfaces/types.ts +95 -0
- package/src/native/NativeSDK.test.ts +819 -0
- package/src/native/NativeSDK.ts +773 -0
- package/src/native/index.ts +39 -0
- package/src/native/provider/NativeProvider.ts +363 -0
- package/src/native/provider/WebViewBridge.test.ts +339 -0
- package/src/native/provider/WebViewBridge.ts +339 -0
- package/src/native/provider/chains/ThruChain.ts +85 -0
- package/src/native/provider/shell.html +88 -0
- package/src/native/provider/shell.test.ts +56 -0
- package/src/native/provider/shell.ts +111 -0
- package/src/native/provider/shims-html.d.ts +4 -0
- package/src/native/react/ThruContext.ts +37 -0
- package/src/native/react/ThruProvider.tsx +168 -0
- package/src/native/react/ThruWalletSheet.tsx +1162 -0
- package/src/native/react/android-webauthn.ts +37 -0
- package/src/native/react/hooks/useAccounts.ts +35 -0
- package/src/native/react/hooks/useThru.ts +11 -0
- package/src/native/react/hooks/useWallet.ts +71 -0
- package/src/native/react/hooks/useWalletAvailability.ts +31 -0
- package/src/native/react/hooks/waitForWallet.ts +21 -0
- package/src/native/react/index.ts +29 -0
- package/src/protocol/index.ts +2 -0
- package/src/protocol/postMessage.ts +283 -0
- package/src/protocol/walletState.ts +12 -0
- package/src/provider/EmbeddedProvider.ts +330 -0
- package/src/provider/IframeManager.ts +438 -0
- package/src/provider/chains/ThruChain.ts +86 -0
- package/src/provider/index.ts +17 -0
- package/src/provider/types/messages.ts +37 -0
- package/src/react/ThruContext.ts +31 -0
- package/src/react/ThruProvider.tsx +169 -0
- package/src/react/hooks/useAccounts.ts +38 -0
- package/src/react/hooks/useThru.ts +11 -0
- package/src/react/hooks/useWallet.ts +81 -0
- package/src/react/index.ts +30 -0
- package/src/react-ui/ThruAccountSwitcher.tsx +187 -0
- package/src/react-ui/custom.d.ts +8 -0
- package/src/react-ui/index.ts +1 -0
- package/src/static/logo.png +0 -0
- package/src/static/logomark_red.svg +11 -0
package/dist/native.js
ADDED
|
@@ -0,0 +1,1126 @@
|
|
|
1
|
+
import { createThruClient } from '@thru/sdk/client';
|
|
2
|
+
|
|
3
|
+
// src/interfaces/accounts.ts
|
|
4
|
+
function resolveSelectedWalletAccount(accounts, selectedAccount) {
|
|
5
|
+
if (selectedAccount) {
|
|
6
|
+
return accounts.find((account) => account.address === selectedAccount.address) ?? selectedAccount;
|
|
7
|
+
}
|
|
8
|
+
return accounts[0] ?? null;
|
|
9
|
+
}
|
|
10
|
+
function resolveWalletAccountByAddress(accounts, address) {
|
|
11
|
+
if (!address) return null;
|
|
12
|
+
return accounts.find((account) => account.address === address) ?? null;
|
|
13
|
+
}
|
|
14
|
+
function normalizeActiveWalletAccounts(accounts, selectedAccount) {
|
|
15
|
+
const activeAccount = resolveSelectedWalletAccount(accounts, selectedAccount);
|
|
16
|
+
return {
|
|
17
|
+
accounts: activeAccount ? [activeAccount] : [],
|
|
18
|
+
selectedAccount: activeAccount
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function normalizeWalletAccountResult(result, selectedAccount) {
|
|
22
|
+
const active = normalizeActiveWalletAccounts(
|
|
23
|
+
result.accounts,
|
|
24
|
+
selectedAccount ?? result.selectedAccount ?? null
|
|
25
|
+
);
|
|
26
|
+
return {
|
|
27
|
+
...result,
|
|
28
|
+
accounts: active.accounts,
|
|
29
|
+
selectedAccount: active.selectedAccount
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// src/interfaces/types.ts
|
|
34
|
+
var AddressType = {
|
|
35
|
+
THRU: "thru"
|
|
36
|
+
};
|
|
37
|
+
var ThruTransactionEncoding = {
|
|
38
|
+
SIGNING_PAYLOAD_BASE64: "signing_payload_base64",
|
|
39
|
+
RAW_TRANSACTION_BASE64: "raw_transaction_base64"
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// src/protocol/postMessage.ts
|
|
43
|
+
var POST_MESSAGE_REQUEST_TYPES = {
|
|
44
|
+
CONNECT: "connect",
|
|
45
|
+
DISCONNECT: "disconnect",
|
|
46
|
+
SIGN_MESSAGE: "signMessage",
|
|
47
|
+
SIGN_TRANSACTION: "signTransaction",
|
|
48
|
+
GET_ACCOUNTS: "getAccounts",
|
|
49
|
+
GET_CONNECTION_STATE: "getConnectionState",
|
|
50
|
+
GET_SIGNING_CONTEXT: "getSigningContext",
|
|
51
|
+
SELECT_ACCOUNT: "selectAccount",
|
|
52
|
+
MANAGE_ACCOUNTS: "manageAccounts"
|
|
53
|
+
};
|
|
54
|
+
var EMBEDDED_PROVIDER_EVENTS = {
|
|
55
|
+
CONNECT_START: "connect_start",
|
|
56
|
+
CONNECT: "connect",
|
|
57
|
+
DISCONNECT: "disconnect",
|
|
58
|
+
CONNECT_ERROR: "connect_error",
|
|
59
|
+
ERROR: "error",
|
|
60
|
+
LOCK: "lock",
|
|
61
|
+
UI_SHOW: "ui_show",
|
|
62
|
+
ACCOUNT_CHANGED: "account_changed"
|
|
63
|
+
};
|
|
64
|
+
var POST_MESSAGE_EVENT_TYPE = "event";
|
|
65
|
+
var IFRAME_READY_EVENT = "iframe:ready";
|
|
66
|
+
var REQUEST_ID_PREFIX = "req";
|
|
67
|
+
var createRequestId = (prefix = REQUEST_ID_PREFIX) => {
|
|
68
|
+
const random = Math.random().toString(36).slice(2, 11);
|
|
69
|
+
return `${prefix}_${Date.now()}_${random}`;
|
|
70
|
+
};
|
|
71
|
+
var ErrorCode = {
|
|
72
|
+
USER_REJECTED: "USER_REJECTED",
|
|
73
|
+
WALLET_LOCKED: "WALLET_LOCKED",
|
|
74
|
+
INVALID_PASSWORD: "INVALID_PASSWORD",
|
|
75
|
+
ALREADY_CONNECTED: "ALREADY_CONNECTED",
|
|
76
|
+
ACCOUNT_NOT_FOUND: "ACCOUNT_NOT_FOUND",
|
|
77
|
+
ACCOUNT_CHANGED: "ACCOUNT_CHANGED",
|
|
78
|
+
INVALID_TRANSACTION: "INVALID_TRANSACTION",
|
|
79
|
+
TRANSACTION_FAILED: "TRANSACTION_FAILED",
|
|
80
|
+
INSUFFICIENT_FUNDS: "INSUFFICIENT_FUNDS",
|
|
81
|
+
NETWORK_ERROR: "NETWORK_ERROR",
|
|
82
|
+
TIMEOUT: "TIMEOUT",
|
|
83
|
+
UNKNOWN_ERROR: "UNKNOWN_ERROR"
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// src/protocol/walletState.ts
|
|
87
|
+
function normalizeConnectionStateResult(result) {
|
|
88
|
+
if (!result.isAuthorized || !result.hasPasskey) {
|
|
89
|
+
return { ...result, accounts: [], selectedAccount: null };
|
|
90
|
+
}
|
|
91
|
+
return normalizeWalletAccountResult(result);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// src/native/provider/chains/ThruChain.ts
|
|
95
|
+
var NativeThruChain = class {
|
|
96
|
+
constructor(bridge, provider, origin) {
|
|
97
|
+
this.bridge = bridge;
|
|
98
|
+
this.provider = provider;
|
|
99
|
+
this.origin = origin;
|
|
100
|
+
}
|
|
101
|
+
get connected() {
|
|
102
|
+
return this.provider.isConnected();
|
|
103
|
+
}
|
|
104
|
+
async connect() {
|
|
105
|
+
const result = await this.provider.connect();
|
|
106
|
+
const selectedAccount = result.selectedAccount;
|
|
107
|
+
const thruAccount = selectedAccount?.accountType === AddressType.THRU ? selectedAccount : result.accounts.find((addr) => addr.accountType === AddressType.THRU);
|
|
108
|
+
if (!thruAccount) {
|
|
109
|
+
throw new Error("Thru address not found in connection result");
|
|
110
|
+
}
|
|
111
|
+
return { publicKey: thruAccount.address };
|
|
112
|
+
}
|
|
113
|
+
async disconnect() {
|
|
114
|
+
await this.provider.disconnect();
|
|
115
|
+
}
|
|
116
|
+
async getSigningContext() {
|
|
117
|
+
if (!this.provider.isConnected()) {
|
|
118
|
+
throw new Error("Wallet not connected");
|
|
119
|
+
}
|
|
120
|
+
const response = await this.bridge.sendMessage({
|
|
121
|
+
id: createRequestId(),
|
|
122
|
+
type: POST_MESSAGE_REQUEST_TYPES.GET_SIGNING_CONTEXT,
|
|
123
|
+
origin: this.origin
|
|
124
|
+
});
|
|
125
|
+
return response.result.signingContext;
|
|
126
|
+
}
|
|
127
|
+
async signTransaction(transaction) {
|
|
128
|
+
if (!this.provider.isConnected()) {
|
|
129
|
+
throw new Error("Wallet not connected");
|
|
130
|
+
}
|
|
131
|
+
this.provider.requestShow();
|
|
132
|
+
try {
|
|
133
|
+
const response = await this.bridge.sendMessage({
|
|
134
|
+
id: createRequestId(),
|
|
135
|
+
type: POST_MESSAGE_REQUEST_TYPES.SIGN_TRANSACTION,
|
|
136
|
+
payload: {
|
|
137
|
+
walletAddress: transaction.walletAddress,
|
|
138
|
+
programAddress: transaction.programAddress,
|
|
139
|
+
instructionData: transaction.instructionData,
|
|
140
|
+
readWriteAddresses: transaction.readWriteAddresses,
|
|
141
|
+
readOnlyAddresses: transaction.readOnlyAddresses,
|
|
142
|
+
review: transaction.review
|
|
143
|
+
},
|
|
144
|
+
origin: this.origin
|
|
145
|
+
});
|
|
146
|
+
return response.result.signedTransaction;
|
|
147
|
+
} finally {
|
|
148
|
+
this.provider.requestHide();
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
// src/native/provider/WebViewBridge.ts
|
|
154
|
+
var PRODUCTION_WALLET_ORIGINS = ["https://wallet.thru.org"];
|
|
155
|
+
function isDevelopmentBuild() {
|
|
156
|
+
const runtime = globalThis;
|
|
157
|
+
const devFlag = runtime.__DEV__;
|
|
158
|
+
if (typeof devFlag === "boolean") return devFlag;
|
|
159
|
+
return runtime.process?.env?.NODE_ENV !== void 0 && runtime.process.env.NODE_ENV !== "production";
|
|
160
|
+
}
|
|
161
|
+
function isPrivateIpv4Host(hostname) {
|
|
162
|
+
const parts = hostname.split(".").map((part) => Number(part));
|
|
163
|
+
if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
const [a, b] = parts;
|
|
167
|
+
return a === 10 || a === 127 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168 || a === 100 && b >= 64 && b <= 127;
|
|
168
|
+
}
|
|
169
|
+
function isAllowedDevelopmentOrigin(url) {
|
|
170
|
+
if (!isDevelopmentBuild()) return false;
|
|
171
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return false;
|
|
172
|
+
const hostname = url.hostname.toLowerCase();
|
|
173
|
+
return hostname === "localhost" || hostname === "::1" || !hostname.includes(".") || hostname.endsWith(".local") || hostname.endsWith(".ts.net") || isPrivateIpv4Host(hostname);
|
|
174
|
+
}
|
|
175
|
+
function validateWalletOrigin(walletUrl) {
|
|
176
|
+
let url;
|
|
177
|
+
try {
|
|
178
|
+
url = new URL(walletUrl);
|
|
179
|
+
} catch {
|
|
180
|
+
throw new Error(
|
|
181
|
+
`Invalid wallet URL: ${walletUrl}. URL must be a valid absolute URL.`
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
const origin = url.origin;
|
|
185
|
+
const isAllowed = PRODUCTION_WALLET_ORIGINS.includes(origin) || isAllowedDevelopmentOrigin(url);
|
|
186
|
+
if (!isAllowed) {
|
|
187
|
+
throw new Error(
|
|
188
|
+
`Untrusted wallet origin: ${origin}. Only trusted origins are allowed: ${PRODUCTION_WALLET_ORIGINS.join(", ")}. Development builds also allow localhost, LAN, and Tailscale wallet origins.`
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
var READY_TIMEOUT_MS = 1e4;
|
|
193
|
+
var SLOW_REQUEST_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
194
|
+
var FAST_REQUEST_TIMEOUT_MS = 30 * 1e3;
|
|
195
|
+
var SLOW_REQUEST_TYPES = /* @__PURE__ */ new Set([
|
|
196
|
+
POST_MESSAGE_REQUEST_TYPES.CONNECT,
|
|
197
|
+
POST_MESSAGE_REQUEST_TYPES.SIGN_MESSAGE,
|
|
198
|
+
POST_MESSAGE_REQUEST_TYPES.SIGN_TRANSACTION,
|
|
199
|
+
POST_MESSAGE_REQUEST_TYPES.MANAGE_ACCOUNTS
|
|
200
|
+
]);
|
|
201
|
+
var WebViewBridge = class {
|
|
202
|
+
constructor(options) {
|
|
203
|
+
this.webView = null;
|
|
204
|
+
this.ready = false;
|
|
205
|
+
this.readyPromise = null;
|
|
206
|
+
this.resolveReady = null;
|
|
207
|
+
this.rejectReady = null;
|
|
208
|
+
this.readyTimer = null;
|
|
209
|
+
this.messageHandlers = /* @__PURE__ */ new Map();
|
|
210
|
+
validateWalletOrigin(options.walletUrl);
|
|
211
|
+
this.walletUrl = options.walletUrl;
|
|
212
|
+
this.walletOrigin = new URL(options.walletUrl).origin;
|
|
213
|
+
this.frameId = createRequestId("frame");
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Compose the URL to load inside the shell <iframe>. The host
|
|
217
|
+
* (ThruWalletSheet) calls this when building the shell HTML.
|
|
218
|
+
*/
|
|
219
|
+
getIframeSrc() {
|
|
220
|
+
const url = new URL(this.walletUrl);
|
|
221
|
+
if (!url.pathname.endsWith("/native")) {
|
|
222
|
+
url.pathname = `${url.pathname.replace(/\/$/, "")}/native`;
|
|
223
|
+
}
|
|
224
|
+
url.searchParams.set("tn_frame_id", this.frameId);
|
|
225
|
+
return url.toString();
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Hand the bridge a WebView ref. Required before `awaitReady()` /
|
|
229
|
+
* `sendMessage()` will resolve.
|
|
230
|
+
*/
|
|
231
|
+
attachWebView(ref) {
|
|
232
|
+
this.webView = ref;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Mark the bridge ready when the native host loads the wallet as the
|
|
236
|
+
* top-level WebView document instead of through the shell iframe.
|
|
237
|
+
*/
|
|
238
|
+
markReady() {
|
|
239
|
+
if (this.ready) return;
|
|
240
|
+
this.ready = true;
|
|
241
|
+
if (this.readyTimer) clearTimeout(this.readyTimer);
|
|
242
|
+
this.readyTimer = null;
|
|
243
|
+
const r = this.resolveReady;
|
|
244
|
+
this.resolveReady = null;
|
|
245
|
+
this.rejectReady = null;
|
|
246
|
+
r?.();
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Returns a promise that resolves when the iframe sends
|
|
250
|
+
* IFRAME_READY_EVENT. Idempotent: returns the same promise on
|
|
251
|
+
* subsequent calls. Rejects after READY_TIMEOUT_MS.
|
|
252
|
+
*/
|
|
253
|
+
awaitReady() {
|
|
254
|
+
if (this.ready) return Promise.resolve();
|
|
255
|
+
if (this.readyPromise) return this.readyPromise;
|
|
256
|
+
this.readyPromise = new Promise((resolve, reject) => {
|
|
257
|
+
this.resolveReady = resolve;
|
|
258
|
+
this.rejectReady = reject;
|
|
259
|
+
this.readyTimer = setTimeout(() => {
|
|
260
|
+
this.readyTimer = null;
|
|
261
|
+
if (this.rejectReady) {
|
|
262
|
+
const r = this.rejectReady;
|
|
263
|
+
this.rejectReady = null;
|
|
264
|
+
this.resolveReady = null;
|
|
265
|
+
r(new Error("WebView ready timeout - wallet failed to load"));
|
|
266
|
+
}
|
|
267
|
+
}, READY_TIMEOUT_MS);
|
|
268
|
+
});
|
|
269
|
+
return this.readyPromise;
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Send a request to the iframe (via injectJavaScript -> shell ->
|
|
273
|
+
* iframe.postMessage) and resolve with the matching response.
|
|
274
|
+
*/
|
|
275
|
+
async sendMessage(request) {
|
|
276
|
+
await this.awaitReady();
|
|
277
|
+
if (!this.webView) {
|
|
278
|
+
throw new Error("WebView not attached - call attachWebView() first");
|
|
279
|
+
}
|
|
280
|
+
const timeoutMs = SLOW_REQUEST_TYPES.has(request.type) ? SLOW_REQUEST_TIMEOUT_MS : FAST_REQUEST_TIMEOUT_MS;
|
|
281
|
+
return new Promise((resolve, reject) => {
|
|
282
|
+
const timer = setTimeout(() => {
|
|
283
|
+
this.messageHandlers.delete(request.id);
|
|
284
|
+
reject(new Error("Request timeout - wallet did not respond"));
|
|
285
|
+
}, timeoutMs);
|
|
286
|
+
this.messageHandlers.set(request.id, (response) => {
|
|
287
|
+
clearTimeout(timer);
|
|
288
|
+
this.messageHandlers.delete(request.id);
|
|
289
|
+
if (response.success) {
|
|
290
|
+
resolve(
|
|
291
|
+
response
|
|
292
|
+
);
|
|
293
|
+
} else {
|
|
294
|
+
const err = new Error(response.error?.message || "Unknown error");
|
|
295
|
+
err.code = response.error?.code;
|
|
296
|
+
err.data = response.error?.data;
|
|
297
|
+
reject(err);
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
const script = `try {
|
|
301
|
+
var msg = ${JSON.stringify(request)};
|
|
302
|
+
if (window.__pushIn) {
|
|
303
|
+
window.__pushIn(msg);
|
|
304
|
+
} else {
|
|
305
|
+
window.dispatchEvent(new MessageEvent('message', {
|
|
306
|
+
data: msg,
|
|
307
|
+
origin: msg.origin || ''
|
|
308
|
+
}));
|
|
309
|
+
}
|
|
310
|
+
} catch (e) {} ; true;`;
|
|
311
|
+
this.webView.injectJavaScript(script);
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Reject all in-flight wallet requests when the native host dismisses the
|
|
316
|
+
* WebView without waiting for a wallet-side response.
|
|
317
|
+
*/
|
|
318
|
+
rejectPendingRequests(message = "User rejected the request") {
|
|
319
|
+
for (const [id, handler] of Array.from(this.messageHandlers.entries())) {
|
|
320
|
+
handler({
|
|
321
|
+
id,
|
|
322
|
+
success: false,
|
|
323
|
+
error: {
|
|
324
|
+
code: ErrorCode.USER_REJECTED,
|
|
325
|
+
message
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Hook this into <WebView onMessage>. The shell forwards iframe
|
|
332
|
+
* postMessage payloads to ReactNativeWebView; we route them here.
|
|
333
|
+
*/
|
|
334
|
+
onMessage(event) {
|
|
335
|
+
let data;
|
|
336
|
+
try {
|
|
337
|
+
data = JSON.parse(event.nativeEvent.data);
|
|
338
|
+
} catch {
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
if (!data || typeof data !== "object") return;
|
|
342
|
+
const msg = data;
|
|
343
|
+
if (msg.frameId !== this.frameId) return;
|
|
344
|
+
if (msg.type === IFRAME_READY_EVENT) {
|
|
345
|
+
this.markReady();
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
if (typeof msg.id === "string" && this.messageHandlers.has(msg.id)) {
|
|
349
|
+
const handler = this.messageHandlers.get(msg.id);
|
|
350
|
+
handler(msg);
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
if (msg.type === POST_MESSAGE_EVENT_TYPE) {
|
|
354
|
+
const evt = msg;
|
|
355
|
+
this.onEvent?.(evt.event, evt.data);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Drop pending handlers and clear ready promise. Call when the host
|
|
360
|
+
* unmounts the WebView.
|
|
361
|
+
*/
|
|
362
|
+
destroy() {
|
|
363
|
+
if (this.readyTimer) {
|
|
364
|
+
clearTimeout(this.readyTimer);
|
|
365
|
+
this.readyTimer = null;
|
|
366
|
+
}
|
|
367
|
+
if (this.rejectReady && this.readyPromise) {
|
|
368
|
+
this.readyPromise.catch(() => {
|
|
369
|
+
});
|
|
370
|
+
this.rejectReady(new Error("Bridge destroyed"));
|
|
371
|
+
}
|
|
372
|
+
this.resolveReady = null;
|
|
373
|
+
this.rejectReady = null;
|
|
374
|
+
this.readyPromise = null;
|
|
375
|
+
this.ready = false;
|
|
376
|
+
this.messageHandlers.clear();
|
|
377
|
+
this.webView = null;
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
// src/native/provider/NativeProvider.ts
|
|
382
|
+
var DEFAULT_WALLET_URL = "https://wallet.thru.org/embedded/native";
|
|
383
|
+
var DEFAULT_ORIGIN = "thru-mobile://app";
|
|
384
|
+
var NativeProvider = class {
|
|
385
|
+
constructor(config = {}) {
|
|
386
|
+
this.connected = false;
|
|
387
|
+
this.accounts = [];
|
|
388
|
+
this.selectedAccount = null;
|
|
389
|
+
this.eventListeners = /* @__PURE__ */ new Map();
|
|
390
|
+
/** Pass through the WebView's `onMessage` event handler. */
|
|
391
|
+
this.onMessage = (event) => {
|
|
392
|
+
this.bridge.onMessage(event);
|
|
393
|
+
};
|
|
394
|
+
const walletUrl = config.walletUrl ?? DEFAULT_WALLET_URL;
|
|
395
|
+
this.origin = config.origin ?? DEFAULT_ORIGIN;
|
|
396
|
+
this.bridge = new WebViewBridge({ walletUrl });
|
|
397
|
+
this.bridge.onEvent = (eventType, payload) => {
|
|
398
|
+
this.emit(eventType, payload);
|
|
399
|
+
if (eventType === EMBEDDED_PROVIDER_EVENTS.UI_SHOW) {
|
|
400
|
+
this.requestShow();
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
if (eventType === EMBEDDED_PROVIDER_EVENTS.DISCONNECT || eventType === EMBEDDED_PROVIDER_EVENTS.LOCK) {
|
|
404
|
+
this.clearConnection();
|
|
405
|
+
this.requestHide();
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
if (eventType === EMBEDDED_PROVIDER_EVENTS.ACCOUNT_CHANGED) {
|
|
409
|
+
const account = payload?.account ?? null;
|
|
410
|
+
this.refreshAccountCache(account);
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
const addressTypes = config.addressTypes ?? [AddressType.THRU];
|
|
414
|
+
if (addressTypes.includes(AddressType.THRU)) {
|
|
415
|
+
this._thruChain = new NativeThruChain(this.bridge, this, this.origin);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
/** Hand the bridge a WebView ref. Required before connect/sign. */
|
|
419
|
+
attachWebView(ref) {
|
|
420
|
+
this.bridge.attachWebView(ref);
|
|
421
|
+
}
|
|
422
|
+
/** Mark a direct top-level WebView wallet document as ready. */
|
|
423
|
+
markWebViewReady() {
|
|
424
|
+
this.bridge.markReady();
|
|
425
|
+
}
|
|
426
|
+
/** Build the URL to load inside the shell <iframe>. The host shell
|
|
427
|
+
template should substitute this for WALLET_URL_PLACEHOLDER. */
|
|
428
|
+
getIframeSrc() {
|
|
429
|
+
return this.bridge.getIframeSrc();
|
|
430
|
+
}
|
|
431
|
+
/** Wallet origin (e.g. https://wallet.thru.org). The shell template
|
|
432
|
+
should substitute this for WALLET_ORIGIN_PLACEHOLDER. */
|
|
433
|
+
getWalletOrigin() {
|
|
434
|
+
return this.bridge.walletOrigin;
|
|
435
|
+
}
|
|
436
|
+
/** Wait for the iframe's IFRAME_READY_EVENT handshake. */
|
|
437
|
+
async initialize() {
|
|
438
|
+
await this.bridge.awaitReady();
|
|
439
|
+
}
|
|
440
|
+
/** Open the wallet UI (called internally; also exposed for host). */
|
|
441
|
+
requestShow() {
|
|
442
|
+
this.onShowRequested?.();
|
|
443
|
+
}
|
|
444
|
+
/** Close the wallet UI (called internally; also exposed for host). */
|
|
445
|
+
requestHide() {
|
|
446
|
+
this.onHideRequested?.();
|
|
447
|
+
}
|
|
448
|
+
/** Reject pending requests after a user-driven native sheet dismiss. */
|
|
449
|
+
rejectPendingRequests(message) {
|
|
450
|
+
this.bridge.rejectPendingRequests(message);
|
|
451
|
+
}
|
|
452
|
+
async connect(options) {
|
|
453
|
+
this.emit(EMBEDDED_PROVIDER_EVENTS.CONNECT_START, {});
|
|
454
|
+
try {
|
|
455
|
+
this.requestShow();
|
|
456
|
+
const payload = {};
|
|
457
|
+
if (options?.metadata) payload.metadata = options.metadata;
|
|
458
|
+
if (options?.preferredAccountAddress) {
|
|
459
|
+
payload.preferredAccountAddress = options.preferredAccountAddress;
|
|
460
|
+
}
|
|
461
|
+
if (options?.intent) payload.intent = options.intent;
|
|
462
|
+
const response = await this.bridge.sendMessage({
|
|
463
|
+
id: createRequestId(),
|
|
464
|
+
type: POST_MESSAGE_REQUEST_TYPES.CONNECT,
|
|
465
|
+
payload,
|
|
466
|
+
origin: this.origin
|
|
467
|
+
});
|
|
468
|
+
const result = normalizeWalletAccountResult(response.result);
|
|
469
|
+
this.connected = true;
|
|
470
|
+
this.accounts = result.accounts;
|
|
471
|
+
this.selectedAccount = result.selectedAccount;
|
|
472
|
+
this.emit(EMBEDDED_PROVIDER_EVENTS.CONNECT, result);
|
|
473
|
+
this.requestHide();
|
|
474
|
+
return result;
|
|
475
|
+
} catch (error) {
|
|
476
|
+
this.requestHide();
|
|
477
|
+
this.emit(EMBEDDED_PROVIDER_EVENTS.CONNECT_ERROR, { error });
|
|
478
|
+
throw error;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
async getConnectionState(options) {
|
|
482
|
+
const payload = {};
|
|
483
|
+
if (options?.metadata) payload.metadata = options.metadata;
|
|
484
|
+
if (options?.preferredAccountAddress) {
|
|
485
|
+
payload.preferredAccountAddress = options.preferredAccountAddress;
|
|
486
|
+
}
|
|
487
|
+
const response = await this.bridge.sendMessage({
|
|
488
|
+
id: createRequestId(),
|
|
489
|
+
type: POST_MESSAGE_REQUEST_TYPES.GET_CONNECTION_STATE,
|
|
490
|
+
payload,
|
|
491
|
+
origin: this.origin
|
|
492
|
+
});
|
|
493
|
+
const result = normalizeConnectionStateResult(response.result);
|
|
494
|
+
if (result.isAuthorized && result.hasPasskey && result.accounts.length > 0) {
|
|
495
|
+
this.hydrateConnection(
|
|
496
|
+
{
|
|
497
|
+
accounts: result.accounts,
|
|
498
|
+
status: "completed",
|
|
499
|
+
metadata: result.metadata ?? void 0,
|
|
500
|
+
selectedAccount: result.selectedAccount
|
|
501
|
+
},
|
|
502
|
+
result.selectedAccount?.address ?? null
|
|
503
|
+
);
|
|
504
|
+
} else {
|
|
505
|
+
this.clearConnection();
|
|
506
|
+
}
|
|
507
|
+
return result;
|
|
508
|
+
}
|
|
509
|
+
async disconnect() {
|
|
510
|
+
try {
|
|
511
|
+
await this.bridge.sendMessage({
|
|
512
|
+
id: createRequestId(),
|
|
513
|
+
type: POST_MESSAGE_REQUEST_TYPES.DISCONNECT,
|
|
514
|
+
origin: this.origin
|
|
515
|
+
});
|
|
516
|
+
this.clearConnection();
|
|
517
|
+
this.emit(EMBEDDED_PROVIDER_EVENTS.DISCONNECT, {});
|
|
518
|
+
} catch (error) {
|
|
519
|
+
this.clearConnection();
|
|
520
|
+
this.emit(EMBEDDED_PROVIDER_EVENTS.ERROR, { error });
|
|
521
|
+
throw error;
|
|
522
|
+
} finally {
|
|
523
|
+
this.requestHide();
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
isConnected() {
|
|
527
|
+
return this.connected;
|
|
528
|
+
}
|
|
529
|
+
hydrateConnection(result, selectedAccountAddress) {
|
|
530
|
+
const selectedAccount = resolveWalletAccountByAddress(result.accounts, selectedAccountAddress) ?? result.selectedAccount ?? null;
|
|
531
|
+
const normalized = normalizeWalletAccountResult(result, selectedAccount);
|
|
532
|
+
this.connected = true;
|
|
533
|
+
this.accounts = normalized.accounts;
|
|
534
|
+
this.selectedAccount = normalized.selectedAccount;
|
|
535
|
+
}
|
|
536
|
+
clearConnection() {
|
|
537
|
+
this.connected = false;
|
|
538
|
+
this.accounts = [];
|
|
539
|
+
this.selectedAccount = null;
|
|
540
|
+
}
|
|
541
|
+
getAccounts() {
|
|
542
|
+
return this.accounts;
|
|
543
|
+
}
|
|
544
|
+
getSelectedAccount() {
|
|
545
|
+
return this.selectedAccount;
|
|
546
|
+
}
|
|
547
|
+
async selectAccount(publicKey) {
|
|
548
|
+
if (!this.connected) throw new Error("Wallet not connected");
|
|
549
|
+
const payload = { publicKey };
|
|
550
|
+
const response = await this.bridge.sendMessage({
|
|
551
|
+
id: createRequestId(),
|
|
552
|
+
type: POST_MESSAGE_REQUEST_TYPES.SELECT_ACCOUNT,
|
|
553
|
+
payload,
|
|
554
|
+
origin: this.origin
|
|
555
|
+
});
|
|
556
|
+
const account = response.result.account;
|
|
557
|
+
this.refreshAccountCache(account);
|
|
558
|
+
return account;
|
|
559
|
+
}
|
|
560
|
+
async manageAccounts() {
|
|
561
|
+
if (!this.connected) throw new Error("Wallet not connected");
|
|
562
|
+
try {
|
|
563
|
+
this.requestShow();
|
|
564
|
+
const response = await this.bridge.sendMessage({
|
|
565
|
+
id: createRequestId(),
|
|
566
|
+
type: POST_MESSAGE_REQUEST_TYPES.MANAGE_ACCOUNTS,
|
|
567
|
+
origin: this.origin
|
|
568
|
+
});
|
|
569
|
+
const result = normalizeWalletAccountResult(response.result);
|
|
570
|
+
this.accounts = result.accounts;
|
|
571
|
+
this.selectedAccount = result.selectedAccount;
|
|
572
|
+
this.requestHide();
|
|
573
|
+
return result;
|
|
574
|
+
} catch (error) {
|
|
575
|
+
this.requestHide();
|
|
576
|
+
this.emit(EMBEDDED_PROVIDER_EVENTS.ERROR, { error });
|
|
577
|
+
throw error;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
get thru() {
|
|
581
|
+
if (!this._thruChain) {
|
|
582
|
+
throw new Error("Thru chain not enabled in provider config");
|
|
583
|
+
}
|
|
584
|
+
return this._thruChain;
|
|
585
|
+
}
|
|
586
|
+
on(event, cb) {
|
|
587
|
+
if (!this.eventListeners.has(event)) {
|
|
588
|
+
this.eventListeners.set(event, /* @__PURE__ */ new Set());
|
|
589
|
+
}
|
|
590
|
+
this.eventListeners.get(event).add(cb);
|
|
591
|
+
}
|
|
592
|
+
off(event, cb) {
|
|
593
|
+
this.eventListeners.get(event)?.delete(cb);
|
|
594
|
+
}
|
|
595
|
+
/** Internal: used by NativeThruChain. */
|
|
596
|
+
getBridge() {
|
|
597
|
+
return this.bridge;
|
|
598
|
+
}
|
|
599
|
+
destroy() {
|
|
600
|
+
this.bridge.destroy();
|
|
601
|
+
this.eventListeners.clear();
|
|
602
|
+
this.clearConnection();
|
|
603
|
+
}
|
|
604
|
+
emit(event, data) {
|
|
605
|
+
this.eventListeners.get(event)?.forEach((cb) => {
|
|
606
|
+
try {
|
|
607
|
+
cb(data);
|
|
608
|
+
} catch (err) {
|
|
609
|
+
console.error(`[NativeProvider] listener error for ${event}:`, err);
|
|
610
|
+
}
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
refreshAccountCache(account) {
|
|
614
|
+
if (!account) {
|
|
615
|
+
this.accounts = [];
|
|
616
|
+
this.selectedAccount = null;
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
this.accounts = [account];
|
|
620
|
+
this.selectedAccount = account;
|
|
621
|
+
}
|
|
622
|
+
};
|
|
623
|
+
var DEFAULT_STORAGE_KEY = "thru.native-sdk.connection.v1";
|
|
624
|
+
var SELECTED_ACCOUNT_STORAGE_KEY_SUFFIX = ".selected-account.v1";
|
|
625
|
+
var CHECKING_WALLET_AVAILABILITY = {
|
|
626
|
+
status: "checking",
|
|
627
|
+
isAuthorized: false,
|
|
628
|
+
isConnected: false,
|
|
629
|
+
isUnlocked: false,
|
|
630
|
+
hasPasskey: false,
|
|
631
|
+
hasWalletAccount: false,
|
|
632
|
+
accounts: [],
|
|
633
|
+
selectedAccount: null,
|
|
634
|
+
metadata: null,
|
|
635
|
+
error: null
|
|
636
|
+
};
|
|
637
|
+
var NativeSDK = class {
|
|
638
|
+
constructor(config = {}) {
|
|
639
|
+
this.eventListeners = /* @__PURE__ */ new Map();
|
|
640
|
+
this.initialized = false;
|
|
641
|
+
this.thruClient = null;
|
|
642
|
+
this.connectInFlight = null;
|
|
643
|
+
this.lastConnectResult = null;
|
|
644
|
+
this.walletAvailability = CHECKING_WALLET_AVAILABILITY;
|
|
645
|
+
/** Bind to the WebView's `onMessage` handler. */
|
|
646
|
+
this.onMessage = (event) => {
|
|
647
|
+
this.provider.onMessage(event);
|
|
648
|
+
};
|
|
649
|
+
this.origin = config.origin ?? "thru-mobile://app";
|
|
650
|
+
this.rpcUrl = config.rpcUrl;
|
|
651
|
+
this.storage = config.storage;
|
|
652
|
+
this.storageKey = config.storageKey ?? DEFAULT_STORAGE_KEY;
|
|
653
|
+
this.selectedAccountStorageKey = config.selectedAccountStorageKey ?? `${this.storageKey}${SELECTED_ACCOUNT_STORAGE_KEY_SUFFIX}`;
|
|
654
|
+
this.iosWebViewMode = config.iosWebViewMode ?? "shell-iframe";
|
|
655
|
+
this.provider = new NativeProvider({
|
|
656
|
+
walletUrl: config.walletUrl,
|
|
657
|
+
origin: this.origin,
|
|
658
|
+
addressTypes: config.addressTypes ?? [AddressType.THRU]
|
|
659
|
+
});
|
|
660
|
+
this.setupEventForwarding();
|
|
661
|
+
}
|
|
662
|
+
/** Hand the WebView ref to the underlying provider/bridge. */
|
|
663
|
+
attachWebView(ref) {
|
|
664
|
+
this.provider.attachWebView(ref);
|
|
665
|
+
}
|
|
666
|
+
/** Mark a direct top-level WebView wallet document as ready. */
|
|
667
|
+
markWebViewReady() {
|
|
668
|
+
this.provider.markWebViewReady();
|
|
669
|
+
}
|
|
670
|
+
/** Build the URL to load inside the shell <iframe>. */
|
|
671
|
+
getIframeSrc() {
|
|
672
|
+
return this.provider.getIframeSrc();
|
|
673
|
+
}
|
|
674
|
+
/** Wallet origin (e.g. https://wallet.thru.org). */
|
|
675
|
+
getWalletOrigin() {
|
|
676
|
+
return this.provider.getWalletOrigin();
|
|
677
|
+
}
|
|
678
|
+
/** Bind host UI lifecycle handlers used by custom WebView hosts. */
|
|
679
|
+
setUiHandlers(handlers) {
|
|
680
|
+
this.provider.onShowRequested = handlers.onShowRequested;
|
|
681
|
+
this.provider.onHideRequested = handlers.onHideRequested;
|
|
682
|
+
}
|
|
683
|
+
clearUiHandlers() {
|
|
684
|
+
this.provider.onShowRequested = void 0;
|
|
685
|
+
this.provider.onHideRequested = void 0;
|
|
686
|
+
}
|
|
687
|
+
/** Reject in-flight wallet requests after a user-driven host dismiss. */
|
|
688
|
+
rejectPendingRequests(message) {
|
|
689
|
+
this.provider.rejectPendingRequests(message);
|
|
690
|
+
}
|
|
691
|
+
/** iOS WebView host mode. Non-iOS hosts should ignore this value. */
|
|
692
|
+
getIosWebViewMode() {
|
|
693
|
+
return this.iosWebViewMode;
|
|
694
|
+
}
|
|
695
|
+
async initialize() {
|
|
696
|
+
if (this.initialized) return;
|
|
697
|
+
await this.provider.initialize();
|
|
698
|
+
this.initialized = true;
|
|
699
|
+
}
|
|
700
|
+
async connect(options) {
|
|
701
|
+
const isAccountSwitch = options?.intent === "switch-account";
|
|
702
|
+
if (this.connectInFlight) return this.connectInFlight;
|
|
703
|
+
if (!isAccountSwitch && this.lastConnectResult && this.provider.isConnected()) {
|
|
704
|
+
return this.lastConnectResult;
|
|
705
|
+
}
|
|
706
|
+
this.emit("connect", { status: "connecting" });
|
|
707
|
+
const inFlight = (async () => {
|
|
708
|
+
try {
|
|
709
|
+
this.provider.requestShow();
|
|
710
|
+
if (!this.initialized) await this.initialize();
|
|
711
|
+
const metadata = this.resolveMetadata(options?.metadata);
|
|
712
|
+
const preferredAccountAddress = isAccountSwitch ? null : await this.readSelectedAccountAddress();
|
|
713
|
+
const providerOptions = metadata || preferredAccountAddress || options?.intent ? {
|
|
714
|
+
...metadata ? { metadata } : {},
|
|
715
|
+
...preferredAccountAddress ? { preferredAccountAddress } : {},
|
|
716
|
+
...options?.intent ? { intent: options.intent } : {}
|
|
717
|
+
} : void 0;
|
|
718
|
+
const result = await this.provider.connect(providerOptions);
|
|
719
|
+
if (!isAccountSwitch) {
|
|
720
|
+
await this.applyPreferredSelectedAccount(result.accounts);
|
|
721
|
+
}
|
|
722
|
+
const selectedAccount = this.provider.getSelectedAccount() ?? result.selectedAccount ?? null;
|
|
723
|
+
const activeResult = normalizeWalletAccountResult(
|
|
724
|
+
{
|
|
725
|
+
...result,
|
|
726
|
+
accounts: this.provider.getAccounts(),
|
|
727
|
+
selectedAccount
|
|
728
|
+
},
|
|
729
|
+
selectedAccount
|
|
730
|
+
);
|
|
731
|
+
this.lastConnectResult = activeResult;
|
|
732
|
+
await this.persistSelectedAccountAddress(
|
|
733
|
+
activeResult.selectedAccount?.address ?? null
|
|
734
|
+
);
|
|
735
|
+
await this.clearPersistedConnection();
|
|
736
|
+
this.setWalletAvailability(
|
|
737
|
+
walletAvailabilityFromConnectResult(activeResult)
|
|
738
|
+
);
|
|
739
|
+
this.emit("connect", activeResult);
|
|
740
|
+
return activeResult;
|
|
741
|
+
} catch (error) {
|
|
742
|
+
this.provider.requestHide();
|
|
743
|
+
if (isUserRejectedError(error) && !isAccountSwitch) {
|
|
744
|
+
this.provider.clearConnection();
|
|
745
|
+
this.lastConnectResult = null;
|
|
746
|
+
await this.clearPersistedConnection();
|
|
747
|
+
this.clearAuthorizedAvailability();
|
|
748
|
+
this.emit("disconnect", { reason: "user_rejected" });
|
|
749
|
+
}
|
|
750
|
+
this.emit("error", error);
|
|
751
|
+
throw error;
|
|
752
|
+
} finally {
|
|
753
|
+
this.connectInFlight = null;
|
|
754
|
+
}
|
|
755
|
+
})();
|
|
756
|
+
this.connectInFlight = inFlight;
|
|
757
|
+
return inFlight;
|
|
758
|
+
}
|
|
759
|
+
async signIn(options) {
|
|
760
|
+
return this.connect({
|
|
761
|
+
metadata: this.resolveSignInMetadata(options),
|
|
762
|
+
...options.intent ? { intent: options.intent } : {}
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
async disconnect() {
|
|
766
|
+
try {
|
|
767
|
+
await this.provider.disconnect();
|
|
768
|
+
this.emit("disconnect", {});
|
|
769
|
+
this.lastConnectResult = null;
|
|
770
|
+
await this.clearPersistedConnection();
|
|
771
|
+
this.clearAuthorizedAvailability();
|
|
772
|
+
} catch (error) {
|
|
773
|
+
this.emit("error", error);
|
|
774
|
+
throw error;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
isConnected() {
|
|
778
|
+
return this.provider.isConnected();
|
|
779
|
+
}
|
|
780
|
+
getWalletAvailability() {
|
|
781
|
+
return this.walletAvailability;
|
|
782
|
+
}
|
|
783
|
+
async restoreConnection(options = {}) {
|
|
784
|
+
await this.clearPersistedConnection();
|
|
785
|
+
return null;
|
|
786
|
+
}
|
|
787
|
+
async syncConnectionState(options) {
|
|
788
|
+
try {
|
|
789
|
+
const state = await this.requestConnectionState(options);
|
|
790
|
+
this.setWalletAvailability(walletAvailabilityFromConnectionState(state));
|
|
791
|
+
await this.applyConnectionState(state);
|
|
792
|
+
return state;
|
|
793
|
+
} catch (error) {
|
|
794
|
+
this.setWalletAvailability(walletAvailabilityFromError(error));
|
|
795
|
+
this.emit("error", error);
|
|
796
|
+
return null;
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
async refreshWalletAvailability(options) {
|
|
800
|
+
try {
|
|
801
|
+
const state = await this.requestConnectionState(options);
|
|
802
|
+
const availability = walletAvailabilityFromConnectionState(state);
|
|
803
|
+
this.setWalletAvailability(availability);
|
|
804
|
+
await this.applyConnectionState(state);
|
|
805
|
+
return availability;
|
|
806
|
+
} catch (error) {
|
|
807
|
+
const availability = walletAvailabilityFromError(error);
|
|
808
|
+
this.setWalletAvailability(availability);
|
|
809
|
+
this.emit("error", error);
|
|
810
|
+
return availability;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
getAccounts() {
|
|
814
|
+
const accounts = this.provider.getAccounts();
|
|
815
|
+
const activeAccounts = this.refreshCachedAccounts(
|
|
816
|
+
accounts,
|
|
817
|
+
this.provider.getSelectedAccount()
|
|
818
|
+
);
|
|
819
|
+
return activeAccounts;
|
|
820
|
+
}
|
|
821
|
+
getSelectedAccount() {
|
|
822
|
+
return this.provider.getSelectedAccount();
|
|
823
|
+
}
|
|
824
|
+
async selectAccount(publicKey) {
|
|
825
|
+
const account = await this.provider.selectAccount(publicKey);
|
|
826
|
+
this.refreshCachedAccounts(this.provider.getAccounts(), account);
|
|
827
|
+
await this.persistSelectedAccountAddress(account.address);
|
|
828
|
+
return account;
|
|
829
|
+
}
|
|
830
|
+
async manageAccounts() {
|
|
831
|
+
if (!this.initialized) await this.initialize();
|
|
832
|
+
const result = await this.provider.manageAccounts();
|
|
833
|
+
const activeResult = normalizeWalletAccountResult(result);
|
|
834
|
+
const selectedAccount = activeResult.selectedAccount ?? null;
|
|
835
|
+
this.refreshCachedAccounts(activeResult.accounts, selectedAccount);
|
|
836
|
+
await this.persistSelectedAccountAddress(selectedAccount?.address ?? null);
|
|
837
|
+
if (this.lastConnectResult) {
|
|
838
|
+
this.setWalletAvailability(
|
|
839
|
+
walletAvailabilityFromConnectResult(this.lastConnectResult)
|
|
840
|
+
);
|
|
841
|
+
}
|
|
842
|
+
this.emit("accountChanged", selectedAccount);
|
|
843
|
+
return activeResult;
|
|
844
|
+
}
|
|
845
|
+
get thru() {
|
|
846
|
+
return this.provider.thru;
|
|
847
|
+
}
|
|
848
|
+
on(event, callback) {
|
|
849
|
+
if (!this.eventListeners.has(event)) {
|
|
850
|
+
this.eventListeners.set(event, /* @__PURE__ */ new Set());
|
|
851
|
+
}
|
|
852
|
+
this.eventListeners.get(event).add(callback);
|
|
853
|
+
}
|
|
854
|
+
off(event, callback) {
|
|
855
|
+
this.eventListeners.get(event)?.delete(callback);
|
|
856
|
+
}
|
|
857
|
+
once(event, callback) {
|
|
858
|
+
const wrapped = (...args) => {
|
|
859
|
+
callback(...args);
|
|
860
|
+
this.off(event, wrapped);
|
|
861
|
+
};
|
|
862
|
+
this.on(event, wrapped);
|
|
863
|
+
}
|
|
864
|
+
destroy() {
|
|
865
|
+
this.provider.destroy();
|
|
866
|
+
this.eventListeners.clear();
|
|
867
|
+
this.initialized = false;
|
|
868
|
+
this.connectInFlight = null;
|
|
869
|
+
this.lastConnectResult = null;
|
|
870
|
+
this.walletAvailability = CHECKING_WALLET_AVAILABILITY;
|
|
871
|
+
}
|
|
872
|
+
/** Lazily-instantiated Thru chain client. */
|
|
873
|
+
getThru() {
|
|
874
|
+
if (!this.thruClient) {
|
|
875
|
+
this.thruClient = createThruClient({ baseUrl: this.rpcUrl });
|
|
876
|
+
}
|
|
877
|
+
return this.thruClient;
|
|
878
|
+
}
|
|
879
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
880
|
+
emit(event, data) {
|
|
881
|
+
this.eventListeners.get(event)?.forEach((cb) => {
|
|
882
|
+
try {
|
|
883
|
+
cb(data);
|
|
884
|
+
} catch (err) {
|
|
885
|
+
console.error(`[NativeSDK] listener error for ${event}:`, err);
|
|
886
|
+
}
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
setupEventForwarding() {
|
|
890
|
+
this.provider.on(EMBEDDED_PROVIDER_EVENTS.DISCONNECT, (data) => {
|
|
891
|
+
this.lastConnectResult = null;
|
|
892
|
+
this.clearAuthorizedAvailability();
|
|
893
|
+
this.emit("disconnect", data);
|
|
894
|
+
});
|
|
895
|
+
this.provider.on(EMBEDDED_PROVIDER_EVENTS.ERROR, (data) => {
|
|
896
|
+
this.emit("error", data);
|
|
897
|
+
});
|
|
898
|
+
this.provider.on(EMBEDDED_PROVIDER_EVENTS.LOCK, (data) => {
|
|
899
|
+
this.lastConnectResult = null;
|
|
900
|
+
this.clearAuthorizedAvailability();
|
|
901
|
+
this.emit("lock", data);
|
|
902
|
+
this.emit("disconnect", { reason: "locked" });
|
|
903
|
+
});
|
|
904
|
+
this.provider.on(EMBEDDED_PROVIDER_EVENTS.ACCOUNT_CHANGED, (data) => {
|
|
905
|
+
const payload = data;
|
|
906
|
+
const account = payload?.account ?? null;
|
|
907
|
+
this.refreshCachedAccounts(this.provider.getAccounts(), account);
|
|
908
|
+
if (account) void this.persistSelectedAccountAddress(account.address);
|
|
909
|
+
this.emit("accountChanged", account);
|
|
910
|
+
});
|
|
911
|
+
}
|
|
912
|
+
async requestConnectionState(options) {
|
|
913
|
+
if (!this.initialized) await this.initialize();
|
|
914
|
+
const metadata = options?.metadata ?? this.lastConnectResult?.metadata ?? void 0;
|
|
915
|
+
const providerOptions = metadata ? { metadata: this.resolveMetadata(metadata) } : void 0;
|
|
916
|
+
const preferredAccountAddress = await this.readSelectedAccountAddress();
|
|
917
|
+
const nextProviderOptions = providerOptions || preferredAccountAddress ? {
|
|
918
|
+
...providerOptions ?? {},
|
|
919
|
+
...preferredAccountAddress ? { preferredAccountAddress } : {}
|
|
920
|
+
} : void 0;
|
|
921
|
+
const state = await this.provider.getConnectionState(nextProviderOptions);
|
|
922
|
+
return normalizeConnectionStateResult(state);
|
|
923
|
+
}
|
|
924
|
+
async applyConnectionState(state) {
|
|
925
|
+
if (state.isAuthorized && state.hasPasskey && state.accounts.length > 0) {
|
|
926
|
+
const result = {
|
|
927
|
+
accounts: state.accounts,
|
|
928
|
+
selectedAccount: state.selectedAccount,
|
|
929
|
+
status: "completed",
|
|
930
|
+
metadata: state.metadata ?? void 0
|
|
931
|
+
};
|
|
932
|
+
const activeResult = normalizeWalletAccountResult(result);
|
|
933
|
+
this.lastConnectResult = activeResult;
|
|
934
|
+
await this.persistSelectedAccountAddress(
|
|
935
|
+
this.provider.getSelectedAccount()?.address ?? activeResult.selectedAccount?.address ?? null
|
|
936
|
+
);
|
|
937
|
+
await this.clearPersistedConnection();
|
|
938
|
+
this.emit("connect", activeResult);
|
|
939
|
+
return;
|
|
940
|
+
}
|
|
941
|
+
const wasConnected = this.provider.isConnected() || !!this.lastConnectResult;
|
|
942
|
+
this.provider.clearConnection();
|
|
943
|
+
this.lastConnectResult = null;
|
|
944
|
+
await this.clearPersistedConnection();
|
|
945
|
+
if (wasConnected) {
|
|
946
|
+
this.emit("disconnect", { reason: "state_unavailable" });
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
setWalletAvailability(availability) {
|
|
950
|
+
this.walletAvailability = availability;
|
|
951
|
+
this.emit("availabilityChanged", availability);
|
|
952
|
+
}
|
|
953
|
+
clearAuthorizedAvailability() {
|
|
954
|
+
const previous = this.walletAvailability.status === "ready" ? this.walletAvailability : null;
|
|
955
|
+
this.setWalletAvailability({
|
|
956
|
+
status: "ready",
|
|
957
|
+
isAuthorized: false,
|
|
958
|
+
isConnected: false,
|
|
959
|
+
isUnlocked: false,
|
|
960
|
+
hasPasskey: previous?.hasPasskey ?? false,
|
|
961
|
+
hasWalletAccount: previous?.hasWalletAccount ?? false,
|
|
962
|
+
accounts: [],
|
|
963
|
+
selectedAccount: null,
|
|
964
|
+
metadata: null,
|
|
965
|
+
error: null
|
|
966
|
+
});
|
|
967
|
+
}
|
|
968
|
+
resolveMetadata(input) {
|
|
969
|
+
if (!input) {
|
|
970
|
+
return { appId: this.origin };
|
|
971
|
+
}
|
|
972
|
+
const metadata = {
|
|
973
|
+
appId: input.appId ?? this.origin
|
|
974
|
+
};
|
|
975
|
+
if (input.appUrl) metadata.appUrl = input.appUrl;
|
|
976
|
+
if (input.appName) metadata.appName = input.appName;
|
|
977
|
+
if (input.imageUrl) metadata.imageUrl = input.imageUrl;
|
|
978
|
+
return metadata;
|
|
979
|
+
}
|
|
980
|
+
resolveSignInMetadata(options) {
|
|
981
|
+
const metadata = {
|
|
982
|
+
appId: options.app_id,
|
|
983
|
+
appName: options.app_display_name
|
|
984
|
+
};
|
|
985
|
+
if (options.app_url) metadata.appUrl = options.app_url;
|
|
986
|
+
if (options.image_url) metadata.imageUrl = options.image_url;
|
|
987
|
+
return metadata;
|
|
988
|
+
}
|
|
989
|
+
refreshCachedAccounts(accounts, selectedAccount) {
|
|
990
|
+
const active = normalizeActiveWalletAccounts(accounts, selectedAccount);
|
|
991
|
+
const nextAccounts = active.accounts;
|
|
992
|
+
const nextSelectedAccount = active.selectedAccount;
|
|
993
|
+
if (this.lastConnectResult && this.provider.isConnected()) {
|
|
994
|
+
this.lastConnectResult = {
|
|
995
|
+
...this.lastConnectResult,
|
|
996
|
+
accounts: nextAccounts,
|
|
997
|
+
selectedAccount: nextSelectedAccount
|
|
998
|
+
};
|
|
999
|
+
if (nextSelectedAccount) {
|
|
1000
|
+
void this.persistSelectedAccountAddress(nextSelectedAccount.address);
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
return nextAccounts;
|
|
1004
|
+
}
|
|
1005
|
+
async applyPreferredSelectedAccount(accounts) {
|
|
1006
|
+
const preferredAddress = await this.readSelectedAccountAddress();
|
|
1007
|
+
if (!preferredAddress) return;
|
|
1008
|
+
if (!accounts.some((account) => account.address === preferredAddress)) {
|
|
1009
|
+
return;
|
|
1010
|
+
}
|
|
1011
|
+
if (this.provider.getSelectedAccount()?.address === preferredAddress) {
|
|
1012
|
+
return;
|
|
1013
|
+
}
|
|
1014
|
+
try {
|
|
1015
|
+
await this.provider.selectAccount(preferredAddress);
|
|
1016
|
+
} catch (error) {
|
|
1017
|
+
console.warn("[NativeSDK] Failed to restore selected account:", error);
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
async persistSelectedAccountAddress(selectedAccountAddress) {
|
|
1021
|
+
if (!this.storage) return;
|
|
1022
|
+
try {
|
|
1023
|
+
if (!selectedAccountAddress) {
|
|
1024
|
+
await this.storage.removeItem(this.selectedAccountStorageKey);
|
|
1025
|
+
return;
|
|
1026
|
+
}
|
|
1027
|
+
const snapshot = {
|
|
1028
|
+
version: 1,
|
|
1029
|
+
origin: this.origin,
|
|
1030
|
+
walletOrigin: this.provider.getWalletOrigin(),
|
|
1031
|
+
savedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1032
|
+
selectedAccountAddress
|
|
1033
|
+
};
|
|
1034
|
+
await this.storage.setItem(
|
|
1035
|
+
this.selectedAccountStorageKey,
|
|
1036
|
+
JSON.stringify(snapshot)
|
|
1037
|
+
);
|
|
1038
|
+
} catch (error) {
|
|
1039
|
+
console.warn("[NativeSDK] Failed to persist selected account:", error);
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
async clearPersistedConnection() {
|
|
1043
|
+
if (!this.storage) return;
|
|
1044
|
+
try {
|
|
1045
|
+
await this.storage.removeItem(this.storageKey);
|
|
1046
|
+
} catch (error) {
|
|
1047
|
+
console.warn("[NativeSDK] Failed to clear connection state:", error);
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
async readSelectedAccountAddress() {
|
|
1051
|
+
if (!this.storage) return null;
|
|
1052
|
+
try {
|
|
1053
|
+
const raw = await this.storage.getItem(this.selectedAccountStorageKey);
|
|
1054
|
+
if (!raw) return null;
|
|
1055
|
+
const parsed = JSON.parse(
|
|
1056
|
+
raw
|
|
1057
|
+
);
|
|
1058
|
+
if (parsed.version !== 1 || parsed.origin !== this.origin || parsed.walletOrigin !== this.provider.getWalletOrigin() || typeof parsed.selectedAccountAddress !== "string" || parsed.selectedAccountAddress.length === 0) {
|
|
1059
|
+
await this.storage.removeItem(this.selectedAccountStorageKey);
|
|
1060
|
+
return null;
|
|
1061
|
+
}
|
|
1062
|
+
return parsed.selectedAccountAddress;
|
|
1063
|
+
} catch (error) {
|
|
1064
|
+
console.warn("[NativeSDK] Failed to restore selected account:", error);
|
|
1065
|
+
try {
|
|
1066
|
+
await this.storage.removeItem(this.selectedAccountStorageKey);
|
|
1067
|
+
} catch {
|
|
1068
|
+
}
|
|
1069
|
+
return null;
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
};
|
|
1073
|
+
function walletAvailabilityFromConnectResult(result, selectedAccount) {
|
|
1074
|
+
const active = normalizeWalletAccountResult(result, null);
|
|
1075
|
+
const hasActiveAccount = active.accounts.length > 0;
|
|
1076
|
+
return {
|
|
1077
|
+
status: "ready",
|
|
1078
|
+
isAuthorized: hasActiveAccount,
|
|
1079
|
+
isConnected: hasActiveAccount,
|
|
1080
|
+
isUnlocked: true,
|
|
1081
|
+
hasPasskey: hasActiveAccount,
|
|
1082
|
+
hasWalletAccount: hasActiveAccount,
|
|
1083
|
+
accounts: active.accounts,
|
|
1084
|
+
selectedAccount: active.selectedAccount,
|
|
1085
|
+
metadata: result.metadata ?? null,
|
|
1086
|
+
error: null
|
|
1087
|
+
};
|
|
1088
|
+
}
|
|
1089
|
+
function walletAvailabilityFromConnectionState(state) {
|
|
1090
|
+
const active = normalizeConnectionStateResult(state);
|
|
1091
|
+
const hasWalletAccount = state.hasWalletAccount ?? state.accounts.length > 0;
|
|
1092
|
+
return {
|
|
1093
|
+
status: "ready",
|
|
1094
|
+
isAuthorized: state.isAuthorized,
|
|
1095
|
+
isConnected: state.isAuthorized && state.isConnected,
|
|
1096
|
+
isUnlocked: state.isUnlocked,
|
|
1097
|
+
hasPasskey: state.hasPasskey,
|
|
1098
|
+
hasWalletAccount,
|
|
1099
|
+
accounts: active.accounts,
|
|
1100
|
+
selectedAccount: active.selectedAccount,
|
|
1101
|
+
metadata: state.isAuthorized ? state.metadata : null,
|
|
1102
|
+
error: null
|
|
1103
|
+
};
|
|
1104
|
+
}
|
|
1105
|
+
function walletAvailabilityFromError(error) {
|
|
1106
|
+
return {
|
|
1107
|
+
status: "error",
|
|
1108
|
+
isAuthorized: false,
|
|
1109
|
+
isConnected: false,
|
|
1110
|
+
isUnlocked: false,
|
|
1111
|
+
hasPasskey: false,
|
|
1112
|
+
hasWalletAccount: false,
|
|
1113
|
+
accounts: [],
|
|
1114
|
+
selectedAccount: null,
|
|
1115
|
+
metadata: null,
|
|
1116
|
+
error: error instanceof Error ? error : new Error("Wallet availability check failed")
|
|
1117
|
+
};
|
|
1118
|
+
}
|
|
1119
|
+
function isUserRejectedError(error) {
|
|
1120
|
+
if (!error || typeof error !== "object") return false;
|
|
1121
|
+
return error.code === ErrorCode.USER_REJECTED;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
export { AddressType, EMBEDDED_PROVIDER_EVENTS, ErrorCode, NativeSDK, POST_MESSAGE_REQUEST_TYPES, ThruTransactionEncoding };
|
|
1125
|
+
//# sourceMappingURL=native.js.map
|
|
1126
|
+
//# sourceMappingURL=native.js.map
|