@solana/web3.js 1.41.1 → 1.41.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.
package/lib/index.cjs.js CHANGED
@@ -4244,6 +4244,82 @@ class ComputeBudgetProgram {
4244
4244
  }
4245
4245
  ComputeBudgetProgram.programId = new PublicKey('ComputeBudget111111111111111111111111111111');
4246
4246
 
4247
+ var objToString = Object.prototype.toString;
4248
+ var objKeys = Object.keys || function(obj) {
4249
+ var keys = [];
4250
+ for (var name in obj) {
4251
+ keys.push(name);
4252
+ }
4253
+ return keys;
4254
+ };
4255
+
4256
+ function stringify(val, isArrayProp) {
4257
+ var i, max, str, keys, key, propVal, toStr;
4258
+ if (val === true) {
4259
+ return "true";
4260
+ }
4261
+ if (val === false) {
4262
+ return "false";
4263
+ }
4264
+ switch (typeof val) {
4265
+ case "object":
4266
+ if (val === null) {
4267
+ return null;
4268
+ } else if (val.toJSON && typeof val.toJSON === "function") {
4269
+ return stringify(val.toJSON(), isArrayProp);
4270
+ } else {
4271
+ toStr = objToString.call(val);
4272
+ if (toStr === "[object Array]") {
4273
+ str = '[';
4274
+ max = val.length - 1;
4275
+ for(i = 0; i < max; i++) {
4276
+ str += stringify(val[i], true) + ',';
4277
+ }
4278
+ if (max > -1) {
4279
+ str += stringify(val[i], true);
4280
+ }
4281
+ return str + ']';
4282
+ } else if (toStr === "[object Object]") {
4283
+ // only object is left
4284
+ keys = objKeys(val).sort();
4285
+ max = keys.length;
4286
+ str = "";
4287
+ i = 0;
4288
+ while (i < max) {
4289
+ key = keys[i];
4290
+ propVal = stringify(val[key], false);
4291
+ if (propVal !== undefined) {
4292
+ if (str) {
4293
+ str += ',';
4294
+ }
4295
+ str += JSON.stringify(key) + ':' + propVal;
4296
+ }
4297
+ i++;
4298
+ }
4299
+ return '{' + str + '}';
4300
+ } else {
4301
+ return JSON.stringify(val);
4302
+ }
4303
+ }
4304
+ case "function":
4305
+ case "undefined":
4306
+ return isArrayProp ? null : undefined;
4307
+ case "string":
4308
+ return JSON.stringify(val);
4309
+ default:
4310
+ return isFinite(val) ? val : null;
4311
+ }
4312
+ }
4313
+
4314
+ var fastStableStringify = function(val) {
4315
+ var returnVal = stringify(val, false);
4316
+ if (returnVal !== undefined) {
4317
+ return ''+ returnVal;
4318
+ }
4319
+ };
4320
+
4321
+ var fastStableStringify$1 = fastStableStringify;
4322
+
4247
4323
  const DESTROY_TIMEOUT_MS = 5000;
4248
4324
  class AgentManager {
4249
4325
  static _newAgent(useHttps) {
@@ -4459,6 +4535,12 @@ const BufferFromRawAccountData = superstruct.coerce(superstruct.instance(buffer.
4459
4535
  */
4460
4536
 
4461
4537
  const BLOCKHASH_CACHE_TIMEOUT_MS = 30 * 1000;
4538
+ /**
4539
+ * HACK.
4540
+ * Copied from rpc-websockets/dist/lib/client.
4541
+ * Otherwise, `yarn build` fails with:
4542
+ * https://gist.github.com/steveluscher/c057eca81d479ef705cdb53162f9971d
4543
+ */
4462
4544
 
4463
4545
  /**
4464
4546
  * @internal
@@ -5333,14 +5415,9 @@ const LogsNotificationResult = superstruct.type({
5333
5415
  * Filter for log subscriptions.
5334
5416
  */
5335
5417
 
5336
- function createSubscriptionWarningMessage(id, label) {
5337
- return 'Ignored unsubscribe request because an active subscription ' + `with id \`${id}\` for '${label}' events could not be found.`;
5338
- }
5339
5418
  /**
5340
5419
  * A connection to a fullnode JSON RPC endpoint
5341
5420
  */
5342
-
5343
-
5344
5421
  class Connection {
5345
5422
  /** @internal */
5346
5423
 
@@ -5364,21 +5441,13 @@ class Connection {
5364
5441
 
5365
5442
  /** @internal */
5366
5443
 
5367
- /** @internal */
5368
-
5369
- /** @internal */
5370
-
5371
- /** @internal */
5372
-
5373
- /** @internal */
5374
-
5375
- /** @internal */
5376
-
5377
- /** @internal */
5378
-
5379
- /** @internal */
5380
-
5381
- /** @internal */
5444
+ /** @internal
5445
+ * A number that we increment every time an active connection closes.
5446
+ * Used to determine whether the same socket connection that was open
5447
+ * when an async operation started is the same one that's active when
5448
+ * its continuation fires.
5449
+ *
5450
+ */
5382
5451
 
5383
5452
  /** @internal */
5384
5453
 
@@ -5394,7 +5463,19 @@ class Connection {
5394
5463
 
5395
5464
  /** @internal */
5396
5465
 
5397
- /** @internal */
5466
+ /**
5467
+ * Special case.
5468
+ * After a signature is processed, RPCs automatically dispose of the
5469
+ * subscription on the server side. We need to track which of these
5470
+ * subscriptions have been disposed in such a way, so that we know
5471
+ * whether the client is dealing with a not-yet-processed signature
5472
+ * (in which case we must tear down the server subscription) or an
5473
+ * already-processed signature (in which case the client can simply
5474
+ * clear out the subscription locally without telling the server).
5475
+ *
5476
+ * NOTE: There is a proposal to eliminate this special case, here:
5477
+ * https://github.com/solana-labs/solana/issues/18892
5478
+ */
5398
5479
 
5399
5480
  /** @internal */
5400
5481
 
@@ -5416,6 +5497,7 @@ class Connection {
5416
5497
  this._rpcWebSocketConnected = false;
5417
5498
  this._rpcWebSocketHeartbeat = null;
5418
5499
  this._rpcWebSocketIdleTimeout = null;
5500
+ this._rpcWebSocketGeneration = 0;
5419
5501
  this._disableBlockhashCaching = false;
5420
5502
  this._pollingBlockhash = false;
5421
5503
  this._blockhashInfo = {
@@ -5424,20 +5506,11 @@ class Connection {
5424
5506
  transactionSignatures: [],
5425
5507
  simulatedSignatures: []
5426
5508
  };
5427
- this._accountChangeSubscriptionCounter = 0;
5428
- this._accountChangeSubscriptions = {};
5429
- this._programAccountChangeSubscriptionCounter = 0;
5430
- this._programAccountChangeSubscriptions = {};
5431
- this._rootSubscriptionCounter = 0;
5432
- this._rootSubscriptions = {};
5433
- this._signatureSubscriptionCounter = 0;
5434
- this._signatureSubscriptions = {};
5435
- this._slotSubscriptionCounter = 0;
5436
- this._slotSubscriptions = {};
5437
- this._logsSubscriptionCounter = 0;
5438
- this._logsSubscriptions = {};
5439
- this._slotUpdateSubscriptionCounter = 0;
5440
- this._slotUpdateSubscriptions = {};
5509
+ this._nextClientSubscriptionId = 0;
5510
+ this._subscriptionDisposeFunctionsByClientSubscriptionId = {};
5511
+ this._subscriptionCallbacksByServerSubscriptionId = {};
5512
+ this._subscriptionsByHash = {};
5513
+ this._subscriptionsAutoDisposedByRpc = new Set();
5441
5514
  let url = new URL(endpoint);
5442
5515
  const useHttps = url.protocol === 'https:';
5443
5516
  let wsEndpoint;
@@ -7205,6 +7278,8 @@ class Connection {
7205
7278
 
7206
7279
 
7207
7280
  _wsOnClose(code) {
7281
+ this._rpcWebSocketGeneration++;
7282
+
7208
7283
  if (this._rpcWebSocketHeartbeat) {
7209
7284
  clearInterval(this._rpcWebSocketHeartbeat);
7210
7285
  this._rpcWebSocketHeartbeat = null;
@@ -7218,85 +7293,20 @@ class Connection {
7218
7293
  } // implicit close, prepare subscriptions for auto-reconnect
7219
7294
 
7220
7295
 
7221
- this._resetSubscriptions();
7222
- }
7223
- /**
7224
- * @internal
7225
- */
7226
-
7227
-
7228
- async _subscribe(sub, rpcMethod, rpcArgs) {
7229
- if (sub.subscriptionId == null) {
7230
- sub.subscriptionId = 'subscribing';
7231
-
7232
- try {
7233
- const id = await this._rpcWebSocket.call(rpcMethod, rpcArgs);
7234
-
7235
- if (typeof id === 'number' && sub.subscriptionId === 'subscribing') {
7236
- // eslint-disable-next-line require-atomic-updates
7237
- sub.subscriptionId = id;
7238
- }
7239
- } catch (err) {
7240
- if (sub.subscriptionId === 'subscribing') {
7241
- // eslint-disable-next-line require-atomic-updates
7242
- sub.subscriptionId = null;
7243
- }
7244
-
7245
- if (err instanceof Error) {
7246
- console.error(`${rpcMethod} error for argument`, rpcArgs, err.message);
7247
- }
7248
- }
7249
- }
7250
- }
7251
- /**
7252
- * @internal
7253
- */
7254
-
7255
-
7256
- async _unsubscribe(sub, rpcMethod) {
7257
- const subscriptionId = sub.subscriptionId;
7258
-
7259
- if (subscriptionId != null && typeof subscriptionId != 'string') {
7260
- const unsubscribeId = subscriptionId;
7261
-
7262
- try {
7263
- await this._rpcWebSocket.call(rpcMethod, [unsubscribeId]);
7264
- } catch (err) {
7265
- if (err instanceof Error) {
7266
- console.error(`${rpcMethod} error:`, err.message);
7267
- }
7268
- }
7269
- }
7270
- }
7271
- /**
7272
- * @internal
7273
- */
7274
-
7275
-
7276
- _resetSubscriptions() {
7277
- Object.values(this._accountChangeSubscriptions).forEach(s => s.subscriptionId = null);
7278
- Object.values(this._logsSubscriptions).forEach(s => s.subscriptionId = null);
7279
- Object.values(this._programAccountChangeSubscriptions).forEach(s => s.subscriptionId = null);
7280
- Object.values(this._rootSubscriptions).forEach(s => s.subscriptionId = null);
7281
- Object.values(this._signatureSubscriptions).forEach(s => s.subscriptionId = null);
7282
- Object.values(this._slotSubscriptions).forEach(s => s.subscriptionId = null);
7283
- Object.values(this._slotUpdateSubscriptions).forEach(s => s.subscriptionId = null);
7296
+ this._subscriptionCallbacksByServerSubscriptionId = {};
7297
+ Object.entries(this._subscriptionsByHash).forEach(([hash, subscription]) => {
7298
+ this._subscriptionsByHash[hash] = { ...subscription,
7299
+ state: 'pending'
7300
+ };
7301
+ });
7284
7302
  }
7285
7303
  /**
7286
7304
  * @internal
7287
7305
  */
7288
7306
 
7289
7307
 
7290
- _updateSubscriptions() {
7291
- const accountKeys = Object.keys(this._accountChangeSubscriptions).map(Number);
7292
- const programKeys = Object.keys(this._programAccountChangeSubscriptions).map(Number);
7293
- const slotKeys = Object.keys(this._slotSubscriptions).map(Number);
7294
- const slotUpdateKeys = Object.keys(this._slotUpdateSubscriptions).map(Number);
7295
- const signatureKeys = Object.keys(this._signatureSubscriptions).map(Number);
7296
- const rootKeys = Object.keys(this._rootSubscriptions).map(Number);
7297
- const logsKeys = Object.keys(this._logsSubscriptions).map(Number);
7298
-
7299
- if (accountKeys.length === 0 && programKeys.length === 0 && slotKeys.length === 0 && slotUpdateKeys.length === 0 && signatureKeys.length === 0 && rootKeys.length === 0 && logsKeys.length === 0) {
7308
+ async _updateSubscriptions() {
7309
+ if (Object.keys(this._subscriptionsByHash).length === 0) {
7300
7310
  if (this._rpcWebSocketConnected) {
7301
7311
  this._rpcWebSocketConnected = false;
7302
7312
  this._rpcWebSocketIdleTimeout = setTimeout(() => {
@@ -7328,60 +7338,167 @@ class Connection {
7328
7338
  return;
7329
7339
  }
7330
7340
 
7331
- for (let id of accountKeys) {
7332
- const sub = this._accountChangeSubscriptions[id];
7333
-
7334
- this._subscribe(sub, 'accountSubscribe', this._buildArgs([sub.publicKey], sub.commitment, 'base64'));
7335
- }
7341
+ const activeWebSocketGeneration = this._rpcWebSocketGeneration;
7336
7342
 
7337
- for (let id of programKeys) {
7338
- const sub = this._programAccountChangeSubscriptions[id];
7343
+ const isCurrentConnectionStillActive = () => {
7344
+ return activeWebSocketGeneration === this._rpcWebSocketGeneration;
7345
+ };
7339
7346
 
7340
- this._subscribe(sub, 'programSubscribe', this._buildArgs([sub.programId], sub.commitment, 'base64', {
7341
- filters: sub.filters
7342
- }));
7343
- }
7347
+ await Promise.all( // Don't be tempted to change this to `Object.entries`. We call
7348
+ // `_updateSubscriptions` recursively when processing the state,
7349
+ // so it's important that we look up the *current* version of
7350
+ // each subscription, every time we process a hash.
7351
+ Object.keys(this._subscriptionsByHash).map(async hash => {
7352
+ const subscription = this._subscriptionsByHash[hash];
7344
7353
 
7345
- for (let id of slotKeys) {
7346
- const sub = this._slotSubscriptions[id];
7354
+ if (subscription === undefined) {
7355
+ // This entry has since been deleted. Skip.
7356
+ return;
7357
+ }
7347
7358
 
7348
- this._subscribe(sub, 'slotSubscribe', []);
7349
- }
7359
+ switch (subscription.state) {
7360
+ case 'pending':
7361
+ case 'unsubscribed':
7362
+ if (subscription.callbacks.size === 0) {
7363
+ /**
7364
+ * You can end up here when:
7365
+ *
7366
+ * - a subscription has recently unsubscribed
7367
+ * without having new callbacks added to it
7368
+ * while the unsubscribe was in flight, or
7369
+ * - when a pending subscription has its
7370
+ * listeners removed before a request was
7371
+ * sent to the server.
7372
+ *
7373
+ * Being that nobody is interested in this
7374
+ * subscription any longer, delete it.
7375
+ */
7376
+ delete this._subscriptionsByHash[hash];
7377
+
7378
+ if (subscription.state === 'unsubscribed') {
7379
+ delete this._subscriptionCallbacksByServerSubscriptionId[subscription.serverSubscriptionId];
7380
+ }
7350
7381
 
7351
- for (let id of slotUpdateKeys) {
7352
- const sub = this._slotUpdateSubscriptions[id];
7382
+ await this._updateSubscriptions();
7383
+ return;
7384
+ }
7353
7385
 
7354
- this._subscribe(sub, 'slotsUpdatesSubscribe', []);
7355
- }
7386
+ await (async () => {
7387
+ const {
7388
+ args,
7389
+ method
7390
+ } = subscription;
7356
7391
 
7357
- for (let id of signatureKeys) {
7358
- const sub = this._signatureSubscriptions[id];
7359
- const args = [sub.signature];
7360
- if (sub.options) args.push(sub.options);
7392
+ try {
7393
+ this._subscriptionsByHash[hash] = { ...subscription,
7394
+ state: 'subscribing'
7395
+ };
7396
+ const serverSubscriptionId = await this._rpcWebSocket.call(method, args);
7397
+ this._subscriptionsByHash[hash] = { ...subscription,
7398
+ serverSubscriptionId,
7399
+ state: 'subscribed'
7400
+ };
7401
+ this._subscriptionCallbacksByServerSubscriptionId[serverSubscriptionId] = subscription.callbacks;
7402
+ await this._updateSubscriptions();
7403
+ } catch (e) {
7404
+ if (e instanceof Error) {
7405
+ console.error(`${method} error for argument`, args, e.message);
7406
+ }
7407
+
7408
+ if (!isCurrentConnectionStillActive()) {
7409
+ return;
7410
+ } // TODO: Maybe add an 'errored' state or a retry limit?
7361
7411
 
7362
- this._subscribe(sub, 'signatureSubscribe', args);
7363
- }
7364
7412
 
7365
- for (let id of rootKeys) {
7366
- const sub = this._rootSubscriptions[id];
7413
+ this._subscriptionsByHash[hash] = { ...subscription,
7414
+ state: 'pending'
7415
+ };
7416
+ await this._updateSubscriptions();
7417
+ }
7418
+ })();
7419
+ break;
7367
7420
 
7368
- this._subscribe(sub, 'rootSubscribe', []);
7369
- }
7421
+ case 'subscribed':
7422
+ if (subscription.callbacks.size === 0) {
7423
+ // By the time we successfully set up a subscription
7424
+ // with the server, the client stopped caring about it.
7425
+ // Tear it down now.
7426
+ await (async () => {
7427
+ const {
7428
+ serverSubscriptionId,
7429
+ unsubscribeMethod
7430
+ } = subscription;
7431
+
7432
+ if (this._subscriptionsAutoDisposedByRpc.has(serverSubscriptionId)) {
7433
+ /**
7434
+ * Special case.
7435
+ * If we're dealing with a subscription that has been auto-
7436
+ * disposed by the RPC, then we can skip the RPC call to
7437
+ * tear down the subscription here.
7438
+ *
7439
+ * NOTE: There is a proposal to eliminate this special case, here:
7440
+ * https://github.com/solana-labs/solana/issues/18892
7441
+ */
7442
+ this._subscriptionsAutoDisposedByRpc.delete(serverSubscriptionId);
7443
+ } else {
7444
+ this._subscriptionsByHash[hash] = { ...subscription,
7445
+ state: 'unsubscribing'
7446
+ };
7447
+
7448
+ try {
7449
+ await this._rpcWebSocket.call(unsubscribeMethod, [serverSubscriptionId]);
7450
+ } catch (e) {
7451
+ if (e instanceof Error) {
7452
+ console.error(`${unsubscribeMethod} error:`, e.message);
7453
+ }
7454
+
7455
+ if (!isCurrentConnectionStillActive()) {
7456
+ return;
7457
+ } // TODO: Maybe add an 'errored' state or a retry limit?
7458
+
7459
+
7460
+ this._subscriptionsByHash[hash] = { ...subscription,
7461
+ state: 'subscribed'
7462
+ };
7463
+ await this._updateSubscriptions();
7464
+ return;
7465
+ }
7466
+ }
7370
7467
 
7371
- for (let id of logsKeys) {
7372
- const sub = this._logsSubscriptions[id];
7373
- let filter;
7468
+ this._subscriptionsByHash[hash] = { ...subscription,
7469
+ state: 'unsubscribed'
7470
+ };
7471
+ await this._updateSubscriptions();
7472
+ })();
7473
+ }
7374
7474
 
7375
- if (typeof sub.filter === 'object') {
7376
- filter = {
7377
- mentions: [sub.filter.toString()]
7378
- };
7379
- } else {
7380
- filter = sub.filter;
7475
+ break;
7381
7476
  }
7477
+ }));
7478
+ }
7479
+ /**
7480
+ * @internal
7481
+ */
7382
7482
 
7383
- this._subscribe(sub, 'logsSubscribe', this._buildArgs([filter], sub.commitment));
7483
+
7484
+ _handleServerNotification(serverSubscriptionId, callbackArgs) {
7485
+ const callbacks = this._subscriptionCallbacksByServerSubscriptionId[serverSubscriptionId];
7486
+
7487
+ if (callbacks === undefined) {
7488
+ return;
7384
7489
  }
7490
+
7491
+ callbacks.forEach(cb => {
7492
+ try {
7493
+ cb( // I failed to find a way to convince TypeScript that `cb` is of type
7494
+ // `TCallback` which is certainly compatible with `Parameters<TCallback>`.
7495
+ // See https://github.com/microsoft/TypeScript/issues/47615
7496
+ // @ts-ignore
7497
+ ...callbackArgs);
7498
+ } catch (e) {
7499
+ console.error(e);
7500
+ }
7501
+ });
7385
7502
  }
7386
7503
  /**
7387
7504
  * @internal
@@ -7389,14 +7506,71 @@ class Connection {
7389
7506
 
7390
7507
 
7391
7508
  _wsOnAccountNotification(notification) {
7392
- const res = superstruct.create(notification, AccountNotificationResult);
7509
+ const {
7510
+ result,
7511
+ subscription
7512
+ } = superstruct.create(notification, AccountNotificationResult);
7393
7513
 
7394
- for (const sub of Object.values(this._accountChangeSubscriptions)) {
7395
- if (sub.subscriptionId === res.subscription) {
7396
- sub.callback(res.result.value, res.result.context);
7397
- return;
7398
- }
7514
+ this._handleServerNotification(subscription, [result.value, result.context]);
7515
+ }
7516
+ /**
7517
+ * @internal
7518
+ */
7519
+
7520
+
7521
+ _makeSubscription(subscriptionConfig,
7522
+ /**
7523
+ * When preparing `args` for a call to `_makeSubscription`, be sure
7524
+ * to carefully apply a default `commitment` property, if necessary.
7525
+ *
7526
+ * - If the user supplied a `commitment` use that.
7527
+ * - Otherwise, if the `Connection::commitment` is set, use that.
7528
+ * - Otherwise, set it to the RPC server default: `finalized`.
7529
+ *
7530
+ * This is extremely important to ensure that these two fundamentally
7531
+ * identical subscriptions produce the same identifying hash:
7532
+ *
7533
+ * - A subscription made without specifying a commitment.
7534
+ * - A subscription made where the commitment specified is the same
7535
+ * as the default applied to the subscription above.
7536
+ *
7537
+ * Example; these two subscriptions must produce the same hash:
7538
+ *
7539
+ * - An `accountSubscribe` subscription for `'PUBKEY'`
7540
+ * - An `accountSubscribe` subscription for `'PUBKEY'` with commitment
7541
+ * `'finalized'`.
7542
+ *
7543
+ * See the 'making a subscription with defaulted params omitted' test
7544
+ * in `connection-subscriptions.ts` for more.
7545
+ */
7546
+ args) {
7547
+ const clientSubscriptionId = this._nextClientSubscriptionId++;
7548
+ const hash = fastStableStringify$1([subscriptionConfig.method, args], true
7549
+ /* isArrayProp */
7550
+ );
7551
+ const existingSubscription = this._subscriptionsByHash[hash];
7552
+
7553
+ if (existingSubscription === undefined) {
7554
+ this._subscriptionsByHash[hash] = { ...subscriptionConfig,
7555
+ args,
7556
+ callbacks: new Set([subscriptionConfig.callback]),
7557
+ state: 'pending'
7558
+ };
7559
+ } else {
7560
+ existingSubscription.callbacks.add(subscriptionConfig.callback);
7399
7561
  }
7562
+
7563
+ this._subscriptionDisposeFunctionsByClientSubscriptionId[clientSubscriptionId] = async () => {
7564
+ delete this._subscriptionDisposeFunctionsByClientSubscriptionId[clientSubscriptionId];
7565
+ const subscription = this._subscriptionsByHash[hash];
7566
+ assert(subscription !== undefined, `Could not find a \`Subscription\` when tearing down client subscription #${clientSubscriptionId}`);
7567
+ subscription.callbacks.delete(subscriptionConfig.callback);
7568
+ await this._updateSubscriptions();
7569
+ };
7570
+
7571
+ this._updateSubscriptions();
7572
+
7573
+ return clientSubscriptionId;
7400
7574
  }
7401
7575
  /**
7402
7576
  * Register a callback to be invoked whenever the specified account changes
@@ -7409,35 +7583,24 @@ class Connection {
7409
7583
 
7410
7584
 
7411
7585
  onAccountChange(publicKey, callback, commitment) {
7412
- const id = ++this._accountChangeSubscriptionCounter;
7413
- this._accountChangeSubscriptions[id] = {
7414
- publicKey: publicKey.toBase58(),
7415
- callback,
7416
- commitment,
7417
- subscriptionId: null
7418
- };
7586
+ const args = this._buildArgs([publicKey.toBase58()], commitment || this._commitment || 'finalized', // Apply connection/server default.
7587
+ 'base64');
7419
7588
 
7420
- this._updateSubscriptions();
7421
-
7422
- return id;
7589
+ return this._makeSubscription({
7590
+ callback,
7591
+ method: 'accountSubscribe',
7592
+ unsubscribeMethod: 'accountUnsubscribe'
7593
+ }, args);
7423
7594
  }
7424
7595
  /**
7425
7596
  * Deregister an account notification callback
7426
7597
  *
7427
- * @param id subscription id to deregister
7598
+ * @param id client subscription id to deregister
7428
7599
  */
7429
7600
 
7430
7601
 
7431
- async removeAccountChangeListener(id) {
7432
- if (this._accountChangeSubscriptions[id]) {
7433
- const subInfo = this._accountChangeSubscriptions[id];
7434
- delete this._accountChangeSubscriptions[id];
7435
- await this._unsubscribe(subInfo, 'accountUnsubscribe');
7436
-
7437
- this._updateSubscriptions();
7438
- } else {
7439
- console.warn(createSubscriptionWarningMessage(id, 'account change'));
7440
- }
7602
+ async removeAccountChangeListener(clientSubscriptionId) {
7603
+ await this._unsubscribeClientSubscription(clientSubscriptionId, 'account change');
7441
7604
  }
7442
7605
  /**
7443
7606
  * @internal
@@ -7445,21 +7608,15 @@ class Connection {
7445
7608
 
7446
7609
 
7447
7610
  _wsOnProgramAccountNotification(notification) {
7448
- const res = superstruct.create(notification, ProgramAccountNotificationResult);
7611
+ const {
7612
+ result,
7613
+ subscription
7614
+ } = superstruct.create(notification, ProgramAccountNotificationResult);
7449
7615
 
7450
- for (const sub of Object.values(this._programAccountChangeSubscriptions)) {
7451
- if (sub.subscriptionId === res.subscription) {
7452
- const {
7453
- value,
7454
- context
7455
- } = res.result;
7456
- sub.callback({
7457
- accountId: value.pubkey,
7458
- accountInfo: value.account
7459
- }, context);
7460
- return;
7461
- }
7462
- }
7616
+ this._handleServerNotification(subscription, [{
7617
+ accountId: result.value.pubkey,
7618
+ accountInfo: result.value.account
7619
+ }, result.context]);
7463
7620
  }
7464
7621
  /**
7465
7622
  * Register a callback to be invoked whenever accounts owned by the
@@ -7474,36 +7631,30 @@ class Connection {
7474
7631
 
7475
7632
 
7476
7633
  onProgramAccountChange(programId, callback, commitment, filters) {
7477
- const id = ++this._programAccountChangeSubscriptionCounter;
7478
- this._programAccountChangeSubscriptions[id] = {
7479
- programId: programId.toBase58(),
7634
+ const args = this._buildArgs([programId.toBase58()], commitment || this._commitment || 'finalized', // Apply connection/server default.
7635
+ 'base64'
7636
+ /* encoding */
7637
+ , filters ? {
7638
+ filters: filters
7639
+ } : undefined
7640
+ /* extra */
7641
+ );
7642
+
7643
+ return this._makeSubscription({
7480
7644
  callback,
7481
- commitment,
7482
- subscriptionId: null,
7483
- filters
7484
- };
7485
-
7486
- this._updateSubscriptions();
7487
-
7488
- return id;
7645
+ method: 'programSubscribe',
7646
+ unsubscribeMethod: 'programUnsubscribe'
7647
+ }, args);
7489
7648
  }
7490
7649
  /**
7491
7650
  * Deregister an account notification callback
7492
7651
  *
7493
- * @param id subscription id to deregister
7652
+ * @param id client subscription id to deregister
7494
7653
  */
7495
7654
 
7496
7655
 
7497
- async removeProgramAccountChangeListener(id) {
7498
- if (this._programAccountChangeSubscriptions[id]) {
7499
- const subInfo = this._programAccountChangeSubscriptions[id];
7500
- delete this._programAccountChangeSubscriptions[id];
7501
- await this._unsubscribe(subInfo, 'programUnsubscribe');
7502
-
7503
- this._updateSubscriptions();
7504
- } else {
7505
- console.warn(createSubscriptionWarningMessage(id, 'program account change'));
7506
- }
7656
+ async removeProgramAccountChangeListener(clientSubscriptionId) {
7657
+ await this._unsubscribeClientSubscription(clientSubscriptionId, 'program account change');
7507
7658
  }
7508
7659
  /**
7509
7660
  * Registers a callback to be invoked whenever logs are emitted.
@@ -7511,35 +7662,26 @@ class Connection {
7511
7662
 
7512
7663
 
7513
7664
  onLogs(filter, callback, commitment) {
7514
- const id = ++this._logsSubscriptionCounter;
7515
- this._logsSubscriptions[id] = {
7516
- filter,
7517
- callback,
7518
- commitment,
7519
- subscriptionId: null
7520
- };
7521
-
7522
- this._updateSubscriptions();
7665
+ const args = this._buildArgs([typeof filter === 'object' ? {
7666
+ mentions: [filter.toString()]
7667
+ } : filter], commitment || this._commitment || 'finalized' // Apply connection/server default.
7668
+ );
7523
7669
 
7524
- return id;
7670
+ return this._makeSubscription({
7671
+ callback,
7672
+ method: 'logsSubscribe',
7673
+ unsubscribeMethod: 'logsUnsubscribe'
7674
+ }, args);
7525
7675
  }
7526
7676
  /**
7527
7677
  * Deregister a logs callback.
7528
7678
  *
7529
- * @param id subscription id to deregister.
7679
+ * @param id client subscription id to deregister.
7530
7680
  */
7531
7681
 
7532
7682
 
7533
- async removeOnLogsListener(id) {
7534
- if (this._logsSubscriptions[id]) {
7535
- const subInfo = this._logsSubscriptions[id];
7536
- delete this._logsSubscriptions[id];
7537
- await this._unsubscribe(subInfo, 'logsUnsubscribe');
7538
-
7539
- this._updateSubscriptions();
7540
- } else {
7541
- console.warn(createSubscriptionWarningMessage(id, 'logs'));
7542
- }
7683
+ async removeOnLogsListener(clientSubscriptionId) {
7684
+ await this._unsubscribeClientSubscription(clientSubscriptionId, 'logs');
7543
7685
  }
7544
7686
  /**
7545
7687
  * @internal
@@ -7547,17 +7689,12 @@ class Connection {
7547
7689
 
7548
7690
 
7549
7691
  _wsOnLogsNotification(notification) {
7550
- const res = superstruct.create(notification, LogsNotificationResult);
7551
- const keys = Object.keys(this._logsSubscriptions).map(Number);
7552
-
7553
- for (let id of keys) {
7554
- const sub = this._logsSubscriptions[id];
7692
+ const {
7693
+ result,
7694
+ subscription
7695
+ } = superstruct.create(notification, LogsNotificationResult);
7555
7696
 
7556
- if (sub.subscriptionId === res.subscription) {
7557
- sub.callback(res.result.value, res.result.context);
7558
- return;
7559
- }
7560
- }
7697
+ this._handleServerNotification(subscription, [result.value, result.context]);
7561
7698
  }
7562
7699
  /**
7563
7700
  * @internal
@@ -7565,14 +7702,12 @@ class Connection {
7565
7702
 
7566
7703
 
7567
7704
  _wsOnSlotNotification(notification) {
7568
- const res = superstruct.create(notification, SlotNotificationResult);
7705
+ const {
7706
+ result,
7707
+ subscription
7708
+ } = superstruct.create(notification, SlotNotificationResult);
7569
7709
 
7570
- for (const sub of Object.values(this._slotSubscriptions)) {
7571
- if (sub.subscriptionId === res.subscription) {
7572
- sub.callback(res.result);
7573
- return;
7574
- }
7575
- }
7710
+ this._handleServerNotification(subscription, [result]);
7576
7711
  }
7577
7712
  /**
7578
7713
  * Register a callback to be invoked upon slot changes
@@ -7583,33 +7718,23 @@ class Connection {
7583
7718
 
7584
7719
 
7585
7720
  onSlotChange(callback) {
7586
- const id = ++this._slotSubscriptionCounter;
7587
- this._slotSubscriptions[id] = {
7721
+ return this._makeSubscription({
7588
7722
  callback,
7589
- subscriptionId: null
7590
- };
7591
-
7592
- this._updateSubscriptions();
7593
-
7594
- return id;
7723
+ method: 'slotSubscribe',
7724
+ unsubscribeMethod: 'slotUnsubscribe'
7725
+ }, []
7726
+ /* args */
7727
+ );
7595
7728
  }
7596
7729
  /**
7597
7730
  * Deregister a slot notification callback
7598
7731
  *
7599
- * @param id subscription id to deregister
7732
+ * @param id client subscription id to deregister
7600
7733
  */
7601
7734
 
7602
7735
 
7603
- async removeSlotChangeListener(id) {
7604
- if (this._slotSubscriptions[id]) {
7605
- const subInfo = this._slotSubscriptions[id];
7606
- delete this._slotSubscriptions[id];
7607
- await this._unsubscribe(subInfo, 'slotUnsubscribe');
7608
-
7609
- this._updateSubscriptions();
7610
- } else {
7611
- console.warn(createSubscriptionWarningMessage(id, 'slot change'));
7612
- }
7736
+ async removeSlotChangeListener(clientSubscriptionId) {
7737
+ await this._unsubscribeClientSubscription(clientSubscriptionId, 'slot change');
7613
7738
  }
7614
7739
  /**
7615
7740
  * @internal
@@ -7617,14 +7742,12 @@ class Connection {
7617
7742
 
7618
7743
 
7619
7744
  _wsOnSlotUpdatesNotification(notification) {
7620
- const res = superstruct.create(notification, SlotUpdateNotificationResult);
7745
+ const {
7746
+ result,
7747
+ subscription
7748
+ } = superstruct.create(notification, SlotUpdateNotificationResult);
7621
7749
 
7622
- for (const sub of Object.values(this._slotUpdateSubscriptions)) {
7623
- if (sub.subscriptionId === res.subscription) {
7624
- sub.callback(res.result);
7625
- return;
7626
- }
7627
- }
7750
+ this._handleServerNotification(subscription, [result]);
7628
7751
  }
7629
7752
  /**
7630
7753
  * Register a callback to be invoked upon slot updates. {@link SlotUpdate}'s
@@ -7636,32 +7759,36 @@ class Connection {
7636
7759
 
7637
7760
 
7638
7761
  onSlotUpdate(callback) {
7639
- const id = ++this._slotUpdateSubscriptionCounter;
7640
- this._slotUpdateSubscriptions[id] = {
7762
+ return this._makeSubscription({
7641
7763
  callback,
7642
- subscriptionId: null
7643
- };
7644
-
7645
- this._updateSubscriptions();
7646
-
7647
- return id;
7764
+ method: 'slotsUpdatesSubscribe',
7765
+ unsubscribeMethod: 'slotsUpdatesUnsubscribe'
7766
+ }, []
7767
+ /* args */
7768
+ );
7648
7769
  }
7649
7770
  /**
7650
7771
  * Deregister a slot update notification callback
7651
7772
  *
7652
- * @param id subscription id to deregister
7773
+ * @param id client subscription id to deregister
7653
7774
  */
7654
7775
 
7655
7776
 
7656
- async removeSlotUpdateListener(id) {
7657
- if (this._slotUpdateSubscriptions[id]) {
7658
- const subInfo = this._slotUpdateSubscriptions[id];
7659
- delete this._slotUpdateSubscriptions[id];
7660
- await this._unsubscribe(subInfo, 'slotsUpdatesUnsubscribe');
7777
+ async removeSlotUpdateListener(clientSubscriptionId) {
7778
+ await this._unsubscribeClientSubscription(clientSubscriptionId, 'slot update');
7779
+ }
7780
+ /**
7781
+ * @internal
7782
+ */
7661
7783
 
7662
- this._updateSubscriptions();
7784
+
7785
+ async _unsubscribeClientSubscription(clientSubscriptionId, subscriptionName) {
7786
+ const dispose = this._subscriptionDisposeFunctionsByClientSubscriptionId[clientSubscriptionId];
7787
+
7788
+ if (dispose) {
7789
+ await dispose();
7663
7790
  } else {
7664
- console.warn(createSubscriptionWarningMessage(id, 'slot update'));
7791
+ console.warn('Ignored unsubscribe request because an active subscription with id ' + `\`${clientSubscriptionId}\` for '${subscriptionName}' events ` + 'could not be found.');
7665
7792
  }
7666
7793
  }
7667
7794
 
@@ -7708,30 +7835,34 @@ class Connection {
7708
7835
 
7709
7836
 
7710
7837
  _wsOnSignatureNotification(notification) {
7711
- const res = superstruct.create(notification, SignatureNotificationResult);
7712
-
7713
- for (const [id, sub] of Object.entries(this._signatureSubscriptions)) {
7714
- if (sub.subscriptionId === res.subscription) {
7715
- if (res.result.value === 'receivedSignature') {
7716
- sub.callback({
7717
- type: 'received'
7718
- }, res.result.context);
7719
- } else {
7720
- // Signatures subscriptions are auto-removed by the RPC service so
7721
- // no need to explicitly send an unsubscribe message
7722
- delete this._signatureSubscriptions[Number(id)];
7723
-
7724
- this._updateSubscriptions();
7725
-
7726
- sub.callback({
7727
- type: 'status',
7728
- result: res.result.value
7729
- }, res.result.context);
7730
- }
7731
-
7732
- return;
7733
- }
7734
- }
7838
+ const {
7839
+ result,
7840
+ subscription
7841
+ } = superstruct.create(notification, SignatureNotificationResult);
7842
+
7843
+ if (result.value !== 'receivedSignature') {
7844
+ /**
7845
+ * Special case.
7846
+ * After a signature is processed, RPCs automatically dispose of the
7847
+ * subscription on the server side. We need to track which of these
7848
+ * subscriptions have been disposed in such a way, so that we know
7849
+ * whether the client is dealing with a not-yet-processed signature
7850
+ * (in which case we must tear down the server subscription) or an
7851
+ * already-processed signature (in which case the client can simply
7852
+ * clear out the subscription locally without telling the server).
7853
+ *
7854
+ * NOTE: There is a proposal to eliminate this special case, here:
7855
+ * https://github.com/solana-labs/solana/issues/18892
7856
+ */
7857
+ this._subscriptionsAutoDisposedByRpc.add(subscription);
7858
+ }
7859
+
7860
+ this._handleServerNotification(subscription, result.value === 'receivedSignature' ? [{
7861
+ type: 'received'
7862
+ }, result.context] : [{
7863
+ type: 'status',
7864
+ result: result.value
7865
+ }, result.context]);
7735
7866
  }
7736
7867
  /**
7737
7868
  * Register a callback to be invoked upon signature updates
@@ -7744,23 +7875,26 @@ class Connection {
7744
7875
 
7745
7876
 
7746
7877
  onSignature(signature, callback, commitment) {
7747
- const id = ++this._signatureSubscriptionCounter;
7748
- this._signatureSubscriptions[id] = {
7749
- signature,
7878
+ const args = this._buildArgs([signature], commitment || this._commitment || 'finalized' // Apply connection/server default.
7879
+ );
7880
+
7881
+ const clientSubscriptionId = this._makeSubscription({
7750
7882
  callback: (notification, context) => {
7751
7883
  if (notification.type === 'status') {
7752
- callback(notification.result, context);
7884
+ callback(notification.result, context); // Signatures subscriptions are auto-removed by the RPC service
7885
+ // so no need to explicitly send an unsubscribe message.
7886
+
7887
+ try {
7888
+ this.removeSignatureListener(clientSubscriptionId); // eslint-disable-next-line no-empty
7889
+ } catch {// Already removed.
7890
+ }
7753
7891
  }
7754
7892
  },
7755
- options: {
7756
- commitment
7757
- },
7758
- subscriptionId: null
7759
- };
7893
+ method: 'signatureSubscribe',
7894
+ unsubscribeMethod: 'signatureUnsubscribe'
7895
+ }, args);
7760
7896
 
7761
- this._updateSubscriptions();
7762
-
7763
- return id;
7897
+ return clientSubscriptionId;
7764
7898
  }
7765
7899
  /**
7766
7900
  * Register a callback to be invoked when a transaction is
@@ -7775,35 +7909,43 @@ class Connection {
7775
7909
 
7776
7910
 
7777
7911
  onSignatureWithOptions(signature, callback, options) {
7778
- const id = ++this._signatureSubscriptionCounter;
7779
- this._signatureSubscriptions[id] = {
7780
- signature,
7781
- callback,
7782
- options,
7783
- subscriptionId: null
7912
+ const {
7913
+ commitment,
7914
+ ...extra
7915
+ } = { ...options,
7916
+ commitment: options && options.commitment || this._commitment || 'finalized' // Apply connection/server default.
7917
+
7784
7918
  };
7785
7919
 
7786
- this._updateSubscriptions();
7920
+ const args = this._buildArgs([signature], commitment, undefined
7921
+ /* encoding */
7922
+ , extra);
7787
7923
 
7788
- return id;
7924
+ const clientSubscriptionId = this._makeSubscription({
7925
+ callback: (notification, context) => {
7926
+ callback(notification, context); // Signatures subscriptions are auto-removed by the RPC service
7927
+ // so no need to explicitly send an unsubscribe message.
7928
+
7929
+ try {
7930
+ this.removeSignatureListener(clientSubscriptionId); // eslint-disable-next-line no-empty
7931
+ } catch {// Already removed.
7932
+ }
7933
+ },
7934
+ method: 'signatureSubscribe',
7935
+ unsubscribeMethod: 'signatureUnsubscribe'
7936
+ }, args);
7937
+
7938
+ return clientSubscriptionId;
7789
7939
  }
7790
7940
  /**
7791
7941
  * Deregister a signature notification callback
7792
7942
  *
7793
- * @param id subscription id to deregister
7943
+ * @param id client subscription id to deregister
7794
7944
  */
7795
7945
 
7796
7946
 
7797
- async removeSignatureListener(id) {
7798
- if (this._signatureSubscriptions[id]) {
7799
- const subInfo = this._signatureSubscriptions[id];
7800
- delete this._signatureSubscriptions[id];
7801
- await this._unsubscribe(subInfo, 'signatureUnsubscribe');
7802
-
7803
- this._updateSubscriptions();
7804
- } else {
7805
- console.warn(createSubscriptionWarningMessage(id, 'signature result'));
7806
- }
7947
+ async removeSignatureListener(clientSubscriptionId) {
7948
+ await this._unsubscribeClientSubscription(clientSubscriptionId, 'signature result');
7807
7949
  }
7808
7950
  /**
7809
7951
  * @internal
@@ -7811,14 +7953,12 @@ class Connection {
7811
7953
 
7812
7954
 
7813
7955
  _wsOnRootNotification(notification) {
7814
- const res = superstruct.create(notification, RootNotificationResult);
7956
+ const {
7957
+ result,
7958
+ subscription
7959
+ } = superstruct.create(notification, RootNotificationResult);
7815
7960
 
7816
- for (const sub of Object.values(this._rootSubscriptions)) {
7817
- if (sub.subscriptionId === res.subscription) {
7818
- sub.callback(res.result);
7819
- return;
7820
- }
7821
- }
7961
+ this._handleServerNotification(subscription, [result]);
7822
7962
  }
7823
7963
  /**
7824
7964
  * Register a callback to be invoked upon root changes
@@ -7829,33 +7969,23 @@ class Connection {
7829
7969
 
7830
7970
 
7831
7971
  onRootChange(callback) {
7832
- const id = ++this._rootSubscriptionCounter;
7833
- this._rootSubscriptions[id] = {
7972
+ return this._makeSubscription({
7834
7973
  callback,
7835
- subscriptionId: null
7836
- };
7837
-
7838
- this._updateSubscriptions();
7839
-
7840
- return id;
7974
+ method: 'rootSubscribe',
7975
+ unsubscribeMethod: 'rootUnsubscribe'
7976
+ }, []
7977
+ /* args */
7978
+ );
7841
7979
  }
7842
7980
  /**
7843
7981
  * Deregister a root notification callback
7844
7982
  *
7845
- * @param id subscription id to deregister
7983
+ * @param id client subscription id to deregister
7846
7984
  */
7847
7985
 
7848
7986
 
7849
- async removeRootChangeListener(id) {
7850
- if (this._rootSubscriptions[id]) {
7851
- const subInfo = this._rootSubscriptions[id];
7852
- delete this._rootSubscriptions[id];
7853
- await this._unsubscribe(subInfo, 'rootUnsubscribe');
7854
-
7855
- this._updateSubscriptions();
7856
- } else {
7857
- console.warn(createSubscriptionWarningMessage(id, 'root change'));
7858
- }
7987
+ async removeRootChangeListener(clientSubscriptionId) {
7988
+ await this._unsubscribeClientSubscription(clientSubscriptionId, 'root change');
7859
7989
  }
7860
7990
 
7861
7991
  }