@drift-labs/sdk 2.83.0-beta.9 → 2.84.0-beta.1

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.
@@ -25,6 +25,14 @@ import {
25
25
  } from '../types';
26
26
  import { containsComputeUnitIxs } from '../util/computeUnits';
27
27
 
28
+ /**
29
+ * Explanation for SIGNATURE_BLOCK_AND_EXPIRY:
30
+ *
31
+ * When the whileValidTxSender waits for confirmation of a given transaction, it needs the last available blockheight and blockhash used in the signature to do so. For pre-signed transactions, these values aren't attached to the transaction object by default. For a "scrappy" workaround which doesn't break backwards compatibility, the SIGNATURE_BLOCK_AND_EXPIRY property is simply attached to the transaction objects as they are created or signed in this handler despite a mismatch in the typescript types. If the values are attached to the transaction when they reach the whileValidTxSender, it can opt-in to use these values.
32
+ */
33
+
34
+ const DEV_TRY_FORCE_TX_TIMEOUTS = false;
35
+
28
36
  export const COMPUTE_UNITS_DEFAULT = 200_000;
29
37
 
30
38
  export type TxBuildingProps = {
@@ -36,7 +44,7 @@ export type TxBuildingProps = {
36
44
  lookupTables?: AddressLookupTableAccount[];
37
45
  forceVersionedTransaction?: boolean;
38
46
  txParams?: TxParams;
39
- recentBlockHash?: BlockhashWithExpiryBlockHeight;
47
+ recentBlockhash?: BlockhashWithExpiryBlockHeight;
40
48
  wallet?: IWallet;
41
49
  };
42
50
 
@@ -139,6 +147,9 @@ export class TxHandler {
139
147
 
140
148
  const signedTx = await this.signTx(tx, additionalSigners);
141
149
 
150
+ // @ts-ignore
151
+ signedTx.SIGNATURE_BLOCK_AND_EXPIRY = recentBlockhash;
152
+
142
153
  return signedTx;
143
154
  }
144
155
 
@@ -202,15 +213,18 @@ export class TxHandler {
202
213
  public async signVersionedTx(
203
214
  tx: VersionedTransaction,
204
215
  additionalSigners: Array<Signer>,
205
- recentBlockHash?: BlockhashWithExpiryBlockHeight,
216
+ recentBlockhash?: BlockhashWithExpiryBlockHeight,
206
217
  wallet?: IWallet
207
218
  ): Promise<VersionedTransaction> {
208
219
  [wallet] = this.getProps(wallet);
209
220
 
210
- if (recentBlockHash) {
211
- tx.message.recentBlockhash = recentBlockHash.blockhash;
221
+ if (recentBlockhash) {
222
+ tx.message.recentBlockhash = recentBlockhash.blockhash;
212
223
 
213
- this.addHashAndExpiryToLookup(recentBlockHash);
224
+ this.addHashAndExpiryToLookup(recentBlockhash);
225
+
226
+ // @ts-ignore
227
+ tx.SIGNATURE_BLOCK_AND_EXPIRY = recentBlockhash;
214
228
  }
215
229
 
216
230
  additionalSigners
@@ -305,10 +319,10 @@ export class TxHandler {
305
319
  }
306
320
 
307
321
  private _generateVersionedTransaction(
308
- recentBlockHash: BlockhashWithExpiryBlockHeight,
322
+ recentBlockhash: BlockhashWithExpiryBlockHeight,
309
323
  message: Message | MessageV0
310
324
  ) {
311
- this.addHashAndExpiryToLookup(recentBlockHash);
325
+ this.addHashAndExpiryToLookup(recentBlockhash);
312
326
 
313
327
  return new VersionedTransaction(message);
314
328
  }
@@ -326,7 +340,12 @@ export class TxHandler {
326
340
  instructions: ixs,
327
341
  }).compileToLegacyMessage();
328
342
 
329
- return this._generateVersionedTransaction(recentBlockhash, message);
343
+ const tx = this._generateVersionedTransaction(recentBlockhash, message);
344
+
345
+ // @ts-ignore
346
+ tx.SIGNATURE_BLOCK_AND_EXPIRY = recentBlockhash;
347
+
348
+ return tx;
330
349
  }
331
350
 
332
351
  public generateVersionedTransaction(
@@ -343,7 +362,12 @@ export class TxHandler {
343
362
  instructions: ixs,
344
363
  }).compileToV0Message(lookupTableAccounts);
345
364
 
346
- return this._generateVersionedTransaction(recentBlockhash, message);
365
+ const tx = this._generateVersionedTransaction(recentBlockhash, message);
366
+
367
+ // @ts-ignore
368
+ tx.SIGNATURE_BLOCK_AND_EXPIRY = recentBlockhash;
369
+
370
+ return tx;
347
371
  }
348
372
 
349
373
  public generateLegacyTransaction(ixs: TransactionInstruction[]) {
@@ -360,8 +384,8 @@ export class TxHandler {
360
384
  instructions: (TransactionInstruction | TransactionInstruction[])[];
361
385
  }
362
386
  ) {
363
- const recentBlockHash =
364
- props?.recentBlockHash ?? (await this.getLatestBlockhashForTransaction());
387
+ const recentBlockhash =
388
+ props?.recentBlockhash ?? (await this.getLatestBlockhashForTransaction());
365
389
 
366
390
  return await Promise.all(
367
391
  props.instructions.map((ix) => {
@@ -369,7 +393,7 @@ export class TxHandler {
369
393
  return this.buildTransaction({
370
394
  ...props,
371
395
  instructions: ix,
372
- recentBlockHash,
396
+ recentBlockhash,
373
397
  });
374
398
  })
375
399
  );
@@ -433,7 +457,13 @@ export class TxHandler {
433
457
 
434
458
  const computeUnitsPrice = baseTxParams?.computeUnitsPrice;
435
459
 
436
- if (computeUnitsPrice > 0 && !hasSetComputeUnitPriceIx) {
460
+ if (process.env.DEV_TRY_FORCE_TX_TIMEOUTS || DEV_TRY_FORCE_TX_TIMEOUTS) {
461
+ allIx.push(
462
+ ComputeBudgetProgram.setComputeUnitPrice({
463
+ microLamports: 0,
464
+ })
465
+ );
466
+ } else if (computeUnitsPrice > 0 && !hasSetComputeUnitPriceIx) {
437
467
  allIx.push(
438
468
  ComputeBudgetProgram.setComputeUnitPrice({
439
469
  microLamports: computeUnitsPrice,
@@ -443,13 +473,13 @@ export class TxHandler {
443
473
 
444
474
  allIx.push(...instructionsArray);
445
475
 
446
- const recentBlockHash =
447
- props?.recentBlockHash ?? (await this.getLatestBlockhashForTransaction());
476
+ const recentBlockhash =
477
+ props?.recentBlockhash ?? (await this.getLatestBlockhashForTransaction());
448
478
 
449
479
  // # Create and return Transaction
450
480
  if (txVersion === 'legacy') {
451
481
  if (forceVersionedTransaction) {
452
- return this.generateLegacyVersionedTransaction(recentBlockHash, allIx);
482
+ return this.generateLegacyVersionedTransaction(recentBlockhash, allIx);
453
483
  } else {
454
484
  return this.generateLegacyTransaction(allIx);
455
485
  }
@@ -461,7 +491,7 @@ export class TxHandler {
461
491
  : [marketLookupTable];
462
492
 
463
493
  return this.generateVersionedTransaction(
464
- recentBlockHash,
494
+ recentBlockhash,
465
495
  allIx,
466
496
  lookupTables
467
497
  );
@@ -482,7 +512,13 @@ export class TxHandler {
482
512
  );
483
513
  }
484
514
 
485
- if (computeUnitsPrice != 0) {
515
+ if (process.env.DEV_TRY_FORCE_TX_TIMEOUTS || DEV_TRY_FORCE_TX_TIMEOUTS) {
516
+ tx.add(
517
+ ComputeBudgetProgram.setComputeUnitPrice({
518
+ microLamports: 0,
519
+ })
520
+ );
521
+ } else if (computeUnitsPrice != 0) {
486
522
  tx.add(
487
523
  ComputeBudgetProgram.setComputeUnitPrice({
488
524
  microLamports: computeUnitsPrice,
@@ -506,18 +542,21 @@ export class TxHandler {
506
542
  keys: string[],
507
543
  wallet?: IWallet,
508
544
  commitment?: Commitment,
509
- recentBlockHash?: BlockhashWithExpiryBlockHeight
545
+ recentBlockhash?: BlockhashWithExpiryBlockHeight
510
546
  ) {
511
- recentBlockHash = recentBlockHash
512
- ? recentBlockHash
547
+ recentBlockhash = recentBlockhash
548
+ ? recentBlockhash
513
549
  : await this.getLatestBlockhashForTransaction();
514
550
 
515
- this.addHashAndExpiryToLookup(recentBlockHash);
551
+ this.addHashAndExpiryToLookup(recentBlockhash);
516
552
 
517
553
  for (const tx of txsToSign) {
518
554
  if (!tx) continue;
519
- tx.recentBlockhash = recentBlockHash.blockhash;
555
+ tx.recentBlockhash = recentBlockhash.blockhash;
520
556
  tx.feePayer = wallet?.publicKey ?? this.wallet?.publicKey;
557
+
558
+ // @ts-ignore
559
+ tx.SIGNATURE_BLOCK_AND_EXPIRY = recentBlockhash;
521
560
  }
522
561
 
523
562
  return this.getSignedTransactionMap(txsToSign, keys, wallet);
@@ -536,18 +575,21 @@ export class TxHandler {
536
575
  keys: string[],
537
576
  wallet?: IWallet,
538
577
  commitment?: Commitment,
539
- recentBlockHash?: BlockhashWithExpiryBlockHeight
578
+ recentBlockhash?: BlockhashWithExpiryBlockHeight
540
579
  ) {
541
- recentBlockHash = recentBlockHash
542
- ? recentBlockHash
580
+ recentBlockhash = recentBlockhash
581
+ ? recentBlockhash
543
582
  : await this.getLatestBlockhashForTransaction();
544
583
 
545
- this.addHashAndExpiryToLookup(recentBlockHash);
584
+ this.addHashAndExpiryToLookup(recentBlockhash);
546
585
 
547
586
  for (const tx of txsToSign) {
548
587
  if (!tx) continue;
549
- tx.recentBlockhash = recentBlockHash.blockhash;
588
+ tx.recentBlockhash = recentBlockhash.blockhash;
550
589
  tx.feePayer = wallet?.publicKey ?? this.wallet?.publicKey;
590
+
591
+ // @ts-ignore
592
+ tx.SIGNATURE_BLOCK_AND_EXPIRY = recentBlockhash;
551
593
  }
552
594
 
553
595
  return this.getSignedTransactionMap(txsToSign, keys, wallet);
@@ -584,13 +626,20 @@ export class TxHandler {
584
626
 
585
627
  this.preSignedCb?.();
586
628
 
587
- const signedTxs = await wallet.signAllTransactions(
588
- txsToSign
589
- .map((tx) => {
590
- return tx as Transaction;
591
- })
592
- .filter((tx) => tx !== undefined)
593
- );
629
+ const filteredTxs = txsToSign
630
+ .map((tx) => {
631
+ return tx as Transaction;
632
+ })
633
+ .filter((tx) => tx !== undefined);
634
+
635
+ const signedTxs = await wallet.signAllTransactions(filteredTxs);
636
+
637
+ signedTxs.forEach((signedTx, index) => {
638
+ // @ts-ignore
639
+ signedTx.SIGNATURE_BLOCK_AND_EXPIRY =
640
+ // @ts-ignore
641
+ txsToSign[index]?.SIGNATURE_BLOCK_AND_EXPIRY;
642
+ });
594
643
 
595
644
  this.handleSignedTxData(
596
645
  signedTxs.map((signedTx) => {
@@ -622,15 +671,14 @@ export class TxHandler {
622
671
  ) {
623
672
  const transactions = await this.buildBulkTransactions(props);
624
673
 
625
- const preppedTransactions =
626
- props.txVersion === 'legacy'
627
- ? this.getPreparedAndSignedLegacyTransactionMap(
628
- transactions as Transaction[],
629
- props.keys,
630
- props.wallet,
631
- props.preFlightCommitment
632
- )
633
- : this.getSignedTransactionMap(transactions, props.keys, props.wallet);
674
+ const preppedTransactions = await (props.txVersion === 'legacy'
675
+ ? this.getPreparedAndSignedLegacyTransactionMap(
676
+ transactions as Transaction[],
677
+ props.keys,
678
+ props.wallet,
679
+ props.preFlightCommitment
680
+ )
681
+ : this.getSignedTransactionMap(transactions, props.keys, props.wallet));
634
682
 
635
683
  return preppedTransactions;
636
684
  }
@@ -15,6 +15,8 @@ import { IWallet } from '../types';
15
15
 
16
16
  const DEFAULT_RETRY = 2000;
17
17
 
18
+ const VALID_BLOCK_HEIGHT_OFFSET = -150; // This is a bit of weirdness but the lastValidBlockHeight value returned from connection.getLatestBlockhash is always 300 blocks ahead of the current block, even though the transaction actually expires after 150 blocks. This accounts for that so that we can at least accuractely estimate the transaction expiry.
19
+
18
20
  type ResolveReference = {
19
21
  resolve?: () => void;
20
22
  };
@@ -93,6 +95,13 @@ export class WhileValidTxSender extends BaseTxSender {
93
95
  );
94
96
  }
95
97
 
98
+ // See SIGNATURE_BLOCK_AND_EXPIRY explanation in txHandler.ts if this is confusing
99
+ // @ts-ignore
100
+ if (preSigned && tx.SIGNATURE_BLOCK_AND_EXPIRY) {
101
+ // @ts-ignore
102
+ latestBlockhash = tx.SIGNATURE_BLOCK_AND_EXPIRY;
103
+ }
104
+
96
105
  // handle subclass-specific side effects
97
106
  const txSig = bs58.encode(
98
107
  signedTx.signatures[0]?.signature || signedTx.signatures[0]
@@ -108,12 +117,20 @@ export class WhileValidTxSender extends BaseTxSender {
108
117
  opts?: ConfirmOptions,
109
118
  preSigned?: boolean
110
119
  ): Promise<TxSigAndSlot> {
111
- const latestBlockhash =
120
+ let latestBlockhash =
112
121
  await this.txHandler.getLatestBlockhashForTransaction();
113
122
 
114
123
  let signedTx;
115
124
  if (preSigned) {
116
125
  signedTx = tx;
126
+
127
+ // See SIGNATURE_BLOCK_AND_EXPIRY explanation in txHandler.ts if this is confusing
128
+ // @ts-ignore
129
+ if (tx.SIGNATURE_BLOCK_AND_EXPIRY) {
130
+ // @ts-ignore
131
+ latestBlockhash = tx.SIGNATURE_BLOCK_AND_EXPIRY;
132
+ }
133
+
117
134
  // @ts-ignore
118
135
  } else if (this.wallet.payer) {
119
136
  tx.message.recentBlockhash = latestBlockhash.blockhash;
@@ -185,7 +202,8 @@ export class WhileValidTxSender extends BaseTxSender {
185
202
  const result = await this.connection.confirmTransaction(
186
203
  {
187
204
  signature: txid,
188
- lastValidBlockHeight,
205
+ lastValidBlockHeight:
206
+ lastValidBlockHeight + VALID_BLOCK_HEIGHT_OFFSET,
189
207
  blockhash,
190
208
  },
191
209
  opts.commitment
@@ -29,6 +29,7 @@ import { Buffer } from 'buffer';
29
29
  import { ZSTDDecoder } from 'zstddec';
30
30
  import { getNonIdleUserFilter, getUserFilter } from '../memcmp';
31
31
  import {
32
+ SyncConfig,
32
33
  UserAccountFilterCriteria as UserFilterCriteria,
33
34
  UserMapConfig,
34
35
  } from './userMapConfig';
@@ -83,6 +84,7 @@ export class UserMap implements UserMapInterface {
83
84
  };
84
85
  private decode;
85
86
  private mostRecentSlot = 0;
87
+ private syncConfig: SyncConfig;
86
88
 
87
89
  private syncPromise?: Promise<void>;
88
90
  private syncPromiseResolver: () => void;
@@ -132,6 +134,10 @@ export class UserMap implements UserMapInterface {
132
134
  decodeFn,
133
135
  });
134
136
  }
137
+
138
+ this.syncConfig = config.syncConfig ?? {
139
+ type: 'default',
140
+ };
135
141
  }
136
142
 
137
143
  public async subscribe() {
@@ -345,6 +351,14 @@ export class UserMap implements UserMapInterface {
345
351
  }
346
352
 
347
353
  public async sync() {
354
+ if (this.syncConfig.type === 'default') {
355
+ return this.defaultSync();
356
+ } else {
357
+ return this.paginatedSync();
358
+ }
359
+ }
360
+
361
+ private async defaultSync() {
348
362
  if (this.syncPromise) {
349
363
  return this.syncPromise;
350
364
  }
@@ -438,6 +452,123 @@ export class UserMap implements UserMapInterface {
438
452
  }
439
453
  }
440
454
 
455
+ private async paginatedSync() {
456
+ if (this.syncPromise) {
457
+ return this.syncPromise;
458
+ }
459
+
460
+ this.syncPromise = new Promise<void>((resolve) => {
461
+ this.syncPromiseResolver = resolve;
462
+ });
463
+
464
+ try {
465
+ const accountsPrefetch = await this.connection.getProgramAccounts(
466
+ this.driftClient.program.programId,
467
+ {
468
+ dataSlice: { offset: 0, length: 0 },
469
+ filters: [
470
+ getUserFilter(),
471
+ ...(!this.includeIdle ? [getNonIdleUserFilter()] : []),
472
+ ],
473
+ }
474
+ );
475
+ const accountPublicKeys = accountsPrefetch.map(
476
+ (account) => account.pubkey
477
+ );
478
+
479
+ const limitConcurrency = async (tasks, limit) => {
480
+ const executing = [];
481
+ const results = [];
482
+
483
+ for (let i = 0; i < tasks.length; i++) {
484
+ const executor = Promise.resolve().then(tasks[i]);
485
+ results.push(executor);
486
+
487
+ if (executing.length < limit) {
488
+ executing.push(executor);
489
+ executor.finally(() => {
490
+ const index = executing.indexOf(executor);
491
+ if (index > -1) {
492
+ executing.splice(index, 1);
493
+ }
494
+ });
495
+ } else {
496
+ await Promise.race(executing);
497
+ }
498
+ }
499
+
500
+ return Promise.all(results);
501
+ };
502
+
503
+ const programAccountBufferMap = new Map<string, Buffer>();
504
+
505
+ // @ts-ignore
506
+ const chunkSize = this.syncConfig.chunkSize ?? 100;
507
+ const tasks = [];
508
+ for (let i = 0; i < accountPublicKeys.length; i += chunkSize) {
509
+ const chunk = accountPublicKeys.slice(i, i + chunkSize);
510
+ tasks.push(async () => {
511
+ const accountInfos =
512
+ await this.connection.getMultipleAccountsInfoAndContext(chunk, {
513
+ commitment: this.commitment,
514
+ });
515
+
516
+ const accountInfosSlot = accountInfos.context.slot;
517
+
518
+ for (let j = 0; j < accountInfos.value.length; j += 1) {
519
+ const accountInfo = accountInfos.value[j];
520
+ if (accountInfo === null) continue;
521
+
522
+ const publicKeyString = chunk[j].toString();
523
+ const buffer = Buffer.from(accountInfo.data);
524
+ programAccountBufferMap.set(publicKeyString, buffer);
525
+
526
+ const decodedUser = this.decode('User', buffer);
527
+
528
+ const currAccountWithSlot = this.getWithSlot(publicKeyString);
529
+ if (
530
+ currAccountWithSlot &&
531
+ currAccountWithSlot.slot <= accountInfosSlot
532
+ ) {
533
+ this.updateUserAccount(
534
+ publicKeyString,
535
+ decodedUser,
536
+ accountInfosSlot
537
+ );
538
+ } else {
539
+ await this.addPubkey(
540
+ new PublicKey(publicKeyString),
541
+ decodedUser,
542
+ accountInfosSlot
543
+ );
544
+ }
545
+ }
546
+ });
547
+ }
548
+
549
+ // @ts-ignore
550
+ const concurrencyLimit = this.syncConfig.concurrencyLimit ?? 10;
551
+ await limitConcurrency(tasks, concurrencyLimit);
552
+
553
+ for (const [key] of this.entries()) {
554
+ if (!programAccountBufferMap.has(key)) {
555
+ const user = this.get(key);
556
+ if (user) {
557
+ await user.unsubscribe();
558
+ this.userMap.delete(key);
559
+ }
560
+ }
561
+ }
562
+ } catch (err) {
563
+ console.error(`Error in UserMap.sync():`, err);
564
+ } finally {
565
+ if (this.syncPromiseResolver) {
566
+ this.syncPromiseResolver();
567
+ }
568
+ this.syncPromise = undefined;
569
+ }
570
+ }
571
+
441
572
  public async unsubscribe() {
442
573
  await this.subscription.unsubscribe();
443
574
 
@@ -7,6 +7,16 @@ export type UserAccountFilterCriteria = {
7
7
  hasOpenOrders: boolean;
8
8
  };
9
9
 
10
+ export type SyncConfig =
11
+ | {
12
+ type: 'default';
13
+ }
14
+ | {
15
+ type: 'paginated';
16
+ chunkSize?: number;
17
+ concurrencyLimit?: number;
18
+ };
19
+
10
20
  export type UserMapConfig = {
11
21
  driftClient: DriftClient;
12
22
  // connection object to use specifically for the UserMap. If undefined, will use the driftClient's connection
@@ -36,4 +46,6 @@ export type UserMapConfig = {
36
46
  // If true, will not do a full sync whenever StateAccount.numberOfSubAccounts changes.
37
47
  // default behavior is to do a full sync on changes.
38
48
  disableSyncOnTotalAccountsChange?: boolean;
49
+
50
+ syncConfig?: SyncConfig;
39
51
  };