@drift-labs/sdk 2.83.0-beta.1 → 2.83.0-beta.11

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.
Files changed (56) hide show
  1. package/VERSION +1 -1
  2. package/lib/adminClient.d.ts +2 -2
  3. package/lib/adminClient.js +7 -9
  4. package/lib/clock/clockSubscriber.d.ts +4 -2
  5. package/lib/clock/clockSubscriber.js +8 -2
  6. package/lib/decode/phoenix.d.ts +6 -0
  7. package/lib/decode/phoenix.js +159 -0
  8. package/lib/driftClient.d.ts +5 -2
  9. package/lib/driftClient.js +92 -47
  10. package/lib/idl/drift.json +63 -0
  11. package/lib/index.d.ts +1 -0
  12. package/lib/index.js +1 -0
  13. package/lib/math/auction.js +2 -2
  14. package/lib/math/market.js +4 -1
  15. package/lib/math/orders.js +1 -1
  16. package/lib/phoenix/phoenixSubscriber.d.ts +2 -0
  17. package/lib/phoenix/phoenixSubscriber.js +15 -2
  18. package/lib/priorityFee/priorityFeeSubscriber.js +1 -1
  19. package/lib/tx/baseTxSender.d.ts +5 -2
  20. package/lib/tx/baseTxSender.js +30 -1
  21. package/lib/tx/fastSingleTxSender.d.ts +1 -1
  22. package/lib/tx/fastSingleTxSender.js +1 -0
  23. package/lib/tx/forwardOnlyTxSender.d.ts +1 -1
  24. package/lib/tx/retryTxSender.d.ts +1 -1
  25. package/lib/tx/retryTxSender.js +1 -0
  26. package/lib/tx/txHandler.d.ts +2 -0
  27. package/lib/tx/txHandler.js +16 -9
  28. package/lib/tx/types.d.ts +2 -1
  29. package/lib/tx/whileValidTxSender.d.ts +1 -1
  30. package/lib/tx/whileValidTxSender.js +1 -0
  31. package/lib/types.d.ts +9 -0
  32. package/lib/types.js +6 -1
  33. package/lib/util/computeUnits.d.ts +7 -1
  34. package/lib/util/computeUnits.js +31 -1
  35. package/package.json +1 -1
  36. package/src/adminClient.ts +12 -10
  37. package/src/clock/clockSubscriber.ts +12 -4
  38. package/src/decode/phoenix.ts +207 -0
  39. package/src/driftClient.ts +188 -104
  40. package/src/idl/drift.json +63 -0
  41. package/src/index.ts +1 -0
  42. package/src/math/auction.ts +2 -2
  43. package/src/math/market.ts +4 -1
  44. package/src/math/orders.ts +5 -2
  45. package/src/phoenix/phoenixSubscriber.ts +15 -3
  46. package/src/priorityFee/priorityFeeSubscriber.ts +1 -1
  47. package/src/tx/baseTxSender.ts +59 -2
  48. package/src/tx/fastSingleTxSender.ts +2 -1
  49. package/src/tx/forwardOnlyTxSender.ts +1 -1
  50. package/src/tx/retryTxSender.ts +4 -1
  51. package/src/tx/txHandler.ts +19 -7
  52. package/src/tx/types.ts +11 -0
  53. package/src/tx/whileValidTxSender.ts +4 -1
  54. package/src/types.ts +6 -0
  55. package/src/util/computeUnits.ts +43 -1
  56. package/tests/decode/phoenix.ts +71 -0
@@ -7,7 +7,7 @@ import {
7
7
  Order,
8
8
  PositionDirection,
9
9
  } from '../types';
10
- import { ZERO, TWO } from '../constants/numericConstants';
10
+ import { ZERO, TWO, ONE } from '../constants/numericConstants';
11
11
  import { BN } from '@coral-xyz/anchor';
12
12
  import { OraclePriceData } from '../oracles/types';
13
13
  import {
@@ -160,7 +160,10 @@ export function getLimitPrice(
160
160
  if (hasAuctionPrice(order, slot)) {
161
161
  limitPrice = getAuctionPrice(order, slot, oraclePriceData.price);
162
162
  } else if (order.oraclePriceOffset !== 0) {
163
- limitPrice = oraclePriceData.price.add(new BN(order.oraclePriceOffset));
163
+ limitPrice = BN.max(
164
+ oraclePriceData.price.add(new BN(order.oraclePriceOffset)),
165
+ ONE
166
+ );
164
167
  } else if (order.price.eq(ZERO)) {
165
168
  limitPrice = fallbackPrice;
166
169
  } else {
@@ -10,6 +10,7 @@ import {
10
10
  import { PRICE_PRECISION } from '../constants/numericConstants';
11
11
  import { BN } from '@coral-xyz/anchor';
12
12
  import { L2Level, L2OrderBookGenerator } from '../dlob/orderBookLevels';
13
+ import { fastDecode } from '../decode/phoenix';
13
14
 
14
15
  export type PhoenixMarketSubscriberConfig = {
15
16
  connection: Connection;
@@ -24,6 +25,7 @@ export type PhoenixMarketSubscriberConfig = {
24
25
  | {
25
26
  type: 'websocket';
26
27
  };
28
+ fastDecode?: boolean;
27
29
  };
28
30
 
29
31
  export class PhoenixSubscriber implements L2OrderBookGenerator {
@@ -36,7 +38,8 @@ export class PhoenixSubscriber implements L2OrderBookGenerator {
36
38
  market: Market;
37
39
  marketCallbackId: string | number;
38
40
  clockCallbackId: string | number;
39
-
41
+ // fastDecode omits trader data from the markets for faster decoding process
42
+ fastDecode: boolean;
40
43
  subscribed: boolean;
41
44
  lastSlot: number;
42
45
  lastUnixTimestamp: number;
@@ -53,6 +56,7 @@ export class PhoenixSubscriber implements L2OrderBookGenerator {
53
56
  }
54
57
  this.lastSlot = 0;
55
58
  this.lastUnixTimestamp = 0;
59
+ this.fastDecode = config.fastDecode ?? true;
56
60
  }
57
61
 
58
62
  public async subscribe(): Promise<void> {
@@ -76,7 +80,11 @@ export class PhoenixSubscriber implements L2OrderBookGenerator {
76
80
  this.marketAddress,
77
81
  (accountInfo, _ctx) => {
78
82
  try {
79
- this.market = this.market.reload(accountInfo.data);
83
+ if (this.fastDecode) {
84
+ this.market.data = fastDecode(accountInfo.data);
85
+ } else {
86
+ this.market = this.market.reload(accountInfo.data);
87
+ }
80
88
  } catch {
81
89
  console.error('Failed to reload Phoenix market data');
82
90
  }
@@ -101,7 +109,11 @@ export class PhoenixSubscriber implements L2OrderBookGenerator {
101
109
  try {
102
110
  this.lastSlot = slot;
103
111
  if (buffer) {
104
- this.market = this.market.reload(buffer);
112
+ if (this.fastDecode) {
113
+ this.market.data = fastDecode(buffer);
114
+ } else {
115
+ this.market = this.market.reload(buffer);
116
+ }
105
117
  }
106
118
  } catch {
107
119
  console.error('Failed to reload Phoenix market data');
@@ -96,8 +96,8 @@ export class PriorityFeeSubscriber {
96
96
  return;
97
97
  }
98
98
 
99
+ this.intervalId = setInterval(this.load.bind(this), this.frequencyMs); // we set the intervalId first, preventing a side effect of unsubscribing failing during the race condition where unsubscribes happens before subscribe is finished
99
100
  await this.load();
100
- this.intervalId = setInterval(this.load.bind(this), this.frequencyMs);
101
101
  }
102
102
 
103
103
  private async loadForSolana(): Promise<void> {
@@ -15,6 +15,10 @@ import {
15
15
  TransactionSignature,
16
16
  Connection,
17
17
  VersionedTransaction,
18
+ SendTransactionError,
19
+ TransactionInstruction,
20
+ AddressLookupTableAccount,
21
+ BlockhashWithExpiryBlockHeight,
18
22
  } from '@solana/web3.js';
19
23
  import { AnchorProvider } from '@coral-xyz/anchor';
20
24
  import assert from 'assert';
@@ -53,7 +57,7 @@ export abstract class BaseTxSender implements TxSender {
53
57
  additionalConnections?;
54
58
  confirmationStrategy?: ConfirmationStrategy;
55
59
  additionalTxSenderCallbacks?: ((base58EncodedTx: string) => void)[];
56
- txHandler: TxHandler;
60
+ txHandler?: TxHandler;
57
61
  }) {
58
62
  this.connection = connection;
59
63
  this.wallet = wallet;
@@ -62,7 +66,13 @@ export abstract class BaseTxSender implements TxSender {
62
66
  this.additionalConnections = additionalConnections;
63
67
  this.confirmationStrategy = confirmationStrategy;
64
68
  this.additionalTxSenderCallbacks = additionalTxSenderCallbacks;
65
- this.txHandler = txHandler;
69
+ this.txHandler =
70
+ txHandler ??
71
+ new TxHandler({
72
+ connection: this.connection,
73
+ wallet: this.wallet,
74
+ confirmationOptions: this.opts,
75
+ });
66
76
  }
67
77
 
68
78
  async send(
@@ -103,6 +113,21 @@ export abstract class BaseTxSender implements TxSender {
103
113
  );
104
114
  }
105
115
 
116
+ async getVersionedTransaction(
117
+ ixs: TransactionInstruction[],
118
+ lookupTableAccounts: AddressLookupTableAccount[],
119
+ _additionalSigners?: Array<Signer>,
120
+ opts?: ConfirmOptions,
121
+ blockhash?: BlockhashWithExpiryBlockHeight
122
+ ): Promise<VersionedTransaction> {
123
+ return this.txHandler.generateVersionedTransaction(
124
+ blockhash,
125
+ ixs,
126
+ lookupTableAccounts,
127
+ this.wallet
128
+ );
129
+ }
130
+
106
131
  async sendVersionedTransaction(
107
132
  tx: VersionedTransaction,
108
133
  additionalSigners?: Array<Signer>,
@@ -342,4 +367,36 @@ export abstract class BaseTxSender implements TxSender {
342
367
  public getTimeoutCount(): number {
343
368
  return this.timeoutCount;
344
369
  }
370
+
371
+ public async checkConfirmationResultForError(
372
+ txSig: string,
373
+ result: RpcResponseAndContext<SignatureResult>
374
+ ) {
375
+ if (result.value.err) {
376
+ await this.reportTransactionError(txSig);
377
+ }
378
+
379
+ return;
380
+ }
381
+
382
+ public async reportTransactionError(txSig: string) {
383
+ const transactionResult = await this.connection.getTransaction(txSig, {
384
+ maxSupportedTransactionVersion: 0,
385
+ });
386
+
387
+ if (!transactionResult?.meta?.err) {
388
+ return undefined;
389
+ }
390
+
391
+ const logs = transactionResult.meta.logMessages;
392
+
393
+ const lastLog = logs[logs.length - 1];
394
+
395
+ const friendlyMessage = lastLog?.match(/(failed:) (.+)/)?.[2];
396
+
397
+ throw new SendTransactionError(
398
+ `Transaction Failed${friendlyMessage ? `: ${friendlyMessage}` : ''}`,
399
+ transactionResult.meta.logMessages
400
+ );
401
+ }
345
402
  }
@@ -48,7 +48,7 @@ export class FastSingleTxSender extends BaseTxSender {
48
48
  skipConfirmation?: boolean;
49
49
  blockhashCommitment?: Commitment;
50
50
  confirmationStrategy?: ConfirmationStrategy;
51
- txHandler: TxHandler;
51
+ txHandler?: TxHandler;
52
52
  }) {
53
53
  super({
54
54
  connection,
@@ -101,6 +101,7 @@ export class FastSingleTxSender extends BaseTxSender {
101
101
  if (!this.skipConfirmation) {
102
102
  try {
103
103
  const result = await this.confirmTransaction(txid, opts.commitment);
104
+ await this.checkConfirmationResultForError(txid, result);
104
105
  slot = result.context.slot;
105
106
  } catch (e) {
106
107
  console.error(e);
@@ -43,7 +43,7 @@ export class ForwardOnlyTxSender extends BaseTxSender {
43
43
  retrySleep?: number;
44
44
  confirmationStrategy?: ConfirmationStrategy;
45
45
  additionalTxSenderCallbacks?: ((base58EncodedTx: string) => void)[];
46
- txHandler: TxHandler;
46
+ txHandler?: TxHandler;
47
47
  }) {
48
48
  super({
49
49
  connection,
@@ -40,7 +40,7 @@ export class RetryTxSender extends BaseTxSender {
40
40
  additionalConnections?;
41
41
  confirmationStrategy?: ConfirmationStrategy;
42
42
  additionalTxSenderCallbacks?: ((base58EncodedTx: string) => void)[];
43
- txHandler: TxHandler;
43
+ txHandler?: TxHandler;
44
44
  }) {
45
45
  super({
46
46
  connection,
@@ -105,6 +105,9 @@ export class RetryTxSender extends BaseTxSender {
105
105
  let slot: number;
106
106
  try {
107
107
  const result = await this.confirmTransaction(txid, opts.commitment);
108
+
109
+ await this.checkConfirmationResultForError(txid, result);
110
+
108
111
  slot = result.context.slot;
109
112
  // eslint-disable-next-line no-useless-catch
110
113
  } catch (e) {
@@ -23,6 +23,7 @@ import {
23
23
  SignedTxData,
24
24
  TxParams,
25
25
  } from '../types';
26
+ import { containsComputeUnitIxs } from '../util/computeUnits';
26
27
 
27
28
  export const COMPUTE_UNITS_DEFAULT = 200_000;
28
29
 
@@ -50,6 +51,7 @@ export class TxHandler {
50
51
  private wallet: IWallet;
51
52
  private confirmationOptions: ConfirmOptions;
52
53
 
54
+ private preSignedCb?: () => void;
53
55
  private onSignedCb?: (txSigs: DriftClientMetricsEvents['txSigned']) => void;
54
56
 
55
57
  constructor(props: {
@@ -59,6 +61,7 @@ export class TxHandler {
59
61
  opts?: {
60
62
  returnBlockHeightsWithSignedTxCallbackData?: boolean;
61
63
  onSignedCb?: (txSigs: DriftClientMetricsEvents['txSigned']) => void;
64
+ preSignedCb?: () => void;
62
65
  };
63
66
  }) {
64
67
  this.connection = props.connection;
@@ -69,6 +72,7 @@ export class TxHandler {
69
72
  this.returnBlockHeightsWithSignedTxCallbackData =
70
73
  props.opts?.returnBlockHeightsWithSignedTxCallbackData ?? false;
71
74
  this.onSignedCb = props.opts?.onSignedCb;
75
+ this.preSignedCb = props.opts?.preSignedCb;
72
76
  }
73
77
 
74
78
  private addHashAndExpiryToLookup(
@@ -177,6 +181,8 @@ export class TxHandler {
177
181
  tx.partialSign(kp);
178
182
  });
179
183
 
184
+ this.preSignedCb?.();
185
+
180
186
  const signedTx = await wallet.signTransaction(tx);
181
187
 
182
188
  // Turn txSig Buffer into base58 string
@@ -213,6 +219,8 @@ export class TxHandler {
213
219
  tx.sign([kp]);
214
220
  });
215
221
 
222
+ this.preSignedCb?.();
223
+
216
224
  //@ts-ignore
217
225
  const signedTx = (await wallet.signTransaction(tx)) as VersionedTransaction;
218
226
 
@@ -406,10 +414,16 @@ export class TxHandler {
406
414
  };
407
415
  }
408
416
 
417
+ const instructionsArray = Array.isArray(instructions)
418
+ ? instructions
419
+ : [instructions];
420
+ const { hasSetComputeUnitLimitIx, hasSetComputeUnitPriceIx } =
421
+ containsComputeUnitIxs(instructionsArray);
422
+
409
423
  // # Create Tx Instructions
410
424
  const allIx = [];
411
425
  const computeUnits = baseTxParams?.computeUnits;
412
- if (computeUnits !== 200_000) {
426
+ if (computeUnits > 0 && !hasSetComputeUnitLimitIx) {
413
427
  allIx.push(
414
428
  ComputeBudgetProgram.setComputeUnitLimit({
415
429
  units: computeUnits,
@@ -419,7 +433,7 @@ export class TxHandler {
419
433
 
420
434
  const computeUnitsPrice = baseTxParams?.computeUnitsPrice;
421
435
 
422
- if (computeUnitsPrice !== 0) {
436
+ if (computeUnitsPrice > 0 && !hasSetComputeUnitPriceIx) {
423
437
  allIx.push(
424
438
  ComputeBudgetProgram.setComputeUnitPrice({
425
439
  microLamports: computeUnitsPrice,
@@ -427,11 +441,7 @@ export class TxHandler {
427
441
  );
428
442
  }
429
443
 
430
- if (Array.isArray(instructions)) {
431
- allIx.push(...instructions);
432
- } else {
433
- allIx.push(instructions);
434
- }
444
+ allIx.push(...instructionsArray);
435
445
 
436
446
  const recentBlockHash =
437
447
  props?.recentBlockHash ?? (await this.getLatestBlockhashForTransaction());
@@ -572,6 +582,8 @@ export class TxHandler {
572
582
  }
573
583
  });
574
584
 
585
+ this.preSignedCb?.();
586
+
575
587
  const signedTxs = await wallet.signAllTransactions(
576
588
  txsToSign
577
589
  .map((tx) => {
package/src/tx/types.ts CHANGED
@@ -1,7 +1,10 @@
1
1
  import {
2
+ AddressLookupTableAccount,
3
+ BlockhashWithExpiryBlockHeight,
2
4
  ConfirmOptions,
3
5
  Signer,
4
6
  Transaction,
7
+ TransactionInstruction,
5
8
  TransactionSignature,
6
9
  VersionedTransaction,
7
10
  } from '@solana/web3.js';
@@ -35,6 +38,14 @@ export interface TxSender {
35
38
  preSigned?: boolean
36
39
  ): Promise<TxSigAndSlot>;
37
40
 
41
+ getVersionedTransaction(
42
+ ixs: TransactionInstruction[],
43
+ lookupTableAccounts: AddressLookupTableAccount[],
44
+ additionalSigners?: Array<Signer>,
45
+ opts?: ConfirmOptions,
46
+ blockhash?: BlockhashWithExpiryBlockHeight
47
+ ): Promise<VersionedTransaction>;
48
+
38
49
  sendRawTransaction(
39
50
  rawTransaction: Buffer | Uint8Array,
40
51
  opts: ConfirmOptions
@@ -50,7 +50,7 @@ export class WhileValidTxSender extends BaseTxSender {
50
50
  additionalConnections?;
51
51
  additionalTxSenderCallbacks?: ((base58EncodedTx: string) => void)[];
52
52
  blockhashCommitment?: Commitment;
53
- txHandler: TxHandler;
53
+ txHandler?: TxHandler;
54
54
  }) {
55
55
  super({
56
56
  connection,
@@ -190,6 +190,9 @@ export class WhileValidTxSender extends BaseTxSender {
190
190
  },
191
191
  opts.commitment
192
192
  );
193
+
194
+ await this.checkConfirmationResultForError(txid, result);
195
+
193
196
  slot = result.context.slot;
194
197
  // eslint-disable-next-line no-useless-catch
195
198
  } catch (e) {
package/src/types.ts CHANGED
@@ -234,6 +234,11 @@ export class StakeAction {
234
234
  static readonly STAKE_TRANSFER = { stakeTransfer: {} };
235
235
  }
236
236
 
237
+ export class SettlePnlMode {
238
+ static readonly TRY_SETTLE = { trySettle: {} };
239
+ static readonly MUST_SETTLE = { mustSettle: {} };
240
+ }
241
+
237
242
  export function isVariant(object: unknown, type: string) {
238
243
  return object.hasOwnProperty(type);
239
244
  }
@@ -1187,6 +1192,7 @@ export type HealthComponent = {
1187
1192
 
1188
1193
  export interface DriftClientMetricsEvents {
1189
1194
  txSigned: SignedTxData[];
1195
+ preTxSigned: void;
1190
1196
  }
1191
1197
 
1192
1198
  export type SignedTxData = {
@@ -1,4 +1,10 @@
1
- import { Connection, Finality, PublicKey } from '@solana/web3.js';
1
+ import {
2
+ ComputeBudgetProgram,
3
+ Connection,
4
+ Finality,
5
+ PublicKey,
6
+ TransactionInstruction,
7
+ } from '@solana/web3.js';
2
8
 
3
9
  export async function findComputeUnitConsumption(
4
10
  programId: PublicKey,
@@ -19,3 +25,39 @@ export async function findComputeUnitConsumption(
19
25
  });
20
26
  return computeUnits;
21
27
  }
28
+
29
+ export function isSetComputeUnitsIx(ix: TransactionInstruction): boolean {
30
+ // Compute budget program discriminator is first byte
31
+ // 2: set compute unit limit
32
+ // 3: set compute unit price
33
+ if (
34
+ ix.programId.equals(ComputeBudgetProgram.programId) &&
35
+ ix.data.at(0) === 2
36
+ ) {
37
+ return true;
38
+ }
39
+ return false;
40
+ }
41
+
42
+ export function isSetComputeUnitPriceIx(ix: TransactionInstruction): boolean {
43
+ // Compute budget program discriminator is first byte
44
+ // 2: set compute unit limit
45
+ // 3: set compute unit price
46
+ if (
47
+ ix.programId.equals(ComputeBudgetProgram.programId) &&
48
+ ix.data.at(0) === 3
49
+ ) {
50
+ return true;
51
+ }
52
+ return false;
53
+ }
54
+
55
+ export function containsComputeUnitIxs(ixs: TransactionInstruction[]): {
56
+ hasSetComputeUnitLimitIx: boolean;
57
+ hasSetComputeUnitPriceIx: boolean;
58
+ } {
59
+ return {
60
+ hasSetComputeUnitLimitIx: ixs.some(isSetComputeUnitsIx),
61
+ hasSetComputeUnitPriceIx: ixs.some(isSetComputeUnitPriceIx),
62
+ };
63
+ }
@@ -0,0 +1,71 @@
1
+ import { MarketData, deserializeMarketData } from '@ellipsis-labs/phoenix-sdk';
2
+ import { fastDecode } from '../../src/decode/phoenix';
3
+ import { Connection, PublicKey } from '@solana/web3.js';
4
+ import assert from 'assert';
5
+
6
+ describe('custom phoenix decode', function () {
7
+ this.timeout(100_000);
8
+
9
+ it('decodes quickly', async function () {
10
+ const connection = new Connection('https://api.mainnet-beta.solana.com');
11
+
12
+ const val = await connection.getAccountInfo(
13
+ new PublicKey('4DoNfFBfF7UokCC2FQzriy7yHK6DY6NVdYpuekQ5pRgg')
14
+ );
15
+
16
+ const numIterations = 100;
17
+
18
+ let regularDecoded: MarketData;
19
+ const regularStart = performance.now();
20
+ for (let i = 0; i < numIterations; i++) {
21
+ regularDecoded = deserializeMarketData(val!.data);
22
+ }
23
+ const regularEnd = performance.now();
24
+
25
+ let fastDecoded: MarketData;
26
+ const fastStart = performance.now();
27
+ for (let i = 0; i < numIterations; i++) {
28
+ fastDecoded = fastDecode(val!.data);
29
+ }
30
+ const fastEnd = performance.now();
31
+
32
+ console.log(`Regular: ${regularEnd - regularStart} ms`);
33
+ console.log(
34
+ `Regular avg: ${(regularEnd - regularStart) / numIterations} ms`
35
+ );
36
+
37
+ console.log(`Fast: ${fastEnd - fastStart} ms`);
38
+ console.log(`Fast avg: ${(fastEnd - fastStart) / numIterations} ms`);
39
+
40
+ // @ts-ignore
41
+ assert(deepEqual(fastDecoded.bids, regularDecoded.bids));
42
+ // @ts-ignore
43
+ assert(deepEqual(regularDecoded.asks, fastDecoded.asks));
44
+ });
45
+ });
46
+
47
+ function deepEqual(obj1: any, obj2: any) {
48
+ if (obj1 === obj2) return true;
49
+
50
+ if (
51
+ obj1 == null ||
52
+ obj2 == null ||
53
+ typeof obj1 !== 'object' ||
54
+ typeof obj2 !== 'object'
55
+ ) {
56
+ return false;
57
+ }
58
+
59
+ const keys1 = Object.keys(obj1);
60
+ const keys2 = Object.keys(obj2);
61
+
62
+ if (keys1.length !== keys2.length) return false;
63
+
64
+ for (const key of keys1) {
65
+ if (!keys2.includes(key) || !deepEqual(obj1[key], obj2[key])) {
66
+ return false;
67
+ }
68
+ }
69
+
70
+ return true;
71
+ }