@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.esm.js CHANGED
@@ -4209,6 +4209,82 @@ class ComputeBudgetProgram {
4209
4209
  }
4210
4210
  ComputeBudgetProgram.programId = new PublicKey('ComputeBudget111111111111111111111111111111');
4211
4211
 
4212
+ var objToString = Object.prototype.toString;
4213
+ var objKeys = Object.keys || function(obj) {
4214
+ var keys = [];
4215
+ for (var name in obj) {
4216
+ keys.push(name);
4217
+ }
4218
+ return keys;
4219
+ };
4220
+
4221
+ function stringify(val, isArrayProp) {
4222
+ var i, max, str, keys, key, propVal, toStr;
4223
+ if (val === true) {
4224
+ return "true";
4225
+ }
4226
+ if (val === false) {
4227
+ return "false";
4228
+ }
4229
+ switch (typeof val) {
4230
+ case "object":
4231
+ if (val === null) {
4232
+ return null;
4233
+ } else if (val.toJSON && typeof val.toJSON === "function") {
4234
+ return stringify(val.toJSON(), isArrayProp);
4235
+ } else {
4236
+ toStr = objToString.call(val);
4237
+ if (toStr === "[object Array]") {
4238
+ str = '[';
4239
+ max = val.length - 1;
4240
+ for(i = 0; i < max; i++) {
4241
+ str += stringify(val[i], true) + ',';
4242
+ }
4243
+ if (max > -1) {
4244
+ str += stringify(val[i], true);
4245
+ }
4246
+ return str + ']';
4247
+ } else if (toStr === "[object Object]") {
4248
+ // only object is left
4249
+ keys = objKeys(val).sort();
4250
+ max = keys.length;
4251
+ str = "";
4252
+ i = 0;
4253
+ while (i < max) {
4254
+ key = keys[i];
4255
+ propVal = stringify(val[key], false);
4256
+ if (propVal !== undefined) {
4257
+ if (str) {
4258
+ str += ',';
4259
+ }
4260
+ str += JSON.stringify(key) + ':' + propVal;
4261
+ }
4262
+ i++;
4263
+ }
4264
+ return '{' + str + '}';
4265
+ } else {
4266
+ return JSON.stringify(val);
4267
+ }
4268
+ }
4269
+ case "function":
4270
+ case "undefined":
4271
+ return isArrayProp ? null : undefined;
4272
+ case "string":
4273
+ return JSON.stringify(val);
4274
+ default:
4275
+ return isFinite(val) ? val : null;
4276
+ }
4277
+ }
4278
+
4279
+ var fastStableStringify = function(val) {
4280
+ var returnVal = stringify(val, false);
4281
+ if (returnVal !== undefined) {
4282
+ return ''+ returnVal;
4283
+ }
4284
+ };
4285
+
4286
+ var fastStableStringify$1 = fastStableStringify;
4287
+
4212
4288
  const DESTROY_TIMEOUT_MS = 5000;
4213
4289
  class AgentManager {
4214
4290
  static _newAgent(useHttps) {
@@ -4424,6 +4500,12 @@ const BufferFromRawAccountData = coerce(instance(Buffer), RawAccountDataResult,
4424
4500
  */
4425
4501
 
4426
4502
  const BLOCKHASH_CACHE_TIMEOUT_MS = 30 * 1000;
4503
+ /**
4504
+ * HACK.
4505
+ * Copied from rpc-websockets/dist/lib/client.
4506
+ * Otherwise, `yarn build` fails with:
4507
+ * https://gist.github.com/steveluscher/c057eca81d479ef705cdb53162f9971d
4508
+ */
4427
4509
 
4428
4510
  /**
4429
4511
  * @internal
@@ -5298,14 +5380,9 @@ const LogsNotificationResult = type({
5298
5380
  * Filter for log subscriptions.
5299
5381
  */
5300
5382
 
5301
- function createSubscriptionWarningMessage(id, label) {
5302
- return 'Ignored unsubscribe request because an active subscription ' + `with id \`${id}\` for '${label}' events could not be found.`;
5303
- }
5304
5383
  /**
5305
5384
  * A connection to a fullnode JSON RPC endpoint
5306
5385
  */
5307
-
5308
-
5309
5386
  class Connection {
5310
5387
  /** @internal */
5311
5388
 
@@ -5329,21 +5406,13 @@ class Connection {
5329
5406
 
5330
5407
  /** @internal */
5331
5408
 
5332
- /** @internal */
5333
-
5334
- /** @internal */
5335
-
5336
- /** @internal */
5337
-
5338
- /** @internal */
5339
-
5340
- /** @internal */
5341
-
5342
- /** @internal */
5343
-
5344
- /** @internal */
5345
-
5346
- /** @internal */
5409
+ /** @internal
5410
+ * A number that we increment every time an active connection closes.
5411
+ * Used to determine whether the same socket connection that was open
5412
+ * when an async operation started is the same one that's active when
5413
+ * its continuation fires.
5414
+ *
5415
+ */
5347
5416
 
5348
5417
  /** @internal */
5349
5418
 
@@ -5359,7 +5428,19 @@ class Connection {
5359
5428
 
5360
5429
  /** @internal */
5361
5430
 
5362
- /** @internal */
5431
+ /**
5432
+ * Special case.
5433
+ * After a signature is processed, RPCs automatically dispose of the
5434
+ * subscription on the server side. We need to track which of these
5435
+ * subscriptions have been disposed in such a way, so that we know
5436
+ * whether the client is dealing with a not-yet-processed signature
5437
+ * (in which case we must tear down the server subscription) or an
5438
+ * already-processed signature (in which case the client can simply
5439
+ * clear out the subscription locally without telling the server).
5440
+ *
5441
+ * NOTE: There is a proposal to eliminate this special case, here:
5442
+ * https://github.com/solana-labs/solana/issues/18892
5443
+ */
5363
5444
 
5364
5445
  /** @internal */
5365
5446
 
@@ -5381,6 +5462,7 @@ class Connection {
5381
5462
  this._rpcWebSocketConnected = false;
5382
5463
  this._rpcWebSocketHeartbeat = null;
5383
5464
  this._rpcWebSocketIdleTimeout = null;
5465
+ this._rpcWebSocketGeneration = 0;
5384
5466
  this._disableBlockhashCaching = false;
5385
5467
  this._pollingBlockhash = false;
5386
5468
  this._blockhashInfo = {
@@ -5389,20 +5471,11 @@ class Connection {
5389
5471
  transactionSignatures: [],
5390
5472
  simulatedSignatures: []
5391
5473
  };
5392
- this._accountChangeSubscriptionCounter = 0;
5393
- this._accountChangeSubscriptions = {};
5394
- this._programAccountChangeSubscriptionCounter = 0;
5395
- this._programAccountChangeSubscriptions = {};
5396
- this._rootSubscriptionCounter = 0;
5397
- this._rootSubscriptions = {};
5398
- this._signatureSubscriptionCounter = 0;
5399
- this._signatureSubscriptions = {};
5400
- this._slotSubscriptionCounter = 0;
5401
- this._slotSubscriptions = {};
5402
- this._logsSubscriptionCounter = 0;
5403
- this._logsSubscriptions = {};
5404
- this._slotUpdateSubscriptionCounter = 0;
5405
- this._slotUpdateSubscriptions = {};
5474
+ this._nextClientSubscriptionId = 0;
5475
+ this._subscriptionDisposeFunctionsByClientSubscriptionId = {};
5476
+ this._subscriptionCallbacksByServerSubscriptionId = {};
5477
+ this._subscriptionsByHash = {};
5478
+ this._subscriptionsAutoDisposedByRpc = new Set();
5406
5479
  let url = new URL(endpoint);
5407
5480
  const useHttps = url.protocol === 'https:';
5408
5481
  let wsEndpoint;
@@ -7170,6 +7243,8 @@ class Connection {
7170
7243
 
7171
7244
 
7172
7245
  _wsOnClose(code) {
7246
+ this._rpcWebSocketGeneration++;
7247
+
7173
7248
  if (this._rpcWebSocketHeartbeat) {
7174
7249
  clearInterval(this._rpcWebSocketHeartbeat);
7175
7250
  this._rpcWebSocketHeartbeat = null;
@@ -7183,85 +7258,20 @@ class Connection {
7183
7258
  } // implicit close, prepare subscriptions for auto-reconnect
7184
7259
 
7185
7260
 
7186
- this._resetSubscriptions();
7187
- }
7188
- /**
7189
- * @internal
7190
- */
7191
-
7192
-
7193
- async _subscribe(sub, rpcMethod, rpcArgs) {
7194
- if (sub.subscriptionId == null) {
7195
- sub.subscriptionId = 'subscribing';
7196
-
7197
- try {
7198
- const id = await this._rpcWebSocket.call(rpcMethod, rpcArgs);
7199
-
7200
- if (typeof id === 'number' && sub.subscriptionId === 'subscribing') {
7201
- // eslint-disable-next-line require-atomic-updates
7202
- sub.subscriptionId = id;
7203
- }
7204
- } catch (err) {
7205
- if (sub.subscriptionId === 'subscribing') {
7206
- // eslint-disable-next-line require-atomic-updates
7207
- sub.subscriptionId = null;
7208
- }
7209
-
7210
- if (err instanceof Error) {
7211
- console.error(`${rpcMethod} error for argument`, rpcArgs, err.message);
7212
- }
7213
- }
7214
- }
7215
- }
7216
- /**
7217
- * @internal
7218
- */
7219
-
7220
-
7221
- async _unsubscribe(sub, rpcMethod) {
7222
- const subscriptionId = sub.subscriptionId;
7223
-
7224
- if (subscriptionId != null && typeof subscriptionId != 'string') {
7225
- const unsubscribeId = subscriptionId;
7226
-
7227
- try {
7228
- await this._rpcWebSocket.call(rpcMethod, [unsubscribeId]);
7229
- } catch (err) {
7230
- if (err instanceof Error) {
7231
- console.error(`${rpcMethod} error:`, err.message);
7232
- }
7233
- }
7234
- }
7235
- }
7236
- /**
7237
- * @internal
7238
- */
7239
-
7240
-
7241
- _resetSubscriptions() {
7242
- Object.values(this._accountChangeSubscriptions).forEach(s => s.subscriptionId = null);
7243
- Object.values(this._logsSubscriptions).forEach(s => s.subscriptionId = null);
7244
- Object.values(this._programAccountChangeSubscriptions).forEach(s => s.subscriptionId = null);
7245
- Object.values(this._rootSubscriptions).forEach(s => s.subscriptionId = null);
7246
- Object.values(this._signatureSubscriptions).forEach(s => s.subscriptionId = null);
7247
- Object.values(this._slotSubscriptions).forEach(s => s.subscriptionId = null);
7248
- Object.values(this._slotUpdateSubscriptions).forEach(s => s.subscriptionId = null);
7261
+ this._subscriptionCallbacksByServerSubscriptionId = {};
7262
+ Object.entries(this._subscriptionsByHash).forEach(([hash, subscription]) => {
7263
+ this._subscriptionsByHash[hash] = { ...subscription,
7264
+ state: 'pending'
7265
+ };
7266
+ });
7249
7267
  }
7250
7268
  /**
7251
7269
  * @internal
7252
7270
  */
7253
7271
 
7254
7272
 
7255
- _updateSubscriptions() {
7256
- const accountKeys = Object.keys(this._accountChangeSubscriptions).map(Number);
7257
- const programKeys = Object.keys(this._programAccountChangeSubscriptions).map(Number);
7258
- const slotKeys = Object.keys(this._slotSubscriptions).map(Number);
7259
- const slotUpdateKeys = Object.keys(this._slotUpdateSubscriptions).map(Number);
7260
- const signatureKeys = Object.keys(this._signatureSubscriptions).map(Number);
7261
- const rootKeys = Object.keys(this._rootSubscriptions).map(Number);
7262
- const logsKeys = Object.keys(this._logsSubscriptions).map(Number);
7263
-
7264
- if (accountKeys.length === 0 && programKeys.length === 0 && slotKeys.length === 0 && slotUpdateKeys.length === 0 && signatureKeys.length === 0 && rootKeys.length === 0 && logsKeys.length === 0) {
7273
+ async _updateSubscriptions() {
7274
+ if (Object.keys(this._subscriptionsByHash).length === 0) {
7265
7275
  if (this._rpcWebSocketConnected) {
7266
7276
  this._rpcWebSocketConnected = false;
7267
7277
  this._rpcWebSocketIdleTimeout = setTimeout(() => {
@@ -7293,60 +7303,167 @@ class Connection {
7293
7303
  return;
7294
7304
  }
7295
7305
 
7296
- for (let id of accountKeys) {
7297
- const sub = this._accountChangeSubscriptions[id];
7298
-
7299
- this._subscribe(sub, 'accountSubscribe', this._buildArgs([sub.publicKey], sub.commitment, 'base64'));
7300
- }
7306
+ const activeWebSocketGeneration = this._rpcWebSocketGeneration;
7301
7307
 
7302
- for (let id of programKeys) {
7303
- const sub = this._programAccountChangeSubscriptions[id];
7308
+ const isCurrentConnectionStillActive = () => {
7309
+ return activeWebSocketGeneration === this._rpcWebSocketGeneration;
7310
+ };
7304
7311
 
7305
- this._subscribe(sub, 'programSubscribe', this._buildArgs([sub.programId], sub.commitment, 'base64', {
7306
- filters: sub.filters
7307
- }));
7308
- }
7312
+ await Promise.all( // Don't be tempted to change this to `Object.entries`. We call
7313
+ // `_updateSubscriptions` recursively when processing the state,
7314
+ // so it's important that we look up the *current* version of
7315
+ // each subscription, every time we process a hash.
7316
+ Object.keys(this._subscriptionsByHash).map(async hash => {
7317
+ const subscription = this._subscriptionsByHash[hash];
7309
7318
 
7310
- for (let id of slotKeys) {
7311
- const sub = this._slotSubscriptions[id];
7319
+ if (subscription === undefined) {
7320
+ // This entry has since been deleted. Skip.
7321
+ return;
7322
+ }
7312
7323
 
7313
- this._subscribe(sub, 'slotSubscribe', []);
7314
- }
7324
+ switch (subscription.state) {
7325
+ case 'pending':
7326
+ case 'unsubscribed':
7327
+ if (subscription.callbacks.size === 0) {
7328
+ /**
7329
+ * You can end up here when:
7330
+ *
7331
+ * - a subscription has recently unsubscribed
7332
+ * without having new callbacks added to it
7333
+ * while the unsubscribe was in flight, or
7334
+ * - when a pending subscription has its
7335
+ * listeners removed before a request was
7336
+ * sent to the server.
7337
+ *
7338
+ * Being that nobody is interested in this
7339
+ * subscription any longer, delete it.
7340
+ */
7341
+ delete this._subscriptionsByHash[hash];
7342
+
7343
+ if (subscription.state === 'unsubscribed') {
7344
+ delete this._subscriptionCallbacksByServerSubscriptionId[subscription.serverSubscriptionId];
7345
+ }
7315
7346
 
7316
- for (let id of slotUpdateKeys) {
7317
- const sub = this._slotUpdateSubscriptions[id];
7347
+ await this._updateSubscriptions();
7348
+ return;
7349
+ }
7318
7350
 
7319
- this._subscribe(sub, 'slotsUpdatesSubscribe', []);
7320
- }
7351
+ await (async () => {
7352
+ const {
7353
+ args,
7354
+ method
7355
+ } = subscription;
7321
7356
 
7322
- for (let id of signatureKeys) {
7323
- const sub = this._signatureSubscriptions[id];
7324
- const args = [sub.signature];
7325
- if (sub.options) args.push(sub.options);
7357
+ try {
7358
+ this._subscriptionsByHash[hash] = { ...subscription,
7359
+ state: 'subscribing'
7360
+ };
7361
+ const serverSubscriptionId = await this._rpcWebSocket.call(method, args);
7362
+ this._subscriptionsByHash[hash] = { ...subscription,
7363
+ serverSubscriptionId,
7364
+ state: 'subscribed'
7365
+ };
7366
+ this._subscriptionCallbacksByServerSubscriptionId[serverSubscriptionId] = subscription.callbacks;
7367
+ await this._updateSubscriptions();
7368
+ } catch (e) {
7369
+ if (e instanceof Error) {
7370
+ console.error(`${method} error for argument`, args, e.message);
7371
+ }
7372
+
7373
+ if (!isCurrentConnectionStillActive()) {
7374
+ return;
7375
+ } // TODO: Maybe add an 'errored' state or a retry limit?
7326
7376
 
7327
- this._subscribe(sub, 'signatureSubscribe', args);
7328
- }
7329
7377
 
7330
- for (let id of rootKeys) {
7331
- const sub = this._rootSubscriptions[id];
7378
+ this._subscriptionsByHash[hash] = { ...subscription,
7379
+ state: 'pending'
7380
+ };
7381
+ await this._updateSubscriptions();
7382
+ }
7383
+ })();
7384
+ break;
7332
7385
 
7333
- this._subscribe(sub, 'rootSubscribe', []);
7334
- }
7386
+ case 'subscribed':
7387
+ if (subscription.callbacks.size === 0) {
7388
+ // By the time we successfully set up a subscription
7389
+ // with the server, the client stopped caring about it.
7390
+ // Tear it down now.
7391
+ await (async () => {
7392
+ const {
7393
+ serverSubscriptionId,
7394
+ unsubscribeMethod
7395
+ } = subscription;
7396
+
7397
+ if (this._subscriptionsAutoDisposedByRpc.has(serverSubscriptionId)) {
7398
+ /**
7399
+ * Special case.
7400
+ * If we're dealing with a subscription that has been auto-
7401
+ * disposed by the RPC, then we can skip the RPC call to
7402
+ * tear down the subscription here.
7403
+ *
7404
+ * NOTE: There is a proposal to eliminate this special case, here:
7405
+ * https://github.com/solana-labs/solana/issues/18892
7406
+ */
7407
+ this._subscriptionsAutoDisposedByRpc.delete(serverSubscriptionId);
7408
+ } else {
7409
+ this._subscriptionsByHash[hash] = { ...subscription,
7410
+ state: 'unsubscribing'
7411
+ };
7412
+
7413
+ try {
7414
+ await this._rpcWebSocket.call(unsubscribeMethod, [serverSubscriptionId]);
7415
+ } catch (e) {
7416
+ if (e instanceof Error) {
7417
+ console.error(`${unsubscribeMethod} error:`, e.message);
7418
+ }
7419
+
7420
+ if (!isCurrentConnectionStillActive()) {
7421
+ return;
7422
+ } // TODO: Maybe add an 'errored' state or a retry limit?
7423
+
7424
+
7425
+ this._subscriptionsByHash[hash] = { ...subscription,
7426
+ state: 'subscribed'
7427
+ };
7428
+ await this._updateSubscriptions();
7429
+ return;
7430
+ }
7431
+ }
7335
7432
 
7336
- for (let id of logsKeys) {
7337
- const sub = this._logsSubscriptions[id];
7338
- let filter;
7433
+ this._subscriptionsByHash[hash] = { ...subscription,
7434
+ state: 'unsubscribed'
7435
+ };
7436
+ await this._updateSubscriptions();
7437
+ })();
7438
+ }
7339
7439
 
7340
- if (typeof sub.filter === 'object') {
7341
- filter = {
7342
- mentions: [sub.filter.toString()]
7343
- };
7344
- } else {
7345
- filter = sub.filter;
7440
+ break;
7346
7441
  }
7442
+ }));
7443
+ }
7444
+ /**
7445
+ * @internal
7446
+ */
7347
7447
 
7348
- this._subscribe(sub, 'logsSubscribe', this._buildArgs([filter], sub.commitment));
7448
+
7449
+ _handleServerNotification(serverSubscriptionId, callbackArgs) {
7450
+ const callbacks = this._subscriptionCallbacksByServerSubscriptionId[serverSubscriptionId];
7451
+
7452
+ if (callbacks === undefined) {
7453
+ return;
7349
7454
  }
7455
+
7456
+ callbacks.forEach(cb => {
7457
+ try {
7458
+ cb( // I failed to find a way to convince TypeScript that `cb` is of type
7459
+ // `TCallback` which is certainly compatible with `Parameters<TCallback>`.
7460
+ // See https://github.com/microsoft/TypeScript/issues/47615
7461
+ // @ts-ignore
7462
+ ...callbackArgs);
7463
+ } catch (e) {
7464
+ console.error(e);
7465
+ }
7466
+ });
7350
7467
  }
7351
7468
  /**
7352
7469
  * @internal
@@ -7354,14 +7471,71 @@ class Connection {
7354
7471
 
7355
7472
 
7356
7473
  _wsOnAccountNotification(notification) {
7357
- const res = create(notification, AccountNotificationResult);
7474
+ const {
7475
+ result,
7476
+ subscription
7477
+ } = create(notification, AccountNotificationResult);
7358
7478
 
7359
- for (const sub of Object.values(this._accountChangeSubscriptions)) {
7360
- if (sub.subscriptionId === res.subscription) {
7361
- sub.callback(res.result.value, res.result.context);
7362
- return;
7363
- }
7479
+ this._handleServerNotification(subscription, [result.value, result.context]);
7480
+ }
7481
+ /**
7482
+ * @internal
7483
+ */
7484
+
7485
+
7486
+ _makeSubscription(subscriptionConfig,
7487
+ /**
7488
+ * When preparing `args` for a call to `_makeSubscription`, be sure
7489
+ * to carefully apply a default `commitment` property, if necessary.
7490
+ *
7491
+ * - If the user supplied a `commitment` use that.
7492
+ * - Otherwise, if the `Connection::commitment` is set, use that.
7493
+ * - Otherwise, set it to the RPC server default: `finalized`.
7494
+ *
7495
+ * This is extremely important to ensure that these two fundamentally
7496
+ * identical subscriptions produce the same identifying hash:
7497
+ *
7498
+ * - A subscription made without specifying a commitment.
7499
+ * - A subscription made where the commitment specified is the same
7500
+ * as the default applied to the subscription above.
7501
+ *
7502
+ * Example; these two subscriptions must produce the same hash:
7503
+ *
7504
+ * - An `accountSubscribe` subscription for `'PUBKEY'`
7505
+ * - An `accountSubscribe` subscription for `'PUBKEY'` with commitment
7506
+ * `'finalized'`.
7507
+ *
7508
+ * See the 'making a subscription with defaulted params omitted' test
7509
+ * in `connection-subscriptions.ts` for more.
7510
+ */
7511
+ args) {
7512
+ const clientSubscriptionId = this._nextClientSubscriptionId++;
7513
+ const hash = fastStableStringify$1([subscriptionConfig.method, args], true
7514
+ /* isArrayProp */
7515
+ );
7516
+ const existingSubscription = this._subscriptionsByHash[hash];
7517
+
7518
+ if (existingSubscription === undefined) {
7519
+ this._subscriptionsByHash[hash] = { ...subscriptionConfig,
7520
+ args,
7521
+ callbacks: new Set([subscriptionConfig.callback]),
7522
+ state: 'pending'
7523
+ };
7524
+ } else {
7525
+ existingSubscription.callbacks.add(subscriptionConfig.callback);
7364
7526
  }
7527
+
7528
+ this._subscriptionDisposeFunctionsByClientSubscriptionId[clientSubscriptionId] = async () => {
7529
+ delete this._subscriptionDisposeFunctionsByClientSubscriptionId[clientSubscriptionId];
7530
+ const subscription = this._subscriptionsByHash[hash];
7531
+ assert(subscription !== undefined, `Could not find a \`Subscription\` when tearing down client subscription #${clientSubscriptionId}`);
7532
+ subscription.callbacks.delete(subscriptionConfig.callback);
7533
+ await this._updateSubscriptions();
7534
+ };
7535
+
7536
+ this._updateSubscriptions();
7537
+
7538
+ return clientSubscriptionId;
7365
7539
  }
7366
7540
  /**
7367
7541
  * Register a callback to be invoked whenever the specified account changes
@@ -7374,35 +7548,24 @@ class Connection {
7374
7548
 
7375
7549
 
7376
7550
  onAccountChange(publicKey, callback, commitment) {
7377
- const id = ++this._accountChangeSubscriptionCounter;
7378
- this._accountChangeSubscriptions[id] = {
7379
- publicKey: publicKey.toBase58(),
7380
- callback,
7381
- commitment,
7382
- subscriptionId: null
7383
- };
7551
+ const args = this._buildArgs([publicKey.toBase58()], commitment || this._commitment || 'finalized', // Apply connection/server default.
7552
+ 'base64');
7384
7553
 
7385
- this._updateSubscriptions();
7386
-
7387
- return id;
7554
+ return this._makeSubscription({
7555
+ callback,
7556
+ method: 'accountSubscribe',
7557
+ unsubscribeMethod: 'accountUnsubscribe'
7558
+ }, args);
7388
7559
  }
7389
7560
  /**
7390
7561
  * Deregister an account notification callback
7391
7562
  *
7392
- * @param id subscription id to deregister
7563
+ * @param id client subscription id to deregister
7393
7564
  */
7394
7565
 
7395
7566
 
7396
- async removeAccountChangeListener(id) {
7397
- if (this._accountChangeSubscriptions[id]) {
7398
- const subInfo = this._accountChangeSubscriptions[id];
7399
- delete this._accountChangeSubscriptions[id];
7400
- await this._unsubscribe(subInfo, 'accountUnsubscribe');
7401
-
7402
- this._updateSubscriptions();
7403
- } else {
7404
- console.warn(createSubscriptionWarningMessage(id, 'account change'));
7405
- }
7567
+ async removeAccountChangeListener(clientSubscriptionId) {
7568
+ await this._unsubscribeClientSubscription(clientSubscriptionId, 'account change');
7406
7569
  }
7407
7570
  /**
7408
7571
  * @internal
@@ -7410,21 +7573,15 @@ class Connection {
7410
7573
 
7411
7574
 
7412
7575
  _wsOnProgramAccountNotification(notification) {
7413
- const res = create(notification, ProgramAccountNotificationResult);
7576
+ const {
7577
+ result,
7578
+ subscription
7579
+ } = create(notification, ProgramAccountNotificationResult);
7414
7580
 
7415
- for (const sub of Object.values(this._programAccountChangeSubscriptions)) {
7416
- if (sub.subscriptionId === res.subscription) {
7417
- const {
7418
- value,
7419
- context
7420
- } = res.result;
7421
- sub.callback({
7422
- accountId: value.pubkey,
7423
- accountInfo: value.account
7424
- }, context);
7425
- return;
7426
- }
7427
- }
7581
+ this._handleServerNotification(subscription, [{
7582
+ accountId: result.value.pubkey,
7583
+ accountInfo: result.value.account
7584
+ }, result.context]);
7428
7585
  }
7429
7586
  /**
7430
7587
  * Register a callback to be invoked whenever accounts owned by the
@@ -7439,36 +7596,30 @@ class Connection {
7439
7596
 
7440
7597
 
7441
7598
  onProgramAccountChange(programId, callback, commitment, filters) {
7442
- const id = ++this._programAccountChangeSubscriptionCounter;
7443
- this._programAccountChangeSubscriptions[id] = {
7444
- programId: programId.toBase58(),
7599
+ const args = this._buildArgs([programId.toBase58()], commitment || this._commitment || 'finalized', // Apply connection/server default.
7600
+ 'base64'
7601
+ /* encoding */
7602
+ , filters ? {
7603
+ filters: filters
7604
+ } : undefined
7605
+ /* extra */
7606
+ );
7607
+
7608
+ return this._makeSubscription({
7445
7609
  callback,
7446
- commitment,
7447
- subscriptionId: null,
7448
- filters
7449
- };
7450
-
7451
- this._updateSubscriptions();
7452
-
7453
- return id;
7610
+ method: 'programSubscribe',
7611
+ unsubscribeMethod: 'programUnsubscribe'
7612
+ }, args);
7454
7613
  }
7455
7614
  /**
7456
7615
  * Deregister an account notification callback
7457
7616
  *
7458
- * @param id subscription id to deregister
7617
+ * @param id client subscription id to deregister
7459
7618
  */
7460
7619
 
7461
7620
 
7462
- async removeProgramAccountChangeListener(id) {
7463
- if (this._programAccountChangeSubscriptions[id]) {
7464
- const subInfo = this._programAccountChangeSubscriptions[id];
7465
- delete this._programAccountChangeSubscriptions[id];
7466
- await this._unsubscribe(subInfo, 'programUnsubscribe');
7467
-
7468
- this._updateSubscriptions();
7469
- } else {
7470
- console.warn(createSubscriptionWarningMessage(id, 'program account change'));
7471
- }
7621
+ async removeProgramAccountChangeListener(clientSubscriptionId) {
7622
+ await this._unsubscribeClientSubscription(clientSubscriptionId, 'program account change');
7472
7623
  }
7473
7624
  /**
7474
7625
  * Registers a callback to be invoked whenever logs are emitted.
@@ -7476,35 +7627,26 @@ class Connection {
7476
7627
 
7477
7628
 
7478
7629
  onLogs(filter, callback, commitment) {
7479
- const id = ++this._logsSubscriptionCounter;
7480
- this._logsSubscriptions[id] = {
7481
- filter,
7482
- callback,
7483
- commitment,
7484
- subscriptionId: null
7485
- };
7486
-
7487
- this._updateSubscriptions();
7630
+ const args = this._buildArgs([typeof filter === 'object' ? {
7631
+ mentions: [filter.toString()]
7632
+ } : filter], commitment || this._commitment || 'finalized' // Apply connection/server default.
7633
+ );
7488
7634
 
7489
- return id;
7635
+ return this._makeSubscription({
7636
+ callback,
7637
+ method: 'logsSubscribe',
7638
+ unsubscribeMethod: 'logsUnsubscribe'
7639
+ }, args);
7490
7640
  }
7491
7641
  /**
7492
7642
  * Deregister a logs callback.
7493
7643
  *
7494
- * @param id subscription id to deregister.
7644
+ * @param id client subscription id to deregister.
7495
7645
  */
7496
7646
 
7497
7647
 
7498
- async removeOnLogsListener(id) {
7499
- if (this._logsSubscriptions[id]) {
7500
- const subInfo = this._logsSubscriptions[id];
7501
- delete this._logsSubscriptions[id];
7502
- await this._unsubscribe(subInfo, 'logsUnsubscribe');
7503
-
7504
- this._updateSubscriptions();
7505
- } else {
7506
- console.warn(createSubscriptionWarningMessage(id, 'logs'));
7507
- }
7648
+ async removeOnLogsListener(clientSubscriptionId) {
7649
+ await this._unsubscribeClientSubscription(clientSubscriptionId, 'logs');
7508
7650
  }
7509
7651
  /**
7510
7652
  * @internal
@@ -7512,17 +7654,12 @@ class Connection {
7512
7654
 
7513
7655
 
7514
7656
  _wsOnLogsNotification(notification) {
7515
- const res = create(notification, LogsNotificationResult);
7516
- const keys = Object.keys(this._logsSubscriptions).map(Number);
7517
-
7518
- for (let id of keys) {
7519
- const sub = this._logsSubscriptions[id];
7657
+ const {
7658
+ result,
7659
+ subscription
7660
+ } = create(notification, LogsNotificationResult);
7520
7661
 
7521
- if (sub.subscriptionId === res.subscription) {
7522
- sub.callback(res.result.value, res.result.context);
7523
- return;
7524
- }
7525
- }
7662
+ this._handleServerNotification(subscription, [result.value, result.context]);
7526
7663
  }
7527
7664
  /**
7528
7665
  * @internal
@@ -7530,14 +7667,12 @@ class Connection {
7530
7667
 
7531
7668
 
7532
7669
  _wsOnSlotNotification(notification) {
7533
- const res = create(notification, SlotNotificationResult);
7670
+ const {
7671
+ result,
7672
+ subscription
7673
+ } = create(notification, SlotNotificationResult);
7534
7674
 
7535
- for (const sub of Object.values(this._slotSubscriptions)) {
7536
- if (sub.subscriptionId === res.subscription) {
7537
- sub.callback(res.result);
7538
- return;
7539
- }
7540
- }
7675
+ this._handleServerNotification(subscription, [result]);
7541
7676
  }
7542
7677
  /**
7543
7678
  * Register a callback to be invoked upon slot changes
@@ -7548,33 +7683,23 @@ class Connection {
7548
7683
 
7549
7684
 
7550
7685
  onSlotChange(callback) {
7551
- const id = ++this._slotSubscriptionCounter;
7552
- this._slotSubscriptions[id] = {
7686
+ return this._makeSubscription({
7553
7687
  callback,
7554
- subscriptionId: null
7555
- };
7556
-
7557
- this._updateSubscriptions();
7558
-
7559
- return id;
7688
+ method: 'slotSubscribe',
7689
+ unsubscribeMethod: 'slotUnsubscribe'
7690
+ }, []
7691
+ /* args */
7692
+ );
7560
7693
  }
7561
7694
  /**
7562
7695
  * Deregister a slot notification callback
7563
7696
  *
7564
- * @param id subscription id to deregister
7697
+ * @param id client subscription id to deregister
7565
7698
  */
7566
7699
 
7567
7700
 
7568
- async removeSlotChangeListener(id) {
7569
- if (this._slotSubscriptions[id]) {
7570
- const subInfo = this._slotSubscriptions[id];
7571
- delete this._slotSubscriptions[id];
7572
- await this._unsubscribe(subInfo, 'slotUnsubscribe');
7573
-
7574
- this._updateSubscriptions();
7575
- } else {
7576
- console.warn(createSubscriptionWarningMessage(id, 'slot change'));
7577
- }
7701
+ async removeSlotChangeListener(clientSubscriptionId) {
7702
+ await this._unsubscribeClientSubscription(clientSubscriptionId, 'slot change');
7578
7703
  }
7579
7704
  /**
7580
7705
  * @internal
@@ -7582,14 +7707,12 @@ class Connection {
7582
7707
 
7583
7708
 
7584
7709
  _wsOnSlotUpdatesNotification(notification) {
7585
- const res = create(notification, SlotUpdateNotificationResult);
7710
+ const {
7711
+ result,
7712
+ subscription
7713
+ } = create(notification, SlotUpdateNotificationResult);
7586
7714
 
7587
- for (const sub of Object.values(this._slotUpdateSubscriptions)) {
7588
- if (sub.subscriptionId === res.subscription) {
7589
- sub.callback(res.result);
7590
- return;
7591
- }
7592
- }
7715
+ this._handleServerNotification(subscription, [result]);
7593
7716
  }
7594
7717
  /**
7595
7718
  * Register a callback to be invoked upon slot updates. {@link SlotUpdate}'s
@@ -7601,32 +7724,36 @@ class Connection {
7601
7724
 
7602
7725
 
7603
7726
  onSlotUpdate(callback) {
7604
- const id = ++this._slotUpdateSubscriptionCounter;
7605
- this._slotUpdateSubscriptions[id] = {
7727
+ return this._makeSubscription({
7606
7728
  callback,
7607
- subscriptionId: null
7608
- };
7609
-
7610
- this._updateSubscriptions();
7611
-
7612
- return id;
7729
+ method: 'slotsUpdatesSubscribe',
7730
+ unsubscribeMethod: 'slotsUpdatesUnsubscribe'
7731
+ }, []
7732
+ /* args */
7733
+ );
7613
7734
  }
7614
7735
  /**
7615
7736
  * Deregister a slot update notification callback
7616
7737
  *
7617
- * @param id subscription id to deregister
7738
+ * @param id client subscription id to deregister
7618
7739
  */
7619
7740
 
7620
7741
 
7621
- async removeSlotUpdateListener(id) {
7622
- if (this._slotUpdateSubscriptions[id]) {
7623
- const subInfo = this._slotUpdateSubscriptions[id];
7624
- delete this._slotUpdateSubscriptions[id];
7625
- await this._unsubscribe(subInfo, 'slotsUpdatesUnsubscribe');
7742
+ async removeSlotUpdateListener(clientSubscriptionId) {
7743
+ await this._unsubscribeClientSubscription(clientSubscriptionId, 'slot update');
7744
+ }
7745
+ /**
7746
+ * @internal
7747
+ */
7626
7748
 
7627
- this._updateSubscriptions();
7749
+
7750
+ async _unsubscribeClientSubscription(clientSubscriptionId, subscriptionName) {
7751
+ const dispose = this._subscriptionDisposeFunctionsByClientSubscriptionId[clientSubscriptionId];
7752
+
7753
+ if (dispose) {
7754
+ await dispose();
7628
7755
  } else {
7629
- console.warn(createSubscriptionWarningMessage(id, 'slot update'));
7756
+ console.warn('Ignored unsubscribe request because an active subscription with id ' + `\`${clientSubscriptionId}\` for '${subscriptionName}' events ` + 'could not be found.');
7630
7757
  }
7631
7758
  }
7632
7759
 
@@ -7673,30 +7800,34 @@ class Connection {
7673
7800
 
7674
7801
 
7675
7802
  _wsOnSignatureNotification(notification) {
7676
- const res = create(notification, SignatureNotificationResult);
7677
-
7678
- for (const [id, sub] of Object.entries(this._signatureSubscriptions)) {
7679
- if (sub.subscriptionId === res.subscription) {
7680
- if (res.result.value === 'receivedSignature') {
7681
- sub.callback({
7682
- type: 'received'
7683
- }, res.result.context);
7684
- } else {
7685
- // Signatures subscriptions are auto-removed by the RPC service so
7686
- // no need to explicitly send an unsubscribe message
7687
- delete this._signatureSubscriptions[Number(id)];
7688
-
7689
- this._updateSubscriptions();
7690
-
7691
- sub.callback({
7692
- type: 'status',
7693
- result: res.result.value
7694
- }, res.result.context);
7695
- }
7696
-
7697
- return;
7698
- }
7699
- }
7803
+ const {
7804
+ result,
7805
+ subscription
7806
+ } = create(notification, SignatureNotificationResult);
7807
+
7808
+ if (result.value !== 'receivedSignature') {
7809
+ /**
7810
+ * Special case.
7811
+ * After a signature is processed, RPCs automatically dispose of the
7812
+ * subscription on the server side. We need to track which of these
7813
+ * subscriptions have been disposed in such a way, so that we know
7814
+ * whether the client is dealing with a not-yet-processed signature
7815
+ * (in which case we must tear down the server subscription) or an
7816
+ * already-processed signature (in which case the client can simply
7817
+ * clear out the subscription locally without telling the server).
7818
+ *
7819
+ * NOTE: There is a proposal to eliminate this special case, here:
7820
+ * https://github.com/solana-labs/solana/issues/18892
7821
+ */
7822
+ this._subscriptionsAutoDisposedByRpc.add(subscription);
7823
+ }
7824
+
7825
+ this._handleServerNotification(subscription, result.value === 'receivedSignature' ? [{
7826
+ type: 'received'
7827
+ }, result.context] : [{
7828
+ type: 'status',
7829
+ result: result.value
7830
+ }, result.context]);
7700
7831
  }
7701
7832
  /**
7702
7833
  * Register a callback to be invoked upon signature updates
@@ -7709,23 +7840,26 @@ class Connection {
7709
7840
 
7710
7841
 
7711
7842
  onSignature(signature, callback, commitment) {
7712
- const id = ++this._signatureSubscriptionCounter;
7713
- this._signatureSubscriptions[id] = {
7714
- signature,
7843
+ const args = this._buildArgs([signature], commitment || this._commitment || 'finalized' // Apply connection/server default.
7844
+ );
7845
+
7846
+ const clientSubscriptionId = this._makeSubscription({
7715
7847
  callback: (notification, context) => {
7716
7848
  if (notification.type === 'status') {
7717
- callback(notification.result, context);
7849
+ callback(notification.result, context); // Signatures subscriptions are auto-removed by the RPC service
7850
+ // so no need to explicitly send an unsubscribe message.
7851
+
7852
+ try {
7853
+ this.removeSignatureListener(clientSubscriptionId); // eslint-disable-next-line no-empty
7854
+ } catch {// Already removed.
7855
+ }
7718
7856
  }
7719
7857
  },
7720
- options: {
7721
- commitment
7722
- },
7723
- subscriptionId: null
7724
- };
7858
+ method: 'signatureSubscribe',
7859
+ unsubscribeMethod: 'signatureUnsubscribe'
7860
+ }, args);
7725
7861
 
7726
- this._updateSubscriptions();
7727
-
7728
- return id;
7862
+ return clientSubscriptionId;
7729
7863
  }
7730
7864
  /**
7731
7865
  * Register a callback to be invoked when a transaction is
@@ -7740,35 +7874,43 @@ class Connection {
7740
7874
 
7741
7875
 
7742
7876
  onSignatureWithOptions(signature, callback, options) {
7743
- const id = ++this._signatureSubscriptionCounter;
7744
- this._signatureSubscriptions[id] = {
7745
- signature,
7746
- callback,
7747
- options,
7748
- subscriptionId: null
7877
+ const {
7878
+ commitment,
7879
+ ...extra
7880
+ } = { ...options,
7881
+ commitment: options && options.commitment || this._commitment || 'finalized' // Apply connection/server default.
7882
+
7749
7883
  };
7750
7884
 
7751
- this._updateSubscriptions();
7885
+ const args = this._buildArgs([signature], commitment, undefined
7886
+ /* encoding */
7887
+ , extra);
7752
7888
 
7753
- return id;
7889
+ const clientSubscriptionId = this._makeSubscription({
7890
+ callback: (notification, context) => {
7891
+ callback(notification, context); // Signatures subscriptions are auto-removed by the RPC service
7892
+ // so no need to explicitly send an unsubscribe message.
7893
+
7894
+ try {
7895
+ this.removeSignatureListener(clientSubscriptionId); // eslint-disable-next-line no-empty
7896
+ } catch {// Already removed.
7897
+ }
7898
+ },
7899
+ method: 'signatureSubscribe',
7900
+ unsubscribeMethod: 'signatureUnsubscribe'
7901
+ }, args);
7902
+
7903
+ return clientSubscriptionId;
7754
7904
  }
7755
7905
  /**
7756
7906
  * Deregister a signature notification callback
7757
7907
  *
7758
- * @param id subscription id to deregister
7908
+ * @param id client subscription id to deregister
7759
7909
  */
7760
7910
 
7761
7911
 
7762
- async removeSignatureListener(id) {
7763
- if (this._signatureSubscriptions[id]) {
7764
- const subInfo = this._signatureSubscriptions[id];
7765
- delete this._signatureSubscriptions[id];
7766
- await this._unsubscribe(subInfo, 'signatureUnsubscribe');
7767
-
7768
- this._updateSubscriptions();
7769
- } else {
7770
- console.warn(createSubscriptionWarningMessage(id, 'signature result'));
7771
- }
7912
+ async removeSignatureListener(clientSubscriptionId) {
7913
+ await this._unsubscribeClientSubscription(clientSubscriptionId, 'signature result');
7772
7914
  }
7773
7915
  /**
7774
7916
  * @internal
@@ -7776,14 +7918,12 @@ class Connection {
7776
7918
 
7777
7919
 
7778
7920
  _wsOnRootNotification(notification) {
7779
- const res = create(notification, RootNotificationResult);
7921
+ const {
7922
+ result,
7923
+ subscription
7924
+ } = create(notification, RootNotificationResult);
7780
7925
 
7781
- for (const sub of Object.values(this._rootSubscriptions)) {
7782
- if (sub.subscriptionId === res.subscription) {
7783
- sub.callback(res.result);
7784
- return;
7785
- }
7786
- }
7926
+ this._handleServerNotification(subscription, [result]);
7787
7927
  }
7788
7928
  /**
7789
7929
  * Register a callback to be invoked upon root changes
@@ -7794,33 +7934,23 @@ class Connection {
7794
7934
 
7795
7935
 
7796
7936
  onRootChange(callback) {
7797
- const id = ++this._rootSubscriptionCounter;
7798
- this._rootSubscriptions[id] = {
7937
+ return this._makeSubscription({
7799
7938
  callback,
7800
- subscriptionId: null
7801
- };
7802
-
7803
- this._updateSubscriptions();
7804
-
7805
- return id;
7939
+ method: 'rootSubscribe',
7940
+ unsubscribeMethod: 'rootUnsubscribe'
7941
+ }, []
7942
+ /* args */
7943
+ );
7806
7944
  }
7807
7945
  /**
7808
7946
  * Deregister a root notification callback
7809
7947
  *
7810
- * @param id subscription id to deregister
7948
+ * @param id client subscription id to deregister
7811
7949
  */
7812
7950
 
7813
7951
 
7814
- async removeRootChangeListener(id) {
7815
- if (this._rootSubscriptions[id]) {
7816
- const subInfo = this._rootSubscriptions[id];
7817
- delete this._rootSubscriptions[id];
7818
- await this._unsubscribe(subInfo, 'rootUnsubscribe');
7819
-
7820
- this._updateSubscriptions();
7821
- } else {
7822
- console.warn(createSubscriptionWarningMessage(id, 'root change'));
7823
- }
7952
+ async removeRootChangeListener(clientSubscriptionId) {
7953
+ await this._unsubscribeClientSubscription(clientSubscriptionId, 'root change');
7824
7954
  }
7825
7955
 
7826
7956
  }