diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8f462 --- /dev/null +++ b/.gitignore @@ -0,0 +1,22 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib +nft-collect +# Test binary, built with `go tests -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ +assets/ +log/ +uploads/ +config/config.yaml +config/config.docker.yaml +.idea +nft-collect \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..cb60eba --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +# nft-collect +![](https://img.shields.io/badge/license-MIT-green) +[![goreportcard for backend-go](https://goreportcard.com/badge/github.com/decert-me/nft-collect)](https://goreportcard.com/report/github.com/decert-me/nft-collect) +## 安装 +```bash +git clone https://github.com/decert-me/nft-collect.git +``` +## 编译 +```bash +go build +``` +## 配置 +```bash +# 主程序配置 +cp ./config/config.demo.yaml ./config/config.yaml +vi ./config/config.yaml +``` +## 运行 +```bash +./nft-collect +``` diff --git a/config/config.demo.yaml b/config/config.demo.yaml new file mode 100644 index 0000000..9644c83 --- /dev/null +++ b/config/config.demo.yaml @@ -0,0 +1,74 @@ +# system configuration +system: + env: develop + addr: 8888 + +# pgsql configuration +pgsql: + path: "127.0.0.1" + port: "5432" + config: "" + db-name: "nft_collect" + username: "postgres" + password: "123456" + max-idle-conns: 10 + max-open-conns: 100 + log-mode: "info" + log-zap: false + +# auth configuration +jwt: + signing-key: "Decert" + expires-time: 86400 + issuer: "Decert" + +# zap configuration +zap: + level: info + format: console + prefix: '[nft-collect]' + director: log + show-line: true + encode-level: LowercaseColorLevelEncoder + stacktrace-key: stacktrace + log-in-console: true + +# local configuration +local: + path: 'uploads/file' + ipfs: 'uploads/ipfs' + +# nft +nft: + ens-rpc: "https://rpc.ankr.com/eth" + api-key: "" + api-key-backup: "" + api-key-pro: "" + cache-time: 15 + logo-path: "assets" + def-contract: ["0x37da9ea159f5c95923ccc65ecae857c2584a899a::polygon","0x8cc6517e45db7a0803fef220d9b577326a12033f::eth","0x2723522702093601e6360cae665518c4f63e9da6::bnb"] + api-config: + - chain: "eth" + chain-id: 1 + api-per-host: "restapi" + symbol: "ETH" + - chain: "bnb" + chain-id: 56 + api-per-host: "bnbapi" + symbol: "BNB" + - chain: "polygon" + chain-id: 137 + api-per-host: "polygonapi" + symbol: "MATIC" + - chain: "arbitrum" + chain-id: 42161 + api-per-host: "arbitrumapi" + symbol: "Arbitrum" + - chain: "optimism" + chain-id: 10 + api-per-host: "optimismapi" + symbol: "Optimism" + - chain: "gnosis" + chain-id: 100 + api-per-host: "gnosisapi" + symbol: "XDAI" \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..22756a2 --- /dev/null +++ b/go.mod @@ -0,0 +1,109 @@ +module nft-collect + +go 1.19 + +require ( + github.com/allegro/bigcache/v3 v3.1.0 + github.com/chenyahui/gin-cache v1.8.0 + github.com/ethereum/go-ethereum v1.10.26 + github.com/fsnotify/fsnotify v1.6.0 + github.com/fvbock/endless v0.0.0-20170109170031-447134032cb6 + github.com/gin-contrib/pprof v1.4.0 + github.com/gin-gonic/gin v1.8.2 + github.com/golang-jwt/jwt/v4 v4.3.0 + github.com/imroc/req/v3 v3.32.0 + github.com/lib/pq v1.10.7 + github.com/natefinch/lumberjack v2.0.0+incompatible + github.com/pkg/errors v0.9.1 + github.com/spf13/viper v1.15.0 + github.com/tidwall/gjson v1.14.4 + github.com/wealdtech/go-ens/v3 v3.5.5 + go.uber.org/zap v1.24.0 + golang.org/x/exp v0.0.0-20230206171751-46f607a40771 + gorm.io/driver/postgres v1.4.7 + gorm.io/gorm v1.24.6 +) + +require ( + github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6 // indirect + github.com/btcsuite/btcd/btcec/v2 v2.2.0 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/deckarep/golang-set v1.8.0 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-ole/go-ole v1.2.1 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.11.2 // indirect + github.com/go-redis/redis/v8 v8.11.5 // indirect + github.com/go-stack/stack v1.8.0 // indirect + github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect + github.com/goccy/go-json v0.10.0 // indirect + github.com/golang/mock v1.6.0 // indirect + github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect + github.com/google/uuid v1.2.0 // indirect + github.com/gorilla/websocket v1.4.2 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/ipfs/go-cid v0.2.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect + github.com/jackc/pgx/v5 v5.2.0 // indirect + github.com/jellydator/ttlcache/v2 v2.11.1 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.0.9 // indirect + github.com/leodido/go-urn v1.2.1 // indirect + github.com/magiconair/properties v1.8.7 // indirect + github.com/mattn/go-isatty v0.0.17 // indirect + github.com/minio/sha256-simd v1.0.0 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/mr-tron/base58 v1.2.0 // indirect + github.com/multiformats/go-base32 v0.0.3 // indirect + github.com/multiformats/go-base36 v0.1.0 // indirect + github.com/multiformats/go-multibase v0.1.1 // indirect + github.com/multiformats/go-multihash v0.2.0 // indirect + github.com/multiformats/go-varint v0.0.6 // indirect + github.com/onsi/ginkgo/v2 v2.2.0 // indirect + github.com/pelletier/go-toml/v2 v2.0.6 // indirect + github.com/quic-go/qpack v0.4.0 // indirect + github.com/quic-go/qtls-go1-18 v0.2.0 // indirect + github.com/quic-go/qtls-go1-19 v0.2.0 // indirect + github.com/quic-go/qtls-go1-20 v0.1.0 // indirect + github.com/quic-go/quic-go v0.32.0 // indirect + github.com/rjeczalik/notify v0.9.1 // indirect + github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect + github.com/spaolacci/murmur3 v1.1.0 // indirect + github.com/spf13/afero v1.9.3 // indirect + github.com/spf13/cast v1.5.0 // indirect + github.com/spf13/jwalterweatherman v1.1.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/subosito/gotenv v1.4.2 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.0 // indirect + github.com/tklauser/go-sysconf v0.3.5 // indirect + github.com/tklauser/numcpus v0.2.2 // indirect + github.com/ugorji/go/codec v1.2.9 // indirect + github.com/wealdtech/go-multicodec v1.4.0 // indirect + go.uber.org/atomic v1.10.0 // indirect + go.uber.org/multierr v1.8.0 // indirect + golang.org/x/crypto v0.6.0 // indirect + golang.org/x/mod v0.6.0 // indirect + golang.org/x/net v0.7.0 // indirect + golang.org/x/sync v0.1.0 // indirect + golang.org/x/sys v0.5.0 // indirect + golang.org/x/text v0.7.0 // indirect + golang.org/x/tools v0.2.0 // indirect + google.golang.org/protobuf v1.28.1 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect + gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + lukechampine.com/blake3 v1.1.6 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..bc0081c --- /dev/null +++ b/go.sum @@ -0,0 +1,796 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6 h1:fLjPD/aNc3UIOA6tDi6QXUemppXK3P9BI7mr2hd6gx8= +github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= +github.com/VictoriaMetrics/fastcache v1.6.0 h1:C/3Oi3EiBCqufydp1neRZkqcwmEiuRT9c3fqvvgKm5o= +github.com/allegro/bigcache/v3 v3.1.0 h1:H2Vp8VOvxcrB91o86fUSVJFqeuz8kpyyB02eH3bSzwk= +github.com/allegro/bigcache/v3 v3.1.0/go.mod h1:aPyh7jEvrog9zAwx5N7+JUQX5dZTSGpxF1LAR4dr35I= +github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= +github.com/btcsuite/btcd/btcec/v2 v2.2.0 h1:fzn1qaOt32TuLjFlkzYSsBC35Q3KUjT1SwPxiMSCF5k= +github.com/btcsuite/btcd/btcec/v2 v2.2.0/go.mod h1:U7MHm051Al6XmscBQ0BoNydpOTsFAn707034b5nY8zU= +github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 h1:q0rUy8C/TYNBQS1+CGKw68tLOFYSNEs0TFnxxnS9+4U= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/cp v0.1.0 h1:SE+dxFebS7Iik5LK0tsi1k9ZCxEaFX4AjQmoyA+1dJk= +github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chenyahui/gin-cache v1.8.0 h1:OnjQcUOLUfhVhQjMNfZSLbSgcPzEnOsKQS52Gh2JQiI= +github.com/chenyahui/gin-cache v1.8.0/go.mod h1:eEAwR4874QJI3dY7rdkoartzwVD0e1iq8wEJaEbzA64= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/deckarep/golang-set v1.8.0 h1:sk9/l/KqpunDwP7pSjUg0keiOOLEnOBHzykLrsPppp4= +github.com/deckarep/golang-set v1.8.0/go.mod h1:5nI87KwE7wgsBU1F4GKAw2Qod7p5kyS383rP6+o6qqo= +github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0= +github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/edsrzf/mmap-go v1.0.0 h1:CEBF7HpRnUCSJgGUb5h1Gm7e3VkmVDrR8lvWVLtrOFw= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/ethereum/go-ethereum v1.10.26 h1:i/7d9RBBwiXCEuyduBQzJw/mKmnvzsN14jqBmytw72s= +github.com/ethereum/go-ethereum v1.10.26/go.mod h1:EYFyF19u3ezGLD4RqOkLq+ZCXzYbLoNDdZlMt7kyKFg= +github.com/fjl/memsize v0.0.0-20190710130421-bcb5799ab5e5 h1:FtmdgXiUlNeRsoNMFlKLDt+S+6hbjVMEW6RGQ7aUf7c= +github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +github.com/fvbock/endless v0.0.0-20170109170031-447134032cb6 h1:6VSn3hB5U5GeA6kQw4TwWIWbOhtvR2hmbBJnTOtqTWc= +github.com/fvbock/endless v0.0.0-20170109170031-447134032cb6/go.mod h1:YxOVT5+yHzKvwhsiSIWmbAYM3Dr9AEEbER2dVayfBkg= +github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff h1:tY80oXqGNY4FhTFhk+o9oFHGINQ/+vhlm8HFzi6znCI= +github.com/gin-contrib/pprof v1.4.0 h1:XxiBSf5jWZ5i16lNOPbMTVdgHBdhfGRD5PZ1LWazzvg= +github.com/gin-contrib/pprof v1.4.0/go.mod h1:RrehPJasUVBPK6yTUwOl8/NP6i0vbUgmxtis+Z5KE90= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U= +github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk= +github.com/gin-gonic/gin v1.8.2 h1:UzKToD9/PoFj/V4rvlKqTRKnQYyz8Sc1MJlv4JHPtvY= +github.com/gin-gonic/gin v1.8.2/go.mod h1:qw5AYuDrzRTnhvusDsrov+fDIxp9Dleuu12h8nfB398= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-ole/go-ole v1.2.1 h1:2lOsA72HgjxAuMlKpFiCbHTvu44PIVkZ5hqm3RSdI/E= +github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= +github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos= +github.com/go-playground/validator/v10 v10.11.2 h1:q3SHpufmypg+erIExEKUmsgmhDTyhcJ38oeKGACXohU= +github.com/go-playground/validator/v10 v10.11.2/go.mod h1:NieE624vt4SCTJtD87arVLvdmjPAeV8BQlHtMnw9D7s= +github.com/go-redis/redis/v8 v8.11.4/go.mod h1:2Z2wHZXdQpCDXEGzqMockDpNyYvi2l4Pxt6RJr792+w= +github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= +github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= +github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/goccy/go-json v0.10.0 h1:mXKd9Qw4NuzShiRlOXKews24ufknHO7gx30lsDyokKA= +github.com/goccy/go-json v0.10.0/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/golang-jwt/jwt/v4 v4.3.0 h1:kHL1vqdqWNfATmA0FNMdmZNMyZI1U6O31X4rlIPoBog= +github.com/golang-jwt/jwt/v4 v4.3.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs= +github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= +github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-bexpr v0.1.10 h1:9kuI5PFotCboP3dkDYFr/wi0gg0QVbSNz5oFRpxn4uE= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.5-0.20210104140557-80c98217689d h1:dg1dEPuWpEqDnvIw251EVy4zlP8gWbsGj4BsUKCRpYs= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao= +github.com/holiman/uint256 v1.2.0 h1:gpSYcPLWGv4sG43I2mVLiDZCNDh/EpGjSk8tmtxitHM= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/huin/goupnp v1.0.3 h1:N8No57ls+MnjlB+JPiCVSOyy/ot7MJTqlo7rn+NYSqQ= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/imroc/req/v3 v3.32.0 h1:jDGm2pVE8e/PExrJTvxEjOmgItVBJqvt3YjTUtZKyQM= +github.com/imroc/req/v3 v3.32.0/go.mod h1:cZ+7C3L/AYOr4tLGG16hZF90F1WzAdAdzt1xFSlizXY= +github.com/ipfs/go-cid v0.2.0 h1:01JTiihFq9en9Vz0lc0VDWvZe/uBonGpzo4THP0vcQ0= +github.com/ipfs/go-cid v0.2.0/go.mod h1:P+HXFDF4CVhaVayiEb4wkAy7zBHxBwsJyt0Y5U6MLro= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.2.0 h1:NdPpngX0Y6z6XDFKqmFQaE+bCtkqzvQIOt1wvBlAqs8= +github.com/jackc/pgx/v5 v5.2.0/go.mod h1:Ptn7zmohNsWEsdxRawMzk3gaKma2obW+NWTnKa0S4nk= +github.com/jackc/puddle/v2 v2.1.2/go.mod h1:2lpufsF5mRHO6SuZkm0fNYxM6SWHfvyFj62KwNzgels= +github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= +github.com/jellydator/ttlcache/v2 v2.11.1 h1:AZGME43Eh2Vv3giG6GeqeLeFXxwxn1/qHItqWZl6U64= +github.com/jellydator/ttlcache/v2 v2.11.1/go.mod h1:RtE5Snf0/57e+2cLWFYWCCsLas2Hy3c5Z4n14XmSvTI= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= +github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= +github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw= +github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= +github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= +github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g= +github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/pointerstructure v1.2.0 h1:O+i9nHnXS3l/9Wu7r4NrEdwA2VFTicjUEN1uBnDo34A= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= +github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= +github.com/multiformats/go-base32 v0.0.3 h1:tw5+NhuwaOjJCC5Pp82QuXbrmLzWg7uxlMFp8Nq/kkI= +github.com/multiformats/go-base32 v0.0.3/go.mod h1:pLiuGC8y0QR3Ue4Zug5UzK9LjgbkL8NSQj0zQ5Nz/AA= +github.com/multiformats/go-base36 v0.1.0 h1:JR6TyF7JjGd3m6FbLU2cOxhC0Li8z8dLNGQ89tUg4F4= +github.com/multiformats/go-base36 v0.1.0/go.mod h1:kFGE83c6s80PklsHO9sRn2NCoffoRdUUOENyW/Vv6sM= +github.com/multiformats/go-multibase v0.1.1 h1:3ASCDsuLX8+j4kx58qnJ4YFq/JWTJpCyDW27ztsVTOI= +github.com/multiformats/go-multibase v0.1.1/go.mod h1:ZEjHE+IsUrgp5mhlEAYjMtZwK1k4haNkcaPg9aoe1a8= +github.com/multiformats/go-multihash v0.2.0 h1:oytJb9ZA1OUW0r0f9ea18GiaPOo4SXyc7p2movyUuo4= +github.com/multiformats/go-multihash v0.2.0/go.mod h1:WxoMcYG85AZVQUyRyo9s4wULvW5qrI9vb2Lt6evduFc= +github.com/multiformats/go-varint v0.0.6 h1:gk85QWKxh3TazbLxED/NlDVv8+q+ReFJk7Y2W/KhfNY= +github.com/multiformats/go-varint v0.0.6/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXSrVKRY101jdMZYE= +github.com/natefinch/lumberjack v2.0.0+incompatible h1:4QJd3OLAMgj7ph+yZTuX13Ld4UpgHp07nNdFX7mqFfM= +github.com/natefinch/lumberjack v2.0.0+incompatible/go.mod h1:Wi9p2TTF5DG5oU+6YfsmYQpsTIOm0B1VNzQg9Mw6nPk= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo/v2 v2.2.0 h1:3ZNA3L1c5FYDFTTxbFeVGGD8jYvjYauHD30YgLxVsNI= +github.com/onsi/ginkgo/v2 v2.2.0/go.mod h1:MEH45j8TBi6u9BMogfbp0stKC5cdGjumZj5Y7AG4VIk= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= +github.com/onsi/gomega v1.20.1 h1:PA/3qinGoukvymdIDV8pii6tiZgC8kbmJO6Z5+b002Q= +github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= +github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU= +github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/tsdb v0.7.1 h1:YZcsG11NqnK4czYLrWd9mpEuAJIHVQLwdrleYfszMAA= +github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo= +github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A= +github.com/quic-go/qtls-go1-18 v0.2.0 h1:5ViXqBZ90wpUcZS0ge79rf029yx0dYB0McyPJwqqj7U= +github.com/quic-go/qtls-go1-18 v0.2.0/go.mod h1:moGulGHK7o6O8lSPSZNoOwcLvJKJ85vVNc7oJFD65bc= +github.com/quic-go/qtls-go1-19 v0.2.0 h1:Cvn2WdhyViFUHoOqK52i51k4nDX8EwIh5VJiVM4nttk= +github.com/quic-go/qtls-go1-19 v0.2.0/go.mod h1:ySOI96ew8lnoKPtSqx2BlI5wCpUVPT05RMAlajtnyOI= +github.com/quic-go/qtls-go1-20 v0.1.0 h1:d1PK3ErFy9t7zxKsG3NXBJXZjp/kMLoIb3y/kV54oAI= +github.com/quic-go/qtls-go1-20 v0.1.0/go.mod h1:JKtK6mjbAVcUTN/9jZpvLbGxvdWIKS8uT7EiStoU1SM= +github.com/quic-go/quic-go v0.32.0 h1:lY02md31s1JgPiiyfqJijpu/UX/Iun304FI3yUqX7tA= +github.com/quic-go/quic-go v0.32.0/go.mod h1:/fCsKANhQIeD5l76c2JFU+07gVE3KaA0FP+0zMWwfwo= +github.com/rjeczalik/notify v0.9.1 h1:CLCKso/QK1snAlnhNR/CNvNiFU2saUtjV0bx3EwNeCE= +github.com/rjeczalik/notify v0.9.1/go.mod h1:rKwnCoCGeuQnwBtTSPL9Dad03Vh2n40ePRrjvIXnJho= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= +github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= +github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible h1:Bn1aCHHRnjv4Bl16T8rcaFjYSrGrIZvpiGO6P3Q4GpU= +github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= +github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= +github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.9.3 h1:41FoI0fD7OR7mGcKE/aOiLkGreyf8ifIOQmJANWogMk= +github.com/spf13/afero v1.9.3/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= +github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= +github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= +github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= +github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.15.0 h1:js3yy885G8xwJa6iOISGFwd+qlUo5AvyXb7CiihdtiU= +github.com/spf13/viper v1.15.0/go.mod h1:fFcTBJxvhhzSJiZy8n+PeW6t8l+KeT/uTARa0jHOQLA= +github.com/status-im/keycard-go v0.0.0-20190316090335-8537d3370df4 h1:Gb2Tyox57NRNuZ2d3rmvB3pcmbu7O1RS3m8WRx7ilrg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8= +github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= +github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= +github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM= +github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tklauser/go-sysconf v0.3.5 h1:uu3Xl4nkLzQfXNsWn15rPc/HQCJKObbt1dKJeWp3vU4= +github.com/tklauser/go-sysconf v0.3.5/go.mod h1:MkWzOF4RMCshBAMXuhXJs64Rte09mITnppBXY/rYEFI= +github.com/tklauser/numcpus v0.2.2 h1:oyhllyrScuYI6g+h/zUvNXNp1wy7x8qQy3t/piefldA= +github.com/tklauser/numcpus v0.2.2/go.mod h1:x3qojaO3uyYt0i56EW/VUYs7uBvdl2fkfZFu0T9wgjM= +github.com/tyler-smith/go-bip39 v1.0.1-0.20181017060643-dbb3b84ba2ef h1:wHSqTBrZW24CsNJDfeh9Ex6Pm0Rcpc7qrgKBiL44vF4= +github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M= +github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= +github.com/ugorji/go/codec v1.2.9 h1:rmenucSohSTiyL09Y+l2OCk+FrMxGMzho2+tjr5ticU= +github.com/ugorji/go/codec v1.2.9/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/urfave/cli/v2 v2.10.2 h1:x3p8awjp/2arX+Nl/G2040AZpOCHS/eMJJ1/a+mye4Y= +github.com/wealdtech/go-ens/v3 v3.5.5 h1:/jq3CDItK0AsFnZtiFJK44JthkAMD5YE3WAJOh4i7lc= +github.com/wealdtech/go-ens/v3 v3.5.5/go.mod h1:w0EDKIm0dIQnqEKls6ORat/or+AVfPEdEXVfN71EeEE= +github.com/wealdtech/go-multicodec v1.4.0 h1:iq5PgxwssxnXGGPTIK1srvt6U5bJwIp7k6kBrudIWxg= +github.com/wealdtech/go-multicodec v1.4.0/go.mod h1:aedGMaTeYkIqi/KCPre1ho5rTb3hGpu/snBOS3GQLw4= +github.com/wealdtech/go-string2eth v1.1.0 h1:USJQmysUrBYYmZs7d45pMb90hRSyEwizP7lZaOZLDAw= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= +go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= +go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= +go.uber.org/multierr v1.8.0 h1:dg6GjLku4EH+249NNmoIciG9N/jURbDG+pFlTkhzIC8= +go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= +go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= +go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80= +golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc= +golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp v0.0.0-20230206171751-46f607a40771 h1:xP7rWLUr1e1n2xkK5YB4LI0hPEy3LJC6Wk+D4pGlOJg= +golang.org/x/exp v0.0.0-20230206171751-46f607a40771/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.6.0 h1:b9gGHsz9/HhJ3HF5DHQytPpuwocVTChQJK3AvoLRD5I= +golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= +golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220923202941-7f9b1623fab7/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210316164454-77fc1eacc6aa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.1.0 h1:xYY+Bajn2a7VBmTM5GikTmnK8ZuX8YgnQCqZpbBNtmA= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210112230658-8b4aab62c064/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.2.0 h1:G6AHpWxTMGY1KyEYoAQ5WTtIekUUvDNjan3ugu60JvE= +golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= +google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce h1:+JknDZhAj8YMt7GC73Ei8pv4MzjDUNPHgQWJdtMAaDU= +gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce/go.mod h1:5AcXVHNjg+BDxry382+8OKon8SEWiKktQR07RKPsv1c= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/postgres v1.4.7 h1:J06jXZCNq7Pdf7LIPn8tZn9LsWjd81BRSKveKNr0ZfA= +gorm.io/driver/postgres v1.4.7/go.mod h1:UJChCNLFKeBqQRE+HrkFUbKbq9idPXmTOk2u4Wok8S4= +gorm.io/gorm v1.24.2/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA= +gorm.io/gorm v1.24.6 h1:wy98aq9oFEetsc4CAbKD2SoBCdMzsbSIvSUUFJuHi5s= +gorm.io/gorm v1.24.6/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +lukechampine.com/blake3 v1.1.6 h1:H3cROdztr7RCfoaTpGZFQsrqvweFLrqS73j7L7cmR5c= +lukechampine.com/blake3 v1.1.6/go.mod h1:tkKEOtDkNtklkXtLNEOGNq5tcV90tJiA1vAA12R78LA= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/internal/app/api/v1/account.go b/internal/app/api/v1/account.go new file mode 100644 index 0000000..2cecad9 --- /dev/null +++ b/internal/app/api/v1/account.go @@ -0,0 +1,113 @@ +package v1 + +import ( + "github.com/gin-gonic/gin" + "go.uber.org/zap" + "nft-collect/internal/app/global" + "nft-collect/internal/app/model/request" + "nft-collect/internal/app/model/response" + "nft-collect/internal/app/service" + "nft-collect/internal/app/utils" + "strings" +) + +func GetCollectionByContract(c *gin.Context) { + var req request.GetCollectionReq + _ = c.ShouldBindQuery(&req) + req.ContractAddress = strings.ToLower(c.Param("address")) + req.AccountAddress = c.GetString("address") + if total, list, err := service.GetCollectionByContract(req); err != nil { + global.LOG.Error("Error!", zap.Error(err)) + response.FailWithMessage("Error", c) + } else { + response.OkWithDetailed(response.PageResult{ + List: list, + Total: total, + Page: req.Page, + PageSize: req.PageSize, + }, "Success", c) + } +} +func GetCollection(c *gin.Context) { + var req request.GetCollectionReq + _ = c.ShouldBindQuery(&req) + req.AccountAddress = strings.ToLower(c.Param("address")) + account := c.GetString("address") + // 检验字段 + if err := utils.Verify(req.PageInfo, utils.PageSizeLimitVerify); err != nil { + response.FailWithMessage(err.Error(), c) + return + } + if total, totalPublic, totalHidden, list, err := service.GetCollection(req, account); err != nil { + global.LOG.Error("Error!", zap.Error(err)) + response.FailWithMessage("Error", c) + } else { + response.OkWithDetailed(response.GetCollectionRes{ + List: list, + Total: total, + TotalPublic: totalPublic, + TotalHidden: totalHidden, + Page: req.Page, + PageSize: req.PageSize, + }, "Success", c) + } +} + +func GetContract(c *gin.Context) { + address := strings.ToLower(c.Param("address")) + account := c.GetString("address") + if !utils.IsValidAddress(address) { + response.FailWithMessage("Param Error", c) + return + } + if list, err := service.GetContract(address, account); err != nil { + global.LOG.Error("Error!", zap.Error(err)) + response.FailWithMessage("Error", c) + } else { + response.OkWithDetailed(list, "Success", c) + } +} + +func AddCollection(c *gin.Context) { + var req request.AddCollectionReq + _ = c.ShouldBindJSON(&req) + address := c.GetString("address") + if len(req.IDs) == 0 || address == "" { + response.FailWithMessage("Error", c) + return + } + if err := service.AddCollection(req.IDs, address, req.Flag); err != nil { + global.LOG.Error("Error!", zap.Error(err)) + response.FailWithMessage("Error", c) + } else { + response.OkWithMessage("Success", c) + } +} + +func UpdatedCollection(c *gin.Context) { + var req request.UpdatedCollectionReq + _ = c.ShouldBindJSON(&req) + address := c.GetString("address") + req.ID = c.Param("id") + if len(req.ID) == 0 || address == "" { + response.FailWithMessage("Error", c) + return + } + if err := service.UpdatedCollection(req, address); err != nil { + global.LOG.Error("Error!", zap.Error(err)) + response.FailWithMessage("Error", c) + } else { + response.OkWithMessage("Success", c) + } +} + +func RefreshUserData(c *gin.Context) { + var req request.RefreshUserDataReq + _ = c.ShouldBindJSON(&req) + if err := service.RefreshUserData(req.Address); err != nil { + global.LOG.Error("Error!", zap.Error(err)) + response.FailWithMessage("Error", c) + } else { + response.OkWithMessage("Success", c) + } +} diff --git a/internal/app/api/v1/ens.go b/internal/app/api/v1/ens.go new file mode 100644 index 0000000..4741d87 --- /dev/null +++ b/internal/app/api/v1/ens.go @@ -0,0 +1,14 @@ +package v1 + +import ( + "github.com/gin-gonic/gin" + "nft-collect/internal/app/model/response" + "nft-collect/internal/app/service" +) + +func GetEnsRecords(c *gin.Context) { + if list, err := service.GetEnsRecords(c, c.Param("q")); err != nil { + } else { + response.OkWithData(list, c) + } +} diff --git a/internal/app/api/v1/system.go b/internal/app/api/v1/system.go new file mode 100644 index 0000000..57b663c --- /dev/null +++ b/internal/app/api/v1/system.go @@ -0,0 +1,49 @@ +package v1 + +import ( + "github.com/gin-gonic/gin" + "go.uber.org/zap" + "nft-collect/internal/app/global" + "nft-collect/internal/app/model/request" + "nft-collect/internal/app/model/response" + "nft-collect/internal/app/service" +) + +func GetDefaultContract(c *gin.Context) { + if list, err := service.GetDefaultContract(); err != nil { + global.LOG.Error("Error!", zap.Error(err)) + response.FailWithMessage("Error", c) + } else { + response.OkWithDetailed(list, "Success", c) + } +} + +func AddDefaultContract(c *gin.Context) { + var req request.AddDefaultContractReq + err := c.ShouldBindJSON(&req) + if err != nil { + response.FailWithMessage("ParameterError", c) + return + } + if err := service.AddDefaultContract(req); err != nil { + global.LOG.Error("Error!", zap.Error(err)) + response.FailWithMessage("Error", c) + } else { + response.OkWithMessage("Success", c) + } +} + +func DelDefaultContract(c *gin.Context) { + var req request.DelDefaultContractReq + err := c.ShouldBindJSON(&req) + if err != nil { + response.FailWithMessage("ParameterError", c) + return + } + if err := service.DelDefaultContract(req); err != nil { + global.LOG.Error("Error!", zap.Error(err)) + response.FailWithMessage("Error", c) + } else { + response.OkWithMessage("Success", c) + } +} diff --git a/internal/app/config/config.go b/internal/app/config/config.go new file mode 100644 index 0000000..08fcceb --- /dev/null +++ b/internal/app/config/config.go @@ -0,0 +1,10 @@ +package config + +type Server struct { + Zap Zap `mapstructure:"zap" json:"zap" yaml:"zap"` + System System `mapstructure:"system" json:"system" yaml:"system"` + // gorm + Pgsql Pgsql `mapstructure:"pgsql" json:"pgsql" yaml:"pgsql"` + NFT NFT `mapstructure:"nft" json:"nft" yaml:"nft"` + JWT JWT `mapstructure:"jwt" json:"jwt" yaml:"jwt"` +} diff --git a/internal/app/config/gorm_pgsql.go b/internal/app/config/gorm_pgsql.go new file mode 100644 index 0000000..2cc15c5 --- /dev/null +++ b/internal/app/config/gorm_pgsql.go @@ -0,0 +1,28 @@ +package config + +type Pgsql struct { + Path string `mapstructure:"path" json:"path" yaml:"path"` // 服务器地址 + Port string `mapstructure:"port" json:"port" yaml:"port"` // 端口 + Config string `mapstructure:"config" json:"config" yaml:"config"` // 高级配置 + Dbname string `mapstructure:"db-name" json:"db-name" yaml:"db-name"` // 数据库名 + Username string `mapstructure:"username" json:"username" yaml:"username"` // 数据库用户名 + Password string `mapstructure:"password" json:"password" yaml:"password"` // 数据库密码 + MaxIdleConns int `mapstructure:"max-idle-conns" json:"max-idle-conns" yaml:"max-idle-conns"` // 空闲中的最大连接数 + MaxOpenConns int `mapstructure:"max-open-conns" json:"max-open-conns" yaml:"max-open-conns"` // 打开到数据库的最大连接数 + LogMode string `mapstructure:"log-mode" json:"log-mode" yaml:"log-mode"` // 是否开启Gorm全局日志 + LogZap bool `mapstructure:"log-zap" json:"log-zap" yaml:"log-zap"` // 是否通过zap写入日志文件 +} + +// Dsn 基于配置文件获取 dsn +func (p *Pgsql) Dsn() string { + return "host=" + p.Path + " user=" + p.Username + " password=" + p.Password + " dbname=" + p.Dbname + " port=" + p.Port + " " + p.Config +} + +// LinkDsn 根据 dbname 生成 dsn +func (p *Pgsql) LinkDsn(dbname string) string { + return "host=" + p.Path + " user=" + p.Username + " password=" + p.Password + " dbname=" + dbname + " port=" + p.Port + " " + p.Config +} + +func (m *Pgsql) GetLogMode() string { + return m.LogMode +} diff --git a/internal/app/config/jwt.go b/internal/app/config/jwt.go new file mode 100644 index 0000000..d26b684 --- /dev/null +++ b/internal/app/config/jwt.go @@ -0,0 +1,7 @@ +package config + +type JWT struct { + SigningKey string `mapstructure:"signing-key" json:"signing-key" yaml:"signing-key"` // jwt签名 + ExpiresTime int64 `mapstructure:"expires-time" json:"expires-time" yaml:"expires-time"` // 过期时间 + Issuer string `mapstructure:"issuer" json:"issuer" yaml:"issuer"` // 签发者 +} diff --git a/internal/app/config/nft.go b/internal/app/config/nft.go new file mode 100644 index 0000000..91eb60e --- /dev/null +++ b/internal/app/config/nft.go @@ -0,0 +1,19 @@ +package config + +type NFT struct { + EnsRpc string `mapstructure:"ens-rpc" json:"ens-rpc" yaml:"ens-rpc"` // ENS 查询 RPC + ApiKey string `mapstructure:"api-key" json:"api-key" yaml:"api-key"` + ApiKeyBackup string `mapstructure:"api-key-backup" json:"api-key-backup" yaml:"api-key-backup"` + ApiKeyPro string `mapstructure:"api-key-pro" json:"api-key-pro" yaml:"api-key-pro"` + CacheTime int `mapstructure:"cache-time" json:"cache-time" yaml:"cache-time"` // 缓存时间 分钟 + LogoPath string `mapstructure:"logo-path" json:"logo-path" yaml:"logo-path"` + DefContract []string `mapstructure:"def-contract" json:"def-contract" yaml:"def-contract"` + APIConfig []APIConfig `mapstructure:"api-config" json:"api-config" yaml:"api-config"` +} + +type APIConfig struct { + Chain string `mapstructure:"chain" json:"chain" yaml:"chain"` + ChainID uint `mapstructure:"chain-id" json:"chain-id" yaml:"chain-id"` + APIPreHost string `mapstructure:"api-per-host" json:"api-per-host" yaml:"api-per-host"` + Symbol string `mapstructure:"symbol" json:"symbol" yaml:"symbol"` +} diff --git a/internal/app/config/system.go b/internal/app/config/system.go new file mode 100644 index 0000000..ea4641b --- /dev/null +++ b/internal/app/config/system.go @@ -0,0 +1,6 @@ +package config + +type System struct { + Env string `mapstructure:"env" json:"env" yaml:"env"` // 环境值 + Addr int `mapstructure:"addr" json:"addr" yaml:"addr"` // 端口值 +} diff --git a/internal/app/config/zap.go b/internal/app/config/zap.go new file mode 100644 index 0000000..65ab73e --- /dev/null +++ b/internal/app/config/zap.go @@ -0,0 +1,12 @@ +package config + +type Zap struct { + Level string `mapstructure:"level" json:"level" yaml:"level"` // 级别 + Format string `mapstructure:"format" json:"format" yaml:"format"` // 输出 + Prefix string `mapstructure:"prefix" json:"prefix" yaml:"prefix"` // 日志前缀 + Director string `mapstructure:"director" json:"director" yaml:"director"` // 日志文件夹 + ShowLine bool `mapstructure:"show-line" json:"show-line" yaml:"show-line"` // 显示行 + EncodeLevel string `mapstructure:"encode-level" json:"encode-level" yaml:"encode-level"` // 编码级 + StacktraceKey string `mapstructure:"stacktrace-key" json:"stacktrace-key" yaml:"stacktrace-key"` // 栈名 + LogInConsole bool `mapstructure:"log-in-console" json:"log-in-console" yaml:"log-in-console"` // 输出控制台 +} diff --git a/internal/app/core/server.go b/internal/app/core/server.go new file mode 100644 index 0000000..6d6c006 --- /dev/null +++ b/internal/app/core/server.go @@ -0,0 +1,37 @@ +package core + +import ( + "fmt" + "nft-collect/internal/app/global" + "nft-collect/internal/app/initialize" + "time" + + "go.uber.org/zap" +) + +type server interface { + ListenAndServe() error +} + +func RunWindowsServer() { + Router := initialize.Routers() + //Router.Static("/form-generator", "./resource/page") + + Host := "0.0.0.0" + if global.CONFIG.System.Env == "public" { + Host = "127.0.0.1" + } + address := fmt.Sprintf("%s:%d", Host, global.CONFIG.System.Addr) + s := initServer(address, Router) + // 保证文本顺序输出 + // In order to ensure that the text order output can be deleted + time.Sleep(10 * time.Microsecond) + global.LOG.Info("server run success on ", zap.String("address", address)) + + fmt.Printf(` + 欢迎使用 nft-collect + 当前版本:V0.0.1 + 默认地址:http://127.0.0.1:%d/ +`, global.CONFIG.System.Addr) + global.LOG.Error(s.ListenAndServe().Error()) +} diff --git a/internal/app/core/server_other.go b/internal/app/core/server_other.go new file mode 100644 index 0000000..41770d6 --- /dev/null +++ b/internal/app/core/server_other.go @@ -0,0 +1,19 @@ +//go:build !windows +// +build !windows + +package core + +import ( + "time" + + "github.com/fvbock/endless" + "github.com/gin-gonic/gin" +) + +func initServer(address string, router *gin.Engine) server { + s := endless.NewServer(address, router) + s.ReadHeaderTimeout = 120 * time.Second + s.WriteTimeout = 120 * time.Second + s.MaxHeaderBytes = 1 << 20 + return s +} diff --git a/internal/app/core/server_win.go b/internal/app/core/server_win.go new file mode 100644 index 0000000..74905ac --- /dev/null +++ b/internal/app/core/server_win.go @@ -0,0 +1,21 @@ +//go:build windows +// +build windows + +package core + +import ( + "net/http" + "time" + + "github.com/gin-gonic/gin" +) + +func initServer(address string, router *gin.Engine) server { + return &http.Server{ + Addr: address, + Handler: router, + ReadTimeout: 30 * time.Second, + WriteTimeout: 30 * time.Second, + MaxHeaderBytes: 1 << 20, + } +} diff --git a/internal/app/core/viper.go b/internal/app/core/viper.go new file mode 100644 index 0000000..122a25b --- /dev/null +++ b/internal/app/core/viper.go @@ -0,0 +1,51 @@ +package core + +import ( + "flag" + "fmt" + "nft-collect/internal/app/global" + + "github.com/fsnotify/fsnotify" + "github.com/spf13/viper" +) + +func Viper(path ...string) *viper.Viper { + var config string + if len(path) == 0 { + flag.StringVar(&config, "c", "", "choose config file.") + flag.Parse() + if config == "" { // 优先级: 命令行 > 环境变量 > 默认值 + config = "config/config.yaml" + fmt.Printf("您正在使用config的默认值,config的路径为%v\n", config) + } else { + fmt.Printf("您正在使用命令行的-c参数传递的值,config的路径为%v\n", config) + } + } else { + config = path[0] + fmt.Printf("您正在使用func Viper()传递的值,config的路径为%v\n", config) + } + + v := viper.New() + v.SetConfigFile(config) + v.SetConfigType("yaml") + err := v.ReadInConfig() + if err != nil { + panic(fmt.Errorf("Fatal error config file: %s \n", err)) + } + v.WatchConfig() + + v.OnConfigChange(func(e fsnotify.Event) { + fmt.Println("config file changed:", e.Name) + if err := v.Unmarshal(&global.CONFIG); err != nil { + fmt.Println(err) + } else { + fmt.Println(global.CONFIG.NFT) + // 配置更改重启 + // utils.Reload() + } + }) + if err := v.Unmarshal(&global.CONFIG); err != nil { + fmt.Println(err) + } + return v +} diff --git a/internal/app/core/zap.go b/internal/app/core/zap.go new file mode 100644 index 0000000..760eeda --- /dev/null +++ b/internal/app/core/zap.go @@ -0,0 +1,99 @@ +package core + +import ( + "fmt" + "nft-collect/internal/app/global" + utils2 "nft-collect/internal/app/utils" + "os" + "time" + + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +func Zap() (logger *zap.Logger) { + if ok, _ := utils2.PathExists(global.CONFIG.Zap.Director); !ok { // 判断是否有Director文件夹 + fmt.Printf("create %v directory\n", global.CONFIG.Zap.Director) + _ = os.Mkdir(global.CONFIG.Zap.Director, os.ModePerm) + } + // 调试级别 + debugPriority := zap.LevelEnablerFunc(func(lev zapcore.Level) bool { + return lev == zap.DebugLevel + }) + // 日志级别 + infoPriority := zap.LevelEnablerFunc(func(lev zapcore.Level) bool { + return lev == zap.InfoLevel + }) + // 警告级别 + warnPriority := zap.LevelEnablerFunc(func(lev zapcore.Level) bool { + return lev == zap.WarnLevel + }) + // 错误级别 + errorPriority := zap.LevelEnablerFunc(func(lev zapcore.Level) bool { + return lev >= zap.ErrorLevel + }) + + now := time.Now().Format("2006-01-02") + + cores := [...]zapcore.Core{ + getEncoderCore(fmt.Sprintf("./%s/%s/debug.log", global.CONFIG.Zap.Director, now), debugPriority), + getEncoderCore(fmt.Sprintf("./%s/%s/info.log", global.CONFIG.Zap.Director, now), infoPriority), + getEncoderCore(fmt.Sprintf("./%s/%s/warn.log", global.CONFIG.Zap.Director, now), warnPriority), + getEncoderCore(fmt.Sprintf("./%s/%s/error.log", global.CONFIG.Zap.Director, now), errorPriority), + } + logger = zap.New(zapcore.NewTee(cores[:]...), zap.AddCaller()) + + if global.CONFIG.Zap.ShowLine { + logger = logger.WithOptions(zap.AddCaller()) + } + return logger +} + +// getEncoderConfig 获取zapcore.EncoderConfig +func getEncoderConfig() (config zapcore.EncoderConfig) { + config = zapcore.EncoderConfig{ + MessageKey: "message", + LevelKey: "level", + TimeKey: "time", + NameKey: "logger", + CallerKey: "caller", + StacktraceKey: global.CONFIG.Zap.StacktraceKey, + LineEnding: zapcore.DefaultLineEnding, + EncodeLevel: zapcore.LowercaseLevelEncoder, + EncodeTime: CustomTimeEncoder, + EncodeDuration: zapcore.SecondsDurationEncoder, + EncodeCaller: zapcore.FullCallerEncoder, + } + switch { + case global.CONFIG.Zap.EncodeLevel == "LowercaseLevelEncoder": // 小写编码器(默认) + config.EncodeLevel = zapcore.LowercaseLevelEncoder + case global.CONFIG.Zap.EncodeLevel == "LowercaseColorLevelEncoder": // 小写编码器带颜色 + config.EncodeLevel = zapcore.LowercaseColorLevelEncoder + case global.CONFIG.Zap.EncodeLevel == "CapitalLevelEncoder": // 大写编码器 + config.EncodeLevel = zapcore.CapitalLevelEncoder + case global.CONFIG.Zap.EncodeLevel == "CapitalColorLevelEncoder": // 大写编码器带颜色 + config.EncodeLevel = zapcore.CapitalColorLevelEncoder + default: + config.EncodeLevel = zapcore.LowercaseLevelEncoder + } + return config +} + +// getEncoder 获取zapcore.Encoder +func getEncoder() zapcore.Encoder { + if global.CONFIG.Zap.Format == "json" { + return zapcore.NewJSONEncoder(getEncoderConfig()) + } + return zapcore.NewConsoleEncoder(getEncoderConfig()) +} + +// getEncoderCore 获取Encoder的zapcore.Core +func getEncoderCore(fileName string, level zapcore.LevelEnabler) (core zapcore.Core) { + writer := utils2.GetWriteSyncer(fileName) // 使用file-rotatelogs进行日志分割 + return zapcore.NewCore(getEncoder(), writer, level) +} + +// CustomTimeEncoder 自定义日志输出时间格式 +func CustomTimeEncoder(t time.Time, enc zapcore.PrimitiveArrayEncoder) { + enc.AppendString(t.Format(global.CONFIG.Zap.Prefix + "2006/01/02 - 15:04:05.000")) +} diff --git a/internal/app/global/global.go b/internal/app/global/global.go new file mode 100644 index 0000000..e899534 --- /dev/null +++ b/internal/app/global/global.go @@ -0,0 +1,15 @@ +package global + +import ( + "go.uber.org/zap" + "gorm.io/gorm" + "nft-collect/internal/app/config" + "nft-collect/pkg/cache" +) + +var ( + DB *gorm.DB // 数据库链接 + LOG *zap.Logger // 日志框架 + CONFIG config.Server // 配置信息 + Cache *cache.BigCacheStore // 缓存 +) diff --git a/internal/app/global/model.go b/internal/app/global/model.go new file mode 100644 index 0000000..9d1fe0a --- /dev/null +++ b/internal/app/global/model.go @@ -0,0 +1,11 @@ +package global + +import ( + "time" +) + +type MODEL struct { + ID string `gorm:"type:uuid;default:uuid_generate_v4();primarykey" json:"id"` // 主键 + CreatedAt time.Time `json:"-"` // 创建时间 + UpdatedAt time.Time `json:"-"` // 更新时间 +} diff --git a/internal/app/initialize/cache.go b/internal/app/initialize/cache.go new file mode 100644 index 0000000..5ece679 --- /dev/null +++ b/internal/app/initialize/cache.go @@ -0,0 +1,14 @@ +package initialize + +import ( + "nft-collect/internal/app/global" + "nft-collect/pkg/cache" + "time" +) + +func InitCache() { + if global.CONFIG.NFT.CacheTime < 3 { + panic("Less than 3 minutes") + } + global.Cache = cache.NewBigCacheStore(time.Duration(global.CONFIG.NFT.CacheTime)*time.Minute, global.LOG) +} diff --git a/internal/app/initialize/contract.go b/internal/app/initialize/contract.go new file mode 100644 index 0000000..6bd63e1 --- /dev/null +++ b/internal/app/initialize/contract.go @@ -0,0 +1,21 @@ +package initialize + +import ( + "nft-collect/internal/app/global" + "nft-collect/internal/app/service" + "strings" +) + +// 获取默认NFT合约信息 +func InitNFTContract() { + for _, api := range global.CONFIG.NFT.APIConfig { + for _, v := range global.CONFIG.NFT.DefContract { + temp := strings.Split(v, "::") + if len(temp) == 0 || (len(temp) == 2 && temp[1] != api.Chain) { + continue + } + contractMap := map[string]struct{}{strings.ToLower(temp[0]): struct{}{}} + service.ItemFiltrateAndDown(contractMap, api) + } + } +} diff --git a/internal/app/initialize/gorm.go b/internal/app/initialize/gorm.go new file mode 100644 index 0000000..d658c5a --- /dev/null +++ b/internal/app/initialize/gorm.go @@ -0,0 +1,34 @@ +package initialize + +import ( + "go.uber.org/zap" + "gorm.io/gorm" + "nft-collect/internal/app/global" + "nft-collect/internal/app/model" + "os" +) + +// InitCommonDB 通用数据库 +func InitCommonDB() { + db := GormPgSql("") + if db != nil { + global.DB = db + RegisterTables(db) // 初始化表 + } +} + +// RegisterTables 注册数据库表专用 +func RegisterTables(db *gorm.DB) { + err := db.AutoMigrate( + model.Collection{}, + model.Contract{}, + model.Account{}, + model.ContractDefault{}, + model.Ens{}, + ) + if err != nil { + global.LOG.Error("register table failed", zap.Error(err)) + os.Exit(0) + } + global.LOG.Info("register table success") +} diff --git a/internal/app/initialize/gorm_pgsql.go b/internal/app/initialize/gorm_pgsql.go new file mode 100644 index 0000000..88a8800 --- /dev/null +++ b/internal/app/initialize/gorm_pgsql.go @@ -0,0 +1,28 @@ +package initialize + +import ( + "gorm.io/driver/postgres" + "gorm.io/gorm" + "nft-collect/internal/app/global" + "nft-collect/internal/app/initialize/internal" +) + +// GormPgSql 初始化 Postgresql 数据库 +func GormPgSql(Prefix string) *gorm.DB { + p := global.CONFIG.Pgsql + if p.Dbname == "" { + return nil + } + pgsqlConfig := postgres.Config{ + DSN: p.Dsn(), // DSN data source name + PreferSimpleProtocol: false, + } + if db, err := gorm.Open(postgres.New(pgsqlConfig), internal.Gorm.Config(Prefix)); err != nil { + return nil + } else { + sqlDB, _ := db.DB() + sqlDB.SetMaxIdleConns(p.MaxIdleConns) + sqlDB.SetMaxOpenConns(p.MaxOpenConns) + return db + } +} diff --git a/internal/app/initialize/internal/gorm.go b/internal/app/initialize/internal/gorm.go new file mode 100644 index 0000000..39dc8ff --- /dev/null +++ b/internal/app/initialize/internal/gorm.go @@ -0,0 +1,52 @@ +package internal + +import ( + "gorm.io/gorm/schema" + "log" + "nft-collect/internal/app/global" + "os" + "time" + + "gorm.io/gorm" + "gorm.io/gorm/logger" +) + +type DBBASE interface { + GetLogMode() string +} + +var Gorm = new(_gorm) + +type _gorm struct{} + +// Config gorm 自定义配置 +func (g *_gorm) Config(Prefix string) *gorm.Config { + if Prefix != "" { + Prefix += "_" + } + config := &gorm.Config{DisableForeignKeyConstraintWhenMigrating: true, NamingStrategy: schema.NamingStrategy{ + TablePrefix: Prefix, + SingularTable: true, + }} + _default := logger.New(NewWriter(log.New(os.Stdout, "\r\n", log.LstdFlags)), logger.Config{ + SlowThreshold: 200 * time.Millisecond, + LogLevel: logger.Warn, + Colorful: true, + }) + var logMode DBBASE + logMode = &global.CONFIG.Pgsql + + switch logMode.GetLogMode() { + case "silent", "Silent": + config.Logger = _default.LogMode(logger.Silent) + case "error", "Error": + config.Logger = _default.LogMode(logger.Error) + case "warn", "Warn": + config.Logger = _default.LogMode(logger.Warn) + case "info", "Info": + config.Logger = _default.LogMode(logger.Info) + default: + config.Logger = _default.LogMode(logger.Info) + } + return config +} diff --git a/internal/app/initialize/internal/logger.go b/internal/app/initialize/internal/logger.go new file mode 100644 index 0000000..1e9848f --- /dev/null +++ b/internal/app/initialize/internal/logger.go @@ -0,0 +1,14 @@ +package internal + +import ( + "gorm.io/gorm/logger" +) + +type writer struct { + logger.Writer +} + +// NewWriter writer 构造函数 +func NewWriter(w logger.Writer) *writer { + return &writer{Writer: w} +} diff --git a/internal/app/initialize/router.go b/internal/app/initialize/router.go new file mode 100644 index 0000000..8fa13c3 --- /dev/null +++ b/internal/app/initialize/router.go @@ -0,0 +1,42 @@ +package initialize + +import ( + "github.com/gin-contrib/pprof" + "github.com/gin-gonic/gin" + "net/http" + "nft-collect/internal/app/global" + "nft-collect/internal/app/middleware" + "nft-collect/internal/app/router" +) + +// 初始化总路由 + +func Routers() *gin.Engine { + var Router *gin.Engine + // 开发环境打开日志 && 打开pprof + if global.CONFIG.System.Env == "develop" { + Router = gin.Default() + pprof.Register(Router) // 性能 + } else { + Router = gin.New() + Router.Use(gin.Recovery()) + } + Router.Use(middleware.Cors()) // 放行跨域请求 + Router.StaticFS(global.CONFIG.NFT.LogoPath, http.Dir(global.CONFIG.NFT.LogoPath)) // 为用户头像和文件提供静态地址 + PublicGroup := Router.Group("") + { + // 健康监测 + PublicGroup.GET("/health", func(c *gin.Context) { + c.JSON(200, "ok") + }) + } + v1Group := Router.Group("v1") + { + router.InitAccountRouter(v1Group) + router.InitSystemRouter(v1Group) + router.InitEnsRouter(v1Group) + } + + global.LOG.Info("router register success") + return Router +} diff --git a/internal/app/middleware/auth.go b/internal/app/middleware/auth.go new file mode 100644 index 0000000..4b2d867 --- /dev/null +++ b/internal/app/middleware/auth.go @@ -0,0 +1,54 @@ +package middleware + +import ( + "fmt" + "github.com/gin-gonic/gin" + "nft-collect/internal/app/model/response" + "nft-collect/internal/app/utils" + "strings" +) + +func Auth() gin.HandlerFunc { + return func(c *gin.Context) { + // jwt鉴权取头部信息: x-token + token := c.Request.Header.Get("x-token") + if token == "" { + response.FailWithDetailed(gin.H{"reload": true}, "授权已过期或非法访问1", c) + c.Abort() + return + } + j := utils.NewJWT() + // parseToken 解析token包含的信息 + claims, err := j.ParseToken(token) + if err != nil { + if err == utils.TokenExpired { + response.FailWithDetailed(gin.H{"reload": true}, "授权已过期", c) + c.Abort() + return + } + response.FailWithDetailed(gin.H{"reload": true}, err.Error(), c) + c.Abort() + return + } + c.Set("address", strings.ToLower(claims.Address)) + } +} + +func Addr() gin.HandlerFunc { + return func(c *gin.Context) { + // 鉴权头部信息: x-token + token := c.Request.Header.Get("x-token") + fmt.Println("token", token) + if token != "" { + j := utils.NewJWT() + // 解析token包含的信息 + claims, err := j.ParseTokenWithNoAuth(token) + if err != nil { + fmt.Println(err) + return + } + fmt.Println("claims", claims) + c.Set("address", strings.ToLower(claims.Address)) + } + } +} diff --git a/internal/app/middleware/cache.go b/internal/app/middleware/cache.go new file mode 100644 index 0000000..30c4bba --- /dev/null +++ b/internal/app/middleware/cache.go @@ -0,0 +1,11 @@ +package middleware + +import ( + cache "github.com/chenyahui/gin-cache" + "github.com/gin-gonic/gin" + "nft-collect/internal/app/global" +) + +func Cache() gin.HandlerFunc { + return cache.CacheByRequestURI(global.Cache, 0) +} diff --git a/internal/app/middleware/cors.go b/internal/app/middleware/cors.go new file mode 100644 index 0000000..3a51804 --- /dev/null +++ b/internal/app/middleware/cors.go @@ -0,0 +1,26 @@ +package middleware + +import ( + "github.com/gin-gonic/gin" + "net/http" +) + +// Cors 直接放行所有跨域请求并放行所有 OPTIONS 方法 +func Cors() gin.HandlerFunc { + return func(c *gin.Context) { + method := c.Request.Method + origin := c.Request.Header.Get("Origin") + c.Header("Access-Control-Allow-Origin", origin) + c.Header("Access-Control-Allow-Headers", "Content-Type,AccessToken,X-CSRF-Token, Authorization, Token,X-Token,X-User-Id,X-Requested-With, X-Lang") + c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE, PUT") + c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type, New-Token, New-Expires-At") + c.Header("Access-Control-Allow-Credentials", "true") + + // 放行所有OPTIONS方法 + if method == "OPTIONS" { + c.AbortWithStatus(http.StatusNoContent) + } + // 处理请求 + c.Next() + } +} diff --git a/internal/app/model/account.go b/internal/app/model/account.go new file mode 100644 index 0000000..6bfa5df --- /dev/null +++ b/internal/app/model/account.go @@ -0,0 +1,16 @@ +package model + +import ( + "github.com/lib/pq" + "nft-collect/internal/app/global" +) + +type Account struct { + global.MODEL + Address string `gorm:"type:char(42);index:account_address,unique;not null;" json:"address" form:"address"` + ContractIDs pq.StringArray `gorm:"type:uuid[]" json:"contract_ids" form:"contract_ids"` // 合约ID + Counts pq.Int64Array `gorm:"type:integer[]" json:"counts" form:"counts"` // 数量 + CountsShow pq.Int64Array `gorm:"type:integer[]" json:"counts_show" form:"counts_show"` // 显示数量 + //CountsDefault pq.Int64Array `gorm:"type:integer[]" json:"counts_default" form:"counts_default"` // 默认合约数量 + Total int `gorm:"column:total;default:0"` // NFT总数 +} diff --git a/internal/app/model/collection.go b/internal/app/model/collection.go new file mode 100644 index 0000000..3fc7535 --- /dev/null +++ b/internal/app/model/collection.go @@ -0,0 +1,53 @@ +package model + +import ( + "nft-collect/internal/app/global" +) + +type Collection struct { + global.MODEL + Chain string `gorm:"column:chain;index:chain_address_contract_token,unique" json:"chain" form:"chain"` // 区块链的简称(eth, bnb, polygon, moonbeam, arbitrum, optimism, platon, avalanche, cronos) + AccountAddress string `gorm:"column:account_address;type:char(42);index:chain_address_contract_token,unique" json:"account_address" form:"account_address"` // 资产持有者的地址 + Status uint8 `gorm:"default:1;" json:"status" form:"status"` // 显示状态(1:隐藏 2:显示) + Flag uint8 `gorm:"default:1;" json:"flag" form:"flag"` // 添加状态(1:未添加 2:已添加) + NFTScanOwn +} + +type CollectionUpdate struct { + global.MODEL + Chain string `gorm:"column:chain;index:chain_address_contract_token,unique" json:"chain" form:"chain"` // 区块链的简称(eth, bnb, polygon, moonbeam, arbitrum, optimism, platon, avalanche, cronos) + AccountAddress string `gorm:"column:account_address;type:char(42);index:chain_address_contract_token,unique" json:"account_address" form:"account_address"` // 资产持有者的地址 + Status uint8 `gorm:"-" json:"status" form:"status"` // 显示状态(1:隐藏 2:显示) + Flag uint8 `gorm:"-" json:"flag" form:"flag"` // 添加状态(1:未添加 2:已添加) + NFTScanOwn +} + +type NFTScanOwn struct { + ContractAddress string `gorm:"index:chain_address_contract_token,unique" json:"contract_address" form:"contract_address"` // 合约地址 + ContractName string `json:"contract_name" form:"contract_name"` // 合约名称 + ContractTokenID string `json:"contract_token_id" form:"contract_token_id"` + TokenID string `gorm:"index:chain_address_contract_token,unique" json:"token_id" form:"token_id"` + ErcType string `gorm:"column:erc_type" json:"erc_type" form:"erc_type"` // NFT 的 erc 标准类型(erc721 或 erc1155) + Amount string `gorm:"column:amount" json:"amount" form:"amount"` // 持有数量 + Minter string `json:"minter" form:"minter"` + Owner string `json:"owner" form:"owner"` + OwnTimestamp int64 `json:"own_timestamp"` + MintTimestamp int64 `json:"mint_timestamp"` + MintTransactionHash string `json:"mint_transaction_hash"` + MintPrice float64 `json:"mint_price"` + TokenURI string `json:"token_uri"` + MetadataJSON string `json:"metadata_json"` + Name string `json:"name"` + ContentType string `json:"content_type"` + ContentURI string `json:"-"` + ImageURI string `json:"image_uri"` + ExternalLink string `gorm:"column:external_link" json:"external_link" form:"external_link"` // NFT对应的网站链接 + //LatestTradePrice interface{} `json:"latest_trade_price"` + //LatestTradeSymbol interface{} `json:"latest_trade_symbol"` + //LatestTradeTimestamp interface{} `json:"latest_trade_timestamp"` + NftscanID string `json:"nftscan_id"` + NftscanURI string `json:"nftscan_uri"` + //Attributes []interface{} `json:"attributes"` + RarityScore float64 `json:"rarity_score"` + RarityRank int `json:"rarity_rank"` +} diff --git a/internal/app/model/contract.go b/internal/app/model/contract.go new file mode 100644 index 0000000..2014b2a --- /dev/null +++ b/internal/app/model/contract.go @@ -0,0 +1,18 @@ +package model + +import ( + "nft-collect/internal/app/global" +) + +type Contract struct { + global.MODEL + Chain string `gorm:"index:chain_address,unique" json:"chain" form:"chain"` // 区块链的简称(eth, bnb, polygon, moonbeam, arbitrum, optimism, platon, avalanche, cronos) + ContractAddress string `gorm:"index:chain_address,unique" json:"contract_address" form:"contract_address"` // 合约地址 + ContractName string `gorm:"default:''" json:"contract_name" form:"contract_name"` // 合约名称 + ContractLogo string `gorm:"default:''" json:"contract_logo" form:"contract_logo"` // 合约Logo + ContractBanner string `gorm:"default:''" json:"contract_banner" form:"contract_banner"` // 合约Banner + ContractDescription string `gorm:"default:''" json:"contract_description" form:"contract_description"` // 合约Description + ContractWebsite string `gorm:"default:''" json:"contract_website" form:"contract_website"` // 合约Website + ContractOwner string `gorm:"default:''" json:"contract_owner" form:"contract_owner"` // 合约Owner + Status uint8 `gorm:"default:1;" json:"-" form:"-"` // 显示状态(1:未获取 2:已获取) +} diff --git a/internal/app/model/contract_default.go b/internal/app/model/contract_default.go new file mode 100644 index 0000000..70b9ddb --- /dev/null +++ b/internal/app/model/contract_default.go @@ -0,0 +1,11 @@ +package model + +import ( + "time" +) + +type ContractDefault struct { + ID uint `gorm:"primarykey"` + CreatedAt time.Time `json:"-"` // 创建时间 + ContractID string `gorm:"type:uuid;unique"` // +} diff --git a/internal/app/model/ens.go b/internal/app/model/ens.go new file mode 100644 index 0000000..a7994e4 --- /dev/null +++ b/internal/app/model/ens.go @@ -0,0 +1,10 @@ +package model + +import "gorm.io/gorm" + +type Ens struct { + gorm.Model + Address string `gorm:"column:address;index:address_domain,UNIQUE;" json:"address"` + Domain string `gorm:"column:domain;index:address_domain,UNIQUE;" json:"domain"` + Avatar string `gorm:"avatar" json:"avatar"` +} diff --git a/internal/app/model/receive/nftscan.go b/internal/app/model/receive/nftscan.go new file mode 100644 index 0000000..75d83ca --- /dev/null +++ b/internal/app/model/receive/nftscan.go @@ -0,0 +1,56 @@ +package receive + +type GetNftPlatformInfo struct { + Msg string `json:"msg"` + Code int `json:"code"` + Data struct { + Image string `json:"image"` + Website string `json:"website"` + Address string `json:"address"` + Description string `json:"description"` + Banner string `json:"banner"` + OpenseaVerify bool `json:"opensea_verify"` + Royalty string `json:"royalty"` + AuthFlag bool `json:"authFlag"` + PageView int `json:"pageView"` + Name string `json:"name"` + ContractCreator string `json:"contractCreator"` + } `json:"data"` +} + +type GetItemModel struct { + Code int `json:"code"` + Msg interface{} `json:"msg"` + Data struct { + ContractAddress string `json:"contract_address"` + Name string `json:"name"` + Symbol string `json:"symbol"` + Description string `json:"description"` + Website string `json:"website"` + Email interface{} `json:"email"` + Twitter string `json:"twitter"` + Discord string `json:"discord"` + Telegram interface{} `json:"telegram"` + Github interface{} `json:"github"` + Instagram interface{} `json:"instagram"` + Medium interface{} `json:"medium"` + LogoURL string `json:"logo_url"` + BannerURL string `json:"banner_url"` + FeaturedURL string `json:"featured_url"` + LargeImageURL string `json:"large_image_url"` + Attributes []interface{} `json:"attributes"` + ErcType string `json:"erc_type"` + DeployBlockNumber int `json:"deploy_block_number"` + Owner string `json:"owner"` + Verified bool `json:"verified"` + OpenseaVerified bool `json:"opensea_verified"` + Royalty int `json:"royalty"` + ItemsTotal int `json:"items_total"` + AmountsTotal int `json:"amounts_total"` + OwnersTotal int `json:"owners_total"` + OpenseaFloorPrice float64 `json:"opensea_floor_price"` + FloorPrice float64 `json:"floor_price"` + CollectionsWithSameName []string `json:"collections_with_same_name"` + PriceSymbol string `json:"price_symbol"` + } `json:"data"` +} diff --git a/internal/app/model/request/account.go b/internal/app/model/request/account.go new file mode 100644 index 0000000..7b90c4d --- /dev/null +++ b/internal/app/model/request/account.go @@ -0,0 +1,34 @@ +package request + +import "nft-collect/internal/app/model" + +type AddContractReq struct { + ContractAddress string `json:"contract_address" form:"contract_address"` // 合约地址 +} + +type AddCollectionReq struct { + Flag int64 `json:"flag"` + IDs []string `json:"ids" form:"ids"` +} + +type UpdatedCollectionReq struct { + ID string `json:"id" form:"id"` + Status uint8 `gorm:"default:1;" json:"status" form:"status"` // 显示状态(1:隐藏 2:显示) +} + +type GetCollectionReq struct { + PageInfo + model.Collection + ContractID string `form:"contract_id"` + ChainID uint `form:"chain_id"` + Search string `form:"search"` + Sort string `form:"sort"` +} + +type AddressReq struct { + Address string `json:"address" form:"address"` +} + +type RefreshUserDataReq struct { + Address string `json:"address" form:"address"` +} diff --git a/internal/app/model/request/common.go b/internal/app/model/request/common.go new file mode 100644 index 0000000..637294e --- /dev/null +++ b/internal/app/model/request/common.go @@ -0,0 +1,9 @@ +package request + +// PageInfo Paging common input parameter structure +type PageInfo struct { + Page int `json:"page" form:"page,default=1"` // 页码 + PageSize int `json:"pageSize" form:"pageSize,default=30"` // 每页大小 +} + +//type PageO diff --git a/internal/app/model/request/ens.go b/internal/app/model/request/ens.go new file mode 100644 index 0000000..9a50a5d --- /dev/null +++ b/internal/app/model/request/ens.go @@ -0,0 +1,5 @@ +package request + +type GetEnsRequest struct { + SearchKey string +} diff --git a/internal/app/model/request/nft.go b/internal/app/model/request/nft.go new file mode 100644 index 0000000..d524a02 --- /dev/null +++ b/internal/app/model/request/nft.go @@ -0,0 +1,7 @@ +package request + +import "nft-collect/internal/app/model" + +type AddCollectionRequest struct { + model.Contract +} diff --git a/internal/app/model/request/system.go b/internal/app/model/request/system.go new file mode 100644 index 0000000..42c49b9 --- /dev/null +++ b/internal/app/model/request/system.go @@ -0,0 +1,9 @@ +package request + +type AddDefaultContractReq struct { + Chain string `json:"chain" form:"chain" binding:"required"` + ContractAddress string `json:"contract_address" form:"contract_address" binding:"required"` // 合约地址 +} +type DelDefaultContractReq struct { + ID string `json:"id" from:"id" binding:"required"` +} diff --git a/internal/app/model/response/account.go b/internal/app/model/response/account.go new file mode 100644 index 0000000..fec7307 --- /dev/null +++ b/internal/app/model/response/account.go @@ -0,0 +1,24 @@ +package response + +import ( + "nft-collect/internal/app/model" +) + +type GetContractRes struct { + model.Contract + Count int64 `gorm:"-" json:"count" form:"count"` +} + +type GetCollectionRes struct { + List interface{} `json:"list"` + Total int64 `json:"total"` + TotalPublic int64 `json:"total_public"` + TotalHidden int64 `json:"total_hidden"` + Page int `json:"page"` + PageSize int `json:"pageSize"` +} + +type GetCollection struct { + model.Collection + ContractLogo string `json:"contract_logo" form:"contract_logo"` // 合约Logo +} diff --git a/internal/app/model/response/common.go b/internal/app/model/response/common.go new file mode 100644 index 0000000..7461096 --- /dev/null +++ b/internal/app/model/response/common.go @@ -0,0 +1,8 @@ +package response + +type PageResult struct { + List interface{} `json:"list"` + Total int64 `json:"total"` + Page int `json:"page"` + PageSize int `json:"pageSize"` +} diff --git a/internal/app/model/response/ens.go b/internal/app/model/response/ens.go new file mode 100644 index 0000000..5016c94 --- /dev/null +++ b/internal/app/model/response/ens.go @@ -0,0 +1,7 @@ +package response + +type GetEnsResponse struct { + Address string `json:"address"` + Domain string `json:"domain"` + Avatar string `json:"avatar"` +} diff --git a/internal/app/model/response/response.go b/internal/app/model/response/response.go new file mode 100644 index 0000000..773a95b --- /dev/null +++ b/internal/app/model/response/response.go @@ -0,0 +1,59 @@ +package response + +import ( + "net/http" + + "github.com/gin-gonic/gin" +) + +type Response struct { + Status int `json:"status"` + Data interface{} `json:"data"` + Message string `json:"message"` +} + +const ( + ERROR = 7 + SUCCESS = 0 +) + +func Result(code int, data interface{}, msg string, c *gin.Context) { + // 开始时间 + c.JSON(http.StatusOK, Response{ + code, + data, + msg, + }) +} + +func Ok(c *gin.Context) { + Result(SUCCESS, map[string]interface{}{}, "操作成功", c) +} + +func OkWithRaw(data interface{}, c *gin.Context) { + c.JSON(http.StatusOK, data) +} + +func OkWithMessage(message string, c *gin.Context) { + Result(SUCCESS, map[string]interface{}{}, message, c) +} + +func OkWithData(data interface{}, c *gin.Context) { + Result(SUCCESS, data, "操作成功", c) +} + +func OkWithDetailed(data interface{}, message string, c *gin.Context) { + Result(SUCCESS, data, message, c) +} + +func Fail(c *gin.Context) { + Result(ERROR, map[string]interface{}{}, "操作失败", c) +} + +func FailWithMessage(message string, c *gin.Context) { + Result(ERROR, map[string]interface{}{}, message, c) +} + +func FailWithDetailed(data interface{}, message string, c *gin.Context) { + Result(ERROR, data, message, c) +} diff --git a/internal/app/router/account.go b/internal/app/router/account.go new file mode 100644 index 0000000..dbea82d --- /dev/null +++ b/internal/app/router/account.go @@ -0,0 +1,25 @@ +package router + +import ( + "github.com/gin-gonic/gin" + "nft-collect/internal/app/api/v1" + "nft-collect/internal/app/middleware" +) + +func InitAccountRouter(Router *gin.RouterGroup) { + accountRouterWithCache := Router.Group("account").Use(middleware.Addr()) + accountRouterWithAuth := Router.Group("account").Use(middleware.Auth()) // auth + { + accountRouterWithCache.GET("/own/:address/contract", v1.GetContract) // Get the list of user NFT contracts + accountRouterWithCache.GET("/own/:address", v1.GetCollection) // Get the NFT data by the user + } + { + accountRouterWithAuth.GET("/contract/:address", v1.GetCollectionByContract) // Get the NFT data by the Contract + } + { + accountRouterWithAuth.POST("/own/collection", v1.AddCollection) // add collection + accountRouterWithAuth.PUT("/own/collection/:id", v1.UpdatedCollection) // update collection status + accountRouterWithAuth.POST("/own/refreshUserData", v1.RefreshUserData) // add collection + + } +} diff --git a/internal/app/router/ens.go b/internal/app/router/ens.go new file mode 100644 index 0000000..f96b975 --- /dev/null +++ b/internal/app/router/ens.go @@ -0,0 +1,13 @@ +package router + +import ( + "github.com/gin-gonic/gin" + v1 "nft-collect/internal/app/api/v1" +) + +func InitEnsRouter(Router *gin.RouterGroup) { + ensRouter := Router.Group("ens") + { + ensRouter.GET("/:q", v1.GetEnsRecords) + } +} diff --git a/internal/app/router/system.go b/internal/app/router/system.go new file mode 100644 index 0000000..9edc464 --- /dev/null +++ b/internal/app/router/system.go @@ -0,0 +1,15 @@ +package router + +import ( + "github.com/gin-gonic/gin" + v1 "nft-collect/internal/app/api/v1" +) + +func InitSystemRouter(Router *gin.RouterGroup) { + systemRouterWithAuth := Router.Group("system") + { + systemRouterWithAuth.GET("/contract/default", v1.GetDefaultContract) + systemRouterWithAuth.POST("/contract/default", v1.AddDefaultContract) + systemRouterWithAuth.DELETE("/contract/default", v1.DelDefaultContract) + } +} diff --git a/internal/app/service/account.go b/internal/app/service/account.go new file mode 100644 index 0000000..a26a8f7 --- /dev/null +++ b/internal/app/service/account.go @@ -0,0 +1,199 @@ +package service + +import ( + "github.com/ethereum/go-ethereum/common" + "gorm.io/gorm" + "nft-collect/internal/app/global" + "nft-collect/internal/app/model" + "nft-collect/internal/app/model/response" + "nft-collect/internal/app/utils" + "nft-collect/pkg/slice" + "strings" +) + +// GetContract +// @description: +// @param: address string +// @return: res []response.GetContractRes, err error +func GetContract(address, account string) (res []response.GetContractRes, err error) { + db := global.DB + var user model.Account + var dealList []string + errFirst := db.Model(&model.Account{}).Where("address = ?", address).First(&user).Error + if errFirst != nil && errFirst != gorm.ErrRecordNotFound { + return res, errFirst + } + var contractDefault []string + err = db.Model(&model.ContractDefault{}).Raw("SELECT contract_id FROM contract_default").Scan(&contractDefault).Error + if errFirst == gorm.ErrRecordNotFound { + initAccount(address) + if err = db.Model(&model.Account{}).Where("address = ?", address).First(&user).Error; err != nil { + return + } + dealList = contractDefault + } else if address != common.HexToAddress("0").String() { + dealList = slice.DiffSlice[string](contractDefault, user.ContractIDs) + go updateAllCollection(address, dealList, false) // update all collection + } + + if len(user.ContractIDs) != len(user.Counts) { + updateContractCount(address) + } + // TODO 查询默认合约 + + contractMap := make(map[string]int64) + for _, id := range dealList { + // TODO 优化 + var count int64 + err = db.Model(&model.Collection{}). + Raw("SElECT COUNT(1) FROM collection a JOIN contract b ON a.contract_address=b.contract_address AND a.chain=b.chain WHERE b.id = ? AND account_address= ? AND a.flag=2", id, address). + Scan(&count).Error + contractMap[id] = count + } + // show different counts + if address == account { + for i, _ := range user.ContractIDs { + contractMap[user.ContractIDs[i]] = user.Counts[i] + } + } else { + for i, _ := range user.ContractIDs { + contractMap[user.ContractIDs[i]] = user.CountsShow[i] + } + } + // slice as query + var contractIDs []string + for _, c := range dealList { + contractIDs = append(contractIDs, c) + } + for _, c := range user.ContractIDs { + contractIDs = append(contractIDs, c) + } + // get contract detail + var contract []model.Contract + err = db.Model(&model.Contract{}).Where("id", contractIDs).Find(&contract).Error + if err != nil { + return res, err + } + // order by contractIDs + for _, id := range contractIDs { + for _, c := range contract { + if id == c.ID { + if contractMap[c.ID] == 0 { + break + } + res = append(res, response.GetContractRes{Contract: c, Count: contractMap[c.ID]}) + break + } + } + } + return res, err +} + +// initAccount +// @description: init account +// @param: address string +// @return: err error +func initAccount(address string) (err error) { + var uuidList []string + // 创建 + for _, v := range global.CONFIG.NFT.DefContract { + var id string + db := global.DB.Model(&model.Contract{}).Select("id") + temp := strings.Split(v, "::") + if len(temp) == 0 { + continue + } + db.Where("contract_address", strings.ToLower(temp[0])) + if len(temp) == 2 { + db.Where("chain", temp[1]) + } + errFirst := db.First(&id).Error + if errFirst != nil { + if errFirst != gorm.ErrRecordNotFound { + return errFirst + } + continue + } + uuidList = append(uuidList, id) + } + user := &model.Account{Address: address} + err = global.DB.Model(&model.Account{}).Create(&user).Error + if err != nil { + return err + } + updateAllCollection(address, uuidList, true) + return err +} + +// updateContractCount +// @description: update the account contract count +// @param: address string +// @return: err error +func updateContractCount(address string) (err error) { + db := global.DB + var user model.Account + if err = db.Model(&model.Account{}).Select("contract_ids").Where("address", address).First(&user).Error; err != nil { + return err + } + var counts []int64 + var countsShow []int64 + var contractIDs []string + for _, v := range user.ContractIDs { + var nftContract model.Contract + if errErrFind := db.Model(&model.Contract{}).Where("id", v).First(&nftContract).Error; errErrFind != nil { + continue + } + var count int64 + var countShow int64 + + // TODO: 添加状态 需要确定是否过滤 + err = db.Model(&model.Collection{}).Where("chain", nftContract.Chain). + Where("contract_address", nftContract.ContractAddress).Where("account_address", address).Where("flag", 2). + Count(&count).Error + if err != nil { + return err + } + err = db.Model(&model.Collection{}).Where("chain", nftContract.Chain). + Where("contract_address", nftContract.ContractAddress).Where("account_address", address).Where("flag", 2).Where("status", 2). + Count(&countShow).Error + if err != nil { + return err + } + contractIDs = append(contractIDs, v) + counts = append(counts, count) + countsShow = append(countsShow, countShow) + } + if err = db.Model(&model.Account{}).Where("address", address).Updates(model.Account{ContractIDs: contractIDs, Counts: counts, CountsShow: countsShow}).Error; err != nil { + return err + } + + // TODO: 清除零数量的合约 + return nil +} + +// addContractToUser +// @description: add contract to user +// @param: id string, address string +// @return: err error +func addContractToUser(id string, address string) (err error) { + db := global.DB.Model(&model.Account{}) + var user model.Account + err = db.Where("address", address).First(&user).Error + + var contractIDs []string + for _, c := range user.ContractIDs { + contractIDs = append(contractIDs, c) + } + if utils.SliceIsExist[string](contractIDs, id) { + return + } + + if err != nil { + return + } + err = db. + Where("id", user.ID). + Update("contract_ids", gorm.Expr("\"contract_ids\"||?", "{"+id+"}")).Error + + return err +} diff --git a/internal/app/service/cahce.go b/internal/app/service/cahce.go new file mode 100644 index 0000000..35ecf4a --- /dev/null +++ b/internal/app/service/cahce.go @@ -0,0 +1,22 @@ +package service + +import ( + "fmt" + "nft-collect/internal/app/global" + "strings" +) + +func DeleteCache(address string) { + iterator := global.Cache.Cache.Iterator() + url := fmt.Sprintf("/account/own/%s", address) + for iterator.SetNext() { + entryInfo, err := iterator.Value() + if err == nil { + // 处理key和value + if strings.Contains(strings.ToLower(entryInfo.Key()), url) { + fmt.Println("delete:", entryInfo.Key()) + global.Cache.Cache.Delete(entryInfo.Key()) + } + } + } +} diff --git a/internal/app/service/collection.go b/internal/app/service/collection.go new file mode 100644 index 0000000..244c37f --- /dev/null +++ b/internal/app/service/collection.go @@ -0,0 +1,506 @@ +package service + +import ( + "encoding/json" + "errors" + "fmt" + "github.com/ethereum/go-ethereum/common" + "github.com/imroc/req/v3" + "github.com/tidwall/gjson" + "go.uber.org/zap" + "gorm.io/gorm" + "gorm.io/gorm/clause" + "nft-collect/internal/app/config" + "nft-collect/internal/app/global" + "nft-collect/internal/app/model" + "nft-collect/internal/app/model/request" + "nft-collect/internal/app/model/response" + "nft-collect/pkg/slice" + "sync" + "time" +) + +// UpdatedCollection +// @description: updated Collection status +// @param: req request.GetCollectionReq +// @return: total int64, res []model.NFT, err error +func UpdatedCollection(req request.UpdatedCollectionReq, address string) (err error) { + raw := global.DB.Model(&model.Collection{}).Where("id = ?", req.ID).Update("status", req.Status) + if raw.RowsAffected == 0 { + return errors.New("error") + } + // 插入配置 + var contractID string + err = global.DB.Raw("SELECT b.id FROM collection a LEFT JOIN contract b ON a.chain=b.chain AND a.contract_address=b.contract_address WHERE a.id= ? ", req.ID). + Scan(&contractID).Error + if err != nil { + return err + } + addContractToUser(contractID, address) + go updateContractCount(address) + return raw.Error +} + +// GetCollection +// @description: get collection +// @param: req request.GetCollectionReq +// @return: total int64, res []model.NFT, err error +func GetCollection(req request.GetCollectionReq, account string) (total, totalPublic, totalHidden int64, res []response.GetCollection, err error) { + limit := req.PageSize + offset := req.PageSize * (req.Page - 1) + + db := global.DB.Model(&model.Collection{}).Select("collection.*,contract.contract_logo"). + Joins("left join contract ON contract.chain=collection.chain AND contract.contract_address=collection.contract_address"). + Where("collection.flag", 2).Where("collection.account_address", req.AccountAddress) + if req.Search != "" { + db.Where("token_id ILIKE ? OR name ILIKE ?", "%"+req.Search+"%", "%"+req.Search+"%") + } + + if req.ContractID != "" { + var contract model.Contract + if err := global.DB.Model(&model.Contract{}).Where("id", req.ContractID).First(&contract).Error; err != nil { + global.LOG.Error("error first", zap.Error(err)) + return total, totalPublic, totalHidden, res, err + } + db.Where("collection.chain", contract.Chain).Where("collection.contract_address", contract.ContractAddress) + } + err = db.Count(&total).Error + if err != nil { + return + } + if err = db.Session(&gorm.Session{}).Where("collection.status", 1).Count(&totalHidden).Error; err != nil { + return + } + if err = db.Session(&gorm.Session{}).Where("collection.status", 2).Count(&totalPublic).Error; err != nil { + return + } + + if req.Status != 0 { + db.Where("collection.status", req.Status) + } else if req.AccountAddress != account { + db.Where("collection.status", 2) + } + + if req.Sort != "asc" && req.Sort != "desc" { + req.Sort = "desc" + } + orderBy := fmt.Sprintf("own_timestamp %s", req.Sort) + err = db.Limit(limit).Offset(offset).Order(orderBy).Find(&res).Error + if err != nil { + return total, totalPublic, totalHidden, res, err + } + + return +} + +// GetCollectionByContract +// @description: get collection by contract +// @param: req request.GetCollectionReq +// @return: total int64, res []model.NFT, err error +func GetCollectionByContract(req request.GetCollectionReq) (total int64, res []model.Collection, err error) { + var api config.APIConfig + for _, v := range global.CONFIG.NFT.APIConfig { + if v.ChainID == req.ChainID { + api = v + break + } + } + if api.Chain == "" { + return total, res, errors.New("not support chain") + } + if req.Page == 1 { + var firstCollection model.Collection + errFind := global.DB.Model(&model.Collection{}). + Select("updated_at"). + Where("account_address", req.AccountAddress). + Where("contract_address", req.ContractAddress). + Order("updated_at desc"). + First(&firstCollection).Error + if errFind == gorm.ErrRecordNotFound { + wg := new(sync.WaitGroup) + wg.Add(2) + go addCollectionByContract(wg, req.AccountAddress, "erc721", api, req.ContractAddress) + go addCollectionByContract(wg, req.AccountAddress, "erc1155", api, req.ContractAddress) + wg.Wait() + } else { + if firstCollection.UpdatedAt.Before(time.Now().Add(-time.Duration(global.CONFIG.NFT.CacheTime) * time.Minute)) { + fmt.Println("缓存") + wg := new(sync.WaitGroup) + wg.Add(2) + go addCollectionByContract(wg, req.AccountAddress, "erc721", api, req.ContractAddress) + go addCollectionByContract(wg, req.AccountAddress, "erc1155", api, req.ContractAddress) + } + } + } + limit := req.PageSize + offset := req.PageSize * (req.Page - 1) + db := global.DB.Model(&model.Collection{}).Where("account_address", req.AccountAddress).Where("contract_address", req.ContractAddress).Where("chain", api.Chain) + err = db.Count(&total).Error + if err != nil { + return total, res, err + } + err = db.Limit(limit).Offset(offset).Order("own_timestamp desc").Find(&res).Error + if err != nil { + return total, res, err + } + + return +} + +// AddCollection +// @description: add collection by ids +// @param: ids []string, address string +// @return: err error +func AddCollection(ids []string, address string, flag int64) (err error) { + tx := global.DB.Begin() + if flag == 2 { + idsMap := make(map[string]struct{}) + for _, id := range ids { + if id == "" { + continue + } + raw := tx.Model(&model.Collection{}).Where("id", id).Updates(map[string]interface{}{"flag": 2, "status": 2}) + if raw.RowsAffected > 0 { + var nft model.Collection + errNft := tx.Model(&model.Collection{}).Where("id", id).First(&nft).Error + if errNft != nil { + continue + } + var idContract string + errNFTContract := tx.Model(&model.Contract{}).Select("id").Where("chain", nft.Chain). + Where("contract_address", nft.ContractAddress).First(&idContract).Error + if errNFTContract != nil { + continue + } + idsMap[idContract] = struct{}{} + } + } + if tx.Commit().Error != nil { + return err + } + + for k, _ := range idsMap { + _ = addContractToUser(k, address) + } + } else if flag == 1 { + for _, id := range ids { + if id == "" { + continue + } + _ = tx.Model(&model.Collection{}).Where("id", id).Updates(map[string]interface{}{"flag": 1, "status": 1}) + } + if tx.Commit().Error != nil { + return err + } + } + updateContractCount(address) + + return nil +} + +// updateAllCollection +// @description: update account all collection +// @param: address string +func updateAllCollection(address string, uuidList []string, init bool) { + var count int64 + db := global.DB.Model(&model.Account{}) + db.Where("(updated_at < ?) AND total < 100 AND address = ? ", time.Now().Add(-time.Duration(global.CONFIG.NFT.CacheTime)*time.Minute), address) + err := db.Count(&count).Error + if err != nil { + global.LOG.Error("error getting", zap.Error(err)) + return + } + if count == 0 && !init { + return + } + // 更新时间 + err = global.DB.Model(&model.Account{}). + Where("address", address).Update("updated_at", time.Now()).Error + if err != nil { + global.LOG.Error("error update", zap.Error(err)) + return + } + var total int + for _, api := range global.CONFIG.NFT.APIConfig { + t, _ := addAllCollection(address, api, "") + total += t + } + err = global.DB.Model(&model.Account{}). + Where("address", address).Update("total", total).Error + if err != nil { + global.LOG.Error("error update", zap.Error(err)) + return + } + for _, v := range uuidList { + var contracts string + if err = global.DB.Model(&model.Contract{}).Select("contract_address").Where("id", v).First(&contracts).Error; err != nil { + return + } + if err = global.DB.Model(&model.Collection{}).Where("account_address", address).Where("contract_address", contracts).Updates(map[string]interface{}{"status": 2, "flag": 2}).Error; err != nil { + return + } + } + // 更新用户展示数量 + _ = updateContractCount(address) +} + +// addCollection +// @description: add or update collection to account +// @param: address string, erc_type string, api config.APIConfig +// @return: err error +func addCollectionByContract(wg *sync.WaitGroup, address string, erc_type string, api config.APIConfig, contract string) (err error) { + defer wg.Done() + var user model.Account + contractList := make(map[common.Address]struct{}) + contractList[common.HexToAddress(contract)] = struct{}{} + if err = global.DB.Model(&model.Account{}).Select("contract_ids").Where("address", address).First(&user).Error; err != nil { + return err + } + for _, v := range user.ContractIDs { + var contracts string + if err = global.DB.Model(&model.Contract{}).Select("contract_address").Where("id", v).First(&contracts).Error; err != nil { + return err + } + contractList[common.HexToAddress(contracts)] = struct{}{} + } + assetsUrl := fmt.Sprintf("https://%s.nftscan.com/api/v2/account/own/%s?limit=300&erc_type=%s&contract_address=%s", api.APIPreHost, address, erc_type, contract) + var cursor string + var nft []model.Collection + var errFlag bool + client := req.C().SetTimeout(120*time.Second). + SetCommonRetryCount(1). + SetUserAgent("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36"). + SetCommonHeader("Referer", "https://docs.nftscan.com/") + if erc_type == "erc721" { + client.SetCommonHeader("X-API-KEY", global.CONFIG.NFT.ApiKey) + } else { + client.SetCommonHeader("X-API-KEY", global.CONFIG.NFT.ApiKeyBackup) + } + i := 0 + for { + fmt.Println("i", i) + i++ + fmt.Println(time.Now()) + reqUrl := assetsUrl + fmt.Sprintf("&cursor=%s", cursor) + fmt.Println(reqUrl) + + req, errReq := client.R().Get(reqUrl) + if errReq != nil { + fmt.Println(errReq) + errFlag = true + break + } + fmt.Println(time.Now()) + res := req.String() + + if gjson.Get(res, "data.total").Uint() == 0 { + fmt.Println("No data") + break + } + + if gjson.Get(res, "code").String() != "200" { + errFlag = true + break + } + var nftScan []model.NFTScanOwn + if errParse := json.Unmarshal([]byte(gjson.Get(res, "data.content").String()), &nftScan); errParse != nil { + fmt.Println(errParse) + errFlag = true + break + } + + for _, v := range nftScan { + if _, ok := contractList[common.HexToAddress(v.ContractAddress)]; !ok { + continue + } + nft = append(nft, model.Collection{Chain: api.Chain, AccountAddress: address, NFTScanOwn: v}) + } + cursor = gjson.Get(res, "data.next").String() + if cursor == "" { + break + } + } + + if len(nft) == 0 { + return nil + } + // 保存数据 + if err = global.DB.Model(&model.Collection{}).Omit("status", "flag").Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "chain"}, {Name: "account_address"}, {Name: "contract_address"}, {Name: "token_id"}}, + UpdateAll: true, + }).Create(&nft).Error; err != nil { + return err + } + _ = errFlag + //if !errFlag { + // go filtrateNFT(address, &nft, api) + //} + // get item details + temp := make(map[string]struct{}) + for _, v := range nft { + if _, ok := temp[v.ContractAddress]; !ok { + temp[v.ContractAddress] = struct{}{} + } + } + if len(temp) == 0 { + return + } + go ItemFiltrateAndDown(temp, api) + + return nil +} + +// addCollection +// @description: add or update collection to account +// @param: address string, erc_type string, api config.APIConfig +// @return: err error +func addAllCollection(address string, api config.APIConfig, contract string) (total int, err error) { + defer func() { + if e := recover(); e != nil { + _ = global.DB.Model(&model.Account{}).Where("address", address).Update("special", true).Error + return + } + }() + var user model.Account + contractList := make(map[common.Address]struct{}) + contractList[common.HexToAddress(contract)] = struct{}{} + + if err = global.DB.Model(&model.Account{}).Select("contract_ids").Where("address", address).First(&user).Error; err != nil { + return total, err + } + // TODO 默认合约 + var contractDefault []string + err = global.DB.Model(&model.ContractDefault{}).Raw("SELECT contract_id FROM contract_default").Scan(&contractDefault).Error + dealList := slice.DiffSlice[string](contractDefault, user.ContractIDs) + + for _, v := range append(user.ContractIDs, dealList...) { + var contracts string + if err = global.DB.Model(&model.Contract{}).Select("contract_address").Where("id", v).First(&contracts).Error; err != nil { + return total, err + } + contractList[common.HexToAddress(contracts)] = struct{}{} + } + assetsUrl := fmt.Sprintf("https://%s.nftscan.com/api/v2/account/own/all/%s?show_attribute=false", api.APIPreHost, address) + //var cursor string + var nft []model.Collection + var tryTimes uint + var errFlag bool + client := req.C().SetTimeout(500*time.Second). + SetCommonRetryCount(1). + SetUserAgent("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36"). + SetCommonHeader("Referer", "https://docs.nftscan.com/"). + SetCommonHeader("X-API-KEY", global.CONFIG.NFT.ApiKey) + + //reqUrl := assetsUrl + fmt.Sprintf("&cursor=%s", cursor) + reqUrl := assetsUrl + fmt.Println(reqUrl) + + req, errReq := client.R().Get(assetsUrl) + if errReq != nil { + tryTimes += 1 + errFlag = true + } + req.ErrorResult() + res := req.String() + + if gjson.Get(res, "code").String() != "200" { + tryTimes += 1 + errFlag = true + } + var nftScan []model.NFTScanOwn + arr := gjson.Get(res, "data.#.assets").Array() + for i, _ := range arr { + var v []model.NFTScanOwn + if errParse := json.Unmarshal([]byte(arr[i].String()), &v); errParse != nil { + tryTimes += 1 + errFlag = true + } + nftScan = append(nftScan, v...) + } + total = len(nftScan) + for _, v := range nftScan { + if _, ok := contractList[common.HexToAddress(v.ContractAddress)]; !ok { + continue + } + nft = append(nft, model.Collection{Chain: api.Chain, AccountAddress: address, NFTScanOwn: v}) + } + + if len(nft) == 0 { + return total, nil + } + // 保存数据 + if err = global.DB.Model(&model.Collection{}).Omit("status", "flag").Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "chain"}, {Name: "account_address"}, {Name: "contract_address"}, {Name: "token_id"}}, + UpdateAll: true, + }).Create(&nft).Error; err != nil { + return total, err + } + if !errFlag { + go filtrateNFT(address, &nft, api) + } + // get item details + temp := make(map[string]struct{}) + for _, v := range nft { + if _, ok := temp[v.ContractAddress]; !ok { + temp[v.ContractAddress] = struct{}{} + } + } + if len(temp) == 0 { + return + } + go ItemFiltrateAndDown(temp, api) + + return total, nil +} + +// filtrateNFT +// @description: if collection not this account,remove collection in this account +// @param: address string, erc_type string, api config.APIConfig +// @return: err error +func filtrateNFT(address string, res *[]model.Collection, api config.APIConfig) (err error) { + var nftSlice []string + var ids []string + for _, v := range *res { + ids = append(ids, v.ID) + } + global.DB.Model(&model.Collection{}).Select("id").Where("chain", api.Chain). + Where("account_address", address). + Find(&nftSlice) + dealList := slice.DiffSlice[string](nftSlice, ids) + if len(dealList) == 0 { + return nil + } + err = global.DB.Model(&model.Collection{}).Where("id", dealList).Delete(&model.Collection{}).Error + return err +} + +func RefreshUserData(address string) (err error) { + if address == common.HexToAddress("0").String() { + return nil + } + db := global.DB + var user model.Account + var dealList []string + errFirst := db.Model(&model.Account{}).Where("address = ?", address).First(&user).Error + if errFirst != nil && errFirst != gorm.ErrRecordNotFound { + return errFirst + } + + var contractDefault []string + err = db.Model(&model.ContractDefault{}).Raw("SELECT contract_id FROM contract_default").Scan(&contractDefault).Error + if errFirst == gorm.ErrRecordNotFound { + initAccount(address) + if err = db.Model(&model.Account{}).Where("address = ?", address).First(&user).Error; err != nil { + return + } + dealList = contractDefault + } else if address != common.HexToAddress("0").String() { + dealList = slice.DiffSlice[string](contractDefault, user.ContractIDs) + go updateAllCollection(address, dealList, false) // update all collection + } + + if len(user.ContractIDs) != len(user.Counts) { + updateContractCount(address) + } + return nil +} diff --git a/internal/app/service/collection_test.go b/internal/app/service/collection_test.go new file mode 100644 index 0000000..8150a4b --- /dev/null +++ b/internal/app/service/collection_test.go @@ -0,0 +1,63 @@ +package service + +import ( + "encoding/json" + "fmt" + "github.com/ethereum/go-ethereum/common" + "github.com/imroc/req/v3" + "github.com/tidwall/gjson" + "nft-collect/internal/app/model" + "testing" + "time" +) + +func Test_addCollectionByContract(t *testing.T) { + assetsUrl := fmt.Sprintf("https://polygonapi.nftscan.com/api/v2/account/own/0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266?limit=100&erc_type=erc721&show_attribute=false&sort_field=&sort_direction=") + client := req.C().SetTimeout(120*time.Second). + SetCommonRetryCount(1). + SetUserAgent("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36"). + SetCommonHeader("Referer", "https://docs.nftscan.com/"). + SetCommonHeader("X-API-KEY", "xxxxxxx") + + var cursor string + contractList := make(map[common.Address]struct{}) + contractList[common.HexToAddress("0x60f028c82f9f3bf71e0c13fe9e8e7f916b345c00")] = struct{}{} + var nft []model.Collection + for { + fmt.Println(time.Now()) + reqUrl := assetsUrl + fmt.Sprintf("&cursor=%s", cursor) + fmt.Println(reqUrl) + + req, errReq := client.R().Get(reqUrl) + if errReq != nil { + fmt.Println(errReq) + break + } + res := req.String() + + if gjson.Get(res, "data.total").Uint() == 0 { + fmt.Println("No data") + break + } + + if gjson.Get(res, "code").String() != "200" { + break + } + var nftScan []model.NFTScanOwn + if errParse := json.Unmarshal([]byte(gjson.Get(res, "data.content").String()), &nftScan); errParse != nil { + fmt.Println(errParse) + break + } + for _, v := range nftScan { + if _, ok := contractList[common.HexToAddress(v.ContractAddress)]; !ok { + continue + } + nft = append(nft, model.Collection{Chain: "polygonapi", AccountAddress: "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266", NFTScanOwn: v}) + } + cursor = gjson.Get(res, "data.next").String() + if cursor == "" { + break + } + } + fmt.Println(len(nft)) +} diff --git a/internal/app/service/contract.go b/internal/app/service/contract.go new file mode 100644 index 0000000..340aebb --- /dev/null +++ b/internal/app/service/contract.go @@ -0,0 +1,127 @@ +package service + +import ( + "bytes" + "encoding/json" + "fmt" + "github.com/imroc/req/v3" + "go.uber.org/zap" + "gorm.io/gorm/clause" + "io" + "nft-collect/internal/app/config" + "nft-collect/internal/app/global" + "nft-collect/internal/app/model" + "nft-collect/internal/app/model/receive" + "nft-collect/pkg/slice" + "os" + "path/filepath" + "strings" +) + +// ItemFiltrateAndDown +// @description: filtrate done and down new connection item details +// @param: contractMap map[string]struct{}, api config.APIConfig +// @return: err error +func ItemFiltrateAndDown(contractMap map[string]struct{}, api config.APIConfig) (err error) { + var addressExist []string + var addressList []string + for key, _ := range contractMap { + addressList = append(addressList, key) + } + if err = global.DB.Model(&model.Contract{}).Select("contract_address").Where("contract_address IN ? AND Status = 2", addressList).Find(&addressExist).Error; err != nil { + return err + } + // 待处理 slice + dealList := slice.DiffSlice[string](addressList, addressExist) + if len(dealList) == 0 { + return nil + } + //fmt.Println("待处理", dealList) + res := downloadItem(dealList, api) + if len(res) == 0 { + return + } + // 保存数据 + if err = global.DB.Model(&model.Contract{}).Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "chain"}, {Name: "contract_address"}}, + UpdateAll: true, + }).Create(&res).Error; err != nil { + return err + } + return nil +} + +// downloadItem +// @description: down new connection item details +// @param: contractAddress []string, api config.APIConfig +// @return: res []model.NFTContract +func downloadItem(contractAddress []string, api config.APIConfig) (res []model.Contract) { + client := req.C().SetCommonHeader("X-API-KEY", global.CONFIG.NFT.ApiKeyPro) + clientDown := req.C() + for _, address := range contractAddress { + url := fmt.Sprintf("https://%s.nftscan.com/api/v2/collections/%s?show_attribute=false", api.APIPreHost, address) + fmt.Println(url) + resp, err := client.R().Get(url) + if err != nil { + continue + } + if !strings.Contains(resp.GetStatus(), "200") { + continue + } + var itemModel receive.GetItemModel + if errParse := json.Unmarshal(resp.Bytes(), &itemModel); errParse != nil { + continue + } + p := itemModel.Data + + if p.ContractAddress == "" { + continue + } + temp := model.Contract{Chain: api.Chain, ContractAddress: address, ContractName: p.Name, + ContractLogo: p.LogoURL, ContractBanner: p.BannerURL, ContractDescription: p.Description, + ContractWebsite: p.Website, ContractOwner: p.Owner, Status: 2, + } + if itemModel.Data.LogoURL != "" { + temp.ContractLogo, err = downloadLogo(clientDown, temp, api) + if err != nil { + global.LOG.Error("downloadLogo error: ", zap.Error(err)) + } + } + res = append(res, temp) + } + return +} + +// downloadLogo +// @description: download logo +// @param: client *req.Client, nftContract model.NFTContract, api config.APIConfig +// @return: res string, err error +func downloadLogo(client *req.Client, nftContract model.Contract, api config.APIConfig) (res string, err error) { + res = global.CONFIG.NFT.LogoPath + "logo.png" + baseUrl := nftContract.ContractLogo + fileName := fmt.Sprintf("%s%s", nftContract.ContractAddress, filepath.Ext(nftContract.ContractLogo)) + dirPath := global.CONFIG.NFT.LogoPath + "/" + api.Chain + "/" + err = os.MkdirAll(dirPath, os.ModePerm) + if err != nil { + global.LOG.Error("Error MkdirAll", zap.Error(err)) + return res, err + } + resp, err := client.R().Get(baseUrl) + if err != nil || !strings.Contains(resp.GetStatus(), "200") { + return res, err + } + defer resp.Body.Close() + f, err := os.Create(dirPath + fileName) + if err != nil { + global.LOG.Error("Error creating", zap.Error(err)) + return res, err + } + body, _ := io.ReadAll(resp.Body) + _, err = io.Copy(f, bytes.NewReader(body)) + if err != nil { + global.LOG.Error("Error io.Copy", zap.Error(err)) + return res, err + } + f.Close() + return "/" + dirPath + fileName, nil +} diff --git a/internal/app/service/ens.go b/internal/app/service/ens.go new file mode 100644 index 0000000..eefcc14 --- /dev/null +++ b/internal/app/service/ens.go @@ -0,0 +1,143 @@ +package service + +import ( + "context" + "errors" + "fmt" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/imroc/req/v3" + "github.com/tidwall/gjson" + ens "github.com/wealdtech/go-ens/v3" + "go.uber.org/zap" + "gorm.io/gorm/clause" + "nft-collect/internal/app/global" + "nft-collect/internal/app/model" + "nft-collect/internal/app/model/response" + "strings" + "time" +) + +// GetEnsRecords 获取ENS记录 +func GetEnsRecords(ctx context.Context, q string) (result interface{}, err error) { + // Get records from Cache + if strings.Contains(q, ".") { + var res model.Ens + errNil := global.DB.Model(&model.Ens{}).Where("domain", q).First(&res).Error + if errNil != nil { + return resolveName(ctx, q) + } + if res.UpdatedAt.Before(time.Now().AddDate(0, 0, -1)) { + go resolveName(ctx, q) + } + return response.GetEnsResponse{Address: res.Address, Domain: res.Domain, Avatar: res.Avatar}, nil + } else { + if !common.IsHexAddress(q) { + return result, errors.New("RecordNotFound") + } + var res model.Ens + errNil := global.DB.Model(&model.Ens{}).Where("address", q).First(&res).Error + if errNil != nil { + return resolveAddress(ctx, common.HexToAddress(q).Hex()) + } + if res.UpdatedAt.Before(time.Now().AddDate(0, 0, -1)) { + go resolveAddress(ctx, common.HexToAddress(q).Hex()) + } + return response.GetEnsResponse{Address: res.Address, Domain: res.Domain, Avatar: res.Avatar}, nil + } +} + +// resolveName 解析 ENS 名称 +func resolveName(ctx context.Context, input string) (result response.GetEnsResponse, err error) { + result.Domain = input + client, err := ethclient.Dial(global.CONFIG.NFT.EnsRpc) + if err != nil { + return result, errors.New("UnexpectedError") + } + // Resolve a name to an address. + address, err := ens.Resolve(client, input) + if err != nil { + return result, nil + } + result.Address = address.Hex() + text, errResolver := ens.NewResolver(client, input) + if errResolver == nil { + avatar, errAvatar := text.Text("avatar") + if errAvatar == nil { + result.Avatar = resolveAvatar(avatar) + } + } + if err != nil { + return result, errors.New("UnexpectedError") + } + ens := model.Ens{Address: address.Hex(), Domain: input, Avatar: result.Avatar} + err = global.DB.Model(&model.Ens{}).Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "address"}, {Name: "domain"}}, + UpdateAll: true, + }).Create(&ens).Error + if err != nil { + return result, errors.New("UnexpectedError") + } + return result, nil +} + +// resolveAddress 解析 ENS 地址 +func resolveAddress(ctx context.Context, input string) (result response.GetEnsResponse, err error) { + result.Address = input + client, err := ethclient.Dial(global.CONFIG.NFT.EnsRpc) + if err != nil { + return result, errors.New("UnexpectedError") + } + // Resolve address to name + domain, err := ens.ReverseResolve(client, common.HexToAddress(input)) + if err != nil { + return result, nil + } + result.Domain = domain + text, errResolver := ens.NewResolver(client, domain) + if errResolver == nil { + avatar, errAvatar := text.Text("avatar") + if errAvatar == nil { + result.Avatar = resolveAvatar(avatar) + } + } + + if err != nil { + return result, errors.New("UnexpectedError") + } + ens := model.Ens{Address: input, Domain: domain, Avatar: result.Avatar} + if err = global.DB.Model(&model.Ens{}).Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "address"}, {Name: "domain"}}, + UpdateAll: true, + }).Create(&ens).Error; err != nil { + return result, errors.New("UnexpectedError") + } + return result, nil +} + +func resolveAvatar(text string) (res string) { + if !strings.Contains(text, "eip155") { + return text + } + fmt.Println(text) + defer func() { + if err := recover(); err != nil { + res = text + } + }() + temp := strings.Split(text, "/") + contractAddr := strings.Split(temp[1], ":")[1] + assetsUrl := fmt.Sprintf("https://restapi.nftscan.com/api/v2/assets/%s/%s?show_attribute=false", contractAddr, temp[2]) + fmt.Println(assetsUrl) + client := req.C().SetTimeout(120*time.Second). + SetCommonRetryCount(1).SetCommonHeader("X-API-KEY", global.CONFIG.NFT.ApiKey). + SetUserAgent("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36"). + SetCommonHeader("Referer", "https://docs.nftscan.com/") + + response, err := client.R().Get(assetsUrl) + if err != nil { + global.LOG.Error("Get error ", zap.Error(err)) + return text + } + return "ipfs://" + gjson.Get(response.String(), "data.image_uri").String() +} diff --git a/internal/app/service/system.go b/internal/app/service/system.go new file mode 100644 index 0000000..c1b08a3 --- /dev/null +++ b/internal/app/service/system.go @@ -0,0 +1,70 @@ +package service + +import ( + "github.com/pkg/errors" + "nft-collect/internal/app/global" + "nft-collect/internal/app/model" + "nft-collect/internal/app/model/request" + "nft-collect/internal/app/utils" + "strings" +) + +func GetDefaultContract() (list interface{}, err error) { + var contractList []model.Contract + err = global.DB.Raw("SELECT b.* FROM contract_default a LEFT JOIN contract b ON a.contract_id=b.id").Scan(&contractList).Error + return contractList, err +} + +func AddDefaultContract(req request.AddDefaultContractReq) (err error) { + for _, api := range global.CONFIG.NFT.APIConfig { + if req.Chain == api.Chain { + contractMap := map[string]struct{}{strings.ToLower(req.ContractAddress): struct{}{}} + ItemFiltrateAndDown(contractMap, api) + break + } + } + var contractID string + err = global.DB.Raw("SELECT id FROM contract WHERE contract_address=? AND chain = ?", req.ContractAddress, req.Chain).Scan(&contractID).Error + if err != nil { + return err + } + if contractID == "" { + return errors.New("contract not exist") + } + err = global.DB.Create(&model.ContractDefault{ContractID: contractID}).Error + return err +} + +func DelDefaultContract(req request.DelDefaultContractReq) (err error) { + var contract model.Contract + err = global.DB.Model(&model.Contract{}).Where("id", req.ID).First(&contract).Error + if err != nil { + return err + } + + var userList []model.Account + if err = global.DB.Model(&model.Account{}).Find(&userList).Error; err != nil { + return + } + tx := global.DB.Begin() + for _, user := range userList { + if utils.SliceIsExist(user.ContractIDs, req.ID) { + continue + } + err = tx.Model(&model.Collection{}). + Where("account_address", user.Address). + Where("contract_address", contract.ContractAddress). + Where("chain", contract.Chain). + Delete(&model.Collection{}).Limit(1000).Error + if err != nil { + tx.Rollback() + return err + } + } + err = tx.Model(&model.ContractDefault{}).Where("contract_id", req.ID).Delete(&model.ContractDefault{}).Error + if err != nil { + tx.Rollback() + return err + } + return tx.Commit().Error +} diff --git a/internal/app/utils/directory.go b/internal/app/utils/directory.go new file mode 100644 index 0000000..512aceb --- /dev/null +++ b/internal/app/utils/directory.go @@ -0,0 +1,50 @@ +package utils + +import ( + "errors" + "nft-collect/internal/app/global" + "os" + + "go.uber.org/zap" +) + +//@function: PathExists +//@description: 文件目录是否存在 +//@param: path string +//@return: bool, error + +func PathExists(path string) (bool, error) { + fi, err := os.Stat(path) + if err == nil { + if fi.IsDir() { + return true, nil + } + return false, errors.New("存在同名文件") + } + if os.IsNotExist(err) { + return false, nil + } + return false, err +} + +//@function: CreateDir +//@description: 批量创建文件夹 +//@param: dirs ...string +//@return: err error + +func CreateDir(dirs ...string) (err error) { + for _, v := range dirs { + exist, err := PathExists(v) + if err != nil { + return err + } + if !exist { + global.LOG.Debug("create directory" + v) + if err := os.MkdirAll(v, os.ModePerm); err != nil { + global.LOG.Error("create directory"+v, zap.Any(" error:", err)) + return err + } + } + } + return err +} diff --git a/internal/app/utils/file_operations.go b/internal/app/utils/file_operations.go new file mode 100644 index 0000000..6c4183e --- /dev/null +++ b/internal/app/utils/file_operations.go @@ -0,0 +1,75 @@ +package utils + +import ( + "os" + "path/filepath" + "reflect" + "strings" +) + +//@function: FileMove +//@description: 文件移动供外部调用 +//@param: src string, dst string(src: 源位置,绝对路径or相对路径, dst: 目标位置,绝对路径or相对路径,必须为文件夹) +//@return: err error + +func FileMove(src string, dst string) (err error) { + if dst == "" { + return nil + } + src, err = filepath.Abs(src) + if err != nil { + return err + } + dst, err = filepath.Abs(dst) + if err != nil { + return err + } + revoke := false + dir := filepath.Dir(dst) +Redirect: + _, err = os.Stat(dir) + if err != nil { + err = os.MkdirAll(dir, 0o755) + if err != nil { + return err + } + if !revoke { + revoke = true + goto Redirect + } + } + return os.Rename(src, dst) +} + +func DeLFile(filePath string) error { + return os.RemoveAll(filePath) +} + +//@function: TrimSpace +//@description: 去除结构体空格 +//@param: target interface (target: 目标结构体,传入必须是指针类型) +//@return: null + +func TrimSpace(target interface{}) { + t := reflect.TypeOf(target) + if t.Kind() != reflect.Ptr { + return + } + t = t.Elem() + v := reflect.ValueOf(target).Elem() + for i := 0; i < t.NumField(); i++ { + switch v.Field(i).Kind() { + case reflect.String: + v.Field(i).SetString(strings.TrimSpace(v.Field(i).String())) + } + } +} + +// FileExist 判断文件是否存在 +func FileExist(path string) bool { + fi, err := os.Lstat(path) + if err == nil { + return !fi.IsDir() + } + return !os.IsNotExist(err) +} diff --git a/internal/app/utils/http_request.go b/internal/app/utils/http_request.go new file mode 100644 index 0000000..13ac384 --- /dev/null +++ b/internal/app/utils/http_request.go @@ -0,0 +1,158 @@ +package utils + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "mime/multipart" + "net/http" + "strings" + "time" +) + +// GetRequest 发送GET请求 +// url: 请求地址 +// response: 请求返回的内容 +func GetRequest(url string) (res string, err error) { + defer func() { + if errRE := recover(); errRE != nil { + err = errors.New("error") + } + }() + tryTimes := 3 // 重试次数 + for i := 0; i < tryTimes; i++ { + // 超时时间:60秒 + client := &http.Client{Timeout: time.Second * 60} + req, errReq := http.NewRequest("GET", url, nil) + req.Header.Set("Accept", "*/*") + req.Header.Set("Content-Type", "application/json") + if errReq != nil { + err = errReq + continue + } + resp, errReq := client.Do(req) + if errReq != nil { + err = errReq + resp.Body.Close() + continue + } + result, _ := io.ReadAll(resp.Body) + resp.Body.Close() + return string(result), nil + } + return "", err +} + +// PostRequest 发送POST请求 +// url: 请求地址 +// data: POST请求提交的数据 +// contentType: 请求体格式,如:application/json +// content: 请求放回的内容 +func PostRequest(url string, data interface{}, contentType string) (res []byte, err error) { + defer func() { + if errRE := recover(); errRE != nil { + err = errors.New("error") + } + }() + tryTimes := 3 // 重试次数 + for i := 0; i < tryTimes; i++ { + // 超时时间:60秒 + client := &http.Client{Timeout: 60 * time.Second} + jsonStr, _ := json.Marshal(data) + resp, errReq := client.Post(url, contentType, bytes.NewBuffer(jsonStr)) + if errReq != nil { + err = errReq + resp.Body.Close() + continue + } + result, _ := io.ReadAll(resp.Body) + resp.Body.Close() + return result, nil + } + return res, err +} + +// PostFileRequest 发送POST请求 上传文件 +// url: 请求地址 +// data: POST请求提交的数据 +func PostFileRequest(url string, header *multipart.FileHeader) (res []byte, err error) { + defer func() { + if errRE := recover(); errRE != nil { + err = errors.New("error") + } + }() + file, err := header.Open() + if err != nil { + return res, err + } + defer file.Close() + + body := &bytes.Buffer{} + //创建一个multipart类型的写文件 + writer := multipart.NewWriter(body) + //使用给出的属性名paramName和文件名filePath创建一个新的form-data头 + part, err := writer.CreateFormFile("file", header.Filename) + if err != nil { + fmt.Println(" post err=", err) + } + //将源复制到目标,将file写入到part 是按默认的缓冲区32k循环操作的,不会将内容一次性全写入内存中,这样就能解决大文件的问题 + _, err = io.Copy(part, file) + err = writer.Close() + if err != nil { + fmt.Println(" post err=", err) + } + tryTimes := 3 // 重试次数 + for i := 0; i < tryTimes; i++ { + request, err := http.NewRequest("POST", url, body) + if err != nil { + return nil, err + } + // writer.FormDataContentType() : 返回w对应的HTTP multipart请求的Content-Type的值,多以multipart/form-data起始 + request.Header.Set("Content-Type", writer.FormDataContentType()) + // 超时时间:60秒 + client := &http.Client{Timeout: 60 * time.Second} + result, errReq := client.Do(request) + if errReq != nil { + err = errReq + // http返回的response的body必须close,否则就会有内存泄露 + result.Body.Close() + continue + } + res, _ = io.ReadAll(result.Body) + result.Body.Close() + return res, nil + } + return res, err +} + +func GraphQlRequest(url string, payload *strings.Reader) (body []byte, err error) { + defer func() { + if errRE := recover(); errRE != nil { + err = errors.New("error") + } + }() + client := &http.Client{} + req, err := http.NewRequest("POST", url, payload) + + if err != nil { + fmt.Println(err) + return + } + req.Header.Add("Content-Type", "application/json") + + res, err := client.Do(req) + if err != nil { + fmt.Println(err) + return + } + defer res.Body.Close() + + body, err = io.ReadAll(res.Body) + if err != nil { + fmt.Println(err) + return + } + return +} diff --git a/internal/app/utils/jwt.go b/internal/app/utils/jwt.go new file mode 100644 index 0000000..1c7a432 --- /dev/null +++ b/internal/app/utils/jwt.go @@ -0,0 +1,100 @@ +package utils + +import ( + "errors" + "nft-collect/internal/app/global" + "time" + + "github.com/golang-jwt/jwt/v4" +) + +type JWT struct { + SigningKey []byte +} + +var ( + TokenExpired = errors.New("token is expired") + TokenNotValidYet = errors.New("token not active yet") + TokenMalformed = errors.New("that's not even a token") + TokenInvalid = errors.New("couldn't handle this token") +) + +func NewJWT() *JWT { + return &JWT{ + []byte(global.CONFIG.JWT.SigningKey), + } +} + +type CustomClaims struct { + BaseClaims + jwt.RegisteredClaims +} + +type BaseClaims struct { + UserID uint `json:"user_id,omitempty"` + UserName string `json:"username,omitempty"` + Address string `json:"address,omitempty"` +} + +func (j *JWT) CreateClaims(baseClaims BaseClaims) CustomClaims { + claims := CustomClaims{ + BaseClaims: baseClaims, + RegisteredClaims: jwt.RegisteredClaims{ + NotBefore: jwt.NewNumericDate(time.Now().Add(-1)), // 签名生效时间 + ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Duration(global.CONFIG.JWT.ExpiresTime) * time.Second)), // 过期时间 + Issuer: global.CONFIG.JWT.Issuer, // 签名的发行者 + }, + } + return claims +} + +// CreateToken 创建一个token +func (j *JWT) CreateToken(claims CustomClaims) (string, error) { + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + return token.SignedString(j.SigningKey) +} + +// ParseToken 解析 token +func (j *JWT) ParseToken(tokenString string) (*CustomClaims, error) { + token, err := jwt.ParseWithClaims(tokenString, &CustomClaims{}, func(token *jwt.Token) (i interface{}, e error) { + return j.SigningKey, nil + }) + if err != nil { + if ve, ok := err.(*jwt.ValidationError); ok { + if ve.Errors&jwt.ValidationErrorMalformed != 0 { + return nil, TokenMalformed + } else if ve.Errors&jwt.ValidationErrorExpired != 0 { + // Token is expired + return nil, TokenExpired + } else if ve.Errors&jwt.ValidationErrorNotValidYet != 0 { + return nil, TokenNotValidYet + } else { + return nil, TokenInvalid + } + } + } + if token != nil { + if claims, ok := token.Claims.(*CustomClaims); ok && token.Valid { + return claims, nil + } + return nil, TokenInvalid + + } else { + return nil, TokenInvalid + } +} + +// ParseToken 解析 token +func (j *JWT) ParseTokenWithNoAuth(tokenString string) (*CustomClaims, error) { + token, _ := jwt.ParseWithClaims(tokenString, &CustomClaims{}, func(token *jwt.Token) (i interface{}, e error) { + return j.SigningKey, nil + }) + if token != nil { + if claims, ok := token.Claims.(*CustomClaims); ok { + return claims, nil + } + return nil, TokenInvalid + } else { + return nil, TokenInvalid + } +} diff --git a/internal/app/utils/rotatelogs.go b/internal/app/utils/rotatelogs.go new file mode 100644 index 0000000..76cfb15 --- /dev/null +++ b/internal/app/utils/rotatelogs.go @@ -0,0 +1,28 @@ +package utils + +import ( + "nft-collect/internal/app/global" + "os" + + "github.com/natefinch/lumberjack" + "go.uber.org/zap/zapcore" +) + +//@function: GetWriteSyncer +//@description: zap logger中加入file-rotatelogs +//@return: zapcore.WriteSyncer, error + +func GetWriteSyncer(file string) zapcore.WriteSyncer { + lumberJackLogger := &lumberjack.Logger{ + Filename: file, // 日志文件的位置 + MaxSize: 10, // 在进行切割之前,日志文件的最大大小(以MB为单位) + MaxBackups: 200, // 保留旧文件的最大个数 + MaxAge: 30, // 保留旧文件的最大天数 + Compress: true, // 是否压缩/归档旧文件 + } + + if global.CONFIG.Zap.LogInConsole { + return zapcore.NewMultiWriteSyncer(zapcore.AddSync(os.Stdout), zapcore.AddSync(lumberJackLogger)) + } + return zapcore.AddSync(lumberJackLogger) +} diff --git a/internal/app/utils/utils.go b/internal/app/utils/utils.go new file mode 100644 index 0000000..fc7fc2c --- /dev/null +++ b/internal/app/utils/utils.go @@ -0,0 +1,108 @@ +package utils + +import ( + "encoding/json" + "github.com/ethereum/go-ethereum/accounts" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/crypto" + "golang.org/x/exp/constraints" + "regexp" +) + +// SliceIsExist 判断元素是否在slice +func SliceIsExist[T comparable](slice []T, val T) bool { + for _, item := range slice { + if item == val { + return true + } + } + return false +} + +func StructToMap(inter []any) map[string]interface{} { + var m map[string]interface{} + for _, v := range inter { + if v == nil { + continue + } + ja, _ := json.Marshal(v) + json.Unmarshal(ja, &m) + } + return m +} + +func MapPushStruct(m map[string]interface{}, inter []any) map[string]interface{} { + for _, v := range inter { + ja, _ := json.Marshal(v) + json.Unmarshal(ja, &m) + } + return m +} + +func SliceMax[T constraints.Ordered](slice []T) (index int, m T) { + for i, e := range slice { + if i == 0 || e > m { + m = e + index = i + } + } + return +} + +func SliceMin[T constraints.Ordered](slice []T) (index int, m T) { + for i, e := range slice { + if i == 0 || e < m { + m = e + index = i + } + } + return +} + +func RemoveDuplicate[T string | int](sliceList []T) []T { + allKeys := make(map[T]bool) + list := []T{} + for _, item := range sliceList { + if _, value := allKeys[item]; !value { + allKeys[item] = true + list = append(list, item) + } + } + return list +} + +func IsValidAddress(iaddress interface{}) bool { + re := regexp.MustCompile("^0x[0-9a-fA-F]{40}$") + switch v := iaddress.(type) { + case string: + return re.MatchString(v) + case common.Address: + return re.MatchString(v.Hex()) + default: + return false + } +} + +// VerifySig 校验签名 +func VerifySig(from, sigHex string, msg []byte) bool { + defer func() { + if err := recover(); err != nil { + return + } + }() + sig := hexutil.MustDecode(sigHex) + + msg = accounts.TextHash(msg) + if sig[crypto.RecoveryIDOffset] == 27 || sig[crypto.RecoveryIDOffset] == 28 { + sig[crypto.RecoveryIDOffset] -= 27 // Transform yellow paper V from 27/28 to 0/1 + } + + recovered, err := crypto.SigToPub(msg, sig) + if err != nil { + return false + } + + recoveredAddr := crypto.PubkeyToAddress(*recovered) + return from == recoveredAddr.Hex() +} diff --git a/internal/app/utils/validator.go b/internal/app/utils/validator.go new file mode 100644 index 0000000..0ba19de --- /dev/null +++ b/internal/app/utils/validator.go @@ -0,0 +1,273 @@ +package utils + +import ( + "errors" + "reflect" + "regexp" + "strconv" + "strings" +) + +type Rules map[string][]string + +type RulesMap map[string]Rules + +var CustomizeMap = make(map[string]Rules) + +//@function: RegisterRule +//@description: 注册自定义规则方案建议在路由初始化层即注册 +//@param: key string, rule Rules +//@return: err error + +func RegisterRule(key string, rule Rules) (err error) { + if CustomizeMap[key] != nil { + return errors.New(key + "已注册,无法重复注册") + } else { + CustomizeMap[key] = rule + return nil + } +} + +//@function: NotEmpty +//@description: 非空 不能为其对应类型的0值 +//@return: string + +func NotEmpty() string { + return "notEmpty" +} + +// @function: RegexpMatch +// @description: 正则校验 校验输入项是否满足正则表达式 +// @param: rule string +// @return: string +func RegexpMatch(rule string) string { + return "regexp=" + rule +} + +//@function: Lt +//@description: 小于入参(<) 如果为string array Slice则为长度比较 如果是 int uint float 则为数值比较 +//@param: mark string +//@return: string + +func Lt(mark string) string { + return "lt=" + mark +} + +//@function: Le +//@description: 小于等于入参(<=) 如果为string array Slice则为长度比较 如果是 int uint float 则为数值比较 +//@param: mark string +//@return: string + +func Le(mark string) string { + return "le=" + mark +} + +//@function: Eq +//@description: 等于入参(==) 如果为string array Slice则为长度比较 如果是 int uint float 则为数值比较 +//@param: mark string +//@return: string + +func Eq(mark string) string { + return "eq=" + mark +} + +//@function: Ne +//@description: 不等于入参(!=) 如果为string array Slice则为长度比较 如果是 int uint float 则为数值比较 +//@param: mark string +//@return: string + +func Ne(mark string) string { + return "ne=" + mark +} + +//@function: Ge +//@description: 大于等于入参(>=) 如果为string array Slice则为长度比较 如果是 int uint float 则为数值比较 +//@param: mark string +//@return: string + +func Ge(mark string) string { + return "ge=" + mark +} + +//@function: Gt +//@description: 大于入参(>) 如果为string array Slice则为长度比较 如果是 int uint float 则为数值比较 +//@param: mark string +//@return: string + +func Gt(mark string) string { + return "gt=" + mark +} + +// + +// @function: Verify +// @description: 校验方法 +// @param: st interface{}, roleMap Rules(入参实例,规则map) +// @return: err error +func Verify(st interface{}, roleMap Rules) (err error) { + compareMap := map[string]bool{ + "lt": true, + "le": true, + "eq": true, + "ne": true, + "ge": true, + "gt": true, + } + + typ := reflect.TypeOf(st) + val := reflect.ValueOf(st) // 获取reflect.Type类型 + + kd := val.Kind() // 获取到st对应的类别 + if kd != reflect.Struct { + return errors.New("expect struct") + } + num := val.NumField() + // 遍历结构体的所有字段 + for i := 0; i < num; i++ { + tagVal := typ.Field(i) + val := val.Field(i) + if len(roleMap[tagVal.Name]) > 0 { + for _, v := range roleMap[tagVal.Name] { + switch { + case v == "notEmpty": + if isBlank(val) { + return errors.New(tagVal.Name + " cannot be empty") + } + case strings.Split(v, "=")[0] == "regexp": + if !regexpMatch(strings.Split(v, "=")[1], val.String()) { + return errors.New(tagVal.Name + " Incorrect format") + } + case compareMap[strings.Split(v, "=")[0]]: + if !compareVerify(val, v) { + return errors.New(tagVal.Name + " Incorrect length or value," + v) + } + } + } + } + } + return nil +} + +//@function: compareVerify +//@description: 长度和数字的校验方法 根据类型自动校验 +//@param: value reflect.Value, VerifyStr string +//@return: bool + +func compareVerify(value reflect.Value, VerifyStr string) bool { + switch value.Kind() { + case reflect.String, reflect.Slice, reflect.Array: + return compare(value.Len(), VerifyStr) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return compare(value.Uint(), VerifyStr) + case reflect.Float32, reflect.Float64: + return compare(value.Float(), VerifyStr) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return compare(value.Int(), VerifyStr) + default: + return false + } +} + +//@function: isBlank +//@description: 非空校验 +//@param: value reflect.Value +//@return: bool + +func isBlank(value reflect.Value) bool { + switch value.Kind() { + case reflect.String: + return value.Len() == 0 + case reflect.Bool: + return !value.Bool() + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return value.Int() == 0 + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return value.Uint() == 0 + case reflect.Float32, reflect.Float64: + return value.Float() == 0 + case reflect.Interface, reflect.Ptr: + return value.IsNil() + } + return reflect.DeepEqual(value.Interface(), reflect.Zero(value.Type()).Interface()) +} + +//@function: compare +//@description: 比较函数 +//@param: value interface{}, VerifyStr string +//@return: bool + +func compare(value interface{}, VerifyStr string) bool { + VerifyStrArr := strings.Split(VerifyStr, "=") + val := reflect.ValueOf(value) + switch val.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + VInt, VErr := strconv.ParseInt(VerifyStrArr[1], 10, 64) + if VErr != nil { + return false + } + switch { + case VerifyStrArr[0] == "lt": + return val.Int() < VInt + case VerifyStrArr[0] == "le": + return val.Int() <= VInt + case VerifyStrArr[0] == "eq": + return val.Int() == VInt + case VerifyStrArr[0] == "ne": + return val.Int() != VInt + case VerifyStrArr[0] == "ge": + return val.Int() >= VInt + case VerifyStrArr[0] == "gt": + return val.Int() > VInt + default: + return false + } + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + VInt, VErr := strconv.Atoi(VerifyStrArr[1]) + if VErr != nil { + return false + } + switch { + case VerifyStrArr[0] == "lt": + return val.Uint() < uint64(VInt) + case VerifyStrArr[0] == "le": + return val.Uint() <= uint64(VInt) + case VerifyStrArr[0] == "eq": + return val.Uint() == uint64(VInt) + case VerifyStrArr[0] == "ne": + return val.Uint() != uint64(VInt) + case VerifyStrArr[0] == "ge": + return val.Uint() >= uint64(VInt) + case VerifyStrArr[0] == "gt": + return val.Uint() > uint64(VInt) + default: + return false + } + case reflect.Float32, reflect.Float64: + VFloat, VErr := strconv.ParseFloat(VerifyStrArr[1], 64) + if VErr != nil { + return false + } + switch { + case VerifyStrArr[0] == "lt": + return val.Float() < VFloat + case VerifyStrArr[0] == "le": + return val.Float() <= VFloat + case VerifyStrArr[0] == "eq": + return val.Float() == VFloat + case VerifyStrArr[0] == "ne": + return val.Float() != VFloat + case VerifyStrArr[0] == "ge": + return val.Float() >= VFloat + case VerifyStrArr[0] == "gt": + return val.Float() > VFloat + default: + return false + } + default: + return false + } +} + +func regexpMatch(rule, matchStr string) bool { + return regexp.MustCompile(rule).MatchString(matchStr) +} diff --git a/internal/app/utils/verify.go b/internal/app/utils/verify.go new file mode 100644 index 0000000..d78dee7 --- /dev/null +++ b/internal/app/utils/verify.go @@ -0,0 +1,10 @@ +package utils + +var ( + IdVerify = Rules{"ID": {NotEmpty()}} + PageInfoVerify = Rules{"Page": {NotEmpty()}, "PageSize": {NotEmpty(), Le("30")}} + PageSizeLimitVerify = Rules{"PageSize": {Le("30")}} + // 用户 + AddContractVerify = Rules{"ContractAddress": {NotEmpty()}} + GetCollectionVerify = Rules{"AccountAddress": {NotEmpty()}, "PageSize": {Le("30")}} +) diff --git a/internal/app/utils/zipfiles.go b/internal/app/utils/zipfiles.go new file mode 100644 index 0000000..3142979 --- /dev/null +++ b/internal/app/utils/zipfiles.go @@ -0,0 +1,70 @@ +package utils + +import ( + "archive/zip" + "io" + "os" + "strings" +) + +//@function: ZipFiles +//@description: 压缩文件 +//@param: filename string, files []string, oldForm, newForm string +//@return: error + +func ZipFiles(filename string, files []string, oldForm, newForm string) error { + newZipFile, err := os.Create(filename) + if err != nil { + return err + } + defer func() { + _ = newZipFile.Close() + }() + + zipWriter := zip.NewWriter(newZipFile) + defer func() { + _ = zipWriter.Close() + }() + + // 把files添加到zip中 + for _, file := range files { + + err = func(file string) error { + zipFile, err := os.Open(file) + if err != nil { + return err + } + defer zipFile.Close() + // 获取file的基础信息 + info, err := zipFile.Stat() + if err != nil { + return err + } + + header, err := zip.FileInfoHeader(info) + if err != nil { + return err + } + + // 使用上面的FileInforHeader() 就可以把文件保存的路径替换成我们自己想要的了,如下面 + header.Name = strings.Replace(file, oldForm, newForm, -1) + + // 优化压缩 + // 更多参考see http://golang.org/pkg/archive/zip/#pkg-constants + header.Method = zip.Deflate + + writer, err := zipWriter.CreateHeader(header) + if err != nil { + return err + } + if _, err = io.Copy(writer, zipFile); err != nil { + return err + } + return nil + }(file) + if err != nil { + return err + } + } + return nil +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..4219f2b --- /dev/null +++ b/main.go @@ -0,0 +1,24 @@ +package main + +import ( + "go.uber.org/zap" + "nft-collect/internal/app/core" + "nft-collect/internal/app/global" + "nft-collect/internal/app/initialize" +) + +func main() { + // 初始化Viper + core.Viper() + // 初始化zap日志库 + global.LOG = core.Zap() + // 注册全局logger + zap.ReplaceGlobals(global.LOG) + // 初始化数据库 + initialize.InitCommonDB() + // 初始化缓存 + initialize.InitCache() + // 初始化默认合约 + initialize.InitNFTContract() + core.RunWindowsServer() +} diff --git a/pkg/auth/README.md b/pkg/auth/README.md new file mode 100644 index 0000000..a12c58a --- /dev/null +++ b/pkg/auth/README.md @@ -0,0 +1,5 @@ +#### auth + +##### 项目简介 +> 认证基础库 + diff --git a/pkg/auth/auth.go b/pkg/auth/auth.go new file mode 100644 index 0000000..962ec98 --- /dev/null +++ b/pkg/auth/auth.go @@ -0,0 +1,92 @@ +package auth + +import ( + "errors" + "github.com/golang-jwt/jwt/v4" + "time" +) + +// Config auth config. +type Config struct { + SigningKey string `mapstructure:"signing-key" json:"signing-key" yaml:"signing-key"` // jwt签名 + ExpiresTime int64 `mapstructure:"expires-time" json:"expires-time" yaml:"expires-time"` // 过期时间 + Issuer string `mapstructure:"issuer" json:"issuer" yaml:"issuer"` // 签发者 +} + +// Auth is the authorization middleware +type Auth struct { + c *Config + key []byte +} + +var ( + TokenExpired = errors.New("token is expired") + TokenNotValidYet = errors.New("token not active yet") + TokenMalformed = errors.New("that's not even a token") + TokenInvalid = errors.New("couldn't handle this token") +) + +func New(c *Config) *Auth { + return &Auth{ + c: c, + key: []byte(c.SigningKey), + } +} + +type CustomClaims struct { + BaseClaims + jwt.RegisteredClaims +} + +type BaseClaims struct { + UserID uint `json:"user_id,omitempty"` + Address string `json:"address,omitempty"` +} + +func (a *Auth) CreateClaims(baseClaims BaseClaims) CustomClaims { + claims := CustomClaims{ + BaseClaims: baseClaims, + RegisteredClaims: jwt.RegisteredClaims{ + NotBefore: jwt.NewNumericDate(time.Now().Add(-1)), // 签名生效时间 + ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Duration(a.c.ExpiresTime) * time.Second)), // 过期时间 + Issuer: a.c.Issuer, // 签名的发行者 + }, + } + return claims +} + +// CreateToken 创建一个token +func (a *Auth) CreateToken(claims CustomClaims) (string, error) { + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + return token.SignedString(a.key) +} + +// ParseToken 解析 token +func (a *Auth) ParseToken(tokenString string) (*CustomClaims, error) { + token, err := jwt.ParseWithClaims(tokenString, &CustomClaims{}, func(token *jwt.Token) (i interface{}, e error) { + return a.key, nil + }) + if err != nil { + if ve, ok := err.(*jwt.ValidationError); ok { + if ve.Errors&jwt.ValidationErrorMalformed != 0 { + return nil, TokenMalformed + } else if ve.Errors&jwt.ValidationErrorExpired != 0 { + // Token is expired + return nil, TokenExpired + } else if ve.Errors&jwt.ValidationErrorNotValidYet != 0 { + return nil, TokenNotValidYet + } else { + return nil, TokenInvalid + } + } + } + if token != nil { + if claims, ok := token.Claims.(*CustomClaims); ok && token.Valid { + return claims, nil + } + return nil, TokenInvalid + + } else { + return nil, TokenInvalid + } +} diff --git a/pkg/cache/bigcache.go b/pkg/cache/bigcache.go new file mode 100644 index 0000000..1287595 --- /dev/null +++ b/pkg/cache/bigcache.go @@ -0,0 +1,65 @@ +package cache + +import ( + "context" + "github.com/allegro/bigcache/v3" + "github.com/chenyahui/gin-cache/persist" + "go.uber.org/zap" + "time" +) + +// BigCacheStore local memory cache store +type BigCacheStore struct { + Cache *bigcache.BigCache +} + +// NewBigCacheStore allocate a local memory store with default expiration +func NewBigCacheStore(defaultExpiration time.Duration, logger *zap.Logger) *BigCacheStore { + config := bigcache.Config{ + Shards: 1024, + LifeWindow: defaultExpiration, + CleanWindow: 3 * time.Second, + MaxEntriesInWindow: 10000, + MaxEntrySize: 1500, + StatsEnabled: false, + Verbose: true, + HardMaxCacheSize: 256, // memory limit, value in MB + Logger: NewZapLogger(logger), + } + cache, err := bigcache.New(context.Background(), config) + if err != nil { + panic(err) + } + return &BigCacheStore{ + Cache: cache, + } +} + +// Set put key value pair to memory store, and expire after expireDuration +func (c *BigCacheStore) Set(key string, value interface{}, expireDuration time.Duration) error { + _ = expireDuration + payload, err := persist.Serialize(value) + if err != nil { + return err + } + return c.Cache.Set(key, payload) +} + +// Delete remove key in memory store, do nothing if key doesn't exist +func (c *BigCacheStore) Delete(key string) error { + return c.Cache.Delete(key) +} + +// Get get key in memory store, if key doesn't exist, return ErrCacheMiss +func (c *BigCacheStore) Get(key string, value interface{}) error { + payload, err := c.Cache.Get(key) + + if err == bigcache.ErrEntryNotFound { + return persist.ErrCacheMiss + } + + if err != nil { + return err + } + return persist.Deserialize(payload, value) +} diff --git a/pkg/cache/logger.go b/pkg/cache/logger.go new file mode 100644 index 0000000..ebf53c6 --- /dev/null +++ b/pkg/cache/logger.go @@ -0,0 +1,18 @@ +package cache + +import ( + "fmt" + "go.uber.org/zap" +) + +type ZapLogger struct { + Zap *zap.Logger +} + +func NewZapLogger(zap *zap.Logger) *ZapLogger { + return &ZapLogger{zap} +} + +func (c *ZapLogger) Printf(format string, v ...interface{}) { + c.Zap.Log(2, fmt.Sprintf(format, v...)) +} diff --git a/pkg/slice/slice.go b/pkg/slice/slice.go new file mode 100644 index 0000000..e4dbace --- /dev/null +++ b/pkg/slice/slice.go @@ -0,0 +1,21 @@ +package slice + +// DiffArray 求两个切片的差集 +func DiffSlice[T string | int](a []T, b []T) []T { + var diffArray []T + temp := map[T]struct{}{} + + for _, val := range b { + if _, ok := temp[val]; !ok { + temp[val] = struct{}{} + } + } + + for _, val := range a { + if _, ok := temp[val]; !ok { + diffArray = append(diffArray, val) + } + } + + return diffArray +} diff --git a/pkg/slice/slice_test.go b/pkg/slice/slice_test.go new file mode 100644 index 0000000..9972d18 --- /dev/null +++ b/pkg/slice/slice_test.go @@ -0,0 +1,27 @@ +package slice + +import ( + "reflect" + "testing" +) + +func TestDiffSlice(t *testing.T) { + type args struct { + a []int + b []int + } + tests := []struct { + name string + args args + want []int + }{ + {name: "test#1", args: args{a: []int{1, 2, 3}, b: []int{3, 4, 5}}, want: []int{1, 2}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := DiffSlice(tt.args.a, tt.args.b); !reflect.DeepEqual(got, tt.want) { + t.Errorf("DiffSlice() = %v, want %v", got, tt.want) + } + }) + } +}