From a4fc968429c5e9672287fca7e646842094e2e69a Mon Sep 17 00:00:00 2001 From: 0xaptosj <129789810+0xaptosj@users.noreply.github.com> Date: Mon, 23 Sep 2024 16:26:22 -0700 Subject: [PATCH] sync latest version --- .../{next-app => }/.eslintrc.json | 0 templates/indexer-template/.gitignore | 3 - templates/indexer-template/LICENSE | 201 ------------------ templates/indexer-template/README.md | 71 ++++++- templates/indexer-template/_gitignore | 34 +++ .../{next-app => }/components.json | 0 .../message-board => contract}/Move.toml | 3 - .../scripts/create_2_messages.move | 0 .../scripts/update_message.move | 0 .../sources/message_board.move | 0 .../tests/test_end_to_end.move | 0 .../indexer-template/contracts/.gitignore | 3 - .../indexer-template/contracts/README.MD | 3 - .../contracts/message-board/README.MD | 15 -- .../message-board/contract_address.txt | 1 - .../message-board/sh_scripts/deploy.sh | 22 -- .../message-board/sh_scripts/get_abis.sh | 17 -- .../message-board/sh_scripts/init.sh | 9 - .../run_create_2_messages_script.sh | 21 -- .../sh_scripts/run_update_message_script.sh | 21 -- .../message-board/sh_scripts/test.sh | 8 - .../message-board/sh_scripts/upgrade.sh | 17 -- templates/indexer-template/indexer/README.md | 20 +- .../up.sql | 15 +- .../2024-07-18-202400_test-migration/down.sql | 9 +- .../2024-07-18-202400_test-migration/up.sql | 9 +- .../2024-08-01-224558_ledger-infos/up.sql | 3 +- .../up.sql | 21 +- .../down.sql | 2 + .../up.sql | 13 ++ .../indexer/src/db_migrations/schema.rs | 14 ++ .../indexer/src/db_models/events_models.rs | 134 ------------ .../indexer/src/db_models/ledger_info.rs | 1 - .../indexer/src/db_models/message.rs | 68 ++++++ .../indexer/src/db_models/mod.rs | 3 +- .../indexer/src/db_models/processor_status.rs | 1 - .../indexer/src/db_models/user_stat.rs | 18 ++ .../indexer/src/health_check_server.rs | 3 +- .../src/processors/events/events_extractor.rs | 73 ++++++- .../src/processors/events/events_processor.rs | 13 +- .../src/processors/events/events_storer.rs | 6 +- .../storers/create_message_event_storer.rs | 141 +++++++++--- .../storers/update_message_event_storer.rs | 179 +++++++++++----- .../indexer/src/utils/chain_id.rs | 19 +- .../indexer/src/utils/database_connection.rs | 26 ++- .../indexer/src/utils/database_execution.rs | 98 ++------- .../indexer/src/utils/database_utils.rs | 20 +- .../utils/latest_processed_version_tracker.rs | 15 +- .../indexer-template/next-app/.example.env | 1 - .../indexer-template/next-app/.gitignore | 37 ---- templates/indexer-template/next-app/README.md | 67 ------ .../next-app/src/components/Message.tsx | 96 --------- .../next-app/src/components/RootHeader.tsx | 28 --- .../src/components/SendTransaction.tsx | 10 - .../next-app/src/db/getLastSuccessVersion.ts | 14 -- .../next-app/src/db/getMessages.ts | 35 --- .../next-app/src/lib/type/message.ts | 21 -- .../next-app/src/lib/utils.ts | 6 - .../{next-app => }/next.config.mjs | 0 .../{next-app => }/package.json | 42 ++-- .../{next-app => }/postcss.config.mjs | 0 .../{next-app => }/public/next.svg | 0 .../{next-app => }/public/vercel.svg | 0 .../indexer-template/scripts/move/compile.js | 15 ++ .../indexer-template/scripts/move/publish.js | 45 ++++ .../indexer-template/scripts/move/test.js | 15 ++ .../indexer-template/scripts/move/upgrade.js | 24 +++ .../{next-app => }/src/app/actions.ts | 22 +- .../src/app/analytics/page.tsx | 9 + .../{next-app => }/src/app/favicon.ico | Bin .../{next-app => }/src/app/globals.css | 0 .../{next-app => }/src/app/layout.tsx | 7 +- .../src/app/message/[messageObjAddr]/page.tsx | 0 .../src/app/message/layout.tsx | 13 ++ .../{next-app => }/src/app/page.tsx | 8 +- .../src/components/Analytics.tsx | 16 ++ .../components/CreateMessage.tsx} | 4 +- .../src/components/ExplorerLink.tsx | 0 .../src/components/IndexerStatus.tsx | 4 +- .../src/components/LabelValueGrid.tsx | 0 .../src/components/Message.tsx | 118 ++++++++++ .../src/components/MessageBoard.tsx | 2 +- .../src/components/RootFooter.tsx | 9 + .../src/components/RootHeader.tsx | 36 ++++ .../src/components/ThemeToggle.tsx | 0 .../src/components/UpdateMessage.tsx | 125 +++++++++++ .../src/components/WrongNetworkAlert.tsx | 0 .../components/analytics-board/columns.tsx | 62 ++++++ .../components/analytics-board/data-table.tsx | 150 +++++++++++++ .../src/components/message-board/columns.tsx | 15 +- .../message-board/data-table-row-actions.tsx | 4 +- .../components/message-board/data-table.tsx | 15 +- .../components/providers/QueryProvider.tsx | 0 .../components/providers/ThemeProvider.tsx | 0 .../components/providers/WalletProvider.tsx | 0 .../src/components/ui/alert.tsx | 0 .../src/components/ui/button.tsx | 0 .../{next-app => }/src/components/ui/card.tsx | 0 .../src/components/ui/checkbox.tsx | 0 .../src/components/ui/collapsible.tsx | 0 .../ui}/data-table-column-header.tsx | 0 .../components/ui}/data-table-pagination.tsx | 0 .../src/components/ui/dialog.tsx | 0 .../src/components/ui/dropdown-menu.tsx | 0 .../{next-app => }/src/components/ui/form.tsx | 0 .../src/components/ui/input.tsx | 0 .../src/components/ui/label.tsx | 0 .../src/components/ui/radio-group.tsx | 0 .../src/components/ui/select.tsx | 0 .../src/components/ui/switch.tsx | 0 .../src/components/ui/table.tsx | 0 .../src/components/ui/toast.tsx | 0 .../src/components/ui/toaster.tsx | 0 .../src/components/ui/tooltip.tsx | 0 .../src/components/ui/use-toast.ts | 0 .../components/wallet/WalletConnection.tsx | 0 .../src/components/wallet/WalletSelector.tsx | 0 .../src/db/getLastSuccessVersion.ts | 12 ++ .../{next-app => }/src/db/getMessage.ts | 15 +- .../indexer-template/src/db/getMessages.ts | 43 ++++ .../indexer-template/src/db/getUserStats.ts | 44 ++++ .../src/lib/abi/message_board_abi.ts | 0 .../{next-app => }/src/lib/aptos.ts | 0 templates/indexer-template/src/lib/db.ts | 5 + .../src/lib/type/indexer_status.ts | 0 .../indexer-template/src/lib/type/message.ts | 8 + .../src/lib/type/user_stats.ts | 9 + templates/indexer-template/src/lib/utils.ts | 6 + .../{next-app => }/tailwind.config.ts | 0 .../{next-app => }/tsconfig.json | 0 130 files changed, 1482 insertions(+), 1108 deletions(-) rename templates/indexer-template/{next-app => }/.eslintrc.json (100%) delete mode 100644 templates/indexer-template/.gitignore delete mode 100644 templates/indexer-template/LICENSE create mode 100644 templates/indexer-template/_gitignore rename templates/indexer-template/{next-app => }/components.json (100%) rename templates/indexer-template/{contracts/message-board => contract}/Move.toml (85%) rename templates/indexer-template/{contracts/message-board => contract}/scripts/create_2_messages.move (100%) rename templates/indexer-template/{contracts/message-board => contract}/scripts/update_message.move (100%) rename templates/indexer-template/{contracts/message-board => contract}/sources/message_board.move (100%) rename templates/indexer-template/{contracts/message-board => contract}/tests/test_end_to_end.move (100%) delete mode 100644 templates/indexer-template/contracts/.gitignore delete mode 100644 templates/indexer-template/contracts/README.MD delete mode 100644 templates/indexer-template/contracts/message-board/README.MD delete mode 100644 templates/indexer-template/contracts/message-board/contract_address.txt delete mode 100755 templates/indexer-template/contracts/message-board/sh_scripts/deploy.sh delete mode 100755 templates/indexer-template/contracts/message-board/sh_scripts/get_abis.sh delete mode 100755 templates/indexer-template/contracts/message-board/sh_scripts/init.sh delete mode 100755 templates/indexer-template/contracts/message-board/sh_scripts/run_create_2_messages_script.sh delete mode 100755 templates/indexer-template/contracts/message-board/sh_scripts/run_update_message_script.sh delete mode 100755 templates/indexer-template/contracts/message-board/sh_scripts/test.sh delete mode 100755 templates/indexer-template/contracts/message-board/sh_scripts/upgrade.sh create mode 100644 templates/indexer-template/indexer/src/db_migrations/migrations/2024-09-19-234343_create-user-stats-table/down.sql create mode 100644 templates/indexer-template/indexer/src/db_migrations/migrations/2024-09-19-234343_create-user-stats-table/up.sql delete mode 100644 templates/indexer-template/indexer/src/db_models/events_models.rs create mode 100644 templates/indexer-template/indexer/src/db_models/message.rs create mode 100644 templates/indexer-template/indexer/src/db_models/user_stat.rs delete mode 100644 templates/indexer-template/next-app/.example.env delete mode 100644 templates/indexer-template/next-app/.gitignore delete mode 100644 templates/indexer-template/next-app/README.md delete mode 100644 templates/indexer-template/next-app/src/components/Message.tsx delete mode 100644 templates/indexer-template/next-app/src/components/RootHeader.tsx delete mode 100644 templates/indexer-template/next-app/src/components/SendTransaction.tsx delete mode 100644 templates/indexer-template/next-app/src/db/getLastSuccessVersion.ts delete mode 100644 templates/indexer-template/next-app/src/db/getMessages.ts delete mode 100644 templates/indexer-template/next-app/src/lib/type/message.ts delete mode 100644 templates/indexer-template/next-app/src/lib/utils.ts rename templates/indexer-template/{next-app => }/next.config.mjs (100%) rename templates/indexer-template/{next-app => }/package.json (53%) rename templates/indexer-template/{next-app => }/postcss.config.mjs (100%) rename templates/indexer-template/{next-app => }/public/next.svg (100%) rename templates/indexer-template/{next-app => }/public/vercel.svg (100%) create mode 100644 templates/indexer-template/scripts/move/compile.js create mode 100644 templates/indexer-template/scripts/move/publish.js create mode 100644 templates/indexer-template/scripts/move/test.js create mode 100644 templates/indexer-template/scripts/move/upgrade.js rename templates/indexer-template/{next-app => }/src/app/actions.ts (58%) create mode 100644 templates/indexer-template/src/app/analytics/page.tsx rename templates/indexer-template/{next-app => }/src/app/favicon.ico (100%) rename templates/indexer-template/{next-app => }/src/app/globals.css (100%) rename templates/indexer-template/{next-app => }/src/app/layout.tsx (90%) rename templates/indexer-template/{next-app => }/src/app/message/[messageObjAddr]/page.tsx (100%) create mode 100644 templates/indexer-template/src/app/message/layout.tsx rename templates/indexer-template/{next-app => }/src/app/page.tsx (51%) create mode 100644 templates/indexer-template/src/components/Analytics.tsx rename templates/indexer-template/{next-app/src/components/PostMessageWithSurf.tsx => src/components/CreateMessage.tsx} (96%) rename templates/indexer-template/{next-app => }/src/components/ExplorerLink.tsx (100%) rename templates/indexer-template/{next-app => }/src/components/IndexerStatus.tsx (94%) rename templates/indexer-template/{next-app => }/src/components/LabelValueGrid.tsx (100%) create mode 100644 templates/indexer-template/src/components/Message.tsx rename templates/indexer-template/{next-app => }/src/components/MessageBoard.tsx (91%) create mode 100644 templates/indexer-template/src/components/RootFooter.tsx create mode 100644 templates/indexer-template/src/components/RootHeader.tsx rename templates/indexer-template/{next-app => }/src/components/ThemeToggle.tsx (100%) create mode 100644 templates/indexer-template/src/components/UpdateMessage.tsx rename templates/indexer-template/{next-app => }/src/components/WrongNetworkAlert.tsx (100%) create mode 100644 templates/indexer-template/src/components/analytics-board/columns.tsx create mode 100644 templates/indexer-template/src/components/analytics-board/data-table.tsx rename templates/indexer-template/{next-app => }/src/components/message-board/columns.tsx (59%) rename templates/indexer-template/{next-app => }/src/components/message-board/data-table-row-actions.tsx (93%) rename templates/indexer-template/{next-app => }/src/components/message-board/data-table.tsx (91%) rename templates/indexer-template/{next-app => }/src/components/providers/QueryProvider.tsx (100%) rename templates/indexer-template/{next-app => }/src/components/providers/ThemeProvider.tsx (100%) rename templates/indexer-template/{next-app => }/src/components/providers/WalletProvider.tsx (100%) rename templates/indexer-template/{next-app => }/src/components/ui/alert.tsx (100%) rename templates/indexer-template/{next-app => }/src/components/ui/button.tsx (100%) rename templates/indexer-template/{next-app => }/src/components/ui/card.tsx (100%) rename templates/indexer-template/{next-app => }/src/components/ui/checkbox.tsx (100%) rename templates/indexer-template/{next-app => }/src/components/ui/collapsible.tsx (100%) rename templates/indexer-template/{next-app/src/components/message-board => src/components/ui}/data-table-column-header.tsx (100%) rename templates/indexer-template/{next-app/src/components/message-board => src/components/ui}/data-table-pagination.tsx (100%) rename templates/indexer-template/{next-app => }/src/components/ui/dialog.tsx (100%) rename templates/indexer-template/{next-app => }/src/components/ui/dropdown-menu.tsx (100%) rename templates/indexer-template/{next-app => }/src/components/ui/form.tsx (100%) rename templates/indexer-template/{next-app => }/src/components/ui/input.tsx (100%) rename templates/indexer-template/{next-app => }/src/components/ui/label.tsx (100%) rename templates/indexer-template/{next-app => }/src/components/ui/radio-group.tsx (100%) rename templates/indexer-template/{next-app => }/src/components/ui/select.tsx (100%) rename templates/indexer-template/{next-app => }/src/components/ui/switch.tsx (100%) rename templates/indexer-template/{next-app => }/src/components/ui/table.tsx (100%) rename templates/indexer-template/{next-app => }/src/components/ui/toast.tsx (100%) rename templates/indexer-template/{next-app => }/src/components/ui/toaster.tsx (100%) rename templates/indexer-template/{next-app => }/src/components/ui/tooltip.tsx (100%) rename templates/indexer-template/{next-app => }/src/components/ui/use-toast.ts (100%) rename templates/indexer-template/{next-app => }/src/components/wallet/WalletConnection.tsx (100%) rename templates/indexer-template/{next-app => }/src/components/wallet/WalletSelector.tsx (100%) create mode 100644 templates/indexer-template/src/db/getLastSuccessVersion.ts rename templates/indexer-template/{next-app => }/src/db/getMessage.ts (63%) create mode 100644 templates/indexer-template/src/db/getMessages.ts create mode 100644 templates/indexer-template/src/db/getUserStats.ts rename templates/indexer-template/{next-app => }/src/lib/abi/message_board_abi.ts (100%) rename templates/indexer-template/{next-app => }/src/lib/aptos.ts (100%) create mode 100644 templates/indexer-template/src/lib/db.ts rename templates/indexer-template/{next-app => }/src/lib/type/indexer_status.ts (100%) create mode 100644 templates/indexer-template/src/lib/type/message.ts create mode 100644 templates/indexer-template/src/lib/type/user_stats.ts create mode 100644 templates/indexer-template/src/lib/utils.ts rename templates/indexer-template/{next-app => }/tailwind.config.ts (100%) rename templates/indexer-template/{next-app => }/tsconfig.json (100%) diff --git a/templates/indexer-template/next-app/.eslintrc.json b/templates/indexer-template/.eslintrc.json similarity index 100% rename from templates/indexer-template/next-app/.eslintrc.json rename to templates/indexer-template/.eslintrc.json diff --git a/templates/indexer-template/.gitignore b/templates/indexer-template/.gitignore deleted file mode 100644 index 4a3cce43..00000000 --- a/templates/indexer-template/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -.idea -.DS_Store -*.pem diff --git a/templates/indexer-template/LICENSE b/templates/indexer-template/LICENSE deleted file mode 100644 index a123aa87..00000000 --- a/templates/indexer-template/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Copyright 2023 Aptos Foundation - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/templates/indexer-template/README.md b/templates/indexer-template/README.md index b9c89409..2f220657 100644 --- a/templates/indexer-template/README.md +++ b/templates/indexer-template/README.md @@ -1,10 +1,67 @@ -# A template to build a full stack app on Aptos +# Aptos full stack template UI -Please read each directory's readme carefully to understand how to use the template. +This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). -This template is an alternative template to [CAD (create-aptos-dapp)](https://aptos.dev/en/build/create-aptos-dapp). Some notable differences are: +## Local development -- This template uses more experimental features, such as [indexer-sdk](https://github.com/aptos-labs/aptos-indexer-processor-sdk). -- This template uses Next.js instead of Vite. -- This templates doesn't support Windows. -- CAD provides more production ready templates that have been audited like token minting, NFT minting, etc. +This template uses `@neondatabase/serverless` to connect to a Postgres database. When testing locally, you can connect to a dev branch of neon DB. + +## Create a read only user in DB + +Frontend should only read from the DB, the indexer is the only one that writes to the DB. So, create a read only user in the DB for frontend to use prevent any accidental write operations. + +```sql +-- Create a readonly user +-- Please don't use any special characters in the password to avoid db sdk give invalid connection string error +CREATE USER readonly WITH PASSWORD 'strong_password' +-- Grant readonly user read access to all tables in public schema +GRANT SELECT ON ALL TABLES IN SCHEMA public TO readonly; +-- Grant readonly user read access to all future tables in public schema +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO readonly; +``` + +Some useful SQLs to check the user and schema: + +```sql +-- Get all users +SELECT * FROM pg_user; +-- Get all schemas +SELECT schema_name FROM information_schema.schemata; +``` + +Then fill the `POSTGRES_URL` in `.env` file with the readonly user. + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. + +This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. diff --git a/templates/indexer-template/_gitignore b/templates/indexer-template/_gitignore new file mode 100644 index 00000000..5a331de5 --- /dev/null +++ b/templates/indexer-template/_gitignore @@ -0,0 +1,34 @@ +# Aptos related files +.aptos +.env +contract/build + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +build +dist-ssr +*.local +package-lock.json +pnpm-lock.yaml + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +.next diff --git a/templates/indexer-template/next-app/components.json b/templates/indexer-template/components.json similarity index 100% rename from templates/indexer-template/next-app/components.json rename to templates/indexer-template/components.json diff --git a/templates/indexer-template/contracts/message-board/Move.toml b/templates/indexer-template/contract/Move.toml similarity index 85% rename from templates/indexer-template/contracts/message-board/Move.toml rename to templates/indexer-template/contract/Move.toml index 7cfb8b81..56a33336 100644 --- a/templates/indexer-template/contracts/message-board/Move.toml +++ b/templates/indexer-template/contract/Move.toml @@ -6,9 +6,6 @@ authors = [] [addresses] message_board_addr = "_" -[dev-addresses] -message_board_addr = "0x999" - [dependencies] AptosFramework = { git = "https://github.com/aptos-labs/aptos-core.git", rev = "mainnet", subdir = "aptos-move/framework/aptos-framework" } diff --git a/templates/indexer-template/contracts/message-board/scripts/create_2_messages.move b/templates/indexer-template/contract/scripts/create_2_messages.move similarity index 100% rename from templates/indexer-template/contracts/message-board/scripts/create_2_messages.move rename to templates/indexer-template/contract/scripts/create_2_messages.move diff --git a/templates/indexer-template/contracts/message-board/scripts/update_message.move b/templates/indexer-template/contract/scripts/update_message.move similarity index 100% rename from templates/indexer-template/contracts/message-board/scripts/update_message.move rename to templates/indexer-template/contract/scripts/update_message.move diff --git a/templates/indexer-template/contracts/message-board/sources/message_board.move b/templates/indexer-template/contract/sources/message_board.move similarity index 100% rename from templates/indexer-template/contracts/message-board/sources/message_board.move rename to templates/indexer-template/contract/sources/message_board.move diff --git a/templates/indexer-template/contracts/message-board/tests/test_end_to_end.move b/templates/indexer-template/contract/tests/test_end_to_end.move similarity index 100% rename from templates/indexer-template/contracts/message-board/tests/test_end_to_end.move rename to templates/indexer-template/contract/tests/test_end_to_end.move diff --git a/templates/indexer-template/contracts/.gitignore b/templates/indexer-template/contracts/.gitignore deleted file mode 100644 index 0f577f58..00000000 --- a/templates/indexer-template/contracts/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -# move -.aptos -build diff --git a/templates/indexer-template/contracts/README.MD b/templates/indexer-template/contracts/README.MD deleted file mode 100644 index 0fa41d47..00000000 --- a/templates/indexer-template/contracts/README.MD +++ /dev/null @@ -1,3 +0,0 @@ -# Contracts - -This directory contains all contracts used by the app. If you want to add a new contract, just create a new folder with the contract name and a `Move.toml` file inside it. diff --git a/templates/indexer-template/contracts/message-board/README.MD b/templates/indexer-template/contracts/message-board/README.MD deleted file mode 100644 index 094c61ae..00000000 --- a/templates/indexer-template/contracts/message-board/README.MD +++ /dev/null @@ -1,15 +0,0 @@ -# Message board contract - -This is a simple message board contract that allows users to post messages on-chain and read messages from the chain. For both `create` and `update` endpoints, contract emits Create event and Update event respectively, then indexer will parse these events and store the message in the database for frontend to query efficiently. - -- `scripts` directory contains Move scripts that can batch multiple contract calls in 1 transaction. -- `tests` directory contains Move unit tests. -- `sources` directory contains the main contract code. -- `sh_scripts` directory contains shell scripts that can be used to test, publish, upgrade the contract, run Move scripts and generate TypeScript ABI. You can run them in this order: - - `./sh_scripts/init.sh`: create a new wallet - - `./sh_scripts/test.sh`: test the contract - - `./sh_scripts/deploy.sh`: deploy the contract - - `./sh_scripts/upgrade.sh`: upgrade the contract, you can only run this when you make compatible changes, changing existing structs and function signatures are considered incompatible changes. For incompatible changes, you need to deploy a new contract and migrate the data to new contract manually. - - `./sh_scripts/get_abis.sh`: generate TypeScript ABI in the frontend directory and node scripts directory. - - `./sh_scripts/run_create_2_messages_script.sh`: run Move script to create 2 messages in 1 transaction. - - `./sh_scripts/run_update_message_script.sh`: run Move script to update a messages in 1 transaction. diff --git a/templates/indexer-template/contracts/message-board/contract_address.txt b/templates/indexer-template/contracts/message-board/contract_address.txt deleted file mode 100644 index a0228e19..00000000 --- a/templates/indexer-template/contracts/message-board/contract_address.txt +++ /dev/null @@ -1 +0,0 @@ -0xda6c2a8c4eae7b4fb7ef1b3319cc201492cc04e49d17b57dd75456e9b85e84b4 diff --git a/templates/indexer-template/contracts/message-board/sh_scripts/deploy.sh b/templates/indexer-template/contracts/message-board/sh_scripts/deploy.sh deleted file mode 100755 index 1eb68b3a..00000000 --- a/templates/indexer-template/contracts/message-board/sh_scripts/deploy.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/bin/sh - -set -e - -echo "##### Deploy module under a new object #####" - -# Profile is the account you used to execute transaction -# Run "aptos init" to create the profile, then get the profile name from .aptos/config.yaml -PUBLISHER_PROFILE=testnet-profile-1 - -PUBLISHER_ADDR=0x$(aptos config show-profiles --profile=$PUBLISHER_PROFILE | grep 'account' | sed -n 's/.*"account": \"\(.*\)\".*/\1/p') - -OUTPUT=$(aptos move create-object-and-publish-package \ - --address-name message_board_addr \ - --named-addresses message_board_addr=$PUBLISHER_ADDR \ - --profile $PUBLISHER_PROFILE \ - --assume-yes) - -# Extract the published contract address and save it to a file -echo "$OUTPUT" | grep "Code was successfully deployed to object address" | awk '{print $NF}' | sed 's/\.$//' > contract_address.txt -echo "Contract published to address: $(cat contract_address.txt)" -echo "Contract address saved to contract_address.txt" diff --git a/templates/indexer-template/contracts/message-board/sh_scripts/get_abis.sh b/templates/indexer-template/contracts/message-board/sh_scripts/get_abis.sh deleted file mode 100755 index 8033c58d..00000000 --- a/templates/indexer-template/contracts/message-board/sh_scripts/get_abis.sh +++ /dev/null @@ -1,17 +0,0 @@ -#! /bin/bash - -NETWORK=testnet - -CONTRACT_ADDRESS=$(cat ./contract_address.txt) - -MODULE_NAME=message_board - -ABI="export const ABI = $(curl https://fullnode.$NETWORK.aptoslabs.com/v1/accounts/$CONTRACT_ADDRESS/module/$MODULE_NAME | sed -n 's/.*"abi":\({.*}\).*}$/\1/p') as const" - -NEXT_APP_ABI_DIR="../../next-app/src/lib/abi" -mkdir -p $NEXT_APP_ABI_DIR -echo $ABI > $NEXT_APP_ABI_DIR/${MODULE_NAME}_abi.ts - -NODE_SCRIPTS_ABI_DIR="../../node-scripts/src/lib/abi" -mkdir -p $NODE_SCRIPTS_ABI_DIR -echo $ABI > $NODE_SCRIPTS_ABI_DIR/${MODULE_NAME}_abi.ts diff --git a/templates/indexer-template/contracts/message-board/sh_scripts/init.sh b/templates/indexer-template/contracts/message-board/sh_scripts/init.sh deleted file mode 100755 index 6deea426..00000000 --- a/templates/indexer-template/contracts/message-board/sh_scripts/init.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/sh - -set -e - -echo "##### Creating a new Aptos account #####" - -aptos init \ - --network testnet \ - --profile testnet-profile-1 diff --git a/templates/indexer-template/contracts/message-board/sh_scripts/run_create_2_messages_script.sh b/templates/indexer-template/contracts/message-board/sh_scripts/run_create_2_messages_script.sh deleted file mode 100755 index 4d915ee5..00000000 --- a/templates/indexer-template/contracts/message-board/sh_scripts/run_create_2_messages_script.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/bin/sh - -set -e - -echo "##### Running move script to create 2 messages in 1 tx #####" - -CONTRACT_ADDRESS=$(cat contract_address.txt) - -# Need to compile the package first -aptos move compile \ - --named-addresses message_board_addr=$CONTRACT_ADDRESS - -# Profile is the account you used to execute transaction -# Run "aptos init" to create the profile, then get the profile name from .aptos/config.yaml -SENDER_PROFILE=testnet-profile-1 - -# Run the script -aptos move run-script \ - --assume-yes \ - --profile $SENDER_PROFILE \ - --compiled-script-path build/message-board/bytecode_scripts/create_2_messages.mv diff --git a/templates/indexer-template/contracts/message-board/sh_scripts/run_update_message_script.sh b/templates/indexer-template/contracts/message-board/sh_scripts/run_update_message_script.sh deleted file mode 100755 index f4b2b51a..00000000 --- a/templates/indexer-template/contracts/message-board/sh_scripts/run_update_message_script.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/bin/sh - -set -e - -echo "##### Running move script to update some messages in 1 tx #####" - -CONTRACT_ADDRESS=$(cat contract_address.txt) - -# Need to compile the package first -aptos move compile \ - --named-addresses message_board_addr=$CONTRACT_ADDRESS - -# Profile is the account you used to execute transaction -# Run "aptos init" to create the profile, then get the profile name from .aptos/config.yaml -SENDER_PROFILE=testnet-profile-1 - -# Run the script -aptos move run-script \ - --assume-yes \ - --profile $SENDER_PROFILE \ - --compiled-script-path build/message-board/bytecode_scripts/update_message.mv diff --git a/templates/indexer-template/contracts/message-board/sh_scripts/test.sh b/templates/indexer-template/contracts/message-board/sh_scripts/test.sh deleted file mode 100755 index 3f067691..00000000 --- a/templates/indexer-template/contracts/message-board/sh_scripts/test.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/sh - -set -e - -echo "##### Running tests #####" - -aptos move test \ - --dev diff --git a/templates/indexer-template/contracts/message-board/sh_scripts/upgrade.sh b/templates/indexer-template/contracts/message-board/sh_scripts/upgrade.sh deleted file mode 100755 index 9e88d176..00000000 --- a/templates/indexer-template/contracts/message-board/sh_scripts/upgrade.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/sh - -set -e - -echo "##### Upgrade module #####" - -# Profile is the account you used to execute transaction -# Run "aptos init" to create the profile, then get the profile name from .aptos/config.yaml -PUBLISHER_PROFILE=testnet-profile-1 - -CONTRACT_ADDRESS=$(cat contract_address.txt) - -aptos move upgrade-object-package \ - --object-address $CONTRACT_ADDRESS \ - --named-addresses message_board_addr=$CONTRACT_ADDRESS \ - --profile $PUBLISHER_PROFILE \ - --assume-yes diff --git a/templates/indexer-template/indexer/README.md b/templates/indexer-template/indexer/README.md index 6a6e0ef6..b08eacbd 100644 --- a/templates/indexer-template/indexer/README.md +++ b/templates/indexer-template/indexer/README.md @@ -4,20 +4,24 @@ This indexer is created from indexer-sdk, see a more detailed readme in [example We use the term indexer and processor interchangeably. -## Pre-requisites +When developing locally, you can use a local Postgres DB and run the indexer locally. + +When deploying to the cloud, I recommend using [Neon Postgres](https://neon.tech/) or Google Cloud SQL for database and Google Cloud Run for hosting indexer. -Create a Vercel account and a Google Cloud account. We use Vercel to host the Postgres DB and Google Cloud to host the indexer. +## Pre-requisites -Create a new Vercel Postgres DB and a new Google Cloud project. +Install rust. -Learn more about Vercel Postgres on [their docs](https://vercel.com/docs/storage/vercel-postgres). +Install postgres. -Install diesel cli to run migrations. +Install diesel cli to run migrations. Please only use this command to install diesel cli because we only need postgres feature. ```sh cargo install diesel_cli --no-default-features --features postgres ``` +Install docker because we need to put indexer in docker container when deploying to cloud. + ## Running the indexer locally **Note: all commends below need to be run in the current indexer directory instead of root directory.** @@ -134,3 +138,9 @@ Video walkthrough: https://drive.google.com/file/d/1JayWuH2qgnqOgzVuZm9MwKT42hj4 Go to cloud run dashboard, create a new service, and select the container image from Artifact Registry, also add a volume to ready the config.yaml file from Secret Manager, then mount the volume to the container. **NOTE**: always allocate cpu so it always runs instead of only run when there is traffic. Min and max instances should be 1. + +## Re-indexing + +If you make change to DB schema or update the point calculation logic, you need to re-index the data. + +**WARNING**: Do not try to backfill the data, the point data logic is read + update, if you backfill like processing same events twice, you will get wrong point data. So please always revert all migrations and re-index from the first tx your contract deployed. diff --git a/templates/indexer-template/indexer/src/db_migrations/migrations/2024-07-18-194547_create-processor-status/up.sql b/templates/indexer-template/indexer/src/db_migrations/migrations/2024-07-18-194547_create-processor-status/up.sql index d34baf1b..fc176980 100644 --- a/templates/indexer-template/indexer/src/db_migrations/migrations/2024-07-18-194547_create-processor-status/up.sql +++ b/templates/indexer-template/indexer/src/db_migrations/migrations/2024-07-18-194547_create-processor-status/up.sql @@ -1,8 +1,9 @@ -- Your SQL goes here -CREATE TABLE processor_status ( - processor VARCHAR(50) NOT NULL, - last_success_version BIGINT NOT NULL, - last_updated TIMESTAMP NOT NULL, - last_transaction_timestamp TIMESTAMP NULL, - PRIMARY KEY (processor) -); \ No newline at end of file +CREATE TABLE + processor_status ( + processor VARCHAR(50) NOT NULL, + last_success_version BIGINT NOT NULL, + last_updated TIMESTAMP NOT NULL, + last_transaction_timestamp TIMESTAMP NULL, + PRIMARY KEY (processor) + ); \ No newline at end of file diff --git a/templates/indexer-template/indexer/src/db_migrations/migrations/2024-07-18-202400_test-migration/down.sql b/templates/indexer-template/indexer/src/db_migrations/migrations/2024-07-18-202400_test-migration/down.sql index 26a34c80..8f68301b 100644 --- a/templates/indexer-template/indexer/src/db_migrations/migrations/2024-07-18-202400_test-migration/down.sql +++ b/templates/indexer-template/indexer/src/db_migrations/migrations/2024-07-18-202400_test-migration/down.sql @@ -1,3 +1,8 @@ -- This file should undo anything in `up.sql` -ALTER TABLE IF EXISTS events ALTER COLUMN inserted_at DROP DEFAULT; -ALTER TABLE IF EXISTS processor_status ALTER COLUMN last_updated DROP DEFAULT; \ No newline at end of file +ALTER TABLE IF EXISTS events +ALTER COLUMN inserted_at +DROP DEFAULT; + +ALTER TABLE IF EXISTS processor_status +ALTER COLUMN last_updated +DROP DEFAULT; \ No newline at end of file diff --git a/templates/indexer-template/indexer/src/db_migrations/migrations/2024-07-18-202400_test-migration/up.sql b/templates/indexer-template/indexer/src/db_migrations/migrations/2024-07-18-202400_test-migration/up.sql index e8e2a5e6..ce0950dd 100644 --- a/templates/indexer-template/indexer/src/db_migrations/migrations/2024-07-18-202400_test-migration/up.sql +++ b/templates/indexer-template/indexer/src/db_migrations/migrations/2024-07-18-202400_test-migration/up.sql @@ -1,3 +1,8 @@ -- Your SQL goes here -ALTER TABLE IF EXISTS events ALTER COLUMN inserted_at SET DEFAULT NOW(); -ALTER TABLE IF EXISTS processor_status ALTER COLUMN last_updated SET DEFAULT NOW(); \ No newline at end of file +ALTER TABLE IF EXISTS events +ALTER COLUMN inserted_at +SET DEFAULT NOW (); + +ALTER TABLE IF EXISTS processor_status +ALTER COLUMN last_updated +SET DEFAULT NOW (); \ No newline at end of file diff --git a/templates/indexer-template/indexer/src/db_migrations/migrations/2024-08-01-224558_ledger-infos/up.sql b/templates/indexer-template/indexer/src/db_migrations/migrations/2024-08-01-224558_ledger-infos/up.sql index 6f9dafee..ba060928 100644 --- a/templates/indexer-template/indexer/src/db_migrations/migrations/2024-08-01-224558_ledger-infos/up.sql +++ b/templates/indexer-template/indexer/src/db_migrations/migrations/2024-08-01-224558_ledger-infos/up.sql @@ -1,2 +1,3 @@ -- Your SQL goes here -CREATE TABLE ledger_infos (chain_id BIGINT UNIQUE PRIMARY KEY NOT NULL); \ No newline at end of file +CREATE TABLE + ledger_infos (chain_id BIGINT UNIQUE PRIMARY KEY NOT NULL); \ No newline at end of file diff --git a/templates/indexer-template/indexer/src/db_migrations/migrations/2024-08-25-233538_create-message-table/up.sql b/templates/indexer-template/indexer/src/db_migrations/migrations/2024-08-25-233538_create-message-table/up.sql index b6002654..406ad3a1 100644 --- a/templates/indexer-template/indexer/src/db_migrations/migrations/2024-08-25-233538_create-message-table/up.sql +++ b/templates/indexer-template/indexer/src/db_migrations/migrations/2024-08-25-233538_create-message-table/up.sql @@ -1,11 +1,12 @@ -- Your SQL goes here -CREATE TABLE messages ( - message_obj_addr VARCHAR(300) NOT NULL UNIQUE PRIMARY KEY, - creator_addr VARCHAR(300) NOT NULL, - creation_timestamp BIGINT NOT NULL, - last_update_timestamp BIGINT NOT NULL, - -- we store the event index so when we update in batch, - -- we ignore when the event index is less than the last update event index - last_update_event_idx BIGINT NOT NULL, - content TEXT NOT NULL -); \ No newline at end of file +CREATE TABLE + messages ( + message_obj_addr VARCHAR(300) NOT NULL UNIQUE PRIMARY KEY, + creator_addr VARCHAR(300) NOT NULL, + creation_timestamp BIGINT NOT NULL, + last_update_timestamp BIGINT NOT NULL, + -- we store the event index so when we update in batch, + -- we ignore when the event index is less than the last update event index + last_update_event_idx BIGINT NOT NULL, + content TEXT NOT NULL + ); \ No newline at end of file diff --git a/templates/indexer-template/indexer/src/db_migrations/migrations/2024-09-19-234343_create-user-stats-table/down.sql b/templates/indexer-template/indexer/src/db_migrations/migrations/2024-09-19-234343_create-user-stats-table/down.sql new file mode 100644 index 00000000..a38bb6d4 --- /dev/null +++ b/templates/indexer-template/indexer/src/db_migrations/migrations/2024-09-19-234343_create-user-stats-table/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +DROP TABLE user_stats; \ No newline at end of file diff --git a/templates/indexer-template/indexer/src/db_migrations/migrations/2024-09-19-234343_create-user-stats-table/up.sql b/templates/indexer-template/indexer/src/db_migrations/migrations/2024-09-19-234343_create-user-stats-table/up.sql new file mode 100644 index 00000000..dc82a631 --- /dev/null +++ b/templates/indexer-template/indexer/src/db_migrations/migrations/2024-09-19-234343_create-user-stats-table/up.sql @@ -0,0 +1,13 @@ +-- Your SQL goes here +CREATE TABLE + user_stats ( + user_addr VARCHAR(300) NOT NULL UNIQUE PRIMARY KEY, + creation_timestamp BIGINT NOT NULL, + last_update_timestamp BIGINT NOT NULL, + created_messages BIGINT NOT NULL, + updated_messages BIGINT NOT NULL, + -- Season 1 points + s1_points BIGINT NOT NULL, + -- All seasons points + total_points BIGINT NOT NULL + ); \ No newline at end of file diff --git a/templates/indexer-template/indexer/src/db_migrations/schema.rs b/templates/indexer-template/indexer/src/db_migrations/schema.rs index c1d69579..09b35594 100644 --- a/templates/indexer-template/indexer/src/db_migrations/schema.rs +++ b/templates/indexer-template/indexer/src/db_migrations/schema.rs @@ -29,8 +29,22 @@ diesel::table! { } } +diesel::table! { + user_stats (user_addr) { + #[max_length = 300] + user_addr -> Varchar, + creation_timestamp -> Int8, + last_update_timestamp -> Int8, + created_messages -> Int8, + updated_messages -> Int8, + s1_points -> Int8, + total_points -> Int8, + } +} + diesel::allow_tables_to_appear_in_same_query!( ledger_infos, messages, processor_status, + user_stats, ); diff --git a/templates/indexer-template/indexer/src/db_models/events_models.rs b/templates/indexer-template/indexer/src/db_models/events_models.rs deleted file mode 100644 index 37c30535..00000000 --- a/templates/indexer-template/indexer/src/db_models/events_models.rs +++ /dev/null @@ -1,134 +0,0 @@ -use crate::schema::messages; -use aptos_indexer_processor_sdk::{ - aptos_protos::transaction::v1::Event as EventPB, utils::convert::standardize_address, -}; -use diesel::{AsChangeset, Insertable}; -use field_count::FieldCount; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, Deserialize, Serialize)] -/// On-chain representation of a message -pub struct MessageOnChain { - pub creator: String, - pub content: String, - pub creation_timestamp: String, - pub last_update_timestamp: String, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -/// On-chain representation of a message creation event -pub struct CreateMessageEventOnChain { - pub message_obj_addr: String, - pub message: MessageOnChain, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -/// On-chain representation of a message update event -pub struct UpdateMessageEventOnChain { - pub message_obj_addr: String, - pub message: MessageOnChain, -} - -#[derive(AsChangeset, Clone, Debug, Deserialize, FieldCount, Insertable, Serialize)] -#[diesel(table_name = messages)] -/// Database representation of a message -pub struct Message { - pub message_obj_addr: String, - pub creator_addr: String, - pub creation_timestamp: i64, - pub last_update_timestamp: i64, - pub last_update_event_idx: i64, - pub content: String, -} - -#[derive(Debug, Clone)] -pub enum ContractEvent { - CreateMessageEvent(Message), - UpdateMessageEvent(Message), -} - -impl ContractEvent { - pub fn from_event(contract_address: &str, event_idx: usize, event: &EventPB) -> Option { - let t: &str = event.type_str.as_ref(); - let should_include = t.starts_with(contract_address); - - if should_include { - if t.starts_with( - format!("{}::message_board::CreateMessageEvent", contract_address).as_str(), - ) { - println!("CreateMessageEvent {}", event.data.as_str()); - let create_message_event_on_chain: CreateMessageEventOnChain = - serde_json::from_str(event.data.as_str()).expect( - format!( - "Failed to parse CreateMessageEvent, {}", - event.data.as_str() - ) - .as_str(), - ); - let creation_timestamp = create_message_event_on_chain - .message - .creation_timestamp - .parse() - .unwrap(); - let message = Message { - message_obj_addr: standardize_address( - &create_message_event_on_chain.message_obj_addr, - ), - creator_addr: standardize_address( - create_message_event_on_chain.message.creator.as_str(), - ), - creation_timestamp, - content: create_message_event_on_chain.message.content, - last_update_timestamp: creation_timestamp, - last_update_event_idx: 0, - }; - Some(ContractEvent::CreateMessageEvent(message)) - } else if t.starts_with( - format!("{}::message_board::UpdateMessageEvent", contract_address).as_str(), - ) { - println!("UpdateMessageEvent {}", event.data.as_str()); - let update_message_event_on_chain: UpdateMessageEventOnChain = - serde_json::from_str(event.data.as_str()).expect( - format!( - "Failed to parse UpdateMessageEvent, {}", - event.data.as_str() - ) - .as_str(), - ); - let message = Message { - message_obj_addr: standardize_address( - &update_message_event_on_chain.message_obj_addr, - ), - content: update_message_event_on_chain.message.content, - creator_addr: standardize_address( - update_message_event_on_chain.message.creator.as_str(), - ), - creation_timestamp: update_message_event_on_chain - .message - .creation_timestamp - .parse() - .unwrap(), - last_update_timestamp: update_message_event_on_chain - .message - .last_update_timestamp - .parse() - .unwrap(), - last_update_event_idx: event_idx as i64, - }; - Some(ContractEvent::UpdateMessageEvent(message)) - } else { - None - } - } else { - None - } - } - - pub fn from_events(contract_address: &str, events: &[EventPB]) -> Vec { - events - .iter() - .enumerate() - .filter_map(|(idx, event)| Self::from_event(contract_address, idx, event)) - .collect() - } -} diff --git a/templates/indexer-template/indexer/src/db_models/ledger_info.rs b/templates/indexer-template/indexer/src/db_models/ledger_info.rs index ece08a7a..e194d18d 100644 --- a/templates/indexer-template/indexer/src/db_models/ledger_info.rs +++ b/templates/indexer-template/indexer/src/db_models/ledger_info.rs @@ -1,4 +1,3 @@ - use diesel::{Identifiable, Insertable, OptionalExtension, QueryDsl, Queryable}; use diesel_async::RunQueryDsl; diff --git a/templates/indexer-template/indexer/src/db_models/message.rs b/templates/indexer-template/indexer/src/db_models/message.rs new file mode 100644 index 00000000..ff966438 --- /dev/null +++ b/templates/indexer-template/indexer/src/db_models/message.rs @@ -0,0 +1,68 @@ +use aptos_indexer_processor_sdk::utils::convert::standardize_address; +use diesel::{AsChangeset, Insertable}; +use field_count::FieldCount; +use serde::{Deserialize, Serialize}; + +use crate::schema::messages; + +#[derive(AsChangeset, Clone, Debug, Deserialize, FieldCount, Insertable, Serialize)] +#[diesel(table_name = messages)] +/// Database representation of a message +pub struct Message { + pub message_obj_addr: String, + pub creator_addr: String, + pub creation_timestamp: i64, + pub last_update_timestamp: i64, + pub last_update_event_idx: i64, + pub content: String, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +/// On-chain representation of a message +pub struct MessageOnChain { + pub creator: String, + pub content: String, + pub creation_timestamp: String, + pub last_update_timestamp: String, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +/// On-chain representation of a message creation event +pub struct CreateMessageEventOnChain { + pub message_obj_addr: String, + pub message: MessageOnChain, +} + +impl CreateMessageEventOnChain { + pub fn to_db_message(&self) -> Message { + let creation_timestamp = self.message.creation_timestamp.parse().unwrap(); + Message { + message_obj_addr: standardize_address(&self.message_obj_addr), + creator_addr: standardize_address(self.message.creator.as_str()), + creation_timestamp, + content: self.message.content.clone(), + last_update_timestamp: creation_timestamp, + last_update_event_idx: 0, + } + } +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +/// On-chain representation of a message update event +pub struct UpdateMessageEventOnChain { + pub message_obj_addr: String, + pub message: MessageOnChain, +} + +impl UpdateMessageEventOnChain { + pub fn to_db_message(&self, last_update_event_idx: i64) -> Message { + Message { + message_obj_addr: standardize_address(&self.message_obj_addr), + content: self.message.content.clone(), + creator_addr: standardize_address(self.message.creator.as_str()), + creation_timestamp: self.message.creation_timestamp.parse().unwrap(), + last_update_timestamp: self.message.last_update_timestamp.parse().unwrap(), + last_update_event_idx, + } + } +} diff --git a/templates/indexer-template/indexer/src/db_models/mod.rs b/templates/indexer-template/indexer/src/db_models/mod.rs index b5db64fa..eb93c44e 100644 --- a/templates/indexer-template/indexer/src/db_models/mod.rs +++ b/templates/indexer-template/indexer/src/db_models/mod.rs @@ -1,3 +1,4 @@ -pub mod events_models; pub mod ledger_info; +pub mod message; pub mod processor_status; +pub mod user_stat; diff --git a/templates/indexer-template/indexer/src/db_models/processor_status.rs b/templates/indexer-template/indexer/src/db_models/processor_status.rs index fad2e91c..9af15e78 100644 --- a/templates/indexer-template/indexer/src/db_models/processor_status.rs +++ b/templates/indexer-template/indexer/src/db_models/processor_status.rs @@ -1,4 +1,3 @@ - use diesel::{AsChangeset, ExpressionMethods, Insertable, OptionalExtension, QueryDsl, Queryable}; use diesel_async::RunQueryDsl; diff --git a/templates/indexer-template/indexer/src/db_models/user_stat.rs b/templates/indexer-template/indexer/src/db_models/user_stat.rs new file mode 100644 index 00000000..c7305dc1 --- /dev/null +++ b/templates/indexer-template/indexer/src/db_models/user_stat.rs @@ -0,0 +1,18 @@ +use diesel::{AsChangeset, Insertable, Queryable}; +use field_count::FieldCount; +use serde::{Deserialize, Serialize}; + +use crate::schema::user_stats; + +#[derive(AsChangeset, Clone, Debug, Deserialize, FieldCount, Insertable, Serialize, Queryable)] +#[diesel(table_name = user_stats)] +/// Database representation of a user's statistics +pub struct UserStat { + pub user_addr: String, + pub creation_timestamp: i64, + pub last_update_timestamp: i64, + pub created_messages: i64, + pub updated_messages: i64, + pub s1_points: i64, + pub total_points: i64, +} diff --git a/templates/indexer-template/indexer/src/health_check_server.rs b/templates/indexer-template/indexer/src/health_check_server.rs index 84e7eb43..3a4d185f 100644 --- a/templates/indexer-template/indexer/src/health_check_server.rs +++ b/templates/indexer-template/indexer/src/health_check_server.rs @@ -8,7 +8,6 @@ use poem::{ }; use serde::{Deserialize, Serialize}; use std::net::{Ipv4Addr, SocketAddrV4}; -use tracing::info; /// This configures the health server. #[derive(Clone, Debug, Deserialize, Serialize)] @@ -26,7 +25,7 @@ impl Default for HealthServerConfig { } pub async fn run(config: HealthServerConfig) -> Result<()> { - info!("Health server starting at {}", config.listen_address); + tracing::info!("Health server starting at {}", config.listen_address); let cors = Cors::new().allow_methods(vec![Method::GET, Method::POST]); let route = Route::new().nest("/", get(root)).with(cors); Server::new(TcpListener::bind(config.listen_address)) diff --git a/templates/indexer-template/indexer/src/processors/events/events_extractor.rs b/templates/indexer-template/indexer/src/processors/events/events_extractor.rs index 36258253..16c953bd 100644 --- a/templates/indexer-template/indexer/src/processors/events/events_extractor.rs +++ b/templates/indexer-template/indexer/src/processors/events/events_extractor.rs @@ -1,14 +1,14 @@ -use crate::db_models::events_models::ContractEvent; use anyhow::Result; use aptos_indexer_processor_sdk::{ - aptos_protos::transaction::v1::{transaction::TxnData, Transaction}, + aptos_protos::transaction::v1::{transaction::TxnData, Event as EventPB, Transaction}, traits::{async_step::AsyncRunType, AsyncStep, NamedStep, Processable}, types::transaction_context::TransactionContext, utils::errors::ProcessorError, }; use async_trait::async_trait; use rayon::prelude::*; -use tracing::warn; + +use crate::db_models::message::{CreateMessageEventOnChain, Message, UpdateMessageEventOnChain}; /// EventsExtractor is a step that extracts events and their metadata from transactions. pub struct EventsExtractor @@ -24,6 +24,14 @@ impl EventsExtractor { } } +impl AsyncStep for EventsExtractor {} + +impl NamedStep for EventsExtractor { + fn name(&self) -> String { + "EventsExtractor".to_string() + } +} + #[async_trait] impl Processable for EventsExtractor { type Input = Transaction; @@ -43,7 +51,7 @@ impl Processable for EventsExtractor { let txn_data = match txn.txn_data.as_ref() { Some(data) => data, None => { - warn!( + tracing::warn!( transaction_version = txn_version, "Transaction data doesn't exist" ); @@ -76,10 +84,59 @@ impl Processable for EventsExtractor { } } -impl AsyncStep for EventsExtractor {} +#[derive(Debug, Clone)] +pub enum ContractEvent { + CreateMessageEvent(Message), + UpdateMessageEvent(Message), +} -impl NamedStep for EventsExtractor { - fn name(&self) -> String { - "EventsExtractor".to_string() +impl ContractEvent { + fn from_event(contract_address: &str, event_idx: usize, event: &EventPB) -> Option { + let t: &str = event.type_str.as_ref(); + let should_include = t.starts_with(contract_address); + + if should_include { + if t.starts_with( + format!("{}::message_board::CreateMessageEvent", contract_address).as_str(), + ) { + println!("CreateMessageEvent {}", event.data.as_str()); + let create_message_event_on_chain: CreateMessageEventOnChain = + serde_json::from_str(event.data.as_str()).unwrap_or_else(|_| { + panic!( + "Failed to parse CreateMessageEvent, {}", + event.data.as_str() + ) + }); + Some(ContractEvent::CreateMessageEvent( + create_message_event_on_chain.to_db_message(), + )) + } else if t.starts_with( + format!("{}::message_board::UpdateMessageEvent", contract_address).as_str(), + ) { + println!("UpdateMessageEvent {}", event.data.as_str()); + let update_message_event_on_chain: UpdateMessageEventOnChain = + serde_json::from_str(event.data.as_str()).unwrap_or_else(|_| { + panic!( + "Failed to parse UpdateMessageEvent, {}", + event.data.as_str() + ) + }); + Some(ContractEvent::UpdateMessageEvent( + update_message_event_on_chain.to_db_message(event_idx as i64), + )) + } else { + None + } + } else { + None + } + } + + pub fn from_events(contract_address: &str, events: &[EventPB]) -> Vec { + events + .iter() + .enumerate() + .filter_map(|(idx, event)| Self::from_event(contract_address, idx, event)) + .collect() } } diff --git a/templates/indexer-template/indexer/src/processors/events/events_processor.rs b/templates/indexer-template/indexer/src/processors/events/events_processor.rs index e789bbf7..0b148908 100644 --- a/templates/indexer-template/indexer/src/processors/events/events_processor.rs +++ b/templates/indexer-template/indexer/src/processors/events/events_processor.rs @@ -5,7 +5,6 @@ use aptos_indexer_processor_sdk::{ common_steps::TransactionStreamStep, traits::IntoRunnableStep, }; -use tracing::info; use super::{events_extractor::EventsExtractor, events_storer::EventsStorer}; use crate::{ @@ -28,8 +27,7 @@ impl EventsProcessor { &config.db_config.postgres_connection_string, config.db_config.db_pool_size, ) - .await - .expect("Failed to create connection pool"); + .await; Ok(Self { config, @@ -41,7 +39,7 @@ impl EventsProcessor { // Merge the starting version from config and the latest processed version from the DB let starting_version = get_starting_version(&self.config, self.db_pool.clone()).await?; - info!( + tracing::info!( "Starting events processor with starting version: {:?}", starting_version ); @@ -84,13 +82,14 @@ impl EventsProcessor { if txn_context.data.is_empty() { continue; } - info!( + tracing::info!( "Finished processing events from versions [{:?}, {:?}]", - txn_context.start_version, txn_context.end_version, + txn_context.start_version, + txn_context.end_version, ); } Err(_) => { - info!("Channel is closed"); + tracing::error!("Channel is closed"); return Ok(()); } } diff --git a/templates/indexer-template/indexer/src/processors/events/events_storer.rs b/templates/indexer-template/indexer/src/processors/events/events_storer.rs index 7f6bfe8e..6097d4df 100644 --- a/templates/indexer-template/indexer/src/processors/events/events_storer.rs +++ b/templates/indexer-template/indexer/src/processors/events/events_storer.rs @@ -7,11 +7,11 @@ use aptos_indexer_processor_sdk::{ }; use async_trait::async_trait; -use super::storers::{ +use super::{events_extractor::ContractEvent, storers::{ create_message_event_storer::process_create_message_events, update_message_event_storer::process_update_message_events, -}; -use crate::{db_models::events_models::ContractEvent, utils::database_utils::ArcDbPool}; +}}; +use crate::utils::database_utils::ArcDbPool; /// EventsStorer is a step that inserts events in the database. pub struct EventsStorer diff --git a/templates/indexer-template/indexer/src/processors/events/storers/create_message_event_storer.rs b/templates/indexer-template/indexer/src/processors/events/storers/create_message_event_storer.rs index 20b3c7ae..2127214c 100644 --- a/templates/indexer-template/indexer/src/processors/events/storers/create_message_event_storer.rs +++ b/templates/indexer-template/indexer/src/processors/events/storers/create_message_event_storer.rs @@ -1,26 +1,84 @@ use ahash::AHashMap; use anyhow::Result; use aptos_indexer_processor_sdk::utils::errors::ProcessorError; -use diesel::{pg::Pg, query_builder::QueryFragment}; -use tracing::error; +use diesel::{insert_into, upsert::excluded, ExpressionMethods, QueryResult}; +use diesel_async::{AsyncConnection, AsyncPgConnection, RunQueryDsl}; +use std::cmp; use crate::{ - db_models::events_models::Message, - schema::messages, + db_models::{message::Message, user_stat::UserStat}, + schema::{messages, user_stats}, utils::{ - database_execution::execute_in_chunks, + database_connection::get_db_connection, database_utils::{get_config_table_chunk_size, ArcDbPool}, }, }; -fn create_message_events_sql( +const POINT_PER_NEW_MESSAGE: i64 = 2; + +async fn execute_create_message_events_sql( + conn: &mut AsyncPgConnection, items_to_insert: Vec, -) -> Vec + diesel::query_builder::QueryId + Send> { - let query = diesel::insert_into(messages::table) - .values(items_to_insert) - .on_conflict(messages::message_obj_addr) - .do_nothing(); - vec![query] + user_stats_changes: AHashMap, +) -> QueryResult<()> { + conn.transaction(|conn| { + Box::pin(async move { + let create_message_query = insert_into(messages::table) + .values(items_to_insert.clone()) + .on_conflict(messages::message_obj_addr) + .do_nothing(); + create_message_query.execute(conn).await?; + + /* + DO NOT backfill data (i.e. process same event twice), you would mess up the user stat!!!! + Instead, if you want to change the point calculation logic, you should delete all data and re-index from scratch. + You can delete all data by revert all DB migrations, see README.md for more details. + */ + let update_user_stat_query = insert_into(user_stats::table) + .values( + user_stats_changes + .iter() + .map( + |( + user_addr, + ( + new_message_count, + earliest_message_creation_time, + latest_message_creation_time, + ), + )| UserStat { + user_addr: user_addr.clone(), + creation_timestamp: *earliest_message_creation_time, + last_update_timestamp: *latest_message_creation_time, + created_messages: *new_message_count, + updated_messages: 0, + s1_points: new_message_count * POINT_PER_NEW_MESSAGE, + total_points: new_message_count * POINT_PER_NEW_MESSAGE, + }, + ) + .collect::>(), + ) + .on_conflict(user_stats::user_addr) + .do_update() + .set(( + user_stats::user_addr.eq(user_stats::user_addr), + user_stats::creation_timestamp.eq(user_stats::creation_timestamp), + user_stats::last_update_timestamp + .eq(excluded(user_stats::last_update_timestamp)), + user_stats::created_messages + .eq(user_stats::created_messages + excluded(user_stats::created_messages)), + user_stats::updated_messages.eq(user_stats::updated_messages), + user_stats::s1_points + .eq(user_stats::s1_points + excluded(user_stats::s1_points)), + user_stats::total_points + .eq(user_stats::total_points + excluded(user_stats::total_points)), + )); + update_user_stat_query.execute(conn).await?; + + Ok(()) + }) + }) + .await } pub async fn process_create_message_events( @@ -28,21 +86,50 @@ pub async fn process_create_message_events( per_table_chunk_sizes: AHashMap, create_events: Vec, ) -> Result<(), ProcessorError> { - let create_result = execute_in_chunks( - pool.clone(), - create_message_events_sql, - &create_events, - get_config_table_chunk_size::("messages", &per_table_chunk_sizes), - ) - .await; - - match create_result { - Ok(_) => Ok(()), - Err(e) => { - error!("Failed to store create message events: {:?}", e); - Err(ProcessorError::ProcessError { - message: e.to_string(), + // Key is user address + // Value is (number of new messages, earliest create message time, latest create message time) + let mut user_stats_changes: AHashMap = AHashMap::new(); + for message in create_events.clone() { + let (new_count, earliest_time, latest_time) = user_stats_changes + .get(&message.creator_addr) + .cloned() + .unwrap_or((0, i64::MAX, 0)); + user_stats_changes.insert( + message.creator_addr.clone(), + ( + new_count + 1, + cmp::min(earliest_time, message.creation_timestamp), + cmp::max(latest_time, message.creation_timestamp), + ), + ); + } + + let chunk_size = get_config_table_chunk_size::("messages", &per_table_chunk_sizes); + let tasks = create_events + .chunks(chunk_size) + .map(|chunk| { + let pool = pool.clone(); + let items = chunk.to_vec(); + let user_stats_changes = user_stats_changes.clone(); + tokio::spawn(async move { + let conn = &mut get_db_connection(&pool).await.expect( + "Failed to get connection from pool while processing create message events", + ); + execute_create_message_events_sql(conn, items, user_stats_changes).await }) - } + }) + .collect::>(); + + let results = futures_util::future::try_join_all(tasks) + .await + .expect("Task panicked executing in chunks"); + for res in results { + res.map_err(|e| { + tracing::warn!("Error running query: {:?}", e); + ProcessorError::ProcessError { + message: e.to_string(), + } + })?; } + Ok(()) } diff --git a/templates/indexer-template/indexer/src/processors/events/storers/update_message_event_storer.rs b/templates/indexer-template/indexer/src/processors/events/storers/update_message_event_storer.rs index f2719257..48bfc8b7 100644 --- a/templates/indexer-template/indexer/src/processors/events/storers/update_message_event_storer.rs +++ b/templates/indexer-template/indexer/src/processors/events/storers/update_message_event_storer.rs @@ -1,50 +1,105 @@ +use std::cmp; + use ahash::AHashMap; use anyhow::Result; use aptos_indexer_processor_sdk::utils::errors::ProcessorError; use diesel::{ - pg::Pg, query_builder::QueryFragment, query_dsl::methods::FilterDsl, upsert::excluded, - BoolExpressionMethods, ExpressionMethods, + insert_into, query_dsl::methods::FilterDsl, upsert::excluded, BoolExpressionMethods, + ExpressionMethods, QueryResult, }; -use tracing::error; +use diesel_async::{AsyncConnection, AsyncPgConnection, RunQueryDsl}; use crate::{ - db_models::events_models::Message, - schema::messages, + db_models::{message::Message, user_stat::UserStat}, + schema::{messages, user_stats}, utils::{ - database_execution::execute_in_chunks, + database_connection::get_db_connection, database_utils::{get_config_table_chunk_size, ArcDbPool}, }, }; -fn update_message_events_sql( +const POINT_PER_UPDATE_MESSAGE: i64 = 1; + +async fn execute_update_message_events_sql( + conn: &mut AsyncPgConnection, items_to_insert: Vec, -) -> Vec + diesel::query_builder::QueryId + Send> { - let query = diesel::insert_into(messages::table) - .values(items_to_insert) - .on_conflict(messages::message_obj_addr) - .do_update() - .set(( - messages::message_obj_addr.eq(excluded(messages::message_obj_addr)), - messages::creator_addr.eq(excluded(messages::creator_addr)), - messages::creation_timestamp.eq(excluded(messages::creation_timestamp)), - messages::last_update_timestamp.eq(excluded(messages::last_update_timestamp)), - messages::last_update_event_idx.eq(excluded(messages::last_update_event_idx)), - messages::content.eq(excluded(messages::content)), - )) - .filter( - // Update only if the last update timestamp is greater than the existing one - // or if the last update timestamp is the same but the event index is greater - messages::last_update_timestamp - .lt(excluded(messages::last_update_timestamp)) - .or(messages::last_update_timestamp - .eq(excluded(messages::last_update_timestamp)) - .and( - messages::last_update_event_idx - .lt(excluded(messages::last_update_event_idx)), - )), - ); + user_stats_changes: AHashMap, +) -> QueryResult<()> { + conn.transaction(|conn| { + Box::pin(async move { + let update_message_query = insert_into(messages::table) + .values(items_to_insert.clone()) + .on_conflict(messages::message_obj_addr) + .do_update() + .set(( + messages::message_obj_addr.eq(messages::message_obj_addr), + messages::creator_addr.eq(messages::creator_addr), + messages::creation_timestamp.eq(messages::creation_timestamp), + messages::last_update_timestamp.eq(excluded(messages::last_update_timestamp)), + messages::last_update_event_idx.eq(excluded(messages::last_update_event_idx)), + messages::content.eq(excluded(messages::content)), + )) + .filter( + // Update only if the last update timestamp is greater than the existing one + // or if the last update timestamp is the same but the event index is greater + messages::last_update_timestamp + .lt(excluded(messages::last_update_timestamp)) + .or(messages::last_update_timestamp + .eq(excluded(messages::last_update_timestamp)) + .and( + messages::last_update_event_idx + .lt(excluded(messages::last_update_event_idx)), + )), + ); + update_message_query.execute(conn).await?; - vec![query] + /* + DO NOT backfill data (i.e. process same event twice), you would mess up the user stat!!!! + Instead, if you want to change the point calculation logic, you should delete all data and re-index from scratch. + You can delete all data by revert all DB migrations, see README.md for more details. + */ + let update_user_stat_query = insert_into(user_stats::table) + .values( + user_stats_changes + .iter() + .map( + |(user_addr, (update_message_count, latest_message_update_time))| { + UserStat { + user_addr: user_addr.clone(), + // This value doesn't matter because we always use the original DB value for creation_timestamp + creation_timestamp: 0, + last_update_timestamp: *latest_message_update_time, + // This value doesn't matter because we always use the original DB value for created_messages + created_messages: 0, + updated_messages: *update_message_count, + s1_points: update_message_count * POINT_PER_UPDATE_MESSAGE, + total_points: update_message_count * POINT_PER_UPDATE_MESSAGE, + } + }, + ) + .collect::>(), + ) + .on_conflict(user_stats::user_addr) + .do_update() + .set(( + user_stats::user_addr.eq(user_stats::user_addr), + user_stats::creation_timestamp.eq(user_stats::creation_timestamp), + user_stats::last_update_timestamp + .eq(excluded(user_stats::last_update_timestamp)), + user_stats::created_messages.eq(user_stats::created_messages), + user_stats::updated_messages + .eq(user_stats::updated_messages + excluded(user_stats::updated_messages)), + user_stats::s1_points + .eq(user_stats::s1_points + excluded(user_stats::s1_points)), + user_stats::total_points + .eq(user_stats::total_points + excluded(user_stats::total_points)), + )); + update_user_stat_query.execute(conn).await?; + + Ok(()) + }) + }) + .await } pub async fn process_update_message_events( @@ -52,7 +107,24 @@ pub async fn process_update_message_events( per_table_chunk_sizes: AHashMap, update_events: Vec, ) -> Result<(), ProcessorError> { - // filter update_events so when there are 2 events updating the same record, only the latest one is sent to DB for update + // Key is user address + // Value is (number of new messages, latest update message time) + let mut user_stats_changes: AHashMap = AHashMap::new(); + for message in update_events.clone() { + let (update_count, latest_time) = user_stats_changes + .get(&message.creator_addr) + .cloned() + .unwrap_or((0, 0)); + user_stats_changes.insert( + message.creator_addr.clone(), + ( + update_count + 1, + cmp::max(latest_time, message.last_update_timestamp), + ), + ); + } + + // Filter update_events so when there are 2 events updating the same record, only the latest one is sent to DB for update // because we cannot update one record with 2 different values in the same transaction let mut filtered_update_events_map: AHashMap = AHashMap::new(); for message in update_events { @@ -72,21 +144,32 @@ pub async fn process_update_message_events( } let filtered_update_events: Vec = filtered_update_events_map.into_values().collect(); - let update_result = execute_in_chunks( - pool.clone(), - update_message_events_sql, - &filtered_update_events, - get_config_table_chunk_size::("messages", &per_table_chunk_sizes), - ) - .await; + let chunk_size = get_config_table_chunk_size::("messages", &per_table_chunk_sizes); + let tasks = filtered_update_events + .chunks(chunk_size) + .map(|chunk| { + let pool = pool.clone(); + let items = chunk.to_vec(); + let user_stats_changes = user_stats_changes.clone(); + tokio::spawn(async move { + let conn = &mut get_db_connection(&pool).await.expect( + "Failed to get connection from pool while processing update message events", + ); + execute_update_message_events_sql(conn, items, user_stats_changes).await + }) + }) + .collect::>(); - match update_result { - Ok(_) => Ok(()), - Err(e) => { - error!("Failed to store update message events: {:?}", e); - Err(ProcessorError::ProcessError { + let results = futures_util::future::try_join_all(tasks) + .await + .expect("Task panicked executing in chunks"); + for res in results { + res.map_err(|e| { + tracing::error!("Error running query: {:?}", e); + ProcessorError::ProcessError { message: e.to_string(), - }) - } + } + })?; } + Ok(()) } diff --git a/templates/indexer-template/indexer/src/utils/chain_id.rs b/templates/indexer-template/indexer/src/utils/chain_id.rs index 371cad29..0546eb1c 100644 --- a/templates/indexer-template/indexer/src/utils/chain_id.rs +++ b/templates/indexer-template/indexer/src/utils/chain_id.rs @@ -1,18 +1,19 @@ use anyhow::{Context, Result}; -use tracing::info; use super::database_utils::ArcDbPool; use crate::{ - db_models::ledger_info::LedgerInfo, schema::ledger_infos, - utils::database_execution::execute_with_better_error_conn, + db_models::ledger_info::LedgerInfo, + schema::ledger_infos, + utils::{ + database_connection::get_db_connection, database_execution::execute_with_better_error, + }, }; /// Verify the chain id from GRPC against the database. pub async fn check_or_update_chain_id(grpc_chain_id: i64, db_pool: ArcDbPool) -> Result { - info!("Checking if chain id is correct"); + tracing::info!("Checking if chain id is correct"); - let mut conn = db_pool - .get() + let mut conn = get_db_connection(&db_pool) .await .expect("Failed to get connection from pool while checking or updating chain id"); @@ -24,14 +25,14 @@ pub async fn check_or_update_chain_id(grpc_chain_id: i64, db_pool: ArcDbPool) -> match maybe_existing_chain_id { Some(chain_id) => { anyhow::ensure!(chain_id == grpc_chain_id, "Wrong chain detected! Trying to index chain {} now but existing data is for chain {}", grpc_chain_id, chain_id); - info!( + tracing::info!( chain_id = chain_id, "Chain id matches! Continue to index...", ); Ok(chain_id as u64) } None => { - info!( + tracing::info!( chain_id = grpc_chain_id, "Adding chain id to db, continue to index..." ); @@ -40,7 +41,7 @@ pub async fn check_or_update_chain_id(grpc_chain_id: i64, db_pool: ArcDbPool) -> chain_id: grpc_chain_id, }) .on_conflict_do_nothing(); - execute_with_better_error_conn(&mut conn, vec![query]) + execute_with_better_error(&mut conn, vec![query]) .await .context("Error updating chain_id!") .map(|_| grpc_chain_id as u64) diff --git a/templates/indexer-template/indexer/src/utils/database_connection.rs b/templates/indexer-template/indexer/src/utils/database_connection.rs index ab983d14..084f06ff 100644 --- a/templates/indexer-template/indexer/src/utils/database_connection.rs +++ b/templates/indexer-template/indexer/src/utils/database_connection.rs @@ -1,12 +1,13 @@ +use aptos_indexer_processor_sdk::utils::errors::ProcessorError; use diesel::ConnectionResult; use diesel_async::{ - pooled_connection::{bb8::Pool, AsyncDieselConnectionManager, ManagerConfig, PoolError}, + pooled_connection::{bb8::Pool, AsyncDieselConnectionManager, ManagerConfig}, AsyncPgConnection, }; use futures_util::{future::BoxFuture, FutureExt}; use std::sync::Arc; -use super::database_utils::{ArcDbPool, MyDbConnection}; +use super::database_utils::{ArcDbPool, DbPoolConnection}; fn establish_connection(database_url: &str) -> BoxFuture> { use native_tls::{Certificate, TlsConnector}; @@ -63,15 +64,26 @@ fn parse_and_clean_db_url(url: &str) -> (String, Option) { (db_url.to_string(), cert_path) } -pub async fn new_db_pool(database_url: &str, max_pool_size: u32) -> Result { - let mut config = ManagerConfig::::default(); +pub async fn new_db_pool(database_url: &str, max_pool_size: u32) -> ArcDbPool { + let mut config = ManagerConfig::::default(); config.custom_setup = Box::new(|conn| Box::pin(establish_connection(conn))); let manager = - AsyncDieselConnectionManager::::new_with_config(database_url, config); + AsyncDieselConnectionManager::::new_with_config(database_url, config); let pool = Pool::builder() .max_size(max_pool_size) .build(manager) - .await?; - Ok(Arc::new(pool)) + .await + .expect("Failed to create db pool"); + + Arc::new(pool) +} + +pub async fn get_db_connection(pool: &ArcDbPool) -> Result { + pool.get().await.map_err(|e| { + tracing::error!("Error getting connection from DB pool: {:?}", e); + ProcessorError::DBStoreError { + message: format!("Failed to get connection from pool: {}", e), + } + }) } diff --git a/templates/indexer-template/indexer/src/utils/database_execution.rs b/templates/indexer-template/indexer/src/utils/database_execution.rs index 09c1ecbd..77ce55d5 100644 --- a/templates/indexer-template/indexer/src/utils/database_execution.rs +++ b/templates/indexer-template/indexer/src/utils/database_execution.rs @@ -1,94 +1,22 @@ -use diesel::{query_builder::QueryFragment, QueryResult}; -use diesel_async::{AsyncConnection, RunQueryDsl}; -use tracing::{debug, warn}; - -use super::database_utils::{clean_data_for_db, ArcDbPool, Backend, MyDbConnection}; - -pub async fn execute_in_chunks( - pool: ArcDbPool, - build_queries: fn(Vec) -> Vec, - items_to_insert: &[T], - chunk_size: usize, -) -> Result<(), diesel::result::Error> -where - U: QueryFragment + diesel::query_builder::QueryId + Send + 'static, - T: serde::Serialize + for<'de> serde::Deserialize<'de> + Clone + Send + 'static, -{ - let tasks = items_to_insert - .chunks(chunk_size) - .map(|chunk| { - let pool = pool.clone(); - let items = chunk.to_vec(); - tokio::spawn(async move { - let queries = build_queries(items.clone()); - execute_or_retry_cleaned(pool, build_queries, items, queries).await - }) - }) - .collect::>(); - - let results = futures_util::future::try_join_all(tasks) - .await - .expect("Task panicked executing in chunks"); - for res in results { - res? - } - - Ok(()) -} - -pub async fn execute_or_retry_cleaned( - pool: ArcDbPool, - build_queries: fn(Vec) -> Vec, - items: Vec, - queries: Vec, -) -> Result<(), diesel::result::Error> -where - U: QueryFragment + diesel::query_builder::QueryId + Send, - T: serde::Serialize + for<'de> serde::Deserialize<'de> + Clone, -{ - match execute_with_better_error(pool.clone(), queries).await { - Ok(_) => {} - Err(_) => { - let cleaned_items = clean_data_for_db(items, true); - let cleaned_queries = build_queries(cleaned_items); - match execute_with_better_error(pool.clone(), cleaned_queries).await { - Ok(_) => {} - Err(e) => { - return Err(e); - } - } - } - } - Ok(()) -} - -pub async fn execute_with_better_error(pool: ArcDbPool, queries: Vec) -> QueryResult<()> -where - U: QueryFragment + diesel::query_builder::QueryId + Send, -{ - let conn = &mut pool.get().await.map_err(|e| { - warn!("Error getting connection from pool: {:?}", e); - diesel::result::Error::DatabaseError( - diesel::result::DatabaseErrorKind::UnableToSendCommand, - Box::new(e.to_string()), - ) - })?; - - execute_with_better_error_conn(conn, queries).await -} - -pub async fn execute_with_better_error_conn( - conn: &mut MyDbConnection, +use diesel::{ + debug_query, + pg::Pg, + query_builder::{QueryFragment, QueryId}, + QueryResult, +}; +use diesel_async::{AsyncConnection, AsyncPgConnection, RunQueryDsl}; + +pub async fn execute_with_better_error( + conn: &mut AsyncPgConnection, queries: Vec, ) -> QueryResult<()> where - U: QueryFragment + diesel::query_builder::QueryId + Send, + U: QueryFragment + QueryId + Send, { let debug_query = queries .iter() - .map(|q| diesel::debug_query::(q).to_string()) + .map(|q| debug_query::(q).to_string()) .collect::>(); - debug!("Executing queries in one DB transaction atomically: {:?}", debug_query); let res = conn .transaction(|conn| { Box::pin(async move { @@ -100,7 +28,7 @@ where }) .await; if let Err(ref e) = res { - warn!("Error running query: {:?}\n{:?}", e, debug_query); + tracing::error!("Error running query: {:?}\n{:?}", e, debug_query); } res } diff --git a/templates/indexer-template/indexer/src/utils/database_utils.rs b/templates/indexer-template/indexer/src/utils/database_utils.rs index d188207a..45f48e73 100644 --- a/templates/indexer-template/indexer/src/utils/database_utils.rs +++ b/templates/indexer-template/indexer/src/utils/database_utils.rs @@ -1,16 +1,13 @@ use ahash::AHashMap; -use aptos_indexer_processor_sdk::utils::convert::remove_null_bytes; use diesel_async::{ pooled_connection::bb8::{Pool, PooledConnection}, AsyncPgConnection, }; use std::sync::Arc; -pub type Backend = diesel::pg::Pg; -pub type MyDbConnection = AsyncPgConnection; -pub type DbPool = Pool; +pub type DbPool = Pool; pub type ArcDbPool = Arc; -pub type DbPoolConnection<'a> = PooledConnection<'a, MyDbConnection>; +pub type DbPoolConnection<'a> = PooledConnection<'a, AsyncPgConnection>; // the max is actually u16::MAX but we see that when the size is too big we get an overflow error so reducing it a bit const MAX_DIESEL_PARAM_SIZE: usize = (u16::MAX / 2) as usize; @@ -27,16 +24,3 @@ pub fn get_config_table_chunk_size( .copied() .unwrap_or_else(|| MAX_DIESEL_PARAM_SIZE / T::field_count()) } - -/// This function will clean the data for postgres. Currently it has support for removing -/// null bytes from strings but in the future we will add more functionality. -pub fn clean_data_for_db serde::Deserialize<'de>>( - items: Vec, - should_remove_null_bytes: bool, -) -> Vec { - if should_remove_null_bytes { - items.iter().map(remove_null_bytes).collect() - } else { - items - } -} diff --git a/templates/indexer-template/indexer/src/utils/latest_processed_version_tracker.rs b/templates/indexer-template/indexer/src/utils/latest_processed_version_tracker.rs index 32105603..0ceed50b 100644 --- a/templates/indexer-template/indexer/src/utils/latest_processed_version_tracker.rs +++ b/templates/indexer-template/indexer/src/utils/latest_processed_version_tracker.rs @@ -1,5 +1,5 @@ use ahash::AHashMap; -use anyhow::{Context, Result}; +use anyhow::Result; use aptos_indexer_processor_sdk::{ traits::{NamedStep, PollableAsyncRunType, PollableAsyncStep, Processable}, types::transaction_context::TransactionContext, @@ -9,7 +9,8 @@ use async_trait::async_trait; use diesel::{query_dsl::methods::FilterDsl, upsert::excluded, ExpressionMethods}; use super::{ - database_connection::new_db_pool, database_execution::execute_with_better_error, + database_connection::{get_db_connection, new_db_pool}, + database_execution::execute_with_better_error, database_utils::ArcDbPool, }; use crate::{ @@ -48,8 +49,7 @@ where &db_config.postgres_connection_string, db_config.db_pool_size, ) - .await - .context("Failed to create connection pool")?; + .await; Ok(Self { pool, tracker_name, @@ -98,7 +98,8 @@ where processor_status::last_success_version .lt(excluded(processor_status::last_success_version)), ); - execute_with_better_error(self.pool.clone(), vec![query]) + let conn = &mut get_db_connection(&self.pool).await?; + execute_with_better_error(conn, vec![query]) .await .map_err(|e| ProcessorError::DBStoreError { message: format!("Failed to update processor status: {}", e), @@ -122,8 +123,8 @@ where &mut self, current_batch: TransactionContext, ) -> Result>, ProcessorError> { - // If there's a gap in the next_version and current_version, save the current_version to seen_versions for - // later processing. + // If there's a gap in the next_version and current_version + // save the current_version to seen_versions for later processing. if self.next_version != current_batch.start_version { tracing::debug!( next_version = self.next_version, diff --git a/templates/indexer-template/next-app/.example.env b/templates/indexer-template/next-app/.example.env deleted file mode 100644 index 280cabbb..00000000 --- a/templates/indexer-template/next-app/.example.env +++ /dev/null @@ -1 +0,0 @@ -POSTGRES_URL="" diff --git a/templates/indexer-template/next-app/.gitignore b/templates/indexer-template/next-app/.gitignore deleted file mode 100644 index 00bba9bb..00000000 --- a/templates/indexer-template/next-app/.gitignore +++ /dev/null @@ -1,37 +0,0 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules -/.pnp -.pnp.js -.yarn/install-state.gz - -# testing -/coverage - -# next.js -/.next/ -/out/ - -# production -/build - -# misc -.DS_Store -*.pem - -# debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# local env files -.env*.local -.env - -# vercel -.vercel - -# typescript -*.tsbuildinfo -next-env.d.ts diff --git a/templates/indexer-template/next-app/README.md b/templates/indexer-template/next-app/README.md deleted file mode 100644 index 06e951b8..00000000 --- a/templates/indexer-template/next-app/README.md +++ /dev/null @@ -1,67 +0,0 @@ -# Aptos full stack template UI - -This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). - -## Local development - -This template uses `@vercel/postgres` to connect to a Postgres database. When testing locally, it cannot connect to local DB. You have to either use a cloud DB on Vercel or a local DB in Docker. I highly recommend using an independent cloud DB on Vercel. You can learn more on [Vercel docs](https://vercel.com/docs/storage/vercel-postgres/local-development). - -## Create a read only user in DB - -This frontend should only read from the DB, the indexer is the only one that writes to the DB. So, create a read only user in the DB for frontend to use for safety. - -```sql --- Create a readonly user --- Please don't use any special characters in the password to avoid @vercel/postgres give invalid connection string error -CREATE USER readonly WITH PASSWORD 'strong_password' --- Grant readonly user read access to all tables in public schema -GRANT SELECT ON ALL TABLES IN SCHEMA public TO readonly; --- Grant readonly user read access to all future tables in public schema -ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO readonly; -``` - -Some useful SQLs to check the user and schema: - -```sql --- Get all users -SELECT * FROM pg_user; --- Get all schemas -SELECT schema_name FROM information_schema.schemata; -``` - -Then fill the `POSTGRES_URL` in `.env` file with the readonly user. - -## Getting Started - -First, run the development server: - -```bash -npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev -``` - -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. - -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. - -This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. - -## Learn More - -To learn more about Next.js, take a look at the following resources: - -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. - -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! - -## Deploy on Vercel - -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. - -Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. diff --git a/templates/indexer-template/next-app/src/components/Message.tsx b/templates/indexer-template/next-app/src/components/Message.tsx deleted file mode 100644 index a1473b86..00000000 --- a/templates/indexer-template/next-app/src/components/Message.tsx +++ /dev/null @@ -1,96 +0,0 @@ -"use client"; - -import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"; -import { LabelValueGrid } from "@/components/LabelValueGrid"; -import { NETWORK } from "@/lib/aptos"; -import { useEffect, useState } from "react"; -import { MessageOnUi } from "@/lib/type/message"; -import { getMessageOnServer } from "@/app/actions"; - -interface MessageProps { - messageObjAddr: `0x${string}`; -} - -export function Message({ messageObjAddr }: MessageProps) { - const [message, setMessage] = useState(); - - useEffect(() => { - getMessageOnServer({ messageObjAddr }).then(({ message }) => { - setMessage(message); - }); - }, [messageObjAddr]); - - if (!message) { - return <>Loading...; - } - - return ( - - - Message - - -
- - - {message.message_obj_addr} - -

- ), - }, - { - label: "Creator address", - value: ( -

- - {message.creator_addr} - -

- ), - }, - { - label: "Creation timestamp", - value: ( -

- {new Date( - message.creation_timestamp * 1000 - ).toLocaleString()} -

- ), - }, - { - label: "Last update timestamp", - value: ( -

- {new Date( - message.last_update_timestamp * 1000 - ).toLocaleString()} -

- ), - }, - { - label: "Content", - value:

{message.content}

, - }, - ]} - /> -
-
-
- ); -} diff --git a/templates/indexer-template/next-app/src/components/RootHeader.tsx b/templates/indexer-template/next-app/src/components/RootHeader.tsx deleted file mode 100644 index 68f825f0..00000000 --- a/templates/indexer-template/next-app/src/components/RootHeader.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { ThemeToggle } from "@/components/ThemeToggle"; -import { WalletSelector } from "@/components/wallet/WalletSelector"; -import { IndexerStatus } from "@/components/IndexerStatus"; - -export const RootHeader = () => { - return ( - - ); -}; diff --git a/templates/indexer-template/next-app/src/components/SendTransaction.tsx b/templates/indexer-template/next-app/src/components/SendTransaction.tsx deleted file mode 100644 index d7032b8b..00000000 --- a/templates/indexer-template/next-app/src/components/SendTransaction.tsx +++ /dev/null @@ -1,10 +0,0 @@ -"use client"; - -import { PostMessageWithSurf } from "@/components/PostMessageWithSurf"; -import { useWallet } from "@aptos-labs/wallet-adapter-react"; - -export const SendTransaction = () => { - const { connected } = useWallet(); - - return connected && ; -}; diff --git a/templates/indexer-template/next-app/src/db/getLastSuccessVersion.ts b/templates/indexer-template/next-app/src/db/getLastSuccessVersion.ts deleted file mode 100644 index 49bc44a6..00000000 --- a/templates/indexer-template/next-app/src/db/getLastSuccessVersion.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { sql } from "@vercel/postgres"; - -export const getLastSuccessVersion = async (): Promise => { - const query = `SELECT last_success_version FROM processor_status`; - const { rows } = await sql.query(query, []); - if (rows.length === 0) { - throw new Error("Status not found"); - } - const status: { - last_success_version: number; - } = rows[0]; - - return status.last_success_version; -}; diff --git a/templates/indexer-template/next-app/src/db/getMessages.ts b/templates/indexer-template/next-app/src/db/getMessages.ts deleted file mode 100644 index c0feb9ea..00000000 --- a/templates/indexer-template/next-app/src/db/getMessages.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { sql } from "@vercel/postgres"; - -import { MessageBoardColumns } from "@/lib/type/message"; - -export type GetMessagesProps = { - page: number; - limit: number; - sortedBy: "creation_timestamp"; - order: "ASC" | "DESC"; -}; - -export const getMessages = async ({ - page, - limit, - sortedBy, - order, -}: GetMessagesProps): Promise<{ - messages: MessageBoardColumns[]; - totalMessages: number; -}> => { - // vercel doesn't allow $1 $2 in the query string, so we do it like this - // we checked the type above to prevent sql injection - const query = `SELECT message_obj_addr, creation_timestamp FROM messages ORDER BY ${sortedBy} ${order} LIMIT $1 OFFSET $2`; - const { rows } = await sql.query(query, [limit, (page - 1) * limit]); - const messages = rows.map((row) => { - return { - message_obj_addr: row.message_obj_addr, - creation_timestamp: row.creation_timestamp, - }; - }); - const { rows: count } = await sql` - SELECT COUNT(*) FROM messages; - `; - return { messages, totalMessages: count[0].count }; -}; diff --git a/templates/indexer-template/next-app/src/lib/type/message.ts b/templates/indexer-template/next-app/src/lib/type/message.ts deleted file mode 100644 index 355225d4..00000000 --- a/templates/indexer-template/next-app/src/lib/type/message.ts +++ /dev/null @@ -1,21 +0,0 @@ -export type MessageInDb = { - message_obj_addr: string; - creator_addr: string; - creation_timestamp: number; - last_update_timestamp: number; - last_update_event_idx: number; - content: string; -}; - -export type MessageOnUi = { - message_obj_addr: `0x${string}`; - creator_addr: `0x${string}`; - creation_timestamp: number; - last_update_timestamp: number; - content: string; -}; - -export type MessageBoardColumns = { - message_obj_addr: `0x${string}`; - creation_timestamp: number; -}; diff --git a/templates/indexer-template/next-app/src/lib/utils.ts b/templates/indexer-template/next-app/src/lib/utils.ts deleted file mode 100644 index bd0c391d..00000000 --- a/templates/indexer-template/next-app/src/lib/utils.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { clsx, type ClassValue } from "clsx" -import { twMerge } from "tailwind-merge" - -export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)) -} diff --git a/templates/indexer-template/next-app/next.config.mjs b/templates/indexer-template/next.config.mjs similarity index 100% rename from templates/indexer-template/next-app/next.config.mjs rename to templates/indexer-template/next.config.mjs diff --git a/templates/indexer-template/next-app/package.json b/templates/indexer-template/package.json similarity index 53% rename from templates/indexer-template/next-app/package.json rename to templates/indexer-template/package.json index 9011684d..e61bbcfe 100644 --- a/templates/indexer-template/next-app/package.json +++ b/templates/indexer-template/package.json @@ -1,17 +1,26 @@ { - "name": "next-app", - "version": "0.1.0", + "name": "indexer-template", "private": true, + "version": "0.0.0", + "license": "Apache-2.0", "scripts": { + "move:test": "node ./scripts/move/test", + "move:compile": "node ./scripts/move/compile", + "move:publish": "node ./scripts/move/publish", + "move:upgrade": "node ./scripts/move/upgrade", "dev": "next dev", "build": "next build", "start": "next start", - "lint": "next lint" + "deploy": "vercel", + "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "_fmt": "prettier 'src/**/*.ts'", + "fmt": "npm run _fmt -- --write" }, "dependencies": { - "@aptos-labs/ts-sdk": "^1.22.2", + "@aptos-labs/ts-sdk": "^1.28.0", "@aptos-labs/wallet-adapter-react": "^3.5.9", "@hookform/resolvers": "^3.7.0", + "@neondatabase/serverless": "^0.9.5", "@radix-ui/react-checkbox": "^1.1.1", "@radix-ui/react-collapsible": "^1.0.3", "@radix-ui/react-dialog": "^1.0.5", @@ -27,7 +36,6 @@ "@tanstack/react-query": "^5.56.2", "@tanstack/react-table": "^8.19.2", "@thalalabs/surf": "1.7.3", - "@vercel/postgres": "^0.9.0", "antd": "^5.1.2", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", @@ -42,13 +50,21 @@ "zod": "^3.23.8" }, "devDependencies": { - "@types/node": "^20", - "@types/react": "^18", - "@types/react-dom": "^18", - "eslint": "^8", - "eslint-config-next": "14.2.5", - "postcss": "^8", - "tailwindcss": "^3.4.1", - "typescript": "^5" + "@types/node": "^20.14.1", + "@types/react": "^18.2.66", + "@types/react-dom": "^18.2.22", + "@typescript-eslint/eslint-plugin": "^7.2.0", + "@typescript-eslint/parser": "^7.2.0", + "autoprefixer": "^10.4.19", + "eslint": "^8.57.0", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.4.6", + "postcss": "^8.4.38", + "prettier": "^3.3.2", + "tailwindcss": "^3.4.3", + "typescript": "^5.2.2", + "tree-kill": "1.2.2", + "dotenv": "16.3.1", + "vercel": "^35.2.4" } } diff --git a/templates/indexer-template/next-app/postcss.config.mjs b/templates/indexer-template/postcss.config.mjs similarity index 100% rename from templates/indexer-template/next-app/postcss.config.mjs rename to templates/indexer-template/postcss.config.mjs diff --git a/templates/indexer-template/next-app/public/next.svg b/templates/indexer-template/public/next.svg similarity index 100% rename from templates/indexer-template/next-app/public/next.svg rename to templates/indexer-template/public/next.svg diff --git a/templates/indexer-template/next-app/public/vercel.svg b/templates/indexer-template/public/vercel.svg similarity index 100% rename from templates/indexer-template/next-app/public/vercel.svg rename to templates/indexer-template/public/vercel.svg diff --git a/templates/indexer-template/scripts/move/compile.js b/templates/indexer-template/scripts/move/compile.js new file mode 100644 index 00000000..05b5e661 --- /dev/null +++ b/templates/indexer-template/scripts/move/compile.js @@ -0,0 +1,15 @@ +require("dotenv").config(); +const cli = require("@aptos-labs/ts-sdk/dist/common/cli/index.js"); + +async function compile() { + const move = new cli.Move(); + + await move.compile({ + packageDirectoryPath: "contract", + namedAddresses: { + // Compile module with account address + message_board_addr: process.env.NEXT_MODULE_PUBLISHER_ACCOUNT_ADDRESS, + }, + }); +} +compile(); diff --git a/templates/indexer-template/scripts/move/publish.js b/templates/indexer-template/scripts/move/publish.js new file mode 100644 index 00000000..3e8304c5 --- /dev/null +++ b/templates/indexer-template/scripts/move/publish.js @@ -0,0 +1,45 @@ +require("dotenv").config(); +const fs = require("node:fs"); +const cli = require("@aptos-labs/ts-sdk/dist/common/cli/index.js"); +const aptosSDK = require("@aptos-labs/ts-sdk") + +async function publish() { + const move = new cli.Move(); + + move + .createObjectAndPublishPackage({ + packageDirectoryPath: "contract", + addressName: "message_board_addr", + namedAddresses: { + // Publish module to new object, but since we create the object on the fly, we fill in the publisher's account address here + message_board_addr: process.env.NEXT_MODULE_PUBLISHER_ACCOUNT_ADDRESS, + }, + extraArguments: [`--private-key=${process.env.NEXT_MODULE_PUBLISHER_ACCOUNT_PRIVATE_KEY}`,`--url=${aptosSDK.NetworkToNodeAPI[process.env.NEXT_PUBLIC_APP_NETWORK]}`], + }) + .then((response) => { + const filePath = ".env"; + let envContent = ""; + + // Check .env file exists and read it + if (fs.existsSync(filePath)) { + envContent = fs.readFileSync(filePath, "utf8"); + } + + // Regular expression to match the NEXT_PUBLIC_MODULE_ADDRESS variable + const regex = /^NEXT_PUBLIC_MODULE_ADDRESS=.*$/m; + const newEntry = `NEXT_PUBLIC_MODULE_ADDRESS=${response.objectAddress}`; + + // Check if NEXT_PUBLIC_MODULE_ADDRESS is already defined + if (envContent.match(regex)) { + // If the variable exists, replace it with the new value + envContent = envContent.replace(regex, newEntry); + } else { + // If the variable does not exist, append it + envContent += `\n${newEntry}`; + } + + // Write the updated content back to the .env file + fs.writeFileSync(filePath, envContent, "utf8"); + }); +} +publish(); diff --git a/templates/indexer-template/scripts/move/test.js b/templates/indexer-template/scripts/move/test.js new file mode 100644 index 00000000..2beb7704 --- /dev/null +++ b/templates/indexer-template/scripts/move/test.js @@ -0,0 +1,15 @@ +require("dotenv").config(); + +const cli = require("@aptos-labs/ts-sdk/dist/common/cli/index.js"); + +async function test() { + const move = new cli.Move(); + + await move.test({ + packageDirectoryPath: "contract", + namedAddresses: { + message_board_addr: "0x100", + }, + }); +} +test(); diff --git a/templates/indexer-template/scripts/move/upgrade.js b/templates/indexer-template/scripts/move/upgrade.js new file mode 100644 index 00000000..d20c804c --- /dev/null +++ b/templates/indexer-template/scripts/move/upgrade.js @@ -0,0 +1,24 @@ +require("dotenv").config(); +const cli = require("@aptos-labs/ts-sdk/dist/common/cli/index.js"); +const aptosSDK = require("@aptos-labs/ts-sdk") + +async function publish() { + if (!process.env.NEXT_PUBLIC_MODULE_ADDRESS) { + throw new Error( + "NEXT_PUBLIC_MODULE_ADDRESS variable is not set, make sure you have published the module before upgrading it" + ); + } + + const move = new cli.Move(); + + move.upgradeObjectPackage({ + packageDirectoryPath: "contract", + objectAddress: process.env.NEXT_PUBLIC_MODULE_ADDRESS, + namedAddresses: { + // Upgrade module from an object + message_board_addr: process.env.NEXT_PUBLIC_MODULE_ADDRESS, + }, + extraArguments: [`--private-key=${process.env.NEXT_MODULE_PUBLISHER_ACCOUNT_PRIVATE_KEY}`,`--url=${aptosSDK.NetworkToNodeAPI[process.env.NEXT_PUBLIC_APP_NETWORK]}`], + }); +} +publish(); diff --git a/templates/indexer-template/next-app/src/app/actions.ts b/templates/indexer-template/src/app/actions.ts similarity index 58% rename from templates/indexer-template/next-app/src/app/actions.ts rename to templates/indexer-template/src/app/actions.ts index a55fdd0d..bd1912c0 100644 --- a/templates/indexer-template/next-app/src/app/actions.ts +++ b/templates/indexer-template/src/app/actions.ts @@ -3,7 +3,9 @@ import { getLastSuccessVersion } from "@/db/getLastSuccessVersion"; import { GetMessageProps, getMessage } from "@/db/getMessage"; import { GetMessagesProps, getMessages } from "@/db/getMessages"; -import { MessageBoardColumns, MessageOnUi } from "@/lib/type/message"; +import { getUserStats, GetUserStatsProps } from "@/db/getUserStats"; +import { Message } from "@/lib/type/message"; +import { UserStat } from "@/lib/type/user_stats"; export const getMessagesOnServer = async ({ page, @@ -11,8 +13,8 @@ export const getMessagesOnServer = async ({ sortedBy, order, }: GetMessagesProps): Promise<{ - messages: MessageBoardColumns[]; - totalMessages: number; + messages: Message[]; + total: number; }> => { return getMessages({ page, limit, sortedBy, order }); }; @@ -20,7 +22,7 @@ export const getMessagesOnServer = async ({ export const getMessageOnServer = async ({ messageObjAddr, }: GetMessageProps): Promise<{ - message: MessageOnUi; + message: Message; }> => { return getMessage({ messageObjAddr }); }; @@ -28,3 +30,15 @@ export const getMessageOnServer = async ({ export const getLastVersionOnServer = async (): Promise => { return getLastSuccessVersion(); }; + +export const getUserStatsOnServer = async ({ + page, + limit, + sortedBy, + order, +}: GetUserStatsProps): Promise<{ + userStats: UserStat[]; + total: number; +}> => { + return getUserStats({ page, limit, sortedBy, order }); +}; diff --git a/templates/indexer-template/src/app/analytics/page.tsx b/templates/indexer-template/src/app/analytics/page.tsx new file mode 100644 index 00000000..ce225131 --- /dev/null +++ b/templates/indexer-template/src/app/analytics/page.tsx @@ -0,0 +1,9 @@ +import { Analytics } from "@/components/Analytics"; + +export default function AnalyticsPage() { + return ( +
+ +
+ ); +} diff --git a/templates/indexer-template/next-app/src/app/favicon.ico b/templates/indexer-template/src/app/favicon.ico similarity index 100% rename from templates/indexer-template/next-app/src/app/favicon.ico rename to templates/indexer-template/src/app/favicon.ico diff --git a/templates/indexer-template/next-app/src/app/globals.css b/templates/indexer-template/src/app/globals.css similarity index 100% rename from templates/indexer-template/next-app/src/app/globals.css rename to templates/indexer-template/src/app/globals.css diff --git a/templates/indexer-template/next-app/src/app/layout.tsx b/templates/indexer-template/src/app/layout.tsx similarity index 90% rename from templates/indexer-template/next-app/src/app/layout.tsx rename to templates/indexer-template/src/app/layout.tsx index 692dbee5..a9940cab 100644 --- a/templates/indexer-template/next-app/src/app/layout.tsx +++ b/templates/indexer-template/src/app/layout.tsx @@ -10,6 +10,7 @@ import { PropsWithChildren } from "react"; import { RootHeader } from "@/components/RootHeader"; import { WrongNetworkAlert } from "@/components/WrongNetworkAlert"; import { QueryProvider } from "@/components/providers/QueryProvider"; +import { RootFooter } from "@/components/RootFooter"; const fontSans = FontSans({ subsets: ["latin"], @@ -17,9 +18,8 @@ const fontSans = FontSans({ }); export const metadata: Metadata = { - title: "Aptos Wallet Adapter Example", - description: - "An example of how to use Aptos Wallet Adapter with React and Next.js.", + title: "Aptos Full Stack Demo", + description: "An demo of a full stack app on Aptos", }; const RootLayout = ({ children }: PropsWithChildren) => { @@ -44,6 +44,7 @@ const RootLayout = ({ children }: PropsWithChildren) => { {children} + diff --git a/templates/indexer-template/next-app/src/app/message/[messageObjAddr]/page.tsx b/templates/indexer-template/src/app/message/[messageObjAddr]/page.tsx similarity index 100% rename from templates/indexer-template/next-app/src/app/message/[messageObjAddr]/page.tsx rename to templates/indexer-template/src/app/message/[messageObjAddr]/page.tsx diff --git a/templates/indexer-template/src/app/message/layout.tsx b/templates/indexer-template/src/app/message/layout.tsx new file mode 100644 index 00000000..d9950b87 --- /dev/null +++ b/templates/indexer-template/src/app/message/layout.tsx @@ -0,0 +1,13 @@ +import type { Metadata } from "next"; +import { PropsWithChildren } from "react"; + +export const metadata: Metadata = { + title: "Message Detail", + description: "Message Detail Page", +}; + +const RootLayout = ({ children }: PropsWithChildren) => { + return
{children}
; +}; + +export default RootLayout; diff --git a/templates/indexer-template/next-app/src/app/page.tsx b/templates/indexer-template/src/app/page.tsx similarity index 51% rename from templates/indexer-template/next-app/src/app/page.tsx rename to templates/indexer-template/src/app/page.tsx index 15a3a34e..3401d06f 100644 --- a/templates/indexer-template/next-app/src/app/page.tsx +++ b/templates/indexer-template/src/app/page.tsx @@ -1,11 +1,11 @@ import { MessageBoard } from "@/components/MessageBoard"; -import { SendTransaction } from "@/components/SendTransaction"; +import { CreateMessage } from "@/components/CreateMessage"; export default function HomePage() { return ( - <> +
- - + +
); } diff --git a/templates/indexer-template/src/components/Analytics.tsx b/templates/indexer-template/src/components/Analytics.tsx new file mode 100644 index 00000000..862db804 --- /dev/null +++ b/templates/indexer-template/src/components/Analytics.tsx @@ -0,0 +1,16 @@ +import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"; +import { DataTable } from "@/components/analytics-board/data-table"; +import { columns } from "@/components/analytics-board/columns"; + +export const Analytics = async () => { + return ( + + + Analytics + + + + + + ); +}; diff --git a/templates/indexer-template/next-app/src/components/PostMessageWithSurf.tsx b/templates/indexer-template/src/components/CreateMessage.tsx similarity index 96% rename from templates/indexer-template/next-app/src/components/PostMessageWithSurf.tsx rename to templates/indexer-template/src/components/CreateMessage.tsx index 0dc77ccd..7fed9511 100644 --- a/templates/indexer-template/next-app/src/components/PostMessageWithSurf.tsx +++ b/templates/indexer-template/src/components/CreateMessage.tsx @@ -28,7 +28,7 @@ const FormSchema = z.object({ stringContent: z.string(), }); -export function PostMessageWithSurf() { +export function CreateMessage() { const { toast } = useToast(); const { connected, account } = useWallet(); const { client: walletClient } = useWalletClient(); @@ -90,7 +90,7 @@ export function PostMessageWithSurf() {
{ }); if (isLoading || !data) { - return
Loading last indexer version
; + return
Loading indexer status
; } if (isError) { @@ -53,7 +53,7 @@ export const IndexerStatus = () => {
- {isHealthy ? "Indexer up to date" : "Indexer lagging"} + {isHealthy ? "Indexer up to date" : "Indexer syncing"}
{ + return await getMessageOnServer({ + messageObjAddr, + }); + }; + + const { data, isLoading, isError, error } = useQuery({ + queryKey: [messageObjAddr], + queryFn: fetchData, + }); + + if (isLoading) { + return
Loading...
; + } + + if (isError) { + return
Error: {error.message}
; + } + + if (!data) { + return
Message not found
; + } + + return ( +
+ + + Message + + +
+ + + {data.message.message_obj_addr} + +

+ ), + }, + { + label: "Creator address", + value: ( +

+ + {data.message.creator_addr} + +

+ ), + }, + { + label: "Creation timestamp", + value: ( +

+ {new Date( + data.message.creation_timestamp * 1000 + ).toLocaleString()} +

+ ), + }, + { + label: "Last update timestamp", + value: ( +

+ {new Date( + data.message.last_update_timestamp * 1000 + ).toLocaleString()} +

+ ), + }, + { + label: "Content", + value:

{data.message.content}

, + }, + ]} + /> +
+
+
+ {data.message.creator_addr == account?.address && ( + + )} +
+ ); +} diff --git a/templates/indexer-template/next-app/src/components/MessageBoard.tsx b/templates/indexer-template/src/components/MessageBoard.tsx similarity index 91% rename from templates/indexer-template/next-app/src/components/MessageBoard.tsx rename to templates/indexer-template/src/components/MessageBoard.tsx index d6ae45bd..0b8fc94b 100644 --- a/templates/indexer-template/next-app/src/components/MessageBoard.tsx +++ b/templates/indexer-template/src/components/MessageBoard.tsx @@ -6,7 +6,7 @@ export const MessageBoard = async () => { return ( - Message Board + Message board diff --git a/templates/indexer-template/src/components/RootFooter.tsx b/templates/indexer-template/src/components/RootFooter.tsx new file mode 100644 index 00000000..5259cf7d --- /dev/null +++ b/templates/indexer-template/src/components/RootFooter.tsx @@ -0,0 +1,9 @@ +import { IndexerStatus } from "@/components/IndexerStatus"; + +export const RootFooter = () => { + return ( +
+ +
+ ); +}; diff --git a/templates/indexer-template/src/components/RootHeader.tsx b/templates/indexer-template/src/components/RootHeader.tsx new file mode 100644 index 00000000..233237a6 --- /dev/null +++ b/templates/indexer-template/src/components/RootHeader.tsx @@ -0,0 +1,36 @@ +import { ThemeToggle } from "@/components/ThemeToggle"; +import { WalletSelector } from "@/components/wallet/WalletSelector"; + +export const RootHeader = () => { + return ( + + ); +}; diff --git a/templates/indexer-template/next-app/src/components/ThemeToggle.tsx b/templates/indexer-template/src/components/ThemeToggle.tsx similarity index 100% rename from templates/indexer-template/next-app/src/components/ThemeToggle.tsx rename to templates/indexer-template/src/components/ThemeToggle.tsx diff --git a/templates/indexer-template/src/components/UpdateMessage.tsx b/templates/indexer-template/src/components/UpdateMessage.tsx new file mode 100644 index 00000000..b4e5e054 --- /dev/null +++ b/templates/indexer-template/src/components/UpdateMessage.tsx @@ -0,0 +1,125 @@ +"use client"; + +import { useWalletClient } from "@thalalabs/surf/hooks"; +import { useWallet } from "@aptos-labs/wallet-adapter-react"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; + +import { getAptosClient } from "@/lib/aptos"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { useToast } from "@/components/ui/use-toast"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { TransactionOnExplorer } from "@/components/ExplorerLink"; +import { ABI } from "@/lib/abi/message_board_abi"; +import { useQueryClient } from "@tanstack/react-query"; + +const FormSchema = z.object({ + stringContent: z.string(), +}); + +type UpdateMessageProps = { + messageObjAddr: `0x${string}`; +}; + +export function UpdateMessage({ messageObjAddr }: UpdateMessageProps) { + const { toast } = useToast(); + const { connected, account } = useWallet(); + const { client: walletClient } = useWalletClient(); + const queryClient = useQueryClient(); + + const form = useForm>({ + resolver: zodResolver(FormSchema), + defaultValues: { + stringContent: "Updated content", + }, + }); + + const onSignAndSubmitTransaction = async ( + data: z.infer + ) => { + if (!account || !walletClient) { + console.error("Account or wallet client not available"); + return; + } + + walletClient + .useABI(ABI) + .update_message({ + type_arguments: [], + arguments: [messageObjAddr, data.stringContent], + }) + .then((committedTransaction) => { + return getAptosClient().waitForTransaction({ + transactionHash: committedTransaction.hash, + }); + }) + .then((executedTransaction) => { + toast({ + title: "Success", + description: ( + + ), + }); + return new Promise((resolve) => setTimeout(resolve, 3000)); + }) + .then(() => { + return queryClient.invalidateQueries({ queryKey: [messageObjAddr] }); + }) + .catch((error) => { + console.error("Error", error); + toast({ + title: "Error", + description: "Failed to create a message", + }); + }); + }; + + return ( + + + Update message content + + + + + ( + + String Content + + + + Store a string content + + + )} + /> + + + + + + ); +} diff --git a/templates/indexer-template/next-app/src/components/WrongNetworkAlert.tsx b/templates/indexer-template/src/components/WrongNetworkAlert.tsx similarity index 100% rename from templates/indexer-template/next-app/src/components/WrongNetworkAlert.tsx rename to templates/indexer-template/src/components/WrongNetworkAlert.tsx diff --git a/templates/indexer-template/src/components/analytics-board/columns.tsx b/templates/indexer-template/src/components/analytics-board/columns.tsx new file mode 100644 index 00000000..d3f71cb6 --- /dev/null +++ b/templates/indexer-template/src/components/analytics-board/columns.tsx @@ -0,0 +1,62 @@ +"use client"; + +import { ColumnDef } from "@tanstack/react-table"; +import { truncateAddress } from "@aptos-labs/wallet-adapter-react"; + +import { DataTableColumnHeader } from "@/components/ui/data-table-column-header"; +import { Message } from "@/lib/type/message"; +import { NETWORK } from "@/lib/aptos"; + +export const columns: ColumnDef[] = [ + { + accessorKey: "user_addr", + header: ({ column }) => ( + + ), + cell: ({ row }) => ( + + ), + enableSorting: false, + }, + { + accessorKey: "created_messages", + header: ({ column }) => ( + + ), + cell: ({ row }) => ( +
{row.getValue("created_messages")}
+ ), + enableSorting: false, + }, + { + accessorKey: "updated_messages", + header: ({ column }) => ( + + ), + cell: ({ row }) => ( +
{row.getValue("updated_messages")}
+ ), + enableSorting: false, + }, + { + accessorKey: "total_points", + header: ({ column }) => ( + + ), + cell: ({ row }) => ( +
{row.getValue("total_points")}
+ ), + enableSorting: true, + }, +]; diff --git a/templates/indexer-template/src/components/analytics-board/data-table.tsx b/templates/indexer-template/src/components/analytics-board/data-table.tsx new file mode 100644 index 00000000..f73b0983 --- /dev/null +++ b/templates/indexer-template/src/components/analytics-board/data-table.tsx @@ -0,0 +1,150 @@ +"use client"; + +import * as React from "react"; +import { + ColumnDef, + SortingState, + flexRender, + getCoreRowModel, + getFacetedRowModel, + getFacetedUniqueValues, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + useReactTable, +} from "@tanstack/react-table"; + +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; + +import { DataTablePagination } from "@/components/ui/data-table-pagination"; +import { getUserStatsOnServer } from "@/app/actions"; +import { useQuery } from "@tanstack/react-query"; + +interface DataTableProps { + columns: ColumnDef[]; +} + +export function DataTable({ + columns, +}: DataTableProps) { + const [sorting, setSorting] = React.useState([ + { id: "total_points", desc: true }, + ]); + const [{ pageIndex, pageSize }, setPagination] = React.useState({ + pageIndex: 0, + pageSize: 5, + }); + + const fetchData = async () => { + return await getUserStatsOnServer({ + page: pageIndex + 1, + limit: pageSize, + sortedBy: sorting[0]?.id as "total_points", + order: sorting[0]?.desc ? "DESC" : "ASC", + }); + }; + + const { data, isLoading, isError, error } = useQuery({ + queryKey: ["user_stats", pageIndex, pageSize, sorting], + queryFn: fetchData, + }); + + const pagination = React.useMemo( + () => ({ + pageIndex, + pageSize, + }), + [pageIndex, pageSize] + ); + + const table = useReactTable({ + data: (data?.userStats as TData[]) || [], + columns, + state: { + sorting, + pagination, + }, + pageCount: Math.ceil(data?.total || 0 / pageSize), + enableRowSelection: true, + onSortingChange: setSorting, + onPaginationChange: setPagination, + getCoreRowModel: getCoreRowModel(), + getFilteredRowModel: getFilteredRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getSortedRowModel: getSortedRowModel(), + getFacetedRowModel: getFacetedRowModel(), + getFacetedUniqueValues: getFacetedUniqueValues(), + manualPagination: true, + }); + + if (isLoading) { +
Loading...
; + } + + if (isError) { + return
Error: {error.message}
; + } + + return ( +
+
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ); + })} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + + ))} + + )) + ) : ( + + + No results. + + + )} + +
+
+ +
+ ); +} diff --git a/templates/indexer-template/next-app/src/components/message-board/columns.tsx b/templates/indexer-template/src/components/message-board/columns.tsx similarity index 59% rename from templates/indexer-template/next-app/src/components/message-board/columns.tsx rename to templates/indexer-template/src/components/message-board/columns.tsx index 093ac2b5..4a2ccd25 100644 --- a/templates/indexer-template/next-app/src/components/message-board/columns.tsx +++ b/templates/indexer-template/src/components/message-board/columns.tsx @@ -1,22 +1,19 @@ "use client"; import { ColumnDef } from "@tanstack/react-table"; -import { truncateAddress } from "@aptos-labs/wallet-adapter-react"; -import { DataTableColumnHeader } from "@/components/message-board/data-table-column-header"; +import { DataTableColumnHeader } from "@/components/ui/data-table-column-header"; import { DataTableRowActions } from "@/components/message-board/data-table-row-actions"; -import { MessageBoardColumns } from "@/lib/type/message"; +import { Message } from "@/lib/type/message"; -export const columns: ColumnDef[] = [ +export const columns: ColumnDef[] = [ { - accessorKey: "message_obj_addr", + accessorKey: "content", header: ({ column }) => ( - + ), cell: ({ row }) => ( -
- {truncateAddress(row.getValue("message_obj_addr"))} -
+
{row.getValue("content")}
), enableSorting: false, }, diff --git a/templates/indexer-template/next-app/src/components/message-board/data-table-row-actions.tsx b/templates/indexer-template/src/components/message-board/data-table-row-actions.tsx similarity index 93% rename from templates/indexer-template/next-app/src/components/message-board/data-table-row-actions.tsx rename to templates/indexer-template/src/components/message-board/data-table-row-actions.tsx index a8c7a869..8c197827 100644 --- a/templates/indexer-template/next-app/src/components/message-board/data-table-row-actions.tsx +++ b/templates/indexer-template/src/components/message-board/data-table-row-actions.tsx @@ -10,10 +10,10 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; -import { MessageBoardColumns } from "@/lib/type/message"; +import { Message } from "@/lib/type/message"; interface DataTableRowActionsProps { - row: Row; + row: Row; } export function DataTableRowActions({ row }: DataTableRowActionsProps) { diff --git a/templates/indexer-template/next-app/src/components/message-board/data-table.tsx b/templates/indexer-template/src/components/message-board/data-table.tsx similarity index 91% rename from templates/indexer-template/next-app/src/components/message-board/data-table.tsx rename to templates/indexer-template/src/components/message-board/data-table.tsx index 5021b59f..a03aaa78 100644 --- a/templates/indexer-template/next-app/src/components/message-board/data-table.tsx +++ b/templates/indexer-template/src/components/message-board/data-table.tsx @@ -23,7 +23,7 @@ import { TableRow, } from "@/components/ui/table"; -import { DataTablePagination } from "@/components/message-board/data-table-pagination"; +import { DataTablePagination } from "@/components/ui/data-table-pagination"; import { getMessagesOnServer } from "@/app/actions"; import { useQuery } from "@tanstack/react-query"; @@ -71,7 +71,7 @@ export function DataTable({ sorting, pagination, }, - pageCount: Math.ceil(data?.totalMessages || 0 / pageSize), + pageCount: Math.ceil(data?.total || 0 / pageSize), enableRowSelection: true, onSortingChange: setSorting, onPaginationChange: setPagination, @@ -85,17 +85,15 @@ export function DataTable({ }); if (isLoading) { - return
Loading...
; +
Loading...
; } if (isError) { return
Error: {error.message}
; } - console.log("data", data); - return ( -
+
@@ -146,10 +144,7 @@ export function DataTable({
- +
); } diff --git a/templates/indexer-template/next-app/src/components/providers/QueryProvider.tsx b/templates/indexer-template/src/components/providers/QueryProvider.tsx similarity index 100% rename from templates/indexer-template/next-app/src/components/providers/QueryProvider.tsx rename to templates/indexer-template/src/components/providers/QueryProvider.tsx diff --git a/templates/indexer-template/next-app/src/components/providers/ThemeProvider.tsx b/templates/indexer-template/src/components/providers/ThemeProvider.tsx similarity index 100% rename from templates/indexer-template/next-app/src/components/providers/ThemeProvider.tsx rename to templates/indexer-template/src/components/providers/ThemeProvider.tsx diff --git a/templates/indexer-template/next-app/src/components/providers/WalletProvider.tsx b/templates/indexer-template/src/components/providers/WalletProvider.tsx similarity index 100% rename from templates/indexer-template/next-app/src/components/providers/WalletProvider.tsx rename to templates/indexer-template/src/components/providers/WalletProvider.tsx diff --git a/templates/indexer-template/next-app/src/components/ui/alert.tsx b/templates/indexer-template/src/components/ui/alert.tsx similarity index 100% rename from templates/indexer-template/next-app/src/components/ui/alert.tsx rename to templates/indexer-template/src/components/ui/alert.tsx diff --git a/templates/indexer-template/next-app/src/components/ui/button.tsx b/templates/indexer-template/src/components/ui/button.tsx similarity index 100% rename from templates/indexer-template/next-app/src/components/ui/button.tsx rename to templates/indexer-template/src/components/ui/button.tsx diff --git a/templates/indexer-template/next-app/src/components/ui/card.tsx b/templates/indexer-template/src/components/ui/card.tsx similarity index 100% rename from templates/indexer-template/next-app/src/components/ui/card.tsx rename to templates/indexer-template/src/components/ui/card.tsx diff --git a/templates/indexer-template/next-app/src/components/ui/checkbox.tsx b/templates/indexer-template/src/components/ui/checkbox.tsx similarity index 100% rename from templates/indexer-template/next-app/src/components/ui/checkbox.tsx rename to templates/indexer-template/src/components/ui/checkbox.tsx diff --git a/templates/indexer-template/next-app/src/components/ui/collapsible.tsx b/templates/indexer-template/src/components/ui/collapsible.tsx similarity index 100% rename from templates/indexer-template/next-app/src/components/ui/collapsible.tsx rename to templates/indexer-template/src/components/ui/collapsible.tsx diff --git a/templates/indexer-template/next-app/src/components/message-board/data-table-column-header.tsx b/templates/indexer-template/src/components/ui/data-table-column-header.tsx similarity index 100% rename from templates/indexer-template/next-app/src/components/message-board/data-table-column-header.tsx rename to templates/indexer-template/src/components/ui/data-table-column-header.tsx diff --git a/templates/indexer-template/next-app/src/components/message-board/data-table-pagination.tsx b/templates/indexer-template/src/components/ui/data-table-pagination.tsx similarity index 100% rename from templates/indexer-template/next-app/src/components/message-board/data-table-pagination.tsx rename to templates/indexer-template/src/components/ui/data-table-pagination.tsx diff --git a/templates/indexer-template/next-app/src/components/ui/dialog.tsx b/templates/indexer-template/src/components/ui/dialog.tsx similarity index 100% rename from templates/indexer-template/next-app/src/components/ui/dialog.tsx rename to templates/indexer-template/src/components/ui/dialog.tsx diff --git a/templates/indexer-template/next-app/src/components/ui/dropdown-menu.tsx b/templates/indexer-template/src/components/ui/dropdown-menu.tsx similarity index 100% rename from templates/indexer-template/next-app/src/components/ui/dropdown-menu.tsx rename to templates/indexer-template/src/components/ui/dropdown-menu.tsx diff --git a/templates/indexer-template/next-app/src/components/ui/form.tsx b/templates/indexer-template/src/components/ui/form.tsx similarity index 100% rename from templates/indexer-template/next-app/src/components/ui/form.tsx rename to templates/indexer-template/src/components/ui/form.tsx diff --git a/templates/indexer-template/next-app/src/components/ui/input.tsx b/templates/indexer-template/src/components/ui/input.tsx similarity index 100% rename from templates/indexer-template/next-app/src/components/ui/input.tsx rename to templates/indexer-template/src/components/ui/input.tsx diff --git a/templates/indexer-template/next-app/src/components/ui/label.tsx b/templates/indexer-template/src/components/ui/label.tsx similarity index 100% rename from templates/indexer-template/next-app/src/components/ui/label.tsx rename to templates/indexer-template/src/components/ui/label.tsx diff --git a/templates/indexer-template/next-app/src/components/ui/radio-group.tsx b/templates/indexer-template/src/components/ui/radio-group.tsx similarity index 100% rename from templates/indexer-template/next-app/src/components/ui/radio-group.tsx rename to templates/indexer-template/src/components/ui/radio-group.tsx diff --git a/templates/indexer-template/next-app/src/components/ui/select.tsx b/templates/indexer-template/src/components/ui/select.tsx similarity index 100% rename from templates/indexer-template/next-app/src/components/ui/select.tsx rename to templates/indexer-template/src/components/ui/select.tsx diff --git a/templates/indexer-template/next-app/src/components/ui/switch.tsx b/templates/indexer-template/src/components/ui/switch.tsx similarity index 100% rename from templates/indexer-template/next-app/src/components/ui/switch.tsx rename to templates/indexer-template/src/components/ui/switch.tsx diff --git a/templates/indexer-template/next-app/src/components/ui/table.tsx b/templates/indexer-template/src/components/ui/table.tsx similarity index 100% rename from templates/indexer-template/next-app/src/components/ui/table.tsx rename to templates/indexer-template/src/components/ui/table.tsx diff --git a/templates/indexer-template/next-app/src/components/ui/toast.tsx b/templates/indexer-template/src/components/ui/toast.tsx similarity index 100% rename from templates/indexer-template/next-app/src/components/ui/toast.tsx rename to templates/indexer-template/src/components/ui/toast.tsx diff --git a/templates/indexer-template/next-app/src/components/ui/toaster.tsx b/templates/indexer-template/src/components/ui/toaster.tsx similarity index 100% rename from templates/indexer-template/next-app/src/components/ui/toaster.tsx rename to templates/indexer-template/src/components/ui/toaster.tsx diff --git a/templates/indexer-template/next-app/src/components/ui/tooltip.tsx b/templates/indexer-template/src/components/ui/tooltip.tsx similarity index 100% rename from templates/indexer-template/next-app/src/components/ui/tooltip.tsx rename to templates/indexer-template/src/components/ui/tooltip.tsx diff --git a/templates/indexer-template/next-app/src/components/ui/use-toast.ts b/templates/indexer-template/src/components/ui/use-toast.ts similarity index 100% rename from templates/indexer-template/next-app/src/components/ui/use-toast.ts rename to templates/indexer-template/src/components/ui/use-toast.ts diff --git a/templates/indexer-template/next-app/src/components/wallet/WalletConnection.tsx b/templates/indexer-template/src/components/wallet/WalletConnection.tsx similarity index 100% rename from templates/indexer-template/next-app/src/components/wallet/WalletConnection.tsx rename to templates/indexer-template/src/components/wallet/WalletConnection.tsx diff --git a/templates/indexer-template/next-app/src/components/wallet/WalletSelector.tsx b/templates/indexer-template/src/components/wallet/WalletSelector.tsx similarity index 100% rename from templates/indexer-template/next-app/src/components/wallet/WalletSelector.tsx rename to templates/indexer-template/src/components/wallet/WalletSelector.tsx diff --git a/templates/indexer-template/src/db/getLastSuccessVersion.ts b/templates/indexer-template/src/db/getLastSuccessVersion.ts new file mode 100644 index 00000000..c9eef093 --- /dev/null +++ b/templates/indexer-template/src/db/getLastSuccessVersion.ts @@ -0,0 +1,12 @@ +import { getPostgresClient } from "@/lib/db"; + +export const getLastSuccessVersion = async (): Promise => { + const rows = + await getPostgresClient()(`SELECT last_success_version FROM processor_status`); + if (rows.length === 0) { + throw new Error("Status not found"); + } + const last_success_version = rows[0].last_success_version; + + return last_success_version; +}; diff --git a/templates/indexer-template/next-app/src/db/getMessage.ts b/templates/indexer-template/src/db/getMessage.ts similarity index 63% rename from templates/indexer-template/next-app/src/db/getMessage.ts rename to templates/indexer-template/src/db/getMessage.ts index e8c9cd78..a5ae7fac 100644 --- a/templates/indexer-template/next-app/src/db/getMessage.ts +++ b/templates/indexer-template/src/db/getMessage.ts @@ -1,6 +1,5 @@ -import { sql } from "@vercel/postgres"; - -import { MessageOnUi, MessageInDb } from "@/lib/type/message"; +import { getPostgresClient } from "@/lib/db"; +import { Message } from "@/lib/type/message"; export type GetMessageProps = { messageObjAddr: `0x${string}`; @@ -9,20 +8,22 @@ export type GetMessageProps = { export const getMessage = async ({ messageObjAddr, }: GetMessageProps): Promise<{ - message: MessageOnUi; + message: Message; }> => { - const query = `SELECT * FROM messages WHERE message_obj_addr = $1`; - const { rows } = await sql.query(query, [messageObjAddr]); + const rows = await getPostgresClient()( + `SELECT * FROM messages WHERE message_obj_addr = '${messageObjAddr}'` + ); if (rows.length === 0) { throw new Error("Message not found"); } - const message: MessageInDb = rows[0]; + const message = rows[0] as Message; const messageConverted = { message_obj_addr: message.message_obj_addr as `0x${string}`, creator_addr: message.creator_addr as `0x${string}`, creation_timestamp: message.creation_timestamp, last_update_timestamp: message.last_update_timestamp, content: message.content, + last_update_event_idx: message.last_update_event_idx, }; return { message: messageConverted }; }; diff --git a/templates/indexer-template/src/db/getMessages.ts b/templates/indexer-template/src/db/getMessages.ts new file mode 100644 index 00000000..5fb170dd --- /dev/null +++ b/templates/indexer-template/src/db/getMessages.ts @@ -0,0 +1,43 @@ +import { getPostgresClient } from "@/lib/db"; +import { Message } from "@/lib/type/message"; + +export type GetMessagesProps = { + page: number; + limit: number; + sortedBy: "creation_timestamp"; + order: "ASC" | "DESC"; +}; + +export const getMessages = async ({ + page, + limit, + sortedBy, + order, +}: GetMessagesProps): Promise<{ + messages: Message[]; + total: number; +}> => { + const rows = await getPostgresClient()( + `SELECT * FROM messages ORDER BY ${sortedBy} ${order} LIMIT ${limit} OFFSET ${ + (page - 1) * limit + }` + ); + + const messages = rows.map((row) => { + return { + message_obj_addr: row.message_obj_addr, + creation_timestamp: parseInt(row.creation_timestamp), + content: row.content, + creator_addr: row.creator_addr, + last_update_timestamp: parseInt(row.last_update_timestamp), + last_update_event_idx: parseInt(row.last_update_event_idx), + }; + }); + + const rows2 = await getPostgresClient()(` + SELECT COUNT(*) FROM messages; + `); + const count = rows2[0].count; + + return { messages, total: count }; +}; diff --git a/templates/indexer-template/src/db/getUserStats.ts b/templates/indexer-template/src/db/getUserStats.ts new file mode 100644 index 00000000..50b18f86 --- /dev/null +++ b/templates/indexer-template/src/db/getUserStats.ts @@ -0,0 +1,44 @@ +import { getPostgresClient } from "@/lib/db"; +import { UserStat } from "@/lib/type/user_stats"; + +export type GetUserStatsProps = { + page: number; + limit: number; + sortedBy: "total_points"; + order: "ASC" | "DESC"; +}; + +export const getUserStats = async ({ + page, + limit, + sortedBy, + order, +}: GetUserStatsProps): Promise<{ + userStats: UserStat[]; + total: number; +}> => { + const rows = await getPostgresClient()( + `SELECT * FROM user_stats ORDER BY ${sortedBy} ${order} LIMIT ${limit} OFFSET ${ + (page - 1) * limit + }` + ); + + const userStats = rows.map((row) => { + return { + user_addr: row.user_addr, + creation_timestamp: parseInt(row.creation_timestamp), + last_update_timestamp: parseInt(row.last_update_timestamp), + created_messages: parseInt(row.created_messages), + updated_messages: parseInt(row.updated_messages), + s1_points: parseInt(row.s1_points), + total_points: parseInt(row.total_points), + }; + }); + + const rows2 = await getPostgresClient()(` + SELECT COUNT(*) FROM user_stats; + `); + const count = rows2[0].count; + + return { userStats, total: count }; +}; diff --git a/templates/indexer-template/next-app/src/lib/abi/message_board_abi.ts b/templates/indexer-template/src/lib/abi/message_board_abi.ts similarity index 100% rename from templates/indexer-template/next-app/src/lib/abi/message_board_abi.ts rename to templates/indexer-template/src/lib/abi/message_board_abi.ts diff --git a/templates/indexer-template/next-app/src/lib/aptos.ts b/templates/indexer-template/src/lib/aptos.ts similarity index 100% rename from templates/indexer-template/next-app/src/lib/aptos.ts rename to templates/indexer-template/src/lib/aptos.ts diff --git a/templates/indexer-template/src/lib/db.ts b/templates/indexer-template/src/lib/db.ts new file mode 100644 index 00000000..8d86294a --- /dev/null +++ b/templates/indexer-template/src/lib/db.ts @@ -0,0 +1,5 @@ +import { neon } from "@neondatabase/serverless"; + +export const getPostgresClient = () => { + return neon(process.env.DATABASE_URL!); +}; diff --git a/templates/indexer-template/next-app/src/lib/type/indexer_status.ts b/templates/indexer-template/src/lib/type/indexer_status.ts similarity index 100% rename from templates/indexer-template/next-app/src/lib/type/indexer_status.ts rename to templates/indexer-template/src/lib/type/indexer_status.ts diff --git a/templates/indexer-template/src/lib/type/message.ts b/templates/indexer-template/src/lib/type/message.ts new file mode 100644 index 00000000..657106f5 --- /dev/null +++ b/templates/indexer-template/src/lib/type/message.ts @@ -0,0 +1,8 @@ +export type Message = { + message_obj_addr: string; + creator_addr: string; + creation_timestamp: number; + last_update_timestamp: number; + last_update_event_idx: number; + content: string; +}; diff --git a/templates/indexer-template/src/lib/type/user_stats.ts b/templates/indexer-template/src/lib/type/user_stats.ts new file mode 100644 index 00000000..a46d2a73 --- /dev/null +++ b/templates/indexer-template/src/lib/type/user_stats.ts @@ -0,0 +1,9 @@ +export type UserStat = { + user_addr: string; + creation_timestamp: number; + last_update_timestamp: number; + created_messages: number; + updated_messages: number; + s1_points: number; + total_points: number; +}; diff --git a/templates/indexer-template/src/lib/utils.ts b/templates/indexer-template/src/lib/utils.ts new file mode 100644 index 00000000..a5ef1935 --- /dev/null +++ b/templates/indexer-template/src/lib/utils.ts @@ -0,0 +1,6 @@ +import { clsx, type ClassValue } from "clsx"; +import { twMerge } from "tailwind-merge"; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} diff --git a/templates/indexer-template/next-app/tailwind.config.ts b/templates/indexer-template/tailwind.config.ts similarity index 100% rename from templates/indexer-template/next-app/tailwind.config.ts rename to templates/indexer-template/tailwind.config.ts diff --git a/templates/indexer-template/next-app/tsconfig.json b/templates/indexer-template/tsconfig.json similarity index 100% rename from templates/indexer-template/next-app/tsconfig.json rename to templates/indexer-template/tsconfig.json