@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
|
@@ -0,0 +1,773 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AddressType,
|
|
3
|
+
type AppMetadata,
|
|
4
|
+
type AddressType as AddressTypeValue,
|
|
5
|
+
type ConnectResult,
|
|
6
|
+
type IThruChain,
|
|
7
|
+
type WalletAccount,
|
|
8
|
+
normalizeActiveWalletAccounts,
|
|
9
|
+
normalizeWalletAccountResult,
|
|
10
|
+
} from "../interfaces";
|
|
11
|
+
import {
|
|
12
|
+
EMBEDDED_PROVIDER_EVENTS,
|
|
13
|
+
ErrorCode,
|
|
14
|
+
type ConnectMetadataInput,
|
|
15
|
+
type ConnectRequestPayload,
|
|
16
|
+
type GetConnectionStateResult,
|
|
17
|
+
type ManageAccountsResult,
|
|
18
|
+
normalizeConnectionStateResult,
|
|
19
|
+
} from "../protocol";
|
|
20
|
+
import { NativeProvider } from "./provider/NativeProvider";
|
|
21
|
+
import type {
|
|
22
|
+
WebViewMessageEventLike,
|
|
23
|
+
WebViewRefLike,
|
|
24
|
+
} from "./provider/WebViewBridge";
|
|
25
|
+
import { createThruClient, type Thru } from "@thru/sdk/client";
|
|
26
|
+
|
|
27
|
+
export type IosWebViewMode = "direct" | "shell-iframe";
|
|
28
|
+
|
|
29
|
+
export type WalletAvailability =
|
|
30
|
+
| {
|
|
31
|
+
status: "checking";
|
|
32
|
+
isAuthorized: false;
|
|
33
|
+
isConnected: false;
|
|
34
|
+
isUnlocked: false;
|
|
35
|
+
hasPasskey: false;
|
|
36
|
+
hasWalletAccount: false;
|
|
37
|
+
accounts: WalletAccount[];
|
|
38
|
+
selectedAccount: null;
|
|
39
|
+
metadata: null;
|
|
40
|
+
error: null;
|
|
41
|
+
}
|
|
42
|
+
| {
|
|
43
|
+
status: "ready";
|
|
44
|
+
isAuthorized: boolean;
|
|
45
|
+
isConnected: boolean;
|
|
46
|
+
isUnlocked: boolean;
|
|
47
|
+
hasPasskey: boolean;
|
|
48
|
+
hasWalletAccount: boolean;
|
|
49
|
+
accounts: WalletAccount[];
|
|
50
|
+
selectedAccount: WalletAccount | null;
|
|
51
|
+
metadata: AppMetadata | null;
|
|
52
|
+
error: null;
|
|
53
|
+
}
|
|
54
|
+
| {
|
|
55
|
+
status: "error";
|
|
56
|
+
isAuthorized: false;
|
|
57
|
+
isConnected: false;
|
|
58
|
+
isUnlocked: false;
|
|
59
|
+
hasPasskey: false;
|
|
60
|
+
hasWalletAccount: false;
|
|
61
|
+
accounts: WalletAccount[];
|
|
62
|
+
selectedAccount: null;
|
|
63
|
+
metadata: null;
|
|
64
|
+
error: Error;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export interface NativeSDKConfig {
|
|
68
|
+
walletUrl?: string;
|
|
69
|
+
/** Stamped on every postMessage so wallet's ConnectedAppsStorage can
|
|
70
|
+
scope per-host. Default: 'thru-mobile://app'. */
|
|
71
|
+
origin?: string;
|
|
72
|
+
rpcUrl?: string;
|
|
73
|
+
addressTypes?: AddressTypeValue[];
|
|
74
|
+
/** iOS-only host mode. Shell iframe is the default; direct is kept
|
|
75
|
+
as an escape hatch for real-device passkey/WebAuthn comparisons. */
|
|
76
|
+
iosWebViewMode?: IosWebViewMode;
|
|
77
|
+
/** Optional host-provided persistent storage (SecureStore,
|
|
78
|
+
AsyncStorage, localStorage-compatible adapter, etc.). */
|
|
79
|
+
storage?: NativeSDKStorage;
|
|
80
|
+
/** Override the legacy connection snapshot key cleared from `storage`. */
|
|
81
|
+
storageKey?: string;
|
|
82
|
+
/** Override the key used to remember the app-local selected account. */
|
|
83
|
+
selectedAccountStorageKey?: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface SignInOptions {
|
|
87
|
+
app_id: string;
|
|
88
|
+
app_display_name: string;
|
|
89
|
+
app_url?: string;
|
|
90
|
+
image_url?: string;
|
|
91
|
+
intent?: ConnectOptions["intent"];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface ConnectOptions {
|
|
95
|
+
metadata?: ConnectMetadataInput;
|
|
96
|
+
preferredAccountAddress?: string;
|
|
97
|
+
intent?: ConnectRequestPayload["intent"];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface RestoreConnectionOptions {
|
|
101
|
+
hydrate?: boolean;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export type SDKEvent =
|
|
105
|
+
| "connect"
|
|
106
|
+
| "disconnect"
|
|
107
|
+
| "lock"
|
|
108
|
+
| "error"
|
|
109
|
+
| "accountChanged"
|
|
110
|
+
| "availabilityChanged";
|
|
111
|
+
|
|
112
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
113
|
+
export type EventCallback = (...args: any[]) => void;
|
|
114
|
+
|
|
115
|
+
export interface NativeSDKStorage {
|
|
116
|
+
getItem: (key: string) => string | null | Promise<string | null>;
|
|
117
|
+
setItem: (key: string, value: string) => void | Promise<void>;
|
|
118
|
+
removeItem: (key: string) => void | Promise<void>;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface NativeSDKUiHandlers {
|
|
122
|
+
onShowRequested?: () => void;
|
|
123
|
+
onHideRequested?: () => void;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const DEFAULT_STORAGE_KEY = "thru.native-sdk.connection.v1";
|
|
127
|
+
const SELECTED_ACCOUNT_STORAGE_KEY_SUFFIX = ".selected-account.v1";
|
|
128
|
+
|
|
129
|
+
const CHECKING_WALLET_AVAILABILITY: WalletAvailability = {
|
|
130
|
+
status: "checking",
|
|
131
|
+
isAuthorized: false,
|
|
132
|
+
isConnected: false,
|
|
133
|
+
isUnlocked: false,
|
|
134
|
+
hasPasskey: false,
|
|
135
|
+
hasWalletAccount: false,
|
|
136
|
+
accounts: [],
|
|
137
|
+
selectedAccount: null,
|
|
138
|
+
metadata: null,
|
|
139
|
+
error: null,
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
interface PersistedSelectedAccountSnapshot {
|
|
143
|
+
version: 1;
|
|
144
|
+
origin: string;
|
|
145
|
+
walletOrigin: string;
|
|
146
|
+
savedAt: string;
|
|
147
|
+
selectedAccountAddress: string;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* NativeSDK - mobile mirror of `@thru/wallet`'s `BrowserSDK`.
|
|
152
|
+
* Public surface matches verbatim except `mountInline(HTMLElement)` is
|
|
153
|
+
* replaced by `attachWebView(WebViewRefLike)` since the host bottom
|
|
154
|
+
* sheet owns the WebView lifecycle.
|
|
155
|
+
*/
|
|
156
|
+
export class NativeSDK {
|
|
157
|
+
private provider: NativeProvider;
|
|
158
|
+
private eventListeners = new Map<SDKEvent, Set<EventCallback>>();
|
|
159
|
+
private initialized = false;
|
|
160
|
+
private thruClient: Thru | null = null;
|
|
161
|
+
private rpcUrl: string | undefined;
|
|
162
|
+
private connectInFlight: Promise<ConnectResult> | null = null;
|
|
163
|
+
private lastConnectResult: ConnectResult | null = null;
|
|
164
|
+
private walletAvailability: WalletAvailability = CHECKING_WALLET_AVAILABILITY;
|
|
165
|
+
private readonly origin: string;
|
|
166
|
+
private readonly storage?: NativeSDKStorage;
|
|
167
|
+
private readonly storageKey: string;
|
|
168
|
+
private readonly selectedAccountStorageKey: string;
|
|
169
|
+
private readonly iosWebViewMode: IosWebViewMode;
|
|
170
|
+
|
|
171
|
+
constructor(config: NativeSDKConfig = {}) {
|
|
172
|
+
this.origin = config.origin ?? "thru-mobile://app";
|
|
173
|
+
this.rpcUrl = config.rpcUrl;
|
|
174
|
+
this.storage = config.storage;
|
|
175
|
+
this.storageKey = config.storageKey ?? DEFAULT_STORAGE_KEY;
|
|
176
|
+
this.selectedAccountStorageKey =
|
|
177
|
+
config.selectedAccountStorageKey ??
|
|
178
|
+
`${this.storageKey}${SELECTED_ACCOUNT_STORAGE_KEY_SUFFIX}`;
|
|
179
|
+
this.iosWebViewMode = config.iosWebViewMode ?? "shell-iframe";
|
|
180
|
+
this.provider = new NativeProvider({
|
|
181
|
+
walletUrl: config.walletUrl,
|
|
182
|
+
origin: this.origin,
|
|
183
|
+
addressTypes: config.addressTypes ?? [AddressType.THRU],
|
|
184
|
+
});
|
|
185
|
+
this.setupEventForwarding();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Hand the WebView ref to the underlying provider/bridge. */
|
|
189
|
+
attachWebView(ref: WebViewRefLike): void {
|
|
190
|
+
this.provider.attachWebView(ref);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Mark a direct top-level WebView wallet document as ready. */
|
|
194
|
+
markWebViewReady(): void {
|
|
195
|
+
this.provider.markWebViewReady();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Bind to the WebView's `onMessage` handler. */
|
|
199
|
+
onMessage = (event: WebViewMessageEventLike): void => {
|
|
200
|
+
this.provider.onMessage(event);
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
/** Build the URL to load inside the shell <iframe>. */
|
|
204
|
+
getIframeSrc(): string {
|
|
205
|
+
return this.provider.getIframeSrc();
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Wallet origin (e.g. https://wallet.thru.org). */
|
|
209
|
+
getWalletOrigin(): string {
|
|
210
|
+
return this.provider.getWalletOrigin();
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Bind host UI lifecycle handlers used by custom WebView hosts. */
|
|
214
|
+
setUiHandlers(handlers: NativeSDKUiHandlers): void {
|
|
215
|
+
this.provider.onShowRequested = handlers.onShowRequested;
|
|
216
|
+
this.provider.onHideRequested = handlers.onHideRequested;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
clearUiHandlers(): void {
|
|
220
|
+
this.provider.onShowRequested = undefined;
|
|
221
|
+
this.provider.onHideRequested = undefined;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Reject in-flight wallet requests after a user-driven host dismiss. */
|
|
225
|
+
rejectPendingRequests(message?: string): void {
|
|
226
|
+
this.provider.rejectPendingRequests(message);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** iOS WebView host mode. Non-iOS hosts should ignore this value. */
|
|
230
|
+
getIosWebViewMode(): IosWebViewMode {
|
|
231
|
+
return this.iosWebViewMode;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async initialize(): Promise<void> {
|
|
235
|
+
if (this.initialized) return;
|
|
236
|
+
await this.provider.initialize();
|
|
237
|
+
this.initialized = true;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async connect(options?: ConnectOptions): Promise<ConnectResult> {
|
|
241
|
+
const isAccountSwitch = options?.intent === "switch-account";
|
|
242
|
+
if (this.connectInFlight) return this.connectInFlight;
|
|
243
|
+
if (
|
|
244
|
+
!isAccountSwitch &&
|
|
245
|
+
this.lastConnectResult &&
|
|
246
|
+
this.provider.isConnected()
|
|
247
|
+
) {
|
|
248
|
+
return this.lastConnectResult;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
this.emit("connect", { status: "connecting" });
|
|
252
|
+
|
|
253
|
+
const inFlight = (async () => {
|
|
254
|
+
try {
|
|
255
|
+
this.provider.requestShow();
|
|
256
|
+
if (!this.initialized) await this.initialize();
|
|
257
|
+
|
|
258
|
+
const metadata = this.resolveMetadata(options?.metadata);
|
|
259
|
+
const preferredAccountAddress = isAccountSwitch
|
|
260
|
+
? null
|
|
261
|
+
: await this.readSelectedAccountAddress();
|
|
262
|
+
const providerOptions =
|
|
263
|
+
metadata || preferredAccountAddress || options?.intent
|
|
264
|
+
? {
|
|
265
|
+
...(metadata ? { metadata } : {}),
|
|
266
|
+
...(preferredAccountAddress ? { preferredAccountAddress } : {}),
|
|
267
|
+
...(options?.intent ? { intent: options.intent } : {}),
|
|
268
|
+
}
|
|
269
|
+
: undefined;
|
|
270
|
+
const result = await this.provider.connect(providerOptions);
|
|
271
|
+
if (!isAccountSwitch) {
|
|
272
|
+
await this.applyPreferredSelectedAccount(result.accounts);
|
|
273
|
+
}
|
|
274
|
+
const selectedAccount =
|
|
275
|
+
this.provider.getSelectedAccount() ?? result.selectedAccount ?? null;
|
|
276
|
+
const activeResult = normalizeWalletAccountResult(
|
|
277
|
+
{
|
|
278
|
+
...result,
|
|
279
|
+
accounts: this.provider.getAccounts(),
|
|
280
|
+
selectedAccount,
|
|
281
|
+
},
|
|
282
|
+
selectedAccount,
|
|
283
|
+
);
|
|
284
|
+
this.lastConnectResult = activeResult;
|
|
285
|
+
await this.persistSelectedAccountAddress(
|
|
286
|
+
activeResult.selectedAccount?.address ?? null,
|
|
287
|
+
);
|
|
288
|
+
await this.clearPersistedConnection();
|
|
289
|
+
this.setWalletAvailability(
|
|
290
|
+
walletAvailabilityFromConnectResult(activeResult),
|
|
291
|
+
);
|
|
292
|
+
this.emit("connect", activeResult);
|
|
293
|
+
return activeResult;
|
|
294
|
+
} catch (error) {
|
|
295
|
+
this.provider.requestHide();
|
|
296
|
+
if (isUserRejectedError(error) && !isAccountSwitch) {
|
|
297
|
+
this.provider.clearConnection();
|
|
298
|
+
this.lastConnectResult = null;
|
|
299
|
+
await this.clearPersistedConnection();
|
|
300
|
+
this.clearAuthorizedAvailability();
|
|
301
|
+
this.emit("disconnect", { reason: "user_rejected" });
|
|
302
|
+
}
|
|
303
|
+
this.emit("error", error);
|
|
304
|
+
throw error;
|
|
305
|
+
} finally {
|
|
306
|
+
this.connectInFlight = null;
|
|
307
|
+
}
|
|
308
|
+
})();
|
|
309
|
+
|
|
310
|
+
this.connectInFlight = inFlight;
|
|
311
|
+
return inFlight;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
async signIn(options: SignInOptions): Promise<ConnectResult> {
|
|
315
|
+
return this.connect({
|
|
316
|
+
metadata: this.resolveSignInMetadata(options),
|
|
317
|
+
...(options.intent ? { intent: options.intent } : {}),
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
async disconnect(): Promise<void> {
|
|
322
|
+
try {
|
|
323
|
+
await this.provider.disconnect();
|
|
324
|
+
this.emit("disconnect", {});
|
|
325
|
+
this.lastConnectResult = null;
|
|
326
|
+
await this.clearPersistedConnection();
|
|
327
|
+
this.clearAuthorizedAvailability();
|
|
328
|
+
} catch (error) {
|
|
329
|
+
this.emit("error", error);
|
|
330
|
+
throw error;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
isConnected(): boolean {
|
|
335
|
+
return this.provider.isConnected();
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
getWalletAvailability(): WalletAvailability {
|
|
339
|
+
return this.walletAvailability;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
async restoreConnection(
|
|
343
|
+
options: RestoreConnectionOptions = {},
|
|
344
|
+
): Promise<ConnectResult | null> {
|
|
345
|
+
void options;
|
|
346
|
+
await this.clearPersistedConnection();
|
|
347
|
+
return null;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
async syncConnectionState(
|
|
351
|
+
options?: ConnectOptions,
|
|
352
|
+
): Promise<GetConnectionStateResult | null> {
|
|
353
|
+
try {
|
|
354
|
+
const state = await this.requestConnectionState(options);
|
|
355
|
+
this.setWalletAvailability(walletAvailabilityFromConnectionState(state));
|
|
356
|
+
await this.applyConnectionState(state);
|
|
357
|
+
return state;
|
|
358
|
+
} catch (error) {
|
|
359
|
+
this.setWalletAvailability(walletAvailabilityFromError(error));
|
|
360
|
+
this.emit("error", error);
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
async refreshWalletAvailability(
|
|
366
|
+
options?: ConnectOptions,
|
|
367
|
+
): Promise<WalletAvailability> {
|
|
368
|
+
try {
|
|
369
|
+
const state = await this.requestConnectionState(options);
|
|
370
|
+
const availability = walletAvailabilityFromConnectionState(state);
|
|
371
|
+
this.setWalletAvailability(availability);
|
|
372
|
+
await this.applyConnectionState(state);
|
|
373
|
+
return availability;
|
|
374
|
+
} catch (error) {
|
|
375
|
+
const availability = walletAvailabilityFromError(error);
|
|
376
|
+
this.setWalletAvailability(availability);
|
|
377
|
+
this.emit("error", error);
|
|
378
|
+
return availability;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
getAccounts(): WalletAccount[] {
|
|
383
|
+
const accounts = this.provider.getAccounts();
|
|
384
|
+
const activeAccounts = this.refreshCachedAccounts(
|
|
385
|
+
accounts,
|
|
386
|
+
this.provider.getSelectedAccount(),
|
|
387
|
+
);
|
|
388
|
+
return activeAccounts;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
getSelectedAccount(): WalletAccount | null {
|
|
392
|
+
return this.provider.getSelectedAccount();
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
async selectAccount(publicKey: string): Promise<WalletAccount> {
|
|
396
|
+
const account = await this.provider.selectAccount(publicKey);
|
|
397
|
+
this.refreshCachedAccounts(this.provider.getAccounts(), account);
|
|
398
|
+
await this.persistSelectedAccountAddress(account.address);
|
|
399
|
+
return account;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
async manageAccounts(): Promise<ManageAccountsResult> {
|
|
403
|
+
if (!this.initialized) await this.initialize();
|
|
404
|
+
const result = await this.provider.manageAccounts();
|
|
405
|
+
const activeResult = normalizeWalletAccountResult(result);
|
|
406
|
+
const selectedAccount = activeResult.selectedAccount ?? null;
|
|
407
|
+
this.refreshCachedAccounts(activeResult.accounts, selectedAccount);
|
|
408
|
+
await this.persistSelectedAccountAddress(selectedAccount?.address ?? null);
|
|
409
|
+
if (this.lastConnectResult) {
|
|
410
|
+
this.setWalletAvailability(
|
|
411
|
+
walletAvailabilityFromConnectResult(this.lastConnectResult),
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
this.emit("accountChanged", selectedAccount);
|
|
415
|
+
return activeResult;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
get thru(): IThruChain {
|
|
419
|
+
return this.provider.thru;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
on(event: SDKEvent, callback: EventCallback): void {
|
|
423
|
+
if (!this.eventListeners.has(event)) {
|
|
424
|
+
this.eventListeners.set(event, new Set());
|
|
425
|
+
}
|
|
426
|
+
this.eventListeners.get(event)!.add(callback);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
off(event: SDKEvent, callback: EventCallback): void {
|
|
430
|
+
this.eventListeners.get(event)?.delete(callback);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
once(event: SDKEvent, callback: EventCallback): void {
|
|
434
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
435
|
+
const wrapped = (...args: any[]) => {
|
|
436
|
+
callback(...args);
|
|
437
|
+
this.off(event, wrapped);
|
|
438
|
+
};
|
|
439
|
+
this.on(event, wrapped);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
destroy(): void {
|
|
443
|
+
this.provider.destroy();
|
|
444
|
+
this.eventListeners.clear();
|
|
445
|
+
this.initialized = false;
|
|
446
|
+
this.connectInFlight = null;
|
|
447
|
+
this.lastConnectResult = null;
|
|
448
|
+
this.walletAvailability = CHECKING_WALLET_AVAILABILITY;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/** Lazily-instantiated Thru chain client. */
|
|
452
|
+
public getThru(): Thru {
|
|
453
|
+
if (!this.thruClient) {
|
|
454
|
+
this.thruClient = createThruClient({ baseUrl: this.rpcUrl });
|
|
455
|
+
}
|
|
456
|
+
return this.thruClient;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
460
|
+
private emit(event: SDKEvent, data?: any): void {
|
|
461
|
+
this.eventListeners.get(event)?.forEach((cb) => {
|
|
462
|
+
try {
|
|
463
|
+
cb(data);
|
|
464
|
+
} catch (err) {
|
|
465
|
+
// eslint-disable-next-line no-console
|
|
466
|
+
console.error(`[NativeSDK] listener error for ${event}:`, err);
|
|
467
|
+
}
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
private setupEventForwarding(): void {
|
|
472
|
+
/* CONNECT is emitted from connect() directly (with the resolved
|
|
473
|
+
ConnectResult), so don't double-emit here. */
|
|
474
|
+
this.provider.on(EMBEDDED_PROVIDER_EVENTS.DISCONNECT, (data) => {
|
|
475
|
+
this.lastConnectResult = null;
|
|
476
|
+
this.clearAuthorizedAvailability();
|
|
477
|
+
this.emit("disconnect", data);
|
|
478
|
+
});
|
|
479
|
+
this.provider.on(EMBEDDED_PROVIDER_EVENTS.ERROR, (data) => {
|
|
480
|
+
this.emit("error", data);
|
|
481
|
+
});
|
|
482
|
+
this.provider.on(EMBEDDED_PROVIDER_EVENTS.LOCK, (data) => {
|
|
483
|
+
this.lastConnectResult = null;
|
|
484
|
+
this.clearAuthorizedAvailability();
|
|
485
|
+
this.emit("lock", data);
|
|
486
|
+
this.emit("disconnect", { reason: "locked" });
|
|
487
|
+
});
|
|
488
|
+
this.provider.on(EMBEDDED_PROVIDER_EVENTS.ACCOUNT_CHANGED, (data) => {
|
|
489
|
+
const payload = data as { account?: WalletAccount } | undefined;
|
|
490
|
+
const account = payload?.account ?? null;
|
|
491
|
+
this.refreshCachedAccounts(this.provider.getAccounts(), account);
|
|
492
|
+
if (account) void this.persistSelectedAccountAddress(account.address);
|
|
493
|
+
this.emit("accountChanged", account);
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
private async requestConnectionState(
|
|
498
|
+
options?: ConnectOptions,
|
|
499
|
+
): Promise<GetConnectionStateResult> {
|
|
500
|
+
if (!this.initialized) await this.initialize();
|
|
501
|
+
|
|
502
|
+
const metadata =
|
|
503
|
+
options?.metadata ?? this.lastConnectResult?.metadata ?? undefined;
|
|
504
|
+
const providerOptions = metadata
|
|
505
|
+
? { metadata: this.resolveMetadata(metadata) }
|
|
506
|
+
: undefined;
|
|
507
|
+
const preferredAccountAddress = await this.readSelectedAccountAddress();
|
|
508
|
+
const nextProviderOptions =
|
|
509
|
+
providerOptions || preferredAccountAddress
|
|
510
|
+
? {
|
|
511
|
+
...(providerOptions ?? {}),
|
|
512
|
+
...(preferredAccountAddress ? { preferredAccountAddress } : {}),
|
|
513
|
+
}
|
|
514
|
+
: undefined;
|
|
515
|
+
const state = await this.provider.getConnectionState(nextProviderOptions);
|
|
516
|
+
return normalizeConnectionStateResult(state);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
private async applyConnectionState(
|
|
520
|
+
state: GetConnectionStateResult,
|
|
521
|
+
): Promise<void> {
|
|
522
|
+
if (state.isAuthorized && state.hasPasskey && state.accounts.length > 0) {
|
|
523
|
+
const result: ConnectResult = {
|
|
524
|
+
accounts: state.accounts,
|
|
525
|
+
selectedAccount: state.selectedAccount,
|
|
526
|
+
status: "completed",
|
|
527
|
+
metadata: state.metadata ?? undefined,
|
|
528
|
+
};
|
|
529
|
+
const activeResult = normalizeWalletAccountResult(result);
|
|
530
|
+
this.lastConnectResult = activeResult;
|
|
531
|
+
await this.persistSelectedAccountAddress(
|
|
532
|
+
this.provider.getSelectedAccount()?.address ??
|
|
533
|
+
activeResult.selectedAccount?.address ??
|
|
534
|
+
null,
|
|
535
|
+
);
|
|
536
|
+
await this.clearPersistedConnection();
|
|
537
|
+
this.emit("connect", activeResult);
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
const wasConnected =
|
|
542
|
+
this.provider.isConnected() || !!this.lastConnectResult;
|
|
543
|
+
this.provider.clearConnection();
|
|
544
|
+
this.lastConnectResult = null;
|
|
545
|
+
await this.clearPersistedConnection();
|
|
546
|
+
if (wasConnected) {
|
|
547
|
+
this.emit("disconnect", { reason: "state_unavailable" });
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
private setWalletAvailability(availability: WalletAvailability): void {
|
|
552
|
+
this.walletAvailability = availability;
|
|
553
|
+
this.emit("availabilityChanged", availability);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
private clearAuthorizedAvailability(): void {
|
|
557
|
+
const previous =
|
|
558
|
+
this.walletAvailability.status === "ready"
|
|
559
|
+
? this.walletAvailability
|
|
560
|
+
: null;
|
|
561
|
+
this.setWalletAvailability({
|
|
562
|
+
status: "ready",
|
|
563
|
+
isAuthorized: false,
|
|
564
|
+
isConnected: false,
|
|
565
|
+
isUnlocked: false,
|
|
566
|
+
hasPasskey: previous?.hasPasskey ?? false,
|
|
567
|
+
hasWalletAccount: previous?.hasWalletAccount ?? false,
|
|
568
|
+
accounts: [],
|
|
569
|
+
selectedAccount: null,
|
|
570
|
+
metadata: null,
|
|
571
|
+
error: null,
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
private resolveMetadata(
|
|
576
|
+
input?: ConnectMetadataInput,
|
|
577
|
+
): ConnectMetadataInput | undefined {
|
|
578
|
+
if (!input) {
|
|
579
|
+
/* On RN we have no window.location.origin; require explicit
|
|
580
|
+
metadata, but stamp the configured origin as appId so the
|
|
581
|
+
wallet can scope per-host. */
|
|
582
|
+
return { appId: this.origin };
|
|
583
|
+
}
|
|
584
|
+
const metadata: ConnectMetadataInput = {
|
|
585
|
+
appId: input.appId ?? this.origin,
|
|
586
|
+
};
|
|
587
|
+
if (input.appUrl) metadata.appUrl = input.appUrl;
|
|
588
|
+
if (input.appName) metadata.appName = input.appName;
|
|
589
|
+
if (input.imageUrl) metadata.imageUrl = input.imageUrl;
|
|
590
|
+
return metadata;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
private resolveSignInMetadata(options: SignInOptions): ConnectMetadataInput {
|
|
594
|
+
const metadata: ConnectMetadataInput = {
|
|
595
|
+
appId: options.app_id,
|
|
596
|
+
appName: options.app_display_name,
|
|
597
|
+
};
|
|
598
|
+
if (options.app_url) metadata.appUrl = options.app_url;
|
|
599
|
+
if (options.image_url) metadata.imageUrl = options.image_url;
|
|
600
|
+
return metadata;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
private refreshCachedAccounts(
|
|
604
|
+
accounts: WalletAccount[],
|
|
605
|
+
selectedAccount?: WalletAccount | null,
|
|
606
|
+
): WalletAccount[] {
|
|
607
|
+
const active = normalizeActiveWalletAccounts(accounts, selectedAccount);
|
|
608
|
+
const nextAccounts = active.accounts;
|
|
609
|
+
const nextSelectedAccount = active.selectedAccount;
|
|
610
|
+
if (this.lastConnectResult && this.provider.isConnected()) {
|
|
611
|
+
this.lastConnectResult = {
|
|
612
|
+
...this.lastConnectResult,
|
|
613
|
+
accounts: nextAccounts,
|
|
614
|
+
selectedAccount: nextSelectedAccount,
|
|
615
|
+
};
|
|
616
|
+
if (nextSelectedAccount) {
|
|
617
|
+
void this.persistSelectedAccountAddress(nextSelectedAccount.address);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
return nextAccounts;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
private async applyPreferredSelectedAccount(
|
|
624
|
+
accounts: WalletAccount[],
|
|
625
|
+
): Promise<void> {
|
|
626
|
+
const preferredAddress = await this.readSelectedAccountAddress();
|
|
627
|
+
if (!preferredAddress) return;
|
|
628
|
+
if (!accounts.some((account) => account.address === preferredAddress)) {
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
if (this.provider.getSelectedAccount()?.address === preferredAddress) {
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
try {
|
|
636
|
+
await this.provider.selectAccount(preferredAddress);
|
|
637
|
+
} catch (error) {
|
|
638
|
+
console.warn("[NativeSDK] Failed to restore selected account:", error);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
private async persistSelectedAccountAddress(
|
|
643
|
+
selectedAccountAddress: string | null,
|
|
644
|
+
): Promise<void> {
|
|
645
|
+
if (!this.storage) return;
|
|
646
|
+
try {
|
|
647
|
+
if (!selectedAccountAddress) {
|
|
648
|
+
await this.storage.removeItem(this.selectedAccountStorageKey);
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
const snapshot: PersistedSelectedAccountSnapshot = {
|
|
653
|
+
version: 1,
|
|
654
|
+
origin: this.origin,
|
|
655
|
+
walletOrigin: this.provider.getWalletOrigin(),
|
|
656
|
+
savedAt: new Date().toISOString(),
|
|
657
|
+
selectedAccountAddress,
|
|
658
|
+
};
|
|
659
|
+
await this.storage.setItem(
|
|
660
|
+
this.selectedAccountStorageKey,
|
|
661
|
+
JSON.stringify(snapshot),
|
|
662
|
+
);
|
|
663
|
+
} catch (error) {
|
|
664
|
+
console.warn("[NativeSDK] Failed to persist selected account:", error);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
private async clearPersistedConnection(): Promise<void> {
|
|
669
|
+
if (!this.storage) return;
|
|
670
|
+
try {
|
|
671
|
+
await this.storage.removeItem(this.storageKey);
|
|
672
|
+
} catch (error) {
|
|
673
|
+
console.warn("[NativeSDK] Failed to clear connection state:", error);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
private async readSelectedAccountAddress(): Promise<string | null> {
|
|
678
|
+
if (!this.storage) return null;
|
|
679
|
+
|
|
680
|
+
try {
|
|
681
|
+
const raw = await this.storage.getItem(this.selectedAccountStorageKey);
|
|
682
|
+
if (!raw) return null;
|
|
683
|
+
|
|
684
|
+
const parsed = JSON.parse(
|
|
685
|
+
raw,
|
|
686
|
+
) as Partial<PersistedSelectedAccountSnapshot>;
|
|
687
|
+
if (
|
|
688
|
+
parsed.version !== 1 ||
|
|
689
|
+
parsed.origin !== this.origin ||
|
|
690
|
+
parsed.walletOrigin !== this.provider.getWalletOrigin() ||
|
|
691
|
+
typeof parsed.selectedAccountAddress !== "string" ||
|
|
692
|
+
parsed.selectedAccountAddress.length === 0
|
|
693
|
+
) {
|
|
694
|
+
await this.storage.removeItem(this.selectedAccountStorageKey);
|
|
695
|
+
return null;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
return parsed.selectedAccountAddress;
|
|
699
|
+
} catch (error) {
|
|
700
|
+
console.warn("[NativeSDK] Failed to restore selected account:", error);
|
|
701
|
+
try {
|
|
702
|
+
await this.storage.removeItem(this.selectedAccountStorageKey);
|
|
703
|
+
} catch {
|
|
704
|
+
/* best effort */
|
|
705
|
+
}
|
|
706
|
+
return null;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
function walletAvailabilityFromConnectResult(
|
|
712
|
+
result: ConnectResult,
|
|
713
|
+
selectedAccount?: WalletAccount | null,
|
|
714
|
+
): WalletAvailability {
|
|
715
|
+
const active = normalizeWalletAccountResult(result, selectedAccount ?? null);
|
|
716
|
+
const hasActiveAccount = active.accounts.length > 0;
|
|
717
|
+
return {
|
|
718
|
+
status: "ready",
|
|
719
|
+
isAuthorized: hasActiveAccount,
|
|
720
|
+
isConnected: hasActiveAccount,
|
|
721
|
+
isUnlocked: true,
|
|
722
|
+
hasPasskey: hasActiveAccount,
|
|
723
|
+
hasWalletAccount: hasActiveAccount,
|
|
724
|
+
accounts: active.accounts,
|
|
725
|
+
selectedAccount: active.selectedAccount,
|
|
726
|
+
metadata: result.metadata ?? null,
|
|
727
|
+
error: null,
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
function walletAvailabilityFromConnectionState(
|
|
732
|
+
state: GetConnectionStateResult,
|
|
733
|
+
): WalletAvailability {
|
|
734
|
+
const active = normalizeConnectionStateResult(state);
|
|
735
|
+
const hasWalletAccount =
|
|
736
|
+
(state as Partial<GetConnectionStateResult>).hasWalletAccount ??
|
|
737
|
+
state.accounts.length > 0;
|
|
738
|
+
return {
|
|
739
|
+
status: "ready",
|
|
740
|
+
isAuthorized: state.isAuthorized,
|
|
741
|
+
isConnected: state.isAuthorized && state.isConnected,
|
|
742
|
+
isUnlocked: state.isUnlocked,
|
|
743
|
+
hasPasskey: state.hasPasskey,
|
|
744
|
+
hasWalletAccount,
|
|
745
|
+
accounts: active.accounts,
|
|
746
|
+
selectedAccount: active.selectedAccount,
|
|
747
|
+
metadata: state.isAuthorized ? state.metadata : null,
|
|
748
|
+
error: null,
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
function walletAvailabilityFromError(error: unknown): WalletAvailability {
|
|
753
|
+
return {
|
|
754
|
+
status: "error",
|
|
755
|
+
isAuthorized: false,
|
|
756
|
+
isConnected: false,
|
|
757
|
+
isUnlocked: false,
|
|
758
|
+
hasPasskey: false,
|
|
759
|
+
hasWalletAccount: false,
|
|
760
|
+
accounts: [],
|
|
761
|
+
selectedAccount: null,
|
|
762
|
+
metadata: null,
|
|
763
|
+
error:
|
|
764
|
+
error instanceof Error
|
|
765
|
+
? error
|
|
766
|
+
: new Error("Wallet availability check failed"),
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
function isUserRejectedError(error: unknown): boolean {
|
|
771
|
+
if (!error || typeof error !== "object") return false;
|
|
772
|
+
return (error as { code?: unknown }).code === ErrorCode.USER_REJECTED;
|
|
773
|
+
}
|