@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
package/src/encoding.ts
ADDED
|
@@ -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,
|
|
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 {
|
|
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
|
}
|
package/src/interfaces/types.ts
CHANGED
|
@@ -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({
|
package/src/native/NativeSDK.ts
CHANGED
|
@@ -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
|
|
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 ??
|
|
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 =
|
|
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
|
-
|
|
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:
|
|
699
|
+
appId: effectiveInput.appId ?? this.origin,
|
|
586
700
|
};
|
|
587
|
-
if (
|
|
588
|
-
if (
|
|
589
|
-
if (
|
|
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
|
|
package/src/native/index.ts
CHANGED
|
@@ -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';
|