├── .gitignore ├── .travis.yml ├── .yarnrc ├── CODE_OF_CONDUCT.md ├── Dockerfile ├── LICENSE ├── README.md ├── lerna.json ├── package-lock.json ├── package.json ├── packages ├── gnarly-bin │ ├── .gitignore │ ├── LICENSE │ ├── package.json │ ├── src │ │ ├── abis │ │ │ └── EtherGoo.ts │ │ └── index.ts │ └── tsconfig.json ├── gnarly-core │ ├── package.json │ ├── src │ │ ├── Blockstream.ts │ │ ├── Gnarly.ts │ │ ├── ReducerRunner.ts │ │ ├── globalstate.ts │ │ ├── index.ts │ │ ├── ingestion │ │ │ ├── IngestApi.ts │ │ │ ├── Web3Api.ts │ │ │ └── index.ts │ │ ├── models │ │ │ ├── ABIItem.ts │ │ │ ├── Block.ts │ │ │ ├── ExternalTransaction.ts │ │ │ ├── InternalTransaction.ts │ │ │ ├── Log.ts │ │ │ └── Transaction.ts │ │ ├── ourbit │ │ │ ├── Ourbit.ts │ │ │ ├── index.ts │ │ │ └── types.ts │ │ ├── polyfills │ │ │ └── asynciterator-polyfill.ts │ │ ├── reducer │ │ │ ├── ReducerContext.ts │ │ │ ├── index.ts │ │ │ └── types.ts │ │ ├── stores │ │ │ ├── IPersistInterface.ts │ │ │ ├── index.ts │ │ │ └── sequelize.ts │ │ ├── typeStore │ │ │ ├── Sequelize.ts │ │ │ └── index.ts │ │ ├── types │ │ │ └── .gitkeep │ │ └── utils.ts │ ├── test │ │ ├── Blockstream.spec.ts │ │ ├── Ourbit.spec.ts │ │ ├── data │ │ │ └── erc20Abi.ts │ │ ├── exports.spec.ts │ │ ├── factories │ │ │ ├── IJSONBlockFactory.ts │ │ │ ├── IJSONExternalTransactionFactory.ts │ │ │ ├── IJSONExternalTransactionReceiptFactory.ts │ │ │ ├── IJSONInternalTransactionFactory.ts │ │ │ └── IJSONLogFactory.ts │ │ ├── globalstate.spec.ts │ │ ├── mocks │ │ │ ├── MockIngestApi.ts │ │ │ └── MockPersistInterface.ts │ │ ├── models │ │ │ └── Models.spec.ts │ │ ├── utils.spec.ts │ │ └── utils │ │ │ └── index.ts │ ├── tsconfig.json │ └── tsconfig.test.json ├── gnarly-reducer-block-meta │ ├── LICENSE │ ├── package.json │ ├── src │ │ ├── index.ts │ │ ├── models │ │ │ ├── index.ts │ │ │ └── sequelize.ts │ │ ├── reducer.ts │ │ └── stores │ │ │ ├── index.ts │ │ │ └── sequelize.ts │ └── tsconfig.json ├── gnarly-reducer-erc20 │ ├── LICENSE │ ├── package.json │ ├── src │ │ ├── abi.ts │ │ ├── index.ts │ │ ├── models │ │ │ ├── index.ts │ │ │ └── sequelize.ts │ │ ├── reducer.ts │ │ └── stores │ │ │ ├── index.ts │ │ │ └── sequelize.ts │ └── tsconfig.json ├── gnarly-reducer-erc721 │ ├── LICENSE │ ├── package.json │ ├── src │ │ ├── index.ts │ │ ├── models │ │ │ ├── index.ts │ │ │ └── sequelize.ts │ │ ├── reducer.ts │ │ └── stores │ │ │ ├── index.ts │ │ │ └── sequelize.ts │ └── tsconfig.json ├── gnarly-reducer-events │ ├── LICENSE │ ├── package.json │ ├── src │ │ ├── index.ts │ │ ├── models │ │ │ ├── index.ts │ │ │ └── sequelize.ts │ │ ├── reducer.ts │ │ └── stores │ │ │ ├── index.ts │ │ │ └── sequelize.ts │ └── tsconfig.json ├── tsconfig.build.json ├── tsconfig.json └── tsconfig.test.json ├── tslint.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | #####=== Node ===##### 3 | 4 | # Logs 5 | logs 6 | *.log 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | 13 | # Directory for instrumented libs generated by jscoverage/JSCover 14 | lib-cov 15 | 16 | # Coverage directory used by tools like istanbul 17 | coverage 18 | 19 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 20 | .grunt 21 | 22 | # node-waf configuration 23 | .lock-wscript 24 | 25 | # Compiled binary addons (http://nodejs.org/api/addons.html) 26 | build/Release 27 | 28 | # Dependency directory 29 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 30 | node_modules 31 | 32 | # Debug log from npm 33 | npm-debug.log 34 | lerna-debug.log 35 | 36 | 37 | lib 38 | .vscode 39 | .env 40 | .nyc_output 41 | lcov.info 42 | .coveralls.yml 43 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: trusty 2 | sudo: required 3 | group: beta 4 | language: node_js 5 | node_js: 6 | - '10' 7 | cache: 8 | directories: 9 | - node_modules 10 | matrix: 11 | fast_finish: true 12 | before_script: 13 | - lerna bootstrap 14 | - travis_retry yarn run build-ts 15 | after_success: 16 | - yarn run coverage 17 | script: 18 | - yarn run lint 19 | - yarn run test 20 | notifications: 21 | slack: 22 | secure: kh0lg6Tkh6ZS44BKqO9S7Kac7o1joIkFY8gZDv/PINc7gscfThbvVExpighnG2ooddqnqQ4cLj8O7hhAwdGBLYCl68OZHQyC3/VQm+S/Uc0oLGkKARb+NQZ6/g1ExlPQ2vnMsOCwcDlpwrBrDw6qGDxQX6WmVKUy8Gp7s25kwYvry6AcXHtb2DQifR1ZTYOC7RBj1ETjmL+9KSyEIIdHg+9jb6mp5Wr367xUwx5hl4KeZulxEQWrqBLEcATvqzrNGXRmQkxmW4LDYoG8ODWdm3GelKVQrxdPGcHoH6bXVuHN+JXbF8zaj/zDqxb0y6Dm9rph6ZlLE9rq1HO9cQZBXOJxBcuC8S/hZ+/dwddgHzg24Fdgaat4G376qa97Ij6jqRNspJPlDjeq0Txk6bhHmAFSnUf/XKg+ym24acorO6VT/jrrq9pH7/uJYupnnhB2jBiHtsWny6nA4WrVv0RfXYaowW9Sx26Z+NKyT8SjSha5WvXxPuPX9IO9JfX9a6D5fU1OVMYGb4cvErnvG5ST08syNUhzaYkQHNWWPeBsoVqODP+pb4hIPTt9ZB0BLlMNR4UBDP/3K39cyNjEnDOxpMSrD/HGLQ1SxR8HDhshhIsBtiZmKzhXn+98dDRTvxx17gQD2u8OjKj4clDuIjNnrzYZUgVpEmxnZJvhnehNidQ= 23 | email: 24 | on_success: change 25 | on_failure: always 26 | -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | workspaces-experimental true 2 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at matt@XLNT.co. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:18.04 2 | 3 | WORKDIR /app 4 | 5 | COPY ./packages/gnarly-bin/pkg/ . 6 | RUN chmod +x -R . 7 | 8 | # keep this up to date with the output from pkg -_- 9 | COPY ./node_modules/sha3/build/Release/sha3.node . 10 | 11 | ENTRYPOINT [ "./gnarly-bin-linux" ] 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2018 XLNT 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "lerna": "3.0.3", 3 | "packages": [ 4 | "packages/*" 5 | ], 6 | "npmClient": "yarn", 7 | "useWorkspaces": true, 8 | "version": "0.6.0", 9 | "publish": { 10 | "exact": true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "version": "0.0.4-alpha.1", 4 | "scripts": { 5 | "watch-ts": "lerna run watch-ts --parallel", 6 | "build-ts": "lerna run build-ts", 7 | "test": "lerna run test --scope @xlnt/gnarly-core", 8 | "lint": "lerna run lint --parallel", 9 | "clean": "lerna clean --yes && lerna exec --parallel -- rm -r ./lib", 10 | "coverage": "yarn run coverage:generate && yarn run coverage:submit", 11 | "coverage:generate": "lerna run coverage --parallel --scope @xlnt/gnarly-core", 12 | "coverage:submit": "lcov-result-merger 'packages/**/lcov.info' | coveralls", 13 | "pkg": "lerna run pkg --scope=@xlnt/gnarly-bin", 14 | "docker-build": "docker build -t shrugs/gnarly-test:demo .", 15 | "docker-push": "docker push shrugs/gnarly-test:demo", 16 | "deploy": "yarn run build-ts && yarn run pkg && yarn run docker-build && yarn run docker-push" 17 | }, 18 | "devDependencies": { 19 | "@types/mocha": "^2.2.48", 20 | "@types/node": "^9.4.0", 21 | "@types/uuid": "^3.4.3", 22 | "coveralls": "^3.0.2", 23 | "lcov-result-merger": "^3.1.0", 24 | "lerna": "^3.0.3", 25 | "nodemon": "^1.14.12", 26 | "ts-node": "^7.0.1", 27 | "tslint": "^5.11.0" 28 | }, 29 | "workspaces": [ 30 | "packages/*" 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /packages/gnarly-bin/.gitignore: -------------------------------------------------------------------------------- 1 | pkg 2 | -------------------------------------------------------------------------------- /packages/gnarly-bin/LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /packages/gnarly-bin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@xlnt/gnarly-bin", 3 | "version": "0.6.0", 4 | "description": "A simple executable for running gnarly reducers.", 5 | "main": "lib/index.js", 6 | "bin": "lib/index.js", 7 | "scripts": { 8 | "prepare": "npm run build", 9 | "build": "npm run build-ts && npm run tslint", 10 | "ts-start": "ts-node --no-cache src/index.ts", 11 | "start": "node lib/index.js", 12 | "watch": "concurrently -k -p \"[{name}]\" -n \"TypeScript,Node\" -c \"cyan.bold,green.bold\" \"npm run watch-ts\" \"npm run watch-node\"", 13 | "test": "nyc --reporter=text mocha -r ts-node/register -r source-map-support/register --full-trace 'test/**/*.spec.ts'", 14 | "watch-test": " mocha --watch --watch-extensions ts -r ts-node/register 'test/**/*.spec.ts'", 15 | "coverage": "nyc report --reporter=text-lcov > ./lcov.info", 16 | "build-ts": "tsc", 17 | "watch-ts": "tsc -w", 18 | "lint": "tslint --project .", 19 | "pkg": "pkg --targets node9-linux-x64,node9-macos-x64 --out-path ./pkg ." 20 | }, 21 | "pkg": { 22 | "assets": "src/abis" 23 | }, 24 | "files": [ 25 | "lib" 26 | ], 27 | "keywords": [ 28 | "gnarly" 29 | ], 30 | "repository": { 31 | "type": "git", 32 | "url": "git@github.com:XLNT/gnarly.git" 33 | }, 34 | "author": "Matt Condon ", 35 | "license": "Apache-2.0", 36 | "devDependencies": { 37 | "@types/chai": "^4.1.4", 38 | "@types/node": "^10.5.7", 39 | "chai": "^4.1.2", 40 | "mocha": "^5.2.0", 41 | "nyc": "^12.0.2", 42 | "pkg": "^4.3.1", 43 | "rosie": "^2.0.1", 44 | "source-map-support": "^0.5.6", 45 | "ts-node": "^7.0.1", 46 | "tslint": "^5.11.0", 47 | "typescript": "^3.0.1" 48 | }, 49 | "publishConfig": { 50 | "access": "public" 51 | }, 52 | "dependencies": { 53 | "@xlnt/gnarly-core": "^0.6.0", 54 | "@xlnt/gnarly-reducer-block-meta": "^0.6.0", 55 | "@xlnt/gnarly-reducer-erc721": "^0.6.0", 56 | "@xlnt/gnarly-reducer-events": "^0.6.0", 57 | "debug": "^3.1.0", 58 | "dotenv": "^5.0.1", 59 | "sequelize": "^4.37.7" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /packages/gnarly-bin/src/abis/EtherGoo.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable-next-line max-line-length 2 | export default [{ anonymous: false, inputs: [{ indexed: false, name: 'player', type: 'address' }, { indexed: false, name: 'unitId', type: 'uint256' }, { indexed: false, name: 'amount', type: 'uint256' }], name: 'UnitBought', type: 'event' }, { anonymous: false, inputs: [{ indexed: false, name: 'player', type: 'address' }, { indexed: false, name: 'unitId', type: 'uint256' }, { indexed: false, name: 'amount', type: 'uint256' }], name: 'UnitSold', type: 'event' }, { anonymous: false, inputs: [{ indexed: false, name: 'attacker', type: 'address' }, { indexed: false, name: 'target', type: 'address' }, { indexed: false, name: 'success', type: 'bool' }, { indexed: false, name: 'gooStolen', type: 'uint256' }], name: 'PlayerAttacked', type: 'event' }, { anonymous: false, inputs: [{ indexed: false, name: 'player', type: 'address' }, { indexed: false, name: 'referal', type: 'address' }, { indexed: false, name: 'amount', type: 'uint256' }], name: 'ReferalGain', type: 'event' }, { anonymous: false, inputs: [{ indexed: false, name: 'player', type: 'address' }, { indexed: false, name: 'upgradeId', type: 'uint256' }, { indexed: false, name: 'txProof', type: 'uint256' }], name: 'UpgradeMigration', type: 'event' }, { anonymous: false, inputs: [{ indexed: true, name: 'from', type: 'address' }, { indexed: true, name: 'to', type: 'address' }, { indexed: false, name: 'tokens', type: 'uint256' }], name: 'Transfer', type: 'event' }, { anonymous: false, inputs: [{ indexed: true, name: 'tokenOwner', type: 'address' }, { indexed: true, name: 'spender', type: 'address' }, { indexed: false, name: 'tokens', type: 'uint256' }], name: 'Approval', type: 'event' }] 3 | -------------------------------------------------------------------------------- /packages/gnarly-bin/src/index.ts: -------------------------------------------------------------------------------- 1 | import dotenv = require('dotenv') 2 | dotenv.config() 3 | 4 | import makeDebug = require('debug') 5 | const debug = makeDebug('gnarly') 6 | 7 | /** 8 | * @TODO - handle lazy-require, config-based bootstrapping 9 | */ 10 | import Sequelize = require('sequelize') 11 | 12 | import Gnarly, { 13 | SequelizePersistInterface, 14 | Web3Api, 15 | } from '@xlnt/gnarly-core' 16 | 17 | import makeERC20Reducer, { 18 | makeSequelizeTypeStore as makeERC20TypeStore, 19 | } from '@xlnt/gnarly-reducer-erc20' 20 | 21 | import makeERC721Reducer, { 22 | makeSequelizeTypeStore as makeERC721TypeStore, 23 | } from '@xlnt/gnarly-reducer-erc721' 24 | 25 | import makeBlockReducer, { 26 | makeSequelizeTypeStore as makeBlockTypeStore, 27 | } from '@xlnt/gnarly-reducer-block-meta' 28 | 29 | import makeEventsReducer, { 30 | makeSequelizeTypeStore as makeEventsTypeStore, 31 | } from '@xlnt/gnarly-reducer-events' 32 | 33 | const ZRX_ADDRESS = '0xe41d2489571d322189246dafa5ebde1f4699f498' 34 | const CRYPTO_KITTIES_ADDRESS = '0x06012c8cf97BEaD5deAe237070F9587f8E7A266d' 35 | const ETHER_GOO_ADDRESS = '0x57b116da40f21f91aec57329ecb763d29c1b2355' 36 | 37 | import etherGooAbi from './abis/EtherGoo' 38 | 39 | enum Keys { 40 | ZRX = 'ZRX', 41 | CryptoKitties = 'cryptoKitties', 42 | Blocks = 'blocks', 43 | Events = 'events', 44 | } 45 | 46 | const main = async () => { 47 | const nodeEndpoint = process.env.NODE_ENDPOINT 48 | const connectionString = process.env.DB_CONNECTION_STRING 49 | 50 | const sequelize = new Sequelize(connectionString, { 51 | logging: false, 52 | pool: { 53 | max: 5, 54 | min: 0, 55 | idle: 20000, 56 | acquire: 20000, 57 | }, 58 | retry: { 59 | max: 1, 60 | // @TODO(shrugs) ^ make this configurable with a default of ~3 61 | }, 62 | }) 63 | 64 | const erc20Reducer = makeERC20Reducer(Keys.ZRX, makeERC20TypeStore( 65 | Sequelize, 66 | sequelize, 67 | ))( 68 | ZRX_ADDRESS, 69 | ) 70 | // ^ using ZRX simply because it has most transfers per block right now 71 | 72 | const erc721Reducer = makeERC721Reducer(Keys.CryptoKitties, makeERC721TypeStore( 73 | Sequelize, 74 | sequelize, 75 | ))( 76 | CRYPTO_KITTIES_ADDRESS, 77 | ) 78 | 79 | const blockReducer = makeBlockReducer(Keys.Blocks, makeBlockTypeStore( 80 | Sequelize, 81 | sequelize, 82 | ))( 83 | ) 84 | 85 | const eventsReducer = makeEventsReducer(Keys.Events, makeEventsTypeStore( 86 | Sequelize, 87 | sequelize, 88 | ))({ 89 | [ETHER_GOO_ADDRESS]: etherGooAbi, 90 | }) 91 | 92 | const reducers = [ 93 | erc20Reducer, 94 | erc721Reducer, 95 | blockReducer, 96 | eventsReducer, 97 | ] 98 | 99 | const store = new SequelizePersistInterface( 100 | Sequelize, 101 | sequelize, 102 | ) 103 | 104 | const ingestApi = new Web3Api(nodeEndpoint) 105 | 106 | const gnarly = new Gnarly( 107 | ingestApi, 108 | store, 109 | reducers, 110 | ) 111 | 112 | let didRequestExit = false 113 | const gracefulExit = async () => { 114 | if (didRequestExit) { 115 | process.exit(1) 116 | } 117 | didRequestExit = true 118 | debug('Gracefully exiting. Send the signal again to force exit.') 119 | await gnarly.bailOut() 120 | process.exit(0) 121 | } 122 | 123 | process.on('SIGINT', gracefulExit) 124 | process.on('SIGTERM', gracefulExit) 125 | 126 | const GNARLY_RESET = (process.env.GNARLY_RESET || 'false') === 'true' 127 | const LATEST_BLOCK_HASH = process.env.LATEST_BLOCK_HASH || null 128 | 129 | await gnarly.reset(GNARLY_RESET) 130 | await gnarly.shaka(LATEST_BLOCK_HASH) 131 | } 132 | 133 | process.on('unhandledRejection', (error) => { 134 | console.error('unhandledRejection:', error, error.stack) 135 | process.exit(1) 136 | }) 137 | 138 | main() 139 | .catch((error) => { 140 | console.error(error, error.stack) 141 | process.exit(1) 142 | }) 143 | -------------------------------------------------------------------------------- /packages/gnarly-bin/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.build.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "lib" 6 | }, 7 | "include": ["src"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/gnarly-core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@xlnt/gnarly-core", 3 | "version": "0.6.0", 4 | "description": "Condense blockchains into steady state with confidence.", 5 | "main": "lib/index.js", 6 | "types": "./src/types/gnarly.d.ts", 7 | "scripts": { 8 | "prepare": "npm run build", 9 | "build": "npm run build-ts && npm run tslint", 10 | "start": "node lib/index.js", 11 | "test": "TS_NODE_PROJECT=./tsconfig.test.json nyc --reporter=text mocha -r ts-node/register -r source-map-support/register --full-trace 'test/**/*.spec.ts'", 12 | "watch-test": " TS_NODE_PROJECT=./tsconfig.test.json mocha --watch --watch-extensions ts -r ts-node/register -r source-map-support/register --full-trace 'test/**/*.spec.ts'", 13 | "coverage": "nyc report --reporter=text-lcov > ./lcov.info", 14 | "build-ts": "tsc", 15 | "watch-ts": "tsc -w", 16 | "lint": "tslint --project ." 17 | }, 18 | "files": [ 19 | "lib" 20 | ], 21 | "repository": "https://github.com/XLNT/gnarly/tree/master/packages/gnarly-core", 22 | "keywords": [ 23 | "ethereum" 24 | ], 25 | "license": "Apache-2.0", 26 | "bugs": { 27 | "url": "https://github.com/XLNT/gnarly/issues" 28 | }, 29 | "homepage": "https://github.com/XLNT/gnarly#readme", 30 | "dependencies": { 31 | "@xlnt/fast-json-patch": "^2.0.8", 32 | "bn.js": "^4.11.8", 33 | "debug": "^3.1.0", 34 | "ethereumjs-blockstream": "^3.1.0", 35 | "isomorphic-fetch": "^2.2.1", 36 | "lodash.identity": "^3.0.0", 37 | "lodash.isplainobject": "^4.0.6", 38 | "moize": "^5.3.1", 39 | "number-to-bn": "^1.7.0", 40 | "p-map": "^1.2.0", 41 | "p-queue": "^2.4.2", 42 | "p-retry": "^2.0.0", 43 | "pg": "^7.4.1", 44 | "sequelize": "^4.35.2", 45 | "uuid": "^3.2.1", 46 | "web3-eth-abi": "^1.0.0-beta.34", 47 | "web3-utils": "^1.0.0-beta.34" 48 | }, 49 | "devDependencies": { 50 | "@types/chai": "^4.1.4", 51 | "@types/chai-spies": "^1.0.0", 52 | "@types/lodash.identity": "^3.0.3", 53 | "@types/lodash.isplainobject": "^4.0.3", 54 | "@types/node": "^10.5.7", 55 | "bn-chai": "^1.0.1", 56 | "chai": "^4.1.2", 57 | "chai-spies": "^1.0.0", 58 | "concurrently": "^3.5.1", 59 | "mocha": "^5.0.4", 60 | "nyc": "^12.0.2", 61 | "rosie": "^2.0.1", 62 | "source-map-support": "^0.5.6", 63 | "ts-node": "^7.0.1", 64 | "tslint": "^5.11.0", 65 | "typescript": "^3.0.1" 66 | }, 67 | "publishConfig": { 68 | "access": "public" 69 | }, 70 | "nyc": { 71 | "extension": [ 72 | ".ts", 73 | ".tsx" 74 | ], 75 | "exclude": [ 76 | "**/*.d.ts", 77 | "lib", 78 | "test", 79 | "coverage", 80 | "src/polyfills" 81 | ], 82 | "all": true, 83 | "sourceMap": true, 84 | "instrument": true 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /packages/gnarly-core/src/Blockstream.ts: -------------------------------------------------------------------------------- 1 | import makeDebug = require('debug') 2 | const debug = makeDebug('gnarly-core:blockstream') 3 | const debugFastForward = makeDebug('gnarly-core:blockstream:fast-forward') 4 | const debugOnBlockAdd = makeDebug('gnarly-core:blockstream:onBlockAdd') 5 | const debugOnBlockInvalidated = makeDebug('gnarly-core:blockstream:onBlockInvalidated') 6 | 7 | import { 8 | BlockAndLogStreamer, 9 | } from 'ethereumjs-blockstream' 10 | import 'isomorphic-fetch' 11 | import PQueue = require('p-queue') 12 | import uuid = require('uuid') 13 | 14 | import { IJSONBlock } from './models/Block' 15 | import { IJSONLog } from './models/Log' 16 | 17 | import { 18 | timeout, 19 | toBN, 20 | } from './utils' 21 | 22 | import { globalState } from './globalstate' 23 | 24 | const MAX_QUEUE_LENGTH = 100 25 | 26 | class BlockStream { 27 | private streamer: BlockAndLogStreamer 28 | 29 | private onBlockAddedSubscriptionToken 30 | private onBlockRemovedSubscriptionToken 31 | private unsubscribeFromNewBlocks 32 | /** 33 | * Whether or not the blockstreamer is syncing blocks from the past or not 34 | */ 35 | private syncing = false 36 | 37 | private pendingTransactions: PQueue = new PQueue({ 38 | concurrency: 1, 39 | }) 40 | 41 | constructor ( 42 | private reducerKey: string, 43 | private processTransaction: (txId: string, fn: () => Promise, extra: object) => Promise , 44 | private rollbackTransaction: (blockHash: string) => Promise, 45 | private onNewBlock: (block: IJSONBlock, syncing: boolean) => () => Promise < any > , 46 | private blockRetention: number = 100, 47 | ) { 48 | this.streamer = new BlockAndLogStreamer(globalState.api.getBlockByHash, globalState.api.getLogs, { 49 | blockRetention: this.blockRetention, 50 | }) 51 | } 52 | 53 | public start = async (fromBlockHash: string = null) => { 54 | let localLatestBlock: IJSONBlock | null = null 55 | 56 | // the primary purpose of this function to to extend the historical block reduction 57 | // beyond the blockRetention limit provided to ethereumjs-blockstream 58 | // because we might have stopped tracking blocks for longer than ~100 blocks and need to catch up 59 | 60 | const remoteLatestBlock = await globalState.api.getLatestBlock() 61 | 62 | if (fromBlockHash !== null) { 63 | // ^ if fromBlockHash is provided, it takes priority 64 | debug('Continuing from blockHash %s', fromBlockHash) 65 | 66 | // so look up the latest block we know about 67 | localLatestBlock = await globalState.api.getBlockByHash(fromBlockHash) 68 | 69 | // need to load that block into the local chain so handlers trigger correctly 70 | // when we defer to the ethereumjs-blockstream reconciliation algorithm where it fetches 71 | // its own historical blocks 72 | this.streamer.reconcileNewBlock(localLatestBlock) 73 | } else { 74 | // we are starting from head 75 | debug('Starting from HEAD') 76 | 77 | // ask the remote for the latest "local" block 78 | localLatestBlock = await globalState.api.getLatestBlock() 79 | } 80 | 81 | const remoteLatestBlockNumber = toBN(remoteLatestBlock.number) 82 | const localLatestBlockNumber = toBN(localLatestBlock.number) 83 | 84 | // subscribe to changes in chain 85 | this.onBlockAddedSubscriptionToken = this.streamer.subscribeToOnBlockAdded(this.onBlockAdd) 86 | this.onBlockRemovedSubscriptionToken = this.streamer.subscribeToOnBlockRemoved(this.onBlockInvalidated) 87 | 88 | debug('Local block number: %d. Remote block number: %d', localLatestBlockNumber, remoteLatestBlockNumber) 89 | 90 | let syncUpToNumber = await this.latestRemoteNumberWithRetentionBuffer() 91 | // if we're not at that block number, start pulling the blocks from history 92 | // until we enter the block retention limit 93 | // once we've gotten to the block retention limit, we need to defer to blockstream's chain 94 | // reconciliation algorithm 95 | if (localLatestBlockNumber.lt(syncUpToNumber)) { 96 | debugFastForward( 97 | 'Starting from %d and continuing to %d', 98 | localLatestBlockNumber.toNumber(), 99 | remoteLatestBlockNumber.toNumber(), 100 | ) 101 | this.syncing = true 102 | let i = localLatestBlockNumber.clone() 103 | while (i.lt(syncUpToNumber)) { 104 | // if we're at the top of the queue 105 | // wait a bit and then add the thing 106 | while (this.pendingTransactions.size >= MAX_QUEUE_LENGTH) { 107 | debugFastForward( 108 | 'Reached max queue size of %d, waiting a bit...', 109 | MAX_QUEUE_LENGTH, 110 | ) 111 | await timeout(5000) 112 | } 113 | 114 | const block = await globalState.api.getBlockByNumber(i) 115 | debugFastForward( 116 | 'block %s (%s)', 117 | toBN(block.number).toString(), 118 | block.hash, 119 | ) 120 | await this.streamer.reconcileNewBlock(block) 121 | 122 | i = toBN(block.number).add(toBN(1)) 123 | // TODO: easy optimization, only check latest block on the last 124 | // iteration 125 | syncUpToNumber = await this.latestRemoteNumberWithRetentionBuffer() 126 | } 127 | 128 | this.syncing = false 129 | } 130 | 131 | this.beginTracking() 132 | } 133 | 134 | public stop = async () => { 135 | this.unsubscribeFromNewBlocks() 136 | if (this.streamer) { 137 | this.streamer.unsubscribeFromOnBlockAdded(this.onBlockAddedSubscriptionToken) 138 | this.streamer.unsubscribeFromOnBlockRemoved(this.onBlockRemovedSubscriptionToken) 139 | } 140 | debug('Pending Transactions: %d', this.pendingTransactions.size) 141 | await this.pendingTransactions.onIdle() 142 | debug('Done! Exiting...') 143 | } 144 | 145 | public initWithHistoricalBlocks = async (historicalBlocks: IJSONBlock[] = []): Promise => { 146 | // ^ if historicalBlocks provided, reconcile blocks 147 | debug( 148 | 'Initializing history with last historical block %s', 149 | toBN(historicalBlocks[historicalBlocks.length - 1].number), 150 | ) 151 | 152 | for (const block of historicalBlocks) { 153 | await this.streamer.reconcileNewBlock(block) 154 | } 155 | } 156 | 157 | private onBlockAdd = async (block: IJSONBlock) => { 158 | const pendingTransaction = async () => { 159 | debugOnBlockAdd( 160 | 'block %s (%s)', 161 | block.number, 162 | block.hash, 163 | ) 164 | 165 | await this.processTransaction( 166 | uuid.v4(), 167 | this.onNewBlock(block, this.syncing), 168 | { 169 | blockHash: block.hash, 170 | }, 171 | ) 172 | 173 | await globalState.store.saveHistoricalBlock(this.reducerKey, this.blockRetention, block) 174 | } 175 | 176 | this.pendingTransactions.add(pendingTransaction) 177 | } 178 | 179 | private onBlockInvalidated = (block: IJSONBlock) => { 180 | const pendingTransaction = async () => { 181 | debugOnBlockInvalidated( 182 | 'block %s (%s)', 183 | block.number, 184 | block.hash, 185 | ) 186 | 187 | // when a block is invalidated, rollback the transaction 188 | await this.rollbackTransaction(block.hash) 189 | // and then delete the historical block 190 | await globalState.store.deleteHistoricalBlock(this.reducerKey, block.hash) 191 | } 192 | 193 | this.pendingTransactions.add(pendingTransaction) 194 | } 195 | 196 | private beginTracking = () => { 197 | this.unsubscribeFromNewBlocks = globalState.api.subscribeToNewBlocks(async () => { 198 | await this.streamer.reconcileNewBlock(await globalState.api.getLatestBlock()) 199 | }) 200 | } 201 | 202 | private latestRemoteNumberWithRetentionBuffer = async () => { 203 | return toBN( 204 | (await globalState.api.getLatestBlock()).number, 205 | ).sub(toBN(this.blockRetention - 10)) 206 | // ^ manually import historical blocks until we're within 90 blocks of HEAD 207 | // and then we can use blockstream's reconciliation algorithm 208 | } 209 | } 210 | 211 | export default BlockStream 212 | -------------------------------------------------------------------------------- /packages/gnarly-core/src/Gnarly.ts: -------------------------------------------------------------------------------- 1 | import makeDebug = require('debug') 2 | const debug = makeDebug('gnarly-core') 3 | 4 | import { globalState } from './globalstate' 5 | 6 | import IIngestApi from './ingestion/IngestApi' 7 | import { IReducer } from './reducer' 8 | import { IPersistInterface } from './stores' 9 | 10 | import ReducerRunner, { makeRunner } from './ReducerRunner' 11 | 12 | class Gnarly { 13 | private runners: ReducerRunner[] = [] 14 | 15 | constructor ( 16 | ingestApi: IIngestApi, 17 | store: IPersistInterface, 18 | private reducers: IReducer[], 19 | ) { 20 | globalState.setApi(ingestApi) 21 | globalState.setStore(store) 22 | this.runners = this.reducers.map((reducer) => makeRunner(reducer)) 23 | } 24 | 25 | public shaka = async (fromBlockHash: string | null) => { 26 | debug('Surfs up!') 27 | this.runners.forEach((runner) => runner.run(fromBlockHash)) 28 | } 29 | 30 | // @TODO(shrugs) - allow adding reducers at runtime 31 | // public addReducer = async (reducer: IReducer, fromBlockHash: string | null) => { 32 | // const runner = makeRunner(reducer) 33 | // this.runners.push(runner) 34 | // // start running the reducer, asynchronously 35 | // runner.run(fromBlockHash) 36 | // } 37 | 38 | public bailOut = async () => { 39 | debug('Gracefully decomposing reducers...') 40 | await Promise.all(this.runners.map((r) => r.stop())) 41 | debug('Now that was gnarly!') 42 | } 43 | 44 | public reset = async (shouldReset: boolean = true) => { 45 | // reset gnarly internal state 46 | if (shouldReset) { 47 | await globalState.store.setdown() 48 | } 49 | await globalState.store.setup() 50 | 51 | // reset all reducer states 52 | debug('%s reducer stores: %s...', shouldReset ? 'Resetting' : 'Setting up') 53 | 54 | await Promise.all(this.runners.map((runner) => runner.reset(shouldReset))) 55 | debug('Done with %s reducers.', shouldReset ? 'resetting' : 'setting up') 56 | } 57 | } 58 | 59 | export default Gnarly 60 | -------------------------------------------------------------------------------- /packages/gnarly-core/src/ReducerRunner.ts: -------------------------------------------------------------------------------- 1 | import makeDebug = require('debug') 2 | 3 | import assert = require('assert') 4 | import Blockstream from './Blockstream' 5 | import { globalState } from './globalstate' 6 | import Block, { IJSONBlock } from './models/Block' 7 | import Ourbit, { 8 | IOperation, 9 | IPatch, 10 | } from './ourbit' 11 | import { IReducer, ReducerContext, ReducerType } from './reducer' 12 | import { 13 | SetdownFn, 14 | SetupFn, 15 | TypeStorer, 16 | } from './typeStore' 17 | 18 | // TODO: should be moved to bin 19 | const BLOCK_RETENTION = 100 20 | 21 | export class ReducerRunner { 22 | public ourbit: Ourbit 23 | public blockstreamer: Blockstream 24 | public shouldResume: boolean = true 25 | 26 | private debug 27 | private context: ReducerContext 28 | 29 | constructor ( 30 | private reducer: IReducer, 31 | ) { 32 | this.debug = makeDebug(`gnarly-core:runner:${this.reducer.config.key}`) 33 | this.context = new ReducerContext(this.reducer.config.key) 34 | 35 | this.ourbit = new Ourbit( 36 | this.reducer.config.key, 37 | this.reducer.state, 38 | this.persistPatchHandler, 39 | this.context, 40 | ) 41 | 42 | this.blockstreamer = new Blockstream( 43 | this.reducer.config.key, 44 | this.ourbit.processTransaction, 45 | this.ourbit.rollbackTransaction, 46 | this.handleNewBlock, 47 | BLOCK_RETENTION, 48 | ) 49 | } 50 | 51 | public run = async (fromBlockHash: string | null) => { 52 | await globalState.store.saveReducer(this.reducer.config.key) 53 | let latestBlockHash = fromBlockHash 54 | 55 | switch (this.reducer.config.type) { 56 | // idempotent reducers are only called from HEAD 57 | case ReducerType.Idempotent: 58 | latestBlockHash = null 59 | break 60 | // TimeVarying and Atomic Reducers start from a provided block hash, the latest in the DB, or HEAD 61 | case ReducerType.TimeVarying: 62 | case ReducerType.Atomic: { 63 | if (this.shouldResume) { 64 | // we're resuming, so replay from store if possible 65 | try { 66 | const latestTransaction = await globalState.store.getLatestTransaction(this.reducer.config.key) 67 | 68 | // load historical chain 69 | const historicalBlocks = await globalState.store.getHistoricalBlocks(this.reducer.config.key) 70 | 71 | if (!latestTransaction || !historicalBlocks || historicalBlocks.length === 0) { 72 | throw new Error('No latest transaction or historical blocks available, skipping resumption.') 73 | } 74 | 75 | try { 76 | const mostRecentHistoricalBlock = historicalBlocks[historicalBlocks.length - 1] 77 | 78 | assert.equal( 79 | mostRecentHistoricalBlock.hash, 80 | latestTransaction.blockHash, 81 | `We have a latestTransaction ${latestTransaction.id} with blockHash ${latestTransaction.blockHash} 82 | but it doesn't match the most recent historical block ${mostRecentHistoricalBlock.hash}!`, 83 | ) 84 | 85 | // let's re-hydrate local state by replaying transactions 86 | this.debug('Attempting to reload ourbit state from %s', latestTransaction.id || 'HEAD') 87 | await this.ourbit.resumeFromTxId(latestTransaction.id) 88 | this.debug('Done reloading ourbit state.') 89 | 90 | this.debug('Attempting to reload blockstream state from %s', latestTransaction.blockHash) 91 | // let's reset the blockstreamer's internal state 92 | await this.blockstreamer.initWithHistoricalBlocks(historicalBlocks) 93 | this.debug('Done reloading blockstream state.') 94 | 95 | latestBlockHash = latestTransaction.blockHash 96 | } catch (error) { 97 | // we weren't able to replace state, which means something is totally broken 98 | this.debug(error) 99 | process.exit(1) 100 | } 101 | } catch (error) { 102 | // there's nothing to replay, so let's mention that and return to default behavior 103 | this.debug(error.message) 104 | } 105 | } else { 106 | // we specifically reset, so let's mention that 107 | this.debug('Explicitely starting from %s', latestBlockHash || 'HEAD') 108 | // and then reset blockstreamer chain 109 | await globalState.store.deleteHistoricalBlocks(this.reducer.config.key) 110 | } 111 | break 112 | } 113 | default: 114 | throw new Error(`Unexpected ReducerType ${this.reducer.config.type}`) 115 | } 116 | 117 | // default behavior is to start from HEAD 118 | this.debug('Streaming blocks from %s', latestBlockHash || 'HEAD') 119 | 120 | // and now ingest blocks from latestBlockHash 121 | await this.blockstreamer.start(latestBlockHash) 122 | 123 | return this.stop.bind(this) 124 | } 125 | 126 | public stop = async () => { 127 | await this.blockstreamer.stop() 128 | } 129 | 130 | public reset = async (shouldReset: boolean = true) => { 131 | this.shouldResume = !shouldReset 132 | 133 | if (shouldReset) { 134 | const setdown = this.reducer.config.typeStore.__setdown as SetdownFn 135 | await setdown() 136 | } 137 | 138 | const setup = this.reducer.config.typeStore.__setup as SetupFn 139 | await setup() 140 | } 141 | 142 | private handleNewBlock = (rawBlock: IJSONBlock, syncing: boolean) => async () => { 143 | const block = await this.normalizeBlock(rawBlock) 144 | 145 | await this.reducer.reduce(this.reducer.state, block, this.context.utils) 146 | } 147 | 148 | private normalizeBlock = async (block: IJSONBlock): Promise => { 149 | return new Block(block) 150 | } 151 | 152 | private persistPatchHandler = async (txId: string, patch: IPatch) => { 153 | for (const op of patch.operations) { 154 | await this.persistOperation(patch.id, op) 155 | } 156 | } 157 | 158 | private persistOperation = async (patchId: string, operation: IOperation) => { 159 | const storer = this.reducer.config.typeStore.store as TypeStorer 160 | await storer(patchId, operation) 161 | } 162 | } 163 | 164 | export const makeRunner = ( 165 | reducer: IReducer, 166 | ) => new ReducerRunner( 167 | reducer, 168 | ) 169 | 170 | export default ReducerRunner 171 | -------------------------------------------------------------------------------- /packages/gnarly-core/src/globalstate.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A global state, modelled after 3 | * https://github.com/mobxjs/mobx/blob/master/src/core/globalstate.ts 4 | */ 5 | 6 | // @TODO(shrugs) - add memoize back and use redis or something 7 | // import { memoize } from 'async-decorators' 8 | import IIngestApi from './ingestion/IngestApi' 9 | import IABIItem, { IABIItemInput } from './models/ABIItem' 10 | import Log from './models/Log' 11 | import { IPersistInterface } from './stores' 12 | import { enhanceAbiItem, onlySupportedAbiItems } from './utils' 13 | 14 | export type ABIItemSet = IABIItem[] 15 | 16 | export class GnarlyGlobals { 17 | // @TODO(shrugs) - do we need to move this to a contract artifact? 18 | public abis: { [s: string]: ABIItemSet } = {} 19 | public api: IIngestApi 20 | public store: IPersistInterface 21 | 22 | public getLogs = async (options) => { 23 | const logs = await this.api.getLogs(options) 24 | return logs.map((l) => new Log(null, l)) 25 | } 26 | 27 | public setApi = (api: IIngestApi) => { 28 | this.api = api 29 | } 30 | 31 | public setStore = (store: IPersistInterface) => { 32 | this.store = store 33 | } 34 | 35 | // @TODO(shrugs) - replace this with a map indexed by signatures 36 | public addABI = (address: string, abi: IABIItemInput[]) => { 37 | this.abis[address.toLowerCase()] = (this.abis[address.toLowerCase()] || []) 38 | .concat( 39 | abi 40 | .filter(onlySupportedAbiItems) 41 | .map(enhanceAbiItem), 42 | ) 43 | } 44 | 45 | public getABI = (address: string): ABIItemSet => this.abis[address.toLowerCase()] 46 | 47 | public getMethod = (address: string, methodId: string): IABIItem => { 48 | // @TODO(shrugs) replace with O(1) precomputed lookup 49 | return (this.abis[address.toLowerCase()] || []) 50 | .find((ai) => ai.shortId === methodId) 51 | } 52 | } 53 | 54 | export let globalState: GnarlyGlobals = new GnarlyGlobals() 55 | -------------------------------------------------------------------------------- /packages/gnarly-core/src/index.ts: -------------------------------------------------------------------------------- 1 | 2 | import './polyfills/asynciterator-polyfill' 3 | 4 | import { globalState } from './globalstate' 5 | 6 | export { 7 | IPatch, 8 | ITransaction, 9 | } from './ourbit/types' 10 | 11 | export { default as Block } from './models/Block' 12 | export { default as Transaction } from './models/Transaction' 13 | export { default as ExternalTransaction } from './models/ExternalTransaction' 14 | export { default as InternalTransaction } from './models/InternalTransaction' 15 | export { default as Log } from './models/Log' 16 | export { default as ABIITem, IABIItemInput } from './models/ABIItem' 17 | 18 | export { 19 | default, 20 | } from './Gnarly' 21 | 22 | export * from './utils' 23 | export * from './reducer' 24 | export * from './stores' 25 | export * from './typeStore' 26 | export * from './ingestion' 27 | 28 | export const addABI = globalState.addABI.bind(globalState) 29 | export const getLogs = globalState.getLogs.bind(globalState) 30 | -------------------------------------------------------------------------------- /packages/gnarly-core/src/ingestion/IngestApi.ts: -------------------------------------------------------------------------------- 1 | import BN = require('bn.js') 2 | import { 3 | FilterOptions, 4 | } from 'ethereumjs-blockstream' 5 | 6 | import { IJSONBlock } from '../models/Block' 7 | import { IJSONExternalTransactionReceipt } from '../models/ExternalTransaction' 8 | import { IJSONLog } from '../models/Log' 9 | 10 | import { IJSONInternalTransaction } from '../models/InternalTransaction' 11 | 12 | export type DecomposeFn = () => void 13 | 14 | export default interface IIngestApi { 15 | /** 16 | * gets a block by number 17 | * @return IJSONBLock 18 | * @throws 19 | */ 20 | getBlockByNumber: (num: BN) => Promise 21 | 22 | /** 23 | * gets a block by hash 24 | * @return IJSONBlock 25 | * @throws 26 | */ 27 | getBlockByHash: (hash: string) => Promise 28 | 29 | /** 30 | * get the latest block 31 | * @returns IJSONBlock 32 | * @throws 33 | */ 34 | getLatestBlock: () => Promise 35 | 36 | /** 37 | * gets logs from filter options 38 | * @returns IJSONLog[] 39 | * @throws 40 | */ 41 | getLogs: (filterOptions: FilterOptions) => Promise 42 | 43 | /** 44 | * gets tx receipt 45 | * @returns IJSONExternalTransactionReceipt 46 | * @throws 47 | */ 48 | getTransactionReceipt: (hash: string) => Promise 49 | 50 | /** 51 | * traces a transaction (parity format) 52 | * @returns IJSONInternalTransaction 53 | * @throws 54 | */ 55 | traceTransaction: (hash: string) => Promise 56 | 57 | /** 58 | * subscribes to new atomic events (blocks, when using a blockchain) 59 | * @returns decomposer fn 60 | */ 61 | subscribeToNewBlocks: (cb) => DecomposeFn 62 | } 63 | -------------------------------------------------------------------------------- /packages/gnarly-core/src/ingestion/Web3Api.ts: -------------------------------------------------------------------------------- 1 | import makeDebug = require('debug') 2 | const debug = makeDebug('gnarly-core:api') 3 | 4 | import BN = require('bn.js') 5 | import { 6 | FilterOptions, 7 | } from 'ethereumjs-blockstream' 8 | import pRetry = require('p-retry') 9 | 10 | import { IJSONBlock } from '../models/Block' 11 | import { IJSONExternalTransactionReceipt } from '../models/ExternalTransaction' 12 | import { IJSONInternalTransaction } from '../models/InternalTransaction' 13 | import { IJSONLog } from '../models/Log' 14 | import IIngestApi from './IngestApi' 15 | 16 | import { 17 | cacheApiRequest, 18 | } from '../utils' 19 | 20 | export default class Web3Api implements IIngestApi { 21 | 22 | private longpollBlocksInterval 23 | 24 | private doFetch = cacheApiRequest( 25 | (method: string, params: any[] = []) => pRetry( 26 | async () => { 27 | const res = await fetch(this.nodeEndpoint, { 28 | method: 'POST', 29 | headers: new Headers({ 'Content-Type': 'application/json' }), 30 | body: JSON.stringify({ 31 | jsonrpc: '2.0', 32 | id: 1, 33 | method, 34 | params, 35 | }), 36 | }) 37 | const data = await res.json() 38 | if (data.result === undefined || data.result === null) { 39 | throw new Error(` 40 | Invalid JSON response: ${JSON.stringify(data, null, 2)} 41 | for ${method} ${JSON.stringify(params, null, 2)} 42 | Retrying... 43 | `) 44 | } 45 | 46 | return data.result 47 | }, { 48 | retries: this.maxRetries, 49 | minTimeout: this.minTimeout, 50 | }, 51 | ) 52 | .catch((error: Error) => { 53 | throw new Error(`Web3Api#fetch failed after ${this.maxRetries} retries: ${error.stack}`) 54 | }), 55 | ) 56 | 57 | public constructor ( 58 | private nodeEndpoint: string, 59 | public maxRetries = 5, 60 | public minTimeout = 100, 61 | ) { 62 | } 63 | 64 | public getBlockByNumber = async (num: BN): Promise => { 65 | debug('[getBlockByNumber] %s %s', num.toString(10), `0x${num.toString(16)}`) 66 | return this.doFetch('eth_getBlockByNumber', [`0x${num.toString(16)}`, true]) 67 | } 68 | 69 | public getBlockByHash = async (hash: string): Promise => { 70 | debug('[getBlockByHash] %s', hash) 71 | return this.doFetch('eth_getBlockByHash', [hash, true]) 72 | } 73 | 74 | public getLatestBlock = async (): Promise => { 75 | debug('[getLatestBlock]') 76 | return this.doFetch('eth_getBlockByNumber', ['latest', true]) 77 | } 78 | 79 | public getLogs = async (filterOptions: FilterOptions): Promise => { 80 | debug('[getLogs] %j', filterOptions) 81 | return this.doFetch('eth_getLogs', [filterOptions]) 82 | } 83 | 84 | public getTransactionReceipt = async (hash: string): Promise => { 85 | return this.doFetch('eth_getTransactionReceipt', [hash]) 86 | } 87 | 88 | public traceTransaction = async (hash: string): Promise => { 89 | return (await this.doFetch('trace_replayTransaction', [hash, ['trace']])).trace 90 | } 91 | 92 | public subscribeToNewBlocks = (cb) => { 93 | // @TODO - https://github.com/XLNT/gnarly/issues/30 94 | // but web3 subscriptions have a lot of issues with them as well 95 | // longpoll timer 96 | this.longpollBlocksInterval = setInterval(cb, 5000) 97 | return () => { 98 | clearInterval(this.longpollBlocksInterval) 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /packages/gnarly-core/src/ingestion/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | default as IIngestApi, 3 | } from './IngestApi' 4 | 5 | export { 6 | default as Web3Api, 7 | } from './Web3Api' 8 | -------------------------------------------------------------------------------- /packages/gnarly-core/src/models/ABIItem.ts: -------------------------------------------------------------------------------- 1 | export interface IInputOutput { 2 | name: string 3 | type: string 4 | indexed?: boolean 5 | } 6 | 7 | export interface IABIItemInput { 8 | anonymous?: boolean 9 | constant?: boolean 10 | inputs?: IInputOutput[] 11 | name: string 12 | outputs?: IInputOutput[] 13 | payable?: boolean 14 | stateMutability?: string 15 | type: string 16 | } 17 | 18 | export default interface IABIItem extends IABIItemInput { 19 | signature: string 20 | // ^ 0x1234567890....... 21 | fullName: string 22 | // ^ doThing(uint256) 23 | shortId: string 24 | // ^ 0x12345678 (guaranteed to be 10 characters) 25 | } 26 | 27 | export const isMethod = (item: IABIItem): boolean => item.type === 'function' 28 | export const isEvent = (item: IABIItem): boolean => item.type === 'event' 29 | -------------------------------------------------------------------------------- /packages/gnarly-core/src/models/Block.ts: -------------------------------------------------------------------------------- 1 | import BN = require('bn.js') 2 | import pMap = require('p-map') 3 | 4 | import ExternalTransaction, { 5 | IJSONExternalTransaction, 6 | } from './ExternalTransaction' 7 | import Transaction from './Transaction' 8 | 9 | import { toBN } from '../utils' 10 | 11 | export interface IJSONBlock { 12 | number: string 13 | hash: string 14 | parentHash: string 15 | nonce: string 16 | sha3Uncles: string 17 | logsBloom: string 18 | transactionsRoot: string 19 | stateRoot: string 20 | miner: string 21 | difficulty: string 22 | totalDifficulty: string 23 | extraData: string 24 | size: string 25 | gasLimit: string 26 | gasUsed: string 27 | timestamp: string 28 | transactions: IJSONExternalTransaction[] 29 | uncles: string[] 30 | } 31 | 32 | export default class Block { 33 | public number: BN 34 | public hash: string 35 | public parentHash: string 36 | public nonce: BN 37 | public sha3Uncles: string 38 | public logsBloom: string 39 | public transactionsRoot: string 40 | public stateRoot: string 41 | public miner: string 42 | public difficulty: BN 43 | public totalDifficulty: BN 44 | public extraData: string 45 | public size: BN 46 | public gasLimit: BN 47 | public gasUsed: BN 48 | public timestamp: BN 49 | public transactions: ExternalTransaction[] 50 | public allTransactions: Transaction[] 51 | public uncles: string[] 52 | 53 | public constructor (block: IJSONBlock) { 54 | this.number = toBN(block.number) 55 | this.hash = block.hash 56 | this.parentHash = block.parentHash 57 | this.nonce = toBN(block.nonce) 58 | this.sha3Uncles = block.sha3Uncles 59 | this.logsBloom = block.logsBloom 60 | this.transactionsRoot = block.transactionsRoot 61 | this.stateRoot = block.stateRoot 62 | this.miner = block.miner 63 | this.difficulty = toBN(block.difficulty) 64 | this.totalDifficulty = toBN(block.totalDifficulty) 65 | this.extraData = block.extraData 66 | this.size = toBN(block.extraData) 67 | this.gasLimit = toBN(block.gasLimit) 68 | this.gasUsed = toBN(block.gasUsed) 69 | this.timestamp = toBN(block.timestamp) 70 | this.transactions = block.transactions 71 | .map((t) => new ExternalTransaction(this, t)) 72 | this.uncles = block.uncles 73 | } 74 | 75 | public loadTransactions = async (): Promise => { 76 | await pMap( 77 | this.transactions, 78 | async (t) => t.getReceipt(), 79 | { concurrency: 20 }, 80 | ) 81 | } 82 | 83 | public loadAllTransactions = async (): Promise => { 84 | await pMap( 85 | this.transactions, 86 | async (t) => t.getInternalTransactions(), 87 | { concurrency: 20 }, 88 | ) 89 | // this looks dumb, but just combines all of the external 90 | // and internal transactions in one single 1-dimensional list 91 | // for easy iteration 92 | this.allTransactions = [].concat( 93 | ...this.transactions, 94 | [].concat( 95 | ...this.transactions 96 | .map((t) => t.internalTransactions), 97 | ), 98 | ) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /packages/gnarly-core/src/models/ExternalTransaction.ts: -------------------------------------------------------------------------------- 1 | import makeDebug = require('debug') 2 | const debug = makeDebug('gnarly-core:ExternalTransaction') 3 | 4 | import BN = require('bn.js') 5 | 6 | import Block from './Block' 7 | import InternalTransaction from './InternalTransaction' 8 | import Log, { IJSONLog } from './Log' 9 | import Transaction from './Transaction' 10 | 11 | import { globalState } from '../globalstate' 12 | import { toBN } from '../utils' 13 | 14 | export interface IJSONExternalTransaction { 15 | hash: string 16 | nonce: string 17 | blockHash: string 18 | blockNumber: string 19 | transactionIndex: string 20 | from: string 21 | to: string 22 | value: string 23 | gasPrice: string 24 | gas: string 25 | input: string 26 | } 27 | 28 | export interface IJSONExternalTransactionReceipt { 29 | blockHash: string 30 | blockNumber: string 31 | contractAddress: string 32 | cumulativeGasUsed: string 33 | from: string 34 | gasUsed: string 35 | logs: IJSONLog[] 36 | logsBloom: string 37 | status: string 38 | to: string 39 | transactionHash: string 40 | transactionIndex: string 41 | } 42 | 43 | export type IJSONExternalTransactionInfo = IJSONExternalTransaction | IJSONExternalTransactionReceipt 44 | 45 | // is JSONExternalTransaction if it has a nonce 46 | const isJSONExternalTransaction = (obj: any): obj is IJSONExternalTransaction => 47 | 'nonce' in obj 48 | 49 | // is a Transaction Receipt if it has a status 50 | const isJSONExternalTransactionReceipt = (obj: any): obj is IJSONExternalTransactionReceipt => 51 | 'status' in obj 52 | 53 | // is an external transaction (vs internal transaction) if it has external property that is truthy 54 | export const isExternalTransaction = (obj: any): obj is ExternalTransaction => 55 | 'external' in obj && obj.external 56 | 57 | export default class ExternalTransaction extends Transaction { 58 | public external: boolean = true 59 | 60 | public block: Block 61 | 62 | public nonce: BN 63 | public hash: string 64 | public index: BN 65 | public blockNumber: BN 66 | public blockHash: string 67 | public cumulativeGasUsed: BN | null 68 | public logs: Log[] 69 | public logsBloom: string 70 | public status: BN 71 | public contractAddress: string | null 72 | public gasPrice: BN 73 | 74 | public internalTransactions: InternalTransaction[] 75 | 76 | public constructor (block: Block, tx: IJSONExternalTransaction) { 77 | super() 78 | this.block = block 79 | 80 | this.setSelf(tx) 81 | } 82 | 83 | public getReceipt = async () => { 84 | await this.getAndSetReceipt() 85 | } 86 | 87 | public getInternalTransactions = async () => { 88 | await this.setInternalTransactions() 89 | } 90 | 91 | private getAndSetReceipt = async () => { 92 | const txReceipt = await globalState.api.getTransactionReceipt(this.hash) 93 | this.setSelf(txReceipt) 94 | } 95 | 96 | private setInternalTransactions = async () => { 97 | let traces 98 | try { 99 | traces = await globalState.api.traceTransaction(this.hash) 100 | this.internalTransactions = traces.map((itx) => new InternalTransaction(this, itx)) 101 | } catch (error) { 102 | throw new Error(`IngestAPI#traceTransaction not working: ${error.stack}`) 103 | } 104 | } 105 | 106 | private setSelf = (tx: IJSONExternalTransactionInfo) => { 107 | if (isJSONExternalTransaction(tx)) { 108 | this.nonce = toBN(tx.nonce) 109 | this.hash = tx.hash 110 | this.index = toBN(tx.transactionIndex) 111 | this.blockNumber = toBN(tx.blockNumber) 112 | this.blockHash = tx.blockHash 113 | this.from = tx.from 114 | this.to = tx.to 115 | this.value = toBN(tx.value) 116 | this.gasPrice = toBN(tx.gasPrice) 117 | this.gas = toBN(tx.gas) 118 | this.input = tx.input 119 | } else if (isJSONExternalTransactionReceipt(tx)) { 120 | this.cumulativeGasUsed = toBN(tx.cumulativeGasUsed) 121 | this.gasUsed = toBN(tx.gasUsed) 122 | this.contractAddress = tx.contractAddress 123 | this.logs = tx.logs.map((l) => new Log(this, l)) 124 | this.status = toBN(tx.status) 125 | } else { 126 | throw new Error(`Unexpected type in Transaction#setSelf(): ${JSON.stringify(tx)}`) 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /packages/gnarly-core/src/models/InternalTransaction.ts: -------------------------------------------------------------------------------- 1 | import BN = require('bn.js') 2 | 3 | import { toBN } from '../utils' 4 | import ExternalTransaction from './ExternalTransaction' 5 | import Transaction from './Transaction' 6 | 7 | export interface IJSONInternalTransaction { 8 | action: { 9 | callType: string, 10 | from: string, 11 | gas: string, 12 | input: string, 13 | to: string, 14 | value: string, 15 | } 16 | // blockHash: string 17 | // blockNumber: string 18 | result: { 19 | gasUsed: string, 20 | output: string, 21 | } 22 | subtraces: number 23 | /** 24 | * traceAddress field 25 | * The traceAddress field of all returned traces, gives the exact location in 26 | * the call trace [index in root, index in first CALL, index in second CALL, …]. 27 | * i.e. if the trace is: 28 | * A 29 | * CALLs B 30 | * CALLs G 31 | * CALLs C 32 | * CALLs G 33 | * 34 | * then it should look something like: 35 | * 36 | * [ {A: []}, {B: [0]}, {G: [0, 0]}, {C: [1]}, {G: [1, 0]} ] 37 | */ 38 | traceAddress: number[] 39 | // transactionHash: string 40 | // transactionPosition: string 41 | type: string 42 | error: string | null 43 | } 44 | 45 | export const isInternalTransaction = (obj: any): obj is InternalTransaction => 'internal' in obj 46 | 47 | export default class InternalTransaction extends Transaction { 48 | public internal: boolean = true 49 | 50 | public transaction: ExternalTransaction 51 | 52 | public callType: string 53 | 54 | public blockHash: string 55 | public blockNumber: BN 56 | public result: { 57 | gasUsed: BN, 58 | output: string, 59 | } 60 | public subtraces: number 61 | public traceAddress: number[] 62 | public transactionHash: string 63 | // public transactionPosition: string 64 | public type: string 65 | // ^ "call" | ?? 66 | 67 | public error: string | null = null 68 | 69 | constructor (tx: ExternalTransaction, itx: IJSONInternalTransaction) { 70 | super() 71 | 72 | this.transaction = tx 73 | this.callType = itx.action.callType 74 | this.from = itx.action.from 75 | this.to = itx.action.to 76 | // @TODO(shrugs) - contractAddress for deploys? 77 | this.input = itx.action.input 78 | this.from = itx.action.from 79 | this.gas = toBN(itx.action.gas) 80 | this.value = toBN(itx.action.value) 81 | this.blockHash = tx.blockHash 82 | this.blockNumber = tx.blockNumber 83 | 84 | this.subtraces = itx.subtraces 85 | this.traceAddress = itx.traceAddress 86 | this.transactionHash = tx.hash 87 | 88 | // this.transactionPosition = tx.transactionPosition 89 | this.type = itx.type 90 | 91 | if (itx.error !== undefined) { 92 | this.error = itx.error 93 | return 94 | } 95 | 96 | this.result = { 97 | ...itx.result, 98 | gasUsed: toBN(itx.result.gasUsed), 99 | } 100 | 101 | // invariant: itx.transactionHash === this.transaction.hash 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /packages/gnarly-core/src/models/Log.ts: -------------------------------------------------------------------------------- 1 | import makeDebug = require('debug') 2 | const debug = makeDebug('gnarly-core:Log') 3 | 4 | import BN = require('bn.js') 5 | import abi = require('web3-eth-abi') 6 | 7 | import { globalState } from '../globalstate' 8 | import { toBN } from '../utils' 9 | import ExternalTransaction from './ExternalTransaction' 10 | 11 | export interface IJSONLog { 12 | address: string 13 | topics: string[] 14 | data: string 15 | blockNumber: string 16 | blockHash: string 17 | transactionHash: string 18 | transactionIndex: string 19 | logIndex: string 20 | removed: boolean 21 | } 22 | 23 | export default class Log { 24 | public readonly logIndex: BN 25 | public readonly blockNumber: BN 26 | public readonly blockHash: string 27 | public readonly transactionHash: string 28 | public readonly transactionIndex: BN 29 | public readonly address: string 30 | public readonly data: string 31 | /** 32 | * topics is an array of length [0, 4] 33 | * that has the indexed arguments from your event 34 | * in solidity, the first argument is always the hash of the event signature 35 | * (this way it's easy to make a logFilter for events of a certain type) 36 | */ 37 | public topics: string[] 38 | public event: string 39 | public eventName: string 40 | public signature: string 41 | public args: object 42 | 43 | private transaction: ExternalTransaction 44 | 45 | public constructor (tx: ExternalTransaction | null, log: IJSONLog) { 46 | this.transaction = tx 47 | 48 | this.logIndex = toBN(log.logIndex) 49 | this.blockNumber = toBN(log.blockNumber) 50 | this.blockHash = log.blockHash 51 | this.transactionHash = log.transactionHash 52 | this.transactionIndex = toBN(log.transactionIndex) 53 | this.address = log.address 54 | this.data = log.data 55 | this.topics = log.topics 56 | } 57 | 58 | public parse = (): boolean => { 59 | const registeredAbi = globalState.getABI(this.address) 60 | 61 | if (!registeredAbi) { return false } 62 | // ^ we do not know about this contract, so we can't try to parse it 63 | 64 | if (this.topics.length === 0) { return false } 65 | // ^ there are no topics, which means this is an anonymous event or something 66 | // and therefore we don't care about it (for now?) 67 | 68 | const [eventSig, ...topics] = this.topics 69 | // ^ the first argument in topics (from solidity) is always the event signature 70 | 71 | // find the inputs by signature 72 | const logAbiItem = registeredAbi.find((item) => item.signature === eventSig) 73 | if (logAbiItem === undefined) { 74 | // ^ we don't have an input that matches this event (incomplete ABI?) 75 | return false 76 | } 77 | 78 | let args 79 | try { 80 | args = abi.decodeLog( 81 | logAbiItem.inputs, 82 | this.data, 83 | topics, 84 | // ^ ignore the signature 85 | ) 86 | } catch (error) { 87 | // decodeLog failed for some reason (null address?) 88 | debug( 89 | `Could not parse log: 90 | blockHash: %s 91 | eventSig: %s 92 | abiItem: %j 93 | data: %s 94 | topics: %j 95 | %O 96 | `, 97 | this.blockHash, 98 | eventSig, 99 | logAbiItem, 100 | this.data, 101 | topics, 102 | error.stack, 103 | ) 104 | 105 | return false 106 | } 107 | 108 | this.event = logAbiItem.fullName 109 | this.eventName = logAbiItem.name 110 | this.signature = logAbiItem.signature 111 | 112 | this.args = args 113 | 114 | return true 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /packages/gnarly-core/src/models/Transaction.ts: -------------------------------------------------------------------------------- 1 | import makeDebug = require('debug') 2 | const debug = makeDebug('gnarly-core:Transaction') 3 | 4 | import BN = require('bn.js') 5 | import abi = require('web3-eth-abi') 6 | 7 | import { globalState } from '../globalstate' 8 | import { getMethodId } from '../utils' 9 | 10 | export default class Transaction { 11 | public from: string 12 | public to: string 13 | public value: BN 14 | public input: string 15 | public gasUsed: BN 16 | public gas: BN 17 | 18 | public method: string 19 | // transfer 20 | public methodName: string 21 | // transfer(address) 22 | public signature: string 23 | // 0x1234567890 24 | public methodId: string 25 | // 0x12345678 26 | public args: object = {} 27 | 28 | public parse = () => { 29 | if (!this.input) { return } 30 | if (this.input.length < 10) { return } 31 | // ^ has data, but not enough for a method call 32 | 33 | const registeredAbi = globalState.getABI(this.to) 34 | if (!registeredAbi) { return } 35 | // ^ we do not know about this contract, so we can't try to parse it 36 | 37 | // parse out method id 38 | const methodId = getMethodId(this.input) 39 | 40 | // look up abi in global state 41 | const methodAbi = globalState.getMethod(this.to, methodId) 42 | if (!methodAbi) { return } 43 | 44 | // we have a method abi, so parse it out 45 | this.method = methodAbi.name 46 | this.methodName = methodAbi.fullName 47 | this.signature = methodAbi.signature 48 | this.methodId = methodAbi.shortId 49 | 50 | // get abi item 51 | const abiItem = registeredAbi.find((item) => item.signature === this.signature) 52 | if (!abiItem) { return } 53 | // ^ using incorrect abi 54 | 55 | const data = this.input.replace(abiItem.shortId, '0x') 56 | // ^ remove function shortId from input data 57 | 58 | try { 59 | this.args = abi.decodeParameters(abiItem.inputs, data) 60 | } catch (error) { 61 | // decodeTransaction failed for some reason (null address?) 62 | debug( 63 | `Could not parse transaction: 64 | %O 65 | `, 66 | error.stack, 67 | ) 68 | return 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /packages/gnarly-core/src/ourbit/Ourbit.ts: -------------------------------------------------------------------------------- 1 | import makeDebug = require('debug') 2 | 3 | import { 4 | applyPatch, 5 | generate, 6 | observe, 7 | unobserve, 8 | } from '@xlnt/fast-json-patch' 9 | import uuid = require('uuid') 10 | 11 | import { globalState } from '../globalstate' 12 | import { ReducerContext } from '../reducer' 13 | import { invertPatch, operationsOfPatches, toOperation } from '../utils' 14 | import { 15 | IOperation, 16 | IPatch, 17 | ITransaction, 18 | ITxExtra, 19 | PersistPatchHandler, 20 | } from './types' 21 | 22 | /* 23 | * Ourbit 24 | * 25 | * > because I couldn't think of a better name than `urbit` and this is our's 26 | * 27 | * a transaction is a discrete set of events that produce patches to the state 28 | * but should be treated as a single atomic unit that can be reverted. 29 | * 30 | * when using gnarly with blockchains, the tx.id is the block hash 31 | * because it happens to be globally unique 32 | * (but this should not be relied on) 33 | * 34 | * ourbit's responsibilities: 35 | * - ourbit = new Ourbit(targetState, store, persistPatch) 36 | * - ourbit.resumeFromTxId(txId = null) 37 | * - ourbit.processTransaction(txId, operation producer) 38 | * - ourbit.rollbackTransaction(blockHash) 39 | */ 40 | class Ourbit { 41 | 42 | private debug 43 | private debugNotifyPatches 44 | 45 | constructor ( 46 | private key: string, 47 | private targetState: object, 48 | private persistPatch: PersistPatchHandler, 49 | private context: ReducerContext, 50 | ) { 51 | this.debug = makeDebug(`gnarly-core:ourbit:${key}`) 52 | this.debugNotifyPatches = makeDebug(`gnarly-core:ourbit:${key}:notifyPatches`) 53 | } 54 | 55 | /** 56 | * Tracks and perists patches (created by fn) by txId 57 | * @param txId transaction id 58 | * @param fn mutating function 59 | */ 60 | public processTransaction = async ( 61 | txId: string, 62 | fn: () => Promise, 63 | extra: ITxExtra = { blockHash: '' }, 64 | ) => { 65 | const patches: IPatch[] = [] 66 | 67 | // watch for patches to the memory state 68 | const observer = observe(this.targetState, (ops) => { 69 | patches.push({ 70 | id: uuid.v4(), 71 | operations: ops.map((op) => ({ 72 | ...op, 73 | volatile: false, 74 | })), 75 | reason: this.context.getCurrentReason(), 76 | }) 77 | }) 78 | 79 | // allow reducer to force-collect patches for order-dependent operations 80 | this.context.setPatchGenerator(() => { 81 | generate(observer) 82 | }) 83 | 84 | // collect any operations that are directly emitted 85 | this.context.setOpCollector((op: IOperation) => { 86 | patches.push({ 87 | id: uuid.v4(), 88 | operations: [op], 89 | reason: this.context.getCurrentReason(), 90 | }) 91 | }) 92 | 93 | // produce operations 94 | await fn() 95 | 96 | // unobserve 97 | unobserve(this.targetState, observer) 98 | 99 | // commit transaction 100 | await this.commitTransaction({ 101 | id: txId, 102 | patches, 103 | ...extra, 104 | }) 105 | } 106 | 107 | /** 108 | * Applys inverse patches from a specific transaction, mutating the target state 109 | * @TODO(shrugs) - make this a "fix-forward" operation and include event log 110 | * @param txId transaction id 111 | */ 112 | public rollbackTransaction = async (blockHash: string) => { 113 | const tx = await globalState.store.getTransactionByBlockHash(this.key, blockHash) 114 | await this.uncommitTransaction(tx) 115 | } 116 | 117 | /** 118 | * Replays all patches on the targetState from txId 119 | * @param txId transaction id 120 | */ 121 | public async resumeFromTxId (txId: string) { 122 | this.debug('Resuming from txId %s', txId) 123 | const allTxs = await globalState.store.getAllTransactionsTo(this.key, txId) 124 | let totalPatches = 0 125 | for await (const batch of allTxs) { 126 | const txBatch = batch as ITransaction[] 127 | txBatch.forEach((tx) => { 128 | totalPatches += tx.patches.length 129 | this.debug('[applyPatch] %s %d', tx.id, tx.patches.length) 130 | const allOperations = operationsOfPatches(tx.patches) 131 | applyPatch(this.targetState, allOperations.map(toOperation)) 132 | }) 133 | } 134 | this.debug('finished applying %d patches', totalPatches) 135 | } 136 | 137 | private notifyPatches = async (txId: string, patches: IPatch[]) => { 138 | this.debugNotifyPatches('txId: %s, patches: %j', txId, patches) 139 | for (const patch of patches) { 140 | await this.persistPatch(txId, patch) 141 | } 142 | } 143 | 144 | private commitTransaction = async (tx: ITransaction) => { 145 | // save transaction 146 | await globalState.store.saveTransaction(this.key, tx) 147 | // apply to store 148 | await this.notifyPatches(tx.id, tx.patches) 149 | // (no need to apply locally because they've been applied by the reducer) 150 | } 151 | 152 | private uncommitTransaction = async (tx: ITransaction) => { 153 | // @TODO(shrugs) - replace this with something like 154 | // this.commitTransaction(invertTransaction(tx)) 155 | 156 | // construct inverse patches 157 | const inversePatches = tx.patches.map(invertPatch) 158 | const inverseOperations = operationsOfPatches(inversePatches) 159 | 160 | const mutableOperations = inverseOperations.filter((op) => !op.volatile) 161 | // apply mutable changes locally 162 | applyPatch(this.targetState, mutableOperations.map(toOperation)) 163 | // apply to store (mutable and volatile) 164 | await this.notifyPatches(tx.id, inversePatches) 165 | // delete transaction 166 | await globalState.store.deleteTransaction(this.key, tx) 167 | } 168 | } 169 | 170 | export default Ourbit 171 | -------------------------------------------------------------------------------- /packages/gnarly-core/src/ourbit/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | default, 3 | } from './Ourbit' 4 | 5 | export * from './types' 6 | -------------------------------------------------------------------------------- /packages/gnarly-core/src/ourbit/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * An Operation is a state transition. 3 | * It is invertable via oldValue. 4 | * If it is volatile, it is not persisted in memory and is handled differently. 5 | */ 6 | export interface IOperation { 7 | path: string, 8 | op: 'add' | 'replace' | 'remove' | 'move' | 'copy' | 'test' | '_get', 9 | // ^ we will only actually have add|replace|remove 10 | // but fast-json-patch expects this type so whatever 11 | value?: any, 12 | oldValue?: any, 13 | volatile: boolean 14 | } 15 | 16 | /** 17 | * A reason for a patch 18 | */ 19 | export interface IReason { 20 | key: string 21 | meta: any 22 | } 23 | 24 | /** 25 | * a gnarly-specific path generated from patch.op.path 26 | */ 27 | export interface IPathThing { 28 | tableName: string 29 | pk: string 30 | indexOrKey: string 31 | } 32 | 33 | /** 34 | * A Patch is a set of operations with a unique id and a reason for their existence. 35 | */ 36 | export interface IPatch { 37 | id: string 38 | reason?: { key: string, meta?: any } 39 | operations: IOperation[], 40 | } 41 | 42 | /** 43 | * A transaction is a set of patches. 44 | */ 45 | export interface ITransaction { 46 | id: string 47 | blockHash: string, 48 | patches: IPatch[] 49 | } 50 | 51 | export type OpCollector = (op: IOperation) => void 52 | 53 | export interface ITxExtra { 54 | blockHash: string 55 | } 56 | 57 | /** 58 | * This function accept patches and persists them to a store. 59 | */ 60 | export type PersistPatchHandler = (txId: string, patch: IPatch) => Promise 61 | -------------------------------------------------------------------------------- /packages/gnarly-core/src/polyfills/asynciterator-polyfill.ts: -------------------------------------------------------------------------------- 1 | (Symbol as any).asyncIterator = 2 | (Symbol as any).asyncIterator || 3 | Symbol.for('Symbol.asyncIterator') 4 | -------------------------------------------------------------------------------- /packages/gnarly-core/src/reducer/ReducerContext.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IOperation, 3 | OpCollector, 4 | } from '../ourbit/types' 5 | 6 | import { 7 | IReducerUtils, 8 | OperationPerformerFn, 9 | PatchGenerator, 10 | } from './types' 11 | 12 | class ReducerContext { 13 | 14 | public utils: IReducerUtils 15 | 16 | private currentReason: string = null 17 | private currentMeta: any = null 18 | 19 | private forceGeneratePatches: PatchGenerator 20 | private opCollector: OpCollector 21 | 22 | constructor ( 23 | private key: string, 24 | ) { 25 | this.utils = { 26 | because: this.because, 27 | operation: this.operation, 28 | emit: this.emit, 29 | } 30 | } 31 | 32 | public because = (reason: string, meta: any, fn: OperationPerformerFn) => { 33 | this.currentReason = reason 34 | this.currentMeta = meta 35 | 36 | this.operation(fn) 37 | 38 | this.currentReason = null 39 | this.currentMeta = null 40 | } 41 | 42 | public getCurrentReason = () => { 43 | return this.currentReason !== null 44 | ? { key: this.currentReason, meta: this.currentMeta } 45 | : undefined 46 | } 47 | 48 | /** 49 | * Perform an explicit operation, which is most likely order-dependent 50 | */ 51 | public operation = (fn: OperationPerformerFn) => { 52 | fn() 53 | this.forceGeneratePatches() 54 | } 55 | 56 | /** 57 | * Emit a specific operation, which is not tracked in the local state 58 | * This should be used for immutable information 59 | * (namely, event logs) 60 | */ 61 | public emit = (op: IOperation) => { 62 | this.opCollector(op) 63 | } 64 | 65 | public setOpCollector = (fn: OpCollector) => { 66 | this.opCollector = fn 67 | } 68 | 69 | public setPatchGenerator = (fn: PatchGenerator) => { 70 | this.forceGeneratePatches = fn 71 | } 72 | } 73 | 74 | export default ReducerContext 75 | -------------------------------------------------------------------------------- /packages/gnarly-core/src/reducer/index.ts: -------------------------------------------------------------------------------- 1 | 2 | export { 3 | default as ReducerContext, 4 | } from './ReducerContext' 5 | 6 | export * from './types' 7 | -------------------------------------------------------------------------------- /packages/gnarly-core/src/reducer/types.ts: -------------------------------------------------------------------------------- 1 | import Block from '../models/Block' 2 | import { IOperation, IReason, OpCollector } from '../ourbit/types' 3 | import { ITypeStore } from '../typeStore' 4 | 5 | export enum ReducerType { 6 | /** 7 | * Idempotent reducers don't care about _when_ they are called. 8 | * While the original state may vary with time, the function of (state) => nextState does _not_. 9 | * 10 | * This reducer type is only called once per-block when the blockstream is fully synced 11 | * (because executing it before then is a waste). 12 | * 13 | * This is most useful for simple getters/computed values from smart contract state 14 | * (like querying a specific user's token balance(s)). 15 | */ 16 | Idempotent = 'IDEMPOTENT', 17 | 18 | /** 19 | * TimeVarying reducers care about the time at which they are called. The original state 20 | * may vary with time, and that information cannot be lost. 21 | * 22 | * This reducer is called once per-block for every block gnarly ingests. 23 | * 24 | * This is most useful for things where the history of state is derived 25 | * (like transaction history or art provenance). 26 | */ 27 | TimeVarying = 'TIME_VARYING', 28 | 29 | /** 30 | * Atomic reducers do not use time-sensitive values in their derivations, but require state 31 | * produced during every block. This means they can be run in parallel. 32 | * 33 | * NOTE: currently an Atomic reducer is === TimeVarying, but may be optimized to run in parallel 34 | * in the future. 35 | * 36 | * This type of reducer is called once per-block and produces an atomic operation. 37 | * 38 | * This is most useful for traditional maps and reductions of state ala MapReduce 39 | * (like keeping track of total transaction count). 40 | */ 41 | Atomic = 'ATOMIC', 42 | } 43 | 44 | export interface IReducerConfig { 45 | /** 46 | * The type of reducer (how and when is it called?) 47 | */ 48 | type: ReducerType, 49 | 50 | /** 51 | * The name of the reducer in the root state. 52 | */ 53 | key: string 54 | 55 | /** 56 | * The typestore to persist the reducer's state 57 | */ 58 | typeStore: ITypeStore 59 | } 60 | 61 | export type voidFunc = () => void 62 | 63 | /** 64 | * Function in charge of generating queued patches 65 | */ 66 | export type PatchGenerator = voidFunc 67 | /** 68 | * Functiont that performs operations on the state where patch order is important 69 | */ 70 | export type OperationPerformerFn = voidFunc 71 | /** 72 | * The operation() function that accepts a performer 73 | */ 74 | export type OperationFn = (fn: OperationPerformerFn) => void 75 | /** 76 | * The emit() function that accepts a direct operation 77 | */ 78 | export type EmitOperationFn = (operation: IOperation) => void 79 | /** 80 | * The because() function that accepts a reason and a operation performer 81 | */ 82 | export type BecauseFn = (reason: string, meta: any, fn: OperationPerformerFn) => void 83 | 84 | export interface IReducerUtils { 85 | emit: EmitOperationFn 86 | because: BecauseFn 87 | operation: OperationFn 88 | } 89 | 90 | /** 91 | * The reduce function takes state and block and produces action (implicit) 92 | */ 93 | export type TransactionProducer = ( 94 | state: object, 95 | block: Block, 96 | utils: IReducerUtils, 97 | ) => Promise 98 | 99 | export interface IReducer { 100 | config: IReducerConfig 101 | state: object, 102 | reduce: TransactionProducer 103 | } 104 | -------------------------------------------------------------------------------- /packages/gnarly-core/src/stores/IPersistInterface.ts: -------------------------------------------------------------------------------- 1 | import { IJSONBlock } from '../models/Block' 2 | import { ITransaction } from '../ourbit/types' 3 | 4 | export interface IPersistInterface { 5 | // @TODO - how do you get typescript to stop complaining about AsyncIterator symbols? 6 | 7 | // reducer CRUD 8 | saveReducer (reducerKey: string): Promise 9 | deleteReducer (reducerKey: string): Promise 10 | 11 | // blockstream CRUD 12 | getHistoricalBlocks (reducerKey: string): Promise 13 | saveHistoricalBlock (reducerKey: string, blockRetention: number, block: IJSONBlock): Promise 14 | deleteHistoricalBlock (reducerKey: string, blockHash: string): Promise 15 | deleteHistoricalBlocks (reducerKey: string): Promise 16 | 17 | // transaction CRUD 18 | getAllTransactionsTo (reducerKey: string, toTxId: null | string): Promise 19 | getLatestTransaction (reducerKey: string): Promise 20 | deleteTransaction (reducerKey: string, tx: ITransaction): Promise 21 | saveTransaction (reducerKey: string, tx: ITransaction): Promise 22 | getTransaction (reducerKey: string, txId: string): Promise 23 | getTransactionByBlockHash (reducerKey: string, blockHash: string): Promise 24 | 25 | // event log CRUD actions 26 | 27 | // setup 28 | setup (): Promise 29 | setdown (): Promise 30 | } 31 | -------------------------------------------------------------------------------- /packages/gnarly-core/src/stores/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | default as SequelizePersistInterface, 3 | makeSequelizeModels, 4 | } from './sequelize' 5 | 6 | export { 7 | IPersistInterface, 8 | } from './IPersistInterface' 9 | -------------------------------------------------------------------------------- /packages/gnarly-core/src/typeStore/Sequelize.ts: -------------------------------------------------------------------------------- 1 | import makeDebug = require('debug') 2 | const debug = makeDebug('gnarly-core:store:sequelize') 3 | 4 | import isPlainObject = require('lodash.isplainobject') 5 | 6 | import { IOperation, IPatch } from '../ourbit/types' 7 | import { 8 | parsePath, 9 | } from '../utils' 10 | 11 | const withOrder = (order, value) => ({ 12 | ...value, 13 | order: parseInt(order, 10), 14 | }) 15 | 16 | const getForeignKeys = (model) => Object.keys(model.rawAttributes).filter((k) => 17 | !!model.rawAttributes[k].references, 18 | ) 19 | 20 | /** 21 | * the persist function accepts a patch and has a side effect of 22 | * updating the sql database given a schema 23 | * 24 | * This needs the Sequelize thing passed in cause literal doesn't work 25 | * without the dialect configuration or something, it's really dumb 26 | * https://github.com/sequelize/sequelize/issue1s/9121 27 | */ 28 | const buildTypeStore = (Sequelize, schema) => async ( 29 | patchId: string, 30 | operation: IOperation, 31 | ) => { 32 | const { Op, literal } = Sequelize 33 | 34 | const { 35 | op, 36 | value, 37 | path, 38 | } = operation 39 | 40 | const { 41 | tableName, 42 | pk, 43 | indexOrKey, 44 | } = parsePath(path) 45 | 46 | debug('path: %j', parsePath(path)) 47 | debug('op: %j', operation) 48 | debug('schema: ', Object.keys(schema)) 49 | 50 | const hasIndexOrKey = indexOrKey !== undefined 51 | // ^ do we have an index OR a key? 52 | const index = parseInt(indexOrKey, 10) 53 | // ^ Number | NaN, doesn't throw 54 | const isIndex = !Number.isNaN(index) 55 | // ^ whether or not this is a numeric index or a string key 56 | 57 | const withMeta = (v) => ({...v, patchId}) 58 | const model = schema[tableName] 59 | const { 60 | primaryKeyAttribute, 61 | primaryKeyAttributes, 62 | } = model 63 | // const foreignKeys = getForeignKeys(model) 64 | // const hasForeignKey = foreignKeys.length > 0 65 | 66 | if (primaryKeyAttributes.length > 1) { 67 | throw new Error(`Gnarly#SequelizeTypeStore only supports single-primary keys at the moment. 68 | Do not use a composite key. Instead concatenate keys to get a single unique "composite" key 69 | and then just add a composite unique index for similar performance.`) 70 | } 71 | 72 | const selector = primaryKeyAttribute 73 | 74 | // restrict all references below to objects with the following window, scoped by primary key value 75 | const window = { 76 | [selector]: { [Op.eq]: pk }, 77 | } 78 | 79 | const addSingle = async (properties) => { 80 | await model.create(withMeta(properties)) 81 | } 82 | 83 | debug( 84 | ` 85 | tableName: %s 86 | pk: %s 87 | indexOrKey: %s 88 | op: %s 89 | value: %j 90 | window: %j 91 | `, 92 | tableName, 93 | pk, 94 | indexOrKey, 95 | op, 96 | (op as any).value, 97 | window, 98 | ) 99 | switch (op) { 100 | case 'add': { 101 | if (isPlainObject(value)) { 102 | // we're inserting a row 103 | if (isIndex) { 104 | // if there's an index on this, we're actually inserting at a specific order 105 | // which means we need to increase the order of everything after this index 106 | await model.update({ 107 | order: literal('"order" + 1'), 108 | }, { 109 | where: { 110 | ...window, 111 | order: { [Op.gte]: index }, 112 | }, 113 | }) 114 | // then insert the new item at that index 115 | await addSingle(withOrder(indexOrKey, value)) 116 | } else { 117 | await addSingle(value) 118 | } 119 | } else if (Array.isArray(value)) { 120 | if (isIndex) { 121 | // if there's an array of values at an index or key here, 122 | // we're trying to do some weird nested situation 123 | // and we don't support that (yet?) 124 | throw new Error(` 125 | Received index or key "${indexOrKey}" 126 | and value ${JSON.stringify(value)} 127 | should be arrays. 128 | `) 129 | } 130 | 131 | // if we get an array of values in an add operation, we're just inserting 132 | // a bunch of rows to initialize the view 133 | // and those rows have some order 134 | // (because things with primary keys will arrive as multiple different patches) 135 | for (const [i, v] of value.entries()) { 136 | await addSingle(withOrder(i, v)) 137 | } 138 | } else { 139 | // we're updating a discreet property but don't know the key 140 | // so this is probably an issue with the user's store typing 141 | throw new Error(` 142 | Attempted to add discreet value ${JSON.stringify(value)} 143 | at primaryKey ${pk} in table ${model.tableName}, 144 | which only supports properties. 145 | `) 146 | } 147 | break 148 | } 149 | case 'replace': { 150 | if (!hasIndexOrKey || isIndex) { 151 | throw new Error(` 152 | No 'indexOrKey' in op ${operation} 153 | for value "${JSON.stringify(value)}" or the value was numeric. 154 | We expect a discreet string key here. 155 | `) 156 | } 157 | 158 | // updating a value which should definitely be a discreet value 159 | model.update(withMeta({ 160 | [indexOrKey]: value, 161 | }), { 162 | where: window, 163 | }) 164 | break 165 | } 166 | case 'remove': { 167 | await model.destroy({ 168 | where: window, 169 | limit: 1, 170 | // if we got an indexOrKey in a removal operation, we need to splice 171 | // by offsetting the window 172 | offset: isIndex ? index : 0, 173 | }) 174 | 175 | if (isIndex) { 176 | // we removed an item, so subtract an order index from everything greater 177 | await model.update({ 178 | order: literal('"order" - 1'), 179 | }, { 180 | where: { 181 | ...window, 182 | order: { [Op.gt]: indexOrKey }, 183 | }, 184 | }) 185 | } 186 | break 187 | } 188 | default: 189 | throw new Error('wut') 190 | } 191 | } 192 | 193 | export default buildTypeStore 194 | -------------------------------------------------------------------------------- /packages/gnarly-core/src/typeStore/index.ts: -------------------------------------------------------------------------------- 1 | import { IOperation } from '../ourbit/types' 2 | 3 | export type TypeStorer = (txId: string, patch: IOperation) => Promise 4 | export type SetupFn = () => Promise 5 | export type SetdownFn = () => Promise 6 | export interface ITypeStore { 7 | [_: string]: TypeStorer | SetupFn | SetdownFn, 8 | } 9 | 10 | export { 11 | default as SequelizeTypeStorer, 12 | } from './Sequelize' 13 | -------------------------------------------------------------------------------- /packages/gnarly-core/src/types/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XLNT/gnarly/e8c2dfe683c115cd296ce4e7be23606de8190958/packages/gnarly-core/src/types/.gitkeep -------------------------------------------------------------------------------- /packages/gnarly-core/src/utils.ts: -------------------------------------------------------------------------------- 1 | import { Operation } from '@xlnt/fast-json-patch' 2 | import BN = require('bn.js') 3 | import _ = require('lodash') 4 | import memoize from 'moize' 5 | import numberToBN = require('number-to-bn') 6 | import pMap = require('p-map') 7 | import uuid = require('uuid') 8 | import web3Utils = require('web3-utils') 9 | 10 | import IABIItem, { IABIItemInput } from './models/ABIItem' 11 | import { 12 | IOperation, 13 | IPatch, 14 | IPathThing, 15 | } from './ourbit/types' 16 | 17 | const API_CACHE_MAX_AGE = 1000 18 | 19 | export const cacheApiRequest = (fn) => memoize(fn, { 20 | isPromise: true, 21 | maxAge: API_CACHE_MAX_AGE, 22 | }) 23 | 24 | export const parsePath = (path: string): IPathThing => { 25 | const [ 26 | emptyString, // ignore this 27 | tableName, 28 | pk, 29 | indexOrKey, 30 | ] = path.split('/') 31 | return { 32 | tableName, 33 | pk, 34 | indexOrKey, 35 | } 36 | } 37 | 38 | export const toBN = (v: string | number | BN): BN => numberToBN(v) 39 | 40 | export const forEach = async (iterable, mapper, opts = { concurrency: 10 }) => 41 | pMap(iterable, mapper, opts) 42 | 43 | export const addressesEqual = (left: string, right: string): boolean => { 44 | return left && right && left.toLowerCase() === right.toLowerCase() 45 | } 46 | 47 | const supportedTypes: string[] = ['function', 'event'] 48 | export const onlySupportedAbiItems = (item: IABIItemInput): boolean => 49 | supportedTypes.includes(item.type) 50 | 51 | export const enhanceAbiItem = (item: IABIItemInput): IABIItem => { 52 | const fullName = web3Utils._jsonInterfaceMethodToString(item) 53 | const signature = web3Utils.sha3(fullName) 54 | const shortId = signature.substr(0, 10) 55 | 56 | return { 57 | ...item, 58 | fullName, 59 | signature, 60 | shortId, 61 | } 62 | } 63 | 64 | // we dont' do anything special here, but it helps add structure 65 | // ¯\_(ツ)_/¯ 66 | export const makeRootTypeStore = (typestore: object): object => typestore 67 | 68 | export const getMethodId = (input: string) => input.substr(0, 10) 69 | // ^0x12345678 70 | 71 | export const toHex = (num: BN) => `0x${num.toString(16)}` 72 | 73 | export const timeout = async (ms: number = 0) => 74 | new Promise((resolve) => 75 | setTimeout(resolve, ms)) 76 | 77 | export const invertOperation = (operation: IOperation): IOperation => { 78 | switch (operation.op) { 79 | case 'add': 80 | return { 81 | ...operation, 82 | op: 'remove', 83 | oldValue: operation.value, 84 | value: undefined, 85 | } 86 | case 'remove': 87 | return { 88 | ...operation, 89 | op: 'add', 90 | value: operation.oldValue, 91 | oldValue: undefined, 92 | } 93 | case 'replace': 94 | return { 95 | ...operation, 96 | op: 'replace', 97 | value: operation.oldValue, 98 | oldValue: operation.value, 99 | } 100 | } 101 | 102 | throw new Error(`Could not invert operation ${JSON.stringify(operation)}`) 103 | } 104 | 105 | export const invertPatch = (patch: IPatch): IPatch => ({ 106 | ...patch, 107 | operations: patch.operations.map(invertOperation).reverse(), 108 | reason: { 109 | key: 'ROLLBACK', 110 | meta: { prevReason: patch.reason }, 111 | }, 112 | }) 113 | 114 | export const operationsOfPatch = (patch: IPatch): IOperation[] => 115 | patch.operations 116 | export const operationsOfPatches = (patches: IPatch[]): IOperation[] => 117 | _.flatMap(patches, operationsOfPatch) 118 | 119 | export const toOperation = (operation: IOperation): Operation => operation as Operation 120 | 121 | export const appendTo = ( 122 | domain: string, 123 | value: any, 124 | ): IOperation => { 125 | // forcefully add uuid to value 126 | value.uuid = uuid.v4() 127 | // for now, typeStores interpret an add operation without an index 128 | // as a normal sort of insert 129 | // so there's actually nothing special to do here 130 | // @TODO(shrugs) - this is not JSON Patch compliant because we should technically 131 | // have the index of the insertion as the final path part 132 | // but that requires a round trip to the database (previously done in-memory) 133 | // and we don't have the need for that right now 134 | return { 135 | op: 'add', 136 | path: `/${domain}/${value.uuid}`, 137 | value, 138 | volatile: true, 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /packages/gnarly-core/test/Blockstream.spec.ts: -------------------------------------------------------------------------------- 1 | import chai = require('chai') 2 | import 'mocha' 3 | 4 | import Blockstream from '../src/Blockstream' 5 | import { globalState } from '../src/globalstate' 6 | import { IJSONBlock } from '../src/models/Block' 7 | import { forEach, timeout, toBN, toHex } from '../src/utils' 8 | 9 | import IJSONBlockFactory from './factories/IJSONBlockFactory' 10 | import MockIngestApi from './mocks/MockIngestApi' 11 | import MockPersistInterface from './mocks/MockPersistInterface' 12 | 13 | const blockAfter = (block: IJSONBlock, fork: number = 1) => IJSONBlockFactory.build({ 14 | hash: toHex(toBN(block.hash).add(toBN(1 + 10 * fork))), 15 | number: toHex(toBN(block.number).add(toBN(1))), 16 | parentHash: block.hash, 17 | nonce: toHex(toBN(block.hash).add(toBN(1 + 10 * fork))), 18 | }) 19 | 20 | const genesis = () => [IJSONBlockFactory.build({ 21 | hash: '0x1', 22 | number: '0x1', 23 | parentHash: '0x0', 24 | nonce: '0x1', 25 | })] 26 | 27 | const buildChain = (from: IJSONBlock[], len: number = 10, fork: number = 1) => { 28 | const chain = [...from] 29 | for (let i = 0; i < len; i++) { 30 | chain.push(blockAfter(chain[chain.length - 1], fork)) 31 | } 32 | return chain 33 | } 34 | 35 | const RETENTION = 20 36 | const MOCK_REDUCER_KEY = 'test' 37 | const WAIT_FOR_HANDLERS = 100 // wait 100ms for handlers to be called 38 | 39 | const should = chai 40 | .use(require('chai-spies')) 41 | .should() 42 | 43 | const bootstrapHistoricalBlocks = async (reducerKey: string, blocks, store: MockPersistInterface, bs: Blockstream) => { 44 | await forEach(blocks, async (block) => store.saveHistoricalBlock(reducerKey, 100, block), { concurrency: 1 }) 45 | await bs.initWithHistoricalBlocks(blocks) 46 | } 47 | 48 | let realChain = [] as IJSONBlock[] 49 | describe('Blockstream', function () { 50 | beforeEach(async function () { 51 | 52 | this.processTransaction = chai.spy(async function (txId: string, fn: () => Promise, extra: object) { 53 | await fn() 54 | }) 55 | 56 | this.rollbackTransaction = chai.spy(async function (blockHash: string) { 57 | return 58 | }) 59 | 60 | this.onNewBlock = chai.spy(function (block: IJSONBlock, syncing: boolean) { 61 | return function () { 62 | // 63 | } 64 | }) 65 | 66 | this.api = new MockIngestApi() 67 | this.store = new MockPersistInterface() 68 | globalState.setApi(this.api) 69 | globalState.setStore(this.store) 70 | 71 | chai.spy.on(this.api, 'getLatestBlock', function () { 72 | return realChain[realChain.length - 1] 73 | }) 74 | chai.spy.on(this.api, 'getBlockByHash', function (hash) { 75 | return realChain.find((b) => b.hash === hash) 76 | }) 77 | chai.spy.on(this.api, 'getBlockByNumber', function (num) { 78 | return realChain.find((b) => b.number === toHex(num)) 79 | }) 80 | 81 | this.bs = new Blockstream( 82 | MOCK_REDUCER_KEY, 83 | this.processTransaction, 84 | this.rollbackTransaction, 85 | this.onNewBlock, 86 | RETENTION, 87 | ) 88 | 89 | this.onBlockAdd = chai.spy.on(this.bs, 'onBlockAdd') 90 | this.onBlockInvalidated = chai.spy.on(this.bs, 'onBlockInvalidated') 91 | }) 92 | 93 | afterEach(async function () { 94 | chai.spy.restore() 95 | }) 96 | 97 | context('instantiation', function () { 98 | it('can be constructed', async function () { 99 | const bs = new Blockstream( 100 | MOCK_REDUCER_KEY, 101 | this.processTransaction, 102 | this.rollbackTransaction, 103 | this.onNewBlock, 104 | 1, 105 | ) 106 | should.exist(bs) 107 | }) 108 | 109 | it('handles default args', async function () { 110 | const bs = new Blockstream( 111 | MOCK_REDUCER_KEY, 112 | this.processTransaction, 113 | this.rollbackTransaction, 114 | this.onNewBlock, 115 | ) 116 | should.exist(bs) 117 | }) 118 | }) 119 | 120 | context('without historical blocks', function () { 121 | 122 | beforeEach(async function () { 123 | realChain = buildChain(genesis(), 8) 124 | 125 | await this.bs.start() 126 | await this.api.forceSendBlock() 127 | await timeout(WAIT_FOR_HANDLERS) 128 | }) 129 | 130 | it('should start from head and only add the first block', async function () { 131 | await timeout(WAIT_FOR_HANDLERS) 132 | await this.bs.stop() 133 | 134 | this.onBlockAdd.should.have.been.called.exactly(1) 135 | this.onBlockInvalidated.should.not.have.been.called() 136 | this.processTransaction.should.have.been.called.exactly(1) 137 | this.rollbackTransaction.should.not.have.been.called() 138 | this.onNewBlock.should.have.been.called.exactly(1) 139 | }) 140 | 141 | it('should add new blocks when presented', async function () { 142 | realChain = buildChain(realChain, 2) 143 | const newBlocks = realChain.slice(-2) 144 | 145 | await this.api.forceSendBlock() 146 | await timeout(WAIT_FOR_HANDLERS) 147 | await this.bs.stop() 148 | 149 | newBlocks.forEach((block) => 150 | this.onNewBlock.should.have.been.called.with(block), 151 | ) 152 | }) 153 | 154 | it('should invalidate forked blocks', async function () { 155 | const shortFork = buildChain(realChain, 2, 1) 156 | const invalidBlocks = shortFork.slice(-2) 157 | const validChain = buildChain(realChain, 3, 2) 158 | const validBlocks = validChain.slice(-3) 159 | 160 | realChain = shortFork 161 | await this.api.forceSendBlock() 162 | await timeout(WAIT_FOR_HANDLERS) 163 | 164 | invalidBlocks.forEach((block) => 165 | this.onBlockAdd.should.have.been.called.with(block), 166 | ) 167 | 168 | realChain = validChain 169 | await this.api.forceSendBlock() 170 | await timeout(WAIT_FOR_HANDLERS) 171 | await this.bs.stop() 172 | 173 | invalidBlocks.forEach((block) => 174 | this.onBlockInvalidated.should.have.been.called.with(block), 175 | ) 176 | validBlocks.forEach((block) => { 177 | this.onBlockAdd.should.have.been.called.with(block) 178 | this.onNewBlock.should.have.been.called.with(block) 179 | }) 180 | }) 181 | }) 182 | 183 | context('with historical blocks', function () { 184 | it('should init with historical blocks and not call any handlers', async function () { 185 | realChain = buildChain(genesis(), 8) 186 | await bootstrapHistoricalBlocks(MOCK_REDUCER_KEY, realChain, this.store, this.bs) 187 | 188 | this.onBlockAdd.should.not.have.been.called() 189 | this.onBlockInvalidated.should.not.have.been.called() 190 | this.processTransaction.should.not.have.been.called() 191 | this.rollbackTransaction.should.not.have.been.called() 192 | this.onNewBlock.should.not.have.been.called() 193 | }) 194 | 195 | context('when presented with a future block within retention', function () { 196 | beforeEach(async function () { 197 | this.localChain = buildChain(genesis(), 7) // 8 blocks long 198 | this.remoteChain = buildChain(this.localChain, 12) // 20 blocks long 199 | this.newBlocks = this.remoteChain.slice(-12) 200 | realChain = this.localChain 201 | 202 | await bootstrapHistoricalBlocks(MOCK_REDUCER_KEY, realChain, this.store, this.bs) 203 | // now set the real chain to the remote chain 204 | realChain = this.remoteChain 205 | await this.bs.start(this.localChain[this.localChain.length - 1].hash) // from HEAD 206 | await this.api.forceSendBlock() 207 | await timeout(WAIT_FOR_HANDLERS) 208 | }) 209 | 210 | it('should fast forward and trigger add handlers', async function () { 211 | await this.bs.stop() 212 | this.newBlocks.forEach((block) => { 213 | this.onBlockAdd.should.have.been.called.with(block) 214 | this.onNewBlock.should.have.been.called.with(block) 215 | }) 216 | }) 217 | 218 | it('should not have invalided any blocks', async function () { 219 | await this.bs.stop() 220 | this.onBlockInvalidated.should.not.have.been.called() 221 | }) 222 | }) 223 | 224 | context('when presented with a future block beyond retention', function () { 225 | beforeEach(async function () { 226 | realChain = buildChain(genesis(), 8) 227 | await bootstrapHistoricalBlocks(MOCK_REDUCER_KEY, realChain, this.store, this.bs) 228 | 229 | const lastLocalBlock = realChain[realChain.length - 1] 230 | const remoteLength = RETENTION + 10 // 10 blocks past retention limit 231 | this.remoteChain = buildChain(realChain, remoteLength) 232 | this.newBlocks = this.remoteChain.slice(-1 * remoteLength) 233 | 234 | realChain = this.remoteChain 235 | await this.bs.start(lastLocalBlock.hash) // start from the local HEAD 236 | 237 | await this.api.forceSendBlock() 238 | await timeout(WAIT_FOR_HANDLERS) 239 | }) 240 | 241 | it('should fast forward manually and then defer to blockstream while triggering add handlers', async function () { 242 | this.newBlocks.forEach((block) => { 243 | this.onBlockAdd.should.have.been.called.with(block) 244 | this.onNewBlock.should.have.been.called.with(block) 245 | }) 246 | }) 247 | }) 248 | 249 | context('when presentend with a short lived fork', function () { 250 | beforeEach(async function () { 251 | const accurateChain = buildChain(genesis(), 7) // 8 blocks long 252 | const shortLivedFork = buildChain(accurateChain, 2, 1) // 10 blocks long 253 | this.invalidBlocks = shortLivedFork.slice(-2) 254 | realChain = shortLivedFork 255 | 256 | await bootstrapHistoricalBlocks(MOCK_REDUCER_KEY, realChain, this.store, this.bs) 257 | // tell blockstreamer that the last two blocks it saw were actually invalid by giving it a new longer chain 258 | await this.bs.start() 259 | const newChain = buildChain(accurateChain, 3, 2) // 11 blocks long 260 | this.validBlocks = newChain.slice(-3) 261 | realChain = newChain 262 | await this.api.forceSendBlock() 263 | await timeout(WAIT_FOR_HANDLERS) 264 | }) 265 | 266 | it('calls rollbackTransaction with the offending blocks', async function () { 267 | await this.bs.stop() 268 | 269 | this.onBlockInvalidated.should.have.been.called() 270 | this.invalidBlocks.forEach((block) => 271 | this.onBlockInvalidated.should.have.been.called.with(block), 272 | ) 273 | this.validBlocks.forEach((block) => 274 | this.onBlockAdd.should.have.been.called.with(block), 275 | ) 276 | }) 277 | }) 278 | }) 279 | }) 280 | -------------------------------------------------------------------------------- /packages/gnarly-core/test/Ourbit.spec.ts: -------------------------------------------------------------------------------- 1 | import chai = require('chai') 2 | import 'mocha' 3 | 4 | import uuid = require('uuid') 5 | import { globalState } from '../src/globalstate' 6 | import * as utils from '../src/utils' 7 | 8 | import Ourbit, { 9 | ITransaction, 10 | } from '../src/ourbit' 11 | import { ReducerContext } from '../src/reducer' 12 | import MockPersistInterface from './mocks/MockPersistInterface' 13 | 14 | chai 15 | .use(require('chai-spies')) 16 | .should() 17 | const sandbox = chai.spy.sandbox() 18 | 19 | const TEST_KEY = 'test' 20 | const TEST_REASON = 'TEST_REASON' 21 | const TEST_META = {} 22 | 23 | describe('Ourbit', () => { 24 | let ourbit: Ourbit = null 25 | 26 | let tx: ITransaction 27 | let targetState 28 | let persistPatch 29 | let context 30 | 31 | const produceFirstPatch = async () => { 32 | await ourbit.processTransaction(tx.id, async () => { 33 | targetState.key = 'value' 34 | }, { blockHash: tx.blockHash }) 35 | } 36 | 37 | beforeEach(() => { 38 | sandbox.on(uuid, 'v4', () => 'uuid') 39 | sandbox.on(globalState, [ 40 | 'setPatchGenerator', 41 | 'setOpCollector', 42 | 'getReason', 43 | ]) 44 | 45 | tx = { 46 | id: '0x1', 47 | blockHash: '0x1', 48 | patches: [{ 49 | id: 'uuid', 50 | reason: undefined, 51 | operations: [{ 52 | op: 'add', 53 | path: '/key', 54 | value: 'value', 55 | volatile: false, 56 | }], 57 | }], 58 | } 59 | 60 | targetState = {} 61 | const store = new MockPersistInterface() 62 | sandbox.on(store, [ 63 | 'saveTransaction', 64 | ]) 65 | globalState.setStore(store) 66 | context = new ReducerContext(TEST_KEY) 67 | 68 | persistPatch = chai.spy() 69 | ourbit = new Ourbit(TEST_KEY, targetState, persistPatch, context) 70 | }) 71 | 72 | afterEach(() => { 73 | sandbox.restore() 74 | }) 75 | 76 | it('should process a transaction', async () => { 77 | await produceFirstPatch() 78 | 79 | globalState.store.saveTransaction.should.have.been.called.with(TEST_KEY, tx) 80 | }) 81 | 82 | it('should process a transaction with default values', async () => { 83 | await ourbit.processTransaction(tx.id, async () => { 84 | targetState.key = 'value' 85 | }) 86 | 87 | const txWithoutBlockHash = tx 88 | tx.blockHash = '' 89 | 90 | globalState.store.saveTransaction.should.have.been.called.with(TEST_KEY, txWithoutBlockHash) 91 | }) 92 | 93 | it('should include a reason if provided', async () => { 94 | tx.patches[0].reason = { key: TEST_REASON, meta: TEST_META } 95 | 96 | await ourbit.processTransaction(tx.id, async () => { 97 | context.because(TEST_REASON, TEST_META, () => { 98 | targetState.key = 'value' 99 | }) 100 | }, { blockHash: tx.blockHash }) 101 | 102 | globalState.store.saveTransaction.should.have.been.called.with(TEST_KEY, tx) 103 | }) 104 | 105 | it('should allow manual collection', async () => { 106 | tx.patches.push({ 107 | id: 'uuid', 108 | reason: undefined, 109 | operations: [{ 110 | op: 'replace', 111 | path: '/key', 112 | oldValue: 'value', 113 | value: 'newValue', 114 | volatile: false, 115 | }], 116 | }) 117 | 118 | await ourbit.processTransaction(tx.id, async () => { 119 | context.operation(() => { 120 | targetState.key = 'value' 121 | }) 122 | context.operation(() => { 123 | targetState.key = 'newValue' 124 | }) 125 | }, { blockHash: tx.blockHash }) 126 | 127 | globalState.store.saveTransaction.should.have.been.called.with(TEST_KEY, tx) 128 | }) 129 | 130 | it('should accept volatile operations', async () => { 131 | targetState.domain = { array: [] } 132 | tx.patches.push({ 133 | id: 'uuid', 134 | reason: undefined, 135 | operations: [{ 136 | op: 'add', 137 | path: '/domain/uuid', 138 | value: { uuid: 'uuid', value: 'value' }, 139 | volatile: true, 140 | }], 141 | }) 142 | 143 | await ourbit.processTransaction(tx.id, async () => { 144 | context.operation(() => { 145 | targetState.key = 'value' 146 | }) 147 | context.emit(utils.appendTo('domain', { 148 | value: 'value', 149 | })) 150 | }, { blockHash: tx.blockHash }) 151 | 152 | globalState.store.saveTransaction.should.have.been.called.with(TEST_KEY, tx) 153 | }) 154 | 155 | it('should revert transactions', async () => { 156 | await produceFirstPatch() 157 | targetState.should.deep.equal({ key: 'value' }) 158 | 159 | await ourbit.processTransaction('0x2', async () => { 160 | targetState.key = 'newValue' 161 | }, { blockHash: '0x2' }) 162 | 163 | await ourbit.rollbackTransaction('0x2') 164 | targetState.should.deep.equal({ key: 'value' }) 165 | 166 | await ourbit.rollbackTransaction(tx.id) 167 | targetState.should.deep.equal({}) 168 | }) 169 | 170 | it('should be able to resume transactions after failure', async () => { 171 | await produceFirstPatch() 172 | 173 | const newState = {} 174 | // new state, same store 175 | const newOurbit = new Ourbit(TEST_KEY, newState, persistPatch, context) 176 | 177 | await newOurbit.resumeFromTxId('0x1') 178 | newState.should.deep.equal({ key: 'value' }) 179 | }) 180 | }) 181 | -------------------------------------------------------------------------------- /packages/gnarly-core/test/data/erc20Abi.ts: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | constant: false, 4 | inputs: [ 5 | { 6 | name: 'spender', 7 | type: 'address', 8 | }, 9 | { 10 | name: 'value', 11 | type: 'uint256', 12 | }, 13 | ], 14 | name: 'approve', 15 | outputs: [ 16 | { 17 | name: '', 18 | type: 'bool', 19 | }, 20 | ], 21 | payable: false, 22 | stateMutability: 'nonpayable', 23 | type: 'function', 24 | }, 25 | { 26 | constant: true, 27 | inputs: [], 28 | name: 'totalSupply', 29 | outputs: [ 30 | { 31 | name: '', 32 | type: 'uint256', 33 | }, 34 | ], 35 | payable: false, 36 | stateMutability: 'view', 37 | type: 'function', 38 | }, 39 | { 40 | constant: false, 41 | inputs: [ 42 | { 43 | name: 'from', 44 | type: 'address', 45 | }, 46 | { 47 | name: 'to', 48 | type: 'address', 49 | }, 50 | { 51 | name: 'value', 52 | type: 'uint256', 53 | }, 54 | ], 55 | name: 'transferFrom', 56 | outputs: [ 57 | { 58 | name: '', 59 | type: 'bool', 60 | }, 61 | ], 62 | payable: false, 63 | stateMutability: 'nonpayable', 64 | type: 'function', 65 | }, 66 | { 67 | constant: true, 68 | inputs: [ 69 | { 70 | name: 'who', 71 | type: 'address', 72 | }, 73 | ], 74 | name: 'balanceOf', 75 | outputs: [ 76 | { 77 | name: '', 78 | type: 'uint256', 79 | }, 80 | ], 81 | payable: false, 82 | stateMutability: 'view', 83 | type: 'function', 84 | }, 85 | { 86 | constant: false, 87 | inputs: [ 88 | { 89 | name: 'to', 90 | type: 'address', 91 | }, 92 | { 93 | name: 'value', 94 | type: 'uint256', 95 | }, 96 | ], 97 | name: 'transfer', 98 | outputs: [ 99 | { 100 | name: '', 101 | type: 'bool', 102 | }, 103 | ], 104 | payable: false, 105 | stateMutability: 'nonpayable', 106 | type: 'function', 107 | }, 108 | { 109 | constant: true, 110 | inputs: [ 111 | { 112 | name: 'owner', 113 | type: 'address', 114 | }, 115 | { 116 | name: 'spender', 117 | type: 'address', 118 | }, 119 | ], 120 | name: 'allowance', 121 | outputs: [ 122 | { 123 | name: '', 124 | type: 'uint256', 125 | }, 126 | ], 127 | payable: false, 128 | stateMutability: 'view', 129 | type: 'function', 130 | }, 131 | { 132 | anonymous: false, 133 | inputs: [ 134 | { 135 | indexed: true, 136 | name: 'owner', 137 | type: 'address', 138 | }, 139 | { 140 | indexed: true, 141 | name: 'spender', 142 | type: 'address', 143 | }, 144 | { 145 | indexed: false, 146 | name: 'value', 147 | type: 'uint256', 148 | }, 149 | ], 150 | name: 'Approval', 151 | type: 'event', 152 | }, 153 | { 154 | anonymous: false, 155 | inputs: [ 156 | { 157 | indexed: true, 158 | name: 'from', 159 | type: 'address', 160 | }, 161 | { 162 | indexed: true, 163 | name: 'to', 164 | type: 'address', 165 | }, 166 | { 167 | indexed: false, 168 | name: 'value', 169 | type: 'uint256', 170 | }, 171 | ], 172 | name: 'Transfer', 173 | type: 'event', 174 | }, 175 | ] 176 | -------------------------------------------------------------------------------- /packages/gnarly-core/test/exports.spec.ts: -------------------------------------------------------------------------------- 1 | import chai = require('chai') 2 | import 'mocha' 3 | 4 | import { addABI, getLogs } from '../src' 5 | import { globalState } from '../src/globalstate' 6 | 7 | import erc20Abi from './data/erc20Abi' 8 | import MockIngestApi from './mocks/MockIngestApi' 9 | 10 | const NUM_LOGS = 4 11 | const MOCK_ADDRESS = '0x0' 12 | 13 | chai 14 | .should() 15 | 16 | describe('gnarly-core exports', function () { 17 | beforeEach(async function () { 18 | globalState.setApi(new MockIngestApi(NUM_LOGS)) 19 | }) 20 | 21 | it('addABI works', async function () { 22 | addABI(MOCK_ADDRESS, erc20Abi) 23 | }) 24 | 25 | it('getLogs works', async function () { 26 | const logs = await getLogs({}) 27 | logs.length.should.equal(NUM_LOGS) 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /packages/gnarly-core/test/factories/IJSONBlockFactory.ts: -------------------------------------------------------------------------------- 1 | import { Factory } from 'rosie' 2 | 3 | export default new Factory() 4 | .attrs({ 5 | number: () => '0x0', 6 | hash: () => '0x0', 7 | parentHash: () => '0x0', 8 | nonce: () => '0x0', 9 | sha3Uncles: () => '0x0', 10 | logsBloom: () => '0x0', 11 | transactionsRoot: () => '0x0', 12 | stateRoot: () => '0x0', 13 | miner: () => '0x0', 14 | difficulty: () => '0x0', 15 | totalDifficulty: () => '0x0', 16 | extraData: () => '0x0', 17 | size: () => '0x0', 18 | gasLimit: () => '0x0', 19 | gasUsed: () => '0x0', 20 | timestamp: () => '0x0', 21 | transactions: () => [], 22 | uncles: () => [], 23 | }) 24 | -------------------------------------------------------------------------------- /packages/gnarly-core/test/factories/IJSONExternalTransactionFactory.ts: -------------------------------------------------------------------------------- 1 | import { Factory } from 'rosie' 2 | 3 | export default new Factory() 4 | .attrs({ 5 | hash: () => '0x0', 6 | nonce: () => '0x0', 7 | blockHash: () => '0x0', 8 | blockNumber: () => '0x0', 9 | transactionIndex: () => '0x0', 10 | from: () => '0x0', 11 | to: () => '0x0', 12 | value: () => '0x0', 13 | gasPrice: () => '0x0', 14 | gas: () => '0x0', 15 | input: () => '0x0', 16 | }) 17 | -------------------------------------------------------------------------------- /packages/gnarly-core/test/factories/IJSONExternalTransactionReceiptFactory.ts: -------------------------------------------------------------------------------- 1 | import { Factory } from 'rosie' 2 | 3 | export default new Factory() 4 | .attrs({ 5 | blockHash: () => '', 6 | blockNumber: () => '', 7 | contractAddress: () => '', 8 | cumulativeGasUsed: () => '', 9 | from: () => '', 10 | gasUsed: () => '', 11 | logs: () => [], 12 | logsBloom: () => '', 13 | status: () => '', 14 | to: () => '', 15 | transactionHash: () => '', 16 | transactionIndex: () => '', 17 | }) 18 | -------------------------------------------------------------------------------- /packages/gnarly-core/test/factories/IJSONInternalTransactionFactory.ts: -------------------------------------------------------------------------------- 1 | import { Factory } from 'rosie' 2 | 3 | export default new Factory() 4 | .attrs({ 5 | action: () => ({ 6 | callType: '0x0', 7 | from: '0x0', 8 | gas: '0x0', 9 | input: '0x0', 10 | to: '0x0', 11 | value: '0x0', 12 | }), 13 | result: () => ({ 14 | gasUsed: '0x0', 15 | output: '0x0', 16 | }), 17 | subtraces: () => 0, 18 | traceAddress: () => [], 19 | type: 'CALL', 20 | error: undefined, 21 | }) 22 | -------------------------------------------------------------------------------- /packages/gnarly-core/test/factories/IJSONLogFactory.ts: -------------------------------------------------------------------------------- 1 | import { Factory } from 'rosie' 2 | 3 | export default new Factory() 4 | .attrs({ 5 | address: () => '0x0', 6 | topics: () => [], 7 | data: () => '0x0', 8 | blockNumber: () => '0x0', 9 | blockHash: () => '0x0', 10 | transactionHash: () => '0x0', 11 | transactionIndex: () => '0x0', 12 | logIndex: () => '0x0', 13 | removed: () => false, 14 | }) 15 | -------------------------------------------------------------------------------- /packages/gnarly-core/test/globalstate.spec.ts: -------------------------------------------------------------------------------- 1 | import chai = require('chai') 2 | import 'mocha' 3 | 4 | import { GnarlyGlobals } from '../src/globalstate' 5 | import Log from '../src/models/Log' 6 | import { enhanceAbiItem } from '../src/utils' 7 | 8 | import MockIngestApi from './mocks/MockIngestApi' 9 | import MockPersistInterface from './mocks/MockPersistInterface' 10 | 11 | import erc20Abi from './data/erc20Abi' 12 | 13 | const NUM_LOGS = 4 14 | const MOCK_ADDRESS = '0x0' 15 | const TRANSFER_METHOD_ID = '0xa9059cbb' 16 | 17 | const should = chai 18 | .use(require('chai-spies')) 19 | .should() 20 | 21 | describe('globalstate', function () { 22 | beforeEach(async function () { 23 | this.globals = new GnarlyGlobals() 24 | }) 25 | 26 | context('api', function () { 27 | beforeEach(async function () { 28 | this.api = new MockIngestApi() 29 | }) 30 | 31 | it('can set/get api', async function () { 32 | this.globals.setApi(this.api) 33 | this.globals.api.should.equal(this.api) 34 | }) 35 | }) 36 | 37 | context('store', function () { 38 | beforeEach(async function () { 39 | this.store = new MockPersistInterface() 40 | }) 41 | 42 | it('can set/get store', async function () { 43 | this.globals.setStore(this.store) 44 | this.globals.store.should.equal(this.store) 45 | }) 46 | }) 47 | 48 | context('abis', function () { 49 | describe('addABI()', function () { 50 | it('can add an abi', async function () { 51 | this.globals.addABI(MOCK_ADDRESS, erc20Abi) 52 | 53 | this.globals.abis[MOCK_ADDRESS].length.should.equal(erc20Abi.length) 54 | }) 55 | }) 56 | 57 | context('with no abis', function () { 58 | it('getABI() returns undefined', async function () { 59 | should.not.exist(this.globals.getABI(MOCK_ADDRESS)) 60 | }) 61 | 62 | it('getMethod() returns undefined', async function () { 63 | should.not.exist(this.globals.getMethod(MOCK_ADDRESS, TRANSFER_METHOD_ID)) 64 | }) 65 | }) 66 | 67 | context('with valid abi', function () { 68 | beforeEach(async function () { 69 | this.globals.addABI(MOCK_ADDRESS, erc20Abi) 70 | }) 71 | 72 | it('can getABI()', async function () { 73 | const abiSet = this.globals.getABI(MOCK_ADDRESS) 74 | abiSet.length.should.equal(erc20Abi.length) 75 | }) 76 | 77 | it('should have enhanced the abi', async function () { 78 | const firstAbi = erc20Abi[0] 79 | const gotAbi = this.globals.getABI(MOCK_ADDRESS)[0] 80 | enhanceAbiItem(firstAbi).should.deep.equal(gotAbi) 81 | }) 82 | 83 | it('can getMethod()', async function () { 84 | const method = this.globals.getMethod(MOCK_ADDRESS, TRANSFER_METHOD_ID) 85 | method.shortId.should.equal(TRANSFER_METHOD_ID) 86 | }) 87 | }) 88 | }) 89 | 90 | context('getLogs()', function () { 91 | context('without logs', function () { 92 | beforeEach(async function () { 93 | this.api = new MockIngestApi(0) 94 | this.globals.setApi(this.api) 95 | }) 96 | 97 | it('returns empty array', async function () { 98 | const logs = await this.globals.getLogs({}) 99 | 100 | logs.length.should.equal(0) 101 | }) 102 | }) 103 | 104 | context('with logs', function () { 105 | beforeEach(async function () { 106 | this.api = new MockIngestApi(NUM_LOGS) 107 | this.globals.setApi(this.api) 108 | }) 109 | 110 | it('can getLogs()', async function () { 111 | const logs = await this.globals.getLogs({}) 112 | 113 | logs.length.should.equal(NUM_LOGS) 114 | logs.map((l) => l.should.be.instanceof(Log)) 115 | }) 116 | }) 117 | }) 118 | }) 119 | -------------------------------------------------------------------------------- /packages/gnarly-core/test/mocks/MockIngestApi.ts: -------------------------------------------------------------------------------- 1 | import BN = require('bn.js') 2 | import { 3 | FilterOptions, 4 | } from 'ethereumjs-blockstream' 5 | 6 | import IIngestApi, { DecomposeFn } from '../../src/ingestion/IngestApi' 7 | import { IJSONBlock } from '../../src/models/Block' 8 | import { IJSONExternalTransactionReceipt } from '../../src/models/ExternalTransaction' 9 | import { IJSONInternalTransaction } from '../../src/models/InternalTransaction' 10 | import { IJSONLog } from '../../src/models/Log' 11 | 12 | import IJSONBlockFactory from '../factories/IJSONBlockFactory' 13 | import IJSONExternalTransactionReceiptFactory from '../factories/IJSONExternalTransactionReceiptFactory' 14 | import IJSONInternalTransactionFactory from '../factories/IJSONInternalTransactionFactory' 15 | import IJSONLogFactory from '../factories/IJSONLogFactory' 16 | 17 | export default class MockIngestApi implements IIngestApi { 18 | 19 | private cb 20 | 21 | constructor ( 22 | private numLogs = 4, 23 | private numInternalTxs = 4, 24 | ) { 25 | 26 | } 27 | 28 | public getBlockByNumber = (num: BN): Promise => { 29 | return IJSONBlockFactory.build({ number: num.toString() }) 30 | } 31 | 32 | public getBlockByHash = (hash: string): Promise => { 33 | return IJSONBlockFactory.build({ hash }) 34 | } 35 | 36 | public getLogs = (filterOptions: FilterOptions): Promise => { 37 | return IJSONLogFactory.buildList(this.numLogs) 38 | } 39 | 40 | public getLatestBlock = (): Promise => { 41 | return IJSONBlockFactory.build() 42 | } 43 | 44 | public getTransactionReceipt = (hash: string): Promise => { 45 | return IJSONExternalTransactionReceiptFactory.build({ hash, logs: IJSONLogFactory.buildList(this.numLogs) }) 46 | } 47 | 48 | public traceTransaction = (hash: string): Promise => { 49 | return IJSONInternalTransactionFactory.buildList(this.numInternalTxs, { hash }) 50 | } 51 | 52 | public subscribeToNewBlocks = (cb): DecomposeFn => { 53 | this.cb = cb 54 | return this.decompose 55 | } 56 | 57 | public forceSendBlock = (block: IJSONBlock) => { 58 | this.cb() 59 | } 60 | 61 | public decompose = () => { 62 | // 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /packages/gnarly-core/test/mocks/MockPersistInterface.ts: -------------------------------------------------------------------------------- 1 | import _ = require('lodash') 2 | import { IJSONBlock } from '../../src/models/Block' 3 | import { 4 | ITransaction, 5 | } from '../../src/ourbit' 6 | import { IPersistInterface } from '../../src/stores' 7 | 8 | async function* iter ( 9 | res: any[] = [], 10 | ) { 11 | for (const thing of res) { 12 | yield thing 13 | } 14 | } 15 | 16 | export default class MockPersistInterface implements IPersistInterface { 17 | 18 | private reducers: any[] = [] 19 | private transactions: ITransaction[] = [] 20 | private historicalBlocks: { [_: string]: IJSONBlock[] } = {} 21 | 22 | public setup = async (reset: boolean = false) => { 23 | // nothing to be done 24 | this.transactions = [] 25 | } 26 | 27 | public setdown = async () => { 28 | // 29 | } 30 | 31 | public saveReducer = (reducerKey: string): Promise => { 32 | this.reducers.push(reducerKey) 33 | return 34 | } 35 | 36 | public deleteReducer = (reducerKey: string): Promise => { 37 | this.reducers = this.reducers.filter((r) => r !== reducerKey) 38 | return 39 | } 40 | 41 | public getHistoricalBlocks = async (reducerKey: string): Promise => { 42 | return (this.historicalBlocks[reducerKey] || []) 43 | } 44 | 45 | public saveHistoricalBlock = async ( 46 | reducerKey: string, 47 | blockRetention: number, 48 | block: IJSONBlock, 49 | ): Promise => { 50 | if (!this.historicalBlocks[reducerKey]) { 51 | this.historicalBlocks[reducerKey] = [] 52 | } 53 | 54 | this.historicalBlocks[reducerKey].push(block) 55 | } 56 | 57 | public deleteHistoricalBlock = async (reducerKey: string, blockHash: string): Promise => { 58 | this.historicalBlocks[reducerKey] = this.historicalBlocks[reducerKey].filter((b) => 59 | b.hash !== blockHash, 60 | ) 61 | } 62 | 63 | public deleteHistoricalBlocks = async (reducerKey: string): Promise => { 64 | this.historicalBlocks[reducerKey] = [] 65 | } 66 | 67 | public async getAllTransactionsTo (reducerKey: string, toTxId: null | string): Promise { 68 | return iter([this.transactions]) 69 | } 70 | 71 | public async getTransactions (reducerKey: string, fromTxId: null | string): Promise { 72 | return this.transactions 73 | } 74 | 75 | public async getLatestTransaction (reducerKey: string): Promise { 76 | return this.transactions[this.transactions.length - 1] 77 | } 78 | 79 | public async deleteTransaction (reducerKey: string, tx: ITransaction) { 80 | const i = _.findIndex(this.transactions, (t) => t.id === tx.id) 81 | this.transactions.splice(i, 1) 82 | return 83 | } 84 | 85 | public async saveTransaction (reducerKey: string, tx: ITransaction) { 86 | this.transactions.push(tx) 87 | return 88 | } 89 | 90 | public async getTransaction (reducerKey: string, txId: string): Promise { 91 | return _.find(this.transactions, (t) => t.id === txId) 92 | } 93 | 94 | public async getTransactionByBlockHash (reducerKey: string, blockHash: string): Promise { 95 | return _.find(this.transactions, (t) => t.blockHash === blockHash) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /packages/gnarly-core/test/models/Models.spec.ts: -------------------------------------------------------------------------------- 1 | import BN = require('bn.js') 2 | import chai = require('chai') 3 | import 'mocha' 4 | 5 | import { globalState } from '../../src/globalstate' 6 | import Block from '../../src/models/Block' 7 | import ExternalTransaction, { 8 | IJSONExternalTransaction, 9 | isExternalTransaction, 10 | } from '../../src/models/ExternalTransaction' 11 | import InternalTransaction from '../../src/models/InternalTransaction' 12 | import Log from '../../src/models/Log' 13 | import { toBN } from '../../src/utils' 14 | 15 | import erc20Abi from '../data/erc20Abi' 16 | import IJSONBlockFactory from '../factories/IJSONBlockFactory' 17 | import IJSONExternalTransactionFactory from '../factories/IJSONExternalTransactionFactory' 18 | import IJSONInternalTransactionFactory from '../factories/IJSONInternalTransactionFactory' 19 | import IJSONLogFactory from '../factories/IJSONLogFactory' 20 | import MockIngestApi from '../mocks/MockIngestApi' 21 | import { expectThrow } from '../utils' 22 | 23 | const should = chai 24 | .use(require('chai-spies')) 25 | .use(require('bn-chai')(BN)) 26 | .should() 27 | 28 | const TRANSFER_EVENT_SIGNATURE = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef' 29 | const TRANSFER_FROM_TOPIC = '0x0000000000000000000000000000000000000000000000000000000000000001' 30 | const TRANSFER_TO_TOPIC = '0x0000000000000000000000000000000000000000000000000000000000000002' 31 | const TRANSFER_FROM = '0x0000000000000000000000000000000000000001' 32 | const TRANSFER_TO = '0x0000000000000000000000000000000000000002' 33 | const TRANSFER_DATA = '0x000000000000000000000000000000000000000000000006659436cf28180000' 34 | const TRANSFER_VALUE = '118000000000000000000' 35 | 36 | const MOCK_ADDRESS = '0x123' 37 | const NUM_INTERNAL_TX = 4 38 | const NUM_LOGS = 4 39 | const NUM_EXT_TX = 2 40 | 41 | describe('Models', function () { 42 | beforeEach(async function () { 43 | globalState.setApi(new MockIngestApi(NUM_LOGS, NUM_INTERNAL_TX)) 44 | }) 45 | 46 | describe('Block', function () { 47 | const bnFields = [ 48 | 'number', 49 | 'nonce', 50 | 'difficulty', 51 | 'totalDifficulty', 52 | 'size', 53 | 'gasLimit', 54 | 'gasUsed', 55 | 'timestamp', 56 | ] 57 | 58 | it('can be constructed', async function () { 59 | const blockData = IJSONBlockFactory.build() 60 | const block = new Block(blockData) 61 | should.exist(block) 62 | // test instantiation 63 | bnFields.forEach((f) => { 64 | block[f].should.be.eq.BN(toBN(blockData[f])) 65 | }) 66 | }) 67 | 68 | it('maps externalTransactions to ExternalTransaction', async function () { 69 | const blockData = IJSONBlockFactory.build({ 70 | transactions: IJSONExternalTransactionFactory.buildList(NUM_EXT_TX), 71 | }) 72 | const block = new Block(blockData) 73 | block.transactions.length.should.equal(NUM_EXT_TX) 74 | block.transactions[0].should.be.instanceof(ExternalTransaction) 75 | }) 76 | 77 | it('can load tx receipts', async function () { 78 | const blockData = IJSONBlockFactory.build({ 79 | transactions: IJSONExternalTransactionFactory.buildList(NUM_EXT_TX), 80 | }) 81 | const block = new Block(blockData) 82 | await block.loadTransactions() 83 | block.transactions.length.should.equal(NUM_EXT_TX) 84 | block.transactions[0].logs.length.should.equal(NUM_LOGS) 85 | }) 86 | 87 | it('can load all transactions with tracing', async function () { 88 | const blockData = IJSONBlockFactory.build({ 89 | transactions: IJSONExternalTransactionFactory.buildList(NUM_EXT_TX), 90 | }) 91 | const block = new Block(blockData) 92 | await block.loadAllTransactions() 93 | // should still have 4 external txs 94 | block.transactions.length.should.equal(NUM_EXT_TX) 95 | // each external tx should have 4 internal txs 96 | block.allTransactions.length.should.equal(NUM_EXT_TX + (NUM_EXT_TX * NUM_INTERNAL_TX)) 97 | }) 98 | }) 99 | 100 | describe('ExternalTransaction', function () { 101 | beforeEach(async function () { 102 | this.blockData = IJSONBlockFactory.build({ 103 | transactions: IJSONExternalTransactionFactory.buildList(NUM_EXT_TX), 104 | }) 105 | this.block = new Block(this.blockData) 106 | }) 107 | 108 | const bnFields = [ 109 | 'nonce', 110 | 'blockNumber', 111 | 'value', 112 | 'gasPrice', 113 | 'gas', 114 | ] 115 | const receiptBNFields = ['cumulativeGasUsed', 'gasUsed', 'status'] 116 | 117 | it('can be constructed', async function () { 118 | const etxData = IJSONExternalTransactionFactory.build() 119 | const etx = new ExternalTransaction(this.block, etxData) 120 | 121 | should.exist(etx) 122 | etx.block.should.equal(this.block) 123 | bnFields.forEach((f) => etx[f].should.be.eq.BN(toBN(etxData[f]))) 124 | etx.index.should.eq.BN(toBN(etxData.transactionIndex)) 125 | }) 126 | 127 | it('can fetch rceipt', async function () { 128 | const etxData = IJSONExternalTransactionFactory.build() 129 | const etx = new ExternalTransaction(this.block, etxData) 130 | await etx.getReceipt() 131 | receiptBNFields.forEach((f) => etx[f].should.eq.BN(toBN('0x0'))) 132 | etx.logs.length.should.equal(NUM_LOGS) 133 | }) 134 | 135 | it('can fetch internal transactions', async function () { 136 | const etxData = IJSONExternalTransactionFactory.build() 137 | const etx = new ExternalTransaction(this.block, etxData) 138 | await etx.getInternalTransactions() 139 | 140 | etx.internalTransactions.length.should.equal(NUM_INTERNAL_TX) 141 | }) 142 | 143 | it('throws for improperly formatted info', async function () { 144 | const badData: IJSONExternalTransaction = {} as IJSONExternalTransaction 145 | const fn = () => new ExternalTransaction(this.block, badData) 146 | fn.should.throw() 147 | }) 148 | 149 | context('without tracing', function () { 150 | beforeEach(async function () { 151 | chai.spy.on(globalState.api, 'traceTransaction', () => { 152 | throw new Error() 153 | }) 154 | }) 155 | 156 | afterEach(async function () { 157 | chai.spy.restore() 158 | }) 159 | 160 | it('swallows error', async function () { 161 | const etxData = IJSONExternalTransactionFactory.build() 162 | const etx = new ExternalTransaction(this.block, etxData) 163 | 164 | await expectThrow(etx.getInternalTransactions()) 165 | }) 166 | }) 167 | 168 | context('isExternalTransaction()', function () { 169 | it('can identify an internal and external transaction', async function () { 170 | const etxData = IJSONExternalTransactionFactory.build() 171 | const etx = new ExternalTransaction(this.block, etxData) 172 | 173 | const itxData = IJSONInternalTransactionFactory.build() 174 | const itx = new InternalTransaction(etx, itxData) 175 | 176 | isExternalTransaction(etx).should.equal(true) 177 | isExternalTransaction(itx).should.equal(false) 178 | }) 179 | }) 180 | }) 181 | 182 | describe('InternalTransaction', function () { 183 | const TEST_ERROR = '0x1' 184 | const TEST_OUTPUT = '0x1' 185 | const TEST_GAS_USED = '0x1' 186 | 187 | beforeEach(async function () { 188 | this.etxData = IJSONExternalTransactionFactory.build() 189 | this.etx = new ExternalTransaction(this.block, this.etxData) 190 | }) 191 | // mostly tested as a side effect of the other tests 192 | it('should set result if error not available', async function () { 193 | const itxData = IJSONInternalTransactionFactory.build({ error: undefined, result: { 194 | output: TEST_OUTPUT, 195 | gasUsed: '0x1', 196 | } }) 197 | const itx = new InternalTransaction(this.etx, itxData) 198 | itx.result.output.should.equal(TEST_OUTPUT) 199 | itx.result.gasUsed.should.eq.BN(toBN(TEST_GAS_USED)) 200 | }) 201 | 202 | it('should set error if available', async function () { 203 | const itxData = IJSONInternalTransactionFactory.build({ error: TEST_ERROR }) 204 | const itx = new InternalTransaction(this.etx, itxData) 205 | should.not.exist(itx.result) 206 | itx.error.should.equal(TEST_ERROR) 207 | }) 208 | }) 209 | 210 | describe('Log', function () { 211 | const bnFields = [ 212 | 'logIndex', 213 | 'blockNumber', 214 | 'transactionIndex', 215 | ] 216 | beforeEach(async function () { 217 | this.blockData = IJSONBlockFactory.build({ 218 | transactions: IJSONExternalTransactionFactory.buildList(NUM_EXT_TX), 219 | }) 220 | this.block = new Block(this.blockData) 221 | 222 | this.etxData = IJSONExternalTransactionFactory.build() 223 | this.etx = new ExternalTransaction(this.block, this.etxData) 224 | }) 225 | 226 | it('can be constructed', async function () { 227 | const logData = IJSONLogFactory.build() 228 | const log = new Log(this.etx, logData) 229 | should.exist(log) 230 | log.should.be.instanceof(Log) 231 | bnFields.forEach((f) => log[f].should.be.eq.BN(toBN(logData[f]))) 232 | }) 233 | 234 | context('parse()', function () { 235 | it('handles no available abi', async function () { 236 | const logData = IJSONLogFactory.build() 237 | const log = new Log(this.etx, logData) 238 | log.parse().should.equal(false) 239 | }) 240 | 241 | it('handles invalid topic length', async function () { 242 | const logData = IJSONLogFactory.build({ address: MOCK_ADDRESS, topics: [] }) 243 | const log = new Log(this.etx, logData) 244 | log.parse().should.equal(false) 245 | }) 246 | 247 | it('handles not-found abi item', async function () { 248 | globalState.addABI(MOCK_ADDRESS, []) 249 | const logData = IJSONLogFactory.build({ address: MOCK_ADDRESS, topics: ['', ''] }) 250 | const log = new Log(this.etx, logData) 251 | log.parse().should.equal(false) 252 | }) 253 | 254 | it('handles invalid log', async function () { 255 | globalState.addABI(MOCK_ADDRESS, erc20Abi) 256 | const logData = IJSONLogFactory.build({ 257 | address: MOCK_ADDRESS, 258 | topics: [ 259 | TRANSFER_EVENT_SIGNATURE, 260 | '0xno', 261 | '0xno', 262 | ], 263 | data: '0x0', 264 | }) 265 | const log = new Log(this.etx, logData) 266 | log.parse().should.equal(false) 267 | }) 268 | 269 | it('should be able to parse valid log for abi item', async function () { 270 | globalState.addABI(MOCK_ADDRESS, erc20Abi) 271 | const logData = IJSONLogFactory.build({ 272 | address: MOCK_ADDRESS, 273 | topics: [ 274 | TRANSFER_EVENT_SIGNATURE, 275 | TRANSFER_FROM_TOPIC, 276 | TRANSFER_TO_TOPIC, 277 | ], 278 | data: TRANSFER_DATA, 279 | }) 280 | const log = new Log(this.etx, logData) 281 | log.parse().should.equal(true) 282 | 283 | log.event.should.equal('Transfer(address,address,uint256)') 284 | log.eventName.should.equal('Transfer') 285 | log.signature.should.equal(TRANSFER_EVENT_SIGNATURE) 286 | const args = log.args as any 287 | args.from.should.equal(TRANSFER_FROM) 288 | args.to.should.equal(TRANSFER_TO) 289 | args.value.should.equal(TRANSFER_VALUE) 290 | }) 291 | }) 292 | }) 293 | }) 294 | -------------------------------------------------------------------------------- /packages/gnarly-core/test/utils.spec.ts: -------------------------------------------------------------------------------- 1 | import chai = require('chai') 2 | import 'mocha' 3 | import uuid = require('uuid') 4 | 5 | import { IABIItemInput } from '../src' 6 | 7 | import { IOperation } from '../src/ourbit' 8 | import * as utils from '../src/utils' 9 | import { expectThrow } from './utils' 10 | 11 | const should = chai 12 | .use(require('chai-spies')) 13 | .should() 14 | 15 | const TRANSFER_ABI: IABIItemInput = { 16 | anonymous: false, 17 | inputs: [ 18 | { indexed: true, name: 'from', type: 'address' }, 19 | { indexed: true, name: 'to', type: 'address' }, 20 | { indexed: false, name: 'tokens', type: 'uint256' }, 21 | ], 22 | name: 'Transfer', 23 | type: 'event', 24 | } 25 | 26 | describe('utils', function () { 27 | before(function () { 28 | chai.spy.on(uuid, 'v4', () => 'uuid') 29 | }) 30 | 31 | after(function () { 32 | chai.spy.restore() 33 | }) 34 | 35 | context('parsePath', function () { 36 | it('should parse path correctly', async function () { 37 | const parts = utils.parsePath('/tableName/pk/indexOrKey') 38 | Object.keys(parts).should.deep.equal(Object.values(parts)) 39 | }) 40 | 41 | it('should parse path without index correctly', async function () { 42 | const parts = utils.parsePath('/tableName/pk') 43 | parts.tableName.should.equal('tableName') 44 | parts.pk.should.equal('pk') 45 | should.not.exist(parts.indexOrKey) 46 | }) 47 | }) 48 | 49 | context('addressesEqual', function () { 50 | it('should work on mixed case addresses', async function () { 51 | utils.addressesEqual('0x1', '0x1').should.equal(true) 52 | utils.addressesEqual('0xA', '0xA').should.equal(true) 53 | utils.addressesEqual('0xA', '0xa').should.equal(true) 54 | utils.addressesEqual('0xa', '0xA').should.equal(true) 55 | }) 56 | 57 | it('should detect non equal addresses', async function () { 58 | utils.addressesEqual('0x1', '0x2').should.equal(false) 59 | }) 60 | }) 61 | 62 | context('forEach', function () { 63 | const opts = [1, 1] 64 | 65 | it('should iterate promises with concurrency', async function () { 66 | const longest = opts[opts.length - 1] * 40 67 | const mapper = async (i) => await utils.timeout(i * 40) 68 | const now = +(new Date()) 69 | await utils.forEach(opts, mapper) 70 | const diff = (+(new Date()) - now) 71 | diff.should.be.at.least(longest).and.at.most(longest * 1.2) 72 | }) 73 | 74 | it('should iterate promises with concurrency 1', async function () { 75 | const total = opts.reduce((memo, i) => memo + (i * 60), 0) 76 | const mapper = async (i) => await utils.timeout(i * 60) 77 | const now = +(new Date()) 78 | await utils.forEach(opts, mapper, { concurrency: 1 }) 79 | const diff = (+(new Date()) - now) 80 | diff.should.be.at.least(total).and.at.most(total * 1.2) 81 | }) 82 | 83 | it('should throw if arguments are invalid', async function () { 84 | await expectThrow(utils.forEach(undefined, undefined, undefined)) 85 | }) 86 | }) 87 | 88 | context('timeout', function () { 89 | const TIMEOUT = 100 90 | 91 | it('should have default, of 0', async function () { 92 | await utils.timeout() 93 | }) 94 | 95 | it('should work', async function () { 96 | const now = +(new Date()) 97 | await utils.timeout(TIMEOUT) 98 | const diff = (+(new Date()) - now) 99 | diff.should.be.at.least(TIMEOUT).and.at.most(TIMEOUT * 1.2) 100 | }) 101 | }) 102 | 103 | context('enhanceAbiItem', function () { 104 | it('should produce name, sig, shortId', async function () { 105 | const enhanced = utils.enhanceAbiItem(TRANSFER_ABI) 106 | enhanced.fullName.should.equal('Transfer(address,address,uint256)') 107 | enhanced.signature.should.equal('0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef') 108 | enhanced.shortId.should.equal('0xddf252ad') 109 | // ^^ https://www.4byte.directory/signatures/?bytes4_signature=0xddf252ad 110 | }) 111 | }) 112 | 113 | context('getMethodId', function () { 114 | it('works', async function () { 115 | utils.getMethodId( 116 | '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef', 117 | ).should.equal('0xddf252ad') 118 | }) 119 | }) 120 | 121 | context('invertOperation', function () { 122 | it('inverts an add operation', async function () { 123 | const add: IOperation = { 124 | op: 'add', 125 | path: '/myThing', 126 | value: { test: true }, 127 | volatile: false, 128 | } 129 | const inverted = utils.invertOperation(add) 130 | // the invert of an add is a removal at the same path 131 | inverted.path.should.equal(add.path) 132 | inverted.op.should.equal('remove') 133 | should.not.exist(inverted.value) 134 | inverted.oldValue.should.equal(add.value) 135 | }) 136 | 137 | it('inverts a remove operation', async function () { 138 | const remove: IOperation = { 139 | op: 'remove', 140 | path: '/myThing', 141 | oldValue: { test: true }, 142 | volatile: false, 143 | } 144 | const inverted = utils.invertOperation(remove) 145 | // the invert of a remove is an add of the old value at that path 146 | inverted.path.should.equal(remove.path) 147 | inverted.op.should.equal('add') 148 | inverted.value.should.equal(remove.oldValue) 149 | should.not.exist(inverted.oldValue) 150 | }) 151 | 152 | it('inverts a replace operation', async function () { 153 | const replace: IOperation = { 154 | op: 'replace', 155 | path: '/myThing', 156 | value: { new: true }, 157 | oldValue: { new: false }, 158 | volatile: false, 159 | } 160 | const inverted = utils.invertOperation(replace) 161 | // the invert of a replace just swaps value/oldValue 162 | inverted.path.should.equal(replace.path) 163 | inverted.op.should.equal('replace') 164 | inverted.value.should.equal(replace.oldValue) 165 | inverted.oldValue.should.equal(replace.value) 166 | }) 167 | 168 | it('should throw on invalid operation', async function () { 169 | const invalid: IOperation = { 170 | op: 'move', 171 | path: '/myThing', 172 | volatile: false, 173 | } 174 | const test = function () { 175 | utils.invertOperation(invalid) 176 | } 177 | test.should.throw() 178 | }) 179 | 180 | it('silently fails on non-invertable operation', async function () { 181 | // @TODO(shrugs) - should this succeed or fail?? 182 | const nonInvertable: IOperation = { 183 | op: 'add', 184 | path: '/myThing', 185 | volatile: false, 186 | } 187 | const test = function () { 188 | utils.invertOperation(nonInvertable) 189 | } 190 | test.should.not.throw() 191 | }) 192 | }) 193 | 194 | context('appendTo', function () { 195 | it('generates a valid op', async function () { 196 | const op = utils.appendTo('domain', { test: true }) 197 | op.op.should.equal('add') 198 | op.path.should.equal('/domain/uuid') 199 | op.value.should.deep.equal({ 200 | uuid: 'uuid', 201 | test: true, 202 | }) 203 | op.volatile.should.equal(true) 204 | }) 205 | }) 206 | 207 | context('cacheApiRequest', function () { 208 | 209 | beforeEach(async function () { 210 | this.spy = chai.spy() 211 | const fn = async (arg: number) => { 212 | this.spy(arg) 213 | return arg 214 | } 215 | 216 | this.memoized = utils.cacheApiRequest(fn) 217 | }) 218 | 219 | afterEach(async function () { 220 | this.memoized.clear() 221 | }) 222 | 223 | it ('should memoize the function that returns a promise', async function () { 224 | await this.memoized(1) 225 | await this.memoized(1) 226 | 227 | this.spy.should.have.been.called.exactly(1) 228 | this.spy.should.have.been.called.with(1) 229 | }) 230 | 231 | // it('should expire after maxAge', async function () { 232 | // // this test is annnoying to run, but it should work 233 | // this.timeout(2500) 234 | // await this.memoized(1) 235 | // await utils.timeout(2000) 236 | // await this.memoized(1) 237 | 238 | // this.spy.should.have.been.called.exactly(2) 239 | // }) 240 | }) 241 | }) 242 | -------------------------------------------------------------------------------- /packages/gnarly-core/test/utils/index.ts: -------------------------------------------------------------------------------- 1 | import chai = require('chai') 2 | 3 | export const expectThrow = async (p) => { 4 | try { 5 | await p 6 | throw new chai.AssertionError('Expected promise to throw, but it did not.') 7 | } catch (error) { 8 | chai.expect(true).to.equal(true) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/gnarly-core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.build.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "lib" 6 | }, 7 | "include": ["src"], 8 | "typeRoots": ["../../node_modules/@types"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/gnarly-core/tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.test.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "lib" 6 | }, 7 | "include": ["src", "test"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/gnarly-reducer-block-meta/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@xlnt/gnarly-reducer-block-meta", 3 | "version": "0.6.0", 4 | "description": "A gnarly reducer for block metadata.", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "prepare": "npm run build", 8 | "build": "npm run build-ts && npm run tslint", 9 | "ts-start": "ts-node src/index.ts", 10 | "start": "node lib/index.js", 11 | "watch": "concurrently -k -p \"[{name}]\" -n \"TypeScript,Node\" -c \"cyan.bold,green.bold\" \"npm run watch-ts\" \"npm run watch-node\"", 12 | "test": "nyc --reporter=text mocha -r ts-node/register -r source-map-support/register --full-trace 'test/**/*.spec.ts'", 13 | "watch-test": " mocha --watch --watch-extensions ts -r ts-node/register 'test/**/*.spec.ts'", 14 | "coverage": "nyc report --reporter=text-lcov > ./lcov.info", 15 | "build-ts": "tsc", 16 | "watch-ts": "tsc -w", 17 | "lint": "tslint --project ." 18 | }, 19 | "files": [ 20 | "lib" 21 | ], 22 | "keywords": [ 23 | "gnarly" 24 | ], 25 | "repository": { 26 | "type": "git", 27 | "url": "git@github.com:XLNT/gnarly.git" 28 | }, 29 | "author": "Matt Condon ", 30 | "license": "Apache-2.0", 31 | "devDependencies": { 32 | "@types/chai": "^4.1.4", 33 | "@types/node": "^10.5.7", 34 | "@xlnt/gnarly-core": "^0.6.0", 35 | "chai": "^4.1.2", 36 | "mocha": "^5.2.0", 37 | "nyc": "^12.0.2", 38 | "rosie": "^2.0.1", 39 | "source-map-support": "^0.5.6", 40 | "tslint": "^5.11.0", 41 | "typescript": "^3.0.1" 42 | }, 43 | "publishConfig": { 44 | "access": "public" 45 | }, 46 | "dependencies": { 47 | "debug": "^3.1.0" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/gnarly-reducer-block-meta/src/index.ts: -------------------------------------------------------------------------------- 1 | 2 | export { 3 | default, 4 | } from './reducer' 5 | 6 | export * from './stores' 7 | export * from './models' 8 | -------------------------------------------------------------------------------- /packages/gnarly-reducer-block-meta/src/models/index.ts: -------------------------------------------------------------------------------- 1 | export { default as sequelizeModels } from './sequelize' 2 | -------------------------------------------------------------------------------- /packages/gnarly-reducer-block-meta/src/models/sequelize.ts: -------------------------------------------------------------------------------- 1 | import { 2 | makeSequelizeModels as makeGnarlyModels, 3 | } from '@xlnt/gnarly-core' 4 | 5 | const sequelizeModels = ( 6 | Sequelize: any, 7 | sequelize: any, 8 | ) => { 9 | const { DataTypes } = Sequelize 10 | const { Patch } = makeGnarlyModels(Sequelize, sequelize) 11 | // @TODO - allow users to decide if they want to coerce these large numbers 12 | // into actual number types 13 | const Block = sequelize.define('block', { 14 | // gnarly-required columns 15 | uuid: { type: DataTypes.STRING, primaryKey: true }, 16 | order: { type: DataTypes.INTEGER }, 17 | 18 | hash: { type: DataTypes.STRING }, 19 | // ^hash === gnarly#Transaction.blockHash 20 | number: { type: DataTypes.STRING }, 21 | unsafeNumber: { type: DataTypes.INTEGER }, 22 | // ^ unsafeNumber is unsafe because it's capped by postgres's integer range 23 | parentHash: { type: DataTypes.STRING }, 24 | nonce: { type: DataTypes.STRING }, 25 | sha3Uncles: { type: DataTypes.STRING }, 26 | logsBloom: { type: DataTypes.TEXT }, 27 | transactionsRoot: { type: DataTypes.STRING }, 28 | stateRoot: { type: DataTypes.STRING }, 29 | miner: { type: DataTypes.STRING }, 30 | difficulty: { type: DataTypes.STRING }, 31 | totalDifficulty: { type: DataTypes.STRING }, 32 | extraData: { type: DataTypes.STRING }, 33 | size: { type: DataTypes.STRING }, 34 | gasLimit: { type: DataTypes.STRING }, 35 | gasUsed: { type: DataTypes.STRING }, 36 | timestamp: { type: DataTypes.DATE }, 37 | }, { 38 | indexes: [ 39 | { fields: ['hash'], unique: true }, 40 | ], 41 | }) 42 | 43 | Block.Patch = Block.belongsTo(Patch) 44 | 45 | return { 46 | Block, 47 | } 48 | } 49 | 50 | export default sequelizeModels 51 | -------------------------------------------------------------------------------- /packages/gnarly-reducer-block-meta/src/reducer.ts: -------------------------------------------------------------------------------- 1 | import { 2 | appendTo, 3 | Block, 4 | EmitOperationFn, 5 | IReducer, 6 | ITypeStore, 7 | ReducerType, 8 | } from '@xlnt/gnarly-core' 9 | 10 | const makeReducer = ( 11 | key: string = 'blocks', 12 | typeStore: ITypeStore, 13 | ) => ( 14 | ): IReducer => { 15 | const makeActions = (state: object, emit: EmitOperationFn) => ({ 16 | emitBlock: (block: Block) => { 17 | emit(appendTo('blocks', { 18 | hash: block.hash, 19 | transactionId: block.hash, 20 | // ^ assumes that gnarly.transactionId === block.hash 21 | number: block.number.toString(), 22 | unsafeNumber: block.number.toString(), 23 | parentHash: block.parentHash, 24 | nonce: block.nonce.toString(), 25 | sha3Uncles: block.sha3Uncles, 26 | logsBloom: block.logsBloom, 27 | transactionsRoot: block.transactionsRoot, 28 | stateRoot: block.stateRoot, 29 | miner: block.miner, 30 | difficulty: block.difficulty.toString(), 31 | totalDifficulty: block.totalDifficulty.toString(), 32 | size: block.size.toString(), 33 | gasLimit: block.gasLimit.toString(), 34 | gasUsed: block.gasUsed.toString(), 35 | timestamp: new Date(block.timestamp.toNumber() * 1000), 36 | })) 37 | }, 38 | }) 39 | 40 | return { 41 | config: { 42 | type: ReducerType.Atomic, 43 | key, 44 | typeStore, 45 | }, 46 | state: {}, 47 | reduce: async ( 48 | state: object, 49 | block: Block, 50 | { because, emit }, 51 | ): Promise => { 52 | const actions = makeActions(state, emit) 53 | because('BLOCK_PRODUCED', {}, () => { 54 | actions.emitBlock(block) 55 | }) 56 | }, 57 | } 58 | } 59 | 60 | export default makeReducer 61 | -------------------------------------------------------------------------------- /packages/gnarly-reducer-block-meta/src/stores/index.ts: -------------------------------------------------------------------------------- 1 | export { default as makeSequelizeTypeStore } from './sequelize' 2 | -------------------------------------------------------------------------------- /packages/gnarly-reducer-block-meta/src/stores/sequelize.ts: -------------------------------------------------------------------------------- 1 | import sequelizeModels from '../models/sequelize' 2 | 3 | import { 4 | SequelizeTypeStorer, 5 | } from '@xlnt/gnarly-core' 6 | 7 | const makeSequelizeTypeStore = ( 8 | Sequelize: any, 9 | sequelize: any, 10 | ) => { 11 | const { 12 | Block, 13 | } = sequelizeModels(Sequelize, sequelize) 14 | 15 | // the type store 16 | return { 17 | __setup: async () => { 18 | await Block.sync() 19 | }, 20 | __setdown: async () => { 21 | await Block.drop({ cascade: true }) 22 | }, 23 | store: SequelizeTypeStorer(Sequelize, { 24 | blocks: Block, 25 | }), 26 | } 27 | } 28 | 29 | export default makeSequelizeTypeStore 30 | -------------------------------------------------------------------------------- /packages/gnarly-reducer-block-meta/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.build.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "lib" 6 | }, 7 | "include": ["src"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/gnarly-reducer-erc20/LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /packages/gnarly-reducer-erc20/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@xlnt/gnarly-reducer-erc20", 3 | "version": "0.6.0", 4 | "description": "A gnarly reducer for ERC20 Token information.", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "prepare": "npm run build", 8 | "build": "npm run build-ts && npm run tslint", 9 | "ts-start": "ts-node src/index.ts", 10 | "start": "node lib/index.js", 11 | "watch": "concurrently -k -p \"[{name}]\" -n \"TypeScript,Node\" -c \"cyan.bold,green.bold\" \"npm run watch-ts\" \"npm run watch-node\"", 12 | "test": "nyc --reporter=text mocha -r ts-node/register -r source-map-support/register --full-trace 'test/**/*.spec.ts'", 13 | "watch-test": " mocha --watch --watch-extensions ts -r ts-node/register 'test/**/*.spec.ts'", 14 | "coverage": "nyc report --reporter=text-lcov > ./lcov.info", 15 | "build-ts": "tsc", 16 | "watch-ts": "tsc -w", 17 | "lint": "tslint --project ." 18 | }, 19 | "files": [ 20 | "lib" 21 | ], 22 | "keywords": [ 23 | "gnarly" 24 | ], 25 | "repository": { 26 | "type": "git", 27 | "url": "git@github.com:XLNT/gnarly.git" 28 | }, 29 | "author": "Steven Nonis ", 30 | "license": "Apache-2.0", 31 | "devDependencies": { 32 | "@types/chai": "^4.1.4", 33 | "@types/node": "^10.5.7", 34 | "@xlnt/gnarly-core": "^0.6.0", 35 | "chai": "^4.1.2", 36 | "mocha": "^5.2.0", 37 | "nyc": "^12.0.2", 38 | "rosie": "^2.0.1", 39 | "source-map-support": "^0.5.6", 40 | "tslint": "^5.11.0", 41 | "typescript": "^3.0.1" 42 | }, 43 | "publishConfig": { 44 | "access": "public" 45 | }, 46 | "dependencies": { 47 | "debug": "^3.1.0" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/gnarly-reducer-erc20/src/abi.ts: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | constant: false, 4 | inputs: [ 5 | { 6 | name: 'spender', 7 | type: 'address', 8 | }, 9 | { 10 | name: 'value', 11 | type: 'uint256', 12 | }, 13 | ], 14 | name: 'approve', 15 | outputs: [ 16 | { 17 | name: '', 18 | type: 'bool', 19 | }, 20 | ], 21 | payable: false, 22 | stateMutability: 'nonpayable', 23 | type: 'function', 24 | }, 25 | { 26 | constant: true, 27 | inputs: [], 28 | name: 'totalSupply', 29 | outputs: [ 30 | { 31 | name: '', 32 | type: 'uint256', 33 | }, 34 | ], 35 | payable: false, 36 | stateMutability: 'view', 37 | type: 'function', 38 | }, 39 | { 40 | constant: false, 41 | inputs: [ 42 | { 43 | name: 'from', 44 | type: 'address', 45 | }, 46 | { 47 | name: 'to', 48 | type: 'address', 49 | }, 50 | { 51 | name: 'value', 52 | type: 'uint256', 53 | }, 54 | ], 55 | name: 'transferFrom', 56 | outputs: [ 57 | { 58 | name: '', 59 | type: 'bool', 60 | }, 61 | ], 62 | payable: false, 63 | stateMutability: 'nonpayable', 64 | type: 'function', 65 | }, 66 | { 67 | constant: true, 68 | inputs: [ 69 | { 70 | name: 'who', 71 | type: 'address', 72 | }, 73 | ], 74 | name: 'balanceOf', 75 | outputs: [ 76 | { 77 | name: '', 78 | type: 'uint256', 79 | }, 80 | ], 81 | payable: false, 82 | stateMutability: 'view', 83 | type: 'function', 84 | }, 85 | { 86 | constant: false, 87 | inputs: [ 88 | { 89 | name: 'to', 90 | type: 'address', 91 | }, 92 | { 93 | name: 'value', 94 | type: 'uint256', 95 | }, 96 | ], 97 | name: 'transfer', 98 | outputs: [ 99 | { 100 | name: '', 101 | type: 'bool', 102 | }, 103 | ], 104 | payable: false, 105 | stateMutability: 'nonpayable', 106 | type: 'function', 107 | }, 108 | { 109 | constant: true, 110 | inputs: [ 111 | { 112 | name: 'owner', 113 | type: 'address', 114 | }, 115 | { 116 | name: 'spender', 117 | type: 'address', 118 | }, 119 | ], 120 | name: 'allowance', 121 | outputs: [ 122 | { 123 | name: '', 124 | type: 'uint256', 125 | }, 126 | ], 127 | payable: false, 128 | stateMutability: 'view', 129 | type: 'function', 130 | }, 131 | { 132 | anonymous: false, 133 | inputs: [ 134 | { 135 | indexed: true, 136 | name: 'owner', 137 | type: 'address', 138 | }, 139 | { 140 | indexed: true, 141 | name: 'spender', 142 | type: 'address', 143 | }, 144 | { 145 | indexed: false, 146 | name: 'value', 147 | type: 'uint256', 148 | }, 149 | ], 150 | name: 'Approval', 151 | type: 'event', 152 | }, 153 | { 154 | anonymous: false, 155 | inputs: [ 156 | { 157 | indexed: true, 158 | name: 'from', 159 | type: 'address', 160 | }, 161 | { 162 | indexed: true, 163 | name: 'to', 164 | type: 'address', 165 | }, 166 | { 167 | indexed: false, 168 | name: 'value', 169 | type: 'uint256', 170 | }, 171 | ], 172 | name: 'Transfer', 173 | type: 'event', 174 | }, 175 | ] 176 | -------------------------------------------------------------------------------- /packages/gnarly-reducer-erc20/src/index.ts: -------------------------------------------------------------------------------- 1 | 2 | export { 3 | default, 4 | } from './reducer' 5 | 6 | export * from './stores' 7 | export * from './models' 8 | -------------------------------------------------------------------------------- /packages/gnarly-reducer-erc20/src/models/index.ts: -------------------------------------------------------------------------------- 1 | export { default as sequelizeModels } from './sequelize' 2 | -------------------------------------------------------------------------------- /packages/gnarly-reducer-erc20/src/models/sequelize.ts: -------------------------------------------------------------------------------- 1 | import { makeSequelizeModels as makeGnarlyModels } from '@xlnt/gnarly-core' 2 | 3 | const sequelizeModels = (Sequelize: any, sequelize: any) => { 4 | const { DataTypes } = Sequelize 5 | const { Patch } = makeGnarlyModels(Sequelize, sequelize) 6 | 7 | const ERC20Balances = sequelize.define( 8 | 'erc20_balances', 9 | { 10 | id: { type: DataTypes.STRING, primaryKey: true }, 11 | tokenAddress: { type: DataTypes.STRING }, 12 | owner: { type: DataTypes.STRING }, 13 | balance: { type: DataTypes.DECIMAL(76, 0) }, 14 | balanceStr: { type: DataTypes.STRING }, 15 | // ^ keep a copy of the balance in string 16 | }, 17 | { 18 | indexes: [ 19 | // composite unique constraint on tokenAddress x owner 20 | { unique: true, fields: ['tokenAddress', 'owner'] }, 21 | // fast lookups of balances by tokenAddress 22 | { fields: ['tokenAddress'] }, 23 | // fast lookups by owner across tokens 24 | { fields: ['owner'] }, 25 | ], 26 | }, 27 | ) 28 | 29 | ERC20Balances.belongsTo(Patch) 30 | 31 | return { 32 | ERC20Balances, 33 | } 34 | } 35 | 36 | export default sequelizeModels 37 | -------------------------------------------------------------------------------- /packages/gnarly-reducer-erc20/src/reducer.ts: -------------------------------------------------------------------------------- 1 | import BN = require('bn.js') 2 | import makeDebug = require('debug') 3 | const debug = makeDebug('gnarly-reducer:erc20') 4 | 5 | import { 6 | addABI, 7 | Block, 8 | getLogs, 9 | IReducer, 10 | ITypeStore, 11 | ReducerType, 12 | toHex, 13 | } from '@xlnt/gnarly-core' 14 | 15 | import abi from './abi' 16 | 17 | const TRANSFER_REASON = 'ERC20_TRANSFER' 18 | const MINT_REASON = 'ERC20_MINT' 19 | const BURN_REASON = 'ERC20_BURN' 20 | 21 | const isZeroAddress = (address: string) => address === '0x0000000000000000000000000000000000000000' 22 | 23 | const makeReducer = (key: string, typeStore: ITypeStore) => ( 24 | tokenAddress: string, 25 | ): IReducer => { 26 | addABI(tokenAddress, abi) 27 | 28 | interface IERC20Tracker { 29 | balances: { 30 | [id: string]: { 31 | id: string, 32 | tokenAddress: string; 33 | owner: string; 34 | balance: string; 35 | balanceStr: string; 36 | }, 37 | }, 38 | } 39 | 40 | const makeActions = (state: IERC20Tracker, { operation, emit }) => ({ 41 | transfer: (from: string, to: string, rawTokenValue: string) => { 42 | debug('transferring %d %s tokens from %s to %s', rawTokenValue, key, from, to) 43 | 44 | // turn rawTokenValue into BN tokenValue 45 | const tokenValue = new BN(rawTokenValue) 46 | 47 | // hacky composite key for O(1) lookups required by JSON-Patch 48 | const fromId = `${tokenAddress}-${from}` 49 | const toId = `${tokenAddress}-${to}` 50 | 51 | // create new addresses' states which are missing 52 | // order dependent, operations must be done before appendTo 53 | if (!state.balances[fromId]) { 54 | operation(() => { 55 | const balance = tokenValue.toString() 56 | 57 | state.balances[fromId] = { 58 | id: fromId, 59 | tokenAddress, 60 | owner: from, 61 | balance, 62 | balanceStr: balance, 63 | } 64 | }) 65 | } 66 | 67 | if (!state.balances[toId]) { 68 | const balance = '0' 69 | 70 | operation(() => { 71 | state.balances[toId] = { 72 | id: toId, 73 | tokenAddress, 74 | owner: to, 75 | balance, 76 | balanceStr: balance, 77 | } 78 | }) 79 | } 80 | 81 | const fromBalance = new BN(state.balances[fromId].balance).sub(tokenValue).toString() 82 | // ^ subtract from sender 83 | const toBalance = new BN(state.balances[toId].balance).add(tokenValue).toString() 84 | // ^ add to receiver 85 | 86 | state.balances[fromId].balance = fromBalance 87 | state.balances[fromId].balanceStr = fromBalance 88 | 89 | state.balances[toId].balance = toBalance 90 | state.balances[toId].balanceStr = toBalance 91 | }, 92 | }) 93 | 94 | return { 95 | config: { 96 | type: ReducerType.TimeVarying, 97 | key, 98 | typeStore, 99 | }, 100 | state: { balances: {} }, 101 | reduce: async ( 102 | state: IERC20Tracker, 103 | block: Block, 104 | { because, operation, emit }, 105 | ): Promise => { 106 | const actions = makeActions(state, { operation, emit }) 107 | const logs = await getLogs({ 108 | fromBlock: toHex(block.number), 109 | toBlock: toHex(block.number), 110 | address: tokenAddress, 111 | }) 112 | 113 | logs.forEach((log) => { 114 | log.parse() 115 | if (log.eventName === 'Transfer') { 116 | const { from, to, value } = log.args 117 | let reason = TRANSFER_REASON 118 | 119 | if (isZeroAddress(from)) { 120 | reason = MINT_REASON 121 | } else if (isZeroAddress(to)) { 122 | reason = BURN_REASON 123 | } 124 | 125 | because(reason, {}, () => { 126 | actions.transfer(from, to, value) 127 | }) 128 | } 129 | }) 130 | }, 131 | } 132 | } 133 | 134 | export default makeReducer 135 | -------------------------------------------------------------------------------- /packages/gnarly-reducer-erc20/src/stores/index.ts: -------------------------------------------------------------------------------- 1 | export { default as makeSequelizeTypeStore } from './sequelize' 2 | -------------------------------------------------------------------------------- /packages/gnarly-reducer-erc20/src/stores/sequelize.ts: -------------------------------------------------------------------------------- 1 | import sequelizeModels from '../models/sequelize' 2 | 3 | import { 4 | SequelizeTypeStorer, 5 | } from '@xlnt/gnarly-core' 6 | 7 | const makeSequelizeTypeStore = ( 8 | Sequelize: any, 9 | sequelize: any, 10 | ) => { 11 | const { 12 | ERC20Balances, 13 | } = sequelizeModels(Sequelize, sequelize) 14 | 15 | // the type store 16 | return { 17 | __setup: async () => { 18 | await ERC20Balances.sync() 19 | }, 20 | __setdown: async () => { 21 | await ERC20Balances.drop({ cascade: true }) 22 | }, 23 | store: SequelizeTypeStorer(Sequelize, { 24 | balances: ERC20Balances, 25 | }), 26 | } 27 | } 28 | 29 | export default makeSequelizeTypeStore 30 | -------------------------------------------------------------------------------- /packages/gnarly-reducer-erc20/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.build.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "lib" 6 | }, 7 | "include": ["src"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/gnarly-reducer-erc721/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@xlnt/gnarly-reducer-erc721", 3 | "version": "0.6.0", 4 | "description": "A gnarly reducer for ERC721 Token information.", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "prepare": "npm run build", 8 | "build": "npm run build-ts && npm run tslint", 9 | "ts-start": "ts-node src/index.ts", 10 | "start": "node lib/index.js", 11 | "watch": "concurrently -k -p \"[{name}]\" -n \"TypeScript,Node\" -c \"cyan.bold,green.bold\" \"npm run watch-ts\" \"npm run watch-node\"", 12 | "test": "nyc --reporter=text mocha -r ts-node/register -r source-map-support/register --full-trace 'test/**/*.spec.ts'", 13 | "watch-test": " mocha --watch --watch-extensions ts -r ts-node/register 'test/**/*.spec.ts'", 14 | "coverage": "nyc report --reporter=text-lcov > ./lcov.info", 15 | "build-ts": "tsc", 16 | "watch-ts": "tsc -w", 17 | "lint": "tslint --project ." 18 | }, 19 | "files": [ 20 | "lib" 21 | ], 22 | "keywords": [ 23 | "gnarly" 24 | ], 25 | "repository": { 26 | "type": "git", 27 | "url": "git@github.com:XLNT/gnarly.git" 28 | }, 29 | "author": "Matt Condon ", 30 | "license": "Apache-2.0", 31 | "devDependencies": { 32 | "@types/chai": "^4.1.4", 33 | "@types/node": "^10.5.7", 34 | "@xlnt/gnarly-core": "^0.6.0", 35 | "chai": "^4.1.2", 36 | "mocha": "^5.2.0", 37 | "nyc": "^12.0.2", 38 | "rosie": "^2.0.1", 39 | "source-map-support": "^0.5.6", 40 | "tslint": "^5.11.0", 41 | "typescript": "^3.0.1" 42 | }, 43 | "publishConfig": { 44 | "access": "public" 45 | }, 46 | "dependencies": { 47 | "debug": "^3.1.0" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/gnarly-reducer-erc721/src/index.ts: -------------------------------------------------------------------------------- 1 | 2 | export { 3 | default, 4 | } from './reducer' 5 | 6 | export * from './stores' 7 | export * from './models' 8 | -------------------------------------------------------------------------------- /packages/gnarly-reducer-erc721/src/models/index.ts: -------------------------------------------------------------------------------- 1 | export { default as sequelizeModels } from './sequelize' 2 | -------------------------------------------------------------------------------- /packages/gnarly-reducer-erc721/src/models/sequelize.ts: -------------------------------------------------------------------------------- 1 | import { 2 | makeSequelizeModels as makeGnarlyModels, 3 | } from '@xlnt/gnarly-core' 4 | 5 | const sequelizeModels = ( 6 | Sequelize: any, 7 | sequelize: any, 8 | ) => { 9 | const { DataTypes } = Sequelize 10 | const { Patch } = makeGnarlyModels(Sequelize, sequelize) 11 | // ownerOf table 12 | const ERC721Tokens = sequelize.define('erc721_tokens', { 13 | id: { type: DataTypes.STRING, primaryKey: true }, 14 | darAddress: { type: DataTypes.STRING }, 15 | tokenId: { type: DataTypes.STRING }, 16 | 17 | // 1:1 properties of token 18 | owner: { type: DataTypes.STRING }, 19 | }, { 20 | indexes: [ 21 | // composite unique constraint on darAddress x tokenId 22 | { unique: true, fields: ['darAddress', 'tokenId'] }, 23 | // fast lookups of tokens by darAddress 24 | { fields: ['darAddress'] }, 25 | // fast lookups by owner across dars 26 | { fields: ['owner'] }, 27 | ], 28 | }) 29 | 30 | const ERC721TokenOwners = sequelize.define('erc721_owners', { 31 | uuid: { type: DataTypes.STRING, primaryKey: true }, 32 | 33 | // properties of each owner 34 | address: { type: DataTypes.STRING }, 35 | 36 | // this is a fk table so it needs an order key 37 | order: { type: DataTypes.INTEGER }, 38 | }) 39 | 40 | ERC721Tokens.belongsTo(Patch) 41 | ERC721TokenOwners.belongsTo(Patch) 42 | 43 | // token has many owners 44 | ERC721Tokens.hasMany(ERC721TokenOwners, { 45 | as: 'Owners', 46 | }) 47 | 48 | ERC721TokenOwners.belongsTo(ERC721Tokens, { 49 | as: 'Token', 50 | }) 51 | 52 | return { 53 | ERC721Tokens, 54 | ERC721TokenOwners, 55 | } 56 | } 57 | 58 | export default sequelizeModels 59 | -------------------------------------------------------------------------------- /packages/gnarly-reducer-erc721/src/reducer.ts: -------------------------------------------------------------------------------- 1 | import makeDebug = require('debug') 2 | const debug = makeDebug('gnarly-reducer:erc721') 3 | 4 | import { 5 | addABI, 6 | appendTo, 7 | Block, 8 | EmitOperationFn, 9 | getLogs, 10 | IReducer, 11 | ITypeStore, 12 | ReducerType, 13 | toHex, 14 | } from '@xlnt/gnarly-core' 15 | 16 | const TRANSFER_REASON = 'ERC721_TRANSFER' 17 | 18 | const makeReducer = ( 19 | key: string, 20 | typeStore: ITypeStore, 21 | ) => ( 22 | darAddress: string, 23 | ): IReducer => { 24 | 25 | // add the abi to the global registry 26 | // @TODO(shrugs): add the full abi as a constant 27 | addABI(darAddress, [{ 28 | anonymous: false, 29 | inputs: [ 30 | { indexed: false, name: 'from', type: 'address' }, 31 | { indexed: false, name: 'to', type: 'address' }, 32 | { indexed: false, name: 'tokenId', type: 'uint256' }, 33 | ], 34 | name: 'Transfer', 35 | type: 'event', 36 | }, { 37 | constant: false, 38 | inputs: [ 39 | { name: '_to', type: 'address' }, 40 | { name: '_tokenId', type: 'uint256' }, 41 | ], 42 | name: 'transfer', 43 | outputs: [], 44 | payable: false, 45 | stateMutability: 'nonpayable', 46 | type: 'function', 47 | }]) 48 | 49 | interface IERC721Tracker { 50 | tokens: { // 1:1 51 | [id: string]: { 52 | id: string, 53 | tokenId: string, 54 | darAddress: string, 55 | owner: string, 56 | }, 57 | } 58 | } 59 | 60 | const makeActions = (state: IERC721Tracker, { operation, emit }) => ({ 61 | transfer: (tokenId: string, from: string, to: string) => { 62 | debug('transferring token %s to %s', tokenId, to) 63 | 64 | // hacky composite key for O(1) lookups required by JSON-Patch 65 | const id = `${darAddress}-${tokenId}` 66 | 67 | const existing = state.tokens[id] 68 | if (existing) { 69 | // push 70 | existing.owner = to 71 | emit(appendTo('owners', { 72 | tokenId, 73 | address: to, 74 | })) 75 | } else { 76 | // init 77 | // order-dependent because of foreign key 78 | operation(() => { 79 | state.tokens[id] = { id, tokenId, darAddress, owner: to } 80 | }) 81 | emit(appendTo('owners', { 82 | tokenId, 83 | address: to, 84 | })) 85 | } 86 | }, 87 | }) 88 | 89 | return { 90 | config: { 91 | type: ReducerType.TimeVarying, 92 | key, 93 | typeStore, 94 | }, 95 | state: { tokens: {} }, 96 | reduce: async ( 97 | state: IERC721Tracker, 98 | block: Block, 99 | { because, operation, emit }, 100 | ): Promise => { 101 | const actions = makeActions(state, { operation, emit }) 102 | const logs = await getLogs({ 103 | fromBlock: toHex(block.number), 104 | toBlock: toHex(block.number), 105 | address: darAddress, 106 | }) 107 | 108 | logs.forEach((log) => { 109 | log.parse() 110 | if (log.eventName === 'Transfer') { 111 | const { to, from, tokenId } = log.args 112 | 113 | because(TRANSFER_REASON, {}, () => { 114 | actions.transfer(tokenId, from, to) 115 | }) 116 | } 117 | }) 118 | }, 119 | } 120 | } 121 | 122 | export default makeReducer 123 | -------------------------------------------------------------------------------- /packages/gnarly-reducer-erc721/src/stores/index.ts: -------------------------------------------------------------------------------- 1 | export { default as makeSequelizeTypeStore } from './sequelize' 2 | -------------------------------------------------------------------------------- /packages/gnarly-reducer-erc721/src/stores/sequelize.ts: -------------------------------------------------------------------------------- 1 | import sequelizeModels from '../models/sequelize' 2 | 3 | import { 4 | SequelizeTypeStorer, 5 | } from '@xlnt/gnarly-core' 6 | 7 | const makeSequelizeTypeStore = ( 8 | Sequelize: any, 9 | sequelize: any, 10 | ) => { 11 | const { 12 | ERC721Tokens, 13 | ERC721TokenOwners, 14 | } = sequelizeModels(Sequelize, sequelize) 15 | 16 | // the type store 17 | return { 18 | __setup: async () => { 19 | await ERC721Tokens.sync() 20 | await ERC721TokenOwners.sync() 21 | }, 22 | __setdown: async () => { 23 | await ERC721TokenOwners.drop({ cascade: true }) 24 | await ERC721Tokens.drop({ cascade: true }) 25 | }, 26 | store: SequelizeTypeStorer(Sequelize, { 27 | tokens: ERC721Tokens, 28 | owners: ERC721TokenOwners, 29 | }), 30 | } 31 | } 32 | 33 | export default makeSequelizeTypeStore 34 | -------------------------------------------------------------------------------- /packages/gnarly-reducer-erc721/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.build.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "lib" 6 | }, 7 | "include": ["src"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/gnarly-reducer-events/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@xlnt/gnarly-reducer-events", 3 | "version": "0.6.0", 4 | "description": "A gnarly reducer for arbitrary event information", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "prepare": "npm run build", 8 | "build": "npm run build-ts && npm run tslint", 9 | "ts-start": "ts-node src/index.ts", 10 | "start": "node lib/index.js", 11 | "watch": "concurrently -k -p \"[{name}]\" -n \"TypeScript,Node\" -c \"cyan.bold,green.bold\" \"npm run watch-ts\" \"npm run watch-node\"", 12 | "test": "nyc --reporter=text mocha -r ts-node/register -r source-map-support/register --full-trace 'test/**/*.spec.ts'", 13 | "watch-test": " mocha --watch --watch-extensions ts -r ts-node/register 'test/**/*.spec.ts'", 14 | "coverage": "nyc report --reporter=text-lcov > ./lcov.info", 15 | "build-ts": "tsc", 16 | "watch-ts": "tsc -w", 17 | "lint": "tslint --project ." 18 | }, 19 | "files": [ 20 | "lib" 21 | ], 22 | "keywords": [ 23 | "gnarly" 24 | ], 25 | "repository": { 26 | "type": "git", 27 | "url": "git@github.com:XLNT/gnarly.git" 28 | }, 29 | "author": "Matt Condon ", 30 | "license": "Apache-2.0", 31 | "devDependencies": { 32 | "@types/chai": "^4.1.4", 33 | "@types/node": "^10.5.7", 34 | "@xlnt/gnarly-core": "^0.6.0", 35 | "chai": "^4.1.2", 36 | "mocha": "^5.2.0", 37 | "nyc": "^12.0.2", 38 | "rosie": "^2.0.1", 39 | "source-map-support": "^0.5.6", 40 | "tslint": "^5.11.0", 41 | "typescript": "^3.0.1" 42 | }, 43 | "publishConfig": { 44 | "access": "public" 45 | }, 46 | "dependencies": { 47 | "arr-flatten": "^1.1.0", 48 | "debug": "^3.1.0" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /packages/gnarly-reducer-events/src/index.ts: -------------------------------------------------------------------------------- 1 | 2 | export { 3 | default, 4 | } from './reducer' 5 | 6 | export * from './stores' 7 | export * from './models' 8 | -------------------------------------------------------------------------------- /packages/gnarly-reducer-events/src/models/index.ts: -------------------------------------------------------------------------------- 1 | export { default as sequelizeModels } from './sequelize' 2 | -------------------------------------------------------------------------------- /packages/gnarly-reducer-events/src/models/sequelize.ts: -------------------------------------------------------------------------------- 1 | 2 | import { 3 | makeSequelizeModels as makeGnarlyModels, 4 | } from '@xlnt/gnarly-core' 5 | 6 | const sequelizeModels = ( 7 | Sequelize: any, 8 | sequelize: any, 9 | ) => { 10 | const { DataTypes } = Sequelize 11 | const { Patch } = makeGnarlyModels(Sequelize, sequelize) 12 | 13 | const Events = sequelize.define('events', { 14 | // gnarly-required columns 15 | uuid: { type: DataTypes.STRING, primaryKey: true }, 16 | 17 | address: { type: DataTypes.STRING }, 18 | event: { type: DataTypes.STRING }, 19 | eventName: { type: DataTypes.STRING }, 20 | signature: { type: DataTypes.STRING }, 21 | args: { type: DataTypes.JSONB }, 22 | }, { 23 | indexes: [ 24 | { fields: ['address'] }, 25 | { fields: ['event'] }, 26 | ], 27 | }) 28 | 29 | Events.Patch = Events.belongsTo(Patch) 30 | 31 | return { 32 | Events, 33 | } 34 | } 35 | 36 | export default sequelizeModels 37 | -------------------------------------------------------------------------------- /packages/gnarly-reducer-events/src/reducer.ts: -------------------------------------------------------------------------------- 1 | import { 2 | addABI, 3 | appendTo, 4 | Block, 5 | EmitOperationFn, 6 | forEach, 7 | getLogs, 8 | IABIItemInput, 9 | ILog, 10 | IReducer, 11 | ITypeStore, 12 | ReducerType, 13 | toHex, 14 | } from '@xlnt/gnarly-core' 15 | import flatten = require('arr-flatten') 16 | 17 | const makeReducer = ( 18 | key: string = 'events', 19 | typeStore: ITypeStore, 20 | ) => ( 21 | config: { [_: string]: IABIItemInput[] } = {}, 22 | ): IReducer => { 23 | const addrs = Object.keys(config) 24 | 25 | // add the abis to the global registry 26 | for (const addr of addrs) { 27 | addABI(addr, config[addr]) 28 | } 29 | 30 | const makeActions = (state: object, emit: EmitOperationFn) => ({ 31 | emit: (log: ILog) => { 32 | emit(appendTo('events', { 33 | address: log.address, 34 | event: log.event, 35 | eventName: log.eventName, 36 | signature: log.signature, 37 | args: log.args, 38 | })) 39 | }, 40 | }) 41 | 42 | return { 43 | config: { 44 | type: ReducerType.Atomic, 45 | key, 46 | typeStore, 47 | }, 48 | state: {}, 49 | reduce: async ( 50 | state: object, 51 | block: Block, 52 | { because, emit }, 53 | ): Promise => { 54 | const actions = makeActions(state, emit) 55 | const logs = await forEach(addrs, async (addr) => getLogs({ 56 | fromBlock: toHex(block.number), 57 | toBlock: toHex(block.number), 58 | address: addr, 59 | })) 60 | 61 | flatten(logs).forEach((log) => { 62 | const recognized = log.parse() 63 | if (recognized) { 64 | because('EVENT_EMITTED', {}, () => { 65 | actions.emit(log) 66 | }) 67 | } 68 | }) 69 | }, 70 | } 71 | } 72 | 73 | export default makeReducer 74 | -------------------------------------------------------------------------------- /packages/gnarly-reducer-events/src/stores/index.ts: -------------------------------------------------------------------------------- 1 | export { default as makeSequelizeTypeStore } from './sequelize' 2 | -------------------------------------------------------------------------------- /packages/gnarly-reducer-events/src/stores/sequelize.ts: -------------------------------------------------------------------------------- 1 | import sequelizeModels from '../models/sequelize' 2 | 3 | import { 4 | SequelizeTypeStorer, 5 | } from '@xlnt/gnarly-core' 6 | 7 | const makeSequelizeTypeStore = ( 8 | Sequelize: any, 9 | sequelize: any, 10 | ) => { 11 | const { 12 | Events, 13 | } = sequelizeModels(Sequelize, sequelize) 14 | 15 | return { 16 | __setup: async () => { 17 | await Events.sync() 18 | }, 19 | __setdown: async () => { 20 | await Events.drop({ cascade: true }) 21 | }, 22 | store: SequelizeTypeStorer(Sequelize, { 23 | events: Events, 24 | }), 25 | } 26 | } 27 | 28 | export default makeSequelizeTypeStore 29 | -------------------------------------------------------------------------------- /packages/gnarly-reducer-events/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.build.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "lib" 6 | }, 7 | "include": ["src"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "declaration": true, 5 | "declarationMap": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "module": "commonjs", 9 | "target": "es6", 10 | "moduleResolution": "node", 11 | "sourceMap": true, 12 | "lib": [ 13 | "es5", 14 | "es6", 15 | "es7", 16 | "dom", 17 | "es2015.core", 18 | "es2015.collection", 19 | "es2015.generator", 20 | "es2015.iterable", 21 | "es2015.promise", 22 | "es2015.proxy", 23 | "es2015.reflect", 24 | "es2015.symbol", 25 | "es2015.symbol.wellknown", 26 | "es2017", 27 | "esnext.asynciterable" 28 | ] 29 | }, 30 | "typeRoots": ["node_modules/@types"] 31 | } 32 | -------------------------------------------------------------------------------- /packages/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { "path": "gnarly-bin" }, 5 | { "path": "gnarly-core" }, 6 | { "path": "gnarly-reducer-block-meta" }, 7 | { "path": "gnarly-reducer-erc20" }, 8 | { "path": "gnarly-reducer-erc721" }, 9 | { "path": "gnarly-reducer-events" } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /packages/tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "declaration": true, 5 | "declarationMap": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "module": "commonjs", 9 | "target": "es6", 10 | "moduleResolution": "node", 11 | "sourceMap": true, 12 | "skipLibCheck": true, 13 | "lib": [ 14 | "es5", 15 | "es6", 16 | "es7", 17 | "dom", 18 | "es2015.core", 19 | "es2015.collection", 20 | "es2015.generator", 21 | "es2015.iterable", 22 | "es2015.promise", 23 | "es2015.proxy", 24 | "es2015.reflect", 25 | "es2015.symbol", 26 | "es2015.symbol.wellknown", 27 | "es2017", 28 | "esnext.asynciterable" 29 | ] 30 | }, 31 | "typeRoots": ["node_modules/@types"] 32 | } 33 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:recommended", 3 | "rules": { 4 | "only-arrow-functions": false, 5 | "space-before-function-paren": true, 6 | "semicolon": [true, "never"], 7 | "indent": [true, "spaces", 2], 8 | "quotemark": [true, "single", "avoid-template", "avoid-escape"], 9 | "object-literal-sort-keys": [false], 10 | "no-console": [false], 11 | "trailing-comma": [true, {"multiline": "always", "singleline": "never"}], 12 | "no-var-requires": false 13 | } 14 | } 15 | --------------------------------------------------------------------------------