@thru/wallet 0.2.25 → 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
@@ -13,6 +13,8 @@ import {
13
13
  createRequestId,
14
14
  type ConnectMetadataInput,
15
15
  type ConnectRequestPayload,
16
+ type CreateAccountPayload,
17
+ type CreateAccountResult,
16
18
  type EmbeddedProviderEvent,
17
19
  type GetConnectionStateResult,
18
20
  type ManageAccountsResult,
@@ -20,6 +22,7 @@ import {
20
22
  normalizeConnectionStateResult,
21
23
  } from "../../protocol";
22
24
  import { NativeThruChain } from "./chains/ThruChain";
25
+ import type { SigningSessionDescriptorStore } from "../../signing-sessions";
23
26
  import {
24
27
  WebViewBridge,
25
28
  type WebViewMessageEventLike,
@@ -28,14 +31,20 @@ import {
28
31
 
29
32
  const DEFAULT_WALLET_URL = "https://wallet.thru.org/embedded/native";
30
33
  const DEFAULT_ORIGIN = "thru-mobile://app";
34
+ const TRANSPARENT_FOCUS_SETTLE_MS = 500;
31
35
 
32
36
  export interface NativeProviderConfig {
33
37
  /** wallet.thru.org/embedded/native URL to load. */
34
38
  walletUrl?: string;
39
+ /** Standard bottom-sheet wallet or transparent auto-signing wallet. */
40
+ walletExperience?: "standard" | "transparent";
35
41
  /** Caller-supplied dapp origin. Stamped on every postMessage so
36
42
  wallet's ConnectedAppsStorage can scope per-host. */
37
43
  origin?: string;
44
+ /** Default app metadata used by trusted transparent requests. */
45
+ metadata?: ConnectMetadataInput;
38
46
  addressTypes?: AddressTypeValue[];
47
+ signingSessions?: SigningSessionDescriptorStore;
39
48
  }
40
49
 
41
50
  export interface ConnectOptions {
@@ -44,6 +53,11 @@ export interface ConnectOptions {
44
53
  intent?: ConnectRequestPayload["intent"];
45
54
  }
46
55
 
56
+ export interface CreateAccountOptions {
57
+ accountName?: string;
58
+ metadata?: ConnectMetadataInput;
59
+ }
60
+
47
61
  export type NativeProviderEvent = EmbeddedProviderEvent;
48
62
  export type NativeProviderEventCallback = (data?: unknown) => void;
49
63
 
@@ -57,10 +71,12 @@ export type NativeProviderEventCallback = (data?: unknown) => void;
57
71
  export class NativeProvider {
58
72
  private readonly bridge: WebViewBridge;
59
73
  private readonly origin: string;
74
+ private readonly transparent: boolean;
60
75
  private _thruChain?: IThruChain;
61
76
  private connected = false;
62
77
  private accounts: WalletAccount[] = [];
63
78
  private selectedAccount: WalletAccount | null = null;
79
+ private isSurfaceShown = false;
64
80
  private readonly eventListeners = new Map<
65
81
  string,
66
82
  Set<NativeProviderEventCallback>
@@ -73,9 +89,14 @@ export class NativeProvider {
73
89
  constructor(config: NativeProviderConfig = {}) {
74
90
  const walletUrl = config.walletUrl ?? DEFAULT_WALLET_URL;
75
91
  this.origin = config.origin ?? DEFAULT_ORIGIN;
92
+ this.transparent = config.walletExperience === "transparent";
76
93
  this.bridge = new WebViewBridge({ walletUrl });
77
94
 
78
95
  this.bridge.onEvent = (eventType, payload) => {
96
+ if (this.transparent && eventType === EMBEDDED_PROVIDER_EVENTS.UI_SHOW) {
97
+ return;
98
+ }
99
+
79
100
  this.emit(eventType as NativeProviderEvent, payload);
80
101
 
81
102
  if (eventType === EMBEDDED_PROVIDER_EVENTS.UI_SHOW) {
@@ -102,7 +123,12 @@ export class NativeProvider {
102
123
 
103
124
  const addressTypes = config.addressTypes ?? [AddressType.THRU];
104
125
  if (addressTypes.includes(AddressType.THRU)) {
105
- this._thruChain = new NativeThruChain(this.bridge, this, this.origin);
126
+ this._thruChain = new NativeThruChain(
127
+ this.bridge,
128
+ this,
129
+ this.origin,
130
+ config.signingSessions,
131
+ );
106
132
  }
107
133
  }
108
134
 
@@ -138,13 +164,28 @@ export class NativeProvider {
138
164
  await this.bridge.awaitReady();
139
165
  }
140
166
 
141
- /** Open the wallet UI (called internally; also exposed for host). */
142
- requestShow(): void {
167
+ /** Open or focus the wallet host surface. Transparent hosts use this
168
+ to give WKWebView a focused document for WebAuthn without showing
169
+ wallet UI. */
170
+ async requestShow(): Promise<void> {
171
+ if (this.transparent) {
172
+ if (!this.isSurfaceShown) {
173
+ this.isSurfaceShown = true;
174
+ this.onShowRequested?.();
175
+ }
176
+ await new Promise((resolve) =>
177
+ setTimeout(resolve, TRANSPARENT_FOCUS_SETTLE_MS),
178
+ );
179
+ return;
180
+ }
181
+ if (this.isSurfaceShown) return;
182
+ this.isSurfaceShown = true;
143
183
  this.onShowRequested?.();
144
184
  }
145
185
 
146
186
  /** Close the wallet UI (called internally; also exposed for host). */
147
187
  requestHide(): void {
188
+ this.isSurfaceShown = false;
148
189
  this.onHideRequested?.();
149
190
  }
150
191
 
@@ -156,7 +197,7 @@ export class NativeProvider {
156
197
  async connect(options?: ConnectOptions): Promise<ConnectResult> {
157
198
  this.emit(EMBEDDED_PROVIDER_EVENTS.CONNECT_START, {});
158
199
  try {
159
- this.requestShow();
200
+ await this.requestShow();
160
201
  const payload: ConnectRequestPayload = {};
161
202
  if (options?.metadata) payload.metadata = options.metadata;
162
203
  if (options?.preferredAccountAddress) {
@@ -172,6 +213,9 @@ export class NativeProvider {
172
213
  });
173
214
 
174
215
  const result = normalizeWalletAccountResult(response.result);
216
+ if (!result.selectedAccount) {
217
+ throw new Error("Wallet did not return an account");
218
+ }
175
219
  this.connected = true;
176
220
  this.accounts = result.accounts;
177
221
  this.selectedAccount = result.selectedAccount;
@@ -186,6 +230,59 @@ export class NativeProvider {
186
230
  }
187
231
  }
188
232
 
233
+ async createAccount(
234
+ options?: CreateAccountOptions,
235
+ ): Promise<CreateAccountResult> {
236
+ try {
237
+ await this.requestShow();
238
+ const payload: CreateAccountPayload = {};
239
+ if (options?.accountName) payload.accountName = options.accountName;
240
+ if (options?.metadata) payload.metadata = options.metadata;
241
+
242
+ const response = await this.bridge.sendMessage({
243
+ id: createRequestId(),
244
+ type: POST_MESSAGE_REQUEST_TYPES.CREATE_ACCOUNT,
245
+ payload,
246
+ origin: this.origin,
247
+ });
248
+
249
+ const normalized = normalizeWalletAccountResult(
250
+ response.result,
251
+ response.result.selectedAccount ?? response.result.account,
252
+ );
253
+ const selectedAccount =
254
+ normalized.selectedAccount ?? response.result.account;
255
+ if (!selectedAccount) {
256
+ throw new Error("Wallet did not return a created account");
257
+ }
258
+ const result: CreateAccountResult = {
259
+ ...response.result,
260
+ accounts: normalized.accounts,
261
+ selectedAccount,
262
+ account: selectedAccount,
263
+ };
264
+ this.connected = true;
265
+ this.accounts = result.accounts;
266
+ this.selectedAccount = result.selectedAccount;
267
+
268
+ this.emit(EMBEDDED_PROVIDER_EVENTS.CONNECT, {
269
+ accounts: result.accounts,
270
+ selectedAccount: result.selectedAccount,
271
+ status: "completed",
272
+ metadata: options?.metadata,
273
+ });
274
+ this.emit(EMBEDDED_PROVIDER_EVENTS.ACCOUNT_CHANGED, {
275
+ account: result.selectedAccount,
276
+ });
277
+ this.requestHide();
278
+ return result;
279
+ } catch (error) {
280
+ this.requestHide();
281
+ this.emit(EMBEDDED_PROVIDER_EVENTS.ERROR, { error });
282
+ throw error;
283
+ }
284
+ }
285
+
189
286
  async getConnectionState(
190
287
  options?: ConnectOptions,
191
288
  ): Promise<GetConnectionStateResult> {
@@ -247,6 +344,10 @@ export class NativeProvider {
247
344
  return this.connected;
248
345
  }
249
346
 
347
+ isTransparent(): boolean {
348
+ return this.transparent;
349
+ }
350
+
250
351
  hydrateConnection(
251
352
  result: ConnectResult,
252
353
  selectedAccountAddress?: string | null,
@@ -292,7 +393,7 @@ export class NativeProvider {
292
393
  async manageAccounts(): Promise<ManageAccountsResult> {
293
394
  if (!this.connected) throw new Error("Wallet not connected");
294
395
  try {
295
- this.requestShow();
396
+ await this.requestShow();
296
397
  const response = await this.bridge.sendMessage({
297
398
  id: createRequestId(),
298
399
  type: POST_MESSAGE_REQUEST_TYPES.MANAGE_ACCOUNTS,
@@ -170,6 +170,16 @@ describe('WebViewBridge', () => {
170
170
  ).toThrow(/Untrusted wallet origin/);
171
171
  });
172
172
 
173
+ it('allows wallet.tid.sh in production builds', () => {
174
+ process.env.NODE_ENV = 'production';
175
+
176
+ const productionBridge = new WebViewBridge({
177
+ walletUrl: 'https://wallet.tid.sh/embedded',
178
+ });
179
+ expect(productionBridge.walletOrigin).toBe('https://wallet.tid.sh');
180
+ productionBridge.destroy();
181
+ });
182
+
173
183
  it('uses the React Native __DEV__ flag when present', () => {
174
184
  process.env.NODE_ENV = 'test';
175
185
  (globalThis as typeof globalThis & { __DEV__?: boolean }).__DEV__ = false;
@@ -193,6 +203,16 @@ describe('WebViewBridge', () => {
193
203
  expect(src.startsWith('http://localhost:3000/embedded')).toBe(true);
194
204
  });
195
205
 
206
+ it('preserves transparent native wallet paths', () => {
207
+ const transparentBridge = new WebViewBridge({
208
+ walletUrl: 'http://localhost:3000/embedded/native/transparent',
209
+ });
210
+ const src = new URL(transparentBridge.getIframeSrc());
211
+ expect(src.pathname).toBe('/embedded/native/transparent');
212
+ expect(src.searchParams.get('tn_frame_id')).toBe(transparentBridge.frameId);
213
+ transparentBridge.destroy();
214
+ });
215
+
196
216
  it('resolves awaitReady on IFRAME_READY_EVENT with matching frameId', async () => {
197
217
  const ready = bridge.awaitReady();
198
218
  bridge.onMessage(readyMessage(bridge.frameId));
@@ -233,7 +253,8 @@ describe('WebViewBridge', () => {
233
253
  await flush();
234
254
  expect(webView.injected.length).toBe(1);
235
255
  expect(webView.injected[0]).toContain('window.__pushIn');
236
- expect(webView.injected[0]).toContain('MessageEvent');
256
+ expect(webView.injected[0]).toContain('window.postMessage');
257
+ expect(webView.injected[0]).toContain(bridge.frameId);
237
258
  expect(webView.injected[0]).toContain(id);
238
259
 
239
260
  bridge.onMessage(responseMessage(bridge.frameId, id, { accounts: [] }));
@@ -16,7 +16,10 @@ import {
16
16
  iframe<->ReactNativeWebView postMessage traffic. This bridge only
17
17
  speaks the RN side: webView.injectJavaScript out, onMessage in. */
18
18
 
19
- const PRODUCTION_WALLET_ORIGINS = ['https://wallet.thru.org'];
19
+ const PRODUCTION_WALLET_ORIGINS = [
20
+ 'https://wallet.thru.org',
21
+ 'https://wallet.tid.sh',
22
+ ];
20
23
 
21
24
  function isDevelopmentBuild(): boolean {
22
25
  const runtime = globalThis as typeof globalThis & {
@@ -88,6 +91,11 @@ function validateWalletOrigin(walletUrl: string): void {
88
91
  }
89
92
  }
90
93
 
94
+ function isNativeEmbeddedWalletPath(pathname: string): boolean {
95
+ const normalized = pathname.replace(/\/+$/, '') || '/';
96
+ return normalized === '/embedded/native' || normalized.startsWith('/embedded/native/');
97
+ }
98
+
91
99
  /* Minimal contract for a react-native-webview ref. We accept both refs
92
100
  ({ current: WebView }) and direct WebView instances. */
93
101
  export interface WebViewRefLike {
@@ -104,9 +112,14 @@ const FAST_REQUEST_TIMEOUT_MS = 30 * 1000;
104
112
 
105
113
  const SLOW_REQUEST_TYPES: ReadonlySet<string> = new Set([
106
114
  POST_MESSAGE_REQUEST_TYPES.CONNECT,
115
+ POST_MESSAGE_REQUEST_TYPES.CREATE_ACCOUNT,
107
116
  POST_MESSAGE_REQUEST_TYPES.SIGN_MESSAGE,
108
117
  POST_MESSAGE_REQUEST_TYPES.SIGN_TRANSACTION,
118
+ POST_MESSAGE_REQUEST_TYPES.SIGN_PASSKEY_CHALLENGE,
109
119
  POST_MESSAGE_REQUEST_TYPES.MANAGE_ACCOUNTS,
120
+ POST_MESSAGE_REQUEST_TYPES.CREATE_SIGNING_SESSION,
121
+ POST_MESSAGE_REQUEST_TYPES.CREATE_SIGNING_SESSION_INSTRUCTION,
122
+ POST_MESSAGE_REQUEST_TYPES.CONFIRM_SIGNING_SESSION,
110
123
  ]);
111
124
 
112
125
  export interface WebViewBridgeOptions {
@@ -154,7 +167,7 @@ export class WebViewBridge {
154
167
  */
155
168
  getIframeSrc(): string {
156
169
  const url = new URL(this.walletUrl);
157
- if (!url.pathname.endsWith('/native')) {
170
+ if (!isNativeEmbeddedWalletPath(url.pathname)) {
158
171
  url.pathname = `${url.pathname.replace(/\/$/, '')}/native`;
159
172
  }
160
173
  url.searchParams.set('tn_frame_id', this.frameId);
@@ -246,14 +259,11 @@ export class WebViewBridge {
246
259
  });
247
260
 
248
261
  const script = `try {
249
- var msg = ${JSON.stringify(request)};
262
+ var msg = ${JSON.stringify({ ...request, frameId: this.frameId })};
250
263
  if (window.__pushIn) {
251
264
  window.__pushIn(msg);
252
265
  } else {
253
- window.dispatchEvent(new MessageEvent('message', {
254
- data: msg,
255
- origin: msg.origin || ''
256
- }));
266
+ window.postMessage(msg, window.location.origin);
257
267
  }
258
268
  } catch (e) {} ; true;`;
259
269
  this.webView!.injectJavaScript(script);
@@ -2,11 +2,42 @@ import {
2
2
  AddressType,
3
3
  type IThruChain,
4
4
  type ThruSigningContext,
5
+ type ThruSigningSession,
6
+ type ThruSigningSessionCreateOptions,
7
+ type ThruSigningSessionDescriptor,
8
+ type ThruSigningSessionInstruction,
9
+ type ThruSigningSessionInstructionCreateOptions,
10
+ type ThruPasskeyChallengeIntent,
11
+ type ThruPasskeyChallengeSignature,
5
12
  type ThruTransactionIntent,
6
13
  } from "../../../interfaces";
7
14
  import { POST_MESSAGE_REQUEST_TYPES, createRequestId } from "../../../protocol";
15
+ import { base64ToBytes } from "../../../encoding";
8
16
  import type { NativeProvider } from "../NativeProvider";
9
17
  import type { WebViewBridge } from "../WebViewBridge";
18
+ import {
19
+ SigningSessionDescriptorStore,
20
+ assertSigningSessionWalletAccountIdx,
21
+ resolveSessionExpirySeconds,
22
+ } from "../../../signing-sessions";
23
+
24
+ function descriptorFromWire(session: {
25
+ id: string;
26
+ walletAddress: string;
27
+ publicKey: string;
28
+ authIdx: number;
29
+ expiresAt: string;
30
+ createdAt: string;
31
+ }): ThruSigningSessionDescriptor {
32
+ return {
33
+ id: session.id,
34
+ walletAddress: session.walletAddress,
35
+ publicKey: session.publicKey,
36
+ authIdx: session.authIdx,
37
+ expiresAt: Number(BigInt(session.expiresAt)),
38
+ createdAt: Number(BigInt(session.createdAt)),
39
+ };
40
+ }
10
41
 
11
42
  /**
12
43
  * NativeThruChain - mirror of EmbeddedThruChain over the WebView bridge.
@@ -17,11 +48,18 @@ export class NativeThruChain implements IThruChain {
17
48
  private readonly bridge: WebViewBridge;
18
49
  private readonly provider: NativeProvider;
19
50
  private readonly origin: string;
51
+ private readonly signingSessions?: SigningSessionDescriptorStore;
20
52
 
21
- constructor(bridge: WebViewBridge, provider: NativeProvider, origin: string) {
53
+ constructor(
54
+ bridge: WebViewBridge,
55
+ provider: NativeProvider,
56
+ origin: string,
57
+ signingSessions?: SigningSessionDescriptorStore,
58
+ ) {
22
59
  this.bridge = bridge;
23
60
  this.provider = provider;
24
61
  this.origin = origin;
62
+ this.signingSessions = signingSessions;
25
63
  }
26
64
 
27
65
  get connected(): boolean {
@@ -46,7 +84,7 @@ export class NativeThruChain implements IThruChain {
46
84
  }
47
85
 
48
86
  async getSigningContext(): Promise<ThruSigningContext> {
49
- if (!this.provider.isConnected()) {
87
+ if (!this.provider.isConnected() && !this.provider.isTransparent()) {
50
88
  throw new Error("Wallet not connected");
51
89
  }
52
90
  const response = await this.bridge.sendMessage({
@@ -58,28 +96,200 @@ export class NativeThruChain implements IThruChain {
58
96
  }
59
97
 
60
98
  async signTransaction(transaction: ThruTransactionIntent): Promise<string> {
61
- if (!this.provider.isConnected()) {
99
+ const signingSessionId = transaction.signingSessionId;
100
+ if (
101
+ !signingSessionId &&
102
+ !this.provider.isConnected() &&
103
+ !this.provider.isTransparent()
104
+ ) {
62
105
  throw new Error("Wallet not connected");
63
106
  }
64
107
 
65
- this.provider.requestShow();
108
+ const session = signingSessionId
109
+ ? await this.requireSigningSession(signingSessionId)
110
+ : null;
111
+ const shouldShowWallet = !signingSessionId;
112
+ if (shouldShowWallet) {
113
+ await this.provider.requestShow();
114
+ }
66
115
  try {
67
116
  const response = await this.bridge.sendMessage({
68
117
  id: createRequestId(),
69
118
  type: POST_MESSAGE_REQUEST_TYPES.SIGN_TRANSACTION,
70
119
  payload: {
71
- walletAddress: transaction.walletAddress,
120
+ walletAddress: transaction.walletAddress ?? session?.walletAddress,
72
121
  programAddress: transaction.programAddress,
73
122
  instructionData: transaction.instructionData,
74
123
  readWriteAddresses: transaction.readWriteAddresses,
75
124
  readOnlyAddresses: transaction.readOnlyAddresses,
76
125
  review: transaction.review,
126
+ signingSessionId,
77
127
  },
78
128
  origin: this.origin,
79
129
  });
80
130
  return response.result.signedTransaction;
131
+ } finally {
132
+ if (shouldShowWallet) {
133
+ this.provider.requestHide();
134
+ }
135
+ }
136
+ }
137
+
138
+ async signPasskeyChallenge(
139
+ challenge: ThruPasskeyChallengeIntent,
140
+ ): Promise<ThruPasskeyChallengeSignature> {
141
+ if (!this.provider.isConnected() && !this.provider.isTransparent()) {
142
+ throw new Error("Wallet not connected");
143
+ }
144
+ await this.provider.requestShow();
145
+ try {
146
+ const response = await this.bridge.sendMessage({
147
+ id: createRequestId(),
148
+ type: POST_MESSAGE_REQUEST_TYPES.SIGN_PASSKEY_CHALLENGE,
149
+ payload: {
150
+ challenge: challenge.challenge,
151
+ walletAddress: challenge.walletAddress,
152
+ },
153
+ origin: this.origin,
154
+ });
155
+ return response.result;
81
156
  } finally {
82
157
  this.provider.requestHide();
83
158
  }
84
159
  }
160
+
161
+ async createSigningSession(
162
+ options: ThruSigningSessionCreateOptions,
163
+ ): Promise<ThruSigningSession> {
164
+ if (!this.provider.isConnected()) {
165
+ throw new Error("Wallet not connected");
166
+ }
167
+ if (!this.signingSessions) {
168
+ throw new Error("NativeSDKStorage is required for signing sessions");
169
+ }
170
+
171
+ const expiresAt = resolveSessionExpirySeconds(options);
172
+ await this.provider.requestShow();
173
+ try {
174
+ const response = await this.bridge.sendMessage({
175
+ id: createRequestId(),
176
+ type: POST_MESSAGE_REQUEST_TYPES.CREATE_SIGNING_SESSION,
177
+ payload: {
178
+ walletAddress: options.walletAddress,
179
+ expiresAt: String(expiresAt),
180
+ review: options.review,
181
+ },
182
+ origin: this.origin,
183
+ });
184
+ const descriptor = descriptorFromWire(response.result.session);
185
+ await this.signingSessions.saveReplacingWalletSessions(descriptor);
186
+ return this.toSigningSession(descriptor);
187
+ } finally {
188
+ this.provider.requestHide();
189
+ }
190
+ }
191
+
192
+ async createSigningSessionInstruction(
193
+ options: ThruSigningSessionInstructionCreateOptions,
194
+ ): Promise<ThruSigningSessionInstruction> {
195
+ if (!this.provider.isConnected()) {
196
+ throw new Error("Wallet not connected");
197
+ }
198
+ if (!this.signingSessions) {
199
+ throw new Error("NativeSDKStorage is required for signing sessions");
200
+ }
201
+
202
+ const expiresAt = resolveSessionExpirySeconds(options);
203
+ assertSigningSessionWalletAccountIdx(options.walletAccountIdx);
204
+ const response = await this.bridge.sendMessage({
205
+ id: createRequestId(),
206
+ type: POST_MESSAGE_REQUEST_TYPES.CREATE_SIGNING_SESSION_INSTRUCTION,
207
+ payload: {
208
+ walletAddress: options.walletAddress,
209
+ expiresAt: String(expiresAt),
210
+ walletAccountIdx: options.walletAccountIdx,
211
+ },
212
+ origin: this.origin,
213
+ });
214
+ const descriptor = descriptorFromWire(response.result.session);
215
+ return {
216
+ session: this.toSigningSession(descriptor),
217
+ programAddress: response.result.programAddress,
218
+ instructionData: base64ToBytes(response.result.instructionData),
219
+ };
220
+ }
221
+
222
+ async confirmSigningSession(id: string): Promise<ThruSigningSession> {
223
+ if (!this.provider.isConnected()) {
224
+ throw new Error("Wallet not connected");
225
+ }
226
+ if (!this.signingSessions) {
227
+ throw new Error("NativeSDKStorage is required for signing sessions");
228
+ }
229
+
230
+ const response = await this.bridge.sendMessage({
231
+ id: createRequestId(),
232
+ type: POST_MESSAGE_REQUEST_TYPES.CONFIRM_SIGNING_SESSION,
233
+ payload: { sessionId: id },
234
+ origin: this.origin,
235
+ });
236
+ const descriptor = descriptorFromWire(response.result.session);
237
+ await this.signingSessions.saveReplacingWalletSessions(descriptor);
238
+ return this.toSigningSession(descriptor);
239
+ }
240
+
241
+ async getSigningSession(id: string): Promise<ThruSigningSession | null> {
242
+ if (!this.signingSessions) return null;
243
+ const descriptor = await this.signingSessions.get(id);
244
+ return descriptor ? this.toSigningSession(descriptor) : null;
245
+ }
246
+
247
+ async getSigningSessions(): Promise<ThruSigningSession[]> {
248
+ if (!this.signingSessions) return [];
249
+ return (await this.signingSessions.list()).map((descriptor) =>
250
+ this.toSigningSession(descriptor),
251
+ );
252
+ }
253
+
254
+ async revokeSigningSession(id: string): Promise<void> {
255
+ try {
256
+ await this.bridge.sendMessage({
257
+ id: createRequestId(),
258
+ type: POST_MESSAGE_REQUEST_TYPES.REVOKE_SIGNING_SESSION,
259
+ payload: { sessionId: id },
260
+ origin: this.origin,
261
+ });
262
+ } finally {
263
+ await this.signingSessions?.remove(id);
264
+ }
265
+ }
266
+
267
+ private async requireSigningSession(
268
+ id: string,
269
+ ): Promise<ThruSigningSessionDescriptor> {
270
+ if (!this.signingSessions) {
271
+ throw new Error("NativeSDKStorage is required for signing sessions");
272
+ }
273
+ const session = await this.signingSessions.get(id);
274
+ if (!session) {
275
+ throw new Error("Signing session is not known to this app");
276
+ }
277
+ return session;
278
+ }
279
+
280
+ private toSigningSession(
281
+ descriptor: ThruSigningSessionDescriptor,
282
+ ): ThruSigningSession {
283
+ return {
284
+ ...descriptor,
285
+ signTransaction: (transaction) =>
286
+ this.signTransaction({
287
+ ...transaction,
288
+ walletAddress: transaction.walletAddress ?? descriptor.walletAddress,
289
+ signingSessionId: descriptor.id,
290
+ }),
291
+ revoke: () => this.revokeSigningSession(descriptor.id),
292
+ toJSON: () => ({ ...descriptor }),
293
+ };
294
+ }
85
295
  }
@@ -1,10 +1,11 @@
1
1
  import { createContext } from 'react';
2
2
  import type {
3
+ CreateAccountOptions,
3
4
  NativeSDK,
4
5
  WalletAvailability,
5
6
  } from "../NativeSDK";
6
7
  import type { WalletAccount } from "../../interfaces";
7
- import type { ManageAccountsResult } from "../../protocol";
8
+ import type { CreateAccountResult, ManageAccountsResult } from "../../protocol";
8
9
 
9
10
  export const CHECKING_WALLET_AVAILABILITY: WalletAvailability = {
10
11
  status: 'checking',
@@ -31,6 +32,7 @@ export interface ThruContextValue {
31
32
  walletAvailability: WalletAvailability;
32
33
  error: Error | null;
33
34
  selectAccount: (account: WalletAccount) => Promise<void>;
35
+ createAccount: (options?: CreateAccountOptions) => Promise<CreateAccountResult>;
34
36
  manageAccounts: () => Promise<ManageAccountsResult>;
35
37
  }
36
38
 
@@ -6,10 +6,12 @@
6
6
  import { type ReactNode, useCallback, useEffect, useState } from "react";
7
7
  import {
8
8
  NativeSDK,
9
+ type CreateAccountOptions,
9
10
  type NativeSDKConfig,
10
11
  type WalletAvailability,
11
12
  } from "../NativeSDK";
12
13
  import type { WalletAccount } from "../../interfaces";
14
+ import type { CreateAccountResult } from "../../protocol";
13
15
  import { CHECKING_WALLET_AVAILABILITY, ThruContext } from "./ThruContext";
14
16
 
15
17
  export interface ThruProviderProps {
@@ -147,6 +149,28 @@ export function ThruProvider({ children, config }: ThruProviderProps) {
147
149
  }
148
150
  }, [sdk]);
149
151
 
152
+ const createAccount = useCallback(
153
+ async (options?: CreateAccountOptions): Promise<CreateAccountResult> => {
154
+ if (!sdk) throw new Error("NativeSDK not initialized");
155
+ try {
156
+ const result = await sdk.createAccount(options);
157
+ setSelectedAccount(result.selectedAccount);
158
+ setAccounts(result.accounts);
159
+ setIsConnected(true);
160
+ setIsConnecting(false);
161
+ setWalletAvailability(sdk.getWalletAvailability());
162
+ return result;
163
+ } catch (err) {
164
+ setError(
165
+ err instanceof Error ? err : new Error("createAccount failed"),
166
+ );
167
+ setIsConnecting(false);
168
+ throw err;
169
+ }
170
+ },
171
+ [sdk],
172
+ );
173
+
150
174
  return (
151
175
  <ThruContext.Provider
152
176
  value={{
@@ -159,6 +183,7 @@ export function ThruProvider({ children, config }: ThruProviderProps) {
159
183
  selectedAccount,
160
184
  walletAvailability,
161
185
  selectAccount,
186
+ createAccount,
162
187
  manageAccounts,
163
188
  }}
164
189
  >