@tonappchain/sdk 0.7.2-alpha-11 → 0.7.2-alpha-14

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 (41) hide show
  1. package/dist/artifacts/dev/ton/internal/build/CrossChainLayer.compiled.json +1 -1
  2. package/dist/src/adapters/BaseContractOpener.d.ts +76 -0
  3. package/dist/src/adapters/BaseContractOpener.js +445 -0
  4. package/dist/src/adapters/LiteClientOpener.d.ts +38 -0
  5. package/dist/src/adapters/LiteClientOpener.js +141 -0
  6. package/dist/src/adapters/OpenerUtils.d.ts +3 -0
  7. package/dist/src/adapters/OpenerUtils.js +39 -0
  8. package/dist/src/adapters/RetryableContractOpener.d.ts +40 -0
  9. package/dist/src/adapters/RetryableContractOpener.js +287 -0
  10. package/dist/src/adapters/SandboxOpener.d.ts +15 -0
  11. package/dist/src/adapters/SandboxOpener.js +35 -0
  12. package/dist/src/adapters/TonClient4Opener.d.ts +23 -0
  13. package/dist/src/adapters/TonClient4Opener.js +87 -0
  14. package/dist/src/adapters/TonClientOpener.d.ts +17 -0
  15. package/dist/src/adapters/TonClientOpener.js +72 -0
  16. package/dist/src/adapters/index.d.ts +7 -2
  17. package/dist/src/adapters/index.js +7 -2
  18. package/dist/src/index.d.ts +1 -1
  19. package/dist/src/index.js +3 -2
  20. package/dist/src/interfaces/ContractOpener.d.ts +71 -2
  21. package/dist/src/interfaces/ITacSDK.d.ts +11 -1
  22. package/dist/src/sdk/Configuration.js +2 -1
  23. package/dist/src/sdk/Consts.d.ts +10 -3
  24. package/dist/src/sdk/Consts.js +13 -5
  25. package/dist/src/sdk/StartTracking.d.ts +4 -1
  26. package/dist/src/sdk/StartTracking.js +11 -6
  27. package/dist/src/sdk/TONTransactionManager.d.ts +1 -3
  28. package/dist/src/sdk/TONTransactionManager.js +3 -4
  29. package/dist/src/sdk/TacSdk.d.ts +3 -1
  30. package/dist/src/sdk/TacSdk.js +5 -3
  31. package/dist/src/sdk/TxFinalizer.d.ts +1 -8
  32. package/dist/src/sdk/TxFinalizer.js +14 -125
  33. package/dist/src/sdk/Utils.d.ts +10 -0
  34. package/dist/src/sdk/Utils.js +53 -0
  35. package/dist/src/structs/InternalStruct.d.ts +2 -17
  36. package/dist/src/structs/Struct.d.ts +117 -5
  37. package/package.json +1 -1
  38. package/dist/src/adapters/contractOpener.d.ts +0 -24
  39. package/dist/src/adapters/contractOpener.js +0 -310
  40. package/dist/src/adapters/retryableContractOpener.d.ts +0 -29
  41. package/dist/src/adapters/retryableContractOpener.js +0 -138
@@ -1,140 +1,27 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.TonIndexerTxFinalizer = exports.TonTxFinalizer = void 0;
4
- const ton_1 = require("@ton/ton");
3
+ exports.TonTxFinalizer = void 0;
5
4
  const errors_1 = require("../errors");
6
5
  const AxiosHttpClient_1 = require("./AxiosHttpClient");
7
6
  const Consts_1 = require("./Consts");
8
7
  const Logger_1 = require("./Logger");
9
8
  const Utils_1 = require("./Utils");
10
- const IGNORE_OPCODE = [
11
- 0xd53276db, // Excess
12
- ];
13
9
  class TonTxFinalizer {
14
- constructor(contractOpener, logger = new Logger_1.NoopLogger()) {
15
- this.contractOpener = contractOpener;
16
- this.logger = logger;
17
- }
18
- // Fetches adjacent transactions from toncenter
19
- async fetchAdjacentTransactions(address, hash, retries = 5, delay = 1000, opts) {
20
- for (let i = retries; i >= 0; i--) {
21
- try {
22
- const txs = await this.contractOpener.getAdjacentTransactions(address, hash, opts);
23
- return txs;
24
- }
25
- catch (error) {
26
- const errorMessage = error.message;
27
- // Rate limit error (429) - retry
28
- if (errorMessage.includes('429')) {
29
- if (i > 0) {
30
- await (0, Utils_1.sleep)(delay);
31
- }
32
- continue;
33
- }
34
- // Log all errors except 404 Not Found
35
- if (!errorMessage.includes('404')) {
36
- const logMessage = error instanceof Error ? error.message : error;
37
- this.logger.warn(`Failed to fetch adjacent transactions for ${hash}:`, logMessage);
38
- }
39
- if (i > 0) {
40
- await (0, Utils_1.sleep)(delay);
41
- }
42
- }
43
- }
44
- return [];
45
- }
46
- // Checks if all transactions in the tree are successful
47
- async trackTransactionTree(address, hash, params = { maxDepth: Consts_1.DEFAULT_FIND_TX_MAX_DEPTH, ignoreOpcodeList: IGNORE_OPCODE }) {
48
- const { maxDepth = Consts_1.DEFAULT_FIND_TX_MAX_DEPTH, ignoreOpcodeList = IGNORE_OPCODE } = params;
49
- const parsedAddress = ton_1.Address.parse(address);
50
- const visitedHashes = new Set();
51
- const queue = [{ address: parsedAddress, hash, depth: 0 }];
52
- while (queue.length > 0) {
53
- const { hash: currentHash, depth: currentDepth, address: currentAddress } = queue.shift();
54
- if (visitedHashes.has(currentHash)) {
55
- continue;
56
- }
57
- visitedHashes.add(currentHash);
58
- this.logger.debug(`Checking hash (depth ${currentDepth}): ${currentHash}`);
59
- const transactions = await this.fetchAdjacentTransactions(currentAddress, currentHash, 5, 1000, {
60
- limit: 10,
61
- archival: true,
62
- });
63
- this.logger.debug(`Found ${transactions.length} adjacent transactions for ${currentHash}`);
64
- if (transactions.length === 0)
65
- continue;
66
- for (const tx of transactions) {
67
- if (tx.description.type !== 'generic' || !tx.inMessage)
68
- continue;
69
- if (tx.inMessage.info.type === 'internal' && tx.inMessage.info.value.coins === Consts_1.IGNORE_MSG_VALUE_1_NANO)
70
- continue; // we ignore messages with 1 nanoton value as they are for notification purpose only
71
- const bodySlice = tx.inMessage.body.beginParse();
72
- if (bodySlice.remainingBits < 32)
73
- continue;
74
- const opcode = bodySlice.loadUint(32);
75
- if (!ignoreOpcodeList.includes(opcode)) {
76
- const { aborted, computePhase, actionPhase } = tx.description;
77
- const failureCase = (() => {
78
- if (aborted) {
79
- return 'Transaction was aborted';
80
- }
81
- if (!computePhase) {
82
- return 'computePhase not present';
83
- }
84
- if (computePhase.type === 'skipped') {
85
- return 'computePhase was skipped';
86
- }
87
- if (!computePhase.success) {
88
- return 'computePhase not successful';
89
- }
90
- if (computePhase.exitCode !== 0) {
91
- return `computePhase.exitCode was not zero (exitCode=${computePhase.exitCode})`;
92
- }
93
- if (actionPhase && !actionPhase.success) {
94
- return 'actionPhase not successful';
95
- }
96
- if (actionPhase && actionPhase.resultCode !== 0) {
97
- return `actionPhase.resultCode was not zero (resultCode=${actionPhase.resultCode})`;
98
- }
99
- return null;
100
- })();
101
- if (failureCase) {
102
- throw (0, errors_1.txFinalizationError)(`${tx.hash().toString('base64')}: ${failureCase}`);
103
- }
104
- if (currentDepth + 1 < maxDepth) {
105
- if (tx.outMessages.size > 0) {
106
- queue.push({
107
- hash: tx.hash().toString('base64'),
108
- address: tx.inMessage.info.dest,
109
- depth: currentDepth + 1,
110
- });
111
- }
112
- }
113
- }
114
- else {
115
- this.logger.debug(`Skipping hash (depth ${currentDepth}): ${tx.hash().toString('base64')}`);
116
- }
117
- }
118
- this.logger.debug(`Finished checking hash (depth ${currentDepth}): ${currentHash}`);
119
- }
120
- }
121
- }
122
- exports.TonTxFinalizer = TonTxFinalizer;
123
- class TonIndexerTxFinalizer {
124
10
  constructor(apiConfig, logger = new Logger_1.NoopLogger(), httpClient = new AxiosHttpClient_1.AxiosHttpClient()) {
125
11
  this.apiConfig = apiConfig;
126
12
  this.logger = logger;
127
13
  this.httpClient = httpClient;
128
14
  }
129
15
  // Fetches adjacent transactions from toncenter
130
- async fetchAdjacentTransactions(hash, retries = 5, delay = 1000) {
16
+ async fetchAdjacentTransactions(hash, retries = Consts_1.DEFAULT_RETRY_MAX_COUNT, delay = Consts_1.DEFAULT_RETRY_DELAY_MS) {
131
17
  for (let i = retries; i >= 0; i--) {
132
18
  try {
133
19
  const url = this.apiConfig.urlBuilder(hash);
20
+ const authHeaders = this.apiConfig.authorization
21
+ ? { [this.apiConfig.authorization.header]: this.apiConfig.authorization.value }
22
+ : undefined;
134
23
  const response = await this.httpClient.get(url, {
135
- headers: {
136
- [this.apiConfig.authorization.header]: this.apiConfig.authorization.value,
137
- },
24
+ ...(authHeaders ? { headers: authHeaders } : {}),
138
25
  transformResponse: [Utils_1.toCamelCaseTransformer],
139
26
  });
140
27
  return response.data.transactions || [];
@@ -161,8 +48,8 @@ class TonIndexerTxFinalizer {
161
48
  return [];
162
49
  }
163
50
  // Checks if all transactions in the tree are successful
164
- async trackTransactionTree(_, hash, params = { maxDepth: Consts_1.DEFAULT_FIND_TX_MAX_DEPTH, ignoreOpcodeList: IGNORE_OPCODE }) {
165
- const { maxDepth = Consts_1.DEFAULT_FIND_TX_MAX_DEPTH, ignoreOpcodeList = IGNORE_OPCODE } = params;
51
+ async trackTransactionTree(_, hash, params = { maxDepth: Consts_1.DEFAULT_FIND_TX_MAX_DEPTH, ignoreOpcodeList: Consts_1.IGNORE_OPCODE }) {
52
+ const { maxDepth = Consts_1.DEFAULT_FIND_TX_MAX_DEPTH, ignoreOpcodeList = Consts_1.IGNORE_OPCODE } = params;
166
53
  const visitedHashes = new Set();
167
54
  const queue = [{ hash, depth: 0 }];
168
55
  while (queue.length > 0) {
@@ -191,18 +78,20 @@ class TonIndexerTxFinalizer {
191
78
  return 'computePh not successful';
192
79
  }
193
80
  if (computePh.exitCode !== 0) {
194
- return `computePh.exitCode was not zero (exitCode=${computePh.exitCode})`;
81
+ return `computePh.exitCode was not zero`;
195
82
  }
196
83
  if (action && !action.success) {
197
84
  return 'action not successful';
198
85
  }
199
86
  if (action && action.resultCode !== 0) {
200
- return `action.resultCode was not zero (resultCode=${action.resultCode})`;
87
+ return `action.resultCode was not zero`;
201
88
  }
202
89
  return null;
203
90
  })();
204
91
  if (failureCase) {
205
- throw (0, errors_1.txFinalizationError)(`${tx.hash}: ${failureCase}`);
92
+ const exitCode = computePh ? computePh.exitCode : 'N/A';
93
+ const resultCode = action ? action.resultCode : 'N/A';
94
+ throw (0, errors_1.txFinalizationError)(`${tx.hash}: ${failureCase} (exitCode=${exitCode}, resultCode=${resultCode})`);
206
95
  }
207
96
  if (currentDepth + 1 < maxDepth) {
208
97
  if (tx.outMsgs.length > 0) {
@@ -217,4 +106,4 @@ class TonIndexerTxFinalizer {
217
106
  }
218
107
  }
219
108
  }
220
- exports.TonIndexerTxFinalizer = TonIndexerTxFinalizer;
109
+ exports.TonTxFinalizer = TonTxFinalizer;
@@ -37,6 +37,16 @@ export declare function getString(cell?: Cell): string;
37
37
  * Calculates (a * b + c / 2) / c with proper rounding
38
38
  */
39
39
  export declare function muldivr(a: bigint, b: bigint, c: bigint): bigint;
40
+ /**
41
+ * Normalize hash string to base64 format
42
+ * Accepts: base64, hex, or raw string
43
+ */
44
+ export declare function normalizeHashToBase64(hash: string): string;
45
+ /**
46
+ * Normalize hash string to hex format
47
+ * Accepts: base64, hex
48
+ */
49
+ export declare function normalizeHashToHex(hash: string): string;
40
50
  export declare function getNormalizedExtMessageHash(message: Message): string;
41
51
  export declare function retry<T>(fn: () => Promise<T>, options: {
42
52
  retries: number;
@@ -20,6 +20,8 @@ exports.getAddressString = getAddressString;
20
20
  exports.getNumber = getNumber;
21
21
  exports.getString = getString;
22
22
  exports.muldivr = muldivr;
23
+ exports.normalizeHashToBase64 = normalizeHashToBase64;
24
+ exports.normalizeHashToHex = normalizeHashToHex;
23
25
  exports.getNormalizedExtMessageHash = getNormalizedExtMessageHash;
24
26
  exports.retry = retry;
25
27
  exports.recurisivelyCollectCellStats = recurisivelyCollectCellStats;
@@ -316,6 +318,57 @@ function muldivr(a, b, c) {
316
318
  }
317
319
  return (a * b + c / 2n) / c;
318
320
  }
321
+ /**
322
+ * Normalize hash string to base64 format
323
+ * Accepts: base64, hex, or raw string
324
+ */
325
+ function normalizeHashToBase64(hash) {
326
+ const input = hash.trim();
327
+ if (!input)
328
+ return input;
329
+ const hex = input.startsWith('0x') ? input.slice(2) : input;
330
+ if (/^[0-9a-fA-F]{64}$/.test(hex)) {
331
+ return Buffer.from(hex, 'hex').toString('base64');
332
+ }
333
+ const decoded = decodeBase64Like(input);
334
+ if (decoded) {
335
+ return decoded.toString('base64');
336
+ }
337
+ return input;
338
+ }
339
+ /**
340
+ * Normalize hash string to hex format
341
+ * Accepts: base64, hex
342
+ */
343
+ function normalizeHashToHex(hash) {
344
+ const input = hash.trim();
345
+ if (!input)
346
+ return input;
347
+ const maybeHex = input.startsWith('0x') ? input.slice(2) : input;
348
+ if (/^[0-9a-fA-F]+$/.test(maybeHex) && maybeHex.length % 2 === 0) {
349
+ return maybeHex.toLowerCase();
350
+ }
351
+ const decoded = decodeBase64Like(input);
352
+ if (decoded) {
353
+ return decoded.toString('hex');
354
+ }
355
+ return input;
356
+ }
357
+ function decodeBase64Like(input) {
358
+ const normalized = input.replace(/-/g, '+').replace(/_/g, '/');
359
+ const padLength = normalized.length % 4;
360
+ const padded = padLength === 0 ? normalized : normalized + '='.repeat(4 - padLength);
361
+ if (!/^[A-Za-z0-9+/]+={0,2}$/.test(padded)) {
362
+ return null;
363
+ }
364
+ try {
365
+ const buf = Buffer.from(padded, 'base64');
366
+ return buf.length > 0 ? buf : null;
367
+ }
368
+ catch {
369
+ return null;
370
+ }
371
+ }
319
372
  function getNormalizedExtMessageHash(message) {
320
373
  if (message.info.type !== 'external-in') {
321
374
  throw new Error(`Message must be "external-in", got ${message.info.type}`);
@@ -99,13 +99,14 @@ export type TransactionDepth = {
99
99
  address?: Address;
100
100
  hash: string;
101
101
  depth: number;
102
+ hashType?: 'unknown' | 'in' | 'out';
102
103
  };
103
104
  export type AdjacentTransactionsResponse = {
104
105
  transactions: ToncenterTransaction[];
105
106
  };
106
107
  export type TxFinalizerConfig = {
107
108
  urlBuilder: (hash: string) => string;
108
- authorization: {
109
+ authorization?: {
109
110
  header: string;
110
111
  value: string;
111
112
  };
@@ -123,22 +124,6 @@ export type ConvertedCurrencyRawResult = {
123
124
  tacPrice: USDPriceInfoRaw;
124
125
  tonPrice: USDPriceInfoRaw;
125
126
  };
126
- export type GetTransactionsOptions = {
127
- limit: number;
128
- lt?: string;
129
- hash?: string;
130
- to_lt?: string;
131
- inclusive?: boolean;
132
- archival?: boolean;
133
- timeoutMs?: number;
134
- retryDelayMs?: number;
135
- };
136
- export type AddressInformation = {
137
- lastTransaction: {
138
- lt: string;
139
- hash: string;
140
- };
141
- };
142
127
  export type TONFeesParams = {
143
128
  accountBitPrice: number;
144
129
  accountCellPrice: number;
@@ -4,7 +4,6 @@ import { AbstractProvider } from 'ethers';
4
4
  import { JettonMinter, JettonMinterData } from '../../artifacts/tonTypes';
5
5
  import type { FT, NFT } from '../assets';
6
6
  import type { Asset, ContractOpener, ILogger } from '../interfaces';
7
- import { ITxFinalizer } from '../interfaces/ITxFinalizer';
8
7
  import { SendResult } from './InternalStruct';
9
8
  export type ContractState = {
10
9
  balance: bigint;
@@ -61,10 +60,6 @@ export type TONParams = {
61
60
  * Address of TON settings contract. Use only for tests.
62
61
  */
63
62
  settingsAddress?: string;
64
- /**
65
- * TxFinalizer for tracking transaction tree
66
- */
67
- txFinalizer?: ITxFinalizer;
68
63
  };
69
64
  export type SDKParams = {
70
65
  /**
@@ -451,7 +446,124 @@ export type TacGasPrice = {
451
446
  fast: number;
452
447
  slow: number;
453
448
  };
449
+ /**
450
+ * Parameters for tracking and validating transaction trees
451
+ */
454
452
  export type TrackTransactionTreeParams = {
453
+ /**
454
+ * Maximum number of transactions to fetch per pagination request
455
+ * @default 100
456
+ */
457
+ limit?: number;
458
+ /**
459
+ * Maximum depth to traverse in the transaction tree, inclusive (prevents infinite loops)
460
+ * @default 10
461
+ */
455
462
  maxDepth?: number;
463
+ /**
464
+ * Maximum number of transactions to scan while searching by hash in account history.
465
+ * Prevents excessive full-history scans on high-activity accounts.
466
+ * @default 100
467
+ */
468
+ maxScannedTransactions?: number;
469
+ /**
470
+ * List of operation codes (opcodes) to skip for extra checks.
471
+ * Core phase validation is still applied.
472
+ * @default [ 0xd53276db ] // Excess
473
+ */
456
474
  ignoreOpcodeList?: number[];
475
+ /**
476
+ * Direction to search the transaction tree:
477
+ * - 'forward': only search children (outgoing messages)
478
+ * - 'backward': only search parents (incoming messages)
479
+ * - 'both': search in both directions (default)
480
+ * @default 'both'
481
+ */
482
+ direction?: 'forward' | 'backward' | 'both';
483
+ /**
484
+ * Internal option: wait for the root transaction to appear before failing with `not_found`.
485
+ * Useful right after sending a message when transaction indexing is eventually consistent.
486
+ * @default true
487
+ */
488
+ waitForRootTransaction?: boolean;
489
+ };
490
+ /**
491
+ * Details about a transaction validation error
492
+ */
493
+ export type TransactionValidationError = {
494
+ /**
495
+ * Base64-encoded hash of the failed transaction (or the searched hash if not found)
496
+ */
497
+ txHash: string;
498
+ /**
499
+ * Exit code from the compute phase, or 'N/A' if compute phase is missing
500
+ */
501
+ exitCode: number | 'N/A';
502
+ /**
503
+ * Result code from the action phase, or 'N/A' if action phase is missing
504
+ */
505
+ resultCode: number | 'N/A';
506
+ /**
507
+ * Reason for validation failure:
508
+ * - 'aborted': default: transaction was aborted
509
+ * - 'compute_phase_missing': compute phase is missing
510
+ * - 'compute_phase_failed': compute phase failed (exitCode !== 0)
511
+ * - 'action_phase_failed': action phase failed (resultCode !== 0)
512
+ * - 'not_found': transaction or message hash not found during traversal
513
+ */
514
+ reason: 'aborted' | 'compute_phase_missing' | 'compute_phase_failed' | 'action_phase_failed' | 'not_found';
515
+ /**
516
+ * Address where the lookup was performed (for reason: 'not_found')
517
+ */
518
+ address?: string;
519
+ /**
520
+ * Hash type used in lookup (for reason: 'not_found')
521
+ */
522
+ hashType?: 'unknown' | 'in' | 'out';
523
+ };
524
+ /**
525
+ * Result of transaction tree tracking and validation
526
+ */
527
+ export type TrackTransactionTreeResult = {
528
+ /**
529
+ * Whether all transactions in the tree passed validation
530
+ */
531
+ success: boolean;
532
+ /**
533
+ * Count of unique transactions checked during traversal
534
+ */
535
+ checkedCount?: number;
536
+ /**
537
+ * Details about the first validation error encountered (if any)
538
+ */
539
+ error?: TransactionValidationError;
540
+ };
541
+ export type GetTransactionsOptions = {
542
+ /** Maximum number of transactions to retrieve */
543
+ limit?: number;
544
+ /** Logical time of the transaction to start from */
545
+ lt?: string;
546
+ /** Hash of the transaction to start from */
547
+ hash?: string;
548
+ /** Logical time of the transaction to end at */
549
+ to_lt?: string;
550
+ /** Whether to include the starting transaction in the results */
551
+ inclusive?: boolean;
552
+ /** Whether to search in archival nodes for historical data */
553
+ archival?: boolean;
554
+ /** Request timeout in milliseconds */
555
+ timeoutMs?: number;
556
+ /** Delay between retry attempts in milliseconds */
557
+ retryDelayMs?: number;
558
+ /** Internal scan guard: maximum transactions to inspect while traversing history */
559
+ maxScannedTransactions?: number;
560
+ };
561
+ export type AddressInformation = {
562
+ /** Information about the last transaction of the address */
563
+ lastTransaction: {
564
+ /** Logical time of the last transaction */
565
+ lt: string;
566
+ /** Hash of the last transaction */
567
+ hash: string;
568
+ };
457
569
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tonappchain/sdk",
3
- "version": "0.7.2-alpha-11",
3
+ "version": "0.7.2-alpha-14",
4
4
  "repository": "https://github.com/TacBuild/tac-sdk.git",
5
5
  "author": "TAC. <developers@tac>",
6
6
  "license": "MIT",
@@ -1,24 +0,0 @@
1
- import { Blockchain } from '@ton/sandbox';
2
- import { Address, TonClient, Transaction } from '@ton/ton';
3
- import { ContractOpener } from '../interfaces';
4
- import { GetTransactionsOptions } from '../structs/InternalStruct';
5
- import { Network } from '../structs/Struct';
6
- export declare function getAdjacentTransactionsHelper(addr: Address, hashB64: string, getTransactions: (addr: Address, opts: GetTransactionsOptions) => Promise<Transaction[]>, opts?: GetTransactionsOptions): Promise<Transaction[]>;
7
- type LiteServer = {
8
- ip: number;
9
- port: number;
10
- id: {
11
- '@type': string;
12
- key: string;
13
- };
14
- };
15
- export declare function liteClientOpener(options: {
16
- liteservers: LiteServer[];
17
- } | {
18
- network: Network;
19
- }): Promise<ContractOpener>;
20
- export declare function sandboxOpener(blockchain: Blockchain): ContractOpener;
21
- export declare function orbsOpener(network: Network): Promise<ContractOpener>;
22
- export declare function orbsOpener4(network: Network, timeout?: number): Promise<ContractOpener>;
23
- export declare function tonClientOpener(client: TonClient): ContractOpener;
24
- export {};