├── .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 |
--------------------------------------------------------------------------------