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