@thru/wallet 0.2.27 → 0.2.28

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.
Files changed (47) hide show
  1. package/README.md +1 -0
  2. package/dist/{BrowserSDK-CpRFiJsW.d.ts → BrowserSDK-CRQTOT8S.d.ts} +178 -3
  3. package/dist/index.d.ts +2 -2
  4. package/dist/index.js +376 -12
  5. package/dist/index.js.map +1 -1
  6. package/dist/native/react/transparent.d.ts +104 -0
  7. package/dist/native/react/transparent.js +2210 -0
  8. package/dist/native/react/transparent.js.map +1 -0
  9. package/dist/native/react.d.ts +5 -90
  10. package/dist/native/react.js +765 -32
  11. package/dist/native/react.js.map +1 -1
  12. package/dist/native.d.ts +105 -1
  13. package/dist/native.js +521 -31
  14. package/dist/native.js.map +1 -1
  15. package/dist/react-ui.js +5 -0
  16. package/dist/react-ui.js.map +1 -1
  17. package/dist/react.d.ts +2 -2
  18. package/dist/react.js +376 -12
  19. package/dist/react.js.map +1 -1
  20. package/package.json +8 -2
  21. package/src/BrowserSDK.ts +32 -1
  22. package/src/encoding.ts +39 -0
  23. package/src/index.ts +5 -1
  24. package/src/interfaces/IThruChain.ts +50 -1
  25. package/src/interfaces/types.ts +52 -0
  26. package/src/native/NativeSDK.test.ts +200 -1
  27. package/src/native/NativeSDK.ts +124 -10
  28. package/src/native/index.ts +12 -0
  29. package/src/native/provider/NativeProvider.ts +106 -5
  30. package/src/native/provider/WebViewBridge.test.ts +22 -1
  31. package/src/native/provider/WebViewBridge.ts +17 -7
  32. package/src/native/provider/chains/ThruChain.ts +215 -5
  33. package/src/native/react/ThruContext.ts +3 -1
  34. package/src/native/react/ThruProvider.tsx +25 -0
  35. package/src/native/react/ThruTransparentWalletBridge.tsx +281 -0
  36. package/src/native/react/hooks/useWallet.ts +12 -1
  37. package/src/native/react/index.ts +11 -0
  38. package/src/native/react/transparent.ts +35 -0
  39. package/src/protocol/postMessage.ts +127 -2
  40. package/src/provider/EmbeddedProvider.ts +7 -1
  41. package/src/provider/IframeManager.test.ts +18 -0
  42. package/src/provider/IframeManager.ts +8 -1
  43. package/src/provider/chains/ThruChain.ts +210 -4
  44. package/src/provider/types/messages.ts +16 -0
  45. package/src/react/index.ts +6 -0
  46. package/src/signing-sessions.test.ts +182 -0
  47. package/src/signing-sessions.ts +204 -0
@@ -0,0 +1,39 @@
1
+ const BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
2
+
3
+ const BASE64_LOOKUP = new Map<string, number>(
4
+ [...BASE64_ALPHABET].map((char, index) => [char, index]),
5
+ );
6
+
7
+ export function base64ToBytes(value: string): Uint8Array {
8
+ const normalized = value.replace(/\s+/g, "");
9
+ if (normalized.length === 0) return new Uint8Array();
10
+ if (normalized.length % 4 === 1) {
11
+ throw new Error("Invalid base64 data");
12
+ }
13
+
14
+ const padded = normalized.padEnd(
15
+ normalized.length + ((4 - (normalized.length % 4)) % 4),
16
+ "=",
17
+ );
18
+ const padding = padded.endsWith("==") ? 2 : padded.endsWith("=") ? 1 : 0;
19
+ const output = new Uint8Array((padded.length / 4) * 3 - padding);
20
+ let outIdx = 0;
21
+
22
+ for (let i = 0; i < padded.length; i += 4) {
23
+ const chars = padded.slice(i, i + 4);
24
+ const a = BASE64_LOOKUP.get(chars[0]);
25
+ const b = BASE64_LOOKUP.get(chars[1]);
26
+ const c = chars[2] === "=" ? 0 : BASE64_LOOKUP.get(chars[2]);
27
+ const d = chars[3] === "=" ? 0 : BASE64_LOOKUP.get(chars[3]);
28
+ if (a === undefined || b === undefined || c === undefined || d === undefined) {
29
+ throw new Error("Invalid base64 data");
30
+ }
31
+
32
+ const chunk = (a << 18) | (b << 12) | (c << 6) | d;
33
+ if (outIdx < output.length) output[outIdx++] = (chunk >> 16) & 0xff;
34
+ if (outIdx < output.length) output[outIdx++] = (chunk >> 8) & 0xff;
35
+ if (outIdx < output.length) output[outIdx++] = chunk & 0xff;
36
+ }
37
+
38
+ return output;
39
+ }
package/src/index.ts CHANGED
@@ -6,7 +6,10 @@ export {
6
6
 
7
7
  export type {
8
8
  ConnectedApp, ConnectResult, IThruChain, SignMessageParams,
9
- SignMessageResult, ThruSigningContext, ThruTransactionIntent, WalletAccount
9
+ SignMessageResult, ThruSigningContext, ThruSigningSession,
10
+ ThruSigningSessionCreateOptions, ThruSigningSessionDescriptor,
11
+ ThruSigningSessionInstruction, ThruSigningSessionInstructionCreateOptions,
12
+ ThruSigningSessionTimestamp, ThruTransactionIntent, WalletAccount
10
13
  } from './interfaces';
11
14
  export {
12
15
  AddressType,
@@ -25,3 +28,4 @@ export {
25
28
  ErrorCode,
26
29
  } from './protocol';
27
30
  export * from './protocol';
31
+ export type { SigningSessionStorage } from './signing-sessions';
@@ -1,4 +1,13 @@
1
- import type { ThruSigningContext, ThruTransactionIntent } from "./types";
1
+ import type {
2
+ ThruSigningContext,
3
+ ThruSigningSession,
4
+ ThruSigningSessionCreateOptions,
5
+ ThruSigningSessionInstruction,
6
+ ThruSigningSessionInstructionCreateOptions,
7
+ ThruPasskeyChallengeIntent,
8
+ ThruPasskeyChallengeSignature,
9
+ ThruTransactionIntent,
10
+ } from "./types";
2
11
 
3
12
  /**
4
13
  * Minimal Thru chain interface exposed to SDK consumers.
@@ -34,4 +43,44 @@ export interface IThruChain {
34
43
  * account ordering, headers, nonces, and final wire layout.
35
44
  */
36
45
  signTransaction(transaction: ThruTransactionIntent): Promise<string>;
46
+
47
+ /**
48
+ * Sign a backend-prepared passkey-manager challenge with the wallet-owned
49
+ * selected passkey and return submit-ready signature fields.
50
+ */
51
+ signPasskeyChallenge(
52
+ challenge: ThruPasskeyChallengeIntent,
53
+ ): Promise<ThruPasskeyChallengeSignature>;
54
+
55
+ /**
56
+ * Create a temporary wallet-owned signing session. The SDK stores the
57
+ * returned descriptor in app-local storage; the wallet stores the private key.
58
+ */
59
+ createSigningSession(
60
+ options: ThruSigningSessionCreateOptions,
61
+ ): Promise<ThruSigningSession>;
62
+
63
+ /**
64
+ * Prepare a temporary signing-session authority instruction without asking
65
+ * for passkey approval. The returned instruction must be included in a
66
+ * later passkey-approved transaction before the session can sign.
67
+ */
68
+ createSigningSessionInstruction(
69
+ options: ThruSigningSessionInstructionCreateOptions,
70
+ ): Promise<ThruSigningSessionInstruction>;
71
+
72
+ /**
73
+ * Confirm that a prepared signing-session instruction landed on-chain and
74
+ * publish the resulting session descriptor into SDK storage.
75
+ */
76
+ confirmSigningSession(id: string): Promise<ThruSigningSession>;
77
+
78
+ /** Return a locally known signing session by id. */
79
+ getSigningSession(id: string): Promise<ThruSigningSession | null>;
80
+
81
+ /** Return locally known signing sessions for this SDK app scope only. */
82
+ getSigningSessions(): Promise<ThruSigningSession[]>;
83
+
84
+ /** Delete a locally known session and ask the wallet to delete its key. */
85
+ revokeSigningSession(id: string): Promise<void>;
37
86
  }
@@ -66,6 +66,43 @@ export interface ThruTransactionReviewPayload {
66
66
  abiReflection?: ThruTransactionReviewAbiReflection;
67
67
  }
68
68
 
69
+ export type ThruSigningSessionTimestamp = Date | number | bigint | string;
70
+
71
+ export interface ThruSigningSessionCreateOptions {
72
+ walletAddress?: string;
73
+ durationSeconds?: number;
74
+ expiresAt?: ThruSigningSessionTimestamp;
75
+ review?: ThruTransactionReviewPayload;
76
+ }
77
+
78
+ export interface ThruSigningSessionInstructionCreateOptions extends Omit<
79
+ ThruSigningSessionCreateOptions,
80
+ "review"
81
+ > {
82
+ walletAccountIdx: number;
83
+ }
84
+
85
+ export interface ThruSigningSessionDescriptor {
86
+ id: string;
87
+ walletAddress: string;
88
+ publicKey: string;
89
+ authIdx: number;
90
+ expiresAt: number;
91
+ createdAt: number;
92
+ }
93
+
94
+ export interface ThruSigningSession extends ThruSigningSessionDescriptor {
95
+ signTransaction(transaction: ThruTransactionIntent): Promise<string>;
96
+ revoke(): Promise<void>;
97
+ toJSON(): ThruSigningSessionDescriptor;
98
+ }
99
+
100
+ export interface ThruSigningSessionInstruction {
101
+ session: ThruSigningSession;
102
+ programAddress: string;
103
+ instructionData: Uint8Array;
104
+ }
105
+
69
106
  export interface ThruTransactionIntent {
70
107
  walletAddress?: string;
71
108
  programAddress: string;
@@ -73,6 +110,21 @@ export interface ThruTransactionIntent {
73
110
  readWriteAddresses?: string[];
74
111
  readOnlyAddresses?: string[];
75
112
  review?: ThruTransactionReviewPayload;
113
+ /** @internal Used by ThruSigningSession handles. */
114
+ signingSessionId?: string;
115
+ }
116
+
117
+ export interface ThruPasskeyChallengeIntent {
118
+ /** base64url-encoded passkey-manager challenge bytes. */
119
+ challenge: string;
120
+ walletAddress?: string;
121
+ }
122
+
123
+ export interface ThruPasskeyChallengeSignature {
124
+ signatureR: string;
125
+ signatureS: string;
126
+ authenticatorData: string;
127
+ clientDataJSON: string;
76
128
  }
77
129
 
78
130
  export interface ConnectedApp {
@@ -1,4 +1,4 @@
1
- import { afterEach, beforeEach, describe, expect, it } from "vitest";
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
2
  import {
3
3
  ErrorCode,
4
4
  EMBEDDED_PROVIDER_EVENTS,
@@ -96,6 +96,20 @@ async function flush(): Promise<void> {
96
96
  for (let i = 0; i < 8; i++) await Promise.resolve();
97
97
  }
98
98
 
99
+ async function wait(ms: number): Promise<void> {
100
+ await new Promise((resolve) => setTimeout(resolve, ms));
101
+ }
102
+
103
+ async function waitForInjectedRequest(webView: MockWebView): Promise<string> {
104
+ for (let i = 0; i < 60; i++) {
105
+ await flush();
106
+ const script = webView.injected[0];
107
+ if (script) return script;
108
+ await wait(50);
109
+ }
110
+ throw new Error("Timed out waiting for injected request");
111
+ }
112
+
99
113
  function parseInjectedRequest(script: string): {
100
114
  id: string;
101
115
  type: string;
@@ -138,6 +152,191 @@ describe("NativeSDK", () => {
138
152
  directSdk.destroy();
139
153
  });
140
154
 
155
+ it("defaults transparent wallet experience to the transparent native route", () => {
156
+ const transparentSdk = new NativeSDK({
157
+ walletExperience: "transparent",
158
+ origin: "thru-mobile://token-dummy",
159
+ });
160
+ const iframeUrl = new URL(transparentSdk.getIframeSrc());
161
+
162
+ expect(iframeUrl.origin).toBe("https://wallet.thru.org");
163
+ expect(iframeUrl.pathname).toBe("/embedded/native/transparent");
164
+
165
+ transparentSdk.destroy();
166
+ });
167
+
168
+ it("requests a transparent focus surface for transparent connect", async () => {
169
+ sdk.destroy();
170
+ sdk = new NativeSDK({
171
+ walletUrl: "http://localhost:3000/embedded/native/transparent",
172
+ walletExperience: "transparent",
173
+ origin: "thru-mobile://token-dummy",
174
+ });
175
+ webView = new MockWebView();
176
+ sdk.attachWebView(webView);
177
+ const onShowRequested = vi.fn();
178
+ const onHideRequested = vi.fn();
179
+ sdk.setUiHandlers({ onShowRequested, onHideRequested });
180
+
181
+ const frameId = frameIdFor(sdk);
182
+ const promise = sdk.signIn({
183
+ app_id: "token_dummy_app",
184
+ app_display_name: "Token Dummy App",
185
+ });
186
+
187
+ sdk.onMessage(readyMessage(frameId));
188
+ await flush();
189
+
190
+ expect(onShowRequested).toHaveBeenCalledTimes(1);
191
+ const request = parseInjectedRequest(await waitForInjectedRequest(webView));
192
+ expect(request.type).toBe(POST_MESSAGE_REQUEST_TYPES.CONNECT);
193
+ expect(request.payload).toEqual({
194
+ metadata: {
195
+ appId: "token_dummy_app",
196
+ appName: "Token Dummy App",
197
+ },
198
+ });
199
+
200
+ const result = {
201
+ accounts: [
202
+ {
203
+ accountType: "thru",
204
+ address: "thru_test_address",
205
+ label: "Account 1",
206
+ },
207
+ ],
208
+ status: "completed",
209
+ };
210
+ sdk.onMessage(responseMessage(frameId, request.id, result));
211
+
212
+ await expect(promise).resolves.toEqual({
213
+ ...result,
214
+ selectedAccount: result.accounts[0],
215
+ });
216
+ expect(onHideRequested).toHaveBeenCalledTimes(1);
217
+ });
218
+
219
+ it("sends transparent createAccount requests through the wallet WebView", async () => {
220
+ sdk.destroy();
221
+ sdk = new NativeSDK({
222
+ walletUrl: "http://localhost:3000/embedded/native/transparent",
223
+ walletExperience: "transparent",
224
+ origin: "thru-mobile://token-dummy",
225
+ });
226
+ webView = new MockWebView();
227
+ sdk.attachWebView(webView);
228
+ const onShowRequested = vi.fn();
229
+ const onHideRequested = vi.fn();
230
+ const onConnect = vi.fn();
231
+ sdk.setUiHandlers({ onShowRequested, onHideRequested });
232
+ sdk.on("connect", onConnect);
233
+
234
+ const frameId = frameIdFor(sdk);
235
+ const promise = sdk.createAccount({
236
+ accountName: "JCoin Account",
237
+ metadata: {
238
+ appId: "token_dummy_app",
239
+ appName: "Token Dummy App",
240
+ appUrl: "thru-mobile://token-dummy",
241
+ },
242
+ });
243
+
244
+ sdk.onMessage(readyMessage(frameId));
245
+ await flush();
246
+
247
+ expect(onShowRequested).toHaveBeenCalledTimes(1);
248
+ const request = parseInjectedRequest(await waitForInjectedRequest(webView));
249
+ expect(request.type).toBe(POST_MESSAGE_REQUEST_TYPES.CREATE_ACCOUNT);
250
+ expect(request.payload).toEqual({
251
+ accountName: "JCoin Account",
252
+ metadata: {
253
+ appId: "token_dummy_app",
254
+ appName: "Token Dummy App",
255
+ appUrl: "thru-mobile://token-dummy",
256
+ },
257
+ });
258
+
259
+ const result = {
260
+ account: {
261
+ accountType: "thru",
262
+ address: "thru_created_address",
263
+ label: "JCoin Account",
264
+ },
265
+ accounts: [
266
+ {
267
+ accountType: "thru",
268
+ address: "thru_created_address",
269
+ label: "JCoin Account",
270
+ },
271
+ ],
272
+ selectedAccount: {
273
+ accountType: "thru",
274
+ address: "thru_created_address",
275
+ label: "JCoin Account",
276
+ },
277
+ signature: "thru_signature",
278
+ vmError: "0",
279
+ userErrorCode: "0",
280
+ executionResult: "0",
281
+ };
282
+ sdk.onMessage(responseMessage(frameId, request.id, result));
283
+
284
+ await expect(promise).resolves.toEqual(result);
285
+ expect(onHideRequested).toHaveBeenCalledTimes(1);
286
+ expect(onConnect).toHaveBeenLastCalledWith({
287
+ accounts: result.accounts,
288
+ selectedAccount: result.selectedAccount,
289
+ status: "completed",
290
+ metadata: {
291
+ appId: "token_dummy_app",
292
+ appName: "Token Dummy App",
293
+ appUrl: "thru-mobile://token-dummy",
294
+ },
295
+ });
296
+ });
297
+
298
+ it("sends transparent passkey challenge signing requests through the wallet WebView", async () => {
299
+ sdk.destroy();
300
+ sdk = new NativeSDK({
301
+ walletUrl: "http://localhost:3000/embedded/native/transparent",
302
+ walletExperience: "transparent",
303
+ origin: "thru-mobile://token-dummy",
304
+ });
305
+ webView = new MockWebView();
306
+ sdk.attachWebView(webView);
307
+ const onShowRequested = vi.fn();
308
+ const onHideRequested = vi.fn();
309
+ sdk.setUiHandlers({ onShowRequested, onHideRequested });
310
+
311
+ const frameId = frameIdFor(sdk);
312
+ const promise = sdk.thru.signPasskeyChallenge({
313
+ challenge: "challenge_base64url",
314
+ walletAddress: "thru_test_address",
315
+ });
316
+
317
+ sdk.onMessage(readyMessage(frameId));
318
+ await flush();
319
+
320
+ expect(onShowRequested).toHaveBeenCalledTimes(1);
321
+ const request = parseInjectedRequest(await waitForInjectedRequest(webView));
322
+ expect(request.type).toBe(POST_MESSAGE_REQUEST_TYPES.SIGN_PASSKEY_CHALLENGE);
323
+ expect(request.payload).toEqual({
324
+ challenge: "challenge_base64url",
325
+ walletAddress: "thru_test_address",
326
+ });
327
+
328
+ const result = {
329
+ signatureR: "01",
330
+ signatureS: "02",
331
+ authenticatorData: "authenticator_data_base64",
332
+ clientDataJSON: "client_data_json_base64",
333
+ };
334
+ sdk.onMessage(responseMessage(frameId, request.id, result));
335
+
336
+ await expect(promise).resolves.toEqual(result);
337
+ expect(onHideRequested).toHaveBeenCalledTimes(1);
338
+ });
339
+
141
340
  it("maps signIn snake-case app metadata to wallet connect metadata", async () => {
142
341
  const frameId = frameIdFor(sdk);
143
342
  const promise = sdk.signIn({
@@ -13,6 +13,7 @@ import {
13
13
  ErrorCode,
14
14
  type ConnectMetadataInput,
15
15
  type ConnectRequestPayload,
16
+ type CreateAccountResult,
16
17
  type GetConnectionStateResult,
17
18
  type ManageAccountsResult,
18
19
  normalizeConnectionStateResult,
@@ -23,8 +24,13 @@ import type {
23
24
  WebViewRefLike,
24
25
  } from "./provider/WebViewBridge";
25
26
  import { createThruClient, type Thru } from "@thru/sdk/client";
27
+ import {
28
+ SigningSessionDescriptorStore,
29
+ resolveSigningSessionStorageKey,
30
+ } from "../signing-sessions";
26
31
 
27
32
  export type IosWebViewMode = "direct" | "shell-iframe";
33
+ export type NativeWalletExperience = "standard" | "transparent";
28
34
 
29
35
  export type WalletAvailability =
30
36
  | {
@@ -66,9 +72,14 @@ export type WalletAvailability =
66
72
 
67
73
  export interface NativeSDKConfig {
68
74
  walletUrl?: string;
75
+ /** Wallet presentation loaded in the native WebView. Transparent mode
76
+ signs in without opening the native wallet sheet. */
77
+ walletExperience?: NativeWalletExperience;
69
78
  /** Stamped on every postMessage so wallet's ConnectedAppsStorage can
70
79
  scope per-host. Default: 'thru-mobile://app'. */
71
80
  origin?: string;
81
+ /** Default app metadata used for connection and transparent hydration. */
82
+ metadata?: ConnectMetadataInput;
72
83
  rpcUrl?: string;
73
84
  addressTypes?: AddressTypeValue[];
74
85
  /** iOS-only host mode. Shell iframe is the default; direct is kept
@@ -81,6 +92,8 @@ export interface NativeSDKConfig {
81
92
  storageKey?: string;
82
93
  /** Override the key used to remember the app-local selected account. */
83
94
  selectedAccountStorageKey?: string;
95
+ /** Override the key used for app-local signing session descriptors. */
96
+ signingSessionStorageKey?: string;
84
97
  }
85
98
 
86
99
  export interface SignInOptions {
@@ -97,6 +110,11 @@ export interface ConnectOptions {
97
110
  intent?: ConnectRequestPayload["intent"];
98
111
  }
99
112
 
113
+ export interface CreateAccountOptions {
114
+ accountName?: string;
115
+ metadata?: ConnectMetadataInput;
116
+ }
117
+
100
118
  export interface RestoreConnectionOptions {
101
119
  hydrate?: boolean;
102
120
  }
@@ -125,6 +143,10 @@ export interface NativeSDKUiHandlers {
125
143
 
126
144
  const DEFAULT_STORAGE_KEY = "thru.native-sdk.connection.v1";
127
145
  const SELECTED_ACCOUNT_STORAGE_KEY_SUFFIX = ".selected-account.v1";
146
+ const SIGNING_SESSION_STORAGE_KEY_SUFFIX = ".signing-sessions.v1";
147
+ const DEFAULT_NATIVE_WALLET_URL = "https://wallet.thru.org/embedded/native";
148
+ const DEFAULT_TRANSPARENT_WALLET_URL =
149
+ "https://wallet.thru.org/embedded/native/transparent";
128
150
 
129
151
  const CHECKING_WALLET_AVAILABILITY: WalletAvailability = {
130
152
  status: "checking",
@@ -139,6 +161,20 @@ const CHECKING_WALLET_AVAILABILITY: WalletAvailability = {
139
161
  error: null,
140
162
  };
141
163
 
164
+ function completeAppMetadata(
165
+ metadata: ConnectMetadataInput | AppMetadata | null | undefined,
166
+ ): AppMetadata | undefined {
167
+ if (!metadata?.appId || !metadata.appName || !metadata.appUrl) {
168
+ return undefined;
169
+ }
170
+ return {
171
+ appId: metadata.appId,
172
+ appName: metadata.appName,
173
+ appUrl: metadata.appUrl,
174
+ ...(metadata.imageUrl ? { imageUrl: metadata.imageUrl } : {}),
175
+ };
176
+ }
177
+
142
178
  interface PersistedSelectedAccountSnapshot {
143
179
  version: 1;
144
180
  origin: string;
@@ -167,6 +203,8 @@ export class NativeSDK {
167
203
  private readonly storageKey: string;
168
204
  private readonly selectedAccountStorageKey: string;
169
205
  private readonly iosWebViewMode: IosWebViewMode;
206
+ private readonly walletExperience: NativeWalletExperience;
207
+ private readonly defaultMetadata?: ConnectMetadataInput;
170
208
 
171
209
  constructor(config: NativeSDKConfig = {}) {
172
210
  this.origin = config.origin ?? "thru-mobile://app";
@@ -177,10 +215,35 @@ export class NativeSDK {
177
215
  config.selectedAccountStorageKey ??
178
216
  `${this.storageKey}${SELECTED_ACCOUNT_STORAGE_KEY_SUFFIX}`;
179
217
  this.iosWebViewMode = config.iosWebViewMode ?? "shell-iframe";
218
+ this.walletExperience = config.walletExperience ?? "standard";
219
+ this.defaultMetadata = config.metadata;
220
+ const walletUrl =
221
+ config.walletUrl ??
222
+ (this.walletExperience === "transparent"
223
+ ? DEFAULT_TRANSPARENT_WALLET_URL
224
+ : DEFAULT_NATIVE_WALLET_URL);
225
+ const walletOrigin = new URL(walletUrl).origin;
226
+ const signingSessions = this.storage
227
+ ? new SigningSessionDescriptorStore(
228
+ this.storage,
229
+ resolveSigningSessionStorageKey({
230
+ walletOrigin,
231
+ appOrigin: this.origin,
232
+ storageKey:
233
+ config.signingSessionStorageKey ??
234
+ `${this.storageKey}${SIGNING_SESSION_STORAGE_KEY_SUFFIX}`,
235
+ }),
236
+ )
237
+ : undefined;
180
238
  this.provider = new NativeProvider({
181
- walletUrl: config.walletUrl,
239
+ walletUrl,
182
240
  origin: this.origin,
241
+ metadata: this.defaultMetadata
242
+ ? this.resolveMetadata(this.defaultMetadata)
243
+ : undefined,
183
244
  addressTypes: config.addressTypes ?? [AddressType.THRU],
245
+ signingSessions,
246
+ walletExperience: this.walletExperience,
184
247
  });
185
248
  this.setupEventForwarding();
186
249
  }
@@ -252,13 +315,13 @@ export class NativeSDK {
252
315
 
253
316
  const inFlight = (async () => {
254
317
  try {
255
- this.provider.requestShow();
318
+ await this.provider.requestShow();
256
319
  if (!this.initialized) await this.initialize();
257
320
 
258
321
  const metadata = this.resolveMetadata(options?.metadata);
259
322
  const preferredAccountAddress = isAccountSwitch
260
323
  ? null
261
- : await this.readSelectedAccountAddress();
324
+ : options?.preferredAccountAddress ?? (await this.readSelectedAccountAddress());
262
325
  const providerOptions =
263
326
  metadata || preferredAccountAddress || options?.intent
264
327
  ? {
@@ -318,11 +381,57 @@ export class NativeSDK {
318
381
  });
319
382
  }
320
383
 
384
+ async createAccount(
385
+ options: CreateAccountOptions = {},
386
+ ): Promise<CreateAccountResult> {
387
+ this.emit("connect", { status: "connecting" });
388
+
389
+ try {
390
+ await this.provider.requestShow();
391
+ if (!this.initialized) await this.initialize();
392
+
393
+ const metadata = this.resolveMetadata(options.metadata);
394
+ const result = await this.provider.createAccount({
395
+ ...(options.accountName ? { accountName: options.accountName } : {}),
396
+ ...(metadata ? { metadata } : {}),
397
+ });
398
+ const selectedAccount = result.selectedAccount ?? result.account;
399
+ const activeResult: CreateAccountResult = {
400
+ ...result,
401
+ accounts: this.provider.getAccounts(),
402
+ selectedAccount,
403
+ account: selectedAccount,
404
+ };
405
+ const completedResult: ConnectResult = {
406
+ accounts: activeResult.accounts,
407
+ selectedAccount: activeResult.selectedAccount,
408
+ status: "completed",
409
+ metadata: completeAppMetadata(metadata),
410
+ };
411
+ this.lastConnectResult = completedResult;
412
+ await this.persistSelectedAccountAddress(
413
+ activeResult.selectedAccount.address,
414
+ );
415
+ await this.clearPersistedConnection();
416
+ this.setWalletAvailability(
417
+ walletAvailabilityFromConnectResult(completedResult),
418
+ );
419
+ this.emit("connect", completedResult);
420
+ this.emit("accountChanged", activeResult.selectedAccount);
421
+ return activeResult;
422
+ } catch (error) {
423
+ this.provider.requestHide();
424
+ this.emit("error", error);
425
+ throw error;
426
+ }
427
+ }
428
+
321
429
  async disconnect(): Promise<void> {
322
430
  try {
323
431
  await this.provider.disconnect();
324
432
  this.emit("disconnect", {});
325
433
  this.lastConnectResult = null;
434
+ await this.persistSelectedAccountAddress(null);
326
435
  await this.clearPersistedConnection();
327
436
  this.clearAuthorizedAvailability();
328
437
  } catch (error) {
@@ -500,11 +609,15 @@ export class NativeSDK {
500
609
  if (!this.initialized) await this.initialize();
501
610
 
502
611
  const metadata =
503
- options?.metadata ?? this.lastConnectResult?.metadata ?? undefined;
612
+ options?.metadata ??
613
+ this.lastConnectResult?.metadata ??
614
+ this.defaultMetadata ??
615
+ undefined;
504
616
  const providerOptions = metadata
505
617
  ? { metadata: this.resolveMetadata(metadata) }
506
618
  : undefined;
507
- const preferredAccountAddress = await this.readSelectedAccountAddress();
619
+ const preferredAccountAddress =
620
+ options?.preferredAccountAddress ?? (await this.readSelectedAccountAddress());
508
621
  const nextProviderOptions =
509
622
  providerOptions || preferredAccountAddress
510
623
  ? {
@@ -575,18 +688,19 @@ export class NativeSDK {
575
688
  private resolveMetadata(
576
689
  input?: ConnectMetadataInput,
577
690
  ): ConnectMetadataInput | undefined {
578
- if (!input) {
691
+ const effectiveInput = input ?? this.defaultMetadata;
692
+ if (!effectiveInput) {
579
693
  /* On RN we have no window.location.origin; require explicit
580
694
  metadata, but stamp the configured origin as appId so the
581
695
  wallet can scope per-host. */
582
696
  return { appId: this.origin };
583
697
  }
584
698
  const metadata: ConnectMetadataInput = {
585
- appId: input.appId ?? this.origin,
699
+ appId: effectiveInput.appId ?? this.origin,
586
700
  };
587
- if (input.appUrl) metadata.appUrl = input.appUrl;
588
- if (input.appName) metadata.appName = input.appName;
589
- if (input.imageUrl) metadata.imageUrl = input.imageUrl;
701
+ if (effectiveInput.appUrl) metadata.appUrl = effectiveInput.appUrl;
702
+ if (effectiveInput.appName) metadata.appName = effectiveInput.appName;
703
+ if (effectiveInput.imageUrl) metadata.imageUrl = effectiveInput.imageUrl;
590
704
  return metadata;
591
705
  }
592
706
 
@@ -2,10 +2,12 @@ export { NativeSDK } from './NativeSDK';
2
2
  export type {
3
3
  EventCallback,
4
4
  ConnectOptions,
5
+ CreateAccountOptions,
5
6
  IosWebViewMode,
6
7
  NativeSDKConfig,
7
8
  NativeSDKStorage,
8
9
  NativeSDKUiHandlers,
10
+ NativeWalletExperience,
9
11
  RestoreConnectionOptions,
10
12
  SDKEvent,
11
13
  SignInOptions,
@@ -22,10 +24,19 @@ export type {
22
24
  IThruChain,
23
25
  SignMessageParams,
24
26
  SignMessageResult,
27
+ ThruPasskeyChallengeIntent,
28
+ ThruPasskeyChallengeSignature,
25
29
  ThruSigningContext,
30
+ ThruSigningSession,
31
+ ThruSigningSessionCreateOptions,
32
+ ThruSigningSessionDescriptor,
33
+ ThruSigningSessionInstruction,
34
+ ThruSigningSessionInstructionCreateOptions,
35
+ ThruSigningSessionTimestamp,
26
36
  ThruTransactionIntent,
27
37
  WalletAccount,
28
38
  } from '../interfaces';
39
+ export type { SigningSessionStorage } from '../signing-sessions';
29
40
 
30
41
  export {
31
42
  EMBEDDED_PROVIDER_EVENTS,
@@ -34,6 +45,7 @@ export {
34
45
  } from '../protocol';
35
46
  export type {
36
47
  ConnectMetadataInput,
48
+ CreateAccountResult,
37
49
  GetConnectionStateResult,
38
50
  ManageAccountsResult,
39
51
  } from '../protocol';