@solana-mobile/wallet-adapter-mobile 0.0.1-alpha.0

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/LICENSE ADDED
@@ -0,0 +1,13 @@
1
+ Copyright 2022 Solana Mobile Inc.
2
+
3
+ Licensed under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License.
5
+ You may obtain a copy of the License at
6
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ Unless required by applicable law or agreed to in writing, software
10
+ distributed under the License is distributed on an "AS IS" BASIS,
11
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ See the License for the specific language governing permissions and
13
+ limitations under the License.
package/README.md ADDED
@@ -0,0 +1,72 @@
1
+ # `@solana-mobile/wallet-adapter-mobile`
2
+
3
+ This is a plugin for use with [`@solana/wallet-adapter`](https://github.com/solana-labs/wallet-adapter). It enables apps to use a native wallet app on a mobile device to sign messages and transactions, and to send transactions if the wallet offers support for sending transactions.
4
+
5
+ ![A screenshot showing the Solana Mobile wallet adapter in use with the wallet adapter dialog](https://user-images.githubusercontent.com/13243/174880433-92486385-6f9a-4221-bb55-c05bab057be6.png)
6
+
7
+ ## Usage
8
+
9
+ Create an instance of the mobile wallet adapter like this.
10
+
11
+ ```typescript
12
+ new SolanaMobileWalletAdapter({
13
+ appIdentity: {
14
+ name: 'My app',
15
+ uri: 'https://myapp.io',
16
+ icon: 'relative/path/to/icon.png',
17
+ },
18
+ authorizationResultCache: createDefaultAuthorizationResultCache(),
19
+ });
20
+ ```
21
+
22
+ Use that adapter instance alongside the other adapters used by your app.
23
+
24
+ ```typescript
25
+ const wallets = useMemo(() => [
26
+ new SolanaMobileWalletAdapter({
27
+ appIdentity: {
28
+ name: 'My app',
29
+ uri: 'https://myapp.io',
30
+ icon: 'relative/path/to/icon.png',
31
+ },
32
+ authorizationResultCache: createDefaultAuthorizationResultCache(),
33
+ });
34
+ new PhantomWalletAdapter(),
35
+ /* ... other wallets ... */
36
+ ]);
37
+
38
+ return (
39
+ <ConnectionProvider endpoint={clusterApiUrl(WalletAdapterNetwork.Devnet)}>
40
+ <WalletProvider wallets={wallets}>
41
+ <MyApp />
42
+ </WalletProvider>
43
+ </ConnectionProvider>
44
+ )
45
+ ```
46
+
47
+ For more information about how to use wallet adapter plugins, visit https://github.com/solana-labs/wallet-adapter
48
+
49
+ ## Configuration
50
+
51
+ ### App identity
52
+
53
+ The `AppIdentity` config identifies your app to a native mobile wallet. When someone connects to a wallet for the first time, the wallet may present this information in the on-screen prompt where the ask if the visitor would like to authorize your app for use with their account.
54
+
55
+ - `name` &ndash; The plain-language name of your application.
56
+ - `uri` &ndash; The uri of your application. This uri may be required to participate in [dApp identity verification](https://github.com/solana-mobile/mobile-wallet-adapter/blob/main/spec/spec.md#dapp-identity-verification) as part of the mobile wallet adapter protocol specification.
57
+ - `icon` &ndash; An icon file path, relative to the `uri`.
58
+
59
+ ### Authorization result cache
60
+
61
+ The first time that someone authorizes a native wallet app for use with your application, you should cache that authorization for future use. You can supply your own implementation that conforms to the `AuthorizationResultCache` interface.
62
+
63
+ ```typescript
64
+ export interface AuthorizationResultCache {
65
+ clear(): Promise<void>;
66
+ get(): Promise<AuthorizationResult | undefined>;
67
+ set(authorizationResult: AuthorizationResult): Promise<void>;
68
+ }
69
+ ```
70
+
71
+ Alternatively, you can use the included `createDefaultAuthorizationResultCache()` method to create a cache that reads and writes the adapter's last-obtained `AuthorizationResult` to your browser's local storage, if available.
72
+
@@ -0,0 +1,344 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ var mobileWalletAdapterProtocol = require('@solana-mobile/mobile-wallet-adapter-protocol');
6
+ var walletAdapterBase = require('@solana/wallet-adapter-base');
7
+ var web3_js = require('@solana/web3.js');
8
+
9
+ /*! *****************************************************************************
10
+ Copyright (c) Microsoft Corporation.
11
+
12
+ Permission to use, copy, modify, and/or distribute this software for any
13
+ purpose with or without fee is hereby granted.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
16
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
17
+ AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
18
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
19
+ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
20
+ OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
21
+ PERFORMANCE OF THIS SOFTWARE.
22
+ ***************************************************************************** */
23
+
24
+ function __awaiter(thisArg, _arguments, P, generator) {
25
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
26
+ return new (P || (P = Promise))(function (resolve, reject) {
27
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
28
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
29
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
30
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
31
+ });
32
+ }
33
+
34
+ const SolanaMobileWalletAdapterWalletName = 'Default wallet app';
35
+ const SIGNATURE_LENGTH_IN_BYTES = 64;
36
+ function getBase64StringFromByteArray(byteArray) {
37
+ return btoa(String.fromCharCode.call(null, ...byteArray));
38
+ }
39
+ function getByteArrayFromBase64String(base64EncodedByteArray) {
40
+ return new Uint8Array(atob(base64EncodedByteArray)
41
+ .split('')
42
+ .map((c) => c.charCodeAt(0)));
43
+ }
44
+ class SolanaMobileWalletAdapter extends walletAdapterBase.BaseMessageSignerWalletAdapter {
45
+ constructor(config) {
46
+ super();
47
+ this.name = SolanaMobileWalletAdapterWalletName;
48
+ this.url = 'https://solanamobile.com';
49
+ this.icon = 'data:image/svg+xml;base64,PHN2ZyBmaWxsPSJub25lIiBoZWlnaHQ9IjI4IiB3aWR0aD0iMjgiIHZpZXdCb3g9Ii0zIDAgMjggMjgiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGcgZmlsbD0iI0RDQjhGRiI+PHBhdGggZD0iTTE3LjQgMTcuNEgxNXYyLjRoMi40di0yLjRabTEuMi05LjZoLTIuNHYyLjRoMi40VjcuOFoiLz48cGF0aCBkPSJNMjEuNiAzVjBoLTIuNHYzaC0zLjZWMGgtMi40djNoLTIuNHY2LjZINC41YTIuMSAyLjEgMCAxIDEgMC00LjJoMi43VjNINC41QTQuNSA0LjUgMCAwIDAgMCA3LjVWMjRoMjEuNnYtNi42aC0yLjR2NC4ySDIuNFYxMS41Yy41LjMgMS4yLjQgMS44LjVoNy41QTYuNiA2LjYgMCAwIDAgMjQgOVYzaC0yLjRabTAgNS43YTQuMiA0LjIgMCAxIDEtOC40IDBWNS40aDguNHYzLjNaIi8+PC9nPjwvc3ZnPg==';
50
+ this._connecting = false;
51
+ this._readyState = typeof window === 'undefined' ||
52
+ typeof document === 'undefined' ||
53
+ !/android/i.test(navigator.userAgent) ||
54
+ !window.isSecureContext
55
+ ? walletAdapterBase.WalletReadyState.Unsupported
56
+ : walletAdapterBase.WalletReadyState.Loadable;
57
+ this._authorizationResultCache = config.authorizationResultCache;
58
+ this._appIdentity = config.appIdentity;
59
+ if (this._readyState !== walletAdapterBase.WalletReadyState.Unsupported) {
60
+ this._authorizationResultCache.get().then((authorizationResult) => {
61
+ if (authorizationResult) {
62
+ // Having a prior authorization result is, right now, the best
63
+ // indication that a mobile wallet is installed. There is no API
64
+ // we can use to test for whether the association URI is supported.
65
+ this.emit('readyStateChange', (this._readyState = walletAdapterBase.WalletReadyState.Installed));
66
+ }
67
+ });
68
+ }
69
+ }
70
+ get publicKey() {
71
+ if (this._publicKey == null && this._authorizationResult != null) {
72
+ this._publicKey = new web3_js.PublicKey(this._authorizationResult.publicKey);
73
+ }
74
+ return this._publicKey ? this._publicKey : null;
75
+ }
76
+ get connected() {
77
+ return !!this._authorizationResult;
78
+ }
79
+ get connecting() {
80
+ return this._connecting;
81
+ }
82
+ get readyState() {
83
+ return this._readyState;
84
+ }
85
+ connect() {
86
+ return __awaiter(this, void 0, void 0, function* () {
87
+ if (this._readyState !== walletAdapterBase.WalletReadyState.Installed && this._readyState !== walletAdapterBase.WalletReadyState.Loadable) {
88
+ const err = new walletAdapterBase.WalletNotReadyError();
89
+ this.emit('error', err);
90
+ throw err;
91
+ }
92
+ this._connecting = true;
93
+ const cachedAuthorizationResult = yield this._authorizationResultCache.get();
94
+ if (cachedAuthorizationResult) {
95
+ this._authorizationResult = cachedAuthorizationResult;
96
+ this._connecting = false;
97
+ if (this._readyState !== walletAdapterBase.WalletReadyState.Installed) {
98
+ this.emit('readyStateChange', (this._readyState = walletAdapterBase.WalletReadyState.Installed));
99
+ }
100
+ this.emit('connect',
101
+ // Having just set an `authorizationResult`, `this.publicKey` is definitely non-null
102
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
103
+ this.publicKey);
104
+ return;
105
+ }
106
+ try {
107
+ yield this.withWallet((mobileWallet) => __awaiter(this, void 0, void 0, function* () {
108
+ const { auth_token, pub_key: base58PublicKey, wallet_uri_base, } = yield mobileWallet('authorize', { identity: this._appIdentity });
109
+ try {
110
+ this._publicKey = new web3_js.PublicKey(base58PublicKey);
111
+ }
112
+ catch (e) {
113
+ throw new walletAdapterBase.WalletPublicKeyError((e instanceof Error && (e === null || e === void 0 ? void 0 : e.message)) || 'Unknown error', e);
114
+ }
115
+ this.handleAuthorizationResult({
116
+ authToken: auth_token,
117
+ publicKey: base58PublicKey,
118
+ walletUriBase: wallet_uri_base,
119
+ }); // TODO: Evaluate whether there's any threat to not `awaiting` this expression
120
+ this.emit('connect',
121
+ // Having just set an `authorizationResult`, `this.publicKey` is definitely non-null
122
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
123
+ this.publicKey);
124
+ }));
125
+ }
126
+ catch (e) {
127
+ throw new walletAdapterBase.WalletConnectionError((e instanceof Error && e.message) || 'Unknown error', e);
128
+ }
129
+ finally {
130
+ this._connecting = false;
131
+ }
132
+ });
133
+ }
134
+ handleAuthorizationResult(authorizationResult) {
135
+ return __awaiter(this, void 0, void 0, function* () {
136
+ this._authorizationResult = authorizationResult;
137
+ yield this._authorizationResultCache.set(authorizationResult);
138
+ });
139
+ }
140
+ performReauthorization(mobileWallet, currentAuthorizationResult) {
141
+ return __awaiter(this, void 0, void 0, function* () {
142
+ try {
143
+ const { auth_token } = yield mobileWallet('reauthorize', {
144
+ auth_token: currentAuthorizationResult.authToken,
145
+ });
146
+ if (currentAuthorizationResult.authToken !== auth_token) {
147
+ this.handleAuthorizationResult(Object.assign(Object.assign({}, currentAuthorizationResult), { authToken: auth_token })); // TODO: Evaluate whether there's any threat to not `awaiting` this expression
148
+ }
149
+ return auth_token;
150
+ }
151
+ catch (e) {
152
+ this.disconnect();
153
+ throw new walletAdapterBase.WalletDisconnectedError((e instanceof Error && (e === null || e === void 0 ? void 0 : e.message)) || 'Unknown error', e);
154
+ }
155
+ });
156
+ }
157
+ disconnect() {
158
+ return __awaiter(this, void 0, void 0, function* () {
159
+ this._authorizationResultCache.clear(); // TODO: Evaluate whether there's any threat to not `awaiting` this expression
160
+ delete this._authorizationResult;
161
+ delete this._publicKey;
162
+ this.emit('disconnect');
163
+ });
164
+ }
165
+ withWallet(callback) {
166
+ var _a;
167
+ return __awaiter(this, void 0, void 0, function* () {
168
+ const walletUriBase = (_a = this._authorizationResult) === null || _a === void 0 ? void 0 : _a.walletUriBase;
169
+ const config = walletUriBase ? { baseUri: walletUriBase } : undefined;
170
+ return yield mobileWalletAdapterProtocol.withLocalWallet(callback, config);
171
+ });
172
+ }
173
+ assertIsAuthorized() {
174
+ const authorizationResult = this._authorizationResult;
175
+ if (!authorizationResult)
176
+ throw new walletAdapterBase.WalletNotConnectedError();
177
+ return authorizationResult;
178
+ }
179
+ performSignTransactions(transactions) {
180
+ return __awaiter(this, void 0, void 0, function* () {
181
+ try {
182
+ const authorizationResult = this.assertIsAuthorized();
183
+ try {
184
+ const serializedTransactions = transactions.map((transaction) => transaction.serialize({
185
+ requireAllSignatures: false,
186
+ verifySignatures: false,
187
+ }));
188
+ const payloads = serializedTransactions.map((serializedTransaction) => serializedTransaction.toString('base64'));
189
+ return yield this.withWallet((mobileWallet) => __awaiter(this, void 0, void 0, function* () {
190
+ const freshAuthToken = yield this.performReauthorization(mobileWallet, authorizationResult);
191
+ const { signed_payloads: base64EncodedCompiledTransactions } = yield mobileWallet('sign_transaction', { auth_token: freshAuthToken, payloads });
192
+ const compiledTransactions = base64EncodedCompiledTransactions.map(getByteArrayFromBase64String);
193
+ const transactions = compiledTransactions.map(web3_js.Transaction.from);
194
+ return transactions;
195
+ }));
196
+ }
197
+ catch (error) {
198
+ throw new walletAdapterBase.WalletSignTransactionError(error === null || error === void 0 ? void 0 : error.message, error);
199
+ }
200
+ }
201
+ catch (error) {
202
+ this.emit('error', error);
203
+ throw error;
204
+ }
205
+ });
206
+ }
207
+ sendTransaction(transaction, connection, _options) {
208
+ return __awaiter(this, void 0, void 0, function* () {
209
+ try {
210
+ const authorizationResult = this.assertIsAuthorized();
211
+ try {
212
+ if (transaction.feePayer == null) {
213
+ transaction.feePayer = this.publicKey || undefined;
214
+ }
215
+ if (transaction.recentBlockhash == null) {
216
+ const { blockhash } = yield connection.getRecentBlockhash(connection.commitment);
217
+ transaction.recentBlockhash = blockhash;
218
+ }
219
+ const serializedTransaction = transaction.serialize({
220
+ requireAllSignatures: false,
221
+ verifySignatures: false,
222
+ });
223
+ const payloads = [serializedTransaction.toString('base64')];
224
+ return yield this.withWallet((mobileWallet) => __awaiter(this, void 0, void 0, function* () {
225
+ const freshAuthToken = yield this.performReauthorization(mobileWallet, authorizationResult);
226
+ let targetCommitment;
227
+ switch (connection.commitment) {
228
+ case 'confirmed':
229
+ case 'finalized':
230
+ case 'processed':
231
+ targetCommitment = connection.commitment;
232
+ break;
233
+ default:
234
+ targetCommitment = 'finalized';
235
+ }
236
+ const { signatures } = yield mobileWallet('sign_and_send_transaction', {
237
+ auth_token: freshAuthToken,
238
+ commitment: targetCommitment,
239
+ payloads,
240
+ });
241
+ return signatures[0];
242
+ }));
243
+ }
244
+ catch (error) {
245
+ throw new walletAdapterBase.WalletSendTransactionError(error === null || error === void 0 ? void 0 : error.message, error);
246
+ }
247
+ }
248
+ catch (error) {
249
+ this.emit('error', error);
250
+ throw error;
251
+ }
252
+ });
253
+ }
254
+ signTransaction(transaction) {
255
+ return __awaiter(this, void 0, void 0, function* () {
256
+ const [signedTransaction] = yield this.performSignTransactions([transaction]);
257
+ return signedTransaction;
258
+ });
259
+ }
260
+ signAllTransactions(transactions) {
261
+ return __awaiter(this, void 0, void 0, function* () {
262
+ const signedTransactions = yield this.performSignTransactions(transactions);
263
+ return signedTransactions;
264
+ });
265
+ }
266
+ signMessage(message) {
267
+ return __awaiter(this, void 0, void 0, function* () {
268
+ try {
269
+ const authorizationResult = this.assertIsAuthorized();
270
+ try {
271
+ return yield this.withWallet((mobileWallet) => __awaiter(this, void 0, void 0, function* () {
272
+ const freshAuthToken = yield this.performReauthorization(mobileWallet, authorizationResult);
273
+ const { signed_payloads: [base64EncodedSignedMessage], } = yield mobileWallet('sign_message', {
274
+ auth_token: freshAuthToken,
275
+ payloads: [getBase64StringFromByteArray(message)],
276
+ });
277
+ const signedMessage = getByteArrayFromBase64String(base64EncodedSignedMessage);
278
+ const signature = signedMessage.slice(-SIGNATURE_LENGTH_IN_BYTES);
279
+ return signature;
280
+ }));
281
+ }
282
+ catch (error) {
283
+ throw new walletAdapterBase.WalletSignMessageError(error === null || error === void 0 ? void 0 : error.message, error);
284
+ }
285
+ }
286
+ catch (error) {
287
+ this.emit('error', error);
288
+ throw error;
289
+ }
290
+ });
291
+ }
292
+ }
293
+
294
+ const CACHE_KEY = 'SolanaMobileWalletAdapterDefaultAuthorizationCache';
295
+ function createDefaultAuthorizationResultCache() {
296
+ let storage;
297
+ try {
298
+ storage = window.localStorage;
299
+ // eslint-disable-next-line no-empty
300
+ }
301
+ catch (_a) { }
302
+ return {
303
+ clear() {
304
+ return __awaiter(this, void 0, void 0, function* () {
305
+ if (!storage) {
306
+ return;
307
+ }
308
+ try {
309
+ storage.removeItem(CACHE_KEY);
310
+ // eslint-disable-next-line no-empty
311
+ }
312
+ catch (_a) { }
313
+ });
314
+ },
315
+ get() {
316
+ return __awaiter(this, void 0, void 0, function* () {
317
+ if (!storage) {
318
+ return;
319
+ }
320
+ try {
321
+ return JSON.parse(storage.getItem(CACHE_KEY)) || undefined;
322
+ // eslint-disable-next-line no-empty
323
+ }
324
+ catch (_a) { }
325
+ });
326
+ },
327
+ set(authorizationResult) {
328
+ return __awaiter(this, void 0, void 0, function* () {
329
+ if (!storage) {
330
+ return;
331
+ }
332
+ try {
333
+ storage.setItem(CACHE_KEY, JSON.stringify(authorizationResult));
334
+ // eslint-disable-next-line no-empty
335
+ }
336
+ catch (_a) { }
337
+ });
338
+ },
339
+ };
340
+ }
341
+
342
+ exports.SolanaMobileWalletAdapter = SolanaMobileWalletAdapter;
343
+ exports.SolanaMobileWalletAdapterWalletName = SolanaMobileWalletAdapterWalletName;
344
+ exports.createDefaultAuthorizationResultCache = createDefaultAuthorizationResultCache;