@vue-solana/core 0.4.0 → 0.4.2

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.
@@ -2,15 +2,34 @@
2
2
 
3
3
  const web3Compat = require('@solana/web3-compat');
4
4
  const bs58 = require('bs58');
5
- const nacl = require('tweetnacl');
5
+ const naclModule = require('tweetnacl');
6
6
 
7
7
  function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e.default : e; }
8
8
 
9
+ function _interopNamespaceCompat(e) {
10
+ if (e && typeof e === 'object' && 'default' in e) return e;
11
+ const n = Object.create(null);
12
+ if (e) {
13
+ for (const k in e) {
14
+ n[k] = e[k];
15
+ }
16
+ }
17
+ n.default = e;
18
+ return n;
19
+ }
20
+
9
21
  const bs58__default = /*#__PURE__*/_interopDefaultCompat(bs58);
10
- const nacl__default = /*#__PURE__*/_interopDefaultCompat(nacl);
22
+ const naclModule__namespace = /*#__PURE__*/_interopNamespaceCompat(naclModule);
11
23
 
24
+ const nacl = resolveTweetNaCl();
25
+ function resolveTweetNaCl() {
26
+ const moduleDefault = naclModule__namespace.default;
27
+ const globalNacl = globalThis.nacl;
28
+ return "box" in naclModule__namespace ? naclModule__namespace : moduleDefault ?? globalNacl;
29
+ }
12
30
  const DEFAULT_IOS_WALLET_CHAINS = ["solana:mainnet", "solana:devnet"];
13
31
  const PENDING_REQUEST_KEY = "vue-solana:ios-wallet:pending";
32
+ const PENDING_REQUEST_TTL_MS = 10 * 60 * 1e3;
14
33
  const SESSION_PREFIX = "vue-solana:ios-wallet:session:";
15
34
  const CALLBACK_PARAMS = /* @__PURE__ */ new Set([
16
35
  "data",
@@ -142,6 +161,11 @@ function adaptSolanaIosWallet(walletInfo, options = {}) {
142
161
  transactions,
143
162
  options
144
163
  );
164
+ if (signedTransactions.length !== transactions.length) {
165
+ throw new Error(
166
+ `iOS wallet returned ${signedTransactions.length} signed transactions for ${transactions.length} requested transactions`
167
+ );
168
+ }
145
169
  return signedTransactions.map(
146
170
  (transaction, index) => deserializeTransaction(transactions[index], transaction)
147
171
  );
@@ -169,36 +193,50 @@ function handleSolanaIosWalletCallback(options = {}) {
169
193
  cleanCallbackUrl(url, options.clearUrl);
170
194
  throw new Error("Received an iOS wallet callback without a pending request");
171
195
  }
196
+ if (isPendingRequestExpired(pending)) {
197
+ clearPendingRequest();
198
+ cleanCallbackUrl(url, options.clearUrl);
199
+ throw new Error("Received an expired iOS wallet callback");
200
+ }
172
201
  const definition = getIosWalletDefinitionById(pending.walletId);
173
202
  const walletEncryptionPublicKey = url.searchParams.get(definition.encryptionPublicKeyParam);
174
203
  const nonce = url.searchParams.get("nonce");
175
204
  const data = url.searchParams.get("data");
176
205
  if (!nonce || !data) {
206
+ clearPendingRequest();
207
+ cleanCallbackUrl(url, options.clearUrl);
177
208
  throw new Error("Received an incomplete iOS wallet callback");
178
209
  }
179
- const sharedSecret = getSharedSecret(
180
- walletEncryptionPublicKey ?? getStoredSession(pending.walletId)?.walletEncryptionPublicKey,
181
- pending.dappEncryptionSecretKey
182
- );
183
- const payload = decryptPayload(data, nonce, sharedSecret);
184
- const result = createCallbackResult(pending, payload);
185
- if (pending.method === "connect") {
186
- if (!walletEncryptionPublicKey) {
187
- throw new Error("iOS wallet connect callback did not include an encryption public key");
210
+ try {
211
+ const sharedSecret = getSharedSecret(
212
+ walletEncryptionPublicKey ?? getStoredSession(pending.walletId)?.walletEncryptionPublicKey,
213
+ pending.dappEncryptionSecretKey
214
+ );
215
+ const payload = decryptPayload(data, nonce, sharedSecret);
216
+ const result = createCallbackResult(pending, payload);
217
+ if (pending.method === "connect") {
218
+ if (!walletEncryptionPublicKey) {
219
+ throw new Error("iOS wallet connect callback did not include an encryption public key");
220
+ }
221
+ const publicKey = getPublicKeyField(payload, "public_key");
222
+ storeSession({
223
+ walletId: pending.walletId,
224
+ publicKey,
225
+ session: getStringField(payload, "session"),
226
+ dappEncryptionPublicKey: pending.dappEncryptionPublicKey,
227
+ dappEncryptionSecretKey: pending.dappEncryptionSecretKey,
228
+ walletEncryptionPublicKey,
229
+ sharedSecret: bs58__default.encode(sharedSecret)
230
+ });
188
231
  }
189
- storeSession({
190
- walletId: pending.walletId,
191
- publicKey: getStringField(payload, "public_key"),
192
- session: getStringField(payload, "session"),
193
- dappEncryptionPublicKey: pending.dappEncryptionPublicKey,
194
- dappEncryptionSecretKey: pending.dappEncryptionSecretKey,
195
- walletEncryptionPublicKey,
196
- sharedSecret: bs58__default.encode(sharedSecret)
197
- });
198
- }
199
- clearPendingRequest();
200
- cleanCallbackUrl(url, options.clearUrl);
201
- return result;
232
+ clearPendingRequest();
233
+ cleanCallbackUrl(url, options.clearUrl);
234
+ return result;
235
+ } catch (cause) {
236
+ clearPendingRequest();
237
+ cleanCallbackUrl(url, options.clearUrl);
238
+ throw cause;
239
+ }
202
240
  }
203
241
  function isSolanaIosWalletInfo(walletInfo) {
204
242
  return walletInfo.source === "deep-link" && isIosWalletDefinition(walletInfo.wallet);
@@ -239,7 +277,7 @@ function getDefaultIosWalletRedirectUrl() {
239
277
  return url.toString();
240
278
  }
241
279
  function launchConnect(definition, options) {
242
- const keyPair = nacl__default.box.keyPair();
280
+ const keyPair = nacl.box.keyPair();
243
281
  const pending = createPendingRequest(definition.id, "connect", keyPair, options.redirectUrl);
244
282
  const appIdentity = options.appIdentity ?? getDefaultIosWalletAppIdentity();
245
283
  const url = new URL(definition.connectUrl);
@@ -298,9 +336,10 @@ async function launchEncryptedWalletRequest(definition, method, options, payload
298
336
  publicKey: bs58__default.decode(session.dappEncryptionPublicKey),
299
337
  secretKey: bs58__default.decode(session.dappEncryptionSecretKey)
300
338
  },
301
- options.redirectUrl
339
+ options.redirectUrl,
340
+ method === "signAllTransactions" && Array.isArray(payload.transactions) ? payload.transactions.length : void 0
302
341
  );
303
- const nonce = nacl__default.randomBytes(nacl__default.box.nonceLength);
342
+ const nonce = nacl.randomBytes(nacl.box.nonceLength);
304
343
  const encryptedPayload = encryptPayload(
305
344
  { ...payload, session: session.session },
306
345
  nonce,
@@ -315,7 +354,7 @@ async function launchEncryptedWalletRequest(definition, method, options, payload
315
354
  openIosWalletUrl(url.toString());
316
355
  return waitForRedirect();
317
356
  }
318
- function createPendingRequest(walletId, method, keyPair, redirectUrl) {
357
+ function createPendingRequest(walletId, method, keyPair, redirectUrl, requestedTransactionCount) {
319
358
  return {
320
359
  id: createRequestId(),
321
360
  walletId,
@@ -323,7 +362,8 @@ function createPendingRequest(walletId, method, keyPair, redirectUrl) {
323
362
  dappEncryptionPublicKey: bs58__default.encode(keyPair.publicKey),
324
363
  dappEncryptionSecretKey: bs58__default.encode(keyPair.secretKey),
325
364
  redirectUrl: redirectUrl ?? getDefaultIosWalletRedirectUrl(),
326
- createdAt: Date.now()
365
+ createdAt: Date.now(),
366
+ requestedTransactionCount
327
367
  };
328
368
  }
329
369
  function createCallbackResult(pending, payload) {
@@ -331,7 +371,7 @@ function createCallbackResult(pending, payload) {
331
371
  return {
332
372
  walletId: pending.walletId,
333
373
  method: pending.method,
334
- publicKey: getStringField(payload, "public_key")
374
+ publicKey: getPublicKeyField(payload, "public_key")
335
375
  };
336
376
  }
337
377
  if (pending.method === "signTransaction") {
@@ -346,6 +386,11 @@ function createCallbackResult(pending, payload) {
346
386
  if (!Array.isArray(transactions) || transactions.some((transaction) => typeof transaction !== "string")) {
347
387
  throw new Error("iOS wallet returned invalid signed transactions");
348
388
  }
389
+ if (typeof pending.requestedTransactionCount === "number" && transactions.length !== pending.requestedTransactionCount) {
390
+ throw new Error(
391
+ `iOS wallet returned ${transactions.length} signed transactions for ${pending.requestedTransactionCount} requested transactions`
392
+ );
393
+ }
349
394
  return {
350
395
  walletId: pending.walletId,
351
396
  method: pending.method,
@@ -369,13 +414,13 @@ function getSharedSecret(walletEncryptionPublicKey, dappSecretKey) {
369
414
  if (!walletEncryptionPublicKey) {
370
415
  throw new Error("Missing iOS wallet encryption public key");
371
416
  }
372
- return nacl__default.box.before(bs58__default.decode(walletEncryptionPublicKey), bs58__default.decode(dappSecretKey));
417
+ return nacl.box.before(bs58__default.decode(walletEncryptionPublicKey), bs58__default.decode(dappSecretKey));
373
418
  }
374
419
  function encryptPayload(payload, nonce, sharedSecret) {
375
- return bs58__default.encode(nacl__default.box.after(encodeJson(payload), nonce, sharedSecret));
420
+ return bs58__default.encode(nacl.box.after(encodeJson(payload), nonce, sharedSecret));
376
421
  }
377
422
  function decryptPayload(data, nonce, sharedSecret) {
378
- const decrypted = nacl__default.box.open.after(bs58__default.decode(data), bs58__default.decode(nonce), sharedSecret);
423
+ const decrypted = nacl.box.open.after(bs58__default.decode(data), bs58__default.decode(nonce), sharedSecret);
379
424
  if (!decrypted) {
380
425
  throw new Error("Unable to decrypt iOS wallet callback");
381
426
  }
@@ -415,7 +460,9 @@ function getStoredSession(walletId) {
415
460
  return null;
416
461
  }
417
462
  try {
418
- return JSON.parse(value);
463
+ const session = JSON.parse(value);
464
+ new web3Compat.PublicKey(session.publicKey);
465
+ return session;
419
466
  } catch {
420
467
  removeStoredSession(walletId);
421
468
  return null;
@@ -483,7 +530,7 @@ function cleanCallbackUrl(url, clearUrl) {
483
530
  window.history.replaceState(window.history.state, document.title, url.toString());
484
531
  }
485
532
  function createRequestId() {
486
- return bs58__default.encode(nacl__default.randomBytes(16));
533
+ return bs58__default.encode(nacl.randomBytes(16));
487
534
  }
488
535
  function getStringField(payload, field) {
489
536
  const value = payload[field];
@@ -492,6 +539,18 @@ function getStringField(payload, field) {
492
539
  }
493
540
  return value;
494
541
  }
542
+ function getPublicKeyField(payload, field) {
543
+ const value = getStringField(payload, field);
544
+ try {
545
+ new web3Compat.PublicKey(value);
546
+ } catch {
547
+ throw new Error(`iOS wallet callback returned an invalid ${field}`);
548
+ }
549
+ return value;
550
+ }
551
+ function isPendingRequestExpired(request) {
552
+ return Date.now() - request.createdAt > PENDING_REQUEST_TTL_MS;
553
+ }
495
554
  function getIosWalletDefinition(walletInfo) {
496
555
  if (!isIosWalletDefinition(walletInfo.wallet)) {
497
556
  throw new Error("Solana wallet info is not an iOS wallet");
@@ -1,9 +1,16 @@
1
1
  import { PublicKey, Transaction, VersionedTransaction } from '@solana/web3-compat';
2
2
  import bs58 from 'bs58';
3
- import nacl from 'tweetnacl';
3
+ import * as naclModule from 'tweetnacl';
4
4
 
5
+ const nacl = resolveTweetNaCl();
6
+ function resolveTweetNaCl() {
7
+ const moduleDefault = naclModule.default;
8
+ const globalNacl = globalThis.nacl;
9
+ return "box" in naclModule ? naclModule : moduleDefault ?? globalNacl;
10
+ }
5
11
  const DEFAULT_IOS_WALLET_CHAINS = ["solana:mainnet", "solana:devnet"];
6
12
  const PENDING_REQUEST_KEY = "vue-solana:ios-wallet:pending";
13
+ const PENDING_REQUEST_TTL_MS = 10 * 60 * 1e3;
7
14
  const SESSION_PREFIX = "vue-solana:ios-wallet:session:";
8
15
  const CALLBACK_PARAMS = /* @__PURE__ */ new Set([
9
16
  "data",
@@ -135,6 +142,11 @@ function adaptSolanaIosWallet(walletInfo, options = {}) {
135
142
  transactions,
136
143
  options
137
144
  );
145
+ if (signedTransactions.length !== transactions.length) {
146
+ throw new Error(
147
+ `iOS wallet returned ${signedTransactions.length} signed transactions for ${transactions.length} requested transactions`
148
+ );
149
+ }
138
150
  return signedTransactions.map(
139
151
  (transaction, index) => deserializeTransaction(transactions[index], transaction)
140
152
  );
@@ -162,36 +174,50 @@ function handleSolanaIosWalletCallback(options = {}) {
162
174
  cleanCallbackUrl(url, options.clearUrl);
163
175
  throw new Error("Received an iOS wallet callback without a pending request");
164
176
  }
177
+ if (isPendingRequestExpired(pending)) {
178
+ clearPendingRequest();
179
+ cleanCallbackUrl(url, options.clearUrl);
180
+ throw new Error("Received an expired iOS wallet callback");
181
+ }
165
182
  const definition = getIosWalletDefinitionById(pending.walletId);
166
183
  const walletEncryptionPublicKey = url.searchParams.get(definition.encryptionPublicKeyParam);
167
184
  const nonce = url.searchParams.get("nonce");
168
185
  const data = url.searchParams.get("data");
169
186
  if (!nonce || !data) {
187
+ clearPendingRequest();
188
+ cleanCallbackUrl(url, options.clearUrl);
170
189
  throw new Error("Received an incomplete iOS wallet callback");
171
190
  }
172
- const sharedSecret = getSharedSecret(
173
- walletEncryptionPublicKey ?? getStoredSession(pending.walletId)?.walletEncryptionPublicKey,
174
- pending.dappEncryptionSecretKey
175
- );
176
- const payload = decryptPayload(data, nonce, sharedSecret);
177
- const result = createCallbackResult(pending, payload);
178
- if (pending.method === "connect") {
179
- if (!walletEncryptionPublicKey) {
180
- throw new Error("iOS wallet connect callback did not include an encryption public key");
191
+ try {
192
+ const sharedSecret = getSharedSecret(
193
+ walletEncryptionPublicKey ?? getStoredSession(pending.walletId)?.walletEncryptionPublicKey,
194
+ pending.dappEncryptionSecretKey
195
+ );
196
+ const payload = decryptPayload(data, nonce, sharedSecret);
197
+ const result = createCallbackResult(pending, payload);
198
+ if (pending.method === "connect") {
199
+ if (!walletEncryptionPublicKey) {
200
+ throw new Error("iOS wallet connect callback did not include an encryption public key");
201
+ }
202
+ const publicKey = getPublicKeyField(payload, "public_key");
203
+ storeSession({
204
+ walletId: pending.walletId,
205
+ publicKey,
206
+ session: getStringField(payload, "session"),
207
+ dappEncryptionPublicKey: pending.dappEncryptionPublicKey,
208
+ dappEncryptionSecretKey: pending.dappEncryptionSecretKey,
209
+ walletEncryptionPublicKey,
210
+ sharedSecret: bs58.encode(sharedSecret)
211
+ });
181
212
  }
182
- storeSession({
183
- walletId: pending.walletId,
184
- publicKey: getStringField(payload, "public_key"),
185
- session: getStringField(payload, "session"),
186
- dappEncryptionPublicKey: pending.dappEncryptionPublicKey,
187
- dappEncryptionSecretKey: pending.dappEncryptionSecretKey,
188
- walletEncryptionPublicKey,
189
- sharedSecret: bs58.encode(sharedSecret)
190
- });
191
- }
192
- clearPendingRequest();
193
- cleanCallbackUrl(url, options.clearUrl);
194
- return result;
213
+ clearPendingRequest();
214
+ cleanCallbackUrl(url, options.clearUrl);
215
+ return result;
216
+ } catch (cause) {
217
+ clearPendingRequest();
218
+ cleanCallbackUrl(url, options.clearUrl);
219
+ throw cause;
220
+ }
195
221
  }
196
222
  function isSolanaIosWalletInfo(walletInfo) {
197
223
  return walletInfo.source === "deep-link" && isIosWalletDefinition(walletInfo.wallet);
@@ -291,7 +317,8 @@ async function launchEncryptedWalletRequest(definition, method, options, payload
291
317
  publicKey: bs58.decode(session.dappEncryptionPublicKey),
292
318
  secretKey: bs58.decode(session.dappEncryptionSecretKey)
293
319
  },
294
- options.redirectUrl
320
+ options.redirectUrl,
321
+ method === "signAllTransactions" && Array.isArray(payload.transactions) ? payload.transactions.length : void 0
295
322
  );
296
323
  const nonce = nacl.randomBytes(nacl.box.nonceLength);
297
324
  const encryptedPayload = encryptPayload(
@@ -308,7 +335,7 @@ async function launchEncryptedWalletRequest(definition, method, options, payload
308
335
  openIosWalletUrl(url.toString());
309
336
  return waitForRedirect();
310
337
  }
311
- function createPendingRequest(walletId, method, keyPair, redirectUrl) {
338
+ function createPendingRequest(walletId, method, keyPair, redirectUrl, requestedTransactionCount) {
312
339
  return {
313
340
  id: createRequestId(),
314
341
  walletId,
@@ -316,7 +343,8 @@ function createPendingRequest(walletId, method, keyPair, redirectUrl) {
316
343
  dappEncryptionPublicKey: bs58.encode(keyPair.publicKey),
317
344
  dappEncryptionSecretKey: bs58.encode(keyPair.secretKey),
318
345
  redirectUrl: redirectUrl ?? getDefaultIosWalletRedirectUrl(),
319
- createdAt: Date.now()
346
+ createdAt: Date.now(),
347
+ requestedTransactionCount
320
348
  };
321
349
  }
322
350
  function createCallbackResult(pending, payload) {
@@ -324,7 +352,7 @@ function createCallbackResult(pending, payload) {
324
352
  return {
325
353
  walletId: pending.walletId,
326
354
  method: pending.method,
327
- publicKey: getStringField(payload, "public_key")
355
+ publicKey: getPublicKeyField(payload, "public_key")
328
356
  };
329
357
  }
330
358
  if (pending.method === "signTransaction") {
@@ -339,6 +367,11 @@ function createCallbackResult(pending, payload) {
339
367
  if (!Array.isArray(transactions) || transactions.some((transaction) => typeof transaction !== "string")) {
340
368
  throw new Error("iOS wallet returned invalid signed transactions");
341
369
  }
370
+ if (typeof pending.requestedTransactionCount === "number" && transactions.length !== pending.requestedTransactionCount) {
371
+ throw new Error(
372
+ `iOS wallet returned ${transactions.length} signed transactions for ${pending.requestedTransactionCount} requested transactions`
373
+ );
374
+ }
342
375
  return {
343
376
  walletId: pending.walletId,
344
377
  method: pending.method,
@@ -408,7 +441,9 @@ function getStoredSession(walletId) {
408
441
  return null;
409
442
  }
410
443
  try {
411
- return JSON.parse(value);
444
+ const session = JSON.parse(value);
445
+ new PublicKey(session.publicKey);
446
+ return session;
412
447
  } catch {
413
448
  removeStoredSession(walletId);
414
449
  return null;
@@ -485,6 +520,18 @@ function getStringField(payload, field) {
485
520
  }
486
521
  return value;
487
522
  }
523
+ function getPublicKeyField(payload, field) {
524
+ const value = getStringField(payload, field);
525
+ try {
526
+ new PublicKey(value);
527
+ } catch {
528
+ throw new Error(`iOS wallet callback returned an invalid ${field}`);
529
+ }
530
+ return value;
531
+ }
532
+ function isPendingRequestExpired(request) {
533
+ return Date.now() - request.createdAt > PENDING_REQUEST_TTL_MS;
534
+ }
488
535
  function getIosWalletDefinition(walletInfo) {
489
536
  if (!isIosWalletDefinition(walletInfo.wallet)) {
490
537
  throw new Error("Solana wallet info is not an iOS wallet");
@@ -136,9 +136,17 @@ function adaptSolanaStandardWallet(walletInfo, options = {}) {
136
136
  chain: options.chain
137
137
  }))
138
138
  );
139
- return results.map(
140
- (result, index) => deserializeTransaction(transactions[index], result.signedTransaction)
141
- );
139
+ if (results.length !== transactions.length) {
140
+ throw new Error(
141
+ `Solana wallet returned ${results.length} signed transactions for ${transactions.length} requested transactions`
142
+ );
143
+ }
144
+ return results.map((result, index) => {
145
+ if (!result) {
146
+ throw new Error("Solana wallet did not return a signed transaction");
147
+ }
148
+ return deserializeTransaction(transactions[index], result.signedTransaction);
149
+ });
142
150
  } : void 0,
143
151
  signAndSendTransaction: hasSignAndSendTransaction(wallet) ? async (transaction, sendOptions) => {
144
152
  const activeAccount = getActiveAccount(account);
@@ -130,9 +130,17 @@ function adaptSolanaStandardWallet(walletInfo, options = {}) {
130
130
  chain: options.chain
131
131
  }))
132
132
  );
133
- return results.map(
134
- (result, index) => deserializeTransaction(transactions[index], result.signedTransaction)
135
- );
133
+ if (results.length !== transactions.length) {
134
+ throw new Error(
135
+ `Solana wallet returned ${results.length} signed transactions for ${transactions.length} requested transactions`
136
+ );
137
+ }
138
+ return results.map((result, index) => {
139
+ if (!result) {
140
+ throw new Error("Solana wallet did not return a signed transaction");
141
+ }
142
+ return deserializeTransaction(transactions[index], result.signedTransaction);
143
+ });
136
144
  } : void 0,
137
145
  signAndSendTransaction: hasSignAndSendTransaction(wallet) ? async (transaction, sendOptions) => {
138
146
  const activeAccount = getActiveAccount(account);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vue-solana/core",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "Framework-agnostic primitives for Vue Solana libraries.",
5
5
  "license": "MIT",
6
6
  "keywords": [