@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.
- package/README.md +1 -0
- package/dist/{BrowserSDK-CpRFiJsW.d.ts → BrowserSDK-CRQTOT8S.d.ts} +178 -3
- package/dist/index.d.ts +2 -2
- package/dist/index.js +376 -12
- package/dist/index.js.map +1 -1
- package/dist/native/react/transparent.d.ts +104 -0
- package/dist/native/react/transparent.js +2210 -0
- package/dist/native/react/transparent.js.map +1 -0
- package/dist/native/react.d.ts +5 -90
- package/dist/native/react.js +765 -32
- package/dist/native/react.js.map +1 -1
- package/dist/native.d.ts +105 -1
- package/dist/native.js +521 -31
- package/dist/native.js.map +1 -1
- package/dist/react-ui.js +5 -0
- package/dist/react-ui.js.map +1 -1
- package/dist/react.d.ts +2 -2
- package/dist/react.js +376 -12
- package/dist/react.js.map +1 -1
- package/package.json +8 -2
- package/src/BrowserSDK.ts +32 -1
- package/src/encoding.ts +39 -0
- package/src/index.ts +5 -1
- package/src/interfaces/IThruChain.ts +50 -1
- package/src/interfaces/types.ts +52 -0
- package/src/native/NativeSDK.test.ts +200 -1
- package/src/native/NativeSDK.ts +124 -10
- package/src/native/index.ts +12 -0
- package/src/native/provider/NativeProvider.ts +106 -5
- package/src/native/provider/WebViewBridge.test.ts +22 -1
- package/src/native/provider/WebViewBridge.ts +17 -7
- package/src/native/provider/chains/ThruChain.ts +215 -5
- package/src/native/react/ThruContext.ts +3 -1
- package/src/native/react/ThruProvider.tsx +25 -0
- package/src/native/react/ThruTransparentWalletBridge.tsx +281 -0
- package/src/native/react/hooks/useWallet.ts +12 -1
- package/src/native/react/index.ts +11 -0
- package/src/native/react/transparent.ts +35 -0
- package/src/protocol/postMessage.ts +127 -2
- package/src/provider/EmbeddedProvider.ts +7 -1
- package/src/provider/IframeManager.test.ts +18 -0
- package/src/provider/IframeManager.ts +8 -1
- package/src/provider/chains/ThruChain.ts +210 -4
- package/src/provider/types/messages.ts +16 -0
- package/src/react/index.ts +6 -0
- package/src/signing-sessions.test.ts +182 -0
- 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(
|
|
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
|
|
142
|
-
|
|
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('
|
|
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 = [
|
|
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
|
|
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.
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
>
|