diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 0000000..25e1a8d --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1,2 @@ +build/blockd +*__debug_* \ No newline at end of file diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..fca36d4 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,19 @@ +FROM golang:alpine AS builder + +LABEL stage=gobuilder + +ENV CGO_ENABLED 0 + +RUN apk update --no-cache && apk add --no-cache tzdata + +WORKDIR /build + +ADD go.mod . +ADD go.sum . +RUN go mod download +COPY . . +RUN go build -ldflags="-s -w" -o /app/blockd cmd/main.go + +EXPOSE 8080 + +CMD ["/app/blockd", "-log-level=debug","-log-local=false","-log-add-source=true","-rest-address=0.0.0.0:8080","-db-host=blockd-db:5432","-db-database=blockd","-db-user=blockd","-db-secret=blockd","-db-enable-tls=false"] diff --git a/backend/Makefile b/backend/Makefile new file mode 100644 index 0000000..bf12ad1 --- /dev/null +++ b/backend/Makefile @@ -0,0 +1,54 @@ +PROJECT_DIR = $(CURDIR) +PROJECT_BIN = ${PROJECT_DIR}/bin +TOOLS_BIN = ${PROJECT_BIN}/tools + +bin.build: + mkdir -p ${PROJECT_DIR}/build + rm -f ${PROJECT_DIR}/build/blockd + go build -ldflags="-s -w" -o ${PROJECT_DIR}/build/blockd ${PROJECT_DIR}/cmd/main.go + +d.build: + sudo docker buildx build . -t blockd:latest + +d.net: + sudo docker network create --driver bridge --subnet=192.168.2.0/24 --attachable blockd-net + +d.drop-net: + sudo docker network rm blockd-net + +up: d.build d.net + sudo docker compose up -d + +.PHONY: run.local +run.local: bin.build + ${PROJECT_DIR}/build/blockd \ + -log-level=debug \ + -log-local=true \ + -log-add-source=true \ + -rest-address=localhost:8081 \ + -db-host=localhost:8432 \ + -db-database=blockd \ + -db-user=blockd \ + -db-secret=blockd \ + -db-enable-tls=false \ + -jwt-secret=local_jwt_secret \ + -cache-host=localhost:6379 + +.PHONY: run.debug +run.debug: bin.build + ${PROJECT_DIR}/build/blockd \ + -log-level=debug \ + -log-local=false \ + -log-add-source=true \ + -rest-address=localhost:8081 \ + -db-host=localhost:8432 \ + -db-database=blockd \ + -db-user=blockd \ + -db-secret=blockd \ + -db-enable-tls=false \ + -jwt-secret=local_jwt_secret \ + -cache-host=localhost:6379 + + +start.d: + sudo systemctl start docker \ No newline at end of file diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 0000000..4c6a394 --- /dev/null +++ b/backend/README.md @@ -0,0 +1,327 @@ +# blockd backend +## Build +### Locally +1. Install Go >= 1.22 +``` sh +curl -LO https://get.golang.org/$(uname)/go_installer && \ + chmod +x go_installer && \ + ./go_installer && \ + rm go_installer +``` +2. Install docker: +``` sh +output=$(which docker); +if [ -z "${output}" ]; then + sudo dnf remove docker \ + docker-client \ + docker-client-latest \ + docker-common \ + docker-latest \ + docker-latest-logrotate \ + docker-logrotate \ + docker-selinux \ + docker-engine-selinux \ + docker-engine + sudo apt -y install dnf-plugins-core + sudo dnf config-manager --add-repo https://download.docker.com/linux/fedora/docker-ce.repo + sudo dnf install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin + sudo systemctl start docker +fi +``` +3. Build it!: +``` sh +make bin.build +``` + +4. Start the server: +``` sh +make d.net && \ +sudo docker compose up blockd-db -d && \ +make run.debug +``` +Or +``` sh +make d.net && \ +sudo docker compose up blockd-db -d && \ +make run.local +``` + +### Docker +Just run +``` sh +make up +``` + +# API +Request content type: application/json +Response content type: application/json + +## POST **/join** +### Request body: +* name (string, optional) +* credentals (object, optional) + credentals.email (string, optional) + credentals.phone (string, optional) + credentals.telegram (string, optional) +* mnemonic (string, **required**) + +### Example +Request: +``` bash +curl --location 'http://localhost:8081/join' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "name": "Bladee The Grand Drainer", + "credentals": { + "email": "bladeee@gmail.com", + "phone": "+79999999999", + "telegram": "@thebladee" + }, + "mnemonic":"airport donate language disagree dumb access insect tribe ozone humor foot jealous much digital confirm" +}' +``` + +Response: +``` json +{ + "token": "token", + "token_expired_at": 1715975501581, + "refresh_token": "refresh_token", + "refresh_token_expired_at": 1716407501581 +} +``` + +## POST **/login** +### Request body: +* mnemonic (string, **required**) + +### Example +Request: +``` bash +curl --location 'http://localhost:8081/login' \ +--header 'Content-Type: application/json' \ +--data '{ + "mnemonic":"airport donate language disagree dumb access insect tribe ozone humor foot jealous much digital confirm" +}' +``` + +Response: +``` json +{ + "token": "token", + "token_expired_at": 1715975501581, + "refresh_token": "refresh_token", + "refresh_token_expired_at": 1716407501581 +} +``` + +## POST **/refresh** +### Request body: +* token (string, **required**) +* refresh_token (string, **required**) + +### Example +Request: +``` bash +curl --location --request GET 'http://localhost:8081/refresh' \ +--header 'Content-Type: application/json' \ +--data '{ + "token": "token", + "refresh_token": "refresh_token" +}' +``` + +Response: +``` json +{ + "token": "token", + "token_expired_at": 1715975501581, + "refresh_token": "refresh_token", + "refresh_token_expired_at": 1716407501581 +} +``` + +## POST **/organizations** +### Request body: +* name (string, **required**) +* address (string, optional) +* wallet_mnemonic (string, optional. *if not provided, creators mnemonic will me used*) + +### Example +Request: +``` bash +curl --location 'http://localhost:8081/organizations' \ +--header 'Content-Type: application/json' \ +--header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MTU0NTY4Mzg4NTAsInVpZCI6ImI2NmU1Mjk4LTU1ZTctNGIxNy1hYzliLTA0MzU3YjBlN2Q0ZSJ9.K1I0QoZEdDYK_HEsJ0PdWOfZ8ugTcPfLqy7fHhvK9nk' \ +--data '{ + "name": "The Drain Gang Inc", + "address": "Backsippestigen 22, 432 36 Varberg, Sweden" +}' +``` + +Response: +``` json +{ + "id": "dfac7846-0f0a-11ef-9262-0242ac120002" +} +``` + +## GET **/organizations** +### Request body: +* cursor (string, optional) +* limit (uint8, optional. Max:50, Default:50) +* offset_date (uint63, optional. *time as unix milli*) + +### Example +Request: +``` bash +curl --location --request GET 'http://localhost:8081/organizations' \ +--header 'Content-Type: application/json' \ +--header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MTU4OTU4MTc3MDYsInVpZCI6IjUyNTNkMzdjLTMxZDQtNDgxMi1iZTcxLWE5ODQwMTVlNGVlMyJ9.YjjHWz7FiMM73e-98pZYHCW9tKDZ_mRWKG3m1PcVTo0' \ +--data '{ + "limit":5, + "cursor":"eyJpZCI6IjAxOGY2ZTc3LWUxNDMtNzcyZi04NjJkLTlkZDM5NzUxYTZkMyJ9" +}' +``` + +Response: +``` json +{ + "_type": "organizations", + "_links": { + "self": { + "href": "/organizations" + } + }, + "items": [ + { + "_links": { + "self": { + "href": "/organizations/018f6e77-ebcc-7547-bc84-2556fbf12300" + } + }, + "id": "018f6e77-ebcc-7547-bc84-2556fbf12300", + "name": "The Drain Gang Inc 6", + "address": "1", + "created_at": 1715556104012, + "updated_at": 1715556104012 + }, + { + "_links": { + "self": { + "href": "/organizations/018f6e77-f5f5-7bcb-b98f-9966e7a8b706" + } + }, + "id": "018f6e77-f5f5-7bcb-b98f-9966e7a8b706", + "name": "The Drain Gang Inc 7", + "address": "1", + "created_at": 1715556106613, + "updated_at": 1715556106613 + } + ], + "pagination": { + "next_cursor": "eyJpZCI6IjAxOGY2ZTc3LWY1ZjUtN2JjYi1iOThmLTk5NjZlN2E4YjcwNiJ9", + "total_items": 2 + } +} +``` + +## GET **/{organization_id}/transactions** +### Request body: + + +### Example +Request: +``` bash +curl --location --request GET 'http://localhost:8081/organizations/018f9078-af60-7589-af64-9312b97aa7be/transactions' \ +--header 'Content-Type: application/json' \ +--header 'Authorization: Bearer TOKEN' \ +--data '{ + "description":"New test tx!", + "amount": 100, + "to":"0x323b5d4c32345ced77393b3530b1eed0f346429d", + "limit":1 +}' +``` + +Response: +``` json +{ + "_type": "transaction", + "_links": { + "self": { + "href": "/organizations/{organization_id}/transactions" + } + }, + "id": "018f8ce2-dada-75fb-9745-8560e5736bec", + "description": "New test tx!", + "organization_id": "018f8ccd-2431-7d21-a0c2-a2735c852764", + "created_by": "018f8ccc-e4fc-7a46-9628-15f9c3301f5b", + "amount": 100, + "to": "MjtdTDI0XO13OTs1MLHu0PNGQp0=", + "max_fee_allowed": 5, + "deadline": 123456767, + "created_at": 1716055628507, + "updated_at": 1716055628507 +} +``` + +## POST **/{organization_id}/transactions** +### Request body: +* ids ([]uuid) +* created_by (uuid) +* to (string) +* cancelled (bool) +* confirmed (bool) +* commited (bool) +* expired (bool) +* pending (bool) +* cursor (string) +* limit (int) +* offset_date (unix milli time) + +### Example +Request: +``` bash +curl --location --request GET 'http://localhost:8081/organizations/018f9112-1805-7b5e-ae30-7fc2151810f3/transactions' \ +--header 'Content-Type: application/json' \ +--header 'Authorization: Bearer TOKEN' \ +--data '{ + "to": "0xD53990543641Ee27E2FC670ad2cf3cA65ccDc8BD", + "pending":true, + "limit":1, + "created_by":"018f9111-f0fb-708a-aec1-55295f5496d6" +}' +``` + +Response: +``` json +{ + "_type": "organizations", + "_links": { + "self": { + "href": "/organizations/018f9112-1805-7b5e-ae30-7fc2151810f3/transactions" + } + }, + "next_cursor": "eyJpZCI6IjAxOGY5MTE1LWU5NmItN2IxMi04Y2JiLWQxNTY5NDNkYjk5NCJ9", + "transactions": [ + { + "_type": "transaction", + "_links": { + "self": { + "href": "/organizations/018f9112-1805-7b5e-ae30-7fc2151810f3/transactions/018f9115-e96b-7b12-8cbb-d156943db994" + } + }, + "id": "018f9115-e96b-7b12-8cbb-d156943db994", + "description": "Test filter by TO!!!!!", + "organization_id": "018f9112-1805-7b5e-ae30-7fc2151810f3", + "created_by": "018f9111-f0fb-708a-aec1-55295f5496d6", + "amount": 1234, + "to": "0xD53990543641Ee27E2FC670ad2cf3cA65ccDc8BD", + "max_fee_allowed": 2.5, + "created_at": 1716136883437, + "updated_at": 1716136883437 + } + ] +} +``` diff --git a/backend/cmd/commands/commands.go b/backend/cmd/commands/commands.go new file mode 100644 index 0000000..551d43d --- /dev/null +++ b/backend/cmd/commands/commands.go @@ -0,0 +1,13 @@ +package commands + +import "github.com/urfave/cli/v2" + +var commandPool []*cli.Command + +func Register(c *cli.Command) { + commandPool = append(commandPool, c) +} + +func Commands() []*cli.Command { + return commandPool +} diff --git a/backend/cmd/load/main.go b/backend/cmd/load/main.go new file mode 100644 index 0000000..b910c2f --- /dev/null +++ b/backend/cmd/load/main.go @@ -0,0 +1,86 @@ +package main + +import ( + "bytes" + "encoding/json" + "log" + "net/http" + "sync" + "time" + + "github.com/emochka2007/block-accounting/internal/interface/rest/domain" + "github.com/emochka2007/block-accounting/internal/pkg/bip39" +) + +func main() { + sAt := time.Now() + + wg := sync.WaitGroup{} + + totalch := make(chan int, 5) + + for i := 0; i < 10; i++ { + wg.Add(1) + + go func(j int) { + defer wg.Done() + + var reqc int + + for { + e, err := bip39.NewEntropy(256) + if err != nil { + log.Println("ERROR: ", err) + break + } + + m, err := bip39.NewMnemonic(e) + if err != nil { + log.Println("ERROR: ", err) + break + } + + req, err := json.Marshal(&domain.JoinRequest{ + Mnemonic: m, + }) + if err != nil { + log.Println("ERROR: ", err) + break + } + + _, err = http.Post("http://localhost:8080/join", "application/json", bytes.NewBuffer(req)) + if err != nil { + log.Println("ERROR: ", err) + break + } + + reqc++ + + log.Println("req ", j) + } + + totalch <- reqc + }(i) + } + + var reqtotoal int + mu := sync.Mutex{} + + go func() { + for c := range totalch { + mu.Lock() + reqtotoal += c + mu.Unlock() + } + }() + + wg.Wait() + + eAt := time.Now() + + rps := float64(reqtotoal) / eAt.Sub(sAt).Seconds() + + log.Println("STARTED_AT: ", sAt, " END_AT: ", eAt) + + log.Println("REQ TOTAL: ", reqtotoal, " RPS:", rps, " SECONDS: ", eAt.Sub(sAt).Seconds()) +} diff --git a/backend/cmd/main.go b/backend/cmd/main.go new file mode 100644 index 0000000..33078af --- /dev/null +++ b/backend/cmd/main.go @@ -0,0 +1,136 @@ +package main + +import ( + "context" + "fmt" + "os" + "os/signal" + "syscall" + + "github.com/emochka2007/block-accounting/cmd/commands" + "github.com/emochka2007/block-accounting/internal/factory" + "github.com/emochka2007/block-accounting/internal/pkg/config" + + cli "github.com/urfave/cli/v2" +) + +func main() { + app := &cli.App{ + Name: "blockd", + Version: "0.1.0", + Commands: commands.Commands(), + Flags: []cli.Flag{ + // common + &cli.StringFlag{ + Name: "log-level", + Value: "debug", + }, + &cli.BoolFlag{ + Name: "log-local", + }, + &cli.StringFlag{ + Name: "log-file", + }, + &cli.BoolFlag{ + Name: "log-add-source", + Value: true, + }, + &cli.StringFlag{ + Name: "jwt-secret", + }, + + // rest + &cli.StringFlag{ + Name: "rest-address", + Value: "localhost:8080", + }, + &cli.BoolFlag{ + Name: "rest-enable-tls", + }, + &cli.StringFlag{ + Name: "rest-cert-path", + }, + &cli.StringFlag{ + Name: "rest-key-path", + }, + + // database + &cli.StringFlag{ + Name: "db-host", + }, + &cli.StringFlag{ + Name: "db-database", + }, + &cli.StringFlag{ + Name: "db-user", + }, + &cli.StringFlag{ + Name: "db-secret", + }, + &cli.BoolFlag{ + Name: "db-enable-tls", + }, + + &cli.StringFlag{ + Name: "cache-host", + }, + &cli.StringFlag{ + Name: "cache-user", + }, + &cli.StringFlag{ + Name: "cache-secret", + }, + }, + Action: func(c *cli.Context) error { + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) + defer stop() + + config := config.Config{ + Common: config.CommonConfig{ + LogLevel: c.String("log-level"), + LogLocal: c.Bool("log-local"), + LogFile: c.String("log-file"), + LogAddSource: c.Bool("log-add-source"), + JWTSecret: []byte(c.String("jwt-secret")), + }, + Rest: config.RestConfig{ + Address: c.String("rest-address"), + TLS: c.Bool("rest-enable-tls"), + }, + DB: config.DBConfig{ + Host: c.String("db-host"), + EnableSSL: c.Bool("db-enable-ssl"), + Database: c.String("db-database"), + User: c.String("db-user"), + Secret: c.String("db-secret"), + + CacheHost: c.String("cache-host"), + CacheUser: c.String("cache-user"), + CacheSecret: c.String("cache-secret"), + }, + } + + fmt.Println(config) + + service, cleanup, err := factory.ProvideService(config) + if err != nil { + panic(err) + } + + defer func() { + cleanup() + service.Stop() + }() + + if err = service.Run(ctx); err != nil { + return err + } + + return nil + }, + } + + if err := app.Run(os.Args); err != nil { + panic(err) + } +} diff --git a/backend/docker-compose.yaml b/backend/docker-compose.yaml new file mode 100644 index 0000000..88de4e0 --- /dev/null +++ b/backend/docker-compose.yaml @@ -0,0 +1,85 @@ +version: '3' +networks: + blockd-net: + name: blockd-net + driver: bridge + +volumes: + blockd-data: {} + prometheus_data: {} + +services: + blockd: + container_name: blockd + image: blockd:latest + ports: + - 8080:8080 + networks: + - blockd-net + depends_on: + blockd-db: + condition: service_healthy + profiles: [blockd] + + blockd-db: + container_name: blockd-db + image: postgres:16 + restart: always + environment: + - POSTGRES_USER=blockd + - POSTGRES_PASSWORD=blockd + - POSTGRES_DB=blockd + volumes: + - blockd-data:/var/lib/postgresql/data + - ./migrations/blockd.sql:/docker-entrypoint-initdb.d/init.sql + ports: + - 8432:5432 + networks: + - blockd-net + healthcheck: + test: pg_isready -U blockd -d blockd + interval: 1s + timeout: 5s + retries: 10 + start_period: 5s + profiles: [blockd, database, noback] + + blockd-cache: + container_name: blockd-cache + image: redis:7.2.4 + restart: always + networks: + - blockd-net + ports: + - 6379:6379 + profiles: [blockd, database, noback] + + prometheus: + image: prom/prometheus + container_name: prometheus + command: + - '--config.file=/etc/prometheus/prometheus.yml' + ports: + - 9091:9090 + restart: unless-stopped + networks: + - blockd-net + volumes: + - ./prometheus:/etc/prometheus + - prometheus_data:/prometheus + profiles: [blockd, metrics, noback] + + grafana: + image: grafana/grafana + container_name: grafana + ports: + - 3112:3000 + restart: unless-stopped + networks: + - blockd-net + environment: + - GF_SECURITY_ADMIN_USER=admin + - GF_SECURITY_ADMIN_PASSWORD=grafana + volumes: + - ./grafana:/etc/grafana/provisioning/datasources + profiles: [blockd, metrics, noback] \ No newline at end of file diff --git a/backend/go.mod b/backend/go.mod new file mode 100644 index 0000000..69d3c61 --- /dev/null +++ b/backend/go.mod @@ -0,0 +1,66 @@ +module github.com/emochka2007/block-accounting + +go 1.22.2 + +require ( + github.com/Masterminds/squirrel v1.5.4 + github.com/btcsuite/btcd v0.22.3 + github.com/btcsuite/btcutil v1.0.3-0.20201208143702-a53e38424cce + github.com/ethereum/go-ethereum v1.14.0 + github.com/fatih/color v1.16.0 + github.com/go-chi/chi/v5 v5.0.12 + github.com/go-chi/render v1.0.3 + github.com/golang-jwt/jwt/v5 v5.2.1 + github.com/google/uuid v1.6.0 + github.com/google/wire v0.6.0 + github.com/lib/pq v1.10.9 + github.com/prometheus/client_golang v1.12.0 + github.com/redis/go-redis/v9 v9.5.1 + github.com/tyler-smith/go-bip39 v1.1.0 + github.com/urfave/cli/v2 v2.27.2 + golang.org/x/crypto v0.22.0 + golang.org/x/sync v0.7.0 +) + +require ( + github.com/Microsoft/go-winio v0.6.1 // indirect + github.com/StackExchange/wmi v1.2.1 // indirect + github.com/ajg/form v1.5.1 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/bits-and-blooms/bitset v1.10.0 // indirect + github.com/btcsuite/btcd/btcec/v2 v2.2.0 // indirect + github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/consensys/bavard v0.1.13 // indirect + github.com/consensys/gnark-crypto v0.12.1 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect + github.com/crate-crypto/go-kzg-4844 v1.0.0 // indirect + github.com/deckarep/golang-set/v2 v2.1.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/ethereum/c-kzg-4844 v1.0.0 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/gorilla/websocket v1.4.2 // indirect + github.com/holiman/uint256 v1.2.4 // indirect + github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect + github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect + github.com/mmcloughlin/addchain v0.4.0 // indirect + github.com/prometheus/client_model v0.2.1-0.20210607210712-147c58e9608a // indirect + github.com/prometheus/common v0.32.1 // indirect + github.com/prometheus/procfs v0.7.3 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect + github.com/supranational/blst v0.3.11 // indirect + github.com/tklauser/go-sysconf v0.3.12 // indirect + github.com/tklauser/numcpus v0.6.1 // indirect + github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 // indirect + golang.org/x/mod v0.17.0 // indirect + golang.org/x/sys v0.19.0 // indirect + golang.org/x/tools v0.20.0 // indirect + google.golang.org/protobuf v1.33.0 // indirect + rsc.io/tmplfunc v0.0.3 // indirect +) diff --git a/backend/go.sum b/backend/go.sum new file mode 100644 index 0000000..93ae596 --- /dev/null +++ b/backend/go.sum @@ -0,0 +1,728 @@ +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.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/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= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +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/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ= +github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= +github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM= +github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= +github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= +github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= +github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA= +github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8= +github.com/VictoriaMetrics/fastcache v1.12.1 h1:i0mICQuojGDL3KblA7wUNlY5lOK6a4bwt3uRKnkZU40= +github.com/VictoriaMetrics/fastcache v1.12.1/go.mod h1:tX04vaqcNoQeGLD+ra5pU5sWkuxnzWhEzLwhP9w653o= +github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= +github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= +github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bits-and-blooms/bitset v1.10.0 h1:ePXTeiPEazB5+opbv5fr8umg2R/1NlzgDsyepwsSr88= +github.com/bits-and-blooms/bitset v1.10.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= +github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= +github.com/btcsuite/btcd v0.22.3 h1:kYNaWFvOw6xvqP0vR20RP1Zq1DVMBxEO8QN5d1/EfNg= +github.com/btcsuite/btcd v0.22.3/go.mod h1:wqgTSL29+50LRkmOVknEdmt8ZojIzhuWvgu/iptuN7Y= +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.1.0 h1:59Kx4K6lzOW5w6nFlA0v5+lk/6sjybR934QNHSJZPTQ= +github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= +github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= +github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= +github.com/btcsuite/btcutil v1.0.3-0.20201208143702-a53e38424cce h1:YtWJF7RHm2pYCvA5t0RPmAaLUhREsKuKd+SLhxFbFeQ= +github.com/btcsuite/btcutil v1.0.3-0.20201208143702-a53e38424cce/go.mod h1:0DVlHczLPewLcPGEIeUEzfOJhqGPQ0mJJRDBtD307+o= +github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= +github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY= +github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= +github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= +github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +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/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/cockroachdb/errors v1.11.1 h1:xSEW75zKaKCWzR3OfxXUxgrk/NtT4G1MiOv5lWZazG8= +github.com/cockroachdb/errors v1.11.1/go.mod h1:8MUxA3Gi6b25tYlFEBGLf+D8aISL+M4MIpiWMSNRfxw= +github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b h1:r6VH0faHjZeQy818SGhaone5OnYfxFR/+AzdY3sf5aE= +github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs= +github.com/cockroachdb/pebble v1.1.0 h1:pcFh8CdCIt2kmEpK0OIatq67Ln9uGDYY3d5XnE0LJG4= +github.com/cockroachdb/pebble v1.1.0/go.mod h1:sEHm5NOXxyiAoKWhoFxT8xMgd/f3RA6qUqQ1BXKrh2E= +github.com/cockroachdb/redact v1.1.5 h1:u1PMllDkdFfPWaNGMyLD1+so+aq3uUItthCFqzwPJ30= +github.com/cockroachdb/redact v1.1.5/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg= +github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 h1:zuQyyAKVxetITBuuhv3BI9cMrmStnpT18zmgmTxunpo= +github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ= +github.com/consensys/bavard v0.1.13 h1:oLhMLOFGTLdlda/kma4VOJazblc7IM5y5QPd2A/YjhQ= +github.com/consensys/bavard v0.1.13/go.mod h1:9ItSMtA/dXMAiL7BG6bqW2m3NdSEObYWoH223nGHukI= +github.com/consensys/gnark-crypto v0.12.1 h1:lHH39WuuFgVHONRl3J0LRBtuYdQTumFSDtJF7HpyG8M= +github.com/consensys/gnark-crypto v0.12.1/go.mod h1:v2Gy7L/4ZRosZ7Ivs+9SfUDr0f5UlG+EM5t7MPHiLuY= +github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/crate-crypto/go-ipa v0.0.0-20231025140028-3c0104f4b233 h1:d28BXYi+wUpz1KBmiF9bWrjEMacUEREV6MBi2ODnrfQ= +github.com/crate-crypto/go-ipa v0.0.0-20231025140028-3c0104f4b233/go.mod h1:geZJZH3SzKCqnz5VT0q/DyIG/tvu/dZk+VIfXicupJs= +github.com/crate-crypto/go-kzg-4844 v1.0.0 h1:TsSgHwrkTKecKJ4kadtHi4b3xHW5dCFUDFnUp1TsawI= +github.com/crate-crypto/go-kzg-4844 v1.0.0/go.mod h1:1kMhvPgI0Ky3yIa+9lFySEBUBXkYxeOi8ZF1sYioxhc= +github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +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/v2 v2.1.0 h1:g47V4Or+DUdzbs8FxCCmgb6VYd+ptPAngjM6dtGktsI= +github.com/deckarep/golang-set/v2 v2.1.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= +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/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/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/ethereum/c-kzg-4844 v1.0.0 h1:0X1LBXxaEtYD9xsyj9B9ctQEZIpnvVDeoBx8aHEwTNA= +github.com/ethereum/c-kzg-4844 v1.0.0/go.mod h1:VewdlzQmpT5QSrVhbBuGoCdFJkpaJlO1aQputP83wc0= +github.com/ethereum/go-ethereum v1.14.0 h1:xRWC5NlB6g1x7vNy4HDBLuqVNbtLrc7v8S6+Uxim1LU= +github.com/ethereum/go-ethereum v1.14.0/go.mod h1:1STrq471D0BQbCX9He0hUj4bHxX2k6mt5nOQJhDNOJ8= +github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= +github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= +github.com/fjl/memsize v0.0.2 h1:27txuSD9or+NZlnOWdKUxeBzTAUkWCVh+4Gf2dWFOzA= +github.com/fjl/memsize v0.0.2/go.mod h1:VvhXpOYNQvB+uIk2RvXzuaQtkQJzzIx6lSBe1xv7hi0= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff h1:tY80oXqGNY4FhTFhk+o9oFHGINQ/+vhlm8HFzi6znCI= +github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff/go.mod h1:x7DCsMOv1taUwEWCzT4cmDeAkigA5/QCwUodaVOe8Ww= +github.com/gballet/go-verkle v0.1.1-0.20231031103413-a67434b50f46 h1:BAIP2GihuqhwdILrV+7GJel5lyPV3u1+PgzrWLc0TkE= +github.com/gballet/go-verkle v0.1.1-0.20231031103413-a67434b50f46/go.mod h1:QNpY22eby74jVhqH4WhDLDwxc/vqsern6pW+u2kbkpc= +github.com/getsentry/sentry-go v0.18.0 h1:MtBW5H9QgdcJabtZcuJG80BMOwaBpkRDZkxRkNC1sN0= +github.com/getsentry/sentry-go v0.18.0/go.mod h1:Kgon4Mby+FJ7ZWHFUAZgVaIa8sxHtnRJRLTXZr51aKQ= +github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s= +github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4= +github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0= +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-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= +github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= +github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +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/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/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb h1:PBC98N2aIaM3XXiurYmW7fx4GZkL8feAMVq7nEjURHk= +github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +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.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +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/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/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/wire v0.6.0 h1:HBkoIh4BdSxoyo9PveV8giw7ZsaBOvzWKfcg/6MrVwI= +github.com/google/wire v0.6.0/go.mod h1:F4QhpQ9EDIdJ1Mbop/NZBRB+5yrR6qg3BnctaoUk6NA= +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/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hashicorp/go-bexpr v0.1.10 h1:9kuI5PFotCboP3dkDYFr/wi0gg0QVbSNz5oFRpxn4uE= +github.com/hashicorp/go-bexpr v0.1.10/go.mod h1:oxlubA2vC/gFVfX1A6JGp7ls7uCDlfJn732ehYYg+g0= +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/holiman/billy v0.0.0-20240216141850-2abb0c79d3c4 h1:X4egAf/gcS1zATw6wn4Ej8vjuVGxeHdan+bRb2ebyv4= +github.com/holiman/billy v0.0.0-20240216141850-2abb0c79d3c4/go.mod h1:5GuXa7vkL8u9FkFuWdVvfR5ix8hRB7DbOAaYULamFpc= +github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao= +github.com/holiman/bloomfilter/v2 v2.0.3/go.mod h1:zpoh+gs7qcpqrHr3dB55AMiJwo0iURXE7ZOP9L9hSkA= +github.com/holiman/uint256 v1.2.4 h1:jUc4Nk8fm9jZabQuqr2JzednajVmBpC+oiTiXZJEApU= +github.com/holiman/uint256 v1.2.4/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc= +github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= +github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= +github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +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/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= +github.com/klauspost/compress v1.15.15 h1:EF27CXIuDsYJ6mmvtBRlEuB2UVOqHG1tAXgZ7yIO+lw= +github.com/klauspost/compress v1.15.15/go.mod h1:ZcK2JAFqKOpnBlxcLsJzYfrS9X1akm9fHZNnD9+Vo/4= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +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/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw= +github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= +github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk= +github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= +github.com/leanovate/gopter v0.2.9 h1:fQjYxZaynp97ozCzfOyOuAGOU4aU/z37zf/tOujFk7c= +github.com/leanovate/gopter v0.2.9/go.mod h1:U2L/78B+KVFIx2VmW6onHJQzXtFb+p5y3y2Sh+Jxxv8= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= +github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI= +github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= +github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/pointerstructure v1.2.0 h1:O+i9nHnXS3l/9Wu7r4NrEdwA2VFTicjUEN1uBnDo34A= +github.com/mitchellh/pointerstructure v1.2.0/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8ohIXc3tViBH44KcwB2g4= +github.com/mmcloughlin/addchain v0.4.0 h1:SobOdjm2xLj1KkXN5/n0xTIWyZA2+s99UCY1iPfkHRY= +github.com/mmcloughlin/addchain v0.4.0/go.mod h1:A86O+tHqZLMNO4w6ZZ4FlVQEadcoqkyU72HC5wJ4RlU= +github.com/mmcloughlin/profile v0.1.1/go.mod h1:IhHD7q1ooxgwTgjxQYkACGA77oFTDdFVejUS1/tS/qU= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +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.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +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_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= +github.com/prometheus/client_golang v1.12.0 h1:C+UIj/QWtmqY13Arb8kwMt5j34/0Z2iKamrJ+ryC0Gg= +github.com/prometheus/client_golang v1.12.0/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.1-0.20210607210712-147c58e9608a h1:CmF68hwI0XsOQ5UwlBopMi2Ow4Pbg32akc4KIVCOm+Y= +github.com/prometheus/client_model v0.2.1-0.20210607210712-147c58e9608a/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= +github.com/prometheus/common v0.32.1 h1:hWIdL3N2HoUx3B8j3YN9mWor0qhY/NlEKZEaXxuIRh4= +github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU= +github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/redis/go-redis/v9 v9.5.1 h1:H1X4D3yHPaYrkL5X06Wh6xNVM/pX0Ft4RV0vMGvLBh8= +github.com/redis/go-redis/v9 v9.5.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik= +github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +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/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/status-im/keycard-go v0.2.0 h1:QDLFswOQu1r5jsycloeQh3bVU8n/NatHHaZobtDnDzA= +github.com/status-im/keycard-go v0.2.0/go.mod h1:wlp8ZLbsmrF6g6WjugPAx+IzoLrkdf9+mHxBEeo3Hbg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +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.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/supranational/blst v0.3.11 h1:LyU6FolezeWAhvQk0k6O/d49jqgO52MSDDfYgbeoEm4= +github.com/supranational/blst v0.3.11/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= +github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= +github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= +github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= +github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= +github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= +github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= +github.com/tyler-smith/go-bip39 v1.1.0 h1:5eUemwrMargf3BSLRRCalXT93Ns6pQJIjYQN2nyfOP8= +github.com/tyler-smith/go-bip39 v1.1.0/go.mod h1:gUYDtqQw1JS3ZJ8UWVcGTGqqr6YIN3CWg+kkNaLt55U= +github.com/urfave/cli/v2 v2.27.2 h1:6e0H+AkS+zDckwPCUrZkKX38mRaau4nL2uipkJpbkcI= +github.com/urfave/cli/v2 v2.27.2/go.mod h1:g0+79LmHHATl7DAcHO99smiR/T7uGLw84w8Y42x+4eM= +github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 h1:+qGGcbkzsfDQNPPe9UDgpxAWQrhbbBXOYJFQDq/dtJw= +github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913/go.mod h1:4aEEwZQutDLsQv2Deui4iYQ6DWTxR14g6m8Wv88+Xqk= +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.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= +golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +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-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= +golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +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-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ= +golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE= +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/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.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +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-20181114220301-adae6a3d119a/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-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +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-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-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= +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-20210514164344-f6687ab2804c/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-20201207232520-09787c993a3a/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.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/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-20181116152217-5ac8a444bdc5/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-20190422165155-953cdadca894/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-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/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-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/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-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/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-20220114195835-da31bd327af9/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.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +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.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= +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.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.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +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.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +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-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.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= +golang.org/x/tools v0.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY= +golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg= +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 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +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/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/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/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/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.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +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-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +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/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= +gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +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.5/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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +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= +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= +rsc.io/tmplfunc v0.0.3 h1:53XFQh69AfOa8Tw0Jm7t+GV7KZhOi6jzsCzTtKbMvzU= +rsc.io/tmplfunc v0.0.3/go.mod h1:AG3sTPzElb1Io3Yg4voV9AGZJuleGAwaVRxL9M49PhA= diff --git a/backend/grafana/grafana.yml b/backend/grafana/grafana.yml new file mode 100644 index 0000000..4431f20 --- /dev/null +++ b/backend/grafana/grafana.yml @@ -0,0 +1,9 @@ +apiVersion: 1 + +datasources: +- name: Prometheus + type: prometheus + url: http://prometheus:9090 + isDefault: true + access: proxy + editable: true \ No newline at end of file diff --git a/backend/internal/factory/interactors.go b/backend/internal/factory/interactors.go new file mode 100644 index 0000000..531bb2b --- /dev/null +++ b/backend/internal/factory/interactors.go @@ -0,0 +1,51 @@ +package factory + +import ( + "log/slog" + + "github.com/emochka2007/block-accounting/internal/pkg/config" + "github.com/emochka2007/block-accounting/internal/usecase/interactors/jwt" + "github.com/emochka2007/block-accounting/internal/usecase/interactors/organizations" + "github.com/emochka2007/block-accounting/internal/usecase/interactors/transactions" + "github.com/emochka2007/block-accounting/internal/usecase/interactors/users" + "github.com/emochka2007/block-accounting/internal/usecase/repository/auth" + "github.com/emochka2007/block-accounting/internal/usecase/repository/cache" + orepo "github.com/emochka2007/block-accounting/internal/usecase/repository/organizations" + txRepo "github.com/emochka2007/block-accounting/internal/usecase/repository/transactions" + urepo "github.com/emochka2007/block-accounting/internal/usecase/repository/users" +) + +func provideUsersInteractor( + log *slog.Logger, + usersRepo urepo.Repository, +) users.UsersInteractor { + return users.NewUsersInteractor(log.WithGroup("users-interactor"), usersRepo) +} + +func provideJWTInteractor( + c config.Config, + usersInteractor users.UsersInteractor, + authRepository auth.Repository, +) jwt.JWTInteractor { + return jwt.NewJWT(c.Common.JWTSecret, usersInteractor, authRepository) +} + +func provideOrganizationsInteractor( + log *slog.Logger, + orgRepo orepo.Repository, + cache cache.Cache, +) organizations.OrganizationsInteractor { + return organizations.NewOrganizationsInteractor(log, orgRepo, cache) +} + +func provideTxInteractor( + log *slog.Logger, + txRepo txRepo.Repository, + orgInteractor organizations.OrganizationsInteractor, +) transactions.TransactionsInteractor { + return transactions.NewTransactionsInteractor( + log.WithGroup("transaction-interactor"), + txRepo, + orgInteractor, + ) +} diff --git a/backend/internal/factory/interface.go b/backend/internal/factory/interface.go new file mode 100644 index 0000000..8dd5efb --- /dev/null +++ b/backend/internal/factory/interface.go @@ -0,0 +1,142 @@ +package factory + +import ( + "log/slog" + "os" + + "github.com/google/wire" + + "github.com/emochka2007/block-accounting/internal/interface/rest" + "github.com/emochka2007/block-accounting/internal/interface/rest/controllers" + "github.com/emochka2007/block-accounting/internal/interface/rest/presenters" + "github.com/emochka2007/block-accounting/internal/pkg/config" + "github.com/emochka2007/block-accounting/internal/pkg/logger" + "github.com/emochka2007/block-accounting/internal/usecase/interactors/jwt" + "github.com/emochka2007/block-accounting/internal/usecase/interactors/organizations" + "github.com/emochka2007/block-accounting/internal/usecase/interactors/transactions" + "github.com/emochka2007/block-accounting/internal/usecase/interactors/users" +) + +var interfaceSet wire.ProviderSet = wire.NewSet( + provideAuthController, + provideOrganizationsController, + provideControllers, + provideTxController, + provideParticipantsController, + + provideAuthPresenter, + provideOrganizationsPresenter, +) + +func provideLogger(c config.Config) *slog.Logger { + lb := new(logger.LoggerBuilder).WithLevel(logger.MapLevel(c.Common.LogLevel)).WithWriter(os.Stdout) + + if c.Common.LogLocal { + lb.Local() + } + + if c.Common.LogFile != "" { + logFile, err := os.Open(c.Common.LogFile) + if err != nil { + panic(err) + } + + lb.WithWriter(logFile) + } + + if c.Common.LogAddSource { + lb.WithSource() + } + + return lb.Build() +} + +func provideAuthPresenter( + jwtInteractor jwt.JWTInteractor, +) presenters.AuthPresenter { + return presenters.NewAuthPresenter(jwtInteractor) +} + +func provideOrganizationsPresenter() presenters.OrganizationsPresenter { + return presenters.NewOrganizationsPresenter() +} + +func provideAuthController( + log *slog.Logger, + usersInteractor users.UsersInteractor, + authPresenter presenters.AuthPresenter, + jwtInteractor jwt.JWTInteractor, +) controllers.AuthController { + return controllers.NewAuthController( + log.WithGroup("auth-controller"), + authPresenter, + usersInteractor, + jwtInteractor, + ) +} + +func provideOrganizationsController( + log *slog.Logger, + organizationsInteractor organizations.OrganizationsInteractor, + presenter presenters.OrganizationsPresenter, +) controllers.OrganizationsController { + return controllers.NewOrganizationsController( + log.WithGroup("organizations-controller"), + organizationsInteractor, + presenter, + ) +} + +func provideTxController( + log *slog.Logger, + txInteractor transactions.TransactionsInteractor, +) controllers.TransactionsController { + return controllers.NewTransactionsController( + log.WithGroup("transactions-controller"), + txInteractor, + presenters.NewTransactionsPresenter(), + ) +} + +func provideParticipantsController( + log *slog.Logger, + orgInteractor organizations.OrganizationsInteractor, + usersInteractor users.UsersInteractor, +) controllers.ParticipantsController { + return controllers.NewParticipantsController( + log.WithGroup("participants-controller"), + orgInteractor, + usersInteractor, + presenters.NewParticipantsPresenter(), + ) +} + +func provideControllers( + log *slog.Logger, + authController controllers.AuthController, + orgController controllers.OrganizationsController, + txController controllers.TransactionsController, + participantsController controllers.ParticipantsController, +) *controllers.RootController { + return controllers.NewRootController( + controllers.NewPingController(log.WithGroup("ping-controller")), + authController, + orgController, + txController, + participantsController, + ) +} + +func provideRestServer( + log *slog.Logger, + controllers *controllers.RootController, + c config.Config, + jwt jwt.JWTInteractor, +) *rest.Server { + return rest.NewServer( + log.WithGroup("rest"), + c.Rest, + controllers, + jwt, + ) +} diff --git a/backend/internal/factory/repositories.go b/backend/internal/factory/repositories.go new file mode 100644 index 0000000..be61dde --- /dev/null +++ b/backend/internal/factory/repositories.go @@ -0,0 +1,50 @@ +package factory + +import ( + "database/sql" + "log/slog" + + "github.com/emochka2007/block-accounting/internal/pkg/config" + "github.com/emochka2007/block-accounting/internal/usecase/repository/auth" + "github.com/emochka2007/block-accounting/internal/usecase/repository/cache" + "github.com/emochka2007/block-accounting/internal/usecase/repository/organizations" + "github.com/emochka2007/block-accounting/internal/usecase/repository/transactions" + "github.com/emochka2007/block-accounting/internal/usecase/repository/users" + "github.com/redis/go-redis/v9" +) + +func provideUsersRepository(db *sql.DB) users.Repository { + return users.NewRepository(db) +} + +func provideOrganizationsRepository( + db *sql.DB, + uRepo users.Repository, +) organizations.Repository { + return organizations.NewRepository(db, uRepo) +} + +func provideTxRepository(db *sql.DB) transactions.Repository { + return transactions.NewRepository(db) +} + +func provideAuthRepository(db *sql.DB) auth.Repository { + return auth.NewRepository(db) +} + +func provideRedisConnection(c config.Config) (*redis.Client, func()) { + r := redis.NewClient(&redis.Options{ + Addr: c.DB.CacheHost, + Username: c.DB.CacheUser, + Password: c.DB.CacheSecret, + }) + + return r, func() { r.Close() } +} + +func provideRedisCache(c *redis.Client, log *slog.Logger) cache.Cache { + return cache.NewRedisCache( + log.WithGroup("redis-cache"), + c, + ) +} diff --git a/backend/internal/factory/wire.go b/backend/internal/factory/wire.go new file mode 100644 index 0000000..ca44f89 --- /dev/null +++ b/backend/internal/factory/wire.go @@ -0,0 +1,33 @@ +//go:build wireinject +// +build wireinject + +package factory + +import ( + "github.com/emochka2007/block-accounting/internal/pkg/config" + "github.com/emochka2007/block-accounting/internal/service" + "github.com/emochka2007/block-accounting/internal/usecase/repository" + "github.com/google/wire" +) + +func ProvideService(c config.Config) (service.Service, func(), error) { + wire.Build( + repository.ProvideDatabaseConnection, + provideRedisConnection, + provideLogger, + provideRedisCache, + provideUsersRepository, + provideUsersInteractor, + provideTxRepository, + provideOrganizationsRepository, + provideOrganizationsInteractor, + provideTxInteractor, + provideAuthRepository, + provideJWTInteractor, + interfaceSet, + provideRestServer, + service.NewService, + ) + + return &service.ServiceImpl{}, func() {}, nil +} diff --git a/backend/internal/factory/wire_gen.go b/backend/internal/factory/wire_gen.go new file mode 100644 index 0000000..289bd27 --- /dev/null +++ b/backend/internal/factory/wire_gen.go @@ -0,0 +1,46 @@ +// Code generated by Wire. DO NOT EDIT. + +//go:generate go run github.com/google/wire/cmd/wire +//go:build !wireinject +// +build !wireinject + +package factory + +import ( + "github.com/emochka2007/block-accounting/internal/pkg/config" + "github.com/emochka2007/block-accounting/internal/service" + "github.com/emochka2007/block-accounting/internal/usecase/repository" +) + +// Injectors from wire.go: + +func ProvideService(c config.Config) (service.Service, func(), error) { + logger := provideLogger(c) + db, cleanup, err := repository.ProvideDatabaseConnection(c) + if err != nil { + return nil, nil, err + } + usersRepository := provideUsersRepository(db) + usersInteractor := provideUsersInteractor(logger, usersRepository) + authRepository := provideAuthRepository(db) + jwtInteractor := provideJWTInteractor(c, usersInteractor, authRepository) + authPresenter := provideAuthPresenter(jwtInteractor) + authController := provideAuthController(logger, usersInteractor, authPresenter, jwtInteractor) + organizationsRepository := provideOrganizationsRepository(db, usersRepository) + client, cleanup2 := provideRedisConnection(c) + cache := provideRedisCache(client, logger) + organizationsInteractor := provideOrganizationsInteractor(logger, organizationsRepository, cache) + organizationsPresenter := provideOrganizationsPresenter() + organizationsController := provideOrganizationsController(logger, organizationsInteractor, organizationsPresenter) + transactionsRepository := provideTxRepository(db) + transactionsInteractor := provideTxInteractor(logger, transactionsRepository, organizationsInteractor) + transactionsController := provideTxController(logger, transactionsInteractor) + participantsController := provideParticipantsController(logger, organizationsInteractor, usersInteractor) + rootController := provideControllers(logger, authController, organizationsController, transactionsController, participantsController) + server := provideRestServer(logger, rootController, c, jwtInteractor) + serviceService := service.NewService(logger, server) + return serviceService, func() { + cleanup2() + cleanup() + }, nil +} diff --git a/backend/internal/interface/rest/controllers/auth.go b/backend/internal/interface/rest/controllers/auth.go new file mode 100644 index 0000000..eeb626c --- /dev/null +++ b/backend/internal/interface/rest/controllers/auth.go @@ -0,0 +1,150 @@ +package controllers + +import ( + "context" + "errors" + "fmt" + "log/slog" + "net/http" + "time" + + "github.com/emochka2007/block-accounting/internal/interface/rest/domain" + "github.com/emochka2007/block-accounting/internal/interface/rest/presenters" + "github.com/emochka2007/block-accounting/internal/pkg/bip39" + "github.com/emochka2007/block-accounting/internal/pkg/hdwallet" + "github.com/emochka2007/block-accounting/internal/usecase/interactors/jwt" + "github.com/emochka2007/block-accounting/internal/usecase/interactors/users" +) + +var ( + ErrorAuthInvalidMnemonic = errors.New("invalid mnemonic") + ErrorTokenRequired = errors.New("token required") +) + +type AuthController interface { + Join(w http.ResponseWriter, req *http.Request) ([]byte, error) + JoinWithInvite(w http.ResponseWriter, req *http.Request) ([]byte, error) + Login(w http.ResponseWriter, req *http.Request) ([]byte, error) + Invite(w http.ResponseWriter, req *http.Request) ([]byte, error) + Refresh(w http.ResponseWriter, req *http.Request) ([]byte, error) +} + +type authController struct { + log *slog.Logger + presenter presenters.AuthPresenter + usersInteractor users.UsersInteractor + jwtInteractor jwt.JWTInteractor +} + +func NewAuthController( + log *slog.Logger, + presenter presenters.AuthPresenter, + usersInteractor users.UsersInteractor, + jwtInteractor jwt.JWTInteractor, +) AuthController { + return &authController{ + log: log, + presenter: presenter, + usersInteractor: usersInteractor, + jwtInteractor: jwtInteractor, + } +} + +func (c *authController) Join(w http.ResponseWriter, req *http.Request) ([]byte, error) { + request, err := presenters.CreateRequest[domain.JoinRequest](req) + if err != nil { + return nil, fmt.Errorf("error create join request. %w", err) + } + + c.log.Debug("join request", slog.String("mnemonic", request.Mnemonic)) + + if !bip39.IsMnemonicValid(request.Mnemonic) { + return nil, fmt.Errorf("error invalid mnemonic. %w", ErrorAuthInvalidMnemonic) + } + + ctx, cancel := context.WithTimeout(req.Context(), 3*time.Second) + defer cancel() + + user, err := c.usersInteractor.Create(ctx, users.CreateParams{ + Name: request.Name, + Email: request.Credentals.Email, + Phone: request.Credentals.Phone, + Tg: request.Credentals.Telegram, + Mnemonic: request.Mnemonic, + Activate: true, + }) + if err != nil { + return nil, fmt.Errorf("error create new user. %w", err) + } + + c.log.Debug("join request", slog.String("user id", user.ID.String())) + + return c.presenter.ResponseJoin(user) +} + +// NIT: wrap with idempotent action handler +func (c *authController) Login(w http.ResponseWriter, req *http.Request) ([]byte, error) { + request, err := presenters.CreateRequest[domain.LoginRequest](req) + if err != nil { + return nil, fmt.Errorf("error create login request. %w", err) + } + + c.log.Debug("login request", slog.String("mnemonic", request.Mnemonic)) + + ctx, cancel := context.WithTimeout(req.Context(), 3*time.Second) + defer cancel() + + seed, err := hdwallet.NewSeedFromMnemonic(request.Mnemonic) + if err != nil { + return nil, fmt.Errorf("error create seed from mnemonic. %w", err) + } + + users, err := c.usersInteractor.Get(ctx, users.GetParams{ + Seed: seed, + }) + if err != nil { + return nil, fmt.Errorf("error fetch user by seed. %w", err) + } + + if len(users) == 0 { + return nil, fmt.Errorf("error empty users set") + } + + c.log.Debug("login request", slog.String("user id", users[0].ID.String())) + + return c.presenter.ResponseLogin(users[0]) +} + +func (c *authController) Refresh(w http.ResponseWriter, req *http.Request) ([]byte, error) { + request, err := presenters.CreateRequest[domain.RefreshRequest](req) + if err != nil { + return nil, fmt.Errorf("error create refresh request. %w", err) + } + + c.log.Debug( + "refresh request", + slog.String("token", request.Token), + slog.String("refresh_token", request.RefreshToken), + ) + + ctx, cancel := context.WithTimeout(req.Context(), 3*time.Second) + defer cancel() + + newTokens, err := c.jwtInteractor.RefreshToken(ctx, request.Token, request.RefreshToken) + if err != nil { + return nil, fmt.Errorf("error refresh access token. %w", err) + } + + return c.presenter.ResponseRefresh(newTokens) +} + +// const mnemonicEntropyBitSize int = 256 + +func (c *authController) Invite(w http.ResponseWriter, req *http.Request) ([]byte, error) { + + return nil, nil +} + +func (c *authController) JoinWithInvite(w http.ResponseWriter, req *http.Request) ([]byte, error) { + return nil, nil // implement +} diff --git a/backend/internal/interface/rest/controllers/multisig.go b/backend/internal/interface/rest/controllers/multisig.go new file mode 100644 index 0000000..46201a1 --- /dev/null +++ b/backend/internal/interface/rest/controllers/multisig.go @@ -0,0 +1,30 @@ +package controllers + +import ( + "fmt" + "log/slog" + "net/http" + + "github.com/emochka2007/block-accounting/internal/interface/rest/domain" + "github.com/emochka2007/block-accounting/internal/interface/rest/presenters" + "github.com/emochka2007/block-accounting/internal/usecase/interactors/chain" +) + +type MultisigController interface { +} + +type multisigController struct { + log *slog.Logger + chainInteractor chain.ChainInteractor +} + +func (c *multisigController) New(w http.ResponseWriter, r *http.Request) ([]byte, error) { + req, err := presenters.CreateRequest[domain.NewMultisigRequest](r) + if err != nil { + return nil, fmt.Errorf("error build new multisig request. %w", err) + } + + c.log.Debug("new_multisig", slog.Any("request", req)) + + panic("implement me!") +} diff --git a/backend/internal/interface/rest/controllers/organization.go b/backend/internal/interface/rest/controllers/organization.go new file mode 100644 index 0000000..187b41d --- /dev/null +++ b/backend/internal/interface/rest/controllers/organization.go @@ -0,0 +1,79 @@ +package controllers + +import ( + "context" + "fmt" + "log/slog" + "net/http" + "time" + + "github.com/emochka2007/block-accounting/internal/interface/rest/domain" + "github.com/emochka2007/block-accounting/internal/interface/rest/presenters" + "github.com/emochka2007/block-accounting/internal/usecase/interactors/organizations" +) + +type OrganizationsController interface { + NewOrganization(w http.ResponseWriter, r *http.Request) ([]byte, error) + ListOrganizations(w http.ResponseWriter, r *http.Request) ([]byte, error) + // todo delete + // todo update +} + +type organizationsController struct { + log *slog.Logger + orgInteractor organizations.OrganizationsInteractor + presenter presenters.OrganizationsPresenter +} + +func NewOrganizationsController( + log *slog.Logger, + orgInteractor organizations.OrganizationsInteractor, + presenter presenters.OrganizationsPresenter, +) OrganizationsController { + return &organizationsController{ + log: log, + orgInteractor: orgInteractor, + presenter: presenter, + } +} + +func (c *organizationsController) NewOrganization(w http.ResponseWriter, r *http.Request) ([]byte, error) { + req, err := presenters.CreateRequest[domain.NewOrganizationRequest](r) + if err != nil { + return nil, fmt.Errorf("error build request. %w", err) + } + + ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second) + defer cancel() + + org, err := c.orgInteractor.Create(ctx, organizations.CreateParams{ + Name: req.Name, + Address: req.Address, + WalletMnemonic: req.WalletMnemonic, + }) + if err != nil { + return nil, fmt.Errorf("error create new organization. %w", err) + } + + return c.presenter.ResponseCreate(org) +} + +func (c *organizationsController) ListOrganizations(w http.ResponseWriter, r *http.Request) ([]byte, error) { + req, err := presenters.CreateRequest[domain.ListOrganizationsRequest](r) + if err != nil { + return nil, fmt.Errorf("error build request. %w", err) + } + + ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second) + defer cancel() + + resp, err := c.orgInteractor.List(ctx, organizations.ListParams{ + Cursor: req.Cursor, + Limit: req.Limit, + }) + if err != nil { + return nil, fmt.Errorf("error fetch organizations list. %w", err) + } + + return c.presenter.ResponseList(resp.Organizations, resp.NextCursor) +} diff --git a/backend/internal/interface/rest/controllers/participants.go b/backend/internal/interface/rest/controllers/participants.go new file mode 100644 index 0000000..702c08f --- /dev/null +++ b/backend/internal/interface/rest/controllers/participants.go @@ -0,0 +1,94 @@ +package controllers + +import ( + "context" + "fmt" + "log/slog" + "net/http" + "time" + + "github.com/emochka2007/block-accounting/internal/interface/rest/domain" + "github.com/emochka2007/block-accounting/internal/interface/rest/presenters" + "github.com/emochka2007/block-accounting/internal/pkg/ctxmeta" + "github.com/emochka2007/block-accounting/internal/usecase/interactors/organizations" + "github.com/emochka2007/block-accounting/internal/usecase/interactors/users" + "github.com/google/uuid" +) + +type ParticipantsController interface { + List(w http.ResponseWriter, r *http.Request) ([]byte, error) +} + +type participantsController struct { + log *slog.Logger + orgInteractor organizations.OrganizationsInteractor + usersInteractor users.UsersInteractor + + presenter presenters.ParticipantsPresenter +} + +func NewParticipantsController( + log *slog.Logger, + orgInteractor organizations.OrganizationsInteractor, + usersInteractor users.UsersInteractor, + presenter presenters.ParticipantsPresenter, +) ParticipantsController { + return &participantsController{ + log: log, + orgInteractor: orgInteractor, + usersInteractor: usersInteractor, + presenter: presenter, + } +} + +func (c *participantsController) List(w http.ResponseWriter, r *http.Request) ([]byte, error) { + req, err := presenters.CreateRequest[domain.ListParticipantsRequest](r) + if err != nil { + return nil, fmt.Errorf("error build list participants request. %w", err) + } + + user, err := ctxmeta.User(r.Context()) + if err != nil { + return nil, fmt.Errorf("error fetch user from context. %w", err) + } + + organizationID, err := ctxmeta.OrganizationId(r.Context()) + if err != nil { + return nil, fmt.Errorf("error fetch organization id from context. %w", err) + } + + ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second) + defer cancel() + + participant, err := c.orgInteractor.Participant(ctx, organizations.ParticipantParams{ + ID: user.Id(), + OrganizationID: organizationID, + }) + if err != nil { + return nil, fmt.Errorf("error fetch user participant. %w", err) + } + + if !participant.IsActive() { + return nil, fmt.Errorf("error participant is inactive") + } + + ids := make(uuid.UUIDs, len(req.IDs)) + for i, id := range req.IDs { + uid, err := uuid.Parse(id) + if err != nil { + return nil, fmt.Errorf("error parse participant id. %w", err) + } + + ids[i] = uid + } + + participants, err := c.orgInteractor.Participants(ctx, organizations.ParticipantsParams{ + IDs: ids, + OrganizationID: organizationID, + }) + if err != nil { + return nil, fmt.Errorf("error fetch participants. %w", err) + } + + return c.presenter.ResponseListParticipants(ctx, participants) +} diff --git a/backend/internal/interface/rest/controllers/ping.go b/backend/internal/interface/rest/controllers/ping.go new file mode 100644 index 0000000..07c3674 --- /dev/null +++ b/backend/internal/interface/rest/controllers/ping.go @@ -0,0 +1,26 @@ +package controllers + +import ( + "log/slog" + "net/http" +) + +type PingController interface { + Ping(w http.ResponseWriter, req *http.Request) ([]byte, error) +} + +type pingController struct { + log *slog.Logger +} + +func NewPingController( + log *slog.Logger, +) PingController { + return &pingController{ + log: log, + } +} + +func (c *pingController) Ping(w http.ResponseWriter, req *http.Request) ([]byte, error) { + return []byte("pong"), nil +} diff --git a/backend/internal/interface/rest/controllers/root.go b/backend/internal/interface/rest/controllers/root.go new file mode 100644 index 0000000..390e837 --- /dev/null +++ b/backend/internal/interface/rest/controllers/root.go @@ -0,0 +1,25 @@ +package controllers + +type RootController struct { + Ping PingController + Auth AuthController + Organizations OrganizationsController + Transactions TransactionsController + Participants ParticipantsController +} + +func NewRootController( + ping PingController, + auth AuthController, + organizations OrganizationsController, + transactions TransactionsController, + participants ParticipantsController, +) *RootController { + return &RootController{ + Ping: ping, + Auth: auth, + Organizations: organizations, + Transactions: transactions, + Participants: participants, + } +} diff --git a/backend/internal/interface/rest/controllers/transactions.go b/backend/internal/interface/rest/controllers/transactions.go new file mode 100644 index 0000000..30de116 --- /dev/null +++ b/backend/internal/interface/rest/controllers/transactions.go @@ -0,0 +1,193 @@ +package controllers + +import ( + "context" + "fmt" + "log/slog" + "net/http" + "time" + + "github.com/go-chi/chi/v5" + "github.com/google/uuid" + + "github.com/emochka2007/block-accounting/internal/interface/rest/domain" + "github.com/emochka2007/block-accounting/internal/interface/rest/presenters" + "github.com/emochka2007/block-accounting/internal/pkg/ctxmeta" + "github.com/emochka2007/block-accounting/internal/pkg/models" + "github.com/emochka2007/block-accounting/internal/usecase/interactors/transactions" + "github.com/ethereum/go-ethereum/common" +) + +type TransactionsController interface { + New(w http.ResponseWriter, r *http.Request) ([]byte, error) + List(w http.ResponseWriter, r *http.Request) ([]byte, error) + UpdateStatus(w http.ResponseWriter, r *http.Request) ([]byte, error) +} + +type transactionsController struct { + log *slog.Logger + txInteractor transactions.TransactionsInteractor + txPresenter presenters.TransactionsPresenter +} + +func NewTransactionsController( + log *slog.Logger, + txInteractor transactions.TransactionsInteractor, + txPresenter presenters.TransactionsPresenter, +) TransactionsController { + return &transactionsController{ + log: log, + txInteractor: txInteractor, + txPresenter: txPresenter, + } +} + +func (c *transactionsController) New(w http.ResponseWriter, r *http.Request) ([]byte, error) { + req, err := presenters.CreateRequest[domain.NewTransactionRequest](r) + if err != nil { + return nil, fmt.Errorf("error build new transaction request. %w", err) + } + + ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second) + defer cancel() + + organizationID, err := ctxmeta.OrganizationId(ctx) + if err != nil { + return nil, fmt.Errorf("error fetch organization ID from context. %w", err) + } + + requestTx, err := c.txPresenter.RequestTransaction(ctx, req) + if err != nil { + return nil, fmt.Errorf("error build transaction model from request. %w", err) + } + + c.log.Debug( + "new reuqest", + slog.Any("req", req), + slog.String("org id", organizationID.String()), + ) + + tx, err := c.txInteractor.Create(ctx, transactions.CreateParams{ + OrganizationId: organizationID, + Tx: requestTx, + }) + if err != nil { + return nil, fmt.Errorf("error create new transaction. %w", err) + } + + return c.txPresenter.ResponseNewTransaction(ctx, tx) +} + +func (c *transactionsController) List(w http.ResponseWriter, r *http.Request) ([]byte, error) { + req, err := presenters.CreateRequest[domain.ListTransactionsRequest](r) + if err != nil { + return nil, fmt.Errorf("error build new transaction request. %w", err) + } + + organizationID, err := ctxmeta.OrganizationId(r.Context()) + if err != nil { + return nil, fmt.Errorf("error fetch organization ID from context. %w", err) + } + + ids := make(uuid.UUIDs, len(req.IDs)) + + for i, id := range req.IDs { + txUUID, err := uuid.Parse(id) + if err != nil { + return nil, fmt.Errorf("error parse tx id. %w", err) + } + + ids[i] = txUUID + } + + var toAddr []byte + + if req.To != "" { + toAddr = common.HexToAddress(req.To).Bytes() + } + + var createdBy uuid.UUID + if req.CreatedBy != "" { + createdBy, err = uuid.Parse(req.CreatedBy) + if err != nil { + return nil, fmt.Errorf("error parse created by id. %w", err) + } + } + + ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second) + defer cancel() + + txs, err := c.txInteractor.List(ctx, transactions.ListParams{ + IDs: ids, + OrganizationID: organizationID, + To: toAddr, + + CreatedBy: createdBy, + + Limit: int64(req.Limit), + Cursor: req.Cursor, + + WithCancelled: req.Cancelled, + WithConfirmed: req.Confirmed, + WithCommited: req.Commited, + WithExpired: req.Expired, + WithPending: req.Pending, + }) + if err != nil { + return nil, fmt.Errorf("error fetch organizations list. %w", err) + } + + return c.txPresenter.ResponseListTransactions(ctx, txs.Txs, txs.NextCursor) +} + +func (c *transactionsController) UpdateStatus(w http.ResponseWriter, r *http.Request) ([]byte, error) { + req, err := presenters.CreateRequest[domain.UpdateTransactionStatusRequest](r) + if err != nil { + return nil, fmt.Errorf("error build new transaction request. %w", err) + } + + organizationID, err := ctxmeta.OrganizationId(r.Context()) + if err != nil { + return nil, fmt.Errorf("error fetch organization ID from context. %w", err) + } + + txIDstr := chi.URLParam(r, "tx_id") + + var txID uuid.UUID + + if txID, err = uuid.Parse(txIDstr); err != nil { + return nil, fmt.Errorf("error parse tx id. %w", err) + } + + ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second) + defer cancel() + + var tx *models.Transaction + + if req.Cancel { + tx, err = c.txInteractor.Cancel(ctx, transactions.CancelParams{ + TxID: txID, + OrganizationID: organizationID, + }) + if err != nil { + return nil, fmt.Errorf("error cancel transaction. %w", err) + } + } else if req.Confirm { + tx, err = c.txInteractor.Confirm(ctx, transactions.ConfirmParams{ + TxID: txID, + OrganizationID: organizationID, + }) + if err != nil { + return nil, fmt.Errorf("error cancel transaction. %w", err) + } + } else { + return nil, fmt.Errorf("error new status required") + } + + return c.txPresenter.ResponseNewTransaction(ctx, tx) +} + +// todo creates a new payout +func (c *transactionsController) NewPayout(w http.ResponseWriter, r *http.Request) ([]byte, error) { + return nil, nil +} diff --git a/backend/internal/interface/rest/domain/dto.go b/backend/internal/interface/rest/domain/dto.go new file mode 100644 index 0000000..c85b9b9 --- /dev/null +++ b/backend/internal/interface/rest/domain/dto.go @@ -0,0 +1,125 @@ +package domain + +import ( + "encoding/json" + "fmt" +) + +// Generic + +type Collection[T any] struct { + Items []T `json:"items,omitempty"` + Pagination Pagination `json:"pagination,omitempty"` +} + +type Pagination struct { + NextCursor string `json:"next_cursor,omitempty"` + TotalItems uint32 `json:"total_items,omitempty"` +} + +// Auth related DTO's + +type JoinRequest struct { + Name string `json:"name,omitempty"` + Credentals struct { + Email string `json:"email,omitempty"` + Phone string `json:"phone,omitempty"` + Telegram string `json:"telegram,omitempty"` + } `json:"credentals,omitempty"` + + Mnemonic string `json:"mnemonic"` +} + +type JoinResponse struct { + Token string `json:"token"` +} + +type LoginRequest struct { + Mnemonic string `json:"mnemonic"` +} + +type RefreshRequest struct { + Token string `json:"token"` + RefreshToken string `json:"refresh_token"` +} + +type LoginResponse struct { + Token string `json:"token"` + ExpiredAt int64 `json:"token_expired_at"` + RefreshToken string `json:"refresh_token"` + RTExpiredAt int64 `json:"refresh_token_expired_at"` +} + +// Organizations + +type NewOrganizationRequest struct { + Name string `json:"name"` + Address string `json:"address"` + WalletMnemonic string `json:"wallet_mnemonic,omitempty"` +} + +type ListOrganizationsRequest struct { + Cursor string `json:"cursor,omitempty"` + Limit uint8 `json:"limit,omitempty"` // Default: 50, Max: 50 +} + +// Transactions + +type NewTransactionRequest struct { + Description string `json:"description,omitempty"` + Amount float64 `json:"amount,omitempty"` + ToAddr string `json:"to,omitempty"` + MaxFeeAllowed float64 `json:"max_fee_allowed,omitempty"` + Deadline int64 `json:"deadline,omitempty"` +} + +type ListTransactionsRequest struct { + IDs []string `json:"ids,omitempty"` + CreatedBy string `json:"created_by,omitempty"` + To string `json:"to,omitempty"` + + Cancelled bool `json:"cancelled,omitempty"` + Confirmed bool `json:"confirmed,omitempty"` + Commited bool `json:"commited,omitempty"` + Expired bool `json:"expired,omitempty"` + Pending bool `json:"pending,omitempty"` + + Cursor string `json:"cursor,omitempty"` + Limit uint8 `json:"limit,omitempty"` // Default: 50, Max: 50 +} + +type UpdateTransactionStatusRequest struct { + Cancel bool `json:"cancel,omitempty"` + Confirm bool `json:"confirm,omitempty"` +} + +// Participants + +type ListParticipantsRequest struct { + IDs []string `json:"ids,omitempty"` + Cursor string `json:"cursor,omitempty"` + Limit uint8 `json:"limit,omitempty"` +} + +type AddEmployeeRequest struct { + Name string `json:"name"` + Position string `json:"position"` + WalletAddress string `json:"wallet_address"` +} + +type NewMultisigRequest struct { + Title string `json:"title"` + Owners []struct { + PublicKey string `json:"public_key"` + } `json:"owners"` +} + +func BuildRequest[T any](data []byte) (*T, error) { + var req T + + if err := json.Unmarshal(data, &req); err != nil { + return nil, fmt.Errorf("error unmarshal request. %w", err) + } + + return &req, nil +} diff --git a/backend/internal/interface/rest/domain/hal/hal.go b/backend/internal/interface/rest/domain/hal/hal.go new file mode 100644 index 0000000..4df70bb --- /dev/null +++ b/backend/internal/interface/rest/domain/hal/hal.go @@ -0,0 +1,126 @@ +package hal + +import ( + "bytes" + "encoding/json" + "fmt" +) + +type Link struct { + Href string `json:"href"` + Title string `json:"title,omitempty"` + Method string `json:"method,omitempty"` +} + +type Resource struct { + Type string `json:"_type,omitempty"` + Links map[string]Link `json:"_links"` + Embedded map[string]any `json:"_embedded,omitempty"` + Payload +} + +type Payload interface{} + +type NewResourceOption func(r *Resource) + +func NewResource(res any, selfLink string, opts ...NewResourceOption) *Resource { + r := &Resource{ + Links: map[string]Link{ + "self": { + Href: selfLink, + }, + }, + Payload: res, + } + + for _, o := range opts { + o(r) + } + + return r +} + +func WithType(t string) NewResourceOption { + return func(r *Resource) { + r.Type = t + } +} + +func WithSelfTitle(t string) NewResourceOption { + return func(r *Resource) { + if l, ok := r.Links["self"]; ok { + l.Title = t + + r.Links["self"] = l + } + } +} + +func (r *Resource) Embed(relation string, resource *Resource) { + if r.Embedded == nil { + r.Embedded = make(map[string]any, 1) + } + + r.Embedded[relation] = resource +} + +func (r *Resource) AddLink(relation string, link Link) { + if r.Links == nil { + r.Links = make(map[string]Link, 1) + } + + r.Links[relation] = link +} + +func (r *Resource) SetType(t string) { + r.Type = t +} + +func (r *Resource) MarshalJSON() ([]byte, error) { + + rootData := struct { + Type string `json:"_type,omitempty"` + Links map[string]Link `json:"_links"` + Embedded map[string]any `json:"_embedded,omitempty"` + }{} + + rootData.Type = r.Type + + if len(r.Links) > 0 { + rootData.Links = r.Links + } + + if len(r.Embedded) > 0 { + rootData.Embedded = r.Embedded + } + + dataRoot, err := json.Marshal(rootData) + if err != nil { + return nil, fmt.Errorf("error marshal root data. %w", err) + } + + dataChild, err := json.Marshal(r.Payload) + if err != nil { + return nil, fmt.Errorf("error marshal payload data. %w", err) + } + + if len(dataRoot) == 2 { + return dataChild, nil + } + + var b bytes.Buffer + + if cap := b.Cap(); cap < (len(dataRoot) + len(dataChild)) { + b.Grow((len(dataRoot) + len(dataChild)) - cap) + } + + b.Write(dataRoot[:len(dataRoot)-1]) + + if len(dataChild) != 2 { + b.Write([]byte(`,`)) + } + + b.Write(dataChild[1:]) + + return b.Bytes(), nil +} diff --git a/backend/internal/interface/rest/domain/organization.go b/backend/internal/interface/rest/domain/organization.go new file mode 100644 index 0000000..d61926a --- /dev/null +++ b/backend/internal/interface/rest/domain/organization.go @@ -0,0 +1,9 @@ +package domain + +type Organization struct { + Id string `json:"id"` + Name string `json:"name"` + Address string `json:"address"` + CreatedAt uint64 `json:"created_at"` + UpdatedAt uint64 `json:"updated_at"` +} diff --git a/backend/internal/interface/rest/domain/participants.go b/backend/internal/interface/rest/domain/participants.go new file mode 100644 index 0000000..eb8dfb9 --- /dev/null +++ b/backend/internal/interface/rest/domain/participants.go @@ -0,0 +1,25 @@ +package domain + +type Participant struct { + ID string `json:"id"` + Name string `json:"name,omitempty"` + Position string `json:"position,omitempty"` + + // if is_user == true, Credentials will be filled with data + Credentials *UserParticipantCredentials `json:"credentials,omitempty"` + + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` + DeletedAt int64 `json:"deleted_at,omitempty"` + + IsUser bool `json:"is_user"` + IsAdmin bool `json:"is_admin"` + IsOwner bool `json:"is_owner"` + IsActive bool `json:"is_active"` +} + +type UserParticipantCredentials struct { + Email string `json:"email,omitempty"` + Phone string `json:"phone,omitempty"` + Telegram string `json:"telegram,omitempty"` +} diff --git a/backend/internal/interface/rest/domain/transaction.go b/backend/internal/interface/rest/domain/transaction.go new file mode 100644 index 0000000..4222259 --- /dev/null +++ b/backend/internal/interface/rest/domain/transaction.go @@ -0,0 +1,18 @@ +package domain + +type Transaction struct { + Id string `json:"id"` + Description string `json:"description"` + OrganizationId string `json:"organization_id"` + CreatedBy string `json:"created_by"` + Amount float64 `json:"amount"` + ToAddr string `json:"to"` + MaxFeeAllowed float64 `json:"max_fee_allowed"` + Deadline int64 `json:"deadline,omitempty"` + Status int `json:"status"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` + ConfirmedAt int64 `json:"confirmed_at,omitempty"` + CancelledAt int64 `json:"cancelled_at,omitempty"` + CommitedAt int64 `json:"commited_at,omitempty"` +} diff --git a/backend/internal/interface/rest/errors.go b/backend/internal/interface/rest/errors.go new file mode 100644 index 0000000..f61e867 --- /dev/null +++ b/backend/internal/interface/rest/errors.go @@ -0,0 +1,46 @@ +package rest + +import ( + "errors" + "net/http" + + "github.com/emochka2007/block-accounting/internal/interface/rest/controllers" + "github.com/emochka2007/block-accounting/internal/usecase/interactors/jwt" +) + +var ( + ErrorBadPathParams = errors.New("bad path params") +) + +type apiError struct { + Code int `json:"code"` + Message string `json:"message"` +} + +func buildApiError(code int, message string) apiError { + return apiError{ + Code: code, + Message: message, + } +} + +func mapError(err error) apiError { + switch { + // server error + case errors.Is(err, ErrorBadPathParams): + return buildApiError(http.StatusBadRequest, "Invalid Path Params") + // auth controller errors + case errors.Is(err, controllers.ErrorAuthInvalidMnemonic): + return buildApiError(http.StatusBadRequest, "Invalid Mnemonic") + case errors.Is(err, controllers.ErrorTokenRequired): + return buildApiError(http.StatusUnauthorized, "Token Required") + + // jwt-related errors + case errors.Is(err, jwt.ErrorTokenExpired): + return buildApiError(http.StatusUnauthorized, "Token Expired") + case errors.Is(err, jwt.ErrorInvalidTokenClaims): + return buildApiError(http.StatusUnauthorized, "Invalid Token") + default: + return buildApiError(http.StatusInternalServerError, "Internal Server Error") + } +} diff --git a/backend/internal/interface/rest/presenters/auth.go b/backend/internal/interface/rest/presenters/auth.go new file mode 100644 index 0000000..2a64f91 --- /dev/null +++ b/backend/internal/interface/rest/presenters/auth.go @@ -0,0 +1,81 @@ +package presenters + +import ( + "encoding/json" + "fmt" + "time" + + "github.com/emochka2007/block-accounting/internal/interface/rest/domain" + "github.com/emochka2007/block-accounting/internal/pkg/models" + "github.com/emochka2007/block-accounting/internal/usecase/interactors/jwt" +) + +type AuthPresenter interface { + ResponseJoin(user *models.User) ([]byte, error) + ResponseLogin(user *models.User) ([]byte, error) + ResponseRefresh(tokens jwt.AccessToken) ([]byte, error) +} + +type authPresenter struct { + jwtInteractor jwt.JWTInteractor +} + +func NewAuthPresenter( + jwtInteractor jwt.JWTInteractor, +) AuthPresenter { + return &authPresenter{ + jwtInteractor: jwtInteractor, + } +} + +func (p *authPresenter) ResponseJoin(user *models.User) ([]byte, error) { + tokens, err := p.jwtInteractor.NewToken(user, 24*time.Hour) + if err != nil { + return nil, fmt.Errorf("error create access token. %w", err) + } + + out, err := json.Marshal(domain.LoginResponse{ + Token: tokens.Token, + RefreshToken: tokens.RefreshToken, + ExpiredAt: tokens.ExpiredAt.UnixMilli(), + RTExpiredAt: tokens.RTExpiredAt.UnixMilli(), + }) + if err != nil { + return nil, fmt.Errorf("error marshal join response. %w", err) + } + + return out, nil +} + +func (p *authPresenter) ResponseLogin(user *models.User) ([]byte, error) { + tokens, err := p.jwtInteractor.NewToken(user, 24*time.Hour) + if err != nil { + return nil, fmt.Errorf("error create access token. %w", err) + } + + out, err := json.Marshal(domain.LoginResponse{ + Token: tokens.Token, + RefreshToken: tokens.RefreshToken, + ExpiredAt: tokens.ExpiredAt.UnixMilli(), + RTExpiredAt: tokens.RTExpiredAt.UnixMilli(), + }) + if err != nil { + return nil, fmt.Errorf("error marshal login response. %w", err) + } + + return out, nil +} + +func (p *authPresenter) ResponseRefresh(tokens jwt.AccessToken) ([]byte, error) { + out, err := json.Marshal(domain.LoginResponse{ + Token: tokens.Token, + RefreshToken: tokens.RefreshToken, + ExpiredAt: tokens.ExpiredAt.UnixMilli(), + RTExpiredAt: tokens.RTExpiredAt.UnixMilli(), + }) + if err != nil { + return nil, fmt.Errorf("error marshal refresh response. %w", err) + } + + return out, nil +} diff --git a/backend/internal/interface/rest/presenters/organizations.go b/backend/internal/interface/rest/presenters/organizations.go new file mode 100644 index 0000000..3b255a6 --- /dev/null +++ b/backend/internal/interface/rest/presenters/organizations.go @@ -0,0 +1,89 @@ +package presenters + +import ( + "encoding/json" + "fmt" + + "github.com/emochka2007/block-accounting/internal/interface/rest/domain" + "github.com/emochka2007/block-accounting/internal/interface/rest/domain/hal" + "github.com/emochka2007/block-accounting/internal/pkg/models" +) + +type OrganizationsPresenter interface { + ResponseCreate(organization *models.Organization) ([]byte, error) + ResponseList(orgs []*models.Organization, nextCursor string) ([]byte, error) + Organizations(orgs []*models.Organization) []*hal.Resource +} + +type organizationsPresenter struct { +} + +func NewOrganizationsPresenter() OrganizationsPresenter { + return &organizationsPresenter{} +} + +func (p *organizationsPresenter) ResponseCreate(o *models.Organization) ([]byte, error) { + org := domain.Organization{ + Id: o.ID.String(), + Name: o.Name, + Address: o.Address, + CreatedAt: uint64(o.CreatedAt.UnixMilli()), + UpdatedAt: uint64(o.UpdatedAt.UnixMilli()), + } + + r := hal.NewResource( + org, + "/organizations/"+org.Id, + hal.WithType("organization"), + ) + + out, err := json.Marshal(r) + if err != nil { + return nil, fmt.Errorf("error marshal organization create response. %w", err) + } + + return out, nil +} + +func (p *organizationsPresenter) ResponseList(orgs []*models.Organization, nextCursor string) ([]byte, error) { + dtoOrgs := domain.Collection[*hal.Resource]{ + Items: p.Organizations(orgs), + Pagination: domain.Pagination{ + NextCursor: nextCursor, + TotalItems: uint32(len(orgs)), + }, + } + + r := hal.NewResource( + dtoOrgs, + "/organizations", + hal.WithType("organizations"), + ) + + out, err := json.Marshal(r) + if err != nil { + return nil, fmt.Errorf("error marshal organizations list response. %w", err) + } + + return out, nil +} + +func (p *organizationsPresenter) Organizations(orgs []*models.Organization) []*hal.Resource { + out := make([]*hal.Resource, len(orgs)) + + for i, o := range orgs { + org := domain.Organization{ + Id: o.ID.String(), + Name: o.Name, + Address: o.Address, + CreatedAt: uint64(o.CreatedAt.UnixMilli()), + UpdatedAt: uint64(o.UpdatedAt.UnixMilli()), + } + + r := hal.NewResource(org, "/organizations/"+org.Id) + + out[i] = r + } + + return out +} diff --git a/backend/internal/interface/rest/presenters/participants.go b/backend/internal/interface/rest/presenters/participants.go new file mode 100644 index 0000000..aae3416 --- /dev/null +++ b/backend/internal/interface/rest/presenters/participants.go @@ -0,0 +1,118 @@ +package presenters + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/emochka2007/block-accounting/internal/interface/rest/domain" + "github.com/emochka2007/block-accounting/internal/interface/rest/domain/hal" + "github.com/emochka2007/block-accounting/internal/pkg/ctxmeta" + "github.com/emochka2007/block-accounting/internal/pkg/models" +) + +type ParticipantsPresenter interface { + ResponseListParticipants( + ctx context.Context, + participants []models.OrganizationParticipant, + ) ([]byte, error) +} + +type participantsPresenter struct{} + +func NewParticipantsPresenter() ParticipantsPresenter { + return new(participantsPresenter) +} + +func (p *participantsPresenter) responseParticipant( + ctx context.Context, + participant models.OrganizationParticipant, +) (*hal.Resource, error) { + domainParticipant := &domain.Participant{ + ID: participant.Id().String(), + Name: participant.ParticipantName(), + Position: participant.Position(), + CreatedAt: participant.CreatedDate().UnixMilli(), + UpdatedAt: participant.UpdatedDate().UnixMilli(), + } + + if !participant.DeletedDate().IsZero() { + domainParticipant.DeletedAt = participant.DeletedDate().UnixMilli() + } + + if user := participant.GetUser(); user != nil { + if user.Credentails != nil { + domainParticipant.Credentials = &domain.UserParticipantCredentials{ + Email: user.Credentails.Email, + Phone: user.Credentails.Phone, + Telegram: user.Credentails.Telegram, + } + } + + domainParticipant.IsUser = true + domainParticipant.IsAdmin = user.IsAdmin() + domainParticipant.IsOwner = user.IsOwner() + + } + + organizationID, err := ctxmeta.OrganizationId(ctx) + if err != nil { + return nil, fmt.Errorf("error fetch organization id from context. %w", err) + } + + r := hal.NewResource( + domainParticipant, + "/organizations/"+organizationID.String()+"/participants"+domainParticipant.ID, + hal.WithType("participant"), + ) + + return r, nil +} + +func (p *participantsPresenter) responseParticipants( + ctx context.Context, + participants []models.OrganizationParticipant, +) (*hal.Resource, error) { + resources := make([]*hal.Resource, len(participants)) + + for i, pt := range participants { + r, err := p.responseParticipant(ctx, pt) + if err != nil { + return nil, fmt.Errorf("error map participant to hal resource. %w", err) + } + + resources[i] = r + } + + organizationID, err := ctxmeta.OrganizationId(ctx) + if err != nil { + return nil, fmt.Errorf("error fetch organization id from context. %w", err) + } + + r := hal.NewResource( + map[string][]*hal.Resource{ + "participants": resources, + }, + "/organizations/"+organizationID.String()+"/participants", + hal.WithType("participants"), + ) + + return r, nil +} + +func (p *participantsPresenter) ResponseListParticipants( + ctx context.Context, + participants []models.OrganizationParticipant, +) ([]byte, error) { + r, err := p.responseParticipants(ctx, participants) + if err != nil { + return nil, fmt.Errorf("error map participants to hal. %w", err) + } + + out, err := json.Marshal(r) + if err != nil { + return nil, fmt.Errorf("error marshal organization create response. %w", err) + } + + return out, nil +} diff --git a/backend/internal/interface/rest/presenters/request.go b/backend/internal/interface/rest/presenters/request.go new file mode 100644 index 0000000..a3a748b --- /dev/null +++ b/backend/internal/interface/rest/presenters/request.go @@ -0,0 +1,25 @@ +package presenters + +import ( + "encoding/json" + "fmt" + "io" + "net/http" +) + +func CreateRequest[T any](r *http.Request) (*T, error) { + defer r.Body.Close() + + data, err := io.ReadAll(r.Body) + if err != nil { + return nil, fmt.Errorf("error read request body. %w", err) + } + + var request T + + if err := json.Unmarshal(data, &request); err != nil { + return nil, fmt.Errorf("error unmarshal join request. %w", err) + } + + return &request, nil +} diff --git a/backend/internal/interface/rest/presenters/tx.go b/backend/internal/interface/rest/presenters/tx.go new file mode 100644 index 0000000..d2fc82c --- /dev/null +++ b/backend/internal/interface/rest/presenters/tx.go @@ -0,0 +1,179 @@ +package presenters + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "time" + + "github.com/emochka2007/block-accounting/internal/interface/rest/domain" + "github.com/emochka2007/block-accounting/internal/interface/rest/domain/hal" + "github.com/emochka2007/block-accounting/internal/pkg/ctxmeta" + "github.com/emochka2007/block-accounting/internal/pkg/models" + "github.com/ethereum/go-ethereum/common" +) + +var ( + ErrorInvalidHexAddress = errors.New("error invalid hex address") +) + +type TransactionsPresenter interface { + RequestTransaction(ctx context.Context, r *domain.NewTransactionRequest) (models.Transaction, error) + ResponseTransaction(ctx context.Context, tx *models.Transaction) (*hal.Resource, error) + ResponseNewTransaction(ctx context.Context, tx *models.Transaction) ([]byte, error) + ResponseTransactionsArray(ctx context.Context, txs []*models.Transaction) ([]*hal.Resource, error) + ResponseListTransactions(ctx context.Context, txs []*models.Transaction, cursor string) ([]byte, error) +} + +type transactionsPresenter struct { +} + +func NewTransactionsPresenter() TransactionsPresenter { + return &transactionsPresenter{} +} + +// RequestTransaction returns a Transaction model WITHOUT CreatedBy user set. CreatedAt set as time.Now() +func (p *transactionsPresenter) RequestTransaction( + ctx context.Context, + r *domain.NewTransactionRequest, +) (models.Transaction, error) { + if !common.IsHexAddress(r.ToAddr) { + return models.Transaction{}, ErrorInvalidHexAddress + } + + toAddress := common.HexToAddress(r.ToAddr) + + organizationID, err := ctxmeta.OrganizationId(ctx) + if err != nil { + return models.Transaction{}, fmt.Errorf("error fetch organization id from context. %w", err) + } + + tx := models.Transaction{ + OrganizationId: organizationID, + Description: r.Description, + Amount: r.Amount, + ToAddr: toAddress.Bytes(), + MaxFeeAllowed: r.MaxFeeAllowed, + CreatedAt: time.Now(), + } + + if r.Deadline > 0 { + tx.Deadline = time.UnixMilli(r.Deadline) + } + + return tx, nil +} + +func (c *transactionsPresenter) ResponseTransaction( + ctx context.Context, + tx *models.Transaction, +) (*hal.Resource, error) { + r := &domain.Transaction{ + Id: tx.Id.String(), + Description: tx.Description, + OrganizationId: tx.OrganizationId.String(), + CreatedBy: tx.CreatedBy.Id().String(), + Amount: tx.Amount, + MaxFeeAllowed: tx.MaxFeeAllowed, + Status: tx.Status, + CreatedAt: tx.CreatedAt.UnixMilli(), + UpdatedAt: tx.UpdatedAt.UnixMilli(), + } + + addr := common.BytesToAddress(tx.ToAddr) + + r.ToAddr = addr.String() + + if !tx.ConfirmedAt.IsZero() { + r.ConfirmedAt = tx.ConfirmedAt.UnixMilli() + } + + if !tx.CancelledAt.IsZero() { + r.CancelledAt = tx.CancelledAt.UnixMilli() + } + + if !tx.CommitedAt.IsZero() { + r.CommitedAt = tx.CommitedAt.UnixMilli() + } + + if !tx.Deadline.IsZero() { + r.Deadline = tx.Deadline.UnixMilli() + } + + return hal.NewResource( + r, + "/organizations/"+tx.OrganizationId.String()+"/transactions/"+tx.Id.String(), + hal.WithType("transaction"), + ), nil +} + +func (p *transactionsPresenter) ResponseTransactionsArray( + ctx context.Context, + txs []*models.Transaction, +) ([]*hal.Resource, error) { + out := make([]*hal.Resource, len(txs)) + + for i, tx := range txs { + r, err := p.ResponseTransaction(ctx, tx) + if err != nil { + return nil, fmt.Errorf("error map transaction to hal reource. %w", err) + } + + out[i] = r + } + + return out, nil +} + +func (p *transactionsPresenter) ResponseListTransactions( + ctx context.Context, + txs []*models.Transaction, + cursor string, +) ([]byte, error) { + organizationID, err := ctxmeta.OrganizationId(ctx) + if err != nil { + return nil, fmt.Errorf("error fetch organization id from context. %w", err) + } + + arr, err := p.ResponseTransactionsArray(ctx, txs) + if err != nil { + return nil, fmt.Errorf("error map transactions list to resource array. %w", err) + } + + txsResource := map[string]any{"transactions": arr} + + if cursor != "" { + txsResource["next_cursor"] = cursor + } + + r := hal.NewResource( + txsResource, + "/organizations/"+organizationID.String()+"/transactions", + hal.WithType("organizations"), + ) + + out, err := json.Marshal(r) + if err != nil { + return nil, fmt.Errorf("error marshal tx to hal resource. %w", err) + } + + return out, nil +} + +func (c *transactionsPresenter) ResponseNewTransaction( + ctx context.Context, + tx *models.Transaction, +) ([]byte, error) { + dtoTx, err := c.ResponseTransaction(ctx, tx) + if err != nil { + return nil, fmt.Errorf("error map tx to dto. %w", err) + } + + out, err := json.Marshal(dtoTx) + if err != nil { + return nil, fmt.Errorf("error marshal tx to hal resource. %w", err) + } + + return out, nil +} diff --git a/backend/internal/interface/rest/server.go b/backend/internal/interface/rest/server.go new file mode 100644 index 0000000..544ffd4 --- /dev/null +++ b/backend/internal/interface/rest/server.go @@ -0,0 +1,277 @@ +package rest + +import ( + "context" + "encoding/json" + "log/slog" + "net/http" + "strings" + "sync" + + "github.com/emochka2007/block-accounting/internal/interface/rest/controllers" + "github.com/emochka2007/block-accounting/internal/pkg/config" + "github.com/emochka2007/block-accounting/internal/pkg/ctxmeta" + "github.com/emochka2007/block-accounting/internal/pkg/logger" + "github.com/emochka2007/block-accounting/internal/pkg/metrics" + "github.com/emochka2007/block-accounting/internal/usecase/interactors/jwt" + "github.com/go-chi/chi/v5" + mw "github.com/go-chi/chi/v5/middleware" + "github.com/go-chi/render" + "github.com/google/uuid" +) + +type Server struct { + *chi.Mux + + ctx context.Context + + log *slog.Logger + addr string + tls bool + controllers *controllers.RootController + + jwt jwt.JWTInteractor + + closeMu sync.RWMutex + closed bool +} + +func NewServer( + log *slog.Logger, + conf config.RestConfig, + controllers *controllers.RootController, + jwt jwt.JWTInteractor, +) *Server { + s := &Server{ + log: log, + addr: conf.Address, + tls: conf.TLS, + controllers: controllers, + jwt: jwt, + } + + s.buildRouter() + + return s +} + +func (s *Server) Serve(ctx context.Context) error { + s.ctx = ctx + + s.log.Info( + "starting rest interface", + slog.String("addr", s.addr), + slog.Bool("tls", s.tls), + ) + + if s.tls { + return http.ListenAndServeTLS(s.addr, "/todo", "/todo", s) + } + + metrics.Initialize(s.Mux) + + return http.ListenAndServe(s.addr, s) +} + +func (s *Server) Close() { + s.closeMu.Lock() + + s.closed = true + + s.closeMu.Unlock() +} + +func (s *Server) buildRouter() { + router := chi.NewRouter() + + router.Use(mw.Recoverer) + router.Use(mw.RequestID) + router.Use(s.handleMw) + + router.Use(render.SetContentType(render.ContentTypeJSON)) + + router.Get("/ping", s.handle(s.controllers.Ping.Ping, "ping")) + + router.Post("/join", s.handle(s.controllers.Auth.Join, "join")) + router.Post("/login", s.handle(s.controllers.Auth.Login, "login")) + router.Get("/refresh", s.handle(s.controllers.Auth.Refresh, "refresh")) + + router.Route("/organizations", func(r chi.Router) { + r = r.With(s.withAuthorization) + + r.Get("/", s.handle(s.controllers.Organizations.ListOrganizations, "list_organizations")) + r.Post("/", s.handle(s.controllers.Organizations.NewOrganization, "new_organization")) + + r.Route("/{organization_id}", func(r chi.Router) { + // r.Put("/", s.handle(s.controllers.Organizations.NewOrganization, "update_organization")) + // r.Delete("/", s.handle(s.controllers.Organizations.NewOrganization, "delete_organization")) + + r.Route("/transactions", func(r chi.Router) { + r.Get("/", s.handle(s.controllers.Transactions.List, "tx_list")) + r.Post("/", s.handle(s.controllers.Transactions.New, "new_tx")) + r.Put( + "/{tx_id}", + s.handle(s.controllers.Transactions.UpdateStatus, "update_tx_status"), + ) + }) + + r.Route("/payout", func(r chi.Router) { + r.Post("/", nil) + }) + + // join via invite link + r.Post("/invite/{hash}", s.handle(s.controllers.Auth.JoinWithInvite, "invite_join")) + + r.Route("/participants", func(r chi.Router) { + r.Get("/", s.handle(s.controllers.Participants.List, "participants_list")) + + r.Put("/{participant_id}", nil) // update user / employee + r.Delete("/{participant_id}", nil) // remove user / employee + + r.Post("/", nil) // add {employee} + + r.Post("/invite", s.handle(s.controllers.Auth.Invite, "invite")) // generate new invite link + }) + + r.Route("/multisig", func(r chi.Router) { + r.Post("/", nil) // new multisig + r.Get("/", nil) // list + + }) + }) + }) + + s.Mux = router +} + +func (s *Server) handle( + h func(w http.ResponseWriter, req *http.Request) ([]byte, error), + method_name string, +) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // started := time.Now() + // defer func() { + // metrics.RequestDurations.(prometheus.ExemplarObserver).ObserveWithExemplar( + // time.Since(started).Seconds(), prometheus.Labels{ + // "reqId": fmt.Sprint(r.Context().Value(mw.RequestIDKey)), + // "method_name": method_name, + // }, + // ) + // }() + + out, err := h(w, r) + if err != nil { + s.log.Error( + "http error", + slog.String("method_name", method_name), + logger.Err(err), + ) + + s.responseError(w, err) + + return + } + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + if _, err = w.Write(out); err != nil { + s.log.Error( + "error write http response", + slog.String("method_name", method_name), + logger.Err(err), + ) + } + } +} + +func (s *Server) responseError(w http.ResponseWriter, e error) { + s.log.Error("error handle request", logger.Err(e)) + + apiErr := mapError(e) + + out, err := json.Marshal(apiErr) + if err != nil { + s.log.Error("error marshal api error", logger.Err(err)) + + return + } + + w.WriteHeader(apiErr.Code) + w.Write(out) +} + +func (s *Server) handleMw(next http.Handler) http.Handler { + // todo add rate limiter && cirquit braker + + fn := func(w http.ResponseWriter, r *http.Request) { + s.closeMu.RLock() + defer s.closeMu.RUnlock() + + if s.closed { // keep mutex closed + return + } + + w.Header().Add("Content-Type", "application/json") + + next.ServeHTTP(w, r) + } + + return http.HandlerFunc(fn) +} + +func (s *Server) withAuthorization(next http.Handler) http.Handler { + fn := func(w http.ResponseWriter, r *http.Request) { + tokenStringRaw := r.Header.Get("Authorization") + if tokenStringRaw == "" { + s.log.Warn( + "unauthorized request", + slog.String("remote_addr", r.RemoteAddr), + slog.String("endpoint", r.RequestURI), + ) + + w.WriteHeader(401) + + return + } + + tokenString := strings.Split(tokenStringRaw, " ")[1] + + user, err := s.jwt.User(tokenString) + if err != nil { + s.log.Warn( + "unauthorized request", + slog.String("remote_addr", r.RemoteAddr), + slog.String("endpoint", r.RequestURI), + logger.Err(err), + ) + + s.responseError(w, err) + return + } + + ctx := ctxmeta.UserContext(r.Context(), user) + + if organizationID := chi.URLParam(r, "organization_id"); organizationID != "" { + organizationUUID, err := uuid.Parse(organizationID) + if err != nil { + s.log.Warn( + "invalid path org id", + slog.String("remote_addr", r.RemoteAddr), + slog.String("endpoint", r.RequestURI), + slog.String("org path id", organizationID), + logger.Err(err), + ) + + s.responseError(w, ErrorBadPathParams) + return + } + + ctx = ctxmeta.OrganizationIdContext(ctx, organizationUUID) + } + + next.ServeHTTP(w, r.WithContext(ctx)) + } + + return http.HandlerFunc(fn) +} diff --git a/backend/internal/pkg/bip39/bip39.go b/backend/internal/pkg/bip39/bip39.go new file mode 100644 index 0000000..574c0e4 --- /dev/null +++ b/backend/internal/pkg/bip39/bip39.go @@ -0,0 +1,348 @@ +package bip39 + +import ( + "crypto/rand" + "crypto/sha256" + "crypto/sha512" + "encoding/binary" + "errors" + "fmt" + "math/big" + "strings" + + "golang.org/x/crypto/pbkdf2" +) + +var ( + // Some bitwise operands for working with big.Ints. + last11BitsMask = big.NewInt(2047) + shift11BitsMask = big.NewInt(2048) + bigOne = big.NewInt(1) + bigTwo = big.NewInt(2) + + // wordLengthChecksumMasksMapping is used to isolate the checksum bits from + //the entropy+checksum byte array. + wordLengthChecksumMasksMapping = map[int]*big.Int{ + 12: big.NewInt(15), + 15: big.NewInt(31), + 18: big.NewInt(63), + 21: big.NewInt(127), + 24: big.NewInt(255), + } + + // wordLengthChecksumShiftMapping is used to lookup the number of operand + // for shifting bits to handle checksums. + wordLengthChecksumShiftMapping = map[int]*big.Int{ + 12: big.NewInt(16), + 15: big.NewInt(8), + 18: big.NewInt(4), + 21: big.NewInt(2), + } + + // wordList is the set of words to use. + wordList []string + + // wordMap is a reverse lookup map for wordList. + wordMap map[string]int +) + +var ( + // ErrInvalidMnemonic is returned when trying to use a malformed mnemonic. + ErrInvalidMnemonic = errors.New("Invalid mnenomic") + + // ErrEntropyLengthInvalid is returned when trying to use an entropy set with + // an invalid size. + ErrEntropyLengthInvalid = errors.New("Entropy length must be [128, 256] and a multiple of 32") + + // ErrValidatedSeedLengthMismatch is returned when a validated seed is not the + // same size as the given seed. This should never happen is present only as a + // sanity assertion. + ErrValidatedSeedLengthMismatch = errors.New("Seed length does not match validated seed length") + + // ErrChecksumIncorrect is returned when entropy has the incorrect checksum. + ErrChecksumIncorrect = errors.New("Checksum incorrect") +) + +func init() { + SetWordList(English) +} + +// SetWordList sets the list of words to use for mnemonics. Currently the list +// that is set is used package-wide. +func SetWordList(list []string) { + wordList = list + wordMap = map[string]int{} + + for i, v := range wordList { + wordMap[v] = i + } +} + +// GetWordList gets the list of words to use for mnemonics. +func GetWordList() []string { + return wordList +} + +// GetWordIndex gets word index in wordMap. +func GetWordIndex(word string) (int, bool) { + idx, ok := wordMap[word] + return idx, ok +} + +// NewEntropy will create random entropy bytes +// so long as the requested size bitSize is an appropriate size. +// +// bitSize has to be a multiple 32 and be within the inclusive range of {128, 256}. +func NewEntropy(bitSize int) ([]byte, error) { + if err := validateEntropyBitSize(bitSize); err != nil { + return nil, err + } + + entropy := make([]byte, bitSize/8) + _, _ = rand.Read(entropy) // err is always nil + + return entropy, nil +} + +// EntropyFromMnemonic takes a mnemonic generated by this library, +// and returns the input entropy used to generate the given mnemonic. +// An error is returned if the given mnemonic is invalid. +func EntropyFromMnemonic(mnemonic string) ([]byte, error) { + mnemonicSlice, isValid := splitMnemonicWords(mnemonic) + if !isValid { + return nil, ErrInvalidMnemonic + } + + // Decode the words into a big.Int. + var ( + wordBytes [2]byte + b = big.NewInt(0) + ) + + for _, v := range mnemonicSlice { + index, found := wordMap[v] + if !found { + return nil, fmt.Errorf("word `%v` not found in reverse map", v) + } + + binary.BigEndian.PutUint16(wordBytes[:], uint16(index)) + b.Mul(b, shift11BitsMask) + b.Or(b, big.NewInt(0).SetBytes(wordBytes[:])) + } + + // Build and add the checksum to the big.Int. + checksum := big.NewInt(0) + checksumMask := wordLengthChecksumMasksMapping[len(mnemonicSlice)] + checksum = checksum.And(b, checksumMask) + + b.Div(b, big.NewInt(0).Add(checksumMask, bigOne)) + + // The entropy is the underlying bytes of the big.Int. Any upper bytes of + // all 0's are not returned so we pad the beginning of the slice with empty + // bytes if necessary. + entropy := b.Bytes() + entropy = padByteSlice(entropy, len(mnemonicSlice)/3*4) + + // Generate the checksum and compare with the one we got from the mneomnic. + entropyChecksumBytes := computeChecksum(entropy) + entropyChecksum := big.NewInt(int64(entropyChecksumBytes[0])) + + if l := len(mnemonicSlice); l != 24 { + checksumShift := wordLengthChecksumShiftMapping[l] + entropyChecksum.Div(entropyChecksum, checksumShift) + } + + if checksum.Cmp(entropyChecksum) != 0 { + return nil, ErrChecksumIncorrect + } + + return entropy, nil +} + +// NewMnemonic will return a string consisting of the mnemonic words for +// the given entropy. +// If the provide entropy is invalid, an error will be returned. +func NewMnemonic(entropy []byte) (string, error) { + // Compute some lengths for convenience. + entropyBitLength := len(entropy) * 8 + checksumBitLength := entropyBitLength / 32 + sentenceLength := (entropyBitLength + checksumBitLength) / 11 + + // Validate that the requested size is supported. + err := validateEntropyBitSize(entropyBitLength) + if err != nil { + return "", err + } + + // Add checksum to entropy. + entropy = addChecksum(entropy) + + // Break entropy up into sentenceLength chunks of 11 bits. + // For each word AND mask the rightmost 11 bits and find the word at that index. + // Then bitshift entropy 11 bits right and repeat. + // Add to the last empty slot so we can work with LSBs instead of MSB. + + // Entropy as an int so we can bitmask without worrying about bytes slices. + entropyInt := new(big.Int).SetBytes(entropy) + + // Slice to hold words in. + words := make([]string, sentenceLength) + + // Throw away big.Int for AND masking. + word := big.NewInt(0) + + for i := sentenceLength - 1; i >= 0; i-- { + // Get 11 right most bits and bitshift 11 to the right for next time. + word.And(entropyInt, last11BitsMask) + entropyInt.Div(entropyInt, shift11BitsMask) + + // Get the bytes representing the 11 bits as a 2 byte slice. + wordBytes := padByteSlice(word.Bytes(), 2) + + // Convert bytes to an index and add that word to the list. + words[i] = wordList[binary.BigEndian.Uint16(wordBytes)] + } + + return strings.Join(words, " "), nil +} + +// MnemonicToByteArray takes a mnemonic string and turns it into a byte array +// suitable for creating another mnemonic. +// An error is returned if the mnemonic is invalid. +func MnemonicToByteArray(mnemonic string, raw ...bool) ([]byte, error) { + var ( + mnemonicSlice = strings.Split(mnemonic, " ") + entropyBitSize = len(mnemonicSlice) * 11 + checksumBitSize = entropyBitSize % 32 + fullByteSize = (entropyBitSize-checksumBitSize)/8 + 1 + ) + + // Turn into raw entropy. + rawEntropyBytes, err := EntropyFromMnemonic(mnemonic) + if err != nil { + return nil, err + } + + // If we want the raw entropy then we're done. + if len(raw) > 0 && raw[0] { + return rawEntropyBytes, nil + } + + // Otherwise add the checksum before returning + return padByteSlice(addChecksum(rawEntropyBytes), fullByteSize), nil +} + +// NewSeedWithErrorChecking creates a hashed seed output given the mnemonic string and a password. +// An error is returned if the mnemonic is not convertible to a byte array. +func NewSeedWithErrorChecking(mnemonic string, password string) ([]byte, error) { + _, err := MnemonicToByteArray(mnemonic) + if err != nil { + return nil, err + } + + return NewSeed(mnemonic, password), nil +} + +// NewSeed creates a hashed seed output given a provided string and password. +// No checking is performed to validate that the string provided is a valid mnemonic. +func NewSeed(mnemonic string, password string) []byte { + return pbkdf2.Key([]byte(mnemonic), []byte("mnemonic"+password), 2048, 64, sha512.New) +} + +// IsMnemonicValid attempts to verify that the provided mnemonic is valid. +// Validity is determined by both the number of words being appropriate, +// and that all the words in the mnemonic are present in the word list. +func IsMnemonicValid(mnemonic string) bool { + _, err := EntropyFromMnemonic(mnemonic) + return err == nil +} + +// Appends to data the first (len(data) / 32)bits of the result of sha256(data) +// Currently only supports data up to 32 bytes. +func addChecksum(data []byte) []byte { + // Get first byte of sha256 + hash := computeChecksum(data) + firstChecksumByte := hash[0] + + // len() is in bytes so we divide by 4 + checksumBitLength := uint(len(data) / 4) + + // For each bit of check sum we want we shift the data one the left + // and then set the (new) right most bit equal to checksum bit at that index + // staring from the left + dataBigInt := new(big.Int).SetBytes(data) + + for i := uint(0); i < checksumBitLength; i++ { + // Bitshift 1 left + dataBigInt.Mul(dataBigInt, bigTwo) + + // Set rightmost bit if leftmost checksum bit is set + if firstChecksumByte&(1<<(7-i)) > 0 { + dataBigInt.Or(dataBigInt, bigOne) + } + } + + return dataBigInt.Bytes() +} + +func computeChecksum(data []byte) []byte { + hasher := sha256.New() + _, _ = hasher.Write(data) // This error is guaranteed to be nil + + return hasher.Sum(nil) +} + +// validateEntropyBitSize ensures that entropy is the correct size for being a +// mnemonic. +func validateEntropyBitSize(bitSize int) error { + if (bitSize%32) != 0 || bitSize < 128 || bitSize > 256 { + return ErrEntropyLengthInvalid + } + + return nil +} + +// padByteSlice returns a byte slice of the given size with contents of the +// given slice left padded and any empty spaces filled with 0's. +func padByteSlice(slice []byte, length int) []byte { + offset := length - len(slice) + if offset <= 0 { + return slice + } + + newSlice := make([]byte, length) + copy(newSlice[offset:], slice) + + return newSlice +} + +// compareByteSlices returns true of the byte slices have equal contents and +// returns false otherwise. +func compareByteSlices(a, b []byte) bool { + if len(a) != len(b) { + return false + } + + for i := range a { + if a[i] != b[i] { + return false + } + } + + return true +} + +func splitMnemonicWords(mnemonic string) ([]string, bool) { + // Create a list of all the words in the mnemonic sentence + words := strings.Fields(mnemonic) + + // Get num of words + numOfWords := len(words) + + // The number of words should be 12, 15, 18, 21 or 24 + if numOfWords%3 != 0 || numOfWords < 12 || numOfWords > 24 { + return nil, false + } + + return words, true +} diff --git a/backend/internal/pkg/bip39/wordslist.go b/backend/internal/pkg/bip39/wordslist.go new file mode 100644 index 0000000..038412c --- /dev/null +++ b/backend/internal/pkg/bip39/wordslist.go @@ -0,0 +1,2071 @@ +package bip39 + +import ( + "fmt" + "hash/crc32" + "strings" +) + +func init() { + // Ensure word list is correct + // $ wget https://raw.githubusercontent.com/bitcoin/bips/master/bip-0039/english.txt + // $ crc32 english.txt + // c1dbd296 + checksum := crc32.ChecksumIEEE([]byte(english)) + if fmt.Sprintf("%x", checksum) != "c1dbd296" { + panic("english checksum invalid") // TODO remove panic + } +} + +// English is a slice of mnemonic words taken from the bip39 specification +// https://raw.githubusercontent.com/bitcoin/bips/master/bip-0039/english.txt +var English = strings.Split(strings.TrimSpace(english), "\n") +var english = `abandon +ability +able +about +above +absent +absorb +abstract +absurd +abuse +access +accident +account +accuse +achieve +acid +acoustic +acquire +across +act +action +actor +actress +actual +adapt +add +addict +address +adjust +admit +adult +advance +advice +aerobic +affair +afford +afraid +again +age +agent +agree +ahead +aim +air +airport +aisle +alarm +album +alcohol +alert +alien +all +alley +allow +almost +alone +alpha +already +also +alter +always +amateur +amazing +among +amount +amused +analyst +anchor +ancient +anger +angle +angry +animal +ankle +announce +annual +another +answer +antenna +antique +anxiety +any +apart +apology +appear +apple +approve +april +arch +arctic +area +arena +argue +arm +armed +armor +army +around +arrange +arrest +arrive +arrow +art +artefact +artist +artwork +ask +aspect +assault +asset +assist +assume +asthma +athlete +atom +attack +attend +attitude +attract +auction +audit +august +aunt +author +auto +autumn +average +avocado +avoid +awake +aware +away +awesome +awful +awkward +axis +baby +bachelor +bacon +badge +bag +balance +balcony +ball +bamboo +banana +banner +bar +barely +bargain +barrel +base +basic +basket +battle +beach +bean +beauty +because +become +beef +before +begin +behave +behind +believe +below +belt +bench +benefit +best +betray +better +between +beyond +bicycle +bid +bike +bind +biology +bird +birth +bitter +black +blade +blame +blanket +blast +bleak +bless +blind +blood +blossom +blouse +blue +blur +blush +board +boat +body +boil +bomb +bone +bonus +book +boost +border +boring +borrow +boss +bottom +bounce +box +boy +bracket +brain +brand +brass +brave +bread +breeze +brick +bridge +brief +bright +bring +brisk +broccoli +broken +bronze +broom +brother +brown +brush +bubble +buddy +budget +buffalo +build +bulb +bulk +bullet +bundle +bunker +burden +burger +burst +bus +business +busy +butter +buyer +buzz +cabbage +cabin +cable +cactus +cage +cake +call +calm +camera +camp +can +canal +cancel +candy +cannon +canoe +canvas +canyon +capable +capital +captain +car +carbon +card +cargo +carpet +carry +cart +case +cash +casino +castle +casual +cat +catalog +catch +category +cattle +caught +cause +caution +cave +ceiling +celery +cement +census +century +cereal +certain +chair +chalk +champion +change +chaos +chapter +charge +chase +chat +cheap +check +cheese +chef +cherry +chest +chicken +chief +child +chimney +choice +choose +chronic +chuckle +chunk +churn +cigar +cinnamon +circle +citizen +city +civil +claim +clap +clarify +claw +clay +clean +clerk +clever +click +client +cliff +climb +clinic +clip +clock +clog +close +cloth +cloud +clown +club +clump +cluster +clutch +coach +coast +coconut +code +coffee +coil +coin +collect +color +column +combine +come +comfort +comic +common +company +concert +conduct +confirm +congress +connect +consider +control +convince +cook +cool +copper +copy +coral +core +corn +correct +cost +cotton +couch +country +couple +course +cousin +cover +coyote +crack +cradle +craft +cram +crane +crash +crater +crawl +crazy +cream +credit +creek +crew +cricket +crime +crisp +critic +crop +cross +crouch +crowd +crucial +cruel +cruise +crumble +crunch +crush +cry +crystal +cube +culture +cup +cupboard +curious +current +curtain +curve +cushion +custom +cute +cycle +dad +damage +damp +dance +danger +daring +dash +daughter +dawn +day +deal +debate +debris +decade +december +decide +decline +decorate +decrease +deer +defense +define +defy +degree +delay +deliver +demand +demise +denial +dentist +deny +depart +depend +deposit +depth +deputy +derive +describe +desert +design +desk +despair +destroy +detail +detect +develop +device +devote +diagram +dial +diamond +diary +dice +diesel +diet +differ +digital +dignity +dilemma +dinner +dinosaur +direct +dirt +disagree +discover +disease +dish +dismiss +disorder +display +distance +divert +divide +divorce +dizzy +doctor +document +dog +doll +dolphin +domain +donate +donkey +donor +door +dose +double +dove +draft +dragon +drama +drastic +draw +dream +dress +drift +drill +drink +drip +drive +drop +drum +dry +duck +dumb +dune +during +dust +dutch +duty +dwarf +dynamic +eager +eagle +early +earn +earth +easily +east +easy +echo +ecology +economy +edge +edit +educate +effort +egg +eight +either +elbow +elder +electric +elegant +element +elephant +elevator +elite +else +embark +embody +embrace +emerge +emotion +employ +empower +empty +enable +enact +end +endless +endorse +enemy +energy +enforce +engage +engine +enhance +enjoy +enlist +enough +enrich +enroll +ensure +enter +entire +entry +envelope +episode +equal +equip +era +erase +erode +erosion +error +erupt +escape +essay +essence +estate +eternal +ethics +evidence +evil +evoke +evolve +exact +example +excess +exchange +excite +exclude +excuse +execute +exercise +exhaust +exhibit +exile +exist +exit +exotic +expand +expect +expire +explain +expose +express +extend +extra +eye +eyebrow +fabric +face +faculty +fade +faint +faith +fall +false +fame +family +famous +fan +fancy +fantasy +farm +fashion +fat +fatal +father +fatigue +fault +favorite +feature +february +federal +fee +feed +feel +female +fence +festival +fetch +fever +few +fiber +fiction +field +figure +file +film +filter +final +find +fine +finger +finish +fire +firm +first +fiscal +fish +fit +fitness +fix +flag +flame +flash +flat +flavor +flee +flight +flip +float +flock +floor +flower +fluid +flush +fly +foam +focus +fog +foil +fold +follow +food +foot +force +forest +forget +fork +fortune +forum +forward +fossil +foster +found +fox +fragile +frame +frequent +fresh +friend +fringe +frog +front +frost +frown +frozen +fruit +fuel +fun +funny +furnace +fury +future +gadget +gain +galaxy +gallery +game +gap +garage +garbage +garden +garlic +garment +gas +gasp +gate +gather +gauge +gaze +general +genius +genre +gentle +genuine +gesture +ghost +giant +gift +giggle +ginger +giraffe +girl +give +glad +glance +glare +glass +glide +glimpse +globe +gloom +glory +glove +glow +glue +goat +goddess +gold +good +goose +gorilla +gospel +gossip +govern +gown +grab +grace +grain +grant +grape +grass +gravity +great +green +grid +grief +grit +grocery +group +grow +grunt +guard +guess +guide +guilt +guitar +gun +gym +habit +hair +half +hammer +hamster +hand +happy +harbor +hard +harsh +harvest +hat +have +hawk +hazard +head +health +heart +heavy +hedgehog +height +hello +helmet +help +hen +hero +hidden +high +hill +hint +hip +hire +history +hobby +hockey +hold +hole +holiday +hollow +home +honey +hood +hope +horn +horror +horse +hospital +host +hotel +hour +hover +hub +huge +human +humble +humor +hundred +hungry +hunt +hurdle +hurry +hurt +husband +hybrid +ice +icon +idea +identify +idle +ignore +ill +illegal +illness +image +imitate +immense +immune +impact +impose +improve +impulse +inch +include +income +increase +index +indicate +indoor +industry +infant +inflict +inform +inhale +inherit +initial +inject +injury +inmate +inner +innocent +input +inquiry +insane +insect +inside +inspire +install +intact +interest +into +invest +invite +involve +iron +island +isolate +issue +item +ivory +jacket +jaguar +jar +jazz +jealous +jeans +jelly +jewel +job +join +joke +journey +joy +judge +juice +jump +jungle +junior +junk +just +kangaroo +keen +keep +ketchup +key +kick +kid +kidney +kind +kingdom +kiss +kit +kitchen +kite +kitten +kiwi +knee +knife +knock +know +lab +label +labor +ladder +lady +lake +lamp +language +laptop +large +later +latin +laugh +laundry +lava +law +lawn +lawsuit +layer +lazy +leader +leaf +learn +leave +lecture +left +leg +legal +legend +leisure +lemon +lend +length +lens +leopard +lesson +letter +level +liar +liberty +library +license +life +lift +light +like +limb +limit +link +lion +liquid +list +little +live +lizard +load +loan +lobster +local +lock +logic +lonely +long +loop +lottery +loud +lounge +love +loyal +lucky +luggage +lumber +lunar +lunch +luxury +lyrics +machine +mad +magic +magnet +maid +mail +main +major +make +mammal +man +manage +mandate +mango +mansion +manual +maple +marble +march +margin +marine +market +marriage +mask +mass +master +match +material +math +matrix +matter +maximum +maze +meadow +mean +measure +meat +mechanic +medal +media +melody +melt +member +memory +mention +menu +mercy +merge +merit +merry +mesh +message +metal +method +middle +midnight +milk +million +mimic +mind +minimum +minor +minute +miracle +mirror +misery +miss +mistake +mix +mixed +mixture +mobile +model +modify +mom +moment +monitor +monkey +monster +month +moon +moral +more +morning +mosquito +mother +motion +motor +mountain +mouse +move +movie +much +muffin +mule +multiply +muscle +museum +mushroom +music +must +mutual +myself +mystery +myth +naive +name +napkin +narrow +nasty +nation +nature +near +neck +need +negative +neglect +neither +nephew +nerve +nest +net +network +neutral +never +news +next +nice +night +noble +noise +nominee +noodle +normal +north +nose +notable +note +nothing +notice +novel +now +nuclear +number +nurse +nut +oak +obey +object +oblige +obscure +observe +obtain +obvious +occur +ocean +october +odor +off +offer +office +often +oil +okay +old +olive +olympic +omit +once +one +onion +online +only +open +opera +opinion +oppose +option +orange +orbit +orchard +order +ordinary +organ +orient +original +orphan +ostrich +other +outdoor +outer +output +outside +oval +oven +over +own +owner +oxygen +oyster +ozone +pact +paddle +page +pair +palace +palm +panda +panel +panic +panther +paper +parade +parent +park +parrot +party +pass +patch +path +patient +patrol +pattern +pause +pave +payment +peace +peanut +pear +peasant +pelican +pen +penalty +pencil +people +pepper +perfect +permit +person +pet +phone +photo +phrase +physical +piano +picnic +picture +piece +pig +pigeon +pill +pilot +pink +pioneer +pipe +pistol +pitch +pizza +place +planet +plastic +plate +play +please +pledge +pluck +plug +plunge +poem +poet +point +polar +pole +police +pond +pony +pool +popular +portion +position +possible +post +potato +pottery +poverty +powder +power +practice +praise +predict +prefer +prepare +present +pretty +prevent +price +pride +primary +print +priority +prison +private +prize +problem +process +produce +profit +program +project +promote +proof +property +prosper +protect +proud +provide +public +pudding +pull +pulp +pulse +pumpkin +punch +pupil +puppy +purchase +purity +purpose +purse +push +put +puzzle +pyramid +quality +quantum +quarter +question +quick +quit +quiz +quote +rabbit +raccoon +race +rack +radar +radio +rail +rain +raise +rally +ramp +ranch +random +range +rapid +rare +rate +rather +raven +raw +razor +ready +real +reason +rebel +rebuild +recall +receive +recipe +record +recycle +reduce +reflect +reform +refuse +region +regret +regular +reject +relax +release +relief +rely +remain +remember +remind +remove +render +renew +rent +reopen +repair +repeat +replace +report +require +rescue +resemble +resist +resource +response +result +retire +retreat +return +reunion +reveal +review +reward +rhythm +rib +ribbon +rice +rich +ride +ridge +rifle +right +rigid +ring +riot +ripple +risk +ritual +rival +river +road +roast +robot +robust +rocket +romance +roof +rookie +room +rose +rotate +rough +round +route +royal +rubber +rude +rug +rule +run +runway +rural +sad +saddle +sadness +safe +sail +salad +salmon +salon +salt +salute +same +sample +sand +satisfy +satoshi +sauce +sausage +save +say +scale +scan +scare +scatter +scene +scheme +school +science +scissors +scorpion +scout +scrap +screen +script +scrub +sea +search +season +seat +second +secret +section +security +seed +seek +segment +select +sell +seminar +senior +sense +sentence +series +service +session +settle +setup +seven +shadow +shaft +shallow +share +shed +shell +sheriff +shield +shift +shine +ship +shiver +shock +shoe +shoot +shop +short +shoulder +shove +shrimp +shrug +shuffle +shy +sibling +sick +side +siege +sight +sign +silent +silk +silly +silver +similar +simple +since +sing +siren +sister +situate +six +size +skate +sketch +ski +skill +skin +skirt +skull +slab +slam +sleep +slender +slice +slide +slight +slim +slogan +slot +slow +slush +small +smart +smile +smoke +smooth +snack +snake +snap +sniff +snow +soap +soccer +social +sock +soda +soft +solar +soldier +solid +solution +solve +someone +song +soon +sorry +sort +soul +sound +soup +source +south +space +spare +spatial +spawn +speak +special +speed +spell +spend +sphere +spice +spider +spike +spin +spirit +split +spoil +sponsor +spoon +sport +spot +spray +spread +spring +spy +square +squeeze +squirrel +stable +stadium +staff +stage +stairs +stamp +stand +start +state +stay +steak +steel +stem +step +stereo +stick +still +sting +stock +stomach +stone +stool +story +stove +strategy +street +strike +strong +struggle +student +stuff +stumble +style +subject +submit +subway +success +such +sudden +suffer +sugar +suggest +suit +summer +sun +sunny +sunset +super +supply +supreme +sure +surface +surge +surprise +surround +survey +suspect +sustain +swallow +swamp +swap +swarm +swear +sweet +swift +swim +swing +switch +sword +symbol +symptom +syrup +system +table +tackle +tag +tail +talent +talk +tank +tape +target +task +taste +tattoo +taxi +teach +team +tell +ten +tenant +tennis +tent +term +test +text +thank +that +theme +then +theory +there +they +thing +this +thought +three +thrive +throw +thumb +thunder +ticket +tide +tiger +tilt +timber +time +tiny +tip +tired +tissue +title +toast +tobacco +today +toddler +toe +together +toilet +token +tomato +tomorrow +tone +tongue +tonight +tool +tooth +top +topic +topple +torch +tornado +tortoise +toss +total +tourist +toward +tower +town +toy +track +trade +traffic +tragic +train +transfer +trap +trash +travel +tray +treat +tree +trend +trial +tribe +trick +trigger +trim +trip +trophy +trouble +truck +true +truly +trumpet +trust +truth +try +tube +tuition +tumble +tuna +tunnel +turkey +turn +turtle +twelve +twenty +twice +twin +twist +two +type +typical +ugly +umbrella +unable +unaware +uncle +uncover +under +undo +unfair +unfold +unhappy +uniform +unique +unit +universe +unknown +unlock +until +unusual +unveil +update +upgrade +uphold +upon +upper +upset +urban +urge +usage +use +used +useful +useless +usual +utility +vacant +vacuum +vague +valid +valley +valve +van +vanish +vapor +various +vast +vault +vehicle +velvet +vendor +venture +venue +verb +verify +version +very +vessel +veteran +viable +vibrant +vicious +victory +video +view +village +vintage +violin +virtual +virus +visa +visit +visual +vital +vivid +vocal +voice +void +volcano +volume +vote +voyage +wage +wagon +wait +walk +wall +walnut +want +warfare +warm +warrior +wash +wasp +waste +water +wave +way +wealth +weapon +wear +weasel +weather +web +wedding +weekend +weird +welcome +west +wet +whale +what +wheat +wheel +when +where +whip +whisper +wide +width +wife +wild +will +win +window +wine +wing +wink +winner +winter +wire +wisdom +wise +wish +witness +wolf +woman +wonder +wood +wool +word +work +world +worry +worth +wrap +wreck +wrestle +wrist +write +wrong +yard +year +yellow +you +young +youth +zebra +zero +zone +zoo +` diff --git a/backend/internal/pkg/config/config.go b/backend/internal/pkg/config/config.go new file mode 100644 index 0000000..056f75f --- /dev/null +++ b/backend/internal/pkg/config/config.go @@ -0,0 +1,40 @@ +package config + +type Config struct { + Common CommonConfig + Rest RestConfig + DB DBConfig + Eth EthConfig +} + +type CommonConfig struct { + LogLevel string + LogLocal bool + LogFile string + LogAddSource bool + + JWTSecret []byte +} + +type RestConfig struct { + Address string + TLS bool +} + +type DBConfig struct { + // persistent database config + Host string + EnableSSL bool + Database string + User string + Secret string + + // cache config + CacheHost string + CacheUser string + CacheSecret string +} + +type EthConfig struct { + // todo +} diff --git a/backend/internal/pkg/ctxmeta/context.go b/backend/internal/pkg/ctxmeta/context.go new file mode 100644 index 0000000..e7c57a7 --- /dev/null +++ b/backend/internal/pkg/ctxmeta/context.go @@ -0,0 +1,48 @@ +package ctxmeta + +import ( + "context" + "fmt" + + "github.com/emochka2007/block-accounting/internal/pkg/models" + "github.com/google/uuid" +) + +type ContextKey string + +var ( + UserContextKey = ContextKey("user") + OrganizationIdContextKey = ContextKey("org-id") + OrganizationParticipantContextKey = ContextKey("org-participant") +) + +func UserContext(parent context.Context, user *models.User) context.Context { + return context.WithValue(parent, UserContextKey, user) +} + +func User(ctx context.Context) (*models.User, error) { + if user, ok := ctx.Value(UserContextKey).(*models.User); ok { + return user, nil + } + + return nil, fmt.Errorf("error user not passed in context") +} + +func OrganizationParticipantContext( + parent context.Context, + participant models.OrganizationParticipant, +) context.Context { + return context.WithValue(parent, OrganizationParticipantContextKey, participant) +} + +func OrganizationIdContext(parent context.Context, id uuid.UUID) context.Context { + return context.WithValue(parent, OrganizationIdContextKey, id) +} + +func OrganizationId(ctx context.Context) (uuid.UUID, error) { + if id, ok := ctx.Value(OrganizationIdContextKey).(uuid.UUID); ok { + return id, nil + } + + return uuid.Nil, fmt.Errorf("error organization id not passed in context") +} diff --git a/backend/internal/pkg/hdwallet/hdwaller.go b/backend/internal/pkg/hdwallet/hdwaller.go new file mode 100644 index 0000000..e8d6762 --- /dev/null +++ b/backend/internal/pkg/hdwallet/hdwaller.go @@ -0,0 +1,562 @@ +package hdwallet + +import ( + "crypto/ecdsa" + "crypto/rand" + "errors" + "fmt" + "math/big" + "os" + "sync" + + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcutil/hdkeychain" + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/tyler-smith/go-bip39" +) + +// DefaultRootDerivationPath is the root path to which custom derivation endpoints +// are appended. As such, the first account will be at m/44'/60'/0'/0, the second +// at m/44'/60'/0'/1, etc. +var DefaultRootDerivationPath = accounts.DefaultRootDerivationPath + +// DefaultBaseDerivationPath is the base path from which custom derivation endpoints +// are incremented. As such, the first account will be at m/44'/60'/0'/0, the second +// at m/44'/60'/0'/1, etc +var DefaultBaseDerivationPath = accounts.DefaultBaseDerivationPath + +const issue179FixEnvar = "GO_ETHEREUM_HDWALLET_FIX_ISSUE_179" + +// Wallet is the underlying wallet struct. +type Wallet struct { + mnemonic string + masterKey *hdkeychain.ExtendedKey + seed []byte + url accounts.URL + paths map[common.Address]accounts.DerivationPath + accounts []accounts.Account + stateLock sync.RWMutex + fixIssue172 bool +} + +func newWallet(seed []byte) (*Wallet, error) { + masterKey, err := hdkeychain.NewMaster(seed, &chaincfg.MainNetParams) + if err != nil { + return nil, err + } + + return &Wallet{ + masterKey: masterKey, + seed: seed, + accounts: []accounts.Account{}, + paths: map[common.Address]accounts.DerivationPath{}, + fixIssue172: false || len(os.Getenv(issue179FixEnvar)) > 0, + }, nil +} + +// NewFromMnemonic returns a new wallet from a BIP-39 mnemonic. +func NewFromMnemonic(mnemonic string, passOpt ...string) (*Wallet, error) { + if mnemonic == "" { + return nil, errors.New("mnemonic is required") + } + + if !bip39.IsMnemonicValid(mnemonic) { + return nil, errors.New("mnemonic is invalid") + } + + seed, err := NewSeedFromMnemonic(mnemonic, passOpt...) + if err != nil { + return nil, err + } + + wallet, err := newWallet(seed) + if err != nil { + return nil, err + } + wallet.mnemonic = mnemonic + + return wallet, nil +} + +// NewFromSeed returns a new wallet from a BIP-39 seed. +func NewFromSeed(seed []byte) (*Wallet, error) { + if len(seed) == 0 { + return nil, errors.New("seed is required") + } + + return newWallet(seed) +} + +// URL implements accounts.Wallet, returning the URL of the device that +// the wallet is on, however this does nothing since this is not a hardware device. +func (w *Wallet) URL() accounts.URL { + return w.url +} + +// Status implements accounts.Wallet, returning a custom status message +// from the underlying vendor-specific hardware wallet implementation, +// however this does nothing since this is not a hardware device. +func (w *Wallet) Status() (string, error) { + return "ok", nil +} + +// Open implements accounts.Wallet, however this does nothing since this +// is not a hardware device. +func (w *Wallet) Open(passphrase string) error { + return nil +} + +// Close implements accounts.Wallet, however this does nothing since this +// is not a hardware device. +func (w *Wallet) Close() error { + return nil +} + +// Accounts implements accounts.Wallet, returning the list of accounts pinned to +// the wallet. If self-derivation was enabled, the account list is +// periodically expanded based on current chain state. +func (w *Wallet) Accounts() []accounts.Account { + // Attempt self-derivation if it's running + // Return whatever account list we ended up with + w.stateLock.RLock() + defer w.stateLock.RUnlock() + + cpy := make([]accounts.Account, len(w.accounts)) + copy(cpy, w.accounts) + return cpy +} + +// Contains implements accounts.Wallet, returning whether a particular account is +// or is not pinned into this wallet instance. +func (w *Wallet) Contains(account accounts.Account) bool { + w.stateLock.RLock() + defer w.stateLock.RUnlock() + + _, exists := w.paths[account.Address] + return exists +} + +// Unpin unpins account from list of pinned accounts. +func (w *Wallet) Unpin(account accounts.Account) error { + w.stateLock.RLock() + defer w.stateLock.RUnlock() + + for i, acct := range w.accounts { + if acct.Address.String() == account.Address.String() { + w.accounts = removeAtIndex(w.accounts, i) + delete(w.paths, account.Address) + return nil + } + } + + return errors.New("account not found") +} + +// SetFixIssue172 determines whether the standard (correct) bip39 +// derivation path was used, or if derivation should be affected by +// Issue172 [0] which was how this library was originally implemented. +// [0] https://github.com/btcsuite/btcutil/pull/182/files +func (w *Wallet) SetFixIssue172(fixIssue172 bool) { + w.fixIssue172 = fixIssue172 +} + +// Derive implements accounts.Wallet, deriving a new account at the specific +// derivation path. If pin is set to true, the account will be added to the list +// of tracked accounts. +func (w *Wallet) Derive(path accounts.DerivationPath, pin bool) (accounts.Account, error) { + // Try to derive the actual account and update its URL if successful + w.stateLock.RLock() // Avoid device disappearing during derivation + + address, err := w.deriveAddress(path) + + w.stateLock.RUnlock() + + // If an error occurred or no pinning was requested, return + if err != nil { + return accounts.Account{}, err + } + + account := accounts.Account{ + Address: address, + URL: accounts.URL{ + Scheme: "", + Path: path.String(), + }, + } + + if !pin { + return account, nil + } + + // Pinning needs to modify the state + w.stateLock.Lock() + defer w.stateLock.Unlock() + + if _, ok := w.paths[address]; !ok { + w.accounts = append(w.accounts, account) + w.paths[address] = path + } + + return account, nil +} + +// SelfDerive implements accounts.Wallet, trying to discover accounts that the +// user used previously (based on the chain state), but ones that he/she did not +// explicitly pin to the wallet manually. To avoid chain head monitoring, self +// derivation only runs during account listing (and even then throttled). +func (w *Wallet) SelfDerive(base []accounts.DerivationPath, chain ethereum.ChainStateReader) { + // TODO: self derivation +} + +// SignHash implements accounts.Wallet, which allows signing arbitrary data. +func (w *Wallet) SignHash(account accounts.Account, hash []byte) ([]byte, error) { + // Make sure the requested account is contained within + path, ok := w.paths[account.Address] + if !ok { + return nil, accounts.ErrUnknownAccount + } + + privateKey, err := w.derivePrivateKey(path) + if err != nil { + return nil, err + } + + return crypto.Sign(hash, privateKey) +} + +// SignTxEIP155 implements accounts.Wallet, which allows the account to sign an ERC-20 transaction. +func (w *Wallet) SignTxEIP155(account accounts.Account, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) { + w.stateLock.RLock() // Comms have own mutex, this is for the state fields + defer w.stateLock.RUnlock() + + // Make sure the requested account is contained within + path, ok := w.paths[account.Address] + if !ok { + return nil, accounts.ErrUnknownAccount + } + + privateKey, err := w.derivePrivateKey(path) + if err != nil { + return nil, err + } + + signer := types.NewEIP155Signer(chainID) + // Sign the transaction and verify the sender to avoid hardware fault surprises + signedTx, err := types.SignTx(tx, signer, privateKey) + if err != nil { + return nil, err + } + + sender, err := types.Sender(signer, signedTx) + if err != nil { + return nil, err + } + + if sender != account.Address { + return nil, fmt.Errorf("signer mismatch: expected %s, got %s", account.Address.Hex(), sender.Hex()) + } + + return signedTx, nil +} + +// SignTx implements accounts.Wallet, which allows the account to sign an Ethereum transaction. +func (w *Wallet) SignTx(account accounts.Account, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) { + w.stateLock.RLock() // Comms have own mutex, this is for the state fields + defer w.stateLock.RUnlock() + + // Make sure the requested account is contained within + path, ok := w.paths[account.Address] + if !ok { + return nil, accounts.ErrUnknownAccount + } + + privateKey, err := w.derivePrivateKey(path) + if err != nil { + return nil, err + } + + signer := types.LatestSignerForChainID(chainID) + + // Sign the transaction and verify the sender to avoid hardware fault surprises + signedTx, err := types.SignTx(tx, signer, privateKey) + if err != nil { + return nil, err + } + + sender, err := types.Sender(signer, signedTx) + if err != nil { + return nil, err + } + + if sender != account.Address { + return nil, fmt.Errorf("signer mismatch: expected %s, got %s", account.Address.Hex(), sender.Hex()) + } + + return signedTx, nil +} + +// SignHashWithPassphrase implements accounts.Wallet, attempting +// to sign the given hash with the given account using the +// passphrase as extra authentication. +func (w *Wallet) SignHashWithPassphrase(account accounts.Account, passphrase string, hash []byte) ([]byte, error) { + return w.SignHash(account, hash) +} + +// SignTxWithPassphrase implements accounts.Wallet, attempting to sign the given +// transaction with the given account using passphrase as extra authentication. +func (w *Wallet) SignTxWithPassphrase(account accounts.Account, passphrase string, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) { + return w.SignTx(account, tx, chainID) +} + +// PrivateKey returns the ECDSA private key of the account. +func (w *Wallet) PrivateKey(account accounts.Account) (*ecdsa.PrivateKey, error) { + path, err := ParseDerivationPath(account.URL.Path) + if err != nil { + return nil, err + } + + return w.derivePrivateKey(path) +} + +// PrivateKeyBytes returns the ECDSA private key in bytes format of the account. +func (w *Wallet) PrivateKeyBytes(account accounts.Account) ([]byte, error) { + privateKey, err := w.PrivateKey(account) + if err != nil { + return nil, err + } + + return crypto.FromECDSA(privateKey), nil +} + +// PrivateKeyHex return the ECDSA private key in hex string format of the account. +func (w *Wallet) PrivateKeyHex(account accounts.Account) (string, error) { + privateKeyBytes, err := w.PrivateKeyBytes(account) + if err != nil { + return "", err + } + + return hexutil.Encode(privateKeyBytes)[2:], nil +} + +// PublicKey returns the ECDSA public key of the account. +func (w *Wallet) PublicKey(account accounts.Account) (*ecdsa.PublicKey, error) { + path, err := ParseDerivationPath(account.URL.Path) + if err != nil { + return nil, err + } + + return w.derivePublicKey(path) +} + +// PublicKeyBytes returns the ECDSA public key in bytes format of the account. +func (w *Wallet) PublicKeyBytes(account accounts.Account) ([]byte, error) { + publicKey, err := w.PublicKey(account) + if err != nil { + return nil, err + } + + return crypto.FromECDSAPub(publicKey), nil +} + +// PublicKeyHex return the ECDSA public key in hex string format of the account. +func (w *Wallet) PublicKeyHex(account accounts.Account) (string, error) { + publicKeyBytes, err := w.PublicKeyBytes(account) + if err != nil { + return "", err + } + + return hexutil.Encode(publicKeyBytes)[4:], nil +} + +// Address returns the address of the account. +func (w *Wallet) Address(account accounts.Account) (common.Address, error) { + publicKey, err := w.PublicKey(account) + if err != nil { + return common.Address{}, err + } + + return crypto.PubkeyToAddress(*publicKey), nil +} + +// AddressBytes returns the address in bytes format of the account. +func (w *Wallet) AddressBytes(account accounts.Account) ([]byte, error) { + address, err := w.Address(account) + if err != nil { + return nil, err + } + return address.Bytes(), nil +} + +// AddressHex returns the address in hex string format of the account. +func (w *Wallet) AddressHex(account accounts.Account) (string, error) { + address, err := w.Address(account) + if err != nil { + return "", err + } + return address.Hex(), nil +} + +// Path return the derivation path of the account. +func (w *Wallet) Path(account accounts.Account) (string, error) { + return account.URL.Path, nil +} + +// SignData signs keccak256(data). The mimetype parameter describes the type of data being signed +func (w *Wallet) SignData(account accounts.Account, mimeType string, data []byte) ([]byte, error) { + // Make sure the requested account is contained within + if !w.Contains(account) { + return nil, accounts.ErrUnknownAccount + } + + return w.SignHash(account, crypto.Keccak256(data)) +} + +// SignDataWithPassphrase signs keccak256(data). The mimetype parameter describes the type of data being signed +func (w *Wallet) SignDataWithPassphrase(account accounts.Account, passphrase, mimeType string, data []byte) ([]byte, error) { + // Make sure the requested account is contained within + if !w.Contains(account) { + return nil, accounts.ErrUnknownAccount + } + + return w.SignHashWithPassphrase(account, passphrase, crypto.Keccak256(data)) +} + +// SignText requests the wallet to sign the hash of a given piece of data, prefixed +// the needed details via SignHashWithPassphrase, or by other means (e.g. unlock +// the account in a keystore). +func (w *Wallet) SignText(account accounts.Account, text []byte) ([]byte, error) { + // Make sure the requested account is contained within + if !w.Contains(account) { + return nil, accounts.ErrUnknownAccount + } + + return w.SignHash(account, accounts.TextHash(text)) +} + +// SignTextWithPassphrase implements accounts.Wallet, attempting to sign the +// given text (which is hashed) with the given account using passphrase as extra authentication. +func (w *Wallet) SignTextWithPassphrase(account accounts.Account, passphrase string, text []byte) ([]byte, error) { + // Make sure the requested account is contained within + if !w.Contains(account) { + return nil, accounts.ErrUnknownAccount + } + + return w.SignHashWithPassphrase(account, passphrase, accounts.TextHash(text)) +} + +// ParseDerivationPath parses the derivation path in string format into []uint32 +func ParseDerivationPath(path string) (accounts.DerivationPath, error) { + return accounts.ParseDerivationPath(path) +} + +// MustParseDerivationPath parses the derivation path in string format into +// []uint32 but will panic if it can't parse it. +func MustParseDerivationPath(path string) accounts.DerivationPath { + parsed, err := accounts.ParseDerivationPath(path) + if err != nil { + panic(err) + } + + return parsed +} + +// NewMnemonic returns a randomly generated BIP-39 mnemonic using 128-256 bits of entropy. +func NewMnemonic(bits int) (string, error) { + entropy, err := bip39.NewEntropy(bits) + if err != nil { + return "", err + } + return bip39.NewMnemonic(entropy) +} + +// NewMnemonicFromEntropy returns a BIP-39 mnemonic from entropy. +func NewMnemonicFromEntropy(entropy []byte) (string, error) { + return bip39.NewMnemonic(entropy) +} + +// NewEntropy returns a randomly generated entropy. +func NewEntropy(bits int) ([]byte, error) { + return bip39.NewEntropy(bits) +} + +// NewSeed returns a randomly generated BIP-39 seed. +func NewSeed() ([]byte, error) { + b := make([]byte, 64) + _, err := rand.Read(b) + return b, err +} + +// NewSeedFromMnemonic returns a BIP-39 seed based on a BIP-39 mnemonic. +func NewSeedFromMnemonic(mnemonic string, passOpt ...string) ([]byte, error) { + if mnemonic == "" { + return nil, errors.New("mnemonic is required") + } + + password := "" + if len(passOpt) > 0 { + password = passOpt[0] + } + + return bip39.NewSeedWithErrorChecking(mnemonic, password) +} + +// DerivePrivateKey derives the private key of the derivation path. +func (w *Wallet) derivePrivateKey(path accounts.DerivationPath) (*ecdsa.PrivateKey, error) { + var err error + key := w.masterKey + for _, n := range path { + if w.fixIssue172 && key.IsAffectedByIssue172() { + key, err = key.Derive(n) + } else { + key, err = key.DeriveNonStandard(n) + } + if err != nil { + return nil, err + } + } + + privateKey, err := key.ECPrivKey() + privateKeyECDSA := privateKey.ToECDSA() + if err != nil { + return nil, err + } + + return privateKeyECDSA, nil +} + +// DerivePublicKey derives the public key of the derivation path. +func (w *Wallet) derivePublicKey(path accounts.DerivationPath) (*ecdsa.PublicKey, error) { + privateKeyECDSA, err := w.derivePrivateKey(path) + if err != nil { + return nil, err + } + + publicKey := privateKeyECDSA.Public() + publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey) + if !ok { + return nil, errors.New("failed to get public key") + } + + return publicKeyECDSA, nil +} + +// DeriveAddress derives the account address of the derivation path. +func (w *Wallet) deriveAddress(path accounts.DerivationPath) (common.Address, error) { + publicKeyECDSA, err := w.derivePublicKey(path) + if err != nil { + return common.Address{}, err + } + + address := crypto.PubkeyToAddress(*publicKeyECDSA) + return address, nil +} + +// removeAtIndex removes an account at index. +func removeAtIndex(accts []accounts.Account, index int) []accounts.Account { + return append(accts[:index], accts[index+1:]...) +} diff --git a/backend/internal/pkg/logger/logger.go b/backend/internal/pkg/logger/logger.go new file mode 100644 index 0000000..135a991 --- /dev/null +++ b/backend/internal/pkg/logger/logger.go @@ -0,0 +1,88 @@ +package logger + +import ( + "io" + "log/slog" + "os" +) + +type LoggerBuilder struct { + local bool + addSource bool + lvl slog.Level + writers []io.Writer +} + +func (b *LoggerBuilder) WithWriter(w io.Writer) *LoggerBuilder { + b.writers = append(b.writers, w) + + return b +} + +func (b *LoggerBuilder) WithLevel(l slog.Level) *LoggerBuilder { + b.lvl = l + + return b +} + +func (b *LoggerBuilder) Local() *LoggerBuilder { + b.local = true + + return b +} + +func (b *LoggerBuilder) WithSource() *LoggerBuilder { + b.addSource = true + + return b +} + +func (b *LoggerBuilder) Build() *slog.Logger { + w := io.MultiWriter(b.writers...) + + if b.local { + opts := PrettyHandlerOptions{ + SlogOpts: &slog.HandlerOptions{ + Level: b.lvl, + AddSource: b.addSource, + }, + } + + handler := opts.NewPrettyHandler(w) + + return slog.New(handler) + } + + return slog.New( + slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ + Level: b.lvl, + AddSource: b.addSource, + }), + ) +} + +func newLogger(lvl slog.Level, w io.Writer) *slog.Logger { + return slog.New( + slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: lvl}), + ) +} + +func Err(err error) slog.Attr { + return slog.Attr{ + Key: "error", + Value: slog.StringValue(err.Error()), + } +} + +func MapLevel(lvl string) slog.Level { + switch lvl { + case "dev", "local", "debug": + return slog.LevelDebug + case "warn": + return slog.LevelWarn + case "error": + return slog.LevelError + default: + return slog.LevelInfo + } +} diff --git a/backend/internal/pkg/logger/slogdiscard.go b/backend/internal/pkg/logger/slogdiscard.go new file mode 100644 index 0000000..b96f44e --- /dev/null +++ b/backend/internal/pkg/logger/slogdiscard.go @@ -0,0 +1,32 @@ +package logger + +import ( + "context" + "log/slog" +) + +func NewDiscardLogger() *slog.Logger { + return slog.New(NewDiscardHandler()) +} + +type DiscardHandler struct{} + +func NewDiscardHandler() *DiscardHandler { + return &DiscardHandler{} +} + +func (h *DiscardHandler) Handle(_ context.Context, _ slog.Record) error { + return nil +} + +func (h *DiscardHandler) WithAttrs(_ []slog.Attr) slog.Handler { + return h +} + +func (h *DiscardHandler) WithGroup(_ string) slog.Handler { + return h +} + +func (h *DiscardHandler) Enabled(_ context.Context, _ slog.Level) bool { + return false +} diff --git a/backend/internal/pkg/logger/slogpretty.go b/backend/internal/pkg/logger/slogpretty.go new file mode 100644 index 0000000..6e95568 --- /dev/null +++ b/backend/internal/pkg/logger/slogpretty.go @@ -0,0 +1,97 @@ +package logger + +import ( + "context" + "encoding/json" + "io" + stdlog "log" + "log/slog" + + "github.com/fatih/color" +) + +type PrettyHandlerOptions struct { + SlogOpts *slog.HandlerOptions +} + +type PrettyHandler struct { + opts PrettyHandlerOptions + slog.Handler + l *stdlog.Logger + attrs []slog.Attr +} + +func (opts PrettyHandlerOptions) NewPrettyHandler( + out io.Writer, +) *PrettyHandler { + h := &PrettyHandler{ + Handler: slog.NewJSONHandler(out, opts.SlogOpts), + l: stdlog.New(out, "", 0), + } + + return h +} + +func (h *PrettyHandler) Handle(_ context.Context, r slog.Record) error { + level := r.Level.String() + ":" + + switch r.Level { + case slog.LevelDebug: + level = color.MagentaString(level) + case slog.LevelInfo: + level = color.BlueString(level) + case slog.LevelWarn: + level = color.YellowString(level) + case slog.LevelError: + level = color.RedString(level) + } + + fields := make(map[string]interface{}, r.NumAttrs()) + + r.Attrs(func(a slog.Attr) bool { + fields[a.Key] = a.Value.Any() + + return true + }) + + for _, a := range h.attrs { + fields[a.Key] = a.Value.Any() + } + + var b []byte + var err error + + if len(fields) > 0 { + b, err = json.MarshalIndent(fields, "", " ") + if err != nil { + return err + } + } + + timeStr := r.Time.Format("[15:05:05.000]") + msg := color.CyanString(r.Message) + + h.l.Println( + timeStr, + level, + msg, + color.WhiteString(string(b)), + ) + + return nil +} + +func (h *PrettyHandler) WithAttrs(attrs []slog.Attr) slog.Handler { + return &PrettyHandler{ + Handler: h.Handler, + l: h.l, + attrs: attrs, + } +} + +func (h *PrettyHandler) WithGroup(name string) slog.Handler { + return &PrettyHandler{ + Handler: h.Handler.WithGroup(name), + l: h.l, + } +} diff --git a/backend/internal/pkg/metrics/metrics.go b/backend/internal/pkg/metrics/metrics.go new file mode 100644 index 0000000..9118a8b --- /dev/null +++ b/backend/internal/pkg/metrics/metrics.go @@ -0,0 +1,43 @@ +package metrics + +import ( + "github.com/go-chi/chi/v5" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/collectors" + "github.com/prometheus/client_golang/prometheus/promhttp" +) + +var ( + RequestsAccepted = prometheus.NewCounter(prometheus.CounterOpts{ + Name: "http_request_count", // metric name + Help: "Total requests counts", + }) + + RequestDurations = prometheus.NewHistogram(prometheus.HistogramOpts{ + Name: "http_request_duration_seconds", + Help: "A histogram of the HTTP request durations in seconds.", + Buckets: prometheus.ExponentialBuckets(0.1, 1.5, 5), + }) +) + +func Initialize(router *chi.Mux) { + reg := prometheus.NewRegistry() + + // Add Go module build info. + reg.MustRegister( + collectors.NewBuildInfoCollector(), + collectors.NewGoCollector(), + RequestsAccepted, + RequestDurations, + ) + + go func() { + router.Handle("/metrics", promhttp.HandlerFor( + reg, + promhttp.HandlerOpts{ + // Opt into OpenMetrics to support exemplars. + EnableOpenMetrics: true, + }, + )) + }() +} diff --git a/backend/internal/pkg/models/organization.go b/backend/internal/pkg/models/organization.go new file mode 100644 index 0000000..dc2b744 --- /dev/null +++ b/backend/internal/pkg/models/organization.go @@ -0,0 +1,27 @@ +package models + +import ( + "encoding/json" + "time" + + "github.com/google/uuid" +) + +type Organizations []*Organization + +func (i *Organizations) MarshalBinary() ([]byte, error) { + return json.Marshal(i) +} + +type Organization struct { + ID uuid.UUID `json:"id"` + Name string `json:"name"` + Address string `json:"addess"` + WalletSeed []byte `json:"wallet_seed"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +func (i *Organization) MarshalBinary() ([]byte, error) { + return json.Marshal(i) +} diff --git a/backend/internal/pkg/models/tx.go b/backend/internal/pkg/models/tx.go new file mode 100644 index 0000000..aa081b5 --- /dev/null +++ b/backend/internal/pkg/models/tx.go @@ -0,0 +1,40 @@ +package models + +import ( + "time" + + "github.com/google/uuid" +) + +type Transaction struct { + Id uuid.UUID + + Description string + OrganizationId uuid.UUID + CreatedBy *OrganizationUser + Amount float64 + + ToAddr []byte + + MaxFeeAllowed float64 + Deadline time.Time + + Status int + + CreatedAt time.Time + UpdatedAt time.Time + + ConfirmedAt time.Time + CancelledAt time.Time + + CommitedAt time.Time +} + +type TransactionConfirmation struct { + TxId uuid.UUID + User *User + OrganizationId uuid.UUID + CreatedAt time.Time + UpdatedAt time.Time + Confirmed bool +} diff --git a/backend/internal/pkg/models/user.go b/backend/internal/pkg/models/user.go new file mode 100644 index 0000000..234ea9b --- /dev/null +++ b/backend/internal/pkg/models/user.go @@ -0,0 +1,201 @@ +package models + +import ( + "time" + + "github.com/google/uuid" +) + +type UserIdentity interface { + Id() uuid.UUID + Seed() []byte +} + +type User struct { + ID uuid.UUID + + Name string + + Credentails *UserCredentials + + PK []byte + Bip39Seed []byte + Activated bool + CreatedAt time.Time + UpdatedAt time.Time + DeletedAt time.Time +} + +type UserCredentials struct { + Email string + Phone string + Telegram string +} + +func NewUser( + id uuid.UUID, + seed []byte, + activated bool, + createdAt time.Time, +) *User { + return &User{ + ID: id, + Bip39Seed: seed, + Activated: activated, + CreatedAt: createdAt, + } +} + +func (u *User) Id() uuid.UUID { + return u.ID +} + +func (u *User) Seed() []byte { + return u.Bip39Seed +} + +type OrganizationParticipantType int + +const ( + OrganizationParticipantTypeUser OrganizationParticipantType = iota + OrganizationParticipantTypeEmployee +) + +type OrganizationParticipant interface { + Id() uuid.UUID + ParticipantName() string + Type() OrganizationParticipantType + + GetUser() *OrganizationUser + GetEmployee() *Employee + + IsAdmin() bool + IsOwner() bool + Position() string + IsActive() bool + + CreatedDate() time.Time + UpdatedDate() time.Time + DeletedDate() time.Time +} + +type OrganizationUser struct { + User + + OrgPosition string + Admin bool + Owner bool + + Employee *Employee + + CreatedAt time.Time + UpdatedAt time.Time + DeletedAt time.Time +} + +func (u *OrganizationUser) ParticipantName() string { + return u.Name +} + +func (u *OrganizationUser) Type() OrganizationParticipantType { + return OrganizationParticipantTypeUser +} + +func (u *OrganizationUser) GetUser() *OrganizationUser { + return u +} + +func (u *OrganizationUser) GetEmployee() *Employee { + return u.Employee +} + +func (u *OrganizationUser) IsAdmin() bool { + return u.Admin +} + +func (u *OrganizationUser) IsOwner() bool { + return u.Owner +} + +func (u *OrganizationUser) Position() string { + return u.OrgPosition +} + +func (u *OrganizationUser) IsActive() bool { + return u.Activated +} + +func (u *OrganizationUser) CreatedDate() time.Time { + return u.CreatedAt +} + +func (u *OrganizationUser) UpdatedDate() time.Time { + return u.UpdatedAt +} + +func (u *OrganizationUser) DeletedDate() time.Time { + return u.DeletedAt +} + +type Employee struct { + ID uuid.UUID + EmployeeName string + UserID uuid.UUID + OrganizationId uuid.UUID + WalletAddress []byte + CreatedAt time.Time + UpdatedAt time.Time + DeletedAt time.Time +} + +func (u *Employee) Id() uuid.UUID { + return u.ID +} + +func (u *Employee) ParticipantName() string { + return u.EmployeeName +} + +func (u *Employee) UserId() uuid.UUID { + return u.UserID +} + +func (u *Employee) Type() OrganizationParticipantType { + return OrganizationParticipantTypeEmployee +} + +func (u *Employee) GetUser() *OrganizationUser { + return nil +} + +func (u *Employee) GetEmployee() *Employee { + return u +} + +func (u *Employee) IsAdmin() bool { + return false +} + +func (u *Employee) IsOwner() bool { + return false +} + +func (u *Employee) Position() string { + return "" // todo +} + +func (u *Employee) IsActive() bool { + return u.DeletedAt.IsZero() +} + +func (u *Employee) CreatedDate() time.Time { + return u.CreatedAt +} + +func (u *Employee) UpdatedDate() time.Time { + return u.UpdatedAt +} + +func (u *Employee) DeletedDate() time.Time { + return u.DeletedAt +} diff --git a/backend/internal/pkg/sqlutils/tx.go b/backend/internal/pkg/sqlutils/tx.go new file mode 100644 index 0000000..fbbee69 --- /dev/null +++ b/backend/internal/pkg/sqlutils/tx.go @@ -0,0 +1,85 @@ +package sqltools + +import ( + "context" + "database/sql" + "errors" + "fmt" +) + +type DBTX interface { + Query(query string, args ...any) (*sql.Rows, error) + QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error) + QueryRow(query string, args ...any) *sql.Row + QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row + Exec(query string, args ...any) (sql.Result, error) + ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error) +} + +type TransactionalStorage interface { + Transaction(ctx context.Context, db *sql.DB, f func(ctx context.Context) error) error + Conn(ctx context.Context) DBTX +} + +type txCtxKey struct{} + +var TxCtxKey = txCtxKey{} + +func Transaction(ctx context.Context, db *sql.DB, fn func(context.Context) error) (err error) { + var tx *sql.Tx = new(sql.Tx) + + hasExternalTx := hasExternalTransaction(ctx) + + defer func() { + if hasExternalTx { + if err != nil { + err = fmt.Errorf("error perform operation. %w", err) + return + } + + return + } + + if err != nil { + if rbErr := tx.Rollback(); rbErr != nil { + err = errors.Join(fmt.Errorf("error rollback transaction. %w", rbErr), err) + return + } + + err = err + + return + } + + if commitErr := tx.Commit(); commitErr != nil { + if rbErr := tx.Rollback(); rbErr != nil { + err = errors.Join(fmt.Errorf("error rollback transaction. %w", rbErr), commitErr, err) + + return + } + + err = fmt.Errorf("error commit transaction. %w", err) + } + }() + + if !hasExternalTx { + tx, err = db.BeginTx(ctx, &sql.TxOptions{ + Isolation: sql.LevelRepeatableRead, + }) + if err != nil { + return fmt.Errorf("error begin transaction. %w", err) + } + + ctx = context.WithValue(ctx, TxCtxKey, tx) + } + + return fn(ctx) +} + +func hasExternalTransaction(ctx context.Context) bool { + if _, ok := ctx.Value(TxCtxKey).(*sql.Tx); ok { + return true + } + + return false +} diff --git a/backend/internal/service/service.go b/backend/internal/service/service.go new file mode 100644 index 0000000..e69c54b --- /dev/null +++ b/backend/internal/service/service.go @@ -0,0 +1,58 @@ +package service + +import ( + "context" + "fmt" + "log/slog" + + "github.com/emochka2007/block-accounting/internal/interface/rest" +) + +type Service interface { + Run(ctx context.Context) error + Stop() +} + +type ServiceImpl struct { + log *slog.Logger + rest *rest.Server +} + +func NewService( + log *slog.Logger, + rest *rest.Server, +) Service { + return &ServiceImpl{ + log: log, + rest: rest, + } +} + +func (s *ServiceImpl) Run(ctx context.Context) error { + s.log.Info("starting blockd service 0w0") + + errch := make(chan error) + + defer s.rest.Close() + + go func() { + defer func() { + close(errch) + }() + + errch <- s.rest.Serve(ctx) + }() + + select { + case <-ctx.Done(): + s.log.Info("shutting down service") + + return nil + case err := <-errch: + return fmt.Errorf("error at service runtime. %w", err) + } +} + +func (s *ServiceImpl) Stop() { + s.log.Info(">w< bye bye! :3") +} diff --git a/backend/internal/usecase/interactors/chain/chain.go b/backend/internal/usecase/interactors/chain/chain.go new file mode 100644 index 0000000..d5a4798 --- /dev/null +++ b/backend/internal/usecase/interactors/chain/chain.go @@ -0,0 +1,24 @@ +package chain + +import ( + "context" + + "github.com/emochka2007/block-accounting/internal/usecase/interactors/users" + "github.com/emochka2007/block-accounting/internal/usecase/repository/transactions" +) + +type ChainInteractor interface { +} + +type chainInteractor struct { + txRepository transactions.Repository + usersInteractor users.UsersInteractor +} + +type NewMultisigParams struct { + OwnersPKs []string +} + +func (i *chainInteractor) NewMultisig(ctx context.Context) { + +} diff --git a/backend/internal/usecase/interactors/jwt/jwt.go b/backend/internal/usecase/interactors/jwt/jwt.go new file mode 100644 index 0000000..569dca3 --- /dev/null +++ b/backend/internal/usecase/interactors/jwt/jwt.go @@ -0,0 +1,286 @@ +package jwt + +import ( + "context" + "crypto/sha512" + "encoding/base64" + "errors" + "fmt" + "time" + + "github.com/emochka2007/block-accounting/internal/pkg/models" + "github.com/emochka2007/block-accounting/internal/usecase/interactors/users" + "github.com/emochka2007/block-accounting/internal/usecase/repository/auth" + "github.com/golang-jwt/jwt/v5" + "github.com/google/uuid" +) + +var ( + ErrorInvalidTokenClaims = errors.New("invalid token claims") + ErrorTokenExpired = errors.New("token expired") +) + +type JWTInteractor interface { + NewToken(user models.UserIdentity, duration time.Duration) (AccessToken, error) + User(token string) (*models.User, error) + RefreshToken(ctx context.Context, token string, rToken string) (AccessToken, error) +} + +type jwtInteractor struct { + secret []byte + usersInteractor users.UsersInteractor + authRepository auth.Repository +} + +func NewJWT( + secret []byte, + usersInteractor users.UsersInteractor, + authRepository auth.Repository, +) JWTInteractor { + return &jwtInteractor{ + secret: secret, + usersInteractor: usersInteractor, + authRepository: authRepository, + } +} + +type AccessToken struct { + Token string + ExpiredAt time.Time + + RefreshToken string + RTExpiredAt time.Time +} + +// NewToken creates new JWT token for given user +func (w *jwtInteractor) NewToken(user models.UserIdentity, duration time.Duration) (AccessToken, error) { + tokens, err := w.newTokens(user.Id(), duration) + if err != nil { + return AccessToken{}, fmt.Errorf("error create new tokens. %w", err) + } + + ctx, cancel := context.WithTimeout(context.TODO(), 2*time.Second) + defer cancel() + + if err := w.authRepository.AddToken(ctx, auth.AddTokenParams{ + UserId: user.Id(), + Token: tokens.Token, + TokenExpiredAt: tokens.ExpiredAt, + RefreshToken: tokens.RefreshToken, + RefreshTokenExpiredAt: tokens.RTExpiredAt, + CreatedAt: time.Now(), + }); err != nil { + return AccessToken{}, fmt.Errorf("error save tokens into repository. %w", err) + } + + return tokens, nil +} + +func (w *jwtInteractor) User(tokenStr string) (*models.User, error) { + claims := make(jwt.MapClaims) + + _, err := jwt.ParseWithClaims(tokenStr, claims, func(t *jwt.Token) (interface{}, error) { + return w.secret, nil + }) + if err != nil { + return nil, errors.Join(fmt.Errorf("error parse jwt token. %w", err), ErrorInvalidTokenClaims) + } + + if expDate, ok := claims["exp"].(float64); ok { + if time.UnixMilli(int64(expDate)).Before(time.Now()) { + return nil, fmt.Errorf("error token expired. %w", ErrorTokenExpired) + } + } else { + return nil, errors.Join(fmt.Errorf("error parse exp date. %w", err), ErrorInvalidTokenClaims) + } + + var userIdString string + var ok bool + + if userIdString, ok = claims["uid"].(string); !ok { + return nil, ErrorInvalidTokenClaims + } + + userId, err := uuid.Parse(userIdString) + if err != nil { + return nil, errors.Join(fmt.Errorf("error parse user id. %w", err), ErrorInvalidTokenClaims) + } + + ctx, cancel := context.WithTimeout(context.TODO(), 2*time.Second) + defer cancel() + + tokens, err := w.authRepository.GetTokens(ctx, auth.GetTokenParams{ + UserId: userId, + Token: tokenStr, + }) + if err != nil { + return nil, fmt.Errorf("error fetch token from repository. %w", err) + } + + if tokens.TokenExpiredAt.Before(time.Now()) { + return nil, fmt.Errorf("error token expired. %w", ErrorTokenExpired) + } + + if tokens.UserId != userId { + return nil, errors.Join(fmt.Errorf("error invalid user id. %w", err), ErrorInvalidTokenClaims) + } + + users, err := w.usersInteractor.Get(ctx, users.GetParams{ + Ids: uuid.UUIDs{tokens.UserId}, + }) + if err != nil || len(users) == 0 { + return nil, fmt.Errorf("error fetch user from repository. %w", err) + } + + return users[0], nil +} + +func (w *jwtInteractor) RefreshToken(ctx context.Context, token string, rToken string) (AccessToken, error) { + claims := make(jwt.MapClaims) + + _, err := jwt.ParseWithClaims(token, claims, func(t *jwt.Token) (interface{}, error) { + return w.secret, nil + }) + if err != nil { + return AccessToken{}, errors.Join(fmt.Errorf("error parse jwt token. %w", err), ErrorInvalidTokenClaims) + } + + var userIdString string + var ok bool + + if userIdString, ok = claims["uid"].(string); !ok { + return AccessToken{}, ErrorInvalidTokenClaims + } + + userId, err := uuid.Parse(userIdString) + if err != nil { + return AccessToken{}, errors.Join(fmt.Errorf("error parse user id. %w", err), ErrorInvalidTokenClaims) + } + + _, err = jwt.ParseWithClaims(rToken, claims, func(t *jwt.Token) (interface{}, error) { + return w.secret, nil + }) + if err != nil { + return AccessToken{}, errors.Join(fmt.Errorf("error parse refresh jwt token. %w", err), ErrorInvalidTokenClaims) + } + + if expDate, ok := claims["exp"].(float64); ok { + if time.UnixMilli(int64(expDate)).Before(time.Now()) { + return AccessToken{}, fmt.Errorf("error refresh token expired. %w", ErrorTokenExpired) + } + } else { + return AccessToken{}, errors.Join(fmt.Errorf("error parse exp date. %w", err), ErrorInvalidTokenClaims) + } + + if userIdString, ok = claims["uid"].(string); !ok { + return AccessToken{}, ErrorInvalidTokenClaims + } + + rTokenUserId, err := uuid.Parse(userIdString) + if err != nil { + return AccessToken{}, errors.Join( + fmt.Errorf("error parse user id from refresh token. %w", err), + ErrorInvalidTokenClaims, + ) + } + + if userId != rTokenUserId { + return AccessToken{}, fmt.Errorf("error user ids corrupted. %w", ErrorInvalidTokenClaims) + } + + ctx, cancel := context.WithTimeout(context.TODO(), 2*time.Second) + defer cancel() + + tokens, err := w.authRepository.GetTokens(ctx, auth.GetTokenParams{ + UserId: userId, + Token: token, + RefreshToken: rToken, + }) + if err != nil { + return AccessToken{}, fmt.Errorf("error fetch token from repository. %w", err) + } + + if tokens.RefreshTokenExpiredAt.Before(time.Now()) { + return AccessToken{}, fmt.Errorf("error token expired. %w", ErrorTokenExpired) + } + + rtHash := sha512.New() + rtHash.Write([]byte(tokens.Token)) + + rtHashStringValid := base64.StdEncoding.EncodeToString(rtHash.Sum(nil)) + + rtHashRaw, ok := claims["rt_hash"] + if !ok { + return AccessToken{}, fmt.Errorf("error refresh token claims corrupted. %w", ErrorInvalidTokenClaims) + } + + rtHashString, ok := rtHashRaw.(string) + if !ok { + return AccessToken{}, fmt.Errorf("error refresh token claims corrupted. %w", ErrorInvalidTokenClaims) + } + + if rtHashString != rtHashStringValid { + return AccessToken{}, fmt.Errorf("error refresh token hash corrupted. %w", ErrorInvalidTokenClaims) + } + + newTokens, err := w.newTokens(userId, 24*time.Hour) + if err != nil { + return AccessToken{}, fmt.Errorf("error create new tokens. %w", err) + } + + if err = w.authRepository.RefreshToken(ctx, auth.RefreshTokenParams{ + UserId: userId, + OldToken: token, + Token: newTokens.Token, + TokenExpiredAt: newTokens.ExpiredAt, + OldRefreshToken: rToken, + RefreshToken: newTokens.RefreshToken, + RefreshTokenExpiredAt: newTokens.RTExpiredAt, + }); err != nil { + return AccessToken{}, fmt.Errorf("error update tokens. %w", err) + } + + return newTokens, nil +} + +func (w *jwtInteractor) newTokens(userId uuid.UUID, duration time.Duration) (AccessToken, error) { + token := jwt.New(jwt.SigningMethodHS256) + + expAt := time.Now().Add(duration) + + claims := token.Claims.(jwt.MapClaims) + claims["uid"] = userId.String() + claims["exp"] = expAt.UnixMilli() + + secret := w.secret + + tokenString, err := token.SignedString([]byte(secret)) + if err != nil { + return AccessToken{}, fmt.Errorf("error sign token. %w", err) + } + + refreshToken := jwt.New(jwt.SigningMethodHS256) + + rtHash := sha512.New() + + rtHash.Write([]byte(tokenString)) + + rtExpAt := expAt.Add(time.Hour * 24 * 5) + + claims = refreshToken.Claims.(jwt.MapClaims) + claims["uid"] = userId.String() + claims["exp"] = rtExpAt.UnixMilli() + claims["rt_hash"] = base64.StdEncoding.EncodeToString(rtHash.Sum(nil)) + + rtokenString, err := refreshToken.SignedString([]byte(secret)) + if err != nil { + return AccessToken{}, fmt.Errorf("error sign refresh token. %w", err) + } + + return AccessToken{ + Token: tokenString, + ExpiredAt: expAt, + RefreshToken: rtokenString, + RTExpiredAt: rtExpAt, + }, nil +} diff --git a/backend/internal/usecase/interactors/organizations/interactor.go b/backend/internal/usecase/interactors/organizations/interactor.go new file mode 100644 index 0000000..75267e6 --- /dev/null +++ b/backend/internal/usecase/interactors/organizations/interactor.go @@ -0,0 +1,310 @@ +package organizations + +import ( + "context" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "log/slog" + "time" + + "github.com/emochka2007/block-accounting/internal/pkg/ctxmeta" + "github.com/emochka2007/block-accounting/internal/pkg/hdwallet" + "github.com/emochka2007/block-accounting/internal/pkg/logger" + "github.com/emochka2007/block-accounting/internal/pkg/models" + "github.com/emochka2007/block-accounting/internal/usecase/repository/cache" + "github.com/emochka2007/block-accounting/internal/usecase/repository/organizations" + "github.com/google/uuid" +) + +var ( + ErrorUnauthorizedAccess = errors.New("unauthorized access") +) + +type CreateParams struct { + Name string + Address string + WalletMnemonic string +} + +type ListParams struct { + Ids uuid.UUIDs + UserId uuid.UUID + + Cursor string + Limit uint8 // Max limit is 50 (may change) +} + +type ParticipantParams struct { + ID uuid.UUID + OrganizationID uuid.UUID + + UsersOnly bool + ActiveOnly bool + EmployeesOnly bool +} + +type ParticipantsParams struct { + IDs uuid.UUIDs + OrganizationID uuid.UUID + + UsersOnly bool + ActiveOnly bool + EmployeesOnly bool +} + +type OrganizationsInteractor interface { + Create( + ctx context.Context, + params CreateParams, + ) (*models.Organization, error) + List( + ctx context.Context, + params ListParams, + ) (*ListResponse, error) + Participant( + ctx context.Context, + params ParticipantParams, + ) (models.OrganizationParticipant, error) + Participants( + ctx context.Context, + params ParticipantsParams, + ) ([]models.OrganizationParticipant, error) +} + +type organizationsInteractor struct { + log *slog.Logger + orgRepository organizations.Repository + cache cache.Cache +} + +func NewOrganizationsInteractor( + log *slog.Logger, + orgRepository organizations.Repository, + cache cache.Cache, +) OrganizationsInteractor { + return &organizationsInteractor{ + log: log, + orgRepository: orgRepository, + cache: cache, + } +} + +type organizationsListCursor struct { + Id uuid.UUID `json:"id"` +} + +func newOrganizationsListCursor(id ...uuid.UUID) *organizationsListCursor { + if len(id) > 0 { + return &organizationsListCursor{id[0]} + } + + return new(organizationsListCursor) +} + +func (c *organizationsListCursor) encode() (string, error) { + data, err := json.Marshal(c) + if err != nil { + return "", fmt.Errorf("couldn't marshal reaction id. %w", err) + } + return base64.StdEncoding.EncodeToString(data), nil + +} + +func (c *organizationsListCursor) decode(s string) error { + if c == nil { + return nil + } + + token, err := base64.StdEncoding.DecodeString(s) + if err != nil { + return fmt.Errorf("error decode token. %w", err) + } + + return json.Unmarshal(token, c) +} + +type ListResponse struct { + Organizations models.Organizations + NextCursor string +} + +func (i ListResponse) MarshalBinary() ([]byte, error) { + return json.Marshal(i) +} + +func (i *organizationsInteractor) List( + ctx context.Context, + params ListParams, +) (*ListResponse, error) { + user, err := ctxmeta.User(ctx) + if err != nil { + return nil, fmt.Errorf("error fetch user from context. %w", err) + } + + if params.UserId != uuid.Nil { + if params.UserId != user.Id() { + return nil, fmt.Errorf("error unauthorized organizations list access. %w", ErrorUnauthorizedAccess) + } + } else { + params.UserId = user.Id() + } + + out := new(ListResponse) + + // BUG: empty org set fetched from cache + // if err := i.cache.Get(ctx, params, out); err != nil && errors.Is(err, redis.Nil) { + // i.log.Error("no cache hit!", logger.Err(err)) + // } else { + // i.log.Debug("cache hit!", slog.AnyValue(out)) + // return out, nil + // } + + if params.Limit <= 0 || params.Limit > 50 { + params.Limit = 50 + } + + cursor := newOrganizationsListCursor() + + if params.Cursor != "" { + if err := cursor.decode(params.Cursor); err != nil { + return nil, fmt.Errorf("error decode cursor value. %w", err) // maybe just log error? + } + } + + i.log.Debug( + "organizations_list", + slog.String("cursor", params.Cursor), + slog.Int("limit", int(params.Limit)), + slog.Any("cursor-id", cursor.Id), + slog.Any("ids", params.Ids), + slog.Any("user_id", params.UserId), + ) + + orgs, err := i.orgRepository.Get(ctx, organizations.GetParams{ + UserId: params.UserId, + Ids: params.Ids, + Limit: int64(params.Limit), + CursorId: cursor.Id, + }) + if err != nil { + return nil, fmt.Errorf("error fetch organizations. %w", err) + } + + var nextCursor string + + if len(orgs) >= 50 || len(orgs) >= int(params.Limit) { + cursor.Id = orgs[len(orgs)-1].ID + if nextCursor, err = cursor.encode(); err != nil { + return nil, fmt.Errorf("error encode next page token. %w", err) // maybe just log error? + } + } + + out = &ListResponse{ + Organizations: orgs, + NextCursor: nextCursor, + } + + if err = i.cache.Cache(ctx, params, *out, time.Hour*1); err != nil { + i.log.Error("error add cache record", logger.Err(err)) + } + + return out, nil +} + +func (i *organizationsInteractor) Create( + ctx context.Context, + params CreateParams, +) (*models.Organization, error) { + var walletSeed []byte + + user, err := ctxmeta.User(ctx) + if err != nil { + return nil, fmt.Errorf("error fetch user from context. %w", err) + } + + if params.WalletMnemonic == "" { + walletSeed = user.Seed() + } else { + seed, err := hdwallet.NewSeedFromMnemonic(params.WalletMnemonic) + if err != nil { + return nil, fmt.Errorf("error convert organization wallet mnemonic into a seed. %w", err) + } + + walletSeed = seed + } + + org := models.Organization{ + ID: uuid.Must(uuid.NewV7()), + Name: params.Name, + Address: params.Address, + WalletSeed: walletSeed, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + if err := i.orgRepository.CreateAndAdd(ctx, org, user); err != nil { + return nil, fmt.Errorf("error create new organization. %w", err) + } + + return &org, nil +} + +func (i *organizationsInteractor) Participant( + ctx context.Context, + params ParticipantParams, +) (models.OrganizationParticipant, error) { + participants, err := i.Participants(ctx, ParticipantsParams{ + IDs: uuid.UUIDs{params.ID}, + OrganizationID: params.OrganizationID, + ActiveOnly: params.ActiveOnly, + UsersOnly: params.UsersOnly, + EmployeesOnly: params.EmployeesOnly, + }) + if err != nil { + return nil, fmt.Errorf("error fetch organization participant. %w", err) + } + + if len(participants) == 0 { + return nil, fmt.Errorf("error organization participant empty. %w", err) + } + + return participants[0], nil +} + +func (i *organizationsInteractor) Participants( + ctx context.Context, + params ParticipantsParams, +) ([]models.OrganizationParticipant, error) { + user, err := ctxmeta.User(ctx) + if err != nil { + return nil, fmt.Errorf("error fetch user from context. %w", err) + } + + _, err = i.orgRepository.Participants(ctx, organizations.ParticipantsParams{ + Ids: uuid.UUIDs{user.Id()}, + OrganizationId: params.OrganizationID, + ActiveOnly: params.ActiveOnly, + UsersOnly: true, + }) + if err != nil { + return nil, errors.Join( + fmt.Errorf("error fetch organization user. %w", err), + ErrorUnauthorizedAccess, + ) + } + + participants, err := i.orgRepository.Participants(ctx, organizations.ParticipantsParams{ + Ids: params.IDs, + OrganizationId: params.OrganizationID, + UsersOnly: params.UsersOnly, + EmployeesOnly: params.EmployeesOnly, + ActiveOnly: params.ActiveOnly, + }) + if err != nil { + return nil, fmt.Errorf("error fetch organization participants. %w", err) + } + + return participants, nil +} diff --git a/backend/internal/usecase/interactors/transactions/interactor.go b/backend/internal/usecase/interactors/transactions/interactor.go new file mode 100644 index 0000000..9ff734c --- /dev/null +++ b/backend/internal/usecase/interactors/transactions/interactor.go @@ -0,0 +1,285 @@ +package transactions + +import ( + "context" + "encoding/base64" + "encoding/json" + "fmt" + "log/slog" + "time" + + "github.com/emochka2007/block-accounting/internal/pkg/ctxmeta" + "github.com/emochka2007/block-accounting/internal/pkg/models" + "github.com/emochka2007/block-accounting/internal/usecase/interactors/organizations" + "github.com/emochka2007/block-accounting/internal/usecase/repository/transactions" + "github.com/google/uuid" +) + +type ListParams struct { + IDs uuid.UUIDs + OrganizationID uuid.UUID + CreatedBy uuid.UUID + + To []byte + + Limit int64 + Cursor string + + WithPending bool + + WithCancelled bool + WithConfirmed bool + WithCommited bool + WithExpired bool + + WithConfirmations bool +} + +type CreateParams struct { + Tx models.Transaction + OrganizationId uuid.UUID +} + +type ConfirmParams struct { + TxID uuid.UUID + OrganizationID uuid.UUID +} + +type CancelParams struct { + TxID uuid.UUID + OrganizationID uuid.UUID + Cause string +} + +type TransactionsInteractor interface { + List(ctx context.Context, params ListParams) (*ListResult, error) + Create(ctx context.Context, params CreateParams) (*models.Transaction, error) + Confirm(ctx context.Context, params ConfirmParams) (*models.Transaction, error) + Cancel(ctx context.Context, params CancelParams) (*models.Transaction, error) +} + +type transactionsInteractor struct { + log *slog.Logger + txRepo transactions.Repository + orgInteractor organizations.OrganizationsInteractor +} + +func NewTransactionsInteractor( + log *slog.Logger, + txRepo transactions.Repository, + orgInteractor organizations.OrganizationsInteractor, +) TransactionsInteractor { + return &transactionsInteractor{ + log: log, + txRepo: txRepo, + orgInteractor: orgInteractor, + } +} + +type txsListCursor struct { + Id uuid.UUID `json:"id"` +} + +func newTxsListCursor(id ...uuid.UUID) *txsListCursor { + if len(id) > 0 { + return &txsListCursor{id[0]} + } + + return new(txsListCursor) +} + +func (c *txsListCursor) encode() (string, error) { + data, err := json.Marshal(c) + if err != nil { + return "", fmt.Errorf("couldn't marshal reaction id. %w", err) + } + return base64.StdEncoding.EncodeToString(data), nil + +} + +func (c *txsListCursor) decode(s string) error { + if c == nil { + return nil + } + + token, err := base64.StdEncoding.DecodeString(s) + if err != nil { + return fmt.Errorf("error decode token. %w", err) + } + + return json.Unmarshal(token, c) +} + +type ListResult struct { + Txs []*models.Transaction + NextCursor string +} + +func (i *transactionsInteractor) List(ctx context.Context, params ListParams) (*ListResult, error) { + if params.Limit == 0 { + params.Limit = 50 + } + + cursor := newTxsListCursor() + + if params.Cursor != "" { + if err := cursor.decode(params.Cursor); err != nil { + return nil, fmt.Errorf("error decode cursor value. %w", err) // maybe just log error? + } + } + + txs, err := i.txRepo.GetTransactions(ctx, transactions.GetTransactionsParams{ + Ids: params.IDs, + OrganizationId: params.OrganizationID, + CreatedById: params.CreatedBy, + To: params.To, + Limit: params.Limit, + CursorId: cursor.Id, + WithCancelled: params.WithCancelled, + WithConfirmed: params.WithConfirmed, + WithCommited: params.WithCommited, + WithExpired: params.WithExpired, + WithPending: params.WithPending, + }) + if err != nil { + return nil, fmt.Errorf("error fetch transaction from repository. %w", err) + } + + var nextCursor string + + if len(txs) >= 50 || len(txs) >= int(params.Limit) { + cursor.Id = txs[len(txs)-1].Id + if nextCursor, err = cursor.encode(); err != nil { + return nil, fmt.Errorf("error encode next page token. %w", err) // maybe just log error? + } + } + + return &ListResult{ + Txs: txs, + NextCursor: nextCursor, + }, nil +} + +func (i *transactionsInteractor) Create( + ctx context.Context, + params CreateParams, +) (*models.Transaction, error) { + user, err := ctxmeta.User(ctx) + if err != nil { + return nil, fmt.Errorf("error fetch user from context. %w", err) + } + + tx := params.Tx + + if tx.Id == uuid.Nil { + tx.Id = uuid.Must(uuid.NewV7()) + } + + participant, err := i.orgInteractor.Participant(ctx, organizations.ParticipantParams{ + ID: user.Id(), + OrganizationID: params.OrganizationId, + ActiveOnly: true, + UsersOnly: true, + }) + if err != nil { + return nil, fmt.Errorf("error fetch actor prticipant. %w", err) + } + + tx.CreatedBy = participant.GetUser() + tx.CreatedAt = time.Now() + tx.UpdatedAt = tx.CreatedAt + + if err = i.txRepo.CreateTransaction(ctx, tx); err != nil { + return nil, fmt.Errorf("error create new tx. %w", err) + } + + return &tx, nil +} + +func (i *transactionsInteractor) Confirm(ctx context.Context, params ConfirmParams) (*models.Transaction, error) { + user, err := ctxmeta.User(ctx) + if err != nil { + return nil, fmt.Errorf("error fetch user from context. %w", err) + } + + participant, err := i.orgInteractor.Participant(ctx, organizations.ParticipantParams{ + ID: user.Id(), + OrganizationID: params.OrganizationID, + ActiveOnly: true, + UsersOnly: true, + }) + if err != nil { + return nil, fmt.Errorf("error fetch actor prticipant. %w", err) + } + + if !participant.IsAdmin() { + return nil, fmt.Errorf("error not enouth rights. %w", organizations.ErrorUnauthorizedAccess) + } + + if err := i.txRepo.ConfirmTransaction(ctx, transactions.ConfirmTransactionParams{ + TxId: params.TxID, + OrganizationId: params.OrganizationID, + UserId: participant.Id(), + }); err != nil { + return nil, fmt.Errorf("error confirm transaction. %w", err) + } + + tx, err := i.txRepo.GetTransactions(ctx, transactions.GetTransactionsParams{ + Ids: uuid.UUIDs{params.TxID}, + OrganizationId: params.OrganizationID, + Limit: 1, + }) + if err != nil { + return nil, fmt.Errorf("error fetch transaction. %w", err) + } + + if len(tx) == 0 { + return nil, fmt.Errorf("error tx not found") + } + + return tx[0], nil +} + +func (i *transactionsInteractor) Cancel(ctx context.Context, params CancelParams) (*models.Transaction, error) { + user, err := ctxmeta.User(ctx) + if err != nil { + return nil, fmt.Errorf("error fetch user from context. %w", err) + } + + participant, err := i.orgInteractor.Participant(ctx, organizations.ParticipantParams{ + ID: user.Id(), + OrganizationID: params.OrganizationID, + ActiveOnly: true, + UsersOnly: true, + }) + if err != nil { + return nil, fmt.Errorf("error fetch actor prticipant. %w", err) + } + + if !participant.IsAdmin() { + return nil, fmt.Errorf("error not enouth rights. %w", organizations.ErrorUnauthorizedAccess) + } + + if err := i.txRepo.CancelTransaction(ctx, transactions.CancelTransactionParams{ + TxId: params.TxID, + OrganizationId: params.OrganizationID, + UserId: participant.Id(), + }); err != nil { + return nil, fmt.Errorf("error confirm transaction. %w", err) + } + + tx, err := i.txRepo.GetTransactions(ctx, transactions.GetTransactionsParams{ + Ids: uuid.UUIDs{params.TxID}, + OrganizationId: params.OrganizationID, + Limit: 1, + }) + if err != nil { + return nil, fmt.Errorf("error fetch transaction. %w", err) + } + + if len(tx) == 0 { + return nil, fmt.Errorf("error tx not found") + } + + return tx[0], nil +} diff --git a/backend/internal/usecase/interactors/users/interactor.go b/backend/internal/usecase/interactors/users/interactor.go new file mode 100644 index 0000000..5c3e1bc --- /dev/null +++ b/backend/internal/usecase/interactors/users/interactor.go @@ -0,0 +1,124 @@ +package users + +import ( + "context" + "errors" + "fmt" + "log/slog" + "time" + + "github.com/emochka2007/block-accounting/internal/pkg/hdwallet" + "github.com/emochka2007/block-accounting/internal/pkg/models" + "github.com/emochka2007/block-accounting/internal/usecase/repository/users" + "github.com/google/uuid" +) + +var ( + ErrorUsersNotFound = errors.New("users not found") +) + +type CreateParams struct { + Name string + Email string + Phone string + Tg string + Mnemonic string + Activate bool +} + +type GetParams struct { + Ids uuid.UUIDs + OrganizationId uuid.UUID + Mnemonic string + Seed []byte +} + +type DeleteParams struct { + Id uuid.UUID + OrganizationId uuid.UUID +} + +type ActivateParams struct { + Id uuid.UUID + OrganizationId uuid.UUID +} + +type UsersInteractor interface { + Create(ctx context.Context, params CreateParams) (*models.User, error) + Update(ctx context.Context, newState models.User) error + Activate(ctx context.Context, params ActivateParams) error + Get(ctx context.Context, params GetParams) ([]*models.User, error) + Delete(ctx context.Context, params DeleteParams) error +} + +type usersInteractor struct { + log *slog.Logger + usersRepo users.Repository +} + +func NewUsersInteractor( + log *slog.Logger, + usersRepo users.Repository, +) UsersInteractor { + return &usersInteractor{ + log: log, + usersRepo: usersRepo, + } +} + +func (i *usersInteractor) Create(ctx context.Context, params CreateParams) (*models.User, error) { + seed, err := hdwallet.NewSeedFromMnemonic(params.Mnemonic) + if err != nil { + return nil, fmt.Errorf("error convert mnemonic into a seed. %w", err) + } + + user := models.NewUser( + uuid.Must(uuid.NewV7()), + seed, + params.Activate, + time.Now(), + ) + + user.Name = params.Name + + user.Credentails = &models.UserCredentials{ + Email: params.Email, + Phone: params.Phone, + Telegram: params.Tg, + } + + if err = i.usersRepo.Create(ctx, user); err != nil { + return nil, fmt.Errorf("error create new user. %w", err) + } + + return user, nil +} + +func (i *usersInteractor) Update(ctx context.Context, newState models.User) error { + return nil +} + +func (i *usersInteractor) Activate(ctx context.Context, params ActivateParams) error { + return nil +} + +func (i *usersInteractor) Get(ctx context.Context, params GetParams) ([]*models.User, error) { + users, err := i.usersRepo.Get(ctx, users.GetParams{ + Ids: params.Ids, + OrganizationId: params.OrganizationId, + Seed: params.Seed, + }) + if err != nil { + return nil, fmt.Errorf("error fetch users from repository. %w", err) + } + + if len(users) == 0 { + return nil, fmt.Errorf("error empty users set. %w", ErrorUsersNotFound) + } + + return users, nil +} + +func (i *usersInteractor) Delete(ctx context.Context, params DeleteParams) error { + return nil +} diff --git a/backend/internal/usecase/repository/auth/repository.go b/backend/internal/usecase/repository/auth/repository.go new file mode 100644 index 0000000..c5ebe6d --- /dev/null +++ b/backend/internal/usecase/repository/auth/repository.go @@ -0,0 +1,191 @@ +package auth + +import ( + "context" + "database/sql" + "errors" + "fmt" + "time" + + sq "github.com/Masterminds/squirrel" + sqltools "github.com/emochka2007/block-accounting/internal/pkg/sqlutils" + "github.com/google/uuid" +) + +type AddTokenParams struct { + UserId uuid.UUID + + Token string + TokenExpiredAt time.Time + + RefreshToken string + RefreshTokenExpiredAt time.Time + + CreatedAt time.Time +} + +type GetTokenParams struct { + UserId uuid.UUID + Token string + RefreshToken string +} + +type RefreshTokenParams struct { + UserId uuid.UUID + + OldToken string + Token string + TokenExpiredAt time.Time + + OldRefreshToken string + RefreshToken string + RefreshTokenExpiredAt time.Time +} + +type AccessToken struct { + UserId uuid.UUID + + Token string + TokenExpiredAt time.Time + + RefreshToken string + RefreshTokenExpiredAt time.Time + + CreatedAt time.Time +} + +type Repository interface { + AddToken(ctx context.Context, params AddTokenParams) error + GetTokens(ctx context.Context, params GetTokenParams) (*AccessToken, error) + RefreshToken(ctx context.Context, params RefreshTokenParams) error +} + +type repositorySQL struct { + db *sql.DB +} + +func (r *repositorySQL) AddToken(ctx context.Context, params AddTokenParams) error { + if err := sqltools.Transaction(ctx, r.db, func(ctx context.Context) error { + query := sq.Insert("access_tokens"). + Columns( + "user_id", + "token", + "refresh_token", + "token_expired_at", + "refresh_token_expired_at", + ). + Values( + params.UserId, + params.Token, + params.RefreshToken, + params.TokenExpiredAt, + params.RefreshTokenExpiredAt, + ).PlaceholderFormat(sq.Dollar) + + if _, err := query.RunWith(r.Conn(ctx)).ExecContext(ctx); err != nil { + return fmt.Errorf("error add tokens. %w", err) + } + + return nil + }); err != nil { + return err + } + + return nil +} + +func (r *repositorySQL) RefreshToken(ctx context.Context, params RefreshTokenParams) error { + if err := sqltools.Transaction(ctx, r.db, func(ctx context.Context) error { + updateQuery := sq.Update("access_tokens"). + SetMap(sq.Eq{ + "token": params.Token, + "refresh_token": params.RefreshToken, + "token_expired_at": params.TokenExpiredAt, + "refresh_token_expired_at": params.RefreshTokenExpiredAt, + }). + Where(sq.Eq{ + "user_id": params.UserId, + "token": params.OldToken, + "refresh_token": params.OldRefreshToken, + }).PlaceholderFormat(sq.Dollar) + + if _, err := updateQuery.RunWith(r.Conn(ctx)).ExecContext(ctx); err != nil { + return fmt.Errorf("error update tokens. %w", err) + } + + return nil + }); err != nil { + return err + } + + return nil +} + +func (r *repositorySQL) GetTokens(ctx context.Context, params GetTokenParams) (*AccessToken, error) { + var token *AccessToken = new(AccessToken) + + if err := sqltools.Transaction(ctx, r.db, func(ctx context.Context) error { + query := sq.Select( + "user_id", + "token", + "token_expired_at", + "refresh_token", + "refresh_token_expired_at", + "created_at", + ).From("access_tokens"). + Where(sq.Eq{ + "token": params.Token, + "user_id": params.UserId, + }).PlaceholderFormat(sq.Dollar) + + if params.RefreshToken != "" { + query = query.Where(sq.Eq{ + "refresh_token": params.RefreshToken, + }) + } + + rows, err := query.RunWith(r.Conn(ctx)).QueryContext(ctx) + if err != nil { + return fmt.Errorf("error fetch token from database. %w", err) + } + + defer func() { + if cErr := rows.Close(); cErr != nil { + err = errors.Join(fmt.Errorf("error close database rows. %w", cErr), err) + } + }() + + for rows.Next() { + if err := rows.Scan( + &token.UserId, + &token.Token, + &token.TokenExpiredAt, + &token.RefreshToken, + &token.RefreshTokenExpiredAt, + &token.CreatedAt, + ); err != nil { + return fmt.Errorf("error scan row. %w", err) + } + } + + return nil + }); err != nil { + return nil, err + } + + return token, nil +} + +func NewRepository(db *sql.DB) Repository { + return &repositorySQL{ + db: db, + } +} + +func (s *repositorySQL) Conn(ctx context.Context) sqltools.DBTX { + if tx, ok := ctx.Value(sqltools.TxCtxKey).(*sql.Tx); ok { + return tx + } + + return s.db +} diff --git a/backend/internal/usecase/repository/cache/cache.go b/backend/internal/usecase/repository/cache/cache.go new file mode 100644 index 0000000..7b5c97d --- /dev/null +++ b/backend/internal/usecase/repository/cache/cache.go @@ -0,0 +1,66 @@ +package cache + +import ( + "bytes" + "context" + "encoding/base64" + "encoding/gob" + "fmt" + "log/slog" + "time" + + "github.com/redis/go-redis/v9" +) + +type Cache interface { + // NOTE: dst MUST be a pointer + Get(ctx context.Context, key any, dst any) error + Cache(ctx context.Context, key any, val any, ttl time.Duration) error +} + +type redisCache struct { + log *slog.Logger + client *redis.Client +} + +func NewRedisCache( + log *slog.Logger, + client *redis.Client, +) Cache { + return &redisCache{ + log: log, + client: client, + } +} + +func (c *redisCache) Get(ctx context.Context, key any, dst any) error { + res := c.client.Get(ctx, c.hashKeyStr(key)) + + if res.Err() != nil { + return fmt.Errorf("error fetch data from cache. %w", res.Err()) + } + + return res.Scan(dst) +} + +func (c *redisCache) Cache(ctx context.Context, k any, v any, ttl time.Duration) error { + res := c.client.Set(ctx, c.hashKeyStr(k), v, ttl) + + if res.Err() != nil { + return fmt.Errorf("error add record to cache. %w", res.Err()) + } + + return nil +} + +func (c *redisCache) hashKey(k any) []byte { + var b bytes.Buffer + + gob.NewEncoder(&b).Encode(k) + + return b.Bytes() +} + +func (c *redisCache) hashKeyStr(k any) string { + return base64.StdEncoding.EncodeToString(c.hashKey(k)) +} diff --git a/backend/internal/usecase/repository/db.go b/backend/internal/usecase/repository/db.go new file mode 100644 index 0000000..b56b393 --- /dev/null +++ b/backend/internal/usecase/repository/db.go @@ -0,0 +1,33 @@ +package repository + +import ( + "database/sql" + "fmt" + + "github.com/emochka2007/block-accounting/internal/pkg/config" + + _ "github.com/lib/pq" +) + +func ProvideDatabaseConnection(c config.Config) (*sql.DB, func(), error) { + sslmode := "disable" + if c.DB.EnableSSL { + sslmode = "enable" + } + + connStr := fmt.Sprintf( + "postgresql://%s:%s@%s/%s?sslmode=%s", + c.DB.User, c.DB.Secret, c.DB.Host, c.DB.Database, sslmode, + ) + + fmt.Println(connStr) + + db, err := sql.Open("postgres", connStr) + if err != nil { + return nil, func() {}, fmt.Errorf("error connecting to database: %w", err) + } + + return db, func() { + db.Close() + }, nil +} diff --git a/backend/internal/usecase/repository/organizations/repository.go b/backend/internal/usecase/repository/organizations/repository.go new file mode 100644 index 0000000..70f53ea --- /dev/null +++ b/backend/internal/usecase/repository/organizations/repository.go @@ -0,0 +1,652 @@ +package organizations + +import ( + "context" + "database/sql" + "errors" + "fmt" + "time" + + sq "github.com/Masterminds/squirrel" + "github.com/emochka2007/block-accounting/internal/pkg/models" + sqltools "github.com/emochka2007/block-accounting/internal/pkg/sqlutils" + "github.com/emochka2007/block-accounting/internal/usecase/repository/users" + "github.com/google/uuid" + "golang.org/x/sync/errgroup" +) + +var ( + ErrorNotFound = errors.New("not found") +) + +type GetParams struct { + Ids uuid.UUIDs + UserId uuid.UUID + + OffsetDate time.Time + CursorId uuid.UUID + Limit int64 +} + +type ParticipantsParams struct { + OrganizationId uuid.UUID + Ids uuid.UUIDs + + // Filters + UsersOnly bool + ActiveOnly bool + EmployeesOnly bool +} + +type AddParticipantParams struct { + OrganizationId uuid.UUID + UserId uuid.UUID + EmployeeId uuid.UUID + IsAdmin bool + IsOwner bool +} + +type DeleteParticipantParams struct { + OrganizationId uuid.UUID + UserId uuid.UUID + EmployeeId uuid.UUID +} + +type Repository interface { + Create(ctx context.Context, org models.Organization) error + Get(ctx context.Context, params GetParams) ([]*models.Organization, error) + Update(ctx context.Context, org models.Organization) error + Delete(ctx context.Context, id uuid.UUID) error + AddParticipant(ctx context.Context, params AddParticipantParams) error + Participants(ctx context.Context, params ParticipantsParams) ([]models.OrganizationParticipant, error) + CreateAndAdd(ctx context.Context, org models.Organization, user *models.User) error + DeleteParticipant(ctx context.Context, params DeleteParticipantParams) error +} + +type repositorySQL struct { + db *sql.DB + usersRepository users.Repository +} + +func NewRepository( + db *sql.DB, + usersRepository users.Repository, +) Repository { + return &repositorySQL{ + db: db, + usersRepository: usersRepository, + } +} + +func (s *repositorySQL) Conn(ctx context.Context) sqltools.DBTX { + if tx, ok := ctx.Value(sqltools.TxCtxKey).(*sql.Tx); ok { + return tx + } + + return s.db +} + +func (r *repositorySQL) Create(ctx context.Context, org models.Organization) error { + if err := sqltools.Transaction(ctx, r.db, func(ctx context.Context) (err error) { + query := sq.Insert("organizations").Columns( + "id", + "name", + "address", + "wallet_seed", + "created_at", + "updated_at", + ).Values( + org.ID, + org.Name, + org.Address, + org.WalletSeed, + org.CreatedAt, + org.UpdatedAt, + ).PlaceholderFormat(sq.Dollar) + + if _, err := query.RunWith(r.Conn(ctx)).ExecContext(ctx); err != nil { + return fmt.Errorf("error insert new organization. %w", err) + } + + return nil + }); err != nil { + return err + } + + return nil +} + +func (r *repositorySQL) Get(ctx context.Context, params GetParams) ([]*models.Organization, error) { + organizations := make([]*models.Organization, 0, params.Limit) + + if err := sqltools.Transaction(ctx, r.db, func(ctx context.Context) (err error) { + query := sq.Select( + "o.id", + "o.name", + "o.address", + "o.wallet_seed", + "o.created_at", + "o.updated_at", + ).From("organizations as o"). + Limit(uint64(params.Limit)). + PlaceholderFormat(sq.Dollar) + + if params.UserId != uuid.Nil { + query = query.InnerJoin("organizations_users as ou on o.id = ou.organization_id"). + Where(sq.Eq{ + "ou.user_id": params.UserId, + }) + } + + if params.CursorId != uuid.Nil { + query = query.Where(sq.Gt{ + "o.id": params.CursorId, + }) + } + + if params.Ids != nil { + query = query.Where(sq.Eq{ + "o.id": params.Ids, + }) + } + + if !params.OffsetDate.IsZero() { + query = query.Where(sq.GtOrEq{ + "o.updated_at": params.OffsetDate, + }) + } + + rows, err := query.RunWith(r.Conn(ctx)).QueryContext(ctx) + if err != nil { + return fmt.Errorf("error fetch organizations from database. %w", err) + } + + defer func() { + if closeErr := rows.Close(); closeErr != nil { + err = errors.Join(fmt.Errorf("error close rows. %w", closeErr), err) + } + }() + + for rows.Next() { + var ( + id uuid.UUID + name string + address string + walletSeed []byte + createdAt time.Time + updatedAt time.Time + ) + + if err = rows.Scan( + &id, + &name, + &address, + &walletSeed, + &createdAt, + &updatedAt, + ); err != nil { + return fmt.Errorf("error scan row. %w", err) + } + + organizations = append(organizations, &models.Organization{ + ID: id, + Name: name, + Address: address, + WalletSeed: walletSeed, + CreatedAt: createdAt, + UpdatedAt: updatedAt, + }) + } + + return nil + }); err != nil { + return nil, err + } + + return organizations, nil +} + +func (r *repositorySQL) Update(ctx context.Context, org models.Organization) error { + if err := sqltools.Transaction(ctx, r.db, func(ctx context.Context) (err error) { + query := sq.Update("organizations as o"). + SetMap(sq.Eq{ + "o.name": org.Name, + "o.address": org.Address, + "o.wallet_seed": org.WalletSeed, + "o.created_at": org.CreatedAt, + "o.updated_at": org.UpdatedAt, + }). + Where(sq.Eq{ + "o.id": org.ID, + }). + PlaceholderFormat(sq.Dollar) + + if _, err := query.RunWith(r.Conn(ctx)).ExecContext(ctx); err != nil { + return fmt.Errorf("error update organization. %w", err) + } + + return nil + }); err != nil { + return err + } + + return nil +} + +func (r *repositorySQL) Delete(ctx context.Context, id uuid.UUID) error { + if err := sqltools.Transaction(ctx, r.db, func(ctx context.Context) (err error) { + query := sq.Delete("organizations as o"). + Where(sq.Eq{ + "o.id": id, + }). + PlaceholderFormat(sq.Dollar) + + if _, err := query.RunWith(r.Conn(ctx)).ExecContext(ctx); err != nil { + return fmt.Errorf("error delete organization. %w", err) + } + + return nil + }); err != nil { + return err + } + + return nil +} + +func (r *repositorySQL) CreateAndAdd(ctx context.Context, org models.Organization, user *models.User) error { + if err := sqltools.Transaction(ctx, r.db, func(ctx context.Context) (err error) { + if err := r.Create(ctx, org); err != nil { + return fmt.Errorf("error create organization. %w", err) + } + + if err := r.AddParticipant(ctx, AddParticipantParams{ + OrganizationId: org.ID, + UserId: user.Id(), + IsAdmin: true, + IsOwner: true, + }); err != nil { + return fmt.Errorf("error add user to newly created organization. %w", err) + } + + return nil + }); err != nil { + return err + } + + return nil +} + +func (r *repositorySQL) AddParticipant(ctx context.Context, params AddParticipantParams) error { + if err := sqltools.Transaction(ctx, r.db, func(ctx context.Context) (err error) { + query := sq.Insert("organizations_users"). + Columns( + "organization_id", + "user_id", + "employee_id", + "added_at", + "updated_at", + "is_admin", + "is_owner", + ). + Values( + params.OrganizationId, + params.UserId, + params.EmployeeId, + time.Now(), + time.Now(), + params.IsAdmin, + params.IsOwner, + ). + PlaceholderFormat(sq.Dollar) + + if _, err := query.RunWith(r.Conn(ctx)).ExecContext(ctx); err != nil { + return fmt.Errorf("error add new participant to organization. %w", err) + } + + return nil + }); err != nil { + return err + } + + return nil +} + +func (r *repositorySQL) DeleteParticipant(ctx context.Context, params DeleteParticipantParams) error { + if err := sqltools.Transaction(ctx, r.db, func(ctx context.Context) (err error) { + deletedAt := time.Now() + + query := sq.Update("organizations_users as ou"). + SetMap(sq.Eq{ + "updated_at": deletedAt, + "deleted_at": deletedAt, + "is_admin": false, + }). + Where(sq.Eq{ + "ou.organization_id": params.OrganizationId, + }). + PlaceholderFormat(sq.Dollar) + + if params.EmployeeId != uuid.Nil { + query = query.Where(sq.Eq{ + "ou.employee_id": params.EmployeeId, + }) + } + + if params.UserId != uuid.Nil { + query = query.Where(sq.Eq{ + "ou.user_id": params.UserId, + }) + } + + if _, err := query.RunWith(r.Conn(ctx)).ExecContext(ctx); err != nil { + return fmt.Errorf("error delete participant from organization. %w", err) + } + + return nil + }); err != nil { + return err + } + + return nil +} + +func (r *repositorySQL) Participants( + ctx context.Context, + params ParticipantsParams, +) ([]models.OrganizationParticipant, error) { + participants := make([]models.OrganizationParticipant, 0, len(params.Ids)) + + if err := sqltools.Transaction(ctx, r.db, func(ctx context.Context) (err error) { + orgUsersModels, err := r.fetchOrganizationUsers(ctx, params) + if err != nil { + return fmt.Errorf("error fetch organization users raw models. %w", err) + } + + eg, egCtx := errgroup.WithContext(ctx) + + var employees []*models.Employee = make([]*models.Employee, 0, len(orgUsersModels)) + if !params.UsersOnly { + eg.Go(func() error { + ids := make(uuid.UUIDs, 0, len(orgUsersModels)) + + for _, m := range orgUsersModels { + if m.employeeID != uuid.Nil { + ids = append(ids, m.employeeID) + } + } + + employees, err = r.fetchEmployees(egCtx, fetchEmployeesParams{ + IDs: ids, + OrganizationId: params.OrganizationId, + }) + if err != nil { + return fmt.Errorf("error fetch employees. %w", err) + } + + return nil + }) + } + + var usrs []*models.User + if !params.EmployeesOnly { + eg.Go(func() error { + ids := make(uuid.UUIDs, 0, len(orgUsersModels)) + + for _, m := range orgUsersModels { + if m.userID != uuid.Nil { + ids = append(ids, m.userID) + } + } + + usrs, err = r.usersRepository.Get(egCtx, users.GetParams{ + Ids: ids, + }) + if err != nil { + return fmt.Errorf("error fetch users by ids. %w", err) + } + + return nil + }) + } + + if err = eg.Wait(); err != nil { + return fmt.Errorf("error organizations users entitiels. %w", err) + } + + for _, ou := range orgUsersModels { + var employee *models.Employee + + if ou.employeeID != uuid.Nil { + for _, e := range employees { + if e.ID == ou.employeeID { + employee = e + + break + } + } + } + + if ou.userID == uuid.Nil && employee != nil { + participants = append(participants, employee) + } + + for _, u := range usrs { + if u.Id() == ou.userID { + participants = append(participants, &models.OrganizationUser{ + User: *u, + OrgPosition: ou.position, + Admin: ou.isAdmin, + Employee: employee, + }) + + break + } + } + } + + return nil + }); err != nil { + return nil, err + } + + if len(participants) == 0 { + return nil, ErrorNotFound + } + + return participants, nil +} + +type fetchOrganizationUsersModel struct { + organizationID uuid.UUID + userID uuid.UUID + employeeID uuid.UUID + position string + addedAt time.Time + updatedAt time.Time + deletedAt time.Time + isAdmin bool + isOwner bool +} + +func (r *repositorySQL) fetchOrganizationUsers( + ctx context.Context, + params ParticipantsParams, +) ([]fetchOrganizationUsersModel, error) { + participants := make([]fetchOrganizationUsersModel, 0, len(params.Ids)) + + if err := sqltools.Transaction(ctx, r.db, func(ctx context.Context) (err error) { + ouQuery := sq.Select( + "ou.organization_id", + "ou.user_id", + "ou.employee_id", + "ou.position", + "ou.added_at", + "ou.updated_at", + "ou.deleted_at", + "ou.is_admin", + "ou.is_owner", + ).Where(sq.Eq{ + "ou.organization_id": params.OrganizationId, + }).From("organizations_users as ou"). + PlaceholderFormat(sq.Dollar) + + if len(params.Ids) > 0 { + ouQuery = ouQuery.Where(sq.Eq{ + "ou.user_id": params.Ids, + }) + } + + fmt.Println(ouQuery.ToSql()) + + rows, err := ouQuery.RunWith(r.Conn(ctx)).QueryContext(ctx) + if err != nil { + return fmt.Errorf("error fetch organization participants. %w", err) + } + + defer func() { + if closeErr := rows.Close(); closeErr != nil { + err = errors.Join(fmt.Errorf("error close rows. %w", closeErr), err) + } + }() + + for rows.Next() { + var ( + organizationID uuid.UUID + userID uuid.UUID + employeeID uuid.UUID + position sql.NullString + addedAt time.Time + updatedAt time.Time + deletedAt sql.NullTime + isAdmin bool + isOwner bool + ) + + if err = rows.Scan( + &organizationID, + &userID, + &employeeID, + &position, + &addedAt, + &updatedAt, + &deletedAt, + &isAdmin, + &isOwner, + ); err != nil { + return fmt.Errorf("error scan row. %w", err) + } + + if params.EmployeesOnly && employeeID == uuid.Nil { + continue + } + + if params.UsersOnly && userID == uuid.Nil { + continue + } + + if params.ActiveOnly && deletedAt.Valid { + continue + } + + participants = append(participants, fetchOrganizationUsersModel{ + organizationID: organizationID, + userID: userID, + employeeID: employeeID, + position: position.String, + addedAt: addedAt, + updatedAt: updatedAt, + deletedAt: deletedAt.Time, + isAdmin: isAdmin, + isOwner: isAdmin, + }) + } + + return err + }); err != nil { + return nil, err + } + + return participants, nil +} + +type fetchEmployeesParams struct { + IDs uuid.UUIDs + OrganizationId uuid.UUID +} + +func (r *repositorySQL) fetchEmployees( + ctx context.Context, + params fetchEmployeesParams, +) ([]*models.Employee, error) { + employees := make([]*models.Employee, 0, len(params.IDs)) + + if err := sqltools.Transaction(ctx, r.db, func(ctx context.Context) (err error) { + query := sq.Select( + "e.id", + "e.user_id", + "e.organization_id", + "e.wallet_address", + "e.created_at", + "e.updated_at", + ).Where(sq.Eq{ + "e.organization_id": params.OrganizationId, + }).From("employees as e"). + PlaceholderFormat(sq.Dollar) + + if len(params.IDs) > 0 { + query = query.Where(sq.Eq{ + "e.id": params.IDs, + }) + } + + fmt.Println(query.ToSql()) + + rows, err := query.RunWith(r.Conn(ctx)).QueryContext(ctx) + if err != nil { + return fmt.Errorf("error fetch employees from database. %w", err) + } + + defer func() { + if closeErr := rows.Close(); closeErr != nil { + err = errors.Join(fmt.Errorf("error close rows. %w", closeErr), err) + } + }() + + for rows.Next() { + var ( + id uuid.UUID + userID uuid.UUID + orgID uuid.UUID + walletAddr []byte + createdAt time.Time + updatedAt time.Time + ) + + if err = rows.Scan( + &id, + &userID, + &orgID, + &walletAddr, + &createdAt, + &updatedAt, + ); err != nil { + return fmt.Errorf("error scan row. %w", err) + } + + employees = append(employees, &models.Employee{ + ID: id, + UserID: userID, + OrganizationId: orgID, + WalletAddress: walletAddr, + CreatedAt: createdAt, + UpdatedAt: updatedAt, + }) + } + + return nil + }); err != nil { + return nil, err + } + + return employees, nil +} diff --git a/backend/internal/usecase/repository/transactions/repository.go b/backend/internal/usecase/repository/transactions/repository.go new file mode 100644 index 0000000..45a4ba1 --- /dev/null +++ b/backend/internal/usecase/repository/transactions/repository.go @@ -0,0 +1,402 @@ +package transactions + +import ( + "context" + "database/sql" + "errors" + "fmt" + "time" + + sq "github.com/Masterminds/squirrel" + "github.com/emochka2007/block-accounting/internal/pkg/models" + sqltools "github.com/emochka2007/block-accounting/internal/pkg/sqlutils" + "github.com/google/uuid" +) + +type GetTransactionsParams struct { + Ids uuid.UUIDs + OrganizationId uuid.UUID + CreatedById uuid.UUID + To []byte + Limit int64 + CursorId uuid.UUID + + WithCancelled bool + WithConfirmed bool + WithCommited bool + WithExpired bool + WithPending bool + + WithConfirmations bool +} + +type ConfirmTransactionParams struct { + TxId uuid.UUID + UserId uuid.UUID + OrganizationId uuid.UUID +} + +type CancelTransactionParams struct { + TxId uuid.UUID + UserId uuid.UUID + OrganizationId uuid.UUID +} + +type Repository interface { + GetTransactions(ctx context.Context, params GetTransactionsParams) ([]*models.Transaction, error) + CreateTransaction(ctx context.Context, tx models.Transaction) error + UpdateTransaction(ctx context.Context, tx models.Transaction) error + DeleteTransaction(ctx context.Context, tx models.Transaction) error + + ConfirmTransaction(ctx context.Context, params ConfirmTransactionParams) error + CancelTransaction(ctx context.Context, params CancelTransactionParams) error +} + +type repositorySQL struct { + db *sql.DB +} + +func NewRepository(db *sql.DB) Repository { + return &repositorySQL{ + db: db, + } +} + +func (s *repositorySQL) Conn(ctx context.Context) sqltools.DBTX { + if tx, ok := ctx.Value(sqltools.TxCtxKey).(*sql.Tx); ok { + return tx + } + + return s.db +} + +func (r *repositorySQL) GetTransactions( + ctx context.Context, + params GetTransactionsParams, +) ([]*models.Transaction, error) { + var txs []*models.Transaction = make([]*models.Transaction, 0, len(params.Ids)) + err := sqltools.Transaction(ctx, r.db, func(ctx context.Context) (err error) { + query := buildGetTransactionsQuery(params) + + rows, err := query.RunWith(r.Conn(ctx)).QueryContext(ctx) + if err != nil { + return fmt.Errorf("error fetch transactions data from database. %w", err) + } + + defer func() { + if cErr := rows.Close(); cErr != nil { + err = errors.Join(fmt.Errorf("error close database rows. %w", cErr), err) + } + }() + + fmt.Println(query.ToSql()) + + for rows.Next() { + var ( + id uuid.UUID + description string + organizationId uuid.UUID + amount float64 + toAddr []byte + maxFeeAllowed float64 + deadline sql.NullTime + createdAt time.Time + updatedAt time.Time + confirmedAt sql.NullTime + cancelledAt sql.NullTime + commitedAt sql.NullTime + + createdById uuid.UUID + createdBySeed []byte + createdByCreatedAt time.Time + createdByActivatedAt sql.NullTime + createdByIsAdmin bool + ) + + if err = rows.Scan( + &id, + &description, + &organizationId, + &createdById, + &amount, + &toAddr, + &maxFeeAllowed, + &deadline, + &createdAt, + &updatedAt, + &confirmedAt, + &cancelledAt, + &commitedAt, + + &createdBySeed, + &createdByCreatedAt, + &createdByActivatedAt, + &createdByIsAdmin, + ); err != nil { + return fmt.Errorf("error scan row. %w", err) + } + + tx := &models.Transaction{ + Id: id, + Description: description, + OrganizationId: organizationId, + Amount: amount, + ToAddr: toAddr, + MaxFeeAllowed: maxFeeAllowed, + CreatedBy: &models.OrganizationUser{ + User: models.User{ + ID: createdById, + Bip39Seed: createdBySeed, + }, + }, + CreatedAt: createdAt, + UpdatedAt: updatedAt, + } + + if deadline.Valid { + tx.Deadline = deadline.Time + } + + if confirmedAt.Valid { + tx.ConfirmedAt = confirmedAt.Time + } + + if commitedAt.Valid { + tx.CommitedAt = commitedAt.Time + } + + if cancelledAt.Valid { + tx.CancelledAt = cancelledAt.Time + } + + if createdByActivatedAt.Valid { + tx.CreatedBy.Activated = true + } + + txs = append(txs, tx) + } + + return nil + }) + if err != nil { + return nil, err + } + + return txs, nil +} + +func (r *repositorySQL) CreateTransaction(ctx context.Context, tx models.Transaction) error { + if err := sqltools.Transaction(ctx, r.db, func(ctx context.Context) error { + columns := []string{ + "id", + "description", + "organization_id", + "created_by", + "amount", + "to_addr", + "max_fee_allowed", + "created_at", + "updated_at", + } + + values := []any{ + tx.Id, + tx.Description, + tx.OrganizationId, + tx.CreatedBy.ID, + tx.Amount, + tx.ToAddr, + tx.MaxFeeAllowed, + tx.CreatedAt, + tx.CreatedAt, + } + + if !tx.Deadline.IsZero() { + columns = append(columns, "deadline") + values = append(values, tx.Deadline) + } + + query := sq.Insert("transactions"). + Columns(columns...). + Values(values...). + PlaceholderFormat(sq.Dollar) + + // todo add optional insertions + + if _, err := query.RunWith(r.Conn(ctx)).ExecContext(ctx); err != nil { + return fmt.Errorf("error insert new transaction. %w", err) + } + + return nil + }); err != nil { + return err + } + + return nil +} + +func (r *repositorySQL) UpdateTransaction(ctx context.Context, tx models.Transaction) error { + return nil +} + +func (r *repositorySQL) DeleteTransaction(ctx context.Context, tx models.Transaction) error { + return nil +} + +func (r *repositorySQL) ConfirmTransaction(ctx context.Context, params ConfirmTransactionParams) error { + if err := sqltools.Transaction(ctx, r.db, func(ctx context.Context) error { + query := sq.Update("transactions"). + SetMap(sq.Eq{ + "confirmed_at": time.Now(), + "cancelled_at": nil, + }). + Where(sq.Eq{ + "id": params.TxId, + "organization_id": params.OrganizationId, + }).PlaceholderFormat(sq.Dollar) + + if _, err := query.RunWith(r.Conn(ctx)).ExecContext(ctx); err != nil { + return fmt.Errorf("error update confirmed at. %w", err) + } + + return nil + }); err != nil { + return err + } + + return nil +} + +func (r *repositorySQL) CancelTransaction(ctx context.Context, params CancelTransactionParams) error { + if err := sqltools.Transaction(ctx, r.db, func(ctx context.Context) error { + query := sq.Update("transactions"). + SetMap(sq.Eq{ + "cancelled_at": time.Now(), + "confirmed_at": nil, + }). + Where(sq.Eq{ + "id": params.TxId, + "organization_id": params.OrganizationId, + }).PlaceholderFormat(sq.Dollar) + + if _, err := query.RunWith(r.Conn(ctx)).ExecContext(ctx); err != nil { + return fmt.Errorf("error update confirmed at. %w", err) + } + + return nil + }); err != nil { + return err + } + + return nil +} + +func buildGetTransactionsQuery(params GetTransactionsParams) sq.SelectBuilder { + query := sq.Select( + `t.id, + t.description, + t.organization_id, + t.created_by, + t.amount, + t.to_addr, + t.max_fee_allowed, + t.deadline, + t.created_at, + t.updated_at, + t.confirmed_at, + t.cancelled_at, + t.commited_at, + + u.seed, + u.created_at, + u.activated_at, + ou.is_admin`, + ).From("transactions as t"). + InnerJoin("users as u on u.id = t.created_by"). + InnerJoin( + `organizations_users as ou on + u.id = ou.user_id and ou.organization_id = t.organization_id`, + ). + Where(sq.Eq{ + "t.organization_id": params.OrganizationId, + }).PlaceholderFormat(sq.Dollar) + + if len(params.Ids) > 0 { + query = query.Where(sq.Eq{ + "t.id": params.Ids, + }) + } + + if params.CreatedById != uuid.Nil { + query = query.Where(sq.Eq{ + "t.created_by": params.CreatedById, + }) + } + + if params.OrganizationId != uuid.Nil { + query = query.Where(sq.Eq{ + "t.organization_id": params.OrganizationId, + }) + } + + if params.To != nil { + query = query.Where(sq.Eq{ + "t.to_addr": params.To, + }) + } + + if params.WithExpired { + query = query.Where(sq.LtOrEq{ + "t.deadline": time.Now(), + }) + } else { + query = query.Where(sq.Or{ + sq.GtOrEq{ + "t.deadline": time.Now(), + }, + sq.Eq{ + "t.deadline": nil, + }, + }) + } + + if params.WithCancelled { + query = query.Where(sq.NotEq{ + "t.cancelled_at": nil, + }) + } + + if params.WithConfirmed { + query = query.Where(sq.NotEq{ + "t.confirmed_at": nil, + }) + } + + if params.WithCommited { + query = query.Where(sq.NotEq{ + "t.commited_at": nil, + }) + } + + if params.Limit <= 0 || params.Limit > 50 { + params.Limit = 50 + } + + if params.WithPending { + query = query.Where(sq.Eq{ + "t.cancelled_at": nil, + "t.commited_at": nil, + "t.confirmed_at": nil, + }) + } + + if params.CursorId != uuid.Nil { + query = query.Where(sq.Gt{ + "t.id": params.CursorId, + }) + } + + query = query.Limit(uint64(params.Limit)) + + return query +} diff --git a/backend/internal/usecase/repository/users/repository.go b/backend/internal/usecase/repository/users/repository.go new file mode 100644 index 0000000..d20bc04 --- /dev/null +++ b/backend/internal/usecase/repository/users/repository.go @@ -0,0 +1,218 @@ +package users + +import ( + "context" + "database/sql" + "errors" + "fmt" + "time" + + sq "github.com/Masterminds/squirrel" + "github.com/emochka2007/block-accounting/internal/pkg/models" + sqltools "github.com/emochka2007/block-accounting/internal/pkg/sqlutils" + "github.com/google/uuid" +) + +type GetParams struct { + Ids uuid.UUIDs + OrganizationId uuid.UUID + Seed []byte +} + +// todo implement +type Repository interface { + Get(ctx context.Context, params GetParams) ([]*models.User, error) + Create(ctx context.Context, user *models.User) error + Activate(ctx context.Context, id uuid.UUID) error + Update(ctx context.Context, user *models.User) error + Delete(ctx context.Context, id string) error +} + +type repositorySQL struct { + db *sql.DB +} + +func NewRepository(db *sql.DB) Repository { + return &repositorySQL{ + db: db, + } +} + +func (s *repositorySQL) Conn(ctx context.Context) sqltools.DBTX { + if tx, ok := ctx.Value(sqltools.TxCtxKey).(*sql.Tx); ok { + return tx + } + + return s.db +} + +func (r *repositorySQL) Get(ctx context.Context, params GetParams) ([]*models.User, error) { + var users []*models.User = make([]*models.User, 0, len(params.Ids)) + + if err := sqltools.Transaction(ctx, r.db, func(ctx context.Context) (err error) { + query := sq.Select("u.id, u.name, u.email, u.phone, u.tg, u.seed, u.created_at, u.activated_at"). + From("users as u"). + PlaceholderFormat(sq.Dollar) + + if len(params.Ids) > 0 { + query = query.Where(sq.Eq{ + "u.id": params.Ids, + }) + } + + if params.OrganizationId != uuid.Nil { + query = query.InnerJoin( + "organizations_users as ou on ou.user_id = u.id", + ).Where(sq.Eq{ + "ou.organization_id": params.OrganizationId, + }) + } + + if params.Seed != nil { + query = query.Where("u.seed = ?", params.Seed) + } + + fmt.Println(query.ToSql()) + + rows, err := query.RunWith(r.Conn(ctx)).QueryContext(ctx) + if err != nil { + return fmt.Errorf("error fetch data from database. %w", err) + } + + defer func() { + if cErr := rows.Close(); cErr != nil { + err = errors.Join(fmt.Errorf("error close database rows. %w", cErr), err) + } + }() + + for rows.Next() { + var ( + id uuid.UUID + + name string + email string + phone string + tg string + + seed []byte + //isAdmin bool + createdAt time.Time + activatedAt sql.NullTime + ) + + if err = rows.Scan( + &id, + &name, + &email, + &phone, + &tg, + &seed, + &createdAt, + &activatedAt, + ); err != nil { + return fmt.Errorf("error scan row. %w", err) + } + + users = append(users, &models.User{ + ID: id, + Name: name, + Credentails: &models.UserCredentials{ + Email: email, + Phone: phone, + Telegram: tg, + }, + Bip39Seed: seed, + //Admin: isAdmin, + CreatedAt: createdAt, + Activated: activatedAt.Valid, + }) + } + + return nil + }); err != nil { + return nil, err + } + + return users, nil +} + +func (r *repositorySQL) Create(ctx context.Context, user *models.User) error { + if err := sqltools.Transaction(ctx, r.db, func(ctx context.Context) error { + columns := []string{"id", "name", "email", "phone", "tg", "seed", "created_at"} + + values := []any{ + user.ID, + user.Name, + user.Credentails.Email, + user.Credentails.Phone, + user.Credentails.Telegram, + user.Bip39Seed, + user.CreatedAt, + } + + if user.Activated { + columns = append(columns, "activated_at") + values = append(values, user.CreatedAt) + } + + query := sq.Insert("users").Columns( + columns..., + ).Values( + values..., + ).PlaceholderFormat(sq.Dollar) + + if _, err := query.RunWith(r.Conn(ctx)).ExecContext(ctx); err != nil { + return fmt.Errorf("error insert new user. %w", err) + } + + return nil + }); err != nil { + return err + } + + return nil +} + +func (r *repositorySQL) Activate(ctx context.Context, id uuid.UUID) error { + if err := sqltools.Transaction(ctx, r.db, func(ctx context.Context) error { + query := sq.Update("users"). + SetMap(sq.Eq{ + "activated_at": time.Now(), + }). + Where(sq.Eq{ + "id": id, + }) + + if _, err := query.RunWith(r.Conn(ctx)).ExecContext(ctx); err != nil { + return fmt.Errorf("error mark user as activated in database. %w", err) + } + + return nil + }); err != nil { + return err + } + + return nil +} + +func (r *repositorySQL) Update(ctx context.Context, user *models.User) error { + if err := sqltools.Transaction(ctx, r.db, func(ctx context.Context) error { + + return nil + }); err != nil { + return err + } + + return nil +} + +func (r *repositorySQL) Delete(ctx context.Context, id string) error { + if err := sqltools.Transaction(ctx, r.db, func(ctx context.Context) error { + + return nil + }); err != nil { + return err + } + + return nil +} diff --git a/backend/migrations/blockd.sql b/backend/migrations/blockd.sql new file mode 100644 index 0000000..74a97a8 --- /dev/null +++ b/backend/migrations/blockd.sql @@ -0,0 +1,172 @@ +create table if not exists users ( + id uuid primary key , + name varchar(250), + email varchar(200), + phone varchar(16), + tg varchar(200), + public_key bytea not null unique, + seed bytea not null unique, + created_at timestamp default current_timestamp, + activated_at timestamp default null +); + +create index if not exists index_users_seed + on users (seed); + +create index if not exists index_users_public_key + on users (public_key); + +create index if not exists index_users_name + on users using hash (name); + +create index if not exists index_users_seed + on users using hash (seed); + +create table if not exists access_tokens ( + user_id uuid not null references users(id), + token varchar(350) not null, + token_expired_at timestamp, + refresh_token varchar(350) not null, + refresh_token_expired_at timestamp, + created_at timestamp default current_timestamp, + remote_addr varchar(100) +); + +create index if not exists index_access_tokens_token_refresh_token + on access_tokens (token, refresh_token); + +create index if not exists index_access_tokens_token_refresh_token_exp + on access_tokens (token, refresh_token, token_expired_at, refresh_token_expired_at); + +create table if not exists organizations ( + id uuid primary key unique, + name varchar(300) default 'My Organization' not null, + address varchar(750) not null, + wallet_seed bytea not null, + created_at timestamp default current_timestamp, + updated_at timestamp default current_timestamp +); + +create index if not exists index_organizations_id + on organizations (id); + +create table employees ( + id uuid primary key, + user_id uuid, + organization_id uuid not null references organizations(id), + wallet_address text not null, + created_at timestamp default current_timestamp, + updated_at timestamp default current_timestamp +); + +create index if not exists index_employees_id_organization_id + on employees (id, organization_id); + +create index if not exists index_user_id_organization_id + on employees (user_id, organization_id); + +create table organizations_users ( + organization_id uuid not null references organizations(id), + user_id uuid not null references users(id), + employee_id uuid default null, + position varchar(300), + added_at timestamp default current_timestamp, + updated_at timestamp default current_timestamp, + deleted_at timestamp default null, + is_admin bool default false, + is_owner bool default false, + primary key(organization_id, user_id) +); + +create index if not exists index_organizations_users_organization_id_user_id_is_admin + on organizations_users (organization_id, user_id, is_admin); + +create index if not exists index_organizations_users_organization_id_user_id + on organizations_users (organization_id, user_id); + +create index if not exists index_organizations_users_organization_id_employee_id + on organizations_users (organization_id, employee_id); + + +create table if not exists transactions ( + id uuid primary key, + description text default 'New Transaction', + organization_id uuid not null, + created_by uuid not null, + amount decimal default 0, + + to_addr bytea not null, + tx_index bytea default null, + + max_fee_allowed decimal default 0, + deadline timestamp default null, + status int default 0, + + created_at timestamp default current_timestamp, + updated_at timestamp default current_timestamp, + + confirmed_at timestamp default null, + cancelled_at timestamp default null, + + commited_at timestamp default null +); + +create index if not exists index_transactions_id_organization_id + on transactions (organization_id); + +create index if not exists index_transactions_id_organization_id_created_by + on transactions (organization_id, created_by); + +create index if not exists index_transactions_organization_id_deadline + on transactions (organization_id, deadline); + +create table transactions_confirmations ( + tx_id uuid not null, + user_id uuid not null, + organization_id uuid not null, + created_at timestamp default current_timestamp, + updated_at timestamp default current_timestamp, + confirmed bool +); + +create index if not exists index_transactions_confirmations_tx_id_user_id_organization_id + on transactions_confirmations (tx_id, user_id, organization_id); + +create table contracts ( + id uuid primary key, + title varchar(250) default 'New Contract', + description text not null, + + address bytea not null, + + payload bytea not null, + + created_by uuid not null references users(id), + organization_id uuid not null references organizations(id), + + status smallint default 0, + tx_index bytea default null, + + created_at timestamp default current_timestamp, + updated_at timestamp default current_timestamp +); + +create table multisigs ( + id uuid primary key, + title varchar(350) default 'New Multi-Sig' +); + +create table multisig_owners ( + multisig_id uuid references multisigs(id), + owner_id uuid references organizations_users(user_id), + created_at timestamp default current_timestamp, + updated_at timestamp default current_timestamp, + primary key (multisig_id, owner_id) +); + +create table multisig_confirmations ( + multisig_id uuid references multisigs(id), + owner_id uuid references organizations_users(user_id), + created_at timestamp default current_timestamp, + primary key (multisig_id, owner_id) +); \ No newline at end of file diff --git a/backend/prometheus/prometheus.yml b/backend/prometheus/prometheus.yml new file mode 100644 index 0000000..c49af7a --- /dev/null +++ b/backend/prometheus/prometheus.yml @@ -0,0 +1,32 @@ +global: + scrape_interval: 15s # By default, scrape targets every 15 seconds. + evaluation_interval: 15s # By default, scrape targets every 15 seconds. + +# A scrape configuration containing exactly one endpoint to scrape: +# Here it's Prometheus itself. +scrape_configs: + # The job name is added as a label `job=` to any timeseries scraped from this config. + - job_name: 'prometheus' + + # Override the global default and scrape targets from this job every 5 seconds. + scrape_interval: 15s + scrape_timeout: 15s + + # metrics_path defaults to '/metrics' + # scheme defaults to 'http'. + + static_configs: + - targets: ['blockd:8080'] + + # The job name is added as a label `job=` to any timeseries scraped from this config. + - job_name: 'prometheus2' + + # Override the global default and scrape targets from this job every 5 seconds. + scrape_interval: 15s + scrape_timeout: 15s + + # metrics_path defaults to '/metrics' + # scheme defaults to 'http'. + + static_configs: + - targets: ['host.docker.internal:8082'] \ No newline at end of file