From 0dd4b5d5934a4ff9da92b268895966a3d5cd4014 Mon Sep 17 00:00:00 2001 From: Mike Shultz Date: Tue, 23 Apr 2019 19:14:33 -0600 Subject: [PATCH] event-cache package (#1975) * initial commit of event-cache * I should learn how to spell * added IndexedDBBackend * forgot the IndexedDBBackend serializer methods * improved docstrings for IndexedDBBackend module * Run tests for event-cache on travis CI * fix path to mocha in event-cache package.json * attempt to fix missing mocha on travis build * might help if I altered the correct job * set NODE_ENV for travis job and moved mocha to dependencies * bootstrap, specifically @origin/contracts does not like NODE_ENV=test * travis debugging * trying relative path to mocha again in npm script * Travis! debugging * moving event-cache job def up higher in the order * missing --scope from bootstrap command * added missing --scope for @origin/ipfs * missing babel packages from event-cache/package.json * removed debugging from .travis.yml * fixed docstring and improved README * docstring fix * removes event table from discovery * added PostgreSQLBackend to event-cache * added --migrations-path to npm migration script for event-cache * added --config to migrate npm script * pg must be installed explicitly, apparently * can't index a column that doesn't exist * load latestBlock from DB if the backend is persistent * start event fetch from latest known to the cache unless otherwise provided * make it prettier * Added PostgreSQLBackend to platform resolution * previous created_at field was for blocktime, as stolen from discovery. Not necessary here. * Make postgres backend's loadSerialized error more informative * run migrations in the event-listener container * don't allow the latestBlock to be walked back and a random eslint error that just showed up * set timestamps:false to prevent sequelize from attempting to use them * removes idb, adds Dexie, adds support for array "OR" args, and fixes bugs in memory backend * merged master * updates graphql, fixes a bug where latest block would not be fetched, adds event ordering, moved pg/sequelize to optionalDependencies, refacored to use require/module.exports, mobile will use memory for now, added webpack config * deploy contracts for testing, and removed some localStorage stuff from a hinky merge * prettier * Try to make travis happy * removed bad sortBy and made sure all returnValues args are strings, same as events * fixing some missed EventCache calls in User resolver * Increate test timeout * removed garbage * use a unique IndexedDB database for each contract instance, and let graphql manage the blockNumber if it wants * address isn't always defined for a contract * WIP mainnet fixes * Efficiency fixes * Removed listener code no longer needed for Nick's event cache. * Event cache updates * IPFS cache * make background grey only in profile view (#2060) * Fix Docker Compose for new contract configuration method (#2102) * Replace linker-client with mobile-bridge in deployments * Add open port in services in Docker to indicate ready * Update service configs * Fix fixtures and environment variables * Update FractionalListing price calculation and booking availability (#2104) * Update nightly price estimation for fractional listing * Make checkout slots as available * Prettify stuffs * Update deployed image tag for origin-dapp [ci skip] * Update deployed image tag for origin-dapp [ci skip] * Update deployed image tag for origin-dapp [ci skip] * Add country column to identity table (#2083) * Add country column to identity table * tweak comments * Fix unit tests * linter * prettier * Make call to handleEvent synchronous * Update deployed image tag for origin-growth [ci skip] * Update deployed image tag for event-listener [ci skip] * New Crowdin translations (#1863) * New translations all-messages.json (Spanish) * New translations all-messages.json (Slovenian) * New translations all-messages.json (Slovak) * New translations all-messages.json (Serbian (Latin)) * New translations all-messages.json (Russian) * New translations all-messages.json (Romanian) * New translations all-messages.json (Portuguese) * New translations all-messages.json (Polish) * New translations all-messages.json (Lao) * New translations all-messages.json (Japanese) * New translations all-messages.json (Bosnian) * New translations all-messages.json (Italian) * New translations all-messages.json (Indonesian) * New translations all-messages.json (Hindi) * New translations all-messages.json (Greek) * New translations all-messages.json (German) * New translations all-messages.json (French) * New translations all-messages.json (Filipino) * New translations all-messages.json (Esperanto) * New translations all-messages.json (Dutch) * New translations all-messages.json (Danish) * New translations all-messages.json (Czech) * New translations all-messages.json (Croatian) * New translations all-messages.json (Chinese Traditional) * New translations all-messages.json (Chinese Simplified) * New translations all-messages.json (Acholi) * New translations all-messages.json (Bengali) * New translations all-messages.json (Korean) * New translations all-messages.json (Vietnamese) * New translations all-messages.json (Ukrainian) * New translations all-messages.json (Turkish) * New translations all-messages.json (Thai) * New translations all-messages.json (Telugu) * New translations all-messages.json (Spanish) * New translations all-messages.json (Slovenian) * New translations all-messages.json (Slovak) * New translations all-messages.json (Serbian (Latin)) * New translations all-messages.json (Russian) * New translations all-messages.json (Romanian) * New translations all-messages.json (Portuguese) * New translations all-messages.json (Polish) * New translations all-messages.json (Lao) * New translations all-messages.json (Japanese) * New translations all-messages.json (Bosnian) * New translations all-messages.json (Italian) * New translations all-messages.json (Indonesian) * New translations all-messages.json (Hindi) * New translations all-messages.json (Greek) * New translations all-messages.json (German) * New translations all-messages.json (French) * New translations all-messages.json (Filipino) * New translations all-messages.json (Esperanto) * New translations all-messages.json (Dutch) * New translations all-messages.json (Danish) * New translations all-messages.json (Czech) * New translations all-messages.json (Croatian) * New translations all-messages.json (Chinese Traditional) * New translations all-messages.json (Chinese Simplified) * New translations all-messages.json (Acholi) * New translations all-messages.json (Russian) * New translations all-messages.json (Russian) * New translations all-messages.json (Russian) * New translations all-messages.json (Chinese Simplified) * New translations all-messages.json (Chinese Simplified) * New translations all-messages.json (Chinese Simplified) * New translations all-messages.json (Chinese Simplified) * New translations all-messages.json (Chinese Simplified) * New translations all-messages.json (Korean) * New translations all-messages.json (Russian) * New translations all-messages.json (Bengali) * New translations all-messages.json (Korean) * New translations all-messages.json (Vietnamese) * New translations all-messages.json (Ukrainian) * New translations all-messages.json (Turkish) * New translations all-messages.json (Thai) * New translations all-messages.json (Telugu) * New translations all-messages.json (Spanish) * New translations all-messages.json (Slovenian) * New translations all-messages.json (Slovak) * New translations all-messages.json (Serbian (Latin)) * New translations all-messages.json (Russian) * New translations all-messages.json (Romanian) * New translations all-messages.json (Portuguese) * New translations all-messages.json (Polish) * New translations all-messages.json (Lao) * New translations all-messages.json (Japanese) * New translations all-messages.json (Bosnian) * New translations all-messages.json (Italian) * New translations all-messages.json (Indonesian) * New translations all-messages.json (Hindi) * New translations all-messages.json (Greek) * New translations all-messages.json (German) * New translations all-messages.json (French) * New translations all-messages.json (Filipino) * New translations all-messages.json (Esperanto) * New translations all-messages.json (Dutch) * New translations all-messages.json (Danish) * New translations all-messages.json (Czech) * New translations all-messages.json (Croatian) * New translations all-messages.json (Chinese Traditional) * New translations all-messages.json (Chinese Simplified) * New translations all-messages.json (Acholi) * New translations all-messages.json (Russian) * New translations all-messages.json (Italian) * New translations all-messages.json (Russian) * New translations all-messages.json (Russian) * New translations all-messages.json (Japanese) * New translations all-messages.json (Russian) * New translations all-messages.json (Russian) * New translations all-messages.json (Russian) * New translations all-messages.json (Russian) * New translations all-messages.json (Russian) * New translations all-messages.json (Italian) * New translations all-messages.json (Italian) * New translations all-messages.json (Italian) * New translations all-messages.json (Bengali) * New translations all-messages.json (Indonesian) * New translations all-messages.json (Slovenian) * New translations all-messages.json (Slovak) * New translations all-messages.json (Serbian (Latin)) * New translations all-messages.json (Russian) * New translations all-messages.json (Romanian) * New translations all-messages.json (Portuguese) * New translations all-messages.json (Polish) * New translations all-messages.json (Lao) * New translations all-messages.json (Korean) * New translations all-messages.json (Bosnian) * New translations all-messages.json (Italian) * New translations all-messages.json (Japanese) * New translations all-messages.json (Hindi) * New translations all-messages.json (Danish) * New translations all-messages.json (Chinese Simplified) * New translations all-messages.json (Chinese Traditional) * New translations all-messages.json (Greek) * New translations all-messages.json (Czech) * New translations all-messages.json (Croatian) * New translations all-messages.json (Dutch) * New translations all-messages.json (Esperanto) * New translations all-messages.json (Filipino) * New translations all-messages.json (French) * New translations all-messages.json (German) * New translations all-messages.json (Spanish) * New translations all-messages.json (Telugu) * New translations all-messages.json (Thai) * New translations all-messages.json (Turkish) * New translations all-messages.json (Ukrainian) * New translations all-messages.json (Vietnamese) * New translations all-messages.json (Acholi) * New translations all-messages.json (Russian) * New translations all-messages.json (Russian) * New translations all-messages.json (Russian) * New translations all-messages.json (Russian) * New translations all-messages.json (Russian) * New translations all-messages.json (Russian) * New translations all-messages.json (Russian) * New translations all-messages.json (Japanese) * New translations all-messages.json (Japanese) * New translations all-messages.json (Italian) * New translations all-messages.json (Italian) * New translations all-messages.json (German) * New translations all-messages.json (Italian) * New translations all-messages.json (German) * New translations all-messages.json (Bengali) * New translations all-messages.json (Korean) * New translations all-messages.json (Vietnamese) * New translations all-messages.json (Ukrainian) * New translations all-messages.json (Turkish) * New translations all-messages.json (Thai) * New translations all-messages.json (Telugu) * New translations all-messages.json (Spanish) * New translations all-messages.json (Slovenian) * New translations all-messages.json (Slovak) * New translations all-messages.json (Serbian (Latin)) * New translations all-messages.json (Russian) * New translations all-messages.json (Romanian) * New translations all-messages.json (Portuguese) * New translations all-messages.json (Polish) * New translations all-messages.json (Lao) * New translations all-messages.json (Japanese) * New translations all-messages.json (Bosnian) * New translations all-messages.json (Italian) * New translations all-messages.json (Indonesian) * New translations all-messages.json (Hindi) * New translations all-messages.json (Greek) * New translations all-messages.json (German) * New translations all-messages.json (French) * New translations all-messages.json (Filipino) * New translations all-messages.json (Esperanto) * New translations all-messages.json (Dutch) * New translations all-messages.json (Danish) * New translations all-messages.json (Czech) * New translations all-messages.json (Croatian) * New translations all-messages.json (Chinese Traditional) * New translations all-messages.json (Chinese Simplified) * New translations all-messages.json (Acholi) * New translations all-messages.json (Italian) * New translations all-messages.json (Russian) * New translations all-messages.json (Italian) * New translations all-messages.json (Russian) * New translations all-messages.json (Japanese) * New translations all-messages.json (Greek) * New translations all-messages.json (Greek) * New translations all-messages.json (Chinese Simplified) * New translations all-messages.json (Chinese Simplified) * New translations all-messages.json (Chinese Simplified) * New translations all-messages.json (Chinese Simplified) * New translations all-messages.json (Chinese Traditional) * New translations all-messages.json (Chinese Traditional) * New translations all-messages.json (Chinese Simplified) * New translations all-messages.json (Bengali) * New translations all-messages.json (Korean) * New translations all-messages.json (Vietnamese) * New translations all-messages.json (Ukrainian) * New translations all-messages.json (Turkish) * New translations all-messages.json (Thai) * New translations all-messages.json (Telugu) * New translations all-messages.json (Spanish) * New translations all-messages.json (Slovenian) * New translations all-messages.json (Slovak) * New translations all-messages.json (Serbian (Latin)) * New translations all-messages.json (Russian) * New translations all-messages.json (Romanian) * New translations all-messages.json (Portuguese) * New translations all-messages.json (Polish) * New translations all-messages.json (Lao) * New translations all-messages.json (Japanese) * New translations all-messages.json (Bosnian) * New translations all-messages.json (Italian) * New translations all-messages.json (Indonesian) * New translations all-messages.json (Hindi) * New translations all-messages.json (Greek) * New translations all-messages.json (German) * New translations all-messages.json (French) * New translations all-messages.json (Filipino) * New translations all-messages.json (Esperanto) * New translations all-messages.json (Dutch) * New translations all-messages.json (Danish) * New translations all-messages.json (Czech) * New translations all-messages.json (Croatian) * New translations all-messages.json (Chinese Traditional) * New translations all-messages.json (Chinese Simplified) * New translations all-messages.json (Acholi) * New translations all-messages.json (Korean) * New translations all-messages.json (Korean) * New translations all-messages.json (Russian) * New translations all-messages.json (Russian) * New translations all-messages.json (Korean) * New translations all-messages.json (Korean) * New translations all-messages.json (Korean) * Update deployed image tag for origin-dapp [ci skip] * Misc mobile fixes (#2106) * Fix spelling mistake * Handle global Notification on mobile * Decrease balance poll interval * Run prettier * Tweak balance poll interval * Update deployed image tag for origin-dapp [ci skip] * Event cache dapp fixes * Re-add indexeddb * Add event-cache to dockerfiles * Disable Postgres backend for now * Revert "Disable Postgres backend for now" This reverts commit 365d04a5ffe60165e67d28bb43cf7a14d0090a0f. * Do not insert in the old event table * Lint * Fix for fresh deploy * GraphQL test fix * Block range fix * Trigger build --- .travis.yml | 22 +- Dockerfile | 1 + .../src/pages/transactions/Listings.js | 2 +- devops/dockerfiles/event-listener | 4 +- devops/dockerfiles/origin-admin | 1 + devops/dockerfiles/origin-cron | 1 + devops/dockerfiles/origin-dapp | 1 + devops/dockerfiles/origin-dapp-creator-client | 1 + devops/dockerfiles/origin-discovery | 1 + devops/dockerfiles/origin-growth | 1 + docker-compose.yml | 2 + .../20190410191046-event-drop-table.js | 62 ++ infra/discovery/src/listener/handler.js | 18 - infra/discovery/src/listener/listener.js | 4 - infra/discovery/src/models/event.js | 39 - package-lock.json | 774 +++++++++--------- packages/event-cache/README.md | 96 +++ .../20190410232631-event-create-table.js | 78 ++ packages/event-cache/package.json | 71 ++ packages/event-cache/src/EventCache.js | 282 +++++++ .../src/backends/AbstractBackend.js | 83 ++ .../src/backends/InMemoryBackend.js | 112 +++ .../src/backends/IndexedDBBackend.js | 381 +++++++++ .../src/backends/PostgreSQLBackend.js | 224 +++++ packages/event-cache/src/backends/browser.js | 7 + packages/event-cache/src/backends/index.js | 9 + packages/event-cache/src/index.js | 21 + packages/event-cache/src/models/event.js | 48 ++ packages/event-cache/src/models/index.js | 43 + packages/event-cache/src/pgconfig.js | 44 + packages/event-cache/src/utils.js | 130 +++ packages/event-cache/test/_contracts.js | 21 + packages/event-cache/test/_ipfs.js | 9 + packages/event-cache/test/const.js | 13 + packages/event-cache/test/index.js | 194 +++++ packages/event-cache/test/indexeddb.js | 142 ++++ packages/event-cache/test/postgres.js | 171 ++++ packages/event-cache/test/setup.js | 26 + packages/event-cache/webpack.config.js | 40 + packages/eventsource/src/index.js | 18 +- packages/graphql/package.json | 1 + packages/graphql/src/configs/mainnet.js | 14 +- packages/graphql/src/configs/rinkeby.js | 10 +- packages/graphql/src/contracts.js | 43 +- .../graphql/src/resolvers/IdentityEvents.js | 7 +- packages/graphql/src/resolvers/Listing.js | 16 +- packages/graphql/src/resolvers/Offer.js | 20 +- packages/graphql/src/resolvers/User.js | 123 +-- .../src/resolvers/marketplace/listings.js | 9 +- packages/graphql/test/_helpers.js | 3 +- packages/graphql/test/index.js | 3 +- 51 files changed, 2886 insertions(+), 560 deletions(-) create mode 100644 infra/discovery/migrations/20190410191046-event-drop-table.js delete mode 100644 infra/discovery/src/models/event.js create mode 100644 packages/event-cache/README.md create mode 100644 packages/event-cache/migrations/20190410232631-event-create-table.js create mode 100644 packages/event-cache/package.json create mode 100644 packages/event-cache/src/EventCache.js create mode 100644 packages/event-cache/src/backends/AbstractBackend.js create mode 100644 packages/event-cache/src/backends/InMemoryBackend.js create mode 100644 packages/event-cache/src/backends/IndexedDBBackend.js create mode 100644 packages/event-cache/src/backends/PostgreSQLBackend.js create mode 100644 packages/event-cache/src/backends/browser.js create mode 100644 packages/event-cache/src/backends/index.js create mode 100644 packages/event-cache/src/index.js create mode 100644 packages/event-cache/src/models/event.js create mode 100644 packages/event-cache/src/models/index.js create mode 100644 packages/event-cache/src/pgconfig.js create mode 100644 packages/event-cache/src/utils.js create mode 100644 packages/event-cache/test/_contracts.js create mode 100644 packages/event-cache/test/_ipfs.js create mode 100644 packages/event-cache/test/const.js create mode 100644 packages/event-cache/test/index.js create mode 100644 packages/event-cache/test/indexeddb.js create mode 100644 packages/event-cache/test/postgres.js create mode 100644 packages/event-cache/test/setup.js create mode 100644 packages/event-cache/webpack.config.js diff --git a/.travis.yml b/.travis.yml index f32237ebe8d3..4202b1c13890 100644 --- a/.travis.yml +++ b/.travis.yml @@ -33,10 +33,28 @@ matrix: node_js: 10 install: - npm install --ignore-scripts - - npm run bootstrap -- --scope @origin/contracts --scope @origin/eventsource --scope @origin/ipfs --scope @origin/messaging-client --scope @origin/services --scope @origin/validator --scope @origin/mobile-bridge --scope @origin/graphql + - npm run bootstrap -- --scope @origin/contracts --scope @origin/eventsource --scope @origin/ipfs --scope @origin/messaging-client --scope @origin/services --scope @origin/validator --scope @origin/mobile-bridge --scope @origin/graphql --scope @origin/event-cache script: - npm run test --prefix packages/graphql + - name: "@origin/event-cache unit tests" + language: node_js + node_js: 10 + addons: + postgresql: 9.6 + services: + - postgresql + env: + - DATABASE_URL=postgres://postgres@localhost/travis_ci_test + before_script: + - psql -c 'create database travis_ci_test;' -U postgres + - lerna run migrate --scope @origin/event-cache + install: + - npm install --ignore-scripts + - npm run bootstrap -- --scope @origin/contracts --scope @origin/eventsource --scope @origin/ipfs --scope @origin/messaging-client --scope @origin/services --scope @origin/validator --scope @origin/mobile-bridge --scope @origin/graphql --scope @origin/event-cache + script: + - npm run test --prefix packages/event-cache + - name: "@origin/bridge unit tests" language: node_js node_js: 10 @@ -103,7 +121,7 @@ matrix: - lerna run migrate --scope @origin/identity install: - npm install --ignore-scripts - - npm run bootstrap -- --scope @origin/contracts --scope @origin/eventsource --scope @origin/ipfs --scope @origin/messaging-client --scope @origin/services --scope @origin/validator --scope @origin/mobile-bridge --scope @origin/graphql --scope @origin/bridge --scope @origin/identity --scope @origin/token --scope @origin/growth + - npm run bootstrap -- --scope @origin/contracts --scope @origin/eventsource --scope @origin/ipfs --scope @origin/messaging-client --scope @origin/services --scope @origin/validator --scope @origin/mobile-bridge --scope @origin/graphql --scope @origin/bridge --scope @origin/identity --scope @origin/token --scope @origin/growth --scope @origin/event-cache script: - npm run test --prefix infra/growth diff --git a/Dockerfile b/Dockerfile index ec79f533f152..97ecb206d6db 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,6 +15,7 @@ COPY ./packages/validator/package.json ./packages/validator/ COPY ./packages/messaging-client/package.json ./packages/messaging-client/ COPY ./packages/mobile-bridge/package.json ./packages/mobile-bridge/ COPY ./packages/eventsource/package.json ./packages/eventsource/ +COPY ./packages/event-cache/package.json ./packages/event-cache/ COPY ./packages/services/package.json ./packages/services/ COPY ./packages/token/package.json ./packages/token/ COPY ./infra/discovery/package.json ./infra/discovery/ diff --git a/dapps/marketplace/src/pages/transactions/Listings.js b/dapps/marketplace/src/pages/transactions/Listings.js index 93b9e690ae29..c4254b8122d9 100644 --- a/dapps/marketplace/src/pages/transactions/Listings.js +++ b/dapps/marketplace/src/pages/transactions/Listings.js @@ -110,7 +110,7 @@ class Listings extends Component {
- {listing.title} + {listing.title || Untitled Listing}
diff --git a/devops/dockerfiles/event-listener b/devops/dockerfiles/event-listener index 24e855fa71d1..f177bc84a5f6 100644 --- a/devops/dockerfiles/event-listener +++ b/devops/dockerfiles/event-listener @@ -16,6 +16,7 @@ COPY package*.json ./ COPY lerna.json ./ COPY ./packages/contracts ./packages/contracts COPY ./packages/eventsource ./packages/eventsource +COPY ./packages/event-cache ./packages/event-cache COPY ./packages/graphql ./packages/graphql COPY ./packages/ipfs ./packages/ipfs COPY ./packages/messaging-client ./packages/messaging-client @@ -35,6 +36,7 @@ RUN npm install --unsafe-perm COPY ./packages/contracts/releases/0.8.6/build/ ./packages/contracts/build/ CMD eval $(envkey-source) && \ - npm run migrate --prefix infra/discovery && \ + npm run migrate --prefix infra/discovery && \ + npm run migrate --prefix packages/event-cache && \ npm run migrate --prefix infra/identity && \ npm run start:listener --prefix infra/discovery diff --git a/devops/dockerfiles/origin-admin b/devops/dockerfiles/origin-admin index 99af2744abbc..24b52c52708e 100644 --- a/devops/dockerfiles/origin-admin +++ b/devops/dockerfiles/origin-admin @@ -7,6 +7,7 @@ COPY package*.json ./ COPY lerna.json ./ COPY ./packages/contracts ./packages/contracts COPY ./packages/eventsource/ ./packages/eventsource/ +COPY ./packages/event-cache ./packages/event-cache COPY ./packages/graphql/ ./packages/graphql/ COPY ./packages/ipfs/ ./packages/ipfs/ COPY ./packages/mobile-bridge/ ./packages/mobile-bridge/ diff --git a/devops/dockerfiles/origin-cron b/devops/dockerfiles/origin-cron index 245d90113ee7..46a19ca64393 100644 --- a/devops/dockerfiles/origin-cron +++ b/devops/dockerfiles/origin-cron @@ -13,6 +13,7 @@ RUN mv envkey-source /usr/local/bin COPY package*.json ./ COPY lerna.json ./ COPY ./packages/eventsource ./packages/eventsource +COPY ./packages/event-cache ./packages/event-cache COPY ./packages/graphql ./packages/graphql COPY ./packages/ipfs ./packages/ipfs COPY ./packages/mobile-bridge ./packages/mobile-bridge diff --git a/devops/dockerfiles/origin-dapp b/devops/dockerfiles/origin-dapp index 99022417846e..e2da4d6309a3 100644 --- a/devops/dockerfiles/origin-dapp +++ b/devops/dockerfiles/origin-dapp @@ -21,6 +21,7 @@ COPY package*.json ./ COPY lerna.json ./ COPY ./packages/contracts ./packages/contracts COPY ./packages/eventsource/ ./packages/eventsource +COPY ./packages/event-cache/ ./packages/event-cache COPY ./packages/graphql/ ./packages/graphql COPY ./packages/ipfs/ ./packages/ipfs COPY ./packages/mobile-bridge/ ./packages/mobile-bridge diff --git a/devops/dockerfiles/origin-dapp-creator-client b/devops/dockerfiles/origin-dapp-creator-client index 0431bdb8ad3e..0dd7bd402b17 100644 --- a/devops/dockerfiles/origin-dapp-creator-client +++ b/devops/dockerfiles/origin-dapp-creator-client @@ -19,6 +19,7 @@ COPY lerna.json ./ # graphql and its dependencies COPY ./packages/graphql ./packages/graphql COPY ./packages/eventsource ./packages/eventsource +COPY ./packages/event-cache ./packages/event-cache COPY ./packages/ipfs ./packages/ipfs COPY ./packages/mobile-bridge ./packages/mobile-bridge COPY ./packages/messaging-client ./packages/messaging-client diff --git a/devops/dockerfiles/origin-discovery b/devops/dockerfiles/origin-discovery index 171c8b3d2c88..9180e39b056f 100644 --- a/devops/dockerfiles/origin-discovery +++ b/devops/dockerfiles/origin-discovery @@ -8,6 +8,7 @@ COPY package*.json ./ COPY lerna.json ./ COPY ./packages/contracts ./packages/contracts COPY ./packages/eventsource ./packages/eventsource +COPY ./packages/event-cache ./packages/event-cache COPY ./packages/graphql ./packages/graphql COPY ./packages/ipfs ./packages/ipfs COPY ./packages/messaging-client ./packages/messaging-client diff --git a/devops/dockerfiles/origin-growth b/devops/dockerfiles/origin-growth index f53e329d9906..8cbceedb3a52 100644 --- a/devops/dockerfiles/origin-growth +++ b/devops/dockerfiles/origin-growth @@ -13,6 +13,7 @@ RUN mv envkey-source /usr/local/bin COPY package*.json ./ COPY lerna.json ./ COPY ./packages/eventsource ./packages/eventsource +COPY ./packages/event-cache ./packages/event-cache COPY ./packages/graphql ./packages/graphql COPY ./packages/ipfs ./packages/ipfs COPY ./packages/mobile-bridge ./packages/mobile-bridge diff --git a/docker-compose.yml b/docker-compose.yml index 39aca1759130..b4c44c2f6d1f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -48,6 +48,7 @@ services: - ./packages/services/:/app/packages/services/ - ./packages/graphql/:/app/packages/graphql/ - ./packages/eventsource/:/app/packages/eventsource/ + - ./packages/event-cache/:/app/packages/event-cache/ - ./packages/ipfs/:/app/packages/ipfs/ - ./packages/messaging-client/:/app/packages/messaging-client/ - ./packages/mobile-bridge/:/app/packages/mobile-bridge/ @@ -68,6 +69,7 @@ services: - /app/packages/services/node_modules/ - /app/packages/graphql/node_modules/ - /app/packages/eventsource/node_modules/ + - /app/packages/event-cache/node_modules/ - /app/packages/ipfs/node_modules/ - /app/packages/messaging-client/node_modules/ - /app/packages/mobile-bridge/node_modules/ diff --git a/infra/discovery/migrations/20190410191046-event-drop-table.js b/infra/discovery/migrations/20190410191046-event-drop-table.js new file mode 100644 index 000000000000..600eafc816e1 --- /dev/null +++ b/infra/discovery/migrations/20190410191046-event-drop-table.js @@ -0,0 +1,62 @@ +'use strict' + +const TABLE_NAME = 'event' + +module.exports = { + up: queryInterface => { + return queryInterface.dropTable(TABLE_NAME) + }, + + down: (queryInterface, Sequelize) => { + // The above is kind of irreversible, but... + return queryInterface + .createTable(TABLE_NAME, { + block_number: { + type: Sequelize.INTEGER, + primaryKey: true + }, + log_index: { + type: Sequelize.INTEGER, + primaryKey: true + }, + contract_address: { + type: Sequelize.CHAR(42), + allowNull: false + }, + transaction_hash: { + type: Sequelize.CHAR(66), + allowNull: false + }, + topic0: { + type: Sequelize.CHAR(66), + allowNull: false + }, + topic1: { + type: Sequelize.CHAR(66), + allowNull: true + }, + topic2: { + type: Sequelize.CHAR(66), + allowNull: true + }, + topic3: { + type: Sequelize.CHAR(66), + allowNull: true + }, + data: { + type: Sequelize.JSONB, + allowNull: false + }, + created_at: { + type: Sequelize.DATE, + allowNull: true + } + }) + .then(() => queryInterface.addIndex(TABLE_NAME, ['contract_address'])) + .then(() => queryInterface.addIndex(TABLE_NAME, ['transaction_hash'])) + .then(() => queryInterface.addIndex(TABLE_NAME, ['topic0'])) + .then(() => queryInterface.addIndex(TABLE_NAME, ['topic1'])) + .then(() => queryInterface.addIndex(TABLE_NAME, ['topic2'])) + .then(() => queryInterface.addIndex(TABLE_NAME, ['topic3'])) + } +} diff --git a/infra/discovery/src/listener/handler.js b/infra/discovery/src/listener/handler.js index c7467f77daff..93f1fb12ee18 100644 --- a/infra/discovery/src/listener/handler.js +++ b/infra/discovery/src/listener/handler.js @@ -2,7 +2,6 @@ const esmImport = require('esm')(module) const graphqlClient = esmImport('@origin/graphql').default const logger = require('./logger') -const db = require('../models') const { withRetrys } = require('./utils') const MarketplaceEventHandler = require('./handler_marketplace') const IdentityEventHandler = require('./handler_identity') @@ -47,29 +46,12 @@ async function handleEvent(event, context) { await withRetrys(async () => { block = await context.web3.eth.getBlock(event.blockNumber) }) - const blockDate = new Date(block.timestamp * 1000) const eventDetails = `blockNumber=${event.blockNumber} \ transactionIndex=${event.transactionIndex} \ eventName=${event.event}` logger.info(`Processing event: ${eventDetails}`) - // Record the event in the DB. - await withRetrys(async () => { - return db.Event.upsert({ - blockNumber: event.blockNumber, - logIndex: event.logIndex, - contractAddress: event.address, - transactionHash: event.transactionHash, - topic0: event.raw.topics[0], - topic1: event.raw.topics[1], - topic2: event.raw.topics[2], - topic3: event.raw.topics[3], - data: event, - createdAt: blockDate - }) - }) - // Call the event handler. // // Note: we run the handler with a retry since we've seen in production cases where we fail loading diff --git a/infra/discovery/src/listener/listener.js b/infra/discovery/src/listener/listener.js index 35205cfaf63f..3d5f366dd346 100644 --- a/infra/discovery/src/listener/listener.js +++ b/infra/discovery/src/listener/listener.js @@ -145,10 +145,6 @@ async function main() { } logger.info(`Querying events from ${processedToBlock} up to ${toBlock}`) - // Update the event caches to set their max block number. - contractsContext.marketplace.eventCache.updateBlock(toBlock) - contractsContext.identityEvents.eventCache.updateBlock(toBlock) - // Retrieve all events for the relevant contracts const eventArrays = await Promise.all([ withRetrys(async () => { diff --git a/infra/discovery/src/models/event.js b/infra/discovery/src/models/event.js deleted file mode 100644 index 348438d8bb89..000000000000 --- a/infra/discovery/src/models/event.js +++ /dev/null @@ -1,39 +0,0 @@ -'use strict' - -module.exports = (sequelize, DataTypes) => { - // Note: all addresses and hashes are stored in lowercase hexadecimal notation. - const Event = sequelize.define( - 'Event', - { - // Block number at which the event was recorded. - blockNumber: { type: DataTypes.INTEGER, primaryKey: true }, - // Index of the event within the block. - logIndex: { type: DataTypes.INTEGER, primaryKey: true }, - // Address of the contract that emitted the event. - contractAddress: DataTypes.CHAR(42), - // Hash of the transaction that triggered firing the event, - transactionHash: DataTypes.CHAR(66), - // First topic is the signature of the event. - topic0: DataTypes.CHAR(66), - // Next 3 topics are the optional indexed event arguments. - topic1: DataTypes.CHAR(66), - topic2: DataTypes.CHAR(66), - topic3: DataTypes.CHAR(66), - // JSON data for the event as returned by web3 method getPastEvents. - data: DataTypes.JSONB, - // Creation date. - createdAt: DataTypes.DATE - }, - { - tableName: 'event', - // Do not automatically add the timestamp attributes (updatedAt, createdAt). - timestamps: false - } - ) - - Event.associate = function() { - // associations can be defined here - } - - return Event -} diff --git a/package-lock.json b/package-lock.json index 569550cfabc2..ecb647bde086 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,12 +13,12 @@ } }, "@babel/generator": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.4.0.tgz", - "integrity": "sha512-/v5I+a1jhGSKLgZDcmAUZ4K/VePi43eRkUs3yePW1HB1iANOD5tqJXwGSG4BZhSksP8J9ejSlwGeTiiOFZOrXQ==", + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.3.4.tgz", + "integrity": "sha512-8EXhHRFqlVVWXPezBW5keTiQi/rJMQTg/Y9uVCEZ0CAF3PKtCCaVRnp64Ii1ujhkoDhhF1fVsImoN4yJ2uz4Wg==", "dev": true, "requires": { - "@babel/types": "^7.4.0", + "@babel/types": "^7.3.4", "jsesc": "^2.5.1", "lodash": "^4.17.11", "source-map": "^0.5.0", @@ -46,12 +46,12 @@ } }, "@babel/helper-split-export-declaration": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.4.0.tgz", - "integrity": "sha512-7Cuc6JZiYShaZnybDmfwhY4UYHzI6rlqhWjaIqbsJGsIqPimEYy5uh3akSRLMg65LSdSEnJ8a8/bWQN6u2oMGw==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.0.0.tgz", + "integrity": "sha512-MXkOJqva62dfC0w85mEf/LucPPS/1+04nmmRMPEBUB++hiiThQ2zPtX/mEWQ3mtzCEjIJvPY8nuwxXtQeQwUag==", "dev": true, "requires": { - "@babel/types": "^7.4.0" + "@babel/types": "^7.0.0" } }, "@babel/highlight": { @@ -66,34 +66,34 @@ } }, "@babel/parser": { - "version": "7.4.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.4.3.tgz", - "integrity": "sha512-gxpEUhTS1sGA63EGQGuA+WESPR/6tz6ng7tSHFCmaTJK/cGK8y37cBTspX+U2xCAue2IQVvF6Z0oigmjwD8YGQ==", + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.3.4.tgz", + "integrity": "sha512-tXZCqWtlOOP4wgCp6RjRvLmfuhnqTLy9VHwRochJBCP2nDm27JnnuFEnXFASVyQNHk36jD1tAammsCEEqgscIQ==", "dev": true }, "@babel/template": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.4.0.tgz", - "integrity": "sha512-SOWwxxClTTh5NdbbYZ0BmaBVzxzTh2tO/TeLTbF6MO6EzVhHTnff8CdBXx3mEtazFBoysmEM6GU/wF+SuSx4Fw==", + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.2.2.tgz", + "integrity": "sha512-zRL0IMM02AUDwghf5LMSSDEz7sBCO2YnNmpg3uWTZj/v1rcG2BmQUvaGU8GhU8BvfMh1k2KIAYZ7Ji9KXPUg7g==", "dev": true, "requires": { "@babel/code-frame": "^7.0.0", - "@babel/parser": "^7.4.0", - "@babel/types": "^7.4.0" + "@babel/parser": "^7.2.2", + "@babel/types": "^7.2.2" } }, "@babel/traverse": { - "version": "7.4.3", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.4.3.tgz", - "integrity": "sha512-HmA01qrtaCwwJWpSKpA948cBvU5BrmviAief/b3AVw936DtcdsTexlbyzNuDnthwhOQ37xshn7hvQaEQk7ISYQ==", + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.3.4.tgz", + "integrity": "sha512-TvTHKp6471OYEcE/91uWmhR6PrrYywQntCHSaZ8CM8Vmp+pjAusal4nGB2WCCQd0rvI7nOMKn9GnbcvTUz3/ZQ==", "dev": true, "requires": { "@babel/code-frame": "^7.0.0", - "@babel/generator": "^7.4.0", + "@babel/generator": "^7.3.4", "@babel/helper-function-name": "^7.1.0", - "@babel/helper-split-export-declaration": "^7.4.0", - "@babel/parser": "^7.4.3", - "@babel/types": "^7.4.0", + "@babel/helper-split-export-declaration": "^7.0.0", + "@babel/parser": "^7.3.4", + "@babel/types": "^7.3.4", "debug": "^4.1.0", "globals": "^11.1.0", "lodash": "^4.17.11" @@ -117,9 +117,9 @@ } }, "@babel/types": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.4.0.tgz", - "integrity": "sha512-aPvkXyU2SPOnztlgo8n9cEiXW755mgyvueUPcpStqdzoSPm0fjO0vQBjLkt3JKJW7ufikfcnMTTPsN1xaTsBPA==", + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.3.4.tgz", + "integrity": "sha512-WEkp8MsLftM7O/ty580wAmZzN1nDmCACc5+jFzUt+GUFNNIi3LdRlueYz0YIlmJhlZx1QYDMZL5vdWCL0fNjFQ==", "dev": true, "requires": { "esutils": "^2.0.2", @@ -128,13 +128,13 @@ } }, "@lerna/add": { - "version": "3.13.3", - "resolved": "https://registry.npmjs.org/@lerna/add/-/add-3.13.3.tgz", - "integrity": "sha512-T3/Lsbo9ZFq+vL3ssaHxA8oKikZAPTJTGFe4CRuQgWCDd/M61+51jeWsngdaHpwzSSRDRjxg8fJTG10y10pnfA==", + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/@lerna/add/-/add-3.13.1.tgz", + "integrity": "sha512-cXk42YbuhzEnADCK8Qte5laC9Qo03eJLVnr0qKY85jQUM/T4URe3IIUemqpg0CpVATrB+Vz+iNdeqw9ng1iALw==", "requires": { - "@lerna/bootstrap": "3.13.3", - "@lerna/command": "3.13.3", - "@lerna/filter-options": "3.13.3", + "@lerna/bootstrap": "3.13.1", + "@lerna/command": "3.13.1", + "@lerna/filter-options": "3.13.0", "@lerna/npm-conf": "3.13.0", "@lerna/validation-error": "3.13.0", "dedent": "^0.7.0", @@ -155,18 +155,18 @@ } }, "@lerna/bootstrap": { - "version": "3.13.3", - "resolved": "https://registry.npmjs.org/@lerna/bootstrap/-/bootstrap-3.13.3.tgz", - "integrity": "sha512-2XzijnLHRZOVQh8pwS7+5GR3cG4uh+EiLrWOishCq2TVzkqgjaS3GGBoef7KMCXfWHoLqAZRr/jEdLqfETLVqg==", + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/@lerna/bootstrap/-/bootstrap-3.13.1.tgz", + "integrity": "sha512-mKdi5Ds5f82PZwEFyB9/W60I3iELobi1i87sTeVrbJh/um7GvqpSPy7kG/JPxyOdMpB2njX6LiJgw+7b6BEPWw==", "requires": { "@lerna/batch-packages": "3.13.0", - "@lerna/command": "3.13.3", - "@lerna/filter-options": "3.13.3", - "@lerna/has-npm-version": "3.13.3", - "@lerna/npm-install": "3.13.3", + "@lerna/command": "3.13.1", + "@lerna/filter-options": "3.13.0", + "@lerna/has-npm-version": "3.13.0", + "@lerna/npm-install": "3.13.0", "@lerna/package-graph": "3.13.0", "@lerna/pulse-till-done": "3.13.0", - "@lerna/rimraf-dir": "3.13.3", + "@lerna/rimraf-dir": "3.13.0", "@lerna/run-lifecycle": "3.13.0", "@lerna/run-parallel-batches": "3.13.0", "@lerna/symlink-binary": "3.13.0", @@ -186,30 +186,30 @@ } }, "@lerna/changed": { - "version": "3.13.3", - "resolved": "https://registry.npmjs.org/@lerna/changed/-/changed-3.13.3.tgz", - "integrity": "sha512-REMZ/1UvYrizUhN7ktlbfMUa0vhMf1ogAe97WQC4I8r3s973Orfhs3aselo1GwudUwM4tMHBH8A9vnll9or3iA==", + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/@lerna/changed/-/changed-3.13.1.tgz", + "integrity": "sha512-BRXitEJGOkoudbxEewW7WhjkLxFD+tTk4PrYpHLyCBk63pNTWtQLRE6dc1hqwh4emwyGncoyW6RgXfLgMZgryw==", "requires": { - "@lerna/collect-updates": "3.13.3", - "@lerna/command": "3.13.3", + "@lerna/collect-updates": "3.13.0", + "@lerna/command": "3.13.1", "@lerna/listable": "3.13.0", "@lerna/output": "3.13.0", - "@lerna/version": "3.13.3" + "@lerna/version": "3.13.1" } }, "@lerna/check-working-tree": { - "version": "3.13.3", - "resolved": "https://registry.npmjs.org/@lerna/check-working-tree/-/check-working-tree-3.13.3.tgz", - "integrity": "sha512-LoGZvTkne+V1WpVdCTU0XNzFKsQa2AiAFKksGRT0v8NQj6VAPp0jfVYDayTqwaWt2Ne0OGKOFE79Y5LStOuhaQ==", + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/@lerna/check-working-tree/-/check-working-tree-3.13.0.tgz", + "integrity": "sha512-dsdO15NXX5To+Q53SYeCrBEpiqv4m5VkaPZxbGQZNwoRen1MloXuqxSymJANQn+ZLEqarv5V56gydebeROPH5A==", "requires": { - "@lerna/describe-ref": "3.13.3", + "@lerna/describe-ref": "3.13.0", "@lerna/validation-error": "3.13.0" } }, "@lerna/child-process": { - "version": "3.13.3", - "resolved": "https://registry.npmjs.org/@lerna/child-process/-/child-process-3.13.3.tgz", - "integrity": "sha512-3/e2uCLnbU+bydDnDwyadpOmuzazS01EcnOleAnuj9235CU2U97DH6OyoG1EW/fU59x11J+HjIqovh5vBaMQjQ==", + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/@lerna/child-process/-/child-process-3.13.0.tgz", + "integrity": "sha512-0iDS8y2jiEucD4fJHEzKoc8aQJgm7s+hG+0RmDNtfT0MM3n17pZnf5JOMtS1FJp+SEXOjMKQndyyaDIPFsnp6A==", "requires": { "chalk": "^2.3.1", "execa": "^1.0.0", @@ -217,15 +217,15 @@ } }, "@lerna/clean": { - "version": "3.13.3", - "resolved": "https://registry.npmjs.org/@lerna/clean/-/clean-3.13.3.tgz", - "integrity": "sha512-xmNauF1PpmDaKdtA2yuRc23Tru4q7UMO6yB1a/TTwxYPYYsAWG/CBK65bV26J7x4RlZtEv06ztYGMa9zh34UXA==", + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/@lerna/clean/-/clean-3.13.1.tgz", + "integrity": "sha512-myGIaXv7RUO2qCFZXvx8SJeI+eN6y9SUD5zZ4/LvNogbOiEIlujC5lUAqK65rAHayQ9ltSa/yK6Xv510xhZXZQ==", "requires": { - "@lerna/command": "3.13.3", - "@lerna/filter-options": "3.13.3", + "@lerna/command": "3.13.1", + "@lerna/filter-options": "3.13.0", "@lerna/prompt": "3.13.0", "@lerna/pulse-till-done": "3.13.0", - "@lerna/rimraf-dir": "3.13.3", + "@lerna/rimraf-dir": "3.13.0", "p-map": "^1.2.0", "p-map-series": "^1.0.0", "p-waterfall": "^1.0.0" @@ -243,23 +243,23 @@ } }, "@lerna/collect-updates": { - "version": "3.13.3", - "resolved": "https://registry.npmjs.org/@lerna/collect-updates/-/collect-updates-3.13.3.tgz", - "integrity": "sha512-sTpALOAxli/ZS+Mjq6fbmjU9YXqFJ2E4FrE1Ijl4wPC5stXEosg2u0Z1uPY+zVKdM+mOIhLxPVdx83rUgRS+Cg==", + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/@lerna/collect-updates/-/collect-updates-3.13.0.tgz", + "integrity": "sha512-uR3u6uTzrS1p46tHQ/mlHog/nRJGBqskTHYYJbgirujxm6FqNh7Do+I1Q/7zSee407G4lzsNxZdm8IL927HemQ==", "requires": { - "@lerna/child-process": "3.13.3", - "@lerna/describe-ref": "3.13.3", + "@lerna/child-process": "3.13.0", + "@lerna/describe-ref": "3.13.0", "minimatch": "^3.0.4", "npmlog": "^4.1.2", "slash": "^1.0.0" } }, "@lerna/command": { - "version": "3.13.3", - "resolved": "https://registry.npmjs.org/@lerna/command/-/command-3.13.3.tgz", - "integrity": "sha512-WHFIQCubJV0T8gSLRNr6exZUxTswrh+iAtJCb86SE0Sa+auMPklE8af7w2Yck5GJfewmxSjke3yrjNxQrstx7w==", + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/@lerna/command/-/command-3.13.1.tgz", + "integrity": "sha512-SYWezxX+iheWvzRoHCrbs8v5zHPaxAx3kWvZhqi70vuGsdOVAWmaG4IvHLn11ztS+Vpd5PM+ztBWSbnykpLFKQ==", "requires": { - "@lerna/child-process": "3.13.3", + "@lerna/child-process": "3.13.0", "@lerna/package-graph": "3.13.0", "@lerna/project": "3.13.1", "@lerna/validation-error": "3.13.0", @@ -289,12 +289,12 @@ } }, "@lerna/create": { - "version": "3.13.3", - "resolved": "https://registry.npmjs.org/@lerna/create/-/create-3.13.3.tgz", - "integrity": "sha512-4M5xT1AyUMwt1gCDph4BfW3e6fZmt0KjTa3FoXkUotf/w/eqTsc2IQ+ULz2+gOFQmtuNbqIZEOK3J4P9ArJJ/A==", + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/@lerna/create/-/create-3.13.1.tgz", + "integrity": "sha512-pLENMXgTkQuvKxAopjKeoLOv9fVUCnpTUD7aLrY5d95/1xqSZlnsOcQfUYcpMf3GpOvHc8ILmI5OXkPqjAf54g==", "requires": { - "@lerna/child-process": "3.13.3", - "@lerna/command": "3.13.3", + "@lerna/child-process": "3.13.0", + "@lerna/command": "3.13.1", "@lerna/npm-conf": "3.13.0", "@lerna/validation-error": "3.13.0", "camelcase": "^5.0.0", @@ -314,9 +314,9 @@ }, "dependencies": { "camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.2.0.tgz", + "integrity": "sha512-IXFsBS2pC+X0j0N/GE7Dm7j3bsEBp+oTpb7F50dwEVX7rf3IgwO9XatnegTsDtniKCUtEJH4fSU6Asw7uoVLfQ==" } } }, @@ -331,44 +331,44 @@ } }, "@lerna/describe-ref": { - "version": "3.13.3", - "resolved": "https://registry.npmjs.org/@lerna/describe-ref/-/describe-ref-3.13.3.tgz", - "integrity": "sha512-5KcLTvjdS4gU5evW8ESbZ0BF44NM5HrP3dQNtWnOUSKJRgsES8Gj0lq9AlB2+YglZfjEftFT03uOYOxnKto4Uw==", + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/@lerna/describe-ref/-/describe-ref-3.13.0.tgz", + "integrity": "sha512-UJefF5mLxLae9I2Sbz5RLYGbqbikRuMqdgTam0MS5OhXnyuuKYBUpwBshCURNb1dPBXTQhSwc7+oUhORx8ojCg==", "requires": { - "@lerna/child-process": "3.13.3", + "@lerna/child-process": "3.13.0", "npmlog": "^4.1.2" } }, "@lerna/diff": { - "version": "3.13.3", - "resolved": "https://registry.npmjs.org/@lerna/diff/-/diff-3.13.3.tgz", - "integrity": "sha512-/DRS2keYbnKaAC+5AkDyZRGkP/kT7v1GlUS0JGZeiRDPQ1H6PzhX09EgE5X6nj0Ytrm0sUasDeN++CDVvgaI+A==", + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/@lerna/diff/-/diff-3.13.1.tgz", + "integrity": "sha512-cKqmpONO57mdvxtp8e+l5+tjtmF04+7E+O0QEcLcNUAjC6UR2OSM77nwRCXDukou/1h72JtWs0jjcdYLwAmApg==", "requires": { - "@lerna/child-process": "3.13.3", - "@lerna/command": "3.13.3", + "@lerna/child-process": "3.13.0", + "@lerna/command": "3.13.1", "@lerna/validation-error": "3.13.0", "npmlog": "^4.1.2" } }, "@lerna/exec": { - "version": "3.13.3", - "resolved": "https://registry.npmjs.org/@lerna/exec/-/exec-3.13.3.tgz", - "integrity": "sha512-c0bD4XqM96CTPV8+lvkxzE7mkxiFyv/WNM4H01YvvbFAJzk+S4Y7cBtRkIYFTfkFZW3FLo8pEgtG1ONtIdM+tg==", + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/@lerna/exec/-/exec-3.13.1.tgz", + "integrity": "sha512-I34wEP9lrAqqM7tTXLDxv/6454WFzrnXDWpNDbiKQiZs6SIrOOjmm6I4FiQsx+rU3o9d+HkC6tcUJRN5mlJUgA==", "requires": { "@lerna/batch-packages": "3.13.0", - "@lerna/child-process": "3.13.3", - "@lerna/command": "3.13.3", - "@lerna/filter-options": "3.13.3", + "@lerna/child-process": "3.13.0", + "@lerna/command": "3.13.1", + "@lerna/filter-options": "3.13.0", "@lerna/run-parallel-batches": "3.13.0", "@lerna/validation-error": "3.13.0" } }, "@lerna/filter-options": { - "version": "3.13.3", - "resolved": "https://registry.npmjs.org/@lerna/filter-options/-/filter-options-3.13.3.tgz", - "integrity": "sha512-DbtQX4eRgrBz1wCFWRP99JBD7ODykYme9ykEK79+RrKph40znhJQRlLg4idogj6IsUEzwo1OHjihCzSfnVo6Cg==", + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/@lerna/filter-options/-/filter-options-3.13.0.tgz", + "integrity": "sha512-SRp7DCo9zrf+7NkQxZMkeyO1GRN6GICoB9UcBAbXhLbWisT37Cx5/6+jh49gYB63d/0/WYHSEPMlheUrpv1Srw==", "requires": { - "@lerna/collect-updates": "3.13.3", + "@lerna/collect-updates": "3.13.0", "@lerna/filter-packages": "3.13.0", "dedent": "^0.7.0" } @@ -418,11 +418,11 @@ } }, "@lerna/github-client": { - "version": "3.13.3", - "resolved": "https://registry.npmjs.org/@lerna/github-client/-/github-client-3.13.3.tgz", - "integrity": "sha512-fcJkjab4kX0zcLLSa/DCUNvU3v8wmy2c1lhdIbL7s7gABmDcV0QZq93LhnEee3VkC9UpnJ6GKG4EkD7eIifBnA==", + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/@lerna/github-client/-/github-client-3.13.1.tgz", + "integrity": "sha512-iPLUp8FFoAKGURksYEYZzfuo9TRA+NepVlseRXFaWlmy36dCQN20AciINpoXiXGoHcEUHXUKHQvY3ARFdMlf3w==", "requires": { - "@lerna/child-process": "3.13.3", + "@lerna/child-process": "3.13.0", "@octokit/plugin-enterprise-rest": "^2.1.1", "@octokit/rest": "^16.16.0", "git-url-parse": "^11.1.2", @@ -435,21 +435,21 @@ "integrity": "sha512-SlZvh1gVRRzYLVluz9fryY1nJpZ0FHDGB66U9tFfvnnxmueckRQxLopn3tXj3NU1kc3QANT2I5BsQkOqZ4TEFQ==" }, "@lerna/has-npm-version": { - "version": "3.13.3", - "resolved": "https://registry.npmjs.org/@lerna/has-npm-version/-/has-npm-version-3.13.3.tgz", - "integrity": "sha512-mQzoghRw4dBg0R9FFfHrj0TH0glvXyzdEZmYZ8Isvx5BSuEEwpsryoywuZSdppcvLu8o7NAdU5Tac8cJ/mT52w==", + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/@lerna/has-npm-version/-/has-npm-version-3.13.0.tgz", + "integrity": "sha512-Oqu7DGLnrMENPm+bPFGOHnqxK8lCnuYr6bk3g/CoNn8/U0qgFvHcq6Iv8/Z04TsvleX+3/RgauSD2kMfRmbypg==", "requires": { - "@lerna/child-process": "3.13.3", + "@lerna/child-process": "3.13.0", "semver": "^5.5.0" } }, "@lerna/import": { - "version": "3.13.3", - "resolved": "https://registry.npmjs.org/@lerna/import/-/import-3.13.3.tgz", - "integrity": "sha512-gDjLAFVavG/CMvj9leBfiwd7vrXqtdFXPIz1oXmghBMnje7nCTbodbNWFe4VDDWx7reDaZIN+6PxTSvrPcF//A==", + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/@lerna/import/-/import-3.13.1.tgz", + "integrity": "sha512-A1Vk1siYx1XkRl6w+zkaA0iptV5TIynVlHPR9S7NY0XAfhykjztYVvwtxarlh6+VcNrO9We6if0+FXCrfDEoIg==", "requires": { - "@lerna/child-process": "3.13.3", - "@lerna/command": "3.13.3", + "@lerna/child-process": "3.13.0", + "@lerna/command": "3.13.1", "@lerna/prompt": "3.13.0", "@lerna/pulse-till-done": "3.13.0", "@lerna/validation-error": "3.13.0", @@ -459,23 +459,23 @@ } }, "@lerna/init": { - "version": "3.13.3", - "resolved": "https://registry.npmjs.org/@lerna/init/-/init-3.13.3.tgz", - "integrity": "sha512-bK/mp0sF6jT0N+c+xrbMCqN4xRoiZCXQzlYsyACxPK99KH/mpHv7hViZlTYUGlYcymtew6ZC770miv5A9wF9hA==", + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/@lerna/init/-/init-3.13.1.tgz", + "integrity": "sha512-M59WACqim8WkH5FQEGOCEZ89NDxCKBfFTx4ZD5ig3LkGyJ8RdcJq5KEfpW/aESuRE9JrZLzVr0IjKbZSxzwEMA==", "requires": { - "@lerna/child-process": "3.13.3", - "@lerna/command": "3.13.3", + "@lerna/child-process": "3.13.0", + "@lerna/command": "3.13.1", "fs-extra": "^7.0.0", "p-map": "^1.2.0", "write-json-file": "^2.3.0" } }, "@lerna/link": { - "version": "3.13.3", - "resolved": "https://registry.npmjs.org/@lerna/link/-/link-3.13.3.tgz", - "integrity": "sha512-IHhtdhA0KlIdevCsq6WHkI2rF3lHWHziJs2mlrEWAKniVrFczbELON1KJAgdJS1k3kAP/WeWVqmIYZ2hJDxMvg==", + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/@lerna/link/-/link-3.13.1.tgz", + "integrity": "sha512-N3h3Fj1dcea+1RaAoAdy4g2m3fvU7m89HoUn5X/Zcw5n2kPoK8kTO+NfhNAatfRV8VtMXst8vbNrWQQtfm0FFw==", "requires": { - "@lerna/command": "3.13.3", + "@lerna/command": "3.13.1", "@lerna/package-graph": "3.13.0", "@lerna/symlink-dependencies": "3.13.0", "p-map": "^1.2.0", @@ -483,12 +483,12 @@ } }, "@lerna/list": { - "version": "3.13.3", - "resolved": "https://registry.npmjs.org/@lerna/list/-/list-3.13.3.tgz", - "integrity": "sha512-rLRDsBCkydMq2FL6WY1J/elvnXIjxxRtb72lfKHdvDEqVdquT5Qgt9ci42hwjmcocFwWcFJgF6BZozj5pbc13A==", + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/@lerna/list/-/list-3.13.1.tgz", + "integrity": "sha512-635iRbdgd9gNvYLLIbYdQCQLr+HioM5FGJLFS0g3DPGygr6iDR8KS47hzCRGH91LU9NcM1mD1RoT/AChF+QbiA==", "requires": { - "@lerna/command": "3.13.3", - "@lerna/filter-options": "3.13.3", + "@lerna/command": "3.13.1", + "@lerna/filter-options": "3.13.0", "@lerna/listable": "3.13.0", "@lerna/output": "3.13.0" } @@ -535,11 +535,11 @@ } }, "@lerna/npm-install": { - "version": "3.13.3", - "resolved": "https://registry.npmjs.org/@lerna/npm-install/-/npm-install-3.13.3.tgz", - "integrity": "sha512-7Jig9MLpwAfcsdQ5UeanAjndChUjiTjTp50zJ+UZz4CbIBIDhoBehvNMTCL2G6pOEC7sGEg6sAqJINAqred6Tg==", + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/@lerna/npm-install/-/npm-install-3.13.0.tgz", + "integrity": "sha512-qNyfts//isYQxore6fsPorNYJmPVKZ6tOThSH97tP0aV91zGMtrYRqlAoUnDwDdAjHPYEM16hNujg2wRmsqqIw==", "requires": { - "@lerna/child-process": "3.13.3", + "@lerna/child-process": "3.13.0", "@lerna/get-npm-exec-opts": "3.13.0", "fs-extra": "^7.0.0", "npm-package-arg": "^6.1.0", @@ -549,26 +549,25 @@ } }, "@lerna/npm-publish": { - "version": "3.13.2", - "resolved": "https://registry.npmjs.org/@lerna/npm-publish/-/npm-publish-3.13.2.tgz", - "integrity": "sha512-HMucPyEYZfom5tRJL4GsKBRi47yvSS2ynMXYxL3kO0ie+j9J7cb0Ir8NmaAMEd3uJWJVFCPuQarehyfTDZsSxg==", + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/@lerna/npm-publish/-/npm-publish-3.13.0.tgz", + "integrity": "sha512-y4WO0XTaf9gNRkI7as6P2ItVDOxmYHwYto357fjybcnfXgMqEA94c3GJ++jU41j0A9vnmYC6/XxpTd9sVmH9tA==", "requires": { "@lerna/run-lifecycle": "3.13.0", "figgy-pudding": "^3.5.1", "fs-extra": "^7.0.0", "libnpmpublish": "^1.1.1", - "npm-package-arg": "^6.1.0", "npmlog": "^4.1.2", "pify": "^3.0.0", "read-package-json": "^2.0.13" } }, "@lerna/npm-run-script": { - "version": "3.13.3", - "resolved": "https://registry.npmjs.org/@lerna/npm-run-script/-/npm-run-script-3.13.3.tgz", - "integrity": "sha512-qR4o9BFt5hI8Od5/DqLalOJydnKpiQFEeN0h9xZi7MwzuX1Ukwh3X22vqsX4YRbipIelSFtrDzleNVUm5jj0ow==", + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/@lerna/npm-run-script/-/npm-run-script-3.13.0.tgz", + "integrity": "sha512-hiL3/VeVp+NFatBjkGN8mUdX24EfZx9rQlSie0CMgtjc7iZrtd0jCguLomSCRHYjJuvqgbp+LLYo7nHVykfkaQ==", "requires": { - "@lerna/child-process": "3.13.3", + "@lerna/child-process": "3.13.0", "@lerna/get-npm-exec-opts": "3.13.0", "npmlog": "^4.1.2" } @@ -661,20 +660,20 @@ } }, "@lerna/publish": { - "version": "3.13.3", - "resolved": "https://registry.npmjs.org/@lerna/publish/-/publish-3.13.3.tgz", - "integrity": "sha512-Ni3pZKueIfgJJoL0OXfbAuWhGlJrDNwGx3CYWp2dbNqJmKD6uBZmsDtmeARKDp92oUK60W0drXCMydkIFFHMDQ==", + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/@lerna/publish/-/publish-3.13.1.tgz", + "integrity": "sha512-KhCJ9UDx76HWCF03i5TD7z5lX+2yklHh5SyO8eDaLptgdLDQ0Z78lfGj3JhewHU2l46FztmqxL/ss0IkWHDL+g==", "requires": { "@lerna/batch-packages": "3.13.0", - "@lerna/check-working-tree": "3.13.3", - "@lerna/child-process": "3.13.3", - "@lerna/collect-updates": "3.13.3", - "@lerna/command": "3.13.3", - "@lerna/describe-ref": "3.13.3", + "@lerna/check-working-tree": "3.13.0", + "@lerna/child-process": "3.13.0", + "@lerna/collect-updates": "3.13.0", + "@lerna/command": "3.13.1", + "@lerna/describe-ref": "3.13.0", "@lerna/log-packed": "3.13.0", "@lerna/npm-conf": "3.13.0", "@lerna/npm-dist-tag": "3.13.0", - "@lerna/npm-publish": "3.13.2", + "@lerna/npm-publish": "3.13.0", "@lerna/output": "3.13.0", "@lerna/pack-directory": "3.13.1", "@lerna/prompt": "3.13.0", @@ -682,7 +681,7 @@ "@lerna/run-lifecycle": "3.13.0", "@lerna/run-parallel-batches": "3.13.0", "@lerna/validation-error": "3.13.0", - "@lerna/version": "3.13.3", + "@lerna/version": "3.13.1", "figgy-pudding": "^3.5.1", "fs-extra": "^7.0.0", "libnpmaccess": "^3.0.1", @@ -716,25 +715,25 @@ } }, "@lerna/rimraf-dir": { - "version": "3.13.3", - "resolved": "https://registry.npmjs.org/@lerna/rimraf-dir/-/rimraf-dir-3.13.3.tgz", - "integrity": "sha512-d0T1Hxwu3gpYVv73ytSL+/Oy8JitsmvOYUR5ouRSABsmqS7ZZCh5t6FgVDDGVXeuhbw82+vuny1Og6Q0k4ilqw==", + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/@lerna/rimraf-dir/-/rimraf-dir-3.13.0.tgz", + "integrity": "sha512-kte+pMemulre8cmPqljxIYjCmdLByz8DgHBHXB49kz2EiPf8JJ+hJFt0PzEubEyJZ2YE2EVAx5Tv5+NfGNUQyQ==", "requires": { - "@lerna/child-process": "3.13.3", + "@lerna/child-process": "3.13.0", "npmlog": "^4.1.2", "path-exists": "^3.0.0", "rimraf": "^2.6.2" } }, "@lerna/run": { - "version": "3.13.3", - "resolved": "https://registry.npmjs.org/@lerna/run/-/run-3.13.3.tgz", - "integrity": "sha512-ygnLIfIYS6YY1JHWOM4CsdZiY8kTYPsDFOLAwASlRnlAXF9HiMT08GFXLmMHIblZJ8yJhsM2+QgraCB0WdxzOQ==", + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/@lerna/run/-/run-3.13.1.tgz", + "integrity": "sha512-nv1oj7bsqppWm1M4ifN+/IIbVu9F4RixrbQD2okqDGYne4RQPAXyb5cEZuAzY/wyGTWWiVaZ1zpj5ogPWvH0bw==", "requires": { "@lerna/batch-packages": "3.13.0", - "@lerna/command": "3.13.3", - "@lerna/filter-options": "3.13.3", - "@lerna/npm-run-script": "3.13.3", + "@lerna/command": "3.13.1", + "@lerna/filter-options": "3.13.0", + "@lerna/npm-run-script": "3.13.0", "@lerna/output": "3.13.0", "@lerna/run-parallel-batches": "3.13.0", "@lerna/timer": "3.13.0", @@ -801,17 +800,17 @@ } }, "@lerna/version": { - "version": "3.13.3", - "resolved": "https://registry.npmjs.org/@lerna/version/-/version-3.13.3.tgz", - "integrity": "sha512-o/yQGAwDHmyu17wTj4Kat1/uDhjYFMeG+H0Y0HC4zJ4a/T6rEiXx7jJrnucPTmTQTDcUBoH/It5LrPYGOPsExA==", + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/@lerna/version/-/version-3.13.1.tgz", + "integrity": "sha512-WpfKc5jZBBOJ6bFS4atPJEbHSiywQ/Gcd+vrwaEGyQHWHQZnPTvhqLuq3q9fIb9sbuhH5pSY6eehhuBrKqTnjg==", "requires": { "@lerna/batch-packages": "3.13.0", - "@lerna/check-working-tree": "3.13.3", - "@lerna/child-process": "3.13.3", - "@lerna/collect-updates": "3.13.3", - "@lerna/command": "3.13.3", + "@lerna/check-working-tree": "3.13.0", + "@lerna/child-process": "3.13.0", + "@lerna/collect-updates": "3.13.0", + "@lerna/command": "3.13.1", "@lerna/conventional-commits": "3.13.0", - "@lerna/github-client": "3.13.3", + "@lerna/github-client": "3.13.1", "@lerna/output": "3.13.0", "@lerna/prompt": "3.13.0", "@lerna/run-lifecycle": "3.13.0", @@ -853,9 +852,9 @@ "integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==" }, "@octokit/endpoint": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-4.0.0.tgz", - "integrity": "sha512-b8sptNUekjREtCTJFpOfSIL4SKh65WaakcyxWzRcSPOk5RxkZJ/S8884NGZFxZ+jCB2rDURU66pSHn14cVgWVg==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-3.2.3.tgz", + "integrity": "sha512-yUPCt4vMIOclox13CUxzuKiPJIFo46b/6GhUnUTw5QySczN1L0DtSxgmIZrZV4SAb9EyAqrceoyrWoYVnfF2AA==", "requires": { "deepmerge": "3.2.0", "is-plain-object": "^2.0.4", @@ -869,11 +868,11 @@ "integrity": "sha512-CTZr64jZYhGWNTDGlSJ2mvIlFsm9OEO3LqWn9I/gmoHI4jRBp4kpHoFYNemG4oA75zUAcmbuWblb7jjP877YZw==" }, "@octokit/request": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@octokit/request/-/request-3.0.0.tgz", - "integrity": "sha512-DZqmbm66tq+a9FtcKrn0sjrUpi0UaZ9QPUCxxyk/4CJ2rseTMpAWRf6gCwOSUCzZcx/4XVIsDk+kz5BVdaeenA==", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-2.4.2.tgz", + "integrity": "sha512-lxVlYYvwGbKSHXfbPk5vxEA8w4zHOH1wobado4a9EfsyD3Cbhuhus1w0Ye9Ro0eMubGO8kNy5d+xNFisM3Tvaw==", "requires": { - "@octokit/endpoint": "^4.0.0", + "@octokit/endpoint": "^3.2.0", "deprecation": "^1.0.1", "is-plain-object": "^2.0.4", "node-fetch": "^2.3.0", @@ -882,12 +881,11 @@ } }, "@octokit/rest": { - "version": "16.24.3", - "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-16.24.3.tgz", - "integrity": "sha512-fBr2ziN4WT9G9sYTfnNVI/0wCb68ZI5isNU48lfWXQDyAy4ftlrh0SkIbhL7aigXUjcY0cX5J46ypyRPH0/U0g==", + "version": "16.19.0", + "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-16.19.0.tgz", + "integrity": "sha512-mUk/GU2LtV95OAM3FnvK7KFFNzUUzEGFldOhWliJnuhwBqxEag1gW85o//L6YphC9wLoTaZQOhCHmQcsCnt2ag==", "requires": { - "@octokit/request": "3.0.0", - "atob-lite": "^2.0.0", + "@octokit/request": "2.4.2", "before-after-hook": "^1.4.0", "btoa-lite": "^1.0.0", "deprecation": "^1.0.1", @@ -1117,11 +1115,6 @@ "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==" }, - "atob-lite": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/atob-lite/-/atob-lite-2.0.0.tgz", - "integrity": "sha1-D+9a1G8b16hQLGVyfwNn1e5D1pY=" - }, "aws-sign2": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", @@ -1232,9 +1225,9 @@ } }, "bluebird": { - "version": "3.5.4", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.4.tgz", - "integrity": "sha512-FG+nFEZChJrbQ9tIccIfZJBz3J7mLrAhxakAbnrJWn8d7aKOC+LWifa0G+p4ZqKp4y13T7juYvdhq9NzKdsrjw==" + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.3.tgz", + "integrity": "sha512-/qKPUQlaW1OyR51WeCPBvRnAlnZFUJkCSG5HzGnuIqhgyJtF+T94lFnn33eiazjRm2LAHVy2guNnaq48X9SJuw==" }, "brace-expansion": { "version": "1.1.11", @@ -1563,9 +1556,9 @@ } }, "commander": { - "version": "2.20.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz", - "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==" + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.19.0.tgz", + "integrity": "sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg==" }, "compare-func": { "version": "1.3.2", @@ -1587,9 +1580,9 @@ } }, "component-emitter": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", - "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", + "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=" }, "concat-map": { "version": "0.0.1", @@ -1663,9 +1656,9 @@ } }, "conventional-changelog-preset-loader": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/conventional-changelog-preset-loader/-/conventional-changelog-preset-loader-2.1.1.tgz", - "integrity": "sha512-K4avzGMLm5Xw0Ek/6eE3vdOXkqnpf9ydb68XYmCc16cJ99XMMbc2oaNMuPwAsxVK6CC1yA4/I90EhmWNj0Q6HA==" + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/conventional-changelog-preset-loader/-/conventional-changelog-preset-loader-2.0.2.tgz", + "integrity": "sha512-pBY+qnUoJPXAXXqVGwQaVmcye05xi6z231QM98wHWamGAmu/ghkBprQAwmF5bdmyobdVxiLhPY3PrCfSeUNzRQ==" }, "conventional-changelog-writer": { "version": "4.0.3", @@ -1708,72 +1701,18 @@ } }, "conventional-recommended-bump": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/conventional-recommended-bump/-/conventional-recommended-bump-4.1.1.tgz", - "integrity": "sha512-JT2vKfSP9kR18RXXf55BRY1O3AHG8FPg5btP3l7LYfcWJsiXI6MCf30DepQ98E8Qhowvgv7a8iev0J1bEDkTFA==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/conventional-recommended-bump/-/conventional-recommended-bump-4.0.4.tgz", + "integrity": "sha512-9mY5Yoblq+ZMqJpBzgS+RpSq+SUfP2miOR3H/NR9drGf08WCrY9B6HAGJZEm6+ThsVP917VHAahSOjM6k1vhPg==", "requires": { - "concat-stream": "^2.0.0", - "conventional-changelog-preset-loader": "^2.1.1", - "conventional-commits-filter": "^2.0.2", - "conventional-commits-parser": "^3.0.2", + "concat-stream": "^1.6.0", + "conventional-changelog-preset-loader": "^2.0.2", + "conventional-commits-filter": "^2.0.1", + "conventional-commits-parser": "^3.0.1", "git-raw-commits": "2.0.0", "git-semver-tags": "^2.0.2", "meow": "^4.0.0", "q": "^1.5.1" - }, - "dependencies": { - "concat-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", - "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", - "requires": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.0.2", - "typedarray": "^0.0.6" - } - }, - "conventional-commits-filter": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/conventional-commits-filter/-/conventional-commits-filter-2.0.2.tgz", - "integrity": "sha512-WpGKsMeXfs21m1zIw4s9H5sys2+9JccTzpN6toXtxhpw2VNF2JUXwIakthKBy+LN4DvJm+TzWhxOMWOs1OFCFQ==", - "requires": { - "lodash.ismatch": "^4.4.0", - "modify-values": "^1.0.0" - } - }, - "conventional-commits-parser": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-3.0.2.tgz", - "integrity": "sha512-y5eqgaKR0F6xsBNVSQ/5cI5qIF3MojddSUi1vKIggRkqUTbkqFKH9P5YX/AT1BVZp9DtSzBTIkvjyVLotLsVog==", - "requires": { - "JSONStream": "^1.0.4", - "is-text-path": "^1.0.0", - "lodash": "^4.2.1", - "meow": "^4.0.0", - "split2": "^2.0.0", - "through2": "^3.0.0", - "trim-off-newlines": "^1.0.0" - } - }, - "readable-stream": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.3.0.tgz", - "integrity": "sha512-EsI+s3k3XsW+fU8fQACLN59ky34AZ14LoeVZpYwmZvldCFo0r0gnelwF2TcMjLor/BTL5aDJVBMkss0dthToPw==", - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - }, - "through2": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.1.tgz", - "integrity": "sha512-M96dvTalPT3YbYLaKaCuwu+j06D/8Jfib0o/PxbVt6Amhv3dUAtW6rTV1jPgJSBG83I/e04Y6xkVdVhSRhi0ww==", - "requires": { - "readable-stream": "2 || 3" - } - } } }, "copy-concurrently": { @@ -1800,13 +1739,14 @@ "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, "cosmiconfig": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.0.tgz", - "integrity": "sha512-nxt+Nfc3JAqf4WIWd0jXLjTJZmsPLrA9DDc4nRw2KFJQJK7DNooqSXrNI7tzLG50CF8axczly5UV929tBmh/7g==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.1.0.tgz", + "integrity": "sha512-kCNPvthka8gvLtzAxQXvWo4FxqRB+ftRZyPZNuab5ngvM9Y7yw7hbEysglptLgpkGX9nAOKTBVkHUAe8xtYR6Q==", "requires": { "import-fresh": "^2.0.0", "is-directory": "^0.3.1", - "js-yaml": "^3.13.0", + "js-yaml": "^3.9.0", + "lodash.get": "^4.4.2", "parse-json": "^4.0.0" } }, @@ -1862,14 +1802,6 @@ "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-3.0.3.tgz", "integrity": "sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==" }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, "debuglog": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/debuglog/-/debuglog-1.0.1.tgz", @@ -2145,9 +2077,9 @@ "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, "eslint": { - "version": "5.16.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-5.16.0.tgz", - "integrity": "sha512-S3Rz11i7c8AA5JPv7xAH+dOyq/Cu/VXHiHXBPOU1k/JAM5dXqQPt3qcrhpHSorXmrpu2g0gkIBVXAqCpzfoZIg==", + "version": "5.15.3", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-5.15.3.tgz", + "integrity": "sha512-vMGi0PjCHSokZxE0NLp2VneGw5sio7SSiDNgIUn2tC0XkWJRNOIoHIg3CliLVfXnJsiHxGAYrkw0PieAu8+KYQ==", "dev": true, "requires": { "@babel/code-frame": "^7.0.0", @@ -2170,7 +2102,7 @@ "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", "inquirer": "^6.2.2", - "js-yaml": "^3.13.0", + "js-yaml": "^3.12.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.3.0", "lodash": "^4.17.11", @@ -2269,16 +2201,38 @@ "requires": { "debug": "^2.6.9", "resolve": "^1.5.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + } } }, "eslint-module-utils": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.4.0.tgz", - "integrity": "sha512-14tltLm38Eu3zS+mt0KvILC3q8jyIAH518MlG+HO0p+yK885Lb1UHTY/UgR91eOyGdmxAPb+OLoW4znqIT6Ndw==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.3.0.tgz", + "integrity": "sha512-lmDJgeOOjk8hObTysjqH7wyMi+nsHwwvfBykwfhjR1LNdd7C2uFJBvx4OpWYpXOw4df1yE1cDEVd1yLHitk34w==", "dev": true, "requires": { "debug": "^2.6.8", "pkg-dir": "^2.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + } } }, "eslint-plugin-es": { @@ -2292,33 +2246,41 @@ } }, "eslint-plugin-flowtype": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-flowtype/-/eslint-plugin-flowtype-3.6.1.tgz", - "integrity": "sha512-VVuPKb5kgWFhxCkAMpL5wi44AK+4nkxa3XXZVa2PKf00n4INNbdKmZC0tT8qeNTHoDPYMXbqak4tGC9YtIOqGw==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-flowtype/-/eslint-plugin-flowtype-3.4.2.tgz", + "integrity": "sha512-sv6O6fiN3dIwhU4qRxfcyIpbKGVvsxwIQ6vgBLudpQKjH1rEyEFEOjGzGEUBTQP9J8LdTZm37OjiqZ0ZeFOa6g==", "dev": true, "requires": { "lodash": "^4.17.11" } }, "eslint-plugin-import": { - "version": "2.17.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.17.2.tgz", - "integrity": "sha512-m+cSVxM7oLsIpmwNn2WXTJoReOF9f/CtLMo7qOVmKd1KntBy0hEcuNZ3erTmWjx+DxRO0Zcrm5KwAvI9wHcV5g==", + "version": "2.16.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.16.0.tgz", + "integrity": "sha512-z6oqWlf1x5GkHIFgrSvtmudnqM6Q60KM4KvpWi5ubonMjycLjndvd5+8VAZIsTlHC03djdgJuyKG6XO577px6A==", "dev": true, "requires": { - "array-includes": "^3.0.3", "contains-path": "^0.1.0", "debug": "^2.6.9", "doctrine": "1.5.0", "eslint-import-resolver-node": "^0.3.2", - "eslint-module-utils": "^2.4.0", + "eslint-module-utils": "^2.3.0", "has": "^1.0.3", "lodash": "^4.17.11", "minimatch": "^3.0.4", "read-pkg-up": "^2.0.0", - "resolve": "^1.10.0" + "resolve": "^1.9.0" }, "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, "doctrine": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-1.5.0.tgz", @@ -2419,17 +2381,17 @@ }, "dependencies": { "ignore": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.1.tgz", - "integrity": "sha512-DWjnQIFLenVrwyRCKZT+7a7/U4Cqgar4WG8V++K3hw+lrW1hc/SIwdiGmtxKCVACmHULTuGeBbHJmbwW7/sAvA==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.0.6.tgz", + "integrity": "sha512-/+hp3kUf/Csa32ktIaj0OlRqQxrgs30n62M90UBpNd9k+ENEch5S+hmbW3DtcJGz3sYFTh4F3A6fQ0q7KWsp4w==", "dev": true } } }, "eslint-plugin-promise": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-4.1.1.tgz", - "integrity": "sha512-faAHw7uzlNPy7b45J1guyjazw28M+7gJokKUjC5JSFoYfUEyy6Gw/i7YQvmv2Yk00sUjWcmzXQLpU1Ki/C2IZQ==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-4.0.1.tgz", + "integrity": "sha512-Si16O0+Hqz1gDHsys6RtFRrW7cCTB6P7p3OJmKp3Y3dxpQE2qwOA7d3xnV+0mBmrPoi0RBnxlCKvqu70te6wjg==", "dev": true }, "eslint-plugin-react": { @@ -2560,6 +2522,14 @@ "to-regex": "^3.0.1" }, "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, "define-property": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", @@ -2695,9 +2665,9 @@ }, "dependencies": { "is-glob": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", - "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.0.tgz", + "integrity": "sha1-lSHHaEXMJhCoUgPd8ICpWML/q8A=", "requires": { "is-extglob": "^2.1.1" } @@ -3228,9 +3198,9 @@ "dev": true }, "handlebars": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.1.2.tgz", - "integrity": "sha512-nvfrjqvt9xQ8Z/w0ijewdD/vvWDTOweBUm96NTr66Wfvo1mJenBLwcYmPs3TIBP5ruzYGD7Hx/DaM9RmhroGPw==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.1.1.tgz", + "integrity": "sha512-3Zhi6C0euYZL5sM0Zcy7lInLXKQ+YLcF/olbN010mzGQ4XVm50JeyBnMqofHh696GrciGruC7kCcApPDJvVgwA==", "requires": { "neo-async": "^2.6.0", "optimist": "^0.6.1", @@ -3467,9 +3437,9 @@ } }, "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.1.0.tgz", + "integrity": "sha512-H2RyIJ7+A3rjkwKC2l5GGtU4H1vkxKCAGsWasNVd0Set+6i4znxbWy6/j16YDPJDWxhsgZiKAstMEP8wCdSpjA==", "dev": true }, "pkg-dir": { @@ -3596,9 +3566,9 @@ } }, "inquirer": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.3.1.tgz", - "integrity": "sha512-MmL624rfkFt4TG9y/Jvmt8vdmOo836U7Y0Hxr2aFk3RelZEGX4Igk0KabWrcaaZaTv9uzglOqWh1Vly+FAWAXA==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.2.2.tgz", + "integrity": "sha512-Z2rREiXA6cHRR9KBOarR3WuLlFzlIfAEIiB45ll5SSadMg7WqOh1MKEjjndfuH5ewXdixWCxqnVfGOQzPeiztA==", "requires": { "ansi-escapes": "^3.2.0", "chalk": "^2.4.2", @@ -3611,7 +3581,7 @@ "run-async": "^2.2.0", "rxjs": "^6.4.0", "string-width": "^2.1.0", - "strip-ansi": "^5.1.0", + "strip-ansi": "^5.0.0", "through": "^2.3.6" }, "dependencies": { @@ -3920,9 +3890,9 @@ "dev": true }, "js-yaml": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", - "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "version": "3.12.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.12.2.tgz", + "integrity": "sha512-QHn/Lh/7HhZ/Twc7vJYQTkjuCa0kaCcDcjK5Zlk2rvnUpy7DxMJ23+Jc2dcyvltwQVg1nygAVlB2oRDFHoRS5Q==", "requires": { "argparse": "^1.0.7", "esprima": "^4.0.0" @@ -4012,25 +3982,25 @@ } }, "lerna": { - "version": "3.13.3", - "resolved": "https://registry.npmjs.org/lerna/-/lerna-3.13.3.tgz", - "integrity": "sha512-0TkG40F02A4wjKraJBztPtj87BjUezFmaZKAha8eLdtngZkSpAdrSANa5K7jnnA8mywmpQwrKJuBmjdNpm9cBw==", - "requires": { - "@lerna/add": "3.13.3", - "@lerna/bootstrap": "3.13.3", - "@lerna/changed": "3.13.3", - "@lerna/clean": "3.13.3", + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/lerna/-/lerna-3.13.1.tgz", + "integrity": "sha512-7kSz8LLozVsoUNTJzJzy+b8TnV9YdviR2Ee2PwGZSlVw3T1Rn7kOAPZjEi+3IWnOPC96zMPHVmjCmzQ4uubalw==", + "requires": { + "@lerna/add": "3.13.1", + "@lerna/bootstrap": "3.13.1", + "@lerna/changed": "3.13.1", + "@lerna/clean": "3.13.1", "@lerna/cli": "3.13.0", - "@lerna/create": "3.13.3", - "@lerna/diff": "3.13.3", - "@lerna/exec": "3.13.3", - "@lerna/import": "3.13.3", - "@lerna/init": "3.13.3", - "@lerna/link": "3.13.3", - "@lerna/list": "3.13.3", - "@lerna/publish": "3.13.3", - "@lerna/run": "3.13.3", - "@lerna/version": "3.13.3", + "@lerna/create": "3.13.1", + "@lerna/diff": "3.13.1", + "@lerna/exec": "3.13.1", + "@lerna/import": "3.13.1", + "@lerna/init": "3.13.1", + "@lerna/link": "3.13.1", + "@lerna/list": "3.13.1", + "@lerna/publish": "3.13.1", + "@lerna/run": "3.13.1", + "@lerna/version": "3.13.1", "import-local": "^1.0.0", "npmlog": "^4.1.2" } @@ -4126,11 +4096,6 @@ "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=" }, - "lodash.ismatch": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz", - "integrity": "sha1-dWy1FQyjum8RCFp4hJZF8Yj4Xzc=" - }, "lodash.set": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz", @@ -4190,9 +4155,9 @@ } }, "macos-release": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/macos-release/-/macos-release-2.2.0.tgz", - "integrity": "sha512-iV2IDxZaX8dIcM7fG6cI46uNmHUxHE4yN+Z8tKHAW1TBPMZDIKHf/3L+YnOuj/FK9il14UaVdHmiQ1tsi90ltA==" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/macos-release/-/macos-release-2.1.0.tgz", + "integrity": "sha512-8TCbwvN1mfNxbBv0yBtfyIFMo3m1QsNbKHv7PYIp/abRBKVQBXN7ecu3aeGGgT18VC/Tf397LBDGZF9KBGJFFw==" }, "make-dir": { "version": "1.3.0", @@ -4263,9 +4228,9 @@ } }, "mem": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz", - "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/mem/-/mem-4.2.0.tgz", + "integrity": "sha512-5fJxa68urlY0Ir8ijatKa3eRz5lwXnRCTvo9+TbTGAuTFJOwpGcY0X05moBd0nW45965Njt4CDI2GFQoG8DvqA==", "requires": { "map-age-cleaner": "^0.1.1", "mimic-fn": "^2.0.0", @@ -4273,9 +4238,9 @@ }, "dependencies": { "mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.0.0.tgz", + "integrity": "sha512-jbex9Yd/3lmICXwYT6gA/j2mNQGU48wCh/VzRd+/Y/PjYQtlg1gLMdZqvu9s/xH7qKvngxRObl56XZR609IMbA==" } } }, @@ -4767,9 +4732,9 @@ } }, "object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.0.tgz", + "integrity": "sha512-6OO5X1+2tYkNyNEx6TsCxEqFfRWaqx6EtMiSbGrw8Ob8v9Ne+Hl8rBAgLBZn5wjEz3s/s6U1WXFUFOcxxAwUpg==", "dev": true }, "object-visit": { @@ -4875,11 +4840,11 @@ } }, "os-name": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/os-name/-/os-name-3.1.0.tgz", - "integrity": "sha512-h8L+8aNjNcMpo/mAIBPn5PXCM16iyPGjHNWo6U1YO8sJTMHtEtyczI6QJnLoplswm6goopQkqc7OAnjhWcugVg==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/os-name/-/os-name-3.0.0.tgz", + "integrity": "sha512-7c74tib2FsdFbQ3W+qj8Tyd1R3Z6tuVRNNxXjJcZ4NgjIEQU9N/prVMqcW29XZPXGACqaXN3jq58/6hoaoXH6g==", "requires": { - "macos-release": "^2.2.0", + "macos-release": "^2.0.0", "windows-release": "^3.1.0" } }, @@ -4908,17 +4873,9 @@ "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=" }, "p-is-promise": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.1.0.tgz", - "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==" - }, - "p-limit": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", - "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", - "requires": { - "p-try": "^1.0.0" - } + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.0.0.tgz", + "integrity": "sha512-pzQPhYMCAgLAKPWD2jC3Se9fEfrD9npNos0y150EeqZll7akhEgGhTW/slB6lHku8AvYGiJ+YJ5hfHKePPgFWg==" }, "p-locate": { "version": "2.0.0", @@ -4926,6 +4883,21 @@ "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", "requires": { "p-limit": "^1.1.0" + }, + "dependencies": { + "p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "requires": { + "p-try": "^1.0.0" + } + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=" + } } }, "p-map": { @@ -4951,11 +4923,6 @@ "resolved": "https://registry.npmjs.org/p-reduce/-/p-reduce-1.0.0.tgz", "integrity": "sha1-GMKw3ZNqRpClKfgjH1ig/bakffo=" }, - "p-try": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=" - }, "p-waterfall": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-waterfall/-/p-waterfall-1.0.0.tgz", @@ -5025,18 +4992,18 @@ } }, "parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.0.tgz", + "integrity": "sha512-8Mf5juOMmiE4FcmzYc4IaiS9L3+9paz2KOiXzkRviCP6aDmN49Hz6EMWz0lGNp9pX80GvvAuLADtyGfW/Em3TA==", "dev": true, "requires": { "callsites": "^3.0.0" }, "dependencies": { "callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.0.0.tgz", + "integrity": "sha512-tWnkwu9YEq2uzlBDI4RcLn8jrFvF9AOi8PxDNU3hZZjJcjkcRAq3vCI+vZcg1SuxISDYe86k9VZFwAxDiJGoAw==", "dev": true } } @@ -5177,9 +5144,9 @@ "dev": true }, "prettier": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.17.0.tgz", - "integrity": "sha512-sXe5lSt2WQlCbydGETgfm1YBShgOX4HxQkFPvbxkcwgDvGDeqVau8h+12+lmSVlP3rHPz0oavfddSZg/q+Szjw==", + "version": "1.16.4", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.16.4.tgz", + "integrity": "sha512-ZzWuos7TI5CKUeQAtFd6Zhm2s6EpAD/ZLApIhsF9pRvRtM1RFo61dM/4MSRUA0SuLugA/zgrZD8m0BaY46Og7g==", "dev": true }, "process-nextick-args": { @@ -5305,9 +5272,9 @@ "integrity": "sha1-Q2CxfGETatOAeDl/8RQW4Ybc+7g=" }, "react-is": { - "version": "16.8.6", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.8.6.tgz", - "integrity": "sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA==", + "version": "16.8.4", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.8.4.tgz", + "integrity": "sha512-PVadd+WaUDOAciICm/J1waJaSvgq+4rHE/K70j0PFqKhkTBsPv/82UGQJNXAngz1fOQLLxI6z1sEDmJDQhCTAA==", "dev": true }, "read": { @@ -5582,9 +5549,9 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "semver": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", - "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==" + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz", + "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==" }, "semver-compare": { "version": "1.0.0", @@ -5685,6 +5652,14 @@ "use": "^3.1.0" }, "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, "define-property": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", @@ -5839,9 +5814,9 @@ } }, "spdx-license-ids": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.4.tgz", - "integrity": "sha512-7j8LYJLeY/Yb6ACbQ7F76qy5jHkp0U6jgBfJsk97bwWlVUnUWsAgpyaCvo17h0/RQGnQ036tVDomiwoI4pDkQA==" + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.3.tgz", + "integrity": "sha512-uBIcIl3Ih6Phe3XHK1NqboJLdGfwr1UN3k6wSD1dZpmPsIkb8AGNbZYJ1fOBk834+Gxy8rpfDxrS6XLEMZMY2g==" }, "split": { "version": "1.0.1", @@ -6224,12 +6199,12 @@ "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" }, "uglify-js": { - "version": "3.5.4", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.5.4.tgz", - "integrity": "sha512-GpKo28q/7Bm5BcX9vOu4S46FwisbPbAmkkqPnGIpKvKTM96I85N6XHQV+k4I6FA2wxgLhcsSyHoNhzucwCflvA==", + "version": "3.4.10", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.10.tgz", + "integrity": "sha512-Y2VsbPVs0FIshJztycsO2SfPk7/KAF/T72qzv9u5EpQ4kB2hQoHlhNQTsNyy6ul7lQtqJN/AoWeS23OzEiEFxw==", "optional": true, "requires": { - "commander": "~2.20.0", + "commander": "~2.19.0", "source-map": "~0.6.1" }, "dependencies": { @@ -6453,11 +6428,32 @@ } }, "windows-release": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/windows-release/-/windows-release-3.2.0.tgz", - "integrity": "sha512-QTlz2hKLrdqukrsapKsINzqMgOUpQW268eJ0OaOpJN32h272waxR9fkB9VoWRtK7uKHG5EHJcTXQBD8XZVJkFA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/windows-release/-/windows-release-3.1.0.tgz", + "integrity": "sha512-hBb7m7acFgQPQc222uEQTmdcGLeBmQLNLFIh0rDk3CwFOBrfjefLzEfEfmpMq8Af/n/GnFf3eYf203FY1PmudA==", "requires": { - "execa": "^1.0.0" + "execa": "^0.10.0" + }, + "dependencies": { + "execa": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-0.10.0.tgz", + "integrity": "sha512-7XOMnz8Ynx1gGo/3hyV9loYNPWM94jG3+3T3Y8tsfSstFmETmENCMU/A/zj8Lyaj1lkgEepKepvd6240tBRvlw==", + "requires": { + "cross-spawn": "^6.0.0", + "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" + } + }, + "get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=" + } } }, "wordwrap": { @@ -6598,9 +6594,9 @@ } }, "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.1.0.tgz", + "integrity": "sha512-H2RyIJ7+A3rjkwKC2l5GGtU4H1vkxKCAGsWasNVd0Set+6i4znxbWy6/j16YDPJDWxhsgZiKAstMEP8wCdSpjA==" }, "string-width": { "version": "2.1.1", @@ -6631,9 +6627,9 @@ }, "dependencies": { "camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.2.0.tgz", + "integrity": "sha512-IXFsBS2pC+X0j0N/GE7Dm7j3bsEBp+oTpb7F50dwEVX7rf3IgwO9XatnegTsDtniKCUtEJH4fSU6Asw7uoVLfQ==" } } } diff --git a/packages/event-cache/README.md b/packages/event-cache/README.md new file mode 100644 index 000000000000..110968e623d4 --- /dev/null +++ b/packages/event-cache/README.md @@ -0,0 +1,96 @@ +# Origin EventCache + +The `event-cache` package provides an augmentation to web3.js Contracts to allow caching of events as they are fetched. + +## Usage + +Best usage is just to patch the Web3 contract with `patchWeb3Contract()`. Then you can access the EventCache API from the `eventCache` attribute on the Web3 contract. + + import { patchWeb3Contract } from 'event-cache' + + const identity = IdentityEvents.deploy().send({ from: '0x0b44611b8ae632be05f24ffe64651f050402ae01' }) + patchWeb3Contract(identity) + + // Get all ident updates from a specific user + const events = await identity.eventCache.getPastEvents('IdentityUpdated', { + filter: { account: alice } + }) + +## EventCache API + +### Usage + + // Example from graphql contract module: + marketplace.eventCache = new EventCache({ + contract: marketplace, + fromBlock: epoch, + config + }) + +- `contract` - The Web3.js initiailzied Contract +- `fromBlock` - The block number to start from when fetching events +- `config` - A configuration `object`. See below: + +#### `config` object + +Example `config` object provided to constructor: + + { + platform: 'browser', + backend: new InMemoryBackend(), + ipfsEventCache: 'QmO0O0O0base64YOO000000....', + ipfsGateway: 'http://localhost:8080' + } + +- `platform` will tell EC which backend to use. Can be `browser`, `mobile`, `nodejs`, `ipfs`, or `auto` +- `backend` is an alternative to `platform` and will override any setting there and can provide any object with the Below API +- `ipfsEventcache`: The IPFS hash of the latest known cached results +- `ipfsGateway`: The HTTP(S) IPFS gateway to fetch cached results from + +#### `getPastEvents(eventName, options)` + +Retrieve all event logs for a specific event. + +- `eventName` - The name of the event +- `options` - An object with filters. This [matches the web3.eth.Contract API](https://web3js.readthedocs.io/en/1.0/web3-eth-contract.html#getpastevents) + +#### `getEvents(params)` + +Retrieve all event logs matching the params. + +- `params` - An object with keys to match against the event name and parameters + +##### Example + + await contract.eventCache.getEvents({ + event: 'IdentityUpdated', + account: '0x0b44611b8ae632be05f24ffe64651f050402ae01' + }) + +### `AbstractBackend` API + +This is the storage interface that `EventCache` uses to store and fetch events. Any backend implemented needs to match this interface at a minimum. + +#### `setLatestBlock(blockNumber)` + +Set the latest block number + +#### `getLatestBlock()` + +Get the latest block number known by the backend + +#### `addEvent(eventObject)` + +Add an event to the storage. + +- `eventObject` - For the structure of `eventObject`, see the [web3.js event Object](https://web3js.readthedocs.io/en/1.0/web3-eth-contract.html#contract-events-return). + +#### `addEvents(arrayOfEventObjects)` + +Add a batch of events as an array of event objects(see above). These can be fed directly from a `web3.eth.Contract`'s `getPastEvents()` call. + +#### `get(argMatchObject)` + +A general-purpose method to fetch events. Since there will be many storage backends, a lot of functionality is hidden by this method. And each backend will probably have a very unique implementation. + +- `argMatchObject` - An object to match against event objects diff --git a/packages/event-cache/migrations/20190410232631-event-create-table.js b/packages/event-cache/migrations/20190410232631-event-create-table.js new file mode 100644 index 000000000000..7cf529fadd80 --- /dev/null +++ b/packages/event-cache/migrations/20190410232631-event-create-table.js @@ -0,0 +1,78 @@ +'use strict' + +const TABLE_NAME = 'event' + +module.exports = { + up: (queryInterface, Sequelize) => { + return queryInterface + .createTable(TABLE_NAME, { + block_number: { + type: Sequelize.INTEGER, + primaryKey: true + }, + log_index: { + type: Sequelize.INTEGER, + primaryKey: true + }, + transaction_index: { + type: Sequelize.INTEGER, + allowNull: false + }, + block_hash: { + type: Sequelize.CHAR(66), + allowNull: false + }, + transaction_hash: { + type: Sequelize.CHAR(66), + allowNull: false + }, + topic0: { + type: Sequelize.CHAR(66) + }, + topic1: { + type: Sequelize.CHAR(66), + allowNull: true + }, + topic2: { + type: Sequelize.CHAR(66), + allowNull: true + }, + topic3: { + type: Sequelize.CHAR(66), + allowNull: true + }, + address: { + type: Sequelize.CHAR(42), + allowNull: false + }, + event: { + type: Sequelize.STRING, + allowNull: false + }, + signature: { + type: Sequelize.STRING, + allowNull: false + }, + data: { + type: Sequelize.JSONB, + allowNull: true + }, + return_values: { + type: Sequelize.JSONB, + allowNull: true + } + }) + .then(() => queryInterface.addIndex(TABLE_NAME, ['event'])) + .then(() => queryInterface.addIndex(TABLE_NAME, ['address'])) + .then(() => queryInterface.addIndex(TABLE_NAME, ['transaction_hash'])) + .then(() => queryInterface.addIndex(TABLE_NAME, ['block_number'])) + .then(() => queryInterface.addIndex(TABLE_NAME, ['topic0'])) + .then(() => queryInterface.addIndex(TABLE_NAME, ['topic1'])) + .then(() => queryInterface.addIndex(TABLE_NAME, ['topic2'])) + .then(() => queryInterface.addIndex(TABLE_NAME, ['topic3'])) + }, + + down: queryInterface => { + return queryInterface.dropTable(TABLE_NAME) + } +} diff --git a/packages/event-cache/package.json b/packages/event-cache/package.json new file mode 100644 index 000000000000..948fca0ad77d --- /dev/null +++ b/packages/event-cache/package.json @@ -0,0 +1,71 @@ +{ + "name": "@origin/event-cache", + "version": "0.1.0", + "description": "Origin Event Cache", + "author": "Mike Shultz ", + "license": "MIT", + "main": "src/index.js", + "files": [ + "dist", + "src" + ], + "scripts": { + "test": "NODE_ENV=test $(pwd)/node_modules/.bin/mocha -r @babel/register --file test/setup test", + "lint": "eslint . && npm run prettier:check", + "prettier": "prettier --write *.js \"src/**/*.js\"", + "prettier:check": "prettier -c *.js \"src/**/*.js\"", + "migrate": "sequelize db:migrate --migrations-path migrations --config src/pgconfig.js" + }, + "dependencies": { + "@origin/ipfs": "^0.1.0", + "bottleneck": "^2.17.1", + "debug": "^4.1.1", + "dexie": "^2.0.4", + "lodash": "^4.17.11", + "mocha": "^5.2.0", + "web3": "1.0.0-beta.34" + }, + "devDependencies": { + "@babel/core": "^7.3.4", + "@babel/plugin-proposal-class-properties": "^7.3.4", + "@babel/plugin-proposal-export-default-from": "^7.2.0", + "@babel/plugin-proposal-object-rest-spread": "^7.3.4", + "@babel/plugin-transform-destructuring": "^7.3.2", + "@babel/plugin-transform-object-assign": "^7.2.0", + "@babel/plugin-transform-runtime": "^7.3.4", + "@babel/preset-env": "^7.3.4", + "@babel/preset-react": "^7.0.0", + "@babel/register": "^7.0.0", + "@babel/runtime": "^7.3.4", + "@origin/contracts": "^0.8.6", + "@origin/services": "^0.1.0", + "eslint": "^5.15.3", + "fake-indexeddb": "^2.1.0", + "lodash-es": "^4.17.11", + "prettier": "^1.16.4", + "sequelize-cli": "^5.4.0", + "webpack": "^4.29.6" + }, + "optionalDependencies": { + "pg": "^7.7.1", + "sequelize": "^5.3.0" + }, + "babel": { + "presets": [ + "@babel/preset-env", + "@babel/preset-react" + ], + "plugins": [ + "@babel/plugin-proposal-export-default-from", + "@babel/plugin-transform-runtime", + "@babel/plugin-transform-destructuring", + "@babel/plugin-transform-object-assign", + "@babel/plugin-proposal-object-rest-spread", + "@babel/plugin-proposal-class-properties" + ] + }, + "prettier": { + "semi": false, + "singleQuote": true + } +} diff --git a/packages/event-cache/src/EventCache.js b/packages/event-cache/src/EventCache.js new file mode 100644 index 000000000000..cb7e3b974366 --- /dev/null +++ b/packages/event-cache/src/EventCache.js @@ -0,0 +1,282 @@ +const flattenDeep = require('lodash/flattenDeep') +const memoize = require('lodash/memoize') +const range = require('lodash/range') +const chunk = require('lodash/chunk') +const Web3 = require('web3') +const Bottleneck = require('bottleneck') + +const { get, post } = require('@origin/ipfs') + +const { debug, validateParams } = require('./utils') +const { + InMemoryBackend, + IndexedDBBackend, + PostgreSQLBackend +} = require('./backends/browser') + +const limiter = new Bottleneck({ maxConcurrent: 25 }) + +const getPastEvents = memoize( + async function(instance, fromBlock, toBlock, batchSize = 10000) { + if (!instance.loadedCache && instance.ipfsEventCache) { + try { + debug('Loading event cache from IPFS', instance.ipfsEventCache) + const cachedEvents = flattenDeep( + await Promise.all( + instance.ipfsEventCache.map(hash => get(instance.ipfsServer, hash)) + ) + ) + fromBlock = cachedEvents[cachedEvents.length - 1].blockNumber + 1 + debug(`Last cached blockNumber: ${fromBlock}`) + debug(`Loaded ${cachedEvents.length} events from IPFS cache`) + await instance.backend.addEvents(cachedEvents) + } catch (e) { + debug(`Error loading IPFS events`, e) + } + + instance.loadedCache = true + } + + const requests = range(fromBlock, toBlock + 1, batchSize).map(start => + limiter.schedule( + args => instance.contract.getPastEvents('allEvents', args), + { fromBlock: start, toBlock: Math.min(start + batchSize - 1, toBlock) } + ) + ) + + const numBlocks = toBlock - fromBlock + 1 + debug(`Get ${numBlocks} blocks in ${requests.length} requests`) + + instance.lastQueriedBlock = toBlock + 1 + + if (!numBlocks) return + + const newEvents = flattenDeep(await Promise.all(requests)) + debug(`Got ${newEvents.length} new events`) + + if (newEvents.length > 0) { + try { + await instance.backend.addEvents(newEvents) + debug(`Added all new events to backend`) + } catch (e) { + debug('Error adding new events to backend', e) + } + } + }, + (...args) => `${args[0].contract._address}-${args[1]}-${args[2]}` +) + +/** + * @class + * @classdesc EventCache to define the interface for EventCache backends + * + * Example configuration object(all optional): + * { + * backend: new InMemoryBackend(), + * platform: 'browser', // or 'nodejs', and eventually 'mobile' + * ipfsServer: 'http://localhost:5002', + * ipfsEventCache: 'QmBase64HashThisisTHISis...' + * } + */ +class EventCache { + /** + * constructor + * + * @param contract {web3.eth.Contract} The contract to patch + * @param fromBlock {number} The block number to start the event search at. If + * null, or not given it will start at the latest known to the cache + * backend + * @param config {object} A configuration JS object (See EventCache) + */ + constructor(contract, originBlock = 0, config) { + this._processConfig(config) + + this.contract = contract + this.originBlock = Number(originBlock) + this.web3 = new Web3(contract.currentProvider) + + const addr = (this.contract._address || 'no-contract').substr(0, 10) + debug(`Initialized ${addr} with originBlock ${this.originBlock}`) + } + + /** + * Detect and return a platform string + * + * @returns {string} - The platform string + */ + _detectPlatform() { + if (typeof window !== 'undefined') { + return 'browser' + } + return 'nodejs' + } + + /** + * _getBackend initializes a storage backend + * + * @returns {object} An initialized storage backend + */ + _getBackend(platform) { + if (!platform) platform = this._detectPlatform() + + switch (platform) { + case 'nodejs': + return new PostgreSQLBackend() + + case 'browser': + return new IndexedDBBackend({ prefix: this.prefix }) + case 'mobile': + case 'memory': + default: + return new InMemoryBackend() + } + } + + /** + * _processConfig processes the provided configuration object + */ + _processConfig(conf) { + this.prefix = conf.prefix || '' + if (typeof conf.backend !== 'undefined') { + this.backend = conf.backend + } else { + this.backend = this._getBackend(conf.platform) + } + + this.ipfsServer = + conf.ipfsGateway || conf.ipfsServer || 'https://ipfs.originprotocol.com' + + this.batchSize = conf.batchSize || 10000 + this.ipfsEventCache = + conf.ipfsEventCache && conf.ipfsEventCache.length + ? conf.ipfsEventCache + : null + + /** + * Only reason to set this false is if something external will manage the + * latest block with setLatestBlock() + */ + this.useLatestFromChain = + typeof conf.useLatestFromChain !== 'undefined' + ? conf.useLatestFromChain + : true + } + + /** + * _fetchEvents makes the necessary calls to fetch the event logs from the JSON-RPC provider + * + * @param fromBlock {number} The block to start the search at + * @param toBlock {T} The number to search to (or 'latest') + * @returns {Array} An array of event objects + */ + async _fetchEvents() { + let fromBlock = this.lastQueriedBlock || this.originBlock + const latestKnown = await this.backend.getLatestBlock() + + if (latestKnown > fromBlock) { + debug(`Set fromBlock to latestKnown (${latestKnown}) + 1`) + fromBlock = latestKnown + 1 + } + + let toBlock = this.latestBlock + if (this.useLatestFromChain || !toBlock) { + toBlock = this.latestBlock = await this.web3.eth.getBlockNumber() + } + + if (fromBlock > toBlock) { + debug(`fromBlock > toBlock (${fromBlock} > ${toBlock})`) + return + } + + await getPastEvents(this, fromBlock, toBlock, this.batchSize) + } + + /** + * getPastEvents retrieves all events + * + * @param eventName {string} The name of the event + * @param options {object} An Object as defined by web3.js' getPastEvents + * @returns {Array} An array of event objects + */ + async getPastEvents(eventName, options) { + let args = {} + if (options && options.filter) { + args = { + event: eventName, + ...options.filter + } + } else { + args = { + event: eventName + } + } + return await this.getEvents(args) + } + + /** + * getEvents retrieves all events fitting the filter + * @param params {object} - An object with params to match events against + * @returns {Array} - An array of event objects + */ + async getEvents(params) { + if (params && !validateParams(this.contract, params)) { + debug(params) + throw new TypeError('Invalid event parameters') + } + + await this._fetchEvents() + return await this.backend.get(params) + } + + /** + * allEvents retrieves all events wihtout filter + * @returns {Array} - An array of event objects + */ + async allEvents() { + await this._fetchEvents() + return await this.backend.all() + } + + /** + * Returns the latest block number known by the backend + * @returns {number} The latest known block number + */ + getBlockNumber() { + return this.backend.getLatestBlock() + } + + /** + * Set the latest known block number, if managing this externally + * @param {number} The latest known block number + */ + setLatestBlock(num) { + debug(`setLatestBlock to ${num}`) + this.latestBlock = num + } + + /** + * saveCheckpoint saves a checkpoint to IPFS for later reload + * + * @returns {string} - The IPFS hash of the checkpoint + */ + async saveCheckpoint() { + const serialized = await this.allEvents() + return await Promise.all( + chunk(serialized, 1500).map(events => post(this.ipfsServer, events, true)) + ) + } + + /** + * loadCheckpoint loads events from an IPFS hash + */ + async loadCheckpoint(ipfsHashes) { + const events = await Promise.all( + ipfsHashes.map(hash => get(this.ipfsServer, hash)) + ) + return flattenDeep(events) + } +} + +module.exports = { + EventCache +} diff --git a/packages/event-cache/src/backends/AbstractBackend.js b/packages/event-cache/src/backends/AbstractBackend.js new file mode 100644 index 000000000000..0e50eafd5242 --- /dev/null +++ b/packages/event-cache/src/backends/AbstractBackend.js @@ -0,0 +1,83 @@ +/** + * @class + * @classdesc AbstractBackend to define the interface for EventCache backends + */ +class AbstractBackend { + constructor() { + this.type = null + + if (new.target === AbstractBackend) { + throw new TypeError('AbstractBackend cannot be used directly') + } + } + + /** + * Returns the latest block number known by the backend + * @returns {number} The latest known block number + */ + getLatestBlock() { + return this.latestBlock + } + + /** + * Sets the latest block number known by the backend + */ + setLatestBlock(blockNumber) { + if (!blockNumber || blockNumber < this.latestBlock) return + this.latestBlock = blockNumber + } + + /** + * Serializes the stored events for storage + */ + async serialize() { + throw new TypeError('serialize() is not implemented by this backend') + } + + /** + * Fetch events from the store + * + * @param argMatchObject {object} A JS object representing the event + * @returns {Array} An array of event objects + */ + // eslint-disable-next-line no-unused-vars + async get(argMatchObject) { + throw new TypeError('get() must be ipmlemented') + } + + /** + * Fetch all events from the store + * + * @returns {Array} An array of event objects + */ + async all() { + throw new TypeError('all() must be ipmlemented') + } + + /** + * Stores a single event + * + * For more info on the eventObject, see: https://web3js.readthedocs.io/en/1.0/web3-eth-contract.html#contract-events-return + * + * @param {object} A JS object representing the event + */ + // eslint-disable-next-line no-unused-vars + async addEvent(eventObject) { + throw new TypeError('addEvent() must be ipmlemented') + } + + /** + * Stores multiple events + * + * @param {Array} An array of JS object representing the event + */ + async addEvents(eventObjects) { + for (let i = 0; i < eventObjects.length; i++) { + await this.addEvent(eventObjects[i]) + } + } +} + +module.exports = { + AbstractBackend +} diff --git a/packages/event-cache/src/backends/InMemoryBackend.js b/packages/event-cache/src/backends/InMemoryBackend.js new file mode 100644 index 000000000000..83b7c2417fc3 --- /dev/null +++ b/packages/event-cache/src/backends/InMemoryBackend.js @@ -0,0 +1,112 @@ +const { AbstractBackend } = require('./AbstractBackend') +const { compareEvents } = require('../utils') + +const ROOT_EVENT_KEYS = [ + 'logIndex', + 'transactionIndex', + 'transactionHash', + 'blockHash', + 'blockNumber', + 'address', + 'type', + 'id', + 'returnValues', + 'event', + 'signature', + 'raw' +] + +/** + * @class + * @classdesc InMemoryBackend to handle storage of EventCache data in memory + */ +class InMemoryBackend extends AbstractBackend { + constructor() { + super() + + this.type = 'memory' + this._storage = Array() // Array of objects + } + + /** + * Dumps an array of event objects + * + * @returns {Array} of events + */ + async serialize() { + return this._storage + } + + /** + * Loads the serialized data from IPFS + * + * @param ipfsData {Array} An array of events to load + */ + async loadSerialized(ipfsData) { + this._storage = ipfsData + } + + /** + * Fetch events from the store matching objects + * + * @param argMatchObject {object} A JS object representing the event + * @returns {Array} An array of event objects + */ + async get(argMatchObject) { + return this._storage + .filter(el => { + const matches = Object.keys(argMatchObject).filter(key => { + let isReturnValue = false + + if (ROOT_EVENT_KEYS.indexOf(key) < 0) { + isReturnValue = true + } + + let matchingEl = el + + if (isReturnValue) matchingEl = el.returnValues + + if (typeof argMatchObject[key] !== 'undefined') { + if ( + (argMatchObject[key] instanceof Array && + argMatchObject[key].indexOf(matchingEl[key]) > -1) || + argMatchObject[key] == matchingEl[key] + ) { + return el + } + } + }) + + // Make sure all provided keys were matched + if (matches.length === Object.keys(argMatchObject).length) { + return el + } + }) + .sort(compareEvents) + } + + /** + * Fetch all events from the store + * + * @returns {Array} An array of event objects + */ + async all() { + return this._storage.sort(compareEvents) + } + + /** + * Stores a single event + * + * For more info on the eventObject, see: https://web3js.readthedocs.io/en/1.0/web3-eth-contract.html#contract-events-return + * + * @param eventObject {object} A JS object representing the event + */ + async addEvent(eventObject) { + this._storage.push(eventObject) + this.setLatestBlock(eventObject.blockNumber) + } +} + +module.exports = { + InMemoryBackend +} diff --git a/packages/event-cache/src/backends/IndexedDBBackend.js b/packages/event-cache/src/backends/IndexedDBBackend.js new file mode 100644 index 000000000000..3d97642c34f7 --- /dev/null +++ b/packages/event-cache/src/backends/IndexedDBBackend.js @@ -0,0 +1,381 @@ +const Dexie = require('dexie').default +const uniq = require('lodash/uniq') +const memoize = require('lodash/memoize') +const intersectionBy = require('lodash/intersectionBy') + +const { AbstractBackend } = require('./AbstractBackend') +const { debug, compareEvents } = require('../utils') + +/** + * Check and see if IndexedDB is available for use + */ +function checkForIndexedDB() { + // Just look for a global, since testing in node may inject + if (typeof window !== 'undefined' && typeof indexedDB === 'undefined') { + throw new Error('Unable to find IndexedDB') + } +} +/** + * Normalize index names so they would match get() args. Used for matching. + * + * @param indexes {Array} of fully qualified index names + * @returns {Array} of normalized index names + */ +function normalizedIndexes(indexes) { + return uniq( + indexes.map(idx => { + if (idx.includes('.')) { + return idx.split('.').pop() + } + return idx + }) + ) +} + +/** + * Initialize the IndexedDB, object store, and indexes + */ +const initDB = memoize(async function({ IndexedDB, IDBKeyRange, dbName }) { + debug(`initDB ${dbName}`) + const opts = {} + const stores = {} + + if (IndexedDB) opts['indexedDB'] = IndexedDB + if (IndexedDB) opts['IDBKeyRange'] = IDBKeyRange + + const db = new Dexie(dbName, opts) + stores[EVENT_STORE] = INDEXES.join(', ') + db.version(SCHEMA_VERSION).stores(stores) + const eventStore = db[EVENT_STORE] + + /** + * Get the known block number from cache. Can be a little slow, but should + * be non-blocking(I think) + */ + const lastIndexedBlock = await new Promise(resolve => { + db[EVENT_STORE].orderBy('blockNumber') + .reverse() + .limit(1) + .toArray() + .then(res => { + if (res && res.length > 0) { + debug(`lastIndexedBlock`, res[0].blockNumber) + resolve(res[0].blockNumber) + } + resolve(null) + }) + }) + + return { db, eventStore, lastIndexedBlock } +}) + +/** + * Create a map of arg names to fully qualified index names + * + * @param indexes {Array} of fully qualified index names + * @returns {object} mapping of arg -> index name + */ +function createIndexeMap(indexes) { + const idxMap = {} + indexes.map(idx => { + if (idx.includes('.')) { + idxMap[idx.split('.').pop()] = idx + } else { + idxMap[idx] = idx + } + }) + return idxMap +} + +/** + * Get a value from an object, using dot notation + * + * @param key {string} key name, can include dot notation + * @returns {any} value + */ +function getFromObject(key, obj) { + if (key.indexOf('.') > -1) { + return key.split('.').reduce((o, i) => o[i], obj) + } else { + return obj[key] + } +} + +const DB_NAME = 'origin-event-cache' +const EVENT_STORE = 'events' +const SCHEMA_VERSION = 1 +// TODO: Fill out the indexes +const INDEXES = [ + '[blockNumber+transactionHash+transactionIndex]', + 'event', + 'blockNumber', + 'transactionHash', + 'transactionIndex', + 'address', + 'returnValues.account', + 'returnValues.party', + 'returnValues.listingID', + 'returnValues.offerID' +] +const NORM_INDEXES = normalizedIndexes(INDEXES) +const ARG_TO_INDEX_MAP = createIndexeMap(INDEXES) + +/** + * @class + * @classdesc IndexedDBBackend for running in-browser storage + */ +class IndexedDBBackend extends AbstractBackend { + constructor(args) { + const { testing = false, prefix = '' } = args || {} + super() + + this.type = 'indexeddb' + this.dbName = `${prefix}${DB_NAME}` + this.ready = false + this._db = null + this._eventStore = null + this.IndexedDB = null + this.IDBKeyRange = null + + // Make sure we're sane + if (testing) { + if (typeof global === 'undefined') { + throw new Error('Environment not sane for testing.') + } + /** + * We're testing here, anything goes! + */ + require('fake-indexeddb/auto') + this.IndexedDB = require('fake-indexeddb') + this.IDBKeyRange = require('fake-indexeddb/lib/FDBKeyRange') + } else { + checkForIndexedDB() + } + } + + /** + * Idle until the DB initialization is done + */ + async waitForReady() { + if (this.ready) { + return + } + + const { db, eventStore, lastIndexedBlock } = await initDB({ + IndexedDB: this.IndexedDB, + IDBKeyRange: this.IDBKeyRange, + dbName: this.dbName + }) + + this.latestBlock = lastIndexedBlock + this._db = db + this._eventStore = eventStore + this.ready = true + } + + /** + * Dumps an array of event objects + * + * @returns {Array} of events + */ + async serialize() { + await this.waitForReady() + return await this._eventStore.getAll(EVENT_STORE) + } + + /** + * Loads the serialized data from IPFS + * + * @param ipfsData {Array} An array of events to load + */ + async loadSerialized(ipfsData) { + await this.waitForReady() + + if (!(ipfsData instanceof Array)) { + throw new TypeError('Serialized data should be an Array of objects') + } + + await this.addEvents(ipfsData) + } + + /** + * Fetch events from the store matching objects + * + * @param argMatchObject {object} A JS object representing the event + * @returns {Array} An array of event objects + */ + async get(argMatchObject) { + await this.waitForReady() + + const indexedArgs = Object.keys(argMatchObject).filter(key => { + if ( + typeof argMatchObject[key] !== 'undefined' && + NORM_INDEXES.includes(key) + ) { + return key + } + }) + + const unindexedArgs = Object.keys(argMatchObject).filter(key => { + if ( + typeof argMatchObject[key] !== 'undefined' && + !NORM_INDEXES.includes(key) + ) { + return key + } + }) + + /** + * A little hinky, but first get the results from any indexed args. There's + * apparently no way to utilize multiple indexes at once, so, we're kind of + * getting spicy here. + * + * Since we also don't know the granularity of each index, we're going to + * run a query against every matching index, then intersect them ourselves + * for the results. + */ + const indexedSet = [] + if (indexedArgs.length > 0) { + let matchedSet = [] + + // Query against each index that maches an arg + for (let i = 0; i < indexedArgs.length; i++) { + // We need to iterate the index manually if we're checking an array + if (argMatchObject[indexedArgs[i]] instanceof Array) { + const res = await this._eventStore + .where(ARG_TO_INDEX_MAP[indexedArgs[i]]) + .anyOf(argMatchObject[indexedArgs[i]]) + .toArray() + + indexedSet.push(res) + } else { + try { + const res = await this._eventStore + .where(ARG_TO_INDEX_MAP[indexedArgs[i]]) + .equals(argMatchObject[indexedArgs[i]]) + .toArray() + indexedSet.push(res) + } catch (err) { + console.log('Error trying to do an index get') + console.log(err) + throw err + } + } + } + + // Get only the objects that matched all the indexed args + matchedSet = intersectionBy(...indexedSet, el => { + return `${el.event}-${el.transactionHash}-${el.logIndex}` + }) + + // And do further matching against the unindexed args, if any + if (unindexedArgs.length > 0) { + return matchedSet.filter(el => { + const matches = Object.keys(unindexedArgs).filter(key => { + if (typeof argMatchObject[key] !== 'undefined') { + if ( + (argMatchObject[key] instanceof Array && + argMatchObject[key].indexOf(getFromObject(key, el)) > -1) || + argMatchObject[key] == getFromObject(key, el) + ) { + return el + } + } + }) + + // Make sure all provided keys were matched + if (matches.length === Object.keys(unindexedArgs).length) { + return el + } + }) + } + + return matchedSet.sort(compareEvents) + } else { + // What the hell, index your life + debug('unindexed get(). This will be slow!', argMatchObject) + + const everything = await this._eventStore.toArray() + + return everything + .filter(el => { + const matches = Object.keys(unindexedArgs).filter(key => { + if (typeof argMatchObject[key] !== 'undefined') { + if ( + (argMatchObject[key] instanceof Array && + argMatchObject[key].indexOf(getFromObject(key, el)) > -1) || + argMatchObject[key] == getFromObject(key, el) + ) { + return el + } + } + }) + + // Make sure all provided keys were matched + if (matches.length === Object.keys(unindexedArgs).length) { + return el + } + }) + .sort(compareEvents) + } + } + + /** + * Fetch all events from the store + * + * @returns {Array} An array of event objects + */ + async all() { + await this.waitForReady() + + const items = await this._eventStore.toArray() + return items.sort(compareEvents) + } + + /** + * Stores multiple events + * + * @param {Array} An array of JS object representing the event + */ + async addEvents(eventObjects) { + await this.waitForReady() + + await this._eventStore.bulkAdd(eventObjects) + this.setLatestBlock(eventObjects[eventObjects.length - 1].blockNumber) + } + + /** + * Returns the latest block number known by the backend + * @returns {number} The latest known block number + */ + async getLatestBlock() { + await this.waitForReady() + return this.latestBlock + } + + /** + * Stores a single event + * + * For more info on the eventObject, see: https://web3js.readthedocs.io/en/1.0/web3-eth-contract.html#contract-events-return + * + * @param eventObject {object} A JS object representing the event + */ + async addEvent(eventObject) { + await this.waitForReady() + + try { + await this._eventStore.add(eventObject) + } catch (err) { + if (String(err).includes('exists')) { + debug('duplicate event') + } else { + throw err + } + } + this.setLatestBlock(eventObject.blockNumber) + } +} + +module.exports = { + IndexedDBBackend +} diff --git a/packages/event-cache/src/backends/PostgreSQLBackend.js b/packages/event-cache/src/backends/PostgreSQLBackend.js new file mode 100644 index 000000000000..13553ee358dd --- /dev/null +++ b/packages/event-cache/src/backends/PostgreSQLBackend.js @@ -0,0 +1,224 @@ +const { AbstractBackend } = require('./AbstractBackend') + +const { debug } = require('../utils') + +/** + * Convert an event object to an object compatible with sequelize + * + * @param eventObject {object} to match the event against + * @returns {object} object matching the sequelize names + */ +function eventToDB(eventObject) { + const dbObject = {} + + Object.keys(eventObject).map(key => { + // Convert camel to underscore + dbObject[ + key + .split(/(?=[A-Z])/) + .join('_') + .toLowerCase() + ] = eventObject[key] + }) + + // Rework the topic structure to match the DB structure + if (dbObject['raw']) { + dbObject['data'] = dbObject['raw']['data'] + if (dbObject['raw']['topics'] instanceof Array) { + for (let i = 0; i < dbObject['raw']['topics'].length; i++) { + if (i > 3) break + dbObject['topic' + i] = dbObject['raw']['topics'][i] || null + } + } + delete dbObject['raw'] + } + + return dbObject +} + +/** + * Convert a sequelize result object to an event object + * + * @param eventObject {object} sequelize object (e.g. result row) + * @returns {object} event object + */ +function DBToEvent(dbObject) { + const evObj = {} + Object.keys(dbObject).map(key => { + // Convert underscore to camel + const camelKey = key + .split(/(?=_[a-z])/) + .map(part => { + return part.startsWith('_') + ? part.charAt(1).toUpperCase() + part.slice(2) + : part + }) + .join('') + evObj[camelKey] = dbObject[key] + }) + + // Rework the topic structure to match an event object + if (evObj['data'] || evObj['topic0']) { + evObj['raw'] = {} + evObj['raw']['data'] = evObj['data'] + evObj['raw']['topics'] = Array() + + if (evObj['topic0']) evObj['raw']['topics'].push(evObj['topic0']) + if (evObj['topic1']) evObj['raw']['topics'].push(evObj['topic1']) + if (evObj['topic2']) evObj['raw']['topics'].push(evObj['topic2']) + if (evObj['topic3']) evObj['raw']['topics'].push(evObj['topic3']) + + delete evObj['data'] + } + delete evObj['topic0'] + delete evObj['topic1'] + delete evObj['topic2'] + delete evObj['topic3'] + + return evObj +} + +/** + * @class + * @classdesc PostgreSQLBackend to handle event storage in PostgreSQL + */ +class PostgreSQLBackend extends AbstractBackend { + constructor() { + super() + + this.type = 'postgresql' + /** + * This import needs to be done here, because sequelize is a PoS and + * requires a connection to be initialized to build the models. And to + * boot, it won't even play nice with ES6/babel and imports, so it also + * doesn't even match the codebase. + * + * Why is it that ORMs are supposed to save time and yet I always end up + * spending more time getting them to work than it would have if I just + * wrote the SQL and did the data translation myself? + * + * Do rants count as useful and productive documentation? + */ + this._models = require('../models') + this._Sequelize = this._models.Sequelize + this._sequelize = this._models.sequelize + this._eventTableColumns = Object.keys(this._models.Event.tableAttributes) + + this._loadLatestBlock() + } + + /** + * Get and set the latest known block from the DB + * + * @returns {number} The latest block number known by the cache + */ + async _loadLatestBlock() { + const result = await this._models.Event.findOne({ + attributes: [ + [ + this._sequelize.fn('MAX', this._sequelize.col('block_number')), + 'max_block' + ] + ] + }) + if (result) { + const maxBlock = result.dataValues.max_block + this.setLatestBlock(maxBlock) + debug(`Cache is current up to block #${maxBlock}`) + return maxBlock + } + return 0 + } + + /** + * Convert an arg match object to something that can be used for sequelize + * queries as a `where` argument + * + * @param obj {object} An argMatchObject + * @returns {object} to use as a WHERE clause with sequelize + */ + _argMatchToWhere(obj) { + const Op = this._Sequelize.Op + const newObj = eventToDB(obj) + Object.keys(newObj).map(key => { + if (!this._eventTableColumns.includes(key)) { + const op = newObj[key] instanceof Array ? Op.or : Op.eq + newObj['return_values.' + key] = { + [op]: newObj[key] + } + delete newObj[key] + } + }) + return newObj + } + + /** + * Dumps an array of event objects + * + * @returns {Array} of events + */ + async serialize() { + const res = await this._models.Event.findAll({ + order: [['block_number'], ['transaction_index'], ['log_index']] + }) + if (res.length < 1) { + return [] + } + + return res.map(row => DBToEvent(row.dataValues)) + } + + /** + * Loads the serialized data from IPFS + * + * @param ipfsData {Array} An array of events to load + */ + // eslint-disable-next-line no-unused-vars + async loadSerialized(ipfsData) { + // TODO? Not sure this is healthy for this backend + throw new Error('Cannot load serialized data on this platform') + } + + /** + * Fetch events from the store matching objects + * + * @param argMatchObject {object} A JS object representing the event + * @returns {Array} An array of event objects + */ + async get(argMatchObject) { + const where = this._argMatchToWhere(argMatchObject) + const results = await this._models.Event.findAll({ where }) + return results.map(row => { + return DBToEvent(row.dataValues) + }) + } + + /** + * Fetch all events from the store + * + * @returns {Array} An array of event objects + */ + async all() { + const results = await this._models.Event.findAll() + return results.map(row => { + return DBToEvent(row.dataValues) + }) + } + + /** + * Stores a single event + * + * For more info on the eventObject, see: https://web3js.readthedocs.io/en/1.0/web3-eth-contract.html#contract-events-return + * + * @param eventObject {object} A JS object representing the event + */ + async addEvent(eventObject) { + const dbObject = eventToDB(eventObject) + await this._models.Event.upsert(dbObject) + this.setLatestBlock(eventObject.blockNumber) + } +} + +module.exports = { + PostgreSQLBackend +} diff --git a/packages/event-cache/src/backends/browser.js b/packages/event-cache/src/backends/browser.js new file mode 100644 index 000000000000..0e08b3673803 --- /dev/null +++ b/packages/event-cache/src/backends/browser.js @@ -0,0 +1,7 @@ +const { InMemoryBackend } = require('./InMemoryBackend') +const { IndexedDBBackend } = require('./IndexedDBBackend') + +module.exports = { + InMemoryBackend, + IndexedDBBackend +} diff --git a/packages/event-cache/src/backends/index.js b/packages/event-cache/src/backends/index.js new file mode 100644 index 000000000000..002c27a48c68 --- /dev/null +++ b/packages/event-cache/src/backends/index.js @@ -0,0 +1,9 @@ +const { InMemoryBackend } = require('./InMemoryBackend') +const { IndexedDBBackend } = require('./IndexedDBBackend') +const { PostgreSQLBackend } = require('./PostgreSQLBackend') + +module.exports = { + InMemoryBackend, + IndexedDBBackend, + PostgreSQLBackend +} diff --git a/packages/event-cache/src/index.js b/packages/event-cache/src/index.js new file mode 100644 index 000000000000..893ab72b25d1 --- /dev/null +++ b/packages/event-cache/src/index.js @@ -0,0 +1,21 @@ +const { EventCache } = require('./EventCache') + +/** + * This function will patch a web3.eth.Contract with the eventCache method + * + * @param contract {web3.eth.Contract} The contract to patch + * @param fromBlock {number} The block number to start the event search at + * @param config {object} A configuration JS object (See EventCache) + */ +function patchWeb3Contract(contract, fromBlock = 0, config) { + if (contract.hasOwnProperty('eventCache')) { + throw new TypeError(`Contract already has eventCache property!`) + } + contract.eventCache = new EventCache(contract, fromBlock, config) + return contract +} + +module.exports = { + EventCache, + patchWeb3Contract +} diff --git a/packages/event-cache/src/models/event.js b/packages/event-cache/src/models/event.js new file mode 100644 index 000000000000..9c9f500dafc0 --- /dev/null +++ b/packages/event-cache/src/models/event.js @@ -0,0 +1,48 @@ +'use strict' +module.exports = (sequelize, DataTypes) => { + const Event = sequelize.define( + 'Event', + { + // Block number at which the event was recorded. + block_number: { + type: DataTypes.INTEGER, + primaryKey: true + }, + // Index of the event within the block. + log_index: { + type: DataTypes.INTEGER, + primaryKey: true + }, + // Index of the transaction within the block. + transaction_index: DataTypes.INTEGER, + // Hash of the block that contains the event, + block_hash: DataTypes.CHAR, + // Hash of the transaction that triggered firing the event, + transaction_hash: DataTypes.CHAR, + // First topic is the signature of the event. + topic0: DataTypes.CHAR, + // Next 3 topics are the optional indexed event arguments. + topic1: DataTypes.CHAR, + topic2: DataTypes.CHAR, + topic3: DataTypes.CHAR, + // Address of the contract + address: DataTypes.CHAR, + // The name of the event + event: DataTypes.STRING, + // The event signature + signature: DataTypes.STRING, + // JSON data for the event as returned by web3 method getPastEvents. + data: DataTypes.JSONB, + // JSON data of decoded event arguments + return_values: DataTypes.JSONB + }, + { + tableName: 'event', + timestamps: false + } + ) + + Event.associate = function() {} + + return Event +} diff --git a/packages/event-cache/src/models/index.js b/packages/event-cache/src/models/index.js new file mode 100644 index 000000000000..bf1cd7e53501 --- /dev/null +++ b/packages/event-cache/src/models/index.js @@ -0,0 +1,43 @@ +'use strict' + +const fs = require('fs') +const path = require('path') +const Sequelize = require('sequelize') +const basename = path.basename(__filename) +const env = process.env.NODE_ENV || 'development' +const config = require(path.join(__dirname, '../pgconfig'))[env] +const db = {} + +let sequelize +if (config.use_env_variable) { + sequelize = new Sequelize(process.env[config.use_env_variable], config) +} else { + sequelize = new Sequelize( + config.database, + config.username, + config.password, + config + ) +} + +fs.readdirSync(__dirname) + .filter(file => { + return ( + file.indexOf('.') !== 0 && file !== basename && file.slice(-3) === '.js' + ) + }) + .forEach(file => { + const model = sequelize['import'](path.join(__dirname, file)) + db[model.name] = model + }) + +Object.keys(db).forEach(modelName => { + if (db[modelName].associate) { + db[modelName].associate(db) + } +}) + +db.sequelize = sequelize +db.Sequelize = Sequelize + +module.exports = db diff --git a/packages/event-cache/src/pgconfig.js b/packages/event-cache/src/pgconfig.js new file mode 100644 index 000000000000..6b89cc2799bf --- /dev/null +++ b/packages/event-cache/src/pgconfig.js @@ -0,0 +1,44 @@ +module.exports = { + development: { + dialect: 'postgres', + use_env_variable: 'DATABASE_URL', + define: { + // Add the timestamp attributes (updatedAt, createdAt). + timestamps: true, + // Disable the modification of table names. + freezeTableName: true, + // Underscore style for field names. + underscored: true + }, + // Disable logging of SQL statements. + logging: false + }, + test: { + dialect: 'postgres', + use_env_variable: 'DATABASE_URL', + define: { + // Add the timestamp attributes (updatedAt, createdAt). + timestamps: true, + // Disable the modification of table names. + freezeTableName: true, + // Underscore style for field names. + underscored: true + }, + // Disable logging of SQL statements. + logging: false + }, + production: { + dialect: 'postgres', + use_env_variable: 'DATABASE_URL', + define: { + // Add the timestamp attributes (updatedAt, createdAt). + timestamps: true, + // Disable the modification of table names. + freezeTableName: true, + // Underscore style for field names. + underscored: true + }, + // Disable logging of SQL statements. + logging: false + } +} diff --git a/packages/event-cache/src/utils.js b/packages/event-cache/src/utils.js new file mode 100644 index 000000000000..f1cac26bc4f6 --- /dev/null +++ b/packages/event-cache/src/utils.js @@ -0,0 +1,130 @@ +const createDebug = require('debug') + +const debug = createDebug('event-cache') + +/* + * Pull a single Event definition from a contract ABI + * + * @param abi {Array} of objects, representing the ABI + * @param eventName {string} name of the event + * @returns {object} The event definition + */ +function getEventDef(abi, eventName) { + for (let i = 0; i < abi.length; i++) { + if (!abi[i].type || abi[i].type !== 'event') continue + if (abi[i].name === eventName) return abi[i] + } + return null +} + +/* + * Pull all Event definitions from a contract ABI + * + * @param abi {Array} of objects, representing the ABI + * @returns {object} The event definition + */ +function getAllEventsDef(abi) { + const defs = new Set() + for (let i = 0; i < abi.length; i++) { + if (!abi[i].type || abi[i].type !== 'event') continue + defs.add(abi[i]) + } + return defs +} + +/* + * Get an array of input names from a single event ABI definition + * + * @param abi {object} representing the ABI of the single Event + * @returns {Array} of input names + */ +function getInputsFromDef(abi) { + return abi.inputs.map(inp => inp.name) +} + +/* + * Validate that the given get() parameters match the event ABI + * + * @param contract {web3.eth.Contract} The contrat being referenced + * @param params {object} The params given to get() + */ +function validateParams(contract, params) { + if (!params) return {} + + const availableEvents = Object.keys(contract.events) + + if (params.event) { + if (params.event instanceof Array) { + if (!params.event.every(ev => availableEvents.includes(ev))) { + console.warning('At least one event does not exist in contract') + return false + } + } else if (!availableEvents.includes(params.event)) { + console.warning(`event does not exist in contract`) + debug(`expected ${params.event}`) + return false + } + } + + // According to the 1.0 docs jsonInterface is a thing, but... not in beta 34? + // const eventDef = contract.jsonInterface.getEvent(params.event) + // const inputs = eventDef.getInputs().map(inp => inp.name) + + // So instead, we're using an internal undocumented 'API' here, so beware + let inputs = new Set() + if (params.event) { + if (params.event instanceof Array) { + params.event.map(ev => { + const eventDef = getEventDef(contract._jsonInterface, ev) + const foundInputs = getInputsFromDef(eventDef) + foundInputs.map(inp => inputs.add(inp)) + }) + } else { + const eventDef = getEventDef(contract._jsonInterface, params.event) + if (!eventDef) { + console.error( + `Unable to find event definition in ABI, but it is defined in Contract object. This probably shouldln't happen.` + ) + } else { + inputs = new Set(getInputsFromDef(eventDef)) + } + } + } else { + // No specific event(s), get all possible inputs + const defs = getAllEventsDef(contract._jsonInterface) + if (defs) { + defs.forEach(ev => { + const ins = getInputsFromDef(ev) + ins.map(_input => inputs.add(_input)) + }) + } + } + return Object.keys(params).every(param => { + if (param === 'event') return true + if (inputs.has(param)) return true + debug('param does not match contract') + return false + }) +} + +/** + * Sorting for events + * + * @param key {string} key name, can include dot notation + * @returns {any} value + */ +function compareEvents(a, b) { + if (a.blockNumber < b.blockNumber) return -1 + if (a.blockNumber > b.blockNumber) return 1 + if (a.transactionIndex < b.transactionIndex) return -1 + if (a.transactionIndex > b.transactionIndex) return 1 + if (a.logIndex < b.logIndex) return -1 + if (a.logIndex > b.logIndex) return 1 + return 0 +} + +module.exports = { + debug, + validateParams, + compareEvents +} diff --git a/packages/event-cache/test/_contracts.js b/packages/event-cache/test/_contracts.js new file mode 100644 index 000000000000..409f93428def --- /dev/null +++ b/packages/event-cache/test/_contracts.js @@ -0,0 +1,21 @@ +const MarketplaceContract = require('@origin/contracts/build/contracts/V00_Marketplace') +const TestTokenContract = require('@origin/contracts/build/contracts/TestToken') +const IdentityEventsContract = require('@origin/contracts/build/contracts/IdentityEvents') + +const Web3 = require('web3') + +// We're testing against a ganache instance launched from ./setup +const web3 = new Web3(new Web3.providers.HttpProvider('http://localhost:8545')) + +module.exports = { + web3, + marketplace: new web3.eth.Contract(MarketplaceContract.abi, null, { + data: MarketplaceContract.bytecode + }), + token: new web3.eth.Contract(TestTokenContract.abi, null, { + data: TestTokenContract.bytecode + }), + identity: new web3.eth.Contract(IdentityEventsContract.abi, null, { + data: IdentityEventsContract.bytecode + }) +} diff --git a/packages/event-cache/test/_ipfs.js b/packages/event-cache/test/_ipfs.js new file mode 100644 index 000000000000..5de51718be9a --- /dev/null +++ b/packages/event-cache/test/_ipfs.js @@ -0,0 +1,9 @@ +const { post } = require('@origin/ipfs') + +async function addObject(jsObject) { + return await post('http://localhost:5002', jsObject) +} + +module.exports = { + addObject +} diff --git a/packages/event-cache/test/const.js b/packages/event-cache/test/const.js new file mode 100644 index 000000000000..56344abc3956 --- /dev/null +++ b/packages/event-cache/test/const.js @@ -0,0 +1,13 @@ +const STD_GAS = 1e6 +const STD_GAS_DEPLOY = 6e6 +const STD_GAS_PRICE = 3e9 +// Due to some random web3.js/BN.js encoding issue... +// const INT_1E18 = '1000000000000000000' +const INT_1E24 = '1000000000000000000000000' + +module.exports = { + STD_GAS, + STD_GAS_PRICE, + STD_GAS_DEPLOY, + INT_1E24 +} diff --git a/packages/event-cache/test/index.js b/packages/event-cache/test/index.js new file mode 100644 index 000000000000..0260f98ad1a2 --- /dev/null +++ b/packages/event-cache/test/index.js @@ -0,0 +1,194 @@ +const assert = require('assert') + +const { InMemoryBackend } = require('../src/backends') +const { EventCache, patchWeb3Contract } = require('../src') + +const contracts = require('./_contracts') +const ipfs = require('./_ipfs') +const { STD_GAS, STD_GAS_DEPLOY, STD_GAS_PRICE, INT_1E24 } = require('./const') + +describe('EventCache', function() { + + let Marketplace, + OriginToken, + IdentityEvents + + let owner, + alice, + bob, + charlie, + denise + + before(async function() { + const accounts = await contracts.web3.eth.getAccounts() + owner = accounts[0] + alice = accounts[1] + bob = accounts[2] + charlie = accounts[3] + denise = accounts[4] + + const deploytx = { + from: owner, + gas: STD_GAS_DEPLOY, + gasPrice: STD_GAS_PRICE + } + + OriginToken = await contracts.token.deploy({ + arguments: ['OriginToken', 'OGN', 18, INT_1E24] + }).send(deploytx) + + Marketplace = await contracts.marketplace.deploy({ + arguments: [OriginToken.address] + }).send(deploytx) + + IdentityEvents = await contracts.identity.deploy().send(deploytx) + + }) + + after(async function() { + console.debug('AFTER') + }) + + it('should initialize the proper backend', async () => { + // Memory + const memoryBackend = new InMemoryBackend() + const eventCacheMemory = new EventCache(IdentityEvents, 0, { + backend: memoryBackend + }) + assert( + eventCacheMemory.backend.type === 'memory', + `Expected 'memory' backend, got '${eventCacheMemory.backend.type}'` + ) + + /* TODO + // Node + const eventCacheNode = new EventCache(IdentityEvents, 0, { + platform: 'nodejs' + }) + assert(eventCacheNode.backend.type == 'postgresql')*/ + + }) + + it('should process events', async () => { + + const tx = { + from: alice, + gas: STD_GAS, + gasPrice: STD_GAS_PRICE + } + + // Memory + const memoryBackend = new InMemoryBackend() + const eventCacheMemory = new EventCache(IdentityEvents, 0, { + backend: memoryBackend + }) + + let expectedEvents = 0 + + // First, lets generate an event + const testObjectHash = await ipfs.addObject({ one: 1 }) + const receipt = await IdentityEvents.methods.emitIdentityUpdated(testObjectHash).send(tx) + assert(receipt.status == 1, 'emitIdentityUpdated() transaction failed') + expectedEvents += 1 + + const initialEvents = await eventCacheMemory.getPastEvents('IdentityUpdated', { + filter: { account: alice } + }) + + assert( + initialEvents.length == expectedEvents, + `Expected ${expectedEvents} event, got ${initialEvents.length}` + ) + assert(initialEvents[0].returnValues.account === alice, 'Unexpected account in event') + assert(initialEvents[0].returnValues.ipfsHash === testObjectHash, 'Unexpected hash in event') + + // Let's do another round + const secondObjectHash = await ipfs.addObject({ two: 2 }) + const secondReceipt = await IdentityEvents.methods.emitIdentityUpdated(secondObjectHash).send(tx) + assert(secondReceipt.status == 1, 'emitIdentityUpdated() transaction failed') + expectedEvents += 1 + + const thirdObjectHash = await ipfs.addObject({ two: 2 }) + const thirdReceipt = await IdentityEvents.methods.emitIdentityUpdated(thirdObjectHash).send( + Object.assign({}, tx, { + from: bob + }) + ) + assert(thirdReceipt.status == 1, 'emitIdentityUpdated() transaction failed') + expectedEvents += 1 + + // Let's see what the cache knows about now + const finalEvents = await eventCacheMemory.getPastEvents('IdentityUpdated') + + assert( + finalEvents.length == expectedEvents, + `Expected ${expectedEvents} event, got ${finalEvents.length}` + ) + assert(finalEvents[0].returnValues.account === alice, 'Unexpected account in #0 event') + assert(finalEvents[0].returnValues.ipfsHash === testObjectHash, 'Unexpected hash in #0 event') + assert(finalEvents[1].returnValues.account === alice, 'Unexpected account in #1 event') + assert(finalEvents[1].returnValues.ipfsHash === secondObjectHash, 'Unexpected hash in #1 event') + assert(finalEvents[2].returnValues.account === bob, 'Unexpected account in #2 event') + assert(finalEvents[2].returnValues.ipfsHash === thirdObjectHash, 'Unexpected hash in #2 event') + + }) + + it('should patch web3 Contract', async () => { + const memoryBackend = new InMemoryBackend() + + assert( + !Marketplace.hasOwnProperty('eventCache'), + 'Marketplace should not have eventsCache prop yet' + ) + + const NewMarketplace = patchWeb3Contract(Marketplace, 0, { + backend: memoryBackend + }) + + assert( + Marketplace.hasOwnProperty('eventCache'), + 'Marketplace should have eventsCache prop after patch' + ) + + assert( + NewMarketplace.hasOwnProperty('eventCache'), + 'NewMarketplace should have eventsCache prop after patch' + ) + }) + + it('should be able to fetch events with array parameters', async () => { + const indexedBackend = new InMemoryBackend() + const eventCache = new EventCache(IdentityEvents, 0, { + backend: indexedBackend + }) + + const tx = { + gas: STD_GAS, + gasPrice: STD_GAS_PRICE, + from: charlie + } + + const firstObjectHash = await ipfs.addObject({ one: 1 }) + const firstReceipt = await IdentityEvents.methods.emitIdentityUpdated(firstObjectHash).send(tx) + assert(firstReceipt.status == 1, 'emitIdentityUpdated() transaction failed') + + const secondObjectHash = await ipfs.addObject({ two: 2 }) + const secondReceipt = await IdentityEvents.methods.emitIdentityUpdated(secondObjectHash).send( + Object.assign({}, tx, { from: denise }) + ) + assert(secondReceipt.status == 1, 'emitIdentityUpdated() transaction failed') + + const orEvents = await eventCache.getPastEvents('IdentityUpdated', { + filter: { account: [charlie, denise] } + }) + + assert( + orEvents.length == 2, + `Request should have returned 2 events, got ${orEvents.length}` + ) + const possibleAccounts = [charlie, denise] + assert(possibleAccounts.indexOf(orEvents[0].returnValues.account) > -1, 'Unexpected account') + assert(possibleAccounts.indexOf(orEvents[1].returnValues.account) > -1, 'Unexpected account') + }) + +}) diff --git a/packages/event-cache/test/indexeddb.js b/packages/event-cache/test/indexeddb.js new file mode 100644 index 000000000000..557d44557daa --- /dev/null +++ b/packages/event-cache/test/indexeddb.js @@ -0,0 +1,142 @@ +const assert = require('assert') + +const { IndexedDBBackend } = require('../src/backends') +const { EventCache } = require('../src') + +const contracts = require('./_contracts') +const ipfs = require('./_ipfs') +const { STD_GAS, STD_GAS_DEPLOY, STD_GAS_PRICE } = require('./const') + +describe('IndexedDB', function() { + let IdentityEvents + + let owner, + alice, + bob, + charlie, + denise + + before(async function() { + const accounts = await contracts.web3.eth.getAccounts() + owner = accounts[0] + alice = accounts[1] + bob = accounts[2] + charlie = accounts[3] + denise = accounts[4] + + IdentityEvents = await contracts.identity.deploy().send({ + from: owner, + gas: STD_GAS_DEPLOY, + gasPrice: STD_GAS_PRICE + }) + + }) + + it('should work with IndexedDB', async () => { + const indexedBackend = new IndexedDBBackend({ testing: true }) + const idxDBCache = new EventCache(IdentityEvents, 0, { + backend: indexedBackend + }) + assert( + idxDBCache.backend.type === 'indexeddb', + `Expected 'indexeddb' backend, got '${idxDBCache.backend.type}'` + ) + }) + + it('should be able to fetch events', async () => { + const indexedBackend = new IndexedDBBackend({ testing: true }) + const idxDBCache = new EventCache(IdentityEvents, 0, { + backend: indexedBackend + }) + + let expectedEvents = 0 + const tx = { + gas: STD_GAS, + gasPrice: STD_GAS_PRICE, + from: alice + } + + // First, lets generate an event + const testObjectHash = await ipfs.addObject({ one: 1 }) + const receipt = await IdentityEvents.methods.emitIdentityUpdated(testObjectHash).send(tx) + assert(receipt.status == 1, 'emitIdentityUpdated() transaction failed') + expectedEvents += 1 + + const initialEvents = await idxDBCache.getPastEvents('IdentityUpdated', { + filter: { account: alice } + }) + + assert( + initialEvents.length == expectedEvents, + `Expected ${expectedEvents} event, got ${initialEvents.length}` + ) + assert(initialEvents[0].returnValues.account === alice, 'Unexpected account in event') + assert(initialEvents[0].returnValues.ipfsHash === testObjectHash, 'Unexpected hash in event') + + // Add more events! + const secondObjectHash = await ipfs.addObject({ two: 2 }) + const secondReceipt = await IdentityEvents.methods.emitIdentityUpdated(secondObjectHash).send(tx) + assert(secondReceipt.status == 1, 'emitIdentityUpdated() transaction failed') + expectedEvents += 1 + + const thirdObjectHash = await ipfs.addObject({ two: 2 }) + const thirdReceipt = await IdentityEvents.methods.emitIdentityUpdated(thirdObjectHash).send( + Object.assign({}, tx, { + from: bob + }) + ) + assert(thirdReceipt.status == 1, 'emitIdentityUpdated() transaction failed') + expectedEvents += 1 + + // Now we should see 2 from alice and one from bob + const aliceEvents = await idxDBCache.getPastEvents('IdentityUpdated', { + filter: { account: alice } + }) + assert( + aliceEvents.length == 2, + `Alice should have 2 events, got ${aliceEvents.length}` + ) + const bobEvents = await idxDBCache.getPastEvents('IdentityUpdated', { + filter: { account: bob } + }) + assert( + bobEvents.length == 1, + `Bob should have 1 event, got ${bobEvents.length}` + ) + }) + + it('should be able to fetch events with array parameters', async () => { + const indexedBackend = new IndexedDBBackend({ testing: true }) + const eventCache = new EventCache(IdentityEvents, 0, { + backend: indexedBackend + }) + + const tx = { + gas: STD_GAS, + gasPrice: STD_GAS_PRICE, + from: charlie + } + + const firstObjectHash = await ipfs.addObject({ one: 1 }) + const firstReceipt = await IdentityEvents.methods.emitIdentityUpdated(firstObjectHash).send(tx) + assert(firstReceipt.status == 1, 'emitIdentityUpdated() transaction failed') + + const secondObjectHash = await ipfs.addObject({ two: 2 }) + const secondReceipt = await IdentityEvents.methods.emitIdentityUpdated(secondObjectHash).send( + Object.assign({}, tx, { from: denise }) + ) + assert(secondReceipt.status == 1, 'emitIdentityUpdated() transaction failed') + + const orEvents = await eventCache.getPastEvents('IdentityUpdated', { + filter: { account: [charlie, denise] } + }) + + assert( + orEvents.length == 2, + `Request should have returned 2 events, got ${orEvents.length}` + ) + const possibleAccounts = [charlie, denise] + assert(possibleAccounts.indexOf(orEvents[0].returnValues.account) > -1, 'Unexpected account') + assert(possibleAccounts.indexOf(orEvents[1].returnValues.account) > -1, 'Unexpected account') + }) +}) diff --git a/packages/event-cache/test/postgres.js b/packages/event-cache/test/postgres.js new file mode 100644 index 000000000000..dde82850e44f --- /dev/null +++ b/packages/event-cache/test/postgres.js @@ -0,0 +1,171 @@ +const assert = require('assert') + +const { PostgreSQLBackend } = require('../src/backends') +const { EventCache } = require('../src') + +const contracts = require('./_contracts') +const ipfs = require('./_ipfs') +const { STD_GAS, STD_GAS_DEPLOY, STD_GAS_PRICE } = require('./const') + +describe('PostgreSQL', function() { + let IdentityEvents + + let owner, + alice, + bob, + charlie, + denise, + elmer + + before(async function() { + const accounts = await contracts.web3.eth.getAccounts() + owner = accounts[0] + alice = accounts[1] + bob = accounts[2] + charlie = accounts[3] + denise = accounts[4] + elmer = accounts[5] + + IdentityEvents = await contracts.identity.deploy().send({ + from: owner, + gas: STD_GAS_DEPLOY, + gasPrice: STD_GAS_PRICE + }) + + }) + + it('should work with PostgreSQL', async () => { + const postgreBackend = new PostgreSQLBackend() + const pgCache = new EventCache(IdentityEvents, 0, { + backend: postgreBackend + }) + assert( + pgCache.backend.type === 'postgresql', + `Expected 'postgresql' backend, got '${pgCache.backend.type}'` + ) + }) + + it('should dump serialized data from PostgreSQL', async () => { + let expectedEvents = 0 + const postgresBackend = new PostgreSQLBackend() + const pgCache = new EventCache(IdentityEvents, 0, { + backend: postgresBackend + }) + assert( + pgCache.backend.type === 'postgresql', + `Expected 'postgresql' backend, got '${pgCache.backend.type}'` + ) + + const tx = { + gas: STD_GAS, + gasPrice: STD_GAS_PRICE, + from: alice + } + + // Generate an event + const testObjectHash = await ipfs.addObject({ one: 1 }) + const receipt = await IdentityEvents.methods.emitIdentityUpdated(testObjectHash).send(tx) + assert(receipt.status == 1, 'emitIdentityUpdated() transaction failed') + expectedEvents += 1 + + const initialEvents = await pgCache.getPastEvents('IdentityUpdated', { + filter: { account: alice } + }) + + assert( + initialEvents.length == expectedEvents, + `Expected ${expectedEvents} event, got ${initialEvents.length}` + ) + assert(initialEvents[0].returnValues.account === alice, 'Unexpected account in event') + assert(initialEvents[0].returnValues.ipfsHash === testObjectHash, 'Unexpected hash in event') + + const serialized = await postgresBackend.serialize() + assert(serialized.length > 0) + }) + + it('should be able to fetch events', async () => { + const postgresBackend = new PostgreSQLBackend() + const pgCache = new EventCache(IdentityEvents, 0, { + backend: postgresBackend + }) + + const tx = { + gas: STD_GAS, + gasPrice: STD_GAS_PRICE, + from: charlie + } + + // First, lets generate an event + const testObjectHash = await ipfs.addObject({ one: 1 }) + const receipt = await IdentityEvents.methods.emitIdentityUpdated(testObjectHash).send(tx) + assert(receipt.status == 1, 'emitIdentityUpdated() transaction failed') + + // Add more events! + const secondObjectHash = await ipfs.addObject({ two: 2 }) + const secondReceipt = await IdentityEvents.methods.emitIdentityUpdated(secondObjectHash).send(tx) + assert(secondReceipt.status == 1, 'emitIdentityUpdated() transaction failed') + + const thirdObjectHash = await ipfs.addObject({ two: 2 }) + const thirdReceipt = await IdentityEvents.methods.emitIdentityUpdated(thirdObjectHash).send( + Object.assign({}, tx, { + from: bob + }) + ) + assert(thirdReceipt.status == 1, 'emitIdentityUpdated() transaction failed') + + // Now we should see 2 from alice and one from bob + const charlieEvents = await pgCache.getPastEvents('IdentityUpdated', { + filter: { account: charlie } + }) + + assert( + charlieEvents.length == 2, + `Charlie should have 2 events, got ${charlieEvents.length}` + ) + const bobEvents = await pgCache.getPastEvents('IdentityUpdated', { + filter: { account: bob } + }) + assert( + bobEvents.length == 1, + `Bob should have 1 event, got ${bobEvents.length}` + ) + }) + + it('should be able to fetch events with array parameters', async () => { + const postgresBackend = new PostgreSQLBackend() + const pgCache = new EventCache(IdentityEvents, 0, { + backend: postgresBackend + }) + + let expectedEvents = 0 + const tx = { + gas: STD_GAS, + gasPrice: STD_GAS_PRICE, + from: denise + } + + const firstObjectHash = await ipfs.addObject({ one: 1 }) + const firstReceipt = await IdentityEvents.methods.emitIdentityUpdated(firstObjectHash).send(tx) + assert(firstReceipt.status == 1, 'emitIdentityUpdated() transaction failed') + expectedEvents += 1 + + const secondObjectHash = await ipfs.addObject({ two: 2 }) + const secondReceipt = await IdentityEvents.methods.emitIdentityUpdated(secondObjectHash).send( + Object.assign({}, tx, { from: elmer }) + ) + assert(secondReceipt.status == 1, 'emitIdentityUpdated() transaction failed') + expectedEvents += 1 + + const orEvents = await pgCache.getPastEvents('IdentityUpdated', { + filter: { account: [denise, elmer] } + }) + + assert( + orEvents.length == expectedEvents, + `Request should have returned 2 events, got ${orEvents.length}` + ) + const possibleAccounts = [denise, elmer] + assert(possibleAccounts.indexOf(orEvents[0].returnValues.account) > -1, 'Unexpected account') + assert(possibleAccounts.indexOf(orEvents[1].returnValues.account) > -1, 'Unexpected account') + }) +}) diff --git a/packages/event-cache/test/setup.js b/packages/event-cache/test/setup.js new file mode 100644 index 000000000000..2916d617bb66 --- /dev/null +++ b/packages/event-cache/test/setup.js @@ -0,0 +1,26 @@ +/** + * This file should be run before all other tests, which can be done by passing + * the --file option to mocha. It sets up and tears down the infrastructure + * (ethereum test node and IPFS) required to run tests. + */ + +import services from '@origin/services' + +const isWatchMode = process.argv.some(arg => arg === '-w' || arg === '--watch') +let shutdown + +before(async function() { + this.timeout(30000) + // Start Ganache (in-memory) and IPFS + shutdown = await services({ ganache: { inMemory: true, total_accounts: 20 }, ipfs: true, deployContracts: true }) +}) + +// Override exit code to prevent error when using Ctrl-c after `npm run test:watch` +if (isWatchMode) { + process.once('exit', () => process.exit(0)) +} else { + // Shutdown ganache etc if we're not in watch mode and tests are finished. + after(async function() { + await shutdown() + }) +} diff --git a/packages/event-cache/webpack.config.js b/packages/event-cache/webpack.config.js new file mode 100644 index 000000000000..3cce24fbc2d8 --- /dev/null +++ b/packages/event-cache/webpack.config.js @@ -0,0 +1,40 @@ +const path = require('path') +const webpack = require('webpack') + +const config = { + target: 'web', + entry: { + app: './src/index.js' + }, + devtool: false, + output: { + filename: '@origin/event-cache.js', + path: path.resolve(__dirname, 'dist') + }, + module: { + noParse: [/^react$/], + rules: [ + { test: /\.flow$/, loader: 'ignore-loader' }, + { + test: /\.js$/, + exclude: /node_modules/, + loader: 'babel-loader' + }, + { + test: /\.mjs$/, + include: /node_modules/, + type: 'javascript/auto' + } + ] + }, + resolve: { + extensions: ['.js', '.json'] + }, + node: { + fs: 'empty' + }, + mode: 'development', + plugins: [new webpack.EnvironmentPlugin({ HOST: 'localhost' })] +} + +module.exports = config diff --git a/packages/eventsource/src/index.js b/packages/eventsource/src/index.js index 5bdb174456e1..c3975d719acb 100644 --- a/packages/eventsource/src/index.js +++ b/packages/eventsource/src/index.js @@ -58,7 +58,7 @@ class OriginEventSource { async getListing(listingId, blockNumber) { const cacheBlockNumber = blockNumber ? blockNumber - : this.contract.eventCache.getBlockNumber() + : this.contract.eventCache.latestBlock const cacheKey = `${listingId}-${cacheBlockNumber}` const networkId = await this.getNetworkId() if (this.listingCache[cacheKey]) { @@ -84,8 +84,9 @@ class OriginEventSource { return null } - const events = await this.contract.eventCache.listings(listingId) - + const events = await this.contract.eventCache.getEvents({ + listingID: String(listingId) + }) events.forEach(e => { if (e.event === 'ListingCreated') { ipfsHash = e.returnValues.ipfsHash @@ -135,7 +136,6 @@ class OriginEventSource { if (data.unitsTotal < 0) { data.unitsTotal = 1 } - // TODO: Dapp1 fractional compat if (rawData.availability && !rawData.weekendPrice) { try { @@ -384,10 +384,12 @@ class OriginEventSource { } let latestBlock, status, ipfsHash, lastEvent, withdrawnBy, createdBlock - const events = await this.contract.eventCache.offers( - listingId, - Number(offerId) - ) + + const events = await this.contract.eventCache.getEvents({ + listingID: String(listingId), + offerID: String(offerId) + }) + events.forEach(e => { if (e.event === 'OfferCreated') { ipfsHash = e.returnValues.ipfsHash diff --git a/packages/graphql/package.json b/packages/graphql/package.json index 605d0f125eb6..507aa0698f2f 100644 --- a/packages/graphql/package.json +++ b/packages/graphql/package.json @@ -26,6 +26,7 @@ "@babel/register": "^7.0.0", "@origin/contracts": "^0.8.6", "@origin/eventsource": "^0.1.0", + "@origin/event-cache": "^0.1.0", "@origin/ipfs": "^0.1.0", "@origin/mobile-bridge": "^0.1.0", "@origin/messaging-client": "^0.1.0", diff --git a/packages/graphql/src/configs/mainnet.js b/packages/graphql/src/configs/mainnet.js index 489082cafb6a..649d14725541 100644 --- a/packages/graphql/src/configs/mainnet.js +++ b/packages/graphql/src/configs/mainnet.js @@ -10,12 +10,22 @@ export default { bridge: 'https://bridge.originprotocol.com', IdentityEvents: '0x8ac16c08105de55a02e2b7462b1eec6085fa4d86', IdentityEvents_Epoch: '7046530', - IdentityEvents_EventCache: 'QmYu5bTLHYnFMCxgnWd6ywfasQQCeKbkzrU2UJAedycKQL', + IdentityEvents_EventCache: [ + 'QmRHLLD86gf7YVUqWSpvsjZNndK8U3n6EMBowLgmYpZRgk', + 'QmPoeDDD8HNptqmzrAHcHc9UTh62aAjJbJ6Nqej9pAyZtH', + 'Qmdm23cmBMNv1hz7gYnmEyuVYsHJVRXQm2YfB4oM86cAwe' + ], attestationIssuer: '0x8EAbA82d8D1046E4F242D4501aeBB1a6d4b5C4Aa', OriginToken: '0x8207c1ffc5b6804f6024322ccf34f29c3541ae26', V00_Marketplace: '0x819bb9964b6ebf52361f1ae42cf4831b921510f9', V00_Marketplace_Epoch: '6436157', - ipfsEventCache: 'QmWyqzZMoQB1zzxJyCAhTZ5XenzX5H8sfE3Uh58uEN3MJh', + V00_Marketplace_EventCache: [ + 'QmbViWEBRQmjmxC5VyELJDeW1auSxgRgjWpw5djcCctPMC', + 'QmaMSwML8H1ZREYFKGcdXXRP96JbGA4EShY9DcNmCH5FKD', + 'QmTKCVgs9xuzqH3D86eDF7G2kxmyW1LLmAv4BN3p1evCgD', + 'QmRhtc4tksc9BAwjzw2GJ1D9Hvf1KXyYcEtoEUSeqBjXCX', + 'QmY6noUzQEo9SUy4Mk1RpshHb7Qwn9sZqwa5PvxAYZyJpz' + ], messagingAccount: '0xBfDd843382B36FFbAcd00b190de6Cb85ff840118', messaging: { messagingNamespace: 'origin', diff --git a/packages/graphql/src/configs/rinkeby.js b/packages/graphql/src/configs/rinkeby.js index 45996ddaee57..5ac29796c83c 100644 --- a/packages/graphql/src/configs/rinkeby.js +++ b/packages/graphql/src/configs/rinkeby.js @@ -8,12 +8,16 @@ export default { discovery: 'https://discovery.staging.originprotocol.com', growth: 'https://growth.staging.originprotocol.com', bridge: 'https://bridge.staging.originprotocol.com', - IdentityEvents: '0x160455a06d8e5aa38862afc34e4eca0566ee4e7e', - IdentityEvents_Epoch: '3670528', OriginToken: '0xa115e16ef6e217f7a327a57031f75ce0487aadb8', V00_Marketplace: '0xe842831533c4bf4b0f71b4521c4320bdb669324e', V00_Marketplace_Epoch: '3086315', - ipfsEventCache: 'QmYqzB3WE4YzyxD9ptQnG6UURw1CR1hj1siqVry4Da2GLx', + V00_Marketplace_EventCache: [ + 'QmTu6ykq6sTKJVFFZftRacQY8XbVb1WCA2HWGjqDAVf9bq', + 'QmUWH95cRDFv98ojZNeL5ZHma1VSv2FZqGAGVig6171qzL' + ], + IdentityEvents: '0x160455a06d8e5aa38862afc34e4eca0566ee4e7e', + IdentityEvents_Epoch: '3670528', + IdentityEvents_EventCache: ['QmfJVGUnDrkVB75RVx4zmq9q2h3UhV3ySLCc9EKVixXsBT'], affiliate: '0xc1a33cda27c68e47e370ff31cdad7d6522ea93d5', arbitrator: '0xc9c1a92ba54c61045ebf566b154dfd6afedea992', messaging: { diff --git a/packages/graphql/src/contracts.js b/packages/graphql/src/contracts.js index 5d5fad1c14d4..2b8c83c2b2b4 100644 --- a/packages/graphql/src/contracts.js +++ b/packages/graphql/src/contracts.js @@ -7,9 +7,8 @@ import { exchangeAbi, factoryAbi } from './contracts/UniswapExchange' import Web3 from 'web3' import EventSource from '@origin/eventsource' +import { patchWeb3Contract } from '@origin/event-cache' -import eventCache from './utils/eventCache' -import genericEventCache from './utils/genericEventCache' import pubsub from './utils/pubsub' import currencies from './utils/currencies' @@ -50,8 +49,8 @@ export function newBlock(blockHeaders) { if (!blockHeaders) return if (blockHeaders.number <= lastBlock) return lastBlock = blockHeaders.number - context.marketplace.eventCache.updateBlock(blockHeaders.number) - context.identityEvents.eventCache.updateBlock(blockHeaders.number) + context.marketplace.eventCache.setLatestBlock(lastBlock) + context.identityEvents.eventCache.setLatestBlock(lastBlock) context.eventSource.resetCache() pubsub.publish('NEW_BLOCK', { newBlock: { ...blockHeaders, id: blockHeaders.hash } @@ -353,12 +352,17 @@ export function toggleMetaMask(enabled) { export function setMarketplace(address, epoch) { context.marketplace = new web3.eth.Contract(MarketplaceContract.abi, address) - context.marketplace.eventCache = eventCache( - context.marketplace, - epoch, - context.web3, - context.config - ) + patchWeb3Contract(context.marketplace, epoch, { + ...context.config, + useLatestFromChain: false, + ipfsEventCache: context.config.V00_Marketplace_EventCache, + prefix: + typeof address === 'undefined' + ? 'Marketplace_' + : `${address.slice(2, 8)}_`, + platform: typeof window === 'undefined' ? 'memory' : 'browser' + }) + if (address) { context.marketplaces = [context.marketplace] } else { @@ -387,13 +391,17 @@ export function setIdentityEvents(address, epoch) { IdentityEventsContract.abi, address ) - context.identityEvents.eventCache = genericEventCache( - context.identityEvents, - epoch, - context.web3, - context.config, - context.config.IdentityEvents_EventCache - ) + patchWeb3Contract(context.identityEvents, epoch, { + ...context.config, + ipfsEventCache: context.config.IdentityEvents_EventCache, + useLatestFromChain: false, + prefix: + typeof address === 'undefined' + ? 'IdentityEvents_' + : `${address.slice(2, 8)}_`, + platform: typeof window === 'undefined' ? 'memory' : 'browser', + batchSize: 2500 + }) context.identityEventsExec = context.identityEvents if (metaMask) { @@ -424,6 +432,7 @@ if (isBrowser) { metaMask = applyWeb3Hack(new Web3(window.web3.currentProvider)) metaMaskEnabled = window.localStorage.metaMaskEnabled ? true : false } + setNetwork(window.localStorage.ognNetwork || 'mainnet') } diff --git a/packages/graphql/src/resolvers/IdentityEvents.js b/packages/graphql/src/resolvers/IdentityEvents.js index 60236c6b9462..bbfd26ea5292 100644 --- a/packages/graphql/src/resolvers/IdentityEvents.js +++ b/packages/graphql/src/resolvers/IdentityEvents.js @@ -64,10 +64,9 @@ export function identity({ id, ipfsHash }) { return null } if (!ipfsHash) { - const events = await contracts.identityEvents.eventCache.allEvents( - undefined, - [null, contracts.web3.utils.padLeft(id.toLowerCase(), 64)] - ) + const events = await contracts.identityEvents.eventCache.getEvents({ + account: id + }) events.forEach(event => { if (event.event === 'IdentityUpdated') { ipfsHash = event.returnValues.ipfsHash diff --git a/packages/graphql/src/resolvers/Listing.js b/packages/graphql/src/resolvers/Listing.js index b0472a8f1ba8..2128df07d751 100644 --- a/packages/graphql/src/resolvers/Listing.js +++ b/packages/graphql/src/resolvers/Listing.js @@ -11,11 +11,15 @@ export default { }, events: async listing => { const { listingId } = parseId(listing.id) - return await listing.contract.eventCache.listings(listingId) + return await listing.contract.eventCache.getEvents({ + listingID: String(listingId) + }) }, totalEvents: async listing => { const { listingId } = parseId(listing.id) - return (await listing.contract.eventCache.listings(listingId)).length + return (await listing.contract.eventCache.getEvents({ + listingID: String(listingId) + })).length }, totalOffers: listing => { const { listingId } = parseId(listing.id) @@ -28,10 +32,10 @@ export default { offers: async listing => listing.allOffers.filter(o => o.valid), createdEvent: async listing => { const { listingId } = parseId(listing.id) - const events = await listing.contract.eventCache.listings( - listingId, - 'ListingCreated' - ) + const events = await listing.contract.eventCache.getEvents({ + listingID: String(listingId), + event: 'ListingCreated' + }) return events[0] }, featured: async listing => { diff --git a/packages/graphql/src/resolvers/Offer.js b/packages/graphql/src/resolvers/Offer.js index 5ba9c41ba10f..31758d760640 100644 --- a/packages/graphql/src/resolvers/Offer.js +++ b/packages/graphql/src/resolvers/Offer.js @@ -7,11 +7,11 @@ import { getIpfsHashFromBytes32 } from '@origin/ipfs' const _firstEventByType = async (offer, eventType) => { const { listingId, offerId } = parseId(offer.id) - const events = await offer.contract.eventCache.offers( - listingId, - offerId, - eventType - ) + const events = await offer.contract.eventCache.getEvents({ + listingID: String(listingId), + offerID: String(offerId), + event: eventType + }) return events[0] } @@ -23,12 +23,18 @@ export default { events: async offer => { const { listingId, offerId } = parseId(offer.id) - return await offer.contract.eventCache.offers(listingId, offerId) + return await offer.contract.eventCache.getEvents({ + listingID: String(listingId), + offerID: offerId + }) }, history: async offer => { const { listingId, offerId } = parseId(offer.id) - const events = await offer.contract.eventCache.offers(listingId, offerId) + const events = await offer.contract.eventCache.getEvents({ + listingID: String(listingId), + offerID: offerId + }) return events.map(event => { const ipfsHash = getIpfsHashFromBytes32(event.returnValues.ipfsHash) return { diff --git a/packages/graphql/src/resolvers/User.js b/packages/graphql/src/resolvers/User.js index ed5a7c750915..b9c1b5321569 100644 --- a/packages/graphql/src/resolvers/User.js +++ b/packages/graphql/src/resolvers/User.js @@ -28,30 +28,34 @@ async function resultsFromIds({ after, allIds, first, fields }) { async function offers(buyer, { first = 10, after, filter }, _, info) { const fields = graphqlFields(info) - const events = await ec().allEvents('OfferCreated', buyer.id) - - let allIds = events - .map(e => `${e.returnValues.listingID}-${e.returnValues.offerID}`) - .reverse() - - if (filter) { - const completedEvents = await ec().allEvents( - ['OfferFinalized', 'OfferWithdrawn', 'OfferRuling'], - undefined, - allIds - ) - const completedIds = uniq( - completedEvents.map( - e => `${e.returnValues.listingID}-${e.returnValues.offerID}` - ) - ) - - if (filter === 'complete') { - allIds = allIds.filter(id => completedIds.indexOf(id) >= 0) - } else if (filter === 'pending') { - allIds = allIds.filter(id => completedIds.indexOf(id) < 0) - } + const offerEvents = await ec().getEvents({ + event: 'OfferCreated', + party: buyer.id + }) + const offerIDs = offerEvents.map(e => e.returnValues.offerID) + const completedOfferEvents = await ec().getEvents({ + event: ['OfferFinalized', 'OfferWithdrawn', 'OfferRuling'], + offerID: offerIDs.map(id => String(id)) + }) + const completedIds = completedOfferEvents.map(ev => { + return `${ev.returnValues.listingID}-${ev.returnValues.offerID}` + }) + + let filteredEvents = offerEvents + if (filter === 'complete') { + filteredEvents = offerEvents.filter(ev => { + const id = `${ev.returnValues.listingID}-${ev.returnValues.offerID}` + return completedIds.indexOf(id) > -1 + }) + } else if (filter === 'pending') { + filteredEvents = offerEvents.filter(ev => { + const id = `${ev.returnValues.listingID}-${ev.returnValues.offerID}` + return completedIds.indexOf(id) < 0 + }) } + const allIds = filteredEvents + .map(ev => `${ev.returnValues.listingID}-${ev.returnValues.offerID}`) + .reverse() return await resultsFromIds({ after, allIds, first, fields }) } @@ -59,20 +63,25 @@ async function offers(buyer, { first = 10, after, filter }, _, info) { async function sales(seller, { first = 10, after, filter }, _, info) { const fields = graphqlFields(info) - const listings = await ec().allEvents('ListingCreated', seller.id) - const listingIds = listings.map(e => Number(e.returnValues.listingID)) - const events = await ec().offers(listingIds, null, 'OfferCreated') + const listings = await ec().getEvents({ + event: 'ListingCreated', + party: seller.id + }) + const listingIds = listings.map(e => e.returnValues.listingID) + const events = await ec().getEvents({ + listingID: listingIds, + event: 'OfferCreated' + }) let allIds = events .map(e => `${e.returnValues.listingID}-${e.returnValues.offerID}`) .reverse() if (filter) { - const completedEvents = await ec().allEvents( - ['OfferFinalized', 'OfferWithdrawn', 'OfferRuling'], - undefined, - allIds - ) + const completedEvents = await ec().getEvents({ + event: ['OfferFinalized', 'OfferWithdrawn', 'OfferRuling'], + offerID: events.map(e => e.returnValues.offerID) + }) const completedIds = uniq( completedEvents.map( e => `${e.returnValues.listingID}-${e.returnValues.offerID}` @@ -90,9 +99,15 @@ async function sales(seller, { first = 10, after, filter }, _, info) { } async function reviews(user) { - const listings = await ec().allEvents('ListingCreated', user.id) - const listingIds = listings.map(e => Number(e.returnValues.listingID)) - const events = await ec().offers(listingIds, null, 'OfferFinalized') + const listings = await ec().getEvents({ + event: 'ListingCreated', + party: user.id + }) + const listingIds = listings.map(e => String(e.returnValues.listingID)) + const events = await ec().getEvents({ + listingID: listingIds, + event: 'OfferFinalized' + }) let nodes = await Promise.all( events.map(event => @@ -124,16 +139,15 @@ async function reviews(user) { async function notifications(user, { first = 10, after, filter }, _, info) { const fields = graphqlFields(info) - const sellerListings = await ec().allEvents('ListingCreated', user.id) + const sellerListings = await ec().getEvents({ + party: user.id, + event: 'ListingCreated' + }) - const sellerListingIds = sellerListings.map(e => - Number(e.returnValues.listingID) - ) + const sellerListingIds = sellerListings.map(e => e.returnValues.listingID) - const sellerEvents = await ec().offers( - sellerListingIds, - null, - [ + const unfilteredSellerEvents = await ec().getEvents({ + event: [ 'OfferCreated', 'OfferFinalized', 'OfferWithdrawn', @@ -141,20 +155,25 @@ async function notifications(user, { first = 10, after, filter }, _, info) { 'OfferDisputed', 'OfferRuling' ], - user.id + listingID: sellerListingIds + }) + const sellerEvents = unfilteredSellerEvents.filter( + e => e.returnValues.party !== user.id ) - const buyerListings = await ec().allEvents('OfferCreated', user.id) + const buyerListings = await ec().getEvents({ + event: 'OfferCreated', + party: user.id + }) - const buyerListingIds = buyerListings.map(e => - Number(e.returnValues.listingID) - ) + const buyerListingIds = buyerListings.map(e => e.returnValues.listingID) - const buyerEvents = await ec().offers( - buyerListingIds, - null, - ['OfferAccepted', 'OfferRuling'], - user.id + const unfilteredBuyerEvents = await ec().getEvents({ + listingID: buyerListingIds, + event: ['OfferAccepted', 'OfferRuling'] + }) + const buyerEvents = unfilteredBuyerEvents.filter( + e => e.returnValues.party !== user.id ) let allEvents = sortBy([...sellerEvents, ...buyerEvents], e => -e.blockNumber) diff --git a/packages/graphql/src/resolvers/marketplace/listings.js b/packages/graphql/src/resolvers/marketplace/listings.js index be6b92e38d8c..8fe1adc5dba5 100644 --- a/packages/graphql/src/resolvers/marketplace/listings.js +++ b/packages/graphql/src/resolvers/marketplace/listings.js @@ -104,11 +104,10 @@ export async function listingsBySeller( info ) { const fields = graphqlFields(info) - const events = await contracts.marketplace.eventCache.allEvents( - 'ListingCreated', - listingSeller.id - ) - + const events = await contracts.marketplace.eventCache.getEvents({ + event: 'ListingCreated', + party: listingSeller.id + }) const ids = events.map(e => Number(e.returnValues.listingID)).reverse() const totalCount = ids.length diff --git a/packages/graphql/test/_helpers.js b/packages/graphql/test/_helpers.js index 82445229ac2a..086f68e026f1 100644 --- a/packages/graphql/test/_helpers.js +++ b/packages/graphql/test/_helpers.js @@ -12,7 +12,8 @@ export async function mutate(mutation, variables, getEvents) { const result = await client.mutate({ mutation, variables }) const blockNumber = await contracts.web3.eth.getBlockNumber() - contracts.marketplace.eventCache.updateBlock(blockNumber) + contracts.marketplace.eventCache.setLatestBlock(blockNumber) + contracts.eventSource.resetCache() // Assume mutation returns an object with a transaction hash in the format // data.mutationName.id diff --git a/packages/graphql/test/index.js b/packages/graphql/test/index.js index af8670747ab9..95194d790e56 100644 --- a/packages/graphql/test/index.js +++ b/packages/graphql/test/index.js @@ -400,6 +400,7 @@ describe('Marketplace', function() { assert(events.OfferCreated) const offer = await getOffer('999-000-2', 0) + assert(offer.id === '999-000-2-0') assert(offer.status === 1) assert(offer.commission === '2000000000000000000') }) @@ -422,6 +423,7 @@ describe('Marketplace', function() { assert(events.OfferCreated) const offer = await getOffer('999-000-2', 1) + assert.strictEqual(offer.id, '999-000-2-1') assert.strictEqual(offer.status, 1) assert.strictEqual(offer.commission, '1000000000000000000') }) @@ -572,7 +574,6 @@ describe('Marketplace', function() { query: queries.GetListing, variables: { id: '999-000-2' } }) - const listing = get(res, 'data.marketplace.listing', {}) assert.strictEqual(listing.unitsPending, 0) assert.strictEqual(listing.unitsSold, 1)