@solana/web3.js 1.41.7 → 1.41.10

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solana/web3.js",
3
- "version": "1.41.7",
3
+ "version": "1.41.10",
4
4
  "description": "Solana Javascript API",
5
5
  "keywords": [
6
6
  "api",
@@ -129,7 +129,7 @@
129
129
  "sinon": "^13.0.2",
130
130
  "sinon-chai": "^3.7.0",
131
131
  "start-server-and-test": "^1.12.0",
132
- "ts-mocha": "^9.0.2",
132
+ "ts-mocha": "^10.0.0",
133
133
  "ts-node": "^10.0.0",
134
134
  "tslib": "^2.1.0",
135
135
  "typedoc": "^0.22.2",
package/src/connection.ts CHANGED
@@ -32,12 +32,15 @@ import {NonceAccount} from './nonce-account';
32
32
  import {PublicKey} from './publickey';
33
33
  import {Signer} from './keypair';
34
34
  import {MS_PER_SLOT} from './timing';
35
- import {Transaction} from './transaction';
35
+ import {Transaction, TransactionStatus} from './transaction';
36
36
  import {Message} from './message';
37
37
  import assert from './util/assert';
38
38
  import {sleep} from './util/sleep';
39
- import {promiseTimeout} from './util/promise-timeout';
40
39
  import {toBuffer} from './util/to-buffer';
40
+ import {
41
+ TransactionExpiredBlockheightExceededError,
42
+ TransactionExpiredTimeoutError,
43
+ } from './util/tx-expiry-custom-errors';
41
44
  import {makeWebsocketUrl} from './util/url';
42
45
  import type {Blockhash} from './blockhash';
43
46
  import type {FeeCalculator} from './fee-calculator';
@@ -281,6 +284,16 @@ export type RpcResponseAndContext<T> = {
281
284
  value: T;
282
285
  };
283
286
 
287
+ /**
288
+ * A strategy for confirming transactions that uses the last valid
289
+ * block height for a given blockhash to check for transaction expiration.
290
+ */
291
+ export type BlockheightBasedTransactionConfimationStrategy = {
292
+ signature: TransactionSignature;
293
+ blockhash: Blockhash;
294
+ lastValidBlockHeight: number;
295
+ };
296
+
284
297
  /**
285
298
  * @internal
286
299
  */
@@ -2825,38 +2838,64 @@ export class Connection {
2825
2838
  return res.result;
2826
2839
  }
2827
2840
 
2828
- /**
2829
- * Confirm the transaction identified by the specified signature.
2830
- */
2841
+ confirmTransaction(
2842
+ strategy: BlockheightBasedTransactionConfimationStrategy,
2843
+ commitment?: Commitment,
2844
+ ): Promise<RpcResponseAndContext<SignatureResult>>;
2845
+
2846
+ /** @deprecated Instead, call `confirmTransaction` using a `TransactionConfirmationConfig` */
2847
+ // eslint-disable-next-line no-dupe-class-members
2848
+ confirmTransaction(
2849
+ strategy: TransactionSignature,
2850
+ commitment?: Commitment,
2851
+ ): Promise<RpcResponseAndContext<SignatureResult>>;
2852
+
2853
+ // eslint-disable-next-line no-dupe-class-members
2831
2854
  async confirmTransaction(
2832
- signature: TransactionSignature,
2855
+ strategy:
2856
+ | BlockheightBasedTransactionConfimationStrategy
2857
+ | TransactionSignature,
2833
2858
  commitment?: Commitment,
2834
2859
  ): Promise<RpcResponseAndContext<SignatureResult>> {
2860
+ let rawSignature: string;
2861
+
2862
+ if (typeof strategy == 'string') {
2863
+ rawSignature = strategy;
2864
+ } else {
2865
+ const config = strategy as BlockheightBasedTransactionConfimationStrategy;
2866
+ rawSignature = config.signature;
2867
+ }
2868
+
2835
2869
  let decodedSignature;
2870
+
2836
2871
  try {
2837
- decodedSignature = bs58.decode(signature);
2872
+ decodedSignature = bs58.decode(rawSignature);
2838
2873
  } catch (err) {
2839
- throw new Error('signature must be base58 encoded: ' + signature);
2874
+ throw new Error('signature must be base58 encoded: ' + rawSignature);
2840
2875
  }
2841
2876
 
2842
2877
  assert(decodedSignature.length === 64, 'signature has invalid length');
2843
2878
 
2844
- const start = Date.now();
2845
2879
  const subscriptionCommitment = commitment || this.commitment;
2846
-
2880
+ let timeoutId;
2847
2881
  let subscriptionId;
2848
- let response: RpcResponseAndContext<SignatureResult> | null = null;
2849
- const confirmPromise = new Promise((resolve, reject) => {
2882
+ let done = false;
2883
+
2884
+ const confirmationPromise = new Promise<{
2885
+ __type: TransactionStatus.PROCESSED;
2886
+ response: RpcResponseAndContext<SignatureResult>;
2887
+ }>((resolve, reject) => {
2850
2888
  try {
2851
2889
  subscriptionId = this.onSignature(
2852
- signature,
2890
+ rawSignature,
2853
2891
  (result: SignatureResult, context: Context) => {
2854
2892
  subscriptionId = undefined;
2855
- response = {
2893
+ const response = {
2856
2894
  context,
2857
2895
  value: result,
2858
2896
  };
2859
- resolve(null);
2897
+ done = true;
2898
+ resolve({__type: TransactionStatus.PROCESSED, response});
2860
2899
  },
2861
2900
  subscriptionCommitment,
2862
2901
  );
@@ -2865,40 +2904,78 @@ export class Connection {
2865
2904
  }
2866
2905
  });
2867
2906
 
2868
- let timeoutMs = this._confirmTransactionInitialTimeout || 60 * 1000;
2869
- switch (subscriptionCommitment) {
2870
- case 'processed':
2871
- case 'recent':
2872
- case 'single':
2873
- case 'confirmed':
2874
- case 'singleGossip': {
2875
- timeoutMs = this._confirmTransactionInitialTimeout || 30 * 1000;
2876
- break;
2907
+ const checkBlockHeight = async () => {
2908
+ try {
2909
+ const blockHeight = await this.getBlockHeight(commitment);
2910
+ return blockHeight;
2911
+ } catch (_e) {
2912
+ return -1;
2877
2913
  }
2878
- // exhaust enums to ensure full coverage
2879
- case 'finalized':
2880
- case 'max':
2881
- case 'root':
2882
- }
2914
+ };
2883
2915
 
2916
+ const expiryPromise = new Promise<
2917
+ | {__type: TransactionStatus.BLOCKHEIGHT_EXCEEDED}
2918
+ | {__type: TransactionStatus.TIMED_OUT; timeoutMs: number}
2919
+ >(resolve => {
2920
+ if (typeof strategy === 'string') {
2921
+ let timeoutMs = this._confirmTransactionInitialTimeout || 60 * 1000;
2922
+ switch (subscriptionCommitment) {
2923
+ case 'processed':
2924
+ case 'recent':
2925
+ case 'single':
2926
+ case 'confirmed':
2927
+ case 'singleGossip': {
2928
+ timeoutMs = this._confirmTransactionInitialTimeout || 30 * 1000;
2929
+ break;
2930
+ }
2931
+ // exhaust enums to ensure full coverage
2932
+ case 'finalized':
2933
+ case 'max':
2934
+ case 'root':
2935
+ }
2936
+
2937
+ timeoutId = setTimeout(
2938
+ () => resolve({__type: TransactionStatus.TIMED_OUT, timeoutMs}),
2939
+ timeoutMs,
2940
+ );
2941
+ } else {
2942
+ let config = strategy as BlockheightBasedTransactionConfimationStrategy;
2943
+ (async () => {
2944
+ let currentBlockHeight = await checkBlockHeight();
2945
+ if (done) return;
2946
+ while (currentBlockHeight <= config.lastValidBlockHeight) {
2947
+ await sleep(1000);
2948
+ if (done) return;
2949
+ currentBlockHeight = await checkBlockHeight();
2950
+ if (done) return;
2951
+ }
2952
+ resolve({__type: TransactionStatus.BLOCKHEIGHT_EXCEEDED});
2953
+ })();
2954
+ }
2955
+ });
2956
+
2957
+ let result: RpcResponseAndContext<SignatureResult>;
2884
2958
  try {
2885
- await promiseTimeout(confirmPromise, timeoutMs);
2959
+ const outcome = await Promise.race([confirmationPromise, expiryPromise]);
2960
+ switch (outcome.__type) {
2961
+ case TransactionStatus.BLOCKHEIGHT_EXCEEDED:
2962
+ throw new TransactionExpiredBlockheightExceededError(rawSignature);
2963
+ case TransactionStatus.PROCESSED:
2964
+ result = outcome.response;
2965
+ break;
2966
+ case TransactionStatus.TIMED_OUT:
2967
+ throw new TransactionExpiredTimeoutError(
2968
+ rawSignature,
2969
+ outcome.timeoutMs / 1000,
2970
+ );
2971
+ }
2886
2972
  } finally {
2973
+ clearTimeout(timeoutId);
2887
2974
  if (subscriptionId) {
2888
2975
  this.removeSignatureListener(subscriptionId);
2889
2976
  }
2890
2977
  }
2891
-
2892
- if (response === null) {
2893
- const duration = (Date.now() - start) / 1000;
2894
- throw new Error(
2895
- `Transaction was not confirmed in ${duration.toFixed(
2896
- 2,
2897
- )} seconds. It is unknown if it succeeded or failed. Check signature ${signature} using the Solana Explorer or CLI tools.`,
2898
- );
2899
- }
2900
-
2901
- return response;
2978
+ return result;
2902
2979
  }
2903
2980
 
2904
2981
  /**
@@ -4174,6 +4251,7 @@ export class Connection {
4174
4251
  * @internal
4175
4252
  */
4176
4253
  _wsOnError(err: Error) {
4254
+ this._rpcWebSocketConnected = false;
4177
4255
  console.error('ws error:', err.message);
4178
4256
  }
4179
4257
 
@@ -4181,6 +4259,7 @@ export class Connection {
4181
4259
  * @internal
4182
4260
  */
4183
4261
  _wsOnClose(code: number) {
4262
+ this._rpcWebSocketConnected = false;
4184
4263
  this._rpcWebSocketGeneration++;
4185
4264
  if (this._rpcWebSocketHeartbeat) {
4186
4265
  clearInterval(this._rpcWebSocketHeartbeat);
@@ -21,6 +21,12 @@ import type {CompiledInstruction} from './message';
21
21
  */
22
22
  export type TransactionSignature = string;
23
23
 
24
+ export const enum TransactionStatus {
25
+ BLOCKHEIGHT_EXCEEDED,
26
+ PROCESSED,
27
+ TIMED_OUT,
28
+ }
29
+
24
30
  /**
25
31
  * Default (empty) signature
26
32
  */
@@ -124,17 +130,30 @@ export type SignaturePubkeyPair = {
124
130
 
125
131
  /**
126
132
  * List of Transaction object fields that may be initialized at construction
127
- *
128
133
  */
129
- export type TransactionCtorFields = {
130
- /** A recent blockhash */
131
- recentBlockhash?: Blockhash | null;
134
+ export type TransactionCtorFields_DEPRECATED = {
132
135
  /** Optional nonce information used for offline nonce'd transactions */
133
136
  nonceInfo?: NonceInformation | null;
134
137
  /** The transaction fee payer */
135
138
  feePayer?: PublicKey | null;
136
139
  /** One or more signatures */
137
140
  signatures?: Array<SignaturePubkeyPair>;
141
+ /** A recent blockhash */
142
+ recentBlockhash?: Blockhash;
143
+ };
144
+
145
+ /**
146
+ * List of Transaction object fields that may be initialized at construction
147
+ */
148
+ export type TransactionBlockhashCtor = {
149
+ /** The transaction fee payer */
150
+ feePayer?: PublicKey | null;
151
+ /** One or more signatures */
152
+ signatures?: Array<SignaturePubkeyPair>;
153
+ /** A recent blockhash */
154
+ blockhash: Blockhash;
155
+ /** the last block chain can advance to before tx is declared expired */
156
+ lastValidBlockHeight: number;
138
157
  };
139
158
 
140
159
  /**
@@ -196,6 +215,11 @@ export class Transaction {
196
215
  */
197
216
  recentBlockhash?: Blockhash;
198
217
 
218
+ /**
219
+ * the last block chain can advance to before tx is declared expired
220
+ * */
221
+ lastValidBlockHeight?: number;
222
+
199
223
  /**
200
224
  * Optional Nonce information. If populated, transaction will use a durable
201
225
  * Nonce hash instead of a recentBlockhash. Must be populated by the caller
@@ -212,11 +236,35 @@ export class Transaction {
212
236
  */
213
237
  _json?: TransactionJSON;
214
238
 
239
+ // Construct a transaction with a blockhash and lastValidBlockHeight
240
+ constructor(opts?: TransactionBlockhashCtor);
241
+
242
+ /**
243
+ * @deprecated `TransactionCtorFields` has been deprecated and will be removed in a future version.
244
+ * Please supply a `TransactionBlockhashCtor` instead.
245
+ */
246
+ constructor(opts?: TransactionCtorFields_DEPRECATED);
247
+
215
248
  /**
216
249
  * Construct an empty Transaction
217
250
  */
218
- constructor(opts?: TransactionCtorFields) {
219
- opts && Object.assign(this, opts);
251
+ constructor(
252
+ opts?: TransactionBlockhashCtor | TransactionCtorFields_DEPRECATED,
253
+ ) {
254
+ if (!opts) {
255
+ return;
256
+ } else if (
257
+ Object.prototype.hasOwnProperty.call(opts, 'lastValidBlockHeight')
258
+ ) {
259
+ const newOpts = opts as TransactionBlockhashCtor;
260
+ Object.assign(this, newOpts);
261
+ this.recentBlockhash = newOpts.blockhash;
262
+ this.lastValidBlockHeight = newOpts.lastValidBlockHeight;
263
+ } else {
264
+ const oldOpts = opts as TransactionCtorFields_DEPRECATED;
265
+ Object.assign(this, oldOpts);
266
+ this.recentBlockhash = oldOpts.recentBlockhash;
267
+ }
220
268
  }
221
269
 
222
270
  /**
@@ -33,12 +33,25 @@ export async function sendAndConfirmTransaction(
33
33
  sendOptions,
34
34
  );
35
35
 
36
- const status = (
37
- await connection.confirmTransaction(
38
- signature,
39
- options && options.commitment,
40
- )
41
- ).value;
36
+ const status =
37
+ transaction.recentBlockhash != null &&
38
+ transaction.lastValidBlockHeight != null
39
+ ? (
40
+ await connection.confirmTransaction(
41
+ {
42
+ signature: signature,
43
+ blockhash: transaction.recentBlockhash,
44
+ lastValidBlockHeight: transaction.lastValidBlockHeight,
45
+ },
46
+ options && options.commitment,
47
+ )
48
+ ).value
49
+ : (
50
+ await connection.confirmTransaction(
51
+ signature,
52
+ options && options.commitment,
53
+ )
54
+ ).value;
42
55
 
43
56
  if (status.err) {
44
57
  throw new Error(
@@ -0,0 +1,35 @@
1
+ export class TransactionExpiredBlockheightExceededError extends Error {
2
+ signature: string;
3
+
4
+ constructor(signature: string) {
5
+ super(`Signature ${signature} has expired: block height exceeded.`);
6
+ this.signature = signature;
7
+ }
8
+ }
9
+
10
+ Object.defineProperty(
11
+ TransactionExpiredBlockheightExceededError.prototype,
12
+ 'name',
13
+ {
14
+ value: 'TransactionExpiredBlockheightExceededError',
15
+ },
16
+ );
17
+
18
+ export class TransactionExpiredTimeoutError extends Error {
19
+ signature: string;
20
+
21
+ constructor(signature: string, timeoutSeconds: number) {
22
+ super(
23
+ `Transaction was not confirmed in ${timeoutSeconds.toFixed(
24
+ 2,
25
+ )} seconds. It is ` +
26
+ 'unknown if it succeeded or failed. Check signature ' +
27
+ `${signature} using the Solana Explorer or CLI tools.`,
28
+ );
29
+ this.signature = signature;
30
+ }
31
+ }
32
+
33
+ Object.defineProperty(TransactionExpiredTimeoutError.prototype, 'name', {
34
+ value: 'TransactionExpiredTimeoutError',
35
+ });