diff --git a/.changeset/fuzzy-cows-sneeze.md b/.changeset/fuzzy-cows-sneeze.md new file mode 100644 index 00000000..4e292233 --- /dev/null +++ b/.changeset/fuzzy-cows-sneeze.md @@ -0,0 +1,5 @@ +--- +'guard-service': minor +--- + +Add Event Synchronization feature: Communicate with other guards to get the payment transaction of an event and move it to reward distribution diff --git a/config/default.yaml b/config/default.yaml index 9a067ea1..5c0fdebc 100644 --- a/config/default.yaml +++ b/config/default.yaml @@ -120,6 +120,11 @@ reward: networkFeeRepoAddress: '' # guards address for receiving network fee of events watchersSharePercent: 50 # watchers share for event fees (payed in tokens or native token) watchersEmissionSharePercent: 0 # watchers share for event fees (payed in emission token) +eventSync: + parallelSyncLimit: 3 + parallelRequestCount: 3 + timeout: 3600 + interval: 60 tss: path: './bin/tss.exe' # path to tss executable file configPath: './bin/conf/conf.env' diff --git a/package-lock.json b/package-lock.json index ee8bc964..34e51f85 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44,19 +44,19 @@ "@rosen-bridge/tx-progress-check": "^1.0.2", "@rosen-bridge/watcher-data-extractor": "^8.0.2", "@rosen-bridge/winston-logger": "1.0.2", - "@rosen-chains/abstract-chain": "10.0.0", - "@rosen-chains/binance": "0.1.2", - "@rosen-chains/bitcoin": "6.0.0", - "@rosen-chains/bitcoin-esplora": "4.0.4", - "@rosen-chains/cardano": "10.0.0", - "@rosen-chains/cardano-blockfrost-network": "7.0.3", - "@rosen-chains/cardano-koios-network": "10.0.3", - "@rosen-chains/ergo": "10.0.0", - "@rosen-chains/ergo-explorer-network": "9.0.3", - "@rosen-chains/ergo-node-network": "9.0.3", - "@rosen-chains/ethereum": "0.1.10", - "@rosen-chains/evm": "5.0.0", - "@rosen-chains/evm-rpc": "2.1.7", + "@rosen-chains/abstract-chain": "11.0.0", + "@rosen-chains/binance": "0.2.0", + "@rosen-chains/bitcoin": "6.1.0", + "@rosen-chains/bitcoin-esplora": "4.0.5", + "@rosen-chains/cardano": "10.1.0", + "@rosen-chains/cardano-blockfrost-network": "7.0.4", + "@rosen-chains/cardano-koios-network": "10.0.4", + "@rosen-chains/ergo": "10.1.0", + "@rosen-chains/ergo-explorer-network": "9.0.4", + "@rosen-chains/ergo-node-network": "9.0.4", + "@rosen-chains/ethereum": "0.2.0", + "@rosen-chains/evm": "5.1.0", + "@rosen-chains/evm-rpc": "2.1.8", "@sinclair/typebox": "^0.30.4", "await-semaphore": "^0.1.3", "axios": "^1.6.8", @@ -4347,9 +4347,9 @@ } }, "node_modules/@rosen-chains/abstract-chain": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/@rosen-chains/abstract-chain/-/abstract-chain-10.0.0.tgz", - "integrity": "sha512-GnSFM1TVwBYP3nC/heesEwbcdEyzJ+/I6yZT/j0wqnwUHUN5So+lIx42gb0V4URlQjyAAVqopRPcRutLgXDrLQ==", + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@rosen-chains/abstract-chain/-/abstract-chain-11.0.0.tgz", + "integrity": "sha512-b7fnljOYU0g9L9J7RyLaKQDZzOAaHNW8uvm3NfQVjOQzS8b2/66IGpFZ6VHcnKo2pN8/pRVIP63qJhv5tZlwCQ==", "dependencies": { "@rosen-bridge/abstract-logger": "^2.0.1", "@rosen-bridge/json-bigint": "^0.1.0", @@ -4360,30 +4360,30 @@ } }, "node_modules/@rosen-chains/binance": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/@rosen-chains/binance/-/binance-0.1.2.tgz", - "integrity": "sha512-u/yBVyO+C0Z1NOFa7WiLPTI1BCqHSckAAdxyJD1Fk2IpiHb4maiv25Bp1OzXB/YyM6JXMIQJUX2V3ZcSQKpxIA==", + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@rosen-chains/binance/-/binance-0.2.0.tgz", + "integrity": "sha512-KHC57slNSFKDAN86i7gbK9jQe1CGH+opqvLV2E12bfgFfGR6GCcabGJEfxTFMRo/kPu8AFcIVfKEfSPKztAdMA==", "dependencies": { "@rosen-bridge/abstract-logger": "^2.0.1", "@rosen-bridge/tokens": "^1.2.1", - "@rosen-chains/abstract-chain": "^10.0.0", - "@rosen-chains/evm": "^5.0.0" + "@rosen-chains/abstract-chain": "^11.0.0", + "@rosen-chains/evm": "^5.1.0" }, "engines": { "node": ">=20.11.0" } }, "node_modules/@rosen-chains/bitcoin": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@rosen-chains/bitcoin/-/bitcoin-6.0.0.tgz", - "integrity": "sha512-Sxs8ir3esn3JLCnlWfYqaHSjdsovT7/LD8SvHogC+mWx3/0txOQ7Ba7JaM0eVS/EK5C6LpwJO8lgVSffnhj3rQ==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@rosen-chains/bitcoin/-/bitcoin-6.1.0.tgz", + "integrity": "sha512-vWlIebQXbfKO5JHQj5XL3SrWZml71nbhC6VGKXmAI1YfsHIaRIZyP68TLaqq+7DGrPcL0OWylyM59l+MK1y1LQ==", "dependencies": { "@rosen-bridge/abstract-logger": "^2.0.1", "@rosen-bridge/bitcoin-utxo-selection": "^0.2.2", "@rosen-bridge/json-bigint": "^0.1.0", "@rosen-bridge/rosen-extractor": "^6.2.2", "@rosen-bridge/tokens": "^1.2.1", - "@rosen-chains/abstract-chain": "^10.0.0", + "@rosen-chains/abstract-chain": "^11.0.0", "bitcoinjs-lib": "^6.1.5" }, "engines": { @@ -4391,14 +4391,14 @@ } }, "node_modules/@rosen-chains/bitcoin-esplora": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@rosen-chains/bitcoin-esplora/-/bitcoin-esplora-4.0.4.tgz", - "integrity": "sha512-i286GBtNlAw7TKwPvd3GRMy536HYl8vFUxjvl3A4dcgtu41EhVGVicUnLFcJRSR4X2X9zLujaEtO4qONelj+5w==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@rosen-chains/bitcoin-esplora/-/bitcoin-esplora-4.0.5.tgz", + "integrity": "sha512-HtXT5P2cadd8VXEjHrEQEZo4fkB4Io1UqrV91GesZs3DE6PPvTS6PA3z4YX2m00ogxg/ASqcmB6V2v72eKFRcg==", "dependencies": { "@rosen-bridge/abstract-logger": "^2.0.1", "@rosen-bridge/json-bigint": "^0.1.0", - "@rosen-chains/abstract-chain": "^10.0.0", - "@rosen-chains/bitcoin": "^6.0.0", + "@rosen-chains/abstract-chain": "^11.0.0", + "@rosen-chains/bitcoin": "^6.1.0", "axios": "^1.6.7", "bitcoinjs-lib": "^6.1.5" }, @@ -4407,69 +4407,69 @@ } }, "node_modules/@rosen-chains/cardano": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/@rosen-chains/cardano/-/cardano-10.0.0.tgz", - "integrity": "sha512-UBPDIGdxkPvng64+PtpYO87fnnUc1BQl6BFypcJXfESt3RvAQ96g8fVMj85J2BMino4eLYAXhbLAK5nEAuF1oA==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/@rosen-chains/cardano/-/cardano-10.1.0.tgz", + "integrity": "sha512-Vn5MpcIumQv/o27k1DWZw3sIGLIlOibVfbcza10LhrwZVNQcOVGf9Ar0SG/xnor6BwmPvG2CQAAnVvZrH/MJeQ==", "dependencies": { "@emurgo/cardano-serialization-lib-nodejs": "^11.3.1", "@rosen-bridge/abstract-logger": "^2.0.1", "@rosen-bridge/json-bigint": "^0.1.0", "@rosen-bridge/rosen-extractor": "^6.2.2", "@rosen-bridge/tokens": "^1.2.1", - "@rosen-chains/abstract-chain": "^10.0.0", + "@rosen-chains/abstract-chain": "^11.0.0", "bech32": "^2.0.0" } }, "node_modules/@rosen-chains/cardano-blockfrost-network": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/@rosen-chains/cardano-blockfrost-network/-/cardano-blockfrost-network-7.0.3.tgz", - "integrity": "sha512-W5GVXXK+o2Bba2C+yVmYs056fN1BxTXVUke1v3SaMMkSeS5r6jAzmh4lNkGJj5+g5XaWgFFTiHaqf2c8vjXIwA==", + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/@rosen-chains/cardano-blockfrost-network/-/cardano-blockfrost-network-7.0.4.tgz", + "integrity": "sha512-C/HwDfBFw5OamM6RHdep+QC0subJSlN7DsZVDX0HidY4mnSytOJxEhoTDQnR7rpIBOxRv84XsV9AgXUkJt2bfg==", "dependencies": { "@blockfrost/blockfrost-js": "^5.4.0", "@emurgo/cardano-serialization-lib-nodejs": "^11.3.1", "@rosen-bridge/abstract-logger": "^2.0.1", - "@rosen-chains/abstract-chain": "^10.0.0", - "@rosen-chains/cardano": "^10.0.0" + "@rosen-chains/abstract-chain": "^11.0.0", + "@rosen-chains/cardano": "^10.1.0" }, "engines": { "node": ">=18.12.0" } }, "node_modules/@rosen-chains/cardano-koios-network": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/@rosen-chains/cardano-koios-network/-/cardano-koios-network-10.0.3.tgz", - "integrity": "sha512-WrMADCK+2TyDSyuYp2DeRgTHe7L74QvKckw1GeUIkvnUEBuJgRPT0Me6+LTNLk5I+S55bY+TJZq5MSsM4qxVig==", + "version": "10.0.4", + "resolved": "https://registry.npmjs.org/@rosen-chains/cardano-koios-network/-/cardano-koios-network-10.0.4.tgz", + "integrity": "sha512-q26lb2cxS8D0gVnN8C3h8qR4I6XPDJjv529MbK3ZGn7Nt0ruk3DqVeIgmRuzn5QsxX5loTbFUzrLCDTM+aqvZw==", "dependencies": { "@emurgo/cardano-serialization-lib-nodejs": "^11.3.1", "@rosen-bridge/abstract-logger": "^2.0.1", "@rosen-bridge/json-bigint": "^0.1.0", - "@rosen-chains/abstract-chain": "^10.0.0", - "@rosen-chains/cardano": "^10.0.0", + "@rosen-chains/abstract-chain": "^11.0.0", + "@rosen-chains/cardano": "^10.1.0", "@rosen-clients/cardano-koios": "^2.0.3" } }, "node_modules/@rosen-chains/ergo": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/@rosen-chains/ergo/-/ergo-10.0.0.tgz", - "integrity": "sha512-0OESwdDsJ86EA3yE12nIsr+kLejIk+GvVCtJJzH4NMqQ5ZgPtNk45jeWyzB+zkqI0z5Kq4dYu2woQUlFfez3yw==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/@rosen-chains/ergo/-/ergo-10.1.0.tgz", + "integrity": "sha512-VrMje12WaAW/SVkWaXgLZYddkzQr3LwUAlJCxVrccgSkP66Gf1olOBTJ8FBXKT0hUem2ZGEE9gqFWBBu/v9SpA==", "dependencies": { "@rosen-bridge/abstract-logger": "^2.0.1", "@rosen-bridge/json-bigint": "^0.1.0", "@rosen-bridge/rosen-extractor": "^6.2.2", "@rosen-bridge/tokens": "^1.2.1", - "@rosen-chains/abstract-chain": "^10.0.0", + "@rosen-chains/abstract-chain": "^11.0.0", "ergo-lib-wasm-nodejs": "^0.24.1" } }, "node_modules/@rosen-chains/ergo-explorer-network": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/@rosen-chains/ergo-explorer-network/-/ergo-explorer-network-9.0.3.tgz", - "integrity": "sha512-/eD1Q77920jkK+TFWJ6M7VzIfzCNmGJm6NDGYnGZRLY84QKk5KcwQ3cTCe1Y7QCj3AZzG0DrW0w1ymdp2PJSiQ==", + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/@rosen-chains/ergo-explorer-network/-/ergo-explorer-network-9.0.4.tgz", + "integrity": "sha512-EQgDpxKtfwvzE6lpD1GlyY9zFO93eV1XnvLlBdf5GMbYE7T5MZRL6R/cdfX9hUhNBmbmLdcO8vN6nd3vlDs3CQ==", "dependencies": { "@rosen-bridge/abstract-logger": "^2.0.1", "@rosen-bridge/json-bigint": "^0.1.0", - "@rosen-chains/abstract-chain": "^10.0.0", - "@rosen-chains/ergo": "^10.0.0", + "@rosen-chains/abstract-chain": "^11.0.0", + "@rosen-chains/ergo": "^10.1.0", "@rosen-clients/ergo-explorer": "^1.1.1", "ergo-lib-wasm-nodejs": "^0.24.1", "it-all": "^3.0.1" @@ -4485,14 +4485,14 @@ } }, "node_modules/@rosen-chains/ergo-node-network": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/@rosen-chains/ergo-node-network/-/ergo-node-network-9.0.3.tgz", - "integrity": "sha512-PUZpTuvy0T3JftIRpDzRcKBI+c2GY2iEP6xBktBkf/P1F5J0RIt37KWQscIryLseJ0GyIispyTkAYBb/T8iHTw==", + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/@rosen-chains/ergo-node-network/-/ergo-node-network-9.0.4.tgz", + "integrity": "sha512-pHYeayVuBB0G9zjNRrKVG3mWNx1yrmy8sjfasADGhRQoD19rKy6F0MEALxLKD28Ll+B85VVU5mmUhFRPByFkcw==", "dependencies": { "@rosen-bridge/abstract-logger": "^2.0.1", "@rosen-bridge/json-bigint": "^0.1.0", - "@rosen-chains/abstract-chain": "^10.0.0", - "@rosen-chains/ergo": "^10.0.0", + "@rosen-chains/abstract-chain": "^11.0.0", + "@rosen-chains/ergo": "^10.1.0", "@rosen-clients/ergo-node": "^1.1.1", "ergo-lib-wasm-nodejs": "^0.24.1", "it-all": "^3.0.1" @@ -4508,29 +4508,29 @@ } }, "node_modules/@rosen-chains/ethereum": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/@rosen-chains/ethereum/-/ethereum-0.1.10.tgz", - "integrity": "sha512-87h6xT3q+ZUvY/Eu8x0v+dRTOLyvCrZPR/j23yDt+rs5YvY83Urp/v3vGAcFrF1eQJtLtzjQuA+4EF3xInEh6Q==", + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@rosen-chains/ethereum/-/ethereum-0.2.0.tgz", + "integrity": "sha512-3J7QGdmu03DJmx1rX+/aFPNoqza+TNxOHvbwXP1QSRBaoOwkiSYDi6Vz3mICsmXE8XTQIXyCokXEBDQUn8UBVA==", "dependencies": { "@rosen-bridge/abstract-logger": "^2.0.1", "@rosen-bridge/tokens": "^1.2.1", - "@rosen-chains/abstract-chain": "^10.0.0", - "@rosen-chains/evm": "^5.0.0" + "@rosen-chains/abstract-chain": "^11.0.0", + "@rosen-chains/evm": "^5.1.0" }, "engines": { "node": ">=20.11.0" } }, "node_modules/@rosen-chains/evm": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@rosen-chains/evm/-/evm-5.0.0.tgz", - "integrity": "sha512-mry4DWGIT7WypREDGFkumkQV5z81CTb4puidz/zpyciW7s8gJhf1+VhBuIeuU8scn4z5Yccs/GDH/knSqltWDQ==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@rosen-chains/evm/-/evm-5.1.0.tgz", + "integrity": "sha512-9qN6COKrPuwbOankezkLTVB2TDOZINTqONzNNPfYQuER8a/1PsF9hzespS9GFzv6ZV/Cuy7+jtWxNjCeQA8m8g==", "dependencies": { "@rosen-bridge/abstract-logger": "^2.0.1", "@rosen-bridge/json-bigint": "^0.1.0", "@rosen-bridge/rosen-extractor": "^6.2.2", "@rosen-bridge/tokens": "^1.2.1", - "@rosen-chains/abstract-chain": "^10.0.0", + "@rosen-chains/abstract-chain": "^11.0.0", "ethers": "^6.11.1" }, "engines": { @@ -4538,14 +4538,14 @@ } }, "node_modules/@rosen-chains/evm-rpc": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/@rosen-chains/evm-rpc/-/evm-rpc-2.1.7.tgz", - "integrity": "sha512-MzEIcDfKMc1QjFSJpPvv/M8na71EKwqIS0YhpVgsFiSQuRC2LI7be8crOj3mFT4G2DRq5C1z0R84c/QDdaPZaA==", + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@rosen-chains/evm-rpc/-/evm-rpc-2.1.8.tgz", + "integrity": "sha512-QdtX6F6omokiWNnotCjmeBG8ZfqxA1mknsGAQdlpX6B+wY5GRqB3UvAHrMaQqY5iq1DCGgal2m/DZeU9ZuN0nw==", "dependencies": { "@rosen-bridge/abstract-logger": "^2.0.1", "@rosen-bridge/evm-address-tx-extractor": "^1.0.3", - "@rosen-chains/abstract-chain": "^10.0.0", - "@rosen-chains/evm": "^5.0.0", + "@rosen-chains/abstract-chain": "^11.0.0", + "@rosen-chains/evm": "^5.1.0", "typeorm": "^0.3.20" }, "engines": { diff --git a/package.json b/package.json index 3c83464e..fe5d38dd 100644 --- a/package.json +++ b/package.json @@ -58,19 +58,19 @@ "@rosen-bridge/tx-progress-check": "^1.0.2", "@rosen-bridge/watcher-data-extractor": "^8.0.2", "@rosen-bridge/winston-logger": "1.0.2", - "@rosen-chains/abstract-chain": "10.0.0", - "@rosen-chains/binance": "0.1.2", - "@rosen-chains/bitcoin": "6.0.0", - "@rosen-chains/bitcoin-esplora": "4.0.4", - "@rosen-chains/cardano": "10.0.0", - "@rosen-chains/cardano-blockfrost-network": "7.0.3", - "@rosen-chains/cardano-koios-network": "10.0.3", - "@rosen-chains/ergo": "10.0.0", - "@rosen-chains/ergo-explorer-network": "9.0.3", - "@rosen-chains/ergo-node-network": "9.0.3", - "@rosen-chains/ethereum": "0.1.10", - "@rosen-chains/evm": "5.0.0", - "@rosen-chains/evm-rpc": "2.1.7", + "@rosen-chains/abstract-chain": "11.0.0", + "@rosen-chains/binance": "0.2.0", + "@rosen-chains/bitcoin": "6.1.0", + "@rosen-chains/bitcoin-esplora": "4.0.5", + "@rosen-chains/cardano": "10.1.0", + "@rosen-chains/cardano-blockfrost-network": "7.0.4", + "@rosen-chains/cardano-koios-network": "10.0.4", + "@rosen-chains/ergo": "10.1.0", + "@rosen-chains/ergo-explorer-network": "9.0.4", + "@rosen-chains/ergo-node-network": "9.0.4", + "@rosen-chains/ethereum": "0.2.0", + "@rosen-chains/evm": "5.1.0", + "@rosen-chains/evm-rpc": "2.1.8", "@sinclair/typebox": "^0.30.4", "await-semaphore": "^0.1.3", "axios": "^1.6.8", diff --git a/src/configs/Configs.ts b/src/configs/Configs.ts index 8be4e0a8..af2fcdfc 100644 --- a/src/configs/Configs.ts +++ b/src/configs/Configs.ts @@ -112,6 +112,18 @@ class Configs { >('tss.pubs'), }; + // event synchronization + static parallelSyncLimit = getConfigIntKeyOrDefault( + 'eventSync.parallelSyncLimit', + 3 + ); + static parallelRequestCount = getConfigIntKeyOrDefault( + 'eventSync.parallelRequestCount', + 3 + ); + static eventSyncTimeout = getConfigIntKeyOrDefault('eventSync.timeout', 3600); + static eventSyncInterval = getConfigIntKeyOrDefault('eventSync.interval', 60); + // guards configs static guardMnemonic = config.get('guard.mnemonic'); static guardSecret = Utils.convertMnemonicToSecretKey(this.guardMnemonic); @@ -192,6 +204,7 @@ class Configs { static multiSigCleanUpInterval = 120; // seconds static tssInstanceRestartGap = 5; // seconds static tssUpdateInterval = 10; // seconds + static detectionUpdateInterval = 10; // seconds static timeoutProcessorInterval = getConfigIntKeyOrDefault( 'intervals.timeoutProcessorInterval', 3600 diff --git a/src/db/DatabaseAction.ts b/src/db/DatabaseAction.ts index d61e0cff..9b7e8b71 100644 --- a/src/db/DatabaseAction.ts +++ b/src/db/DatabaseAction.ts @@ -383,6 +383,31 @@ class DatabaseAction { }); }; + /** + * inserts a tx record into transactions table + */ + insertCompletedTx = async ( + paymentTx: PaymentTransaction, + event: ConfirmedEventEntity | null, + requiredSign: number, + order: ArbitraryEntity | null + ): Promise => { + await this.TransactionRepository.insert({ + txId: paymentTx.txId, + txJson: paymentTx.toJson(), + type: paymentTx.txType, + chain: paymentTx.network, + status: TransactionStatus.completed, + lastStatusUpdate: String(Math.round(Date.now() / 1000)), + lastCheck: 0, + event: event !== null ? event : undefined, + order: order !== null ? order : undefined, + failedInSign: false, + signFailedCount: 0, + requiredSign: requiredSign, + }); + }; + /** * @param eventId the event trigger id * @param eventBoxHeight the event trigger box mined height diff --git a/src/guard/Tss.ts b/src/guard/Tss.ts index 767fd700..caa967fe 100644 --- a/src/guard/Tss.ts +++ b/src/guard/Tss.ts @@ -1,9 +1,6 @@ import { - ECDSA, EcdsaSigner, - EdDSA, EddsaSigner, - GuardDetection, StatusEnum, TssSigner, } from '@rosen-bridge/tss'; @@ -13,23 +10,18 @@ import Configs from '../configs/Configs'; import { spawn } from 'child_process'; import { DefaultLoggerFactory } from '@rosen-bridge/abstract-logger'; import { TssAlgorithms } from '../utils/constants'; +import DetectionHandler from '../handlers/DetectionHandler'; const logger = DefaultLoggerFactory.getInstance().getLogger(import.meta.url); class Tss { private static instance: Tss; - protected static curveGuardDetection: GuardDetection; - protected static tssCurveSigner: TssSigner; - protected static curve = { - DETECTION_CHANNEL: 'ecdsa-detection', - SIGNING_CHANNEL: 'tss-ecdsa-signing', + protected static CHANNELS = { + curve: 'tss-ecdsa-signing', + edward: 'tss-eddsa-signing', }; - protected static edwardGuardDetection: GuardDetection; + protected static tssCurveSigner: TssSigner; protected static tssEdwardSigner: TssSigner; - protected static edward = { - DETECTION_CHANNEL: 'eddsa-detection', - SIGNING_CHANNEL: 'tss-eddsa-signing', - }; protected static dialer: Dialer; protected static trustKey: string; @@ -114,40 +106,26 @@ class Tss { * initializes curve (ECDSA) tss prerequisites */ static initCurveTss = async () => { - // initialize guard detection + // initialize tss const curvePublicKeys = Configs.tssKeys.pubs.map((pub) => pub.curvePub); const shareIds = Configs.tssKeys.pubs.map((pub) => pub.curveShareId); - const ecdsaSigner = new ECDSA(Configs.tssKeys.secret); - Tss.curveGuardDetection = new GuardDetection({ - guardsPublicKey: curvePublicKeys, - signer: ecdsaSigner, - submit: this.generateSubmitMessageWrapper(Tss.curve.DETECTION_CHANNEL), - getPeerId: () => Promise.resolve(Tss.dialer.getDialerId()), - }); - await Tss.curveGuardDetection.init(); - // initialize tss Tss.tssCurveSigner = new EcdsaSigner({ tssApiUrl: `${Configs.tssUrl}:${Configs.tssPort}`, getPeerId: () => Promise.resolve(Tss.dialer.getDialerId()), callbackUrl: Configs.tssBaseCallBackUrl + '/' + TssAlgorithms.curve, shares: shareIds, - submitMsg: this.generateSubmitMessageWrapper(Tss.curve.SIGNING_CHANNEL), + submitMsg: this.generateSubmitMessageWrapper(Tss.CHANNELS.curve), secret: Configs.tssKeys.secret, - detection: Tss.curveGuardDetection, + detection: DetectionHandler.getInstance().getDetection().curve, guardsPk: curvePublicKeys, signPerRoundLimit: Configs.tssParallelSignCount, logger: DefaultLoggerFactory.getInstance().getLogger('tssSigner'), }); - // subscribe to channels + // subscribe to channel Tss.dialer.subscribeChannel( - Tss.curve.DETECTION_CHANNEL, - async (msg: string, channal: string, peerId: string) => - await Tss.curveGuardDetection.handleMessage(msg, peerId) - ); - Tss.dialer.subscribeChannel( - Tss.curve.SIGNING_CHANNEL, + Tss.CHANNELS.curve, async (msg: string, channal: string, peerId: string) => await Tss.tssCurveSigner.handleMessage(msg, peerId) ); @@ -157,40 +135,26 @@ class Tss { * initializes edward (EdDSA) tss prerequisites */ static initEdwardTss = async () => { - // initialize guard detection + // initialize tss const edwardPublicKeys = Configs.tssKeys.pubs.map((pub) => pub.edwardPub); const shareIds = Configs.tssKeys.pubs.map((pub) => pub.edwardShareId); - const eddsaSigner = new EdDSA(Configs.tssKeys.secret); - Tss.edwardGuardDetection = new GuardDetection({ - guardsPublicKey: edwardPublicKeys, - signer: eddsaSigner, - submit: this.generateSubmitMessageWrapper(Tss.edward.DETECTION_CHANNEL), - getPeerId: () => Promise.resolve(Tss.dialer.getDialerId()), - }); - await Tss.edwardGuardDetection.init(); - // initialize tss Tss.tssEdwardSigner = new EddsaSigner({ tssApiUrl: `${Configs.tssUrl}:${Configs.tssPort}`, getPeerId: () => Promise.resolve(Tss.dialer.getDialerId()), callbackUrl: Configs.tssBaseCallBackUrl + '/' + TssAlgorithms.edward, shares: shareIds, - submitMsg: this.generateSubmitMessageWrapper(Tss.edward.SIGNING_CHANNEL), + submitMsg: this.generateSubmitMessageWrapper(Tss.CHANNELS.edward), secret: Configs.tssKeys.secret, - detection: Tss.edwardGuardDetection, + detection: DetectionHandler.getInstance().getDetection().edward, guardsPk: edwardPublicKeys, signPerRoundLimit: Configs.tssParallelSignCount, logger: DefaultLoggerFactory.getInstance().getLogger('tssSigner'), }); - // subscribe to channels - Tss.dialer.subscribeChannel( - Tss.edward.DETECTION_CHANNEL, - async (msg: string, channal: string, peerId: string) => - await Tss.edwardGuardDetection.handleMessage(msg, peerId) - ); + // subscribe to channel Tss.dialer.subscribeChannel( - Tss.edward.SIGNING_CHANNEL, + Tss.CHANNELS.edward, async (msg: string, channal: string, peerId: string) => await Tss.tssEdwardSigner.handleMessage(msg, peerId) ); @@ -265,11 +229,9 @@ class Tss { } /** - * update guard detection and tss + * update tss instances */ update = async (): Promise => { - await Tss.curveGuardDetection.update(); - await Tss.edwardGuardDetection.update(); await Tss.tssCurveSigner.update(); await Tss.tssEdwardSigner.update(); }; diff --git a/src/handlers/DetectionHandler.ts b/src/handlers/DetectionHandler.ts new file mode 100644 index 00000000..201db303 --- /dev/null +++ b/src/handlers/DetectionHandler.ts @@ -0,0 +1,116 @@ +import { DefaultLoggerFactory } from '@rosen-bridge/abstract-logger'; +import { ECDSA, EdDSA, GuardDetection } from '@rosen-bridge/tss'; +import Dialer from '../communication/Dialer'; +import Configs from '../configs/Configs'; + +const logger = DefaultLoggerFactory.getInstance().getLogger(import.meta.url); + +class DetectionHandler { + private static instance: DetectionHandler; + protected static dialer: Dialer; + protected static CHANNELS = { + curve: 'ecdsa-detection', + edward: 'eddsa-detection', + }; + protected curveDetection: GuardDetection; + protected edwardDetection: GuardDetection; + + private constructor() { + // generate ECDSA guard detection + const curvePublicKeys = Configs.tssKeys.pubs.map((pub) => pub.curvePub); + const ecdsaSigner = new ECDSA(Configs.tssKeys.secret); + this.curveDetection = new GuardDetection({ + guardsPublicKey: curvePublicKeys, + signer: ecdsaSigner, + submit: this.generateSubmitMessageWrapper( + DetectionHandler.CHANNELS.curve + ), + getPeerId: () => Promise.resolve(DetectionHandler.dialer.getDialerId()), + }); + + // generate EdDSA guard detection + const edwardPublicKeys = Configs.tssKeys.pubs.map((pub) => pub.edwardPub); + const eddsaSigner = new EdDSA(Configs.tssKeys.secret); + this.edwardDetection = new GuardDetection({ + guardsPublicKey: edwardPublicKeys, + signer: eddsaSigner, + submit: this.generateSubmitMessageWrapper( + DetectionHandler.CHANNELS.edward + ), + getPeerId: () => Promise.resolve(DetectionHandler.dialer.getDialerId()), + }); + } + + /** + * initializes DetectionHandler + */ + static init = async () => { + DetectionHandler.dialer = await Dialer.getInstance(); + DetectionHandler.instance = new DetectionHandler(); + + // initialize detection instances + await this.instance.curveDetection.init(); + await this.instance.edwardDetection.init(); + + // subscribe to channels + DetectionHandler.dialer.subscribeChannel( + DetectionHandler.CHANNELS.curve, + async (msg: string, channal: string, peerId: string) => + await this.instance.curveDetection.handleMessage(msg, peerId) + ); + DetectionHandler.dialer.subscribeChannel( + DetectionHandler.CHANNELS.edward, + async (msg: string, channal: string, peerId: string) => + await this.instance.edwardDetection.handleMessage(msg, peerId) + ); + + logger.debug('DetectionHandler initialized'); + }; + + /** + * generates a DetectionHandler object if it doesn't exist + * @returns DetectionHandler instance + */ + static getInstance = () => { + if (!DetectionHandler.instance) + throw Error(`DetectionHandler instance doesn't exist`); + return DetectionHandler.instance; + }; + + /** + * generates a function to wrap channel send message to dialer + * @param channel + */ + protected generateSubmitMessageWrapper = (channel: string) => { + return async (msg: string, peers: Array) => { + if (peers.length === 0) + await DetectionHandler.dialer.sendMessage(channel, msg); + else + await Promise.all( + peers.map(async (peer) => + DetectionHandler.dialer.sendMessage(channel, msg, peer) + ) + ); + }; + }; + + /** + * @returns both ECDSA and EdDSA guard detection instances + */ + getDetection = () => { + return { + curve: this.curveDetection, + edward: this.edwardDetection, + }; + }; + + /** + * update guard detection instances + */ + update = async (): Promise => { + await this.curveDetection.update(); + await this.edwardDetection.update(); + }; +} + +export default DetectionHandler; diff --git a/src/index.ts b/src/index.ts index 47bbd81a..a4f490ca 100644 --- a/src/index.ts +++ b/src/index.ts @@ -20,6 +20,9 @@ import GuardPkHandler from './handlers/GuardPkHandler'; import MinimumFeeHandler from './handlers/MinimumFeeHandler'; import { minimumFeeUpdateJob } from './jobs/minimumFee'; import { NotificationHandler } from './handlers/NotificationHandler'; +import Dialer from './communication/Dialer'; +import EventSynchronization from './synchronization/EventSynchronization'; +import DetectionHandler from './handlers/DetectionHandler'; const init = async () => { // initialize NotificationHandler object @@ -34,6 +37,10 @@ const init = async () => { // initialize express Apis await initApiServer(); + // initialize Dialer and DetectionHandler + await Dialer.getInstance(); + await DetectionHandler.init(); + // initialize tss multiSig object await MultiSigHandler.init(Configs.guardSecret); initializeMultiSigJobs(); @@ -55,6 +62,9 @@ const init = async () => { // initialize TxAgreement object await TxAgreement.getInstance(); + // initialize EventSynchronization object + await EventSynchronization.init(); + // initialize MinimumFeeHandler await MinimumFeeHandler.init(Configs.tokens()); minimumFeeUpdateJob(); diff --git a/src/jobs/runProcessors.ts b/src/jobs/runProcessors.ts index 45ff6637..5c7a300e 100644 --- a/src/jobs/runProcessors.ts +++ b/src/jobs/runProcessors.ts @@ -6,6 +6,11 @@ import ColdStorage from '../coldStorage/ColdStorage'; import ColdStorageConfig from '../coldStorage/ColdStorageConfig'; import TxAgreement from '../agreement/TxAgreement'; import ArbitraryProcessor from '../arbitrary/ArbitraryProcessor'; +import EventSynchronization from '../synchronization/EventSynchronization'; +import DetectionHandler from '../handlers/DetectionHandler'; +import { DefaultLoggerFactory } from '@rosen-bridge/abstract-logger'; + +const logger = DefaultLoggerFactory.getInstance().getLogger(import.meta.url); /** * sends generated tx to agreement @@ -88,11 +93,12 @@ const transactionJob = () => { }; /** - * runs timeout leftover events and orders job + * runs timeout leftover events, orders and event active syncs job */ const timeoutProcessorJob = async () => { await EventProcessor.TimeoutLeftoverEvents(); await ArbitraryProcessor.getInstance().timeoutLeftoverOrders(); + await EventSynchronization.getInstance().timeoutActiveSyncs(); setTimeout(timeoutProcessorJob, Configs.timeoutProcessorInterval * 1000); }; @@ -108,6 +114,30 @@ const requeueWaitingEventsJob = async () => { ); }; +/** + * runs event active synchronizations jobs + */ +const eventSyncJob = async () => { + await EventSynchronization.getInstance().processSyncQueue(); + await EventSynchronization.getInstance().sendSyncBatch(); + setTimeout(eventSyncJob, Configs.eventSyncInterval * 1000); +}; + +/** + * runs Detection update job + */ +const detectionUpdateJob = () => { + DetectionHandler.getInstance() + .update() + .then(() => + setTimeout(detectionUpdateJob, Configs.detectionUpdateInterval * 1000) + ) + .catch((e) => { + logger.error(`Detection update job failed with error: ${e}`); + setTimeout(detectionUpdateJob, Configs.detectionUpdateInterval * 1000); + }); +}; + /** * runs all processors and their related jobs */ @@ -121,6 +151,8 @@ const runProcessors = () => { requeueWaitingEventsJob, Configs.requeueWaitingEventsInterval * 1000 ); + setTimeout(eventSyncJob, Configs.eventSyncInterval * 1000); + setTimeout(detectionUpdateJob, Configs.detectionUpdateInterval * 1000); }; export { runProcessors }; diff --git a/src/synchronization/EventSynchronization.ts b/src/synchronization/EventSynchronization.ts new file mode 100644 index 00000000..4da04ae6 --- /dev/null +++ b/src/synchronization/EventSynchronization.ts @@ -0,0 +1,557 @@ +import { Communicator, ECDSA, GuardDetection } from '@rosen-bridge/tss'; +import { Semaphore } from 'await-semaphore'; +import { DefaultLoggerFactory } from '@rosen-bridge/abstract-logger'; +import { + ConfirmationStatus, + ImpossibleBehavior, + PaymentTransaction, + SigningStatus, + TransactionType, +} from '@rosen-chains/abstract-chain'; +import { isEqual, sampleSize, countBy, shuffle } from 'lodash-es'; +import { EventStatus, TransactionStatus } from '../utils/constants'; +import { + ActiveSync, + SynchronizationMessageTypes, + SyncRequest, + SyncResponse, +} from './Interfaces'; +import Dialer from '../communication/Dialer'; +import * as TransactionSerializer from '../transaction/TransactionSerializer'; +import Configs from '../configs/Configs'; +import GuardTurn from '../utils/GuardTurn'; +import GuardPkHandler from '../handlers/GuardPkHandler'; +import { DatabaseAction } from '../db/DatabaseAction'; +import EventVerifier from '../verification/EventVerifier'; +import EventSerializer from '../event/EventSerializer'; +import MinimumFeeHandler from '../handlers/MinimumFeeHandler'; +import ChainHandler from '../handlers/ChainHandler'; +import EventOrder from '../event/EventOrder'; +import DetectionHandler from '../handlers/DetectionHandler'; + +const logger = DefaultLoggerFactory.getInstance().getLogger(import.meta.url); + +class EventSynchronization extends Communicator { + private static instance: EventSynchronization; + protected static CHANNEL = 'event-synchronization'; + protected static dialer: Dialer; + protected detection: GuardDetection; + protected eventQueue: string[]; + protected activeSyncMap: Map; + protected approvalSemaphore: Semaphore; + protected parallelSyncLimit: number; + protected parallelRequestCount: number; + protected requiredApproval: number; + + protected constructor(publicKeys: string[], detection: GuardDetection) { + super( + logger, + new ECDSA(Configs.tssKeys.secret), + EventSynchronization.sendMessageWrapper, + publicKeys, + GuardTurn.UP_TIME_LENGTH + ); + this.detection = detection; + this.eventQueue = []; + this.activeSyncMap = new Map(); + this.approvalSemaphore = new Semaphore(1); + this.parallelSyncLimit = Configs.parallelSyncLimit; + this.parallelRequestCount = Configs.parallelRequestCount; + this.requiredApproval = GuardPkHandler.getInstance().requiredSign - 1; + } + + /** + * initializes EventSynchronization + */ + static init = async () => { + EventSynchronization.instance = new EventSynchronization( + Configs.tssKeys.pubs.map((pub) => pub.curvePub), + DetectionHandler.getInstance().getDetection().curve + ); + this.dialer = await Dialer.getInstance(); + this.dialer.subscribeChannel( + EventSynchronization.CHANNEL, + EventSynchronization.instance.messageHandlerWrapper + ); + }; + + /** + * generates a EventSynchronization object if it doesn't exist + * @returns EventSynchronization instance + */ + static getInstance = () => { + if (!EventSynchronization.instance) + throw Error(`EventSynchronization instance doesn't exist`); + return EventSynchronization.instance; + }; + + /** + * wraps communicator send message to dialer + * @param msg + * @param peers + */ + static sendMessageWrapper = async (msg: string, peers: Array) => { + if (peers.length === 0) { + EventSynchronization.dialer.sendMessage( + EventSynchronization.CHANNEL, + msg + ); + } else { + for (const peerId of peers) { + EventSynchronization.dialer.sendMessage( + EventSynchronization.CHANNEL, + msg, + peerId + ); + } + } + }; + + /** + * wraps dialer handle message to communicator + * @param msg + * @param channel + * @param peerId + */ + messageHandlerWrapper = async ( + msg: string, + channel: string, + peerId: string + ) => { + this.handleMessage(msg, peerId); + }; + + /** + * adds an event to synchronization queue + * @param eventId + */ + addEventToQueue = (eventId: string): void => { + this.eventQueue.push(eventId); + }; + + /** + * verifies events in the queue and starts synchronization process for them + */ + processSyncQueue = async (): Promise => { + if (this.eventQueue.length === 0) { + logger.info(`No event to sync`); + return; + } + + if (this.activeSyncMap.size >= this.parallelSyncLimit) { + logger.info( + `Already syncing for [${this.activeSyncMap.size}] events, [${this.eventQueue.length}] events are waiting for sync in queue` + ); + return; + } + + let eventId: string; + this.eventQueue = shuffle(this.eventQueue); + while ( + this.eventQueue.length && + this.activeSyncMap.size < this.parallelSyncLimit + ) { + eventId = this.eventQueue.pop()!; + const baseError = `Received event [${eventId}] for synchronization but `; + + // check if event is already in synchronization process + if (this.activeSyncMap.get(eventId)) { + logger.debug(`event is [${eventId}] is already in synchronization`); + continue; + } + + // get event from database + const eventEntity = await DatabaseAction.getInstance().getEventById( + eventId + ); + if (eventEntity === null) { + logger.warn(baseError + `event is not found`); + continue; + } + const event = EventSerializer.fromConfirmedEntity(eventEntity); + + // check if event is confirmed enough + if (!(await EventVerifier.isEventConfirmedEnough(event))) { + logger.warn(baseError + `event is not confirmed enough`); + continue; + } + + // get minimum-fee and verify event + const feeConfig = MinimumFeeHandler.getEventFeeConfig(event); + + // verify event + if (!(await EventVerifier.verifyEvent(event, feeConfig))) { + logger.warn(baseError + `but event hasn't verified`); + await DatabaseAction.getInstance().setEventStatus( + eventId, + EventStatus.rejected + ); + continue; + } + + // active synchronization for the event + this.activeSyncMap.set(eventId, { + timestamp: Math.floor(Date.now() / 1000), + responses: Array(this.guardPks.length).fill(undefined), + }); + logger.info(`Activated synchronization for event [${eventId}]`); + } + }; + + /** + * gets guard peerId by his index + * @param index + */ + protected getPeerIdByIndex = async ( + index: number + ): Promise => { + const pk = this.guardPks[index]; + const activeGuards = await this.detection.activeGuards(); + return activeGuards.find((_) => _.publicKey === pk)?.peerId; + }; + + /** + * sends requests for all active syncs + */ + sendSyncBatch = async (): Promise => { + logger.info(`Sending event synchronization batches`); + for (const [eventId, activeSync] of this.activeSyncMap) { + const indexes = activeSync.responses.reduce( + ( + indexes: number[], + response: PaymentTransaction | undefined, + index: number + ) => { + if (response === undefined) indexes.push(index); + return indexes; + }, + [] + ); + const selectedIndexes = sampleSize(indexes, this.parallelRequestCount); + logger.debug( + `Sending sync request for event [${eventId}] to guards [${indexes.join( + ',' + )}]` + ); + + const selectedPeers = ( + await Promise.all(selectedIndexes.map(this.getPeerIdByIndex)) + ).filter((_) => _) as string[]; + logger.info( + `Sending sync request for event [${eventId}] to peers [${selectedPeers.join( + ',' + )}]` + ); + if (selectedPeers.length === 0) continue; + + const payload: SyncRequest = { eventId: eventId }; + await this.sendMessage( + SynchronizationMessageTypes.request, + payload, + selectedPeers, + Math.round(Date.now() / 1000) + ); + } + }; + + /** + * handles received message from event-synchronization channel + * @param type + * @param payload + * @param signature + * @param senderIndex + * @param peerId + * @param timestamp + */ + processMessage = async ( + type: string, + payload: unknown, + signature: string, + senderIndex: number, + peerId: string, + timestamp: number + ): Promise => { + try { + switch (type) { + case SynchronizationMessageTypes.request: { + const request = payload as SyncRequest; + await this.processSyncRequest( + request.eventId, + senderIndex, + timestamp, + peerId + ); + break; + } + case SynchronizationMessageTypes.response: { + const response = payload as SyncResponse; + const tx = TransactionSerializer.fromJson(response.txJson); + await this.processSyncResponse(tx, senderIndex); + break; + } + default: + logger.warn( + `Received unexpected message type [${type}] in event-synchronization channel` + ); + } + } catch (e) { + logger.warn( + `An error occurred while handling event-synchronization message: ${e}}` + ); + logger.warn(e.stack); + } + }; + + /** + * checks if such event exists and has a completed tx in type of payment + * sends the tx if so, otherwise does nothing + * @param eventId + * @param senderIndex index of the guard that sent the request + * @param timestamp + * @param receiver the guard who will receive this response + */ + protected processSyncRequest = async ( + eventId: string, + senderIndex: number, + timestamp: number, + receiver: string + ): Promise => { + const baseError = `Sync request received for event [${eventId}] but `; + // get event from database + const eventEntity = await DatabaseAction.getInstance().getEventById( + eventId + ); + if (eventEntity === null) { + logger.warn(baseError + `event is not found`); + return; + } + + // check if event has completed tx in type of payment + const eventTxs = await DatabaseAction.getInstance().getEventValidTxsByType( + eventId, + TransactionType.payment + ); + if (eventTxs.length === 0) { + logger.info(baseError + `event has no valid transaction`); + return; + } else if (eventTxs.length === 1) { + const txEntity = eventTxs[0]; + if (txEntity.status === TransactionStatus.completed) { + logger.info( + `Sending tx [${txEntity.txId}] for syncing event [${eventId}] to guard [${senderIndex}]` + ); + // send response to sender guard + const payload: SyncResponse = { txJson: txEntity.txJson }; + await this.sendMessage( + SynchronizationMessageTypes.response, + payload, + [receiver], + timestamp + ); + } else { + logger.info( + baseError + + `tx [${txEntity.txId}] is not completed yet (in status [${txEntity.status}])` + ); + return; + } + } else { + throw new ImpossibleBehavior( + `event [${eventId}] has [${ + eventTxs.length + }] valid transactions for type payment: [${eventTxs + .map((_) => _.txId) + .join(',')}]` + ); + } + }; + + /** + * verifies the sync response sent by other guards, save the transaction if its verified + * @param tx the payment transaction id + * @param senderIndex index of the guard that sent the response + */ + protected processSyncResponse = async ( + tx: PaymentTransaction, + senderIndex: number + ): Promise => { + if (!(await this.verifySynchronizationResponse(tx))) return; + logger.info( + `Guard [${senderIndex}] responded the sync request of event [${tx.eventId}] with transaction [${tx.txId}]` + ); + + await this.approvalSemaphore.acquire().then(async (release) => { + try { + const activeSync = this.activeSyncMap.get(tx.eventId); + if (activeSync) { + activeSync.responses[senderIndex] = tx; + const occurrences = countBy(activeSync.responses.filter((_) => _)); + + if ( + Math.max(...Object.values(occurrences)) >= this.requiredApproval + ) { + logger.info( + `The majority of guards responded the sync request of event [${tx.eventId}] with transaction [${tx.txId}]` + ); + await this.setTxAsApproved(tx); + } else { + logger.debug( + `event [${tx.eventId}] sync status is: [${JSON.stringify( + activeSync.responses.map((_) => _?.txId) + )}]` + ); + } + } + release(); + } catch (e) { + release(); + throw e; + } + }); + }; + + /** + * verifies the transaction sent by other guards for synchronization + * conditions: + * - there is a request for this event + * - tx type is payment + * - PaymentTransaction object consistency is verified + * - tx order is equal to expected event order + * - tx is confirmed enough + * - tx satisfies the chain conditions + * @param tx + * @returns true if transaction verified + */ + protected verifySynchronizationResponse = async ( + tx: PaymentTransaction + ): Promise => { + const baseError = `Received tx [${tx.txId}] for syncing event [${tx.eventId}] but `; + // verify sync request + const activeSync = this.activeSyncMap.get(tx.eventId); + if (!activeSync) { + logger.info(baseError + `sync request for this event is not active`); + return false; + } + + // get event from database + const eventEntity = await DatabaseAction.getInstance().getEventById( + tx.eventId + ); + if (eventEntity === null) { + throw new ImpossibleBehavior(baseError + `event is not found`); + } + const event = EventSerializer.fromConfirmedEntity(eventEntity); + + // verify tx type + if (tx.txType !== TransactionType.payment) { + logger.warn(baseError + `transaction type is unexpected (${tx.txType})`); + return false; + } + + // verify PaymentTransaction object consistency + const chain = ChainHandler.getInstance().getChain(tx.network); + if (!(await chain.verifyPaymentTransaction(tx))) { + logger.warn(baseError + `tx object has inconsistency`); + return false; + } + + // verify tx order + const feeConfig = MinimumFeeHandler.getEventFeeConfig(event); + const txOrder = chain.extractTransactionOrder(tx); + const expectedOrder = await EventOrder.createEventPaymentOrder( + event, + feeConfig, + [] + ); + if (!isEqual(txOrder, expectedOrder)) { + logger.warn(baseError + `tx extracted order is not verified`); + return false; + } + + // check if tx is confirmed enough + const txConfirmation = await chain.getTxConfirmationStatus( + tx.txId, + tx.txType + ); + if (txConfirmation === ConfirmationStatus.NotConfirmedEnough) { + logger.warn(baseError + `tx is not confirmed enough`); + return false; + } else if (txConfirmation === ConfirmationStatus.NotFound) { + logger.warn(baseError + `tx is not found`); + return false; + } + + // check chain-specific conditions + if (!chain.verifyTransactionExtraConditions(tx, SigningStatus.Signed)) { + logger.warn(baseError + `extra conditions are not verified`); + return false; + } + + return true; + }; + + /** + * inserts the transaction as completed into db and updates the event + * @param tx + */ + protected setTxAsApproved = async (tx: PaymentTransaction): Promise => { + const dbAction = DatabaseAction.getInstance(); + const txRecord = await dbAction.getTxById(tx.txId); + const event = await dbAction.getEventById(tx.eventId); + try { + if (event === null) { + throw new ImpossibleBehavior( + `Tx [${tx.txId}] is approved as event [${tx.eventId}] payment but event is not found` + ); + } + if (txRecord !== null) { + throw new ImpossibleBehavior( + `Tx [${tx.txId}] is already in database with status [${txRecord.status}]` + ); + } + + await dbAction.insertCompletedTx( + tx, + event, + GuardPkHandler.getInstance().requiredSign, + null + ); + await DatabaseAction.getInstance().setEventStatusToPending( + tx.eventId, + EventStatus.pendingReward + ); + this.activeSyncMap.delete(tx.eventId); + } catch (e) { + logger.warn( + `An error occurred while finalizing event [${tx.eventId}] synchronization: ${e}` + ); + logger.warn(e.stack); + } + }; + + /** + * deletes active event syncs that are timed out + */ + timeoutActiveSyncs = async (): Promise => { + await this.approvalSemaphore.acquire().then(async (release) => { + logger.info(`Clearing active event synchronizations`); + try { + for (const [eventId, activeSync] of this.activeSyncMap) { + if ( + Math.floor(Date.now() / 1000) - activeSync.timestamp >= + Configs.eventSyncTimeout + ) { + logger.info(`event [${eventId}] synchronization is timed out`); + this.activeSyncMap.delete(eventId); + } + } + release(); + } catch (e) { + release(); + throw e; + } + }); + }; +} + +export default EventSynchronization; diff --git a/src/synchronization/Interfaces.ts b/src/synchronization/Interfaces.ts new file mode 100644 index 00000000..95ef8591 --- /dev/null +++ b/src/synchronization/Interfaces.ts @@ -0,0 +1,19 @@ +import { PaymentTransaction } from '@rosen-chains/abstract-chain'; + +export interface ActiveSync { + timestamp: number; + responses: Array; +} + +export interface SyncRequest { + eventId: string; +} + +export interface SyncResponse { + txJson: string; +} + +export enum SynchronizationMessageTypes { + request = 'request', + response = 'response', +} diff --git a/src/verification/TransactionVerifier.ts b/src/verification/TransactionVerifier.ts index f1dfb8df..f77e117f 100644 --- a/src/verification/TransactionVerifier.ts +++ b/src/verification/TransactionVerifier.ts @@ -4,6 +4,7 @@ import { ImpossibleBehavior, PaymentOrder, PaymentTransaction, + SigningStatus, TransactionType, } from '@rosen-chains/abstract-chain'; import ChainHandler from '../handlers/ChainHandler'; @@ -58,9 +59,9 @@ class TransactionVerifier { } // verify extra conditions - if (!chain.verifyTransactionExtraConditions(tx)) { + if (!chain.verifyTransactionExtraConditions(tx, SigningStatus.UnSigned)) { logger.debug( - `Transaction [${tx.txId}] is invalid: Extra conditions is not verified` + `Transaction [${tx.txId}] is invalid: Extra conditions are not verified` ); return false; } diff --git a/tests/synchronization/EventSynchronization.spec.ts b/tests/synchronization/EventSynchronization.spec.ts new file mode 100644 index 00000000..e0808865 --- /dev/null +++ b/tests/synchronization/EventSynchronization.spec.ts @@ -0,0 +1,1919 @@ +import TestEventSynchronization from './TestEventSynchronization'; +import * as EventTestData from '../event/testData'; +import TestConfigs from '../testUtils/TestConfigs'; +import DatabaseActionMock from '../db/mocked/DatabaseAction.mock'; +import { EventStatus, TransactionStatus } from '../../src/utils/constants'; +import { + mockIsEventConfirmedEnough, + mockVerifyEvent, +} from '../verification/mocked/EventVerifier.mock'; +import { mockGetEventFeeConfig } from '../event/mocked/MinimumFee.mock'; +import EventSerializer from '../../src/event/EventSerializer'; +import GuardPkHandler from '../../src/handlers/GuardPkHandler'; +import TestUtils from '../testUtils/TestUtils'; +import { mockPaymentTransaction } from '../agreement/testData'; +import { + ConfirmationStatus, + PaymentOrder, + TransactionType, +} from '@rosen-chains/abstract-chain'; +import { SynchronizationMessageTypes } from '../../src/synchronization/Interfaces'; +import ChainHandlerMock from '../handlers/ChainHandler.mock'; +import { mockCreateEventPaymentOrder } from '../event/mocked/EventOrder.mock'; +import Configs from '../../src/configs/Configs'; + +describe('EventSynchronization', () => { + describe('addEventToQueue', () => { + /** + * @target EventSynchronization.addEventToQueue should add the event to the memory queue + * @dependencies + * @scenario + * - run test + * - check events in memory + * @expected + * - memory queue should contains mocked event + */ + it('should add the event to the memory queue', async () => { + // run test + const eventId = 'event-id'; + const eventSync = new TestEventSynchronization(); + eventSync.addEventToQueue(eventId); + + // check events in memory + const queue = eventSync.getEventQueue(); + expect(queue).toEqual([eventId]); + }); + }); + + describe('processSyncQueue', () => { + const guardsLen = GuardPkHandler.getInstance().guardsLen; + + beforeAll(() => { + vi.useFakeTimers(); + vi.setSystemTime(new Date(TestConfigs.currentTimeStamp)); + mockGetEventFeeConfig({ + bridgeFee: 0n, + networkFee: 0n, + rsnRatio: 0n, + feeRatio: 100n, + rsnRatioDivisor: 1000000000000n, + feeRatioDivisor: 10000n, + }); + }); + + afterAll(() => { + vi.useRealTimers(); + }); + + beforeEach(async () => { + await DatabaseActionMock.clearTables(); + }); + + /** + * @target EventSynchronization.processSyncQueue should add event to active sync + * when event is verified + * @dependencies + * - Date + * - database + * - EventVerifier + * - MinimumFee + * @scenario + * - mock event + * - insert mocked event into db + * - insert event into queue + * - mock EventVerifier + * - mock `isEventConfirmedEnough` + * - mock `verifyEvent` + * - run test + * - check active syncs in memory + * @expected + * - mocked event should be in memory + * - mocked event sync responses should be initiated + * - memory queue should be empty + */ + it('should add event to active sync when event is verified', async () => { + // mock event + const mockedEvent = EventTestData.mockEventTrigger().event; + const eventId = EventSerializer.getId(mockedEvent); + + // insert mocked event into db + await DatabaseActionMock.insertEventRecord( + mockedEvent, + EventStatus.pendingPayment + ); + + // insert event into queue + const eventSync = new TestEventSynchronization(); + eventSync.addEventToQueue(eventId); + + // mock EventVerifier + mockIsEventConfirmedEnough(true); + mockVerifyEvent(true); + + // run test + await eventSync.processSyncQueue(); + + // check active syncs in memory + const activeSyncs = eventSync.getActiveSyncMap(); + expect(activeSyncs.get(eventId)).toEqual({ + timestamp: TestConfigs.currentTimeStamp / 1000, + responses: Array(guardsLen).fill(undefined), + }); + expect(eventSync.getEventQueue().length).toEqual(0); + }); + + /** + * @target EventSynchronization.processSyncQueue should NOT add event to active sync + * when there are already maximum number of events in active syncs + * @dependencies + * - Date + * - database + * - EventVerifier + * - MinimumFee + * @scenario + * - mock event + * - insert mocked event into db + * - insert event into queue + * - insert 3 events into active sync + * - mock EventVerifier + * - mock `isEventConfirmedEnough` + * - mock `verifyEvent` + * - run test + * - check active syncs in memory + * @expected + * - mocked event should still be in queue + * - active sync map length should still be 3 + */ + it('should NOT add event to active sync when there are already maximum number of events in active syncs', async () => { + // mock event + const mockedEvent = EventTestData.mockEventTrigger().event; + const eventId = EventSerializer.getId(mockedEvent); + + // insert mocked event into db + await DatabaseActionMock.insertEventRecord( + mockedEvent, + EventStatus.pendingPayment + ); + + // insert event into queue + const eventSync = new TestEventSynchronization(); + eventSync.addEventToQueue(eventId); + + // insert 3 events into active sync + for (let i = 0; i < 3; i++) { + eventSync.insertEventIntoActiveSync(TestUtils.generateRandomId(), { + timestamp: TestConfigs.currentTimeStamp, + responses: [], + }); + } + + // mock EventVerifier + mockIsEventConfirmedEnough(true); + mockVerifyEvent(true); + + // run test + await eventSync.processSyncQueue(); + + // check active syncs in memory + const activeSyncs = eventSync.getActiveSyncMap(); + expect(activeSyncs.size).toEqual(3); + expect(eventSync.getEventQueue()).toEqual([eventId]); + }); + + /** + * @target EventSynchronization.processSyncQueue should skip event when event + * is already in active sync + * @dependencies + * - Date + * - database + * - EventVerifier + * - MinimumFee + * @scenario + * - mock event + * - insert mocked event into db + * - insert event into queue and active sync + * - mock EventVerifier + * - mock `isEventConfirmedEnough` + * - mock `verifyEvent` + * - run test + * - check active syncs in memory + * @expected + * - active sync should remain unchanged + * - memory queue should be empty + */ + it('should skip event when event is already in active sync', async () => { + // mock event + const mockedEvent = EventTestData.mockEventTrigger().event; + const eventId = EventSerializer.getId(mockedEvent); + + // insert mocked event into db + await DatabaseActionMock.insertEventRecord( + mockedEvent, + EventStatus.pendingPayment + ); + + // insert event into queue and active sync + const eventSync = new TestEventSynchronization(); + eventSync.addEventToQueue(eventId); + const timestamp = TestConfigs.currentTimeStamp / 1000 - 100; + const responses = Array(guardsLen).fill(undefined); + responses[2] = mockPaymentTransaction( + TransactionType.payment, + mockedEvent.toChain + ); + eventSync.insertEventIntoActiveSync(eventId, { + timestamp: timestamp, + responses: responses, + }); + + // mock EventVerifier + mockIsEventConfirmedEnough(true); + mockVerifyEvent(true); + + // run test + await eventSync.processSyncQueue(); + + // check active syncs in memory + const activeSyncs = eventSync.getActiveSyncMap(); + expect(activeSyncs.size).toEqual(1); + expect(activeSyncs.get(eventId)).toEqual({ + timestamp: timestamp, + responses: responses, + }); + expect(eventSync.getEventQueue().length).toEqual(0); + }); + + /** + * @target EventSynchronization.processSyncQueue should skip event when event + * is not in the database + * @dependencies + * - Date + * - database + * - EventVerifier + * - MinimumFee + * @scenario + * - mock event + * - insert event into queue + * - mock EventVerifier + * - mock `isEventConfirmedEnough` + * - mock `verifyEvent` + * - run test + * - check active syncs in memory + * @expected + * - active sync should remain empty + * - memory queue should be empty + */ + it('should skip event when event is not in the database', async () => { + // mock event + const mockedEvent = EventTestData.mockEventTrigger().event; + const eventId = EventSerializer.getId(mockedEvent); + + // insert event into queue + const eventSync = new TestEventSynchronization(); + eventSync.addEventToQueue(eventId); + + // mock EventVerifier + mockIsEventConfirmedEnough(true); + mockVerifyEvent(true); + + // run test + await eventSync.processSyncQueue(); + + // check active syncs in memory + const activeSyncs = eventSync.getActiveSyncMap(); + expect(activeSyncs.size).toEqual(0); + expect(eventSync.getEventQueue().length).toEqual(0); + }); + + /** + * @target EventSynchronization.processSyncQueue should skip event when event + * is not confirmed enough + * @dependencies + * - Date + * - database + * - EventVerifier + * - MinimumFee + * @scenario + * - mock event + * - insert mocked event into db + * - insert event into queue + * - mock EventVerifier + * - mock `isEventConfirmedEnough` to return false + * - mock `verifyEvent` + * - run test + * - check active syncs in memory + * @expected + * - active sync should remain empty + * - memory queue should be empty + */ + it('should skip event when event is not confirmed enough', async () => { + // mock event + const mockedEvent = EventTestData.mockEventTrigger().event; + const eventId = EventSerializer.getId(mockedEvent); + + // insert mocked event into db + await DatabaseActionMock.insertEventRecord( + mockedEvent, + EventStatus.pendingPayment + ); + + // insert event into queue + const eventSync = new TestEventSynchronization(); + eventSync.addEventToQueue(eventId); + + // mock EventVerifier + mockIsEventConfirmedEnough(false); + mockVerifyEvent(true); + + // run test + await eventSync.processSyncQueue(); + + // check active syncs in memory + const activeSyncs = eventSync.getActiveSyncMap(); + expect(activeSyncs.size).toEqual(0); + expect(eventSync.getEventQueue().length).toEqual(0); + }); + + /** + * @target EventSynchronization.processSyncQueue should set event as rejected when event + * is not verified + * @dependencies + * - Date + * - database + * - EventVerifier + * - MinimumFee + * @scenario + * - mock event + * - insert mocked event into db + * - insert event into queue + * - mock EventVerifier + * - mock `isEventConfirmedEnough` + * - mock `verifyEvent` to return false + * - run test + * - check active syncs in memory + * @expected + * - active sync should remain empty + * - memory queue should be empty + * - event status should be updated in db + */ + it('should set event as rejected when event is not verified', async () => { + // mock event + const mockedEvent = EventTestData.mockEventTrigger().event; + const eventId = EventSerializer.getId(mockedEvent); + + // insert mocked event into db + await DatabaseActionMock.insertEventRecord( + mockedEvent, + EventStatus.pendingPayment + ); + + // insert event into queue + const eventSync = new TestEventSynchronization(); + eventSync.addEventToQueue(eventId); + + // mock EventVerifier + mockIsEventConfirmedEnough(true); + mockVerifyEvent(false); + + // run test + await eventSync.processSyncQueue(); + + // check active syncs in memory + const activeSyncs = eventSync.getActiveSyncMap(); + expect(activeSyncs.size).toEqual(0); + expect(eventSync.getEventQueue().length).toEqual(0); + + // event status should be updated in db + const dbEvents = (await DatabaseActionMock.allEventRecords()).map( + (event) => [event.id, event.status] + ); + expect(dbEvents.length).toEqual(1); + expect(dbEvents).to.deep.contain([ + EventSerializer.getId(mockedEvent), + EventStatus.rejected, + ]); + }); + }); + + describe('sendSyncBatch', () => { + const guardsLen = GuardPkHandler.getInstance().guardsLen; + const publicKeys = GuardPkHandler.getInstance().publicKeys; + + beforeAll(() => { + vi.useFakeTimers(); + vi.setSystemTime(new Date(TestConfigs.currentTimeStamp)); + }); + + afterAll(() => { + vi.useRealTimers(); + }); + + /** + * @target EventSynchronization.sendSyncBatch should send sync request to random + * guards for each events + * @dependencies + * - Date + * - GuardDetection + * @scenario + * - mock two events + * - insert events into active sync + * - mock EventSynchronization.sendMessage + * - mock detection.activeGuards + * - run test + * - check if function got called + * @expected + * - `sendMessage` should got called with expected arguments + */ + it('should send sync request to random guards for each events', async () => { + // mock two events + const mockedEvent1 = EventTestData.mockEventTrigger().event; + const eventId1 = EventSerializer.getId(mockedEvent1); + const mockedEvent2 = EventTestData.mockEventTrigger().event; + const eventId2 = EventSerializer.getId(mockedEvent2); + + // insert events into active sync + const eventSync = new TestEventSynchronization(); + const timestamp = TestConfigs.currentTimeStamp / 1000 - 100; + eventSync.insertEventIntoActiveSync(eventId1, { + timestamp: timestamp, + responses: Array(guardsLen).fill(undefined), + }); + eventSync.insertEventIntoActiveSync(eventId2, { + timestamp: timestamp, + responses: Array(guardsLen).fill(undefined), + }); + + // mock EventSynchronization.sendMessage + const mockedSendMessage = vi.fn(); + const sendMessageSpy = vi.spyOn(eventSync as any, 'sendMessage'); + sendMessageSpy.mockImplementation(mockedSendMessage); + + // mock detection.activeGuards + vi.spyOn((eventSync as any).detection, 'activeGuards').mockResolvedValue( + publicKeys.map((pk, index) => ({ + publicKey: pk, + peerId: `peer-${index}`, + })) + ); + + // run test + await eventSync.sendSyncBatch(); + + // `sendMessage` should got called with expected arguments + expect(mockedSendMessage).toHaveBeenCalledWith( + SynchronizationMessageTypes.request, + { eventId: eventId1 }, + expect.any(Array), + TestConfigs.currentTimeStamp / 1000 + ); + expect(mockedSendMessage).toHaveBeenCalledWith( + SynchronizationMessageTypes.request, + { eventId: eventId2 }, + expect.any(Array), + TestConfigs.currentTimeStamp / 1000 + ); + }); + + /** + * @target EventSynchronization.sendSyncBatch should send sync request only to + * the guards that didn't response yet + * @dependencies + * - Date + * - GuardDetection + * @scenario + * - mock event + * - insert event into active sync + * - mock EventSynchronization.sendMessage + * - mock detection.activeGuards + * - run test + * - check if function got called + * @expected + * - `sendMessage` should got called with expected arguments + */ + it("should send sync request only to the guards that didn't response yet", async () => { + // mock event + const mockedEvent = EventTestData.mockEventTrigger().event; + const eventId = EventSerializer.getId(mockedEvent); + + // insert event into active sync + const eventSync = new TestEventSynchronization(); + const responses = [ + ...Array(guardsLen - 2).fill(mockPaymentTransaction()), + undefined, + undefined, + ]; + eventSync.insertEventIntoActiveSync(eventId, { + timestamp: TestConfigs.currentTimeStamp / 1000 - 100, + responses: responses, + }); + + // mock EventSynchronization.sendMessage + const mockedSendMessage = vi.fn(); + const sendMessageSpy = vi.spyOn(eventSync as any, 'sendMessage'); + sendMessageSpy.mockImplementation(mockedSendMessage); + + // mock detection.activeGuards + vi.spyOn((eventSync as any).detection, 'activeGuards').mockResolvedValue( + publicKeys.map((pk, index) => ({ + publicKey: pk, + peerId: `peer-${index}`, + })) + ); + + // run test + await eventSync.sendSyncBatch(); + + // `sendMessage` should got called with expected arguments + expect(mockedSendMessage).toHaveBeenCalledWith( + SynchronizationMessageTypes.request, + { eventId: eventId }, + expect.arrayContaining([ + `peer-${guardsLen - 1}`, + `peer-${guardsLen - 2}`, + ]), + TestConfigs.currentTimeStamp / 1000 + ); + expect((mockedSendMessage.mock.lastCall as any[])[2].length).toEqual(2); + }); + + /** + * @target EventSynchronization.sendSyncBatch should not send any request when + * selected guards are not active + * @dependencies + * - Date + * - GuardDetection + * @scenario + * - mock event + * - insert events into active sync + * - mock EventSynchronization.sendMessage + * - mock detection.activeGuards + * - run test + * - check if function got called + * @expected + * - `sendMessage` should NOT got called + */ + it('should not send any request when selected guards are not active', async () => { + // mock event + const mockedEvent = EventTestData.mockEventTrigger().event; + const eventId = EventSerializer.getId(mockedEvent); + + // insert events into active sync + const eventSync = new TestEventSynchronization(); + eventSync.insertEventIntoActiveSync(eventId, { + timestamp: TestConfigs.currentTimeStamp / 1000 - 100, + responses: Array(guardsLen).fill(undefined), + }); + + // mock EventSynchronization.sendMessage + const mockedSendMessage = vi.fn(); + const sendMessageSpy = vi.spyOn(eventSync as any, 'sendMessage'); + sendMessageSpy.mockImplementation(mockedSendMessage); + + // mock detection.activeGuards + vi.spyOn((eventSync as any).detection, 'activeGuards').mockResolvedValue( + [] + ); + + // run test + await eventSync.sendSyncBatch(); + + // `sendMessage` should NOT got called + expect(mockedSendMessage).not.toHaveBeenCalled(); + }); + }); + + describe('processSyncRequest', () => { + beforeAll(() => { + vi.useFakeTimers(); + vi.setSystemTime(new Date(TestConfigs.currentTimeStamp)); + }); + + afterAll(() => { + vi.useRealTimers(); + }); + + beforeEach(async () => { + await DatabaseActionMock.clearTables(); + }); + + /** + * @target EventSynchronization.processSyncRequest should send sync response when + * event has a completed tx in payment type + * @dependencies + * - database + * - Date + * @scenario + * - mock event and transaction and insert into db + * - mock EventSynchronization.sendMessage + * - run test + * - check if function got called + * @expected + * - `sendMessage` should got called with expected arguments + */ + it('should send sync response when event has a completed tx in payment type', async () => { + // mock event and transaction and insert into db + const mockedEvent = EventTestData.mockEventTrigger().event; + const eventId = EventSerializer.getId(mockedEvent); + const tx = mockPaymentTransaction( + TransactionType.payment, + mockedEvent.toChain, + eventId + ); + await DatabaseActionMock.insertEventRecord( + mockedEvent, + EventStatus.pendingReward + ); + await DatabaseActionMock.insertTxRecord(tx, TransactionStatus.completed); + + // mock EventSynchronization.sendMessage + const eventSync = new TestEventSynchronization(); + const mockedSendMessage = vi.fn(); + const sendMessageSpy = vi.spyOn(eventSync as any, 'sendMessage'); + sendMessageSpy.mockImplementation(mockedSendMessage); + + // run test + await eventSync.processMessage( + SynchronizationMessageTypes.request, + { eventId: eventId }, + 'signature', + 0, + 'peer-0', + TestConfigs.currentTimeStamp / 1000 + ); + + // `sendMessage` should got called with expected arguments + expect(mockedSendMessage).toHaveBeenCalledWith( + SynchronizationMessageTypes.response, + { txJson: tx.toJson() }, + expect.any(Array), + TestConfigs.currentTimeStamp / 1000 + ); + }); + + /** + * @target EventSynchronization.processSyncRequest should do nothing when event is not found + * @dependencies + * - database + * - Date + * @scenario + * - mock EventSynchronization.sendMessage + * - run test + * - check if function got called + * @expected + * - `sendMessage` should NOT got called + */ + it('should do nothing when event is not found', async () => { + // mock EventSynchronization.sendMessage + const eventSync = new TestEventSynchronization(); + const mockedSendMessage = vi.fn(); + const sendMessageSpy = vi.spyOn(eventSync as any, 'sendMessage'); + sendMessageSpy.mockImplementation(mockedSendMessage); + + // run test + await eventSync.processMessage( + SynchronizationMessageTypes.request, + { eventId: 'event-id' }, + 'signature', + 0, + 'peer-0', + TestConfigs.currentTimeStamp / 1000 + ); + + // `sendMessage` should NOT got called + expect(mockedSendMessage).not.toHaveBeenCalledWith(); + }); + + /** + * @target EventSynchronization.processSyncRequest should do nothing when event has no transaction + * @dependencies + * - database + * - Date + * @scenario + * - mock event insert into db + * - mock EventSynchronization.sendMessage + * - run test + * - check if function got called + * @expected + * - `sendMessage` should NOT got called + */ + it('should do nothing when event has no transaction', async () => { + // mock event insert into db + const mockedEvent = EventTestData.mockEventTrigger().event; + const eventId = EventSerializer.getId(mockedEvent); + await DatabaseActionMock.insertEventRecord( + mockedEvent, + EventStatus.pendingReward + ); + + // mock EventSynchronization.sendMessage + const eventSync = new TestEventSynchronization(); + const mockedSendMessage = vi.fn(); + const sendMessageSpy = vi.spyOn(eventSync as any, 'sendMessage'); + sendMessageSpy.mockImplementation(mockedSendMessage); + + // run test + await eventSync.processMessage( + SynchronizationMessageTypes.request, + { eventId: eventId }, + 'signature', + 0, + 'peer-0', + TestConfigs.currentTimeStamp / 1000 + ); + + // `sendMessage` should NOT got called + expect(mockedSendMessage).not.toHaveBeenCalledWith(); + }); + + /** + * @target EventSynchronization.processSyncRequest should do nothing when tx is not completed + * @dependencies + * - database + * - Date + * @scenario + * - mock event and transaction and insert into db + * - mock EventSynchronization.sendMessage + * - run test + * - check if function got called + * @expected + * - `sendMessage` should NOT got called + */ + it('should do nothing when tx is not completed', async () => { + // mock event and transaction and insert into db + const mockedEvent = EventTestData.mockEventTrigger().event; + const eventId = EventSerializer.getId(mockedEvent); + const tx = mockPaymentTransaction( + TransactionType.payment, + mockedEvent.toChain, + eventId + ); + await DatabaseActionMock.insertEventRecord( + mockedEvent, + EventStatus.pendingReward + ); + await DatabaseActionMock.insertTxRecord(tx, TransactionStatus.sent); + + // mock EventSynchronization.sendMessage + const eventSync = new TestEventSynchronization(); + const mockedSendMessage = vi.fn(); + const sendMessageSpy = vi.spyOn(eventSync as any, 'sendMessage'); + sendMessageSpy.mockImplementation(mockedSendMessage); + + // run test + await eventSync.processMessage( + SynchronizationMessageTypes.request, + { eventId: eventId }, + 'signature', + 0, + 'peer-0', + TestConfigs.currentTimeStamp / 1000 + ); + + // `sendMessage` should NOT got called + expect(mockedSendMessage).not.toHaveBeenCalledWith(); + }); + }); + + describe('processSyncResponse', () => { + const guardsLen = GuardPkHandler.getInstance().guardsLen; + const requiredApproval = GuardPkHandler.getInstance().requiredSign - 1; + + beforeEach(async () => { + await DatabaseActionMock.clearTables(); + }); + + /** + * @target EventSynchronization.processSyncResponse should set tx as approved when + * enough guards responded a transaction + * @dependencies + * - database + * @scenario + * - mock event and transaction and insert into db + * - insert event into active sync + * - mock EventSynchronization + * - mock `verifySynchronizationResponse` + * - mock `setTxAsApproved` + * - run test + * - check if function got called + * @expected + * - `setTxAsApproved` should got called + */ + it('should set tx as approved when enough guards responded a transaction', async () => { + // mock event and transaction and insert into db + const mockedEvent = EventTestData.mockEventTrigger().event; + const eventId = EventSerializer.getId(mockedEvent); + const tx = mockPaymentTransaction( + TransactionType.payment, + mockedEvent.toChain, + eventId + ); + await DatabaseActionMock.insertEventRecord( + mockedEvent, + EventStatus.pendingPayment + ); + + // insert event into active sync + const eventSync = new TestEventSynchronization(); + const responses = [ + undefined, + ...Array(requiredApproval - 1).fill(tx), + ...Array(guardsLen - requiredApproval).fill(undefined), + ]; + eventSync.insertEventIntoActiveSync(eventId, { + timestamp: TestConfigs.currentTimeStamp / 1000 - 100, + responses: responses, + }); + + // mock EventSynchronization + vi.spyOn( + eventSync as any, + 'verifySynchronizationResponse' + ).mockResolvedValue(true); + const mockedSetTxAsApproved = vi.fn(); + const setTxAsApprovedSpy = vi.spyOn(eventSync as any, 'setTxAsApproved'); + setTxAsApprovedSpy.mockImplementation(mockedSetTxAsApproved); + + // run test + await eventSync.processMessage( + SynchronizationMessageTypes.response, + { txJson: tx.toJson() }, + 'signature', + 0, + 'peer-0', + TestConfigs.currentTimeStamp / 1000 + ); + + // `setTxAsApproved` should got called + expect(mockedSetTxAsApproved).toHaveBeenCalled(); + }); + + /** + * @target EventSynchronization.processSyncResponse should ignore duplicate response + * @dependencies + * - database + * @scenario + * - mock event and transaction and insert into db + * - insert event into active sync + * - mock EventSynchronization + * - mock `verifySynchronizationResponse` + * - mock `setTxAsApproved` + * - run test + * - check if function got called + * @expected + * - `setTxAsApproved` should NOT got called + */ + it('should ignore duplicate response', async () => { + // mock event and transaction and insert into db + const mockedEvent = EventTestData.mockEventTrigger().event; + const eventId = EventSerializer.getId(mockedEvent); + const tx = mockPaymentTransaction( + TransactionType.payment, + mockedEvent.toChain, + eventId + ); + await DatabaseActionMock.insertEventRecord( + mockedEvent, + EventStatus.pendingPayment + ); + + // insert event into active sync + const eventSync = new TestEventSynchronization(); + const responses = [ + ...Array(requiredApproval - 1).fill(tx), + ...Array(guardsLen - requiredApproval + 1).fill(undefined), + ]; + eventSync.insertEventIntoActiveSync(eventId, { + timestamp: TestConfigs.currentTimeStamp / 1000 - 100, + responses: responses, + }); + + // mock EventSynchronization + vi.spyOn( + eventSync as any, + 'verifySynchronizationResponse' + ).mockResolvedValue(true); + const mockedSetTxAsApproved = vi.fn(); + const setTxAsApprovedSpy = vi.spyOn(eventSync as any, 'setTxAsApproved'); + setTxAsApprovedSpy.mockImplementation(mockedSetTxAsApproved); + + // run test + await eventSync.processMessage( + SynchronizationMessageTypes.response, + { txJson: tx.toJson() }, + 'signature', + 0, + 'peer-0', + TestConfigs.currentTimeStamp / 1000 + ); + + // `setTxAsApproved` should NOT got called + expect(mockedSetTxAsApproved).not.toHaveBeenCalled(); + }); + + /** + * @target EventSynchronization.processSyncResponse should do nothing when enough + * guards didn't response with the same transaction + * @dependencies + * - database + * @scenario + * - mock event and transaction and insert into db + * - insert event into active sync + * - mock EventSynchronization + * - mock `verifySynchronizationResponse` + * - mock `setTxAsApproved` + * - run test + * - check if function got called + * - check active syncs in memory + * @expected + * - `setTxAsApproved` should NOT got called + * - response should be added to active sync + */ + it("should do nothing when enough guards didn't response with the same transaction", async () => { + // mock event and transaction and insert into db + const mockedEvent = EventTestData.mockEventTrigger().event; + const eventId = EventSerializer.getId(mockedEvent); + const tx = mockPaymentTransaction( + TransactionType.payment, + mockedEvent.toChain, + eventId + ); + const anotherTx = mockPaymentTransaction( + TransactionType.payment, + mockedEvent.toChain, + eventId + ); + await DatabaseActionMock.insertEventRecord( + mockedEvent, + EventStatus.pendingPayment + ); + + // insert event into active sync + const eventSync = new TestEventSynchronization(); + const responses = [ + undefined, + ...Array(requiredApproval - 2).fill(tx), + ...Array(requiredApproval - 2).fill(anotherTx), + ...Array(guardsLen - 2 * requiredApproval + 3).fill(undefined), + ]; + eventSync.insertEventIntoActiveSync(eventId, { + timestamp: TestConfigs.currentTimeStamp / 1000 - 100, + responses: responses, + }); + + // mock EventSynchronization + vi.spyOn( + eventSync as any, + 'verifySynchronizationResponse' + ).mockResolvedValue(true); + const mockedSetTxAsApproved = vi.fn(); + const setTxAsApprovedSpy = vi.spyOn(eventSync as any, 'setTxAsApproved'); + setTxAsApprovedSpy.mockImplementation(mockedSetTxAsApproved); + + // run test + await eventSync.processMessage( + SynchronizationMessageTypes.response, + { txJson: tx.toJson() }, + 'signature', + 0, + 'peer-0', + TestConfigs.currentTimeStamp / 1000 + ); + + // `setTxAsApproved` should NOT got called + expect(mockedSetTxAsApproved).not.toHaveBeenCalled(); + + // response should be added to active sync + const activeSync = eventSync.getActiveSyncMap(); + expect(activeSync.get(eventId)?.responses.map((_) => _?.txId)).toEqual( + [tx, ...responses.slice(1)].map((_) => _?.txId) + ); + }); + }); + + describe(`verifySynchronizationResponse`, () => { + const guardsLen = GuardPkHandler.getInstance().guardsLen; + + beforeAll(() => { + mockGetEventFeeConfig({ + bridgeFee: 0n, + networkFee: 0n, + rsnRatio: 0n, + feeRatio: 100n, + rsnRatioDivisor: 1000000000000n, + feeRatioDivisor: 10000n, + }); + }); + + beforeEach(async () => { + await DatabaseActionMock.clearTables(); + ChainHandlerMock.resetMock(); + }); + + /** + * @target EventSynchronization.verifySynchronizationResponse should return true + * when all conditions are met + * @dependencies + * - database + * - ChainHandler + * - MinimumFee + * - EventOrder + * @scenario + * - mock event and transaction and insert into db + * - insert event into active sync + * - mock a PaymentOrder + * - mock ChainHandler `getChain` + * - mock `verifyPaymentTransaction` + * - mock `extractTransactionOrder` + * - mock `getTxConfirmationStatus` + * - mock `verifyTransactionExtraConditions` + * - mock EventOrder.createEventPaymentOrder to return mocked order + * - run test + * - check returned value + * @expected + * - returned value should be true + */ + it('should return true when all conditions are met', async () => { + // mock event and transaction and insert into db + const mockedEvent = EventTestData.mockEventTrigger().event; + const eventId = EventSerializer.getId(mockedEvent); + const tx = mockPaymentTransaction( + TransactionType.payment, + mockedEvent.toChain, + eventId + ); + await DatabaseActionMock.insertEventRecord( + mockedEvent, + EventStatus.pendingPayment + ); + + // insert event into active sync + const eventSync = new TestEventSynchronization(); + const responses = Array(guardsLen).fill(undefined); + eventSync.insertEventIntoActiveSync(eventId, { + timestamp: TestConfigs.currentTimeStamp / 1000 - 100, + responses: responses, + }); + + // mock a PaymentOrder + const mockedOrder: PaymentOrder = [ + { + address: 'address', + assets: { + nativeToken: 10n, + tokens: [], + }, + }, + ]; + + // mock ChainHandler + ChainHandlerMock.mockChainName(mockedEvent.toChain); + // mock `verifyPaymentTransaction` + ChainHandlerMock.mockChainFunction( + mockedEvent.toChain, + 'verifyPaymentTransaction', + true, + true + ); + // mock `extractTransactionOrder` + ChainHandlerMock.mockChainFunction( + mockedEvent.toChain, + 'extractTransactionOrder', + mockedOrder, + false + ); + // mock `getTxConfirmationStatus` + ChainHandlerMock.mockChainFunction( + mockedEvent.toChain, + 'getTxConfirmationStatus', + ConfirmationStatus.ConfirmedEnough, + false + ); + // mock `verifyTransactionExtraConditions` + ChainHandlerMock.mockChainFunction( + mockedEvent.toChain, + 'verifyTransactionExtraConditions', + true, + false + ); + + // mock EventOrder.createEventPaymentOrder to return mocked order + mockCreateEventPaymentOrder(mockedOrder); + + // run test + const result = await eventSync.callVerifySynchronizationResponse(tx); + + // check returned value + expect(result).toEqual(true); + }); + + /** + * @target EventSynchronization.verifySynchronizationResponse should return false + * when event has no active sync + * @dependencies + * - database + * - ChainHandler + * - MinimumFee + * - EventOrder + * @scenario + * - mock event and transaction and insert into db + * - mock a PaymentOrder + * - mock ChainHandler `getChain` + * - mock `verifyPaymentTransaction` + * - mock `extractTransactionOrder` + * - mock `getTxConfirmationStatus` + * - mock `verifyTransactionExtraConditions` + * - mock EventOrder.createEventPaymentOrder to return mocked order + * - run test + * - check returned value + * @expected + * - returned value should be false + */ + it('should return false when event has no active sync', async () => { + // mock event and transaction and insert into db + const mockedEvent = EventTestData.mockEventTrigger().event; + const eventId = EventSerializer.getId(mockedEvent); + const tx = mockPaymentTransaction( + TransactionType.payment, + mockedEvent.toChain, + eventId + ); + await DatabaseActionMock.insertEventRecord( + mockedEvent, + EventStatus.pendingPayment + ); + + // mock a PaymentOrder + const mockedOrder: PaymentOrder = [ + { + address: 'address', + assets: { + nativeToken: 10n, + tokens: [], + }, + }, + ]; + + // mock ChainHandler + ChainHandlerMock.mockChainName(mockedEvent.toChain); + // mock `verifyPaymentTransaction` + ChainHandlerMock.mockChainFunction( + mockedEvent.toChain, + 'verifyPaymentTransaction', + true, + true + ); + // mock `extractTransactionOrder` + ChainHandlerMock.mockChainFunction( + mockedEvent.toChain, + 'extractTransactionOrder', + mockedOrder, + false + ); + // mock `getTxConfirmationStatus` + ChainHandlerMock.mockChainFunction( + mockedEvent.toChain, + 'getTxConfirmationStatus', + ConfirmationStatus.ConfirmedEnough, + false + ); + // mock `verifyTransactionExtraConditions` + ChainHandlerMock.mockChainFunction( + mockedEvent.toChain, + 'verifyTransactionExtraConditions', + true, + false + ); + + // mock EventOrder.createEventPaymentOrder to return mocked order + mockCreateEventPaymentOrder(mockedOrder); + + // run test + const eventSync = new TestEventSynchronization(); + const result = await eventSync.callVerifySynchronizationResponse(tx); + + // check returned value + expect(result).toEqual(false); + }); + + /** + * @target EventSynchronization.verifySynchronizationResponse should return false + * when transaction type is not payment + * @dependencies + * - database + * - ChainHandler + * - MinimumFee + * - EventOrder + * @scenario + * - mock event and transaction and insert into db + * - insert event into active sync + * - mock a PaymentOrder + * - mock ChainHandler `getChain` + * - mock `verifyPaymentTransaction` + * - mock `extractTransactionOrder` + * - mock `getTxConfirmationStatus` + * - mock `verifyTransactionExtraConditions` + * - mock EventOrder.createEventPaymentOrder to return mocked order + * - run test + * - check returned value + * @expected + * - returned value should be false + */ + it('should return false when transaction type is not payment', async () => { + // mock event and transaction and insert into db + const mockedEvent = EventTestData.mockEventTrigger().event; + const eventId = EventSerializer.getId(mockedEvent); + const tx = mockPaymentTransaction( + TransactionType.manual, + mockedEvent.toChain, + eventId + ); + await DatabaseActionMock.insertEventRecord( + mockedEvent, + EventStatus.pendingPayment + ); + + // insert event into active sync + const eventSync = new TestEventSynchronization(); + const responses = Array(guardsLen).fill(undefined); + eventSync.insertEventIntoActiveSync(eventId, { + timestamp: TestConfigs.currentTimeStamp / 1000 - 100, + responses: responses, + }); + + // mock a PaymentOrder + const mockedOrder: PaymentOrder = [ + { + address: 'address', + assets: { + nativeToken: 10n, + tokens: [], + }, + }, + ]; + + // mock ChainHandler + ChainHandlerMock.mockChainName(mockedEvent.toChain); + // mock `verifyPaymentTransaction` + ChainHandlerMock.mockChainFunction( + mockedEvent.toChain, + 'verifyPaymentTransaction', + true, + true + ); + // mock `extractTransactionOrder` + ChainHandlerMock.mockChainFunction( + mockedEvent.toChain, + 'extractTransactionOrder', + mockedOrder, + false + ); + // mock `getTxConfirmationStatus` + ChainHandlerMock.mockChainFunction( + mockedEvent.toChain, + 'getTxConfirmationStatus', + ConfirmationStatus.ConfirmedEnough, + false + ); + // mock `verifyTransactionExtraConditions` + ChainHandlerMock.mockChainFunction( + mockedEvent.toChain, + 'verifyTransactionExtraConditions', + true, + false + ); + + // mock EventOrder.createEventPaymentOrder to return mocked order + mockCreateEventPaymentOrder(mockedOrder); + + // run test + const result = await eventSync.callVerifySynchronizationResponse(tx); + + // check returned value + expect(result).toEqual(false); + }); + + /** + * @target EventSynchronization.verifySynchronizationResponse should return false + * when transaction object is not consistent + * @dependencies + * - database + * - ChainHandler + * - MinimumFee + * - EventOrder + * @scenario + * - mock event and transaction and insert into db + * - insert event into active sync + * - mock a PaymentOrder + * - mock ChainHandler `getChain` + * - mock `verifyPaymentTransaction` to return false + * - mock `extractTransactionOrder` + * - mock `getTxConfirmationStatus` + * - mock `verifyTransactionExtraConditions` + * - mock EventOrder.createEventPaymentOrder to return mocked order + * - run test + * - check returned value + * @expected + * - returned value should be false + */ + it('should return false when transaction object is not consistent', async () => { + // mock event and transaction and insert into db + const mockedEvent = EventTestData.mockEventTrigger().event; + const eventId = EventSerializer.getId(mockedEvent); + const tx = mockPaymentTransaction( + TransactionType.payment, + mockedEvent.toChain, + eventId + ); + await DatabaseActionMock.insertEventRecord( + mockedEvent, + EventStatus.pendingPayment + ); + + // insert event into active sync + const eventSync = new TestEventSynchronization(); + const responses = Array(guardsLen).fill(undefined); + eventSync.insertEventIntoActiveSync(eventId, { + timestamp: TestConfigs.currentTimeStamp / 1000 - 100, + responses: responses, + }); + + // mock a PaymentOrder + const mockedOrder: PaymentOrder = [ + { + address: 'address', + assets: { + nativeToken: 10n, + tokens: [], + }, + }, + ]; + + // mock ChainHandler + ChainHandlerMock.mockChainName(mockedEvent.toChain); + // mock `verifyPaymentTransaction` + ChainHandlerMock.mockChainFunction( + mockedEvent.toChain, + 'verifyPaymentTransaction', + false, + true + ); + // mock `extractTransactionOrder` + ChainHandlerMock.mockChainFunction( + mockedEvent.toChain, + 'extractTransactionOrder', + mockedOrder, + false + ); + // mock `getTxConfirmationStatus` + ChainHandlerMock.mockChainFunction( + mockedEvent.toChain, + 'getTxConfirmationStatus', + ConfirmationStatus.ConfirmedEnough, + false + ); + // mock `verifyTransactionExtraConditions` + ChainHandlerMock.mockChainFunction( + mockedEvent.toChain, + 'verifyTransactionExtraConditions', + true, + false + ); + + // mock EventOrder.createEventPaymentOrder to return mocked order + mockCreateEventPaymentOrder(mockedOrder); + + // run test + const result = await eventSync.callVerifySynchronizationResponse(tx); + + // check returned value + expect(result).toEqual(false); + }); + + /** + * @target EventSynchronization.verifySynchronizationResponse should return false + * when transaction order is not verified + * @dependencies + * - database + * - ChainHandler + * - MinimumFee + * - EventOrder + * @scenario + * - mock event and transaction and insert into db + * - insert event into active sync + * - mock a PaymentOrder + * - mock ChainHandler `getChain` + * - mock `verifyPaymentTransaction` + * - mock `extractTransactionOrder` + * - mock `getTxConfirmationStatus` + * - mock `verifyTransactionExtraConditions` + * - mock EventOrder.createEventPaymentOrder to return different order + * - run test + * - check returned value + * @expected + * - returned value should be false + */ + it('should return false when transaction order is not verified', async () => { + // mock event and transaction and insert into db + const mockedEvent = EventTestData.mockEventTrigger().event; + const eventId = EventSerializer.getId(mockedEvent); + const tx = mockPaymentTransaction( + TransactionType.payment, + mockedEvent.toChain, + eventId + ); + await DatabaseActionMock.insertEventRecord( + mockedEvent, + EventStatus.pendingPayment + ); + + // insert event into active sync + const eventSync = new TestEventSynchronization(); + const responses = Array(guardsLen).fill(undefined); + eventSync.insertEventIntoActiveSync(eventId, { + timestamp: TestConfigs.currentTimeStamp / 1000 - 100, + responses: responses, + }); + + // mock a PaymentOrder + const mockedOrder: PaymentOrder = [ + { + address: 'address', + assets: { + nativeToken: 10n, + tokens: [], + }, + }, + ]; + + // mock ChainHandler + ChainHandlerMock.mockChainName(mockedEvent.toChain); + // mock `verifyPaymentTransaction` + ChainHandlerMock.mockChainFunction( + mockedEvent.toChain, + 'verifyPaymentTransaction', + true, + true + ); + // mock `extractTransactionOrder` + ChainHandlerMock.mockChainFunction( + mockedEvent.toChain, + 'extractTransactionOrder', + mockedOrder, + false + ); + // mock `getTxConfirmationStatus` + ChainHandlerMock.mockChainFunction( + mockedEvent.toChain, + 'getTxConfirmationStatus', + ConfirmationStatus.ConfirmedEnough, + false + ); + // mock `verifyTransactionExtraConditions` + ChainHandlerMock.mockChainFunction( + mockedEvent.toChain, + 'verifyTransactionExtraConditions', + true, + false + ); + + // mock EventOrder.createEventPaymentOrder to return mocked order + mockCreateEventPaymentOrder([ + { + address: 'different-address', + assets: { + nativeToken: 10n, + tokens: [], + }, + }, + ]); + + // run test + const result = await eventSync.callVerifySynchronizationResponse(tx); + + // check returned value + expect(result).toEqual(false); + }); + + /** + * @target EventSynchronization.verifySynchronizationResponse should return false + * when transaction is not confirmed enough + * @dependencies + * - database + * - ChainHandler + * - MinimumFee + * - EventOrder + * @scenario + * - mock event and transaction and insert into db + * - insert event into active sync + * - mock a PaymentOrder + * - mock ChainHandler `getChain` + * - mock `verifyPaymentTransaction` + * - mock `extractTransactionOrder` + * - mock `getTxConfirmationStatus` to return NotConfirmedEnough + * - mock `verifyTransactionExtraConditions` + * - mock EventOrder.createEventPaymentOrder to return mocked order + * - run test + * - check returned value + * @expected + * - returned value should be false + */ + it('should return false when transaction is not confirmed enough', async () => { + // mock event and transaction and insert into db + const mockedEvent = EventTestData.mockEventTrigger().event; + const eventId = EventSerializer.getId(mockedEvent); + const tx = mockPaymentTransaction( + TransactionType.payment, + mockedEvent.toChain, + eventId + ); + await DatabaseActionMock.insertEventRecord( + mockedEvent, + EventStatus.pendingPayment + ); + + // insert event into active sync + const eventSync = new TestEventSynchronization(); + const responses = Array(guardsLen).fill(undefined); + eventSync.insertEventIntoActiveSync(eventId, { + timestamp: TestConfigs.currentTimeStamp / 1000 - 100, + responses: responses, + }); + + // mock a PaymentOrder + const mockedOrder: PaymentOrder = [ + { + address: 'address', + assets: { + nativeToken: 10n, + tokens: [], + }, + }, + ]; + + // mock ChainHandler + ChainHandlerMock.mockChainName(mockedEvent.toChain); + // mock `verifyPaymentTransaction` + ChainHandlerMock.mockChainFunction( + mockedEvent.toChain, + 'verifyPaymentTransaction', + true, + true + ); + // mock `extractTransactionOrder` + ChainHandlerMock.mockChainFunction( + mockedEvent.toChain, + 'extractTransactionOrder', + mockedOrder, + false + ); + // mock `getTxConfirmationStatus` + ChainHandlerMock.mockChainFunction( + mockedEvent.toChain, + 'getTxConfirmationStatus', + ConfirmationStatus.NotConfirmedEnough, + false + ); + // mock `verifyTransactionExtraConditions` + ChainHandlerMock.mockChainFunction( + mockedEvent.toChain, + 'verifyTransactionExtraConditions', + true, + false + ); + + // mock EventOrder.createEventPaymentOrder to return mocked order + mockCreateEventPaymentOrder(mockedOrder); + + // run test + const result = await eventSync.callVerifySynchronizationResponse(tx); + + // check returned value + expect(result).toEqual(false); + }); + + /** + * @target EventSynchronization.verifySynchronizationResponse should return false + * when transaction is not found + * @dependencies + * - database + * - ChainHandler + * - MinimumFee + * - EventOrder + * @scenario + * - mock event and transaction and insert into db + * - insert event into active sync + * - mock a PaymentOrder + * - mock ChainHandler `getChain` + * - mock `verifyPaymentTransaction` + * - mock `extractTransactionOrder` + * - mock `getTxConfirmationStatus` to return NotFound + * - mock `verifyTransactionExtraConditions` + * - mock EventOrder.createEventPaymentOrder to return mocked order + * - run test + * - check returned value + * @expected + * - returned value should be false + */ + it('should return false when transaction is not found', async () => { + // mock event and transaction and insert into db + const mockedEvent = EventTestData.mockEventTrigger().event; + const eventId = EventSerializer.getId(mockedEvent); + const tx = mockPaymentTransaction( + TransactionType.payment, + mockedEvent.toChain, + eventId + ); + await DatabaseActionMock.insertEventRecord( + mockedEvent, + EventStatus.pendingPayment + ); + + // insert event into active sync + const eventSync = new TestEventSynchronization(); + const responses = Array(guardsLen).fill(undefined); + eventSync.insertEventIntoActiveSync(eventId, { + timestamp: TestConfigs.currentTimeStamp / 1000 - 100, + responses: responses, + }); + + // mock a PaymentOrder + const mockedOrder: PaymentOrder = [ + { + address: 'address', + assets: { + nativeToken: 10n, + tokens: [], + }, + }, + ]; + + // mock ChainHandler + ChainHandlerMock.mockChainName(mockedEvent.toChain); + // mock `verifyPaymentTransaction` + ChainHandlerMock.mockChainFunction( + mockedEvent.toChain, + 'verifyPaymentTransaction', + true, + true + ); + // mock `extractTransactionOrder` + ChainHandlerMock.mockChainFunction( + mockedEvent.toChain, + 'extractTransactionOrder', + mockedOrder, + false + ); + // mock `getTxConfirmationStatus` + ChainHandlerMock.mockChainFunction( + mockedEvent.toChain, + 'getTxConfirmationStatus', + ConfirmationStatus.NotFound, + false + ); + // mock `verifyTransactionExtraConditions` + ChainHandlerMock.mockChainFunction( + mockedEvent.toChain, + 'verifyTransactionExtraConditions', + true, + false + ); + + // mock EventOrder.createEventPaymentOrder to return mocked order + mockCreateEventPaymentOrder(mockedOrder); + + // run test + const result = await eventSync.callVerifySynchronizationResponse(tx); + + // check returned value + expect(result).toEqual(false); + }); + + /** + * @target EventSynchronization.verifySynchronizationResponse should return false + * when transaction extra conditions are not verified + * @dependencies + * - database + * - ChainHandler + * - MinimumFee + * - EventOrder + * @scenario + * - mock event and transaction and insert into db + * - insert event into active sync + * - mock a PaymentOrder + * - mock ChainHandler `getChain` + * - mock `verifyPaymentTransaction` + * - mock `extractTransactionOrder` + * - mock `getTxConfirmationStatus` + * - mock `verifyTransactionExtraConditions` to return false + * - mock EventOrder.createEventPaymentOrder to return mocked order + * - run test + * - check returned value + * @expected + * - returned value should be false + */ + it('should return false when transaction extra conditions are not verified', async () => { + // mock event and transaction and insert into db + const mockedEvent = EventTestData.mockEventTrigger().event; + const eventId = EventSerializer.getId(mockedEvent); + const tx = mockPaymentTransaction( + TransactionType.payment, + mockedEvent.toChain, + eventId + ); + await DatabaseActionMock.insertEventRecord( + mockedEvent, + EventStatus.pendingPayment + ); + + // insert event into active sync + const eventSync = new TestEventSynchronization(); + const responses = Array(guardsLen).fill(undefined); + eventSync.insertEventIntoActiveSync(eventId, { + timestamp: TestConfigs.currentTimeStamp / 1000 - 100, + responses: responses, + }); + + // mock a PaymentOrder + const mockedOrder: PaymentOrder = [ + { + address: 'address', + assets: { + nativeToken: 10n, + tokens: [], + }, + }, + ]; + + // mock ChainHandler + ChainHandlerMock.mockChainName(mockedEvent.toChain); + // mock `verifyPaymentTransaction` + ChainHandlerMock.mockChainFunction( + mockedEvent.toChain, + 'verifyPaymentTransaction', + true, + true + ); + // mock `extractTransactionOrder` + ChainHandlerMock.mockChainFunction( + mockedEvent.toChain, + 'extractTransactionOrder', + mockedOrder, + false + ); + // mock `getTxConfirmationStatus` + ChainHandlerMock.mockChainFunction( + mockedEvent.toChain, + 'getTxConfirmationStatus', + ConfirmationStatus.ConfirmedEnough, + false + ); + // mock `verifyTransactionExtraConditions` + ChainHandlerMock.mockChainFunction( + mockedEvent.toChain, + 'verifyTransactionExtraConditions', + false, + false + ); + + // mock EventOrder.createEventPaymentOrder to return mocked order + mockCreateEventPaymentOrder(mockedOrder); + + // run test + const result = await eventSync.callVerifySynchronizationResponse(tx); + + // check returned value + expect(result).toEqual(false); + }); + }); + + describe(`setTxAsApproved`, () => { + beforeEach(async () => { + await DatabaseActionMock.clearTables(); + }); + + /** + * @target EventSynchronization.setTxAsApproved should insert transaction + * into database and update event status + * @dependencies + * - database + * @scenario + * - mock event and transaction and insert into db + * - insert event into active sync + * - run test + * - check database + * - check active syncs in memory + * @expected + * - tx should be inserted into db + * - event status should be updated in db + * - event should be removed from active sync + */ + it('should insert transaction into database and update event status', async () => { + // mock event and transaction and insert into db + const mockedEvent = EventTestData.mockEventTrigger().event; + const eventId = EventSerializer.getId(mockedEvent); + const paymentTx = mockPaymentTransaction( + TransactionType.payment, + mockedEvent.toChain, + eventId + ); + await DatabaseActionMock.insertEventRecord( + mockedEvent, + EventStatus.pendingPayment + ); + + // insert event into active sync + const eventSync = new TestEventSynchronization(); + const responses = Array(GuardPkHandler.getInstance().guardsLen).fill( + undefined + ); + eventSync.insertEventIntoActiveSync(eventId, { + timestamp: TestConfigs.currentTimeStamp / 1000 - 100, + responses: responses, + }); + + // run test + await eventSync.callSetTxAsApproved(paymentTx); + + // tx should be inserted into db + const dbTxs = (await DatabaseActionMock.allTxRecords()).map((tx) => [ + tx.txId, + tx.txJson, + tx.event.id, + tx.status, + ]); + expect(dbTxs.length).toEqual(1); + expect(dbTxs).to.deep.contain([ + paymentTx.txId, + paymentTx.toJson(), + eventId, + TransactionStatus.completed, + ]); + + // event status should be updated in db + const dbEvents = (await DatabaseActionMock.allEventRecords()).map( + (event) => [event.id, event.status] + ); + expect(dbEvents.length).toEqual(1); + expect(dbEvents).to.deep.contain([eventId, EventStatus.pendingReward]); + + // event should be removed from active sync + expect(eventSync.getActiveSyncMap().size).toEqual(0); + }); + }); + + describe('timeoutActiveSyncs', () => { + const guardsLen = GuardPkHandler.getInstance().guardsLen; + + beforeAll(() => { + vi.useFakeTimers(); + vi.setSystemTime(new Date(TestConfigs.currentTimeStamp)); + }); + + afterAll(() => { + vi.useRealTimers(); + }); + + /** + * @target EventSynchronization.timeoutActiveSyncs should remove event from + * active sync when enough time is passed + * @dependencies + * - Date + * @scenario + * - mock two events + * - insert events into active sync + * - run test + * - check active syncs in memory + * @expected + * - one event should be removed from active sync + */ + it('should remove event from active sync when enough time is passed', async () => { + // mock two events + const mockedEvent1 = EventTestData.mockEventTrigger().event; + const eventId1 = EventSerializer.getId(mockedEvent1); + const mockedEvent2 = EventTestData.mockEventTrigger().event; + const eventId2 = EventSerializer.getId(mockedEvent2); + + // insert events into active sync + const eventSync = new TestEventSynchronization(); + eventSync.insertEventIntoActiveSync(eventId1, { + timestamp: + TestConfigs.currentTimeStamp / 1000 - Configs.eventSyncTimeout - 100, + responses: Array(guardsLen).fill(undefined), + }); + const event2ActiveSync = { + timestamp: + TestConfigs.currentTimeStamp / 1000 - Configs.eventSyncTimeout + 100, + responses: Array(guardsLen).fill(undefined), + }; + eventSync.insertEventIntoActiveSync(eventId2, event2ActiveSync); + + // run test + await eventSync.timeoutActiveSyncs(); + + // one event should be removed from active sync + const activeSyncMap = eventSync.getActiveSyncMap(); + expect(activeSyncMap.size).toEqual(1); + expect(activeSyncMap.get(eventId2)).toEqual(event2ActiveSync); + }); + }); +}); diff --git a/tests/synchronization/TestEventSynchronization.ts b/tests/synchronization/TestEventSynchronization.ts new file mode 100644 index 00000000..315b15aa --- /dev/null +++ b/tests/synchronization/TestEventSynchronization.ts @@ -0,0 +1,38 @@ +import { PaymentTransaction } from '@rosen-chains/abstract-chain'; +import GuardPkHandler from '../../src/handlers/GuardPkHandler'; +import EventSynchronization from '../../src/synchronization/EventSynchronization'; +import { ActiveSync } from '../../src/synchronization/Interfaces'; + +class TestEventSynchronization extends EventSynchronization { + constructor() { + super(GuardPkHandler.getInstance().publicKeys, { + activeGuards: vi.fn(), + } as any); + } + + getEventQueue = (): string[] => { + return this.eventQueue; + }; + + getActiveSyncMap = (): Map => { + return this.activeSyncMap; + }; + + insertEventIntoQueue = (value: string): void => { + this.eventQueue.push(value); + }; + + insertEventIntoActiveSync = ( + eventId: string, + activeSync: ActiveSync + ): void => { + this.activeSyncMap.set(eventId, activeSync); + }; + + callVerifySynchronizationResponse = (tx: PaymentTransaction) => + this.verifySynchronizationResponse(tx); + + callSetTxAsApproved = (tx: PaymentTransaction) => this.setTxAsApproved(tx); +} + +export default TestEventSynchronization;