@solana/web3.js 1.66.4 → 1.67.0

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.66.4",
3
+ "version": "1.67.0",
4
4
  "description": "Solana Javascript API",
5
5
  "keywords": [
6
6
  "api",
package/src/connection.ts CHANGED
@@ -28,7 +28,7 @@ import {AgentManager} from './agent-manager';
28
28
  import {EpochSchedule} from './epoch-schedule';
29
29
  import {SendTransactionError, SolanaJSONRPCError} from './errors';
30
30
  import fetchImpl, {Response} from './fetch-impl';
31
- import {NonceAccount} from './nonce-account';
31
+ import {DurableNonce, NonceAccount} from './nonce-account';
32
32
  import {PublicKey} from './publickey';
33
33
  import {Signer} from './keypair';
34
34
  import {MS_PER_SLOT} from './timing';
@@ -45,6 +45,7 @@ import {sleep} from './utils/sleep';
45
45
  import {toBuffer} from './utils/to-buffer';
46
46
  import {
47
47
  TransactionExpiredBlockheightExceededError,
48
+ TransactionExpiredNonceInvalidError,
48
49
  TransactionExpiredTimeoutError,
49
50
  } from './transaction/expiry-custom-errors';
50
51
  import {makeWebsocketUrl} from './utils/makeWebsocketUrl';
@@ -338,6 +339,28 @@ function extractCommitmentFromConfig<TConfig>(
338
339
  return {commitment, config};
339
340
  }
340
341
 
342
+ /**
343
+ * A strategy for confirming durable nonce transactions.
344
+ */
345
+ export type DurableNonceTransactionConfirmationStrategy = {
346
+ /**
347
+ * The lowest slot at which to fetch the nonce value from the
348
+ * nonce account. This should be no lower than the slot at
349
+ * which the last-known value of the nonce was fetched.
350
+ */
351
+ minContextSlot: number;
352
+ /**
353
+ * The account where the current value of the nonce is stored.
354
+ */
355
+ nonceAccountPubkey: PublicKey;
356
+ /**
357
+ * The nonce value that was used to sign the transaction
358
+ * for which confirmation is being sought.
359
+ */
360
+ nonceValue: DurableNonce;
361
+ signature: TransactionSignature;
362
+ };
363
+
341
364
  /**
342
365
  * @internal
343
366
  */
@@ -2438,6 +2461,26 @@ export type GetTransactionCountConfig = {
2438
2461
  minContextSlot?: number;
2439
2462
  };
2440
2463
 
2464
+ /**
2465
+ * Configuration object for `getNonce`
2466
+ */
2467
+ export type GetNonceConfig = {
2468
+ /** Optional commitment level */
2469
+ commitment?: Commitment;
2470
+ /** The minimum slot that the request can be evaluated at */
2471
+ minContextSlot?: number;
2472
+ };
2473
+
2474
+ /**
2475
+ * Configuration object for `getNonceAndContext`
2476
+ */
2477
+ export type GetNonceAndContextConfig = {
2478
+ /** Optional commitment level */
2479
+ commitment?: Commitment;
2480
+ /** The minimum slot that the request can be evaluated at */
2481
+ minContextSlot?: number;
2482
+ };
2483
+
2441
2484
  /**
2442
2485
  * Information describing an account
2443
2486
  */
@@ -3348,7 +3391,9 @@ export class Connection {
3348
3391
  }
3349
3392
 
3350
3393
  confirmTransaction(
3351
- strategy: BlockheightBasedTransactionConfirmationStrategy,
3394
+ strategy:
3395
+ | BlockheightBasedTransactionConfirmationStrategy
3396
+ | DurableNonceTransactionConfirmationStrategy,
3352
3397
  commitment?: Commitment,
3353
3398
  ): Promise<RpcResponseAndContext<SignatureResult>>;
3354
3399
 
@@ -3363,6 +3408,7 @@ export class Connection {
3363
3408
  async confirmTransaction(
3364
3409
  strategy:
3365
3410
  | BlockheightBasedTransactionConfirmationStrategy
3411
+ | DurableNonceTransactionConfirmationStrategy
3366
3412
  | TransactionSignature,
3367
3413
  commitment?: Commitment,
3368
3414
  ): Promise<RpcResponseAndContext<SignatureResult>> {
@@ -3371,8 +3417,9 @@ export class Connection {
3371
3417
  if (typeof strategy == 'string') {
3372
3418
  rawSignature = strategy;
3373
3419
  } else {
3374
- const config =
3375
- strategy as BlockheightBasedTransactionConfirmationStrategy;
3420
+ const config = strategy as
3421
+ | BlockheightBasedTransactionConfirmationStrategy
3422
+ | DurableNonceTransactionConfirmationStrategy;
3376
3423
  rawSignature = config.signature;
3377
3424
  }
3378
3425
 
@@ -3386,31 +3433,58 @@ export class Connection {
3386
3433
 
3387
3434
  assert(decodedSignature.length === 64, 'signature has invalid length');
3388
3435
 
3389
- const subscriptionCommitment = commitment || this.commitment;
3390
- let timeoutId;
3436
+ if (typeof strategy === 'string') {
3437
+ return await this.confirmTransactionUsingLegacyTimeoutStrategy({
3438
+ commitment: commitment || this.commitment,
3439
+ signature: rawSignature,
3440
+ });
3441
+ } else if ('lastValidBlockHeight' in strategy) {
3442
+ return await this.confirmTransactionUsingBlockHeightExceedanceStrategy({
3443
+ commitment: commitment || this.commitment,
3444
+ strategy,
3445
+ });
3446
+ } else {
3447
+ return await this.confirmTransactionUsingDurableNonceStrategy({
3448
+ commitment: commitment || this.commitment,
3449
+ strategy,
3450
+ });
3451
+ }
3452
+ }
3453
+
3454
+ private getTransactionConfirmationPromise({
3455
+ commitment,
3456
+ signature,
3457
+ }: {
3458
+ commitment?: Commitment;
3459
+ signature: string;
3460
+ }): {
3461
+ abortConfirmation(): void;
3462
+ confirmationPromise: Promise<{
3463
+ __type: TransactionStatus.PROCESSED;
3464
+ response: RpcResponseAndContext<SignatureResult>;
3465
+ }>;
3466
+ } {
3391
3467
  let signatureSubscriptionId: number | undefined;
3392
3468
  let disposeSignatureSubscriptionStateChangeObserver:
3393
3469
  | SubscriptionStateChangeDisposeFn
3394
3470
  | undefined;
3395
3471
  let done = false;
3396
-
3397
3472
  const confirmationPromise = new Promise<{
3398
3473
  __type: TransactionStatus.PROCESSED;
3399
3474
  response: RpcResponseAndContext<SignatureResult>;
3400
3475
  }>((resolve, reject) => {
3401
3476
  try {
3402
3477
  signatureSubscriptionId = this.onSignature(
3403
- rawSignature,
3478
+ signature,
3404
3479
  (result: SignatureResult, context: Context) => {
3405
3480
  signatureSubscriptionId = undefined;
3406
3481
  const response = {
3407
3482
  context,
3408
3483
  value: result,
3409
3484
  };
3410
- done = true;
3411
3485
  resolve({__type: TransactionStatus.PROCESSED, response});
3412
3486
  },
3413
- subscriptionCommitment,
3487
+ commitment,
3414
3488
  );
3415
3489
  const subscriptionSetupPromise = new Promise<void>(
3416
3490
  resolveSubscriptionSetup => {
@@ -3432,16 +3506,42 @@ export class Connection {
3432
3506
  (async () => {
3433
3507
  await subscriptionSetupPromise;
3434
3508
  if (done) return;
3435
- const response = await this.getSignatureStatus(rawSignature);
3509
+ const response = await this.getSignatureStatus(signature);
3436
3510
  if (done) return;
3437
3511
  if (response == null) {
3438
3512
  return;
3439
3513
  }
3440
3514
  const {context, value} = response;
3515
+ if (value == null) {
3516
+ return;
3517
+ }
3441
3518
  if (value?.err) {
3442
3519
  reject(value.err);
3443
- }
3444
- if (value) {
3520
+ } else {
3521
+ switch (commitment) {
3522
+ case 'confirmed':
3523
+ case 'single':
3524
+ case 'singleGossip': {
3525
+ if (value.confirmationStatus === 'processed') {
3526
+ return;
3527
+ }
3528
+ break;
3529
+ }
3530
+ case 'finalized':
3531
+ case 'max':
3532
+ case 'root': {
3533
+ if (
3534
+ value.confirmationStatus === 'processed' ||
3535
+ value.confirmationStatus === 'confirmed'
3536
+ ) {
3537
+ return;
3538
+ }
3539
+ break;
3540
+ }
3541
+ // exhaust enums to ensure full coverage
3542
+ case 'processed':
3543
+ case 'recent':
3544
+ }
3445
3545
  done = true;
3446
3546
  resolve({
3447
3547
  __type: TransactionStatus.PROCESSED,
@@ -3456,80 +3556,250 @@ export class Connection {
3456
3556
  reject(err);
3457
3557
  }
3458
3558
  });
3559
+ const abortConfirmation = () => {
3560
+ if (disposeSignatureSubscriptionStateChangeObserver) {
3561
+ disposeSignatureSubscriptionStateChangeObserver();
3562
+ disposeSignatureSubscriptionStateChangeObserver = undefined;
3563
+ }
3564
+ if (signatureSubscriptionId) {
3565
+ this.removeSignatureListener(signatureSubscriptionId);
3566
+ signatureSubscriptionId = undefined;
3567
+ }
3568
+ };
3569
+ return {abortConfirmation, confirmationPromise};
3570
+ }
3459
3571
 
3460
- const expiryPromise = new Promise<
3461
- | {__type: TransactionStatus.BLOCKHEIGHT_EXCEEDED}
3462
- | {__type: TransactionStatus.TIMED_OUT; timeoutMs: number}
3463
- >(resolve => {
3464
- if (typeof strategy === 'string') {
3465
- let timeoutMs = this._confirmTransactionInitialTimeout || 60 * 1000;
3466
- switch (subscriptionCommitment) {
3467
- case 'processed':
3468
- case 'recent':
3469
- case 'single':
3470
- case 'confirmed':
3471
- case 'singleGossip': {
3472
- timeoutMs = this._confirmTransactionInitialTimeout || 30 * 1000;
3473
- break;
3474
- }
3475
- // exhaust enums to ensure full coverage
3476
- case 'finalized':
3477
- case 'max':
3478
- case 'root':
3572
+ private async confirmTransactionUsingBlockHeightExceedanceStrategy({
3573
+ commitment,
3574
+ strategy: {lastValidBlockHeight, signature},
3575
+ }: {
3576
+ commitment?: Commitment;
3577
+ strategy: BlockheightBasedTransactionConfirmationStrategy;
3578
+ }) {
3579
+ let done: boolean = false;
3580
+ const expiryPromise = new Promise<{
3581
+ __type: TransactionStatus.BLOCKHEIGHT_EXCEEDED;
3582
+ }>(resolve => {
3583
+ const checkBlockHeight = async () => {
3584
+ try {
3585
+ const blockHeight = await this.getBlockHeight(commitment);
3586
+ return blockHeight;
3587
+ } catch (_e) {
3588
+ return -1;
3479
3589
  }
3480
-
3481
- timeoutId = setTimeout(
3482
- () => resolve({__type: TransactionStatus.TIMED_OUT, timeoutMs}),
3483
- timeoutMs,
3484
- );
3590
+ };
3591
+ (async () => {
3592
+ let currentBlockHeight = await checkBlockHeight();
3593
+ if (done) return;
3594
+ while (currentBlockHeight <= lastValidBlockHeight) {
3595
+ await sleep(1000);
3596
+ if (done) return;
3597
+ currentBlockHeight = await checkBlockHeight();
3598
+ if (done) return;
3599
+ }
3600
+ resolve({__type: TransactionStatus.BLOCKHEIGHT_EXCEEDED});
3601
+ })();
3602
+ });
3603
+ const {abortConfirmation, confirmationPromise} =
3604
+ this.getTransactionConfirmationPromise({commitment, signature});
3605
+ let result: RpcResponseAndContext<SignatureResult>;
3606
+ try {
3607
+ const outcome = await Promise.race([confirmationPromise, expiryPromise]);
3608
+ if (outcome.__type === TransactionStatus.PROCESSED) {
3609
+ result = outcome.response;
3485
3610
  } else {
3486
- let config =
3487
- strategy as BlockheightBasedTransactionConfirmationStrategy;
3488
- const checkBlockHeight = async () => {
3489
- try {
3490
- const blockHeight = await this.getBlockHeight(commitment);
3491
- return blockHeight;
3492
- } catch (_e) {
3493
- return -1;
3611
+ throw new TransactionExpiredBlockheightExceededError(signature);
3612
+ }
3613
+ } finally {
3614
+ done = true;
3615
+ abortConfirmation();
3616
+ }
3617
+ return result;
3618
+ }
3619
+
3620
+ private async confirmTransactionUsingDurableNonceStrategy({
3621
+ commitment,
3622
+ strategy: {minContextSlot, nonceAccountPubkey, nonceValue, signature},
3623
+ }: {
3624
+ commitment?: Commitment;
3625
+ strategy: DurableNonceTransactionConfirmationStrategy;
3626
+ }) {
3627
+ let done: boolean = false;
3628
+ const expiryPromise = new Promise<{
3629
+ __type: TransactionStatus.NONCE_INVALID;
3630
+ slotInWhichNonceDidAdvance: number | null;
3631
+ }>(resolve => {
3632
+ let currentNonceValue: string | undefined = nonceValue;
3633
+ let lastCheckedSlot: number | null = null;
3634
+ const getCurrentNonceValue = async () => {
3635
+ try {
3636
+ const {context, value: nonceAccount} = await this.getNonceAndContext(
3637
+ nonceAccountPubkey,
3638
+ {
3639
+ commitment,
3640
+ minContextSlot,
3641
+ },
3642
+ );
3643
+ lastCheckedSlot = context.slot;
3644
+ return nonceAccount?.nonce;
3645
+ } catch (e) {
3646
+ // If for whatever reason we can't reach/read the nonce
3647
+ // account, just keep using the last-known value.
3648
+ return currentNonceValue;
3649
+ }
3650
+ };
3651
+ (async () => {
3652
+ currentNonceValue = await getCurrentNonceValue();
3653
+ if (done) return;
3654
+ while (
3655
+ true // eslint-disable-line no-constant-condition
3656
+ ) {
3657
+ if (nonceValue !== currentNonceValue) {
3658
+ resolve({
3659
+ __type: TransactionStatus.NONCE_INVALID,
3660
+ slotInWhichNonceDidAdvance: lastCheckedSlot,
3661
+ });
3662
+ return;
3494
3663
  }
3495
- };
3496
- (async () => {
3497
- let currentBlockHeight = await checkBlockHeight();
3664
+ await sleep(2000);
3498
3665
  if (done) return;
3499
- while (currentBlockHeight <= config.lastValidBlockHeight) {
3500
- await sleep(1000);
3501
- if (done) return;
3502
- currentBlockHeight = await checkBlockHeight();
3503
- if (done) return;
3504
- }
3505
- resolve({__type: TransactionStatus.BLOCKHEIGHT_EXCEEDED});
3506
- })();
3507
- }
3666
+ currentNonceValue = await getCurrentNonceValue();
3667
+ if (done) return;
3668
+ }
3669
+ })();
3508
3670
  });
3509
-
3671
+ const {abortConfirmation, confirmationPromise} =
3672
+ this.getTransactionConfirmationPromise({commitment, signature});
3510
3673
  let result: RpcResponseAndContext<SignatureResult>;
3511
3674
  try {
3512
3675
  const outcome = await Promise.race([confirmationPromise, expiryPromise]);
3513
- switch (outcome.__type) {
3514
- case TransactionStatus.BLOCKHEIGHT_EXCEEDED:
3515
- throw new TransactionExpiredBlockheightExceededError(rawSignature);
3516
- case TransactionStatus.PROCESSED:
3517
- result = outcome.response;
3676
+ if (outcome.__type === TransactionStatus.PROCESSED) {
3677
+ result = outcome.response;
3678
+ } else {
3679
+ // Double check that the transaction is indeed unconfirmed.
3680
+ let signatureStatus:
3681
+ | RpcResponseAndContext<SignatureStatus | null>
3682
+ | null
3683
+ | undefined;
3684
+ while (
3685
+ true // eslint-disable-line no-constant-condition
3686
+ ) {
3687
+ const status = await this.getSignatureStatus(signature);
3688
+ if (status == null) {
3689
+ break;
3690
+ }
3691
+ if (
3692
+ status.context.slot <
3693
+ (outcome.slotInWhichNonceDidAdvance ?? minContextSlot)
3694
+ ) {
3695
+ await sleep(400);
3696
+ continue;
3697
+ }
3698
+ signatureStatus = status;
3518
3699
  break;
3519
- case TransactionStatus.TIMED_OUT:
3520
- throw new TransactionExpiredTimeoutError(
3521
- rawSignature,
3522
- outcome.timeoutMs / 1000,
3523
- );
3700
+ }
3701
+ if (signatureStatus?.value) {
3702
+ const commitmentForStatus = commitment || 'finalized';
3703
+ const {confirmationStatus} = signatureStatus.value;
3704
+ switch (commitmentForStatus) {
3705
+ case 'processed':
3706
+ case 'recent':
3707
+ if (
3708
+ confirmationStatus !== 'processed' &&
3709
+ confirmationStatus !== 'confirmed' &&
3710
+ confirmationStatus !== 'finalized'
3711
+ ) {
3712
+ throw new TransactionExpiredNonceInvalidError(signature);
3713
+ }
3714
+ break;
3715
+ case 'confirmed':
3716
+ case 'single':
3717
+ case 'singleGossip':
3718
+ if (
3719
+ confirmationStatus !== 'confirmed' &&
3720
+ confirmationStatus !== 'finalized'
3721
+ ) {
3722
+ throw new TransactionExpiredNonceInvalidError(signature);
3723
+ }
3724
+ break;
3725
+ case 'finalized':
3726
+ case 'max':
3727
+ case 'root':
3728
+ if (confirmationStatus !== 'finalized') {
3729
+ throw new TransactionExpiredNonceInvalidError(signature);
3730
+ }
3731
+ break;
3732
+ default:
3733
+ // Exhaustive switch.
3734
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
3735
+ ((_: never) => {})(commitmentForStatus);
3736
+ }
3737
+ result = {
3738
+ context: signatureStatus.context,
3739
+ value: {err: signatureStatus.value.err},
3740
+ };
3741
+ } else {
3742
+ throw new TransactionExpiredNonceInvalidError(signature);
3743
+ }
3524
3744
  }
3525
3745
  } finally {
3526
- clearTimeout(timeoutId);
3527
- if (disposeSignatureSubscriptionStateChangeObserver) {
3528
- disposeSignatureSubscriptionStateChangeObserver();
3746
+ done = true;
3747
+ abortConfirmation();
3748
+ }
3749
+ return result;
3750
+ }
3751
+
3752
+ private async confirmTransactionUsingLegacyTimeoutStrategy({
3753
+ commitment,
3754
+ signature,
3755
+ }: {
3756
+ commitment?: Commitment;
3757
+ signature: string;
3758
+ }) {
3759
+ let timeoutId;
3760
+ const expiryPromise = new Promise<{
3761
+ __type: TransactionStatus.TIMED_OUT;
3762
+ timeoutMs: number;
3763
+ }>(resolve => {
3764
+ let timeoutMs = this._confirmTransactionInitialTimeout || 60 * 1000;
3765
+ switch (commitment) {
3766
+ case 'processed':
3767
+ case 'recent':
3768
+ case 'single':
3769
+ case 'confirmed':
3770
+ case 'singleGossip': {
3771
+ timeoutMs = this._confirmTransactionInitialTimeout || 30 * 1000;
3772
+ break;
3773
+ }
3774
+ // exhaust enums to ensure full coverage
3775
+ case 'finalized':
3776
+ case 'max':
3777
+ case 'root':
3529
3778
  }
3530
- if (signatureSubscriptionId) {
3531
- this.removeSignatureListener(signatureSubscriptionId);
3779
+ timeoutId = setTimeout(
3780
+ () => resolve({__type: TransactionStatus.TIMED_OUT, timeoutMs}),
3781
+ timeoutMs,
3782
+ );
3783
+ });
3784
+ const {abortConfirmation, confirmationPromise} =
3785
+ this.getTransactionConfirmationPromise({
3786
+ commitment,
3787
+ signature,
3788
+ });
3789
+ let result: RpcResponseAndContext<SignatureResult>;
3790
+ try {
3791
+ const outcome = await Promise.race([confirmationPromise, expiryPromise]);
3792
+ if (outcome.__type === TransactionStatus.PROCESSED) {
3793
+ result = outcome.response;
3794
+ } else {
3795
+ throw new TransactionExpiredTimeoutError(
3796
+ signature,
3797
+ outcome.timeoutMs / 1000,
3798
+ );
3532
3799
  }
3800
+ } finally {
3801
+ clearTimeout(timeoutId);
3802
+ abortConfirmation();
3533
3803
  }
3534
3804
  return result;
3535
3805
  }
@@ -4682,11 +4952,11 @@ export class Connection {
4682
4952
  */
4683
4953
  async getNonceAndContext(
4684
4954
  nonceAccount: PublicKey,
4685
- commitment?: Commitment,
4955
+ commitmentOrConfig?: Commitment | GetNonceAndContextConfig,
4686
4956
  ): Promise<RpcResponseAndContext<NonceAccount | null>> {
4687
4957
  const {context, value: accountInfo} = await this.getAccountInfoAndContext(
4688
4958
  nonceAccount,
4689
- commitment,
4959
+ commitmentOrConfig,
4690
4960
  );
4691
4961
 
4692
4962
  let value = null;
@@ -4705,9 +4975,9 @@ export class Connection {
4705
4975
  */
4706
4976
  async getNonce(
4707
4977
  nonceAccount: PublicKey,
4708
- commitment?: Commitment,
4978
+ commitmentOrConfig?: Commitment | GetNonceConfig,
4709
4979
  ): Promise<NonceAccount | null> {
4710
- return await this.getNonceAndContext(nonceAccount, commitment)
4980
+ return await this.getNonceAndContext(nonceAccount, commitmentOrConfig)
4711
4981
  .then(x => x.value)
4712
4982
  .catch(e => {
4713
4983
  throw new Error(
@@ -1,7 +1,6 @@
1
1
  import * as BufferLayout from '@solana/buffer-layout';
2
2
  import {Buffer} from 'buffer';
3
3
 
4
- import type {Blockhash} from './blockhash';
5
4
  import * as Layout from './layout';
6
5
  import {PublicKey} from './publickey';
7
6
  import type {FeeCalculator} from './fee-calculator';
@@ -36,9 +35,14 @@ const NonceAccountLayout = BufferLayout.struct<
36
35
 
37
36
  export const NONCE_ACCOUNT_LENGTH = NonceAccountLayout.span;
38
37
 
38
+ /**
39
+ * A durable nonce is a 32 byte value encoded as a base58 string.
40
+ */
41
+ export type DurableNonce = string;
42
+
39
43
  type NonceAccountArgs = {
40
44
  authorizedPubkey: PublicKey;
41
- nonce: Blockhash;
45
+ nonce: DurableNonce;
42
46
  feeCalculator: FeeCalculator;
43
47
  };
44
48
 
@@ -47,7 +51,7 @@ type NonceAccountArgs = {
47
51
  */
48
52
  export class NonceAccount {
49
53
  authorizedPubkey: PublicKey;
50
- nonce: Blockhash;
54
+ nonce: DurableNonce;
51
55
  feeCalculator: FeeCalculator;
52
56
 
53
57
  /**
@@ -33,3 +33,16 @@ export class TransactionExpiredTimeoutError extends Error {
33
33
  Object.defineProperty(TransactionExpiredTimeoutError.prototype, 'name', {
34
34
  value: 'TransactionExpiredTimeoutError',
35
35
  });
36
+
37
+ export class TransactionExpiredNonceInvalidError extends Error {
38
+ signature: string;
39
+
40
+ constructor(signature: string) {
41
+ super(`Signature ${signature} has expired: the nonce is no longer valid.`);
42
+ this.signature = signature;
43
+ }
44
+ }
45
+
46
+ Object.defineProperty(TransactionExpiredNonceInvalidError.prototype, 'name', {
47
+ value: 'TransactionExpiredNonceInvalidError',
48
+ });