From 183efe849bfbaf3f800cf1e010badf22b2e3c6ed Mon Sep 17 00:00:00 2001 From: neeraj Date: Thu, 6 Jun 2024 18:24:45 +0530 Subject: [PATCH] Generate route processor watcher --- .../route-processor-watcher/.eslintignore | 2 + .../route-processor-watcher/.eslintrc.json | 28 + packages/route-processor-watcher/.gitignore | 7 + .../route-processor-watcher/.husky/pre-commit | 4 + packages/route-processor-watcher/.npmrc | 1 + packages/route-processor-watcher/LICENSE | 661 +++++++++++++++++ packages/route-processor-watcher/README.md | 212 ++++++ .../environments/local.toml | 109 +++ packages/route-processor-watcher/package.json | 76 ++ .../src/artifacts/RouteProcessor.json | 291 ++++++++ .../src/cli/checkpoint-cmds/create.ts | 44 ++ .../src/cli/checkpoint-cmds/verify.ts | 40 + .../src/cli/checkpoint.ts | 39 + .../src/cli/export-state.ts | 38 + .../src/cli/import-state.ts | 39 + .../src/cli/index-block.ts | 38 + .../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 + .../route-processor-watcher/src/cli/reset.ts | 24 + .../src/cli/watch-contract.ts | 38 + .../route-processor-watcher/src/client.ts | 55 ++ .../route-processor-watcher/src/database.ts | 287 ++++++++ .../src/entity/BlockProgress.ts | 48 ++ .../src/entity/Contract.ts | 27 + .../src/entity/Event.ts | 38 + .../src/entity/FrothyEntity.ts | 21 + .../src/entity/Route.ts | 49 ++ .../src/entity/RouteProcessor.ts | 31 + .../src/entity/State.ts | 31 + .../src/entity/StateSyncStatus.ts | 17 + .../src/entity/Subscriber.ts | 21 + .../src/entity/SyncStatus.ts | 45 ++ .../src/entity/User.ts | 28 + packages/route-processor-watcher/src/fill.ts | 48 ++ .../route-processor-watcher/src/gql/index.ts | 3 + .../src/gql/mutations/index.ts | 4 + .../src/gql/mutations/watchContract.gql | 3 + .../src/gql/queries/_meta.gql | 11 + .../src/gql/queries/events.gql | 37 + .../src/gql/queries/eventsInRange.gql | 37 + .../src/gql/queries/getState.gql | 15 + .../src/gql/queries/getStateByCID.gql | 15 + .../src/gql/queries/getSyncStatus.gql | 12 + .../src/gql/queries/index.ts | 15 + .../src/gql/queries/route.gql | 13 + .../src/gql/queries/routeProcessor.gql | 7 + .../src/gql/queries/routeProcessors.gql | 7 + .../src/gql/queries/routes.gql | 13 + .../src/gql/queries/user.gql | 6 + .../src/gql/queries/users.gql | 6 + .../src/gql/subscriptions/index.ts | 4 + .../src/gql/subscriptions/onEvent.gql | 37 + packages/route-processor-watcher/src/hooks.ts | 86 +++ .../route-processor-watcher/src/indexer.ts | 689 ++++++++++++++++++ .../route-processor-watcher/src/job-runner.ts | 48 ++ .../route-processor-watcher/src/resolvers.ts | 380 ++++++++++ .../route-processor-watcher/src/schema.gql | 309 ++++++++ .../route-processor-watcher/src/server.ts | 43 ++ packages/route-processor-watcher/src/types.ts | 3 + .../RouteProcessor/RouteProcessor.wasm | Bin 0 -> 189556 bytes .../RouteProcessor/abis/RouteProcessor3.json | 289 ++++++++ .../subgraph-build/schema.graphql | 38 + .../subgraph-build/subgraph.yaml | 24 + .../route-processor-watcher/tsconfig.json | 74 ++ 66 files changed, 4786 insertions(+) create mode 100644 packages/route-processor-watcher/.eslintignore create mode 100644 packages/route-processor-watcher/.eslintrc.json create mode 100644 packages/route-processor-watcher/.gitignore create mode 100644 packages/route-processor-watcher/.husky/pre-commit create mode 100644 packages/route-processor-watcher/.npmrc create mode 100644 packages/route-processor-watcher/LICENSE create mode 100644 packages/route-processor-watcher/README.md create mode 100644 packages/route-processor-watcher/environments/local.toml create mode 100644 packages/route-processor-watcher/package.json create mode 100644 packages/route-processor-watcher/src/artifacts/RouteProcessor.json create mode 100644 packages/route-processor-watcher/src/cli/checkpoint-cmds/create.ts create mode 100644 packages/route-processor-watcher/src/cli/checkpoint-cmds/verify.ts create mode 100644 packages/route-processor-watcher/src/cli/checkpoint.ts create mode 100644 packages/route-processor-watcher/src/cli/export-state.ts create mode 100644 packages/route-processor-watcher/src/cli/import-state.ts create mode 100644 packages/route-processor-watcher/src/cli/index-block.ts create mode 100644 packages/route-processor-watcher/src/cli/inspect-cid.ts create mode 100644 packages/route-processor-watcher/src/cli/reset-cmds/job-queue.ts create mode 100644 packages/route-processor-watcher/src/cli/reset-cmds/state.ts create mode 100644 packages/route-processor-watcher/src/cli/reset-cmds/watcher.ts create mode 100644 packages/route-processor-watcher/src/cli/reset.ts create mode 100644 packages/route-processor-watcher/src/cli/watch-contract.ts create mode 100644 packages/route-processor-watcher/src/client.ts create mode 100644 packages/route-processor-watcher/src/database.ts create mode 100644 packages/route-processor-watcher/src/entity/BlockProgress.ts create mode 100644 packages/route-processor-watcher/src/entity/Contract.ts create mode 100644 packages/route-processor-watcher/src/entity/Event.ts create mode 100644 packages/route-processor-watcher/src/entity/FrothyEntity.ts create mode 100644 packages/route-processor-watcher/src/entity/Route.ts create mode 100644 packages/route-processor-watcher/src/entity/RouteProcessor.ts create mode 100644 packages/route-processor-watcher/src/entity/State.ts create mode 100644 packages/route-processor-watcher/src/entity/StateSyncStatus.ts create mode 100644 packages/route-processor-watcher/src/entity/Subscriber.ts create mode 100644 packages/route-processor-watcher/src/entity/SyncStatus.ts create mode 100644 packages/route-processor-watcher/src/entity/User.ts create mode 100644 packages/route-processor-watcher/src/fill.ts create mode 100644 packages/route-processor-watcher/src/gql/index.ts create mode 100644 packages/route-processor-watcher/src/gql/mutations/index.ts create mode 100644 packages/route-processor-watcher/src/gql/mutations/watchContract.gql create mode 100644 packages/route-processor-watcher/src/gql/queries/_meta.gql create mode 100644 packages/route-processor-watcher/src/gql/queries/events.gql create mode 100644 packages/route-processor-watcher/src/gql/queries/eventsInRange.gql create mode 100644 packages/route-processor-watcher/src/gql/queries/getState.gql create mode 100644 packages/route-processor-watcher/src/gql/queries/getStateByCID.gql create mode 100644 packages/route-processor-watcher/src/gql/queries/getSyncStatus.gql create mode 100644 packages/route-processor-watcher/src/gql/queries/index.ts create mode 100644 packages/route-processor-watcher/src/gql/queries/route.gql create mode 100644 packages/route-processor-watcher/src/gql/queries/routeProcessor.gql create mode 100644 packages/route-processor-watcher/src/gql/queries/routeProcessors.gql create mode 100644 packages/route-processor-watcher/src/gql/queries/routes.gql create mode 100644 packages/route-processor-watcher/src/gql/queries/user.gql create mode 100644 packages/route-processor-watcher/src/gql/queries/users.gql create mode 100644 packages/route-processor-watcher/src/gql/subscriptions/index.ts create mode 100644 packages/route-processor-watcher/src/gql/subscriptions/onEvent.gql create mode 100644 packages/route-processor-watcher/src/hooks.ts create mode 100644 packages/route-processor-watcher/src/indexer.ts create mode 100644 packages/route-processor-watcher/src/job-runner.ts create mode 100644 packages/route-processor-watcher/src/resolvers.ts create mode 100644 packages/route-processor-watcher/src/schema.gql create mode 100644 packages/route-processor-watcher/src/server.ts create mode 100644 packages/route-processor-watcher/src/types.ts create mode 100644 packages/route-processor-watcher/subgraph-build/RouteProcessor/RouteProcessor.wasm create mode 100644 packages/route-processor-watcher/subgraph-build/RouteProcessor/abis/RouteProcessor3.json create mode 100644 packages/route-processor-watcher/subgraph-build/schema.graphql create mode 100644 packages/route-processor-watcher/subgraph-build/subgraph.yaml create mode 100644 packages/route-processor-watcher/tsconfig.json diff --git a/packages/route-processor-watcher/.eslintignore b/packages/route-processor-watcher/.eslintignore new file mode 100644 index 0000000..55cb522 --- /dev/null +++ b/packages/route-processor-watcher/.eslintignore @@ -0,0 +1,2 @@ +# Don't lint build output. +dist diff --git a/packages/route-processor-watcher/.eslintrc.json b/packages/route-processor-watcher/.eslintrc.json new file mode 100644 index 0000000..a2b842c --- /dev/null +++ b/packages/route-processor-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/route-processor-watcher/.gitignore b/packages/route-processor-watcher/.gitignore new file mode 100644 index 0000000..1c88d0a --- /dev/null +++ b/packages/route-processor-watcher/.gitignore @@ -0,0 +1,7 @@ +node_modules/ +dist/ +out/ +gql-logs/ + +.vscode +.idea diff --git a/packages/route-processor-watcher/.husky/pre-commit b/packages/route-processor-watcher/.husky/pre-commit new file mode 100644 index 0000000..9dcd433 --- /dev/null +++ b/packages/route-processor-watcher/.husky/pre-commit @@ -0,0 +1,4 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +yarn lint diff --git a/packages/route-processor-watcher/.npmrc b/packages/route-processor-watcher/.npmrc new file mode 100644 index 0000000..6b64c5b --- /dev/null +++ b/packages/route-processor-watcher/.npmrc @@ -0,0 +1 @@ +@cerc-io:registry=https://git.vdb.to/api/packages/cerc-io/npm/ diff --git a/packages/route-processor-watcher/LICENSE b/packages/route-processor-watcher/LICENSE new file mode 100644 index 0000000..331f7cf --- /dev/null +++ b/packages/route-processor-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/route-processor-watcher/README.md b/packages/route-processor-watcher/README.md new file mode 100644 index 0000000..3d7adcb --- /dev/null +++ b/packages/route-processor-watcher/README.md @@ -0,0 +1,212 @@ +# route-processor-watcher + +## Setup + +* Run the following command to install required packages: + + ```bash + yarn + ``` + +* Create a postgres12 database for the watcher: + + ```bash + sudo su - postgres + createdb route-processor-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 route-processor-watcher-job-queue + ``` + + ``` + postgres@tesla:~$ psql -U postgres -h localhost route-processor-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. + + route-processor-watcher-job-queue=# CREATE EXTENSION pgcrypto; + CREATE EXTENSION + route-processor-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/route-processor-watcher/environments/local.toml b/packages/route-processor-watcher/environments/local.toml new file mode 100644 index 0000000..973238e --- /dev/null +++ b/packages/route-processor-watcher/environments/local.toml @@ -0,0 +1,109 @@ +[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 = "route-processor-watcher" + username = "postgres" + password = "postgres" + synchronize = true + logging = false + +[upstream] + [upstream.ethServer] + gqlApiEndpoint = "http://127.0.0.1:8082/graphql" + 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 = false + + # Boolean flag to specify if rpcProviderEndpoint is an FEVM RPC endpoint + isFEVM = false + + # 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/route-processor-watcher-job-queue" + maxCompletionLagInSecs = 300 + jobDelayInMilliSecs = 100 + eventsInBatch = 50 + subgraphEventsOrder = true + blockDelayInMilliSecs = 2000 + + # 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/route-processor-watcher/package.json b/packages/route-processor-watcher/package.json new file mode 100644 index 0000000..74f67a3 --- /dev/null +++ b/packages/route-processor-watcher/package.json @@ -0,0 +1,76 @@ +{ + "name": "@cerc-io/route-processor-watcher", + "version": "0.1.0", + "description": "route-processor-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": "git+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.93", + "@cerc-io/ipld-eth-client": "^0.2.93", + "@cerc-io/solidity-mapper": "^0.2.93", + "@cerc-io/util": "^0.2.93", + "@cerc-io/graph-node": "^0.2.93", + "@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" + } +} diff --git a/packages/route-processor-watcher/src/artifacts/RouteProcessor.json b/packages/route-processor-watcher/src/artifacts/RouteProcessor.json new file mode 100644 index 0000000..6ec7033 --- /dev/null +++ b/packages/route-processor-watcher/src/artifacts/RouteProcessor.json @@ -0,0 +1,291 @@ +{ + "abi": [ + { + "inputs": [ + { + "internalType": "address", + "name": "_bentoBox", + "type": "address" + }, + { + "internalType": "address[]", + "name": "priviledgedUserList", + "type": "address[]" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "tokenIn", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "tokenOut", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amountIn", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amountOutMin", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amountOut", + "type": "uint256" + } + ], + "name": "Route", + "type": "event" + }, + { + "inputs": [], + "name": "bentoBox", + "outputs": [ + { + "internalType": "contract IBentoBoxMinimal", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "tokenIn", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amountIn", + "type": "uint256" + }, + { + "internalType": "address", + "name": "tokenOut", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amountOutMin", + "type": "uint256" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "bytes", + "name": "route", + "type": "bytes" + } + ], + "name": "processRoute", + "outputs": [ + { + "internalType": "uint256", + "name": "amountOut", + "type": "uint256" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [], + "name": "renounceOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "resume", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "internalType": "bool", + "name": "priviledge", + "type": "bool" + } + ], + "name": "setPriviledge", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address payable", + "name": "transferValueTo", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amountValueTransfer", + "type": "uint256" + }, + { + "internalType": "address", + "name": "tokenIn", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amountIn", + "type": "uint256" + }, + { + "internalType": "address", + "name": "tokenOut", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amountOutMin", + "type": "uint256" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "bytes", + "name": "route", + "type": "bytes" + } + ], + "name": "transferValueAndprocessRoute", + "outputs": [ + { + "internalType": "uint256", + "name": "amountOut", + "type": "uint256" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "int256", + "name": "amount0Delta", + "type": "int256" + }, + { + "internalType": "int256", + "name": "amount1Delta", + "type": "int256" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "uniswapV3SwapCallback", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "stateMutability": "payable", + "type": "receive" + } + ] +} \ No newline at end of file diff --git a/packages/route-processor-watcher/src/cli/checkpoint-cmds/create.ts b/packages/route-processor-watcher/src/cli/checkpoint-cmds/create.ts new file mode 100644 index 0000000..e771c70 --- /dev/null +++ b/packages/route-processor-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/route-processor-watcher/src/cli/checkpoint-cmds/verify.ts b/packages/route-processor-watcher/src/cli/checkpoint-cmds/verify.ts new file mode 100644 index 0000000..3709f54 --- /dev/null +++ b/packages/route-processor-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/route-processor-watcher/src/cli/checkpoint.ts b/packages/route-processor-watcher/src/cli/checkpoint.ts new file mode 100644 index 0000000..d05ad8a --- /dev/null +++ b/packages/route-processor-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/route-processor-watcher/src/cli/export-state.ts b/packages/route-processor-watcher/src/cli/export-state.ts new file mode 100644 index 0000000..bcd1c8a --- /dev/null +++ b/packages/route-processor-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/route-processor-watcher/src/cli/import-state.ts b/packages/route-processor-watcher/src/cli/import-state.ts new file mode 100644 index 0000000..04ce0e8 --- /dev/null +++ b/packages/route-processor-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/route-processor-watcher/src/cli/index-block.ts b/packages/route-processor-watcher/src/cli/index-block.ts new file mode 100644 index 0000000..19a302a --- /dev/null +++ b/packages/route-processor-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/route-processor-watcher/src/cli/inspect-cid.ts b/packages/route-processor-watcher/src/cli/inspect-cid.ts new file mode 100644 index 0000000..4f5955e --- /dev/null +++ b/packages/route-processor-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/route-processor-watcher/src/cli/reset-cmds/job-queue.ts b/packages/route-processor-watcher/src/cli/reset-cmds/job-queue.ts new file mode 100644 index 0000000..c33cbfd --- /dev/null +++ b/packages/route-processor-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/route-processor-watcher/src/cli/reset-cmds/state.ts b/packages/route-processor-watcher/src/cli/reset-cmds/state.ts new file mode 100644 index 0000000..33211d6 --- /dev/null +++ b/packages/route-processor-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/route-processor-watcher/src/cli/reset-cmds/watcher.ts b/packages/route-processor-watcher/src/cli/reset-cmds/watcher.ts new file mode 100644 index 0000000..827fd28 --- /dev/null +++ b/packages/route-processor-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/route-processor-watcher/src/cli/reset.ts b/packages/route-processor-watcher/src/cli/reset.ts new file mode 100644 index 0000000..95648c8 --- /dev/null +++ b/packages/route-processor-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/route-processor-watcher/src/cli/watch-contract.ts b/packages/route-processor-watcher/src/cli/watch-contract.ts new file mode 100644 index 0000000..7d6ce1a --- /dev/null +++ b/packages/route-processor-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/route-processor-watcher/src/client.ts b/packages/route-processor-watcher/src/client.ts new file mode 100644 index 0000000..8bb2bb0 --- /dev/null +++ b/packages/route-processor-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/route-processor-watcher/src/database.ts b/packages/route-processor-watcher/src/database.ts new file mode 100644 index 0000000..c057647 --- /dev/null +++ b/packages/route-processor-watcher/src/database.ts @@ -0,0 +1,287 @@ +// +// 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 { RouteProcessor } from './entity/RouteProcessor'; +import { Route } from './entity/Route'; +import { User } from './entity/User'; + +export const SUBGRAPH_ENTITIES = new Set([RouteProcessor, Route, User]); +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/route-processor-watcher/src/entity/BlockProgress.ts b/packages/route-processor-watcher/src/entity/BlockProgress.ts new file mode 100644 index 0000000..ded4a86 --- /dev/null +++ b/packages/route-processor-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/route-processor-watcher/src/entity/Contract.ts b/packages/route-processor-watcher/src/entity/Contract.ts new file mode 100644 index 0000000..e4defa8 --- /dev/null +++ b/packages/route-processor-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/route-processor-watcher/src/entity/Event.ts b/packages/route-processor-watcher/src/entity/Event.ts new file mode 100644 index 0000000..91f1e6d --- /dev/null +++ b/packages/route-processor-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/route-processor-watcher/src/entity/FrothyEntity.ts b/packages/route-processor-watcher/src/entity/FrothyEntity.ts new file mode 100644 index 0000000..9898ce8 --- /dev/null +++ b/packages/route-processor-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/route-processor-watcher/src/entity/Route.ts b/packages/route-processor-watcher/src/entity/Route.ts new file mode 100644 index 0000000..9a3697b --- /dev/null +++ b/packages/route-processor-watcher/src/entity/Route.ts @@ -0,0 +1,49 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import { Entity, PrimaryColumn, Column, Index } from 'typeorm'; +import { bigintTransformer } from '@cerc-io/util'; + +@Entity() +@Index(['blockNumber']) +export class Route { + @PrimaryColumn('varchar') + id!: string; + + @PrimaryColumn('varchar', { length: 66 }) + blockHash!: string; + + @Column('integer') + blockNumber!: number; + + @Column('varchar') + from!: string; + + @Column('varchar') + to!: string; + + @Column('varchar') + tokenIn!: string; + + @Column('varchar') + tokenOut!: string; + + @Column('numeric', { transformer: bigintTransformer }) + amountIn!: bigint; + + @Column('numeric', { transformer: bigintTransformer }) + amountOutMin!: bigint; + + @Column('numeric', { transformer: bigintTransformer }) + amountOut!: bigint; + + @Column('numeric', { transformer: bigintTransformer }) + timestamp!: bigint; + + @Column('boolean', { default: false }) + isPruned!: boolean; + + @Column('boolean', { default: false }) + isRemoved!: boolean; +} diff --git a/packages/route-processor-watcher/src/entity/RouteProcessor.ts b/packages/route-processor-watcher/src/entity/RouteProcessor.ts new file mode 100644 index 0000000..e0313e3 --- /dev/null +++ b/packages/route-processor-watcher/src/entity/RouteProcessor.ts @@ -0,0 +1,31 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import { Entity, PrimaryColumn, Column, Index } from 'typeorm'; +import { bigintTransformer } from '@cerc-io/util'; + +@Entity() +@Index(['blockNumber']) +export class RouteProcessor { + @PrimaryColumn('varchar') + id!: string; + + @PrimaryColumn('varchar', { length: 66 }) + blockHash!: string; + + @Column('integer') + blockNumber!: number; + + @Column('numeric', { transformer: bigintTransformer }) + routeCount!: bigint; + + @Column('numeric', { transformer: bigintTransformer }) + userCount!: bigint; + + @Column('boolean', { default: false }) + isPruned!: boolean; + + @Column('boolean', { default: false }) + isRemoved!: boolean; +} diff --git a/packages/route-processor-watcher/src/entity/State.ts b/packages/route-processor-watcher/src/entity/State.ts new file mode 100644 index 0000000..bc05bca --- /dev/null +++ b/packages/route-processor-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/route-processor-watcher/src/entity/StateSyncStatus.ts b/packages/route-processor-watcher/src/entity/StateSyncStatus.ts new file mode 100644 index 0000000..1535eb4 --- /dev/null +++ b/packages/route-processor-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/route-processor-watcher/src/entity/Subscriber.ts b/packages/route-processor-watcher/src/entity/Subscriber.ts new file mode 100644 index 0000000..2cccb84 --- /dev/null +++ b/packages/route-processor-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/route-processor-watcher/src/entity/SyncStatus.ts b/packages/route-processor-watcher/src/entity/SyncStatus.ts new file mode 100644 index 0000000..cc13c70 --- /dev/null +++ b/packages/route-processor-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/route-processor-watcher/src/entity/User.ts b/packages/route-processor-watcher/src/entity/User.ts new file mode 100644 index 0000000..1458585 --- /dev/null +++ b/packages/route-processor-watcher/src/entity/User.ts @@ -0,0 +1,28 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import { Entity, PrimaryColumn, Column, Index } from 'typeorm'; +import { bigintTransformer } from '@cerc-io/util'; + +@Entity() +@Index(['blockNumber']) +export class User { + @PrimaryColumn('varchar') + id!: string; + + @PrimaryColumn('varchar', { length: 66 }) + blockHash!: string; + + @Column('integer') + blockNumber!: number; + + @Column('numeric', { transformer: bigintTransformer }) + routeCount!: bigint; + + @Column('boolean', { default: false }) + isPruned!: boolean; + + @Column('boolean', { default: false }) + isRemoved!: boolean; +} diff --git a/packages/route-processor-watcher/src/fill.ts b/packages/route-processor-watcher/src/fill.ts new file mode 100644 index 0000000..210341e --- /dev/null +++ b/packages/route-processor-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/route-processor-watcher/src/gql/index.ts b/packages/route-processor-watcher/src/gql/index.ts new file mode 100644 index 0000000..4732f68 --- /dev/null +++ b/packages/route-processor-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/route-processor-watcher/src/gql/mutations/index.ts b/packages/route-processor-watcher/src/gql/mutations/index.ts new file mode 100644 index 0000000..0c3bd85 --- /dev/null +++ b/packages/route-processor-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/route-processor-watcher/src/gql/mutations/watchContract.gql b/packages/route-processor-watcher/src/gql/mutations/watchContract.gql new file mode 100644 index 0000000..2ecc74f --- /dev/null +++ b/packages/route-processor-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/route-processor-watcher/src/gql/queries/_meta.gql b/packages/route-processor-watcher/src/gql/queries/_meta.gql new file mode 100644 index 0000000..d686e04 --- /dev/null +++ b/packages/route-processor-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/route-processor-watcher/src/gql/queries/events.gql b/packages/route-processor-watcher/src/gql/queries/events.gql new file mode 100644 index 0000000..74a988c --- /dev/null +++ b/packages/route-processor-watcher/src/gql/queries/events.gql @@ -0,0 +1,37 @@ +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 OwnershipTransferredEvent { + previousOwner + newOwner + } + ... on RouteEvent { + from + to + tokenIn + tokenOut + amountIn + amountOutMin + amountOut + } + } + proof{ + data + } + } +} \ No newline at end of file diff --git a/packages/route-processor-watcher/src/gql/queries/eventsInRange.gql b/packages/route-processor-watcher/src/gql/queries/eventsInRange.gql new file mode 100644 index 0000000..9f0d636 --- /dev/null +++ b/packages/route-processor-watcher/src/gql/queries/eventsInRange.gql @@ -0,0 +1,37 @@ +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 OwnershipTransferredEvent { + previousOwner + newOwner + } + ... on RouteEvent { + from + to + tokenIn + tokenOut + amountIn + amountOutMin + amountOut + } + } + proof{ + data + } + } +} \ No newline at end of file diff --git a/packages/route-processor-watcher/src/gql/queries/getState.gql b/packages/route-processor-watcher/src/gql/queries/getState.gql new file mode 100644 index 0000000..3b8f605 --- /dev/null +++ b/packages/route-processor-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/route-processor-watcher/src/gql/queries/getStateByCID.gql b/packages/route-processor-watcher/src/gql/queries/getStateByCID.gql new file mode 100644 index 0000000..6c3c4fd --- /dev/null +++ b/packages/route-processor-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/route-processor-watcher/src/gql/queries/getSyncStatus.gql b/packages/route-processor-watcher/src/gql/queries/getSyncStatus.gql new file mode 100644 index 0000000..48175b4 --- /dev/null +++ b/packages/route-processor-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/route-processor-watcher/src/gql/queries/index.ts b/packages/route-processor-watcher/src/gql/queries/index.ts new file mode 100644 index 0000000..fb7362a --- /dev/null +++ b/packages/route-processor-watcher/src/gql/queries/index.ts @@ -0,0 +1,15 @@ +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 routeProcessor = fs.readFileSync(path.join(__dirname, 'routeProcessor.gql'), 'utf8'); +export const routeProcessors = fs.readFileSync(path.join(__dirname, 'routeProcessors.gql'), 'utf8'); +export const route = fs.readFileSync(path.join(__dirname, 'route.gql'), 'utf8'); +export const routes = fs.readFileSync(path.join(__dirname, 'routes.gql'), 'utf8'); +export const user = fs.readFileSync(path.join(__dirname, 'user.gql'), 'utf8'); +export const users = fs.readFileSync(path.join(__dirname, 'users.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/route-processor-watcher/src/gql/queries/route.gql b/packages/route-processor-watcher/src/gql/queries/route.gql new file mode 100644 index 0000000..0f9882c --- /dev/null +++ b/packages/route-processor-watcher/src/gql/queries/route.gql @@ -0,0 +1,13 @@ +query route($id: ID!, $block: Block_height){ + route(id: $id, block: $block){ + id + from + to + tokenIn + tokenOut + amountIn + amountOutMin + amountOut + timestamp + } +} \ No newline at end of file diff --git a/packages/route-processor-watcher/src/gql/queries/routeProcessor.gql b/packages/route-processor-watcher/src/gql/queries/routeProcessor.gql new file mode 100644 index 0000000..2a4c91a --- /dev/null +++ b/packages/route-processor-watcher/src/gql/queries/routeProcessor.gql @@ -0,0 +1,7 @@ +query routeProcessor($id: ID!, $block: Block_height){ + routeProcessor(id: $id, block: $block){ + id + routeCount + userCount + } +} \ No newline at end of file diff --git a/packages/route-processor-watcher/src/gql/queries/routeProcessors.gql b/packages/route-processor-watcher/src/gql/queries/routeProcessors.gql new file mode 100644 index 0000000..bde6ff2 --- /dev/null +++ b/packages/route-processor-watcher/src/gql/queries/routeProcessors.gql @@ -0,0 +1,7 @@ +query routeProcessors($block: Block_height, $where: RouteProcessor_filter, $orderBy: RouteProcessor_orderBy, $orderDirection: OrderDirection, $first: Int, $skip: Int){ + routeProcessors(block: $block, where: $where, orderBy: $orderBy, orderDirection: $orderDirection, first: $first, skip: $skip){ + id + routeCount + userCount + } +} \ No newline at end of file diff --git a/packages/route-processor-watcher/src/gql/queries/routes.gql b/packages/route-processor-watcher/src/gql/queries/routes.gql new file mode 100644 index 0000000..ad320fc --- /dev/null +++ b/packages/route-processor-watcher/src/gql/queries/routes.gql @@ -0,0 +1,13 @@ +query routes($block: Block_height, $where: Route_filter, $orderBy: Route_orderBy, $orderDirection: OrderDirection, $first: Int, $skip: Int){ + routes(block: $block, where: $where, orderBy: $orderBy, orderDirection: $orderDirection, first: $first, skip: $skip){ + id + from + to + tokenIn + tokenOut + amountIn + amountOutMin + amountOut + timestamp + } +} \ No newline at end of file diff --git a/packages/route-processor-watcher/src/gql/queries/user.gql b/packages/route-processor-watcher/src/gql/queries/user.gql new file mode 100644 index 0000000..6723b71 --- /dev/null +++ b/packages/route-processor-watcher/src/gql/queries/user.gql @@ -0,0 +1,6 @@ +query user($id: ID!, $block: Block_height){ + user(id: $id, block: $block){ + id + routeCount + } +} \ No newline at end of file diff --git a/packages/route-processor-watcher/src/gql/queries/users.gql b/packages/route-processor-watcher/src/gql/queries/users.gql new file mode 100644 index 0000000..d058f7c --- /dev/null +++ b/packages/route-processor-watcher/src/gql/queries/users.gql @@ -0,0 +1,6 @@ +query users($block: Block_height, $where: User_filter, $orderBy: User_orderBy, $orderDirection: OrderDirection, $first: Int, $skip: Int){ + users(block: $block, where: $where, orderBy: $orderBy, orderDirection: $orderDirection, first: $first, skip: $skip){ + id + routeCount + } +} \ No newline at end of file diff --git a/packages/route-processor-watcher/src/gql/subscriptions/index.ts b/packages/route-processor-watcher/src/gql/subscriptions/index.ts new file mode 100644 index 0000000..f12910c --- /dev/null +++ b/packages/route-processor-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/route-processor-watcher/src/gql/subscriptions/onEvent.gql b/packages/route-processor-watcher/src/gql/subscriptions/onEvent.gql new file mode 100644 index 0000000..e8340df --- /dev/null +++ b/packages/route-processor-watcher/src/gql/subscriptions/onEvent.gql @@ -0,0 +1,37 @@ +subscription onEvent{ + onEvent{ + block{ + cid + hash + number + timestamp + parentHash + } + tx{ + hash + index + from + to + } + contract + eventIndex + event{ + ... on OwnershipTransferredEvent { + previousOwner + newOwner + } + ... on RouteEvent { + from + to + tokenIn + tokenOut + amountIn + amountOutMin + amountOut + } + } + proof{ + data + } + } +} \ No newline at end of file diff --git a/packages/route-processor-watcher/src/hooks.ts b/packages/route-processor-watcher/src/hooks.ts new file mode 100644 index 0000000..d45498b --- /dev/null +++ b/packages/route-processor-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/route-processor-watcher/src/indexer.ts b/packages/route-processor-watcher/src/indexer.ts new file mode 100644 index 0000000..006737e --- /dev/null +++ b/packages/route-processor-watcher/src/indexer.ts @@ -0,0 +1,689 @@ +// +// 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 RouteProcessorArtifacts from './artifacts/RouteProcessor.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 { RouteProcessor } from './entity/RouteProcessor'; +import { Route } from './entity/Route'; +import { User } from './entity/User'; +/* 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_ROUTEPROCESSOR = 'RouteProcessor'; + +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: RouteProcessorABI } = RouteProcessorArtifacts; + + assert(RouteProcessorABI); + this._abiMap.set(KIND_ROUTEPROCESSOR, RouteProcessorABI); + + const RouteProcessorContractInterface = new ethers.utils.Interface(RouteProcessorABI); + this._contractMap.set(KIND_ROUTEPROCESSOR, RouteProcessorContractInterface); + + const RouteProcessorEventSignatures = Object.values(RouteProcessorContractInterface.events).map(value => { + return RouteProcessorContractInterface.getEventTopic(value); + }); + this.eventSignaturesMap.set(KIND_ROUTEPROCESSOR, RouteProcessorEventSignatures); + + 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('RouteProcessor', { + id: 'ID', + routeCount: 'BigInt', + userCount: 'BigInt' + }); + this._entityTypesMap.set('Route', { + id: 'ID', + from: 'Bytes', + to: 'Bytes', + tokenIn: 'Bytes', + tokenOut: 'Bytes', + amountIn: 'BigInt', + amountOutMin: 'BigInt', + amountOut: 'BigInt', + timestamp: 'BigInt' + }); + this._entityTypesMap.set('User', { + id: 'ID', + routeCount: 'BigInt' + }); + } + + // 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/route-processor-watcher/src/job-runner.ts b/packages/route-processor-watcher/src/job-runner.ts new file mode 100644 index 0000000..93d6820 --- /dev/null +++ b/packages/route-processor-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/route-processor-watcher/src/resolvers.ts b/packages/route-processor-watcher/src/resolvers.ts new file mode 100644 index 0000000..0f72ba2 --- /dev/null +++ b/packages/route-processor-watcher/src/resolvers.ts @@ -0,0 +1,380 @@ +// +// 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 { RouteProcessor } from './entity/RouteProcessor'; +import { Route } from './entity/Route'; +import { User } from './entity/User'; + +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 + }); + } 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: { + routeProcessor: async ( + _: any, + { id, block = {} }: { id: string, block: BlockHeight }, + expressContext: ExpressContext, + info: GraphQLResolveInfo + ) => { + log('routeProcessor', id, JSON.stringify(block, jsonBigIntStringReplacer)); + + // Set cache-control hints + // setGQLCacheHints(info, block, gqlCacheConfig); + + return executeAndRecordMetrics( + indexer, + gqlLogger, + 'routeProcessor', + expressContext, + async () => indexer.getSubgraphEntity(RouteProcessor, id, block, info) + ); + }, + + routeProcessors: 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('routeProcessors', JSON.stringify(block, jsonBigIntStringReplacer), JSON.stringify(where, jsonBigIntStringReplacer), first, skip, orderBy, orderDirection); + + // Set cache-control hints + // setGQLCacheHints(info, block, gqlCacheConfig); + + return executeAndRecordMetrics( + indexer, + gqlLogger, + 'routeProcessors', + expressContext, + async () => indexer.getSubgraphEntities( + RouteProcessor, + block, + where, + { limit: first, skip, orderBy, orderDirection }, + info + ) + ); + }, + + route: async ( + _: any, + { id, block = {} }: { id: string, block: BlockHeight }, + expressContext: ExpressContext, + info: GraphQLResolveInfo + ) => { + log('route', id, JSON.stringify(block, jsonBigIntStringReplacer)); + + // Set cache-control hints + // setGQLCacheHints(info, block, gqlCacheConfig); + + return executeAndRecordMetrics( + indexer, + gqlLogger, + 'route', + expressContext, + async () => indexer.getSubgraphEntity(Route, id, block, info) + ); + }, + + routes: 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('routes', JSON.stringify(block, jsonBigIntStringReplacer), JSON.stringify(where, jsonBigIntStringReplacer), first, skip, orderBy, orderDirection); + + // Set cache-control hints + // setGQLCacheHints(info, block, gqlCacheConfig); + + return executeAndRecordMetrics( + indexer, + gqlLogger, + 'routes', + expressContext, + async () => indexer.getSubgraphEntities( + Route, + block, + where, + { limit: first, skip, orderBy, orderDirection }, + info + ) + ); + }, + + user: async ( + _: any, + { id, block = {} }: { id: string, block: BlockHeight }, + expressContext: ExpressContext, + info: GraphQLResolveInfo + ) => { + log('user', id, JSON.stringify(block, jsonBigIntStringReplacer)); + + // Set cache-control hints + // setGQLCacheHints(info, block, gqlCacheConfig); + + return executeAndRecordMetrics( + indexer, + gqlLogger, + 'user', + expressContext, + async () => indexer.getSubgraphEntity(User, id, block, info) + ); + }, + + users: 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('users', JSON.stringify(block, jsonBigIntStringReplacer), JSON.stringify(where, jsonBigIntStringReplacer), first, skip, orderBy, orderDirection); + + // Set cache-control hints + // setGQLCacheHints(info, block, gqlCacheConfig); + + return executeAndRecordMetrics( + indexer, + gqlLogger, + 'users', + expressContext, + async () => indexer.getSubgraphEntities( + User, + 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/route-processor-watcher/src/schema.gql b/packages/route-processor-watcher/src/schema.gql new file mode 100644 index 0000000..0df7c5e --- /dev/null +++ b/packages/route-processor-watcher/src/schema.gql @@ -0,0 +1,309 @@ +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 = OwnershipTransferredEvent | RouteEvent + +type OwnershipTransferredEvent { + previousOwner: String! + newOwner: String! +} + +type RouteEvent { + from: String! + to: String! + tokenIn: String! + tokenOut: String! + amountIn: BigInt! + amountOutMin: BigInt! + amountOut: BigInt! +} + +input Block_height { + hash: Bytes + number: Int +} + +input BlockChangedFilter { + number_gte: Int! +} + +enum OrderDirection { + asc + desc +} + +enum RouteProcessor_orderBy { + id + routeCount + userCount +} + +input RouteProcessor_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!] + routeCount: BigInt + routeCount_not: BigInt + routeCount_gt: BigInt + routeCount_lt: BigInt + routeCount_gte: BigInt + routeCount_lte: BigInt + routeCount_in: [BigInt!] + routeCount_not_in: [BigInt!] + userCount: BigInt + userCount_not: BigInt + userCount_gt: BigInt + userCount_lt: BigInt + userCount_gte: BigInt + userCount_lte: BigInt + userCount_in: [BigInt!] + userCount_not_in: [BigInt!] + _change_block: BlockChangedFilter + and: [RouteProcessor_filter] + or: [RouteProcessor_filter] +} + +enum Route_orderBy { + id + from + to + tokenIn + tokenOut + amountIn + amountOutMin + amountOut + timestamp +} + +input Route_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!] + from: Bytes + from_not: Bytes + from_gt: Bytes + from_lt: Bytes + from_gte: Bytes + from_lte: Bytes + from_in: [Bytes!] + from_not_in: [Bytes!] + from_contains: Bytes + from_not_contains: Bytes + to: Bytes + to_not: Bytes + to_gt: Bytes + to_lt: Bytes + to_gte: Bytes + to_lte: Bytes + to_in: [Bytes!] + to_not_in: [Bytes!] + to_contains: Bytes + to_not_contains: Bytes + tokenIn: Bytes + tokenIn_not: Bytes + tokenIn_gt: Bytes + tokenIn_lt: Bytes + tokenIn_gte: Bytes + tokenIn_lte: Bytes + tokenIn_in: [Bytes!] + tokenIn_not_in: [Bytes!] + tokenIn_contains: Bytes + tokenIn_not_contains: Bytes + tokenOut: Bytes + tokenOut_not: Bytes + tokenOut_gt: Bytes + tokenOut_lt: Bytes + tokenOut_gte: Bytes + tokenOut_lte: Bytes + tokenOut_in: [Bytes!] + tokenOut_not_in: [Bytes!] + tokenOut_contains: Bytes + tokenOut_not_contains: Bytes + amountIn: BigInt + amountIn_not: BigInt + amountIn_gt: BigInt + amountIn_lt: BigInt + amountIn_gte: BigInt + amountIn_lte: BigInt + amountIn_in: [BigInt!] + amountIn_not_in: [BigInt!] + amountOutMin: BigInt + amountOutMin_not: BigInt + amountOutMin_gt: BigInt + amountOutMin_lt: BigInt + amountOutMin_gte: BigInt + amountOutMin_lte: BigInt + amountOutMin_in: [BigInt!] + amountOutMin_not_in: [BigInt!] + amountOut: BigInt + amountOut_not: BigInt + amountOut_gt: BigInt + amountOut_lt: BigInt + amountOut_gte: BigInt + amountOut_lte: BigInt + amountOut_in: [BigInt!] + amountOut_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!] + _change_block: BlockChangedFilter + and: [Route_filter] + or: [Route_filter] +} + +enum User_orderBy { + id + routeCount +} + +input User_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!] + routeCount: BigInt + routeCount_not: BigInt + routeCount_gt: BigInt + routeCount_lt: BigInt + routeCount_gte: BigInt + routeCount_lte: BigInt + routeCount_in: [BigInt!] + routeCount_not_in: [BigInt!] + _change_block: BlockChangedFilter + and: [User_filter] + or: [User_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!] + routeProcessor(id: ID!, block: Block_height): RouteProcessor + routeProcessors(block: Block_height, where: RouteProcessor_filter, orderBy: RouteProcessor_orderBy, orderDirection: OrderDirection, first: Int = 100, skip: Int = 0): [RouteProcessor!]! + route(id: ID!, block: Block_height): Route + routes(block: Block_height, where: Route_filter, orderBy: Route_orderBy, orderDirection: OrderDirection, first: Int = 100, skip: Int = 0): [Route!]! + user(id: ID!, block: Block_height): User + users(block: Block_height, where: User_filter, orderBy: User_orderBy, orderDirection: OrderDirection, first: Int = 100, skip: Int = 0): [User!]! + _meta(block: Block_height): _Meta_ + getStateByCID(cid: String!): ResultState + getState(blockHash: String!, contractAddress: String!, kind: String): ResultState + getSyncStatus: SyncStatus +} + +type RouteProcessor { + id: ID! + routeCount: BigInt! + userCount: BigInt! +} + +type Route { + id: ID! + from: Bytes! + to: Bytes! + tokenIn: Bytes! + tokenOut: Bytes! + amountIn: BigInt! + amountOutMin: BigInt! + amountOut: BigInt! + timestamp: BigInt! +} + +type User { + id: ID! + routeCount: BigInt! +} + +type Mutation { + watchContract(address: String!, kind: String!, checkpoint: Boolean!, startingBlock: Int): Boolean! +} + +type Subscription { + onEvent: ResultEvent! +} diff --git a/packages/route-processor-watcher/src/server.ts b/packages/route-processor-watcher/src/server.ts new file mode 100644 index 0000000..679134f --- /dev/null +++ b/packages/route-processor-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/route-processor-watcher/src/types.ts b/packages/route-processor-watcher/src/types.ts new file mode 100644 index 0000000..c456217 --- /dev/null +++ b/packages/route-processor-watcher/src/types.ts @@ -0,0 +1,3 @@ +// +// Copyright 2021 Vulcanize, Inc. +// diff --git a/packages/route-processor-watcher/subgraph-build/RouteProcessor/RouteProcessor.wasm b/packages/route-processor-watcher/subgraph-build/RouteProcessor/RouteProcessor.wasm new file mode 100644 index 0000000000000000000000000000000000000000..b0fa247bee31dbbe251e0fbfe57b29f149468cdb GIT binary patch literal 189556 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 literal 0 HcmV?d00001 diff --git a/packages/route-processor-watcher/subgraph-build/RouteProcessor/abis/RouteProcessor3.json b/packages/route-processor-watcher/subgraph-build/RouteProcessor/abis/RouteProcessor3.json new file mode 100644 index 0000000..2595a54 --- /dev/null +++ b/packages/route-processor-watcher/subgraph-build/RouteProcessor/abis/RouteProcessor3.json @@ -0,0 +1,289 @@ +[ + { + "inputs": [ + { + "internalType": "address", + "name": "_bentoBox", + "type": "address" + }, + { + "internalType": "address[]", + "name": "priviledgedUserList", + "type": "address[]" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "tokenIn", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "tokenOut", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amountIn", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amountOutMin", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amountOut", + "type": "uint256" + } + ], + "name": "Route", + "type": "event" + }, + { + "inputs": [], + "name": "bentoBox", + "outputs": [ + { + "internalType": "contract IBentoBoxMinimal", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "tokenIn", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amountIn", + "type": "uint256" + }, + { + "internalType": "address", + "name": "tokenOut", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amountOutMin", + "type": "uint256" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "bytes", + "name": "route", + "type": "bytes" + } + ], + "name": "processRoute", + "outputs": [ + { + "internalType": "uint256", + "name": "amountOut", + "type": "uint256" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [], + "name": "renounceOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "resume", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "internalType": "bool", + "name": "priviledge", + "type": "bool" + } + ], + "name": "setPriviledge", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address payable", + "name": "transferValueTo", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amountValueTransfer", + "type": "uint256" + }, + { + "internalType": "address", + "name": "tokenIn", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amountIn", + "type": "uint256" + }, + { + "internalType": "address", + "name": "tokenOut", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amountOutMin", + "type": "uint256" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "bytes", + "name": "route", + "type": "bytes" + } + ], + "name": "transferValueAndprocessRoute", + "outputs": [ + { + "internalType": "uint256", + "name": "amountOut", + "type": "uint256" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "int256", + "name": "amount0Delta", + "type": "int256" + }, + { + "internalType": "int256", + "name": "amount1Delta", + "type": "int256" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "uniswapV3SwapCallback", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "stateMutability": "payable", + "type": "receive" + } +] \ No newline at end of file diff --git a/packages/route-processor-watcher/subgraph-build/schema.graphql b/packages/route-processor-watcher/subgraph-build/schema.graphql new file mode 100644 index 0000000..531523d --- /dev/null +++ b/packages/route-processor-watcher/subgraph-build/schema.graphql @@ -0,0 +1,38 @@ +type RouteProcessor @entity { + # route processor address + id: ID! + # amount of routes processed + routeCount: BigInt! + # amount of users + userCount: BigInt! +} + +type Route @entity { + # tx hash - tx index + id: ID! + # from address + from: Bytes! + # to address + to: Bytes! + # tokenIn address + tokenIn: Bytes! + # tokenOut address + tokenOut: Bytes! + # amount of tokenIn + amountIn: BigInt! + # min amount of tokenOut + amountOutMin: BigInt! + # amount of tokenOut + amountOut: BigInt! + # timestamp + timestamp: BigInt! +} + +type User @entity { + # user address + id: ID! + # amount of routes processed + routeCount: BigInt! + # list of routes processed + #routes: [Route!]! @derivedFrom(field: "from") +} diff --git a/packages/route-processor-watcher/subgraph-build/subgraph.yaml b/packages/route-processor-watcher/subgraph-build/subgraph.yaml new file mode 100644 index 0000000..f0c61b1 --- /dev/null +++ b/packages/route-processor-watcher/subgraph-build/subgraph.yaml @@ -0,0 +1,24 @@ +specVersion: 0.0.4 +schema: + file: schema.graphql +dataSources: + - kind: ethereum/contract + name: RouteProcessor + network: mainnet + source: + address: "0x1f2fcf1d036b375b384012e61d3aa33f8c256bbe" + startBlock: 3685230 + abi: RouteProcessor + mapping: + kind: ethereum/events + apiVersion: 0.0.6 + language: wasm/assemblyscript + file: RouteProcessor/RouteProcessor.wasm + entities: [] + abis: + - name: RouteProcessor + file: RouteProcessor/abis/RouteProcessor3.json + eventHandlers: + - event: Route(indexed address,address,indexed address,indexed + address,uint256,uint256,uint256) + handler: handleRoute diff --git a/packages/route-processor-watcher/tsconfig.json b/packages/route-processor-watcher/tsconfig.json new file mode 100644 index 0000000..f4b8852 --- /dev/null +++ b/packages/route-processor-watcher/tsconfig.json @@ -0,0 +1,74 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig.json to read more about this file */ + + /* Basic Options */ + // "incremental": true, /* Enable incremental compilation */ + "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', or 'ESNEXT'. */ + "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ + "lib": ["es2019"], /* Specify library files to be included in the compilation. */ + // "allowJs": true, /* Allow javascript files to be compiled. */ + // "checkJs": true, /* Report errors in .js files. */ + // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */ + // "declaration": true, /* Generates corresponding '.d.ts' file. */ + // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ + "sourceMap": true, /* Generates corresponding '.map' file. */ + // "outFile": "./", /* Concatenate and emit output to single file. */ + "outDir": "dist", /* Redirect output structure to the directory. */ + // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ + // "composite": true, /* Enable project compilation */ + // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ + // "removeComments": true, /* Do not emit comments to output. */ + // "noEmit": true, /* Do not emit outputs. */ + // "importHelpers": true, /* Import emit helpers from 'tslib'. */ + "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ + // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ + + /* Strict Type-Checking Options */ + "strict": true, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* Enable strict null checks. */ + // "strictFunctionTypes": true, /* Enable strict checking of function types. */ + // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ + // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ + // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ + // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ + + /* Additional Checks */ + // "noUnusedLocals": true, /* Report errors on unused locals. */ + // "noUnusedParameters": true, /* Report errors on unused parameters. */ + // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an 'override' modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */ + + /* Module Resolution Options */ + // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ + // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ + // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ + // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ + // "typeRoots": [], /* List of folders to include type definitions from. */ + // "types": [], /* Type declaration files to be included in compilation. */ + // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ + "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ + // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + + /* Source Map Options */ + // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ + // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ + + /* Experimental Options */ + "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ + // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + + /* Advanced Options */ + "skipLibCheck": true, /* Skip type checking of declaration files. */ + "forceConsistentCasingInFileNames": true, /* Disallow inconsistently-cased references to the same file. */ + "resolveJsonModule": true /* Enabling the option allows importing JSON, and validating the types in that JSON file. */ + }, + "include": ["src/**/*"] +}