├── services
├── bidcollect
│ ├── website
│ │ ├── static
│ │ │ ├── styles.css
│ │ │ └── favicon
│ │ │ │ ├── favicon.ico
│ │ │ │ ├── favicon-16x16.png
│ │ │ │ ├── favicon-32x32.png
│ │ │ │ ├── apple-touch-icon.png
│ │ │ │ ├── android-chrome-192x192.png
│ │ │ │ └── android-chrome-512x512.png
│ │ ├── templates
│ │ │ ├── index_root.html
│ │ │ ├── index_files.html
│ │ │ └── base.html
│ │ ├── htmldata.go
│ │ ├── utils.go
│ │ ├── devserver.go
│ │ └── generator.go
│ ├── types
│ │ ├── consts.go
│ │ ├── types_test.go
│ │ └── types.go
│ ├── webserver
│ │ ├── handler.go
│ │ └── webserver.go
│ ├── bidcollector.go
│ ├── ultrasound-stream.go
│ ├── data-api-poller.go
│ ├── bid-processor.go
│ └── getheader-poller.go
└── website
│ ├── types.go
│ ├── html.go
│ ├── templates
│ ├── base.html
│ └── daily-stats.html
│ ├── utils_test.go
│ └── webserver_data.go
├── static
├── images
│ ├── cows1.jpg
│ ├── robot1.jpg
│ ├── robot4.jpg
│ ├── ogimage.png
│ ├── robot4-c.png
│ ├── logo-space.png
│ └── logo1-square.png
├── favicon
│ ├── favicon.ico
│ ├── favicon-16x16.png
│ ├── favicon-32x32.png
│ ├── apple-touch-icon.png
│ ├── android-chrome-192x192.png
│ ├── android-chrome-512x512.png
│ └── site.webmanifest
├── README.md
└── sortable.min.js
├── docs
├── img
│ └── bidcollect-overview.png
└── 2024-06_bidcollect.md
├── vars
├── relays.go
├── builder_addresses.go
├── builder_aliases_test.go
├── vars.go
├── builder_aliases.go
└── config.go
├── scripts
├── bidcollect
│ ├── s3
│ │ ├── get-folders.sh
│ │ ├── get-files.sh
│ │ └── upload-file-to-r2.sh
│ ├── bids-combine-and-upload-yesterday.sh
│ └── bids-combine-and-upload.sh
├── backfill-stats.sh
├── backfill.sh
├── send-pushover-notification.sh
└── website-healthcheck.sh
├── common
├── errors.go
├── utils_test.go
├── logging.go
├── ultrasoundbid.go
├── ultrasoundbid_test.go
├── eth_node.go
├── request.go
├── relayentry.go
├── ultrasoundbid_encoding.go
└── utils.go
├── main.go
├── database
├── migrations
│ ├── migration.go
│ ├── 004_add_block_timestamp.go
│ ├── 002_add_blob_count.go
│ ├── 003_add_blob_index.go
│ └── 001_init_database.go
├── util_test.go
├── database_test.go
├── vars
│ └── tables.go
├── util.go
├── typesconv.go
└── types.go
├── cmd
├── version.go
├── service
│ ├── service.go
│ ├── website.go
│ ├── bidcollect.go
│ └── backfill_runner.go
├── core
│ ├── core.go
│ └── data-api-backfill.go
├── util
│ ├── util.go
│ └── update-extradata.go
└── root.go
├── .github
├── pull_request_template.md
└── workflows
│ ├── checks.yml
│ └── release.yml
├── .env.example
├── .gitignore
├── staticcheck.conf
├── Dockerfile
├── config-hoodi.yaml
├── config-mainnet.yaml
├── .golangci.yaml
├── Makefile
├── go.mod
└── README.md
/services/bidcollect/website/static/styles.css:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/static/images/cows1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flashbots/relayscan/HEAD/static/images/cows1.jpg
--------------------------------------------------------------------------------
/static/images/robot1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flashbots/relayscan/HEAD/static/images/robot1.jpg
--------------------------------------------------------------------------------
/static/images/robot4.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flashbots/relayscan/HEAD/static/images/robot4.jpg
--------------------------------------------------------------------------------
/static/favicon/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flashbots/relayscan/HEAD/static/favicon/favicon.ico
--------------------------------------------------------------------------------
/static/images/ogimage.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flashbots/relayscan/HEAD/static/images/ogimage.png
--------------------------------------------------------------------------------
/static/images/robot4-c.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flashbots/relayscan/HEAD/static/images/robot4-c.png
--------------------------------------------------------------------------------
/static/images/logo-space.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flashbots/relayscan/HEAD/static/images/logo-space.png
--------------------------------------------------------------------------------
/static/images/logo1-square.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flashbots/relayscan/HEAD/static/images/logo1-square.png
--------------------------------------------------------------------------------
/docs/img/bidcollect-overview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flashbots/relayscan/HEAD/docs/img/bidcollect-overview.png
--------------------------------------------------------------------------------
/static/favicon/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flashbots/relayscan/HEAD/static/favicon/favicon-16x16.png
--------------------------------------------------------------------------------
/static/favicon/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flashbots/relayscan/HEAD/static/favicon/favicon-32x32.png
--------------------------------------------------------------------------------
/static/favicon/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flashbots/relayscan/HEAD/static/favicon/apple-touch-icon.png
--------------------------------------------------------------------------------
/static/favicon/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flashbots/relayscan/HEAD/static/favicon/android-chrome-192x192.png
--------------------------------------------------------------------------------
/static/favicon/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flashbots/relayscan/HEAD/static/favicon/android-chrome-512x512.png
--------------------------------------------------------------------------------
/services/bidcollect/website/static/favicon/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flashbots/relayscan/HEAD/services/bidcollect/website/static/favicon/favicon.ico
--------------------------------------------------------------------------------
/static/README.md:
--------------------------------------------------------------------------------
1 | https://purecss.io/start/
2 | https://purecss.io/layouts/marketing/
3 | https://github.com/pure-css/pure/tree/master/site/static/layouts/marketing
--------------------------------------------------------------------------------
/services/bidcollect/website/static/favicon/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flashbots/relayscan/HEAD/services/bidcollect/website/static/favicon/favicon-16x16.png
--------------------------------------------------------------------------------
/services/bidcollect/website/static/favicon/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flashbots/relayscan/HEAD/services/bidcollect/website/static/favicon/favicon-32x32.png
--------------------------------------------------------------------------------
/services/bidcollect/website/static/favicon/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flashbots/relayscan/HEAD/services/bidcollect/website/static/favicon/apple-touch-icon.png
--------------------------------------------------------------------------------
/vars/relays.go:
--------------------------------------------------------------------------------
1 | package vars
2 |
3 | // Relay URLs - populated from config file
4 | var (
5 | RelayFlashbots string
6 | RelayUltrasound string
7 | RelayURLs []string
8 | )
9 |
--------------------------------------------------------------------------------
/services/bidcollect/website/static/favicon/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flashbots/relayscan/HEAD/services/bidcollect/website/static/favicon/android-chrome-192x192.png
--------------------------------------------------------------------------------
/services/bidcollect/website/static/favicon/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flashbots/relayscan/HEAD/services/bidcollect/website/static/favicon/android-chrome-512x512.png
--------------------------------------------------------------------------------
/scripts/bidcollect/s3/get-folders.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | aws --profile r2 s3 ls s3://relayscan-bidarchive/$1 --endpoint-url "https://${CLOUDFLARE_R2_ACCOUNT_ID}.r2.cloudflarestorage.com" | awk '{ print $2 }'
--------------------------------------------------------------------------------
/vars/builder_addresses.go:
--------------------------------------------------------------------------------
1 | package vars
2 |
3 | // BuilderAddresses maps coinbase addresses to their owned addresses
4 | // Populated from config file
5 | var BuilderAddresses map[string]map[string]bool
6 |
--------------------------------------------------------------------------------
/common/errors.go:
--------------------------------------------------------------------------------
1 | package common
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | )
7 |
8 | var (
9 | ErrMissingRelayPubkey = fmt.Errorf("missing relay public key")
10 | ErrURLEmpty = errors.New("url is empty")
11 | )
12 |
--------------------------------------------------------------------------------
/scripts/backfill-stats.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -e
3 | dir=$( dirname -- "$0"; )
4 | cd $dir
5 | cd ..
6 | source .env.prod
7 | ./relayscan core update-builder-stats --backfill --daily --verbose 2>&1 | /usr/bin/tee -a /var/log/relayscan-stats.log
8 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/flashbots/relayscan/cmd"
5 | "github.com/flashbots/relayscan/vars"
6 | )
7 |
8 | var Version = "dev" // is set during build process
9 |
10 | func main() {
11 | vars.Version = Version
12 | cmd.Execute()
13 | }
14 |
--------------------------------------------------------------------------------
/scripts/backfill.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -e
3 | dir=$( dirname -- "$0"; )
4 | cd $dir
5 | cd ..
6 | source .env.prod
7 | ./relayscan core data-api-backfill 2>&1 | /usr/bin/tee /var/log/relayscan.log
8 | ./relayscan core check-payload-value 2>&1 | /usr/bin/tee -a /var/log/relayscan.log
9 |
--------------------------------------------------------------------------------
/scripts/bidcollect/s3/get-files.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # require one argument
3 | if [ $# -ne 1 ]; then
4 | echo "Usage: $0
17 | The data is dedicated to the public domain under the CC-0 license.
4 |
5 | Ethereum Mainnet
6 |
7 | {{ range .EthMainnetMonths }}
8 |
11 |
12 |
13 |
14 |
15 |
16 |
| ../ | 15 |16 | |
| 33 | {{ if eq $change "1" }}{{ end }} 34 | 35 | {{ .Filename }} 36 | | 37 |{{ .Size | humanBytes }} | 38 |
46 | The data is dedicated to the public domain under the CC-0 license. 47 |
48 | {{ end }} -------------------------------------------------------------------------------- /.github/workflows/checks.yml: -------------------------------------------------------------------------------- 1 | name: Checks 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | test: 11 | name: Test 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Set up Go 15 | uses: actions/setup-go@v6 16 | with: 17 | go-version: ^1.24 18 | 19 | - name: Check out code into the Go module directory 20 | uses: actions/checkout@v5 21 | 22 | - name: Run unit tests and generate the coverage report 23 | run: make test-race 24 | 25 | lint: 26 | name: Lint 27 | runs-on: ubuntu-latest 28 | steps: 29 | - name: Check out code into the Go module directory 30 | uses: actions/checkout@v5 31 | 32 | - name: Set up Go 33 | uses: actions/setup-go@v6 34 | with: 35 | go-version: ^1.24 36 | 37 | - name: Download dependencies 38 | run: go mod download 39 | 40 | - name: Install gofumpt 41 | run: go install mvdan.cc/gofumpt@v0.6.0 42 | 43 | - name: Install staticcheck 44 | run: go install honnef.co/go/tools/cmd/staticcheck@2025.1.1 45 | 46 | - name: Install golangci-lint 47 | run: go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.1.2 48 | 49 | - name: Lint 50 | run: make lint 51 | 52 | - name: Ensure go mod tidy runs without changes 53 | run: | 54 | go mod tidy 55 | git diff-index HEAD 56 | git diff-index --quiet HEAD 57 | -------------------------------------------------------------------------------- /cmd/service/website.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "os" 5 | 6 | relaycommon "github.com/flashbots/mev-boost-relay/common" 7 | "github.com/flashbots/relayscan/database" 8 | "github.com/flashbots/relayscan/services/website" 9 | "github.com/flashbots/relayscan/vars" 10 | "github.com/spf13/cobra" 11 | ) 12 | 13 | var ( 14 | websiteDefaultListenAddr = relaycommon.GetEnv("LISTEN_ADDR", "localhost:9060") 15 | websiteListenAddr string 16 | websiteDev = os.Getenv("DEV") == "1" 17 | ) 18 | 19 | func init() { 20 | // rootCmd.AddCommand(websiteCmd) 21 | websiteCmd.Flags().StringVar(&websiteListenAddr, "listen-addr", websiteDefaultListenAddr, "listen address for webserver") 22 | websiteCmd.Flags().BoolVar(&websiteDev, "dev", websiteDev, "development mode") 23 | } 24 | 25 | var websiteCmd = &cobra.Command{ 26 | Use: "website", 27 | Short: "Start the website server", 28 | Run: func(cmd *cobra.Command, args []string) { 29 | var err error 30 | 31 | // Connect to Postgres 32 | db := database.MustConnectPostgres(log, vars.DefaultPostgresDSN) 33 | 34 | // Create the website service 35 | opts := &website.WebserverOpts{ 36 | ListenAddress: websiteListenAddr, 37 | DB: db, 38 | Log: log, 39 | Dev: websiteDev, 40 | } 41 | 42 | srv, err := website.NewWebserver(opts) 43 | if err != nil { 44 | log.WithError(err).Fatal("failed to create service") 45 | } 46 | 47 | // Start the server 48 | log.Infof("Webserver starting on %s (%s) ...", websiteListenAddr, vars.Version) 49 | log.Fatal(srv.StartServer()) 50 | }, 51 | } 52 | -------------------------------------------------------------------------------- /services/bidcollect/webserver/handler.go: -------------------------------------------------------------------------------- 1 | package webserver 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strings" 7 | 8 | "github.com/flashbots/relayscan/services/bidcollect/types" 9 | "github.com/google/uuid" 10 | ) 11 | 12 | type SSESubscription struct { 13 | uid string 14 | msgC chan string 15 | } 16 | 17 | func (srv *Server) handleSSESubscription(w http.ResponseWriter, r *http.Request) { 18 | // SSE server for transactions 19 | srv.log.Info("SSE connection opened for transactions") 20 | 21 | // Set CORS headers to allow all origins. You may want to restrict this to specific origins in a production environment. 22 | w.Header().Set("Access-Control-Allow-Origin", "*") 23 | w.Header().Set("Access-Control-Expose-Headers", "Content-Type") 24 | 25 | w.Header().Set("Content-Type", "text/event-stream") 26 | w.Header().Set("Cache-Control", "no-cache") 27 | w.Header().Set("Connection", "keep-alive") 28 | 29 | subscriber := SSESubscription{ 30 | uid: uuid.New().String(), 31 | msgC: make(chan string, 100), 32 | } 33 | srv.addSubscriber(&subscriber) 34 | 35 | // Send CSV header 36 | helloMsg := strings.Join(types.CommonBidCSVFields, ",") + "\n" 37 | fmt.Fprint(w, helloMsg) //nolint:errcheck 38 | w.(http.Flusher).Flush() //nolint:forcetypeassert 39 | 40 | // Wait for txs or end of request... 41 | for { 42 | select { 43 | case <-r.Context().Done(): 44 | srv.log.Info("SSE closed, removing subscriber") 45 | srv.removeSubscriber(&subscriber) 46 | return 47 | 48 | case msg := <-subscriber.msgC: 49 | fmt.Fprintf(w, "%s\n", msg) //nolint:errcheck 50 | w.(http.Flusher).Flush() //nolint:forcetypeassert 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /services/bidcollect/types/types_test.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestSourceTypes(t *testing.T) { 11 | require.Equal(t, 0, SourceTypeGetHeader) 12 | require.Equal(t, 1, SourceTypeDataAPI) 13 | require.Equal(t, 2, SourceTypeUltrasoundStream) 14 | } 15 | 16 | func TestCSVHasNotChanged(t *testing.T) { 17 | // The specific field ordering is used in many places throughout the ecosystem and must not be changed. 18 | expectedResult := "source_type,received_at_ms,timestamp_ms,slot,slot_t_ms,value,block_hash,parent_hash,builder_pubkey,block_number,block_fee_recipient,relay,proposer_pubkey,proposer_fee_recipient,optimistic_submission" 19 | currentResult := strings.Join(CommonBidCSVFields, ",") 20 | require.Equal(t, expectedResult, currentResult) 21 | 22 | bid := CommonBid{ 23 | SourceType: SourceTypeGetHeader, 24 | ReceivedAtMs: 1, 25 | TimestampMs: 2, 26 | Slot: 3, 27 | BlockNumber: 4, 28 | BlockHash: "5", 29 | ParentHash: "6", 30 | BuilderPubkey: "7", 31 | Value: "8", 32 | BlockFeeRecipient: "9", 33 | Relay: "10", 34 | ProposerPubkey: "11", 35 | ProposerFeeRecipient: "12", 36 | OptimisticSubmission: true, 37 | } 38 | asCSV := bid.ToCSVLine(",") 39 | expected := "0,1,2,3,-1606824058998,8,5,6,7,4,9,10,11,12," 40 | require.Equal(t, expected, asCSV) 41 | 42 | // When source type is data-api, then optimistic field is included 43 | bid.SourceType = SourceTypeDataAPI 44 | asCSV = bid.ToCSVLine(",") 45 | expected = "1,1,2,3,-1606824058998,8,5,6,7,4,9,10,11,12,true" 46 | require.Equal(t, expected, asCSV) 47 | } 48 | -------------------------------------------------------------------------------- /scripts/website-healthcheck.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Check health of relayscan.io and send notifications if state changes. 4 | # 5 | # https://www.relayscan.io/healthz 6 | # 7 | # This script is intended to be run as a cron job. 8 | # 9 | # It uses a temporary file to not send multiple notifications and store the error. 10 | # 11 | set -o errexit 12 | set -o nounset 13 | set -o pipefail 14 | 15 | url="https://www.relayscan.io/healthz" 16 | # url="localhost:9060/healthz" 17 | check_fn="/tmp/relayscan-error.txt" 18 | check_cmd="curl -s $url" 19 | 20 | # load environment variables $PUSHOVER_APP_TOKEN and $PUSHOVER_APP_KEY 21 | source "$(dirname "$0")/../.env.prod" 22 | 23 | function send_notification() { 24 | curl -s \ 25 | --form-string "token=$PUSHOVER_APP_TOKEN" \ 26 | --form-string "user=$PUSHOVER_APP_KEY" \ 27 | --form-string "message=$1" \ 28 | https://api.pushover.net/1/messages.json 29 | } 30 | 31 | function error() { 32 | # don't run if notification was alreaty sent 33 | if [ -f $check_fn ]; then 34 | return 35 | fi 36 | 37 | echo "relayscan.io is unhealthy" 38 | send_notification "relayscan.io is unhealthy" 39 | curl -vvvv $url > $check_fn 2>&1 40 | } 41 | 42 | function reset() { 43 | # Don't run if there is no error 44 | if [ ! -f $check_fn ]; then 45 | return 46 | fi 47 | 48 | rm $check_fn 49 | echo "relayscan.io is healthy again" 50 | send_notification "relayscan.io is healthy again" 51 | } 52 | 53 | # Allow errors, to catch curl error exit code 54 | set +e 55 | # echo $check_cmd 56 | $check_cmd 57 | if [ $? -eq 0 ]; then 58 | echo "All good" 59 | reset 60 | else 61 | echo "curl error $?" 62 | error 63 | fi 64 | -------------------------------------------------------------------------------- /config-hoodi.yaml: -------------------------------------------------------------------------------- 1 | # Hoodi beacon chain genesis timestamp (Mar/17, 2025, 12:10 UTC) - https://github.com/eth-clients/hoodi 2 | genesis: 1742213400 3 | 4 | relays: 5 | flashbots: "https://0xafa4c6985aa049fb79dd37010438cfebeb0f2bd42b115b89dd678dab0670c1de38da0c4e9138c9290a398ecd9a0b3110@boost-relay-hoodi.flashbots.net" 6 | ultrasound: "https://0xb1559beef7b5ba3127485bbbb090362d9f497ba64e177ee2c8e7db74746306efad687f2cf8574e38d70067d40ef136dc@relay-hoodi.ultrasound.money" 7 | all: 8 | - "https://0xafa4c6985aa049fb79dd37010438cfebeb0f2bd42b115b89dd678dab0670c1de38da0c4e9138c9290a398ecd9a0b3110@boost-relay-hoodi.flashbots.net" 9 | - "https://0xb1559beef7b5ba3127485bbbb090362d9f497ba64e177ee2c8e7db74746306efad687f2cf8574e38d70067d40ef136dc@relay-hoodi.ultrasound.money" 10 | - "https://0x821f2a65afb70e7f2e820a925a9b4c80a159620582c1766b1b09729fec178b11ea22abb3a51f07b288be815a1a2ff516@bloxroute.hoodi.blxrbdn.com" 11 | - "https://0xaa58208899c6105603b74396734a6263cc7d947f444f396a90f7b7d3e65d102aec7e5e5291b27e08d02c50a050825c2f@hoodi.titanrelay.xyz" 12 | - "https://0x98f0ef62f00780cf8eb06701a7d22725b9437d4768bb19b363e882ae87129945ec206ec2dc16933f31d983f8225772b6@hoodi.aestus.live" 13 | - "https://0xb20c3fe59db9c3655088839ef3d972878d182eb745afd8abb1dd2abf6c14f93cd5934ed4446a5fe1ba039e2bc0cf1011@hoodi-relay.ethgas.com" 14 | # haven't had success getting data from TOOL + interstate, but these endpoints are supposed to work... 15 | # - "https://0x9110847c15a7f5c80a9fdd5db989a614cc01104e53bd8c252b6f46a4842c7fdef6b9593336035b5094878deff386804c@hoodi-builder-proxy-alpha.interstate.so:443" 16 | # - "https://0xa0f46566247ceb1f259a7189d5ac8bf2f0f07c135f081b0b5a9f226ef864bf6362c74306fcd02a87b7941f6feac57dc7@relay-hoodi.nuconstruct.xyz" 17 | 18 | builder_addresses: 19 | -------------------------------------------------------------------------------- /common/eth_node.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "math/big" 7 | 8 | ethcommon "github.com/ethereum/go-ethereum/common" 9 | "github.com/ethereum/go-ethereum/core/types" 10 | "github.com/ethereum/go-ethereum/ethclient" 11 | ) 12 | 13 | type EthNode struct { 14 | Clients []*ethclient.Client 15 | } 16 | 17 | func NewEthNode(uris ...string) (*EthNode, error) { 18 | if len(uris) == 0 { 19 | return nil, ErrURLEmpty 20 | } 21 | node := &EthNode{} //nolint:exhaustruct 22 | for _, uri := range uris { 23 | client, err := ethclient.Dial(uri) 24 | if err != nil { 25 | fmt.Println("Error connecting to eth node", uri, err) 26 | return nil, err 27 | } 28 | node.Clients = append(node.Clients, client) 29 | } 30 | return node, nil 31 | } 32 | 33 | func (n *EthNode) BlockByNumber(blockNumber int64) (block *types.Block, err error) { 34 | for _, client := range n.Clients { 35 | block, err = client.BlockByNumber(context.Background(), big.NewInt(blockNumber)) 36 | if err == nil { 37 | return block, nil 38 | } 39 | } 40 | return nil, err 41 | } 42 | 43 | func (n *EthNode) BlockByHash(blockHash string) (block *types.Block, err error) { 44 | for _, client := range n.Clients { 45 | block, err = client.BlockByHash(context.Background(), ethcommon.HexToHash(blockHash)) 46 | if err == nil { 47 | return block, nil 48 | } 49 | } 50 | return nil, err 51 | } 52 | 53 | func (n *EthNode) GetBalanceDiff(address string, blockNumber int64) (diff *big.Int, err error) { 54 | for _, client := range n.Clients { 55 | balanceBefore, err := client.BalanceAt(context.Background(), ethcommon.HexToAddress(address), big.NewInt(blockNumber-1)) 56 | if err != nil { 57 | continue 58 | } 59 | 60 | balanceAfter, err := client.BalanceAt(context.Background(), ethcommon.HexToAddress(address), big.NewInt(blockNumber)) 61 | if err != nil { 62 | continue 63 | } 64 | 65 | balanceDiff := new(big.Int).Sub(balanceAfter, balanceBefore) 66 | return balanceDiff, nil 67 | } 68 | return nil, err 69 | } 70 | -------------------------------------------------------------------------------- /vars/config.go: -------------------------------------------------------------------------------- 1 | package vars 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "gopkg.in/yaml.v3" 8 | ) 9 | 10 | // Config holds the application configuration loaded from YAML 11 | type Config struct { 12 | Genesis int64 `yaml:"genesis"` 13 | Relays RelaysConfig `yaml:"relays"` 14 | BuilderAddresses map[string][]string `yaml:"builder_addresses"` 15 | } 16 | 17 | // RelaysConfig holds relay URL configuration 18 | type RelaysConfig struct { 19 | Flashbots string `yaml:"flashbots"` 20 | Ultrasound string `yaml:"ultrasound"` 21 | All []string `yaml:"all"` 22 | } 23 | 24 | var loadedConfig *Config 25 | 26 | // LoadConfig loads the configuration from a YAML file 27 | func LoadConfig(path string) error { 28 | data, err := os.ReadFile(path) 29 | if err != nil { 30 | return fmt.Errorf("failed to read config file %s: %w", path, err) 31 | } 32 | 33 | var cfg Config 34 | if err := yaml.Unmarshal(data, &cfg); err != nil { 35 | return fmt.Errorf("failed to parse config file %s: %w", path, err) 36 | } 37 | 38 | loadedConfig = &cfg 39 | 40 | // Populate package-level variables for backwards compatibility 41 | Genesis = int(cfg.Genesis) 42 | RelayFlashbots = cfg.Relays.Flashbots 43 | RelayUltrasound = cfg.Relays.Ultrasound 44 | RelayURLs = cfg.Relays.All 45 | BuilderAddresses = buildAddressMap(cfg.BuilderAddresses) 46 | 47 | return nil 48 | } 49 | 50 | // MustLoadConfig loads the configuration or panics on error 51 | func MustLoadConfig(path string) { 52 | if err := LoadConfig(path); err != nil { 53 | panic(err) 54 | } 55 | } 56 | 57 | // GetConfig returns the loaded configuration 58 | func GetConfig() *Config { 59 | return loadedConfig 60 | } 61 | 62 | // buildAddressMap converts the config format to the expected map[coinbase]map[address]bool format 63 | func buildAddressMap(addresses map[string][]string) map[string]map[string]bool { 64 | result := make(map[string]map[string]bool) 65 | for coinbase, addrs := range addresses { 66 | result[coinbase] = make(map[string]bool) 67 | for _, addr := range addrs { 68 | result[coinbase][addr] = true 69 | } 70 | } 71 | return result 72 | } 73 | -------------------------------------------------------------------------------- /services/bidcollect/website/htmldata.go: -------------------------------------------------------------------------------- 1 | package website 2 | 3 | import ( 4 | "text/template" 5 | 6 | "github.com/flashbots/relayscan/common" 7 | ) 8 | 9 | type HTMLData struct { 10 | Title string 11 | Path string 12 | 13 | // Root page 14 | EthMainnetMonths []string 15 | 16 | // File-listing page 17 | CurrentNetwork string 18 | CurrentMonth string 19 | Files []FileEntry 20 | } 21 | 22 | type FileEntry struct { 23 | Filename string 24 | Size uint64 25 | Modified string 26 | } 27 | 28 | func prettyInt(i uint64) string { 29 | return printer.Sprintf("%d", i) 30 | } 31 | 32 | func caseIt(s string) string { 33 | return caser.String(s) 34 | } 35 | 36 | func percent(cnt, total uint64) string { 37 | p := float64(cnt) / float64(total) * 100 38 | return printer.Sprintf("%.2f", p) 39 | } 40 | 41 | func substr10(s string) string { 42 | return s[:10] 43 | } 44 | 45 | var DummyHTMLData = &HTMLData{ 46 | Title: "", 47 | Path: "", 48 | 49 | EthMainnetMonths: []string{ 50 | "2023-08", 51 | "2023-09", 52 | }, 53 | 54 | CurrentNetwork: "Ethereum Mainnet", 55 | CurrentMonth: "2023-08", 56 | Files: []FileEntry{ 57 | {"2023-08-29_all.csv.zip", 97210118, "02:02:23 2023-09-02"}, 58 | {"2023-08-29_top.csv.zip", 7210118, "02:02:23 2023-09-02"}, 59 | 60 | {"2023-08-30_all.csv.zip", 97210118, "02:02:23 2023-09-02"}, 61 | {"2023-08-30_top.csv.zip", 7210118, "02:02:23 2023-09-02"}, 62 | 63 | {"2023-08-31_all.csv.zip", 97210118, "02:02:23 2023-09-02"}, 64 | {"2023-08-31_top.csv.zip", 7210118, "02:02:23 2023-09-02"}, 65 | }, 66 | } 67 | 68 | var funcMap = template.FuncMap{ 69 | "prettyInt": prettyInt, 70 | "caseIt": caseIt, 71 | "percent": percent, 72 | "humanBytes": common.HumanBytes, 73 | "substr10": substr10, 74 | } 75 | 76 | func ParseIndexTemplate() (*template.Template, error) { 77 | return template.New("index.html").Funcs(funcMap).ParseFiles("services/bidcollect/website/templates/index_root.html", "services/bidcollect/website/templates/base.html") 78 | } 79 | 80 | func ParseFilesTemplate() (*template.Template, error) { 81 | return template.New("index.html").Funcs(funcMap).ParseFiles("services/bidcollect/website/templates/index_files.html", "services/bidcollect/website/templates/base.html") 82 | } 83 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | docker-image: 10 | name: Publish Docker Image 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout sources 15 | uses: actions/checkout@v2 16 | 17 | - name: Get tag version 18 | run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV 19 | 20 | - name: Print version 21 | run: | 22 | echo $RELEASE_VERSION 23 | echo ${{ env.RELEASE_VERSION }} 24 | 25 | - name: Set up QEMU 26 | uses: docker/setup-qemu-action@v2 27 | 28 | - name: Set up Docker Buildx 29 | uses: docker/setup-buildx-action@v2 30 | 31 | - name: Extract metadata (tags, labels) for Docker 32 | id: meta 33 | uses: docker/metadata-action@v4 34 | with: 35 | images: flashbots/relayscan 36 | tags: | 37 | type=sha 38 | type=pep440,pattern={{version}} 39 | type=pep440,pattern={{major}}.{{minor}} 40 | type=raw,value=latest,enable=${{ !contains(env.RELEASE_VERSION, '-') }} 41 | 42 | - name: Login to DockerHub 43 | uses: docker/login-action@v2 44 | with: 45 | username: ${{ secrets.DOCKERHUB_USERNAME }} 46 | password: ${{ secrets.DOCKERHUB_TOKEN }} 47 | 48 | - name: Build and push 49 | uses: docker/build-push-action@v3 50 | with: 51 | context: . 52 | push: true 53 | build-args: | 54 | VERSION=${{ env.RELEASE_VERSION }} 55 | platforms: linux/amd64,linux/arm64 56 | tags: ${{ steps.meta.outputs.tags }} 57 | labels: ${{ steps.meta.outputs.labels }} 58 | 59 | github-release: 60 | runs-on: ubuntu-latest 61 | steps: 62 | - name: Checkout sources 63 | uses: actions/checkout@v2 64 | 65 | - name: Create release 66 | id: create_release 67 | uses: actions/create-release@v1 68 | env: 69 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 70 | with: 71 | tag_name: ${{ github.ref }} 72 | release_name: ${{ github.ref }} 73 | draft: true 74 | prerelease: false 75 | -------------------------------------------------------------------------------- /common/request.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "errors" 8 | "fmt" 9 | "io" 10 | "net/http" 11 | ) 12 | 13 | var errHTTPErrorResponse = errors.New("HTTP error response") 14 | 15 | // type ErrorMessage struct { 16 | // code int 17 | // message string 18 | // } 19 | 20 | func SendHTTPRequest(ctx context.Context, client http.Client, method, url string, payload, dst any) (code int, err error) { 21 | var req *http.Request 22 | 23 | if payload == nil { 24 | req, err = http.NewRequestWithContext(ctx, method, url, nil) 25 | } else { 26 | payloadBytes, err2 := json.Marshal(payload) 27 | if err2 != nil { 28 | return 0, fmt.Errorf("could not marshal request: %w", err2) 29 | } 30 | req, err = http.NewRequestWithContext(ctx, method, url, bytes.NewReader(payloadBytes)) 31 | 32 | // Set content-type 33 | req.Header.Add("Content-Type", "application/json") 34 | } 35 | if err != nil { 36 | return 0, fmt.Errorf("could not prepare request: %w", err) 37 | } 38 | 39 | // Execute request 40 | resp, err := client.Do(req) 41 | if err != nil { 42 | return 0, err 43 | } 44 | defer resp.Body.Close() //nolint:errcheck 45 | 46 | if resp.StatusCode == http.StatusNoContent { 47 | return resp.StatusCode, nil 48 | } 49 | 50 | if resp.StatusCode > 299 { 51 | bodyBytes, err := io.ReadAll(resp.Body) 52 | if err != nil { 53 | return resp.StatusCode, fmt.Errorf("could not read error response body for status code %d: %w", resp.StatusCode, err) 54 | } 55 | return resp.StatusCode, fmt.Errorf("%w: %d / %s", errHTTPErrorResponse, resp.StatusCode, string(bodyBytes)) 56 | } 57 | 58 | if dst == nil { 59 | // still read the body to reuse http connection (see also https://stackoverflow.com/a/17953506) 60 | _, err = io.Copy(io.Discard, resp.Body) 61 | if err != nil { 62 | return resp.StatusCode, fmt.Errorf("could not read response body: %w", err) 63 | } 64 | } else { 65 | bodyBytes, err := io.ReadAll(resp.Body) 66 | if err != nil { 67 | return resp.StatusCode, fmt.Errorf("could not read response body: %w", err) 68 | } 69 | 70 | if err := json.Unmarshal(bodyBytes, dst); err != nil { 71 | return resp.StatusCode, fmt.Errorf("could not unmarshal response %s: %w", string(bodyBytes), err) 72 | } 73 | } 74 | 75 | return resp.StatusCode, nil 76 | } 77 | -------------------------------------------------------------------------------- /services/website/html.go: -------------------------------------------------------------------------------- 1 | package website 2 | 3 | import ( 4 | _ "embed" 5 | "text/template" 6 | "time" 7 | 8 | "github.com/dustin/go-humanize" 9 | "github.com/flashbots/relayscan/database" 10 | ) 11 | 12 | type Stats struct { 13 | Since time.Time 14 | Until time.Time 15 | 16 | TimeStr string // i.e. 24h, 12h, 1h or 7d 17 | 18 | TopRelays []*database.TopRelayEntry 19 | TopBuilders []*TopBuilderDisplayEntry 20 | BuilderProfits []*database.BuilderProfitEntry 21 | TopBuildersByRelay map[string][]*TopBuilderDisplayEntry 22 | } 23 | 24 | func NewStats() *Stats { 25 | return &Stats{ 26 | TopRelays: make([]*database.TopRelayEntry, 0), 27 | TopBuilders: make([]*TopBuilderDisplayEntry, 0), 28 | BuilderProfits: make([]*database.BuilderProfitEntry, 0), 29 | TopBuildersByRelay: make(map[string][]*TopBuilderDisplayEntry), 30 | } 31 | } 32 | 33 | type HTMLData struct { 34 | Title string 35 | TimeSpans []string 36 | TimeSpan string 37 | View string // overview or builder-profit 38 | 39 | Stats *Stats // stats for this view 40 | 41 | LastUpdateSlot uint64 42 | LastUpdateTime time.Time 43 | LastUpdateTimeStr string 44 | } 45 | 46 | type HTMLDataDailyStats struct { 47 | Title string 48 | 49 | Day string 50 | DayPrev string 51 | DayNext string 52 | TimeSince string 53 | TimeUntil string 54 | 55 | TopRelays []*database.TopRelayEntry 56 | TopBuildersBySummary []*TopBuilderDisplayEntry 57 | BuilderProfits []*database.BuilderProfitEntry 58 | } 59 | 60 | var funcMap = template.FuncMap{ 61 | "weiToEth": weiToEth, 62 | "prettyInt": prettyInt, 63 | "caseIt": caseIt, 64 | "percent": percent, 65 | "relayTable": relayTable, 66 | "builderTable": builderTable, 67 | "builderProfitTable": builderProfitTable, 68 | "humanTime": humanize.Time, 69 | "lowercaseNoWhitespace": lowercaseNoWhitespace, 70 | } 71 | 72 | func ParseIndexTemplate() (*template.Template, error) { 73 | return template.New("index.html").Funcs(funcMap).ParseFiles("services/website/templates/index.html", "services/website/templates/base.html") 74 | } 75 | 76 | func ParseDailyStatsTemplate() (*template.Template, error) { 77 | return template.New("daily-stats.html").Funcs(funcMap).ParseFiles("services/website/templates/daily-stats.html", "services/website/templates/base.html") 78 | } 79 | -------------------------------------------------------------------------------- /config-mainnet.yaml: -------------------------------------------------------------------------------- 1 | # Mainnet beacon chain genesis timestamp (Dec 1, 2020) 2 | genesis: 1606824023 3 | 4 | relays: 5 | flashbots: "https://0xac6e77dfe25ecd6110b8e780608cce0dab71fdd5ebea22a16c0205200f2f8e2e3ad3b71d3499c54ad14d6c21b41a37ae@boost-relay.flashbots.net" 6 | ultrasound: "https://0xa1559ace749633b997cb3fdacffb890aeebdb0f5a3b6aaa7eeeaf1a38af0a8fe88b9e4b1f61f236d2e64d95733327a62@relay.ultrasound.money" 7 | all: 8 | - "https://0xac6e77dfe25ecd6110b8e780608cce0dab71fdd5ebea22a16c0205200f2f8e2e3ad3b71d3499c54ad14d6c21b41a37ae@boost-relay.flashbots.net" 9 | - "https://0xa1559ace749633b997cb3fdacffb890aeebdb0f5a3b6aaa7eeeaf1a38af0a8fe88b9e4b1f61f236d2e64d95733327a62@relay.ultrasound.money" 10 | - "https://0x8b5d2e73e2a3a55c6c87b8b6eb92e0149a125c852751db1422fa951e42a09b82c142c3ea98d0d9930b056a3bc9896b8f@bloxroute.max-profit.blxrbdn.com" 11 | - "https://0xb0b07cd0abef743db4260b0ed50619cf6ad4d82064cb4fbec9d3ec530f7c5e6793d9f286c4e082c0244ffb9f2658fe88@bloxroute.regulated.blxrbdn.com" 12 | - "https://0xb3ee7afcf27f1f1259ac1787876318c6584ee353097a50ed84f51a1f21a323b3736f271a895c7ce918c038e4265918be@relay.edennetwork.io" 13 | - "https://0xa7ab7a996c8584251c8f925da3170bdfd6ebc75d50f5ddc4050a6fdc77f2a3b5fce2cc750d0865e05d7228af97d69561@agnostic-relay.net" 14 | - "https://0xa15b52576bcbf1072f4a011c0f99f9fb6c66f3e1ff321f11f461d15e31b1cb359caa092c71bbded0bae5b5ea401aab7e@aestus.live" 15 | - "https://0x8c4ed5e24fe5c6ae21018437bde147693f68cda427cd1122cf20819c30eda7ed74f72dece09bb313f2a1855595ab677d@titanrelay.xyz" 16 | - "https://0x88ef3061f598101ca713d556cf757763d9be93d33c3092d3ab6334a36855b6b4a4020528dd533a62d25ea6648251e62e@relay.ethgas.com" 17 | - "https://0xb66921e917a8f4cfc3c52e10c1e5c77b1255693d9e6ed6f5f444b71ca4bb610f2eff4fa98178efbf4dd43a30472c497e@relay.btcs.com" 18 | 19 | builder_addresses: 20 | # Coinbase addresses mapped to their owned addresses 21 | "0xdadb0d80178819f2319190d340ce9a924f783711": 22 | - "0x59cadf9199248b50d40a6891c9e329ea13a88d31" 23 | - "0x75cc09358f100583d66f5277138bfb476345dc1b" 24 | - "0x397b28d85d77fef1576e129bb35b322c2bee1ba1" 25 | "0x4838b106fce9647bdf1e7877bf73ce8b0bad5f97": 26 | - "0x9fc3da866e7df3a1c57ade1a97c9f00a70f010c8" 27 | - "0xb29b9fd58cdb2e3bb068bc8560d8c13b2454684d" 28 | "0x1f9090aae28b8a3dceadf281b0f12828e676c326": 29 | - "0x0affb0a96fbefaa97dce488dfd97512346cf3ab8" 30 | "0x95222290dd7278aa3ddd389cc1e1d165cc4bafe5": 31 | - "0xa83114a443da1cecefc50368531cace9f37fcccb" 32 | - "0x28c74c0f29b686f21ea731bd2a8b88b6954475ba" 33 | -------------------------------------------------------------------------------- /services/bidcollect/website/utils.go: -------------------------------------------------------------------------------- 1 | package website 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "time" 7 | 8 | "go.uber.org/zap" 9 | "golang.org/x/text/cases" 10 | "golang.org/x/text/language" 11 | "golang.org/x/text/message" 12 | ) 13 | 14 | var ( 15 | // Printer for pretty printing numbers 16 | printer = message.NewPrinter(language.English) 17 | 18 | // Caser is used for casing strings 19 | caser = cases.Title(language.English) 20 | ) 21 | 22 | type HTTPErrorResp struct { 23 | Code int `json:"code"` 24 | Message string `json:"message"` 25 | } 26 | 27 | // responseWriter is a minimal wrapper for http.ResponseWriter that allows the 28 | // written HTTP status code to be captured for logging. 29 | type responseWriter struct { 30 | http.ResponseWriter 31 | status int 32 | wroteHeader bool 33 | } 34 | 35 | func wrapResponseWriter(w http.ResponseWriter) *responseWriter { 36 | return &responseWriter{ResponseWriter: w} //nolint:exhaustruct 37 | } 38 | 39 | func (rw *responseWriter) Status() int { 40 | return rw.status 41 | } 42 | 43 | func (rw *responseWriter) WriteHeader(code int) { 44 | if rw.wroteHeader { 45 | return 46 | } 47 | 48 | rw.status = code 49 | rw.ResponseWriter.WriteHeader(code) 50 | rw.wroteHeader = true 51 | } 52 | 53 | // LoggingMiddlewareZap logs the incoming HTTP request & its duration. 54 | func LoggingMiddlewareZap(logger *zap.Logger, next http.Handler) http.Handler { 55 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 56 | // Handle panics 57 | defer func() { 58 | if msg := recover(); msg != nil { 59 | w.WriteHeader(http.StatusInternalServerError) 60 | var method, url string 61 | if r != nil { 62 | method = r.Method 63 | url = r.URL.EscapedPath() 64 | } 65 | logger.Error("HTTP request handler panicked", 66 | zap.Any("error", msg), 67 | zap.String("method", method), 68 | zap.String("url", url), 69 | ) 70 | } 71 | }() 72 | 73 | start := time.Now() 74 | wrapped := wrapResponseWriter(w) 75 | next.ServeHTTP(w, r) 76 | 77 | // Passing request stats both in-message (for the human reader) 78 | // as well as inside the structured log (for the machine parser) 79 | logger.Info(fmt.Sprintf("%s %s %d", r.Method, r.URL.EscapedPath(), wrapped.status), 80 | zap.Int("durationMs", int(time.Since(start).Milliseconds())), 81 | zap.Int("status", wrapped.status), 82 | zap.String("logType", "access"), 83 | zap.String("method", r.Method), 84 | zap.String("path", r.URL.EscapedPath()), 85 | ) 86 | }) 87 | } 88 | -------------------------------------------------------------------------------- /.golangci.yaml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | linters: 3 | enable-all: true 4 | disable: 5 | - cyclop 6 | - forbidigo 7 | - funlen 8 | - gochecknoglobals 9 | - gochecknoinits 10 | - gocritic 11 | - godot 12 | - godox 13 | - lll 14 | - nestif 15 | - nilnil 16 | - nlreturn 17 | - noctx 18 | - nonamedreturns 19 | - paralleltest 20 | - revive 21 | - testpackage 22 | - unparam 23 | - varnamelen 24 | - wrapcheck 25 | - wsl 26 | - interfacebloat 27 | - dupword 28 | - mnd 29 | 30 | # 31 | # Disabled because of generics: 32 | # 33 | - contextcheck 34 | - rowserrcheck 35 | - sqlclosecheck 36 | - wastedassign 37 | 38 | # 39 | # Disabled because deprecated: 40 | # 41 | 42 | linters-settings: 43 | # 44 | # The G108 rule throws a false positive. We're not actually vulnerable. If 45 | # you're not careful the profiling endpoint is automatically exposed on 46 | # /debug/pprof if you import net/http/pprof. See this link: 47 | # 48 | # https://mmcloughlin.com/posts/your-pprof-is-showing 49 | # 50 | gosec: 51 | excludes: 52 | - G108 53 | 54 | gocognit: 55 | min-complexity: 100 # default: 30 56 | 57 | gocyclo: 58 | min-complexity: 33 # default: 30 59 | 60 | maintidx: 61 | under: 15 62 | 63 | tagliatelle: 64 | case: 65 | rules: 66 | json: snake 67 | 68 | gofumpt: 69 | extra-rules: true 70 | 71 | exhaustruct: 72 | exclude: 73 | # 74 | # Because it's easier to read without the other fields. 75 | # 76 | - 'GetPayloadsFilters' 77 | 78 | # 79 | # Structures outside our control that have a ton of settings. It doesn't 80 | # make sense to specify all of the fields. 81 | # 82 | - 'cobra.Command' 83 | - 'database.*Entry' 84 | - 'http.Server' 85 | - 'logrus.*Formatter' 86 | - 'Options' # redis 87 | 88 | # 89 | # Excluded because there are private fields (not capitalized) that are 90 | # not initialized. If possible, I think these should be altered. 91 | # 92 | - 'Datastore' 93 | - 'Housekeeper' 94 | - 'MockBeaconClient' 95 | - 'RelayAPI' 96 | - 'Webserver' 97 | 98 | formatters: 99 | enable: 100 | - gci 101 | - gofmt 102 | - gofumpt 103 | - goimports 104 | settings: 105 | gofumpt: 106 | extra-rules: true 107 | exclusions: 108 | generated: lax 109 | paths: 110 | - third_party$ 111 | - builtin$ 112 | - examples$ 113 | -------------------------------------------------------------------------------- /scripts/bidcollect/bids-combine-and-upload.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Combine bid CSVs (from bidcollect) into a single CSV, and upload to R2/S3 4 | # 5 | set -e 6 | 7 | # require directory as first argument 8 | if [ -z "$1" ]; then 9 | echo "Usage: $0
112 |
113 | 115 | Docs · github.com/flashbots/relayscan 116 |
117 |Illuminate, Democratize, Distribute
118 |{{ .TimeSince }} {{ .TimeUntil }} (UTC)
7 |8 | prev day | 9 | {{ if ne .DayNext "" }}next day {{ else }}next day {{ end}} 10 |
11 || Relay | 23 |Payloads | 24 |Percent | 25 |26 | |
|---|---|---|---|
| {{ .Relay }} | 32 |{{ .NumPayloads | prettyInt }} | 33 |{{ .Percent }} % | 34 |
| Builder (extra_data) | 47 |Blocks | 48 |Percent | 49 ||
|---|---|---|---|
| {{ if .Info.ExtraData }}{{ .Info.ExtraData }}{{ else }} {{ end }} | 55 |{{ .Info.NumBlocks | prettyInt }} | 56 |{{ .Info.Percent }} % | 57 |{{ if gt (len .Children) 1 }}{{ end }} | 58 |
| {{ .ExtraData }} | 63 |{{ .NumBlocks | prettyInt }} | 64 |{{ .Percent }} % | 65 |66 | |
| Builder extra_data | 80 |Blocks | 81 |Blocks with profit | 82 |Blocks with subsidy | 83 |Overall profit (ETH) | 84 |Subsidies (ETH) | 85 |
|---|---|---|---|---|---|
|
91 | {{ if .ExtraData }}{{ .ExtraData }}{{ else }} {{ end }}
92 | {{ if ne (len .Aliases) 0 }}
93 |
94 |
95 |
96 | extra_data values:
97 |
104 |
105 | {{ end }}
106 |
|
107 | {{ .NumBlocks | prettyInt }} | 108 |{{ .NumBlocksProfit | prettyInt }} | 109 |{{ .NumBlocksSubsidised | prettyInt }} | 110 |{{ .ProfitTotal }} | 111 |{{ .SubsidiesTotal }} | 112 |
⚠️ Disclaimer: Relayscan uses block.coinbase ETH balance difference to measure builder's profits which could introduce inaccuracies when builders:
119 | 120 |