@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.
@@ -4793,6 +4793,82 @@ module.exports = exports;
4793
4793
 
4794
4794
  var crossFetch = /*@__PURE__*/getDefaultExportFromCjs(browserPonyfill.exports);
4795
4795
 
4796
+ var objToString = Object.prototype.toString;
4797
+ var objKeys = Object.keys || function(obj) {
4798
+ var keys = [];
4799
+ for (var name in obj) {
4800
+ keys.push(name);
4801
+ }
4802
+ return keys;
4803
+ };
4804
+
4805
+ function stringify(val, isArrayProp) {
4806
+ var i, max, str, keys, key, propVal, toStr;
4807
+ if (val === true) {
4808
+ return "true";
4809
+ }
4810
+ if (val === false) {
4811
+ return "false";
4812
+ }
4813
+ switch (typeof val) {
4814
+ case "object":
4815
+ if (val === null) {
4816
+ return null;
4817
+ } else if (val.toJSON && typeof val.toJSON === "function") {
4818
+ return stringify(val.toJSON(), isArrayProp);
4819
+ } else {
4820
+ toStr = objToString.call(val);
4821
+ if (toStr === "[object Array]") {
4822
+ str = '[';
4823
+ max = val.length - 1;
4824
+ for(i = 0; i < max; i++) {
4825
+ str += stringify(val[i], true) + ',';
4826
+ }
4827
+ if (max > -1) {
4828
+ str += stringify(val[i], true);
4829
+ }
4830
+ return str + ']';
4831
+ } else if (toStr === "[object Object]") {
4832
+ // only object is left
4833
+ keys = objKeys(val).sort();
4834
+ max = keys.length;
4835
+ str = "";
4836
+ i = 0;
4837
+ while (i < max) {
4838
+ key = keys[i];
4839
+ propVal = stringify(val[key], false);
4840
+ if (propVal !== undefined) {
4841
+ if (str) {
4842
+ str += ',';
4843
+ }
4844
+ str += JSON.stringify(key) + ':' + propVal;
4845
+ }
4846
+ i++;
4847
+ }
4848
+ return '{' + str + '}';
4849
+ } else {
4850
+ return JSON.stringify(val);
4851
+ }
4852
+ }
4853
+ case "function":
4854
+ case "undefined":
4855
+ return isArrayProp ? null : undefined;
4856
+ case "string":
4857
+ return JSON.stringify(val);
4858
+ default:
4859
+ return isFinite(val) ? val : null;
4860
+ }
4861
+ }
4862
+
4863
+ var fastStableStringify = function(val) {
4864
+ var returnVal = stringify(val, false);
4865
+ if (returnVal !== undefined) {
4866
+ return ''+ returnVal;
4867
+ }
4868
+ };
4869
+
4870
+ var fastStableStringify$1 = fastStableStringify;
4871
+
4796
4872
  const MINIMUM_SLOT_PER_EPOCH = 32; // Returns the number of trailing zeros in the binary representation of self.
4797
4873
 
4798
4874
  function trailingZeros(n) {
@@ -4959,6 +5035,12 @@ const BufferFromRawAccountData = superstruct.coerce(superstruct.instance(buffer.
4959
5035
  */
4960
5036
 
4961
5037
  const BLOCKHASH_CACHE_TIMEOUT_MS = 30 * 1000;
5038
+ /**
5039
+ * HACK.
5040
+ * Copied from rpc-websockets/dist/lib/client.
5041
+ * Otherwise, `yarn build` fails with:
5042
+ * https://gist.github.com/steveluscher/c057eca81d479ef705cdb53162f9971d
5043
+ */
4962
5044
 
4963
5045
  /**
4964
5046
  * @internal
@@ -5827,14 +5909,9 @@ const LogsNotificationResult = superstruct.type({
5827
5909
  * Filter for log subscriptions.
5828
5910
  */
5829
5911
 
5830
- function createSubscriptionWarningMessage(id, label) {
5831
- return 'Ignored unsubscribe request because an active subscription ' + `with id \`${id}\` for '${label}' events could not be found.`;
5832
- }
5833
5912
  /**
5834
5913
  * A connection to a fullnode JSON RPC endpoint
5835
5914
  */
5836
-
5837
-
5838
5915
  class Connection {
5839
5916
  /** @internal */
5840
5917
 
@@ -5858,21 +5935,13 @@ class Connection {
5858
5935
 
5859
5936
  /** @internal */
5860
5937
 
5861
- /** @internal */
5862
-
5863
- /** @internal */
5864
-
5865
- /** @internal */
5866
-
5867
- /** @internal */
5868
-
5869
- /** @internal */
5870
-
5871
- /** @internal */
5872
-
5873
- /** @internal */
5874
-
5875
- /** @internal */
5938
+ /** @internal
5939
+ * A number that we increment every time an active connection closes.
5940
+ * Used to determine whether the same socket connection that was open
5941
+ * when an async operation started is the same one that's active when
5942
+ * its continuation fires.
5943
+ *
5944
+ */
5876
5945
 
5877
5946
  /** @internal */
5878
5947
 
@@ -5888,7 +5957,19 @@ class Connection {
5888
5957
 
5889
5958
  /** @internal */
5890
5959
 
5891
- /** @internal */
5960
+ /**
5961
+ * Special case.
5962
+ * After a signature is processed, RPCs automatically dispose of the
5963
+ * subscription on the server side. We need to track which of these
5964
+ * subscriptions have been disposed in such a way, so that we know
5965
+ * whether the client is dealing with a not-yet-processed signature
5966
+ * (in which case we must tear down the server subscription) or an
5967
+ * already-processed signature (in which case the client can simply
5968
+ * clear out the subscription locally without telling the server).
5969
+ *
5970
+ * NOTE: There is a proposal to eliminate this special case, here:
5971
+ * https://github.com/solana-labs/solana/issues/18892
5972
+ */
5892
5973
 
5893
5974
  /** @internal */
5894
5975
 
@@ -5910,6 +5991,7 @@ class Connection {
5910
5991
  this._rpcWebSocketConnected = false;
5911
5992
  this._rpcWebSocketHeartbeat = null;
5912
5993
  this._rpcWebSocketIdleTimeout = null;
5994
+ this._rpcWebSocketGeneration = 0;
5913
5995
  this._disableBlockhashCaching = false;
5914
5996
  this._pollingBlockhash = false;
5915
5997
  this._blockhashInfo = {
@@ -5918,20 +6000,11 @@ class Connection {
5918
6000
  transactionSignatures: [],
5919
6001
  simulatedSignatures: []
5920
6002
  };
5921
- this._accountChangeSubscriptionCounter = 0;
5922
- this._accountChangeSubscriptions = {};
5923
- this._programAccountChangeSubscriptionCounter = 0;
5924
- this._programAccountChangeSubscriptions = {};
5925
- this._rootSubscriptionCounter = 0;
5926
- this._rootSubscriptions = {};
5927
- this._signatureSubscriptionCounter = 0;
5928
- this._signatureSubscriptions = {};
5929
- this._slotSubscriptionCounter = 0;
5930
- this._slotSubscriptions = {};
5931
- this._logsSubscriptionCounter = 0;
5932
- this._logsSubscriptions = {};
5933
- this._slotUpdateSubscriptionCounter = 0;
5934
- this._slotUpdateSubscriptions = {};
6003
+ this._nextClientSubscriptionId = 0;
6004
+ this._subscriptionDisposeFunctionsByClientSubscriptionId = {};
6005
+ this._subscriptionCallbacksByServerSubscriptionId = {};
6006
+ this._subscriptionsByHash = {};
6007
+ this._subscriptionsAutoDisposedByRpc = new Set();
5935
6008
  let url = new URL(endpoint);
5936
6009
  const useHttps = url.protocol === 'https:';
5937
6010
  let wsEndpoint;
@@ -7699,6 +7772,8 @@ class Connection {
7699
7772
 
7700
7773
 
7701
7774
  _wsOnClose(code) {
7775
+ this._rpcWebSocketGeneration++;
7776
+
7702
7777
  if (this._rpcWebSocketHeartbeat) {
7703
7778
  clearInterval(this._rpcWebSocketHeartbeat);
7704
7779
  this._rpcWebSocketHeartbeat = null;
@@ -7712,85 +7787,20 @@ class Connection {
7712
7787
  } // implicit close, prepare subscriptions for auto-reconnect
7713
7788
 
7714
7789
 
7715
- this._resetSubscriptions();
7716
- }
7717
- /**
7718
- * @internal
7719
- */
7720
-
7721
-
7722
- async _subscribe(sub, rpcMethod, rpcArgs) {
7723
- if (sub.subscriptionId == null) {
7724
- sub.subscriptionId = 'subscribing';
7725
-
7726
- try {
7727
- const id = await this._rpcWebSocket.call(rpcMethod, rpcArgs);
7728
-
7729
- if (typeof id === 'number' && sub.subscriptionId === 'subscribing') {
7730
- // eslint-disable-next-line require-atomic-updates
7731
- sub.subscriptionId = id;
7732
- }
7733
- } catch (err) {
7734
- if (sub.subscriptionId === 'subscribing') {
7735
- // eslint-disable-next-line require-atomic-updates
7736
- sub.subscriptionId = null;
7737
- }
7738
-
7739
- if (err instanceof Error) {
7740
- console.error(`${rpcMethod} error for argument`, rpcArgs, err.message);
7741
- }
7742
- }
7743
- }
7744
- }
7745
- /**
7746
- * @internal
7747
- */
7748
-
7749
-
7750
- async _unsubscribe(sub, rpcMethod) {
7751
- const subscriptionId = sub.subscriptionId;
7752
-
7753
- if (subscriptionId != null && typeof subscriptionId != 'string') {
7754
- const unsubscribeId = subscriptionId;
7755
-
7756
- try {
7757
- await this._rpcWebSocket.call(rpcMethod, [unsubscribeId]);
7758
- } catch (err) {
7759
- if (err instanceof Error) {
7760
- console.error(`${rpcMethod} error:`, err.message);
7761
- }
7762
- }
7763
- }
7764
- }
7765
- /**
7766
- * @internal
7767
- */
7768
-
7769
-
7770
- _resetSubscriptions() {
7771
- Object.values(this._accountChangeSubscriptions).forEach(s => s.subscriptionId = null);
7772
- Object.values(this._logsSubscriptions).forEach(s => s.subscriptionId = null);
7773
- Object.values(this._programAccountChangeSubscriptions).forEach(s => s.subscriptionId = null);
7774
- Object.values(this._rootSubscriptions).forEach(s => s.subscriptionId = null);
7775
- Object.values(this._signatureSubscriptions).forEach(s => s.subscriptionId = null);
7776
- Object.values(this._slotSubscriptions).forEach(s => s.subscriptionId = null);
7777
- Object.values(this._slotUpdateSubscriptions).forEach(s => s.subscriptionId = null);
7790
+ this._subscriptionCallbacksByServerSubscriptionId = {};
7791
+ Object.entries(this._subscriptionsByHash).forEach(([hash, subscription]) => {
7792
+ this._subscriptionsByHash[hash] = { ...subscription,
7793
+ state: 'pending'
7794
+ };
7795
+ });
7778
7796
  }
7779
7797
  /**
7780
7798
  * @internal
7781
7799
  */
7782
7800
 
7783
7801
 
7784
- _updateSubscriptions() {
7785
- const accountKeys = Object.keys(this._accountChangeSubscriptions).map(Number);
7786
- const programKeys = Object.keys(this._programAccountChangeSubscriptions).map(Number);
7787
- const slotKeys = Object.keys(this._slotSubscriptions).map(Number);
7788
- const slotUpdateKeys = Object.keys(this._slotUpdateSubscriptions).map(Number);
7789
- const signatureKeys = Object.keys(this._signatureSubscriptions).map(Number);
7790
- const rootKeys = Object.keys(this._rootSubscriptions).map(Number);
7791
- const logsKeys = Object.keys(this._logsSubscriptions).map(Number);
7792
-
7793
- if (accountKeys.length === 0 && programKeys.length === 0 && slotKeys.length === 0 && slotUpdateKeys.length === 0 && signatureKeys.length === 0 && rootKeys.length === 0 && logsKeys.length === 0) {
7802
+ async _updateSubscriptions() {
7803
+ if (Object.keys(this._subscriptionsByHash).length === 0) {
7794
7804
  if (this._rpcWebSocketConnected) {
7795
7805
  this._rpcWebSocketConnected = false;
7796
7806
  this._rpcWebSocketIdleTimeout = setTimeout(() => {
@@ -7822,60 +7832,167 @@ class Connection {
7822
7832
  return;
7823
7833
  }
7824
7834
 
7825
- for (let id of accountKeys) {
7826
- const sub = this._accountChangeSubscriptions[id];
7827
-
7828
- this._subscribe(sub, 'accountSubscribe', this._buildArgs([sub.publicKey], sub.commitment, 'base64'));
7829
- }
7835
+ const activeWebSocketGeneration = this._rpcWebSocketGeneration;
7830
7836
 
7831
- for (let id of programKeys) {
7832
- const sub = this._programAccountChangeSubscriptions[id];
7837
+ const isCurrentConnectionStillActive = () => {
7838
+ return activeWebSocketGeneration === this._rpcWebSocketGeneration;
7839
+ };
7833
7840
 
7834
- this._subscribe(sub, 'programSubscribe', this._buildArgs([sub.programId], sub.commitment, 'base64', {
7835
- filters: sub.filters
7836
- }));
7837
- }
7841
+ await Promise.all( // Don't be tempted to change this to `Object.entries`. We call
7842
+ // `_updateSubscriptions` recursively when processing the state,
7843
+ // so it's important that we look up the *current* version of
7844
+ // each subscription, every time we process a hash.
7845
+ Object.keys(this._subscriptionsByHash).map(async hash => {
7846
+ const subscription = this._subscriptionsByHash[hash];
7838
7847
 
7839
- for (let id of slotKeys) {
7840
- const sub = this._slotSubscriptions[id];
7848
+ if (subscription === undefined) {
7849
+ // This entry has since been deleted. Skip.
7850
+ return;
7851
+ }
7841
7852
 
7842
- this._subscribe(sub, 'slotSubscribe', []);
7843
- }
7853
+ switch (subscription.state) {
7854
+ case 'pending':
7855
+ case 'unsubscribed':
7856
+ if (subscription.callbacks.size === 0) {
7857
+ /**
7858
+ * You can end up here when:
7859
+ *
7860
+ * - a subscription has recently unsubscribed
7861
+ * without having new callbacks added to it
7862
+ * while the unsubscribe was in flight, or
7863
+ * - when a pending subscription has its
7864
+ * listeners removed before a request was
7865
+ * sent to the server.
7866
+ *
7867
+ * Being that nobody is interested in this
7868
+ * subscription any longer, delete it.
7869
+ */
7870
+ delete this._subscriptionsByHash[hash];
7871
+
7872
+ if (subscription.state === 'unsubscribed') {
7873
+ delete this._subscriptionCallbacksByServerSubscriptionId[subscription.serverSubscriptionId];
7874
+ }
7844
7875
 
7845
- for (let id of slotUpdateKeys) {
7846
- const sub = this._slotUpdateSubscriptions[id];
7876
+ await this._updateSubscriptions();
7877
+ return;
7878
+ }
7847
7879
 
7848
- this._subscribe(sub, 'slotsUpdatesSubscribe', []);
7849
- }
7880
+ await (async () => {
7881
+ const {
7882
+ args,
7883
+ method
7884
+ } = subscription;
7850
7885
 
7851
- for (let id of signatureKeys) {
7852
- const sub = this._signatureSubscriptions[id];
7853
- const args = [sub.signature];
7854
- if (sub.options) args.push(sub.options);
7886
+ try {
7887
+ this._subscriptionsByHash[hash] = { ...subscription,
7888
+ state: 'subscribing'
7889
+ };
7890
+ const serverSubscriptionId = await this._rpcWebSocket.call(method, args);
7891
+ this._subscriptionsByHash[hash] = { ...subscription,
7892
+ serverSubscriptionId,
7893
+ state: 'subscribed'
7894
+ };
7895
+ this._subscriptionCallbacksByServerSubscriptionId[serverSubscriptionId] = subscription.callbacks;
7896
+ await this._updateSubscriptions();
7897
+ } catch (e) {
7898
+ if (e instanceof Error) {
7899
+ console.error(`${method} error for argument`, args, e.message);
7900
+ }
7901
+
7902
+ if (!isCurrentConnectionStillActive()) {
7903
+ return;
7904
+ } // TODO: Maybe add an 'errored' state or a retry limit?
7855
7905
 
7856
- this._subscribe(sub, 'signatureSubscribe', args);
7857
- }
7858
7906
 
7859
- for (let id of rootKeys) {
7860
- const sub = this._rootSubscriptions[id];
7907
+ this._subscriptionsByHash[hash] = { ...subscription,
7908
+ state: 'pending'
7909
+ };
7910
+ await this._updateSubscriptions();
7911
+ }
7912
+ })();
7913
+ break;
7861
7914
 
7862
- this._subscribe(sub, 'rootSubscribe', []);
7863
- }
7915
+ case 'subscribed':
7916
+ if (subscription.callbacks.size === 0) {
7917
+ // By the time we successfully set up a subscription
7918
+ // with the server, the client stopped caring about it.
7919
+ // Tear it down now.
7920
+ await (async () => {
7921
+ const {
7922
+ serverSubscriptionId,
7923
+ unsubscribeMethod
7924
+ } = subscription;
7925
+
7926
+ if (this._subscriptionsAutoDisposedByRpc.has(serverSubscriptionId)) {
7927
+ /**
7928
+ * Special case.
7929
+ * If we're dealing with a subscription that has been auto-
7930
+ * disposed by the RPC, then we can skip the RPC call to
7931
+ * tear down the subscription here.
7932
+ *
7933
+ * NOTE: There is a proposal to eliminate this special case, here:
7934
+ * https://github.com/solana-labs/solana/issues/18892
7935
+ */
7936
+ this._subscriptionsAutoDisposedByRpc.delete(serverSubscriptionId);
7937
+ } else {
7938
+ this._subscriptionsByHash[hash] = { ...subscription,
7939
+ state: 'unsubscribing'
7940
+ };
7941
+
7942
+ try {
7943
+ await this._rpcWebSocket.call(unsubscribeMethod, [serverSubscriptionId]);
7944
+ } catch (e) {
7945
+ if (e instanceof Error) {
7946
+ console.error(`${unsubscribeMethod} error:`, e.message);
7947
+ }
7948
+
7949
+ if (!isCurrentConnectionStillActive()) {
7950
+ return;
7951
+ } // TODO: Maybe add an 'errored' state or a retry limit?
7952
+
7953
+
7954
+ this._subscriptionsByHash[hash] = { ...subscription,
7955
+ state: 'subscribed'
7956
+ };
7957
+ await this._updateSubscriptions();
7958
+ return;
7959
+ }
7960
+ }
7864
7961
 
7865
- for (let id of logsKeys) {
7866
- const sub = this._logsSubscriptions[id];
7867
- let filter;
7962
+ this._subscriptionsByHash[hash] = { ...subscription,
7963
+ state: 'unsubscribed'
7964
+ };
7965
+ await this._updateSubscriptions();
7966
+ })();
7967
+ }
7868
7968
 
7869
- if (typeof sub.filter === 'object') {
7870
- filter = {
7871
- mentions: [sub.filter.toString()]
7872
- };
7873
- } else {
7874
- filter = sub.filter;
7969
+ break;
7875
7970
  }
7971
+ }));
7972
+ }
7973
+ /**
7974
+ * @internal
7975
+ */
7876
7976
 
7877
- this._subscribe(sub, 'logsSubscribe', this._buildArgs([filter], sub.commitment));
7977
+
7978
+ _handleServerNotification(serverSubscriptionId, callbackArgs) {
7979
+ const callbacks = this._subscriptionCallbacksByServerSubscriptionId[serverSubscriptionId];
7980
+
7981
+ if (callbacks === undefined) {
7982
+ return;
7878
7983
  }
7984
+
7985
+ callbacks.forEach(cb => {
7986
+ try {
7987
+ cb( // I failed to find a way to convince TypeScript that `cb` is of type
7988
+ // `TCallback` which is certainly compatible with `Parameters<TCallback>`.
7989
+ // See https://github.com/microsoft/TypeScript/issues/47615
7990
+ // @ts-ignore
7991
+ ...callbackArgs);
7992
+ } catch (e) {
7993
+ console.error(e);
7994
+ }
7995
+ });
7879
7996
  }
7880
7997
  /**
7881
7998
  * @internal
@@ -7883,14 +8000,71 @@ class Connection {
7883
8000
 
7884
8001
 
7885
8002
  _wsOnAccountNotification(notification) {
7886
- const res = superstruct.create(notification, AccountNotificationResult);
8003
+ const {
8004
+ result,
8005
+ subscription
8006
+ } = superstruct.create(notification, AccountNotificationResult);
7887
8007
 
7888
- for (const sub of Object.values(this._accountChangeSubscriptions)) {
7889
- if (sub.subscriptionId === res.subscription) {
7890
- sub.callback(res.result.value, res.result.context);
7891
- return;
7892
- }
8008
+ this._handleServerNotification(subscription, [result.value, result.context]);
8009
+ }
8010
+ /**
8011
+ * @internal
8012
+ */
8013
+
8014
+
8015
+ _makeSubscription(subscriptionConfig,
8016
+ /**
8017
+ * When preparing `args` for a call to `_makeSubscription`, be sure
8018
+ * to carefully apply a default `commitment` property, if necessary.
8019
+ *
8020
+ * - If the user supplied a `commitment` use that.
8021
+ * - Otherwise, if the `Connection::commitment` is set, use that.
8022
+ * - Otherwise, set it to the RPC server default: `finalized`.
8023
+ *
8024
+ * This is extremely important to ensure that these two fundamentally
8025
+ * identical subscriptions produce the same identifying hash:
8026
+ *
8027
+ * - A subscription made without specifying a commitment.
8028
+ * - A subscription made where the commitment specified is the same
8029
+ * as the default applied to the subscription above.
8030
+ *
8031
+ * Example; these two subscriptions must produce the same hash:
8032
+ *
8033
+ * - An `accountSubscribe` subscription for `'PUBKEY'`
8034
+ * - An `accountSubscribe` subscription for `'PUBKEY'` with commitment
8035
+ * `'finalized'`.
8036
+ *
8037
+ * See the 'making a subscription with defaulted params omitted' test
8038
+ * in `connection-subscriptions.ts` for more.
8039
+ */
8040
+ args) {
8041
+ const clientSubscriptionId = this._nextClientSubscriptionId++;
8042
+ const hash = fastStableStringify$1([subscriptionConfig.method, args], true
8043
+ /* isArrayProp */
8044
+ );
8045
+ const existingSubscription = this._subscriptionsByHash[hash];
8046
+
8047
+ if (existingSubscription === undefined) {
8048
+ this._subscriptionsByHash[hash] = { ...subscriptionConfig,
8049
+ args,
8050
+ callbacks: new Set([subscriptionConfig.callback]),
8051
+ state: 'pending'
8052
+ };
8053
+ } else {
8054
+ existingSubscription.callbacks.add(subscriptionConfig.callback);
7893
8055
  }
8056
+
8057
+ this._subscriptionDisposeFunctionsByClientSubscriptionId[clientSubscriptionId] = async () => {
8058
+ delete this._subscriptionDisposeFunctionsByClientSubscriptionId[clientSubscriptionId];
8059
+ const subscription = this._subscriptionsByHash[hash];
8060
+ assert(subscription !== undefined, `Could not find a \`Subscription\` when tearing down client subscription #${clientSubscriptionId}`);
8061
+ subscription.callbacks.delete(subscriptionConfig.callback);
8062
+ await this._updateSubscriptions();
8063
+ };
8064
+
8065
+ this._updateSubscriptions();
8066
+
8067
+ return clientSubscriptionId;
7894
8068
  }
7895
8069
  /**
7896
8070
  * Register a callback to be invoked whenever the specified account changes
@@ -7903,35 +8077,24 @@ class Connection {
7903
8077
 
7904
8078
 
7905
8079
  onAccountChange(publicKey, callback, commitment) {
7906
- const id = ++this._accountChangeSubscriptionCounter;
7907
- this._accountChangeSubscriptions[id] = {
7908
- publicKey: publicKey.toBase58(),
7909
- callback,
7910
- commitment,
7911
- subscriptionId: null
7912
- };
8080
+ const args = this._buildArgs([publicKey.toBase58()], commitment || this._commitment || 'finalized', // Apply connection/server default.
8081
+ 'base64');
7913
8082
 
7914
- this._updateSubscriptions();
7915
-
7916
- return id;
8083
+ return this._makeSubscription({
8084
+ callback,
8085
+ method: 'accountSubscribe',
8086
+ unsubscribeMethod: 'accountUnsubscribe'
8087
+ }, args);
7917
8088
  }
7918
8089
  /**
7919
8090
  * Deregister an account notification callback
7920
8091
  *
7921
- * @param id subscription id to deregister
8092
+ * @param id client subscription id to deregister
7922
8093
  */
7923
8094
 
7924
8095
 
7925
- async removeAccountChangeListener(id) {
7926
- if (this._accountChangeSubscriptions[id]) {
7927
- const subInfo = this._accountChangeSubscriptions[id];
7928
- delete this._accountChangeSubscriptions[id];
7929
- await this._unsubscribe(subInfo, 'accountUnsubscribe');
7930
-
7931
- this._updateSubscriptions();
7932
- } else {
7933
- console.warn(createSubscriptionWarningMessage(id, 'account change'));
7934
- }
8096
+ async removeAccountChangeListener(clientSubscriptionId) {
8097
+ await this._unsubscribeClientSubscription(clientSubscriptionId, 'account change');
7935
8098
  }
7936
8099
  /**
7937
8100
  * @internal
@@ -7939,21 +8102,15 @@ class Connection {
7939
8102
 
7940
8103
 
7941
8104
  _wsOnProgramAccountNotification(notification) {
7942
- const res = superstruct.create(notification, ProgramAccountNotificationResult);
8105
+ const {
8106
+ result,
8107
+ subscription
8108
+ } = superstruct.create(notification, ProgramAccountNotificationResult);
7943
8109
 
7944
- for (const sub of Object.values(this._programAccountChangeSubscriptions)) {
7945
- if (sub.subscriptionId === res.subscription) {
7946
- const {
7947
- value,
7948
- context
7949
- } = res.result;
7950
- sub.callback({
7951
- accountId: value.pubkey,
7952
- accountInfo: value.account
7953
- }, context);
7954
- return;
7955
- }
7956
- }
8110
+ this._handleServerNotification(subscription, [{
8111
+ accountId: result.value.pubkey,
8112
+ accountInfo: result.value.account
8113
+ }, result.context]);
7957
8114
  }
7958
8115
  /**
7959
8116
  * Register a callback to be invoked whenever accounts owned by the
@@ -7968,36 +8125,30 @@ class Connection {
7968
8125
 
7969
8126
 
7970
8127
  onProgramAccountChange(programId, callback, commitment, filters) {
7971
- const id = ++this._programAccountChangeSubscriptionCounter;
7972
- this._programAccountChangeSubscriptions[id] = {
7973
- programId: programId.toBase58(),
8128
+ const args = this._buildArgs([programId.toBase58()], commitment || this._commitment || 'finalized', // Apply connection/server default.
8129
+ 'base64'
8130
+ /* encoding */
8131
+ , filters ? {
8132
+ filters: filters
8133
+ } : undefined
8134
+ /* extra */
8135
+ );
8136
+
8137
+ return this._makeSubscription({
7974
8138
  callback,
7975
- commitment,
7976
- subscriptionId: null,
7977
- filters
7978
- };
7979
-
7980
- this._updateSubscriptions();
7981
-
7982
- return id;
8139
+ method: 'programSubscribe',
8140
+ unsubscribeMethod: 'programUnsubscribe'
8141
+ }, args);
7983
8142
  }
7984
8143
  /**
7985
8144
  * Deregister an account notification callback
7986
8145
  *
7987
- * @param id subscription id to deregister
8146
+ * @param id client subscription id to deregister
7988
8147
  */
7989
8148
 
7990
8149
 
7991
- async removeProgramAccountChangeListener(id) {
7992
- if (this._programAccountChangeSubscriptions[id]) {
7993
- const subInfo = this._programAccountChangeSubscriptions[id];
7994
- delete this._programAccountChangeSubscriptions[id];
7995
- await this._unsubscribe(subInfo, 'programUnsubscribe');
7996
-
7997
- this._updateSubscriptions();
7998
- } else {
7999
- console.warn(createSubscriptionWarningMessage(id, 'program account change'));
8000
- }
8150
+ async removeProgramAccountChangeListener(clientSubscriptionId) {
8151
+ await this._unsubscribeClientSubscription(clientSubscriptionId, 'program account change');
8001
8152
  }
8002
8153
  /**
8003
8154
  * Registers a callback to be invoked whenever logs are emitted.
@@ -8005,35 +8156,26 @@ class Connection {
8005
8156
 
8006
8157
 
8007
8158
  onLogs(filter, callback, commitment) {
8008
- const id = ++this._logsSubscriptionCounter;
8009
- this._logsSubscriptions[id] = {
8010
- filter,
8011
- callback,
8012
- commitment,
8013
- subscriptionId: null
8014
- };
8015
-
8016
- this._updateSubscriptions();
8159
+ const args = this._buildArgs([typeof filter === 'object' ? {
8160
+ mentions: [filter.toString()]
8161
+ } : filter], commitment || this._commitment || 'finalized' // Apply connection/server default.
8162
+ );
8017
8163
 
8018
- return id;
8164
+ return this._makeSubscription({
8165
+ callback,
8166
+ method: 'logsSubscribe',
8167
+ unsubscribeMethod: 'logsUnsubscribe'
8168
+ }, args);
8019
8169
  }
8020
8170
  /**
8021
8171
  * Deregister a logs callback.
8022
8172
  *
8023
- * @param id subscription id to deregister.
8173
+ * @param id client subscription id to deregister.
8024
8174
  */
8025
8175
 
8026
8176
 
8027
- async removeOnLogsListener(id) {
8028
- if (this._logsSubscriptions[id]) {
8029
- const subInfo = this._logsSubscriptions[id];
8030
- delete this._logsSubscriptions[id];
8031
- await this._unsubscribe(subInfo, 'logsUnsubscribe');
8032
-
8033
- this._updateSubscriptions();
8034
- } else {
8035
- console.warn(createSubscriptionWarningMessage(id, 'logs'));
8036
- }
8177
+ async removeOnLogsListener(clientSubscriptionId) {
8178
+ await this._unsubscribeClientSubscription(clientSubscriptionId, 'logs');
8037
8179
  }
8038
8180
  /**
8039
8181
  * @internal
@@ -8041,17 +8183,12 @@ class Connection {
8041
8183
 
8042
8184
 
8043
8185
  _wsOnLogsNotification(notification) {
8044
- const res = superstruct.create(notification, LogsNotificationResult);
8045
- const keys = Object.keys(this._logsSubscriptions).map(Number);
8046
-
8047
- for (let id of keys) {
8048
- const sub = this._logsSubscriptions[id];
8186
+ const {
8187
+ result,
8188
+ subscription
8189
+ } = superstruct.create(notification, LogsNotificationResult);
8049
8190
 
8050
- if (sub.subscriptionId === res.subscription) {
8051
- sub.callback(res.result.value, res.result.context);
8052
- return;
8053
- }
8054
- }
8191
+ this._handleServerNotification(subscription, [result.value, result.context]);
8055
8192
  }
8056
8193
  /**
8057
8194
  * @internal
@@ -8059,14 +8196,12 @@ class Connection {
8059
8196
 
8060
8197
 
8061
8198
  _wsOnSlotNotification(notification) {
8062
- const res = superstruct.create(notification, SlotNotificationResult);
8199
+ const {
8200
+ result,
8201
+ subscription
8202
+ } = superstruct.create(notification, SlotNotificationResult);
8063
8203
 
8064
- for (const sub of Object.values(this._slotSubscriptions)) {
8065
- if (sub.subscriptionId === res.subscription) {
8066
- sub.callback(res.result);
8067
- return;
8068
- }
8069
- }
8204
+ this._handleServerNotification(subscription, [result]);
8070
8205
  }
8071
8206
  /**
8072
8207
  * Register a callback to be invoked upon slot changes
@@ -8077,33 +8212,23 @@ class Connection {
8077
8212
 
8078
8213
 
8079
8214
  onSlotChange(callback) {
8080
- const id = ++this._slotSubscriptionCounter;
8081
- this._slotSubscriptions[id] = {
8215
+ return this._makeSubscription({
8082
8216
  callback,
8083
- subscriptionId: null
8084
- };
8085
-
8086
- this._updateSubscriptions();
8087
-
8088
- return id;
8217
+ method: 'slotSubscribe',
8218
+ unsubscribeMethod: 'slotUnsubscribe'
8219
+ }, []
8220
+ /* args */
8221
+ );
8089
8222
  }
8090
8223
  /**
8091
8224
  * Deregister a slot notification callback
8092
8225
  *
8093
- * @param id subscription id to deregister
8226
+ * @param id client subscription id to deregister
8094
8227
  */
8095
8228
 
8096
8229
 
8097
- async removeSlotChangeListener(id) {
8098
- if (this._slotSubscriptions[id]) {
8099
- const subInfo = this._slotSubscriptions[id];
8100
- delete this._slotSubscriptions[id];
8101
- await this._unsubscribe(subInfo, 'slotUnsubscribe');
8102
-
8103
- this._updateSubscriptions();
8104
- } else {
8105
- console.warn(createSubscriptionWarningMessage(id, 'slot change'));
8106
- }
8230
+ async removeSlotChangeListener(clientSubscriptionId) {
8231
+ await this._unsubscribeClientSubscription(clientSubscriptionId, 'slot change');
8107
8232
  }
8108
8233
  /**
8109
8234
  * @internal
@@ -8111,14 +8236,12 @@ class Connection {
8111
8236
 
8112
8237
 
8113
8238
  _wsOnSlotUpdatesNotification(notification) {
8114
- const res = superstruct.create(notification, SlotUpdateNotificationResult);
8239
+ const {
8240
+ result,
8241
+ subscription
8242
+ } = superstruct.create(notification, SlotUpdateNotificationResult);
8115
8243
 
8116
- for (const sub of Object.values(this._slotUpdateSubscriptions)) {
8117
- if (sub.subscriptionId === res.subscription) {
8118
- sub.callback(res.result);
8119
- return;
8120
- }
8121
- }
8244
+ this._handleServerNotification(subscription, [result]);
8122
8245
  }
8123
8246
  /**
8124
8247
  * Register a callback to be invoked upon slot updates. {@link SlotUpdate}'s
@@ -8130,32 +8253,36 @@ class Connection {
8130
8253
 
8131
8254
 
8132
8255
  onSlotUpdate(callback) {
8133
- const id = ++this._slotUpdateSubscriptionCounter;
8134
- this._slotUpdateSubscriptions[id] = {
8256
+ return this._makeSubscription({
8135
8257
  callback,
8136
- subscriptionId: null
8137
- };
8138
-
8139
- this._updateSubscriptions();
8140
-
8141
- return id;
8258
+ method: 'slotsUpdatesSubscribe',
8259
+ unsubscribeMethod: 'slotsUpdatesUnsubscribe'
8260
+ }, []
8261
+ /* args */
8262
+ );
8142
8263
  }
8143
8264
  /**
8144
8265
  * Deregister a slot update notification callback
8145
8266
  *
8146
- * @param id subscription id to deregister
8267
+ * @param id client subscription id to deregister
8147
8268
  */
8148
8269
 
8149
8270
 
8150
- async removeSlotUpdateListener(id) {
8151
- if (this._slotUpdateSubscriptions[id]) {
8152
- const subInfo = this._slotUpdateSubscriptions[id];
8153
- delete this._slotUpdateSubscriptions[id];
8154
- await this._unsubscribe(subInfo, 'slotsUpdatesUnsubscribe');
8271
+ async removeSlotUpdateListener(clientSubscriptionId) {
8272
+ await this._unsubscribeClientSubscription(clientSubscriptionId, 'slot update');
8273
+ }
8274
+ /**
8275
+ * @internal
8276
+ */
8155
8277
 
8156
- this._updateSubscriptions();
8278
+
8279
+ async _unsubscribeClientSubscription(clientSubscriptionId, subscriptionName) {
8280
+ const dispose = this._subscriptionDisposeFunctionsByClientSubscriptionId[clientSubscriptionId];
8281
+
8282
+ if (dispose) {
8283
+ await dispose();
8157
8284
  } else {
8158
- console.warn(createSubscriptionWarningMessage(id, 'slot update'));
8285
+ console.warn('Ignored unsubscribe request because an active subscription with id ' + `\`${clientSubscriptionId}\` for '${subscriptionName}' events ` + 'could not be found.');
8159
8286
  }
8160
8287
  }
8161
8288
 
@@ -8202,30 +8329,34 @@ class Connection {
8202
8329
 
8203
8330
 
8204
8331
  _wsOnSignatureNotification(notification) {
8205
- const res = superstruct.create(notification, SignatureNotificationResult);
8206
-
8207
- for (const [id, sub] of Object.entries(this._signatureSubscriptions)) {
8208
- if (sub.subscriptionId === res.subscription) {
8209
- if (res.result.value === 'receivedSignature') {
8210
- sub.callback({
8211
- type: 'received'
8212
- }, res.result.context);
8213
- } else {
8214
- // Signatures subscriptions are auto-removed by the RPC service so
8215
- // no need to explicitly send an unsubscribe message
8216
- delete this._signatureSubscriptions[Number(id)];
8217
-
8218
- this._updateSubscriptions();
8219
-
8220
- sub.callback({
8221
- type: 'status',
8222
- result: res.result.value
8223
- }, res.result.context);
8224
- }
8225
-
8226
- return;
8227
- }
8228
- }
8332
+ const {
8333
+ result,
8334
+ subscription
8335
+ } = superstruct.create(notification, SignatureNotificationResult);
8336
+
8337
+ if (result.value !== 'receivedSignature') {
8338
+ /**
8339
+ * Special case.
8340
+ * After a signature is processed, RPCs automatically dispose of the
8341
+ * subscription on the server side. We need to track which of these
8342
+ * subscriptions have been disposed in such a way, so that we know
8343
+ * whether the client is dealing with a not-yet-processed signature
8344
+ * (in which case we must tear down the server subscription) or an
8345
+ * already-processed signature (in which case the client can simply
8346
+ * clear out the subscription locally without telling the server).
8347
+ *
8348
+ * NOTE: There is a proposal to eliminate this special case, here:
8349
+ * https://github.com/solana-labs/solana/issues/18892
8350
+ */
8351
+ this._subscriptionsAutoDisposedByRpc.add(subscription);
8352
+ }
8353
+
8354
+ this._handleServerNotification(subscription, result.value === 'receivedSignature' ? [{
8355
+ type: 'received'
8356
+ }, result.context] : [{
8357
+ type: 'status',
8358
+ result: result.value
8359
+ }, result.context]);
8229
8360
  }
8230
8361
  /**
8231
8362
  * Register a callback to be invoked upon signature updates
@@ -8238,23 +8369,26 @@ class Connection {
8238
8369
 
8239
8370
 
8240
8371
  onSignature(signature, callback, commitment) {
8241
- const id = ++this._signatureSubscriptionCounter;
8242
- this._signatureSubscriptions[id] = {
8243
- signature,
8372
+ const args = this._buildArgs([signature], commitment || this._commitment || 'finalized' // Apply connection/server default.
8373
+ );
8374
+
8375
+ const clientSubscriptionId = this._makeSubscription({
8244
8376
  callback: (notification, context) => {
8245
8377
  if (notification.type === 'status') {
8246
- callback(notification.result, context);
8378
+ callback(notification.result, context); // Signatures subscriptions are auto-removed by the RPC service
8379
+ // so no need to explicitly send an unsubscribe message.
8380
+
8381
+ try {
8382
+ this.removeSignatureListener(clientSubscriptionId); // eslint-disable-next-line no-empty
8383
+ } catch {// Already removed.
8384
+ }
8247
8385
  }
8248
8386
  },
8249
- options: {
8250
- commitment
8251
- },
8252
- subscriptionId: null
8253
- };
8387
+ method: 'signatureSubscribe',
8388
+ unsubscribeMethod: 'signatureUnsubscribe'
8389
+ }, args);
8254
8390
 
8255
- this._updateSubscriptions();
8256
-
8257
- return id;
8391
+ return clientSubscriptionId;
8258
8392
  }
8259
8393
  /**
8260
8394
  * Register a callback to be invoked when a transaction is
@@ -8269,35 +8403,43 @@ class Connection {
8269
8403
 
8270
8404
 
8271
8405
  onSignatureWithOptions(signature, callback, options) {
8272
- const id = ++this._signatureSubscriptionCounter;
8273
- this._signatureSubscriptions[id] = {
8274
- signature,
8275
- callback,
8276
- options,
8277
- subscriptionId: null
8406
+ const {
8407
+ commitment,
8408
+ ...extra
8409
+ } = { ...options,
8410
+ commitment: options && options.commitment || this._commitment || 'finalized' // Apply connection/server default.
8411
+
8278
8412
  };
8279
8413
 
8280
- this._updateSubscriptions();
8414
+ const args = this._buildArgs([signature], commitment, undefined
8415
+ /* encoding */
8416
+ , extra);
8281
8417
 
8282
- return id;
8418
+ const clientSubscriptionId = this._makeSubscription({
8419
+ callback: (notification, context) => {
8420
+ callback(notification, context); // Signatures subscriptions are auto-removed by the RPC service
8421
+ // so no need to explicitly send an unsubscribe message.
8422
+
8423
+ try {
8424
+ this.removeSignatureListener(clientSubscriptionId); // eslint-disable-next-line no-empty
8425
+ } catch {// Already removed.
8426
+ }
8427
+ },
8428
+ method: 'signatureSubscribe',
8429
+ unsubscribeMethod: 'signatureUnsubscribe'
8430
+ }, args);
8431
+
8432
+ return clientSubscriptionId;
8283
8433
  }
8284
8434
  /**
8285
8435
  * Deregister a signature notification callback
8286
8436
  *
8287
- * @param id subscription id to deregister
8437
+ * @param id client subscription id to deregister
8288
8438
  */
8289
8439
 
8290
8440
 
8291
- async removeSignatureListener(id) {
8292
- if (this._signatureSubscriptions[id]) {
8293
- const subInfo = this._signatureSubscriptions[id];
8294
- delete this._signatureSubscriptions[id];
8295
- await this._unsubscribe(subInfo, 'signatureUnsubscribe');
8296
-
8297
- this._updateSubscriptions();
8298
- } else {
8299
- console.warn(createSubscriptionWarningMessage(id, 'signature result'));
8300
- }
8441
+ async removeSignatureListener(clientSubscriptionId) {
8442
+ await this._unsubscribeClientSubscription(clientSubscriptionId, 'signature result');
8301
8443
  }
8302
8444
  /**
8303
8445
  * @internal
@@ -8305,14 +8447,12 @@ class Connection {
8305
8447
 
8306
8448
 
8307
8449
  _wsOnRootNotification(notification) {
8308
- const res = superstruct.create(notification, RootNotificationResult);
8450
+ const {
8451
+ result,
8452
+ subscription
8453
+ } = superstruct.create(notification, RootNotificationResult);
8309
8454
 
8310
- for (const sub of Object.values(this._rootSubscriptions)) {
8311
- if (sub.subscriptionId === res.subscription) {
8312
- sub.callback(res.result);
8313
- return;
8314
- }
8315
- }
8455
+ this._handleServerNotification(subscription, [result]);
8316
8456
  }
8317
8457
  /**
8318
8458
  * Register a callback to be invoked upon root changes
@@ -8323,33 +8463,23 @@ class Connection {
8323
8463
 
8324
8464
 
8325
8465
  onRootChange(callback) {
8326
- const id = ++this._rootSubscriptionCounter;
8327
- this._rootSubscriptions[id] = {
8466
+ return this._makeSubscription({
8328
8467
  callback,
8329
- subscriptionId: null
8330
- };
8331
-
8332
- this._updateSubscriptions();
8333
-
8334
- return id;
8468
+ method: 'rootSubscribe',
8469
+ unsubscribeMethod: 'rootUnsubscribe'
8470
+ }, []
8471
+ /* args */
8472
+ );
8335
8473
  }
8336
8474
  /**
8337
8475
  * Deregister a root notification callback
8338
8476
  *
8339
- * @param id subscription id to deregister
8477
+ * @param id client subscription id to deregister
8340
8478
  */
8341
8479
 
8342
8480
 
8343
- async removeRootChangeListener(id) {
8344
- if (this._rootSubscriptions[id]) {
8345
- const subInfo = this._rootSubscriptions[id];
8346
- delete this._rootSubscriptions[id];
8347
- await this._unsubscribe(subInfo, 'rootUnsubscribe');
8348
-
8349
- this._updateSubscriptions();
8350
- } else {
8351
- console.warn(createSubscriptionWarningMessage(id, 'root change'));
8352
- }
8481
+ async removeRootChangeListener(clientSubscriptionId) {
8482
+ await this._unsubscribeClientSubscription(clientSubscriptionId, 'root change');
8353
8483
  }
8354
8484
 
8355
8485
  }