├── .eslintignore
├── .eslintrc.json
├── .gitignore
├── .npmignore
├── .prettierrc
├── .travis.yml
├── .vscode
└── launch.json
├── LICENSE
├── README.md
├── index.js
├── package-lock.json
├── package.json
├── src
├── Actions
│ ├── Actions.ts
│ ├── Helpers.ts
│ ├── Ledger.ts
│ ├── Pending.ts
│ └── index.ts
├── Buckets
│ ├── BucketCalc.ts
│ ├── IBucketPair.ts
│ └── index.ts
├── Cache
│ ├── Cache.ts
│ └── index.ts
├── Config
│ ├── Config.ts
│ ├── IConfigParams.ts
│ └── index.ts
├── EconomicStrategy
│ ├── EconomicStrategyManager.ts
│ ├── IEconomicStrategy.ts
│ ├── NormalizedTimes.ts
│ ├── ProfitabilityCalculator.ts
│ └── index.ts
├── Enum
│ ├── AbortReason.ts
│ ├── EconomicStrategyStatus.ts
│ ├── FnSignatures.ts
│ ├── ReconnectMsg.ts
│ ├── TxSendStatus.ts
│ ├── TxStatus.ts
│ └── index.ts
├── Logger
│ ├── DefaultLogger.ts
│ ├── ILogger.ts
│ └── index.ts
├── Router
│ ├── Router.ts
│ └── index.ts
├── Scanner
│ ├── BaseScanner.ts
│ ├── BucketWatchCallback.ts
│ ├── BucketsManager.ts
│ ├── CacheScanner.ts
│ ├── ChainScanner.ts
│ ├── IBucketWatcher.ts
│ ├── TimeNodeScanner.ts
│ ├── WatchableBucket.ts
│ ├── WatchableBucketFactory.ts
│ └── index.ts
├── Stats
│ ├── StatsDB.ts
│ └── index.ts
├── TimeNode.ts
├── TxPool
│ ├── DirectTxPool.ts
│ ├── ITxPool.ts
│ ├── TxPool.ts
│ ├── TxPoolProcessor.ts
│ └── index.ts
├── Types
│ ├── ITransactionOptions.ts
│ ├── IntervalId.ts
│ ├── Operation.ts
│ └── index.ts
├── Version.ts
├── Wallet
│ ├── AccountState.ts
│ ├── IWalletReceipt.ts
│ ├── TransactionReceiptAwaiter.ts
│ ├── Wallet.ts
│ └── index.ts
├── WsReconnect
│ └── index.ts
├── global.d.ts
└── index.ts
├── test
├── e2e
│ ├── TestConfig.ts
│ ├── TestCreateWallet.ts
│ ├── TestScheduleTx.ts
│ └── TestTimeNode.ts
├── helpers
│ ├── Helpers.ts
│ ├── MockTxRequest.ts
│ ├── createWallet.ts
│ ├── index.ts
│ ├── mockConfig.ts
│ ├── network.ts
│ └── scheduleTestTx.ts
├── mocha.opts
└── unit
│ ├── UnitTestAccountState.ts
│ ├── UnitTestActions.ts
│ ├── UnitTestBucketCalc.ts
│ ├── UnitTestBuckets.ts
│ ├── UnitTestCache.ts
│ ├── UnitTestCacheScanner.ts
│ ├── UnitTestConfig.ts
│ ├── UnitTestEconomicStrategy.ts
│ ├── UnitTestLedger.ts
│ ├── UnitTestPending.ts
│ ├── UnitTestProfitabilityCalculator.ts
│ ├── UnitTestRouter.ts
│ ├── UnitTestScanner.ts
│ ├── UnitTestStats.ts
│ ├── UnitTestTimeNode.ts
│ ├── UnitTestTxPoolProcessor.ts
│ ├── UnitTestWallet.ts
│ ├── UnitTestWatchableBucket.ts
│ └── dependencies.ts
├── tsconfig.json
└── tslint.json
/.eslintignore:
--------------------------------------------------------------------------------
1 | built/*.js
2 | test/*.js
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "eslint:recommended"
4 | ],
5 | "parserOptions": {
6 | "sourceType": "module",
7 | "ecmaVersion": 8,
8 | "ecmaFeatures": {
9 | "jsx": true,
10 | "experimentalObjectRestSpread": true
11 | }
12 | },
13 | "env": {
14 | "node": true,
15 | "es6": true,
16 | "mocha": true
17 | },
18 | "rules": {
19 | "linebreak-style": 0,
20 | "object-curly-spacing": [
21 | "error",
22 | "always"
23 | ],
24 | "no-trailing-spaces": "error",
25 | "keyword-spacing": "error",
26 | "quotes": [
27 | "error",
28 | "single",
29 | {
30 | "allowTemplateLiterals": true
31 | }
32 | ]
33 | }
34 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | node_modules/*
3 | src/*.b
4 | yarn-error.log
5 | yarn.lock
6 | *.b
7 | Routing.ts
8 | old_tests
9 | old_tests/*
10 | built/
11 | built/*
12 | .cache
13 | dist/
14 | dist/*
15 | stats.db
16 |
17 | # Coverage directory
18 | coverage
19 | .nyc_output
20 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | *
2 | !built/src/**
3 | !built/package.json
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 100,
3 | "singleQuote": true,
4 | "useTabs": false,
5 | "semi": true,
6 | "tabWidth": 2,
7 | "trailingComma": "none"
8 | }
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | dist: trusty
2 | sudo: required
3 | group: beta
4 | language: node_js
5 | node_js:
6 | - "8"
7 | cache:
8 | directories:
9 | - node_modules
10 | before_install: npm install -g truffle@4.1.14
11 | install: npm install
12 |
13 | before_script:
14 | - npm run ganache > /dev/null &
15 | - sleep 5
16 | script:
17 | - npx eac-deploy-contracts
18 | - npm run build
19 | - npm run test:coverage
20 | - npm run test:e2e
21 |
22 | after_success:
23 | - npm run report-coverage
24 |
25 | matrix:
26 | fast_finish: true
27 | include:
28 | - before_install: true
29 | before_script: true
30 | script: npm run lint
31 | - env: RUN_ONLY_OPTIONAL_TESTS=false; PROVIDER_URL=ws://localhost:8545
32 | - env: RUN_ONLY_OPTIONAL_TESTS=true
33 | allow_failures:
34 | - env: RUN_ONLY_OPTIONAL_TESTS=true
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.2.0",
3 | "configurations": [
4 | {
5 | "type": "node",
6 | "request": "launch",
7 | "name": "Mocha All",
8 | "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha",
9 | "args": [
10 | "-r",
11 | "ts-node/register",
12 | "${workspaceFolder}/test/e2e/*.ts",
13 | "${workspaceFolder}/test/unit/*.ts",
14 | "--exit"
15 | ],
16 | "console": "integratedTerminal",
17 | "internalConsoleOptions": "neverOpen"
18 | },
19 | {
20 | "type": "node",
21 | "request": "launch",
22 | "name": "Mocha Unit Tests",
23 | "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha",
24 | "args": [
25 | "-r",
26 | "ts-node/register",
27 | "${workspaceFolder}/test/unit/*.ts",
28 | "--exit"
29 | ],
30 | "console": "integratedTerminal",
31 | "internalConsoleOptions": "neverOpen"
32 | },
33 | {
34 | "type": "node",
35 | "request": "launch",
36 | "name": "Mocha E2E",
37 | "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha",
38 | "args": [
39 | "-r",
40 | "ts-node/register",
41 | "${workspaceFolder}/test/e2e/*.ts",
42 | "--exit"
43 | ],
44 | "console": "integratedTerminal",
45 | "internalConsoleOptions": "neverOpen"
46 | },
47 | {
48 | "name": "Current TS Tests File",
49 | "type": "node",
50 | "request": "launch",
51 | "program": "${workspaceRoot}/node_modules/mocha/bin/_mocha",
52 | "args": ["-r", "ts-node/register", "${relativeFile}"],
53 | "cwd": "${workspaceRoot}",
54 | "protocol": "inspector"
55 | }
56 | ]
57 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Piper Merriam
4 | Copyright (c) 2017, 2018 Lsaether, ChronoLogic
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in all
14 | copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | SOFTWARE.
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [
](https://github.com/chronologic)
2 |
3 | [](https://badge.fury.io/js/%40ethereum-alarm-clock%2Ftimenode-core)
4 | [](https://travis-ci.org/ethereum-alarm-clock/timenode-core)
5 | [](https://greenkeeper.io/)
6 | [](https://coveralls.io/github/ethereum-alarm-clock/timenode-core?branch=master)
7 |
8 | # timenode-core
9 |
10 | This package contains all of the key logic necessary for the operation of an [Ethereum Alarm Clock](https://github.com/ethereum-alarm-clock/ethereum-alarm-clock) TimeNode.
11 |
12 | ## Contribute
13 |
14 | If you would like to hack on `timenode-core` or notice a bug, please open an issue or come find us on the Ethereum Alarm Clock Gitter channel and tell us. If you're feeling more ambitious and would like to contribute directly via a pull request, that's cool too. We will review all pull requests and issues opened on this repository. Even if you think something isn't working right or that it should work another way, we would really appreciate if you helped us by opening an issue!
15 |
16 | ## How to Build
17 |
18 | If you decide to contribute then you will be working on the TypeScript files in the `src/` directory. However, we don't export these files to the world, but we transpile them down to ES5 first. We do this by initiating the TypeScript compiler.
19 |
20 | But, you can use the scripts provided in the `package.json` file to help you build the files.
21 |
22 | ```
23 | npm run build
24 | ```
25 |
26 | It will produce an `index.js` file which can be imported into any project and used.
27 |
28 | ## Test
29 | ```
30 | npm run ganache
31 | npx eac-deploy-contracts
32 | npm run test
33 | ```
34 |
35 | ## How to Lint
36 |
37 | You can use one of the helper scripts to use [Prettier]() to lint for you. It will create backups of all the files that it changes before changing them, and knows how to handle both JavaScript and TypeScript sources.
38 |
39 | ```
40 | npm run fmt
41 | ```
42 |
43 | You can clean the backups files that are created like so:
44 |
45 | ```
46 | npm run clean-backups
47 | ```
48 |
49 | ## Want more?
50 |
51 | This package is a part of EAC.JS family ~
52 | * [lib](https://github.com/ethereum-alarm-clock/lib)
53 | * [timenode-core](https://github.com/ethereum-alarm-clock/timenode-core)
54 | * [cli](https://github.com/ethereum-alarm-clock/cli)
55 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | const { Config, TimeNode, StatsDB, Wallet, version } = require('./built/src');
2 |
3 | module.exports = {
4 | Config,
5 | TimeNode,
6 | StatsDB,
7 | Wallet,
8 | version
9 | };
10 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@ethereum-alarm-clock/timenode-core",
3 | "version": "7.0.0",
4 | "description": "Contains key logic for the Ethereum Alarm Clock TimeNode",
5 | "main": "index.js",
6 | "scripts": {
7 | "build": "tsc && cp package.json built/",
8 | "clean-backups": "find . -type f -name '*.b' -delete",
9 | "clean-built": "rm -rf built",
10 | "clean": "npm run clean-backups && npm run clean-built",
11 | "ganache": "ganache-cli -m \"shove afford modify census bridge good random error fault floor fringe oblige\" -i 1002 -b 1",
12 | "lint": "tslint --project .",
13 | "lint-fix": "tslint --fix --project .",
14 | "prepack": "npm run build",
15 | "test": "npm run test:e2e && npm run test:unit",
16 | "test:e2e": "mocha --timeout 50000 -r ts-node/register test/e2e/*.ts --exit",
17 | "test:unit": "mocha --timeout 50000 -r ts-node/register test/unit/*.ts --exit",
18 | "test:coverage": "nyc npm run test:unit",
19 | "report-coverage": "cat ./coverage/lcov.info | coveralls"
20 | },
21 | "lint-staged": {
22 | "*.{ts,tsx}": [
23 | "tslint --fix",
24 | "prettier --write --config ./.prettierrc --config-precedence file-override",
25 | "git add"
26 | ]
27 | },
28 | "repository": {
29 | "type": "git",
30 | "url": "git+https://github.com/ethereum-alarm-clock/timenode-core.git"
31 | },
32 | "keywords": [
33 | "ethereum",
34 | "smart-contracts",
35 | "ethereum-alarm-clock"
36 | ],
37 | "author": "lsaether",
38 | "license": "MIT",
39 | "bugs": {
40 | "url": "https://github.com/ethereum-alarm-clock/timenode-core/issues"
41 | },
42 | "homepage": "https://github.com/ethereum-alarm-clock/timenode-core#readme",
43 | "dependencies": {
44 | "@ethereum-alarm-clock/lib": "0.3.4",
45 | "bignumber.js": "8.0.2",
46 | "ethereum-common": "0.2.1",
47 | "ethereumjs-block": "2.2.0",
48 | "ethereumjs-devp2p": "2.5.1",
49 | "ethereumjs-tx": "1.3.7",
50 | "ethereumjs-wallet": "0.6.3",
51 | "lokijs": "1.5.6",
52 | "node-fetch": "2.3.0"
53 | },
54 | "devDependencies": {
55 | "@types/chai": "4.1.7",
56 | "@types/ethereumjs-tx": "1.0.1",
57 | "@types/lokijs": "1.5.2",
58 | "@types/node": "11.9.4",
59 | "@types/node-fetch": "2.1.6",
60 | "@types/web3": "1.0.18",
61 | "chai": "4.2.0",
62 | "coveralls": "3.0.2",
63 | "ganache-cli": "6.3.0",
64 | "husky": "1.3.1",
65 | "lint-staged": "8.1.4",
66 | "mocha": "5.2.0",
67 | "mocha-typescript": "1.1.17",
68 | "moment": "2.24.0",
69 | "nyc": "13.3.0",
70 | "prettier": "1.16.4",
71 | "source-map-support": "0.5.10",
72 | "ts-node": "8.0.2",
73 | "tslint": "5.12.1",
74 | "tslint-config-prettier": "1.18.0",
75 | "tslint-microsoft-contrib": "6.0.0",
76 | "tslint-sonarts": "1.9.0",
77 | "typemoq": "2.1.0",
78 | "typescript": "3.3.3",
79 | "web3": "1.0.0-beta.36",
80 | "websocket": "1.0.28"
81 | },
82 | "nyc": {
83 | "extension": [
84 | ".ts",
85 | ".tsx"
86 | ],
87 | "include": [
88 | "src/"
89 | ],
90 | "exclude": [
91 | "src/*.d.ts"
92 | ],
93 | "reporter": [
94 | "lcov"
95 | ],
96 | "all": true,
97 | "report-dir": "./coverage"
98 | },
99 | "husky": {
100 | "hooks": {
101 | "pre-commit": "lint-staged"
102 | }
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/src/Actions/Actions.ts:
--------------------------------------------------------------------------------
1 | import BigNumber from 'bignumber.js';
2 |
3 | import Cache, { ICachedTxDetails } from '../Cache';
4 | import { ILogger } from '../Logger';
5 | import { Address } from '../Types';
6 | import ITransactionOptions from '../Types/ITransactionOptions';
7 | import { IWalletReceipt, Wallet } from '../Wallet';
8 | import { getAbortedExecuteStatus, isAborted, isExecuted } from './Helpers';
9 | import { ILedger } from './Ledger';
10 | import { Pending } from './Pending';
11 | import { Operation } from '../Types/Operation';
12 | import { TxSendStatus } from '../Enum/TxSendStatus';
13 | import { Util, ITransactionRequest } from '@ethereum-alarm-clock/lib';
14 |
15 | export default interface IActions {
16 | claim(
17 | txRequest: ITransactionRequest,
18 | nextAccount: Address,
19 | gasPrice: BigNumber
20 | ): Promise;
21 | execute(txRequest: ITransactionRequest, gasPrice: BigNumber): Promise;
22 | }
23 |
24 | export default class Actions implements IActions {
25 | private logger: ILogger;
26 | private wallet: Wallet;
27 | private ledger: ILedger;
28 | private cache: Cache;
29 | private util: Util;
30 | private pending: Pending;
31 |
32 | constructor(
33 | wallet: Wallet,
34 | ledger: ILedger,
35 | logger: ILogger,
36 | cache: Cache,
37 | util: Util,
38 | pending: Pending
39 | ) {
40 | this.wallet = wallet;
41 | this.logger = logger;
42 | this.ledger = ledger;
43 | this.cache = cache;
44 | this.util = util;
45 | this.pending = pending;
46 | }
47 |
48 | public async claim(
49 | txRequest: ITransactionRequest,
50 | nextAccount: Address,
51 | gasPrice: BigNumber
52 | ): Promise {
53 | const context = TxSendStatus.claim;
54 | //TODO: merge wallet ifs into 1 getWalletStatus or something
55 | if (this.wallet.hasPendingTransaction(txRequest.address, Operation.CLAIM)) {
56 | return TxSendStatus.STATUS(TxSendStatus.PROGRESS, context);
57 | }
58 | if (!this.wallet.isAccountAbleToSendTx(nextAccount)) {
59 | return TxSendStatus.STATUS(TxSendStatus.BUSY, context);
60 | }
61 | if (await this.pending.hasPending(txRequest, { type: Operation.CLAIM, checkGasPrice: true })) {
62 | return TxSendStatus.STATUS(TxSendStatus.PENDING, context);
63 | }
64 |
65 | const opts = this.getClaimingOpts(txRequest, gasPrice);
66 | const { receipt, from, status } = await this.wallet.sendFromAccount(nextAccount, opts);
67 |
68 | this.ledger.accountClaiming(receipt, txRequest, opts, from);
69 |
70 | if (status === TxSendStatus.OK) {
71 | this.cache.get(txRequest.address).claimedBy = from;
72 | return TxSendStatus.STATUS(TxSendStatus.SUCCESS, context);
73 | } else if (status === TxSendStatus.UNKNOWN_ERROR) {
74 | this.logger.error(status);
75 | return TxSendStatus.STATUS(TxSendStatus.FAIL, context);
76 | } else {
77 | return TxSendStatus.STATUS(status, context);
78 | }
79 | }
80 |
81 | public async execute(txRequest: ITransactionRequest, gasPrice: BigNumber): Promise {
82 | const context = TxSendStatus.execute;
83 | if (this.wallet.hasPendingTransaction(txRequest.address, Operation.EXECUTE)) {
84 | return TxSendStatus.STATUS(TxSendStatus.PROGRESS, context);
85 | }
86 | if (!this.wallet.isNextAccountFree()) {
87 | return TxSendStatus.STATUS(TxSendStatus.BUSY, context);
88 | }
89 |
90 | const opts = this.getExecutionOpts(txRequest, gasPrice);
91 | const claimIndex = this.wallet.getAddresses().indexOf(txRequest.claimedBy);
92 | const wasClaimedByOurNode = claimIndex > -1;
93 | let executionResult: IWalletReceipt;
94 |
95 | if (wasClaimedByOurNode && txRequest.inReservedWindow()) {
96 | this.logger.debug(
97 | `Claimed by our node ${claimIndex} and inReservedWindow`,
98 | txRequest.address
99 | );
100 | executionResult = await this.wallet.sendFromIndex(claimIndex, opts);
101 | } else if (!(await this.hasPendingExecuteTransaction(txRequest))) {
102 | executionResult = await this.wallet.sendFromNext(opts);
103 | } else {
104 | return TxSendStatus.STATUS(TxSendStatus.PENDING, context);
105 | }
106 |
107 | const { receipt, from, status } = executionResult;
108 |
109 | if (status === TxSendStatus.OK) {
110 | await txRequest.refreshData();
111 | let executionStatus = TxSendStatus.STATUS(TxSendStatus.SUCCESS, context);
112 | const success = isExecuted(receipt);
113 |
114 | if (success) {
115 | this.cache.get(txRequest.address).wasCalled = true;
116 | } else if (isAborted(receipt)) {
117 | executionStatus = getAbortedExecuteStatus(receipt);
118 | } else {
119 | executionStatus = TxSendStatus.STATUS(TxSendStatus.FAIL, context);
120 | }
121 |
122 | this.ledger.accountExecution(txRequest, receipt, opts, from, success);
123 |
124 | return executionStatus;
125 | } else if (status === TxSendStatus.UNKNOWN_ERROR) {
126 | this.logger.error(status, txRequest.address);
127 | } else {
128 | return TxSendStatus.STATUS(status, context);
129 | }
130 |
131 | return TxSendStatus.STATUS(TxSendStatus.FAIL, context);
132 | }
133 |
134 | public async cleanup(): Promise {
135 | throw Error('Not implemented according to latest EAC changes.');
136 | }
137 |
138 | private async hasPendingExecuteTransaction(txRequest: ITransactionRequest): Promise {
139 | return this.pending.hasPending(txRequest, {
140 | type: Operation.EXECUTE,
141 | checkGasPrice: true,
142 | minPrice: txRequest.gasPrice
143 | });
144 | }
145 |
146 | private getClaimingOpts(
147 | txRequest: ITransactionRequest,
148 | gasPrice: BigNumber
149 | ): ITransactionOptions {
150 | return {
151 | to: txRequest.address,
152 | value: txRequest.requiredDeposit,
153 | gas: new BigNumber('120000'),
154 | gasPrice,
155 | data: txRequest.claimData,
156 | operation: Operation.CLAIM
157 | };
158 | }
159 |
160 | private getExecutionOpts(
161 | txRequest: ITransactionRequest,
162 | gasPrice: BigNumber
163 | ): ITransactionOptions {
164 | const gas = this.util.calculateGasAmount(txRequest);
165 |
166 | return {
167 | to: txRequest.address,
168 | value: new BigNumber(0),
169 | gas,
170 | gasPrice,
171 | data: txRequest.executeData,
172 | operation: Operation.EXECUTE
173 | };
174 | }
175 | }
176 |
--------------------------------------------------------------------------------
/src/Actions/Helpers.ts:
--------------------------------------------------------------------------------
1 | import { AbortReason, TxSendStatus } from '../Enum';
2 | import { TransactionReceipt } from 'web3/types';
3 |
4 | const EXECUTED_EVENT = '0x3e504bb8b225ad41f613b0c3c4205cdd752d1615b4d77cd1773417282fcfb5d9';
5 | const ABORTED_EVENT = '0xc008bc849b42227c61d5063a1313ce509a6e99211bfd59e827e417be6c65c81b';
6 | const CLAIMED_EVENT = '0xbcb472984264b16baa8cde752f2af002ea8ce06f35d81caee36625234edd2a46';
7 |
8 | const abortReasonToExecuteStatus = new Map([
9 | [AbortReason.WasCancelled, TxSendStatus.ABORTED_WAS_CANCELLED],
10 | [AbortReason.AlreadyCalled, TxSendStatus.ABORTED_ALREADY_CALLED],
11 | [AbortReason.BeforeCallWindow, TxSendStatus.ABORTED_BEFORE_CALL_WINDOW],
12 | [AbortReason.AfterCallWindow, TxSendStatus.ABORTED_AFTER_CALL_WINDOW],
13 | [AbortReason.ReservedForClaimer, TxSendStatus.ABORTED_RESERVED_FOR_CLAIMER],
14 | [AbortReason.InsufficientGas, TxSendStatus.ABORTED_INSUFFICIENT_GAS],
15 | [AbortReason.TooLowGasPrice, TxSendStatus.ABORTED_TOO_LOW_GAS_PRICE],
16 | [AbortReason.Unknown, TxSendStatus.ABORTED_UNKNOWN]
17 | ]);
18 |
19 | function isExecuted(receipt: TransactionReceipt): boolean {
20 | return Boolean(receipt) && receipt.logs[0].topics.indexOf(EXECUTED_EVENT) > -1;
21 | }
22 |
23 | function isAborted(receipt: TransactionReceipt): boolean {
24 | return Boolean(receipt) && receipt.logs[0].topics.indexOf(ABORTED_EVENT) > -1;
25 | }
26 |
27 | const getAbortedExecuteStatus = (receipt: TransactionReceipt) => {
28 | const reason = parseInt(receipt.logs[0].data, 16);
29 | const abortReason = receipt && !isNaN(reason) ? (reason as AbortReason) : AbortReason.Unknown;
30 |
31 | return abortReasonToExecuteStatus.get(abortReason) || TxSendStatus.ABORTED_UNKNOWN;
32 | };
33 |
34 | const isTransactionStatusSuccessful = (status: string | number | boolean) => {
35 | return [true, 1, '0x1', '0x01'].indexOf(status) !== -1;
36 | };
37 |
38 | export {
39 | isExecuted,
40 | isAborted,
41 | getAbortedExecuteStatus,
42 | isTransactionStatusSuccessful,
43 | EXECUTED_EVENT,
44 | ABORTED_EVENT,
45 | CLAIMED_EVENT
46 | };
47 |
--------------------------------------------------------------------------------
/src/Actions/Ledger.ts:
--------------------------------------------------------------------------------
1 | import BigNumber from 'bignumber.js';
2 |
3 | import { IStatsDB } from '../Stats/StatsDB';
4 | import ITransactionOptions from '../Types/ITransactionOptions';
5 | import { isTransactionStatusSuccessful } from './Helpers';
6 | import { TransactionReceipt } from 'web3/types';
7 | import { ITransactionRequest } from '@ethereum-alarm-clock/lib';
8 |
9 | export interface ILedger {
10 | accountClaiming(
11 | receipt: TransactionReceipt,
12 | txRequest: ITransactionRequest,
13 | opts: any,
14 | from: string
15 | ): boolean;
16 | accountExecution(
17 | txRequest: ITransactionRequest,
18 | receipt: TransactionReceipt,
19 | opts: ITransactionOptions,
20 | from: string,
21 | success: boolean
22 | ): boolean;
23 | }
24 |
25 | export class Ledger implements ILedger {
26 | private statsDB: IStatsDB;
27 |
28 | constructor(statsDB: IStatsDB) {
29 | this.statsDB = statsDB;
30 | }
31 |
32 | public accountClaiming(
33 | receipt: TransactionReceipt,
34 | txRequest: ITransactionRequest,
35 | opts: ITransactionOptions,
36 | from: string
37 | ): boolean {
38 | if (!receipt) {
39 | return false;
40 | }
41 |
42 | const gasUsed = new BigNumber(receipt.gasUsed);
43 | const gasPrice = new BigNumber(opts.gasPrice);
44 | const success = isTransactionStatusSuccessful(receipt.status);
45 | let txCost = gasUsed.multipliedBy(gasPrice);
46 | if (success) {
47 | txCost = txCost.plus(txRequest.requiredDeposit);
48 | }
49 |
50 | this.statsDB.claimed(from, txRequest.address, txCost, success);
51 |
52 | return true;
53 | }
54 |
55 | public accountExecution(
56 | txRequest: ITransactionRequest,
57 | receipt: TransactionReceipt,
58 | opts: ITransactionOptions,
59 | from: string,
60 | success: boolean
61 | ): boolean {
62 | let bounty = new BigNumber(0);
63 | let cost = new BigNumber(0);
64 |
65 | const gasUsed = new BigNumber(receipt.gasUsed);
66 | const actualGasPrice = opts.gasPrice;
67 |
68 | if (success) {
69 | const data = receipt.logs[0].data;
70 | bounty = new BigNumber(data.slice(0, 66)).minus(gasUsed.multipliedBy(actualGasPrice));
71 | } else {
72 | cost = gasUsed.multipliedBy(actualGasPrice);
73 | }
74 |
75 | this.statsDB.executed(from, txRequest.address, cost, bounty, success);
76 |
77 | return true;
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/src/Actions/Pending.ts:
--------------------------------------------------------------------------------
1 | import BigNumber from 'bignumber.js';
2 | import { Operation } from '../Types/Operation';
3 | import { ITransactionRequestPending, GasPriceUtil } from '@ethereum-alarm-clock/lib';
4 | import { ITxPool, ITxPoolTxDetails } from '../TxPool/ITxPool';
5 |
6 | interface PendingOpts {
7 | type: Operation;
8 | checkGasPrice: boolean;
9 | minPrice?: BigNumber;
10 | }
11 |
12 | const NETWORK_GAS_PRICE_RATIO = 0.3;
13 |
14 | export class Pending {
15 | private gasPriceUtil: GasPriceUtil;
16 | private txPool: ITxPool;
17 |
18 | constructor(gasPriceUtil: GasPriceUtil, txPool: ITxPool) {
19 | this.gasPriceUtil = gasPriceUtil;
20 | this.txPool = txPool;
21 | }
22 |
23 | /**
24 | *
25 | *
26 | * @param {ITransactionRequestPending} txRequest Transaction Request object to check.
27 | * @param {PendingOpts} opts Options for pending check
28 | * @returns {Promise} True if a pending transaction to this address exists.
29 | * @memberof Pending
30 | */
31 | public async hasPending(
32 | txRequest: ITransactionRequestPending,
33 | opts: PendingOpts
34 | ): Promise {
35 | return this.txPool.running() ? this.hasPendingPool(txRequest, opts) : false;
36 | }
37 |
38 | private async hasPendingPool(
39 | txRequest: ITransactionRequestPending,
40 | opts: PendingOpts
41 | ): Promise {
42 | const currentGasPrice = await this.gasPriceUtil.networkGasPrice();
43 | return Array.from(this.txPool.pool.values()).some(poolTx => {
44 | const hasCorrectAddress = poolTx.to === txRequest.address;
45 | const withValidGasPrice =
46 | !opts.checkGasPrice || this.hasValidGasPrice(currentGasPrice, poolTx, opts.minPrice);
47 | const hasCorrectOperation = poolTx.operation === opts.type;
48 |
49 | return hasCorrectAddress && withValidGasPrice && hasCorrectOperation;
50 | });
51 | }
52 |
53 | private hasValidGasPrice(
54 | networkPrice: BigNumber,
55 | transaction: ITxPoolTxDetails,
56 | minPrice?: BigNumber
57 | ) {
58 | const hasMinPrice: boolean = !minPrice || minPrice.lte(transaction.gasPrice);
59 | return (
60 | hasMinPrice &&
61 | networkPrice &&
62 | networkPrice.times(NETWORK_GAS_PRICE_RATIO).lte(transaction.gasPrice.valueOf())
63 | );
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/Actions/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './Actions';
2 |
--------------------------------------------------------------------------------
/src/Buckets/BucketCalc.ts:
--------------------------------------------------------------------------------
1 | import { Bucket } from './IBucketPair';
2 | import { Block } from 'web3/eth/types';
3 | import { Util, Constants, RequestFactory } from '@ethereum-alarm-clock/lib';
4 |
5 | export interface IBucketCalc {
6 | getBuckets(): Promise;
7 | }
8 |
9 | export class BucketCalc {
10 | private requestFactory: Promise;
11 | private util: Util;
12 |
13 | constructor(util: Util, requestFactory: Promise) {
14 | this.util = util;
15 | this.requestFactory = requestFactory;
16 | }
17 |
18 | public async getBuckets(): Promise {
19 | const latest: Block = await this.util.getBlock('latest');
20 |
21 | const currentBuckets = await this.getCurrentBuckets(latest);
22 | const nextBuckets = await this.getNextBuckets(latest);
23 | const afterNextBuckets = await this.getAfterNextBuckets(latest);
24 |
25 | return currentBuckets.concat(nextBuckets).concat(afterNextBuckets);
26 | }
27 |
28 | private async getCurrentBuckets(latest: Block): Promise {
29 | return [
30 | (await this.requestFactory).calcBucket(latest.number, 1),
31 | (await this.requestFactory).calcBucket(latest.timestamp, 2)
32 | ];
33 | }
34 |
35 | private async getNextBuckets(latest: Block): Promise {
36 | const nextBlockInterval = latest.number + Constants.BUCKET_SIZE.block;
37 | const nextTsInterval = latest.timestamp + Constants.BUCKET_SIZE.timestamp;
38 |
39 | return [
40 | (await this.requestFactory).calcBucket(nextBlockInterval, 1),
41 | (await this.requestFactory).calcBucket(nextTsInterval, 2)
42 | ];
43 | }
44 |
45 | private async getAfterNextBuckets(latest: Block): Promise {
46 | const nextBlockInterval = latest.number + 2 * Constants.BUCKET_SIZE.block;
47 | const nextTsInterval = latest.timestamp + 2 * Constants.BUCKET_SIZE.timestamp;
48 |
49 | return [
50 | (await this.requestFactory).calcBucket(nextBlockInterval, 1),
51 | (await this.requestFactory).calcBucket(nextTsInterval, 2)
52 | ];
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/Buckets/IBucketPair.ts:
--------------------------------------------------------------------------------
1 | type Bucket = number;
2 |
3 | interface IBucketPair {
4 | blockBucket: Bucket;
5 | timestampBucket: Bucket;
6 | }
7 |
8 | export { Bucket, IBucketPair };
9 |
--------------------------------------------------------------------------------
/src/Buckets/index.ts:
--------------------------------------------------------------------------------
1 | export { Bucket, IBucketPair } from './IBucketPair';
2 | export { IBucketCalc, BucketCalc } from './BucketCalc';
3 |
--------------------------------------------------------------------------------
/src/Cache/Cache.ts:
--------------------------------------------------------------------------------
1 | import { ILogger, DefaultLogger } from '../Logger';
2 | import BigNumber from 'bignumber.js';
3 | import { TxStatus } from '../Enum';
4 |
5 | export interface ICachedTxDetails {
6 | bounty: BigNumber;
7 | temporalUnit: number;
8 | claimedBy: string;
9 | wasCalled: boolean;
10 | windowStart: BigNumber;
11 | claimWindowStart: BigNumber;
12 | status: TxStatus;
13 | }
14 |
15 | export default class Cache {
16 | public cache: {} = {};
17 | public logger: ILogger;
18 |
19 | constructor(logger: ILogger = new DefaultLogger()) {
20 | this.logger = logger;
21 | }
22 |
23 | public set(key: string, value: T) {
24 | this.cache[key] = value;
25 | }
26 |
27 | public get(key: string, fallback?: any): T {
28 | const value = this.cache[key];
29 | if (value === undefined) {
30 | if (fallback === undefined) {
31 | throw new Error('attempted to access key entry that does not exist: ' + key);
32 | }
33 |
34 | return fallback;
35 | }
36 |
37 | return value;
38 | }
39 |
40 | public has(key: string) {
41 | if (this.cache[key] === undefined) {
42 | return false;
43 | }
44 | return true;
45 | }
46 |
47 | public del(key: string) {
48 | delete this.cache[key];
49 | }
50 |
51 | public length(): number {
52 | return this.stored().length;
53 | }
54 |
55 | public stored() {
56 | return Object.keys(this.cache) || [];
57 | }
58 |
59 | public isEmpty() {
60 | return this.length() === 0;
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/Cache/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './Cache';
2 | export { ICachedTxDetails } from './Cache';
3 |
--------------------------------------------------------------------------------
/src/Config/Config.ts:
--------------------------------------------------------------------------------
1 | // tslint:disable-next-line:no-reference
2 | ///
3 |
4 | import { EAC, Util, GasPriceUtil } from '@ethereum-alarm-clock/lib';
5 | import Cache from '../Cache';
6 | import { Wallet } from '../Wallet';
7 | import { IConfigParams } from './IConfigParams';
8 | import { IEconomicStrategy, EconomicStrategyManager } from '../EconomicStrategy';
9 | import { ILogger, DefaultLogger } from '../Logger';
10 | import { StatsDB } from '../Stats';
11 | import { ICachedTxDetails } from '../Cache/Cache';
12 | import BigNumber from 'bignumber.js';
13 | import { IEconomicStrategyManager } from '../EconomicStrategy/EconomicStrategyManager';
14 | import { Ledger } from '../Actions/Ledger';
15 | import { Pending } from '../Actions/Pending';
16 | import { AccountState } from '../Wallet/AccountState';
17 | import { TxPool, DirectTxPool, ITxPool } from '../TxPool';
18 | import Web3 = require('web3');
19 |
20 | export default class Config implements IConfigParams {
21 | public static readonly DEFAULT_ECONOMIC_STRATEGY: IEconomicStrategy = {
22 | maxDeposit: new BigNumber(1000000000000000000),
23 | minBalance: new BigNumber(0),
24 | minProfitability: new BigNumber(0),
25 | maxGasSubsidy: 100,
26 | minClaimWindow: 30,
27 | minClaimWindowBlock: 2,
28 | minExecutionWindow: 150,
29 | minExecutionWindowBlock: 10,
30 | usingSmartGasEstimation: false
31 | };
32 |
33 | public activeProviderUrl: string;
34 | public autostart: boolean;
35 | public cache: Cache;
36 | public claiming: boolean;
37 | public eac: EAC;
38 | public economicStrategy?: IEconomicStrategy;
39 | public economicStrategyManager: IEconomicStrategyManager;
40 | public gasPriceUtil: GasPriceUtil;
41 | public ledger: Ledger;
42 | public logger?: ILogger;
43 | public maxRetries?: number;
44 | public ms: number;
45 | public pending: Pending;
46 | public providerUrls: string[];
47 | public scanSpread: any;
48 | public statsDb: StatsDB;
49 | public statsDbLoaded: Promise;
50 | public txPool: ITxPool;
51 | public util: Util;
52 | public wallet: Wallet;
53 | public web3: Web3;
54 | public walletStoresAsPrivateKeys: boolean;
55 | public directTxPool: boolean;
56 |
57 | // tslint:disable-next-line:cognitive-complexity
58 | constructor(params: IConfigParams) {
59 | if (!params.providerUrls.length) {
60 | throw new Error('Must pass at least 1 providerUrl to the config object.');
61 | }
62 |
63 | this.web3 = Util.getWeb3FromProviderUrl(params.providerUrls[0]);
64 | this.activeProviderUrl = params.providerUrls[0];
65 | this.util = new Util(this.web3);
66 | this.gasPriceUtil = new GasPriceUtil(this.web3);
67 | this.eac = new EAC(this.web3);
68 | this.providerUrls = params.providerUrls;
69 |
70 | this.economicStrategy = params.economicStrategy || Config.DEFAULT_ECONOMIC_STRATEGY;
71 |
72 | this.autostart = params.autostart !== undefined ? params.autostart : true;
73 | this.directTxPool = params.directTxPool || false;
74 | this.claiming = params.claiming || false;
75 | this.maxRetries = params.maxRetries || 30;
76 | this.ms = params.ms || 4000;
77 | this.scanSpread = params.scanSpread || 50;
78 | this.walletStoresAsPrivateKeys = params.walletStoresAsPrivateKeys || false;
79 | this.logger = params.logger || new DefaultLogger();
80 | this.txPool = this.directTxPool
81 | ? new DirectTxPool(this.web3, this.logger)
82 | : new TxPool(this.web3, this.util, this.logger);
83 | this.cache = new Cache(this.logger);
84 | this.economicStrategyManager = new EconomicStrategyManager(
85 | this.economicStrategy,
86 | this.gasPriceUtil,
87 | this.cache,
88 | this.eac,
89 | this.util,
90 | this.logger
91 | );
92 | this.pending = new Pending(this.gasPriceUtil, this.txPool);
93 |
94 | if (params.walletStores && params.walletStores.length && params.walletStores.length > 0) {
95 | this.wallet = new Wallet(this.util, new AccountState(), this.logger);
96 |
97 | params.walletStores = params.walletStores.map((store: object | string) => {
98 | if (typeof store === 'object') {
99 | return JSON.stringify(store);
100 | }
101 |
102 | return store;
103 | });
104 |
105 | if (this.walletStoresAsPrivateKeys) {
106 | this.wallet.loadPrivateKeys(params.walletStores);
107 | } else {
108 | if (params.password) {
109 | this.wallet.decrypt(params.walletStores, params.password);
110 | } else {
111 | throw new Error(
112 | 'Unable to unlock the wallet. Please provide a password as a config param'
113 | );
114 | }
115 | }
116 | } else {
117 | this.wallet = null;
118 | }
119 |
120 | this.statsDb = params.statsDb ? new StatsDB(params.statsDb) : null;
121 | if (this.statsDb) {
122 | this.statsDbLoaded = this.statsDb.init();
123 | }
124 |
125 | this.ledger = new Ledger(this.statsDb);
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/src/Config/IConfigParams.ts:
--------------------------------------------------------------------------------
1 | import { IEconomicStrategy } from '../EconomicStrategy';
2 | import { ILogger } from '../Logger';
3 |
4 | export interface IConfigParams {
5 | autostart?: boolean;
6 | claiming?: boolean;
7 | economicStrategy?: IEconomicStrategy;
8 | logger?: ILogger | null;
9 | maxRetries?: number;
10 | ms?: any;
11 | password?: any;
12 | providerUrls: string[];
13 | scanSpread?: number | null;
14 | statsDb?: any;
15 | walletStores?: any;
16 | walletStoresAsPrivateKeys?: boolean;
17 | directTxPool?: boolean;
18 | }
19 |
--------------------------------------------------------------------------------
/src/Config/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './Config';
2 |
--------------------------------------------------------------------------------
/src/EconomicStrategy/EconomicStrategyManager.ts:
--------------------------------------------------------------------------------
1 | import { BigNumber } from 'bignumber.js';
2 | import Cache, { ICachedTxDetails } from '../Cache';
3 | import { EconomicStrategyStatus } from '../Enum';
4 | import { ILogger, DefaultLogger } from '../Logger';
5 | import { Address } from '../Types';
6 | import { IEconomicStrategy } from './IEconomicStrategy';
7 | import { NormalizedTimes } from './NormalizedTimes';
8 | import { EAC, Util, GasPriceUtil, ITransactionRequest } from '@ethereum-alarm-clock/lib';
9 | import { ProfitabilityCalculator } from './ProfitabilityCalculator';
10 |
11 | export interface IEconomicStrategyManager {
12 | strategy: IEconomicStrategy;
13 |
14 | shouldClaimTx(
15 | txRequest: ITransactionRequest,
16 | nextAccount: Address,
17 | gasPrice: BigNumber
18 | ): Promise;
19 | shouldExecuteTx(txRequest: ITransactionRequest, gasPrice: BigNumber): Promise;
20 | getExecutionGasPrice(txRequest: ITransactionRequest): Promise;
21 | }
22 |
23 | export class EconomicStrategyManager {
24 | public strategy: IEconomicStrategy;
25 |
26 | private gasPriceUtil: GasPriceUtil;
27 | private util: Util;
28 | private logger: ILogger;
29 | private cache: Cache;
30 | private eac: EAC;
31 | private profitabilityCalculator: ProfitabilityCalculator;
32 |
33 | constructor(
34 | strategy: IEconomicStrategy,
35 | gasPriceUtil: GasPriceUtil,
36 | cache: Cache,
37 | eac: EAC,
38 | util: Util,
39 | logger: ILogger = new DefaultLogger()
40 | ) {
41 | this.strategy = strategy;
42 | this.gasPriceUtil = gasPriceUtil;
43 | this.util = util;
44 | this.logger = logger;
45 | this.cache = cache;
46 | this.eac = eac;
47 | this.profitabilityCalculator = new ProfitabilityCalculator(util, gasPriceUtil, logger);
48 |
49 | if (!this.strategy) {
50 | throw new Error('Unable to initialize EconomicStrategyManager, strategy is null');
51 | }
52 |
53 | this.logger.debug(`EconomicStrategyManager initialized with ${JSON.stringify(strategy)}`);
54 | }
55 |
56 | /**
57 | * Tests transaction if claiming should be performed
58 | *
59 | * @param {ITransactionRequest} txRequest Request under test
60 | * @param {Address} nextAccount Account
61 | * @returns {Promise} Status
62 | * @memberof EconomicStrategyManager
63 | */
64 | public async shouldClaimTx(
65 | txRequest: ITransactionRequest,
66 | nextAccount: Address,
67 | gasPrice: BigNumber
68 | ): Promise {
69 | const profitable = await this.isClaimingProfitable(txRequest, gasPrice);
70 | if (!profitable) {
71 | return EconomicStrategyStatus.NOT_PROFITABLE;
72 | }
73 |
74 | const enoughBalance = await this.isAboveMinBalanceLimit(nextAccount, txRequest);
75 | if (!enoughBalance) {
76 | return EconomicStrategyStatus.INSUFFICIENT_BALANCE;
77 | }
78 |
79 | const exceedsDepositLimit = this.exceedsMaxDeposit(txRequest);
80 | if (exceedsDepositLimit) {
81 | return EconomicStrategyStatus.DEPOSIT_TOO_HIGH;
82 | }
83 |
84 | const tooShortReserved = this.tooShortReserved(txRequest);
85 | if (tooShortReserved) {
86 | return EconomicStrategyStatus.TOO_SHORT_RESERVED;
87 | }
88 |
89 | const tooShortClaimWindow = await this.tooShortClaimWindow(txRequest);
90 | if (tooShortClaimWindow) {
91 | return EconomicStrategyStatus.TOO_SHORT_CLAIM_WINDOW;
92 | }
93 |
94 | return EconomicStrategyStatus.CLAIM;
95 | }
96 |
97 | public async getExecutionGasPrice(txRequest: ITransactionRequest): Promise {
98 | const { average } = await this.gasPriceUtil.getAdvancedNetworkGasPrice();
99 | const currentNetworkPrice = this.strategy.usingSmartGasEstimation
100 | ? (await this.smartGasEstimation(txRequest)) || average
101 | : average;
102 |
103 | const minGasPrice = txRequest.gasPrice;
104 |
105 | return currentNetworkPrice.isGreaterThan(minGasPrice) ? currentNetworkPrice : minGasPrice;
106 | }
107 |
108 | public async shouldExecuteTx(
109 | txRequest: ITransactionRequest,
110 | targetGasPrice: BigNumber
111 | ): Promise {
112 | const expectedProfit = await this.profitabilityCalculator.executionProfitability(
113 | txRequest,
114 | targetGasPrice
115 | );
116 | const shouldExecute = expectedProfit.isGreaterThanOrEqualTo(0);
117 |
118 | this.logger.debug(
119 | `shouldExecuteTx: expectedProfit=${expectedProfit} >= 0 returns ${shouldExecute}`,
120 | txRequest.address
121 | );
122 |
123 | return shouldExecute;
124 | }
125 |
126 | private async tooShortClaimWindow(txRequest: ITransactionRequest): Promise {
127 | const { minClaimWindowBlock, minClaimWindow } = this.strategy;
128 | const { claimWindowEnd, temporalUnit } = txRequest;
129 | const now = await txRequest.now();
130 |
131 | const minWindow = temporalUnit === 1 ? minClaimWindowBlock : minClaimWindow;
132 |
133 | return claimWindowEnd.minus(now).lt(minWindow);
134 | }
135 |
136 | private tooShortReserved(txRequest: ITransactionRequest): boolean {
137 | const { minExecutionWindowBlock, minExecutionWindow } = this.strategy;
138 | const { reservedWindowSize, temporalUnit } = txRequest;
139 |
140 | const minWindow = temporalUnit === 1 ? minExecutionWindowBlock : minExecutionWindow;
141 |
142 | return minWindow && reservedWindowSize.lt(minWindow);
143 | }
144 |
145 | private exceedsMaxDeposit(txRequest: ITransactionRequest): boolean {
146 | const requiredDeposit = txRequest.requiredDeposit;
147 | const maxDeposit = this.strategy.maxDeposit;
148 |
149 | return requiredDeposit.gt(maxDeposit);
150 | }
151 |
152 | private getTxRequestsClaimedBy(address: string): string[] {
153 | return this.cache.stored().filter((txAddress: string) => {
154 | const tx = this.cache.get(txAddress);
155 | return tx.claimedBy === address && !tx.wasCalled;
156 | });
157 | }
158 |
159 | private async isAboveMinBalanceLimit(
160 | nextAccount: Address,
161 | txRequest: ITransactionRequest
162 | ): Promise {
163 | const minBalance = this.strategy.minBalance;
164 | const currentBalance: BigNumber = await this.util.balanceOf(nextAccount);
165 | const txRequestsClaimed: string[] = this.getTxRequestsClaimedBy(nextAccount);
166 | const gasPrices: BigNumber[] = await Promise.all(
167 | txRequestsClaimed.map(async (address: string) => {
168 | const tx = this.eac.transactionRequest(address);
169 | await tx.refreshData();
170 |
171 | return tx.gasPrice;
172 | })
173 | );
174 |
175 | let costOfExecutingFutureTransactions = new BigNumber(0);
176 |
177 | if (gasPrices.length) {
178 | const subsidyFactor = this.maxSubsidyFactor;
179 | costOfExecutingFutureTransactions = gasPrices.reduce((sum: BigNumber, current: BigNumber) =>
180 | sum.plus(current.times(subsidyFactor))
181 | );
182 | }
183 |
184 | const requiredBalance = minBalance.plus(costOfExecutingFutureTransactions);
185 | const isAboveMinBalanceLimit = currentBalance.gt(requiredBalance);
186 |
187 | this.logger.debug(
188 | `isAboveMinBalanceLimit: currentBalance=${currentBalance} > minBalance=${minBalance} + costOfExecutingFutureTransactions=${costOfExecutingFutureTransactions} returns ${isAboveMinBalanceLimit}`,
189 | txRequest.address
190 | );
191 |
192 | return isAboveMinBalanceLimit;
193 | }
194 |
195 | private async isClaimingProfitable(
196 | txRequest: ITransactionRequest,
197 | claimingGasPrice: BigNumber
198 | ): Promise {
199 | const expectedProfit = await this.profitabilityCalculator.claimingProfitability(
200 | txRequest,
201 | claimingGasPrice
202 | );
203 | const minProfitability = this.strategy.minProfitability;
204 | const isProfitable = expectedProfit.isGreaterThanOrEqualTo(minProfitability);
205 |
206 | this.logger.debug(
207 | `isClaimingProfitable: claimingGasPrice=${claimingGasPrice} expectedProfit=${expectedProfit} >= minProfitability=${minProfitability} returns ${isProfitable}`,
208 | txRequest.address
209 | );
210 |
211 | return isProfitable;
212 | }
213 |
214 | private get maxSubsidyFactor(): number {
215 | const maxGasSubsidy = this.strategy.maxGasSubsidy / 100;
216 | return maxGasSubsidy + 1;
217 | }
218 |
219 | private async smartGasEstimation(txRequest: ITransactionRequest): Promise {
220 | const gasStats = await GasPriceUtil.getEthGasStationStats();
221 | if (!gasStats) {
222 | return null;
223 | }
224 |
225 | const { temporalUnit } = txRequest;
226 | const now = await txRequest.now();
227 | const inReservedWindow = await txRequest.inReservedWindow();
228 |
229 | const timeLeft = inReservedWindow
230 | ? txRequest.reservedWindowEnd.plus(now)
231 | : txRequest.executionWindowEnd.plus(now);
232 | const normalizedTimeLeft =
233 | temporalUnit === 1 ? timeLeft.multipliedBy(gasStats.blockTime) : timeLeft;
234 |
235 | const gasEstimation = new NormalizedTimes(gasStats, temporalUnit).pickGasPrice(
236 | normalizedTimeLeft
237 | );
238 |
239 | this.logger.debug(
240 | `smartGasEstimation: inReservedWindow=${inReservedWindow} timeLeft=${timeLeft} normalizedTimeLeft=${normalizedTimeLeft} returns ${gasEstimation}`,
241 | txRequest.address
242 | );
243 |
244 | return gasEstimation;
245 | }
246 | }
247 |
--------------------------------------------------------------------------------
/src/EconomicStrategy/IEconomicStrategy.ts:
--------------------------------------------------------------------------------
1 | import BigNumber from 'bignumber.js';
2 |
3 | export interface IEconomicStrategy {
4 | /**
5 | * Maximum deposit a TimeNode would be willing
6 | * to stake while claiming a transaction.
7 | */
8 | maxDeposit?: BigNumber;
9 |
10 | /**
11 | * Minimum balance a TimeNode has to
12 | * have in order to claim a transaction.
13 | */
14 | minBalance?: BigNumber;
15 |
16 | /**
17 | * Minimum profitability a scheduled transactions
18 | * has to bring in order for the TimeNode to claim it.
19 | */
20 | minProfitability?: BigNumber;
21 |
22 | /**
23 | * A number which defines the percentage with which
24 | * the TimeNode would be able to subsidize the amount of gas
25 | * it sends at the time of the execution.
26 | *
27 | * e.g. If the scheduled transaction has set the gas price to 20 gwei
28 | * and `maxGasSubsidy` is set to 50, the TimeNode would be willing
29 | * to subsidize gas costs to up to 30 gwei.
30 | */
31 | maxGasSubsidy?: number;
32 |
33 | minExecutionWindow?: number;
34 |
35 | minExecutionWindowBlock?: number;
36 |
37 | minClaimWindow?: number;
38 |
39 | minClaimWindowBlock?: number;
40 |
41 | /**
42 | * Smart gas estimation will use the Eth Gas Station API to
43 | * retrieve information about the speed of gas prices and pick
44 | * the gas price which better fits execution situations.
45 | */
46 | usingSmartGasEstimation?: boolean;
47 | }
48 |
--------------------------------------------------------------------------------
/src/EconomicStrategy/NormalizedTimes.ts:
--------------------------------------------------------------------------------
1 | import BigNumber from 'bignumber.js';
2 | import { EthGasStationInfo } from '@ethereum-alarm-clock/lib';
3 |
4 | export class NormalizedTimes {
5 | private gasStats: EthGasStationInfo;
6 | private temporalUnit: number;
7 |
8 | constructor(gasStats: EthGasStationInfo, temporalUnit: number) {
9 | this.gasStats = gasStats;
10 | this.temporalUnit = temporalUnit;
11 | }
12 |
13 | public pickGasPrice(timeLeft: BigNumber): BigNumber {
14 | if (timeLeft > this.safeLow) {
15 | return this.gasStats.safeLow;
16 | } else if (timeLeft > this.avg) {
17 | return this.gasStats.average;
18 | } else if (timeLeft > this.fast) {
19 | return this.gasStats.fast;
20 | } else if (timeLeft > this.fastest) {
21 | return this.gasStats.fastest;
22 | } else {
23 | return null;
24 | }
25 | }
26 |
27 | private get safeLow(): BigNumber {
28 | return this.normalize(this.gasStats.safeLow);
29 | }
30 |
31 | private get avg(): BigNumber {
32 | return this.normalize(this.gasStats.average);
33 | }
34 |
35 | private get fast(): BigNumber {
36 | return this.normalize(this.gasStats.fast);
37 | }
38 |
39 | private get fastest(): BigNumber {
40 | return this.normalize(this.gasStats.fastest);
41 | }
42 |
43 | private normalize(value: BigNumber): BigNumber {
44 | return this.isBlock ? this.normalizeToBlock(value) : this.normalizeToTimestamp(value);
45 | }
46 |
47 | private normalizeToBlock(value: BigNumber): BigNumber {
48 | return value.div(this.gasStats.blockTime).decimalPlaces(0);
49 | }
50 |
51 | private normalizeToTimestamp(value: BigNumber): BigNumber {
52 | return value.multipliedBy(10);
53 | }
54 |
55 | private get isBlock() {
56 | return this.temporalUnit === 1;
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/EconomicStrategy/ProfitabilityCalculator.ts:
--------------------------------------------------------------------------------
1 | import { ITransactionRequest, Util, GasPriceUtil } from '@ethereum-alarm-clock/lib';
2 | import BigNumber from 'bignumber.js';
3 | import { ILogger, DefaultLogger } from '../Logger';
4 |
5 | const CLAIMING_GAS_ESTIMATE = 100000; // Claiming gas is around 75k, we add a small surplus
6 |
7 | export class ProfitabilityCalculator {
8 | private util: Util;
9 | private logger: ILogger;
10 | private gasPriceUtil: GasPriceUtil;
11 |
12 | constructor(util: Util, gasPriceUtil: GasPriceUtil, logger: ILogger = new DefaultLogger()) {
13 | this.util = util;
14 | this.gasPriceUtil = gasPriceUtil;
15 | this.logger = logger;
16 | }
17 |
18 | public async claimingProfitability(txRequest: ITransactionRequest, claimingGasPrice: BigNumber) {
19 | const paymentModifier = await this.getPaymentModifier(txRequest);
20 | const claimingGasCost = claimingGasPrice.times(CLAIMING_GAS_ESTIMATE);
21 | const { average } = await this.gasPriceUtil.getAdvancedNetworkGasPrice();
22 | const executionSubsidy = this.calculateExecutionSubsidy(txRequest, average);
23 |
24 | const reward = txRequest.bounty
25 | .times(paymentModifier)
26 | .minus(claimingGasCost)
27 | .minus(executionSubsidy);
28 |
29 | this.logger.debug(
30 | `claimingProfitability: paymentModifier=${paymentModifier} targetGasPrice=${claimingGasPrice} bounty=${
31 | txRequest.bounty
32 | } reward=${reward}`,
33 | txRequest.address
34 | );
35 |
36 | return reward;
37 | }
38 |
39 | public async executionProfitability(
40 | txRequest: ITransactionRequest,
41 | executionGasPrice: BigNumber
42 | ) {
43 | const paymentModifier = await this.getPaymentModifier(txRequest);
44 | const executionSubsidy = this.calculateExecutionSubsidy(txRequest, executionGasPrice);
45 | const { requiredDeposit, bounty } = txRequest;
46 |
47 | const reward = bounty
48 | .times(paymentModifier)
49 | .minus(executionSubsidy)
50 | .plus(txRequest.isClaimed ? requiredDeposit : 0)
51 | .decimalPlaces(0);
52 |
53 | this.logger.debug(
54 | `executionProfitability: executionSubsidy=${executionSubsidy} for executionGasPrice=${executionGasPrice} returns expectedReward=${reward}`,
55 | txRequest.address
56 | );
57 |
58 | return reward;
59 | }
60 |
61 | private calculateExecutionSubsidy(txRequest: ITransactionRequest, gasPrice: BigNumber) {
62 | let executionSubsidy = new BigNumber(0);
63 |
64 | if (txRequest.gasPrice < gasPrice) {
65 | const executionGasAmount = this.util.calculateGasAmount(txRequest);
66 | executionSubsidy = gasPrice.minus(txRequest.gasPrice).times(executionGasAmount);
67 | }
68 |
69 | return executionSubsidy;
70 | }
71 |
72 | private async getPaymentModifier(txRequest: ITransactionRequest) {
73 | return (await txRequest.claimPaymentModifier()).dividedBy(100);
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/EconomicStrategy/index.ts:
--------------------------------------------------------------------------------
1 | export { IEconomicStrategy } from './IEconomicStrategy';
2 | export { EconomicStrategyManager } from './EconomicStrategyManager';
3 |
--------------------------------------------------------------------------------
/src/Enum/AbortReason.ts:
--------------------------------------------------------------------------------
1 | export enum AbortReason {
2 | WasCancelled, //0
3 | AlreadyCalled, //1
4 | BeforeCallWindow, //2
5 | AfterCallWindow, //3
6 | ReservedForClaimer, //4
7 | InsufficientGas, //5
8 | TooLowGasPrice,
9 | Unknown //6
10 | }
11 |
--------------------------------------------------------------------------------
/src/Enum/EconomicStrategyStatus.ts:
--------------------------------------------------------------------------------
1 | export enum EconomicStrategyStatus {
2 | NOT_PROFITABLE = 'Transaction not profitable.',
3 | INSUFFICIENT_BALANCE = 'Not enough balance to claim.',
4 | CLAIM = 'Transaction can be claimed.',
5 | DEPOSIT_TOO_HIGH = 'The transaction deposit is too high.',
6 | TOO_SHORT_CLAIM_WINDOW = 'Claim window is too short',
7 | TOO_SHORT_RESERVED = 'Reserved window is too short'
8 | }
9 |
--------------------------------------------------------------------------------
/src/Enum/FnSignatures.ts:
--------------------------------------------------------------------------------
1 | export enum FnSignatures {
2 | claim = '0x4e71d92d',
3 | execute = '0x61461954'
4 | }
5 |
--------------------------------------------------------------------------------
/src/Enum/ReconnectMsg.ts:
--------------------------------------------------------------------------------
1 | export enum ReconnectMsg {
2 | NULL = '',
3 | ALREADY_RECONNECTED = 'Recent reconnection. Not attempting for a few seconds.',
4 | RECONNECTED = 'Reconnected!',
5 | MAX_ATTEMPTS = 'Max attempts reached. Stopped TimeNode.',
6 | RECONNECTING = 'Reconnecting in progress.',
7 | FAIL = 'Reconnection failed! Trying again...'
8 | }
9 |
--------------------------------------------------------------------------------
/src/Enum/TxSendStatus.ts:
--------------------------------------------------------------------------------
1 | export enum TxSendStatus {
2 | NOT_ENOUGH_FUNDS = "Account doesn't have enough funds to send transaction.",
3 | UNKNOWN_ERROR = 'An error happened',
4 | OK = 'OK',
5 | claim = 'Claiming',
6 | execute = 'Execution',
7 | default = 'Default',
8 | SUCCESS = 'SUCCESS',
9 |
10 | BUSY = 'Sending transaction is already in progress. Please wait for account to complete tx.',
11 | PROGRESS = 'Transaction in progress',
12 | MINED_IN_UNCLE = 'Transaction mined in uncle block',
13 | FAIL = 'FAILED',
14 | PENDING = 'PENDING',
15 |
16 | TYPE_VARIABLE = 'Unknown message or context',
17 | NOT_ENABLED = 'Claiming: Skipped - Claiming disabled',
18 | ABORTED_WAS_CANCELLED = 'Execution: Aborted with reason WasCancelled',
19 | ABORTED_ALREADY_CALLED = 'Execution: Aborted with reason AlreadyCalled',
20 | ABORTED_BEFORE_CALL_WINDOW = 'Execution: Aborted with reason BeforeCallWindow',
21 | ABORTED_AFTER_CALL_WINDOW = 'Execution: Aborted with reason AfterCallWindow',
22 | ABORTED_RESERVED_FOR_CLAIMER = 'Execution: Aborted with reason ReservedForClaimer',
23 | ABORTED_INSUFFICIENT_GAS = 'Execution: Aborted with reason InsufficientGas',
24 | ABORTED_TOO_LOW_GAS_PRICE = 'Execution: Aborted with reason TooLowGasPrice',
25 | ABORTED_UNKNOWN = 'Execution: Aborted with reason UNKNOWN'
26 | }
27 |
28 | // tslint:disable-next-line:no-namespace
29 | export namespace TxSendStatus {
30 | export function STATUS(msg: TxSendStatus, context: TxSendStatus.claim | TxSendStatus.execute) {
31 | switch (msg) {
32 | case TxSendStatus.SUCCESS:
33 | this.TYPE_VARIABLE = `${context}: Success`;
34 | break;
35 | case TxSendStatus.BUSY:
36 | this.TYPE_VARIABLE = `${context}: Skipped - Account is busy`;
37 | break;
38 | case TxSendStatus.PROGRESS:
39 | this.TYPE_VARIABLE = `${context}: Skipped - In progress`;
40 | break;
41 | case TxSendStatus.PENDING:
42 | this.TYPE_VARIABLE = TxSendStatus.claim
43 | ? 'Claiming: Skipped - Other claiming found'
44 | : 'Execution: Skipped - Other execution found';
45 | break;
46 | case TxSendStatus.FAIL:
47 | this.TYPE_VARIABLE = TxSendStatus.claim
48 | ? 'Claiming: Transaction already claimed'
49 | : 'Execution: Unable to send the execute action';
50 | break;
51 | case TxSendStatus.MINED_IN_UNCLE:
52 | this.TYPE_VARIABLE = `${context}: Transaction mined in uncle block`;
53 | break;
54 | }
55 |
56 | return this.TYPE_VARIABLE;
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/Enum/TxStatus.ts:
--------------------------------------------------------------------------------
1 | export enum TxStatus {
2 | BeforeClaimWindow,
3 | ClaimWindow,
4 | FreezePeriod,
5 | ExecutionWindow,
6 | Executed,
7 | Missed,
8 | Done
9 | }
10 |
--------------------------------------------------------------------------------
/src/Enum/index.ts:
--------------------------------------------------------------------------------
1 | export { AbortReason } from './AbortReason';
2 | export { TxSendStatus } from './TxSendStatus';
3 | export { EconomicStrategyStatus } from './EconomicStrategyStatus';
4 | export { FnSignatures } from './FnSignatures';
5 | export { ReconnectMsg } from './ReconnectMsg';
6 | export { TxStatus } from './TxStatus';
7 |
--------------------------------------------------------------------------------
/src/Logger/DefaultLogger.ts:
--------------------------------------------------------------------------------
1 | import { ILogger } from './ILogger';
2 |
3 | declare const console: any;
4 |
5 | export class DefaultLogger implements ILogger {
6 | public debug(msg: string, address: string = ''): void {
7 | this.formatPrint('DEBUG', msg, address);
8 | }
9 |
10 | public error(msg: string, address: string = ''): void {
11 | this.formatPrint('ERROR', msg, address);
12 | }
13 |
14 | public info(msg: string, address: string = ''): void {
15 | this.formatPrint('INFO', msg, address);
16 | }
17 |
18 | private formatPrint(kind: string, msg: string, address: string = ''): void {
19 | const txRequest = address ? ` [${address}]` : '';
20 | console.log(`${this.now()} [${kind}]${txRequest} ${msg}`);
21 | }
22 |
23 | private now(): string {
24 | return new Date().toISOString();
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/Logger/ILogger.ts:
--------------------------------------------------------------------------------
1 | export interface ILogger {
2 | debug(message: string, txRequest?: string): void;
3 | error(message: string, txRequest?: string): void;
4 | info(message: string, txRequest?: string): void;
5 | }
6 |
--------------------------------------------------------------------------------
/src/Logger/index.ts:
--------------------------------------------------------------------------------
1 | export { DefaultLogger } from './DefaultLogger';
2 | export { ILogger } from './ILogger';
3 |
--------------------------------------------------------------------------------
/src/Router/Router.ts:
--------------------------------------------------------------------------------
1 | import IActions from '../Actions';
2 | import { TxSendStatus, EconomicStrategyStatus, TxStatus } from '../Enum';
3 | import { Address } from '../Types';
4 | import { IEconomicStrategyManager } from '../EconomicStrategy/EconomicStrategyManager';
5 | import Cache, { ICachedTxDetails } from '../Cache';
6 | import { ILogger } from '../Logger';
7 | import { Wallet } from '../Wallet';
8 | import { Operation } from '../Types/Operation';
9 | import { ITransactionRequest, GasPriceUtil } from '@ethereum-alarm-clock/lib';
10 |
11 | type Transition = (txRequest: ITransactionRequest) => Promise;
12 |
13 | export default interface IRouter {
14 | route(txRequest: ITransactionRequest): Promise;
15 | }
16 |
17 | export default class Router implements IRouter {
18 | private actions: IActions;
19 | private cache: Cache;
20 | private logger: ILogger;
21 | private txRequestStates: object = {};
22 | private transitions: Map = new Map();
23 | private economicStrategyManager: IEconomicStrategyManager;
24 | private wallet: Wallet;
25 | private isClaimingEnabled: boolean;
26 | private gasPriceUtil: GasPriceUtil;
27 |
28 | constructor(
29 | isClaimingEnabled: boolean,
30 | cache: Cache,
31 | logger: ILogger,
32 | actions: IActions,
33 | economicStrategyManager: IEconomicStrategyManager,
34 | gasPriceUtil: GasPriceUtil,
35 | wallet: Wallet
36 | ) {
37 | this.actions = actions;
38 | this.cache = cache;
39 | this.logger = logger;
40 | this.wallet = wallet;
41 | this.economicStrategyManager = economicStrategyManager;
42 | this.isClaimingEnabled = isClaimingEnabled;
43 | this.gasPriceUtil = gasPriceUtil;
44 |
45 | this.transitions
46 | .set(TxStatus.BeforeClaimWindow, this.beforeClaimWindow.bind(this))
47 | .set(TxStatus.ClaimWindow, this.claimWindow.bind(this))
48 | .set(TxStatus.FreezePeriod, this.freezePeriod.bind(this))
49 | .set(TxStatus.ExecutionWindow, this.executionWindow.bind(this))
50 | .set(TxStatus.Executed, this.executed.bind(this))
51 | .set(TxStatus.Missed, this.missed.bind(this))
52 | .set(TxStatus.Done, this.done.bind(this));
53 | }
54 |
55 | public async beforeClaimWindow(txRequest: ITransactionRequest): Promise {
56 | if (txRequest.isCancelled) {
57 | // TODO Status.CleanUp?
58 | return TxStatus.Executed;
59 | }
60 |
61 | if (await txRequest.beforeClaimWindow()) {
62 | return TxStatus.BeforeClaimWindow;
63 | }
64 |
65 | return TxStatus.ClaimWindow;
66 | }
67 |
68 | public async claimWindow(txRequest: ITransactionRequest): Promise {
69 | const context = TxSendStatus.claim;
70 | if (this.wallet.isWaitingForConfirmation(txRequest.address, Operation.CLAIM)) {
71 | return TxStatus.ClaimWindow;
72 | }
73 |
74 | if (!(await txRequest.inClaimWindow()) || txRequest.isClaimed) {
75 | this.cache.get(txRequest.address).claimedBy = txRequest.claimedBy;
76 | return TxStatus.FreezePeriod;
77 | }
78 |
79 | if (this.isClaimingEnabled) {
80 | const nextAccount: Address = this.wallet.nextAccount.getAddressString();
81 | const fastestGas = (await this.gasPriceUtil.getAdvancedNetworkGasPrice()).fastest;
82 | const shouldClaimStatus: EconomicStrategyStatus = await this.economicStrategyManager.shouldClaimTx(
83 | txRequest,
84 | nextAccount,
85 | fastestGas
86 | );
87 |
88 | if (shouldClaimStatus === EconomicStrategyStatus.CLAIM) {
89 | try {
90 | const claimingStatus: TxSendStatus = await this.actions.claim(
91 | txRequest,
92 | nextAccount,
93 | fastestGas
94 | );
95 |
96 | this.handleWalletTransactionResult(claimingStatus, txRequest);
97 |
98 | if (
99 | claimingStatus === TxSendStatus.STATUS(TxSendStatus.SUCCESS, context) ||
100 | claimingStatus === TxSendStatus.STATUS(TxSendStatus.FAIL, context)
101 | ) {
102 | return TxStatus.FreezePeriod;
103 | }
104 | } catch (err) {
105 | this.logger.error(err, txRequest.address);
106 | throw new Error(err);
107 | }
108 | } else {
109 | this.logger.info(`Claiming: Skipped - ${shouldClaimStatus}`, txRequest.address);
110 | }
111 | }
112 |
113 | return TxStatus.ClaimWindow;
114 | }
115 |
116 | public async freezePeriod(txRequest: ITransactionRequest): Promise {
117 | if (await txRequest.inFreezePeriod()) {
118 | return TxStatus.FreezePeriod;
119 | }
120 |
121 | if (await txRequest.inExecutionWindow()) {
122 | return TxStatus.ExecutionWindow;
123 | }
124 |
125 | return TxStatus.FreezePeriod;
126 | }
127 |
128 | public async inReservedWindowAndNotClaimedLocally(
129 | txRequest: ITransactionRequest
130 | ): Promise {
131 | const inReserved = await txRequest.inReservedWindow();
132 | return inReserved && txRequest.isClaimed && !this.isLocalClaim(txRequest);
133 | }
134 |
135 | public async executionWindow(txRequest: ITransactionRequest): Promise {
136 | const context = TxSendStatus.execute;
137 | if (this.wallet.isWaitingForConfirmation(txRequest.address, Operation.EXECUTE)) {
138 | return TxStatus.ExecutionWindow;
139 | }
140 | if (txRequest.wasCalled) {
141 | return TxStatus.Executed;
142 | }
143 | if (await this.isTransactionMissed(txRequest)) {
144 | return TxStatus.Missed;
145 | }
146 |
147 | if (await this.inReservedWindowAndNotClaimedLocally(txRequest)) {
148 | return TxStatus.ExecutionWindow;
149 | }
150 |
151 | const executionGas = await this.economicStrategyManager.getExecutionGasPrice(txRequest);
152 | const shouldExecute = await this.economicStrategyManager.shouldExecuteTx(
153 | txRequest,
154 | executionGas
155 | );
156 |
157 | if (shouldExecute) {
158 | try {
159 | const executionStatus: TxSendStatus = await this.actions.execute(txRequest, executionGas);
160 |
161 | this.handleWalletTransactionResult(executionStatus, txRequest);
162 |
163 | if (executionStatus === TxSendStatus.STATUS(TxSendStatus.SUCCESS, context)) {
164 | return TxStatus.Executed;
165 | }
166 | } catch (err) {
167 | this.logger.error(err, txRequest.address);
168 | throw new Error(err);
169 | }
170 | } else {
171 | this.logger.info('Not profitable to execute. Gas price too high.', txRequest.address);
172 | }
173 |
174 | return TxStatus.ExecutionWindow;
175 | }
176 |
177 | public async executed(txRequest: ITransactionRequest): Promise {
178 | /**
179 | * We don't cleanup because cleanup needs refactor according to latest logic in EAC
180 | * https://github.com/ethereum-alarm-clock/ethereum-alarm-clock/blob/master/contracts/Library/RequestLib.sol#L433
181 | *
182 | * await this.actions.cleanup(txRequest);
183 | */
184 | this.cache.get(txRequest.address).wasCalled = true;
185 |
186 | return TxStatus.Done;
187 | }
188 |
189 | public async missed(): Promise {
190 | // TODO cleanup
191 | return TxStatus.Done;
192 | }
193 |
194 | public async isTransactionMissed(txRequest: ITransactionRequest): Promise {
195 | const now = await txRequest.now();
196 | const afterExecutionWindow = txRequest.executionWindowEnd.isLessThanOrEqualTo(now);
197 |
198 | return afterExecutionWindow && !txRequest.wasCalled;
199 | }
200 |
201 | public isLocalClaim(txRequest: ITransactionRequest): boolean {
202 | const localClaim = this.wallet.isKnownAddress(txRequest.claimedBy);
203 |
204 | if (!localClaim) {
205 | this.logger.debug(`In reserve window and not claimed by this TimeNode.`, txRequest.address);
206 | }
207 |
208 | return localClaim;
209 | }
210 |
211 | public async route(txRequest: ITransactionRequest): Promise {
212 | let current: TxStatus = this.txRequestStates[txRequest.address] || TxStatus.BeforeClaimWindow;
213 | let previous;
214 |
215 | try {
216 | while (current !== previous) {
217 | const transition = this.transitions.get(current);
218 | const next = await transition(txRequest);
219 | if (current !== next) {
220 | this.logger.debug(
221 | `Transition from ${TxStatus[current]} to ${TxStatus[next]} completed`,
222 | txRequest.address
223 | );
224 | }
225 |
226 | previous = current;
227 | current = next;
228 | }
229 | } catch (err) {
230 | this.logger.error(`Transition from ${TxStatus[current]} failed: ${err}`);
231 | }
232 |
233 | this.txRequestStates[txRequest.address] = current;
234 | return current;
235 | }
236 |
237 | private async done(txRequest: ITransactionRequest): Promise {
238 | this.logger.info('Finished. Deleting from cache...', txRequest.address);
239 | this.cache.del(txRequest.address);
240 | return TxStatus.Done;
241 | }
242 |
243 | private handleWalletTransactionResult(
244 | status: TxSendStatus,
245 | txRequest: ITransactionRequest
246 | ): void {
247 | switch (status) {
248 | case TxSendStatus.STATUS(TxSendStatus.SUCCESS, TxSendStatus.claim):
249 | this.logger.info('CLAIMED.', txRequest.address); //TODO: replace with SUCCESS string
250 | break;
251 | case TxSendStatus.STATUS(TxSendStatus.SUCCESS, TxSendStatus.execute):
252 | this.logger.info('EXECUTED.', txRequest.address); //TODO: replace with SUCCESS string
253 | break;
254 | case TxSendStatus.STATUS(TxSendStatus.BUSY, TxSendStatus.claim):
255 | case TxSendStatus.NOT_ENABLED:
256 | case TxSendStatus.STATUS(TxSendStatus.PENDING, TxSendStatus.claim):
257 | case TxSendStatus.STATUS(TxSendStatus.BUSY, TxSendStatus.execute):
258 | case TxSendStatus.STATUS(TxSendStatus.PENDING, TxSendStatus.execute):
259 | case TxSendStatus.STATUS(TxSendStatus.MINED_IN_UNCLE, TxSendStatus.execute):
260 | case TxSendStatus.STATUS(TxSendStatus.MINED_IN_UNCLE, TxSendStatus.claim):
261 | this.logger.info(status, txRequest.address);
262 | break;
263 | case TxSendStatus.STATUS(TxSendStatus.FAIL, TxSendStatus.claim):
264 | case TxSendStatus.STATUS(TxSendStatus.FAIL, TxSendStatus.execute):
265 | case TxSendStatus.ABORTED_AFTER_CALL_WINDOW:
266 | case TxSendStatus.ABORTED_BEFORE_CALL_WINDOW:
267 | case TxSendStatus.ABORTED_ALREADY_CALLED:
268 | case TxSendStatus.ABORTED_INSUFFICIENT_GAS:
269 | case TxSendStatus.ABORTED_RESERVED_FOR_CLAIMER:
270 | case TxSendStatus.ABORTED_TOO_LOW_GAS_PRICE:
271 | case TxSendStatus.ABORTED_WAS_CANCELLED:
272 | this.logger.error(status, txRequest.address);
273 | break;
274 | case TxSendStatus.STATUS(TxSendStatus.PROGRESS, TxSendStatus.claim):
275 | case TxSendStatus.STATUS(TxSendStatus.PROGRESS, TxSendStatus.execute):
276 | // skip logging this status
277 | break;
278 | }
279 | }
280 | }
281 |
--------------------------------------------------------------------------------
/src/Router/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './Router';
2 |
--------------------------------------------------------------------------------
/src/Scanner/BaseScanner.ts:
--------------------------------------------------------------------------------
1 | import Config from '../Config';
2 | import IRouter from '../Router';
3 | import { Util } from '@ethereum-alarm-clock/lib';
4 |
5 | export default class BaseScanner {
6 | public config: Config;
7 | public router: IRouter;
8 | public util: Util;
9 |
10 | constructor(config: Config, router: IRouter) {
11 | this.config = config;
12 | this.util = config.util;
13 | this.router = router;
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/Scanner/BucketWatchCallback.ts:
--------------------------------------------------------------------------------
1 | import { ITransactionRequestRaw } from '@ethereum-alarm-clock/lib';
2 |
3 | export type BucketWatchCallback = (request: ITransactionRequestRaw) => void;
4 |
--------------------------------------------------------------------------------
/src/Scanner/BucketsManager.ts:
--------------------------------------------------------------------------------
1 | import { WatchableBucket } from './WatchableBucket';
2 | import { Bucket } from '../Buckets';
3 | import { BucketWatchCallback } from './BucketWatchCallback';
4 | import { WatchableBucketFactory } from './WatchableBucketFactory';
5 | import { ILogger, DefaultLogger } from '../Logger';
6 |
7 | export class BucketsManager {
8 | private buckets: WatchableBucket[] = [];
9 | private watchableBucketFactory: WatchableBucketFactory;
10 | private logger: ILogger;
11 |
12 | constructor(
13 | watchableBucketFactory: WatchableBucketFactory,
14 | logger: ILogger = new DefaultLogger()
15 | ) {
16 | this.watchableBucketFactory = watchableBucketFactory;
17 | this.logger = logger;
18 | }
19 |
20 | public async stop() {
21 | await Promise.all(this.buckets.map(b => b.stop()));
22 | this.buckets = [];
23 | return;
24 | }
25 |
26 | public async update(buckets: Bucket[], callback: BucketWatchCallback) {
27 | this.logger.debug(`Buckets: updating with ${buckets}`);
28 |
29 | const toStart = await Promise.all(
30 | buckets
31 | .filter(b => !this.knownBucket(b))
32 | .map(b => this.watchableBucketFactory.create(b, callback))
33 | );
34 | const toSkip = this.buckets.filter(b => buckets.indexOf(b.bucket) > -1);
35 | const toStop = this.buckets.filter(b => buckets.indexOf(b.bucket) === -1);
36 |
37 | const starting = toStart.map(b => b.watch());
38 | const stopping = toStop.map(b => b.stop());
39 |
40 | await Promise.all(starting);
41 | await Promise.all(stopping);
42 |
43 | this.buckets = toSkip.concat(toStart);
44 |
45 | this.logger.debug(`Buckets: updated ${this.buckets.map(b => b.bucket)}`);
46 | }
47 |
48 | private knownBucket(bucket: Bucket): boolean {
49 | return this.buckets.some(b => b.bucket === bucket);
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/Scanner/CacheScanner.ts:
--------------------------------------------------------------------------------
1 | import BigNumber from 'bignumber.js';
2 |
3 | import { IntervalId } from '../Types';
4 | import BaseScanner from './BaseScanner';
5 | import IRouter from '../Router';
6 | import Config from '../Config';
7 | import { TxStatus } from '../Enum';
8 |
9 | import { ICachedTxDetails } from '../Cache';
10 | import { ITransactionRequest } from '@ethereum-alarm-clock/lib';
11 |
12 | export default class CacheScanner extends BaseScanner {
13 | public cacheInterval: IntervalId;
14 | public avgBlockTime: number;
15 |
16 | private routes: Set = new Set();
17 |
18 | constructor(config: Config, router: IRouter) {
19 | super(config, router);
20 | }
21 |
22 | public async scanCache(): Promise {
23 | if (this.config.cache.isEmpty()) {
24 | return;
25 | }
26 |
27 | this.avgBlockTime = await this.config.util.getAverageBlockTime();
28 |
29 | let txRequests = this.getCacheTxRequests();
30 | txRequests = this.prioritizeTransactions(txRequests);
31 | txRequests.forEach((txRequest: ITransactionRequest) => this.route(txRequest));
32 | }
33 |
34 | public getCacheTxRequests(): ITransactionRequest[] {
35 | return this.config.cache.stored().map(address => this.config.eac.transactionRequest(address));
36 | }
37 |
38 | /*
39 | * Prioritizes transactions in the following order:
40 | * 1. Transactions in FreezePeriod come first
41 | * 2. Sorts sorted by windowStart
42 | * 3. If some 2 transactions have windowStart set to the same block,
43 | * it sorts those by whichever has the highest bounty.
44 | */
45 | private prioritizeTransactions(txRequests: ITransactionRequest[]): ITransactionRequest[] {
46 | const getTxFromCache = (address: string) => this.config.cache.get(address);
47 |
48 | const blockTransactions = txRequests.filter(
49 | (tx: ITransactionRequest) => getTxFromCache(tx.address).temporalUnit === 1
50 | );
51 | const timestampTransactions = txRequests.filter(
52 | (tx: ITransactionRequest) => getTxFromCache(tx.address).temporalUnit === 2
53 | );
54 |
55 | blockTransactions.sort((currentTx, nextTx) =>
56 | this.claimWindowStartSort(
57 | getTxFromCache(currentTx.address).claimWindowStart,
58 | getTxFromCache(nextTx.address).claimWindowStart
59 | )
60 | );
61 | blockTransactions.sort((currentTx, nextTx) =>
62 | this.higherBountySortIfInSameBlock(
63 | getTxFromCache(currentTx.address),
64 | getTxFromCache(nextTx.address)
65 | )
66 | );
67 |
68 | timestampTransactions.sort((currentTx, nextTx) =>
69 | this.claimWindowStartSort(
70 | getTxFromCache(currentTx.address).claimWindowStart,
71 | getTxFromCache(nextTx.address).claimWindowStart
72 | )
73 | );
74 | timestampTransactions.sort((currentTx, nextTx) =>
75 | this.higherBountySortIfInSameBlock(
76 | getTxFromCache(currentTx.address),
77 | getTxFromCache(nextTx.address)
78 | )
79 | );
80 |
81 | txRequests = blockTransactions
82 | .concat(timestampTransactions)
83 | .sort((currentTx, nextTx) => this.prioritizeFreezePeriod(currentTx, nextTx));
84 |
85 | return txRequests;
86 | }
87 |
88 | private claimWindowStartSort(
89 | currentClaimWindowStart: BigNumber,
90 | nextClaimWindowStart: BigNumber
91 | ): number {
92 | if (currentClaimWindowStart.isLessThan(nextClaimWindowStart)) {
93 | return -1;
94 | } else if (currentClaimWindowStart.isGreaterThan(nextClaimWindowStart)) {
95 | return 1;
96 | }
97 | return 0;
98 | }
99 |
100 | private prioritizeFreezePeriod(
101 | currentTx: ITransactionRequest,
102 | nextTx: ITransactionRequest
103 | ): number {
104 | const statusA = this.config.cache.get(currentTx.address).status;
105 | const statusB = this.config.cache.get(nextTx.address).status;
106 |
107 | if (statusA === statusB) {
108 | return 0;
109 | }
110 |
111 | return statusA === TxStatus.FreezePeriod && statusB !== TxStatus.FreezePeriod ? -1 : 1;
112 | }
113 |
114 | private higherBountySortIfInSameBlock(
115 | currentTx: ICachedTxDetails,
116 | nextTx: ICachedTxDetails
117 | ): number {
118 | const blockTime = currentTx.temporalUnit === 1 ? 1 : this.avgBlockTime;
119 |
120 | const blockDifference = currentTx.windowStart.minus(nextTx.windowStart).abs();
121 | const isInSameBlock = blockDifference.isLessThanOrEqualTo(blockTime);
122 |
123 | if (isInSameBlock) {
124 | if (currentTx.bounty.isLessThan(nextTx.bounty)) {
125 | return 1;
126 | } else if (currentTx.bounty.isGreaterThan(nextTx.bounty)) {
127 | return -1;
128 | }
129 | }
130 |
131 | return 0;
132 | }
133 |
134 | private async route(txRequest: ITransactionRequest): Promise {
135 | const address = txRequest.address;
136 | if (!this.routes.has(address)) {
137 | this.routes.add(address);
138 |
139 | try {
140 | await txRequest.refreshData();
141 | await this.router.route(txRequest);
142 | } finally {
143 | this.routes.delete(address);
144 | }
145 | } else {
146 | this.config.logger.debug(`Routing in progress. Skipping...`, address);
147 | }
148 | }
149 | }
150 |
--------------------------------------------------------------------------------
/src/Scanner/ChainScanner.ts:
--------------------------------------------------------------------------------
1 | import Config from '../Config';
2 | import IRouter from '../Router';
3 | import { IntervalId, Address } from '../Types';
4 | import CacheScanner from './CacheScanner';
5 | import { BucketCalc, IBucketCalc } from '../Buckets';
6 | import { TxStatus } from '../Enum';
7 | import { BucketsManager } from './BucketsManager';
8 | import { WatchableBucketFactory } from './WatchableBucketFactory';
9 | import BigNumber from 'bignumber.js';
10 | import { ITransactionRequestRaw, RequestFactory } from '@ethereum-alarm-clock/lib';
11 |
12 | export default class ChainScanner extends CacheScanner {
13 | public bucketCalc: IBucketCalc;
14 |
15 | public chainInterval: IntervalId;
16 | public eventWatchers: {} = {};
17 | public requestFactory: Promise;
18 |
19 | private bucketsManager: BucketsManager;
20 |
21 | constructor(config: Config, router: IRouter) {
22 | super(config, router);
23 | this.requestFactory = config.eac.requestFactory();
24 | this.bucketCalc = new BucketCalc(config.util, this.requestFactory);
25 | this.bucketsManager = new BucketsManager(
26 | new WatchableBucketFactory(this.requestFactory, this.config.logger),
27 | this.config.logger
28 | );
29 |
30 | this.handleRequest = this.handleRequest.bind(this);
31 | }
32 |
33 | public async watchBlockchain(): Promise {
34 | const newBuckets = await this.bucketCalc.getBuckets();
35 | return this.bucketsManager.update(newBuckets, this.handleRequest);
36 | }
37 |
38 | protected async stopAllWatchers(): Promise {
39 | return this.bucketsManager.stop();
40 | }
41 |
42 | private handleRequest(request: ITransactionRequestRaw): void {
43 | if (!this.isValid(request.address)) {
44 | throw new Error(`[${request.address}] NOT VALID`);
45 | }
46 |
47 | request.address = request.address.toLowerCase();
48 |
49 | this.config.logger.info('Discovered.', request.address);
50 | if (!this.config.cache.has(request.address)) {
51 | this.store(request);
52 |
53 | this.config.wallet.getAddresses().forEach((from: Address) => {
54 | this.config.statsDb.discovered(from, request.address);
55 | });
56 | }
57 | }
58 |
59 | private isValid(requestAddress: string): boolean {
60 | if (!this.config.eac.util.isNotNullAddress(requestAddress)) {
61 | this.config.logger.debug('Warning.. Transaction Request with NULL_ADDRESS found.');
62 | return false;
63 | } else if (!this.config.eac.util.checkValidAddress(requestAddress)) {
64 | // This should, conceivably, never happen unless there is a bug in @ethereum-alarm-clock/lib.
65 | throw new Error(
66 | `[${requestAddress}] Received invalid response from Request Tracker - CRITICAL BUG`
67 | );
68 | }
69 | return true;
70 | }
71 |
72 | private store(txRequest: ITransactionRequestRaw) {
73 | const windowStart = new BigNumber(txRequest.params[7]);
74 | const freezePeriod = new BigNumber(txRequest.params[3]);
75 | const claimWindowSize = new BigNumber(txRequest.params[2]);
76 |
77 | const claimWindowStart = windowStart.minus(freezePeriod).minus(claimWindowSize);
78 |
79 | this.config.cache.set(txRequest.address, {
80 | bounty: new BigNumber(txRequest.params[1]),
81 | temporalUnit: parseInt(txRequest.params[5], 10),
82 | claimedBy: null,
83 | wasCalled: false,
84 | windowStart,
85 | claimWindowStart,
86 | status: TxStatus.BeforeClaimWindow
87 | });
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/src/Scanner/IBucketWatcher.ts:
--------------------------------------------------------------------------------
1 | import { BucketWatchCallback } from './BucketWatchCallback';
2 |
3 | export interface IBucketWatcher {
4 | watchRequestsByBucket(bucket: number, callBack: BucketWatchCallback): Promise;
5 | stopWatch(watcher: any): Promise;
6 | }
7 |
--------------------------------------------------------------------------------
/src/Scanner/TimeNodeScanner.ts:
--------------------------------------------------------------------------------
1 | /* eslint no-await-in-loop: 'off' */
2 | import { Util } from '@ethereum-alarm-clock/lib';
3 |
4 | import Config from '../Config';
5 | import IRouter from '../Router';
6 | import { ITxPool } from '../TxPool';
7 | import { IntervalId } from '../Types';
8 | import ChainScanner from './ChainScanner';
9 |
10 | declare const clearInterval: any;
11 | declare const setInterval: any;
12 |
13 | export interface ITimeNodeScanner {
14 | scanning: boolean;
15 | txPool: ITxPool;
16 |
17 | start(): Promise;
18 | stop(): Promise;
19 | }
20 |
21 | export default class TimeNodeScanner extends ChainScanner implements ITimeNodeScanner {
22 | public scanning: boolean = false;
23 | public txPool: ITxPool;
24 |
25 | constructor(config: Config, router: IRouter) {
26 | super(config, router);
27 | this.txPool = config.txPool;
28 | }
29 |
30 | public async start(): Promise {
31 | if (!(await Util.isWatchingEnabled(this.config.web3))) {
32 | throw new Error(
33 | 'Your provider does not support eth_getFilterLogs calls. Please use different provider.'
34 | );
35 | }
36 |
37 | await this.txPool.start();
38 |
39 | this.scanning = true;
40 | this.cacheInterval = await this.runAndSetInterval(() => this.scanCache(), this.config.ms);
41 | this.chainInterval = await this.runAndSetInterval(() => this.watchBlockchain(), 5 * 60 * 1000);
42 |
43 | // Mark that we've started.
44 | this.config.logger.info('Scanner STARTED');
45 | return this.scanning;
46 | }
47 |
48 | public async stop(): Promise {
49 | if (this.scanning) {
50 | this.scanning = false;
51 | // Clear scanning intervals.
52 | clearInterval(this.cacheInterval);
53 | clearInterval(this.chainInterval);
54 |
55 | await this.txPool.stop();
56 |
57 | // Mark that we've stopped.
58 | this.config.logger.info('Scanner STOPPED');
59 | }
60 |
61 | await this.stopAllWatchers();
62 |
63 | return this.scanning;
64 | }
65 |
66 | private async runAndSetInterval(fn: () => Promise, interval: number): Promise {
67 | if (!this.scanning) {
68 | this.config.logger.debug('Not starting intervals when TimeNode is intentionally stopped.');
69 | return null;
70 | }
71 | const wrapped = async (): Promise => {
72 | try {
73 | await fn();
74 | } catch (e) {
75 | this.config.logger.error(e);
76 | }
77 | };
78 |
79 | await wrapped();
80 | return setInterval(wrapped, interval);
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/src/Scanner/WatchableBucket.ts:
--------------------------------------------------------------------------------
1 | import { ILogger, DefaultLogger } from '../Logger';
2 | import { Bucket } from '../Buckets';
3 | import { BucketWatchCallback } from './BucketWatchCallback';
4 | import { IBucketWatcher } from './IBucketWatcher';
5 |
6 | export class WatchableBucket {
7 | private bucketNumber: Bucket;
8 | private watcher: any;
9 | private requestFactory: IBucketWatcher;
10 | private logger: ILogger;
11 | private callBack: BucketWatchCallback;
12 |
13 | constructor(
14 | bucket: Bucket,
15 | requestFactory: IBucketWatcher,
16 | callBack: BucketWatchCallback,
17 | logger: ILogger = new DefaultLogger()
18 | ) {
19 | this.bucketNumber = bucket;
20 | this.requestFactory = requestFactory;
21 | this.callBack = callBack;
22 | this.logger = logger;
23 | }
24 |
25 | public async watch() {
26 | if (this.watcher) {
27 | this.logger.debug(`WatchableBucket: Bucket ${this.bucketNumber} already watched.`);
28 | return;
29 | }
30 | await this.start();
31 | }
32 |
33 | public async stop() {
34 | try {
35 | if (this.watcher) {
36 | await this.requestFactory.stopWatch(this.watcher);
37 | this.watcher = null;
38 |
39 | this.logger.debug(`Buckets: Watcher for bucket=${this.bucketNumber} has been stopped`);
40 | }
41 | } catch (err) {
42 | this.logger.error(`Buckets: Stopping bucket=${this.bucketNumber} watching failed!`);
43 | }
44 | }
45 |
46 | public get bucket(): Bucket {
47 | return this.bucketNumber;
48 | }
49 |
50 | private async start() {
51 | try {
52 | const watcher = await this.requestFactory.watchRequestsByBucket(
53 | this.bucketNumber,
54 | this.callBack
55 | );
56 | this.watcher = watcher;
57 |
58 | this.logger.debug(`Buckets: Watcher for bucket=${this.bucketNumber} has been started`);
59 | } catch (err) {
60 | this.logger.error(`Buckets: Starting bucket=${this.bucketNumber} watching failed!`);
61 | }
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/Scanner/WatchableBucketFactory.ts:
--------------------------------------------------------------------------------
1 | import { WatchableBucket } from './WatchableBucket';
2 | import { IBucketWatcher } from './IBucketWatcher';
3 | import { Bucket } from '../Buckets';
4 | import { BucketWatchCallback } from './BucketWatchCallback';
5 | import { ILogger } from '../Logger';
6 |
7 | export class WatchableBucketFactory {
8 | private requestFactory: Promise;
9 | private logger: ILogger;
10 |
11 | constructor(requestFactory: Promise, logger: ILogger) {
12 | this.requestFactory = requestFactory;
13 | this.logger = logger;
14 | }
15 |
16 | public async create(bucket: Bucket, callback: BucketWatchCallback): Promise {
17 | return new WatchableBucket(bucket, await this.requestFactory, callback, this.logger);
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/Scanner/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './TimeNodeScanner';
2 |
--------------------------------------------------------------------------------
/src/Stats/StatsDB.ts:
--------------------------------------------------------------------------------
1 | import BigNumber from 'bignumber.js';
2 |
3 | enum StatsEntryAction {
4 | Discover,
5 | Claim,
6 | Execute
7 | }
8 |
9 | enum StatsEntryResult {
10 | NOK,
11 | OK
12 | }
13 |
14 | export interface IStatsEntry {
15 | from: string;
16 | txAddress: string;
17 | timestamp: number;
18 | action: StatsEntryAction;
19 | cost: BigNumber;
20 | bounty: BigNumber;
21 | result: StatsEntryResult;
22 | }
23 |
24 | export interface IStatsDB {
25 | init(): Promise;
26 | discovered(from: string, txAddress: string): void;
27 | claimed(from: string, txAddress: string, cost: BigNumber, success: boolean): void;
28 | executed(
29 | from: string,
30 | txAddress: string,
31 | cost: BigNumber,
32 | bounty: BigNumber,
33 | success: boolean
34 | ): void;
35 | getFailedExecutions(from: string): IStatsEntry[];
36 | getSuccessfulExecutions(from: string): IStatsEntry[];
37 | getFailedClaims(from: string): IStatsEntry[];
38 | getSuccessfulClaims(from: string): IStatsEntry[];
39 | getDiscovered(from: string): IStatsEntry[];
40 | clear(from: string): void;
41 | clearAll(): void;
42 | totalCost(from: string): BigNumber;
43 | totalBounty(from: string): BigNumber;
44 | }
45 |
46 | export class StatsDB implements IStatsDB {
47 | private COLLECTION_NAME: string = 'timenode-stats';
48 | private db: Loki;
49 | private isLoaded: boolean;
50 |
51 | constructor(db: Loki) {
52 | this.db = db;
53 | }
54 |
55 | public init(): Promise {
56 | return new Promise((resolve, reject) => {
57 | this.db.loadDatabase({}, err => {
58 | if (err) {
59 | reject(err);
60 | }
61 |
62 | const collection = this.db.getCollection(this.COLLECTION_NAME);
63 |
64 | if (!collection) {
65 | this.db.addCollection(this.COLLECTION_NAME);
66 | } else {
67 | collection.data.forEach(stat => {
68 | stat.bounty = new BigNumber(stat.bounty);
69 | stat.cost = new BigNumber(stat.cost);
70 | });
71 | }
72 |
73 | this.isLoaded = true;
74 | resolve(true);
75 | });
76 | });
77 | }
78 |
79 | public discovered(from: string, txAddress: string) {
80 | if (this.exists(from, txAddress, StatsEntryAction.Discover)) {
81 | return;
82 | }
83 |
84 | this.insert({
85 | from,
86 | txAddress,
87 | timestamp: new Date().getTime(),
88 | action: StatsEntryAction.Discover,
89 | cost: new BigNumber(0),
90 | bounty: new BigNumber(0),
91 | result: StatsEntryResult.OK
92 | });
93 | }
94 |
95 | public claimed(from: string, txAddress: string, cost: BigNumber, success: boolean) {
96 | this.insert({
97 | from,
98 | txAddress,
99 | timestamp: new Date().getTime(),
100 | action: StatsEntryAction.Claim,
101 | cost,
102 | bounty: new BigNumber(0),
103 | result: success ? StatsEntryResult.OK : StatsEntryResult.NOK
104 | });
105 | }
106 |
107 | public executed(
108 | from: string,
109 | txAddress: string,
110 | cost: BigNumber,
111 | bounty: BigNumber,
112 | success: boolean
113 | ) {
114 | this.insert({
115 | from,
116 | txAddress,
117 | timestamp: new Date().getTime(),
118 | action: StatsEntryAction.Execute,
119 | cost,
120 | bounty,
121 | result: success ? StatsEntryResult.OK : StatsEntryResult.NOK
122 | });
123 | }
124 |
125 | public getFailedExecutions(from: string): IStatsEntry[] {
126 | return this.select(from, StatsEntryAction.Execute, StatsEntryResult.NOK).data();
127 | }
128 |
129 | public getSuccessfulExecutions(from: string): IStatsEntry[] {
130 | return this.select(from, StatsEntryAction.Execute, StatsEntryResult.OK).data();
131 | }
132 |
133 | public getFailedClaims(from: string): IStatsEntry[] {
134 | return this.select(from, StatsEntryAction.Claim, StatsEntryResult.NOK).data();
135 | }
136 |
137 | public getSuccessfulClaims(from: string): IStatsEntry[] {
138 | return this.select(from, StatsEntryAction.Claim, StatsEntryResult.OK).data();
139 | }
140 |
141 | public getDiscovered(from: string): IStatsEntry[] {
142 | return this.select(from, StatsEntryAction.Discover, StatsEntryResult.OK).data();
143 | }
144 |
145 | public clear(from: string) {
146 | this.collection
147 | .chain()
148 | .find({ from })
149 | .remove();
150 | }
151 |
152 | public clearAll() {
153 | this.collection.clear();
154 | }
155 |
156 | public totalCost(from: string): BigNumber {
157 | return this.collection
158 | .chain()
159 | .where((item: IStatsEntry) => item.from === from && item.cost.gt(0))
160 | .mapReduce(
161 | (item: IStatsEntry) => item.cost,
162 | (costs: BigNumber[]) => costs.reduce((sum, cost) => sum.plus(cost), new BigNumber(0))
163 | );
164 | }
165 |
166 | public totalBounty(from: string): BigNumber {
167 | return this.select(from, StatsEntryAction.Execute, StatsEntryResult.OK).mapReduce(
168 | (item: IStatsEntry) => item.bounty,
169 | (bounties: BigNumber[]) =>
170 | bounties.reduce((sum, bounty) => sum.plus(bounty), new BigNumber(0))
171 | );
172 | }
173 |
174 | private select(from: string, action: StatsEntryAction, result: StatsEntryResult): any {
175 | return this.collection.chain().find({ from, action, result });
176 | }
177 |
178 | private exists(from: string, txAddress: string, action: StatsEntryAction): boolean {
179 | return this.collection.find({ from, txAddress, action }).length >= 1;
180 | }
181 |
182 | private insert(entry: IStatsEntry) {
183 | this.collection.insert(entry);
184 | }
185 |
186 | private get collection(): Collection {
187 | this.ensureLoaded();
188 |
189 | return this.db.getCollection(this.COLLECTION_NAME);
190 | }
191 |
192 | private ensureLoaded() {
193 | if (!this.isLoaded) {
194 | throw new Error('DB not loaded, use init() before');
195 | }
196 | }
197 | }
198 |
--------------------------------------------------------------------------------
/src/Stats/index.ts:
--------------------------------------------------------------------------------
1 | export { StatsDB } from './StatsDB';
2 |
--------------------------------------------------------------------------------
/src/TimeNode.ts:
--------------------------------------------------------------------------------
1 | import Actions from './Actions';
2 | import Config from './Config';
3 | import Scanner from './Scanner';
4 | import Router from './Router';
5 | import Version from './Version';
6 | import WsReconnect from './WsReconnect';
7 | import { Util, Networks } from '@ethereum-alarm-clock/lib';
8 |
9 | export default class TimeNode {
10 | public actions: Actions;
11 | public config: Config;
12 | public scanner: Scanner;
13 | public router: Router;
14 | public wsReconnect: WsReconnect;
15 |
16 | constructor(config: Config) {
17 | this.actions = new Actions(
18 | config.wallet,
19 | config.ledger,
20 | config.logger,
21 | config.cache,
22 | config.util,
23 | config.pending
24 | );
25 |
26 | this.router = new Router(
27 | config.claiming,
28 | config.cache,
29 | config.logger,
30 | this.actions,
31 | config.economicStrategyManager,
32 | config.gasPriceUtil,
33 | config.wallet
34 | );
35 |
36 | this.config = config;
37 | this.scanner = new Scanner(this.config, this.router);
38 |
39 | const { logger, providerUrls } = this.config;
40 | if (Util.isWSConnection(providerUrls[0])) {
41 | logger.debug('WebSockets provider detected! Setting up reconnect events...');
42 | this.wsReconnect = new WsReconnect(this);
43 | this.wsReconnect.setup();
44 | }
45 |
46 | this.startupMessage();
47 | }
48 |
49 | public startupMessage(): void {
50 | this.config.logger.info('EAC-TimeNode');
51 | this.config.logger.info('Version: ' + Version);
52 | this.logNetwork();
53 | }
54 |
55 | public async logNetwork(): Promise {
56 | const id = await this.config.web3.eth.net.getId();
57 | this.config.logger.info('Operating on ' + Networks[id]);
58 | }
59 |
60 | public async startScanning(): Promise {
61 | // If already scanning, hard-reset the Scanner module.
62 | if (this.scanner.scanning) {
63 | await this.scanner.stop();
64 | }
65 |
66 | return this.scanner.start();
67 | }
68 |
69 | public stopScanning(): Promise {
70 | return this.scanner.stop();
71 | }
72 |
73 | public startClaiming(): boolean {
74 | this.config.claiming = true;
75 | return this.config.claiming;
76 | }
77 |
78 | public stopClaiming(): boolean {
79 | this.config.claiming = false;
80 | return this.config.claiming;
81 | }
82 |
83 | public getClaimedNotExecutedTransactions(): object {
84 | const cachedTransactionsAddresses = this.config.cache.stored();
85 | const accounts = this.config.wallet.getAddresses();
86 |
87 | const claimedPendingExecution: {} = {};
88 |
89 | for (const account of accounts) {
90 | claimedPendingExecution[account] = [];
91 | }
92 |
93 | for (const address of cachedTransactionsAddresses) {
94 | const cachedTx = this.config.cache.get(address);
95 |
96 | const claimerIndex = accounts.indexOf(cachedTx.claimedBy);
97 | if (claimerIndex !== -1 && !cachedTx.wasCalled) {
98 | const claimer = this.config.wallet.getAddresses()[claimerIndex];
99 | claimedPendingExecution[claimer].push(address);
100 | }
101 | }
102 |
103 | return claimedPendingExecution;
104 | }
105 |
106 | public getUnsucessfullyClaimedTransactions(): object {
107 | const accounts = this.config.wallet.getAddresses();
108 |
109 | const unsuccessfulClaims: {} = {};
110 |
111 | for (const account of accounts) {
112 | const failedClaims = this.config.statsDb
113 | .getFailedClaims(account)
114 | .map(entry => entry.txAddress);
115 | unsuccessfulClaims[account] = failedClaims;
116 | }
117 |
118 | return unsuccessfulClaims;
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/src/TxPool/DirectTxPool.ts:
--------------------------------------------------------------------------------
1 | import { Networks } from '@ethereum-alarm-clock/lib';
2 | import BigNumber from 'bignumber.js';
3 | import { randomBytes } from 'crypto';
4 | import { bootstrapNodes } from 'ethereum-common';
5 | import * as devp2p from 'ethereumjs-devp2p';
6 | import EthereumTx = require('ethereumjs-tx');
7 | import Web3 = require('web3');
8 |
9 | import { ITxPool, ITxPoolTxDetails } from '.';
10 | import { DefaultLogger, ILogger } from '../Logger';
11 | import { Operation } from '../Types/Operation';
12 |
13 | export default class DirectTxPool implements ITxPool {
14 | private static ExecuteData = '61461954';
15 | private static ClaimData = '4e71d92d';
16 | private static MaxPeers = 25;
17 | private static NetworkStatusMessage = new Map([
18 | [
19 | Networks.Mainnet,
20 | {
21 | networkId: Networks.Mainnet,
22 | td: devp2p._util.int2buffer(17179869184), // total difficulty in genesis block
23 | bestHash: Buffer.from(
24 | 'd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3',
25 | 'hex'
26 | ),
27 | genesisHash: Buffer.from(
28 | 'd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3',
29 | 'hex'
30 | )
31 | }
32 | ],
33 | [
34 | Networks.Kovan,
35 | {
36 | networkId: Networks.Kovan,
37 | td: devp2p._util.int2buffer(131072), // total difficulty in genesis block
38 | bestHash: Buffer.from(
39 | 'a3c565fc15c7478862d50ccd6561e3c06b24cc509bf388941c25ea985ce32cb9',
40 | 'hex'
41 | ),
42 | genesisHash: Buffer.from(
43 | 'a3c565fc15c7478862d50ccd6561e3c06b24cc509bf388941c25ea985ce32cb9',
44 | 'hex'
45 | )
46 | }
47 | ]
48 | ]);
49 |
50 | public pool: Map = new Map();
51 | private isRunning: boolean = false;
52 | private web3: Web3;
53 | private privateKey: Buffer;
54 | private logger: ILogger;
55 | private chainId: Networks;
56 |
57 | private dpt: any;
58 | private rlpx: any;
59 |
60 | private status: NodeJS.Timeout;
61 |
62 | constructor(web3: Web3, logger: ILogger = new DefaultLogger()) {
63 | this.web3 = web3;
64 | this.privateKey = randomBytes(32);
65 | this.logger = logger;
66 | }
67 |
68 | public running(): boolean {
69 | return this.isRunning;
70 | }
71 |
72 | public async start() {
73 | const chainId = await this.web3.eth.net.getId();
74 |
75 | if (!this.isRunning && this.isChainSupported(chainId)) {
76 | this.chainId = chainId;
77 | this.startListening();
78 | }
79 | }
80 | // tslint:disable-next-line:no-empty
81 | public async stop() {
82 | clearInterval(this.status);
83 | }
84 |
85 | private isChainSupported(chainId: number) {
86 | return chainId === Networks.Mainnet || chainId === Networks.Kovan;
87 | }
88 |
89 | private get bootNodes() {
90 | if (this.chainId === Networks.Mainnet) {
91 | return bootstrapNodes
92 | .filter((node: any) => {
93 | return node.chainId === Networks.Mainnet;
94 | })
95 | .map((node: any) => {
96 | return {
97 | address: node.ip,
98 | udpPort: node.port,
99 | tcpPort: node.port
100 | };
101 | });
102 | } else if (this.chainId === Networks.Kovan) {
103 | return [
104 | {
105 | address: '40.71.221.215',
106 | udpPort: 30303,
107 | tcpPort: 30303
108 | },
109 | {
110 | address: '52.166.117.77',
111 | udpPort: 30303,
112 | tcpPort: 30303
113 | },
114 | {
115 | address: '52.165.239.18',
116 | udpPort: 30303,
117 | tcpPort: 30303
118 | },
119 | {
120 | address: '52.243.47.56',
121 | udpPort: 30303,
122 | tcpPort: 30303
123 | },
124 | {
125 | address: '40.68.248.100',
126 | udpPort: 30303,
127 | tcpPort: 30303
128 | }
129 | ];
130 | }
131 | }
132 |
133 | private bootstrap(bootNodes: any[]) {
134 | this.logger.debug(`[p2p] Bootstraping with ${bootNodes.length} nodes`);
135 |
136 | this.dpt = new devp2p.DPT(this.privateKey, {
137 | refreshInterval: 30000,
138 | endpoint: {
139 | address: '0.0.0.0',
140 | udpPort: null,
141 | tcpPort: null
142 | }
143 | });
144 |
145 | bootNodes.forEach((bootNode: any) => {
146 | this.dpt
147 | .bootstrap(bootNode)
148 | .catch((e: Error) => this.logger.debug(`[p2p] bootstrap error ${e.message}`));
149 | });
150 |
151 | clearInterval(this.status);
152 | this.status = setInterval(() => {
153 | const peersCount = this.dpt.getPeers().length;
154 | const openSlots = this.rlpx._getOpenSlots();
155 | const connected = DirectTxPool.MaxPeers - openSlots;
156 |
157 | this.logger.debug(
158 | `[p2p] Discovered ${peersCount} nodes, connected to ${connected}/${DirectTxPool.MaxPeers}`
159 | );
160 | }, 60 * 1000);
161 | }
162 |
163 | private register() {
164 | return new devp2p.RLPx(this.privateKey, {
165 | dpt: this.dpt,
166 | maxPeers: DirectTxPool.MaxPeers,
167 | capabilities: [devp2p.ETH.eth63],
168 | listenPort: null
169 | });
170 | }
171 |
172 | private async fetchLatestBlockHash(): Promise {
173 | const latestBlock = await this.web3.eth.getBlock('latest');
174 | return Buffer.from(latestBlock.hash.substring(2), 'hex');
175 | }
176 |
177 | private async sendStatus(eth: any) {
178 | const message = DirectTxPool.NetworkStatusMessage.get(this.chainId);
179 |
180 | if (message) {
181 | message.bestHash = await this.fetchLatestBlockHash();
182 | try {
183 | eth.sendStatus(message);
184 | } catch (error) {
185 | this.logger.debug(`[p2p] Send status error ${error.message}`);
186 | }
187 | } else {
188 | throw new Error(`[p2p] Status message not defined for ${this.chainId}`);
189 | }
190 | }
191 |
192 | private peerAdded(peer: any) {
193 | const eth = peer.getProtocols()[0];
194 |
195 | this.sendStatus(eth);
196 |
197 | eth.on('message', async (code: any, payload: any[]) => {
198 | if (code === devp2p.ETH.MESSAGE_CODES.TX) {
199 | this.onTransaction(payload);
200 | }
201 | });
202 | }
203 |
204 | private decodeOperation(tx: EthereumTx): Operation {
205 | const data = tx.data.toString('hex');
206 | let result = Operation.OTHER;
207 |
208 | if (data.startsWith(DirectTxPool.ClaimData)) {
209 | result = Operation.CLAIM;
210 | } else if (data.startsWith(DirectTxPool.ExecuteData)) {
211 | result = Operation.EXECUTE;
212 | }
213 |
214 | return result;
215 | }
216 |
217 | private onTransaction(payload: any[]) {
218 | this.logger.debug(`[p2p] Received ${payload.length} transactions`);
219 | try {
220 | for (const rawTx of payload) {
221 | const tx = new EthereumTx(rawTx);
222 | const hash = tx.hash().toString('hex');
223 | const operation = this.decodeOperation(tx);
224 | if (!this.pool.has(hash) && tx.validate(false) && operation !== Operation.OTHER) {
225 | this.logger.debug(`[p2p] Transaction discovered ${hash} to ${tx.to.toString('hex')}`);
226 |
227 | const to = `0x${tx.to.toString('hex')}`;
228 | const gasPrice = new BigNumber(`0x${tx.gasPrice.toString('hex')}`);
229 |
230 | this.pool.set(hash, {
231 | to,
232 | gasPrice,
233 | operation,
234 | timestamp: new Date().getTime()
235 | });
236 | }
237 | }
238 | } catch (e) {
239 | this.logger.error(`[p2p] onTransaction error ${e.message}`);
240 | }
241 | }
242 |
243 | private startListening() {
244 | this.bootstrap(this.bootNodes);
245 | this.rlpx = this.register();
246 |
247 | this.rlpx.on('peer:added', (peer: any) => {
248 | this.peerAdded(peer);
249 | });
250 |
251 | this.rlpx.on('error', (error: any) => this.logger.debug(`[p2p] rlpx error ${error.message}`));
252 | }
253 | }
254 |
--------------------------------------------------------------------------------
/src/TxPool/ITxPool.ts:
--------------------------------------------------------------------------------
1 | import BigNumber from 'bignumber.js';
2 | import { Operation } from '../Types/Operation';
3 |
4 | export interface ITxPoolTxDetails {
5 | to: string;
6 | gasPrice: BigNumber;
7 | timestamp: number;
8 | operation: Operation;
9 | }
10 |
11 | export interface ITxPool {
12 | pool: Map;
13 | running(): boolean;
14 | start(): Promise;
15 | stop(): Promise;
16 | }
17 |
--------------------------------------------------------------------------------
/src/TxPool/TxPool.ts:
--------------------------------------------------------------------------------
1 | import { CLAIMED_EVENT, EXECUTED_EVENT } from '../Actions/Helpers';
2 | import { ILogger } from '../Logger';
3 | import TxPoolProcessor from './TxPoolProcessor';
4 | import Web3 = require('web3');
5 | import { Subscribe, Log, Callback } from 'web3/types';
6 | import { Util } from '@ethereum-alarm-clock/lib';
7 | import { ITxPool, ITxPoolTxDetails } from './ITxPool';
8 |
9 | const SCAN_INTERVAL = 5000;
10 | const TIME_IN_POOL = 5 * 60 * 1000;
11 |
12 | export default class TxPool implements ITxPool {
13 | public pool: Map = new Map();
14 |
15 | private logger: ILogger;
16 | private util: Util;
17 | private web3: Web3;
18 | private txPoolProcessor: TxPoolProcessor;
19 | private subs: Map> = new Map>();
20 | private cleaningTask: NodeJS.Timeout;
21 |
22 | constructor(web3: Web3, util: Util, logger: ILogger) {
23 | this.web3 = web3;
24 | this.logger = logger;
25 | this.util = util;
26 | this.txPoolProcessor = new TxPoolProcessor(this.util, this.logger);
27 | }
28 |
29 | public running() {
30 | return !!this.subs[EXECUTED_EVENT] && !!this.subs[CLAIMED_EVENT];
31 | }
32 |
33 | public async start() {
34 | if (this.running()) {
35 | await this.stop();
36 | }
37 |
38 | await this.watchPending();
39 | this.clearMined();
40 |
41 | this.logger.debug('TxPool started');
42 | }
43 |
44 | public async stop() {
45 | await this.stopTopic(CLAIMED_EVENT);
46 | await this.stopTopic(EXECUTED_EVENT);
47 |
48 | if (this.cleaningTask) {
49 | clearInterval(this.cleaningTask);
50 | }
51 |
52 | this.logger.debug('TxPool STOPPED');
53 | }
54 |
55 | private async stopTopic(topic: string) {
56 | const subscription = this.subs.get(topic);
57 | if (subscription) {
58 | try {
59 | await this.util.stopFilter(this.subs[topic] as Subscribe);
60 | this.subs.delete(topic);
61 | } catch (err) {
62 | this.logger.error(err);
63 | }
64 | }
65 | }
66 |
67 | private async watchPending() {
68 | await this.watchTopic(CLAIMED_EVENT);
69 | await this.watchTopic(EXECUTED_EVENT);
70 | }
71 |
72 | private async watchTopic(topic: string) {
73 | // this is the hack to unlock undocumented toBlock feature used by geth
74 | const subscribe = this.web3.eth.subscribe as (
75 | type: 'logs',
76 | options?: any,
77 | callback?: Callback>
78 | ) => Promise>;
79 | const subscription = await subscribe('logs', { toBlock: 'pending', topics: [topic] });
80 |
81 | if (!this.subs[topic]) {
82 | return;
83 | }
84 |
85 | subscription.on('data', (data: Log) => {
86 | if (data.topics && data.topics.indexOf(topic) !== -1) {
87 | this.txPoolProcessor.process(data, this.pool);
88 | }
89 | });
90 |
91 | subscription.on('error', error => {
92 | this.logger.error(error.toString());
93 | });
94 |
95 | this.subs.set(topic, subscription);
96 | }
97 |
98 | private clearMined() {
99 | this.cleaningTask = setInterval(() => {
100 | const now = new Date().getTime();
101 | this.pool.forEach((value, key) => {
102 | if (now - value.timestamp > TIME_IN_POOL) {
103 | this.pool.delete(key);
104 | }
105 | });
106 | }, SCAN_INTERVAL);
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/src/TxPool/TxPoolProcessor.ts:
--------------------------------------------------------------------------------
1 | import { ILogger, DefaultLogger } from '../Logger';
2 | import { CLAIMED_EVENT, EXECUTED_EVENT } from '../Actions/Helpers';
3 | import { Operation } from '../Types/Operation';
4 | import { ITxPoolTxDetails } from '.';
5 | import { Util } from '@ethereum-alarm-clock/lib';
6 | import BigNumber from 'bignumber.js';
7 | import { Log } from 'web3/types';
8 |
9 | export default class TxPoolProcessor {
10 | private logger: ILogger;
11 | private util: Util;
12 |
13 | constructor(util: Util, logger: ILogger = new DefaultLogger()) {
14 | this.logger = logger;
15 | this.util = util;
16 | }
17 |
18 | public async process(transaction: Log, pool: Map) {
19 | if (!this.hasKnownEvents(transaction)) {
20 | throw new Error('Unknown events');
21 | }
22 |
23 | this.logger.debug(
24 | `Pending transaction discovered ${JSON.stringify(transaction)}`,
25 | transaction.address
26 | );
27 |
28 | const { transactionHash } = transaction;
29 | const operation =
30 | transaction.topics.indexOf(CLAIMED_EVENT) > -1 ? Operation.CLAIM : Operation.EXECUTE;
31 |
32 | if (!pool.has(transactionHash)) {
33 | await this.setTxPoolDetails(pool, transactionHash, operation);
34 | }
35 | }
36 |
37 | private hasKnownEvents(transaction: Log): boolean {
38 | return (
39 | transaction.topics.indexOf(CLAIMED_EVENT) > -1 ||
40 | transaction.topics.indexOf(EXECUTED_EVENT) > -1
41 | );
42 | }
43 |
44 | private async setTxPoolDetails(
45 | pool: Map,
46 | transactionHash: string,
47 | operation: Operation
48 | ) {
49 | try {
50 | const poolDetails = await this.getTxPoolDetails(transactionHash, operation);
51 |
52 | pool.set(transactionHash, poolDetails);
53 | } catch (e) {
54 | this.logger.error(e);
55 | }
56 | }
57 |
58 | private async getTxPoolDetails(
59 | transactionHash: string,
60 | operation: Operation
61 | ): Promise {
62 | const tx = await this.util.getTransaction(transactionHash);
63 | return {
64 | to: tx.to,
65 | gasPrice: new BigNumber(tx.gasPrice),
66 | timestamp: new Date().getTime(),
67 | operation
68 | };
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/TxPool/index.ts:
--------------------------------------------------------------------------------
1 | import DirectTxPool from './DirectTxPool';
2 | import { ITxPool, ITxPoolTxDetails } from './ITxPool';
3 | import TxPool from './TxPool';
4 |
5 | export { DirectTxPool, TxPool, ITxPoolTxDetails, ITxPool };
6 |
--------------------------------------------------------------------------------
/src/Types/ITransactionOptions.ts:
--------------------------------------------------------------------------------
1 | import { BigNumber } from 'bignumber.js';
2 | import { Operation } from './Operation';
3 |
4 | export default interface ITransactionOptions {
5 | to: string;
6 | value: BigNumber;
7 | gas: BigNumber;
8 | gasPrice: BigNumber;
9 | data: string;
10 | operation: Operation;
11 | }
12 |
--------------------------------------------------------------------------------
/src/Types/IntervalId.ts:
--------------------------------------------------------------------------------
1 | export type IntervalId = number;
2 |
--------------------------------------------------------------------------------
/src/Types/Operation.ts:
--------------------------------------------------------------------------------
1 | export enum Operation {
2 | CLAIM,
3 | EXECUTE,
4 | OTHER
5 | }
6 |
--------------------------------------------------------------------------------
/src/Types/index.ts:
--------------------------------------------------------------------------------
1 | export { IntervalId } from './IntervalId';
2 | export type Address = string;
3 |
--------------------------------------------------------------------------------
/src/Version.ts:
--------------------------------------------------------------------------------
1 | type Version = string;
2 |
3 | declare const require: any;
4 |
5 | const version: Version = require('../package.json').version;
6 |
7 | export default version;
8 |
--------------------------------------------------------------------------------
/src/Wallet/AccountState.ts:
--------------------------------------------------------------------------------
1 | import { Operation } from '../Types/Operation';
2 |
3 | export enum TransactionState {
4 | ERROR,
5 | PENDING,
6 | SENT,
7 | CONFIRMED
8 | }
9 |
10 | export interface IAccountState {
11 | set(account: string, to: string, operation: Operation, state: TransactionState): void;
12 | hasPending(account: string): boolean;
13 | isPending(to: string, operation: Operation): boolean;
14 | isSent(to: string, operation: Operation): boolean;
15 | }
16 |
17 | export class AccountState implements IAccountState {
18 | private states: Map> = new Map<
19 | string,
20 | Map
21 | >();
22 |
23 | public set(account: string, to: string, operation: Operation, state: TransactionState) {
24 | const hasAccount = this.states.has(account);
25 | if (!hasAccount) {
26 | this.states.set(account, new Map());
27 | }
28 |
29 | const key = this.createKey(to, operation);
30 | this.states.get(account).set(key, state);
31 | }
32 |
33 | public hasPending(account: string): boolean {
34 | const accountStates = this.states.get(account);
35 | if (!accountStates) {
36 | return false;
37 | }
38 |
39 | return Array.from(accountStates.values()).some(s => s === TransactionState.PENDING);
40 | }
41 |
42 | public isSent(to: string, operation: Operation): boolean {
43 | return this.hasState(to, operation, TransactionState.SENT);
44 | }
45 |
46 | public isPending(to: string, operation: Operation): boolean {
47 | return this.hasState(to, operation, TransactionState.PENDING);
48 | }
49 |
50 | private hasState(to: string, operation: Operation, state: TransactionState): boolean {
51 | const transactions = Array.from(this.states.values());
52 | const key = this.createKey(to, operation);
53 |
54 | return transactions.some(tx => tx.get(key) === state);
55 | }
56 |
57 | private createKey(to: string, operation: Operation): string {
58 | return to.concat('-', operation.toString());
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/Wallet/IWalletReceipt.ts:
--------------------------------------------------------------------------------
1 | import { TxSendStatus } from '../Enum/TxSendStatus';
2 | import { TransactionReceipt } from 'web3/types';
3 |
4 | export interface IWalletReceipt {
5 | receipt?: TransactionReceipt;
6 | from: string;
7 | status: TxSendStatus;
8 | }
9 |
--------------------------------------------------------------------------------
/src/Wallet/TransactionReceiptAwaiter.ts:
--------------------------------------------------------------------------------
1 | import { TransactionReceipt } from 'web3/types';
2 | import { Util } from '@ethereum-alarm-clock/lib';
3 |
4 | const POLL_INTERVAL = 3000;
5 |
6 | export interface ITransactionReceiptAwaiter {
7 | waitForConfirmations(hash: string, blocks: number): Promise;
8 | }
9 |
10 | export class TransactionReceiptAwaiter implements ITransactionReceiptAwaiter {
11 | private util: Util;
12 |
13 | public constructor(util: Util) {
14 | this.util = util;
15 | }
16 |
17 | public async waitForConfirmations(
18 | hash: string,
19 | blocks: number = 12
20 | ): Promise {
21 | return this.awaitTx(hash, {
22 | ensureNotUncle: true,
23 | interval: POLL_INTERVAL,
24 | blocks
25 | });
26 | }
27 |
28 | // tslint:disable-next-line:cognitive-complexity
29 | private awaitTx(hash: string, options: any): Promise {
30 | const interval = options && options.interval ? options.interval : 500;
31 | const transactionReceiptAsync = async (txnHash: string, resolve: any, reject: any) => {
32 | try {
33 | const receipt = this.util.getReceipt(txnHash);
34 | if (!receipt) {
35 | setTimeout(() => {
36 | transactionReceiptAsync(txnHash, resolve, reject);
37 | }, interval);
38 | } else {
39 | if (options && options.ensureNotUncle) {
40 | const resolvedReceipt = await receipt;
41 | if (!resolvedReceipt || !resolvedReceipt.blockNumber) {
42 | setTimeout(() => {
43 | transactionReceiptAsync(txnHash, resolve, reject);
44 | }, interval);
45 | } else {
46 | try {
47 | const block = await this.util.getBlock(resolvedReceipt.blockNumber);
48 | const current = await this.util.getBlock('latest');
49 | if (current.number - block.number >= options.blocks) {
50 | const txn = await this.util.getTransaction(txnHash);
51 | if (txn.blockNumber != null) {
52 | resolve(resolvedReceipt);
53 | } else {
54 | reject(
55 | new Error(
56 | 'Transaction with hash: ' + txnHash + ' ended up in an uncle block.'
57 | )
58 | );
59 | }
60 | } else {
61 | setTimeout(() => {
62 | transactionReceiptAsync(txnHash, resolve, reject);
63 | }, interval);
64 | }
65 | } catch (e) {
66 | setTimeout(() => {
67 | transactionReceiptAsync(txnHash, resolve, reject);
68 | }, interval);
69 | }
70 | }
71 | } else {
72 | resolve(receipt);
73 | }
74 | }
75 | } catch (e) {
76 | reject(e);
77 | }
78 | };
79 |
80 | return new Promise((resolve, reject) => {
81 | transactionReceiptAsync(hash, resolve, reject);
82 | });
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/src/Wallet/Wallet.ts:
--------------------------------------------------------------------------------
1 | // tslint:disable-next-line:no-reference
2 | ///
3 | import { Util } from '@ethereum-alarm-clock/lib';
4 | import ethTx = require('ethereumjs-tx');
5 | import * as ethWallet from 'ethereumjs-wallet';
6 | import { TransactionReceipt } from 'web3/types';
7 |
8 | import { TxSendStatus } from '../Enum/TxSendStatus';
9 | import { DefaultLogger, ILogger } from '../Logger';
10 | import { Address } from '../Types';
11 | import ITransactionOptions from '../Types/ITransactionOptions';
12 | import { Operation } from '../Types/Operation';
13 | import { AccountState, IAccountState, TransactionState } from './AccountState';
14 | import { IWalletReceipt } from './IWalletReceipt';
15 | import { ITransactionReceiptAwaiter, TransactionReceiptAwaiter } from './TransactionReceiptAwaiter';
16 |
17 | export interface V3Wallet {
18 | privKey: any;
19 | getAddressString(): string;
20 | toV3(password: string, opts: object): string;
21 | }
22 |
23 | export class Wallet {
24 | public nonce: number = 0;
25 | public accountState: IAccountState;
26 |
27 | private CONFIRMATION_BLOCKS = 6;
28 | private logger: ILogger;
29 | private accounts: V3Wallet[] = [];
30 | private util: Util;
31 | private transactionAwaiter: ITransactionReceiptAwaiter;
32 |
33 | constructor(
34 | util: Util,
35 | accountState: IAccountState = new AccountState(),
36 | logger: ILogger = new DefaultLogger()
37 | ) {
38 | this.logger = logger;
39 | this.accountState = accountState;
40 | this.util = util;
41 | this.transactionAwaiter = new TransactionReceiptAwaiter(util);
42 | }
43 |
44 | get nextAccount(): V3Wallet {
45 | return this.accounts[this.nonce % this.accounts.length];
46 | }
47 |
48 | public create(numAccounts: number) {
49 | for (let i = 0; i < numAccounts; i++) {
50 | const wallet = ethWallet.generate();
51 | this.add(wallet);
52 | }
53 | }
54 |
55 | public add(wallet: any) {
56 | const address = wallet.getAddressString();
57 |
58 | if (!this.accounts.some(a => a.getAddressString() === address)) {
59 | this.accounts.push(wallet);
60 | }
61 |
62 | return wallet;
63 | }
64 |
65 | public encrypt(password: string, opts: object) {
66 | return this.accounts.map(wallet => wallet.toV3(password, opts));
67 | }
68 |
69 | public loadPrivateKeys(privateKeys: string[]) {
70 | privateKeys.forEach(privateKey => {
71 | const wallet = ethWallet.fromPrivateKey(Buffer.from(privateKey, 'hex'));
72 |
73 | if (wallet) {
74 | this.add(wallet);
75 | } else {
76 | throw new Error("Couldn't load private key.");
77 | }
78 | });
79 | }
80 |
81 | public decrypt(encryptedKeyStores: (string | object)[], password: string) {
82 | encryptedKeyStores.forEach(keyStore => {
83 | keyStore = typeof keyStore === 'object' ? JSON.stringify(keyStore) : keyStore;
84 | const wallet = ethWallet.fromV3(keyStore, password, true);
85 |
86 | if (wallet) {
87 | this.add(wallet);
88 | } else {
89 | throw new Error("Couldn't decrypt key store. Wrong password?");
90 | }
91 | });
92 | }
93 |
94 | /**
95 | * sendFromNext will send a transaction from the account in this wallet that is next according to this.nonce
96 | * @param {TransactionParams} opts {to, value, gas, gasPrice, data}
97 | * @returns {Promise} A promise which will resolve to the transaction receipt
98 | */
99 | public sendFromNext(opts: any): Promise {
100 | const next = this.nonce++ % this.accounts.length;
101 |
102 | return this.sendFromIndex(next, opts);
103 | }
104 |
105 | public getNonce(account: string): Promise {
106 | return this.util.getTransactionCount(account);
107 | }
108 |
109 | public isWalletAbleToSendTx(idx: number): boolean {
110 | if (this.accounts[idx] === undefined) {
111 | throw new Error('Index is outside range of addresses.');
112 | }
113 |
114 | const from: string = this.accounts[idx].getAddressString();
115 |
116 | return this.isAccountAbleToSendTx(from);
117 | }
118 |
119 | public isAccountAbleToSendTx(account: Address): boolean {
120 | return !this.accountState.hasPending(account);
121 | }
122 |
123 | public isWaitingForConfirmation(to: Address, operation: Operation): boolean {
124 | return this.accountState.isSent(to, operation);
125 | }
126 |
127 | public isNextAccountFree(): boolean {
128 | return this.isWalletAbleToSendTx(this.nonce % this.accounts.length);
129 | }
130 |
131 | public hasPendingTransaction(to: string, operation: Operation): boolean {
132 | return this.accountState.isPending(to, operation);
133 | }
134 |
135 | public async sendFromIndex(idx: number, opts: ITransactionOptions): Promise {
136 | if (this.accounts[idx] === undefined) {
137 | throw new Error('Index is outside range of addresses.');
138 | }
139 |
140 | const account = this.accounts[idx];
141 | const from: string = account.getAddressString();
142 | return this.sendFromAccount(from, opts);
143 | }
144 |
145 | public async sendFromAccount(from: Address, opts: ITransactionOptions): Promise {
146 | if (this.hasPendingTransaction(opts.to, opts.operation)) {
147 | return {
148 | from,
149 | status: TxSendStatus.PROGRESS
150 | };
151 | }
152 |
153 | const balance = await this.util.balanceOf(from);
154 |
155 | if (balance.eq(0)) {
156 | this.logger.info(`${TxSendStatus.NOT_ENOUGH_FUNDS} ${from}`);
157 | return {
158 | from,
159 | status: TxSendStatus.NOT_ENOUGH_FUNDS
160 | };
161 | }
162 |
163 | const nonce = this.util.toHex(await this.getNonce(from));
164 | const v3Wallet = this.accounts.find((wallet: V3Wallet) => {
165 | return wallet.getAddressString() === from;
166 | });
167 | const signedTx = await this.signTransaction(v3Wallet, nonce, opts);
168 |
169 | if (!this.isAccountAbleToSendTx(from)) {
170 | return {
171 | from,
172 | status: TxSendStatus.BUSY
173 | };
174 | }
175 |
176 | let hash: string;
177 | let receipt: TransactionReceipt;
178 |
179 | try {
180 | this.accountState.set(from, opts.to, opts.operation, TransactionState.PENDING);
181 |
182 | this.logger.info(`Sending ${Operation[opts.operation]}`, opts.to);
183 | this.logger.debug(`Tx: ${JSON.stringify(signedTx)}`);
184 |
185 | const res = await this.sendRawTransaction(signedTx);
186 | hash = res.hash;
187 |
188 | if (res.error && !this.isEVMError(res.error)) {
189 | throw res.error;
190 | }
191 |
192 | receipt = await this.transactionAwaiter.waitForConfirmations(hash, 1);
193 |
194 | this.accountState.set(from, opts.to, opts.operation, TransactionState.SENT);
195 |
196 | this.logger.debug(`Receipt: ${JSON.stringify(receipt)}`);
197 | } catch (error) {
198 | this.accountState.set(from, opts.to, opts.operation, TransactionState.ERROR);
199 | this.logger.error(error, opts.to);
200 | return {
201 | from,
202 | status: TxSendStatus.UNKNOWN_ERROR
203 | };
204 | }
205 |
206 | try {
207 | this.logger.debug(`Awaiting for confirmation for tx ${hash} from ${from}`, opts.to);
208 |
209 | receipt = await this.transactionAwaiter.waitForConfirmations(hash, this.CONFIRMATION_BLOCKS);
210 | this.accountState.set(from, opts.to, opts.operation, TransactionState.CONFIRMED);
211 |
212 | this.logger.debug(`Transaction ${hash} from ${from} confirmed`, opts.to);
213 | } catch (error) {
214 | this.accountState.set(from, opts.to, opts.operation, TransactionState.ERROR);
215 | return {
216 | from,
217 | status: TxSendStatus.MINED_IN_UNCLE
218 | };
219 | }
220 |
221 | const status = this.isTransactionStatusSuccessful(receipt)
222 | ? TxSendStatus.OK
223 | : TxSendStatus.FAIL;
224 |
225 | return { receipt, from, status };
226 | }
227 |
228 | public getAccounts(): V3Wallet[] {
229 | return this.accounts;
230 | }
231 |
232 | public getAddresses(): string[] {
233 | return this.accounts.map(account => account.getAddressString());
234 | }
235 |
236 | public isKnownAddress(address: string): boolean {
237 | return this.getAddresses().some(addr => addr === address);
238 | }
239 |
240 | public async sendRawTransaction(tx: ethTx): Promise<{ hash: string; error?: Error }> {
241 | const serialized = '0x'.concat(tx.serialize().toString('hex'));
242 |
243 | const sending = this.util.sendRawTransaction(serialized);
244 |
245 | return new Promise<{ hash: string; error?: Error }>(resolve =>
246 | sending
247 | .once('transactionHash', receipt => resolve({ hash: receipt }))
248 | .on('error', error => resolve({ hash: tx.hash().toString('hex'), error }))
249 | );
250 | }
251 |
252 | private async signTransaction(from: V3Wallet, nonce: number | string, opts: any): Promise {
253 | const params = {
254 | nonce,
255 | from: from.getAddressString(),
256 | to: opts.to,
257 | gas: this.util.toHex(opts.gas),
258 | gasPrice: this.util.toHex(opts.gasPrice),
259 | value: this.util.toHex(opts.value),
260 | data: opts.data
261 | };
262 |
263 | const tx = new ethTx(params);
264 | tx.sign(Buffer.from(from.privKey, 'hex'));
265 |
266 | return tx;
267 | }
268 |
269 | private isTransactionStatusSuccessful(receipt: TransactionReceipt): boolean {
270 | if (receipt) {
271 | return [true, 1, '0x1', '0x01'].indexOf(receipt.status) !== -1;
272 | }
273 |
274 | return false;
275 | }
276 |
277 | private isEVMError(error: Error): boolean {
278 | return error && error.message.includes('reverted by the EVM');
279 | }
280 | }
281 |
--------------------------------------------------------------------------------
/src/Wallet/index.ts:
--------------------------------------------------------------------------------
1 | export { Wallet } from './Wallet';
2 | export { IWalletReceipt } from './IWalletReceipt';
3 |
--------------------------------------------------------------------------------
/src/WsReconnect/index.ts:
--------------------------------------------------------------------------------
1 | import TimeNode from '../TimeNode';
2 | import { ReconnectMsg } from '../Enum';
3 | import { EAC, Util } from '@ethereum-alarm-clock/lib';
4 | import Web3 = require('web3');
5 |
6 | declare const setTimeout: any;
7 |
8 | export default class WsReconnect {
9 | private timeNode: TimeNode;
10 | private reconnectTries: number = 0;
11 | private reconnecting: boolean = false;
12 | private reconnected: boolean = false;
13 |
14 | constructor(timeNode: TimeNode) {
15 | this.timeNode = timeNode;
16 | }
17 |
18 | public setup(): void {
19 | const {
20 | logger,
21 | web3: { currentProvider }
22 | } = this.timeNode.config;
23 |
24 | // apparently provider has method .on even though types don't show it
25 | const p = currentProvider as any;
26 |
27 | p.on('error', (err: any) => {
28 | logger.debug(`[WS ERROR] ${err}`);
29 | p._timeout();
30 |
31 | setTimeout(async () => {
32 | const msg: ReconnectMsg = await this.handleWsDisconnect();
33 | logger.debug(`[WS RECONNECT] ${msg}`);
34 | }, this.reconnectTries * 1000);
35 | });
36 |
37 | p.on('end', (err: any) => {
38 | logger.debug(`[WS END] Type= ${err.type} Reason= ${err.reason}`);
39 | p._timeout();
40 |
41 | setTimeout(async () => {
42 | const msg = await this.handleWsDisconnect();
43 | logger.debug(`[WS RECONNECT] ${msg}`);
44 | }, this.reconnectTries * 1000);
45 | });
46 | }
47 |
48 | private async handleWsDisconnect(): Promise {
49 | if (this.reconnected) {
50 | return ReconnectMsg.ALREADY_RECONNECTED;
51 | }
52 | if (this.reconnectTries >= this.timeNode.config.maxRetries) {
53 | await this.timeNode.stopScanning();
54 | return ReconnectMsg.MAX_ATTEMPTS;
55 | }
56 | if (this.reconnecting) {
57 | return ReconnectMsg.RECONNECTING;
58 | }
59 |
60 | // Try to reconnect.
61 | this.reconnecting = true;
62 | const nextProviderUrl = await this.wsReconnect();
63 | if (nextProviderUrl) {
64 | this.timeNode.config.activeProviderUrl = nextProviderUrl;
65 | await this.timeNode.startScanning();
66 | this.reconnectTries = 0;
67 | this.setup();
68 | this.reconnected = true;
69 | this.reconnecting = false;
70 | setTimeout(() => {
71 | this.reconnected = false;
72 | }, 10000);
73 | return ReconnectMsg.RECONNECTED;
74 | }
75 |
76 | this.reconnecting = false;
77 | this.reconnectTries++;
78 | setTimeout(() => {
79 | this.handleWsDisconnect();
80 | }, this.reconnectTries * 1000);
81 |
82 | return ReconnectMsg.FAIL;
83 | }
84 |
85 | private async wsReconnect(): Promise {
86 | const {
87 | config: { logger, providerUrls }
88 | } = this.timeNode;
89 | logger.debug('Attempting WS Reconnect.');
90 | try {
91 | const providerUrl = providerUrls[this.reconnectTries % providerUrls.length];
92 | this.timeNode.config.web3.setProvider(new Web3.providers.WebsocketProvider(providerUrl));
93 | this.timeNode.config.eac = new EAC(this.timeNode.config.web3);
94 | if (await Util.isWatchingEnabled(this.timeNode.config.web3)) {
95 | return providerUrl;
96 | } else {
97 | throw new Error('Invalid providerUrl! eth_getFilterLogs not enabled.');
98 | }
99 | } catch (err) {
100 | logger.error(err.message);
101 | logger.info(`Reconnect tries: ${this.reconnectTries}`);
102 | return null;
103 | }
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/src/global.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'ethereumjs-wallet';
2 | declare module 'ethereum-common';
3 | declare module 'ethereumjs-devp2p';
4 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import Config from './Config';
2 | import TimeNode from './TimeNode';
3 | import { Wallet } from './Wallet';
4 | import version from './Version';
5 |
6 | export { Config, TimeNode, Wallet, version };
7 |
--------------------------------------------------------------------------------
/test/e2e/TestConfig.ts:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai';
2 | import { mockConfig } from '../helpers';
3 |
4 | if (process.env.RUN_ONLY_OPTIONAL_TESTS !== 'true') {
5 | describe('Config', () => {
6 | it('creates a config from standard params', async () => {
7 | const config = await mockConfig();
8 | expect(config).to.exist; // tslint:disable-line no-unused-expression
9 | }).timeout(10000);
10 | });
11 | }
12 |
--------------------------------------------------------------------------------
/test/e2e/TestCreateWallet.ts:
--------------------------------------------------------------------------------
1 | import { assert } from 'chai';
2 |
3 | import { createWallet, createWalletKeystore } from '../helpers';
4 |
5 | // tslint:disable-next-line:no-hardcoded-credentials
6 | const password = 'password123';
7 |
8 | if (process.env.RUN_ONLY_OPTIONAL_TESTS !== 'true') {
9 | describe('CreateWallet', () => {
10 | it('creates a new wallet', () => {
11 | const wallet = createWallet(1);
12 | assert.exists(wallet);
13 | });
14 |
15 | it('creates a new encrypted wallet', () => {
16 | const wallet = createWalletKeystore(1, password);
17 | assert.exists(wallet);
18 | });
19 | });
20 | }
21 |
--------------------------------------------------------------------------------
/test/e2e/TestScheduleTx.ts:
--------------------------------------------------------------------------------
1 | import { Util } from '@ethereum-alarm-clock/lib';
2 | import { expect } from 'chai';
3 | import { providerUrl } from '../helpers';
4 | import { getHelperMethods } from '../helpers/Helpers';
5 | import { scheduleTestTx } from '../helpers/scheduleTestTx';
6 |
7 | const web3 = Util.getWeb3FromProviderUrl(providerUrl);
8 |
9 | if (process.env.RUN_ONLY_OPTIONAL_TESTS !== 'true') {
10 | describe('ScheduleTx', () => {
11 | it('schedules a basic transaction', async () => {
12 | const { withSnapshotRevert } = getHelperMethods(web3);
13 |
14 | await withSnapshotRevert(async () => {
15 | const receipt = await scheduleTestTx();
16 |
17 | expect(receipt).to.exist; // tslint:disable-line no-unused-expression
18 | });
19 | }).timeout(20000);
20 | });
21 | }
22 |
--------------------------------------------------------------------------------
/test/e2e/TestTimeNode.ts:
--------------------------------------------------------------------------------
1 | import { EAC } from '@ethereum-alarm-clock/lib';
2 | import { assert, expect } from 'chai';
3 | import Web3 = require('web3');
4 |
5 | import { Config, TimeNode } from '../../src';
6 | import { mockConfig } from '../helpers';
7 | import { getHelperMethods } from '../helpers/Helpers';
8 | import { scheduleTestTx } from '../helpers/scheduleTestTx';
9 |
10 | const TIMENODE_ADDRESS = '0x487a54e1d033db51c8ee8c03edac2a0f8a6892c6';
11 |
12 | // tslint:disable-next-line:no-big-function
13 | describe('TimeNode', () => {
14 | let config: Config;
15 | let myAccount: string;
16 | let eac: EAC;
17 | let web3: Web3;
18 | let withSnapshotRevert: any;
19 | let timeNode: TimeNode;
20 |
21 | before(async () => {
22 | config = await mockConfig();
23 | myAccount = config.wallet.getAddresses()[0];
24 | eac = config.eac;
25 | web3 = config.web3;
26 |
27 | const helpers = getHelperMethods(web3);
28 | withSnapshotRevert = helpers.withSnapshotRevert;
29 |
30 | timeNode = new TimeNode(config);
31 | });
32 |
33 | it('starts a basic timenode', () => {
34 | expect(timeNode.scanner.scanning).to.equal(false);
35 | }).timeout(200000);
36 |
37 | it('starts scanning', async () => {
38 | await timeNode.startScanning();
39 | expect(timeNode.scanner.scanning).to.equal(true);
40 |
41 | await timeNode.stopScanning();
42 | });
43 |
44 | if (process.env.RUN_ONLY_OPTIONAL_TESTS === 'true') {
45 | it('executes 30 transactions', async () => {
46 | const TRANSACTIONS_TO_SCHEDULE = 30;
47 | const scheduledTransactionsMap = {};
48 |
49 | for (let i = 0; i < TRANSACTIONS_TO_SCHEDULE; i++) {
50 | const transactionAddress: string = (await scheduleTestTx(270 + 5 * i)).toLowerCase();
51 |
52 | scheduledTransactionsMap[transactionAddress] = {
53 | executionLogged: false
54 | };
55 | }
56 |
57 | await timeNode.startScanning();
58 |
59 | console.log('SCHEDULED TX ADDRESSES TO EXECUTE', scheduledTransactionsMap);
60 |
61 | timeNode.config.logger.info = (msg: any, txRequest: string) => {
62 | txRequest = txRequest.toLowerCase();
63 |
64 | if (msg.includes && msg.includes('EXECUTED') && scheduledTransactionsMap[txRequest]) {
65 | scheduledTransactionsMap[txRequest].executionLogged = true;
66 | }
67 | console.log(txRequest, msg);
68 | };
69 |
70 | let allExecutionsLogged = false;
71 |
72 | await new Promise(resolve => {
73 | const allExecutionsLoggedCheckInterval = setInterval(async () => {
74 | for (const txAddress in scheduledTransactionsMap) {
75 | if (!scheduledTransactionsMap.hasOwnProperty(txAddress)) {
76 | continue;
77 | }
78 |
79 | allExecutionsLogged =
80 | scheduledTransactionsMap[txAddress] &&
81 | scheduledTransactionsMap[txAddress].executionLogged;
82 | }
83 |
84 | if (allExecutionsLogged) {
85 | await timeNode.stopScanning();
86 |
87 | for (const transactionAddress in scheduledTransactionsMap) {
88 | if (!scheduledTransactionsMap.hasOwnProperty(transactionAddress)) {
89 | continue;
90 | }
91 |
92 | const transactionRequest = eac.transactionRequest(transactionAddress);
93 |
94 | await transactionRequest.fillData();
95 |
96 | assert.ok(transactionRequest.wasCalled, `${transactionAddress} hasn't been called!`);
97 | assert.ok(
98 | transactionRequest.wasSuccessful,
99 | `${transactionAddress} isn't successful!`
100 | );
101 | }
102 |
103 | clearInterval(allExecutionsLoggedCheckInterval);
104 |
105 | resolve();
106 | }
107 | }, 1000);
108 | });
109 |
110 | assert.ok(
111 | allExecutionsLogged,
112 | `All transactions' executions should be logged, but they weren't.`
113 | );
114 |
115 | console.log('FINAL STATUS OF MASS TX EXECUTION:', scheduledTransactionsMap);
116 | }).timeout(600000);
117 | } else {
118 | it('claims and executes transaction', async () => {
119 | await withSnapshotRevert(async () => {
120 | await timeNode.startScanning();
121 |
122 | const TEST_TX_ADDRESS = (await scheduleTestTx()).toLowerCase();
123 | const TEST_TX_REQUEST = eac.transactionRequest(TEST_TX_ADDRESS);
124 |
125 | await TEST_TX_REQUEST.fillData();
126 |
127 | console.log('SCHEDULED TX ADDRESS TO CLAIM', TEST_TX_ADDRESS);
128 |
129 | const originalLoggerInfoMethod = timeNode.config.logger.info;
130 | let claimedLogged = false;
131 | let executionLogged = false;
132 |
133 | timeNode.config.logger.info = (msg: any, txRequest: string) => {
134 | txRequest = txRequest && txRequest.toLowerCase();
135 |
136 | if (msg === 'CLAIMED.' && txRequest === TEST_TX_ADDRESS) {
137 | claimedLogged = true;
138 | }
139 |
140 | if (msg === 'EXECUTED.' && txRequest === TEST_TX_ADDRESS) {
141 | executionLogged = true;
142 | }
143 |
144 | console.log(txRequest, msg);
145 | };
146 |
147 | await new Promise((resolve, reject) => {
148 | const claimedLoggedInterval = setInterval(async () => {
149 | if (!claimedLogged) {
150 | return;
151 | }
152 |
153 | clearInterval(claimedLoggedInterval);
154 |
155 | await TEST_TX_REQUEST.refreshData();
156 |
157 | assert.ok(TEST_TX_REQUEST.isClaimed, `${TEST_TX_ADDRESS} hasn't been claimed!`);
158 | expect(TEST_TX_REQUEST.address).to.equal(TEST_TX_ADDRESS);
159 | expect(TEST_TX_REQUEST.claimData).to.equal('0x4e71d92d');
160 | expect(TEST_TX_REQUEST.claimedBy).to.equal(TIMENODE_ADDRESS);
161 | expect(timeNode.getClaimedNotExecutedTransactions()[myAccount]).to.include(
162 | TEST_TX_ADDRESS
163 | );
164 | }, 1000);
165 |
166 | const executionLoggedInterval = setInterval(async () => {
167 | if (!executionLogged) {
168 | return;
169 | }
170 |
171 | if (!claimedLogged) {
172 | reject(`Transaction hasn't been claimed before being executed.`);
173 | }
174 |
175 | await timeNode.stopScanning();
176 |
177 | clearInterval(executionLoggedInterval);
178 |
179 | await TEST_TX_REQUEST.refreshData();
180 |
181 | assert.ok(TEST_TX_REQUEST.wasCalled, `${TEST_TX_ADDRESS} hasn't been called!`);
182 | assert.ok(TEST_TX_REQUEST.wasSuccessful, `${TEST_TX_ADDRESS} isn't successful!`);
183 |
184 | expect(timeNode.getClaimedNotExecutedTransactions()[myAccount]).to.not.include(
185 | TEST_TX_ADDRESS
186 | );
187 |
188 | timeNode.config.logger.info = originalLoggerInfoMethod;
189 |
190 | resolve();
191 | }, 1000);
192 | });
193 | });
194 | }).timeout(400000);
195 | }
196 | });
197 |
--------------------------------------------------------------------------------
/test/helpers/Helpers.ts:
--------------------------------------------------------------------------------
1 | import Web3 = require('web3');
2 | import { JsonRPCResponse } from 'web3/providers';
3 |
4 | export const getHelperMethods = (web3: Web3) => {
5 | function sendRpc(method: any, params?: any): Promise {
6 | return new Promise((resolve, reject) => {
7 | web3.currentProvider.send(
8 | {
9 | jsonrpc: '2.0',
10 | method,
11 | params: params || [],
12 | id: new Date().getTime()
13 | },
14 | Object.assign((err: Error, res: JsonRPCResponse) => resolve(res))
15 | );
16 | });
17 | }
18 |
19 | function takeSnapshot(): Promise {
20 | return sendRpc('evm_snapshot').then(res => res.result);
21 | }
22 |
23 | function revertSnapshot(id: number): Promise {
24 | return sendRpc('evm_revert', id).then(res => res.result);
25 | }
26 |
27 | async function withSnapshotRevert(fn: any): Promise {
28 | const snapshot = await takeSnapshot();
29 | try {
30 | await fn();
31 | } catch (error) {
32 | console.log(`Error ${error} in withSnapshotRevert`);
33 | }
34 | return revertSnapshot(snapshot);
35 | }
36 |
37 | return { withSnapshotRevert };
38 | };
39 |
--------------------------------------------------------------------------------
/test/helpers/MockTxRequest.ts:
--------------------------------------------------------------------------------
1 | import BigNumber from 'bignumber.js';
2 | import * as moment from 'moment';
3 | import { TxStatus, FnSignatures } from '../../src/Enum';
4 | import Web3 = require('web3');
5 | import { ITransactionRequest } from '@ethereum-alarm-clock/lib';
6 |
7 | const mockTxRequest = async (
8 | web3: Web3,
9 | isBlock: boolean = false
10 | ): Promise => {
11 | const claimedBy = '0x0000000000000000000000000000000000000000';
12 | const requiredDeposit = new BigNumber(web3.utils.toWei('0.1', 'ether'));
13 |
14 | const hoursLater = (num: number) =>
15 | moment()
16 | .add(num, 'hour')
17 | .unix();
18 | const daysLater = (num: number) =>
19 | moment()
20 | .add(num, 'day')
21 | .unix();
22 |
23 | const currentBlockNumber = await web3.eth.getBlockNumber();
24 | const blocksLater = (num: number) => currentBlockNumber + num;
25 | const oneHourWindowSize = new BigNumber(isBlock ? 255 : 3600);
26 |
27 | return {
28 | address: '0x24f8e3501b00bd219e864650f5625cd4f9272a25',
29 | bounty: new BigNumber(web3.utils.toWei('0.1', 'ether')),
30 | callGas: new BigNumber(Math.pow(10, 6)),
31 | gasPrice: new BigNumber(web3.utils.toWei('21', 'gwei')),
32 | claimedBy,
33 | isClaimed: false,
34 | requiredDeposit,
35 | temporalUnit: isBlock ? 1 : 2,
36 | claimWindowStart: new BigNumber(isBlock ? blocksLater(100) : hoursLater(1)),
37 | windowStart: new BigNumber(isBlock ? blocksLater(300) : daysLater(1)),
38 | windowSize: oneHourWindowSize,
39 | freezePeriod: oneHourWindowSize, // ~1h
40 | reservedWindowSize: oneHourWindowSize,
41 | wasCalled: false,
42 | get claimData() {
43 | return FnSignatures.claim;
44 | },
45 | get claimWindowEnd() {
46 | return this.windowStart.minus(this.freezePeriod);
47 | },
48 | get freezePeriodEnd() {
49 | return this.claimWindowEnd.plus(this.freezePeriod);
50 | },
51 | get reservedWindowEnd() {
52 | return this.windowStart.plus(this.reservedWindowSize);
53 | },
54 | get executionWindowEnd() {
55 | return this.windowStart.plus(this.windowSize);
56 | },
57 | async claimPaymentModifier(): Promise {
58 | return new BigNumber(100);
59 | },
60 | isClaimedBy(address: string) {
61 | return this.claimedBy === address;
62 | },
63 | async beforeClaimWindow(): Promise {
64 | return this.claimWindowStart.isGreaterThan(await this.now());
65 | },
66 | async inClaimWindow() {
67 | const now = await this.now();
68 | return (
69 | this.claimWindowStart.isLessThanOrEqualTo(now) && this.claimWindowEnd.isGreaterThan(now)
70 | );
71 | },
72 | async inFreezePeriod() {
73 | const now = await this.now();
74 | return (
75 | this.claimWindowEnd.isLessThanOrEqualTo(now) && this.freezePeriodEnd.isGreaterThan(now)
76 | );
77 | },
78 | async inExecutionWindow() {
79 | const now = await this.now();
80 | return (
81 | this.windowStart.isLessThanOrEqualTo(now) &&
82 | this.executionWindowEnd.isGreaterThanOrEqualTo(now)
83 | );
84 | },
85 | async inReservedWindow() {
86 | const now = await this.now();
87 | return this.windowStart.isLessThanOrEqualTo(now) && this.reservedWindowEnd.isGreaterThan(now);
88 | },
89 | async now(): Promise {
90 | return new BigNumber(isBlock ? new BigNumber(currentBlockNumber) : moment().unix());
91 | },
92 | async refreshData(): Promise {
93 | return true;
94 | },
95 | executeData: '',
96 | isCancelled: false
97 | } as ITransactionRequest;
98 | };
99 |
100 | const mockTxStatus = async (
101 | tx: ITransactionRequest,
102 | status: TxStatus
103 | ): Promise => {
104 | if (status === TxStatus.BeforeClaimWindow) {
105 | return tx;
106 | }
107 |
108 | if (status === TxStatus.ClaimWindow) {
109 | const claimWindowStart =
110 | tx.temporalUnit === 1
111 | ? 0
112 | : moment()
113 | .subtract(1, 'hour')
114 | .unix();
115 | tx.claimWindowStart = new BigNumber(claimWindowStart);
116 | }
117 |
118 | if (status === TxStatus.FreezePeriod) {
119 | tx.claimWindowStart = tx.claimWindowStart.minus(tx.freezePeriod);
120 | }
121 |
122 | if (status === TxStatus.ExecutionWindow) {
123 | tx.isClaimed = true;
124 | tx.windowStart = await tx.now();
125 | }
126 |
127 | if (status === TxStatus.Executed) {
128 | const windowStarts =
129 | tx.temporalUnit === 1
130 | ? 0
131 | : moment()
132 | .subtract(1, 'week')
133 | .unix();
134 |
135 | tx.isClaimed = true;
136 | tx.wasCalled = true;
137 | tx.claimWindowStart = new BigNumber(windowStarts);
138 | tx.windowStart = new BigNumber(windowStarts);
139 | if (tx.temporalUnit === 1) {
140 | tx.windowSize = new BigNumber(windowStarts);
141 | }
142 | }
143 |
144 | if (status === TxStatus.Missed) {
145 | const windowStarts =
146 | tx.temporalUnit === 1
147 | ? 0
148 | : moment()
149 | .subtract(1, 'week')
150 | .unix();
151 |
152 | tx.claimWindowStart = new BigNumber(windowStarts);
153 | tx.windowStart = new BigNumber(windowStarts);
154 | if (tx.temporalUnit === 1) {
155 | tx.windowSize = new BigNumber(windowStarts);
156 | }
157 | }
158 |
159 | return tx;
160 | };
161 |
162 | export { mockTxRequest, mockTxStatus };
163 |
--------------------------------------------------------------------------------
/test/helpers/createWallet.ts:
--------------------------------------------------------------------------------
1 | import { Wallet } from '../../src/index';
2 |
3 | export function createWalletKeystore(num: number, password: string) {
4 | const wallet = new Wallet(null, null, null);
5 | wallet.create(num);
6 |
7 | return wallet.encrypt(password, {});
8 | }
9 |
10 | export function createWallet(num: number) {
11 | const wallet = new Wallet(null, null, null);
12 | wallet.create(num);
13 | return wallet;
14 | }
15 |
--------------------------------------------------------------------------------
/test/helpers/index.ts:
--------------------------------------------------------------------------------
1 | export { mockTxRequest, mockTxStatus } from './MockTxRequest';
2 | export { mockConfig, PRIVATE_KEY } from './mockConfig';
3 | export { createWallet, createWalletKeystore } from './createWallet';
4 | export { providerUrl } from './network';
5 |
--------------------------------------------------------------------------------
/test/helpers/mockConfig.ts:
--------------------------------------------------------------------------------
1 | import * as loki from 'lokijs';
2 | import { Config } from '../../src/index';
3 | import { providerUrl } from './network';
4 | import { DefaultLogger } from '../../src/Logger';
5 |
6 | const PRIVATE_KEY = 'fdf2e15fd858d9d81e31baa1fe76de9c7d49af0018a1322aa2b9e493b02afa26';
7 |
8 | const mockConfig = async () => {
9 | // tslint:disable-next-line:no-hardcoded-credentials
10 | const password = 'password123';
11 | const wallet = [PRIVATE_KEY];
12 | const config = new Config({
13 | autostart: true,
14 | claiming: true,
15 | logger: new DefaultLogger(),
16 | ms: 4000,
17 | password,
18 | providerUrls: [providerUrl],
19 | scanSpread: 0,
20 | statsDb: new loki('stats.db'),
21 | walletStores: wallet,
22 | walletStoresAsPrivateKeys: true
23 | });
24 |
25 | await config.statsDbLoaded;
26 | return config;
27 | };
28 |
29 | export { mockConfig, PRIVATE_KEY };
30 |
--------------------------------------------------------------------------------
/test/helpers/network.ts:
--------------------------------------------------------------------------------
1 | export const providerUrl =
2 | process && process.env.PROVIDER_URL ? process.env.PROVIDER_URL : 'ws://localhost:8545';
3 |
--------------------------------------------------------------------------------
/test/helpers/scheduleTestTx.ts:
--------------------------------------------------------------------------------
1 | import { EAC, Util } from '@ethereum-alarm-clock/lib';
2 | import BigNumber from 'bignumber.js';
3 | import { providerUrl } from '../helpers';
4 |
5 | const web3 = Util.getWeb3FromProviderUrl(providerUrl);
6 |
7 | export const SCHEDULED_TX_PARAMS = {
8 | callValue: new BigNumber(Math.pow(10, 18))
9 | };
10 |
11 | export const scheduleTestTx = async (blocksInFuture = 270) => {
12 | const eac = new EAC(web3);
13 |
14 | const { callValue } = SCHEDULED_TX_PARAMS;
15 |
16 | const callGas = new BigNumber(1000000);
17 | const gasPrice = new BigNumber(web3.utils.toWei('20', 'gwei'));
18 | const fee = new BigNumber(0);
19 | const bounty = new BigNumber(web3.utils.toWei('0.1', 'ether'));
20 |
21 | const accounts = await web3.eth.getAccounts();
22 | const mainAccount = accounts[0];
23 |
24 | const receipt = await eac.schedule({
25 | from: mainAccount,
26 | toAddress: mainAccount,
27 | callGas,
28 | callValue,
29 | windowSize: new BigNumber(30),
30 | windowStart: new BigNumber((await web3.eth.getBlockNumber()) + blocksInFuture),
31 | gasPrice,
32 | fee,
33 | bounty,
34 | requiredDeposit: new BigNumber('0'),
35 | timestampScheduling: false
36 | });
37 |
38 | return eac.getTxRequestFromReceipt(receipt);
39 | };
40 |
--------------------------------------------------------------------------------
/test/mocha.opts:
--------------------------------------------------------------------------------
1 | --require ts-node/register
2 | --require source-map-support/register
3 | --recursive
--------------------------------------------------------------------------------
/test/unit/UnitTestAccountState.ts:
--------------------------------------------------------------------------------
1 | import { AccountState, TransactionState } from '../../src/Wallet/AccountState';
2 | import { assert } from 'chai';
3 | import { Operation } from '../../src/Types/Operation';
4 |
5 | describe('Account State Unit Tests', () => {
6 | describe('hasPending', () => {
7 | it('returns false when empty', () => {
8 | const accountState = new AccountState();
9 | const result = accountState.hasPending('0x0');
10 |
11 | assert.isFalse(result);
12 | });
13 |
14 | it('returns false when different account has pending', () => {
15 | const accountState = new AccountState();
16 | accountState.set('0x1', '0x0', Operation.CLAIM, TransactionState.PENDING);
17 |
18 | const result = accountState.hasPending('0x0');
19 |
20 | assert.isFalse(result);
21 | });
22 |
23 | it('returns true when account has pending transaction', () => {
24 | const accountState = new AccountState();
25 | const pendingAccount = '0x1';
26 | accountState.set(pendingAccount, '0x0', Operation.CLAIM, TransactionState.PENDING);
27 |
28 | const result = accountState.hasPending(pendingAccount);
29 |
30 | assert.isTrue(result);
31 | });
32 | });
33 |
34 | describe('isSent', () => {
35 | it('returns false when empty', () => {
36 | const accountState = new AccountState();
37 | const tx = '0x0';
38 |
39 | const result = accountState.isSent(tx, Operation.CLAIM);
40 |
41 | assert.isFalse(result);
42 | });
43 |
44 | it('returns false when no sent', () => {
45 | const accountState = new AccountState();
46 | const tx = '0x0';
47 |
48 | accountState.set('0x1', tx, Operation.CLAIM, TransactionState.PENDING);
49 |
50 | const result = accountState.isSent(tx, Operation.CLAIM);
51 |
52 | assert.isFalse(result);
53 | });
54 |
55 | it('returns true when sent', () => {
56 | const accountState = new AccountState();
57 | const tx = '0x0';
58 | accountState.set('0x1', tx, Operation.CLAIM, TransactionState.PENDING);
59 | accountState.set('0x1', tx, Operation.CLAIM, TransactionState.SENT);
60 | accountState.set('0x1', tx, Operation.EXECUTE, TransactionState.SENT);
61 |
62 | const result = accountState.isSent(tx, Operation.EXECUTE);
63 |
64 | assert.isTrue(result);
65 | });
66 |
67 | it('returns false when no sent and other are sent', () => {
68 | const accountState = new AccountState();
69 | const tx = '0x0';
70 | const tx2 = '0x1234';
71 |
72 | accountState.set('0x1', tx, Operation.EXECUTE, TransactionState.PENDING);
73 | accountState.set('0x1', tx, Operation.EXECUTE, TransactionState.CONFIRMED);
74 |
75 | accountState.set('0x1', tx2, Operation.EXECUTE, TransactionState.SENT);
76 |
77 | const result = accountState.isSent(tx, Operation.EXECUTE);
78 |
79 | assert.isFalse(result);
80 | });
81 |
82 | it('returns false when no other operation is sent', () => {
83 | const accountState = new AccountState();
84 | const tx = '0x0';
85 |
86 | accountState.set('0x1', tx, Operation.CLAIM, TransactionState.CONFIRMED);
87 | accountState.set('0x1', tx, Operation.EXECUTE, TransactionState.PENDING);
88 |
89 | const result = accountState.isSent(tx, Operation.EXECUTE);
90 |
91 | assert.isFalse(result);
92 | });
93 | });
94 | });
95 |
--------------------------------------------------------------------------------
/test/unit/UnitTestActions.ts:
--------------------------------------------------------------------------------
1 | import { assert } from 'chai';
2 | import {
3 | isExecuted,
4 | EXECUTED_EVENT,
5 | isTransactionStatusSuccessful,
6 | ABORTED_EVENT,
7 | isAborted,
8 | getAbortedExecuteStatus
9 | } from '../../src/Actions/Helpers';
10 | import { TxSendStatus } from '../../src/Enum';
11 | import { TransactionReceipt } from 'web3/types';
12 |
13 | describe('Actions Helpers Unit Tests', () => {
14 | describe('isExecuted()', () => {
15 | it('returns true when executed event present in receipt', () => {
16 | const receipt = {
17 | logs: [
18 | {
19 | topics: [EXECUTED_EVENT]
20 | }
21 | ]
22 | } as TransactionReceipt;
23 | assert.isTrue(isExecuted(receipt));
24 | });
25 |
26 | it('returns false when receipt executed event address mismatches', () => {
27 | const receipt = {
28 | logs: [{ topics: ['0x0'] }]
29 | } as TransactionReceipt;
30 | assert.isFalse(isExecuted(receipt));
31 | });
32 |
33 | it('returns false when no receipt', () => {
34 | const receipt: any = null;
35 | assert.isFalse(isExecuted(receipt));
36 | });
37 | });
38 |
39 | describe('isTransactionStatusSuccessful()', () => {
40 | it('returns true when status code is 1', () => {
41 | assert.isTrue(isTransactionStatusSuccessful(1));
42 | assert.isTrue(isTransactionStatusSuccessful('0x1'));
43 | assert.isTrue(isTransactionStatusSuccessful('0x01'));
44 | });
45 |
46 | it('returns false status other than 1', () => {
47 | assert.isFalse(isTransactionStatusSuccessful(null));
48 | assert.isFalse(isTransactionStatusSuccessful(undefined));
49 |
50 | assert.isFalse(isTransactionStatusSuccessful(2));
51 | assert.isFalse(isTransactionStatusSuccessful('2'));
52 | assert.isFalse(isTransactionStatusSuccessful('0x02'));
53 | });
54 | });
55 |
56 | describe('isAborted()', () => {
57 | it('returns true when executed event was aborted', () => {
58 | const receipt = {
59 | logs: [
60 | {
61 | topics: [ABORTED_EVENT]
62 | }
63 | ]
64 | } as TransactionReceipt;
65 | assert.isTrue(isAborted(receipt));
66 | });
67 | });
68 |
69 | describe('getAbortedExecuteStatus()', () => {
70 | it('returns TxSendStatus.ABORTED_WAS_CANCELLED when AbortReason.WasCancelled', () => {
71 | const receipt = {
72 | logs: [
73 | {
74 | topics: [ABORTED_EVENT],
75 | data: '0x0000000000000000000000000000000000000000000000000000000000000000'
76 | }
77 | ]
78 | } as TransactionReceipt;
79 |
80 | const executeStatus = getAbortedExecuteStatus(receipt);
81 |
82 | assert.equal(TxSendStatus.ABORTED_WAS_CANCELLED, executeStatus);
83 | });
84 |
85 | it('returns TxSendStatus.ABORTED_ALREADY_CALLED when AbortReason.AlreadyCalled', () => {
86 | const receipt = {
87 | logs: [
88 | {
89 | topics: [ABORTED_EVENT],
90 | data: '0x0000000000000000000000000000000000000000000000000000000000000001'
91 | }
92 | ]
93 | } as TransactionReceipt;
94 |
95 | const executeStatus = getAbortedExecuteStatus(receipt);
96 |
97 | assert.equal(TxSendStatus.ABORTED_ALREADY_CALLED, executeStatus);
98 | });
99 |
100 | it('returns TxSendStatus.ABORTED_BEFORE_CALL_WINDOW when AbortReason.BeforeCallWindow', () => {
101 | const receipt = {
102 | logs: [
103 | {
104 | topics: [ABORTED_EVENT],
105 | data: '0x0000000000000000000000000000000000000000000000000000000000000002'
106 | }
107 | ]
108 | } as TransactionReceipt;
109 |
110 | const executeStatus = getAbortedExecuteStatus(receipt);
111 |
112 | assert.equal(TxSendStatus.ABORTED_BEFORE_CALL_WINDOW, executeStatus);
113 | });
114 |
115 | it('returns TxSendStatus.ABORTED_AFTER_CALL_WINDOW when AbortReason.AfterCallWindow', () => {
116 | const receipt = {
117 | logs: [
118 | {
119 | topics: [ABORTED_EVENT],
120 | data: '0x0000000000000000000000000000000000000000000000000000000000000003'
121 | }
122 | ]
123 | } as TransactionReceipt;
124 |
125 | const executeStatus = getAbortedExecuteStatus(receipt);
126 |
127 | assert.equal(TxSendStatus.ABORTED_AFTER_CALL_WINDOW, executeStatus);
128 | });
129 |
130 | it('returns TxSendStatus.ABORTED_RESERVED_FOR_CLAIMER when AbortReason.ReservedForClaimer', () => {
131 | const receipt = {
132 | logs: [
133 | {
134 | topics: [ABORTED_EVENT],
135 | data: '0x0000000000000000000000000000000000000000000000000000000000000004'
136 | }
137 | ]
138 | } as TransactionReceipt;
139 |
140 | const executeStatus = getAbortedExecuteStatus(receipt);
141 |
142 | assert.equal(TxSendStatus.ABORTED_RESERVED_FOR_CLAIMER, executeStatus);
143 | });
144 |
145 | it('returns TxSendStatus.ABORTED_INSUFFICIENT_GAS when AbortReason.InsufficientGas', () => {
146 | const receipt = {
147 | logs: [
148 | {
149 | topics: [ABORTED_EVENT],
150 | data: '0x0000000000000000000000000000000000000000000000000000000000000005'
151 | }
152 | ]
153 | } as TransactionReceipt;
154 |
155 | const executeStatus = getAbortedExecuteStatus(receipt);
156 |
157 | assert.equal(TxSendStatus.ABORTED_INSUFFICIENT_GAS, executeStatus);
158 | });
159 |
160 | it('returns TxSendStatus.ABORTED_TOO_LOW_GAS_PRICE when AbortReason.TooLowGasPrice', () => {
161 | const receipt = {
162 | logs: [
163 | {
164 | topics: [ABORTED_EVENT],
165 | data: '0x0000000000000000000000000000000000000000000000000000000000000006'
166 | }
167 | ]
168 | } as TransactionReceipt;
169 |
170 | const executeStatus = getAbortedExecuteStatus(receipt);
171 |
172 | assert.equal(TxSendStatus.ABORTED_TOO_LOW_GAS_PRICE, executeStatus);
173 | });
174 |
175 | it('returns TxSendStatus.ABORTED_UNKNOWN when unknown reason appeared', () => {
176 | const receipt = {
177 | logs: [
178 | {
179 | topics: [ABORTED_EVENT],
180 | data: '0x0000000000000000000000000000000000000000000000000000000000000008'
181 | }
182 | ]
183 | } as TransactionReceipt;
184 |
185 | const executeStatus = getAbortedExecuteStatus(receipt);
186 |
187 | assert.equal(TxSendStatus.ABORTED_UNKNOWN, executeStatus);
188 | });
189 |
190 | // tslint:disable-next-line:no-identical-functions
191 | it('returns TxSendStatus.ABORTED_UNKNOWN when no data found', () => {
192 | const receipt = {
193 | logs: [
194 | {
195 | topics: [ABORTED_EVENT],
196 | data: ''
197 | }
198 | ]
199 | } as TransactionReceipt;
200 |
201 | const executeStatus = getAbortedExecuteStatus(receipt);
202 |
203 | assert.equal(TxSendStatus.ABORTED_UNKNOWN, executeStatus);
204 | });
205 | });
206 | });
207 |
--------------------------------------------------------------------------------
/test/unit/UnitTestBucketCalc.ts:
--------------------------------------------------------------------------------
1 | import { assert } from 'chai';
2 | import * as TypeMoq from 'typemoq';
3 | import { BucketCalc } from '../../src/Buckets';
4 | import { mockConfig } from '../helpers';
5 | import { Block } from 'web3/eth/types';
6 | import { Util, Constants } from '@ethereum-alarm-clock/lib';
7 |
8 | const BucketSize = Constants.BUCKET_SIZE;
9 |
10 | describe('ButcketCalc', () => {
11 | describe('getBuckets()', async () => {
12 | it('returns current, next and after next buckets', async () => {
13 | const defaultBlock: Block = { number: 10000, timestamp: 10000000000 } as Block;
14 | const util = TypeMoq.Mock.ofType();
15 | util.setup(u => u.getBlock('latest')).returns(async () => defaultBlock);
16 |
17 | const config = await mockConfig();
18 | const requestFactory = config.eac.requestFactory();
19 |
20 | const bucketCalc = new BucketCalc(util.object, requestFactory);
21 |
22 | const buckets = await bucketCalc.getBuckets();
23 |
24 | assert.equal(buckets.length, 6);
25 |
26 | assert.include(
27 | buckets,
28 | -1 * (defaultBlock.number - (defaultBlock.number % BucketSize.block))
29 | );
30 |
31 | assert.include(
32 | buckets,
33 | defaultBlock.timestamp - (defaultBlock.timestamp % BucketSize.timestamp)
34 | );
35 |
36 | const expectedNextBlockInterval = defaultBlock.number + BucketSize.block;
37 | const expectedNextTimestampInterval = defaultBlock.timestamp + BucketSize.timestamp;
38 | assert.include(
39 | buckets,
40 | -1 * (expectedNextBlockInterval - (expectedNextBlockInterval % BucketSize.block))
41 | );
42 | assert.include(
43 | buckets,
44 | expectedNextTimestampInterval - (expectedNextTimestampInterval % BucketSize.timestamp)
45 | );
46 |
47 | const expectedAfterNextBlockInterval = defaultBlock.number + 2 * BucketSize.block;
48 | const expectedAfterNextTimestampInterval = defaultBlock.timestamp + 2 * BucketSize.timestamp;
49 | assert.include(
50 | buckets,
51 | -1 * (expectedAfterNextBlockInterval - (expectedAfterNextBlockInterval % BucketSize.block))
52 | );
53 | assert.include(
54 | buckets,
55 | expectedAfterNextTimestampInterval -
56 | (expectedAfterNextTimestampInterval % BucketSize.timestamp)
57 | );
58 | });
59 | });
60 | });
61 |
--------------------------------------------------------------------------------
/test/unit/UnitTestBuckets.ts:
--------------------------------------------------------------------------------
1 | import * as TypeMoq from 'typemoq';
2 |
3 | import { Bucket } from '../../src/Buckets';
4 | import { BucketsManager } from '../../src/Scanner/BucketsManager';
5 | import { WatchableBucket } from '../../src/Scanner/WatchableBucket';
6 | import { WatchableBucketFactory } from '../../src/Scanner/WatchableBucketFactory';
7 |
8 | // tslint:disable-next-line:no-big-function
9 | describe('WatchableBucket', () => {
10 | const createWatchableBucket = (
11 | timesWatch: TypeMoq.Times,
12 | timesStop: TypeMoq.Times,
13 | bucket: Bucket = 0
14 | ) => {
15 | const watchableBucket = TypeMoq.Mock.ofType();
16 | watchableBucket.setup(w => w.watch()).verifiable(timesWatch);
17 | watchableBucket.setup(w => w.stop()).verifiable(timesStop);
18 | watchableBucket.setup(w => w.bucket).returns(() => bucket);
19 | watchableBucket.setup((x: any) => x.then).returns(() => undefined);
20 |
21 | return watchableBucket;
22 | };
23 |
24 | const registerInFactory = (
25 | watchableBucketFactoryMock: TypeMoq.IMock,
26 | bucket: Bucket,
27 | watchableBucketMock: TypeMoq.IMock,
28 | times: TypeMoq.Times = TypeMoq.Times.once()
29 | ) => {
30 | watchableBucketFactoryMock
31 | .setup(r => r.create(bucket, TypeMoq.It.isAny()))
32 | .returns(async () => watchableBucketMock.object)
33 | .verifiable(times);
34 | };
35 |
36 | it('should watch all buckets when used for the first time', async () => {
37 | const buckets = [1, 2, 3, -1, -2, -3];
38 | const expectedStarts = TypeMoq.Times.exactly(buckets.length);
39 |
40 | const watchableBucket = createWatchableBucket(expectedStarts, TypeMoq.Times.never());
41 |
42 | const watchableBucketFactory = TypeMoq.Mock.ofType();
43 | watchableBucketFactory
44 | .setup(r => r.create(TypeMoq.It.isAny(), TypeMoq.It.isAny()))
45 | .returns(async () => watchableBucket.object)
46 | .verifiable(expectedStarts);
47 |
48 | const bucketsManager = new BucketsManager(watchableBucketFactory.object);
49 |
50 | await bucketsManager.update(buckets, null);
51 |
52 | watchableBucket.verifyAll();
53 | watchableBucketFactory.verifyAll();
54 | });
55 |
56 | it('should not watch same buckets when already watched', async () => {
57 | const buckets = [1, 2, 3, -1, -2, -3];
58 | const watchableBuckets = buckets.map(b =>
59 | createWatchableBucket(TypeMoq.Times.once(), TypeMoq.Times.never(), b)
60 | );
61 |
62 | const watchableBucketFactory = TypeMoq.Mock.ofType();
63 | watchableBuckets.forEach(w => registerInFactory(watchableBucketFactory, w.object.bucket, w));
64 |
65 | const bucketsManager = new BucketsManager(watchableBucketFactory.object);
66 |
67 | await bucketsManager.update(buckets, null);
68 | await bucketsManager.update(buckets, null);
69 |
70 | watchableBuckets.forEach(w => w.verifyAll());
71 | watchableBucketFactory.verifyAll();
72 | });
73 |
74 | it('should stop old buckets', async () => {
75 | const bucket1 = 1;
76 | const bucket2 = 2;
77 | const bucket3 = 3;
78 | const bucket4 = -5;
79 |
80 | const toStop = [bucket1, bucket2];
81 | const toSkip = [bucket3, bucket4];
82 |
83 | const buckets = toStop.concat(toSkip);
84 | const newBuckets = toSkip;
85 |
86 | const watchableToStop = toStop.map(b =>
87 | createWatchableBucket(TypeMoq.Times.once(), TypeMoq.Times.once(), b)
88 | );
89 | const watchableToSkip = toSkip.map(b =>
90 | createWatchableBucket(TypeMoq.Times.once(), TypeMoq.Times.never(), b)
91 | );
92 | const watchable = watchableToStop.concat(watchableToSkip);
93 |
94 | const watchableBucketFactory = TypeMoq.Mock.ofType();
95 | watchable.forEach(w => registerInFactory(watchableBucketFactory, w.object.bucket, w));
96 |
97 | const bucketsManager = new BucketsManager(watchableBucketFactory.object);
98 |
99 | await bucketsManager.update(buckets, null);
100 | await bucketsManager.update(newBuckets, null);
101 |
102 | watchable.forEach(w => w.verifyAll());
103 | watchableBucketFactory.verifyAll();
104 | });
105 |
106 | it('should start new buckets', async () => {
107 | const bucket1 = 1;
108 | const bucket2 = 2;
109 | const bucket3 = 3;
110 | const bucket4 = -5;
111 |
112 | const toStart = [bucket1, bucket2];
113 | const toSkip = [bucket3, bucket4];
114 |
115 | const buckets = toSkip;
116 | const newBuckets = toSkip.concat(toStart);
117 |
118 | const watchableToStop = toStart.map(b =>
119 | createWatchableBucket(TypeMoq.Times.once(), TypeMoq.Times.never(), b)
120 | );
121 | const watchableToSkip = toSkip.map(b =>
122 | createWatchableBucket(TypeMoq.Times.once(), TypeMoq.Times.never(), b)
123 | );
124 | const watchable = watchableToStop.concat(watchableToSkip);
125 |
126 | const watchableBucketFactory = TypeMoq.Mock.ofType();
127 | watchable.forEach(w => registerInFactory(watchableBucketFactory, w.object.bucket, w));
128 |
129 | const bucketsManager = new BucketsManager(watchableBucketFactory.object);
130 |
131 | await bucketsManager.update(buckets, null);
132 | await bucketsManager.update(newBuckets, null);
133 |
134 | watchable.forEach(w => w.verifyAll());
135 | watchableBucketFactory.verifyAll();
136 | });
137 | });
138 |
--------------------------------------------------------------------------------
/test/unit/UnitTestCache.ts:
--------------------------------------------------------------------------------
1 | import { expect, assert } from 'chai';
2 | import Cache from '../../src/Cache';
3 |
4 | let cache: Cache;
5 |
6 | beforeEach(() => {
7 | cache = new Cache(null);
8 | });
9 |
10 | describe('Cache unit tests', () => {
11 | describe('get()', () => {
12 | it('get the key value', () => {
13 | cache.set('key', 'value');
14 | const result = cache.get('key');
15 | expect(result).to.equals('value');
16 | });
17 |
18 | it('throws an error if value is undefined', () => {
19 | cache.set('key', undefined);
20 | expect(() => cache.get('key')).to.throw();
21 | });
22 |
23 | it('returns `d` if value and `d` is undefined', () => {
24 | cache.set('key', undefined);
25 | const d = () => console.log('callback');
26 | const result = cache.get('key', d);
27 | assert.equal(result, d);
28 | });
29 | });
30 |
31 | describe('length()', () => {
32 | it('return proper length', () => {
33 | cache.set('key', 'value');
34 | const result = cache.length();
35 | expect(result).to.equals(1);
36 | });
37 | });
38 |
39 | describe('stored()', () => {
40 | it('returns stored values', () => {
41 | cache.set('key', 'value');
42 | cache.set('key2', 'value2');
43 |
44 | const results = cache.stored();
45 |
46 | expect(results.length).to.equals(2);
47 | expect(results[0]).to.equals('key');
48 | expect(results[1]).to.equals('key2');
49 | });
50 | });
51 |
52 | describe('del()', () => {
53 | it('returns stored values', () => {
54 | const key = 'key';
55 | cache.set(key, 'value');
56 | cache.del(key);
57 | expect(() => cache.get(key)).to.throw();
58 | });
59 | });
60 |
61 | describe('has()', () => {
62 | it('returns true when value is in cache', () => {
63 | cache.set('key', 'value');
64 | assert.isTrue(cache.has('key'));
65 | });
66 |
67 | it('returns false when value is not in cache', () => {
68 | assert.isFalse(cache.has('key'));
69 | });
70 | });
71 | });
72 |
--------------------------------------------------------------------------------
/test/unit/UnitTestCacheScanner.ts:
--------------------------------------------------------------------------------
1 | import * as TypeMoq from 'typemoq';
2 | import IRouter from '../../src/Router';
3 | import { Config } from '../../src';
4 | import CacheScanner from '../../src/Scanner/CacheScanner';
5 | import Cache, { ICachedTxDetails } from '../../src/Cache';
6 | import { TxStatus } from '../../src/Enum';
7 | import { assert } from 'chai';
8 | import BigNumber from 'bignumber.js';
9 | import { EAC, Util, ITransactionRequest } from '@ethereum-alarm-clock/lib';
10 |
11 | describe('Cache Scanner Unit Tests', () => {
12 | const BLOCKTIME = 14;
13 | const eac = {
14 | transactionRequest(address: string) {
15 | const req = TypeMoq.Mock.ofType();
16 | req.setup(r => r.address).returns(() => address);
17 | return req.object;
18 | }
19 | };
20 |
21 | const mockTx = (params: any) => {
22 | const TX_DEFAULTS: ICachedTxDetails = {
23 | status: TxStatus.ClaimWindow,
24 | temporalUnit: 2,
25 | windowStart: new BigNumber(10000),
26 | claimWindowStart: new BigNumber(9750),
27 | claimedBy: '0x0',
28 | bounty: new BigNumber(10e9),
29 | wasCalled: false
30 | };
31 |
32 | const transaction = TypeMoq.Mock.ofType();
33 | transaction.setup(tx => tx.status).returns(() => params.status || TX_DEFAULTS.status);
34 | transaction
35 | .setup(tx => tx.temporalUnit)
36 | .returns(() => params.temporalUnit || TX_DEFAULTS.temporalUnit);
37 | transaction
38 | .setup(tx => tx.windowStart)
39 | .returns(() => params.windowStart || TX_DEFAULTS.windowStart);
40 | transaction
41 | .setup(tx => tx.claimWindowStart)
42 | .returns(() => params.claimWindowStart || TX_DEFAULTS.claimWindowStart);
43 | transaction.setup(tx => tx.bounty).returns(() => params.bounty || TX_DEFAULTS.bounty);
44 | return transaction;
45 | };
46 |
47 | it('does not route when cache empty', async () => {
48 | const cache = TypeMoq.Mock.ofType>();
49 | cache.setup(c => c.isEmpty()).returns(() => true);
50 |
51 | const router = TypeMoq.Mock.ofType();
52 | const config = TypeMoq.Mock.ofType();
53 | config.setup(c => c.cache).returns(() => cache.object);
54 |
55 | const scanner = new CacheScanner(config.object, router.object);
56 |
57 | await scanner.scanCache();
58 |
59 | router.verify(r => r.route(TypeMoq.It.isAny()), TypeMoq.Times.never());
60 | });
61 |
62 | it('calculates the average blocktime', async () => {
63 | const util = TypeMoq.Mock.ofType();
64 | util.setup(u => u.getAverageBlockTime()).returns(() => Promise.resolve(BLOCKTIME));
65 |
66 | const transaction = TypeMoq.Mock.ofType();
67 | transaction.setup(tx => tx.status).returns(() => TxStatus.ClaimWindow);
68 |
69 | const cache = new Cache();
70 | cache.set('1', transaction.object);
71 |
72 | const router = TypeMoq.Mock.ofType();
73 | const config = TypeMoq.Mock.ofType();
74 | config.setup(c => c.cache).returns(() => cache);
75 | config.setup(c => c.util).returns(() => util.object);
76 | config.setup(c => c.eac).returns(() => eac as EAC);
77 |
78 | const scanner = new CacheScanner(config.object, router.object);
79 | assert.notExists(scanner.avgBlockTime);
80 |
81 | await scanner.scanCache();
82 |
83 | assert.exists(scanner.avgBlockTime);
84 | assert.strictEqual(scanner.avgBlockTime, BLOCKTIME);
85 | });
86 |
87 | it('does prioritize requests in FreezePeriod ', async () => {
88 | const tx1 = mockTx({ status: TxStatus.FreezePeriod });
89 | const tx2 = mockTx({ status: TxStatus.Executed });
90 | const tx3 = mockTx({ status: TxStatus.ClaimWindow });
91 |
92 | const cache = new Cache();
93 | cache.set('3', tx3.object);
94 | cache.set('2', tx2.object);
95 | cache.set('1', tx1.object);
96 |
97 | const routed: ITransactionRequest[] = [];
98 |
99 | const router = TypeMoq.Mock.ofType();
100 | router.setup(r => r.route(TypeMoq.It.isAny())).callback(txRequest => routed.push(txRequest));
101 |
102 | const config = TypeMoq.Mock.ofType();
103 | config.setup(c => c.cache).returns(() => cache);
104 | config.setup(c => c.eac).returns(() => eac as EAC);
105 |
106 | const util = TypeMoq.Mock.ofType();
107 | util.setup(u => u.getAverageBlockTime()).returns(() => Promise.resolve(BLOCKTIME));
108 | config.setup(c => c.util).returns(() => util.object);
109 |
110 | const scanner = new CacheScanner(config.object, router.object);
111 |
112 | await scanner.scanCache();
113 |
114 | router.verify(r => r.route(TypeMoq.It.isAny()), TypeMoq.Times.exactly(3));
115 |
116 | assert.equal(routed.shift().address, '1');
117 | });
118 |
119 | it('prioritizes the tx with a higher bounty if in the same block', async () => {
120 | const tx1 = mockTx({ bounty: new BigNumber(10e9) });
121 | const tx2 = mockTx({ bounty: new BigNumber(10e10) });
122 | const tx3 = mockTx({ bounty: new BigNumber(10e8) });
123 |
124 | const cache = new Cache();
125 | cache.set('3', tx3.object);
126 | cache.set('2', tx2.object);
127 | cache.set('1', tx1.object);
128 |
129 | const routed: ITransactionRequest[] = [];
130 |
131 | const router = TypeMoq.Mock.ofType();
132 | router.setup(r => r.route(TypeMoq.It.isAny())).callback(txRequest => routed.push(txRequest));
133 |
134 | const config = TypeMoq.Mock.ofType();
135 | config.setup(c => c.cache).returns(() => cache);
136 | config.setup(c => c.eac).returns(() => eac as EAC);
137 |
138 | const util = TypeMoq.Mock.ofType();
139 | util.setup(u => u.getAverageBlockTime()).returns(() => Promise.resolve(BLOCKTIME));
140 | config.setup(c => c.util).returns(() => util.object);
141 |
142 | const scanner = new CacheScanner(config.object, router.object);
143 |
144 | await scanner.scanCache();
145 |
146 | router.verify(r => r.route(TypeMoq.It.isAny()), TypeMoq.Times.exactly(3));
147 |
148 | assert.equal(routed.shift().address, '2');
149 | });
150 |
151 | it('prioritizes block tx over timestamp tx even if a higher bounty', async () => {
152 | const tx1 = mockTx({ temporalUnit: 2 });
153 | const tx2 = mockTx({ temporalUnit: 2 });
154 | const tx3 = mockTx({ temporalUnit: 1 });
155 |
156 | const cache = new Cache();
157 | cache.set('1', tx1.object);
158 | cache.set('2', tx2.object);
159 | cache.set('3', tx3.object);
160 |
161 | const routed: ITransactionRequest[] = [];
162 |
163 | const router = TypeMoq.Mock.ofType();
164 | router.setup(r => r.route(TypeMoq.It.isAny())).callback(txRequest => routed.push(txRequest));
165 |
166 | const config = TypeMoq.Mock.ofType();
167 | config.setup(c => c.cache).returns(() => cache);
168 | config.setup(c => c.eac).returns(() => eac as EAC);
169 |
170 | const util = TypeMoq.Mock.ofType();
171 | util.setup(u => u.getAverageBlockTime()).returns(() => Promise.resolve(BLOCKTIME));
172 | config.setup(c => c.util).returns(() => util.object);
173 |
174 | const scanner = new CacheScanner(config.object, router.object);
175 |
176 | await scanner.scanCache();
177 |
178 | router.verify(r => r.route(TypeMoq.It.isAny()), TypeMoq.Times.exactly(3));
179 |
180 | assert.equal(routed.shift().address, '3');
181 | });
182 | });
183 |
--------------------------------------------------------------------------------
/test/unit/UnitTestConfig.ts:
--------------------------------------------------------------------------------
1 | /* tslint:disable:no-unused-expressions */
2 | import { expect, assert } from 'chai';
3 | import Config from '../../src/Config';
4 | import { DefaultLogger } from '../../src/Logger';
5 | import { PRIVATE_KEY, providerUrl } from '../helpers';
6 | import { BigNumber } from 'bignumber.js';
7 |
8 | const WALLET_PASSWD = 'Wak9bk7DwZYL';
9 | const WALLET_KEYSTORE = `{"version":3,"id":"90ff6d22-668b-492a-bc56-8b560fece46d","address":"487a54e1d033db51c8ee8c03edac2a0f8a6892c6","crypto":{"ciphertext":"115c232498a4f47d10de6c7148b8ebefdac44581b74475bba51675e83d7244dd","cipherparams":{"iv":"b53497a50161315e3fe4f33a9b74c22b"},"cipher":"aes-128-ctr","kdf":"scrypt","kdfparams":{"dklen":32,"salt":"6d6e0329bf255c65c64e9e2face95227ab3221e8648635ac1b65a3c78ce25bbc","n":8192,"r":8,"p":1},"mac":"d771499de6091c8947f8ee9e5e91c3ea9b0b08ed90844c34f77ae2e33b40f40e"}}`;
10 |
11 | describe('Config unit tests', () => {
12 | describe('constructor()', () => {
13 | it('throws an error when initiating without required params', () => {
14 | expect(() => new Config({ providerUrls: null })).to.throw();
15 | });
16 |
17 | it('check all default values are set when empty config', () => {
18 | const config = new Config({ providerUrls: [providerUrl] });
19 |
20 | assert.isTrue(config.autostart);
21 | assert.isFalse(config.claiming);
22 | assert.equal(config.ms, 4000);
23 | assert.equal(config.scanSpread, 50);
24 | assert.isFalse(config.walletStoresAsPrivateKeys);
25 | expect(config.logger).to.exist; // tslint:disable-line no-unused-expression
26 | assert.isNull(config.wallet);
27 | assert.equal(config.economicStrategy.maxDeposit, Config.DEFAULT_ECONOMIC_STRATEGY.maxDeposit);
28 | assert.equal(config.economicStrategy.minBalance, Config.DEFAULT_ECONOMIC_STRATEGY.minBalance);
29 | assert.equal(
30 | config.economicStrategy.minProfitability,
31 | Config.DEFAULT_ECONOMIC_STRATEGY.minProfitability
32 | );
33 | assert.equal(
34 | config.economicStrategy.maxGasSubsidy,
35 | Config.DEFAULT_ECONOMIC_STRATEGY.maxGasSubsidy
36 | );
37 | });
38 |
39 | it('check all values are set when added to config object', () => {
40 | const decimals = 1000000000000000000;
41 | const economicStrategy = {
42 | maxDeposit: new BigNumber(1 * decimals),
43 | minBalance: new BigNumber(5 * decimals),
44 | minProfitability: new BigNumber(0.01 * decimals),
45 | maxGasSubsidy: 200
46 | };
47 |
48 | const config = new Config({
49 | providerUrls: [providerUrl],
50 | autostart: false,
51 | claiming: true,
52 | economicStrategy,
53 | ms: 10000,
54 | scanSpread: 100,
55 | walletStoresAsPrivateKeys: true,
56 | logger: new DefaultLogger(),
57 | walletStores: [PRIVATE_KEY]
58 | });
59 |
60 | assert.isFalse(config.autostart);
61 | assert.isTrue(config.claiming);
62 | assert.equal(config.ms, 10000);
63 | assert.equal(config.scanSpread, 100);
64 | assert.isTrue(config.walletStoresAsPrivateKeys);
65 | expect(config.logger).to.exist; // tslint:disable-line no-unused-expression
66 | assert.equal(config.wallet.getAccounts().length, 1);
67 | assert.equal(config.economicStrategy, economicStrategy);
68 | });
69 |
70 | it('wallet decrypted when using a keystore string', () => {
71 | const config = new Config({
72 | providerUrls: [providerUrl],
73 | walletStores: [WALLET_KEYSTORE],
74 | password: WALLET_PASSWD
75 | });
76 |
77 | assert.equal(config.wallet.getAccounts().length, 1);
78 | });
79 |
80 | it('wallet decrypted when using a keystore object', () => {
81 | const config = new Config({
82 | providerUrls: [providerUrl],
83 | walletStores: [JSON.parse(WALLET_KEYSTORE)],
84 | password: WALLET_PASSWD
85 | });
86 |
87 | assert.equal(config.wallet.getAccounts().length, 1);
88 | });
89 |
90 | it('throws an error when using a keystore without a password', () => {
91 | expect(
92 | () =>
93 | new Config({
94 | providerUrls: [providerUrl],
95 | walletStores: [WALLET_KEYSTORE]
96 | })
97 | ).to.throw();
98 | });
99 | });
100 | });
101 |
--------------------------------------------------------------------------------
/test/unit/UnitTestLedger.ts:
--------------------------------------------------------------------------------
1 | import * as TypeMoq from 'typemoq';
2 | import { assert } from 'chai';
3 | import BigNumber from 'bignumber.js';
4 | import { ILedger, Ledger } from '../../src/Actions/Ledger';
5 | import { IStatsDB } from '../../src/Stats/StatsDB';
6 | import { Operation } from '../../src/Types/Operation';
7 | import { TransactionReceipt, Log } from 'web3/types';
8 | import { ITransactionRequest } from '@ethereum-alarm-clock/lib';
9 |
10 | const account1: string = '0xd0700ed9f4d178adf25b45f7fa8a4ec7c230b098';
11 | const account2: string = '0x0054a7eef4dc5d729115c71cba074151b3d41804';
12 |
13 | const tx1: string = '0xaa55bf414ecef0285dcece4ddf78a0ee8beb6707';
14 |
15 | const gas = new BigNumber('100000');
16 | const gasPrice = new BigNumber(100000000);
17 | const requiredDeposit = new BigNumber(10000000);
18 | const opts = {
19 | to: account1,
20 | value: new BigNumber(0),
21 | gas,
22 | gasPrice,
23 | data: '0x0',
24 | operation: Operation.EXECUTE
25 | };
26 |
27 | let ledger: ILedger;
28 | let stats: TypeMoq.IMock;
29 |
30 | const txRequest = TypeMoq.Mock.ofType();
31 | txRequest.setup(x => x.requiredDeposit).returns(() => requiredDeposit);
32 | txRequest.setup(x => x.address).returns(() => tx1);
33 |
34 | const reset = async () => {
35 | stats = TypeMoq.Mock.ofType();
36 | ledger = new Ledger(stats.object);
37 | };
38 |
39 | beforeEach(reset);
40 |
41 | describe('Ledger Unit Tests', async () => {
42 | it('should account for required deposit and tx cost when claiming was successful', async () => {
43 | const receipt = TypeMoq.Mock.ofType();
44 | receipt.setup(r => r.status).returns(() => true);
45 | receipt.setup(r => r.gasUsed).returns(() => gas.toNumber());
46 |
47 | ledger.accountClaiming(receipt.object, txRequest.object, opts, account2);
48 |
49 | const expectedCost = gasPrice.multipliedBy(gas).plus(requiredDeposit);
50 |
51 | assert.doesNotThrow(() =>
52 | stats.verify(x => x.claimed(account2, tx1, expectedCost, true), TypeMoq.Times.once())
53 | );
54 | });
55 |
56 | it('should account for tx cost when claiming failed', async () => {
57 | const receipt = TypeMoq.Mock.ofType();
58 | receipt.setup(r => r.status).returns(() => false);
59 | receipt.setup(r => r.gasUsed).returns(() => gas.toNumber());
60 |
61 | ledger.accountClaiming(receipt.object, txRequest.object, opts, account2);
62 |
63 | const expectedCost = gasPrice.multipliedBy(gas);
64 |
65 | assert.doesNotThrow(() =>
66 | stats.verify(x => x.claimed(account2, tx1, expectedCost, false), TypeMoq.Times.once())
67 | );
68 | });
69 |
70 | it('should account for bounty only when execution was successful', async () => {
71 | const log = TypeMoq.Mock.ofType();
72 | log
73 | .setup(l => l.data)
74 | .returns(
75 | () =>
76 | '0x000000000000000000000000000000000000000000000000000fe3c87f4b736300000000000000000000000000000000000000000000000000000002540be4000000000000000000000000000000000000000000000000000000000000030cd6'
77 | );
78 |
79 | const receipt = TypeMoq.Mock.ofType();
80 | receipt.setup(r => r.status).returns(() => true);
81 | receipt.setup(r => r.gasUsed).returns(() => gas.toNumber());
82 | receipt.setup(r => r.logs).returns(() => [log.object]);
83 |
84 | ledger.accountExecution(txRequest.object, receipt.object, opts, account2, true);
85 |
86 | const gasUsed = new BigNumber(receipt.object.gasUsed);
87 | const actualGasPrice = opts.gasPrice;
88 | const expectedCost = new BigNumber(0);
89 | const expectedReward = new BigNumber(
90 | '0x000000000000000000000000000000000000000000000000000fe3c87f4b7363'
91 | ).minus(gasUsed.multipliedBy(actualGasPrice));
92 |
93 | assert.doesNotThrow(() =>
94 | stats.verify(
95 | x => x.executed(account2, tx1, expectedCost, expectedReward, true),
96 | TypeMoq.Times.once()
97 | )
98 | );
99 | });
100 |
101 | it('should account for tx costs when execution was not successful', async () => {
102 | const receipt = TypeMoq.Mock.ofType();
103 | receipt.setup(r => r.status).returns(() => false);
104 | receipt.setup(r => r.gasUsed).returns(() => gas.toNumber());
105 |
106 | ledger.accountExecution(txRequest.object, receipt.object, opts, account2, false);
107 |
108 | const expectedReward = new BigNumber(0);
109 | const expectedCost = gasPrice.multipliedBy(gas);
110 |
111 | assert.doesNotThrow(() =>
112 | stats.verify(
113 | x => x.executed(account2, tx1, expectedCost, expectedReward, false),
114 | TypeMoq.Times.once()
115 | )
116 | );
117 | });
118 | });
119 |
--------------------------------------------------------------------------------
/test/unit/UnitTestPending.ts:
--------------------------------------------------------------------------------
1 | import { assert } from 'chai';
2 | import * as TypeMoq from 'typemoq';
3 | import { ITxPool, ITxPoolTxDetails } from '../../src/TxPool';
4 | import { Pending } from '../../src/Actions/Pending';
5 | import { BigNumber } from 'bignumber.js';
6 | import { Operation } from '../../src/Types/Operation';
7 | import { GasPriceUtil } from '@ethereum-alarm-clock/lib';
8 |
9 | describe('Pending Unit Tests', () => {
10 | function createTxPoolDetails(address: string, poolOperation: Operation, gasPrice: BigNumber) {
11 | const item = TypeMoq.Mock.ofType();
12 | item.setup(i => i.to).returns(() => address);
13 | item.setup(i => i.operation).returns(() => poolOperation);
14 | item.setup(i => i.gasPrice).returns(() => gasPrice);
15 | return item;
16 | }
17 |
18 | function createUtils(gasPrice: BigNumber) {
19 | const util = TypeMoq.Mock.ofType();
20 | util.setup(u => u.networkGasPrice()).returns(async () => gasPrice);
21 | return util;
22 | }
23 |
24 | function createPool(transactionHash: string, item: ITxPoolTxDetails) {
25 | const pool = new Map();
26 | pool.set(transactionHash, item);
27 |
28 | const txPool = TypeMoq.Mock.ofType();
29 | txPool.setup(p => p.running()).returns(() => true);
30 | txPool.setup(p => p.pool).returns(() => pool);
31 | return txPool;
32 | }
33 |
34 | it('should return false when pool is not running', async () => {
35 | const txPool = TypeMoq.Mock.ofType();
36 | txPool.setup(p => p.running()).returns(() => false);
37 |
38 | const pending = new Pending(null, txPool.object);
39 | const result = await pending.hasPending(null, null);
40 |
41 | assert.isFalse(result);
42 | });
43 |
44 | it('should return false when pool is empty', async () => {
45 | const address = '2';
46 | const gasPrice = new BigNumber(100000);
47 |
48 | const poolOperation = Operation.CLAIM;
49 | const requestedOperation = poolOperation;
50 |
51 | const txPool = TypeMoq.Mock.ofType();
52 | txPool.setup(p => p.running()).returns(() => false);
53 | txPool.setup(p => p.pool).returns(() => new Map());
54 |
55 | const util = createUtils(gasPrice);
56 |
57 | const pending = new Pending(util.object, txPool.object);
58 |
59 | const result = await pending.hasPending(
60 | { address, gasPrice },
61 | { type: requestedOperation, checkGasPrice: false }
62 | );
63 |
64 | assert.isFalse(result);
65 | });
66 |
67 | it('should return true when pool contains request and gasPrice check is off', async () => {
68 | const transactionHash = '1';
69 | const address = '2';
70 | const gasPrice = new BigNumber(100000);
71 |
72 | const poolOperation = Operation.CLAIM;
73 | const requestedOperation = poolOperation;
74 |
75 | const item = createTxPoolDetails(address, poolOperation, gasPrice);
76 | const txPool = createPool(transactionHash, item.object);
77 | const util = createUtils(gasPrice);
78 |
79 | const pending = new Pending(util.object, txPool.object);
80 |
81 | const result = await pending.hasPending(
82 | { address, gasPrice },
83 | { type: requestedOperation, checkGasPrice: false }
84 | );
85 |
86 | assert.isTrue(result);
87 | });
88 |
89 | it('should return false when pool contains CLAIM transaction but request is for EXECUTE operation', async () => {
90 | const transactionHash = '1';
91 | const address = '2';
92 | const gasPrice = new BigNumber(100000);
93 |
94 | const poolOperation = Operation.CLAIM;
95 | const requestedOperation = Operation.EXECUTE;
96 |
97 | const item = createTxPoolDetails(address, poolOperation, gasPrice);
98 | const txPool = createPool(transactionHash, item.object);
99 | const util = createUtils(gasPrice);
100 |
101 | const pending = new Pending(util.object, txPool.object);
102 |
103 | const result = await pending.hasPending(
104 | { address, gasPrice },
105 | { type: requestedOperation, checkGasPrice: false }
106 | );
107 |
108 | assert.isFalse(result);
109 | });
110 |
111 | it('should return false when pool contains transaction but gasPrice is lower than 1/3 of network gasPrice', async () => {
112 | const transactionHash = '1';
113 | const address = '2';
114 | const networkGasPrice = new BigNumber(100000);
115 | const gasPrice = networkGasPrice.div(5);
116 |
117 | const poolOperation = Operation.CLAIM;
118 | const requestedOperation = poolOperation;
119 |
120 | const item = createTxPoolDetails(address, poolOperation, gasPrice);
121 | const txPool = createPool(transactionHash, item.object);
122 | const util = createUtils(networkGasPrice);
123 |
124 | const pending = new Pending(util.object, txPool.object);
125 |
126 | const result = await pending.hasPending(
127 | { address, gasPrice },
128 | { type: requestedOperation, checkGasPrice: true }
129 | );
130 |
131 | assert.isFalse(result);
132 | });
133 | });
134 |
--------------------------------------------------------------------------------
/test/unit/UnitTestProfitabilityCalculator.ts:
--------------------------------------------------------------------------------
1 | import {
2 | GasPriceEstimation,
3 | Util,
4 | ITransactionRequest,
5 | GasPriceUtil
6 | } from '@ethereum-alarm-clock/lib';
7 | import BigNumber from 'bignumber.js';
8 | import * as TypeMoq from 'typemoq';
9 |
10 | import { ProfitabilityCalculator } from '../../src/EconomicStrategy/ProfitabilityCalculator';
11 | import { assert } from 'chai';
12 |
13 | // tslint:disable-next-line:no-big-function
14 | describe('Profitability Calculator Tests', () => {
15 | const MWei = new BigNumber(1000000);
16 | const GWei = MWei.times(1000);
17 | const Szabo = GWei.times(1000);
18 | const Finney = Szabo.times(1000);
19 |
20 | const account = '0x123456';
21 | const defaultBounty = Finney.times(20);
22 | const defaultGasPrice = GWei;
23 | const defaultPaymentModifier = new BigNumber(10); //10%
24 | const defaultCallGas = new BigNumber(21000);
25 | const CLAIMING_GAS_ESTIMATE = 100000;
26 |
27 | const util = new Util(null);
28 |
29 | const createTxRequest = (
30 | gasPrice = defaultGasPrice,
31 | bounty = defaultBounty,
32 | claimedBy = account,
33 | paymentModifier = defaultPaymentModifier,
34 | temporalUnit = 1,
35 | reservedWindowSize = new BigNumber(3600),
36 | claimWindowEnd = new BigNumber(123155)
37 | ) => {
38 | const txRequest = TypeMoq.Mock.ofType();
39 | txRequest.setup(tx => tx.gasPrice).returns(() => gasPrice);
40 | txRequest.setup(tx => tx.now()).returns(() => Promise.resolve(new BigNumber(123123)));
41 | txRequest.setup(tx => tx.reservedWindowEnd).returns(() => new BigNumber(23423));
42 | txRequest.setup(tx => tx.reservedWindowSize).returns(() => reservedWindowSize);
43 | txRequest.setup(tx => tx.executionWindowEnd).returns(() => new BigNumber(23423));
44 | txRequest.setup(tx => tx.bounty).returns(() => bounty);
45 | txRequest.setup(tx => tx.requiredDeposit).returns(() => MWei);
46 | txRequest.setup(tx => tx.claimPaymentModifier()).returns(async () => paymentModifier);
47 | txRequest.setup(tx => tx.claimedBy).returns(() => claimedBy);
48 | txRequest.setup(tx => tx.address).returns(() => '0x987654321');
49 | txRequest.setup(tx => tx.temporalUnit).returns(() => temporalUnit);
50 | txRequest.setup(tx => tx.claimWindowEnd).returns(() => claimWindowEnd);
51 | txRequest.setup(tx => tx.callGas).returns(() => defaultCallGas);
52 | txRequest.setup(tx => tx.isClaimed).returns(() => true);
53 |
54 | return txRequest;
55 | };
56 |
57 | const createGasPriceUtil = (gasPrice = defaultGasPrice) => {
58 | const gasPriceUtil = TypeMoq.Mock.ofType();
59 | gasPriceUtil.setup(u => u.networkGasPrice()).returns(() => Promise.resolve(gasPrice));
60 | gasPriceUtil.setup(u => u.getGasPrice()).returns(() => Promise.resolve(gasPrice));
61 | gasPriceUtil
62 | .setup(u => u.getAdvancedNetworkGasPrice())
63 | .returns(() =>
64 | Promise.resolve({
65 | safeLow: gasPrice,
66 | average: gasPrice,
67 | fast: gasPrice,
68 | fastest: gasPrice
69 | } as GasPriceEstimation)
70 | );
71 |
72 | return gasPriceUtil.object;
73 | };
74 |
75 | const calculateExpectedRewardWhenClaiming = (
76 | txRequest: ITransactionRequest,
77 | paymentModifier: BigNumber | number,
78 | claimingGasCost: BigNumber | number,
79 | executionSubsidy: BigNumber | number
80 | ) =>
81 | txRequest.bounty
82 | .times(paymentModifier)
83 | .minus(claimingGasCost)
84 | .minus(executionSubsidy)
85 | .decimalPlaces(0);
86 |
87 | const calculateExpectedRewardWhenExecuting = (
88 | txRequest: ITransactionRequest,
89 | paymentModifier: BigNumber | number,
90 | executionSubsidy: BigNumber | number
91 | ) =>
92 | txRequest.bounty
93 | .times(paymentModifier)
94 | .minus(executionSubsidy)
95 | .plus(txRequest.isClaimed ? txRequest.requiredDeposit : 0)
96 | .decimalPlaces(0);
97 |
98 | const zeroProfitabilityExecutionGasPrice = (
99 | txRequest: ITransactionRequest,
100 | paymentModifier: BigNumber | number,
101 | executionGasAmount: BigNumber
102 | ) =>
103 | txRequest.bounty
104 | .times(paymentModifier)
105 | .plus(txRequest.isClaimed ? txRequest.requiredDeposit : 0)
106 | .dividedBy(executionGasAmount)
107 | .plus(txRequest.gasPrice);
108 |
109 | describe('claiming profitability', () => {
110 | it('calculates profitability with default values', async () => {
111 | const strategy = new ProfitabilityCalculator(util, createGasPriceUtil());
112 |
113 | const paymentModifier = defaultPaymentModifier.div(100);
114 | const claimingGasCost = defaultGasPrice.times(CLAIMING_GAS_ESTIMATE);
115 | const executionSubsidy = 0;
116 |
117 | const txRequest = createTxRequest().object;
118 | const expectedReward = calculateExpectedRewardWhenClaiming(
119 | txRequest,
120 | paymentModifier,
121 | claimingGasCost,
122 | executionSubsidy
123 | );
124 |
125 | const result = await strategy.claimingProfitability(txRequest, defaultGasPrice);
126 |
127 | assert.isTrue(expectedReward.isEqualTo(result));
128 | assert.isTrue(result.isGreaterThan(0));
129 | });
130 |
131 | it('calculates profitability with 0 minimum execution gas price', async () => {
132 | const strategy = new ProfitabilityCalculator(util, createGasPriceUtil());
133 | const paymentModifier = defaultPaymentModifier.div(100);
134 | const claimingGasCost = defaultGasPrice.times(CLAIMING_GAS_ESTIMATE);
135 |
136 | const transactionExecutionGasPrice = new BigNumber(0);
137 | const txRequest = createTxRequest(transactionExecutionGasPrice).object;
138 |
139 | const executionSubsidy = util.calculateGasAmount(txRequest).times(defaultGasPrice); // this means that max gas Price is 2x
140 | const expectedReward = calculateExpectedRewardWhenClaiming(
141 | txRequest,
142 | paymentModifier,
143 | claimingGasCost,
144 | executionSubsidy
145 | );
146 |
147 | const result = await strategy.claimingProfitability(txRequest, defaultGasPrice);
148 |
149 | assert.isTrue(expectedReward.isEqualTo(result));
150 | assert.isTrue(result.isGreaterThan(0));
151 | });
152 | });
153 |
154 | describe('execution profitability', () => {
155 | it('calculates profitability with default values', async () => {
156 | const strategy = new ProfitabilityCalculator(util, createGasPriceUtil());
157 |
158 | const paymentModifier = defaultPaymentModifier.div(100);
159 | const executionSubsidy = 0;
160 |
161 | const txRequest = createTxRequest().object;
162 | const expectedReward = calculateExpectedRewardWhenExecuting(
163 | txRequest,
164 | paymentModifier,
165 | executionSubsidy
166 | );
167 |
168 | const result = await strategy.executionProfitability(txRequest, defaultGasPrice);
169 |
170 | assert.isTrue(expectedReward.isEqualTo(result));
171 | assert.isTrue(result.isGreaterThan(0));
172 | });
173 |
174 | it('returns 0 profitability when network gas price at zero profitability', async () => {
175 | const strategy = new ProfitabilityCalculator(util, createGasPriceUtil());
176 | const txRequest = createTxRequest().object;
177 |
178 | const paymentModifier = defaultPaymentModifier.dividedBy(100);
179 | const executionGasAmount = util.calculateGasAmount(txRequest);
180 |
181 | const maximumGasPrice = zeroProfitabilityExecutionGasPrice(
182 | txRequest,
183 | paymentModifier,
184 | executionGasAmount
185 | );
186 | const executionSubsidy = maximumGasPrice.minus(txRequest.gasPrice).times(executionGasAmount);
187 |
188 | const expectedReward = calculateExpectedRewardWhenExecuting(
189 | txRequest,
190 | paymentModifier,
191 | executionSubsidy
192 | );
193 |
194 | const result = await strategy.executionProfitability(txRequest, maximumGasPrice);
195 |
196 | assert.isTrue(expectedReward.isEqualTo(result));
197 | assert.isTrue(result.isZero());
198 | });
199 |
200 | it('returns negative profitability when network gas price above zero profitability', async () => {
201 | const strategy = new ProfitabilityCalculator(util, createGasPriceUtil());
202 | const txRequest = createTxRequest().object;
203 |
204 | const paymentModifier = defaultPaymentModifier.div(100);
205 | const executionGasAmount = util.calculateGasAmount(txRequest);
206 |
207 | const maximumGasPrice = zeroProfitabilityExecutionGasPrice(
208 | txRequest,
209 | paymentModifier,
210 | executionGasAmount
211 | );
212 | const negativeRewardGasPrice = maximumGasPrice.plus(1);
213 |
214 | const executionSubsidy = negativeRewardGasPrice
215 | .minus(txRequest.gasPrice)
216 | .times(executionGasAmount);
217 |
218 | const expectedReward = calculateExpectedRewardWhenExecuting(
219 | txRequest,
220 | paymentModifier,
221 | executionSubsidy
222 | );
223 |
224 | const result = await strategy.executionProfitability(txRequest, negativeRewardGasPrice);
225 |
226 | assert.isTrue(expectedReward.isEqualTo(result));
227 | assert.isTrue(result.isNegative());
228 | });
229 | });
230 | });
231 |
--------------------------------------------------------------------------------
/test/unit/UnitTestScanner.ts:
--------------------------------------------------------------------------------
1 | /* tslint:disable:no-unused-expression */
2 | import { assert, expect } from 'chai';
3 | import * as TypeMoq from 'typemoq';
4 |
5 | import { Config } from '../../src';
6 | import IRouter from '../../src/Router';
7 | import Scanner from '../../src/Scanner';
8 | import { mockConfig } from '../helpers';
9 | import { Util } from '@ethereum-alarm-clock/lib';
10 |
11 | let config: Config;
12 | let scanner: Scanner;
13 |
14 | const reset = async () => {
15 | const router = TypeMoq.Mock.ofType();
16 | config = await mockConfig();
17 |
18 | scanner = new Scanner(config, router.object);
19 | };
20 |
21 | beforeEach(reset);
22 |
23 | describe('Scanner Unit Tests', () => {
24 | it('initializes the Scanner', () => {
25 | scanner = new Scanner(config, null);
26 | expect(scanner).to.exist;
27 | });
28 |
29 | describe('start()', async () => {
30 | it('returns true for scanning and chainScanner/cacheScanner', async () => {
31 | await scanner.start();
32 | assert.isTrue(scanner.scanning);
33 | expect(scanner.cacheInterval).to.exist;
34 | expect(scanner.chainInterval).to.exist;
35 | }).timeout(5000);
36 |
37 | it('returns true when watching disabled', async () => {
38 | const originalIsWatchingEnabled = Util.isWatchingEnabled;
39 |
40 | Util.isWatchingEnabled = () => Promise.resolve(false);
41 | expect(scanner.start).to.throw;
42 |
43 | Util.isWatchingEnabled = originalIsWatchingEnabled;
44 | }).timeout(5000);
45 | });
46 |
47 | describe('stop()', async () => {
48 | it('returns false for scanning and chainScanner/cacheScanner', async () => {
49 | await scanner.start();
50 | await scanner.stop();
51 | assert.isNotTrue(scanner.scanning);
52 | assert.equal(scanner.cacheInterval[0], null);
53 | assert.equal(scanner.chainInterval[0], null);
54 | }).timeout(50000);
55 | });
56 | });
57 |
--------------------------------------------------------------------------------
/test/unit/UnitTestTimeNode.ts:
--------------------------------------------------------------------------------
1 | import { expect, assert } from 'chai';
2 | import { TimeNode, Config } from '../../src/index';
3 | import { mockConfig } from '../helpers';
4 | import { BigNumber } from 'bignumber.js';
5 | import { TxStatus } from '../../src/Enum';
6 | import { Util } from '@ethereum-alarm-clock/lib';
7 |
8 | let config: Config;
9 | let myAccount: string;
10 | let timenode: TimeNode;
11 | const emitEvents = {
12 | emitClose: (self: any) => {
13 | self.emit('close');
14 | },
15 | emitEnd: (self: any) => {
16 | self.connection._client.emit('connectFailed');
17 | },
18 | emitError: (self: any) => {
19 | //Trigger connection failed event
20 | self.connection._client.emit('connectFailed');
21 | }
22 | };
23 |
24 | before(async () => {
25 | config = await mockConfig();
26 | myAccount = config.wallet.getAddresses()[0];
27 | timenode = new TimeNode(config);
28 | });
29 |
30 | describe('TimeNode Unit Tests', () => {
31 | it('initializes a basic timenode', () => {
32 | expect(timenode).to.exist; // tslint:disable-line no-unused-expression
33 | });
34 |
35 | describe('startScanning()', () => {
36 | it('returns true when started scanning', async () => {
37 | assert.isTrue(await timenode.startScanning());
38 | assert.isTrue(timenode.scanner.scanning);
39 | }).timeout(5000);
40 |
41 | it('hard resets the scanner module when already scanning', async () => {
42 | timenode.scanner.scanning = true;
43 | assert.isTrue(await timenode.startScanning());
44 | assert.isTrue(timenode.scanner.scanning);
45 | }).timeout(5000);
46 | });
47 |
48 | describe('startClaiming()', () => {
49 | it('returns false when stopped scanning', async () => {
50 | assert.isFalse(await timenode.stopScanning());
51 | }).timeout(5000);
52 | });
53 |
54 | describe('startClaiming()', () => {
55 | it('returns true when started claiming', () => {
56 | assert.isTrue(timenode.startClaiming());
57 | assert.isTrue(timenode.config.claiming);
58 | });
59 | });
60 |
61 | describe('stopClaiming()', () => {
62 | it('returns false when stopped claiming', () => {
63 | assert.isFalse(timenode.stopClaiming());
64 | assert.isFalse(timenode.config.claiming);
65 | });
66 | });
67 |
68 | describe('logNetwork()', () => {
69 | it('logs the network id', async () => {
70 | let networkLogged = false;
71 |
72 | timenode.config.logger.info = () => {
73 | networkLogged = true;
74 | };
75 |
76 | timenode.config.web3.eth.net.getId = () => Promise.resolve(1);
77 |
78 | await timenode.logNetwork();
79 | assert.isTrue(networkLogged);
80 | });
81 | });
82 |
83 | describe('getClaimedNotExecutedTransactions()', () => {
84 | beforeEach(async () => {
85 | config = await mockConfig();
86 | timenode = new TimeNode(config);
87 | timenode.config.statsDb.clearAll();
88 | });
89 |
90 | it('returns 0 when no transactions', () => {
91 | const txs = timenode.getClaimedNotExecutedTransactions()[myAccount];
92 | assert.equal(txs.length, 0);
93 | });
94 |
95 | it('returns a transaction', () => {
96 | const tx = {
97 | bounty: new BigNumber(10e9), // 10 gwei
98 | temporalUnit: 1,
99 | claimedBy: config.wallet.getAddresses()[0],
100 | wasCalled: false,
101 | windowStart: new BigNumber(10000),
102 | claimWindowStart: new BigNumber(9000),
103 | status: TxStatus.FreezePeriod
104 | };
105 | config.cache.set('tx', tx);
106 |
107 | const txs = timenode.getClaimedNotExecutedTransactions()[myAccount];
108 | assert.equal(txs.length, 1);
109 | });
110 | });
111 |
112 | describe('getUnsucessfullyClaimedTransactions()', () => {
113 | it('returns empty array when no failed claims', () => {
114 | const txs = timenode.getUnsucessfullyClaimedTransactions()[myAccount];
115 | assert.equal(txs.length, 0);
116 | });
117 |
118 | it('returns failed claims when they are present', () => {
119 | const failedClaimAddress = '0xe87529a6123a74320e13a6dabf3606630683c029';
120 |
121 | config.statsDb.claimed(
122 | config.wallet.getAddresses()[0],
123 | failedClaimAddress,
124 | new BigNumber(0),
125 | false
126 | );
127 |
128 | const txs = timenode.getUnsucessfullyClaimedTransactions()[myAccount];
129 | assert.equal(txs.length, 1);
130 |
131 | assert.deepEqual(txs, ['0xe87529a6123a74320e13a6dabf3606630683c029']);
132 | });
133 | });
134 |
135 | describe('handleDisconnections', () => {
136 | it('detects Error Disconnect', async () => {
137 | const newconfig = await mockConfig();
138 | if (!Util.isWSConnection(newconfig.providerUrls[0])) {
139 | return;
140 | }
141 | const runningNode = new TimeNode(newconfig);
142 | let triggered: boolean;
143 | await runningNode.startScanning();
144 | assert.isTrue(runningNode.scanner.scanning);
145 | Object.assign(runningNode.wsReconnect, {
146 | handleWsDisconnect: () => {
147 | triggered = true;
148 | runningNode.stopScanning();
149 | }
150 | });
151 | emitEvents.emitError(runningNode.config.web3.currentProvider);
152 | setTimeout(() => {
153 | assert.isTrue(triggered, 'Disconnect not detected');
154 | }, 7000);
155 | });
156 |
157 | it('detects End Disconnect', async () => {
158 | const newconfig = await mockConfig();
159 | if (!Util.isWSConnection(newconfig.providerUrls[0])) {
160 | return;
161 | }
162 | const runningNode = new TimeNode(newconfig);
163 | let triggered: boolean;
164 | await runningNode.startScanning();
165 | assert.isTrue(runningNode.scanner.scanning);
166 | Object.assign(runningNode.wsReconnect, {
167 | handleWsDisconnect: () => {
168 | triggered = true;
169 | runningNode.stopScanning();
170 | }
171 | });
172 | emitEvents.emitEnd(runningNode.config.web3.currentProvider);
173 | setTimeout(() => {
174 | assert.isTrue(triggered, 'Disconnect not detected');
175 | }, 7000);
176 | });
177 |
178 | it('does not restart connection on stop Timenode', async () => {
179 | const newconfig = await mockConfig();
180 | if (!Util.isWSConnection(newconfig.providerUrls[0])) {
181 | return;
182 | }
183 | const runningNode = new TimeNode(newconfig);
184 | let triggered: boolean;
185 | await runningNode.startScanning();
186 | assert.isTrue(runningNode.scanner.scanning);
187 | Object.assign(runningNode, {
188 | wsReconnect: () => {
189 | triggered = true;
190 | }
191 | });
192 | runningNode.stopScanning();
193 | assert.isUndefined(triggered, 'Invalid Disconnect detected');
194 | });
195 | });
196 | });
197 |
--------------------------------------------------------------------------------
/test/unit/UnitTestTxPoolProcessor.ts:
--------------------------------------------------------------------------------
1 | import { Util } from '@ethereum-alarm-clock/lib';
2 | import { BigNumber } from 'bignumber.js';
3 | import { assert } from 'chai';
4 | import * as TypeMoq from 'typemoq';
5 | import { Log } from 'web3/types';
6 |
7 | import { CLAIMED_EVENT } from '../../src/Actions/Helpers';
8 | import { ITxPoolTxDetails } from '../../src/TxPool';
9 | import TxPoolProcessor from '../../src/TxPool/TxPoolProcessor';
10 | import { Operation } from '../../src/Types/Operation';
11 |
12 | describe('TxPoolProcessor Unit Tests', () => {
13 | function setup(tx: { to: string; gasPrice: BigNumber }) {
14 | const util = TypeMoq.Mock.ofType();
15 | util.setup(u => u.getTransaction(TypeMoq.It.isAnyString())).returns(async () => tx as any);
16 | const pool = new Map();
17 | const processor = new TxPoolProcessor(util.object);
18 | return { processor, pool };
19 | }
20 |
21 | it('registers tx in pool', async () => {
22 | const address = '1';
23 | const gasPrice = new BigNumber(10000);
24 | const transactionHash = '1';
25 |
26 | const tx = { to: address, gasPrice };
27 | const { processor, pool } = setup(tx);
28 |
29 | const filterTx: Log = {
30 | address,
31 | blockNumber: 1,
32 | data: '',
33 | logIndex: 0,
34 | blockHash: '',
35 | transactionIndex: 0,
36 | topics: [CLAIMED_EVENT],
37 | transactionHash
38 | };
39 |
40 | await processor.process(filterTx, pool);
41 |
42 | const res = pool.get(transactionHash);
43 |
44 | assert.isTrue(pool.has(transactionHash));
45 | assert.equal(res.operation, Operation.CLAIM);
46 | assert.equal(res.to, address);
47 | assert.isTrue(res.gasPrice.isEqualTo(gasPrice));
48 | });
49 | });
50 |
--------------------------------------------------------------------------------
/test/unit/UnitTestWatchableBucket.ts:
--------------------------------------------------------------------------------
1 | import * as TypeMoq from 'typemoq';
2 | import { IBucketWatcher } from '../../src/Scanner/IBucketWatcher';
3 | import { WatchableBucket } from '../../src/Scanner/WatchableBucket';
4 |
5 | describe('WatchableBucket', () => {
6 | it('should not stop previous watch when there was not any started', async () => {
7 | const requestFactoryMock = TypeMoq.Mock.ofType();
8 | requestFactoryMock
9 | .setup(r => r.stopWatch(TypeMoq.It.isAny()))
10 | .verifiable(TypeMoq.Times.exactly(0));
11 |
12 | const bucket = 1;
13 | const watchableBucket = new WatchableBucket(bucket, requestFactoryMock.object, null);
14 |
15 | await watchableBucket.stop();
16 |
17 | requestFactoryMock.verifyAll();
18 | });
19 |
20 | it('should stop previous watch if there was any started', async () => {
21 | const requestFactoryMock = TypeMoq.Mock.ofType();
22 | requestFactoryMock.setup(r => r.stopWatch(TypeMoq.It.isAny())).verifiable(TypeMoq.Times.once());
23 | requestFactoryMock
24 | .setup(r => r.watchRequestsByBucket(TypeMoq.It.isAny(), TypeMoq.It.isAny()))
25 | .returns(value => value);
26 |
27 | const bucket = 1;
28 |
29 | const watchableBucket = new WatchableBucket(bucket, requestFactoryMock.object, null);
30 |
31 | await watchableBucket.watch();
32 | await watchableBucket.stop();
33 |
34 | requestFactoryMock.verifyAll();
35 | });
36 |
37 | it('should not stop previous more than once', async () => {
38 | const requestFactoryMock = TypeMoq.Mock.ofType();
39 | requestFactoryMock.setup(r => r.stopWatch(TypeMoq.It.isAny())).verifiable(TypeMoq.Times.once());
40 | requestFactoryMock
41 | .setup(r => r.watchRequestsByBucket(TypeMoq.It.isAny(), TypeMoq.It.isAny()))
42 | .returns(value => value);
43 |
44 | const bucket = 1;
45 |
46 | const watchableBucket = new WatchableBucket(bucket, requestFactoryMock.object, null);
47 |
48 | await watchableBucket.watch();
49 | await watchableBucket.stop();
50 | await watchableBucket.stop();
51 |
52 | requestFactoryMock.verifyAll();
53 | });
54 | });
55 |
--------------------------------------------------------------------------------
/test/unit/dependencies.ts:
--------------------------------------------------------------------------------
1 | import { assert } from 'chai';
2 |
3 | // from https://docs.npmjs.com/files/package.json#dependencies
4 | const nonExactPrefixes = ['~', '^', '>', '>=', '<', '<='];
5 | const packageJSON = require('../../package.json');
6 |
7 | describe('package.json', () => {
8 | const assertVersion = (version: string) => {
9 | nonExactPrefixes.forEach(badPrefix => {
10 | assert.isFalse(version.includes(badPrefix));
11 | });
12 | };
13 | it('dependencies should not contain any non-exact versions', () => {
14 | const deps = Object.keys(packageJSON.dependencies).map(key => packageJSON.dependencies[key]);
15 | deps.forEach(assertVersion);
16 | });
17 | it('devDependencies should not contain any non-exact versions', () => {
18 | const deps = Object.keys(packageJSON.devDependencies).map(
19 | key => packageJSON.devDependencies[key]
20 | );
21 | deps.forEach(assertVersion);
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "suppressImplicitAnyIndexErrors": true,
4 | "noImplicitAny": true,
5 | "noUnusedLocals": true,
6 | "noImplicitReturns": true,
7 | "noUnusedParameters": false,
8 | "outDir": "./built",
9 | "allowJs": true,
10 | "lib": [
11 | "es2015"
12 | ],
13 | "target": "es2015",
14 | "moduleResolution": "node",
15 | "module": "none",
16 | "sourceMap": true
17 | },
18 | "include": [
19 | "./src/**/*",
20 | "./test/**/*"
21 | ]
22 | }
23 |
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "defaultSeverity": "error",
3 | "extends": ["tslint:recommended", "tslint-config-prettier", "tslint-sonarts"],
4 | "jsRules": {
5 | "quotemark": false,
6 | "max-line-length": false,
7 | "trailing-comma": false,
8 | "object-literal-sort-keys": false
9 | },
10 | "rules": {
11 | "quotemark": false,
12 | "trailing-comma": false,
13 | "object-literal-sort-keys": false,
14 | "arrow-return-shorthand": true,
15 | "prefer-method-signature": true,
16 | "arrow-parens": false,
17 | "no-unnecessary-type-assertion": true,
18 | "array-type": [true, "array"],
19 | "interface-name": false,
20 | "no-duplicate-imports": true,
21 | "no-console": false,
22 | "no-var-requires": false,
23 | "comment-format": false,
24 | "ordered-imports": false,
25 | "await-promise": [true, "Transition"],
26 | "no-unsafe-finally": true,
27 | "no-return-await": true
28 | },
29 | "rulesDirectory": [
30 | "node_modules/tslint-microsoft-contrib"
31 | ]
32 | }
--------------------------------------------------------------------------------