@rosen-bridge/tx-pot 0.1.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/.eslintignore +1 -0
- package/README.md +36 -0
- package/dist/db/entities/TransactionEntity.d.ts +15 -0
- package/dist/db/entities/TransactionEntity.d.ts.map +1 -0
- package/dist/db/entities/TransactionEntity.js +81 -0
- package/dist/db/migrations/index.d.ts +7 -0
- package/dist/db/migrations/index.d.ts.map +1 -0
- package/dist/db/migrations/index.js +7 -0
- package/dist/db/migrations/postgres/1706350644686-migration.d.ts +7 -0
- package/dist/db/migrations/postgres/1706350644686-migration.d.ts.map +1 -0
- package/dist/db/migrations/postgres/1706350644686-migration.js +28 -0
- package/dist/db/migrations/sqlite/1706007154531-migration.d.ts +7 -0
- package/dist/db/migrations/sqlite/1706007154531-migration.d.ts.map +1 -0
- package/dist/db/migrations/sqlite/1706007154531-migration.js +28 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/network/AbstractPotChainManager.d.ts +36 -0
- package/dist/network/AbstractPotChainManager.d.ts.map +1 -0
- package/dist/network/AbstractPotChainManager.js +3 -0
- package/dist/transaction/TxPot.d.ts +164 -0
- package/dist/transaction/TxPot.d.ts.map +1 -0
- package/dist/transaction/TxPot.js +386 -0
- package/dist/transaction/types.d.ts +35 -0
- package/dist/transaction/types.d.ts.map +1 -0
- package/dist/transaction/types.js +21 -0
- package/dist/transaction/utils.d.ts +8 -0
- package/dist/transaction/utils.d.ts.map +1 -0
- package/dist/transaction/utils.js +56 -0
- package/lib/db/entities/TransactionEntity.ts +44 -0
- package/lib/db/migrations/index.ts +7 -0
- package/lib/db/migrations/postgres/1706350644686-migration.ts +31 -0
- package/lib/db/migrations/sqlite/1706007154531-migration.ts +31 -0
- package/lib/index.ts +5 -0
- package/lib/network/AbstractPotChainManager.ts +44 -0
- package/lib/transaction/TxPot.ts +519 -0
- package/lib/transaction/types.ts +46 -0
- package/lib/transaction/utils.ts +59 -0
- package/package.json +39 -0
- package/tests/.gitkeep +0 -0
- package/tests/db/dataSource.mock.ts +18 -0
- package/tests/network/TestPotChainManager.ts +23 -0
- package/tests/transaction/TestTxPot.ts +32 -0
- package/tests/transaction/TxPot.spec.ts +1517 -0
- package/tests/transaction/testData.ts +84 -0
- package/tsconfig.build.json +8 -0
- package/tsconfig.build.tsbuildinfo +1 -0
- package/tsconfig.json +9 -0
- package/vitest.config.ts +13 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { MigrationInterface, QueryRunner } from 'typeorm';
|
|
2
|
+
|
|
3
|
+
export class Migration1706350644686 implements MigrationInterface {
|
|
4
|
+
name = 'Migration1706350644686';
|
|
5
|
+
|
|
6
|
+
public async up(queryRunner: QueryRunner): Promise<void> {
|
|
7
|
+
await queryRunner.query(`
|
|
8
|
+
CREATE TABLE "transaction_entity" (
|
|
9
|
+
"txId" character varying NOT NULL,
|
|
10
|
+
"chain" character varying NOT NULL,
|
|
11
|
+
"txType" character varying NOT NULL,
|
|
12
|
+
"status" character varying NOT NULL,
|
|
13
|
+
"requiredSign" integer NOT NULL,
|
|
14
|
+
"lastCheck" integer NOT NULL,
|
|
15
|
+
"lastStatusUpdate" character varying NOT NULL,
|
|
16
|
+
"failedInSign" boolean NOT NULL,
|
|
17
|
+
"signFailedCount" integer NOT NULL,
|
|
18
|
+
"serializedTx" character varying NOT NULL,
|
|
19
|
+
"extra" character varying,
|
|
20
|
+
"extra2" character varying,
|
|
21
|
+
CONSTRAINT "PK_cafcc9d8e76fef57bc0cf385caa" PRIMARY KEY ("txId", "chain")
|
|
22
|
+
)
|
|
23
|
+
`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
public async down(queryRunner: QueryRunner): Promise<void> {
|
|
27
|
+
await queryRunner.query(`
|
|
28
|
+
DROP TABLE "transaction_entity"
|
|
29
|
+
`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { MigrationInterface, QueryRunner } from 'typeorm';
|
|
2
|
+
|
|
3
|
+
export class Migration1706007154531 implements MigrationInterface {
|
|
4
|
+
name = 'Migration1706007154531';
|
|
5
|
+
|
|
6
|
+
public async up(queryRunner: QueryRunner): Promise<void> {
|
|
7
|
+
await queryRunner.query(`
|
|
8
|
+
CREATE TABLE "transaction_entity" (
|
|
9
|
+
"txId" varchar NOT NULL,
|
|
10
|
+
"chain" varchar NOT NULL,
|
|
11
|
+
"txType" varchar NOT NULL,
|
|
12
|
+
"status" varchar NOT NULL,
|
|
13
|
+
"requiredSign" integer NOT NULL,
|
|
14
|
+
"lastCheck" integer NOT NULL,
|
|
15
|
+
"lastStatusUpdate" varchar NOT NULL,
|
|
16
|
+
"failedInSign" boolean NOT NULL,
|
|
17
|
+
"signFailedCount" integer NOT NULL,
|
|
18
|
+
"serializedTx" varchar NOT NULL,
|
|
19
|
+
"extra" varchar,
|
|
20
|
+
"extra2" varchar,
|
|
21
|
+
PRIMARY KEY ("txId", "chain")
|
|
22
|
+
)
|
|
23
|
+
`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
public async down(queryRunner: QueryRunner): Promise<void> {
|
|
27
|
+
await queryRunner.query(`
|
|
28
|
+
DROP TABLE "transaction_entity"
|
|
29
|
+
`);
|
|
30
|
+
}
|
|
31
|
+
}
|
package/lib/index.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { AbstractPotChainManager } from './network/AbstractPotChainManager';
|
|
2
|
+
export { TxPot } from './transaction/TxPot';
|
|
3
|
+
export * from './transaction/types';
|
|
4
|
+
export { TransactionEntity } from './db/entities/TransactionEntity';
|
|
5
|
+
export { migrations } from './db/migrations/index';
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { SigningStatus } from '../transaction/types';
|
|
2
|
+
|
|
3
|
+
export abstract class AbstractPotChainManager {
|
|
4
|
+
/**
|
|
5
|
+
* gets the blockchain current height
|
|
6
|
+
*/
|
|
7
|
+
abstract getHeight: () => Promise<number>;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* returns required number of confirmation
|
|
11
|
+
* @param txType
|
|
12
|
+
*/
|
|
13
|
+
abstract getTxRequiredConfirmation: (txType: string) => number;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* gets number of confirmation for a tx
|
|
17
|
+
* returns -1 if tx is not in the blockchain
|
|
18
|
+
* @param txId
|
|
19
|
+
*/
|
|
20
|
+
abstract getTxConfirmation: (txId: string) => Promise<number>;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* checks if a tx is still valid and can be sent to the network
|
|
24
|
+
* @param serializedTx
|
|
25
|
+
* @param signingStatus
|
|
26
|
+
*/
|
|
27
|
+
abstract isTxValid: (
|
|
28
|
+
serializedTx: string,
|
|
29
|
+
signingStatus: SigningStatus
|
|
30
|
+
) => Promise<boolean>;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* submits a tx to the blockchain
|
|
34
|
+
* @param serializedTx
|
|
35
|
+
*/
|
|
36
|
+
abstract submitTransaction: (serializedTx: string) => Promise<void>;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* checks if a tx is in mempool
|
|
40
|
+
* returns false if the chain has no mempool
|
|
41
|
+
* @param txId
|
|
42
|
+
*/
|
|
43
|
+
abstract isTxInMempool: (txId: string) => Promise<boolean>;
|
|
44
|
+
}
|
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
import { DataSource, Repository } from 'typeorm';
|
|
2
|
+
import { TransactionEntity } from '../db/entities/TransactionEntity';
|
|
3
|
+
import { AbstractLogger, DummyLogger } from '@rosen-bridge/logger-interface';
|
|
4
|
+
import {
|
|
5
|
+
CallbackFunction,
|
|
6
|
+
SigningStatus,
|
|
7
|
+
TransactionStatus,
|
|
8
|
+
TxOptions,
|
|
9
|
+
UnregisteredChain,
|
|
10
|
+
ValidatorFunction,
|
|
11
|
+
} from './types';
|
|
12
|
+
import { txOptionToClause } from './utils';
|
|
13
|
+
import { AbstractPotChainManager } from '../network/AbstractPotChainManager';
|
|
14
|
+
|
|
15
|
+
export class TxPot {
|
|
16
|
+
protected static instance: TxPot;
|
|
17
|
+
protected readonly txRepository: Repository<TransactionEntity>;
|
|
18
|
+
protected chains = new Map<string, AbstractPotChainManager>();
|
|
19
|
+
protected validators = new Map<string, Map<string, ValidatorFunction>>();
|
|
20
|
+
protected txTypeCallbacks = new Map<
|
|
21
|
+
string,
|
|
22
|
+
Map<TransactionStatus, CallbackFunction>
|
|
23
|
+
>();
|
|
24
|
+
protected logger: AbstractLogger;
|
|
25
|
+
|
|
26
|
+
protected constructor(dataSource: DataSource, logger?: AbstractLogger) {
|
|
27
|
+
this.txRepository = dataSource.getRepository(TransactionEntity);
|
|
28
|
+
this.logger = logger ?? new DummyLogger();
|
|
29
|
+
this.logger.debug('TxPot instantiated');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* initiates TxPot
|
|
34
|
+
* @param dataSource typeorm data source
|
|
35
|
+
* @param logger
|
|
36
|
+
* @returns
|
|
37
|
+
*/
|
|
38
|
+
public static setup = (
|
|
39
|
+
dataSource: DataSource,
|
|
40
|
+
logger?: AbstractLogger
|
|
41
|
+
): TxPot => {
|
|
42
|
+
TxPot.instance = new TxPot(dataSource, logger);
|
|
43
|
+
return TxPot.instance;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* returns TxPot instance (throws error if none exists)
|
|
48
|
+
* @returns TxPot instance
|
|
49
|
+
*/
|
|
50
|
+
public static getInstance = (): TxPot => {
|
|
51
|
+
if (!TxPot.instance) throw Error(`TxPot instance doesn't exist`);
|
|
52
|
+
return TxPot.instance;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* registers a chain to TxPot
|
|
57
|
+
* @param chain
|
|
58
|
+
* @param chainManager
|
|
59
|
+
*/
|
|
60
|
+
registerChain = (
|
|
61
|
+
chain: string,
|
|
62
|
+
chainManager: AbstractPotChainManager
|
|
63
|
+
): void => {
|
|
64
|
+
this.chains.set(chain, chainManager);
|
|
65
|
+
this.logger.debug(
|
|
66
|
+
`A TxPot chain manager is registered for chain [${chain}]`
|
|
67
|
+
);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* registers a validator function
|
|
72
|
+
* @param chain
|
|
73
|
+
* @param txType
|
|
74
|
+
* @param validator
|
|
75
|
+
*/
|
|
76
|
+
registerValidator = (
|
|
77
|
+
chain: string,
|
|
78
|
+
txType: string,
|
|
79
|
+
validator: ValidatorFunction
|
|
80
|
+
): void => {
|
|
81
|
+
let chainValidators = this.validators.get(chain);
|
|
82
|
+
if (!chainValidators) {
|
|
83
|
+
chainValidators = new Map<string, ValidatorFunction>();
|
|
84
|
+
this.validators.set(chain, chainValidators);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
chainValidators.set(txType, validator);
|
|
88
|
+
this.logger.debug(
|
|
89
|
+
`A tx validator function is registered for chain [${chain}] and type [${txType}]`
|
|
90
|
+
);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* registers a callback function
|
|
95
|
+
* the callback will be called when status of any transactions
|
|
96
|
+
* of given type changes to given status
|
|
97
|
+
* @param txType
|
|
98
|
+
* @param status
|
|
99
|
+
* @param callback
|
|
100
|
+
*/
|
|
101
|
+
registerCallback = (
|
|
102
|
+
txType: string,
|
|
103
|
+
status: TransactionStatus,
|
|
104
|
+
callback: CallbackFunction
|
|
105
|
+
): void => {
|
|
106
|
+
let typeCallbacks = this.txTypeCallbacks.get(txType);
|
|
107
|
+
if (!typeCallbacks) {
|
|
108
|
+
typeCallbacks = new Map<TransactionStatus, CallbackFunction>();
|
|
109
|
+
this.txTypeCallbacks.set(txType, typeCallbacks);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
typeCallbacks.set(status, callback);
|
|
113
|
+
this.logger.debug(
|
|
114
|
+
`A tx status callback function is registered for type [${txType}] and status [${status}]`
|
|
115
|
+
);
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* returns chain manager for given chain
|
|
120
|
+
* throws error if no manager is registered for it
|
|
121
|
+
* @param chain
|
|
122
|
+
*/
|
|
123
|
+
protected getChainManager = (chain: string): AbstractPotChainManager => {
|
|
124
|
+
const manager = this.chains.get(chain);
|
|
125
|
+
if (!manager)
|
|
126
|
+
throw new UnregisteredChain(
|
|
127
|
+
`No manager is registered for chain [${chain}]`
|
|
128
|
+
);
|
|
129
|
+
return manager;
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* sets the tx as invalid if enough blocks is passed from last check
|
|
134
|
+
* @param tx
|
|
135
|
+
*/
|
|
136
|
+
protected setTransactionAsInvalid = async (
|
|
137
|
+
tx: TransactionEntity
|
|
138
|
+
): Promise<void> => {
|
|
139
|
+
const manager = this.getChainManager(tx.chain);
|
|
140
|
+
|
|
141
|
+
const currentHeight = await manager.getHeight();
|
|
142
|
+
const requiredConfirmation = manager.getTxRequiredConfirmation(tx.txType);
|
|
143
|
+
|
|
144
|
+
if (currentHeight - tx.lastCheck >= requiredConfirmation) {
|
|
145
|
+
await this.setTxStatus(tx, TransactionStatus.INVALID);
|
|
146
|
+
this.logger.info(`Tx [${tx.txId}] is invalid`);
|
|
147
|
+
} else {
|
|
148
|
+
this.logger.info(
|
|
149
|
+
`Tx [${
|
|
150
|
+
tx.txId
|
|
151
|
+
}] seems invalid. Waiting for enough confirmation of this proposition [${
|
|
152
|
+
currentHeight - tx.lastCheck
|
|
153
|
+
}/${requiredConfirmation}]`
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* validates a transaction
|
|
160
|
+
* returns true if no validator functions is set or tx is valid
|
|
161
|
+
* otherwise handle the tx as invalid and returns false
|
|
162
|
+
* @param tx
|
|
163
|
+
*/
|
|
164
|
+
protected validateTx = async (tx: TransactionEntity): Promise<boolean> => {
|
|
165
|
+
const validator = this.validators.get(tx.chain)?.get(tx.txType);
|
|
166
|
+
if (validator === undefined) {
|
|
167
|
+
// tx is valid since no validator is found
|
|
168
|
+
this.logger.debug(
|
|
169
|
+
`No validator function is found for chain [${tx.chain}] and type [${tx.txType}]`
|
|
170
|
+
);
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
if (await validator(tx)) return true;
|
|
174
|
+
|
|
175
|
+
await this.setTransactionAsInvalid(tx);
|
|
176
|
+
return false;
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* updates the status of a tx
|
|
181
|
+
* @param txKey tx id and chain
|
|
182
|
+
* @param status new status
|
|
183
|
+
*/
|
|
184
|
+
protected setTxStatus = async (
|
|
185
|
+
tx: TransactionEntity,
|
|
186
|
+
status: TransactionStatus
|
|
187
|
+
): Promise<void> => {
|
|
188
|
+
await this.txRepository.update(
|
|
189
|
+
{
|
|
190
|
+
txId: tx.txId,
|
|
191
|
+
chain: tx.chain,
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
status: status,
|
|
195
|
+
lastStatusUpdate: this.currentTime(),
|
|
196
|
+
}
|
|
197
|
+
);
|
|
198
|
+
const callback = this.txTypeCallbacks.get(tx.txType)?.get(status);
|
|
199
|
+
if (callback)
|
|
200
|
+
callback(tx, status).catch((e) => {
|
|
201
|
+
this.logger.debug(
|
|
202
|
+
`An error occurred while handling tx [${tx.txId}] status change: ${e}`
|
|
203
|
+
);
|
|
204
|
+
if (e instanceof Error && e.stack) this.logger.debug(e.stack);
|
|
205
|
+
});
|
|
206
|
+
else
|
|
207
|
+
this.logger.debug(
|
|
208
|
+
`No callback function is set for type [${tx.txType}] and status [${status}]`
|
|
209
|
+
);
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* @returns current timestamp in seconds and string format
|
|
214
|
+
*/
|
|
215
|
+
protected currentTime = () => String(Math.round(Date.now() / 1000));
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* submits the signed transaction to the blockchain
|
|
219
|
+
* @param tx
|
|
220
|
+
*/
|
|
221
|
+
protected processSignedTx = async (tx: TransactionEntity): Promise<void> => {
|
|
222
|
+
const manager = this.getChainManager(tx.chain);
|
|
223
|
+
try {
|
|
224
|
+
await manager.submitTransaction(tx.serializedTx);
|
|
225
|
+
} catch (e) {
|
|
226
|
+
this.logger.warn(
|
|
227
|
+
`Failed to submit tx [${tx.txId}] to chain [${tx.chain}]: ${e}`
|
|
228
|
+
);
|
|
229
|
+
if (e instanceof Error && e.stack) this.logger.warn(e.stack);
|
|
230
|
+
}
|
|
231
|
+
await this.setTxStatus(tx, TransactionStatus.SENT);
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* processes the sent transaction
|
|
236
|
+
* @param tx
|
|
237
|
+
*/
|
|
238
|
+
protected processesSentTx = async (tx: TransactionEntity): Promise<void> => {
|
|
239
|
+
const manager = this.getChainManager(tx.chain);
|
|
240
|
+
|
|
241
|
+
const txConfirmation = await manager.getTxConfirmation(tx.txId);
|
|
242
|
+
const requiredConfirmation = manager.getTxRequiredConfirmation(tx.txType);
|
|
243
|
+
|
|
244
|
+
if (txConfirmation >= requiredConfirmation) {
|
|
245
|
+
// tx is confirmed enough
|
|
246
|
+
await this.setTxStatus(tx, TransactionStatus.COMPLETED);
|
|
247
|
+
} else if (txConfirmation === -1) {
|
|
248
|
+
// tx is not mined, checking mempool...
|
|
249
|
+
if (await manager.isTxInMempool(tx.txId)) {
|
|
250
|
+
// tx is in mempool, updating last check...
|
|
251
|
+
const height = await manager.getHeight();
|
|
252
|
+
await this.updateTxLastCheck(tx.txId, tx.chain, height);
|
|
253
|
+
this.logger.info(`Tx [${tx.txId}] is in mempool`);
|
|
254
|
+
} else {
|
|
255
|
+
// tx is not in mempool, checking if tx is still valid
|
|
256
|
+
const isValidTx = await manager.isTxValid(
|
|
257
|
+
tx.serializedTx,
|
|
258
|
+
SigningStatus.Signed
|
|
259
|
+
);
|
|
260
|
+
const isValidToType = await this.validateTx(tx);
|
|
261
|
+
|
|
262
|
+
if (isValidTx && isValidToType) {
|
|
263
|
+
// tx is valid. resending...
|
|
264
|
+
this.logger.info(`Tx [${tx.txId}] is still valid. Resending tx...`);
|
|
265
|
+
await manager.submitTransaction(tx.serializedTx);
|
|
266
|
+
} else {
|
|
267
|
+
// tx seems invalid. reset status if enough blocks past.
|
|
268
|
+
await this.setTransactionAsInvalid(tx);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
} else {
|
|
272
|
+
// tx is mined, but is not confirmed enough, updating last check...
|
|
273
|
+
const height = await manager.getHeight();
|
|
274
|
+
await this.updateTxLastCheck(tx.txId, tx.chain, height);
|
|
275
|
+
this.logger.info(
|
|
276
|
+
`Tx [${tx.txId}] is in confirmation process [${txConfirmation}/${requiredConfirmation}]`
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* runs all jobs of TxPot
|
|
283
|
+
* - process signed txs
|
|
284
|
+
* - process sent txs
|
|
285
|
+
*/
|
|
286
|
+
update = async (): Promise<void> => {
|
|
287
|
+
// process signed txs
|
|
288
|
+
const signedTxs = await this.getTxsByStatus(TransactionStatus.SIGNED);
|
|
289
|
+
for (const tx of signedTxs) {
|
|
290
|
+
try {
|
|
291
|
+
await this.processSignedTx(tx);
|
|
292
|
+
} catch (e) {
|
|
293
|
+
this.logger.warn(
|
|
294
|
+
`An error occurred while processing tx [${tx.txId}] with status [${TransactionStatus.SIGNED}]: ${e}`
|
|
295
|
+
);
|
|
296
|
+
if (e instanceof Error && e.stack) this.logger.warn(e.stack);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
this.logger.debug(
|
|
300
|
+
`Processed [${signedTxs.length}] txs with status [${TransactionStatus.SIGNED}]`
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
// process sent txs
|
|
304
|
+
const sentTxs = await this.getTxsByStatus(TransactionStatus.SENT);
|
|
305
|
+
for (const tx of sentTxs) {
|
|
306
|
+
try {
|
|
307
|
+
await this.processesSentTx(tx);
|
|
308
|
+
} catch (e) {
|
|
309
|
+
this.logger.warn(
|
|
310
|
+
`An error occurred while processing tx [${tx.txId}] with status [${TransactionStatus.SENT}]: ${e}`
|
|
311
|
+
);
|
|
312
|
+
if (e instanceof Error && e.stack) this.logger.warn(e.stack);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
this.logger.debug(
|
|
316
|
+
`Processed [${sentTxs.length}] txs with status [${TransactionStatus.SENT}]`
|
|
317
|
+
);
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* gets transactions by status
|
|
322
|
+
* @param status
|
|
323
|
+
* @param validate
|
|
324
|
+
* @returns
|
|
325
|
+
*/
|
|
326
|
+
getTxsByStatus = async (
|
|
327
|
+
status: TransactionStatus,
|
|
328
|
+
validate = false
|
|
329
|
+
): Promise<Array<TransactionEntity>> => {
|
|
330
|
+
const txs = await this.txRepository.find({
|
|
331
|
+
where: {
|
|
332
|
+
status: status,
|
|
333
|
+
},
|
|
334
|
+
});
|
|
335
|
+
if (!validate) return txs;
|
|
336
|
+
|
|
337
|
+
// validate the transactions
|
|
338
|
+
const validTxs: Array<TransactionEntity> = [];
|
|
339
|
+
for (const tx of txs) {
|
|
340
|
+
if (await this.validateTx(tx)) validTxs.push(tx);
|
|
341
|
+
}
|
|
342
|
+
return validTxs;
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* inserts a new transaction into db
|
|
347
|
+
* @param txId
|
|
348
|
+
* @param chain
|
|
349
|
+
* @param txType
|
|
350
|
+
* @param requiredSign
|
|
351
|
+
* @param serializedTx
|
|
352
|
+
* @param initialStatus
|
|
353
|
+
* @param lastCheck
|
|
354
|
+
*/
|
|
355
|
+
addTx = async (
|
|
356
|
+
txId: string,
|
|
357
|
+
chain: string,
|
|
358
|
+
txType: string,
|
|
359
|
+
requiredSign: number,
|
|
360
|
+
serializedTx: string,
|
|
361
|
+
initialStatus = TransactionStatus.APPROVED,
|
|
362
|
+
lastCheck = 0
|
|
363
|
+
): Promise<void> => {
|
|
364
|
+
await this.txRepository.insert({
|
|
365
|
+
txId: txId,
|
|
366
|
+
chain: chain,
|
|
367
|
+
txType: txType,
|
|
368
|
+
status: initialStatus,
|
|
369
|
+
requiredSign: requiredSign,
|
|
370
|
+
lastCheck: lastCheck,
|
|
371
|
+
lastStatusUpdate: this.currentTime(),
|
|
372
|
+
failedInSign: false,
|
|
373
|
+
signFailedCount: 0,
|
|
374
|
+
serializedTx: serializedTx,
|
|
375
|
+
});
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* updates the status of a tx
|
|
380
|
+
* @param txId
|
|
381
|
+
* @param chain
|
|
382
|
+
* @param status new status
|
|
383
|
+
*/
|
|
384
|
+
setTxStatusById = async (
|
|
385
|
+
txId: string,
|
|
386
|
+
chain: string,
|
|
387
|
+
status: TransactionStatus
|
|
388
|
+
): Promise<void> => {
|
|
389
|
+
const tx = await this.txRepository.findOneOrFail({
|
|
390
|
+
where: { txId, chain },
|
|
391
|
+
});
|
|
392
|
+
await this.setTxStatus(tx, status);
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* updates tx info when failed in sign process
|
|
397
|
+
* @param txId
|
|
398
|
+
* @param chain
|
|
399
|
+
*/
|
|
400
|
+
setTxAsSignFailed = async (txId: string, chain: string): Promise<void> => {
|
|
401
|
+
await this.txRepository.update(
|
|
402
|
+
{
|
|
403
|
+
txId: txId,
|
|
404
|
+
chain: chain,
|
|
405
|
+
status: TransactionStatus.IN_SIGN,
|
|
406
|
+
},
|
|
407
|
+
{
|
|
408
|
+
status: TransactionStatus.SIGN_FAILED,
|
|
409
|
+
lastStatusUpdate: this.currentTime(),
|
|
410
|
+
signFailedCount: () => '"signFailedCount" + 1',
|
|
411
|
+
failedInSign: true,
|
|
412
|
+
}
|
|
413
|
+
);
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* updates the tx and set status as signed
|
|
418
|
+
* @param txId
|
|
419
|
+
* @param chain
|
|
420
|
+
* @param serializedTx
|
|
421
|
+
* @param currentHeight current height of the blockchain
|
|
422
|
+
* @param extra
|
|
423
|
+
* @param extra2
|
|
424
|
+
*/
|
|
425
|
+
setTxAsSigned = async (
|
|
426
|
+
txId: string,
|
|
427
|
+
chain: string,
|
|
428
|
+
serializedTx: string,
|
|
429
|
+
currentHeight: number,
|
|
430
|
+
extra?: string,
|
|
431
|
+
extra2?: string
|
|
432
|
+
): Promise<void> => {
|
|
433
|
+
const updatedFields: Partial<TransactionEntity> = {
|
|
434
|
+
serializedTx: serializedTx,
|
|
435
|
+
status: TransactionStatus.SIGNED,
|
|
436
|
+
lastStatusUpdate: this.currentTime(),
|
|
437
|
+
lastCheck: currentHeight,
|
|
438
|
+
};
|
|
439
|
+
if (extra) updatedFields.extra = extra;
|
|
440
|
+
if (extra2) updatedFields.extra2 = extra2;
|
|
441
|
+
|
|
442
|
+
await this.txRepository.update({ txId, chain }, updatedFields);
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* updates last check value of a tx
|
|
447
|
+
* @param txId
|
|
448
|
+
* @param chain
|
|
449
|
+
* @param currentHeight current height of the blockchain
|
|
450
|
+
*/
|
|
451
|
+
updateTxLastCheck = async (
|
|
452
|
+
txId: string,
|
|
453
|
+
chain: string,
|
|
454
|
+
currentHeight: number
|
|
455
|
+
): Promise<void> => {
|
|
456
|
+
await this.txRepository.update(
|
|
457
|
+
{ txId, chain },
|
|
458
|
+
{ lastCheck: currentHeight }
|
|
459
|
+
);
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* updates failedInSign field of a transaction to false
|
|
464
|
+
* @param txId
|
|
465
|
+
* @param chain
|
|
466
|
+
*/
|
|
467
|
+
resetFailedInSign = async (txId: string, chain: string): Promise<void> => {
|
|
468
|
+
await this.txRepository.update(
|
|
469
|
+
{ txId, chain },
|
|
470
|
+
{
|
|
471
|
+
failedInSign: false,
|
|
472
|
+
}
|
|
473
|
+
);
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* updates requiredSign field of a transaction
|
|
478
|
+
* @param txId
|
|
479
|
+
* @param chain
|
|
480
|
+
* @param requiredSign
|
|
481
|
+
*/
|
|
482
|
+
updateRequiredSign = async (
|
|
483
|
+
txId: string,
|
|
484
|
+
chain: string,
|
|
485
|
+
requiredSign: number
|
|
486
|
+
): Promise<void> => {
|
|
487
|
+
await this.txRepository.update(
|
|
488
|
+
{ txId, chain },
|
|
489
|
+
{
|
|
490
|
+
requiredSign: requiredSign,
|
|
491
|
+
}
|
|
492
|
+
);
|
|
493
|
+
};
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* gets the transaction by its id and chain
|
|
497
|
+
* @param txId
|
|
498
|
+
* @param chain
|
|
499
|
+
*/
|
|
500
|
+
getTxByKey = async (
|
|
501
|
+
txId: string,
|
|
502
|
+
chain: string
|
|
503
|
+
): Promise<TransactionEntity | null> => {
|
|
504
|
+
return await this.txRepository.findOne({
|
|
505
|
+
where: { txId, chain },
|
|
506
|
+
});
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* @returns the transactions with valid status
|
|
511
|
+
*/
|
|
512
|
+
getTxsQuery = (
|
|
513
|
+
options: Array<TxOptions> = []
|
|
514
|
+
): Promise<TransactionEntity[]> => {
|
|
515
|
+
return this.txRepository.find({
|
|
516
|
+
where: options.map(txOptionToClause),
|
|
517
|
+
});
|
|
518
|
+
};
|
|
519
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { TransactionEntity } from '../db/entities/TransactionEntity';
|
|
2
|
+
|
|
3
|
+
export type ChainRequiredConfirmations = Record<string, number>; // tx type => required number
|
|
4
|
+
export type RequiredConfirmations = Record<string, ChainRequiredConfirmations>;
|
|
5
|
+
|
|
6
|
+
export type ValidatorFunction = (tx: TransactionEntity) => Promise<boolean>;
|
|
7
|
+
export type CallbackFunction = (
|
|
8
|
+
tx: TransactionEntity,
|
|
9
|
+
newStatus: TransactionStatus
|
|
10
|
+
) => Promise<void>;
|
|
11
|
+
|
|
12
|
+
export enum TransactionStatus {
|
|
13
|
+
APPROVED = 'approved',
|
|
14
|
+
IN_SIGN = 'in-sign',
|
|
15
|
+
SIGN_FAILED = 'sign-failed',
|
|
16
|
+
SIGNED = 'signed',
|
|
17
|
+
SENT = 'sent',
|
|
18
|
+
INVALID = 'invalid',
|
|
19
|
+
COMPLETED = 'completed',
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export enum SigningStatus {
|
|
23
|
+
Signed,
|
|
24
|
+
UnSigned,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type FieldValue<T> = T | Array<T>;
|
|
28
|
+
export interface FieldOption<T> {
|
|
29
|
+
not: boolean;
|
|
30
|
+
value: FieldValue<T>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface TxOptions {
|
|
34
|
+
txId?: FieldValue<string>;
|
|
35
|
+
chain?: string;
|
|
36
|
+
txType?: string;
|
|
37
|
+
status?: FieldOption<TransactionStatus>;
|
|
38
|
+
failedInSign?: boolean;
|
|
39
|
+
extra?: FieldValue<string>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export class UnregisteredChain extends Error {
|
|
43
|
+
constructor(msg: string) {
|
|
44
|
+
super('UnregisteredChain: ' + msg);
|
|
45
|
+
}
|
|
46
|
+
}
|