@rosen-bridge/abstract-extractor 0.3.0 → 1.0.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.
Files changed (49) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/ergo/AbstractErgoExtractor.d.ts +31 -8
  3. package/dist/ergo/AbstractErgoExtractor.d.ts.map +1 -1
  4. package/dist/ergo/AbstractErgoExtractor.js +73 -8
  5. package/dist/ergo/AbstractErgoExtractorAction.d.ts +38 -10
  6. package/dist/ergo/AbstractErgoExtractorAction.d.ts.map +1 -1
  7. package/dist/ergo/AbstractErgoExtractorAction.js +134 -1
  8. package/dist/ergo/AbstractErgoExtractorEntity.d.ts +11 -0
  9. package/dist/ergo/AbstractErgoExtractorEntity.d.ts.map +1 -0
  10. package/dist/ergo/AbstractErgoExtractorEntity.js +57 -0
  11. package/dist/ergo/index.d.ts +1 -0
  12. package/dist/ergo/index.d.ts.map +1 -1
  13. package/dist/ergo/index.js +2 -1
  14. package/dist/ergo/initializable/AbstractInitializable.d.ts +4 -3
  15. package/dist/ergo/initializable/AbstractInitializable.d.ts.map +1 -1
  16. package/dist/ergo/initializable/AbstractInitializable.js +9 -5
  17. package/dist/ergo/initializable/AbstractInitializableAction.d.ts +7 -2
  18. package/dist/ergo/initializable/AbstractInitializableAction.d.ts.map +1 -1
  19. package/dist/ergo/initializable/AbstractInitializableAction.js +11 -1
  20. package/dist/ergo/interfaces.d.ts +21 -7
  21. package/dist/ergo/interfaces.d.ts.map +1 -1
  22. package/dist/ergo/interfaces.js +8 -1
  23. package/lib/ergo/AbstractErgoExtractor.ts +107 -23
  24. package/lib/ergo/AbstractErgoExtractorAction.ts +187 -18
  25. package/lib/ergo/AbstractErgoExtractorEntity.ts +28 -0
  26. package/lib/ergo/index.ts +1 -0
  27. package/lib/ergo/initializable/AbstractInitializable.ts +22 -9
  28. package/lib/ergo/initializable/AbstractInitializableAction.ts +19 -3
  29. package/lib/ergo/interfaces.ts +25 -7
  30. package/package.json +6 -2
  31. package/tests/{AbstractExtractor.mock.ts → AbstractErgoExtractor.mock.ts} +12 -7
  32. package/tests/AbstractErgoExtractor.spec.ts +281 -0
  33. package/tests/AbstractErgoExtractorAction.mock.ts +45 -0
  34. package/tests/AbstractErgoExtractorAction.spec.ts +269 -0
  35. package/tests/initializable/AbstractInitializable.mock.ts +15 -8
  36. package/tests/initializable/AbstractInitializable.spec.ts +37 -5
  37. package/tests/initializable/AbstractInitializableAction.mock.ts +45 -0
  38. package/tests/initializable/AbstractInitializableAction.spec.ts +65 -0
  39. package/tests/testData.ts +38 -2
  40. package/tests/testUtils.ts +22 -0
  41. package/tsconfig.build.tsbuildinfo +1 -1
  42. package/dist/ergo/initializable/InitializableByAddress.d.ts +0 -19
  43. package/dist/ergo/initializable/InitializableByAddress.d.ts.map +0 -1
  44. package/dist/ergo/initializable/InitializableByAddress.js +0 -30
  45. package/dist/ergo/initializable/InitializableByToken.d.ts +0 -19
  46. package/dist/ergo/initializable/InitializableByToken.d.ts.map +0 -1
  47. package/dist/ergo/initializable/InitializableByToken.js +0 -30
  48. package/dist/tsconfig.tsbuildinfo +0 -1
  49. package/tests/AbstractExtractor.spec.ts +0 -106
@@ -1,39 +1,208 @@
1
+ import {
2
+ DataSource,
3
+ In,
4
+ Repository,
5
+ Not,
6
+ EntityTarget,
7
+ FindOptionsWhere,
8
+ } from 'typeorm';
9
+ import { chunk, difference, pick } from 'lodash-es';
10
+ import { AbstractLogger, DummyLogger } from '@rosen-bridge/abstract-logger';
11
+ import JsonBigInt from '@rosen-bridge/json-bigint';
12
+
1
13
  import { BlockInfo } from '../interfaces';
2
- import { SpendInfo } from './interfaces';
14
+ import { AbstractBoxData, BoxInfo, SpendInfo } from './interfaces';
15
+ import { DB_CHUNK_SIZE } from '../constants';
16
+ import { AbstractErgoExtractorEntity } from './AbstractErgoExtractorEntity';
17
+
18
+ export abstract class AbstractErgoExtractorAction<
19
+ ExtractedData extends AbstractBoxData,
20
+ ExtractorEntity extends AbstractErgoExtractorEntity
21
+ > {
22
+ private readonly datasource: DataSource;
23
+ readonly logger: AbstractLogger;
24
+ protected readonly repository: Repository<ExtractorEntity>;
25
+ private repo: EntityTarget<ExtractorEntity>;
26
+
27
+ constructor(
28
+ dataSource: DataSource,
29
+ repo: EntityTarget<ExtractorEntity>,
30
+ logger?: AbstractLogger
31
+ ) {
32
+ this.datasource = dataSource;
33
+ this.logger = logger ? logger : new DummyLogger();
34
+ this.repository = this.datasource.getRepository(repo);
35
+ this.repo = repo;
36
+ }
3
37
 
4
- export abstract class AbstractErgoExtractorAction<ExtractedData> {
5
38
  /**
6
- * insert all extracted box data in an atomic transaction
7
- * @param data
8
- * @param extractorId
9
- * @return process success
39
+ * create the database entity from extracted data and block information
10
40
  */
11
- abstract insertBoxes: (
41
+ protected abstract createEntity: (
12
42
  data: ExtractedData[],
13
- extractorId: string
14
- ) => Promise<boolean>;
43
+ block: BlockInfo,
44
+ extractor: string
45
+ ) => Array<Omit<ExtractorEntity, 'id'>>;
46
+
47
+ /**
48
+ * convert the database entity back to raw data
49
+ */
50
+ protected abstract convertEntityToData: (
51
+ entities: ExtractorEntity[]
52
+ ) => ExtractedData[];
53
+
54
+ /**
55
+ * insert all extracted box data in an atomic transaction
56
+ * update the data if a box with the same id is already stored in db
57
+ * @param boxes
58
+ * @param block
59
+ * @param extractor
60
+ * @return inserted items and updated box ids
61
+ * returns undefined in case of any problem
62
+ */
63
+ storeBoxes = async (
64
+ boxes: Array<ExtractedData>,
65
+ block: BlockInfo,
66
+ extractor: string
67
+ ): Promise<boolean> => {
68
+ let success = true;
69
+ let boxesToInsert: ExtractedData[] = [],
70
+ boxesToUpdate: ExtractedData[] = [];
71
+ const queryRunner = this.datasource.createQueryRunner();
72
+ await queryRunner.connect();
73
+ await queryRunner.startTransaction();
74
+ try {
75
+ const repository = queryRunner.manager.getRepository(this.repo);
76
+ const dbBoxIds = (
77
+ await repository.findBy({
78
+ boxId: In(boxes.map((item) => item.boxId)),
79
+ extractor: extractor,
80
+ } as FindOptionsWhere<ExtractorEntity>)
81
+ ).map((box) => box.boxId);
82
+ if (dbBoxIds.length > 0)
83
+ this.logger.debug(`Found stored boxes with same boxId`, dbBoxIds);
84
+
85
+ boxesToUpdate = boxes.filter((box) => dbBoxIds.includes(box.boxId));
86
+ boxesToInsert = difference(boxes, boxesToUpdate);
87
+
88
+ if (boxesToInsert.length > 0) {
89
+ this.logger.debug(`Inserting boxes`);
90
+ await repository.insert(
91
+ this.createEntity(boxesToInsert, block, extractor) as any
92
+ );
93
+ }
94
+ if (boxesToUpdate.length > 0)
95
+ this.logger.info(
96
+ `Updating boxes with following Ids in the database: [${boxesToUpdate
97
+ .map((col) => col.boxId)
98
+ .join(', ')}]`
99
+ );
100
+ for (const box of this.createEntity(boxesToUpdate, block, extractor)) {
101
+ this.logger.debug(
102
+ `Updating boxes in database [${JsonBigInt.stringify(box)}]`
103
+ );
104
+ await repository.update(
105
+ {
106
+ boxId: box.boxId,
107
+ extractor: extractor,
108
+ } as FindOptionsWhere<ExtractorEntity>,
109
+ box as any
110
+ );
111
+ }
112
+ await queryRunner.commitTransaction();
113
+ } catch (e) {
114
+ this.logger.error(`An error occurred during store boxes action: ${e}`);
115
+ await queryRunner.rollbackTransaction();
116
+ success = false;
117
+ } finally {
118
+ await queryRunner.release();
119
+ }
120
+ return success;
121
+ };
15
122
 
16
123
  /**
17
124
  * update spending information of stored boxes
125
+ * chunk spendInfos to prevent large database queries
126
+ * Note: It only updates the spendHeight and spendBlock fields. If updating
127
+ * anything else is required, override this implementation to include the
128
+ * additional fields.
18
129
  * @param spendInfos
19
130
  * @param block
20
- * @param extractorId
131
+ * @param extractor
132
+ * @returns spent box ids
21
133
  */
22
- abstract spendBoxes: (
23
- spendInfos: SpendInfo[],
134
+ spendBoxes = async (
135
+ spendInfos: Array<SpendInfo>,
24
136
  block: BlockInfo,
25
- extractorId: string
26
- ) => Promise<void>;
137
+ extractor: string
138
+ ): Promise<BoxInfo[]> => {
139
+ const spentData = [];
140
+ const spendInfoChunks = chunk(spendInfos, DB_CHUNK_SIZE);
141
+ for (const spendInfoChunk of spendInfoChunks) {
142
+ const boxIds = spendInfoChunk.map((info) => info.boxId);
143
+ const updateResult = await this.repository.update(
144
+ {
145
+ boxId: In(boxIds),
146
+ extractor: extractor,
147
+ } as FindOptionsWhere<ExtractorEntity>,
148
+ { spendBlock: block.hash, spendHeight: block.height } as any
149
+ );
150
+
151
+ if (updateResult.affected && updateResult.affected > 0) {
152
+ const spentRows = await this.repository.findBy({
153
+ boxId: In(boxIds),
154
+ spendBlock: block.hash,
155
+ } as FindOptionsWhere<ExtractorEntity>);
156
+ spentData.push(...spentRows);
157
+ for (const row of spentRows) {
158
+ this.logger.debug(
159
+ `Spent box with boxId [${row.boxId}] at height ${block.height}`
160
+ );
161
+ }
162
+ }
163
+ }
164
+ return spentData.map((data) => pick(data, 'boxId'));
165
+ };
27
166
 
28
167
  /**
29
168
  * delete extracted data from a specific block
30
169
  * if a box is spend in this block mark it as unspent
31
170
  * if a box is created in this block remove it from database
32
171
  * @param block
33
- * @param extractorId
172
+ * @param extractor
173
+ * @return deleted items and updated box ids
34
174
  */
35
- abstract deleteBlockBoxes: (
175
+ deleteBlockBoxes = async (
36
176
  block: string,
37
- extractorId: string
38
- ) => Promise<void>;
177
+ extractor: string
178
+ ): Promise<{ deletedData: ExtractedData[]; updatedData: BoxInfo[] }> => {
179
+ this.logger.info(
180
+ `Deleting boxes in block ${block} and extractor ${extractor}`
181
+ );
182
+ const deletedData = await this.repository.find({
183
+ where: { extractor: extractor, block: block } as any,
184
+ });
185
+ const updatedData = await this.repository.find({
186
+ where: {
187
+ extractor: extractor,
188
+ spendBlock: block,
189
+ block: Not(block),
190
+ } as any,
191
+ });
192
+ await this.repository.delete({
193
+ extractor: extractor,
194
+ block: block,
195
+ } as any);
196
+ await this.repository.update(
197
+ {
198
+ spendBlock: block,
199
+ extractor: extractor,
200
+ } as FindOptionsWhere<ExtractorEntity>,
201
+ { spendBlock: null, spendHeight: 0 } as any
202
+ );
203
+ return {
204
+ deletedData: this.convertEntityToData(deletedData),
205
+ updatedData: updatedData.map((data) => pick(data, 'boxId')),
206
+ };
207
+ };
39
208
  }
@@ -0,0 +1,28 @@
1
+ import { Column, PrimaryGeneratedColumn, Unique } from 'typeorm';
2
+
3
+ @Unique(['boxId', 'extractor'])
4
+ export abstract class AbstractErgoExtractorEntity {
5
+ @PrimaryGeneratedColumn()
6
+ id: number;
7
+
8
+ @Column({ type: 'varchar' })
9
+ boxId: string;
10
+
11
+ @Column({ type: 'varchar' })
12
+ block: string;
13
+
14
+ @Column({ type: 'int' })
15
+ height: number;
16
+
17
+ @Column({ nullable: true, type: 'varchar' })
18
+ spendBlock?: string | null;
19
+
20
+ @Column({ nullable: true, type: 'int' })
21
+ spendHeight?: number | null;
22
+
23
+ @Column({ type: 'varchar' })
24
+ extractor: string;
25
+
26
+ @Column({ type: 'varchar' })
27
+ serialized: string;
28
+ }
package/lib/ergo/index.ts CHANGED
@@ -6,3 +6,4 @@ export * from './network/NodeNetwork';
6
6
  export * from './network/AbstractNetwork';
7
7
  export * from './initializable';
8
8
  export * from './utils';
9
+ export * from './AbstractErgoExtractorEntity';
@@ -1,6 +1,8 @@
1
1
  import { AbstractLogger } from '@rosen-bridge/abstract-logger';
2
+ import { groupBy, sortBy } from 'lodash-es';
3
+
2
4
  import {
3
- ErgoExtractedData,
5
+ AbstractBoxData,
4
6
  ErgoNetworkType,
5
7
  ExtendedTransaction,
6
8
  } from '../interfaces';
@@ -10,14 +12,18 @@ import { AbstractInitializableErgoExtractorAction } from './AbstractInitializabl
10
12
  import { BlockInfo } from '../../interfaces';
11
13
  import { ExplorerNetwork } from '../network/ExplorerNetwork';
12
14
  import { NodeNetwork } from '../network/NodeNetwork';
13
- import { groupBy, sortBy } from 'lodash-es';
15
+ import { AbstractErgoExtractorEntity } from '../AbstractErgoExtractorEntity';
14
16
 
15
17
  export abstract class AbstractInitializableErgoExtractor<
16
- ExtractedData extends ErgoExtractedData
17
- > extends AbstractErgoExtractor<ExtractedData> {
18
+ ExtractedData extends AbstractBoxData,
19
+ ExtractorEntity extends AbstractErgoExtractorEntity
20
+ > extends AbstractErgoExtractor<ExtractedData, ExtractorEntity> {
18
21
  protected initialize: boolean;
19
22
  private address: string;
20
- protected abstract actions: AbstractInitializableErgoExtractorAction<ExtractedData>;
23
+ protected abstract actions: AbstractInitializableErgoExtractorAction<
24
+ ExtractedData,
25
+ ExtractorEntity
26
+ >;
21
27
 
22
28
  private network: ExplorerNetwork | NodeNetwork;
23
29
 
@@ -112,14 +118,15 @@ export abstract class AbstractInitializableErgoExtractor<
112
118
  */
113
119
  private initializeWithNode = async (initialBlock: BlockInfo) => {
114
120
  const txCountBeforeInit = await this.getTotalTxCount();
121
+ let offset = 0,
122
+ total = 1,
123
+ round = 1;
115
124
  await this.initWithRetrial(async () => {
116
125
  // Repeat the whole process twice to cover all spent boxes
117
126
  // After round 1 all boxes have been saved and processed once
118
127
  // After round 2 spending information of all stored boxes are updated successfully
119
- for (let round = 0; round <= 1; round++) {
128
+ while (round <= 2) {
120
129
  this.logger.debug(`Starting round ${round} of initialization`);
121
- let offset = 0,
122
- total = 1;
123
130
  while (offset < total) {
124
131
  const response = await (
125
132
  this.network as NodeNetwork
@@ -138,6 +145,8 @@ export abstract class AbstractInitializableErgoExtractor<
138
145
  if (txs.length > 0) await this.processTransactionBatch(txs);
139
146
  offset += API_LIMIT;
140
147
  }
148
+ round++;
149
+ offset = 0; // next round initial offset
141
150
  }
142
151
  });
143
152
  const txCountAfterInit = await this.getTotalTxCount();
@@ -167,7 +176,11 @@ export abstract class AbstractInitializableErgoExtractor<
167
176
  this.logger.debug(
168
177
  `Processing transactions at height ${blockTxs[0].inclusionHeight}`
169
178
  );
170
- await this.processTransactions(blockTxs, block);
179
+ const success = await this.processTransactions(blockTxs, block);
180
+ if (!success)
181
+ throw Error(
182
+ `Processing transactions failed at height ${blockTxs[0].inclusionHeight}`
183
+ );
171
184
  }
172
185
  };
173
186
 
@@ -1,11 +1,27 @@
1
+ import { DataSource, EntityTarget } from 'typeorm';
2
+ import { AbstractLogger } from '@rosen-bridge/abstract-logger';
3
+
1
4
  import { AbstractErgoExtractorAction } from '../AbstractErgoExtractorAction';
5
+ import { AbstractBoxData } from '../interfaces';
6
+ import { AbstractErgoExtractorEntity } from '../AbstractErgoExtractorEntity';
2
7
 
3
8
  export abstract class AbstractInitializableErgoExtractorAction<
4
- ExtractedData
5
- > extends AbstractErgoExtractorAction<ExtractedData> {
9
+ ExtractedData extends AbstractBoxData,
10
+ ExtractorEntity extends AbstractErgoExtractorEntity
11
+ > extends AbstractErgoExtractorAction<ExtractedData, ExtractorEntity> {
12
+ constructor(
13
+ dataSource: DataSource,
14
+ repo: EntityTarget<ExtractorEntity>,
15
+ logger?: AbstractLogger
16
+ ) {
17
+ super(dataSource, repo, logger);
18
+ }
19
+
6
20
  /**
7
21
  * remove all existing data for the extractor
8
22
  * @param extractorId
9
23
  */
10
- abstract removeAllData: (extractorId: string) => Promise<void>;
24
+ removeAllData = async (extractorId: string) => {
25
+ await this.repository.delete({ extractor: extractorId } as any);
26
+ };
11
27
  }
@@ -62,14 +62,32 @@ export interface SpendInfo {
62
62
  boxId: string;
63
63
  txId: string;
64
64
  index: number;
65
+ extras?: string[];
65
66
  }
66
67
 
67
- export interface ErgoExtractedData {
68
+ export interface AbstractBoxData {
69
+ boxId: string;
70
+ serialized: string;
71
+ }
72
+
73
+ export enum CallbackType {
74
+ Insert = 'insert',
75
+ Update = 'update',
76
+ Spend = 'spend',
77
+ Delete = 'delete',
78
+ }
79
+
80
+ export interface BoxInfo {
68
81
  boxId: string;
69
- height: number;
70
- blockId: string;
71
- spendBlock?: string;
72
- spendHeight?: number;
73
- spendTxId?: string;
74
- spendIndex?: number;
75
82
  }
83
+
84
+ export type CallbackDataMap<ExtractedData extends AbstractBoxData> = {
85
+ [CallbackType.Update]: BoxInfo[];
86
+ [CallbackType.Insert]: ExtractedData[];
87
+ [CallbackType.Delete]: ExtractedData[];
88
+ [CallbackType.Spend]: BoxInfo[];
89
+ };
90
+
91
+ export type CallbackMap<ExtractedData extends AbstractBoxData> = {
92
+ [K in CallbackType]: (data: CallbackDataMap<ExtractedData>[K]) => void;
93
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rosen-bridge/abstract-extractor",
3
- "version": "0.3.0",
3
+ "version": "1.0.0",
4
4
  "description": "Rosen Bridge extractor interfaces to work with scanner",
5
5
  "repository": "",
6
6
  "license": "GPL-3.0",
@@ -20,9 +20,11 @@
20
20
  "devDependencies": {
21
21
  "@types/lodash-es": "^4.17.12",
22
22
  "@types/node": "^20.11.9",
23
+ "@types/uuid": "^10.0.0",
23
24
  "@typescript-eslint/eslint-plugin": "^6.19.1",
24
25
  "@typescript-eslint/parser": "^6.19.1",
25
26
  "@vitest/coverage-istanbul": "^1.2.2",
27
+ "await-semaphore": "^0.1.3",
26
28
  "eslint": "^8.56.0",
27
29
  "eslint-config-prettier": "^9.1.0",
28
30
  "extensionless": "^1.9.6",
@@ -38,6 +40,8 @@
38
40
  "@rosen-bridge/json-bigint": "^0.1.0",
39
41
  "@rosen-clients/ergo-explorer": "^1.1.2",
40
42
  "@rosen-clients/ergo-node": "^1.1.1",
41
- "lodash-es": "^4.17.21"
43
+ "lodash-es": "^4.17.21",
44
+ "typeorm": "^0.3.20",
45
+ "uuid": "^9.0.0"
42
46
  }
43
47
  }
@@ -3,12 +3,19 @@ import {
3
3
  AbstractErgoExtractor,
4
4
  BlockInfo,
5
5
  OutputBox,
6
- ErgoExtractedData,
6
+ AbstractBoxData,
7
7
  AbstractErgoExtractorAction,
8
+ AbstractErgoExtractorEntity,
8
9
  } from '../lib';
9
10
 
10
- export class MockedErgoExtractor extends AbstractErgoExtractor<ErgoExtractedData> {
11
- actions: AbstractErgoExtractorAction<ErgoExtractedData>;
11
+ export class MockedErgoExtractor extends AbstractErgoExtractor<
12
+ AbstractBoxData,
13
+ AbstractErgoExtractorEntity
14
+ > {
15
+ actions: AbstractErgoExtractorAction<
16
+ AbstractBoxData,
17
+ AbstractErgoExtractorEntity
18
+ >;
12
19
 
13
20
  getId = () => 'Test';
14
21
 
@@ -17,10 +24,8 @@ export class MockedErgoExtractor extends AbstractErgoExtractor<ErgoExtractedData
17
24
  hasData = (box: V1.OutputInfo | OutputBox) => false;
18
25
 
19
26
  extractBoxData = (
20
- box: V1.OutputInfo | OutputBox,
21
- blockId: string,
22
- height: number
23
- ): Omit<ErgoExtractedData, 'spendBlock' | 'spendHeight'> | undefined => {
27
+ box: V1.OutputInfo | OutputBox
28
+ ): AbstractBoxData | undefined => {
24
29
  return undefined;
25
30
  };
26
31
  }