From 9b3a3bc5b88e01b8c5b019c5cdc314816887ec98 Mon Sep 17 00:00:00 2001 From: nivida Date: Mon, 16 Nov 2020 10:26:27 +0100 Subject: [PATCH 001/107] POC govern-tx created --- .../lib/transactions/AbstractTransaction.ts | 56 ++ packages/govern-tx/package.json | 28 + packages/govern-tx/src/auth/Authenticator.ts | 64 ++ packages/govern-tx/src/index.ts | 35 ++ .../src/transactions/ChallengeTransaction.ts | 5 + .../src/transactions/ExecuteTransaction.ts | 0 .../src/transactions/ScheduleTransaction.ts | 5 + packages/govern-tx/test/lib/.gitkeep | 0 packages/govern-tx/test/src/.gitkeep | 0 packages/govern-tx/tsconfig.json | 20 + packages/govern-tx/tslint.json | 6 + yarn.lock | 550 +++++++----------- 12 files changed, 441 insertions(+), 328 deletions(-) create mode 100644 packages/govern-tx/lib/transactions/AbstractTransaction.ts create mode 100644 packages/govern-tx/package.json create mode 100644 packages/govern-tx/src/auth/Authenticator.ts create mode 100644 packages/govern-tx/src/index.ts create mode 100644 packages/govern-tx/src/transactions/ChallengeTransaction.ts create mode 100644 packages/govern-tx/src/transactions/ExecuteTransaction.ts create mode 100644 packages/govern-tx/src/transactions/ScheduleTransaction.ts create mode 100644 packages/govern-tx/test/lib/.gitkeep create mode 100644 packages/govern-tx/test/src/.gitkeep create mode 100644 packages/govern-tx/tsconfig.json create mode 100644 packages/govern-tx/tslint.json diff --git a/packages/govern-tx/lib/transactions/AbstractTransaction.ts b/packages/govern-tx/lib/transactions/AbstractTransaction.ts new file mode 100644 index 000000000..1234b09bb --- /dev/null +++ b/packages/govern-tx/lib/transactions/AbstractTransaction.ts @@ -0,0 +1,56 @@ + +export default abstract class AbstractTransaction { + /** + * The function signature used to create a transaction + * + * @var signature + * + * @protected + */ + protected signature: string; + + /** + * The parameters used to create the transaction + * + * @var {Array parameters} + */ + private parameters: any[]; + + /** + * @param {Configuration} configuration + * @param {Array} parameters + * + * @constructor + */ + constructor(private configuration: Configuration, parameters: any[]) { + this.parameters = this.validateParameters(parameters); + } + + /** + * Validates the given parameters. + * + * @method validateParameters + * + * @param {Array} parameters + * + * @returns {Array} + * + * @protected + */ + protected validateParameters(parameters: any[]): any[] { + return parameters; + } + + /** + * Executes the transaction and returns the TransactionReceipt + * + * @method execute + * + * @returns {Promise} + * + * @public + */ + public execute(): Promise { + // TODO + } +} \ No newline at end of file diff --git a/packages/govern-tx/package.json b/packages/govern-tx/package.json new file mode 100644 index 000000000..92f6a3ce7 --- /dev/null +++ b/packages/govern-tx/package.json @@ -0,0 +1,28 @@ +{ + "name": "govern-tx", + "version": "1.0.0-beta.12", + "description": "Transactions service of Govern", + "main": "./src/index.js", + "scripts": { + "test": "jest" + }, + "authors": [ + { + "name": "Samuel Furter", + "homepage": "https://github.com/nivida" + } + ], + "license": "GPL-3.0", + "devDependencies": { + "@ethereumjs/config-tsc": "^1.1.1", + "@ethereumjs/config-tslint": "^1.1.1", + "@types/jest": "^26.0.15", + "jest": "^26.6.1", + "ts-jest": "^26.4.2", + "tslint": "^6.1.3", + "typescript": "^4.0.5" + }, + "dependencies": { + "fastify": "^3.8.0" + } +} diff --git a/packages/govern-tx/src/auth/Authenticator.ts b/packages/govern-tx/src/auth/Authenticator.ts new file mode 100644 index 000000000..55e1c64b8 --- /dev/null +++ b/packages/govern-tx/src/auth/Authenticator.ts @@ -0,0 +1,64 @@ + +export default class Authenticator { + /** + * Checks if the given public key is existing in the whitelist and if no rate limit is exceeded + * + * @param {string} signedMessage - The signed message from the user + * + * @returns Promise + * + * @public + */ + public authenticate(signedMessage: string): Promise { + return Promise.resolve(true); + } + + /** + * TODO: If a DB is in usage can we remove this method + * + * Checks if the rate limit of the given account is execeeded or if the globally set is + * + * @method isRateLimitExceeded + * + * @param {string} publicKey - Public key of the user + * + * @returns {boolean} + * + * @private + */ + private isRateLimitExceeded(publicKey: string): boolean { + return false; + } + + /** + * TODO: If a DB is in usage can we remove this method + * + * Checks if the given public key is existing in the whitelist + * + * @method isKnown + * + * @param {string} publicKey - Public key of the user + * + * @returns {boolean} + * + * @private + */ + private isKnown(publicKey: string): boolean { + return true; // TODO: implement whitelist check + } + + /** + * Extracts the public key from the given signed message + * + * @method getPublicKey + * + * @param signedMessage - The signed message from the user + * + * @returns {boolean} + * + * @private + */ + private getPublicKey(signedMessage: string): string { + return '0x0...'; + } +} \ No newline at end of file diff --git a/packages/govern-tx/src/index.ts b/packages/govern-tx/src/index.ts new file mode 100644 index 000000000..835b79b43 --- /dev/null +++ b/packages/govern-tx/src/index.ts @@ -0,0 +1,35 @@ +import fastify from 'fastify' + +import ExecuteTransaction from './transactions/ExecuteTransaction' +import ChallengeTransaction from './transactions/ChallengeTransaction' +import ScheduleTransaction from './transactions/ScheduleTransaction' + +const server = fastify() +const config = new Configuration() + +// TODO: define request schema (eg. request validation, auth pre handler etc.) + +// Calls GovernQueue.execute +server.post('/execute', {}, async (request, reply) => { + return await new ExecuteTransaction(config, request.params).execute() +}) + +// Calls GovernQueue.schedule +server.post('/schedule', {}, async () => { + return await new ScheduleTransaction(config, request.params).execute() +}) + +// Calls GovernQueue.challenge +server.post('/challenge', {}, async () => { + return await new ChallengeTransaction(config, request.params).execute() +}) + + +server.listen(4040, (error, address) => { + if (error) { + console.error(error) + process.exit(0) + } + + console.log(`Server is listening at ${address}`) +}) diff --git a/packages/govern-tx/src/transactions/ChallengeTransaction.ts b/packages/govern-tx/src/transactions/ChallengeTransaction.ts new file mode 100644 index 000000000..ee4541616 --- /dev/null +++ b/packages/govern-tx/src/transactions/ChallengeTransaction.ts @@ -0,0 +1,5 @@ +import AbstractTransaction from '../../lib/transactions/AbstractTransaction'; + +export default class ChallengeTransaction extends AbstractTransaction { + protected signature: string = 'challenge(...)'; +} \ No newline at end of file diff --git a/packages/govern-tx/src/transactions/ExecuteTransaction.ts b/packages/govern-tx/src/transactions/ExecuteTransaction.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/govern-tx/src/transactions/ScheduleTransaction.ts b/packages/govern-tx/src/transactions/ScheduleTransaction.ts new file mode 100644 index 000000000..12ba5ca7b --- /dev/null +++ b/packages/govern-tx/src/transactions/ScheduleTransaction.ts @@ -0,0 +1,5 @@ +import AbstractTransaction from '../../lib/transactions/AbstractTransaction'; + +export default class ScheduleTransaction extends AbstractTransaction { + protected signature: string = 'schedule(...)'; +} diff --git a/packages/govern-tx/test/lib/.gitkeep b/packages/govern-tx/test/lib/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/packages/govern-tx/test/src/.gitkeep b/packages/govern-tx/test/src/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/packages/govern-tx/tsconfig.json b/packages/govern-tx/tsconfig.json new file mode 100644 index 000000000..f7e9a54b5 --- /dev/null +++ b/packages/govern-tx/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "@ethereumjs/config-tsc", + "compilerOptions": { + "removeComments": true, + "strictPropertyInitialization": false, + "outDir": "./dist", + "inlineSources": true, + "types": [ + "jest" + ], + "lib": [ + "dom", + "es2018" + ] + }, + "include": [ + "./internal/**/*", + "./public/**/*" + ] +} diff --git a/packages/govern-tx/tslint.json b/packages/govern-tx/tslint.json new file mode 100644 index 000000000..8d923d657 --- /dev/null +++ b/packages/govern-tx/tslint.json @@ -0,0 +1,6 @@ +{ + "extends": "@ethereumjs/config-tslint", + "rules": { + "prefer-conditional-expression": true + } +} diff --git a/yarn.lock b/yarn.lock index d06245bad..d99d432df 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4058,7 +4058,7 @@ dependencies: "@types/istanbul-lib-report" "*" -"@types/jest@26.x", "@types/jest@^26.0.0", "@types/jest@^26.0.14", "@types/jest@^26.0.15": +"@types/jest@26.x", "@types/jest@^26.0.14", "@types/jest@^26.0.15": version "26.0.15" resolved "https://registry.yarnpkg.com/@types/jest/-/jest-26.0.15.tgz#12e02c0372ad0548e07b9f4e19132b834cb1effe" integrity sha512-s2VMReFXRg9XXxV+CW9e5Nz8fH2K1aEhwgjUqPPbQd7g95T0laAcvLv032EhFHIa5GHsZ8W7iJEQVaJq6k3Gog== @@ -4392,7 +4392,7 @@ resolved "https://registry.yarnpkg.com/@types/zen-observable/-/zen-observable-0.8.1.tgz#5668c0bce55a91f2b9566b1d8a4c0a8dbbc79764" integrity sha512-wmk0xQI6Yy7Fs/il4EpOcflG4uonUpYGqvZARESLc2oy4u69fkatFLbJOeW4Q6awO15P4rduAe6xkwHevpXcUQ== -"@typescript-eslint/eslint-plugin@^2.10.0", "@typescript-eslint/eslint-plugin@^2.29.0": +"@typescript-eslint/eslint-plugin@^2.10.0": version "2.34.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.34.0.tgz#6f8ce8a46c7dea4a6f1d171d2bb8fbae6dac2be9" integrity sha512-4zY3Z88rEE99+CNvTbXSyovv2z9PNOVffTWD2W8QF5s2prBQtwN2zadqERcrHpcR7O/+KMI3fcTAmUUhK/iQcQ== @@ -4412,7 +4412,7 @@ eslint-scope "^5.0.0" eslint-utils "^2.0.0" -"@typescript-eslint/parser@^2.10.0", "@typescript-eslint/parser@^2.29.0": +"@typescript-eslint/parser@^2.10.0": version "2.34.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-2.34.0.tgz#50252630ca319685420e9a39ca05fe185a256bc8" integrity sha512-03ilO0ucSD0EPTw2X4PntSIRFtDPWjrVq7C3/Z3VQHRC7+13YB55rcJI3Jt+YgeHbjUdJPcPa7b23rXCBokuyA== @@ -4954,6 +4954,11 @@ abstract-leveldown@~2.6.0: dependencies: xtend "~4.0.0" +abstract-logging@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/abstract-logging/-/abstract-logging-2.0.1.tgz#6b0c371df212db7129b57d2e7fcf282b8bf1c839" + integrity sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA== + accepts@^1.3.5, accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.7: version "1.3.7" resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd" @@ -5094,7 +5099,7 @@ ajv-keywords@^3.1.0, ajv-keywords@^3.4.1, ajv-keywords@^3.5.2: resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== -ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.3, ajv@^6.12.4: +ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.2, ajv@^6.11.0, ajv@^6.12.2, ajv@^6.12.3, ajv@^6.12.4: version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== @@ -5469,6 +5474,11 @@ archive-type@^4.0.0: dependencies: file-type "^4.2.0" +archy@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/archy/-/archy-1.0.0.tgz#f9c8c13757cc1dd7bc379ac77b2c62a5c2868c40" + integrity sha1-+cjBN1fMHde8N5rHeyxipcKGjEA= + are-we-there-yet@~1.1.2: version "1.1.5" resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz#4b35c2944f062a8bfcda66410760350fe9ddfc21" @@ -5712,14 +5722,6 @@ async-eventemitter@^0.2.2: dependencies: async "^2.4.0" -async-kit@^2.2.3: - version "2.2.4" - resolved "https://registry.yarnpkg.com/async-kit/-/async-kit-2.2.4.tgz#53249064fc5c894c46210cbd1c1a9ff5bd44bf9f" - integrity sha512-LuWbpSYdTwrGv5MWhsUY69UaQAc3AYMwf/LwTupotu/ubb/1TjDd03WK1eoMXRK/s3bmi4aUkKY0TmxYQgRrmw== - dependencies: - nextgen-events "^0.14.5" - tree-kit "^0.5.27" - async-limiter@^1.0.0, async-limiter@~1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd" @@ -5776,6 +5778,11 @@ atob@^2.1.2: resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== +atomic-sleep@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/atomic-sleep/-/atomic-sleep-1.0.0.tgz#eb85b77a601fc932cfe432c5acd364a9e2c9075b" + integrity sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ== + authereum@^0.0.4-beta.157: version "0.0.4-beta.201" resolved "https://registry.yarnpkg.com/authereum/-/authereum-0.0.4-beta.201.tgz#ea380efc6d231dc4222dc20cd9395b02318dd0c3" @@ -5809,6 +5816,16 @@ autoprefixer@^9.6.1: postcss "^7.0.32" postcss-value-parser "^4.1.0" +avvio@^7.1.2: + version "7.2.0" + resolved "https://registry.yarnpkg.com/avvio/-/avvio-7.2.0.tgz#b4bf4eaf4a0207a4e6a58a7859207250793cc81b" + integrity sha512-KtC63UyZARidAoIV8wXutAZnDIbZcXBqLjTAhZOX+mdMZBQCh5il/15MvCvma1178nhTwvN2D0TOAdiKG1MpUA== + dependencies: + archy "^1.0.0" + debug "^4.0.0" + fastq "^1.6.1" + queue-microtask "^1.1.2" + await-semaphore@^0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/await-semaphore/-/await-semaphore-0.1.3.tgz#2b88018cc8c28e06167ae1cdff02504f1f9688d3" @@ -7389,7 +7406,16 @@ chai@^4.2.0: pathval "^1.1.0" type-detect "^4.0.5" -chalk@1.x, chalk@^1.1.3: +chalk@2.4.2, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0, chalk@^2.3.1, chalk@^2.3.2, chalk@^2.4.1, chalk@^2.4.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +chalk@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" integrity sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg= @@ -7400,15 +7426,6 @@ chalk@1.x, chalk@^1.1.3: strip-ansi "^3.0.0" supports-color "^2.0.0" -chalk@2.4.2, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0, chalk@^2.3.1, chalk@^2.3.2, chalk@^2.4.1, chalk@^2.4.2: - version "2.4.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" - integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== - dependencies: - ansi-styles "^3.2.1" - escape-string-regexp "^1.0.5" - supports-color "^5.3.0" - chalk@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4" @@ -7606,11 +7623,6 @@ cli-cursor@^3.1.0: dependencies: restore-cursor "^3.1.0" -cli-spinners@^1.0.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-1.3.1.tgz#002c1990912d0d59580c93bd36c056de99e4259a" - integrity sha512-1QL4544moEsDVH9T/l6Cemov/37iv1RtoKf7NJ04A60+4MREXNfx/QvavbH6QoGdsD4N4Mwy49cmaINR/o2mdg== - cli-spinners@^2.2.0: version "2.5.0" resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.5.0.tgz#12763e47251bf951cb75c201dfa58ff1bcb2d047" @@ -7830,11 +7842,6 @@ command-line-args@^4.0.7: find-replace "^1.0.3" typical "^2.6.1" -commander@2.19.0: - version "2.19.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a" - integrity sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg== - commander@3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/commander/-/commander-3.0.2.tgz#6837c3fb677ad9933d1cfba42dd14d5117d6b39e" @@ -7868,11 +7875,6 @@ compare-func@^2.0.0: array-ify "^1.0.0" dot-prop "^5.1.0" -compare-versions@^3.6.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-3.6.0.tgz#1a5689913685e5a87637b8d3ffca75514ec41d62" - integrity sha512-W6Af2Iw1z4CB7q4uU4hv646dW9GQuBM+YpC0UvUCWSD8w90SJjp+ujJuXaEMtAXBtSqGfMPuFOVn4/+FlaqfBA== - component-emitter@^1.2.1: version "1.3.0" resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" @@ -8091,7 +8093,7 @@ cookie@0.4.0: resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba" integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg== -cookie@^0.4.1: +cookie@^0.4.0, cookie@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.1.tgz#afd713fe26ebd21ba95ceb61f9a8116e50a537d1" integrity sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA== @@ -8175,17 +8177,6 @@ cosmiconfig@^5.0.0, cosmiconfig@^5.1.0, cosmiconfig@^5.2.1: js-yaml "^3.13.1" parse-json "^4.0.0" -cosmiconfig@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.0.0.tgz#ef9b44d773959cae63ddecd122de23853b60f8d3" - integrity sha512-pondGvTuVYDk++upghXJabWzL6Kxu6f26ljFw64Swq9v6sQPUL3EUlVDV56diOjpCayKihL6hVe8exIACU4XcA== - dependencies: - "@types/parse-json" "^4.0.0" - import-fresh "^3.2.1" - parse-json "^5.0.0" - path-type "^4.0.0" - yaml "^1.10.0" - coveralls@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/coveralls/-/coveralls-3.1.0.tgz#13c754d5e7a2dd8b44fe5269e21ca394fb4d615b" @@ -8245,15 +8236,6 @@ cross-spawn@7.0.1: shebang-command "^2.0.0" which "^2.0.1" -cross-spawn@^5.0.1: - version "5.1.0" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" - integrity sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk= - dependencies: - lru-cache "^4.0.1" - shebang-command "^1.2.0" - which "^1.2.9" - cross-spawn@^6.0.0, cross-spawn@^6.0.5: version "6.0.5" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" @@ -9022,11 +9004,6 @@ dicer@0.3.0: dependencies: streamsearch "0.1.2" -diff-match-patch@^1.0.0: - version "1.0.5" - resolved "https://registry.yarnpkg.com/diff-match-patch/-/diff-match-patch-1.0.5.tgz#abb584d5f10cd1196dfc55aa03701592ae3f7b37" - integrity sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw== - diff-sequences@^24.9.0: version "24.9.0" resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-24.9.0.tgz#5715d6244e2aa65f48bba0bc972db0b0b11e95b5" @@ -10676,7 +10653,7 @@ ethers@^4.0.0-beta.1, ethers@^4.0.32: uuid "2.0.1" xmlhttprequest "1.8.0" -ethers@^5.0.0, ethers@^5.0.1, ethers@^5.0.14, ethers@^5.0.16, ethers@^5.0.2, ethers@^5.0.7: +ethers@^5.0.0, ethers@^5.0.1, ethers@^5.0.14, ethers@^5.0.2, ethers@^5.0.7: version "5.0.19" resolved "https://registry.yarnpkg.com/ethers/-/ethers-5.0.19.tgz#a4636f62a180135b13fd1f0a393477beafd535b7" integrity sha512-0AZnUgZh98q888WAd1oI3aLeI+iyDtrupjANVtPPS7O63lVopkR/No8A1NqSkgl/rU+b2iuu2mUZor6GD4RG2w== @@ -10778,32 +10755,6 @@ exec-sh@^0.3.2: resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.3.4.tgz#3a018ceb526cc6f6df2bb504b2bfe8e3a4934ec5" integrity sha512-sEFIkc61v75sWeOe72qyrqg2Qg0OuLESziUDk/O/z2qgS15y2gWVFrI6f2Qn/qw/0/NCfCEsmNA4zOjkwEZT1A== -execa@0.6.3: - version "0.6.3" - resolved "https://registry.yarnpkg.com/execa/-/execa-0.6.3.tgz#57b69a594f081759c69e5370f0d17b9cb11658fe" - integrity sha1-V7aaWU8IF1nGnlNw8NF7nLEWWP4= - dependencies: - cross-spawn "^5.0.1" - get-stream "^3.0.0" - is-stream "^1.1.0" - npm-run-path "^2.0.0" - p-finally "^1.0.0" - signal-exit "^3.0.0" - strip-eof "^1.0.0" - -execa@^0.9.0: - version "0.9.0" - resolved "https://registry.yarnpkg.com/execa/-/execa-0.9.0.tgz#adb7ce62cf985071f60580deb4a88b9e34712d01" - integrity sha512-BbUMBiX4hqiHZUA5+JujIjNb6TyAlp2D5KLheMjMluwOuzcnylDL4AxZYLLn1n2AGB49eSWwyKvvEQoRpnAtmA== - dependencies: - cross-spawn "^5.0.1" - get-stream "^3.0.0" - is-stream "^1.1.0" - npm-run-path "^2.0.0" - p-finally "^1.0.0" - signal-exit "^3.0.0" - strip-eof "^1.0.0" - execa@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8" @@ -11023,6 +10974,11 @@ fake-merkle-patricia-tree@^1.0.1: dependencies: checkpoint-store "^1.1.0" +fast-decode-uri-component@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz#46f8b6c22b30ff7a81357d4f59abfae938202543" + integrity sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg== + fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" @@ -11062,16 +11018,62 @@ fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0: resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== +fast-json-stringify@^2.2.1: + version "2.2.9" + resolved "https://registry.yarnpkg.com/fast-json-stringify/-/fast-json-stringify-2.2.9.tgz#6298eb0a78e540d74b7507afd55d1a456d584d96" + integrity sha512-O8YmNoc7LnfSafVaTfa1yXVFT4UMsi/N7cYcNZw6w5D5tltyu6XGXvH45mvWfsrcFoSK+H0q0exGXsUqC18z/g== + dependencies: + ajv "^6.11.0" + deepmerge "^4.2.2" + string-similarity "^4.0.1" + fast-levenshtein@~2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= -fast-safe-stringify@^2.0.6: +fast-redact@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/fast-redact/-/fast-redact-3.0.0.tgz#ac2f9e36c9f4976f5db9fb18c6ffbaf308cf316d" + integrity sha512-a/S/Hp6aoIjx7EmugtzLqXmcNsyFszqbt6qQ99BdG61QjBZF6shNis0BYR6TsZOQ1twYc0FN2Xdhwwbv6+KD0w== + +fast-safe-stringify@^2.0.6, fast-safe-stringify@^2.0.7: version "2.0.7" resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz#124aa885899261f68aedb42a7c080de9da608743" integrity sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA== +fastify-error@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/fastify-error/-/fastify-error-0.2.0.tgz#9a1c28d4f42b6259e7a549671c8e5e2d85660634" + integrity sha512-zabxsBatj59ROG0fhP36zNdc5Q1/eYeH9oSF9uvfrurZf8/JKfrJbMcIGrLpLWcf89rS6L91RHWm20A/X85hcA== + +fastify-warning@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/fastify-warning/-/fastify-warning-0.2.0.tgz#e717776026a4493dc9a2befa44db6d17f618008f" + integrity sha512-s1EQguBw/9qtc1p/WTY4eq9WMRIACkj+HTcOIK1in4MV5aFaQC9ZCIt0dJ7pr5bIf4lPpHvAtP2ywpTNgs7hqw== + +fastify@^3.8.0: + version "3.8.0" + resolved "https://registry.yarnpkg.com/fastify/-/fastify-3.8.0.tgz#455bfa70394322247050c330d0e52532b349662d" + integrity sha512-w57/uvyQWzF/KSr9CbWQ5nfTqSSfYcmrems9Lc3VvtrAF7EsLbfZQBQZul6xwvE1uEfxA4nGdoUKqpU7xiv7cw== + dependencies: + abstract-logging "^2.0.0" + ajv "^6.12.2" + avvio "^7.1.2" + fast-json-stringify "^2.2.1" + fastify-error "^0.2.0" + fastify-warning "^0.2.0" + find-my-way "^3.0.5" + flatstr "^1.0.12" + light-my-request "^4.2.0" + pino "^6.2.1" + proxy-addr "^2.0.5" + readable-stream "^3.4.0" + rfdc "^1.1.4" + secure-json-parse "^2.0.0" + semver "^7.3.2" + tiny-lru "^7.0.0" + fastq@^1.6.0: version "1.8.0" resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.8.0.tgz#550e1f9f59bbc65fe185cb6a9b4d95357107f481" @@ -11079,6 +11081,13 @@ fastq@^1.6.0: dependencies: reusify "^1.0.4" +fastq@^1.6.1: + version "1.9.0" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.9.0.tgz#e16a72f338eaca48e91b5c23593bcc2ef66b7947" + integrity sha512-i7FVWL8HhVY+CTkwFxkN2mk3h+787ixS5S63eb78diVRc1MCssarHq3W5cj0av7YDSwmaV928RNag+U1etRQ7w== + dependencies: + reusify "^1.0.4" + faye-websocket@^0.10.0: version "0.10.0" resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.10.0.tgz#4e492f8d04dfb6f89003507f6edbf2d501e7c6f4" @@ -11254,6 +11263,15 @@ find-cache-dir@^3.2.0: make-dir "^3.0.2" pkg-dir "^4.1.0" +find-my-way@^3.0.5: + version "3.0.5" + resolved "https://registry.yarnpkg.com/find-my-way/-/find-my-way-3.0.5.tgz#f71c5ef1b4865401e1b97ba428121a8f55439eec" + integrity sha512-FweGg0cv1sBX8z7WhvBX5B5AECW4Zdh/NiB38Oa0qwSNIyPgRBCl/YjxuZn/rz38E/MMBHeVKJ22i7W3c626Gg== + dependencies: + fast-decode-uri-component "^1.0.1" + safe-regex2 "^2.0.0" + semver-store "^0.3.0" + find-replace@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/find-replace/-/find-replace-1.0.3.tgz#b88e7364d2d9c959559f388c66670d6130441fa0" @@ -11292,13 +11310,6 @@ find-up@^2.0.0, find-up@^2.1.0: dependencies: locate-path "^2.0.0" -find-versions@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/find-versions/-/find-versions-3.2.0.tgz#10297f98030a786829681690545ef659ed1d254e" - integrity sha512-P8WRou2S+oe222TOCHitLy8zj+SIsVJh52VP4lvXkaFVnOFFdoWv1H1Jjvel1aI6NCFOAaeAVm8qrI0odiLcww== - dependencies: - semver-regex "^2.0.0" - find-yarn-workspace-root@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/find-yarn-workspace-root/-/find-yarn-workspace-root-1.2.1.tgz#40eb8e6e7c2502ddfaa2577c176f221422f860db" @@ -11328,6 +11339,11 @@ flatmap@0.0.3: resolved "https://registry.yarnpkg.com/flatmap/-/flatmap-0.0.3.tgz#1f18a4d938152d495965f9c958d923ab2dd669b4" integrity sha1-Hxik2TgVLUlZZfnJWNkjqy3WabQ= +flatstr@^1.0.12: + version "1.0.12" + resolved "https://registry.yarnpkg.com/flatstr/-/flatstr-1.0.12.tgz#c2ba6a08173edbb6c9640e3055b95e287ceb5931" + integrity sha512-4zPxDyhCyiN2wIAtSLI6gc82/EjqZc1onI4Mz/l0pWrAlsSfYH/2ZIcU+e3oA2wDwbzIWNKwa23F8rh6+DRWkw== + flatted@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.2.tgz#4575b21e2bcee7434aa9be662f4b7b5f9c2b5138" @@ -11921,17 +11937,6 @@ globals@^9.18.0: resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a" integrity sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ== -globby@6.1.0, globby@^6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/globby/-/globby-6.1.0.tgz#f5a6d70e8395e21c858fb0489d64df02424d506c" - integrity sha1-9abXDoOV4hyFj7BInWTfAkJNUGw= - dependencies: - array-union "^1.0.1" - glob "^7.0.3" - object-assign "^4.0.1" - pify "^2.0.0" - pinkie-promise "^2.0.0" - globby@8.0.2: version "8.0.2" resolved "https://registry.yarnpkg.com/globby/-/globby-8.0.2.tgz#5697619ccd95c5275dbb2d6faa42087c1a941d8d" @@ -11971,6 +11976,17 @@ globby@^11.0.1: merge2 "^1.3.0" slash "^3.0.0" +globby@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-6.1.0.tgz#f5a6d70e8395e21c858fb0489d64df02424d506c" + integrity sha1-9abXDoOV4hyFj7BInWTfAkJNUGw= + dependencies: + array-union "^1.0.1" + glob "^7.0.3" + object-assign "^4.0.1" + pify "^2.0.0" + pinkie-promise "^2.0.0" + globby@^9.2.0: version "9.2.0" resolved "https://registry.yarnpkg.com/globby/-/globby-9.2.0.tgz#fd029a706c703d29bdd170f4b6db3a3f7a7cb63d" @@ -12669,22 +12685,6 @@ husky@^0.14.3: normalize-path "^1.0.0" strip-indent "^2.0.0" -husky@^4.2.5: - version "4.3.0" - resolved "https://registry.yarnpkg.com/husky/-/husky-4.3.0.tgz#0b2ec1d66424e9219d359e26a51c58ec5278f0de" - integrity sha512-tTMeLCLqSBqnflBZnlVDhpaIMucSGaYyX6855jM4AguGeWCeSzNdb1mfyWduTZ3pe3SJVvVWGL0jO1iKZVPfTA== - dependencies: - chalk "^4.0.0" - ci-info "^2.0.0" - compare-versions "^3.6.0" - cosmiconfig "^7.0.0" - find-versions "^3.2.0" - opencollective-postinstall "^2.0.2" - pkg-dir "^4.2.0" - please-upgrade-node "^3.2.0" - slash "^3.0.0" - which-pm-runs "^1.0.0" - iconv-lite@0.4.24, iconv-lite@^0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" @@ -12737,7 +12737,7 @@ ignore-walk@^3.0.1: dependencies: minimatch "^3.0.4" -ignore@^3.3.5, ignore@^3.3.7: +ignore@^3.3.5: version "3.3.10" resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.10.tgz#0a97fb876986e8081c631160f8f9f389157f0043" integrity sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug== @@ -12792,7 +12792,7 @@ import-fresh@^2.0.0: caller-path "^2.0.0" resolve-from "^3.0.0" -import-fresh@^3.0.0, import-fresh@^3.1.0, import-fresh@^3.2.1: +import-fresh@^3.0.0, import-fresh@^3.1.0: version "3.2.1" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.2.1.tgz#633ff618506e793af5ac91bf48b72677e15cbe66" integrity sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ== @@ -12916,7 +12916,7 @@ inquirer@7.0.4: strip-ansi "^5.1.0" through "^2.3.6" -inquirer@^6.2.0, inquirer@^6.2.2: +inquirer@^6.2.0: version "6.5.2" resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-6.5.2.tgz#ad50942375d036d327ff528c08bd5fab089928ca" integrity sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ== @@ -13207,14 +13207,6 @@ is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.2: resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.2.tgz#c7c6715cd22d4ddb48d3e19970223aceabb080d9" integrity sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA== -is-ci-cli@^2.1.1: - version "2.1.2" - resolved "https://registry.yarnpkg.com/is-ci-cli/-/is-ci-cli-2.1.2.tgz#52f233e3f6d7718642f2bb356a5b5110b29d9b02" - integrity sha512-dgGkNUs6ws6RfkjQYwkrFhwkdyNkWxyng1Tz7W7OnkGPhXV9z1ofnzmTmuXpvlCfolB8Z7BBL/Zi3LDB6b2hTg== - dependencies: - cross-spawn "^7.0.0" - is-ci "^2.0.0" - is-ci@^1.0.10: version "1.2.1" resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-1.2.1.tgz#e3779c8ee17fccf428488f6e281187f2e632841c" @@ -14921,11 +14913,6 @@ just-map-keys@^1.1.0: resolved "https://registry.yarnpkg.com/just-map-keys/-/just-map-keys-1.1.0.tgz#9663c9f971ba46e17f2b05e66fec81149375f230" integrity sha512-oNKi+4y7fr8lXnhKYpBbCkiwHRVkAnx0VDkCeTDtKKMzGr1Lz1Yym+RSieKUTKim68emC5Yxrb4YmiF9STDO+g== -kebab-case@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/kebab-case/-/kebab-case-1.0.0.tgz#3f9e4990adcad0c686c0e701f7645868f75f91eb" - integrity sha1-P55JkK3K0MaGwOcB92RYaPdfkes= - keccak256@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/keccak256/-/keccak256-1.0.1.tgz#f579e937d6f32ac4ab62ff862d50204f775bb6f6" @@ -15307,6 +15294,17 @@ libp2p-crypto@~0.16.1: tweetnacl "^1.0.0" ursa-optional "~0.10.0" +light-my-request@^4.2.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/light-my-request/-/light-my-request-4.3.0.tgz#0178dfe7f298089df976f8e4cc0e10998be50aad" + integrity sha512-WrEvI7V41ZbEUe0bsfuS170QrYSVADKA0JiWyK/lVtm4Ra26pl9CYKBdlr823/s37N2wMJze8YNkHbg11aZWAw== + dependencies: + ajv "^6.12.2" + cookie "^0.4.0" + fastify-warning "^0.2.0" + readable-stream "^3.6.0" + set-cookie-parser "^2.4.1" + lines-and-columns@^1.1.6: version "1.1.6" resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" @@ -15573,7 +15571,7 @@ lodash.upperfirst@^4.3.1: resolved "https://registry.yarnpkg.com/lodash.upperfirst/-/lodash.upperfirst-4.3.1.tgz#1365edf431480481ef0d1c68957a5ed99d49f7ce" integrity sha1-E2Xt9DFIBIHvDRxolXpe2Z1J984= -lodash@4.17.20, "lodash@>=3.5 <5", lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.1: +lodash@4.17.20, "lodash@>=3.5 <5", lodash@^4.15.0, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.1: version "4.17.20" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== @@ -15590,13 +15588,6 @@ log-symbols@3.0.0, log-symbols@^3.0.0: dependencies: chalk "^2.4.2" -log-symbols@^2.1.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-2.2.0.tgz#5740e1c5d6f0dfda4ad9323b5332107ef6b4c40a" - integrity sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg== - dependencies: - chalk "^2.0.1" - loglevel@^1.6.6, loglevel@^1.6.7, loglevel@^1.6.8: version "1.7.0" resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.7.0.tgz#728166855a740d59d38db01cf46f042caa041bb0" @@ -15668,14 +15659,6 @@ lru-cache@^3.2.0: dependencies: pseudomap "^1.0.1" -lru-cache@^4.0.1: - version "4.1.5" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd" - integrity sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g== - dependencies: - pseudomap "^1.0.2" - yallist "^2.1.2" - lru_map@^0.3.3: version "0.3.3" resolved "https://registry.yarnpkg.com/lru_map/-/lru_map-0.3.3.tgz#b5c8351b9464cbd750335a79650a0ec0e56118dd" @@ -16314,11 +16297,6 @@ move-concurrently@^1.0.1: rimraf "^2.5.4" run-queue "^1.0.3" -mri@^1.1.0: - version "1.1.6" - resolved "https://registry.yarnpkg.com/mri/-/mri-1.1.6.tgz#49952e1044db21dbf90f6cd92bc9c9a777d415a6" - integrity sha512-oi1b3MfbyGa7FJMP9GmLTttni5JoICpYBRlq+x5V16fZbLsnL9N3wFqqIm/nIG43FjUFkFh9Epzp/kzUGUnJxQ== - ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" @@ -16584,16 +16562,6 @@ next-tick@~1.0.0: resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c" integrity sha1-yobR/ogoFpsBICCOPchCS524NCw= -nextgen-events@^0.14.5: - version "0.14.6" - resolved "https://registry.yarnpkg.com/nextgen-events/-/nextgen-events-0.14.6.tgz#945b3fc75951fe8c945f8455c35bf644a3a2c8b1" - integrity sha512-Ln9d5Midoah7RCxFk8z9tAAcRW/VkB4wZ61Nnw8aqM1/lb/WfPAnlzpLGYRghEjwZdXQNQedTfD/gclYMeI0eQ== - -nextgen-events@^0.9.8: - version "0.9.9" - resolved "https://registry.yarnpkg.com/nextgen-events/-/nextgen-events-0.9.9.tgz#39a8afc4a2b845388c57e2c6bb9716711986a3a0" - integrity sha1-OaivxKK4RTiMV+LGu5cWcRmGo6A= - nice-try@^1.0.4: version "1.0.5" resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" @@ -16982,26 +16950,6 @@ nwsapi@^2.0.7, nwsapi@^2.1.3, nwsapi@^2.2.0: resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.0.tgz#204879a9e3d068ff2a55139c2c772780681a38b7" integrity sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ== -oao@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/oao/-/oao-2.0.0.tgz#2309ea06d9090d55fa1505e0ccee5ec710247c0f" - integrity sha512-ZIOSQ0rqm9CLjCDBARE47QA6Tb/dk4k+s+FuzIpW2ZgfMqh9tYioZ4B2p62Ye3ahRzrpAXVQiPshwPGte2lpiw== - dependencies: - commander "2.19.0" - execa "0.6.3" - globby "6.1.0" - inquirer "^6.2.2" - kebab-case "1.0.0" - minimatch "^3.0.4" - rimraf "2.6.3" - semver "5.6.0" - shelljs "0.7.8" - split "1.0.1" - storyboard "3.1.4" - storyboard-listener-console "3.1.4" - storyboard-listener-console-parallel "3.1.4" - timm "^1.6.2" - oauth-sign@~0.9.0: version "0.9.0" resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" @@ -17224,7 +17172,7 @@ open@^7.0.2: is-docker "^2.0.0" is-wsl "^2.1.1" -opencollective-postinstall@^2.0.0, opencollective-postinstall@^2.0.2: +opencollective-postinstall@^2.0.0: version "2.0.3" resolved "https://registry.yarnpkg.com/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz#7a0fff978f6dbfa4d006238fbac98ed4198c3259" integrity sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q== @@ -17277,16 +17225,6 @@ optionator@^0.8.1, optionator@^0.8.3: type-check "~0.3.2" word-wrap "~1.2.3" -ora@^1.3.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/ora/-/ora-1.4.0.tgz#884458215b3a5d4097592285f93321bb7a79e2e5" - integrity sha512-iMK1DOQxzzh2MBlVsU42G80mnrvUhqsMh74phHtDlrcTZPK0pH6o7l7DRshK+0YsxDyEuaOkziVdvM3T0QTzpw== - dependencies: - chalk "^2.1.0" - cli-cursor "^2.1.0" - cli-spinners "^1.0.1" - log-symbols "^2.1.0" - ora@^4.0.0: version "4.1.1" resolved "https://registry.yarnpkg.com/ora/-/ora-4.1.1.tgz#566cc0348a15c36f5f0e979612842e02ba9dddbc" @@ -17866,6 +17804,23 @@ pinkie@^2.0.0: resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" integrity sha1-clVrgM+g1IqXToDnckjoDtT3+HA= +pino-std-serializers@^2.4.2: + version "2.5.0" + resolved "https://registry.yarnpkg.com/pino-std-serializers/-/pino-std-serializers-2.5.0.tgz#40ead781c65a0ce7ecd9c1c33f409d31fe712315" + integrity sha512-wXqbqSrIhE58TdrxxlfLwU9eDhrzppQDvGhBEr1gYbzzM4KKo3Y63gSjiDXRKLVS2UOXdPNR2v+KnQgNrs+xUg== + +pino@^6.2.1: + version "6.7.0" + resolved "https://registry.yarnpkg.com/pino/-/pino-6.7.0.tgz#d5d96b7004fed78816b5694fda3eab02b5ca6d23" + integrity sha512-vPXJ4P9rWCwzlTJt+f0Ni4THc3DWyt8iDDCO4edQ8narTu6hnpzdXu8FqeSJCGndl1W6lfbYQUQihUO54y66Lw== + dependencies: + fast-redact "^3.0.0" + fast-safe-stringify "^2.0.7" + flatstr "^1.0.12" + pino-std-serializers "^2.4.2" + quick-format-unescaped "^4.0.1" + sonic-boom "^1.0.2" + pirates@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.1.tgz#643a92caf894566f91b2b986d2c66950a8e2fb87" @@ -17913,18 +17868,6 @@ pkginfo@^0.4.1: resolved "https://registry.yarnpkg.com/pkginfo/-/pkginfo-0.4.1.tgz#b5418ef0439de5425fc4995042dced14fb2a84ff" integrity sha1-tUGO8EOd5UJfxJlQQtztFPsqhP8= -platform@1.3.3: - version "1.3.3" - resolved "https://registry.yarnpkg.com/platform/-/platform-1.3.3.tgz#646c77011899870b6a0903e75e997e8e51da7461" - integrity sha1-ZGx3ARiZhwtqCQPnXpl+jlHadGE= - -please-upgrade-node@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz#aeddd3f994c933e4ad98b99d9a556efa0e2fe942" - integrity sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg== - dependencies: - semver-compare "^1.0.0" - pluralize@^8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-8.0.0.tgz#1a6fa16a38d12a1901e0320fa017051c539ce3b1" @@ -18673,19 +18616,6 @@ prebuild-install@5.3.3: tunnel-agent "^0.6.0" which-pm-runs "^1.0.0" -precise-commits@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/precise-commits/-/precise-commits-1.0.2.tgz#4659be01a9c3310f50ce51ddf913fead1d7cc940" - integrity sha512-PYkoNTFXVvZRzJTDxdgzmPanhSNGj5Wtj2NgSo7IhwNXGcKktX+L4DJhyIrhFSLsWWAvd+cYyyU2eXlaX5QxzA== - dependencies: - diff-match-patch "^1.0.0" - execa "^0.9.0" - find-up "^2.1.0" - glob "^7.1.2" - ignore "^3.3.7" - mri "^1.1.0" - ora "^1.3.0" - precond@0.2: version "0.2.3" resolved "https://registry.yarnpkg.com/precond/-/precond-0.2.3.tgz#aa9591bcaa24923f1e0f4849d240f47efc1075ac" @@ -18897,7 +18827,7 @@ protons@^1.0.1: signed-varint "^2.0.1" varint "^5.0.0" -proxy-addr@~2.0.5: +proxy-addr@^2.0.5, proxy-addr@~2.0.5: version "2.0.6" resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.6.tgz#fdc2336505447d3f2f2c638ed272caf614bbb2bf" integrity sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw== @@ -18910,7 +18840,7 @@ prr@~1.0.1: resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476" integrity sha1-0/wRS6BplaRexok/SEzrHXj19HY= -pseudomap@^1.0.1, pseudomap@^1.0.2: +pseudomap@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" integrity sha1-8FKijacOYYkX7wqKw0wa5aaChrM= @@ -19114,6 +19044,16 @@ querystringify@^2.1.1: resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6" integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ== +queue-microtask@^1.1.2: + version "1.2.0" + resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.0.tgz#f27d002cbfac741072afa0e9af3a119b0e8724a3" + integrity sha512-J95OVUiS4b8qqmpqhCodN8yPpHG2mpZUPQ8tDGyIY0VhM+kBHszOuvsMJVGNQ1OH2BnTFbqz45i+2jGpDw9H0w== + +quick-format-unescaped@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/quick-format-unescaped/-/quick-format-unescaped-4.0.1.tgz#437a5ea1a0b61deb7605f8ab6a8fd3858dbeb701" + integrity sha512-RyYpQ6Q5/drsJyOhrWHYMWTedvjTIat+FTwv0K4yoUxzvekw2aRHMQJLlnvt8UantkZg2++bEzD9EdxXqkWf4A== + quick-lru@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-1.1.0.tgz#4360b17c61136ad38078397ff11416e186dcfbb8" @@ -19967,6 +19907,11 @@ ret@~0.1.10: resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== +ret@~0.2.0: + version "0.2.2" + resolved "https://registry.yarnpkg.com/ret/-/ret-0.2.2.tgz#b6861782a1f4762dce43402a71eb7a283f44573c" + integrity sha512-M0b3YWQs7R3Z917WRQy1HHA7Ba7D8hvZg6UE5mLykJxQVE2ju0IXbGlaHPPlkY+WN7wFP+wUMXmBFA0aV6vYGQ== + retry@0.12.0, retry@^0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" @@ -19995,6 +19940,11 @@ rework@1.0.1: convert-source-map "^0.3.3" css "^2.0.0" +rfdc@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.1.4.tgz#ba72cc1367a0ccd9cf81a870b3b58bd3ad07f8c2" + integrity sha512-5C9HXdzK8EAqN7JDif30jqsBzavB7wLpaubisuQIGHWf2gUXSpzy6ArX/+Da8RjFpagWsCn+pIgxTMAmKw9Zug== + rgb-regex@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/rgb-regex/-/rgb-regex-1.0.1.tgz#c0e0d6882df0e23be254a475e8edd41915feaeb1" @@ -20117,6 +20067,13 @@ safe-json-utils@1.0.0: resolved "https://registry.yarnpkg.com/safe-json-utils/-/safe-json-utils-1.0.0.tgz#8b1d68b13cff2ac6a5b68e6c9651cf7f8bb56d9b" integrity sha512-n0hJm6BgX8wk3G+AS8MOQnfcA8dfE6ZMUfwkHUNx69YxPlU3HDaZTHXWto35Z+C4mOjK1odlT95WutkGC+0Idw== +safe-regex2@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/safe-regex2/-/safe-regex2-2.0.0.tgz#b287524c397c7a2994470367e0185e1916b1f5b9" + integrity sha512-PaUSFsUaNNuKwkBijoAPHAK6/eM6VirvyPWlZ7BAQy4D+hCvh4B6lIG+nPdhbFfIbP+gTGBcrdsOaUs0F+ZBOQ== + dependencies: + ret "~0.2.0" + safe-regex@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e" @@ -20282,6 +20239,11 @@ secp256k1@^4.0.1: node-addon-api "^2.0.0" node-gyp-build "^4.2.0" +secure-json-parse@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/secure-json-parse/-/secure-json-parse-2.1.0.tgz#ae76f5624256b5c497af887090a5d9e156c9fb20" + integrity sha512-GckO+MS/wT4UogDyoI/H/S1L0MCcKS1XX/vp48wfmU7Nw4woBmb8mIpu4zPBQjKlRT88/bt9xdoV4111jPpNJA== + seedrandom@3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/seedrandom/-/seedrandom-3.0.1.tgz#eb3dde015bcf55df05a233514e5df44ef9dce083" @@ -20311,26 +20273,16 @@ semaphore@>=1.0.1, semaphore@^1.0.3, semaphore@^1.1.0: resolved "https://registry.yarnpkg.com/semaphore/-/semaphore-1.1.0.tgz#aaad8b86b20fe8e9b32b16dc2ee682a8cd26a8aa" integrity sha512-O4OZEaNtkMd/K0i6js9SL+gqy0ZCBMgUvlSqHKi4IBdjhe7wB8pwztUk1BbZ1fmrvpwFrPbHzqd2w5pTcJH6LA== -semver-compare@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc" - integrity sha1-De4hahyUGrN+nvsXiPavxf9VN/w= - -semver-regex@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/semver-regex/-/semver-regex-2.0.0.tgz#a93c2c5844539a770233379107b38c7b4ac9d338" - integrity sha512-mUdIBBvdn0PLOeP3TEkMH7HHeUP3GjsXCwKarjv/kGmUFOYg1VqEemKhoQpWMu6X2I8kHeuVdGibLGkVK+/5Qw== +semver-store@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/semver-store/-/semver-store-0.3.0.tgz#ce602ff07df37080ec9f4fb40b29576547befbe9" + integrity sha512-TcZvGMMy9vodEFSse30lWinkj+JgOBvPn8wRItpQRSayhc+4ssDs335uklkfvQQJgL/WvmHLVj4Ycv2s7QCQMg== "semver@2 || 3 || 4 || 5", "semver@2.x || 3.x || 4 || 5", semver@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0, semver@^5.7.0, semver@^5.7.1: version "5.7.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== -semver@5.6.0: - version "5.6.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.6.0.tgz#7e74256fbaa49c75aa7c7a205cc22799cac80004" - integrity sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg== - semver@6.2.0: version "6.2.0" resolved "https://registry.yarnpkg.com/semver/-/semver-6.2.0.tgz#4d813d9590aaf8a9192693d6c85b9344de5901db" @@ -20426,6 +20378,11 @@ set-blocking@^2.0.0, set-blocking@~2.0.0: resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= +set-cookie-parser@^2.4.1: + version "2.4.6" + resolved "https://registry.yarnpkg.com/set-cookie-parser/-/set-cookie-parser-2.4.6.tgz#43bdea028b9e6f176474ee5298e758b4a44799c3" + integrity sha512-mNCnTUF0OYPwYzSHbdRdCfNNHqrne+HS5tS5xNb6yJbdP9wInV0q5xPLE0EyfV/Q3tImo3y/OXpD8Jn0Jtnjrg== + set-immediate-shim@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz#4b2b1b27eb808a9f8dcc481a58e5e56f599f3f61" @@ -20525,15 +20482,6 @@ shell-quote@1.7.2: resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.7.2.tgz#67a7d02c76c9da24f99d20808fcaded0e0e04be2" integrity sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg== -shelljs@0.7.8: - version "0.7.8" - resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.7.8.tgz#decbcf874b0d1e5fb72e14b164a9683048e9acb3" - integrity sha1-3svPh0sNHl+3LhSxZKloMEjprLM= - dependencies: - glob "^7.0.0" - interpret "^1.0.0" - rechoir "^0.6.2" - shelljs@^0.8.3: version "0.8.4" resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.4.tgz#de7684feeb767f8716b326078a8a00875890e3c2" @@ -20781,6 +20729,14 @@ solidity-coverage@^0.7.10: shelljs "^0.8.3" web3 "^1.3.0" +sonic-boom@^1.0.2: + version "1.3.0" + resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-1.3.0.tgz#5c77c846ce6c395dddf2eb8e8e65f9cc576f2e76" + integrity sha512-4nX6OYvOYr6R76xfQKi6cZpTO3YSWe/vd+QdIfoH0lBy0MnPkeAbb2rRWgmgADkXUeCKPwO1FZAKlAVWAadELw== + dependencies: + atomic-sleep "^1.0.0" + flatstr "^1.0.12" + sort-keys-length@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/sort-keys-length/-/sort-keys-length-1.0.1.tgz#9cb6f4f4e9e48155a6aa0671edd336ff1479a188" @@ -20948,7 +20904,7 @@ split2@^3.1.0: dependencies: readable-stream "^3.0.0" -split@1.0.1, split@^1.0.0: +split@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/split/-/split-1.0.1.tgz#605bd9be303aa59fb35f9229fbea0ddec9ea07d9" integrity sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg== @@ -21073,40 +21029,6 @@ store@2.0.12: resolved "https://registry.yarnpkg.com/store/-/store-2.0.12.tgz#8c534e2a0b831f72b75fc5f1119857c44ef5d593" integrity sha1-jFNOKguDH3K3X8XxEZhXxE711ZM= -storyboard-core@^3.1.1: - version "3.2.0" - resolved "https://registry.yarnpkg.com/storyboard-core/-/storyboard-core-3.2.0.tgz#6765e2c71bdc5b8ed1db52faf896df43461bca7c" - integrity sha512-v2p3SDYi22go1oLrJPnsiwXgSsjqKL6oryy9B3rz2UEpLwqWArjUoaECciD+/sfwJFJs1etIjUbE4tBx2G+adQ== - dependencies: - chalk "1.x" - lodash "^4.17.10" - platform "1.3.3" - semver "^5.3.0" - timm "^1.6.1" - uuid "^3.0.1" - -storyboard-listener-console-parallel@3.1.4: - version "3.1.4" - resolved "https://registry.yarnpkg.com/storyboard-listener-console-parallel/-/storyboard-listener-console-parallel-3.1.4.tgz#110d4582f4ab924c2ca5c56eb6a3c61fe4d162b3" - integrity sha512-VjgpN02DISKSNWNB9eSF3zhWA5nRINQITan/g/CuS45S7YqISKCP3+ztOLLDmfYvrmg6PrjahwLMq0GvQ37ZDA== - dependencies: - terminal-kit "^0.26.1" - timm "^1.6.1" - -storyboard-listener-console@3.1.4: - version "3.1.4" - resolved "https://registry.yarnpkg.com/storyboard-listener-console/-/storyboard-listener-console-3.1.4.tgz#165c23629721a0327b181d4b926bc2d9787eb661" - integrity sha512-zP2x0XKXHXGYrz4/RS4kxMp/4JqEvREV3rdkfbb6uqGld9gPKNqAdRDLQPN7cJRycTipTt+qew7RFGDTIVSIVA== - dependencies: - timm "^1.6.1" - -storyboard@3.1.4: - version "3.1.4" - resolved "https://registry.yarnpkg.com/storyboard/-/storyboard-3.1.4.tgz#f39a908f168541ffae2af6ed8a06f27e0eed12e2" - integrity sha512-YkWhyz6IxvL/+kN9iMtcw3XVppqY/J8wBRDMpKxnkMniD/QKsKnHPMsxloob2B2+ONwASQPbzZkNXeyY2/kXwg== - dependencies: - storyboard-core "^3.1.1" - stream-browserify@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.2.tgz#87521d38a44aa7ee91ce1cd2a47df0cb49dd660b" @@ -21162,14 +21084,6 @@ strict-uri-encode@^2.0.0: resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz#b9c7330c7042862f6b142dc274bbcc5866ce3546" integrity sha1-ucczDHBChi9rFC3CdLvMWGbONUY= -string-kit@^0.5.12: - version "0.5.27" - resolved "https://registry.yarnpkg.com/string-kit/-/string-kit-0.5.27.tgz#5bd58b7172d7efd7a2981a398967b8dbc78fabe1" - integrity sha512-folwNms0Xq4SCUmsRZfnj1uQsD1lrH/fTXdGCYgdlDxMEWMfMfvt8A3Fc60/Zwvxj74nVBBJzxc2NaW5KaeWAA== - dependencies: - tree-kit "^0.5.24" - xregexp "^3.2.0" - string-length@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/string-length/-/string-length-2.0.0.tgz#d40dbb686a3ace960c1cffca562bf2c45f8363ed" @@ -21194,6 +21108,11 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" +string-similarity@^4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/string-similarity/-/string-similarity-4.0.3.tgz#ef52d6fc59c8a0fc93b6307fbbc08cc6e18cde21" + integrity sha512-QEwJzNFCqq+5AGImk5z4vbsEPTN/+gtyKfXBVLBcbPBRPNganZGfQnIuf9yJ+GiwSnD65sT8xrw/uwU1Q1WmfQ== + string-width@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" @@ -21696,16 +21615,6 @@ temp-write@^3.4.0: temp-dir "^1.0.0" uuid "^3.0.1" -terminal-kit@^0.26.1: - version "0.26.2" - resolved "https://registry.yarnpkg.com/terminal-kit/-/terminal-kit-0.26.2.tgz#545e61585e90c284782a5bb0d17f6f1be9b8f1ad" - integrity sha1-VF5hWF6QwoR4Kluw0X9vG+m48a0= - dependencies: - async-kit "^2.2.3" - nextgen-events "^0.9.8" - string-kit "^0.5.12" - tree-kit "^0.5.26" - terminal-link@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/terminal-link/-/terminal-link-2.1.1.tgz#14a64a27ab3c0df933ea546fba55f2d078edc994" @@ -21857,11 +21766,6 @@ timers-browserify@^2.0.4: dependencies: setimmediate "^1.0.4" -timm@^1.6.1, timm@^1.6.2: - version "1.7.1" - resolved "https://registry.yarnpkg.com/timm/-/timm-1.7.1.tgz#96bab60c7d45b5a10a8a4d0f0117c6b7e5aff76f" - integrity sha512-IjZc9KIotudix8bMaBW6QvMuq64BrJWFs1+4V0lXwWGQZwH+LnX87doAYhem4caOEusRP9/g6jVDQmZ8XOk1nw== - timsort@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4" @@ -21872,6 +21776,11 @@ tiny-invariant@^1.0.2, tiny-invariant@^1.0.6: resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.1.0.tgz#634c5f8efdc27714b7f386c35e6760991d230875" integrity sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw== +tiny-lru@^7.0.0: + version "7.0.6" + resolved "https://registry.yarnpkg.com/tiny-lru/-/tiny-lru-7.0.6.tgz#b0c3cdede1e5882aa2d1ae21cb2ceccf2a331f24" + integrity sha512-zNYO0Kvgn5rXzWpL0y3RS09sMK67eGaQj9805jlK9G6pSadfriTczzLHFXa/xcW4mIRfmlB9HyQ/+SgL0V1uow== + tiny-warning@^1.0.0, tiny-warning@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754" @@ -22001,11 +21910,6 @@ tree-kill@^1.2.2: resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc" integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A== -tree-kit@^0.5.24, tree-kit@^0.5.26, tree-kit@^0.5.27: - version "0.5.27" - resolved "https://registry.yarnpkg.com/tree-kit/-/tree-kit-0.5.27.tgz#d055a7ae6a087dda918cd92ac8c8c2abf5cfaea3" - integrity sha512-0AtAzYDYaKSzeEPK3SI72lg/io5jrBxnT1gIRxEQasJycpQf5iXGh6YAl1kkh9wHmLlNRhDx0oj+GZEQHVe+cw== - trim-newlines@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613" @@ -22080,7 +21984,7 @@ ts-invariant@^0.4.0, ts-invariant@^0.4.4: dependencies: tslib "^1.9.3" -ts-jest@^26.1.0, ts-jest@^26.4.2: +ts-jest@^26.4.2: version "26.4.2" resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-26.4.2.tgz#00b6c970bee202ceef7c6e6e9805c4837b22dab8" integrity sha512-0+MynTTzzbuy5rGjzsCKjxHJk5gY906c/FSaqQ3081+G7dp2Yygfa9hVlbrtNNcztffh1mC6Rs9jb/yHpcjsoQ== @@ -24784,11 +24688,6 @@ xmlhttprequest@*, xmlhttprequest@1.8.0: resolved "https://registry.yarnpkg.com/xmlhttprequest/-/xmlhttprequest-1.8.0.tgz#67fe075c5c24fef39f9d65f5f7b7fe75171968fc" integrity sha1-Z/4HXFwk/vOfnWX197f+dRcZaPw= -xregexp@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/xregexp/-/xregexp-3.2.0.tgz#cb3601987bfe2695b584000c18f1c4a8c322878e" - integrity sha1-yzYBmHv+JpW1hAAMGPHEqMMih44= - xregexp@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/xregexp/-/xregexp-4.3.0.tgz#7e92e73d9174a99a59743f67a4ce879a04b5ae50" @@ -24831,11 +24730,6 @@ yaeti@^0.0.6: resolved "https://registry.yarnpkg.com/yaeti/-/yaeti-0.0.6.tgz#f26f484d72684cf42bedfb76970aa1608fbf9577" integrity sha1-8m9ITXJoTPQr7ft2lwqhYI+/lXc= -yallist@^2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" - integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI= - yallist@^3.0.0, yallist@^3.0.2, yallist@^3.0.3: version "3.1.1" resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" @@ -24846,7 +24740,7 @@ yallist@^4.0.0: resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== -yaml@^1.10.0, yaml@^1.5.1, yaml@^1.7.2: +yaml@^1.5.1, yaml@^1.7.2: version "1.10.0" resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.0.tgz#3b593add944876077d4d683fee01081bd9fff31e" integrity sha512-yr2icI4glYaNG+KWONODapy2/jDdMSDnrONSjblABjD9B4Z5LgiircSt8m8sRZFNi08kG9Sm0uSHtEmP3zaEGg== From 84d98e466e83ee44eb52c68d21a9c214ced61b30 Mon Sep 17 00:00:00 2001 From: nivida Date: Mon, 16 Nov 2020 11:47:49 +0100 Subject: [PATCH 002/107] Whitelist POC etc. implemented --- packages/govern-tx/lib/AbstractAction.ts | 43 +++++++++++ .../lib/transactions/AbstractTransaction.ts | 44 +++-------- .../lib/whitelist/AbstractWhitelistAction.ts | 21 ++++++ packages/govern-tx/src/auth/Authenticator.ts | 60 +++++++-------- .../govern-tx/src/config/Configuration.ts | 4 + packages/govern-tx/src/db/Database.ts | 26 +++++++ packages/govern-tx/src/db/Whitelist.ts | 74 +++++++++++++++++++ packages/govern-tx/src/index.ts | 36 ++++++++- .../src/transactions/ChallengeTransaction.ts | 2 +- .../src/transactions/ExecuteTransaction.ts | 5 ++ .../govern-tx/src/whitelist/AddItemAction.ts | 17 +++++ .../src/whitelist/DeleteItemAction.ts | 16 ++++ .../govern-tx/src/whitelist/GetListAction.ts | 17 +++++ 13 files changed, 295 insertions(+), 70 deletions(-) create mode 100644 packages/govern-tx/lib/AbstractAction.ts create mode 100644 packages/govern-tx/lib/whitelist/AbstractWhitelistAction.ts create mode 100644 packages/govern-tx/src/config/Configuration.ts create mode 100644 packages/govern-tx/src/db/Database.ts create mode 100644 packages/govern-tx/src/db/Whitelist.ts create mode 100644 packages/govern-tx/src/whitelist/AddItemAction.ts create mode 100644 packages/govern-tx/src/whitelist/DeleteItemAction.ts create mode 100644 packages/govern-tx/src/whitelist/GetListAction.ts diff --git a/packages/govern-tx/lib/AbstractAction.ts b/packages/govern-tx/lib/AbstractAction.ts new file mode 100644 index 000000000..117823586 --- /dev/null +++ b/packages/govern-tx/lib/AbstractAction.ts @@ -0,0 +1,43 @@ +export default abstract class AbstractAction { + /** + * The parameters used to create the transaction + * + * @var {Object} parameters + */ + private parameters: any; + + /** + * @param {Array} parameters + * + * @constructor + */ + constructor(parameters: any[]) { + this.parameters = this.validateParameters(parameters); + } + + /** + * Validates the given parameters. + * + * @method validateParameters + * + * @param {Array} parameters + * + * @returns {Array} + * + * @protected + */ + protected validateParameters(parameters: any[]): any[] { + return parameters; + } + + /** + * Executes the actual action + * + * @method execute + * + * @returns {Promise} + * + * @public + */ + public abstract execute(): Promise +} diff --git a/packages/govern-tx/lib/transactions/AbstractTransaction.ts b/packages/govern-tx/lib/transactions/AbstractTransaction.ts index 1234b09bb..9949e2a08 100644 --- a/packages/govern-tx/lib/transactions/AbstractTransaction.ts +++ b/packages/govern-tx/lib/transactions/AbstractTransaction.ts @@ -1,44 +1,26 @@ +import AbstractAction from '../AbstractAction' +import Configuration from '../../src/config/Configuration' -export default abstract class AbstractTransaction { +export interface TransactionReceipt {} + +export default abstract class AbstractTransaction extends AbstractAction { /** * The function signature used to create a transaction * - * @var signature + * @var {string} signature * * @protected */ protected signature: string; /** - * The parameters used to create the transaction - * - * @var {Array parameters} - */ - private parameters: any[]; - - /** - * @param {Configuration} configuration - * @param {Array} parameters + * @param {Object} parameters - The given parameters by the user + * @param {Configuration} configuration - The configuration object to execute the transaction * * @constructor */ - constructor(private configuration: Configuration, parameters: any[]) { - this.parameters = this.validateParameters(parameters); - } - - /** - * Validates the given parameters. - * - * @method validateParameters - * - * @param {Array} parameters - * - * @returns {Array} - * - * @protected - */ - protected validateParameters(parameters: any[]): any[] { - return parameters; + constructor(parameters: any, private configuration: Configuration) { + super(parameters); } /** @@ -50,7 +32,5 @@ export default abstract class AbstractTransaction { * * @public */ - public execute(): Promise { - // TODO - } -} \ No newline at end of file + public abstract execute(): Promise +} diff --git a/packages/govern-tx/lib/whitelist/AbstractWhitelistAction.ts b/packages/govern-tx/lib/whitelist/AbstractWhitelistAction.ts new file mode 100644 index 000000000..dff6f2b3f --- /dev/null +++ b/packages/govern-tx/lib/whitelist/AbstractWhitelistAction.ts @@ -0,0 +1,21 @@ +import Whitelist from '../../src/db/Whitelist' + +export default abstract class AbstractWhitelistAction { + /** + * @param {Whitelist} whitelist - The whitelist DB adapter + * + * @constructor + */ + constructor(private whitelist: Whitelist) {} + + /** + * Executes the actual action + * + * @method execute + * + * @returns {Promise} + * + * @public + */ + public abstract execute(): Promise +} \ No newline at end of file diff --git a/packages/govern-tx/src/auth/Authenticator.ts b/packages/govern-tx/src/auth/Authenticator.ts index 55e1c64b8..e50c66d31 100644 --- a/packages/govern-tx/src/auth/Authenticator.ts +++ b/packages/govern-tx/src/auth/Authenticator.ts @@ -1,5 +1,13 @@ +import Whitelist, {ListItem} from '../db/Whitelist' +// TODO: Implement session key to only authenticate once export default class Authenticator { + /** + * + * @param {Whitelist} whitelist + */ + constructor(private whitelist: Whitelist) { } + /** * Checks if the given public key is existing in the whitelist and if no rate limit is exceeded * @@ -9,56 +17,42 @@ export default class Authenticator { * * @public */ - public authenticate(signedMessage: string): Promise { - return Promise.resolve(true); - } + public async authenticate(signedMessage: string): Promise { + const item = await this.getItem(signedMessage); - /** - * TODO: If a DB is in usage can we remove this method - * - * Checks if the rate limit of the given account is execeeded or if the globally set is - * - * @method isRateLimitExceeded - * - * @param {string} publicKey - Public key of the user - * - * @returns {boolean} - * - * @private - */ - private isRateLimitExceeded(publicKey: string): boolean { - return false; + if (item && item.rateLimit < item.executedTransactions) { + return true + } + + return false } /** - * TODO: If a DB is in usage can we remove this method - * - * Checks if the given public key is existing in the whitelist + * Extracts the public key from the given signed message * - * @method isKnown + * @method getPublicKey * - * @param {string} publicKey - Public key of the user + * @param signedMessage - The signed message from the user * * @returns {boolean} - * + * * @private */ - private isKnown(publicKey: string): boolean { - return true; // TODO: implement whitelist check + private getPublicKey(signedMessage: string): string { + return '0x0...'; } /** - * Extracts the public key from the given signed message * - * @method getPublicKey + * @method getItem * - * @param signedMessage - The signed message from the user + * @param {string} signedMessage * - * @returns {boolean} + * @returns {ListItem} * * @private */ - private getPublicKey(signedMessage: string): string { - return '0x0...'; + private getItem(signedMessage: string): ListItem { + return this.whitelist.getItemByKey(this.getPublicKey(signedMessage)); } -} \ No newline at end of file +} diff --git a/packages/govern-tx/src/config/Configuration.ts b/packages/govern-tx/src/config/Configuration.ts new file mode 100644 index 000000000..706671555 --- /dev/null +++ b/packages/govern-tx/src/config/Configuration.ts @@ -0,0 +1,4 @@ + +export default class Configuration { + // TODO: Implement validation etc. if necessary +} \ No newline at end of file diff --git a/packages/govern-tx/src/db/Database.ts b/packages/govern-tx/src/db/Database.ts new file mode 100644 index 000000000..106da104b --- /dev/null +++ b/packages/govern-tx/src/db/Database.ts @@ -0,0 +1,26 @@ +import Configuration from '../config/Configuration' + +export default class Database { + /** + * @param configuration - The configuration object of this service + * + * @constructor + */ + constructor(private configuration: Configuration) { } + + /** + * Executes a query on the DB + * + * @method query + * + * @param {string} query - The SQL statement + * + * @returns {Promise} + * + * @public + */ + public query(query: string): Promise { + //TODO: Decide which DB to use (Should we share the DB with TheGraph?) and implement the DB adapter with the related driver + return Promise.resolve(true) + } +} \ No newline at end of file diff --git a/packages/govern-tx/src/db/Whitelist.ts b/packages/govern-tx/src/db/Whitelist.ts new file mode 100644 index 000000000..1dbac6f69 --- /dev/null +++ b/packages/govern-tx/src/db/Whitelist.ts @@ -0,0 +1,74 @@ +import Database from './Database' + +// TODO: Define DB schema to handle global rate limits and to have Admin public key + +export interface ListItem { + publicKey: string, + rateLimit: number, + executedTransactions: number +} + +export default class Whitelist { + /** + * @param {Database} db - The Database adapter + * + * @constructor + */ + constructor(private db: Database) {} + + /** + * Returns the whitelist + * + * @method getList + * + * @returns {Promise} + */ + public getList(): Promise { + return this.db.query('SELECT * from whitelist') + } + + /** + * Returns a item from the whitelist by his public key + * + * @method getItemByKey + * + * @param {string} publicKey - The public key to look for + * + * @returns {Promise} + * + * @public + */ + public getItemByKey(publicKey: string): Promise { + return this.db.query(`SELECT * from whitelist WHERE PublicKey='${publicKey}'`) + } + + /** + * Adds a new item to the whitelist + * + * @method addItem + * + * @param {string} publicKey - The public key we would like to add + * + * @returns {Promise} + * + * @public + */ + public addItem(publicKey: string, rateLimit: string): Promise { + return this.db.query(`INSERT INTO whitelist VALUES (${publicKey}, ${rateLimit})`); + } + + /** + * Removes a item from the whitelist + * + * @method deleteItem + * + * @param publicKey - The public key to delete + * + * @returns {Promise} + * + * @public + */ + public deleteItem(publicKey: string): Promise { + return this.db.query(`DELETE FROM whitelist WHERE PublicKey='${publicKey}'`); + } +} diff --git a/packages/govern-tx/src/index.ts b/packages/govern-tx/src/index.ts index 835b79b43..30314a59b 100644 --- a/packages/govern-tx/src/index.ts +++ b/packages/govern-tx/src/index.ts @@ -1,30 +1,58 @@ +// TODO: define request schema (eg. request validation, auth pre handler etc.) + import fastify from 'fastify' +import Configuration from './config/Configuration' + import ExecuteTransaction from './transactions/ExecuteTransaction' import ChallengeTransaction from './transactions/ChallengeTransaction' import ScheduleTransaction from './transactions/ScheduleTransaction' +import AddItemAction from './whitelist/AddItemAction' +import DeleteItemAction from './whitelist/DeleteItemAction' +import GetListAction from './whitelist/GetListAction' + const server = fastify() const config = new Configuration() -// TODO: define request schema (eg. request validation, auth pre handler etc.) + +/* -------------------- * + * Transactions * + * -------------------- */ // Calls GovernQueue.execute -server.post('/execute', {}, async (request, reply) => { +server.post('/execute', {}, async (request, reply): Promise => { return await new ExecuteTransaction(config, request.params).execute() }) // Calls GovernQueue.schedule -server.post('/schedule', {}, async () => { +server.post('/schedule', {}, async (request, reply): Promise => { return await new ScheduleTransaction(config, request.params).execute() }) // Calls GovernQueue.challenge -server.post('/challenge', {}, async () => { +server.post('/challenge', {}, async (request, reply): Promise => { return await new ChallengeTransaction(config, request.params).execute() }) +/* -------------------- * + * Whitelist * + * -------------------- */ + +server.post('/whitelist', {}, async (request, reply): Promise => { + return await new AddItemAction(config, request.params).execute() +}) + +server.delete('/whitelist', {}, async (request, reply): Promise => { + return await new DeleteItemAction(config, request.params).execite() +}) + +server.get('/whitelist', {}, async (request, reply): Promise => { + return await new GetListAction(config, request.params).execute() +}) + + server.listen(4040, (error, address) => { if (error) { console.error(error) diff --git a/packages/govern-tx/src/transactions/ChallengeTransaction.ts b/packages/govern-tx/src/transactions/ChallengeTransaction.ts index ee4541616..5156a890e 100644 --- a/packages/govern-tx/src/transactions/ChallengeTransaction.ts +++ b/packages/govern-tx/src/transactions/ChallengeTransaction.ts @@ -2,4 +2,4 @@ import AbstractTransaction from '../../lib/transactions/AbstractTransaction'; export default class ChallengeTransaction extends AbstractTransaction { protected signature: string = 'challenge(...)'; -} \ No newline at end of file +} diff --git a/packages/govern-tx/src/transactions/ExecuteTransaction.ts b/packages/govern-tx/src/transactions/ExecuteTransaction.ts index e69de29bb..80bf80ad3 100644 --- a/packages/govern-tx/src/transactions/ExecuteTransaction.ts +++ b/packages/govern-tx/src/transactions/ExecuteTransaction.ts @@ -0,0 +1,5 @@ +import AbstractTransaction from '../../lib/transactions/AbstractTransaction'; + +export default class ExecuteTransaction extends AbstractTransaction { + protected signature: string = 'execute(...)'; +} diff --git a/packages/govern-tx/src/whitelist/AddItemAction.ts b/packages/govern-tx/src/whitelist/AddItemAction.ts new file mode 100644 index 000000000..903983d07 --- /dev/null +++ b/packages/govern-tx/src/whitelist/AddItemAction.ts @@ -0,0 +1,17 @@ +import AbstractWhitelistAction from "../../lib/whitelist/AbstractWhitelistAction"; +import AbstractWhitelistAction from "../../lib/whitelist/AbstractWhitelistAction"; + +export default class AddItemAction extends AbstractWhitelistAction { + /** + * Adds a new item to the whitelist + * + * @method execute + * + * @returns {Promise} + * + * @public + */ + public execute(): Promise { + return Promise.resolve(true) + } +} diff --git a/packages/govern-tx/src/whitelist/DeleteItemAction.ts b/packages/govern-tx/src/whitelist/DeleteItemAction.ts new file mode 100644 index 000000000..39fdadecd --- /dev/null +++ b/packages/govern-tx/src/whitelist/DeleteItemAction.ts @@ -0,0 +1,16 @@ +import AbstractWhitelistAction from "../../lib/whitelist/AbstractWhitelistAction"; + +export default class DeleteItemAction extends AbstractWhitelistAction { + /** + * Adds a new item to the whitelist + * + * @method execute + * + * @returns {Promise} + * + * @public + */ + public execute(): Promise { + return Promise.resolve(true) + } +} diff --git a/packages/govern-tx/src/whitelist/GetListAction.ts b/packages/govern-tx/src/whitelist/GetListAction.ts new file mode 100644 index 000000000..71144545b --- /dev/null +++ b/packages/govern-tx/src/whitelist/GetListAction.ts @@ -0,0 +1,17 @@ +import AbstractWhitelistAction from '../../lib/whitelist/AbstractWhitelistAction'; +import {ListItem} from '../db/Whitelist' + +export default class GetListAction extends AbstractWhitelistAction { + /** + * Adds a new item to the whitelist + * + * @method execute + * + * @returns {Promise} + * + * @public + */ + public execute(): Promise { + return Promise.resolve([{publicKey: '0x0', rateLimit: 1}]) + } +} From 591be32b017aa10203730e03a9b064f11ba8027d Mon Sep 17 00:00:00 2001 From: nivida Date: Mon, 16 Nov 2020 11:54:19 +0100 Subject: [PATCH 003/107] code-style improvements --- .../govern-tx/lib/transactions/AbstractTransaction.ts | 1 + .../govern-tx/lib/whitelist/AbstractWhitelistAction.ts | 8 ++++---- packages/govern-tx/src/config/Configuration.ts | 2 +- packages/govern-tx/src/db/Database.ts | 2 +- packages/govern-tx/src/index.ts | 2 +- .../govern-tx/src/transactions/ChallengeTransaction.ts | 4 ++++ packages/govern-tx/src/transactions/ExecuteTransaction.ts | 4 ++++ .../govern-tx/src/transactions/ScheduleTransaction.ts | 4 ++++ 8 files changed, 20 insertions(+), 7 deletions(-) diff --git a/packages/govern-tx/lib/transactions/AbstractTransaction.ts b/packages/govern-tx/lib/transactions/AbstractTransaction.ts index 9949e2a08..13011e6d6 100644 --- a/packages/govern-tx/lib/transactions/AbstractTransaction.ts +++ b/packages/govern-tx/lib/transactions/AbstractTransaction.ts @@ -1,6 +1,7 @@ import AbstractAction from '../AbstractAction' import Configuration from '../../src/config/Configuration' +// TODO: Use type from ethers export interface TransactionReceipt {} export default abstract class AbstractTransaction extends AbstractAction { diff --git a/packages/govern-tx/lib/whitelist/AbstractWhitelistAction.ts b/packages/govern-tx/lib/whitelist/AbstractWhitelistAction.ts index dff6f2b3f..44e015825 100644 --- a/packages/govern-tx/lib/whitelist/AbstractWhitelistAction.ts +++ b/packages/govern-tx/lib/whitelist/AbstractWhitelistAction.ts @@ -1,4 +1,4 @@ -import Whitelist from '../../src/db/Whitelist' +import Whitelist, {ListItem} from '../../src/db/Whitelist' export default abstract class AbstractWhitelistAction { /** @@ -13,9 +13,9 @@ export default abstract class AbstractWhitelistAction { * * @method execute * - * @returns {Promise} + * @returns {Promise} * * @public */ - public abstract execute(): Promise -} \ No newline at end of file + public abstract execute(): Promise +} diff --git a/packages/govern-tx/src/config/Configuration.ts b/packages/govern-tx/src/config/Configuration.ts index 706671555..02fd3a051 100644 --- a/packages/govern-tx/src/config/Configuration.ts +++ b/packages/govern-tx/src/config/Configuration.ts @@ -1,4 +1,4 @@ export default class Configuration { // TODO: Implement validation etc. if necessary -} \ No newline at end of file +} diff --git a/packages/govern-tx/src/db/Database.ts b/packages/govern-tx/src/db/Database.ts index 106da104b..869a55fa4 100644 --- a/packages/govern-tx/src/db/Database.ts +++ b/packages/govern-tx/src/db/Database.ts @@ -23,4 +23,4 @@ export default class Database { //TODO: Decide which DB to use (Should we share the DB with TheGraph?) and implement the DB adapter with the related driver return Promise.resolve(true) } -} \ No newline at end of file +} diff --git a/packages/govern-tx/src/index.ts b/packages/govern-tx/src/index.ts index 30314a59b..56f99eb64 100644 --- a/packages/govern-tx/src/index.ts +++ b/packages/govern-tx/src/index.ts @@ -45,7 +45,7 @@ server.post('/whitelist', {}, async (request, reply): Promise => { }) server.delete('/whitelist', {}, async (request, reply): Promise => { - return await new DeleteItemAction(config, request.params).execite() + return await new DeleteItemAction(config, request.params).execute() }) server.get('/whitelist', {}, async (request, reply): Promise => { diff --git a/packages/govern-tx/src/transactions/ChallengeTransaction.ts b/packages/govern-tx/src/transactions/ChallengeTransaction.ts index 5156a890e..6c70e362f 100644 --- a/packages/govern-tx/src/transactions/ChallengeTransaction.ts +++ b/packages/govern-tx/src/transactions/ChallengeTransaction.ts @@ -2,4 +2,8 @@ import AbstractTransaction from '../../lib/transactions/AbstractTransaction'; export default class ChallengeTransaction extends AbstractTransaction { protected signature: string = 'challenge(...)'; + + public execute(): Promise { + + } } diff --git a/packages/govern-tx/src/transactions/ExecuteTransaction.ts b/packages/govern-tx/src/transactions/ExecuteTransaction.ts index 80bf80ad3..1bae8566a 100644 --- a/packages/govern-tx/src/transactions/ExecuteTransaction.ts +++ b/packages/govern-tx/src/transactions/ExecuteTransaction.ts @@ -2,4 +2,8 @@ import AbstractTransaction from '../../lib/transactions/AbstractTransaction'; export default class ExecuteTransaction extends AbstractTransaction { protected signature: string = 'execute(...)'; + + public execute(): Promise { + + } } diff --git a/packages/govern-tx/src/transactions/ScheduleTransaction.ts b/packages/govern-tx/src/transactions/ScheduleTransaction.ts index 12ba5ca7b..aff10e9aa 100644 --- a/packages/govern-tx/src/transactions/ScheduleTransaction.ts +++ b/packages/govern-tx/src/transactions/ScheduleTransaction.ts @@ -2,4 +2,8 @@ import AbstractTransaction from '../../lib/transactions/AbstractTransaction'; export default class ScheduleTransaction extends AbstractTransaction { protected signature: string = 'schedule(...)'; + + public execute(): Promise { + + } } From 961e756ae07e9ce7dfffbe73582a642ae3b380c5 Mon Sep 17 00:00:00 2001 From: nivida Date: Mon, 16 Nov 2020 13:43:35 +0100 Subject: [PATCH 004/107] code style improvements, index.ts updated for auth, Authenticator updated --- packages/govern-tx/lib/AbstractAction.ts | 10 +-- .../lib/transactions/AbstractTransaction.ts | 2 +- .../lib/whitelist/AbstractWhitelistAction.ts | 8 +- packages/govern-tx/package.json | 3 +- packages/govern-tx/src/auth/Authenticator.ts | 6 +- packages/govern-tx/src/auth/auth-plugin.ts | 0 packages/govern-tx/src/index.ts | 80 ++++++++++--------- yarn.lock | 28 +++++++ 8 files changed, 88 insertions(+), 49 deletions(-) create mode 100644 packages/govern-tx/src/auth/auth-plugin.ts diff --git a/packages/govern-tx/lib/AbstractAction.ts b/packages/govern-tx/lib/AbstractAction.ts index 117823586..c1e1f7c83 100644 --- a/packages/govern-tx/lib/AbstractAction.ts +++ b/packages/govern-tx/lib/AbstractAction.ts @@ -7,11 +7,11 @@ export default abstract class AbstractAction { private parameters: any; /** - * @param {Array} parameters + * @param {Object} parameters * * @constructor */ - constructor(parameters: any[]) { + constructor(parameters: any) { this.parameters = this.validateParameters(parameters); } @@ -20,13 +20,13 @@ export default abstract class AbstractAction { * * @method validateParameters * - * @param {Array} parameters + * @param {Object} parameters * - * @returns {Array} + * @returns {Object} * * @protected */ - protected validateParameters(parameters: any[]): any[] { + protected validateParameters(parameters: any): any { return parameters; } diff --git a/packages/govern-tx/lib/transactions/AbstractTransaction.ts b/packages/govern-tx/lib/transactions/AbstractTransaction.ts index 13011e6d6..e97a21602 100644 --- a/packages/govern-tx/lib/transactions/AbstractTransaction.ts +++ b/packages/govern-tx/lib/transactions/AbstractTransaction.ts @@ -20,7 +20,7 @@ export default abstract class AbstractTransaction extends AbstractAction { * * @constructor */ - constructor(parameters: any, private configuration: Configuration) { + constructor(private configuration: Configuration, parameters: any) { super(parameters); } diff --git a/packages/govern-tx/lib/whitelist/AbstractWhitelistAction.ts b/packages/govern-tx/lib/whitelist/AbstractWhitelistAction.ts index 44e015825..154eda8f5 100644 --- a/packages/govern-tx/lib/whitelist/AbstractWhitelistAction.ts +++ b/packages/govern-tx/lib/whitelist/AbstractWhitelistAction.ts @@ -1,12 +1,16 @@ +import AbstractAction from '../AbstractAction' import Whitelist, {ListItem} from '../../src/db/Whitelist' -export default abstract class AbstractWhitelistAction { +export default abstract class AbstractWhitelistAction extends AbstractAction { /** + * @param {any} parameters - The given parameters by the user * @param {Whitelist} whitelist - The whitelist DB adapter * * @constructor */ - constructor(private whitelist: Whitelist) {} + constructor(private whitelist: Whitelist, parameters: any = {}) { + super(parameters) + } /** * Executes the actual action diff --git a/packages/govern-tx/package.json b/packages/govern-tx/package.json index 92f6a3ce7..e41f134d2 100644 --- a/packages/govern-tx/package.json +++ b/packages/govern-tx/package.json @@ -23,6 +23,7 @@ "typescript": "^4.0.5" }, "dependencies": { - "fastify": "^3.8.0" + "fastify": "^3.8.0", + "fastify-plugin": "^3.0.0" } } diff --git a/packages/govern-tx/src/auth/Authenticator.ts b/packages/govern-tx/src/auth/Authenticator.ts index e50c66d31..57dc338fc 100644 --- a/packages/govern-tx/src/auth/Authenticator.ts +++ b/packages/govern-tx/src/auth/Authenticator.ts @@ -1,6 +1,6 @@ import Whitelist, {ListItem} from '../db/Whitelist' -// TODO: Implement session key to only authenticate once +// TODO: Implement Request object from Fastify export default class Authenticator { /** * @@ -18,9 +18,7 @@ export default class Authenticator { * @public */ public async authenticate(signedMessage: string): Promise { - const item = await this.getItem(signedMessage); - - if (item && item.rateLimit < item.executedTransactions) { + if (await this.getItem(signedMessage)) { return true } diff --git a/packages/govern-tx/src/auth/auth-plugin.ts b/packages/govern-tx/src/auth/auth-plugin.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/govern-tx/src/index.ts b/packages/govern-tx/src/index.ts index 56f99eb64..77147280c 100644 --- a/packages/govern-tx/src/index.ts +++ b/packages/govern-tx/src/index.ts @@ -1,8 +1,11 @@ // TODO: define request schema (eg. request validation, auth pre handler etc.) import fastify from 'fastify' +import pkAuth from './auth/auth-plugin' import Configuration from './config/Configuration' +import Database from './db/Database' +import Whitelist from './db/Whitelist' import ExecuteTransaction from './transactions/ExecuteTransaction' import ChallengeTransaction from './transactions/ChallengeTransaction' @@ -12,47 +15,52 @@ import AddItemAction from './whitelist/AddItemAction' import DeleteItemAction from './whitelist/DeleteItemAction' import GetListAction from './whitelist/GetListAction' -const server = fastify() const config = new Configuration() +const whitelist = new Whitelist(new Database(config)); - -/* -------------------- * - * Transactions * - * -------------------- */ - -// Calls GovernQueue.execute -server.post('/execute', {}, async (request, reply): Promise => { - return await new ExecuteTransaction(config, request.params).execute() -}) - -// Calls GovernQueue.schedule -server.post('/schedule', {}, async (request, reply): Promise => { - return await new ScheduleTransaction(config, request.params).execute() -}) - -// Calls GovernQueue.challenge -server.post('/challenge', {}, async (request, reply): Promise => { - return await new ChallengeTransaction(config, request.params).execute() -}) - - -/* -------------------- * - * Whitelist * - * -------------------- */ - -server.post('/whitelist', {}, async (request, reply): Promise => { - return await new AddItemAction(config, request.params).execute() -}) - -server.delete('/whitelist', {}, async (request, reply): Promise => { - return await new DeleteItemAction(config, request.params).execute() +const server = fastify({ + logger: { + level: 'debug' // Make this configurable with a process ENV + } }) - -server.get('/whitelist', {}, async (request, reply): Promise => { - return await new GetListAction(config, request.params).execute() +server.register(pkAuth) + +server.after(() => { + // Register Auth pre handler hook + server.addHook('preHandler', server.pkAuth) + + /* -------------------- * + * Transactions * + * -------------------- */ + server.post('/execute', {}, async (request, reply): Promise => { + return await new ExecuteTransaction(config, request.params).execute() + }) + + server.post('/schedule', {}, async (request, reply): Promise => { + return await new ScheduleTransaction(config, request.params).execute() + }) + + server.post('/challenge', {}, async (request, reply): Promise => { + return await new ChallengeTransaction(config, request.params).execute() + }) + + + /* -------------------- * + * Whitelist * + * -------------------- */ + server.post('/whitelist', {}, async (request, reply): Promise => { + return await new AddItemAction(whitelist, request.params).execute() + }) + + server.delete('/whitelist', {}, async (request, reply): Promise => { + return await new DeleteItemAction(whitelist, request.params).execute() + }) + + server.get('/whitelist', {}, async (request, reply): Promise => { + return await new GetListAction(whitelist).execute() + }) }) - server.listen(4040, (error, address) => { if (error) { console.error(error) diff --git a/yarn.lock b/yarn.lock index d99d432df..27766e9dc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6634,6 +6634,13 @@ base@^0.11.1: mixin-deep "^1.2.0" pascalcase "^0.1.1" +basic-auth@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/basic-auth/-/basic-auth-2.0.1.tgz#b998279bf47ce38344b4f3cf916d4679bbf51e3a" + integrity sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg== + dependencies: + safe-buffer "5.1.2" + batch@0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16" @@ -11042,11 +11049,32 @@ fast-safe-stringify@^2.0.6, fast-safe-stringify@^2.0.7: resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz#124aa885899261f68aedb42a7c080de9da608743" integrity sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA== +fastify-basic-auth@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/fastify-basic-auth/-/fastify-basic-auth-1.0.1.tgz#c5fcc02ef47f97f8c024c90f1173e82b6a4e8c1f" + integrity sha512-Rpg3d+2cev+cLZSUFnpePP/n1UoWZ8seLA4cfujhPeV1u5uG8HEH4Pk3NitL/sjXG2GMuuyMh3SBBMxPd+hBjw== + dependencies: + basic-auth "^2.0.1" + fastify-plugin "^2.0.0" + http-errors "^1.7.3" + fastify-error@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/fastify-error/-/fastify-error-0.2.0.tgz#9a1c28d4f42b6259e7a549671c8e5e2d85660634" integrity sha512-zabxsBatj59ROG0fhP36zNdc5Q1/eYeH9oSF9uvfrurZf8/JKfrJbMcIGrLpLWcf89rS6L91RHWm20A/X85hcA== +fastify-plugin@^2.0.0: + version "2.3.4" + resolved "https://registry.yarnpkg.com/fastify-plugin/-/fastify-plugin-2.3.4.tgz#b17abdc36a97877d88101fb86ad8a07f2c07de87" + integrity sha512-I+Oaj6p9oiRozbam30sh39BiuiqBda7yK2nmSPVwDCfIBlKnT8YB3MY+pRQc2Fcd07bf6KPGklHJaQ2Qu81TYQ== + dependencies: + semver "^7.3.2" + +fastify-plugin@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/fastify-plugin/-/fastify-plugin-3.0.0.tgz#cf1b8c8098e3b5a7c8c30e6aeb06903370c054ca" + integrity sha512-ZdCvKEEd92DNLps5n0v231Bha8bkz1DjnPP/aEz37rz/q42Z5JVLmgnqR4DYuNn3NXAO3IDCPyRvgvxtJ4Ym4w== + fastify-warning@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/fastify-warning/-/fastify-warning-0.2.0.tgz#e717776026a4493dc9a2befa44db6d17f618008f" From cb9401e7065c80c63c9f8f9a579472d064d82f40 Mon Sep 17 00:00:00 2001 From: nivida Date: Mon, 16 Nov 2020 14:50:31 +0100 Subject: [PATCH 005/107] draft Authenticator implemented --- packages/govern-tx/package.json | 6 +- packages/govern-tx/src/auth/Authenticator.ts | 69 ++++++-- packages/govern-tx/src/auth/auth-plugin.ts | 38 +++++ packages/govern-tx/src/index.ts | 24 +-- yarn.lock | 162 ++++++++++++++++--- 5 files changed, 249 insertions(+), 50 deletions(-) diff --git a/packages/govern-tx/package.json b/packages/govern-tx/package.json index e41f134d2..7c6585d66 100644 --- a/packages/govern-tx/package.json +++ b/packages/govern-tx/package.json @@ -23,7 +23,11 @@ "typescript": "^4.0.5" }, "dependencies": { + "@ethersproject/bytes": "^5.0.5", + "@ethersproject/wallet": "^5.0.7", "fastify": "^3.8.0", - "fastify-plugin": "^3.0.0" + "fastify-jwt": "^2.1.3", + "fastify-plugin": "^3.0.0", + "jsonwebtoken": "^8.5.1" } } diff --git a/packages/govern-tx/src/auth/Authenticator.ts b/packages/govern-tx/src/auth/Authenticator.ts index 57dc338fc..42a35c5bd 100644 --- a/packages/govern-tx/src/auth/Authenticator.ts +++ b/packages/govern-tx/src/auth/Authenticator.ts @@ -1,30 +1,71 @@ import Whitelist, {ListItem} from '../db/Whitelist' +import jwt, {SignOptions, VerifyOptions} from 'jsonwebtoken' +import {verifyMessage} from '@ethersproject/wallet'; +import {arrayify} from '@ethersproject/bytes' -// TODO: Implement Request object from Fastify +export interface JWTOptions { + sign: SignOptions, + verify: VerifyOptions +} + +// TODO: Implement provide possibility to configure JWT export default class Authenticator { /** - * * @param {Whitelist} whitelist + * @param {string} secret + * @param {SignOptions} jqtOptions + * + * @constructor */ - constructor(private whitelist: Whitelist) { } + constructor( + private whitelist: Whitelist, + private secret: string, + private jwtOptions: JWTOptions + ) { } /** * Checks if the given public key is existing in the whitelist and if no rate limit is exceeded * - * @param {string} signedMessage - The signed message from the user + * @method authenticate * - * @returns Promise + * @param {string} message - The message from the user + * @param {string} signature - The sent signature from the user + * + * @returns Promise - Returns false or the JWT * * @public */ - public async authenticate(signedMessage: string): Promise { - if (await this.getItem(signedMessage)) { - return true + public async authenticate(message: string, signature: string): Promise { + const publicKey = this.getPublicKey(message, signature); + + if (await this.getItem(publicKey)) { + return jwt.sign({data: publicKey}, this.secret, this.jwtOptions.sign) } return false } + /** + * Verifiey the given JWT + * + * @method verify + * + * @param {string} token + * + * @returns {boolean} + * + * @public + */ + public verify(token: string): boolean { + try { + jwt.verify(token, this.secret, this.jwtOptions.verify) + + return true; + } catch(error) { + return false; + } + } + /** * Extracts the public key from the given signed message * @@ -36,21 +77,21 @@ export default class Authenticator { * * @private */ - private getPublicKey(signedMessage: string): string { - return '0x0...'; - } + private getPublicKey(message: string, signature: string): string { + return verifyMessage(arrayify(message), signature); /** + * Returns a item from the whitelist with the given key * * @method getItem * - * @param {string} signedMessage + * @param {string} publicKey * * @returns {ListItem} * * @private */ - private getItem(signedMessage: string): ListItem { - return this.whitelist.getItemByKey(this.getPublicKey(signedMessage)); + private getItem(publicKey: string): ListItem { + return this.whitelist.getItemByKey(publicKey); } } diff --git a/packages/govern-tx/src/auth/auth-plugin.ts b/packages/govern-tx/src/auth/auth-plugin.ts index e69de29bb..8c7fd299e 100644 --- a/packages/govern-tx/src/auth/auth-plugin.ts +++ b/packages/govern-tx/src/auth/auth-plugin.ts @@ -0,0 +1,38 @@ +import fp from 'fastify-plugin' +import Authenticator from './Authenticator' +import { Unauthorized } from 'http-errors' + +export interface AuthOptions { + authenticator: Authenticator, + secret: string +} + + +function authPlugin(fastify, options: AuthOptions, next) { + const authenticator = options.authenticator + const secret = options.secret + + fastify.decorate('pkAuth', auth) + + next() + + function auth(request, reply, next) { + if (request.header.autorization && authenticator.verify(request.header.autorization)) { + next() + return + } + + const body = JSON.parse(request.body) + const token = authenticator.authenticate(body.message, body.signature) + if (!token) { + reply() + return + } + + next(new Unauthorized('Unknown account!')) + } +} + + +const plugin = fp(authPlugin, '3.x') +export default plugin diff --git a/packages/govern-tx/src/index.ts b/packages/govern-tx/src/index.ts index 77147280c..b59b22f62 100644 --- a/packages/govern-tx/src/index.ts +++ b/packages/govern-tx/src/index.ts @@ -32,32 +32,32 @@ server.after(() => { /* -------------------- * * Transactions * * -------------------- */ - server.post('/execute', {}, async (request, reply): Promise => { - return await new ExecuteTransaction(config, request.params).execute() + server.post('/execute', {}, (request, reply): Promise => { + return new ExecuteTransaction(config, request.params).execute() }) - server.post('/schedule', {}, async (request, reply): Promise => { - return await new ScheduleTransaction(config, request.params).execute() + server.post('/schedule', {}, (request, reply): Promise => { + return new ScheduleTransaction(config, request.params).execute() }) - server.post('/challenge', {}, async (request, reply): Promise => { - return await new ChallengeTransaction(config, request.params).execute() + server.post('/challenge', {}, (request, reply): Promise => { + return new ChallengeTransaction(config, request.params).execute() }) /* -------------------- * * Whitelist * * -------------------- */ - server.post('/whitelist', {}, async (request, reply): Promise => { - return await new AddItemAction(whitelist, request.params).execute() + server.post('/whitelist', {}, (request, reply): Promise => { + return new AddItemAction(whitelist, request.params).execute() }) - server.delete('/whitelist', {}, async (request, reply): Promise => { - return await new DeleteItemAction(whitelist, request.params).execute() + server.delete('/whitelist', {}, (request, reply): Promise => { + return new DeleteItemAction(whitelist, request.params).execute() }) - server.get('/whitelist', {}, async (request, reply): Promise => { - return await new GetListAction(whitelist).execute() + server.get('/whitelist', {}, (request, reply): Promise => { + return new GetListAction(whitelist).execute() }) }) diff --git a/yarn.lock b/yarn.lock index 27766e9dc..9bfd87fc4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1520,7 +1520,7 @@ "@ethersproject/logger" "^5.0.5" bn.js "^4.4.0" -"@ethersproject/bytes@5.0.5", "@ethersproject/bytes@>=5.0.0-beta.129", "@ethersproject/bytes@^5.0.4": +"@ethersproject/bytes@5.0.5", "@ethersproject/bytes@>=5.0.0-beta.129", "@ethersproject/bytes@^5.0.4", "@ethersproject/bytes@^5.0.5": version "5.0.5" resolved "https://registry.yarnpkg.com/@ethersproject/bytes/-/bytes-5.0.5.tgz#688b70000e550de0c97a151a21f15b87d7f97d7c" integrity sha512-IEj9HpZB+ACS6cZ+QQMTqmu/cnUK2fYNE6ms/PVxjoBjoxc6HCraLpam1KuRvreMy0i523PLmjN8OYeikRdcUQ== @@ -1739,7 +1739,7 @@ "@ethersproject/constants" "^5.0.4" "@ethersproject/logger" "^5.0.5" -"@ethersproject/wallet@5.0.7": +"@ethersproject/wallet@5.0.7", "@ethersproject/wallet@^5.0.7": version "5.0.7" resolved "https://registry.yarnpkg.com/@ethersproject/wallet/-/wallet-5.0.7.tgz#9d4540f97d534e3d61548ace30f15857209b3f02" integrity sha512-n2GX1+2Tc0qV8dguUcLkjNugINKvZY7u/5fEsn0skW9rz5+jHTR5IKMV6jSfXA+WjQT8UCNMvkI3CNcdhaPbTQ== @@ -4076,6 +4076,13 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4= +"@types/jsonwebtoken@^8.3.2": + version "8.5.0" + resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-8.5.0.tgz#2531d5e300803aa63279b232c014acf780c981c5" + integrity sha512-9bVao7LvyorRGZCw0VmH/dr7Og+NdjYSsKAxB43OQoComFbBgsEpoR9JW6+qSq/ogwVBg8GI2MfAlk4SYI4OLg== + dependencies: + "@types/node" "*" + "@types/keygrip@*": version "1.0.2" resolved "https://registry.yarnpkg.com/@types/keygrip/-/keygrip-1.0.2.tgz#513abfd256d7ad0bf1ee1873606317b33b1b2a72" @@ -6634,13 +6641,6 @@ base@^0.11.1: mixin-deep "^1.2.0" pascalcase "^0.1.1" -basic-auth@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/basic-auth/-/basic-auth-2.0.1.tgz#b998279bf47ce38344b4f3cf916d4679bbf51e3a" - integrity sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg== - dependencies: - safe-buffer "5.1.2" - batch@0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16" @@ -7032,6 +7032,11 @@ buffer-crc32@~0.2.3: resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" integrity sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI= +buffer-equal-constant-time@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" + integrity sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk= + buffer-fill@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/buffer-fill/-/buffer-fill-1.0.0.tgz#f8f78b76789888ef39f205cd637f68e702122b2c" @@ -9350,6 +9355,13 @@ eccrypto-js@5.2.0: randombytes "2.1.0" secp256k1 "3.8.0" +ecdsa-sig-formatter@1.0.11: + version "1.0.11" + resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" + integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== + dependencies: + safe-buffer "^5.0.1" + ee-first@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" @@ -11049,20 +11061,29 @@ fast-safe-stringify@^2.0.6, fast-safe-stringify@^2.0.7: resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz#124aa885899261f68aedb42a7c080de9da608743" integrity sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA== -fastify-basic-auth@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/fastify-basic-auth/-/fastify-basic-auth-1.0.1.tgz#c5fcc02ef47f97f8c024c90f1173e82b6a4e8c1f" - integrity sha512-Rpg3d+2cev+cLZSUFnpePP/n1UoWZ8seLA4cfujhPeV1u5uG8HEH4Pk3NitL/sjXG2GMuuyMh3SBBMxPd+hBjw== +fastfall@^1.5.0: + version "1.5.1" + resolved "https://registry.yarnpkg.com/fastfall/-/fastfall-1.5.1.tgz#3fee03331a49d1d39b3cdf7a5e9cd66f475e7b94" + integrity sha1-P+4DMxpJ0dObPN96XpzWb0dee5Q= dependencies: - basic-auth "^2.0.1" - fastify-plugin "^2.0.0" - http-errors "^1.7.3" + reusify "^1.0.0" fastify-error@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/fastify-error/-/fastify-error-0.2.0.tgz#9a1c28d4f42b6259e7a549671c8e5e2d85660634" integrity sha512-zabxsBatj59ROG0fhP36zNdc5Q1/eYeH9oSF9uvfrurZf8/JKfrJbMcIGrLpLWcf89rS6L91RHWm20A/X85hcA== +fastify-jwt@^2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/fastify-jwt/-/fastify-jwt-2.1.3.tgz#d016a2f6a810299edc72ad79ce2949fb88a20604" + integrity sha512-8732zt+7UA9JzeRebJFCH+56laMCAxq/Wyou6pzXZzEWcPGPLcRqCk+R0CcgwjjVToo6wLIeNNKHFegyemyHug== + dependencies: + "@types/jsonwebtoken" "^8.3.2" + fastify-plugin "^2.0.0" + http-errors "^1.7.1" + jsonwebtoken "^8.3.0" + steed "^1.1.3" + fastify-plugin@^2.0.0: version "2.3.4" resolved "https://registry.yarnpkg.com/fastify-plugin/-/fastify-plugin-2.3.4.tgz#b17abdc36a97877d88101fb86ad8a07f2c07de87" @@ -11102,20 +11123,36 @@ fastify@^3.8.0: semver "^7.3.2" tiny-lru "^7.0.0" -fastq@^1.6.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.8.0.tgz#550e1f9f59bbc65fe185cb6a9b4d95357107f481" - integrity sha512-SMIZoZdLh/fgofivvIkmknUXyPnvxRE3DhtZ5Me3Mrsk5gyPL42F0xr51TdRXskBxHfMp+07bcYzfsYEsSQA9Q== +fastparallel@^2.2.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/fastparallel/-/fastparallel-2.4.0.tgz#65fbec1a5e5902494be772cf5765cbaaece08688" + integrity sha512-sacwQ7wwKlQXsa7TN24UvMBLZNLmVcPhmxccC9riFqb3N+fSczJL8eWdnZodZ/KijGVgNBBfvF/NeXER08uXnQ== dependencies: reusify "^1.0.4" + xtend "^4.0.2" -fastq@^1.6.1: +fastq@^1.3.0, fastq@^1.6.1: version "1.9.0" resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.9.0.tgz#e16a72f338eaca48e91b5c23593bcc2ef66b7947" integrity sha512-i7FVWL8HhVY+CTkwFxkN2mk3h+787ixS5S63eb78diVRc1MCssarHq3W5cj0av7YDSwmaV928RNag+U1etRQ7w== dependencies: reusify "^1.0.4" +fastq@^1.6.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.8.0.tgz#550e1f9f59bbc65fe185cb6a9b4d95357107f481" + integrity sha512-SMIZoZdLh/fgofivvIkmknUXyPnvxRE3DhtZ5Me3Mrsk5gyPL42F0xr51TdRXskBxHfMp+07bcYzfsYEsSQA9Q== + dependencies: + reusify "^1.0.4" + +fastseries@^1.7.0: + version "1.7.2" + resolved "https://registry.yarnpkg.com/fastseries/-/fastseries-1.7.2.tgz#d22ce13b9433dff3388d91dbd6b8bda9b21a0f4b" + integrity sha1-0izhO5Qz3/M4jZHb1ri9qbIaD0s= + dependencies: + reusify "^1.0.0" + xtend "^4.0.0" + faye-websocket@^0.10.0: version "0.10.0" resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.10.0.tgz#4e492f8d04dfb6f89003507f6edbf2d501e7c6f4" @@ -12604,7 +12641,7 @@ http-errors@1.7.3, http-errors@~1.7.2: statuses ">= 1.5.0 < 2" toidentifier "1.0.0" -http-errors@^1.7.3: +http-errors@^1.7.1, http-errors@^1.7.3: version "1.8.0" resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.8.0.tgz#75d1bbe497e1044f51e4ee9e704a62f28d336507" integrity sha512-4I8r0C5JDhT5VkvI47QktDW75rNlGVsUf/8hzjCC/wkWI/jdTRmBb9aI7erSG82r1bjKY3F6k28WnsVxB1C73A== @@ -14905,6 +14942,22 @@ jsonschema@^1.2.4: resolved "https://registry.yarnpkg.com/jsonschema/-/jsonschema-1.4.0.tgz#1afa34c4bc22190d8e42271ec17ac8b3404f87b2" integrity sha512-/YgW6pRMr6M7C+4o8kS+B/2myEpHCrxO4PEWnqJNBFMjn7EWXqlQ4tGwL6xTHeRplwuZmcAncdvfOad1nT2yMw== +jsonwebtoken@^8.3.0, jsonwebtoken@^8.5.1: + version "8.5.1" + resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d" + integrity sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w== + dependencies: + jws "^3.2.2" + lodash.includes "^4.3.0" + lodash.isboolean "^3.0.3" + lodash.isinteger "^4.0.4" + lodash.isnumber "^3.0.3" + lodash.isplainobject "^4.0.6" + lodash.isstring "^4.0.1" + lodash.once "^4.0.0" + ms "^2.1.1" + semver "^5.6.0" + jsprim@^1.2.2: version "1.4.1" resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" @@ -14941,6 +14994,23 @@ just-map-keys@^1.1.0: resolved "https://registry.yarnpkg.com/just-map-keys/-/just-map-keys-1.1.0.tgz#9663c9f971ba46e17f2b05e66fec81149375f230" integrity sha512-oNKi+4y7fr8lXnhKYpBbCkiwHRVkAnx0VDkCeTDtKKMzGr1Lz1Yym+RSieKUTKim68emC5Yxrb4YmiF9STDO+g== +jwa@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a" + integrity sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA== + dependencies: + buffer-equal-constant-time "1.0.1" + ecdsa-sig-formatter "1.0.11" + safe-buffer "^5.0.1" + +jws@^3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304" + integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA== + dependencies: + jwa "^1.4.1" + safe-buffer "^5.0.1" + keccak256@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/keccak256/-/keccak256-1.0.1.tgz#f579e937d6f32ac4ab62ff862d50204f775bb6f6" @@ -15469,11 +15539,41 @@ lodash.get@^4.4.2: resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk= +lodash.includes@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" + integrity sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8= + +lodash.isboolean@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" + integrity sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY= + +lodash.isinteger@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" + integrity sha1-YZwK89A/iwTDH1iChAt3sRzWg0M= + lodash.ismatch@^4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz#756cb5150ca3ba6f11085a78849645f188f85f37" integrity sha1-dWy1FQyjum8RCFp4hJZF8Yj4Xzc= +lodash.isnumber@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc" + integrity sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w= + +lodash.isplainobject@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" + integrity sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs= + +lodash.isstring@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" + integrity sha1-1SfftUVuynzJu5XV2ur4i6VKVFE= + lodash.kebabcase@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz#8489b1cb0d29ff88195cceca448ff6d6cc295c36" @@ -15499,6 +15599,11 @@ lodash.merge@^4.6.2: resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== +lodash.once@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" + integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w= + lodash.pad@^4.5.1: version "4.5.1" resolved "https://registry.yarnpkg.com/lodash.pad/-/lodash.pad-4.5.1.tgz#4330949a833a7c8da22cc20f6a26c4d59debba70" @@ -19950,7 +20055,7 @@ retry@^0.10.0: resolved "https://registry.yarnpkg.com/retry/-/retry-0.10.1.tgz#e76388d217992c252750241d3d3956fed98d8ff4" integrity sha1-52OI0heZLCUnUCQdPTlW/tmNj/Q= -reusify@^1.0.4: +reusify@^1.0.0, reusify@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== @@ -21052,6 +21157,17 @@ stealthy-require@^1.1.1: resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b" integrity sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks= +steed@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/steed/-/steed-1.1.3.tgz#f1525dd5adb12eb21bf74749537668d625b9abc5" + integrity sha1-8VJd1a2xLrIb90dJU3Zo1iW5q8U= + dependencies: + fastfall "^1.5.0" + fastparallel "^2.2.0" + fastq "^1.3.0" + fastseries "^1.7.0" + reusify "^1.0.0" + store@2.0.12: version "2.0.12" resolved "https://registry.yarnpkg.com/store/-/store-2.0.12.tgz#8c534e2a0b831f72b75fc5f1119857c44ef5d593" From 0154840aebec13824356f1d5ef159dfdf9ddcfe0 Mon Sep 17 00:00:00 2001 From: nivida Date: Mon, 16 Nov 2020 14:51:21 +0100 Subject: [PATCH 006/107] typo fixed --- packages/govern-tx/src/auth/Authenticator.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/govern-tx/src/auth/Authenticator.ts b/packages/govern-tx/src/auth/Authenticator.ts index 42a35c5bd..0c32bd9d1 100644 --- a/packages/govern-tx/src/auth/Authenticator.ts +++ b/packages/govern-tx/src/auth/Authenticator.ts @@ -79,6 +79,7 @@ export default class Authenticator { */ private getPublicKey(message: string, signature: string): string { return verifyMessage(arrayify(message), signature); + } /** * Returns a item from the whitelist with the given key From a0f9161ca311cbaa919823ac239edfc88708341b Mon Sep 17 00:00:00 2001 From: nivida Date: Mon, 16 Nov 2020 15:26:34 +0100 Subject: [PATCH 007/107] draft fastify auth plugin installed --- packages/govern-tx/package.json | 1 + packages/govern-tx/src/auth/auth-plugin.ts | 16 ++++++++++------ yarn.lock | 14 ++++++++++++++ 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/packages/govern-tx/package.json b/packages/govern-tx/package.json index 7c6585d66..cb2d64361 100644 --- a/packages/govern-tx/package.json +++ b/packages/govern-tx/package.json @@ -26,6 +26,7 @@ "@ethersproject/bytes": "^5.0.5", "@ethersproject/wallet": "^5.0.7", "fastify": "^3.8.0", + "fastify-cookie": "^4.1.0", "fastify-jwt": "^2.1.3", "fastify-plugin": "^3.0.0", "jsonwebtoken": "^8.5.1" diff --git a/packages/govern-tx/src/auth/auth-plugin.ts b/packages/govern-tx/src/auth/auth-plugin.ts index 8c7fd299e..afd59c26b 100644 --- a/packages/govern-tx/src/auth/auth-plugin.ts +++ b/packages/govern-tx/src/auth/auth-plugin.ts @@ -1,35 +1,39 @@ import fp from 'fastify-plugin' import Authenticator from './Authenticator' import { Unauthorized } from 'http-errors' +import fastifyCookie from 'fastify-cookie' export interface AuthOptions { authenticator: Authenticator, - secret: string + secret: string, + cookieName: string } function authPlugin(fastify, options: AuthOptions, next) { const authenticator = options.authenticator const secret = options.secret - + fastify.register(fastifyCookie) fastify.decorate('pkAuth', auth) next() function auth(request, reply, next) { - if (request.header.autorization && authenticator.verify(request.header.autorization)) { + if (authenticator.verify(request.cookies[options.cookieName])) { next() + return } const body = JSON.parse(request.body) const token = authenticator.authenticate(body.message, body.signature) if (!token) { - reply() + next(new Unauthorized('Unknown account!')) return } - - next(new Unauthorized('Unknown account!')) + + reply.setCookie(request.cookies[options.cookieName], token, {secure: true}) + next() } } diff --git a/yarn.lock b/yarn.lock index 9bfd87fc4..d04dc0af5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8100,6 +8100,11 @@ cookie-signature@1.0.6: resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw= +cookie-signature@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.1.0.tgz#cc94974f91fb9a9c1bb485e95fc2b7f4b120aff2" + integrity sha512-Alvs19Vgq07eunykd3Xy2jF0/qSNv2u7KDbAek9H5liV1UMijbqFs5cycZvv5dVsvseT/U4H8/7/w8Koh35C4A== + cookie@0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba" @@ -11068,6 +11073,15 @@ fastfall@^1.5.0: dependencies: reusify "^1.0.0" +fastify-cookie@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/fastify-cookie/-/fastify-cookie-4.1.0.tgz#b063f2b97cf9de7a33eb799a951b604ede48a915" + integrity sha512-+se+kNPFDE49JCiBYQrfPfchcW9s8BXjCqP5ijuYpbydTcMlo9pnyd8tyxLi43SIXBmeTBkB1CjklmD7nlHsXg== + dependencies: + cookie "^0.4.0" + cookie-signature "^1.1.0" + fastify-plugin "^2.0.0" + fastify-error@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/fastify-error/-/fastify-error-0.2.0.tgz#9a1c28d4f42b6259e7a549671c8e5e2d85660634" From 6b77717a915affc4250295c904014edeb7b8755c Mon Sep 17 00:00:00 2001 From: nivida Date: Mon, 16 Nov 2020 15:44:25 +0100 Subject: [PATCH 008/107] naming checked, index updated for AuthPlugin, and usful todo comments added --- packages/govern-tx/src/auth/Authenticator.ts | 2 +- packages/govern-tx/src/auth/auth-plugin.ts | 6 ++---- packages/govern-tx/src/index.ts | 18 +++++++++++++++--- 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/packages/govern-tx/src/auth/Authenticator.ts b/packages/govern-tx/src/auth/Authenticator.ts index 0c32bd9d1..2ae424e36 100644 --- a/packages/govern-tx/src/auth/Authenticator.ts +++ b/packages/govern-tx/src/auth/Authenticator.ts @@ -20,7 +20,7 @@ export default class Authenticator { constructor( private whitelist: Whitelist, private secret: string, - private jwtOptions: JWTOptions + private jwtOptions?: JWTOptions ) { } /** diff --git a/packages/govern-tx/src/auth/auth-plugin.ts b/packages/govern-tx/src/auth/auth-plugin.ts index afd59c26b..b7e97cf35 100644 --- a/packages/govern-tx/src/auth/auth-plugin.ts +++ b/packages/govern-tx/src/auth/auth-plugin.ts @@ -5,16 +5,13 @@ import fastifyCookie from 'fastify-cookie' export interface AuthOptions { authenticator: Authenticator, - secret: string, cookieName: string } - function authPlugin(fastify, options: AuthOptions, next) { const authenticator = options.authenticator - const secret = options.secret fastify.register(fastifyCookie) - fastify.decorate('pkAuth', auth) + fastify.decorate('authPlugin', auth) next() @@ -29,6 +26,7 @@ function authPlugin(fastify, options: AuthOptions, next) { const token = authenticator.authenticate(body.message, body.signature) if (!token) { next(new Unauthorized('Unknown account!')) + return } diff --git a/packages/govern-tx/src/index.ts b/packages/govern-tx/src/index.ts index b59b22f62..8bb60227e 100644 --- a/packages/govern-tx/src/index.ts +++ b/packages/govern-tx/src/index.ts @@ -1,11 +1,12 @@ // TODO: define request schema (eg. request validation, auth pre handler etc.) import fastify from 'fastify' -import pkAuth from './auth/auth-plugin' +import authPlugin from './auth/auth-plugin' import Configuration from './config/Configuration' import Database from './db/Database' import Whitelist from './db/Whitelist' +import Authenticator from './auth/Authenticator' import ExecuteTransaction from './transactions/ExecuteTransaction' import ChallengeTransaction from './transactions/ChallengeTransaction' @@ -23,11 +24,22 @@ const server = fastify({ level: 'debug' // Make this configurable with a process ENV } }) -server.register(pkAuth) + +// Register AuthPlugin +server.register( + authPlugin, + { + authenticator: new Authenticator(// TODO: Pass JWT options + whitelist, + 'secret' + ), + cookieName: 'govern_token' + } +) server.after(() => { // Register Auth pre handler hook - server.addHook('preHandler', server.pkAuth) + server.addHook('preHandler', server.authPlugin) /* -------------------- * * Transactions * From fac287ad60d6ab9f0fd85a13486420a4ff484c50 Mon Sep 17 00:00:00 2001 From: nivida Date: Mon, 16 Nov 2020 17:25:01 +0100 Subject: [PATCH 009/107] typo fixed --- packages/govern-tx/src/auth/auth-plugin.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/govern-tx/src/auth/auth-plugin.ts b/packages/govern-tx/src/auth/auth-plugin.ts index b7e97cf35..225a9de15 100644 --- a/packages/govern-tx/src/auth/auth-plugin.ts +++ b/packages/govern-tx/src/auth/auth-plugin.ts @@ -15,7 +15,7 @@ function authPlugin(fastify, options: AuthOptions, next) { next() - function auth(request, reply, next) { + async function auth(request, reply, next) { if (authenticator.verify(request.cookies[options.cookieName])) { next() @@ -23,7 +23,7 @@ function authPlugin(fastify, options: AuthOptions, next) { } const body = JSON.parse(request.body) - const token = authenticator.authenticate(body.message, body.signature) + const token = await authenticator.authenticate(body.message, body.signature) if (!token) { next(new Unauthorized('Unknown account!')) From ff120f08386e9c122eeb6e74747b4404818cdbc8 Mon Sep 17 00:00:00 2001 From: nivida Date: Mon, 16 Nov 2020 17:33:41 +0100 Subject: [PATCH 010/107] Authenticator simplified --- packages/govern-tx/src/auth/Authenticator.ts | 35 ++------------------ 1 file changed, 2 insertions(+), 33 deletions(-) diff --git a/packages/govern-tx/src/auth/Authenticator.ts b/packages/govern-tx/src/auth/Authenticator.ts index 2ae424e36..00c0b46df 100644 --- a/packages/govern-tx/src/auth/Authenticator.ts +++ b/packages/govern-tx/src/auth/Authenticator.ts @@ -8,7 +8,6 @@ export interface JWTOptions { verify: VerifyOptions } -// TODO: Implement provide possibility to configure JWT export default class Authenticator { /** * @param {Whitelist} whitelist @@ -36,9 +35,9 @@ export default class Authenticator { * @public */ public async authenticate(message: string, signature: string): Promise { - const publicKey = this.getPublicKey(message, signature); + const publicKey = verifyMessage(arrayify(message), signature); - if (await this.getItem(publicKey)) { + if (await this.whitelist.getItemByKey(publicKey)) { return jwt.sign({data: publicKey}, this.secret, this.jwtOptions.sign) } @@ -65,34 +64,4 @@ export default class Authenticator { return false; } } - - /** - * Extracts the public key from the given signed message - * - * @method getPublicKey - * - * @param signedMessage - The signed message from the user - * - * @returns {boolean} - * - * @private - */ - private getPublicKey(message: string, signature: string): string { - return verifyMessage(arrayify(message), signature); - } - - /** - * Returns a item from the whitelist with the given key - * - * @method getItem - * - * @param {string} publicKey - * - * @returns {ListItem} - * - * @private - */ - private getItem(publicKey: string): ListItem { - return this.whitelist.getItemByKey(publicKey); - } } From 315b3c154fd14bb868c98dac2d6540be65b20081 Mon Sep 17 00:00:00 2001 From: nivida Date: Mon, 16 Nov 2020 17:40:04 +0100 Subject: [PATCH 011/107] auth-plugin improved --- packages/govern-tx/src/auth/auth-plugin.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/govern-tx/src/auth/auth-plugin.ts b/packages/govern-tx/src/auth/auth-plugin.ts index 225a9de15..780ec09b0 100644 --- a/packages/govern-tx/src/auth/auth-plugin.ts +++ b/packages/govern-tx/src/auth/auth-plugin.ts @@ -16,7 +16,9 @@ function authPlugin(fastify, options: AuthOptions, next) { next() async function auth(request, reply, next) { - if (authenticator.verify(request.cookies[options.cookieName])) { + const cookie = request.cookies[options.cookieName]; + + if (cookie && authenticator.verify(cookie)) { next() return @@ -30,7 +32,7 @@ function authPlugin(fastify, options: AuthOptions, next) { return } - reply.setCookie(request.cookies[options.cookieName], token, {secure: true}) + reply.setCookie(options.cookieName, token, {secure: true}) next() } } From 2a3ffeca24da16d4ad37b016da5625253d6336e6 Mon Sep 17 00:00:00 2001 From: nivida Date: Tue, 17 Nov 2020 10:23:17 +0100 Subject: [PATCH 012/107] Auth structure simplified --- packages/govern-tx/src/auth/Authenticator.ts | 31 ++++++++++++--- packages/govern-tx/src/auth/auth-plugin.ts | 42 -------------------- packages/govern-tx/src/index.ts | 15 ++----- 3 files changed, 28 insertions(+), 60 deletions(-) delete mode 100644 packages/govern-tx/src/auth/auth-plugin.ts diff --git a/packages/govern-tx/src/auth/Authenticator.ts b/packages/govern-tx/src/auth/Authenticator.ts index 00c0b46df..1dbb16034 100644 --- a/packages/govern-tx/src/auth/Authenticator.ts +++ b/packages/govern-tx/src/auth/Authenticator.ts @@ -2,6 +2,9 @@ import Whitelist, {ListItem} from '../db/Whitelist' import jwt, {SignOptions, VerifyOptions} from 'jsonwebtoken' import {verifyMessage} from '@ethersproject/wallet'; import {arrayify} from '@ethersproject/bytes' +import { Unauthorized } from 'http-errors' +import fastifyCookie from 'fastify-cookie' +import fastify, { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; export interface JWTOptions { sign: SignOptions, @@ -17,10 +20,14 @@ export default class Authenticator { * @constructor */ constructor( + private fastify: FastifyInstance, private whitelist: Whitelist, private secret: string, + private cookieName: string, private jwtOptions?: JWTOptions - ) { } + ) { + fastify.register(fastifyCookie) + } /** * Checks if the given public key is existing in the whitelist and if no rate limit is exceeded @@ -34,14 +41,26 @@ export default class Authenticator { * * @public */ - public async authenticate(message: string, signature: string): Promise { - const publicKey = verifyMessage(arrayify(message), signature); + public async authenticate(request: FastifyRequest, reply: FastifyReply): Promise { + const cookie = request.cookies[this.cookieName]; + + if (cookie && this.verify(cookie)) { + return + } + const publicKey: string = verifyMessage(arrayify(request.body.message), body.signature); + let token: string; + if (await this.whitelist.getItemByKey(publicKey)) { - return jwt.sign({data: publicKey}, this.secret, this.jwtOptions.sign) + token = jwt.sign({data: publicKey}, this.secret, this.jwtOptions.sign) + } + + if (!token) { + throw new Unauthorized('Unknown account!') } - return false + reply.setCookie(this.cookieName, token, {secure: true}) + return } /** @@ -55,7 +74,7 @@ export default class Authenticator { * * @public */ - public verify(token: string): boolean { + private verify(token: string): boolean { try { jwt.verify(token, this.secret, this.jwtOptions.verify) diff --git a/packages/govern-tx/src/auth/auth-plugin.ts b/packages/govern-tx/src/auth/auth-plugin.ts deleted file mode 100644 index 780ec09b0..000000000 --- a/packages/govern-tx/src/auth/auth-plugin.ts +++ /dev/null @@ -1,42 +0,0 @@ -import fp from 'fastify-plugin' -import Authenticator from './Authenticator' -import { Unauthorized } from 'http-errors' -import fastifyCookie from 'fastify-cookie' - -export interface AuthOptions { - authenticator: Authenticator, - cookieName: string -} - -function authPlugin(fastify, options: AuthOptions, next) { - const authenticator = options.authenticator - fastify.register(fastifyCookie) - fastify.decorate('authPlugin', auth) - - next() - - async function auth(request, reply, next) { - const cookie = request.cookies[options.cookieName]; - - if (cookie && authenticator.verify(cookie)) { - next() - - return - } - - const body = JSON.parse(request.body) - const token = await authenticator.authenticate(body.message, body.signature) - if (!token) { - next(new Unauthorized('Unknown account!')) - - return - } - - reply.setCookie(options.cookieName, token, {secure: true}) - next() - } -} - - -const plugin = fp(authPlugin, '3.x') -export default plugin diff --git a/packages/govern-tx/src/index.ts b/packages/govern-tx/src/index.ts index 8bb60227e..136c5c3e3 100644 --- a/packages/govern-tx/src/index.ts +++ b/packages/govern-tx/src/index.ts @@ -25,21 +25,12 @@ const server = fastify({ } }) -// Register AuthPlugin -server.register( - authPlugin, - { - authenticator: new Authenticator(// TODO: Pass JWT options - whitelist, - 'secret' - ), - cookieName: 'govern_token' - } -) +// TODO: Pass JWT options +const authenticator = new Authenticator(server, whitelist, 'secret', 'govern_token') server.after(() => { // Register Auth pre handler hook - server.addHook('preHandler', server.authPlugin) + server.addHook('preHandler', authenticator.authenticate) /* -------------------- * * Transactions * From 175d8d18dc54d2ca79c3f3435d4b75585d28414e Mon Sep 17 00:00:00 2001 From: nivida Date: Tue, 17 Nov 2020 10:30:14 +0100 Subject: [PATCH 013/107] fastify configurations updated --- packages/govern-tx/src/index.ts | 67 ++++++++++++++++++--------------- 1 file changed, 36 insertions(+), 31 deletions(-) diff --git a/packages/govern-tx/src/index.ts b/packages/govern-tx/src/index.ts index 136c5c3e3..38eafaaba 100644 --- a/packages/govern-tx/src/index.ts +++ b/packages/govern-tx/src/index.ts @@ -22,49 +22,54 @@ const whitelist = new Whitelist(new Database(config)); const server = fastify({ logger: { level: 'debug' // Make this configurable with a process ENV - } + }, + ignoreTrailingSlash: true + //https: {} TODO: Configure TLS }) -// TODO: Pass JWT options -const authenticator = new Authenticator(server, whitelist, 'secret', 'govern_token') +/* -------------------- * +* Setup Auth * +* -------------------- */ +const authenticator = new Authenticator(server, whitelist, 'secret', 'govern_token') // TODO: Pass JWT options +server.addHook('preHandler', authenticator.authenticate.bind(authenticator)) -server.after(() => { - // Register Auth pre handler hook - server.addHook('preHandler', authenticator.authenticate) - /* -------------------- * - * Transactions * - * -------------------- */ - server.post('/execute', {}, (request, reply): Promise => { - return new ExecuteTransaction(config, request.params).execute() - }) +/* -------------------- * +* Transactions * +* -------------------- */ +server.post('/execute', {}, (request, reply): Promise => { + return new ExecuteTransaction(config, request.params).execute() +}) - server.post('/schedule', {}, (request, reply): Promise => { - return new ScheduleTransaction(config, request.params).execute() - }) +server.post('/schedule', {}, (request, reply): Promise => { + return new ScheduleTransaction(config, request.params).execute() +}) - server.post('/challenge', {}, (request, reply): Promise => { - return new ChallengeTransaction(config, request.params).execute() - }) +server.post('/challenge', {}, (request, reply): Promise => { + return new ChallengeTransaction(config, request.params).execute() +}) - /* -------------------- * - * Whitelist * - * -------------------- */ - server.post('/whitelist', {}, (request, reply): Promise => { - return new AddItemAction(whitelist, request.params).execute() - }) +/* -------------------- * +* Whitelist * +* -------------------- */ +server.post('/whitelist', {}, (request, reply): Promise => { + return new AddItemAction(whitelist, request.params).execute() +}) - server.delete('/whitelist', {}, (request, reply): Promise => { - return new DeleteItemAction(whitelist, request.params).execute() - }) +server.delete('/whitelist', {}, (request, reply): Promise => { + return new DeleteItemAction(whitelist, request.params).execute() +}) - server.get('/whitelist', {}, (request, reply): Promise => { - return new GetListAction(whitelist).execute() - }) +server.get('/whitelist', {}, (request, reply): Promise => { + return new GetListAction(whitelist).execute() }) -server.listen(4040, (error, address) => { + +/* -------------------- * +* Start Server * +* -------------------- */ +server.listen(4040, '0.0.0.0', (error: Error, address: string): void => { if (error) { console.error(error) process.exit(0) From 367d104f67c5ae3b1743bfd9c1a56b5796a6e6a0 Mon Sep 17 00:00:00 2001 From: nivida Date: Tue, 17 Nov 2020 10:52:52 +0100 Subject: [PATCH 014/107] basic Configuration class created and started to define the fastify request/response schema --- packages/govern-tx/src/auth/Authenticator.ts | 1 + .../govern-tx/src/config/Configuration.ts | 89 ++++++++++++++++++- packages/govern-tx/src/index.ts | 13 ++- 3 files changed, 101 insertions(+), 2 deletions(-) diff --git a/packages/govern-tx/src/auth/Authenticator.ts b/packages/govern-tx/src/auth/Authenticator.ts index 1dbb16034..acfee26f3 100644 --- a/packages/govern-tx/src/auth/Authenticator.ts +++ b/packages/govern-tx/src/auth/Authenticator.ts @@ -11,6 +11,7 @@ export interface JWTOptions { verify: VerifyOptions } +// TODO: Implement rules for Admin public key resp. to execute whitelist actions export default class Authenticator { /** * @param {Whitelist} whitelist diff --git a/packages/govern-tx/src/config/Configuration.ts b/packages/govern-tx/src/config/Configuration.ts index 02fd3a051..922e3b2b9 100644 --- a/packages/govern-tx/src/config/Configuration.ts +++ b/packages/govern-tx/src/config/Configuration.ts @@ -1,4 +1,91 @@ +export interface EthereumOptions { + url: string +} + +export interface DatabaseOptions { + user: string, + host: string, + password: string, + database: string, + port: number +} + +// TODO: Add if required input validations export default class Configuration { - // TODO: Implement validation etc. if necessary + /** + * @param {EthereumOptions} _ethereum + * @param {DatabaseOptions} _database + * + * @constructor + */ + constructor(private _ethereum: EthereumOptions, private _database: DatabaseOptions) { } + + /** + * Getter for the database options. + * + * @property database + * + * @returns {DatabaseOptions} + * + * @public + */ + public get database(): DatabaseOptions { + return this._database; + } + + /** + * Setter for the database options. + * + * @property database + * + * @returns {void} + * + * @public + */ + public set database(value: DatabaseOptions) { + this._database = value; + } + + /** + * Getter for ethereum node options + * + * @property ethereum + * + * @returns {EthereumOptions} + * + * @public + */ + public get ethereum(): EthereumOptions { + return this._ethereum; + } + + /** + * Setter for the ethereum node options + * + * @property ethereum + * + * @returns {EthereumOptions} + * + * @public + */ + public set ethereum(value: EthereumOptions) { + this._ethereum = value; + } + + /** + * Returns the internal options object as new object + * + * @method toObject + * + * @returns {object} + * + * @public + */ + public toObject(): any { + return { + ethereum: this._ethereum, + database: this._database + } + } } diff --git a/packages/govern-tx/src/index.ts b/packages/govern-tx/src/index.ts index 38eafaaba..3466ac3d7 100644 --- a/packages/govern-tx/src/index.ts +++ b/packages/govern-tx/src/index.ts @@ -1,7 +1,6 @@ // TODO: define request schema (eg. request validation, auth pre handler etc.) import fastify from 'fastify' -import authPlugin from './auth/auth-plugin' import Configuration from './config/Configuration' import Database from './db/Database' @@ -27,6 +26,18 @@ const server = fastify({ //https: {} TODO: Configure TLS }) +const schema = { + body: { + type: 'object', + required: ['message', 'signature'], + properties: { + message: { type: 'string' }, + signature: { type: 'string' } + } + }, + //response: {} TODO: Define response validation for each action/command +} + /* -------------------- * * Setup Auth * * -------------------- */ From 6a620ae5aecd6c4f2220348ddb2e1edce899a1c5 Mon Sep 17 00:00:00 2001 From: nivida Date: Tue, 17 Nov 2020 10:54:53 +0100 Subject: [PATCH 015/107] dummy configuration added --- packages/govern-tx/src/index.ts | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/packages/govern-tx/src/index.ts b/packages/govern-tx/src/index.ts index 3466ac3d7..5589d5fc9 100644 --- a/packages/govern-tx/src/index.ts +++ b/packages/govern-tx/src/index.ts @@ -15,7 +15,18 @@ import AddItemAction from './whitelist/AddItemAction' import DeleteItemAction from './whitelist/DeleteItemAction' import GetListAction from './whitelist/GetListAction' -const config = new Configuration() +const config = new Configuration( + { + url: 'localhost:8545' + }, + { + user: 'govern', + host: 'localhost', + password: 'dev', + database: 'govern', + port: 4000 + } +) const whitelist = new Whitelist(new Database(config)); const server = fastify({ @@ -48,15 +59,15 @@ server.addHook('preHandler', authenticator.authenticate.bind(authenticator)) /* -------------------- * * Transactions * * -------------------- */ -server.post('/execute', {}, (request, reply): Promise => { +server.post('/execute', {schema}, (request, reply): Promise => { return new ExecuteTransaction(config, request.params).execute() }) -server.post('/schedule', {}, (request, reply): Promise => { +server.post('/schedule', {schema}, (request, reply): Promise => { return new ScheduleTransaction(config, request.params).execute() }) -server.post('/challenge', {}, (request, reply): Promise => { +server.post('/challenge', {schema}, (request, reply): Promise => { return new ChallengeTransaction(config, request.params).execute() }) @@ -64,15 +75,15 @@ server.post('/challenge', {}, (request, reply): Promise => { /* -------------------- * * Whitelist * * -------------------- */ -server.post('/whitelist', {}, (request, reply): Promise => { +server.post('/whitelist', {schema}, (request, reply): Promise => { return new AddItemAction(whitelist, request.params).execute() }) -server.delete('/whitelist', {}, (request, reply): Promise => { +server.delete('/whitelist', {schema}, (request, reply): Promise => { return new DeleteItemAction(whitelist, request.params).execute() }) -server.get('/whitelist', {}, (request, reply): Promise => { +server.get('/whitelist', {schema}, (request, reply): Promise => { return new GetListAction(whitelist).execute() }) From 2a560162d38255bcaab8a94c984c05a4a6984487 Mon Sep 17 00:00:00 2001 From: nivida Date: Tue, 17 Nov 2020 11:00:08 +0100 Subject: [PATCH 016/107] definition of routes improved --- packages/govern-tx/src/index.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/govern-tx/src/index.ts b/packages/govern-tx/src/index.ts index 5589d5fc9..d5ecbe4e9 100644 --- a/packages/govern-tx/src/index.ts +++ b/packages/govern-tx/src/index.ts @@ -59,15 +59,15 @@ server.addHook('preHandler', authenticator.authenticate.bind(authenticator)) /* -------------------- * * Transactions * * -------------------- */ -server.post('/execute', {schema}, (request, reply): Promise => { +server.post('/execute', {schema}, (request): Promise => { return new ExecuteTransaction(config, request.params).execute() }) -server.post('/schedule', {schema}, (request, reply): Promise => { +server.post('/schedule', {schema}, (request): Promise => { return new ScheduleTransaction(config, request.params).execute() }) -server.post('/challenge', {schema}, (request, reply): Promise => { +server.post('/challenge', {schema}, (request): Promise => { return new ChallengeTransaction(config, request.params).execute() }) @@ -75,15 +75,15 @@ server.post('/challenge', {schema}, (request, reply): Promise => { +server.post('/whitelist', {schema}, (request): Promise => { return new AddItemAction(whitelist, request.params).execute() }) -server.delete('/whitelist', {schema}, (request, reply): Promise => { +server.delete('/whitelist', {schema}, (request): Promise => { return new DeleteItemAction(whitelist, request.params).execute() }) -server.get('/whitelist', {schema}, (request, reply): Promise => { +server.get('/whitelist', {schema}, (): Promise => { return new GetListAction(whitelist).execute() }) From 0854ae68e8cedb35ea0653c75ea1ca235d901fb4 Mon Sep 17 00:00:00 2001 From: nivida Date: Tue, 17 Nov 2020 11:40:24 +0100 Subject: [PATCH 017/107] index.ts cleaned up and Boostrap created --- .../lib/transactions/AbstractTransaction.ts | 21 ++ .../lib/whitelist/AbstractWhitelistAction.ts | 11 + packages/govern-tx/src/Bootstrap.ts | 194 ++++++++++++++++++ .../govern-tx/src/config/Configuration.ts | 76 ++++++- packages/govern-tx/src/index.ts | 121 +++-------- 5 files changed, 323 insertions(+), 100 deletions(-) create mode 100644 packages/govern-tx/src/Bootstrap.ts diff --git a/packages/govern-tx/lib/transactions/AbstractTransaction.ts b/packages/govern-tx/lib/transactions/AbstractTransaction.ts index e97a21602..7148e4f80 100644 --- a/packages/govern-tx/lib/transactions/AbstractTransaction.ts +++ b/packages/govern-tx/lib/transactions/AbstractTransaction.ts @@ -34,4 +34,25 @@ export default abstract class AbstractTransaction extends AbstractAction { * @public */ public abstract execute(): Promise + + /** + * Returns the schema of a transaction command + * + * @property schema + * + * @returns {any} + */ + public static get schema(): any { + return { + body: { + type: 'object', + required: ['message', 'signature'], + properties: { + message: { type: 'string' }, + signature: { type: 'string' } + } + }, + //response: {} TODO: Define response validation for each action/command + } + } } diff --git a/packages/govern-tx/lib/whitelist/AbstractWhitelistAction.ts b/packages/govern-tx/lib/whitelist/AbstractWhitelistAction.ts index 154eda8f5..7cb03481e 100644 --- a/packages/govern-tx/lib/whitelist/AbstractWhitelistAction.ts +++ b/packages/govern-tx/lib/whitelist/AbstractWhitelistAction.ts @@ -22,4 +22,15 @@ export default abstract class AbstractWhitelistAction extends AbstractAction { * @public */ public abstract execute(): Promise + + /** + * Returns the schema of a whitelist command + * + * @property schema + * + * @returns {any} + */ + public static get schema(): any { + return {} // TODO: Define schema + } } diff --git a/packages/govern-tx/src/Bootstrap.ts b/packages/govern-tx/src/Bootstrap.ts new file mode 100644 index 000000000..d76bf9e69 --- /dev/null +++ b/packages/govern-tx/src/Bootstrap.ts @@ -0,0 +1,194 @@ +import fastify, { FastifyInstance } from 'fastify' +import Configuration from './config/Configuration' +import Database from './db/Database' +import Whitelist from './db/Whitelist' +import Authenticator, { JWTOptions } from './auth/Authenticator'; + +import ExecuteTransaction from './transactions/ExecuteTransaction' +import ChallengeTransaction from './transactions/ChallengeTransaction' +import ScheduleTransaction from './transactions/ScheduleTransaction' + +import AddItemAction from './whitelist/AddItemAction' +import DeleteItemAction from './whitelist/DeleteItemAction' +import GetListAction from './whitelist/GetListAction' +import AbstractTransaction from '../lib/transactions/AbstractTransaction' +import AbstractWhitelistAction from '../lib/whitelist/AbstractWhitelistAction'; + +export default class Bootstrap { + /** + * @property {FastifyInstance} server + * + * @private + */ + private server: FastifyInstance + + /** + * @property {Authenticator} authenticator + * + * @private + */ + private authenticator: Authenticator + + /** + * @property {whitelist} Whitelist + * + * @private + */ + private whitelist: Whitelist + + /** + * @param {Configuration} config + * + * @constructor + */ + constructor(private config: Configuration) { + this.setServer() + this.setupAuth() + this.registerTransactionRoutes() + this.registerWhitelistRoutes() + } + + /** + * Starts the entire server + * + * @method run + * + * @returns {void} + * + * @public + */ + public run(): void { + this.server.listen( + this.config.server.port, + this.config.server.host, + (error: Error, address: string): void => { + if (error) { + console.error(error) + process.exit(0) + } + + console.log(`Server is listening at ${address}`) + } + ) + } + + /** + * TODO: Could be done cleaner but don't think it is necessary + * + * Register all transaction relates routes + * + * @method registerTransactionRoutes + * + * @returns {void} + * + * @private + */ + private registerTransactionRoutes(): void { + this.server.post( + '/execute', + {schema: AbstractTransaction.schema}, + (request): Promise => { + return new ExecuteTransaction(this.config, request.params).execute() + } + ) + + this.server.post( + '/schedule', + {schema: AbstractTransaction.schema}, + (request): Promise => { + return new ScheduleTransaction(this.config, request.params).execute() + } + ) + + this.server.post( + '/challenge', + {schema: AbstractTransaction.schema}, + (request): Promise => { + return new ChallengeTransaction(this.config, request.params).execute() + } + ) + } + + /** + * TODO: Could be done cleaner but don't think it is necessary + * + * Register all whitelist relates routes + * + * @method registerWhitelistRoutes + * + * @returns {void} + * + * @private + */ + private registerWhitelistRoutes(): void { + this.server.post( + '/whitelist', + {schema: AbstractWhitelistAction.schema}, + (request): Promise => { + return new AddItemAction(this.whitelist, request.params).execute() + } + ) + + this.server.delete( + '/whitelist', + {schema: AbstractWhitelistAction.schema}, + (request): Promise => { + return new DeleteItemAction(this.whitelist, request.params).execute() + } + ) + + this.server.get( + '/whitelist', + {schema: AbstractWhitelistAction.schema}, + (): Promise => { + return new GetListAction(this.whitelist).execute() + } + ) + } + + /** + * Inititates the server instance + * + * @method setServer + * + * @returns {void} + * + * @private + */ + private setServer(): void { + this.server = fastify({ + logger: { + level: this.config.server.logLevel ?? 'debug' + }, + ignoreTrailingSlash: true + //https: {} TODO: Configure TLS + }) + } + + /** + * Registers the authentication handler + * + * @method setupAuth + * + * @returns {void} + * + * @private + */ + private setupAuth(): void { + // TODO: Change database handling here to only have one open connection + this.whitelist = new Whitelist(new Database(this.config)); + + this.authenticator = new Authenticator( + this.server, + this.whitelist, + this.config.auth.secret, + this.config.auth.cookieName, + this.config.auth.jwtOptions + ) + + this.server.addHook( + 'preHandler', + this.authenticator.authenticate.bind(this.authenticator) + ) + } +} diff --git a/packages/govern-tx/src/config/Configuration.ts b/packages/govern-tx/src/config/Configuration.ts index 922e3b2b9..c79bdbb7a 100644 --- a/packages/govern-tx/src/config/Configuration.ts +++ b/packages/govern-tx/src/config/Configuration.ts @@ -1,3 +1,4 @@ +import { JWTOptions } from '../auth/Authenticator'; export interface EthereumOptions { url: string @@ -11,6 +12,18 @@ export interface DatabaseOptions { port: number } +export interface AuthOptions { + secret: string, + cookieName: string + jwtOptions?: JWTOptions +} + +export interface ServerOptions { + host: string, + port: number, + logLevel?: string +} + // TODO: Add if required input validations export default class Configuration { /** @@ -19,7 +32,12 @@ export default class Configuration { * * @constructor */ - constructor(private _ethereum: EthereumOptions, private _database: DatabaseOptions) { } + constructor( + private _ethereum: EthereumOptions, + private _database: DatabaseOptions, + private _auth: AuthOptions, + private _server: ServerOptions, + ) { } /** * Getter for the database options. @@ -73,6 +91,58 @@ export default class Configuration { this._ethereum = value; } + /** + * Getter for the authentication options + * + * @property auth + * + * @returns {AuthOptions} + * + * @public + */ + public get auth() { + return this._auth; + } + + /** + * Setter for the authentication options + * + * @property auth + * + * @returns {void} + * + * @public + */ + public set auth(value: AuthOptions) { + this._auth = value; + } + + /** + * Getter for the server options + * + * @property server + * + * @returns {ServerOptions} + * + * @public + */ + public get server() { + return this._server; + } + + /** + * Setter for the server options + * + * @property server + * + * @returns {void} + * + * @public + */ + public set server(value: ServerOptions) { + this._server = value; + } + /** * Returns the internal options object as new object * @@ -85,7 +155,9 @@ export default class Configuration { public toObject(): any { return { ethereum: this._ethereum, - database: this._database + database: this._database, + auth: this._auth, + server: this._server } } } diff --git a/packages/govern-tx/src/index.ts b/packages/govern-tx/src/index.ts index d5ecbe4e9..63e5d0918 100644 --- a/packages/govern-tx/src/index.ts +++ b/packages/govern-tx/src/index.ts @@ -1,101 +1,26 @@ -// TODO: define request schema (eg. request validation, auth pre handler etc.) - -import fastify from 'fastify' - import Configuration from './config/Configuration' -import Database from './db/Database' -import Whitelist from './db/Whitelist' -import Authenticator from './auth/Authenticator' - -import ExecuteTransaction from './transactions/ExecuteTransaction' -import ChallengeTransaction from './transactions/ChallengeTransaction' -import ScheduleTransaction from './transactions/ScheduleTransaction' - -import AddItemAction from './whitelist/AddItemAction' -import DeleteItemAction from './whitelist/DeleteItemAction' -import GetListAction from './whitelist/GetListAction' - -const config = new Configuration( - { - url: 'localhost:8545' - }, - { - user: 'govern', - host: 'localhost', - password: 'dev', - database: 'govern', - port: 4000 - } -) -const whitelist = new Whitelist(new Database(config)); - -const server = fastify({ - logger: { - level: 'debug' // Make this configurable with a process ENV - }, - ignoreTrailingSlash: true - //https: {} TODO: Configure TLS -}) - -const schema = { - body: { - type: 'object', - required: ['message', 'signature'], - properties: { - message: { type: 'string' }, - signature: { type: 'string' } +import Bootstrap from './Bootstrap' + +new Bootstrap( + new Configuration( + { + url: 'localhost:8545' + }, + { + user: 'govern', + host: 'localhost', + password: 'dev', + database: 'govern', + port: 4000 + }, + { + secret: 'secret', + cookieName: 'govern_cookie' + }, + { + host: '0.0.0.0', + port: 4040 } - }, - //response: {} TODO: Define response validation for each action/command -} - -/* -------------------- * -* Setup Auth * -* -------------------- */ -const authenticator = new Authenticator(server, whitelist, 'secret', 'govern_token') // TODO: Pass JWT options -server.addHook('preHandler', authenticator.authenticate.bind(authenticator)) - - -/* -------------------- * -* Transactions * -* -------------------- */ -server.post('/execute', {schema}, (request): Promise => { - return new ExecuteTransaction(config, request.params).execute() -}) - -server.post('/schedule', {schema}, (request): Promise => { - return new ScheduleTransaction(config, request.params).execute() -}) - -server.post('/challenge', {schema}, (request): Promise => { - return new ChallengeTransaction(config, request.params).execute() -}) - - -/* -------------------- * -* Whitelist * -* -------------------- */ -server.post('/whitelist', {schema}, (request): Promise => { - return new AddItemAction(whitelist, request.params).execute() -}) - -server.delete('/whitelist', {schema}, (request): Promise => { - return new DeleteItemAction(whitelist, request.params).execute() -}) - -server.get('/whitelist', {schema}, (): Promise => { - return new GetListAction(whitelist).execute() -}) - - -/* -------------------- * -* Start Server * -* -------------------- */ -server.listen(4040, '0.0.0.0', (error: Error, address: string): void => { - if (error) { - console.error(error) - process.exit(0) - } + ) +).run() - console.log(`Server is listening at ${address}`) -}) From b69520e38845206d6b75029a024d55c6264c8cf9 Mon Sep 17 00:00:00 2001 From: nivida Date: Tue, 17 Nov 2020 12:14:54 +0100 Subject: [PATCH 018/107] simple Database adapter implemented --- packages/govern-tx/package.json | 3 +- packages/govern-tx/src/Bootstrap.ts | 3 +- .../govern-tx/src/config/Configuration.ts | 4 +- packages/govern-tx/src/db/Database.ts | 40 ++++++++++++++++--- packages/govern-tx/src/index.ts | 1 - yarn.lock | 5 +++ 6 files changed, 45 insertions(+), 11 deletions(-) diff --git a/packages/govern-tx/package.json b/packages/govern-tx/package.json index cb2d64361..fc43d92db 100644 --- a/packages/govern-tx/package.json +++ b/packages/govern-tx/package.json @@ -29,6 +29,7 @@ "fastify-cookie": "^4.1.0", "fastify-jwt": "^2.1.3", "fastify-plugin": "^3.0.0", - "jsonwebtoken": "^8.5.1" + "jsonwebtoken": "^8.5.1", + "postgres": "^1.0.2" } } diff --git a/packages/govern-tx/src/Bootstrap.ts b/packages/govern-tx/src/Bootstrap.ts index d76bf9e69..2657f8e17 100644 --- a/packages/govern-tx/src/Bootstrap.ts +++ b/packages/govern-tx/src/Bootstrap.ts @@ -175,8 +175,7 @@ export default class Bootstrap { * @private */ private setupAuth(): void { - // TODO: Change database handling here to only have one open connection - this.whitelist = new Whitelist(new Database(this.config)); + this.whitelist = new Whitelist(new Database(this.config.database)); this.authenticator = new Authenticator( this.server, diff --git a/packages/govern-tx/src/config/Configuration.ts b/packages/govern-tx/src/config/Configuration.ts index c79bdbb7a..9ca6f612d 100644 --- a/packages/govern-tx/src/config/Configuration.ts +++ b/packages/govern-tx/src/config/Configuration.ts @@ -100,7 +100,7 @@ export default class Configuration { * * @public */ - public get auth() { + public get auth(): AuthOptions { return this._auth; } @@ -126,7 +126,7 @@ export default class Configuration { * * @public */ - public get server() { + public get server(): ServerOptions { return this._server; } diff --git a/packages/govern-tx/src/db/Database.ts b/packages/govern-tx/src/db/Database.ts index 869a55fa4..1643d861c 100644 --- a/packages/govern-tx/src/db/Database.ts +++ b/packages/govern-tx/src/db/Database.ts @@ -1,12 +1,43 @@ -import Configuration from '../config/Configuration' +import postgres from 'postgres' +import { DatabaseOptions } from '../config/Configuration' export default class Database { + /** + * The sql function of the postgres client + * + * @property {Function} sql + * + * @private + */ + private sql; + /** - * @param configuration - The configuration object of this service + * @param {DatabaseOptions} config - The database configuration * * @constructor */ - constructor(private configuration: Configuration) { } + constructor(private config: DatabaseOptions) { + this.connect() + } + + /** + * Establishes the connection to the postgres DB + * + * @method connect + * + * @returns {void} + * + * @private + */ + private connect(): void { + this.sql = postgres({ + host: this.config.host, + port: this.config.port, + database: this.config.database, + username: this.config.user, + password: this.config.password + }); + } /** * Executes a query on the DB @@ -20,7 +51,6 @@ export default class Database { * @public */ public query(query: string): Promise { - //TODO: Decide which DB to use (Should we share the DB with TheGraph?) and implement the DB adapter with the related driver - return Promise.resolve(true) + return this.sql(query) } } diff --git a/packages/govern-tx/src/index.ts b/packages/govern-tx/src/index.ts index 63e5d0918..017d15eae 100644 --- a/packages/govern-tx/src/index.ts +++ b/packages/govern-tx/src/index.ts @@ -23,4 +23,3 @@ new Bootstrap( } ) ).run() - diff --git a/yarn.lock b/yarn.lock index d04dc0af5..d8724490a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18727,6 +18727,11 @@ postcss@^7, postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.14, postcss@^7.0.17, po source-map "^0.6.1" supports-color "^6.1.0" +postgres@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/postgres/-/postgres-1.0.2.tgz#7b9f6769934f727cec0f1d7ef58d4915a327f5dd" + integrity sha512-zeLgt42KSUNgX/uvo+gbVxTAYwgSY6MIKuU/a8YWuObX4rtGuKrVWopvEAqIAPSO0FeHS1TsSKnqPjoufPy8NA== + postinstall-postinstall@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/postinstall-postinstall/-/postinstall-postinstall-2.1.0.tgz#4f7f77441ef539d1512c40bd04c71b06a4704ca3" From 108af5e2a70423c6a1be775be5d4f7acb4da067a Mon Sep 17 00:00:00 2001 From: nivida Date: Tue, 17 Nov 2020 12:33:25 +0100 Subject: [PATCH 019/107] whitelist actions with the corresponding validations implemented --- packages/govern-tx/lib/AbstractAction.ts | 22 ++++++++++++- .../lib/transactions/AbstractTransaction.ts | 14 ++------ .../lib/whitelist/AbstractWhitelistAction.ts | 8 +++-- packages/govern-tx/package.json | 1 + .../govern-tx/src/whitelist/AddItemAction.ts | 32 +++++++++++++++++-- .../src/whitelist/DeleteItemAction.ts | 22 ++++++++++++- .../govern-tx/src/whitelist/GetListAction.ts | 2 +- 7 files changed, 81 insertions(+), 20 deletions(-) diff --git a/packages/govern-tx/lib/AbstractAction.ts b/packages/govern-tx/lib/AbstractAction.ts index c1e1f7c83..c7c1b2187 100644 --- a/packages/govern-tx/lib/AbstractAction.ts +++ b/packages/govern-tx/lib/AbstractAction.ts @@ -4,7 +4,7 @@ export default abstract class AbstractAction { * * @var {Object} parameters */ - private parameters: any; + protected parameters: any; /** * @param {Object} parameters @@ -40,4 +40,24 @@ export default abstract class AbstractAction { * @public */ public abstract execute(): Promise + + /** + * Returns the schema of a whitelist command + * + * @property schema + * + * @returns {any} + */ + public static get schema(): any { + return { + body: { + type: 'object', + required: ['message', 'signature'], + properties: { + message: { type: 'string' }, + signature: { type: 'string' } + } + } + } + } } diff --git a/packages/govern-tx/lib/transactions/AbstractTransaction.ts b/packages/govern-tx/lib/transactions/AbstractTransaction.ts index 7148e4f80..2698c0b59 100644 --- a/packages/govern-tx/lib/transactions/AbstractTransaction.ts +++ b/packages/govern-tx/lib/transactions/AbstractTransaction.ts @@ -36,6 +36,8 @@ export default abstract class AbstractTransaction extends AbstractAction { public abstract execute(): Promise /** + * TODO: Define response validation + * * Returns the schema of a transaction command * * @property schema @@ -43,16 +45,6 @@ export default abstract class AbstractTransaction extends AbstractAction { * @returns {any} */ public static get schema(): any { - return { - body: { - type: 'object', - required: ['message', 'signature'], - properties: { - message: { type: 'string' }, - signature: { type: 'string' } - } - }, - //response: {} TODO: Define response validation for each action/command - } + return super.schema() } } diff --git a/packages/govern-tx/lib/whitelist/AbstractWhitelistAction.ts b/packages/govern-tx/lib/whitelist/AbstractWhitelistAction.ts index 7cb03481e..3d751680a 100644 --- a/packages/govern-tx/lib/whitelist/AbstractWhitelistAction.ts +++ b/packages/govern-tx/lib/whitelist/AbstractWhitelistAction.ts @@ -8,7 +8,7 @@ export default abstract class AbstractWhitelistAction extends AbstractAction { * * @constructor */ - constructor(private whitelist: Whitelist, parameters: any = {}) { + constructor(protected whitelist: Whitelist, parameters: any = {}) { super(parameters) } @@ -24,13 +24,15 @@ export default abstract class AbstractWhitelistAction extends AbstractAction { public abstract execute(): Promise /** - * Returns the schema of a whitelist command + * TODO: Define response validation + * + * Returns the schema of a transaction command * * @property schema * * @returns {any} */ public static get schema(): any { - return {} // TODO: Define schema + return super.schema() } } diff --git a/packages/govern-tx/package.json b/packages/govern-tx/package.json index fc43d92db..8f2ae9770 100644 --- a/packages/govern-tx/package.json +++ b/packages/govern-tx/package.json @@ -23,6 +23,7 @@ "typescript": "^4.0.5" }, "dependencies": { + "@ethersproject/address": "^5.0.5", "@ethersproject/bytes": "^5.0.5", "@ethersproject/wallet": "^5.0.7", "fastify": "^3.8.0", diff --git a/packages/govern-tx/src/whitelist/AddItemAction.ts b/packages/govern-tx/src/whitelist/AddItemAction.ts index 903983d07..d8f320a33 100644 --- a/packages/govern-tx/src/whitelist/AddItemAction.ts +++ b/packages/govern-tx/src/whitelist/AddItemAction.ts @@ -1,7 +1,30 @@ -import AbstractWhitelistAction from "../../lib/whitelist/AbstractWhitelistAction"; -import AbstractWhitelistAction from "../../lib/whitelist/AbstractWhitelistAction"; +import {isAddress} from '@ethersproject/address' +import AbstractWhitelistAction from "../../lib/whitelist/AbstractWhitelistAction" export default class AddItemAction extends AbstractWhitelistAction { + /** + * Validates the given parameters. + * + * @method validateParameters + * + * @param {Object} parameters + * + * @returns {Object} + * + * @protected + */ + protected validateParameters(parameters: any): any { + if (!isAddress(this.parameters.message.publicKey)) { + throw new Error('Invalid public key passed!') + } + + if (!(this.parameters.message.rateLimit > 0)) { + throw new Error('Invalid rate limit passed!') + } + + return parameters; + } + /** * Adds a new item to the whitelist * @@ -12,6 +35,9 @@ export default class AddItemAction extends AbstractWhitelistAction { * @public */ public execute(): Promise { - return Promise.resolve(true) + return this.whitelist.addItem( + this.parameters.publicKey, + this.parameters.rateLimit + ) } } diff --git a/packages/govern-tx/src/whitelist/DeleteItemAction.ts b/packages/govern-tx/src/whitelist/DeleteItemAction.ts index 39fdadecd..edf91cfff 100644 --- a/packages/govern-tx/src/whitelist/DeleteItemAction.ts +++ b/packages/govern-tx/src/whitelist/DeleteItemAction.ts @@ -1,6 +1,26 @@ +import {isAddress} from '@ethersproject/address' import AbstractWhitelistAction from "../../lib/whitelist/AbstractWhitelistAction"; export default class DeleteItemAction extends AbstractWhitelistAction { + /** + * Validates the given parameters. + * + * @method validateParameters + * + * @param {Object} parameters + * + * @returns {Object} + * + * @protected + */ + protected validateParameters(parameters: any): any { + if (!isAddress(this.parameters.message.publicKey)) { + throw new Error('Invalid public key passed!') + } + + return parameters; + } + /** * Adds a new item to the whitelist * @@ -11,6 +31,6 @@ export default class DeleteItemAction extends AbstractWhitelistAction { * @public */ public execute(): Promise { - return Promise.resolve(true) + return this.whitelist.deleteItem(this.parameters.message.publicKey); } } diff --git a/packages/govern-tx/src/whitelist/GetListAction.ts b/packages/govern-tx/src/whitelist/GetListAction.ts index 71144545b..debfc23eb 100644 --- a/packages/govern-tx/src/whitelist/GetListAction.ts +++ b/packages/govern-tx/src/whitelist/GetListAction.ts @@ -12,6 +12,6 @@ export default class GetListAction extends AbstractWhitelistAction { * @public */ public execute(): Promise { - return Promise.resolve([{publicKey: '0x0', rateLimit: 1}]) + return this.whitelist.getList() } } From ea624f40804b8036b76a82793f9c6b6013f13415 Mon Sep 17 00:00:00 2001 From: nivida Date: Tue, 17 Nov 2020 12:34:37 +0100 Subject: [PATCH 020/107] condition fixed --- packages/govern-tx/src/whitelist/AddItemAction.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/govern-tx/src/whitelist/AddItemAction.ts b/packages/govern-tx/src/whitelist/AddItemAction.ts index d8f320a33..cada52550 100644 --- a/packages/govern-tx/src/whitelist/AddItemAction.ts +++ b/packages/govern-tx/src/whitelist/AddItemAction.ts @@ -18,7 +18,7 @@ export default class AddItemAction extends AbstractWhitelistAction { throw new Error('Invalid public key passed!') } - if (!(this.parameters.message.rateLimit > 0)) { + if (this.parameters.message.rateLimit == 0) { throw new Error('Invalid rate limit passed!') } From d97a4ef62d069c5cd6462a4b0461e9c8b4d11643 Mon Sep 17 00:00:00 2001 From: nivida Date: Tue, 17 Nov 2020 14:07:57 +0100 Subject: [PATCH 021/107] types updated and Admin DB entity created --- .../lib/whitelist/AbstractWhitelistAction.ts | 2 +- packages/govern-tx/src/auth/Authenticator.ts | 2 +- packages/govern-tx/src/db/Admin.ts | 70 +++++++++++++++++++ packages/govern-tx/src/db/Whitelist.ts | 37 +++++++--- .../govern-tx/src/whitelist/AddItemAction.ts | 5 +- .../src/whitelist/DeleteItemAction.ts | 1 + .../govern-tx/src/whitelist/GetListAction.ts | 2 +- 7 files changed, 103 insertions(+), 16 deletions(-) create mode 100644 packages/govern-tx/src/db/Admin.ts diff --git a/packages/govern-tx/lib/whitelist/AbstractWhitelistAction.ts b/packages/govern-tx/lib/whitelist/AbstractWhitelistAction.ts index 3d751680a..a66f90990 100644 --- a/packages/govern-tx/lib/whitelist/AbstractWhitelistAction.ts +++ b/packages/govern-tx/lib/whitelist/AbstractWhitelistAction.ts @@ -4,7 +4,7 @@ import Whitelist, {ListItem} from '../../src/db/Whitelist' export default abstract class AbstractWhitelistAction extends AbstractAction { /** * @param {any} parameters - The given parameters by the user - * @param {Whitelist} whitelist - The whitelist DB adapter + * @param {Whitelist} whitelist - The whitelist entitiy * * @constructor */ diff --git a/packages/govern-tx/src/auth/Authenticator.ts b/packages/govern-tx/src/auth/Authenticator.ts index acfee26f3..93cb2cce9 100644 --- a/packages/govern-tx/src/auth/Authenticator.ts +++ b/packages/govern-tx/src/auth/Authenticator.ts @@ -52,7 +52,7 @@ export default class Authenticator { const publicKey: string = verifyMessage(arrayify(request.body.message), body.signature); let token: string; - if (await this.whitelist.getItemByKey(publicKey)) { + if (await this.whitelist.keyExists(publicKey)) { token = jwt.sign({data: publicKey}, this.secret, this.jwtOptions.sign) } diff --git a/packages/govern-tx/src/db/Admin.ts b/packages/govern-tx/src/db/Admin.ts new file mode 100644 index 000000000..fc4146b7f --- /dev/null +++ b/packages/govern-tx/src/db/Admin.ts @@ -0,0 +1,70 @@ +import Database from './Database' + +export interface AdminItem { + PublicKey: string +} + +export default class Admin { + /** + * @param {Database} db - The Database adapter + * + * @constructor + */ + constructor(private db: Database) {} + + /** + * Checks if a given publicKey does have admin rights + * + * @method isAdmin + * + * @param {string} publicKey + * + * @returns {boolean} + */ + public async isAdmin(publicKey: string): Promise { + return (await this.db.query(`SELECT * FROM admins WHERE PublicKey='${publicKey}'`)).length > 0 + } + + /** + * Adds a admin record + * + * @method addAdmin + * + * @param {string} publicKey + * + * @returns {AdminItem} + * + * @public + */ + public addAdmin(publicKey: string): Promise { + return this.db.query(`INSERT INTO admins VALUES (${publicKey})`) + } + + /** + * Deletes a admin record + * + * @method deleteAdmin + * + * @param {string} publicKey + * + * @returns {boolean} + * + * @public + */ + public async deleteAdmin(publicKey: string): Promise { + return (await this.db.query(`DELETE FROM admins WHERE PublicKey='${publicKey}'`)) > 0 + } + + /** + * Returns all admin records + * + * @method getAdmins + * + * @returns {AdminItem[]} + * + * @public + */ + public getAdmins(): Promise { + return this.db.query('SELECT * from admins') + } +} \ No newline at end of file diff --git a/packages/govern-tx/src/db/Whitelist.ts b/packages/govern-tx/src/db/Whitelist.ts index 1dbac6f69..e92bf78c3 100644 --- a/packages/govern-tx/src/db/Whitelist.ts +++ b/packages/govern-tx/src/db/Whitelist.ts @@ -3,9 +3,9 @@ import Database from './Database' // TODO: Define DB schema to handle global rate limits and to have Admin public key export interface ListItem { - publicKey: string, - rateLimit: number, - executedTransactions: number + PublicKey: string, + RateLimit: number, + ExecutedTransactions: number } export default class Whitelist { @@ -24,7 +24,22 @@ export default class Whitelist { * @returns {Promise} */ public getList(): Promise { - return this.db.query('SELECT * from whitelist') + return this.db.query('SELECT * FROM whitelist') + } + + /** + * Checks if a key is existing + * + * @method keyExists + * + * @param {string} publicKey - The public key to look for + * + * @returns {Promise { + return (await this.getItemByKey(publicKey)).length > 0; } /** @@ -34,12 +49,12 @@ export default class Whitelist { * * @param {string} publicKey - The public key to look for * - * @returns {Promise} + * @returns {Promise} * * @public */ - public getItemByKey(publicKey: string): Promise { - return this.db.query(`SELECT * from whitelist WHERE PublicKey='${publicKey}'`) + public getItemByKey(publicKey: string): Promise { + return this.db.query(`SELECT * FROM whitelist WHERE PublicKey='${publicKey}'`) } /** @@ -49,11 +64,11 @@ export default class Whitelist { * * @param {string} publicKey - The public key we would like to add * - * @returns {Promise} + * @returns {Promise} * * @public */ - public addItem(publicKey: string, rateLimit: string): Promise { + public addItem(publicKey: string, rateLimit: string): Promise { return this.db.query(`INSERT INTO whitelist VALUES (${publicKey}, ${rateLimit})`); } @@ -68,7 +83,7 @@ export default class Whitelist { * * @public */ - public deleteItem(publicKey: string): Promise { - return this.db.query(`DELETE FROM whitelist WHERE PublicKey='${publicKey}'`); + public async deleteItem(publicKey: string): Promise { + return (await this.db.query(`DELETE FROM whitelist WHERE PublicKey='${publicKey}'`)) > 0; } } diff --git a/packages/govern-tx/src/whitelist/AddItemAction.ts b/packages/govern-tx/src/whitelist/AddItemAction.ts index cada52550..ab4d87a0c 100644 --- a/packages/govern-tx/src/whitelist/AddItemAction.ts +++ b/packages/govern-tx/src/whitelist/AddItemAction.ts @@ -1,5 +1,6 @@ import {isAddress} from '@ethersproject/address' import AbstractWhitelistAction from "../../lib/whitelist/AbstractWhitelistAction" +import {ListItem} from '../db/Whitelist' export default class AddItemAction extends AbstractWhitelistAction { /** @@ -30,11 +31,11 @@ export default class AddItemAction extends AbstractWhitelistAction { * * @method execute * - * @returns {Promise} + * @returns {Promise} * * @public */ - public execute(): Promise { + public execute(): Promise { return this.whitelist.addItem( this.parameters.publicKey, this.parameters.rateLimit diff --git a/packages/govern-tx/src/whitelist/DeleteItemAction.ts b/packages/govern-tx/src/whitelist/DeleteItemAction.ts index edf91cfff..b1d1de3e0 100644 --- a/packages/govern-tx/src/whitelist/DeleteItemAction.ts +++ b/packages/govern-tx/src/whitelist/DeleteItemAction.ts @@ -1,5 +1,6 @@ import {isAddress} from '@ethersproject/address' import AbstractWhitelistAction from "../../lib/whitelist/AbstractWhitelistAction"; +import {ListItem} from '../db/Whitelist' export default class DeleteItemAction extends AbstractWhitelistAction { /** diff --git a/packages/govern-tx/src/whitelist/GetListAction.ts b/packages/govern-tx/src/whitelist/GetListAction.ts index debfc23eb..a4b1f5933 100644 --- a/packages/govern-tx/src/whitelist/GetListAction.ts +++ b/packages/govern-tx/src/whitelist/GetListAction.ts @@ -7,7 +7,7 @@ export default class GetListAction extends AbstractWhitelistAction { * * @method execute * - * @returns {Promise} + * @returns {Promise} * * @public */ From 09974c83912f1ded7924b836ad03a61499bb3c5d Mon Sep 17 00:00:00 2001 From: nivida Date: Tue, 17 Nov 2020 14:33:27 +0100 Subject: [PATCH 022/107] Authenticator updated --- packages/govern-tx/src/auth/Authenticator.ts | 22 +++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/packages/govern-tx/src/auth/Authenticator.ts b/packages/govern-tx/src/auth/Authenticator.ts index 93cb2cce9..800b016d7 100644 --- a/packages/govern-tx/src/auth/Authenticator.ts +++ b/packages/govern-tx/src/auth/Authenticator.ts @@ -1,4 +1,5 @@ import Whitelist, {ListItem} from '../db/Whitelist' +import Admin from '../db/Admin'; import jwt, {SignOptions, VerifyOptions} from 'jsonwebtoken' import {verifyMessage} from '@ethersproject/wallet'; import {arrayify} from '@ethersproject/bytes' @@ -11,7 +12,6 @@ export interface JWTOptions { verify: VerifyOptions } -// TODO: Implement rules for Admin public key resp. to execute whitelist actions export default class Authenticator { /** * @param {Whitelist} whitelist @@ -23,6 +23,7 @@ export default class Authenticator { constructor( private fastify: FastifyInstance, private whitelist: Whitelist, + private admin: Admin, private secret: string, private cookieName: string, private jwtOptions?: JWTOptions @@ -44,17 +45,24 @@ export default class Authenticator { */ public async authenticate(request: FastifyRequest, reply: FastifyReply): Promise { const cookie = request.cookies[this.cookieName]; - + const publicKey: string = verifyMessage(arrayify(request.body.message), body.signature); + if (cookie && this.verify(cookie)) { + if (request.routerPath === '/whitelist' && !(await this.admin.isAdmin(publicKey))) { + throw new Unauthorized('Not allowed action!') + } + return - } + } - const publicKey: string = verifyMessage(arrayify(request.body.message), body.signature); let token: string; - - if (await this.whitelist.keyExists(publicKey)) { + + if ( + (request.routerPath !== '/whitelist' && await this.whitelist.keyExists(publicKey)) || + (request.routerPath === '/whitelist' && await this.admin.isAdmin(publicKey)) + ) { token = jwt.sign({data: publicKey}, this.secret, this.jwtOptions.sign) - } + } if (!token) { throw new Unauthorized('Unknown account!') From a1e8a992f7eeacc8856bb21cd138aacc2128450b Mon Sep 17 00:00:00 2001 From: nivida Date: Tue, 17 Nov 2020 14:36:35 +0100 Subject: [PATCH 023/107] initiation of Authenticator in Bootstrap updated for admin role handling and types adjusted --- packages/govern-tx/src/Bootstrap.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/govern-tx/src/Bootstrap.ts b/packages/govern-tx/src/Bootstrap.ts index 2657f8e17..c23a991dc 100644 --- a/packages/govern-tx/src/Bootstrap.ts +++ b/packages/govern-tx/src/Bootstrap.ts @@ -1,8 +1,9 @@ import fastify, { FastifyInstance } from 'fastify' import Configuration from './config/Configuration' import Database from './db/Database' -import Whitelist from './db/Whitelist' -import Authenticator, { JWTOptions } from './auth/Authenticator'; +import Whitelist, { ListItem } from './db/Whitelist' +import Admin from './db/Admin' +import Authenticator, { JWTOptions } from './auth/Authenticator' import ExecuteTransaction from './transactions/ExecuteTransaction' import ChallengeTransaction from './transactions/ChallengeTransaction' @@ -12,7 +13,7 @@ import AddItemAction from './whitelist/AddItemAction' import DeleteItemAction from './whitelist/DeleteItemAction' import GetListAction from './whitelist/GetListAction' import AbstractTransaction from '../lib/transactions/AbstractTransaction' -import AbstractWhitelistAction from '../lib/whitelist/AbstractWhitelistAction'; +import AbstractWhitelistAction from '../lib/whitelist/AbstractWhitelistAction' export default class Bootstrap { /** @@ -124,7 +125,7 @@ export default class Bootstrap { this.server.post( '/whitelist', {schema: AbstractWhitelistAction.schema}, - (request): Promise => { + (request): Promise => { return new AddItemAction(this.whitelist, request.params).execute() } ) @@ -175,11 +176,15 @@ export default class Bootstrap { * @private */ private setupAuth(): void { - this.whitelist = new Whitelist(new Database(this.config.database)); + const database = new Database(this.config.database) + const admin = new Admin(database); + this.whitelist = new Whitelist(database) + this.authenticator = new Authenticator( this.server, this.whitelist, + admin, this.config.auth.secret, this.config.auth.cookieName, this.config.auth.jwtOptions From 03ce0a96c6fa326ffc8e06dd9d019d2ac498c6dd Mon Sep 17 00:00:00 2001 From: nivida Date: Tue, 17 Nov 2020 14:43:42 +0100 Subject: [PATCH 024/107] typo fixed and funcDoc improved in Authenticator --- packages/govern-tx/src/auth/Authenticator.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/govern-tx/src/auth/Authenticator.ts b/packages/govern-tx/src/auth/Authenticator.ts index 800b016d7..e69d219ee 100644 --- a/packages/govern-tx/src/auth/Authenticator.ts +++ b/packages/govern-tx/src/auth/Authenticator.ts @@ -32,20 +32,20 @@ export default class Authenticator { } /** - * Checks if the given public key is existing in the whitelist and if no rate limit is exceeded + * Checks if the given public key is existing and if this account is allowed to execute the requested action * * @method authenticate * * @param {string} message - The message from the user * @param {string} signature - The sent signature from the user * - * @returns Promise - Returns false or the JWT + * @returns Promise * * @public */ public async authenticate(request: FastifyRequest, reply: FastifyReply): Promise { const cookie = request.cookies[this.cookieName]; - const publicKey: string = verifyMessage(arrayify(request.body.message), body.signature); + const publicKey: string = verifyMessage(arrayify(request.body.message), request.body.signature); if (cookie && this.verify(cookie)) { if (request.routerPath === '/whitelist' && !(await this.admin.isAdmin(publicKey))) { From 8e65ab07f23891d86429d8bfb93d7c45663438b5 Mon Sep 17 00:00:00 2001 From: nivida Date: Tue, 17 Nov 2020 14:50:57 +0100 Subject: [PATCH 025/107] Authenticator cleaned up --- packages/govern-tx/src/auth/Authenticator.ts | 40 ++++++++++++++++---- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/packages/govern-tx/src/auth/Authenticator.ts b/packages/govern-tx/src/auth/Authenticator.ts index e69d219ee..257e2b970 100644 --- a/packages/govern-tx/src/auth/Authenticator.ts +++ b/packages/govern-tx/src/auth/Authenticator.ts @@ -1,11 +1,11 @@ -import Whitelist, {ListItem} from '../db/Whitelist' -import Admin from '../db/Admin'; +import fastify, { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; import jwt, {SignOptions, VerifyOptions} from 'jsonwebtoken' import {verifyMessage} from '@ethersproject/wallet'; import {arrayify} from '@ethersproject/bytes' import { Unauthorized } from 'http-errors' import fastifyCookie from 'fastify-cookie' -import fastify, { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; +import Whitelist from '../db/Whitelist' +import Admin from '../db/Admin'; export interface JWTOptions { sign: SignOptions, @@ -48,7 +48,7 @@ export default class Authenticator { const publicKey: string = verifyMessage(arrayify(request.body.message), request.body.signature); if (cookie && this.verify(cookie)) { - if (request.routerPath === '/whitelist' && !(await this.admin.isAdmin(publicKey))) { + if (!(await this.hasPermission(request.routerPath, publicKey))) { throw new Unauthorized('Not allowed action!') } @@ -57,10 +57,7 @@ export default class Authenticator { let token: string; - if ( - (request.routerPath !== '/whitelist' && await this.whitelist.keyExists(publicKey)) || - (request.routerPath === '/whitelist' && await this.admin.isAdmin(publicKey)) - ) { + if (await this.hasPermission(request.routerPath, publicKey)) { token = jwt.sign({data: publicKey}, this.secret, this.jwtOptions.sign) } @@ -72,6 +69,33 @@ export default class Authenticator { return } + /** + * Checks if the current requesting user has permissions + * + * @method hasPermission + * + * @param {string} routerPath + * @param {string} publicKey + * + * @returns {Promise} + * + * @private + */ + private async hasPermission(routerPath, publicKey): Promise { + if ( + routerPath !== '/whitelist' && + (await this.whitelist.keyExists(publicKey) || await this.admin.isAdmin(publicKey)) + ) { + return true + } + + if (routerPath === '/whitelist' && this.admin.isAdmin(publicKey)) { + return true + } + + return false + } + /** * Verifiey the given JWT * From b186491bc4d7d8a65667bd6d5fd102284d6a8c39 Mon Sep 17 00:00:00 2001 From: nivida Date: Tue, 17 Nov 2020 14:54:21 +0100 Subject: [PATCH 026/107] codestyle++ --- packages/govern-tx/src/auth/Authenticator.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/govern-tx/src/auth/Authenticator.ts b/packages/govern-tx/src/auth/Authenticator.ts index 257e2b970..50d92e88b 100644 --- a/packages/govern-tx/src/auth/Authenticator.ts +++ b/packages/govern-tx/src/auth/Authenticator.ts @@ -2,7 +2,7 @@ import fastify, { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify' import jwt, {SignOptions, VerifyOptions} from 'jsonwebtoken' import {verifyMessage} from '@ethersproject/wallet'; import {arrayify} from '@ethersproject/bytes' -import { Unauthorized } from 'http-errors' +import { Unauthorized, HttpError } from 'http-errors' import fastifyCookie from 'fastify-cookie' import Whitelist from '../db/Whitelist' import Admin from '../db/Admin'; @@ -13,6 +13,8 @@ export interface JWTOptions { } export default class Authenticator { + private NOT_ALLOWED: HttpError = new Unauthorized('Not allowed action!') + /** * @param {Whitelist} whitelist * @param {string} secret @@ -49,7 +51,7 @@ export default class Authenticator { if (cookie && this.verify(cookie)) { if (!(await this.hasPermission(request.routerPath, publicKey))) { - throw new Unauthorized('Not allowed action!') + throw this.NOT_ALLOWED } return @@ -62,7 +64,7 @@ export default class Authenticator { } if (!token) { - throw new Unauthorized('Unknown account!') + throw this.NOT_ALLOWED } reply.setCookie(this.cookieName, token, {secure: true}) @@ -81,7 +83,7 @@ export default class Authenticator { * * @private */ - private async hasPermission(routerPath, publicKey): Promise { + private async hasPermission(routerPath: string, publicKey: string): Promise { if ( routerPath !== '/whitelist' && (await this.whitelist.keyExists(publicKey) || await this.admin.isAdmin(publicKey)) From 1fde9238102a4188e6cce3b150285601b06b15b7 Mon Sep 17 00:00:00 2001 From: nivida Date: Tue, 17 Nov 2020 14:57:54 +0100 Subject: [PATCH 027/107] conditions improved in Authenticator --- packages/govern-tx/src/auth/Authenticator.ts | 25 +++++++++++++------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/packages/govern-tx/src/auth/Authenticator.ts b/packages/govern-tx/src/auth/Authenticator.ts index 50d92e88b..6ffd6a796 100644 --- a/packages/govern-tx/src/auth/Authenticator.ts +++ b/packages/govern-tx/src/auth/Authenticator.ts @@ -13,6 +13,11 @@ export interface JWTOptions { } export default class Authenticator { + /** + * @property {HttpError} + * + * @private + */ private NOT_ALLOWED: HttpError = new Unauthorized('Not allowed action!') /** @@ -57,18 +62,22 @@ export default class Authenticator { return } - let token: string; if (await this.hasPermission(request.routerPath, publicKey)) { - token = jwt.sign({data: publicKey}, this.secret, this.jwtOptions.sign) - } + reply.setCookie( + this.cookieName, + jwt.sign( + {data: publicKey}, + this.secret, + this.jwtOptions.sign + ), + {secure: true} + ) - if (!token) { - throw this.NOT_ALLOWED - } + return + } - reply.setCookie(this.cookieName, token, {secure: true}) - return + throw this.NOT_ALLOWED } /** From f2eef73f9c01060191e607f9d142e60ac3a0bb56 Mon Sep 17 00:00:00 2001 From: nivida Date: Mon, 23 Nov 2020 10:56:01 +0100 Subject: [PATCH 028/107] codestyle++, request handling updated --- packages/govern-tx/lib/AbstractAction.ts | 25 +++++++----- .../lib/transactions/AbstractTransaction.ts | 39 +++++++++++++++---- .../lib/transactions/ContractFunction.ts | 0 .../lib/whitelist/AbstractWhitelistAction.ts | 15 +++++-- packages/govern-tx/src/auth/Authenticator.ts | 1 - .../govern-tx/src/whitelist/AddItemAction.ts | 22 +++++------ .../src/whitelist/DeleteItemAction.ts | 18 ++++----- 7 files changed, 77 insertions(+), 43 deletions(-) create mode 100644 packages/govern-tx/lib/transactions/ContractFunction.ts diff --git a/packages/govern-tx/lib/AbstractAction.ts b/packages/govern-tx/lib/AbstractAction.ts index c7c1b2187..17e26cb74 100644 --- a/packages/govern-tx/lib/AbstractAction.ts +++ b/packages/govern-tx/lib/AbstractAction.ts @@ -1,33 +1,38 @@ +export interface Request { + message: string | any, + signature: string +} + export default abstract class AbstractAction { /** * The parameters used to create the transaction * - * @var {Object} parameters + * @var {Request} parameters */ - protected parameters: any; + protected request: Request; /** - * @param {Object} parameters + * @param {Request} parameters * * @constructor */ - constructor(parameters: any) { - this.parameters = this.validateParameters(parameters); + constructor(request: Request) { + this.request = this.validateRequest(request); } /** - * Validates the given parameters. + * Validates the given request body. * - * @method validateParameters + * @method validateRequest * - * @param {Object} parameters + * @param {Request} request * * @returns {Object} * * @protected */ - protected validateParameters(parameters: any): any { - return parameters; + protected validateRequest(request: Request): any { + return request; } /** diff --git a/packages/govern-tx/lib/transactions/AbstractTransaction.ts b/packages/govern-tx/lib/transactions/AbstractTransaction.ts index 2698c0b59..b64176fc7 100644 --- a/packages/govern-tx/lib/transactions/AbstractTransaction.ts +++ b/packages/govern-tx/lib/transactions/AbstractTransaction.ts @@ -1,27 +1,48 @@ -import AbstractAction from '../AbstractAction' +import AbstractAction, { Request } from '../AbstractAction' +import ContractFunction from '../transactions/ContractFunction' import Configuration from '../../src/config/Configuration' +import { hexDataSlice } from 'ethers/lib/utils'; // TODO: Use type from ethers export interface TransactionReceipt {} +export interface TransactionRequest extends Request { + function: ContractFunction +} + export default abstract class AbstractTransaction extends AbstractAction { /** - * The function signature used to create a transaction + * The function identifier used to create a transaction * - * @var {string} signature + * @var {string} functionABI * * @protected */ - protected signature: string; + protected functionABI: any; /** - * @param {Object} parameters - The given parameters by the user + * @param {Request} request - The request body given by the user * @param {Configuration} configuration - The configuration object to execute the transaction * * @constructor */ - constructor(private configuration: Configuration, parameters: any) { - super(parameters); + constructor(private configuration: Configuration, request: TransactionRequest) { + super(request); + } + + /** + * Validates the given request body. + * + * @method validateRequest + * + * @param {TransactionRequest} request - The request body given by the user + * + * @public + */ + public validateRequest(request: TransactionRequest) { + request.function = new ContractFunction(this.functionABI, request.message) + + return request; } /** @@ -33,7 +54,9 @@ export default abstract class AbstractTransaction extends AbstractAction { * * @public */ - public abstract execute(): Promise + public execute(): Promise { + + } /** * TODO: Define response validation diff --git a/packages/govern-tx/lib/transactions/ContractFunction.ts b/packages/govern-tx/lib/transactions/ContractFunction.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/govern-tx/lib/whitelist/AbstractWhitelistAction.ts b/packages/govern-tx/lib/whitelist/AbstractWhitelistAction.ts index a66f90990..17c032e46 100644 --- a/packages/govern-tx/lib/whitelist/AbstractWhitelistAction.ts +++ b/packages/govern-tx/lib/whitelist/AbstractWhitelistAction.ts @@ -1,15 +1,22 @@ -import AbstractAction from '../AbstractAction' +import AbstractAction, { Request } from '../AbstractAction' import Whitelist, {ListItem} from '../../src/db/Whitelist' +export interface WhitelistRequest extends Request { + message: { + publicKey: string, + rateLimit: number + } +} + export default abstract class AbstractWhitelistAction extends AbstractAction { /** - * @param {any} parameters - The given parameters by the user + * @param {WhitelistRequest} request - The given request body by the user * @param {Whitelist} whitelist - The whitelist entitiy * * @constructor */ - constructor(protected whitelist: Whitelist, parameters: any = {}) { - super(parameters) + constructor(protected whitelist: Whitelist, request: WhitelistRequest) { + super(request) } /** diff --git a/packages/govern-tx/src/auth/Authenticator.ts b/packages/govern-tx/src/auth/Authenticator.ts index 6ffd6a796..790c6b5a1 100644 --- a/packages/govern-tx/src/auth/Authenticator.ts +++ b/packages/govern-tx/src/auth/Authenticator.ts @@ -62,7 +62,6 @@ export default class Authenticator { return } - if (await this.hasPermission(request.routerPath, publicKey)) { reply.setCookie( this.cookieName, diff --git a/packages/govern-tx/src/whitelist/AddItemAction.ts b/packages/govern-tx/src/whitelist/AddItemAction.ts index ab4d87a0c..0f977d955 100644 --- a/packages/govern-tx/src/whitelist/AddItemAction.ts +++ b/packages/govern-tx/src/whitelist/AddItemAction.ts @@ -1,29 +1,29 @@ import {isAddress} from '@ethersproject/address' -import AbstractWhitelistAction from "../../lib/whitelist/AbstractWhitelistAction" +import AbstractWhitelistAction, { WhitelistRequest } from "../../lib/whitelist/AbstractWhitelistAction" import {ListItem} from '../db/Whitelist' export default class AddItemAction extends AbstractWhitelistAction { /** - * Validates the given parameters. + * Validates the given request body. * - * @method validateParameters + * @method validateRequest * - * @param {Object} parameters + * @param {WhitelistRequest} request * - * @returns {Object} + * @returns {WhitelistRequest} * * @protected */ - protected validateParameters(parameters: any): any { - if (!isAddress(this.parameters.message.publicKey)) { + protected validateRequest(request: WhitelistRequest): WhitelistRequest { + if (!isAddress(request.message.publicKey)) { throw new Error('Invalid public key passed!') } - if (this.parameters.message.rateLimit == 0) { + if (request.message.rateLimit == 0) { throw new Error('Invalid rate limit passed!') } - return parameters; + return request; } /** @@ -37,8 +37,8 @@ export default class AddItemAction extends AbstractWhitelistAction { */ public execute(): Promise { return this.whitelist.addItem( - this.parameters.publicKey, - this.parameters.rateLimit + this.request.message.publicKey, + this.request.message.rateLimit ) } } diff --git a/packages/govern-tx/src/whitelist/DeleteItemAction.ts b/packages/govern-tx/src/whitelist/DeleteItemAction.ts index b1d1de3e0..b3b412a9c 100644 --- a/packages/govern-tx/src/whitelist/DeleteItemAction.ts +++ b/packages/govern-tx/src/whitelist/DeleteItemAction.ts @@ -1,25 +1,25 @@ import {isAddress} from '@ethersproject/address' -import AbstractWhitelistAction from "../../lib/whitelist/AbstractWhitelistAction"; +import AbstractWhitelistAction, { WhitelistRequest } from "../../lib/whitelist/AbstractWhitelistAction"; import {ListItem} from '../db/Whitelist' export default class DeleteItemAction extends AbstractWhitelistAction { /** - * Validates the given parameters. + * Validates the given request body. * - * @method validateParameters + * @method validateRequest * - * @param {Object} parameters + * @param {WhitelistRequest} request * - * @returns {Object} + * @returns {WhitelistRequest} * * @protected */ - protected validateParameters(parameters: any): any { - if (!isAddress(this.parameters.message.publicKey)) { + protected validateRequest(request: WhitelistRequest): WhitelistRequest { + if (!isAddress(request.message.publicKey)) { throw new Error('Invalid public key passed!') } - return parameters; + return request; } /** @@ -32,6 +32,6 @@ export default class DeleteItemAction extends AbstractWhitelistAction { * @public */ public execute(): Promise { - return this.whitelist.deleteItem(this.parameters.message.publicKey); + return this.whitelist.deleteItem(this.request.message.publicKey); } } From ba8dfff8701155706eff76be698d84d14c16ae26 Mon Sep 17 00:00:00 2001 From: nivida Date: Mon, 23 Nov 2020 11:11:43 +0100 Subject: [PATCH 029/107] wallet draft implementation --- .../lib/transactions/AbstractTransaction.ts | 2 +- packages/govern-tx/src/wallet/Wallet.ts | 53 +++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 packages/govern-tx/src/wallet/Wallet.ts diff --git a/packages/govern-tx/lib/transactions/AbstractTransaction.ts b/packages/govern-tx/lib/transactions/AbstractTransaction.ts index b64176fc7..f0a4ef48a 100644 --- a/packages/govern-tx/lib/transactions/AbstractTransaction.ts +++ b/packages/govern-tx/lib/transactions/AbstractTransaction.ts @@ -14,7 +14,7 @@ export default abstract class AbstractTransaction extends AbstractAction { /** * The function identifier used to create a transaction * - * @var {string} functionABI + * @property {string} functionABI * * @protected */ diff --git a/packages/govern-tx/src/wallet/Wallet.ts b/packages/govern-tx/src/wallet/Wallet.ts new file mode 100644 index 000000000..99692f9b5 --- /dev/null +++ b/packages/govern-tx/src/wallet/Wallet.ts @@ -0,0 +1,53 @@ +import {Wallet as EthersWallet} from '@ethersproject/wallet' +import Database from '../db/Database' +import ContractFunction from '../../lib/transactions/ContractFunction' + +export default class Wallet { + /** + * The used wallet object to sign a transaction + * + * @property {EthersWallet} wallet + * + * @private + */ + private wallet: EthersWallet + + /** + * @param {Database} db - Database adapter + * + * @constructor + */ + constructor(private db: Database) {} + + /** + * Signs the given contract function with the Aragon privateKey + * + * @method sign + * + * @param {ContractFunction} contractFunction + * + * @returns {Promise} + * + * @public + */ + public async sign(contractFunction: ContractFunction): Promise { + return (await this.getWallet()).signTransaction(contractFunction.toTransaction()) + } + + /** + * Returns the ethers wallet instance prepopulated with the Aragon privateKey + * + * @method getWallet + * + * @returns {EthersWallet} + * + * @private + */ + private async getWallet(): Promise { + if (!this.wallet) { + this.wallet = new EthersWallet(await this.db.query("SELECT PrivateKey FROM wallet WHERE id='0'")) + } + + return this.wallet + } +} \ No newline at end of file From f4983da758fe164f910792d81b214c198111024d Mon Sep 17 00:00:00 2001 From: nivida Date: Mon, 23 Nov 2020 11:57:48 +0100 Subject: [PATCH 030/107] Draft provider added, actions updated, and server initiation in Bootstrap updated --- .../lib/transactions/AbstractTransaction.ts | 48 ++++++--------- .../lib/whitelist/AbstractWhitelistAction.ts | 2 +- packages/govern-tx/package.json | 7 ++- packages/govern-tx/src/Bootstrap.ts | 61 ++++++++++++++++--- .../govern-tx/src/config/Configuration.ts | 1 + packages/govern-tx/src/index.ts | 3 +- packages/govern-tx/src/provider/Provider.ts | 45 ++++++++++++++ .../src/transactions/ChallengeTransaction.ts | 6 +- .../src/transactions/ExecuteTransaction.ts | 6 +- .../src/transactions/ScheduleTransaction.ts | 6 +- 10 files changed, 128 insertions(+), 57 deletions(-) create mode 100644 packages/govern-tx/src/provider/Provider.ts diff --git a/packages/govern-tx/lib/transactions/AbstractTransaction.ts b/packages/govern-tx/lib/transactions/AbstractTransaction.ts index f0a4ef48a..e8ce0aedb 100644 --- a/packages/govern-tx/lib/transactions/AbstractTransaction.ts +++ b/packages/govern-tx/lib/transactions/AbstractTransaction.ts @@ -1,48 +1,38 @@ +import { TransactionReceipt } from '@ethersproject/abstract-provider' import AbstractAction, { Request } from '../AbstractAction' import ContractFunction from '../transactions/ContractFunction' -import Configuration from '../../src/config/Configuration' -import { hexDataSlice } from 'ethers/lib/utils'; - -// TODO: Use type from ethers -export interface TransactionReceipt {} - -export interface TransactionRequest extends Request { - function: ContractFunction -} +import Provider from '../../src/provider/Provider' export default abstract class AbstractTransaction extends AbstractAction { /** - * The function identifier used to create a transaction + * The function ABI used to create a transaction * - * @property {string} functionABI + * @property {Object} functionABI * * @protected */ - protected functionABI: any; + protected functionABI: any /** - * @param {Request} request - The request body given by the user - * @param {Configuration} configuration - The configuration object to execute the transaction + * The contract function * - * @constructor + * @property {ContractFunction} contractFunction + * + * @private */ - constructor(private configuration: Configuration, request: TransactionRequest) { - super(request); - } + private contractFunction: ContractFunction /** - * Validates the given request body. - * - * @method validateRequest - * - * @param {TransactionRequest} request - The request body given by the user + * @param {Request} request - The request body given by the user * - * @public + * @constructor */ - public validateRequest(request: TransactionRequest) { - request.function = new ContractFunction(this.functionABI, request.message) - - return request; + constructor( + private provider: Provider, + request: Request + ) { + super(request); + this.contractFunction = new ContractFunction(this.functionABI, request.message) } /** @@ -55,7 +45,7 @@ export default abstract class AbstractTransaction extends AbstractAction { * @public */ public execute(): Promise { - + return this.provider.sendTransaction(this.contractFunction) } /** diff --git a/packages/govern-tx/lib/whitelist/AbstractWhitelistAction.ts b/packages/govern-tx/lib/whitelist/AbstractWhitelistAction.ts index 17c032e46..232122aa2 100644 --- a/packages/govern-tx/lib/whitelist/AbstractWhitelistAction.ts +++ b/packages/govern-tx/lib/whitelist/AbstractWhitelistAction.ts @@ -15,7 +15,7 @@ export default abstract class AbstractWhitelistAction extends AbstractAction { * * @constructor */ - constructor(protected whitelist: Whitelist, request: WhitelistRequest) { + constructor(protected whitelist: Whitelist, request?: WhitelistRequest) { super(request) } diff --git a/packages/govern-tx/package.json b/packages/govern-tx/package.json index 8f2ae9770..d7f4c3127 100644 --- a/packages/govern-tx/package.json +++ b/packages/govern-tx/package.json @@ -23,9 +23,10 @@ "typescript": "^4.0.5" }, "dependencies": { - "@ethersproject/address": "^5.0.5", - "@ethersproject/bytes": "^5.0.5", - "@ethersproject/wallet": "^5.0.7", + "@ethersproject/address": "^5.0.7", + "@ethersproject/bytes": "^5.0.6", + "@ethersproject/wallet": "^5.0.8", + "@ethersproject/providers": "^5.0.15", "fastify": "^3.8.0", "fastify-cookie": "^4.1.0", "fastify-jwt": "^2.1.3", diff --git a/packages/govern-tx/src/Bootstrap.ts b/packages/govern-tx/src/Bootstrap.ts index c23a991dc..7c53d0d24 100644 --- a/packages/govern-tx/src/Bootstrap.ts +++ b/packages/govern-tx/src/Bootstrap.ts @@ -1,5 +1,9 @@ import fastify, { FastifyInstance } from 'fastify' +import { TransactionReceipt } from '@ethersproject/abstract-provider' +import { Request } from '../lib/AbstractAction' import Configuration from './config/Configuration' +import Provider from './provider/Provider' +import Wallet from './wallet/Wallet' import Database from './db/Database' import Whitelist, { ListItem } from './db/Whitelist' import Admin from './db/Admin' @@ -37,6 +41,20 @@ export default class Bootstrap { */ private whitelist: Whitelist + /** + * @property {Provider} provider + * + * @private + */ + private provider: Provider + + /** + * @property {Database} database + * + * @private + */ + private database: Database + /** * @param {Configuration} config * @@ -44,6 +62,8 @@ export default class Bootstrap { */ constructor(private config: Configuration) { this.setServer() + this.setDatabase() + this.setProvider() this.setupAuth() this.registerTransactionRoutes() this.registerWhitelistRoutes() @@ -89,7 +109,7 @@ export default class Bootstrap { '/execute', {schema: AbstractTransaction.schema}, (request): Promise => { - return new ExecuteTransaction(this.config, request.params).execute() + return new ExecuteTransaction(this.provider, request.params as Request).execute() } ) @@ -97,7 +117,7 @@ export default class Bootstrap { '/schedule', {schema: AbstractTransaction.schema}, (request): Promise => { - return new ScheduleTransaction(this.config, request.params).execute() + return new ScheduleTransaction(this.provider, request.params as Request).execute() } ) @@ -105,7 +125,7 @@ export default class Bootstrap { '/challenge', {schema: AbstractTransaction.schema}, (request): Promise => { - return new ChallengeTransaction(this.config, request.params).execute() + return new ChallengeTransaction(this.provider, request.params as Request).execute() } ) } @@ -126,7 +146,7 @@ export default class Bootstrap { '/whitelist', {schema: AbstractWhitelistAction.schema}, (request): Promise => { - return new AddItemAction(this.whitelist, request.params).execute() + return new AddItemAction(this.whitelist, request.params as Request).execute() } ) @@ -134,7 +154,7 @@ export default class Bootstrap { '/whitelist', {schema: AbstractWhitelistAction.schema}, (request): Promise => { - return new DeleteItemAction(this.whitelist, request.params).execute() + return new DeleteItemAction(this.whitelist, request.params as Request).execute() } ) @@ -166,6 +186,32 @@ export default class Bootstrap { }) } + /** + * Initiates the database instance + * + * @method setProvider + * + * @returns {void} + * + * @private + */ + private setDatabase(): void { + this.database = new Database(this.config.database) + } + + /** + * Initiates the provider instance + * + * @method setProvider + * + * @returns {void} + * + * @private + */ + private setProvider(): void { + this.provider = new Provider(this.config, new Wallet(this.database)) + } + /** * Registers the authentication handler * @@ -176,9 +222,8 @@ export default class Bootstrap { * @private */ private setupAuth(): void { - const database = new Database(this.config.database) - const admin = new Admin(database); - this.whitelist = new Whitelist(database) + const admin = new Admin(this.database); + this.whitelist = new Whitelist(this.database) this.authenticator = new Authenticator( diff --git a/packages/govern-tx/src/config/Configuration.ts b/packages/govern-tx/src/config/Configuration.ts index 9ca6f612d..e3b0f580b 100644 --- a/packages/govern-tx/src/config/Configuration.ts +++ b/packages/govern-tx/src/config/Configuration.ts @@ -2,6 +2,7 @@ import { JWTOptions } from '../auth/Authenticator'; export interface EthereumOptions { url: string + blockConfirmations: number } export interface DatabaseOptions { diff --git a/packages/govern-tx/src/index.ts b/packages/govern-tx/src/index.ts index 017d15eae..4afc06ad6 100644 --- a/packages/govern-tx/src/index.ts +++ b/packages/govern-tx/src/index.ts @@ -4,7 +4,8 @@ import Bootstrap from './Bootstrap' new Bootstrap( new Configuration( { - url: 'localhost:8545' + url: 'localhost:8545', + blockConfirmations: 42 }, { user: 'govern', diff --git a/packages/govern-tx/src/provider/Provider.ts b/packages/govern-tx/src/provider/Provider.ts new file mode 100644 index 000000000..06729b610 --- /dev/null +++ b/packages/govern-tx/src/provider/Provider.ts @@ -0,0 +1,45 @@ +import Wallet from '../wallet/Wallet' +import { BaseProvider } from '@ethersproject/providers' +import { TransactionReceipt } from '@ethersproject/abstract-provider' +import ContractFunction from '../lib/transactions/ContractFunction' +import Configuration from '../config/Configuration' + +export default class Provider { + /** + * The base provider of ethers.js + * + * @property {BaseProvider} provider + * + * @private + */ + private provider: BaseProvider + + /** + * @param {Configuration} configuration + * @param {Wallet} wallet + * + * @constructor + */ + constructor(private configuration: Configuration, private wallet: Wallet) { + this.provider = new BaseProvider(this.configuration.ethereum.url) + } + + /** + * Sends the transaction and returns the receipt after the configured block confirmations are reached + * + * @method sendTransaction + * + * @param {ContractFunction} contractFunction + * + * @returns {Promise} + */ + public async sendTransaction(contractFunction: ContractFunction): Promise { + return ( + await this.provider.sendTransaction( + await this.wallet.sign(contractFunction) + ) + ).wait( + this.configuration.ethereum.blockConfirmations + ) + } +} \ No newline at end of file diff --git a/packages/govern-tx/src/transactions/ChallengeTransaction.ts b/packages/govern-tx/src/transactions/ChallengeTransaction.ts index 6c70e362f..8e66d9abd 100644 --- a/packages/govern-tx/src/transactions/ChallengeTransaction.ts +++ b/packages/govern-tx/src/transactions/ChallengeTransaction.ts @@ -1,9 +1,5 @@ import AbstractTransaction from '../../lib/transactions/AbstractTransaction'; export default class ChallengeTransaction extends AbstractTransaction { - protected signature: string = 'challenge(...)'; - - public execute(): Promise { - - } + protected functionABI: any = {} } diff --git a/packages/govern-tx/src/transactions/ExecuteTransaction.ts b/packages/govern-tx/src/transactions/ExecuteTransaction.ts index 1bae8566a..25dde4ca3 100644 --- a/packages/govern-tx/src/transactions/ExecuteTransaction.ts +++ b/packages/govern-tx/src/transactions/ExecuteTransaction.ts @@ -1,9 +1,5 @@ import AbstractTransaction from '../../lib/transactions/AbstractTransaction'; export default class ExecuteTransaction extends AbstractTransaction { - protected signature: string = 'execute(...)'; - - public execute(): Promise { - - } + protected functionABI: any = {} } diff --git a/packages/govern-tx/src/transactions/ScheduleTransaction.ts b/packages/govern-tx/src/transactions/ScheduleTransaction.ts index aff10e9aa..1eb5e70ca 100644 --- a/packages/govern-tx/src/transactions/ScheduleTransaction.ts +++ b/packages/govern-tx/src/transactions/ScheduleTransaction.ts @@ -1,9 +1,5 @@ import AbstractTransaction from '../../lib/transactions/AbstractTransaction'; export default class ScheduleTransaction extends AbstractTransaction { - protected signature: string = 'schedule(...)'; - - public execute(): Promise { - - } + protected functionABI: any = {} } From 0df7f8ba43f7f222d7bd742c7b4b6c6c6deaeed9 Mon Sep 17 00:00:00 2001 From: nivida Date: Mon, 23 Nov 2020 12:36:19 +0100 Subject: [PATCH 031/107] codestyle and types improved --- packages/govern-tx/lib/AbstractAction.ts | 17 +++++++++-------- .../lib/transactions/AbstractTransaction.ts | 12 +++++++----- .../lib/whitelist/AbstractWhitelistAction.ts | 11 ++++++----- packages/govern-tx/src/Bootstrap.ts | 2 +- packages/govern-tx/src/auth/Authenticator.ts | 17 ++++++++++------- packages/govern-tx/src/config/Configuration.ts | 2 ++ packages/govern-tx/src/db/Admin.ts | 2 +- packages/govern-tx/src/db/Whitelist.ts | 1 + packages/govern-tx/src/provider/Provider.ts | 2 +- packages/govern-tx/src/wallet/Wallet.ts | 2 +- 10 files changed, 39 insertions(+), 29 deletions(-) diff --git a/packages/govern-tx/lib/AbstractAction.ts b/packages/govern-tx/lib/AbstractAction.ts index 17e26cb74..930eb9147 100644 --- a/packages/govern-tx/lib/AbstractAction.ts +++ b/packages/govern-tx/lib/AbstractAction.ts @@ -1,3 +1,4 @@ +import { FastifySchema } from 'fastify' export interface Request { message: string | any, signature: string @@ -5,7 +6,7 @@ export interface Request { export default abstract class AbstractAction { /** - * The parameters used to create the transaction + * The given request by the user * * @var {Request} parameters */ @@ -18,7 +19,7 @@ export default abstract class AbstractAction { */ constructor(request: Request) { this.request = this.validateRequest(request); - } + } /** * Validates the given request body. @@ -27,11 +28,11 @@ export default abstract class AbstractAction { * * @param {Request} request * - * @returns {Object} + * @returns {Request} * * @protected */ - protected validateRequest(request: Request): any { + protected validateRequest(request: Request): Request { return request; } @@ -47,13 +48,13 @@ export default abstract class AbstractAction { public abstract execute(): Promise /** - * Returns the schema of a whitelist command + * Returns the required request schema * * @property schema * - * @returns {any} + * @returns {FastifySchema} */ - public static get schema(): any { + public static get schema(): FastifySchema { return { body: { type: 'object', @@ -63,6 +64,6 @@ export default abstract class AbstractAction { signature: { type: 'string' } } } - } + } as FastifySchema } } diff --git a/packages/govern-tx/lib/transactions/AbstractTransaction.ts b/packages/govern-tx/lib/transactions/AbstractTransaction.ts index e8ce0aedb..a87bd1e12 100644 --- a/packages/govern-tx/lib/transactions/AbstractTransaction.ts +++ b/packages/govern-tx/lib/transactions/AbstractTransaction.ts @@ -1,3 +1,4 @@ +import { FastifySchema } from 'fastify'; import { TransactionReceipt } from '@ethersproject/abstract-provider' import AbstractAction, { Request } from '../AbstractAction' import ContractFunction from '../transactions/ContractFunction' @@ -11,7 +12,7 @@ export default abstract class AbstractTransaction extends AbstractAction { * * @protected */ - protected functionABI: any + protected functionABI: any // TODO: Create functionABI object interface definition /** * The contract function @@ -23,6 +24,7 @@ export default abstract class AbstractTransaction extends AbstractAction { private contractFunction: ContractFunction /** + * @param {Provider} provider - The Ethereum provider object * @param {Request} request - The request body given by the user * * @constructor @@ -53,11 +55,11 @@ export default abstract class AbstractTransaction extends AbstractAction { * * Returns the schema of a transaction command * - * @property schema + * @property {FastifySchema} schema * - * @returns {any} + * @returns {FastifySchema} */ - public static get schema(): any { - return super.schema() + public static get schema(): FastifySchema { + return super.schema } } diff --git a/packages/govern-tx/lib/whitelist/AbstractWhitelistAction.ts b/packages/govern-tx/lib/whitelist/AbstractWhitelistAction.ts index 232122aa2..59064a07e 100644 --- a/packages/govern-tx/lib/whitelist/AbstractWhitelistAction.ts +++ b/packages/govern-tx/lib/whitelist/AbstractWhitelistAction.ts @@ -1,3 +1,4 @@ +import { FastifySchema } from 'fastify'; import AbstractAction, { Request } from '../AbstractAction' import Whitelist, {ListItem} from '../../src/db/Whitelist' @@ -10,8 +11,8 @@ export interface WhitelistRequest extends Request { export default abstract class AbstractWhitelistAction extends AbstractAction { /** + * @param {Whitelist} whitelist - The whitelist database entitiy * @param {WhitelistRequest} request - The given request body by the user - * @param {Whitelist} whitelist - The whitelist entitiy * * @constructor */ @@ -35,11 +36,11 @@ export default abstract class AbstractWhitelistAction extends AbstractAction { * * Returns the schema of a transaction command * - * @property schema + * @property {FastifySchema} schema * - * @returns {any} + * @returns {FastifySchema} */ - public static get schema(): any { - return super.schema() + public static get schema(): FastifySchema { + return super.schema } } diff --git a/packages/govern-tx/src/Bootstrap.ts b/packages/govern-tx/src/Bootstrap.ts index 7c53d0d24..85a5e3b1e 100644 --- a/packages/govern-tx/src/Bootstrap.ts +++ b/packages/govern-tx/src/Bootstrap.ts @@ -35,7 +35,7 @@ export default class Bootstrap { private authenticator: Authenticator /** - * @property {whitelist} Whitelist + * @property {Whitelist} whitelist * * @private */ diff --git a/packages/govern-tx/src/auth/Authenticator.ts b/packages/govern-tx/src/auth/Authenticator.ts index 790c6b5a1..60ade332a 100644 --- a/packages/govern-tx/src/auth/Authenticator.ts +++ b/packages/govern-tx/src/auth/Authenticator.ts @@ -14,16 +14,19 @@ export interface JWTOptions { export default class Authenticator { /** - * @property {HttpError} + * @property {HttpError} NOT_ALLOWED * * @private */ private NOT_ALLOWED: HttpError = new Unauthorized('Not allowed action!') /** + * @param {FastifyInstance} fastify * @param {Whitelist} whitelist + * @param {Admin} admin * @param {string} secret - * @param {SignOptions} jqtOptions + * @param {string} cookieName + * @param {JWTOptions} jwtOptions * * @constructor */ @@ -43,8 +46,8 @@ export default class Authenticator { * * @method authenticate * - * @param {string} message - The message from the user - * @param {string} signature - The sent signature from the user + * @param {FastifyRequest} request - The fastify request object + * @param {FastifyReply} reply - The fastify response object * * @returns Promise * @@ -52,7 +55,7 @@ export default class Authenticator { */ public async authenticate(request: FastifyRequest, reply: FastifyReply): Promise { const cookie = request.cookies[this.cookieName]; - const publicKey: string = verifyMessage(arrayify(request.body.message), request.body.signature); + const publicKey: string = verifyMessage(arrayify(request.body.message), request.body.signature); // TODO: Fix type definition if (cookie && this.verify(cookie)) { if (!(await this.hasPermission(request.routerPath, publicKey))) { @@ -107,7 +110,7 @@ export default class Authenticator { } /** - * Verifiey the given JWT + * Verify the given JWT * * @method verify * @@ -115,7 +118,7 @@ export default class Authenticator { * * @returns {boolean} * - * @public + * @private */ private verify(token: string): boolean { try { diff --git a/packages/govern-tx/src/config/Configuration.ts b/packages/govern-tx/src/config/Configuration.ts index e3b0f580b..17a3b9bc2 100644 --- a/packages/govern-tx/src/config/Configuration.ts +++ b/packages/govern-tx/src/config/Configuration.ts @@ -30,6 +30,8 @@ export default class Configuration { /** * @param {EthereumOptions} _ethereum * @param {DatabaseOptions} _database + * @param {AuthOptions} _auth + * @param {ServerOptions} _server * * @constructor */ diff --git a/packages/govern-tx/src/db/Admin.ts b/packages/govern-tx/src/db/Admin.ts index fc4146b7f..4826ed1f6 100644 --- a/packages/govern-tx/src/db/Admin.ts +++ b/packages/govern-tx/src/db/Admin.ts @@ -67,4 +67,4 @@ export default class Admin { public getAdmins(): Promise { return this.db.query('SELECT * from admins') } -} \ No newline at end of file +} diff --git a/packages/govern-tx/src/db/Whitelist.ts b/packages/govern-tx/src/db/Whitelist.ts index e92bf78c3..9b5fe555f 100644 --- a/packages/govern-tx/src/db/Whitelist.ts +++ b/packages/govern-tx/src/db/Whitelist.ts @@ -63,6 +63,7 @@ export default class Whitelist { * @method addItem * * @param {string} publicKey - The public key we would like to add + * @param {string} rateLimit - The amount of allowed transactions for this user * * @returns {Promise} * diff --git a/packages/govern-tx/src/provider/Provider.ts b/packages/govern-tx/src/provider/Provider.ts index 06729b610..8273b85d5 100644 --- a/packages/govern-tx/src/provider/Provider.ts +++ b/packages/govern-tx/src/provider/Provider.ts @@ -42,4 +42,4 @@ export default class Provider { this.configuration.ethereum.blockConfirmations ) } -} \ No newline at end of file +} diff --git a/packages/govern-tx/src/wallet/Wallet.ts b/packages/govern-tx/src/wallet/Wallet.ts index 99692f9b5..bf31a9130 100644 --- a/packages/govern-tx/src/wallet/Wallet.ts +++ b/packages/govern-tx/src/wallet/Wallet.ts @@ -50,4 +50,4 @@ export default class Wallet { return this.wallet } -} \ No newline at end of file +} From 08d5c3c0f70819ab6689665d2c2f44fc0971f40d Mon Sep 17 00:00:00 2001 From: nivida Date: Mon, 23 Nov 2020 12:36:58 +0100 Subject: [PATCH 032/107] typo fixed --- packages/govern-tx/src/Bootstrap.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/govern-tx/src/Bootstrap.ts b/packages/govern-tx/src/Bootstrap.ts index 85a5e3b1e..9ddb282d9 100644 --- a/packages/govern-tx/src/Bootstrap.ts +++ b/packages/govern-tx/src/Bootstrap.ts @@ -96,7 +96,7 @@ export default class Bootstrap { /** * TODO: Could be done cleaner but don't think it is necessary * - * Register all transaction relates routes + * Register all transaction related routes * * @method registerTransactionRoutes * @@ -133,7 +133,7 @@ export default class Bootstrap { /** * TODO: Could be done cleaner but don't think it is necessary * - * Register all whitelist relates routes + * Register all whitelist related routes * * @method registerWhitelistRoutes * From f7dbaf449a6dd54025490d37d753433e2c2345c1 Mon Sep 17 00:00:00 2001 From: nivida Date: Mon, 23 Nov 2020 16:37:09 +0100 Subject: [PATCH 033/107] transaction handling updated --- .../lib/transactions/AbstractTransaction.ts | 18 +++++- .../lib/transactions/ContractFunction.ts | 58 +++++++++++++++++++ .../govern-tx/src/config/Configuration.ts | 4 ++ packages/govern-tx/src/index.ts | 4 ++ packages/govern-tx/src/provider/Provider.ts | 38 ++++++++++-- .../src/transactions/ChallengeTransaction.ts | 16 +++++ .../src/transactions/ExecuteTransaction.ts | 16 +++++ .../src/transactions/ScheduleTransaction.ts | 16 +++++ packages/govern-tx/src/wallet/Wallet.ts | 43 ++++++++++---- 9 files changed, 196 insertions(+), 17 deletions(-) diff --git a/packages/govern-tx/lib/transactions/AbstractTransaction.ts b/packages/govern-tx/lib/transactions/AbstractTransaction.ts index a87bd1e12..82feb6648 100644 --- a/packages/govern-tx/lib/transactions/AbstractTransaction.ts +++ b/packages/govern-tx/lib/transactions/AbstractTransaction.ts @@ -3,6 +3,7 @@ import { TransactionReceipt } from '@ethersproject/abstract-provider' import AbstractAction, { Request } from '../AbstractAction' import ContractFunction from '../transactions/ContractFunction' import Provider from '../../src/provider/Provider' +import Configuration from '../../src/config/Configuration'; export default abstract class AbstractTransaction extends AbstractAction { /** @@ -14,6 +15,15 @@ export default abstract class AbstractTransaction extends AbstractAction { */ protected functionABI: any // TODO: Create functionABI object interface definition + /** + * The contract name + * + * @property {string} contract + * + * @protected + */ + protected contract: string + /** * The contract function * @@ -34,7 +44,11 @@ export default abstract class AbstractTransaction extends AbstractAction { request: Request ) { super(request); - this.contractFunction = new ContractFunction(this.functionABI, request.message) + + this.contractFunction = new ContractFunction( + this.functionABI, + request.message + ) } /** @@ -47,7 +61,7 @@ export default abstract class AbstractTransaction extends AbstractAction { * @public */ public execute(): Promise { - return this.provider.sendTransaction(this.contractFunction) + return this.provider.sendTransaction(this.contract, this.contractFunction) } /** diff --git a/packages/govern-tx/lib/transactions/ContractFunction.ts b/packages/govern-tx/lib/transactions/ContractFunction.ts index e69de29bb..100382a21 100644 --- a/packages/govern-tx/lib/transactions/ContractFunction.ts +++ b/packages/govern-tx/lib/transactions/ContractFunction.ts @@ -0,0 +1,58 @@ +import { defaultAbiCoder } from 'ethers/lib/utils'; +import { Result } from '@ethersproject/abi'; + +export interface TransactionOptions { + from?: string, + to: string, + data: string +} + +export default class ContractFunction { + /** + * The decoded function arguments + * + * @property functionArguments + * + * @private + */ + private functionArguments: any; + + /** + * @param {any} abiItem + * @param {string} requestMsg + * + * @constructor + */ + constructor( + private abiItem, + private requestMsg: string, + ) { + this.functionArguments = this.decode() + } + + /** + * Encodes the function by the given ABI item and the function parameters + * + * @method encode + * + * @returns {string} + * + * @public + */ + public encode(): string { + return defaultAbiCoder.encode(this.abiItem, this.functionArguments) + } + + /** + * Returns the decoded values by the given ABI item and the request message + * + * @method decode + * + * @returns {Result} + * + * @private + */ + public decode(): Result { + return defaultAbiCoder.decode(this.abiItem, this.requestMsg) + } +} diff --git a/packages/govern-tx/src/config/Configuration.ts b/packages/govern-tx/src/config/Configuration.ts index 17a3b9bc2..e0f91c3e0 100644 --- a/packages/govern-tx/src/config/Configuration.ts +++ b/packages/govern-tx/src/config/Configuration.ts @@ -3,6 +3,10 @@ import { JWTOptions } from '../auth/Authenticator'; export interface EthereumOptions { url: string blockConfirmations: number + publicKey: string + contracts: { + [name: string]: string + } } export interface DatabaseOptions { diff --git a/packages/govern-tx/src/index.ts b/packages/govern-tx/src/index.ts index 4afc06ad6..720d467ea 100644 --- a/packages/govern-tx/src/index.ts +++ b/packages/govern-tx/src/index.ts @@ -4,6 +4,10 @@ import Bootstrap from './Bootstrap' new Bootstrap( new Configuration( { + publicKey: '0x0...', + contracts: { + GovernQueue: '0x0...' + }, url: 'localhost:8545', blockConfirmations: 42 }, diff --git a/packages/govern-tx/src/provider/Provider.ts b/packages/govern-tx/src/provider/Provider.ts index 8273b85d5..a3095e18a 100644 --- a/packages/govern-tx/src/provider/Provider.ts +++ b/packages/govern-tx/src/provider/Provider.ts @@ -1,7 +1,7 @@ import Wallet from '../wallet/Wallet' -import { BaseProvider } from '@ethersproject/providers' +import { BaseProvider, TransactionRequest } from '@ethersproject/providers' import { TransactionReceipt } from '@ethersproject/abstract-provider' -import ContractFunction from '../lib/transactions/ContractFunction' +import ContractFunction from '../../lib/transactions/ContractFunction' import Configuration from '../config/Configuration' export default class Provider { @@ -29,17 +29,47 @@ export default class Provider { * * @method sendTransaction * + * @param {string} contract * @param {ContractFunction} contractFunction * * @returns {Promise} */ - public async sendTransaction(contractFunction: ContractFunction): Promise { + public async sendTransaction(contract: string, contractFunction: ContractFunction): Promise { return ( await this.provider.sendTransaction( - await this.wallet.sign(contractFunction) + await this.wallet.sign( + await this.getTransactionOptions( + contract, + contractFunction + ), + this.configuration.ethereum.publicKey + ) ) ).wait( this.configuration.ethereum.blockConfirmations ) } + + /** + * TODO: Add gas price definition (could get loaded from ethgasstation to a have average gas price) + * + * Returns the transaction options + * + * @method getTransactionOptions + * + * @param {string} contract - The name of the contract + * @param {ContractFunction} contractFunction - The ContractFunction object + * + * @returns {Promise} + */ + private async getTransactionOptions(contract: string, contractFunction: ContractFunction): Promise { + const txOptions: TransactionRequest = { + to: this.configuration.ethereum.contracts[contract], + data: contractFunction.encode() + } + + txOptions.gasLimit = await this.provider.estimateGas(txOptions) + + return txOptions + } } diff --git a/packages/govern-tx/src/transactions/ChallengeTransaction.ts b/packages/govern-tx/src/transactions/ChallengeTransaction.ts index 8e66d9abd..0b938a327 100644 --- a/packages/govern-tx/src/transactions/ChallengeTransaction.ts +++ b/packages/govern-tx/src/transactions/ChallengeTransaction.ts @@ -1,5 +1,21 @@ import AbstractTransaction from '../../lib/transactions/AbstractTransaction'; export default class ChallengeTransaction extends AbstractTransaction { + /** + * The contract name + * + * @property {string} contract + * + * @protected + */ + protected contract: string = 'GovernQueue' + + /** + * The function ABI used to create a transaction + * + * @property {Object} functionABI + * + * @protected + */ protected functionABI: any = {} } diff --git a/packages/govern-tx/src/transactions/ExecuteTransaction.ts b/packages/govern-tx/src/transactions/ExecuteTransaction.ts index 25dde4ca3..3c1179478 100644 --- a/packages/govern-tx/src/transactions/ExecuteTransaction.ts +++ b/packages/govern-tx/src/transactions/ExecuteTransaction.ts @@ -1,5 +1,21 @@ import AbstractTransaction from '../../lib/transactions/AbstractTransaction'; export default class ExecuteTransaction extends AbstractTransaction { + /** + * The contract name + * + * @property {string} contract + * + * @protected + */ + protected contract: string = 'GovernQueue' + + /** + * The function ABI used to create a transaction + * + * @property {Object} functionABI + * + * @protected + */ protected functionABI: any = {} } diff --git a/packages/govern-tx/src/transactions/ScheduleTransaction.ts b/packages/govern-tx/src/transactions/ScheduleTransaction.ts index 1eb5e70ca..27aa27c99 100644 --- a/packages/govern-tx/src/transactions/ScheduleTransaction.ts +++ b/packages/govern-tx/src/transactions/ScheduleTransaction.ts @@ -1,5 +1,21 @@ import AbstractTransaction from '../../lib/transactions/AbstractTransaction'; export default class ScheduleTransaction extends AbstractTransaction { + /** + * The contract name + * + * @property {string} contract + * + * @protected + */ + protected contract: string = 'GovernQueue' + + /** + * The function ABI used to create a transaction + * + * @property {Object} functionABI + * + * @protected + */ protected functionABI: any = {} } diff --git a/packages/govern-tx/src/wallet/Wallet.ts b/packages/govern-tx/src/wallet/Wallet.ts index bf31a9130..d635c1a25 100644 --- a/packages/govern-tx/src/wallet/Wallet.ts +++ b/packages/govern-tx/src/wallet/Wallet.ts @@ -1,6 +1,11 @@ import {Wallet as EthersWallet} from '@ethersproject/wallet' +import { TransactionRequest } from '@ethersproject/providers' import Database from '../db/Database' -import ContractFunction from '../../lib/transactions/ContractFunction' + +export interface WalletItem { + PrivateKey: string + PublicKey: string +} export default class Wallet { /** @@ -12,42 +17,58 @@ export default class Wallet { */ private wallet: EthersWallet + /** + * The public key of the loaded wallet account + * + * @property {string} publicKey + * + * @private + */ + private publicKey: string; + /** * @param {Database} db - Database adapter * * @constructor */ - constructor(private db: Database) {} + constructor(private db: Database) { } /** * Signs the given contract function with the Aragon privateKey * * @method sign * - * @param {ContractFunction} contractFunction + * @param {TransactionRequest} txOptions + * @param {string} publicKey * * @returns {Promise} * * @public */ - public async sign(contractFunction: ContractFunction): Promise { - return (await this.getWallet()).signTransaction(contractFunction.toTransaction()) + public async sign(txOptions: TransactionRequest, publicKey: string): Promise { + await this.loadWallet(publicKey) + + txOptions.from = publicKey + + return this.wallet.signTransaction(txOptions) } /** * Returns the ethers wallet instance prepopulated with the Aragon privateKey * - * @method getWallet + * @method loadWallet + * + * @param {string} publicKey * * @returns {EthersWallet} * * @private */ - private async getWallet(): Promise { - if (!this.wallet) { - this.wallet = new EthersWallet(await this.db.query("SELECT PrivateKey FROM wallet WHERE id='0'")) + private async loadWallet(publicKey: string): Promise { + if (!this.wallet || this.publicKey !== publicKey) { + const pk = await this.db.query(`SELECT PrivateKey FROM wallet WHERE PublicKey='${publicKey}'`); + this.wallet = new EthersWallet(pk) + this.publicKey = publicKey; } - - return this.wallet } } From a497e80ef119d04b553f8280065bda2520e21341 Mon Sep 17 00:00:00 2001 From: nivida Date: Mon, 23 Nov 2020 16:37:47 +0100 Subject: [PATCH 034/107] not required interface definition removed in ContractFunction --- packages/govern-tx/lib/transactions/ContractFunction.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/packages/govern-tx/lib/transactions/ContractFunction.ts b/packages/govern-tx/lib/transactions/ContractFunction.ts index 100382a21..9bd838840 100644 --- a/packages/govern-tx/lib/transactions/ContractFunction.ts +++ b/packages/govern-tx/lib/transactions/ContractFunction.ts @@ -1,12 +1,6 @@ import { defaultAbiCoder } from 'ethers/lib/utils'; import { Result } from '@ethersproject/abi'; -export interface TransactionOptions { - from?: string, - to: string, - data: string -} - export default class ContractFunction { /** * The decoded function arguments From 2720ce4c9a7fcd53d3fbc37327756a5b75e13101 Mon Sep 17 00:00:00 2001 From: nivida Date: Mon, 23 Nov 2020 16:46:06 +0100 Subject: [PATCH 035/107] draft for submitter manipulation added --- .../lib/transactions/AbstractTransaction.ts | 7 ++++++- .../lib/transactions/ContractFunction.ts | 6 ++++-- packages/govern-tx/src/Bootstrap.ts | 8 ++++---- packages/govern-tx/src/provider/Provider.ts | 16 ++++++++-------- 4 files changed, 22 insertions(+), 15 deletions(-) diff --git a/packages/govern-tx/lib/transactions/AbstractTransaction.ts b/packages/govern-tx/lib/transactions/AbstractTransaction.ts index 82feb6648..0da35685b 100644 --- a/packages/govern-tx/lib/transactions/AbstractTransaction.ts +++ b/packages/govern-tx/lib/transactions/AbstractTransaction.ts @@ -3,7 +3,7 @@ import { TransactionReceipt } from '@ethersproject/abstract-provider' import AbstractAction, { Request } from '../AbstractAction' import ContractFunction from '../transactions/ContractFunction' import Provider from '../../src/provider/Provider' -import Configuration from '../../src/config/Configuration'; +import { EthereumOptions } from '../../src/config/Configuration'; export default abstract class AbstractTransaction extends AbstractAction { /** @@ -34,12 +34,14 @@ export default abstract class AbstractTransaction extends AbstractAction { private contractFunction: ContractFunction /** + * @param {EthereumOptions} config * @param {Provider} provider - The Ethereum provider object * @param {Request} request - The request body given by the user * * @constructor */ constructor( + private config: EthereumOptions, // TODO: Overthink configuration handling. I have the feeling I'm injecting it to often. private provider: Provider, request: Request ) { @@ -61,6 +63,9 @@ export default abstract class AbstractTransaction extends AbstractAction { * @public */ public execute(): Promise { + // TODO: This handling doesn't look that clean. Find a better solution. + this.contractFunction.functionArguments.container.payload.submitter = this.config.publicKey + return this.provider.sendTransaction(this.contract, this.contractFunction) } diff --git a/packages/govern-tx/lib/transactions/ContractFunction.ts b/packages/govern-tx/lib/transactions/ContractFunction.ts index 9bd838840..6eb6aae2b 100644 --- a/packages/govern-tx/lib/transactions/ContractFunction.ts +++ b/packages/govern-tx/lib/transactions/ContractFunction.ts @@ -9,16 +9,18 @@ export default class ContractFunction { * * @private */ - private functionArguments: any; + public functionArguments: any; /** + * TODO: Define ABI item interface + * * @param {any} abiItem * @param {string} requestMsg * * @constructor */ constructor( - private abiItem, + private abiItem: any, private requestMsg: string, ) { this.functionArguments = this.decode() diff --git a/packages/govern-tx/src/Bootstrap.ts b/packages/govern-tx/src/Bootstrap.ts index 9ddb282d9..ab50e2b83 100644 --- a/packages/govern-tx/src/Bootstrap.ts +++ b/packages/govern-tx/src/Bootstrap.ts @@ -109,7 +109,7 @@ export default class Bootstrap { '/execute', {schema: AbstractTransaction.schema}, (request): Promise => { - return new ExecuteTransaction(this.provider, request.params as Request).execute() + return new ExecuteTransaction(this.config.ethereum, this.provider, request.params as Request).execute() } ) @@ -117,7 +117,7 @@ export default class Bootstrap { '/schedule', {schema: AbstractTransaction.schema}, (request): Promise => { - return new ScheduleTransaction(this.provider, request.params as Request).execute() + return new ScheduleTransaction(this.config.ethereum, this.provider, request.params as Request).execute() } ) @@ -125,7 +125,7 @@ export default class Bootstrap { '/challenge', {schema: AbstractTransaction.schema}, (request): Promise => { - return new ChallengeTransaction(this.provider, request.params as Request).execute() + return new ChallengeTransaction(this.config.ethereum, this.provider, request.params as Request).execute() } ) } @@ -209,7 +209,7 @@ export default class Bootstrap { * @private */ private setProvider(): void { - this.provider = new Provider(this.config, new Wallet(this.database)) + this.provider = new Provider(this.config.ethereum, new Wallet(this.database)) } /** diff --git a/packages/govern-tx/src/provider/Provider.ts b/packages/govern-tx/src/provider/Provider.ts index a3095e18a..adebbfbeb 100644 --- a/packages/govern-tx/src/provider/Provider.ts +++ b/packages/govern-tx/src/provider/Provider.ts @@ -1,8 +1,8 @@ -import Wallet from '../wallet/Wallet' import { BaseProvider, TransactionRequest } from '@ethersproject/providers' import { TransactionReceipt } from '@ethersproject/abstract-provider' +import Wallet from '../wallet/Wallet' +import { EthereumOptions } from '../config/Configuration'; import ContractFunction from '../../lib/transactions/ContractFunction' -import Configuration from '../config/Configuration' export default class Provider { /** @@ -15,13 +15,13 @@ export default class Provider { private provider: BaseProvider /** - * @param {Configuration} configuration + * @param {EthereumOptions} config * @param {Wallet} wallet * * @constructor */ - constructor(private configuration: Configuration, private wallet: Wallet) { - this.provider = new BaseProvider(this.configuration.ethereum.url) + constructor(private config: EthereumOptions, private wallet: Wallet) { + this.provider = new BaseProvider(this.config.url) } /** @@ -42,11 +42,11 @@ export default class Provider { contract, contractFunction ), - this.configuration.ethereum.publicKey + this.config.publicKey ) ) ).wait( - this.configuration.ethereum.blockConfirmations + this.config.blockConfirmations ) } @@ -64,7 +64,7 @@ export default class Provider { */ private async getTransactionOptions(contract: string, contractFunction: ContractFunction): Promise { const txOptions: TransactionRequest = { - to: this.configuration.ethereum.contracts[contract], + to: this.config.contracts[contract], data: contractFunction.encode() } From c6a344bb4197fef9da9af9746702c5288964afd4 Mon Sep 17 00:00:00 2001 From: nivida Date: Mon, 23 Nov 2020 16:54:46 +0100 Subject: [PATCH 036/107] ToDos added to think about tomorrow --- packages/govern-tx/lib/transactions/AbstractTransaction.ts | 3 ++- packages/govern-tx/src/config/Configuration.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/govern-tx/lib/transactions/AbstractTransaction.ts b/packages/govern-tx/lib/transactions/AbstractTransaction.ts index 0da35685b..759d383cf 100644 --- a/packages/govern-tx/lib/transactions/AbstractTransaction.ts +++ b/packages/govern-tx/lib/transactions/AbstractTransaction.ts @@ -5,6 +5,7 @@ import ContractFunction from '../transactions/ContractFunction' import Provider from '../../src/provider/Provider' import { EthereumOptions } from '../../src/config/Configuration'; +// TODO: Overthink dependency handling of AbstractTransaction -> Provider -> Wallet and the configuration. Ignoring separation of concerns for Provider/Wallet? export default abstract class AbstractTransaction extends AbstractAction { /** * The function ABI used to create a transaction @@ -63,7 +64,7 @@ export default abstract class AbstractTransaction extends AbstractAction { * @public */ public execute(): Promise { - // TODO: This handling doesn't look that clean. Find a better solution. + // TODO: This handling doesn't look that clean. Find a better solution also without code duplication. this.contractFunction.functionArguments.container.payload.submitter = this.config.publicKey return this.provider.sendTransaction(this.contract, this.contractFunction) diff --git a/packages/govern-tx/src/config/Configuration.ts b/packages/govern-tx/src/config/Configuration.ts index e0f91c3e0..66064dfc2 100644 --- a/packages/govern-tx/src/config/Configuration.ts +++ b/packages/govern-tx/src/config/Configuration.ts @@ -29,7 +29,8 @@ export interface ServerOptions { logLevel?: string } -// TODO: Add if required input validations +// TODO: Add input validations for Ethereum addresses +// TODO: Change constructor to only have one required argument to pass export default class Configuration { /** * @param {EthereumOptions} _ethereum From 761a270c912be7153bdcc2fde411f41a1aac07f2 Mon Sep 17 00:00:00 2001 From: nivida Date: Tue, 24 Nov 2020 10:15:06 +0100 Subject: [PATCH 037/107] correct encoding/decoding API implemented from ethers.js --- packages/govern-tx/lib/AbstractAction.ts | 1 + .../lib/transactions/AbstractTransaction.ts | 5 ++-- .../lib/transactions/ContractFunction.ts | 25 ++++++++++++------- packages/govern-tx/src/Bootstrap.ts | 1 - 4 files changed, 19 insertions(+), 13 deletions(-) diff --git a/packages/govern-tx/lib/AbstractAction.ts b/packages/govern-tx/lib/AbstractAction.ts index 930eb9147..ca1c101f6 100644 --- a/packages/govern-tx/lib/AbstractAction.ts +++ b/packages/govern-tx/lib/AbstractAction.ts @@ -1,4 +1,5 @@ import { FastifySchema } from 'fastify' + export interface Request { message: string | any, signature: string diff --git a/packages/govern-tx/lib/transactions/AbstractTransaction.ts b/packages/govern-tx/lib/transactions/AbstractTransaction.ts index 759d383cf..929c43799 100644 --- a/packages/govern-tx/lib/transactions/AbstractTransaction.ts +++ b/packages/govern-tx/lib/transactions/AbstractTransaction.ts @@ -42,7 +42,7 @@ export default abstract class AbstractTransaction extends AbstractAction { * @constructor */ constructor( - private config: EthereumOptions, // TODO: Overthink configuration handling. I have the feeling I'm injecting it to often. + private config: EthereumOptions, private provider: Provider, request: Request ) { @@ -64,8 +64,7 @@ export default abstract class AbstractTransaction extends AbstractAction { * @public */ public execute(): Promise { - // TODO: This handling doesn't look that clean. Find a better solution also without code duplication. - this.contractFunction.functionArguments.container.payload.submitter = this.config.publicKey + this.contractFunction.functionArguments[0].payload.submitter = this.config.publicKey return this.provider.sendTransaction(this.contract, this.contractFunction) } diff --git a/packages/govern-tx/lib/transactions/ContractFunction.ts b/packages/govern-tx/lib/transactions/ContractFunction.ts index 6eb6aae2b..9e380ae4e 100644 --- a/packages/govern-tx/lib/transactions/ContractFunction.ts +++ b/packages/govern-tx/lib/transactions/ContractFunction.ts @@ -1,15 +1,21 @@ -import { defaultAbiCoder } from 'ethers/lib/utils'; -import { Result } from '@ethersproject/abi'; +import { defaultAbiCoder, Fragment, JsonFragment } from '@ethersproject/abi'; export default class ContractFunction { /** * The decoded function arguments * - * @property functionArguments + * @property {Result} functionArguments * * @private */ - public functionArguments: any; + public functionArguments: any[]; + + /** + * The ABI item as ethers.js Fragment object + * + * @property {Fragment} abiItem + */ + private abiItem: Fragment; /** * TODO: Define ABI item interface @@ -20,9 +26,10 @@ export default class ContractFunction { * @constructor */ constructor( - private abiItem: any, + abiItem: JsonFragment, private requestMsg: string, ) { + this.abiItem = Fragment.fromObject(abiItem) this.functionArguments = this.decode() } @@ -36,7 +43,7 @@ export default class ContractFunction { * @public */ public encode(): string { - return defaultAbiCoder.encode(this.abiItem, this.functionArguments) + return defaultAbiCoder.encode(this.abiItem.inputs, this.functionArguments) } /** @@ -46,9 +53,9 @@ export default class ContractFunction { * * @returns {Result} * - * @private + * @public */ - public decode(): Result { - return defaultAbiCoder.decode(this.abiItem, this.requestMsg) + public decode(): any[] { + return defaultAbiCoder.decode(this.abiItem.inputs, this.requestMsg) as any[] } } diff --git a/packages/govern-tx/src/Bootstrap.ts b/packages/govern-tx/src/Bootstrap.ts index ab50e2b83..0b313adaa 100644 --- a/packages/govern-tx/src/Bootstrap.ts +++ b/packages/govern-tx/src/Bootstrap.ts @@ -182,7 +182,6 @@ export default class Bootstrap { level: this.config.server.logLevel ?? 'debug' }, ignoreTrailingSlash: true - //https: {} TODO: Configure TLS }) } From 269aa544784e2419090552694cb0954ed65f1a4d Mon Sep 17 00:00:00 2001 From: nivida Date: Tue, 24 Nov 2020 10:34:44 +0100 Subject: [PATCH 038/107] ABI items added and type definitions improved --- .../lib/transactions/AbstractTransaction.ts | 4 +- packages/govern-tx/src/Bootstrap.ts | 6 +- .../{ => challenge}/ChallengeTransaction.ts | 7 +- .../src/transactions/challenge/challenge.json | 142 ++++++++++++++++++ .../{ => execute}/ExecuteTransaction.ts | 5 +- .../src/transactions/execute/execute.json | 142 ++++++++++++++++++ .../{ => schedule}/ScheduleTransaction.ts | 5 +- .../src/transactions/schedule/schedule.json | 137 +++++++++++++++++ 8 files changed, 436 insertions(+), 12 deletions(-) rename packages/govern-tx/src/transactions/{ => challenge}/ChallengeTransaction.ts (60%) create mode 100644 packages/govern-tx/src/transactions/challenge/challenge.json rename packages/govern-tx/src/transactions/{ => execute}/ExecuteTransaction.ts (68%) create mode 100644 packages/govern-tx/src/transactions/execute/execute.json rename packages/govern-tx/src/transactions/{ => schedule}/ScheduleTransaction.ts (68%) create mode 100644 packages/govern-tx/src/transactions/schedule/schedule.json diff --git a/packages/govern-tx/lib/transactions/AbstractTransaction.ts b/packages/govern-tx/lib/transactions/AbstractTransaction.ts index 929c43799..d0e5add67 100644 --- a/packages/govern-tx/lib/transactions/AbstractTransaction.ts +++ b/packages/govern-tx/lib/transactions/AbstractTransaction.ts @@ -1,11 +1,11 @@ import { FastifySchema } from 'fastify'; import { TransactionReceipt } from '@ethersproject/abstract-provider' +import { JsonFragment } from '@ethersproject/abi'; import AbstractAction, { Request } from '../AbstractAction' import ContractFunction from '../transactions/ContractFunction' import Provider from '../../src/provider/Provider' import { EthereumOptions } from '../../src/config/Configuration'; -// TODO: Overthink dependency handling of AbstractTransaction -> Provider -> Wallet and the configuration. Ignoring separation of concerns for Provider/Wallet? export default abstract class AbstractTransaction extends AbstractAction { /** * The function ABI used to create a transaction @@ -14,7 +14,7 @@ export default abstract class AbstractTransaction extends AbstractAction { * * @protected */ - protected functionABI: any // TODO: Create functionABI object interface definition + protected functionABI: JsonFragment /** * The contract name diff --git a/packages/govern-tx/src/Bootstrap.ts b/packages/govern-tx/src/Bootstrap.ts index 0b313adaa..2e8f3607f 100644 --- a/packages/govern-tx/src/Bootstrap.ts +++ b/packages/govern-tx/src/Bootstrap.ts @@ -9,9 +9,9 @@ import Whitelist, { ListItem } from './db/Whitelist' import Admin from './db/Admin' import Authenticator, { JWTOptions } from './auth/Authenticator' -import ExecuteTransaction from './transactions/ExecuteTransaction' -import ChallengeTransaction from './transactions/ChallengeTransaction' -import ScheduleTransaction from './transactions/ScheduleTransaction' +import ExecuteTransaction from './transactions/execute/ExecuteTransaction' +import ChallengeTransaction from './transactions/challenge/ChallengeTransaction' +import ScheduleTransaction from './transactions/schedule/ScheduleTransaction' import AddItemAction from './whitelist/AddItemAction' import DeleteItemAction from './whitelist/DeleteItemAction' diff --git a/packages/govern-tx/src/transactions/ChallengeTransaction.ts b/packages/govern-tx/src/transactions/challenge/ChallengeTransaction.ts similarity index 60% rename from packages/govern-tx/src/transactions/ChallengeTransaction.ts rename to packages/govern-tx/src/transactions/challenge/ChallengeTransaction.ts index 0b938a327..54042dca0 100644 --- a/packages/govern-tx/src/transactions/ChallengeTransaction.ts +++ b/packages/govern-tx/src/transactions/challenge/ChallengeTransaction.ts @@ -1,4 +1,5 @@ -import AbstractTransaction from '../../lib/transactions/AbstractTransaction'; +import AbstractTransaction from '../../../lib/transactions/AbstractTransaction' +import * as challengeABI from './challenge.json' export default class ChallengeTransaction extends AbstractTransaction { /** @@ -13,9 +14,9 @@ export default class ChallengeTransaction extends AbstractTransaction { /** * The function ABI used to create a transaction * - * @property {Object} functionABI + * @property {JsonFragment} functionABI * * @protected */ - protected functionABI: any = {} + protected functionABI: any = challengeABI } diff --git a/packages/govern-tx/src/transactions/challenge/challenge.json b/packages/govern-tx/src/transactions/challenge/challenge.json new file mode 100644 index 000000000..0ecbe337a --- /dev/null +++ b/packages/govern-tx/src/transactions/challenge/challenge.json @@ -0,0 +1,142 @@ +{ + "inputs": [ + { + "components": [ + { + "components": [ + { + "internalType": "uint256", + "name": "nonce", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "executionTime", + "type": "uint256" + }, + { + "internalType": "address", + "name": "submitter", + "type": "address" + }, + { + "internalType": "contract IERC3000Executor", + "name": "executor", + "type": "address" + }, + { + "components": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "internalType": "struct ERC3000Data.Action[]", + "name": "actions", + "type": "tuple[]" + }, + { + "internalType": "bytes32", + "name": "allowFailuresMap", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "proof", + "type": "bytes" + } + ], + "internalType": "struct ERC3000Data.Payload", + "name": "payload", + "type": "tuple" + }, + { + "components": [ + { + "internalType": "uint256", + "name": "executionDelay", + "type": "uint256" + }, + { + "components": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "internalType": "struct ERC3000Data.Collateral", + "name": "scheduleDeposit", + "type": "tuple" + }, + { + "components": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "internalType": "struct ERC3000Data.Collateral", + "name": "challengeDeposit", + "type": "tuple" + }, + { + "internalType": "address", + "name": "resolver", + "type": "address" + }, + { + "internalType": "bytes", + "name": "rules", + "type": "bytes" + } + ], + "internalType": "struct ERC3000Data.Config", + "name": "config", + "type": "tuple" + } + ], + "internalType": "struct ERC3000Data.Container", + "name": "_container", + "type": "tuple" + }, + { + "internalType": "bytes", + "name": "_reason", + "type": "bytes" + } + ], + "name": "challenge", + "outputs": [ + { + "internalType": "uint256", + "name": "disputeId", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + } + \ No newline at end of file diff --git a/packages/govern-tx/src/transactions/ExecuteTransaction.ts b/packages/govern-tx/src/transactions/execute/ExecuteTransaction.ts similarity index 68% rename from packages/govern-tx/src/transactions/ExecuteTransaction.ts rename to packages/govern-tx/src/transactions/execute/ExecuteTransaction.ts index 3c1179478..a8d84f737 100644 --- a/packages/govern-tx/src/transactions/ExecuteTransaction.ts +++ b/packages/govern-tx/src/transactions/execute/ExecuteTransaction.ts @@ -1,4 +1,5 @@ -import AbstractTransaction from '../../lib/transactions/AbstractTransaction'; +import AbstractTransaction from '../../../lib/transactions/AbstractTransaction' +import * as executeABI from './execute.json' export default class ExecuteTransaction extends AbstractTransaction { /** @@ -17,5 +18,5 @@ export default class ExecuteTransaction extends AbstractTransaction { * * @protected */ - protected functionABI: any = {} + protected functionABI: any = executeABI } diff --git a/packages/govern-tx/src/transactions/execute/execute.json b/packages/govern-tx/src/transactions/execute/execute.json new file mode 100644 index 000000000..47dd9c2c4 --- /dev/null +++ b/packages/govern-tx/src/transactions/execute/execute.json @@ -0,0 +1,142 @@ +{ + "inputs": [ + { + "components": [ + { + "components": [ + { + "internalType": "uint256", + "name": "nonce", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "executionTime", + "type": "uint256" + }, + { + "internalType": "address", + "name": "submitter", + "type": "address" + }, + { + "internalType": "contract IERC3000Executor", + "name": "executor", + "type": "address" + }, + { + "components": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "internalType": "struct ERC3000Data.Action[]", + "name": "actions", + "type": "tuple[]" + }, + { + "internalType": "bytes32", + "name": "allowFailuresMap", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "proof", + "type": "bytes" + } + ], + "internalType": "struct ERC3000Data.Payload", + "name": "payload", + "type": "tuple" + }, + { + "components": [ + { + "internalType": "uint256", + "name": "executionDelay", + "type": "uint256" + }, + { + "components": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "internalType": "struct ERC3000Data.Collateral", + "name": "scheduleDeposit", + "type": "tuple" + }, + { + "components": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "internalType": "struct ERC3000Data.Collateral", + "name": "challengeDeposit", + "type": "tuple" + }, + { + "internalType": "address", + "name": "resolver", + "type": "address" + }, + { + "internalType": "bytes", + "name": "rules", + "type": "bytes" + } + ], + "internalType": "struct ERC3000Data.Config", + "name": "config", + "type": "tuple" + } + ], + "internalType": "struct ERC3000Data.Container", + "name": "_container", + "type": "tuple" + } + ], + "name": "execute", + "outputs": [ + { + "internalType": "bytes32", + "name": "failureMap", + "type": "bytes32" + }, + { + "internalType": "bytes[]", + "name": "execResults", + "type": "bytes[]" + } + ], + "stateMutability": "nonpayable", + "type": "function" + } + \ No newline at end of file diff --git a/packages/govern-tx/src/transactions/ScheduleTransaction.ts b/packages/govern-tx/src/transactions/schedule/ScheduleTransaction.ts similarity index 68% rename from packages/govern-tx/src/transactions/ScheduleTransaction.ts rename to packages/govern-tx/src/transactions/schedule/ScheduleTransaction.ts index 27aa27c99..2ce74c118 100644 --- a/packages/govern-tx/src/transactions/ScheduleTransaction.ts +++ b/packages/govern-tx/src/transactions/schedule/ScheduleTransaction.ts @@ -1,4 +1,5 @@ -import AbstractTransaction from '../../lib/transactions/AbstractTransaction'; +import AbstractTransaction from '../../../lib/transactions/AbstractTransaction' +import * as scheduleABI from './schedule.json' export default class ScheduleTransaction extends AbstractTransaction { /** @@ -17,5 +18,5 @@ export default class ScheduleTransaction extends AbstractTransaction { * * @protected */ - protected functionABI: any = {} + protected functionABI: any = scheduleABI } diff --git a/packages/govern-tx/src/transactions/schedule/schedule.json b/packages/govern-tx/src/transactions/schedule/schedule.json new file mode 100644 index 000000000..cfe6b3aaa --- /dev/null +++ b/packages/govern-tx/src/transactions/schedule/schedule.json @@ -0,0 +1,137 @@ +{ + "inputs": [ + { + "components": [ + { + "components": [ + { + "internalType": "uint256", + "name": "nonce", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "executionTime", + "type": "uint256" + }, + { + "internalType": "address", + "name": "submitter", + "type": "address" + }, + { + "internalType": "contract IERC3000Executor", + "name": "executor", + "type": "address" + }, + { + "components": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "internalType": "struct ERC3000Data.Action[]", + "name": "actions", + "type": "tuple[]" + }, + { + "internalType": "bytes32", + "name": "allowFailuresMap", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "proof", + "type": "bytes" + } + ], + "internalType": "struct ERC3000Data.Payload", + "name": "payload", + "type": "tuple" + }, + { + "components": [ + { + "internalType": "uint256", + "name": "executionDelay", + "type": "uint256" + }, + { + "components": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "internalType": "struct ERC3000Data.Collateral", + "name": "scheduleDeposit", + "type": "tuple" + }, + { + "components": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "internalType": "struct ERC3000Data.Collateral", + "name": "challengeDeposit", + "type": "tuple" + }, + { + "internalType": "address", + "name": "resolver", + "type": "address" + }, + { + "internalType": "bytes", + "name": "rules", + "type": "bytes" + } + ], + "internalType": "struct ERC3000Data.Config", + "name": "config", + "type": "tuple" + } + ], + "internalType": "struct ERC3000Data.Container", + "name": "_container", + "type": "tuple" + } + ], + "name": "schedule", + "outputs": [ + { + "internalType": "bytes32", + "name": "containerHash", + "type": "bytes32" + } + ], + "stateMutability": "nonpayable", + "type": "function" + } + \ No newline at end of file From 29b6eefcd162515622ee6866ec347d52911744d1 Mon Sep 17 00:00:00 2001 From: nivida Date: Tue, 24 Nov 2020 10:40:13 +0100 Subject: [PATCH 039/107] typescript related improvements --- .../src/transactions/challenge/ChallengeTransaction.ts | 3 ++- .../govern-tx/src/transactions/execute/ExecuteTransaction.ts | 5 +++-- .../src/transactions/schedule/ScheduleTransaction.ts | 5 +++-- packages/govern-tx/tsconfig.json | 5 +++-- 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/packages/govern-tx/src/transactions/challenge/ChallengeTransaction.ts b/packages/govern-tx/src/transactions/challenge/ChallengeTransaction.ts index 54042dca0..5d45d7fdf 100644 --- a/packages/govern-tx/src/transactions/challenge/ChallengeTransaction.ts +++ b/packages/govern-tx/src/transactions/challenge/ChallengeTransaction.ts @@ -1,3 +1,4 @@ +import { JsonFragment } from '@ethersproject/abi'; import AbstractTransaction from '../../../lib/transactions/AbstractTransaction' import * as challengeABI from './challenge.json' @@ -18,5 +19,5 @@ export default class ChallengeTransaction extends AbstractTransaction { * * @protected */ - protected functionABI: any = challengeABI + protected functionABI: JsonFragment = challengeABI } diff --git a/packages/govern-tx/src/transactions/execute/ExecuteTransaction.ts b/packages/govern-tx/src/transactions/execute/ExecuteTransaction.ts index a8d84f737..6a5adf7b3 100644 --- a/packages/govern-tx/src/transactions/execute/ExecuteTransaction.ts +++ b/packages/govern-tx/src/transactions/execute/ExecuteTransaction.ts @@ -1,3 +1,4 @@ +import { JsonFragment } from '@ethersproject/abi'; import AbstractTransaction from '../../../lib/transactions/AbstractTransaction' import * as executeABI from './execute.json' @@ -14,9 +15,9 @@ export default class ExecuteTransaction extends AbstractTransaction { /** * The function ABI used to create a transaction * - * @property {Object} functionABI + * @property {JsonFragment} functionABI * * @protected */ - protected functionABI: any = executeABI + protected functionABI: JsonFragment = executeABI } diff --git a/packages/govern-tx/src/transactions/schedule/ScheduleTransaction.ts b/packages/govern-tx/src/transactions/schedule/ScheduleTransaction.ts index 2ce74c118..0acd6df8a 100644 --- a/packages/govern-tx/src/transactions/schedule/ScheduleTransaction.ts +++ b/packages/govern-tx/src/transactions/schedule/ScheduleTransaction.ts @@ -1,3 +1,4 @@ +import { JsonFragment } from '@ethersproject/abi'; import AbstractTransaction from '../../../lib/transactions/AbstractTransaction' import * as scheduleABI from './schedule.json' @@ -14,9 +15,9 @@ export default class ScheduleTransaction extends AbstractTransaction { /** * The function ABI used to create a transaction * - * @property {Object} functionABI + * @property {JsonFragment} functionABI * * @protected */ - protected functionABI: any = scheduleABI + protected functionABI: JsonFragment = scheduleABI } diff --git a/packages/govern-tx/tsconfig.json b/packages/govern-tx/tsconfig.json index f7e9a54b5..cc0324886 100644 --- a/packages/govern-tx/tsconfig.json +++ b/packages/govern-tx/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "@ethereumjs/config-tsc", "compilerOptions": { + "resolveJsonModule": true, "removeComments": true, "strictPropertyInitialization": false, "outDir": "./dist", @@ -14,7 +15,7 @@ ] }, "include": [ - "./internal/**/*", - "./public/**/*" + "./src/**/*", + "./lib/**/*", ] } From 657ac340aaa6d54225cfe1041203ce556569762a Mon Sep 17 00:00:00 2001 From: nivida Date: Tue, 24 Nov 2020 10:52:56 +0100 Subject: [PATCH 040/107] basic response validation schema added --- .../lib/transactions/AbstractTransaction.ts | 27 +++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/packages/govern-tx/lib/transactions/AbstractTransaction.ts b/packages/govern-tx/lib/transactions/AbstractTransaction.ts index d0e5add67..5d189016a 100644 --- a/packages/govern-tx/lib/transactions/AbstractTransaction.ts +++ b/packages/govern-tx/lib/transactions/AbstractTransaction.ts @@ -70,7 +70,7 @@ export default abstract class AbstractTransaction extends AbstractAction { } /** - * TODO: Define response validation + * TODO: Test BigNumber handling of the response and the fastify schema validation * * Returns the schema of a transaction command * @@ -79,6 +79,29 @@ export default abstract class AbstractTransaction extends AbstractAction { * @returns {FastifySchema} */ public static get schema(): FastifySchema { - return super.schema + const schema = AbstractAction.schema + + schema.response = { + 200: { + type: 'object', + properties: { + to: { type: 'string' }, + from: { type: 'string' }, + contractAddress: { type: 'string' }, + transactionIndex: { type: 'number' }, + gasUsed: { type: 'object' }, // BigNumber + logsBloom: { type: 'string' }, + blockHash: { type: 'string' }, + transactionHash: { type: 'string' }, + logs: { type: 'array' }, + confirmations: { type: 'number' }, + cumulativeGasUsed: { type: 'object'}, // BigNumber + byzantium: { type: 'boolean' }, + status: { type: 'number' } + } + } + } + + return schema } } From 824d31bc0edf61cb3aff26e840b99dec59ca133d Mon Sep 17 00:00:00 2001 From: nivida Date: Tue, 24 Nov 2020 11:18:34 +0100 Subject: [PATCH 041/107] Configuration constructor improved and index.ts updated --- .../govern-tx/src/config/Configuration.ts | 61 ++++++++++++++++--- packages/govern-tx/src/index.ts | 44 ++++++------- 2 files changed, 74 insertions(+), 31 deletions(-) diff --git a/packages/govern-tx/src/config/Configuration.ts b/packages/govern-tx/src/config/Configuration.ts index 66064dfc2..ecc4709a1 100644 --- a/packages/govern-tx/src/config/Configuration.ts +++ b/packages/govern-tx/src/config/Configuration.ts @@ -29,23 +29,64 @@ export interface ServerOptions { logLevel?: string } +export interface Config { + ethereum: EthereumOptions, + database: DatabaseOptions, + auth: AuthOptions, + server: ServerOptions +} + // TODO: Add input validations for Ethereum addresses -// TODO: Change constructor to only have one required argument to pass export default class Configuration { /** - * @param {EthereumOptions} _ethereum - * @param {DatabaseOptions} _database - * @param {AuthOptions} _auth - * @param {ServerOptions} _server + * The options to connect to a Ethereum node and how TXs should be handled + * + * @property {EthereumOptions} _ethereum + * + * @private + */ + private _ethereum: EthereumOptions + + /** + * The options to connect to the Postgres database + * + * @property {DatabaseOptions} _database + * + * @private + */ + private _database: DatabaseOptions + + /** + * The options to configure the Authenticator used by fastify + * + * @property {AuthOptions} _auth + * + * @private + */ + private _auth: AuthOptions + + /** + * The options to configure fastify server + * + * @property {ServerOptions} _server + * + * @private + */ + private _server: ServerOptions + + /** + * @param {Config} config - The wrapper object for all configuration properties * * @constructor */ constructor( - private _ethereum: EthereumOptions, - private _database: DatabaseOptions, - private _auth: AuthOptions, - private _server: ServerOptions, - ) { } + config: Config + ) { + this.ethereum = config.ethereum + this.database = config.database + this.auth = config.auth + this.server = config.server + } /** * Getter for the database options. diff --git a/packages/govern-tx/src/index.ts b/packages/govern-tx/src/index.ts index 720d467ea..9fdfb0cec 100644 --- a/packages/govern-tx/src/index.ts +++ b/packages/govern-tx/src/index.ts @@ -4,27 +4,29 @@ import Bootstrap from './Bootstrap' new Bootstrap( new Configuration( { - publicKey: '0x0...', - contracts: { - GovernQueue: '0x0...' + ethereum: { + publicKey: '0x0...', + contracts: { + GovernQueue: '0x0...' + }, + url: 'localhost:8545', + blockConfirmations: 42 + }, + database: { + user: 'govern', + host: 'localhost', + password: 'dev', + database: 'govern', + port: 4000 }, - url: 'localhost:8545', - blockConfirmations: 42 - }, - { - user: 'govern', - host: 'localhost', - password: 'dev', - database: 'govern', - port: 4000 - }, - { - secret: 'secret', - cookieName: 'govern_cookie' - }, - { - host: '0.0.0.0', - port: 4040 - } + auth: { + secret: 'secret', + cookieName: 'govern_cookie' + }, + server: { + host: '0.0.0.0', + port: 4040 + } + } ) ).run() From 43dd4e23180503ef1b1ce3e1c84b8b8f51121581 Mon Sep 17 00:00:00 2001 From: nivida Date: Tue, 24 Nov 2020 11:22:30 +0100 Subject: [PATCH 042/107] types fixed and TODO's updated --- packages/govern-tx/lib/AbstractAction.ts | 2 +- packages/govern-tx/lib/whitelist/AbstractWhitelistAction.ts | 2 +- packages/govern-tx/src/provider/Provider.ts | 3 +-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/govern-tx/lib/AbstractAction.ts b/packages/govern-tx/lib/AbstractAction.ts index ca1c101f6..17f94a1bc 100644 --- a/packages/govern-tx/lib/AbstractAction.ts +++ b/packages/govern-tx/lib/AbstractAction.ts @@ -18,7 +18,7 @@ export default abstract class AbstractAction { * * @constructor */ - constructor(request: Request) { + constructor(request: Request | undefined) { this.request = this.validateRequest(request); } diff --git a/packages/govern-tx/lib/whitelist/AbstractWhitelistAction.ts b/packages/govern-tx/lib/whitelist/AbstractWhitelistAction.ts index 59064a07e..4f0b9776e 100644 --- a/packages/govern-tx/lib/whitelist/AbstractWhitelistAction.ts +++ b/packages/govern-tx/lib/whitelist/AbstractWhitelistAction.ts @@ -41,6 +41,6 @@ export default abstract class AbstractWhitelistAction extends AbstractAction { * @returns {FastifySchema} */ public static get schema(): FastifySchema { - return super.schema + return AbstractAction.schema } } diff --git a/packages/govern-tx/src/provider/Provider.ts b/packages/govern-tx/src/provider/Provider.ts index adebbfbeb..9f6bf7ecd 100644 --- a/packages/govern-tx/src/provider/Provider.ts +++ b/packages/govern-tx/src/provider/Provider.ts @@ -4,6 +4,7 @@ import Wallet from '../wallet/Wallet' import { EthereumOptions } from '../config/Configuration'; import ContractFunction from '../../lib/transactions/ContractFunction' +// TODO: Check populating of TX options export default class Provider { /** * The base provider of ethers.js @@ -51,8 +52,6 @@ export default class Provider { } /** - * TODO: Add gas price definition (could get loaded from ethgasstation to a have average gas price) - * * Returns the transaction options * * @method getTransactionOptions From f2b0288edc812b57f79f651c3d533e6d125a5f9b Mon Sep 17 00:00:00 2001 From: nivida Date: Mon, 30 Nov 2020 11:14:16 +0100 Subject: [PATCH 043/107] TODO's removed, Authenticator simplified, and dependencies updated --- .../lib/transactions/ContractFunction.ts | 2 - packages/govern-tx/package.json | 4 - packages/govern-tx/src/Bootstrap.ts | 8 - packages/govern-tx/src/auth/Authenticator.ts | 79 +----- yarn.lock | 249 ++++++------------ 5 files changed, 95 insertions(+), 247 deletions(-) diff --git a/packages/govern-tx/lib/transactions/ContractFunction.ts b/packages/govern-tx/lib/transactions/ContractFunction.ts index 9e380ae4e..5d91d68b3 100644 --- a/packages/govern-tx/lib/transactions/ContractFunction.ts +++ b/packages/govern-tx/lib/transactions/ContractFunction.ts @@ -18,8 +18,6 @@ export default class ContractFunction { private abiItem: Fragment; /** - * TODO: Define ABI item interface - * * @param {any} abiItem * @param {string} requestMsg * diff --git a/packages/govern-tx/package.json b/packages/govern-tx/package.json index d7f4c3127..1f03424b1 100644 --- a/packages/govern-tx/package.json +++ b/packages/govern-tx/package.json @@ -28,10 +28,6 @@ "@ethersproject/wallet": "^5.0.8", "@ethersproject/providers": "^5.0.15", "fastify": "^3.8.0", - "fastify-cookie": "^4.1.0", - "fastify-jwt": "^2.1.3", - "fastify-plugin": "^3.0.0", - "jsonwebtoken": "^8.5.1", "postgres": "^1.0.2" } } diff --git a/packages/govern-tx/src/Bootstrap.ts b/packages/govern-tx/src/Bootstrap.ts index 2e8f3607f..70d814f31 100644 --- a/packages/govern-tx/src/Bootstrap.ts +++ b/packages/govern-tx/src/Bootstrap.ts @@ -94,8 +94,6 @@ export default class Bootstrap { } /** - * TODO: Could be done cleaner but don't think it is necessary - * * Register all transaction related routes * * @method registerTransactionRoutes @@ -131,8 +129,6 @@ export default class Bootstrap { } /** - * TODO: Could be done cleaner but don't think it is necessary - * * Register all whitelist related routes * * @method registerWhitelistRoutes @@ -226,12 +222,8 @@ export default class Bootstrap { this.authenticator = new Authenticator( - this.server, this.whitelist, admin, - this.config.auth.secret, - this.config.auth.cookieName, - this.config.auth.jwtOptions ) this.server.addHook( diff --git a/packages/govern-tx/src/auth/Authenticator.ts b/packages/govern-tx/src/auth/Authenticator.ts index 60ade332a..21c968f7d 100644 --- a/packages/govern-tx/src/auth/Authenticator.ts +++ b/packages/govern-tx/src/auth/Authenticator.ts @@ -1,17 +1,10 @@ -import fastify, { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; -import jwt, {SignOptions, VerifyOptions} from 'jsonwebtoken' -import {verifyMessage} from '@ethersproject/wallet'; -import {arrayify} from '@ethersproject/bytes' +import { FastifyRequest, FastifyReply } from 'fastify'; +import { verifyMessage } from '@ethersproject/wallet'; +import { arrayify } from '@ethersproject/bytes' import { Unauthorized, HttpError } from 'http-errors' -import fastifyCookie from 'fastify-cookie' import Whitelist from '../db/Whitelist' import Admin from '../db/Admin'; -export interface JWTOptions { - sign: SignOptions, - verify: VerifyOptions -} - export default class Authenticator { /** * @property {HttpError} NOT_ALLOWED @@ -21,25 +14,12 @@ export default class Authenticator { private NOT_ALLOWED: HttpError = new Unauthorized('Not allowed action!') /** - * @param {FastifyInstance} fastify * @param {Whitelist} whitelist * @param {Admin} admin - * @param {string} secret - * @param {string} cookieName - * @param {JWTOptions} jwtOptions * * @constructor */ - constructor( - private fastify: FastifyInstance, - private whitelist: Whitelist, - private admin: Admin, - private secret: string, - private cookieName: string, - private jwtOptions?: JWTOptions - ) { - fastify.register(fastifyCookie) - } + constructor(private whitelist: Whitelist, private admin: Admin) { } /** * Checks if the given public key is existing and if this account is allowed to execute the requested action @@ -54,28 +34,16 @@ export default class Authenticator { * @public */ public async authenticate(request: FastifyRequest, reply: FastifyReply): Promise { - const cookie = request.cookies[this.cookieName]; - const publicKey: string = verifyMessage(arrayify(request.body.message), request.body.signature); // TODO: Fix type definition - - if (cookie && this.verify(cookie)) { - if (!(await this.hasPermission(request.routerPath, publicKey))) { - throw this.NOT_ALLOWED - } - - return - } - - if (await this.hasPermission(request.routerPath, publicKey)) { - reply.setCookie( - this.cookieName, - jwt.sign( - {data: publicKey}, - this.secret, - this.jwtOptions.sign - ), - {secure: true} + // TODO: Fix type definition + if ( + await this.hasPermission( + request.routerPath, + verifyMessage( + arrayify(request.body.message), + request.body.signature + ) ) - + ) { return } @@ -108,25 +76,4 @@ export default class Authenticator { return false } - - /** - * Verify the given JWT - * - * @method verify - * - * @param {string} token - * - * @returns {boolean} - * - * @private - */ - private verify(token: string): boolean { - try { - jwt.verify(token, this.secret, this.jwtOptions.verify) - - return true; - } catch(error) { - return false; - } - } } diff --git a/yarn.lock b/yarn.lock index d8724490a..3580878ef 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1496,6 +1496,17 @@ "@ethersproject/rlp" "^5.0.3" bn.js "^4.4.0" +"@ethersproject/address@^5.0.7": + version "5.0.8" + resolved "https://registry.yarnpkg.com/@ethersproject/address/-/address-5.0.8.tgz#0c551659144a5a7643c6bea337149d410825298f" + integrity sha512-V87DHiZMZR6hmFYmoGaHex0D53UEbZpW75uj8AqPbjYUmi65RB4N2LPRcJXuWuN2R0Y2CxkvW6ArijWychr5FA== + dependencies: + "@ethersproject/bignumber" "^5.0.10" + "@ethersproject/bytes" "^5.0.4" + "@ethersproject/keccak256" "^5.0.3" + "@ethersproject/logger" "^5.0.5" + "@ethersproject/rlp" "^5.0.3" + "@ethersproject/base64@5.0.4", "@ethersproject/base64@^5.0.3": version "5.0.4" resolved "https://registry.yarnpkg.com/@ethersproject/base64/-/base64-5.0.4.tgz#b0d8fdbf3dda977cf546dcd35725a7b1d5256caa" @@ -1520,13 +1531,29 @@ "@ethersproject/logger" "^5.0.5" bn.js "^4.4.0" -"@ethersproject/bytes@5.0.5", "@ethersproject/bytes@>=5.0.0-beta.129", "@ethersproject/bytes@^5.0.4", "@ethersproject/bytes@^5.0.5": +"@ethersproject/bignumber@^5.0.10": + version "5.0.12" + resolved "https://registry.yarnpkg.com/@ethersproject/bignumber/-/bignumber-5.0.12.tgz#fe4a78667d7cb01790f75131147e82d6ea7e7cba" + integrity sha512-mbFZjwthx6vFlHG9owXP/C5QkNvsA+xHpDCkPPPdG2n1dS9AmZAL5DI0InNLid60rQWL3MXpEl19tFmtL7Q9jw== + dependencies: + "@ethersproject/bytes" "^5.0.8" + "@ethersproject/logger" "^5.0.5" + bn.js "^4.4.0" + +"@ethersproject/bytes@5.0.5", "@ethersproject/bytes@>=5.0.0-beta.129", "@ethersproject/bytes@^5.0.4": version "5.0.5" resolved "https://registry.yarnpkg.com/@ethersproject/bytes/-/bytes-5.0.5.tgz#688b70000e550de0c97a151a21f15b87d7f97d7c" integrity sha512-IEj9HpZB+ACS6cZ+QQMTqmu/cnUK2fYNE6ms/PVxjoBjoxc6HCraLpam1KuRvreMy0i523PLmjN8OYeikRdcUQ== dependencies: "@ethersproject/logger" "^5.0.5" +"@ethersproject/bytes@^5.0.6", "@ethersproject/bytes@^5.0.8": + version "5.0.8" + resolved "https://registry.yarnpkg.com/@ethersproject/bytes/-/bytes-5.0.8.tgz#cf1246a6a386086e590063a4602b1ffb6cc43db1" + integrity sha512-O+sJNVGzzuy51g+EMK8BegomqNIg+C2RO6vOt0XP6ac4o4saiq69FnjlsrNslaiMFVO7qcEHBsWJ9hx1tj1lMw== + dependencies: + "@ethersproject/logger" "^5.0.5" + "@ethersproject/constants@5.0.5", "@ethersproject/constants@>=5.0.0-beta.128", "@ethersproject/constants@^5.0.4": version "5.0.5" resolved "https://registry.yarnpkg.com/@ethersproject/constants/-/constants-5.0.5.tgz#0ed19b002e8404bdf6d135234dc86a7d9bcf9b71" @@ -1660,6 +1687,31 @@ bech32 "1.1.4" ws "7.2.3" +"@ethersproject/providers@^5.0.15": + version "5.0.17" + resolved "https://registry.yarnpkg.com/@ethersproject/providers/-/providers-5.0.17.tgz#f380e7831149e24e7a1c6c9b5fb1d6dfc729d024" + integrity sha512-bJnvs5X7ttU5x2ekGJYG7R3Z+spZawLFfR0IDsbaMDLiCwZOyrgk+VTBU7amSFLT0WUhWFv8WwSUB+AryCQG1Q== + dependencies: + "@ethersproject/abstract-provider" "^5.0.4" + "@ethersproject/abstract-signer" "^5.0.4" + "@ethersproject/address" "^5.0.4" + "@ethersproject/basex" "^5.0.3" + "@ethersproject/bignumber" "^5.0.7" + "@ethersproject/bytes" "^5.0.4" + "@ethersproject/constants" "^5.0.4" + "@ethersproject/hash" "^5.0.4" + "@ethersproject/logger" "^5.0.5" + "@ethersproject/networks" "^5.0.3" + "@ethersproject/properties" "^5.0.3" + "@ethersproject/random" "^5.0.3" + "@ethersproject/rlp" "^5.0.3" + "@ethersproject/sha2" "^5.0.3" + "@ethersproject/strings" "^5.0.4" + "@ethersproject/transactions" "^5.0.5" + "@ethersproject/web" "^5.0.6" + bech32 "1.1.4" + ws "7.2.3" + "@ethersproject/random@5.0.4", "@ethersproject/random@^5.0.3": version "5.0.4" resolved "https://registry.yarnpkg.com/@ethersproject/random/-/random-5.0.4.tgz#98f7cf65b0e588cec39ef24843e391ed5004556f" @@ -1739,7 +1791,7 @@ "@ethersproject/constants" "^5.0.4" "@ethersproject/logger" "^5.0.5" -"@ethersproject/wallet@5.0.7", "@ethersproject/wallet@^5.0.7": +"@ethersproject/wallet@5.0.7": version "5.0.7" resolved "https://registry.yarnpkg.com/@ethersproject/wallet/-/wallet-5.0.7.tgz#9d4540f97d534e3d61548ace30f15857209b3f02" integrity sha512-n2GX1+2Tc0qV8dguUcLkjNugINKvZY7u/5fEsn0skW9rz5+jHTR5IKMV6jSfXA+WjQT8UCNMvkI3CNcdhaPbTQ== @@ -1760,6 +1812,27 @@ "@ethersproject/transactions" "^5.0.5" "@ethersproject/wordlists" "^5.0.4" +"@ethersproject/wallet@^5.0.8": + version "5.0.9" + resolved "https://registry.yarnpkg.com/@ethersproject/wallet/-/wallet-5.0.9.tgz#976c7d950489c40308d676869d24e59ab7b82ad1" + integrity sha512-GfpQF56PO/945SJq7Wdg5F5U6wkxaDgkAzcgGbCW6Joz8oW8MzKItkvYCzMh+j/8gJMzFncsuqX4zg2gq3J6nQ== + dependencies: + "@ethersproject/abstract-provider" "^5.0.4" + "@ethersproject/abstract-signer" "^5.0.4" + "@ethersproject/address" "^5.0.4" + "@ethersproject/bignumber" "^5.0.7" + "@ethersproject/bytes" "^5.0.4" + "@ethersproject/hash" "^5.0.4" + "@ethersproject/hdnode" "^5.0.4" + "@ethersproject/json-wallets" "^5.0.6" + "@ethersproject/keccak256" "^5.0.3" + "@ethersproject/logger" "^5.0.5" + "@ethersproject/properties" "^5.0.3" + "@ethersproject/random" "^5.0.3" + "@ethersproject/signing-key" "^5.0.4" + "@ethersproject/transactions" "^5.0.5" + "@ethersproject/wordlists" "^5.0.4" + "@ethersproject/web@5.0.9", "@ethersproject/web@^5.0.6": version "5.0.9" resolved "https://registry.yarnpkg.com/@ethersproject/web/-/web-5.0.9.tgz#b08f8295f4bfd4777c8723fe9572f5453b9f03cb" @@ -4076,13 +4149,6 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4= -"@types/jsonwebtoken@^8.3.2": - version "8.5.0" - resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-8.5.0.tgz#2531d5e300803aa63279b232c014acf780c981c5" - integrity sha512-9bVao7LvyorRGZCw0VmH/dr7Og+NdjYSsKAxB43OQoComFbBgsEpoR9JW6+qSq/ogwVBg8GI2MfAlk4SYI4OLg== - dependencies: - "@types/node" "*" - "@types/keygrip@*": version "1.0.2" resolved "https://registry.yarnpkg.com/@types/keygrip/-/keygrip-1.0.2.tgz#513abfd256d7ad0bf1ee1873606317b33b1b2a72" @@ -7032,11 +7098,6 @@ buffer-crc32@~0.2.3: resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" integrity sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI= -buffer-equal-constant-time@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" - integrity sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk= - buffer-fill@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/buffer-fill/-/buffer-fill-1.0.0.tgz#f8f78b76789888ef39f205cd637f68e702122b2c" @@ -8100,11 +8161,6 @@ cookie-signature@1.0.6: resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw= -cookie-signature@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.1.0.tgz#cc94974f91fb9a9c1bb485e95fc2b7f4b120aff2" - integrity sha512-Alvs19Vgq07eunykd3Xy2jF0/qSNv2u7KDbAek9H5liV1UMijbqFs5cycZvv5dVsvseT/U4H8/7/w8Koh35C4A== - cookie@0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba" @@ -9360,13 +9416,6 @@ eccrypto-js@5.2.0: randombytes "2.1.0" secp256k1 "3.8.0" -ecdsa-sig-formatter@1.0.11: - version "1.0.11" - resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" - integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== - dependencies: - safe-buffer "^5.0.1" - ee-first@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" @@ -11066,50 +11115,11 @@ fast-safe-stringify@^2.0.6, fast-safe-stringify@^2.0.7: resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz#124aa885899261f68aedb42a7c080de9da608743" integrity sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA== -fastfall@^1.5.0: - version "1.5.1" - resolved "https://registry.yarnpkg.com/fastfall/-/fastfall-1.5.1.tgz#3fee03331a49d1d39b3cdf7a5e9cd66f475e7b94" - integrity sha1-P+4DMxpJ0dObPN96XpzWb0dee5Q= - dependencies: - reusify "^1.0.0" - -fastify-cookie@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/fastify-cookie/-/fastify-cookie-4.1.0.tgz#b063f2b97cf9de7a33eb799a951b604ede48a915" - integrity sha512-+se+kNPFDE49JCiBYQrfPfchcW9s8BXjCqP5ijuYpbydTcMlo9pnyd8tyxLi43SIXBmeTBkB1CjklmD7nlHsXg== - dependencies: - cookie "^0.4.0" - cookie-signature "^1.1.0" - fastify-plugin "^2.0.0" - fastify-error@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/fastify-error/-/fastify-error-0.2.0.tgz#9a1c28d4f42b6259e7a549671c8e5e2d85660634" integrity sha512-zabxsBatj59ROG0fhP36zNdc5Q1/eYeH9oSF9uvfrurZf8/JKfrJbMcIGrLpLWcf89rS6L91RHWm20A/X85hcA== -fastify-jwt@^2.1.3: - version "2.1.3" - resolved "https://registry.yarnpkg.com/fastify-jwt/-/fastify-jwt-2.1.3.tgz#d016a2f6a810299edc72ad79ce2949fb88a20604" - integrity sha512-8732zt+7UA9JzeRebJFCH+56laMCAxq/Wyou6pzXZzEWcPGPLcRqCk+R0CcgwjjVToo6wLIeNNKHFegyemyHug== - dependencies: - "@types/jsonwebtoken" "^8.3.2" - fastify-plugin "^2.0.0" - http-errors "^1.7.1" - jsonwebtoken "^8.3.0" - steed "^1.1.3" - -fastify-plugin@^2.0.0: - version "2.3.4" - resolved "https://registry.yarnpkg.com/fastify-plugin/-/fastify-plugin-2.3.4.tgz#b17abdc36a97877d88101fb86ad8a07f2c07de87" - integrity sha512-I+Oaj6p9oiRozbam30sh39BiuiqBda7yK2nmSPVwDCfIBlKnT8YB3MY+pRQc2Fcd07bf6KPGklHJaQ2Qu81TYQ== - dependencies: - semver "^7.3.2" - -fastify-plugin@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/fastify-plugin/-/fastify-plugin-3.0.0.tgz#cf1b8c8098e3b5a7c8c30e6aeb06903370c054ca" - integrity sha512-ZdCvKEEd92DNLps5n0v231Bha8bkz1DjnPP/aEz37rz/q42Z5JVLmgnqR4DYuNn3NXAO3IDCPyRvgvxtJ4Ym4w== - fastify-warning@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/fastify-warning/-/fastify-warning-0.2.0.tgz#e717776026a4493dc9a2befa44db6d17f618008f" @@ -11137,21 +11147,6 @@ fastify@^3.8.0: semver "^7.3.2" tiny-lru "^7.0.0" -fastparallel@^2.2.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/fastparallel/-/fastparallel-2.4.0.tgz#65fbec1a5e5902494be772cf5765cbaaece08688" - integrity sha512-sacwQ7wwKlQXsa7TN24UvMBLZNLmVcPhmxccC9riFqb3N+fSczJL8eWdnZodZ/KijGVgNBBfvF/NeXER08uXnQ== - dependencies: - reusify "^1.0.4" - xtend "^4.0.2" - -fastq@^1.3.0, fastq@^1.6.1: - version "1.9.0" - resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.9.0.tgz#e16a72f338eaca48e91b5c23593bcc2ef66b7947" - integrity sha512-i7FVWL8HhVY+CTkwFxkN2mk3h+787ixS5S63eb78diVRc1MCssarHq3W5cj0av7YDSwmaV928RNag+U1etRQ7w== - dependencies: - reusify "^1.0.4" - fastq@^1.6.0: version "1.8.0" resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.8.0.tgz#550e1f9f59bbc65fe185cb6a9b4d95357107f481" @@ -11159,13 +11154,12 @@ fastq@^1.6.0: dependencies: reusify "^1.0.4" -fastseries@^1.7.0: - version "1.7.2" - resolved "https://registry.yarnpkg.com/fastseries/-/fastseries-1.7.2.tgz#d22ce13b9433dff3388d91dbd6b8bda9b21a0f4b" - integrity sha1-0izhO5Qz3/M4jZHb1ri9qbIaD0s= +fastq@^1.6.1: + version "1.9.0" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.9.0.tgz#e16a72f338eaca48e91b5c23593bcc2ef66b7947" + integrity sha512-i7FVWL8HhVY+CTkwFxkN2mk3h+787ixS5S63eb78diVRc1MCssarHq3W5cj0av7YDSwmaV928RNag+U1etRQ7w== dependencies: - reusify "^1.0.0" - xtend "^4.0.0" + reusify "^1.0.4" faye-websocket@^0.10.0: version "0.10.0" @@ -12655,7 +12649,7 @@ http-errors@1.7.3, http-errors@~1.7.2: statuses ">= 1.5.0 < 2" toidentifier "1.0.0" -http-errors@^1.7.1, http-errors@^1.7.3: +http-errors@^1.7.3: version "1.8.0" resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.8.0.tgz#75d1bbe497e1044f51e4ee9e704a62f28d336507" integrity sha512-4I8r0C5JDhT5VkvI47QktDW75rNlGVsUf/8hzjCC/wkWI/jdTRmBb9aI7erSG82r1bjKY3F6k28WnsVxB1C73A== @@ -14956,22 +14950,6 @@ jsonschema@^1.2.4: resolved "https://registry.yarnpkg.com/jsonschema/-/jsonschema-1.4.0.tgz#1afa34c4bc22190d8e42271ec17ac8b3404f87b2" integrity sha512-/YgW6pRMr6M7C+4o8kS+B/2myEpHCrxO4PEWnqJNBFMjn7EWXqlQ4tGwL6xTHeRplwuZmcAncdvfOad1nT2yMw== -jsonwebtoken@^8.3.0, jsonwebtoken@^8.5.1: - version "8.5.1" - resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d" - integrity sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w== - dependencies: - jws "^3.2.2" - lodash.includes "^4.3.0" - lodash.isboolean "^3.0.3" - lodash.isinteger "^4.0.4" - lodash.isnumber "^3.0.3" - lodash.isplainobject "^4.0.6" - lodash.isstring "^4.0.1" - lodash.once "^4.0.0" - ms "^2.1.1" - semver "^5.6.0" - jsprim@^1.2.2: version "1.4.1" resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" @@ -15008,23 +14986,6 @@ just-map-keys@^1.1.0: resolved "https://registry.yarnpkg.com/just-map-keys/-/just-map-keys-1.1.0.tgz#9663c9f971ba46e17f2b05e66fec81149375f230" integrity sha512-oNKi+4y7fr8lXnhKYpBbCkiwHRVkAnx0VDkCeTDtKKMzGr1Lz1Yym+RSieKUTKim68emC5Yxrb4YmiF9STDO+g== -jwa@^1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a" - integrity sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA== - dependencies: - buffer-equal-constant-time "1.0.1" - ecdsa-sig-formatter "1.0.11" - safe-buffer "^5.0.1" - -jws@^3.2.2: - version "3.2.2" - resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304" - integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA== - dependencies: - jwa "^1.4.1" - safe-buffer "^5.0.1" - keccak256@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/keccak256/-/keccak256-1.0.1.tgz#f579e937d6f32ac4ab62ff862d50204f775bb6f6" @@ -15553,41 +15514,11 @@ lodash.get@^4.4.2: resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk= -lodash.includes@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" - integrity sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8= - -lodash.isboolean@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" - integrity sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY= - -lodash.isinteger@^4.0.4: - version "4.0.4" - resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" - integrity sha1-YZwK89A/iwTDH1iChAt3sRzWg0M= - lodash.ismatch@^4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz#756cb5150ca3ba6f11085a78849645f188f85f37" integrity sha1-dWy1FQyjum8RCFp4hJZF8Yj4Xzc= -lodash.isnumber@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc" - integrity sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w= - -lodash.isplainobject@^4.0.6: - version "4.0.6" - resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" - integrity sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs= - -lodash.isstring@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" - integrity sha1-1SfftUVuynzJu5XV2ur4i6VKVFE= - lodash.kebabcase@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz#8489b1cb0d29ff88195cceca448ff6d6cc295c36" @@ -15613,11 +15544,6 @@ lodash.merge@^4.6.2: resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== -lodash.once@^4.0.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" - integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w= - lodash.pad@^4.5.1: version "4.5.1" resolved "https://registry.yarnpkg.com/lodash.pad/-/lodash.pad-4.5.1.tgz#4330949a833a7c8da22cc20f6a26c4d59debba70" @@ -20074,7 +20000,7 @@ retry@^0.10.0: resolved "https://registry.yarnpkg.com/retry/-/retry-0.10.1.tgz#e76388d217992c252750241d3d3956fed98d8ff4" integrity sha1-52OI0heZLCUnUCQdPTlW/tmNj/Q= -reusify@^1.0.0, reusify@^1.0.4: +reusify@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== @@ -21176,17 +21102,6 @@ stealthy-require@^1.1.1: resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b" integrity sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks= -steed@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/steed/-/steed-1.1.3.tgz#f1525dd5adb12eb21bf74749537668d625b9abc5" - integrity sha1-8VJd1a2xLrIb90dJU3Zo1iW5q8U= - dependencies: - fastfall "^1.5.0" - fastparallel "^2.2.0" - fastq "^1.3.0" - fastseries "^1.7.0" - reusify "^1.0.0" - store@2.0.12: version "2.0.12" resolved "https://registry.yarnpkg.com/store/-/store-2.0.12.tgz#8c534e2a0b831f72b75fc5f1119857c44ef5d593" From c49dc62a8e2d4a0af033ffacb7bca1b03bc437b4 Mon Sep 17 00:00:00 2001 From: nivida Date: Mon, 30 Nov 2020 12:23:25 +0100 Subject: [PATCH 044/107] DB diagramm, README, and package.json commands added --- packages/govern-tx/README.md | 66 +++++++++++++++++++++++++ packages/govern-tx/assets/db_model.png | Bin 0 -> 132019 bytes packages/govern-tx/package.json | 5 ++ 3 files changed, 71 insertions(+) create mode 100644 packages/govern-tx/README.md create mode 100644 packages/govern-tx/assets/db_model.png diff --git a/packages/govern-tx/README.md b/packages/govern-tx/README.md new file mode 100644 index 000000000..1d1113033 --- /dev/null +++ b/packages/govern-tx/README.md @@ -0,0 +1,66 @@ +# Govern TX + +The govern-tx package does include the transaction service of the govern project. +This service can get deployed on any server or cloud infrastructure who has nodejs installed. + +## Commands + +- ``yarn dev`` - Starts the docker containers and the server in dev mode +- ``yarn start`` - Starts the docker containers and the server in production mode +- ``yarn start:server`` - Starts the server without starting the docker containers +- ``yarn start:containers`` - Starts the docker containers +- ``yarn stop:containers`` - Stops the docker containers +- ``yarn test`` - Does execute the unit and e2e tests of this package + +## Postgres DB diagramm + +![DB Diagramm](./assets/db_model.png) + +## Directory Structure + +Overall are the thoughts to have a modularized structure with the separation between abstract classes and concrete implementations with ``lib`` and ``src``. +Means ``lib`` and ``src`` should (If abstraction is required for all module) have the same base structure. This is important to increase the readability. + +``` +# All concrete implementations are located in the ./src/ folder +./src/ + +# Authentication handler used in the fastify pre-handler +./src/auth + +# Configratuon object with the required validations of the input +./src/config + +# DB wrapper and entities used in the server actions +./src/db + +# Provider class used to connect to a Ethereum node +./src/provider + +# All transactions related action commands +./src/transactions + +# The wallet we use to sign the transactions +./src/wallet + +# All whitelist related CRUD action commands +./src/whitelist + +# This class is used to boot the entire server and is used in ./src/index.ts +./Bootstrap.ts + +# All abstract classes and helpers are located in the ./lib/ folder +./lib/ + +# Base abstraction for all actions +./lib/AbstractAction.ts + +# Base abstraction for all transaction related actions and VO for contract function encoding +./lib/transactions + +# Base abstraction for all whiteliste related CRUD actions +./lib/whitelist + +# Home of all tests (e2e & unit) +./test +``` diff --git a/packages/govern-tx/assets/db_model.png b/packages/govern-tx/assets/db_model.png new file mode 100644 index 0000000000000000000000000000000000000000..692ee2f1407605f0b04b549c23f596c6c878d3f8 GIT binary patch literal 132019 zcmeEuhd-75|Gz|vj0TE~l08n6WFJYAjErmw*(3AVqM?Xmgk(mt$sWfcWrQ-a9h|cF zF%HM}dtXQQ{rTMY@ALUQzJI{ov~_W_iMkNuh(^6s;eqeQ!-JKkdRO--MXPc zLUII8LPEB2^a%K-iIKC1gyguXwYtxzkO6N@UfPh*7cLOc!asg=q34behw;}ZVk>4u9%*s{Hjk^h~inV(N(|O z+Ym?{Eq4Bpk?(}})b?asvWVrL=4BV$aT19IpEv>99&&aQeRd@B=z~xd6|)FdQo+ZE zn3hN70C(NXqtr{u6_(>?4PSC0F^|=KYr}X;xM@ z2cGCA>ePnIb5iQD`Va2wz1^pTy~+mEZj*EU)bm~!YE*5osX%m zAEM%U&`K5srwK1{??Kd=%tk!=MByAxedCjM+SxY|KV&aFYbR4D+Y7u!cKXTc^({q( z6uMbiYMUN4o$wx6{eD>vMybGSY&Qa=HVg)2l_jPH&q_UIYDSEZU+R_W*D}M-T~j}m z*U3ZSp(k-Trv8#Ck8tD}RoBld%kUvKxR71HDQ&+0iO8>_eW)aSJ%t~sup z`}Q;Et?fLI!|B(iH;#R{<|nC%(Zkq=D`_eEssak((@+rhet-Y$C{r>MdEgrMP;<3wJLfl+^T%L zUj>kMc9ZrT8B>i^iBc|VAQzz7p?sk$m36;gJq8tzVkXIU4Bir53m(|dgTJw?nGQBN zOvgSyHz&EFz(`HNJ5v3;>W^2LdUU3S<`u~`Z7%f|svZf$HTF;C0W?E*xjvEhlPJE4 zkX$;l$A0E%z+HBRi-B5~4)y9C?<+oc=a9%JLv_-YPnSP3J1C_)$q&a<Kq} zuK2xYJ*J^c$u&<)&5ZiJ6JCfsVpyc?dvr1BDb-T*hJcT74GlJ+@0Z{P=ML*gs6SW7 zF$*%rK(+wIGs<7N?wo&3*K!PVbo*G-FNaz7>hmXG-bp>&NwL_dWlG~D;Uw&I*h#GL zW&ced`26{ZTU-|zHAr}4>bdE|Z{M<2Lfull#i+#c9$_w6#5s_7PF0mdxw&E7c|~>Q z!HU9)nKMiw)>LshtzP5lYwOqPuk&8tZ1KEJksh0^=$3Ls{Zy8CcV#!Fjo{j~wX18< zF)=ZvF$KM61Yh=^%cIPb*3T$ev_@ylbR}3D>Px678C=Jx+NKVtpi{eM*75hfn9z@F zo*Jvj-mF#j&-mWK`r&&QgH3yHes_LXP3Ep)P^wwRm(I8p&G(x5#cEU4sbQFGJ?F;_d*6LdMG+USdr%z|D9v71T z^ror5AY5RhjGif+ojU;42s?_2JLyw~^Q^Yrt)@>p`CazpYE z2H3)PgCFgS?CuR*!MI}d2gAw|9~hW;V4zPAdt+vi~NPHb`94CP_% zyf&JSDn@-m|JW^}80HuIG)js7+x*=E2in?ei zpBCghZ)4QGYc*x%={jcRiqGuiSHy-DhD0=WHwEe%&$skTI?*^qjrp<3pF1V?!TDR- z#))N-Q8lhhC%L%h?|4L=iLAJ?d@s^0@^Y5A9=Eu3rNpNFZL5~x*XxB}n%d5JU+uY~ z9XS|&^R}`oCZ<)#2%ik>zL;$8kO$2ugMj#C*cv1g(71d zvN0wNxaRSP`R(%zkH~Gw+Mm{a*8Z$|_p#NpX{USe-r!vBfkOlQ#gYwjqs~sTPxC*57kt^t{&bT-+8rjSi{#kzvN+L!~3kLL35#^lX~0cwaq!)h}N`m z%;^`V)v513k+6=m^Rh%!y(Ul|rsAiU%O;rCY`F-sRuTw=Q^* zZ>4LCavvB(&ehN5YdzOmG)a}5e#$K5aC}4;)$F4=@PSrV$*-zL+|EkdurDH1E!_ScTi|}q=v&!QMpvjZB4Z}QQ5YG>7mM0KHiAY@7Qko zyWUdwRfoyNBI)}`=k_iW5kGX5n2qDA%zDircHYmRHo362geEKgUY`*x-_h}wy_Mvp zS-4A@|JWWjXt!oJrKhe%{&`Rg>j%~({|el#&WI!~JElT?1M&+9YW$shxh|{S&))H_cj`denxJ&&?mlRWG>? zb%j|LS)z?N{bzPd_bSI$v3oRo+lGYx#W2PdMqe0X?Pbr>-O{PK$)t)Tl;a8(UnA&i zx#G~|y}ljArXeG>n79&wFC2~P%4lrAo9sxKAgncSIQ#A2C3!;qy@mAWggeP}7Re={ zPpe<9ttA$i(5*?AvIXov?IC>jTRxJ);GR7uQGV>P^v8krfT22)%&^&)btG#`XY82n zntN8{R9Y=H%6s(n5n9fXXmF4ukNf(fnYXrN>LmHx4?q6UJd65p`^A#OlIBNBBVmTN zL)aHvtXtY)*mXF40QW}OKBGz1i{Wv(k1?R=2!5!mWTB!$!Ux_TB_TV+L_!YU9Rhzc zhnW9-fAbIz$zkI6q$DIk)+A)V&rt=h(7$l-2VHaUdN}eq2?h9c68w3jkp6M@5q!$w zKi-pVfMXyS$_GLv~T2D?*np$WXGgvr9RfTS#c!xcU2X@JafDm87XVV5rpf-?kNJRMvgcnCVUa2zc1$2vD2x|lgzKXSEpbYO?pePHV7 z<|=*R0(7H4{~gfz(8Ky)cXDv~y)Cdo7<2?CEOZI>=i1;>Dd<}Xb!(4@cDgsL?E%lg zJ!FK%u3eHMUhw}N`qwQFF4cB<=q&GO53Y2T`4`uJU;OU}|9&Cyo_haw&&y($|NWl- zcIfw|QZQ)m|Hg|0MiajUoR*=Kg8eBq8A|_{rc@xvbJjQ1w7@G+Gw2_w5BQJg;1#?- zB=Mk#ql=n^1WuxKLr%-%(EKn(^*L=c#gaeedFcSMqfal89*W@NIZAut{WD`SIXK-h zjVGq^bV=+gcaN(D2c|sDfBb_?>#Z)lB~3wHpB-*>{(J!ZmainCvJZtIjLeUqPzVcx zWU|`GVuHBKWCBjFS#sx1obl@9cZ1VcNy(1VvBMve9QsdhG7fAQU~a`e;*$S`nxpugBme98dtG=+>}YeR=>OU>sdVjsWa%FZ|HG0$IPs?f z{=<@gSaJyXl79^6Z!YW~mi&K)@efP>VaY#1_^%1fKY7XD{3hAae^~MlOURD?hlD=h z1AV{Oj#|9?{1H`R)$!Ko{{4MFr&U^7Yxg5#?Jpj~VH~H@WVfr$)XWg90MMnh}X-*92$XQPJr_+)L@h%N`>6>)uj4!PdS@5^f&wgUrkCu--|!>4(;3_~^g- z8M@4DKtXl_(?KN`2c2b8^|q#%6#ZfU7|Gvg_|DJXVLMVO=1kl(RG@v01Z-0-PA~W? z0gvI@fCBA_?lT9>eGSgi?CQZM(oc)-s*(JS1}kI0-PJa91d+Rk0+O)s7&0<1y|;qC zoPR42v-#LNa`-YDi4hb20h|^0Hva=I7hW*qPfGIFI0|2-Jc@sdSPV@d-Vf58qKoVu ztv~E$`2SYVscQoYw8cel66JH56P(rUD(Y)V?xnJSisWxJyw!oH^i{h!juZC`Pni-6 z=LDC>SSXti#rUt?1!MyXvRkr*iTn!Z1!sNukTWC6-(kCz3{Zo=Fs-8+$aFH%n}eHp zKadp#2pl_#FS=r|b^C8>qooZ`(a}~7Jpsul;0`$JvZ#>?RuAatJ{iehF@S(cpg5wq zA!c#no`LF8|D(G9s4iqG{|D8L3vToxEYkDC%C%nUdB&Z&>H)tKFD_~9J(u`qWf1>n zcW)?FhK8kEb|hXzY!arc>w4!9q-R&kfLke?D(}3>-CKPH;Z=VVegm==Q>4kgnF8>R&x8}*Wvd%y)jbyE*S+zXPW%{T4@fWVF}I( zi@x3>EjDVL5I?^HCc`kReRgQZX;I88X4v28>;TTvHvQ{(tXlr|yN$XCl@>h zLhY$qMw56}iX`WUxmBNERN&w52Df?JPR{qQVrqH#bchZplowq6&C|a7?lC-*n?-2I zzx*2UKDxP&;f{qjvhp7-sWLY)B1Ybuv+Wa1U`^JwJMtIL4^wgkHQcn#JEK;Mx7e9$ z5YwM!L*ZhUXQ*|M>Ccb9xQO@Oo3LIkX5M37im7p;8wDj5*r# z3yCi7-}aRVJgUTm(rz7&rAl6zM<+WC<+nQeW^-{noreB3^(+=Pk}L^h5c(#K>>_$| z72wQmvuE0N?vs1PC0{@Mw~$6w#n-_TF!J@+6SMk{0n00Mj!pC8E~u7HuaV3X@@8K@ zadLRxm>BY;(r>k0e)uU;xxOVWCb(G>o4uiL$x>=-SJcL*Fzf}h{P?Aiks-P-_5Nmr zyxGRjL(7e?5C)4!avMZTlptZ3o}ioVY1Gd7b6(h7eZLn`&pBS1q1Z^VI@$Ev@C+f|n-z({?OW#Oj-lEk*J*~=qAC?N@=q&W z>#0GLQ!zHa(!(`S`+Hr;uN;ebBj&$+2rHVwdiwA-?CuiPwIy$zMhvc+t$8hMYkqRM zP&myDU-X6J7=Fdzhr6@RaC)sbyTe|q>`DWtEMj0M%?WRu9K2`W=4ep3JM39dEiMvv zZEi4z&%oCw^qPCJT6x~c$}U=b)f5}W#SmRC*l(M&Hs*ry5qLgvy_z$2I-Xe#lh!#J zMHb9rkC4XSNm$L1b#v7vhNJGgAdo5A#m++R0`rvV@4_kK3onH`CicW2MUegC&nK@} zJC|P!$4t!0!a&yaHb;P2_UrIOOtoTuf}r(Wlg=Z0uwFQk-qie%kIOb!>K>>GEH9wO0s%LsP0bS zl+SJMr)mU@*gAd>mOC7i%k#n;xDhAy#kQI<%$g3m(_kt3^LjdR{3Ne>@-AHdd2RW!N*H7mFs*?pJ>=%XEivfrB(`J z$8x52$};mB$i0+~dTiR$Htz0w2;Vbw_wX^R%^Q_(g&B3LYFu*M3+%VpA%>; z#OQ?w&Oy#~I|dd)Ur&10gq9<$n;!!hme=M#s<6r=JY;kegMlJzmnc_YronEll) zJYv)ZTUla%cfNM`=JJH}Rem02_d(>l%s^6z6VoR^h(>-ySup|~H+t&A^fw8H`vwS* zdDZSGbENv$)^HZq`VX);VSLr1rDCcH_I^Ugl}F_J+~+dU7r!y@kIjv}w9F{WW91hV zlk)Q%^((xAl_8&KZrd@Oj(ut$5*dZDOCo4YE>HR1cem!| zq7W)Xt-t*J?f~$CV!NIi&1Nk0`IRk2PMzfhUeAs9Ar7J5Y@Ye!g%gT2p|+~hnd61n zkyQ`K8PlJe^p0QrYx0$!SA%b2zZwF~QqRYGeF$V3BzSYru#$ zR`SwKm`LvQV0#VT;+%7%D3GEzURB22PEOCyaxSgiu`*4#78eC&3tq+`ky>?0p_l;f zsHHqR(_)a3jGp@D!vmJ5WC6`mk={L`1~R1QjJLM8e9HxUUl#cK37YmSTVW<4Ol%yp}CfOnlIseE+C_Y+-Nz|t7P$({*Og1FQlRs=$UCI1D z9ua?_AF)6`j8@-?B?6))2s104C9tDaQkS@YSL7<(Afc$@6}t~MaU~i)*oQO0I_C7R zY0kY)h{_9-0Lh1d55lN9*q*X>_nEzE(07?1AI4=(WPR%^_Xl{F7s6bS0-LPp3};-u zWc9G*vIHf5(QhN}+U6)mxrDXw^VbJXYgrUAQoT_DBU@%So7BRFi_L3|6R-07MS3Gg zpx_=BpEvx=P&(Q>VdfW$R9lJpmxJs{2&BVRmr%E&K^V{uw8P#3{9f3$P5zy_bwmKC z#&LFn7z&P(TAS1+OF7utm3A_5W-&5;ik6r`RW`}_aS?RCYG6hOx5w1NUim5Gc_r4C zEi)Q7iVI*oIGzsFVt&);kRi!E8)e&+So|8WJvw04)mr6@1~Eg;<@*R2^5RZ0F+NrS zLT295qGt?&CRD1$B0u=bbxZ_^M;uUOB>{X)mrCU!D0XF${iyL{E1%;YqmuMV9Z#6| z8{!m1Jdc*@m-}_Xdq-y;HT;|txwxJeGPZyifq4q>&hxxS&ArPQd#&te^2k`x4Thcw z*qFfW83QGbnuI%o>}^aZ!(aHw8ZH*+Fp!KzZhE5vABD^G{rpiItaHSZ~ z8)u2_2Pt#M1|&zAth1G&%d8wH_o)B=n)5mDHb-UFxy(o3T3Q?1k=Ts!Ul%5#P~b~Jcle+`oEVMq>+0yvv|e&Osy6U{g5A|5n&M0KaKWY3m#JZ7AILS zLDMAWEH_fH=yhdptACZ;Z2>|U;9HPr<4;M?f);vTp-Oq!dm!4pI`O#QqU8gk)l@03 z1I$dBAuIamhvPY?eEogI616e-T)8~i#pAfH^UKuAq}uxr zeAYKiPD{HNJ0_1febF1caIEyUJ?2#{`{Z{M^+%a$GL~x*@g^8VYyU#m9K);S^ge9Z zVbz-Ily5XpkTO>Wpj+Oi&H4x>-Ud(g^>bj#P5C~7gZR~PAK1u|c=a#mA*qrE2;^c5 zWLJNl5%+l{SGy{ybQIB8FWWdwD|?EHQ7HJLkAX|{L$ki`eJ}0B^hCDsi!5S$GcRKl zWhdLkJ%{@HZPG+nP~^!jGH)17c9WS$vziM^Un7}>ue9wqT1r;$trk?%QzW}<%P}{r z0zQlFJ~BMcER}LU0XLN!i8__+{ZK!|w@ywHGRg2`fMr}Cl(_c=f%orJJ5NOw$V6>a z3>^aue*$t$CcEp@K@fu}A&z1<(sJr)kt3xk*abu+YK=j_u>a(Ascf%IhcfG!XAgnK z&Qr^LyJ~ZH{BGpt<4Y#Jdm@#Se*3<9cM`7_@4kCZg?C7Nq@UxU2BT$s8gK zIOWgO+7kFvR(k!9VH9CO(8Ap}!A+QT@_LD{eP3IEdVTp*z2J~~y}ct(RIdvG0(sb21Fu^kU*L2;?s zbjn_0n70!J)))7re`bzRF#qisq!sGiKw}b9j6U#zyJlZv5Q|JFmvh2JL;3>oYmbAy zV;lL*k?8O}Nc9YBaj1poh5PHw4L<{v-MyMds0vh(RE`@4I}6LriK;_EF(wi;7~kSs zBRzfPYPd+ZsWyX-mHg@`SdBE7ns1JIV%al<$s^b18l%^9?a{UV74|*7xK;ttYx$C6 zKYMq}Od6AYeWi>?+se9VFv2pWbvE`nWj@KEJ};aTg9-7qGah7{hPV3slt)-9dLpJu z8DOHW_YHk^WGW?BkCex_5n98VjL#HLeOqu4X@k(v06yS|vma-^tO6!rb0e{VTrQO7 zit>RoGzXz2cKKL0F@2xbVxOBMjOU}*`gG677}(rWJ|pHfnCr3my`1CNszt4dp4IE2 zmt}*&9A>3MsI7a=!{inB>ry>-HnW!dMG9^Jz-P?8`Pg>jEWe=U75unrgN}0>LCvdh zLL{q0a!Snn{{9*Cw~s?yh6`4~4Zi(W>u zM4C1^pg2G(AjpA~M|AiIAU@#3sgHh+RlG3oPtA!Tr}n+~Q@2Dc(2UzjSA-|OC7QVA z+Sj$aAl5_mJO|1Y1hMVgYiK9?#lm!AnlkMPZq(i$!1@}--ocN}PJPQjM(>n$=YT1H zX_ifwJzn!JKgL`ZfG*RQZS;2tNnLhZ@9!v!ZkMp>>o-r-9%Lx>QTzN!#|5D=NF;L# zWMF|C%mDBQfLX!v#J>carzEoHE zy{Yeo{Z(U;NEc;$rCldWL(SRn%MjJmhrlgpQ&8ryP&xBqZs!J7V4Lr`-wlQ$U?{C^ zhBHLZp(S*G*SJzoB++TgQ&9mc^9(Ui%C`SmAtV)1V7w3#vL1z4WbtUBUj`*qa2W4F z;tt@QwO6PdcNGu~MavtK9?E^xy+98!6G-|M(BI3K?^Y3!AfuBY7M?WdN+==pGS_!{ z`(|UtR9H(l)4K}7cniKyyBiR3kbqgB?AexC>4P8-wE^k1I*0D}-)t#DqF(!8K(MgEm-KxNMNg4?1n1EN` zt{4Wvqc&tYdPK3pQx<{FRz;!MiD%yZw1p1qKU<@<;#Vz?*fsZ=95YMBp7fPqQ93z_ z-me>~*nVd@ni{_tQjYbk?b@P&XoI%{qP^NiqWJ-Z+79=6&oYob6wbQw+f?DeR0EWL zC=dlw1hg`C=Iwg@yC+*s5ULg(!@`=TNE7lEEraW^E;empc5Dg({t1A>SMoxk-W$6u zpfLBlt_wjYMJH{uoIUeIuEp=rQw7*GtGXBnDUZ71p*eztDe5#)VQ#iA^;FJt0BUvlC4mU~5-;$dFKybRP4E{NEVKzUw z&`2$hSl#{E?coEl-|-U&Dj(ne*3Ma|8kDT0B6668JQq9nVnKswB2Sx9Ki_MCh#ik#(5FgGm$) zLQg|lPzIMW{cCkAtnOCt*HtfG?Ma&|xa6$cxfbPw2GD!zN*Vc=pyMy%aBVLYDyt3# zSJlISFF42P2AK?d4$x`6MDblNB1?yXGPz;Zsn!5%d%g|^rug)O5|$^1O}u3KX$Ds< zLr}7t{asHTD#!x*Cp72kUp#oC|JK8-5w$!g$@bC~=TnlSvfl5Rtm{3o(YY~v+jt*9 zA6JZ%_f$IzyJ#`IARlPBwHJ9iu$J+ID$>PoH^T#zlfkom2Z7Wc?AKn@hZu`)LSFmN zmd1gDRh3-+Am0JVS^Ncf0N}XY30M*Ev?S7k!enf8A#Y zzllQ`*pDIePs&Pp^?k0}@pc3tYGY0pCGH1icFeZEIp~#O_Wr1C*Vre`e1YGY$HLtocAg%D*%c(vB_N=Q6`rPlqyH^15~>TIX^X@ta~Z3)i)h8E_US9p(vFr-**;i$wm`P#dm<@jxVf`o?VFnT4(k> zqw~Of-Ucx<+A0gv1+lJw+M60plZ0$$ssoN-K1mX>-RPd)?b$)Zfq-2p$AA9>S8bWaswkB9Ysn{un|c6K&=DZ?ySOKYQ6(m*zJ08$AJc)C21N(D%q1v22$B>V~`1bkIWX=m#ulOv2t|P6YB5 zv4P~P7+`$z3d`OTlUS^JoM;rW3OL<7>=}Zo@gEl|oT*~t%Z%xP!(?B`9o|~|D!KZ@ zCJwPoj6^ID!cpe*6xwAG?EY@juJ{!q_Q&1G7E&{?h~*VRLbk8}q*eWNuouo#9WLb52F#UQ6MaDQ8*y&uZuOEYWuiw@rvhXNYL)8ogV#{=u4?v;nez3p&er4nOq&aYT@j&;?G@Ef?9N)^XPD zxFOy**vP_fK7?HlPDU**WS!4?y4Y&aZNGa=(+g0tj&)~dC|v{HB(tHjeW# zlhKcwYmtgqMz+Sj1h=e`6J%Bl6I|~)g_q7{0Kg7cu3e*FHx*`r`nXnzWFhw=NbT6& zdAnnC4&9eQ*(O*T_4Gm0K0aonX|*6);#@S@{W14l^Z8hBta0)PTDh#}?ysCKi-sC* z6#>r;7XwE+MIZOoRCun|#KlWHySr))wD}dqUnGC>omF!V9ituMYIN9&NNxR|Sk7K1 zJsAiA#GiHCk<@+qS>uIalu=D@MnKVcjetF5D<~jYRYsgw$xH+e#=T4*3Mm0ejJH;| z&{J;sgT&nGHF_yL*JT z(Yywxfm&+YZZsA|9$T0n(rAiA9U;j4id9&R=oUmsZMySQZT_D-VkUkzqnYESN5=h#fex&Yk@>rNAxx|(f3o5|ETs; zJ0B}_kO@79y5-%jS9k7{0d*V&_^X5Xb}QsJ71I2FvcVd~+EH04JH5w(8dCOlDO(0{ z7m-7ESkZCVNJPQzY{?S{l7VspDbYW)b;k7!qFmSH(lIUHe&-kPp5NJ zRILSPz{pDqM@Ec8wZ&d{VEZ%izPL&m!c3Rsktsjf1#A}K&f>8DCSpaqt0?7^JJGHE zHM?eC02!)A#X*Mp8wd6LKp_BnyEEIT7B_rN&-umN|669k{@Va67^d)>Y5=B)15)-B0# zYX;Vpzm^+V#H{0x$thV$n7T+8J@knj+`4oHhb@YvInEoKeJ)xNbe7{9P)}LHQ1y)d5(Fn55tb@;?oM~dj!K`s_9wi`#lg9=n z+3X{D>Fame3-(T%3`}*AJ1?-gc@j(1A+xg>$L7k~%BkhR(Kb?as~_RMp>yjl`C3$> zw$7Oou}LK+cRe*8jU^|&U^8)1t1G;H_~ZDvhM8JR>%aQJN^7Ztm4`1^o+|3@?Hp4< zNUeVkMD6pjr~zdsAcr`w8WH5vt75%hTi2VgdCPE(Pdb54Y)r0Z_D9K>^g&G`biD=u z!k#`%4^hfB5H{VX^M&~7G28{@SZ{4>ie>ac^tQMJ;LX+=lz;AJo0$s%P05K&W9>CH z={tm}bF90uhN&s4CvO>fwlJFZ|2zbmOejJmVkxhDi_&SaPuAd(K&{Anj%)})_Dw8k z_098iD1S~HKY?7Bn^LQp3S8#IN+Py>)f(brha(DlSM;n&ViLL&>1Mx5iy?i>c6H_0 zRwN;$e$TdZs;O7M(ERJ*`rVjjrRcB>FmANJa5Wf)E7ZG-9b~+SYeoIss;%GOo10h< z3SLK<>2<#$W=WYS5K$J#g>FG^*X{Hhv4TgT`c2Bet4{WC(g4mFo*e>~cREstjt7j> z@HhY1{S2C>CL{J;{r>Y!AxGlLtdy4S%@#&9b6i4v9Tr^wl7_H97Co4hK>H(X`*UyM zn1yY*boYvMW!hYE)BaRp!X_n%ubZ|LbEW&$V?!KfFKc`U85L{T>}j=_9&=8)b3fez z1 z4}(G_h%v+sDzriepAm&f0h#LK#Ym!YvMT^BKCSXR|IdQ67<5s~@!_{VmuMKQyl857 zsPsEmgyE*5nZgE?gXu1MX?K^|lDOr~D4`I3RT=hs`VLuvtC5M=-3d0Q-e?4UG~)C$ zr(7p!=quda^{Hd?cQ(fxTu2NUq38BLZ+S@y+vDJG?1y>cw5+o4iMJ9wjDBCtkJQhe zi1^^^#VC8VG_6i*9nbId?J4%PlmK9IDpZxnft2Q+*aig11Z{k8q8d^3VVP6L&^)yHGpK-wGxBOZyxfZ2MNc@zL&$xd z=jtTvg?*z59SgbrAFW&Rsf&_8B(Q1;X_~E(Xoa;^auy?QYQ(gW|75J`_>$aazWa9Z zNPzK=UyvsiK@N#W{wyC_fFWhru$4;)g&uM*g^QGcwBG}w%9g}>>pDnrpWV*}s0YYb zjsSs(_CLM4^azTGL%C#Lve%qrH^xD7uW@Vf!*;!*&dZ0fOoopHj`c^T2o5;3qfH#) zD8d{kpu=}I03y<5O7Mj}vN9!8ig%i)V|$9Kpy`3^hFh~sy-n=1DI;&kp_M*vgPiUy8gwt!M2+3pNvf^4C7d0${6sLX z2;?2DBMCFYkViWJC2y-h&W*j}1<;wB2|bPHqJTqReH#sT{G*TdeyMxB&lLQw39pCZ z@D4eZ8Pq_|+wL;7Ds@M7%$C(k(@WZ|NAyZ;!jQ6(q7b+Y^$JcLMmw^6beqS6-)T58 zFVHUl*ZZa_eHiVd)5|6zQL|DhsJ}SW8XLW}gY`$BYf4(h1j>`5Nok-q%hv>e_qb?& z_yUop9so?3R6v`EA;AxW!kX!k2D9reNHOg0k)lV?-VKRUEcBoo#uxrZ)LZp9OLuL` ze0rZ{{UL6XPJQvxz+;YHC%J`O~E_mtaa56XNALHBJAhAEa~eNnm?% z@t0YMNh>AzKz~2$U>cZ79aN={qXtvtPlBq{wD{yS=a<^i@x!L+6EXU^H&)KH73Wvy zl#W2v3h2QUKuJRggMu9)no6G)rI)R$e;EBoCnQw}_Kn&qs9;Zkh&v0Igg-Tg@aP?f zx~Y%?735*HnT_Gc_WXo}F3Ctq4vb^zaeQP`X3VN?N0^@&)ce9r`3yP?7usRMa72lV z0Es6#A+#a+XM%x?0-3h_>%?692LP&+=e|wrcmw_Rj^JCc1P>;UAj`1p%!2OYSSj2G z50K$8bfZA-kUv~NKsb%hqGW6t%wbp!^qhwt#12{O*r0Z6GDd?cABh~pM3M*` z+q+4_d`4_!I~hgixYzkk0UF=KB4*OPRSCvUmInm;LK4MHxS^q% zMd`rWV1M++&AH`qW#C#&65Z8bow>@x+AJ-x8Z0Yy4k^8RyRLLqvM<^5(W`UD8(|IO z{RSeIO-a~2g&O@{(Q*GAGDvIQ6Pb-lW4F!JOdO zTTR+o*N$c*_hj*{YTb%Ug!Jz*39$*-0rx*5b&&QCPzT0Q_0$x#XGqQ?OfUP_1@;n- zb0HS(-Nrrh)}jz?yW{=R1_X@1(&ta(!XGrT31@d>rsJuajx;RQ9X9O`KSYj%OV29y zHN2lP7QBXz2{7K?yfv~d2F70Xx8m?_1L;9GoDmj3uo{>zYUtA@+s)MEK~=VT9dB5O z^80I;1CikbCjG#ZQiA>PCY(0n`!l$KG}Ov59`qFFu?1fO)Y{RE_DFY}f-;{!A$(y> zgpIB1muGVTEXhmzPSV!BAh$t^TbscUOg1!ZUE79Z@ZDSHR-6p{D5EgZJnRCE0n2%& zAJTPRfdJyxT5q_VWa2^zEk3deE9)HF?>bED=00eOTysq+@24rtxpba!WF(x*3FFZ7 z(GHVDR7Q0ZplQBnBO<0d3m##}DJai5C^SkyB=~WaW2=ie7!l_XV=36N{i_pICe>J& z_Fy0M#GH229C;v-uLr0 zSoAZrnXYut7X9P506wkvw2riE{0l-6FI&`)vC}#adCP7EPzy0yFcRx@R#5z`(N+Jh z%wL15UE}hJs=|Yrr=Z_E9!W2CTg2baDLH6r zQNxE1QErOK`yok#{5`Wcc(rpy+B0FYtjSPaeWcUpv=(3C>Iwkn<*=xt33MPQO}W(D zQYMi89ZW8iLg`8Ro zha>j36qeh(*?;-TyO$ZQp$|jzo59%^aiPkKs&iJ`yW&BL`<$^n z5(#8TC6WR6xp`mrWa+(F;e`bR<{oTA1`JtUTYZse28KsrJdUU+GBeu!)pV#pEf*Bp zsQISxWDFXIvq@8YJ&(Y&;J(K(gFfGRA=JXVNF;xrs}!~$O!WXPxdeTql_1XcEo7l~ zt2l5)Ef?+Vvnaw`UzJlpclh=OgI3$ZMWm+mK1dOtsHs}}XIj!z35?O`M0N_he7GKVrQg}g8r>Nxu=hphmQkfD%pDP5* zxb)}s__*S+(zCdFFiW6)*>&ztmX46Pk$po%p|Q}#a}po@!5AUCar~a@9bXrV>d0*o z+x3Jt)dEam#21RD>4!lLnL{42t-EKhn!{TwOnl64-f0WV>uh46oDl@B|GVS69$-wME+yv9piLqVRt)p(pdv033dp&duYb_W z(Sx$mhad2opjm(g2mWi12ee@7-4A}MBOJ;EDY+G5Zv$nzUF2G;p3>(lF9`7v&^ZH* z3YoFu10kAiKpQhBH0RzyI&%*&STBxu_w;nnv=}(>kI4Q#R{bxL!9nV$$m)5~0ku%( z@iTzJbSzXXtkA5f@j0_Yzx``vp~{Q-wR>Bs?tXi#$xYLpX(282M4fJ@5-_Z|l;pp+ z86#V4+3v78wf08X%yeVvmo&EW_&U&wIwjj5LR0{mTmB!a_Vs!SVWkzOxB|1mII79+ zy5?4l1}}v05AOGt*U(QTB8)r5X{GV%x#;ylg$by30HfdJTq@0VBlF69j@0l(ohSr^Ys6)C3dQSk;076#I+GtvN;+MU26n~t{n`Ie>ms^ol{c5p*Q_Su zPG==}jJ|aGe2n{aw2lU#?uN1vFqrr9KgDV{RW2e2?P~37HYW>bvvbN|`K@~;)?K4$ z!iX)pmy$o1&Ubs=epqdlF~In(r6TsABc`>Cf)@n8d3(@@CG*k(8FJc=5V21t$^dv8 z3!bNWLVcAd(qm`2$!x7h2gXqias907hO``dIR|^yd(&C|E+ln2eswXu|{UcSMiQD;){TB zw-!J3n<1r5=lctXV@^)H#j8UPGG(IRD~LYuJ%i?lJ1=QvHrQhf94d8f^Gdx0;NE+i zQ)31GyS4#O;02Gr&;(?I`(}UE9eM#T=)ZA{=&IPmRe+PYI;B!TtR1BSK!rFF`e8+% z6y5SYK@M^h&q&Khylku2dJV{*9n?bE7Xa5O&73$$(Wa+3e|zPtAf+2f5JM9yjukQ> zu-Xxc$cHub&5T5M{RF4R zvkFr(8$X{ccOv~tG7X7OgyE~0+f@_1lDy+?w3Jl*tp zu=!g9FmtM6p$ua>SYoZfqP#aCW7q#Na7Dw7FkqQH z-B)0;vzokbw@E7_v4wgx@Gv%1u&n>Wk>h97J1qRBz$u{=tRCzP0=nU_S-i`1$Gbv| z1IkT5gPVpD?{$4x6a~aE7p#AF3d~5uHHiI3P^CTvCBvz%_fe2oIuruJ_BbsV<8zv= zbxeHS&N+WQuVAm1a7}o9#2c5oGVXiZWOr7FLwsbQtdYsK{#_6&Q?r>E#*q}geqkMG zL^x}J#(AS3C?od)&gZmj#vPp+Zej!GQE$XuER<7p)GO;%ofWa}7<)#gidbx9tz?vY z)K$KL=f}^U1Ul=@nbDn|4=)h8EPB3!KRxx}8E3k$sesmy;t%Z*t?K!3 zjw4=UjiDhq`BmA5fst!a$#WW{5$yD&>DQ~?n@t??Qo2UM$f0+A>tRd4RY^Jk zTQ6#F*Eu~s!Rd2y`Yi~KwhkfE$|>dlb&SRejMIAV3;?6-;|wX|KK8pQ9^iV+RjgyK z#Y$%RrCPndZC;OaHoSm8u?-!%XvEIMc>oU zN)e!zWUY*QuGbOd3}W^KkK4O2cmb7_>On_`Oaqix$`d8i#Abn(Qc46VTCdomA@gq9 z?UswUmiS8#uHzSnQEMAO*;u#}T{Y(XFfdYd8B+~3Gh;*w+OdtV&Yo)C#t%9)`_C`W z<`k3(+HA^rEd7L>QE&^cW_!_Nb0QWz@3aIvcBuIO;^0E)Z|&JmkJmm;lJ)m%O}-dz zwe9=s^Rp0pd2%p#XmY=_=bo~3LywLLOVByzTX$u-0sP^OLnCZA-z!By0o7r+9W+_= zu6VADpSkwveof|t$fjjO)m%QtIrk^rK{2JzxLyW$w;vqp;OS0BY|Zj!yMK+9x`@TM z7r;BFXI85v%M@b$w&|m(@WHoE(`AA=pMsku|1qwn$Ok}fHB$1?i04$|8%cyi(Aqf= zHGKkZQsNzIch~z}+NDE|_}8CMyx}ZnoVBox-S_jsd1Lt{E_U!=xC;rp`=}O2hk@@} zk6D+Bn4tKFR#CVQo z0(>gj-|w8yQXTZrh-jbTDU+=qvB6=-h+6|<6eaBe4@@cAb0LQWN{!rlv?Hpy3l_LK zY;%4|1|)&JvgfN&iq3zS5i6mr-rw6^-kK|D0zVq3-@0NFiSWhau(FX&oSaz9o)YUz za}IclRKQKIppj;7@bGP-+r7?D1*pZUey1iD=ovWhmfsyPF4c?_ec>cD=y)p>E=r2d zD;wmyXXw-rX?B}+EkUXc{ePn8=tX2tf`pq5K-O(h)MqZSKc{A9kPo5>1!&vL$I+_gS)%8YD-^N7RmT1ToB#rz@fKkzWVL#n;mSj#QT@0L{b1Pm943` zyKu|aNB7PZL>D9Lg>3y_D7C$gv^GS9*b>C~)B?Ofo9dnQPf=Hm3Y1wdB~Gcn++}^2 zD_|g`XHCcXJDqbAXTHKoS|OgU-ELuA&2tSkTVk7(>BvDLTdI9?#$W-n{*(*6vfrRh zY~2D5%4T0r1qG5_0sdV1Cb4IO6i+a@0&(K?EhNF-MN0qqdrbaW#bgbh$XfD)|MzP_ zp-BWkdIy<#n8Z3?hJ@7K_igN|ETfFyyRjH%P+^%d26(NylzI%EQNB(VWs!G>tTW)^ z&5=4vJqzarqXq?8kNz1mq11Cu^QDsse?=bGozuWNwREFZZg_Lf#v@G6 zO?b$4eX#qWh8fl?E2dxcI_8l^Z1`wb_i23CFq41JPH&sr{hy{yT*dE{-M2cp{1`&0fK#63!@%3X)1 zAl5aA(Si0ZdCAKK?aSm{aw#!Ea>o%qS0ykbJ*j( zolPzcIv#z=jJ!s6`#BGJGKK&GWW%O#*zLw?es4%?Kl^bzM#*;hSN=+?D0esPS9YG_ z>6ibZ3i!m;F28rYP;{Z(E*B8}I`UM>_w4CIyeNxa zp_Ro}??E10VZ5*@I|uc`(qAig(LndTXtyo6AE56(8v8?r(K#;p0E+y{vIt2=JaAtx zZ?|ZB{>wKwYea83QlliUZl1ogy#GV9BExZEC>MhFq@%BRkW9mG$We4dsbM>=K?CPv z%bNr&qhy{YSxRG*8KJO|;T_~`12>Pl`Ap=<&6yB4k9hJmBfZKKkNENO*!Rr+1jEi0 z)vM1p7R%PE>j+3U+&zpRRI>Q5$MBopWpb2edKBwEw6Tg{RFavfz)FpvoU5B(k|F(G zwH~etUB8*Uxj^rso9RZQ+SmYISWM&PTaMH>^r7rP5>N3v z=1ochC&LxT3CDZ=9_$j0<2S{acmxD)V1^Me^Y{v7m?%YS!os#(zRUxoS%|3dB`GTCC#LzYd-EF7NF zOgKk{6y0uxV3@8ziyl8bDN)idN7gcizl?|xp;Jz+;%mEphM0o$$@HL)#Lge2QIT^= zdx(m>3CeEaiC)%&tw_p%kN6n|CnI{CM5^5XPEt+68Gv$zP-B5XufB!+S@2wNF@WT~ zow1d763o_Z&i9sMN0&D?zRkBIdY7;=oAsp(D#E3nKs_z}%ii5ff_`Uq!&Z!6Z zcu-5zabvJ|<=al2qCL-%U2cmn^RWpBjp2PT^Dg<}cI-g18R@c@&yR-f7&rZQDsjOaFrWuZPdhL8b8K&F z#{v4xpn`y^>gNUSoWa4WwZyZv5TPhe?9d7 z+PwLB3{GPoWJZempZX&0zu=J5=y_2G3kyIO2Ja)V#|XqlZnMcriGAmH^kQX4FbQc& zQ5Co{Ih1|cO#92v#~ME5_L-$`il+EB;9{=S2)~ykg!e~y_BIOr0GY3&H@0;?h%oIr z;OX8mecm+TNFf^Og;x!En-;2FHgcz}>0|>av^W9p8h-eD>wdK3kn`>2>;H2xjf2~D z)@l21@#sX9)fF>a088Hd`RW6e_SC`ju74tm9dU$&&sRLQ4wCT&LXE=?wFRQBd8>2V zND87KE#%}RXcb!@5SlL;eT!0(*u@I1{W9=v>`ss%OGTQWxJg8Af4Kzv&A6Rk8M_8VE&V~eSfF-t3NXCCQ88>_+kVgnrq<>2Io zybaNm01eaFk4PGe9H}3#>D_mh9zYcN-|v)RlXBv_=;MAX(Yc(PP0)mg^%zJvTm6MT zSc_Z0up!_Vl%EgRJ9cizhfVbUD9(VBpC@Rr)qfIK^Figcuh-8r9&COJiDAm$zdSvR zX^k0iA$ZmAuMPcL_WKC+1GnJs=PUofMI*nwQ8Nhamb3??;ugX6&9R`X&}t))Pm{|^ z;5W;x(uv5`Ql~fR+E6N9HEE6R1|aZ0kseI7LjXxl>euJH6cDoNA}wTpI4WZZ!$V=E zZ03EZ5d3bnChAp64@G z!2azo9xv!n66Iq}DoOKbpbBihl3c6ZSQ=fa%ZxjG1Bl;x z<9^@Pzu+>Lo51mW&ftQteYa+EeEGe>v8$bjh17@`;i%m137$KQ$MH|T1~@+N>-sVV zY4stdlrj@=mONn*1*p;T9koDO5cR4A3|&-~atLO@I&CJq8}c}iZqLDf_3(bYPcRY| zwkQ1a>pr4mm^P%dfDZRqJKm79aNV?vx8Phk>{_Y0`?}Z?UFgni}Z$kre z2-pyG-e4eH35?I$Evm)`qI5Rvp^A3eJkTVJ&Xc<2^~{cATu(X9ofY53b}UsLKOJoE z*1rahGyC9?*SS85)eT3uctqLb6n_A$)Z=o83biy61iY&SNe@op|A7!gl!P$jVKZ7^ zr>>#X#YO9(Tf7l{P!Jjs1rtpNsx^^-V8u4{%-HyspiZQWPW;O#1RdF0pdme<&vWJga-^A1 zyYZRkpl*x52wzMhoJk_g3duQ10ND^O#=uYb@|B8>kwC5QUtT)=dgLQp{~QuSEuG_l zYM$<0R=6^({va}>BuEMz$GGio)j>}}3uUOKbMjCyo0{Ajzvi(yfN(RvB`L*F12E}d zYByOgvA5*ky}WO*piuRhwH2ecy!8DtZeAh-`_tD%xa8JSWRBKSrSm@m6nUDb#B z#G~ulILUmZFOH!YU}lm`vc2hnq;)bY{6xZ&NQY`K94T!ntGc)6k{%X6{UBLzf9S&g zhD=9>Cg1+nc$nVP%SzEx7fv{mVmQt@?{Hh39g<61AEvp*(dd5(RBrq)}n)tY9iRaKjsa;Ffmw7EoN$yLOfUxLe z7|v-`x?#d>;>{o3Y$rbR=lsFH4`fi`l3HlLd3u$1#Z%nOQY@1#z3ofR?GTxEVzrU; zxU0zbSBxqzS3eQvbhN{CONm7mW4r)mx&oc

z3;TOkb~CBT6ai&G)-|tJtYbV^L~QykizDDjp3}bB&LLStc<>FfFC+y}7{zOYo$tT_8hiVV*j}v5H9BeB6*G8{_t&Lb%6%E1#oeqD;KBEG5kRt*o;Xsp4wA4 zPTaM*{o_qWjLTy6iIzZSd7`X;KSO%yMLZ>L%|#TiBw}oQoy z+i@`1w+Qrv11gt0T&^TLC%U&WN}ToKqsEI@XAVwFz%dUq0yOyw)68uuPu;WUTER#E*hP-i>2Y z+#DaO{B6NTjbvtH_LTT28$7X#?bt|nKnxj85%SR()Bs8Lv)vQGu?u!2Df)6I2N9re z|2T5>G+73D&)BlhZ3FN#=dd}TIZiU~$!NRhl;o&A_MRE zi=B$iVJKr5CIxHzc&blkU7BLX9v`lG5l=3q9A58#!3rT$AJhs0PP(%6?E+=+?f)qh zbfd&Fg|}50;B2q4S#zMHC1J!vP0IIq%=4z8nccNxx*(WR3@WG`XsCuQHW zjnur*ZwzLe`0{)Asnj}fmd18~^ioC_v+S)ES2`yQ(O8u~Qg-hDTl{+nDDnE85z0!Q z$jI3Q-z&>|c*HFceBUhcTLi1s=58JZ3MKvdfzpZ3VY=)o8fu!}J;~{xGBOwc_*@Y5 zrQpi**>eE2E(tRFo{jY+hzORRqoIC%zm{l`_+F6rruWIKVrcB^GVOdj1-P*Q%O8Cc z_9A?eUB@;S4Cpv{vE$&)Lg@A;guMs}m2@Mo@i=XlXo?SxM>p+X1qsuPuUEvBq#j{?*=*n@htEHfJ^9A5(7lkh-ID$N#Fv$es(S9%Gd!m`E zHU@j!{|FuaL_9kZpgcdw-DLpRCpE-J5qf;qI((tWP`?s$6nrLh>hLF0 zh+uJP`kw;NA}<^j@&A?=UdqB_H=(cs)gyX$t|vRL7ZhK%zbCbxkxFG#beiU0g?&@s zqnR=fO^$&3FR7LuGt4$PUyD9kgV4!I8agFwt9Lz0sOmvXJi zj}8^i3HVs}Jim4;C%lPfZ~SjF;m1bTMNWz9ATKxoNyF190x52~)`FkU;17x>=QWQX z_xzXj;5AlB!N;fYNy7N#!OQbMR_BUbuG+x(y;SZ&{L?TR97F`?M!UQZUnW1|${keF z-Bm9b$cb70dAN1JG0)hSQeVfLp>A%ncw(L52Y!-0I^1E894{*5@F~3@#s2)Gz@Z3# z=y~S-K>$jf+L}B64k#_ti}*?Xc57iI6Y$bd)217Mo6ibuJfnlHO4c8kg5m$JDI{a- z7iN31ueC%9G(F=VAH}D!7~b_Aox1k>xdAp?Yxk}W@8%06i>Kgw)L(ZQ7580cniB9< z6Bs#@dXLLx`(CML@zWU7-n`G#?H^xG4`IG&#I5;iUX=h)#C`=-CJ~z}lQd!{ zjF#2J0l#jxC?0emNxQcR8j*<85SkNyi#Gbvu$C5UULPR?;R zrDL4>d^^%15-CelQN9CK7bb3Bv7`QXSO|n}9+PT|Q2CQ`9K4K{fc#1~9a1=bHLib8W71b-x&8P1YfK1- zM&zBK%Mtz(dqN{K#mU&a`(Um$U2k`nP%#MHrnn$@SwvmwZ&j6{8-Md~t>l-(+Fb8M zjN{M}_X70x(rtJVukyZKa37fJHO?R2@gx%^uR0~Zp`UnFIT#O5KrER_vNc+`8{>Pm62Kx)iF2WKXx6?#M zxH%x5Lr1XKJc@ScGu8FhxwnZ_{5z}J?p!5IS5@|dKT`_6F(t3Ap;%L+;%lTo842Qa z-s%-{LP{HKldu-@W+Ojt_oON} z(Wyy>08$*IR{T1)H`}7~B?i6So|nR2ohMxG#x|G1Ixf{+H_E;J#;op@P#2Yje6M5U z$#-ctjU0Q8C=*T8+L`uxn}>%tQ(TPzHz3x^*+rTT=vLUBO2_tEMHeh!2gwb*nxki@ z4d1emVeh_DR8xfJv4`n-ofUM$Dg!QA2VD}9bv>*gR=GBLiUFVs(Nc}jRsxl#j8vEp zSo6>FWBQ=Tdm8*Ecd4Gn16ZR@3!*Z@NEhZUgTOzj<1$vngKH`y=lpz1zkk=;GQo*6|&g zve?c@tJ%)r74zOK}K_H6_cpW9)kn zjp2f+NE`H8KZk0dXp;?p;YMjyI!>{_E+W@X^o8M%UfXH&MlBltCjnyEqDD~aXT>sa zEqlaTHr58kudnv#xv1$Oa!ry~!ea8*`ko}>uTC+rrr_a85lVt^cQwvD62Cw`?22?k zlz`EM7oXp%c85fWx+_}A6#YoiF{2`-H6E|Oc+x(uadl6llAk0GexeIollCpLbvEMi zDb>((N~#r~T~A4ITaFa;Jb=9Qo>^l;9J(!|vMQs0sf1w|(1g(wKx;5UT_qOUiSjTb zvwpNg&m25`dme!)5;{6|$GP3F!c~vhtEO8`6kJ*x->PHkd?{UOBF-&&_RLl2vY&wK zr~W3_md4-SWV`rRVsBKUoU*k#uP-c~0G=tTyPU+f=h~^ zXG7~%5frP=2n|%!8=s(i7Ep|@-|6~iTDI`Y$LtWd+nD|Z^1Mi z(n)V7p&#VW^&L2>}a_MmE32~2x$ftG9)yr-sp`mi8U2*oYyYMj%-v? zQ_>P04_0e7jT%Z$2>RRR#ofrbpqE+j^C2eo#mTINn)w~T-Q{|n?* zltUkriq$UbwPVS3jNo{U$K1z{PCm#n|6SYz!4m7D3vu$}5T?sU~v2YCI!b7*2%|U*O;LseL>^bg{_aAU;U%eA$I9&( zy@`}WKi6KdP-L)pYy`PoZ7VOHERh15a6pHY< z?>%1#PNJp*&GBO1k3MG&9s3aXdFYDy0YQs#r!BMt6nWcR+myXLNLgGs?ffLfmRAG{ z5xjMJG)gsc){5o&L0f}y0_LIaY4_G5^ipd5c%4C(_&20|<(B!F7Z+#B-~2Z54{E#K z3sHiWk=H_UTRB9PUCoBdol73{j_PlxO@c7`y44PH(4`552mQu|N!12RB}Y-TB@DW||&x}*Z86YNmh=PP}@@_@_fe|S2J z*23r;TjFg&H1-J}sHSa;gUjj9{`M1Fdyo%WT0yuiEB{w;cy$z$NS}pB=HPnbgAS60=q=x1bR4$m|)Lq#kw6DmLs- z66FMxbt`%D1*bx8No}`vyARlWqf)#`S&$P4Gbf)APAW&Xdha%+j^X_*?dcc|%|W$B zka%5L0nh?{n}0kdiJqhn!t-M!((m7MijmmAp)b?xBh?WcLr%;CH3J>)7F{RWt~;T& zP{6!995Ufoily!Z-^P>`;vI7tH9T(3yxOT=rawx0^aSEfbDc3|ENO}{r-{r$UK>=e zipwi9rU}Yv=Imb4DGLYjW#v7ag+A|PNc+c{{%3He+2BVgY_WYQV{ z`!Mv`9W+3P0KPZUJ1q|r;3`5}+jAB@Kt7sEww*6+L6Rvw+>~d3UR-ghHmh-?oXX z1zNoOMRIR^8qM;P3O5U+^$a*!kwL`%x!iB`M(J-Dr;I$*di_2o?=K4%{D4EORNTaHvsJSW4MkW*6AS zWjdFs+iy!0Fb37fj$_NVB0M}T;uKosA< z&Zp!ZtGA&i8d;&N_EgY_x>Da2L5`LPfcHEx0g&1{vAj^w3*)~~obZEe<+EeTWXG>u zgTJ5g`y=>Fw#*;nb5A*HwH+Tck27sl$k#;MpfDv74qGnDZVzZ6(Eh-@&yA0#EZ&7h zPzVWl%iLXu2L5&^IvpV(`fly(!~wDKs!ZuW7|7T=c;T(Ma*k$5ah6LU;Do;HW}UY=J{?9*I#) z)U|6`iKky4Lk%!kgTm6OjoLYVPSohn_ZliIt;&4&{k@^WVfRKs-5h-nce&AU%i_Uq z3!v_#XAQh90gRLGAZkiuo0_&`E(uLeVxb4sV9Dl#Z+8gLnt#p`OUiixbb+hLrwGsm zK%GC%l|aL}7M=eO(~*q72uIlax{#-&8U!%`FZ%Tx3{*&X4CfzVT8|Je}$Qbk|fX(&_Yl?h=FKbUNI8;}+hP$+mSmk%@4F>yLD2 zjm_RGn>V(qCF%TnS~h)IVpPc>{70fp6rkVUzMJ2qaB?F(W2p(Y65g4)Y9?F_eHBlM z&Fqe*{>t>mg%leYFBfwMva-DAOy$h8N0I?5J^zKJMMEBM(v+N5QUn+5IxF%1CPN^C zoHC&>h_<4dS~)ouoi+_mV`7X!-arco2B!4d(&E=)94Qj1E!I~cMe!hYnSZaxF;t%X zFpR-iF*GayajWY+2vFwJ?kt^I;h|gu8M}6=6X%{i-drA^fXS29w%K*J;VO?k8z-5z zJ$(wDLOhKAnqK<-%;pa=^i&QPyKUf;44b!j7rkx}pxGpo-W&pC^)+(Z=Sk0^Td4k1 ztEtF>Hw6%yAivWIDd)bqlFIrW?PRz!I3c}oPA-!snR3Q%qIv1~2X#1D7CI0QD&n+G zEJoY4UIh(?hr1sgM87Yn0;LH6%EeB=jLR9mXHzPC2K6U@{{gM$SD2INmnaf#oKy3T zD||dYw~Dku*Y|f;)^gI+o~Hdgap8G8Cj`fAcPQeJ=6%(FbpGDM_9p7&657BkK5&Kg zqnT_9@EEfXC%Z-==8f_C{Ku6T0^w{ROi#Qj68ya0M==DjBGnT(qt;Efjli8`iQQCS zyyTf?t-GU$jMUqMtU!vB>amFRJ1*`EDRBzNlCoa z)1g&j{V~jS0|rhafeMUhjS<~m&pi%6s=Q=naU>BRYmw$@rg>=0p04Fo-)~C+ETmV0 z<%bo3-qM<7QZDzGNc#m)HHE(+|uN$7G6DY$hrblRAx!7tsUBYdQSs#t&AZQFYE z)foc*QYx= zq$#J4-V{0lXvwe$=;CWm^p-o@Sz&|~F{vvT+;;#NFg6#l^1^TuqnFn~7tbHWqFj`< z-46gcI^s3DH+!}0e#V18>vz0hv`inpHa8t+aspbYxcSdLqW`%I;Ol7#3rGO>+803+ zL5wQX|G3B>R(5$wLGNCBzHa4$1DCiCv3N%yDV85ECyA8AH^@wb%7s_V;O}HWy$-qv zKeNv>cBYj&S>N5f&_7;-W4YiV8@Qz0n>tRb3$<5$)sA>D1h#`DrE7firNJqL%I0}e z676#IX3zdYz#(lAEag6a4%#VdqntaeK}I_5LH?i`5^H>dR2h2}ew{Sk~xLd=RBB3f4%V{j^qO?LEW z<|(5OYQlnd^neA7!; z&%w=l=K4nq+RXG-fEffDq)&#b7BvIXW(nY!71C66T7ijExNsD|P(2qveV(UkdzW)E zq&CeR27(djj+Hg7c>qBVq+a4jv|Rf=;IcLqacQ5*Gu9R zqR+HR?3cq_k|Q;i%_$DWEarm`lGf{k`i(6mf6XJujG987SDz650oFNQDn_J%JsDR< zO~fNUjQmNYDKEVF%Z%@`7&bi4=y?8DUaKMj6F#jaaS75QHT z3PSWr{(qG~fvx>7#td@(|8E$x|Bt=!lfQt`uIALM7)Xbam_No#f!g9~mZ2p%?A3`r^c z1Eb7+ZJACIn;BO?e_CKl*BD_{PQj%Q9YDmSq?x(RZ%)QtQ%hD_-FX5SN9k08ui5;W zQvg4ICx^C@yBX%<{$5L%H1MYv+Znw+pP`vwk1#>Ak$a0zd-IIy2DPC$LzpE;z1i+e z#bl+J$2}j{!mjAd?5fdiS+q}j7C~>ut<~0}=l<8-FIA<#$kI2os2QZeEwK6>7p5sX zn_99xn@+#J7|7O}z>~atRm$40|B|E`SRw;RnopQKy%aBv06H=fM=$a$Wg03H_vkJ< z6edF&zfv18oXN+Xi^2=Yn#F3crw`Yx5(#TI71wS#g{=8;^$P~7obgx1^+j4)x<($d z?fKpLCdzz|YEUlWsa&4ANi`mP$QURrm%Ntg-FK#VI?&@7gCgT?3j#tc^F~x|PSEVf z+Ik)cLUf-m-A?3os=aD7O$Uv;F$_OnVHJleK9O>Zca!^~sv46?FDW*UmhQP70BW)3 zWM6-y8Gw7GPf)mU#I4pre|OI0YlsCjeDbXHCkX;^9x6GRTcq2}U;t|0rOZL!`2s5N zYel8E7H+o7pXCm@$qHc=WDVRjaZVn45^Y z>psOn0Zgv{Z*SfWX@7d1;mD+UwRaYfUqSOdV`yBN0HAI!7yI(THZs%R06EhoquXx zNUC`bg3K;^an#sxwgB#zCN2LI}x2F-E>btzhBfo%;GlIzk#q1Q_%W=<1XB2yFp+Q&wz7;&3 zv_1YM_5^QNCfobTwz!4Av(Z`WbG;p`j7mx3zuGA1(&@x}NBbc3mT#%++H;sI#o0M* zwU`m18!vTErW7I)YX1=UX_liyY*ZWh-}jz& zTRh(JSw1YG&U2OG40a?qmn1{G^ydIfe@p-`k)QnbDZ=>jIn4b^=;6R10_vuv%AIlU zJ*|6YlQI%ps$eC3+q<_NxByXvx4=D`myJGh015_H8fRmUKN=y{9RJ>%I*2 zBpLQ+yaLTCA`sKPq$r_t3FNYa%&{9phi}CCFzKZ0xsyS>#sJ_OAJ1~KaVtktV>}0> zxj%OhiZhy;lS@jO+m@NX?y6DI^Yt&kS9wPg8o!avjs&X?Hp+h4+nQz6Q<5p<%FWf5 zmhW_}Ypo;0GUe$2gJ1C=sf_6-y8(|yV|^*8CV;~aBcz`-88wmE);z9?T+ws&!t+HB z4Tao*hIG!4caszt-n{N+8a_oQ(n+7^B{@$Z+xd3;WM}}3RGZd=_VlsM6cIVaRn{d% zy`QhishA~i1AAf9E%Sv0ytBdu;EYSND$X3`rh0lq2>etq>l!f%+}!e7({6H`!Jzr= zTShYS*1^D4+PP6!3*cdfJL93sW8Y-PH@aN0HIwdkEto62Z8QVWkM2vJ5?7#|wG&2f z#A+b$$x36a+fgdXLxh6Fs#QKh5S~X=%x+NuPw+^p%|WdrY5KMeRA1$Dr{5SiE)!Ic zs%t9dvVP#SFeOA6=@8z6cNcyQFXnlVh<#%{a%BLGf^$+>A&|LG$I29JYRiO-_;&|j zK6ykj(Rf#_=xv@+ty+hg8IhRcnKa6hMkisIpkYjYoWHJ7#>Fy~x+?)xgLFr<77#w8 z3YBiVa~~_ae9=ZqGbVMBNnQ8c`AO2VI6ACg4%|)fZKGHIVrS2Aa9U&I58ti3Nz?k4 z6Ym?Tcf8Zj}th@NmV zvRk8Lfc>FnWj>--()yyk+0f?SC4ch zc$N?Iz*4PtKPB3Vcf~xYSRZnE->sN;`Qu&lW8PeT4=9u0^+kfHSP!FHcEPUs>d@BR zz3;CaOs@wap2YGb?a7WIoQL`;NUi>#Q-0o;~i zRsB*@(r&@!4|m&oY?DlW&h_T+KGt$nfBdNsm;(d^-_G;4ylK8V-E_H5p-oxwhee%A zu1dXOHw;3UlT}{`gL1T#R_u8j2V!4mBjw5?`)V?%%njb5c>eVS0SbNw&-9+nZ$O)a zEq$9kmp&01dNd&ce% zj>PbN&{eM{+jsab{oa|lcDv`pGEMff(p4!dkn#;hIybiF-hUf+%NtP_w2i5Gf2{3z zoWvQ#kyTPtdEKyql-urSYzgf6U3XQUL#yzllcw96Pzed~8@QorCC}5-=$DVAF2R)J zeGm&!2Yo!pJawnJ0~=VU(CfCp@l9feC?5;jE|CEco+zX(OUkpYOx{t zh#*ty{x=vJ*v2pZ#5@(!bUt6nG)<83RyRy7=<$T;2Cy-MHp{?=?zTjfzP z6&p&K#Xab?hy?_B1; ztxJ&{w?bzK00EOVh&rgYBYt39907rDcx!%=811@+y$TY4u802JX$4dkjU{l^QcTm3 zTACzJ2PZqD(mFn#*lsl^PJT3APqi*rp4R{03EL^lnyO52}dZ}P*7-G`k1u8GS3h^t`e*HcuDy@oI^XIQ=?X4jRbKBN`qfRX1o zl{N?@_rU5%0ezY{i{bdKD$qK1HiuvR%8|a0)g&u)NXt^c+1`Pz)e=>|o>6!1o~5%Q zUxold=JZO5diwn}o-+?o7=>3jEVhVCv3Pu= zCg~trA_}bA;>!!+a9LKG#ka4sQ(kq&1t`oSwPhV`kMG2AK|J{^Qv+UR^n+vwEUdF* zaJS-8+WST&;-auIHk9yqn_74T^rB;z=m}vBt@O9I(kH#@y%hcElNfz3tx+V$NcYDhS4T!^Tvw)gE-26K7f*SO zaS4Q7ypetyNt0f@zY^wBlVf>-Qd_*jCi z7_$!}95M$g!lg<2QddMN_Ij_xDw1}qeZSQM11&m*h-PQsF;1C^l!E-KwKDmal9OUl zid*QJ*?W$qyp0Ub6A>oYuSH(7nh)WgPz#()M2TSrGzR8^?alP4u0YqzKGO)jMKcr( znAxs5<&DXwz}32W`|0|bH!4Qd;%4@1qy|h4HoKErzj`6g5lmvP*e0MDrkTpjJcWnH zyUC;~qqp-5%9!`O<28atjc48IN|BDzv*TVQubBT^*NYcfRlHr(vBo9Q&Tcu$Wiw{!Yq`TM+ z@K)vXtxtC=Sio4WLGg*rxp%3oK(y`qBhoex%%s(3_aou9E~c@Zqv+I=cD-!tGkg62 zptQUuV7y7X4>1gJJ+|8r!_LNB*0wu{VFgdsa>G7Joki}<*j)U28lx6zrl@&O zszXZ>G!%*MGoVfrgn0(6f1(2d%&h=AyA?J=8j`&d0GMn_LbtOHDeno)cvex-oC|NXfBSa<17qbIS6;tZZ0jHm6o zMCBz;k7Gv$Ba>J{7;L)KHG0)9&qw<0k<3X}sp3{=6y5E(6FQ>{R5qAX1GSMJ$Oj(r!)N8|0r>4AXPAR>DCGjq|NZd19(IA3b8ylZFc#x zT$~mFSMO?zn*%d0&&bTqA^Qde8Up^1cVQw`xPlM$YoS1*}R0>BP zS}-Ebql`Hz4*FWpmQQ(la_vFK$+1jxnjDwM+<*?4s3GPqs4F*BZxf!3zKkg-3Spa| z)VB7}{Nj-5C>Oy+&IH%;j?;0RSp>P2y?S4sC3MjD2~l;ln<*JQMn39~5j_fb*Pc>i z*pVpaOUgs5@+I#dE8nz&8IsJQNKZ$o0S(T1YBXH36c_>7II|DARQf}29W+SlFF6hO zy)TJ2o`NCot-uDU6Le!?1+%z)XUJFRorZA|3&k^!)RjN!1<~oI8HgZtFk7k?D#pR7 zvcNJjDQ>pB&WHYpZLiK?g=dj7YZ7yD@2IcV2EEp#Eq12K5g_@Izo6gXlqB@(CKdsd zkTF&E)h3LcFW_sEt3Hm=?UXxTkhB`ABr?qes`;)yDbUoIyj+3NTMRbz$$d_~8{99D zdwwhDA>t1rmu!MQJJmZrxh(i&vVxd-SF>5ZeQ1B^8SL4(Chp1sN@4Zik9_Ei;R4*d zDup8n8xPbd}!3iXKy*pR}j#zPsvn`DN?u=frM(-#nycYAVU=IVCiF zOquf}krg??s^=P9so=vkVKqvGF8e6sPp_La1lL!?FY}0r&-rX%G#&#H`NM3Lx_o=b zS$nu4C?ax2v6lvra7U^8@kw0JIcvPv4Vl;Z;FSFDm!p2E4c)JaNQ&)TXv=x@b;lgs87+>gB>gzK4xnneg-+!k_rFiX z6pWVf;?9)mDoQDPMn2E={8)0!RhV%~BN|aV28Z`lxVkLxaVgQTGdlcfV|B^i5L>D} zv(|owo%w-r3j{$5vKRdgP>x_S^^2F6Z|bv0(FxJqE?MY|FJD9ANloJo^s*#D!`RbL6-D`f&R>8 z1s<5WDyU;DMs7u-eqGD{DI+Wn@VyV?!h}e2tY$?wD>VolfbZ;v!W zdui_L+!lg#vl2rlajxauq8lu4=TJS-zKvDH$|usyn37CICujBXTL_7}xRE^fW3Ydkj)zD$V11bcL7XF)(6 z7b$<%s*1a2#VIrR&GMf#C=$^JzByJZCIEIP{OE9ylMwB_%WQhD>1p4OA2yOw1tC>7 zJ-%JOf>zyuQYVBn(U&|>Nqp1L4*Zhli=Qs7)PhFkR!Ava>r90Dn1Ww;C9vC_-58eI z>7A#}G*_3aMTlNj>?lV#wTdV$f0bEsZ_8>=Jn5x^B|<%a5kJPM+k};o%SxIF=TS57 zU(~W2&D(x|h2=}Cq8g=)XqSLvi#J1=1Ns{=7Ky2CQW*Q&e z0UF%JXK~{*oDP_9mE_5s-6*NJY~<4ETy`@w{eh_Vx(UQZTMCu3AY$CLd?Cu1kB2us zfTHUj#DMI_h65^$h$TD@su?W5YZ7k*c6kk^!q^(DJACWmF5M{jjRh1jBAoBkJQaD; zFFEPVp1X6lQ(nm^9+h9HB(~bwFc;{1rL5thNsH2YYHEA1@X}9tZYrZUUW!yMKJ@66 zNyiPX#b4d&c99cGYKGoiN)f|iXw5`{sbH=r`7j+*$c>|DPQl)hsdbs+e`D{>!>L}w zw&449N%B>cO1_@dmn`L8}9qMuj{Z)1BW$=EPm}Q zsL|r6q465+(ee_+l=sH43sYwK2hDrz@e9{B-Hls56tmYeN&8J_3QX&nbXf-Cp-H3k zMqAXg8T0jX;nT5N2r=7!|DLJ1t21nP~@cl%@b%BQST%_%93&OF#cU4BEC)F$B_NT%6n@Htdplc1cdGi0PwcV8D6mF8H&1 zDw(%I9Nf=x-w7maap^BU5@q+HI>cgw^tYRc4MkQ(Vy3ucpE_#%pKAm`QP@5b)4B5n z40AM;-K&JZ;7F-8%i~{fcK*|+SAiv8@J_uy9I2EZUZXODPXoO^L+isJfv;u@Q=P%~irNMOylvuTUjhZ6 z;_^cIKN1I=TU;~geuJ1D2H0z3`>cG!`BSW-7uN1Hi3KNnZv3?b^k@&gX1&>6zQ)&h z{Y*}xazVuUcZ@q3>p!?-<&UpH%26m=_1lwYm7&tOdM@2J7rIvrBmrv6YQ=(QvBp2d zy6Q8-yL+&q<$ve6Ui*jTvp2k-fKsUw`hO9M0%G-_*GLOe7+2ev5W(>0ZxHS zm`b0$PRBuCW5r^=-99g2^(cQCJxpnR4k-pzKeX~~rRVW#sK~h-@7$^id6Dl#wKsxn zMkEvZu=_}1FOU;2N2Ui~|DBKYO%7i3(ScpVHk9V}@Z_D&jlc#JZIPXSIvkDiAlvsf ztQ(H6-1AvS!BOfa(yN!GiZh0rMYx`CZNNU9YdH#!`=lbB$+SPJ!|(f*}Z-ZzmNcX&EOBf}h(L^(RHB zp^8Kw3)9`JvmxM-0UXF4M4o{LwG;(FGWmLiwvoCSAbM<;JulXTi(3{|WtYD7i5d}` ziDw!%c_z%HqeB}RFC0L`v<=Qu^A}zT5aHb$V02#LX8aLk1 z%D7C4y1^T3<7e42O94xI3>~m(_orM-Si2lCbX& zvtbc#*G%}>U1m^62uG=ewtyuN2wt5cQn`2fXo1M*FBwQrRY3u8s{>qqP!B(a1+AGE z$%C>;6K|?B8A{G{%bNFZ{;5v&kg3Ah#E1c?=Z&8$u`I!&Mr@q{7|7t2y;tfR15kc; zZAKiR*75rQ<(C=Em${edIxoMu=A5{YQ0g{o4Gn9Xz5*NF;A1H}$oUBq`kSY2q-6`j zJ-M>jOHWfHp#E9>C)KT6@eYL*7PH&HNE*yMG$gtNYy>W~woTc~N7jF(FrDS!vaa}JGSC6@pJ2C~>HZ3~7GeL| zS_DJ7(RpT@*c)EGmD~hjqlai;4E6= z%&Q;LT$b!jS@|luf_PSl9#zl=Ym?CPx*C+EVqmi-xpGey7DDi+=8V1_du2zSfEX+e1`JCAwt?~PP{MM0DqUAOZPQ-JZXpAv2&b*uk5r~mURjW-hu*7T zp3|5z0SpTgXB2^EeJKvPS2qGpVoWk0SkqD9i`98n&C~+oy*c2!I5_x+D#iM2tvNxd zb+m5X3UQ1PEpB{$q1)eaB3!Tq~R2$bH z12W3ThT3}1I3-Wgtf^*c3r`6bk5E*`z1bio<3O~GjCi0SDgjK3g{1H9-@Bdq z$b}nF+I$DkuQ{8uK#O^r>qQV#ZCC|Xbp+5!EZkx{PVI%lp7B{8fWyLX>pK2xixEA+ z(cp#@xVo-UXytS_!bT^1KuHYT`S?q%E9D`M3+?h#%br{7!r zG%ftCZsxPA9IurdY8RY=pt&J!OpSF+)!akTd$=`X{3Ed4tU#Y6m(5F!?F~FW^bJvt z&N;exf+r5^Kb|=MGsBaSzTZZ|%8;fxLT6bENtpAz*0IQAkdS30o-sHV~MMmP`8#1;0^=%4v4~By?R3VQ9tF#u85D_*>ni4usvztOPq0t$@MKG@^4sMR$sbjr zee9+dmv}+4c!8AL%yo|mhp(b(;ql`#E7=FkI;={yX^QGoIy$1iWLRk?+CsK#4(oeK zFQ@^Cj={KweDmb<1+{Jl%B^{3`7r(AWhCg+-P5icRsBL5<4&H|{R%CG85Y@bM^|zK zF32C44myAoiEa4}T`tCN6o4(2o4hs)DaN?P<-A96A^wn{B0qpm4T+38=`2tvdD)0D7L_QoGx9{`>CO{WHQeR*92jGTOJGw&TIhWn%! zNM~B<56OG3^qhYX6hEGLV;98}#CN4k7l9IyX-fK^1n&81NZ_RC_y$vvWH0JVvLp2c zge_)eoW{Qkw@S-J)}pz>(y8L@h*wH1&Fv8DH1GKma8wl_=73nyo&fR}jAP(3D!|HLL*Fdn*OtD>@fzhRV9F zH4p}SNta^JYi^4s*^;!`=FgVW!BJZYUj5xc^9$*vu>?+=9j?b7`{4?r=@3JcqP^D z%rw!L?j&E`M9{}ec9c@V`$eje4{}n}Nvu#^(w{7z%FNHq<$VV!v2+7cmHnB5mYG~= z-eV;4buGMm+m?ZJUv>rqGmMQrO9V=RrGFt6x)|(i7py zin*S|R$WhCP!AO z>Srv4Qf#tHJ_|2I53k{F2&FhZooTA-c_%cXsQScez5Uv>v6X<1jV47p9+%lQunJO> z@i$uE-+4XqeH733u1cOVmmDoc9Sh{GrI1BPk#TyxRp~9&X)fUUL(0mdVFatTb8NC3 zY&So+zXF*xlC{B`)j&u9%^5>q3>@D<4*yKkW-z(STAe*iX;1U?3 zmeRG}TTGGF*VHm-Ek4!!m7vWzYFo|cBgi*l6)R&_TO&fF$`o@Okva4evm-J=vCjapN_p%r2>UC4#tBZfA+D<8oZrvDUFO4vez@$(Zs<)-M?E(Sux~yze zFm(tP>xG=$N5K`*aL}WF+^pS(!VH0e9hO_80hZh3ve+*nCxFQ`alUoF z4K&skG-67v{kMXI8i5yNT-ZX5HDlD=M)3qymDKP5i>3}LmYUG3(a^pqrWlo*43=fn zW>+kY&OE_(|hxJe9H34gGe#bI;7Wb-Qbx633Q2?Ox+k-G*K{bb9#Ltek%J}3+FHGc29 zxTdylvkM7IJ*hi(`2dRn+~9(dd9WVl|HB=zR|>9PnXUlSZ|U4aRiQw-5{HEOWT}La zlmZc;1PP=)F&VQ;jx|pZU0t z14KhMZ_TP7o9mUEMZWFK{+L<7mX3ZJ3Wo}5@IOfNFF<;GvFe?mIO1r{GmLDbq@^wv z^U`G<0>=6E*2mwQqCMCxk)+{ucyd9oGI)*%mp>YUK^QlVJP*&C3kwFhL~$?h@i+%q zW1O_v>Q#4GIqaeJ2j4x(U0ip(@wel=R;-+XjJJXYl@%e?)!Ns&W*8r2iX6!{#n`c% zL5I1{cD2qwxX|O{eHu182b%yT{kU1$3`(fHhrg1Kf;nd29z#Z{956yH3msj48%m+J=UG@2#G zDro>d7YY;wtfU{6n_j3?&3>$3Ndh$UQch84I)_3w_CzYb9>i>#pRQ(L8M5?m@Zv%o zEX`As%1sFUlBGPe&4+>_>*=tcw%QccWW0hO?F!S<1ug|)Ss^p2Q6bp_6G$lo?eJ?O zTGv}cucuT@);NR-fn;*u%#lxjB~X@u`hN(e0Yk24g4Razot$PU3f==h9S*~Dt*a(n z&|iZHaWf7(#h-nv<_nz6r|M7C01S4Urb8bnX?UF7D}L=2vI&*C`5?3$4)Uy_cXZ;3 zfP)b`gHB1m&L0Tg8?y&4d+bz-OR72ifc2xeM?4x~-hyA30AFrL)*aK2DVZ{@l2Zlz zgn5oeOvG#~05Vur!0mm;cL@1?B3tvs@mB?)*{kA$la;Ev5u}}a=V1~>&#kx!+)O#h zFqg+?)F72whPs?1v%3%ic>k>-HyAAflRdITqONuH6+3<HnNr zZv={rfvJLr)S_zuHmz6x80F1MZW*WoL#g3mMFSDz9`JzI>vdf@-)-^Oaw0#5ukK<_-xjwT^it& zzhAs0yp|yPEIt`~lzRiltx8D9sEr>v<_3jWLzwpBK!Rcr2erL`a~;p)2#}##7k%(e z2%Dnq8Z5LkNStI3H(LUFN90I;M4BboT%S&kaZ*-@9Ti{H!MNJNKv?7TU)qGneT7H3 zvmk5DP6kZNuNsaFrul^=2AdV9$8jK2*aZGjBXS#(do#Om!uI2vP`x>tL);9|3=j@fW@#=F z<_F_q->K)8LtC)u@^>1Smq@QYM-V3L+>=xzg3E&OwHo`FfpI zmJK9Aa!d;w;6V`$Xd_j6z`*N(yI|ZE33%kmU!_1u6EqvcgZ`2}cRFye2i#FnCBbrg zSKlvn`Q>-WEM?|qTjnR(%yBo=i^l(c)p@QJoD^c0YlOWXNa{ny9@R&A!DO9 z`oO<4vu)dn(P1k)<#wXK0ZAHxU26wrnkx77*Co(KVSW3+B4<4dCJYWXytHXc@PDAT z{(J&wD{h9Nw1a67=72-}RJW4ONA6)VT^4I#NcPPns816nHRIs9w#&qS#J~$*X z#rm7qr_-$ahHv2F@F^|4av^!Qf&US>vmy-8WZgiueOYFn0Hb@PvoKqKKy*_XPNQCjqwK?$!%dTM4uQS2HX|MK9v`*+O(jtz8fx&5q~%U=60JhgoKVAJx!i5s+2;3(frT7iI+`o#l?-d~Q$}6jZuq){O!(m7 zruW)xmYu(%PW@9otfvGp$0pFGjE7Hjq=rgOXXGh|eP`M#hT^@@iQ_N>W>NshGB*73 z=6-wfUpGN1G^>E{p*=GSrPnlW9R8Tg6nY0McXe|U!d^rTiDX$#UCs2S?uPe~-KvRC!t5n7pP6fv}TOCiqd=_rwtSU&2W4iEzFx>n-vF zA3>8&hI4NS_rqs01**T=1%`u>{~8V=)F5W(EBnreu&PvZ2z->vOm)A08-0n%{T1eD zVTZcOe!KQc7?rw+K=C7jDlgHWP>IsBRVM z_rdy`XuvvS77c&=+|R%MOK<2j&S_#(eO%$<5xH4gy$KJZjVO>}$Rabv-l{9Ic?KK=Dsgf%`bTHYcV_ zr7vjUej}-Oz|geqVDnFZ=gt-o#*iX|wiBC^h0IgvyiS1j{Qtfg0oU^_Uo1mB#veo} zXTHO*d@Vl7hT)DMj60D0^6>SaGxQxQS^lG0;GzH3tcDMuDZ7E}Ef#VBe1RE65>h{e zjQqSg!HA)Z@p$PvY$U4-B>>9d5HVKla%2>iQe z{2UE9iQKcdS?U6sj%N-dwBl-K`wC8jY}>ug!_Xh&1$WxwU^~gab#>gRUH-4>z0d#R zv0XwPPt;)YY5}V=tG;}zuZQvw%jFq(81%hQ{^P`qVTEO4#zXs?0r4uhl!N)eH}3{C zZqd}1vSCE#YIcDPbBhGB6^H`1`5rtAxVy9uL0d@s3 zDGvWCDU-4uxUIGK*M8QTgUV#6^2GAvE!oO*dJ2_SCqxi&2C@5^phS9_dpoBc-k|dQ zr~qX><8?IQE7qcx8t9qg2c%v2+&N)pf=?KA&u~*%RN_A_hoZ=5&vbiLEV6P4Ot1n? zI}8W{i3O<~JT5%n50;wH5h<;yg~&TovkdMhCB|PM$}9Z&^Tzbh|hqh zVvfdkHOySxihcalIv7k#46?6w7jqaIKm?!2^_PZJg9&_;PGl~N{SCqvEyWwN6Bdox zDZXN6vgeu#K^?d(0lX97`O<|9!S7nVzBal(@S9jZ8GPt$#e|Mh)Q?NU zV0oJw+6&ezYaTu1SKza@FGBWSt{xs+Fs$jRchrtxkt6>Y+KQq1(e^qXf9%}ms4Wel zRd{H>jC?E66B3_zSTMH(yw}$opQ{u@jzT_hF@f-a{4fPh*Gyl(`dsG|w~+b>EZC zlCf)vxfj=fZ>}Ipg}!Y#-WrsemwV?t!|uXQ&xvqOHbLeshEgK!LtaqLUR(siK@T9D z7SNvwoEb!QB$8t|9P%+f!pO;p+2#|)+FDnPs5FUYFuQ%!^F!P>Ds^y^MjjNzFiPy@ zdgqgrFWFi*9agpNre?6_7BGtFr31Cag>62*6ne!Egzs*jxv*_JB?J7iT$tEg|K)2F z4K2e&&BIuKPTyNQ@DTxN*Jc5pvII`cuV(dl-2(fOPt@FCpv|&h(mn>U#D^C4O^Ek7 z>^ND@NDaOWl8d0ZDi!R^V1S&(ary-OES56&&Su;}A${L%6zF$nX<#m;@tdwG7RHI1 zjy%0$o=c*w2C&*DZD2au3H|$s3c_C@poco!Y(_3G(qCd)G;_SsZ?|H$2a{~-h~ihX zH_sMB19C8un#4s#N8J(j0m>l!iw_%$1rEbQ($FBv{uKyz();SF*FEa%U_Kjs_BIOf zqZly89MmJQosVbJUNRqj6d!?6Z)t@g&+oKXpi;J9!5lUODuOBUl9%t11*4R5i*76C z7~E7xzB$${K>l|}-mJ9i)NP>p1ue1co^fe}G}!{nI)r=+wZ*L3KS{exjHoX4!t5#n zwjyCt0*qNi9E!ZC55ar=p|S2y0vwSnCNl~(=`wORvVoBxl~Z`P$@Ur`@q5SEWuQxi z@cRL%#^%tiCY`)g8R`aFTxm8oS#tN!YiV++7CAXj&Wjx=*eo-ybS9NuAh=HnrcErh zqUlvoXvaCcd%N>GH1+pxKG!M#wHvSFvLm=Iz}rjyK9Ekc3LyOF1xxhR zW#+->PVSp~ker-tmJd||tc>lHFB!O7RnA2^>zso*o=ky^uD8igEjz;0Wf^9(KA{^$ zUMq+a8+=es`rP-&sw;wHfTOgnGU&AI;}Ey**ek>PigYvJT1v(H=ulc(7jC)baRI`p$O z%a?t<+!^SoxiwsUP7GA)9(b~o{H(b(KEeQV@qrR?H!Di4h^r;5J#`?>bfA>d!zzpe zFh%b09=dPr8S$18JK7GKU&5{=)7oeB#6Uw6C<11Ip%D}ln5Pk+Y$YK+XfC4IBH5-B z!%V)FK4$7it~Hd{mBBpVV>wJs#Mg1yWN2DMFWNjD1k&z)*^UhuqM^8Puu*DaH5{T; zkFeLzpcJF4{Yvq=;#jp9MxX4;jlR|MJ|mkB0x{i(c4#nQxYyARRT3;9h;P1QV^S67CFKn+x2HRh2Lt0kf;^eo@ z_U0uQSP?$2KG)%02mjiGt3Z5Mv(5y=<}NTxF8U$NOiNlb1$^29X~rt2sN8Ai_{h5{ z_+tE9A7c%3U@a|qsq^O(#M+~jwY3#(q*a5Pe+GG}D|$Xh^Kv|3{LI?Nrxp`Zw5$wu z@4S`1BK7(ztuY2fl$k(qs8T(_>x~JdLXqN@eAJ_@Nb#MDnj|{xAN-wynsjSw>cjv8 z^S&+?=cm*ked6NsB*74vU~=qnP@IYjEcWlVUDOo>^|OOwgJ#7OelNkgsjO+mn*>a2 zx9JTEpE%(vU!t+aVy7%VX~o}F+}|{Wo69{PP$5~J~@_Pv9rKATr>I+P7Z~bDJtfH$<4`+CJnEJ zmkRxbF?re(Aq270RA4`qd-S>g>XPk~c73$`(AOAn%Gj`O%sdKb&Qji;^BPyK>o$rU z-n|pMa_TE+(8S~n_)JQmb?g+E=MDkE02$jKut$lKYQ(9k*+alYF zK$e1@o?~REol8Y~o)_AqK(s7r?e35 z?a>W9>tG4_hvA(ahE?<`F8;s*I=+wVeA)PCI|lgr7=x7+&pFfTzU!(}F zm{_Y#XIeP@{!{Totc(+#DxbEK|AqRU7B}4zG4)}h=GNC80kDsPU2gLh?}q3lE%i_I zItO1?yhs6~ZU#*J7YK3m$ITuZe{8zDi@cZJ$hpM;V)qpA&bfFwg@hOcq_7S z|KB$UUwZOt3Xy-P2Eq!5nsR*iozp`;==YuG0K3wr=XSx;IAH z!?WwU!4=3gS}f41nUfV^dLbel{i2~TWow5j%7Ol*_~0c1b?`r0%kzVtsBD} zk@E9Dz-ZVGW^dZdWztb%t!CN#{K=n9Lq5E|NT=@jb4Dk?JtY|RM9ITNMXVh8=lNA< zP-4@sW#%FdOjh?&zr$H%3TI#eqfp?1E>fkjPfRYgB8$oAMPND650iuju==po+xt%E zgZv+UhzT7u*`Hjs$d4R8{sJAtgko8axF@_0k%i0uK4d)_Qb!k(BLYg5rD$N2Sug`` z2f5%YjGbQF_3tNM*7b?=l?xZ|y8aQ>Z8o4UD2nUag!$_Q7M5(}cXSlJSmP(t@Ajar z1HT%;%H0~|cGG4B-teud(0>*mbsqDC9JF;MfO*_K59|q?9jL;(!0y`s)Gk>0O@KuP2?-%bcW<^6p zly9h}uliCGWCxB)YBzvgiRe8=S^l@4wK>w(L1vSnh^TRB_Ju+7(i`wC>jCxa%7Of zz(UV}tSog^?n1n7@^sJEkDR9%sO;9TaN^(Em0{cvGATG5QUf96T$veQ_(Z8vu=P*e z&x_%EU%th_AY3?$6}JBv-uLM{3{XOU=$!^!n}6)N(8I9R4(rtq*U>91Kk6WV2K;9d zCCL}hw$h?i;&Yd>_(4c*xc;9k0e#dpY@i#{L=8#q1HHnPoUIrFzxM<-(^0i%8uCvv zF^_D>DL}4)Hn}&37CuRjsbfUzpZb2dFcKJB;0#fBbk0Jp? zh<+$}@Yyc~lt)yv@rzM%ASvvE?(J%CiPP|dM{yVUbbDRe8TCw5_|>*){>)EF60%jgS`^M$!jf%24Zm(Co!$! z0}_}aW)(*}gdk377t71RSg09hfN2-vK}6P$PG=?rp4=P-@^%3drJ~VCog1NV);`fK z+&aT@_3(2&9~g7l|Bz;%;VMhJ1GK;FWBn~*DO<7?;?RkSlOKise?9F_#P#_EyMXEV z_(Srg(9lAKz%7w1z=y;w2D!JNu;B_|`;WZuy9YG9ei+tV{??mvMBD8uhJfy zZw?TK@YzS2^?pbJNW$`M{W9c0HAkBM&l~xw#t85^UnAn!kYyAYMpnRu>ZM|cpznQ; zOh$&5yYy{@R-rt&ZIkg{2|u3wc{@=4lMqFCC>0=1Og}P+i*D0ss8j9gN!eP$KwG_slR9u1NCJ2+&Ks7%c>!-Z^tJq{bwAizQ5U;*jiTxsw!+t zGodaqW#m0`gS83U_{I2_6liV{vfjXHW?-Kgx0`*ddx3lK7~6H!$GCkZCc6vX-|jS~&#*6TqA0!Th2wZ)-z;&h8(4HKKm6 zFNZQ-O8NkZsx^}Hc1ysK;>H+UkD~UK0}zk@cX1VevQX*T1cI0{%JQ}wKo7%gWivoa zC8e_59wFabq*_R+|ae>ARjGiov;poyJ7Ln%tQApT?BdSL5Gq;>P9x zr?|`ORd6N#I2A2LI1AJY8OiCvoD>(UD9{syTe*#H>+69_^W-N^OM82E!xP94iy+JH z1(Jy!B!A#Fbl4&4Gi=R!1cof|=_rJH0^2|(ESsZE$ioASJQNm$cpaJP2vUmna)kD8 zA>z7_qJ;0i_zG#(M5hSEfo!{Buhq=SNLwL;LK4oiA9O0^{0FW&=;)O=C8>gS5Ze7| zrhx9_qygDCEj_7vsHov&S%=KgcSG40XHxl2NrQS~nw}LvTx&2=DzfPU%SH~XGr+|% z0DikJQKXjZ>%ECcd+cH?F0*4I2f~uaYPX)nfobrkS(qf$+Ek{?=EN__LJV32N>xdw zDAKJCmR826P9uQpLBXw-R&7pj@fFE7mb<779s;uFFealA%~wY-ABtd2;NTTKzvM<6((SjcYFp|PK#dGChvs_tn@AdSJb1W zl0e)}g@?tJaD{*yPvNnyy=bFqkFb!Jz;5T+`V`Pd|tYMFEjT&Eo*)^ zKJAzDubptvif>Ejd2BxWD4wVL^ZP%ib89gqsvNoxoKbFpG2K^JzrH$9;*3<zd@sKqwq-Vj%PZ;YpS>ou0e0%VxRHq^5DuDZ4DdQle zJy#vx;l&P6K;?w7Uv1q+&c{7@VE6nNb0doY;Bz@h&BrV{ao6(@n*c2>+~XWq^2kqx zO`uU?M++8T+px>-YM{QYhdKMWHkfn#q{60JsmtV-u62LZukoK+G2Ont`#u~d9hAw|tahMOjtOAA0Al#C?^chZu?sg)7~#B>7WBfa`ug!jj@dqYaK5-A^3ml#1+93?nZ%D*>SiZh-#OeON}_+Ri#vq(_Tr4=M`gG-m-~fVU1;CFUEm(ph$og7TQCnxdh-F~~vWkyj z3VFai|y z_6_95jY)zKu1FpzvQSfJLWI>&roNiVi1mtI@Ts7{-)Wm@f!N+i_r`BE0e4_w$>!ay zQ%{irRk_8nmDP1!3ON&BUCIP1WqOiJOR~kJqTIZiMSG>XoG?LnWykZBVRlMv1w%IV zCtj#%Q}b80WD6q#hkf?ALdg5=Ue5$YAeTf?6V)xOCqkP0d~7 zhC-q@v*_y4;XGCv(tRFd#sF46@m3)I&0aa{H_x)3wOc+~C$S`v!jl4j-`DnHyeQf~ zjv3l2nS%@+B^U|v$qlhO5pL2(%&5s9)uzE(@m)WLKW=YFL%UVqvd#OGMdM<3Drz0b zj6U4wv`1B-HMAGD$wk7ZAdA3;PO7w0V#gksjScvzP^%y*M#d;cNqYzu&JSt#o}0Un zfO8Pr;Y)FLGZG8@#Owt~{hDu5VqGuwcw?TS;DKwEg+U9`Pu-eRf0waPVK<0ZRNrte zID}z@Gd@fIlc_Xqj((TeYNwHN6&(nci3cv`k#m(mrj(l1rQywur5}9@JhKobzjhVO zji24@CB4nA2kpKC2YqL&sC5TlG>rFHDntX*60r+!eZ4%kb?!bpKG@UP|chPd7IToZG4O?_(j&I^?{+nUq@1@c=2Sz3%}D(J-D=E zqTvn?xcKEf!(^fR@_GcE(z7j@e797!zfKJoDuc*i)_cdE6lZ21;{!|*+!~sY&;~kf zXy^$IF|6HjJ+TE2z_H5&uks4Mset?{)wyk!h*(umN0bL&nRm*gk$;}CUW5ttnP2huD`CL`^=;E+Sb$1D)4N$Sx{ z1*|*dC6wqqwf$8Fkg9QpuGQ4r>5bdZKk`&3UWQ$`}s+Z$(c=t5ZOh`M)rpug(d zvW*gaPwBF%fDB@53c_Go-rr#)jLtPdu?X%i)O?4XKjY@*Ff{`2Ct$qDo21m zUsd*c7{T1V_0hc#?(|n{+ao6H$6VTyU0@_6noT^h(M_QekhpGYVvjsk^ zBPR{H75+{cu*3WxzbiLaE8B_#1;9!2S^+(B@<&01M zaAgZnMzShathO||k(VqjdVJHn}rU=8&#aX~Kv%F>}}mw+dD3=M&` ze-EgG!pG~@qWxN03G)%q4t}*c@*xgJMfSoRdb|+F+x@%w3f_kSNXh|c!EE!n_;J2+ zDhj;e-Y$kq>2TCq$(@ha1&Be}7+vf9|M^-4i?zJRUV~<-6D5(^UH4)^?zRr8oJ)Tq z1wK8iv>w^25cdZ!%7B;d;Ln>WDbHR{i%4;B3&R)h}q*YH3V{h>h>3=m79 zK}%75s))EEt{wZd^~1g=}dN=qJVJfpXwq1OUl&6$gbC)PI7R z7StqV=dVV80hm{FJ8qHFH{xXU-#avZC!`G4Ioc&h$oc8<=ZmXLC{aA78uhJD=Kk+H z2F^|5gV?*HKy1!~%DS{k(`N`Gs42?+WWK)(Rm&y!=GpQX_S^+OGE=}gOhg=9YwyFP z0XH!IEnWbVFasD9Fo5kich~xg+>+G?@9k2LVM=FrR>^mcqFE%!kmD6FS%v_vng!+; zu8)r#nBaeSl_9kqAXOR2?Fg#>!HT6Asdp? zbfsz^FFI&Q{n)6Fjb*}rM>f#=T97524H%H-l=?OR@J>2ME=_AE+1)rMb%!q%-$=sB zkAUIFak{RzsWQ-O=9Fy|nvOw9iAP+#FXA_M>lav$J!|uQMzB0vX$#=WA~<05nPkqV zY5H;q?LTpO7k0{Yp&&488c`MQoxG7_w;}3dY|;<)rstj|{lFxT({xbqBb49k*<^^g zPSHYHj$z`v>!f2E&^I_D z(QOYu*z_y|HG*NbW#HvtH|xIq)PCHJD+CiYXvK>4ow%MrAve1>Fdy3;%l3D}ggZB`2^?PadBe|mIhZ%>^a9m%^8OMamzwpNnOrQY> z5+GA|-?-1)F|d7Dh!T#4+BqZj4K*{ekZDj+jA@TUGL;w3=zb$V>7{5Tk&TU8(gi z!^7~6)&MFLZ`;gYn|R$*JCoFElb%`lOUY^SV?UT#mucEhDO1^a<-8VdC9H1zdRrosy$ZxL2`4p#XP@D$LhjZ(KzT& z3V}1c{1}@J@eMAs&qk_W>}qtiif0|zGrTEH-M{XqUubKUCOG^!XEa`eNbnF7Ps&o< zo}?sDud9gL$n6TroH~-H83$81Q*4BH-03y6KUcMy0tU; zB~{+DjII)|QRh_b9O@^wM2R=pg8_H~b{sr^3(-VnF|68AllWxMl^a)E zIJIwFGkHQwA{|^t81y0>tsT1lMQ5QD-c`wAG@&k~XQGHqDSdYA`u>y@glz#~^DLZG zfqmh5stRv#T;cCGx$y6H$cayYlD(+*pvQ@1yQ*-%J<@fEnpBh;gHhLhDIj*0|5%3L z_x;^DL{?}|rXHX#`15=qs;Il6Nxkr1goP8m9uWb9xVNNhy6Zp17StVdWd9Gb<-z6D z%KYT53|}LNLa*dOcYvtsUlV7e`FhwmjBr*>J9qCNk^6Y^_m$$rXVL-|I=XY7`qv^B z!Ql+4PDYfz`iHc+#K6Xko`qv?)X3h5V6_1;ZeFQkIq~N}Gm%y7mwKWo^bkkNTc!vF z2#+5k#Z)a>>um}rF!FHN<-!OcfA?{t?n`pP*V-Nrj3!{xK^z~4YUmko`Y8;NU#XLs zflmzP>J&xi0-)ROX#7jLrt>KF<1{LLXQ5TQAOQ^LuA%&Qqo!-FREgHvXPrI|XLXyb zePJtv;;&e>3Oo4uFxv)5DoZgsgs;II;`JWMF|NU*T8$!AOO|2HXiAd=|EWO)(qTKQ zS#%9=qCw|wR)G~MZD;9!Is};Mc=SBNuu_rc`OkP}d#PFFI}}r1oUj8Xs?=G7>e`j* zm5+3r!znn6J?bB44khcZ&vYJoMR{>*c@<*PMM6up7z+hnY++oezd_R1a9sQQV#Xjt*u9c+GE)b&b|J6IekAi+g$xGF;hsJu zchylO&xUaJb#^twND7c?IVH#>PyF5L#2^;KlThb#C>!vhqyiEbL$vl*#q#_dgga(8 zTGc>i=yu=cf50nAImW5A<4-hvcOH_X1M3(;a`yR!5t@Yub*;C~5^h2GvwD;KeV60e z2us@}DMhNZ$aDvI)JGtfeSJpiI2j_kqyddW%J)*>JCF7QKYWtP&E(d1m*Ar>z4|5T zbY7!^M|^9mcw?mD;A(H%n@?^GaUX<^=vT+2Yg<5DE~12l$0fufa>ZVtvi|o?{`Z^w z@8$Zx-;;J}ln3v9V??~KZX8lFz$EfFX^U<9$>E?Lx(Qc5ztAUd@OJ(W>mJgp{@4Bl zhcE~#TJ15(hOkqesOHPxaQe!gvd!Z6K}~^0K+mw<`9R|913j3oc6V(JU|Dvw$aDi7 zj%)$+&4s38Z>j6_wP-f|s`SIH>DSP(Na?XdNHH5> z!2-PQ@6w}-B1}PhsNpV(V3ix-*RlxJE0%2#JkSef~tKq)G0mYzQ1TA$kt6(j2*2Qu^TQVDz*Kao4?F*&`#G@=jL zq3u}%;Y&a%(tuHtvgBY!r|=x=z`;mL2SgO2KO|qiA^C2E8e*;sg(^hjopiELEG|NJ z;z87ocN_89B~kp$X(;FJ&s!ewMY(|23gL}D%&@3WJ^5mZY2s*g<7M)zO1mBD7_n`e z)cWNM3OYyNwS|vJ{fWN(04J(kzS#N{3uWNcQGwZQ_SDMGHjTtmXu#M)Jx7TRUXJ#* zxd^fZN-D@44v6AyOexA9(+V7b(Tp7J>gc z3ih3BBOCY?PYfOTei%}4u-L>Gq}N_#VC0?q)sh66VclZ1$=w%bcZUFT?A7s~`P>WDCO6>hfqtffBG=-!* z7dWLC11EbO&_I^O%z`fcMTDdyt>V3m%AcUKa&0-)UI+qkS%>wp{HC)eb63~rp@zeD zOF`E(8YG>P(rR`NK)rXJ>1jjGeh4d;-LZ*BrU$7)0-%>pTpA-bEUkcmt{-Sm<%7@* zc@Ma2_kmJyK=GJLO}^67LfG}u3$!t+Yl=&ypg`=05}VlNM+X$zyuU(^BaL;raPV5| z(Z?Q*JMa+iAfPLz|sbKqt{0Xzqmbp%U?@Y9D05Lh^1k3H)$ANUD zGXl`qyjOSXJ*?Lt_xQnfHNIEsMox{^MW9yg5=?{Bv0i?l7R>27=Pc##9o8LGA6Nyj+9 zQZfn*Ei^prpvhTI{a1nr=+Jy$0Lc%c?n{6Hgl~7YwdF$g#Na{HmRtJ;2~7|61|;3tbr?l)S_H3N#P>IOx)<;OaJs zwXa-vBl>-pmf67xfaf7#ovcqCP)!(DUjKS%Gbdb7cjr)SS<1*fl&1x3#&@gG09l9R zb9F$)kYB=dq5Z&2i(qpJ5Y%RyAuqJ;J9c(hna%BAcYM6TcMD8nsUAqAF;RKI(Ca)P z=YQ+C2Q8{`%6n2W=LKn(fjqmvPk-<#1%7J9*^T@x?^5NuZ63_tS-^|%b?(;RXbYLV zu~-PC0RlwHi9v>^kdbpn>YngF_ZFm8`${m@seHL4mQ6=Fw>>N@dEiJtg7J z1T;H-%2T@T@~qTIEnHrZ2M_v1;3+JpFI2!d>KZ9t64XdwpxS`W5rTQ}3}K?~mde`^ z9al@S(ceG6PU|F}l04(vdKQy-N*t^1k7>~z1&&zzNrv6ia6PEeP9SE%u(xr;zAah8 zo>uBI@VY%Uetq5XVn<6`j9-O`=KkM7CToYSbvp|gC$0v^?IYjX5$@+nkV~F9^CyM` zt3g8C=6cUu7_s5A8tUnNlynCHWKmPlwzbyj!$sRqQR&gzh|wqGqIR=V-GSKw^!B-+ z4D18m>8pJmu|P4z?N#wpO#C|JwQ5&1iRw_~RR|62aNFpq4$>>A{Yy;nOKA+syt86L zN%ELZ5s=lqkes@5s1Ec|Z>4H=MO*2vaqVtrj)MR%TP)*r52zOsw>|~LcmfDtDT&8r zDe}%ByG}w1!&w(XRn(rViK^9OzzZluhG9~N&e#O1!-EWS5r~Q+fp6V=a52D6R{=ql z%ry5AqXS|yopF$tiRHUeuZt6VwZy`|%Mi=IswT7VUL#%Huc>{&20Jx!=+)eAeEi9V zEi6OlBDdfYv9oP7W9#dV3J9^YOOBa_h*m5Y>CZ_K%&dV)8qZASyDNhxN@{!h2jR$` z`UB=vcWA#bPYAQb7K0-+-Wkp7!1 z3BKo?_j|wZj`16J+&k{@pEFKmXJ_rT*P3gV=Xs`i!^UHcGJbK&&ZEAsG7#F!Orl2? zO9BQfvpD0@KMR-|Yf%4;1f~ODr>N!VVT@mj5-4g>R>eReBIUlvvh_1nAVX^3u-_Z= zGle_^?V6X`oxg$eUn~v?9dkB!BHf?_B&>!{xYZNJo_P7+~gw>@8;kev=KbI$f^qsL|`r@mcf%4$3bGpCX@6ugl^jp#F78 zeIdPh+0J#^o#N-TFs<4Z3Jl+b2ZfO5ZKfX>Ee8>c-a-E>E0hJbm+Uzt_25rGc(Owz zT@DKBQX4Y1{|Y{o<8*9<0mF%L@9@f{Th6npY^=ko29Z>7F53l|oBOKj3ds4^ICRy1 ziAQv{XrQxExyAjmbSswxT12=Q9$evPM-!1R2?rc599i<-HI#JY?2jnTZck9Nt>$~M zYyj%+{ht&2L4q*4l&$W zKVJ;a;U$-y(@%xl?e_yAqx`fVGROlzlY9^d9%)7A`m%|0qWn+*r%9h2Eqt!+0T>vY zvA+72KMFQd`AYvnK3PEO;X$DPN%btI9|&Y;1a4|BPRkY-hJ*PSP)EpJ@e?##E6xbD zs&!)RI_DNGS(z2#+8{bC(SG6elJ4s|Sks^k{Q}!qHNW9VuB4G{=sFzcxUV&z$5sm- z!GTd@WHuVw02;is%w@geKrE`f4+}bNg@DX^O2=D%vn)tw7Ptr%Le{Nt^a@XCfgUY3 z1yWdg=Yeh?hXs1fAhiEY8Q+H^bnlSQ9s!$mPGE(QBq!nA>RQ&}l~jyC+o>6VQ)fCH zNv7b>HuN_d7w!Wr96<(hu&G8FIdE7@b@haSU}olKoX&>tx8xg)T4e&*IF3DM{A{1T zTr79-{XNCTnH86_uoZK_)e#dgNUHRSe*{YnRk49MS7v&MNK9O0^yE~#|6o$y z*$$MkW->6~OWF|uvu&48eFe(kwg}qv2PeTN4zvz9@M|nB$>WG5;t%H!L8=`Y8r1Hh z6o?XKZ|8{;YFqn(C_#$(gKabr=Ecv|W(96N7>$TfC^Z00*yLF>R(TtwOp-V91R_ih z0=TcNWP}mqOu+GP${9Me$+i%+b%fq{Ey=H9X|KAZvLb_2rQ9(-4Po|X(E7a}U~SHY z*DGGr-WRno!UxE=xv=@k$?^t(cE0-M#sZFHJ{WKv0PATJ30BSIGW%DiL4yLxGMkK#o*NdSLJTb)-VY9>{Cs{yOyJ z`}RKSLj>nDho?0A_d~PfCQOLrBm7 z?M}OE#h>v;zN^g8tGJ>BDX5crkB^#Xfy)?3#tOVqull-gW`^Pe4Iai35J6OzyMA)A2(*N ze+$gwE%gw0_5sf^#9Pv~30f2Pft9;F^_nNpt`R~+JV1>i-9o_@x|7cT41fD%$s!6W zjV7%YeA)HMK#a=cKZ%MD@_zWe{S|JoYz|z2enbw1HJ#^`^HylioFI4o1XrK*2rQ#) z(RFZs?A|Fcx8ieQbTsr?R_U^ihUdG$xuhstKato8_N5E#3-2a{T4?7aC}(r(s0@O- z>J4Qh=UCbAXO+_kpow7U&RtXy1-~F)u|T`9gv{!!Ag_FA0E4YKVr9A1|1N?J_JN5I zJLQuggn^^2c`wW|v|K+I1~rUSBRz+ndbju>)V^5Fe^Ymh*GJLBQ@M7{sCf3X0a!GT zz5x9Y4{0%L_Y8P||8;iHS!m@n2C8>TBp`^@15~iLR>L$8-2Og*Il9^#(c_22H!<+l zk&!j)*Bvn^ur1K6=?+F(vk=yHWvrhOxKi6x(3@@z09V4xaqxIqg_e`Ws#-Ktu^~no z8~vW({u6-9B@cgq`Tfx03RQ77P}C1w9?cKm)<%6&NoE}T>A84h@f7eW<9x)Qk*Hy zjer5ihpO${Z>3$Aa{td2qV4y+*k_%>^E>c$58-veK+hfP;$eX5lif0&8QoUyeDFyd z0X%IqenyotI9~;l4~;9;=pFIrSoEFp#$zu5ogIR zY(3*#Dn|~4)TQ9e+XDo`Wy+-=f{iE>6cgfmv@y`o3+cQQK1ZmX=hT38;kxYoH1N*( zm7v^^MwNO9xacplfMA@k|A8BCu$Dr+Abcq8tWFeaklR}b_==;-hxbf39j>_yh^$kf zu5jI;9wp07!*JJuB$@PbADpwN$)TjkGko-JH@`bxbv@%}L7|*7nl%gj%rwm5Lgr=` zV*)GH&xpIsCyPxA`K;tLYPBMcb~bHgKf~(V+>Ko}5$X$UJsa`SaxN)|nLnn7$j zgCIL@zDY~54{alio(oHt5FoYZ-49hqS9_W?=~er~#);-v2mp`HkjA*7At2bLj5Ot` zXO%~MFXS5aRpYj@qMldZPlckSL%_nZtNG(RJVTs5lhCH* zx1nx}(~)&tR|F15CLCgwxtTsn;QIGfGoJAo5tA(UOZCOF&kt^{0*41>cjTYDgA>_^ zVNeev!h+t@wFU^44iKpbgfO)`V(MOS+;1QCwi4GdpF0VY)mfols0FGmoJwOxPB;x) z6U`l@b=brSaIq=`)4Bp8dHTi@mO^pOma=+HyXaw;@JQc8UdCvh*{b4llUf(+H&GlJ zP${qESulDYXG!mfa3E-md8J$H|H#*=HK1p+!tXTR< zp{;P8X(>X%Ws4s0GynM|7)SUJ2{7{auJn9DXak)1%NAz>Brtyl+QF_gdaDSB@IA$a zGqV#=MlN#g9uG|@S{{7fe`(1v+CHS3+G5Ui7GUWYfAZ$0w+aKa=hMc~zQO88r4wE) zQ>EKy)XBkF`@5JGrQ^C=ea8n+Ogc!i11Zz1s|ndHud|(qXsKMPwda;O8XLJNv6s3^ z23wev0ojQ++YPT|!@2lsk7?>P3euFVs|NtKVg>JAt7tW&-KU49wS9x zuHwC?hvJiizoUm;kz$0UBtaZ_Ur>$zSiKarlgb77Pn~rgWVEbG zh1|k(i$L59>h&kyHZ-Ofh$97!GeigurHSiIif6`5Op4r}DHVYv{ktXo{@1xU@2q(Y zaVLr%L8-AcGPr9h2VVTd5xj5^A}~gOJgEFNKibqYbi!M$gO#O9uX^NVsH|5n0wD z_x6%>|QyKPt%7GF22CiaubRzRYQO( z7`*h$`<7y>8nHr%x%$E7-$6}H9NYq#*YM;yFW}M~)11kLdKsuAl^xJCsAtneG1-}5 z|EolXz?Er$m%dl{x!+cGnI-ZZi`S zt~8}Hv_nBk9p>>FJIsg^2~AH13p9kH6A7bFUcw_cAt$cO1qc_X0b}E+*&ECvmuD@Vc-cGq;!Yn^N5=Bnn6iCk4fGAmQ+=;clkP(tEz(c|6<5b^X%oOTD zi?h%8OS#JM={O{?b&%kA^Im}Yy0=W0;lq>;o;~=Na@AnIjet;MPACM zeU=_-orE<;sBoy7gnngo;>6T2#j2qcWswRYJ$0cMH0YF^fr*cBOd*j`i6c{&9%AKw z@Hh8@R;CRK*i2SQB7Iy`B1R%##eA-!Z;YW)kS%EX$WxGF`|{%WwqX#AE}CRewnicD z-%N7yn0`RIY5OTz4vg%XEqx%MP(C04VmH=;5X_i(Lx%YzZDitT#Z=X}eRRyA+eXa@ z#TuFHHrT|J*Bwbu9U@$6_eNMC3JA!a@ul2>t^nazBcsQG)|S^>RkDlAx_ub1wqKYy zw`m2yn^Br46%u*PR7ywfHApQe?T0YpUY$$~Sd>q4l**)h_neoG55c~=iB6{mYkU7n;W$UVL@EQZj@J?&t265M%x1o{C4$k?#IR z(BHFpfA5c$VF<;}0Ri~Zch4BP0Z;{Zr5;>TQy<^UWaLrPK7W336XKmxs4}VyULSkW zspu4e z>joa@csRyq!Ni@O%5nQW`sHp<6!E7_@F$!)BThA$twFqb3`sy4?g&JELb66!E}FN!Kx91 z*UxQFFe=~6I3iY6fPieu!7ZU;d{s*_+G9k8WnJQ+K8Bak%d?aTxwcouL1(`+YM~?i z?NzB^imev~lHO0cH`=@n0vhCgiyJuEMHC!`aC&Zb#wj%YK2bpEw2dfVc*;t@otsp= zu0-_kb*lN83iu>fdWr$YcXAdn8(H1XNNSWV?fI-cWdi^?gQ2t*dLB?28*P;UtZoaMst&!g zhjuh3D9Y*{slf59_t6%>C*nnA(_;K9o&6Pa6HXm~K+p`k!PbCsPKTs~QoFTf11<9`LyMRNvzHMGKjCPEJ??R< zB1)wdhlGvxUq1h{|IZ}^D^>Tn#6xmerJmp`?U^NJ8IzIPC8_Um#PqGk2Q%+%k~JU~ z?~)mfI`j#NcQuxJ^IGmkjErJGwNg9KnokhDHR^e|7nD1Hcm-J+o0V7=&dwm&qSo)Y zT_+#6Vf~)0NI4uk_govKf2bN+IxvlUiCZpq+}xT_mN-u|PeK_Tfu56+AU_4u%arkw zojfl5nQ<`f&b3_W&-;akU+ZIg-NE4H_3AH3+$wqIm7ZwNq9!C{CzMdR9$|q^>N1;O zMTfY)tCYhEPk;%{a}uAfy6u1Sk-9&XCo&g`6HtN~;n-Oc*$P=0r2aLLI$#j}_TbNj*XrcT|T;S5SkJ8aB8F^ zR@w3_d4R|?sF|F-V`T<@DFYEP3VTvg&lIjZDMtQe^ePhRT4TDXQlX1S0g&SoJ5nv* z73rB?qBJbKDb7HgN_(%3rF46nP55CTDNxkaH31a5f&mq1rDJFGsj2Wu(~Z-@0WgJCf|vRR$IXBM2qJE`kAcIr*$BjQ4xS9woY#1P#;YLoR~vnX zsR#+oIj9xMzET%X(}(loEGpT3mc3ylQ-FZR{mJz2w*k5>l zGj2($6>|~wko|*m-d+gDK~dapqe_l}=Udk?9ldYvR^uv@Q>>Z{6l$J}LUOa*E4GH2 zrFpv)Ubtz6#za|GTCt|cd)|dD4EhS3~-wUbYvFz8;BU1=~yOo z>lmc0qnERYDWQs3)EmhPia}Zt{p~A&!}JWw`}MZ9nInLU`o zM&=ms5IrdB1hv#T8(nOmCld<4>Yp4yplo?rN@%d^W#F}X!OowS>tlh^icxuIbkkE* z{9950`$eI*eVxxiFZWpDD0U>tLb&^Sg9B2-MW=ClxgEwor+dOX)KxDY`ZFc`=gME5 z$YaDsN}Gcgb=i=gYe=(RR(4+9!Px{{Bx1%H5Q$9CmO3M&IOtX~6(QdvStOO0?w#sA z)z}JBc`7UH#)3RSdKnR|k=~cf*t3PCciVyNRR_Emy{9;{N||z$ym#9Gg*W5AV*thF zptzrD9{Tn;<(H9D-MSb4^-Yq(YNorsjJU0Wh0bGH|TG0Hms~vwa zQjNY&Po^Uwp+!GgjXVy}sIXTHN*KL$QWjPQT$e)+WfsRg)e|1QMsr=<8hbAqj9d-? z-9i&q$8Dv4LGMux98)RYjEXy|8Onc*3GZ!+eNiM8hy1iaVYqJkdDyB zlX@jlDvttZbA?d#$P$BLKh5Qf^_F2|htsr|;sX4wEnAa?Q24I&c3#(}c63v|(`SX! ze(@{^@J>S_OA;SAlM~y<;h=SjRfE~n8w^E$z!&86gc5;*)d;NJwpt#EPr#lNOx_Qw zT1NIm?ijbrLv1e)T#&l`$}yG~%J039ZN)2g%~wvhqGkE`F3syOFYS`vJW{)w43n|K zRSX&n!7FTI@(Wj=Kq9!-gW!>zsy@DK{N}dxuW0)x?JK2Q0SURAHpg@1f2=l!eSW4( zYGLF2rKSkeJextk7pa4o)JT7q;duHurdx8|ixgmmalJm`Ltgd=EcgC$yLgK|lF%T? zIyk)x7B*3*f|JoQ)a=c`Bz5&*!}nKWEUH=)CuKfnL26@mCvJ6;2Rv0(!Oywb!8vtL z;lV>&&%r<3c&Q+SRwNTZG5?>qV_W-4k#T%<(L8NYi|SCLgLD;yBFNTMPMcGw*g87bu^FUh z%JaC#_Bd%#q(|q*us!FlDIKykJZ@;VRC6ZeB6)nid=}~kCgcm!2dPa2HAqA=ZYC(u zo**onD`*rHikcX7BhoYK44N9n#c5><%Q9>8PO5mhpW6-~xCR#~fSfWrKf}&C zKw$RYM3a66Tu8&!6hv&`yTWzsIR3~(H4;K|4r~(Gkyz=d6JKRZdt?_{TO|c4oM`0N zm`O~^?%!tz`I+ZHMM-J%lTe;o@2rho|EexXpPV=-<@aJ_(Rt#a)fd;4rq-*)W4NJD^345mOj1TgpPIembFbycOZFm#_|Ite2T%| zcX6-v@@x4oDg!MYNLB&)Vlonm30=G6=h>|8_O|bx7HcT*4<3YkXw6Cn>S`$e)iCz* z=n|2M$k(AemHx`(x%Kb!p-n>zp?YEai^IYJ;6cza!n+UnM5}$lTfIX4 zg;3tu6WvR02&^70Y@PWmvqtI^q%0SD0w1?&Cq_f7qM9dIJqPPztskuBHgU905vg1_ zmPyQDTT-v>=du|r8u1?mf+X@o$9fweK2^?bK}e$NXo95KQ-m=A*#y5eJV8+7>f^gN z0X0TRS8$(9qE}s4nf-YI4%^wzlDD1)XKTRk<_S2Y_xGykrl4eh@LLl;%LJmv-fp&p z6+wB6Fr#IFPBdS(UY|jX$4gzGS!)vz^m?#Tjw@flH2LciiopFOi*0IvcfL%RjhUp) zwbbfA>v3a-g+K~Tu#N-(_qWSM0wYv`gw+T8prBOxL4#HukTjR*;u97wF0fCgNzF7a{7>T_N?)XSh}l_y%6MFe*x zVS)G=z8HG=%Suyr22-?)s-9h*pIllC3R&ja?uIG40#?tIIWv6I8s!;s8~(@-l} zk+D15;c@)dT+FQ*2Ol>u!Ab&E5%;(_ z_iaPuzmi)r#AjZHlSeBK)(ndHRnrFd%p9OrME^a%@E_JFh?6c|CMy#d7b$II^S8?C zAV3WwhCcL9+qHu)mW|rr+3LExL&3BrbSb6;O_w5HoTzr05z^!Ld(1N!1zG+X@LEMvFa-N(RjqTWk-kut z&+yUGvmm18qKDLc?ucDpOlY^5_9>a`k&(Z;^P!}2t-=U>0X5(0g12-%ea2`8~8*#3}UNWvcN(Avetm*<9)A7 z?(zk-npGIaJ<$U?|8?cnu8LK8jkHa`%g#IpHgO{yx&2TW4Qj{0dn(P*YL~T-OFO z>|1dXPON~W#)q_-IoI6!hu_0wOx<1f?d4jT$TFK5ul$`^}3X^ zT6%fiiV)Wg`d~&)$;$e;r)Np>>*CecBC$9eb)(npS>ou8aqG~NK7NFn>HvX`#8m~p zqJ-M$^kaI}qPMFaMHD1JwSwOT`_hSi-J>R6R7JMm911VJE3B>$R#ZJta5s3ICTesk zDW-dxu|wrFNOF)TIVST{y%Q(vQ=yW0%h8^ozPxhCe zP_2d8cwT!Uiwza|2(38`Y9BUyg)jJ41VMwXzwB@HJXi274zH=rl@%L-^ zy&HZTrGHs)_WzM>z&e6z>_6i+X^Ik~wRMN9s>zw@1f zbGGeerR;zA)Et<>|K@QQa&WEZrLS{$aQ6Fe9vP;@^S_?c-&67*UeNDV@^7o|_mupe z64k4}ZOOl_g5Oi}drE#E!vAoWe)}c==2`rom=d*SyN9&62VTcP(We07=Po`R#yffVQf0m(16CzLhwBy(`| z%kSW|=3U>vIQ42LRmt+_t2K|X{`OB#T=gJ|5rj50Z(|OmS!64<_nnRlmhCQQ!c3XJ z&Du2|K=9H%R~n7v;c?{eR^?wDK|syZo{AKN#o#wDLt^iK#e_XB1!O7@}gK1=H+BT zn3(4MDd~T*-iW0kO!NcSjR8up>`8k8A30X}r?Y2-IsFBF zJ@eV=YA`YK{wdb%HxdeKQC2(w`r@(1^_Ro@_cH#yjM*FH|JO2(_UmcNtaxC)LB+KE zb44-3ieT}vI&@Mq>9KeE((boLI4%1UTFA^G=LO<+WNN>{X1${%NBFhnQeev^3CEhT zwj2*^xx<3hW&SM^eu7Fl=3O)?$_^oycPdsrzQ#WyJUauen-GlR0DN7KFxmW9Q;7%o`jlzFGAcFem+=C(c#X zDKH@YnJyRE`&=FD1y@y@c`he`fAf!Q8QSu1*6GG%qr)-GVHBJJQ`E7jZP%-V#?^*4LTKTh0>0&8OD?sjyH%>_f$?g- zcP^4Lt$oDt=`dx?>>xi)keIo29rMjm#b7#9w+r23+i$88yor{L)<){gr&BW}(>N#| zUE#7syl)Nd)=r5a^9cM+zJ;fDuRDEu8`ZPlFSlx9u0UMKt))ix+?h*_JzdsCbI>k4 z?8$@mre{CPK)dZnIImo(vasm}!ERD%l*TSocVfY4@h+O#${F?0q1HO_fcyOPh{;T$ zN%I$e88)HCPg_59#~ZcKd6M2G4vtHBPDG#ZP1raUX>rN8^-nYRiBa9-9u4vGiM@k_ z-JYUHPvtc3s#6zyQlKB|^y*~Cr286Di!ZSh^IYoG^i=t(G0!x@w-*^i8LaVFOifL9 zRPKRCD-|Z5hfWgu12Da2-PckFC9`L~7(c9fmnsplo>*~)s4N{+YTlEDC*HIl6&io( zOy81md~k8k)=TfY6E&2)`(ER})dXv1GW=Xzt_mpIQ!qO)Bh%lVXARuqozjaad(L%k z4eq>zW>|y_`|s-Y7>)Vmbq$^2%Bls&m=j!Zf~?0u@iR0L>(A=ArPnl)7BW1hq(@uo zUA!}b@B@2&Am>|WYP^(P{^ZsEZMP;TPVgrR<(`s|*k92nYdgY05%;gBY_hwg6yY!+ z+uh{da^I_Du&WBxyH<5Pm#e=JKK|n2Y|yN0KL!7+_-S7fCO277#JoSVbZWDwT)!86 zV|RIRTPkjXwooZyqAOH;qzV%-twbDcQ>m%0v-YlgT@h!xQUTLnNZW~@F$hYjPb}DG zt~)#Qr6jI#vwO;9{P38j$rTZz*N8#Ym!7PgrkJr48=5DmG<>dExXx6J0CSb?xecx& z?^dZ_o3|~;3$&Xpa!*5bgJUv8>|1SRFW5LY;>$N`JiC)fM$^9}cud4>uz18YQ_FG!W zaUCr1;N_J{mZo!?`(DoMto>vV@k*w7@F~Y(dhtg4G?8*n zLfU>a;nLe7)=`b)ZZ3l#>HvRwU%A@asRO$zZp#+8gC)V|PnT=>H`%`bcI9WX(W9W2 zJ~wSa=I@dh!e(4_Jd`~xP?iP1a`fBNgDiJ1OAoD;J-4TFi5K6#ox1#nQjhce)~~BA z-?&>k;inl+Vboqe{zAp9QF&SEoI_#O_g5pe)vMxTZj*3lO5fHshmFnkw^~W!WfoDm z1?n36lf1@hzMdOquLMh2O>SIQIa^cgDXg|%P+Bn6;<(z(&B85uZf1eZp`G0WX7KDG z+%LB}39O`n@0Xn{H@jgc^#ruf$Se>4tZbPmbE&7=nTFAddnlQgJ{XCwU}Vn?>U^tnqLo$%=C!KpI=(~P^#b$45ON-d&ak9mu>Ek zkBe^JJiLN2;6BvNm?|D=YIUn9gx#V@bV-$6O!02=eQ2gt3MhByndX4Rj+R2zhAv;h z#1QihX7w|}BZX4~PUG3dQ%wfJhD!*SGm6)KE>tunmI~vWzC6QEVU4hYW6g7WGWwj` zl96qaC@F%-OT#2xHo3@j;$mr3#DmRVJrj-wHwaP9^y~Oc*&ytXmb{^!LRy8l+ldJm zEvA)Mm4?M*o|Uy@KIY(F2p;jj4y6b_I@d=Ir;4CR53k{?$|Vz(7FBC_%JpcS`N24B z)bvaHshYuN{%c3C8fF~$5fxlDrZRIlR4&cl>WjC=r)5*k#ZekOpQSwwYX&cikXl^a zltYU>t377?Gn>`K6HRkwkCzmD^y|VABy4jlPQ=$+ot~L?!X95utr4%BeW>yiTdt9| zoPqNk>1RMMAZX0{QE=w76E)o=VO7S{QzZp9-=3=AB0tYe3DuiV9yHrfh&K~4$=Ku$ zK1z)BVDh-`owH*T#nyL~MObS=1Rgg!ss7iQ(TL8B&E7_=U`|aRof&El#U(R0<4>E^ zK;u9Qs|LE@Oz=ej4d!6W#sGM0&QLSuef7sF;Y-&#W_p3hjT z`za@NHz2}K1aHnZ8Q4uJa_Fs&>c7__U11Y=OAM3Y*8k&>_##T{>_mg}SF?;%g>lu) ztre8vG3QgQ#KGtLsLRVN*Je({5Jx|KvhfaJ4Lb=9`(u5;{83kkhArf2q{%w!&XCtG zqHIizl9Zr)nZ_p_B7#n3#L|6Wylv_ ztYDFLIpC_qwVWcy_SI&og+-S#!B%xoByWj0H7?V&Win94y7l>knoA$6@%ZZ*TTD$& z88TpF+bTaUQ>4QT~$cerXoGt z*D0ZoZ9RsM$|q!|=cVd(`uRiwwG2MvHWTaE&vc6Xkb@GxK18a&A$NJl z9S!WKhwB<4;xlj~In6N;xMiK#usUiEuN zUcLK}K?%ue8p(Tdx}=oi)@O3ad9-22kzP~JzAq`NgQLaqlJZlv8Iz;&j^7>#yJ4T5 z!Mh}{WPBYZnPUwH=Q>RW<5Wx?H5A*W>hw!4hA}5!W(WA7cQk+^!}gMtVZ-G9Wy7qY z_s`Xo(~P&AI)6%X^iWz+$c5lmckjs$KlnQ%2lKXEQ9SzMlxgS+hGb!TF@v^NGDZ0$ zUP(&k%5Z(K#&}9G0RP-zI)3tp9~XhI*i2l)OhDs-7;}dHF^s=fFCq6JLLke3!PF#-HW&4$ z--#vVQ8inR#E~+c)Z37Qg)Bc5Pi^<=lI~5o z)LN)&DBJc;Hdp1rm||zeqMRSwm}g}Y#22ZFG5+jbrh0ZOoE)l(ICe$XSWKItYVRAc zJGgjiVnq*bgooJPY~ej!$WUt~R*RLnDHq}=qyYj_yL<_&K@m6VVDyhIr37nLoI0*8<`af>MHw^46I7kWB{9w~QNMCnU< z#@D6g*0MXTplRD_gYs{1ls-KFgza+Q1&?r}gzbF5z_FP}^?8h(|JnKEBsciLj{*-gUs zQAM7kryWNI7$dDSbK_#0lw3~=$h-EDNlm#qCi2Ad5$k$Zm^5_WD#2D1|FmkZ(`RN3 zNe95~h`dYqwHyETncTr~bQQzv`A7nuvZ^*J+HE@DD6!Sd{c!#ES49^}a&3*>XBEm^ z0=0U$2-X={M<(lyb72nC9Z7iB_)zdp>{mRpX%S165DI&(x`x z{BkD+y)8o+FTPd8gH@9?buY(sHy77~tpsPVJ1jq6-^B&*Non?1rzO-3=;c0BXO3~m z44Dh)*}^g(s+nN){+05(xf@p4pp&5Y9^$1`*&dwP_XAqWQ@RDc?G{xNheJ9bnOM3xSn&IdnFa~hsXg<>lAJs4FPP|O zXjV`(RgI@rX=D`LpB;H6-d$;dJ1JehM_J+-^#J~BkyEvhuhHZ{bgqvlJ#gj#3hqho zAVc71fOmzxc7{*UX#3$}wHfauKhpDsd`8ta~Y;n--!n zr;E>j`*QiVPg5vl0kzGz!SUvRwCBtD=zdXlXC_C-RU(iD>X4MJ`1bQw6v2Z)Pv)=E zX`Q%O-U}Ss2f9ZKQ70e5F5(24R&{|`=E$;zXa7gxi~m1C5C| zg^9$*M4!&E*v@#RErgx@lXIFG$pLo;ub5L9&$4&b6pf+qPOWu6eCN?jG1X0_px(M? z5d7P$Mybz9$@V!!*Q>t%KaE454bHmNbQF0v-Cv367iaUJ{s@SKpm$Y*!_6rx_b`(s_DnPvifT>Afo*SGZ-ytzZIz z_QLx;qQX0QrKR~#pH0tt(X_T|+DY@c6ZUbwy=aYF)!K^U$D6$--!kYlskp~3$;{PA zGK6(w98KTAUaO>*H4xX-x)&O=Mx4SCv7YWH7W9xEADTjbxwj$O<=p)BIS`CBsP(>J zeOAo|18V$6jvbo$B9~{AOWjHK18<0gVAI`ej2)VULZ(TU6ferYIvcJ|7c%Cb&*xc& z!Od*5R!S;ejx2Ikegi=_N?7YUJ6sY_Eadl?(%!s9Gt&r zdjaY9BKf@~Ik|sZk_DuH+U*x0rleY#Q4~6@BDi6dtG0$jqE~>!~DGT z70sU`+2;NsnCzV|W!R~w0$oVB2fpr!VTF!RN`Rtac3|C0b}2y}Y;yd~F7|gbpnOAc z`EDxDc3IBS;bzf|^$%HLfF-)w_Q##~YKW&Ix!hS3< za1#RsBIgetD3kWq+;?kc&j~3S&ZV1n7Fz6s=kG-jQy5q8$8#<^^H=A~4@Ej%{ zLhQ#%f+D$rl{+il07obMlF_jim-$5RO-ZP@7erv#?`9|540{dFzSVjt{raI+J8%Gp<^qvCzX z(*hCp)v6|NwQYxs1A7wSy&EOBE3m(t3crLC@~FekyATE7LeO2uSdk-VDBMhTX~O)~ zXk6)=4Eb60BW_+xRANgjTFHJaeVBYP*#qqNN5 zv!{BW6B-l5JGTb}Al_)@n%{m?unM@* zM)T!lz7uVa^fXUE3-x`wtzGXn`Cx#4$DZB8@HX{pp_aXtEaY8TF5TdJ-jPYuxP&Oj}c9XmxR{r~0*GfU5ne zE3WgFoq>$Yu3ODVnU;xExf;@dQb7YHAn7KA8NZMS9f*1u6?EBlOysnBO?@nXrOZ!X=W6MEmu+LU zPl1g+cT265^{&U4!sPMyfOL`FBuIjULsO5QEReEJNXuyuvn}XOc)gjI)bezx5Yu|8 z>9i#$Myey)Dy)#;DiCuFUk}RUGWe=E8L#P|I^8v`bF$|(V*ZQBQ`KFZ1igflC!oJH!SCzt%uf%1dTXR-X8Hq!3lmK2@^^qRh(cQ4*E?th zs$bTM5&;QHZ$%K{OLaQ|ihM+4=tgTjk3iVAv%T&{1;|Hz-(_-=P`Pf6@v z+92pDp0F?ap%x0P`l>Td<$>nV)cND<@5Z`uzCY z2VqAARuI`sh}-!lJeu^o6H-hgt!l)LRx9+~N{}LUYsj{gOf?U(7GspbCQzR526+rE z5LU7=H1L}KQmP!}uRKXdGI4PrTt+z)g;`C%v}QB97vT9;7r%9^uDJwF1~kHKo}Vb` zA=rY@N@W(PeI^3MR@@ZcQnwF3E-8PD*{1E+(xGz+X1q8XD8Cv=Td@EH!FmT;Glb`w zPnm~Ys!B;sHy(`FNnK0#6QHb}KW7yY)a%*d#BaalABK2e|vbOS|e zrfR&e?9GR+TygG1@^k3Yks#$gQ6?JH%uM^RN}t*?B<{A)Ds$7JcIDu!$&QDl)ybLD zZDI7?8QwG2R^R7V?SA@#0}pD1J#NLYBqdE9KkQi1ZiuWc$)z3wjkC z*(?@i2;i_}RySuxPPl7wDdbfrZMfdVbdE%;lr_Kc5z?m=fb6c29;kDnSSlyd8A*`+e^L1yHWbnpfotZ@d2at24Xf}M34 zKaGzMw0gh(jKfPA5UvFSW$v?)5Su*MH1FGeMxIS)UN=yi;h*ToNp{N^AgdYvbyj&) zy-9o!t<4{oObwAPQ5GyxR8`|5%s5 zQ8GO&8|?9ti$4x&vn42JP@vnbI@hna5DcDg!Ii6~phShf_&`;jivSun=NbsTY1Zy; z^_PPAGJo`-_Rb>E|H4aFYUw<(K=mc3F*Hlk4ZEyW^f=zg6m^EFstT(ah2A^rqd}E? zbOVk0c@xh|cZDa)4S3qiDpYW-p(m&gLgT38NJ-6a091t|ihsV9s;xfu7VK+iF(Y28 zrf4TrAat1Jd9JmVN6GSFUvup)LdW;C#RT&=T z%P9hDn6NGFJ?O?D{bSF(F_)l2^Stfl6$ytMI~?Sy=33#k4F zzYEx6wujO%0LT?=HQG!}leQc{#DZT+Q<;Oms>*{#A9ldQ(fBNhtEcCUZupYT?q}~v z^*(omhFA&E;6}EwZ(M}hTYBy~`u;Kz?JU(nP+8W@fL2|8)!?6m2Y~`x=wu9ZM7krl zZE`-3D^d*8^z%a8E!H$yqqXCE@XroeFydylLQx=;D4&-3SrHE+YMOJ&ONK(`#H*!!wlVeZX(^t!0Z|3={S%qhknsr5btt(7NH#vQ} zpVQ=6cC*1>UzpT#{DczALqP8uK!nZGLFqS`Q^|)zw_MN)Ze`VpT@lsyX22NHK>YAD z&G0Yp15FroS2zyOV1CE5dL=BZ>q|W%n72R+Xf50{VvqVH3!hhTgehJw;jvd- zb>6uaADZmVNCeHSy%gw*iHCN)_@L9~Ct&M{U~~xC|%GJL(NMoYzV0XR{frCTMV?j8JyTAbMaYnFOV>bJ8 z4~S0=Y#jadR{!={wG7s}#4ZL-?AG>0IaVGx$*)ksn(h;|kwLH0<$?$Q}5^V5%ejpYZl#YcCta2<>1EnSeZ0lHf zOz!Vs?S?N_&(GgKK1)F2EIYh|SBX_eL7^pTrg6D`mut1Xq)FGSzr%@xe z-Kkveuw|}(cMK|+is!}jc0zS)B zoOSs<00opi)cM8s`1|K9gbsGpUBZr3{}&LsI4QpzYj!|542y>Cb1_Zw-#(LqU=KzZd%Ng?{z_$%X!VaU&1;w-x`rxWSYA?V|h#7bR$= zJA@DM;IlqsT!984C&bgTbTu@q)4%1!Ibk<{hgebzL@K@T86oDOqQucuGuvTq;1f}; zIrz^w93=E-*Qp0%AX}RCX>%OYJ`=r)n8eq!qT@_|%Q14D-MP|TR3ZmuJ$(k`60y)E=n8&oBD4u+gj8Hf zNIvMN-QVO?fkoMHkCF=S*%8rKFN@aLqWqFG)IZ`$vnsDRcy`~bJ$1p4@0_X2pIe8a zos2}38wj^*LVHs?m&{kTJd9=X+ZEY7R9=xHcr!zXp*cHM;m~gvc}#E2gD)C)Ec*q zpM<-A>{u%^AqL*%TUZwBoX#*NdtRu$Z-=H(Ir7>h= zZV?J{26nN^pLBH+4qDqOlUBdGB;5;{{`9IY59#XyQO}?qm|M9d;E7=soC*yzqP)H< z{rNFUcUPS(di;Z5!@+#!PR{BcC?y%XSoENHi)*K64YY(c@@% ze72KYigL8(NzRX$KCv^(Ghi-!IX)!!NLt>pDX+B(qGa8Iic09AfN86;I^2CYb!;^~ zTfOQsH^W~w0~mj%&N`FuM)d@~aDP(%@u!xI%Ult6vt(Y|wF3LQMXFK*57A2us@OO# zat7r$Ir5m9Z;y_^Zm31LP5ksAU2suBRf?=yW9z_i@rJb|T&3C(?_ZM%fz3May9}1A z?d*q!c(|T9#N6Jz!`)>LTqK1E2@QKb0?aKz_m77;*&vUwiM_ z)>PYdjVfZ{5=F2eqM#t6(u7E_>ID|Cfs{y-k|14r2N4w&6bmF2At(aUg&<8r5vd79 ziWq4MgkGeVQ1)CPzW1}Y@BRFMt)Jjnk+pKJv&=chm}3&FPs{%^`H%R1nKclp4=Pg( z&N9Eaw2}q;j_g2)EbO|4A1ppceJ{V1N}r^eTD2m$BC<;&;F!62#;iTUZKwqZW}Lh3 zIab;L6okxod!z@f>my9{>rZ%Bq%Upsp)cTQ8wK5Jg6kE=2$#LbU&K1#1eWM;#9Ui| z=TubPTdKRuY{534N%7 z8Q$j@>Mblj>{5Q{)55A#YUa$SzG3n9VFeB1mEjWZrCa?dfhPxu9XwkCVxO7FR)OF+ z7&?7N)KAQcD(jg{e?zD>1XrrJe7q1ZBNtF8fB>!YWWy!(c9N#A!2&4jMUS z!G~P6d-vtzr7fxZ!o z`8k~)xU#Q=_?xsl6 z+UU-D={{P^_{fcgudfXr*3yAp_^_ZpX&GJUM7z(s@Ncx_$M2U`YeRwEVSwez&iq<+ z6>W~>sjFa5jPVi*@imy_D>y;H>Tmm!&0+RZ)C(S4q`D^2MphjRYfnCmPI$&O0myr~A}hm$SeT-+~PAhlF&>)jx7G|8nVS@>faA5Mp-W&gAZudl&fE2M*%v|zxC zvY~8u1<`=foUSK>&Cb!6$~?NjN8QGJ&*d*8VC^ncoA@yO!Bdxa9`bDf#mDXtl~rNP z5#>D#KEzrFaW?}wEFR-RD?=pDh+wly)v{GoDTx6zu8As$dR`c788|(pdQ$^)tD2Ad z6i{qfx0}!A9)i!3y3}6!B7{GEGaCYCS(QDt#MO_FYO)h_S=q{xM{|7I zOW6(e6#fPJXp6eZ5l1bBiLlykZv*K5GqH$Ue0kDb-Mq&%%l0Z9^U54dzxfNtCor`g z$}U`9#iz=*IvPjgU)741?~jdLq1$$mpO@le?K)nCGU0`u?Xfo5bM~%j6Xp%nedmuU z8?~DBd5wA)J}M{|dJh<`A=f| zjmyrvmW>C$;JJ&g%mBYmY@5Ot76wd^o?yG(#dAaSDm) ztQvrcU*7$IY7TCV28uzmf`J^aqPhYK^v(`1Pho}n>>-S2Dsb`hi{G?s>c|EUiiB`` z43cdh*38auwT#Ui#I;xBNP$^bxm9(0Hkwici8|LEJ}a{e3C(bn2iy`=IMaD{gQ!R9 z_$b*TDMf2$^Cp;un1g$3O*lU5EJ4k8*pIxhz?Lqo3V+=>7l1RbZcCVQ>)l&u-?oKz)VvZR}r13F{f`U zr6|#uGhD?_=WJ<;IMxI^@U{o^V3iX((5-c<3EC8w4^d~YDwEijpYW;$9u&Nj?asGU zaLwCUvDgO(NBqLte0$<*~aM$DFhhcjP z7l&=hUAxH1$~QF8Dq?lyt)Ur9*EXMLT{i+Y{!}upT;*e4@wZQ*qd>OO&_~AjCW4?X zVBU|LbDykTpR@Os1_tGr83=yHh4?NQ$8{m?WTKkbP|Un?)dGlSLIz@mDv|QV6yqLK zEgr`Qn9+=J5YMEY9OQ3F^-SK*RtU^$Y>pxU#?i+&l*V~TTkF2enIxGyPAiV;+5Dt@ zZHon0qYzE4$~@6GHpekb@~g4ZYBP&ItKnt z?sm(W2k0X)5<1clu#pY(%J6T~12Gtm1el5k12KCyM=b6!TnA(xjV7$(lT^5HKrlHV zat96FiONDj=q>qZ|}GtTSOJj^P{4U(Ghw`PP7Z%W33m|0>nhC*?b&TEpWa zhduqT6h373xdTW`sw5n^Me_PyI|9!YY`8pE>to=}bs55DPOCDVF9|u#8jSyaG+dGulD#+&(X37$UL)7|ZdBveNn7v{AR&9{Q6UY z{q`lNqOQRvL=BwwKX8^jUN6brlO!o|nj_e?SA1@t{-W`?|cOJ&M?!$Va3 zijo)4$^A2_a+)f=mH@Ru#q40}n4|b=WLC!zxwp@nSS_)w&E;5uOmNq=_Q4fm?6cYoosW_u z7h!JbN;asWFra6NQH0JTDsDYLfH;pbHt%$g)r1hyW2$&Fw;+Lsp$XZM7I8Czg*v_G z1zkLm+|v*ZIk7q=M?@XcIP;T}_kIyTActfi)$HXbt&&grhw2{swnNLsmpC2cho=^k zMq{)O6g<8bt?d<*mH(t1Um8NbqoSitI@x-|NPoY_JBhJ-v)0*dS_3L1s=3>urIQy1 z1R!6>Hcq(|Ohlf-AAd}nG#2#NnC|dd_9jnjb~L;*I7$yNTzKtCDBQS5=b+a_M92P1 z>_e)cR)CO5#{z|G-G3oL?ote~WZyRMoIq1<)LkFZDa`xq8`nPRb$IX_MTehqh z@VR*<*Jok&5RUT+n{}tRqg|AHES#cK^--)RRYE1ds@I1sLXxdr53!dPjI<0n5?69GSbY?ODz88^clrWGYd}a6?bhl}95wDJ{DNJika>scoVIxvJ9j*df7;yizCkAWf~W5XI3pSwg!gWD9JMkh26bH5y0=^Fs4e@Che~N%Vxn1o zZ)}Kq@{4Q#ik5Xp-%Z%qDtQe5osyq4KJpPsns4Gw<}#r53#ruxIyGN#popGz^8iB! z0E2=dz04UGg(-jJVtBB^Om1z+Or?jq*X1ntW@#NA_HwS|i?oH4doHh_-vZGOq&J4A;Tg z5x$L0RhG`;%f&kTI~r@M5pUKzHo_tqT>nIA6y&rkpcmcA3_`$*=|i?8j3aMf&=sKz zV7oBzUC{gJ4)S#^{4B*1yT@p1DWt{DurR04&>&$%gE5@72cu|H(UNAci1fJKHM56O z<*zXLDibt^*ob?syZUw;?{!?Yrvf4JRak&e_6bL>(z%gBumBC(DVYP$-WFtkTfcdK zo$a14R~SeGPv1RLvPH1K4bhIGkpct;#u67OUml>JBE7HYyfy8@0aoEH74w?9aMx{Yql=btmVSG^ z13E&58xhpRo#pn*Y|OVuSG$T}W;R%!d}lrR@*8WPiZ3)*RV0BtDOQzJ@ZWuX^DTh> z^%~KFwtRsGZ@YXA3?9;pN(D(rA%=s6617WJ7mERD@kJyj&q8}!VWQdB0~Tkkt>8?1 zF>pd8ReYpDGWdkZ6Ws$Zk)A8&hzeVd_EwR-@A3SOzF*svQgj!fvIithWmYu^GTMk+ zMW{II%$|M8ps*3`hm|TK9w746WRE5$7s85qL?2?`1d$@zMM5R_RL2XXV=%77-E*bpb;Jr-~bm&)0X1P%DNtj{rgBPLED zC&fe@!zBldB}i^fb^p>08bJaM#JuALC2sbRdc^4*KOvAVXWqJb02IbGiRv*kj)J4# znS8l+y?xoV2hUCX*hx&MB9jbcz{1P74`S_Dvi`NFEO<{;ORDPI!uTk1RbZFOiYM-r zYHf0NTTC;!a}w7ac*6Fm3GT7oLN#fujFVWM32I=LdCBZn*Sk?5^w{#veC@2%M@anF z#g4~T&&odJti0EMgZR$aJdO8>)N4v%1y8`6Egi>Iog#|wq(B?%uay4MZkoDjcHPWS#k0WktzoE*bo&icV9Wl}T`+?}IJU+^l}tvyr33G4oUj28&QJ$o&Y>ccsBzNuFw$E<;DZ3K=^Pdh|1V7la z?{%jrQ~1u4HpMrYZF1J_YdpBcf{}^g_>L_Hx9r{Xkh8)^Zzq^zb>i!~;M@gYpipXyEg;P>RaGf8 z;1%-4G*b!=fVl>zM2F{ueO$Z#h&_IOF}Hy^Yg~$bSCCZagIDyTlxxxW#nPw~*29OzV>};8Azix|X4;sG>~{P1e%_l%c|x&7iFN_=9zgZNc0YK>?0FS`J~3Vm2VriK-f}uU=yQplA_|@D+QQCKI2Jeu19U zV=2=*Z6Li1=y9=kO7@g68-fe8{U2|>C?HA!gk;O`^bkLAumMhG4zhDlsQmh7&Ur_c5hrcv>qM zPPm2g@g<=xsM$hoT3RR^5SG&ToVUR8iF!xS+xGl z$_Sr8F*NM$BlRhZ9LL)v;pFwKPT1Q?sakKZS=yUS2nfxM{-AgM=_H)~`BI=v9t`*p z!{`oW3xfd<AZ%y5t+Vv?X@h_BxrUrb)a>=-YinzZ@;4JZsY0z&!VNv2Nicz7+5sJfxj%D`-uc9?~{bu zV#-I|R8z(La)s%1np$3?e!r54;)OYQ8a)?Y)JBxat(W!wN+P|%4oMeqm!Dudp!UF) z_qF0$__zd0wRuGq_f=saR^`UN4J1KA7zZa|zA^q0&D*u}(R;*fu7dGNlRY7fHdZR)oh}6+X#jgM)=V1_i@*zF^A97aO&BPMQ@gW`xV60 ztQ*$vcMM;)K>Y;P#aN&5;AKvhBa*?s7JCsDU2 zKrJ4=UaKIDP3_5bcnLuv#9bPS+iTsw5cddfkRPy1beC+sb;b^27)_Rd(&;+ghaB!4 zL4m5oZj>}(D#X%z|3qfIdlfcea)FC z#Z;00xrv+kJh(N9@@&!Kc(Wvd$FBSy>6reSyL!1-B4YPKV6n*BA%Fe7UMq#mhABRS zT~GLgAc{5K zNTucIBeYoVywBr>E8Rf)_Z+p2ICsFe{ok+RzvQbsLM=c!dyR!qv#Ol!XL~E0F_qg) zZSDhVA?1UV)R{Ez;z&*QisTT(g-&+MwoAA#l)z8s0zMOxJ{ql{$;M=(N=6^>J_*gz zs54|>j63%oum&OF-0tUj<)|c;%rJ1(|82d2 z5*c-dTKUkF`-9-wyev#EY5dD{PSRX~st74l>c`!k14lBNf1Kf-ubWSog+;4l?n|kW z?SJ#)5j5g`FXuSzB-Q2T6|WY#yIw4koH zmq7++c+kTq%5%JOsy+g4?vxItGt-4#g~LD0<0G|3jKckyst~|ocgG9oSW!lI0iglc zwQYt9RC@K&UfI7@a2qH=o-zauQYWYp~=8d3sAGMIkHZHH}?SV>} z>;BJRnLDI6Y3ZhX9n&6EVjwj=chdWHWEKs@qYI3v4_p|fZz_lGk#_!yB_+o{_zul3ApBEzK#SkYc%z~Tg)zZ&@)1AKY? z;gS6-)gA7jI_pjEk z)Jz>b!3+PJgX3KMFF(*gzyvu1Y9m%wDddR`P`MvLHgc(78I>Di@af7N%U@dhK0J)9 zVb?3kwIMSHU!032NBdU`I73-sT?R70Y4|lBWq@ToF75qoe3J`mmTKBNS@YEaf`$X_ zrmgRPt?EftIHl9Z>Nr;}g7p3*ypA}d;K^qz*((Kce^uF3Z~kkrO+Xu7);WP)`?MZR zfgby>ktbJoR!|#4q1^7Tuq1Jj+dsTmmDj(3PhTw4Un?>y zgfF?ckA?JmzxIP6+oy7#d*$9mfiU`5(}R&K)hs&>p1{FXK?U12r9K607i@+7+s?0U z!Ij9;=}OgHeH+n+@TI&T`?s$xjJQ)UWZPoi{PNILfm@@*o?cp+-dlF;g*Pv`hakD~ zr(n!`z*j*MUo!G$^+`*4S2&A|9~_lAj}_W$GE z+eywB37%a6c+lnUetKs6F^P89u9f(j5?91t@bg|Lw6%A0vj=v>BpqO)+Mb6pNe_`D z>tpKGwEzOK+KO|ZbzH5uQ-esycDPKyx86*VPVI!Mdm*l+pZ@se5b-vVHvv{95d?&te0pn&r1EQx#i1bq8@?^GIhyw}KXjNwm zCu9pXtU!ms8#!w0JrRMxIU*o+Bm|Fwj=3d3w)MyM(gUNeLMwc!zI)3n&eAlYyOw@@}iEt#dUlHd-Ry^9_VAL8FfGvb&kc^H6*3#eHaTtqJNR z6cjcWihkx@`NAgElZEZzf>=rL!@yMK;*-~C4pR9SKsg~f1S3k+tx`1 zAkWzmk*M_kLi&e7-Tj_Fi&eK@96$slrQic_?QkQf78F{OI-~uz@0+f#>rDkij!ua9 z^>);4(}J>Hp@6fB^Pi_0nCO?}#y;h-AfR>tRMIDTlhRz^IS+6w{;vMihUe@^|BPCF zPKTA}tOtHLcNXey4GQP#&aepCpK=JlfVDCGQl`RbeXS-VG_GLlWSn{CWJpmJ69rrZ zK*|^VLoXIj{SM+9{i$2U!$`W(nM|xXjyRuetYz^yVG)gwy$ELZODVI6qJHSa)+kQq z$#LV;MO%e)qEqooBE<%T0Hw6rm>Ao&fY%#IucU?DS8bcH5UFH4mxOOkfrQS#O497h zr-+V)9v=a z*xIY0En<6}6gB!mkRG;Vnoup66jo##VV!l13$&~6e0J*jEA;pT0`-8FIdXog zppCU}%KG2Q-@fZUkS;8jpJQ1KwkWVFzIY!^1JP6GX2sc4>A*OprMuL9<9;9EvNYqf z>|zHQWyoWu$vn|!z{b0_QdFZ|01VlUpdw7T8zTvqy#`*>d%b}F5}qMj4)l0HI_Yeo zAU?jxc$YByiT}~^%<_`qV$@wbwEBd>OrG{)>rt1CNwbe-iB?eOP>PUE@6KaoIM&NZ}hInu)t4L72U37S3&!IZ6KD+2NK){1+}l{+hJ^>Axfi?g>x5kPojm(QcNrDG~%qK zt7*()vLeJubuFGca^RDYEW?5Qd3yH5K?mJ|e3Hry+(wl--H2Ev+w_U*rR3!)!(~kt zC)_u+w-U)G@tKst%WhQ~k3O{0gi8{iOzPo`YKHO?P2DJFa$OuIUYoIog-FGRjH$H4 z3Dg$44y>U4yS;v|AV_SRN5?+PuC5>rBoC!Abe&Zge^xI~_TsyJVQ$2wP*)FFywVvU zSMAL+^!8U9oB{$4lMWly9*~pLpR*r|C`YWN0{DS3q1h3+uVB+ayx1XN121VjmvW?{ zU9pJ`i?$(#|G;gH^51WN4m~Ixk9UJ40FLab^QT@z%(#9cr?fYZ#7{0vn2 zka(imatu^VWLIpghkA(I)(?HiZkfxkUbBdR`O+_VW;UyO3g`d)*g4s7S;QuPtZbe( z7mWiJf9_P!_h&hA2flr{vUoTb+!mJu;#Z?q{gInCc2r~ z*4}p-ZsBD2k%@m7*u_*oH(9~(UBQ0C(4g^vR=Qj3V~KBXr51jc1gHw4}KGlJcigY=uUb;X{J$a4vnR<^NhW^oVw$><4hUsV&v_ zhVY~z%v6j0t9e8?abUJdM9|(0v<%0WzOyXdC{AKh-g)QA^(WFjq2mnBMDIHvYQ07r zN@0mr_cU0x2AMGw7f0WE{K#_dUxy+42Z*49dFN&pynOW1$mmik^R~ohuI1Ts7X1R7 zmk#988CT8qx28HYntIf;=WKIx^V_bTJ)pTMQo*5q<`0zv;A`->U$0@|&q9|?X5$zFiSbESu;5e}pQ47Am@C-@>&>Vh;;A?_ z=D!u(UGBF20uvyj2VXl*%?t9YVaW|E->brJpR`qkZr;mmE7ov`D5G-BCBF+C!t*_Rz>+y=m(>2wI22^rz%U!7c{C5niqU!J~**}$SlO(S$#ndz&71vC=}}Z z{M`VkD{Z}=xhKqMrIEV%M2?xMY6WQzWJxQ4H>IIpuhXj7MG^BsSY^Y9L zdYkQMBDuE@lhsCt3aO2#eMppoQ;ptkX~I_{w7kg2m{R4(H5qL`nlLc-u7$GY!htqwSKQT zmmS(gLrMF#BUG~ev>tKMU#|O5DU=;8Ckjkiqv|X--N_{sjtO!{*7gYwhTX^r?{8-A z$3+~%qnkDBLLEBb2pOE7;0WXB3X6Sd((8XBAkt+jj`C8iFzI<0E$7&rTD5vL5|+%l z5xUx7+qQk5X-_QdZ#YV9CYQ-B;FZ$OSY2E>#^L4u&omOyM_|42TVFHvZ#t6}+IzYO zq%nmz;!2YYrV_=cGdP*=C62kbc5!8wDR&RNA8-aT^~X0P_)D~q<~@uW*&~n}O-DF! zZ@26==Onyg5@Df3HdiWQYpdo1n8tYossMFE!f{*ohKTzkyPGBIRHNsnvkOTwR&HYp z(zR|ib#jvEdH&f$BX=0XG|1*vmSH zcb%)e_k)pGO;z^eC(x938oUXViE#M2muqL}9$!jbm0C*T;kzy1@ss4w+a_VwN82{H zy<#bzX_J4*Ld7AE2rWLv`#G@LYRK!|il{s86U5X41)BnML(;$!58~MtCBou{ zCz#Tk9;T?j2$&K-WmJ=PTy^*%{yU0%LYt4+HqIMF*;QkyS*J~kYRM!N^yHLWz?s%q zjU}2Y)-uM3>RBBzt-*|vW`t+tlH0StT7PV{sQf(JBm3rmrNUEu+Xo?JR2F%Jy~UPz zSTErM4$aBjwCT*N#_Ht!p);9!Xef*Gh#>3sFc{r=PZ{5Dq8qdzK@GPvV+bE0JkQwD$F$OAI8>O|ggT_GLBFZbakP33Lc? z+T{FreU|%Vl|3FkZLd>lRXlV?C#11DNIIOjIjMP*Elj7DPSzmTPv09bx0;z1C(Z;8 zHDv2m=F&b8znM6v&-cD`sJXLv8daj8sNSyuA;5nqRZkp%=(Yd1QWbUR#A9CroTLYw z^@YjHK<()oI#w$;g2KtT*aL=@QPbA7g!2)UAKRWH8L{BDP9iGaP7v$7?U)ZVlZ&Yk zOgG{~b%?17^0E8x)JnCk9Je@o!0FoJbT0@HX;yjj4?Eh;Ir~b5Pw8I}9^d}q_F~H! zykzcoef=#p8|!=2gR9*wJ$!BrB`S%al)}kRY~cGTPqV3U?-78B9kv@}Y2}SZSnS8c z*A8fHnrZc@wlqLFrj;9wPVM(K-&vuakJqz5-!?qNfya2X;IPs7gU1vb_eyAdaBF3B zZ*tMcZSjV8*W#HwwYadQYal62xC1{L09J1|ADb|5au3stst;cP*!k)|=R^ z+Xc`DdGmn>JmtNro`=ryD<)T{hiR@uH&yq#DsV@{x*1gWIw%&m9Eq?}_dZK2-+~xT zjv;#7REY~W!bVH)x@%T?$4O%eK+j)}c_p%z#q=Y5Nx4)%-|A_?U{x0)~{UODuKMwc7E88#z10}MLQGozUNLYkDQszz>};V zpTVe<0Yk2>NQmtUrypz*$;^2{b}-&_p#LHm`?57r$vJt*)bhlue4Y^>;$02 z!il~WS`JR`8;QUfXo%~{sIKsGak_{MB3^N^pLh5}a6l?jVK}(HN-ep^-pub+-HWKn zH?PP~n>OEUMGM!dHVv_Z_RFkkv}&lZm{uB&qN-OmJ1in9M)aGax8a3Z&H$tmH(zFwSsd^n2DUnn;0MwkAmyLMBX{jmZX zcC<01?a+yP40D1t9V!>5*B`)`cNxV~nI{o&$o_Jk(NuV?IKDzp*s+Q}K4)Jo-W;4w z4H7Q)!DS?4k7>Ws8F<3h$9IzxQAgFTb+3mg-9uO_L(dVrP79NOIjNDBn?HGmYy2q>YJ`3b zGSL69LRIqz^h{>W3bjaWi<(LF3_u~Sk7GaH-&Xcr!5zZFzB|-)F!})oldn4wt@67w zDPyf5WCmTo9z?jKZ#yEWdzf4j#iRm6=qwnw3L`#pTqH2ZH2ddG$%RH!n+S;ZBDZ4U z;bl+bpLHo*W)B-f@^8UJ_KU1krczwV8<%6NEFyJrx)*bj>M; zh}zB`t04sMsu=1sqQOiwA4B{Myv-Bj83r#L=wR)0VosG_jrngEF(cgDjUADz&57Xb zhL}@jn}0@m9TLa6AKhg3aNGUUh@L(`Z`Xm?K(;=bRM9)cbBYMtiT+23jK+4n5e&jTO*{@31bpY@ zrW!Sp&fu)OUzt|*-eISz5;P>$%s8Fe@f2U~@q*0K{a4yCq~Gux%#L@~Vx z7+{)BLwTm{Db>>~@T?({u!eWj&FIGCN$>ti2ZWgsE&S`Mh6oBPzU z1vJ!QViQ|C%^Mr8*%1j^tC*T|3jj3Dbb{4g68&lIXeo}l0D#X&LnY{tp{=2d>+u4r zLW=WHgi5SnF;cg0H}?syo$vWNlG$*n6sj)dGC9MBXQ{Q`S>8hQs_nXLd8sFSWf@3J z(Uz+YvOs4dMr6mhmD*MvyBZE((GMPSAZ#v)5bO_${fHCrBc?%|(Jb_J^Ua?*Nnpjh zsTAA%Y^Gas)!f1*-M%DG<^yY5J%l|gXOXLS#!lrOoM zRwVW1K?x)k@IAQ1!5n@z((ZZzwQ!4CLJ^{!>IAj4hzG&zhnR_*b(IC9JCr$Nq&MVI z2@5&AMjp7n{etKV(jN7($)?dR;LOMlZ`=h+;ti?X?{d`~7e6kySBgbfkK%?L@Z66D zwb>YsFsq$E7lx5fs@E%XXvCbJX!pcxE{i6uO}=6g5<`(LgIY2E z|9JBjIl^3x`|Zt3xvS*Fnp(?`+S_h=8pTntb$daWhV4E*H>HQa)DM zKWb`MML!Xw@8@&%fDBbfk$|a%FIJeCG&Q_Uvh~UW;Ih_>u22#pXfR)o7nZR!xfNC$ z68g#oM2EY57Mp$aXAZ4L-^nSSOu$vM58~xUCv;#jTLyUAX!3QO4_jZLXAY}9Dh2#j z$#vPxo8qCS*(DvIdFB+^1pbZgFP=9GublXBG`WMAph4Ez{nF~ubO||{uHui%fz~j< zGBr1D{|h~(8s;1<2lG8G-a1u}+;#fZ`^fc{D-y&}@0kzE!gsV{r5)1Tg|pENDY?CM zM*at~x-%^NP&EKHs_u3B#Co=|&HZZ$m)3o=shvA>P*Tnp@pm1WP9^OSZ_~BX`e6~( zf@IA+hwL@kZkQg+!#<7peh_83i-9Oy2a;)r>W(uC_!eB#mdT|#!gB6X{<+)YL*=`x zy|I@+TZlA-Fi#dhNd~(iY&C;%xUG94_RPRRcXs}jszY4&cdNW&v`-il!elfrayauV zUat(=mH3?MCOz`*`Di=>D0}zAmfcy$@zm)aQ!8uV#dZ2G)*_^1pe(vLw{AdTH9|Uq z?0uacP@u6VtSwWgvfcoy#q<{Ku~`SehGD9!6&WUhI&$(|gi>&7VZw`*@DfGUk!pD! za1(vL_yloVZWP&8BbID9I~>_FlqUCHCmMf?_^m{U{f2{)Q-VE6+Jr-=oWT_WPoEZ= zifZz-Udgh)Ro6S^-qH_YOaL2N%PQJfVsBzglUz1f>c7fCd>-Nu3x!T34tds_IR34< zAnPlL%aZ(Um?+6g-k<7PJ5Z`ZSgdtRJ#)I2F$>9aGq$oKk@>z}(?ruSQaZsyAZwyQ zN2;hY`;ooi%7MIA4JRaBjf#UMH+B<2ZT*MfRRf0KVenzWLKDvkge|?Vt&9 z418g*svrs55&A{RHX-a{n9~$e(+9MTz<{WG@m7N?-JP~Xz%r6{_r+EhE|3e8XEaoA z11jc~62&?Wx=+mKtu2Vf%`?9G#Bnp9X|qzKSQE({C%hn?%Kn~P%0Rjs$t16lk*QbX zmGImvQY;|$I6}v;HpB`~YB_0lK{5+LVW#w6n-M?_N*$nrT|AE%-Ybz~Xn>1Jny&bU z0ML`Vhfc6CptnoAw&52*>c4KBnW!9IMkol z7!?Z|E&c#dilxYv**meHZ2Ymo*TDa4_!_kSKUGM+=E(4gWQFBErLL>Ka;$d(6v*## zAcD16(tAot&M6k#`%emFB&5c0kDcOl`^nE|*%2d|iG=y38wVZJJlc|6AtKC(_7?8( zM%)(=rmgvqk<)D$JH4Wsz_7&q_7#W7!YR`LI)BKSA4p2d4n|EzO}y-7ZrgB-{V|c! z2UZfpV#$J|LI`Nt7m)d0k?b!w;_ns-&iTHX~N~ zJCth*7XVV*6#wFi2;Co1qfI&FzNix+h=)~&6gKztcEQoYAL*$6jCKUkyPw@!{k--i zlAA=4v`ASn)K91{53v(G%Y-$>{OExErzKf|gj{(#jMNZ0a!nDxLx^74vp?}Uw|%$2 zXi3$Ln_**AlOXv=cW-G{E~U<0vRqR9#LG4IK)fs?kx<(esIQ6p=bt2*7dc=qep-LE zH!aw6^+S{Bi!-zRs|K2($9;QBa|)b2Pvo0VqYzd639?o@Op{V1o>h4}wGw6q5T(n$ z+i9cfREwsvvPmh_sqrOy`uZN8z80{8-pKyU;i@>X+I|GRZy2cElo^MrQxl%=eOiru z+rJxlxOD*FOt4ou_l+_{y6c%Mr56IW9qD>ynKI5(p7T4G1#59jFi{i}@Dpm0l##En z-6stQ*hA!50xIs4$+c;oG=YF2$tJ;)0oqiER5tQ^DSP2;Db|6u7ej5hyZ#?vH@2HK z^@%mI#hSP)H66Osab)|VrexyXpVhUmVC&t4-*B;z?E$90Vv?6&iP+XOwQVG%3moVZ z$A4DyBM+loAV$Zl-q!M%*Njx_Kxv2=85ktVO}H2EZkOqcRyG^wOD3CXO~`Y^1fGc^ zize|!Bu(QYE{5Ko2EUu+MD`LKJ=yT(_sFj&eGZ>2PQAG*Z$r|j3rPATb|r7Mn-BIB zaz?R(WS%BwQi7EiFF(xgPZaF_J`|K#bK__xph4e$A72&)`TBxZR$5gaA;F};^_V2 zW{cgWO)}Y(1&D=A2Bsa%kRlL!^Sq5A@&`|xVVEmcnpP*Xu&Nx>XLRo`2fyU}8=G2; zy;QH@IBtcV$0Iw0ckpwnqb8(Xl)ZY1rYa^_-(G(va>&9@p2fnJwvjB;rn{lkS`ptL z_IzZeQ7F3FidElUz>jF_tJFP)HJy$?S{ZRSPhwS#x1OOv2-JBaV+|gw#F{2jNWgcq zO3fE2PYs&7i63ewTemgBVIqbFK;F5a9p%Jc45iq5SuW0VFOPAv=qqHT*5x=LVHr1& z6l62|^(V;3g|#ylOs8cNFJRx~|JY`Gbmnw~KE1MroL^?4b>z=w|^TL#)|k;Sa&fC-FYI z$=(sYn_+jD{VHdGoib&rOEjI!aor{W2|jw$G!kh?c=W3pUI&Xei76A8EtCi)1v13a z`Qq3h&Fyn~)n*ll1a>2;5@hvA!x7_PnkR9;-4r^6`U%9kwoPhY5n*PC_g!u|#sPa> z^H8z=v{AL$^(utPh2G@VQ35Ud7>OPlG)S)sY8x7ec6LyD9N=3Lr^0(^#=!~4tn zL~m>)W{-2einLaH#OMQXB2RjmE$$41p9Oelcxcs20g9u)}^J<<$|cDy)nE&a`xfolaWP_ z{y7|7YjzzsQB9$G#$lMsulmJ@45=-RG!TGRjeQx3<34XHp?fDPwF)6Bd4A|=j&Hbo z_hAf9s>g$;_i63K-Ow1%C=sWHg~E?4KC>laf`Ml_U;I+o)93v*`5K5{uvwdlj4B{C zetbN3wV%Pk6In%qAX(*=TIZ{|t3r8d$WJGvT&_p7vHXPfz>T#c_T7OLDSH3RdTfX0^#&~Lpm$d_x4_=vj*6uUKVT;WP&cH_~;!~iQV zg?lt?-&F4C{Im~4*!i$9k*;750Ks>Ly+>UlTn5)Jd$;<5y#OoCs@3wO)HFa9v!%$> z*cT$8te;;;J!_jAD>PUopZ7vupV;cC=?tvAGTt|JKy6$rTJpS?^_m1UP6KWp*%jBk zdUK?&nTE8L2DN`p2cn?Wfo~D8P4-Ls7_rm1fF@*n49`aS!Tt00=3TWO?GswB`1}tm z0l@?;yvHlVDu!l`VaW&7?`=g?LW6n+-f%iE1-lQ+(FAd7T5}tF-;}$n<66V%I*`s9 zi1;hVuPB0xtPp*ienep!4>P2>!e5;f_IiO_H!~Q8feNxNJ)5<{tF3c@>Jdl z$?vp<_-GD@)T4`_%gOa_La=An5D@zscDTZ&sk8u%%kuBH^SZRV^%_I<$$(b}RBv$e z99Zk~GMC9cw zA$n-x_wU!*qJ?gQc-1N}hPWyz*En=yn{PWpB)$Y7{eAC$YU-m|7?AEpb|A6egJg2) zH8O*{fSpCBBlUoYBO7;KJPYz@?-2Pc$3c;`*|sJ55a^x!KJ8q6?jj3>7|*`mI|Adk zvK)Mou2<)C)(Rwd?1g@8l8M3YzdrvJo;m$j=OhTR*TW+-Gr!jOrGnq$hzR)i>N%`@ z`bHGYlt9QBzmt!s@{4(zfgcFA|qHg`KiyQzSR~H?W`PFb7 zocQ6+Pm0A?Kiz});Mc~Cwyy3He;@kqNdKK4|JQ~1DrrheT>Wd z^>lx{mkgw9{{d$IaTu*()3eTO{Jo$4zh45}y)GSYWpb-}jh_G01Kosb8Oxa1HE8XB z4DQ+hK7mc(fS={s!TyKWd0Po7U*i;V`W;;T;a>k^5C7ARU@ZPlo&U?gAmjOW>ioAw z^LOg}ojQMao&Q)Ne-Erb9-ONq^!LE}!&> zEG1TH}3pD DgGUUv literal 0 HcmV?d00001 diff --git a/packages/govern-tx/package.json b/packages/govern-tx/package.json index 1f03424b1..d9da90b10 100644 --- a/packages/govern-tx/package.json +++ b/packages/govern-tx/package.json @@ -4,6 +4,11 @@ "description": "Transactions service of Govern", "main": "./src/index.js", "scripts": { + "dev": "ts-node-dev src/index.ts", + "start": "yarn start:containers && yarn start:server", + "start:server": "node --loader ts-node/esm.mjs src/index.ts", + "start:containers": "docker-compose up -d", + "stop:containers": "docker-compose down", "test": "jest" }, "authors": [ From 4a581d4695e97b6910abdce54423046418e0c200 Mon Sep 17 00:00:00 2001 From: nivida Date: Tue, 1 Dec 2020 10:14:08 +0100 Subject: [PATCH 045/107] BootstrapTest template added and jest.config file created --- packages/govern-tx/jest.config.js | 25 ++++++ packages/govern-tx/src/Bootstrap.ts | 4 +- packages/govern-tx/test/src/BootstrapTest.ts | 83 ++++++++++++++++++++ 3 files changed, 111 insertions(+), 1 deletion(-) create mode 100644 packages/govern-tx/jest.config.js create mode 100644 packages/govern-tx/test/src/BootstrapTest.ts diff --git a/packages/govern-tx/jest.config.js b/packages/govern-tx/jest.config.js new file mode 100644 index 000000000..69cf730de --- /dev/null +++ b/packages/govern-tx/jest.config.js @@ -0,0 +1,25 @@ +module.exports = { + rootDir: './', + preset: 'ts-jest', + notifyMode: 'success-change', + collectCoverage: true, + coverageDirectory: './coverage/', + coverageThreshold: { + global: { + functions: 80, + lines: 80, + statements: 80 + } + }, + notify: true, + clearMocks: true, + resetMocks: true, + resetModules: true, + testMatch: ['/**/**Test.ts'], + bail: true, + coveragePathIgnorePatterns: [ + 'node_modules', + 'dist' + ] + } + \ No newline at end of file diff --git a/packages/govern-tx/src/Bootstrap.ts b/packages/govern-tx/src/Bootstrap.ts index 70d814f31..6c16b93dd 100644 --- a/packages/govern-tx/src/Bootstrap.ts +++ b/packages/govern-tx/src/Bootstrap.ts @@ -1,13 +1,15 @@ import fastify, { FastifyInstance } from 'fastify' import { TransactionReceipt } from '@ethersproject/abstract-provider' + import { Request } from '../lib/AbstractAction' import Configuration from './config/Configuration' import Provider from './provider/Provider' import Wallet from './wallet/Wallet' + import Database from './db/Database' import Whitelist, { ListItem } from './db/Whitelist' import Admin from './db/Admin' -import Authenticator, { JWTOptions } from './auth/Authenticator' +import Authenticator from './auth/Authenticator' import ExecuteTransaction from './transactions/execute/ExecuteTransaction' import ChallengeTransaction from './transactions/challenge/ChallengeTransaction' diff --git a/packages/govern-tx/test/src/BootstrapTest.ts b/packages/govern-tx/test/src/BootstrapTest.ts new file mode 100644 index 000000000..ae4d4965b --- /dev/null +++ b/packages/govern-tx/test/src/BootstrapTest.ts @@ -0,0 +1,83 @@ +import { FastifyInstance } from 'fastify'; + +import Configuration from '../../src/config/Configuration'; +import Provider from '../../src/provider/Provider'; +import Wallet from '../../src/wallet/Wallet'; + +import Database from '../../src/db/Database'; +import Whitelist from '../../src/db/Whitelist'; +import Admin from '../../src/db/Admin'; + +import Authenticator from '../../src/auth/Authenticator'; + +import ExecuteTransaction from '../../src/transactions/execute/ExecuteTransaction'; +import ChallengeTransaction from '../../src/transactions/challenge/ChallengeTransaction'; +import ScheduleTransaction from '../../src/transactions/schedule/ScheduleTransaction'; + +import AddItemAction from '../../src/whitelist/AddItemAction'; +import DeleteItemAction from '../../src/whitelist/DeleteItemAction'; +import GetListAction from '../../src/whitelist/GetListAction'; + +import Bootstrap from '../../src/Bootstrap'; + +//Mocks +jest.mock('fastify') + +jest.mock('../../src/config/Configuration') +jest.mock('../../src/provider/Provider') +jest.mock('../../src/wallet/Wallet') + +jest.mock('../../src/db/Database') +jest.mock('../../src/db/Whitelist') +jest.mock('../../src/db/Admin') + +jest.mock('../../src/auth/Authenticator') + +jest.mock('../../src/transactions/execute/ExecuteTransaction') +jest.mock('../../src/transactions/challenge/ChallengeTransaction') +jest.mock('../../src/transactions/schedule/ScheduleTransaction') + +jest.mock('../../src/whitelist/AddItemAction') +jest.mock('../../src/whitelist/DeleteItemAction') +jest.mock('../../src/whitelist/GetListAction') + +/** + * Bootstrap test + */ +describe('BootstrapTest', () => { + let bootstrap, + fastifyMock: FastifyInstance, + configurationMock: Configuration, + providerMock: Provider, + walletMock: Wallet, + databaseMock: Database, + whitelistMock: Whitelist, + adminMock: Admin, + authenticatorMock: Authenticator, + executeTransactionMock: ExecuteTransaction, + challengeTransactionMock: ChallengeTransaction, + scheduleTransactionMock: ScheduleTransaction, + addItemActionMock: AddItemAction, + deleteItemActionMock: DeleteItemAction, + getListActionMock: GetListAction; + + beforeEach(() => { + bootstrap = new Bootstrap({...}) + }) + + it('calls the constructor and initiates the class as expected', async () => { + + }) + + it('calls the constructor and throws a error', async () => { + + }) + + it('calls run and starts the server as expected on the configured port', async () => { + + }) + + it('calls run and does log the error as expected', async () => { + + }) +}) From 073d9ae731e2734ef7a39a1648de1080696ec328 Mon Sep 17 00:00:00 2001 From: nivida Date: Fri, 4 Dec 2020 12:53:12 +0100 Subject: [PATCH 046/107] draft test cases for BootstrapTest implemented and Configuration updated --- packages/govern-tx/src/Bootstrap.ts | 3 +- .../govern-tx/src/config/Configuration.ts | 46 ---- packages/govern-tx/src/index.ts | 4 - packages/govern-tx/test/src/BootstrapTest.ts | 214 +++++++++++++++--- 4 files changed, 183 insertions(+), 84 deletions(-) diff --git a/packages/govern-tx/src/Bootstrap.ts b/packages/govern-tx/src/Bootstrap.ts index 6c16b93dd..b3d555760 100644 --- a/packages/govern-tx/src/Bootstrap.ts +++ b/packages/govern-tx/src/Bootstrap.ts @@ -178,8 +178,7 @@ export default class Bootstrap { this.server = fastify({ logger: { level: this.config.server.logLevel ?? 'debug' - }, - ignoreTrailingSlash: true + } }) } diff --git a/packages/govern-tx/src/config/Configuration.ts b/packages/govern-tx/src/config/Configuration.ts index ecc4709a1..1db2cea39 100644 --- a/packages/govern-tx/src/config/Configuration.ts +++ b/packages/govern-tx/src/config/Configuration.ts @@ -1,5 +1,3 @@ -import { JWTOptions } from '../auth/Authenticator'; - export interface EthereumOptions { url: string blockConfirmations: number @@ -17,12 +15,6 @@ export interface DatabaseOptions { port: number } -export interface AuthOptions { - secret: string, - cookieName: string - jwtOptions?: JWTOptions -} - export interface ServerOptions { host: string, port: number, @@ -32,7 +24,6 @@ export interface ServerOptions { export interface Config { ethereum: EthereumOptions, database: DatabaseOptions, - auth: AuthOptions, server: ServerOptions } @@ -56,15 +47,6 @@ export default class Configuration { */ private _database: DatabaseOptions - /** - * The options to configure the Authenticator used by fastify - * - * @property {AuthOptions} _auth - * - * @private - */ - private _auth: AuthOptions - /** * The options to configure fastify server * @@ -84,7 +66,6 @@ export default class Configuration { ) { this.ethereum = config.ethereum this.database = config.database - this.auth = config.auth this.server = config.server } @@ -140,32 +121,6 @@ export default class Configuration { this._ethereum = value; } - /** - * Getter for the authentication options - * - * @property auth - * - * @returns {AuthOptions} - * - * @public - */ - public get auth(): AuthOptions { - return this._auth; - } - - /** - * Setter for the authentication options - * - * @property auth - * - * @returns {void} - * - * @public - */ - public set auth(value: AuthOptions) { - this._auth = value; - } - /** * Getter for the server options * @@ -205,7 +160,6 @@ export default class Configuration { return { ethereum: this._ethereum, database: this._database, - auth: this._auth, server: this._server } } diff --git a/packages/govern-tx/src/index.ts b/packages/govern-tx/src/index.ts index 9fdfb0cec..2dd7a562e 100644 --- a/packages/govern-tx/src/index.ts +++ b/packages/govern-tx/src/index.ts @@ -19,10 +19,6 @@ new Bootstrap( database: 'govern', port: 4000 }, - auth: { - secret: 'secret', - cookieName: 'govern_cookie' - }, server: { host: '0.0.0.0', port: 4040 diff --git a/packages/govern-tx/test/src/BootstrapTest.ts b/packages/govern-tx/test/src/BootstrapTest.ts index ae4d4965b..773c49783 100644 --- a/packages/govern-tx/test/src/BootstrapTest.ts +++ b/packages/govern-tx/test/src/BootstrapTest.ts @@ -1,4 +1,5 @@ -import { FastifyInstance } from 'fastify'; +//@ts-nocheck +import { fastify } from 'fastify'; import Configuration from '../../src/config/Configuration'; import Provider from '../../src/provider/Provider'; @@ -10,18 +11,21 @@ import Admin from '../../src/db/Admin'; import Authenticator from '../../src/auth/Authenticator'; -import ExecuteTransaction from '../../src/transactions/execute/ExecuteTransaction'; -import ChallengeTransaction from '../../src/transactions/challenge/ChallengeTransaction'; -import ScheduleTransaction from '../../src/transactions/schedule/ScheduleTransaction'; - -import AddItemAction from '../../src/whitelist/AddItemAction'; -import DeleteItemAction from '../../src/whitelist/DeleteItemAction'; -import GetListAction from '../../src/whitelist/GetListAction'; - import Bootstrap from '../../src/Bootstrap'; +import ExecuteTransaction from '../../src/transactions/execute/ExecuteTransaction'; //Mocks -jest.mock('fastify') +const fastifyMock: any = { + post: jest.fn(), + delete: jest.fn(), + get: jest.fn(), + addHook: jest.fn() +} +jest.mock('fastify', () => { + return jest.fn(() => { + return fastifyMock + }); +}) jest.mock('../../src/config/Configuration') jest.mock('../../src/provider/Provider') @@ -45,39 +49,185 @@ jest.mock('../../src/whitelist/GetListAction') * Bootstrap test */ describe('BootstrapTest', () => { - let bootstrap, - fastifyMock: FastifyInstance, - configurationMock: Configuration, - providerMock: Provider, - walletMock: Wallet, - databaseMock: Database, - whitelistMock: Whitelist, - adminMock: Admin, - authenticatorMock: Authenticator, - executeTransactionMock: ExecuteTransaction, - challengeTransactionMock: ChallengeTransaction, - scheduleTransactionMock: ScheduleTransaction, - addItemActionMock: AddItemAction, - deleteItemActionMock: DeleteItemAction, - getListActionMock: GetListAction; + let bootstrap: Bootstrap + + const config = { + ethereum: { + publicKey: '0x00', + contracts: { + GovernQueue: '0x00' + }, + url: 'localhost:8545', + blockConfirmations: 42 + }, + database: { + user: 'govern', + host: 'localhost', + password: 'dev', + database: 'govern', + port: 4000 + }, + server: { + host: '0.0.0.0', + port: 4040 + } + } beforeEach(() => { - bootstrap = new Bootstrap({...}) + fastifyMock.post = jest.fn((path, schemaObj, callback) => { + switch(path) { + case '/execute': + case '/schedule': + case '/challenge': + expect(schemaObj).toEqual({schema: AbstractTransaction.schema}) + break + case '/whitelist': + expect(schemaObj).toEqual({schema: AbstractWhitelistAction.schema}) + break + } + + callback({params: true}) + }) + + fastifyMock.delete = jest.fn((path, schemaObj, callback) => { + expect(path).toEqual('/whitelist') + + expect(schemaObj).toEqual({schema: AbstractWhitelistAction.schema}) + + callback({params: true}) + }) + + fastifyMock.get = jest.fn((path, schemaObj, callback) => { + expect(path).toEquela('/whitelist') + + expect(schemaObj).toEqual({schema: AbstractWhitelistAction.schema}) + + callback({params: true}) + }) + + bootstrap = new Bootstrap( + new Configuration(config) + ) }) - it('calls the constructor and initiates the class as expected', async () => { - + it('calls the constructor and initiates the class as expected', (done) => { + /******************************************** + * Expectations for all invoked constructors + ********************************************/ + expect( + fastify + ).toHaveBeenNthCalledWith( + 1, + { + logger: { + level: 'debug' + } + } + ) + + expect(bootstrap.server).toEqual(fastifyMock) + + expect(bootstrap.database).toBeInstanceOf(Database) + + expect(Database).toHaveBeenNthCalledWith(1, config.database) + + expect(bootstrap.provider).toBeInstanceOf(Provider) + + expect(Provider).toHaveBeenNthCalledWith(1, config.ethereum, Wallet.mock.instances[0]) + + expect(Wallet).toHaveBeenNthCalledWith(1, config.database) + + expect(bootstrap.whitelist).toBeInstanceOf(Whitelist) + + expect(Admin).toHaveBeenNthCalledWith(1, bootstrap.database) + + expect(bootstrap.authenticator).toBeInstanceOf(Authenticator) + + expect(Authenticator).toHaveBeenNthCalledWith(1, bootstrap.whitelist, Admin.mock.instances[0]) + + /*********************************** + * Expectations for added Auth Hook + ***********************************/ + + expect(bootstrap.server.addHook).toHaveBeenNthCalledWith(1, 'preHandler', Authenticator.mock.instances[0].authenticate) + + /************************************ + * Expectations for all added routes + ************************************/ + expect(ExecuteTransaction).toHaveBeenNthCalledWith(1, config.ethereum, providerMock, true) + expect(executeTranscationMock.execute).toHaveBeenCalledTimes(1) + + expect(ScheduleTransaction).toHaveBeenNthCalledWith(1, config.ethereum, providerMock, true) + expect(scheduleTransactionMock.execute).toHaveBeenCalledTimes(1) + + expect(ChallengeTransaction).toHaveBeenNthCalledWith(1, config.ethereum, providerMock, true) + expect(challengeTransactionMock.execute).toHaveBeenCalledTimes(1) + + expect(AddItemAction).toHaveBeenNthCalledWith(1, whitelistMock, true) + expect(addItemActionMock.execute).toHaveBeenCalledTimes(1) + + expect(DeleteItemAction).toHaveBeenNthCalledWith(1, whitelistMock, true) + expect(deleteItemActionMock.execute).toHaveBeenCalledTimes(1) + + expect(GetListAction).toHaveBeenNthCalledWith(1, whitelistMock, true) + expect(getListItemAction.execute).toHaveBeenCalledTimes(1) + + expect(fastifyMock.post).toHaveBeenCalledTimes(4) + expect(fastifyMock.delete).toHaveBeenCalledTimes(1) + expect(fastifyMock.get).toHaveBeenCalledTimes(1) }) - it('calls the constructor and throws a error', async () => { - + it('calls the constructor and uses the configured logging level for fastify', async () => { + config.server.logLevel = 'warn' + + bootstrap = new Bootstrap( + new Configuration(config) + ) + + expect( + fastify + ).toHaveBeenCalledWith( + { + logger: { + level: 'warn' + } + } + ) }) - it('calls run and starts the server as expected on the configured port', async () => { + it('calls run and starts the server as expected on the configured port', (done) => { + bootstrap.run() + console.log = jest.fn(); + + bootstrap.server.listen = jest.fn((port, host, callback) => { + expect(port).toEqual(config.server.port) + expect(host).toEqual(config.server.host) + callback(false, 'myAddress') + + expect(console.log).toHaveBeenNthCalledWith(1, `Server is listening at myAddress`) + + done() + }) }) - it('calls run and does log the error as expected', async () => { + it('calls run and does log the error as expected', (done) => { + bootstrap.run() + const realProcess = process + console.error = jest.fn(); + process = {...realProcess, exit: jest.fn()} + + bootstrap.server.listen = jest.fn((port, host, callback) => { + expect(port).toEqual(config.server.port) + expect(host).toEqual(config.server.host) + + callback(true, null) + + expect(console.log).toHaveBeenNthCalledWith(1, true) + + expect(process.exit).toHaveBeenNthCalledWith(1, 0) + done() + }) }) }) From b1fca8c87d987d78cf5d0e490efb11e8c97d7c49 Mon Sep 17 00:00:00 2001 From: nivida Date: Fri, 4 Dec 2020 13:15:07 +0100 Subject: [PATCH 047/107] mock classes added to BootstrapTest --- packages/govern-tx/test/src/BootstrapTest.ts | 58 +++++++++++++++----- 1 file changed, 44 insertions(+), 14 deletions(-) diff --git a/packages/govern-tx/test/src/BootstrapTest.ts b/packages/govern-tx/test/src/BootstrapTest.ts index 773c49783..7af2fe0e0 100644 --- a/packages/govern-tx/test/src/BootstrapTest.ts +++ b/packages/govern-tx/test/src/BootstrapTest.ts @@ -1,18 +1,28 @@ //@ts-nocheck -import { fastify } from 'fastify'; +import { fastify } from 'fastify' -import Configuration from '../../src/config/Configuration'; -import Provider from '../../src/provider/Provider'; -import Wallet from '../../src/wallet/Wallet'; +import AbstractWhitelistAction from '../../lib/whitelist/AbstractWhitelistAction' +import AbstractTransaction from '../../lib/transactions/AbstractTransaction' -import Database from '../../src/db/Database'; -import Whitelist from '../../src/db/Whitelist'; -import Admin from '../../src/db/Admin'; +import Configuration from '../../src/config/Configuration' +import Provider from '../../src/provider/Provider' +import Wallet from '../../src/wallet/Wallet' -import Authenticator from '../../src/auth/Authenticator'; +import Database from '../../src/db/Database' +import Whitelist from '../../src/db/Whitelist' +import Admin from '../../src/db/Admin' + +import Authenticator from '../../src/auth/Authenticator' -import Bootstrap from '../../src/Bootstrap'; import ExecuteTransaction from '../../src/transactions/execute/ExecuteTransaction'; +import ChallengeTransaction from '../../src/transactions/challenge/ChallengeTransaction' +import ScheduleTransaction from '../../src/transactions/schedule/ScheduleTransaction' + +import AddItemAction from '../../src/whitelist/AddItemAction'; +import DeleteItemAction from '../../src/whitelist/DeleteItemAction'; +import GetListAction from '../../src/whitelist/GetListAction'; + +import Bootstrap from '../../src/Bootstrap' //Mocks const fastifyMock: any = { @@ -49,7 +59,13 @@ jest.mock('../../src/whitelist/GetListAction') * Bootstrap test */ describe('BootstrapTest', () => { - let bootstrap: Bootstrap + let bootstrap: Bootstrap, + executeTransactionMock: ExecuteTransaction, + scheduleTransactionMock: ScheduleTransaction, + challengeTransactionMock: ChallengeTransaction, + addItemActionMock: AddItemAction, + deleteItemActionMock: DeleteItemAction, + getListActionMock: GetListAction const config = { ethereum: { @@ -77,16 +93,26 @@ describe('BootstrapTest', () => { fastifyMock.post = jest.fn((path, schemaObj, callback) => { switch(path) { case '/execute': + expect(schemaObj).toEqual({schema: AbstractTransaction.schema}) + callback({params: true}) + executeTransactionMock = ExecuteTransaction.mock.instances[0] + break case '/schedule': + expect(schemaObj).toEqual({schema: AbstractTransaction.schema}) + callback({params: true}) + scheduleTransactionMock = ScheduleTransaction.mock.instances[0] + break case '/challenge': expect(schemaObj).toEqual({schema: AbstractTransaction.schema}) + callback({params: true}) + challengeTransactionMock = ChallengeTransaction.mock.instances[0] break case '/whitelist': expect(schemaObj).toEqual({schema: AbstractWhitelistAction.schema}) + callback({params: true}) + addItemActionMock = AddItemAction.mock.instances[0] break } - - callback({params: true}) }) fastifyMock.delete = jest.fn((path, schemaObj, callback) => { @@ -95,6 +121,8 @@ describe('BootstrapTest', () => { expect(schemaObj).toEqual({schema: AbstractWhitelistAction.schema}) callback({params: true}) + + deleteItemActionMock = DeleteItemAction.mock.instances[0] }) fastifyMock.get = jest.fn((path, schemaObj, callback) => { @@ -103,6 +131,8 @@ describe('BootstrapTest', () => { expect(schemaObj).toEqual({schema: AbstractWhitelistAction.schema}) callback({params: true}) + + getListActionMock = GetListAction.mock.instances[0] }) bootstrap = new Bootstrap( @@ -155,7 +185,7 @@ describe('BootstrapTest', () => { * Expectations for all added routes ************************************/ expect(ExecuteTransaction).toHaveBeenNthCalledWith(1, config.ethereum, providerMock, true) - expect(executeTranscationMock.execute).toHaveBeenCalledTimes(1) + expect(executeTransactionMock.execute).toHaveBeenCalledTimes(1) expect(ScheduleTransaction).toHaveBeenNthCalledWith(1, config.ethereum, providerMock, true) expect(scheduleTransactionMock.execute).toHaveBeenCalledTimes(1) @@ -170,7 +200,7 @@ describe('BootstrapTest', () => { expect(deleteItemActionMock.execute).toHaveBeenCalledTimes(1) expect(GetListAction).toHaveBeenNthCalledWith(1, whitelistMock, true) - expect(getListItemAction.execute).toHaveBeenCalledTimes(1) + expect(getListActionMock.execute).toHaveBeenCalledTimes(1) expect(fastifyMock.post).toHaveBeenCalledTimes(4) expect(fastifyMock.delete).toHaveBeenCalledTimes(1) From eba5cd7d67166748772dba3f66f64f0d3992e92d Mon Sep 17 00:00:00 2001 From: nivida Date: Fri, 4 Dec 2020 15:16:11 +0100 Subject: [PATCH 048/107] types updated, postgres dep checked, BootstrapTest updated --- packages/govern-tx/.gitignore | 1 + packages/govern-tx/lib/AbstractAction.ts | 4 ++-- packages/govern-tx/package.json | 7 ++++--- packages/govern-tx/src/auth/Authenticator.ts | 3 ++- packages/govern-tx/src/db/Database.ts | 5 +++-- packages/govern-tx/src/whitelist/AddItemAction.ts | 4 ++-- .../govern-tx/src/whitelist/DeleteItemAction.ts | 2 +- packages/govern-tx/test/src/BootstrapTest.ts | 14 ++++++++------ packages/govern-tx/tsconfig.json | 3 ++- yarn.lock | 13 +++++++++---- 10 files changed, 34 insertions(+), 22 deletions(-) create mode 100644 packages/govern-tx/.gitignore diff --git a/packages/govern-tx/.gitignore b/packages/govern-tx/.gitignore new file mode 100644 index 000000000..4b4d86310 --- /dev/null +++ b/packages/govern-tx/.gitignore @@ -0,0 +1 @@ +coverage/ \ No newline at end of file diff --git a/packages/govern-tx/lib/AbstractAction.ts b/packages/govern-tx/lib/AbstractAction.ts index 17f94a1bc..e54ba2102 100644 --- a/packages/govern-tx/lib/AbstractAction.ts +++ b/packages/govern-tx/lib/AbstractAction.ts @@ -11,7 +11,7 @@ export default abstract class AbstractAction { * * @var {Request} parameters */ - protected request: Request; + protected request: Request | undefined; /** * @param {Request} parameters @@ -33,7 +33,7 @@ export default abstract class AbstractAction { * * @protected */ - protected validateRequest(request: Request): Request { + protected validateRequest(request: Request | undefined): Request | undefined { return request; } diff --git a/packages/govern-tx/package.json b/packages/govern-tx/package.json index d9da90b10..82352707e 100644 --- a/packages/govern-tx/package.json +++ b/packages/govern-tx/package.json @@ -25,14 +25,15 @@ "jest": "^26.6.1", "ts-jest": "^26.4.2", "tslint": "^6.1.3", - "typescript": "^4.0.5" + "typescript": "^4.0.5", + "@types/node": "^14.14.10" }, "dependencies": { "@ethersproject/address": "^5.0.7", "@ethersproject/bytes": "^5.0.6", - "@ethersproject/wallet": "^5.0.8", "@ethersproject/providers": "^5.0.15", + "@ethersproject/wallet": "^5.0.8", "fastify": "^3.8.0", - "postgres": "^1.0.2" + "postgres": "^2.0.0-beta.2" } } diff --git a/packages/govern-tx/src/auth/Authenticator.ts b/packages/govern-tx/src/auth/Authenticator.ts index 21c968f7d..2584696f7 100644 --- a/packages/govern-tx/src/auth/Authenticator.ts +++ b/packages/govern-tx/src/auth/Authenticator.ts @@ -34,12 +34,13 @@ export default class Authenticator { * @public */ public async authenticate(request: FastifyRequest, reply: FastifyReply): Promise { - // TODO: Fix type definition if ( await this.hasPermission( request.routerPath, verifyMessage( + //@ts-ignore arrayify(request.body.message), + //@ts-ignore request.body.signature ) ) diff --git a/packages/govern-tx/src/db/Database.ts b/packages/govern-tx/src/db/Database.ts index 1643d861c..6fd4ace3f 100644 --- a/packages/govern-tx/src/db/Database.ts +++ b/packages/govern-tx/src/db/Database.ts @@ -1,15 +1,16 @@ -import postgres from 'postgres' +const postgres = require('postgres') import { DatabaseOptions } from '../config/Configuration' export default class Database { /** + * TODO: Define type * The sql function of the postgres client * * @property {Function} sql * * @private */ - private sql; + private sql: any; /** * @param {DatabaseOptions} config - The database configuration diff --git a/packages/govern-tx/src/whitelist/AddItemAction.ts b/packages/govern-tx/src/whitelist/AddItemAction.ts index 0f977d955..2671ab4d8 100644 --- a/packages/govern-tx/src/whitelist/AddItemAction.ts +++ b/packages/govern-tx/src/whitelist/AddItemAction.ts @@ -37,8 +37,8 @@ export default class AddItemAction extends AbstractWhitelistAction { */ public execute(): Promise { return this.whitelist.addItem( - this.request.message.publicKey, - this.request.message.rateLimit + this.request?.message.publicKey, + this.request?.message.rateLimit ) } } diff --git a/packages/govern-tx/src/whitelist/DeleteItemAction.ts b/packages/govern-tx/src/whitelist/DeleteItemAction.ts index b3b412a9c..9255ec7a8 100644 --- a/packages/govern-tx/src/whitelist/DeleteItemAction.ts +++ b/packages/govern-tx/src/whitelist/DeleteItemAction.ts @@ -32,6 +32,6 @@ export default class DeleteItemAction extends AbstractWhitelistAction { * @public */ public execute(): Promise { - return this.whitelist.deleteItem(this.request.message.publicKey); + return this.whitelist.deleteItem(this.request?.message.publicKey); } } diff --git a/packages/govern-tx/test/src/BootstrapTest.ts b/packages/govern-tx/test/src/BootstrapTest.ts index 7af2fe0e0..b3d5fcead 100644 --- a/packages/govern-tx/test/src/BootstrapTest.ts +++ b/packages/govern-tx/test/src/BootstrapTest.ts @@ -32,12 +32,13 @@ const fastifyMock: any = { addHook: jest.fn() } jest.mock('fastify', () => { - return jest.fn(() => { - return fastifyMock - }); + return { + default: jest.fn(() => { + return fastifyMock + }) + } }) -jest.mock('../../src/config/Configuration') jest.mock('../../src/provider/Provider') jest.mock('../../src/wallet/Wallet') @@ -126,7 +127,7 @@ describe('BootstrapTest', () => { }) fastifyMock.get = jest.fn((path, schemaObj, callback) => { - expect(path).toEquela('/whitelist') + expect(path).toEqual('/whitelist') expect(schemaObj).toEqual({schema: AbstractWhitelistAction.schema}) @@ -253,11 +254,12 @@ describe('BootstrapTest', () => { callback(true, null) - expect(console.log).toHaveBeenNthCalledWith(1, true) + expect(console.error).toHaveBeenNthCalledWith(1, true) expect(process.exit).toHaveBeenNthCalledWith(1, 0) done() + process = realProcess }) }) }) diff --git a/packages/govern-tx/tsconfig.json b/packages/govern-tx/tsconfig.json index cc0324886..c7cbba784 100644 --- a/packages/govern-tx/tsconfig.json +++ b/packages/govern-tx/tsconfig.json @@ -7,7 +7,8 @@ "outDir": "./dist", "inlineSources": true, "types": [ - "jest" + "jest", + "node" ], "lib": [ "dom", diff --git a/yarn.lock b/yarn.lock index 3580878ef..6f113186d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4240,6 +4240,11 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-12.19.1.tgz#303f74c8a2b35644594139e948b2be470ae1186f" integrity sha512-/xaVmBBjOGh55WCqumLAHXU9VhjGtmyTGqJzFBXRWZzByOXI5JAJNx9xPVGEsNizrNwcec92fQMj458MWfjN1A== +"@types/node@^14.14.10": + version "14.14.10" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.10.tgz#5958a82e41863cfc71f2307b3748e3491ba03785" + integrity sha512-J32dgx2hw8vXrSbu4ZlVhn1Nm3GbeCFNw2FWL8S5QKucHGY0cyNwjdQdO+KMBZ4wpmC7KhLCiNsdk1RFRIYUQQ== + "@types/node@^14.14.6": version "14.14.6" resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.6.tgz#146d3da57b3c636cc0d1769396ce1cfa8991147f" @@ -18653,10 +18658,10 @@ postcss@^7, postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.14, postcss@^7.0.17, po source-map "^0.6.1" supports-color "^6.1.0" -postgres@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/postgres/-/postgres-1.0.2.tgz#7b9f6769934f727cec0f1d7ef58d4915a327f5dd" - integrity sha512-zeLgt42KSUNgX/uvo+gbVxTAYwgSY6MIKuU/a8YWuObX4rtGuKrVWopvEAqIAPSO0FeHS1TsSKnqPjoufPy8NA== +postgres@^2.0.0-beta.2: + version "2.0.0-beta.2" + resolved "https://registry.yarnpkg.com/postgres/-/postgres-2.0.0-beta.2.tgz#4844381137b6394a66c4c7566aedb63392ffe416" + integrity sha512-x5/FBSLsU/eVeKcIREsaCBeFp5CFbZoberSug3dgsJMlvWwGBtl753hALBVR+GJ5Dty1lb+zfqV9ySHyOndypA== postinstall-postinstall@^2.1.0: version "2.1.0" From f8d49f2dbe05616a22179c0d7f2950a7e1dd8616 Mon Sep 17 00:00:00 2001 From: nivida Date: Fri, 4 Dec 2020 15:43:59 +0100 Subject: [PATCH 049/107] BootstrapTest implemented --- packages/govern-tx/test/src/BootstrapTest.ts | 59 ++++++++++++-------- 1 file changed, 35 insertions(+), 24 deletions(-) diff --git a/packages/govern-tx/test/src/BootstrapTest.ts b/packages/govern-tx/test/src/BootstrapTest.ts index b3d5fcead..fd9932802 100644 --- a/packages/govern-tx/test/src/BootstrapTest.ts +++ b/packages/govern-tx/test/src/BootstrapTest.ts @@ -29,13 +29,16 @@ const fastifyMock: any = { post: jest.fn(), delete: jest.fn(), get: jest.fn(), - addHook: jest.fn() + addHook: jest.fn(), + options: {} } jest.mock('fastify', () => { return { - default: jest.fn(() => { + default: (options) => { + fastifyMock.options = options + return fastifyMock - }) + } } }) @@ -90,7 +93,7 @@ describe('BootstrapTest', () => { } } - beforeEach(() => { + it('calls the constructor and initiates the class as expected', () => { fastifyMock.post = jest.fn((path, schemaObj, callback) => { switch(path) { case '/execute': @@ -139,16 +142,13 @@ describe('BootstrapTest', () => { bootstrap = new Bootstrap( new Configuration(config) ) - }) - it('calls the constructor and initiates the class as expected', (done) => { /******************************************** * Expectations for all invoked constructors ********************************************/ expect( - fastify - ).toHaveBeenNthCalledWith( - 1, + fastifyMock.options + ).toEqual( { logger: { level: 'debug' @@ -166,7 +166,7 @@ describe('BootstrapTest', () => { expect(Provider).toHaveBeenNthCalledWith(1, config.ethereum, Wallet.mock.instances[0]) - expect(Wallet).toHaveBeenNthCalledWith(1, config.database) + expect(Wallet).toHaveBeenNthCalledWith(1, Database.mock.instances[0]) expect(bootstrap.whitelist).toBeInstanceOf(Whitelist) @@ -179,28 +179,31 @@ describe('BootstrapTest', () => { /*********************************** * Expectations for added Auth Hook ***********************************/ - - expect(bootstrap.server.addHook).toHaveBeenNthCalledWith(1, 'preHandler', Authenticator.mock.instances[0].authenticate) + expect(bootstrap.server.addHook).toHaveBeenNthCalledWith( + 1, + 'preHandler', + expect.any(Function) + ) /************************************ * Expectations for all added routes ************************************/ - expect(ExecuteTransaction).toHaveBeenNthCalledWith(1, config.ethereum, providerMock, true) + expect(ExecuteTransaction).toHaveBeenNthCalledWith(1, config.ethereum, Provider.mock.instances[0], true) expect(executeTransactionMock.execute).toHaveBeenCalledTimes(1) - expect(ScheduleTransaction).toHaveBeenNthCalledWith(1, config.ethereum, providerMock, true) + expect(ScheduleTransaction).toHaveBeenNthCalledWith(1, config.ethereum, Provider.mock.instances[0], true) expect(scheduleTransactionMock.execute).toHaveBeenCalledTimes(1) - expect(ChallengeTransaction).toHaveBeenNthCalledWith(1, config.ethereum, providerMock, true) + expect(ChallengeTransaction).toHaveBeenNthCalledWith(1, config.ethereum, Provider.mock.instances[0], true) expect(challengeTransactionMock.execute).toHaveBeenCalledTimes(1) - expect(AddItemAction).toHaveBeenNthCalledWith(1, whitelistMock, true) + expect(AddItemAction).toHaveBeenNthCalledWith(1, Whitelist.mock.instances[0], true) expect(addItemActionMock.execute).toHaveBeenCalledTimes(1) - expect(DeleteItemAction).toHaveBeenNthCalledWith(1, whitelistMock, true) + expect(DeleteItemAction).toHaveBeenNthCalledWith(1, Whitelist.mock.instances[0], true) expect(deleteItemActionMock.execute).toHaveBeenCalledTimes(1) - expect(GetListAction).toHaveBeenNthCalledWith(1, whitelistMock, true) + expect(GetListAction).toHaveBeenNthCalledWith(1, Whitelist.mock.instances[0]) expect(getListActionMock.execute).toHaveBeenCalledTimes(1) expect(fastifyMock.post).toHaveBeenCalledTimes(4) @@ -208,7 +211,7 @@ describe('BootstrapTest', () => { expect(fastifyMock.get).toHaveBeenCalledTimes(1) }) - it('calls the constructor and uses the configured logging level for fastify', async () => { + it('calls the constructor and uses the configured logging level for fastify', () => { config.server.logLevel = 'warn' bootstrap = new Bootstrap( @@ -216,8 +219,8 @@ describe('BootstrapTest', () => { ) expect( - fastify - ).toHaveBeenCalledWith( + fastifyMock.options + ).toEqual( { logger: { level: 'warn' @@ -227,10 +230,9 @@ describe('BootstrapTest', () => { }) it('calls run and starts the server as expected on the configured port', (done) => { - bootstrap.run() console.log = jest.fn(); - bootstrap.server.listen = jest.fn((port, host, callback) => { + fastifyMock.listen = jest.fn((port, host, callback) => { expect(port).toEqual(config.server.port) expect(host).toEqual(config.server.host) @@ -240,10 +242,14 @@ describe('BootstrapTest', () => { done() }) + + bootstrap = new Bootstrap( + new Configuration(config) + ) + bootstrap.run() }) it('calls run and does log the error as expected', (done) => { - bootstrap.run() const realProcess = process console.error = jest.fn(); process = {...realProcess, exit: jest.fn()} @@ -261,5 +267,10 @@ describe('BootstrapTest', () => { done() process = realProcess }) + + bootstrap = new Bootstrap( + new Configuration(config) + ) + bootstrap.run() }) }) From b3d5a9259cb7114f5c104f0427563b9b3c4517b9 Mon Sep 17 00:00:00 2001 From: nivida Date: Fri, 4 Dec 2020 15:48:53 +0100 Subject: [PATCH 050/107] unused import removed in BootstrapTest --- packages/govern-tx/test/src/BootstrapTest.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/govern-tx/test/src/BootstrapTest.ts b/packages/govern-tx/test/src/BootstrapTest.ts index fd9932802..e61777ece 100644 --- a/packages/govern-tx/test/src/BootstrapTest.ts +++ b/packages/govern-tx/test/src/BootstrapTest.ts @@ -1,6 +1,4 @@ //@ts-nocheck -import { fastify } from 'fastify' - import AbstractWhitelistAction from '../../lib/whitelist/AbstractWhitelistAction' import AbstractTransaction from '../../lib/transactions/AbstractTransaction' From 8ffb8ee25addd0cca8437b327e1e471da8323fc6 Mon Sep 17 00:00:00 2001 From: nivida Date: Mon, 7 Dec 2020 09:59:49 +0100 Subject: [PATCH 051/107] Test template for Authenticator added --- packages/govern-tx/src/auth/Authenticator.ts | 4 +- .../test/src/auth/AuthenticatorTest.ts | 88 +++++++++++++++++++ 2 files changed, 90 insertions(+), 2 deletions(-) create mode 100644 packages/govern-tx/test/src/auth/AuthenticatorTest.ts diff --git a/packages/govern-tx/src/auth/Authenticator.ts b/packages/govern-tx/src/auth/Authenticator.ts index 2584696f7..009977de0 100644 --- a/packages/govern-tx/src/auth/Authenticator.ts +++ b/packages/govern-tx/src/auth/Authenticator.ts @@ -1,4 +1,4 @@ -import { FastifyRequest, FastifyReply } from 'fastify'; +import { FastifyRequest } from 'fastify'; import { verifyMessage } from '@ethersproject/wallet'; import { arrayify } from '@ethersproject/bytes' import { Unauthorized, HttpError } from 'http-errors' @@ -33,7 +33,7 @@ export default class Authenticator { * * @public */ - public async authenticate(request: FastifyRequest, reply: FastifyReply): Promise { + public async authenticate(request: FastifyRequest): Promise { if ( await this.hasPermission( request.routerPath, diff --git a/packages/govern-tx/test/src/auth/AuthenticatorTest.ts b/packages/govern-tx/test/src/auth/AuthenticatorTest.ts new file mode 100644 index 000000000..c6fa69835 --- /dev/null +++ b/packages/govern-tx/test/src/auth/AuthenticatorTest.ts @@ -0,0 +1,88 @@ +import { verifyMessage } from '@ethersproject/wallet'; +import { arrayify } from '@ethersproject/bytes' +import { Unauthorized } from 'http-errors'; +import { FastifyRequest } from 'fastify'; +import Authenticator from '../../../src/auth/Authenticator'; +import Admin from '../../../src/db/Admin'; +import Whitelist from '../../../src/db/Whitelist'; +import Database from '../../../src/db/Database'; + + + +// Mocks +jest.mock('../../../src/db/Admin') +jest.mock('../../../src/db/Whitelist') +jest.mock('../../../src/db/Database') +jest.mock('@ethersproject/wallet', () => { + return { + verifyMessage: jest.fn(() => {return '0x00'}) + } +}) +jest.mock('@ethersproject/bytes', () => { + return { + arrayify: jest.fn((value) => { + return value + }) + } +}) + +/** + * Authenticator test + */ +describe('AuthenticatorTest', () => { + let authenticator: Authenticator, + whitelistMock: Whitelist, + databaseMock: Database, + adminMock: Admin; + + const NO_ALLOWED = new Unauthorized('Not allowed action!'); + + beforeEach(() => { + databaseMock = new Database({ + user: 'user', + host: 'host', + password: 'password', + database: 'databaseName', + port: 1000 + }) + + whitelistMock = new Whitelist(databaseMock) + adminMock = new Admin(databaseMock) + + authenticator = new Authenticator(whitelistMock, adminMock) + }) + + it('calls authenticate as normal user and grants access to the transaction actions', async () => { + const request = { + routerPath: '/execute' + body: { + signature: '0x00', + message: '0x00' + } + }; + + whitelistMock.keyExists = jest.fn((publicKey) => { + expect(publicKey).toEqual('0x00') + + return Promise.resolve(true) + }) + + await authenticator.authenticate(request as FastifyRequest) + + expect(verifyMessage).toHaveBeenNthCalledWith(1, request.body.message, request.body.signature) + + expect(arrayify).toHaveBeenNthCalledWith(1, request.body.message) + }) + + it('calls authenticate as normal user and restricts access to the whitelist', async () => { + + }) + + it('calls authencticate as admin user and grants access to the transaction actions', async () => { + + }) + + it('calls authenticate as admin user and grants access to the whitelist actions', async () => { + + }) +}) \ No newline at end of file From b922984da6ee7714cd4b02f33a87c8bbeefca26e Mon Sep 17 00:00:00 2001 From: nivida Date: Mon, 7 Dec 2020 11:15:41 +0100 Subject: [PATCH 052/107] Authenticator whitelist check fixed and AuthenticatorTest implemented --- packages/govern-tx/src/auth/Authenticator.ts | 2 +- .../test/src/auth/AuthenticatorTest.ts | 97 +++++++++++++------ 2 files changed, 71 insertions(+), 28 deletions(-) diff --git a/packages/govern-tx/src/auth/Authenticator.ts b/packages/govern-tx/src/auth/Authenticator.ts index 009977de0..31ff3e6a2 100644 --- a/packages/govern-tx/src/auth/Authenticator.ts +++ b/packages/govern-tx/src/auth/Authenticator.ts @@ -71,7 +71,7 @@ export default class Authenticator { return true } - if (routerPath === '/whitelist' && this.admin.isAdmin(publicKey)) { + if (routerPath === '/whitelist' && await this.admin.isAdmin(publicKey)) { return true } diff --git a/packages/govern-tx/test/src/auth/AuthenticatorTest.ts b/packages/govern-tx/test/src/auth/AuthenticatorTest.ts index c6fa69835..d590fc4fe 100644 --- a/packages/govern-tx/test/src/auth/AuthenticatorTest.ts +++ b/packages/govern-tx/test/src/auth/AuthenticatorTest.ts @@ -7,24 +7,12 @@ import Admin from '../../../src/db/Admin'; import Whitelist from '../../../src/db/Whitelist'; import Database from '../../../src/db/Database'; - - // Mocks jest.mock('../../../src/db/Admin') jest.mock('../../../src/db/Whitelist') jest.mock('../../../src/db/Database') -jest.mock('@ethersproject/wallet', () => { - return { - verifyMessage: jest.fn(() => {return '0x00'}) - } -}) -jest.mock('@ethersproject/bytes', () => { - return { - arrayify: jest.fn((value) => { - return value - }) - } -}) +jest.mock('@ethersproject/wallet') +jest.mock('@ethersproject/bytes') /** * Authenticator test @@ -32,12 +20,28 @@ jest.mock('@ethersproject/bytes', () => { describe('AuthenticatorTest', () => { let authenticator: Authenticator, whitelistMock: Whitelist, + whitelistMockClass = (Whitelist as jest.MockedClass), databaseMock: Database, - adminMock: Admin; + adminMock: Admin, + adminMockClass = (Admin as jest.MockedClass), + request = { + routerPath: '/execute', + body: { + signature: '0x00', + message: '0x00' + } + }; + const NO_ALLOWED = new Unauthorized('Not allowed action!'); beforeEach(() => { + const verifyMock = verifyMessage as jest.MockedFunction + const arrayifyMock = arrayify as jest.MockedFunction + + verifyMock.mockReturnValue('0x00') + arrayifyMock.mockReturnValue(new Uint8Array(0x00)) + databaseMock = new Database({ user: 'user', host: 'host', @@ -46,43 +50,82 @@ describe('AuthenticatorTest', () => { port: 1000 }) - whitelistMock = new Whitelist(databaseMock) - adminMock = new Admin(databaseMock) + new Whitelist(databaseMock) + whitelistMock = whitelistMockClass.mock.instances[0] + + new Admin(databaseMock) + adminMock = adminMockClass.mock.instances[0] authenticator = new Authenticator(whitelistMock, adminMock) }) it('calls authenticate as normal user and grants access to the transaction actions', async () => { - const request = { - routerPath: '/execute' - body: { - signature: '0x00', - message: '0x00' - } - }; - whitelistMock.keyExists = jest.fn((publicKey) => { expect(publicKey).toEqual('0x00') - + return Promise.resolve(true) }) await authenticator.authenticate(request as FastifyRequest) - expect(verifyMessage).toHaveBeenNthCalledWith(1, request.body.message, request.body.signature) + expect(verifyMessage).toHaveBeenNthCalledWith(1, new Uint8Array(0x00), request.body.signature) expect(arrayify).toHaveBeenNthCalledWith(1, request.body.message) }) it('calls authenticate as normal user and restricts access to the whitelist', async () => { + adminMock.isAdmin = jest.fn((publicKey) => { + expect(publicKey).toEqual('0x00') + return Promise.resolve(false) + }) + + request.routerPath = '/whitelist' + + await expect(authenticator.authenticate(request as FastifyRequest)).rejects.toThrowError(NO_ALLOWED) + + expect(verifyMessage).toHaveBeenNthCalledWith(1, new Uint8Array(0x00), request.body.signature) + + expect(arrayify).toHaveBeenNthCalledWith(1, request.body.message) + + expect(adminMock.isAdmin).toHaveBeenNthCalledWith(1, '0x00') }) it('calls authencticate as admin user and grants access to the transaction actions', async () => { + adminMock.isAdmin = jest.fn((publicKey) => { + expect(publicKey).toEqual('0x00') + + return Promise.resolve(true) + }) + whitelistMock.keyExists = jest.fn((publicKey) => { + expect(publicKey).toEqual('0x00') + + return Promise.resolve(false) + }) + + request.routerPath = '/execute' + + await authenticator.authenticate(request as FastifyRequest) + + expect(verifyMessage).toHaveBeenNthCalledWith(1, new Uint8Array(0x00), request.body.signature) + + expect(arrayify).toHaveBeenNthCalledWith(1, request.body.message) }) it('calls authenticate as admin user and grants access to the whitelist actions', async () => { + adminMock.isAdmin = jest.fn((publicKey) => { + expect(publicKey).toEqual('0x00') + + return Promise.resolve(true) + }) + + request.routerPath = '/whitelist' + await authenticator.authenticate(request as FastifyRequest) + + expect(verifyMessage).toHaveBeenNthCalledWith(1, new Uint8Array(0x00), request.body.signature) + + expect(arrayify).toHaveBeenNthCalledWith(1, request.body.message) }) }) \ No newline at end of file From f6c848485f46701d0ce6915e8e132f6137964c05 Mon Sep 17 00:00:00 2001 From: nivida Date: Mon, 7 Dec 2020 11:39:32 +0100 Subject: [PATCH 053/107] expectations added to AuthenticatorTest --- packages/govern-tx/test/src/auth/AuthenticatorTest.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/govern-tx/test/src/auth/AuthenticatorTest.ts b/packages/govern-tx/test/src/auth/AuthenticatorTest.ts index d590fc4fe..2466ebc3b 100644 --- a/packages/govern-tx/test/src/auth/AuthenticatorTest.ts +++ b/packages/govern-tx/test/src/auth/AuthenticatorTest.ts @@ -71,6 +71,8 @@ describe('AuthenticatorTest', () => { expect(verifyMessage).toHaveBeenNthCalledWith(1, new Uint8Array(0x00), request.body.signature) expect(arrayify).toHaveBeenNthCalledWith(1, request.body.message) + + expect(whitelistMock.keyExists).toHaveBeenCalled() }) it('calls authenticate as normal user and restricts access to the whitelist', async () => { @@ -88,7 +90,7 @@ describe('AuthenticatorTest', () => { expect(arrayify).toHaveBeenNthCalledWith(1, request.body.message) - expect(adminMock.isAdmin).toHaveBeenNthCalledWith(1, '0x00') + expect(adminMock.isAdmin).toHaveBeenCalled() }) it('calls authencticate as admin user and grants access to the transaction actions', async () => { @@ -111,6 +113,10 @@ describe('AuthenticatorTest', () => { expect(verifyMessage).toHaveBeenNthCalledWith(1, new Uint8Array(0x00), request.body.signature) expect(arrayify).toHaveBeenNthCalledWith(1, request.body.message) + + expect(adminMock.isAdmin).toHaveBeenCalled() + + expect(whitelistMock.keyExists).toHaveBeenCalled() }) it('calls authenticate as admin user and grants access to the whitelist actions', async () => { @@ -127,5 +133,7 @@ describe('AuthenticatorTest', () => { expect(verifyMessage).toHaveBeenNthCalledWith(1, new Uint8Array(0x00), request.body.signature) expect(arrayify).toHaveBeenNthCalledWith(1, request.body.message) + + expect(adminMock.isAdmin).toHaveBeenCalled() }) }) \ No newline at end of file From 7bbb682cd0da7eea9c34b8b6b0338d209fdce104 Mon Sep 17 00:00:00 2001 From: nivida Date: Mon, 7 Dec 2020 11:41:33 +0100 Subject: [PATCH 054/107] Configuration VO excluded from coverage --- packages/govern-tx/src/config/Configuration.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/govern-tx/src/config/Configuration.ts b/packages/govern-tx/src/config/Configuration.ts index 1db2cea39..1d6ef07c5 100644 --- a/packages/govern-tx/src/config/Configuration.ts +++ b/packages/govern-tx/src/config/Configuration.ts @@ -1,3 +1,6 @@ +/* istanbul ignore file */ +// Ignore added because VOs doesn't have to be tested especially if they do not have any logic + export interface EthereumOptions { url: string blockConfirmations: number @@ -27,7 +30,6 @@ export interface Config { server: ServerOptions } -// TODO: Add input validations for Ethereum addresses export default class Configuration { /** * The options to connect to a Ethereum node and how TXs should be handled From 86b1038fb03c3ef23d62d299f50ed3a3ee05c050 Mon Sep 17 00:00:00 2001 From: nivida Date: Mon, 7 Dec 2020 13:36:13 +0100 Subject: [PATCH 055/107] DatabaseTest added --- packages/govern-tx/src/db/Database.ts | 2 +- .../govern-tx/test/src/db/DatabaseTest.ts | 50 +++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 packages/govern-tx/test/src/db/DatabaseTest.ts diff --git a/packages/govern-tx/src/db/Database.ts b/packages/govern-tx/src/db/Database.ts index 6fd4ace3f..64ca19457 100644 --- a/packages/govern-tx/src/db/Database.ts +++ b/packages/govern-tx/src/db/Database.ts @@ -1,4 +1,4 @@ -const postgres = require('postgres') +import postgres = require('postgres'); import { DatabaseOptions } from '../config/Configuration' export default class Database { diff --git a/packages/govern-tx/test/src/db/DatabaseTest.ts b/packages/govern-tx/test/src/db/DatabaseTest.ts new file mode 100644 index 000000000..b7b769ac3 --- /dev/null +++ b/packages/govern-tx/test/src/db/DatabaseTest.ts @@ -0,0 +1,50 @@ +import postgres = require('postgres'); +import Database from '../../../src/db/Database'; + +const config = { + host: 'host', + port: 100, + database: 'database', + user: 'user', + password: 'password' +} + +// Mocks +const postgressMockFunc = jest.fn() + +jest.mock('postgres', () => { + return (options: any) => { + expect(options).toEqual({ + host: 'host', + port: 100, + database: 'database', + username: 'user', + password: 'password' + }) + + return postgressMockFunc + } +}) + +/** + * Database test + */ +describe('DatabaseTest', () => { + let database: Database; + + beforeEach(() => { + database = new Database(config) + }) + + it('calls the constructor and establishes the connection as expected', () => { + //@ts-ignore + expect(database.sql).toEqual(postgressMockFunc) + }) + + it('calls query and calls the postgres sql function as expected', () => { + database.query('ASDF') + + //@ts-ignore + expect(database.sql).toHaveBeenNthCalledWith(1, 'ASDF') + }) +}) \ No newline at end of file From 09ba82ebeb2dad065173074b4fe98a867c32d2af Mon Sep 17 00:00:00 2001 From: nivida Date: Mon, 7 Dec 2020 13:42:55 +0100 Subject: [PATCH 056/107] DatabaseTest simplified --- .../govern-tx/test/src/db/DatabaseTest.ts | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/packages/govern-tx/test/src/db/DatabaseTest.ts b/packages/govern-tx/test/src/db/DatabaseTest.ts index b7b769ac3..1e3f4019f 100644 --- a/packages/govern-tx/test/src/db/DatabaseTest.ts +++ b/packages/govern-tx/test/src/db/DatabaseTest.ts @@ -1,4 +1,3 @@ -import postgres = require('postgres'); import Database from '../../../src/db/Database'; const config = { @@ -10,8 +9,6 @@ const config = { } // Mocks -const postgressMockFunc = jest.fn() - jest.mock('postgres', () => { return (options: any) => { expect(options).toEqual({ @@ -22,7 +19,7 @@ jest.mock('postgres', () => { password: 'password' }) - return postgressMockFunc + return jest.fn(() => Promise.resolve('EXECUTED')) } }) @@ -36,15 +33,7 @@ describe('DatabaseTest', () => { database = new Database(config) }) - it('calls the constructor and establishes the connection as expected', () => { - //@ts-ignore - expect(database.sql).toEqual(postgressMockFunc) + it('calls query and calls the postgres sql function as expected', async () => { + expect(await database.query('ASDF')).toEqual('EXECUTED') }) - - it('calls query and calls the postgres sql function as expected', () => { - database.query('ASDF') - - //@ts-ignore - expect(database.sql).toHaveBeenNthCalledWith(1, 'ASDF') - }) -}) \ No newline at end of file +}) From df2318c5c31ae243d34185ae64730807203b58d7 Mon Sep 17 00:00:00 2001 From: nivida Date: Mon, 7 Dec 2020 13:50:38 +0100 Subject: [PATCH 057/107] Mock types handling in AuthenticatorTest improved and AdminTest template added --- .../test/src/auth/AuthenticatorTest.ts | 6 +- packages/govern-tx/test/src/db/AdminTest.ts | 62 +++++++++++++++++++ 2 files changed, 64 insertions(+), 4 deletions(-) create mode 100644 packages/govern-tx/test/src/db/AdminTest.ts diff --git a/packages/govern-tx/test/src/auth/AuthenticatorTest.ts b/packages/govern-tx/test/src/auth/AuthenticatorTest.ts index 2466ebc3b..1ac0414de 100644 --- a/packages/govern-tx/test/src/auth/AuthenticatorTest.ts +++ b/packages/govern-tx/test/src/auth/AuthenticatorTest.ts @@ -20,10 +20,8 @@ jest.mock('@ethersproject/bytes') describe('AuthenticatorTest', () => { let authenticator: Authenticator, whitelistMock: Whitelist, - whitelistMockClass = (Whitelist as jest.MockedClass), databaseMock: Database, adminMock: Admin, - adminMockClass = (Admin as jest.MockedClass), request = { routerPath: '/execute', body: { @@ -51,10 +49,10 @@ describe('AuthenticatorTest', () => { }) new Whitelist(databaseMock) - whitelistMock = whitelistMockClass.mock.instances[0] + whitelistMock = (Whitelist as jest.MockedClass).mock.instances[0] new Admin(databaseMock) - adminMock = adminMockClass.mock.instances[0] + adminMock = (Admin as jest.MockedClass).mock.instances[0] authenticator = new Authenticator(whitelistMock, adminMock) }) diff --git a/packages/govern-tx/test/src/db/AdminTest.ts b/packages/govern-tx/test/src/db/AdminTest.ts new file mode 100644 index 000000000..74f84045a --- /dev/null +++ b/packages/govern-tx/test/src/db/AdminTest.ts @@ -0,0 +1,62 @@ +import Database from '../../../src/db/Database'; +import Admin from '../../../src/db/Admin'; + +// Mocks +jest.mock('../../../src/db/Database') + +/** + * Admin test + */ +describe('AdminTest', () => { + let admin: Admin, + databaseMock: Database; + + beforeEach(() => { + new Database({ + host: 'host', + port: 100, + database: 'database', + user: 'user', + password: 'password' + }) + databaseMock = (Database as jest.MockedClass).mock.instances[0] + + admin = new Admin(databaseMock); + }) + + it('calls isAdmin and returns true', () => { + + }) + + it('calls isAdmin and returns false', () => { + + }) + + it('calls addAdmin and returns the added record', () => { + + }) + + it('calls addAdmin and throws as expected', () => { + + }) + + it('calls deleteAdmin and returns true', () => { + + }) + + it('calls deleteAdmin and returns false', () => { + + }) + + it('calls deleteAdmin and throws as expected', () => { + + }) + + it('calls getAdmins and returns the expected list of admin records', () => { + + }) + + it('calls getAdmins and throws as expected', () => { + + }) +}) \ No newline at end of file From bb3ddb317bb79e2da6b5115177c889103f29e5fa Mon Sep 17 00:00:00 2001 From: nivida Date: Mon, 7 Dec 2020 14:00:19 +0100 Subject: [PATCH 058/107] AdminTest added --- packages/govern-tx/src/db/Admin.ts | 2 +- packages/govern-tx/test/src/db/AdminTest.ts | 84 ++++++++++++++++++--- 2 files changed, 75 insertions(+), 11 deletions(-) diff --git a/packages/govern-tx/src/db/Admin.ts b/packages/govern-tx/src/db/Admin.ts index 4826ed1f6..6626f39ac 100644 --- a/packages/govern-tx/src/db/Admin.ts +++ b/packages/govern-tx/src/db/Admin.ts @@ -52,7 +52,7 @@ export default class Admin { * @public */ public async deleteAdmin(publicKey: string): Promise { - return (await this.db.query(`DELETE FROM admins WHERE PublicKey='${publicKey}'`)) > 0 + return (await this.db.query(`DELETE FROM admins WHERE PublicKey='${publicKey}'`)).length > 0 } /** diff --git a/packages/govern-tx/test/src/db/AdminTest.ts b/packages/govern-tx/test/src/db/AdminTest.ts index 74f84045a..fd14b7eb5 100644 --- a/packages/govern-tx/test/src/db/AdminTest.ts +++ b/packages/govern-tx/test/src/db/AdminTest.ts @@ -24,39 +24,103 @@ describe('AdminTest', () => { admin = new Admin(databaseMock); }) - it('calls isAdmin and returns true', () => { + it('calls isAdmin and returns true', async () => { + databaseMock.query = jest.fn((query) => { + expect(query).toEqual(`SELECT * FROM admins WHERE PublicKey='0x00'`) + return Promise.resolve([0]) + }) + + await expect(admin.isAdmin('0x00')).resolves.toEqual(true) + }) + + it('calls isAdmin and returns false', async () => { + databaseMock.query = jest.fn((query) => { + expect(query).toEqual(`SELECT * FROM admins WHERE PublicKey='0x00'`) + + return Promise.resolve([]) + }) + + await expect(admin.isAdmin('0x00')).resolves.toEqual(false) }) - it('calls isAdmin and returns false', () => { + it('calls isAdmin and throws as expected', async () => { + databaseMock.query = jest.fn((query) => { + expect(query).toEqual(`SELECT * FROM admins WHERE PublicKey='0x00'`) + + return Promise.reject('NOPE') + }) + await expect(admin.isAdmin('0x00')).rejects.toEqual('NOPE') }) - it('calls addAdmin and returns the added record', () => { + it('calls addAdmin and returns the expected value', async () => { + databaseMock.query = jest.fn((query) => { + expect(query).toEqual(`INSERT INTO admins VALUES (0x00)`) + return Promise.resolve(true) + }) + + await expect(admin.addAdmin('0x00')).resolves.toEqual(true) }) - it('calls addAdmin and throws as expected', () => { + it('calls addAdmin and throws as expected', async () => { + databaseMock.query = jest.fn((query) => { + expect(query).toEqual(`INSERT INTO admins VALUES (0x00)`) + + return Promise.reject('NOPE') + }) + await expect(admin.addAdmin('0x00')).rejects.toEqual('NOPE') }) - it('calls deleteAdmin and returns true', () => { + it('calls deleteAdmin and returns true', async () => { + databaseMock.query = jest.fn((query) => { + expect(query).toEqual(`DELETE FROM admins WHERE PublicKey='0x00'`) + + return Promise.resolve([0]) + }) + await expect(admin.deleteAdmin('0x00')).resolves.toEqual(true) }) - it('calls deleteAdmin and returns false', () => { + it('calls deleteAdmin and returns false', async () => { + databaseMock.query = jest.fn((query) => { + expect(query).toEqual(`DELETE FROM admins WHERE PublicKey='0x00'`) + return Promise.resolve([]) + }) + + await expect(admin.deleteAdmin('0x00')).resolves.toEqual(false) }) - it('calls deleteAdmin and throws as expected', () => { + it('calls deleteAdmin and throws as expected', async () => { + databaseMock.query = jest.fn((query) => { + expect(query).toEqual(`DELETE FROM admins WHERE PublicKey='0x00'`) + + return Promise.reject('NOPE') + }) + await expect(admin.deleteAdmin('0x00')).rejects.toEqual('NOPE') }) - it('calls getAdmins and returns the expected list of admin records', () => { + it('calls getAdmins and returns the expected value', async () => { + databaseMock.query = jest.fn((query) => { + expect(query).toEqual('SELECT * from admins') + + return Promise.resolve(true) + }) + await expect(admin.getAdmins()).resolves.toEqual(true) }) - it('calls getAdmins and throws as expected', () => { - + it('calls getAdmins and throws as expected', async () => { + databaseMock.query = jest.fn((query) => { + expect(query).toEqual('SELECT * from admins') + + return Promise.reject('NOPE') + }) + + await expect(admin.getAdmins()).rejects.toEqual('NOPE') }) }) \ No newline at end of file From 6d4c89311ee60237d6c71e6a7111d54ea608e0c4 Mon Sep 17 00:00:00 2001 From: nivida Date: Mon, 7 Dec 2020 14:05:52 +0100 Subject: [PATCH 059/107] Whitelist.deleteItem fixed and WhitelistTest template added --- packages/govern-tx/src/db/Whitelist.ts | 2 +- .../govern-tx/test/src/db/WhitelistTest.ts | 74 +++++++++++++++++++ 2 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 packages/govern-tx/test/src/db/WhitelistTest.ts diff --git a/packages/govern-tx/src/db/Whitelist.ts b/packages/govern-tx/src/db/Whitelist.ts index 9b5fe555f..1f54a2c20 100644 --- a/packages/govern-tx/src/db/Whitelist.ts +++ b/packages/govern-tx/src/db/Whitelist.ts @@ -85,6 +85,6 @@ export default class Whitelist { * @public */ public async deleteItem(publicKey: string): Promise { - return (await this.db.query(`DELETE FROM whitelist WHERE PublicKey='${publicKey}'`)) > 0; + return (await this.db.query(`DELETE FROM whitelist WHERE PublicKey='${publicKey}'`)).length > 0; } } diff --git a/packages/govern-tx/test/src/db/WhitelistTest.ts b/packages/govern-tx/test/src/db/WhitelistTest.ts new file mode 100644 index 000000000..f14618d0c --- /dev/null +++ b/packages/govern-tx/test/src/db/WhitelistTest.ts @@ -0,0 +1,74 @@ +import Database from '../../../src/db/Database'; +import Whitelist from '../../../src/db/Whitelist'; + +// Mocks +jest.mock('../../../src/db/Database') + +/** + * Whitelist test + */ +describe('WhitelistTest', () => { + let whitelist: Whitelist, + databaseMock: Database; + + beforeEach(() => { + new Database({ + host: 'host', + port: 100, + database: 'database', + user: 'user', + password: 'password' + }) + databaseMock = (Database as jest.MockedClass).mock.instances[0] + + whitelist = new Whitelist(databaseMock); + }) + + it('calls getList and returns the expected value', () => { + + }) + + it('calls getList and throws as expected', () => { + + }) + + it('calls keyExists and returns true', () => { + + }) + + it('calls keyExists and returns false', () => { + + }) + + it('calls keyExists and throws as expected', () => { + + }) + + it('calls getItemByKey and returns the expected value', () => { + + }) + + it('calls getItemByKey and throws as expected', () => { + + }) + + it('calls addItem and returns the expected value', () => { + + }) + + it('calls addItem and throws as expected', () => { + + }) + + it('calls deleteItem and returns true', () => { + + }) + + it('calls deleteItem and returns false', () => { + + }) + + it('calls deleteItem and throws as expected', () => { + + }) +}) \ No newline at end of file From 9e64044e9537986de145e0c7b1b25959b29f57e6 Mon Sep 17 00:00:00 2001 From: nivida Date: Mon, 7 Dec 2020 14:28:26 +0100 Subject: [PATCH 060/107] Whitelist.addItem type updated and WhitelistTest added --- packages/govern-tx/src/db/Whitelist.ts | 4 +- .../govern-tx/test/src/db/WhitelistTest.ts | 100 +++++++++++++++--- 2 files changed, 88 insertions(+), 16 deletions(-) diff --git a/packages/govern-tx/src/db/Whitelist.ts b/packages/govern-tx/src/db/Whitelist.ts index 1f54a2c20..ab8304ce7 100644 --- a/packages/govern-tx/src/db/Whitelist.ts +++ b/packages/govern-tx/src/db/Whitelist.ts @@ -63,13 +63,13 @@ export default class Whitelist { * @method addItem * * @param {string} publicKey - The public key we would like to add - * @param {string} rateLimit - The amount of allowed transactions for this user + * @param {number} rateLimit - The amount of allowed transactions for this user * * @returns {Promise} * * @public */ - public addItem(publicKey: string, rateLimit: string): Promise { + public addItem(publicKey: string, rateLimit: number): Promise { return this.db.query(`INSERT INTO whitelist VALUES (${publicKey}, ${rateLimit})`); } diff --git a/packages/govern-tx/test/src/db/WhitelistTest.ts b/packages/govern-tx/test/src/db/WhitelistTest.ts index f14618d0c..726b72eb9 100644 --- a/packages/govern-tx/test/src/db/WhitelistTest.ts +++ b/packages/govern-tx/test/src/db/WhitelistTest.ts @@ -24,51 +24,123 @@ describe('WhitelistTest', () => { whitelist = new Whitelist(databaseMock); }) - it('calls getList and returns the expected value', () => { + it('calls getList and returns the expected value', async () => { + databaseMock.query = jest.fn((query) => { + expect(query).toEqual('SELECT * FROM whitelist') + return Promise.resolve(true) + }) + + await expect(whitelist.getList()).resolves.toEqual(true) }) - it('calls getList and throws as expected', () => { + it('calls getList and throws as expected', async () => { + databaseMock.query = jest.fn((query) => { + expect(query).toEqual('SELECT * FROM whitelist') + + return Promise.reject('NOPE') + }) + await expect(whitelist.getList()).rejects.toEqual('NOPE') }) - it('calls keyExists and returns true', () => { + it('calls keyExists and returns true', async () => { + databaseMock.query = jest.fn((query) => { + expect(query).toEqual(`SELECT * FROM whitelist WHERE PublicKey='0x00'`) + return Promise.resolve([0]) + }) + + await expect(whitelist.keyExists('0x00')).resolves.toEqual(true) }) - it('calls keyExists and returns false', () => { + it('calls keyExists and returns false', async () => { + databaseMock.query = jest.fn((query) => { + expect(query).toEqual(`SELECT * FROM whitelist WHERE PublicKey='0x00'`) + return Promise.resolve([]) + }) + + await expect(whitelist.keyExists('0x00')).resolves.toEqual(false) }) - it('calls keyExists and throws as expected', () => { + it('calls keyExists and throws as expected', async () => { + databaseMock.query = jest.fn((query) => { + expect(query).toEqual(`SELECT * FROM whitelist WHERE PublicKey='0x00'`) + + return Promise.reject('NOPE') + }) + await expect(whitelist.keyExists('0x00')).rejects.toEqual('NOPE') }) - it('calls getItemByKey and returns the expected value', () => { + it('calls getItemByKey and returns the expected value', async () => { + databaseMock.query = jest.fn((query) => { + expect(query).toEqual(`SELECT * FROM whitelist WHERE PublicKey='0x00'`) + return Promise.resolve(true) + }) + + await expect(whitelist.getItemByKey('0x00')).resolves.toEqual(true) }) - it('calls getItemByKey and throws as expected', () => { + it('calls getItemByKey and throws as expected', async () => { + databaseMock.query = jest.fn((query) => { + expect(query).toEqual(`SELECT * FROM whitelist WHERE PublicKey='0x00'`) + return Promise.reject('NOPE') + }) + + await expect(whitelist.getItemByKey('0x00')).rejects.toEqual('NOPE') }) - it('calls addItem and returns the expected value', () => { + it('calls addItem and returns the expected value', async () => { + databaseMock.query = jest.fn((query) => { + expect(query).toEqual(`INSERT INTO whitelist VALUES (0x00, 0)`) + + return Promise.resolve(true) + }) + await expect(whitelist.addItem('0x00', 0)).resolves.toEqual(true) }) - it('calls addItem and throws as expected', () => { + it('calls addItem and throws as expected', async () => { + databaseMock.query = jest.fn((query) => { + expect(query).toEqual(`INSERT INTO whitelist VALUES (0x00, 0)`) + return Promise.reject('NOPE') + }) + + await expect(whitelist.addItem('0x00', 0)).rejects.toEqual('NOPE') }) - it('calls deleteItem and returns true', () => { + it('calls deleteItem and returns true', async () => { + databaseMock.query = jest.fn((query) => { + expect(query).toEqual(`DELETE FROM whitelist WHERE PublicKey='0x00'`) + + return Promise.resolve([0]) + }) + await expect(whitelist.deleteItem('0x00')).resolves.toEqual(true) }) - it('calls deleteItem and returns false', () => { + it('calls deleteItem and returns false', async () => { + databaseMock.query = jest.fn((query) => { + expect(query).toEqual(`DELETE FROM whitelist WHERE PublicKey='0x00'`) + + return Promise.resolve([]) + }) + await expect(whitelist.deleteItem('0x00')).resolves.toEqual(false) }) - it('calls deleteItem and throws as expected', () => { - + it('calls deleteItem and throws as expected', async () => { + databaseMock.query = jest.fn((query) => { + expect(query).toEqual(`DELETE FROM whitelist WHERE PublicKey='0x00'`) + + return Promise.reject('NOPE') + }) + + await expect(whitelist.deleteItem('0x00')).rejects.toEqual('NOPE') }) -}) \ No newline at end of file +}) From da15584558e06dda25ed96dc25ed133eef492ef8 Mon Sep 17 00:00:00 2001 From: nivida Date: Mon, 7 Dec 2020 14:40:11 +0100 Subject: [PATCH 061/107] ProviderTest template added --- .../test/src/provider/ProviderTest.ts | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 packages/govern-tx/test/src/provider/ProviderTest.ts diff --git a/packages/govern-tx/test/src/provider/ProviderTest.ts b/packages/govern-tx/test/src/provider/ProviderTest.ts new file mode 100644 index 000000000..8d80c07af --- /dev/null +++ b/packages/govern-tx/test/src/provider/ProviderTest.ts @@ -0,0 +1,66 @@ +import Wallet from '../../../src/wallet/Wallet' +import Database from '../../../src/db/Database' +import Provider from '../../../src/provider/Provider' +import { BaseProvider } from '@ethersproject/providers'; + +// Mocks +jest.mock('@ethersproject/providers') +jest.mock('../../../src/wallet/Wallet') +jest.mock('../../../src/db/Database')s +jest.mock('../../../lib/transactions/ContractFunction') + +/** + * Provider test + */ +describe('ProviderTest', () => { + let provider: Provider, + walletMock: Wallet, + databaseMock: Database, + baseProviderMock: BaseProvider; + + beforeEach(() => { + new Database({ + host: 'host', + port: 100, + database: 'database', + user: 'user', + password: 'password' + }) + databaseMock = (Database as jest.MockedClass).mock.instances[0] + + new Wallet(databaseMock) + walletMock = (Wallet as jest.MockedClass).mock.instances[0] + + provider = new Provider( + { + url: 'url', + blockConfirmations: 0, + publicKey: 'publicKey', + contracts: {GovernQueue: '0x00'} + }, + walletMock + ) + + baseProviderMock = (BaseProvider as jest.MockedClass).mock.instances[0] + }) + + it('calls sendTransaction and returns with the expected value', async () => { + + }) + + it('calls sendTransaction and the wallet throws on signing', async () => { + + }) + + it('calls sendTransaction and returns an prepopulating of the TX options throws', async () => { + + }) + + it('calls sendTransaction and the BaseProvider throws on executing the request', async () => { + + }) + + it('calls sendTransaction and the BaseProvider throws while waiting until it is mined', async () => { + + }) +}) From 8ac1d5826d44dd4ff3ad4f36a847be58b9bc9935 Mon Sep 17 00:00:00 2001 From: nivida Date: Wed, 9 Dec 2020 10:46:03 +0100 Subject: [PATCH 062/107] existing tests improved and Provider test case template added --- .../test/src/auth/AuthenticatorTest.ts | 49 +++------- packages/govern-tx/test/src/db/AdminTest.ts | 80 ++++++--------- .../govern-tx/test/src/db/WhitelistTest.ts | 98 +++++++------------ .../test/src/provider/ProviderTest.ts | 62 ++++++++++-- 4 files changed, 136 insertions(+), 153 deletions(-) diff --git a/packages/govern-tx/test/src/auth/AuthenticatorTest.ts b/packages/govern-tx/test/src/auth/AuthenticatorTest.ts index 1ac0414de..29998c92d 100644 --- a/packages/govern-tx/test/src/auth/AuthenticatorTest.ts +++ b/packages/govern-tx/test/src/auth/AuthenticatorTest.ts @@ -34,11 +34,8 @@ describe('AuthenticatorTest', () => { const NO_ALLOWED = new Unauthorized('Not allowed action!'); beforeEach(() => { - const verifyMock = verifyMessage as jest.MockedFunction - const arrayifyMock = arrayify as jest.MockedFunction - - verifyMock.mockReturnValue('0x00') - arrayifyMock.mockReturnValue(new Uint8Array(0x00)) + (arrayify as jest.MockedFunction).mockReturnValue(new Uint8Array(0x00)); + (verifyMessage as jest.MockedFunction).mockReturnValue('0x00') databaseMock = new Database({ user: 'user', @@ -58,11 +55,7 @@ describe('AuthenticatorTest', () => { }) it('calls authenticate as normal user and grants access to the transaction actions', async () => { - whitelistMock.keyExists = jest.fn((publicKey) => { - expect(publicKey).toEqual('0x00') - - return Promise.resolve(true) - }) + (whitelistMock.keyExists as jest.MockedFunction).mockReturnValueOnce(Promise.resolve(true)) await authenticator.authenticate(request as FastifyRequest) @@ -70,16 +63,12 @@ describe('AuthenticatorTest', () => { expect(arrayify).toHaveBeenNthCalledWith(1, request.body.message) - expect(whitelistMock.keyExists).toHaveBeenCalled() + expect(whitelistMock.keyExists).toHaveBeenNthCalledWith(1, '0x00') }) it('calls authenticate as normal user and restricts access to the whitelist', async () => { - adminMock.isAdmin = jest.fn((publicKey) => { - expect(publicKey).toEqual('0x00') + (adminMock.isAdmin as jest.MockedFunction).mockReturnValueOnce(Promise.resolve(false)) - return Promise.resolve(false) - }) - request.routerPath = '/whitelist' await expect(authenticator.authenticate(request as FastifyRequest)).rejects.toThrowError(NO_ALLOWED) @@ -88,21 +77,13 @@ describe('AuthenticatorTest', () => { expect(arrayify).toHaveBeenNthCalledWith(1, request.body.message) - expect(adminMock.isAdmin).toHaveBeenCalled() + expect(adminMock.isAdmin).toHaveBeenNthCalledWith(1, '0x00') }) it('calls authencticate as admin user and grants access to the transaction actions', async () => { - adminMock.isAdmin = jest.fn((publicKey) => { - expect(publicKey).toEqual('0x00') - - return Promise.resolve(true) - }) + (adminMock.isAdmin as jest.MockedFunction).mockReturnValueOnce(Promise.resolve(true)); - whitelistMock.keyExists = jest.fn((publicKey) => { - expect(publicKey).toEqual('0x00') - - return Promise.resolve(false) - }) + (whitelistMock.keyExists as jest.MockedFunction).mockReturnValueOnce(Promise.resolve(false)) request.routerPath = '/execute' @@ -112,18 +93,14 @@ describe('AuthenticatorTest', () => { expect(arrayify).toHaveBeenNthCalledWith(1, request.body.message) - expect(adminMock.isAdmin).toHaveBeenCalled() + expect(adminMock.isAdmin).toHaveBeenNthCalledWith(1, '0x00') - expect(whitelistMock.keyExists).toHaveBeenCalled() + expect(whitelistMock.keyExists).toHaveBeenNthCalledWith(1, '0x00') }) it('calls authenticate as admin user and grants access to the whitelist actions', async () => { - adminMock.isAdmin = jest.fn((publicKey) => { - expect(publicKey).toEqual('0x00') - - return Promise.resolve(true) - }) - + (adminMock.isAdmin as jest.MockedFunction).mockReturnValueOnce(Promise.resolve(true)); + request.routerPath = '/whitelist' await authenticator.authenticate(request as FastifyRequest) @@ -132,6 +109,6 @@ describe('AuthenticatorTest', () => { expect(arrayify).toHaveBeenNthCalledWith(1, request.body.message) - expect(adminMock.isAdmin).toHaveBeenCalled() + expect(adminMock.isAdmin).toHaveBeenNthCalledWith(1, '0x00') }) }) \ No newline at end of file diff --git a/packages/govern-tx/test/src/db/AdminTest.ts b/packages/govern-tx/test/src/db/AdminTest.ts index fd14b7eb5..a3f6bf45a 100644 --- a/packages/govern-tx/test/src/db/AdminTest.ts +++ b/packages/govern-tx/test/src/db/AdminTest.ts @@ -25,102 +25,82 @@ describe('AdminTest', () => { }) it('calls isAdmin and returns true', async () => { - databaseMock.query = jest.fn((query) => { - expect(query).toEqual(`SELECT * FROM admins WHERE PublicKey='0x00'`) - - return Promise.resolve([0]) - }) + (databaseMock.query as jest.MockedFunction).mockReturnValueOnce(Promise.resolve([0])) await expect(admin.isAdmin('0x00')).resolves.toEqual(true) + + expect(databaseMock.query).toHaveBeenNthCalledWith(1, `SELECT * FROM admins WHERE PublicKey='0x00'`) }) it('calls isAdmin and returns false', async () => { - databaseMock.query = jest.fn((query) => { - expect(query).toEqual(`SELECT * FROM admins WHERE PublicKey='0x00'`) - - return Promise.resolve([]) - }) + (databaseMock.query as jest.MockedFunction).mockReturnValueOnce(Promise.resolve([])) await expect(admin.isAdmin('0x00')).resolves.toEqual(false) + + expect(databaseMock.query).toHaveBeenNthCalledWith(1, `SELECT * FROM admins WHERE PublicKey='0x00'`) }) it('calls isAdmin and throws as expected', async () => { - databaseMock.query = jest.fn((query) => { - expect(query).toEqual(`SELECT * FROM admins WHERE PublicKey='0x00'`) - - return Promise.reject('NOPE') - }) + (databaseMock.query as jest.MockedFunction).mockReturnValueOnce(Promise.reject('NOPE')) await expect(admin.isAdmin('0x00')).rejects.toEqual('NOPE') + + expect(databaseMock.query).toHaveBeenNthCalledWith(1, `SELECT * FROM admins WHERE PublicKey='0x00'`) }) it('calls addAdmin and returns the expected value', async () => { - databaseMock.query = jest.fn((query) => { - expect(query).toEqual(`INSERT INTO admins VALUES (0x00)`) - - return Promise.resolve(true) - }) + (databaseMock.query as jest.MockedFunction).mockReturnValueOnce(Promise.resolve(true)) await expect(admin.addAdmin('0x00')).resolves.toEqual(true) + + expect(databaseMock.query).toHaveBeenNthCalledWith(1, `INSERT INTO admins VALUES (0x00)`) }) it('calls addAdmin and throws as expected', async () => { - databaseMock.query = jest.fn((query) => { - expect(query).toEqual(`INSERT INTO admins VALUES (0x00)`) - - return Promise.reject('NOPE') - }) + (databaseMock.query as jest.MockedFunction).mockReturnValueOnce(Promise.reject('NOPE')) await expect(admin.addAdmin('0x00')).rejects.toEqual('NOPE') + + expect(databaseMock.query).toHaveBeenNthCalledWith(1, `INSERT INTO admins VALUES (0x00)`) }) it('calls deleteAdmin and returns true', async () => { - databaseMock.query = jest.fn((query) => { - expect(query).toEqual(`DELETE FROM admins WHERE PublicKey='0x00'`) - - return Promise.resolve([0]) - }) + (databaseMock.query as jest.MockedFunction).mockReturnValueOnce(Promise.resolve([0])) await expect(admin.deleteAdmin('0x00')).resolves.toEqual(true) + + expect(databaseMock.query).toHaveBeenNthCalledWith(1, `DELETE FROM admins WHERE PublicKey='0x00'`) }) it('calls deleteAdmin and returns false', async () => { - databaseMock.query = jest.fn((query) => { - expect(query).toEqual(`DELETE FROM admins WHERE PublicKey='0x00'`) - - return Promise.resolve([]) - }) + (databaseMock.query as jest.MockedFunction).mockReturnValueOnce(Promise.resolve([])) await expect(admin.deleteAdmin('0x00')).resolves.toEqual(false) + + expect(databaseMock.query).toHaveBeenNthCalledWith(1, `DELETE FROM admins WHERE PublicKey='0x00'`) }) it('calls deleteAdmin and throws as expected', async () => { - databaseMock.query = jest.fn((query) => { - expect(query).toEqual(`DELETE FROM admins WHERE PublicKey='0x00'`) - - return Promise.reject('NOPE') - }) + (databaseMock.query as jest.MockedFunction).mockReturnValueOnce(Promise.reject('NOPE')) await expect(admin.deleteAdmin('0x00')).rejects.toEqual('NOPE') + + expect(databaseMock.query).toHaveBeenNthCalledWith(1, `DELETE FROM admins WHERE PublicKey='0x00'`) }) it('calls getAdmins and returns the expected value', async () => { - databaseMock.query = jest.fn((query) => { - expect(query).toEqual('SELECT * from admins') - - return Promise.resolve(true) - }) + (databaseMock.query as jest.MockedFunction).mockReturnValueOnce(Promise.resolve(true)) await expect(admin.getAdmins()).resolves.toEqual(true) + + expect(databaseMock.query).toHaveBeenNthCalledWith(1, 'SELECT * from admins') }) it('calls getAdmins and throws as expected', async () => { - databaseMock.query = jest.fn((query) => { - expect(query).toEqual('SELECT * from admins') - - return Promise.reject('NOPE') - }) + (databaseMock.query as jest.MockedFunction).mockReturnValueOnce(Promise.reject('NOPE')) await expect(admin.getAdmins()).rejects.toEqual('NOPE') + + expect(databaseMock.query).toHaveBeenNthCalledWith(1, 'SELECT * from admins') }) }) \ No newline at end of file diff --git a/packages/govern-tx/test/src/db/WhitelistTest.ts b/packages/govern-tx/test/src/db/WhitelistTest.ts index 726b72eb9..688e937f1 100644 --- a/packages/govern-tx/test/src/db/WhitelistTest.ts +++ b/packages/govern-tx/test/src/db/WhitelistTest.ts @@ -25,122 +25,98 @@ describe('WhitelistTest', () => { }) it('calls getList and returns the expected value', async () => { - databaseMock.query = jest.fn((query) => { - expect(query).toEqual('SELECT * FROM whitelist') - - return Promise.resolve(true) - }) + (databaseMock.query as jest.MockedFunction).mockReturnValueOnce(Promise.resolve(true)) await expect(whitelist.getList()).resolves.toEqual(true) + + expect(databaseMock.query).toHaveBeenNthCalledWith(1, 'SELECT * FROM whitelist') }) it('calls getList and throws as expected', async () => { - databaseMock.query = jest.fn((query) => { - expect(query).toEqual('SELECT * FROM whitelist') - - return Promise.reject('NOPE') - }) + (databaseMock.query as jest.MockedFunction).mockReturnValueOnce(Promise.reject('NOPE')) await expect(whitelist.getList()).rejects.toEqual('NOPE') + + expect(databaseMock.query).toHaveBeenNthCalledWith(1, 'SELECT * FROM whitelist') }) it('calls keyExists and returns true', async () => { - databaseMock.query = jest.fn((query) => { - expect(query).toEqual(`SELECT * FROM whitelist WHERE PublicKey='0x00'`) - - return Promise.resolve([0]) - }) + (databaseMock.query as jest.MockedFunction).mockReturnValueOnce(Promise.resolve([0])) await expect(whitelist.keyExists('0x00')).resolves.toEqual(true) + + expect(databaseMock.query).toHaveBeenNthCalledWith(1, `SELECT * FROM whitelist WHERE PublicKey='0x00'`) }) it('calls keyExists and returns false', async () => { - databaseMock.query = jest.fn((query) => { - expect(query).toEqual(`SELECT * FROM whitelist WHERE PublicKey='0x00'`) - - return Promise.resolve([]) - }) + (databaseMock.query as jest.MockedFunction).mockReturnValueOnce(Promise.resolve([])) await expect(whitelist.keyExists('0x00')).resolves.toEqual(false) + + expect(databaseMock.query).toHaveBeenNthCalledWith(1, `SELECT * FROM whitelist WHERE PublicKey='0x00'`) }) it('calls keyExists and throws as expected', async () => { - databaseMock.query = jest.fn((query) => { - expect(query).toEqual(`SELECT * FROM whitelist WHERE PublicKey='0x00'`) - - return Promise.reject('NOPE') - }) + (databaseMock.query as jest.MockedFunction).mockReturnValueOnce(Promise.reject('NOPE')) await expect(whitelist.keyExists('0x00')).rejects.toEqual('NOPE') + + expect(databaseMock.query).toHaveBeenNthCalledWith(1, `SELECT * FROM whitelist WHERE PublicKey='0x00'`) }) it('calls getItemByKey and returns the expected value', async () => { - databaseMock.query = jest.fn((query) => { - expect(query).toEqual(`SELECT * FROM whitelist WHERE PublicKey='0x00'`) - - return Promise.resolve(true) - }) + (databaseMock.query as jest.MockedFunction).mockReturnValueOnce(Promise.resolve(true)) await expect(whitelist.getItemByKey('0x00')).resolves.toEqual(true) + + expect(databaseMock.query).toHaveBeenNthCalledWith(1, `SELECT * FROM whitelist WHERE PublicKey='0x00'`) }) it('calls getItemByKey and throws as expected', async () => { - databaseMock.query = jest.fn((query) => { - expect(query).toEqual(`SELECT * FROM whitelist WHERE PublicKey='0x00'`) - - return Promise.reject('NOPE') - }) + (databaseMock.query as jest.MockedFunction).mockReturnValueOnce(Promise.reject('NOPE')) await expect(whitelist.getItemByKey('0x00')).rejects.toEqual('NOPE') + + expect(databaseMock.query).toHaveBeenNthCalledWith(1, `SELECT * FROM whitelist WHERE PublicKey='0x00'`) }) it('calls addItem and returns the expected value', async () => { - databaseMock.query = jest.fn((query) => { - expect(query).toEqual(`INSERT INTO whitelist VALUES (0x00, 0)`) - - return Promise.resolve(true) - }) + (databaseMock.query as jest.MockedFunction).mockReturnValueOnce(Promise.resolve(true)) await expect(whitelist.addItem('0x00', 0)).resolves.toEqual(true) + + expect(databaseMock.query).toHaveBeenNthCalledWith(1, `INSERT INTO whitelist VALUES (0x00, 0)`) }) it('calls addItem and throws as expected', async () => { - databaseMock.query = jest.fn((query) => { - expect(query).toEqual(`INSERT INTO whitelist VALUES (0x00, 0)`) - - return Promise.reject('NOPE') - }) + (databaseMock.query as jest.MockedFunction).mockReturnValueOnce(Promise.reject('NOPE')) await expect(whitelist.addItem('0x00', 0)).rejects.toEqual('NOPE') + + expect(databaseMock.query).toHaveBeenNthCalledWith(1, `INSERT INTO whitelist VALUES (0x00, 0)`) }) it('calls deleteItem and returns true', async () => { - databaseMock.query = jest.fn((query) => { - expect(query).toEqual(`DELETE FROM whitelist WHERE PublicKey='0x00'`) - - return Promise.resolve([0]) - }) + (databaseMock.query as jest.MockedFunction).mockReturnValueOnce(Promise.resolve([0])) await expect(whitelist.deleteItem('0x00')).resolves.toEqual(true) + + expect(databaseMock.query).toHaveBeenNthCalledWith(1, `DELETE FROM whitelist WHERE PublicKey='0x00'`) }) it('calls deleteItem and returns false', async () => { - databaseMock.query = jest.fn((query) => { - expect(query).toEqual(`DELETE FROM whitelist WHERE PublicKey='0x00'`) - - return Promise.resolve([]) - }) - + (databaseMock.query as jest.MockedFunction).mockReturnValueOnce(Promise.resolve([])) + await expect(whitelist.deleteItem('0x00')).resolves.toEqual(false) + + expect(databaseMock.query).toHaveBeenNthCalledWith(1, `DELETE FROM whitelist WHERE PublicKey='0x00'`) }) it('calls deleteItem and throws as expected', async () => { - databaseMock.query = jest.fn((query) => { - expect(query).toEqual(`DELETE FROM whitelist WHERE PublicKey='0x00'`) - - return Promise.reject('NOPE') - }) + (databaseMock.query as jest.MockedFunction).mockReturnValueOnce(Promise.reject('NOPE')) await expect(whitelist.deleteItem('0x00')).rejects.toEqual('NOPE') + + expect(databaseMock.query).toHaveBeenNthCalledWith(1, `DELETE FROM whitelist WHERE PublicKey='0x00'`) }) }) diff --git a/packages/govern-tx/test/src/provider/ProviderTest.ts b/packages/govern-tx/test/src/provider/ProviderTest.ts index 8d80c07af..21c1bb90c 100644 --- a/packages/govern-tx/test/src/provider/ProviderTest.ts +++ b/packages/govern-tx/test/src/provider/ProviderTest.ts @@ -1,22 +1,40 @@ +import { BaseProvider, TransactionResponse } from '@ethersproject/providers'; +import { BigNumber } from "@ethersproject/bignumber"; +import { JsonFragment } from '@ethersproject/abi'; +import ContractFunction from '../../../lib/transactions/ContractFunction'; import Wallet from '../../../src/wallet/Wallet' import Database from '../../../src/db/Database' import Provider from '../../../src/provider/Provider' -import { BaseProvider } from '@ethersproject/providers'; // Mocks jest.mock('@ethersproject/providers') jest.mock('../../../src/wallet/Wallet') -jest.mock('../../../src/db/Database')s +jest.mock('../../../src/db/Database') jest.mock('../../../lib/transactions/ContractFunction') +// Mock TX response class +class TXResponse implements TransactionResponse { + public hash = '0x00' + public confirmations = 1 + public from = '0x00' + public nonce = 0 + public gasLimit = BigNumber.from(0) + public gasPrice = BigNumber.from(0) + public data = '0x00' + public value = BigNumber.from(0) + public chainId = 0 + wait = jest.fn() +} + /** * Provider test */ -describe('ProviderTest', () => { +describe.skip('ProviderTest', () => { let provider: Provider, walletMock: Wallet, databaseMock: Database, - baseProviderMock: BaseProvider; + baseProviderMock: BaseProvider, + contractFunctionMock: ContractFunction; beforeEach(() => { new Database({ @@ -31,6 +49,9 @@ describe('ProviderTest', () => { new Wallet(databaseMock) walletMock = (Wallet as jest.MockedClass).mock.instances[0] + new ContractFunction({} as JsonFragment, 'request') + contractFunctionMock = (ContractFunction as jest.MockedClass).mock.instances[0] + provider = new Provider( { url: 'url', @@ -45,14 +66,43 @@ describe('ProviderTest', () => { }) it('calls sendTransaction and returns with the expected value', async () => { + const txResponse = new TXResponse() + + txResponse.wait.mockReturnValueOnce(Promise.resolve('RECEIPT')) + + (walletMock.sign as jest.MockedFunction).mockReturnValueOnce('0x00') + + (contractFunctionMock.encode as jest.MockedFunction).mockReturnValue('0x00') + + (baseProviderMock.sendTransaction as jest.MockedFunction).mockReturnValue(Promise.resolve(txResponse)) + + (baseProviderMock.estimateGas as jest.MockedFunction).mockReturnValue(Promise.resolve(BigNumber.from(0))) + + expect(baseProviderMock.sendTransaction).toHaveBeenNthCalledWith(1, '0x00') + + expect(txResponse.wait).toHaveBeenNthCalledWith(1, 0) + expect(walletMock.sign).toHaveBeenNthCalledWith( + 1, + { + to: '0x00', + data: '0x00', + gasLimit: BigNumber.from(0) + } + ) + + expect(contractFunctionMock.encode).toHaveBeenCalledTimes(1) }) it('calls sendTransaction and the wallet throws on signing', async () => { }) - it('calls sendTransaction and returns an prepopulating of the TX options throws', async () => { + it('calls sendTransaction and estimation of the gas throws', async () => { + + }) + + it('calls sendTransaction and encoding of the contract function throws', async () => { }) @@ -61,6 +111,6 @@ describe('ProviderTest', () => { }) it('calls sendTransaction and the BaseProvider throws while waiting until it is mined', async () => { - + }) }) From b7001714ae07b32cd8c2438f23bf2b6771ba3821 Mon Sep 17 00:00:00 2001 From: nivida Date: Wed, 9 Dec 2020 11:01:19 +0100 Subject: [PATCH 063/107] ProviderTest implemented --- .../test/src/provider/ProviderTest.ts | 103 ++++++++++++++++-- 1 file changed, 92 insertions(+), 11 deletions(-) diff --git a/packages/govern-tx/test/src/provider/ProviderTest.ts b/packages/govern-tx/test/src/provider/ProviderTest.ts index 21c1bb90c..f86c82992 100644 --- a/packages/govern-tx/test/src/provider/ProviderTest.ts +++ b/packages/govern-tx/test/src/provider/ProviderTest.ts @@ -29,7 +29,7 @@ class TXResponse implements TransactionResponse { /** * Provider test */ -describe.skip('ProviderTest', () => { +describe('ProviderTest', () => { let provider: Provider, walletMock: Wallet, databaseMock: Database, @@ -68,19 +68,21 @@ describe.skip('ProviderTest', () => { it('calls sendTransaction and returns with the expected value', async () => { const txResponse = new TXResponse() - txResponse.wait.mockReturnValueOnce(Promise.resolve('RECEIPT')) + txResponse.wait.mockReturnValueOnce(Promise.resolve('RECEIPT')); - (walletMock.sign as jest.MockedFunction).mockReturnValueOnce('0x00') + (walletMock.sign as jest.MockedFunction).mockReturnValueOnce(Promise.resolve('0x00')); - (contractFunctionMock.encode as jest.MockedFunction).mockReturnValue('0x00') + (contractFunctionMock.encode as jest.MockedFunction).mockReturnValue('0x00'); - (baseProviderMock.sendTransaction as jest.MockedFunction).mockReturnValue(Promise.resolve(txResponse)) + (baseProviderMock.sendTransaction as jest.MockedFunction).mockReturnValue(Promise.resolve(txResponse)); - (baseProviderMock.estimateGas as jest.MockedFunction).mockReturnValue(Promise.resolve(BigNumber.from(0))) + (baseProviderMock.estimateGas as jest.MockedFunction).mockReturnValue(Promise.resolve(BigNumber.from(0))); - expect(baseProviderMock.sendTransaction).toHaveBeenNthCalledWith(1, '0x00') + await expect(provider.sendTransaction('GovernQueue', contractFunctionMock)).resolves.toEqual('RECEIPT'); - expect(txResponse.wait).toHaveBeenNthCalledWith(1, 0) + expect(baseProviderMock.sendTransaction).toHaveBeenNthCalledWith(1, '0x00'); + + expect(txResponse.wait).toHaveBeenNthCalledWith(1, 0); expect(walletMock.sign).toHaveBeenNthCalledWith( 1, @@ -88,29 +90,108 @@ describe.skip('ProviderTest', () => { to: '0x00', data: '0x00', gasLimit: BigNumber.from(0) - } - ) + }, + 'publicKey' + ); - expect(contractFunctionMock.encode).toHaveBeenCalledTimes(1) + expect(contractFunctionMock.encode).toHaveBeenCalledTimes(1); }) it('calls sendTransaction and the wallet throws on signing', async () => { + (walletMock.sign as jest.MockedFunction).mockReturnValueOnce(Promise.reject('NOPE')); + + (contractFunctionMock.encode as jest.MockedFunction).mockReturnValue('0x00'); + + (baseProviderMock.estimateGas as jest.MockedFunction).mockReturnValue(Promise.resolve(BigNumber.from(0))); + + await expect(provider.sendTransaction('GovernQueue', contractFunctionMock)).rejects.toEqual('NOPE'); + + expect(walletMock.sign).toHaveBeenNthCalledWith( + 1, + { + to: '0x00', + data: '0x00', + gasLimit: BigNumber.from(0) + }, + 'publicKey' + ); + expect(contractFunctionMock.encode).toHaveBeenCalledTimes(1); }) it('calls sendTransaction and estimation of the gas throws', async () => { + (contractFunctionMock.encode as jest.MockedFunction).mockReturnValue('0x00'); + + (baseProviderMock.estimateGas as jest.MockedFunction).mockReturnValue(Promise.reject('NOPE')); + await expect(provider.sendTransaction('GovernQueue', contractFunctionMock)).rejects.toEqual('NOPE'); + + expect(contractFunctionMock.encode).toHaveBeenCalledTimes(1); }) it('calls sendTransaction and encoding of the contract function throws', async () => { + (contractFunctionMock.encode as jest.MockedFunction).mockImplementation(() => {throw 'NOPE'}) + + await expect(provider.sendTransaction('GovernQueue', contractFunctionMock)).rejects.toEqual('NOPE'); + expect(contractFunctionMock.encode).toHaveBeenCalledTimes(1); }) it('calls sendTransaction and the BaseProvider throws on executing the request', async () => { + (walletMock.sign as jest.MockedFunction).mockReturnValueOnce(Promise.resolve('0x00')); + + (contractFunctionMock.encode as jest.MockedFunction).mockReturnValue('0x00'); + (baseProviderMock.sendTransaction as jest.MockedFunction).mockReturnValue(Promise.reject('NOPE')); + + (baseProviderMock.estimateGas as jest.MockedFunction).mockReturnValue(Promise.resolve(BigNumber.from(0))); + + await expect(provider.sendTransaction('GovernQueue', contractFunctionMock)).rejects.toEqual('NOPE'); + + expect(baseProviderMock.sendTransaction).toHaveBeenNthCalledWith(1, '0x00'); + + expect(walletMock.sign).toHaveBeenNthCalledWith( + 1, + { + to: '0x00', + data: '0x00', + gasLimit: BigNumber.from(0) + }, + 'publicKey' + ); + + expect(contractFunctionMock.encode).toHaveBeenCalledTimes(1); }) it('calls sendTransaction and the BaseProvider throws while waiting until it is mined', async () => { + const txResponse = new TXResponse() + + txResponse.wait.mockReturnValueOnce(Promise.reject('NOPE')); + + (walletMock.sign as jest.MockedFunction).mockReturnValueOnce(Promise.resolve('0x00')); + + (contractFunctionMock.encode as jest.MockedFunction).mockReturnValue('0x00'); + + (baseProviderMock.sendTransaction as jest.MockedFunction).mockReturnValue(Promise.resolve(txResponse)); + + (baseProviderMock.estimateGas as jest.MockedFunction).mockReturnValue(Promise.resolve(BigNumber.from(0))); + + await expect(provider.sendTransaction('GovernQueue', contractFunctionMock)).rejects.toEqual('NOPE'); + + expect(baseProviderMock.sendTransaction).toHaveBeenNthCalledWith(1, '0x00'); + + expect(txResponse.wait).toHaveBeenNthCalledWith(1, 0); + + expect(walletMock.sign).toHaveBeenNthCalledWith( + 1, + { + to: '0x00', + data: '0x00', + gasLimit: BigNumber.from(0) + }, + 'publicKey' + ); + expect(contractFunctionMock.encode).toHaveBeenCalledTimes(1); }) }) From abfef9351ea8e81bd4f4796d21f0d5506082efb3 Mon Sep 17 00:00:00 2001 From: nivida Date: Wed, 9 Dec 2020 13:01:21 +0100 Subject: [PATCH 064/107] AuthenticatorTest and ProviderTest improved. Transaction tests added --- .../test/src/auth/AuthenticatorTest.ts | 15 ++------- .../test/src/provider/ProviderTest.ts | 13 +------- .../challenge/ChallengeTransactionTest.ts | 33 +++++++++++++++++++ .../execute/ExecuteTransactionTest.ts | 33 +++++++++++++++++++ .../schedule/ScheduleTransactionTest.ts | 33 +++++++++++++++++++ 5 files changed, 102 insertions(+), 25 deletions(-) create mode 100644 packages/govern-tx/test/src/transactions/challenge/ChallengeTransactionTest.ts create mode 100644 packages/govern-tx/test/src/transactions/execute/ExecuteTransactionTest.ts create mode 100644 packages/govern-tx/test/src/transactions/schedule/ScheduleTransactionTest.ts diff --git a/packages/govern-tx/test/src/auth/AuthenticatorTest.ts b/packages/govern-tx/test/src/auth/AuthenticatorTest.ts index 29998c92d..aeb44888c 100644 --- a/packages/govern-tx/test/src/auth/AuthenticatorTest.ts +++ b/packages/govern-tx/test/src/auth/AuthenticatorTest.ts @@ -10,7 +10,6 @@ import Database from '../../../src/db/Database'; // Mocks jest.mock('../../../src/db/Admin') jest.mock('../../../src/db/Whitelist') -jest.mock('../../../src/db/Database') jest.mock('@ethersproject/wallet') jest.mock('@ethersproject/bytes') @@ -20,7 +19,6 @@ jest.mock('@ethersproject/bytes') describe('AuthenticatorTest', () => { let authenticator: Authenticator, whitelistMock: Whitelist, - databaseMock: Database, adminMock: Admin, request = { routerPath: '/execute', @@ -30,25 +28,16 @@ describe('AuthenticatorTest', () => { } }; - const NO_ALLOWED = new Unauthorized('Not allowed action!'); beforeEach(() => { (arrayify as jest.MockedFunction).mockReturnValue(new Uint8Array(0x00)); (verifyMessage as jest.MockedFunction).mockReturnValue('0x00') - databaseMock = new Database({ - user: 'user', - host: 'host', - password: 'password', - database: 'databaseName', - port: 1000 - }) - - new Whitelist(databaseMock) + new Whitelist({} as Database) whitelistMock = (Whitelist as jest.MockedClass).mock.instances[0] - new Admin(databaseMock) + new Admin({} as Database) adminMock = (Admin as jest.MockedClass).mock.instances[0] authenticator = new Authenticator(whitelistMock, adminMock) diff --git a/packages/govern-tx/test/src/provider/ProviderTest.ts b/packages/govern-tx/test/src/provider/ProviderTest.ts index f86c82992..466ac3ed2 100644 --- a/packages/govern-tx/test/src/provider/ProviderTest.ts +++ b/packages/govern-tx/test/src/provider/ProviderTest.ts @@ -9,7 +9,6 @@ import Provider from '../../../src/provider/Provider' // Mocks jest.mock('@ethersproject/providers') jest.mock('../../../src/wallet/Wallet') -jest.mock('../../../src/db/Database') jest.mock('../../../lib/transactions/ContractFunction') // Mock TX response class @@ -32,21 +31,11 @@ class TXResponse implements TransactionResponse { describe('ProviderTest', () => { let provider: Provider, walletMock: Wallet, - databaseMock: Database, baseProviderMock: BaseProvider, contractFunctionMock: ContractFunction; beforeEach(() => { - new Database({ - host: 'host', - port: 100, - database: 'database', - user: 'user', - password: 'password' - }) - databaseMock = (Database as jest.MockedClass).mock.instances[0] - - new Wallet(databaseMock) + new Wallet({} as Database) walletMock = (Wallet as jest.MockedClass).mock.instances[0] new ContractFunction({} as JsonFragment, 'request') diff --git a/packages/govern-tx/test/src/transactions/challenge/ChallengeTransactionTest.ts b/packages/govern-tx/test/src/transactions/challenge/ChallengeTransactionTest.ts new file mode 100644 index 000000000..943b7f2b7 --- /dev/null +++ b/packages/govern-tx/test/src/transactions/challenge/ChallengeTransactionTest.ts @@ -0,0 +1,33 @@ +import { EthereumOptions } from '../../../../src/config/Configuration' +import { Request } from '../../../../lib/AbstractAction' +import Provider from '../../../../src/provider/Provider' +import * as challengeABI from '../../../../src/transactions/challenge/challenge.json' +import ChallengeTransaction from '../../../../src/transactions/challenge/ChallengeTransaction' + +// Mocks +jest.mock('../../../../lib/transactions/AbstractTransaction') + +/** + * ChallengeTransaction test + */ +describe('ChallengeTransaction Test', () => { + let challengeTransaction: ChallengeTransaction + + beforeEach(() => { + challengeTransaction = new ChallengeTransaction( + {} as EthereumOptions, + {} as Provider, + {} as Request + ) + }) + + it('has the correct contract defined', () => { + //@ts-ignore + expect(challengeTransaction.contract).toEqual('GovernQueue') + }) + + it('has the correct function abi defined', () => { + //@ts-ignore + expect(challengeTransaction.functionABI).toEqual(challengeABI) + }) +}) diff --git a/packages/govern-tx/test/src/transactions/execute/ExecuteTransactionTest.ts b/packages/govern-tx/test/src/transactions/execute/ExecuteTransactionTest.ts new file mode 100644 index 000000000..d1f3f8f83 --- /dev/null +++ b/packages/govern-tx/test/src/transactions/execute/ExecuteTransactionTest.ts @@ -0,0 +1,33 @@ +import { EthereumOptions } from '../../../../src/config/Configuration' +import { Request } from '../../../../lib/AbstractAction' +import Provider from '../../../../src/provider/Provider' +import * as executeABI from '../../../../src/transactions/execute/execute.json' +import ExecuteTransaction from '../../../../src/transactions/execute/ExecuteTransaction' + +// Mocks +jest.mock('../../../../lib/transactions/AbstractTransaction') + +/** + * ExecuteTransaction test + */ +describe('ExecuteTransaction Test', () => { + let executeTransaction: ExecuteTransaction + + beforeEach(() => { + executeTransaction = new ExecuteTransaction( + {} as EthereumOptions, + {} as Provider, + {} as Request + ) + }) + + it('has the correct contract defined', () => { + //@ts-ignore + expect(executeTransaction.contract).toEqual('GovernQueue') + }) + + it('has the correct function abi defined', () => { + //@ts-ignore + expect(executeTransaction.functionABI).toEqual(executeABI) + }) +}) diff --git a/packages/govern-tx/test/src/transactions/schedule/ScheduleTransactionTest.ts b/packages/govern-tx/test/src/transactions/schedule/ScheduleTransactionTest.ts new file mode 100644 index 000000000..91d5e5a5e --- /dev/null +++ b/packages/govern-tx/test/src/transactions/schedule/ScheduleTransactionTest.ts @@ -0,0 +1,33 @@ +import { EthereumOptions } from '../../../../src/config/Configuration' +import { Request } from '../../../../lib/AbstractAction' +import Provider from '../../../../src/provider/Provider' +import * as scheduleABI from '../../../../src/transactions/schedule/schedule.json' +import ScheduleTransaction from '../../../../src/transactions/schedule/ScheduleTransaction' + +// Mocks +jest.mock('../../../../lib/transactions/AbstractTransaction') + +/** + * ScheduleTransaction test + */ +describe('ScheduleTransaction Test', () => { + let scheduleTransaction: ScheduleTransaction + + beforeEach(() => { + scheduleTransaction = new ScheduleTransaction( + {} as EthereumOptions, + {} as Provider, + {} as Request + ) + }) + + it('has the correct contract defined', () => { + //@ts-ignore + expect(scheduleTransaction.contract).toEqual('GovernQueue') + }) + + it('has the correct function abi defined', () => { + //@ts-ignore + expect(scheduleTransaction.functionABI).toEqual(scheduleABI) + }) +}) From db1ee85f7e9b4eb098e3495493313569d1e30671 Mon Sep 17 00:00:00 2001 From: nivida Date: Thu, 10 Dec 2020 10:29:07 +0100 Subject: [PATCH 065/107] WalletTest implemented --- .../govern-tx/test/src/wallet/WalletTest.ts | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 packages/govern-tx/test/src/wallet/WalletTest.ts diff --git a/packages/govern-tx/test/src/wallet/WalletTest.ts b/packages/govern-tx/test/src/wallet/WalletTest.ts new file mode 100644 index 000000000..523ee25cf --- /dev/null +++ b/packages/govern-tx/test/src/wallet/WalletTest.ts @@ -0,0 +1,81 @@ +import {Wallet as EthersWallet} from '@ethersproject/wallet' +import { TransactionRequest } from '@ethersproject/providers'; +import { DatabaseOptions } from '../../../src/config/Configuration'; +import Database from '../../../src/db/Database'; +import Wallet from '../../../src/wallet/Wallet'; + +// Mocks +jest.mock('../../../src/db/Database') +jest.mock('@ethersproject/wallet') + +/** + * Wallet test + */ +describe('Wallet Test', () => { + let wallet: Wallet, + databaseMock: Database + + beforeEach(() => { + new Database({} as DatabaseOptions) + databaseMock = (Database as jest.MockedClass).mock.instances[0] + + EthersWallet.prototype.signTransaction = jest.fn() + + wallet = new Wallet(databaseMock) + }) + + it.skip('calls sign and returns the expected value', async () => { + (databaseMock.query as jest.MockedFunction).mockReturnValueOnce(Promise.resolve('0x01')); + + (EthersWallet.prototype.signTransaction as jest.MockedFunction).mockReturnValueOnce(Promise.resolve('0x02')) + + await expect(wallet.sign({} as TransactionRequest, '0x00')).resolves.toEqual('0x02') + + expect(databaseMock.query).toHaveBeenNthCalledWith(1, `SELECT PrivateKey FROM wallet WHERE PublicKey='0x00'`) + + expect(EthersWallet).toHaveBeenNthCalledWith(1, '0x01') + + expect(EthersWallet.prototype.signTransaction).toHaveBeenNthCalledWith(1, {from: '0x00'}) + }) + + it.skip('calls sign and throws as expected', async () => { + (databaseMock.query as jest.MockedFunction).mockReturnValueOnce(Promise.reject('NOPE')); + + await expect(wallet.sign({} as TransactionRequest, '0x00')).rejects.toEqual('NOPE') + + expect(databaseMock.query).toHaveBeenNthCalledWith(1, `SELECT PrivateKey FROM wallet WHERE PublicKey='0x00'`) + }) + + it('calls sign and uses the already loaded wallet', async () => { + (EthersWallet.prototype.signTransaction as jest.MockedFunction).mockReturnValue(Promise.resolve('0x02')); + + await expect(wallet.sign({} as TransactionRequest, '0x00')).resolves.toEqual('0x02') + await expect(wallet.sign({} as TransactionRequest, '0x00')).resolves.toEqual('0x02') + + expect(databaseMock.query).toHaveBeenCalledTimes(1) + + expect(EthersWallet.prototype.signTransaction).toHaveBeenNthCalledWith(1, {from: '0x00'}) + + expect(EthersWallet.prototype.signTransaction).toHaveBeenNthCalledWith(2, {from: '0x00'}) + }) + + it.skip('calls sign with another publicKey and releads to wallet for it', async () => { + (databaseMock.query as jest.MockedFunction).mockReturnValueOnce(Promise.resolve('0x01')); + (databaseMock.query as jest.MockedFunction).mockReturnValueOnce(Promise.resolve('0x03')); + + (EthersWallet.prototype.signTransaction as jest.MockedFunction).mockReturnValue(Promise.resolve('0x02')) + + await expect(wallet.sign({} as TransactionRequest, '0x00')).resolves.toEqual('0x02') + await expect(wallet.sign({} as TransactionRequest, '0x01')).resolves.toEqual('0x02') + + expect(databaseMock.query).toHaveBeenNthCalledWith(1, `SELECT PrivateKey FROM wallet WHERE PublicKey='0x00'`) + + expect(EthersWallet).toHaveBeenNthCalledWith(1, '0x01') + + expect(EthersWallet).toHaveBeenNthCalledWith(2, '0x03') + + expect(EthersWallet.prototype.signTransaction).toHaveBeenNthCalledWith(1, {from: '0x00'}) + + expect(EthersWallet.prototype.signTransaction).toHaveBeenNthCalledWith(2, {from: '0x01'}) + }) +}) From cd70b870fcc9c857d1a96eeec7868f7c643ec92d Mon Sep 17 00:00:00 2001 From: nivida Date: Thu, 10 Dec 2020 10:57:46 +0100 Subject: [PATCH 066/107] types updated in AddItemAction and AddItemActionTest implemented --- .../govern-tx/src/whitelist/AddItemAction.ts | 4 +- .../test/src/whitelist/AddItemActionTest.ts | 87 +++++++++++++++++++ 2 files changed, 89 insertions(+), 2 deletions(-) create mode 100644 packages/govern-tx/test/src/whitelist/AddItemActionTest.ts diff --git a/packages/govern-tx/src/whitelist/AddItemAction.ts b/packages/govern-tx/src/whitelist/AddItemAction.ts index 2671ab4d8..18f090426 100644 --- a/packages/govern-tx/src/whitelist/AddItemAction.ts +++ b/packages/govern-tx/src/whitelist/AddItemAction.ts @@ -37,8 +37,8 @@ export default class AddItemAction extends AbstractWhitelistAction { */ public execute(): Promise { return this.whitelist.addItem( - this.request?.message.publicKey, - this.request?.message.rateLimit + (this.request as WhitelistRequest).message.publicKey, + (this.request as WhitelistRequest).message.rateLimit ) } } diff --git a/packages/govern-tx/test/src/whitelist/AddItemActionTest.ts b/packages/govern-tx/test/src/whitelist/AddItemActionTest.ts new file mode 100644 index 000000000..0a8331567 --- /dev/null +++ b/packages/govern-tx/test/src/whitelist/AddItemActionTest.ts @@ -0,0 +1,87 @@ +import { isAddress } from '@ethersproject/address'; +import { WhitelistRequest } from '../../../lib/whitelist/AbstractWhitelistAction'; +import Database from '../../../src/db/Database'; +import Whitelist, { ListItem } from '../../../src/db/Whitelist'; +import AddItemAction from '../../../src/whitelist/AddItemAction'; + + +// Mocks +jest.mock('../../../src/db/Whitelist') +jest.mock('@ethersproject/address') + +/** + * AddItemAction test + */ +describe('AddItemAction Test', () => { + let addItemAction: AddItemAction, + whitelistMock: Whitelist + + const request: WhitelistRequest = { + message: { + publicKey: '0x00', + rateLimit: 1 + }, + signature: '' + } + + beforeEach(() => { + new Whitelist({} as Database) + whitelistMock = (Whitelist as jest.MockedClass).mock.instances[0] + }) + + it('calls validateRequest and returns the expected values', () => { + (isAddress as jest.MockedFunction).mockReturnValueOnce(true) + + addItemAction = new AddItemAction(whitelistMock, request) + + expect(isAddress).toHaveBeenNthCalledWith(1, '0x00') + }) + + it('calls validateRequest and throws because of a invalid ethereum address', () => { + (isAddress as jest.MockedFunction).mockReturnValueOnce(false) + + expect(() => { + addItemAction = new AddItemAction(whitelistMock, request) + }).toThrow('Invalid public key passed!') + + expect(isAddress).toHaveBeenNthCalledWith(1, '0x00') + }) + + it('calls validateRequest and throws because of a invalid rate limit', () => { + (isAddress as jest.MockedFunction).mockReturnValueOnce(true) + + request.message.rateLimit = 0; + + expect(() => { + addItemAction = new AddItemAction(whitelistMock, request) + }).toThrow('Invalid rate limit passed!') + + expect(isAddress).toHaveBeenNthCalledWith(1, '0x00') + + request.message.rateLimit = 1; + }) + + it('calls execute and returns the expected result', async () => { + (isAddress as jest.MockedFunction).mockReturnValueOnce(true); + + (whitelistMock.addItem as jest.MockedFunction).mockReturnValueOnce(Promise.resolve({} as ListItem)); + + addItemAction = new AddItemAction(whitelistMock, request) + + await expect(addItemAction.execute()).resolves.toEqual({}) + + expect(whitelistMock.addItem).toHaveBeenNthCalledWith(1, '0x00', 1) + }) + + it('calls execute and throws as expected', async () => { + (isAddress as jest.MockedFunction).mockReturnValueOnce(true); + + (whitelistMock.addItem as jest.MockedFunction).mockReturnValueOnce(Promise.reject('NOPE')); + + addItemAction = new AddItemAction(whitelistMock, request) + + await expect(addItemAction.execute()).rejects.toEqual('NOPE') + + expect(whitelistMock.addItem).toHaveBeenNthCalledWith(1, '0x00', 1) + }) +}) From 7dbe2654589fa43e14f39e36a02f39fd4122b14c Mon Sep 17 00:00:00 2001 From: nivida Date: Thu, 10 Dec 2020 11:03:02 +0100 Subject: [PATCH 067/107] code style improved and DeleteItemActionTest implemented --- .../lib/whitelist/AbstractWhitelistAction.ts | 2 +- .../src/whitelist/DeleteItemAction.ts | 2 +- .../test/src/whitelist/AddItemActionTest.ts | 1 - .../src/whitelist/DeleteItemActionTest.ts | 71 +++++++++++++++++++ 4 files changed, 73 insertions(+), 3 deletions(-) create mode 100644 packages/govern-tx/test/src/whitelist/DeleteItemActionTest.ts diff --git a/packages/govern-tx/lib/whitelist/AbstractWhitelistAction.ts b/packages/govern-tx/lib/whitelist/AbstractWhitelistAction.ts index 4f0b9776e..a99a017f0 100644 --- a/packages/govern-tx/lib/whitelist/AbstractWhitelistAction.ts +++ b/packages/govern-tx/lib/whitelist/AbstractWhitelistAction.ts @@ -5,7 +5,7 @@ import Whitelist, {ListItem} from '../../src/db/Whitelist' export interface WhitelistRequest extends Request { message: { publicKey: string, - rateLimit: number + rateLimit?: number } } diff --git a/packages/govern-tx/src/whitelist/DeleteItemAction.ts b/packages/govern-tx/src/whitelist/DeleteItemAction.ts index 9255ec7a8..f10c86463 100644 --- a/packages/govern-tx/src/whitelist/DeleteItemAction.ts +++ b/packages/govern-tx/src/whitelist/DeleteItemAction.ts @@ -32,6 +32,6 @@ export default class DeleteItemAction extends AbstractWhitelistAction { * @public */ public execute(): Promise { - return this.whitelist.deleteItem(this.request?.message.publicKey); + return this.whitelist.deleteItem((this.request as WhitelistRequest).message.publicKey); } } diff --git a/packages/govern-tx/test/src/whitelist/AddItemActionTest.ts b/packages/govern-tx/test/src/whitelist/AddItemActionTest.ts index 0a8331567..16886ec12 100644 --- a/packages/govern-tx/test/src/whitelist/AddItemActionTest.ts +++ b/packages/govern-tx/test/src/whitelist/AddItemActionTest.ts @@ -4,7 +4,6 @@ import Database from '../../../src/db/Database'; import Whitelist, { ListItem } from '../../../src/db/Whitelist'; import AddItemAction from '../../../src/whitelist/AddItemAction'; - // Mocks jest.mock('../../../src/db/Whitelist') jest.mock('@ethersproject/address') diff --git a/packages/govern-tx/test/src/whitelist/DeleteItemActionTest.ts b/packages/govern-tx/test/src/whitelist/DeleteItemActionTest.ts new file mode 100644 index 000000000..6aecb52c6 --- /dev/null +++ b/packages/govern-tx/test/src/whitelist/DeleteItemActionTest.ts @@ -0,0 +1,71 @@ +import { isAddress } from '@ethersproject/address'; +import { WhitelistRequest } from '../../../lib/whitelist/AbstractWhitelistAction'; +import Database from '../../../src/db/Database'; +import Whitelist, { ListItem } from '../../../src/db/Whitelist'; +import DeleteItemAction from '../../../src/whitelist/DeleteItemAction'; + +// Mocks +jest.mock('../../../src/db/Whitelist') +jest.mock('@ethersproject/address') + +/** + * DeleteItemAction test + */ +describe('DeleteItemAction Test', () => { + let deleteItemAction: DeleteItemAction, + whitelistMock: Whitelist + + const request: WhitelistRequest = { + message: { + publicKey: '0x00' + }, + signature: '' + } + + beforeEach(() => { + new Whitelist({} as Database) + whitelistMock = (Whitelist as jest.MockedClass).mock.instances[0] + }) + + it('calls validateRequest and returns the expected values', () => { + (isAddress as jest.MockedFunction).mockReturnValueOnce(true) + + deleteItemAction = new DeleteItemAction(whitelistMock, request) + + expect(isAddress).toHaveBeenNthCalledWith(1, '0x00') + }) + + it('calls validateRequest and throws because of a invalid ethereum address', () => { + (isAddress as jest.MockedFunction).mockReturnValueOnce(false) + + expect(() => { + deleteItemAction = new DeleteItemAction(whitelistMock, request) + }).toThrow('Invalid public key passed!') + + expect(isAddress).toHaveBeenNthCalledWith(1, '0x00') + }) + + it('calls execute and returns the expected result', async () => { + (isAddress as jest.MockedFunction).mockReturnValueOnce(true); + + (whitelistMock.deleteItem as jest.MockedFunction).mockReturnValueOnce(Promise.resolve(true)); + + deleteItemAction = new DeleteItemAction(whitelistMock, request) + + await expect(deleteItemAction.execute()).resolves.toEqual(true) + + expect(whitelistMock.deleteItem).toHaveBeenNthCalledWith(1, '0x00') + }) + + it('calls execute and throws as expected', async () => { + (isAddress as jest.MockedFunction).mockReturnValueOnce(true); + + (whitelistMock.deleteItem as jest.MockedFunction).mockReturnValueOnce(Promise.reject('NOPE')); + + deleteItemAction = new DeleteItemAction(whitelistMock, request) + + await expect(deleteItemAction.execute()).rejects.toEqual('NOPE') + + expect(whitelistMock.deleteItem).toHaveBeenNthCalledWith(1, '0x00') + }) +}) From b68dca78da597d321690b58628b2e80c848a6c33 Mon Sep 17 00:00:00 2001 From: nivida Date: Thu, 10 Dec 2020 11:06:01 +0100 Subject: [PATCH 068/107] GetListActionTest implemented --- .../test/src/whitelist/GetListActionTest.ts | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 packages/govern-tx/test/src/whitelist/GetListActionTest.ts diff --git a/packages/govern-tx/test/src/whitelist/GetListActionTest.ts b/packages/govern-tx/test/src/whitelist/GetListActionTest.ts new file mode 100644 index 000000000..49fd81d21 --- /dev/null +++ b/packages/govern-tx/test/src/whitelist/GetListActionTest.ts @@ -0,0 +1,46 @@ +import { isAddress } from '@ethersproject/address'; +import { WhitelistRequest } from '../../../lib/whitelist/AbstractWhitelistAction'; +import Database from '../../../src/db/Database'; +import Whitelist, { ListItem } from '../../../src/db/Whitelist'; +import GetListAction from '../../../src/whitelist/GetListAction'; + +// Mocks +jest.mock('../../../src/db/Whitelist') +jest.mock('@ethersproject/address') + +/** + * GetListAction test + */ +describe('GetListAction Test', () => { + let getListAction: GetListAction, + whitelistMock: Whitelist + + beforeEach(() => { + new Whitelist({} as Database) + whitelistMock = (Whitelist as jest.MockedClass).mock.instances[0] + }) + + it('calls execute and returns the expected result', async () => { + (isAddress as jest.MockedFunction).mockReturnValueOnce(true); + + (whitelistMock.getList as jest.MockedFunction).mockReturnValueOnce(Promise.resolve([{}] as ListItem[])); + + getListAction = new GetListAction(whitelistMock) + + await expect(getListAction.execute()).resolves.toEqual([{}]) + + expect(whitelistMock.getList).toHaveBeenNthCalledWith(1) + }) + + it('calls execute and throws as expected', async () => { + (isAddress as jest.MockedFunction).mockReturnValueOnce(true); + + (whitelistMock.getList as jest.MockedFunction).mockReturnValueOnce(Promise.reject('NOPE')); + + getListAction = new GetListAction(whitelistMock) + + await expect(getListAction.execute()).rejects.toEqual('NOPE') + + expect(whitelistMock.getList).toHaveBeenNthCalledWith(1) + }) +}) From 429f2dfee54865eaeb185c6868ddc82e3e4619a7 Mon Sep 17 00:00:00 2001 From: nivida Date: Thu, 10 Dec 2020 11:27:26 +0100 Subject: [PATCH 069/107] AbstractActionTest implemented and test names updated of transaction, wallet, and whitelist tests --- packages/govern-tx/test/lib/.gitkeep | 0 .../govern-tx/test/lib/AbstractActionTest.ts | 37 +++++++++++++++++++ .../challenge/ChallengeTransactionTest.ts | 2 +- .../execute/ExecuteTransactionTest.ts | 2 +- .../schedule/ScheduleTransactionTest.ts | 2 +- .../govern-tx/test/src/wallet/WalletTest.ts | 2 +- .../test/src/whitelist/AddItemActionTest.ts | 2 +- .../src/whitelist/DeleteItemActionTest.ts | 2 +- .../test/src/whitelist/GetListActionTest.ts | 2 +- 9 files changed, 44 insertions(+), 7 deletions(-) delete mode 100644 packages/govern-tx/test/lib/.gitkeep create mode 100644 packages/govern-tx/test/lib/AbstractActionTest.ts diff --git a/packages/govern-tx/test/lib/.gitkeep b/packages/govern-tx/test/lib/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/packages/govern-tx/test/lib/AbstractActionTest.ts b/packages/govern-tx/test/lib/AbstractActionTest.ts new file mode 100644 index 000000000..72b6e73f9 --- /dev/null +++ b/packages/govern-tx/test/lib/AbstractActionTest.ts @@ -0,0 +1,37 @@ +import AbstractAction, { Request } from '../../lib/AbstractAction'; + +class MockAction extends AbstractAction { + public execute(): Promise { + return Promise.resolve(true) + } +} + +/** + * AbstractAction test + */ +describe('AbstractAction Test', () => { + let action: MockAction + + beforeEach(() => { + action = new MockAction({message: true} as Request) + }) + + it('has the correct schema defined', () => { + //@ts-ignore + expect(AbstractAction.schema).toEqual({ + body: { + type: 'object', + required: ['message', 'signature'], + properties: { + message: { type: 'string' }, + signature: { type: 'string' } + } + } + }) + }) + + it('has set the request property', () => { + //@ts-ignore + expect(action.request).toEqual({message: true}) + }) +}) diff --git a/packages/govern-tx/test/src/transactions/challenge/ChallengeTransactionTest.ts b/packages/govern-tx/test/src/transactions/challenge/ChallengeTransactionTest.ts index 943b7f2b7..fa8b84e72 100644 --- a/packages/govern-tx/test/src/transactions/challenge/ChallengeTransactionTest.ts +++ b/packages/govern-tx/test/src/transactions/challenge/ChallengeTransactionTest.ts @@ -10,7 +10,7 @@ jest.mock('../../../../lib/transactions/AbstractTransaction') /** * ChallengeTransaction test */ -describe('ChallengeTransaction Test', () => { +describe('ChallengeTransactionTest', () => { let challengeTransaction: ChallengeTransaction beforeEach(() => { diff --git a/packages/govern-tx/test/src/transactions/execute/ExecuteTransactionTest.ts b/packages/govern-tx/test/src/transactions/execute/ExecuteTransactionTest.ts index d1f3f8f83..a50d126ba 100644 --- a/packages/govern-tx/test/src/transactions/execute/ExecuteTransactionTest.ts +++ b/packages/govern-tx/test/src/transactions/execute/ExecuteTransactionTest.ts @@ -10,7 +10,7 @@ jest.mock('../../../../lib/transactions/AbstractTransaction') /** * ExecuteTransaction test */ -describe('ExecuteTransaction Test', () => { +describe('ExecuteTransactionTest', () => { let executeTransaction: ExecuteTransaction beforeEach(() => { diff --git a/packages/govern-tx/test/src/transactions/schedule/ScheduleTransactionTest.ts b/packages/govern-tx/test/src/transactions/schedule/ScheduleTransactionTest.ts index 91d5e5a5e..48b6b14b0 100644 --- a/packages/govern-tx/test/src/transactions/schedule/ScheduleTransactionTest.ts +++ b/packages/govern-tx/test/src/transactions/schedule/ScheduleTransactionTest.ts @@ -10,7 +10,7 @@ jest.mock('../../../../lib/transactions/AbstractTransaction') /** * ScheduleTransaction test */ -describe('ScheduleTransaction Test', () => { +describe('ScheduleTransactionTest', () => { let scheduleTransaction: ScheduleTransaction beforeEach(() => { diff --git a/packages/govern-tx/test/src/wallet/WalletTest.ts b/packages/govern-tx/test/src/wallet/WalletTest.ts index 523ee25cf..d8a6ce7e1 100644 --- a/packages/govern-tx/test/src/wallet/WalletTest.ts +++ b/packages/govern-tx/test/src/wallet/WalletTest.ts @@ -11,7 +11,7 @@ jest.mock('@ethersproject/wallet') /** * Wallet test */ -describe('Wallet Test', () => { +describe('WalletTest', () => { let wallet: Wallet, databaseMock: Database diff --git a/packages/govern-tx/test/src/whitelist/AddItemActionTest.ts b/packages/govern-tx/test/src/whitelist/AddItemActionTest.ts index 16886ec12..e139df26d 100644 --- a/packages/govern-tx/test/src/whitelist/AddItemActionTest.ts +++ b/packages/govern-tx/test/src/whitelist/AddItemActionTest.ts @@ -11,7 +11,7 @@ jest.mock('@ethersproject/address') /** * AddItemAction test */ -describe('AddItemAction Test', () => { +describe('AddItemActionTest', () => { let addItemAction: AddItemAction, whitelistMock: Whitelist diff --git a/packages/govern-tx/test/src/whitelist/DeleteItemActionTest.ts b/packages/govern-tx/test/src/whitelist/DeleteItemActionTest.ts index 6aecb52c6..acada6190 100644 --- a/packages/govern-tx/test/src/whitelist/DeleteItemActionTest.ts +++ b/packages/govern-tx/test/src/whitelist/DeleteItemActionTest.ts @@ -11,7 +11,7 @@ jest.mock('@ethersproject/address') /** * DeleteItemAction test */ -describe('DeleteItemAction Test', () => { +describe('DeleteItemActionTest', () => { let deleteItemAction: DeleteItemAction, whitelistMock: Whitelist diff --git a/packages/govern-tx/test/src/whitelist/GetListActionTest.ts b/packages/govern-tx/test/src/whitelist/GetListActionTest.ts index 49fd81d21..203ad8ac0 100644 --- a/packages/govern-tx/test/src/whitelist/GetListActionTest.ts +++ b/packages/govern-tx/test/src/whitelist/GetListActionTest.ts @@ -11,7 +11,7 @@ jest.mock('@ethersproject/address') /** * GetListAction test */ -describe('GetListAction Test', () => { +describe('GetListActionTest', () => { let getListAction: GetListAction, whitelistMock: Whitelist From 304dfadf5caa207b957c715307dacc5f5f4debbf Mon Sep 17 00:00:00 2001 From: nivida Date: Thu, 10 Dec 2020 11:58:34 +0100 Subject: [PATCH 070/107] AbstractTransaction fixed and AbstractTransactionTest created --- .../lib/transactions/AbstractTransaction.ts | 23 ++---- .../transactions/AbstractTransactionTest.ts | 76 +++++++++++++++++++ 2 files changed, 83 insertions(+), 16 deletions(-) create mode 100644 packages/govern-tx/test/lib/transactions/AbstractTransactionTest.ts diff --git a/packages/govern-tx/lib/transactions/AbstractTransaction.ts b/packages/govern-tx/lib/transactions/AbstractTransaction.ts index 5d189016a..58dac10b1 100644 --- a/packages/govern-tx/lib/transactions/AbstractTransaction.ts +++ b/packages/govern-tx/lib/transactions/AbstractTransaction.ts @@ -25,15 +25,6 @@ export default abstract class AbstractTransaction extends AbstractAction { */ protected contract: string - /** - * The contract function - * - * @property {ContractFunction} contractFunction - * - * @private - */ - private contractFunction: ContractFunction - /** * @param {EthereumOptions} config * @param {Provider} provider - The Ethereum provider object @@ -47,11 +38,6 @@ export default abstract class AbstractTransaction extends AbstractAction { request: Request ) { super(request); - - this.contractFunction = new ContractFunction( - this.functionABI, - request.message - ) } /** @@ -64,9 +50,14 @@ export default abstract class AbstractTransaction extends AbstractAction { * @public */ public execute(): Promise { - this.contractFunction.functionArguments[0].payload.submitter = this.config.publicKey + const contractFunction = new ContractFunction( + this.functionABI, + (this.request as Request).message + ) + + contractFunction.functionArguments[0].payload.submitter = this.config.publicKey - return this.provider.sendTransaction(this.contract, this.contractFunction) + return this.provider.sendTransaction(this.contract, contractFunction) } /** diff --git a/packages/govern-tx/test/lib/transactions/AbstractTransactionTest.ts b/packages/govern-tx/test/lib/transactions/AbstractTransactionTest.ts new file mode 100644 index 000000000..39596dad6 --- /dev/null +++ b/packages/govern-tx/test/lib/transactions/AbstractTransactionTest.ts @@ -0,0 +1,76 @@ +import { Request } from '../../../lib//AbstractAction'; +import Provider from '../../../src/provider/Provider'; +import Wallet from '../../../src/wallet/Wallet'; +import { JsonFragment } from '@ethersproject/abi'; +import { TransactionReceipt } from '@ethersproject/abstract-provider'; +import { EthereumOptions } from '../../../src/config/Configuration'; +import AbstractAction from '../../../lib/AbstractAction'; +import ContractFunction from '../../../lib/transactions/ContractFunction'; +import AbstractTransaction from '../../../lib/transactions/AbstractTransaction'; + +// Mocks +class MockTransaction extends AbstractTransaction { + protected functionABI = {} + protected contract = 'CONTRACT_NAME' +} + +jest.mock('../../../src/provider/Provider') +jest.mock('../../../lib/transactions/ContractFunction') + +/** + * AbstractAction test + */ +describe('AbstractAction Test', () => { + let txAction: MockTransaction, + providerMock: Provider, + contractFunctionMock: ContractFunction + + beforeEach(() => { + new Provider({} as EthereumOptions, {} as Wallet) + providerMock = (Provider as jest.MockedClass).mock.instances[0] + + ContractFunction.prototype.functionArguments = [{payload: {submitter: ''}}] + + txAction = new MockTransaction( + { + publicKey: '0x00' + } as EthereumOptions, + providerMock, + { + message: 'MESSAGE' + } as Request + ) + }) + + it('calls execute and returns the expected value', async () => { + (providerMock.sendTransaction as jest.MockedFunction).mockReturnValueOnce(Promise.resolve({} as TransactionReceipt)) + + await expect(txAction.execute()).resolves.toEqual({}) + + contractFunctionMock = (ContractFunction as jest.MockedClass).mock.instances[0] + + expect(ContractFunction).toHaveBeenNthCalledWith(1, {} as JsonFragment, 'MESSAGE') + + expect(providerMock.sendTransaction).toHaveBeenNthCalledWith(1, 'CONTRACT_NAME', contractFunctionMock) + + expect(contractFunctionMock.functionArguments[0].payload.submitter).toEqual('0x00') + }) + + it('calls execute and throws as expected', async () => { + (providerMock.sendTransaction as jest.MockedFunction).mockReturnValueOnce(Promise.reject('NOPE')) + + await expect(txAction.execute()).rejects.toEqual('NOPE') + + contractFunctionMock = (ContractFunction as jest.MockedClass).mock.instances[0] + + expect(ContractFunction).toHaveBeenNthCalledWith(1, {} as JsonFragment, 'MESSAGE') + + expect(providerMock.sendTransaction).toHaveBeenNthCalledWith(1, 'CONTRACT_NAME', contractFunctionMock) + + expect(contractFunctionMock.functionArguments[0].payload.submitter).toEqual('0x00') + }) + + it('has the correct schema defined', () => { + expect(AbstractTransaction.schema).toEqual(Object.assign(AbstractAction.schema, AbstractTransaction.schema)) + }) +}) From 3136ce45fa9b553bb2b5c952a19f3a82a71b7fab Mon Sep 17 00:00:00 2001 From: nivida Date: Thu, 10 Dec 2020 12:16:32 +0100 Subject: [PATCH 071/107] ContractFunctionTest implemented --- .../transactions/AbstractTransactionTest.ts | 4 +- .../lib/transactions/ContractFunctionTest.ts | 58 +++++++++++++++++++ 2 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 packages/govern-tx/test/lib/transactions/ContractFunctionTest.ts diff --git a/packages/govern-tx/test/lib/transactions/AbstractTransactionTest.ts b/packages/govern-tx/test/lib/transactions/AbstractTransactionTest.ts index 39596dad6..7f4205cc0 100644 --- a/packages/govern-tx/test/lib/transactions/AbstractTransactionTest.ts +++ b/packages/govern-tx/test/lib/transactions/AbstractTransactionTest.ts @@ -18,9 +18,9 @@ jest.mock('../../../src/provider/Provider') jest.mock('../../../lib/transactions/ContractFunction') /** - * AbstractAction test + * AbstractTransaction test */ -describe('AbstractAction Test', () => { +describe('AbstractTransactionTest', () => { let txAction: MockTransaction, providerMock: Provider, contractFunctionMock: ContractFunction diff --git a/packages/govern-tx/test/lib/transactions/ContractFunctionTest.ts b/packages/govern-tx/test/lib/transactions/ContractFunctionTest.ts new file mode 100644 index 000000000..1208f7bf2 --- /dev/null +++ b/packages/govern-tx/test/lib/transactions/ContractFunctionTest.ts @@ -0,0 +1,58 @@ +import { defaultAbiCoder, Fragment, JsonFragment } from '@ethersproject/abi'; +import ContractFunction from '../../../lib/transactions/ContractFunction'; + +// Mocks +jest.mock('@ethersproject/abi') + +/** + * ContractFunction test + */ +describe('ContractFunctionTest', () => { + let contractFunction: ContractFunction + + beforeEach(() => { + (defaultAbiCoder.decode as jest.MockedFunction).mockReturnValueOnce(['ARGUMENT']); + + (Fragment.fromObject as jest.MockedFunction).mockReturnValueOnce({inputs: 'INPUTS'} as any); + + contractFunction = new ContractFunction({} as JsonFragment, 'MESSAGE') + + expect(defaultAbiCoder.decode).toHaveBeenNthCalledWith(1, 'INPUTS', 'MESSAGE') + + expect(contractFunction.functionArguments).toEqual(['ARGUMENT']) + }) + + it('calls encode and returns the expected value', () => { + (defaultAbiCoder.encode as jest.MockedFunction).mockReturnValueOnce('ENCODED') + + expect(contractFunction.encode()).toEqual('ENCODED') + + expect(defaultAbiCoder.encode).toHaveBeenNthCalledWith(1, 'INPUTS', ['ARGUMENT']) + }) + + it('calls encode and throws as expected', () => { + //@ts-ignore + (defaultAbiCoder.encode as jest.MockedFunction).mockImplementation(() => {throw 'NOPE'}) + + expect(() => contractFunction.encode()).toThrow('NOPE') + + expect(defaultAbiCoder.encode).toHaveBeenNthCalledWith(1, 'INPUTS', ['ARGUMENT']) + }) + + it('calls decode and returns the expected value', () => { + (defaultAbiCoder.decode as jest.MockedFunction).mockReturnValueOnce(['DECODED']); + + expect(contractFunction.decode()).toEqual(['DECODED']) + + expect(defaultAbiCoder.decode).toHaveBeenNthCalledWith(1, 'INPUTS', 'MESSAGE') + }) + + it('calls decode and throws as expected', () => { + //@ts-ignore + (defaultAbiCoder.decode as jest.MockedFunction).mockImplementation(() => {throw 'NOPE'}) + + expect(() => contractFunction.decode()).toThrow('NOPE') + + expect(defaultAbiCoder.decode).toHaveBeenNthCalledWith(1, 'INPUTS', 'MESSAGE') + }) +}) From a19e260dcd343e9bfb619cce79265532f3648e54 Mon Sep 17 00:00:00 2001 From: nivida Date: Thu, 10 Dec 2020 12:22:00 +0100 Subject: [PATCH 072/107] AbstractActionTest updated and AbstractWhitelistActionTest implemented --- .../govern-tx/test/lib/AbstractActionTest.ts | 1 - .../whitelist/AbstractWhitelistActionTest.ts | 42 +++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 packages/govern-tx/test/lib/whitelist/AbstractWhitelistActionTest.ts diff --git a/packages/govern-tx/test/lib/AbstractActionTest.ts b/packages/govern-tx/test/lib/AbstractActionTest.ts index 72b6e73f9..c798a9ebb 100644 --- a/packages/govern-tx/test/lib/AbstractActionTest.ts +++ b/packages/govern-tx/test/lib/AbstractActionTest.ts @@ -17,7 +17,6 @@ describe('AbstractAction Test', () => { }) it('has the correct schema defined', () => { - //@ts-ignore expect(AbstractAction.schema).toEqual({ body: { type: 'object', diff --git a/packages/govern-tx/test/lib/whitelist/AbstractWhitelistActionTest.ts b/packages/govern-tx/test/lib/whitelist/AbstractWhitelistActionTest.ts new file mode 100644 index 000000000..3053c3a78 --- /dev/null +++ b/packages/govern-tx/test/lib/whitelist/AbstractWhitelistActionTest.ts @@ -0,0 +1,42 @@ +import Whitelist from '../../../src/db/Whitelist'; +import AbstractWhitelistAction from '../../../lib/whitelist/AbstractWhitelistAction'; + +// Mocks +class MockAction extends AbstractWhitelistAction { + public execute(): Promise { + return Promise.resolve(true) + } +} + +/** + * AbstractWhitelistAction test + */ +describe('AbstractWhitelistAction Test', () => { + let action: MockAction + + beforeEach(() => { + action = new MockAction( + {} as Whitelist, + { + message: { + publicKey: '0x00', + rateLimit: 0 + }, + signature: '' + } + ) + }) + + it('has the correct schema defined', () => { + expect(AbstractWhitelistAction.schema).toEqual({ + body: { + type: 'object', + required: ['message', 'signature'], + properties: { + message: { type: 'string' }, + signature: { type: 'string' } + } + } + }) + }) +}) From c10ec793b26ae2d30a0a56dcc24b65a3dbfda75e Mon Sep 17 00:00:00 2001 From: nivida Date: Thu, 10 Dec 2020 12:53:32 +0100 Subject: [PATCH 073/107] basic docker-compose added and init.sql to create the empty tables --- packages/govern-tx/docker-compose.yml | 18 ++++++++++++++++++ packages/govern-tx/postgres/init.sql | 14 ++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 packages/govern-tx/docker-compose.yml create mode 100644 packages/govern-tx/postgres/init.sql diff --git a/packages/govern-tx/docker-compose.yml b/packages/govern-tx/docker-compose.yml new file mode 100644 index 000000000..8ff5d75b3 --- /dev/null +++ b/packages/govern-tx/docker-compose.yml @@ -0,0 +1,18 @@ +version: "3" +services: + postgres: + image: postgres + ports: + - "5432:5432" + environment: + POSTGRES_USER: govern-tx + POSTGRES_PASSWORD: tx-service + POSTGRES_DB: govern-tx + user: "${HOST_UID}:${HOST_GID}" + volumes: + - ./dev-data/postgres:/var/lib/postgresql/data + - ./postgres:/docker-entrypoint-initdb.d/ + ganache: + image: trufflesuite/ganache-cli + ports: + - "8545:8545" diff --git a/packages/govern-tx/postgres/init.sql b/packages/govern-tx/postgres/init.sql new file mode 100644 index 000000000..0283bbdec --- /dev/null +++ b/packages/govern-tx/postgres/init.sql @@ -0,0 +1,14 @@ +CREATE TABLE whitelist { + ID int NOT NULL AUTO_INCREMENT, + PublicKey char(42) NOT NULL, + "Limit" int DEFAULT 100 NOT NULL, + Executed int DEFAULT 0 NOT NULL, + PRIMARY KEY (id) +}; + +CREATE TABLE admin { + ID int NOT NULL AUTO_INCREMENT, + PublicKey char(42) NOT NULL, + PrivateKey char(66) NOT NULL, + PRIMARY KEY (id) +}; From 025b6088b23394753687c4f1757ad842c504fe13 Mon Sep 17 00:00:00 2001 From: nivida Date: Thu, 10 Dec 2020 13:04:23 +0100 Subject: [PATCH 074/107] config setup implemented --- packages/govern-tx/config/dev.config.ts | 21 +++++++++++++ packages/govern-tx/config/prod.config.ts | 21 +++++++++++++ packages/govern-tx/src/index.ts | 38 ++++++++---------------- 3 files changed, 54 insertions(+), 26 deletions(-) create mode 100644 packages/govern-tx/config/dev.config.ts create mode 100644 packages/govern-tx/config/prod.config.ts diff --git a/packages/govern-tx/config/dev.config.ts b/packages/govern-tx/config/dev.config.ts new file mode 100644 index 000000000..64ad58bde --- /dev/null +++ b/packages/govern-tx/config/dev.config.ts @@ -0,0 +1,21 @@ +export const config = { + ethereum: { + publicKey: '', // Add default EOA + contracts: { + GovernQueue: '' // Add deployment address can get calculated if deployed with create2 + }, + url: 'localhost:8545', + blockConfirmations: 42 + }, + database: { + user: 'govern-tx', + host: 'localhost', + password: 'tx-service', + database: 'govern-tx', + port: 5432 + }, + server: { + host: '0.0.0.0', + port: 4040 + } +} diff --git a/packages/govern-tx/config/prod.config.ts b/packages/govern-tx/config/prod.config.ts new file mode 100644 index 000000000..64ad58bde --- /dev/null +++ b/packages/govern-tx/config/prod.config.ts @@ -0,0 +1,21 @@ +export const config = { + ethereum: { + publicKey: '', // Add default EOA + contracts: { + GovernQueue: '' // Add deployment address can get calculated if deployed with create2 + }, + url: 'localhost:8545', + blockConfirmations: 42 + }, + database: { + user: 'govern-tx', + host: 'localhost', + password: 'tx-service', + database: 'govern-tx', + port: 5432 + }, + server: { + host: '0.0.0.0', + port: 4040 + } +} diff --git a/packages/govern-tx/src/index.ts b/packages/govern-tx/src/index.ts index 2dd7a562e..7bfbc09ac 100644 --- a/packages/govern-tx/src/index.ts +++ b/packages/govern-tx/src/index.ts @@ -1,28 +1,14 @@ -import Configuration from './config/Configuration' +import { config as prodConfig } from '../config/prod.config' +import { config as devConfig } from '../config/dev.config' +import Configuration, { Config } from './config/Configuration' import Bootstrap from './Bootstrap' -new Bootstrap( - new Configuration( - { - ethereum: { - publicKey: '0x0...', - contracts: { - GovernQueue: '0x0...' - }, - url: 'localhost:8545', - blockConfirmations: 42 - }, - database: { - user: 'govern', - host: 'localhost', - password: 'dev', - database: 'govern', - port: 4000 - }, - server: { - host: '0.0.0.0', - port: 4040 - } - } - ) -).run() +let config: Config; + +if (process.env.DEV) { + config = devConfig +} else { + config = prodConfig +} + +new Bootstrap(new Configuration(config)).run() From 4ea093f1963d3fbebcf6bf1dab1ad030b3e9b180 Mon Sep 17 00:00:00 2001 From: nivida Date: Thu, 10 Dec 2020 13:12:18 +0100 Subject: [PATCH 075/107] types added to config, index.ts improved, and e2e config etc. added --- packages/govern-tx/config/dev.config.ts | 4 +++- packages/govern-tx/config/prod.config.ts | 4 +++- packages/govern-tx/jest.config.e2e.js | 4 ++++ packages/govern-tx/package.json | 5 +++-- packages/govern-tx/src/index.ts | 16 ++++++---------- 5 files changed, 19 insertions(+), 14 deletions(-) create mode 100644 packages/govern-tx/jest.config.e2e.js diff --git a/packages/govern-tx/config/dev.config.ts b/packages/govern-tx/config/dev.config.ts index 64ad58bde..e1c4d7a83 100644 --- a/packages/govern-tx/config/dev.config.ts +++ b/packages/govern-tx/config/dev.config.ts @@ -1,4 +1,6 @@ -export const config = { +import { Config } from '../src/config/Configuration'; + +export const config: Config = { ethereum: { publicKey: '', // Add default EOA contracts: { diff --git a/packages/govern-tx/config/prod.config.ts b/packages/govern-tx/config/prod.config.ts index 64ad58bde..e1c4d7a83 100644 --- a/packages/govern-tx/config/prod.config.ts +++ b/packages/govern-tx/config/prod.config.ts @@ -1,4 +1,6 @@ -export const config = { +import { Config } from '../src/config/Configuration'; + +export const config: Config = { ethereum: { publicKey: '', // Add default EOA contracts: { diff --git a/packages/govern-tx/jest.config.e2e.js b/packages/govern-tx/jest.config.e2e.js new file mode 100644 index 000000000..1315a4696 --- /dev/null +++ b/packages/govern-tx/jest.config.e2e.js @@ -0,0 +1,4 @@ +const jestConfig = require('./jest.config.js') +jestConfig.testMatch = ['/**/**Test.e2e.ts'] + +module.exports = jestConfig; diff --git a/packages/govern-tx/package.json b/packages/govern-tx/package.json index 82352707e..21d6cf106 100644 --- a/packages/govern-tx/package.json +++ b/packages/govern-tx/package.json @@ -4,12 +4,13 @@ "description": "Transactions service of Govern", "main": "./src/index.js", "scripts": { - "dev": "ts-node-dev src/index.ts", + "dev": "DEV=true ts-node-dev src/index.ts", "start": "yarn start:containers && yarn start:server", "start:server": "node --loader ts-node/esm.mjs src/index.ts", "start:containers": "docker-compose up -d", "stop:containers": "docker-compose down", - "test": "jest" + "e2e": "yarn start && jest -c ./jest.config.e2e.js", + "test": "jest && yarn e2e" }, "authors": [ { diff --git a/packages/govern-tx/src/index.ts b/packages/govern-tx/src/index.ts index 7bfbc09ac..f39cefd52 100644 --- a/packages/govern-tx/src/index.ts +++ b/packages/govern-tx/src/index.ts @@ -1,14 +1,10 @@ import { config as prodConfig } from '../config/prod.config' import { config as devConfig } from '../config/dev.config' -import Configuration, { Config } from './config/Configuration' +import Configuration from './config/Configuration' import Bootstrap from './Bootstrap' -let config: Config; - -if (process.env.DEV) { - config = devConfig -} else { - config = prodConfig -} - -new Bootstrap(new Configuration(config)).run() +new Bootstrap( + new Configuration( + process.env.DEV ? devConfig : prodConfig + ) +).run() From 6da8bc8e44f8d20ce1057ee06d57a06bf8aa393a Mon Sep 17 00:00:00 2001 From: nivida Date: Thu, 10 Dec 2020 13:21:55 +0100 Subject: [PATCH 076/107] db_model.png updated --- packages/govern-tx/assets/db_model.png | Bin 132019 -> 66543 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/packages/govern-tx/assets/db_model.png b/packages/govern-tx/assets/db_model.png index 692ee2f1407605f0b04b549c23f596c6c878d3f8..25b0fe072e4b80920140018de8108276bc3ef0b2 100644 GIT binary patch literal 66543 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 DgGUUv From e4c75dc26be61dfe6845e434eb83df6cdfd1aece Mon Sep 17 00:00:00 2001 From: nivida Date: Thu, 10 Dec 2020 13:37:38 +0100 Subject: [PATCH 077/107] type def fixed in AddItemAction, dev.config updated, and Provider fixed --- packages/govern-tx/config/dev.config.ts | 2 +- packages/govern-tx/src/provider/Provider.ts | 6 ++-- .../govern-tx/src/whitelist/AddItemAction.ts | 2 +- .../test/src/provider/ProviderTest.ts | 28 +++++++++---------- 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/packages/govern-tx/config/dev.config.ts b/packages/govern-tx/config/dev.config.ts index e1c4d7a83..aac306fc7 100644 --- a/packages/govern-tx/config/dev.config.ts +++ b/packages/govern-tx/config/dev.config.ts @@ -6,7 +6,7 @@ export const config: Config = { contracts: { GovernQueue: '' // Add deployment address can get calculated if deployed with create2 }, - url: 'localhost:8545', + url: 'http://localhost:8545', blockConfirmations: 42 }, database: { diff --git a/packages/govern-tx/src/provider/Provider.ts b/packages/govern-tx/src/provider/Provider.ts index 9f6bf7ecd..4f91e4f25 100644 --- a/packages/govern-tx/src/provider/Provider.ts +++ b/packages/govern-tx/src/provider/Provider.ts @@ -1,4 +1,4 @@ -import { BaseProvider, TransactionRequest } from '@ethersproject/providers' +import { JsonRpcProvider, TransactionRequest } from '@ethersproject/providers' import { TransactionReceipt } from '@ethersproject/abstract-provider' import Wallet from '../wallet/Wallet' import { EthereumOptions } from '../config/Configuration'; @@ -13,7 +13,7 @@ export default class Provider { * * @private */ - private provider: BaseProvider + private provider: JsonRpcProvider /** * @param {EthereumOptions} config @@ -22,7 +22,7 @@ export default class Provider { * @constructor */ constructor(private config: EthereumOptions, private wallet: Wallet) { - this.provider = new BaseProvider(this.config.url) + this.provider = new JsonRpcProvider(this.config.url) } /** diff --git a/packages/govern-tx/src/whitelist/AddItemAction.ts b/packages/govern-tx/src/whitelist/AddItemAction.ts index 18f090426..8749c617d 100644 --- a/packages/govern-tx/src/whitelist/AddItemAction.ts +++ b/packages/govern-tx/src/whitelist/AddItemAction.ts @@ -38,7 +38,7 @@ export default class AddItemAction extends AbstractWhitelistAction { public execute(): Promise { return this.whitelist.addItem( (this.request as WhitelistRequest).message.publicKey, - (this.request as WhitelistRequest).message.rateLimit + ((this.request as WhitelistRequest).message.rateLimit as number) ) } } diff --git a/packages/govern-tx/test/src/provider/ProviderTest.ts b/packages/govern-tx/test/src/provider/ProviderTest.ts index 466ac3ed2..c25a8bf16 100644 --- a/packages/govern-tx/test/src/provider/ProviderTest.ts +++ b/packages/govern-tx/test/src/provider/ProviderTest.ts @@ -1,4 +1,4 @@ -import { BaseProvider, TransactionResponse } from '@ethersproject/providers'; +import { JsonRpcProvider, TransactionResponse } from '@ethersproject/providers'; import { BigNumber } from "@ethersproject/bignumber"; import { JsonFragment } from '@ethersproject/abi'; import ContractFunction from '../../../lib/transactions/ContractFunction'; @@ -31,7 +31,7 @@ class TXResponse implements TransactionResponse { describe('ProviderTest', () => { let provider: Provider, walletMock: Wallet, - baseProviderMock: BaseProvider, + jsonRpcProviderMock: JsonRpcProvider, contractFunctionMock: ContractFunction; beforeEach(() => { @@ -51,7 +51,7 @@ describe('ProviderTest', () => { walletMock ) - baseProviderMock = (BaseProvider as jest.MockedClass).mock.instances[0] + jsonRpcProviderMock = (JsonRpcProvider as jest.MockedClass).mock.instances[0] }) it('calls sendTransaction and returns with the expected value', async () => { @@ -63,13 +63,13 @@ describe('ProviderTest', () => { (contractFunctionMock.encode as jest.MockedFunction).mockReturnValue('0x00'); - (baseProviderMock.sendTransaction as jest.MockedFunction).mockReturnValue(Promise.resolve(txResponse)); + (jsonRpcProviderMock.sendTransaction as jest.MockedFunction).mockReturnValue(Promise.resolve(txResponse)); - (baseProviderMock.estimateGas as jest.MockedFunction).mockReturnValue(Promise.resolve(BigNumber.from(0))); + (jsonRpcProviderMock.estimateGas as jest.MockedFunction).mockReturnValue(Promise.resolve(BigNumber.from(0))); await expect(provider.sendTransaction('GovernQueue', contractFunctionMock)).resolves.toEqual('RECEIPT'); - expect(baseProviderMock.sendTransaction).toHaveBeenNthCalledWith(1, '0x00'); + expect(jsonRpcProviderMock.sendTransaction).toHaveBeenNthCalledWith(1, '0x00'); expect(txResponse.wait).toHaveBeenNthCalledWith(1, 0); @@ -91,7 +91,7 @@ describe('ProviderTest', () => { (contractFunctionMock.encode as jest.MockedFunction).mockReturnValue('0x00'); - (baseProviderMock.estimateGas as jest.MockedFunction).mockReturnValue(Promise.resolve(BigNumber.from(0))); + (jsonRpcProviderMock.estimateGas as jest.MockedFunction).mockReturnValue(Promise.resolve(BigNumber.from(0))); await expect(provider.sendTransaction('GovernQueue', contractFunctionMock)).rejects.toEqual('NOPE'); @@ -111,7 +111,7 @@ describe('ProviderTest', () => { it('calls sendTransaction and estimation of the gas throws', async () => { (contractFunctionMock.encode as jest.MockedFunction).mockReturnValue('0x00'); - (baseProviderMock.estimateGas as jest.MockedFunction).mockReturnValue(Promise.reject('NOPE')); + (jsonRpcProviderMock.estimateGas as jest.MockedFunction).mockReturnValue(Promise.reject('NOPE')); await expect(provider.sendTransaction('GovernQueue', contractFunctionMock)).rejects.toEqual('NOPE'); @@ -131,13 +131,13 @@ describe('ProviderTest', () => { (contractFunctionMock.encode as jest.MockedFunction).mockReturnValue('0x00'); - (baseProviderMock.sendTransaction as jest.MockedFunction).mockReturnValue(Promise.reject('NOPE')); + (jsonRpcProviderMock.sendTransaction as jest.MockedFunction).mockReturnValue(Promise.reject('NOPE')); - (baseProviderMock.estimateGas as jest.MockedFunction).mockReturnValue(Promise.resolve(BigNumber.from(0))); + (jsonRpcProviderMock.estimateGas as jest.MockedFunction).mockReturnValue(Promise.resolve(BigNumber.from(0))); await expect(provider.sendTransaction('GovernQueue', contractFunctionMock)).rejects.toEqual('NOPE'); - expect(baseProviderMock.sendTransaction).toHaveBeenNthCalledWith(1, '0x00'); + expect(jsonRpcProviderMock.sendTransaction).toHaveBeenNthCalledWith(1, '0x00'); expect(walletMock.sign).toHaveBeenNthCalledWith( 1, @@ -161,13 +161,13 @@ describe('ProviderTest', () => { (contractFunctionMock.encode as jest.MockedFunction).mockReturnValue('0x00'); - (baseProviderMock.sendTransaction as jest.MockedFunction).mockReturnValue(Promise.resolve(txResponse)); + (jsonRpcProviderMock.sendTransaction as jest.MockedFunction).mockReturnValue(Promise.resolve(txResponse)); - (baseProviderMock.estimateGas as jest.MockedFunction).mockReturnValue(Promise.resolve(BigNumber.from(0))); + (jsonRpcProviderMock.estimateGas as jest.MockedFunction).mockReturnValue(Promise.resolve(BigNumber.from(0))); await expect(provider.sendTransaction('GovernQueue', contractFunctionMock)).rejects.toEqual('NOPE'); - expect(baseProviderMock.sendTransaction).toHaveBeenNthCalledWith(1, '0x00'); + expect(jsonRpcProviderMock.sendTransaction).toHaveBeenNthCalledWith(1, '0x00'); expect(txResponse.wait).toHaveBeenNthCalledWith(1, 0); From 52d063a93267fdaeed4a44ad01b5a73ceb76388b Mon Sep 17 00:00:00 2001 From: nivida Date: Thu, 10 Dec 2020 14:34:50 +0100 Subject: [PATCH 078/107] missing funcSig added to ContractFunction encoding --- .../govern-tx/lib/transactions/ContractFunction.ts | 3 ++- packages/govern-tx/package.json | 5 +++-- yarn.lock | 14 ++++++++++++++ 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/packages/govern-tx/lib/transactions/ContractFunction.ts b/packages/govern-tx/lib/transactions/ContractFunction.ts index 5d91d68b3..395b266ae 100644 --- a/packages/govern-tx/lib/transactions/ContractFunction.ts +++ b/packages/govern-tx/lib/transactions/ContractFunction.ts @@ -1,4 +1,5 @@ import { defaultAbiCoder, Fragment, JsonFragment } from '@ethersproject/abi'; +import { id } from '@ethersproject/hash' export default class ContractFunction { /** @@ -41,7 +42,7 @@ export default class ContractFunction { * @public */ public encode(): string { - return defaultAbiCoder.encode(this.abiItem.inputs, this.functionArguments) + return this.abiItem.format() + defaultAbiCoder.encode(this.abiItem.inputs, this.functionArguments) } /** diff --git a/packages/govern-tx/package.json b/packages/govern-tx/package.json index 21d6cf106..3a2acf9d4 100644 --- a/packages/govern-tx/package.json +++ b/packages/govern-tx/package.json @@ -23,15 +23,16 @@ "@ethereumjs/config-tsc": "^1.1.1", "@ethereumjs/config-tslint": "^1.1.1", "@types/jest": "^26.0.15", + "@types/node": "^14.14.10", "jest": "^26.6.1", "ts-jest": "^26.4.2", "tslint": "^6.1.3", - "typescript": "^4.0.5", - "@types/node": "^14.14.10" + "typescript": "^4.0.5" }, "dependencies": { "@ethersproject/address": "^5.0.7", "@ethersproject/bytes": "^5.0.6", + "@ethersproject/hash": "^5.0.9", "@ethersproject/providers": "^5.0.15", "@ethersproject/wallet": "^5.0.8", "fastify": "^3.8.0", diff --git a/yarn.lock b/yarn.lock index 6f113186d..705654052 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1590,6 +1590,20 @@ "@ethersproject/properties" "^5.0.4" "@ethersproject/strings" "^5.0.4" +"@ethersproject/hash@^5.0.9": + version "5.0.9" + resolved "https://registry.yarnpkg.com/@ethersproject/hash/-/hash-5.0.9.tgz#81252a848185b584aa600db4a1a68cad9229a4d4" + integrity sha512-e8/i2ZDeGSgCxXT0vocL54+pMbw5oX5fNjb2E3bAIvdkh5kH29M7zz1jHu1QDZnptIuvCZepIbhUH8lxKE2/SQ== + dependencies: + "@ethersproject/abstract-signer" "^5.0.6" + "@ethersproject/address" "^5.0.5" + "@ethersproject/bignumber" "^5.0.8" + "@ethersproject/bytes" "^5.0.4" + "@ethersproject/keccak256" "^5.0.3" + "@ethersproject/logger" "^5.0.5" + "@ethersproject/properties" "^5.0.4" + "@ethersproject/strings" "^5.0.4" + "@ethersproject/hdnode@5.0.5", "@ethersproject/hdnode@^5.0.4": version "5.0.5" resolved "https://registry.yarnpkg.com/@ethersproject/hdnode/-/hdnode-5.0.5.tgz#1f89aad0a5ba9dfae3a85a36e0669f8bc7a74781" From 21eed44261661d72297ac1b400fbfc0e40400c5e Mon Sep 17 00:00:00 2001 From: nivida Date: Thu, 10 Dec 2020 14:39:07 +0100 Subject: [PATCH 079/107] ContractFunction class improved --- packages/govern-tx/lib/transactions/ContractFunction.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/govern-tx/lib/transactions/ContractFunction.ts b/packages/govern-tx/lib/transactions/ContractFunction.ts index 395b266ae..c8f74cfb8 100644 --- a/packages/govern-tx/lib/transactions/ContractFunction.ts +++ b/packages/govern-tx/lib/transactions/ContractFunction.ts @@ -42,7 +42,7 @@ export default class ContractFunction { * @public */ public encode(): string { - return this.abiItem.format() + defaultAbiCoder.encode(this.abiItem.inputs, this.functionArguments) + return id(this.abiItem.format()) + defaultAbiCoder.encode(this.abiItem.inputs, this.functionArguments).replace('0x', '') } /** From d7f4dc9092f2e0e1f01024821d0ba73c309c5fc3 Mon Sep 17 00:00:00 2001 From: nivida Date: Thu, 10 Dec 2020 15:35:29 +0100 Subject: [PATCH 080/107] ContractFunctionTest updated --- packages/govern-tx/.gitignore | 3 ++- .../lib/transactions/ContractFunction.ts | 8 ++++++- .../lib/transactions/ContractFunctionTest.ts | 21 +++++++++++++------ 3 files changed, 24 insertions(+), 8 deletions(-) diff --git a/packages/govern-tx/.gitignore b/packages/govern-tx/.gitignore index 4b4d86310..076c97772 100644 --- a/packages/govern-tx/.gitignore +++ b/packages/govern-tx/.gitignore @@ -1 +1,2 @@ -coverage/ \ No newline at end of file +coverage/ +dev-data/ diff --git a/packages/govern-tx/lib/transactions/ContractFunction.ts b/packages/govern-tx/lib/transactions/ContractFunction.ts index c8f74cfb8..e2050519a 100644 --- a/packages/govern-tx/lib/transactions/ContractFunction.ts +++ b/packages/govern-tx/lib/transactions/ContractFunction.ts @@ -42,7 +42,13 @@ export default class ContractFunction { * @public */ public encode(): string { - return id(this.abiItem.format()) + defaultAbiCoder.encode(this.abiItem.inputs, this.functionArguments).replace('0x', '') + return id(this.abiItem.format()) + + ( + defaultAbiCoder.encode( + this.abiItem.inputs, + this.functionArguments + ).replace('0x','') + ) } /** diff --git a/packages/govern-tx/test/lib/transactions/ContractFunctionTest.ts b/packages/govern-tx/test/lib/transactions/ContractFunctionTest.ts index 1208f7bf2..a1df4ed83 100644 --- a/packages/govern-tx/test/lib/transactions/ContractFunctionTest.ts +++ b/packages/govern-tx/test/lib/transactions/ContractFunctionTest.ts @@ -1,19 +1,22 @@ -import { defaultAbiCoder, Fragment, JsonFragment } from '@ethersproject/abi'; +import { defaultAbiCoder, Fragment, JsonFragment, FunctionFragment } from '@ethersproject/abi'; +import { id } from '@ethersproject/hash' import ContractFunction from '../../../lib/transactions/ContractFunction'; // Mocks jest.mock('@ethersproject/abi') +jest.mock('@ethersproject/hash') /** * ContractFunction test */ describe('ContractFunctionTest', () => { - let contractFunction: ContractFunction + let contractFunction: ContractFunction, + fragmentMock = {inputs: 'INPUTS', format: jest.fn()} beforeEach(() => { (defaultAbiCoder.decode as jest.MockedFunction).mockReturnValueOnce(['ARGUMENT']); - - (Fragment.fromObject as jest.MockedFunction).mockReturnValueOnce({inputs: 'INPUTS'} as any); + + (Fragment.fromObject as jest.MockedFunction).mockReturnValueOnce(fragmentMock as any); contractFunction = new ContractFunction({} as JsonFragment, 'MESSAGE') @@ -23,11 +26,17 @@ describe('ContractFunctionTest', () => { }) it('calls encode and returns the expected value', () => { - (defaultAbiCoder.encode as jest.MockedFunction).mockReturnValueOnce('ENCODED') + (id as jest.MockedFunction).mockReturnValueOnce('0x0'); + + (fragmentMock.format as jest.MockedFunction).mockReturnValueOnce('SIGNATURE'); - expect(contractFunction.encode()).toEqual('ENCODED') + (defaultAbiCoder.encode as jest.MockedFunction).mockReturnValueOnce('0xENCODED'); + + expect(contractFunction.encode()).toEqual('0x0ENCODED') expect(defaultAbiCoder.encode).toHaveBeenNthCalledWith(1, 'INPUTS', ['ARGUMENT']) + + expect(id).toHaveBeenNthCalledWith(1, 'SIGNATURE') }) it('calls encode and throws as expected', () => { From ff32f42bb4dd84419026fb9ec43d73933adb3c44 Mon Sep 17 00:00:00 2001 From: nivida Date: Fri, 11 Dec 2020 11:14:02 +0100 Subject: [PATCH 081/107] init.sql updated and tx limit per account implemented --- packages/govern-tx/lib/AbstractAction.ts | 3 ++- .../lib/transactions/AbstractTransaction.ts | 18 ++++++++++--- packages/govern-tx/package.json | 5 ++-- packages/govern-tx/postgres/init.sql | 6 ++++- packages/govern-tx/src/Bootstrap.ts | 25 ++++++++++++++++--- packages/govern-tx/src/auth/Authenticator.ts | 16 +++++++----- packages/govern-tx/src/db/Whitelist.ts | 21 +++++++++++++--- 7 files changed, 73 insertions(+), 21 deletions(-) diff --git a/packages/govern-tx/lib/AbstractAction.ts b/packages/govern-tx/lib/AbstractAction.ts index e54ba2102..3b5fc8dc6 100644 --- a/packages/govern-tx/lib/AbstractAction.ts +++ b/packages/govern-tx/lib/AbstractAction.ts @@ -2,7 +2,8 @@ import { FastifySchema } from 'fastify' export interface Request { message: string | any, - signature: string + signature: string, + publicKey: string } export default abstract class AbstractAction { diff --git a/packages/govern-tx/lib/transactions/AbstractTransaction.ts b/packages/govern-tx/lib/transactions/AbstractTransaction.ts index 58dac10b1..e11ba8e1e 100644 --- a/packages/govern-tx/lib/transactions/AbstractTransaction.ts +++ b/packages/govern-tx/lib/transactions/AbstractTransaction.ts @@ -5,6 +5,7 @@ import AbstractAction, { Request } from '../AbstractAction' import ContractFunction from '../transactions/ContractFunction' import Provider from '../../src/provider/Provider' import { EthereumOptions } from '../../src/config/Configuration'; +import Whitelist from '../../src/db/Whitelist'; export default abstract class AbstractTransaction extends AbstractAction { /** @@ -26,16 +27,20 @@ export default abstract class AbstractTransaction extends AbstractAction { protected contract: string /** - * @param {EthereumOptions} config + * @param {EthereumOptions} config - Ethereum related configurations * @param {Provider} provider - The Ethereum provider object + * @param {Whitelist} whitelist - Whitelist DB adapter * @param {Request} request - The request body given by the user + * @param {string} publicKey - The public key of the user * * @constructor */ constructor( private config: EthereumOptions, private provider: Provider, - request: Request + private whitelist: Whitelist, + request: Request, + private publicKey: string ) { super(request); } @@ -49,7 +54,7 @@ export default abstract class AbstractTransaction extends AbstractAction { * * @public */ - public execute(): Promise { + public async execute(): Promise { const contractFunction = new ContractFunction( this.functionABI, (this.request as Request).message @@ -57,7 +62,12 @@ export default abstract class AbstractTransaction extends AbstractAction { contractFunction.functionArguments[0].payload.submitter = this.config.publicKey - return this.provider.sendTransaction(this.contract, contractFunction) + let receipt: TransactionReceipt; + + receipt = await this.provider.sendTransaction(this.contract, contractFunction) + this.whitelist.increaseExecutionCounter(this.publicKey) + + return receipt; } /** diff --git a/packages/govern-tx/package.json b/packages/govern-tx/package.json index 3a2acf9d4..e2097f4fd 100644 --- a/packages/govern-tx/package.json +++ b/packages/govern-tx/package.json @@ -4,9 +4,8 @@ "description": "Transactions service of Govern", "main": "./src/index.js", "scripts": { - "dev": "DEV=true ts-node-dev src/index.ts", - "start": "yarn start:containers && yarn start:server", - "start:server": "node --loader ts-node/esm.mjs src/index.ts", + "dev": "yarn start:containers && DEV=true ts-node-dev src/index.ts", + "start": "node --loader ts-node/esm.mjs src/index.ts", "start:containers": "docker-compose up -d", "stop:containers": "docker-compose down", "e2e": "yarn start && jest -c ./jest.config.e2e.js", diff --git a/packages/govern-tx/postgres/init.sql b/packages/govern-tx/postgres/init.sql index 0283bbdec..07756f461 100644 --- a/packages/govern-tx/postgres/init.sql +++ b/packages/govern-tx/postgres/init.sql @@ -9,6 +9,10 @@ CREATE TABLE whitelist { CREATE TABLE admin { ID int NOT NULL AUTO_INCREMENT, PublicKey char(42) NOT NULL, - PrivateKey char(66) NOT NULL, + PrivateKey char(64) NOT NULL, PRIMARY KEY (id) }; + +INSERT INTO whitelist (PublicKey, "Limit") VALUES ("0x6d6dC708643A2782bE27191E2ABCae7E1B0cA38B", 100) + +INSERT INTO admin (PublicKey, PrivateKey) VALUES ("0x6FA59C2C8EAaCC1f8875794f2DAe145Bb32Bd9B1", "3d4ba04a9c7b159a998d146760cba981ea05784404e38c6fa0a2fe852fbdd648") diff --git a/packages/govern-tx/src/Bootstrap.ts b/packages/govern-tx/src/Bootstrap.ts index b3d555760..980b4738c 100644 --- a/packages/govern-tx/src/Bootstrap.ts +++ b/packages/govern-tx/src/Bootstrap.ts @@ -20,6 +20,7 @@ import DeleteItemAction from './whitelist/DeleteItemAction' import GetListAction from './whitelist/GetListAction' import AbstractTransaction from '../lib/transactions/AbstractTransaction' import AbstractWhitelistAction from '../lib/whitelist/AbstractWhitelistAction' +import { AuthenticatedRequest } from './auth/Authenticator'; export default class Bootstrap { /** @@ -109,7 +110,13 @@ export default class Bootstrap { '/execute', {schema: AbstractTransaction.schema}, (request): Promise => { - return new ExecuteTransaction(this.config.ethereum, this.provider, request.params as Request).execute() + return new ExecuteTransaction( + this.config.ethereum, + this.provider, + this.whitelist, + request.params as Request, + (request as AuthenticatedRequest).publicKey + ).execute() } ) @@ -117,7 +124,13 @@ export default class Bootstrap { '/schedule', {schema: AbstractTransaction.schema}, (request): Promise => { - return new ScheduleTransaction(this.config.ethereum, this.provider, request.params as Request).execute() + return new ScheduleTransaction( + this.config.ethereum, + this.provider, + this.whitelist, + request.params as Request, + (request as AuthenticatedRequest).publicKey + ).execute() } ) @@ -125,7 +138,13 @@ export default class Bootstrap { '/challenge', {schema: AbstractTransaction.schema}, (request): Promise => { - return new ChallengeTransaction(this.config.ethereum, this.provider, request.params as Request).execute() + return new ChallengeTransaction( + this.config.ethereum, + this.provider, + this.whitelist, + request.params as Request, + (request as AuthenticatedRequest).publicKey + ).execute() } ) } diff --git a/packages/govern-tx/src/auth/Authenticator.ts b/packages/govern-tx/src/auth/Authenticator.ts index 31ff3e6a2..bf9ba80bc 100644 --- a/packages/govern-tx/src/auth/Authenticator.ts +++ b/packages/govern-tx/src/auth/Authenticator.ts @@ -5,6 +5,10 @@ import { Unauthorized, HttpError } from 'http-errors' import Whitelist from '../db/Whitelist' import Admin from '../db/Admin'; +export interface AuthenticatedRequest extends FastifyRequest { + publicKey: string +} + export default class Authenticator { /** * @property {HttpError} NOT_ALLOWED @@ -34,17 +38,17 @@ export default class Authenticator { * @public */ public async authenticate(request: FastifyRequest): Promise { + //@ts-ignore + const publicKey = verifyMessage(arrayify(request.body.message), request.body.signature) + if ( await this.hasPermission( request.routerPath, - verifyMessage( - //@ts-ignore - arrayify(request.body.message), - //@ts-ignore - request.body.signature - ) + publicKey ) ) { + (request as AuthenticatedRequest).publicKey = publicKey + return } diff --git a/packages/govern-tx/src/db/Whitelist.ts b/packages/govern-tx/src/db/Whitelist.ts index ab8304ce7..d3c325262 100644 --- a/packages/govern-tx/src/db/Whitelist.ts +++ b/packages/govern-tx/src/db/Whitelist.ts @@ -39,7 +39,7 @@ export default class Whitelist { * @public */ public async keyExists(publicKey: string): Promise { - return (await this.getItemByKey(publicKey)).length > 0; + return (await this.getItemByKey(publicKey)).length > 0 } /** @@ -70,7 +70,7 @@ export default class Whitelist { * @public */ public addItem(publicKey: string, rateLimit: number): Promise { - return this.db.query(`INSERT INTO whitelist VALUES (${publicKey}, ${rateLimit})`); + return this.db.query(`INSERT INTO whitelist VALUES (${publicKey}, ${rateLimit})`) } /** @@ -85,6 +85,21 @@ export default class Whitelist { * @public */ public async deleteItem(publicKey: string): Promise { - return (await this.db.query(`DELETE FROM whitelist WHERE PublicKey='${publicKey}'`)).length > 0; + return (await this.db.query(`DELETE FROM whitelist WHERE PublicKey='${publicKey}'`)).length > 0 + } + + /** + * Increases the execution counter + * + * @method increaseExecutionCounter + * + * @param {string} publicKey + * + * @returns {Promise} + * + * @public + */ + public async increaseExecutionCounter(publicKey: string): Promise { + return this.db.query(`UPDATE whitelist SET Executed = Executed + 1 WHERE PublicKey='${publicKey}'`) } } From 9ccbb949bbe38f81c02416ebe0fc12c4c28e947f Mon Sep 17 00:00:00 2001 From: nivida Date: Fri, 11 Dec 2020 11:26:06 +0100 Subject: [PATCH 082/107] CreateTransaction implemented and admin handling improved --- packages/govern-tx/config/dev.config.ts | 3 +- packages/govern-tx/config/prod.config.ts | 3 +- .../lib/transactions/AbstractTransaction.ts | 8 +++- packages/govern-tx/src/Bootstrap.ts | 25 +++++++++-- packages/govern-tx/src/auth/Authenticator.ts | 20 +++++---- .../transactions/create/CreateTransaction.ts | 23 ++++++++++ .../src/transactions/create/create.json | 45 +++++++++++++++++++ 7 files changed, 112 insertions(+), 15 deletions(-) create mode 100644 packages/govern-tx/src/transactions/create/CreateTransaction.ts create mode 100644 packages/govern-tx/src/transactions/create/create.json diff --git a/packages/govern-tx/config/dev.config.ts b/packages/govern-tx/config/dev.config.ts index aac306fc7..f67d2ae34 100644 --- a/packages/govern-tx/config/dev.config.ts +++ b/packages/govern-tx/config/dev.config.ts @@ -4,7 +4,8 @@ export const config: Config = { ethereum: { publicKey: '', // Add default EOA contracts: { - GovernQueue: '' // Add deployment address can get calculated if deployed with create2 + GovernQueue: '', // Add deployment address can get calculated if deployed with create2 + GovernBaseFactory: '' }, url: 'http://localhost:8545', blockConfirmations: 42 diff --git a/packages/govern-tx/config/prod.config.ts b/packages/govern-tx/config/prod.config.ts index e1c4d7a83..83db549ab 100644 --- a/packages/govern-tx/config/prod.config.ts +++ b/packages/govern-tx/config/prod.config.ts @@ -4,7 +4,8 @@ export const config: Config = { ethereum: { publicKey: '', // Add default EOA contracts: { - GovernQueue: '' // Add deployment address can get calculated if deployed with create2 + GovernQueue: '', // Add deployment address can get calculated if deployed with create2 + GovernBaseFactory: '' }, url: 'localhost:8545', blockConfirmations: 42 diff --git a/packages/govern-tx/lib/transactions/AbstractTransaction.ts b/packages/govern-tx/lib/transactions/AbstractTransaction.ts index e11ba8e1e..18224fd23 100644 --- a/packages/govern-tx/lib/transactions/AbstractTransaction.ts +++ b/packages/govern-tx/lib/transactions/AbstractTransaction.ts @@ -40,7 +40,8 @@ export default abstract class AbstractTransaction extends AbstractAction { private provider: Provider, private whitelist: Whitelist, request: Request, - private publicKey: string + private publicKey: string, + private admin: boolean ) { super(request); } @@ -65,7 +66,10 @@ export default abstract class AbstractTransaction extends AbstractAction { let receipt: TransactionReceipt; receipt = await this.provider.sendTransaction(this.contract, contractFunction) - this.whitelist.increaseExecutionCounter(this.publicKey) + + if(!this.admin) { + this.whitelist.increaseExecutionCounter(this.publicKey) + } return receipt; } diff --git a/packages/govern-tx/src/Bootstrap.ts b/packages/govern-tx/src/Bootstrap.ts index 980b4738c..2cbe0180e 100644 --- a/packages/govern-tx/src/Bootstrap.ts +++ b/packages/govern-tx/src/Bootstrap.ts @@ -14,6 +14,7 @@ import Authenticator from './auth/Authenticator' import ExecuteTransaction from './transactions/execute/ExecuteTransaction' import ChallengeTransaction from './transactions/challenge/ChallengeTransaction' import ScheduleTransaction from './transactions/schedule/ScheduleTransaction' +import CreateTransaction from './transactions/create/CreateTransaction'; import AddItemAction from './whitelist/AddItemAction' import DeleteItemAction from './whitelist/DeleteItemAction' @@ -115,7 +116,8 @@ export default class Bootstrap { this.provider, this.whitelist, request.params as Request, - (request as AuthenticatedRequest).publicKey + (request as AuthenticatedRequest).publicKey, + (request as AuthenticatedRequest).admin ).execute() } ) @@ -129,7 +131,8 @@ export default class Bootstrap { this.provider, this.whitelist, request.params as Request, - (request as AuthenticatedRequest).publicKey + (request as AuthenticatedRequest).publicKey, + (request as AuthenticatedRequest).admin ).execute() } ) @@ -143,7 +146,23 @@ export default class Bootstrap { this.provider, this.whitelist, request.params as Request, - (request as AuthenticatedRequest).publicKey + (request as AuthenticatedRequest).publicKey, + (request as AuthenticatedRequest).admin + ).execute() + } + ) + + this.server.post( + '/create', + {schema: AbstractTransaction.schema}, + (request): Promise => { + return new CreateTransaction( + this.config.ethereum, + this.provider, + this.whitelist, + request.params as Request, + (request as AuthenticatedRequest).publicKey, + (request as AuthenticatedRequest).admin ).execute() } ) diff --git a/packages/govern-tx/src/auth/Authenticator.ts b/packages/govern-tx/src/auth/Authenticator.ts index bf9ba80bc..352dc6341 100644 --- a/packages/govern-tx/src/auth/Authenticator.ts +++ b/packages/govern-tx/src/auth/Authenticator.ts @@ -7,6 +7,7 @@ import Admin from '../db/Admin'; export interface AuthenticatedRequest extends FastifyRequest { publicKey: string + admin: boolean } export default class Authenticator { @@ -43,7 +44,7 @@ export default class Authenticator { if ( await this.hasPermission( - request.routerPath, + request, publicKey ) ) { @@ -60,22 +61,25 @@ export default class Authenticator { * * @method hasPermission * - * @param {string} routerPath + * @param {FastifyRequest} request * @param {string} publicKey * * @returns {Promise} * * @private */ - private async hasPermission(routerPath: string, publicKey: string): Promise { - if ( - routerPath !== '/whitelist' && - (await this.whitelist.keyExists(publicKey) || await this.admin.isAdmin(publicKey)) - ) { + private async hasPermission(request: FastifyRequest, publicKey: string): Promise { + if (await this.admin.isAdmin(publicKey)) { + (request as AuthenticatedRequest).admin = true + return true } - if (routerPath === '/whitelist' && await this.admin.isAdmin(publicKey)) { + if ( + request.routerPath !== '/whitelist' && await this.whitelist.keyExists(publicKey) + ) { + (request as AuthenticatedRequest).admin = false + return true } diff --git a/packages/govern-tx/src/transactions/create/CreateTransaction.ts b/packages/govern-tx/src/transactions/create/CreateTransaction.ts new file mode 100644 index 000000000..1c0de5fd0 --- /dev/null +++ b/packages/govern-tx/src/transactions/create/CreateTransaction.ts @@ -0,0 +1,23 @@ +import { JsonFragment } from '@ethersproject/abi'; +import AbstractTransaction from '../../../lib/transactions/AbstractTransaction' +import * as createABI from './create.json' + +export default class CreateTransaction extends AbstractTransaction { + /** + * The contract name + * + * @property {string} contract + * + * @protected + */ + protected contract: string = 'GovernBaseFactory' + + /** + * The function ABI used to create a transaction + * + * @property {JsonFragment} functionABI + * + * @protected + */ + protected functionABI: JsonFragment = createABI +} diff --git a/packages/govern-tx/src/transactions/create/create.json b/packages/govern-tx/src/transactions/create/create.json new file mode 100644 index 000000000..6d443c77a --- /dev/null +++ b/packages/govern-tx/src/transactions/create/create.json @@ -0,0 +1,45 @@ +{ + "inputs": [ + { + "internalType": "string", + "name": "_name", + "type": "string" + }, + { + "internalType": "contract IERC20", + "name": "_token", + "type": "address" + }, + { + "internalType": "string", + "name": "_tokenName", + "type": "string" + }, + { + "internalType": "string", + "name": "_tokenSymbol", + "type": "string" + }, + { + "internalType": "bool", + "name": "_useProxies", + "type": "bool" + } + ], + "name": "newGovernWithoutConfig", + "outputs": [ + { + "internalType": "contract Govern", + "name": "govern", + "type": "address" + }, + { + "internalType": "contract GovernQueue", + "name": "queue", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "function" + } + \ No newline at end of file From 01671f3c76e5288e51765bf61d31bc10160adb7f Mon Sep 17 00:00:00 2001 From: nivida Date: Fri, 11 Dec 2020 11:33:59 +0100 Subject: [PATCH 083/107] Whitelist.limitReached implemented and Authenticator updated --- .../lib/transactions/AbstractTransaction.ts | 7 ++-- packages/govern-tx/src/auth/Authenticator.ts | 4 ++- packages/govern-tx/src/db/Whitelist.ts | 32 +++++++++++++++---- 3 files changed, 30 insertions(+), 13 deletions(-) diff --git a/packages/govern-tx/lib/transactions/AbstractTransaction.ts b/packages/govern-tx/lib/transactions/AbstractTransaction.ts index 18224fd23..c5c1578a3 100644 --- a/packages/govern-tx/lib/transactions/AbstractTransaction.ts +++ b/packages/govern-tx/lib/transactions/AbstractTransaction.ts @@ -60,12 +60,9 @@ export default abstract class AbstractTransaction extends AbstractAction { this.functionABI, (this.request as Request).message ) - contractFunction.functionArguments[0].payload.submitter = this.config.publicKey - - let receipt: TransactionReceipt; - - receipt = await this.provider.sendTransaction(this.contract, contractFunction) + + let receipt: TransactionReceipt = await this.provider.sendTransaction(this.contract, contractFunction) if(!this.admin) { this.whitelist.increaseExecutionCounter(this.publicKey) diff --git a/packages/govern-tx/src/auth/Authenticator.ts b/packages/govern-tx/src/auth/Authenticator.ts index 352dc6341..a4154e80f 100644 --- a/packages/govern-tx/src/auth/Authenticator.ts +++ b/packages/govern-tx/src/auth/Authenticator.ts @@ -76,7 +76,9 @@ export default class Authenticator { } if ( - request.routerPath !== '/whitelist' && await this.whitelist.keyExists(publicKey) + request.routerPath !== '/whitelist' && + await this.whitelist.keyExists(publicKey) && + !(await this.whitelist.limitReached(publicKey)) ) { (request as AuthenticatedRequest).admin = false diff --git a/packages/govern-tx/src/db/Whitelist.ts b/packages/govern-tx/src/db/Whitelist.ts index d3c325262..c15d091f3 100644 --- a/packages/govern-tx/src/db/Whitelist.ts +++ b/packages/govern-tx/src/db/Whitelist.ts @@ -1,11 +1,10 @@ import Database from './Database' -// TODO: Define DB schema to handle global rate limits and to have Admin public key - export interface ListItem { + ID: number PublicKey: string, - RateLimit: number, - ExecutedTransactions: number + Limit: number, + Executed: number } export default class Whitelist { @@ -28,6 +27,8 @@ export default class Whitelist { } /** + * TODO: Check return values of postgres package + * * Checks if a key is existing * * @method keyExists @@ -39,7 +40,7 @@ export default class Whitelist { * @public */ public async keyExists(publicKey: string): Promise { - return (await this.getItemByKey(publicKey)).length > 0 + return typeof (await this.getItemByKey(publicKey)).ID !== 'undefined' } /** @@ -49,11 +50,11 @@ export default class Whitelist { * * @param {string} publicKey - The public key to look for * - * @returns {Promise} + * @returns {Promise} * * @public */ - public getItemByKey(publicKey: string): Promise { + public getItemByKey(publicKey: string): Promise { return this.db.query(`SELECT * FROM whitelist WHERE PublicKey='${publicKey}'`) } @@ -102,4 +103,21 @@ export default class Whitelist { public async increaseExecutionCounter(publicKey: string): Promise { return this.db.query(`UPDATE whitelist SET Executed = Executed + 1 WHERE PublicKey='${publicKey}'`) } + + /** + * Returns true if the limit isn't reached otherwise false + * + * @method increaseExecutionCounter + * + * @param {string} publicKey + * + * @returns {Promise} + * + * @public + */ + public async limitReached(publicKey: string): Promise { + const item: ListItem = await this.getItemByKey(publicKey) + + return (item.Executed + 1) >= item.Limit + } } From cdf10815683f0215e5caa168c7f6f249e2339d9e Mon Sep 17 00:00:00 2001 From: nivida Date: Fri, 11 Dec 2020 11:35:42 +0100 Subject: [PATCH 084/107] AdminItem interface updated --- packages/govern-tx/src/db/Admin.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/govern-tx/src/db/Admin.ts b/packages/govern-tx/src/db/Admin.ts index 6626f39ac..ca2209486 100644 --- a/packages/govern-tx/src/db/Admin.ts +++ b/packages/govern-tx/src/db/Admin.ts @@ -1,7 +1,9 @@ import Database from './Database' export interface AdminItem { + ID: number PublicKey: string + PrivateKey: string } export default class Admin { From 0f3417cde29cb5a937ddb7de36bf469ac4f82038 Mon Sep 17 00:00:00 2001 From: nivida Date: Fri, 11 Dec 2020 11:45:49 +0100 Subject: [PATCH 085/107] request handling simplified --- packages/govern-tx/lib/AbstractAction.ts | 10 ++++---- .../lib/transactions/AbstractTransaction.ts | 18 ++++++------- .../lib/whitelist/AbstractWhitelistAction.ts | 8 +++--- packages/govern-tx/src/Bootstrap.ts | 25 ++++++------------- .../govern-tx/src/whitelist/AddItemAction.ts | 17 +++++++------ .../src/whitelist/DeleteItemAction.ts | 14 +++++------ 6 files changed, 42 insertions(+), 50 deletions(-) diff --git a/packages/govern-tx/lib/AbstractAction.ts b/packages/govern-tx/lib/AbstractAction.ts index 3b5fc8dc6..d7bacf3af 100644 --- a/packages/govern-tx/lib/AbstractAction.ts +++ b/packages/govern-tx/lib/AbstractAction.ts @@ -1,6 +1,6 @@ -import { FastifySchema } from 'fastify' +import { FastifySchema, FastifyRequest } from 'fastify' -export interface Request { +export interface Params { message: string | any, signature: string, publicKey: string @@ -12,14 +12,14 @@ export default abstract class AbstractAction { * * @var {Request} parameters */ - protected request: Request | undefined; + protected request: FastifyRequest; /** * @param {Request} parameters * * @constructor */ - constructor(request: Request | undefined) { + constructor(request: FastifyRequest) { this.request = this.validateRequest(request); } @@ -34,7 +34,7 @@ export default abstract class AbstractAction { * * @protected */ - protected validateRequest(request: Request | undefined): Request | undefined { + protected validateRequest(request: FastifyRequest): FastifyRequest { return request; } diff --git a/packages/govern-tx/lib/transactions/AbstractTransaction.ts b/packages/govern-tx/lib/transactions/AbstractTransaction.ts index c5c1578a3..61d9e44e7 100644 --- a/packages/govern-tx/lib/transactions/AbstractTransaction.ts +++ b/packages/govern-tx/lib/transactions/AbstractTransaction.ts @@ -1,11 +1,13 @@ -import { FastifySchema } from 'fastify'; +import { FastifySchema, FastifyRequest } from 'fastify'; import { TransactionReceipt } from '@ethersproject/abstract-provider' import { JsonFragment } from '@ethersproject/abi'; -import AbstractAction, { Request } from '../AbstractAction' +import AbstractAction from '../AbstractAction' import ContractFunction from '../transactions/ContractFunction' import Provider from '../../src/provider/Provider' import { EthereumOptions } from '../../src/config/Configuration'; import Whitelist from '../../src/db/Whitelist'; +import { Params } from '../AbstractAction'; +import { AuthenticatedRequest } from '../../src/auth/Authenticator'; export default abstract class AbstractTransaction extends AbstractAction { /** @@ -39,9 +41,7 @@ export default abstract class AbstractTransaction extends AbstractAction { private config: EthereumOptions, private provider: Provider, private whitelist: Whitelist, - request: Request, - private publicKey: string, - private admin: boolean + request: FastifyRequest, ) { super(request); } @@ -58,14 +58,14 @@ export default abstract class AbstractTransaction extends AbstractAction { public async execute(): Promise { const contractFunction = new ContractFunction( this.functionABI, - (this.request as Request).message + (this.request.params as Params).message ) contractFunction.functionArguments[0].payload.submitter = this.config.publicKey - + let receipt: TransactionReceipt = await this.provider.sendTransaction(this.contract, contractFunction) - if(!this.admin) { - this.whitelist.increaseExecutionCounter(this.publicKey) + if(!(this.request as AuthenticatedRequest).admin) { + this.whitelist.increaseExecutionCounter((this.request as AuthenticatedRequest).publicKey) } return receipt; diff --git a/packages/govern-tx/lib/whitelist/AbstractWhitelistAction.ts b/packages/govern-tx/lib/whitelist/AbstractWhitelistAction.ts index a99a017f0..8371660f0 100644 --- a/packages/govern-tx/lib/whitelist/AbstractWhitelistAction.ts +++ b/packages/govern-tx/lib/whitelist/AbstractWhitelistAction.ts @@ -1,8 +1,8 @@ -import { FastifySchema } from 'fastify'; -import AbstractAction, { Request } from '../AbstractAction' +import { FastifyRequest, FastifySchema } from 'fastify'; +import AbstractAction, { Params } from '../AbstractAction' import Whitelist, {ListItem} from '../../src/db/Whitelist' -export interface WhitelistRequest extends Request { +export interface WhitelistParams extends Params { message: { publicKey: string, rateLimit?: number @@ -16,7 +16,7 @@ export default abstract class AbstractWhitelistAction extends AbstractAction { * * @constructor */ - constructor(protected whitelist: Whitelist, request?: WhitelistRequest) { + constructor(protected whitelist: Whitelist, request: FastifyRequest) { super(request) } diff --git a/packages/govern-tx/src/Bootstrap.ts b/packages/govern-tx/src/Bootstrap.ts index 2cbe0180e..74f333094 100644 --- a/packages/govern-tx/src/Bootstrap.ts +++ b/packages/govern-tx/src/Bootstrap.ts @@ -1,7 +1,6 @@ import fastify, { FastifyInstance } from 'fastify' import { TransactionReceipt } from '@ethersproject/abstract-provider' -import { Request } from '../lib/AbstractAction' import Configuration from './config/Configuration' import Provider from './provider/Provider' import Wallet from './wallet/Wallet' @@ -115,9 +114,7 @@ export default class Bootstrap { this.config.ethereum, this.provider, this.whitelist, - request.params as Request, - (request as AuthenticatedRequest).publicKey, - (request as AuthenticatedRequest).admin + request ).execute() } ) @@ -130,9 +127,7 @@ export default class Bootstrap { this.config.ethereum, this.provider, this.whitelist, - request.params as Request, - (request as AuthenticatedRequest).publicKey, - (request as AuthenticatedRequest).admin + request ).execute() } ) @@ -145,9 +140,7 @@ export default class Bootstrap { this.config.ethereum, this.provider, this.whitelist, - request.params as Request, - (request as AuthenticatedRequest).publicKey, - (request as AuthenticatedRequest).admin + request ).execute() } ) @@ -160,9 +153,7 @@ export default class Bootstrap { this.config.ethereum, this.provider, this.whitelist, - request.params as Request, - (request as AuthenticatedRequest).publicKey, - (request as AuthenticatedRequest).admin + request ).execute() } ) @@ -182,7 +173,7 @@ export default class Bootstrap { '/whitelist', {schema: AbstractWhitelistAction.schema}, (request): Promise => { - return new AddItemAction(this.whitelist, request.params as Request).execute() + return new AddItemAction(this.whitelist, request).execute() } ) @@ -190,15 +181,15 @@ export default class Bootstrap { '/whitelist', {schema: AbstractWhitelistAction.schema}, (request): Promise => { - return new DeleteItemAction(this.whitelist, request.params as Request).execute() + return new DeleteItemAction(this.whitelist, request).execute() } ) this.server.get( '/whitelist', {schema: AbstractWhitelistAction.schema}, - (): Promise => { - return new GetListAction(this.whitelist).execute() + (request): Promise => { + return new GetListAction(this.whitelist, request).execute() } ) } diff --git a/packages/govern-tx/src/whitelist/AddItemAction.ts b/packages/govern-tx/src/whitelist/AddItemAction.ts index 8749c617d..f38aa2e8f 100644 --- a/packages/govern-tx/src/whitelist/AddItemAction.ts +++ b/packages/govern-tx/src/whitelist/AddItemAction.ts @@ -1,5 +1,6 @@ import {isAddress} from '@ethersproject/address' -import AbstractWhitelistAction, { WhitelistRequest } from "../../lib/whitelist/AbstractWhitelistAction" +import { FastifyRequest } from 'fastify' +import AbstractWhitelistAction, { WhitelistParams } from "../../lib/whitelist/AbstractWhitelistAction" import {ListItem} from '../db/Whitelist' export default class AddItemAction extends AbstractWhitelistAction { @@ -8,18 +9,18 @@ export default class AddItemAction extends AbstractWhitelistAction { * * @method validateRequest * - * @param {WhitelistRequest} request + * @param {FastifyRequest} request * - * @returns {WhitelistRequest} + * @returns {FastifyRequest} * * @protected */ - protected validateRequest(request: WhitelistRequest): WhitelistRequest { - if (!isAddress(request.message.publicKey)) { + protected validateRequest(request: FastifyRequest): FastifyRequest { + if (!isAddress((request.params as WhitelistParams).message.publicKey)) { throw new Error('Invalid public key passed!') } - if (request.message.rateLimit == 0) { + if ((request.params as WhitelistParams).message.rateLimit == 0) { throw new Error('Invalid rate limit passed!') } @@ -37,8 +38,8 @@ export default class AddItemAction extends AbstractWhitelistAction { */ public execute(): Promise { return this.whitelist.addItem( - (this.request as WhitelistRequest).message.publicKey, - ((this.request as WhitelistRequest).message.rateLimit as number) + (this.request.params as WhitelistParams).message.publicKey, + ((this.request.params as WhitelistParams).message.rateLimit as number) ) } } diff --git a/packages/govern-tx/src/whitelist/DeleteItemAction.ts b/packages/govern-tx/src/whitelist/DeleteItemAction.ts index f10c86463..080d0302e 100644 --- a/packages/govern-tx/src/whitelist/DeleteItemAction.ts +++ b/packages/govern-tx/src/whitelist/DeleteItemAction.ts @@ -1,6 +1,6 @@ import {isAddress} from '@ethersproject/address' -import AbstractWhitelistAction, { WhitelistRequest } from "../../lib/whitelist/AbstractWhitelistAction"; -import {ListItem} from '../db/Whitelist' +import { FastifyRequest } from 'fastify'; +import AbstractWhitelistAction, { WhitelistParams } from "../../lib/whitelist/AbstractWhitelistAction"; export default class DeleteItemAction extends AbstractWhitelistAction { /** @@ -8,14 +8,14 @@ export default class DeleteItemAction extends AbstractWhitelistAction { * * @method validateRequest * - * @param {WhitelistRequest} request + * @param {FastifyRequest} request * - * @returns {WhitelistRequest} + * @returns {FastifyRequest} * * @protected */ - protected validateRequest(request: WhitelistRequest): WhitelistRequest { - if (!isAddress(request.message.publicKey)) { + protected validateRequest(request: FastifyRequest): FastifyRequest { + if (!isAddress((this.request.params as WhitelistParams).message.publicKey)) { throw new Error('Invalid public key passed!') } @@ -32,6 +32,6 @@ export default class DeleteItemAction extends AbstractWhitelistAction { * @public */ public execute(): Promise { - return this.whitelist.deleteItem((this.request as WhitelistRequest).message.publicKey); + return this.whitelist.deleteItem((this.request.params as WhitelistParams).message.publicKey); } } From 06681d2e6ae72f1001c2d3d2138e4fd27f555e67 Mon Sep 17 00:00:00 2001 From: nivida Date: Fri, 11 Dec 2020 11:47:52 +0100 Subject: [PATCH 086/107] types improved --- packages/govern-tx/lib/AbstractAction.ts | 1 - packages/govern-tx/src/auth/Authenticator.ts | 9 +++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/govern-tx/lib/AbstractAction.ts b/packages/govern-tx/lib/AbstractAction.ts index d7bacf3af..cba773d4a 100644 --- a/packages/govern-tx/lib/AbstractAction.ts +++ b/packages/govern-tx/lib/AbstractAction.ts @@ -3,7 +3,6 @@ import { FastifySchema, FastifyRequest } from 'fastify' export interface Params { message: string | any, signature: string, - publicKey: string } export default abstract class AbstractAction { diff --git a/packages/govern-tx/src/auth/Authenticator.ts b/packages/govern-tx/src/auth/Authenticator.ts index a4154e80f..7e9b55dc9 100644 --- a/packages/govern-tx/src/auth/Authenticator.ts +++ b/packages/govern-tx/src/auth/Authenticator.ts @@ -4,6 +4,7 @@ import { arrayify } from '@ethersproject/bytes' import { Unauthorized, HttpError } from 'http-errors' import Whitelist from '../db/Whitelist' import Admin from '../db/Admin'; +import { Params } from '../../lib/AbstractAction'; export interface AuthenticatedRequest extends FastifyRequest { publicKey: string @@ -39,8 +40,12 @@ export default class Authenticator { * @public */ public async authenticate(request: FastifyRequest): Promise { - //@ts-ignore - const publicKey = verifyMessage(arrayify(request.body.message), request.body.signature) + const publicKey = verifyMessage( + arrayify( + (request.body as Params).message + ), + (request.body as Params).signature + ) if ( await this.hasPermission( From 6c0056d0c4c18f80a8bea19c177a13b0a0d4c740 Mon Sep 17 00:00:00 2001 From: nivida Date: Fri, 11 Dec 2020 12:00:08 +0100 Subject: [PATCH 087/107] unused import removed and important TODO added to Authenticator --- packages/govern-tx/src/Bootstrap.ts | 1 - packages/govern-tx/src/auth/Authenticator.ts | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/govern-tx/src/Bootstrap.ts b/packages/govern-tx/src/Bootstrap.ts index 74f333094..6ff4bf577 100644 --- a/packages/govern-tx/src/Bootstrap.ts +++ b/packages/govern-tx/src/Bootstrap.ts @@ -20,7 +20,6 @@ import DeleteItemAction from './whitelist/DeleteItemAction' import GetListAction from './whitelist/GetListAction' import AbstractTransaction from '../lib/transactions/AbstractTransaction' import AbstractWhitelistAction from '../lib/whitelist/AbstractWhitelistAction' -import { AuthenticatedRequest } from './auth/Authenticator'; export default class Bootstrap { /** diff --git a/packages/govern-tx/src/auth/Authenticator.ts b/packages/govern-tx/src/auth/Authenticator.ts index 7e9b55dc9..af4eba1d2 100644 --- a/packages/govern-tx/src/auth/Authenticator.ts +++ b/packages/govern-tx/src/auth/Authenticator.ts @@ -80,6 +80,7 @@ export default class Authenticator { return true } + // TODO: Fire only one SQL statement to check if the key exists and if the limit is reached if ( request.routerPath !== '/whitelist' && await this.whitelist.keyExists(publicKey) && From d50f98895339f6c278c20ccdfc83fd9197a686d8 Mon Sep 17 00:00:00 2001 From: nivida Date: Fri, 11 Dec 2020 12:24:41 +0100 Subject: [PATCH 088/107] Authenticator error handling updated to not leak any internal information --- packages/govern-tx/src/auth/Authenticator.ts | 39 +++++++++++--------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/packages/govern-tx/src/auth/Authenticator.ts b/packages/govern-tx/src/auth/Authenticator.ts index af4eba1d2..44903c66d 100644 --- a/packages/govern-tx/src/auth/Authenticator.ts +++ b/packages/govern-tx/src/auth/Authenticator.ts @@ -40,24 +40,29 @@ export default class Authenticator { * @public */ public async authenticate(request: FastifyRequest): Promise { - const publicKey = verifyMessage( - arrayify( - (request.body as Params).message - ), - (request.body as Params).signature - ) - - if ( - await this.hasPermission( - request, - publicKey + console.log(request.body) + try { + const publicKey = verifyMessage( + arrayify( + (request.body as Params).message + ), + (request.body as Params).signature ) - ) { - (request as AuthenticatedRequest).publicKey = publicKey - - return - } - + + if ( + await this.hasPermission( + request, + publicKey + ) + ) { + (request as AuthenticatedRequest).publicKey = publicKey + + return + } + } catch(e) { + throw this.NOT_ALLOWED + } + throw this.NOT_ALLOWED } From eba373de3b6ef28a3ea05b3734c14dd11bdaac0b Mon Sep 17 00:00:00 2001 From: nivida Date: Fri, 11 Dec 2020 12:49:11 +0100 Subject: [PATCH 089/107] changes from Auth testing --- packages/govern-tx/postgres/init.sql | 2 +- packages/govern-tx/src/auth/Authenticator.ts | 40 +++++++++----------- packages/govern-tx/src/db/Database.ts | 2 +- 3 files changed, 20 insertions(+), 24 deletions(-) diff --git a/packages/govern-tx/postgres/init.sql b/packages/govern-tx/postgres/init.sql index 07756f461..ba72235f5 100644 --- a/packages/govern-tx/postgres/init.sql +++ b/packages/govern-tx/postgres/init.sql @@ -6,7 +6,7 @@ CREATE TABLE whitelist { PRIMARY KEY (id) }; -CREATE TABLE admin { +CREATE TABLE admins { ID int NOT NULL AUTO_INCREMENT, PublicKey char(42) NOT NULL, PrivateKey char(64) NOT NULL, diff --git a/packages/govern-tx/src/auth/Authenticator.ts b/packages/govern-tx/src/auth/Authenticator.ts index 44903c66d..a1813045f 100644 --- a/packages/govern-tx/src/auth/Authenticator.ts +++ b/packages/govern-tx/src/auth/Authenticator.ts @@ -40,29 +40,25 @@ export default class Authenticator { * @public */ public async authenticate(request: FastifyRequest): Promise { - console.log(request.body) - try { - const publicKey = verifyMessage( - arrayify( - (request.body as Params).message - ), - (request.body as Params).signature + request.body = JSON.parse((request.body as string)) + const publicKey = verifyMessage( + arrayify( + (request.body as Params).message + ), + (request.body as Params).signature + ) + + if ( + await this.hasPermission( + request, + publicKey ) - - if ( - await this.hasPermission( - request, - publicKey - ) - ) { - (request as AuthenticatedRequest).publicKey = publicKey - - return - } - } catch(e) { - throw this.NOT_ALLOWED - } - + ) { + (request as AuthenticatedRequest).publicKey = publicKey + + return + } + throw this.NOT_ALLOWED } diff --git a/packages/govern-tx/src/db/Database.ts b/packages/govern-tx/src/db/Database.ts index 64ca19457..680a8b6e4 100644 --- a/packages/govern-tx/src/db/Database.ts +++ b/packages/govern-tx/src/db/Database.ts @@ -52,6 +52,6 @@ export default class Database { * @public */ public query(query: string): Promise { - return this.sql(query) + return this.sql.unsafe(query) // TODO: Change back to normal call of the sql function } } From 6ee9493c613852031495efae74d552d7c820a6d4 Mon Sep 17 00:00:00 2001 From: nivida Date: Fri, 11 Dec 2020 13:10:25 +0100 Subject: [PATCH 090/107] init.sql corrected to postgres sql syntax --- packages/govern-tx/postgres/init.sql | 28 +++++++++----------- packages/govern-tx/src/auth/Authenticator.ts | 1 + 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/packages/govern-tx/postgres/init.sql b/packages/govern-tx/postgres/init.sql index ba72235f5..033b365d2 100644 --- a/packages/govern-tx/postgres/init.sql +++ b/packages/govern-tx/postgres/init.sql @@ -1,18 +1,16 @@ -CREATE TABLE whitelist { - ID int NOT NULL AUTO_INCREMENT, - PublicKey char(42) NOT NULL, - "Limit" int DEFAULT 100 NOT NULL, - Executed int DEFAULT 0 NOT NULL, - PRIMARY KEY (id) -}; +CREATE TABLE whitelist ( + ID SERIAL PRIMARY KEY, + PublicKey CHAR(42) NOT NULL, + TxLimit INT DEFAULT 100 NOT NULL, + Executed INT DEFAULT 0 NOT NULL +); -CREATE TABLE admins { - ID int NOT NULL AUTO_INCREMENT, - PublicKey char(42) NOT NULL, - PrivateKey char(64) NOT NULL, - PRIMARY KEY (id) -}; +CREATE TABLE admins ( + ID SERIAL PRIMARY KEY, + PublicKey CHAR(42) NOT NULL, + PrivateKey CHAR(64) NOT NULL +); -INSERT INTO whitelist (PublicKey, "Limit") VALUES ("0x6d6dC708643A2782bE27191E2ABCae7E1B0cA38B", 100) +INSERT INTO whitelist (PublicKey, TxLimit) VALUES ('0x6d6dC708643A2782bE27191E2ABCae7E1B0cA38B', '100'); -INSERT INTO admin (PublicKey, PrivateKey) VALUES ("0x6FA59C2C8EAaCC1f8875794f2DAe145Bb32Bd9B1", "3d4ba04a9c7b159a998d146760cba981ea05784404e38c6fa0a2fe852fbdd648") +INSERT INTO admins (PublicKey, PrivateKey) VALUES ('0x6d6dC708643A2782bE27191E2ABCae7E1B0cA38B', '3d4ba04a9c7b159a998d146760cba981ea05784404e38c6fa0a2fe852fbdd648'); diff --git a/packages/govern-tx/src/auth/Authenticator.ts b/packages/govern-tx/src/auth/Authenticator.ts index a1813045f..7cb921705 100644 --- a/packages/govern-tx/src/auth/Authenticator.ts +++ b/packages/govern-tx/src/auth/Authenticator.ts @@ -41,6 +41,7 @@ export default class Authenticator { */ public async authenticate(request: FastifyRequest): Promise { request.body = JSON.parse((request.body as string)) + const publicKey = verifyMessage( arrayify( (request.body as Params).message From 5f120510514ee823469b19aba148ff4aee95a09b Mon Sep 17 00:00:00 2001 From: nivida Date: Fri, 11 Dec 2020 13:20:33 +0100 Subject: [PATCH 091/107] Authenticator updated --- packages/govern-tx/src/auth/Authenticator.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/govern-tx/src/auth/Authenticator.ts b/packages/govern-tx/src/auth/Authenticator.ts index 7cb921705..7f74d4fa1 100644 --- a/packages/govern-tx/src/auth/Authenticator.ts +++ b/packages/govern-tx/src/auth/Authenticator.ts @@ -41,7 +41,7 @@ export default class Authenticator { */ public async authenticate(request: FastifyRequest): Promise { request.body = JSON.parse((request.body as string)) - + const publicKey = verifyMessage( arrayify( (request.body as Params).message @@ -76,7 +76,7 @@ export default class Authenticator { * @private */ private async hasPermission(request: FastifyRequest, publicKey: string): Promise { - if (await this.admin.isAdmin(publicKey)) { + if (request.routerPath === '/whitelist' && await this.admin.isAdmin(publicKey)) { (request as AuthenticatedRequest).admin = true return true @@ -85,8 +85,10 @@ export default class Authenticator { // TODO: Fire only one SQL statement to check if the key exists and if the limit is reached if ( request.routerPath !== '/whitelist' && - await this.whitelist.keyExists(publicKey) && - !(await this.whitelist.limitReached(publicKey)) + ( + (await this.whitelist.keyExists(publicKey) && !(await this.whitelist.limitReached(publicKey))) || + await this.admin.isAdmin(publicKey) + ) ) { (request as AuthenticatedRequest).admin = false From fd5b73244f1869e9d5a7b4a7256072fa3fe14bfb Mon Sep 17 00:00:00 2001 From: nivida Date: Fri, 11 Dec 2020 13:27:58 +0100 Subject: [PATCH 092/107] preHandler does already have the body parsed.. postman autocomplete cheeked me --- packages/govern-tx/src/auth/Authenticator.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/govern-tx/src/auth/Authenticator.ts b/packages/govern-tx/src/auth/Authenticator.ts index 7f74d4fa1..96494ab7c 100644 --- a/packages/govern-tx/src/auth/Authenticator.ts +++ b/packages/govern-tx/src/auth/Authenticator.ts @@ -40,8 +40,6 @@ export default class Authenticator { * @public */ public async authenticate(request: FastifyRequest): Promise { - request.body = JSON.parse((request.body as string)) - const publicKey = verifyMessage( arrayify( (request.body as Params).message From 3f264621033c730606b23e537c31591e03839fe0 Mon Sep 17 00:00:00 2001 From: nivida Date: Fri, 11 Dec 2020 13:34:39 +0100 Subject: [PATCH 093/107] request body handling adjusted for whitelist actions --- .../govern-tx/lib/whitelist/AbstractWhitelistAction.ts | 2 +- packages/govern-tx/postgres/init.sql | 4 ++-- packages/govern-tx/src/whitelist/AddItemAction.ts | 8 ++++---- packages/govern-tx/src/whitelist/DeleteItemAction.ts | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/govern-tx/lib/whitelist/AbstractWhitelistAction.ts b/packages/govern-tx/lib/whitelist/AbstractWhitelistAction.ts index 8371660f0..905509e7b 100644 --- a/packages/govern-tx/lib/whitelist/AbstractWhitelistAction.ts +++ b/packages/govern-tx/lib/whitelist/AbstractWhitelistAction.ts @@ -5,7 +5,7 @@ import Whitelist, {ListItem} from '../../src/db/Whitelist' export interface WhitelistParams extends Params { message: { publicKey: string, - rateLimit?: number + txLimit?: number } } diff --git a/packages/govern-tx/postgres/init.sql b/packages/govern-tx/postgres/init.sql index 033b365d2..c235b9b79 100644 --- a/packages/govern-tx/postgres/init.sql +++ b/packages/govern-tx/postgres/init.sql @@ -11,6 +11,6 @@ CREATE TABLE admins ( PrivateKey CHAR(64) NOT NULL ); -INSERT INTO whitelist (PublicKey, TxLimit) VALUES ('0x6d6dC708643A2782bE27191E2ABCae7E1B0cA38B', '100'); +INSERT INTO whitelist (PublicKey, TxLimit) VALUES ('0x6FA59C2C8EAaCC1f8875794f2DAe145Bb32Bd9B1', '100'); -INSERT INTO admins (PublicKey, PrivateKey) VALUES ('0x6d6dC708643A2782bE27191E2ABCae7E1B0cA38B', '3d4ba04a9c7b159a998d146760cba981ea05784404e38c6fa0a2fe852fbdd648'); +INSERT INTO admins (PublicKey, PrivateKey) VALUES ('0x6FA59C2C8EAaCC1f8875794f2DAe145Bb32Bd9B1', '3d4ba04a9c7b159a998d146760cba981ea05784404e38c6fa0a2fe852fbdd648'); diff --git a/packages/govern-tx/src/whitelist/AddItemAction.ts b/packages/govern-tx/src/whitelist/AddItemAction.ts index f38aa2e8f..f6988deed 100644 --- a/packages/govern-tx/src/whitelist/AddItemAction.ts +++ b/packages/govern-tx/src/whitelist/AddItemAction.ts @@ -16,11 +16,11 @@ export default class AddItemAction extends AbstractWhitelistAction { * @protected */ protected validateRequest(request: FastifyRequest): FastifyRequest { - if (!isAddress((request.params as WhitelistParams).message.publicKey)) { + if (!isAddress((request.body as WhitelistParams).message.publicKey)) { throw new Error('Invalid public key passed!') } - if ((request.params as WhitelistParams).message.rateLimit == 0) { + if ((request.body as WhitelistParams).message.txLimit == 0) { throw new Error('Invalid rate limit passed!') } @@ -38,8 +38,8 @@ export default class AddItemAction extends AbstractWhitelistAction { */ public execute(): Promise { return this.whitelist.addItem( - (this.request.params as WhitelistParams).message.publicKey, - ((this.request.params as WhitelistParams).message.rateLimit as number) + (this.request.body as WhitelistParams).message.publicKey, + ((this.request.body as WhitelistParams).message.txLimit as number) ) } } diff --git a/packages/govern-tx/src/whitelist/DeleteItemAction.ts b/packages/govern-tx/src/whitelist/DeleteItemAction.ts index 080d0302e..1135076fe 100644 --- a/packages/govern-tx/src/whitelist/DeleteItemAction.ts +++ b/packages/govern-tx/src/whitelist/DeleteItemAction.ts @@ -15,7 +15,7 @@ export default class DeleteItemAction extends AbstractWhitelistAction { * @protected */ protected validateRequest(request: FastifyRequest): FastifyRequest { - if (!isAddress((this.request.params as WhitelistParams).message.publicKey)) { + if (!isAddress((this.request.body as WhitelistParams).message.publicKey)) { throw new Error('Invalid public key passed!') } @@ -32,6 +32,6 @@ export default class DeleteItemAction extends AbstractWhitelistAction { * @public */ public execute(): Promise { - return this.whitelist.deleteItem((this.request.params as WhitelistParams).message.publicKey); + return this.whitelist.deleteItem((this.request.body as WhitelistParams).message.publicKey); } } From 0429fa0a2b4e2f200ead10b06925c4ac3265bdca Mon Sep 17 00:00:00 2001 From: nivida Date: Fri, 11 Dec 2020 14:13:10 +0100 Subject: [PATCH 094/107] Authenticator updated to handle whitelist and tx actions, sql schema improved, AddItemAction tested and implemented required changes --- packages/govern-tx/lib/AbstractAction.ts | 7 ++++++- packages/govern-tx/package.json | 1 + packages/govern-tx/postgres/init.sql | 6 ++++-- packages/govern-tx/src/auth/Authenticator.ts | 13 ++++++++++--- packages/govern-tx/src/db/Database.ts | 1 - packages/govern-tx/src/db/Whitelist.ts | 2 +- 6 files changed, 22 insertions(+), 8 deletions(-) diff --git a/packages/govern-tx/lib/AbstractAction.ts b/packages/govern-tx/lib/AbstractAction.ts index cba773d4a..09dec938d 100644 --- a/packages/govern-tx/lib/AbstractAction.ts +++ b/packages/govern-tx/lib/AbstractAction.ts @@ -61,7 +61,12 @@ export default abstract class AbstractAction { type: 'object', required: ['message', 'signature'], properties: { - message: { type: 'string' }, + message: { + oneOf: [ + { type: 'string'}, + { type: 'object'} + ] + }, signature: { type: 'string' } } } diff --git a/packages/govern-tx/package.json b/packages/govern-tx/package.json index e2097f4fd..e43743e42 100644 --- a/packages/govern-tx/package.json +++ b/packages/govern-tx/package.json @@ -34,6 +34,7 @@ "@ethersproject/hash": "^5.0.9", "@ethersproject/providers": "^5.0.15", "@ethersproject/wallet": "^5.0.8", + "@ethersproject/strings": "^5.0.7", "fastify": "^3.8.0", "postgres": "^2.0.0-beta.2" } diff --git a/packages/govern-tx/postgres/init.sql b/packages/govern-tx/postgres/init.sql index c235b9b79..8ccce9e62 100644 --- a/packages/govern-tx/postgres/init.sql +++ b/packages/govern-tx/postgres/init.sql @@ -2,13 +2,15 @@ CREATE TABLE whitelist ( ID SERIAL PRIMARY KEY, PublicKey CHAR(42) NOT NULL, TxLimit INT DEFAULT 100 NOT NULL, - Executed INT DEFAULT 0 NOT NULL + Executed INT DEFAULT 0 NOT NULL, + UNIQUE(PublicKey) ); CREATE TABLE admins ( ID SERIAL PRIMARY KEY, PublicKey CHAR(42) NOT NULL, - PrivateKey CHAR(64) NOT NULL + PrivateKey CHAR(64) NOT NULL, + UNIQUE(PublicKey) ); INSERT INTO whitelist (PublicKey, TxLimit) VALUES ('0x6FA59C2C8EAaCC1f8875794f2DAe145Bb32Bd9B1', '100'); diff --git a/packages/govern-tx/src/auth/Authenticator.ts b/packages/govern-tx/src/auth/Authenticator.ts index 96494ab7c..c5b0ec56b 100644 --- a/packages/govern-tx/src/auth/Authenticator.ts +++ b/packages/govern-tx/src/auth/Authenticator.ts @@ -1,6 +1,7 @@ import { FastifyRequest } from 'fastify'; import { verifyMessage } from '@ethersproject/wallet'; import { arrayify } from '@ethersproject/bytes' +import { toUtf8Bytes } from '@ethersproject/strings' import { Unauthorized, HttpError } from 'http-errors' import Whitelist from '../db/Whitelist' import Admin from '../db/Admin'; @@ -40,10 +41,16 @@ export default class Authenticator { * @public */ public async authenticate(request: FastifyRequest): Promise { + let message = (request.body as Params).message + + if (typeof message === 'object' && message != null) { + message = toUtf8Bytes(JSON.stringify(message)) + } else { + message = arrayify(message) + } + const publicKey = verifyMessage( - arrayify( - (request.body as Params).message - ), + message, (request.body as Params).signature ) diff --git a/packages/govern-tx/src/db/Database.ts b/packages/govern-tx/src/db/Database.ts index 680a8b6e4..25bc3baf1 100644 --- a/packages/govern-tx/src/db/Database.ts +++ b/packages/govern-tx/src/db/Database.ts @@ -3,7 +3,6 @@ import { DatabaseOptions } from '../config/Configuration' export default class Database { /** - * TODO: Define type * The sql function of the postgres client * * @property {Function} sql diff --git a/packages/govern-tx/src/db/Whitelist.ts b/packages/govern-tx/src/db/Whitelist.ts index c15d091f3..04f25f19b 100644 --- a/packages/govern-tx/src/db/Whitelist.ts +++ b/packages/govern-tx/src/db/Whitelist.ts @@ -71,7 +71,7 @@ export default class Whitelist { * @public */ public addItem(publicKey: string, rateLimit: number): Promise { - return this.db.query(`INSERT INTO whitelist VALUES (${publicKey}, ${rateLimit})`) + return this.db.query(`INSERT INTO whitelist (PublicKey, TxLimit) VALUES ('${publicKey}', ${rateLimit})`) } /** From eb6c386e2ae4563dd388d41dff6379e0bec303e7 Mon Sep 17 00:00:00 2001 From: nivida Date: Fri, 11 Dec 2020 14:36:58 +0100 Subject: [PATCH 095/107] DeleteItemAction.validateRequest fixed and removing of function sig hash from the request message added to ContractFunction.decode --- packages/govern-tx/lib/transactions/ContractFunction.ts | 2 +- packages/govern-tx/src/whitelist/DeleteItemAction.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/govern-tx/lib/transactions/ContractFunction.ts b/packages/govern-tx/lib/transactions/ContractFunction.ts index e2050519a..b85e20bbb 100644 --- a/packages/govern-tx/lib/transactions/ContractFunction.ts +++ b/packages/govern-tx/lib/transactions/ContractFunction.ts @@ -61,6 +61,6 @@ export default class ContractFunction { * @public */ public decode(): any[] { - return defaultAbiCoder.decode(this.abiItem.inputs, this.requestMsg) as any[] + return defaultAbiCoder.decode(this.abiItem.inputs, '0x' + this.requestMsg.slice(10)) as any[] } } diff --git a/packages/govern-tx/src/whitelist/DeleteItemAction.ts b/packages/govern-tx/src/whitelist/DeleteItemAction.ts index 1135076fe..603eed280 100644 --- a/packages/govern-tx/src/whitelist/DeleteItemAction.ts +++ b/packages/govern-tx/src/whitelist/DeleteItemAction.ts @@ -15,7 +15,7 @@ export default class DeleteItemAction extends AbstractWhitelistAction { * @protected */ protected validateRequest(request: FastifyRequest): FastifyRequest { - if (!isAddress((this.request.body as WhitelistParams).message.publicKey)) { + if (!isAddress((request.body as WhitelistParams).message.publicKey)) { throw new Error('Invalid public key passed!') } From 088a97cdebb163a7797455d4a91244b6dd176aab Mon Sep 17 00:00:00 2001 From: nivida Date: Fri, 11 Dec 2020 14:40:32 +0100 Subject: [PATCH 096/107] postgres DB schema extended --- packages/govern-tx/postgres/init.sql | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/govern-tx/postgres/init.sql b/packages/govern-tx/postgres/init.sql index 8ccce9e62..18715a4b4 100644 --- a/packages/govern-tx/postgres/init.sql +++ b/packages/govern-tx/postgres/init.sql @@ -9,10 +9,18 @@ CREATE TABLE whitelist ( CREATE TABLE admins ( ID SERIAL PRIMARY KEY, PublicKey CHAR(42) NOT NULL, - PrivateKey CHAR(64) NOT NULL, UNIQUE(PublicKey) ); +CREATE TABLE wallet { + ID SERIAL PRIMARY KEY, + PublicKey CHAR(42) NOT NULL, + PrivateKey CHAR(64) NOT NULL, + UNIQUE(PublicKey) +} + INSERT INTO whitelist (PublicKey, TxLimit) VALUES ('0x6FA59C2C8EAaCC1f8875794f2DAe145Bb32Bd9B1', '100'); -INSERT INTO admins (PublicKey, PrivateKey) VALUES ('0x6FA59C2C8EAaCC1f8875794f2DAe145Bb32Bd9B1', '3d4ba04a9c7b159a998d146760cba981ea05784404e38c6fa0a2fe852fbdd648'); +INSERT INTO admins (PublicKey) VALUES ('0x6FA59C2C8EAaCC1f8875794f2DAe145Bb32Bd9B1', '3d4ba04a9c7b159a998d146760cba981ea05784404e38c6fa0a2fe852fbdd648'); + +INSERT INTO wallet (PublicKey, PrivateKey) VALUES ('0x6FA59C2C8EAaCC1f8875794f2DAe145Bb32Bd9B1', '3d4ba04a9c7b159a998d146760cba981ea05784404e38c6fa0a2fe852fbdd648'); From 2153a23f1c02c2ff6dacce1d23ab5d46e7fd505a Mon Sep 17 00:00:00 2001 From: nivida Date: Fri, 11 Dec 2020 14:46:16 +0100 Subject: [PATCH 097/107] query in Admin entity fixed --- packages/govern-tx/postgres/init.sql | 8 ++++---- packages/govern-tx/src/db/Admin.ts | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/govern-tx/postgres/init.sql b/packages/govern-tx/postgres/init.sql index 18715a4b4..16ce9e338 100644 --- a/packages/govern-tx/postgres/init.sql +++ b/packages/govern-tx/postgres/init.sql @@ -7,20 +7,20 @@ CREATE TABLE whitelist ( ); CREATE TABLE admins ( - ID SERIAL PRIMARY KEY, + ID SERIAL PRIMARY KEY, PublicKey CHAR(42) NOT NULL, UNIQUE(PublicKey) ); CREATE TABLE wallet { - ID SERIAL PRIMARY KEY, - PublicKey CHAR(42) NOT NULL, + ID SERIAL PRIMARY KEY, + PublicKey CHAR(42) NOT NULL, PrivateKey CHAR(64) NOT NULL, UNIQUE(PublicKey) } INSERT INTO whitelist (PublicKey, TxLimit) VALUES ('0x6FA59C2C8EAaCC1f8875794f2DAe145Bb32Bd9B1', '100'); -INSERT INTO admins (PublicKey) VALUES ('0x6FA59C2C8EAaCC1f8875794f2DAe145Bb32Bd9B1', '3d4ba04a9c7b159a998d146760cba981ea05784404e38c6fa0a2fe852fbdd648'); +INSERT INTO admins (PublicKey) VALUES ('0x6FA59C2C8EAaCC1f8875794f2DAe145Bb32Bd9B1'); INSERT INTO wallet (PublicKey, PrivateKey) VALUES ('0x6FA59C2C8EAaCC1f8875794f2DAe145Bb32Bd9B1', '3d4ba04a9c7b159a998d146760cba981ea05784404e38c6fa0a2fe852fbdd648'); diff --git a/packages/govern-tx/src/db/Admin.ts b/packages/govern-tx/src/db/Admin.ts index ca2209486..fe126b9c2 100644 --- a/packages/govern-tx/src/db/Admin.ts +++ b/packages/govern-tx/src/db/Admin.ts @@ -39,7 +39,7 @@ export default class Admin { * @public */ public addAdmin(publicKey: string): Promise { - return this.db.query(`INSERT INTO admins VALUES (${publicKey})`) + return this.db.query(`INSERT INTO admins VALUES ('${publicKey}')`) } /** From 5433b466e28787252ce1424a8cd2f232c9df0462 Mon Sep 17 00:00:00 2001 From: nivida Date: Fri, 11 Dec 2020 14:51:42 +0100 Subject: [PATCH 098/107] DB schema simplified. No need for a ID if the PK is anyways unique --- packages/govern-tx/postgres/init.sql | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/packages/govern-tx/postgres/init.sql b/packages/govern-tx/postgres/init.sql index 16ce9e338..f9e5fe106 100644 --- a/packages/govern-tx/postgres/init.sql +++ b/packages/govern-tx/postgres/init.sql @@ -1,22 +1,16 @@ CREATE TABLE whitelist ( - ID SERIAL PRIMARY KEY, - PublicKey CHAR(42) NOT NULL, + PublicKey CHAR(42) PRIMARY KEY, TxLimit INT DEFAULT 100 NOT NULL, - Executed INT DEFAULT 0 NOT NULL, - UNIQUE(PublicKey) + Executed INT DEFAULT 0 NOT NULL ); CREATE TABLE admins ( - ID SERIAL PRIMARY KEY, - PublicKey CHAR(42) NOT NULL, - UNIQUE(PublicKey) + PublicKey CHAR(42) PRIMARY KEY ); CREATE TABLE wallet { - ID SERIAL PRIMARY KEY, - PublicKey CHAR(42) NOT NULL, - PrivateKey CHAR(64) NOT NULL, - UNIQUE(PublicKey) + PublicKey CHAR(42) PRIMARY KEY, + PrivateKey CHAR(64) NOT NULL } INSERT INTO whitelist (PublicKey, TxLimit) VALUES ('0x6FA59C2C8EAaCC1f8875794f2DAe145Bb32Bd9B1', '100'); From 777a814d0410dc7ee9f24f0f5c3d510ab86dc7b2 Mon Sep 17 00:00:00 2001 From: nivida Date: Fri, 11 Dec 2020 14:52:59 +0100 Subject: [PATCH 099/107] Whitelist.addItem query fixed --- packages/govern-tx/src/db/Whitelist.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/govern-tx/src/db/Whitelist.ts b/packages/govern-tx/src/db/Whitelist.ts index 04f25f19b..ea9e77adb 100644 --- a/packages/govern-tx/src/db/Whitelist.ts +++ b/packages/govern-tx/src/db/Whitelist.ts @@ -71,7 +71,7 @@ export default class Whitelist { * @public */ public addItem(publicKey: string, rateLimit: number): Promise { - return this.db.query(`INSERT INTO whitelist (PublicKey, TxLimit) VALUES ('${publicKey}', ${rateLimit})`) + return this.db.query(`INSERT INTO whitelist (PublicKey, TxLimit) VALUES ('${publicKey}', '${rateLimit}')`) } /** From 753e5c71e3ecfff7d8c894f2bd3fe7cd30c9e2cb Mon Sep 17 00:00:00 2001 From: nivida Date: Fri, 11 Dec 2020 15:10:32 +0100 Subject: [PATCH 100/107] DB model for documentation updated --- packages/govern-tx/assets/db_model.png | Bin 66543 -> 86628 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/packages/govern-tx/assets/db_model.png b/packages/govern-tx/assets/db_model.png index 25b0fe072e4b80920140018de8108276bc3ef0b2..facf4e440b5a9366cd595a8c399d80db13be6733 100644 GIT binary patch literal 86628 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

2|YN0iv(ZX=0+dKmUk%{s$Kqn|{`C|~I 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 Date: Fri, 11 Dec 2020 16:27:24 +0100 Subject: [PATCH 101/107] types improved for Database and his implemented entities --- packages/govern-tx/src/db/Admin.ts | 9 ++++----- packages/govern-tx/src/db/Database.ts | 2 +- packages/govern-tx/src/db/Whitelist.ts | 11 +++++------ 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/packages/govern-tx/src/db/Admin.ts b/packages/govern-tx/src/db/Admin.ts index fe126b9c2..5a3b00f6e 100644 --- a/packages/govern-tx/src/db/Admin.ts +++ b/packages/govern-tx/src/db/Admin.ts @@ -1,7 +1,6 @@ import Database from './Database' export interface AdminItem { - ID: number PublicKey: string PrivateKey: string } @@ -24,7 +23,7 @@ export default class Admin { * @returns {boolean} */ public async isAdmin(publicKey: string): Promise { - return (await this.db.query(`SELECT * FROM admins WHERE PublicKey='${publicKey}'`)).length > 0 + return (await this.db.query(`SELECT * FROM admins WHERE PublicKey='${publicKey}'`)).length > 0 } /** @@ -39,7 +38,7 @@ export default class Admin { * @public */ public addAdmin(publicKey: string): Promise { - return this.db.query(`INSERT INTO admins VALUES ('${publicKey}')`) + return this.db.query(`INSERT INTO admins VALUES ('${publicKey}')`) } /** @@ -54,7 +53,7 @@ export default class Admin { * @public */ public async deleteAdmin(publicKey: string): Promise { - return (await this.db.query(`DELETE FROM admins WHERE PublicKey='${publicKey}'`)).length > 0 + return (await this.db.query(`DELETE FROM admins WHERE PublicKey='${publicKey}'`)).length > 0 } /** @@ -67,6 +66,6 @@ export default class Admin { * @public */ public getAdmins(): Promise { - return this.db.query('SELECT * from admins') + return this.db.query('SELECT * from admins') } } diff --git a/packages/govern-tx/src/db/Database.ts b/packages/govern-tx/src/db/Database.ts index 25bc3baf1..ebefafa6f 100644 --- a/packages/govern-tx/src/db/Database.ts +++ b/packages/govern-tx/src/db/Database.ts @@ -50,7 +50,7 @@ export default class Database { * * @public */ - public query(query: string): Promise { + public query(query: string): Promise { return this.sql.unsafe(query) // TODO: Change back to normal call of the sql function } } diff --git a/packages/govern-tx/src/db/Whitelist.ts b/packages/govern-tx/src/db/Whitelist.ts index ea9e77adb..d668d121b 100644 --- a/packages/govern-tx/src/db/Whitelist.ts +++ b/packages/govern-tx/src/db/Whitelist.ts @@ -1,7 +1,6 @@ import Database from './Database' export interface ListItem { - ID: number PublicKey: string, Limit: number, Executed: number @@ -23,7 +22,7 @@ export default class Whitelist { * @returns {Promise} */ public getList(): Promise { - return this.db.query('SELECT * FROM whitelist') + return this.db.query('SELECT * FROM whitelist') } /** @@ -55,7 +54,7 @@ export default class Whitelist { * @public */ public getItemByKey(publicKey: string): Promise { - return this.db.query(`SELECT * FROM whitelist WHERE PublicKey='${publicKey}'`) + return this.db.query(`SELECT * FROM whitelist WHERE PublicKey='${publicKey}'`) } /** @@ -71,7 +70,7 @@ export default class Whitelist { * @public */ public addItem(publicKey: string, rateLimit: number): Promise { - return this.db.query(`INSERT INTO whitelist (PublicKey, TxLimit) VALUES ('${publicKey}', '${rateLimit}')`) + return this.db.query(`INSERT INTO whitelist (PublicKey, TxLimit) VALUES ('${publicKey}', '${rateLimit}')`) } /** @@ -86,7 +85,7 @@ export default class Whitelist { * @public */ public async deleteItem(publicKey: string): Promise { - return (await this.db.query(`DELETE FROM whitelist WHERE PublicKey='${publicKey}'`)).length > 0 + return (await this.db.query(`DELETE FROM whitelist WHERE PublicKey='${publicKey}'`)).length > 0 } /** @@ -101,7 +100,7 @@ export default class Whitelist { * @public */ public async increaseExecutionCounter(publicKey: string): Promise { - return this.db.query(`UPDATE whitelist SET Executed = Executed + 1 WHERE PublicKey='${publicKey}'`) + return this.db.query(`UPDATE whitelist SET Executed = Executed + 1 WHERE PublicKey='${publicKey}'`) } /** From 64b524354be57d9e3059fdb1d81541e0cf6f6700 Mon Sep 17 00:00:00 2001 From: nivida Date: Fri, 11 Dec 2020 16:32:51 +0100 Subject: [PATCH 102/107] Whitelist.ts queries improved --- packages/govern-tx/src/db/Whitelist.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/govern-tx/src/db/Whitelist.ts b/packages/govern-tx/src/db/Whitelist.ts index d668d121b..f63a08133 100644 --- a/packages/govern-tx/src/db/Whitelist.ts +++ b/packages/govern-tx/src/db/Whitelist.ts @@ -39,7 +39,7 @@ export default class Whitelist { * @public */ public async keyExists(publicKey: string): Promise { - return typeof (await this.getItemByKey(publicKey)).ID !== 'undefined' + return typeof (await this.getItemByKey(publicKey)) !== 'undefined' } /** @@ -53,8 +53,8 @@ export default class Whitelist { * * @public */ - public getItemByKey(publicKey: string): Promise { - return this.db.query(`SELECT * FROM whitelist WHERE PublicKey='${publicKey}'`) + public async getItemByKey(publicKey: string): Promise { + return (await this.db.query(`SELECT * FROM whitelist WHERE PublicKey='${publicKey}'`))[0] } /** From f55b8c576bc0ee909882240c2904e72306e88de1 Mon Sep 17 00:00:00 2001 From: nivida Date: Fri, 11 Dec 2020 16:43:10 +0100 Subject: [PATCH 103/107] generics added and code style improvment --- packages/govern-tx/lib/AbstractAction.ts | 4 ++-- packages/govern-tx/lib/transactions/AbstractTransaction.ts | 2 +- packages/govern-tx/lib/whitelist/AbstractWhitelistAction.ts | 6 +++--- packages/govern-tx/src/whitelist/AddItemAction.ts | 2 +- packages/govern-tx/src/whitelist/DeleteItemAction.ts | 2 +- packages/govern-tx/src/whitelist/GetListAction.ts | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/govern-tx/lib/AbstractAction.ts b/packages/govern-tx/lib/AbstractAction.ts index 09dec938d..b3d40c03e 100644 --- a/packages/govern-tx/lib/AbstractAction.ts +++ b/packages/govern-tx/lib/AbstractAction.ts @@ -5,7 +5,7 @@ export interface Params { signature: string, } -export default abstract class AbstractAction { +export default abstract class AbstractAction { /** * The given request by the user * @@ -46,7 +46,7 @@ export default abstract class AbstractAction { * * @public */ - public abstract execute(): Promise + public abstract execute(): Promise /** * Returns the required request schema diff --git a/packages/govern-tx/lib/transactions/AbstractTransaction.ts b/packages/govern-tx/lib/transactions/AbstractTransaction.ts index 61d9e44e7..cf8c5988a 100644 --- a/packages/govern-tx/lib/transactions/AbstractTransaction.ts +++ b/packages/govern-tx/lib/transactions/AbstractTransaction.ts @@ -9,7 +9,7 @@ import Whitelist from '../../src/db/Whitelist'; import { Params } from '../AbstractAction'; import { AuthenticatedRequest } from '../../src/auth/Authenticator'; -export default abstract class AbstractTransaction extends AbstractAction { +export default abstract class AbstractTransaction extends AbstractAction { /** * The function ABI used to create a transaction * diff --git a/packages/govern-tx/lib/whitelist/AbstractWhitelistAction.ts b/packages/govern-tx/lib/whitelist/AbstractWhitelistAction.ts index 905509e7b..fb0da93a8 100644 --- a/packages/govern-tx/lib/whitelist/AbstractWhitelistAction.ts +++ b/packages/govern-tx/lib/whitelist/AbstractWhitelistAction.ts @@ -9,7 +9,7 @@ export interface WhitelistParams extends Params { } } -export default abstract class AbstractWhitelistAction extends AbstractAction { +export default abstract class AbstractWhitelistAction extends AbstractAction { /** * @param {Whitelist} whitelist - The whitelist database entitiy * @param {WhitelistRequest} request - The given request body by the user @@ -25,11 +25,11 @@ export default abstract class AbstractWhitelistAction extends AbstractAction { * * @method execute * - * @returns {Promise} + * @returns {Promise} * * @public */ - public abstract execute(): Promise + public abstract execute(): Promise /** * TODO: Define response validation diff --git a/packages/govern-tx/src/whitelist/AddItemAction.ts b/packages/govern-tx/src/whitelist/AddItemAction.ts index f6988deed..c06745197 100644 --- a/packages/govern-tx/src/whitelist/AddItemAction.ts +++ b/packages/govern-tx/src/whitelist/AddItemAction.ts @@ -3,7 +3,7 @@ import { FastifyRequest } from 'fastify' import AbstractWhitelistAction, { WhitelistParams } from "../../lib/whitelist/AbstractWhitelistAction" import {ListItem} from '../db/Whitelist' -export default class AddItemAction extends AbstractWhitelistAction { +export default class AddItemAction extends AbstractWhitelistAction { /** * Validates the given request body. * diff --git a/packages/govern-tx/src/whitelist/DeleteItemAction.ts b/packages/govern-tx/src/whitelist/DeleteItemAction.ts index 603eed280..a29033950 100644 --- a/packages/govern-tx/src/whitelist/DeleteItemAction.ts +++ b/packages/govern-tx/src/whitelist/DeleteItemAction.ts @@ -2,7 +2,7 @@ import {isAddress} from '@ethersproject/address' import { FastifyRequest } from 'fastify'; import AbstractWhitelistAction, { WhitelistParams } from "../../lib/whitelist/AbstractWhitelistAction"; -export default class DeleteItemAction extends AbstractWhitelistAction { +export default class DeleteItemAction extends AbstractWhitelistAction { /** * Validates the given request body. * diff --git a/packages/govern-tx/src/whitelist/GetListAction.ts b/packages/govern-tx/src/whitelist/GetListAction.ts index a4b1f5933..d644071bd 100644 --- a/packages/govern-tx/src/whitelist/GetListAction.ts +++ b/packages/govern-tx/src/whitelist/GetListAction.ts @@ -1,7 +1,7 @@ import AbstractWhitelistAction from '../../lib/whitelist/AbstractWhitelistAction'; import {ListItem} from '../db/Whitelist' -export default class GetListAction extends AbstractWhitelistAction { +export default class GetListAction extends AbstractWhitelistAction { /** * Adds a new item to the whitelist * From 1285e50cf2e3ec5ba1f800962b5b70b302493075 Mon Sep 17 00:00:00 2001 From: nivida Date: Fri, 11 Dec 2020 16:48:18 +0100 Subject: [PATCH 104/107] code style improved (EOF) --- packages/govern-tx/src/transactions/challenge/challenge.json | 1 - packages/govern-tx/src/transactions/create/create.json | 1 - packages/govern-tx/src/transactions/execute/execute.json | 1 - packages/govern-tx/src/transactions/schedule/schedule.json | 1 - packages/govern-tx/test/src/auth/AuthenticatorTest.ts | 2 +- packages/govern-tx/test/src/db/AdminTest.ts | 2 +- 6 files changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/govern-tx/src/transactions/challenge/challenge.json b/packages/govern-tx/src/transactions/challenge/challenge.json index 0ecbe337a..9c56b72f9 100644 --- a/packages/govern-tx/src/transactions/challenge/challenge.json +++ b/packages/govern-tx/src/transactions/challenge/challenge.json @@ -139,4 +139,3 @@ "stateMutability": "nonpayable", "type": "function" } - \ No newline at end of file diff --git a/packages/govern-tx/src/transactions/create/create.json b/packages/govern-tx/src/transactions/create/create.json index 6d443c77a..616a70a01 100644 --- a/packages/govern-tx/src/transactions/create/create.json +++ b/packages/govern-tx/src/transactions/create/create.json @@ -42,4 +42,3 @@ "stateMutability": "nonpayable", "type": "function" } - \ No newline at end of file diff --git a/packages/govern-tx/src/transactions/execute/execute.json b/packages/govern-tx/src/transactions/execute/execute.json index 47dd9c2c4..dde6dfc5f 100644 --- a/packages/govern-tx/src/transactions/execute/execute.json +++ b/packages/govern-tx/src/transactions/execute/execute.json @@ -139,4 +139,3 @@ "stateMutability": "nonpayable", "type": "function" } - \ No newline at end of file diff --git a/packages/govern-tx/src/transactions/schedule/schedule.json b/packages/govern-tx/src/transactions/schedule/schedule.json index cfe6b3aaa..8b717088f 100644 --- a/packages/govern-tx/src/transactions/schedule/schedule.json +++ b/packages/govern-tx/src/transactions/schedule/schedule.json @@ -134,4 +134,3 @@ "stateMutability": "nonpayable", "type": "function" } - \ No newline at end of file diff --git a/packages/govern-tx/test/src/auth/AuthenticatorTest.ts b/packages/govern-tx/test/src/auth/AuthenticatorTest.ts index aeb44888c..a623820dd 100644 --- a/packages/govern-tx/test/src/auth/AuthenticatorTest.ts +++ b/packages/govern-tx/test/src/auth/AuthenticatorTest.ts @@ -100,4 +100,4 @@ describe('AuthenticatorTest', () => { expect(adminMock.isAdmin).toHaveBeenNthCalledWith(1, '0x00') }) -}) \ No newline at end of file +}) diff --git a/packages/govern-tx/test/src/db/AdminTest.ts b/packages/govern-tx/test/src/db/AdminTest.ts index a3f6bf45a..319c71a31 100644 --- a/packages/govern-tx/test/src/db/AdminTest.ts +++ b/packages/govern-tx/test/src/db/AdminTest.ts @@ -103,4 +103,4 @@ describe('AdminTest', () => { expect(databaseMock.query).toHaveBeenNthCalledWith(1, 'SELECT * from admins') }) -}) \ No newline at end of file +}) From 9816d2fe0adc11826654d6ee5b69185861ee9b46 Mon Sep 17 00:00:00 2001 From: nivida Date: Mon, 14 Dec 2020 13:58:40 +0100 Subject: [PATCH 105/107] code style improvements --- packages/govern-tx/lib/transactions/AbstractTransaction.ts | 2 +- packages/govern-tx/src/whitelist/AddItemAction.ts | 2 +- packages/govern-tx/src/whitelist/DeleteItemAction.ts | 2 +- packages/govern-tx/test/src/auth/AuthenticatorTest.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/govern-tx/lib/transactions/AbstractTransaction.ts b/packages/govern-tx/lib/transactions/AbstractTransaction.ts index cf8c5988a..fd7aac3f3 100644 --- a/packages/govern-tx/lib/transactions/AbstractTransaction.ts +++ b/packages/govern-tx/lib/transactions/AbstractTransaction.ts @@ -43,7 +43,7 @@ export default abstract class AbstractTransaction extends AbstractAction { throw new Error('Invalid rate limit passed!') } - return request; + return request } /** diff --git a/packages/govern-tx/src/whitelist/DeleteItemAction.ts b/packages/govern-tx/src/whitelist/DeleteItemAction.ts index a29033950..eb3737140 100644 --- a/packages/govern-tx/src/whitelist/DeleteItemAction.ts +++ b/packages/govern-tx/src/whitelist/DeleteItemAction.ts @@ -32,6 +32,6 @@ export default class DeleteItemAction extends AbstractWhitelistAction { * @public */ public execute(): Promise { - return this.whitelist.deleteItem((this.request.body as WhitelistParams).message.publicKey); + return this.whitelist.deleteItem((this.request.body as WhitelistParams).message.publicKey) } } diff --git a/packages/govern-tx/test/src/auth/AuthenticatorTest.ts b/packages/govern-tx/test/src/auth/AuthenticatorTest.ts index a623820dd..0ad997cc4 100644 --- a/packages/govern-tx/test/src/auth/AuthenticatorTest.ts +++ b/packages/govern-tx/test/src/auth/AuthenticatorTest.ts @@ -88,7 +88,7 @@ describe('AuthenticatorTest', () => { }) it('calls authenticate as admin user and grants access to the whitelist actions', async () => { - (adminMock.isAdmin as jest.MockedFunction).mockReturnValueOnce(Promise.resolve(true)); + (adminMock.isAdmin as jest.MockedFunction).mockReturnValueOnce(Promise.resolve(true)) request.routerPath = '/whitelist' From 98609edf19af7d74beea95a1ce799eb08a8d950f Mon Sep 17 00:00:00 2001 From: nivida Date: Tue, 15 Dec 2020 12:25:38 +0100 Subject: [PATCH 106/107] Wallet types updated and Whitelist GET request fixed --- packages/govern-tx/src/Bootstrap.ts | 17 ++++++++--------- packages/govern-tx/src/auth/Authenticator.ts | 15 +++++++++------ packages/govern-tx/src/wallet/Wallet.ts | 2 +- 3 files changed, 18 insertions(+), 16 deletions(-) diff --git a/packages/govern-tx/src/Bootstrap.ts b/packages/govern-tx/src/Bootstrap.ts index 6ff4bf577..765ac4dba 100644 --- a/packages/govern-tx/src/Bootstrap.ts +++ b/packages/govern-tx/src/Bootstrap.ts @@ -1,4 +1,4 @@ -import fastify, { FastifyInstance } from 'fastify' +import fastify, { FastifyInstance, FastifyRequest } from 'fastify' import { TransactionReceipt } from '@ethersproject/abstract-provider' import Configuration from './config/Configuration' @@ -108,7 +108,7 @@ export default class Bootstrap { this.server.post( '/execute', {schema: AbstractTransaction.schema}, - (request): Promise => { + (request: FastifyRequest): Promise => { return new ExecuteTransaction( this.config.ethereum, this.provider, @@ -121,7 +121,7 @@ export default class Bootstrap { this.server.post( '/schedule', {schema: AbstractTransaction.schema}, - (request): Promise => { + (request: FastifyRequest): Promise => { return new ScheduleTransaction( this.config.ethereum, this.provider, @@ -134,7 +134,7 @@ export default class Bootstrap { this.server.post( '/challenge', {schema: AbstractTransaction.schema}, - (request): Promise => { + (request: FastifyRequest): Promise => { return new ChallengeTransaction( this.config.ethereum, this.provider, @@ -147,7 +147,7 @@ export default class Bootstrap { this.server.post( '/create', {schema: AbstractTransaction.schema}, - (request): Promise => { + (request: FastifyRequest): Promise => { return new CreateTransaction( this.config.ethereum, this.provider, @@ -171,7 +171,7 @@ export default class Bootstrap { this.server.post( '/whitelist', {schema: AbstractWhitelistAction.schema}, - (request): Promise => { + (request: FastifyRequest): Promise => { return new AddItemAction(this.whitelist, request).execute() } ) @@ -179,15 +179,14 @@ export default class Bootstrap { this.server.delete( '/whitelist', {schema: AbstractWhitelistAction.schema}, - (request): Promise => { + (request: FastifyRequest): Promise => { return new DeleteItemAction(this.whitelist, request).execute() } ) this.server.get( '/whitelist', - {schema: AbstractWhitelistAction.schema}, - (request): Promise => { + (request: FastifyRequest): Promise => { return new GetListAction(this.whitelist, request).execute() } ) diff --git a/packages/govern-tx/src/auth/Authenticator.ts b/packages/govern-tx/src/auth/Authenticator.ts index c5b0ec56b..1c0568e46 100644 --- a/packages/govern-tx/src/auth/Authenticator.ts +++ b/packages/govern-tx/src/auth/Authenticator.ts @@ -41,16 +41,19 @@ export default class Authenticator { * @public */ public async authenticate(request: FastifyRequest): Promise { - let message = (request.body as Params).message + if (request.method == 'GET') { + request.body = '' - if (typeof message === 'object' && message != null) { - message = toUtf8Bytes(JSON.stringify(message)) - } else { - message = arrayify(message) + for await (const data of request.raw) { + request.body += data.toString() + } + + request.body = JSON.parse(request.body as string); + (request.body as Params).message = toUtf8Bytes((request.body as Params).message) } const publicKey = verifyMessage( - message, + arrayify((request.body as Params).message), (request.body as Params).signature ) diff --git a/packages/govern-tx/src/wallet/Wallet.ts b/packages/govern-tx/src/wallet/Wallet.ts index d635c1a25..7a0ccf2de 100644 --- a/packages/govern-tx/src/wallet/Wallet.ts +++ b/packages/govern-tx/src/wallet/Wallet.ts @@ -66,7 +66,7 @@ export default class Wallet { */ private async loadWallet(publicKey: string): Promise { if (!this.wallet || this.publicKey !== publicKey) { - const pk = await this.db.query(`SELECT PrivateKey FROM wallet WHERE PublicKey='${publicKey}'`); + const pk = (await this.db.query(`SELECT PrivateKey FROM wallet WHERE PublicKey='${publicKey}'`))[0]; this.wallet = new EthersWallet(pk) this.publicKey = publicKey; } From 621e855b8ca538133be092b460149f30ca56889f Mon Sep 17 00:00:00 2001 From: Giorgi-Lagidze Date: Wed, 17 Feb 2021 16:57:21 +0400 Subject: [PATCH 107/107] fix govern-tx tests --- packages/govern-tx/src/Bootstrap.ts | 1 + packages/govern-tx/src/db/Database.ts | 2 +- .../govern-tx/test/lib/AbstractActionTest.ts | 18 ++++++++--- .../transactions/AbstractTransactionTest.ts | 21 ++++++++++--- .../lib/transactions/ContractFunctionTest.ts | 10 +++---- .../whitelist/AbstractWhitelistActionTest.ts | 19 +++++++++--- packages/govern-tx/test/src/BootstrapTest.ts | 30 +++++++++---------- packages/govern-tx/test/src/db/AdminTest.ts | 4 +-- .../govern-tx/test/src/db/WhitelistTest.ts | 6 ++-- .../challenge/ChallengeTransactionTest.ts | 5 ++-- .../execute/ExecuteTransactionTest.ts | 5 ++-- .../schedule/ScheduleTransactionTest.ts | 5 ++-- .../govern-tx/test/src/wallet/WalletTest.ts | 26 +++++----------- .../test/src/whitelist/AddItemActionTest.ts | 30 ++++++++++--------- .../src/whitelist/DeleteItemActionTest.ts | 21 ++++++------- .../test/src/whitelist/GetListActionTest.ts | 5 ++-- 16 files changed, 117 insertions(+), 91 deletions(-) diff --git a/packages/govern-tx/src/Bootstrap.ts b/packages/govern-tx/src/Bootstrap.ts index 765ac4dba..a0e7e723b 100644 --- a/packages/govern-tx/src/Bootstrap.ts +++ b/packages/govern-tx/src/Bootstrap.ts @@ -186,6 +186,7 @@ export default class Bootstrap { this.server.get( '/whitelist', + {schema: AbstractWhitelistAction.schema}, (request: FastifyRequest): Promise => { return new GetListAction(this.whitelist, request).execute() } diff --git a/packages/govern-tx/src/db/Database.ts b/packages/govern-tx/src/db/Database.ts index ebefafa6f..b1f627a96 100644 --- a/packages/govern-tx/src/db/Database.ts +++ b/packages/govern-tx/src/db/Database.ts @@ -51,6 +51,6 @@ export default class Database { * @public */ public query(query: string): Promise { - return this.sql.unsafe(query) // TODO: Change back to normal call of the sql function + return this.sql(query); } } diff --git a/packages/govern-tx/test/lib/AbstractActionTest.ts b/packages/govern-tx/test/lib/AbstractActionTest.ts index c798a9ebb..aa04118d7 100644 --- a/packages/govern-tx/test/lib/AbstractActionTest.ts +++ b/packages/govern-tx/test/lib/AbstractActionTest.ts @@ -1,6 +1,11 @@ -import AbstractAction, { Request } from '../../lib/AbstractAction'; +import AbstractAction from '../../lib/AbstractAction'; -class MockAction extends AbstractAction { +interface MockInterface { + message: string | any +} + + +class MockAction extends AbstractAction { public execute(): Promise { return Promise.resolve(true) } @@ -13,7 +18,7 @@ describe('AbstractAction Test', () => { let action: MockAction beforeEach(() => { - action = new MockAction({message: true} as Request) + action = new MockAction({message: true} as any) }) it('has the correct schema defined', () => { @@ -22,7 +27,12 @@ describe('AbstractAction Test', () => { type: 'object', required: ['message', 'signature'], properties: { - message: { type: 'string' }, + message: { + oneOf: [ + { type: 'string'}, + { type: 'object'} + ] + }, signature: { type: 'string' } } } diff --git a/packages/govern-tx/test/lib/transactions/AbstractTransactionTest.ts b/packages/govern-tx/test/lib/transactions/AbstractTransactionTest.ts index 7f4205cc0..5dc66d86b 100644 --- a/packages/govern-tx/test/lib/transactions/AbstractTransactionTest.ts +++ b/packages/govern-tx/test/lib/transactions/AbstractTransactionTest.ts @@ -1,4 +1,6 @@ -import { Request } from '../../../lib//AbstractAction'; +// import { Request } from '../../../lib//AbstractAction'; +import { FastifySchema, FastifyRequest } from 'fastify' + import Provider from '../../../src/provider/Provider'; import Wallet from '../../../src/wallet/Wallet'; import { JsonFragment } from '@ethersproject/abi'; @@ -7,6 +9,8 @@ import { EthereumOptions } from '../../../src/config/Configuration'; import AbstractAction from '../../../lib/AbstractAction'; import ContractFunction from '../../../lib/transactions/ContractFunction'; import AbstractTransaction from '../../../lib/transactions/AbstractTransaction'; +import Whitelist from '../../../src/db/Whitelist'; +import Database from '../../../src/db/Database'; // Mocks class MockTransaction extends AbstractTransaction { @@ -16,6 +20,7 @@ class MockTransaction extends AbstractTransaction { jest.mock('../../../src/provider/Provider') jest.mock('../../../lib/transactions/ContractFunction') +jest.mock('../../../src/db/Whitelist') /** * AbstractTransaction test @@ -23,12 +28,16 @@ jest.mock('../../../lib/transactions/ContractFunction') describe('AbstractTransactionTest', () => { let txAction: MockTransaction, providerMock: Provider, - contractFunctionMock: ContractFunction + contractFunctionMock: ContractFunction, + whiteListMock: Whitelist beforeEach(() => { new Provider({} as EthereumOptions, {} as Wallet) providerMock = (Provider as jest.MockedClass).mock.instances[0] + new Whitelist({} as Database); + whiteListMock = (Whitelist as jest.MockedClass).mock.instances[0]; + ContractFunction.prototype.functionArguments = [{payload: {submitter: ''}}] txAction = new MockTransaction( @@ -36,10 +45,14 @@ describe('AbstractTransactionTest', () => { publicKey: '0x00' } as EthereumOptions, providerMock, + whiteListMock, { - message: 'MESSAGE' - } as Request + params: { + message: 'MESSAGE' + } + } as unknown as FastifyRequest ) + }) it('calls execute and returns the expected value', async () => { diff --git a/packages/govern-tx/test/lib/transactions/ContractFunctionTest.ts b/packages/govern-tx/test/lib/transactions/ContractFunctionTest.ts index a1df4ed83..e23efb04a 100644 --- a/packages/govern-tx/test/lib/transactions/ContractFunctionTest.ts +++ b/packages/govern-tx/test/lib/transactions/ContractFunctionTest.ts @@ -17,10 +17,10 @@ describe('ContractFunctionTest', () => { (defaultAbiCoder.decode as jest.MockedFunction).mockReturnValueOnce(['ARGUMENT']); (Fragment.fromObject as jest.MockedFunction).mockReturnValueOnce(fragmentMock as any); + // calldata(4 byte + arguments) + contractFunction = new ContractFunction({} as JsonFragment, '0x9f7b4579MESSAGE') - contractFunction = new ContractFunction({} as JsonFragment, 'MESSAGE') - - expect(defaultAbiCoder.decode).toHaveBeenNthCalledWith(1, 'INPUTS', 'MESSAGE') + expect(defaultAbiCoder.decode).toHaveBeenNthCalledWith(1, 'INPUTS', '0xMESSAGE') expect(contractFunction.functionArguments).toEqual(['ARGUMENT']) }) @@ -53,7 +53,7 @@ describe('ContractFunctionTest', () => { expect(contractFunction.decode()).toEqual(['DECODED']) - expect(defaultAbiCoder.decode).toHaveBeenNthCalledWith(1, 'INPUTS', 'MESSAGE') + expect(defaultAbiCoder.decode).toHaveBeenNthCalledWith(1, 'INPUTS', '0xMESSAGE') }) it('calls decode and throws as expected', () => { @@ -62,6 +62,6 @@ describe('ContractFunctionTest', () => { expect(() => contractFunction.decode()).toThrow('NOPE') - expect(defaultAbiCoder.decode).toHaveBeenNthCalledWith(1, 'INPUTS', 'MESSAGE') + expect(defaultAbiCoder.decode).toHaveBeenNthCalledWith(1, 'INPUTS', '0xMESSAGE') }) }) diff --git a/packages/govern-tx/test/lib/whitelist/AbstractWhitelistActionTest.ts b/packages/govern-tx/test/lib/whitelist/AbstractWhitelistActionTest.ts index 3053c3a78..78dd9d2d6 100644 --- a/packages/govern-tx/test/lib/whitelist/AbstractWhitelistActionTest.ts +++ b/packages/govern-tx/test/lib/whitelist/AbstractWhitelistActionTest.ts @@ -1,8 +1,14 @@ import Whitelist from '../../../src/db/Whitelist'; import AbstractWhitelistAction from '../../../lib/whitelist/AbstractWhitelistAction'; +import WhiteListParams from '../../../lib/whitelist/AbstractWhitelistAction' + +interface MockInterface { + message: string | any +} + // Mocks -class MockAction extends AbstractWhitelistAction { +class MockAction extends AbstractWhitelistAction{ public execute(): Promise { return Promise.resolve(true) } @@ -20,10 +26,10 @@ describe('AbstractWhitelistAction Test', () => { { message: { publicKey: '0x00', - rateLimit: 0 + txLimit: 0 }, signature: '' - } + } as any, ) }) @@ -33,7 +39,12 @@ describe('AbstractWhitelistAction Test', () => { type: 'object', required: ['message', 'signature'], properties: { - message: { type: 'string' }, + message: { + oneOf: [ + { type: 'string'}, + { type: 'object'} + ] + }, signature: { type: 'string' } } } diff --git a/packages/govern-tx/test/src/BootstrapTest.ts b/packages/govern-tx/test/src/BootstrapTest.ts index e61777ece..fd4504ac4 100644 --- a/packages/govern-tx/test/src/BootstrapTest.ts +++ b/packages/govern-tx/test/src/BootstrapTest.ts @@ -28,7 +28,8 @@ const fastifyMock: any = { delete: jest.fn(), get: jest.fn(), addHook: jest.fn(), - options: {} + options: {}, + FastifyRequest: jest.fn() } jest.mock('fastify', () => { return { @@ -96,22 +97,22 @@ describe('BootstrapTest', () => { switch(path) { case '/execute': expect(schemaObj).toEqual({schema: AbstractTransaction.schema}) - callback({params: true}) + callback(fastifyMock.FastifyRequest) executeTransactionMock = ExecuteTransaction.mock.instances[0] break case '/schedule': expect(schemaObj).toEqual({schema: AbstractTransaction.schema}) - callback({params: true}) + callback(fastifyMock.FastifyRequest) scheduleTransactionMock = ScheduleTransaction.mock.instances[0] break case '/challenge': expect(schemaObj).toEqual({schema: AbstractTransaction.schema}) - callback({params: true}) + callback(fastifyMock.FastifyRequest) challengeTransactionMock = ChallengeTransaction.mock.instances[0] break case '/whitelist': expect(schemaObj).toEqual({schema: AbstractWhitelistAction.schema}) - callback({params: true}) + callback(fastifyMock.FastifyRequest) addItemActionMock = AddItemAction.mock.instances[0] break } @@ -122,7 +123,7 @@ describe('BootstrapTest', () => { expect(schemaObj).toEqual({schema: AbstractWhitelistAction.schema}) - callback({params: true}) + callback(fastifyMock.FastifyRequest) deleteItemActionMock = DeleteItemAction.mock.instances[0] }) @@ -132,7 +133,7 @@ describe('BootstrapTest', () => { expect(schemaObj).toEqual({schema: AbstractWhitelistAction.schema}) - callback({params: true}) + callback(fastifyMock.FastifyRequest) getListActionMock = GetListAction.mock.instances[0] }) @@ -186,25 +187,22 @@ describe('BootstrapTest', () => { /************************************ * Expectations for all added routes ************************************/ - expect(ExecuteTransaction).toHaveBeenNthCalledWith(1, config.ethereum, Provider.mock.instances[0], true) + expect(ExecuteTransaction).toHaveBeenNthCalledWith(1, config.ethereum, Provider.mock.instances[0], Whitelist.mock.instances[0], fastifyMock.FastifyRequest) expect(executeTransactionMock.execute).toHaveBeenCalledTimes(1) - expect(ScheduleTransaction).toHaveBeenNthCalledWith(1, config.ethereum, Provider.mock.instances[0], true) + expect(ScheduleTransaction).toHaveBeenNthCalledWith(1, config.ethereum, Provider.mock.instances[0], Whitelist.mock.instances[0], fastifyMock.FastifyRequest) expect(scheduleTransactionMock.execute).toHaveBeenCalledTimes(1) - expect(ChallengeTransaction).toHaveBeenNthCalledWith(1, config.ethereum, Provider.mock.instances[0], true) + expect(ChallengeTransaction).toHaveBeenNthCalledWith(1, config.ethereum, Provider.mock.instances[0], Whitelist.mock.instances[0], fastifyMock.FastifyRequest) expect(challengeTransactionMock.execute).toHaveBeenCalledTimes(1) - expect(AddItemAction).toHaveBeenNthCalledWith(1, Whitelist.mock.instances[0], true) + expect(AddItemAction).toHaveBeenNthCalledWith(1, Whitelist.mock.instances[0], fastifyMock.FastifyRequest) expect(addItemActionMock.execute).toHaveBeenCalledTimes(1) - expect(DeleteItemAction).toHaveBeenNthCalledWith(1, Whitelist.mock.instances[0], true) + expect(DeleteItemAction).toHaveBeenNthCalledWith(1, Whitelist.mock.instances[0], fastifyMock.FastifyRequest) expect(deleteItemActionMock.execute).toHaveBeenCalledTimes(1) - expect(GetListAction).toHaveBeenNthCalledWith(1, Whitelist.mock.instances[0]) - expect(getListActionMock.execute).toHaveBeenCalledTimes(1) - - expect(fastifyMock.post).toHaveBeenCalledTimes(4) + expect(fastifyMock.post).toHaveBeenCalledTimes(5) expect(fastifyMock.delete).toHaveBeenCalledTimes(1) expect(fastifyMock.get).toHaveBeenCalledTimes(1) }) diff --git a/packages/govern-tx/test/src/db/AdminTest.ts b/packages/govern-tx/test/src/db/AdminTest.ts index 319c71a31..e30c24259 100644 --- a/packages/govern-tx/test/src/db/AdminTest.ts +++ b/packages/govern-tx/test/src/db/AdminTest.ts @@ -53,7 +53,7 @@ describe('AdminTest', () => { await expect(admin.addAdmin('0x00')).resolves.toEqual(true) - expect(databaseMock.query).toHaveBeenNthCalledWith(1, `INSERT INTO admins VALUES (0x00)`) + expect(databaseMock.query).toHaveBeenNthCalledWith(1, `INSERT INTO admins VALUES ('0x00')`) }) it('calls addAdmin and throws as expected', async () => { @@ -61,7 +61,7 @@ describe('AdminTest', () => { await expect(admin.addAdmin('0x00')).rejects.toEqual('NOPE') - expect(databaseMock.query).toHaveBeenNthCalledWith(1, `INSERT INTO admins VALUES (0x00)`) + expect(databaseMock.query).toHaveBeenNthCalledWith(1, `INSERT INTO admins VALUES ('0x00')`) }) it('calls deleteAdmin and returns true', async () => { diff --git a/packages/govern-tx/test/src/db/WhitelistTest.ts b/packages/govern-tx/test/src/db/WhitelistTest.ts index 688e937f1..a172ac70a 100644 --- a/packages/govern-tx/test/src/db/WhitelistTest.ts +++ b/packages/govern-tx/test/src/db/WhitelistTest.ts @@ -65,7 +65,7 @@ describe('WhitelistTest', () => { }) it('calls getItemByKey and returns the expected value', async () => { - (databaseMock.query as jest.MockedFunction).mockReturnValueOnce(Promise.resolve(true)) + (databaseMock.query as jest.MockedFunction).mockReturnValueOnce(Promise.resolve([true])) await expect(whitelist.getItemByKey('0x00')).resolves.toEqual(true) @@ -85,7 +85,7 @@ describe('WhitelistTest', () => { await expect(whitelist.addItem('0x00', 0)).resolves.toEqual(true) - expect(databaseMock.query).toHaveBeenNthCalledWith(1, `INSERT INTO whitelist VALUES (0x00, 0)`) + expect(databaseMock.query).toHaveBeenNthCalledWith(1, `INSERT INTO whitelist (PublicKey, TxLimit) VALUES ('0x00', '0')`) }) it('calls addItem and throws as expected', async () => { @@ -93,7 +93,7 @@ describe('WhitelistTest', () => { await expect(whitelist.addItem('0x00', 0)).rejects.toEqual('NOPE') - expect(databaseMock.query).toHaveBeenNthCalledWith(1, `INSERT INTO whitelist VALUES (0x00, 0)`) + expect(databaseMock.query).toHaveBeenNthCalledWith(1, `INSERT INTO whitelist (PublicKey, TxLimit) VALUES ('0x00', '0')`) }) it('calls deleteItem and returns true', async () => { diff --git a/packages/govern-tx/test/src/transactions/challenge/ChallengeTransactionTest.ts b/packages/govern-tx/test/src/transactions/challenge/ChallengeTransactionTest.ts index fa8b84e72..e0bb5d057 100644 --- a/packages/govern-tx/test/src/transactions/challenge/ChallengeTransactionTest.ts +++ b/packages/govern-tx/test/src/transactions/challenge/ChallengeTransactionTest.ts @@ -1,8 +1,8 @@ import { EthereumOptions } from '../../../../src/config/Configuration' -import { Request } from '../../../../lib/AbstractAction' import Provider from '../../../../src/provider/Provider' import * as challengeABI from '../../../../src/transactions/challenge/challenge.json' import ChallengeTransaction from '../../../../src/transactions/challenge/ChallengeTransaction' +import Whitelist from '../../../../src/db/Whitelist'; // Mocks jest.mock('../../../../lib/transactions/AbstractTransaction') @@ -17,7 +17,8 @@ describe('ChallengeTransactionTest', () => { challengeTransaction = new ChallengeTransaction( {} as EthereumOptions, {} as Provider, - {} as Request + {} as Whitelist, + {} as any ) }) diff --git a/packages/govern-tx/test/src/transactions/execute/ExecuteTransactionTest.ts b/packages/govern-tx/test/src/transactions/execute/ExecuteTransactionTest.ts index a50d126ba..f6c404f64 100644 --- a/packages/govern-tx/test/src/transactions/execute/ExecuteTransactionTest.ts +++ b/packages/govern-tx/test/src/transactions/execute/ExecuteTransactionTest.ts @@ -1,8 +1,8 @@ import { EthereumOptions } from '../../../../src/config/Configuration' -import { Request } from '../../../../lib/AbstractAction' import Provider from '../../../../src/provider/Provider' import * as executeABI from '../../../../src/transactions/execute/execute.json' import ExecuteTransaction from '../../../../src/transactions/execute/ExecuteTransaction' +import Whitelist from '../../../../src/db/Whitelist'; // Mocks jest.mock('../../../../lib/transactions/AbstractTransaction') @@ -17,7 +17,8 @@ describe('ExecuteTransactionTest', () => { executeTransaction = new ExecuteTransaction( {} as EthereumOptions, {} as Provider, - {} as Request + {} as Whitelist, + {} as any ) }) diff --git a/packages/govern-tx/test/src/transactions/schedule/ScheduleTransactionTest.ts b/packages/govern-tx/test/src/transactions/schedule/ScheduleTransactionTest.ts index 48b6b14b0..86341fe14 100644 --- a/packages/govern-tx/test/src/transactions/schedule/ScheduleTransactionTest.ts +++ b/packages/govern-tx/test/src/transactions/schedule/ScheduleTransactionTest.ts @@ -1,8 +1,8 @@ import { EthereumOptions } from '../../../../src/config/Configuration' -import { Request } from '../../../../lib/AbstractAction' import Provider from '../../../../src/provider/Provider' import * as scheduleABI from '../../../../src/transactions/schedule/schedule.json' import ScheduleTransaction from '../../../../src/transactions/schedule/ScheduleTransaction' +import Whitelist from '../../../../src/db/Whitelist'; // Mocks jest.mock('../../../../lib/transactions/AbstractTransaction') @@ -17,7 +17,8 @@ describe('ScheduleTransactionTest', () => { scheduleTransaction = new ScheduleTransaction( {} as EthereumOptions, {} as Provider, - {} as Request + {} as Whitelist, + {} as any ) }) diff --git a/packages/govern-tx/test/src/wallet/WalletTest.ts b/packages/govern-tx/test/src/wallet/WalletTest.ts index d8a6ce7e1..afa8dbed0 100644 --- a/packages/govern-tx/test/src/wallet/WalletTest.ts +++ b/packages/govern-tx/test/src/wallet/WalletTest.ts @@ -24,8 +24,8 @@ describe('WalletTest', () => { wallet = new Wallet(databaseMock) }) - it.skip('calls sign and returns the expected value', async () => { - (databaseMock.query as jest.MockedFunction).mockReturnValueOnce(Promise.resolve('0x01')); + it('calls sign and returns the expected value', async () => { + (databaseMock.query as jest.MockedFunction).mockReturnValueOnce(Promise.resolve(['0x01'])); (EthersWallet.prototype.signTransaction as jest.MockedFunction).mockReturnValueOnce(Promise.resolve('0x02')) @@ -38,7 +38,7 @@ describe('WalletTest', () => { expect(EthersWallet.prototype.signTransaction).toHaveBeenNthCalledWith(1, {from: '0x00'}) }) - it.skip('calls sign and throws as expected', async () => { + it('calls sign and throws as expected', async () => { (databaseMock.query as jest.MockedFunction).mockReturnValueOnce(Promise.reject('NOPE')); await expect(wallet.sign({} as TransactionRequest, '0x00')).rejects.toEqual('NOPE') @@ -46,22 +46,10 @@ describe('WalletTest', () => { expect(databaseMock.query).toHaveBeenNthCalledWith(1, `SELECT PrivateKey FROM wallet WHERE PublicKey='0x00'`) }) - it('calls sign and uses the already loaded wallet', async () => { - (EthersWallet.prototype.signTransaction as jest.MockedFunction).mockReturnValue(Promise.resolve('0x02')); - - await expect(wallet.sign({} as TransactionRequest, '0x00')).resolves.toEqual('0x02') - await expect(wallet.sign({} as TransactionRequest, '0x00')).resolves.toEqual('0x02') - - expect(databaseMock.query).toHaveBeenCalledTimes(1) - - expect(EthersWallet.prototype.signTransaction).toHaveBeenNthCalledWith(1, {from: '0x00'}) - - expect(EthersWallet.prototype.signTransaction).toHaveBeenNthCalledWith(2, {from: '0x00'}) - }) - it.skip('calls sign with another publicKey and releads to wallet for it', async () => { - (databaseMock.query as jest.MockedFunction).mockReturnValueOnce(Promise.resolve('0x01')); - (databaseMock.query as jest.MockedFunction).mockReturnValueOnce(Promise.resolve('0x03')); + it('calls sign with another publicKey and releads to wallet for it', async () => { + (databaseMock.query as jest.MockedFunction).mockReturnValueOnce(Promise.resolve(['0x01'])); + (databaseMock.query as jest.MockedFunction).mockReturnValueOnce(Promise.resolve(['0x03'])); (EthersWallet.prototype.signTransaction as jest.MockedFunction).mockReturnValue(Promise.resolve('0x02')) @@ -78,4 +66,4 @@ describe('WalletTest', () => { expect(EthersWallet.prototype.signTransaction).toHaveBeenNthCalledWith(2, {from: '0x01'}) }) -}) +}) \ No newline at end of file diff --git a/packages/govern-tx/test/src/whitelist/AddItemActionTest.ts b/packages/govern-tx/test/src/whitelist/AddItemActionTest.ts index e139df26d..26712b763 100644 --- a/packages/govern-tx/test/src/whitelist/AddItemActionTest.ts +++ b/packages/govern-tx/test/src/whitelist/AddItemActionTest.ts @@ -1,9 +1,9 @@ import { isAddress } from '@ethersproject/address'; -import { WhitelistRequest } from '../../../lib/whitelist/AbstractWhitelistAction'; import Database from '../../../src/db/Database'; import Whitelist, { ListItem } from '../../../src/db/Whitelist'; import AddItemAction from '../../../src/whitelist/AddItemAction'; + // Mocks jest.mock('../../../src/db/Whitelist') jest.mock('@ethersproject/address') @@ -15,12 +15,14 @@ describe('AddItemActionTest', () => { let addItemAction: AddItemAction, whitelistMock: Whitelist - const request: WhitelistRequest = { - message: { - publicKey: '0x00', - rateLimit: 1 - }, - signature: '' + const request = { + body : { + message: { + publicKey: '0x00', + txLimit: 1 + }, + signature: '' + } } beforeEach(() => { @@ -31,7 +33,7 @@ describe('AddItemActionTest', () => { it('calls validateRequest and returns the expected values', () => { (isAddress as jest.MockedFunction).mockReturnValueOnce(true) - addItemAction = new AddItemAction(whitelistMock, request) + addItemAction = new AddItemAction(whitelistMock, request as any) expect(isAddress).toHaveBeenNthCalledWith(1, '0x00') }) @@ -40,7 +42,7 @@ describe('AddItemActionTest', () => { (isAddress as jest.MockedFunction).mockReturnValueOnce(false) expect(() => { - addItemAction = new AddItemAction(whitelistMock, request) + addItemAction = new AddItemAction(whitelistMock, request as any) }).toThrow('Invalid public key passed!') expect(isAddress).toHaveBeenNthCalledWith(1, '0x00') @@ -49,15 +51,15 @@ describe('AddItemActionTest', () => { it('calls validateRequest and throws because of a invalid rate limit', () => { (isAddress as jest.MockedFunction).mockReturnValueOnce(true) - request.message.rateLimit = 0; + request.body.message.txLimit = 0; expect(() => { - addItemAction = new AddItemAction(whitelistMock, request) + addItemAction = new AddItemAction(whitelistMock, request as any) }).toThrow('Invalid rate limit passed!') expect(isAddress).toHaveBeenNthCalledWith(1, '0x00') - request.message.rateLimit = 1; + request.body.message.txLimit = 1; }) it('calls execute and returns the expected result', async () => { @@ -65,7 +67,7 @@ describe('AddItemActionTest', () => { (whitelistMock.addItem as jest.MockedFunction).mockReturnValueOnce(Promise.resolve({} as ListItem)); - addItemAction = new AddItemAction(whitelistMock, request) + addItemAction = new AddItemAction(whitelistMock, request as any) await expect(addItemAction.execute()).resolves.toEqual({}) @@ -77,7 +79,7 @@ describe('AddItemActionTest', () => { (whitelistMock.addItem as jest.MockedFunction).mockReturnValueOnce(Promise.reject('NOPE')); - addItemAction = new AddItemAction(whitelistMock, request) + addItemAction = new AddItemAction(whitelistMock, request as any) await expect(addItemAction.execute()).rejects.toEqual('NOPE') diff --git a/packages/govern-tx/test/src/whitelist/DeleteItemActionTest.ts b/packages/govern-tx/test/src/whitelist/DeleteItemActionTest.ts index acada6190..78d4ce727 100644 --- a/packages/govern-tx/test/src/whitelist/DeleteItemActionTest.ts +++ b/packages/govern-tx/test/src/whitelist/DeleteItemActionTest.ts @@ -1,5 +1,4 @@ import { isAddress } from '@ethersproject/address'; -import { WhitelistRequest } from '../../../lib/whitelist/AbstractWhitelistAction'; import Database from '../../../src/db/Database'; import Whitelist, { ListItem } from '../../../src/db/Whitelist'; import DeleteItemAction from '../../../src/whitelist/DeleteItemAction'; @@ -15,11 +14,13 @@ describe('DeleteItemActionTest', () => { let deleteItemAction: DeleteItemAction, whitelistMock: Whitelist - const request: WhitelistRequest = { - message: { - publicKey: '0x00' - }, - signature: '' + const request = { + body: { + message: { + publicKey: '0x00' + }, + signature: '' + } } beforeEach(() => { @@ -30,7 +31,7 @@ describe('DeleteItemActionTest', () => { it('calls validateRequest and returns the expected values', () => { (isAddress as jest.MockedFunction).mockReturnValueOnce(true) - deleteItemAction = new DeleteItemAction(whitelistMock, request) + deleteItemAction = new DeleteItemAction(whitelistMock, request as any) expect(isAddress).toHaveBeenNthCalledWith(1, '0x00') }) @@ -39,7 +40,7 @@ describe('DeleteItemActionTest', () => { (isAddress as jest.MockedFunction).mockReturnValueOnce(false) expect(() => { - deleteItemAction = new DeleteItemAction(whitelistMock, request) + deleteItemAction = new DeleteItemAction(whitelistMock, request as any) }).toThrow('Invalid public key passed!') expect(isAddress).toHaveBeenNthCalledWith(1, '0x00') @@ -50,7 +51,7 @@ describe('DeleteItemActionTest', () => { (whitelistMock.deleteItem as jest.MockedFunction).mockReturnValueOnce(Promise.resolve(true)); - deleteItemAction = new DeleteItemAction(whitelistMock, request) + deleteItemAction = new DeleteItemAction(whitelistMock, request as any) await expect(deleteItemAction.execute()).resolves.toEqual(true) @@ -62,7 +63,7 @@ describe('DeleteItemActionTest', () => { (whitelistMock.deleteItem as jest.MockedFunction).mockReturnValueOnce(Promise.reject('NOPE')); - deleteItemAction = new DeleteItemAction(whitelistMock, request) + deleteItemAction = new DeleteItemAction(whitelistMock, request as any) await expect(deleteItemAction.execute()).rejects.toEqual('NOPE') diff --git a/packages/govern-tx/test/src/whitelist/GetListActionTest.ts b/packages/govern-tx/test/src/whitelist/GetListActionTest.ts index 203ad8ac0..81c1bbcbc 100644 --- a/packages/govern-tx/test/src/whitelist/GetListActionTest.ts +++ b/packages/govern-tx/test/src/whitelist/GetListActionTest.ts @@ -1,5 +1,4 @@ import { isAddress } from '@ethersproject/address'; -import { WhitelistRequest } from '../../../lib/whitelist/AbstractWhitelistAction'; import Database from '../../../src/db/Database'; import Whitelist, { ListItem } from '../../../src/db/Whitelist'; import GetListAction from '../../../src/whitelist/GetListAction'; @@ -25,7 +24,7 @@ describe('GetListActionTest', () => { (whitelistMock.getList as jest.MockedFunction).mockReturnValueOnce(Promise.resolve([{}] as ListItem[])); - getListAction = new GetListAction(whitelistMock) + getListAction = new GetListAction(whitelistMock, {} as any) await expect(getListAction.execute()).resolves.toEqual([{}]) @@ -37,7 +36,7 @@ describe('GetListActionTest', () => { (whitelistMock.getList as jest.MockedFunction).mockReturnValueOnce(Promise.reject('NOPE')); - getListAction = new GetListAction(whitelistMock) + getListAction = new GetListAction(whitelistMock, {} as any) await expect(getListAction.execute()).rejects.toEqual('NOPE')