From 45e4fca67024fe048829ecd6abc805adc9e843ac Mon Sep 17 00:00:00 2001 From: Prathamesh Musale Date: Wed, 12 Jun 2024 06:35:56 +0000 Subject: [PATCH] Generate watcher for sushiswap blocks subgraph (#3) Part of [Generate watchers for sushiswap subgraphs deployed in graph-node](https://www.notion.so/Generate-watchers-for-sushiswap-subgraphs-deployed-in-graph-node-b3f2e475373d4ab1887d9f8720bd5ae6) Co-authored-by: neeraj Reviewed-on: https://git.vdb.to/cerc-io/sushiswap-watcher-ts/pulls/3 Co-authored-by: Prathamesh Musale Co-committed-by: Prathamesh Musale --- packages/blocks-watcher/.eslintignore | 2 + packages/blocks-watcher/.eslintrc.json | 28 + packages/blocks-watcher/.gitignore | 8 + packages/blocks-watcher/.husky/pre-commit | 4 + packages/blocks-watcher/.npmrc | 1 + packages/blocks-watcher/LICENSE | 661 +++++++++++++++++ packages/blocks-watcher/README.md | 217 ++++++ .../blocks-watcher/environments/local.toml | 111 +++ packages/blocks-watcher/package.json | 77 ++ .../src/artifacts/UniswapV2Factory.json | 217 ++++++ .../src/cli/checkpoint-cmds/create.ts | 44 ++ .../src/cli/checkpoint-cmds/verify.ts | 40 + packages/blocks-watcher/src/cli/checkpoint.ts | 39 + .../blocks-watcher/src/cli/export-state.ts | 38 + .../blocks-watcher/src/cli/import-state.ts | 39 + .../blocks-watcher/src/cli/index-block.ts | 38 + .../blocks-watcher/src/cli/inspect-cid.ts | 38 + .../src/cli/reset-cmds/job-queue.ts | 22 + .../src/cli/reset-cmds/state.ts | 24 + .../src/cli/reset-cmds/watcher.ts | 37 + packages/blocks-watcher/src/cli/reset.ts | 24 + .../blocks-watcher/src/cli/watch-contract.ts | 38 + packages/blocks-watcher/src/client.ts | 55 ++ packages/blocks-watcher/src/database.ts | 285 ++++++++ packages/blocks-watcher/src/entity/Block.ts | 64 ++ .../src/entity/BlockProgress.ts | 48 ++ .../blocks-watcher/src/entity/Contract.ts | 27 + packages/blocks-watcher/src/entity/Event.ts | 38 + .../blocks-watcher/src/entity/FrothyEntity.ts | 21 + packages/blocks-watcher/src/entity/State.ts | 31 + .../src/entity/StateSyncStatus.ts | 17 + .../blocks-watcher/src/entity/Subscriber.ts | 21 + .../blocks-watcher/src/entity/SyncStatus.ts | 45 ++ packages/blocks-watcher/src/fill.ts | 48 ++ packages/blocks-watcher/src/gql/index.ts | 3 + .../blocks-watcher/src/gql/mutations/index.ts | 4 + .../src/gql/mutations/watchContract.gql | 3 + .../blocks-watcher/src/gql/queries/_meta.gql | 11 + .../blocks-watcher/src/gql/queries/block.gql | 18 + .../blocks-watcher/src/gql/queries/blocks.gql | 18 + .../blocks-watcher/src/gql/queries/events.gql | 30 + .../src/gql/queries/eventsInRange.gql | 30 + .../src/gql/queries/getState.gql | 15 + .../src/gql/queries/getStateByCID.gql | 15 + .../src/gql/queries/getSyncStatus.gql | 12 + .../blocks-watcher/src/gql/queries/index.ts | 11 + .../src/gql/subscriptions/index.ts | 4 + .../src/gql/subscriptions/onEvent.gql | 30 + packages/blocks-watcher/src/hooks.ts | 86 +++ packages/blocks-watcher/src/indexer.ts | 683 ++++++++++++++++++ packages/blocks-watcher/src/job-runner.ts | 48 ++ packages/blocks-watcher/src/resolvers.ts | 288 ++++++++ packages/blocks-watcher/src/schema.gql | 337 +++++++++ packages/blocks-watcher/src/server.ts | 43 ++ packages/blocks-watcher/src/types.ts | 3 + .../UniswapV2Factory/UniswapV2Factory.wasm | Bin 0 -> 186771 bytes .../abis/UniswapV2Factory.json | 215 ++++++ .../subgraph-build/schema.graphql | 16 + .../subgraph-build/subgraph.yaml | 25 + packages/blocks-watcher/tsconfig.json | 74 ++ yarn.lock | 78 ++ 61 files changed, 4547 insertions(+) create mode 100644 packages/blocks-watcher/.eslintignore create mode 100644 packages/blocks-watcher/.eslintrc.json create mode 100644 packages/blocks-watcher/.gitignore create mode 100755 packages/blocks-watcher/.husky/pre-commit create mode 100644 packages/blocks-watcher/.npmrc create mode 100644 packages/blocks-watcher/LICENSE create mode 100644 packages/blocks-watcher/README.md create mode 100644 packages/blocks-watcher/environments/local.toml create mode 100644 packages/blocks-watcher/package.json create mode 100644 packages/blocks-watcher/src/artifacts/UniswapV2Factory.json create mode 100644 packages/blocks-watcher/src/cli/checkpoint-cmds/create.ts create mode 100644 packages/blocks-watcher/src/cli/checkpoint-cmds/verify.ts create mode 100644 packages/blocks-watcher/src/cli/checkpoint.ts create mode 100644 packages/blocks-watcher/src/cli/export-state.ts create mode 100644 packages/blocks-watcher/src/cli/import-state.ts create mode 100644 packages/blocks-watcher/src/cli/index-block.ts create mode 100644 packages/blocks-watcher/src/cli/inspect-cid.ts create mode 100644 packages/blocks-watcher/src/cli/reset-cmds/job-queue.ts create mode 100644 packages/blocks-watcher/src/cli/reset-cmds/state.ts create mode 100644 packages/blocks-watcher/src/cli/reset-cmds/watcher.ts create mode 100644 packages/blocks-watcher/src/cli/reset.ts create mode 100644 packages/blocks-watcher/src/cli/watch-contract.ts create mode 100644 packages/blocks-watcher/src/client.ts create mode 100644 packages/blocks-watcher/src/database.ts create mode 100644 packages/blocks-watcher/src/entity/Block.ts create mode 100644 packages/blocks-watcher/src/entity/BlockProgress.ts create mode 100644 packages/blocks-watcher/src/entity/Contract.ts create mode 100644 packages/blocks-watcher/src/entity/Event.ts create mode 100644 packages/blocks-watcher/src/entity/FrothyEntity.ts create mode 100644 packages/blocks-watcher/src/entity/State.ts create mode 100644 packages/blocks-watcher/src/entity/StateSyncStatus.ts create mode 100644 packages/blocks-watcher/src/entity/Subscriber.ts create mode 100644 packages/blocks-watcher/src/entity/SyncStatus.ts create mode 100644 packages/blocks-watcher/src/fill.ts create mode 100644 packages/blocks-watcher/src/gql/index.ts create mode 100644 packages/blocks-watcher/src/gql/mutations/index.ts create mode 100644 packages/blocks-watcher/src/gql/mutations/watchContract.gql create mode 100644 packages/blocks-watcher/src/gql/queries/_meta.gql create mode 100644 packages/blocks-watcher/src/gql/queries/block.gql create mode 100644 packages/blocks-watcher/src/gql/queries/blocks.gql create mode 100644 packages/blocks-watcher/src/gql/queries/events.gql create mode 100644 packages/blocks-watcher/src/gql/queries/eventsInRange.gql create mode 100644 packages/blocks-watcher/src/gql/queries/getState.gql create mode 100644 packages/blocks-watcher/src/gql/queries/getStateByCID.gql create mode 100644 packages/blocks-watcher/src/gql/queries/getSyncStatus.gql create mode 100644 packages/blocks-watcher/src/gql/queries/index.ts create mode 100644 packages/blocks-watcher/src/gql/subscriptions/index.ts create mode 100644 packages/blocks-watcher/src/gql/subscriptions/onEvent.gql create mode 100644 packages/blocks-watcher/src/hooks.ts create mode 100644 packages/blocks-watcher/src/indexer.ts create mode 100644 packages/blocks-watcher/src/job-runner.ts create mode 100644 packages/blocks-watcher/src/resolvers.ts create mode 100644 packages/blocks-watcher/src/schema.gql create mode 100644 packages/blocks-watcher/src/server.ts create mode 100644 packages/blocks-watcher/src/types.ts create mode 100644 packages/blocks-watcher/subgraph-build/UniswapV2Factory/UniswapV2Factory.wasm create mode 100644 packages/blocks-watcher/subgraph-build/UniswapV2Factory/abis/UniswapV2Factory.json create mode 100644 packages/blocks-watcher/subgraph-build/schema.graphql create mode 100644 packages/blocks-watcher/subgraph-build/subgraph.yaml create mode 100644 packages/blocks-watcher/tsconfig.json diff --git a/packages/blocks-watcher/.eslintignore b/packages/blocks-watcher/.eslintignore new file mode 100644 index 0000000..55cb522 --- /dev/null +++ b/packages/blocks-watcher/.eslintignore @@ -0,0 +1,2 @@ +# Don't lint build output. +dist diff --git a/packages/blocks-watcher/.eslintrc.json b/packages/blocks-watcher/.eslintrc.json new file mode 100644 index 0000000..a2b842c --- /dev/null +++ b/packages/blocks-watcher/.eslintrc.json @@ -0,0 +1,28 @@ +{ + "env": { + "browser": true, + "es2021": true + }, + "extends": [ + "semistandard", + "plugin:@typescript-eslint/recommended" + ], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": 12, + "sourceType": "module" + }, + "plugins": [ + "@typescript-eslint" + ], + "rules": { + "indent": ["error", 2, { "SwitchCase": 1 }], + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/explicit-module-boundary-types": [ + "warn", + { + "allowArgumentsExplicitlyTypedAsAny": true + } + ] + } +} diff --git a/packages/blocks-watcher/.gitignore b/packages/blocks-watcher/.gitignore new file mode 100644 index 0000000..549d70b --- /dev/null +++ b/packages/blocks-watcher/.gitignore @@ -0,0 +1,8 @@ +node_modules/ +dist/ +out/ + +.vscode +.idea + +gql-logs/ diff --git a/packages/blocks-watcher/.husky/pre-commit b/packages/blocks-watcher/.husky/pre-commit new file mode 100755 index 0000000..9dcd433 --- /dev/null +++ b/packages/blocks-watcher/.husky/pre-commit @@ -0,0 +1,4 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +yarn lint diff --git a/packages/blocks-watcher/.npmrc b/packages/blocks-watcher/.npmrc new file mode 100644 index 0000000..6b64c5b --- /dev/null +++ b/packages/blocks-watcher/.npmrc @@ -0,0 +1 @@ +@cerc-io:registry=https://git.vdb.to/api/packages/cerc-io/npm/ diff --git a/packages/blocks-watcher/LICENSE b/packages/blocks-watcher/LICENSE new file mode 100644 index 0000000..331f7cf --- /dev/null +++ b/packages/blocks-watcher/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for + software and other kinds of works, specifically designed to ensure + cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed + to take away your freedom to share and change the works. By contrast, + our General Public Licenses are intended to guarantee your freedom to + share and change all versions of a program--to make sure it remains free + software for all its users. + + When we speak of free software, we are referring to freedom, not + price. Our General Public Licenses are designed to make sure that you + have the freedom to distribute copies of free software (and charge for + them if you wish), that you receive source code or can get it if you + want it, that you can change the software or use pieces of it in new + free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights + with two steps: (1) assert copyright on the software, and (2) offer + you this License which gives you legal permission to copy, distribute + and/or modify the software. + + A secondary benefit of defending all users' freedom is that + improvements made in alternate versions of the program, if they + receive widespread use, become available for other developers to + incorporate. Many developers of free software are heartened and + encouraged by the resulting cooperation. However, in the case of + software used on network servers, this result may fail to come about. + The GNU General Public License permits making a modified version and + letting the public access it on a server without ever releasing its + source code to the public. + + The GNU Affero General Public License is designed specifically to + ensure that, in such cases, the modified source code becomes available + to the community. It requires the operator of a network server to + provide the source code of the modified version running there to the + users of that server. Therefore, public use of a modified version, on + a publicly accessible server, gives the public access to the source + code of the modified version. + + An older license, called the Affero General Public License and + published by Affero, was designed to accomplish similar goals. This is + a different license, not a version of the Affero GPL, but Affero has + released a new version of the Affero GPL which permits relicensing under + this license. + + The precise terms and conditions for copying, distribution and + modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of + works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this + License. Each licensee is addressed as "you". "Licensees" and + "recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work + in a fashion requiring copyright permission, other than the making of an + exact copy. The resulting work is called a "modified version" of the + earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based + on the Program. + + To "propagate" a work means to do anything with it that, without + permission, would make you directly or secondarily liable for + infringement under applicable copyright law, except executing it on a + computer or modifying a private copy. Propagation includes copying, + distribution (with or without modification), making available to the + public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other + parties to make or receive copies. Mere interaction with a user through + a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" + to the extent that it includes a convenient and prominently visible + feature that (1) displays an appropriate copyright notice, and (2) + tells the user that there is no warranty for the work (except to the + extent that warranties are provided), that licensees may convey the + work under this License, and how to view a copy of this License. If + the interface presents a list of user commands or options, such as a + menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work + for making modifications to it. "Object code" means any non-source + form of a work. + + A "Standard Interface" means an interface that either is an official + standard defined by a recognized standards body, or, in the case of + interfaces specified for a particular programming language, one that + is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other + than the work as a whole, that (a) is included in the normal form of + packaging a Major Component, but which is not part of that Major + Component, and (b) serves only to enable use of the work with that + Major Component, or to implement a Standard Interface for which an + implementation is available to the public in source code form. A + "Major Component", in this context, means a major essential component + (kernel, window system, and so on) of the specific operating system + (if any) on which the executable work runs, or a compiler used to + produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all + the source code needed to generate, install, and (for an executable + work) run the object code and to modify the work, including scripts to + control those activities. However, it does not include the work's + System Libraries, or general-purpose tools or generally available free + programs which are used unmodified in performing those activities but + which are not part of the work. For example, Corresponding Source + includes interface definition files associated with source files for + the work, and the source code for shared libraries and dynamically + linked subprograms that the work is specifically designed to require, + such as by intimate data communication or control flow between those + subprograms and other parts of the work. + + The Corresponding Source need not include anything that users + can regenerate automatically from other parts of the Corresponding + Source. + + The Corresponding Source for a work in source code form is that + same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of + copyright on the Program, and are irrevocable provided the stated + conditions are met. This License explicitly affirms your unlimited + permission to run the unmodified Program. The output from running a + covered work is covered by this License only if the output, given its + content, constitutes a covered work. This License acknowledges your + rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not + convey, without conditions so long as your license otherwise remains + in force. You may convey covered works to others for the sole purpose + of having them make modifications exclusively for you, or provide you + with facilities for running those works, provided that you comply with + the terms of this License in conveying all material for which you do + not control copyright. Those thus making or running the covered works + for you must do so exclusively on your behalf, under your direction + and control, on terms that prohibit them from making any copies of + your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under + the conditions stated below. Sublicensing is not allowed; section 10 + makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological + measure under any applicable law fulfilling obligations under article + 11 of the WIPO copyright treaty adopted on 20 December 1996, or + similar laws prohibiting or restricting circumvention of such + measures. + + When you convey a covered work, you waive any legal power to forbid + circumvention of technological measures to the extent such circumvention + is effected by exercising rights under this License with respect to + the covered work, and you disclaim any intention to limit operation or + modification of the work as a means of enforcing, against the work's + users, your or third parties' legal rights to forbid circumvention of + technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you + receive it, in any medium, provided that you conspicuously and + appropriately publish on each copy an appropriate copyright notice; + keep intact all notices stating that this License and any + non-permissive terms added in accord with section 7 apply to the code; + keep intact all notices of the absence of any warranty; and give all + recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, + and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to + produce it from the Program, in the form of source code under the + terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent + works, which are not by their nature extensions of the covered work, + and which are not combined with it such as to form a larger program, + in or on a volume of a storage or distribution medium, is called an + "aggregate" if the compilation and its resulting copyright are not + used to limit the access or legal rights of the compilation's users + beyond what the individual works permit. Inclusion of a covered work + in an aggregate does not cause this License to apply to the other + parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms + of sections 4 and 5, provided that you also convey the + machine-readable Corresponding Source under the terms of this License, + in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded + from the Corresponding Source as a System Library, need not be + included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any + tangible personal property which is normally used for personal, family, + or household purposes, or (2) anything designed or sold for incorporation + into a dwelling. In determining whether a product is a consumer product, + doubtful cases shall be resolved in favor of coverage. For a particular + product received by a particular user, "normally used" refers to a + typical or common use of that class of product, regardless of the status + of the particular user or of the way in which the particular user + actually uses, or expects or is expected to use, the product. A product + is a consumer product regardless of whether the product has substantial + commercial, industrial or non-consumer uses, unless such uses represent + the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, + procedures, authorization keys, or other information required to install + and execute modified versions of a covered work in that User Product from + a modified version of its Corresponding Source. The information must + suffice to ensure that the continued functioning of the modified object + code is in no case prevented or interfered with solely because + modification has been made. + + If you convey an object code work under this section in, or with, or + specifically for use in, a User Product, and the conveying occurs as + part of a transaction in which the right of possession and use of the + User Product is transferred to the recipient in perpetuity or for a + fixed term (regardless of how the transaction is characterized), the + Corresponding Source conveyed under this section must be accompanied + by the Installation Information. But this requirement does not apply + if neither you nor any third party retains the ability to install + modified object code on the User Product (for example, the work has + been installed in ROM). + + The requirement to provide Installation Information does not include a + requirement to continue to provide support service, warranty, or updates + for a work that has been modified or installed by the recipient, or for + the User Product in which it has been modified or installed. Access to a + network may be denied when the modification itself materially and + adversely affects the operation of the network or violates the rules and + protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, + in accord with this section must be in a format that is publicly + documented (and with an implementation available to the public in + source code form), and must require no special password or key for + unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this + License by making exceptions from one or more of its conditions. + Additional permissions that are applicable to the entire Program shall + be treated as though they were included in this License, to the extent + that they are valid under applicable law. If additional permissions + apply only to part of the Program, that part may be used separately + under those permissions, but the entire Program remains governed by + this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option + remove any additional permissions from that copy, or from any part of + it. (Additional permissions may be written to require their own + removal in certain cases when you modify the work.) You may place + additional permissions on material, added by you to a covered work, + for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you + add to a covered work, you may (if authorized by the copyright holders of + that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further + restrictions" within the meaning of section 10. If the Program as you + received it, or any part of it, contains a notice stating that it is + governed by this License along with a term that is a further + restriction, you may remove that term. If a license document contains + a further restriction but permits relicensing or conveying under this + License, you may add to a covered work material governed by the terms + of that license document, provided that the further restriction does + not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you + must place, in the relevant source files, a statement of the + additional terms that apply to those files, or a notice indicating + where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the + form of a separately written license, or stated as exceptions; + the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly + provided under this License. Any attempt otherwise to propagate or + modify it is void, and will automatically terminate your rights under + this License (including any patent licenses granted under the third + paragraph of section 11). + + However, if you cease all violation of this License, then your + license from a particular copyright holder is reinstated (a) + provisionally, unless and until the copyright holder explicitly and + finally terminates your license, and (b) permanently, if the copyright + holder fails to notify you of the violation by some reasonable means + prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is + reinstated permanently if the copyright holder notifies you of the + violation by some reasonable means, this is the first time you have + received notice of violation of this License (for any work) from that + copyright holder, and you cure the violation prior to 30 days after + your receipt of the notice. + + Termination of your rights under this section does not terminate the + licenses of parties who have received copies or rights from you under + this License. If your rights have been terminated and not permanently + reinstated, you do not qualify to receive new licenses for the same + material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or + run a copy of the Program. Ancillary propagation of a covered work + occurring solely as a consequence of using peer-to-peer transmission + to receive a copy likewise does not require acceptance. However, + nothing other than this License grants you permission to propagate or + modify any covered work. These actions infringe copyright if you do + not accept this License. Therefore, by modifying or propagating a + covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically + receives a license from the original licensors, to run, modify and + propagate that work, subject to this License. You are not responsible + for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an + organization, or substantially all assets of one, or subdividing an + organization, or merging organizations. If propagation of a covered + work results from an entity transaction, each party to that + transaction who receives a copy of the work also receives whatever + licenses to the work the party's predecessor in interest had or could + give under the previous paragraph, plus a right to possession of the + Corresponding Source of the work from the predecessor in interest, if + the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the + rights granted or affirmed under this License. For example, you may + not impose a license fee, royalty, or other charge for exercise of + rights granted under this License, and you may not initiate litigation + (including a cross-claim or counterclaim in a lawsuit) alleging that + any patent claim is infringed by making, using, selling, offering for + sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this + License of the Program or a work on which the Program is based. The + work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims + owned or controlled by the contributor, whether already acquired or + hereafter acquired, that would be infringed by some manner, permitted + by this License, of making, using, or selling its contributor version, + but do not include claims that would be infringed only as a + consequence of further modification of the contributor version. For + purposes of this definition, "control" includes the right to grant + patent sublicenses in a manner consistent with the requirements of + this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free + patent license under the contributor's essential patent claims, to + make, use, sell, offer for sale, import and otherwise run, modify and + propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express + agreement or commitment, however denominated, not to enforce a patent + (such as an express permission to practice a patent or covenant not to + sue for patent infringement). To "grant" such a patent license to a + party means to make such an agreement or commitment not to enforce a + patent against the party. + + If you convey a covered work, knowingly relying on a patent license, + and the Corresponding Source of the work is not available for anyone + to copy, free of charge and under the terms of this License, through a + publicly available network server or other readily accessible means, + then you must either (1) cause the Corresponding Source to be so + available, or (2) arrange to deprive yourself of the benefit of the + patent license for this particular work, or (3) arrange, in a manner + consistent with the requirements of this License, to extend the patent + license to downstream recipients. "Knowingly relying" means you have + actual knowledge that, but for the patent license, your conveying the + covered work in a country, or your recipient's use of the covered work + in a country, would infringe one or more identifiable patents in that + country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or + arrangement, you convey, or propagate by procuring conveyance of, a + covered work, and grant a patent license to some of the parties + receiving the covered work authorizing them to use, propagate, modify + or convey a specific copy of the covered work, then the patent license + you grant is automatically extended to all recipients of the covered + work and works based on it. + + A patent license is "discriminatory" if it does not include within + the scope of its coverage, prohibits the exercise of, or is + conditioned on the non-exercise of one or more of the rights that are + specifically granted under this License. You may not convey a covered + work if you are a party to an arrangement with a third party that is + in the business of distributing software, under which you make payment + to the third party based on the extent of your activity of conveying + the work, and under which the third party grants, to any of the + parties who would receive the covered work from you, a discriminatory + patent license (a) in connection with copies of the covered work + conveyed by you (or copies made from those copies), or (b) primarily + for and in connection with specific products or compilations that + contain the covered work, unless you entered into that arrangement, + or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting + any implied license or other defenses to infringement that may + otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or + otherwise) that contradict the conditions of this License, they do not + excuse you from the conditions of this License. If you cannot convey a + covered work so as to satisfy simultaneously your obligations under this + License and any other pertinent obligations, then as a consequence you may + not convey it at all. For example, if you agree to terms that obligate you + to collect a royalty for further conveying from those to whom you convey + the Program, the only way you could satisfy both those terms and this + License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the + Program, your modified version must prominently offer all users + interacting with it remotely through a computer network (if your version + supports such interaction) an opportunity to receive the Corresponding + Source of your version by providing access to the Corresponding Source + from a network server at no charge, through some standard or customary + means of facilitating copying of software. This Corresponding Source + shall include the Corresponding Source for any work covered by version 3 + of the GNU General Public License that is incorporated pursuant to the + following paragraph. + + Notwithstanding any other provision of this License, you have + permission to link or combine any covered work with a work licensed + under version 3 of the GNU General Public License into a single + combined work, and to convey the resulting work. The terms of this + License will continue to apply to the part which is the covered work, + but the work with which it is combined will remain governed by version + 3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of + the GNU Affero General Public License from time to time. Such new versions + will be similar in spirit to the present version, but may differ in detail to + address new problems or concerns. + + Each version is given a distinguishing version number. If the + Program specifies that a certain numbered version of the GNU Affero General + Public License "or any later version" applies to it, you have the + option of following the terms and conditions either of that numbered + version or of any later version published by the Free Software + Foundation. If the Program does not specify a version number of the + GNU Affero General Public License, you may choose any version ever published + by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future + versions of the GNU Affero General Public License can be used, that proxy's + public statement of acceptance of a version permanently authorizes you + to choose that version for the Program. + + Later license versions may give you additional or different + permissions. However, no additional obligations are imposed on any + author or copyright holder as a result of your choosing to follow a + later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY + APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT + HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY + OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, + THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM + IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF + ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING + WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS + THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY + GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE + USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF + DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD + PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), + EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF + SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided + above cannot be given local legal effect according to their terms, + reviewing courts shall apply local law that most closely approximates + an absolute waiver of all civil liability in connection with the + Program, unless a warranty or assumption of liability accompanies a + copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest + possible use to the public, the best way to achieve this is to make it + free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest + to attach them to the start of each source file to most effectively + state the exclusion of warranty; and each file should have at least + the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + + Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer + network, you should also make sure that it provides a way for users to + get its source. For example, if your program is a web application, its + interface could display a "Source" link that leads users to an archive + of the code. There are many ways you could offer source, and different + solutions will be better for different programs; see section 13 for the + specific requirements. + + You should also get your employer (if you work as a programmer) or school, + if any, to sign a "copyright disclaimer" for the program, if necessary. + For more information on this, and how to apply and follow the GNU AGPL, see + . diff --git a/packages/blocks-watcher/README.md b/packages/blocks-watcher/README.md new file mode 100644 index 0000000..095ecb1 --- /dev/null +++ b/packages/blocks-watcher/README.md @@ -0,0 +1,217 @@ +# blocks-watcher + +## Source + +* Subgraph: [sushiswap-subgraphs v0.1.0-watcher-0.1.0](https://github.com/cerc-io/sushiswap-subgraphs/releases/tag/v0.1.0-watcher-0.1.0) + * Package: [subgraphs/blocks](https://github.com/cerc-io/sushiswap-subgraphs/tree/v0.1.0-watcher-0.1.0/subgraphs/blocks) + +## Setup + +* Run the following command to install required packages: + + ```bash + yarn + ``` + +* Create a postgres12 database for the watcher: + + ```bash + sudo su - postgres + createdb blocks-watcher + ``` + +* If the watcher is an `active` watcher: + + Create database for the job queue and enable the `pgcrypto` extension on them (https://github.com/timgit/pg-boss/blob/master/docs/usage.md#intro): + + ``` + createdb blocks-watcher-job-queue + ``` + + ``` + postgres@tesla:~$ psql -U postgres -h localhost blocks-watcher-job-queue + Password for user postgres: + psql (12.7 (Ubuntu 12.7-1.pgdg18.04+1)) + SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, bits: 256, compression: off) + Type "help" for help. + + blocks-watcher-job-queue=# CREATE EXTENSION pgcrypto; + CREATE EXTENSION + blocks-watcher-job-queue=# exit + ``` + +* In the [config file](./environments/local.toml): + + * Update the database connection settings. + + * Update the `upstream` config and provide the `ipld-eth-server` GQL API endpoint. + + * Update the `server` config with state checkpoint settings. + +## Customize + +* Indexing on an event: + + * Edit the custom hook function `handleEvent` (triggered on an event) in [hooks.ts](./src/hooks.ts) to perform corresponding indexing using the `Indexer` object. + + * While using the indexer storage methods for indexing, pass `diff` as true if default state is desired to be generated using the state variables being indexed. + +* Generating state: + + * Edit the custom hook function `createInitialState` (triggered if the watcher passes the start block, checkpoint: `true`) in [hooks.ts](./src/hooks.ts) to save an initial `State` using the `Indexer` object. + + * Edit the custom hook function `createStateDiff` (triggered on a block) in [hooks.ts](./src/hooks.ts) to save the state in a `diff` `State` using the `Indexer` object. The default state (if exists) is updated. + + * Edit the custom hook function `createStateCheckpoint` (triggered just before default and CLI checkpoint) in [hooks.ts](./src/hooks.ts) to save the state in a `checkpoint` `State` using the `Indexer` object. + +### GQL Caching + +To enable GQL requests caching: + +* Update the `server.gql.cache` config with required settings. + +* In the GQL [schema file](./src/schema.gql), use the `cacheControl` directive to apply cache hints at schema level. + + * Eg. Set `inheritMaxAge` to true for non-scalar fields of a type. + +* In the GQL [resolvers file](./src/resolvers.ts), uncomment the `setGQLCacheHints()` calls in resolvers for required queries. + +## Run + +* If the watcher is a `lazy` watcher: + + * Run the server: + + ```bash + yarn server + ``` + + GQL console: http://localhost:3008/graphql + +* If the watcher is an `active` watcher: + + * Run the job-runner: + + ```bash + yarn job-runner + ``` + + * Run the server: + + ```bash + yarn server + ``` + + GQL console: http://localhost:3008/graphql + + * To watch a contract: + + ```bash + yarn watch:contract --address --kind --checkpoint --starting-block [block-number] + ``` + + * `address`: Address or identifier of the contract to be watched. + * `kind`: Kind of the contract. + * `checkpoint`: Turn checkpointing on (`true` | `false`). + * `starting-block`: Starting block for the contract (default: `1`). + + Examples: + + Watch a contract with its address and checkpointing on: + + ```bash + yarn watch:contract --address 0x1F78641644feB8b64642e833cE4AFE93DD6e7833 --kind ERC20 --checkpoint true + ``` + + Watch a contract with its identifier and checkpointing on: + + ```bash + yarn watch:contract --address MyProtocol --kind protocol --checkpoint true + ``` + + * To fill a block range: + + ```bash + yarn fill --start-block --end-block + ``` + + * `start-block`: Block number to start filling from. + * `end-block`: Block number till which to fill. + + * To create a checkpoint for a contract: + + ```bash + yarn checkpoint create --address --block-hash [block-hash] + ``` + + * `address`: Address or identifier of the contract for which to create a checkpoint. + * `block-hash`: Hash of a block (in the pruned region) at which to create the checkpoint (default: latest canonical block hash). + + * To verify a checkpoint: + + ```bash + yarn checkpoint verify --cid + ``` + + `cid`: CID of the checkpoint for which to verify. + + * To reset the watcher to a previous block number: + + * Reset watcher: + + ```bash + yarn reset watcher --block-number + ``` + + * Reset job-queue: + + ```bash + yarn reset job-queue + ``` + + * Reset state: + + ```bash + yarn reset state --block-number + ``` + + * `block-number`: Block number to which to reset the watcher. + + * To export and import the watcher state: + + * In source watcher, export watcher state: + + ```bash + yarn export-state --export-file [export-file-path] --block-number [snapshot-block-height] + ``` + + * `export-file`: Path of file to which to export the watcher data. + * `block-number`: Block height at which to take snapshot for export. + + * In target watcher, run job-runner: + + ```bash + yarn job-runner + ``` + + * Import watcher state: + + ```bash + yarn import-state --import-file + ``` + + * `import-file`: Path of file from which to import the watcher data. + + * Run server: + + ```bash + yarn server + ``` + + * To inspect a CID: + + ```bash + yarn inspect-cid --cid + ``` + + * `cid`: CID to be inspected. diff --git a/packages/blocks-watcher/environments/local.toml b/packages/blocks-watcher/environments/local.toml new file mode 100644 index 0000000..0dfb296 --- /dev/null +++ b/packages/blocks-watcher/environments/local.toml @@ -0,0 +1,111 @@ +[server] + host = "127.0.0.1" + port = 3008 + kind = "active" + + # Checkpointing state. + checkpointing = true + + # Checkpoint interval in number of blocks. + checkpointInterval = 2000 + + # Enable state creation + # CAUTION: Disable only if state creation is not desired or can be filled subsequently + enableState = true + + subgraphPath = "./subgraph-build" + + # Interval to restart wasm instance periodically + wasmRestartBlocksInterval = 20 + + # Interval in number of blocks at which to clear entities cache. + clearEntitiesCacheInterval = 1000 + + # Flag to specify whether RPC endpoint supports block hash as block tag parameter + rpcSupportsBlockHashParam = true + + # Server GQL config + [server.gql] + path = "/graphql" + + # Max block range for which to return events in eventsInRange GQL query. + # Use -1 for skipping check on block range. + maxEventsBlockRange = 1000 + + # Log directory for GQL requests + logDir = "./gql-logs" + + # GQL cache settings + [server.gql.cache] + enabled = true + + # Max in-memory cache size (in bytes) (default 8 MB) + # maxCacheSize + + # GQL cache-control max-age settings (in seconds) + maxAge = 15 + timeTravelMaxAge = 86400 # 1 day + +[metrics] + host = "127.0.0.1" + port = 9000 + [metrics.gql] + port = 9001 + +[database] + type = "postgres" + host = "localhost" + port = 5432 + database = "blocks-watcher" + username = "postgres" + password = "postgres" + synchronize = true + logging = false + +[upstream] + [upstream.ethServer] + rpcProviderEndpoints = [ + "http://127.0.0.1:8081" + ] + + # Boolean flag to specify if rpc-eth-client should be used for RPC endpoint instead of ipld-eth-client (ipld-eth-server GQL client) + rpcClient = true + + # Boolean flag to specify if rpcProviderEndpoint is an FEVM RPC endpoint + isFEVM = true + + # Boolean flag to filter event logs by contracts + filterLogsByAddresses = true + # Boolean flag to filter event logs by topics + filterLogsByTopics = true + + [upstream.cache] + name = "requests" + enabled = false + deleteOnStart = false + +[jobQueue] + dbConnectionString = "postgres://postgres:postgres@localhost/blocks-watcher-job-queue" + maxCompletionLagInSecs = 300 + jobDelayInMilliSecs = 100 + eventsInBatch = 50 + subgraphEventsOrder = true + blockDelayInMilliSecs = 2000 + + # Number of blocks by which block processing lags behind head + blockProcessingOffset = 16 + + # Boolean to switch between modes of processing events when starting the server. + # Setting to true will fetch filtered events and required blocks in a range of blocks and then process them. + # Setting to false will fetch blocks consecutively with its events and then process them (Behaviour is followed in realtime processing near head). + useBlockRanges = true + + # Block range in which logs are fetched during historical blocks processing + historicalLogsBlockRange = 2000 + + # Max block range of historical processing after which it waits for completion of events processing + # If set to -1 historical processing does not wait for events processing and completes till latest canonical block + historicalMaxFetchAhead = 10000 + + # Max number of retries to fetch new block after which watcher will failover to other RPC endpoints + maxNewBlockRetries = 3 diff --git a/packages/blocks-watcher/package.json b/packages/blocks-watcher/package.json new file mode 100644 index 0000000..677d6e0 --- /dev/null +++ b/packages/blocks-watcher/package.json @@ -0,0 +1,77 @@ +{ + "name": "@cerc-io/blocks-watcher", + "version": "0.1.0", + "description": "blocks-watcher", + "private": true, + "main": "dist/index.js", + "scripts": { + "lint": "eslint --max-warnings=0 .", + "build": "yarn clean && tsc && yarn copy-assets", + "clean": "rm -rf ./dist", + "prepare": "husky install", + "copy-assets": "copyfiles -u 1 src/**/*.gql dist/", + "server": "DEBUG=vulcanize:* YARN_CHILD_PROCESS=true node --enable-source-maps dist/server.js", + "server:dev": "DEBUG=vulcanize:* YARN_CHILD_PROCESS=true ts-node src/server.ts", + "job-runner": "DEBUG=vulcanize:* YARN_CHILD_PROCESS=true node --enable-source-maps dist/job-runner.js", + "job-runner:dev": "DEBUG=vulcanize:* YARN_CHILD_PROCESS=true ts-node src/job-runner.ts", + "watch:contract": "DEBUG=vulcanize:* ts-node src/cli/watch-contract.ts", + "fill": "DEBUG=vulcanize:* ts-node src/fill.ts", + "fill:state": "DEBUG=vulcanize:* ts-node src/fill.ts --state", + "reset": "DEBUG=vulcanize:* ts-node src/cli/reset.ts", + "checkpoint": "DEBUG=vulcanize:* node --enable-source-maps dist/cli/checkpoint.js", + "checkpoint:dev": "DEBUG=vulcanize:* ts-node src/cli/checkpoint.ts", + "export-state": "DEBUG=vulcanize:* node --enable-source-maps dist/cli/export-state.js", + "export-state:dev": "DEBUG=vulcanize:* ts-node src/cli/export-state.ts", + "import-state": "DEBUG=vulcanize:* node --enable-source-maps dist/cli/import-state.js", + "import-state:dev": "DEBUG=vulcanize:* ts-node src/cli/import-state.ts", + "inspect-cid": "DEBUG=vulcanize:* ts-node src/cli/inspect-cid.ts", + "index-block": "DEBUG=vulcanize:* ts-node src/cli/index-block.ts" + }, + "repository": { + "type": "git", + "url": "https://github.com/cerc-io/watcher-ts.git" + }, + "author": "", + "license": "AGPL-3.0", + "bugs": { + "url": "https://github.com/cerc-io/watcher-ts/issues" + }, + "homepage": "https://github.com/cerc-io/watcher-ts#readme", + "dependencies": { + "@apollo/client": "^3.3.19", + "@cerc-io/cli": "^0.2.97", + "@cerc-io/ipld-eth-client": "^0.2.97", + "@cerc-io/solidity-mapper": "^0.2.97", + "@cerc-io/util": "^0.2.97", + "@cerc-io/graph-node": "^0.2.97", + "@ethersproject/providers": "^5.4.4", + "debug": "^4.3.1", + "decimal.js": "^10.3.1", + "ethers": "^5.4.4", + "graphql": "^15.5.0", + "json-bigint": "^1.0.0", + "reflect-metadata": "^0.1.13", + "typeorm": "0.2.37", + "yargs": "^17.0.1" + }, + "devDependencies": { + "@ethersproject/abi": "^5.3.0", + "@types/debug": "^4.1.5", + "@types/json-bigint": "^1.0.0", + "@types/yargs": "^17.0.0", + "@typescript-eslint/eslint-plugin": "^5.47.1", + "@typescript-eslint/parser": "^5.47.1", + "copyfiles": "^2.4.1", + "eslint": "^8.35.0", + "eslint-config-semistandard": "^15.0.1", + "eslint-config-standard": "^16.0.3", + "eslint-plugin-import": "^2.27.5", + "eslint-plugin-node": "^11.1.0", + "eslint-plugin-promise": "^5.1.0", + "eslint-plugin-standard": "^5.0.0", + "husky": "^7.0.2", + "ts-node": "^10.2.1", + "typescript": "^5.0.2", + "winston": "^3.13.0" + } +} diff --git a/packages/blocks-watcher/src/artifacts/UniswapV2Factory.json b/packages/blocks-watcher/src/artifacts/UniswapV2Factory.json new file mode 100644 index 0000000..244820a --- /dev/null +++ b/packages/blocks-watcher/src/artifacts/UniswapV2Factory.json @@ -0,0 +1,217 @@ +{ + "abi": [ + { + "inputs": [ + { + "internalType": "address", + "name": "_feeToSetter", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "token0", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "token1", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "pair", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "PairCreated", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "allPairs", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "allPairsLength", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "tokenA", + "type": "address" + }, + { + "internalType": "address", + "name": "tokenB", + "type": "address" + } + ], + "name": "createPair", + "outputs": [ + { + "internalType": "address", + "name": "pair", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "feeTo", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "feeToSetter", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "getPair", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "migrator", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pairCodeHash", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_feeTo", + "type": "address" + } + ], + "name": "setFeeTo", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_feeToSetter", + "type": "address" + } + ], + "name": "setFeeToSetter", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_migrator", + "type": "address" + } + ], + "name": "setMigrator", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } + ] +} \ No newline at end of file diff --git a/packages/blocks-watcher/src/cli/checkpoint-cmds/create.ts b/packages/blocks-watcher/src/cli/checkpoint-cmds/create.ts new file mode 100644 index 0000000..e771c70 --- /dev/null +++ b/packages/blocks-watcher/src/cli/checkpoint-cmds/create.ts @@ -0,0 +1,44 @@ +// +// Copyright 2022 Vulcanize, Inc. +// + +import { CreateCheckpointCmd } from '@cerc-io/cli'; +import { getGraphDbAndWatcher } from '@cerc-io/graph-node'; + +import { Database, ENTITY_QUERY_TYPE_MAP, ENTITY_TO_LATEST_ENTITY_MAP } from '../../database'; +import { Indexer } from '../../indexer'; + +export const command = 'create'; + +export const desc = 'Create checkpoint'; + +export const builder = { + address: { + type: 'string', + require: true, + demandOption: true, + describe: 'Contract address to create the checkpoint for.' + }, + blockHash: { + type: 'string', + describe: 'Blockhash at which to create the checkpoint.' + } +}; + +export const handler = async (argv: any): Promise => { + const createCheckpointCmd = new CreateCheckpointCmd(); + await createCheckpointCmd.init(argv, Database); + + const { graphWatcher } = await getGraphDbAndWatcher( + createCheckpointCmd.config.server, + createCheckpointCmd.clients.ethClient, + createCheckpointCmd.ethProvider, + createCheckpointCmd.database.baseDatabase, + ENTITY_QUERY_TYPE_MAP, + ENTITY_TO_LATEST_ENTITY_MAP + ); + + await createCheckpointCmd.initIndexer(Indexer, graphWatcher); + + await createCheckpointCmd.exec(); +}; diff --git a/packages/blocks-watcher/src/cli/checkpoint-cmds/verify.ts b/packages/blocks-watcher/src/cli/checkpoint-cmds/verify.ts new file mode 100644 index 0000000..3709f54 --- /dev/null +++ b/packages/blocks-watcher/src/cli/checkpoint-cmds/verify.ts @@ -0,0 +1,40 @@ +// +// Copyright 2022 Vulcanize, Inc. +// + +import { VerifyCheckpointCmd } from '@cerc-io/cli'; +import { getGraphDbAndWatcher } from '@cerc-io/graph-node'; + +import { Database, ENTITY_QUERY_TYPE_MAP, ENTITY_TO_LATEST_ENTITY_MAP } from '../../database'; +import { Indexer } from '../../indexer'; + +export const command = 'verify'; + +export const desc = 'Verify checkpoint'; + +export const builder = { + cid: { + type: 'string', + alias: 'c', + demandOption: true, + describe: 'Checkpoint CID to be verified' + } +}; + +export const handler = async (argv: any): Promise => { + const verifyCheckpointCmd = new VerifyCheckpointCmd(); + await verifyCheckpointCmd.init(argv, Database); + + const { graphWatcher, graphDb } = await getGraphDbAndWatcher( + verifyCheckpointCmd.config.server, + verifyCheckpointCmd.clients.ethClient, + verifyCheckpointCmd.ethProvider, + verifyCheckpointCmd.database.baseDatabase, + ENTITY_QUERY_TYPE_MAP, + ENTITY_TO_LATEST_ENTITY_MAP + ); + + await verifyCheckpointCmd.initIndexer(Indexer, graphWatcher); + + await verifyCheckpointCmd.exec(graphDb); +}; diff --git a/packages/blocks-watcher/src/cli/checkpoint.ts b/packages/blocks-watcher/src/cli/checkpoint.ts new file mode 100644 index 0000000..d05ad8a --- /dev/null +++ b/packages/blocks-watcher/src/cli/checkpoint.ts @@ -0,0 +1,39 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import yargs from 'yargs'; +import 'reflect-metadata'; +import debug from 'debug'; + +import { DEFAULT_CONFIG_PATH } from '@cerc-io/util'; + +import { hideBin } from 'yargs/helpers'; + +const log = debug('vulcanize:checkpoint'); + +const main = async () => { + return yargs(hideBin(process.argv)) + .parserConfiguration({ + 'parse-numbers': false + }).options({ + configFile: { + alias: 'f', + type: 'string', + require: true, + demandOption: true, + describe: 'configuration file path (toml)', + default: DEFAULT_CONFIG_PATH + } + }) + .commandDir('checkpoint-cmds', { extensions: ['ts', 'js'], exclude: /([a-zA-Z0-9\s_\\.\-:])+(.d.ts)$/ }) + .demandCommand(1) + .help() + .argv; +}; + +main().then(() => { + process.exit(); +}).catch(err => { + log(err); +}); diff --git a/packages/blocks-watcher/src/cli/export-state.ts b/packages/blocks-watcher/src/cli/export-state.ts new file mode 100644 index 0000000..bcd1c8a --- /dev/null +++ b/packages/blocks-watcher/src/cli/export-state.ts @@ -0,0 +1,38 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import 'reflect-metadata'; +import debug from 'debug'; + +import { ExportStateCmd } from '@cerc-io/cli'; +import { getGraphDbAndWatcher } from '@cerc-io/graph-node'; + +import { Database, ENTITY_QUERY_TYPE_MAP, ENTITY_TO_LATEST_ENTITY_MAP } from '../database'; +import { Indexer } from '../indexer'; + +const log = debug('vulcanize:export-state'); + +const main = async (): Promise => { + const exportStateCmd = new ExportStateCmd(); + await exportStateCmd.init(Database); + + const { graphWatcher } = await getGraphDbAndWatcher( + exportStateCmd.config.server, + exportStateCmd.clients.ethClient, + exportStateCmd.ethProvider, + exportStateCmd.database.baseDatabase, + ENTITY_QUERY_TYPE_MAP, + ENTITY_TO_LATEST_ENTITY_MAP + ); + + await exportStateCmd.initIndexer(Indexer, graphWatcher); + + await exportStateCmd.exec(); +}; + +main().catch(err => { + log(err); +}).finally(() => { + process.exit(0); +}); diff --git a/packages/blocks-watcher/src/cli/import-state.ts b/packages/blocks-watcher/src/cli/import-state.ts new file mode 100644 index 0000000..04ce0e8 --- /dev/null +++ b/packages/blocks-watcher/src/cli/import-state.ts @@ -0,0 +1,39 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import 'reflect-metadata'; +import debug from 'debug'; + +import { ImportStateCmd } from '@cerc-io/cli'; +import { getGraphDbAndWatcher } from '@cerc-io/graph-node'; + +import { Database, ENTITY_QUERY_TYPE_MAP, ENTITY_TO_LATEST_ENTITY_MAP } from '../database'; +import { Indexer } from '../indexer'; +import { State } from '../entity/State'; + +const log = debug('vulcanize:import-state'); + +export const main = async (): Promise => { + const importStateCmd = new ImportStateCmd(); + await importStateCmd.init(Database); + + const { graphWatcher, graphDb } = await getGraphDbAndWatcher( + importStateCmd.config.server, + importStateCmd.clients.ethClient, + importStateCmd.ethProvider, + importStateCmd.database.baseDatabase, + ENTITY_QUERY_TYPE_MAP, + ENTITY_TO_LATEST_ENTITY_MAP + ); + + await importStateCmd.initIndexer(Indexer, graphWatcher); + + await importStateCmd.exec(State, graphDb); +}; + +main().catch(err => { + log(err); +}).finally(() => { + process.exit(0); +}); diff --git a/packages/blocks-watcher/src/cli/index-block.ts b/packages/blocks-watcher/src/cli/index-block.ts new file mode 100644 index 0000000..19a302a --- /dev/null +++ b/packages/blocks-watcher/src/cli/index-block.ts @@ -0,0 +1,38 @@ +// +// Copyright 2022 Vulcanize, Inc. +// + +import 'reflect-metadata'; +import debug from 'debug'; + +import { IndexBlockCmd } from '@cerc-io/cli'; +import { getGraphDbAndWatcher } from '@cerc-io/graph-node'; + +import { Database, ENTITY_QUERY_TYPE_MAP, ENTITY_TO_LATEST_ENTITY_MAP } from '../database'; +import { Indexer } from '../indexer'; + +const log = debug('vulcanize:index-block'); + +const main = async (): Promise => { + const indexBlockCmd = new IndexBlockCmd(); + await indexBlockCmd.init(Database); + + const { graphWatcher } = await getGraphDbAndWatcher( + indexBlockCmd.config.server, + indexBlockCmd.clients.ethClient, + indexBlockCmd.ethProvider, + indexBlockCmd.database.baseDatabase, + ENTITY_QUERY_TYPE_MAP, + ENTITY_TO_LATEST_ENTITY_MAP + ); + + await indexBlockCmd.initIndexer(Indexer, graphWatcher); + + await indexBlockCmd.exec(); +}; + +main().catch(err => { + log(err); +}).finally(() => { + process.exit(0); +}); diff --git a/packages/blocks-watcher/src/cli/inspect-cid.ts b/packages/blocks-watcher/src/cli/inspect-cid.ts new file mode 100644 index 0000000..4f5955e --- /dev/null +++ b/packages/blocks-watcher/src/cli/inspect-cid.ts @@ -0,0 +1,38 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import 'reflect-metadata'; +import debug from 'debug'; + +import { InspectCIDCmd } from '@cerc-io/cli'; +import { getGraphDbAndWatcher } from '@cerc-io/graph-node'; + +import { Database, ENTITY_QUERY_TYPE_MAP, ENTITY_TO_LATEST_ENTITY_MAP } from '../database'; +import { Indexer } from '../indexer'; + +const log = debug('vulcanize:inspect-cid'); + +const main = async (): Promise => { + const inspectCIDCmd = new InspectCIDCmd(); + await inspectCIDCmd.init(Database); + + const { graphWatcher } = await getGraphDbAndWatcher( + inspectCIDCmd.config.server, + inspectCIDCmd.clients.ethClient, + inspectCIDCmd.ethProvider, + inspectCIDCmd.database.baseDatabase, + ENTITY_QUERY_TYPE_MAP, + ENTITY_TO_LATEST_ENTITY_MAP + ); + + await inspectCIDCmd.initIndexer(Indexer, graphWatcher); + + await inspectCIDCmd.exec(); +}; + +main().catch(err => { + log(err); +}).finally(() => { + process.exit(0); +}); diff --git a/packages/blocks-watcher/src/cli/reset-cmds/job-queue.ts b/packages/blocks-watcher/src/cli/reset-cmds/job-queue.ts new file mode 100644 index 0000000..c33cbfd --- /dev/null +++ b/packages/blocks-watcher/src/cli/reset-cmds/job-queue.ts @@ -0,0 +1,22 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import debug from 'debug'; + +import { getConfig, resetJobs, Config } from '@cerc-io/util'; + +const log = debug('vulcanize:reset-job-queue'); + +export const command = 'job-queue'; + +export const desc = 'Reset job queue'; + +export const builder = {}; + +export const handler = async (argv: any): Promise => { + const config: Config = await getConfig(argv.configFile); + await resetJobs(config); + + log('Job queue reset successfully'); +}; diff --git a/packages/blocks-watcher/src/cli/reset-cmds/state.ts b/packages/blocks-watcher/src/cli/reset-cmds/state.ts new file mode 100644 index 0000000..33211d6 --- /dev/null +++ b/packages/blocks-watcher/src/cli/reset-cmds/state.ts @@ -0,0 +1,24 @@ +// +// Copyright 2022 Vulcanize, Inc. +// + +import { ResetStateCmd } from '@cerc-io/cli'; + +import { Database } from '../../database'; + +export const command = 'state'; + +export const desc = 'Reset State to a given block number'; + +export const builder = { + blockNumber: { + type: 'number' + } +}; + +export const handler = async (argv: any): Promise => { + const resetStateCmd = new ResetStateCmd(); + await resetStateCmd.init(argv, Database); + + await resetStateCmd.exec(); +}; diff --git a/packages/blocks-watcher/src/cli/reset-cmds/watcher.ts b/packages/blocks-watcher/src/cli/reset-cmds/watcher.ts new file mode 100644 index 0000000..827fd28 --- /dev/null +++ b/packages/blocks-watcher/src/cli/reset-cmds/watcher.ts @@ -0,0 +1,37 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import { ResetWatcherCmd } from '@cerc-io/cli'; +import { getGraphDbAndWatcher } from '@cerc-io/graph-node'; + +import { Database, ENTITY_QUERY_TYPE_MAP, ENTITY_TO_LATEST_ENTITY_MAP } from '../../database'; +import { Indexer } from '../../indexer'; + +export const command = 'watcher'; + +export const desc = 'Reset watcher to a block number'; + +export const builder = { + blockNumber: { + type: 'number' + } +}; + +export const handler = async (argv: any): Promise => { + const resetWatcherCmd = new ResetWatcherCmd(); + await resetWatcherCmd.init(argv, Database); + + const { graphWatcher } = await getGraphDbAndWatcher( + resetWatcherCmd.config.server, + resetWatcherCmd.clients.ethClient, + resetWatcherCmd.ethProvider, + resetWatcherCmd.database.baseDatabase, + ENTITY_QUERY_TYPE_MAP, + ENTITY_TO_LATEST_ENTITY_MAP + ); + + await resetWatcherCmd.initIndexer(Indexer, graphWatcher); + + await resetWatcherCmd.exec(); +}; diff --git a/packages/blocks-watcher/src/cli/reset.ts b/packages/blocks-watcher/src/cli/reset.ts new file mode 100644 index 0000000..95648c8 --- /dev/null +++ b/packages/blocks-watcher/src/cli/reset.ts @@ -0,0 +1,24 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import 'reflect-metadata'; +import debug from 'debug'; + +import { getResetYargs } from '@cerc-io/util'; + +const log = debug('vulcanize:reset'); + +const main = async () => { + return getResetYargs() + .commandDir('reset-cmds', { extensions: ['ts', 'js'], exclude: /([a-zA-Z0-9\s_\\.\-:])+(.d.ts)$/ }) + .demandCommand(1) + .help() + .argv; +}; + +main().then(() => { + process.exit(); +}).catch(err => { + log(err); +}); diff --git a/packages/blocks-watcher/src/cli/watch-contract.ts b/packages/blocks-watcher/src/cli/watch-contract.ts new file mode 100644 index 0000000..7d6ce1a --- /dev/null +++ b/packages/blocks-watcher/src/cli/watch-contract.ts @@ -0,0 +1,38 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import 'reflect-metadata'; +import debug from 'debug'; + +import { WatchContractCmd } from '@cerc-io/cli'; +import { getGraphDbAndWatcher } from '@cerc-io/graph-node'; + +import { Database, ENTITY_QUERY_TYPE_MAP, ENTITY_TO_LATEST_ENTITY_MAP } from '../database'; +import { Indexer } from '../indexer'; + +const log = debug('vulcanize:watch-contract'); + +const main = async (): Promise => { + const watchContractCmd = new WatchContractCmd(); + await watchContractCmd.init(Database); + + const { graphWatcher } = await getGraphDbAndWatcher( + watchContractCmd.config.server, + watchContractCmd.clients.ethClient, + watchContractCmd.ethProvider, + watchContractCmd.database.baseDatabase, + ENTITY_QUERY_TYPE_MAP, + ENTITY_TO_LATEST_ENTITY_MAP + ); + + await watchContractCmd.initIndexer(Indexer, graphWatcher); + + await watchContractCmd.exec(); +}; + +main().catch(err => { + log(err); +}).finally(() => { + process.exit(0); +}); diff --git a/packages/blocks-watcher/src/client.ts b/packages/blocks-watcher/src/client.ts new file mode 100644 index 0000000..8bb2bb0 --- /dev/null +++ b/packages/blocks-watcher/src/client.ts @@ -0,0 +1,55 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import { gql } from '@apollo/client/core'; +import { GraphQLClient, GraphQLConfig } from '@cerc-io/ipld-eth-client'; + +import { queries, mutations, subscriptions } from './gql'; + +export class Client { + _config: GraphQLConfig; + _client: GraphQLClient; + + constructor (config: GraphQLConfig) { + this._config = config; + + this._client = new GraphQLClient(config); + } + + async getEvents (blockHash: string, contractAddress: string, name: string): Promise { + const { events } = await this._client.query( + gql(queries.events), + { blockHash, contractAddress, name } + ); + + return events; + } + + async getEventsInRange (fromBlockNumber: number, toBlockNumber: number): Promise { + const { eventsInRange } = await this._client.query( + gql(queries.eventsInRange), + { fromBlockNumber, toBlockNumber } + ); + + return eventsInRange; + } + + async watchContract (contractAddress: string, startingBlock?: number): Promise { + const { watchContract } = await this._client.mutate( + gql(mutations.watchContract), + { contractAddress, startingBlock } + ); + + return watchContract; + } + + async watchEvents (onNext: (value: any) => void): Promise { + return this._client.subscribe( + gql(subscriptions.onEvent), + ({ data }) => { + onNext(data.onEvent); + } + ); + } +} diff --git a/packages/blocks-watcher/src/database.ts b/packages/blocks-watcher/src/database.ts new file mode 100644 index 0000000..e2a46ad --- /dev/null +++ b/packages/blocks-watcher/src/database.ts @@ -0,0 +1,285 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import assert from 'assert'; +import { Connection, ConnectionOptions, DeepPartial, FindConditions, QueryRunner, FindManyOptions, EntityTarget } from 'typeorm'; +import path from 'path'; + +import { + ENTITY_QUERY_TYPE, + Database as BaseDatabase, + DatabaseInterface, + QueryOptions, + StateKind, + Where +} from '@cerc-io/util'; + +import { Contract } from './entity/Contract'; +import { Event } from './entity/Event'; +import { SyncStatus } from './entity/SyncStatus'; +import { StateSyncStatus } from './entity/StateSyncStatus'; +import { BlockProgress } from './entity/BlockProgress'; +import { State } from './entity/State'; +import { Block } from './entity/Block'; + +export const SUBGRAPH_ENTITIES = new Set([Block]); +export const ENTITIES = [...SUBGRAPH_ENTITIES]; +// Map: Entity to suitable query type. +export const ENTITY_QUERY_TYPE_MAP = new Map any, ENTITY_QUERY_TYPE>([]); + +export const ENTITY_TO_LATEST_ENTITY_MAP: Map = new Map(); + +export class Database implements DatabaseInterface { + _config: ConnectionOptions; + _conn!: Connection; + _baseDatabase: BaseDatabase; + _propColMaps: { [key: string]: Map; }; + + constructor (config: ConnectionOptions) { + assert(config); + + this._config = { + ...config, + subscribers: [path.join(__dirname, 'entity/Subscriber.*')], + entities: [path.join(__dirname, 'entity/*')] + }; + + this._baseDatabase = new BaseDatabase(this._config); + this._propColMaps = {}; + } + + get baseDatabase (): BaseDatabase { + return this._baseDatabase; + } + + async init (): Promise { + this._conn = await this._baseDatabase.init(); + } + + async close (): Promise { + return this._baseDatabase.close(); + } + + getNewState (): State { + return new State(); + } + + async getStates (where: FindConditions): Promise { + const repo = this._conn.getRepository(State); + + return this._baseDatabase.getStates(repo, where); + } + + async getLatestState (contractAddress: string, kind: StateKind | null, blockNumber?: number): Promise { + const repo = this._conn.getRepository(State); + + return this._baseDatabase.getLatestState(repo, contractAddress, kind, blockNumber); + } + + async getPrevState (blockHash: string, contractAddress: string, kind?: string): Promise { + const repo = this._conn.getRepository(State); + + return this._baseDatabase.getPrevState(repo, blockHash, contractAddress, kind); + } + + // Fetch all diff States after the specified block number. + async getDiffStatesInRange (contractAddress: string, startblock: number, endBlock: number): Promise { + const repo = this._conn.getRepository(State); + + return this._baseDatabase.getDiffStatesInRange(repo, contractAddress, startblock, endBlock); + } + + async saveOrUpdateState (dbTx: QueryRunner, state: State): Promise { + const repo = dbTx.manager.getRepository(State); + + return this._baseDatabase.saveOrUpdateState(repo, state); + } + + async removeStates (dbTx: QueryRunner, blockNumber: number, kind: string): Promise { + const repo = dbTx.manager.getRepository(State); + + await this._baseDatabase.removeStates(repo, blockNumber, kind); + } + + async removeStatesAfterBlock (dbTx: QueryRunner, blockNumber: number): Promise { + const repo = dbTx.manager.getRepository(State); + + await this._baseDatabase.removeStatesAfterBlock(repo, blockNumber); + } + + async getStateSyncStatus (): Promise { + const repo = this._conn.getRepository(StateSyncStatus); + + return this._baseDatabase.getStateSyncStatus(repo); + } + + async updateStateSyncStatusIndexedBlock (queryRunner: QueryRunner, blockNumber: number, force?: boolean): Promise { + const repo = queryRunner.manager.getRepository(StateSyncStatus); + + return this._baseDatabase.updateStateSyncStatusIndexedBlock(repo, blockNumber, force); + } + + async updateStateSyncStatusCheckpointBlock (queryRunner: QueryRunner, blockNumber: number, force?: boolean): Promise { + const repo = queryRunner.manager.getRepository(StateSyncStatus); + + return this._baseDatabase.updateStateSyncStatusCheckpointBlock(repo, blockNumber, force); + } + + async getContracts (): Promise { + const repo = this._conn.getRepository(Contract); + + return this._baseDatabase.getContracts(repo); + } + + async createTransactionRunner (): Promise { + return this._baseDatabase.createTransactionRunner(); + } + + async getProcessedBlockCountForRange (fromBlockNumber: number, toBlockNumber: number): Promise<{ expected: number, actual: number }> { + const repo = this._conn.getRepository(BlockProgress); + + return this._baseDatabase.getProcessedBlockCountForRange(repo, fromBlockNumber, toBlockNumber); + } + + async getEventsInRange (fromBlockNumber: number, toBlockNumber: number): Promise> { + const repo = this._conn.getRepository(Event); + + return this._baseDatabase.getEventsInRange(repo, fromBlockNumber, toBlockNumber); + } + + async saveEventEntity (queryRunner: QueryRunner, entity: Event): Promise { + const repo = queryRunner.manager.getRepository(Event); + return this._baseDatabase.saveEventEntity(repo, entity); + } + + async getBlockEvents (blockHash: string, where: Where, queryOptions: QueryOptions): Promise { + const repo = this._conn.getRepository(Event); + + return this._baseDatabase.getBlockEvents(repo, blockHash, where, queryOptions); + } + + async saveBlockWithEvents (queryRunner: QueryRunner, block: DeepPartial, events: DeepPartial[]): Promise { + const blockRepo = queryRunner.manager.getRepository(BlockProgress); + const eventRepo = queryRunner.manager.getRepository(Event); + + return this._baseDatabase.saveBlockWithEvents(blockRepo, eventRepo, block, events); + } + + async saveEvents (queryRunner: QueryRunner, events: Event[]): Promise { + const eventRepo = queryRunner.manager.getRepository(Event); + + return this._baseDatabase.saveEvents(eventRepo, events); + } + + async saveBlockProgress (queryRunner: QueryRunner, block: DeepPartial): Promise { + const repo = queryRunner.manager.getRepository(BlockProgress); + + return this._baseDatabase.saveBlockProgress(repo, block); + } + + async saveContract (queryRunner: QueryRunner, address: string, kind: string, checkpoint: boolean, startingBlock: number, context?: any): Promise { + const repo = queryRunner.manager.getRepository(Contract); + + return this._baseDatabase.saveContract(repo, address, kind, checkpoint, startingBlock, context); + } + + async updateSyncStatusIndexedBlock (queryRunner: QueryRunner, blockHash: string, blockNumber: number, force = false): Promise { + const repo = queryRunner.manager.getRepository(SyncStatus); + + return this._baseDatabase.updateSyncStatusIndexedBlock(repo, blockHash, blockNumber, force); + } + + async updateSyncStatusCanonicalBlock (queryRunner: QueryRunner, blockHash: string, blockNumber: number, force = false): Promise { + const repo = queryRunner.manager.getRepository(SyncStatus); + + return this._baseDatabase.updateSyncStatusCanonicalBlock(repo, blockHash, blockNumber, force); + } + + async updateSyncStatusChainHead (queryRunner: QueryRunner, blockHash: string, blockNumber: number, force = false): Promise { + const repo = queryRunner.manager.getRepository(SyncStatus); + + return this._baseDatabase.updateSyncStatusChainHead(repo, blockHash, blockNumber, force); + } + + async updateSyncStatusProcessedBlock (queryRunner: QueryRunner, blockHash: string, blockNumber: number, force = false): Promise { + const repo = queryRunner.manager.getRepository(SyncStatus); + + return this._baseDatabase.updateSyncStatusProcessedBlock(repo, blockHash, blockNumber, force); + } + + async updateSyncStatusIndexingError (queryRunner: QueryRunner, hasIndexingError: boolean): Promise { + const repo = queryRunner.manager.getRepository(SyncStatus); + + return this._baseDatabase.updateSyncStatusIndexingError(repo, hasIndexingError); + } + + async updateSyncStatus (queryRunner: QueryRunner, syncStatus: DeepPartial): Promise { + const repo = queryRunner.manager.getRepository(SyncStatus); + + return this._baseDatabase.updateSyncStatus(repo, syncStatus); + } + + async getSyncStatus (queryRunner: QueryRunner): Promise { + const repo = queryRunner.manager.getRepository(SyncStatus); + + return this._baseDatabase.getSyncStatus(repo); + } + + async getEvent (id: string): Promise { + const repo = this._conn.getRepository(Event); + + return this._baseDatabase.getEvent(repo, id); + } + + async getBlocksAtHeight (height: number, isPruned: boolean): Promise { + const repo = this._conn.getRepository(BlockProgress); + + return this._baseDatabase.getBlocksAtHeight(repo, height, isPruned); + } + + async markBlocksAsPruned (queryRunner: QueryRunner, blocks: BlockProgress[]): Promise { + const repo = queryRunner.manager.getRepository(BlockProgress); + + return this._baseDatabase.markBlocksAsPruned(repo, blocks); + } + + async getBlockProgress (blockHash: string): Promise { + const repo = this._conn.getRepository(BlockProgress); + return this._baseDatabase.getBlockProgress(repo, blockHash); + } + + async getBlockProgressEntities (where: FindConditions, options: FindManyOptions): Promise { + const repo = this._conn.getRepository(BlockProgress); + + return this._baseDatabase.getBlockProgressEntities(repo, where, options); + } + + async getEntitiesForBlock (blockHash: string, tableName: string): Promise { + return this._baseDatabase.getEntitiesForBlock(blockHash, tableName); + } + + async updateBlockProgress (queryRunner: QueryRunner, block: BlockProgress, lastProcessedEventIndex: number): Promise { + const repo = queryRunner.manager.getRepository(BlockProgress); + + return this._baseDatabase.updateBlockProgress(repo, block, lastProcessedEventIndex); + } + + async removeEntities (queryRunner: QueryRunner, entity: new () => Entity, findConditions?: FindManyOptions | FindConditions): Promise { + return this._baseDatabase.removeEntities(queryRunner, entity, findConditions); + } + + async deleteEntitiesByConditions (queryRunner: QueryRunner, entity: EntityTarget, findConditions: FindConditions): Promise { + await this._baseDatabase.deleteEntitiesByConditions(queryRunner, entity, findConditions); + } + + async getAncestorAtHeight (blockHash: string, height: number): Promise { + return this._baseDatabase.getAncestorAtHeight(blockHash, height); + } + + _getPropertyColumnMapForEntity (entityName: string): Map { + return this._conn.getMetadata(entityName).ownColumns.reduce((acc, curr) => { + return acc.set(curr.propertyName, curr.databaseName); + }, new Map()); + } +} diff --git a/packages/blocks-watcher/src/entity/Block.ts b/packages/blocks-watcher/src/entity/Block.ts new file mode 100644 index 0000000..84b446e --- /dev/null +++ b/packages/blocks-watcher/src/entity/Block.ts @@ -0,0 +1,64 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import { Entity, PrimaryColumn, Column, Index } from 'typeorm'; +import { bigintTransformer } from '@cerc-io/util'; + +@Entity() +@Index(['blockNumber']) +export class Block { + @PrimaryColumn('varchar') + id!: string; + + @PrimaryColumn('varchar', { length: 66 }) + blockHash!: string; + + @Column('integer') + blockNumber!: number; + + @Column('numeric', { transformer: bigintTransformer }) + number!: bigint; + + @Column('numeric', { transformer: bigintTransformer }) + timestamp!: bigint; + + @Column('varchar', { nullable: true }) + parentHash!: string | null; + + @Column('varchar', { nullable: true }) + author!: string | null; + + @Column('numeric', { nullable: true, transformer: bigintTransformer }) + difficulty!: bigint | null; + + @Column('numeric', { nullable: true, transformer: bigintTransformer }) + totalDifficulty!: bigint | null; + + @Column('numeric', { nullable: true, transformer: bigintTransformer }) + gasUsed!: bigint | null; + + @Column('numeric', { nullable: true, transformer: bigintTransformer }) + gasLimit!: bigint | null; + + @Column('varchar', { nullable: true }) + receiptsRoot!: string | null; + + @Column('varchar', { nullable: true }) + transactionsRoot!: string | null; + + @Column('varchar', { nullable: true }) + stateRoot!: string | null; + + @Column('numeric', { nullable: true, transformer: bigintTransformer }) + size!: bigint | null; + + @Column('varchar', { nullable: true }) + unclesHash!: string | null; + + @Column('boolean', { default: false }) + isPruned!: boolean; + + @Column('boolean', { default: false }) + isRemoved!: boolean; +} diff --git a/packages/blocks-watcher/src/entity/BlockProgress.ts b/packages/blocks-watcher/src/entity/BlockProgress.ts new file mode 100644 index 0000000..ded4a86 --- /dev/null +++ b/packages/blocks-watcher/src/entity/BlockProgress.ts @@ -0,0 +1,48 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import { Entity, PrimaryGeneratedColumn, Column, Index, CreateDateColumn } from 'typeorm'; +import { BlockProgressInterface } from '@cerc-io/util'; + +@Entity() +@Index(['blockHash'], { unique: true }) +@Index(['blockNumber']) +@Index(['parentHash']) +export class BlockProgress implements BlockProgressInterface { + @PrimaryGeneratedColumn() + id!: number; + + @Column('varchar', { nullable: true }) + cid!: string | null; + + @Column('varchar', { length: 66 }) + blockHash!: string; + + @Column('varchar', { length: 66 }) + parentHash!: string; + + @Column('integer') + blockNumber!: number; + + @Column('integer') + blockTimestamp!: number; + + @Column('integer') + numEvents!: number; + + @Column('integer') + numProcessedEvents!: number; + + @Column('integer') + lastProcessedEventIndex!: number; + + @Column('boolean') + isComplete!: boolean; + + @Column('boolean', { default: false }) + isPruned!: boolean; + + @CreateDateColumn() + createdAt!: Date; +} diff --git a/packages/blocks-watcher/src/entity/Contract.ts b/packages/blocks-watcher/src/entity/Contract.ts new file mode 100644 index 0000000..e4defa8 --- /dev/null +++ b/packages/blocks-watcher/src/entity/Contract.ts @@ -0,0 +1,27 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import { Entity, PrimaryGeneratedColumn, Column, Index } from 'typeorm'; + +@Entity() +@Index(['address'], { unique: true }) +export class Contract { + @PrimaryGeneratedColumn() + id!: number; + + @Column('varchar', { length: 42 }) + address!: string; + + @Column('varchar') + kind!: string; + + @Column('boolean') + checkpoint!: boolean; + + @Column('integer') + startingBlock!: number; + + @Column('jsonb', { nullable: true }) + context!: Record; +} diff --git a/packages/blocks-watcher/src/entity/Event.ts b/packages/blocks-watcher/src/entity/Event.ts new file mode 100644 index 0000000..91f1e6d --- /dev/null +++ b/packages/blocks-watcher/src/entity/Event.ts @@ -0,0 +1,38 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import { Entity, PrimaryGeneratedColumn, Column, Index, ManyToOne } from 'typeorm'; +import { BlockProgress } from './BlockProgress'; + +@Entity() +@Index(['block', 'contract']) +@Index(['block', 'contract', 'eventName']) +export class Event { + @PrimaryGeneratedColumn() + id!: number; + + @ManyToOne(() => BlockProgress, { onDelete: 'CASCADE' }) + block!: BlockProgress; + + @Column('varchar', { length: 66 }) + txHash!: string; + + @Column('integer') + index!: number; + + @Column('varchar', { length: 42 }) + contract!: string; + + @Column('varchar', { length: 256 }) + eventName!: string; + + @Column('text') + eventInfo!: string; + + @Column('text') + extraInfo!: string; + + @Column('text') + proof!: string; +} diff --git a/packages/blocks-watcher/src/entity/FrothyEntity.ts b/packages/blocks-watcher/src/entity/FrothyEntity.ts new file mode 100644 index 0000000..9898ce8 --- /dev/null +++ b/packages/blocks-watcher/src/entity/FrothyEntity.ts @@ -0,0 +1,21 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import { Entity, PrimaryColumn, Column, Index } from 'typeorm'; + +@Entity() +@Index(['blockNumber']) +export class FrothyEntity { + @PrimaryColumn('varchar') + id!: string; + + @PrimaryColumn('varchar') + name!: string; + + @PrimaryColumn('varchar', { length: 66 }) + blockHash!: string; + + @Column('integer') + blockNumber!: number; +} diff --git a/packages/blocks-watcher/src/entity/State.ts b/packages/blocks-watcher/src/entity/State.ts new file mode 100644 index 0000000..bc05bca --- /dev/null +++ b/packages/blocks-watcher/src/entity/State.ts @@ -0,0 +1,31 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import { Entity, PrimaryGeneratedColumn, Column, Index, ManyToOne } from 'typeorm'; +import { StateKind } from '@cerc-io/util'; +import { BlockProgress } from './BlockProgress'; + +@Entity() +@Index(['cid'], { unique: true }) +@Index(['block', 'contractAddress']) +@Index(['block', 'contractAddress', 'kind'], { unique: true }) +export class State { + @PrimaryGeneratedColumn() + id!: number; + + @ManyToOne(() => BlockProgress, { onDelete: 'CASCADE' }) + block!: BlockProgress; + + @Column('varchar', { length: 42 }) + contractAddress!: string; + + @Column('varchar') + cid!: string; + + @Column({ type: 'enum', enum: StateKind }) + kind!: StateKind; + + @Column('bytea') + data!: Buffer; +} diff --git a/packages/blocks-watcher/src/entity/StateSyncStatus.ts b/packages/blocks-watcher/src/entity/StateSyncStatus.ts new file mode 100644 index 0000000..1535eb4 --- /dev/null +++ b/packages/blocks-watcher/src/entity/StateSyncStatus.ts @@ -0,0 +1,17 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'; + +@Entity() +export class StateSyncStatus { + @PrimaryGeneratedColumn() + id!: number; + + @Column('integer') + latestIndexedBlockNumber!: number; + + @Column('integer') + latestCheckpointBlockNumber!: number; +} diff --git a/packages/blocks-watcher/src/entity/Subscriber.ts b/packages/blocks-watcher/src/entity/Subscriber.ts new file mode 100644 index 0000000..2cccb84 --- /dev/null +++ b/packages/blocks-watcher/src/entity/Subscriber.ts @@ -0,0 +1,21 @@ +// +// Copyright 2022 Vulcanize, Inc. +// + +import { EventSubscriber, EntitySubscriberInterface, InsertEvent, UpdateEvent } from 'typeorm'; + +import { afterEntityInsertOrUpdate } from '@cerc-io/util'; + +import { FrothyEntity } from './FrothyEntity'; +import { ENTITY_TO_LATEST_ENTITY_MAP, SUBGRAPH_ENTITIES } from '../database'; + +@EventSubscriber() +export class EntitySubscriber implements EntitySubscriberInterface { + async afterInsert (event: InsertEvent): Promise { + await afterEntityInsertOrUpdate(FrothyEntity, SUBGRAPH_ENTITIES, event, ENTITY_TO_LATEST_ENTITY_MAP); + } + + async afterUpdate (event: UpdateEvent): Promise { + await afterEntityInsertOrUpdate(FrothyEntity, SUBGRAPH_ENTITIES, event, ENTITY_TO_LATEST_ENTITY_MAP); + } +} diff --git a/packages/blocks-watcher/src/entity/SyncStatus.ts b/packages/blocks-watcher/src/entity/SyncStatus.ts new file mode 100644 index 0000000..cc13c70 --- /dev/null +++ b/packages/blocks-watcher/src/entity/SyncStatus.ts @@ -0,0 +1,45 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'; +import { SyncStatusInterface } from '@cerc-io/util'; + +@Entity() +export class SyncStatus implements SyncStatusInterface { + @PrimaryGeneratedColumn() + id!: number; + + @Column('varchar', { length: 66 }) + chainHeadBlockHash!: string; + + @Column('integer') + chainHeadBlockNumber!: number; + + @Column('varchar', { length: 66 }) + latestIndexedBlockHash!: string; + + @Column('integer') + latestIndexedBlockNumber!: number; + + @Column('varchar', { length: 66 }) + latestProcessedBlockHash!: string; + + @Column('integer') + latestProcessedBlockNumber!: number; + + @Column('varchar', { length: 66 }) + latestCanonicalBlockHash!: string; + + @Column('integer') + latestCanonicalBlockNumber!: number; + + @Column('varchar', { length: 66 }) + initialIndexedBlockHash!: string; + + @Column('integer') + initialIndexedBlockNumber!: number; + + @Column('boolean', { default: false }) + hasIndexingError!: boolean; +} diff --git a/packages/blocks-watcher/src/fill.ts b/packages/blocks-watcher/src/fill.ts new file mode 100644 index 0000000..210341e --- /dev/null +++ b/packages/blocks-watcher/src/fill.ts @@ -0,0 +1,48 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import 'reflect-metadata'; +import debug from 'debug'; + +import { FillCmd } from '@cerc-io/cli'; +import { getContractEntitiesMap } from '@cerc-io/util'; +import { getGraphDbAndWatcher } from '@cerc-io/graph-node'; + +import { Database, ENTITY_QUERY_TYPE_MAP, ENTITY_TO_LATEST_ENTITY_MAP } from './database'; +import { Indexer } from './indexer'; + +const log = debug('vulcanize:fill'); + +export const main = async (): Promise => { + const fillCmd = new FillCmd(); + await fillCmd.init(Database); + + const { graphWatcher } = await getGraphDbAndWatcher( + fillCmd.config.server, + fillCmd.clients.ethClient, + fillCmd.ethProvider, + fillCmd.database.baseDatabase, + ENTITY_QUERY_TYPE_MAP, + ENTITY_TO_LATEST_ENTITY_MAP + ); + + await fillCmd.initIndexer(Indexer, graphWatcher); + + // Get contractEntitiesMap required for fill-state + // NOTE: Assuming each entity type is only mapped to a single contract + const contractEntitiesMap = getContractEntitiesMap(graphWatcher.dataSources); + + await fillCmd.exec(contractEntitiesMap); +}; + +main().catch(err => { + log(err); +}).finally(() => { + process.exit(); +}); + +process.on('SIGINT', () => { + log(`Exiting process ${process.pid} with code 0`); + process.exit(0); +}); diff --git a/packages/blocks-watcher/src/gql/index.ts b/packages/blocks-watcher/src/gql/index.ts new file mode 100644 index 0000000..4732f68 --- /dev/null +++ b/packages/blocks-watcher/src/gql/index.ts @@ -0,0 +1,3 @@ +export * as mutations from './mutations'; +export * as queries from './queries'; +export * as subscriptions from './subscriptions'; diff --git a/packages/blocks-watcher/src/gql/mutations/index.ts b/packages/blocks-watcher/src/gql/mutations/index.ts new file mode 100644 index 0000000..0c3bd85 --- /dev/null +++ b/packages/blocks-watcher/src/gql/mutations/index.ts @@ -0,0 +1,4 @@ +import fs from 'fs'; +import path from 'path'; + +export const watchContract = fs.readFileSync(path.join(__dirname, 'watchContract.gql'), 'utf8'); diff --git a/packages/blocks-watcher/src/gql/mutations/watchContract.gql b/packages/blocks-watcher/src/gql/mutations/watchContract.gql new file mode 100644 index 0000000..2ecc74f --- /dev/null +++ b/packages/blocks-watcher/src/gql/mutations/watchContract.gql @@ -0,0 +1,3 @@ +mutation watchContract($address: String!, $kind: String!, $checkpoint: Boolean!, $startingBlock: Int){ + watchContract(address: $address, kind: $kind, checkpoint: $checkpoint, startingBlock: $startingBlock) +} \ No newline at end of file diff --git a/packages/blocks-watcher/src/gql/queries/_meta.gql b/packages/blocks-watcher/src/gql/queries/_meta.gql new file mode 100644 index 0000000..d686e04 --- /dev/null +++ b/packages/blocks-watcher/src/gql/queries/_meta.gql @@ -0,0 +1,11 @@ +query _meta($block: Block_height){ + _meta(block: $block){ + block{ + hash + number + timestamp + } + deployment + hasIndexingErrors + } +} \ No newline at end of file diff --git a/packages/blocks-watcher/src/gql/queries/block.gql b/packages/blocks-watcher/src/gql/queries/block.gql new file mode 100644 index 0000000..3844476 --- /dev/null +++ b/packages/blocks-watcher/src/gql/queries/block.gql @@ -0,0 +1,18 @@ +query block($id: ID!, $block: Block_height){ + block(id: $id, block: $block){ + id + number + timestamp + parentHash + author + difficulty + totalDifficulty + gasUsed + gasLimit + receiptsRoot + transactionsRoot + stateRoot + size + unclesHash + } +} \ No newline at end of file diff --git a/packages/blocks-watcher/src/gql/queries/blocks.gql b/packages/blocks-watcher/src/gql/queries/blocks.gql new file mode 100644 index 0000000..a083ff9 --- /dev/null +++ b/packages/blocks-watcher/src/gql/queries/blocks.gql @@ -0,0 +1,18 @@ +query blocks($block: Block_height, $where: Block_filter, $orderBy: Block_orderBy, $orderDirection: OrderDirection, $first: Int, $skip: Int){ + blocks(block: $block, where: $where, orderBy: $orderBy, orderDirection: $orderDirection, first: $first, skip: $skip){ + id + number + timestamp + parentHash + author + difficulty + totalDifficulty + gasUsed + gasLimit + receiptsRoot + transactionsRoot + stateRoot + size + unclesHash + } +} \ No newline at end of file diff --git a/packages/blocks-watcher/src/gql/queries/events.gql b/packages/blocks-watcher/src/gql/queries/events.gql new file mode 100644 index 0000000..130267b --- /dev/null +++ b/packages/blocks-watcher/src/gql/queries/events.gql @@ -0,0 +1,30 @@ +query events($blockHash: String!, $contractAddress: String!, $name: String){ + events(blockHash: $blockHash, contractAddress: $contractAddress, name: $name){ + block{ + cid + hash + number + timestamp + parentHash + } + tx{ + hash + index + from + to + } + contract + eventIndex + event{ + ... on PairCreatedEvent { + token0 + token1 + pair + null + } + } + proof{ + data + } + } +} \ No newline at end of file diff --git a/packages/blocks-watcher/src/gql/queries/eventsInRange.gql b/packages/blocks-watcher/src/gql/queries/eventsInRange.gql new file mode 100644 index 0000000..4a1ab93 --- /dev/null +++ b/packages/blocks-watcher/src/gql/queries/eventsInRange.gql @@ -0,0 +1,30 @@ +query eventsInRange($fromBlockNumber: Int!, $toBlockNumber: Int!){ + eventsInRange(fromBlockNumber: $fromBlockNumber, toBlockNumber: $toBlockNumber){ + block{ + cid + hash + number + timestamp + parentHash + } + tx{ + hash + index + from + to + } + contract + eventIndex + event{ + ... on PairCreatedEvent { + token0 + token1 + pair + null + } + } + proof{ + data + } + } +} \ No newline at end of file diff --git a/packages/blocks-watcher/src/gql/queries/getState.gql b/packages/blocks-watcher/src/gql/queries/getState.gql new file mode 100644 index 0000000..3b8f605 --- /dev/null +++ b/packages/blocks-watcher/src/gql/queries/getState.gql @@ -0,0 +1,15 @@ +query getState($blockHash: String!, $contractAddress: String!, $kind: String){ + getState(blockHash: $blockHash, contractAddress: $contractAddress, kind: $kind){ + block{ + cid + hash + number + timestamp + parentHash + } + contractAddress + cid + kind + data + } +} \ No newline at end of file diff --git a/packages/blocks-watcher/src/gql/queries/getStateByCID.gql b/packages/blocks-watcher/src/gql/queries/getStateByCID.gql new file mode 100644 index 0000000..6c3c4fd --- /dev/null +++ b/packages/blocks-watcher/src/gql/queries/getStateByCID.gql @@ -0,0 +1,15 @@ +query getStateByCID($cid: String!){ + getStateByCID(cid: $cid){ + block{ + cid + hash + number + timestamp + parentHash + } + contractAddress + cid + kind + data + } +} \ No newline at end of file diff --git a/packages/blocks-watcher/src/gql/queries/getSyncStatus.gql b/packages/blocks-watcher/src/gql/queries/getSyncStatus.gql new file mode 100644 index 0000000..48175b4 --- /dev/null +++ b/packages/blocks-watcher/src/gql/queries/getSyncStatus.gql @@ -0,0 +1,12 @@ +query getSyncStatus{ + getSyncStatus{ + latestIndexedBlockHash + latestIndexedBlockNumber + latestCanonicalBlockHash + latestCanonicalBlockNumber + initialIndexedBlockHash + initialIndexedBlockNumber + latestProcessedBlockHash + latestProcessedBlockNumber + } +} \ No newline at end of file diff --git a/packages/blocks-watcher/src/gql/queries/index.ts b/packages/blocks-watcher/src/gql/queries/index.ts new file mode 100644 index 0000000..7fca1a5 --- /dev/null +++ b/packages/blocks-watcher/src/gql/queries/index.ts @@ -0,0 +1,11 @@ +import fs from 'fs'; +import path from 'path'; + +export const events = fs.readFileSync(path.join(__dirname, 'events.gql'), 'utf8'); +export const eventsInRange = fs.readFileSync(path.join(__dirname, 'eventsInRange.gql'), 'utf8'); +export const block = fs.readFileSync(path.join(__dirname, 'block.gql'), 'utf8'); +export const blocks = fs.readFileSync(path.join(__dirname, 'blocks.gql'), 'utf8'); +export const _meta = fs.readFileSync(path.join(__dirname, '_meta.gql'), 'utf8'); +export const getStateByCID = fs.readFileSync(path.join(__dirname, 'getStateByCID.gql'), 'utf8'); +export const getState = fs.readFileSync(path.join(__dirname, 'getState.gql'), 'utf8'); +export const getSyncStatus = fs.readFileSync(path.join(__dirname, 'getSyncStatus.gql'), 'utf8'); diff --git a/packages/blocks-watcher/src/gql/subscriptions/index.ts b/packages/blocks-watcher/src/gql/subscriptions/index.ts new file mode 100644 index 0000000..f12910c --- /dev/null +++ b/packages/blocks-watcher/src/gql/subscriptions/index.ts @@ -0,0 +1,4 @@ +import fs from 'fs'; +import path from 'path'; + +export const onEvent = fs.readFileSync(path.join(__dirname, 'onEvent.gql'), 'utf8'); diff --git a/packages/blocks-watcher/src/gql/subscriptions/onEvent.gql b/packages/blocks-watcher/src/gql/subscriptions/onEvent.gql new file mode 100644 index 0000000..ecde54b --- /dev/null +++ b/packages/blocks-watcher/src/gql/subscriptions/onEvent.gql @@ -0,0 +1,30 @@ +subscription onEvent{ + onEvent{ + block{ + cid + hash + number + timestamp + parentHash + } + tx{ + hash + index + from + to + } + contract + eventIndex + event{ + ... on PairCreatedEvent { + token0 + token1 + pair + null + } + } + proof{ + data + } + } +} \ No newline at end of file diff --git a/packages/blocks-watcher/src/hooks.ts b/packages/blocks-watcher/src/hooks.ts new file mode 100644 index 0000000..d45498b --- /dev/null +++ b/packages/blocks-watcher/src/hooks.ts @@ -0,0 +1,86 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import assert from 'assert'; + +import { + ResultEvent, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + updateStateForMappingType, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + updateStateForElementaryType +} from '@cerc-io/util'; + +import { Indexer } from './indexer'; + +/** + * Hook function to store an initial state. + * @param indexer Indexer instance. + * @param blockHash Hash of the concerned block. + * @param contractAddress Address of the concerned contract. + * @returns Data block to be stored. + */ +export async function createInitialState (indexer: Indexer, contractAddress: string, blockHash: string): Promise { + assert(indexer); + assert(blockHash); + assert(contractAddress); + + // Store an empty State. + const stateData: any = { + state: {} + }; + + // Use updateStateForElementaryType to update initial state with an elementary property. + // Eg. const stateData = updateStateForElementaryType(stateData, '_totalBalance', result.value.toString()); + + // Use updateStateForMappingType to update initial state with a nested property. + // Eg. const stateData = updateStateForMappingType(stateData, '_allowances', [owner, spender], allowance.value.toString()); + + // Return initial state data to be saved. + return stateData; +} + +/** + * Hook function to create state diff. + * @param indexer Indexer instance that contains methods to fetch the contract variable values. + * @param blockHash Block hash of the concerned block. + */ +export async function createStateDiff (indexer: Indexer, blockHash: string): Promise { + assert(indexer); + assert(blockHash); + + // Use indexer.createDiff() method to save custom state diff(s). +} + +/** + * Hook function to create state checkpoint + * @param indexer Indexer instance. + * @param contractAddress Address of the concerned contract. + * @param blockHash Block hash of the concerned block. + * @returns Whether to disable default checkpoint. If false, the state from this hook is updated with that from default checkpoint. + */ +export async function createStateCheckpoint (indexer: Indexer, contractAddress: string, blockHash: string): Promise { + assert(indexer); + assert(blockHash); + assert(contractAddress); + + // Use indexer.createStateCheckpoint() method to create a custom checkpoint. + + // Return false to update the state created by this hook by auto-generated checkpoint state. + // Return true to disable update of the state created by this hook by auto-generated checkpoint state. + return false; +} + +/** + * Event hook function. + * @param indexer Indexer instance that contains methods to fetch and update the contract values in the database. + * @param eventData ResultEvent object containing event information. + */ +export async function handleEvent (indexer: Indexer, eventData: ResultEvent): Promise { + assert(indexer); + assert(eventData); + + // Use indexer methods to index data. + // Pass `diff` parameter to indexer methods as true to save an auto-generated state from the indexed data. +} diff --git a/packages/blocks-watcher/src/indexer.ts b/packages/blocks-watcher/src/indexer.ts new file mode 100644 index 0000000..bc66bb9 --- /dev/null +++ b/packages/blocks-watcher/src/indexer.ts @@ -0,0 +1,683 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import assert from 'assert'; +import { DeepPartial, FindConditions, FindManyOptions, ObjectLiteral } from 'typeorm'; +import debug from 'debug'; +import { ethers, constants } from 'ethers'; +import { GraphQLResolveInfo } from 'graphql'; + +import { JsonFragment } from '@ethersproject/abi'; +import { BaseProvider } from '@ethersproject/providers'; +import { MappingKey, StorageLayout } from '@cerc-io/solidity-mapper'; +import { + Indexer as BaseIndexer, + IndexerInterface, + ValueResult, + ServerConfig, + JobQueue, + Where, + QueryOptions, + BlockHeight, + ResultMeta, + updateSubgraphState, + dumpSubgraphState, + GraphWatcherInterface, + StateKind, + StateStatus, + ResultEvent, + getResultEvent, + DatabaseInterface, + Clients, + EthClient, + UpstreamConfig, + EthFullBlock, + EthFullTransaction, + ExtraEventData +} from '@cerc-io/util'; +import { GraphWatcher } from '@cerc-io/graph-node'; + +import UniswapV2FactoryArtifacts from './artifacts/UniswapV2Factory.json'; +import { Database, ENTITIES, SUBGRAPH_ENTITIES } from './database'; +import { createInitialState, handleEvent, createStateDiff, createStateCheckpoint } from './hooks'; +import { Contract } from './entity/Contract'; +import { Event } from './entity/Event'; +import { SyncStatus } from './entity/SyncStatus'; +import { StateSyncStatus } from './entity/StateSyncStatus'; +import { BlockProgress } from './entity/BlockProgress'; +import { State } from './entity/State'; +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { Block } from './entity/Block'; +/* eslint-enable @typescript-eslint/no-unused-vars */ + +import { FrothyEntity } from './entity/FrothyEntity'; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const log = debug('vulcanize:indexer'); + +const KIND_UNISWAPV2FACTORY = 'UniswapV2Factory'; + +export class Indexer implements IndexerInterface { + _db: Database; + _ethClient: EthClient; + _ethProvider: BaseProvider; + _baseIndexer: BaseIndexer; + _serverConfig: ServerConfig; + _upstreamConfig: UpstreamConfig; + _graphWatcher: GraphWatcher; + + _abiMap: Map; + _storageLayoutMap: Map; + _contractMap: Map; + eventSignaturesMap: Map; + + _entityTypesMap: Map; + _relationsMap: Map; + + _subgraphStateMap: Map; + + constructor ( + config: { + server: ServerConfig; + upstream: UpstreamConfig; + }, + db: DatabaseInterface, + clients: Clients, + ethProvider: BaseProvider, + jobQueue: JobQueue, + graphWatcher?: GraphWatcherInterface + ) { + assert(db); + assert(clients.ethClient); + + this._db = db as Database; + this._ethClient = clients.ethClient; + this._ethProvider = ethProvider; + this._serverConfig = config.server; + this._upstreamConfig = config.upstream; + this._baseIndexer = new BaseIndexer(config, this._db, this._ethClient, this._ethProvider, jobQueue); + assert(graphWatcher); + this._graphWatcher = graphWatcher as GraphWatcher; + + this._abiMap = new Map(); + this._storageLayoutMap = new Map(); + this._contractMap = new Map(); + this.eventSignaturesMap = new Map(); + + const { abi: UniswapV2FactoryABI } = UniswapV2FactoryArtifacts; + + assert(UniswapV2FactoryABI); + this._abiMap.set(KIND_UNISWAPV2FACTORY, UniswapV2FactoryABI); + + const UniswapV2FactoryContractInterface = new ethers.utils.Interface(UniswapV2FactoryABI); + this._contractMap.set(KIND_UNISWAPV2FACTORY, UniswapV2FactoryContractInterface); + + const UniswapV2FactoryEventSignatures = Object.values(UniswapV2FactoryContractInterface.events).map(value => { + return UniswapV2FactoryContractInterface.getEventTopic(value); + }); + this.eventSignaturesMap.set(KIND_UNISWAPV2FACTORY, UniswapV2FactoryEventSignatures); + + this._entityTypesMap = new Map(); + this._populateEntityTypesMap(); + + this._relationsMap = new Map(); + this._populateRelationsMap(); + + this._subgraphStateMap = new Map(); + } + + get serverConfig (): ServerConfig { + return this._serverConfig; + } + + get upstreamConfig (): UpstreamConfig { + return this._upstreamConfig; + } + + get storageLayoutMap (): Map { + return this._storageLayoutMap; + } + + get graphWatcher (): GraphWatcher { + return this._graphWatcher; + } + + async init (): Promise { + await this._baseIndexer.fetchContracts(); + await this._baseIndexer.fetchStateStatus(); + } + + switchClients ({ ethClient, ethProvider }: { ethClient: EthClient, ethProvider: BaseProvider }): void { + this._ethClient = ethClient; + this._ethProvider = ethProvider; + this._baseIndexer.switchClients({ ethClient, ethProvider }); + this._graphWatcher.switchClients({ ethClient, ethProvider }); + } + + async getMetaData (block: BlockHeight): Promise { + return this._baseIndexer.getMetaData(block); + } + + getResultEvent (event: Event): ResultEvent { + return getResultEvent(event); + } + + async getStorageValue (storageLayout: StorageLayout, blockHash: string, contractAddress: string, variable: string, ...mappingKeys: MappingKey[]): Promise { + return this._baseIndexer.getStorageValue( + storageLayout, + blockHash, + contractAddress, + variable, + ...mappingKeys + ); + } + + async getEntitiesForBlock (blockHash: string, tableName: string): Promise { + return this._db.getEntitiesForBlock(blockHash, tableName); + } + + async processInitialState (contractAddress: string, blockHash: string): Promise { + // Call initial state hook. + return createInitialState(this, contractAddress, blockHash); + } + + async processStateCheckpoint (contractAddress: string, blockHash: string): Promise { + // Call checkpoint hook. + return createStateCheckpoint(this, contractAddress, blockHash); + } + + async processCanonicalBlock (blockHash: string, blockNumber: number): Promise { + console.time('time:indexer#processCanonicalBlock-finalize_auto_diffs'); + // Finalize staged diff blocks if any. + await this._baseIndexer.finalizeDiffStaged(blockHash); + console.timeEnd('time:indexer#processCanonicalBlock-finalize_auto_diffs'); + + // Call custom stateDiff hook. + await createStateDiff(this, blockHash); + + this._graphWatcher.pruneEntityCacheFrothyBlocks(blockHash, blockNumber); + } + + async processCheckpoint (blockHash: string): Promise { + // Return if checkpointInterval is <= 0. + const checkpointInterval = this._serverConfig.checkpointInterval; + if (checkpointInterval <= 0) return; + + console.time('time:indexer#processCheckpoint-checkpoint'); + await this._baseIndexer.processCheckpoint(this, blockHash, checkpointInterval); + console.timeEnd('time:indexer#processCheckpoint-checkpoint'); + } + + async processCLICheckpoint (contractAddress: string, blockHash?: string): Promise { + return this._baseIndexer.processCLICheckpoint(this, contractAddress, blockHash); + } + + async getPrevState (blockHash: string, contractAddress: string, kind?: string): Promise { + return this._db.getPrevState(blockHash, contractAddress, kind); + } + + async getLatestState (contractAddress: string, kind: StateKind | null, blockNumber?: number): Promise { + return this._db.getLatestState(contractAddress, kind, blockNumber); + } + + async getStatesByHash (blockHash: string): Promise { + return this._baseIndexer.getStatesByHash(blockHash); + } + + async getStateByCID (cid: string): Promise { + return this._baseIndexer.getStateByCID(cid); + } + + async getStates (where: FindConditions): Promise { + return this._db.getStates(where); + } + + getStateData (state: State): any { + return this._baseIndexer.getStateData(state); + } + + // Method used to create auto diffs (diff_staged). + async createDiffStaged (contractAddress: string, blockHash: string, data: any): Promise { + console.time('time:indexer#createDiffStaged-auto_diff'); + await this._baseIndexer.createDiffStaged(contractAddress, blockHash, data); + console.timeEnd('time:indexer#createDiffStaged-auto_diff'); + } + + // Method to be used by createStateDiff hook. + async createDiff (contractAddress: string, blockHash: string, data: any): Promise { + const block = await this.getBlockProgress(blockHash); + assert(block); + + await this._baseIndexer.createDiff(contractAddress, block, data); + } + + // Method to be used by createStateCheckpoint hook. + async createStateCheckpoint (contractAddress: string, blockHash: string, data: any): Promise { + const block = await this.getBlockProgress(blockHash); + assert(block); + + return this._baseIndexer.createStateCheckpoint(contractAddress, block, data); + } + + // Method to be used by export-state CLI. + async createCheckpoint (contractAddress: string, blockHash: string): Promise { + const block = await this.getBlockProgress(blockHash); + assert(block); + + return this._baseIndexer.createCheckpoint(this, contractAddress, block); + } + + // Method to be used by fill-state CLI. + async createInit (blockHash: string, blockNumber: number): Promise { + // Create initial state for contracts. + await this._baseIndexer.createInit(this, blockHash, blockNumber); + } + + async saveOrUpdateState (state: State): Promise { + return this._baseIndexer.saveOrUpdateState(state); + } + + async removeStates (blockNumber: number, kind: StateKind): Promise { + await this._baseIndexer.removeStates(blockNumber, kind); + } + + async getSubgraphEntity ( + entity: new () => Entity, + id: string, + block: BlockHeight, + queryInfo: GraphQLResolveInfo + ): Promise { + const data = await this._graphWatcher.getEntity(entity, id, this._relationsMap, block, queryInfo); + + return data; + } + + async getSubgraphEntities ( + entity: new () => Entity, + block: BlockHeight, + where: { [key: string]: any } = {}, + queryOptions: QueryOptions = {}, + queryInfo: GraphQLResolveInfo + ): Promise { + return this._graphWatcher.getEntities(entity, this._relationsMap, block, where, queryOptions, queryInfo); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async triggerIndexingOnEvent (event: Event, extraData: ExtraEventData): Promise { + const resultEvent = this.getResultEvent(event); + + console.time('time:indexer#processEvent-mapping_code'); + // Call subgraph handler for event. + await this._graphWatcher.handleEvent(resultEvent, extraData); + console.timeEnd('time:indexer#processEvent-mapping_code'); + + // Call custom hook function for indexing on event. + await handleEvent(this, resultEvent); + } + + async processEvent (event: Event, extraData: ExtraEventData): Promise { + // Trigger indexing of data based on the event. + await this.triggerIndexingOnEvent(event, extraData); + } + + async processBlock (blockProgress: BlockProgress): Promise { + console.time('time:indexer#processBlock-init_state'); + // Call a function to create initial state for contracts. + await this._baseIndexer.createInit(this, blockProgress.blockHash, blockProgress.blockNumber); + console.timeEnd('time:indexer#processBlock-init_state'); + + this._graphWatcher.updateEntityCacheFrothyBlocks(blockProgress); + } + + async processBlockAfterEvents (blockHash: string, blockNumber: number, extraData: ExtraEventData): Promise { + console.time('time:indexer#processBlockAfterEvents-mapping_code'); + // Call subgraph handler for block. + await this._graphWatcher.handleBlock(blockHash, blockNumber, extraData); + console.timeEnd('time:indexer#processBlockAfterEvents-mapping_code'); + + console.time('time:indexer#processBlockAfterEvents-dump_subgraph_state'); + // Persist subgraph state to the DB. + await this.dumpSubgraphState(blockHash); + console.timeEnd('time:indexer#processBlockAfterEvents-dump_subgraph_state'); + } + + parseEventNameAndArgs (kind: string, logObj: any): { eventParsed: boolean, eventDetails: any } { + const { topics, data } = logObj; + + const contract = this._contractMap.get(kind); + assert(contract); + + let logDescription: ethers.utils.LogDescription; + try { + logDescription = contract.parseLog({ data, topics }); + } catch (err) { + // Return if no matching event found + if ((err as Error).message.includes('no matching event')) { + log(`WARNING: Skipping event for contract ${kind} as no matching event found in the ABI`); + return { eventParsed: false, eventDetails: {} }; + } + + throw err; + } + + const { eventName, eventInfo, eventSignature } = this._baseIndexer.parseEvent(logDescription); + + return { + eventParsed: true, + eventDetails: { + eventName, + eventInfo, + eventSignature + } + }; + } + + async getStateSyncStatus (): Promise { + return this._db.getStateSyncStatus(); + } + + async updateStateSyncStatusIndexedBlock (blockNumber: number, force?: boolean): Promise { + if (!this._serverConfig.enableState) { + return; + } + + const dbTx = await this._db.createTransactionRunner(); + let res; + + try { + res = await this._db.updateStateSyncStatusIndexedBlock(dbTx, blockNumber, force); + await dbTx.commitTransaction(); + } catch (error) { + await dbTx.rollbackTransaction(); + throw error; + } finally { + await dbTx.release(); + } + + return res; + } + + async updateStateSyncStatusCheckpointBlock (blockNumber: number, force?: boolean): Promise { + const dbTx = await this._db.createTransactionRunner(); + let res; + + try { + res = await this._db.updateStateSyncStatusCheckpointBlock(dbTx, blockNumber, force); + await dbTx.commitTransaction(); + } catch (error) { + await dbTx.rollbackTransaction(); + throw error; + } finally { + await dbTx.release(); + } + + return res; + } + + async getLatestCanonicalBlock (): Promise { + const syncStatus = await this.getSyncStatus(); + assert(syncStatus); + + if (syncStatus.latestCanonicalBlockHash === constants.HashZero) { + return; + } + + const latestCanonicalBlock = await this.getBlockProgress(syncStatus.latestCanonicalBlockHash); + assert(latestCanonicalBlock); + + return latestCanonicalBlock; + } + + async getLatestStateIndexedBlock (): Promise { + return this._baseIndexer.getLatestStateIndexedBlock(); + } + + async addContracts (): Promise { + // Watching all the contracts in the subgraph. + await this._graphWatcher.addContracts(); + } + + async watchContract (address: string, kind: string, checkpoint: boolean, startingBlock: number, context?: any): Promise { + return this._baseIndexer.watchContract(address, kind, checkpoint, startingBlock, context); + } + + updateStateStatusMap (address: string, stateStatus: StateStatus): void { + this._baseIndexer.updateStateStatusMap(address, stateStatus); + } + + cacheContract (contract: Contract): void { + return this._baseIndexer.cacheContract(contract); + } + + async saveEventEntity (dbEvent: Event): Promise { + return this._baseIndexer.saveEventEntity(dbEvent); + } + + async saveEvents (dbEvents: Event[]): Promise { + return this._baseIndexer.saveEvents(dbEvents); + } + + async getEventsByFilter (blockHash: string, contract?: string, name?: string): Promise> { + return this._baseIndexer.getEventsByFilter(blockHash, contract, name); + } + + isWatchedContract (address : string): Contract | undefined { + return this._baseIndexer.isWatchedContract(address); + } + + getWatchedContracts (): Contract[] { + return this._baseIndexer.getWatchedContracts(); + } + + getContractsByKind (kind: string): Contract[] { + return this._baseIndexer.getContractsByKind(kind); + } + + async getProcessedBlockCountForRange (fromBlockNumber: number, toBlockNumber: number): Promise<{ expected: number, actual: number }> { + return this._baseIndexer.getProcessedBlockCountForRange(fromBlockNumber, toBlockNumber); + } + + async getEventsInRange (fromBlockNumber: number, toBlockNumber: number): Promise> { + return this._baseIndexer.getEventsInRange(fromBlockNumber, toBlockNumber, this._serverConfig.gql.maxEventsBlockRange); + } + + async getSyncStatus (): Promise { + return this._baseIndexer.getSyncStatus(); + } + + async getBlocks (blockFilter: { blockHash?: string, blockNumber?: number }): Promise { + return this._baseIndexer.getBlocks(blockFilter); + } + + async updateSyncStatusIndexedBlock (blockHash: string, blockNumber: number, force = false): Promise { + return this._baseIndexer.updateSyncStatusIndexedBlock(blockHash, blockNumber, force); + } + + async updateSyncStatusChainHead (blockHash: string, blockNumber: number, force = false): Promise { + return this._baseIndexer.updateSyncStatusChainHead(blockHash, blockNumber, force); + } + + async updateSyncStatusCanonicalBlock (blockHash: string, blockNumber: number, force = false): Promise { + const syncStatus = this._baseIndexer.updateSyncStatusCanonicalBlock(blockHash, blockNumber, force); + await this.pruneFrothyEntities(blockNumber); + + return syncStatus; + } + + async updateSyncStatusProcessedBlock (blockHash: string, blockNumber: number, force = false): Promise { + return this._baseIndexer.updateSyncStatusProcessedBlock(blockHash, blockNumber, force); + } + + async updateSyncStatusIndexingError (hasIndexingError: boolean): Promise { + return this._baseIndexer.updateSyncStatusIndexingError(hasIndexingError); + } + + async updateSyncStatus (syncStatus: DeepPartial): Promise { + return this._baseIndexer.updateSyncStatus(syncStatus); + } + + async getEvent (id: string): Promise { + return this._baseIndexer.getEvent(id); + } + + async getBlockProgress (blockHash: string): Promise { + return this._baseIndexer.getBlockProgress(blockHash); + } + + async getBlockProgressEntities (where: FindConditions, options: FindManyOptions): Promise { + return this._baseIndexer.getBlockProgressEntities(where, options); + } + + async getBlocksAtHeight (height: number, isPruned: boolean): Promise { + return this._baseIndexer.getBlocksAtHeight(height, isPruned); + } + + async fetchAndSaveFilteredEventsAndBlocks (startBlock: number, endBlock: number): Promise<{ + blockProgress: BlockProgress, + events: DeepPartial[], + ethFullBlock: EthFullBlock; + ethFullTransactions: EthFullTransaction[]; + }[]> { + return this._baseIndexer.fetchAndSaveFilteredEventsAndBlocks(startBlock, endBlock, this.eventSignaturesMap, this.parseEventNameAndArgs.bind(this)); + } + + async fetchEventsForContracts (blockHash: string, blockNumber: number, addresses: string[]): Promise[]> { + return this._baseIndexer.fetchEventsForContracts(blockHash, blockNumber, addresses, this.eventSignaturesMap, this.parseEventNameAndArgs.bind(this)); + } + + async saveBlockAndFetchEvents (block: DeepPartial): Promise<[ + BlockProgress, + DeepPartial[], + EthFullTransaction[] + ]> { + return this._saveBlockAndFetchEvents(block); + } + + async getBlockEvents (blockHash: string, where: Where, queryOptions: QueryOptions): Promise> { + return this._baseIndexer.getBlockEvents(blockHash, where, queryOptions); + } + + async removeUnknownEvents (block: BlockProgress): Promise { + return this._baseIndexer.removeUnknownEvents(Event, block); + } + + async markBlocksAsPruned (blocks: BlockProgress[]): Promise { + await this._baseIndexer.markBlocksAsPruned(blocks); + + await this._graphWatcher.pruneEntities(FrothyEntity, blocks, SUBGRAPH_ENTITIES); + } + + async pruneFrothyEntities (blockNumber: number): Promise { + await this._graphWatcher.pruneFrothyEntities(FrothyEntity, blockNumber); + } + + async resetLatestEntities (blockNumber: number): Promise { + await this._graphWatcher.resetLatestEntities(blockNumber); + } + + async updateBlockProgress (block: BlockProgress, lastProcessedEventIndex: number): Promise { + return this._baseIndexer.updateBlockProgress(block, lastProcessedEventIndex); + } + + async getAncestorAtHeight (blockHash: string, height: number): Promise { + return this._baseIndexer.getAncestorAtHeight(blockHash, height); + } + + async resetWatcherToBlock (blockNumber: number): Promise { + const entities = [...ENTITIES, FrothyEntity]; + await this._baseIndexer.resetWatcherToBlock(blockNumber, entities); + + await this.resetLatestEntities(blockNumber); + } + + async clearProcessedBlockData (block: BlockProgress): Promise { + const entities = [...ENTITIES, FrothyEntity]; + await this._baseIndexer.clearProcessedBlockData(block, entities); + + await this.resetLatestEntities(block.blockNumber); + } + + getEntityTypesMap (): Map { + return this._entityTypesMap; + } + + getRelationsMap (): Map { + return this._relationsMap; + } + + updateSubgraphState (contractAddress: string, data: any): void { + return updateSubgraphState(this._subgraphStateMap, contractAddress, data); + } + + async dumpSubgraphState (blockHash: string, isStateFinalized = false): Promise { + return dumpSubgraphState(this, this._subgraphStateMap, blockHash, isStateFinalized); + } + + _populateEntityTypesMap (): void { + this._entityTypesMap.set('Block', { + id: 'ID', + number: 'BigInt', + timestamp: 'BigInt', + parentHash: 'String', + author: 'String', + difficulty: 'BigInt', + totalDifficulty: 'BigInt', + gasUsed: 'BigInt', + gasLimit: 'BigInt', + receiptsRoot: 'String', + transactionsRoot: 'String', + stateRoot: 'String', + size: 'BigInt', + unclesHash: 'String' + }); + } + + // eslint-disable-next-line @typescript-eslint/no-empty-function + _populateRelationsMap (): void { + } + + async _saveBlockAndFetchEvents ({ + cid: blockCid, + blockHash, + blockNumber, + blockTimestamp, + parentHash + }: DeepPartial): Promise<[ + BlockProgress, + DeepPartial[], + EthFullTransaction[] + ]> { + assert(blockHash); + assert(blockNumber); + + const { events: dbEvents, transactions } = await this._baseIndexer.fetchEvents(blockHash, blockNumber, this.eventSignaturesMap, this.parseEventNameAndArgs.bind(this)); + + const dbTx = await this._db.createTransactionRunner(); + try { + const block = { + cid: blockCid, + blockHash, + blockNumber, + blockTimestamp, + parentHash + }; + + console.time(`time:indexer#_saveBlockAndFetchEvents-db-save-${blockNumber}`); + const blockProgress = await this._db.saveBlockWithEvents(dbTx, block, dbEvents); + await dbTx.commitTransaction(); + console.timeEnd(`time:indexer#_saveBlockAndFetchEvents-db-save-${blockNumber}`); + + return [blockProgress, [], transactions]; + } catch (error) { + await dbTx.rollbackTransaction(); + throw error; + } finally { + await dbTx.release(); + } + } + + async getFullTransactions (txHashList: string[]): Promise { + return this._baseIndexer.getFullTransactions(txHashList); + } +} diff --git a/packages/blocks-watcher/src/job-runner.ts b/packages/blocks-watcher/src/job-runner.ts new file mode 100644 index 0000000..93d6820 --- /dev/null +++ b/packages/blocks-watcher/src/job-runner.ts @@ -0,0 +1,48 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import debug from 'debug'; + +import { JobRunnerCmd } from '@cerc-io/cli'; +import { JobRunner } from '@cerc-io/util'; +import { getGraphDbAndWatcher } from '@cerc-io/graph-node'; + +import { Indexer } from './indexer'; +import { Database, ENTITY_QUERY_TYPE_MAP, ENTITY_TO_LATEST_ENTITY_MAP } from './database'; + +const log = debug('vulcanize:job-runner'); + +export const main = async (): Promise => { + const jobRunnerCmd = new JobRunnerCmd(); + await jobRunnerCmd.init(Database); + + const { graphWatcher } = await getGraphDbAndWatcher( + jobRunnerCmd.config.server, + jobRunnerCmd.clients.ethClient, + jobRunnerCmd.ethProvider, + jobRunnerCmd.database.baseDatabase, + ENTITY_QUERY_TYPE_MAP, + ENTITY_TO_LATEST_ENTITY_MAP + ); + + await jobRunnerCmd.initIndexer(Indexer, graphWatcher); + + await jobRunnerCmd.exec(async (jobRunner: JobRunner): Promise => { + await jobRunner.subscribeBlockProcessingQueue(); + await jobRunner.subscribeHistoricalProcessingQueue(); + await jobRunner.subscribeEventProcessingQueue(); + await jobRunner.subscribeBlockCheckpointQueue(); + await jobRunner.subscribeHooksQueue(); + }); +}; + +main().then(() => { + log('Starting job runner...'); +}).catch(err => { + log(err); +}); + +process.on('uncaughtException', err => { + log('uncaughtException', err); +}); diff --git a/packages/blocks-watcher/src/resolvers.ts b/packages/blocks-watcher/src/resolvers.ts new file mode 100644 index 0000000..c611715 --- /dev/null +++ b/packages/blocks-watcher/src/resolvers.ts @@ -0,0 +1,288 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import assert from 'assert'; +import debug from 'debug'; +import { GraphQLResolveInfo } from 'graphql'; +import { ExpressContext } from 'apollo-server-express'; +import winston from 'winston'; + +import { + gqlTotalQueryCount, + gqlQueryCount, + gqlQueryDuration, + getResultState, + IndexerInterface, + GraphQLBigInt, + GraphQLBigDecimal, + BlockHeight, + OrderDirection, + jsonBigIntStringReplacer, + EventWatcher, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + setGQLCacheHints +} from '@cerc-io/util'; + +import { Indexer } from './indexer'; + +import { Block } from './entity/Block'; + +const log = debug('vulcanize:resolver'); + +const executeAndRecordMetrics = async ( + indexer: Indexer, + gqlLogger: winston.Logger, + opName: string, + expressContext: ExpressContext, + operation: () => Promise +) => { + gqlTotalQueryCount.inc(1); + gqlQueryCount.labels(opName).inc(1); + const endTimer = gqlQueryDuration.labels(opName).startTimer(); + + try { + const [result, syncStatus] = await Promise.all([ + operation(), + indexer.getSyncStatus() + ]); + + gqlLogger.info({ + opName, + query: expressContext.req.body.query, + variables: expressContext.req.body.variables, + latestIndexedBlockNumber: syncStatus?.latestIndexedBlockNumber, + urlPath: expressContext.req.path, + apiKey: expressContext.req.header('x-api-key'), + origin: expressContext.req.headers.origin + }); + return result; + } catch (error) { + gqlLogger.error({ + opName, + error, + query: expressContext.req.body.query, + variables: expressContext.req.body.variables, + urlPath: expressContext.req.path, + apiKey: expressContext.req.header('x-api-key'), + origin: expressContext.req.headers.origin + }); + + throw error; + } finally { + endTimer(); + } +}; + +export const createResolvers = async ( + indexerArg: IndexerInterface, + eventWatcher: EventWatcher, + gqlLogger: winston.Logger +): Promise => { + const indexer = indexerArg as Indexer; + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const gqlCacheConfig = indexer.serverConfig.gql.cache; + + return { + BigInt: GraphQLBigInt, + + BigDecimal: GraphQLBigDecimal, + + Event: { + __resolveType: (obj: any) => { + assert(obj.__typename); + + return obj.__typename; + } + }, + + Subscription: { + onEvent: { + subscribe: () => eventWatcher.getEventIterator() + } + }, + + Mutation: { + watchContract: async (_: any, { address, kind, checkpoint, startingBlock = 1 }: { address: string, kind: string, checkpoint: boolean, startingBlock: number }): Promise => { + log('watchContract', address, kind, checkpoint, startingBlock); + await indexer.watchContract(address, kind, checkpoint, startingBlock); + + return true; + } + }, + + Query: { + block: async ( + _: any, + { id, block = {} }: { id: string, block: BlockHeight }, + expressContext: ExpressContext, + info: GraphQLResolveInfo + ) => { + log('block', id, JSON.stringify(block, jsonBigIntStringReplacer)); + + // Set cache-control hints + // setGQLCacheHints(info, block, gqlCacheConfig); + + return executeAndRecordMetrics( + indexer, + gqlLogger, + 'block', + expressContext, + async () => indexer.getSubgraphEntity(Block, id, block, info) + ); + }, + + blocks: async ( + _: any, + { block = {}, where, first, skip, orderBy, orderDirection }: { block: BlockHeight, where: { [key: string]: any }, first: number, skip: number, orderBy: string, orderDirection: OrderDirection }, + expressContext: ExpressContext, + info: GraphQLResolveInfo + ) => { + log('blocks', JSON.stringify(block, jsonBigIntStringReplacer), JSON.stringify(where, jsonBigIntStringReplacer), first, skip, orderBy, orderDirection); + + // Set cache-control hints + // setGQLCacheHints(info, block, gqlCacheConfig); + + return executeAndRecordMetrics( + indexer, + gqlLogger, + 'blocks', + expressContext, + async () => indexer.getSubgraphEntities( + Block, + block, + where, + { limit: first, skip, orderBy, orderDirection }, + info + ) + ); + }, + + events: async ( + _: any, + { blockHash, contractAddress, name }: { blockHash: string, contractAddress: string, name?: string }, + expressContext: ExpressContext + ) => { + log('events', blockHash, contractAddress, name); + + return executeAndRecordMetrics( + indexer, + gqlLogger, + 'events', + expressContext, + async () => { + const block = await indexer.getBlockProgress(blockHash); + if (!block || !block.isComplete) { + throw new Error(`Block hash ${blockHash} number ${block?.blockNumber} not processed yet`); + } + + const events = await indexer.getEventsByFilter(blockHash, contractAddress, name); + return events.map(event => indexer.getResultEvent(event)); + } + ); + }, + + eventsInRange: async ( + _: any, + { fromBlockNumber, toBlockNumber }: { fromBlockNumber: number, toBlockNumber: number }, + expressContext: ExpressContext + ) => { + log('eventsInRange', fromBlockNumber, toBlockNumber); + + return executeAndRecordMetrics( + indexer, + gqlLogger, + 'eventsInRange', + expressContext, + async () => { + const syncStatus = await indexer.getSyncStatus(); + + if (!syncStatus) { + throw new Error('No blocks processed yet'); + } + + if ((fromBlockNumber < syncStatus.initialIndexedBlockNumber) || (toBlockNumber > syncStatus.latestProcessedBlockNumber)) { + throw new Error(`Block range should be between ${syncStatus.initialIndexedBlockNumber} and ${syncStatus.latestProcessedBlockNumber}`); + } + + const events = await indexer.getEventsInRange(fromBlockNumber, toBlockNumber); + return events.map(event => indexer.getResultEvent(event)); + } + ); + }, + + getStateByCID: async ( + _: any, + { cid }: { cid: string }, + expressContext: ExpressContext + ) => { + log('getStateByCID', cid); + + return executeAndRecordMetrics( + indexer, + gqlLogger, + 'getStateByCID', + expressContext, + async () => { + const state = await indexer.getStateByCID(cid); + + return state && state.block.isComplete ? getResultState(state) : undefined; + } + ); + }, + + getState: async ( + _: any, + { blockHash, contractAddress, kind }: { blockHash: string, contractAddress: string, kind: string }, + expressContext: ExpressContext + ) => { + log('getState', blockHash, contractAddress, kind); + + return executeAndRecordMetrics( + indexer, + gqlLogger, + 'getState', + expressContext, + async () => { + const state = await indexer.getPrevState(blockHash, contractAddress, kind); + + return state && state.block.isComplete ? getResultState(state) : undefined; + } + ); + }, + + _meta: async ( + _: any, + { block = {} }: { block: BlockHeight }, + expressContext: ExpressContext + ) => { + log('_meta'); + + return executeAndRecordMetrics( + indexer, + gqlLogger, + '_meta', + expressContext, + async () => indexer.getMetaData(block) + ); + }, + + getSyncStatus: async ( + _: any, + __: Record, + expressContext: ExpressContext + ) => { + log('getSyncStatus'); + + return executeAndRecordMetrics( + indexer, + gqlLogger, + 'getSyncStatus', + expressContext, + async () => indexer.getSyncStatus() + ); + } + } + }; +}; diff --git a/packages/blocks-watcher/src/schema.gql b/packages/blocks-watcher/src/schema.gql new file mode 100644 index 0000000..9f22585 --- /dev/null +++ b/packages/blocks-watcher/src/schema.gql @@ -0,0 +1,337 @@ +directive @cacheControl(maxAge: Int, inheritMaxAge: Boolean, scope: CacheControlScope) on FIELD_DEFINITION | OBJECT | INTERFACE | UNION + +enum CacheControlScope { + PUBLIC + PRIVATE +} + +scalar BigInt + +scalar BigDecimal + +scalar Bytes + +type Proof { + data: String! +} + +type _Block_ { + cid: String + hash: String! + number: Int! + timestamp: Int! + parentHash: String! +} + +type _Transaction_ { + hash: String! + index: Int! + from: String! + to: String! +} + +type ResultEvent { + block: _Block_! + tx: _Transaction_! + contract: String! + eventIndex: Int! + event: Event! + proof: Proof +} + +union Event = PairCreatedEvent + +type PairCreatedEvent { + token0: String! + token1: String! + pair: String! + null: BigInt! +} + +input Block_height { + hash: Bytes + number: Int +} + +input BlockChangedFilter { + number_gte: Int! +} + +enum OrderDirection { + asc + desc +} + +enum Block_orderBy { + id + number + timestamp + parentHash + author + difficulty + totalDifficulty + gasUsed + gasLimit + receiptsRoot + transactionsRoot + stateRoot + size + unclesHash +} + +input Block_filter { + id: ID + id_not: ID + id_gt: ID + id_lt: ID + id_gte: ID + id_lte: ID + id_in: [ID!] + id_not_in: [ID!] + number: BigInt + number_not: BigInt + number_gt: BigInt + number_lt: BigInt + number_gte: BigInt + number_lte: BigInt + number_in: [BigInt!] + number_not_in: [BigInt!] + timestamp: BigInt + timestamp_not: BigInt + timestamp_gt: BigInt + timestamp_lt: BigInt + timestamp_gte: BigInt + timestamp_lte: BigInt + timestamp_in: [BigInt!] + timestamp_not_in: [BigInt!] + parentHash: String + parentHash_not: String + parentHash_gt: String + parentHash_lt: String + parentHash_gte: String + parentHash_lte: String + parentHash_in: [String!] + parentHash_not_in: [String!] + parentHash_starts_with: String + parentHash_starts_with_nocase: String + parentHash_not_starts_with: String + parentHash_not_starts_with_nocase: String + parentHash_ends_with: String + parentHash_ends_with_nocase: String + parentHash_not_ends_with: String + parentHash_not_ends_with_nocase: String + parentHash_contains: String + parentHash_not_contains: String + parentHash_contains_nocase: String + parentHash_not_contains_nocase: String + author: String + author_not: String + author_gt: String + author_lt: String + author_gte: String + author_lte: String + author_in: [String!] + author_not_in: [String!] + author_starts_with: String + author_starts_with_nocase: String + author_not_starts_with: String + author_not_starts_with_nocase: String + author_ends_with: String + author_ends_with_nocase: String + author_not_ends_with: String + author_not_ends_with_nocase: String + author_contains: String + author_not_contains: String + author_contains_nocase: String + author_not_contains_nocase: String + difficulty: BigInt + difficulty_not: BigInt + difficulty_gt: BigInt + difficulty_lt: BigInt + difficulty_gte: BigInt + difficulty_lte: BigInt + difficulty_in: [BigInt!] + difficulty_not_in: [BigInt!] + totalDifficulty: BigInt + totalDifficulty_not: BigInt + totalDifficulty_gt: BigInt + totalDifficulty_lt: BigInt + totalDifficulty_gte: BigInt + totalDifficulty_lte: BigInt + totalDifficulty_in: [BigInt!] + totalDifficulty_not_in: [BigInt!] + gasUsed: BigInt + gasUsed_not: BigInt + gasUsed_gt: BigInt + gasUsed_lt: BigInt + gasUsed_gte: BigInt + gasUsed_lte: BigInt + gasUsed_in: [BigInt!] + gasUsed_not_in: [BigInt!] + gasLimit: BigInt + gasLimit_not: BigInt + gasLimit_gt: BigInt + gasLimit_lt: BigInt + gasLimit_gte: BigInt + gasLimit_lte: BigInt + gasLimit_in: [BigInt!] + gasLimit_not_in: [BigInt!] + receiptsRoot: String + receiptsRoot_not: String + receiptsRoot_gt: String + receiptsRoot_lt: String + receiptsRoot_gte: String + receiptsRoot_lte: String + receiptsRoot_in: [String!] + receiptsRoot_not_in: [String!] + receiptsRoot_starts_with: String + receiptsRoot_starts_with_nocase: String + receiptsRoot_not_starts_with: String + receiptsRoot_not_starts_with_nocase: String + receiptsRoot_ends_with: String + receiptsRoot_ends_with_nocase: String + receiptsRoot_not_ends_with: String + receiptsRoot_not_ends_with_nocase: String + receiptsRoot_contains: String + receiptsRoot_not_contains: String + receiptsRoot_contains_nocase: String + receiptsRoot_not_contains_nocase: String + transactionsRoot: String + transactionsRoot_not: String + transactionsRoot_gt: String + transactionsRoot_lt: String + transactionsRoot_gte: String + transactionsRoot_lte: String + transactionsRoot_in: [String!] + transactionsRoot_not_in: [String!] + transactionsRoot_starts_with: String + transactionsRoot_starts_with_nocase: String + transactionsRoot_not_starts_with: String + transactionsRoot_not_starts_with_nocase: String + transactionsRoot_ends_with: String + transactionsRoot_ends_with_nocase: String + transactionsRoot_not_ends_with: String + transactionsRoot_not_ends_with_nocase: String + transactionsRoot_contains: String + transactionsRoot_not_contains: String + transactionsRoot_contains_nocase: String + transactionsRoot_not_contains_nocase: String + stateRoot: String + stateRoot_not: String + stateRoot_gt: String + stateRoot_lt: String + stateRoot_gte: String + stateRoot_lte: String + stateRoot_in: [String!] + stateRoot_not_in: [String!] + stateRoot_starts_with: String + stateRoot_starts_with_nocase: String + stateRoot_not_starts_with: String + stateRoot_not_starts_with_nocase: String + stateRoot_ends_with: String + stateRoot_ends_with_nocase: String + stateRoot_not_ends_with: String + stateRoot_not_ends_with_nocase: String + stateRoot_contains: String + stateRoot_not_contains: String + stateRoot_contains_nocase: String + stateRoot_not_contains_nocase: String + size: BigInt + size_not: BigInt + size_gt: BigInt + size_lt: BigInt + size_gte: BigInt + size_lte: BigInt + size_in: [BigInt!] + size_not_in: [BigInt!] + unclesHash: String + unclesHash_not: String + unclesHash_gt: String + unclesHash_lt: String + unclesHash_gte: String + unclesHash_lte: String + unclesHash_in: [String!] + unclesHash_not_in: [String!] + unclesHash_starts_with: String + unclesHash_starts_with_nocase: String + unclesHash_not_starts_with: String + unclesHash_not_starts_with_nocase: String + unclesHash_ends_with: String + unclesHash_ends_with_nocase: String + unclesHash_not_ends_with: String + unclesHash_not_ends_with_nocase: String + unclesHash_contains: String + unclesHash_not_contains: String + unclesHash_contains_nocase: String + unclesHash_not_contains_nocase: String + _change_block: BlockChangedFilter + and: [Block_filter] + or: [Block_filter] +} + +type _MetaBlock_ { + hash: Bytes + number: Int! + timestamp: Int +} + +type _Meta_ { + block: _MetaBlock_! + deployment: String! + hasIndexingErrors: Boolean! +} + +type ResultState { + block: _Block_! + contractAddress: String! + cid: String! + kind: String! + data: String! +} + +type SyncStatus { + latestIndexedBlockHash: String! + latestIndexedBlockNumber: Int! + latestCanonicalBlockHash: String! + latestCanonicalBlockNumber: Int! + initialIndexedBlockHash: String! + initialIndexedBlockNumber: Int! + latestProcessedBlockHash: String! + latestProcessedBlockNumber: Int! +} + +type Query { + events(blockHash: String!, contractAddress: String!, name: String): [ResultEvent!] + eventsInRange(fromBlockNumber: Int!, toBlockNumber: Int!): [ResultEvent!] + block(id: ID!, block: Block_height): Block + blocks(block: Block_height, where: Block_filter, orderBy: Block_orderBy, orderDirection: OrderDirection, first: Int = 100, skip: Int = 0): [Block!]! + _meta(block: Block_height): _Meta_ + getStateByCID(cid: String!): ResultState + getState(blockHash: String!, contractAddress: String!, kind: String): ResultState + getSyncStatus: SyncStatus +} + +type Block { + id: ID! + number: BigInt! + timestamp: BigInt! + parentHash: String + author: String + difficulty: BigInt + totalDifficulty: BigInt + gasUsed: BigInt + gasLimit: BigInt + receiptsRoot: String + transactionsRoot: String + stateRoot: String + size: BigInt + unclesHash: String +} + +type Mutation { + watchContract(address: String!, kind: String!, checkpoint: Boolean!, startingBlock: Int): Boolean! +} + +type Subscription { + onEvent: ResultEvent! +} diff --git a/packages/blocks-watcher/src/server.ts b/packages/blocks-watcher/src/server.ts new file mode 100644 index 0000000..679134f --- /dev/null +++ b/packages/blocks-watcher/src/server.ts @@ -0,0 +1,43 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import fs from 'fs'; +import path from 'path'; +import 'reflect-metadata'; +import debug from 'debug'; + +import { ServerCmd } from '@cerc-io/cli'; +import { getGraphDbAndWatcher } from '@cerc-io/graph-node'; + +import { createResolvers } from './resolvers'; +import { Indexer } from './indexer'; +import { Database, ENTITY_QUERY_TYPE_MAP, ENTITY_TO_LATEST_ENTITY_MAP } from './database'; + +const log = debug('vulcanize:server'); + +export const main = async (): Promise => { + const serverCmd = new ServerCmd(); + await serverCmd.init(Database); + + const { graphWatcher } = await getGraphDbAndWatcher( + serverCmd.config.server, + serverCmd.clients.ethClient, + serverCmd.ethProvider, + serverCmd.database.baseDatabase, + ENTITY_QUERY_TYPE_MAP, + ENTITY_TO_LATEST_ENTITY_MAP + ); + + await serverCmd.initIndexer(Indexer, graphWatcher); + + const typeDefs = fs.readFileSync(path.join(__dirname, 'schema.gql')).toString(); + + return serverCmd.exec(createResolvers, typeDefs); +}; + +main().then(() => { + log('Starting server...'); +}).catch(err => { + log(err); +}); diff --git a/packages/blocks-watcher/src/types.ts b/packages/blocks-watcher/src/types.ts new file mode 100644 index 0000000..c456217 --- /dev/null +++ b/packages/blocks-watcher/src/types.ts @@ -0,0 +1,3 @@ +// +// Copyright 2021 Vulcanize, Inc. +// diff --git a/packages/blocks-watcher/subgraph-build/UniswapV2Factory/UniswapV2Factory.wasm b/packages/blocks-watcher/subgraph-build/UniswapV2Factory/UniswapV2Factory.wasm new file mode 100644 index 0000000000000000000000000000000000000000..02dc46eb860c85b0a62e63ed59a2668c7de8607a GIT binary patch literal 186771 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