├── .gitignore
├── README.md
├── Transfer.go
├── alert.go
├── analysis.go
├── apiService.go
├── cmd
└── fee-tracer
│ ├── config.go
│ └── main.go
├── config-devnet.yaml.sample
├── config-mainnet-beta.yaml.sample
├── config-testnet.yaml.sample
├── config.go
├── config.yaml.sample
├── database.go
├── docker
└── Dockerfile.default
├── error.go
├── errorRespIdentifier.go
├── go.mod
├── go.sum
├── influxdb.go
├── main.go
├── output.go
├── ping_test.go
├── report-post.go
├── rpcEndpoint.go
├── rpcFailover.go
├── rpcPing.go
├── script
├── README
├── api-monitoring-each-server.sh
├── api-monitoring.sh
├── cp-to-real-config.sh
├── docker-build-fee-tracer.sh
├── solana-ping-api-restart.sh
└── update-from-wks.sh
└── workers.go
/.gitignore:
--------------------------------------------------------------------------------
1 | # tempate from https://github.com/github/gitignore/blob/main/Go.gitignore
2 | # If you prefer the allow list template instead of the deny list, see community template:
3 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
4 | #
5 | # Binaries for programs and plugins
6 | *.exe
7 | *.exe~
8 | *.dll
9 | *.so
10 | *.dylib
11 | solana-ping-api-service*
12 | # Test binary, built with `go test -c`
13 | *.test
14 |
15 | # Output of the go coverage tool, specifically when used with LiteIDE
16 | *.out
17 |
18 | # serialized data and backup
19 | *.serialized
20 | *.bk
21 | # Dependency directories (remove the comment below to include it)
22 | vendor/
23 |
24 | # go.sum , do not commit config.yaml to avoid accidental info leak
25 | config.yaml
26 | config-mainnet-beta.yaml
27 | config-testnet.yaml
28 | config-devnet.yaml
29 | *.env
30 | # Go workspace file
31 | go.work.*
32 |
33 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Solana Ping Service
2 |
3 | ## Functions Provided
4 | - High frequently send transactions and record results
5 | - provide http API service
6 | - generate a report and submit to slack periodically
7 | - actively check confirmation losses and send an alert to slack
8 | - Spam Filter of slack alert
9 |
10 | ## Server Setup
11 | ### API Service
12 | API service for getting the results of ping service.
13 | Use `APIServer: Enabled: true` to turn on in in config-{cluster}.yaml.
14 |
15 | ### PingService
16 | This is similar to "solana ping" tool in solana tool but can do concurrent rpc query.
17 | It send transactions to rpc endpoint and wait for transactions is confirmed.
18 | Use `PingServiceEnabled: true` to turn on in config-{cluster}.yaml.
19 | ### RetensionService
20 | Use `Retension: Enabled: true` in config.yaml to turn on. Default is Off.
21 | Clean database data periodically.
22 |
23 | ### ReportService
24 | Use `Report: Enabled:true` in config-{cluster}.yaml to turn on.
25 | ping-api service supports sedning report & alert to both slack and discord.
26 | Use `Report: Slack: Report: Enabled:true` to turn on Slack Report.
27 | This sends summary of ping result to a slack channel periodically.
28 | Use `Report: Slack: Alert: Enabled:true` to turn on Slack Alert.
29 | This will send alert when a event is triggered. See **Alert Spam Filter** for more info.
30 | Use `Report: Discord: Report: Enabled:true` to turn on Discord Report.
31 | Use `Report: Discord: Alert: Enabled:true` to turn on Discord Alert.
32 |
33 |
34 | + Example:Run only API Query Server
35 | In config.yaml ServerSetup:
36 |
37 | ```
38 | (config.yaml)
39 | Retension:
40 | Enabled: false
41 | (config-{cluster}.yaml)
42 | PingEnabled: true
43 | Report:
44 | Enabled: true
45 | Slack:
46 | Report:
47 | Enabled: true
48 | Alert:
49 | Enabled: true
50 | Discord:
51 | Enabled: true
52 | Report:
53 | Enabled: false
54 | Alert:
55 | Enabled: true
56 | ```
57 | ## Installation
58 | - download executable file
59 | - or build from source
60 | - Install golang
61 | - clone from github.com/solana-labs/solana-ping-api
62 | - go mod tidy to download packages
63 | - go build
64 | - mkdir ~/.config/ping-api
65 | - put config.yaml in ~/.config/ping-api/config.yaml
66 |
67 | ### Using GCP Database
68 | - Install & Setup google cloud CLI
69 | - download [Cloud SQL Auth proxy](https://cloud.google.com/sql/docs/postgres/sql-proxy)
70 | - chmod +x cloud_sql_proxy
71 | - run cloud_sql_proxy
72 |
73 | ## setup recommendation
74 | - mkdir ~/ping-api-server
75 | - cp scripts in script to ~/ping-api-server
76 | - make solana-ping-api system service
77 | - create a /etc/systemd/system/solana-ping-api.service
78 | - remember to reload by ```sudo systemctl daemon-reload```
79 |
80 | ```
81 | [Unit]
82 | Description=Solana Ping API Service
83 | After=network.target
84 | StartLimitIntervalSec=1
85 |
86 | [Service]
87 | Type=simple
88 | Restart=always
89 | RestartSec=30
90 | User=sol
91 | LogRateLimitIntervalSec=0
92 | ExecStart=/home/sol/ping-api-server/solana-ping-restart.sh
93 |
94 | [Install]
95 | WantedBy=multi-user.target
96 |
97 | ```
98 |
99 | - put executable file in ~/ping-api-server
100 | - cp config.yaml.samle to ~/ping-api-server/config.yaml and modify it
101 | - use cp-to-real-config.sh to copy config.yaml to ~/.config/ping-api/config.yaml
102 | - start service by sudo sysmtemctl start solana-ping-api.service
103 | - you can check log by ```sudo tail -f /var/log/syslog | grep ping-api```
104 |
105 | ## Alert Spam Filter
106 |
107 | Alert Spam Filter could be changed frequently. The updte to date (4/18/2022) setting is as below.
108 | ```
109 | Threshold increases when
110 | Loss > 20 % -> new threshold = 50% -> send alert
111 | Loss > 50 % -> new threshold = 75% -> send alert
112 | Loss > 75 % -> new threshold = 100% -> send alert
113 | Threshold decreases when
114 | Loss > 75 % to < 75% -> new threshold = 75% -> send alert
115 | Loss > 50 % to < 50% -> new threshold = 50% -> send alert
116 | Loss > 20 % to < 20% -> new threshold = 20% -> NOT send alert
117 | ```
--------------------------------------------------------------------------------
/Transfer.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "log"
7 | "math/rand"
8 | "time"
9 |
10 | "github.com/blocto/solana-go-sdk/client"
11 | "github.com/blocto/solana-go-sdk/common"
12 | "github.com/blocto/solana-go-sdk/program/cmptbdgprog"
13 | "github.com/blocto/solana-go-sdk/program/sysprog"
14 | "github.com/blocto/solana-go-sdk/rpc"
15 | "github.com/blocto/solana-go-sdk/types"
16 | )
17 |
18 | var (
19 | txTimeoutDefault = 10 * time.Second
20 | waitConfirmationTimeoutDefault = 50 * time.Second
21 | statusCheckTimeDefault = 1 * time.Second
22 | )
23 |
24 | func Transfer(c *client.Client, sender types.Account, feePayer types.Account, receiverPubkey string, txTimeout time.Duration) (txHash string, pingErr PingResultError) {
25 | // to fetch recent blockhash
26 | res, err := c.GetLatestBlockhash(context.Background())
27 | if err != nil {
28 | log.Println("Failed to get latest blockhash, err: ", err)
29 | return "", PingResultError(fmt.Sprintf("Failed to get latest blockhash, err: %v", err))
30 | }
31 | // create a message
32 | message := types.NewMessage(types.NewMessageParam{
33 | FeePayer: feePayer.PublicKey,
34 | RecentBlockhash: res.Blockhash, // recent blockhash
35 | Instructions: []types.Instruction{
36 | sysprog.Transfer(sysprog.TransferParam{
37 | From: sender.PublicKey, // from
38 | To: common.PublicKeyFromString(receiverPubkey), // to
39 | Amount: 1, // SOL
40 | }),
41 | },
42 | })
43 |
44 | // create tx by message + signer
45 | tx, err := types.NewTransaction(types.NewTransactionParam{
46 | Message: message,
47 | Signers: []types.Account{feePayer, sender},
48 | })
49 | if err != nil {
50 | log.Printf("Error: Failed to create a new transaction, err: %v", err)
51 | return "", PingResultError(fmt.Sprintf("Failed to new a tx, err: %v", err))
52 | }
53 | // send tx
54 | if txTimeout <= 0 {
55 | txTimeout = time.Duration(txTimeoutDefault)
56 | }
57 | ctx, _ := context.WithTimeout(context.TODO(), txTimeout)
58 | txHash, err = c.SendTransaction(ctx, tx)
59 |
60 | if err != nil {
61 | log.Printf("Error: Failed to send tx, err: %v", err)
62 | return "", PingResultError(fmt.Sprintf("Failed to send a tx, err: %v", err))
63 | }
64 | return txHash, EmptyPingResultError
65 | }
66 |
67 | type SendPingTxParam struct {
68 | Client *client.Client
69 | FeePayer types.Account
70 | RequestComputeUnits uint32
71 | ComputeUnitPrice uint64 // micro lamports
72 | ReceiverPubkey string
73 | }
74 |
75 | func SendPingTx(param SendPingTxParam) (string, string, PingResultError) {
76 | // There can be intermittent failures querying for blockhash, so retry a few
77 | // times if necessary.
78 | retry := 5
79 | errRecords := make([]string, 0, retry)
80 |
81 | for retry > 0 {
82 | retry -= 1
83 | time.Sleep(10 * time.Millisecond)
84 |
85 | // Get a recent blockhash.
86 | latestBlockhashResponse, err := param.Client.GetLatestBlockhashWithConfig(
87 | context.Background(),
88 | client.GetLatestBlockhashConfig{
89 | Commitment: rpc.CommitmentConfirmed,
90 | },
91 | )
92 | if err != nil {
93 | errRecords = append(errRecords, fmt.Sprintf("failed to get the latest blockhash, err: %v", err))
94 | continue
95 | }
96 | blockhash := latestBlockhashResponse.Blockhash
97 |
98 | // Generate a random amount for trasferring. This entropy is needed to
99 | // ensure we don't send duplicates in cases where the blockhash hasn't
100 | // moved between pings.
101 | rand.Seed(time.Now().UnixNano())
102 | amount := uint64(rand.Intn(1000)) + 1
103 |
104 | // Construct the tx.
105 | tx, err := types.NewTransaction(types.NewTransactionParam{
106 | Signers: []types.Account{param.FeePayer},
107 | Message: types.NewMessage(types.NewMessageParam{
108 | FeePayer: param.FeePayer.PublicKey,
109 | RecentBlockhash: blockhash,
110 | Instructions: []types.Instruction{
111 | cmptbdgprog.SetComputeUnitLimit(cmptbdgprog.SetComputeUnitLimitParam{
112 | Units: param.RequestComputeUnits,
113 | }),
114 | cmptbdgprog.SetComputeUnitPrice(cmptbdgprog.SetComputeUnitPriceParam{
115 | MicroLamports: param.ComputeUnitPrice,
116 | }),
117 | sysprog.Transfer(sysprog.TransferParam{
118 | From: param.FeePayer.PublicKey,
119 | To: common.PublicKeyFromString(param.ReceiverPubkey),
120 | Amount: amount,
121 | }),
122 | },
123 | }),
124 | })
125 | if err != nil {
126 | errRecords = append(errRecords, fmt.Sprintf("failed to create a ping tx, err: %v", err))
127 | continue
128 | }
129 |
130 | // Send the tx.
131 | txhash, err := param.Client.SendTransactionWithConfig(
132 | context.Background(),
133 | tx,
134 | client.SendTransactionConfig{
135 | SkipPreflight: true,
136 | },
137 | )
138 | if err != nil {
139 | errRecords = append(errRecords, fmt.Sprintf("failed to send the ping tx, err: %v", err))
140 | continue
141 | }
142 | return txhash, blockhash, PingResultError("")
143 | }
144 |
145 | return "", "", PingResultError(fmt.Sprintf("failed to send a ping tx, errs: %v", errRecords))
146 | }
147 |
148 | /*
149 | timeout: timeout for checking a block with the assigned htxHash status
150 | requestTimeout: timeout for GetSignatureStatus
151 | checkInterval: interval to check for status
152 |
153 | */
154 |
155 | func waitConfirmation(c *client.Client, txHash string, timeout time.Duration, requestTimeout time.Duration, checkInterval time.Duration) PingResultError {
156 | if timeout <= 0 {
157 | timeout = waitConfirmationTimeoutDefault
158 | log.Println("timeout is not set! Use default timeout", timeout, " sec")
159 | }
160 |
161 | ctx, _ := context.WithTimeout(context.TODO(), requestTimeout)
162 | elapse := time.Now()
163 | for {
164 | resp, err := c.GetSignatureStatus(ctx, txHash)
165 | now := time.Now()
166 | if err != nil {
167 | if now.Sub(elapse).Seconds() < timeout.Seconds() {
168 | continue
169 | } else {
170 | return PingResultError(fmt.Sprintf("failed to get signatureStatus, err: %v", err))
171 | }
172 | }
173 | if resp != nil {
174 | if *resp.ConfirmationStatus == rpc.CommitmentConfirmed || *resp.ConfirmationStatus == rpc.CommitmentFinalized {
175 | return EmptyPingResultError
176 | }
177 | }
178 | if now.Sub(elapse).Seconds() > timeout.Seconds() {
179 | if resp != nil && *resp.ConfirmationStatus == rpc.CommitmentProcessed {
180 | return PingResultError(ErrInProcessedStateTimeout.Error())
181 | }
182 | return PingResultError(ErrWaitForConfirmedTimeout.Error())
183 | }
184 |
185 | if checkInterval <= 0 {
186 | checkInterval = statusCheckTimeDefault
187 | }
188 | time.Sleep(checkInterval)
189 | }
190 | }
191 |
192 | func waitConfirmationOrBlockhashInvalid(c *client.Client, txHash, blockhash string) PingResultError {
193 | startTime := time.Now()
194 | endTime := startTime.Add(3 * time.Minute)
195 |
196 | for time.Now().Before(endTime) {
197 | time.Sleep(1 * time.Second)
198 |
199 | // Check if blockhash has expired.
200 | isBlockhashValid, err := isBlockhashValid(c, context.Background(), blockhash)
201 | if err != nil {
202 | continue
203 | }
204 | if !isBlockhashValid {
205 | // Ping expired!
206 | return PingResultError(fmt.Sprintf("blockhash is not valid, txHash: %v, blockhash: %v, err: %v", txHash, blockhash, err))
207 | }
208 |
209 | // Check tx signature status.
210 | getSignatureStatus, err := c.GetSignatureStatus(context.Background(), txHash)
211 | if err != nil {
212 | continue
213 | }
214 | if getSignatureStatus == nil {
215 | continue
216 | }
217 | commitment := *getSignatureStatus.ConfirmationStatus
218 | if commitment == rpc.CommitmentConfirmed || commitment == rpc.CommitmentFinalized {
219 | // Ping has landed!
220 | return EmptyPingResultError
221 | }
222 | }
223 |
224 | // Ping timed out!
225 | return PingResultError(fmt.Sprintf("the confirmation process exceeds 3 mins, txHash: %v, blockhash: %v", txHash, blockhash))
226 | }
227 |
228 | func isBlockhashValid(c *client.Client, ctx context.Context, blockhash string) (bool, error) {
229 | // check for confirmed commitment
230 | b1, err := c.IsBlockhashValidWithConfig(
231 | ctx,
232 | blockhash,
233 | client.IsBlockhashValidConfig{
234 | Commitment: rpc.CommitmentConfirmed,
235 | },
236 | )
237 | if err != nil {
238 | return false, err
239 | }
240 |
241 | // check for finalized commitment
242 | b2, err := c.IsBlockhashValidWithConfig(
243 | ctx,
244 | blockhash,
245 | client.IsBlockhashValidConfig{
246 | Commitment: rpc.CommitmentFinalized,
247 | },
248 | )
249 | if err != nil {
250 | return false, err
251 | }
252 |
253 | return b1 || b2, nil
254 | }
255 |
--------------------------------------------------------------------------------
/alert.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "log"
5 | "os"
6 | "strconv"
7 | "strings"
8 | )
9 |
10 | const AlertTriggerNameLength = 30
11 |
12 | type AlertTrigger struct {
13 | Name string
14 | LastLoss float64
15 | CurrentLoss float64
16 | ThresholdIndex int
17 | ThresholdLevels []float64
18 | ThresholdAsc bool
19 | FilePath string
20 | }
21 |
22 | func NewAlertTrigger(conf ClusterConfig) AlertTrigger {
23 | s := AlertTrigger{}
24 | s.Name = "default"
25 | s.FilePath = conf.Report.LevelFilePath
26 | s.CurrentLoss = 0
27 | s.LastLoss = 0
28 | s.ThresholdLevels = []float64{float64(conf.Report.LossThreshold), float64(50), float64(75), float64(100)}
29 | s.ThresholdIndex = s.ReadIndexFromFile()
30 | return s
31 | }
32 | func NewAlertTriggerByParams(alertName string, levelFilePath string, lossThreshold float64) AlertTrigger {
33 | s := AlertTrigger{}
34 | alertName = strings.Trim(alertName, " ")
35 | if len(alertName) > AlertTriggerNameLength {
36 | log.Panic("alertName length > ", AlertTriggerNameLength)
37 | }
38 | if len(alertName) == 0 {
39 | s.Name = "default"
40 | } else {
41 | s.Name = alertName
42 | }
43 |
44 | s.FilePath = levelFilePath
45 | s.CurrentLoss = 0
46 | s.LastLoss = 0
47 | s.ThresholdLevels = []float64{lossThreshold, float64(50), float64(75), float64(100)}
48 | s.ThresholdIndex = s.ReadIndexFromFile()
49 | return s
50 | }
51 | func (s *AlertTrigger) Update(currentLoss float64) {
52 | s.LastLoss = s.CurrentLoss
53 | s.CurrentLoss = currentLoss * 100
54 | }
55 |
56 | // get a threshold index which is 1 level higher than loss value
57 | func (s *AlertTrigger) UpThresholdIndex(loss float64) int {
58 | if loss < s.ThresholdLevels[0] {
59 | return 0
60 | }
61 | if loss >= s.ThresholdLevels[len(s.ThresholdLevels)-1] {
62 | return len(s.ThresholdLevels) - 1
63 | }
64 | // > level 0
65 | for i, v := range s.ThresholdLevels {
66 | if loss < v {
67 | return i
68 | }
69 | }
70 | return 0
71 | }
72 |
73 | func (s *AlertTrigger) WritIndexToFile(level int) {
74 | if s.FilePath != "" {
75 | os.WriteFile(s.FilePath, []byte(strconv.Itoa(level)), 0777)
76 | log.Println(s.Name, " trigger ", "WriteLevelToFile : ", strconv.Itoa(level), " to ", s.FilePath)
77 | }
78 | }
79 |
80 | func (s *AlertTrigger) ReadIndexFromFile() int {
81 | if s.FilePath != "" {
82 | v, err := os.ReadFile(s.FilePath)
83 | if err != nil {
84 | return 0
85 | }
86 | level, err := strconv.Atoi(string(v))
87 | if err != nil {
88 | return 0
89 | }
90 | log.Println(s.Name, " trigger ", "ReadFromFile : ", level, " from ", s.FilePath)
91 | return level
92 | }
93 | return 0
94 | }
95 |
96 | // Doing rule here
97 | func (s *AlertTrigger) ShouldAlertSend() bool {
98 | if s.ThresholdLevels[0] == 0 {
99 | return true
100 | }
101 | if s.CurrentLoss < s.ThresholdLevels[0] {
102 | s.ThresholdIndex = 0
103 | s.WritIndexToFile(0)
104 | log.Println(s.Name, " trigger ", "Loss = ", s.CurrentLoss, " < ", s.ThresholdLevels[0], "Index:", s.ThresholdIndex)
105 | return false
106 | }
107 | // adjust threshold up, include index = 0
108 | if s.CurrentLoss > s.ThresholdLevels[s.ThresholdIndex] {
109 | s.ThresholdIndex = s.UpThresholdIndex(s.CurrentLoss)
110 | s.ThresholdAsc = true
111 | s.WritIndexToFile(s.ThresholdIndex)
112 | log.Println(s.Name, " trigger ", "ThresholdLevel Up To :", s.ThresholdIndex, " Loss:", s.CurrentLoss, " ShouldSend", true)
113 | return true
114 | }
115 | // adjust threshold down, index = 0 does not need to down level
116 | if s.CurrentLoss < s.ThresholdLevels[s.ThresholdIndex-1] {
117 | s.ThresholdIndex = s.UpThresholdIndex(s.CurrentLoss)
118 | s.ThresholdAsc = false
119 | s.WritIndexToFile(s.ThresholdIndex)
120 | log.Println(s.Name, " trigger ", "ThresholdLevel Down To :", s.ThresholdIndex, " Loss:", s.CurrentLoss, " ShouldSend", true)
121 | return true
122 | }
123 | log.Println(s.Name, " trigger ", "ThresholdLevel NOT change. Loss:", s.CurrentLoss, "Index:", s.ThresholdIndex)
124 | return false
125 | }
126 |
--------------------------------------------------------------------------------
/analysis.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "time"
7 | )
8 |
9 | // collect ping result and divided them into a group by each min
10 | type Group1Min struct {
11 | Result []PingResult
12 | TimeStamp int64
13 | }
14 |
15 | // statistic of ping result take-time
16 | type TimeStatistic struct {
17 | Min int64
18 | Mean float64
19 | Max int64
20 | Stddev float64
21 | Sum int64
22 | }
23 |
24 | // statistic of ping result
25 | type PingSatistic struct {
26 | Submitted float64
27 | Confirmed float64
28 | Loss float64
29 | Count int
30 | TimeMeasure TakeTime
31 | TimeStatistic
32 | Errors []string
33 | TimeStamp int64
34 | }
35 |
36 | // statistic without a group
37 | type GlobalStatistic struct {
38 | Submitted float64
39 | Confirmed float64
40 | Loss float64
41 | Count int
42 | TimeStatistic
43 | }
44 |
45 | // All statistic Data
46 | type GroupsAllStatistic struct {
47 | PingStatisticList []PingSatistic
48 | RawPingStaticList []PingSatistic
49 | GlobalErrorStatistic map[string]int
50 | GlobalStatistic
51 | }
52 |
53 | func (g *GroupsAllStatistic) GetGroupsAllStatistic(raw bool) GlobalStatistic {
54 | groupStat := GlobalStatistic{}
55 | var sumOfSubmitted, sumOfConfirmed float64
56 | var sumOfCount int
57 | var sumTimeMeasure TakeTime
58 | if !raw {
59 | for _, pg := range g.PingStatisticList {
60 | sumOfSubmitted += pg.Submitted
61 | sumOfConfirmed += pg.Confirmed
62 | sumOfCount += pg.Count
63 | sumTimeMeasure.Times = append(sumTimeMeasure.Times, pg.TimeMeasure.Times...)
64 | }
65 |
66 | } else { // raw data (without exception)
67 | for _, pg := range g.RawPingStaticList {
68 | sumOfSubmitted += pg.Submitted
69 | sumOfConfirmed += pg.Confirmed
70 | sumOfCount += pg.Count
71 | sumTimeMeasure.Times = append(sumTimeMeasure.Times, pg.TimeMeasure.Times...)
72 |
73 | }
74 | }
75 |
76 | groupStat.Submitted = sumOfSubmitted
77 | groupStat.Confirmed = sumOfConfirmed
78 | groupStat.Loss = 1
79 | if groupStat.Submitted > 0 {
80 | groupStat.Loss = (groupStat.Submitted - groupStat.Confirmed) / groupStat.Submitted
81 | } else if len(g.GlobalErrorStatistic) == 0 { // No Data
82 | groupStat.Loss = 0
83 | }
84 |
85 | tMax, tMean, tMin, tStddev, tSum := sumTimeMeasure.Statistic()
86 | groupStat.TimeStatistic = TimeStatistic{
87 | Min: tMin,
88 | Mean: tMean,
89 | Max: tMax,
90 | Stddev: tStddev,
91 | Sum: tSum,
92 | }
93 | return groupStat
94 | }
95 |
96 | // grouping1Min: group []PingResult into 1 min group.
97 | func grouping1Min(pr []PingResult, startTime int64, endTime int64) []Group1Min {
98 | window := int64(60)
99 | groups := []Group1Min{}
100 | for periodend := endTime; periodend > startTime; periodend = periodend - window {
101 | minGroup := Group1Min{}
102 | for _, pResult := range pr {
103 | if pResult.TimeStamp <= periodend && pResult.TimeStamp > periodend-window {
104 | minGroup.Result = append(minGroup.Result, pResult)
105 | }
106 | }
107 | minGroup.TimeStamp = periodend
108 | // debug: printPingResultGroup(minGroup.Result, periodend, periodend-window)
109 | groups = append(groups, minGroup)
110 | }
111 | return groups
112 | }
113 |
114 | func statisticCompute(cConf ClusterConfig, groups []Group1Min) *GroupsAllStatistic {
115 | stat := GroupsAllStatistic{}
116 | stat.PingStatisticList = []PingSatistic{}
117 | stat.RawPingStaticList = []PingSatistic{}
118 | stat.GlobalErrorStatistic = make(map[string]int)
119 |
120 | for _, group := range groups {
121 | filterGroupStat := PingSatistic{}
122 | rawGroupStat := PingSatistic{}
123 | for _, singlePing := range group.Result {
124 | errorException := false
125 | errorCount := len(singlePing.Error)
126 | if errorCount > 0 {
127 | for _, e := range singlePing.Error {
128 | stat.GlobalErrorStatistic[string(e)] = stat.GlobalErrorStatistic[string(e)] + 1
129 | if PingResultError(e).IsInErrorList(StatisticErrorExceptionList) {
130 | errorException = true
131 | } else {
132 | filterGroupStat.Errors = append(filterGroupStat.Errors, string(e))
133 | }
134 | rawGroupStat.Errors = append(rawGroupStat.Errors, string(e))
135 | }
136 | }
137 | // Raw Data Statistic
138 | rawGroupStat.TimeStamp = group.TimeStamp
139 | rawGroupStat.Submitted += float64(singlePing.Submitted)
140 | rawGroupStat.Confirmed += float64(singlePing.Confirmed)
141 | rawGroupStat.Count += 1
142 | rawGroupStat.TimeMeasure.AddTime(singlePing.TakeTime)
143 | // Data Statistic (Filtered by error filter)
144 | filterGroupStat.TimeStamp = group.TimeStamp // Need a ts to present the group
145 | if !errorException {
146 | filterGroupStat.Submitted += float64(singlePing.Submitted)
147 | filterGroupStat.Confirmed += float64(singlePing.Confirmed)
148 | filterGroupStat.Count += 1
149 | if errorCount <= 0 {
150 | filterGroupStat.TimeMeasure.AddTime(singlePing.TakeTime)
151 | } else if (errorCount > 0) && !errorException { // general error is considered as a timeout
152 | t := time.Duration(cConf.PingConfig.TxTimeout) * time.Second
153 | filterGroupStat.TimeMeasure.AddTime(t.Milliseconds())
154 | } // if StatisticErrorExceptionList , do not count as a satistic
155 | }
156 | }
157 | // raw data
158 | if rawGroupStat.Submitted == 0 { // no data
159 | rawGroupStat.Loss = 1
160 | } else {
161 | rawGroupStat.Loss = (rawGroupStat.Submitted - rawGroupStat.Confirmed) / rawGroupStat.Submitted
162 | }
163 | tMax, tMean, tMin, tStddev, tSum := rawGroupStat.TimeMeasure.Statistic()
164 | rawGroupStat.TimeStatistic = TimeStatistic{
165 | Min: tMin,
166 | Mean: tMean,
167 | Max: tMax,
168 | Stddev: tStddev,
169 | Sum: tSum,
170 | }
171 | stat.RawPingStaticList = append(stat.RawPingStaticList, rawGroupStat)
172 |
173 | // data with filter
174 | if filterGroupStat.Submitted == 0 { // no data
175 | filterGroupStat.Loss = 1
176 | } else {
177 | filterGroupStat.Loss = (filterGroupStat.Submitted - filterGroupStat.Confirmed) / filterGroupStat.Submitted
178 | }
179 |
180 | tMax, tMean, tMin, tStddev, tSum = filterGroupStat.TimeMeasure.Statistic()
181 | filterGroupStat.TimeStatistic = TimeStatistic{
182 | Min: tMin,
183 | Mean: tMean,
184 | Max: tMax,
185 | Stddev: tStddev,
186 | Sum: tSum,
187 | }
188 | stat.PingStatisticList = append(stat.PingStatisticList, filterGroupStat)
189 | }
190 | return &stat
191 | }
192 |
193 | func printPingResultGroup(pr []PingResult, from int64, to int64) {
194 | for i, v := range pr {
195 | fmt.Println("group ", i, "", from, "~", to, ":", v.TimeStamp)
196 | }
197 | }
198 |
199 | func printStatistic(cConf ClusterConfig, stat *GroupsAllStatistic) {
200 | for i, g := range stat.PingStatisticList {
201 | statisticTime := fmt.Sprintf("min/mean/max/stddev ms = %d/%3.0f/%d/%3.0f",
202 | g.TimeStatistic.Min, g.TimeStatistic.Mean, g.TimeStatistic.Max, g.TimeStatistic.Stddev)
203 |
204 | log.Println(fmt.Sprintf("%d->{ hostname: %s, submitted: %3.0f,confirmed:%3.0f, loss: %3.1f%s, count:%d %s}",
205 | i, cConf.HostName, g.Submitted, g.Confirmed, g.Loss*100, "%", g.Count, statisticTime))
206 | }
207 | for i, g := range stat.RawPingStaticList {
208 | statisticTime := fmt.Sprintf("min/mean/max/stddev ms = %d/%3.0f/%d/%3.0f",
209 | g.TimeStatistic.Min, g.TimeStatistic.Mean, g.TimeStatistic.Max, g.TimeStatistic.Stddev)
210 | errString := ""
211 | for _, v := range g.Errors {
212 | errString = errString + v + "\n"
213 | }
214 | log.Println(fmt.Sprintf("RAW %d->{ hostname: %s, submitted: %3.0f,confirmed:%3.0f, loss: %3.3f%s, count:%d %s}",
215 | i, cConf.HostName, g.Submitted, g.Confirmed, g.Loss, "%", g.Count, statisticTime))
216 | }
217 | }
218 |
--------------------------------------------------------------------------------
/apiService.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "log"
5 | "net/http"
6 | "time"
7 |
8 | "github.com/gin-contrib/timeout"
9 | "github.com/gin-gonic/gin"
10 | )
11 |
12 | func APIService(c ClustersToRun) {
13 | runCluster := func(mode ConnectionMode, host string, hostSSL string, key string, crt string) {
14 | router := gin.Default()
15 | router.GET("/:cluster/latest", getLatest)
16 | router.GET("/:cluster/last6hours", timeout.New(timeout.WithTimeout(10*time.Second), timeout.WithHandler(last6hours)))
17 | router.GET("/:cluster/last6hours/nocomputeprice", timeout.New(timeout.WithTimeout(10*time.Second), timeout.WithHandler(last6hoursNoPrice)))
18 | router.GET("/:cluster/last6hours/all", timeout.New(timeout.WithTimeout(10*time.Second), timeout.WithHandler(last6hoursAll)))
19 | router.GET("/health", health)
20 | router.GET("/:cluster/rpc", getRPCEndpoint)
21 | if mode == HTTPS {
22 | err := router.RunTLS(hostSSL, crt, key)
23 | if err != nil {
24 | log.Panic("api service is not up!!!", err)
25 | panic("api service is not up!!!")
26 | }
27 | log.Println("HTTPS server is up!", " Server:", host)
28 | } else if mode == HTTP {
29 | err := router.Run(host)
30 | if err != nil {
31 | log.Panic("api service is not up!!!", err)
32 | panic("api service is not up!!!")
33 | }
34 | log.Println("HTTP server is up!", " Server", host)
35 | } else if config.Mainnet.APIServer.Mode == BOTH {
36 | err := router.RunTLS(host, crt, key)
37 | if err != nil {
38 | log.Panic("api service is not up!!!", err)
39 | panic("api service is not up!!!")
40 | }
41 | log.Println("HTTPS server is up!", " Server:", host)
42 | err = router.Run(host)
43 | log.Println("HTTP server is up!", " Server", host)
44 | if err != nil {
45 | log.Panic("api service is not up!!!", err)
46 | panic("api service is not up!!!")
47 | }
48 | } else {
49 | log.Panic("Invalid ServerSetup Mode")
50 | }
51 | }
52 | // Single Cluster or all Cluster
53 | switch c {
54 | case RunMainnetBeta:
55 | if config.Mainnet.APIServer.Enabled {
56 | go runCluster(config.Mainnet.APIServer.Mode,
57 | config.Mainnet.APIServer.IP,
58 | config.Mainnet.APIServer.SSLIP,
59 | config.Mainnet.APIServer.KeyPath,
60 | config.Mainnet.APIServer.CrtPath)
61 | log.Println("--- API Server Mainnet Start--- ")
62 | }
63 |
64 | case RunTestnet:
65 | if config.Testnet.APIServer.Enabled {
66 | go runCluster(config.Testnet.APIServer.Mode,
67 | config.Testnet.APIServer.IP,
68 | config.Testnet.APIServer.SSLIP,
69 | config.Testnet.APIServer.KeyPath,
70 | config.Testnet.APIServer.CrtPath)
71 | log.Println("--- API Server Testnet Start--- ")
72 | }
73 |
74 | case RunDevnet:
75 | if config.Devnet.APIServer.Enabled {
76 | go runCluster(config.Devnet.APIServer.Mode,
77 | config.Devnet.APIServer.IP,
78 | config.Devnet.APIServer.SSLIP,
79 | config.Devnet.APIServer.KeyPath,
80 | config.Devnet.APIServer.CrtPath)
81 | log.Println("--- API Server Devnet Start--- ")
82 | }
83 | case RunAllClusters:
84 | if config.Mainnet.APIServer.Enabled {
85 | go runCluster(config.Mainnet.APIServer.Mode,
86 | config.Mainnet.APIServer.IP,
87 | config.Mainnet.APIServer.SSLIP,
88 | config.Mainnet.APIServer.KeyPath,
89 | config.Mainnet.APIServer.CrtPath)
90 | log.Println("--- API Server Mainnet Start--- ")
91 | }
92 | if config.Testnet.APIServer.Enabled {
93 | go runCluster(config.Testnet.APIServer.Mode,
94 | config.Testnet.APIServer.IP,
95 | config.Testnet.APIServer.SSLIP,
96 | config.Testnet.APIServer.KeyPath,
97 | config.Testnet.APIServer.CrtPath)
98 | log.Println("--- API Server Testnet Start--- ")
99 | }
100 | if config.Devnet.APIServer.Enabled {
101 | go runCluster(config.Devnet.APIServer.Mode,
102 | config.Devnet.APIServer.IP,
103 | config.Devnet.APIServer.SSLIP,
104 | config.Devnet.APIServer.KeyPath,
105 | config.Devnet.APIServer.CrtPath)
106 | log.Println("--- API Server Devnet Start--- ")
107 | }
108 |
109 | default:
110 | panic(ErrInvalidCluster)
111 | }
112 | }
113 |
114 | func health(c *gin.Context) {
115 | c.Data(200, c.ContentType(), []byte("OK"))
116 | }
117 |
118 | func getRPCEndpoint(c *gin.Context) {
119 | cluster := c.Param("cluster")
120 | var e *FailoverEndpoint
121 | switch cluster {
122 | case "mainnet-beta":
123 | e = mainnetFailover.GetEndpoint()
124 | case "testnet":
125 | e = testnetFailover.GetEndpoint()
126 | case "devnet":
127 | e = devnetFailover.GetEndpoint()
128 | default:
129 | c.AbortWithStatus(http.StatusNotFound)
130 | log.Println("StatusNotFound Error:", cluster)
131 | return
132 | }
133 | // to avoid leak of token
134 | c.IndentedJSON(http.StatusOK, FailoverEndpoint{
135 | Endpoint: e.Endpoint,
136 | MaxRetry: e.MaxRetry,
137 | Piority: e.Piority,
138 | Retry: e.Retry})
139 | }
140 |
141 | func getLatest(c *gin.Context) {
142 | cluster := c.Param("cluster")
143 | var ret DataPoint1MinResultJSON
144 | switch cluster {
145 | case "mainnet-beta":
146 | ret = GetLatestResult(MainnetBeta)
147 | case "testnet":
148 | ret = GetLatestResult(Testnet)
149 | case "devnet":
150 | ret = GetLatestResult(Devnet)
151 | default:
152 | c.AbortWithStatus(http.StatusNotFound)
153 | log.Println("StatusNotFound Error:", cluster)
154 | return
155 | }
156 | c.IndentedJSON(http.StatusOK, ret)
157 | }
158 | func last6hours(c *gin.Context) {
159 | cluster := c.Param("cluster")
160 | var ret []DataPoint1MinResultJSON
161 | switch cluster {
162 | case "mainnet-beta":
163 | ret = GetLast6hours(MainnetBeta, HasComputeUnitPrice, 0)
164 | case "testnet":
165 | ret = GetLast6hours(Testnet, HasComputeUnitPrice, 0)
166 | case "devnet":
167 | ret = GetLast6hours(Devnet, HasComputeUnitPrice, 0)
168 | default:
169 | c.AbortWithStatus(http.StatusNotFound)
170 | return
171 | }
172 | c.IndentedJSON(http.StatusOK, ret)
173 | }
174 |
175 | func last6hoursNoPrice(c *gin.Context) {
176 | cluster := c.Param("cluster")
177 | var ret []DataPoint1MinResultJSON
178 | switch cluster {
179 | case "mainnet-beta":
180 | ret = GetLast6hours(MainnetBeta, NoComputeUnitPrice, 0)
181 | case "testnet":
182 | ret = GetLast6hours(Testnet, NoComputeUnitPrice, 0)
183 | case "devnet":
184 | ret = GetLast6hours(Devnet, NoComputeUnitPrice, 0)
185 | default:
186 | c.AbortWithStatus(http.StatusNotFound)
187 | return
188 | }
189 | c.IndentedJSON(http.StatusOK, ret)
190 | }
191 |
192 | func last6hoursAll(c *gin.Context) {
193 | cluster := c.Param("cluster")
194 | var ret []DataPoint1MinResultJSON
195 | switch cluster {
196 | case "mainnet-beta":
197 | ret = GetLast6hours(MainnetBeta, AllData, 0)
198 | case "testnet":
199 | ret = GetLast6hours(Testnet, AllData, 0)
200 | case "devnet":
201 | ret = GetLast6hours(Devnet, AllData, 0)
202 | default:
203 | c.AbortWithStatus(http.StatusNotFound)
204 | return
205 | }
206 | c.IndentedJSON(http.StatusOK, ret)
207 | }
208 |
209 | // GetLatestResult return the latest DataPoint1Min PingResult from the cluster and convert it into PingResultJSON
210 | func GetLatestResult(c Cluster) DataPoint1MinResultJSON {
211 | records := getLastN(c, DataPoint1Min, 1, HasComputeUnitPrice, 0)
212 | if len(records) > 0 {
213 | return To1MinWindowJson(&records[0])
214 | }
215 |
216 | return DataPoint1MinResultJSON{}
217 | }
218 |
219 | // GetLast6hours return the latest 6hr DataPoint1Min PingResult from the cluster and convert it into PingResultJSON
220 | func GetLast6hours(c Cluster, priceType ComputeUnitPriceType, threshold uint64) []DataPoint1MinResultJSON {
221 | lastRecord := getLastN(c, DataPoint1Min, 1, priceType, 0)
222 | now := time.Now().UTC().Unix()
223 | if len(lastRecord) > 0 {
224 | if (now - lastRecord[0].TimeStamp) < 60 { // in past one min, there is a record, use it
225 | now = lastRecord[0].TimeStamp
226 | }
227 | }
228 |
229 | // (-1) because getAfter function return only after .
230 | beginOfPast60Hours := now - 6*60*60
231 | var records []PingResult
232 | switch priceType {
233 | case NoComputeUnitPrice:
234 | records = getAfter(c, DataPoint1Min, beginOfPast60Hours, NoComputeUnitPrice, 0)
235 | case HasComputeUnitPrice:
236 | records = getAfter(c, DataPoint1Min, beginOfPast60Hours, HasComputeUnitPrice, 0)
237 | case ComputeUnitPriceThreshold:
238 | records = getAfter(c, DataPoint1Min, beginOfPast60Hours, ComputeUnitPriceThreshold, threshold)
239 | case AllData:
240 | fallthrough
241 | default:
242 | records = getAfter(c, DataPoint1Min, beginOfPast60Hours, AllData, 0)
243 | }
244 |
245 | if len(records) == 0 {
246 | return []DataPoint1MinResultJSON{}
247 | }
248 | groups := grouping1Min(records, beginOfPast60Hours, now)
249 | if len(groups) != 6*60 {
250 | log.Println("WARN! groups is not 360!", " beginOfPast60Hours:", beginOfPast60Hours, "now")
251 | }
252 |
253 | groupsStat := statisticCompute(GetClusterConfig(c), groups)
254 | ret := []DataPoint1MinResultJSON{}
255 | for _, g := range groupsStat.PingStatisticList {
256 | ret = append(ret, PingResultToJson(&g))
257 | }
258 | return ret
259 | }
260 |
261 | func GetClusterConfig(c Cluster) ClusterConfig {
262 | switch c {
263 | case MainnetBeta:
264 | return config.Mainnet
265 | case Testnet:
266 | return config.Testnet
267 | case Devnet:
268 | return config.Devnet
269 | }
270 | return ClusterConfig{}
271 | }
272 |
--------------------------------------------------------------------------------
/cmd/fee-tracer/config.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | type config struct {
4 | InfluxDB influxdb
5 | Solana solana
6 | }
7 |
8 | type influxdb struct {
9 | URL string `env:"INFLUX_URL"`
10 | Username string `env:"INFLUX_USER"`
11 | Password string `env:"INFLUX_PWD"`
12 | Database string `env:"INFLUX_DATABASE"`
13 | }
14 |
15 | type solana struct {
16 | URL string `env:"SOLANA_URL,required"`
17 | Percentile uint16 `env:"SOLANA_FEE_PERCENTILE" envDefault:"0"`
18 | }
19 |
--------------------------------------------------------------------------------
/cmd/fee-tracer/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "flag"
6 | "fmt"
7 | "os"
8 | "sort"
9 | "time"
10 |
11 | solana_client "github.com/blocto/solana-go-sdk/client"
12 | solana_common "github.com/blocto/solana-go-sdk/common"
13 | solana_rpc "github.com/blocto/solana-go-sdk/rpc"
14 | "github.com/caarlos0/env/v10"
15 | influxdb2 "github.com/influxdata/influxdb-client-go/v2"
16 | "github.com/rs/zerolog"
17 | "github.com/rs/zerolog/log"
18 | )
19 |
20 | func main() {
21 | // init log
22 | zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
23 | log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339})
24 |
25 | debug := flag.Bool("debug", false, "sets log level to debug")
26 | flag.Parse()
27 | zerolog.SetGlobalLevel(zerolog.InfoLevel)
28 | if *debug {
29 | zerolog.SetGlobalLevel(zerolog.DebugLevel)
30 | }
31 |
32 | // init config
33 | cfg := config{}
34 | if err := env.Parse(&cfg); err != nil {
35 | log.Fatal().Err(err).Msg("Config")
36 | }
37 |
38 | log.Info().Msg("Start")
39 |
40 | for {
41 | func() {
42 | // init solana client
43 | solanaConn := solana_client.NewClient(cfg.Solana.URL)
44 |
45 | // get the fees
46 | var prioritizationFees solana_rpc.PrioritizationFees
47 | var err error
48 |
49 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
50 | defer cancel()
51 |
52 | prioritizationFees, err = solanaConn.GetRecentPrioritizationFeesWithConfig(
53 | ctx,
54 | []solana_common.PublicKey{},
55 | solana_rpc.GetRecentPrioritizationFeesConfig{
56 | Percentile: cfg.Solana.Percentile,
57 | },
58 | )
59 | if err != nil {
60 | log.Error().Err(err).Msg("Solana Client")
61 | return
62 | }
63 |
64 | sort.Slice(prioritizationFees, func(i, j int) bool {
65 | return prioritizationFees[i].Slot > prioritizationFees[j].Slot
66 | })
67 | if len(prioritizationFees) == 0 {
68 | log.Warn().Err(fmt.Errorf("got empty fees")).Msg("Solana Client")
69 | return
70 | }
71 | fee := prioritizationFees[0]
72 | if e := log.Debug(); e.Enabled() {
73 | e.
74 | Uint64("slot", fee.Slot).
75 | Uint16("percentile", cfg.Solana.Percentile).
76 | Uint64("fee", fee.PrioritizationFee).
77 | Msg("Datapoint")
78 | }
79 |
80 | // write the datapoint
81 | if cfg.InfluxDB.URL != "" {
82 | influxConn := influxdb2.NewClient(
83 | cfg.InfluxDB.URL,
84 | fmt.Sprintf("%v/%v", cfg.InfluxDB.Username, cfg.InfluxDB.Password),
85 | )
86 | defer influxConn.Close()
87 |
88 | writeAPI := influxConn.WriteAPI("anzaxyz", cfg.InfluxDB.Database)
89 |
90 | writeAPI.WritePoint(
91 | influxdb2.NewPoint("fees",
92 | map[string]string{"percentile": fmt.Sprintf("%v", cfg.Solana.Percentile)},
93 | map[string]interface{}{"slot": int64(fee.Slot), "fee": int64(fee.PrioritizationFee)},
94 | time.Now(),
95 | ),
96 | )
97 | }
98 |
99 | time.Sleep(100 * time.Microsecond)
100 | }()
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/config-devnet.yaml.sample:
--------------------------------------------------------------------------------
1 | APIServer:
2 | Enabled: true
3 | Mode: http
4 | IP: "0.0.0.0:8080"
5 | SSLIP: "0.0.0.0:8433"
6 | KeyPath: "/yourpath/privkey.pem"
7 | CrtPath: "/yourpath/crt.pem"
8 | PingServiceEnabled: true
9 | AlternativeEnpoint:
10 | HostList:
11 | - Endpoint: http://127.0.0.1:8999
12 | Piority: 1
13 | MaxRetry: 10
14 | AccessToken:
15 | - Endpoint: http://localhost:8999
16 | Piority: 2
17 | MaxRetry: 5
18 | AccessToken:
19 | - Endpoint: http://api.devnet.solana.com
20 | Piority: 3
21 | MaxRetry: 5
22 | AccessToken:
23 | SlackAlert: # failover alert
24 | Enabled: true
25 | Webhook:
26 | DiscordAlert:
27 | Enabled: true
28 | Webhook:
29 | PingConfig:
30 | Receiver: 9qT3WeLV5o3t3GVgCk9A3mpTRjSb9qBvnfrAsVKLhmU5 # change your receive account here
31 | NumWorkers: 3 #change number of concurent run here
32 | BatchCount: 1
33 | BatchInverval: 1
34 | TxTimeout: 45 # timeout time
35 | WaitConfirmationTimeout: 75
36 | StatusCheckInterval: 1
37 | MinPerPingTime: 10
38 | ComputeFeeDualMode: false # send tx both with and without compute fee
39 | RequestUnits: 200000 # change RequestUnits
40 | ComputeUnitPrice: 1000 # change ComputeUnitPrice
41 | Report:
42 | Interval: 600
43 | LossThreshold: 20
44 | LevelFilePath: /yourpath/level-devnet.env
45 | Slack:
46 | Report:
47 | Enabled: true
48 | Webhook:
49 | Alert:
50 | Enabled: true
51 | Webhook:
52 | Discord:
53 | BotName: ping-service
54 | BotAvatarURL:
55 | Report:
56 | Enabled: true
57 | Webhook:
58 | Alert:
59 | Enabled: true
60 | Webhook:
61 |
62 |
--------------------------------------------------------------------------------
/config-mainnet-beta.yaml.sample:
--------------------------------------------------------------------------------
1 | APIServer:
2 | Enabled: true
3 | Mode: http
4 | IP: "0.0.0.0:8080"
5 | SSLIP: "0.0.0.0:8433"
6 | KeyPath: "/yourpath/privkey.pem"
7 | CrtPath: "/yourpath/crt.pem"
8 | PingServiceEnabled: true
9 | AlternativeEnpoint:
10 | HostList:
11 | - Endpoint: http://127.0.0.1:8999
12 | MaxRetry: 10
13 | AccessToken:
14 | - Endpoint: http://localhost:8999
15 | Piority: 2
16 | MaxRetry: 5
17 | AccessToken:
18 | - Endpoint: http://api.mainnet-beta.solana.com
19 | Piority: 3
20 | MaxRetry: 5
21 | AccessToken:
22 | SlackAlert: # failover alert
23 | Enabled: true
24 | Webhook:
25 | DiscordAlert:
26 | Enabled: true
27 | Webhook:
28 | PingConfig:
29 | Receiver: 9qT3WeLV5o3t3GVgCk9A3mpTRjSb9qBvnfrAsVKLhmU5 # change your receive account here
30 | NumWorkers: 3 #change number of concurent run here
31 | BatchCount: 1
32 | BatchInverval: 1
33 | TxTimeout: 45 # timeout time
34 | WaitConfirmationTimeout: 75
35 | StatusCheckInterval: 1
36 | MinPerPingTime: 10
37 | ComputeFeeDualMode: false # send tx both with and without compute fee
38 | RequestUnits: 200000 # change RequestUnits
39 | ComputeUnitPrice: 1000 # change ComputeUnitPrice
40 | Report:
41 | Interval: 600
42 | LossThreshold: 20
43 | LevelFilePath: /yourpath/level-mainnet.env
44 | Slack:
45 | Report:
46 | Enabled: true
47 | Webhook:
48 | Alert:
49 | Enabled: true
50 | Webhook:
51 | Discord:
52 | BotName: ping-service
53 | BotAvatarURL:
54 | Report:
55 | Enabled: false
56 | Webhook:
57 | Alert:
58 | Enabled: true
59 | Webhook:
--------------------------------------------------------------------------------
/config-testnet.yaml.sample:
--------------------------------------------------------------------------------
1 | APIServer:
2 | Enabled: true
3 | Mode: http
4 | IP: "0.0.0.0:8080"
5 | SSLIP: "0.0.0.0:8433"
6 | KeyPath: "/yourpath/privkey.pem"
7 | CrtPath: "/yourpath/crt.pem"
8 | PingServiceEnabled: true
9 | AlternativeEnpoint:
10 | HostList:
11 | - Endpoint: http://127.0.0.1:8999
12 | MaxRetry: 10
13 | AccessToken:
14 | - Endpoint: http://localhost:8999
15 | Piority: 2
16 | MaxRetry: 5
17 | AccessToken:
18 | - Endpoint: http://api.testnet.solana.com
19 | Piority: 3
20 | MaxRetry: 5
21 | AccessToken:
22 | SlackAlert: # failover alert
23 | Enabled: true
24 | Webhook:
25 | DiscordAlert:
26 | Enabled: true
27 | Webhook:
28 | PingConfig:
29 | Receiver: 9qT3WeLV5o3t3GVgCk9A3mpTRjSb9qBvnfrAsVKLhmU5 # change your receive account here
30 | NumWorkers: 3 #change number of concurent run here
31 | BatchCount: 1
32 | BatchInverval: 1
33 | TxTimeout: 45 # timeout time
34 | WaitConfirmationTimeout: 75
35 | StatusCheckInterval: 1
36 | MinPerPingTime: 10
37 | ComputeFeeDualMode: false # send tx both with and without compute fee
38 | RequestUnits: 200000 # change RequestUnits
39 | ComputeUnitPrice: 1000 # change ComputeUnitPrice
40 | Report:
41 | Interval: 600
42 | LossThreshold: 20
43 | LevelFilePath: /yourpath/level-testnet.env
44 | Slack:
45 | Report:
46 | Enabled: false
47 | Webhook:
48 | Alert:
49 | Enabled: true
50 | Webhook:
51 | Discord:
52 | BotName: ping-service
53 | BotAvatarURL:
54 | Report:
55 | Enabled: false
56 | Webhook:
57 | Alert:
58 | Enabled: true
59 | Webhook:
60 |
--------------------------------------------------------------------------------
/config.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bufio"
5 | "log"
6 | "os"
7 | "strings"
8 |
9 | // jww "github.com/spf13/jwalterweatherman"
10 | "github.com/spf13/viper"
11 | )
12 |
13 | // ConnectionMode is a type of client connection
14 | type ConnectionMode string
15 |
16 | const (
17 | HTTP ConnectionMode = "http"
18 | HTTPS ConnectionMode = "https"
19 | BOTH ConnectionMode = "both"
20 | )
21 |
22 | type PingConfig struct {
23 | Receiver string
24 | NumWorkers int
25 | BatchCount int
26 | BatchInverval int
27 | TxTimeout int64
28 | WaitConfirmationTimeout int64
29 | StatusCheckInterval int64
30 | MinPerPingTime int64
31 | ComputeFeeDualMode bool
32 | RequestUnits uint32
33 | ComputeUnitPrice uint64
34 | }
35 | type WebHookConfig struct {
36 | Enabled bool
37 | Webhook string
38 | }
39 | type SlackReport struct {
40 | Report WebHookConfig
41 | Alert WebHookConfig
42 | }
43 | type DiscordReport struct {
44 | BotName string
45 | BotAvatarURL string
46 | Report WebHookConfig
47 | Alert WebHookConfig
48 | }
49 | type Report struct {
50 | Enabled bool
51 | Interval int
52 | LossThreshold float64
53 | LevelFilePath string
54 | Slack SlackReport
55 | Discord DiscordReport
56 | }
57 | type APIServer struct {
58 | Enabled bool
59 | Mode ConnectionMode
60 | IP string
61 | SSLIP string
62 | KeyPath string
63 | CrtPath string
64 | }
65 | type Database struct {
66 | UseGoogleCloud bool
67 | GCloudCredentialPath string
68 | DBConn string
69 | }
70 | type InfluxdbConfig struct {
71 | Enabled bool
72 | InfluxdbURL string
73 | AccessToken string
74 | Orgnization string
75 | Bucket string
76 | }
77 | type Retension struct {
78 | Enabled bool
79 | KeepHours int64
80 | UpdateIntervalSec int64
81 | }
82 |
83 | type SolanaCLIConfig struct {
84 | JsonRPCURL string
85 | WebsocketURL string
86 | KeypairPath string
87 | AddressLabels map[string]string
88 | Commitment string
89 | }
90 |
91 | type ClusterCLIConfig struct {
92 | Dir string
93 | MainnetPath string
94 | TestnetPath string
95 | DevnetPath string
96 | ConfigMain SolanaCLIConfig
97 | ConfigTestnet SolanaCLIConfig
98 | ConfigDevnet SolanaCLIConfig
99 | }
100 |
101 | type RPCEndpoint struct {
102 | Endpoint string
103 | AccessToken string
104 | Piority int
105 | MaxRetry int
106 | }
107 | type EndpointAlert struct {
108 | Enabled bool
109 | Webhook string
110 | }
111 | type AlternativeEnpoint struct {
112 | HostList []RPCEndpoint
113 | SlackAlert EndpointAlert
114 | DiscordAlert EndpointAlert
115 | }
116 |
117 | type ClusterPing struct {
118 | APIServer
119 | PingServiceEnabled bool
120 | AlternativeEnpoint
121 | PingConfig
122 | Report
123 | }
124 |
125 | type ClusterConfig struct {
126 | Cluster
127 | HostName string
128 | ClusterPing
129 | }
130 |
131 | type Config struct {
132 | Database
133 | InfluxdbConfig
134 | Mainnet ClusterConfig
135 | Testnet ClusterConfig
136 | Devnet ClusterConfig
137 | ClusterCLIConfig
138 | Retension
139 | }
140 |
141 | func loadConfig() Config {
142 | // jww.SetLogThreshold(jww.LevelTrace)
143 | // jww.SetStdoutThreshold(jww.LevelTrace)
144 | c := Config{}
145 | v := viper.New()
146 | userHome, err := os.UserHomeDir()
147 | if err != nil {
148 | panic("loadConfig error:" + err.Error())
149 | }
150 |
151 | v.AddConfigPath(userHome + "/.config/ping-api")
152 | v.SetConfigType("yaml")
153 | v.AutomaticEnv()
154 | hostname, err := os.Hostname()
155 | if err != nil {
156 | hostname = "unknown"
157 | }
158 | // setup config.yaml
159 | v.SetConfigName("config")
160 | v.ReadInConfig()
161 | // setup config.yaml (Database)
162 | c.Database.UseGoogleCloud = v.GetBool("Database.UseGoogleCloud")
163 | c.Database.GCloudCredentialPath = v.GetString("Database.GCloudCredentialPath")
164 | c.DBConn = v.GetString("Database.DBConn")
165 | gcloudCredential := os.Getenv("GOOGLE_APPLICATION_CREDENTIALS")
166 | if len(gcloudCredential) == 0 && len(c.Database.GCloudCredentialPath) != 0 {
167 | os.Setenv("GOOGLE_APPLICATION_CREDENTIALS", c.Database.GCloudCredentialPath)
168 | }
169 | log.Println("GOOGLE_APPLICATION_CREDENTIALS=", os.Getenv("GOOGLE_APPLICATION_CREDENTIALS"))
170 | // setup influxdb in config.yaml
171 | c.InfluxdbConfig = InfluxdbConfig{
172 | Enabled: v.GetBool("InfluxdbConfig.Enabled"),
173 | InfluxdbURL: v.GetString("InfluxdbConfig.InfluxdbURL"),
174 | AccessToken: v.GetString("InfluxdbConfig.AccessToken"),
175 | Orgnization: v.GetString("InfluxdbConfig.Orgnization"),
176 | Bucket: v.GetString("InfluxdbConfig.Bucket"),
177 | }
178 | // setup config.yaml (Retension)
179 | c.Retension = Retension{
180 | Enabled: v.GetBool("Retension.Enabled"),
181 | KeepHours: v.GetInt64("Retension.KeepHours"),
182 | UpdateIntervalSec: v.GetInt64("Retension.UpdateIntervalSec"),
183 | }
184 | // setup config.yaml (ClusterConfigFile)
185 | c.ClusterCLIConfig = ClusterCLIConfig{
186 | Dir: v.GetString("SolanaCliFile.Dir"),
187 | MainnetPath: v.GetString("SolanaCliFile.MainnetPath"),
188 | TestnetPath: v.GetString("SolanaCliFile.TestnetPath"),
189 | DevnetPath: v.GetString("SolanaCliFile.DevnetPath"),
190 | }
191 |
192 | if len(c.ClusterCLIConfig.MainnetPath) > 0 {
193 | sConfig, err := ReadSolanaCLIConfigFile(c.ClusterCLIConfig.Dir + c.ClusterCLIConfig.MainnetPath)
194 | if err != nil {
195 | log.Fatal(err)
196 | }
197 | c.ClusterCLIConfig.ConfigMain = sConfig
198 | }
199 | if len(c.ClusterCLIConfig.TestnetPath) > 0 {
200 | sConfig, err := ReadSolanaCLIConfigFile(c.ClusterCLIConfig.Dir + c.ClusterCLIConfig.TestnetPath)
201 | if err != nil {
202 | log.Fatal(err)
203 | }
204 | c.ClusterCLIConfig.ConfigTestnet = sConfig
205 | }
206 | if len(c.ClusterCLIConfig.DevnetPath) > 0 {
207 | sConfig, err := ReadSolanaCLIConfigFile(c.ClusterCLIConfig.Dir + c.ClusterCLIConfig.DevnetPath)
208 | if err != nil {
209 | log.Fatal(err)
210 | }
211 | c.ClusterCLIConfig.ConfigDevnet = sConfig
212 | }
213 | // setup config.yaml (SolanaCliFile) all cluster services
214 | configMainnetFile := v.GetString("ClusterConfigFile.Mainnet")
215 | configTestnetFile := v.GetString("ClusterConfigFile.Testnet")
216 | configDevnetFile := v.GetString("ClusterConfigFile.Devnet")
217 | // Read Each Cluster Configurations
218 | // setup config.yaml for mainnet
219 | v.SetConfigName(configMainnetFile)
220 | v.ReadInConfig()
221 | c.Mainnet = ClusterConfig{
222 | Cluster: MainnetBeta,
223 | HostName: hostname,
224 | ClusterPing: ReadClusterPingConfig(v),
225 | }
226 | if c.Mainnet.APIServer.Mode != HTTP &&
227 | c.Mainnet.APIServer.Mode != HTTPS && c.Mainnet.APIServer.Mode != BOTH {
228 | c.Mainnet.APIServer.Mode = HTTP
229 | log.Println("Mainnet API server mode not support! use default mode")
230 | }
231 | v.SetConfigName(configTestnetFile)
232 | v.ReadInConfig()
233 | c.Testnet = ClusterConfig{
234 | Cluster: Testnet,
235 | HostName: hostname,
236 | ClusterPing: ReadClusterPingConfig(v),
237 | }
238 | if c.Testnet.APIServer.Mode != HTTP &&
239 | c.Testnet.APIServer.Mode != HTTPS && c.Testnet.APIServer.Mode != BOTH {
240 | c.Testnet.APIServer.Mode = HTTP
241 | log.Println("Mainnet API server mode not support! use default mode")
242 | }
243 | v.SetConfigName(configDevnetFile)
244 | v.ReadInConfig()
245 | c.Devnet = ClusterConfig{
246 | Cluster: Devnet,
247 | HostName: hostname,
248 | ClusterPing: ReadClusterPingConfig(v),
249 | }
250 | if c.Devnet.APIServer.Mode != HTTP &&
251 | c.Devnet.APIServer.Mode != HTTPS && c.Devnet.APIServer.Mode != BOTH {
252 | c.Devnet.APIServer.Mode = HTTP
253 | log.Println("Devnet API server mode not support! use default mode")
254 | }
255 | return c
256 | }
257 |
258 | func ReadSolanaCLIConfigFile(filepath string) (SolanaCLIConfig, error) {
259 | configmap := make(map[string]string, 1)
260 | addressmap := make(map[string]string, 1)
261 |
262 | f, err := os.Open(filepath)
263 | if err != nil {
264 | log.Printf("error opening file: %v\n", err)
265 | return SolanaCLIConfig{}, err
266 | }
267 | r := bufio.NewReader(f)
268 | line, _, err := r.ReadLine()
269 | for err == nil {
270 | k, v := ToKeyPair(string(line))
271 | if k == "address_labels" {
272 | line, _, err := r.ReadLine()
273 | lKey, lVal := ToKeyPair(string(line))
274 | if err == nil && string(line)[0:1] == " " {
275 | if len(lKey) > 0 && len(lVal) > 0 {
276 | addressmap[lKey] = lVal
277 | }
278 | } else {
279 | configmap[k] = v
280 | }
281 | } else {
282 | configmap[k] = v
283 | }
284 |
285 | line, _, err = r.ReadLine()
286 | }
287 | return SolanaCLIConfig{
288 | JsonRPCURL: configmap["json_rpc_url"],
289 | WebsocketURL: configmap["websocket_url:"],
290 | KeypairPath: configmap["keypair_path"],
291 | AddressLabels: addressmap,
292 | Commitment: configmap["commitment"],
293 | }, nil
294 | }
295 |
296 | func ToKeyPair(line string) (key string, val string) {
297 | noSpaceLine := strings.TrimSpace(string(line))
298 | idx := strings.Index(noSpaceLine, ":")
299 | if idx == -1 || idx == 0 { // not found or only have :
300 | return "", ""
301 | }
302 | if (len(noSpaceLine) - 1) == idx { // no value
303 | return strings.TrimSpace(noSpaceLine[0:idx]), ""
304 | }
305 | return strings.TrimSpace(noSpaceLine[0:idx]), strings.TrimSpace(noSpaceLine[idx+1:])
306 | }
307 |
308 | func ReadClusterPingConfig(v *viper.Viper) ClusterPing {
309 | v.Debug()
310 | clusterConf := ClusterPing{}
311 | v.Unmarshal(&clusterConf)
312 | return clusterConf
313 | }
314 |
--------------------------------------------------------------------------------
/config.yaml.sample:
--------------------------------------------------------------------------------
1 | ClusterConfigFile:
2 | Mainnet: config-mainnet-beta
3 | Testnet: config-testnet
4 | Devnet: config-devnet
5 | SolanaCliFile: # solana-cli config file
6 | Dir: /home/sol/.config/solana/cli/
7 | MainnetPath: config-mainnet-beta.yml
8 | TestnetPath: config-testnet.yml
9 | DevnetPath: config-devnet.yml
10 | Database:
11 | UseGoogleCloud: false
12 | GCloudCredentialPath: "/somepath/dev.json"
13 | DBConn: "user= password= host=localhost port=5432 dbname=" #use for both googlecloud or local database
14 | InfluxdbConfig:
15 | Enabled: true
16 | InfluxdbURL:
17 | AccessToken:
18 | Bucket:
19 | Retension:
20 | Enabled: false #Retension service
21 | KeepHours: 48
22 | UpdateIntervalSec: 3600
23 |
--------------------------------------------------------------------------------
/database.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/lib/pq"
7 | )
8 |
9 | // ComputeUnitPriceType tell program fetch what kind of compute data to fetch
10 | type ComputeUnitPriceType string
11 |
12 | // Cluster enum
13 | const (
14 | AllData ComputeUnitPriceType = "all"
15 | NoComputeUnitPrice = "zero"
16 | HasComputeUnitPrice = "hasprice"
17 | ComputeUnitPriceThreshold = "threshold"
18 | )
19 |
20 | // PingResult is a struct to store ping result and database structure
21 | type PingResult struct {
22 | TimeStamp int64 `gorm:"autoIncrement:false"`
23 | Cluster string
24 | Hostname string
25 | PingType string `gorm:"NOT NULL"`
26 | Submitted int `gorm:"NOT NULL"`
27 | Confirmed int `gorm:"NOT NULL"`
28 | Loss float64
29 | Max int64
30 | Mean int64
31 | Min int64
32 | Stddev int64
33 | TakeTime int64
34 | RequestComputeUnits uint32
35 | ComputeUnitPrice uint64
36 | Error pq.StringArray `gorm:"type:text[];"NOT NULL"`
37 | CreatedAt time.Time `gorm:"type:timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP" json:"created_at,omitempty"`
38 | UpdatedAt time.Time `gorm:"type:timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP" json:"updated_at,omitempty"`
39 | }
40 |
41 | func addRecord(data PingResult) error {
42 | result := database.Create(&data)
43 | return result.Error
44 | }
45 |
46 | func getLastN(c Cluster, pType PingType, n int, priceType ComputeUnitPriceType, threshold uint64) []PingResult {
47 | ret := []PingResult{}
48 | switch priceType {
49 | case NoComputeUnitPrice:
50 | database.Order("time_stamp desc").Where("cluster=? AND ping_type=? AND compute_unit_price = ?", c, string(pType), 0).Limit(n).Find(&ret)
51 | case HasComputeUnitPrice:
52 | database.Order("time_stamp desc").Where("cluster=? AND ping_type=? AND compute_unit_price > ?", c, string(pType), 0).Limit(n).Find(&ret)
53 | case ComputeUnitPriceThreshold:
54 | database.Order("time_stamp desc").Where("cluster=? AND ping_type=? AND compute_unit_price > ?", c, string(pType), threshold).Limit(n).Find(&ret)
55 | case AllData:
56 | fallthrough
57 | default:
58 | database.Order("time_stamp desc").Where("cluster=? AND ping_type=?", c, string(pType)).Limit(n).Find(&ret)
59 | }
60 | return ret
61 | }
62 | func getAfter(c Cluster, pType PingType, t int64, priceType ComputeUnitPriceType, threshold uint64) []PingResult {
63 | ret := []PingResult{}
64 | now := time.Now().UTC().Unix()
65 | switch priceType {
66 | case NoComputeUnitPrice:
67 | database.Where("cluster=? AND ping_type=? AND time_stamp > ? AND time_stamp < ? AND compute_unit_price = ?", c, string(pType), t, now, 0).Find(&ret)
68 | case HasComputeUnitPrice:
69 | database.Where("cluster=? AND ping_type=? AND time_stamp > ? AND time_stamp < ? AND compute_unit_price > ?", c, string(pType), t, now, 0).Find(&ret)
70 | case ComputeUnitPriceThreshold:
71 | database.Where("cluster=? AND ping_type=? AND time_stamp > ? AND time_stamp < ? AND compute_unit_price > ?", c, string(pType), t, now, threshold).Find(&ret)
72 | case AllData:
73 | fallthrough
74 | default:
75 | database.Where("cluster=? AND ping_type=? AND time_stamp > ? AND time_stamp < ?", c, string(pType), t, now).Find(&ret)
76 | }
77 |
78 | return ret
79 | }
80 |
81 | func deleteTimeBefore(t int64) {
82 | database.Where("time_stamp < ?", t).Delete(&[]PingResult{})
83 | }
84 |
--------------------------------------------------------------------------------
/docker/Dockerfile.default:
--------------------------------------------------------------------------------
1 | FROM golang:1.22 AS builder
2 | ARG COMPONENT
3 | WORKDIR /workspace
4 | COPY . .
5 | RUN go build -o app ./cmd/$COMPONENT
6 |
7 | FROM debian:latest
8 | RUN \
9 | apt-get update && \
10 | apt-get install ca-certificates curl vim -y
11 | WORKDIR /workspace
12 | COPY --from=builder /workspace/app .
13 | CMD ["./app"]
14 |
--------------------------------------------------------------------------------
/error.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "errors"
5 | "strings"
6 | )
7 |
8 | // ping response error type
9 | type PingResultError string
10 |
11 | const EmptyPingResultError = ""
12 |
13 | // self defined Errors
14 | var (
15 | ErrInvalidCluster = errors.New("invalid cluster")
16 | ErrFindIndexNotFound = errors.New("findIndex does not find pattern")
17 | ErrParseMessageError = errors.New("parse message error")
18 | ErrConvertWrongType = errors.New("parse result convert to type fail")
19 | ErrParseSplit = errors.New("split message fail")
20 | ErrResultInvalid = errors.New("invalid Result")
21 | ErrNoPingResult = errors.New("no Ping Result")
22 | ErrNoPingResultRecord = errors.New("no Ping Result Record")
23 | ErrNoPingResultShort = errors.New("PingResultError has no shortname")
24 | ErrTransactionLoss = errors.New("TransactionLoss")
25 | ErrInProcessedStateTimeout = errors.New("TxHash is still in processd state but timeout")
26 | ErrWaitForConfirmedTimeout = errors.New("Wait for a confirmed block timeout")
27 | ErrGetKeyPair = errors.New("No valid KeyPair")
28 | ErrKeyPairFile = errors.New("Read KeyPair File Error")
29 | )
30 |
31 | // Setup Statistic / Alert / Report Error Exception List
32 | var (
33 | ResponseErrIdentifierList []ErrRespIdentifier
34 | // Error which does not use in Statistic computation
35 | StatisticErrorExceptionList []ErrRespIdentifier
36 | // Error does not show in slack alert
37 | AlertErrorExceptionList []ErrRespIdentifier
38 | // Error does not show in the report Error List
39 | ReportErrorExceptionList []ErrRespIdentifier
40 | // error that does not be added into TakeTime
41 | PingTakeTimeErrExpectionList []ErrRespIdentifier
42 | )
43 |
44 | func (e PingResultError) IsBlockhashNotFound() bool {
45 | return BlockhashNotFound.IsIdentical(e)
46 | }
47 | func (e PingResultError) IsTransactionHasAlreadyBeenProcessed() bool {
48 | return TransactionHasAlreadyBeenProcessed.IsIdentical(e)
49 | }
50 |
51 | func (e PingResultError) IsRPCServerDeadlineExceeded() bool {
52 | return RPCServerDeadlineExceeded.IsIdentical(e)
53 | }
54 |
55 | func (e PingResultError) IsServiceUnavilable() bool {
56 | return ServiceUnavilable503.IsIdentical(e)
57 | }
58 |
59 | func (e PingResultError) IsTooManyRequest429() bool {
60 | return TooManyRequest429.IsIdentical(e)
61 | }
62 |
63 | func (e PingResultError) IsNumSlotsBehind() bool {
64 | return NumSlotsBehind.IsIdentical(e)
65 | }
66 |
67 | func (e PingResultError) IsErrRPCEOF() bool {
68 | return RPCEOF.IsIdentical(e)
69 | }
70 | func (e PingResultError) IsErrGatewayTimeout504() bool {
71 | return GatewayTimeout504.IsIdentical(e)
72 | }
73 | func (e PingResultError) IsConnectionRefused() bool {
74 | return ConnectionRefused.IsIdentical(e)
75 | }
76 | func (e PingResultError) IsNoSuchHost() bool {
77 | return NoSuchHost.IsIdentical(e)
78 | }
79 |
80 | func (p PingResultError) IsInErrorList(inErrs []ErrRespIdentifier) bool {
81 | for _, idf := range inErrs {
82 | if idf.IsIdentical(p) {
83 | return true
84 | }
85 | }
86 | return false
87 | }
88 | func (p PingResultError) Short() string {
89 | for _, idf := range ResponseErrIdentifierList {
90 | if idf.IsIdentical(p) {
91 | return idf.Short
92 | }
93 | }
94 | return string(p)
95 | }
96 |
97 | func (p PingResultError) Subsitute(old string, new string) string {
98 | return strings.ReplaceAll(string(p), old, new)
99 | }
100 |
101 | func (p PingResultError) HasError() bool {
102 | return p != EmptyPingResultError
103 | }
104 |
105 | func ResponseErrIdentifierInit() []ErrRespIdentifier {
106 | ResponseErrIdentifierList = []ErrRespIdentifier{}
107 | ResponseErrIdentifierList = append(ResponseErrIdentifierList, BlockhashNotFound)
108 | ResponseErrIdentifierList = append(ResponseErrIdentifierList, TransactionHasAlreadyBeenProcessed)
109 | ResponseErrIdentifierList = append(ResponseErrIdentifierList, RPCServerDeadlineExceeded)
110 | ResponseErrIdentifierList = append(ResponseErrIdentifierList, ServiceUnavilable503)
111 | ResponseErrIdentifierList = append(ResponseErrIdentifierList, TooManyRequest429)
112 | ResponseErrIdentifierList = append(ResponseErrIdentifierList, NumSlotsBehind)
113 | ResponseErrIdentifierList = append(ResponseErrIdentifierList, RPCEOF)
114 | ResponseErrIdentifierList = append(ResponseErrIdentifierList, GatewayTimeout504)
115 | ResponseErrIdentifierList = append(ResponseErrIdentifierList, NoSuchHost)
116 | ResponseErrIdentifierList = append(ResponseErrIdentifierList, TxHasAlreadyProcess)
117 | return ResponseErrIdentifierList
118 | }
119 |
120 | func StatisticErrExpectionInit() []ErrRespIdentifier {
121 | StatisticErrorExceptionList = []ErrRespIdentifier{}
122 | StatisticErrorExceptionList = append(StatisticErrorExceptionList, BlockhashNotFound)
123 | StatisticErrorExceptionList = append(StatisticErrorExceptionList, TransactionHasAlreadyBeenProcessed)
124 | return StatisticErrorExceptionList
125 | }
126 |
127 | func AlertErrExpectionInit() []ErrRespIdentifier {
128 | AlertErrorExceptionList = []ErrRespIdentifier{}
129 | AlertErrorExceptionList = append(AlertErrorExceptionList, RPCServerDeadlineExceeded)
130 | AlertErrorExceptionList = append(AlertErrorExceptionList, BlockhashNotFound)
131 | AlertErrorExceptionList = append(AlertErrorExceptionList, TransactionHasAlreadyBeenProcessed)
132 | return AlertErrorExceptionList
133 | }
134 |
135 | func ReportErrExpectionInit() []ErrRespIdentifier {
136 | ReportErrorExceptionList = []ErrRespIdentifier{}
137 | ReportErrorExceptionList = append(ReportErrorExceptionList, TransactionHasAlreadyBeenProcessed)
138 | return ReportErrorExceptionList
139 | }
140 |
141 | func PingTakeTimeErrExpectionInit() []ErrRespIdentifier {
142 | PingTakeTimeErrExpectionList = []ErrRespIdentifier{}
143 | return PingTakeTimeErrExpectionList
144 | }
145 |
--------------------------------------------------------------------------------
/errorRespIdentifier.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import "strings"
4 |
5 | /*
6 | Add an Known Response Error (Error from endpoint Server)
7 | 1. Add its error text from response
8 | ie.BlockhashNotFoundText
9 | 2. create and ErrIdentifier
10 | ie.
11 | BlockhashNotFound = ErrIdentifier{
12 | Text: PingResultError(BlockhashNotFoundText),
13 | Key: []string{"BlockhashNotFound"},
14 | Short: "BlockhashNotFound"}
15 | 3. Add to KnownErrIdentifierInit in error.go
16 | KnownErrIdentifierList =
17 | append(KnownErrIdentifierList, BlockhashNotFound)
18 | */
19 |
20 | type ErrRespIdentifier struct {
21 | Text PingResultError
22 | Key []string
23 | Short string
24 | }
25 |
26 | // response errors
27 | var (
28 | BlockhashNotFoundText = `rpc response error: {"code":-32002,"message":"Transaction simulation failed: Blockhash not found","data":{"accounts":null,"err":"BlockhashNotFound","logs":[],"unitsConsumed":0}}`
29 | TransactionHasAlreadyBeenProcessedText = `rpc response error: {"code":-32002,"message":"Transaction simulation failed: This transaction has already been processed","data":{"accounts":null,"err":"AlreadyProcessed","logs":[],"unitsConsumed":0}}`
30 | RPCServerDeadlineExceededText = `rpc: call error, err: failed to do request, err: Post "https://api.internal.mainnet-beta.solana.com": context deadline exceeded`
31 | ServiceUnavilable503Text = `rpc: call error, err: get status code: 503, body:
503 Service Unavailable
32 | No server is available to handle this request.
33 | `
34 | TooManyRequest429Text = `rpc: call error, err: get status code: 429, body: 429 Too Many Requests
35 | 429 Too Many Requests
nginx/1.21.5`
36 | NumSlotsBehindText = `{count:5 : rpc response error: {"code":-32005,"message":"Node is behind by 153 slots","data":{"numSlotsBehind":153}}`
37 | RPCEOFText = `rpc: call error, err: failed to do request, err: Post "https://api.internal.mainnet-beta.solana.com": EOF, body: `
38 | GatewayTimeout504Text = `rpc: call error, err: get status code: 504, body: 504 Gateway Time-out
39 | The server didn't respond in time.
40 |
41 | `
42 | NoSuchHostText = `rpc: call error, err: failed to do request, err: Post "https://api.internal.mainnet-beta.solana.comx": dial tcp: lookup api.internal.mainnet-beta.solana.comx: no such host, body:`
43 | ConnectionRefusedText = `rpc: call error, err: failed to do request, err: Post "https://api.devnet.solana.com/": dial tcp 139.178.65.155:443: connect: connection refused, body: `
44 | TxHasAlreadyProcessText = `rpc response error: {"code":-32002,"message":"Transaction simulation failed: This transaction has already been processed","data":{"accounts":null,"err":"AlreadyProcessed","logs":[],"unitsConsumed":0}}`
45 | )
46 |
47 | var (
48 | BlockhashNotFound = ErrRespIdentifier{
49 | Text: PingResultError(BlockhashNotFoundText),
50 | Key: []string{"BlockhashNotFound"},
51 | Short: "BlockhashNotFound"}
52 | TransactionHasAlreadyBeenProcessed = ErrRespIdentifier{
53 | Text: PingResultError(TransactionHasAlreadyBeenProcessedText),
54 | Key: []string{"AlreadyProcessed"},
55 | Short: "transaction has already been processed"}
56 | RPCServerDeadlineExceeded = ErrRespIdentifier{
57 | Text: PingResultError(RPCServerDeadlineExceededText),
58 | Key: []string{"context deadline exceeded"},
59 | Short: "post to rpc server response timeout"}
60 | ServiceUnavilable503 = ErrRespIdentifier{
61 | Text: PingResultError(ServiceUnavilable503Text),
62 | Key: []string{"code: 503"},
63 | Short: "503-service-unavailable"}
64 | TooManyRequest429 = ErrRespIdentifier{
65 | Text: PingResultError(TooManyRequest429Text),
66 | Key: []string{"code: 429"},
67 | Short: "429-too-many-requests"}
68 | ConnectionRefused = ErrRespIdentifier{
69 | Text: PingResultError(ConnectionRefusedText),
70 | Key: []string{"connection refused"},
71 | Short: "connection-refused"}
72 | NumSlotsBehind = ErrRespIdentifier{
73 | Text: PingResultError(NumSlotsBehindText),
74 | Key: []string{"numSlotsBehind"},
75 | Short: "numSlotsBehind"}
76 | RPCEOF = ErrRespIdentifier{
77 | Text: PingResultError(RPCEOFText),
78 | Key: []string{"EOF"},
79 | Short: "rpc error EOF"}
80 | GatewayTimeout504 = ErrRespIdentifier{
81 | Text: PingResultError(GatewayTimeout504Text),
82 | Key: []string{"code: 504"},
83 | Short: "504-gateway-timeout"}
84 | NoSuchHost = ErrRespIdentifier{
85 | Text: PingResultError(NoSuchHostText),
86 | Key: []string{"no such host"},
87 | Short: "no-such-host"}
88 | TxHasAlreadyProcess = ErrRespIdentifier{
89 | Text: PingResultError(TxHasAlreadyProcessText),
90 | Key: []string{"transaction has already been processed"},
91 | Short: "tx-has-been-processed"}
92 | )
93 |
94 | func (e ErrRespIdentifier) IsIdentical(p PingResultError) bool {
95 | for _, k := range e.Key {
96 | if strings.Contains(string(p), k) {
97 | return true
98 | }
99 | }
100 | return false
101 | }
102 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module solana-labs/solana-ping-api-service
2 |
3 | go 1.21
4 |
5 | require (
6 | github.com/GoogleCloudPlatform/cloudsql-proxy v1.29.0
7 | github.com/blocto/solana-go-sdk v1.27.1-0.20240322074811-5a60fbd6a563
8 | github.com/caarlos0/env/v10 v10.0.0
9 | github.com/gin-contrib/timeout v0.0.3
10 | github.com/gin-gonic/gin v1.7.7
11 | github.com/influxdata/influxdb-client-go/v2 v2.12.0
12 | github.com/lib/pq v1.10.4
13 | github.com/parnurzeal/gorequest v0.2.16
14 | github.com/rs/zerolog v1.15.0
15 | github.com/spf13/viper v1.10.1
16 | gorm.io/driver/postgres v1.3.1
17 | gorm.io/gorm v1.23.3
18 | )
19 |
20 | require (
21 | cloud.google.com/go/compute v1.19.1 // indirect
22 | cloud.google.com/go/compute/metadata v0.2.3 // indirect
23 | filippo.io/edwards25519 v1.0.0-rc.1 // indirect
24 | github.com/deepmap/oapi-codegen v1.8.2 // indirect
25 | github.com/elazarl/goproxy v0.0.0-20211114080932-d06c3be7c11b // indirect
26 | github.com/fsnotify/fsnotify v1.5.1 // indirect
27 | github.com/gin-contrib/sse v0.1.0 // indirect
28 | github.com/go-playground/locales v0.14.0 // indirect
29 | github.com/go-playground/universal-translator v0.18.0 // indirect
30 | github.com/go-playground/validator/v10 v10.10.1 // indirect
31 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
32 | github.com/golang/protobuf v1.5.3 // indirect
33 | github.com/google/uuid v1.3.0 // indirect
34 | github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect
35 | github.com/googleapis/gax-go/v2 v2.7.1 // indirect
36 | github.com/hashicorp/hcl v1.0.0 // indirect
37 | github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839 // indirect
38 | github.com/jackc/chunkreader/v2 v2.0.1 // indirect
39 | github.com/jackc/pgconn v1.11.0 // indirect
40 | github.com/jackc/pgio v1.0.0 // indirect
41 | github.com/jackc/pgpassfile v1.0.0 // indirect
42 | github.com/jackc/pgproto3/v2 v2.2.0 // indirect
43 | github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect
44 | github.com/jackc/pgtype v1.10.0 // indirect
45 | github.com/jackc/pgx/v4 v4.15.0 // indirect
46 | github.com/jinzhu/inflection v1.0.0 // indirect
47 | github.com/jinzhu/now v1.1.5 // indirect
48 | github.com/json-iterator/go v1.1.12 // indirect
49 | github.com/leodido/go-urn v1.2.1 // indirect
50 | github.com/magiconair/properties v1.8.6 // indirect
51 | github.com/mattn/go-isatty v0.0.16 // indirect
52 | github.com/mitchellh/mapstructure v1.4.3 // indirect
53 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
54 | github.com/modern-go/reflect2 v1.0.2 // indirect
55 | github.com/mr-tron/base58 v1.2.0 // indirect
56 | github.com/near/borsh-go v0.3.2-0.20220516180422-1ff87d108454 // indirect
57 | github.com/pelletier/go-toml v1.9.4 // indirect
58 | github.com/pkg/errors v0.9.1 // indirect
59 | github.com/smartystreets/goconvey v1.7.2 // indirect
60 | github.com/spf13/afero v1.8.2 // indirect
61 | github.com/spf13/cast v1.4.1 // indirect
62 | github.com/spf13/jwalterweatherman v1.1.0 // indirect
63 | github.com/spf13/pflag v1.0.5 // indirect
64 | github.com/subosito/gotenv v1.2.0 // indirect
65 | github.com/ugorji/go/codec v1.2.7 // indirect
66 | go.opencensus.io v0.24.0 // indirect
67 | go.uber.org/atomic v1.9.0 // indirect
68 | go.uber.org/multierr v1.8.0 // indirect
69 | go.uber.org/zap v1.21.0 // indirect
70 | golang.org/x/crypto v0.16.0 // indirect
71 | golang.org/x/net v0.19.0 // indirect
72 | golang.org/x/oauth2 v0.7.0 // indirect
73 | golang.org/x/sys v0.15.0 // indirect
74 | golang.org/x/text v0.14.0 // indirect
75 | golang.org/x/time v0.0.0-20220224211638-0e9765cccd65 // indirect
76 | google.golang.org/api v0.114.0 // indirect
77 | google.golang.org/appengine v1.6.7 // indirect
78 | google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
79 | google.golang.org/grpc v1.56.3 // indirect
80 | google.golang.org/protobuf v1.30.0 // indirect
81 | gopkg.in/ini.v1 v1.66.4 // indirect
82 | gopkg.in/yaml.v2 v2.4.0 // indirect
83 | moul.io/http2curl v1.0.0 // indirect
84 | )
85 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
3 | cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
4 | cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
5 | cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
6 | cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
7 | cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
8 | cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
9 | cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
10 | cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
11 | cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
12 | cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
13 | cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
14 | cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
15 | cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
16 | cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
17 | cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
18 | cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
19 | cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY=
20 | cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg=
21 | cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8=
22 | cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0=
23 | cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY=
24 | cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM=
25 | cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY=
26 | cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ=
27 | cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI=
28 | cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4=
29 | cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc=
30 | cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA=
31 | cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A=
32 | cloud.google.com/go v0.110.0 h1:Zc8gqp3+a9/Eyph2KDmcGaPtbKRIoqq4YTlL4NMD0Ys=
33 | cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
34 | cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
35 | cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
36 | cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
37 | cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
38 | cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
39 | cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow=
40 | cloud.google.com/go/compute v1.2.0/go.mod h1:xlogom/6gr8RJGBe7nT2eGsQYAFUbbv8dbC29qE3Xmw=
41 | cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM=
42 | cloud.google.com/go/compute v1.19.1 h1:am86mquDUgjGNWxiGn+5PGLbmgiWXlE/yNWpIpNvuXY=
43 | cloud.google.com/go/compute v1.19.1/go.mod h1:6ylj3a05WF8leseCdIf77NK0g1ey+nj5IKd5/kvShxE=
44 | cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
45 | cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
46 | cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
47 | cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
48 | cloud.google.com/go/longrunning v0.4.1 h1:v+yFJOfKC3yZdY6ZUI933pIYdhyhV8S3NpWrXWmg7jM=
49 | cloud.google.com/go/longrunning v0.4.1/go.mod h1:4iWDqhBZ70CvZ6BfETbvam3T8FMvLK+eFj0E6AaRQTo=
50 | cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
51 | cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
52 | cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
53 | cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
54 | cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
55 | cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
56 | cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
57 | cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
58 | cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
59 | cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
60 | dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
61 | filippo.io/edwards25519 v1.0.0-rc.1 h1:m0VOOB23frXZvAOK44usCgLWvtsxIoMCTBGJZlpmGfU=
62 | filippo.io/edwards25519 v1.0.0-rc.1/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns=
63 | github.com/Azure/azure-sdk-for-go/sdk/azcore v0.19.0/go.mod h1:h6H6c8enJmmocHUbLiiGY6sx7f9i+X3m1CHdd5c6Rdw=
64 | github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.11.0/go.mod h1:HcM1YX14R7CJcghJGOYCgdezslRSVzqwLf/q+4Y2r/0=
65 | github.com/Azure/azure-sdk-for-go/sdk/internal v0.7.0/go.mod h1:yqy467j36fJxcRV2TzfVZ1pCb5vxm4BtZPUdYWe/Xo8=
66 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
67 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
68 | github.com/GoogleCloudPlatform/cloudsql-proxy v1.29.0 h1:YNu23BtH0PKF+fg3ykSorCp6jSTjcEtfnYLzbmcjVRA=
69 | github.com/GoogleCloudPlatform/cloudsql-proxy v1.29.0/go.mod h1:spvB9eLJH9dutlbPSRmHvSXXHOwGRyeXh1jVdquA2G8=
70 | github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc=
71 | github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
72 | github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
73 | github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
74 | github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
75 | github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
76 | github.com/blocto/solana-go-sdk v1.27.1-0.20240322074811-5a60fbd6a563 h1:N/GvPe+S5pY9eiJUoLhOChR7A5ahg0Xm6rotO04lvuo=
77 | github.com/blocto/solana-go-sdk v1.27.1-0.20240322074811-5a60fbd6a563/go.mod h1:Xoyhhb3hrGpEQ5rJps5a3OgMwDpmEhrd9bgzFKkkwMs=
78 | github.com/caarlos0/env/v10 v10.0.0 h1:yIHUBZGsyqCnpTkbjk8asUlx6RFhhEs+h7TOBdgdzXA=
79 | github.com/caarlos0/env/v10 v10.0.0/go.mod h1:ZfulV76NvVPw3tm591U4SwL3Xx9ldzBP9aGxzeN7G18=
80 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
81 | github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
82 | github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
83 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
84 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
85 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
86 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
87 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
88 | github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
89 | github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
90 | github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=
91 | github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
92 | github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
93 | github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
94 | github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
95 | github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
96 | github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
97 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
98 | github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
99 | github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
100 | github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
101 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
102 | github.com/cyberdelia/templates v0.0.0-20141128023046-ca7fffd4298c/go.mod h1:GyV+0YP4qX0UQ7r2MoYZ+AvYDp12OF5yg4q8rGnyNh4=
103 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
104 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
105 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
106 | github.com/deepmap/oapi-codegen v1.8.2 h1:SegyeYGcdi0jLLrpbCMoJxnUUn8GBXHsvr4rbzjuhfU=
107 | github.com/deepmap/oapi-codegen v1.8.2/go.mod h1:YLgSKSDv/bZQB7N4ws6luhozi3cEdRktEqrX88CvjIw=
108 | github.com/denisenkom/go-mssqldb v0.12.0/go.mod h1:iiK0YP1ZeepvmBQk/QpLEhhTNJgfzrpArPY/aFvc9yU=
109 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
110 | github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
111 | github.com/elazarl/goproxy v0.0.0-20211114080932-d06c3be7c11b h1:1XqENn2YoYZd6w3Awx+7oa+aR87DFIZJFLF2n1IojA0=
112 | github.com/elazarl/goproxy v0.0.0-20211114080932-d06c3be7c11b/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM=
113 | github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8=
114 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
115 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
116 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
117 | github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
118 | github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
119 | github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
120 | github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
121 | github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
122 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
123 | github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI=
124 | github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU=
125 | github.com/getkin/kin-openapi v0.61.0/go.mod h1:7Yn5whZr5kJi6t+kShccXS8ae1APpYTW6yheSwk8Yi4=
126 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
127 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
128 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
129 | github.com/gin-contrib/timeout v0.0.3 h1:ysZQ7kChgqlzBkuLgwTTDjTPP2uqdI68XxRyqIFK68g=
130 | github.com/gin-contrib/timeout v0.0.3/go.mod h1:F3fjkmFc4I1QdF7MyVwtO6ZkPueBckNoiOVpU73HGgU=
131 | github.com/gin-gonic/gin v1.7.2/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY=
132 | github.com/gin-gonic/gin v1.7.7 h1:3DoBmSbJbZAWqXJC3SLjAPfutPJJRN1U5pALB7EeTTs=
133 | github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U=
134 | github.com/go-chi/chi/v5 v5.0.0/go.mod h1:BBug9lr0cqtdAhsu6R4AAdvufI0/XBzAQSsUqJpoZOs=
135 | github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
136 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
137 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
138 | github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
139 | github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
140 | github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
141 | github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
142 | github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
143 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
144 | github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
145 | github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU=
146 | github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
147 | github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
148 | github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho=
149 | github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
150 | github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
151 | github.com/go-playground/validator/v10 v10.10.1 h1:uA0+amWMiglNZKZ9FJRKUAe9U3RX91eVn1JYXMWt7ig=
152 | github.com/go-playground/validator/v10 v10.10.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU=
153 | github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
154 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
155 | github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
156 | github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw=
157 | github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
158 | github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
159 | github.com/golang-sql/sqlexp v0.0.0-20170517235910-f1bb20e5a188/go.mod h1:vXjM/+wXQnTPR4KqTKDgJukSZ6amVRtWMPEjE6sQoK8=
160 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
161 | github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
162 | github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
163 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
164 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
165 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
166 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
167 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
168 | github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
169 | github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
170 | github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
171 | github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
172 | github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
173 | github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
174 | github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
175 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
176 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
177 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
178 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
179 | github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
180 | github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
181 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
182 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
183 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
184 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
185 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
186 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
187 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
188 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
189 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
190 | github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
191 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
192 | github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
193 | github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
194 | github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
195 | github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219/go.mod h1:/X8TswGSh1pIozq4ZwCfxS0WA5JGXguxk94ar/4c87Y=
196 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
197 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
198 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
199 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
200 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
201 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
202 | github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
203 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
204 | github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
205 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
206 | github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
207 | github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
208 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
209 | github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
210 | github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
211 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
212 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
213 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
214 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
215 | github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
216 | github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
217 | github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk=
218 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
219 | github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
220 | github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
221 | github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
222 | github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
223 | github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
224 | github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
225 | github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
226 | github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
227 | github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
228 | github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
229 | github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
230 | github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
231 | github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
232 | github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
233 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
234 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
235 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
236 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
237 | github.com/googleapis/enterprise-certificate-proxy v0.2.3 h1:yk9/cqRKtT9wXZSsRH9aurXEpJX+U6FLtpYTdC3R06k=
238 | github.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k=
239 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
240 | github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
241 | github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0=
242 | github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM=
243 | github.com/googleapis/gax-go/v2 v2.7.1 h1:gF4c0zjUP2H/s/hEGyLA3I0fA2ZWjzYiONAD6cvPr8A=
244 | github.com/googleapis/gax-go/v2 v2.7.1/go.mod h1:4orTrqY6hXxxaUL4LHIPl6lGo8vAE38/qKbhSAKP6QI=
245 | github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
246 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
247 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
248 | github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
249 | github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
250 | github.com/hanwen/go-fuse v1.0.0/go.mod h1:unqXarDXqzAk0rt98O2tVndEPIpUgLD9+rwFisZH3Ok=
251 | github.com/hanwen/go-fuse/v2 v2.1.0/go.mod h1:oRyA5eK+pvJyv5otpO/DgccS8y/RvYMaO00GgRLGryc=
252 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
253 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
254 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
255 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
256 | github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
257 | github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
258 | github.com/influxdata/influxdb-client-go/v2 v2.12.0 h1:LGct9uIp36IT+8RAJdmJGQbNonGi26YfYYSpDIyq8fI=
259 | github.com/influxdata/influxdb-client-go/v2 v2.12.0/go.mod h1:YteV91FiQxRdccyJ2cHvj2f/5sq4y4Njqu1fQzsQCOU=
260 | github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839 h1:W9WBk7wlPfJLvMCdtV4zPulc4uCPrlywQOmbFOhgQNU=
261 | github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo=
262 | github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
263 | github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
264 | github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8=
265 | github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
266 | github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA=
267 | github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE=
268 | github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s=
269 | github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o=
270 | github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY=
271 | github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI=
272 | github.com/jackc/pgconn v1.10.1/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI=
273 | github.com/jackc/pgconn v1.11.0 h1:HiHArx4yFbwl91X3qqIHtUFoiIfLNJXCQRsnzkiwwaQ=
274 | github.com/jackc/pgconn v1.11.0/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI=
275 | github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE=
276 | github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=
277 | github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE=
278 | github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c=
279 | github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc=
280 | github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak=
281 | github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
282 | github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
283 | github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78=
284 | github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA=
285 | github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg=
286 | github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
287 | github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
288 | github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
289 | github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
290 | github.com/jackc/pgproto3/v2 v2.2.0 h1:r7JypeP2D3onoQTCxWdTpCtJ4D+qpKr0TxvoyMhZ5ns=
291 | github.com/jackc/pgproto3/v2 v2.2.0/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
292 | github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg=
293 | github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
294 | github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg=
295 | github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc=
296 | github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw=
297 | github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM=
298 | github.com/jackc/pgtype v1.9.1/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4=
299 | github.com/jackc/pgtype v1.10.0 h1:ILnBWrRMSXGczYvmkYD6PsYyVFUNLTnIUJHHDLmqk38=
300 | github.com/jackc/pgtype v1.10.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4=
301 | github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y=
302 | github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM=
303 | github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=
304 | github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs=
305 | github.com/jackc/pgx/v4 v4.14.1/go.mod h1:RgDuE4Z34o7XE92RpLsvFiOEfrAUT0Xt2KxvX73W06M=
306 | github.com/jackc/pgx/v4 v4.15.0 h1:B7dTkXsdILD3MF987WGGCcg+tvLW6bZJdEcqVFeU//w=
307 | github.com/jackc/pgx/v4 v4.15.0/go.mod h1:D/zyOyXiaM1TmVWnOM18p0xdDtdakRBa0RsVGI3U3bw=
308 | github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
309 | github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
310 | github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
311 | github.com/jackc/puddle v1.2.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
312 | github.com/jackc/puddle v1.2.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
313 | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
314 | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
315 | github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
316 | github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
317 | github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
318 | github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
319 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
320 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
321 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
322 | github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
323 | github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
324 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
325 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
326 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
327 | github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
328 | github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
329 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
330 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
331 | github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
332 | github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
333 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
334 | github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
335 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
336 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
337 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
338 | github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k=
339 | github.com/labstack/echo/v4 v4.2.1/go.mod h1:AA49e0DZ8kk5jTOOCKNuPR6oTnBS0dYiM4FW1e6jwpg=
340 | github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
341 | github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
342 | github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
343 | github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
344 | github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
345 | github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
346 | github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
347 | github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
348 | github.com/lib/pq v1.10.4 h1:SO9z7FRPzA03QhHKJrH5BXA6HU1rS4V2nIVrrNC1iYk=
349 | github.com/lib/pq v1.10.4/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
350 | github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo=
351 | github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
352 | github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
353 | github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
354 | github.com/matryer/moq v0.0.0-20190312154309-6cfb0558e1bd/go.mod h1:9ELz6aaclSIGnZBoaSLZ3NAl1VTufbOrXBPvtcy6WiQ=
355 | github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
356 | github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
357 | github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
358 | github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
359 | github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
360 | github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
361 | github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
362 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
363 | github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
364 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
365 | github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
366 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
367 | github.com/mitchellh/mapstructure v1.4.3 h1:OVowDSCllw/YjdLkam3/sm7wEtOy59d8ndGgCcyj8cs=
368 | github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
369 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
370 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
371 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
372 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
373 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
374 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
375 | github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8=
376 | github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
377 | github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
378 | github.com/near/borsh-go v0.3.2-0.20220516180422-1ff87d108454 h1:lFN7TVecCMbCHVNfEofDqqaVsuAlkFyDmmO7EF4nXj4=
379 | github.com/near/borsh-go v0.3.2-0.20220516180422-1ff87d108454/go.mod h1:NeMochZp7jN/pYFuxLkrZtmLqbADmnp/y1+/dL+AsyQ=
380 | github.com/parnurzeal/gorequest v0.2.16 h1:T/5x+/4BT+nj+3eSknXmCTnEVGSzFzPGdpqmUVVZXHQ=
381 | github.com/parnurzeal/gorequest v0.2.16/go.mod h1:3Kh2QUMJoqw3icWAecsyzkpY7UzRfDhbRdTjtNwNiUE=
382 | github.com/pelletier/go-toml v1.9.4 h1:tjENF6MfZAg8e4ZmZTeWaWiT2vXtsoO6+iuOjFhECwM=
383 | github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
384 | github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA=
385 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
386 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
387 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
388 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
389 | github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
390 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
391 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
392 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
393 | github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
394 | github.com/rogpeppe/go-charset v0.0.0-20180617210344-2471d30d28b4/go.mod h1:qgYeAmZ5ZIpBWTGllZSQnw97Dj+woV0toclVaRGI8pc=
395 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
396 | github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
397 | github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
398 | github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
399 | github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
400 | github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
401 | github.com/rs/zerolog v1.15.0 h1:uPRuwkWF4J6fGsJ2R0Gn2jB1EQiav9k3S6CSdygQJXY=
402 | github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
403 | github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
404 | github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
405 | github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
406 | github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
407 | github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
408 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
409 | github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs=
410 | github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
411 | github.com/smartystreets/goconvey v1.7.2 h1:9RBaZCeXEQ3UselpuwUQHltGVXvdwm6cv1hgR6gDIPg=
412 | github.com/smartystreets/goconvey v1.7.2/go.mod h1:Vw0tHAZW6lzCRk3xgdin6fKYcG+G3Pg9vgXWeJpQFMM=
413 | github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
414 | github.com/spf13/afero v1.8.2 h1:xehSyVa0YnHWsJ49JFljMpg1HX19V6NDZ1fkm1Xznbo=
415 | github.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo=
416 | github.com/spf13/cast v1.4.1 h1:s0hze+J0196ZfEMTs80N7UlFt0BDuQ7Q+JDnHiMWKdA=
417 | github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
418 | github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
419 | github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
420 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
421 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
422 | github.com/spf13/viper v1.10.1 h1:nuJZuYpG7gTj/XqiUwg8bA0cp1+M2mC3J4g5luUYBKk=
423 | github.com/spf13/viper v1.10.1/go.mod h1:IGlFPqhNAPKRxohIzWpI5QEy4kuI7tcl5WvR+8qy1rU=
424 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
425 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
426 | github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
427 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
428 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
429 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
430 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
431 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
432 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
433 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
434 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
435 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
436 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
437 | github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
438 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
439 | github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
440 | github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
441 | github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
442 | github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
443 | github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
444 | github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=
445 | github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
446 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
447 | github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
448 | github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
449 | github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
450 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
451 | github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
452 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
453 | github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
454 | github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
455 | go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
456 | go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
457 | go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
458 | go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
459 | go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
460 | go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
461 | go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
462 | go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
463 | go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
464 | go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
465 | go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
466 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
467 | go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
468 | go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
469 | go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
470 | go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
471 | go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
472 | go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
473 | go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
474 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
475 | go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
476 | go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
477 | go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
478 | go.uber.org/multierr v1.8.0 h1:dg6GjLku4EH+249NNmoIciG9N/jURbDG+pFlTkhzIC8=
479 | go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak=
480 | go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
481 | go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
482 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
483 | go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
484 | go.uber.org/zap v1.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8=
485 | go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw=
486 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
487 | golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
488 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
489 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
490 | golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
491 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
492 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
493 | golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
494 | golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
495 | golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
496 | golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
497 | golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
498 | golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
499 | golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
500 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
501 | golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
502 | golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
503 | golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY=
504 | golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
505 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
506 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
507 | golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
508 | golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
509 | golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
510 | golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
511 | golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
512 | golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
513 | golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
514 | golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
515 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
516 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
517 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
518 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
519 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
520 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
521 | golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
522 | golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
523 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
524 | golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
525 | golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
526 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
527 | golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
528 | golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
529 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
530 | golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
531 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
532 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
533 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
534 | golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
535 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
536 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
537 | golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
538 | golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
539 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
540 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
541 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
542 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
543 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
544 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
545 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
546 | golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
547 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
548 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
549 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
550 | golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
551 | golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
552 | golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
553 | golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
554 | golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
555 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
556 | golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
557 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
558 | golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
559 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
560 | golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
561 | golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
562 | golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
563 | golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
564 | golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
565 | golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
566 | golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
567 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
568 | golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
569 | golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
570 | golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
571 | golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
572 | golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
573 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
574 | golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
575 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
576 | golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
577 | golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
578 | golang.org/x/net v0.0.0-20211020060615-d418f374d309/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
579 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
580 | golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
581 | golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
582 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
583 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
584 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
585 | golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
586 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
587 | golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
588 | golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
589 | golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
590 | golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
591 | golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
592 | golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
593 | golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
594 | golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
595 | golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
596 | golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
597 | golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
598 | golang.org/x/oauth2 v0.7.0 h1:qe6s0zUXlPX80/dITx3440hWZ7GwMwgDDyrSGTPJG/g=
599 | golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4=
600 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
601 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
602 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
603 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
604 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
605 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
606 | golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
607 | golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
608 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
609 | golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
610 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
611 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
612 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
613 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
614 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
615 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
616 | golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
617 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
618 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
619 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
620 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
621 | golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
622 | golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
623 | golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
624 | golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
625 | golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
626 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
627 | golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
628 | golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
629 | golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
630 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
631 | golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
632 | golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
633 | golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
634 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
635 | golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
636 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
637 | golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
638 | golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
639 | golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
640 | golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
641 | golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
642 | golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
643 | golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
644 | golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
645 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
646 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
647 | golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
648 | golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
649 | golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
650 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
651 | golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
652 | golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
653 | golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
654 | golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
655 | golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
656 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
657 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
658 | golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
659 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
660 | golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
661 | golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
662 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
663 | golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
664 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
665 | golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
666 | golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
667 | golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
668 | golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
669 | golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
670 | golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
671 | golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
672 | golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
673 | golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
674 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
675 | golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
676 | golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
677 | golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
678 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
679 | golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
680 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
681 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
682 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
683 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
684 | golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
685 | golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
686 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
687 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
688 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
689 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
690 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
691 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
692 | golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
693 | golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
694 | golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
695 | golang.org/x/time v0.0.0-20220224211638-0e9765cccd65 h1:M73Iuj3xbbb9Uk1DYhzydthsj6oOd6l9bpuFcNoUvTs=
696 | golang.org/x/time v0.0.0-20220224211638-0e9765cccd65/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
697 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
698 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
699 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
700 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
701 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
702 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
703 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
704 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
705 | golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
706 | golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
707 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
708 | golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
709 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
710 | golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
711 | golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
712 | golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
713 | golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
714 | golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
715 | golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
716 | golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
717 | golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
718 | golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
719 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
720 | golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
721 | golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
722 | golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
723 | golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
724 | golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
725 | golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
726 | golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
727 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
728 | golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
729 | golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
730 | golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
731 | golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
732 | golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
733 | golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
734 | golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
735 | golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
736 | golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
737 | golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
738 | golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
739 | golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
740 | golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
741 | golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
742 | golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
743 | golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
744 | golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
745 | golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
746 | golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
747 | golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
748 | golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
749 | golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
750 | golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
751 | golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
752 | golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
753 | golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
754 | golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
755 | golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
756 | golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
757 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
758 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
759 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
760 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
761 | google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
762 | google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
763 | google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
764 | google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
765 | google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
766 | google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
767 | google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
768 | google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
769 | google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
770 | google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
771 | google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
772 | google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
773 | google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
774 | google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
775 | google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
776 | google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
777 | google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
778 | google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
779 | google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
780 | google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU=
781 | google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94=
782 | google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo=
783 | google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4=
784 | google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw=
785 | google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU=
786 | google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k=
787 | google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE=
788 | google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE=
789 | google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI=
790 | google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I=
791 | google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo=
792 | google.golang.org/api v0.66.0/go.mod h1:I1dmXYpX7HGwz/ejRxwQp2qj5bFAz93HiCU1C1oYd9M=
793 | google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g=
794 | google.golang.org/api v0.68.0/go.mod h1:sOM8pTpwgflXRhz+oC8H2Dr+UcbMqkPPWNJo88Q7TH8=
795 | google.golang.org/api v0.114.0 h1:1xQPji6cO2E2vLiI+C/XiFAnsn1WV3mjaEwGLhi3grE=
796 | google.golang.org/api v0.114.0/go.mod h1:ifYI2ZsFK6/uGddGfAD5BMxlnkBqCmqHSDUVi45N5Yg=
797 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
798 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
799 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
800 | google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
801 | google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
802 | google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
803 | google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
804 | google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
805 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
806 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
807 | google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
808 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
809 | google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
810 | google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
811 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
812 | google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
813 | google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
814 | google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
815 | google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
816 | google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
817 | google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
818 | google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
819 | google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
820 | google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
821 | google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
822 | google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
823 | google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
824 | google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
825 | google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
826 | google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
827 | google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
828 | google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
829 | google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
830 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
831 | google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
832 | google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
833 | google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
834 | google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
835 | google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
836 | google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
837 | google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
838 | google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
839 | google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
840 | google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
841 | google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
842 | google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
843 | google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
844 | google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
845 | google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
846 | google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
847 | google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A=
848 | google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
849 | google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
850 | google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
851 | google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24=
852 | google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=
853 | google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=
854 | google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
855 | google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
856 | google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w=
857 | google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
858 | google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
859 | google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
860 | google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
861 | google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
862 | google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
863 | google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
864 | google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
865 | google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
866 | google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
867 | google.golang.org/genproto v0.0.0-20220114231437-d2e6a121cae0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
868 | google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
869 | google.golang.org/genproto v0.0.0-20220201184016-50beb8ab5c44/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
870 | google.golang.org/genproto v0.0.0-20220204002441-d6cc3cc0770e/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
871 | google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
872 | google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A=
873 | google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU=
874 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
875 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
876 | google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
877 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
878 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
879 | google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
880 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
881 | google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
882 | google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
883 | google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
884 | google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
885 | google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
886 | google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
887 | google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
888 | google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
889 | google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
890 | google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
891 | google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
892 | google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
893 | google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
894 | google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
895 | google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
896 | google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
897 | google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
898 | google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
899 | google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
900 | google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
901 | google.golang.org/grpc v1.56.3 h1:8I4C0Yq1EjstUzUJzpcRVbuYA2mODtEmpWiQoN/b2nc=
902 | google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s=
903 | google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
904 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
905 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
906 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
907 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
908 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
909 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
910 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
911 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
912 | google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
913 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
914 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
915 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
916 | google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
917 | google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
918 | google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
919 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
920 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
921 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
922 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
923 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
924 | gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
925 | gopkg.in/ini.v1 v1.66.4 h1:SsAcf+mM7mRZo2nJNGt8mZCjG8ZRaNGMURJw7BsIST4=
926 | gopkg.in/ini.v1 v1.66.4/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
927 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
928 | gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
929 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
930 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
931 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
932 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
933 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
934 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
935 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
936 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
937 | gorm.io/driver/postgres v1.3.1 h1:Pyv+gg1Gq1IgsLYytj/S2k7ebII3CzEdpqQkPOdH24g=
938 | gorm.io/driver/postgres v1.3.1/go.mod h1:WwvWOuR9unCLpGWCL6Y3JOeBWvbKi6JLhayiVclSZZU=
939 | gorm.io/gorm v1.23.1/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
940 | gorm.io/gorm v1.23.3 h1:jYh3nm7uLZkrMVfA8WVNjDZryKfr7W+HTlInVgKFJAg=
941 | gorm.io/gorm v1.23.3/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
942 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
943 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
944 | honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
945 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
946 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
947 | honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
948 | honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
949 | moul.io/http2curl v1.0.0 h1:6XwpyZOYsgZJrU8exnG87ncVkU1FVCcTRpwzOkTDUi8=
950 | moul.io/http2curl v1.0.0/go.mod h1:f6cULg+e4Md/oW1cYmwW4IWQOVl2lGbmCNGOHvzX2kE=
951 | rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
952 | rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
953 | rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
954 |
--------------------------------------------------------------------------------
/influxdb.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "log"
6 | "time"
7 |
8 | influxdb2 "github.com/influxdata/influxdb-client-go/v2"
9 | influxdb2write "github.com/influxdata/influxdb-client-go/v2/api/write"
10 | )
11 |
12 | // InfluxdbAsyncBatchSize influxdb asynchronize batch size
13 | const InfluxdbAsyncBatchSize = 20
14 |
15 | // InfluxdbClient is a client to connect to influxdb in influx cloud
16 | type InfluxdbClient struct {
17 | Bucket string
18 | Organization string
19 | AccessToken string
20 | InfluxCloudURL string
21 | Client influxdb2.Client
22 | }
23 |
24 | // NewInfluxdbClient Create a new InfluxClient
25 | func NewInfluxdbClient(config InfluxdbConfig) *InfluxdbClient {
26 | c := new(InfluxdbClient)
27 | c.Bucket = config.Bucket
28 | c.AccessToken = config.AccessToken
29 | c.InfluxCloudURL = config.InfluxdbURL
30 | c.Client = influxdb2.NewClientWithOptions(c.InfluxCloudURL, c.AccessToken,
31 | influxdb2.DefaultOptions().SetBatchSize(InfluxdbAsyncBatchSize))
32 | return c
33 | }
34 |
35 | // PrepareInfluxdbData prepare influxdb datapoint form PingResult
36 | func (i *InfluxdbClient) PrepareInfluxdbData(r PingResult) *influxdb2write.Point {
37 | return influxdb2.NewPoint(r.Cluster,
38 | map[string]string{},
39 | map[string]interface{}{
40 | "hostname": r.Hostname,
41 | "compute_unit_price": int64(r.ComputeUnitPrice),
42 | "request_compute_unit": int64(r.RequestComputeUnits),
43 | "submit": r.Submitted,
44 | "confirmed": r.Confirmed,
45 | "loss": r.Loss,
46 | "max": r.Max,
47 | "min": r.Min,
48 | "mean": r.Mean,
49 | "stddev": r.Stddev,
50 | "take_time": r.TakeTime,
51 | "error": r.Error,
52 | },
53 | time.Now())
54 | }
55 |
56 | // SendDatapointAsync send datapoint to influx cloud
57 | func (i *InfluxdbClient) SendDatapointAsync(p *influxdb2write.Point) {
58 | if i.Client == nil {
59 | log.Println("ERROR! InfluxClient has not initiated!")
60 | return
61 | }
62 | go func() {
63 | writeAPI := i.Client.WriteAPIBlocking(i.Organization, i.Bucket)
64 | err := writeAPI.WritePoint(context.Background(), p)
65 | if err != nil {
66 | log.Println("Influxdb write point ERROR! ", err)
67 | }
68 | writeAPI.Flush(context.Background())
69 | }()
70 | }
71 |
72 | // ClientClose close Client connection
73 | func (i *InfluxdbClient) ClientClose() {
74 | i.Client.Close()
75 | }
76 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "flag"
5 | "log"
6 | "strings"
7 | "time"
8 |
9 | _ "github.com/GoogleCloudPlatform/cloudsql-proxy/proxy/dialers/postgres"
10 | "github.com/blocto/solana-go-sdk/rpc"
11 | "gorm.io/driver/postgres"
12 | "gorm.io/gorm"
13 | )
14 |
15 | var config Config
16 |
17 | // Cluster enum
18 | type Cluster string
19 |
20 | var database *gorm.DB
21 |
22 | const useGCloudDB = true
23 |
24 | type ClustersToRun string
25 |
26 | // Cluster enum
27 | const (
28 | MainnetBeta Cluster = "MainnetBeta"
29 | Testnet = "Testnet"
30 | Devnet = "Devnet"
31 | )
32 |
33 | var influxdb *InfluxdbClient
34 | var userInputClusterMode string
35 | var mainnetFailover RPCFailover
36 | var testnetFailover RPCFailover
37 | var devnetFailover RPCFailover
38 |
39 | const (
40 | RunMainnetBeta ClustersToRun = "mainnet"
41 | RunTestnet = "testnet"
42 | RunDevnet = "devnet"
43 | RunAllClusters = "all"
44 | )
45 |
46 | func init() {
47 | config = loadConfig()
48 | log.Println(" *** Config Start *** ")
49 | log.Println("--- //// Database Config --- ")
50 | log.Println(config.Database)
51 | log.Println("--- //// Influxdb Config --- ")
52 | log.Println(config.InfluxdbConfig)
53 | log.Println("--- //// Retension --- ")
54 | log.Println(config.Retension)
55 | log.Println("--- //// ClusterCLIConfig--- ")
56 | log.Println("ClusterCLIConfig Mainnet", config.ClusterCLIConfig.ConfigMain)
57 | log.Println("ClusterCLIConfig Testnet", config.ClusterCLIConfig.ConfigTestnet)
58 | log.Println("ClusterCLIConfig Devnet", config.ClusterCLIConfig.ConfigDevnet)
59 | log.Println("--- Mainnet Ping --- ")
60 | log.Println("Mainnet.ClusterPing.APIServer", config.Mainnet.ClusterPing.APIServer)
61 | log.Println("Mainnet.ClusterPing.PingServiceEnabled", config.Mainnet.ClusterPing.PingServiceEnabled)
62 | log.Println("Mainnet.ClusterPing.AlternativeEnpoint.HostList", config.Mainnet.ClusterPing.AlternativeEnpoint.HostList)
63 | log.Println("Mainnet.ClusterPing.PingConfig", config.Mainnet.ClusterPing.PingConfig)
64 | log.Println("Mainnet.ClusterPing.Report", config.Mainnet.ClusterPing.Report)
65 | log.Println("--- Testnet Ping --- ")
66 | log.Println("Mainnet.ClusterPing.APIServer", config.Testnet.ClusterPing.APIServer)
67 | log.Println("Mainnet.ClusterPing.PingServiceEnabled", config.Mainnet.ClusterPing.PingServiceEnabled)
68 | log.Println("Testnet.ClusterPing.AlternativeEnpoint.HostList", config.Testnet.ClusterPing.AlternativeEnpoint.HostList)
69 | log.Println("Testnet.ClusterPing.PingConfig", config.Testnet.ClusterPing.PingConfig)
70 | log.Println("Testnet.ClusterPing.Report", config.Testnet.ClusterPing.Report)
71 | log.Println("--- Devnet Ping --- ")
72 | log.Println("Devnet.ClusterPing.APIServer", config.Devnet.ClusterPing.APIServer)
73 | log.Println("Devnet.ClusterPing.Enabled", config.Devnet.ClusterPing.PingServiceEnabled)
74 | log.Println("Devnet.ClusterPing.AlternativeEnpoint.HostList", config.Devnet.ClusterPing.AlternativeEnpoint.HostList)
75 | log.Println("Devnet.ClusterPing.PingConfig", config.Devnet.ClusterPing.PingConfig)
76 | log.Println("Devnet.ClusterPing.Report", config.Devnet.ClusterPing.Report)
77 |
78 | log.Println(" *** Config End *** ")
79 |
80 | ResponseErrIdentifierInit()
81 | StatisticErrExpectionInit()
82 | AlertErrExpectionInit()
83 | ReportErrExpectionInit()
84 | PingTakeTimeErrExpectionInit()
85 |
86 | if config.Database.UseGoogleCloud {
87 | gormDB, err := gorm.Open(postgres.New(postgres.Config{
88 | DriverName: "cloudsqlpostgres",
89 | DSN: config.DBConn,
90 | }))
91 | if err != nil {
92 | log.Panic(err)
93 | }
94 | database = gormDB
95 | } else {
96 | gormDB, err := gorm.Open(postgres.Open(config.DBConn), &gorm.Config{})
97 | if err != nil {
98 | log.Panic(err)
99 | }
100 | database = gormDB
101 | }
102 | log.Println("database connected")
103 | if config.InfluxdbConfig.Enabled {
104 | influxdb = NewInfluxdbClient(config.InfluxdbConfig)
105 | }
106 | /// ---- Start RPC Failover ---
107 | log.Println("RPC Endpoint Failover Setting ---")
108 | if len(config.Mainnet.AlternativeEnpoint.HostList) <= 0 {
109 | mainnetFailover = NewRPCFailover([]RPCEndpoint{{
110 | Endpoint: rpc.MainnetRPCEndpoint,
111 | Piority: 1,
112 | MaxRetry: 30}})
113 | } else {
114 | mainnetFailover = NewRPCFailover(config.Mainnet.AlternativeEnpoint.HostList)
115 | }
116 | if len(config.Testnet.AlternativeEnpoint.HostList) <= 0 {
117 | testnetFailover = NewRPCFailover([]RPCEndpoint{{
118 | Endpoint: rpc.MainnetRPCEndpoint,
119 | Piority: 1,
120 | MaxRetry: 30}})
121 | } else {
122 | testnetFailover = NewRPCFailover(config.Testnet.AlternativeEnpoint.HostList)
123 | }
124 | if len(config.Devnet.AlternativeEnpoint.HostList) <= 0 {
125 | devnetFailover = NewRPCFailover([]RPCEndpoint{{
126 | Endpoint: rpc.MainnetRPCEndpoint,
127 | Piority: 1,
128 | MaxRetry: 30}})
129 | } else {
130 | devnetFailover = NewRPCFailover(config.Devnet.AlternativeEnpoint.HostList)
131 | }
132 | }
133 |
134 | func main() {
135 | defer func() {
136 | if influxdb != nil {
137 | influxdb.ClientClose()
138 | }
139 | if database != nil {
140 | sqldb, err := database.DB()
141 | if err == nil {
142 | sqldb.Close()
143 | }
144 | }
145 | }()
146 | flag.Parse()
147 | clustersToRun := flag.Arg(0)
148 | if !(strings.Compare(clustersToRun, string(RunMainnetBeta)) == 0 ||
149 | strings.Compare(clustersToRun, string(RunTestnet)) == 0 ||
150 | strings.Compare(clustersToRun, string(RunDevnet)) == 0 ||
151 | strings.Compare(clustersToRun, string(RunAllClusters)) == 0) {
152 | go launchWorkers(RunMainnetBeta)
153 | go APIService(RunMainnetBeta)
154 | } else {
155 | go launchWorkers(ClustersToRun(clustersToRun))
156 | go APIService(ClustersToRun(clustersToRun))
157 | }
158 |
159 | for {
160 | time.Sleep(10 * time.Second)
161 | }
162 | }
163 |
--------------------------------------------------------------------------------
/output.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "strings"
6 | "time"
7 | )
8 |
9 | // ReportPingResultJSON is a struct convert from PingResult to desire json output struct
10 | type DataPoint1MinResultJSON struct {
11 | Submitted int `json:"submitted"`
12 | Confirmed int `json:"confirmed"`
13 | Loss string `json:"loss"`
14 | Mean int `json:"mean_ms"`
15 | TimeStamp string `json:"ts"`
16 | ErrorCount int `json:"error_count"`
17 | Error string `json:"error"`
18 | }
19 |
20 | // SlackText slack structure
21 | type SlackText struct {
22 | SText string `json:"text"`
23 | SType string `json:"type"`
24 | }
25 |
26 | // Block slack structure
27 | type Block struct {
28 | BlockType string `json:"type"`
29 | BlockText SlackText `json:"text"`
30 | }
31 |
32 | // SlackPayload slack structure
33 | type SlackPayload struct {
34 | Blocks []Block `json:"blocks"`
35 | }
36 |
37 | // DiscordPayload slack structure
38 | type DiscordPayload struct {
39 | BotName string `json:"username"`
40 | BotAvatarURL string `json:"avatar_url"`
41 | Content string `json:"content"`
42 | }
43 |
44 | func ErrorsToString(errs []string) (errsString string) {
45 | for i, e := range errs {
46 | if i == 0 {
47 | errsString = e
48 | } else {
49 | errsString = errsString + ";" + e
50 | }
51 | }
52 | return
53 | }
54 |
55 | func To1MinWindowJson(r *PingResult) DataPoint1MinResultJSON {
56 | // Check result
57 | jsonResult := DataPoint1MinResultJSON{Submitted: r.Submitted, Confirmed: r.Confirmed, Mean: int(r.Mean), Error: ErrorsToString(r.Error)}
58 | loss := fmt.Sprintf("%3.1f%s", r.Loss, "%")
59 | jsonResult.Loss = loss
60 | ts := time.Unix(r.TimeStamp, 0)
61 | jsonResult.TimeStamp = ts.Format(time.RFC3339)
62 | return jsonResult
63 | }
64 |
65 | // PingResultToJson convert PingSatistic to RingResult Json format for API
66 | func PingResultToJson(stat *PingSatistic) DataPoint1MinResultJSON {
67 | _, mean, _, _, _ := stat.TimeMeasure.Statistic()
68 | var errorShow string
69 | loss := fmt.Sprintf("%3.1f%s", stat.Loss*100, "%")
70 | if stat.Count == 0 {
71 | errorShow = "No Data"
72 | loss = fmt.Sprintf("%3.1f%s", float64(0), "%")
73 | }
74 |
75 | jsonResult := DataPoint1MinResultJSON{Submitted: int(stat.Submitted), Confirmed: int(stat.Confirmed), Mean: int(mean), ErrorCount: int(len(stat.Errors)), Error: errorShow}
76 |
77 | jsonResult.Loss = loss
78 | ts := time.Unix(stat.TimeStamp, 0).UTC()
79 | jsonResult.TimeStamp = ts.Format(time.RFC3339)
80 | return jsonResult
81 | }
82 |
83 | // ReportPayload get the report within specified minutes
84 | func (s *SlackPayload) ReportPayload(c Cluster, data *GroupsAllStatistic, globalSatistic GlobalStatistic, hideKeywords []string, messageMemo string) {
85 | // Header Block
86 | headerText := fmt.Sprintf("total-submitted: %3.0f, total-confirmed:%3.0f, average-loss:%3.1f%s\n memo:%s",
87 | globalSatistic.Submitted,
88 | globalSatistic.Confirmed,
89 | globalSatistic.Loss*100, "%",
90 | messageMemo)
91 | header := Block{
92 | BlockType: "section",
93 | BlockText: SlackText{
94 | SType: "mrkdwn",
95 | SText: headerText,
96 | },
97 | }
98 | s.Blocks = append(s.Blocks, header)
99 | // BodyBlock
100 | body := Block{}
101 | records := reportRecordBlock(data)
102 | description := "( Submitted, Confirmed, Loss, min/mean/max/stddev ms )"
103 | //memo := "* rpc error : context deadline exceeded does not count as a transaction\n** BlockhashNotFound error does not show in Error List"
104 | memo := "*BlockhashNotFound do not count as a transaction\n"
105 | errorRecords := reportErrorBlock(data, hideKeywords)
106 | body = Block{
107 | BlockType: "section",
108 | BlockText: SlackText{SType: "mrkdwn", SText: fmt.Sprintf("```%s\n%s\n%s\n%s```", description, records, memo, errorRecords)},
109 | }
110 | s.Blocks = append(s.Blocks, body)
111 | }
112 |
113 | func (s *SlackPayload) AlertPayload(conf ClusterConfig, gStat *GlobalStatistic, errorStistic map[string]int, thresholdAdj float64, hideKeywords []string, messageMemo string) {
114 | var text, timeStatis string
115 | if gStat.TimeStatistic.Stddev <= 0 {
116 | timeStatis = fmt.Sprintf(" %d/%3.0f/%d/%s ", gStat.TimeStatistic.Min, gStat.TimeStatistic.Mean, gStat.TimeStatistic.Max, "NaN")
117 | } else {
118 | timeStatis = fmt.Sprintf(" %d/%3.0f/%d/%3.0f ", gStat.TimeStatistic.Min, gStat.TimeStatistic.Mean, gStat.TimeStatistic.Max, gStat.TimeStatistic.Stddev)
119 | }
120 | errsorStatis := ""
121 | for k, v := range errorStistic {
122 | if !PingResultError(k).IsInErrorList(AlertErrorExceptionList) {
123 | errsorStatis = fmt.Sprintf("%s%s(%d)", errsorStatis, PingResultError(k).Short(), v)
124 | }
125 | }
126 | for _, w := range hideKeywords {
127 | errsorStatis = strings.ReplaceAll(errsorStatis, w, "")
128 | }
129 |
130 | text = fmt.Sprintf("{ hostname: %s, memo: %s ,submitted: %3.0f, confirmed:%3.0f, loss: %3.1f%s, confirmation: min/mean/max/stddev = %s, next_threshold:%3.0f%s, error: %s}",
131 | conf.HostName, messageMemo, gStat.Submitted, gStat.Confirmed, gStat.Loss*100, "%", timeStatis, thresholdAdj, "%", errsorStatis)
132 |
133 | header := Block{
134 | BlockType: "section",
135 | BlockText: SlackText{
136 | SType: "mrkdwn",
137 | SText: text,
138 | },
139 | }
140 | s.Blocks = append(s.Blocks, header)
141 | }
142 |
143 | func (s *SlackPayload) FailoverAlertPayload(conf ClusterConfig, endpoint FailoverEndpoint, workerNum int) {
144 | text := fmt.Sprintf("{ hostname: %s, cluster:%s, worker:%d, msg:%s}",
145 | conf.HostName, conf.Cluster, workerNum,
146 | fmt.Sprintf("failover to %s", endpoint.Endpoint))
147 |
148 | header := Block{
149 | BlockType: "section",
150 | BlockText: SlackText{
151 | SType: "mrkdwn",
152 | SText: text,
153 | },
154 | }
155 | s.Blocks = append(s.Blocks, header)
156 | }
157 |
158 | func reportRecordBlock(data *GroupsAllStatistic) string {
159 | text := ""
160 | timeStatis := ""
161 | for _, ps := range data.PingStatisticList {
162 | if ps.TimeStatistic.Stddev <= 0 {
163 | timeStatis = fmt.Sprintf(" %d/%3.0f/%d/%s ", ps.TimeStatistic.Min, ps.TimeStatistic.Mean, ps.TimeStatistic.Max, "NaN")
164 | } else {
165 | timeStatis = fmt.Sprintf(" %d/%3.0f/%d/%3.0f ", ps.TimeStatistic.Min, ps.TimeStatistic.Mean, ps.TimeStatistic.Max, ps.TimeStatistic.Stddev)
166 | }
167 | lossPercentage := ps.Loss * 100
168 | if ps.Count > 0 {
169 | text = fmt.Sprintf("%s( %3.0f, %3.0f, %3.1f%s, %s )\n", text, ps.Submitted, ps.Confirmed, lossPercentage, "%", timeStatis)
170 | }
171 | }
172 | return text
173 | }
174 |
175 | // ReportPayload get the report within specified minutes
176 | func (s *DiscordPayload) ReportPayload(c Cluster, data *GroupsAllStatistic, globalSatistic GlobalStatistic, hideKeywords []string, messageMemo string) {
177 | summary := fmt.Sprintf("**total-submitted: %3.0f total-confirmed: %3.0f average-loss: %3.1f%s**\nmemo: %s",
178 | globalSatistic.Submitted,
179 | globalSatistic.Confirmed,
180 | globalSatistic.Loss*100, "%",
181 | messageMemo)
182 | header := "( Submitted, Confirmed, Loss, min/mean/max/stddev ms )"
183 | records := reportRecordBlock(data)
184 | memo := "*BlockhashNotFound do not count as a transaction\n"
185 | errorRecords := reportErrorBlock(data, hideKeywords)
186 | s.Content = fmt.Sprintf("%s\n```%s\n%s\n%s\n%s```", summary, header, records, memo, errorRecords)
187 | }
188 |
189 | // AlertPayload get the report within specified minutes
190 | func (s *DiscordPayload) AlertPayload(conf ClusterConfig, gStat *GlobalStatistic, errorStistic map[string]int, thresholdAdj float64, hideKeywords []string, messageMemo string) {
191 | var timeStatis string
192 | if gStat.TimeStatistic.Stddev <= 0 {
193 | timeStatis = fmt.Sprintf(" %d/%3.0f/%d/%s ", gStat.TimeStatistic.Min, gStat.TimeStatistic.Mean, gStat.TimeStatistic.Max, "NaN")
194 | } else {
195 | timeStatis = fmt.Sprintf(" %d/%3.0f/%d/%3.0f ", gStat.TimeStatistic.Min, gStat.TimeStatistic.Mean, gStat.TimeStatistic.Max, gStat.TimeStatistic.Stddev)
196 | }
197 | errsorStatis := ""
198 | for k, v := range errorStistic {
199 | if !PingResultError(k).IsInErrorList(AlertErrorExceptionList) {
200 | errsorStatis = fmt.Sprintf("%s%s(%d)", errsorStatis, PingResultError(k).Short(), v)
201 | }
202 | }
203 | for _, w := range hideKeywords {
204 | errsorStatis = strings.ReplaceAll(errsorStatis, w, "")
205 | }
206 |
207 | text := fmt.Sprintf("```{ hostname: %s, memo: %s, submitted: %3.0f, confirmed:%3.0f, loss: %3.1f%s, confirmation: min/mean/max/stddev = %s, next_threshold:%3.0f%s, error: %s}```",
208 | conf.HostName, messageMemo, gStat.Submitted, gStat.Confirmed, gStat.Loss*100, "%", timeStatis, thresholdAdj, "%", errsorStatis)
209 | s.Content = text
210 | }
211 |
212 | // FailoverAlertPayload get the report within specified minutes
213 | func (s *DiscordPayload) FailoverAlertPayload(conf ClusterConfig, endpoint FailoverEndpoint, workerNum int) {
214 | s.Content = fmt.Sprintf("```{ hostname: %s, cluster:%s, worker:%d, msg:%s}```",
215 | conf.HostName, conf.Cluster, workerNum,
216 | fmt.Sprintf("failover to %s", endpoint.Endpoint))
217 |
218 | }
219 | func reportErrorBlock(data *GroupsAllStatistic, hideKeywords []string) string {
220 | var exceededText, errorText, blackHashText string
221 | if len(data.GlobalErrorStatistic) == 0 {
222 | return ""
223 | }
224 | for k, v := range data.GlobalErrorStatistic {
225 |
226 | if RPCServerDeadlineExceeded.IsIdentical(PingResultError(k)) {
227 | exceededText = fmt.Sprintf("*(count:%d) RPC Server context deadline exceed\n", v)
228 | } else if BlockhashNotFound.IsIdentical(PingResultError(k)) {
229 | blackHashText = fmt.Sprintf("*(count:%d) BlockhashNotFound\n", v)
230 | } else {
231 | errorText = fmt.Sprintf("%s\n(count: %d) %s\n", errorText, v, k)
232 | }
233 | }
234 | for _, w := range hideKeywords {
235 | errorText = strings.ReplaceAll(errorText, w, "")
236 | }
237 | if len(data.GlobalErrorStatistic) > 0 {
238 | return fmt.Sprintf("Error List:\n%s%s%s", exceededText, blackHashText, errorText)
239 | }
240 | return ""
241 | }
242 |
243 | func reportRawErrorBlock(data *GroupsAllStatistic) string {
244 | var errorText string
245 | if len(data.GlobalErrorStatistic) == 0 {
246 | return ""
247 | }
248 | for k, v := range data.GlobalErrorStatistic {
249 | errorText = fmt.Sprintf("%s\n(count: %d) %s", errorText, v, k)
250 | }
251 | return fmt.Sprintf("Error List:%s", errorText)
252 | }
253 |
--------------------------------------------------------------------------------
/ping_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "testing"
6 | "time"
7 | )
8 |
9 | var sch1 = PingResult{
10 | TimeStamp: time.Now().UTC().Unix(),
11 | Cluster: "Devnet",
12 | Hostname: "solana-ping-api",
13 | PingType: "report",
14 | Submitted: 10,
15 | Confirmed: 9,
16 | Max: 12000,
17 | Mean: 8000,
18 | Min: 500,
19 | Stddev: 100,
20 | TakeTime: 90,
21 | Error: []string{},
22 | }
23 |
24 | var hook = "https://hooks.slack.com/services/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"
25 |
26 | func TestIsIdentical(t *testing.T) {
27 | su503 := PingResultError(ServiceUnavilable503Text)
28 | blackhash := PingResultError(BlockhashNotFoundText)
29 | if !ServiceUnavilable503.IsIdentical(su503) {
30 | t.Fatal("ServiceUnavilable503 should be true")
31 | }
32 | if BlockhashNotFound.IsIdentical(su503) {
33 | t.Fatal("BlockhashNotFound should be false")
34 | }
35 | if !blackhash.IsInErrorList(StatisticErrorExceptionList) {
36 | t.Fatal("blackhash should be in the list")
37 | }
38 | if su503.IsInErrorList(StatisticErrorExceptionList) {
39 | t.Fatal("503 should not be in the list")
40 | }
41 | if !(su503.Short() == ServiceUnavilable503.Short) {
42 | t.Fatal("503 short is not correct")
43 | } else {
44 | fmt.Println(su503.Short())
45 | }
46 | }
47 |
48 | // func TestParse(t *testing.T) {
49 | // pings := []PingResult{sch1}
50 | // avg := generateStatisticData(pings)
51 | // payload := SlackPayload{}
52 | // payload.ToReportPayload(MainnetBeta, pings, avg)
53 | // err := SlackSend(hook, &payload)
54 | // if err != nil {
55 | // log.Print(err)
56 | // }
57 |
58 | // }
59 |
60 | /*
61 | func TestConfig(t *testing.T) {
62 | config := loadConfig()
63 | log.Println(config.Clusters)
64 | log.Println(config.ServerIP)
65 | log.Println(config.SolanaConfig)
66 | log.Println(config.SolanaPing)
67 | log.Println(config.Slack)
68 | log.Println(config.Cleaner)
69 | }
70 | */
71 |
72 | /*
73 | func TestSlack(t *testing.T) {
74 |
75 | payload := SlackPayload{}
76 |
77 | payload.GetReportPayload(Devnet)
78 |
79 | errs := SlackSend(config.Slack.WebHook, &payload)
80 | if errs != nil {
81 | t.Error(errs)
82 | }
83 |
84 | }
85 | */
86 | var output = `Source Account: 8Juu8gXPHCKougXvkg3ZY8HdJcBdqFch7hHKjKFNWTFV
87 |
88 | msg BD5heio2zqXdNuN1i7hia6dKbLVq7RvYt9HanYdj8nWD
89 | ✅ 1 lamport(s) transferred: seq=0 time= 773ms signature=2twfwLjCj7eLxXPDkyfPqJFP66Jb3Npkb6nTVeJXXZC5rThYp9Mn67SX8Pk8km5SAyQwYdasrK7cUP4JJN2BdXYr
90 | msg BD5heio2zqXdNuN1i7hia6dKbLVq7RvYt9HanYdj8nWD
91 | ✅ 1 lamport(s) transferred: seq=1 time=1106ms signature=Nbyz2bzmb2FNqdmJoP18KoHeNE8ByvCHh2D4tiDsWxn7Z5BoWXsA5iGrhgjbqRDe5FQy5gafHfcaXw73c4spYod
92 | msg BD5heio2zqXdNuN1i7hia6dKbLVq7RvYt9HanYdj8nWD
93 | ✅ 1 lamport(s) transferred: seq=2 time= 773ms signature=2JXmQNaLW2FM3w9uMmTJqRZiHJkrZay1e6B7ti9mAWebgDWqCek6YoHmrLoLu7wNqSLuHSmgjpKv5ERoq4HYyMMz
94 |
95 | --- transaction statistics ---
96 | 3 transactions submitted, 3 transactions confirmed, 0.0% transaction loss
97 | confirmation min/mean/max/stddev = 773/884/1106/192 ms
98 | `
99 |
--------------------------------------------------------------------------------
/report-post.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "log"
7 |
8 | "github.com/parnurzeal/gorequest"
9 | )
10 |
11 | // SlackSend send the payload to then Slack Channel
12 | func SlackSend(webhookURL string, payload *SlackPayload) []error {
13 | data, err := json.Marshal(*payload)
14 | if err != nil {
15 | return []error{fmt.Errorf("marshal payload error. Status:%s", err.Error())}
16 | }
17 | request := gorequest.New()
18 | resp, _, errs := request.Post(webhookURL).Send(string(data)).End()
19 |
20 | if errs != nil {
21 | return errs
22 | }
23 | if resp.StatusCode >= 400 {
24 | log.Println(resp.StatusCode, " payload:", string(data))
25 | return []error{fmt.Errorf("slack sending msg. Status: %v", resp.Status)}
26 | }
27 |
28 | log.Println("Slack send successfully! =>", webhookURL)
29 | return nil
30 | }
31 |
32 | // DiscordSend send the payload to discord channel by webhook
33 | func DiscordSend(webhookURL string, payload *DiscordPayload) []error {
34 | data, err := json.Marshal(*payload)
35 | if err != nil {
36 | return []error{fmt.Errorf("marshal payload error. Status:%s", err.Error())}
37 | }
38 | request := gorequest.New()
39 | resp, _, errs := request.Post(webhookURL).Send(string(data)).End()
40 |
41 | if errs != nil {
42 | return errs
43 | }
44 | if resp.StatusCode >= 400 {
45 | log.Println(resp.StatusCode, " payload:", string(data))
46 | return []error{fmt.Errorf("Discord sending msg. Status: %v", resp.Status)}
47 | }
48 | log.Println("Discord send successfully! =>", webhookURL)
49 | return nil
50 | }
51 |
--------------------------------------------------------------------------------
/rpcEndpoint.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "sort"
5 | "sync"
6 | )
7 |
8 | type RpcEndpointPool []RpcEndpoint
9 |
10 | var RetryMutex sync.Mutex
11 |
12 | type RpcEndpoint struct {
13 | Piority int
14 | Host string
15 | Retry int
16 | }
17 |
18 | func (r RpcEndpointPool) Len() int { return len(r) }
19 | func (r RpcEndpointPool) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
20 | func (r RpcEndpointPool) Less(i, j int) bool { return r[i].Piority < r[j].Piority }
21 |
22 | func sortEndpoint(endpoints []RpcEndpoint) {
23 | sort.Sort(RpcEndpointPool(endpoints))
24 | }
25 |
26 | func (r *RpcEndpoint) GoNext(max_retry int) bool {
27 | if r.Retry < max_retry {
28 | return false
29 | }
30 | return true
31 | }
32 |
33 | func (r *RpcEndpoint) AddRetry() {
34 | RetryMutex.Lock()
35 | defer RetryMutex.Unlock()
36 | r.Retry += 1
37 |
38 | }
39 |
40 | func (r *RpcEndpoint) ResetRetry() {
41 | RetryMutex.Lock()
42 | defer RetryMutex.Unlock()
43 | r.Retry = 0
44 | }
45 |
--------------------------------------------------------------------------------
/rpcFailover.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "sort"
7 | "strings"
8 | "sync"
9 |
10 | "github.com/blocto/solana-go-sdk/client"
11 | )
12 |
13 | var failoverMutex sync.Mutex
14 |
15 | type FailoverEndpointList []FailoverEndpoint
16 | type FailoverEndpoint struct {
17 | Endpoint string
18 | AccessToken string
19 | Piority int
20 | MaxRetry int
21 | Retry int
22 | }
23 |
24 | type RPCFailover struct {
25 | curIndex int
26 | Endpoints []FailoverEndpoint
27 | }
28 |
29 | func (f FailoverEndpointList) Len() int { return len(f) }
30 | func (f FailoverEndpointList) Swap(i, j int) { f[i], f[j] = f[j], f[i] }
31 | func (f FailoverEndpointList) Less(i, j int) bool { return f[i].Piority < f[j].Piority }
32 |
33 | func NewRPCFailover(endpoints []RPCEndpoint) RPCFailover {
34 | fList := []FailoverEndpoint{}
35 | for _, e := range endpoints {
36 | endpointExist := strings.TrimRight(e.Endpoint, " /")
37 | if len(endpointExist) == 0 {
38 | continue
39 | }
40 | tokenExist := strings.Trim(e.AccessToken, " ")
41 | fList = append(fList,
42 | FailoverEndpoint{
43 | Endpoint: endpointExist,
44 | AccessToken: tokenExist,
45 | Piority: e.Piority,
46 | MaxRetry: e.MaxRetry})
47 | }
48 | sort.Sort(FailoverEndpointList(FailoverEndpointList(fList)))
49 | return RPCFailover{
50 | Endpoints: fList,
51 | }
52 | }
53 |
54 | func (f *RPCFailover) IsFail() bool {
55 | if f.Endpoints[f.curIndex].Retry >= f.Endpoints[f.curIndex].MaxRetry {
56 | return true
57 | }
58 | return false
59 | }
60 |
61 | func (f *RPCFailover) GetNext() string {
62 | failoverMutex.Lock()
63 | defer failoverMutex.Unlock()
64 | f.curIndex = f.curIndex + 1
65 | return f.Endpoints[f.curIndex].Endpoint
66 | }
67 |
68 | func (f *RPCFailover) GoNext(cur *client.Client, config ClusterConfig, workerNum int) *client.Client {
69 | failoverMutex.Lock()
70 | defer failoverMutex.Unlock()
71 | var next *client.Client
72 | retries := f.GetEndpoint().Retry
73 | if retries < f.GetEndpoint().MaxRetry { // Go Next
74 | if cur != nil {
75 | return cur
76 | } else {
77 | connectionEndpoint := f.GetEndpoint().Endpoint
78 | if len(f.GetEndpoint().AccessToken) != 0 {
79 | connectionEndpoint = fmt.Sprintf("%s/%s", f.GetEndpoint().Endpoint, f.GetEndpoint().AccessToken)
80 | }
81 | return client.NewClient(connectionEndpoint)
82 | }
83 | }
84 | idx := f.GetNextIndex()
85 | if len(f.Endpoints[idx].AccessToken) != 0 {
86 | next = client.NewClient(fmt.Sprintf("%s/%s", f.Endpoints[idx].Endpoint, f.Endpoints[idx].AccessToken))
87 | } else {
88 | next = client.NewClient(f.Endpoints[idx].Endpoint)
89 | }
90 | log.Println("GoNext!!! New Endpoint:", f.GetEndpoint())
91 | if config.AlternativeEnpoint.SlackAlert.Enabled {
92 | var slack SlackPayload
93 | slack.FailoverAlertPayload(config, *f.GetEndpoint(), workerNum)
94 | SlackSend(config.AlternativeEnpoint.SlackAlert.Webhook, &slack)
95 | }
96 | return next
97 | }
98 |
99 | func (f *RPCFailover) GetEndpoint() *FailoverEndpoint {
100 | return &f.Endpoints[f.curIndex]
101 | }
102 |
103 | func (f *FailoverEndpoint) RetryResult(err PingResultError) {
104 | failoverMutex.Lock()
105 | defer failoverMutex.Unlock()
106 | if err.HasError() {
107 | if err.IsTooManyRequest429() ||
108 | err.IsServiceUnavilable() ||
109 | err.IsErrGatewayTimeout504() ||
110 | err.IsNoSuchHost() ||
111 | err.IsConnectionRefused() {
112 | f.Retry += 1
113 | }
114 | } else {
115 | f.Retry = 0
116 | }
117 | }
118 |
119 | func (f *RPCFailover) GetNextIndex() int {
120 | if f.curIndex < 0 {
121 | log.Panic("current Index of FailoverEndpoint < 0")
122 | }
123 | if f.curIndex+1 > len(f.Endpoints)-1 {
124 | f.curIndex = 0
125 | } else {
126 | f.curIndex = f.curIndex + 1
127 | }
128 | return f.curIndex
129 | }
130 |
--------------------------------------------------------------------------------
/rpcPing.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "math"
6 | "sort"
7 | "strings"
8 | "time"
9 |
10 | "github.com/blocto/solana-go-sdk/client"
11 | "github.com/blocto/solana-go-sdk/common"
12 | "github.com/blocto/solana-go-sdk/types"
13 | )
14 |
15 | const WaitConfirmationQueryTimeout = 10
16 |
17 | // TakeTime a struct to record a serious of start and end time. Start and End is used for current record and Times is for store take-time
18 | type TakeTime struct {
19 | Times []int64
20 | Start int64
21 | End int64
22 | }
23 |
24 | // Ping similar to solana-bench-tps. It send a transaction to the cluster
25 | func Ping(c *client.Client, pType PingType, acct types.Account, config ClusterConfig, feeEnabled bool) (PingResult, PingResultError) {
26 | resultErrs := []string{}
27 | timer := TakeTime{}
28 | result := PingResult{
29 | Cluster: string(config.Cluster),
30 | Hostname: config.HostName,
31 | PingType: string(pType),
32 | }
33 | confirmedCount := 0
34 |
35 | computeUnitPrice := getFee(c, acct)
36 |
37 | for i := 0; i < config.BatchCount; i++ {
38 | if i > 0 {
39 | time.Sleep(time.Duration(config.BatchInverval))
40 | }
41 | timer.TimerStart()
42 |
43 | if !feeEnabled || 0 == config.ComputeUnitPrice {
44 | txhash, pingErr := Transfer(c, acct, acct, config.Receiver, time.Duration(config.TxTimeout)*time.Second)
45 | if pingErr.HasError() {
46 | timer.TimerStop()
47 | if !pingErr.IsInErrorList(PingTakeTimeErrExpectionList) {
48 | timer.Add()
49 | }
50 | resultErrs = append(resultErrs, string(pingErr))
51 | continue
52 | }
53 |
54 | waitErr := waitConfirmation(
55 | c,
56 | txhash,
57 | time.Duration(config.WaitConfirmationTimeout)*time.Second,
58 | time.Duration(WaitConfirmationQueryTimeout)*time.Second,
59 | time.Duration(config.StatusCheckInterval)*time.Millisecond,
60 | )
61 | timer.TimerStop()
62 | if waitErr.HasError() {
63 | resultErrs = append(resultErrs, string(waitErr))
64 | if !waitErr.IsInErrorList(PingTakeTimeErrExpectionList) {
65 | timer.Add()
66 | }
67 | continue
68 | }
69 | timer.Add()
70 | confirmedCount++
71 | } else {
72 | txhash, blockhash, pingErr := SendPingTx(SendPingTxParam{
73 | Client: c,
74 | FeePayer: acct,
75 | RequestComputeUnits: config.RequestUnits,
76 | ComputeUnitPrice: computeUnitPrice,
77 | ReceiverPubkey: config.Receiver,
78 | })
79 | if pingErr.HasError() {
80 | timer.TimerStop()
81 | if !pingErr.IsInErrorList(PingTakeTimeErrExpectionList) {
82 | timer.Add()
83 | }
84 | resultErrs = append(resultErrs, string(pingErr))
85 | continue
86 | }
87 | waitErr := waitConfirmationOrBlockhashInvalid(c, txhash, blockhash)
88 | timer.TimerStop()
89 | if waitErr.HasError() {
90 | resultErrs = append(resultErrs, string(waitErr))
91 | if !waitErr.IsInErrorList(PingTakeTimeErrExpectionList) {
92 | timer.Add()
93 | }
94 | continue
95 | }
96 | timer.Add()
97 | confirmedCount++
98 | }
99 | }
100 | result.TimeStamp = time.Now().UTC().Unix()
101 | result.Submitted = config.BatchCount
102 | result.Confirmed = confirmedCount
103 | result.Loss = (float64(result.Submitted-result.Confirmed) / float64(result.Submitted)) * 100
104 | max, mean, min, stdDev, total := timer.Statistic()
105 | result.Max = max
106 | result.Mean = int64(mean)
107 | result.Min = min
108 | result.Stddev = int64(stdDev)
109 | result.TakeTime = total
110 | result.ComputeUnitPrice = computeUnitPrice
111 | result.RequestComputeUnits = config.RequestUnits
112 | result.Error = resultErrs
113 | stringErrors := []string(result.Error)
114 | if 0 == len(stringErrors) {
115 | return result, EmptyPingResultError
116 | }
117 | return result, PingResultError(strings.Join(stringErrors[:], ","))
118 | }
119 |
120 | // TimerStart Record start time in ms format
121 | func (t *TakeTime) TimerStart() {
122 | t.Start = time.Now().UTC().UnixMilli()
123 | }
124 |
125 | // TimerStop Record stop time in ms format
126 | func (t *TakeTime) TimerStop() {
127 | t.End = time.Now().UTC().UnixMilli()
128 | }
129 |
130 | // Add save the end - stop in ms
131 | func (t *TakeTime) Add() {
132 | t.Times = append(t.Times, (t.End - t.Start))
133 | }
134 |
135 | // AddTime add a take time directly into Times
136 | func (t *TakeTime) AddTime(ts int64) {
137 | t.Times = append(t.Times, ts)
138 | }
139 |
140 | // TotalTime sum of data in Times
141 | func (t *TakeTime) TotalTime() int64 {
142 | sum := int64(0)
143 | for _, ts := range t.Times {
144 | sum += ts
145 | }
146 | return sum
147 | }
148 |
149 | // Statistic analyze data in TakeTime to return max/mean/min/stddev/sum
150 | func (t *TakeTime) Statistic() (max int64, mean float64, min int64, stddev float64, sum int64) {
151 | count := 0
152 | for _, ts := range t.Times {
153 | if ts <= 0 { // do not use 0 data because it is the bad data
154 | continue
155 | }
156 | if max == 0 {
157 | max = ts
158 | }
159 | if min == 0 {
160 | min = ts
161 | }
162 | if ts >= max {
163 | max = ts
164 | }
165 | if ts <= min {
166 | min = ts
167 | }
168 | sum += ts
169 | count++
170 |
171 | }
172 | if count > 0 {
173 | mean = float64(sum) / float64(count)
174 | for _, ts := range t.Times {
175 | if ts > 0 { // if ts = 0 , ping fail.
176 | stddev += math.Pow(float64(ts)-mean, 2)
177 | }
178 | }
179 | stddev = math.Sqrt(stddev / float64(count))
180 | }
181 | return
182 | }
183 |
184 | func getFee(c *client.Client, account types.Account) uint64 {
185 | // at least 1 to trigger the system send the fee tx. can be updated if the logic is fixed.
186 | computeUnitPrice := uint64(1)
187 |
188 | // get the max(the last 100 blocks)
189 | fees, err := c.GetRecentPrioritizationFees(context.Background(), []common.PublicKey{account.PublicKey})
190 | if err == nil {
191 | sort.Slice(fees, func(i, j int) bool {
192 | return fees[i].Slot > fees[j].Slot
193 | })
194 | for i := 0; i < len(fees) && i < 100; i++ {
195 | var pFee = fees[i].PrioritizationFee
196 | if pFee > computeUnitPrice {
197 | computeUnitPrice = pFee
198 | }
199 |
200 | cap := uint64(100_000_000)
201 | if computeUnitPrice > cap {
202 | computeUnitPrice = cap
203 | break
204 | }
205 | }
206 | }
207 |
208 | return computeUnitPrice
209 | }
210 |
--------------------------------------------------------------------------------
/script/README:
--------------------------------------------------------------------------------
1 | Install Example Steps
2 |
3 | mkdir ~/ping-api-server
4 | cp scripts/* ~/ping-api-server
5 | cp config.yaml ~/ping-api-server
6 | # Change config.yaml
7 | ./cp-to-real-config.sh
8 | ./update-from-wks.sh
9 |
10 | # create a /etc/systemd/system/solana-ping-api.service
11 | # [Unit]
12 | # Description=Solana Ping API Service
13 | # After=network.target
14 | # StartLimitIntervalSec=1
15 |
16 | # [Service]
17 | # Type=simple
18 | # Restart=always
19 | # RestartSec=30
20 | # User=sol
21 | # LogRateLimitIntervalSec=0
22 | # ExecStart=/home/sol/ping-api-server/restart.sh
23 |
24 | # [Install]
25 | # WantedBy=multi-user.target
26 |
27 | sudo systemctl daemon-reload
28 | sudo systemctl start solana-ping-api.service
29 |
30 |
--------------------------------------------------------------------------------
/script/api-monitoring-each-server.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | # if restart_time_file records 3 times retry attempt. It won't restart service
3 | # after 3 retry times attmpts , delete restart_time_file to restart checking routine
4 | #
5 | host_ip=""
6 | declare -A cluster_query_cmd
7 | cluster_query_cmd[mainnet]="http://$host_ip/mainnet-beta/last6hours"
8 | cluster_query_cmd[testnet]="http://$host_ip/testnet/last6hours"
9 | cluster_query_cmd[devnet]="http://$host_ip/devnet/last6hours"
10 |
11 | slack_webhook=""
12 | restart_time_file="$PWD/ping_server_resart_times.out"
13 | restart_times=0
14 | restart_cmd=""
15 |
16 | slack_alert(){
17 | sdata=$(jq --null-input --arg val "$slacktext" '{"text":$val}')
18 | echo try to sen to slack $sdata
19 | curl -X POST -H 'Content-type: application/json' --data "$sdata" $slack_webhook
20 | }
21 |
22 | write_retry_times() {
23 | echo $restart_times > $restart_time_file
24 | echo $(cat $restart_time_file)
25 | }
26 | read_retry_times() {
27 | restart_times=$(cat $restart_time_file)
28 | #echo read from file $restart_times
29 | }
30 |
31 | # return is_alive, 1: alive 0:not alive (404/408 etc)
32 | alive_check() {
33 | for retry in 0 1 2
34 | do
35 | echo retry=$retry
36 | if [[ $retry -gt 0 ]];then
37 | sleep 10
38 | fi
39 | alive_status_code=$(curl -o /dev/null -s -w "%{http_code}\n" --connect-timeout 10 ${cluster_query_cmd[$cluster]})
40 | if [[ $alive_status_code == 204 || $alive_status_code == 200 ]];then
41 | is_alive=1
42 | restart_times=0
43 | write_retry_times # reset to zero
44 | echo $(date --rfc-3339=seconds) => $cluster $alive_name is alive, status:$alive_status_code
45 | break
46 | else
47 | is_alive=0
48 | echo $(date --rfc-3339=seconds) => $cluster $alive_name is NOT alive, status:$alive_status_code
49 | fi
50 |
51 | done
52 | }
53 |
54 | if [[ ! -f "$restart_time_file" ]];then
55 | echo $restart_times > $restart_time_file
56 | else
57 | read_retry_times
58 | fi
59 | echo $(date --rfc-3339=seconds) initial restart_times = $restart_times
60 |
61 | for cluster in "mainnet"
62 | do
63 | alive_check
64 | if [[ $is_alive -eq 0 ]];then
65 | # restart solana-ping-api.service
66 | #exec /etc/init.d/solana-ping-api.service restart
67 | if [[ $restart_times -lt 2 ]];then
68 | slacktext="{hostname: $HOSTNAME, cluster:$cluster, internal_ip:$host_ip, msg: api last6hours return $alive_status_code and the server is restarting }"
69 | slack_alert
70 | restart_times=$(expr $restart_times + 1)
71 | write_retry_times
72 | exec /home/sol/ping-api-server/restart-api.sh
73 | exit 0
74 | else
75 | slacktext="{hostname: $HOSTNAME, cluster:$cluster, internal_ip:$host_ip, msg: api last6hours return $alive_status_code and has restarted 2 times."
76 | slack_alert
77 | fi
78 | fi
79 | done
80 |
--------------------------------------------------------------------------------
/script/api-monitoring.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | declare -A cluster_query_cmd
4 | cluster_query_cmd[mainnet]="https://ping.solana.com/mainnet-beta/last6hours"
5 | cluster_query_cmd[testnet]="https://ping.solana.com/testnet/last6hours"
6 | cluster_query_cmd[devnet]="https://ping.solana.com/devnet/last6hours"
7 |
8 | slack_webhook=""
9 |
10 |
11 | slack_alert(){
12 | sdata=$(jq --null-input --arg val "$slacktext" '{"text":$val}')
13 | curl -X POST -H 'Content-type: application/json' --data "$sdata" $slack_webhook
14 | }
15 |
16 | # return is_alive, 1: alive 0:not alive (404/408 etc)
17 | alive_check() {
18 | for retry in 0 1 2
19 | do
20 | if [[ $retry -gt 0 ]];then
21 | echo retry $retry after 30 sec ...
22 | sleep 30
23 | fi
24 | alive_status_code=$(curl -o /dev/null -s -w "%{http_code}\n" --connect-timeout 10 ${cluster_query_cmd[$cluster]})
25 | if [[ $alive_status_code == 204 || $alive_status_code == 200 ]];then
26 | is_alive=1
27 | restart_times=0
28 | echo $(date --rfc-3339=seconds) => $cluster $alive_name is alive, status:$alive_status_code
29 | break
30 | else
31 | is_alive=0
32 | echo $(date --rfc-3339=seconds) => $cluster $alive_name is NOT alive, status:$alive_status_code
33 | fi
34 | done
35 | }
36 |
37 | echo $(date --rfc-3339=seconds) ping-api-alive check ----
38 |
39 | for cluster in "mainnet"
40 | do
41 | alive_check
42 | if [[ $is_alive -eq 0 ]];then
43 | slacktext="{hostname: $HOSTNAME, cluster:$cluster, msg: api last6hours return $alive_status_code. Ping API Service has broken }"
44 | slack_alert
45 | fi
46 | done
47 |
--------------------------------------------------------------------------------
/script/cp-to-real-config.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | cp ./config.yaml ~/.config/ping-api/config.yaml
4 |
5 |
--------------------------------------------------------------------------------
/script/docker-build-fee-tracer.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | here=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)
4 |
5 | docker build \
6 | -f "$here/../docker/Dockerfile.default" \
7 | --build-arg COMPONENT=fee-tracer \
8 | -t anzaxyz/fee-tracer \
9 | .
--------------------------------------------------------------------------------
/script/solana-ping-api-restart.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -ex
3 | logdir="$HOME/ping-api-server/log"
4 |
5 | if ! [[ -d $logdir ]];then
6 | mkdir $logdir
7 | fi
8 | log="$logdir/solana-ping-api.log"
9 | ## Your executable location
10 | exec /home/sol/ping-api-server/solana-ping-api-service 2 >> $log
11 | ret=$?
12 |
13 | echo $ret
14 |
--------------------------------------------------------------------------------
/script/update-from-wks.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | rm ~/ping-api-server/solana-ping-api-service
3 | cp ~/wks_go/solana-ping-api/solana-ping-api-service .
4 | #cp ~/wks_go/solana-ping-api/config.yaml
5 |
--------------------------------------------------------------------------------
/workers.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "io/ioutil"
7 | "log"
8 | "sort"
9 | "strings"
10 | "time"
11 |
12 | "github.com/blocto/solana-go-sdk/client"
13 | "github.com/blocto/solana-go-sdk/types"
14 | )
15 |
16 | type PingType string
17 |
18 | const DefaultAlertThredHold = 20
19 | const DualModeNoFeeTriggerName = "no-fee-dualmode"
20 | const (
21 | DataPointReport PingType = "report"
22 | DataPoint1Min PingType = "datapoint1min"
23 | )
24 |
25 | func launchWorkers(c ClustersToRun) {
26 | // Run Ping Service
27 | runCluster := func(clusterConf ClusterConfig) {
28 | if !clusterConf.PingServiceEnabled {
29 | log.Println("==> go pingDataWorker", clusterConf.Cluster, " PingServiceEnabled ", clusterConf.PingServiceEnabled)
30 | } else {
31 | for i := 0; i < clusterConf.PingConfig.NumWorkers; i++ {
32 | log.Println("==> go pingDataWorker", clusterConf.Cluster, " n:", clusterConf.PingConfig.NumWorkers, "i:", i)
33 | go pingDataWorker(clusterConf, i)
34 | time.Sleep(2 * time.Second)
35 | }
36 | }
37 | if clusterConf.Report.Enabled {
38 | go reportWorker(clusterConf)
39 | }
40 | }
41 | // Single Cluster or all Cluster
42 | switch c {
43 | case RunMainnetBeta:
44 | runCluster(config.Mainnet)
45 | case RunTestnet:
46 | runCluster(config.Testnet)
47 | case RunDevnet:
48 | runCluster(config.Devnet)
49 | case RunAllClusters:
50 | runCluster(config.Mainnet)
51 | runCluster(config.Testnet)
52 | runCluster(config.Devnet)
53 | default:
54 | panic(ErrInvalidCluster)
55 | }
56 | // Run Retension Service
57 | if config.Retension.Enabled {
58 | time.Sleep(2 * time.Second)
59 | go retensionServiceWorker()
60 | }
61 | }
62 |
63 | func pingDataWorker(cConf ClusterConfig, workerNum int) {
64 | log.Println(">> Solana DataPoint1MinWorker for ", cConf.Cluster, " worker:", workerNum, " start!")
65 | defer log.Println(">> Solana DataPoint1MinWorker for ", cConf.Cluster, " worker:", workerNum, " end!")
66 | var failover RPCFailover
67 | var c *client.Client
68 | var acct types.Account
69 |
70 | switch cConf.Cluster {
71 | case MainnetBeta:
72 | failover = mainnetFailover
73 | clusterAcct, err := getConfigKeyPair(config.ClusterCLIConfig.ConfigMain)
74 | if err != nil {
75 | log.Panic("getConfigKeyPair Error")
76 | }
77 | acct = clusterAcct
78 | case Testnet:
79 | failover = testnetFailover
80 | clusterAcct, err := getConfigKeyPair(config.ClusterCLIConfig.ConfigTestnet)
81 | if err != nil {
82 | log.Panic("Testnet getConfigKeyPair Error")
83 | }
84 | acct = clusterAcct
85 | case Devnet:
86 | failover = devnetFailover
87 | clusterAcct, err := getConfigKeyPair(config.ClusterCLIConfig.ConfigDevnet)
88 | if err != nil {
89 | log.Panic("Devnet getConfigKeyPair Error")
90 | }
91 | acct = clusterAcct
92 | default:
93 | panic(ErrInvalidCluster)
94 | }
95 | pingWithFee := true
96 |
97 | for {
98 | c = failover.GoNext(c, cConf, workerNum)
99 | result, err := Ping(c, DataPoint1Min, acct, cConf, pingWithFee)
100 | extraTimeStart := time.Now().UTC().Unix()
101 | if cConf.PingConfig.ComputeFeeDualMode {
102 | if !pingWithFee {
103 | result.ComputeUnitPrice = 0
104 | result.RequestComputeUnits = 0
105 | }
106 | }
107 | addRecord(result)
108 | if influxdb != nil && influxdb.Client != nil {
109 | influxdb.SendDatapointAsync(influxdb.PrepareInfluxdbData(result))
110 | }
111 | failover.GetEndpoint().RetryResult(err)
112 | extraTimeStop := time.Now().UTC().Unix()
113 | waitTime := cConf.ClusterPing.PingConfig.MinPerPingTime - (result.TakeTime / 1000) - (extraTimeStop - extraTimeStart)
114 | if waitTime > 0 {
115 | time.Sleep(time.Duration(waitTime) * time.Second)
116 | }
117 | if cConf.PingConfig.ComputeFeeDualMode {
118 | pingWithFee = !pingWithFee
119 | }
120 | }
121 | }
122 |
123 | func retensionServiceWorker() {
124 | log.Println(">> Retension Service Worker start!")
125 | defer log.Println(">> Retension Service Worker end!")
126 | for {
127 | now := time.Now().UTC().Unix()
128 | if config.Retension.KeepHours < 6 {
129 | config.Retension.KeepHours = 6
130 | }
131 | timeB4 := now - (config.Retension.KeepHours * 60 * 60)
132 | deleteTimeBefore(timeB4)
133 | if config.Retension.UpdateIntervalSec < 300 {
134 | config.Retension.UpdateIntervalSec = 300
135 | }
136 | time.Sleep(time.Duration(config.Retension.UpdateIntervalSec) * time.Second)
137 | }
138 | }
139 |
140 | func getConfigKeyPair(c SolanaCLIConfig) (types.Account, error) {
141 | body, err := ioutil.ReadFile(c.KeypairPath)
142 | if err != nil {
143 | return types.Account{}, ErrKeyPairFile
144 | }
145 | key := []byte{}
146 | err = json.Unmarshal(body, &key)
147 | if err != nil {
148 | return types.Account{}, err
149 | }
150 | acct, err := types.AccountFromBytes(key)
151 | if err != nil {
152 | return types.Account{}, err
153 | }
154 | return acct, nil
155 | }
156 |
157 | func reportWorker(cConf ClusterConfig) {
158 | log.Println(">> Report Worker for ", cConf.Cluster, " start!")
159 | defer log.Println(">> Report Worker for ", cConf.Cluster, " end!")
160 | var lastReporTime int64
161 | trigger := NewAlertTrigger(cConf)
162 |
163 | var triggerNoFee AlertTrigger // TriggerNoFee is used only when ComputeFeeDualMode is on
164 | if cConf.PingConfig.ComputeUnitPrice > 0 && cConf.PingConfig.ComputeFeeDualMode {
165 | triggerNoFee = NewAlertTriggerByParams(DualModeNoFeeTriggerName, cConf.Report.LevelFilePath+".nofee", cConf.LossThreshold)
166 | }
167 |
168 | for {
169 | now := time.Now().UTC().Unix()
170 | if lastReporTime == 0 { // server restart will cause lasterReportTime zero
171 | lastReporTime = now - int64(cConf.Report.Interval)
172 | log.Println("reconstruct lastReport time=", lastReporTime, "time now=", time.Now().UTC().Unix())
173 | }
174 | sendReportAlert := func(slackReportEnabled bool, slackAlertEnabled bool,
175 | discordReportEnabled bool, discordAlertEnabled bool,
176 | groupStatistic *GroupsAllStatistic, globalStatistic GlobalStatistic,
177 | toSendAlert bool, alertTrigger AlertTrigger, messageMemo string) {
178 | var accessToken string
179 | switch cConf.Cluster {
180 | case MainnetBeta:
181 | accessToken = mainnetFailover.GetEndpoint().AccessToken
182 | case Testnet:
183 | accessToken = testnetFailover.GetEndpoint().AccessToken
184 | case Devnet:
185 | accessToken = devnetFailover.GetEndpoint().AccessToken
186 | default:
187 | panic(fmt.Sprintf("%s:%s", "no such cluster", cConf.Cluster))
188 | }
189 | if slackReportEnabled {
190 | slackReportSend(cConf, groupStatistic, &globalStatistic, []string{accessToken}, messageMemo)
191 | }
192 | if slackAlertEnabled && toSendAlert {
193 | slackAlertSend(cConf, &globalStatistic, groupStatistic.GlobalErrorStatistic,
194 | alertTrigger.ThresholdLevels[alertTrigger.ThresholdIndex], []string{accessToken}, messageMemo)
195 | }
196 | if discordReportEnabled {
197 | discordReportSend(cConf, groupStatistic, &globalStatistic, []string{accessToken}, messageMemo)
198 | }
199 | if discordAlertEnabled && toSendAlert {
200 | discordAlertSend(cConf, &globalStatistic, groupStatistic.GlobalErrorStatistic,
201 | alertTrigger.ThresholdLevels[alertTrigger.ThresholdIndex], []string{accessToken}, messageMemo)
202 | }
203 | }
204 | getDataFromComputeFee := AllData
205 | if cConf.PingConfig.ComputeUnitPrice > 0 && cConf.PingConfig.RequestUnits > 0 {
206 | getDataFromComputeFee = HasComputeUnitPrice
207 | }
208 | data := getAfter(cConf.Cluster, DataPoint1Min, lastReporTime, getDataFromComputeFee, 0)
209 | if len(data) <= 0 { // No Data
210 | log.Println(cConf.Cluster, " getAfter return empty")
211 | time.Sleep(30 * time.Second)
212 | continue
213 | }
214 | groupsStat, globalStat := getGlobalStatistis(cConf, data, lastReporTime, now)
215 | trigger.Update(globalStat.Loss)
216 | // ShouldAlertSend execute once only. TODO: make shouldAlertSend a function which does not modify any value
217 | alertSend := trigger.ShouldAlertSend()
218 | messageMemo := ""
219 | if cConf.PingConfig.ComputeUnitPrice > 0 {
220 | // count txs by fee
221 | m := map[uint64]uint64{}
222 | for _, v := range data {
223 | m[v.ComputeUnitPrice]++
224 | }
225 |
226 | // sort keys for pretty output
227 | keys := make([]uint64, 0, len(m))
228 | for k := range m {
229 | keys = append(keys, k)
230 | }
231 | sort.Slice(keys, func(i, j int) bool { return keys[i] < keys[j] })
232 |
233 | // prepare output
234 | outputs := make([]string, 0, len(keys))
235 | for _, computeUnitPrice := range keys {
236 | txCount := m[computeUnitPrice]
237 | outputs = append(outputs, fmt.Sprintf("%vtx@%vml", txCount, computeUnitPrice))
238 | }
239 |
240 | messageMemo = fmt.Sprintf("with-fee (max: 10^8 ml), ({count}tx@{compute_unit_price}ml: %v)", strings.Join(outputs, ","))
241 | } else {
242 | messageMemo = "no-fee"
243 | }
244 |
245 | sendReportAlert(cConf.Report.Slack.Report.Enabled, cConf.Report.Slack.Alert.Enabled,
246 | cConf.Report.Discord.Report.Enabled, cConf.Report.Discord.Alert.Enabled,
247 | groupsStat, globalStat, alertSend, trigger, messageMemo)
248 |
249 | // ComputeFeeDualMode for no-fee alert
250 | if cConf.PingConfig.ComputeUnitPrice > 0 && cConf.PingConfig.RequestUnits > 0 && cConf.PingConfig.ComputeFeeDualMode {
251 | dataNoFee := getAfter(cConf.Cluster, DataPoint1Min, lastReporTime, NoComputeUnitPrice, 0)
252 | if len(data) <= 0 { // No Data
253 | log.Println(cConf.Cluster, "ComputeFeeDualMode noComputeUnitPrice getAfter return empty")
254 | } else {
255 | groupsStatNoFee, globalStatNoFee := getGlobalStatistis(cConf, dataNoFee, lastReporTime, now)
256 | triggerNoFee.Update(globalStatNoFee.Loss)
257 | alertSendNoFee := triggerNoFee.ShouldAlertSend()
258 | sendReportAlert(cConf.Report.Slack.Report.Enabled, cConf.Report.Slack.Alert.Enabled,
259 | cConf.Report.Discord.Report.Enabled, cConf.Report.Discord.Alert.Enabled,
260 | groupsStatNoFee, globalStatNoFee, alertSendNoFee, triggerNoFee, "no-fee (dual-mode)")
261 | }
262 | }
263 | lastReporTime = now
264 | time.Sleep(time.Duration(cConf.Report.Interval) * time.Second)
265 | }
266 | }
267 |
268 | func slackReportSend(cConf ClusterConfig, groupsStat *GroupsAllStatistic, globalStat *GlobalStatistic, hideKeywords []string, memo string) {
269 | payload := SlackPayload{}
270 | payload.ReportPayload(cConf.Cluster, groupsStat, *globalStat, hideKeywords, memo)
271 | err := SlackSend(cConf.Report.Slack.Report.Webhook, &payload)
272 | if err != nil {
273 | log.Println("slackReportSend Error:", err)
274 | }
275 | }
276 |
277 | func slackAlertSend(conf ClusterConfig, globalStat *GlobalStatistic, globalErrorStatistic map[string]int, threadhold float64, hideKeywords []string, messageMemo string) {
278 | payload := SlackPayload{}
279 | payload.AlertPayload(conf, globalStat, globalErrorStatistic, threadhold, hideKeywords, messageMemo)
280 | err := SlackSend(conf.Report.Slack.Alert.Webhook, &payload)
281 | if err != nil {
282 | log.Println("slackAlertSend Error:", err)
283 | }
284 | }
285 |
286 | func discordReportSend(cConf ClusterConfig, groupsStat *GroupsAllStatistic, globalStat *GlobalStatistic, hideKeywords []string, messageMemo string) {
287 | payload := DiscordPayload{BotAvatarURL: cConf.Report.Discord.BotAvatarURL, BotName: cConf.Report.Discord.BotName}
288 | payload.ReportPayload(cConf.Cluster, groupsStat, *globalStat, hideKeywords, messageMemo)
289 | err := DiscordSend(cConf.Report.Discord.Report.Webhook, &payload)
290 | if err != nil {
291 | log.Println("discordReportSend Error:", err)
292 | }
293 | }
294 |
295 | func discordAlertSend(cConf ClusterConfig, globalStat *GlobalStatistic, globalErrorStatistic map[string]int, threadhold float64, hideKeywords []string, messageMemo string) {
296 | payload := DiscordPayload{BotAvatarURL: cConf.Report.Discord.BotAvatarURL, BotName: cConf.Report.Discord.BotName}
297 | payload.AlertPayload(cConf, globalStat, globalErrorStatistic, threadhold, hideKeywords, messageMemo)
298 | err := DiscordSend(cConf.Report.Discord.Alert.Webhook, &payload)
299 | if err != nil {
300 | log.Println("discordAlertSend Error:", err)
301 | }
302 | }
303 |
304 | func getGlobalStatistis(cConf ClusterConfig, resutls []PingResult, lastReportTime int64, currentTime int64) (*GroupsAllStatistic, GlobalStatistic) {
305 | groups := grouping1Min(resutls, lastReportTime, currentTime)
306 | groupsStat := statisticCompute(cConf, groups)
307 | return groupsStat, groupsStat.GetGroupsAllStatistic(false) // get raw data
308 | }
309 |
--------------------------------------------------------------------------------