├── .codecov.yml ├── .github └── workflows │ └── go.yaml ├── .gitignore ├── Dockerfile ├── LICENSE ├── Makefile ├── Procfile ├── README.md ├── analytics.png ├── config.yaml ├── contract ├── delegateprofile.abi └── delegateprofile.go ├── epochctx └── epochctx.go ├── go.mod ├── go.sum ├── go.test.sh ├── graphql ├── generated.go ├── gqlgen.yml ├── models_gen.go ├── resolver.go ├── schema.graphql └── scripts │ └── gqlgen.go ├── indexcontext └── context.go ├── indexprotocol ├── accounts │ ├── protocol.go │ └── protocol_test.go ├── actions │ ├── bucket.go │ ├── hermes.go │ ├── hermes_abi.go │ ├── hermes_test.go │ ├── protocol.go │ ├── protocol_test.go │ ├── xrc20.go │ └── xrc20_test.go ├── blocks │ ├── protocol.go │ └── protocol_test.go ├── protocol.go ├── protocol_test.go ├── registry.go ├── rewards │ ├── protocol.go │ └── protocol_test.go └── votings │ ├── bucketoperator.go │ ├── candidateoperator.go │ ├── probation.go │ ├── protocol.go │ ├── protocol_test.go │ ├── stakingprotocol.go │ └── stakingprotocol_test.go ├── indexservice └── indexer.go ├── main.go ├── queryprotocol ├── actions │ ├── protocol.go │ └── protocol_test.go ├── chainmeta │ ├── chainmetautil │ │ └── chainmetautil.go │ ├── protocol.go │ └── protocol_test.go ├── hermes2 │ └── protocol.go ├── productivity │ └── protocol.go ├── protocol.go ├── rewards │ └── protocol.go └── votings │ └── protocol.go ├── sql ├── mysql.go ├── mysql_test.go ├── rds.go ├── rds_test.go ├── storebase.go ├── storebase_tests.go └── util.go └── testutil ├── blockbuilder.go └── cleanup.go /.codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | range: 50...90 3 | status: 4 | project: 5 | default: 6 | enabled: yes 7 | target: 60% 8 | patch: 9 | default: 10 | enabled: yes 11 | target: auto 12 | -------------------------------------------------------------------------------- /.github/workflows/go.yaml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | 16 | - name: Set up Go 17 | uses: actions/setup-go@v2 18 | with: 19 | go-version: 1.14 20 | 21 | - name: Build 22 | run: go build -v ./... 23 | 24 | - name: Test 25 | run: go test -short -p 1 ./... 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/* 2 | .idea/* 3 | *.db 4 | *.db-journal 5 | # Binaries for programs and plugins 6 | bin/* 7 | *.exe 8 | *.exe~ 9 | *.dll 10 | *.so 11 | *.dylib 12 | 13 | # Test binary, build with `go test -c` 14 | *.test 15 | 16 | # Output of the go coverage tool, specifically when used with LiteIDE 17 | *.out 18 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.13.4-stretch 2 | 3 | WORKDIR apps/iotex-analytics/ 4 | 5 | RUN apt-get install -y --no-install-recommends make 6 | 7 | COPY go.mod . 8 | COPY go.sum . 9 | 10 | RUN go mod download 11 | 12 | COPY . . 13 | 14 | RUN rm -rf ./bin/server && \ 15 | go build -o ./bin/server -v . && \ 16 | cp ./bin/server /usr/local/bin/iotex-server && \ 17 | mkdir -p /etc/iotex/ && \ 18 | cp config.yaml /etc/iotex/config.yaml && \ 19 | rm -rf apps/iotex-analytics/ 20 | 21 | CMD [ "iotex-server"] 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ######################################################################################################################## 2 | # Copyright (c) 2019 IoTeX 3 | # This is an alpha (internal) release and is not suitable for production. This source code is provided 'as is' and no 4 | # warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent 5 | # permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache 6 | # License 2.0 that can be found in the LICENSE file. 7 | ######################################################################################################################## 8 | 9 | # Go parameters 10 | GOCMD=go 11 | GOLINT=golint 12 | GOBUILD=$(GOCMD) build 13 | GOCLEAN=$(GOCMD) clean 14 | GOTEST=$(GOCMD) test 15 | BUILD_TARGET_SERVER=server 16 | 17 | # Pkgs 18 | ALL_PKGS := $(shell go list ./... ) 19 | PKGS := $(shell go list ./... | grep -v /test/ ) 20 | ROOT_PKG := "github.com/iotexproject/iotex-analytics" 21 | 22 | # Docker parameters 23 | DOCKERCMD=docker 24 | 25 | all: clean build test 26 | 27 | .PHONY: build 28 | build: 29 | $(GOBUILD) -o ./bin/$(BUILD_TARGET_SERVER) -v . 30 | 31 | .PHONY: fmt 32 | fmt: 33 | $(GOCMD) fmt ./... 34 | 35 | .PHONY: lint 36 | lint: 37 | go list ./... | grep -v /vendor/ | xargs $(GOLINT) 38 | 39 | .PHONY: test 40 | test: fmt 41 | $(GOTEST) -short -p 1 ./... 42 | 43 | .PHONY: clean 44 | clean: 45 | @echo "Cleaning..." 46 | $(ECHO_V)rm -rf ./bin/$(BUILD_TARGET_SERVER) 47 | $(ECHO_V)$(GOCLEAN) -i $(PKGS) 48 | 49 | .PHONY: run 50 | run: 51 | $(GOBUILD) -o ./bin/$(BUILD_TARGET_SERVER) -v . 52 | ./bin/$(BUILD_TARGET_SERVER) 53 | 54 | .PHONY: docker 55 | docker: 56 | $(DOCKERCMD) build -t $(USER)/iotex-analytics:latest . 57 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: bin/iotex-analytics -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # This repo is not actively maintained. Please refer to https://github.com/iotexproject/iotex-analyser-api 2 | 3 |

4 | 5 |

6 | 7 | 8 | The independent service that analyzes data from IoTeX blockchain 9 | 10 | ## How it works 11 | Analytics serves as an indexer for IoTeX blockchain. It gets the raw data including all the actions and receipts from an IoTeX full node and voting information from IoTeX election service and processes the data based on different dimensions. Currently, analytics registers five index protocols: accounts, blocks, actions, rewards, and votings. Each protocol keeps track of its relevant data and writes it into the corresponding database tables. Specifically, accounts protocol monitors the balance change of each account. Blocks protocol maintains block metadata and block producing history. Actions protocol logs action metadata and records more detailed information for special transactions, such as XRC smart contracts and Hermes smart contract. Rewards protocol keeps track of rewarding history and synthesize the reward aggregations for each candidate. Votings protocol is responsible for syncing the most recent candidate registrations and votes. In order to make the abovementioned data publicly accessible, analytics also builds a data serving layer upon the underlying database. Now it supports GraphQL API which contains a built-in interactive user interface. Feel free to play on the [Mainnet Analytics Playground](https://analytics.iotexscan.io/). For each available query, please refer to the [Documentation](https://docs.iotex.io/docs/misc.html#analytics) for usage and examples. 12 | 13 | If you want to build your own analytics and run it as a service, please go to the next section. 14 | 15 | ## Get started 16 | 17 | ### Minimum requirements 18 | 19 | | Components | Version | Description | 20 | |----------|-------------|-------------| 21 | | [Golang](https://golang.org) | ≥ 1.11.5 | Go programming language | 22 | 23 | ## Run as a service 24 | 1. If you put the project code under your `$GOPATH/src`, you will need to set up an environment variable: 25 | ``` 26 | export GO111MODULE=on 27 | ``` 28 | 29 | 2. Specify MySQL connection string and datababse name by setting up the following environment variables: 30 | ``` 31 | export CONNECTION_STRING=username:password@protocol(address)/ 32 | export DB_NAME=dbname 33 | ``` 34 | e.g. 35 | ``` 36 | export CONNECTION_STRING=root:rootuser@tcp(127.0.0.1:3306)/ 37 | export DB_NAME=analytics 38 | ``` 39 | Note that you need to set up a MySQL DB instance beforehand. 40 | 41 | 3. Specify IoTeX Public API address and IoTeX election service address by setting up the following environment variables: 42 | ``` 43 | export CHAIN_ENDPOINT=Full_Node_IP:API_Port 44 | export ELECTION_ENDPOINT=Election_Host_IP:Election_Port 45 | ``` 46 | If you don't have access to an IoTeX full node, you can use the following setups: 47 | ``` 48 | export CHAIN_ENDPOINT=35.233.188.105:14014 49 | export ELECTION_ENDPOINT=35.233.188.105:8089 50 | ``` 51 | 52 | 4. Specify server port (OPTIONAL): 53 | ``` 54 | export PORT=Port_Number 55 | ``` 56 | Port number = 8089 by default 57 | 58 | 5. Start IoTeX-Analytics server: 59 | ``` 60 | make run 61 | ``` 62 | 63 | 6. If you want to query analytical data through GraphQL playground, after starting the server, go to http://localhost:8089/ 64 | 65 | You need to change the port number if you specify a different one. 66 | 67 | ## Start a service in Docker Container 68 | 69 | You can find the docker image on [docker hub](https://hub.docker.com/r/iotex/iotex-analytics). 70 | 71 | 1. Pull the docker image: 72 | 73 | ``` 74 | docker pull iotex/iotex-analytics:v0.1.0 75 | ``` 76 | 77 | 2. Run the following command to start a node: 78 | 79 | ``` 80 | docker run -d --restart on-failure --name analytics \ 81 | -p 8089:8089 \ 82 | -e CONFIG=/etc/iotex/config.yaml \ 83 | -e CHAIN_ENDPOINT=35.233.188.105:14014 \ 84 | -e ELECTION_ENDPOINT=35.233.188.105:8089 \ 85 | -e CONNECTION_STRING=root:rootuser@tcp(host.docker.internal:3306)/ \ 86 | -e DB_NAME=analytics \ 87 | iotex/iotex-analytics:v0.1.0 \ 88 | iotex-server 89 | ``` 90 | 91 | Note that you might need to change environment variables above based on your settings. 92 | 93 | Now the service should be started successfully. 94 | 95 | 96 | -------------------------------------------------------------------------------- /analytics.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iotexproject/iotex-analytics/3dda6958d27d4b24f0cc205547d455516b571833/analytics.png -------------------------------------------------------------------------------- /config.yaml: -------------------------------------------------------------------------------- 1 | numDelegates: 24 2 | numCandidateDelegates: 36 3 | numSubEpochs: 15 4 | numSubEpochsDardanelles: 30 5 | dardanellesHeight: 1816201 6 | dardanellesOn: true 7 | fairbankHeight: 5165641 8 | consensusScheme: "ROLLDPOS" 9 | rangeQueryLimit: 100 10 | rewardPortionCfg: 11 | rewardPortionContract: "io1lfl4ppn2c3wcft04f0rk0jy9lyn4pcjcm7638u" 12 | rewardportionContractDeployHeight: 5095225 13 | genesis: 14 | account: 15 | initBalances: 16 | io1uqhmnttmv0pg8prugxxn7d8ex9angrvfjfthxa: "9800000000000000000000000000" 17 | io1v3gkc49d5vwtdfdka2ekjl3h468egun8e43r7z: "100000000000000000000000000" 18 | io1vrl48nsdm8jaujccd9cx4ve23cskr0ys6urx92: "100000000000000000000000000" 19 | io1llupp3n8q5x8usnr5w08j6hc6hn55x64l46rr7: "100000000000000000000000000" 20 | io1ns7y0pxmklk8ceattty6n7makpw76u770u5avy: "100000000000000000000000000" 21 | io1xuavja5dwde8pvy4yms06yyncad4yavghjhwra: "100000000000000000000000000" 22 | io1cdqx6p5rquudxuewflfndpcl0l8t5aezen9slr: "100000000000000000000000000" 23 | io1hh97f273nhxcq8ajzcpujtt7p9pqyndfmavn9r: "100000000000000000000000000" 24 | io1yhvu38epz5vmkjaclp45a7t08r27slmcc0zjzh: "100000000000000000000000000" 25 | io1cl6rl2ev5dfa988qmgzg2x4hfazmp9vn2g66ng: "100000000000000000000000000" 26 | io1skmqp33qme8knyw0fzgt9takwrc2nvz4sevk5c: "100000000000000000000000000" 27 | io1fxzh50pa6qc6x5cprgmgw4qrp5vw97zk5pxt3q: "100000000000000000000000000" 28 | io1jh0ekmccywfkmj7e8qsuzsupnlk3w5337hjjg2: "100000000000000000000000000" 29 | io1juvx5g063eu4ts832nukp4vgcwk2gnc5cu9ayd: "100000000000000000000000000" 30 | io19d0p3ah4g8ww9d7kcxfq87yxe7fnr8rpth5shj: "100000000000000000000000000" 31 | io1ed52svvdun2qv8sf2m0xnynuxfaulv6jlww7ur: "100000000000000000000000000" 32 | io158hyzrmf4a8xll7gfc8xnwlv70jgp44tzy5nvd: "100000000000000000000000000" 33 | io19kshh892255x4h5ularvr3q3al2v8cgl80fqrt: "100000000000000000000000000" 34 | io1ph0u2psnd7muq5xv9623rmxdsxc4uapxhzpg02: "100000000000000000000000000" 35 | io1znka733xefxjjw2wqddegplwtefun0mfdmz7dw: "100000000000000000000000000" 36 | io13sj9mzpewn25ymheukte4v39hvjdtrfp00mlyv: "100000000000000000000000000" 37 | io14gnqxf9dpkn05g337rl7eyt2nxasphf5m6n0rd: "100000000000000000000000000" 38 | io1l3wc0smczyay8xq747e2hw63mzg3ctp6uf8wsg: "100000000000000000000000000" 39 | io1q4tdrahguffdu4e9j9aj4f38p2nee0r9vlhx7s: "100000000000000000000000000" 40 | io1k9y4a9juk45zaqwvjmhtz6yjc68twqds4qcvzv: "100000000000000000000000000" 41 | io15flratm0nhh5xpxz2lznrrpmnwteyd86hxdtj0: "100000000000000000000000000" 42 | io1eq4ehs6xx6zj9gcsax7h3qydwlxut9xcfcjras: "100000000000000000000000000" 43 | io10a298zmzvrt4guq79a9f4x7qedj59y7ery84he: "100000000000000000000000000" 44 | poll: 45 | skipManifiedCandidate: true 46 | voteThreshold: "100000000000000000000" 47 | scoreThreshold: "0" 48 | selfStakingThreshold: "0" 49 | gravityChain: 50 | gravityChainStartHeight: 7614500 51 | gravityChainAPIs: 52 | - https://mainnet.infura.io/v3/e1f5217dc75d4b77bfede00ca895635b 53 | registerContractAddress: 0x8619a5c5232c32bae63c442b905a27ae46598566 54 | rewardPercentageStartHeight: 8300000 55 | rewarding: 56 | numDelegatesForEpochReward: 100 57 | numDelegatesForFoundationBonus: 36 58 | productivityThreshold: 85 59 | exemptCandidatesFromEpochReward: 60 | - "696f726f626f746270303031" 61 | - "696f726f626f746270303032" 62 | - "696f726f626f746270303033" 63 | - "696f726f626f746270303034" 64 | - "696f726f626f746270303035" 65 | - "696f726f626f746270303036" 66 | - "696f726f626f746270303037" 67 | - "696f726f626f746270303038" 68 | - "696f726f626f746270303039" 69 | - "696f726f626f746270303130" 70 | - "696f726f626f746270303132" 71 | - "696f726f626f746270303131" 72 | - "696f726f626f746270303331" 73 | - "696f726f626f746270303332" 74 | - "696f726f626f746270303333" 75 | - "696f726f626f746270303334" 76 | - "696f726f626f746270303335" 77 | - "696f726f626f746270303336" 78 | - "696f726f626f746270303133" 79 | - "696f726f626f746270303134" 80 | - "696f726f626f746270303135" 81 | - "696f726f626f746270303136" 82 | - "696f726f626f746270303137" 83 | - "696f726f626f746270303138" 84 | - "696f726f626f746270303139" 85 | - "696f726f626f746270303230" 86 | - "696f726f626f746270303231" 87 | - "696f726f626f746270303232" 88 | - "696f726f626f746270303233" 89 | - "696f726f626f746270303234" 90 | - "696f726f626f746270303235" 91 | - "696f726f626f746270303236" 92 | - "696f726f626f746270303237" 93 | - "696f726f626f746270303238" 94 | - "696f726f626f746270303239" 95 | - "696f726f626f746270303330" 96 | 97 | hermesConfig: 98 | hermesContractAddress: "io1fqulsuv8p820wmr0yd39jzx0m3pnpmuzzcywh8" 99 | multiSendContractAddressList: 100 | - "io1lvemm43lz6np0hzcqlpk0kpxxww623z5hs4mwu" 101 | - "io16y9wk2xnwurvtgmd2mds2gcdfe2lmzad6dcw29" 102 | voteWeightCalConsts: 103 | durationLg: 1.2 104 | autoStake: 1 105 | selfStake: 1.06 106 | zap: 107 | development: false 108 | level: info 109 | encoding: json 110 | disableCaller: false 111 | disableStacktrace: false 112 | outputPaths: ["analytics.log"] 113 | errorOutputPaths: ["stderr"] 114 | -------------------------------------------------------------------------------- /contract/delegateprofile.abi: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "constant": false, 4 | "inputs": [ 5 | { 6 | "name": "_delegate", 7 | "type": "address" 8 | }, 9 | { 10 | "name": "_name", 11 | "type": "string" 12 | }, 13 | { 14 | "name": "_value", 15 | "type": "bytes" 16 | } 17 | ], 18 | "name": "updateProfileForDelegate", 19 | "outputs": [], 20 | "payable": false, 21 | "stateMutability": "nonpayable", 22 | "type": "function" 23 | }, 24 | { 25 | "constant": true, 26 | "inputs": [], 27 | "name": "register", 28 | "outputs": [ 29 | { 30 | "name": "", 31 | "type": "address" 32 | } 33 | ], 34 | "payable": false, 35 | "stateMutability": "view", 36 | "type": "function" 37 | }, 38 | { 39 | "constant": true, 40 | "inputs": [ 41 | { 42 | "name": "_delegate", 43 | "type": "address" 44 | } 45 | ], 46 | "name": "getEncodedProfile", 47 | "outputs": [ 48 | { 49 | "name": "code_", 50 | "type": "bytes" 51 | } 52 | ], 53 | "payable": false, 54 | "stateMutability": "view", 55 | "type": "function" 56 | }, 57 | { 58 | "constant": true, 59 | "inputs": [ 60 | { 61 | "name": "_address", 62 | "type": "address" 63 | } 64 | ], 65 | "name": "isOwner", 66 | "outputs": [ 67 | { 68 | "name": "", 69 | "type": "bool" 70 | } 71 | ], 72 | "payable": false, 73 | "stateMutability": "view", 74 | "type": "function" 75 | }, 76 | { 77 | "constant": true, 78 | "inputs": [ 79 | { 80 | "name": "_name", 81 | "type": "string" 82 | } 83 | ], 84 | "name": "getFieldByName", 85 | "outputs": [ 86 | { 87 | "name": "verifier_", 88 | "type": "address" 89 | }, 90 | { 91 | "name": "deprecated_", 92 | "type": "bool" 93 | } 94 | ], 95 | "payable": false, 96 | "stateMutability": "view", 97 | "type": "function" 98 | }, 99 | { 100 | "constant": false, 101 | "inputs": [ 102 | { 103 | "name": "_byteCode", 104 | "type": "bytes" 105 | } 106 | ], 107 | "name": "updateProfileWithByteCode", 108 | "outputs": [], 109 | "payable": false, 110 | "stateMutability": "nonpayable", 111 | "type": "function" 112 | }, 113 | { 114 | "constant": false, 115 | "inputs": [], 116 | "name": "withdraw", 117 | "outputs": [], 118 | "payable": false, 119 | "stateMutability": "nonpayable", 120 | "type": "function" 121 | }, 122 | { 123 | "constant": false, 124 | "inputs": [], 125 | "name": "unpause", 126 | "outputs": [], 127 | "payable": false, 128 | "stateMutability": "nonpayable", 129 | "type": "function" 130 | }, 131 | { 132 | "constant": true, 133 | "inputs": [], 134 | "name": "paused", 135 | "outputs": [ 136 | { 137 | "name": "", 138 | "type": "bool" 139 | } 140 | ], 141 | "payable": false, 142 | "stateMutability": "view", 143 | "type": "function" 144 | }, 145 | { 146 | "constant": false, 147 | "inputs": [ 148 | { 149 | "name": "_name", 150 | "type": "string" 151 | }, 152 | { 153 | "name": "_verifierAddr", 154 | "type": "address" 155 | } 156 | ], 157 | "name": "newField", 158 | "outputs": [], 159 | "payable": false, 160 | "stateMutability": "nonpayable", 161 | "type": "function" 162 | }, 163 | { 164 | "constant": false, 165 | "inputs": [ 166 | { 167 | "name": "_name", 168 | "type": "string" 169 | }, 170 | { 171 | "name": "_value", 172 | "type": "bytes" 173 | } 174 | ], 175 | "name": "updateProfile", 176 | "outputs": [], 177 | "payable": false, 178 | "stateMutability": "nonpayable", 179 | "type": "function" 180 | }, 181 | { 182 | "constant": false, 183 | "inputs": [], 184 | "name": "pause", 185 | "outputs": [], 186 | "payable": false, 187 | "stateMutability": "nonpayable", 188 | "type": "function" 189 | }, 190 | { 191 | "constant": true, 192 | "inputs": [ 193 | { 194 | "name": "_idx", 195 | "type": "uint256" 196 | } 197 | ], 198 | "name": "getFieldByIndex", 199 | "outputs": [ 200 | { 201 | "name": "name_", 202 | "type": "string" 203 | }, 204 | { 205 | "name": "verifier_", 206 | "type": "address" 207 | }, 208 | { 209 | "name": "deprecated_", 210 | "type": "bool" 211 | } 212 | ], 213 | "payable": false, 214 | "stateMutability": "view", 215 | "type": "function" 216 | }, 217 | { 218 | "constant": true, 219 | "inputs": [], 220 | "name": "owner", 221 | "outputs": [ 222 | { 223 | "name": "", 224 | "type": "address" 225 | } 226 | ], 227 | "payable": false, 228 | "stateMutability": "view", 229 | "type": "function" 230 | }, 231 | { 232 | "constant": false, 233 | "inputs": [ 234 | { 235 | "name": "_delegate", 236 | "type": "address" 237 | }, 238 | { 239 | "name": "_byteCode", 240 | "type": "bytes" 241 | } 242 | ], 243 | "name": "updateProfileWithByteCodeForDelegate", 244 | "outputs": [], 245 | "payable": false, 246 | "stateMutability": "nonpayable", 247 | "type": "function" 248 | }, 249 | { 250 | "constant": true, 251 | "inputs": [ 252 | { 253 | "name": "", 254 | "type": "uint256" 255 | } 256 | ], 257 | "name": "fieldNames", 258 | "outputs": [ 259 | { 260 | "name": "", 261 | "type": "string" 262 | } 263 | ], 264 | "payable": false, 265 | "stateMutability": "view", 266 | "type": "function" 267 | }, 268 | { 269 | "constant": true, 270 | "inputs": [ 271 | { 272 | "name": "_addr", 273 | "type": "address" 274 | } 275 | ], 276 | "name": "registered", 277 | "outputs": [ 278 | { 279 | "name": "", 280 | "type": "bool" 281 | } 282 | ], 283 | "payable": false, 284 | "stateMutability": "view", 285 | "type": "function" 286 | }, 287 | { 288 | "constant": true, 289 | "inputs": [ 290 | { 291 | "name": "_delegate", 292 | "type": "address" 293 | }, 294 | { 295 | "name": "_field", 296 | "type": "string" 297 | } 298 | ], 299 | "name": "getProfileByField", 300 | "outputs": [ 301 | { 302 | "name": "", 303 | "type": "bytes" 304 | } 305 | ], 306 | "payable": false, 307 | "stateMutability": "view", 308 | "type": "function" 309 | }, 310 | { 311 | "constant": false, 312 | "inputs": [ 313 | { 314 | "name": "_name", 315 | "type": "string" 316 | } 317 | ], 318 | "name": "deprecateField", 319 | "outputs": [], 320 | "payable": false, 321 | "stateMutability": "nonpayable", 322 | "type": "function" 323 | }, 324 | { 325 | "constant": true, 326 | "inputs": [], 327 | "name": "numOfFields", 328 | "outputs": [ 329 | { 330 | "name": "", 331 | "type": "uint256" 332 | } 333 | ], 334 | "payable": false, 335 | "stateMutability": "view", 336 | "type": "function" 337 | }, 338 | { 339 | "constant": false, 340 | "inputs": [ 341 | { 342 | "name": "_newOwner", 343 | "type": "address" 344 | } 345 | ], 346 | "name": "transferOwnership", 347 | "outputs": [], 348 | "payable": false, 349 | "stateMutability": "nonpayable", 350 | "type": "function" 351 | }, 352 | { 353 | "inputs": [ 354 | { 355 | "name": "registerAddr", 356 | "type": "address" 357 | } 358 | ], 359 | "payable": false, 360 | "stateMutability": "nonpayable", 361 | "type": "constructor" 362 | }, 363 | { 364 | "anonymous": false, 365 | "inputs": [ 366 | { 367 | "indexed": false, 368 | "name": "fee", 369 | "type": "uint256" 370 | } 371 | ], 372 | "name": "FeeUpdated", 373 | "type": "event" 374 | }, 375 | { 376 | "anonymous": false, 377 | "inputs": [ 378 | { 379 | "indexed": false, 380 | "name": "delegate", 381 | "type": "address" 382 | }, 383 | { 384 | "indexed": false, 385 | "name": "name", 386 | "type": "string" 387 | }, 388 | { 389 | "indexed": false, 390 | "name": "value", 391 | "type": "bytes" 392 | } 393 | ], 394 | "name": "ProfileUpdated", 395 | "type": "event" 396 | }, 397 | { 398 | "anonymous": false, 399 | "inputs": [ 400 | { 401 | "indexed": false, 402 | "name": "name", 403 | "type": "string" 404 | } 405 | ], 406 | "name": "FieldDeprecated", 407 | "type": "event" 408 | }, 409 | { 410 | "anonymous": false, 411 | "inputs": [ 412 | { 413 | "indexed": false, 414 | "name": "name", 415 | "type": "string" 416 | } 417 | ], 418 | "name": "NewField", 419 | "type": "event" 420 | }, 421 | { 422 | "anonymous": false, 423 | "inputs": [], 424 | "name": "Pause", 425 | "type": "event" 426 | }, 427 | { 428 | "anonymous": false, 429 | "inputs": [], 430 | "name": "Unpause", 431 | "type": "event" 432 | } 433 | ] -------------------------------------------------------------------------------- /epochctx/epochctx.go: -------------------------------------------------------------------------------- 1 | package epochctx 2 | 3 | import "github.com/iotexproject/iotex-core/pkg/log" 4 | 5 | // EpochCtx defines epoch context 6 | type EpochCtx struct { 7 | numCandidateDelegates uint64 8 | numDelegates uint64 9 | numSubEpochs uint64 10 | numSubEpochsDardanelles uint64 11 | dardanellesHeight uint64 12 | dardanellesOn bool 13 | fairbankHeight uint64 14 | } 15 | 16 | // Option is optional setting for epoch context 17 | type Option func(*EpochCtx) error 18 | 19 | // EnableDardanellesSubEpoch will set give numSubEpochs at give height. 20 | func EnableDardanellesSubEpoch(height, numSubEpochs uint64) Option { 21 | return func(e *EpochCtx) error { 22 | e.dardanellesOn = true 23 | e.numSubEpochsDardanelles = numSubEpochs 24 | e.dardanellesHeight = height 25 | return nil 26 | } 27 | } 28 | 29 | // FairbankHeight will set fairbank height. 30 | func FairbankHeight(height uint64) Option { 31 | return func(e *EpochCtx) error { 32 | e.fairbankHeight = height 33 | return nil 34 | } 35 | } 36 | 37 | // NewEpochCtx returns a new epoch context 38 | func NewEpochCtx(numCandidateDelegates, numDelegates, numSubEpochs uint64, opts ...Option) *EpochCtx { 39 | if numCandidateDelegates < numDelegates { 40 | numCandidateDelegates = numDelegates 41 | } 42 | e := &EpochCtx{ 43 | numCandidateDelegates: numCandidateDelegates, 44 | numDelegates: numDelegates, 45 | numSubEpochs: numSubEpochs, 46 | } 47 | 48 | for _, opt := range opts { 49 | if err := opt(e); err != nil { 50 | log.S().Panicf("Failed to execute epoch context creation option %p: %v", opt, err) 51 | } 52 | } 53 | return e 54 | } 55 | 56 | // GetEpochNumber returns the number of the epoch for a given height 57 | func (e *EpochCtx) GetEpochNumber(height uint64) uint64 { 58 | if height == 0 { 59 | return 0 60 | } 61 | if !e.dardanellesOn || height <= e.dardanellesHeight { 62 | return (height-1)/e.numDelegates/e.numSubEpochs + 1 63 | } 64 | dardanellesEpoch := e.GetEpochNumber(e.dardanellesHeight) 65 | dardanellesEpochHeight := e.GetEpochHeight(dardanellesEpoch) 66 | return dardanellesEpoch + (height-dardanellesEpochHeight)/e.numDelegates/e.numSubEpochsDardanelles 67 | } 68 | 69 | // GetEpochHeight returns the start height of an epoch 70 | func (e *EpochCtx) GetEpochHeight(epochNum uint64) uint64 { 71 | if epochNum == 0 { 72 | return 0 73 | } 74 | dardanellesEpoch := e.GetEpochNumber(e.dardanellesHeight) 75 | if !e.dardanellesOn || epochNum <= dardanellesEpoch { 76 | return (epochNum-1)*e.numDelegates*e.numSubEpochs + 1 77 | } 78 | dardanellesEpochHeight := e.GetEpochHeight(dardanellesEpoch) 79 | return dardanellesEpochHeight + (epochNum-dardanellesEpoch)*e.numDelegates*e.numSubEpochsDardanelles 80 | } 81 | 82 | // NumCandidateDelegates returns the number of candidate delegates 83 | func (e *EpochCtx) NumCandidateDelegates() uint64 { 84 | return e.numCandidateDelegates 85 | } 86 | 87 | // FairbankEffectiveHeight returns the effective height of fairbank 88 | func (e *EpochCtx) FairbankEffectiveHeight() uint64 { 89 | return e.fairbankHeight + e.numDelegates*e.numSubEpochsDardanelles 90 | } 91 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/iotexproject/iotex-analytics 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/99designs/gqlgen v0.8.3 7 | github.com/agnivade/levenshtein v1.0.2 // indirect 8 | github.com/cenkalti/backoff v2.2.1+incompatible 9 | github.com/ethereum/go-ethereum v1.9.5 10 | github.com/go-sql-driver/mysql v1.4.1 11 | github.com/golang/mock v1.4.4 12 | github.com/golang/protobuf v1.4.3 13 | github.com/iotexproject/go-pkgs v0.1.5-0.20210105202208-2dc9b27250a6 14 | github.com/iotexproject/iotex-address v0.2.4 15 | github.com/iotexproject/iotex-core v1.2.0 16 | github.com/iotexproject/iotex-election v0.3.5-0.20201031050050-c3ab4f339a54 17 | github.com/iotexproject/iotex-proto v0.5.0 18 | github.com/pkg/errors v0.9.1 19 | github.com/prometheus/client_golang v1.3.0 20 | github.com/rs/zerolog v1.18.0 21 | github.com/stretchr/testify v1.6.1 22 | github.com/vektah/gqlparser v1.1.2 23 | go.uber.org/zap v1.14.0 24 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 25 | golang.org/x/tools v0.0.0-20200318150045-ba25ddc85566 // indirect 26 | google.golang.org/grpc v1.33.1 27 | gopkg.in/yaml.v2 v2.4.0 28 | ) 29 | 30 | replace github.com/ethereum/go-ethereum => github.com/iotexproject/go-ethereum v0.3.1 31 | 32 | exclude github.com/dgraph-io/badger v2.0.0-rc.2+incompatible 33 | 34 | exclude github.com/dgraph-io/badger v2.0.0-rc2+incompatible 35 | 36 | exclude github.com/ipfs/go-ds-badger v0.0.3 37 | -------------------------------------------------------------------------------- /go.test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | echo "" > coverage.txt 5 | 6 | for d in $(go list ./... | grep -v vendor); do 7 | go test -short -coverprofile=profile.out -covermode=count "$d" 8 | if [ -f profile.out ]; then 9 | cat profile.out >> coverage.txt 10 | rm profile.out 11 | fi 12 | done -------------------------------------------------------------------------------- /graphql/gqlgen.yml: -------------------------------------------------------------------------------- 1 | # .gqlgen.yml example 2 | # 3 | # Refer to https://gqlgen.com/config/ 4 | # for detailed .gqlgen.yml documentation. 5 | 6 | schema: 7 | - schema.graphql 8 | exec: 9 | filename: generated.go 10 | model: 11 | filename: models_gen.go 12 | resolver: 13 | filename: resolver.go 14 | type: Resolver 15 | -------------------------------------------------------------------------------- /graphql/schema.graphql: -------------------------------------------------------------------------------- 1 | type Query { 2 | account: Account 3 | chain: Chain 4 | delegate(startEpoch: Int!, epochCount: Int!, delegateName: String!): Delegate 5 | voting(startEpoch: Int!, epochCount: Int!): Voting 6 | hermes(startEpoch: Int!, epochCount: Int!, rewardAddress: String!, waiverThreshold: Int!): Hermes 7 | hermesAverageStats(startEpoch: Int!, epochCount: Int!, rewardAddress: String!): AverageHermesStats 8 | xrc20: Xrc20 9 | xrc721: Xrc721 10 | action: Action 11 | topHolders(endEpochNumber: Int!, pagination: Pagination!):[TopHolder]! 12 | hermes2(startEpoch: Int!, epochCount: Int!): Hermes2 13 | bucketsByVoter(address: String!, pagination: Pagination): [BucketInfo]! 14 | bucketsByCandidate(name: String!, pagination: Pagination): [BucketInfo]! 15 | } 16 | 17 | type TopHolder{ 18 | address:String! 19 | balance:String! 20 | } 21 | 22 | type XrcInfo{ 23 | contract:String! 24 | hash:String! 25 | timestamp:String! 26 | from:String! 27 | to:String! 28 | quantity:String! 29 | } 30 | 31 | type Xrc20 { 32 | byContractAddress(address:String!,numPerPage:Int!,page:Int!): XrcList 33 | byAddress(address:String!,numPerPage:Int!,page:Int!): XrcList 34 | byPage(pagination: Pagination!): XrcList 35 | xrc20Addresses(pagination: Pagination!): XrcAddressList 36 | tokenHolderAddresses(tokenAddress:String!): XrcHolderAddressList 37 | byContractAndAddress(contract:String!,address:String!,numPerPage:Int!,page:Int!): XrcList 38 | } 39 | 40 | type Xrc721 { 41 | byContractAddress(address:String!,numPerPage:Int!,page:Int!): XrcList 42 | byAddress(address:String!,numPerPage:Int!,page:Int!): XrcList 43 | byPage(pagination: Pagination!): XrcList 44 | xrc721Addresses(pagination: Pagination!): XrcAddressList 45 | tokenHolderAddresses(tokenAddress:String!): XrcHolderAddressList 46 | } 47 | 48 | type Account { 49 | activeAccounts(count: Int!): [String!] 50 | alias(operatorAddress: String!): Alias 51 | operatorAddress(aliasName: String!): OperatorAddress 52 | totalNumberOfHolders: Int! 53 | totalAccountSupply :String! 54 | } 55 | 56 | type Action { 57 | byDates(startDate: Int!, endDate: Int!): ActionList 58 | byHash(actHash: String!): ActionDetail 59 | byAddress(address: String!): ActionList 60 | byAddressAndType(address: String!, type: String!): ActionList 61 | byBucketIndex(bucketIndex: Int!): ActionList 62 | evmTransfersByAddress(address: String!): EvmTransferList 63 | byType(type: String!): ActionList 64 | byVoter(voter: String!): ActionList 65 | } 66 | 67 | type Delegate { 68 | reward: Reward 69 | productivity: Productivity 70 | bookkeeping(percentage: Int!, includeBlockReward: Boolean, includeFoundationBonus: Boolean!): Bookkeeping 71 | bucketInfo: BucketInfoOutput 72 | staking: StakingOutput 73 | probationHistoricalRate: String! 74 | } 75 | 76 | type StakingOutput{ 77 | exist: Boolean! 78 | stakingInfo: [StakingInformation]! 79 | } 80 | 81 | type StakingInformation{ 82 | epochNumber: Int! 83 | totalStaking: String! 84 | selfStaking: String! 85 | } 86 | 87 | type Voting { 88 | candidateInfo: [CandidateInfoList]! 89 | votingMeta: VotingMeta 90 | rewardSources(voterIotexAddress: String!): RewardSources 91 | } 92 | 93 | 94 | type CandidateInfoList { 95 | epochNumber: Int! 96 | candidates: [CandidateInfo]! 97 | } 98 | 99 | type CandidateInfo { 100 | name: String! 101 | address: String! 102 | totalWeightedVotes: String! 103 | selfStakingTokens: String! 104 | operatorAddress: String! 105 | rewardAddress: String! 106 | } 107 | 108 | type Hermes { 109 | exist: Boolean! 110 | hermesDistribution: [HermesDistribution]! 111 | } 112 | 113 | type HermesDistribution { 114 | delegateName: String! 115 | rewardDistribution: [RewardDistribution]! 116 | stakingIotexAddress: String! 117 | voterCount: Int! 118 | waiveServiceFee: Boolean! 119 | refund: String! 120 | } 121 | 122 | type AverageHermesStats { 123 | exist: Boolean! 124 | averagePerEpoch: [HermesAverage]! 125 | } 126 | 127 | type HermesAverage { 128 | delegateName: String! 129 | rewardDistribution: String! 130 | totalWeightedVotes: String! 131 | } 132 | 133 | type VotingMeta { 134 | exist: Boolean! 135 | candidateMeta: [CandidateMeta]! 136 | } 137 | 138 | type RewardSources { 139 | exist: Boolean! 140 | delegateDistributions: [DelegateAmount]! 141 | } 142 | 143 | type ActionList { 144 | exist: Boolean! 145 | actions(pagination: Pagination): [ActionInfo]! 146 | count: Int! 147 | } 148 | 149 | type XrcList { 150 | exist: Boolean! 151 | xrc20(pagination: Pagination): [XrcInfo]! 152 | xrc721(pagination: Pagination): [XrcInfo]! 153 | count: Int! 154 | } 155 | 156 | type XrcAddressList { 157 | exist: Boolean! 158 | addresses(pagination: Pagination): [String]! 159 | count: Int! 160 | } 161 | 162 | type XrcHolderAddressList { 163 | addresses(pagination: Pagination): [String]! 164 | count: Int! 165 | } 166 | 167 | type ActionInfo { 168 | actHash: String! 169 | blkHash: String! 170 | timeStamp: Int! 171 | actType: String! 172 | sender: String! 173 | recipient: String! 174 | amount: String! 175 | gasFee: String! 176 | } 177 | 178 | type Alias { 179 | exist: Boolean! 180 | aliasName: String! 181 | } 182 | 183 | type OperatorAddress { 184 | exist: Boolean! 185 | operatorAddress: String! 186 | } 187 | 188 | type Reward { 189 | exist: Boolean! 190 | blockReward: String! 191 | epochReward: String! 192 | foundationBonus: String! 193 | } 194 | 195 | type Productivity { 196 | exist: Boolean! 197 | production: String! 198 | expectedProduction: String! 199 | } 200 | 201 | type BucketInfo { 202 | voterEthAddress: String! 203 | voterIotexAddress: String! 204 | isNative: Boolean! 205 | votes: String! 206 | weightedVotes: String! 207 | remainingDuration: String! 208 | startTime: String! 209 | decay: Boolean! 210 | } 211 | 212 | type Bookkeeping { 213 | exist: Boolean! 214 | rewardDistribution(pagination: Pagination): [RewardDistribution]! 215 | count: Int! 216 | } 217 | 218 | type BucketInfoOutput { 219 | exist: Boolean! 220 | bucketInfoList(pagination: Pagination): [BucketInfoList]! 221 | } 222 | 223 | type BucketInfoList { 224 | epochNumber: Int! 225 | bucketInfo: [BucketInfo]! 226 | count: Int! 227 | } 228 | 229 | type RewardDistribution { 230 | voterEthAddress: String! 231 | voterIotexAddress: String! 232 | amount: String! 233 | } 234 | 235 | type DelegateAmount { 236 | delegateName: String! 237 | amount: String! 238 | } 239 | 240 | type Chain { 241 | mostRecentEpoch: Int! 242 | mostRecentBlockHeight: Int! 243 | votingResultMeta: VotingResultMeta 244 | mostRecentTPS(blockWindow: Int!): Float! 245 | numberOfActions(pagination: EpochRange): NumberOfActions 246 | totalTransferredTokens(pagination: EpochRange): String! 247 | totalSupply: String! 248 | totalCirculatingSupply: String! 249 | totalCirculatingSupplyNoRewardPool: String! 250 | } 251 | 252 | type NumberOfActions{ 253 | exist: Boolean! 254 | count: Int! 255 | } 256 | 257 | type VotingResultMeta { 258 | totalCandidates: Int! 259 | totalWeightedVotes: String! 260 | votedTokens: String! 261 | } 262 | 263 | #[TODO] combine candidateMeta with votingResultMeta 264 | type CandidateMeta{ 265 | epochNumber: Int! 266 | totalCandidates: Int! 267 | consensusDelegates: Int! 268 | totalWeightedVotes: String! 269 | votedTokens: String! 270 | } 271 | 272 | type ActionDetail{ 273 | actionInfo: ActionInfo 274 | evmTransfers: [EvmTransfer]! 275 | } 276 | 277 | type EvmTransfer{ 278 | from: String! 279 | to: String! 280 | quantity: String! 281 | } 282 | 283 | type EvmTransferDetail{ 284 | from: String! 285 | to: String! 286 | quantity: String! 287 | actHash: String! 288 | blkHash: String! 289 | timeStamp: Int! 290 | } 291 | 292 | type EvmTransferList{ 293 | exist: Boolean! 294 | evmTransfers(pagination: Pagination): [EvmTransferDetail]! 295 | count: Int! 296 | } 297 | 298 | input Pagination{ 299 | skip: Int! 300 | first: Int! 301 | } 302 | 303 | input EpochRange{ 304 | startEpoch: Int! 305 | epochCount: Int! 306 | } 307 | 308 | type Hermes2 { 309 | byDelegate(delegateName: String!): ByDelegateResponse 310 | byVoter(voterAddress: String!): ByVoterResponse 311 | hermesMeta: HermesMeta 312 | } 313 | 314 | type VoterInfo { 315 | voterAddress: String! 316 | fromEpoch: Int! 317 | toEpoch: Int! 318 | amount: String! 319 | actionHash: String! 320 | timestamp: String! 321 | } 322 | 323 | type ByDelegateResponse { 324 | exist: Boolean! 325 | voterInfoList(pagination: Pagination): [VoterInfo]! 326 | count: Int! 327 | totalRewardsDistributed: String! 328 | distributionRatio: [Ratio]! 329 | } 330 | 331 | type Ratio { 332 | epochNumber: Int! 333 | blockRewardRatio: Float! 334 | epochRewardRatio: Float! 335 | foundationBonusRatio: Float! 336 | } 337 | 338 | type DelegateInfo { 339 | delegateName: String! 340 | fromEpoch: Int! 341 | toEpoch: Int! 342 | amount: String! 343 | actionHash: String! 344 | timestamp: String! 345 | } 346 | 347 | type ByVoterResponse { 348 | exist: Boolean! 349 | delegateInfoList(pagination: Pagination): [DelegateInfo]! 350 | count: Int! 351 | totalRewardsReceived: String! 352 | } 353 | 354 | type HermesMeta{ 355 | exist: Boolean! 356 | numberOfDelegates: Int! 357 | numberOfRecipients: Int! 358 | totalRewardsDistributed: String! 359 | } 360 | -------------------------------------------------------------------------------- /graphql/scripts/gqlgen.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 IoTeX 2 | // This is an alpha (internal) release and is not suitable for production. This source code is provided 'as is' and no 3 | // warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent 4 | // permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache 5 | // License 2.0 that can be found in the LICENSE file. 6 | 7 | package main 8 | 9 | import "github.com/99designs/gqlgen/cmd" 10 | 11 | func main() { 12 | cmd.Execute() 13 | } 14 | -------------------------------------------------------------------------------- /indexcontext/context.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 IoTeX 2 | // This is an alpha (internal) release and is not suitable for production. This source code is provided 'as is' and no 3 | // warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent 4 | // permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache 5 | // License 2.0 that can be found in the LICENSE file. 6 | 7 | package indexcontext 8 | 9 | import ( 10 | "context" 11 | 12 | "github.com/iotexproject/iotex-core/pkg/log" 13 | "github.com/iotexproject/iotex-election/pb/api" 14 | "github.com/iotexproject/iotex-proto/golang/iotexapi" 15 | ) 16 | 17 | type indexCtxKey struct{} 18 | 19 | // IndexCtx provides the indexer with auxiliary information 20 | type IndexCtx struct { 21 | ChainClient iotexapi.APIServiceClient 22 | ElectionClient api.APIServiceClient 23 | ConsensusScheme string 24 | } 25 | 26 | // WithIndexCtx adds IndexCtx into context 27 | func WithIndexCtx(ctx context.Context, indexCtx IndexCtx) context.Context { 28 | return context.WithValue(ctx, indexCtxKey{}, indexCtx) 29 | } 30 | 31 | // MustGetIndexCtx must get index context 32 | func MustGetIndexCtx(ctx context.Context) IndexCtx { 33 | indexCtx, ok := ctx.Value(indexCtxKey{}).(IndexCtx) 34 | if !ok { 35 | log.S().Panic("Miss index context") 36 | } 37 | return indexCtx 38 | } 39 | -------------------------------------------------------------------------------- /indexprotocol/accounts/protocol_test.go: -------------------------------------------------------------------------------- 1 | package accounts 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "testing" 7 | 8 | "github.com/golang/mock/gomock" 9 | "github.com/stretchr/testify/require" 10 | 11 | "github.com/iotexproject/iotex-core/test/mock/mock_apiserviceclient" 12 | "github.com/iotexproject/iotex-proto/golang/iotexapi" 13 | "github.com/iotexproject/iotex-proto/golang/iotextypes" 14 | 15 | "github.com/iotexproject/iotex-analytics/epochctx" 16 | "github.com/iotexproject/iotex-analytics/indexcontext" 17 | s "github.com/iotexproject/iotex-analytics/sql" 18 | "github.com/iotexproject/iotex-analytics/testutil" 19 | ) 20 | 21 | const ( 22 | connectStr = "bfe10c7cf8aa29:8bed5959@tcp(us-cdbr-east-04.cleardb.com:3306)/" 23 | dbName = "heroku_067cec75e0ba5ba" 24 | ) 25 | 26 | func TestProtocol(t *testing.T) { 27 | ctrl := gomock.NewController(t) 28 | defer ctrl.Finish() 29 | 30 | require := require.New(t) 31 | ctx := context.Background() 32 | 33 | testutil.CleanupDatabase(t, connectStr, dbName) 34 | 35 | store := s.NewMySQL(connectStr, dbName, false) 36 | require.NoError(store.Start(ctx)) 37 | defer func() { 38 | _, err := store.GetDB().Exec("DROP DATABASE " + dbName) 39 | require.NoError(err) 40 | require.NoError(store.Stop(ctx)) 41 | }() 42 | 43 | p := NewProtocol(store, epochctx.NewEpochCtx(1, 1, 1)) 44 | 45 | require.NoError(p.CreateTables(ctx)) 46 | 47 | blk, err := testutil.BuildCompleteBlock(uint64(1), uint64(2)) 48 | require.NoError(err) 49 | chainClient := mock_apiserviceclient.NewMockServiceClient(ctrl) 50 | ctx = indexcontext.WithIndexCtx(context.Background(), indexcontext.IndexCtx{ 51 | ChainClient: chainClient, 52 | ConsensusScheme: "ROLLDPOS", 53 | }) 54 | chainClient.EXPECT().GetTransactionLogByBlockHeight(gomock.Any(), gomock.Any()).Times(1).Return(&iotexapi.GetTransactionLogByBlockHeightResponse{ 55 | TransactionLogs: &iotextypes.TransactionLogs{ 56 | Logs: []*iotextypes.TransactionLog{ 57 | { 58 | ActionHash: []byte("1"), 59 | NumTransactions: uint64(1), 60 | Transactions: []*iotextypes.TransactionLog_Transaction{{ 61 | Topic: []byte(""), 62 | Amount: "1", 63 | Sender: testutil.Addr1, 64 | Recipient: testutil.Addr1, 65 | Type: iotextypes.TransactionLogType_NATIVE_TRANSFER, 66 | }}, 67 | }, 68 | { 69 | ActionHash: []byte("2"), 70 | NumTransactions: uint64(1), 71 | Transactions: []*iotextypes.TransactionLog_Transaction{{ 72 | Topic: []byte(""), 73 | Amount: "2", 74 | Sender: testutil.Addr1, 75 | Recipient: testutil.Addr2, 76 | Type: iotextypes.TransactionLogType_NATIVE_TRANSFER, 77 | }}, 78 | }, 79 | }, 80 | }, 81 | }, nil) 82 | chainClient.EXPECT().GetTransactionLogByBlockHeight(gomock.Any(), gomock.Any()).Times(1).Return(&iotexapi.GetTransactionLogByBlockHeightResponse{ 83 | TransactionLogs: &iotextypes.TransactionLogs{ 84 | Logs: []*iotextypes.TransactionLog{}, 85 | }, 86 | }, nil) 87 | require.NoError(store.Transact(func(tx *sql.Tx) error { 88 | return p.HandleBlock(ctx, tx, blk) 89 | })) 90 | 91 | blk2, err := testutil.BuildEmptyBlock(2) 92 | require.NoError(err) 93 | 94 | require.NoError(store.Transact(func(tx *sql.Tx) error { 95 | return p.HandleBlock(ctx, tx, blk2) 96 | })) 97 | 98 | // get balance history 99 | balanceHistory, err := p.getBalanceHistory(testutil.Addr1) 100 | require.NoError(err) 101 | require.Equal(2, len(balanceHistory)) 102 | require.Contains([]string{"1", "2"}, balanceHistory[1].Amount) 103 | 104 | // get account income 105 | accountIncome, err := p.getAccountIncome(uint64(1), testutil.Addr1) 106 | require.NoError(err) 107 | require.Equal("-2", accountIncome.Income) 108 | } 109 | -------------------------------------------------------------------------------- /indexprotocol/actions/bucket.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 IoTeX 2 | // This is an alpha (internal) release and is not suitable for production. This source code is provided 'as is' and no 3 | // warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent 4 | // permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache 5 | // License 2.0 that can be found in the LICENSE file. 6 | 7 | package actions 8 | 9 | import ( 10 | "context" 11 | "database/sql" 12 | "encoding/hex" 13 | "fmt" 14 | "math/big" 15 | "strings" 16 | 17 | "github.com/iotexproject/go-pkgs/hash" 18 | "github.com/iotexproject/iotex-address/address" 19 | "github.com/iotexproject/iotex-core/blockchain/block" 20 | ) 21 | 22 | const ( 23 | 24 | // BucketActionsTableName is the table name of bucket actions 25 | BucketActionsTableName = "bucket_actions" 26 | 27 | createBucketActionTable = "CREATE TABLE IF NOT EXISTS %s (" + 28 | "action_hash VARCHAR(64) NOT NULL," + 29 | "bucket_id DECIMAL(65,0) NOT NULL," + 30 | "PRIMARY KEY (action_hash)," + 31 | "KEY `bucket_id_index` (`bucket_id`)," + 32 | "CONSTRAINT `fb_bucket_actions_action_hash` FOREIGN KEY (`action_hash`) REFERENCES `action_history` (`action_hash`) ON DELETE NO ACTION ON UPDATE NO ACTION" + 33 | ") ENGINE=InnoDB DEFAULT CHARSET=latin1;" 34 | 35 | insertBucketAction = "INSERT IGNORE INTO %s (action_hash, bucket_id) VALUES %s" 36 | ) 37 | 38 | // CreateBucketActionTables creates tables 39 | func (p *Protocol) CreateBucketActionTables(ctx context.Context) error { 40 | // create block by action table 41 | _, err := p.Store.GetDB().Exec(fmt.Sprintf(createBucketActionTable, BucketActionsTableName)) 42 | return err 43 | } 44 | 45 | // updateXrc20History stores Xrc20 information into Xrc20 history table 46 | func (p *Protocol) updateBucketActions( 47 | ctx context.Context, 48 | tx *sql.Tx, 49 | blk *block.Block, 50 | ) error { 51 | valStrs := make([]string, 0) 52 | valArgs := make([]interface{}, 0) 53 | 54 | h := hash.Hash160b([]byte("staking")) 55 | stakingProtocolAddr, err := address.FromBytes(h[:]) 56 | if err != nil { 57 | return err 58 | } 59 | for _, receipt := range blk.Receipts { 60 | actionHash := hex.EncodeToString(receipt.ActionHash[:]) 61 | for _, log := range receipt.Logs() { 62 | if log.Address == stakingProtocolAddr.String() && len(log.Topics) > 1 { 63 | bucketIndex := new(big.Int).SetBytes(log.Topics[1][:]) 64 | valStrs = append(valStrs, "(?, ?)") 65 | valArgs = append(valArgs, actionHash, bucketIndex.String()) 66 | } 67 | } 68 | } 69 | if len(valArgs) == 0 { 70 | return nil 71 | } 72 | insertQuery := fmt.Sprintf(insertBucketAction, BucketActionsTableName, strings.Join(valStrs, ",")) 73 | _, err = tx.Exec(insertQuery, valArgs...) 74 | 75 | return err 76 | } 77 | -------------------------------------------------------------------------------- /indexprotocol/actions/hermes.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 IoTeX 2 | // This is an alpha (internal) release and is not suitable for production. This source code is provided 'as is' and no 3 | // warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent 4 | // permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache 5 | // License 2.0 that can be found in the LICENSE file. 6 | 7 | package actions 8 | 9 | import ( 10 | "bytes" 11 | "context" 12 | "database/sql" 13 | "encoding/hex" 14 | "fmt" 15 | "math/big" 16 | "strings" 17 | 18 | "github.com/ethereum/go-ethereum/accounts/abi" 19 | "github.com/ethereum/go-ethereum/crypto" 20 | "github.com/pkg/errors" 21 | 22 | "github.com/iotexproject/go-pkgs/hash" 23 | "github.com/iotexproject/iotex-core/action" 24 | ) 25 | 26 | const ( 27 | // HermesContractTableName is the table name of hermes contract 28 | HermesContractTableName = "hermes_contract" 29 | 30 | createHermesContract = "CREATE TABLE IF NOT EXISTS %s " + 31 | "(epoch_number DECIMAL(65, 0) NOT NULL, action_hash VARCHAR(64) NOT NULL, from_epoch DECIMAL(65, 0) NOT NULL, " + 32 | "to_epoch DECIMAL(65, 0) NOT NULL, delegate_name VARCHAR(255) NOT NULL, timestamp VARCHAR(128) NOT NULL, " + 33 | "PRIMARY KEY (action_hash))" 34 | insertHermesContract = "INSERT INTO %s (epoch_number, action_hash, from_epoch, to_epoch, delegate_name, timestamp) VALUES %s" 35 | 36 | // DistributeMsgEmitter represents the distribute event in hermes contract 37 | DistributeMsgEmitter = "Distribute(uint256,uint256,bytes32,uint256,uint256)" 38 | 39 | // DistributeEventName is the distribute event name 40 | DistributeEventName = "Distribute" 41 | ) 42 | 43 | // HermesContractInfo defines a contract info for hermes 44 | type HermesContractInfo struct { 45 | EpochNumber uint64 46 | ActionHash string 47 | FromEpoch uint64 48 | ToEpoch uint64 49 | DelegateName string 50 | Timestamp string 51 | } 52 | 53 | // CreateHermesTables creates tables 54 | func (p *Protocol) CreateHermesTables(ctx context.Context) error { 55 | if _, err := p.Store.GetDB().Exec(fmt.Sprintf(createHermesContract, HermesContractTableName)); err != nil { 56 | return err 57 | } 58 | return nil 59 | } 60 | 61 | func (p *Protocol) updateHermesContract(tx *sql.Tx, receipts []*action.Receipt, epochNumber uint64, timestamp string) error { 62 | contractList := make([]HermesContractInfo, 0) 63 | for _, receipt := range receipts { 64 | fromEpoch, toEpoch, delegateName, exist, err := getDistributeEventFromLog(receipt.Logs()) 65 | if err != nil { 66 | return errors.Wrap(err, "failed to get distribute event information from log") 67 | } 68 | if !exist { 69 | continue 70 | } 71 | actionHash := receipt.ActionHash 72 | contract := HermesContractInfo{ 73 | EpochNumber: epochNumber, 74 | ActionHash: hex.EncodeToString(actionHash[:]), 75 | FromEpoch: fromEpoch, 76 | ToEpoch: toEpoch, 77 | DelegateName: delegateName, 78 | Timestamp: timestamp, 79 | } 80 | contractList = append(contractList, contract) 81 | } 82 | if len(contractList) == 0 { 83 | return nil 84 | } 85 | return p.insertHermesContract(tx, contractList) 86 | } 87 | 88 | func (p *Protocol) insertHermesContract(tx *sql.Tx, contractList []HermesContractInfo) error { 89 | valStrs := make([]string, 0, len(contractList)) 90 | valArgs := make([]interface{}, 0, len(contractList)*6) 91 | for _, list := range contractList { 92 | valStrs = append(valStrs, "(?, ?, ?, ?, ?, ?)") 93 | valArgs = append(valArgs, list.EpochNumber, list.ActionHash, list.FromEpoch, list.ToEpoch, list.DelegateName, list.Timestamp) 94 | } 95 | insertQuery := fmt.Sprintf(insertHermesContract, HermesContractTableName, strings.Join(valStrs, ",")) 96 | 97 | if _, err := tx.Exec(insertQuery, valArgs...); err != nil { 98 | return err 99 | } 100 | return nil 101 | } 102 | 103 | func emitterIsDistributeByTopic(logTopic hash.Hash256) bool { 104 | now := string(logTopic[:]) 105 | emitter := string(crypto.Keccak256([]byte(DistributeMsgEmitter))[:]) 106 | if strings.Compare(emitter, now) != 0 { 107 | return false 108 | } 109 | return true 110 | } 111 | 112 | func getDelegateNameFromTopic(logTopic hash.Hash256) string { 113 | n := bytes.IndexByte(logTopic[:], 0) 114 | return string(logTopic[:n]) 115 | } 116 | 117 | func getDistributeEventFromLog(logs []*action.Log) (uint64, uint64, string, bool, error) { 118 | num := len(logs) 119 | // reverse range 120 | for num > 0 { 121 | num-- 122 | log := logs[num] 123 | if len(log.Topics) < 2 { 124 | continue 125 | } 126 | emiterTopic := log.Topics[0] 127 | if emitterIsDistributeByTopic(emiterTopic) == false { 128 | continue 129 | } 130 | hermesABI, err := abi.JSON(strings.NewReader(HermesABI)) 131 | if err != nil { 132 | return 0, 0, "", false, err 133 | } 134 | 135 | event := struct { 136 | StartEpoch *big.Int 137 | EndEpoch *big.Int 138 | DelegateName [32]byte 139 | NumOfRecipients *big.Int 140 | TotalAmount *big.Int 141 | }{} 142 | if err := hermesABI.Unpack(&event, DistributeEventName, log.Data); err != nil { 143 | return 0, 0, "", false, err 144 | } 145 | 146 | delegateNameTopic := log.Topics[1] 147 | delegateName := getDelegateNameFromTopic(delegateNameTopic) 148 | return event.StartEpoch.Uint64(), event.EndEpoch.Uint64(), delegateName, true, nil 149 | } 150 | return 0, 0, "", false, nil 151 | } 152 | -------------------------------------------------------------------------------- /indexprotocol/actions/hermes_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 IoTeX 2 | // This is an alpha (internal) release and is not suitable for production. This source code is provided 'as is' and no 3 | // warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent 4 | // permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache 5 | // License 2.0 that can be found in the LICENSE file. 6 | 7 | package actions 8 | 9 | import ( 10 | "encoding/hex" 11 | "strconv" 12 | "testing" 13 | 14 | "github.com/iotexproject/go-pkgs/hash" 15 | "github.com/iotexproject/iotex-core/action" 16 | "github.com/stretchr/testify/require" 17 | ) 18 | 19 | func stringToHash256(str string) (hash.Hash256, error) { 20 | var receiptHash hash.Hash256 21 | for i := range receiptHash { 22 | receiptHash[i] = 0 23 | } 24 | for i := range receiptHash { 25 | tmpStr := str[i*2 : i*2+2] 26 | s, err := strconv.ParseUint(tmpStr, 16, 32) 27 | if err != nil { 28 | return receiptHash, err 29 | } 30 | receiptHash[i] = byte(s) 31 | } 32 | return receiptHash, nil 33 | } 34 | 35 | func TestGetDelegateNameFromTopic(t *testing.T) { 36 | require := require.New(t) 37 | delegateNameTopic, err := stringToHash256("746865626f74746f6b656e230000000000000000000000000000000000000000") 38 | require.NoError(err) 39 | delegateName := getDelegateNameFromTopic(delegateNameTopic) 40 | require.Equal("thebottoken#", delegateName) 41 | } 42 | 43 | func TestEmiterIsHermesByTopic(t *testing.T) { 44 | require := require.New(t) 45 | emiterTopic, err := stringToHash256("7de680eab607fdcc6137464e40d375ad63446cf255dcea9bd4a19676f7f24f56") 46 | require.NoError(err) 47 | require.True(emitterIsDistributeByTopic(emiterTopic)) 48 | 49 | emiterTopic, err = stringToHash256("6a5c4f52260adc90a8637fe2d8fbbc4141b625fa6840fca5f3e5cef6a4992293") 50 | require.NoError(err) 51 | require.False(emitterIsDistributeByTopic(emiterTopic)) 52 | } 53 | 54 | func TestGetDistributeEventFromLog(t *testing.T) { 55 | require := require.New(t) 56 | topic1, err := stringToHash256("7de680eab607fdcc6137464e40d375ad63446cf255dcea9bd4a19676f7f24f56") 57 | require.NoError(err) 58 | topic2, err := stringToHash256("746865626f74746f6b656e230000000000000000000000000000000000000000") 59 | require.NoError(err) 60 | data, err := hex.DecodeString("0000000000000000000000000000000000000000000000000000000000002128000000000000000000000000000000000000000000000000000000000000213d000000000000000000000000000000000000000000000000000000000000000b000000000000000000000000000000000000000000000185f30377486ca2e646") 61 | require.NoError(err) 62 | logs := []*action.Log{ 63 | { 64 | Topics: []hash.Hash256{topic1, topic2}, 65 | Data: data, 66 | }, 67 | } 68 | fromEpoch, toEpoch, delegateName, exist, err := getDistributeEventFromLog(logs) 69 | require.NoError(err) 70 | require.True(exist) 71 | require.Equal(uint64(8488), fromEpoch) 72 | require.Equal(uint64(8509), toEpoch) 73 | require.Equal("thebottoken#", delegateName) 74 | } 75 | -------------------------------------------------------------------------------- /indexprotocol/actions/protocol_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 IoTeX 2 | // This is an alpha (internal) release and is not suitable for production. This source code is provided 'as is' and no 3 | // warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent 4 | // permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache 5 | // License 2.0 that can be found in the LICENSE file. 6 | 7 | package actions 8 | 9 | import ( 10 | "context" 11 | "database/sql" 12 | "encoding/hex" 13 | "math/big" 14 | "strconv" 15 | "testing" 16 | 17 | "github.com/golang/mock/gomock" 18 | "github.com/stretchr/testify/require" 19 | 20 | "github.com/iotexproject/iotex-core/state" 21 | "github.com/iotexproject/iotex-core/test/mock/mock_apiserviceclient" 22 | "github.com/iotexproject/iotex-election/pb/api" 23 | mock_election "github.com/iotexproject/iotex-election/test/mock/mock_apiserviceclient" 24 | "github.com/iotexproject/iotex-proto/golang/iotexapi" 25 | "github.com/iotexproject/iotex-proto/golang/iotextypes" 26 | 27 | "github.com/iotexproject/iotex-analytics/epochctx" 28 | "github.com/iotexproject/iotex-analytics/indexcontext" 29 | "github.com/iotexproject/iotex-analytics/indexprotocol" 30 | "github.com/iotexproject/iotex-analytics/indexprotocol/blocks" 31 | s "github.com/iotexproject/iotex-analytics/sql" 32 | "github.com/iotexproject/iotex-analytics/testutil" 33 | ) 34 | 35 | const ( 36 | //connectStr = "root:rootuser@tcp(127.0.0.1:3306)/" 37 | connectStr = "bfe10c7cf8aa29:8bed5959@tcp(us-cdbr-east-04.cleardb.com:3306)/" 38 | dbName = "heroku_067cec75e0ba5ba" 39 | ) 40 | 41 | func TestProtocol(t *testing.T) { 42 | ctrl := gomock.NewController(t) 43 | defer ctrl.Finish() 44 | 45 | require := require.New(t) 46 | ctx := context.Background() 47 | 48 | testutil.CleanupDatabase(t, connectStr, dbName) 49 | 50 | store := s.NewMySQL(connectStr, dbName, false) 51 | require.NoError(store.Start(ctx)) 52 | defer func() { 53 | _, err := store.GetDB().Exec("DROP DATABASE " + dbName) 54 | require.NoError(err) 55 | require.NoError(store.Stop(ctx)) 56 | }() 57 | 58 | bp := blocks.NewProtocol(store, epochctx.NewEpochCtx(36, 24, 15, epochctx.FairbankHeight(1000000)), indexprotocol.GravityChain{GravityChainStartHeight: 1}) 59 | p := NewProtocol(store, indexprotocol.HermesConfig{ 60 | HermesContractAddress: "testAddr", 61 | MultiSendContractAddressList: []string{"testAddr"}, 62 | }, epochctx.NewEpochCtx(36, 24, 15, epochctx.FairbankHeight(1000000))) 63 | 64 | require.NoError(bp.CreateTables(ctx)) 65 | require.NoError(p.CreateTables(ctx)) 66 | 67 | chainClient := mock_apiserviceclient.NewMockServiceClient(ctrl) 68 | electionClient := mock_election.NewMockAPIServiceClient(ctrl) 69 | bpctx := indexcontext.WithIndexCtx(context.Background(), indexcontext.IndexCtx{ 70 | ChainClient: chainClient, 71 | ElectionClient: electionClient, 72 | ConsensusScheme: "ROLLDPOS", 73 | }) 74 | 75 | electionClient.EXPECT().GetCandidates(gomock.Any(), gomock.Any()).Times(1).Return( 76 | &api.CandidateResponse{ 77 | Candidates: []*api.Candidate{ 78 | { 79 | Name: "616c6661", 80 | OperatorAddress: testutil.Addr1, 81 | }, 82 | { 83 | Name: "627261766f", 84 | OperatorAddress: testutil.Addr2, 85 | }, 86 | }, 87 | }, nil, 88 | ) 89 | readStateRequest := &iotexapi.ReadStateRequest{ 90 | ProtocolID: []byte(indexprotocol.PollProtocolID), 91 | MethodName: []byte("ActiveBlockProducersByEpoch"), 92 | Arguments: [][]byte{[]byte(strconv.FormatUint(1, 10))}, 93 | } 94 | candidateList := state.CandidateList{ 95 | { 96 | Address: testutil.Addr1, 97 | RewardAddress: testutil.RewardAddr1, 98 | Votes: big.NewInt(100), 99 | }, 100 | { 101 | Address: testutil.Addr2, 102 | RewardAddress: testutil.RewardAddr2, 103 | Votes: big.NewInt(10), 104 | }, 105 | } 106 | data, err := candidateList.Serialize() 107 | require.NoError(err) 108 | chainClient.EXPECT().ReadState(gomock.Any(), readStateRequest).Times(1).Return(&iotexapi.ReadStateResponse{ 109 | Data: data, 110 | }, nil) 111 | 112 | chainClient.EXPECT().ReadContract(gomock.Any(), gomock.Any()).AnyTimes().Return(&iotexapi.ReadContractResponse{ 113 | Receipt: &iotextypes.Receipt{Status: 1}, 114 | Data: "xx", 115 | }, nil) 116 | blk, err := testutil.BuildCompleteBlock(uint64(180), uint64(361)) 117 | require.NoError(err) 118 | 119 | require.NoError(store.Transact(func(tx *sql.Tx) error { 120 | return bp.HandleBlock(bpctx, tx, blk) 121 | })) 122 | 123 | require.NoError(store.Transact(func(tx *sql.Tx) error { 124 | return p.HandleBlock(bpctx, tx, blk) 125 | })) 126 | 127 | // get action 128 | actionHash := blk.Actions[1].Hash() 129 | receiptHash := blk.Receipts[1].Hash() 130 | actionHistory, err := p.getActionHistory(hex.EncodeToString(actionHash[:])) 131 | require.NoError(err) 132 | 133 | require.Equal("transfer", actionHistory.ActionType) 134 | require.Equal(hex.EncodeToString(receiptHash[:]), actionHistory.ReceiptHash) 135 | require.Equal(uint64(180), actionHistory.BlockHeight) 136 | require.Equal(testutil.Addr1, actionHistory.From) 137 | require.Equal(testutil.Addr2, actionHistory.To) 138 | require.Equal("0", actionHistory.GasPrice) 139 | require.Equal(uint64(2), actionHistory.GasConsumed) 140 | require.Equal(uint64(102), actionHistory.Nonce) 141 | require.Equal("2", actionHistory.Amount) 142 | require.Equal("success", actionHistory.ReceiptStatus) 143 | } 144 | -------------------------------------------------------------------------------- /indexprotocol/actions/xrc20_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 IoTeX 2 | // This is an alpha (internal) release and is not suitable for production. This source code is provided 'as is' and no 3 | // warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent 4 | // permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache 5 | // License 2.0 that can be found in the LICENSE file. 6 | 7 | package actions 8 | 9 | import ( 10 | "context" 11 | "database/sql" 12 | "encoding/hex" 13 | "fmt" 14 | "math/big" 15 | "strconv" 16 | "testing" 17 | 18 | "github.com/golang/mock/gomock" 19 | "github.com/pkg/errors" 20 | "github.com/stretchr/testify/require" 21 | 22 | "github.com/iotexproject/iotex-core/state" 23 | "github.com/iotexproject/iotex-core/test/mock/mock_apiserviceclient" 24 | "github.com/iotexproject/iotex-election/pb/api" 25 | mock_election "github.com/iotexproject/iotex-election/test/mock/mock_apiserviceclient" 26 | "github.com/iotexproject/iotex-proto/golang/iotexapi" 27 | "github.com/iotexproject/iotex-proto/golang/iotextypes" 28 | 29 | "github.com/iotexproject/iotex-analytics/epochctx" 30 | "github.com/iotexproject/iotex-analytics/indexcontext" 31 | "github.com/iotexproject/iotex-analytics/indexprotocol" 32 | "github.com/iotexproject/iotex-analytics/indexprotocol/blocks" 33 | s "github.com/iotexproject/iotex-analytics/sql" 34 | "github.com/iotexproject/iotex-analytics/testutil" 35 | ) 36 | 37 | func TestXrc20(t *testing.T) { 38 | ctrl := gomock.NewController(t) 39 | defer ctrl.Finish() 40 | 41 | require := require.New(t) 42 | ctx := context.Background() 43 | 44 | testutil.CleanupDatabase(t, connectStr, dbName) 45 | 46 | store := s.NewMySQL(connectStr, dbName, false) 47 | require.NoError(store.Start(ctx)) 48 | defer func() { 49 | _, err := store.GetDB().Exec("DROP DATABASE " + dbName) 50 | require.NoError(err) 51 | require.NoError(store.Stop(ctx)) 52 | }() 53 | 54 | bp := blocks.NewProtocol(store, epochctx.NewEpochCtx(36, 24, 15, epochctx.FairbankHeight(1000000)), indexprotocol.GravityChain{GravityChainStartHeight: 1}) 55 | p := NewProtocol(store, indexprotocol.HermesConfig{ 56 | HermesContractAddress: "testAddr", 57 | MultiSendContractAddressList: []string{"testAddr"}, 58 | }, epochctx.NewEpochCtx(36, 24, 15, epochctx.FairbankHeight(1000000))) 59 | 60 | require.NoError(bp.CreateTables(ctx)) 61 | require.NoError(p.CreateTables(ctx)) 62 | 63 | chainClient := mock_apiserviceclient.NewMockServiceClient(ctrl) 64 | electionClient := mock_election.NewMockAPIServiceClient(ctrl) 65 | bpctx := indexcontext.WithIndexCtx(context.Background(), indexcontext.IndexCtx{ 66 | ChainClient: chainClient, 67 | ElectionClient: electionClient, 68 | ConsensusScheme: "ROLLDPOS", 69 | }) 70 | 71 | electionClient.EXPECT().GetCandidates(gomock.Any(), gomock.Any()).Times(1).Return( 72 | &api.CandidateResponse{ 73 | Candidates: []*api.Candidate{ 74 | { 75 | Name: "616c6661", 76 | OperatorAddress: testutil.Addr1, 77 | }, 78 | { 79 | Name: "627261766f", 80 | OperatorAddress: testutil.Addr2, 81 | }, 82 | }, 83 | }, nil, 84 | ) 85 | readStateRequest := &iotexapi.ReadStateRequest{ 86 | ProtocolID: []byte(indexprotocol.PollProtocolID), 87 | MethodName: []byte("ActiveBlockProducersByEpoch"), 88 | Arguments: [][]byte{[]byte(strconv.FormatUint(1, 10))}, 89 | } 90 | candidateList := state.CandidateList{ 91 | { 92 | Address: testutil.Addr1, 93 | RewardAddress: testutil.RewardAddr1, 94 | Votes: big.NewInt(100), 95 | }, 96 | { 97 | Address: testutil.Addr2, 98 | RewardAddress: testutil.RewardAddr2, 99 | Votes: big.NewInt(10), 100 | }, 101 | } 102 | data, err := candidateList.Serialize() 103 | require.NoError(err) 104 | chainClient.EXPECT().ReadState(gomock.Any(), readStateRequest).Times(1).Return(&iotexapi.ReadStateResponse{ 105 | Data: data, 106 | }, nil) 107 | 108 | chainClient.EXPECT().ReadContract(gomock.Any(), gomock.Any()).AnyTimes().Return(&iotexapi.ReadContractResponse{ 109 | Receipt: &iotextypes.Receipt{Status: 1}, 110 | Data: "xx", 111 | }, nil) 112 | blk, err := testutil.BuildCompleteBlock(uint64(180), uint64(361)) 113 | require.NoError(err) 114 | 115 | require.NoError(store.Transact(func(tx *sql.Tx) error { 116 | return bp.HandleBlock(bpctx, tx, blk) 117 | })) 118 | 119 | require.NoError(store.Transact(func(tx *sql.Tx) error { 120 | return p.HandleBlock(bpctx, tx, blk) 121 | })) 122 | 123 | // for xrc20 124 | actionHash := blk.Actions[6].Hash() 125 | receiptHash := blk.Receipts[6].Hash() 126 | xrc20History, err := p.getXrc20History("xxxxx") 127 | require.NoError(err) 128 | 129 | require.Equal(hex.EncodeToString(actionHash[:]), xrc20History[0].ActionHash) 130 | require.Equal(hex.EncodeToString(receiptHash[:]), xrc20History[0].ReceiptHash) 131 | require.Equal("xxxxx", xrc20History[0].Address) 132 | 133 | require.Equal(transferSha3, xrc20History[0].Topics) 134 | require.Equal("0000000000000000000000006356908ace09268130dee2b7de643314bbeb3683000000000000000000000000da7e12ef57c236a06117c5e0d04a228e7181cf360000000000000000000000000000000000000000000000000de0b6b3a7640000", xrc20History[0].Data) 135 | require.Equal("100000", xrc20History[0].BlockHeight) 136 | require.Equal("888", xrc20History[0].Index) 137 | require.Equal("failure", xrc20History[0].Status) 138 | 139 | // for xrc 721 140 | actionHash = blk.Actions[7].Hash() 141 | receiptHash = blk.Receipts[7].Hash() 142 | xrc20History, err = getXrc721History(p, "io1xpvzahnl4h46f9ea6u03ec2hkusrzu020th8xx") 143 | require.NoError(err) 144 | 145 | require.Equal(hex.EncodeToString(actionHash[:]), xrc20History[0].ActionHash) 146 | require.Equal(hex.EncodeToString(receiptHash[:]), xrc20History[0].ReceiptHash) 147 | require.Equal("io1xpvzahnl4h46f9ea6u03ec2hkusrzu020th8xx", xrc20History[0].Address) 148 | 149 | // split 256 `topic` to 192 `topic` & 64 `data` 150 | require.Equal("ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ff003f0d751d3a71172f723fbbc4d262dd47adf0", xrc20History[0].Topics) 151 | require.Equal("0000000000000000000000000000000000000000000000000000000000000006", xrc20History[0].Data) 152 | require.Equal("100001", xrc20History[0].BlockHeight) 153 | require.Equal("666", xrc20History[0].Index) 154 | require.Equal("failure", xrc20History[0].Status) 155 | } 156 | 157 | // getActionHistory returns action history by action hash 158 | func getXrc721History(p *Protocol, address string) ([]*Xrc20History, error) { 159 | db := p.Store.GetDB() 160 | 161 | getQuery := fmt.Sprintf(selectXrc20History, "xrc721_history") 162 | stmt, err := db.Prepare(getQuery) 163 | if err != nil { 164 | return nil, errors.Wrap(err, "failed to prepare get query") 165 | } 166 | defer stmt.Close() 167 | 168 | rows, err := stmt.Query(address) 169 | if err != nil { 170 | return nil, errors.Wrap(err, "failed to execute get query") 171 | } 172 | 173 | var xrc20History Xrc20History 174 | parsedRows, err := s.ParseSQLRows(rows, &xrc20History) 175 | if err != nil { 176 | return nil, errors.Wrap(err, "failed to parse results") 177 | } 178 | 179 | if len(parsedRows) == 0 { 180 | return nil, indexprotocol.ErrNotExist 181 | } 182 | ret := make([]*Xrc20History, 0) 183 | for _, parsedRow := range parsedRows { 184 | r := parsedRow.(*Xrc20History) 185 | ret = append(ret, r) 186 | } 187 | 188 | return ret, nil 189 | } 190 | -------------------------------------------------------------------------------- /indexprotocol/blocks/protocol_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 IoTeX 2 | // This is an alpha (internal) release and is not suitable for production. This source code is provided 'as is' and no 3 | // warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent 4 | // permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache 5 | // License 2.0 that can be found in the LICENSE file. 6 | 7 | package blocks 8 | 9 | import ( 10 | "context" 11 | "database/sql" 12 | "encoding/hex" 13 | "math/big" 14 | "strconv" 15 | "testing" 16 | 17 | "github.com/golang/mock/gomock" 18 | "github.com/stretchr/testify/require" 19 | 20 | "github.com/iotexproject/iotex-core/state" 21 | "github.com/iotexproject/iotex-core/test/mock/mock_apiserviceclient" 22 | "github.com/iotexproject/iotex-election/pb/api" 23 | mock_election "github.com/iotexproject/iotex-election/test/mock/mock_apiserviceclient" 24 | "github.com/iotexproject/iotex-proto/golang/iotexapi" 25 | 26 | "github.com/iotexproject/iotex-analytics/epochctx" 27 | "github.com/iotexproject/iotex-analytics/indexcontext" 28 | "github.com/iotexproject/iotex-analytics/indexprotocol" 29 | s "github.com/iotexproject/iotex-analytics/sql" 30 | "github.com/iotexproject/iotex-analytics/testutil" 31 | ) 32 | 33 | const ( 34 | connectStr = "bfe10c7cf8aa29:8bed5959@tcp(us-cdbr-east-04.cleardb.com:3306)/" 35 | dbName = "heroku_067cec75e0ba5ba" 36 | ) 37 | 38 | func TestProtocol(t *testing.T) { 39 | ctrl := gomock.NewController(t) 40 | defer ctrl.Finish() 41 | 42 | require := require.New(t) 43 | ctx := context.Background() 44 | 45 | testutil.CleanupDatabase(t, connectStr, dbName) 46 | 47 | store := s.NewMySQL(connectStr, dbName, false) 48 | require.NoError(store.Start(ctx)) 49 | defer func() { 50 | _, err := store.GetDB().Exec("DROP DATABASE " + dbName) 51 | require.NoError(err) 52 | require.NoError(store.Stop(ctx)) 53 | }() 54 | 55 | p := NewProtocol(store, epochctx.NewEpochCtx(36, 24, 15, epochctx.FairbankHeight(1000000)), indexprotocol.GravityChain{GravityChainStartHeight: 1}) 56 | 57 | require.NoError(p.CreateTables(ctx)) 58 | 59 | blk1, err := testutil.BuildCompleteBlock(uint64(1), uint64(2)) 60 | require.NoError(err) 61 | 62 | chainClient := mock_apiserviceclient.NewMockServiceClient(ctrl) 63 | electionClient := mock_election.NewMockAPIServiceClient(ctrl) 64 | ctx = indexcontext.WithIndexCtx(context.Background(), indexcontext.IndexCtx{ 65 | ChainClient: chainClient, 66 | ElectionClient: electionClient, 67 | ConsensusScheme: "ROLLDPOS", 68 | }) 69 | 70 | electionClient.EXPECT().GetCandidates(gomock.Any(), gomock.Any()).Times(1).Return( 71 | &api.CandidateResponse{ 72 | Candidates: []*api.Candidate{ 73 | { 74 | Name: "616c6661", 75 | OperatorAddress: testutil.Addr1, 76 | }, 77 | { 78 | Name: "627261766f", 79 | OperatorAddress: testutil.Addr2, 80 | }, 81 | }, 82 | }, nil, 83 | ) 84 | readStateRequest := &iotexapi.ReadStateRequest{ 85 | ProtocolID: []byte(indexprotocol.PollProtocolID), 86 | MethodName: []byte("ActiveBlockProducersByEpoch"), 87 | Arguments: [][]byte{[]byte(strconv.FormatUint(1, 10))}, 88 | } 89 | candidateList := state.CandidateList{ 90 | { 91 | Address: testutil.Addr1, 92 | RewardAddress: testutil.RewardAddr1, 93 | Votes: big.NewInt(100), 94 | }, 95 | { 96 | Address: testutil.Addr2, 97 | RewardAddress: testutil.RewardAddr2, 98 | Votes: big.NewInt(10), 99 | }, 100 | } 101 | data, err := candidateList.Serialize() 102 | require.NoError(err) 103 | chainClient.EXPECT().ReadState(gomock.Any(), readStateRequest).Times(1).Return(&iotexapi.ReadStateResponse{ 104 | Data: data, 105 | }, nil) 106 | 107 | require.NoError(store.Transact(func(tx *sql.Tx) error { 108 | return p.HandleBlock(ctx, tx, blk1) 109 | })) 110 | 111 | blk2, err := testutil.BuildEmptyBlock(2) 112 | require.NoError(err) 113 | 114 | readStateRequest = &iotexapi.ReadStateRequest{ 115 | ProtocolID: []byte(indexprotocol.PollProtocolID), 116 | MethodName: []byte("ActiveBlockProducersByEpoch"), 117 | Arguments: [][]byte{[]byte(strconv.FormatUint(2, 10))}, 118 | } 119 | 120 | require.NoError(store.Transact(func(tx *sql.Tx) error { 121 | return p.HandleBlock(ctx, tx, blk2) 122 | })) 123 | 124 | blockHistory, err := p.getBlockHistory(uint64(1)) 125 | require.NoError(err) 126 | 127 | blk1Hash := blk1.HashBlock() 128 | require.Equal(uint64(1), blockHistory.EpochNumber) 129 | require.Equal(hex.EncodeToString(blk1Hash[:]), blockHistory.BlockHash) 130 | require.Equal("616c6661", blockHistory.ProducerName) 131 | require.Equal("627261766f", blockHistory.ExpectedProducerName) 132 | } 133 | -------------------------------------------------------------------------------- /indexprotocol/protocol.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 IoTeX 2 | // This is an alpha (internal) release and is not suitable for production. This source code is provided 'as is' and no 3 | // warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent 4 | // permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache 5 | // License 2.0 that can be found in the LICENSE file. 6 | 7 | package indexprotocol 8 | 9 | import ( 10 | "context" 11 | "database/sql" 12 | "encoding/hex" 13 | "fmt" 14 | "strconv" 15 | "strings" 16 | 17 | "github.com/golang/protobuf/proto" 18 | "github.com/pkg/errors" 19 | 20 | "github.com/iotexproject/iotex-core/blockchain/block" 21 | "github.com/iotexproject/iotex-proto/golang/iotexapi" 22 | "github.com/iotexproject/iotex-proto/golang/iotextypes" 23 | ) 24 | 25 | const ( 26 | // PollProtocolID is ID of poll protocol 27 | PollProtocolID = "poll" 28 | protocolID = "staking" 29 | readBucketsLimit = 30000 30 | readCandidatesLimit = 20000 31 | ) 32 | 33 | var ( 34 | // ErrNotExist indicates certain item does not exist in Blockchain database 35 | ErrNotExist = errors.New("not exist in DB") 36 | // ErrAlreadyExist indicates certain item already exists in Blockchain database 37 | ErrAlreadyExist = errors.New("already exist in DB") 38 | // ErrUnimplemented indicates a method is not implemented yet 39 | ErrUnimplemented = errors.New("method is unimplemented") 40 | ) 41 | 42 | // Genesis defines the genesis configurations that should be recorded by the corresponding protocol before 43 | // indexing the first block 44 | type ( 45 | Genesis struct { 46 | Account `yaml:"account"` 47 | } 48 | // Account contains the configs for account protocol 49 | Account struct { 50 | // InitBalanceMap is the address and initial balance mapping before the first block. 51 | InitBalanceMap map[string]string `yaml:"initBalances"` 52 | } 53 | //Poll contains the configs for voting protocol 54 | Poll struct { 55 | SkipManifiedCandidate bool `yaml:"skipManifiedCandidate"` 56 | VoteThreshold string `yaml:"voteThreshold"` 57 | ScoreThreshold string `yaml:"scoreThreshold"` 58 | SelfStakingThreshold string `yaml:"selfStakingThreshold"` 59 | } 60 | // GravityChain contains the configs for gravity chain 61 | GravityChain struct { 62 | GravityChainStartHeight uint64 `yaml:"gravityChainStartHeight"` 63 | GravityChainAPIs []string `yaml:"gravityChainAPIs"` 64 | RegisterContractAddress string `yaml:"registerContractAddress"` 65 | RewardPercentageStartHeight uint64 `yaml:"rewardPercentageStartHeight"` 66 | } 67 | // Rewarding contains the configs for rewarding 68 | Rewarding struct { 69 | NumDelegatesForEpochReward uint64 `yaml:"numDelegatesForEpochReward"` 70 | NumDelegatesForFoundationBonus uint64 `yaml:"numDelegatesForFoundationBonus"` 71 | ProductivityThreshold uint64 `yaml:"productivityThreshold"` 72 | ExemptCandidatesFromEpochReward []string `yaml:"exemptCandidatesFromEpochReward"` 73 | } 74 | // HermesConfig defines hermes addr 75 | HermesConfig struct { 76 | HermesContractAddress string `yaml:"hermesContractAddress"` 77 | MultiSendContractAddressList []string `yaml:"multiSendContractAddressList"` 78 | } 79 | // VoteWeightCalConsts is for staking 80 | VoteWeightCalConsts struct { 81 | DurationLg float64 `yaml:"durationLg"` 82 | AutoStake float64 `yaml:"autoStake"` 83 | SelfStake float64 `yaml:"selfStake"` 84 | } 85 | // RewardPortionCfg is contains the configs for rewarding portion contract 86 | RewardPortionCfg struct { 87 | RewardPortionContract string `yaml:"rewardPortionContract"` 88 | RewardPortionContractDeployHeight uint64 `yaml:"rewardportionContractDeployHeight"` 89 | } 90 | ) 91 | 92 | // Protocol defines the protocol interfaces for block indexer 93 | type Protocol interface { 94 | BlockHandler 95 | CreateTables(context.Context) error 96 | Initialize(context.Context, *sql.Tx, *Genesis) error 97 | } 98 | 99 | // BlockHandler is the interface of handling block 100 | type BlockHandler interface { 101 | HandleBlock(context.Context, *sql.Tx, *block.Block) error 102 | } 103 | 104 | // GetGravityChainStartHeight get gravity chain start height 105 | func GetGravityChainStartHeight( 106 | chainClient iotexapi.APIServiceClient, 107 | height uint64, 108 | ) (uint64, error) { 109 | readStateRequest := &iotexapi.ReadStateRequest{ 110 | ProtocolID: []byte(PollProtocolID), 111 | MethodName: []byte("GetGravityChainStartHeight"), 112 | Arguments: [][]byte{[]byte(strconv.FormatUint(height, 10))}, 113 | } 114 | readStateRes, err := chainClient.ReadState(context.Background(), readStateRequest) 115 | if err != nil { 116 | return uint64(0), errors.Wrap(err, "failed to get gravity chain start height") 117 | } 118 | gravityChainStartHeight, err := strconv.ParseUint(string(readStateRes.GetData()), 10, 64) 119 | if err != nil { 120 | return uint64(0), errors.Wrap(err, "failed to parse gravityChainStartHeight") 121 | } 122 | return gravityChainStartHeight, nil 123 | } 124 | 125 | // GetAllStakingBuckets get all buckets by height 126 | func GetAllStakingBuckets(chainClient iotexapi.APIServiceClient, height uint64) (voteBucketListAll *iotextypes.VoteBucketList, err error) { 127 | voteBucketListAll = &iotextypes.VoteBucketList{} 128 | for i := uint32(0); ; i++ { 129 | offset := i * readBucketsLimit 130 | size := uint32(readBucketsLimit) 131 | voteBucketList, err := getStakingBuckets(chainClient, offset, size, height) 132 | if err != nil { 133 | return nil, errors.Wrap(err, "failed to get bucket") 134 | } 135 | for _, bucket := range voteBucketList.Buckets { 136 | if bucket.UnstakeStartTime.AsTime().After(bucket.StakeStartTime.AsTime()) { 137 | continue 138 | } 139 | voteBucketListAll.Buckets = append(voteBucketListAll.Buckets, bucket) 140 | } 141 | if len(voteBucketList.Buckets) < readBucketsLimit { 142 | break 143 | } 144 | } 145 | return 146 | } 147 | 148 | // getStakingBuckets get specific buckets by height 149 | func getStakingBuckets(chainClient iotexapi.APIServiceClient, offset, limit uint32, height uint64) (voteBucketList *iotextypes.VoteBucketList, err error) { 150 | methodName, err := proto.Marshal(&iotexapi.ReadStakingDataMethod{ 151 | Method: iotexapi.ReadStakingDataMethod_BUCKETS, 152 | }) 153 | if err != nil { 154 | return nil, err 155 | } 156 | arg, err := proto.Marshal(&iotexapi.ReadStakingDataRequest{ 157 | Request: &iotexapi.ReadStakingDataRequest_Buckets{ 158 | Buckets: &iotexapi.ReadStakingDataRequest_VoteBuckets{ 159 | Pagination: &iotexapi.PaginationParam{ 160 | Offset: offset, 161 | Limit: limit, 162 | }, 163 | }, 164 | }, 165 | }) 166 | if err != nil { 167 | return nil, err 168 | } 169 | readStateRequest := &iotexapi.ReadStateRequest{ 170 | ProtocolID: []byte(protocolID), 171 | MethodName: methodName, 172 | Arguments: [][]byte{arg}, 173 | Height: fmt.Sprintf("%d", height), 174 | } 175 | ctx := context.WithValue(context.Background(), &iotexapi.ReadStateRequest{}, iotexapi.ReadStakingDataMethod_BUCKETS) 176 | readStateRes, err := chainClient.ReadState(ctx, readStateRequest) 177 | if err != nil { 178 | return 179 | } 180 | voteBucketList = &iotextypes.VoteBucketList{} 181 | if err := proto.Unmarshal(readStateRes.GetData(), voteBucketList); err != nil { 182 | return nil, errors.Wrap(err, "failed to unmarshal VoteBucketList") 183 | } 184 | return 185 | } 186 | 187 | // GetAllStakingCandidates get all candidates by height 188 | func GetAllStakingCandidates(chainClient iotexapi.APIServiceClient, height uint64) (candidateListAll *iotextypes.CandidateListV2, err error) { 189 | candidateListAll = &iotextypes.CandidateListV2{} 190 | for i := uint32(0); ; i++ { 191 | offset := i * readCandidatesLimit 192 | size := uint32(readCandidatesLimit) 193 | candidateList, err := getStakingCandidates(chainClient, offset, size, height) 194 | if err != nil { 195 | return nil, errors.Wrap(err, "failed to get candidates") 196 | } 197 | // filter out candidates whose master bucket are unstaked/withdrawn 198 | for _, c := range candidateList.Candidates { 199 | if c.SelfStakingTokens != "0" { 200 | candidateListAll.Candidates = append(candidateListAll.Candidates, c) 201 | } 202 | } 203 | if len(candidateList.Candidates) < readCandidatesLimit { 204 | break 205 | } 206 | } 207 | return 208 | } 209 | 210 | // getStakingCandidates get specific candidates by height 211 | func getStakingCandidates(chainClient iotexapi.APIServiceClient, offset, limit uint32, height uint64) (candidateList *iotextypes.CandidateListV2, err error) { 212 | methodName, err := proto.Marshal(&iotexapi.ReadStakingDataMethod{ 213 | Method: iotexapi.ReadStakingDataMethod_CANDIDATES, 214 | }) 215 | if err != nil { 216 | return nil, err 217 | } 218 | arg, err := proto.Marshal(&iotexapi.ReadStakingDataRequest{ 219 | Request: &iotexapi.ReadStakingDataRequest_Candidates_{ 220 | Candidates: &iotexapi.ReadStakingDataRequest_Candidates{ 221 | Pagination: &iotexapi.PaginationParam{ 222 | Offset: offset, 223 | Limit: limit, 224 | }, 225 | }, 226 | }, 227 | }) 228 | if err != nil { 229 | return nil, err 230 | } 231 | readStateRequest := &iotexapi.ReadStateRequest{ 232 | ProtocolID: []byte(protocolID), 233 | MethodName: methodName, 234 | Arguments: [][]byte{arg}, 235 | Height: fmt.Sprintf("%d", height), 236 | } 237 | ctx := context.WithValue(context.Background(), &iotexapi.ReadStateRequest{}, iotexapi.ReadStakingDataMethod_CANDIDATES) 238 | readStateRes, err := chainClient.ReadState(ctx, readStateRequest) 239 | if err != nil { 240 | return 241 | } 242 | candidateList = &iotextypes.CandidateListV2{} 243 | if err := proto.Unmarshal(readStateRes.GetData(), candidateList); err != nil { 244 | return nil, errors.Wrap(err, "failed to unmarshal VoteBucketList") 245 | } 246 | return 247 | } 248 | 249 | // EncodeDelegateName converts a delegate name input to an internal format 250 | func EncodeDelegateName(name string) (string, error) { 251 | l := len(name) 252 | switch { 253 | case l == 24: 254 | return name, nil 255 | case l <= 12: 256 | prefixZeros := []byte{} 257 | for i := 0; i < 12-len(name); i++ { 258 | prefixZeros = append(prefixZeros, byte(0)) 259 | } 260 | suffixZeros := []byte{} 261 | for strings.HasSuffix(name, "#") { 262 | name = strings.TrimSuffix(name, "#") 263 | suffixZeros = append(suffixZeros, byte(0)) 264 | } 265 | return hex.EncodeToString(append(append(prefixZeros, []byte(name)...), suffixZeros...)), nil 266 | } 267 | return "", errors.Errorf("invalid length %d", l) 268 | } 269 | 270 | // DecodeDelegateName converts format to readable delegate name 271 | func DecodeDelegateName(name string) (string, error) { 272 | suffix := "" 273 | for strings.HasSuffix(name, "00") { 274 | name = strings.TrimSuffix(name, "00") 275 | suffix += "#" 276 | } 277 | aliasBytes, err := hex.DecodeString(strings.TrimLeft(name, "0")) 278 | if err != nil { 279 | return "", err 280 | } 281 | aliasString := string(aliasBytes) + suffix 282 | return aliasString, nil 283 | } 284 | -------------------------------------------------------------------------------- /indexprotocol/protocol_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 IoTeX 2 | // This is an alpha (internal) release and is not suitable for production. This source code is provided 'as is' and no 3 | // warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent 4 | // permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache 5 | // License 2.0 that can be found in the LICENSE file. 6 | 7 | package indexprotocol 8 | 9 | import ( 10 | "testing" 11 | 12 | "github.com/stretchr/testify/require" 13 | "google.golang.org/grpc" 14 | 15 | "github.com/iotexproject/iotex-proto/golang/iotexapi" 16 | ) 17 | 18 | const candidateName = "726f626f7462703030303030" 19 | 20 | func TestEnDecodeName(t *testing.T) { 21 | require := require.New(t) 22 | decoded, err := DecodeDelegateName(candidateName) 23 | require.NoError(err) 24 | 25 | encoded, err := EncodeDelegateName(decoded) 26 | require.NoError(err) 27 | require.Equal(candidateName, encoded) 28 | } 29 | 30 | func TestActiveDelegates(t *testing.T) { 31 | require := require.New(t) 32 | 33 | conn, err := grpc.Dial("api.iotex.one:80", grpc.WithBlock(), grpc.WithInsecure()) 34 | require.NoError(err) 35 | defer conn.Close() 36 | cli := iotexapi.NewAPIServiceClient(conn) 37 | 38 | ad := make(map[uint64]map[string]bool) 39 | for _, i := range []uint64{11409481, 11410201, 11410921} { 40 | candidateList, err := GetAllStakingCandidates(cli, i) 41 | require.NoError(err) 42 | ad[i] = make(map[string]bool) 43 | for _, c := range candidateList.Candidates { 44 | ad[i][c.Name] = true 45 | } 46 | } 47 | require.Equal(3, len(ad)) 48 | 49 | // active 50 | active := []string{ 51 | "nodeasy", "rocketfuel", "tgb", "capitmu", "pubxpayments", "yvalidator", "consensusnet", 52 | "zhcapital", "metanyx", "bcf", "binancevote", "thebottoken", "iotfi", "slowmist", "blockboost", 53 | "iosg", "hashquark", "longz", "cryptozoo", "hofancrypto", "satoshi", "gamefantasy", "bittaker", 54 | "swft", "iotask", "infstones", "blockfolio", "staking4all", "rockx", "elitex", "eatliverun", 55 | "cobo", "cpc", "iotexcore", "iotexlab", "hackster", "coingecko", "matrix", "chainshield", 56 | "iotexteam", "hashbuy", "satoshimusk", "sesameseed", "iotexicu", "royalland", "a4x", "smartstake", 57 | "coredev", "ducapital", "hotbit", "pnp", "emmasiotx", "mrtrump", "enlightiv", "keys", "wetez", 58 | "droute", "iotexunion", "xeon", 59 | } 60 | // these delegates are no longer active 61 | inactive := []string{ 62 | "ratel", "square2", "citex2018", "offline", "iotxplorerio", 63 | "ra", "kita", "draperdragon", "airfoil", "square4", 64 | } 65 | 66 | for _, m := range ad { 67 | for _, name := range active { 68 | require.True(m[name]) 69 | } 70 | for _, name := range inactive { 71 | require.False(m[name]) 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /indexprotocol/registry.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 IoTeX 2 | // This is an alpha (internal) release and is not suitable for production. This source code is provided 'as is' and no 3 | // warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent 4 | // permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache 5 | // License 2.0 that can be found in the LICENSE file. 6 | 7 | package indexprotocol 8 | 9 | import ( 10 | "sync" 11 | 12 | "github.com/pkg/errors" 13 | 14 | "github.com/iotexproject/iotex-core/pkg/log" 15 | ) 16 | 17 | // Registry is the hub of all protocols deployed on the chain 18 | type Registry struct { 19 | protocols sync.Map 20 | } 21 | 22 | // Register registers the protocol with a unique ID 23 | func (r *Registry) Register(id string, p Protocol) error { 24 | _, loaded := r.protocols.LoadOrStore(id, p) 25 | if loaded { 26 | return errors.Errorf("Protocol with ID %s is already registered", id) 27 | } 28 | return nil 29 | } 30 | 31 | // ForceRegister registers the protocol with a unique ID and force replacing the previous protocol if it exists 32 | func (r *Registry) ForceRegister(id string, p Protocol) error { 33 | r.protocols.Store(id, p) 34 | return nil 35 | } 36 | 37 | // Find finds a protocol by ID 38 | func (r *Registry) Find(id string) (Protocol, bool) { 39 | value, ok := r.protocols.Load(id) 40 | if !ok { 41 | return nil, false 42 | } 43 | p, ok := value.(Protocol) 44 | if !ok { 45 | log.S().Panic("Registry stores the item which is not a protocol") 46 | } 47 | return p, true 48 | } 49 | 50 | // All returns all protocols 51 | func (r *Registry) All() []Protocol { 52 | all := make([]Protocol, 0) 53 | r.protocols.Range(func(_, value interface{}) bool { 54 | p, ok := value.(Protocol) 55 | if !ok { 56 | log.S().Panic("Registry stores the item which is not a protocol") 57 | } 58 | all = append(all, p) 59 | return true 60 | }) 61 | return all 62 | } 63 | -------------------------------------------------------------------------------- /indexprotocol/rewards/protocol_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 IoTeX 2 | // This is an alpha (internal) release and is not suitable for production. This source code is provided 'as is' and no 3 | // warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent 4 | // permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache 5 | // License 2.0 that can be found in the LICENSE file. 6 | 7 | package rewards 8 | 9 | import ( 10 | "context" 11 | "database/sql" 12 | "encoding/hex" 13 | "testing" 14 | 15 | "github.com/golang/mock/gomock" 16 | "github.com/stretchr/testify/require" 17 | 18 | "github.com/iotexproject/iotex-core/test/mock/mock_apiserviceclient" 19 | "github.com/iotexproject/iotex-election/pb/api" 20 | mock_election "github.com/iotexproject/iotex-election/test/mock/mock_apiserviceclient" 21 | 22 | "github.com/iotexproject/iotex-analytics/epochctx" 23 | "github.com/iotexproject/iotex-analytics/indexcontext" 24 | "github.com/iotexproject/iotex-analytics/indexprotocol" 25 | s "github.com/iotexproject/iotex-analytics/sql" 26 | "github.com/iotexproject/iotex-analytics/testutil" 27 | ) 28 | 29 | const ( 30 | connectStr = "bfe10c7cf8aa29:8bed5959@tcp(us-cdbr-east-04.cleardb.com:3306)/" 31 | dbName = "heroku_067cec75e0ba5ba" 32 | ) 33 | 34 | func TestProtocol(t *testing.T) { 35 | ctrl := gomock.NewController(t) 36 | defer ctrl.Finish() 37 | 38 | require := require.New(t) 39 | ctx := context.Background() 40 | 41 | testutil.CleanupDatabase(t, connectStr, dbName) 42 | 43 | store := s.NewMySQL(connectStr, dbName, false) 44 | require.NoError(store.Start(ctx)) 45 | defer func() { 46 | _, err := store.GetDB().Exec("DROP DATABASE " + dbName) 47 | require.NoError(err) 48 | require.NoError(store.Stop(ctx)) 49 | }() 50 | 51 | p := NewProtocol(store, epochctx.NewEpochCtx(1, 1, 1, epochctx.FairbankHeight(100000)), indexprotocol.Rewarding{}, indexprotocol.GravityChain{GravityChainStartHeight: 1}) 52 | 53 | require.NoError(p.CreateTables(ctx)) 54 | 55 | blk1, err := testutil.BuildCompleteBlock(uint64(1), uint64(2)) 56 | require.NoError(err) 57 | 58 | chainClient := mock_apiserviceclient.NewMockServiceClient(ctrl) 59 | electionClient := mock_election.NewMockAPIServiceClient(ctrl) 60 | ctx = indexcontext.WithIndexCtx(context.Background(), indexcontext.IndexCtx{ 61 | ChainClient: chainClient, 62 | ElectionClient: electionClient, 63 | ConsensusScheme: "ROLLDPOS", 64 | }) 65 | 66 | electionClient.EXPECT().GetCandidates(gomock.Any(), gomock.Any()).Times(1).Return( 67 | &api.CandidateResponse{ 68 | Candidates: []*api.Candidate{ 69 | { 70 | Name: "616c6661", 71 | RewardAddress: testutil.RewardAddr1, 72 | }, 73 | { 74 | Name: "627261766f", 75 | RewardAddress: testutil.RewardAddr2, 76 | }, 77 | { 78 | Name: "636861726c6965", 79 | RewardAddress: testutil.RewardAddr3, 80 | }, 81 | }, 82 | }, nil, 83 | ) 84 | 85 | require.NoError(store.Transact(func(tx *sql.Tx) error { 86 | return p.HandleBlock(ctx, tx, blk1) 87 | })) 88 | 89 | actionHash1 := blk1.Actions[4].Hash() 90 | rewardHistoryList, err := p.getRewardHistory(hex.EncodeToString(actionHash1[:])) 91 | require.NoError(err) 92 | require.Equal(1, len(rewardHistoryList)) 93 | require.Equal(uint64(1), rewardHistoryList[0].EpochNumber) 94 | require.Equal("616c6661", rewardHistoryList[0].CandidateName) 95 | require.Equal(testutil.RewardAddr1, rewardHistoryList[0].RewardAddress) 96 | require.Equal("16", rewardHistoryList[0].BlockReward) 97 | require.Equal("0", rewardHistoryList[0].EpochReward) 98 | require.Equal("0", rewardHistoryList[0].FoundationBonus) 99 | 100 | actionHash2 := blk1.Actions[5].Hash() 101 | rewardHistoryList, err = p.getRewardHistory(hex.EncodeToString(actionHash2[:])) 102 | require.NoError(err) 103 | require.Equal(3, len(rewardHistoryList)) 104 | } 105 | -------------------------------------------------------------------------------- /indexprotocol/votings/bucketoperator.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 IoTeX 2 | // This is an alpha (internal) release and is not suitable for production. This source code is provided 'as is' and no 3 | // warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent 4 | // permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache 5 | // License 2.0 that can be found in the LICENSE file. 6 | 7 | package votings 8 | 9 | import ( 10 | "database/sql" 11 | "encoding/hex" 12 | "fmt" 13 | "math/big" 14 | "reflect" 15 | "strconv" 16 | "strings" 17 | "time" 18 | 19 | "github.com/golang/protobuf/proto" 20 | "github.com/golang/protobuf/ptypes" 21 | "github.com/pkg/errors" 22 | 23 | "github.com/iotexproject/go-pkgs/hash" 24 | "github.com/iotexproject/iotex-analytics/indexprotocol" 25 | s "github.com/iotexproject/iotex-analytics/sql" 26 | "github.com/iotexproject/iotex-election/committee" 27 | "github.com/iotexproject/iotex-proto/golang/iotextypes" 28 | ) 29 | 30 | const ( 31 | bucketCreationSQLITE = "CREATE TABLE IF NOT EXISTS %s (id INTEGER PRIMARY KEY AUTOINCREMENT, hash TEXT UNIQUE, index DECIMAL(65, 0), candidate TEXT, owner TEXT, staked_amount BLOB, staked_duration TEXT, create_time TIMESTAMP NULL DEFAULT NULL, stake_start_time TIMESTAMP NULL DEFAULT NULL, unstake_start_time TIMESTAMP NULL DEFAULT NULL, auto_stake INTEGER, KEY `owner_index` (`owner`), KEY `candidate_index` (`candidate`))" 32 | bucketCreationMySQL = "CREATE TABLE IF NOT EXISTS %s (id INTEGER PRIMARY KEY AUTO_INCREMENT, hash VARCHAR(64) UNIQUE, `index` DECIMAL(65, 0), candidate VARCHAR(41), owner VARCHAR(41), staked_amount BLOB, staked_duration TEXT, create_time TIMESTAMP NULL DEFAULT NULL, stake_start_time TIMESTAMP NULL DEFAULT NULL, unstake_start_time TIMESTAMP NULL DEFAULT NULL, auto_stake INTEGER, KEY `owner_index` (`owner`), KEY `candidate_index` (`candidate`))" 33 | 34 | // InsertVoteBucketsQuery is query to insert vote buckets for sqlite 35 | InsertVoteBucketsQuery = "INSERT OR IGNORE INTO %s (hash, index, candidate, owner, staked_amount, staked_duration, create_time, stake_start_time, unstake_start_time, auto_stake) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" 36 | // InsertVoteBucketsQueryMySQL is query to insert vote buckets for mysql 37 | InsertVoteBucketsQueryMySQL = "INSERT IGNORE INTO %s (hash, `index`, candidate, owner, staked_amount, staked_duration, create_time, stake_start_time, unstake_start_time, auto_stake) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" 38 | // VoteBucketRecordQuery is query to return vote buckets by ids 39 | VoteBucketRecordQuery = "SELECT id, `index`, candidate, owner, staked_amount, staked_duration, create_time, stake_start_time, unstake_start_time, auto_stake FROM %s WHERE id IN (%s)" 40 | ) 41 | 42 | type ( 43 | // StakingBucket is the data structure of staking bucket 44 | StakingBucket struct { 45 | ID int64 46 | Index uint64 47 | Candidate, Owner string 48 | StakedAmount []byte 49 | StakedDuration string 50 | CreateTime, StakeStartTime, UnstakeStartTime time.Time 51 | AutoStake int64 52 | } 53 | ) 54 | 55 | // BucketRecordQuery is query to return buckets by ids 56 | const BucketRecordQuery = "SELECT id, start_time, duration, amount, decay, voter, candidate FROM %s WHERE id IN (%s)" 57 | 58 | // NewBucketTableOperator creates an operator for vote bucket table 59 | func NewBucketTableOperator(tableName string, driverName committee.DRIVERTYPE) (committee.Operator, error) { 60 | var creation string 61 | switch driverName { 62 | case committee.SQLITE: 63 | creation = bucketCreationSQLITE 64 | case committee.MYSQL: 65 | creation = bucketCreationMySQL 66 | default: 67 | return nil, errors.New("Wrong driver type") 68 | } 69 | return committee.NewRecordTableOperator( 70 | tableName, 71 | driverName, 72 | InsertVoteBuckets, 73 | QueryVoteBuckets, 74 | creation, 75 | ) 76 | } 77 | 78 | // QueryVoteBuckets returns vote buckets by ids 79 | func QueryVoteBuckets(tableName string, frequencies map[int64]int, sdb *sql.DB, tx *sql.Tx) (ret interface{}, err error) { 80 | size := 0 81 | ids := make([]int64, 0, len(frequencies)) 82 | for id, f := range frequencies { 83 | ids = append(ids, id) 84 | size += f 85 | } 86 | var rows *sql.Rows 87 | if tx != nil { 88 | rows, err = tx.Query(fmt.Sprintf(VoteBucketRecordQuery, tableName, atos(ids))) 89 | } else { 90 | rows, err = sdb.Query(fmt.Sprintf(VoteBucketRecordQuery, tableName, atos(ids))) 91 | } 92 | if err != nil { 93 | return nil, err 94 | } 95 | if err := rows.Err(); err != nil { 96 | return nil, err 97 | } 98 | defer rows.Close() 99 | mp, err := ParseBuckets(rows) 100 | if err != nil { 101 | return nil, err 102 | } 103 | buckets := make([]*iotextypes.VoteBucket, 0) 104 | for id, bucket := range mp { 105 | for i := frequencies[id]; i > 0; i-- { 106 | buckets = append(buckets, bucket) 107 | } 108 | } 109 | 110 | return &iotextypes.VoteBucketList{Buckets: buckets}, nil 111 | } 112 | 113 | // ParseBuckets parse buckets to vote bucket list 114 | func ParseBuckets(rows *sql.Rows) (map[int64]*iotextypes.VoteBucket, error) { 115 | var b StakingBucket 116 | parsedRows, err := s.ParseSQLRows(rows, &b) 117 | if err != nil { 118 | return nil, errors.Wrap(err, "failed to parse results") 119 | } 120 | 121 | if len(parsedRows) == 0 { 122 | return nil, indexprotocol.ErrNotExist 123 | } 124 | buckets := map[int64]*iotextypes.VoteBucket{} 125 | for _, parsedRow := range parsedRows { 126 | b, ok := parsedRow.(*StakingBucket) 127 | if !ok { 128 | return nil, errors.New("failed to convert") 129 | } 130 | duration, err := strconv.ParseUint(b.StakedDuration, 10, 32) 131 | if err != nil { 132 | return nil, err 133 | } 134 | createTime, err := ptypes.TimestampProto(b.CreateTime) 135 | if err != nil { 136 | return nil, err 137 | } 138 | stakeTime, err := ptypes.TimestampProto(b.StakeStartTime) 139 | if err != nil { 140 | return nil, err 141 | } 142 | unstakeTime, err := ptypes.TimestampProto(b.UnstakeStartTime) 143 | if err != nil { 144 | return nil, err 145 | } 146 | bucket := &iotextypes.VoteBucket{ 147 | Index: b.Index, 148 | CandidateAddress: string(b.Candidate), 149 | Owner: string(b.Owner), 150 | StakedAmount: string(b.StakedAmount), 151 | StakedDuration: uint32(duration), 152 | CreateTime: createTime, 153 | StakeStartTime: stakeTime, 154 | UnstakeStartTime: unstakeTime, 155 | AutoStake: b.AutoStake == 1, 156 | } 157 | buckets[b.ID] = bucket 158 | } 159 | 160 | return buckets, nil 161 | } 162 | 163 | // InsertVoteBuckets inserts vote bucket records into table by tx 164 | func InsertVoteBuckets(tableName string, driverName committee.DRIVERTYPE, records interface{}, tx *sql.Tx) (frequencies map[hash.Hash256]int, err error) { 165 | buckets, ok := records.(*iotextypes.VoteBucketList) 166 | if !ok { 167 | return nil, errors.Errorf("invalid record type %s, *types.Bucket expected", reflect.TypeOf(records)) 168 | } 169 | if buckets == nil { 170 | return nil, nil 171 | } 172 | var stmt *sql.Stmt 173 | switch driverName { 174 | case committee.SQLITE: 175 | stmt, err = tx.Prepare(fmt.Sprintf(InsertVoteBucketsQuery, tableName)) 176 | case committee.MYSQL: 177 | stmt, err = tx.Prepare(fmt.Sprintf(InsertVoteBucketsQueryMySQL, tableName)) 178 | default: 179 | return nil, errors.New("wrong driver type") 180 | } 181 | if err != nil { 182 | return nil, err 183 | } 184 | defer func() { 185 | closeErr := stmt.Close() 186 | if err == nil && closeErr != nil { 187 | err = closeErr 188 | } 189 | }() 190 | frequencies = make(map[hash.Hash256]int) 191 | for _, bucket := range buckets.Buckets { 192 | h, err := hashBucket(bucket) 193 | if err != nil { 194 | return nil, err 195 | } 196 | if f, ok := frequencies[h]; ok { 197 | frequencies[h] = f + 1 198 | } else { 199 | frequencies[h] = 1 200 | } 201 | duration := big.NewInt(0).SetUint64(uint64(bucket.StakedDuration)) 202 | ct := time.Unix(bucket.CreateTime.Seconds, int64(bucket.CreateTime.Nanos)) 203 | sst := time.Unix(bucket.StakeStartTime.Seconds, int64(bucket.StakeStartTime.Nanos)) 204 | ust := time.Unix(bucket.UnstakeStartTime.Seconds, int64(bucket.UnstakeStartTime.Nanos)) 205 | if _, err = stmt.Exec( 206 | hex.EncodeToString(h[:]), 207 | bucket.Index, 208 | bucket.CandidateAddress, 209 | bucket.Owner, 210 | []byte(bucket.StakedAmount), 211 | duration.String(), 212 | ct, 213 | sst, 214 | ust, 215 | bucket.AutoStake, 216 | ); err != nil { 217 | return nil, err 218 | } 219 | } 220 | 221 | return frequencies, nil 222 | } 223 | 224 | func atos(a []int64) string { 225 | if len(a) == 0 { 226 | return "" 227 | } 228 | 229 | b := make([]string, len(a)) 230 | for i, v := range a { 231 | b[i] = strconv.FormatInt(v, 10) 232 | } 233 | return strings.Join(b, ",") 234 | } 235 | 236 | func hashBucket(bucket *iotextypes.VoteBucket) (hash.Hash256, error) { 237 | data, err := proto.Marshal(bucket) 238 | if err != nil { 239 | return hash.ZeroHash256, err 240 | } 241 | return hash.Hash256b(data), nil 242 | } 243 | -------------------------------------------------------------------------------- /indexprotocol/votings/candidateoperator.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 IoTeX 2 | // This is an alpha (internal) release and is not suitable for production. This source code is provided 'as is' and no 3 | // warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent 4 | // permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache 5 | // License 2.0 that can be found in the LICENSE file. 6 | 7 | package votings 8 | 9 | import ( 10 | "database/sql" 11 | "encoding/hex" 12 | "fmt" 13 | "reflect" 14 | 15 | "github.com/golang/protobuf/proto" 16 | "github.com/pkg/errors" 17 | 18 | "github.com/iotexproject/go-pkgs/hash" 19 | "github.com/iotexproject/iotex-election/committee" 20 | "github.com/iotexproject/iotex-proto/golang/iotextypes" 21 | 22 | "github.com/iotexproject/iotex-analytics/indexprotocol" 23 | s "github.com/iotexproject/iotex-analytics/sql" 24 | ) 25 | 26 | const ( 27 | candidateCreationSQLITE = "CREATE TABLE IF NOT EXISTS %s (id INTEGER PRIMARY KEY AUTOINCREMENT, hash TEXT UNIQUE, owner BLOB, operator BLOB, reward BLOB, name BLOB, votes BLOB, self_stake_bucket_idx DECIMAL(65, 0), self_stake BLOB)" 28 | candidateCreationMySQL = "CREATE TABLE IF NOT EXISTS %s (id INTEGER PRIMARY KEY AUTO_INCREMENT, hash VARCHAR(64) UNIQUE, owner BLOB, operator BLOB, reward BLOB, name BLOB, votes BLOB, self_stake_bucket_idx DECIMAL(65, 0), self_stake BLOB)" 29 | 30 | // InsertCandidateQuerySQLITE is query to insert candidates in SQLITE driver 31 | InsertCandidateQuerySQLITE = "INSERT OR IGNORE INTO %s (hash, owner, operator, reward, name, votes, self_stake_bucket_idx, self_stake) VALUES (?, ?, ?, ?, ?, ?, ?, ?)" 32 | // InsertCandidateQueryMySQL is query to insert candidates in MySQL driver 33 | InsertCandidateQueryMySQL = "INSERT IGNORE INTO %s (hash, owner, operator, reward, name, votes, self_stake_bucket_idx, self_stake) VALUES (?, ?, ?, ?, ?, ?, ?, ?)" 34 | // CandidateQuery is query to get candidates by ids 35 | CandidateQuery = "SELECT id, owner, operator, reward, name, votes, self_stake_bucket_idx, self_stake FROM %s WHERE id IN (%s)" 36 | ) 37 | 38 | type ( 39 | candidateStruct struct { 40 | ID int64 41 | Owner, Operator, Reward, Name, Votes []byte 42 | SelfStakeBucketIdx uint64 43 | SelfStake []byte 44 | } 45 | ) 46 | 47 | // NewCandidateTableOperator create an operator for candidate table 48 | func NewCandidateTableOperator(tableName string, driverName committee.DRIVERTYPE) (committee.Operator, error) { 49 | var creation string 50 | switch driverName { 51 | case committee.SQLITE: 52 | creation = candidateCreationSQLITE 53 | case committee.MYSQL: 54 | creation = candidateCreationMySQL 55 | default: 56 | return nil, errors.New("Wrong driver type") 57 | } 58 | return committee.NewRecordTableOperator( 59 | tableName, 60 | driverName, 61 | InsertCandidates, 62 | QueryCandidates, 63 | creation, 64 | ) 65 | } 66 | 67 | // QueryCandidates get all candidates by ids 68 | func QueryCandidates(tableName string, frequencies map[int64]int, sdb *sql.DB, tx *sql.Tx) (ret interface{}, err error) { 69 | size := 0 70 | ids := make([]int64, 0, len(frequencies)) 71 | for id, f := range frequencies { 72 | ids = append(ids, id) 73 | size += f 74 | } 75 | var rows *sql.Rows 76 | if tx != nil { 77 | rows, err = tx.Query(fmt.Sprintf(CandidateQuery, tableName, atos(ids))) 78 | } else { 79 | rows, err = sdb.Query(fmt.Sprintf(CandidateQuery, tableName, atos(ids))) 80 | } 81 | if err != nil { 82 | return 83 | } 84 | if err = rows.Err(); err != nil { 85 | return 86 | } 87 | defer rows.Close() 88 | var cs candidateStruct 89 | parsedRows, err := s.ParseSQLRows(rows, &cs) 90 | if err != nil { 91 | return nil, errors.Wrap(err, "failed to parse results") 92 | } 93 | 94 | if len(parsedRows) == 0 { 95 | return nil, indexprotocol.ErrNotExist 96 | } 97 | 98 | candidates := make([]*iotextypes.CandidateV2, 0) 99 | for _, parsedRow := range parsedRows { 100 | cs, ok := parsedRow.(*candidateStruct) 101 | if !ok { 102 | return nil, errors.New("failed to convert") 103 | } 104 | candidate := &iotextypes.CandidateV2{ 105 | OwnerAddress: string(cs.Owner), 106 | OperatorAddress: string(cs.Operator), 107 | RewardAddress: string(cs.Reward), 108 | Name: string(cs.Name), 109 | TotalWeightedVotes: string(cs.Votes), 110 | SelfStakeBucketIdx: cs.SelfStakeBucketIdx, 111 | SelfStakingTokens: string(cs.SelfStake), 112 | } 113 | for i := frequencies[cs.ID]; i > 0; i-- { 114 | candidates = append(candidates, candidate) 115 | } 116 | } 117 | 118 | return &iotextypes.CandidateListV2{Candidates: candidates}, nil 119 | } 120 | 121 | // InsertCandidates inserts candidate records into table by tx 122 | func InsertCandidates(tableName string, driverName committee.DRIVERTYPE, records interface{}, tx *sql.Tx) (frequencies map[hash.Hash256]int, err error) { 123 | candidates, ok := records.(*iotextypes.CandidateListV2) 124 | if !ok { 125 | return nil, errors.Errorf("Unexpected type %s", reflect.TypeOf(records)) 126 | } 127 | if candidates == nil { 128 | return nil, nil 129 | } 130 | var candStmt *sql.Stmt 131 | switch driverName { 132 | case committee.SQLITE: 133 | candStmt, err = tx.Prepare(fmt.Sprintf(InsertCandidateQuerySQLITE, tableName)) 134 | case committee.MYSQL: 135 | candStmt, err = tx.Prepare(fmt.Sprintf(InsertCandidateQueryMySQL, tableName)) 136 | default: 137 | return nil, errors.New("wrong driver type") 138 | } 139 | defer func() { 140 | closeErr := candStmt.Close() 141 | if err == nil && closeErr != nil { 142 | err = closeErr 143 | } 144 | }() 145 | frequencies = make(map[hash.Hash256]int) 146 | for _, candidate := range candidates.Candidates { 147 | var h hash.Hash256 148 | if h, err = hashCandidate(candidate); err != nil { 149 | return nil, err 150 | } 151 | if f, ok := frequencies[h]; ok { 152 | frequencies[h] = f + 1 153 | } else { 154 | frequencies[h] = 1 155 | } 156 | if _, err = candStmt.Exec( 157 | hex.EncodeToString(h[:]), 158 | []byte(candidate.OwnerAddress), 159 | []byte(candidate.OperatorAddress), 160 | []byte(candidate.RewardAddress), 161 | []byte(candidate.Name), 162 | []byte(candidate.TotalWeightedVotes), 163 | candidate.SelfStakeBucketIdx, 164 | []byte(candidate.SelfStakingTokens), 165 | ); err != nil { 166 | return nil, err 167 | } 168 | } 169 | 170 | return frequencies, nil 171 | } 172 | 173 | func hashCandidate(candidate *iotextypes.CandidateV2) (hash.Hash256, error) { 174 | data, err := proto.Marshal(candidate) 175 | if err != nil { 176 | return hash.ZeroHash256, err 177 | } 178 | return hash.Hash256b(data), nil 179 | } 180 | -------------------------------------------------------------------------------- /indexprotocol/votings/probation.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 IoTeX 2 | // This is an alpha (internal) release and is not suitable for production. This source code is provided 'as is' and no 3 | // warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent 4 | // permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache 5 | // License 2.0 that can be found in the LICENSE file. 6 | 7 | package votings 8 | 9 | import ( 10 | "context" 11 | "database/sql" 12 | "encoding/hex" 13 | "fmt" 14 | "math/big" 15 | "strconv" 16 | 17 | "github.com/golang/protobuf/proto" 18 | "github.com/pkg/errors" 19 | "google.golang.org/grpc/codes" 20 | "google.golang.org/grpc/status" 21 | 22 | s "github.com/iotexproject/iotex-analytics/sql" 23 | "github.com/iotexproject/iotex-election/types" 24 | "github.com/iotexproject/iotex-election/util" 25 | "github.com/iotexproject/iotex-proto/golang/iotexapi" 26 | "github.com/iotexproject/iotex-proto/golang/iotextypes" 27 | ) 28 | 29 | const ( 30 | // ProbationListTableName is the table name of probation list 31 | ProbationListTableName = "probation_list" 32 | // EpochAddressIndexName is the index name of epoch number and address on probation table 33 | EpochAddressIndexName = "epoch_address_index" 34 | createProbationList = "CREATE TABLE IF NOT EXISTS %s " + 35 | "(epoch_number DECIMAL(65, 0) NOT NULL,intensity_rate DECIMAL(65, 0) NOT NULL,address VARCHAR(41) NOT NULL, count DECIMAL(65, 0) NOT NULL,PRIMARY KEY (`epoch_number`, `address`), UNIQUE KEY %s (epoch_number, address))" 36 | insertProbationList = "INSERT IGNORE INTO %s (epoch_number,intensity_rate,address,count) VALUES (?, ?, ?, ?)" 37 | selectProbationList = "SELECT * FROM %s WHERE epoch_number=?" 38 | ) 39 | 40 | type ( 41 | // ProbationList defines the schema of "probation_list" table 42 | ProbationList struct { 43 | EpochNumber uint64 44 | IntensityRate uint64 45 | Address string 46 | Count uint64 47 | } 48 | ) 49 | 50 | func (p *Protocol) createProbationListTable(tx *sql.Tx) error { 51 | if _, err := tx.Exec(fmt.Sprintf(createProbationList, ProbationListTableName, EpochAddressIndexName)); err != nil { 52 | return err 53 | } 54 | return nil 55 | } 56 | 57 | func (p *Protocol) updateProbationListTable(tx *sql.Tx, epochNum uint64, probationList *iotextypes.ProbationCandidateList) error { 58 | if probationList == nil { 59 | return nil 60 | } 61 | insertQuery := fmt.Sprintf(insertProbationList, ProbationListTableName) 62 | for _, k := range probationList.ProbationList { 63 | if _, err := tx.Exec(insertQuery, epochNum, probationList.IntensityRate, k.Address, k.Count); err != nil { 64 | return errors.Wrap(err, "failed to update probation list table") 65 | } 66 | } 67 | return nil 68 | } 69 | 70 | func (p *Protocol) fetchProbationList(cli iotexapi.APIServiceClient, epochNum uint64) (*iotextypes.ProbationCandidateList, error) { 71 | request := &iotexapi.ReadStateRequest{ 72 | ProtocolID: []byte("poll"), 73 | MethodName: []byte("ProbationListByEpoch"), 74 | Arguments: [][]byte{[]byte(strconv.FormatUint(epochNum, 10))}, 75 | } 76 | out, err := cli.ReadState(context.Background(), request) 77 | if err != nil { 78 | sta, ok := status.FromError(err) 79 | if ok && sta.Code() == codes.NotFound { 80 | return nil, nil 81 | } 82 | return nil, err 83 | } 84 | probationList := &iotextypes.ProbationCandidateList{} 85 | if out.Data != nil { 86 | if err := proto.Unmarshal(out.Data, probationList); err != nil { 87 | return nil, errors.Wrap(err, "failed to unmarshal probationList") 88 | } 89 | } 90 | return probationList, nil 91 | } 92 | 93 | // getProbationList gets probation list from table 94 | func (p *Protocol) getProbationList(epochNumber uint64) ([]*ProbationList, error) { 95 | db := p.Store.GetDB() 96 | getQuery := fmt.Sprintf(selectProbationList, 97 | ProbationListTableName) 98 | stmt, err := db.Prepare(getQuery) 99 | if err != nil { 100 | return nil, errors.Wrap(err, "failed to prepare get query") 101 | } 102 | defer stmt.Close() 103 | rows, err := stmt.Query(epochNumber) 104 | if err != nil { 105 | return nil, errors.Wrap(err, "failed to execute get query") 106 | } 107 | var pb ProbationList 108 | parsedRows, err := s.ParseSQLRows(rows, &pb) 109 | if err != nil { 110 | return nil, errors.Wrap(err, "failed to parse results") 111 | } 112 | if len(parsedRows) == 0 { 113 | return nil, nil 114 | } 115 | var pblist []*ProbationList 116 | for _, parsedRow := range parsedRows { 117 | pb := parsedRow.(*ProbationList) 118 | pblist = append(pblist, pb) 119 | } 120 | return pblist, nil 121 | } 122 | 123 | // filterCandidates returns filtered candidate list by given raw candidate and probation list 124 | func filterCandidates( 125 | candidates []*types.Candidate, 126 | unqualifiedList *iotextypes.ProbationCandidateList, 127 | epochStartHeight uint64, 128 | ) ([]*types.Candidate, error) { 129 | candidatesMap := make(map[string]*types.Candidate) 130 | updatedVotingPower := make(map[string]*big.Int) 131 | intensityRate := float64(uint32(100)-unqualifiedList.IntensityRate) / float64(100) 132 | 133 | probationMap := make(map[string]uint32) 134 | for _, elem := range unqualifiedList.ProbationList { 135 | probationMap[elem.Address] = elem.Count 136 | } 137 | for _, cand := range candidates { 138 | filterCand := cand.Clone() 139 | candOpAddr := string(cand.OperatorAddress()) 140 | if _, ok := probationMap[candOpAddr]; ok { 141 | // if it is an unqualified delegate, multiply the voting power with probation intensity rate 142 | votingPower := new(big.Float).SetInt(filterCand.Score()) 143 | newVotingPower, _ := votingPower.Mul(votingPower, big.NewFloat(intensityRate)).Int(nil) 144 | filterCand.SetScore(newVotingPower) 145 | } 146 | updatedVotingPower[candOpAddr] = filterCand.Score() 147 | candidatesMap[candOpAddr] = filterCand 148 | } 149 | // sort again with updated voting power 150 | sorted := util.Sort(updatedVotingPower, epochStartHeight) 151 | var verifiedCandidates []*types.Candidate 152 | for _, name := range sorted { 153 | verifiedCandidates = append(verifiedCandidates, candidatesMap[name]) 154 | } 155 | return verifiedCandidates, nil 156 | } 157 | 158 | // filterStakingCandidates returns filtered candidate list by given raw candidate and probation list 159 | func filterStakingCandidates( 160 | candidates *iotextypes.CandidateListV2, 161 | unqualifiedList *iotextypes.ProbationCandidateList, 162 | epochStartHeight uint64, 163 | ) (*iotextypes.CandidateListV2, error) { 164 | candidatesMap := make(map[string]*iotextypes.CandidateV2) 165 | updatedVotingPower := make(map[string]*big.Int) 166 | intensityRate := float64(uint32(100)-unqualifiedList.IntensityRate) / float64(100) 167 | 168 | probationMap := make(map[string]uint32) 169 | for _, elem := range unqualifiedList.ProbationList { 170 | probationMap[elem.Address] = elem.Count 171 | } 172 | for _, cand := range candidates.Candidates { 173 | filterCand := *cand 174 | votingPowerInt, ok := new(big.Int).SetString(cand.TotalWeightedVotes, 10) 175 | if !ok { 176 | return nil, errors.New("total weighted votes convert error") 177 | } 178 | votingPower := new(big.Float).SetInt(votingPowerInt) 179 | if _, ok := probationMap[cand.OperatorAddress]; ok { 180 | newVotingPower, _ := votingPower.Mul(votingPower, big.NewFloat(intensityRate)).Int(nil) 181 | filterCand.TotalWeightedVotes = newVotingPower.String() 182 | } 183 | totalWeightedVotes, ok := new(big.Int).SetString(filterCand.TotalWeightedVotes, 10) 184 | if !ok { 185 | return nil, errors.New("total weighted votes convert error") 186 | } 187 | updatedVotingPower[cand.OperatorAddress] = totalWeightedVotes 188 | candidatesMap[cand.OperatorAddress] = &filterCand 189 | } 190 | // sort again with updated voting power 191 | sorted := util.Sort(updatedVotingPower, epochStartHeight) 192 | verifiedCandidates := &iotextypes.CandidateListV2{} 193 | for _, name := range sorted { 194 | verifiedCandidates.Candidates = append(verifiedCandidates.Candidates, candidatesMap[name]) 195 | } 196 | return verifiedCandidates, nil 197 | } 198 | 199 | func stakingProbationListToMap(candidateList *iotextypes.CandidateListV2, probationList []*ProbationList) (intensityRate float64, probationMap map[string]uint64) { 200 | probationMap = make(map[string]uint64) 201 | if probationList != nil { 202 | for _, can := range candidateList.Candidates { 203 | for _, pb := range probationList { 204 | intensityRate = float64(uint64(100)-pb.IntensityRate) / float64(100) 205 | if pb.Address == can.OperatorAddress { 206 | probationMap[can.OwnerAddress] = pb.Count 207 | } 208 | } 209 | } 210 | } 211 | return 212 | } 213 | 214 | func probationListToMap(delegates []*types.Candidate, pblist []*ProbationList) (intensityRate float64, probationMap map[string]uint64) { 215 | probationMap = make(map[string]uint64) 216 | if pblist != nil { 217 | for _, delegate := range delegates { 218 | delegateOpAddr := string(delegate.OperatorAddress()) 219 | for _, pb := range pblist { 220 | intensityRate = float64(uint64(100)-pb.IntensityRate) / float64(100) 221 | if pb.Address == delegateOpAddr { 222 | probationMap[hex.EncodeToString(delegate.Name())] = pb.Count 223 | } 224 | } 225 | } 226 | } 227 | return 228 | } 229 | 230 | func convertProbationListToLocal(probationList *iotextypes.ProbationCandidateList) (ret []*ProbationList) { 231 | if probationList == nil { 232 | return nil 233 | } 234 | ret = make([]*ProbationList, 0) 235 | for _, pb := range probationList.ProbationList { 236 | p := &ProbationList{ 237 | 0, 238 | uint64(probationList.IntensityRate), 239 | pb.Address, 240 | uint64(pb.Count), 241 | } 242 | ret = append(ret, p) 243 | } 244 | return 245 | } 246 | -------------------------------------------------------------------------------- /indexprotocol/votings/protocol_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 IoTeX 2 | // This is an alpha (internal) release and is not suitable for production. This source code is provided 'as is' and no 3 | // warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent 4 | // permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache 5 | // License 2.0 that can be found in the LICENSE file. 6 | 7 | package votings 8 | 9 | import ( 10 | "context" 11 | "database/sql" 12 | "encoding/hex" 13 | "math/big" 14 | "strconv" 15 | "testing" 16 | "time" 17 | 18 | "github.com/golang/mock/gomock" 19 | "github.com/golang/protobuf/ptypes" 20 | "github.com/stretchr/testify/require" 21 | 22 | "github.com/iotexproject/iotex-core/action/protocol/vote" 23 | "github.com/iotexproject/iotex-core/test/mock/mock_apiserviceclient" 24 | "github.com/iotexproject/iotex-election/db" 25 | "github.com/iotexproject/iotex-election/pb/api" 26 | "github.com/iotexproject/iotex-election/pb/election" 27 | mock_election "github.com/iotexproject/iotex-election/test/mock/mock_apiserviceclient" 28 | "github.com/iotexproject/iotex-proto/golang/iotexapi" 29 | "github.com/iotexproject/iotex-proto/golang/iotextypes" 30 | 31 | "github.com/iotexproject/iotex-analytics/epochctx" 32 | "github.com/iotexproject/iotex-analytics/indexcontext" 33 | "github.com/iotexproject/iotex-analytics/indexprotocol" 34 | s "github.com/iotexproject/iotex-analytics/sql" 35 | "github.com/iotexproject/iotex-analytics/testutil" 36 | ) 37 | 38 | const ( 39 | connectStr = "bfe10c7cf8aa29:8bed5959@tcp(us-cdbr-east-04.cleardb.com:3306)/" 40 | dbName = "heroku_067cec75e0ba5ba" 41 | selectAggregateVoting = "SELECT aggregate_votes FROM %s WHERE epoch_number=? AND candidate_name=? AND voter_address=?" 42 | selectVotingMeta = "SELECT total_weighted FROM %s WHERE epoch_number=?" 43 | ) 44 | 45 | func TestProtocol(t *testing.T) { 46 | ctrl := gomock.NewController(t) 47 | defer ctrl.Finish() 48 | require := require.New(t) 49 | ctx := context.Background() 50 | 51 | testutil.CleanupDatabase(t, connectStr, dbName) 52 | store := s.NewMySQL(connectStr, dbName, false) 53 | require.NoError(store.Start(ctx)) 54 | defer func() { 55 | _, err := store.GetDB().Exec("DROP DATABASE " + dbName) 56 | require.NoError(err) 57 | require.NoError(store.Stop(ctx)) 58 | }() 59 | cfg := indexprotocol.VoteWeightCalConsts{ 60 | DurationLg: 1.2, 61 | AutoStake: 1, 62 | SelfStake: 1.06, 63 | } 64 | p, err := NewProtocol(store, 65 | epochctx.NewEpochCtx(36, 24, 15, epochctx.FairbankHeight(1000000)), 66 | indexprotocol.GravityChain{}, 67 | indexprotocol.Poll{ 68 | VoteThreshold: "0", 69 | ScoreThreshold: "0", 70 | SelfStakingThreshold: "0", 71 | }, 72 | cfg, 73 | indexprotocol.RewardPortionCfg{ 74 | RewardPortionContract: "", 75 | RewardPortionContractDeployHeight: uint64(123), 76 | }) 77 | require.NoError(err) 78 | require.NoError(p.CreateTables(ctx)) 79 | 80 | blk, err := testutil.BuildCompleteBlock(uint64(361), uint64(721)) 81 | require.NoError(err) 82 | 83 | chainClient := mock_apiserviceclient.NewMockServiceClient(ctrl) 84 | electionClient := mock_election.NewMockAPIServiceClient(ctrl) 85 | ctx = indexcontext.WithIndexCtx(context.Background(), indexcontext.IndexCtx{ 86 | ChainClient: chainClient, 87 | ElectionClient: electionClient, 88 | ConsensusScheme: "ROLLDPOS", 89 | }) 90 | 91 | timestamp, err := ptypes.TimestampProto(time.Unix(1000, 0)) 92 | require.NoError(err) 93 | chainClient.EXPECT().GetElectionBuckets(gomock.Any(), gomock.Any()).Times(1).Return(&iotexapi.GetElectionBucketsResponse{ 94 | Buckets: []*iotextypes.ElectionBucket{}, 95 | }, db.ErrNotExist) 96 | name1, err := hex.DecodeString("abcd") 97 | require.NoError(err) 98 | name2, err := hex.DecodeString("1234") 99 | require.NoError(err) 100 | 101 | voter1, err := hex.DecodeString("11") 102 | require.NoError(err) 103 | voter2, err := hex.DecodeString("22") 104 | require.NoError(err) 105 | voter3, err := hex.DecodeString("33") 106 | require.NoError(err) 107 | 108 | electionClient.EXPECT().GetRawData(gomock.Any(), gomock.Any()).Times(1).Return( 109 | &api.RawDataResponse{ 110 | Timestamp: timestamp, 111 | Buckets: []*election.Bucket{ 112 | { 113 | Voter: voter1, 114 | Candidate: name1, 115 | StartTime: timestamp, 116 | Duration: ptypes.DurationProto(time.Duration(10 * 24)), 117 | Decay: true, 118 | Amount: new(big.Int).SetInt64(100).Bytes(), 119 | }, 120 | { 121 | Voter: voter2, 122 | Candidate: name1, 123 | StartTime: timestamp, 124 | Duration: ptypes.DurationProto(time.Duration(10 * 24)), 125 | Decay: true, 126 | Amount: new(big.Int).SetInt64(50).Bytes(), 127 | }, 128 | { 129 | Voter: voter3, 130 | Candidate: name2, 131 | StartTime: timestamp, 132 | Duration: ptypes.DurationProto(time.Duration(10 * 24)), 133 | Decay: true, 134 | Amount: new(big.Int).SetInt64(100).Bytes(), 135 | }, 136 | }, 137 | Registrations: []*election.Registration{ 138 | { 139 | Name: name1, 140 | Address: []byte("112233"), 141 | OperatorAddress: []byte(testutil.Addr1), 142 | RewardAddress: []byte(testutil.RewardAddr1), 143 | SelfStakingWeight: 100, 144 | }, 145 | { 146 | Name: name2, 147 | Address: []byte("445566"), 148 | OperatorAddress: []byte(testutil.Addr2), 149 | RewardAddress: []byte(testutil.RewardAddr2), 150 | SelfStakingWeight: 102, 151 | }, 152 | }, 153 | }, nil, 154 | ) 155 | 156 | probationListByEpochRequest := &iotexapi.ReadStateRequest{ 157 | ProtocolID: []byte(indexprotocol.PollProtocolID), 158 | MethodName: []byte("ProbationListByEpoch"), 159 | Arguments: [][]byte{[]byte(strconv.FormatUint(2, 10))}, 160 | } 161 | 162 | probationList := &vote.ProbationList{ 163 | IntensityRate: uint32(0), 164 | ProbationInfo: make(map[string]uint32), 165 | } 166 | 167 | data, err := probationList.Serialize() 168 | chainClient.EXPECT().ReadState(gomock.Any(), probationListByEpochRequest).Times(1).Return(&iotexapi.ReadStateResponse{ 169 | Data: data, 170 | }, nil) 171 | 172 | readStateRequestForGravityHeight := &iotexapi.ReadStateRequest{ 173 | ProtocolID: []byte(indexprotocol.PollProtocolID), 174 | MethodName: []byte("GetGravityChainStartHeight"), 175 | Arguments: [][]byte{[]byte(strconv.FormatUint(1, 10))}, 176 | } 177 | chainClient.EXPECT().ReadState(gomock.Any(), readStateRequestForGravityHeight).Times(1).Return(&iotexapi.ReadStateResponse{ 178 | Data: []byte(strconv.FormatUint(1000, 10)), 179 | }, nil) 180 | require.NoError(store.Transact(func(tx *sql.Tx) error { 181 | return p.HandleBlock(ctx, tx, blk) 182 | })) 183 | // Probation Test 184 | // VotingResult 185 | res1, err := p.GetVotingResult(2, "abcd") 186 | require.NoError(err) 187 | res2, err := p.GetVotingResult(2, "1234") 188 | require.NoError(err) 189 | require.Equal("abcd", res1.DelegateName) 190 | require.Equal("1234", res2.DelegateName) 191 | require.Equal("150", res1.TotalWeightedVotes) 192 | require.Equal("100", res2.TotalWeightedVotes) 193 | 194 | /* 195 | // takes too long time to pass it, need further investigate 196 | // AggregateVoting 197 | getQuery := fmt.Sprintf(selectAggregateVoting, AggregateVotingTableName) 198 | stmt, err := store.GetDB().Prepare(getQuery) 199 | require.NoError(err) 200 | defer stmt.Close() 201 | var weightedVotes uint64 202 | require.NoError(stmt.QueryRow(2, "abcd", "11").Scan(&weightedVotes)) 203 | require.Equal(uint64(10), weightedVotes) // 100 * 0.1 204 | // VotingMeta 205 | getQuery = fmt.Sprintf(selectVotingMeta, VotingMetaTableName) 206 | stmt, err = store.GetDB().Prepare(getQuery) 207 | require.NoError(err) 208 | defer stmt.Close() 209 | var totalWeightedVotes string 210 | require.NoError(stmt.QueryRow(2).Scan(&totalWeightedVotes)) 211 | require.Equal("115", totalWeightedVotes) 212 | */ 213 | } 214 | -------------------------------------------------------------------------------- /indexprotocol/votings/stakingprotocol_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 IoTeX 2 | // This is an alpha (internal) release and is not suitable for production. This source code is provided 'as is' and no 3 | // warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent 4 | // permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache 5 | // License 2.0 that can be found in the LICENSE file. 6 | 7 | package votings 8 | 9 | import ( 10 | "context" 11 | "encoding/hex" 12 | "testing" 13 | "time" 14 | 15 | "github.com/golang/mock/gomock" 16 | "github.com/golang/protobuf/proto" 17 | "github.com/golang/protobuf/ptypes" 18 | "github.com/golang/protobuf/ptypes/timestamp" 19 | "github.com/stretchr/testify/require" 20 | 21 | "github.com/iotexproject/iotex-core/ioctl/util" 22 | "github.com/iotexproject/iotex-core/test/mock/mock_apiserviceclient" 23 | "github.com/iotexproject/iotex-proto/golang/iotexapi" 24 | "github.com/iotexproject/iotex-proto/golang/iotextypes" 25 | 26 | "github.com/iotexproject/iotex-analytics/epochctx" 27 | "github.com/iotexproject/iotex-analytics/indexprotocol" 28 | s "github.com/iotexproject/iotex-analytics/sql" 29 | "github.com/iotexproject/iotex-analytics/testutil" 30 | ) 31 | 32 | var ( 33 | now = time.Now() 34 | buckets = []*iotextypes.VoteBucket{ 35 | &iotextypes.VoteBucket{ 36 | Index: 10, 37 | CandidateAddress: "io1mflp9m6hcgm2qcghchsdqj3z3eccrnekx9p0ms", 38 | StakedAmount: "30000", 39 | StakedDuration: 1, // one day 40 | CreateTime: ×tamp.Timestamp{Seconds: now.Unix(), Nanos: 0}, 41 | StakeStartTime: ×tamp.Timestamp{Seconds: now.Unix(), Nanos: 0}, 42 | UnstakeStartTime: ×tamp.Timestamp{Seconds: now.Unix(), Nanos: 0}, 43 | AutoStake: false, 44 | Owner: "io1l9vaqmanwj47tlrpv6etf3pwq0s0snsq4vxke2", 45 | }, 46 | &iotextypes.VoteBucket{ 47 | Index: 11, 48 | CandidateAddress: "io1mflp9m6hcgm2qcghchsdqj3z3eccrnekx9p0ms", 49 | StakedAmount: "30000", 50 | StakedDuration: 1, 51 | CreateTime: ×tamp.Timestamp{Seconds: now.Unix(), Nanos: 0}, 52 | StakeStartTime: ×tamp.Timestamp{Seconds: now.Unix(), Nanos: 0}, 53 | UnstakeStartTime: ×tamp.Timestamp{Seconds: now.Unix(), Nanos: 0}, 54 | AutoStake: false, 55 | Owner: "io1ph0u2psnd7muq5xv9623rmxdsxc4uapxhzpg02", 56 | }, 57 | &iotextypes.VoteBucket{ 58 | Index: 12, 59 | CandidateAddress: "io1mflp9m6hcgm2qcghchsdqj3z3eccrnekx9p0ms", 60 | StakedAmount: "30000", 61 | StakedDuration: 1, 62 | CreateTime: ×tamp.Timestamp{Seconds: now.Unix(), Nanos: 0}, 63 | StakeStartTime: ×tamp.Timestamp{Seconds: now.Unix(), Nanos: 0}, 64 | UnstakeStartTime: ×tamp.Timestamp{Seconds: now.Unix(), Nanos: 0}, 65 | AutoStake: false, 66 | Owner: "io1vdtfpzkwpyngzvx7u2mauepnzja7kd5rryp0sg", 67 | }, 68 | } 69 | candidates = []*iotextypes.CandidateV2{ 70 | &iotextypes.CandidateV2{ 71 | OwnerAddress: "io1mflp9m6hcgm2qcghchsdqj3z3eccrnekx9p0ms", 72 | OperatorAddress: "io1mflp9m6hcgm2qcghchsdqj3z3eccrnekx9p0ms", 73 | RewardAddress: "io1mflp9m6hcgm2qcghchsdqj3z3eccrnekx9p0ms", 74 | Name: delegateName, 75 | TotalWeightedVotes: "10", 76 | SelfStakeBucketIdx: 6666, 77 | SelfStakingTokens: "99999", 78 | }, 79 | } 80 | delegateName = "xxxx" 81 | ) 82 | 83 | func TestStaking(t *testing.T) { 84 | ctrl := gomock.NewController(t) 85 | defer ctrl.Finish() 86 | chainClient := mock_apiserviceclient.NewMockServiceClient(ctrl) 87 | mock(chainClient, t) 88 | height := uint64(110000) 89 | epochNumber := uint64(68888) 90 | require := require.New(t) 91 | ctx := context.Background() 92 | //use for remote database 93 | testutil.CleanupDatabase(t, connectStr, dbName) 94 | store := s.NewMySQL(connectStr, dbName, false) 95 | require.NoError(store.Start(ctx)) 96 | defer func() { 97 | //use for remote database 98 | _, err := store.GetDB().Exec("DROP DATABASE " + dbName) 99 | require.NoError(err) 100 | require.NoError(store.Stop(ctx)) 101 | }() 102 | require.NoError(store.Start(context.Background())) 103 | cfg := indexprotocol.VoteWeightCalConsts{ 104 | DurationLg: 1.2, 105 | AutoStake: 1, 106 | SelfStake: 1.06, 107 | } 108 | p, err := NewProtocol(store, epochctx.NewEpochCtx(36, 24, 15, epochctx.FairbankHeight(110000)), indexprotocol.GravityChain{}, indexprotocol.Poll{ 109 | VoteThreshold: "100000000000000000000", 110 | ScoreThreshold: "0", 111 | SelfStakingThreshold: "0", 112 | }, cfg, indexprotocol.RewardPortionCfg{"io1lfl4ppn2c3wcft04f0rk0jy9lyn4pcjcm7638u", 100000}) 113 | require.NoError(err) 114 | require.NoError(p.CreateTables(context.Background())) 115 | tx, err := p.Store.GetDB().Begin() 116 | require.NoError(err) 117 | chainClient.EXPECT().GetLogs(gomock.Any(), gomock.Any()).AnyTimes().Return(&iotexapi.GetLogsResponse{Logs: []*iotextypes.Log{&iotextypes.Log{}}}, nil) 118 | require.NoError(p.processStaking(tx, chainClient, height, height, epochNumber, nil)) 119 | require.NoError(tx.Commit()) 120 | 121 | // case I: checkout bucket if it's written right 122 | ret, err := p.stakingBucketTableOperator.Get(height, p.Store.GetDB(), nil) 123 | require.NoError(err) 124 | bucketList, ok := ret.(*iotextypes.VoteBucketList) 125 | require.True(ok) 126 | bucketsBytes, _ := proto.Marshal(&iotextypes.VoteBucketList{Buckets: buckets}) 127 | bucketListBytes, _ := proto.Marshal(bucketList) 128 | require.EqualValues(bucketsBytes, bucketListBytes) 129 | 130 | // case II: checkout candidate if it's written right 131 | ret, err = p.stakingCandidateTableOperator.Get(height, p.Store.GetDB(), nil) 132 | require.NoError(err) 133 | candidateList, ok := ret.(*iotextypes.CandidateListV2) 134 | require.True(ok) 135 | require.Equal(delegateName, candidateList.Candidates[0].Name) 136 | candidatesBytes, _ := proto.Marshal(&iotextypes.CandidateListV2{Candidates: candidates}) 137 | candidateListBytes, _ := proto.Marshal(candidateList) 138 | require.EqualValues(candidatesBytes, candidateListBytes) 139 | 140 | // case III: check getStakingBucketInfoByEpoch 141 | encodedName, err := indexprotocol.EncodeDelegateName(delegateName) 142 | require.NoError(err) 143 | bucketInfo, err := p.getStakingBucketInfoByEpoch(height, epochNumber, encodedName) 144 | require.NoError(err) 145 | 146 | ethAddress1, err := util.IoAddrToEvmAddr("io1l9vaqmanwj47tlrpv6etf3pwq0s0snsq4vxke2") 147 | require.NoError(err) 148 | require.Equal(hex.EncodeToString(ethAddress1.Bytes()), bucketInfo[0].VoterAddress) 149 | 150 | ethAddress2, err := util.IoAddrToEvmAddr("io1ph0u2psnd7muq5xv9623rmxdsxc4uapxhzpg02") 151 | require.NoError(err) 152 | require.Equal(hex.EncodeToString(ethAddress2.Bytes()), bucketInfo[1].VoterAddress) 153 | 154 | ethAddress3, err := util.IoAddrToEvmAddr("io1vdtfpzkwpyngzvx7u2mauepnzja7kd5rryp0sg") 155 | require.NoError(err) 156 | require.Equal(hex.EncodeToString(ethAddress3.Bytes()), bucketInfo[2].VoterAddress) 157 | for _, b := range bucketInfo { 158 | require.True(b.Decay) 159 | require.Equal(epochNumber, b.EpochNumber) 160 | require.True(b.IsNative) 161 | dur, err := time.ParseDuration(b.RemainingDuration) 162 | require.NoError(err) 163 | require.True(dur.Seconds() <= float64(86400)) 164 | // 'now' need to format b/c b.StartTime's nano time is set to 0 165 | require.Equal(now.Format("2006-01-02 15:04:05 -0700 MST"), b.StartTime) 166 | require.Equal("30000", b.Votes) 167 | require.Equal("30000", b.WeightedVotes) 168 | } 169 | } 170 | 171 | func TestRemainingTime(t *testing.T) { 172 | require := require.New(t) 173 | // case I: now is before start time 174 | bucketTime := time.Now().Add(time.Second * 100) 175 | timestamp, _ := ptypes.TimestampProto(bucketTime) 176 | bucket := &iotextypes.VoteBucket{ 177 | StakeStartTime: timestamp, 178 | StakedDuration: 100, 179 | } 180 | remaining := CalcRemainingTime(bucket) 181 | require.Equal(time.Duration(0), remaining) 182 | 183 | // case II: now is between start time and starttime+stakedduration 184 | bucketTime = time.Unix(time.Now().Unix()-10, 0) 185 | timestamp, _ = ptypes.TimestampProto(bucketTime) 186 | bucket = &iotextypes.VoteBucket{ 187 | StakeStartTime: timestamp, 188 | StakedDuration: 100, 189 | AutoStake: false, 190 | } 191 | remaining = CalcRemainingTime(bucket) 192 | require.True(remaining > 0 && remaining < time.Duration(100*24*time.Hour)) 193 | 194 | // case III: AutoStake is true 195 | bucketTime = time.Unix(time.Now().Unix()-10, 0) 196 | timestamp, _ = ptypes.TimestampProto(bucketTime) 197 | bucket = &iotextypes.VoteBucket{ 198 | StakeStartTime: timestamp, 199 | StakedDuration: 100, 200 | AutoStake: true, 201 | } 202 | remaining = CalcRemainingTime(bucket) 203 | require.Equal(time.Duration(100*24*time.Hour), remaining) 204 | 205 | // case IV: now is after starttime+stakedduration 206 | bucketTime = time.Unix(time.Now().Unix()-86410, 0) 207 | timestamp, _ = ptypes.TimestampProto(bucketTime) 208 | bucket = &iotextypes.VoteBucket{ 209 | StakeStartTime: timestamp, 210 | StakedDuration: 1, 211 | } 212 | remaining = CalcRemainingTime(bucket) 213 | require.Equal(time.Duration(0), remaining) 214 | } 215 | 216 | func TestFilterStakingCandidates(t *testing.T) { 217 | require := require.New(t) 218 | cl := &iotextypes.CandidateListV2{Candidates: candidates} 219 | unqualifiedList := &iotextypes.ProbationCandidateList{ 220 | IntensityRate: 10, 221 | ProbationList: []*iotextypes.ProbationCandidateList_Info{ 222 | &iotextypes.ProbationCandidateList_Info{ 223 | Address: "io1mflp9m6hcgm2qcghchsdqj3z3eccrnekx9p0ms", 224 | Count: 10, 225 | }, 226 | }, 227 | } 228 | cl, err := filterStakingCandidates(cl, unqualifiedList, 10) 229 | require.NoError(err) 230 | require.Equal("9", cl.Candidates[0].TotalWeightedVotes) 231 | } 232 | 233 | func mock(chainClient *mock_apiserviceclient.MockServiceClient, t *testing.T) { 234 | protocolID := "staking" 235 | readBucketsLimit := uint32(30000) 236 | readCandidatesLimit := uint32(20000) 237 | require := require.New(t) 238 | methodNameBytes, _ := proto.Marshal(&iotexapi.ReadStakingDataMethod{ 239 | Method: iotexapi.ReadStakingDataMethod_BUCKETS, 240 | }) 241 | arg, err := proto.Marshal(&iotexapi.ReadStakingDataRequest{ 242 | Request: &iotexapi.ReadStakingDataRequest_Buckets{ 243 | Buckets: &iotexapi.ReadStakingDataRequest_VoteBuckets{ 244 | Pagination: &iotexapi.PaginationParam{ 245 | Offset: 0, 246 | Limit: readBucketsLimit, 247 | }, 248 | }, 249 | }, 250 | }) 251 | readStateRequest := &iotexapi.ReadStateRequest{ 252 | ProtocolID: []byte(protocolID), 253 | MethodName: methodNameBytes, 254 | Arguments: [][]byte{arg}, 255 | Height: "110000", 256 | } 257 | 258 | vbl := &iotextypes.VoteBucketList{Buckets: buckets} 259 | s, err := proto.Marshal(vbl) 260 | require.NoError(err) 261 | ctx := context.WithValue(context.Background(), &iotexapi.ReadStateRequest{}, iotexapi.ReadStakingDataMethod_BUCKETS) 262 | first := chainClient.EXPECT().ReadState(ctx, readStateRequest).AnyTimes().Return(&iotexapi.ReadStateResponse{ 263 | Data: s, 264 | }, nil) 265 | 266 | // mock candidate 267 | methodNameBytes, err = proto.Marshal(&iotexapi.ReadStakingDataMethod{ 268 | Method: iotexapi.ReadStakingDataMethod_CANDIDATES, 269 | }) 270 | require.NoError(err) 271 | arg, err = proto.Marshal(&iotexapi.ReadStakingDataRequest{ 272 | Request: &iotexapi.ReadStakingDataRequest_Candidates_{ 273 | Candidates: &iotexapi.ReadStakingDataRequest_Candidates{ 274 | Pagination: &iotexapi.PaginationParam{ 275 | Offset: 0, 276 | Limit: readCandidatesLimit, 277 | }, 278 | }, 279 | }, 280 | }) 281 | readStateRequest = &iotexapi.ReadStateRequest{ 282 | ProtocolID: []byte(protocolID), 283 | MethodName: methodNameBytes, 284 | Arguments: [][]byte{arg}, 285 | Height: "110000", 286 | } 287 | 288 | cl := &iotextypes.CandidateListV2{Candidates: candidates} 289 | s, err = proto.Marshal(cl) 290 | require.NoError(err) 291 | ctx = context.WithValue(context.Background(), &iotexapi.ReadStateRequest{}, iotexapi.ReadStakingDataMethod_CANDIDATES) 292 | second := chainClient.EXPECT().ReadState(ctx, readStateRequest).AnyTimes().Return(&iotexapi.ReadStateResponse{ 293 | Data: s, 294 | }, nil) 295 | third := chainClient.EXPECT().ReadState(gomock.Any(), gomock.Any()).AnyTimes().Return(&iotexapi.ReadStateResponse{ 296 | Data: []byte("888888888"), 297 | }, nil) 298 | gomock.InOrder( 299 | first, 300 | second, 301 | third, 302 | ) 303 | } 304 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 IoTeX 2 | // This is an alpha (internal) release and is not suitable for production. This source code is provided 'as is' and no 3 | // warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent 4 | // permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache 5 | // License 2.0 that can be found in the LICENSE file. 6 | 7 | // usage: go build -o ./bin/server -v . 8 | // ./bin/server 9 | 10 | package main 11 | 12 | import ( 13 | "bytes" 14 | "context" 15 | "io/ioutil" 16 | "net/http" 17 | "os" 18 | "strconv" 19 | "time" 20 | 21 | "github.com/99designs/gqlgen/handler" 22 | "github.com/iotexproject/iotex-core/pkg/log" 23 | "github.com/iotexproject/iotex-election/pb/api" 24 | "github.com/iotexproject/iotex-proto/golang/iotexapi" 25 | "go.uber.org/zap" 26 | "go.uber.org/zap/zapcore" 27 | "google.golang.org/grpc" 28 | "gopkg.in/yaml.v2" 29 | 30 | "github.com/iotexproject/iotex-analytics/graphql" 31 | "github.com/iotexproject/iotex-analytics/indexcontext" 32 | "github.com/iotexproject/iotex-analytics/indexservice" 33 | "github.com/iotexproject/iotex-analytics/queryprotocol/actions" 34 | "github.com/iotexproject/iotex-analytics/queryprotocol/chainmeta" 35 | "github.com/iotexproject/iotex-analytics/queryprotocol/hermes2" 36 | "github.com/iotexproject/iotex-analytics/queryprotocol/productivity" 37 | "github.com/iotexproject/iotex-analytics/queryprotocol/rewards" 38 | "github.com/iotexproject/iotex-analytics/queryprotocol/votings" 39 | "github.com/iotexproject/iotex-analytics/sql" 40 | ) 41 | 42 | const defaultPort = "8089" 43 | 44 | func main() { 45 | port := os.Getenv("PORT") 46 | if port == "" { 47 | port = defaultPort 48 | } 49 | 50 | configPath := os.Getenv("CONFIG") 51 | if configPath == "" { 52 | configPath = "config.yaml" 53 | } 54 | 55 | chainEndpoint := os.Getenv("CHAIN_ENDPOINT") 56 | if chainEndpoint == "" { 57 | chainEndpoint = "127.0.0.1:14014" 58 | } 59 | 60 | electionEndpoint := os.Getenv("ELECTION_ENDPOINT") 61 | if electionEndpoint == "" { 62 | electionEndpoint = "127.0.0.1:8090" 63 | } 64 | 65 | connectionStr := os.Getenv("CONNECTION_STRING") 66 | if connectionStr == "" { 67 | connectionStr = "root:rootuser@tcp(127.0.0.1:3306)/" 68 | } 69 | 70 | dbName := os.Getenv("DB_NAME") 71 | if dbName == "" { 72 | dbName = "analytics" 73 | } 74 | 75 | data, err := ioutil.ReadFile(configPath) 76 | if err != nil { 77 | log.L().Fatal("Failed to load config file", zap.Error(err)) 78 | } 79 | var cfg indexservice.Config 80 | if err := yaml.Unmarshal(data, &cfg); err != nil { 81 | log.L().Fatal("failed to unmarshal config", zap.Error(err)) 82 | } 83 | 84 | if cfg.Zap == nil { 85 | zapCfg := zap.NewProductionConfig() 86 | cfg.Zap = &zapCfg 87 | } else { 88 | if cfg.Zap.Development { 89 | cfg.Zap.EncoderConfig = zap.NewDevelopmentEncoderConfig() 90 | } else { 91 | cfg.Zap.EncoderConfig = zap.NewProductionEncoderConfig() 92 | } 93 | } 94 | cfg.Zap.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder 95 | cfg.Zap.EncoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder 96 | logger, err := cfg.Zap.Build() 97 | if err == nil { 98 | zap.ReplaceGlobals(logger) 99 | } 100 | readOnly := os.Getenv("READ_ONLY") 101 | if readOnly != "" { 102 | cfg.ReadOnly = readOnly == "true" 103 | } 104 | 105 | store := sql.NewMySQL(connectionStr, dbName, cfg.ReadOnly) 106 | maxOpenConnsStr := os.Getenv("MAX_OPEN_CONNECTIONS") 107 | if maxOpenConnsStr != "" { 108 | maxOpenConns, err := strconv.Atoi(maxOpenConnsStr) 109 | if err != nil { 110 | log.L().Info("failed to parse parameter", zap.String("MAX_OPEN_CONNECTIONS", maxOpenConnsStr), zap.Error(err)) 111 | } 112 | store.SetMaxOpenConns(maxOpenConns) 113 | } 114 | 115 | idx := indexservice.NewIndexer(store, cfg) 116 | if err := idx.RegisterDefaultProtocols(); err != nil { 117 | log.L().Fatal("Failed to register default protocols", zap.Error(err)) 118 | } 119 | 120 | http.Handle("/", graphqlHandler(handler.Playground("GraphQL playground", "/query"))) 121 | http.Handle("/query", graphqlHandler(handler.GraphQL(graphql.NewExecutableSchema(graphql.Config{Resolvers: &graphql.Resolver{ 122 | PP: productivity.NewProtocol(idx), 123 | RP: rewards.NewProtocol(idx), 124 | VP: votings.NewProtocol(idx), 125 | AP: actions.NewProtocol(idx), 126 | CP: chainmeta.NewProtocol(idx), 127 | HP: hermes2.NewProtocol(idx, cfg.HermesConfig), 128 | }})))) 129 | //http.Handle("/metrics", promhttp.Handler()) 130 | //log.S().Infof("connect to http://localhost:%s/ for GraphQL playground", port) 131 | 132 | // Start GraphQL query service 133 | go func() { 134 | if err := http.ListenAndServe(":"+port, nil); err != nil { 135 | log.L().Fatal("Failed to serve index query service", zap.Error(err)) 136 | } 137 | }() 138 | 139 | grpcCtx1, cancel := context.WithTimeout(context.Background(), 10*time.Second) 140 | defer cancel() 141 | conn1, err := grpc.DialContext(grpcCtx1, chainEndpoint, grpc.WithBlock(), grpc.WithInsecure()) 142 | if err != nil { 143 | log.L().Error("Failed to connect to chain's API server.") 144 | } 145 | chainClient := iotexapi.NewAPIServiceClient(conn1) 146 | 147 | grpcCtx2, cancel := context.WithTimeout(context.Background(), 10*time.Second) 148 | defer cancel() 149 | conn2, err := grpc.DialContext(grpcCtx2, electionEndpoint, grpc.WithBlock(), grpc.WithInsecure()) 150 | if err != nil { 151 | log.L().Error("Failed to connect to election's API server.") 152 | } 153 | electionClient := api.NewAPIServiceClient(conn2) 154 | 155 | ctx := indexcontext.WithIndexCtx(context.Background(), indexcontext.IndexCtx{ 156 | ChainClient: chainClient, 157 | ElectionClient: electionClient, 158 | ConsensusScheme: idx.Config.ConsensusScheme, 159 | }) 160 | 161 | if err := idx.Start(ctx); err != nil { 162 | log.L().Fatal("Failed to start the indexer", zap.Error(err)) 163 | } 164 | 165 | defer func() { 166 | if err := idx.Stop(ctx); err != nil { 167 | log.L().Fatal("Failed to stop the indexer", zap.Error(err)) 168 | } 169 | }() 170 | 171 | select {} 172 | } 173 | 174 | func graphqlHandler(playgroundHandler http.Handler) http.Handler { 175 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 176 | w.Header().Set("Access-Control-Allow-Origin", "*") 177 | w.Header().Set("Access-Control-Allow-Headers", "*") 178 | if r.Method == "POST" { 179 | body, err := ioutil.ReadAll(r.Body) 180 | if err != nil { 181 | log.L().Error("Failed to read request body", zap.Error(err)) 182 | w.WriteHeader(http.StatusInternalServerError) 183 | return 184 | } 185 | // clone body 186 | r.Body = ioutil.NopCloser(bytes.NewReader(body)) 187 | clientIP, clientID := getIPID(r) 188 | log.L().Info("request stat", 189 | zap.String("clientIP", clientIP), 190 | zap.String("clientID", clientID), 191 | zap.ByteString("body", body)) 192 | } 193 | playgroundHandler.ServeHTTP(w, r) 194 | }) 195 | } 196 | 197 | func getIPID(r *http.Request) (ip, id string) { 198 | ip = r.Header.Get("X-Forwarded-For") 199 | if ip == "" { 200 | ip = r.RemoteAddr 201 | } 202 | id = r.Header.Get("x-iotex-client-id") 203 | if id == "" { 204 | id = "unknown" 205 | } 206 | return 207 | } 208 | -------------------------------------------------------------------------------- /queryprotocol/chainmeta/chainmetautil/chainmetautil.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 IoTeX 2 | // This is an alpha (internal) release and is not suitable for production. This source code is provided 'as is' and no 3 | // warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent 4 | // permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache 5 | // License 2.0 that can be found in the LICENSE file. 6 | 7 | package chainmetautil 8 | 9 | import ( 10 | "fmt" 11 | 12 | "github.com/pkg/errors" 13 | 14 | "github.com/iotexproject/iotex-analytics/indexprotocol" 15 | "github.com/iotexproject/iotex-analytics/indexprotocol/blocks" 16 | "github.com/iotexproject/iotex-analytics/queryprotocol" 17 | s "github.com/iotexproject/iotex-analytics/sql" 18 | ) 19 | 20 | const ( 21 | selectBlockHistory = "SELECT epoch_number, block_height FROM %s" 22 | selectBlockHistoryMax = "SELECT MAX(epoch_number),MAX(block_height) FROM %s" 23 | ) 24 | 25 | // GetCurrentEpochAndHeight gets current epoch number and tip block height 26 | func GetCurrentEpochAndHeight(registry *indexprotocol.Registry, store s.Store) (uint64, uint64, error) { 27 | _, ok := registry.Find(blocks.ProtocolID) 28 | if !ok { 29 | return uint64(0), uint64(0), errors.New("blocks protocol is unregistered") 30 | } 31 | db := store.GetDB() 32 | // Check existence 33 | exist, err := queryprotocol.RowExists(db, fmt.Sprintf(selectBlockHistory, 34 | blocks.BlockHistoryTableName)) 35 | if err != nil { 36 | return uint64(0), uint64(0), errors.Wrap(err, "failed to check if the row exists") 37 | } 38 | if !exist { 39 | return uint64(0), uint64(0), indexprotocol.ErrNotExist 40 | } 41 | 42 | getQuery := fmt.Sprintf(selectBlockHistoryMax, blocks.BlockHistoryTableName) 43 | stmt, err := db.Prepare(getQuery) 44 | if err != nil { 45 | return uint64(0), uint64(0), errors.Wrap(err, "failed to prepare get query") 46 | 47 | } 48 | defer stmt.Close() 49 | 50 | var epoch, tipHeight uint64 51 | if err = stmt.QueryRow().Scan(&epoch, &tipHeight); err != nil { 52 | return uint64(0), uint64(0), errors.Wrap(err, "failed to execute get query") 53 | } 54 | return epoch, tipHeight, nil 55 | } 56 | -------------------------------------------------------------------------------- /queryprotocol/chainmeta/protocol.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 IoTeX 2 | // This is an alpha (internal) release and is not suitable for production. This source code is provided 'as is' and no 3 | // warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent 4 | // permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache 5 | // License 2.0 that can be found in the LICENSE file. 6 | 7 | package chainmeta 8 | 9 | import ( 10 | "context" 11 | "fmt" 12 | "math/big" 13 | "strings" 14 | "time" 15 | 16 | "github.com/pkg/errors" 17 | 18 | "github.com/iotexproject/iotex-address/address" 19 | "github.com/iotexproject/iotex-analytics/indexprotocol" 20 | "github.com/iotexproject/iotex-analytics/indexprotocol/accounts" 21 | "github.com/iotexproject/iotex-analytics/indexprotocol/blocks" 22 | "github.com/iotexproject/iotex-analytics/indexservice" 23 | "github.com/iotexproject/iotex-analytics/queryprotocol/chainmeta/chainmetautil" 24 | s "github.com/iotexproject/iotex-analytics/sql" 25 | "github.com/iotexproject/iotex-proto/golang/iotexapi" 26 | ) 27 | 28 | const ( 29 | lockAddresses = "io1uqhmnttmv0pg8prugxxn7d8ex9angrvfjfthxa" // Separate multiple addresses with "," 30 | totalBalance = "12700000000000000000000000000" // 10B + 2.7B (due to Postmortem 1) 31 | nsv1Balance = "262281303940000000000000000" 32 | bnfxBalance = "3414253030000000000000000" 33 | 34 | selectBlockHistory = "SELECT transfer,execution,depositToRewardingFund,claimFromRewardingFund,grantReward,putPollResult,timestamp FROM %s WHERE block_height>=? AND block_height<=?" 35 | selectBlockHistorySum = "SELECT SUM(transfer)+SUM(execution)+SUM(depositToRewardingFund)+SUM(claimFromRewardingFund)+SUM(grantReward)+SUM(putPollResult)+SUM(stakeCreate)+SUM(stakeUnstake)+SUM(stakeWithdraw)+SUM(stakeAddDeposit)+SUM(stakeRestake)+SUM(stakeChangeCandidate)+SUM(stakeTransferOwnership)+SUM(candidateRegister)+SUM(candidateUpdate) FROM %s WHERE epoch_number>=? and epoch_number<=?" 36 | selectTotalTransferred = "select IFNULL(SUM(amount),0) from %s where epoch_number>=? and epoch_number<=?" 37 | selectBalanceByAddress = "SELECT IFNULL(SUM(income),0) from %s WHERE address=?" 38 | ) 39 | 40 | // Protocol defines the protocol of querying tables 41 | type Protocol struct { 42 | indexer *indexservice.Indexer 43 | } 44 | 45 | // ChainMeta defines chain meta 46 | type ChainMeta struct { 47 | MostRecentEpoch string 48 | MostRecentBlockHeight string 49 | MostRecentTps string 50 | } 51 | 52 | type blkInfo struct { 53 | Transfer int 54 | Execution int 55 | DepositToRewardingFund int 56 | ClaimFromRewardingFund int 57 | GrantReward int 58 | PutPollResult int 59 | Timestamp int 60 | } 61 | 62 | // NewProtocol creates a new protocol 63 | func NewProtocol(idx *indexservice.Indexer) *Protocol { 64 | return &Protocol{indexer: idx} 65 | } 66 | 67 | // MostRecentTPS get most tps 68 | func (p *Protocol) MostRecentTPS(ranges uint64) (tps float64, err error) { 69 | _, ok := p.indexer.Registry.Find(blocks.ProtocolID) 70 | if !ok { 71 | err = errors.New("blocks protocol is unregistered") 72 | return 73 | } 74 | if ranges == uint64(0) { 75 | err = errors.New("TPS block window should be greater than 0") 76 | return 77 | } 78 | db := p.indexer.Store.GetDB() 79 | _, tipHeight, err := chainmetautil.GetCurrentEpochAndHeight(p.indexer.Registry, p.indexer.Store) 80 | if err != nil { 81 | err = errors.Wrap(err, "failed to get most recent block height") 82 | return 83 | } 84 | blockLimit := ranges 85 | if tipHeight < ranges { 86 | blockLimit = tipHeight 87 | } 88 | start := tipHeight - blockLimit + 1 89 | end := tipHeight 90 | getQuery := fmt.Sprintf(selectBlockHistory, 91 | blocks.BlockHistoryTableName) 92 | stmt, err := db.Prepare(getQuery) 93 | if err != nil { 94 | err = errors.Wrap(err, "failed to prepare get query") 95 | return 96 | } 97 | defer stmt.Close() 98 | 99 | rows, err := stmt.Query(start, end) 100 | if err != nil { 101 | err = errors.Wrap(err, "failed to execute get query") 102 | return 103 | } 104 | var blk blkInfo 105 | parsedRows, err := s.ParseSQLRows(rows, &blk) 106 | if err != nil { 107 | err = errors.Wrap(err, "failed to parse results") 108 | return 109 | } 110 | if len(parsedRows) == 0 { 111 | err = indexprotocol.ErrNotExist 112 | return 113 | } 114 | var numActions int 115 | startTime := parsedRows[0].(*blkInfo).Timestamp 116 | endTime := parsedRows[0].(*blkInfo).Timestamp 117 | for _, parsedRow := range parsedRows { 118 | blk := parsedRow.(*blkInfo) 119 | numActions += blk.Transfer + blk.Execution + blk.ClaimFromRewardingFund + blk.DepositToRewardingFund + blk.GrantReward + blk.PutPollResult 120 | if blk.Timestamp > startTime { 121 | startTime = blk.Timestamp 122 | } 123 | if blk.Timestamp < endTime { 124 | endTime = blk.Timestamp 125 | } 126 | } 127 | t1 := time.Unix(int64(startTime), 0) 128 | t2 := time.Unix(int64(endTime), 0) 129 | timeDiff := (t1.Sub(t2) + 10*time.Second) / time.Millisecond 130 | tps = float64(numActions*1000) / float64(timeDiff) 131 | return 132 | } 133 | 134 | // GetLastEpochAndHeight gets last epoch number and block height 135 | func (p *Protocol) GetLastEpochAndHeight() (uint64, uint64, error) { 136 | return chainmetautil.GetCurrentEpochAndHeight(p.indexer.Registry, p.indexer.Store) 137 | } 138 | 139 | // GetNumberOfActions gets number of actions 140 | func (p *Protocol) GetNumberOfActions(startEpoch uint64, epochCount uint64) (numberOfActions uint64, err error) { 141 | db := p.indexer.Store.GetDB() 142 | 143 | currentEpoch, _, err := chainmetautil.GetCurrentEpochAndHeight(p.indexer.Registry, p.indexer.Store) 144 | if err != nil { 145 | err = errors.Wrap(err, "failed to get current epoch") 146 | return 147 | } 148 | if startEpoch > currentEpoch { 149 | err = indexprotocol.ErrNotExist 150 | return 151 | } 152 | 153 | endEpoch := startEpoch + epochCount - 1 154 | getQuery := fmt.Sprintf(selectBlockHistorySum, blocks.BlockHistoryTableName) 155 | stmt, err := db.Prepare(getQuery) 156 | if err != nil { 157 | err = errors.Wrap(err, "failed to prepare get query") 158 | return 159 | } 160 | defer stmt.Close() 161 | 162 | if err = stmt.QueryRow(startEpoch, endEpoch).Scan(&numberOfActions); err != nil { 163 | err = errors.Wrap(err, "failed to execute get query") 164 | return 165 | } 166 | return 167 | } 168 | 169 | // GetTotalTransferredTokens gets number of actions 170 | func (p *Protocol) GetTotalTransferredTokens(startEpoch uint64, epochCount uint64) (total string, err error) { 171 | db := p.indexer.Store.GetDB() 172 | currentEpoch, _, err := chainmetautil.GetCurrentEpochAndHeight(p.indexer.Registry, p.indexer.Store) 173 | if err != nil { 174 | err = errors.Wrap(err, "failed to get current epoch") 175 | return 176 | } 177 | if startEpoch > currentEpoch { 178 | err = indexprotocol.ErrNotExist 179 | return 180 | } 181 | endEpoch := startEpoch + epochCount - 1 182 | getQuery := fmt.Sprintf(selectTotalTransferred, accounts.BalanceHistoryTableName) 183 | stmt, err := db.Prepare(getQuery) 184 | if err != nil { 185 | err = errors.Wrap(err, "failed to prepare get query") 186 | return 187 | } 188 | defer stmt.Close() 189 | 190 | if err = stmt.QueryRow(startEpoch, endEpoch).Scan(&total); err != nil { 191 | err = errors.Wrap(err, "failed to execute get query") 192 | return 193 | } 194 | return 195 | } 196 | 197 | // GetTotalSupply 10B - Balance(all zero address) + 2.7B (due to Postmortem 1) - Balance(nsv1) - Balance(bnfx) 198 | func (p *Protocol) GetTotalSupply() (count string, err error) { 199 | // get zero address balance. 200 | zeroAddressBalance, err := p.getBalanceSumByAddress(address.ZeroAddress) 201 | if err != nil { 202 | return "0", err 203 | } 204 | 205 | zeroAddressBalanceInt, ok := new(big.Int).SetString(zeroAddressBalance, 10) 206 | if !ok { 207 | err = errors.New("failed to format to big int:" + zeroAddressBalance) 208 | return 209 | } 210 | 211 | // Convert string format to big.Int format 212 | totalBalanceInt, _ := new(big.Int).SetString(totalBalance, 10) 213 | nsv1BalanceInt, _ := new(big.Int).SetString(nsv1Balance, 10) 214 | bnfxBalanceInt, _ := new(big.Int).SetString(bnfxBalance, 10) 215 | 216 | // Compute 10B + 2.7B (due to Postmortem 1) - Balance(all zero address) - Balance(nsv1) - Balance(bnfx) 217 | return new(big.Int).Sub(new(big.Int).Sub(new(big.Int).Sub(totalBalanceInt, zeroAddressBalanceInt), nsv1BalanceInt), bnfxBalanceInt).String(), nil 218 | } 219 | 220 | // GetTotalCirculatingSupply total supply - SUM(lock addresses) 221 | func (p *Protocol) GetTotalCirculatingSupply(totalSupply string) (count string, err error) { 222 | // Sum lock addresses balances 223 | lockAddressesBalanceInt, err := p.getLockAddressesBalance(strings.Split(lockAddresses, ",")) 224 | if err != nil { 225 | return "0", err 226 | } 227 | 228 | // Convert string format to big.Int format 229 | totalSupplyInt, ok := new(big.Int).SetString(totalSupply, 10) 230 | if !ok { 231 | err = errors.New("failed to format to big int:" + totalSupply) 232 | return "0", err 233 | 234 | } 235 | 236 | // Compute total supply - SUM(lock addresses) 237 | return new(big.Int).Sub(totalSupplyInt, lockAddressesBalanceInt).String(), nil 238 | } 239 | 240 | // GetTotalCirculatingSupplyNoRewardPool totalCirculatingSupply - reward pool fund 241 | func (p *Protocol) GetTotalCirculatingSupplyNoRewardPool(ctx context.Context, totalCirculatingSupply string) (count string, err error) { 242 | // AvailableBalance == Rewards in the pool that has not been issued to anyone 243 | availableRewardInt, err := p.getAvailableReward(ctx) 244 | if err != nil { 245 | return "0", err 246 | } 247 | 248 | // Convert string format to big.Int format 249 | totalCirculatingSupplyInt, ok := new(big.Int).SetString(totalCirculatingSupply, 10) 250 | if !ok { 251 | err = errors.New("failed to format to big int:" + totalCirculatingSupply) 252 | return "0", err 253 | 254 | } 255 | 256 | // Compute totalCirculatingSupply - reward pool fund 257 | return new(big.Int).Sub(totalCirculatingSupplyInt, availableRewardInt).String(), nil 258 | } 259 | 260 | func (p *Protocol) getBalanceSumByAddress(address string) (balance string, err error) { 261 | db := p.indexer.Store.GetDB() 262 | getQuery := fmt.Sprintf(selectBalanceByAddress, accounts.AccountIncomeTableName) 263 | stmt, err := db.Prepare(getQuery) 264 | if err != nil { 265 | err = errors.Wrap(err, "failed to prepare get query") 266 | return 267 | } 268 | 269 | defer stmt.Close() 270 | 271 | if err = stmt.QueryRow(address).Scan(&balance); err != nil { 272 | err = errors.Wrap(err, "failed to execute get query") 273 | return 274 | } 275 | return 276 | } 277 | 278 | func (p *Protocol) getLockAddressesBalance(addresses []string) (*big.Int, error) { 279 | lockAddressesBalanceInt := big.NewInt(0) 280 | for _, address := range addresses { 281 | balance, err := p.getBalanceSumByAddress(address) 282 | if err != nil { 283 | return nil, err 284 | } 285 | balanceInt, ok := new(big.Int).SetString(balance, 10) 286 | if !ok { 287 | err = errors.New("failed to format to big int:" + balance) 288 | return nil, err 289 | } 290 | 291 | lockAddressesBalanceInt = new(big.Int).Add(lockAddressesBalanceInt, balanceInt) 292 | } 293 | return lockAddressesBalanceInt, nil 294 | } 295 | 296 | func (p *Protocol) getAvailableReward(ctx context.Context) (*big.Int, error) { 297 | request := &iotexapi.ReadStateRequest{ 298 | ProtocolID: []byte("rewarding"), 299 | MethodName: []byte("AvailableBalance"), 300 | } 301 | 302 | response, err := p.indexer.ChainClient.ReadState(ctx, request) 303 | if err != nil { 304 | return nil, err 305 | } 306 | availableRewardInt, ok := new(big.Int).SetString(string(response.Data), 10) 307 | if !ok { 308 | err = errors.New("failed to format to big int:" + string(response.Data)) 309 | return nil, err 310 | } 311 | return availableRewardInt, nil 312 | } 313 | -------------------------------------------------------------------------------- /queryprotocol/chainmeta/protocol_test.go: -------------------------------------------------------------------------------- 1 | package chainmeta 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | 10 | "github.com/iotexproject/iotex-analytics/indexprotocol" 11 | "github.com/iotexproject/iotex-analytics/indexservice" 12 | s "github.com/iotexproject/iotex-analytics/sql" 13 | "github.com/iotexproject/iotex-analytics/testutil" 14 | ) 15 | 16 | const ( 17 | connectStr = "bfe10c7cf8aa29:8bed5959@tcp(us-cdbr-east-04.cleardb.com:3306)/" 18 | dbName = "heroku_067cec75e0ba5ba" 19 | ) 20 | 21 | func TestProtocol_MostRecentTPS(t *testing.T) { 22 | 23 | require := require.New(t) 24 | ctx := context.Background() 25 | var err error 26 | 27 | testutil.CleanupDatabase(t, connectStr, dbName) 28 | 29 | store := s.NewMySQL(connectStr, dbName, false) 30 | require.NoError(store.Start(ctx)) 31 | defer func() { 32 | _, err := store.GetDB().Exec("DROP DATABASE " + dbName) 33 | require.NoError(err) 34 | require.NoError(store.Stop(ctx)) 35 | }() 36 | 37 | var cfg indexservice.Config 38 | cfg.Poll = indexprotocol.Poll{ 39 | VoteThreshold: "100000000000000000000", 40 | ScoreThreshold: "0", 41 | SelfStakingThreshold: "0", 42 | } 43 | idx := indexservice.NewIndexer(store, cfg) 44 | p := NewProtocol(idx) 45 | 46 | t.Run("Testing unregistered", func(t *testing.T) { 47 | _, err = p.MostRecentTPS(1) 48 | require.EqualError(err, "blocks protocol is unregistered") 49 | }) 50 | 51 | idx.RegisterDefaultProtocols() 52 | 53 | t.Run("Testing 0 range", func(t *testing.T) { 54 | _, err = p.MostRecentTPS(0) 55 | assert.EqualError(t, err, "TPS block window should be greater than 0") 56 | }) 57 | 58 | } 59 | -------------------------------------------------------------------------------- /queryprotocol/hermes2/protocol.go: -------------------------------------------------------------------------------- 1 | package hermes2 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/pkg/errors" 8 | 9 | "github.com/iotexproject/iotex-analytics/indexprotocol" 10 | "github.com/iotexproject/iotex-analytics/indexprotocol/accounts" 11 | "github.com/iotexproject/iotex-analytics/indexprotocol/actions" 12 | "github.com/iotexproject/iotex-analytics/indexprotocol/votings" 13 | "github.com/iotexproject/iotex-analytics/indexservice" 14 | s "github.com/iotexproject/iotex-analytics/sql" 15 | ) 16 | 17 | const ( 18 | // SelectCountByDelegateName selects the count of Hermes distribution by delegate name 19 | SelectCountByDelegateName = selectCount + fromJoinedTables + delegateFilter 20 | // SelectCountByVoterAddress selects the count of Hermes distribution by voter address 21 | SelectCountByVoterAddress = selectCount + fromJoinedTables + voterFilter 22 | 23 | fromJoinedTables = "FROM (SELECT * FROM %s WHERE epoch_number >= ? AND epoch_number <= ? AND `from` in (%s)) " + 24 | "AS t1 INNER JOIN (SELECT * FROM %s WHERE epoch_number >= ? AND epoch_number <= ?) AS t2 ON t1.action_hash = t2.action_hash " 25 | timeOrdering = "ORDER BY `timestamp` desc limit ?,?" 26 | fromTable = "FROM %s " 27 | selectVoter = "SELECT `to`, from_epoch, to_epoch, amount, t1.action_hash, `timestamp` " 28 | delegateFilter = "WHERE delegate_name = ? " 29 | selectHermesDistributionByDelegateName = selectVoter + fromJoinedTables + delegateFilter + timeOrdering 30 | delegateFilterWithEpochRange = "WHERE delegate_name = ? AND epoch_number >= ? AND epoch_number <= ? " 31 | selectDelegate = "SELECT delegate_name, from_epoch, to_epoch, amount, t1.action_hash, `timestamp` " 32 | voterFilter = "WHERE `to` = ? " 33 | selectHermesDistributionByVoterAddress = selectDelegate + fromJoinedTables + voterFilter + timeOrdering 34 | selectDistributionRatio = "SELECT block_reward_percentage AS block_reward_ratio, epoch_reward_percentage as epoch_reward_ratio, foundation_bonus_percentage as foundation_bonus_ratio, epoch_number " 35 | selectDistributionRatioByDelegateName = selectDistributionRatio + fromTable + delegateFilterWithEpochRange 36 | selectCount = "SELECT COUNT(*),IFNULL(SUM(amount),0) " 37 | selectHermesMeta = "SELECT COUNT(DISTINCT delegate_name), COUNT(DISTINCT `to`), IFNULL(SUM(amount),0) " + fromJoinedTables 38 | ) 39 | 40 | // HermesArg defines Hermes request parameters 41 | type HermesArg struct { 42 | StartEpoch int 43 | EpochCount int 44 | Offset uint64 45 | Size uint64 46 | } 47 | 48 | // VoterInfo defines voter information 49 | type VoterInfo struct { 50 | VoterAddress string 51 | FromEpoch uint64 52 | ToEpoch uint64 53 | Amount string 54 | ActionHash string 55 | Timestamp string 56 | } 57 | 58 | // Ratio defines delegate reward distribution ratio 59 | type Ratio struct { 60 | BlockRewardRatio float64 61 | EpochRewardRatio float64 62 | FoundationBonusRatio float64 63 | EpochNumber int 64 | } 65 | 66 | // DelegateInfo defines delegate information 67 | type DelegateInfo struct { 68 | DelegateName string 69 | FromEpoch uint64 70 | ToEpoch uint64 71 | Amount string 72 | ActionHash string 73 | Timestamp string 74 | } 75 | 76 | // Protocol defines the protocol of querying tables 77 | type Protocol struct { 78 | indexer *indexservice.Indexer 79 | hermesConfig indexprotocol.HermesConfig 80 | } 81 | 82 | // NewProtocol creates a new protocol 83 | func NewProtocol(idx *indexservice.Indexer, cfg indexprotocol.HermesConfig) *Protocol { 84 | return &Protocol{ 85 | indexer: idx, 86 | hermesConfig: cfg, 87 | } 88 | } 89 | 90 | // GetHermes2ByDelegate gets Hermes voter list by delegate name 91 | func (p *Protocol) GetHermes2ByDelegate(arg HermesArg, delegateName string) ([]*VoterInfo, error) { 92 | db := p.indexer.Store.GetDB() 93 | getQuery := fmt.Sprintf(selectHermesDistributionByDelegateName, accounts.BalanceHistoryTableName, strings.Join(wrapperQueryValue(p.hermesConfig.MultiSendContractAddressList), ","), actions.HermesContractTableName) 94 | stmt, err := db.Prepare(getQuery) 95 | if err != nil { 96 | return nil, errors.Wrap(err, "failed to prepare get query") 97 | } 98 | defer stmt.Close() 99 | endEpoch := arg.StartEpoch + arg.EpochCount - 1 100 | rows, err := stmt.Query(arg.StartEpoch, endEpoch, arg.StartEpoch, endEpoch, 101 | delegateName, arg.Offset, arg.Size) 102 | if err != nil { 103 | return nil, errors.Wrap(err, "failed to execute get query") 104 | } 105 | 106 | var voterInfo VoterInfo 107 | parsedRows, err := s.ParseSQLRows(rows, &voterInfo) 108 | if err != nil { 109 | return nil, errors.Wrap(err, "failed to parse results") 110 | } 111 | if len(parsedRows) == 0 { 112 | return nil, indexprotocol.ErrNotExist 113 | } 114 | 115 | voterInfoList := make([]*VoterInfo, 0) 116 | for _, parsedRow := range parsedRows { 117 | voterInfoList = append(voterInfoList, parsedRow.(*VoterInfo)) 118 | } 119 | 120 | return voterInfoList, nil 121 | } 122 | 123 | // GetHermes2ByVoter gets Hermes delegate list by voter name 124 | func (p *Protocol) GetHermes2ByVoter(arg HermesArg, voterAddress string) ([]*DelegateInfo, error) { 125 | db := p.indexer.Store.GetDB() 126 | getQuery := fmt.Sprintf(selectHermesDistributionByVoterAddress, accounts.BalanceHistoryTableName, strings.Join(wrapperQueryValue(p.hermesConfig.MultiSendContractAddressList), ","), actions.HermesContractTableName) 127 | stmt, err := db.Prepare(getQuery) 128 | if err != nil { 129 | return nil, errors.Wrap(err, "failed to prepare get query") 130 | } 131 | defer stmt.Close() 132 | 133 | endEpoch := arg.StartEpoch + arg.EpochCount - 1 134 | rows, err := stmt.Query(arg.StartEpoch, endEpoch, arg.StartEpoch, endEpoch, 135 | voterAddress, arg.Offset, arg.Size) 136 | if err != nil { 137 | return nil, errors.Wrap(err, "failed to execute get query") 138 | } 139 | 140 | var delegateInfo DelegateInfo 141 | parsedRows, err := s.ParseSQLRows(rows, &delegateInfo) 142 | if err != nil { 143 | return nil, errors.Wrap(err, "failed to parse results") 144 | } 145 | if len(parsedRows) == 0 { 146 | return nil, indexprotocol.ErrNotExist 147 | } 148 | 149 | delegateInfoList := make([]*DelegateInfo, 0) 150 | for _, parsedRow := range parsedRows { 151 | delegateInfoList = append(delegateInfoList, parsedRow.(*DelegateInfo)) 152 | } 153 | 154 | return delegateInfoList, nil 155 | } 156 | 157 | // GetHermes2Ratio gets Hermes distribution ratio list by delegate name 158 | func (p *Protocol) GetHermes2Ratio(arg HermesArg, delegateName string) ([]*Ratio, error) { 159 | 160 | db := p.indexer.Store.GetDB() 161 | getQuery := fmt.Sprintf(selectDistributionRatioByDelegateName, votings.VotingResultTableName) 162 | stmt, err := db.Prepare(getQuery) 163 | if err != nil { 164 | return nil, errors.Wrap(err, "failed to prepare get query") 165 | } 166 | defer stmt.Close() 167 | 168 | endEpoch := arg.StartEpoch + arg.EpochCount - 1 169 | rows, err := stmt.Query(delegateName, arg.StartEpoch, endEpoch) 170 | if err != nil { 171 | return nil, errors.Wrap(err, "failed to execute get query") 172 | } 173 | 174 | var distributionRatioInfo Ratio 175 | parsedRows, err := s.ParseSQLRows(rows, &distributionRatioInfo) 176 | if err != nil { 177 | return nil, errors.Wrap(err, "failed to parse results") 178 | } 179 | if len(parsedRows) == 0 { 180 | return nil, indexprotocol.ErrNotExist 181 | } 182 | distributionRatioList := make([]*Ratio, 0) 183 | for _, parsedRow := range parsedRows { 184 | distributionRatioList = append(distributionRatioList, parsedRow.(*Ratio)) 185 | } 186 | return distributionRatioList, nil 187 | } 188 | 189 | // GetHermes2Count gets the count of Hermes distributions 190 | func (p *Protocol) GetHermes2Count(arg HermesArg, selectQuery string, filter string) (count int, total string, err error) { 191 | db := p.indexer.Store.GetDB() 192 | getQuery := fmt.Sprintf(selectQuery, accounts.BalanceHistoryTableName, strings.Join(wrapperQueryValue(p.hermesConfig.MultiSendContractAddressList), ","), actions.HermesContractTableName) 193 | stmt, err := db.Prepare(getQuery) 194 | if err != nil { 195 | err = errors.Wrap(err, "failed to prepare get query") 196 | return 197 | } 198 | defer stmt.Close() 199 | 200 | endEpoch := arg.StartEpoch + arg.EpochCount - 1 201 | if err = stmt.QueryRow(arg.StartEpoch, endEpoch, arg.StartEpoch, endEpoch, 202 | filter).Scan(&count, &total); err != nil { 203 | err = errors.Wrap(err, "failed to execute get query") 204 | return 205 | } 206 | return 207 | } 208 | 209 | // GetHermes2Meta gets the hermes meta info 210 | func (p *Protocol) GetHermes2Meta(startEpoch int, epochCount int) (numberOfDelegates int, 211 | numberOfRecipients int, totalRewardsDistributed string, err error) { 212 | endEpoch := startEpoch + epochCount - 1 213 | db := p.indexer.Store.GetDB() 214 | getQuery := fmt.Sprintf(selectHermesMeta, accounts.BalanceHistoryTableName, strings.Join(wrapperQueryValue(p.hermesConfig.MultiSendContractAddressList), ","), actions.HermesContractTableName) 215 | stmt, err := db.Prepare(getQuery) 216 | if err != nil { 217 | err = errors.Wrap(err, "failed to prepare get query") 218 | return 219 | } 220 | defer stmt.Close() 221 | if err = stmt.QueryRow(startEpoch, endEpoch, startEpoch, endEpoch). 222 | Scan(&numberOfDelegates, &numberOfRecipients, &totalRewardsDistributed); err != nil { 223 | err = errors.Wrap(err, "failed to execute get query") 224 | return 225 | } 226 | return 227 | } 228 | 229 | func wrapperQueryValue(queryValues []string) []string { 230 | ret := make([]string, len(queryValues)) 231 | for index, str := range queryValues { 232 | ret[index] = "'" + str + "'" 233 | } 234 | return ret 235 | } 236 | -------------------------------------------------------------------------------- /queryprotocol/productivity/protocol.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 IoTeX 2 | // This is an alpha (internal) release and is not suitable for production. This source code is provided 'as is' and no 3 | // warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent 4 | // permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache 5 | // License 2.0 that can be found in the LICENSE file. 6 | 7 | package productivity 8 | 9 | import ( 10 | "fmt" 11 | 12 | "github.com/pkg/errors" 13 | 14 | "github.com/iotexproject/iotex-analytics/indexprotocol" 15 | "github.com/iotexproject/iotex-analytics/indexprotocol/blocks" 16 | "github.com/iotexproject/iotex-analytics/indexservice" 17 | "github.com/iotexproject/iotex-analytics/queryprotocol" 18 | "github.com/iotexproject/iotex-analytics/queryprotocol/chainmeta/chainmetautil" 19 | s "github.com/iotexproject/iotex-analytics/sql" 20 | ) 21 | 22 | const ( 23 | selectProductivity = "SELECT * FROM %s WHERE epoch_number >= ? and epoch_number <= ? and delegate_name = ?" 24 | selectProductivitySum = "SELECT SUM(production), SUM(expected_production) FROM %s WHERE " + 25 | "epoch_number >= %d AND epoch_number <= %d AND delegate_name=?" 26 | selectProductivitySumGroup = "SELECT SUM(production),SUM(expected_production) FROM %s WHERE epoch_number>=? AND epoch_number<=? GROUP BY delegate_name" 27 | ) 28 | 29 | // Protocol defines the protocol of querying tables 30 | type Protocol struct { 31 | indexer *indexservice.Indexer 32 | } 33 | 34 | type productivity struct { 35 | SumOfProduction uint64 36 | SumOfExpectedProduction uint64 37 | } 38 | 39 | // NewProtocol creates a new protocol 40 | func NewProtocol(idx *indexservice.Indexer) *Protocol { 41 | return &Protocol{indexer: idx} 42 | } 43 | 44 | // GetProductivityHistory gets productivity history 45 | func (p *Protocol) GetProductivityHistory(startEpoch uint64, epochCount uint64, producerName string) (string, string, error) { 46 | if _, ok := p.indexer.Registry.Find(blocks.ProtocolID); !ok { 47 | return "", "", errors.New("blocks protocol is unregistered") 48 | } 49 | 50 | db := p.indexer.Store.GetDB() 51 | 52 | endEpoch := startEpoch + epochCount - 1 53 | 54 | // Check existence 55 | exist, err := queryprotocol.RowExists(db, fmt.Sprintf(selectProductivity, 56 | blocks.ProductivityTableName), startEpoch, endEpoch, producerName) 57 | if err != nil { 58 | return "", "", errors.Wrap(err, "failed to check if the row exists") 59 | } 60 | if !exist { 61 | return "", "", indexprotocol.ErrNotExist 62 | } 63 | 64 | getQuery := fmt.Sprintf(selectProductivitySum, blocks.ProductivityTableName, startEpoch, endEpoch) 65 | stmt, err := db.Prepare(getQuery) 66 | if err != nil { 67 | return "", "", errors.Wrap(err, "failed to prepare get query") 68 | } 69 | defer stmt.Close() 70 | 71 | var production, expectedProduction string 72 | if err = stmt.QueryRow(producerName).Scan(&production, &expectedProduction); err != nil { 73 | return "", "", errors.Wrap(err, "failed to execute get query") 74 | } 75 | return production, expectedProduction, nil 76 | } 77 | 78 | // GetAverageProductivity handles GetAverageProductivity request 79 | func (p *Protocol) GetAverageProductivity(startEpoch uint64, epochCount uint64) (averageProcucitvity float64, err error) { 80 | if _, ok := p.indexer.Registry.Find(blocks.ProtocolID); !ok { 81 | err = errors.New("blocks protocol is unregistered") 82 | return 83 | } 84 | 85 | currentEpoch, _, err := chainmetautil.GetCurrentEpochAndHeight(p.indexer.Registry, p.indexer.Store) 86 | if err != nil { 87 | err = errors.Wrap(err, "failed to get current epoch number") 88 | } 89 | if startEpoch > currentEpoch { 90 | err = errors.New("epoch number is not exist") 91 | return 92 | } 93 | 94 | db := p.indexer.Store.GetDB() 95 | 96 | getQuery := fmt.Sprintf(selectProductivitySumGroup, blocks.ProductivityTableName) 97 | stmt, err := db.Prepare(getQuery) 98 | if err != nil { 99 | err = errors.Wrap(err, "failed to prepare get query") 100 | return 101 | } 102 | defer stmt.Close() 103 | 104 | rows, err := stmt.Query(startEpoch, startEpoch+epochCount-1) 105 | if err != nil { 106 | err = errors.Wrap(err, "failed to execute get query") 107 | return 108 | } 109 | 110 | var product productivity 111 | parsedRows, err := s.ParseSQLRows(rows, &product) 112 | if err != nil { 113 | err = errors.Wrap(err, "failed to parse results") 114 | return 115 | } 116 | 117 | if len(parsedRows) == 0 { 118 | err = indexprotocol.ErrNotExist 119 | return 120 | } 121 | var productivitySums float64 122 | for _, parsedRow := range parsedRows { 123 | p := parsedRow.(*productivity) 124 | productivitySums += float64(p.SumOfProduction) / float64(p.SumOfExpectedProduction) 125 | } 126 | averageProcucitvity = productivitySums / float64(len(parsedRows)) 127 | return 128 | } 129 | -------------------------------------------------------------------------------- /queryprotocol/protocol.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 IoTeX 2 | // This is an alpha (internal) release and is not suitable for production. This source code is provided 'as is' and no 3 | // warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent 4 | // permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache 5 | // License 2.0 that can be found in the LICENSE file. 6 | 7 | package queryprotocol 8 | 9 | import ( 10 | "database/sql" 11 | "fmt" 12 | 13 | "github.com/pkg/errors" 14 | ) 15 | 16 | // RowExists checks whether a row exists 17 | func RowExists(db *sql.DB, query string, args ...interface{}) (bool, error) { 18 | var exists bool 19 | query = fmt.Sprintf("SELECT exists (%s)", query) 20 | stmt, err := db.Prepare(query) 21 | if err != nil { 22 | return false, errors.Wrap(err, "failed to prepare query") 23 | } 24 | defer stmt.Close() 25 | 26 | err = stmt.QueryRow(args...).Scan(&exists) 27 | if err != nil && err != sql.ErrNoRows { 28 | return false, errors.Wrap(err, "failed to query the row") 29 | } 30 | return exists, nil 31 | } 32 | -------------------------------------------------------------------------------- /sql/mysql.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 IoTeX 2 | // This is an alpha (internal) release and is not suitable for production. This source code is provided 'as is' and no 3 | // warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent 4 | // permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache 5 | // License 2.0 that can be found in the LICENSE file. 6 | 7 | package sql 8 | 9 | import ( 10 | // this is required for mysql usage 11 | _ "github.com/go-sql-driver/mysql" 12 | ) 13 | 14 | // NewMySQL instantiates a mysql 15 | func NewMySQL(connectStr string, dbName string, readOnly bool) Store { 16 | return newStoreBase("mysql", connectStr, dbName, readOnly) 17 | } 18 | -------------------------------------------------------------------------------- /sql/mysql_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 IoTeX 2 | // This is an alpha (internal) release and is not suitable for production. This source code is provided 'as is' and no 3 | // warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent 4 | // permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache 5 | // License 2.0 that can be found in the LICENSE file. 6 | 7 | package sql 8 | 9 | import ( 10 | "testing" 11 | 12 | "github.com/iotexproject/iotex-analytics/testutil" 13 | ) 14 | 15 | const ( 16 | connectStr = "bfe10c7cf8aa29:8bed5959@tcp(us-cdbr-east-04.cleardb.com:3306)/" 17 | dbName = "heroku_067cec75e0ba5ba" 18 | ) 19 | 20 | func TestMySQLStorePutGet(t *testing.T) { 21 | testutil.CleanupDatabase(t, connectStr, dbName) 22 | testRDSStorePutGet := TestStorePutGet 23 | t.Run("MySQL Store", func(t *testing.T) { 24 | testRDSStorePutGet(NewMySQL(connectStr, dbName, false), t) 25 | }) 26 | testutil.CleanupDatabase(t, connectStr, dbName) 27 | } 28 | 29 | func TestMySQLStoreTransaction(t *testing.T) { 30 | testutil.CleanupDatabase(t, connectStr, dbName) 31 | testSQLite3StoreTransaction := TestStoreTransaction 32 | t.Run("MySQL Store", func(t *testing.T) { 33 | testSQLite3StoreTransaction(NewMySQL(connectStr, dbName, false), t) 34 | }) 35 | testutil.CleanupDatabase(t, connectStr, dbName) 36 | } 37 | -------------------------------------------------------------------------------- /sql/rds.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 IoTeX 2 | // This is an alpha (internal) release and is not suitable for production. This source code is provided 'as is' and no 3 | // warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent 4 | // permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache 5 | // License 2.0 that can be found in the LICENSE file. 6 | 7 | package sql 8 | 9 | import ( 10 | "fmt" 11 | 12 | // we need mysql import because it's called in file, (but compile will complain because there is no display) 13 | _ "github.com/go-sql-driver/mysql" 14 | ) 15 | 16 | // RDS is the cloud rds config 17 | type RDS struct { 18 | // AwsRDSEndpoint is the endpoint of aws rds 19 | AwsRDSEndpoint string `yaml:"awsRDSEndpoint"` 20 | // AwsRDSPort is the port of aws rds 21 | AwsRDSPort uint64 `yaml:"awsRDSPort"` 22 | // AwsRDSUser is the user to access aws rds 23 | AwsRDSUser string `yaml:"awsRDSUser"` 24 | // AwsPass is the pass to access aws rds 25 | AwsPass string `yaml:"awsPass"` 26 | // AwsDBName is the db name of aws rds 27 | AwsDBName string `yaml:"awsDBName"` 28 | // AwsMaxConns is the max num of connections 29 | AwsMaxConns uint16 `yaml:"awsMaxConn"` 30 | } 31 | 32 | // NewAwsRDS instantiates an aws rds 33 | func NewAwsRDS(cfg RDS) Store { 34 | connectStr := fmt.Sprintf("%s:%s@tcp(%s:%d)/", 35 | cfg.AwsRDSUser, cfg.AwsPass, cfg.AwsRDSEndpoint, cfg.AwsRDSPort) 36 | store := newStoreBase("mysql", connectStr, cfg.AwsDBName, false) 37 | if cfg.AwsMaxConns > 0 { 38 | store.SetMaxOpenConns(int(cfg.AwsMaxConns)) 39 | } 40 | return store 41 | } 42 | -------------------------------------------------------------------------------- /sql/rds_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 IoTeX 2 | // This is an alpha (internal) release and is not suitable for production. This source code is provided 'as is' and no 3 | // warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent 4 | // permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache 5 | // License 2.0 that can be found in the LICENSE file. 6 | 7 | package sql 8 | 9 | import ( 10 | "testing" 11 | ) 12 | 13 | func TestRDSStorePutGet(t *testing.T) { 14 | t.Skip("Skipping when RDS credentail not provided.") 15 | testRDSStorePutGet := TestStorePutGet 16 | 17 | cfg := RDS{} 18 | t.Run("RDS Store", func(t *testing.T) { 19 | testRDSStorePutGet(NewAwsRDS(cfg), t) 20 | }) 21 | } 22 | 23 | func TestRDSStoreTransaction(t *testing.T) { 24 | t.Skip("Skipping when RDS credentail not provided.") 25 | testRDSStoreTransaction := TestStoreTransaction 26 | 27 | cfg := RDS{} 28 | t.Run("RDS Store", func(t *testing.T) { 29 | testRDSStoreTransaction(NewAwsRDS(cfg), t) 30 | }) 31 | } 32 | -------------------------------------------------------------------------------- /sql/storebase.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 IoTeX 2 | // This is an alpha (internal) release and is not suitable for production. This source code is provided 'as is' and no 3 | // warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent 4 | // permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache 5 | // License 2.0 that can be found in the LICENSE file. 6 | 7 | package sql 8 | 9 | import ( 10 | "context" 11 | "database/sql" 12 | "os" 13 | "sync" 14 | "time" 15 | 16 | "github.com/rs/zerolog" 17 | 18 | // this is required for mysql usage 19 | "github.com/iotexproject/iotex-core/pkg/lifecycle" 20 | ) 21 | 22 | // Store is the interface of KV store. 23 | type Store interface { 24 | lifecycle.StartStopper 25 | 26 | // Get DB instance 27 | GetDB() *sql.DB 28 | 29 | // Transact wrap the transaction 30 | Transact(txFunc func(*sql.Tx) error) (err error) 31 | 32 | // SetMaxOpenConns sets the max number of open connections 33 | SetMaxOpenConns(int) 34 | } 35 | 36 | // storebase is a MySQL instance 37 | type storeBase struct { 38 | mutex sync.RWMutex 39 | db *sql.DB 40 | maxConns int 41 | connectStr string 42 | dbName string 43 | driverName string 44 | readOnly bool 45 | } 46 | 47 | // logger is initialized with default settings 48 | var logger = zerolog.New(os.Stderr).Level(zerolog.InfoLevel).With().Timestamp().Logger() 49 | 50 | // NewStoreBase instantiates an store base 51 | func newStoreBase(driverName string, connectStr string, dbName string, readOnly bool) Store { 52 | return &storeBase{db: nil, connectStr: connectStr, dbName: dbName, driverName: driverName, readOnly: readOnly} 53 | } 54 | 55 | // Start opens the SQL (creates new file if not existing yet) 56 | func (s *storeBase) Start(ctx context.Context) error { 57 | s.mutex.Lock() 58 | defer s.mutex.Unlock() 59 | 60 | if s.db != nil { 61 | return nil 62 | } 63 | 64 | if !s.readOnly { 65 | // Use db to perform SQL operations on database 66 | db, err := sql.Open(s.driverName, s.connectStr) 67 | if err != nil { 68 | return err 69 | } 70 | if _, err = db.Exec("CREATE DATABASE IF NOT EXISTS " + s.dbName); err != nil { 71 | return err 72 | } 73 | db.Close() 74 | } 75 | 76 | db, err := sql.Open(s.driverName, s.connectStr+s.dbName+"?autocommit=false&parseTime=true") 77 | if err != nil { 78 | return err 79 | } 80 | s.db = db 81 | s.db.SetMaxOpenConns(s.maxConns) 82 | s.db.SetMaxIdleConns(10) 83 | s.db.SetConnMaxLifetime(5 * time.Minute) 84 | 85 | return nil 86 | } 87 | 88 | // Stop closes the SQL 89 | func (s *storeBase) Stop(_ context.Context) error { 90 | s.mutex.Lock() 91 | defer s.mutex.Unlock() 92 | 93 | if s.db != nil { 94 | err := s.db.Close() 95 | s.db = nil 96 | return err 97 | } 98 | return nil 99 | } 100 | 101 | func (s *storeBase) SetMaxOpenConns(size int) { 102 | s.maxConns = size 103 | } 104 | 105 | func (s *storeBase) GetDB() *sql.DB { 106 | s.mutex.RLock() 107 | defer s.mutex.RUnlock() 108 | 109 | return s.db 110 | } 111 | 112 | // Transact wrap the transaction 113 | func (s *storeBase) Transact(txFunc func(*sql.Tx) error) (err error) { 114 | tx, err := s.db.Begin() 115 | if err != nil { 116 | return err 117 | } 118 | defer func() { 119 | switch { 120 | case recover() != nil: 121 | if rollbackErr := tx.Rollback(); rollbackErr != nil { 122 | logger.Error().Err(rollbackErr) // log err after Rollback 123 | } 124 | case err != nil: 125 | // err is non-nil; don't change it 126 | if rollbackErr := tx.Rollback(); rollbackErr != nil { 127 | logger.Error().Err(rollbackErr) 128 | } 129 | default: 130 | // err is nil; if Commit returns error update err 131 | if commitErr := tx.Commit(); commitErr != nil { 132 | logger.Error().Err(commitErr) 133 | } 134 | } 135 | }() 136 | err = txFunc(tx) 137 | return err 138 | } 139 | -------------------------------------------------------------------------------- /sql/storebase_tests.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 IoTeX 2 | // This is an alpha (internal) release and is not suitable for production. This source code is provided 'as is' and no 3 | // warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent 4 | // permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache 5 | // License 2.0 that can be found in the LICENSE file. 6 | 7 | package sql 8 | 9 | import ( 10 | "context" 11 | "database/sql" 12 | "errors" 13 | "testing" 14 | 15 | "github.com/stretchr/testify/require" 16 | 17 | "github.com/iotexproject/go-pkgs/hash" 18 | ) 19 | 20 | // ActionHistory define the schema for action history 21 | type ActionHistory struct { 22 | NodeAddress string 23 | UserAddress string 24 | ActionHash string 25 | } 26 | 27 | // TestStorePutGet define the common test cases for put and get 28 | func TestStorePutGet(sqlStore Store, t *testing.T) { 29 | require := require.New(t) 30 | ctx := context.Background() 31 | 32 | err := sqlStore.Start(ctx) 33 | require.Nil(err) 34 | defer func() { 35 | err = sqlStore.Stop(ctx) 36 | require.Nil(err) 37 | }() 38 | 39 | dbinstance := sqlStore.GetDB() 40 | 41 | nodeAddress := "aaa" 42 | userAddress := "bbb" 43 | actionHash := hash.ZeroHash256 44 | 45 | // create table 46 | _, err = dbinstance.Exec("CREATE TABLE IF NOT EXISTS action_history (node_address TEXT NOT NULL, user_address " + 47 | "TEXT NOT NULL, action_hash BLOB(32) NOT NULL)") 48 | require.Nil(err) 49 | 50 | // insert 51 | stmt, err := dbinstance.Prepare("INSERT INTO action_history (node_address,user_address,action_hash) VALUES (?, ?, ?)") 52 | require.Nil(err) 53 | defer stmt.Close() 54 | 55 | res, err := stmt.Exec(nodeAddress, userAddress, actionHash[:]) 56 | require.Nil(err) 57 | 58 | affect, err := res.RowsAffected() 59 | require.Nil(err) 60 | require.Equal(int64(1), affect) 61 | 62 | // get 63 | stmt, err = dbinstance.Prepare("SELECT * FROM action_history WHERE node_address=?") 64 | require.Nil(err) 65 | defer stmt.Close() 66 | 67 | rows, err := stmt.Query(nodeAddress) 68 | require.Nil(err) 69 | 70 | var actionHistory ActionHistory 71 | parsedRows, err := ParseSQLRows(rows, &actionHistory) 72 | require.Nil(err) 73 | require.Equal(1, len(parsedRows)) 74 | require.Equal(nodeAddress, parsedRows[0].(*ActionHistory).NodeAddress) 75 | require.Equal(userAddress, parsedRows[0].(*ActionHistory).UserAddress) 76 | require.Equal(string(actionHash[:]), parsedRows[0].(*ActionHistory).ActionHash) 77 | 78 | // delete 79 | stmt, err = dbinstance.Prepare("DELETE FROM action_history WHERE node_address=? AND user_address=? AND action_hash=?") 80 | require.Nil(err) 81 | defer stmt.Close() 82 | 83 | res, err = stmt.Exec(nodeAddress, userAddress, actionHash[:]) 84 | require.Nil(err) 85 | 86 | affect, err = res.RowsAffected() 87 | require.Nil(err) 88 | require.Equal(int64(1), affect) 89 | 90 | // get 91 | stmt, err = dbinstance.Prepare("SELECT * FROM action_history WHERE node_address=?") 92 | require.Nil(err) 93 | defer stmt.Close() 94 | 95 | rows, err = stmt.Query(nodeAddress) 96 | require.Nil(err) 97 | 98 | parsedRows, err = ParseSQLRows(rows, &actionHistory) 99 | require.Nil(err) 100 | require.Equal(0, len(parsedRows)) 101 | } 102 | 103 | // TestStoreTransaction define the common test cases for transaction 104 | func TestStoreTransaction(sqlStore Store, t *testing.T) { 105 | require := require.New(t) 106 | ctx := context.Background() 107 | 108 | err := sqlStore.Start(ctx) 109 | require.Nil(err) 110 | defer func() { 111 | err = sqlStore.Stop(ctx) 112 | require.Nil(err) 113 | }() 114 | 115 | dbinstance := sqlStore.GetDB() 116 | 117 | nodeAddress := "aaa" 118 | userAddress1 := "bbb1" 119 | userAddress2 := "bbb2" 120 | actionHash := hash.ZeroHash256 121 | 122 | // create table 123 | _, err = dbinstance.Exec("CREATE TABLE IF NOT EXISTS action_history (node_address TEXT NOT NULL, user_address " + 124 | "TEXT NOT NULL, action_hash BLOB(32) NOT NULL)") 125 | require.Nil(err) 126 | 127 | // get 128 | stmt, err := dbinstance.Prepare("SELECT * FROM action_history WHERE node_address=? AND user_address=?") 129 | require.Nil(err) 130 | defer stmt.Close() 131 | rows, err := stmt.Query(nodeAddress, userAddress1) 132 | require.Nil(err) 133 | var actionHistory ActionHistory 134 | parsedRows, err := ParseSQLRows(rows, &actionHistory) 135 | require.Nil(err) 136 | require.Equal(0, len(parsedRows)) 137 | 138 | stmt, err = dbinstance.Prepare("SELECT * FROM action_history WHERE node_address=? AND user_address=?") 139 | require.Nil(err) 140 | defer stmt.Close() 141 | rows, err = stmt.Query(nodeAddress, userAddress2) 142 | require.Nil(err) 143 | parsedRows, err = ParseSQLRows(rows, &actionHistory) 144 | require.Nil(err) 145 | require.Equal(0, len(parsedRows)) 146 | 147 | // insert transaction with fail 148 | err = sqlStore.Transact(func(tx *sql.Tx) error { 149 | insertQuery := "INSERT INTO action_history (node_address,user_address,action_hash) VALUES (?, ?, ?)" 150 | if _, err := tx.Exec(insertQuery, nodeAddress, userAddress1, actionHash[:]); err != nil { 151 | return err 152 | } 153 | if _, err := tx.Exec(insertQuery, nodeAddress, userAddress1, actionHash[:]); err != nil { 154 | return errors.New("create an error") 155 | } 156 | return errors.New("create an error") 157 | }) 158 | println(err) 159 | require.NotNil(err) 160 | 161 | // get 162 | stmt, err = dbinstance.Prepare("SELECT * FROM action_history WHERE node_address=? AND user_address=?") 163 | require.Nil(err) 164 | defer stmt.Close() 165 | rows, err = stmt.Query(nodeAddress, userAddress1) 166 | require.Nil(err) 167 | parsedRows, err = ParseSQLRows(rows, &actionHistory) 168 | require.Nil(err) 169 | require.Equal(0, len(parsedRows)) 170 | 171 | stmt, err = dbinstance.Prepare("SELECT * FROM action_history WHERE node_address=? AND user_address=?") 172 | require.Nil(err) 173 | defer stmt.Close() 174 | rows, err = stmt.Query(nodeAddress, userAddress2) 175 | require.Nil(err) 176 | parsedRows, err = ParseSQLRows(rows, &actionHistory) 177 | require.Nil(err) 178 | require.Equal(0, len(parsedRows)) 179 | 180 | // insert 181 | err = sqlStore.Transact(func(tx *sql.Tx) error { 182 | insertQuery := "INSERT INTO action_history (node_address,user_address,action_hash) VALUES (?, ?, ?)" 183 | if _, err := tx.Exec(insertQuery, nodeAddress, userAddress1, actionHash[:]); err != nil { 184 | return err 185 | } 186 | if _, err := tx.Exec(insertQuery, nodeAddress, userAddress2, actionHash[:]); err != nil { 187 | return err 188 | } 189 | return nil 190 | }) 191 | require.Nil(err) 192 | 193 | // get 194 | stmt, err = dbinstance.Prepare("SELECT * FROM action_history WHERE node_address=? AND user_address=?") 195 | require.Nil(err) 196 | defer stmt.Close() 197 | rows, err = stmt.Query(nodeAddress, userAddress1) 198 | require.Nil(err) 199 | parsedRows, err = ParseSQLRows(rows, &actionHistory) 200 | require.Nil(err) 201 | require.Equal(1, len(parsedRows)) 202 | require.Equal(nodeAddress, parsedRows[0].(*ActionHistory).NodeAddress) 203 | require.Equal(userAddress1, parsedRows[0].(*ActionHistory).UserAddress) 204 | require.Equal(string(actionHash[:]), parsedRows[0].(*ActionHistory).ActionHash) 205 | 206 | stmt, err = dbinstance.Prepare("SELECT * FROM action_history WHERE node_address=? AND user_address=?") 207 | require.Nil(err) 208 | defer stmt.Close() 209 | rows, err = stmt.Query(nodeAddress, userAddress2) 210 | require.Nil(err) 211 | parsedRows, err = ParseSQLRows(rows, &actionHistory) 212 | require.Nil(err) 213 | require.Equal(1, len(parsedRows)) 214 | require.Equal(nodeAddress, parsedRows[0].(*ActionHistory).NodeAddress) 215 | require.Equal(userAddress2, parsedRows[0].(*ActionHistory).UserAddress) 216 | require.Equal(string(actionHash[:]), parsedRows[0].(*ActionHistory).ActionHash) 217 | 218 | // delete 219 | err = sqlStore.Transact(func(tx *sql.Tx) error { 220 | deleteQuery := "DELETE FROM action_history WHERE node_address=? AND user_address=? AND action_hash=?" 221 | if _, err := tx.Exec(deleteQuery, nodeAddress, userAddress1, actionHash[:]); err != nil { 222 | return err 223 | } 224 | if _, err := tx.Exec(deleteQuery, nodeAddress, userAddress2, actionHash[:]); err != nil { 225 | return err 226 | } 227 | return nil 228 | }) 229 | require.Nil(err) 230 | 231 | // get 232 | stmt, err = dbinstance.Prepare("SELECT * FROM action_history WHERE node_address=? AND user_address=?") 233 | require.Nil(err) 234 | defer stmt.Close() 235 | rows, err = stmt.Query(nodeAddress, userAddress1) 236 | require.Nil(err) 237 | parsedRows, err = ParseSQLRows(rows, &actionHistory) 238 | require.Nil(err) 239 | require.Equal(0, len(parsedRows)) 240 | 241 | stmt, err = dbinstance.Prepare("SELECT * FROM action_history WHERE node_address=? AND user_address=?") 242 | require.Nil(err) 243 | defer stmt.Close() 244 | rows, err = stmt.Query(nodeAddress, userAddress2) 245 | require.Nil(err) 246 | parsedRows, err = ParseSQLRows(rows, &actionHistory) 247 | require.Nil(err) 248 | require.Equal(0, len(parsedRows)) 249 | } 250 | -------------------------------------------------------------------------------- /sql/util.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 IoTeX 2 | // This is an alpha (internal) release and is not suitable for production. This source code is provided 'as is' and no 3 | // warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent 4 | // permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache 5 | // License 2.0 that can be found in the LICENSE file. 6 | 7 | package sql 8 | 9 | import ( 10 | "database/sql" 11 | "reflect" 12 | ) 13 | 14 | // ParseSQLRows will parse the row 15 | func ParseSQLRows(rows *sql.Rows, schema interface{}) ([]interface{}, error) { 16 | var parsedRows []interface{} 17 | 18 | // Fetch rows 19 | for rows.Next() { 20 | newSchema := reflect.New(reflect.ValueOf(schema).Elem().Type()).Interface() 21 | 22 | s := reflect.ValueOf(newSchema).Elem() 23 | 24 | var fields []interface{} 25 | for i := 0; i < s.NumField(); i++ { 26 | fields = append(fields, s.Field(i).Addr().Interface()) 27 | } 28 | 29 | err := rows.Scan(fields...) 30 | if err != nil { 31 | return nil, err 32 | } 33 | parsedRows = append(parsedRows, newSchema) 34 | } 35 | 36 | return parsedRows, nil 37 | } 38 | -------------------------------------------------------------------------------- /testutil/blockbuilder.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 IoTeX 2 | // This is an alpha (internal) release and is not suitable for production. This source code is provided 'as is' and no 3 | // warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent 4 | // permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache 5 | // License 2.0 that can be found in the LICENSE file. 6 | 7 | package testutil 8 | 9 | import ( 10 | "encoding/hex" 11 | "math/big" 12 | 13 | "github.com/golang/protobuf/proto" 14 | "github.com/golang/protobuf/ptypes" 15 | 16 | "github.com/iotexproject/go-pkgs/hash" 17 | "github.com/iotexproject/iotex-address/address" 18 | "github.com/iotexproject/iotex-core/action" 19 | "github.com/iotexproject/iotex-core/action/protocol/rewarding/rewardingpb" 20 | "github.com/iotexproject/iotex-core/blockchain/block" 21 | "github.com/iotexproject/iotex-core/pkg/version" 22 | "github.com/iotexproject/iotex-core/test/identityset" 23 | "github.com/iotexproject/iotex-proto/golang/iotextypes" 24 | ) 25 | 26 | var ( 27 | // Addr1 is a testing address 28 | Addr1 = identityset.Address(0).String() 29 | // PubKey1 is a testing public key 30 | PubKey1 = identityset.PrivateKey(0).PublicKey() 31 | // Addr2 is a testing address 32 | Addr2 = identityset.Address(1).String() 33 | // PubKey2 is testing public key 34 | PubKey2 = identityset.PrivateKey(1).PublicKey() 35 | // RewardAddr1 is a testing reward address 36 | RewardAddr1 = identityset.Address(2).String() 37 | // RewardAddr2 is a testing reward address 38 | RewardAddr2 = identityset.Address(3).String() 39 | // RewardAddr3 is a testing reward address 40 | RewardAddr3 = identityset.Address(4).String() 41 | // SigPlaceholder is a placeholder signature 42 | SigPlaceholder = make([]byte, 65) 43 | ) 44 | 45 | // BuildCompleteBlock builds a complete block 46 | func BuildCompleteBlock(height uint64, nextEpochHeight uint64) (*block.Block, error) { 47 | blk := block.Block{} 48 | 49 | if err := blk.ConvertFromBlockPb(&iotextypes.Block{ 50 | Header: &iotextypes.BlockHeader{ 51 | Core: &iotextypes.BlockHeaderCore{ 52 | Version: version.ProtocolVersion, 53 | Height: height, 54 | Timestamp: ptypes.TimestampNow(), 55 | }, 56 | ProducerPubkey: PubKey1.Bytes(), 57 | }, 58 | Body: &iotextypes.BlockBody{ 59 | Actions: []*iotextypes.Action{ 60 | { 61 | Core: &iotextypes.ActionCore{ 62 | Action: &iotextypes.ActionCore_Transfer{ 63 | Transfer: &iotextypes.Transfer{Recipient: Addr1, Amount: "1"}, 64 | }, 65 | Version: version.ProtocolVersion, 66 | Nonce: 101, 67 | }, 68 | SenderPubKey: PubKey1.Bytes(), 69 | Signature: SigPlaceholder, 70 | }, 71 | { 72 | Core: &iotextypes.ActionCore{ 73 | Action: &iotextypes.ActionCore_Transfer{ 74 | Transfer: &iotextypes.Transfer{Recipient: Addr2, Amount: "2"}, 75 | }, 76 | Version: version.ProtocolVersion, 77 | Nonce: 102, 78 | }, 79 | SenderPubKey: PubKey1.Bytes(), 80 | Signature: SigPlaceholder, 81 | }, 82 | { 83 | Core: &iotextypes.ActionCore{ 84 | Action: &iotextypes.ActionCore_Execution{ 85 | Execution: &iotextypes.Execution{Contract: Addr2}, 86 | }, 87 | Version: version.ProtocolVersion, 88 | Nonce: 103, 89 | }, 90 | SenderPubKey: PubKey1.Bytes(), 91 | Signature: SigPlaceholder, 92 | }, 93 | { 94 | Core: &iotextypes.ActionCore{ 95 | Action: &iotextypes.ActionCore_PutPollResult{ 96 | PutPollResult: &iotextypes.PutPollResult{ 97 | Height: nextEpochHeight, 98 | Candidates: &iotextypes.CandidateList{ 99 | Candidates: []*iotextypes.Candidate{ 100 | { 101 | Address: Addr1, 102 | Votes: big.NewInt(100).Bytes(), 103 | PubKey: PubKey1.Bytes(), 104 | }, 105 | { 106 | Address: Addr2, 107 | Votes: big.NewInt(50).Bytes(), 108 | PubKey: PubKey2.Bytes(), 109 | }, 110 | }, 111 | }, 112 | }, 113 | }, 114 | Version: version.ProtocolVersion, 115 | Nonce: 104, 116 | }, 117 | SenderPubKey: PubKey1.Bytes(), 118 | Signature: SigPlaceholder, 119 | }, 120 | { 121 | Core: &iotextypes.ActionCore{ 122 | Action: &iotextypes.ActionCore_GrantReward{ 123 | GrantReward: &iotextypes.GrantReward{ 124 | Height: height, 125 | Type: iotextypes.RewardType_BlockReward, 126 | }, 127 | }, 128 | Version: version.ProtocolVersion, 129 | Nonce: 105, 130 | }, 131 | SenderPubKey: PubKey1.Bytes(), 132 | Signature: SigPlaceholder, 133 | }, 134 | { 135 | Core: &iotextypes.ActionCore{ 136 | Action: &iotextypes.ActionCore_GrantReward{ 137 | GrantReward: &iotextypes.GrantReward{ 138 | Height: height, 139 | Type: iotextypes.RewardType_EpochReward, 140 | }, 141 | }, 142 | Version: version.ProtocolVersion, 143 | Nonce: 106, 144 | }, 145 | SenderPubKey: PubKey1.Bytes(), 146 | Signature: SigPlaceholder, 147 | }, 148 | { 149 | Core: &iotextypes.ActionCore{ 150 | Action: &iotextypes.ActionCore_Execution{ 151 | Execution: &iotextypes.Execution{}, 152 | }, 153 | Version: version.ProtocolVersion, 154 | Nonce: 107, 155 | }, 156 | SenderPubKey: PubKey1.Bytes(), 157 | Signature: SigPlaceholder, 158 | }, 159 | { 160 | Core: &iotextypes.ActionCore{ 161 | Action: &iotextypes.ActionCore_Execution{ 162 | Execution: &iotextypes.Execution{}, 163 | }, 164 | Version: version.ProtocolVersion, 165 | Nonce: 108, 166 | }, 167 | SenderPubKey: PubKey1.Bytes(), 168 | Signature: SigPlaceholder, 169 | }, 170 | }, 171 | }, 172 | }); err != nil { 173 | return nil, err 174 | } 175 | 176 | receipts := []*action.Receipt{ 177 | { 178 | ActionHash: blk.Actions[0].Hash(), 179 | Status: 1, 180 | GasConsumed: 1, 181 | ContractAddress: "1", 182 | }, 183 | { 184 | ActionHash: blk.Actions[1].Hash(), 185 | Status: 1, 186 | GasConsumed: 2, 187 | ContractAddress: "2", 188 | }, 189 | { 190 | ActionHash: blk.Actions[2].Hash(), 191 | Status: 3, 192 | GasConsumed: 3, 193 | ContractAddress: "3", 194 | }, 195 | { 196 | ActionHash: blk.Actions[3].Hash(), 197 | Status: 4, 198 | GasConsumed: 4, 199 | }, 200 | } 201 | testReceipt := &action.Receipt{ 202 | ActionHash: blk.Actions[4].Hash(), 203 | Status: 5, 204 | GasConsumed: 5, 205 | ContractAddress: "5"} 206 | testReceipt.AddLogs( 207 | createRewardLog(uint64(1), blk.Actions[4].Hash(), rewardingpb.RewardLog_BLOCK_REWARD, RewardAddr1, "16")) 208 | receipts = append(receipts, testReceipt) 209 | testReceipt2 := &action.Receipt{ 210 | ActionHash: blk.Actions[5].Hash(), 211 | Status: 6, 212 | GasConsumed: 6, 213 | ContractAddress: "6", 214 | } 215 | testReceipt2.AddLogs(createRewardLog(height, blk.Actions[5].Hash(), rewardingpb.RewardLog_EPOCH_REWARD, RewardAddr1, "10")) 216 | testReceipt2.AddLogs(createRewardLog(height, blk.Actions[5].Hash(), rewardingpb.RewardLog_EPOCH_REWARD, RewardAddr2, "20")) 217 | testReceipt2.AddLogs(createRewardLog(height, blk.Actions[5].Hash(), rewardingpb.RewardLog_EPOCH_REWARD, RewardAddr3, "30")) 218 | testReceipt2.AddLogs(createRewardLog(height, blk.Actions[5].Hash(), rewardingpb.RewardLog_FOUNDATION_BONUS, RewardAddr1, "100")) 219 | testReceipt2.AddLogs(createRewardLog(height, blk.Actions[5].Hash(), rewardingpb.RewardLog_FOUNDATION_BONUS, RewardAddr2, "100")) 220 | testReceipt2.AddLogs(createRewardLog(height, blk.Actions[5].Hash(), rewardingpb.RewardLog_FOUNDATION_BONUS, RewardAddr3, "100")) 221 | receipts = append(receipts, testReceipt2) 222 | // add for xrc20 223 | transferHash, _ := hex.DecodeString("ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef") 224 | data, _ := hex.DecodeString("0000000000000000000000006356908ace09268130dee2b7de643314bbeb3683000000000000000000000000da7e12ef57c236a06117c5e0d04a228e7181cf360000000000000000000000000000000000000000000000000de0b6b3a7640000") 225 | testReceipt3 := &action.Receipt{ 226 | ActionHash: blk.Actions[6].Hash(), 227 | Status: 7, 228 | GasConsumed: 7, 229 | ContractAddress: "7", 230 | } 231 | testReceipt3.AddLogs(&action.Log{ 232 | Address: "xxxxx", 233 | Topics: []hash.Hash256{hash.BytesToHash256(transferHash)}, 234 | Data: data, 235 | BlockHeight: 100000, 236 | ActionHash: blk.Actions[6].Hash(), 237 | Index: 888, 238 | }) 239 | receipts = append(receipts, testReceipt3) 240 | 241 | // add for xrc721 242 | transferHash, _ = hex.DecodeString("ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ff003f0d751d3a71172f723fbbc4d262dd47adf00000000000000000000000000000000000000000000000000000000000000006") 243 | testReceipt4 := &action.Receipt{ 244 | ActionHash: blk.Actions[7].Hash(), 245 | Status: 8, 246 | GasConsumed: 8, 247 | ContractAddress: "888", 248 | } 249 | testReceipt4.AddLogs(&action.Log{ 250 | Address: "io1xpvzahnl4h46f9ea6u03ec2hkusrzu020th8xx", 251 | Topics: []hash.Hash256{ 252 | hash.BytesToHash256(transferHash[:32]), 253 | hash.BytesToHash256(transferHash[32:64]), 254 | hash.BytesToHash256(transferHash[64:96]), 255 | hash.BytesToHash256(transferHash[96:128]), 256 | }, 257 | BlockHeight: 100001, 258 | ActionHash: blk.Actions[7].Hash(), 259 | Index: 666, 260 | }) 261 | receipts = append(receipts, testReceipt4) 262 | 263 | blk.Receipts = make([]*action.Receipt, 0) 264 | /*for _, receipt := range receipts { 265 | blk.Receipts = append(blk.Receipts, receipt) 266 | }*/ 267 | blk.Receipts = append(blk.Receipts, receipts...) 268 | 269 | return &blk, nil 270 | } 271 | 272 | // BuildEmptyBlock builds an empty block 273 | func BuildEmptyBlock(height uint64) (*block.Block, error) { 274 | blk := block.Block{} 275 | 276 | if err := blk.ConvertFromBlockPb(&iotextypes.Block{ 277 | Header: &iotextypes.BlockHeader{ 278 | Core: &iotextypes.BlockHeaderCore{ 279 | Version: version.ProtocolVersion, 280 | Height: height, 281 | Timestamp: ptypes.TimestampNow(), 282 | }, 283 | ProducerPubkey: PubKey1.Bytes(), 284 | }, 285 | Body: &iotextypes.BlockBody{}, 286 | }); err != nil { 287 | return nil, err 288 | } 289 | return &blk, nil 290 | } 291 | 292 | func createRewardLog( 293 | blkHeight uint64, 294 | actionHash hash.Hash256, 295 | rewardType rewardingpb.RewardLog_RewardType, 296 | rewardAddr string, 297 | amount string, 298 | ) *action.Log { 299 | h := hash.Hash160b([]byte("rewarding")) 300 | addr, _ := address.FromBytes(h[:]) 301 | log := &action.Log{ 302 | Address: addr.String(), 303 | Topics: nil, 304 | BlockHeight: blkHeight, 305 | ActionHash: actionHash, 306 | } 307 | 308 | rewardData := rewardingpb.RewardLog{ 309 | Type: rewardType, 310 | Addr: rewardAddr, 311 | Amount: amount, 312 | } 313 | 314 | data, _ := proto.Marshal(&rewardData) 315 | log.Data = data 316 | return log 317 | } 318 | -------------------------------------------------------------------------------- /testutil/cleanup.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 IoTeX 2 | // This is an alpha (internal) release and is not suitable for production. This source code is provided 'as is' and no 3 | // warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent 4 | // permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache 5 | // License 2.0 that can be found in the LICENSE file. 6 | 7 | package testutil 8 | 9 | import ( 10 | "database/sql" 11 | "fmt" 12 | "testing" 13 | ) 14 | 15 | // CleanupDatabase detects the existence of a MySQL database and drops it if found 16 | func CleanupDatabase(t *testing.T, connectStr string, dbName string) { 17 | db, err := sql.Open("mysql", connectStr) 18 | if err != nil { 19 | t.Error("Failed to open the database") 20 | } 21 | if _, err := db.Exec("DROP DATABASE IF EXISTS " + dbName); err != nil { 22 | fmt.Println(err) 23 | t.Error("Failed to drop the database") 24 | } 25 | if err := db.Close(); err != nil { 26 | t.Error("Failed to close the database") 27 | } 28 | } 29 | --------------------------------------------------------------------------------